fix(web): restore v02 workbench layout

This commit is contained in:
Codex
2026-06-03 15:49:52 +08:00
parent 10906ec726
commit 2fa23bd71d
5 changed files with 94 additions and 7 deletions
+14 -2
View File
@@ -290,6 +290,7 @@ initSessionSidebar();
initCopySessionIdButton();
initCodeAgentTimeoutControl();
initConversationScrollMemory();
renderConversation();
initCommandBar();
initLiveBuildOverlay();
initWorkbenchLogout(el.logoutButton);
@@ -620,11 +621,11 @@ function refreshSessionSidebar(options = {}) {
}
function renderSessionSidebar() {
const view = sessionTabsFromConversations(sessionSidebarConversations(), {
const view = safeSessionTabsView(() => sessionTabsFromConversations(sessionSidebarConversations(), {
selectedConversationId: state.conversationId,
selectedSessionId: state.sessionId,
activeTraceId: state.activeTraceId
});
}));
replaceChildren(el.sessionTabs, ...view.tabs.map(sessionTabButton));
const pending = Boolean(state.sessionList.pendingAction);
el.sessionCreate.disabled = pending;
@@ -634,6 +635,17 @@ function renderSessionSidebar() {
el.sessionStatus.title = state.sessionList.error || state.sessionList.loadedAt || "session list not loaded";
}
function safeSessionTabsView(factory) {
try {
const view = factory();
if (Array.isArray(view?.tabs)) return view;
state.sessionList.error = "session tabs renderer returned an invalid view";
} catch (error) {
state.sessionList.error = error?.message || String(error);
}
return { tabs: [], activeKey: null, activeConversation: null };
}
function renderCopySessionIdButton(activeConversation) {
if (!el.copySessionId) return;
const conversationId = textOf(activeConversation?.conversationId) || textOf(state.conversationId);
+1 -2
View File
@@ -208,7 +208,7 @@
</tbody>
</table>
</div>
</section
</section>
<section class="view settings-view" id="settings" data-view="settings" aria-labelledby="settings-title" hidden>
<div class="workspace-panel settings-panel">
<div class="panel-title-row">
@@ -222,7 +222,6 @@
</div>
</div>
</section>
>
<form class="command-bar" id="command-form" aria-label="Agent 输入">
<label class="agent-timeout-control" for="code-agent-provider-profile">
+31
View File
@@ -46,6 +46,7 @@ console.log("hwlab-cloud-web check: dist freshness verified");
const html = readWeb("index.html");
const styles = readWeb("styles.css");
const app = readCloudWebAppSource(rootDir);
assertCloudWebHtmlStructure(html);
assertTraceHelpersBound(readWeb("app-trace.ts"));
const liveStatus = readWeb("live-status.ts");
const helpMarkdown = readWeb("help.md");
@@ -260,6 +261,16 @@ function assertIncludes(source, term, message) {
assert.ok(source.includes(term), message);
}
function assertCloudWebHtmlStructure(source) {
assertDoesNotInclude(source, "</section\n", "Cloud Web HTML must not contain an unterminated closing section tag");
assertDoesNotInclude(source, "\n>\n\n <form", "Cloud Web HTML must not contain a stray tag close before the command bar");
assertIncludes(source, " </section>\n <section class=\"view settings-view\"", "settings view must follow a complete section close");
assertIncludes(source, " </section>\n\n <form class=\"command-bar\"", "command bar must follow a complete section close");
const centerWorkspace = sectionByClass(source, "section", "center-workspace");
assertIncludes(centerWorkspace, 'id="settings" data-view="settings"', "settings view must stay inside center workspace");
assertIncludes(centerWorkspace, 'id="command-form"', "command form must stay inside center workspace");
}
function assertDoesNotInclude(source, term, message) {
assert.equal(source.includes(term), false, message);
}
@@ -313,6 +324,26 @@ function sectionById(source, tagName, id) {
return source.slice(start);
}
function sectionByClass(source, tagName, className) {
const startPattern = new RegExp(`<${tagName}\\b[^>]*\\bclass=["'][^"']*\\b${escapeRegExp(className)}\\b[^"']*["'][^>]*>`, "iu");
const startMatch = startPattern.exec(source);
if (!startMatch) return "";
const start = startMatch.index;
const tagPattern = new RegExp(`</?${tagName}\\b[^>]*>`, "giu");
tagPattern.lastIndex = start;
let depth = 0;
for (const match of source.slice(start).matchAll(tagPattern)) {
const absoluteEnd = start + match.index + match[0].length;
if (match[0].startsWith("</")) {
depth -= 1;
if (depth === 0) return source.slice(start, absoluteEnd);
} else {
depth += 1;
}
}
return source.slice(start);
}
function cssRule(source, selector) {
const match = new RegExp(`${escapeRegExp(selector)}\\s*\\{`, "u").exec(source);
if (!match) return "";
+7
View File
@@ -77,6 +77,13 @@ test("session tabs default to newest visible conversation", () => {
assert.equal(view.activeKey, "ses_b");
});
test("session tabs expose a stable empty conversation view", () => {
const view = sessionTabsFromConversations([]);
assert.deepEqual(view, { tabs: [], activeKey: null, activeConversation: null });
assert.equal(Array.isArray(view.tabs), true);
});
test("session tabs surface authoritative messageCount and fall back to message length", () => {
const explicit = sessionTabsFromConversations([
conversation("cnv_explicit", "ses_explicit", "trc_explicit", "completed", "2026-06-03T00:00:01Z", {
+41 -3
View File
@@ -2768,20 +2768,58 @@ tbody tr:last-child td {
@media (max-width: 520px) {
.workbench-shell {
--rail-width: 64px;
--session-sidebar-width: 128px;
grid-template-columns: var(--rail-width) var(--session-sidebar-width) minmax(0, 1fr);
grid-template-rows: minmax(0, 1fr) clamp(260px, 34dvh, 320px);
--session-sidebar-width: 0px;
grid-template-columns: minmax(0, 1fr);
grid-template-rows: auto minmax(0, 1fr) clamp(260px, 34dvh, 320px);
}
.activity-rail {
grid-column: 1;
grid-row: 1;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
padding: 7px;
border-right: 0;
border-bottom: 1px solid var(--line);
overflow-x: auto;
overflow-y: hidden;
}
.rail-spacer {
display: none;
}
.rail-button {
flex: 0 0 auto;
min-height: 36px;
padding: 6px 3px;
font-size: 11px;
}
.sidebar-toggle {
min-height: 36px;
}
.session-sidebar {
display: none;
}
.center-workspace {
grid-column: 1;
grid-row: 2;
}
.right-sidebar {
grid-column: 1;
grid-row: 3;
width: 100%;
padding-left: 8px;
border-left: 0;
border-top: 1px solid var(--line);
}
.topbar {
display: block;
}