Skip to content

feat(webui): open.file.in.web — single-source 'open file' capability (webui-parity slice 12) - #49

Merged
fengzhi09 merged 3 commits into
mainfrom
feat/open-file-in-web
Sep 27, 2026
Merged

fengzhi09 merged 3 commits into
mainfrom
feat/open-file-in-web

Conversation

@fengzhi09

Copy link
Copy Markdown
Collaborator

What

Slice 12 closes the gap the slice-02 acceptance flagged: the user requires 「文件预览功能是 空间目录/轮次总结都可以触发的能力,open.file.in.web」, and measurement showed 0/2 entry points and no single-source action — the viewer existed but nothing could open it.

  • Single source — webapp/lib/open-file.ts owns the whole capability: openFileInWeb(path) / closeOpenFile() / getOpenFilePath() / subscribeOpenFile(listener), persisting the last path under webui:open-file:path (the webui: namespace slice 01 established).
  • Entry 1 — file tree (空间目录): FileRow becomes a <button data-testid="files-tree-file-open">. The hover 「复制绝对路径」 control is kept as its own target so copying never opens the preview by accident.
  • Entry 2 — turn summary (轮次总结): ToolCard renders block.toolPaths as data-testid="tool-card-path" chips, threaded via a new onOpenFile prop through ActivityGroup; the page-level handler also opens the files panel so the preview is visible even when the right panel was closed.
  • Empty/close states with bilingual guidance copy.
  • Single-source contract pinned by tests at three levels (module behaviour, source tripwire, disabled-storage SSR safety).

Acceptance (independent agent) — PASS-WITH-CONCERNS

  • Single source: the four exports are defined once; the only callers are panels.tsx (tree) and page.tsx (chat handler); chat.tsx imports nothing and takes a prop. Independent grep confirms no second open path exists.
  • Entry 1 live-verified: clicking a file in the merged slice-01 tree rendered the slice-02 preview; the copy-path control left the open path unchanged.
  • Entry 2 live-verified with real server-shaped data — the agent injected a session using the exact format the server writes: 「查看 1 个文件」 → expand → Read tool card → chip visible → click opened the preview and auto-opened the files panel. The data path is real: ACP tool_update.locations → applyToolUpdate writes @ ${p} → transcript.ts#TOOL_PATH_LINE → block.toolPaths → chip.

Disclosed limitation (verified accurate, not waived): the current dev database has zero → tool headers and zero @ path body lines, so for those existing sessions no chips render. This is a data dependency, not dead code — the path is proven above.

Merge-order note: this branch edits chat.tsx and page.tsx, which slices 06 and 07 also touch. The acceptance mapped the overlapping hunks (chat.tsx import block; ToolCard body for 12↔06; page.tsx imports and the handler block for 12↔07). Recommended landing order: 07 → 06 → 12, threading onOpenFile through 06's ToolCard during its rebase.

Gates

test:webapp 406/406 · typecheck ✓ · build ✓ · check:source ✓ (4602 files)

The slice 02 preview component had no entry point — neither the file tree
(空间目录) nor the turn summary (轮次总结) reached it. Slice 12 wires both
through ONE shared action so a click in either place lands on the same
right-hand preview surface (file-preview-pane).

Action: lib/open-file.ts owns the single-source 'open.file.in.web'
contract — openFileInWeb(path) is the only way to open a file, both entry
points call it, persistence (localStorage 'webui:open-file:path') makes a
refresh reopen the same file. The companion webapp unit test pins the
contract at three levels: module behaviour, source-tree single-source
(grep the webapp for callers), and the page-level panel-open discipline.

Entry 1 (file tree): panels.tsx FilesPanel renders FileRow as a clickable
button (testid files-tree-file-open) that calls openFileInWeb; the
hover-only 'copy absolute path' button is unchanged.

Entry 2 (turn summary): chat.tsx ActivityGroup → ToolCard renders
block.toolPaths (the server's '@ /path' body lines) as clickable buttons
(testid tool-card-path) that call the page-level onOpenFile handler. The
handler sets the path AND auto-opens the files panel when it is closed,
so the preview the user triggered is the one they see.

Honest limitation: the slice-12 wiring is in place for the turn-summary
entry, but the current session transcripts in the dev DB don't carry the
server's 'locations' data, so the per-file chips aren't visible on those
specific sessions. The wiring is correct and a session whose tool updates
include locations will render clickable path chips as designed.

Persistence key: webui:open-file:path (localStorage). Slice 07 owns the
cross-restart durability migration; this slice only adds the per-file
'refresh reopens same file' guarantee.

Gates: webapp typecheck 0; test:webapp 406/406 pass; webapp build ✓;
source-inventory regenerated.
CI's webapp:typecheck rejected the test with TS2403/TS2322: the
disabled-storage case redeclared the ambient 'window' global with a
structural type narrower than the DOM lib's Window & typeof globalThis.

Install the stub with Object.defineProperty on globalThis instead, which
keeps the same behaviour under test (storage disabled must not throw at
import time) without shadowing the ambient declaration.

webapp:typecheck (CI entry point): clean.
Conflict resolution threaded slice 12's onOpenFile through slice 07's
ScrollRestoredChat wrapper so the turn-summary file chips survive the
merge: ChatProps keeps both slices' optional props, page.tsx keeps both
import sets, and ScrollRestoredChat forwards onOpenFile to the inner Chat.

webapp:typecheck clean · test:webapp 464/464 · check:source 4621
@fengzhi09
fengzhi09 force-pushed the feat/open-file-in-web branch from 48f682c to cb91e2e Compare September 27, 2026 12:13
@fengzhi09
fengzhi09 merged commit f25ee22 into main Sep 27, 2026
8 checks passed
@fengzhi09
fengzhi09 deleted the feat/open-file-in-web branch September 27, 2026 12:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant