029 · Files
Files: explorer, editor, diff and language intelligence (spec_files_029)
| Status | Phase 1 implemented (2026-09-27, on main); phase 2 largely done; phase 3 detailed in spec_visual_editor_037 (draft) |
| Source | pkg/engine/files.go, pkg/engine/tools/userfs.go, proto/blitz/v1/file.proto, apps/service/internal/server/files.go, apps/electron/src/main.ts (the close prompt; unsaved.go in the Wails app until v0.6.5); the page’s src/files/ |
| Depends on | spec_workspace_018, spec_service_021, spec_desktop_024, spec_filetools_006 |
The desktop app shows only the conversation: you can’t see where things are, so you can’t point the agent at them, and you can’t make a small change yourself. This adds the workspace’s files to the window: a tree to browse, an editor to read and change them, diffs, and language intelligence.
The owner’s decisions (2026-09-27):
- The editor sits beside the conversation, not in a view of its own, so you can talk while you look (“test and learn”: revisit once used). Revisited the same day: the window is laid out like an IDE — files on the left (minimizable), the editor in the middle, the chat on the right, the workspace a dropdown in the top bar (spec_desktop_024 DSK-65).
- Blitz installs a missing language server when you agree (phase 3).
- The agent is told about files you change between its turns.
- Hidden files (dotfiles, ignored files, the agent’s blocked paths, and agent instruction files such as AGENTS.md) are hidden by default, with a toggle to show them.
| Phase | What |
|---|---|
| 1 | FileService; the Files shelf (tree, git status, hidden toggle, file operations); the editor (tabs, highlighting, word and keyword completion, saving with conflict detection); quick open; paths in the conversation open the editor; the agent told of your edits |
| 2 | Diffs in the editor (unsaved against disk, against HEAD, the agent’s changes this session) and in the Changes view, reverting one hunk of the agent’s changes; find in files; Add to chat (a file or lines as a reference in the composer); badges for files the agent read or changed; image and Markdown previews; reveal in Finder |
| 3 | LanguageService over language servers: completion, hover, go to definition, references, problems (with Ask Blitz to fix); installing a missing server; the agent’s use of the same. Detailed, with the visual Markdown editor, in spec_visual_editor_037 |
Every request names its workspace (SVC-10) and a path relative to it.
- FIL-10 Scope: the workspace directory only, through
os.Root(tools.Workspace), so no path,..or symlink leaves it. Other roots the agent may use (sandbox.allowed_paths) aren’t browsed. - FIL-11 The agent’s path rules bind the agent, not you: a blocked path (
sandbox.blocked_paths) or a read-only root is listed with the rule that applies (blocked,read_only) so the window can say so, and you may open and change it..gitis never listed. - FIL-12
ListDir(path, show_hidden): the directory’s entries, folders first then by name, each with its kind (file, folder, symlink), size, modification time, git status (FIL-13), why it’s hidden (dot,ignored,blocked,agent, or none) and the agent’s rule. 2026-10-14 (#35):agentis a file named inmemory.filesormemory.local_files(AGENTS.md, CLAUDE.md, GEMINI.md, BLITZ.md, CLAUDE.local.md, BLITZ.local.md by default), in any folder: support files, hidden like dotfiles; they still open from ⌘P,/memoryand the settings. Hidden entries are left out unlessshow_hidden. At most 5,000 entries (truncatedbeyond). - FIL-13 Git status comes from
git status --porcelain=v1 -z --untracked-files=all --ignored=noand ignored files fromgit check-ignore -z --stdin, both with the repository’s filter drivers blanked andcore.fsmonitoroff, asGitDiffdoes (running git never runs the repository’s configured programs). Statuses: modified, added, deleted, renamed, untracked, conflicted; a folder reports that something under it changed. 2026-10-04: a file also says how much of its change is staged (staged):all(in the index, nothing changed since),some(staged, then changed again) or none (untracked and conflicted files count as not staged). Outside a repository there’s no status and only dotfiles are hidden. - FIL-14
ReadFile(path): the text, aversion(SHA-256 of the content), size, and whether it’s binary (a NUL in the first 8 kB, or not UTF-8) or too large (over 2 MB, ortools.max_file_size_bytesif smaller); binary and too-large files come without content. - FIL-15
WriteFile(path, text, version)replaces the file atomically (temporary file and rename, its mode kept) only if its content still hasversion("": it must not exist). OtherwiseFailedPreconditionwith reasonFILE_CHANGEDand the current version. It returns the new version. - FIL-16
CreateFolder,Rename(from, to)(never over an existing path),Delete(path)(a folder with everything in it). Your writes aren’t checkpoints of the agent’s turns, so Undo last turn doesn’t undo them; undoing an agent’s turn over a file you’ve since changed asks first (CHK conflict, as before). - FIL-17
FindFiles(query, limit): files whose path matchesqueryas a fuzzy subsequence, best first (consecutive runs, matches at the start of a name or after/,_,-,., and shorter paths rank higher), leaving out hidden files. The list comes fromgit ls-files --cached --others --exclude-standardin a repository, else a walk (at most 50,000 files), and is kept for 5 seconds. Withfolders(for @ completion, DSK-77a) the files’ folders are candidates too, asdir/. - FIL-18
StatFiles(paths): each file’s version (or that it’s gone), so the window can see changes made by the agent, a shell command or a worker.
- FIL-20 Your writes, renames and deletions are recorded per workspace. The next turn’s prompt (not an aside) carries them, and they’re forgotten:
<user-edits>The user changed these files in the editor since your last turn: a.go (edited), b.go (deleted). Read them again before relying on what you saw.</user-edits> - FIL-21 A change you make while the agent waits for approval of an edit to the same file is caught as before (the edit is refused and the agent reads the file again).
- FIL-30 The Files shelf: a panel on the left of the window, minimized to a rail (Show files, Go to file) with its Minimize button (remembered per window). It lists the workspace as a tree loaded folder by folder: icons by kind, git status as a coloured letter (M, A, D, R, U, !) with the name tinted (a staged change’s letter on a tinted chip, a partly staged one’s outlined; the tooltip says Staged, Partly staged or Not staged), folders with changes marked, hidden entries dimmed when shown, a lock on blocked or read-only ones with the rule in its tooltip. The header has New file, New folder, Refresh, Collapse all, Show hidden files (off by default, remembered per window) and Minimize. Its right edge is dragged to widen or narrow it (200–560 px, remembered; double-click for the default 272 px). It never floats over the editor or the chat: in a narrow window the panels shrink to their minimums (the shelf to 180 px), and the shelf can be minimized to the rail (spec_desktop_024 DSK-37).
- FIL-31 Each entry’s menu: Open, New file, New folder (in a folder), Rename (in place), Delete (confirmed, saying a folder goes with everything in it and that only git can bring files back), Copy path, Copy relative path. Keyboard: arrows move and open folders, Enter opens, F2 renames, Delete deletes, Escape clears the selection. A click on the tree’s background also clears it, so New file and New folder go in the workspace again. 2026-10-01: drag an entry onto a folder (or a file in it) to move it there, or onto the tree’s background to move it to the workspace (
RenameFile; not onto itself, under itself or where it is). The folder it would go in is highlighted, a closed folder opens when the drag rests on it, and a name that’s taken says so. - FIL-32 Opening a file shows it in the editor in the middle, between the shelf and the chat (switching the middle from Changes or Workers). With no file open the chat takes the middle instead, and moves back to the side when one opens. The chat’s width is dragged at its left edge (remembered); the editor takes the rest.
- FIL-33 The window notices changes made elsewhere: after each tool call that writes, at the end of a turn, and every 5 seconds while the shelf or editor is shown, it lists the open folders again and checks the open files (
StatFiles). A file you haven’t changed reloads; one you have gets a bar: Reload (discard yours) or Keep mine (the next save asks to overwrite). - FIL-34 Go to file (⌘P, the top bar’s search, the files rail): the command palette’s files,
FindFilesas you type, first among its results; Enter opens,name:12at line 12 (spec_desktop_024 DSK-78e). - FIL-35 Paths in the conversation open the editor: file paths in tool calls, and inline code in answers that names a file in the workspace (for example
internal/cart/discount.go, with:42or:42:7going to that line). The inline code gets a link style only once the file is known to exist (FindFiles’ list).
- FIL-40 CodeMirror 6: tabs (a dot for unsaved changes, middle click or ⌘W closes, asking about unsaved changes; right-click for FIL-40a), the path as breadcrumbs, line numbers, the active line, bracket matching, folding, search and replace (⌘F), go to line (⌃G), multiple cursors, indentation from the file (tabs or spaces), soft wrap off (a toggle).
- FIL-40a 2026-10-14 (#32): a tab’s context menu: Close, Close others, Close to the right, Close saved and Close all (an item with nothing to close is disabled). When the tabs it closes include unsaved ones, one question names them all: Save saves each and closes those that saved (a failed save stays open), Discard closes them all, Cancel keeps them. The active tab stays if it’s still open, else the next one that is, else the one before.
- FIL-41 Highlighting by file name (
@codemirror/language-data: Go, TypeScript and JavaScript, Python, Rust, Java, C and C++, JSON, YAML, TOML, Markdown, HTML, CSS, SQL, shell, protobuf, Dockerfile and more), each language’s parser loaded when first needed. Colours from the window’s theme, light and dark, matching the conversation’s code blocks. - FIL-42 Completion without a language server: the language’s keywords and snippets where CodeMirror has them, and words from the open files. Phase 3 adds a language server’s (spec_visual_editor_037 VE-32).
- FIL-43 ⌘S saves (
WriteFilewith the version it loaded).FILE_CHANGEDasks: Overwrite, Reload (discard yours), or cancel; phase 2 adds Compare. - FIL-44 Binary and too-large files open as a note saying so, with the size. Blocked files open with a bar: “Blitz’s agent can’t read this file”; read-only roots likewise.
- FIL-45 Unsaved changes survive switching workspaces and views; closing a workspace (any way: its menu, the workspace dropdown, its details) or the window with unsaved changes asks first. The page tells the app how many files are unsaved (
SetUnsaved, in the window’s language), and the app’s close handler asks with a native dialog (the Electron window’s, EL-14).
- FIL-50 Diff (
@codemirror/merge): unsaved against disk; the file againstHEAD; the agent’s changes this session (its checkpoints). Side by side or inline. The Changes view uses the same view. - FIL-51 Revert one hunk of the agent’s changes (a write of the file without that hunk, recorded as your edit, FIL-20).
- FIL-52 Find in files (⌘⇧F): the agent’s grep over the workspace, results by file with the line, opening at the match.
- FIL-53 ✅ Files and folders done 2026-09-29. Add to context: the file bar’s @ (Add to context) and Start a conversation about this file buttons, and the tree’s right-click Add file/folder to context and Start a conversation about this file/folder, put
@path(a folder aspath/) at the end of the workspace’s composer (once;addToContext,appendMention), or in a new session’s; an image is also attached. The service expands them (WS-21a). Blocked files and folders don’t offer them. Still to do: dragging onto the composer, and the selected lines (@path:10-24, Explain the selection). - FIL-54 ✅ Previews done 2026-09-29. Previews (
files/Preview.tsx,previewKind): Markdown (.md,.markdown,.mdx) and SVG are text with a Source / Preview switch in the file’s bar (the preview renders the editor’s current text, unsaved edits included: Markdown with the chat’s renderer and the document style of FIL-57, SVG as an image so nothing in it runs). 2026-10-18 (the owner’s request): HTML (.html,.htm,.xhtml) is text with the same switch and opens in Preview as Markdown does: the editor’s current text in a frame sandboxed without the app’s origin (allow-scripts allow-forms allow-popups allow-modals: its scripts run but reach neither the app, its storage nor the service), on white, redrawn 0.4 s after typing stops; what the page takes from the workspace by a relative path (stylesheets and scripts inlined as text, images, media and icons asdata:URLs;files/htmlPreview.ts) is read through the file API, never outside the workspace; web URLs are left as written; a file that can’t be read stays as referenced. 2026-10-18: A Word document (.docx) previews read-only as its text:ReadPreviewgives it as Markdown (docx.ToMarkdown: paragraphs, headings, bold and italic, list items, tables, links’ text; images, comments, headers and footers left out), shown as a Markdown file is. 2026-10-01: Markdown opens in Preview (defaultView), except when it opens at a line (a#L12link, Go to file; a workspace search hit opens the preview at the hit, spec_search_035 SRCH-43) or is empty (a new file, to write); the switch reads Preview / Source for Markdown (the view it opens in first); a tab keeps the view it was switched to, and ⌘⇧V / Ctrl+Shift+V switches; images (.png .jpg .jpeg .gif .webp .bmp .ico, on a checkerboard, with their size in pixels) and PDFs (drawn page by page with pdf.js, loaded only when a PDF opens; the first 100 pages) show only as previews, fromReadPreview(their bytes and media type, up to 32 MB; other files areNO_PREVIEW). In the app, images and PDFs also have Open in the system viewer (OpenDocument: only those file types, only a regular file). 2026-10-02: sound (.wav .mp3 .m4a .aac .ogg .opus .flac) plays in a player, and a Markdown file can be exported as a PDF beside it (spec_desktop_024 DSK-93, DSK-94). Still to do: tree badges for files the agent read or changed. (Reveal in Finder is FIL-58.) - FIL-55 ✅ Done 2026-10-01. Diagrams: a
```mermaidblock draws as a diagram in the chat and in Markdown previews (mermaid.tsx, Mermaid 11 loaded the first time one shows). Strict security level: labels are text, no scripts or click handlers, and the SVG passes through Mermaid’s DOMPurify before it reaches the page. The colours are the window’s (Mermaid’s base theme from the M3 tokens, per theme). The block has Show source and Copy; text that doesn’t parse (a reply still coming in, a typo) shows as its code with why. Agents are told to draw diagrams this way, never in ASCII (the engine’s## Diagramsinstruction, every agent). - FIL-56 ✅ Done 2026-10-01. Links in Markdown (
files/mdlinks.tsresolveLink): web and mail links open in the system browser, as before;#headingscrolls the preview to it (headings get GitHub’s ids, and a#on hover); a relative path opens what it names, resolved against the document’s folder (/pathfrom the workspace’s root): a file in the editor, at#L12its line, at#headingits preview scrolled there; a folder in the tree. A path that leaves the workspace isn’t followed, and one that isn’t there says so. In the chat, a relative link opens the workspace file it names when the workspace has it. - FIL-57 ✅ Done 2026-10-01. Previews are documents (
files/document.css): a 760 px measure, 15 px text at 1.7, headings with rules under the first two, tables with a header row and stripes, quotes with the accent bar, code on the window’s code colours, diagrams on a card; all from--doc-*variables over the M3 tokens. The chat keeps its compact style. Settings › Appearance › Document styles… (a dialog; until v0.7.1 the section Settings › Documents) has CSS for each theme over the defaults (markdown_css_light,markdown_css_darkindesktop.json, 64 KB each): a CSS editor per theme, starting from a template of every variable, commented out; Reset; and a sample, in the window’s theme, that shows it as it’s typed. The CSS of the theme in effect is the page’s last stylesheet. - FIL-58 ✅ Done 2026-10-01. Show in Finder / Show in Dolphin / … in the tree’s right-click menu (the background’s shows the workspace), in the app only (
RevealPath,FileManager): macOSopen -R, Windowsexplorer /select,, Linuxorg.freedesktop.FileManager1.ShowItemsover D-Bus (dbus-send; Dolphin, Files, Nemo, Caja, Thunar), else the folder it’s in. The name is the system’s file manager’s (on Linux fromxdg-mime query default inode/directory; “file manager” when unknown). The path must exist; nothing is run. - FIL-59 2026-10-01. Git in the tree’s menu (
FileService.GitFileAction,Workspace.GitFileAction; rules infiles/gitMenu.ts, unit-tested), only in a repository and only for an entry: Include in commit (git add --allthe file, or what changed under the folder), Leave out of commit (git restore --staged), Discard changes… (git restore --source=HEAD --staged --worktree, only for a tracked change, confirmed: it puts the file or folder back as it was in the last commit, which only git’s history can undo; the agent is told, FIL-20), Do not version (an anchored/pathor/folder/line in the workspace’s.gitignore, once), and Commit changes…, which opens the commit dialog (spec_desktop_024 DSK-90a). Which items show follows the entry’s git status: for a file, Include in commit unless it’s all staged and Leave out of commit when any of it is (2026-10-04: they followed the status alone, so both always showed and the flag never changed on staging); for a changed folder, both. Git runs with the repository’s filter drivers and fsmonitor off and literal pathspecs; outside a repository the reason isNOT_A_REPOSITORY. The tree and open tabs reload afterwards. The tree’s dialogs render at the page’s level, so glass panes don’t confine them. - FIL-59a 2026-10-14 (#34, #33).
- Version it again (was Remove from .gitignore): an entry git ignores (
FileEntry.ignored, set fromgit check-ignorewhatever its hidden reason, so an ignored dotfile shows as ignored too) offers Version it again instead of Do not version (GIT_ACTION_UNIGNORE,Workspace.unignore). It takes the anchored line Do not version wrote (/path,/folder/) out of the workspace’s.gitignore. When git still ignores the path after that (a pattern such as*.log, another.gitignore,.git/info/exclude), the file is put back as it was and the reason isIGNORED_ELSEWHERE, naming the pattern and itsfile:line: Blitz never rewrites rules it didn’t write. Ignored entries show only with hidden files shown. - View history (was View revisions): a tracked file’s Version control menu has View history… (since FIL-59b,
files/HistoryDrawer.tsx). It lists the commits that changed the file, newest first, following renames (FileService.GetFileHistory:git log --follow, up to 50, at most 200), with subject, short hash, author and date. Picking one shows how the file today differs from it (GetFileRevisionDiff:git diff -M <commit> -- <then> <now>, so a rename since shows as one). Restore this version asks first, then writes the file as it was in that commit (RestoreFileRevision:git cat-file blob <commit>:<then path>), refused withFILE_CHANGEDif the file changed since the dialog read its version. The index is left alone, so the restore shows as a change to commit or discard, and the agent is told (FIL-20). A revision must be a hex hash from the file’s own history (BAD_REVISIONotherwise), so nothing else reaches git’s command line. The tree and open tabs reload after.
- Version it again (was Remove from .gitignore): an entry git ignores (
- FIL-59b 2026-10-15 (the owner’s request). History as a graph, for files, folders and the workspace.
- Where: View history… on any tracked file or folder in the tree’s Version control menu, on an editor tab’s menu, and Workspace history… in the Changes menu. They all open
HistoryDrawerthroughshowHistory(events.ts). - The graph: the commits that changed the file (following renames), anything under the folder, or anything at all, newest first, 100 at a time with Show older commits. Each row has its lane in a column drawn as SVG beside it (
files/historyGraph.ts: lanes asgit log --graphlays them out, from each commit’s parents as git simplifies the history to the path; a merge is a ring, a branch has a lane and colour of its own until it joins). Rows are grouped by date (Today, Yesterday, This week, This month, then by month), with the subject, short hash, an author dot coloured from the author’s address, how long ago, and a bar of the lines added and removed (files/history.ts). - Diffs: one commit selected shows Its changes (against its first parent, or nothing for a first commit; the default for folders) or Compared with now (the default for files). Shift-, ⌘- or Ctrl-click a second commit to compare the two, older to newer. Diffs over 2 MB are cut at a line and say so.
- Restore: Restore this version for a file (FIL-59a). Restore this folder as it was (or the workspace) first asks the service which files it would put back (
preview): those changed since are restored and those deleted since come back; files added since are kept; nothing is staged. A confirmation lists them (the first 20, then a count). - The service:
GetFileHistory(a path,""for the workspace;skip,more; each revision’s parents, author’s address, lines added and removed, files),GetFileRevisionDiff(against: now, parent or another commit) andRestoreFileRevision(a folder, withpreview). A revision must be a hex hash that names a commit (git rev-parse --verify … ^{commit}), and for a file one in its history (BAD_REVISION).
- Where: View history… on any tracked file or folder in the tree’s Version control menu, on an editor tab’s menu, and Workspace history… in the Changes menu. They all open
- FIL-60 2026-10-01. New files go to git. When a turn ends in a workspace inside a git repository, the files the agent created in it (absent before the turn and there after, from its checkpoint:
Checkpoints.Created) are added to the index in onegit add, with[git] auto_add(on by default). Left out: files that existed before (changed, not created), files git ignores (git check-ignore, run first, sincegit addrefuses an ignored path), scratch files (*.tmp,*.temp,*.swp,*.swo,*~,*.orig,*.rej,*.bak,.DS_Store,Thumbs.db,desktop.ini,.#*, and anything undertmp/,temp/,.tmp/,.cache/), and files outside the workspace. Files made by shell commands aren’t checkpointed, so build output never qualifies. The turn says what was added (“Added to version control: a.go, docs/guide.md”), or that it failed; nothing is committed. Worker runs and stopped turns count too.
- FIL-64 Was FIL-60, a number §7’s git item also had; renumbered 2026-10-08.
LanguageService: the service runs a language server per workspace and language (gopls, typescript-language-server, pyright, rust-analyzer, and others by configuration) and answersComplete,Hover,Definition,Referencesand aDiagnosticsstream. The page’s transport can’t hold a two-way stream, so these are separate calls. Detailed in spec_visual_editor_037 §5–§6. - FIL-61 (Detailed in spec_visual_editor_037 VE-43.) Language servers run the project’s code (build files, plugins), so they start only in trusted workspaces (the project’s settings trusted, spec_project_config_031), in the OS sandbox with the workspace writable.
- FIL-62 A missing server: the window offers to install it. Blitz installs into
~/.blitz/tools(never system-wide) with the language’s own tool (go install gopls@<pinned>, npm for the TypeScript and Python servers,rustup component add rust-analyzer) or a release binary checked against its published checksum, in the OS sandbox with the network allowed, and says what it runs first. If the language’s own tool is missing, it says how to get it. - FIL-63 Problems inline and in a list, with Ask Blitz to fix. The agent gets tools over the same servers (definitions, references, diagnostics).
- FIL-90 The engine: confinement (paths out,
.., symlinks out,.git), hidden reasons (dot, git-ignored, blocked), git status of each kind, versions andFILE_CHANGED, create/rename/delete, fuzzy ranking, and the user-edit note reaching the next turn once. - FIL-91 The service: each call over Connect, and
FILE_CHANGEDwith the current version in the error’s details. - FIL-92 The page: the tree’s state (expand, refresh keeping what’s open, hidden toggle), path detection in answers, language choice by file name, the unsaved-changes rules; screenshots with the fake service.
- FIL-70 2026-10-03. A CSV or TSV file (
.csv,.tsv,.tab) opens as a table (TableView.tsx; source view one click away when the editor can hold it; a file too large for the editor, or not UTF-8, only as a table). The engine reads it a page at a time (FileService.ReadTable,pkg/engine/table), streamed, so a file of any size opens: about a second a page for 900,000 rows (175 MB). Its format is sniffed: the delimiter (TSV by name, else the header’s commonest of,;tab|) and the encoding (Latin-1 / Windows-1252 decoded, said by a badge); quotes are lenient and ragged rows read. - FIL-71 Its columns show their kind (number, date, text, empty; from the first 2,000 rows), and what they hold once a model described the file (search.enrich, spec_search_035 SRCH-63) under the name and on hover. A search across all columns (ignoring case); a filter under each column: a comparison with a number (
>5,>=5,<0,<=0,=10,!=10), a range (10..20), equality with text (=north,!=north),emptyor!empty, else text the cell contains; a filter that can’t be read says so (BAD_FILTER). A header’s click sorts by it, ascending then descending then off: numbers as numbers, empty cells last, ties in file order; past 200,000 matching rows the rows stay in file order and the view says to filter first. Pages of 100 rows, with “matched of total rows” and the rows’ own numbers. Opened at a line (a search hit), it shows that row’s page with the row marked.
- FIL-72 2026-10-03. Find in the file a tab shows: Cmd/Ctrl+F (while the editor has the focus, or nothing has), the magnifier in the file’s bar, or Find in file in the palette (Cmd/Ctrl+K). In a Markdown preview, a find bar (
files/FindBar.tsx) under the file’s bar: the text typed, found anywhere in the rendered document whatever its case, every match marked and the current one the strongest (the CSS Custom Highlight API: the document stays as rendered), “3 of 12” or “No matches”, Enter / Shift+Enter or the arrows for the next and the one before, scrolled to; Escape or ✕ closes it and takes the marks away, as do another file or the source; it opens over a search hit’s marks, which give way. In the source, CodeMirror’s find and replace, at the top as the find bar is, in the app’s style and words (match case, regex, whole word; Enter, Shift+Enter, Escape).
The open issues of 2026-10-12, for Markdown in Preview, Visual, the chat and Export as PDF.
- FIL-65 Issue #20, 2026-10-12. Pictures show when they don’t come from the web (
files/pictures.ts): a picture written into the document as adata:URL (PNG, JPEG, GIF, WebP, AVIF, BMP, SVG; an SVG shown as a picture runs no script), and in a document a picture in the workspace (, relative to the file or to the workspace’s root with/), read throughFileService.ReadPreviewand shown from memory (the latest 64 kept). A web picture (https://…) is never fetched (DSK-24): it stays a link that opens in the browser. An HTML block that’s nothing but one<img>is a picture too: itssrc,altand a plainwidth/heightare kept and every other attribute dropped; any other HTML is still never rendered. The same in Preview, in Visual (an<img>block shows its picture with its source behind Edit, as a diagram’s) and in the chat (data:pictures only: a reply has no file to be relative to). Export as PDF already put each picture in (FIL-54). Base64 split over lines, or with spaces, isn’t a Markdown picture: it shows as the text it is. - FIL-66 Issue #22, 2026-10-12. Zoom and width. The editor pane zooms, 50–200 % in a browser’s steps (
files/zoom.ts): ⌘ (Ctrl on Linux and Windows) with+/−/0, ⌘/Ctrl and the wheel (a notch about a step), or a pinch, over the pane. It scales the code editor and the documents in it (Preview, Visual, their code blocks and the reading column), never the chat or the rest of the window; the pane’s bar shows the zoom when it isn’t 100 % (click: 100 %). It’s kept in the preferences (editor_zoom), saved once it settles. Document width (doc_width, Settings › Appearance, and the Markdown tab’s ↔ button): Full (the default, 2026-10-12: the owner’s review, “fill the available space”) uses the whole editor, for wide monitors; Readable keeps a document to its column (FIL-57,--doc-measure, scaled with the zoom). Diagrams pan and zoom (panzoom.ts), in Preview, Visual and the chat: ⌘/Ctrl and the wheel or a pinch over a diagram zoom it at the pointer (a quarter to eight times; the pane’s zoom isn’t touched), dragging pans, a double-click fits it again; its head has − / + / Fit and Expand, which shows it over the window (fitted; the same gestures; Escape or a click beside it closes it). Export as PDF prints diagrams as drawn, never as zoomed. - FIL-67 Issue #6, 2026-10-12. Math, as GitHub writes it:
$…$inline,$$…$$and```mathblocks, drawn by KaTeX (files/math.ts;files/mathRender.tsin a chunk of its own, loaded the first time a text has math: until then it shows as its source). A single$is math only as GitHub and Pandoc read it: no space just inside either dollar and no digit after the closing one, so “costs $5 and $10” stays text;\$is a dollar. The chat takes$$…$$and```mathonly, so a reply’s prices stay prices. KaTeX runs untrusted (trust: false: no\href,\url,\includegraphics), and a formula it can’t draw shows its source in the error colour; wide formulas scroll sideways; its fonts are the app’s own, never fetched. Preview and the chat draw it (remark-math,rehype-katex); Source highlights it (the TeX as a string, the dollars as marks); Visual draws it in place (mathInline,mathBlock): a click on inline math opens its TeX in a field drawn as you type (Enter or leaving keeps it, Escape puts it back, emptying it removes it), typing$x$makes inline math, a math block shows its TeX behind Edit (highlighted as LaTeX), and Equation in the toolbar, the/menu and the right-click menu adds one. Untouched math is written back as it was (VE-04); changed math as$…$(or$$…$$if it was), a block as$$…$$or the```mathfence it was read from; a$typed as text that could open math is written\$(it reads the same). - FIL-68 Issue #21, 2026-10-12. Math in Export as PDF. The printed page is the preview’s (FIL-54), so its math is KaTeX’s drawing; the page has no base URL (the printers load it as a string), so KaTeX’s fonts, which the window’s CSS names by relative URL, are put in as
data:fonts, their WOFF2 only, when the page has math (withMathFontsinfiles/printPage.ts). Checked by printing a page of inline, display and fenced math with the app’s own printer: the PDF embeds KaTeX_Main, KaTeX_Math and KaTeX_Size and draws every formula; a font that can’t be read prints as it would have. (The page’s dev server may read KaTeX’s folder in Bazel’s output tree,vite.config.ts, so development draws math with its fonts too.)