Skip to content

feat(webui): mount built-in browser panel + fix history and workspace-relative paths (slice 04b) - #53

Merged
fengzhi09 merged 1 commit into
mainfrom
feat/browser-panel-mount
Sep 27, 2026
Merged

fengzhi09 merged 1 commit into
mainfrom
feat/browser-panel-mount

Conversation

@fengzhi09

Copy link
Copy Markdown
Collaborator

What

Slice 04b: mounts the built-in browser panel (slice 04 shipped it unmounted because panels.tsx belonged to the then-in-flight slice 03) and fixes the two functional defects that slice 04's acceptance assigned to the wiring step.

  • F1 — history: the controlled-sync effect now distinguishes echo from external change. When the new controlled path equals currentPath(history) the change is the panel's own navigation — keep the stack, sync the draft, clear errors. When they differ it is external (a file-tree HTML click) and re-seeds via createHistory. Back/forward dedupe no-op steps so a click at the boundary does not echo back to the parent.
  • F2 — workspace-relative resolution: new resolveWorkspacePath(path, workspaceDir) stitches relative → absolute before the sandbox URL is built, and BrowserPanel gained a real workspaceDir prop (the doc comment previously promised one that did not exist). .. segments pass through untouched so the server containment gate remains the single source of truth.
  • Mount: "browser" added to PanelKind and validKinds (so it round-trips across reload), toolbar launcher wired, page.tsx owns browserPath. File-tree .html/.htm rows route to onOpenInBrowser (sets path and opens the panel); non-HTML rows still go through slice 12's single-source openFileInWeb. Switching workspaces clears the relative path so the next gesture re-anchors.

Acceptance — PASS (independent agent, live UI + fresh gates)

  • Reachability via the real toolbar only (browserPath never seeded): the panel opens alongside 文件/工作区, and the files panel (tree + slice-12 preview), workspace switch dialog and the git 变更 view all still work.
  • Relative paths render: index.html, pages/about.html, ./index.html all resolve to the workspace; absolute in-root renders; non-HTML refused client-side; out-of-root and .. traversal → 403 with the explicit allowed-roots error.
  • Back/forward genuinely work: navigate → navigate → back lands on page 1 with the address bar synced and back disabled (exactly one entry per navigation, no echo duplicates); forward returns to page 2. Panel refresh keeps position and stack.
  • Sandbox unchanged: the rendered attribute is literally allow-scripts on every observed iframe — no allow-same-origin / allow-top-navigation / popups / forms / modals. Preview JS executed, yet the address bar still refuses http(s)://, file://, javascript:, vbscript:, data: with an inline error and no iframe created.
  • Optional integrations verified: file-tree .html click opens in the browser panel; workspace switch clears the path and the same relative string re-anchors to the new workspace.

Disclosed, non-blocking

  • F3 remains open by design: relative sub-resources (<link href="style.css">) do not resolve under /api/fs/raw because that route has no path-suffix resolution. The slice proved F2 end-to-end using absolute /api/fs/raw?path=… URLs instead of changing the fs route's contract. F3 is a follow-up slice.
  • The iframe renders the route's JSON error body on 403/404 (carry-over from slice 04).
  • Fresh-checkout ordering: the full server suite fails router-auth-gate if dist/webui/webapp/out is absent (passes after pnpm build) — pre-existing infra quirk, this diff is webapp-only; worth its own ticket.

Gates

Full server suite 1790 pass / 0 fail / 2 skipped · test:webapp 514/514 · typecheck ✓ · build ✓ · check:source ✓

…ace-relative resolution (webui-parity slice 04b)

Slice 04 shipped the panel exported but deliberately unmounted because
components/panels.tsx was owned by the in-flight slice 03. Slice 03
merged; this slice wires the panel into the right-hand drawer AND
fixes the two functional defects acceptance surfaced.

F1 — history echo. The controlled-sync effect unconditionally rebuilt
the back/forward stack on every controlledPath change. Under the
natural round-trip wiring (onNavigate -> parent state -> currentPath
prop) the Go button pushed once and the effect wiped the back stack.
The fix detects echo vs external: if the new controlledPath equals
the internal currentPath the change is the panel's own navigation —
keep the stack and only sync the address bar. If they differ it is an
external change (file-tree click) and re-seed the stack. The push is
also deduped inside the back/forward handlers so a no-op step does not
fire an empty echo back to the parent.

F2 — workspace-relative resolution. The address bar accepted
workspace-relative paths but buildSandboxUrl emitted them verbatim.
The server route resolves relative paths against its own CWD, which
is almost never an allowed root, so a workspace-relative entry landed
on a 403 / ENOENT. The component now carries a real workspaceDir prop
and a new helper resolveWorkspacePath in lib/browser-nav.ts stitches
the two halves before the URL is built. Absolute in-root paths pass
through unchanged; .. segments are deliberately preserved so the
server-side containment gate stays the single source of truth.

Mount. 'browser' added to PanelKind in components/panels.tsx and to
the deserialiser's valid-kinds set in lib/persist.ts; the toolbar
launcher (slice-04 left it disabled) is now wired through
onOpenBrowser -> openPanel('browser'). The file tree routes HTML/HTM
rows through onOpenInBrowser -> browserPath + panel='browser', while
non-HTML rows still call the slice-12 single-source openFileInWeb.
Switching workspaces clears the relative path so the next gesture
re-anchors cleanly.

F3 — sub-resource subset URLs. Multi-file static sites that load
relative CSS/JS cannot resolve them under /api/fs/raw because the
route has no path-suffix resolution. The slice instructions pin 'do
not change the fs route's contract in this slice unless you can do it
additively and safely' — skipped. The HTML the self-check serves uses
absolute /api/fs/raw URLs that include the workspace dir explicitly
to prove F2 still works end-to-end; F3 will be a follow-up slice.

Tests. 13 new unit tests in webapp/test/browser-nav.test.ts (workspace
resolution rules: empty / POSIX-absolute / relative / ./ collapse /
trailing separators / .. preservation / Windows drives; and three
buildSandboxUrl overload cases). 1 new persist test confirming the
'browser' kind round-trips. All 514 webapp tests pass; webapp:typecheck
+ repo typecheck clean.
@fengzhi09
fengzhi09 force-pushed the feat/browser-panel-mount branch from 67dceb0 to 7a12557 Compare September 27, 2026 14:55
@fengzhi09
fengzhi09 merged commit f12e111 into main Sep 27, 2026
8 checks passed
@fengzhi09
fengzhi09 deleted the feat/browser-panel-mount branch September 27, 2026 15:03
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