Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 23 additions & 9 deletions tests/web/app-render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ const SNAPSHOT = {
id: "sa-1",
title: "explore",
status: "done",
createdAt: "2026-09-01T10:00:00Z",
settledAt: "2026-09-01T10:00:30Z",
createdAt: Date.parse("2026-09-01T10:00:00Z"),
settledAt: Date.parse("2026-09-01T10:00:30Z"),
},
],
omitted: 2,
Expand All @@ -102,8 +102,8 @@ const SNAPSHOT = {
runId: "wf-1",
name: "delivery",
status: "completed",
startedAt: "2026-09-01T10:00:00Z",
finishedAt: "2026-09-01T10:01:00Z",
startedAt: Date.parse("2026-09-01T10:00:00Z"),
finishedAt: Date.parse("2026-09-01T10:01:00Z"),
agents: { total: 1, running: 0, done: 1, error: 0, uncertain: 0 },
},
],
Expand Down Expand Up @@ -438,14 +438,28 @@ test("app.js renders a full session without runtime errors", async () => {
const conversation = elements.get("conversation");
assert.ok(conversation, "conversation element exists");
assert.match(conversation.innerHTML, /message-row user/);
assert.match(conversation.innerHTML, /assistant-detail/);
assert.match(conversation.innerHTML, /tool-details/);
assert.match(conversation.innerHTML, /runtime-activity/);
assert.match(conversation.innerHTML, /thinking-line/);
assert.match(conversation.innerHTML, /tool-line/);
// Settled tool calls carry an outcome glyph at the row's right edge.
assert.match(conversation.innerHTML, /tool-status done/);
assert.match(conversation.innerHTML, /activity-card subagent/);
assert.match(conversation.innerHTML, /activity-card workflow/);
assert.match(conversation.innerHTML, /explore/);
assert.match(conversation.innerHTML, /\+2 omitted/);
assert.match(conversation.innerHTML, /delivery/);
assert.match(conversation.innerHTML, /file1/);
assert.match(conversation.innerHTML, /最终总结/);
// Copy/time shows on user messages and each turn's final assistant answer
// (this fixture is a single turn: one user row + one final answer).
const actionBars = conversation.innerHTML.match(/message-actions/g) || [];
assert.equal(actionBars.length, 2);
const activityBar = elements.get("activity-bar");
assert.ok(activityBar, "activity bar element exists");
assert.equal(activityBar.hidden, false);
assert.match(activityBar.innerHTML, /activity-chip workflow done/);
assert.match(activityBar.innerHTML, /activity-chip subagent done/);
assert.match(activityBar.innerHTML, /delivery/);
assert.match(activityBar.innerHTML, /\+2/);
const turnRail = elements.get("turn-rail");
assert.ok(turnRail, "turn rail element exists");
});

