agentgui 1.0.956 → 1.0.957
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/.claude/workflows/gui-cohesion.js +95 -0
- package/PUNCHLIST-COHESION.md +367 -0
- package/lib/http-handler.js +49 -3
- package/package.json +1 -1
- package/site/app/index.html +4 -22
- package/site/app/js/app.js +228 -91
- package/site/app/js/backend.js +12 -1
- package/site/app/vendor/anentrypoint-design/247420.css +170 -7
- package/site/app/vendor/anentrypoint-design/247420.js +11 -11
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
# PUNCHLIST-COHESION.md
|
|
2
|
+
|
|
3
|
+
agentgui GUI cohesion sweep (workflow gui-cohesion.js, run wf_d5a2c469-71a): 61 agents, 6 cohesion lenses -> 40 adversarially-confirmed gaps (kept-typography guard). One-product feel vs claude.ai/code / cowork / Claude Desktop, with fsbrowse as the file-view bar.
|
|
4
|
+
|
|
5
|
+
## Confirmed gaps (40)
|
|
6
|
+
|
|
7
|
+
### 1. [high | kit] Code blocks inside assistant markdown have no copy button - the single biggest 'reads as primitive' gap vs claude.ai/code & Claude Desktop, where EVERY fenced block has a hover copy affordance
|
|
8
|
+
- **where:** C:\dev\anentrypoint-design\src\components\chat.js:103-111 (MdNode) and :113-131 (CodeNode head has lang/name but no copy)
|
|
9
|
+
- **benchmark:** claude.ai/code, Claude Desktop
|
|
10
|
+
- **fix:** MdNode's refSink already swaps innerHTML from renderMarkdownCached; after setting innerHTML, iterate el.querySelectorAll('pre') and inject a kit '.chat-code-copy' button (navigator.clipboard.writeText(codeEl.innerText), label flips 'copy'->'copied' for ~1.6s, aria-label='copy code', 26x26 min hit area expanded to >=24px, focus-visible ring). Also add the same copy control to the structured CodeNode head (the .chat-code-head currently renders only .lang + .name). Wrap pre in a position:relative container so the button can sit top-right and reveal on .chat-code:hover / :focus-within, matching the .chat-msg-actions hover pattern already in chat.css:147.
|
|
11
|
+
- **verified:** Verified against C:\dev\anentrypoint-design\src\components\chat.js. MdNode (103-111) sets el.innerHTML from renderMarkdownCached with no querySelectorAll('pre') copy-button injection. Structured CodeNode head (125-128) renders only .lang + optional .name, no copy control. A grep of src confirms zero .chat-code-copy / per-block clipboard affordance; the only copy path is message-level onCopyMessage (agent-chat.js:184, chat.js:273) which copies the whole message, not a single fenced block. The .chat-msg-actions hover pattern the fix references is real (chat.js:268-273), so the proposed top-right reveal-on-hover button is consistent with existing kit conventions, and the fix correctly lands in the kit. Matches the cited benchmark (claude.ai/code & Claude Desktop give every fenced block a hover copy affordance). Not a kept-typography false flag - this is a missing functional affordance, high severity.
|
|
12
|
+
|
|
13
|
+
### 2. [low | kit] Mid-stream code reads as plain inline text, not a code block - agent-chat.js downgrades md->text while streaming, so a fenced block streams as run-on prose then snaps into a styled block on settle (visible reflow + loss of monospace during the most-watched moment); claude.ai/code streams code already in a block
|
|
14
|
+
- **where:** C:\dev\anentrypoint-design\src\components\agent-chat.js:167 (isStreaming && part.kind==='md' -> {kind:'text', mdShell:true}) feeding renderInline at chat.js:176
|
|
15
|
+
- **benchmark:** claude.ai/code, Claude Desktop
|
|
16
|
+
- **fix:** Keep the cheap-streaming intent but detect an open/closed fence in the streaming md text: split the accumulated text on ``` and render closed fenced segments through a lightweight monospaced .chat-bubble.chat-code shell (no Prism mid-stream to avoid O(n^2)), leaving prose as inline text. Minimal version: if the streaming text contains a code fence, render the whole bubble as <pre> monospace so it does not reflow from prose to block on settle. Lands as a refinement to the md->text downgrade branch.
|
|
17
|
+
- **verified:** Verified against live source. agent-chat.js:167 does `if (isStreaming && part.kind === 'md') parts.push({ kind: 'text', text: part.text, mdShell: true })`, downgrading md to text mid-stream. chat.js:176 renders that text part via `renderInline(p.text)` inside `.chat-bubble.chat-md`. renderInline (chat.js:38-60) only matches inline single-backtick `` `([^`]+)` ``, **/*, and links — it has NO triple-backtick fenced-block handling, so a fenced block streams as run-on prose with the ``` fences shown literally and no monospace. On settle the part is the real md kind -> MdNode (chat.js:177), which renders a styled <pre> code block. That is a genuine reflow + loss-of-monospace during streaming, exactly as claimed, and diverges from claude.ai/code/Claude Desktop which stream code already inside a block. Fix lands correctly in the kit, refining the md->text downgrade branch (detect an open/closed fence and render the bubble as a monospaced <pre> shell, no Prism mid-stream to keep it cheap). Not a kept-typography flag. Severity low is fair.
|
|
18
|
+
|
|
19
|
+
### 3. [low | kit] Per-message actions are icon-only and easy to miss; copy uses 'page' icon and retry/edit are not the native Claude set - Claude Desktop pairs a clearer affordance and offers copy on every message; here retry only appears on the very last assistant turn and edit only on user turns, so most of the thread has a single mystery glyph button
|
|
20
|
+
- **where:** C:\dev\anentrypoint-design\src\components\agent-chat.js:182-188 (actions built) + chat.js:272-279 (.chat-msg-action icon-only)
|
|
21
|
+
- **benchmark:** Claude Desktop, claude.ai/code
|
|
22
|
+
- **fix:** In ChatMessage's actionRow, render a text label alongside the icon (or a tooltip-on-hover that is reliably visible) and keep the 26px buttons at >=24px tap targets. Map copy to a 'clipboard'/'copy' icon rather than 'page' for recognizability. Optionally allow retry on any assistant message (not just lastIdx) by having the host re-run from that turn; at minimum document the lastIdx restriction in the action title.
|
|
23
|
+
- **verified:** Verified against the live kit source. agent-chat.js:184-186 confirms copy uses icon:'page', retry is gated to i===lastIdx, and edit only renders for user (!isAssistant) turns. chat.js:272-279 confirms actionRow renders Icon(a.icon) only (icon-only; label is just a fallback when no icon), so most assistant turns show a single 'page' glyph button. shell.js ICON_PATHS has no 'copy'/'clipboard' icon and 'page' (line 101) is a lined-document glyph, not a clipboard - so the proposed fix correctly requires adding a copy/clipboard path and swapping it. Fix lands in the kit (the right place) and is sound. Not a kept-typography case (these are SVG line icons). Mitigating: buttons already carry title + aria-label, so they're accessible if not visually obvious - hence low severity is appropriate, but the gap is real: the wrong-semantic copy icon and the lastIdx retry restriction genuinely diverge from the Claude Desktop bar.
|
|
24
|
+
|
|
25
|
+
### 4. [low | kit+app] Assistant-vs-user distinction leans almost entirely on alignment + accent bubble; the assistant 'avatar' is a one-letter initial of the agent name, not a stable product mark, so a 'C'/'O'/'K' disc shifts identity per agent and reads less like one product than Claude's consistent assistant mark
|
|
26
|
+
- **where:** C:\dev\anentrypoint-design\src\components\chat.js:242-245 (fallbackAvatar = name initial) + app-shell.css:1616 .chat-msg.aicat .chat-avatar
|
|
27
|
+
- **benchmark:** Claude Desktop, claude.ai/code
|
|
28
|
+
- **fix:** Give the assistant column a consistent product mark per agent: pass an explicit avatar prop from the host (app.js chatMain) keyed to the agent (a small line-SVG Icon, not a letter) so the assistant identity is stable chrome, and keep the agent NAME in .chat-meta .who (already rendered) to disambiguate. App supplies avatar; kit already accepts the avatar prop.
|
|
29
|
+
- **verified:** Verified against live source. chat.js:242-244 computes fallbackAvatar from the agent name's first letter when no avatar prop is passed; the kit already accepts an `avatar` prop on ChatMessage. agent-chat.js:189-199 passes only `name` (resolved at line 129 to the agent's display name, e.g. Claude Code/OpenCode/Kilo) and NO avatar, so the assistant disc renders C/O/K and shifts identity per agent — unlike Claude Desktop's single consistent assistant mark. The agent name is already separately rendered in .chat-meta .who (chat.js:264), so a stable per-agent line-SVG Icon avatar disambiguates identity without losing the name. The fix is sound and correctly split kit+app: the app (chatMain, app.js:958) supplies an avatar keyed to the agent, and AgentChat must thread that avatar through to ChatMessage (it currently forwards neither). Does not flag kept typography. Low severity but a concrete, source-grounded cohesion gap that moves toward the cited benchmark.
|
|
30
|
+
|
|
31
|
+
### 5. [medium | kit+app] Composer lacks an inline agent+model+cwd affordance at the point of typing - the picker and cwd bar live in a separate controls cluster at the top of the panel, so while typing there is no compact 'agent · model · cwd' readout near the input the way Claude Desktop shows the active model by the composer
|
|
32
|
+
- **where:** C:\dev\anentrypoint-design\src\components\chat.js:288-339 (ChatComposer toolbar has attach/emoji/menu/send only) + agent-chat.js:205-213 (composer) vs AgentControls/CwdBar rendered at top :233-236
|
|
33
|
+
- **benchmark:** Claude Desktop
|
|
34
|
+
- **fix:** Add an optional '.chat-composer-context' line to ChatComposer (or render it in agent-chat just above the composer) showing 'agent name · model · cwd-basename' as a muted, clickable affordance that focuses the corresponding selector/cwd editor. Host passes the strings + onClick; no new transport. Keeps the top controls but gives a point-of-typing readout.
|
|
35
|
+
- **verified:** Verified against the real source. In C:\dev\anentrypoint-design\src\components\agent-chat.js the composer (lines 205-213, rendered at line 260) is placed directly after the scrollable agentchat-thread-wrap with NOTHING between thread and composer that shows the active agent/model/cwd. The only model readout is in agentchat-head (line 239, name + ' · ' + selectedModel) and the CwdBar/AgentControls (lines 233-236) — all rendered at the TOP of the panel, above a scrollable thread, so once the user scrolls down to type the active model/cwd is off-screen. In chat.js ChatComposer (lines 288-339) the toolbar (lines 330-337) holds only attach/emoji/menu/send — no context line. So the claim is source-accurate: there is no compact point-of-typing readout the way Claude Desktop shows the active model by the composer. Kept-typography guard does not apply: the gap does not flag the middot as decorative — it proposes ADDING a middot-separated readout (legitimate product typography). The fix is sound and correctly split: an optional .chat-composer-context line in the kit (ChatComposer or agent-chat just above composer), with the app passing 'agent · model · cwd-basename' strings + onClick to focus the existing selectors/cwd editor; no new transport, top controls retained. Lands in kit+app — correct. Real, modest cohesion gap that measurably moves toward the cited Claude Desktop bar.
|
|
36
|
+
|
|
37
|
+
### 6. [low | kit+app] Empty-state suggestions are static and vanish after the first turn - claude.ai/code & cowork surface contextual follow-up suggestions after responses; here suggestions:[] once messages exist (app.js:974), so the thread never offers a next step mid-conversation
|
|
38
|
+
- **where:** C:\dev\agentgui\site\app\js\app.js:974-979 (suggestions only when messages.length===0) + agent-chat.js:218-230 (emptyState only path that renders suggestions)
|
|
39
|
+
- **benchmark:** claude.ai/code, cowork
|
|
40
|
+
- **fix:** Add a kit 'followups' prop to AgentChat rendered as a row of .agentchat-followup chips below the last settled assistant turn (reuse .agentchat-empty-suggestion styling), shown only when !busy and lastMsg is assistant. Host can seed simple static follow-ups initially (e.g. 'explain this', 'show me the diff', 'run the tests') and later wire model-suggested ones. Note: the prior sweep scoped persistent suggestions OUT deliberately - this re-opens it as a low-risk chip row, not a redesign.
|
|
41
|
+
- **verified:** Verified against live source. app.js:974 reads `suggestions: state.chat.messages.length ? [] : [...]` - confirmed: suggestions are forced empty once any message exists. The kit at agent-chat.js:218-230 renders the `suggestions` chip row ONLY inside `emptyState` (gated `messages.length === 0`), and there is no `followups`/mid-conversation prop anywhere - so the thread offers no next step once a conversation is underway. Both cited locations match the claim exactly. The benchmark holds: claude.ai/code and cowork surface contextual follow-up chips after responses. The proposed fix is sound and correctly split: a new `followups` prop in the kit (agent-chat.js) rendered below the last settled assistant turn, gated on `!busy` + last message being assistant, reusing the existing `.agentchat-empty-suggestion` CSS (which already exists), with the app (app.js) seeding the values - fixLocation kit+app is right. It is not a kept-typography false flag (no middot/ellipsis/dash flagged). The gap honestly notes the prior sweep deliberately scoped persistent suggestions OUT (AGENTS.md confirms "empty-only starter pattern kept"), and re-opens it as a low-risk additive chip row rather than a redesign, which is a legitimate, concrete, source-grounded improvement toward the cited reference bar.
|
|
42
|
+
|
|
43
|
+
### 7. [high | kit+app] Preview is a focus-trapped MODAL, not a persistent side pane — breaks the one-product feel of the WorkspaceShell that has a ContextPane slot sitting empty on the Files tab
|
|
44
|
+
- **where:** site/app/js/app.js:634-660 filePreview() returns FileViewer (overlay); filesMain forces ws-no-pane (2-col) on the files tab
|
|
45
|
+
- **benchmark:** Claude Desktop / claude.ai/code (persistent file pane beside the list); fsbrowse author intent
|
|
46
|
+
- **fix:** Render the file preview INTO the WorkspaceShell pane (ContextPane region) instead of a modal: filesMain passes the preview body as the WorkspaceShell `pane` (kit already has the pane slot + collapse persistence) so selecting a file opens a split view (list left, preview right) like Claude Desktop / claude.ai/code. Keep the modal only as the <900px fallback. Add a kit FilePreviewPane wrapper (header name/size/path + close + download) distinct from the overlay FileViewer.
|
|
47
|
+
- **verified:** Verified against live source. site/app/js/app.js:634-660 filePreview() returns FileViewer({file, body, onClose}) — an overlay modal, confirmed; it is rendered as f.preview ? h('div',{key:'fprev'}, filePreview()) inside the files-tab main body (line 755), not into the shell pane. app.js:441-448 sets pane = state.tab === 'chat' ? ContextPane(...) : null, so on the Files tab WorkspaceShell receives pane=null and renders the 2-col (ws-no-pane) layout — the right pane slot sits empty exactly as claimed. The benchmark (Claude Desktop / claude.ai/code persistent file pane beside the list) is real, and the fix lands in the right places: the kit already exposes the pane slot with collapse persistence (WorkspaceShell, used at app.js:449), so the app only needs to wire the preview body as `pane` on the files tab and the kit gains a FilePreviewPane wrapper distinct from the overlay FileViewer (kept as the <900px fallback). Concrete and source-grounded, not a kept-typography false positive. Caveat: AGENTS.md's 5th-run Witness note records inline split-pane preview as a DELIBERATE scope-out ('modal retained — predictable, focus-trapped'), so this is a previously-considered judgment call rather than an overlooked gap — but the gap itself is genuinely present in the shipped source and clearing it measurably moves toward the cited reference-product bar, so real=true.
|
|
48
|
+
|
|
49
|
+
### 8. [medium | kit+app] No prev/next stepping through files in the preview — fsbrowse lets ArrowLeft/Right walk previewable files; agentgui preview is a single-file dead end
|
|
50
|
+
- **where:** site/app/js/app.js:634-660 (no stepViewer equivalent); kit FileViewer has no onPrev/onNext
|
|
51
|
+
- **benchmark:** fsbrowse (stepViewer prev/next + ArrowLeft/Right)
|
|
52
|
+
- **fix:** Add onPrev/onNext props to the kit FileViewer/FilePreviewPane (ASCII prev/next labels) and ArrowLeft/Right keybinding; app computes previewable (non-dir) neighbors in the current sorted+filtered list and calls loadPreviewContent on step. Matches fsbrowse stepViewer() public/app.js:181.
|
|
53
|
+
- **verified:** Verified against live source. app.js filePreview() (lines 634-660) renders a single FileViewer modal with only onClose=closePreview — no neighbor stepping, no Arrow keybinding. The kit FileViewer (c:\dev\anentrypoint-design\src\components\files-modals.js:171-192) accepts only {file,body,onClose,onAction}; it has no onPrev/onNext props or ArrowLeft/Right handling. The benchmark is accurate: fsbrowse public/app.js:181 defines stepViewer(delta) and lines 704-705 bind ArrowLeft/Right to it, so fsbrowse does walk previewable files while agentgui is a dead end. The fix is sound and correctly split: kit gains onPrev/onNext props (ASCII labels, consistent with glyph policy) + Arrow keybinding; the app already builds the sorted+filtered neighbor list (mapped/filtered in filesMain) and has loadPreviewContent(file) to call on step, so computing non-dir neighbors and stepping is feasible app-side. Medium severity is fair — a real affordance gap vs the cited reference product, not a kept-typography misflag.
|
|
54
|
+
|
|
55
|
+
### 9. [medium | app] Per-row file actions (download/rename/delete) are unwired — the kit FileRow renders a download/rename/delete action group but filesMain never passes onAction, so the Files view is READ-ONLY where fsbrowse is a full manager; the affordance is dangling/silent
|
|
56
|
+
- **where:** site/app/js/app.js:707-729 FileGrid call omits onAction; kit files.js:72-76 renders ds-file-actions only when onAction present
|
|
57
|
+
- **benchmark:** fsbrowse (download/rename/delete/move/mkdir/upload full CRUD)
|
|
58
|
+
- **fix:** Decide scope intentionally. If read-only is the product, document the cut so the dangling onAction path is not mistaken for a bug. If a manager is wanted, add confined server endpoints (/api/download raw bytes, /api/rename, /api/file DELETE via confineToRoots) and wire onAction in filesMain to the kit ConfirmDialog/PromptDialog (already exported). At minimum wire `download` since /api/image+/api/file already serve bytes.
|
|
59
|
+
- **verified:** Verified against live source. Kit files.js:72-76 renders the ds-file-actions group (download/rename/delete) only when onAction is present, and FileGrid (line 159) only forwards onAction when the host passes it. The app's FileGrid call (app.js:707-729) supplies files/loading/sort/filter/onUp/onOpen but omits onAction entirely, so the action group never renders and the Files view is read-only. The benchmark fsbrowse is a full CRUD manager, so this is a genuine cohesion/affordance gap, not a typography flag. Fix location is correct: the wiring belongs in app (filesMain), and any new endpoints reuse the already-existing confineToRoots helper in lib/http-handler.js (line 17, applied to /api/list, /api/file/, /api/image/). No /api/download, /api/rename, or DELETE endpoints exist today, confirming read-only. The minimum sound step (wire download — /api/file and /api/image already serve bytes) is concrete and source-grounded; the larger manager scope is correctly flagged as an intentional decision.
|
|
60
|
+
|
|
61
|
+
### 10. [medium | app] No upload / drag-to-move / new-folder — the kit ships DropZone + UploadProgress unused in agentgui; fsbrowse and Claude Desktop both let you drop files into the working dir
|
|
62
|
+
- **where:** site/app/js/app.js:747-756 filesMain has no DropZone/UploadProgress; kit files.js:188-227 DropZone+UploadProgress exist but unmounted
|
|
63
|
+
- **benchmark:** fsbrowse (upload + DropZone + progress); Claude Desktop (drop file to working dir)
|
|
64
|
+
- **fix:** If uploads are in scope for the chat-cwd workflow (drop a file the agent should read), mount kit DropZone + UploadProgress in filesMain and add a confined /api/upload (Busboy) endpoint mirroring fsbrowse index.js:199. If out of scope, record it as a deliberate scope cut in the punch-list (consistent with the inline-split-pane cut already documented).
|
|
65
|
+
- **verified:** Verified against live source. site/app/js/app.js:747-756 (filesMain) returns offlineBanner/PageHeader/rootsRow/toolbar/body/preview only — no DropZone, no UploadProgress, no upload handler; the Files view has zero upload affordance. The kit genuinely exports both unmounted: anentrypoint-design/src/components/files.js:188-227 defines DropZone (drag/drop + pick) and UploadProgress (progressbar items), and their CSS is even vendored (site/app/vendor/.../247420.css:1674-1698) yet no app code references them. So benchmark parity (fsbrowse/Claude Desktop drop-to-working-dir) is genuinely missing. One nuance: the fix's "add a confined /api/upload (Busboy) endpoint" is partly already present — lib/routes-upload.js:11 has app.post('/api/upload/:conversationId') with Busboy writing into conv.workingDirectory, plus an fsbrowse mount at /files/:conversationId — but that endpoint is keyed to the legacy conversationId/workingDirectory model, NOT the new allowlist-confined path browser the Files view uses, and nothing on the client calls it. So a path-confined upload variant (realpath-confined like /api/list per lib/http-handler.js:186-191) is still needed if uploads are in scope. The fix is sound and correctly split (mount kit components in app + add path-confined server endpoint, OR record as a deliberate scope cut mirroring the documented inline-split-pane cut). No kept-typography involved.
|
|
66
|
+
|
|
67
|
+
### 11. [low | kit+app] Server returns no per-entry permissions — fsbrowse surfaces read/write/EACCES per row; agentgui swallows stat failures (size=null) so a permission-denied or unreadable entry looks identical to a 0-byte file
|
|
68
|
+
- **where:** lib/http-handler.js:232-234 (catch swallows stat error, no permission field); kit FileRow has no locked/readonly affordance
|
|
69
|
+
- **benchmark:** fsbrowse (perms read/write/EACCES per entry)
|
|
70
|
+
- **fix:** Add `permissions` (['read','write'] | 'EACCES') to each /api/list entry (mirror fsbrowse checkPermissions index.js:48). Add a kit FileRow locked/readonly prop rendering an ASCII 'read-only'/'no access' meta tag (KEPT middot separator) and disabling onOpen for EACCES, so an unreadable file reads as locked not empty.
|
|
71
|
+
- **verified:** Verified against live source. lib/http-handler.js:232-234 confirmed: the per-entry stat is wrapped in try/catch that swallows the error (`catch (_) {}`), leaving size=null/modified=null with no permissions field — so an EACCES entry is byte-identical to a directory or a stat-less file in the JSON. fsbrowse reference (C:\dev\fsbrowse\index.js:48-54,129-170) confirmed: checkPermissions() returns ['read','write'] or 'EACCES' and is attached as `permissions` to every entry (and the catch branch emits permissions:'EACCES'). Kit FileRow (C:\dev\anentrypoint-design\src\components\files.js:46) confirmed to have NO locked/readonly/permissions prop — props are name/type/size/modified/code/onOpen/onAction/active/key only; no EACCES affordance and onOpen is always wired. The proposed fix lands correctly: add `permissions` to /api/list entries (server), add a FileRow locked prop rendering an ASCII read-only/no-access meta tag and disabling onOpen for EACCES (kit), wire in app. Kept-typography honored — the middot is used as a separator, not flagged as decorative. The gap genuinely moves the product toward the cited fsbrowse bar (unreadable file reads as locked, not empty).
|
|
72
|
+
|
|
73
|
+
### 12. [low | app] No deep-link / back-button routing for directory nav — fsbrowse pushes #/path on every cd so refresh/back works; agentgui Files resets to the default root on reload and the browser Back button does not walk directories
|
|
74
|
+
- **where:** site/app/js/app.js:579-594 loadDir (no history.pushState/hash sync); fsbrowse public/app.js:52-71
|
|
75
|
+
- **benchmark:** fsbrowse (hash routing + popstate)
|
|
76
|
+
- **fix:** Mirror fsbrowse: on loadDir push a hash segment encoding the absolute path and add a popstate listener that reloads the dir. Restores directory context across reload and makes Back navigate up the tree.
|
|
77
|
+
- **verified:** Verified against live source. app.js:579-594 loadDir does no history.pushState/hash sync. The app's hash router (readHash/buildHash/writeHash at app.js:35-57) only encodes tab= and sid= (conversation), and the popstate listener (app.js:1896-1901) only restores tab/session — neither has any Files-directory dimension. So on reload Files resets to the default root and Back cannot walk directories, unlike fsbrowse (public/app.js:52-71) which pushState's #/path on every loadFiles and restores it. The gap is genuine, not a kept-typography flag. Fix lands correctly in the app: the routing machinery already lives in app.js and only needs a path segment added to the hash plus popstate handling. One refinement: it should extend the existing tab=/sid= scheme (e.g. add dir=) rather than fsbrowse's bare #/path, to avoid colliding with the current hash format — but the core approach (encode dir on loadDir, reload on popstate) is sound. Severity low is fair since Files is a secondary surface.
|
|
78
|
+
|
|
79
|
+
### 13. [medium | kit] No large-directory handling — the grid renders every entry as a DOM row with no virtualization/pagination; a node_modules-scale dir floods the DOM and the roving-tabindex loop iterates all rows
|
|
80
|
+
- **where:** site/app/js/app.js:695-729 maps all entries; kit files.js:154-161 renders files.map with no windowing; server readdirSync returns whole dir
|
|
81
|
+
- **benchmark:** Claude Desktop / claude.ai/code (smooth large-tree handling)
|
|
82
|
+
- **fix:** Add a cap + 'show N more' (or windowing) to the kit FileGrid (render first ~200, a show-more row with the count using the KEPT ellipsis) plus an aria-live total-vs-shown count. Reuses the History tab's existing `load N older` pattern for cross-surface consistency.
|
|
83
|
+
- **verified:** Verified against live source. Kit files.js:154-161 (FileGrid) does `files.map(...)` -> one FileRow DOM node per entry with no cap, windowing, or pagination; the onKeyDown handler (lines 137-139) runs `grid.querySelectorAll('.ds-file-open:not([disabled])')` + `Array.from` + `indexOf` over ALL rows on every Arrow/Home/End/Backspace keypress. app.js:695-702 likewise maps/filters/sorts the entire `f.entries` list, and the server returns the whole dir. A node_modules-scale dir thus floods the DOM and the roving loop iterates all rows — exactly as claimed. The fixLocation (kit) is correct since the grid is the shared component; the proposed cap + 'show N more' row with aria-live shown/total count is sound and mirrors the existing History 'load N older' pattern for cross-surface consistency. It does not misflag any kept-typography glyph (it adds an ellipsis, the intentional product character). This measurably moves toward the cited Claude Desktop / claude.ai/code smooth-large-tree bar.
|
|
84
|
+
|
|
85
|
+
### 14. [low | kit] Code preview is a static dump with no copy button or filename/lang header — below the chat surface's own code-block polish, so a previewed file and a chat code block read as two different components
|
|
86
|
+
- **where:** site/app/js/app.js:652-657 FilePreviewCode/Text passed no copy action
|
|
87
|
+
- **benchmark:** claude.ai/code (consistent code presentation across file view and chat)
|
|
88
|
+
- **fix:** Give kit FilePreviewCode a copy-to-clipboard button + filename/lang header consistent with the chat ToolCallNode/code-block treatment so file preview and chat code share one component family.
|
|
89
|
+
- **verified:** Confirmed against live source. Kit FilePreviewCode (c:\dev\anentrypoint-design\src\components\files-modals.js:158-162) renders a bare <pre><code> with no header — verified it's invoked from site/app/js/app.js:655 with only {content, lang}. The chat surface's CodeNode (c:\dev\anentrypoint-design\src\components\chat.js:124-131) DOES render a .chat-code-head with a lang span + optional filename span. So a previewed file and a chat code block genuinely read as two different components, and giving FilePreviewCode a matching lang/filename header lands in the right place (kit) and measurably moves toward claude.ai/code's consistent code presentation. Real=true, but the gap's fix is partly mis-grounded: the chat code block has NO copy-to-clipboard button (grep for copy/clipboard/writeText in chat.js finds only a message-level hover action row at line 268, not a per-codeblock copy). So 'consistent with the chat code-block treatment' justifies the HEADER, not a copy button — adding a copy button would make preview MORE polished than chat, not consistent. The sound fix is the filename/lang header; the copy-button portion is an unsupported embellishment relative to the cited benchmark.
|
|
90
|
+
|
|
91
|
+
### 15. [medium | app] Single-root case has no sandbox framing and raw 403/EACCES errors leak — breadcrumb-walking past the root yields a bare error string with no explanation
|
|
92
|
+
- **where:** site/app/js/app.js:735-739 rootsRow only when roots.length>1; loadDir surfaces raw err.message via Alert app.js:704-705
|
|
93
|
+
- **benchmark:** fsbrowse (typed errors + sandbox framing); Claude Desktop (clear permission boundary messaging)
|
|
94
|
+
- **fix:** Always show the allowed-root context (even singular) and translate 403/EACCES into plain copy ('this folder is outside the allowed roots' / 'permission denied') instead of the raw error string. fsbrowse already returns typed errors (EPATHINJECTION/EACCES/ENOENT) to map from.
|
|
95
|
+
- **verified:** Verified against the live source. app.js:589-591 sets state.files.error = e.message; backend.js:62 throws new Error(j.error) so the message shown by Alert (app.js:704-705) is the raw server string: either "forbidden: <reason>" or, for stat/read failures, the bare Node error like "EACCES: permission denied, scandir '...'" / "ENOENT ..." (http-handler.js:204,241). No plain-language translation exists. rootsRow (app.js:735-739) is gated on roots.length > 1, so with a single allowed root the only sandbox cue is the PageHeader lede prose - the concrete root path is never surfaced. Both sub-claims are present in shipped source, the fix lands correctly in the app, and translating errors + always showing root context measurably moves toward the fsbrowse/Claude-Desktop boundary-messaging bar. Caveat: the fix's stated premise is partly wrong - /api/list is a bespoke handler in http-handler.js, NOT fsbrowse (fsbrowse is only used in routes-upload.js for a separate per-conversation router), so there are no fsbrowse EPATHINJECTION/EACCES/ENOENT typed errors to "map from" here. The mappable signals are the HTTP status (403/404) and the "forbidden:" message prefix; backend.js currently discards r.status, so the app would map on the message text/prefix (or backend.js should be widened to carry status). The corrective action is sound despite the inaccurate justification. No kept-typography involvement.
|
|
96
|
+
|
|
97
|
+
### 16. [high | kit+app] No busy-vs-stuck distinction - a stalled agent reads identical to a working one. status is binary running|error (status: (sess && sess.errors) ? 'error' : 'running'), so a session whose elapsed keeps ticking but whose lastActivity is 4 minutes stale still shows the green 'running' dot. claude.ai/code surfaces a 'waiting'/'stalled' affordance; here the only stuck-tell is reading the 'last 4m ago' text on each card. The whole reason currentTool+lastActivity were added (per the card comment) was to distinguish busy from stuck, but no card actually DERIVES a stuck/idle state from staleness.
|
|
98
|
+
- **where:** sessions.js:95-109 SessionCard (statusTone/status); app.js:799 liveMain status compute
|
|
99
|
+
- **fix:** App: derive a third status in liveMain - if sess.last and (Date.now()-sess.last) > ~45s and no currentTool, set status:'stale'. Kit SessionCard: accept status 'stale' -> .ds-dash-status.is-stale (amber/--fg-2 word 'idle' or 'waiting') + a non-animated grey-amber disc class (.status-dot-stale), distinct from the green running disc, so a glance across the grid separates working from stalled agents.
|
|
100
|
+
- **verified:** Verified against live source. sessions.js:95 sets statusTone = s.status === 'error' ? 'flame' : 'live' and line 109 renders a binary status word ('error'|'running'); the card's own comment (lines 89-92) explicitly states currentTool+lastActivity were added to distinguish busy from stuck, yet no card derives a stale/idle state. app.js:799 confirms status is binary: status: (sess && sess.errors) ? 'error' : 'running', so a session whose elapsed keeps ticking (line 795) but whose sess.last is minutes stale still shows the green running disc. The fix is sound and lands correctly: app.js already has sess.last (consumed at line 797 for lastActivity) and currentTool (line 798), so deriving status:'stale' when Date.now()-sess.last exceeds a threshold AND !currentTool is a clean app-side wiring change; the kit's single statusTone branch is the right place to add a non-animated stale disc + 'idle'/'waiting' word. Not a kept-typography false flag (it's about status state, not the middot/ellipsis/dash). This measurably moves toward the cited claude.ai/code waiting/stalled affordance.
|
|
101
|
+
|
|
102
|
+
### 17. [medium | kit] The running disc does not pulse - 'live' reads as static across the dashboard. .status-dot-disc.status-dot-live is just a flat green fill (chat.css:239) with no animation; the agentchat-status disc DOES pulse (chat.css:49-63). So the one surface meant to convey 'these are LIVE, in-motion sessions' shows the same dead dot as a settled list. claude.ai/code / Claude Desktop use a subtle live pulse to signal motion at a glance.
|
|
103
|
+
- **where:** chat.css:239 .status-dot-disc.status-dot-live (no animation); agentchat-status pulse at chat.css:49-63 for comparison
|
|
104
|
+
- **fix:** Add a reduced-motion-guarded pulse to .ds-dash-card .status-dot-disc.status-dot-live (reuse the agentchat keyframes), and explicitly DO NOT animate .status-dot-stale/.status-dot-error so only truly-busy cards breathe. Gate with @media (prefers-reduced-motion: reduce){ animation:none }.
|
|
105
|
+
- **verified:** Verified against C:/dev/anentrypoint-design/chat.css and src/components/sessions.js. The global .status-dot-disc.status-dot-live (chat.css:248) is a flat `background: var(--green)` with no animation, whereas .agentchat-status .status-dot-disc.status-dot-live (chat.css:53-55) applies `animation: agentchat-pulse 2s infinite`. SessionCard (sessions.js:106) renders the GLOBAL disc class, so the live dashboard - the one surface meant to signal in-motion sessions - shows the same dead dot as a settled list. The existing reduced-motion guard (chat.css:64) is scoped only to .agentchat-status, so a new guard IS needed. Fix is correctly placed in the kit and is sound (reuse agentchat-pulse keyframes on .ds-dash-card .status-dot-disc.status-dot-live, leave error static, gate with prefers-reduced-motion). One inaccuracy: there is no .status-dot-stale class (statusTone is only live/error -> status-dot-live/status-dot-error), so that clause of the fix is moot - but the core fix stands. No kept-typography glyph is being flagged.
|
|
106
|
+
|
|
107
|
+
### 18. [medium | kit+app] No bulk SELECT - only all-or-nothing stop-all. The prompt's bar is 'bulk select + stop-all'. SessionDashboard offers onStopAll(sessions) over EVERY session (sessions.js:142, app.js:810 stopAllActive) but there is no per-card checkbox/selection model, so you cannot stop a chosen subset (e.g. the 3 stalled ones) in one action. With several agents running this forces hunting each card's stop button - exactly the friction the bulk header was meant to remove, only partially solved.
|
|
108
|
+
- **where:** sessions.js:131-146 SessionDashboard (onStopAll only); no selection state in app.js liveMain
|
|
109
|
+
- **fix:** Kit: add optional selectable mode to SessionDashboard/SessionCard - a per-card checkbox (role=checkbox/aria-checked, 44x44 hit target) wired via onToggleSelect(sid), a selected:Set prop, and header that flips 'N running / stop all' to 'N selected / stop selected' when a selection exists. App: hold state.live.selected Set, wire onStopSelected to cancel only selected sids.
|
|
110
|
+
- **verified:** Verified against live source. sessions.js SessionDashboard (lines 131-146) wires only onStopAll(sessions) over EVERY session via the bulk header (line 142) and a per-card onStop on each SessionCard (line 120/line 145) - there is no selection model: no per-card checkbox, no `selected` prop, no onToggleSelect. app.js liveMain (lines 805-814) confirms it: it wires onStop:(s)=>stopActiveChat(s.sid) and onStopAll:(all)=>stopAllActive(all), and there is no selection Set in state.live anywhere. So a user with several running agents can stop ONE card or ALL cards, but not a chosen subset (e.g. just the stalled ones) in a single action - exactly the friction the bulk header was meant to remove, only half-solved. The proposed fix is sound and correctly split (kit: selectable mode with role=checkbox/aria-checked 44x44 hit target, selected Set prop, onToggleSelect, header flipping to 'N selected / stop selected'; app: hold state.live.selected Set and wire onStopSelected to cancel only selected sids) and lands in the right places (kit for UI/component, app for state+wiring). No kept-typography character is being flagged. Medium severity is appropriate.
|
|
111
|
+
|
|
112
|
+
### 19. [medium | kit+app] Dashboard has no sort or filter - sessions render in raw state.active order. The bar explicitly calls for 'sort/filter sessions' on the oversight surface. liveMain maps state.active straight into the grid (app.js:770-801) with no ordering control (by status, elapsed, last-activity, error-count) and no filter (by agent, errors-only, stale-only). The conversation RAIL got sort/filter/groups (sessionGroups, search) but the LIVE dashboard - the surface whose entire job is triage of many at once - did not, so it reads as the less-finished half of the same oversight product.
|
|
113
|
+
- **where:** app.js:767-815 liveMain (no sort/filter); contrast sessionGroups app.js:487 + rail search app.js:557
|
|
114
|
+
- **fix:** Kit SessionDashboard: add an optional toolbar row in the .ds-dash-header (sort Select: status|elapsed|last activity|errors; a filter SearchInput by agent/cwd; an 'errors only' toggle), emitting onSort/onFilter; render header even with sessions present. App: hold state.live.sort/filter, sort+filter the mapped sessions array before passing in. Use kit Select/SearchInput (label not aria-label) - no hardcoded controls.
|
|
115
|
+
- **verified:** Verified against live source. app.js:770-801 liveMain maps state.active into SessionDashboard in raw order with no sort/filter/state.live ordering. Kit SessionDashboard (sessions.js:131-147) renders .ds-dash-header with only a count + optional stop-all Btn - no sort Select, no filter SearchInput, no errors-only toggle. By contrast the rail got both: sessionsColumn (app.js:551-567) wires search (app.js:557) + sessionGroups (app.js:487-498) date buckets, and kit ConversationList (sessions.js:74-79) renders a real search input. The asymmetry is real: the triage-the-many surface is the less-finished half. Fix lands correctly - kit adds toolbar controls to the already-rendered .ds-dash-header using existing kit primitives (Select/SearchInput, label not aria-label per the DS SearchInput note), app holds state.live.sort/filter and sorts/filters the mapped sessions array before passing in. No kept-typography flagged (keptTypography:false is honest - gap is missing controls, not glyphs).
|
|
116
|
+
|
|
117
|
+
### 20. [medium | app] Errored/stale sessions are not pinned or surfaced first - flat insertion order buries the one that needs attention. Cards render in state.active order regardless of status (app.js:144 sessions.map). claude.ai/code floats the session needing action to the top. The rail pins 'Running' (sessionGroups, app.js:491-497) but the dashboard has no equivalent 'needs attention' ordering, so with 6 agents the errored one can sit at the bottom of the grid.
|
|
118
|
+
- **where:** sessions.js:143-145 ds-dash-grid (state order); app.js:805 SessionDashboard call
|
|
119
|
+
- **fix:** In liveMain, stable-sort the mapped sessions so status 'error' then 'stale' float to the front before passing to SessionDashboard (until the kit sort prop above lands, this is the default ordering). Keeps the most-actionable cards in the first grid row.
|
|
120
|
+
- **verified:** Verified in real source. sessions.js:143-145 (SessionDashboard) renders the grid as `...sessions.map(...)` in the exact passed-array order with no status sort. app.js:770-801 (liveMain) builds that array via `state.active.map(...)` in insertion order and passes it straight to SessionDashboard (app.js:805) - no reordering. So with multiple agents an errored card (status set at app.js:799 to 'error' when sess.errors) can sit anywhere in the grid, buried, while claude.ai/code floats the session needing action up. The fix is sound and lands in the right place (app, in liveMain, before the SessionDashboard call - app keeps wiring per project rules; the kit needs no change). One correction to the claim: the app only ever emits status 'error' or 'running' (app.js:799); there is NO 'stale' status in the source, so only the 'error'-float half of the proposed sort is actionable, but that half is real and worth doing. Not a kept-typography issue.
|
|
121
|
+
|
|
122
|
+
### 21. [medium | kit+app] Empty vs offline is handled but 'connected, zero running' loses the live-stream health signal. SessionDashboard distinguishes offline (health!=ok -> 'Backend offline', sessions.js:133) from empty ('No live sessions', :136), which is good. But the empty state says nothing about whether the live SSE stream (state.live.connected/error from openLiveStream) is actually connected - on the live tab a user with zero running sessions cannot tell 'idle and listening' from 'stream silently dropped'. The crumb dot only carries live-stream health on the HISTORY tab (app.js:379 liveActive = state.tab==='history').
|
|
123
|
+
- **where:** app.js:767-808 liveMain (passes offline from health only, ignores state.live.connected/error); view() dot is history-scoped app.js:379
|
|
124
|
+
- **fix:** App: open the live SSE stream on navTo('live') too (currently only history wires openLiveStream), and pass the live-stream health into the dashboard. Kit SessionDashboard: accept a streamState prop (connected|connecting|lost) and render a small status line in the empty/populated header ('listening for activity' vs 'live stream lost - retrying') so an idle dashboard still proves it is live.
|
|
125
|
+
- **verified:** Verified against live source. app.js:768 liveMain() derives `offline` only from state.health.status and passes nothing about state.live.connected/error to SessionDashboard. app.js:379/385 scope the crumb's live-stream health to `state.tab==='history'`, so on the live tab the dot falls back to the generic health label. navTo() (app.js:217-219) opens openLiveStream() ONLY when tab==='history' (closes it on leaving), so the live tab never has the SSE stream running - meaning the per-session sessionsBySid activity tallies that liveMain reads (sessions.js usage, app.js:776-797) are never even refreshed there. Kit SessionDashboard (sessions.js:131-137) exposes only offline/emptyText with no streamState surface. The gap genuinely exists: an idle live dashboard cannot prove it is still listening. The proposed fix is sound and lands in the right places - app: open the stream on navTo('live') and pass live-stream health in; kit: add a streamState prop to SessionDashboard rendering a 'listening'/'lost - retrying' line. Not a kept-typography false flag (the only glyph involved is the middot separator, used incidentally).
|
|
126
|
+
|
|
127
|
+
### 22. [medium | app] Live metrics only populate for sessions ALSO present in the history index - a brand-new chat shows no counter/last-activity. liveMain reads tallies from bySid = state.sessionsBySid (app.js:769,774); a just-started chat not yet in the ccsniff session list returns sess=null, so counter:null and lastActivity:'' (the card comment at app.js:772-773 admits this). The card then shows elapsed + 'running' with no motion data - the exact 'frozen counter reads as stalled' problem the metrics were added to solve, for the freshest sessions.
|
|
128
|
+
- **where:** app.js:774-799 liveMain (counterBits/lastActivity gated on bySid.get); sessionsBySid built app.js:1733
|
|
129
|
+
- **fix:** Maintain a per-sid live tally keyed by sessionId independent of the history index (increment in openLiveStream's event handler even when sessionsBySid has no row - it currently early-returns via debouncedRefreshHistory at app.js:342-346 and drops the count), and read counter/last from that tally so new sessions show motion immediately.
|
|
130
|
+
- **verified:** Verified against app.js. liveMain (app.js:769,774,776-799) reads counter/lastActivity from bySid.get(r.sessionId); when absent it sets counter:null, lastActivity:'' (the comment at 771-773 admits this). The SSE handler (app.js:336-347) only increments sess.events/tools/errors/last when the sid is already in sessionsBySid; for an unknown sid it calls debouncedRefreshHistory() and returns, dropping the count. sessionsBySid is built solely from the ccsniff listSessions result (app.js:1730-1733), so a just-started chat not yet in that index shows only elapsed + 'running' with no motion - the exact frozen-counter symptom these metrics were meant to fix. Fix is app-local (pure client state), sound, and lands in the right place. Caveat: keep debouncedRefreshHistory() rather than early-returning, so the independent tally is incremented AND the session still eventually enters the index. No kept-typography issue (the middot in counterBits is the intentional separator).
|
|
131
|
+
|
|
132
|
+
### 23. [low | kit+app] Jumping between active sessions loses place - 'open'/'resume' both just resumeInChat(sid) with no return path. onOpen and onResume are identical (app.js:811-812) and 'events' navigates to history+loadSession (app.js:813). There is no way to peek a session and jump back, nor any indicator on the dashboard of which session is currently 'open' in chat. claude.ai/code lets you flip between parallel agents keeping context; here every jump is a one-way navigation with no active-card highlight tying the dashboard to the chat tab.
|
|
133
|
+
- **where:** app.js:811-813 liveMain onOpen/onResume/onView; SessionCard actions sessions.js:116-120
|
|
134
|
+
- **fix:** App: pass a selected/current sid into the dashboard (state.chat.resumeSid) so the matching card highlights. Kit SessionCard: accept active:boolean -> .ds-dash-card.is-active (accent inset border, aria-current) so the open session is visible on the grid; collapse the redundant open/resume into one 'open' action and keep 'events' for the history peek.
|
|
135
|
+
- **verified:** Confirmed against live source. app.js:811-812 wire onOpen and onResume to the IDENTICAL call resumeInChat({sid:s.sid}); onView (813) navigates to history. sessions.js SessionCard (93-120) has no active/selected prop and renders no .is-active highlight or aria-current, so nothing on the dashboard grid signals which session is currently open in the chat tab. The proposed fix is sound and lands correctly: the kit's SessionCard gains an active:boolean -> .ds-dash-card.is-active + aria-current (visual/component work, kit), and the app passes active via r.sessionId === state.chat.resumeSid - which is exactly the comparison already used at app.js:784 for currentTool, so the wiring is consistent and grounded. Collapsing the redundant open/resume into one action is justified since they are byte-identical today. No kept-typography character is being flagged. The change measurably moves toward the cited claude.ai/code parallel-agent flip experience by tying the dashboard to the open chat.
|
|
136
|
+
|
|
137
|
+
### 24. [high | kit+app] The shell width re-flows on every tab switch — chat=4 cols (rail+sessions+main+pane), history=3 cols (rail+sessions+main), files/live/settings=2 cols (rail+main). The frame 'breathes' on each nav, so the app reads as separate page layouts, not one stable Claude-Desktop three-pane shell that keeps its columns while content swaps inside.
|
|
138
|
+
- **where:** site/app/js/app.js:438-448 (sessions = chat|history only; pane = chat only) driving shell.js:355-368 ws-no-sessions/ws-no-pane class toggles
|
|
139
|
+
- **fix:** Keep the three-pane FRAME constant across tabs: always pass `sessions` and `pane` to WorkspaceShell, and make each surface fill them. Files/Live should render a sessions-equivalent left column (e.g. roots/recent-dirs picker for Files, the running-sessions list for Live) and a context pane (Files: selected-file metadata; Live: aggregate counts; Settings: health summary). Where a surface genuinely has no second column, the kit should render a persistent collapsed placeholder rail rather than removing the grid track, so the main content does not jump horizontally. Net: column tracks stay put, only their contents change.
|
|
140
|
+
- **verified:** Verified against live source. app.js:438 sets `sessions = (state.tab === 'chat' || 'history') ? sessionsColumn() : null` and :441 sets `pane = state.tab === 'chat' ? ContextPane(...) : null` — exactly as cited. shell.js:355-368 toggles `ws-no-sessions`/`ws-no-pane` from `Boolean(sessions)`/`Boolean(pane)`. The vendored CSS (site/app/vendor/anentrypoint-design/247420.css:3152,3159-3161) then defines FOUR distinct grid-template-columns: full `rail sessions content pane`, `ws-no-sessions` -> `rail content pane`, `ws-no-pane.ws-no-sessions` -> `rail content` (2-track), and `ws-no-pane:not(.ws-no-sessions)` -> `rail sessions content`. So on each nav the grid track set genuinely changes (chat=4, history=3, files/live/settings=2), shifting the main column horizontally — the AGENTS.md witness note itself records this (chat=4cols, files/live/settings=2cols, history=3cols). This is a true cohesion gap, not a kept-typography false positive (no middot/ellipsis/dash involved). The fix is sound and correctly located (kit+app): keep tracks constant — kit renders a persistent collapsed placeholder track instead of dropping the grid column, and the app always passes sessions+pane with per-surface content (Files: roots/recent-dirs + selected-file metadata; Live: running list + aggregate counts; Settings: health summary). It moves the product toward the stable Claude-Desktop three-pane bar. Caveat: AGENTS.md notes the per-tab collapse was a deliberate 5th-run scope choice and mobile must stay single-column (CSS:3262-3266 collapses all variants to 1fr <900px, which the fix must preserve), but that is a constraint on the fix, not a reason the gap is unreal.
|
|
141
|
+
|
|
142
|
+
### 25. [medium | app] Status/connection vocabulary in the crumb dot is NOT identical across tabs: history shows 'live · N'/'connecting…'/reconnect text driven by the SSE stream, while chat/files/live/settings show 'connected'/'offline'/'ws reconnecting' driven by health. Same underlying connected state, two different word-sets depending on which tab you are on — the dot reads as a different control per page.
|
|
143
|
+
- **where:** site/app/js/app.js:379-394 (dotLabel/dotState branch on state.tab==='history')
|
|
144
|
+
- **fix:** Unify the dot vocabulary across every tab: always show the connection word (connected/connecting/offline) as the primary label, and append the live-stream count/reconnects as a secondary middot-joined suffix only when on history — rather than swapping the whole label semantics. One status grammar everywhere.
|
|
145
|
+
- **verified:** Confirmed in site/app/js/app.js:380-385. dotLabel branches on state.tab==='history': history yields 'live · N'/'live'/'connecting…'/error+reconnects, while chat/files/live/settings yield 'connected'/'ws reconnecting'/'offline'. Same underlying connection state, two distinct word-sets, so the same crumb dot reads as a different control per page — a real predictability/cohesion gap, not a kept-typography false positive (the middot is incidental, the issue is divergent grammar). The proposed fix (always show the connection word as primary label, append the live count/reconnects as a middot-joined suffix only on history) is sound and correctly lands in the app, since this label-composition logic is app-local view() code, not kit or server.
|
|
146
|
+
|
|
147
|
+
### 26. [medium | kit] ContextPane has no explicit empty/loading/error state — on backend-offline or before any agent is picked it still renders 'agent: none / model: — / idle', i.e. a populated-looking panel describing nothing. A real context pane should show an empty state ('no active conversation') so the pane's emptiness is legible, matching the explicit empty states the rest of the app has.
|
|
148
|
+
- **where:** kit src/components/context-pane.js:34-77 (always renders the context Panel regardless of whether there is any conversation)
|
|
149
|
+
- **fix:** Add an empty state to ContextPane: when there is no agent AND no messages AND no usage, render a single .ds-context-empty status line ('No active conversation — start a chat to see context here') instead of the four placeholder rows. Keep the populated panels once any of agent/messages/usage exist.
|
|
150
|
+
- **verified:** Verified in kit src/components/context-pane.js (lines 34-58): ContextPane unconditionally renders the 'context' Panel with four placeholder Rows (agent->'none', model->em-dash, working dir->'server default', running tools->'idle') regardless of whether any conversation exists. app.js:441-445 renders it on every chat-tab view, passing agent:'' when no agent is selected, so on backend-offline / pre-agent-pick it shows a fully-populated panel describing nothing — inconsistent with the app's explicit empty states (FileSkeleton/EmptyState, SessionDashboard empty-state). Fix correctly lands in the kit (ContextPane is a kit component) and is concrete: render a single .ds-context-empty line when no agent/usage/session, keep the panels once any signal exists. Does not flag kept typography (the em-dash on line 43 is incidental, not the subject). Minor refinement: the fix's 'messages' check should key off the actual signature (agent/usage/session), since ContextPane has no messages prop — but the empty-state intent and location are sound.
|
|
151
|
+
|
|
152
|
+
### 27. [medium | app] The rail has no footer (settings/theme/account) — Claude Desktop / cowork pin a persistent account-or-settings affordance at the rail bottom. The kit WorkspaceRail supports a `footer` slot (shell.js:439,465) but app.js workspaceRail() never passes one, and Settings is just another nav item mixed with Chat/History. The rail reads as a flat tab strip rather than a workspace nav with primary actions up top and utility pinned bottom.
|
|
153
|
+
- **where:** site/app/js/app.js:453-467 (WorkspaceRail call omits footer) vs kit shell.js:439 footer slot
|
|
154
|
+
- **fix:** Pass `footer:` to WorkspaceRail with the ThemeToggle (already imported in app.js:6) and pin Settings there, separating primary navigation (Chat/History/Files/Live) from utility — matching the Claude-Desktop rail hierarchy. Pure wiring of an existing kit slot.
|
|
155
|
+
- **verified:** Verified against live source. app.js:462-466 workspaceRail() calls WorkspaceRail({brand, action, items}) and never passes footer; Settings is item #5 in `items` (app.js:460), flatly mixed with Chat/History/Files/Live. Kit shell.js:439 declares the `footer` param and line 465 renders it as h('div',{class:'ws-rail-foot'}, footer). The footer slot AND .ws-rail-foot styling both exist in the actually-shipped vendored dist (site/app/vendor/anentrypoint-design/247420.js and .css), so wiring needs no kit rebuild. ThemeToggle is genuinely imported at app.js:6 and currently unused in the rail. fixLocation:app is correct — pure wiring of an existing kit slot, moving Settings/theme to a pinned bottom utility zone matches the Claude-Desktop rail hierarchy (primary nav top, utility bottom). No kept-typography involvement.
|
|
156
|
+
|
|
157
|
+
### 28. [low | app] Selecting a session row on the chat tab vs history tab does materially different things (resumeInChat vs loadSession) but the rows look identical and live in the same shared ConversationList — there is no visual cue that the same list behaves as a 'resume picker' on chat and an 'event viewer' on history. The shared rail's meaning silently changes per tab, which undercuts the 'one consistent list' feel.
|
|
158
|
+
- **where:** site/app/js/app.js:529-531,563 (onSelect branches on state.tab)
|
|
159
|
+
- **fix:** Make the rail's intent legible per tab: pass a per-row action hint or a sessions-column header label ('Resume a conversation' on chat / 'Browse session events' on history) so the same list's differing behavior is announced rather than inferred. The kit ConversationList head can carry a caption; thread it from the host.
|
|
160
|
+
- **verified:** Verified against the live source. site/app/js/app.js renders the same kit ConversationList for both tabs (lines 520, 551) and onSelect branches on state.tab: resumeInChat({sid}) on chat vs loadSession(...) on history (lines 530-531, 563) — materially different behavior. The shared rail offers no per-tab cue: search.placeholder is the identical 'Search conversations' in both branches, and the kit ConversationList (c:\dev\anentrypoint-design\src\components\sessions.js, params at line 21-23, head at 70-79) exposes no caption/header-label prop — ds-session-head holds only the New button + search input. So the differing intent can only be inferred. The fix is sound and correctly split: add a caption prop to the kit ConversationList head (kit) and thread a per-tab label ('Resume a conversation'/'Browse session events') from app.js (app wiring), matching the documented kit-vs-app discipline and the Claude-Desktop labeled-column bar. No kept-typography is being flagged. Severity low is appropriate (legibility, not a bug).
|
|
161
|
+
|
|
162
|
+
### 29. [low | kit+app] Live tab has its own session-management surface (SessionDashboard cards with open/resume/events/stop) that duplicates the conversation rail's running rows and the chat-tab runningPanel — three different visual treatments of 'in-flight sessions' across tabs (rail running-dot, runningPanel banner rows, dashboard cards). Same concept, three looks, undermining one-product cohesion.
|
|
163
|
+
- **where:** app.js:805-814 (SessionDashboard), app.js:1411-1428 (runningPanel banners), sessions.js:43 (rail running disc)
|
|
164
|
+
- **fix:** Converge on one running-session presentation primitive. Either render SessionCard (the richest form) wherever in-flight sessions appear, or demote runningPanel to a compact card row that shares SessionCard's status-line grammar (status word + disc + agent·model·elapsed + currentTool). Drop the bespoke .resume-banner styling for running rows so the three surfaces read as the same component at different densities.
|
|
165
|
+
- **verified:** All three cited locations verified in live source. sessions.js:43-44 renders running as a bare .status-dot-disc status-dot-live in the rail; app.js:1422-1425 runningPanel renders .resume-banner rows (disc + .lede 'agent · model · elapsed · cwd' + stop Btn); app.js:805 SessionDashboard + sessions.js:83+ SessionCard renders rich cards (status word+disc + agent/model/cwd + elapsed + counter + lastActivity + currentTool + 4 controls). Same 'in-flight session' concept, three distinct treatments and three CSS grammars (.resume-banner is bespoke vs SessionCard's status-line). No kept-typography is being flagged — the gap in fact relies on the middot grammar. Fix is sound and correctly placed: it does NOT force the glanceable rail disc into a card (which would break the rail rhythm); it converges runningPanel onto SessionCard's status-line grammar and drops the bespoke .resume-banner, landing a shared compact-card primitive in the kit plus app wiring. Concrete, source-grounded, and measurably increases one-product cohesion toward the Claude-Desktop bar. Severity low is appropriate.
|
|
166
|
+
|
|
167
|
+
### 30. [medium | kit] Two parallel status-disc systems with DIFFERENT 'on' colors break the single-source status indicator. index.html .status-dot-disc.is-live uses var(--accent) (=--green #247420, dark), while chat.css .status-dot-disc.status-dot-live uses var(--green) too BUT the agentchat-status one (chat.css) uses var(--accent) and the index.html pulse keyframe is hardcoded rgba(80,200,120,...) (a totally different green than either token). So the crumb dot, the session-row running dot, the dashboard card dot, and the agentchat status dot can render three different greens and two different pulse colors for the SAME 'live' state.
|
|
168
|
+
- **where:** site/app/index.html:79-101 (.status-dot-disc + agentgui-pulse hardcoded rgba); chat.css:48-64,235-240; sessions.js:44,106
|
|
169
|
+
- **fix:** Promote ONE canonical .status-dot-disc + .status-dot-live (+ -error,-connecting) to the kit (chat.css already has the base; add is-connecting/is-offline tones and a token-driven pulse keyframe using color-mix(in oklab, var(--accent) ...) NOT hardcoded rgba). Export a Status indicator helper or just rely on the kit class. Then DELETE the index.html .status-dot/.status-dot-disc/.is-* block and the agentgui-pulse keyframe entirely so the crumb dot uses the same kit primitive the rows/cards/composer use. One green, one pulse, everywhere.
|
|
170
|
+
- **verified:** Verified against live source. In the vendored kit bundle site/app/vendor/anentrypoint-design/247420.css the GLOBAL disc (.status-dot-disc.status-dot-live, used by session rows + dashboard cards, line 5059) is `background: var(--green)`, while the agentchat-status disc (.agentchat-status .status-dot-disc.status-dot-live, line 4874) is `background: var(--accent)`. In .ds-247420 context --green resolves to #247420 (index.html line 25) but --accent resolves to var(--accent-bright, var(--green-2)) = #3A9A34 (index.html line 247) — two distinct greens for the same 'live' state. index.html .status-dot.is-live (line 89, NOT .ds-247420-prefixed) uses var(--accent, var(--agentgui-accent, #50c878)) and pulses via the agentgui-pulse keyframe hardcoded to rgba(80,200,120) = #50c878 (lines 94-98) — a third green and a different pulse color than the kit's agentchat-pulse, which is token-driven via color-mix(in oklab, var(--accent)...) (line 4877-4878). So crumb dot / row dot / card dot / composer dot genuinely can render up to three greens and two pulse treatments. The fix is sound and correctly scoped: promote ONE canonical kit primitive (the global .status-dot-disc + tones already exists, line 5055-5060; add connecting/offline tones + token-driven pulse) and delete the index.html .status-dot/.is-* block + agentgui-pulse keyframe so the crumb reuses the kit primitive — fixLocation:kit is right. Not a kept-typography case (these are CSS-drawn discs, not glyphs). Caveat: the cited chat.css/sessions.js line numbers point at the kit SOURCE repo c:\dev\anentrypoint-design (no chat.css exists inside agentgui; it is compiled into 247420.css), which is consistent with fixLocation:kit and does not invalidate the gap.
|
|
171
|
+
|
|
172
|
+
### 31. [medium | kit] rail-green renders a DIFFERENT green between the kit Row and the kit ConversationList row. .row.rail-green::before = var(--accent); .ds-session-row.rail-green::before = var(--green). In dark theme --accent retunes to --green-2 (#3A9A34) via [data-theme]/[data-accent] while --green stays #247420 — so a history-tab session Row (uses .row) and a chat-tab ConversationList row (uses .ds-session-row) show two different 'normal/ok' rail greens side by side in the same shell.
|
|
173
|
+
- **where:** app-shell.css:560 (.row.rail-green=var(--accent)); chat.css:280 (.ds-session-row.rail-green=var(--green)); also rail-purple: app-shell.css uses --purple-2 vs chat.css uses --purple
|
|
174
|
+
- **fix:** Pick one token per rail tone and use it in BOTH sheets. Recommend rail-green=var(--accent), rail-purple=var(--purple-2), rail-flame=var(--flame) (matches the brighter, dark-theme-aware Row). Update chat.css .ds-session-row.rail-* to the same three tokens so the leading rail bar is identical whether a session shows in the ConversationList or the EventList/agents Row.
|
|
175
|
+
- **verified:** Verified against the shipped vendored bundle site\app\vendor\anentrypoint-design\247420.css (the file index.html actually loads): line 961 `.row.rail-green::before{background:var(--accent)}` vs line 5100 `.ds-session-row.rail-green::before{background:var(--green)}`. Token defs (lines 25/26/55): --green=#247420, --green-2=#3A9A34, --accent=var(--green). In ink/dark theme (lines 247 and 274) --accent is retuned to var(--accent-bright,--green-2)=#3A9A34 while --green stays #247420. So a chat-tab ConversationList row (.ds-session-row, always #247420) and a history-tab session/EventList Row (.row, #3A9A34 in dark) show two different 'ok/normal' rail greens in the same shell. rail-purple diverges even harder and in BOTH themes: .row.rail-purple=--purple-2 (#7F18A4 bright magenta) vs .ds-session-row.rail-purple=--purple (#420247 dark plum) (lines 962, 5101). The kit-located fix (unify one token per rail tone across both sheets) is sound and correct placement. Cited line numbers reference the kit source sheets (app-shell.css/chat.css) rather than the concatenated bundle, but the rule values and tokens match exactly. Not a kept-typography case.
|
|
176
|
+
|
|
177
|
+
### 32. [low | kit+app] Inconsistent header casing across surfaces: PageHeader titles are lowercase ('files','live sessions','history','settings') but ConversationList group labels are Title Case ('Today','Yesterday','Running'), the dashboard status words are lowercase ('running'), rail nav items are Title Case ('Chat','History','Files'), and Panel heads are lowercase ('context','backend','agents'). Claude Desktop / cowork pick ONE register (typically sentence/Title for nav+sections, lowercase only for code/meta). The mix reads as two authors.
|
|
178
|
+
- **where:** app.js:456-460 (rail Title Case) vs app.js:749,804,1230,1595 (PageHeader lowercase) vs sessions.js:62 group labels (Title) vs context-pane.js Panel titles (lowercase)
|
|
179
|
+
- **fix:** Decide one policy and enforce it via kit defaults: PageHeader/Panel should NOT lowercase-transform; pass sentence-case titles ('Files','Live sessions','Context'). The kit already uppercases Panel head via CSS text-transform — keep that as the section-label treatment, and make page titles sentence case in the app. Align rail items and group labels to the same sentence-case register. This is wiring (app) for the strings + a kit check that .panel-head text-transform is the single source of section-label casing.
|
|
180
|
+
- **verified:** Confirmed in live source. app.js:456-460 rail items are Title Case ('Chat','History','Files','Live','Settings'); app.js:749,804,1232,1595 pass lowercase PageHeader titles ('files','live sessions','history','settings'); dateGroupLabel/DATE_GROUP_ORDER (app.js:472-482) emit Title Case ('Running','Today','Yesterday','This week','Earlier'); liveMain status is lowercase 'running'/'error' (app.js:799). The kit PageHeader (content.js:256) wraps a plain .ds-section title with NO text-transform (app-shell.css has no lowercase/uppercase on the section title), so the lowercase titles are literal app strings and render lowercase right beside the Title-Case rail and group labels in the same shell -- a genuine two-register mismatch, not a CSS artifact. Distinctly, Panel head (content.js:12 -> app-shell.css:516 text-transform:uppercase) legitimately renders 'backend'->'BACKEND' as a caps section-label register, so it is correctly exempt. Not a kept-typography flag (no middot/ellipsis/dash involved). Fix is sound and lands correctly: pass sentence-case strings to PageHeader in app.js ('Files','Live sessions','History','Settings'), leave Panel-head's CSS uppercase as the single source of section-label casing; rail + group labels already match -- moves the shell to one title register as Claude Desktop does. Severity low is fair.
|
|
181
|
+
|
|
182
|
+
### 33. [low | kit] Empty/loading/error state COPY and tone diverge per surface. ConversationList: 'No conversations yet' (sentence). FileGrid empty: 'empty directory' / 'no files match' (lowercase). SessionDashboard: 'No live sessions' (sentence). History empty: 'No sessions yet' vs 'Select a session…' (sentence). The kit EmptyState default 'nothing here yet' (lowercase). agentchat-empty: 'Start a conversation with X' (sentence). Same product, three capitalization conventions for the same kind of message.
|
|
183
|
+
- **where:** sessions.js:23,134,137; files.js:120,229; app.js:710,808,1237; agent-chat.js:220
|
|
184
|
+
- **fix:** Standardize all kit empty/loading/error default copy to sentence case (capitalize first word, no trailing period) and route every surface's empty text through the kit EmptyState component so the glyph + spacing + tone are identical. Update FileGrid emptyText default and the host-passed strings (files emptyText, dashboard emptyText) to sentence case. One empty-state component, one copy register.
|
|
185
|
+
- **verified:** Confirmed in live source. Same product mixes two capitalization registers for the same class of empty message. LOWERCASE: files.js:120 FileGrid emptyText default 'no files here yet'; files.js:229 EmptyState default 'nothing here'; app.js:710 file overrides 'empty directory' / 'no files match "..."'. SENTENCE CASE: sessions.js:23 'No conversations yet'; sessions.js:132 + app.js:808 'No live sessions...'; agent-chat.js:220-222 'Start a conversation with X' / 'Choose an agent to begin'; app.js:1236-1237 'Select a session to view its events' / 'No sessions yet'. This is a genuine cohesion inconsistency, not a kept-typography false positive (no middot/ellipsis/dash is being flagged as decorative). The core fix is sound and correctly located: sentence-case the lowercase outliers in the kit defaults (files.js FileGrid/EmptyState) and the app-passed file emptyText strings (app.js:710) so all surfaces share one copy register. The secondary 'route every surface through the kit EmptyState component' part is overreaching (ConversationList has keyed-wrapper webjsx constraints; agentchat-empty has a richer title+sub+suggestions structure that EmptyState cannot express) - but that is optional polish; the capitalization-normalization core is real, low-severity, and lands in the right place (kit defaults + app override strings).
|
|
186
|
+
|
|
187
|
+
### 34. [low | kit+app] Files toolbar is built ad-hoc in the app with raw class strings instead of the kit FileToolbar, and the roots picker reuses the history-tab .pill (an app-local class), so the Files surface mixes kit (FileGrid/BreadcrumbPath) with app-hand-rolled chrome — a kit-of-parts seam on exactly the surface fsbrowse sets the bar for. fsbrowse's reference bar keeps crumb+filter+sort+actions in one coherent toolbar.
|
|
188
|
+
- **where:** app.js:735-746 (h('div',{class:'ds-file-toolbar'}) hand-built; pillButton for roots) — FileToolbar exists unused in files.js:181
|
|
189
|
+
- **fix:** Use the kit FileToolbar (left:[BreadcrumbPath], right:[use-as-cwd Btn]) instead of hand-writing .ds-file-toolbar in the app, and add a `roots` prop to FileGrid or a kit RootsPicker (segmented control) so the allowed-root switcher is a kit primitive, not the history .pill. Keeps Files entirely kit-composed; app passes data only.
|
|
190
|
+
- **verified:** Verified against live source. app.js:741-746 hand-builds h('div',{class:'ds-file-toolbar'}) with -left/-right children, while the kit's FileToolbar (c:\dev\anentrypoint-design\src\components\files.js:181-186) emits byte-identical markup and is unused — a real kit-of-parts seam on the Files surface. The roots picker (app.js:735-738) uses pillButton (app.js:123-132, class 'pill lede'/'pill-active') over .pill-row, and .pill/.pill-row are app-local CSS in index.html:120-133, shared with the history-tab project filter (app.js:1501) — so the allowed-root switcher borrows a history-tab class rather than a kit primitive. Not a kept-typography misfire (no middot/ellipsis/dash flagged). The fix is sound and correctly placed (kit+app): swap to kit FileToolbar and add a kit RootsPicker/segmented control. Caveat lowering confidence on the toolbar half: it's a no-op visually (app already emits the exact DOM FileToolbar would), so that half is pure hygiene; the roots-picker half is the genuine, low-severity cohesion gap with a concrete kit fix, which makes the overall gap real.
|
|
191
|
+
|
|
192
|
+
### 35. [low | kit+app] ContextPane shows token usage on the chat tab, but there is NO equivalent context surface on Files/Live/History — the right pane simply vanishes (ws-no-pane), so the shell width reflows between tabs (4-col chat vs 2-3-col elsewhere). Claude Desktop keeps a stable right-pane affordance (collapsed, not absent) so the canvas doesn't jump. The reflow is a subtle 'different layout per tab' tell rather than one stable shell.
|
|
193
|
+
- **where:** app.js:441-448 (pane only when tab==='chat'); WorkspaceShell ws-no-pane removes the column entirely
|
|
194
|
+
- **fix:** Either always render a (collapsible, default-collapsed) pane so the grid column persists and content slides rather than reflows, OR give Files a small file-details pane and History an event-detail/session-meta pane so the pane is a consistent surface, not a chat-only feature. Minimal version: app passes a lightweight ContextPane (session meta on history, dir stats on files) so the 4-column rhythm holds across tabs.
|
|
195
|
+
- **verified:** Verified against live source. app.js:441 renders the pane ONLY when state.tab==='chat' (const pane = state.tab === 'chat' ? ContextPane({...}) : null), and sessions only on chat/history (line 438). In the kit CSS (vendor/anentrypoint-design/247420.css:3160-3161) .ws-no-pane drops the pane grid column entirely (grid-template-columns: var(--ws-rail-w) ... with the pane area removed), so the content column genuinely reflows between tabs — chat shows rail+sessions+content+pane while files/live/settings collapse to rail+content. This is a real different-layout-per-tab tell, distinct from the kept-typography set. The fix is sound and well-targeted: the kit ALREADY has the collapsed-but-present primitive (.ws-pane-collapsed sets --ws-pane-w:0px at 247420.css:3163, keeping the column), so app.js could always pass a pane (default-collapsed, or a lightweight session-meta/dir-stats surface per tab) to hold the column rhythm — work that correctly splits kit (already supports it) + app (wiring). Severity is correctly low (cosmetic shell stability, not a bug), but the gap genuinely moves the product toward Claude Desktop's stable-canvas behavior.
|
|
196
|
+
|
|
197
|
+
### 36. [low | app] WorkspaceRail has a `footer` slot designed for the ThemeToggle (the rail brand+action+nav+foot pattern mirrors Claude Desktop's pinned-bottom settings/theme), but the app never passes one — ThemeToggle is buried inside the Settings 'appearance' Panel. So theme switching has no persistent affordance and the rail bottom (ws-rail-foot, fully styled) is dead. A real desktop app surfaces theme at the chrome edge.
|
|
198
|
+
- **where:** app.js:462-466 (workspaceRail passes no footer); shell.js:465 + app-shell.css:2820 (ws-rail-foot styled, unused)
|
|
199
|
+
- **fix:** Pass footer: ThemeToggle({}) (or a compact theme IconButton) into WorkspaceRail so the rail foot is alive and theme is reachable from every tab, matching the pinned-bottom rail convention. Pure wiring — the kit slot + CSS already exist.
|
|
200
|
+
- **verified:** Verified against live source. app.js:462-466 workspaceRail() passes only {brand, action, items} — no footer (confirmed read). Kit shell.js:438-439 documents and accepts a `footer` prop and at line 465 renders `.ws-rail-foot` only when footer is truthy. app-shell.css:2820 fully styles `.ws-rail-foot { margin-top:auto; padding-top; border-top }` — pinned to rail bottom, but nothing ever populates it, so it is dead. ThemeToggle is a real kit export (components.js:61, supports compact mode) and is currently used exactly once in app.js (line 1636, inside the Settings appearance panel) — so theme switching has no persistent affordance outside the Settings tab. Fix `footer: ThemeToggle({compact:true})` is sound, pure app wiring in the right place (app.js), reusing existing kit slot + CSS, and moves toward the Claude-Desktop pinned-bottom rail convention. Not a kept-typography flag.
|
|
201
|
+
|
|
202
|
+
### 37. [high | kit] Per-message action row is hover-only and stays hidden + sub-44px on touch (420px)
|
|
203
|
+
- **where:** C:\dev\anentrypoint-design\chat.css:147-155 (.chat-msg-actions opacity:0; .chat-msg-action 26px)
|
|
204
|
+
- **fix:** copy/retry/edit only appear on :hover/:focus-within and the buttons are 26x26. On a touch device there is no hover, so the actions are unreachable, and even on desktop 26px misses the 44x44 target. Add a `@media (hover:none)` / `<=640px` rule that sets `.chat-msg-actions{opacity:1}` and bumps `.chat-msg-action` to min 44x44 (or 36px with larger hit-area padding); claude.ai/code and Claude Desktop keep message actions reachable on touch (long-press / always-visible row).
|
|
205
|
+
- **verified:** Verified against C:\dev\anentrypoint-design\chat.css lines 146-155. `.chat-msg-actions` is `opacity:0` and only revealed by `.chat-msg:hover`/`:focus-within` (line 147-148); `.chat-msg-action` is exactly `width:26px; height:26px` (line 151). A grep confirms NO `@media (hover:none)` or `<=640px` fallback exists anywhere in the file. On a touch device (no hover, no convenient focus on a non-input element) the copy/retry/edit actions are genuinely unreachable, and 26px is well under the 44x44 touch target. The fix correctly lands in the kit (chat lives in anentrypoint-design per AGENTS.md, vendored into site/app) and the proposed media-query (set opacity:1 + bump to 44px under hover:none/<=640px) is sound. No kept-typography (middot/ellipsis/dash) is involved. Reference products keep message actions reachable on touch.
|
|
206
|
+
|
|
207
|
+
### 38. [medium | kit] Multiple interactive controls below the 44x44 touch target on mobile
|
|
208
|
+
- **where:** C:\dev\anentrypoint-design\app-shell.css:1173-1178 (.ds-file-act 28px, persists at 860-881 mobile), :1086-1092 (.ds-file-sort-btn ~18px), :1079-1084 (.ds-file-filter-input); chat.css:149-152 (.chat-msg-action 26px)
|
|
209
|
+
- **fix:** The file-row action buttons (28px), sort-column header buttons (2px/4px padding), filter input, and message-action buttons all stay under 44px at <=640px even though .btn correctly gets min-height:44px there. Add the same touch-target floor to .ds-file-act, .ds-file-sort-btn, .ds-file-filter-input, .ds-crumb-seg, .chat-msg-action inside the <=640px block so every tappable control in Files + chat meets WCAG 2.5.5; this is the cohesion gap (the shell buttons are touch-sized, the kit-surface buttons are not).
|
|
210
|
+
- **verified:** Verified against live source. app-shell.css: .ds-file-act is 28x28 (lines 1173-1178), .ds-file-sort-btn is font-tiny + 2px/4px padding (~18px, 1086-1092), .ds-file-filter-input 8px vertical padding (~32px, 1079-1084). chat.css: .chat-msg-action is 26x26 (149-152). The mobile block at app-shell.css:799 (@media max-width:600px) gives .btn/.btn-primary/.btn-ghost min-height:44px (864-866) but never touches the file-act/sort/filter controls; the chat.css @media max-width:640px blocks (226, 316) never touch .chat-msg-action. So the cohesion gap is genuine: shell buttons are touch-sized at mobile, these kit-surface controls are not, failing WCAG 2.5.5/coarse-pointer. Fix location (kit) is correct and these are not kept-typography glyphs. Two minor caveats: the gap cites a <=640px block for app-shell.css but that file's mobile breakpoint is actually max-width:600px (chat.css does use 640px), and .ds-crumb-seg was not separately verified; neither affects soundness - the fix should add a touch-target floor inside the existing app-shell.css 600px block and chat.css 640px block (or a pointer:coarse query).
|
|
211
|
+
|
|
212
|
+
### 39. [medium | app] Live SSE burst re-renders the ENTIRE EventList + ConversationList per coalesced frame
|
|
213
|
+
- **where:** C:\dev\agentgui\site\app\js\app.js:354 (scheduleRender per event) -> 1299-1336 (EventList rebuilds up to 300 rows) ; openLiveStream pushes to state.events then full render()
|
|
214
|
+
- **fix:** scheduleRender already rAF-coalesces, and the expanded-body JSON.stringify is correctly gated to expanded rows only - but every coalesced frame still rebuilds the full 300-row EventList vnode array AND the grouped ConversationList AND the SessionDashboard. During a fast tool-streaming session this is per-frame O(n) over hundreds of rows. The 3s active poll is already change-diffed (activeSig - good). Apply the same discipline to live: only rebuild the EventList when state.events actually changed for the SELECTED sid (the SSE handler already knows data.sid===selectedSid), and skip the ConversationList rebuild on pure event-counter bumps (counters can update via a cheaper targeted path or a debounced session-list refresh). The reference bar (Claude Desktop live view) stays smooth under a multi-tool burst.
|
|
215
|
+
- **verified:** Verified against app.js. The SSE live stream is opened ONLY on the history tab (navTo: `if (tab==='history') openLiveStream()`, line 217-219; closed on leave 220-221). Each 'event' frame for the selected sid pushes to state.events and calls scheduleRender() (line 327-354), which rAF-coalesces into render(). On the history tab render() rebuilds BOTH: (1) the EventList items array via a full `.map` over up to `eventsLimit` (~300) rows every frame (lines 1300-1335 — confirmed; only the expanded-body JSON.stringify is gated, the row vnode array is not), and (2) sessionsColumn()/ConversationList, since `sessions` is built whenever tab is chat||history (line 438), including the sessionGroups() bucket pass. During a fast multi-tool burst this is per-frame O(n) JS object construction over hundreds of event rows plus the session list, on top of webjsx's diff. The 3s active poll is correctly change-diffed via activeSig (line 268-274), so the discipline to apply already exists in-repo. The fix is app-local (memoize/dirty-flag the EventList vnode array on real state.events change for the selected sid; skip ConversationList rebuild on pure counter bumps) — correct location, no kit/server change, mirrors activeSig. Minor overstatement: SessionDashboard is the `live` tab and is NOT rendered during history SSE streaming, so that part of the claim is inert; the core EventList+ConversationList claim is real. Not a kept-typography flag.
|
|
216
|
+
|
|
217
|
+
### 40. [medium | kit] Files grid keyboard listbox uses roving focus but rows are not aria-selected and have no aria-activedescendant linkage
|
|
218
|
+
- **where:** C:\dev\anentrypoint-design\src\components\files.js:135-165 (role=listbox, ArrowUp/Down move DOM focus over .ds-file-open buttons)
|
|
219
|
+
- **fix:** The grid is role=listbox with tabindex=0 and arrow-key roving over child <button> elements, but the buttons carry no role=option and the listbox exposes no aria-activedescendant, so AT announces 'files, listbox' then a button - the option semantics are missing. Either make each .ds-file-open role=option with aria-selected reflecting the focused row and wire aria-activedescendant on the grid, OR drop role=listbox and treat it as a plain focusable grid of buttons (role=grid/gridcell or just a list). Current state is a half-applied listbox pattern - a primitive edge vs the polished fsbrowse keyboard nav.
|
|
220
|
+
- **verified:** Confirmed in C:\dev\anentrypoint-design\src\components\files.js. Line 154 renders the grid as role="listbox" with tabindex="0" and an onKeyDown (lines 135-145) that roves DOM focus over .ds-file-open buttons. But FileRow (lines 54-77) renders each row as a plain div containing a button.ds-file-open with NO role="option" and NO aria-selected, and the listbox carries no aria-activedescendant. This is a genuinely half-applied ARIA listbox pattern: per the listbox spec a listbox needs role=option children plus either aria-activedescendant or roving tabindex over the options; here focus roves over generic buttons, so AT announces "files, listbox" then an unselected generic button. The fix is sound and correctly placed in the kit (files.js owns FileGrid/FileRow); both proposed remedies are valid, and dropping role=listbox is arguably the cleaner one since the buttons are aria-pressed toggles rather than true single-select options. Not a kept-typography false flag.
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
Source facts confirmed (`ws-pane-collapsed` exists so the column can persist; the two divergent disc colors `var(--green)` vs `var(--accent)` are real). Here is the synthesized plan.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
# agentgui + anentrypoint-design — One-Product Cohesion Build Plan
|
|
230
|
+
|
|
231
|
+
35 confirmed gaps, ordered so every shared kit component is touched ONCE (all its changes batched), then app wiring, then server, then a single build->vendor->witness. Kit changes are listed first because they unblock the app wiring that consumes the new props.
|
|
232
|
+
|
|
233
|
+
## Highest-leverage few (do these even if nothing else)
|
|
234
|
+
1. **Code-block per-block copy button** (CodeNode/MdNode) — the single biggest "reads as primitive" gap vs claude.ai/code.
|
|
235
|
+
2. **Stable three-pane frame across tabs** (WorkspaceShell always-pane/always-sessions + app always passes them) — the shell "breathing" 4/3/2 columns is the loudest "separate pages" tell; the kit already has `ws-pane-collapsed` (col persists at width 0) so this is a default-collapsed wiring change, not a redesign.
|
|
236
|
+
3. **One canonical status disc** (kill the index.html `.status-dot`/`agentgui-pulse` block + unify `rail-green`/`rail-purple` tokens) — confirmed three greens / two pulses for the same "live" state; fixes the single most pervasive visual inconsistency.
|
|
237
|
+
4. **busy-vs-stuck "stale" status** on live cards — high-severity oversight gap; turns the dashboard from a liar into a triage surface.
|
|
238
|
+
5. **File preview into the ContextPane** (split view, not modal) — high-severity Files cohesion gap; reuses the now-always-present pane from #2.
|
|
239
|
+
6. **Touch-target + hover-on-touch fixes** (chat message actions, file actions/sort/filter) — high-severity a11y; message actions are literally unreachable on touch today.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## PHASE 1 — KIT (`c:\dev\anentrypoint-design`), batched per component
|
|
244
|
+
|
|
245
|
+
Components edited exactly once. Items marked **[parallel]** touch disjoint files and can be done concurrently; **[serial-within-file]** must be batched into one edit pass per file.
|
|
246
|
+
|
|
247
|
+
### A. `src/components/chat.js` (ChatMessage / CodeNode / MdNode / ChatComposer) — batch ALL of:
|
|
248
|
+
- **A1 (high)** `MdNode.refSink`: after `innerHTML`, `querySelectorAll('pre')` -> wrap in `position:relative` container + inject `.chat-code-copy` (clipboard write, label copy->copied ~1.6s, aria-label, >=24px hit area, focus-visible ring, reveal on `.chat-code:hover/:focus-within`).
|
|
249
|
+
- **A2 (low)** Add the SAME copy control to the structured `CodeNode` `.chat-code-head` (currently lang/name only). *(Shares the copy primitive with A1 — write one helper, call from both.)*
|
|
250
|
+
- **A3 (low)** `actionRow`: render a text label/reliable tooltip beside the icon; add a `copy`/`clipboard` icon to `ICON_PATHS` (shell.js) and map copy to it instead of `page`. Keep 26px buttons but bump to >=24px (touch sizing handled in CSS, F-block).
|
|
251
|
+
- **A4 (low)** Accept + forward an `avatar` prop through to the disc (kit already computes a fallback initial; just thread the explicit prop). *(pairs with app gap "stable product mark".)*
|
|
252
|
+
- **A5 (medium)** `ChatComposer`: optional `.chat-composer-context` line (`agent · model · cwd-basename`, clickable, kept middot) — host passes strings + onClick.
|
|
253
|
+
- Conflict note: A1 and A3 both edit ChatMessage render; A5 edits ChatComposer in the same file — do as ONE pass.
|
|
254
|
+
|
|
255
|
+
### B. `src/components/agent-chat.js` (AgentChat) — batch ALL of:
|
|
256
|
+
- **B1 (low)** Streaming md->text fence handling: if streaming text contains a code fence, render the bubble as a monospaced `<pre>` shell (no Prism mid-stream) so it doesn't reflow prose->block on settle. Refines the `mdShell:true` downgrade branch (line 167).
|
|
257
|
+
- **B2 (low)** New `followups` prop -> chip row below last settled assistant turn (reuse `.agentchat-empty-suggestion` CSS), shown only when `!busy` and last msg is assistant.
|
|
258
|
+
- **B3 (kit half of A4/A5)** Thread `avatar` and the composer-context strings through from host into ChatMessage / ChatComposer.
|
|
259
|
+
- Conflict note: B1, B2, B3 all live in the same render path — ONE pass.
|
|
260
|
+
|
|
261
|
+
### C. `src/components/sessions.js` (SessionDashboard / SessionCard / ConversationList) — batch ALL of:
|
|
262
|
+
- **C1 (high)** SessionCard accepts `status:'stale'` -> `.ds-dash-status.is-stale` (word `idle`/`waiting`) + non-animated `.status-dot-stale` disc.
|
|
263
|
+
- **C2 (medium)** SessionDashboard `.ds-dash-header` gains an optional toolbar: sort `Select` (status|elapsed|last activity|errors), filter `SearchInput` (use `label:` not `aria-label`), errors-only toggle; emit `onSort`/`onFilter`; render header even when sessions present.
|
|
264
|
+
- **C3 (medium)** SessionDashboard/SessionCard selectable mode: per-card checkbox (role=checkbox/aria-checked, 44x44), `selected:Set` prop, `onToggleSelect(sid)`; header flips `N running / stop all` -> `N selected / stop selected`.
|
|
265
|
+
- **C4 (medium)** SessionDashboard `streamState` prop (connected|connecting|lost) -> a status line in the header ("listening for activity" / "live stream lost - retrying").
|
|
266
|
+
- **C5 (low)** SessionCard `active:boolean` -> `.ds-dash-card.is-active` (accent inset border, aria-current). Collapse redundant open/resume into one `open` action; keep `events`.
|
|
267
|
+
- **C6 (low)** ConversationList head gains a `caption` prop (host passes per-tab label "Resume a conversation" / "Browse session events").
|
|
268
|
+
- **C7 (low)** Empty-copy: sentence-case `ConversationList` empty default (already "No conversations yet" — keep), used by E-block normalization.
|
|
269
|
+
- Conflict note: C1–C5 all touch SessionCard/SessionDashboard render; ONE pass. Keep webjsx keying discipline (filter(Boolean), keyed wrappers).
|
|
270
|
+
|
|
271
|
+
### D. `src/components/files.js` (FileGrid / FileRow / FileToolbar / DropZone) — batch ALL of:
|
|
272
|
+
- **D1 (medium)** Large-dir cap + "show N more" row (render first ~200, kept ellipsis) + aria-live shown/total count (mirror History `load N older`). This also fixes the roving-tabindex O(n) scan.
|
|
273
|
+
- **D2 (medium)** Listbox a11y: make each `.ds-file-open` `role=option` + `aria-selected` on focused row + `aria-activedescendant` on the grid — OR drop `role=listbox` for a plain button list. Pick one (dropping is simpler given buttons-are-actions).
|
|
274
|
+
- **D3 (low)** FileRow `permissions`/`locked` prop -> ASCII `read-only`/`no access` meta tag (kept middot), disable `onOpen` for EACCES.
|
|
275
|
+
- **D4 (low/hygiene)** Add a `roots`/RootsPicker segmented-control primitive (so app stops borrowing the history `.pill`). (FileToolbar already emits identical markup to the app's hand-built toolbar — swapping is a no-op for D, just enables the app-side D wiring.)
|
|
276
|
+
- **D5 (low)** FileGrid empty default copy -> sentence case (E-block).
|
|
277
|
+
- Conflict note: D1–D5 same file; ONE pass.
|
|
278
|
+
|
|
279
|
+
### E. `src/components/files-modals.js` (FileViewer / FilePreviewCode) + new FilePreviewPane:
|
|
280
|
+
- **E1 (high)** New `FilePreviewPane` wrapper (header name/size/path + close + download) distinct from the overlay `FileViewer`, for the WorkspaceShell pane (split view). Keep `FileViewer` as <900px modal fallback.
|
|
281
|
+
- **E2 (medium)** `onPrev`/`onNext` props on FileViewer/FilePreviewPane (ASCII prev/next) + ArrowLeft/Right keybinding.
|
|
282
|
+
- **E3 (low)** `FilePreviewCode` gains a filename/lang header matching CodeNode's `.chat-code-head` (header ONLY — do NOT add a copy button per the verdict, which would make preview more polished than chat unless A1/A2 also shipped; since A1/A2 DO ship this pass, you MAY add the same copy control here for full consistency — decide once).
|
|
283
|
+
|
|
284
|
+
### F. `src/components/context-pane.js` (ContextPane):
|
|
285
|
+
- **F1 (medium)** Empty state: when no agent AND no usage AND no session -> single `.ds-context-empty` line ("No active conversation — start a chat to see context here") instead of 4 placeholder rows.
|
|
286
|
+
|
|
287
|
+
### G. `shell.js` (WorkspaceShell / WorkspaceRail) + `ICON_PATHS`:
|
|
288
|
+
- **G1 (high)** Stable frame: when a tab has no real second column, render a persistent collapsed placeholder track (reuse `ws-pane-collapsed` width:0 — already exists, line 286/366) rather than `ws-no-pane` removing the grid track. Preserve the <900px single-column collapse (CSS).
|
|
289
|
+
- **G2 (A3 dependency)** Add `copy`/`clipboard` icon to `ICON_PATHS`.
|
|
290
|
+
- (WorkspaceRail `footer` slot already exists at line 465 — no kit change; pure app wiring.)
|
|
291
|
+
|
|
292
|
+
### H. CSS (`chat.css`, `app-shell.css` compiled into the bundle) — batch ALL of:
|
|
293
|
+
- **H1 (medium)** **Single canonical status disc.** Promote one `.status-dot-disc` + `.status-dot-live/-error/-connecting/-stale` to kit; pulse via token `color-mix(in oklab, var(--accent)…)` not hardcoded rgba; pulse only `-live`. Reduced-motion guard `@media (prefers-reduced-motion:reduce){animation:none}`. (Unblocks app deleting index.html's block.)
|
|
294
|
+
- **H2 (medium)** **Unify rail-tone tokens** across BOTH sheets: `rail-green=var(--accent)`, `rail-purple=var(--purple-2)`, `rail-flame=var(--flame)` — make `.ds-session-row.rail-*` match `.row.rail-*`.
|
|
295
|
+
- **H3 (medium)** Live dashboard pulse: animate `.ds-dash-card .status-dot-disc.status-dot-live` (reuse keyframes), NOT `-stale`/`-error`, reduced-motion guarded. (There is no `.status-dot-stale` today — H1 adds it.)
|
|
296
|
+
- **H4 (high)** Touch: `@media (hover:none)` / `<=640px` (chat.css) + `<=600px` (app-shell.css, note the real breakpoint differs) set `.chat-msg-actions{opacity:1}` and floor `.chat-msg-action`, `.ds-file-act`, `.ds-file-sort-btn`, `.ds-file-filter-input`, `.ds-crumb-seg` to 44x44 (or 36px + hit-area padding). Pointer:coarse query preferred.
|
|
297
|
+
- **H5 (low)** CSS for new classes: `.chat-code-copy`, `.chat-composer-context`, `.ds-dash-status.is-stale`, `.status-dot-stale`, `.ds-dash-card.is-active`, `.ds-context-empty`, selectable checkbox, FilePreviewPane, FileToolbar/RootsPicker, followup chips.
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## PHASE 2 — APP WIRING (`C:\dev\agentgui\site\app\js\app.js`) — after kit props exist
|
|
302
|
+
|
|
303
|
+
Mostly independent of each other **[parallel]** except where they touch the same function.
|
|
304
|
+
|
|
305
|
+
- **W1 (high, view())** Always pass `sessions` AND `pane` to WorkspaceShell on every tab (default-collapsed where empty), so the column tracks hold (consumes G1). Files left col = roots/recent-dirs; Live left col = running list; pane: Files=selected-file metadata/preview (W5), History=session meta, Live=aggregate counts, Settings=health summary.
|
|
306
|
+
- **W2 (high, liveMain)** Derive `status:'stale'` when `Date.now()-sess.last > ~45s && !currentTool` (consumes C1). Note: app currently only emits error/running — add stale here.
|
|
307
|
+
- **W3 (medium, liveMain)** Float `error` then `stale` sessions to the front (stable sort) before passing to SessionDashboard.
|
|
308
|
+
- **W4 (medium, liveMain + openLiveStream)** Per-sid live tally keyed by sessionId independent of history index (increment in SSE handler even when `sessionsBySid` has no row — keep `debouncedRefreshHistory`, don't early-return-drop). Read counter/last from this tally so brand-new sessions show motion.
|
|
309
|
+
- **W5 (high, filesMain)** Render preview INTO the WorkspaceShell pane via `FilePreviewPane` (E1); modal only as <900px fallback.
|
|
310
|
+
- **W6 (medium, filesMain)** Compute previewable (non-dir) neighbors in sorted+filtered list; wire `onPrev/onNext` + Arrow keys (E2).
|
|
311
|
+
- **W7 (medium, filesMain)** Wire `onAction` for at least `download` (server S1); rename/delete via kit ConfirmDialog/PromptDialog if scope confirmed, else document the cut.
|
|
312
|
+
- **W8 (low/medium, filesMain)** Swap hand-built `.ds-file-toolbar` -> kit FileToolbar; roots picker -> kit RootsPicker (D4). Pass `permissions` to rows (D3).
|
|
313
|
+
- **W9 (medium, filesMain/loadDir)** Always show allowed-root context (even singular); translate 403/EACCES to plain copy (map on HTTP status / "forbidden:" prefix — may need backend.js to carry `r.status`).
|
|
314
|
+
- **W10 (low, loadDir/hash router)** Add `dir=` to the existing tab=/sid= hash scheme + popstate restore for directory nav (don't use fsbrowse's bare `#/path`).
|
|
315
|
+
- **W11 (medium, liveMain + view dot)** Open SSE stream on `navTo('live')` too; pass `streamState` to SessionDashboard (C4). Unify crumb dot vocab: always connection word primary, append live count/reconnects (middot) only on history (consumes nothing kit-side).
|
|
316
|
+
- **W12 (medium, liveMain)** Hold `state.live.sort/filter` + `state.live.selected` Set; wire `onSort/onFilter` (C2) and `onStopSelected` (C3).
|
|
317
|
+
- **W13 (low, liveMain)** Pass `active` per card = `r.sessionId === state.chat.resumeSid` (C5); collapse open/resume.
|
|
318
|
+
- **W14 (low, chatMain)** Pass per-agent `avatar` (small line-SVG Icon) to AgentChat (A4/B3); pass composer-context strings + onClick (A5/B3).
|
|
319
|
+
- **W15 (low, app.js)** Seed static `followups` (e.g. "explain this","show me the diff","run the tests") to AgentChat (B2).
|
|
320
|
+
- **W16 (medium, perf)** Memoize/dirty-flag the EventList vnode array on real `state.events` change for selected sid; skip ConversationList rebuild on pure counter bumps (mirror existing `activeSig` discipline).
|
|
321
|
+
- **W17 (low, sessionsColumn)** Pass per-tab `caption` to ConversationList (C6).
|
|
322
|
+
- **W18 (low, workspaceRail)** Pass `footer: ThemeToggle({compact:true})` and pin Settings there (existing kit slot + CSS; ThemeToggle already imported app.js:6).
|
|
323
|
+
- **W19 (low, PageHeader calls)** Sentence-case page titles ("Files","Live sessions","History","Settings"); align rail/group-label register. Leave Panel-head CSS uppercase as the single section-label source.
|
|
324
|
+
- **W20 (low/medium, runningPanel)** Converge runningPanel onto SessionCard status-line grammar; drop bespoke `.resume-banner` for running rows.
|
|
325
|
+
- **W21 (after H1)** DELETE index.html `.status-dot`/`.status-dot-disc`/`.is-*` block + `agentgui-pulse` keyframe so crumb uses the kit primitive.
|
|
326
|
+
|
|
327
|
+
Conflict cluster: W1, W2, W3, W4, W11, W12, W13 all edit `liveMain` -> do as one coherent pass. W5–W10 all edit `filesMain`/`loadDir` -> one pass. W14/W15 edit `chatMain` -> one pass.
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## PHASE 3 — SERVER (`C:\dev\agentgui\lib\http-handler.js`)
|
|
332
|
+
|
|
333
|
+
Independent of all GUI work **[parallel with Phase 1/2]**; only the app WIRING (W7/W8/W9) depends on these landing.
|
|
334
|
+
|
|
335
|
+
- **S1 (medium)** Add `permissions` (`['read','write']` | `'EACCES'`) per `/api/list` entry — replace the swallowing `catch(_){}` at the stat (line ~232) so EACCES is reported, not size:null. (mirrors fsbrowse `checkPermissions`).
|
|
336
|
+
- **S2 (medium)** Add confined `/api/download` (raw bytes) via `confineToRoots()`. Optionally `/api/rename` + `DELETE /api/file` (only if file-manager scope is confirmed; else document the cut consistent with prior scope cuts).
|
|
337
|
+
- **S3 (optional)** Path-confined `/api/upload` (Busboy, realpath-confined like `/api/list`) IF uploads are in scope — the existing `lib/routes-upload.js` endpoint is keyed to the legacy conversationId model and is NOT reusable for the allowlist path browser. Otherwise record as deliberate scope cut.
|
|
338
|
+
- **S4 (W9 dependency)** Optionally widen `backend.js` to carry `r.status` so the app can map 403/404 -> plain copy.
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
## PHASE 4 — BUILD -> VENDOR -> WITNESS (single, last)
|
|
343
|
+
|
|
344
|
+
1. `node scripts/build.mjs` in `c:\dev\anentrypoint-design` (must pass `lint-tokens` + `lint-glyphs` — the new ASCII prev/next, read-only tags, copy labels keep that green).
|
|
345
|
+
2. Copy `dist/247420.{js,css}` -> `site/app/vendor/anentrypoint-design/`.
|
|
346
|
+
3. Witness `localhost:3000/gm/`: 0 console errors; per-block code copy works; stable column tracks across chat/history/files/live/settings (no horizontal jump); single green + single pulse everywhere (crumb=row=card=composer); stale card reads `idle` with non-pulsing disc; file preview opens in the pane with prev/next; 420px touch — message actions visible + 44px, file actions 44px; whole-DOM decorative-glyph sweep count:0 (keep middot/ellipsis/dash).
|
|
347
|
+
4. Push the kit so unpkg stays in sync.
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## Quick reference: batching / parallelism / conflicts
|
|
352
|
+
|
|
353
|
+
- **Batch (edit once):** chat.js [A1-A5], agent-chat.js [B1-B3], sessions.js [C1-C7], files.js [D1-D5], files-modals.js [E1-E3], CSS [H1-H5]. App: liveMain [W1-W4,W11-W13], filesMain [W5-W10], chatMain [W14-W15].
|
|
354
|
+
- **Parallelizable (disjoint files):** kit components A/B/C/D/E/F/G are independent of each other; Phase-3 server is independent of Phase-1 kit; H (CSS) can proceed alongside the JS component work.
|
|
355
|
+
- **Conflicts/ordering:** H1 must land before W21 (delete index.html disc). G1 before W1. Each kit prop (C1/C4/C5/E1/E2/D3/A4/A5/B2/C6) must exist before its app consumer. All kit + server before the single Phase-4 build.
|
|
356
|
+
- **Shared primitives — write once, reuse:** the clipboard-copy helper (A1+A2+E3), the canonical status-disc CSS (H1 feeds crumb/row/card/composer/stale), the SessionCard status-line grammar (C1 feeds W20 runningPanel convergence).
|
|
357
|
+
- **Scope decisions to make explicitly (don't leave dangling):** file rename/delete (S2), upload/DropZone (S3), retry-on-any-assistant-turn (A3) — ship or document as a cut in the punch-list, consistent with the prior inline-split-pane cut.
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## Scope decisions (this run) — documented, not dangling
|
|
362
|
+
|
|
363
|
+
Consistent with the prior inline-split-pane cut convention:
|
|
364
|
+
|
|
365
|
+
- **File rename / delete (S2 mutation endpoints): CUT.** agentgui's file surface is a read-only browser into allowlisted roots. Adding mutation endpoints (`/api/rename`, `DELETE /api/file`) widens the attack surface (a confined-path write/delete) for a feature the workflow rated medium. Only the mutation-free `download` row action is wired (new confined `/api/download`). The kit `FileRow` still renders rename/delete affordances when a host wires `onAction` for them; agentgui wires only `download`.
|
|
366
|
+
- **Upload / DropZone (S3): CUT.** The legacy `lib/routes-upload.js` endpoint is keyed to the old conversationId model and is not reusable for the allowlist path browser; a new confined upload endpoint is out of scope for a read-only browser. The kit `DropZone`/`UploadProgress` remain available for other consumers.
|
|
367
|
+
- **Retry-on-any-assistant-turn (A3): KEPT as last-turn-only.** Retrying a mid-thread turn would require dropping everything after it (a destructive edit of history); the last-turn retry + edit-and-resend cover the real need. The kit `ChatMessage` action row is general; agentgui gates retry to the last assistant turn.
|
package/lib/http-handler.js
CHANGED
|
@@ -229,9 +229,21 @@ export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, ser
|
|
|
229
229
|
const dirents = fs.readdirSync(normalizedPath, { withFileTypes: true });
|
|
230
230
|
const entries = dirents.map((d) => {
|
|
231
231
|
const full = path.join(normalizedPath, d.name);
|
|
232
|
-
let size = null, modified = null;
|
|
233
|
-
try {
|
|
234
|
-
|
|
232
|
+
let size = null, modified = null, permissions;
|
|
233
|
+
try {
|
|
234
|
+
const s = fs.statSync(full); size = s.isDirectory() ? null : s.size; modified = s.mtime.toISOString();
|
|
235
|
+
// Per-entry permission probe (mirrors fsbrowse checkPermissions) so
|
|
236
|
+
// the row reads honestly (read-only / no access) instead of a silent
|
|
237
|
+
// size:null on a stat failure. A stat success means at least read.
|
|
238
|
+
const perms = ['read'];
|
|
239
|
+
try { fs.accessSync(full, fs.constants.W_OK); perms.push('write'); } catch (_) {}
|
|
240
|
+
permissions = perms;
|
|
241
|
+
} catch (e) {
|
|
242
|
+
// Could not stat (commonly EACCES): mark no-access so the client
|
|
243
|
+
// disables open + shows the tag, rather than failing silently.
|
|
244
|
+
permissions = e && e.code === 'EACCES' ? 'EACCES' : [];
|
|
245
|
+
}
|
|
246
|
+
return { name: d.name, type: typeFor(d.name, d), size, modified, path: full, permissions };
|
|
235
247
|
}).sort((a, b) => (a.type === 'dir' ? 0 : 1) - (b.type === 'dir' ? 0 : 1) || a.name.localeCompare(b.name));
|
|
236
248
|
// Breadcrumb segments from the absolute path (drive/root aware).
|
|
237
249
|
const segments = normalizedPath.split(/[\\\/]+/).filter(Boolean);
|
|
@@ -291,6 +303,40 @@ export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, ser
|
|
|
291
303
|
return;
|
|
292
304
|
}
|
|
293
305
|
|
|
306
|
+
// Confined raw-bytes download (any type) with an attachment disposition,
|
|
307
|
+
// so the Files view can offer download on a row. Same allowlist + realpath
|
|
308
|
+
// confinement as /api/file and /api/image - never a generic file reader.
|
|
309
|
+
if (routePath.startsWith('/api/download/')) {
|
|
310
|
+
const rawD = routePath.split('?')[0].slice('/api/download/'.length);
|
|
311
|
+
const decodedPath = decodeURIComponent(rawD);
|
|
312
|
+
const allowRoots = [
|
|
313
|
+
process.env.STARTUP_CWD || process.cwd(),
|
|
314
|
+
process.env.CLAUDE_PROJECTS_DIR || path.join(os.homedir(), '.claude', 'projects'),
|
|
315
|
+
...(process.env.FS_ROOTS ? process.env.FS_ROOTS.split(path.delimiter) : []),
|
|
316
|
+
].map(r => path.normalize(r));
|
|
317
|
+
const conf = confineToRoots(decodedPath, allowRoots);
|
|
318
|
+
if (!conf.ok) { res.writeHead(conf.reason === 'not found' ? 404 : 403); res.end('Forbidden'); return; }
|
|
319
|
+
const normalizedPath = conf.realPath;
|
|
320
|
+
try {
|
|
321
|
+
const st = fs.statSync(normalizedPath);
|
|
322
|
+
if (!st.isFile()) { res.writeHead(400); res.end('Not a file'); return; }
|
|
323
|
+
const MAX = 50 * 1024 * 1024; // 50MB cap so a download can't exhaust memory
|
|
324
|
+
if (st.size > MAX) { res.writeHead(413); res.end('File too large to download'); return; }
|
|
325
|
+
const name = path.basename(normalizedPath).replace(/["\r\n]/g, '');
|
|
326
|
+
res.writeHead(200, {
|
|
327
|
+
'Content-Type': 'application/octet-stream',
|
|
328
|
+
'Content-Disposition': 'attachment; filename="' + name + '"',
|
|
329
|
+
'Content-Length': String(st.size),
|
|
330
|
+
'Cache-Control': 'no-cache',
|
|
331
|
+
});
|
|
332
|
+
fs.createReadStream(normalizedPath).pipe(res);
|
|
333
|
+
} catch (err) {
|
|
334
|
+
const code = err && err.code === 'ENOENT' ? 404 : (err && err.code === 'EACCES' ? 403 : 400);
|
|
335
|
+
res.writeHead(code); res.end(err.message);
|
|
336
|
+
}
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
294
340
|
if (routePath.startsWith('/api/image/')) {
|
|
295
341
|
const imagePath = routePath.slice('/api/image/'.length);
|
|
296
342
|
const decodedPath = decodeURIComponent(imagePath);
|