The web app6 of 12
Files
On this page
The Files panel is a window onto one real folder on your disk. It is not a per-call scratchpad and it is not a database.
Screenshot placeholder: the Files panel in the left rail — a search box, four filter chips (All / Code / Images / Docs), a folder row with a yellow "new" badge, and three file rows each with a coloured type icon, an extension tag, an open-in-new-tab control and a bin.
The folder is her working directory
The library lives at ~/.kotoba/files and can be moved with KOTOBA_FILES_DIR
(api/src/kotoba/core/file_library.py:32-36). By default the library is her working directory —
they are the same folder, not a copy of one (api/src/kotoba/core/file_library.py:1-7).
That has two consequences worth knowing:
- Anything you drop into that folder yourself is immediately something she can read and work on.
- Anything she creates or edits appears in this panel, across turns, restarts and page reloads
(
api/src/kotoba/server.py:899-906).
The disk is the truth. The panel does not merge events into a list it keeps: a files_changed event
bumps a counter and the whole list is re-read (components/panels/FilesPanel.tsx:304-321).
Getting files in
There is no upload control in this panel. The three ways a file gets into the library are:
- She makes it. Her file tools write into it.
- You put it there with your own file manager, terminal or editor — it is an ordinary folder.
- The transcript's paperclip, for images, PDFs and text files. That is a different path: an attachment is carried by the next turn, not saved into the library. See The transcript.
Getting files out
Three controls per row (FilesPanel.tsx:79-133):
| Control | What it does |
|---|---|
| The row itself | opens the file in the in-app viewer |
| The arrow icon | opens it in a new browser tab |
| The bin | deletes it — two clicks |
Delete arms on the first click and fires on the second; if you do not confirm within 3 seconds it
disarms itself (FilesPanel.tsx:64-68, 112). There is no undo — the row is removed optimistically and
the disk is re-read straight afterwards to correct the list if the delete failed
(FilesPanel.tsx:294-300).
The viewer's header has New tab and Save; Save is a plain download of the raw file
(FilesPanel.tsx:215-220). Escape closes the viewer, and so does clicking outside it
(FilesPanel.tsx:178-183, 204).
The tree
Files are shown one folder deep at a time, with a breadcrumb trail back to the root
(FilesPanel.tsx:391-401, 423). A folder row carries a badge when it contains anything unseen at
any depth, and new wins over edited (FilesPanel.tsx:337-357).
Files in the current folder are sorted newest-first by modification time (FilesPanel.tsx:358).
Searching switches the panel out of tree mode: it searches the whole library by path and shows full
paths in the results (FilesPanel.tsx:325-334, 431).
Four filter chips: All, Code, Images, Docs (FilesPanel.tsx:266-271). The
classification is by extension (FilesPanel.tsx:25-40):
- Code —
py js ts tsx jsx json html css scss sh rb go rs java c cpp yml yaml toml - Images —
png jpg jpeg gif webp svg ico bmp, or anything the backend tagged as an image - Docs — everything else, including
md txt rst log csv env pdf
A new or edited badge sits on a file until you open it; opening one tells the backend so the badge
does not come back on the next page load (FilesPanel.tsx:284-292).
The extension is pinned outside the truncated part of a long name. An ellipsis eats the end of a
name, and report.html and report.pdf used to render as identical cards
(FilesPanel.tsx:74-78).
The viewer
What you get depends on the file (FilesPanel.tsx:225-262):
| File | View |
|---|---|
| Image | rendered directly |
| Markdown | rendered, with a Code toggle for the source |
| HTML | rendered in a sandboxed iframe, with a Code toggle for the source |
| Anything else | plain monospace text |
| Binary, or gone | "Couldn't open this one — it may have been a binary file or it's no longer around." |
The HTML preview runs in an iframe with sandbox="allow-scripts" and without allow-same-origin,
which makes it a unique null origin that cannot read the parent page's storage
(FilesPanel.tsx:236-243). The Markdown renderer does not emit raw HTML.
Everything the viewer draws — including the "Code" view — is stripped of invisible and bidirectional
control characters first (FilesPanel.tsx:162-163, 184-190). A source file that reverses itself on
screen is the exact attack this is for.
The path is treated differently from the name. The path is protocol: it round-trips to disk
byte-for-byte for open, delete and the raw URL, so it is kept raw — scrubbing it would orphan a file
with a hostile name. The gate sits on every drawn name instead (FilesPanel.tsx:1-9, 57-58).
Limits
The backend caps what may be written into the library (api/src/kotoba/core/file_library.py:26-29):
| Limit | Value |
|---|---|
| Text file written by a tool | 200 000 characters |
| Image written by a tool | 12 000 000 bytes |
| File count before pruning | 500 — and pruning never runs by default, because it only applies when the library mirrors a separate working directory |
Paths are jailed: nothing she writes and nothing this panel opens can escape the library folder
(api/src/kotoba/core/file_library.py:141-142, server.py:909-921).