test("app.js moves the bootstrap token into tab storage and clears the URL", async () => {
Expand Down
80 changes: 65 additions & 15 deletions tests/web/web-host.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,11 @@ test("serves workspaces through a runtime isolated from terminal sessions", asyn
assert.match(pageHtml, /id="workspace-menu"/);
assert.match(pageHtml, /Rename workspace/);
assert.match(pageHtml, /Remove from sidebar/);
assert.doesNotMatch(pageHtml, /theme-picker-trigger|data-theme-value/);
assert.doesNotMatch(pageHtml, /settings-dialog|language-picker/u);
// Settings persistence/entry is deferred to #350 (canonical config contract).
assert.doesNotMatch(
pageHtml,
/settings-dialog|open-settings|theme-picker|language-picker/,
);

const app = await fetch(`${launched.origin}/app.js`);
assert.equal(app.status, 200);
Expand Down Expand Up @@ -163,23 +166,47 @@ test("serves workspaces through a runtime isolated from terminal sessions", asyn
assert.match(appSource, /visibleUngroupedSessions/);
assert.match(appSource, /workspaceDeleteConfirm/);
assert.match(appSource, /workspace-delete-dialog/);
assert.match(appSource, /runtime-activity/);
assert.match(appSource, /message-copy/);
assert.match(appSource, /messageActionsMarkup/);
assert.match(appSource, /turn-tick/);
assert.match(appSource, /turn-rail/);
assert.match(appSource, /scrollIntoView/);
assert.match(appSource, /activity-card/);
assert.match(appSource, /familyToolCallCard/);
assert.match(appSource, /toolLineMarkup/);
assert.match(appSource, /toolCallSummary/);
assert.match(appSource, /groupRows/);
assert.match(appSource, /tool-group/);
assert.match(appSource, /thinkingLineMarkup/);
assert.match(appSource, /data-thinking-start/);
assert.match(appSource, /message-edit-input/);
assert.match(appSource, /enterMessageEdit/);
assert.match(appSource, /renderActivityBar/);
assert.match(appSource, /activity-chip/);
assert.match(appSource, /runtime_changed/);
assert.match(appSource, /resultsByCallId/);
assert.match(appSource, /pinnedToBottom/);
assert.match(appSource, /behavior: "instant"/);
assert.match(appSource, /customType/);
assert.match(appSource, /subagent-result/);
assert.match(appSource, /workflow-result/);
assert.match(appSource, /sessionStorage\.setItem/);
assert.match(appSource, /history\.replaceState/);
assert.match(appSource, /\/events\?cursor=/);
assert.doesNotMatch(appSource, /localStorage|openpi\.archived-sessions/);
assert.doesNotMatch(
appSource,
/applyTheme|message-edit-input|enterMessageEdit/,
);
assert.doesNotMatch(appSource, /language-picker|open-settings/u);
assert.doesNotMatch(appSource, /openpi\.archived-sessions/);
assert.doesNotMatch(appSource, /applyTheme|data-theme-value|open-settings/);
assert.doesNotMatch(appSource, /openpi\.language|openpi\.web\.theme/);

const marked = await fetch(`${launched.origin}/marked.js`);
assert.equal(marked.status, 200);
assert.match(marked.headers.get("content-type") || "", /javascript/);
assert.match(await marked.text(), /marked v18/);

const favicon = await fetch(`${launched.origin}/favicon.svg`);
assert.equal(favicon.status, 200);
assert.match(favicon.headers.get("content-type") || "", /image\/svg\+xml/);
assert.match(await favicon.text(), /<svg/);

const styles = await fetch(`${launched.origin}/styles.css`);
assert.equal(styles.status, 200);
const stylesSource = await styles.text();
Expand All @@ -202,7 +229,7 @@ test("serves workspaces through a runtime isolated from terminal sessions", asyn
);
assert.match(
stylesSource,
/\.workspace-picker-row \{[^}]*width: min\(780px, 100%\)/,
/\.workspace-picker-row \{[^}]*width: min\(840px, 100%\)/,
);
assert.match(
stylesSource,
Expand All @@ -219,27 +246,50 @@ test("serves workspaces through a runtime isolated from terminal sessions", asyn
assert.match(stylesSource, /\.composer-hint \{ display: none; \}/);
assert.match(
stylesSource,
/\.message-row\.assistant \{ width: min\(780px, calc\(100% - 48px\)\); \}/,
/\.message-row\.assistant \{ width: min\(840px, calc\(100% - 48px\)\); padding: 6px 0; \}/,
);
assert.match(
stylesSource,
/\.message-row\.assistant \.message-content \{ width: 100%; max-width: none; \}/,
);
assert.match(
stylesSource,
/\.message-row\.assistant \.message-details \{ width: min\(760px, calc\(100% - 4px\)\); margin-right: auto; margin-left: auto; \}/,
/\.message-row\.assistant \.message-details \{ width: min\(820px, calc\(100% - 4px\)\); margin-right: auto; margin-left: auto; \}/,
);
assert.match(
stylesSource,
/\.message-row\.detail-only \{ padding: 12px 0; \}/,
/\.message-row\.detail-only \{ padding: 6px 0; \}/,
);
assert.match(
stylesSource,
/\.message-row\.detail-only \.message-details \{ margin: 0 auto; \}/,
);
assert.match(stylesSource, /\.workspace-delete-dialog/);
assert.match(stylesSource, /\.runtime-activity \{/);
assert.doesNotMatch(stylesSource, /html\[data-theme=/);
assert.match(stylesSource, /\.message-actions \{/);
assert.match(stylesSource, /\.turn-rail \{/);
assert.match(stylesSource, /\.turn-tick-label \{/);
assert.match(stylesSource, /\.activity-card \{/);
assert.match(stylesSource, /\.activity-chip \{/);
assert.match(stylesSource, /\.activity-bar \{/);
assert.match(stylesSource, /\.tool-icon \{/);
assert.match(stylesSource, /\.tool-line\.error/);
assert.match(stylesSource, /\.tool-group \{/);
assert.match(stylesSource, /\.thinking-line/);
assert.doesNotMatch(
stylesSource,
/html\[data-theme=|settings-dialog|language-picker/,
);
assert.match(stylesSource, /@keyframes pixel-nudge/);
// Narrow-screen regression: the off-canvas sidebar must not keep a
// backdrop-filter, or Chromium blurs the whole page backdrop (#352 P1).
assert.match(
stylesSource,
/@media \(max-width: 760px\) \{[\s\S]*?\.session-sidebar \{[^}]*backdrop-filter: none;/,
);
assert.match(
stylesSource,
/@media \(max-width: 760px\) \{[\s\S]*?\.session-sidebar::before \{ display: none; \}/,
);

const unauthorized = await fetch(`${launched.origin}/api/snapshot`);
assert.equal(unauthorized.status, 401);
Expand Down
7 changes: 5 additions & 2 deletions web/host/web-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,8 @@ export class WebHost {
if (
url.pathname === "/styles.css" ||
url.pathname === "/app.js" ||
url.pathname === "/marked.js"
url.pathname === "/marked.js" ||
url.pathname === "/favicon.svg"
) {
if (request.method !== "GET")
return this.json(response, 405, {
Expand All @@ -329,7 +330,9 @@ export class WebHost {
response.writeHead(200, {
"Content-Type": file.endsWith(".css")
? "text/css; charset=utf-8"
: "text/javascript; charset=utf-8",
: file.endsWith(".svg")
? "image/svg+xml; charset=utf-8"
: "text/javascript; charset=utf-8",
"Cache-Control": "no-store",
});
response.end(body);
Expand Down
Loading
Loading