feat(webui): file-open actions — any file opens the panel, three ways out (webui-parity slice 14) - #55
Merged
Conversation
added 2 commits
September 27, 2026 23:50
…e 14) Widen the click surface so every file row opens the right panel and the preview pane renders four distinguishable unsupported-state reasons (binary / oversize / out-of-bounds / unknown). Two new server endpoints (`POST /api/fs/open-default`, `POST /api/fs/reveal`) hand the file off to the OS default application or point the file manager at it, gated by the same containment + per-node realpath check the other /api/fs/* routes use. execFile-only, no shell. Bilingual strings in a new i18n module to avoid colliding with other slices' dictionaries. Server: - lib/open-target.js: cross-platform opener with PATH probe, argv-safe execFile, parent-first containment that distinguishes 'out of bounds' from 'not a regular file' so the UI can render an actionable hint rather than a generic refusal. - routes/fs.js: handleFsOpenDefault + handleFsReveal + structured code mapping (missing-path/out-of-bounds/not-a-regular-file/no-opener/ spawn-failed) -> HTTP status. JSON body cap honoured. - app.js: Hono registrations + OWNED_ROUTES ledger entry. Webapp: - lib/file-open-reason.ts: pure classifier shared between the React surface and tests; one regex owns the containment predicate so the PreviewError block and the unit suite cannot drift. - lib/i18n-file-open.ts: bilingual strings (en + zh, no orphan keys). - components/file-preview.tsx: PreviewError renders the reason, the two action buttons, the disabled-by-server reason tooltip, and a transient failure banner so a click never silently no-ops. - components/panels.tsx + app/page.tsx: every non-HTML row now opens the right panel through the page-level onOpenFile callback (which both publishes the path and opens the panel); panels.tsx stays passive, the test pins no setPanel call inside it. - lib/api.ts: openFileWithDefault + revealInFileManager raw-fetch helpers (request() throws on non-OK and would lose the structured code field). Tests: - test/routes/fs-open-target.test.js: containment, directory/non-existent rejection, symlink escape, argv-literal guarantee, no-opener PATH probe. 15/15. - test/file-open-reason.test.ts + test/i18n-file-open.test.ts: classifier and i18n symmetry, placeholder substitution. - test/open-file.test.ts: panels.tsx must NOT call setPanel; the page-level callback is the only owner of the click side effect. - test/server/app-hono.test.js: OWNED_ROUTES ledger updated for the two new routes.
…e 14 fix) Three changes addressing acceptance feedback: 1. **Third action "下载查看"** — adds a download button to the PreviewError. Reuses /api/fs/raw?download=1 (Content-Disposition: attachment) so the same containment gate + 20 MiB cap + regular-file check stay in one place. New fsRawDownloadUrl helper + i18n keys (fileOpen.action.download, fileOpen.action.download.aria). The button is always enabled; for out-of-bounds paths it sets aria-disabled and preventDefault so the click never fires a doomed GET. New server tests pin containment refusals + the header shape. 2. **Centred empty state** — PreviewError now renders vertically centred in the panel's full height (header glyph + reason + actions stacked). The old left-aligned card read as a small status banner; the centred layout matches the DSH reference and the ticket's empty-state intent. 3. **Per-action disable / hint** — the fireAction guard used to block both open-default and reveal when EITHER was disabled, so a host that could reveal but not open-default would silently no-op the reveal too. Now each action is gated on its own disabledByServer key. The disabled tooltip also matches the classifier: outOfBounds says "this path is outside the workspace"; noOpener says "this environment has no GUI opener" — the two reasons no longer share the same copy. New i18n keys fileOpen.button.disabledHint.outOfBounds and fileOpen.button.disabledHint.noOpener; test pins their distinctness. Gates: webapp:typecheck 0; root typecheck 0; check:source passes (inventory regenerated); webapp suite 558/558; server suite 937/937 (+5 new download tests, all 15 prior slice-14 tests still green).
weekbin
added a commit
that referenced
this pull request
Sep 28, 2026
Bring chore/sync-upstream-0.5.5 up to date with main so the pull request is mergeable again. main advanced by nine webui-parity changes (#55-#63) while this branch was open, and the earlier CI run on 13f52a8 was green across ubuntu, macos and windows. The only conflict was docs/tui-capabilities.md: the 0.5.5 sync and the webui slash-command work each appended independent sections after the capability matrix. Both are kept, in that order, so the document still describes the TUI, the webui and the shared engine. release/public-source.json and tsconfig.standalone.json are regenerated from their sources rather than taken from the automatic merge.
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Slice 14 of the webui-parity program, from the user's report: clicking a file in the tree did nothing useful for types that cannot be previewed, and the panel had no way out.
openFileInWebremains the single action; onlypage.tsxcalls it (pinned by a test).POST /api/fs/open-defaultandPOST /api/fs/reveal. Containment first proves the parent is in an allowed root, thenrealpathSync+ a second containment recheck (catches symlink escapes), thenlstatSyncrequires an existing regular file. Spawn isexecFile(bin, argv)only — no shell, no option-injection surface. Structured codes map to 400/403/503/502, including ano-openerprobe so an absent opener is reported rather than failing silently./api/fs/raw?download=1) rather than adding a second streaming path — containment, the 20 MiB cap, and the regular-file check all stay in one place; onlyContent-Dispositionis added.Acceptance (independent agent) — first pass FAIL, then PASS
The first pass failed on two gaps, both fixed before this PR: the 下载查看 action did not exist at all, and the empty state was left-aligned. Re-verification passed all five items:
Content-Disposition: attachment; filename="archive.zip", 23 bytes, md5 identical to source. Out-of-bounds: the anchor isaria-disabledwithpreventDefault(instrumented: zero fetches on click) andraw?download=1&path=/etc/hostname→ 403 with no bytes.open-defaultdisabled for lack of an opener,revealnow fires its own request and surfaces its own 503 + banner instead of silently no-op'ing; the out-of-bounds state no longer shows the "no GUI opener" copy..htmlrows still route to the browser iframe; onlypage.tsxcallsopenFileInWeb.Disclosed, non-blocking
Download failures surface through the browser's own download UI (the panel adds no banner by design); the wire answer is a 4xx. In the out-of-bounds state the per-action tooltip branch is unreachable because the server never marks those actions as server-disabled — the reason line above the buttons carries the explanation.
Gates
test:webapp558/558 ·webapp:typecheck0 · repotypecheck0 ·check:source✓ · full server suite 1833 pass / 0 fail / 2 skipped