fix(web): restore v02 workbench layout
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 "";
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user