feat(workspace): read existing files in bound folders + visible Files panel#271
Conversation
…les panel Three small fixes so a bound workspace folder works like a CLI working directory rather than a write-only mirror: 1. apps/desktop/src/main/index.ts -- new seedFsMapFromWorkspace() walks the bound workspace on every generation and loads its text files into fsMap. Without this, the agent only saw the bundled frames/skills and treated the user's real folder as empty even after binding. 2. apps/desktop/src/main/index.ts -- listDir() now returns the full recursive tree (flat paths) instead of just the first segment, so the agent gets the whole project in one list_files call instead of recursing one directory at a time. 3. apps/desktop/src/renderer/src/components/PreviewPane.tsx -- drop the `&& previewHtml` gate on the Files tab so the workspace bind UI is reachable before any generation has happened. Otherwise users have to prompt first to discover a panel that's gated on the prompt's output. Caps in the seeder: 500 files, 1 MB per file, skip dotfiles, node_modules, .git, dist, build, .next, .turbo, .cache, coverage, .idea, .vscode. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Register a custom Electron protocol that serves files from the bound workspace folder. When a design has a workspace, the preview iframe loads `workspace://<designId>/index.html` instead of injecting srcdoc, so relative imports (./styles.css, /assets/logo.png, fonts, JS modules) resolve against the actual project root. Designs without a workspace keep the existing srcdoc path -- no regression for one-off generations. Security: the path resolver validates designId against the snapshots DB, restricts MIME types to a static whitelist, and uses path.resolve plus an inside-workspace check for defense-in-depth on top of WHATWG URL's own `..` collapsing. Null bytes are rejected. Iframe stays sandboxed with `allow-scripts` only (opaque origin). Cache-Control: no-store on responses + a content-hash cache buster on the iframe src means every fs_updated event reloads the page from disk. 17 vitest cases cover MIME, traversal (plain/encoded/null-byte), URL shape, default index.html, query strings, and encoded filenames. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The runtime verifier wraps the artifact in `buildSrcdoc()`, which expects a JSX module (TWEAK_DEFAULTS + ReactDOM.createRoot). Real workspace files -- plain HTML, framework code, anything that links to external assets -- don't fit that mold, so the verifier flags them as broken and the agent self-heals by flattening the user's actual project into a single self-contained doc, silently destroying the source. When a workspace is bound the user has their own tooling (their dev server, their browser, their linters) acting as ground truth. Skip verification in that mode and let the agent emit surgical edits. Verified manually: editing a 16K real-world dashboard.html with relative links/scripts now produces a 16K file with the requested character-level change and no structural flattening, instead of the prior behaviour where the agent would balloon it to a self-contained document. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the virtual single-`index.html` row in the Files panel with a
real walk of the bound workspace. A new `files:list:v1` IPC scans the
folder (skipping node_modules / .git / build outputs and dotfiles),
classifies entries as html or asset, and ships size + mtime for the
panel UI. The hook switches between this backend and the legacy
snapshots-derived synthesis based on whether a workspace is bound,
so non-workspace designs keep working unchanged.
Double-clicking a file in the panel now actually previews that file:
PreviewPane derives the iframe path from the active file tab and the
workspace:// URL becomes per-file (path segments encoded). The cache
buster keys on path + content hash so switching tabs reloads cleanly.
Two follow-on fixes the new flow surfaced:
- Pool entries are now created for any design with a bound workspace,
not only those with in-memory previewHtml. Without this, opening a
fresh workspace-backed design and clicking a file dropped the user
into the new-design EmptyState instead of the iframe.
- The workspace:// MIME whitelist learns .jsx / .ts / .tsx so HTML
files that pull source via `<script type="text/babel">` for in-
browser transpilation actually render. Babel-standalone projects
are common in design-tool exports and were silently failing with
unsupported_mime.
11 vitest cases cover walker behaviour: empty workspace, html/asset
classification, sort order, nested paths, skip dirs, dotfiles, the
max-files cap, and missing-dir tolerance.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Inject a small click handler into every HTML response served from the workspace protocol. When the user clicks a `<a href="other.html">` that resolves to another workspace file, the handler preventDefaults the in-iframe navigation and posts the target path to the parent renderer, which opens it as its own canvas tab via openCanvasFileTab. Without this, clicking a link inside (say) "Aide Sketch.html" silently repointed the iframe at "Profil Sketch.html" while the tab still showed "Aide Sketch.html" -- one tab, two files, total confusion. With it, each canvas tab stays pinned to a single file. Only `.html` / `.htm` workspace links are intercepted; hash links, javascript: URLs, external schemes, and asset links pass through to the browser default. The OPEN_FILE_TAB path bypasses the iframe trust check that gates element/overlay messages -- those need a stable contentWindow comparison, which is unreliable across iframe re- navigations, and the message can only originate from a sandboxed workspace:// page anyway. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a chevron button to the Files tab that hides the central file-list aside, letting the preview iframe use the full canvas width. The CanvasTabBar at the top stays visible regardless, so the user can keep switching between already-opened file tabs even when the list is collapsed. The collapsed state lives in the Zustand store (filesPanelCollapsed) so it survives tab switches and re-mounts. Default is expanded. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous bind path threw "Workspace path is already bound to another design" when a second design tried to point at a folder another design already owned. Upstream's own v0.2 doc explicitly states multiple sessions can share a workspace, so the guard fights its own model and forces users to either duplicate the folder or shuffle bindings to spin up a parallel design view of the same project. Replace the throw with an info-level log of the overlap (so the shared state remains auditable) and let the bind proceed. Updated the two test cases that asserted on the rejection to assert on the new shared- bind behaviour instead -- both designs now end up with the same path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both the agent-seeding walker (`seedFsMapFromWorkspace`) and the Files panel walker (`walkWorkspaceFiles`) maintained their own copy of the 500-file cap, the skip-dir allowlist, and the dot-file skip rule. Diverging would mean the agent and the Files panel disagree on what counts as a workspace file. Extract the shared bits to `workspace-walk.ts`. The two walkers stay separate (one is sync + reads file content for the agent, the other is async + returns metadata for the renderer) but pull from one source of truth for filtering. Also drop the broken `Logger as CoreLogger` re-import in `files-ipc.ts` in favor of the `CoreLogger` type already exported from `@open-codesign/core`, which fixes the tsc error introduced when the file was added. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The nav-intercept script was packed onto a single 700-character line. That hides the control flow from anyone debugging why a click in an iframe did or didn't bubble out as an OPEN_FILE_TAB postMessage. The script body is now multi-line and indented; the runtime behaviour is identical (whitespace inside a `<script>` is irrelevant to the browser). Also drop the broken `Logger as CoreLogger` re-import in favor of the `CoreLogger` type from `@open-codesign/core` (consistent with `index.ts` and `files-ipc.ts`), and replace the missing-from-this-tsconfig `BodyInit` annotation with the concrete `string | Uint8Array` we actually pass to `new Response`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Review mode: initial
Findings
-
[Minor] Missing changeset — the PR adds user-visible features (workspace protocol, Files panel, collapsible file list) but does not include a changeset. The project uses changesets for versioning; without one these changes won't appear in the changelog. Follow the template: run
pnpm changesetand commit the generated markdown file.
Suggested fix:pnpm changeset # select 'minor' bump, write summary describing workspace binding, protocol, and Files panel changes git add .changeset/*.md && git commit -m "chore: add changeset for workspace-read-existing"
-
[Minor]
seedFsMapFromWorkspaceuses synchronousreaddirSync/readFileSyncinside a generation-initialization path, which blocks the main process. With 500 files at up to 1MB each, this could introduce perceptible UI jank. Consider an asynchronous walker or a streaming approach to keep the event loop responsive.
Suggested fix:// apps/desktop/src/main/index.ts:278-390 // Replace sync walk with async walk using fs.promises, or use a worker. // For now, at least wrap in a setTimeout to defer execution.
-
[Nit] Collapsed rail width is hardcoded as
w-[36px]inFilesTabView.tsx:214. Consider using a design token (e.g.,var(--space-9)or a named--width-railvariable) to stay consistent with the project's token-based styling.
Suggested fix:// apps/desktop/src/renderer/src/components/FilesTabView.tsx:214 // Replace className="w-[36px]" with a token-based value, e.g., w-[var(--width-rail)]
Questions
None.
Summary
- Reviewed all 16 changed files (1302 additions, 108 deletions).
- Feature is well-structured: workspace:// protocol with path-traversal hardening, shared walk rules, IPC-backed file listing, collapsible Files panel, and agent fsMap seeding.
- Security handled correctly: null-byte rejection, WHATWG URL normalization, defense-in-depth path.resolve guard, MIME whitelist, no-store caching, sandboxed iframe.
- Hard constraints satisfied: no new SQLite tables, no bundled runtimes, no direct provider SDK imports, UI tokens used, Zustand state.
- Tests added for
walkWorkspaceFiles(11 cases) andresolveWorkspaceUrl(18 cases); existing test updated for shared workspace binding. - Minor concerns: missing changeset, sync seed function, hardcoded pixel value. None blocking.
Testing
- Unit tests pass (1054/1054 per PR description).
- Workspace protocol tests: 18 tests covering URL resolution, traversal, MIME, null bytes.
- Files IPC tests: 11 tests covering walk, filtering, sorting, caps.
- Missing: dedicated test for
seedFsMapFromWorkspace. Not run (automation).
Open-CoDesign Bot
|
Thanks, this is a strong direction. I like the product shape here: a bound workspace should behave like a real working folder, and the Files panel / I do not think we should merge this as-is yet, but it looks close. Could you please address these before merge?
This is user-visible behavior: existing workspace files become visible to the agent, workspace assets load through
Please switch this to a bounded async walker, and add a total byte cap in addition to the per-file cap. The concern is both UI responsiveness and memory/context pressure from eagerly loading too much workspace text into the runtime fs map. A good shape would be:
I understand why workspace iframe navigation needs special handling, but right now Optional polish:
Once those are addressed, I’d be happy to take another pass for merge. |
There was a problem hiding this comment.
Review mode: follow-up after new commits
Findings
No new issues introduced in this follow-up diff.
All three findings from the previous review have been addressed:
- ✅ Changeset added (
.changeset/workspace-files-agent-visible.md). - ✅
seedFsMapFromWorkspaceconverted from synchronous to asyncfs.promises— no more main-thread blocking. - ✅ Hardcoded
w-[36px]replaced withvar(--size-files-rail)token, and the token is declared inpackages/ui/src/tokens.css.
The follow-up also added:
WORKSPACE_SEED_MAX_TOTAL_BYTESandbytesLoadedtracking for a total-byte cap.getTrustedWorkspaceFileTabPathandisSafeWorkspaceHtmlPathwith unit tests, hardening the nav-intercept postMessage path.createRuntimeTextEditorFsandrunGeneratemade async to support the async seed function.currentDesignlifted to a higher scope for workspace-path access in theonMessagehandler.
All hard constraints remain satisfied. Test count increased with new seed and nav-intercept tests. No regressions identified.
Summary
- Follow-up resolves all prior concerns. The code is clean, well-tested, and follows project conventions. Ready for merge.
Open-CoDesign Bot
There was a problem hiding this comment.
Review mode: follow-up after new commits
Findings
No new issues introduced in this follow-up diff. The only change since the previous review is a cosmetic comment fix in packages/providers/src/gateway-compat.ts (ellipsis character replaced with three dots). No behavioral or structural impact.
All prior concerns have been addressed and no regressions are present. The PR remains clean and ready for merge.
Open-CoDesign Bot
There was a problem hiding this comment.
Review mode: follow-up after new commits
Findings
-
[Minor] Hardcoded UI dimensions in
FilesTabView.tsxviolate the hard constraint that every UI value must come frompackages/uitokens. The collapsed rail usesh-[36px],text-[12px], andtext-[11px]instead of referencing token variables.
Suggested fix:/* In tokens.css, add if missing */ --size-file-header: 36px; --font-size-label: 12px; --font-size-action: 11px;
Then use
h-[var(--size-file-header)],text-[var(--font-size-label)], etc. -
[Nit] The
doneverifier is skipped for workspace-bound designs. This is a documented trade-off, but it means the agent receives no runtime error feedback for real workspace files. Consider logging a warning or adding a minimal check (e.g., file exists, response status) in a follow-up. -
[Nit] FilesTabView collapsed preview always uses
srcDoceven when a workspace is bound. While the main preview iframe usesworkspace://, the tab-internal iframe does not, which may break relative imports for selected workspace files. This is minor as the primary preview is always via PreviewPane.
Summary
This PR is a high-quality, well-tested feature addition that enables workspace file seeding, the workspace:// protocol, and a real Files panel. Security hardening (path traversal, MIME whitelist, null byte checks) is thorough. The shared workspace-walk module ensures the agent and panel stay consistent. The only issues are minor UI token usage and a few cosmetic comment changes. No blockers or majors. Merge after optionally addressing the token nit.
Testing
- Unit tests cover
walkWorkspaceFiles,resolveWorkspaceUrl,seedFsMapFromWorkspace, path-safety helpers, and the workspace-binding changes (1054/1054 pass). - Missing: integration test for
registerWorkspaceProtocolHandler(requires Electron runtime — acceptable per project limits). - Missing: test for the
registerFilesIpchandler directly (the walk function is tested, handler logic is thin).
Open-CoDesign Bot
There was a problem hiding this comment.
Review mode: follow-up after new commits
Findings
-
[Minor] Hardcoded UI dimensions in
FilesTabView.tsx(unchanged from previous review) —h-[36px],text-[12px], andtext-[11px]still violate the hard constraint that every UI value must come frompackages/uitokens. The collapsed rail and action button use raw pixel values instead of token variables.
Suggested fix:/* In tokens.css, add if missing */ --size-file-header: 36px; --font-size-label: 12px; --font-size-action: 11px;
Then replace
h-[36px]withh-[var(--size-file-header)],text-[12px]withtext-[var(--font-size-label)], etc. -
[Nit] The
doneverifier is skipped for workspace-bound designs (still the case — no change in this diff). The agent receives no runtime error feedback for real workspace files. Consider logging a warning or adding a minimal file-exists/status check in a follow-up.
Summary
The new diff only contains cosmetic comment updates (em dash to double hyphen, PRINCIPLES number formatting). No behavioral changes were introduced. The two issues noted in the previous review remain unaddressed. Merge after optionally fixing the token compliance issue.
Testing
- No new tests in this diff; existing test suite still passes (1054/1054).
Open-CoDesign Bot
There was a problem hiding this comment.
Review mode: follow-up after new commits
Findings
-
[Minor] Hardcoded UI dimensions in
FilesTabView.tsx(unchanged from previous review) —h-[36px],text-[12px],text-[11px],w-6 h-6,w-3.5 h-3.5still use raw pixel values instead of design tokens. Per project constraints, every UI value in app chrome must come frompackages/uitokens.
Suggested fix: define tokens for these values intokens.cssand reference them, e.g.--size-file-header: 36px,--font-size-label: 12px,--font-size-action: 11px,--size-icon-sm: 14px, etc., then update the class names accordingly. -
[Minor] Potential symlink traversal in
workspace-protocol.ts— the path containment check only verifies that the resolved string path starts with the workspace directory, but does not resolve symlinks. If a file inside the workspace is a symlink pointing outside (e.g., to/etc/passwd),readFilewill follow it and serve arbitrary files. While the iframe is sandboxed (allow-scriptsonly), this could leak sensitive content if an attacker controls workspace file content.
Suggested fix: after computingabsPath, callfs.realpath.native(absPath)and verify that the real path still starts with the real workspace directory. Wrap the result in an async check and return a 403 if it escapes.import { realpath } from 'node:fs/promises'; // ... const realAbsPath = await realpath(absPath); const realWorkspace = await realpath(normalizedWorkspace); const isInside = realAbsPath === realWorkspace || realAbsPath.startsWith(`${realWorkspace}${sep}`); if (!isInside) { return { ok: false, error: 'traversal' }; }
-
[Nit] The
doneverifier is skipped for workspace-bound designs (as described in PR). This is intentional but means the agent receives no runtime error feedback for real workspace files. Consider adding a minimal file-exists check or logging in a follow-up.
Summary
This PR introduces significant workspace functionality: seeding real files into the agent's fsMap, a privileged workspace:// protocol for direct disk access, a shared workspace walker, and a collapsible Files panel that lists real files. The implementation is generally well-structured with ample test coverage. However, two issues from the previous review remain: hardcoded UI dimensions (FilesTabView.tsx) and the lack of symlink path traversal hardening in the workspace protocol handler. Addressing these before merge would strengthen the feature.
Testing
- New unit tests for
walkWorkspaceFiles(11 cases),resolveWorkspaceUrl(18 cases),seedFsMapFromWorkspace(2 cases) - Updated existing tests for
createRuntimeTextEditorFsandbindWorkspace - 1054/1054 tests pass
- Suggest adding a test case for symlink traversal in
workspace-protocol.test.ts
Open-CoDesign Bot
Signed-off-by: hqhq1025 <1506751656@qq.com>
There was a problem hiding this comment.
Review mode: follow-up after new commits
Findings
-
[Minor] Hardcoded UI dimensions in
FilesTabView.tsxremain unchanged from the previous review —h-[36px],text-[12px],text-[11px],w-6 h-6,w-3.5 h-3.5still use raw pixel values instead of design tokens. Per project constraints, every UI value in app chrome must come frompackages/uitokens. (Already flagged in previous review; the contributor noted these are post-feature polish commits but did not address this item.)
Suggested fix: define tokens for these values intokens.cssand reference them, e.g.--size-file-header: 36px,--font-size-label: 12px,--font-size-action: 11px,--size-icon-sm: 14px, etc., then update the class names accordingly. -
The symlink traversal issue (previous Major recommendation) has been addressed:
resolveWorkspaceRealPathnow usesrealpathto resolve symlinks before checking containment, and the protocol handler passes the real path toreadFile. Two new test cases cover both blocked (outside symlink) and allowed (inside symlink) scenarios. No further action needed on this front.
Summary
The follow-up commits successfully harden the workspace:// protocol against symlink-induced path traversal. The only remaining item from the initial review is the hardcoded UI dimensions in FilesTabView.tsx, which is a Minor issue that can be cleaned up in a subsequent polish PR. No new issues introduced.
Testing
- 1054/1054 tests pass (no change from previous run)
- New tests for
resolveWorkspaceRealPathcover symlink escape and internal symlink cases
Open-CoDesign Bot
Summary
When a design is bound to a workspace folder, the agent and the renderer now treat that folder as the source of truth for what's "in" the design instead of acting like the folder is empty.
seedFsMapFromWorkspaceloads the folder's existing text files (HTML/CSS/JS/TS/JSON/MD/SVG/...) into the runtime fsMap before the first turn, solist_filesandviewsee the user's real content. Capped at 500 files / 1MB per file with a skip-list fornode_modules,.git, build outputs, etc.workspace://protocol -- a privileged Electron scheme serves real assets directly from disk for iframe preview, with path-traversal hardening, an explicit MIME allowlist, no-store caching, and a designId regex check before any DB lookup. Subresources (./styles.css, fonts, images) resolve relative to the served HTML the way the user expects.files:list:v1IPC walks the bound folder and returns{path, kind: 'html'|'asset', size, updatedAt}to the renderer. Same skip-list and 500-file cap as the agent walker, factored into a sharedworkspace-walkmodule so the agent and the panel can never disagree on what counts as a workspace file. The panel itself is collapsible.<a href>to another.htmlin the same folder postsOPEN_FILE_TABup to the renderer instead of silently swapping the iframe under a tab still labelled with the previous file.list_filesrecursion -- the agent'slist_filestool now returns the full recursive tree underdir, not just the first directory level, so a single call gives a complete project map. The tool description was updated to match.doneverifier is skipped when the design has a bound workspace (the verifier's HTML-blob assumption doesn't hold once the agent edits real files), and the same folder can now be bound to multiple designs without the unique-constraint failure.Hard constraints checked
console.*inapps/desktop/src/main/**, no provider SDK importsany, strict mode passes,verbatimModuleSyntaxrespectedworkspace-protocol.test.ts(18 tests)Test plan
pnpm typecheck-- cleanpnpm lint(Biome) -- cleanpnpm test-- 1054 / 1054 pass, including newfiles-ipc.test.tsandworkspace-protocol.test.tsworkspace://Notes for reviewers
The two cleanup commits at the tip (
refactor(main): share workspace walk...andchore(workspace-protocol): ...) are post-feature polish: dedup of the skip-rules between the two walkers, expansion of the inlined nav-intercept script for readability, and a fix for two pre-existingtscerrors (Logger as CoreLoggerimport that was never exported, and aBodyInitannotation missing from this tsconfig's lib).🤖 Generated with Claude Code