Skip to content

feat(files): read-only Files card, reader, and rich viewer - #125

Merged
howdeploy merged 4 commits into
howdeploy:mainfrom
tab11pm:feat/file-browser-rich-viewer
Oct 12, 2026
Merged

howdeploy merged 4 commits into
howdeploy:mainfrom
tab11pm:feat/file-browser-rich-viewer

Conversation

@tab11pm

@tab11pm tab11pm commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a read-only Files canvas card with a browser and reader, plus rich rendering for Markdown and code.

  • Roots. A card is rooted at a live terminal session working directory (main resolves the session id) or a folder picked in a native dialog. The renderer gets only an opaque rootId and a label; every listing/read/search pairs it with a relative path.
  • Access boundary. New FileAccessService (main) canonicalizes root and target with realpath, rejects absolute paths, .., escaping symlinks and non-regular files, re-checks containment around the open, and caps bytes, entries, search depth and results. New allow-listed files:* IPC channels are guarded by the trusted-main-renderer check and exposed through the typed preload files capability.
  • Browser + reader. On-demand directory tree, quick open by file name, text/code reads (bounded, with a truncation marker), image previews under a MIME allow-list, explicit binary/unsupported/too-large states, and per-card persistence/restore with an unavailable state when the root is gone.
  • Rich rendering. Markdown renders as GitHub-Flavored Markdown (headings, lists, task lists, tables, blockquotes, rules, inline and fenced code with syntax highlighting); recognized code files are syntax-highlighted by extension; unknown text stays plain monospace. Output is sanitized (no raw HTML/scripts/event handlers, no innerHTML, dangerous schemes neutralized), http(s) links open in the built-in browser, only embedded data: images render, and files above the ~250 KB threshold fall back to plain text with a notice.
  • Startup cost. The Files card and its Markdown/highlight renderers are lazy-loaded so they stay out of the app's static import graph.

Notes

  • The unrelated custom-agents WIP is intentionally not part of this branch.
  • tests/renderer-bundle-splitting.test.mjs static-graph budget is raised from 1.50 MB to 1.55 MB (still well under the old ~1.6 MB single chunk) because the card adds startup orchestration to App.tsx.
  • openspec/ is not included: upstream does not track it.

Verification

  • npm test — 1809 pass, 0 fail (29 skipped)
  • npm run typecheck — both projects pass
  • npm run build — succeeds

Adds the Files canvas card: a read-only browser and reader rooted either at a
live terminal session working directory or at a folder picked in a native
dialog. Main owns access through a new FileAccessService that canonicalizes root
and target with realpath, rejects absolute paths, `..`, escaping symlinks and
non-regular files, and enforces byte, entry, depth and result caps; the renderer
only receives an opaque root id plus relative paths over the allow-listed
`files:*` channels. Cards persist and restore their root, bounds, open file and
expanded folders, and show an unavailable state when a root is gone.

Text and code are rendered richly: Markdown files render as GitHub-Flavored
Markdown (headings, lists, task lists, tables, blockquotes, rules, inline and
fenced code) and recognized code files are syntax-highlighted by extension.
Rendering is sanitized (no raw HTML, event handlers or dangerous schemes; no
innerHTML), http(s) links open in the built-in browser, only embedded `data:`
images render, and files above the ~250 KB threshold fall back to plain text
with a notice. The heavy Markdown/highlight renderers and the card are
lazy-loaded to keep them out of the startup bundle.

