Repository navigation
Conversation
Terminal and response-viewer links only knew rooted absolute paths. Agent output is full of the other forms: Claude Code echoes an attached image as `~/repos/x/garage.png`, scripts print `builds/captures/x.png`, and Claude Code wraps tool output a few columns short of the edge, so a long path arrives as `.../captures/2` + `026-10-10/x.png` and only its prefix linked. - constants.js: RELATIVE_FILE_PATH_LINK_PATTERN + findFilePathLinks() (one entry point for both consumers; never inside an http(s) URL), resolveLinkedFilePath() (relative -> session workingDir, ~/ as is), terminalPathLine() (glues a path a program broke over lines; guarded so ordinary short lines are never joined). - terminal-ui.js: provider uses them, no overlapping links, opens the resolved path. Touch selection keeps terminalLogicalLine. - app.js: response viewer links the same forms; only its own links are resolved, a markdown document's rebased links stay as they were. - Server: registerExternalAttachment accepts ~/ (local homedir(); remote probe expands $HOME on the host). Guards run on the resolved path. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Folder paths in the terminal and the response viewer are now links (rooted or ~/ without an extension, relative with a trailing slash); a click opens a grid of the folder's supported files, newest first, with subfolder chips and a parent button. A tile opens the file preview, which pages through the folder with ‹ › / ←/→; the preview of any file gets a "Show folder" button. Server: POST /api/sessions/:id/folders grants + lists a folder (local fs or one ssh round trip for a remote case) under the attachment guard; GET …/folders/:folderId/files/:name/(raw|preview|thumbnail) serves one file by bare name through resolveServableAttachmentPath, so browser requests still never carry a path. Grants live in their own small registry so a big folder cannot evict the session's attachments. The attachment raw route's body is now the shared serveServableRaw helper. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
CLAUDE.md key pattern and architecture-invariants (File-path links) get the new path forms (`findFilePathLinks`, `~/` expanded server-side, `terminalPathLine` join rules) and the folder grid (grant + bare-name routes, own registry, z-order, captured keys); remote-sessions lists the folder routes for a remote case; security-architecture states the trust level of a folder grant. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Thanks @Randalix, this is a really nice piece of work. It makes Two things need a change before this can merge, both on the server side of the folder grid:
Smaller things, fine to fold into the same push:
Once 1 and 2 are in, I am happy to merge. If you would rather I take the smaller items at merge time, say so and I will. |
… smaller items
- Multi-user: a non-admin's folder must resolve inside their own userSpacePath
or the session's workingDir (folderAllowedForUser, the path picker's rule),
checked on the resolved folder before it is read and again on each served
file after symlink resolution; 403 otherwise.
- listFolder probes the local folder with probePathKind({ pastCap: true })
first (unknown -> 503 via describeUnknownPath, never 404), then async
fs.realpath; resolveFolderFile uses probePathKind instead of fs.stat.
- The guard runs on the realpath before readdir.
- FOLDER_PATH_LINK_PATTERN ends a folder at ':' (quadratic `://` rescan on
colon runs; PATH=/a:/b linked as one folder).
- Load counters drop stale preview / folder-grid responses; closing the grid
invalidates a listing still in flight.
- validFolderFileName refuses a leading dot; the remote listing matches
extensions in any case.
- Docs: folder grid in the z-index lists; multi-user rule and bounded probe in
the invariants and security docs.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Thanks for the careful review — all of it is in 1. Multi-user. New 2. Bounded probing. Smaller items
Full |
|
Thanks again @Randalix, and thanks for turning the first round around so fast. This PR makes Two things need a change before merge, both found by running the shipped code:
Two smaller bugs, ideally in the same push:
Nits, no action needed for merge:
Once 1 and 2 are in, I am happy to merge. If you would rather I take the two smaller bugs at merge time, say so and I will. |
…gate; accept long names Folder routes only looked at the requested name, so `shot.png -> ../secret.bin` and `notes.txt -> ../history` were served (registration answers 400 for both), and a FIFO or device behind a supported name parked a worker on an open that never returned. After resolving, the target's extension must be supported and it must be a regular file (locally fs.stat, remotely the probe kind). A 101..255-character file name answered 404 "Route not found": Fastify caps a path parameter at 100 characters. The server now sets routerOptions.maxParamLength (MAX_ROUTE_PARAM_LENGTH, shared with the route test harness) and a wiring guard pins it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
`git ls-files`, `find` and `ls -R` print one path per row. An extensionless entry followed by an equal-or-shorter one looked like a path broken short of the edge, so `src/components` + `src/index.ts` linked `src/componentssrc/index.ts` and the real link was gone. Measured with the shipped terminalPathLine over this repo: `git ls-files` 8 false joins (one over 6 rows), `find src docs scripts` 5. A next row is now a new entry when it opens with `./` or `../`, or has a `/` and opens on the same top directory the broken token did. A wrapped path continues inside a name, so the captures/2 + 026-10-10/... case still joins. After the change: 1 and 0 (`docker/server.Dockerfile` + `docs/SPEEDRUN.md`, a broken token with no extension that no row-shape rule can tell from a wrap). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A Docker case is a local session for the file routes, but `~` inside the container is the container's own seeded home. Expanding `~/x.png` on the host opened a different file than the one the agent printed. The attachment POST and POST …/folders now answer 400 for `~/` when the session has `docker` set (absolute paths are unaffected: the workspace is a bind mount). Docs: the resolved-target gate, the route-parameter limit, the listing join rule and the Docker `~/` refusal in architecture-invariants.md, security-architecture.md and CLAUDE.md. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Thanks, all four reproduce here and are fixed in 1. Resolved target. After 2. Listing join. Your rule, in 3. Long names. I took the second option: 4. Docker. Docs moved with it (architecture-invariants, security-architecture, CLAUDE.md). Full |
sshd runs the command in the remote user's login shell. On Hufflepuff cd is an alias that echoes the directory, so the listing script captured the folder path twice and zsh aborted on the glob: every folder grid over ssh failed with "remote host unreachable: no matches found". Wrap the script in sh -c, as the transcript read already does. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
One more commit on the branch (5b8c738), found while running the folder grid on an SSH case: the remote listing failed on a host whose login shell makes
The listing script now runs under an explicit Local runs on this commit: folder-grid-routes, remote-files, file-routes-remote and file-path-link-forms, 118 tests, plus tsc, eslint and prettier on the two touched files. I did not repeat the full suite for this one commit; CI will. Confirmed live on a Hufflepuff case: workspace, a subfolder and |
Closes #603.
Two commits, one PR: the folder grid's links are the third stage of
findFilePathLinks(), which the first commit introduces, so the second cannot land alone.1.
feat(preview): open ~/, relative and line-broken file pathsconstants.js:RELATIVE_FILE_PATH_LINK_PATTERN(same extension list as the rooted one, pinned equal),findFilePathLinks()(rooted first, then relative; no overlap, never inside anhttp(s)URL),resolveLinkedFilePath()(relative →workingDir,./..collapsed; absolute and~/unchanged),terminalPathLine().terminal-ui.js) and response viewer (app.js). Only links the linkifier created (data-linkified) are resolved on click — a rendered markdown document's own rebased links keep their old behaviour (found in review: otherwise a../link in a document would have gone through the attachment route instead of bouncing off the workspace route).~/is routed to the attachment route (_isExternalPreviewPath);registerExternalAttachmentexpands it locally withhomedir(), the remote probe script with$HOME; the guard then runs on the resolved path exactly as before.terminalPathLine, link provider only — touch selection keepsterminalLogicalLine): join the next row when the previous one ends in a path token with/and no extension and the join reaches an extension; never join a row that starts a new rooted/~/path; a 3+ row chain is kept only if a later row completes it; the broken row must be at least as long as the continuing one (keepscd /mnt/foo+ a longer next line apart).2.
feat(preview): folder grid — browse every previewable file of a foldersrc/folder-listing.ts:listFolder()— absolute or~/; localrealpath+readdir, remoteremoteListFolder()(one ssh call throughrunWithRemoteSshLimit; POSIXsh, NUL-separated records,cd -Pfor symlinks, GNU/BSDstat); guard on the folder and on every listed file; hidden entries and unsupported types left out; newest first; capped at 1000 entries.folderGrants: own registry (fld_<uuid>, same folder → same id, 32 per session, cleared with the session).file-routes.ts):POST /api/sessions/:id/folders;GET …/folders/:folderId/files/:name/raw|preview|thumbnail.:namemust be a bare supported file name (no/,\,.,.., NUL); then the joined path goes throughresolveServableAttachmentPath. The attachment raw route's body is now the sharedserveServableRaw()(unchanged behaviour: range, xlsx cap, 502 on ssh failure). Remote: office preview and thumbnail answer 400, as for attachments.Not a folder): the client falls back to the file preview onNot a folder, so a clicked extensionless file still opens (found in review).panels-ui.js,index.html,styles.css): grid overlay at z 5090 (below the preview's 5100, above the response viewer's 5000), with the foldable hinge rule like every centred overlay. Tiles: images lazy fromraw, PDF/Office fromthumbnail(type label where the server has none), others a type label. A tile's preview renders from the folder routes (openFilePreview(..., { folderFile })), so paging never registers attachments. ‹ › and ←/→ page (wrapping); Esc closes the preview, then the grid; keys are caught in the capture phase so they never reach xterm. "Show folder" in any preview whose folder is known (not for attachment cards that only carry a bare name).Docs
CLAUDE.md(File-path links key pattern),docs/architecture-invariants.md(two new paragraphs under File-path links),docs/remote-sessions.md(route table),docs/security-architecture.md(folder grants trust level).Tests
test/file-path-link-forms.test.ts(18),test/routes/folder-grid-routes.test.ts(15, including the listing script on a realshwith~, symlinks, odd names, the cap, file vs missing),test/folder-grid-ui.test.ts(14).link-provider-regex(both new patterns in the linear-time check, extension lists equal),response-viewer-file-links;foldable-layout(overlay count 7 → 8);webview-loopback-linksreads a 3500-character window instead of 2500 (the extra folder branch pushed the URL handler to char 2498; the order it asserts is unchanged).npm teston the branch (rebased on currentmaster, after 1.41.0): 10715 passed, 12 skipped, 0 failed (546 files).tsc, eslint, prettier,check:frontend-syntax,check:public-assetsgreen.Live verification (my instance, 2026-10-10)
~/path and an absolute path hard-wrapped like Claude Code (first row longer). One link each, the wrapped one across both rows and hoverable from both; click → preview, image loaded (naturalWidth192) for all three, relative resolved to absolute,~through the attachment route.POST …/folderson a 29-file style-frame folder → 29 files, 5 subfolders, newest first;rawof a tile200 image/png1.27 MB;..%2F..%2Fetc%2Fpasswd→ 400. Browser: 29 tiles, 28 images loaded after 2.5 s (lazy), tile → preview, → pages to the next file, Esc closes the preview (grid stays), second Esc the grid.Known limits (deliberate)
workingDir, not a directory the agentcd'd into.Node.jsunderline as relative paths (click → not found).No changeset.
🤖 Generated with Claude Code