@firstpick/pi-package-webui 0.3.8 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -7
- package/bin/pi-webui.mjs +716 -37
- package/package.json +11 -22
- package/public/app.js +1839 -67
- package/public/index.html +41 -3
- package/public/service-worker.js +1 -1
- package/public/styles.css +415 -0
- package/tests/fixtures/fake-pi.mjs +10 -1
- package/tests/http-endpoints-harness.test.mjs +99 -2
- package/tests/mobile-static.test.mjs +80 -23
|
@@ -24,16 +24,16 @@ const [pkgRaw, html, css, app, server, extension, readme, startScript, manifestR
|
|
|
24
24
|
const pkg = JSON.parse(pkgRaw);
|
|
25
25
|
const manifest = JSON.parse(manifestRaw);
|
|
26
26
|
const companionDependencies = {
|
|
27
|
-
"@firstpick/pi-extension-git-footer-status": "^0.
|
|
28
|
-
"@firstpick/pi-extension-release-aur": "^0.1.
|
|
29
|
-
"@firstpick/pi-extension-release-npm": "^0.
|
|
27
|
+
"@firstpick/pi-extension-git-footer-status": "^0.3.3",
|
|
28
|
+
"@firstpick/pi-extension-release-aur": "^0.1.6",
|
|
29
|
+
"@firstpick/pi-extension-release-npm": "^0.4.0",
|
|
30
30
|
"@firstpick/pi-extension-safety-guard": "^0.2.3",
|
|
31
|
-
"@firstpick/pi-extension-setup-skills": "^0.1.
|
|
32
|
-
"@firstpick/pi-extension-stats": "^0.2.
|
|
33
|
-
"@firstpick/pi-extension-todo-progress": "^0.
|
|
34
|
-
"@firstpick/pi-extension-tools": "^0.1.
|
|
35
|
-
"@firstpick/pi-prompts-git-pr": "^0.1.
|
|
36
|
-
"@firstpick/pi-themes-bundle": "^0.1.
|
|
31
|
+
"@firstpick/pi-extension-setup-skills": "^0.1.8",
|
|
32
|
+
"@firstpick/pi-extension-stats": "^0.2.6",
|
|
33
|
+
"@firstpick/pi-extension-todo-progress": "^0.2.4",
|
|
34
|
+
"@firstpick/pi-extension-tools": "^0.1.6",
|
|
35
|
+
"@firstpick/pi-prompts-git-pr": "^0.1.2",
|
|
36
|
+
"@firstpick/pi-themes-bundle": "^0.1.4",
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
assert.match(html, /viewport-fit=cover/, "viewport should opt into safe-area-aware full-screen layout");
|
|
@@ -421,6 +421,10 @@ assert.match(app, /api\("\/api\/shutdown", \{ method: "POST", scoped: false \}\)
|
|
|
421
421
|
assert.match(server, /url\.pathname === "\/api\/restart" && req\.method === "POST"/, "server should expose restart endpoint");
|
|
422
422
|
assert.match(server, /PI_WEBUI_RESTORE_TABS: JSON\.stringify\(restorableTabs \|\| \[\]\)/, "server restart should preserve restorable tab metadata");
|
|
423
423
|
assert.match(server, /if \(webuiDevServer\) env\.PI_WEBUI_DEV = "1";/, "server restart should explicitly preserve dev mode");
|
|
424
|
+
assert.match(server, /async function resolveUpdateTasks\(\)[\s\S]*currentWebuiPackageUpdateTask\(\)[\s\S]*agentPackageRootUpdateTask\(\)[\s\S]*npmGlobalPackageRootUpdateTask\(\)[\s\S]*bunGlobalPackageRootUpdateTask\(\)/, "server update should include current, Pi-agent, npm-global, and Bun-global Web UI/Pi package roots");
|
|
425
|
+
assert.match(server, /function packageInstallSpecs\(packageNames\)[\s\S]*`\$\{packageName\}@latest`/, "server package update tasks should force latest Web UI/Pi package specs instead of staying inside stale semver ranges");
|
|
426
|
+
assert.match(app, /Run Pi\/Web UI package updates now\?/, "frontend update confirmation should describe the broader package update set");
|
|
427
|
+
assert.match(readme, /detected local\/global Web UI and Pi package-manager updates/, "README should document that update refreshes local and global Web UI\/Pi package roots");
|
|
424
428
|
assert.match(server, /async function closeNetworkAccess\(\)/, "server should expose a local-only rebind helper for closing network access");
|
|
425
429
|
assert.match(server, /url\.pathname === "\/api\/network\/close" && req\.method === "POST"/, "server should route network close requests");
|
|
426
430
|
assert.match(server, /server\.closeAllConnections\?\.\(\)/, "network rebind should force-close long-lived clients so close-to-localhost can complete");
|
|
@@ -490,6 +494,7 @@ assert.match(
|
|
|
490
494
|
"side-panel section toggles should expand at most one section at a time",
|
|
491
495
|
);
|
|
492
496
|
assert.match(app, /function renderCodexUsage\(\)/, "frontend should render Codex usage buckets in the side panel");
|
|
497
|
+
assert.match(app, /if \(normalized === "prolite"\) return "Usage";/, "Codex Prolite plan labels should display as Usage in the side panel");
|
|
493
498
|
assert.match(app, /api\(`\/api\/codex-usage\$\{suffix\}`, \{ scoped: false \}\)/, "Codex usage should load through a server endpoint without browser credentials");
|
|
494
499
|
assert.match(app, /restoreSidePanelSectionState\(\);\nbindSidePanelSectionToggles\(\);/, "side panel section state should restore before toggles are bound");
|
|
495
500
|
assert.match(app, /OPTIONAL_FEATURES_STORAGE_KEY/, "optional feature disable toggles should persist in browser storage");
|
|
@@ -547,7 +552,7 @@ assert.match(app, /function setOptionalFeatureDisabled\(featureId, disabled\)[\s
|
|
|
547
552
|
const workspaceInfoSource = server.match(/async function getWorkspaceInfo[\s\S]*?\n}\n\nlet activeGitWorkflowProcess/)?.[0] || "";
|
|
548
553
|
assert.ok(workspaceInfoSource, "server workspace info source should be inspectable");
|
|
549
554
|
assert.doesNotMatch(workspaceInfoSource, /runCommand\("git"|branchStatus|isRepo/, "Web UI workspace endpoint should not duplicate git footer status collection");
|
|
550
|
-
assert.match(app, /function renderOptionalFeatureDependentDisplays\(\)[\s\S]*renderOptionalFeatureControls\(\);[\s\S]*renderThemeSelect\(\);[\s\S]*renderWidgets\(\);[\s\S]*renderStatus\(\);[\s\S]*renderCommands\(\);[\s\S]*renderAllMessages\(\{ preserveScroll: true \}\);[\s\S]*if \(streamRawText\) renderStreamingAssistantText\(\);/, "optional feature toggles should immediately refresh visible controls, commands, transcript, and live stream displays");
|
|
555
|
+
assert.match(app, /function renderOptionalFeatureDependentDisplays\(\)[\s\S]*renderOptionalFeatureControls\(\);[\s\S]*renderThemeSelect\(\);[\s\S]*renderWidgets\(\);[\s\S]*renderStatus\(\);[\s\S]*renderCommands\(\);[\s\S]*renderAllMessages\(\{ preserveScroll: true, forceRebuild: true \}\);[\s\S]*if \(streamRawText\) renderStreamingAssistantText\(\);/, "optional feature toggles should immediately refresh visible controls, commands, transcript, and live stream displays");
|
|
551
556
|
assert.match(app, /function setOptionalFeatureDisabled\(featureId, disabled\)[\s\S]*renderOptionalFeatureDependentDisplays\(\);[\s\S]*const tabContext = activeTabContext\(\);[\s\S]*refreshCommands\(tabContext\)/, "optional feature enable/disable should re-render the GUI and then refresh command capabilities");
|
|
552
557
|
assert.match(app, /function setOptionalControlState\(button, available, unavailableTitle\)[\s\S]*setAttribute\("aria-label", nextAriaLabel\)[\s\S]*setAttribute\("data-tooltip", nextTooltip\)/, "optional feature button disabled state should update accessible labels and visible tooltips");
|
|
553
558
|
assert.match(app, /const hasGitWorkflow = isOptionalFeatureEnabled\("gitWorkflow"\);\n\s+elements\.gitWorkflowButton\.hidden = !hasGitWorkflow/, "guided git workflow composer button should be hidden when unavailable or disabled");
|
|
@@ -588,6 +593,13 @@ assert.match(app, /addGitWorkflowAction\("Create PR", \(\) => createGitPrBranch\
|
|
|
588
593
|
assert.match(app, /const GIT_WORKFLOW_CREATE_PR_TOOLTIP = \[[\s\S]*"Create PR branch:"[\s\S]*"1\. Ask Pi to generate a type\/feature-name branch from staged changes\."[\s\S]*"6\. Push and Create PR will push upstream, run \/pr, let you review, then run gh pr create\."/, "Create PR should have an up-to-date step-by-step tooltip");
|
|
589
594
|
assert.match(app, /const GIT_WORKFLOW_MANUAL_BRANCH_TOOLTIP = \[[\s\S]*"Manual PR branch:"[\s\S]*"1\. Skip agent branch-name generation\."[\s\S]*"6\. Push and Create PR will push upstream, run \/pr, let you review, then run gh pr create\."/, "Manual branch should have an up-to-date step-by-step tooltip");
|
|
590
595
|
assert.match(app, /addGitWorkflowAction\("Manual branch", \(\) => createGitPrBranchManually\(\), "", false, GIT_WORKFLOW_MANUAL_BRANCH_TOOLTIP\)/, "Manual branch should render with its tooltip");
|
|
596
|
+
assert.match(app, /function renderGitWorkflowManualCommitInput\(\)[\s\S]*git-workflow-message-input[\s\S]*Commit input[\s\S]*commitGitWorkflow\("input", tabId\)/, "Message stage should render a manual commit message input with a Commit input action");
|
|
597
|
+
assert.match(app, /gitWorkflow\.step === "generate"\) \{\n\s+renderGitWorkflowManualCommitInput\(\);\n\s+addGitWorkflowAction\("Run \/git-staged-msg"/, "Message process stage should show manual input before generated-message actions");
|
|
598
|
+
assert.match(app, /renderGitWorkflowManualCommitInput\(\);[\s\S]*addGitWorkflowAction\("Commit short"/, "Commit choice stage should keep manual commit input before generated commit choices");
|
|
599
|
+
assert.match(app, /async function commitGitWorkflow\(variant[\s\S]*variant === "input"[\s\S]*message: inputMessage/, "Commit input should send the typed message to the git workflow commit API");
|
|
600
|
+
assert.match(app, /const donePatch = variant === "input"[\s\S]*message: true, commit: true/, "Commit input should mark both message and commit workflow processes done");
|
|
601
|
+
assert.match(server, /\["short", "long", "input"\][\s\S]*cleanGitCommitMessageInput\(body\.message\)[\s\S]*git commit -m <input message>/, "server should accept typed git workflow commit messages");
|
|
602
|
+
assert.match(css, /\.git-workflow-message-input-row \{[\s\S]*flex:\s*1 1 100%/, "manual git workflow commit input should span the Message stage actions row");
|
|
591
603
|
assert.match(css, /\.git-workflow-actions button\[data-tooltip\]::after \{[\s\S]*content:\s*attr\(data-tooltip\)[\s\S]*white-space:\s*pre-line/, "guided git workflow action tooltips should render multiline step lists");
|
|
592
604
|
assert.match(app, /function gitBranchNamePromptMessage\(\)[\s\S]*hasAvailableCommand\("git-branch-name"\)[\s\S]*return "\/git-branch-name"/, "guided git workflow should ask the agent to generate PR branch names when the prompt is available");
|
|
593
605
|
assert.match(app, /async function loadGitWorkflowBranchName\([\s\S]*gitWorkflowRequest\("\/api\/git-workflow\/branch-name"/, "guided git workflow should load generated agent branch names before branch creation");
|
|
@@ -636,7 +648,7 @@ assert.match(app, /function scheduleLiveToolRunRender\(run[\s\S]*?liveToolRender
|
|
|
636
648
|
assert.match(app, /function handleToolExecutionUpdate\(event\)[\s\S]*?event\.partialResult[\s\S]*?scheduleLiveToolRunRender\(run, \{ scroll: false \}\)/, "live tool_execution_update events should update transcript-visible tool cards without replacing them per event");
|
|
637
649
|
assert.match(app, /function captureReusableToolCards\(\)[\s\S]*?\.message\.toolExecution\[data-tool-call-id\]/, "full transcript re-renders should capture existing tool cards before clearing the chat");
|
|
638
650
|
assert.match(app, /function appendMessage\(message,[\s\S]*?reusableToolCards = null[\s\S]*?reuseToolExecutionBubble\(reusableToolCards, message/, "message rendering should reuse matching tool cards instead of replacing them during refreshes");
|
|
639
|
-
assert.match(app, /function renderAllMessages\(\{ preserveScroll = false \} = \{\}\)[\s\S]*?const reusableToolCards = captureReusableToolCards\(\);[\s\S]*?appendTranscriptMessage\(item\.message,[\s\S]*?reusableToolCards,/, "transcript refreshes should pass reusable tool cards through to item rendering");
|
|
651
|
+
assert.match(app, /function renderAllMessages\(\{ preserveScroll = false, forceRebuild = false \} = \{\}\)[\s\S]*?const reusableToolCards = captureReusableToolCards\(\);[\s\S]*?appendTranscriptMessage\(entry\.item\.message,[\s\S]*?reusableToolCards,/, "transcript refreshes should pass reusable tool cards through to item rendering");
|
|
640
652
|
assert.match(app, /const keyedToolExecution = message\.role === "toolExecution" && message\.toolCallId[\s\S]*?keyedToolExecution \? "toolExecution"[\s\S]*?keyedToolExecution \? "" : message\.title[\s\S]*?keyedToolExecution \? "" : message\.timestamp/, "tool action entry identity should stay stable when live transient cards become persisted transcript cards");
|
|
641
653
|
assert.match(app, /appendText\(preview, toolResultPreviewText\(message, 10\), "code-block tool-result-preview-text"\)/, "collapsed tool results should render the first ten preview lines by default");
|
|
642
654
|
assert.match(app, /function assistantDisplayMessages\(message\)/, "assistant history should split thinking and tool-call parts out of the final Assistant output card");
|
|
@@ -669,7 +681,7 @@ assert.match(app, /function renderStreamingAssistantText\(\)[\s\S]*?const assist
|
|
|
669
681
|
assert.match(app, /const STREAM_OUTPUT_HIDE_DELAY_MS = 300/, "stream output hiding should be debounced to prevent rapid flicker");
|
|
670
682
|
assert.match(app, /const STREAM_OUTPUT_TOOLCALL_GUARD_MS = 220/, "live assistant text should be briefly guarded so pre-tool-call text can be suppressed");
|
|
671
683
|
assert.match(app, /function scheduleStreamBubbleHide\([\s\S]*?STREAM_OUTPUT_MIN_VISIBLE_MS/, "stream output cards should observe a minimum visible duration before hiding");
|
|
672
|
-
assert.match(app, /if \(assistantText\) \{[\s\S]*?
|
|
684
|
+
assert.match(app, /if \(assistantText\) \{[\s\S]*?renderStreamingMarkdown\(streamText, assistantText\);[\s\S]*?\} else \{\n\s+scheduleStreamBubbleHide\(\);/, "empty filtered stream output should schedule hide while visible stream output renders as Markdown");
|
|
673
685
|
assert.match(app, /if \(streamToolCallSeen \|\| streamBubble\) renderStreamingAssistantText\(\);\n\s+else scheduleStreamingAssistantTextRender\(\);/, "live assistant text should wait briefly before showing unless it is already visible or follows a tool call");
|
|
674
686
|
assert.match(app, /streamToolCallSeen = true;\n\s+suppressStreamingAssistantTextBeforeToolCall\(\);/, "tool-call starts should remove pending assistant text from the live transcript");
|
|
675
687
|
assert.match(app, /const created = appendMessage\(\{ role: "assistant", title: "final output"/, "live Assistant cards should be created only for final output text without a noisy Assistant label");
|
|
@@ -939,6 +951,12 @@ assert.ok(matrixBackground.length > 100000, "Matrix background image should be p
|
|
|
939
951
|
assert.ok(mochaBackground.length > 8000, "Catppuccin Mocha background image should be present as a compact PNG asset");
|
|
940
952
|
|
|
941
953
|
assert.match(server, /AuthStorage, SessionManager/, "server should import AuthStorage for safe OAuth token refresh");
|
|
954
|
+
assert.match(server, /DefaultPackageManager/, "server should use Pi's package resolver when controlling Web UI tab extension loading");
|
|
955
|
+
assert.match(server, /WEBUI_CONTROLLED_PACKAGES = new Set\(\[WEBUI_PACKAGE, \.\.\.OPTIONAL_FEATURE_PACKAGES\.values\(\)\]\)/, "server should identify Web UI-controlled packages for de-duplicated feature loading");
|
|
956
|
+
assert.match(server, /const args = \["--mode", "rpc", "--no-extensions", "--no-skills", "--no-prompt-templates", "--no-themes"\]/, "Web UI tabs should disable implicit resource loading before adding curated resource paths");
|
|
957
|
+
assert.match(server, /normalPiResourcePathsForTab[\s\S]*WEBUI_CONTROLLED_PACKAGES\.has\(packageName\)[\s\S]*continue/, "Web UI tab resource resolution should exclude separately installed Web UI feature packages");
|
|
958
|
+
assert.match(server, /startedWebuiResourcePaths\(resourceType\)/, "Web UI tabs should load feature resources from the started Web UI package");
|
|
959
|
+
assert.match(server, /workspacePackageRootForName\(nodeModulesRef\.packageName\)/, "dev Web UI should prefer top-level workspace packages for node_modules manifest entries");
|
|
942
960
|
assert.match(server, /const CODEX_TOKEN_REFRESH_SKEW_MS = 5 \* 60 \* 1000/, "server should refresh Codex OAuth tokens before they expire");
|
|
943
961
|
assert.match(server, /url\.pathname === "\/api\/codex-usage" && req\.method === "GET"/, "server should expose a sanitized Codex usage endpoint");
|
|
944
962
|
assert.match(server, /OPENAI_CODEX_USAGE_ENDPOINT/, "server should query Codex usage from the backend, not the browser");
|
|
@@ -1090,10 +1108,13 @@ assert.match(readme, /browser notifications when a tab needs an extension UI res
|
|
|
1090
1108
|
assert.match(readme, /blocked-tab browser notifications, and optional agent-done notifications require browser service-worker\/notification support/, "README should document notification requirements");
|
|
1091
1109
|
assert.match(readme, /Side-panel theme picker backed by optional `@firstpick\/pi-themes-bundle` themes when loaded/, "README should describe optional theme selection");
|
|
1092
1110
|
assert.match(readme, /## Optional companion packages/, "README should document optional Web UI companion packages");
|
|
1111
|
+
assert.match(readme, /curates Pi resources from the Web UI package that started the server/, "README should document started-package-based Web UI feature loading");
|
|
1112
|
+
assert.match(readme, /separately installed Web UI companion packages are ignored to avoid loading two copies/, "README should document duplicate companion suppression");
|
|
1093
1113
|
assert.match(readme, /checks loaded Pi capabilities directly through RPC-visible commands and live widget events/, "README should document capability-based startup checks");
|
|
1094
1114
|
assert.match(readme, /side panel shows each optional feature as enabled, disabled, or install-needed/, "README should document optional feature side-panel controls");
|
|
1095
1115
|
assert.match(readme, /Installing a missing feature is an explicit, warned action/, "README should document optional feature install warning behavior");
|
|
1096
1116
|
assert.match(readme, /\.\/start-webui\.sh --dev --cwd \/path\/to\/project/, "README should document the dev helper launcher");
|
|
1117
|
+
assert.match(readme, /sync-pi-package-symlinks\.sh[\s\S]*only one copy is loaded/, "README should document dev companion symlink setup");
|
|
1097
1118
|
assert.match(startScript, /--dev\)/, "start-webui.sh should accept a --dev flag");
|
|
1098
1119
|
assert.match(startScript, /local_pi_webui_bin\(\)/, "start-webui.sh should resolve this checkout's local server entrypoint");
|
|
1099
1120
|
assert.match(startScript, /webui_cmd=\(node "\$local_webui_bin"\)/, "start-webui.sh --dev should run the local bin with node");
|
|
@@ -1108,15 +1129,8 @@ for (const [name, range] of Object.entries(companionDependencies)) {
|
|
|
1108
1129
|
assert.equal(pkg.dependencies?.[name], undefined, `webui package should not require optional companion ${name}`);
|
|
1109
1130
|
}
|
|
1110
1131
|
assert.equal(pkg.bundledDependencies, undefined, "webui optional companion packages should not be bundled into the tarball");
|
|
1132
|
+
assert.ok(pkg.pi?.extensions?.includes("./index.ts"), "webui Pi manifest should load its own extension");
|
|
1111
1133
|
for (const extensionPath of [
|
|
1112
|
-
"../pi-extension-git-footer-status/index.ts",
|
|
1113
|
-
"../pi-extension-release-aur/index.ts",
|
|
1114
|
-
"../pi-extension-release-npm/index.ts",
|
|
1115
|
-
"../pi-extension-safety-guard/index.ts",
|
|
1116
|
-
"../pi-extension-setup-skills/index.ts",
|
|
1117
|
-
"../pi-extension-stats/index.ts",
|
|
1118
|
-
"../pi-extension-todo-progress/index.ts",
|
|
1119
|
-
"../pi-extension-tools/index.ts",
|
|
1120
1134
|
"node_modules/@firstpick/pi-extension-git-footer-status/index.ts",
|
|
1121
1135
|
"node_modules/@firstpick/pi-extension-release-aur/index.ts",
|
|
1122
1136
|
"node_modules/@firstpick/pi-extension-release-npm/index.ts",
|
|
@@ -1128,13 +1142,56 @@ for (const extensionPath of [
|
|
|
1128
1142
|
]) {
|
|
1129
1143
|
assert.ok(pkg.pi?.extensions?.includes(extensionPath), `webui Pi manifest should load ${extensionPath} when present`);
|
|
1130
1144
|
}
|
|
1131
|
-
|
|
1145
|
+
for (const siblingExtensionPath of [
|
|
1146
|
+
"../pi-extension-git-footer-status/index.ts",
|
|
1147
|
+
"../pi-extension-release-aur/index.ts",
|
|
1148
|
+
"../pi-extension-release-npm/index.ts",
|
|
1149
|
+
"../pi-extension-safety-guard/index.ts",
|
|
1150
|
+
"../pi-extension-setup-skills/index.ts",
|
|
1151
|
+
"../pi-extension-stats/index.ts",
|
|
1152
|
+
"../pi-extension-todo-progress/index.ts",
|
|
1153
|
+
"../pi-extension-tools/index.ts",
|
|
1154
|
+
]) {
|
|
1155
|
+
assert.ok(!pkg.pi?.extensions?.includes(siblingExtensionPath), `webui Pi manifest should avoid duplicate sibling load path ${siblingExtensionPath}`);
|
|
1156
|
+
}
|
|
1132
1157
|
assert.ok(pkg.pi?.skills?.includes("node_modules/@firstpick/pi-extension-release-aur/skills"), "webui Pi manifest should load release-aur nested skills when present");
|
|
1133
|
-
assert.ok(pkg.pi?.
|
|
1158
|
+
assert.ok(!pkg.pi?.skills?.includes("../pi-extension-release-aur/skills"), "webui Pi manifest should avoid duplicate release-aur sibling skills");
|
|
1134
1159
|
assert.ok(pkg.pi?.prompts?.includes("node_modules/@firstpick/pi-prompts-git-pr/prompts"), "webui Pi manifest should load guided-git nested prompts when present");
|
|
1135
|
-
assert.ok(pkg.pi?.
|
|
1160
|
+
assert.ok(!pkg.pi?.prompts?.includes("../pi-package-prompts-git-pr/prompts"), "webui Pi manifest should avoid duplicate guided-git sibling prompts");
|
|
1136
1161
|
assert.ok(pkg.pi?.themes?.includes("node_modules/@firstpick/pi-themes-bundle/themes"), "webui Pi manifest should load nested bundled themes when present");
|
|
1162
|
+
assert.ok(!pkg.pi?.themes?.includes("../pi-package-themes-bundle/themes"), "webui Pi manifest should avoid duplicate sibling bundled themes");
|
|
1137
1163
|
assert.ok(pkg.scripts?.check?.includes("node --check public/app.js"), "check script should syntax-check app.js");
|
|
1138
1164
|
assert.ok(pkg.scripts?.check?.includes("node tests/run-all.mjs"), "check script should run the shared test runner");
|
|
1139
1165
|
|
|
1166
|
+
// --- Performance: keyed transcript reconciliation (P0-1) ---
|
|
1167
|
+
assert.match(app, /let renderedTranscriptState = \{ epoch: "", entries: \[\] \};/, "transcript reconciliation should track rendered entries");
|
|
1168
|
+
assert.match(app, /function renderAllMessages\(\{ preserveScroll = false, forceRebuild = false \} = \{\}\)[\s\S]*?if \(prefixLength === 0\) resetChatOutput\(\);[\s\S]*?removeChatBubblesAfterPrefix\(/, "renderAllMessages should reuse the unchanged transcript prefix instead of always rebuilding");
|
|
1169
|
+
assert.match(app, /function removeChatBubblesAfterPrefix\(keptKeys\)[\s\S]*?child === elements\.stickyUserPromptButton \|\| child === runIndicatorBubble/, "prefix removal must preserve the sticky prompt button and run indicator");
|
|
1170
|
+
assert.match(app, /function resetChatOutput\(\) \{\n liveToolCards\.clear\(\);\n renderedTranscriptState = \{ epoch: "", entries: \[\] \};/, "full chat resets must clear reconciliation state");
|
|
1171
|
+
assert.match(app, /function transcriptRenderEpoch\(\)[\s\S]*?thinkingOutputVisible/, "reconciliation epoch must include thinking visibility so toggles rebuild the transcript");
|
|
1172
|
+
assert.match(app, /pruneDisconnectedLiveToolCards\(\);/, "reconciliation must prune live tool card references to removed DOM nodes");
|
|
1173
|
+
|
|
1174
|
+
// --- Performance: incremental streaming markdown (P0-3) ---
|
|
1175
|
+
assert.match(app, /function streamingMarkdownStableBoundary\(text\)[\s\S]*?for \(let index = 0; index < lines\.length - 1; index \+= 1\)/, "streaming markdown boundary must never treat the final partial line as stable");
|
|
1176
|
+
assert.match(app, /function renderStreamingMarkdown\(block, text\)[\s\S]*?if \(!text\.startsWith\(state\.stableText\)\)/, "streaming markdown must fall back to a full re-render when earlier content changes");
|
|
1177
|
+
assert.match(app, /streamRawText = "";\n streamMarkdownState = null;/, "resetting the stream bubble must clear incremental markdown state");
|
|
1178
|
+
|
|
1179
|
+
// --- Performance: delta transcript fetch (P1-1) ---
|
|
1180
|
+
assert.match(app, /function mergeMessagesDelta\(previous, data\)[\s\S]*?messagesLookEqual\(previous\[since\], data\.messages\[0\]\)/, "delta merges must verify the one-message overlap before applying");
|
|
1181
|
+
assert.match(app, /async function refreshMessages\(tabContext = activeTabContext\(\)\)[\s\S]*?\/api\/messages\?since=/, "message refreshes should request transcript deltas");
|
|
1182
|
+
assert.match(app, /if \(!nextMessages\) \{[\s\S]*?api\("\/api\/messages", \{ tabId: tabContext\.tabId \}\)/, "delta failures must fall back to a full transcript fetch");
|
|
1183
|
+
assert.match(server, /function applyMessagesSinceParam\(response, url\)/, "server should slice get_messages results for \\?since= requests");
|
|
1184
|
+
assert.match(app, /const messageStaticSignatureCache = new WeakMap\(\);/, "static message signatures should be cached by object identity");
|
|
1185
|
+
assert.match(app, /case "tool_execution_end":(?:(?!scheduleRefreshMessages)[\s\S])*?break;/, "tool completions must not trigger full transcript refreshes");
|
|
1186
|
+
assert.match(app, /case "message_end":[\s\S]*?scheduleRefreshMessages\(\);/, "assistant message completion must still reconcile the transcript");
|
|
1187
|
+
|
|
1188
|
+
// --- UX: transcript search (P2-1) ---
|
|
1189
|
+
assert.match(html, /id="chatSearchBar"[\s\S]*?id="chatSearchInput"[\s\S]*?id="chatSearchPrevButton"[\s\S]*?id="chatSearchNextButton"[\s\S]*?id="chatSearchCloseButton"/, "transcript search bar markup should exist with navigation controls");
|
|
1190
|
+
assert.match(app, /function openChatSearch\(\)[\s\S]*?elements\.chatSearchInput\?\.focus\(\)/, "opening transcript search should focus the input");
|
|
1191
|
+
assert.match(app, /\(event\.ctrlKey \|\| event\.metaKey\) && !event\.altKey && !event\.shiftKey && event\.key\.toLowerCase\(\) === "f"/, "Ctrl\/Cmd+F should open the transcript search");
|
|
1192
|
+
assert.match(app, /function focusChatSearchMatch\(\)[\s\S]*?details\.open = true;[\s\S]*?scrollIntoView/, "navigating to a search match should expand collapsed tool output and scroll to the bubble");
|
|
1193
|
+
assert.match(app, /autoFollowChat = false;\n lastChatProgrammaticScrollAt = performance\.now\(\);/, "search navigation must not fight chat auto-follow");
|
|
1194
|
+
assert.match(css, /\.message\.search-current \{/, "current search match should have a highlight style");
|
|
1195
|
+
assert.match(css, /\.chat-search-bar \{/, "transcript search bar should be styled");
|
|
1196
|
+
|
|
1140
1197
|
console.log("mobile static checks passed");
|