Excluded: the unrelated custom-agents WIP is intentionally not part of this
branch. Also raises the renderer static-graph budget in
tests/renderer-bundle-splitting.test.mjs (still well under the old ~1.6 MB
single chunk) to account for the new card's startup orchestration.
`FileAccessService` registered roots with `realpathSync` but canonicalized
targets with the async `realpath`, which on Windows can return a differently
cased or 8.3-shortened path. The prefix containment check then treated the root
itself (and every child) as outside the root, so listing/reading failed. Resolve
targets with the same `realpathSync` used at registration and compare
normalized (case-insensitive, `\\?\`-stripped) paths on Windows.

Windows CI: tests/file-access-service.test.mjs and files-ipc-security.test.mjs
were the 13 failing tests; they now pass on Linux and should on Windows.
@howdeploy

Copy link
Copy Markdown
Owner

Reviewed head 82aaed08802e3d759888d5da58e300de3710ec9b. All three platform CI jobs pass. The Windows path-normalization fix, main-owned file access, safe Markdown rendering, and lazy rich-viewer imports are useful. There are still two correctness/performance issues to fix before merging.

P2 — Bind every asynchronous completion to the current card/root generation

loadFileRoot() applies a listing result without checking whether the card still owns that root. openFileDirectory() has the same problem for child listings. Switching folders invalidates fileReadSequence, but not outstanding searches; quickOpenFiles() checks only the search sequence, which is unchanged by a root switch. Startup restore also checks only whether the card remains open, not whether the user has replaced its root in the meantime.

Example: start a slow listing/search in folder A, switch the same card to folder B, let B finish first, then let A finish. A's tree or search results can overwrite B's runtime state. Clicking an old result subsequently resolves that relative path against B, giving an error or opening a different same-named file. A late listing can also recreate runtime state after a card was closed.

Please invalidate outstanding operations when the root changes or the card closes, and check card existence plus the current root/generation before applying any asynchronous success or error. Apply the guard consistently to initial listings, child listings, quick-open searches, and restore operations; the file-read sequence alone does not cover these paths.

Regression coverage needed with deferred responses:

  • A delayed root-A listing or child listing must not modify a card now rooted at B.
  • A delayed root-A search must not repopulate B's quick-open results, including when the query was cleared during the switch.
  • A delayed restore completion must not overwrite a user-selected root.
  • Closing a card must prevent late completions from recreating its runtime state or retaining a newly registered root.

P2 — Bound traversal work, not just the number of matching results

In FileAccessService.search(), maxSearchResults limits matches and maxSearchDepth limits depth, but there is no visited-entry/directory or time budget. A query with no matches can traverse an arbitrarily wide tree. quickOpenFiles() starts another traversal on every input change; its sequence check discards stale UI results but does not stop the underlying work.

Directory listing has a related issue: list() enumerates the directory and stats every regular file before applying maxDirectoryEntries, so that cap does not bound the expensive work either.

Please add a bounded traversal-work budget with an explicit incomplete/truncated result, and avoid accumulating obsolete searches when the user types quickly. Debouncing is useful, but service-side bounds are still needed for a single large no-match search. Ensure the listing cap also bounds metadata work. If a search is truncated, retain that information through the renderer rather than silently presenting an incomplete result as exhaustive.

Regression coverage needed: a wide tree with no matches should stop at the work budget; a burst of queries should not leave all obsolete traversals running; large directory listings should perform bounded metadata work. Preserve the existing containment, symlink, and read-size checks.

Integration note

#123 adds individual material cards, while this PR adds a folder browser/reader; both can be useful. Their current heads have merge conflicts in 10 files, including the canvas, minimap, App.tsx, preload, shared contracts, and tests. Once either PR lands, please rebase/adapt the other without dropping support for the first feature.

These findings are from source review and the existing CI results; I have not run new local tests or a live UI reproduction.

@tab11pm

tab11pm commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

@howdeploy Thank you for the detailed review. We are working through the two P2 findings and the requested regression coverage. The merge conflicts with main are resolved, but please hold off on merging #125 into main until we have addressed those findings and verified the follow-up changes. We will update this PR when it is ready for another review.

howdeploy added a commit that referenced this pull request Oct 12, 2026
This integrates the six contributions accepted during the maintainer's DEV review, together with the maintainer's local UI and minimap fixes. Rejected orchestration features, including #133, are excluded entirely.

## Included contributions and credit

- #125 by @tab11pm: read-only file cards, directory browsing, and richer Markdown/code previews.
- #130 by @SaneSanders: image/material remarks and handoff to an existing agent session.
- #131 by @BIackFIame: macOS clipboard/edit context menus for text fields.
- #138 by @pepechka-kun: usage history, measured quota changes, local-log evidence, and explicit attribution uncertainty.
- #139 by @CLOSETTTY: interactive mascots and the Codex mascot creation workflow.
- #140 by @teo-nex: the macOS node-pty file-descriptor leak fix, patch delivery, and resource diagnostics.

Thank you to all six contributors. Each original PR head and its author commits are retained in Git history through separate merge commits. This integration must use a merge commit rather than squash or rebase.

## Maintainer patches

- Fit the minimap overview to the camera viewport and all canvas objects together, with padding; retain stable drag coordinates during a gesture.
- Keep the material handoff icon fixed in the header, remove the count beneath it, and place equally sized Cancel/Send actions at the bottom right of the handoff dialog.
- Split usage history into five left-side sections: conditional estimate, quotas, agents, local-log tokens, and source state. Use pages within sections to fit the window without a long scrolling report.
- Resolve shared App/IPC/preload integrations while retaining the file, handoff, and usage-history flows.
- Retain the approved DEV dependency set and Electron 43.7.7; reuse the matching lockfile with patch-package 8.0.1.

## Scope

#132, #133, #134, #135, and #137 are not included. #136 is also excluded pending the previously requested UI/UX redesign. No commits from that cumulative orchestration series are ancestors of this branch.

Material handoff in #130 does not automatically apply a saved JSON agent report to the material state; this remains the reviewed behavior.

## Validation

- `VITE_BUILD_CHANNEL=DEV npm run build` passed, including the script's TypeScript checks and native-helper build.
- `git diff --check` passed.
- Verified that all six accepted PR heads are ancestors of the integration and all six excluded PR heads are not.
- Verified that the three approved local UI/minimap implementations match the selected DEV copy.
- Added minimap geometry coverage for distant camera positions and adapted usage-history fixtures for section navigation.
- The maintainer manually reviewed the accepted features in the earlier combined DEV build. The final integration without #133 has not received another manual UI pass. Automated tests and macOS/Windows runtime checks were not run locally for this integration.
@howdeploy
howdeploy merged commit 4598cbd into howdeploy:main Oct 12, 2026
3 checks passed
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.

2 participants