Spec 004 — File browser
Status: Draft Related: PRD FR-19, bettergpt mockups
Goal
For any chat, show the chat's home directory as a tree. Tapping a file shows its contents (syntax-highlighted for code, rendered for markdown, image-preview for images). Inspiration: Cortex's mockups/bettergpt/index-split-view-filebrowser.html.
Routes
GET /api/chats/:chat_id/files/tree?path=<subpath>— JSON:[{name, path, type:"dir"|"file", size, mtime}]GET /api/chats/:chat_id/files/content?path=<subpath>— raw bytes; client picks renderer from MIME/extensionPOST /api/chats/:chat_id/files?path=<subpath>— write (later)DELETE /api/chats/:chat_id/files?path=<subpath>— delete (later)
Backend runs each as sudo -u chat-<hex> → reads the chat user's home. Paths are always relative to home; .. is rejected via realpath-and-prefix check.
UI
Desktop / tablet
Three columns: [Messages | File tree | File viewer or Terminal]. Tree + viewer share ~50/50 of the right two-thirds; messages fixed 1/3.
Mobile
Swipe-tab layout. Tab 2 is the file browser; it splits vertically into tree (top 40%) + viewer (bottom 60%) once a file is selected. Back-gesture returns to messages.
Mockup references in docs/mockups/README.md.
Renderers
| Type | Renderer |
|---|---|
| Markdown | react-markdown with remark-gfm |
| Code | react-syntax-highlighter with Prism themes |
| JSON/YAML | syntax-highlighted, collapsible tree |
| Image | <img> with width 100% |
<iframe> or react-pdf (post-v1) |
|
| Binary | "N bytes, not previewed" + download link |
Hidden files
Show dotfiles by default (Jacob wants to see .picortex/prompts/discriminator.md). Exclude .git/objects/* (noise) behind a "show all" toggle.
Security
- Path check:
realpath(home + path)must start withrealpath(home). - Enforce read-size cap (10 MB). Above that, return
413. - Content-type sniff server-side; never echo untrusted
Content-Typeheaders from the filesystem. - HTML content is returned as
text/plainunless explicitly requested as rendered markdown.
Caching
- Tree responses cached with
ETag(home-dir mtime + size-of-listing hash) - File content cached with
ETag(file mtime + size) - Browser cache:
Cache-Control: private, max-age=5
Testing
- Unit: path-sanitizer rejects
.., symlinks-out, absolute paths, null bytes. - Integration: real filesystem; navigate, assert sorted, assert rejected on another chat's home.
- E2E: Playwright on mobile; tap through tree, render markdown.
Open questions
- OQ1: Do we want write support in v1? (Yes, for editing
.picortex/prompts/discriminator.mddirectly.) Post-S7. - OQ2: Search within a chat's files? (Defer to v0.2 — the terminal can
rg.)