024 · Desktop
Desktop app (spec_desktop_024)
| Status | Implemented, second version (Material 3 redesign, 2026-09-27). Since v0.7.0 the window is the Electron app (spec_electron_038); the Wails shell that showed this page until v0.6.5 is removed from the repository (restorable from the v0.6.5 tag), and its requirements here are marked as such |
| Source | Window: apps/electron/src/main.ts (spec_electron_038), pkg/pagehost (the page’s calls to its host, the preferences), pkg/pageserver/proxy.go (the proxy, in blitz-tray --serve), pkg/fileview (showing files). Page: apps/desktop/web/src/ — shell App.tsx, MenuBar.tsx, state.tsx; views Workspace.tsx, Conversation.tsx, WorkspaceSettingsForm.tsx, Changes.tsx, Workers.tsx; dialogs SettingsDrawer.tsx, WorkspaceFields.tsx; Markdown.tsx; logic turns.ts, prefs.ts, theme.ts, palette.ts, options.ts, errors.ts; api.ts, desktop.ts; design system m3.css, app.css, ui/controls.tsx; development fake dev/fake.ts; gen/ (generated). Build: apps/desktop/web/{BUILD.bazel,package.json,vite.config.ts,index.html}, the root pnpm-lock.yaml and pnpm-workspace.yaml, apps/electron/appicon.png, apps/electron/packaging/{BUILD.bazel,linux/blitz-desktop.desktop,sign_macos.sh}, .github/workflows/release.yml (desktop job) |
| Tests | pkg/pagehost/{prefs,cli,cli_install}_test.go, pkg/pageserver/proxy_test.go, pkg/fileview/fileview_test.go (Go, -race); apps/desktop/web/src/{turns,prefs}.test.ts, apps/desktop/web/src/files/printPage.test.ts, apps/desktop/web/src/files/previewKind.test.ts (vitest); manual checks in MANUAL_VERIFICATION §37 |
| Depends on | spec_service_021 (all behaviour), spec_workers_023, spec_approvals_005, spec_sessions_017 (rewind) |
Blitz.app is a window onto the per-user Blitz service, laid out like an IDE: the workspace in view chosen from a dropdown, its files, an editor and the conversation, with its changes and workers a click away. It contains no engine. Every workspace, turn, approval, session and worker lives in the service (blitzd), which keeps running — and keeps running workers — after the window closes. The app adds only what the service can’t do for itself: detecting and installing the service, native dialogs, and a bridge from the web view to the service’s Unix socket.
It keeps only its own settings (theme, layout, the workspaces it knows and how they are named) in ~/.blitz/desktop.json. The design follows Material 3, with an IDE’s layout since 2026-09-27 (files, editor, chat; earlier Google AI Studio’s: a navigation drawer, a centred conversation, a run settings panel). Not yet: see §16.
- DSK-01 Product name Blitz; macOS bundle
Blitz.app, bundle identifierdev.blitz.desktop, executableblitz-desktopon Linux; version stamped into release builds from the git tag (//build:version, spec_monorepo_028 MR-20),devotherwise; the page asks for it (Version()); copyright “Ryan McGuinness, Apache License 2.0”. 2026-10-15 (the owner’s request): where people read Blitz’s version (Settings › About, the Service pane and its stale-service note, the window’s stale-service banner, the restart message, the status bar), a release’s is shown asvN.N.N(shownVersion,serviceVersion.ts), as the tags, releases and docs write it;devand a commit stay as they are. The stamped version has nov(packages need a digit first), and it’s what the stale check compares. - DSK-02 Removed in v0.7.0 (the Wails app; see spec_electron_038). The app’s executable must never be named
blitz/Blitz: macOS file systems ignore case, and the bundled CLI (Contents/MacOS/blitz) would overwrite it. - DSK-03 Removed in v0.7.0 (the Wails app; see spec_electron_038). Minimum macOS 13.0: the build sets
-mmacosx-version-min=13.0, andLSMinimumSystemVersioninInfo.plist/Info.dev.plistsays the same. - DSK-04 The CLI and the service (
blitz,blitzd) are packaged with the app (since v0.7.0 in the app’sbin/withblitz-tray:Blitz.app/Contents/Resources/app/bin/on macOS,resources/app/bin/elsewhere; EL-03, EL-80) so the app can install the service without a separate install:blitz service installfindsblitzdbeside itself. - DSK-05 Releases package the app for macOS (a notarised disk image; universal until v0.6.5, one per Mac since) and Linux (a
.deb, and a.pkg.tar.zstfor Arch Linux): spec_release_025 REL-20–26. There’s no Windows package: on Windows the tray serves this page to a browser instead (spec_windows_036). Since v0.7.0 the desktop app released is the Electron app, on every platform, Windows included (spec_electron_038 EL-80); the Wails app is removed from the repository.
Until v0.6.5 (the Wails app). Since v0.7.0 the page runs in the Electron window and its host is blitz-tray --serve (pkg/pagehost, pkg/pageserver): spec_electron_038 §2.
┌──────────────── Blitz.app ────────────────┐ ┌────────── blitzd ───────────┐
│ Web view (React page, embedded dist/) │ │ Connect API: Session, │
│ ├─ Connect-ES clients ─► same origin ───┼─┐ │ Workspace, Worker services │
│ └─ window.go.main.App.* (Wails binding) │ │ │ workspaces, turns, workers │
│ Go (Wails v2): │ │ HTTP │ │
│ ├─ asset server: page files │ │ over │ │
│ ├─ serviceProxy: /blitz.v1.* ───────────┼─┴─Unix─► ~/.blitz/run/blitz.sock │
│ └─ App: ServiceStatus, InstallService, │ socket└─────────────────────────────┘
│ ChooseWorkspace, Get/SavePrefs, │
│ OpenURL │
└───────────────────────────────────────────┘
- DSK-10 Part of the repository’s one Go module. Until v0.6.5 the desktop app alone used cgo (WebKit on macOS, WebKitGTK on Linux); since, its Go is the host’s (
pkg/pagehostinblitz-tray), pure Go but for the tray’s Dock icon on macOS. It never reaches the engine or the service’s internals (spec_monorepo_028 MR-01): it usespkg/socket(the socket’s path,BaseURLand HTTP client),pkg/loginitem,pkg/configandpkg/legal; its tests run a real service throughapps/service/servicetest. - DSK-11 Window: title “Blitz”, 1280×820, minimum 720×480. On macOS the title bar is hidden and inset: the page draws the whole window in its theme’s colours, leaves room for the window buttons at the left of the top bar, and marks its top areas as drag regions (
.drag-region; controls in them areno-drag). Since v0.7.0 that’s the Electron window (apps/electron/src/main.ts, EL-12); until then the Wails window (mac.TitleBarHiddenInset,--wails-draggable, one GoAppvalue bound to the page aswindow.go.main.App). - DSK-100 Removed in v0.7.0 (the Wails app; see spec_electron_038). On Linux the Electron window draws with the graphics card unless
BLITZ_GPU=offor thegpupreference (Use the graphics card) is off: EL-07. What follows was the Wails app’s. 2026-10-13 (the owner’s request, after a Linux soak run held WebKit at 100% CPU idle). On Linux the window draws with the graphics card where WebKitGTK can use it (linux.Options.WebviewGpuPolicyon demand; Wails, given no Linux options, draws in software, its default since wails#2977). Use the graphics card (gpu,"on"by default, in the preferences; Settings › Appearance with the advanced settings, shown only in the Linux app) turns it off, from the next start.BLITZ_GPU=off(oron) in the environment overrides the setting, for a driver that keeps the app from starting at all. On NVIDIA (/dev/nvidiactlpresent) WebKitGTK leaves its DMA-BUF renderer off and draws in software whatever the policy says, so with the GPU on Blitz setsWEBKIT_FORCE_DMABUF_RENDERER=1andWEBKIT_DMABUF_RENDERER_FORCE_SHM=1(each unless already set; neither whenWEBKIT_DISABLE_DMABUF_RENDERERis set): the web process draws on the GPU and hands frames to the GTK3 window in shared memory. Forced on with GPU buffers, NVIDIA’s EGL attaches a Wayland explicit-sync object (wp_linux_drm_syncobj_surface_v1) to GTK3’s surface, which commits without an acquire point, and the compositor ends the connection (protocol error 71). Closing the window (once the editor’s unsaved changes are settled) ends the web processes through WebKitGTK (webkit_web_view_terminate_web_process) before GTK’s main loop and the window go (the Wails app’swebprocesspackage): left to exit on its own with the GPU on NVIDIA, the web process segfaults in its exit handlers, NVIDIA’s EGL tearing down while WebKit’s painting threads still release their GL contexts, a core dump and a crash report on about half the closes; ended by WebKit, it’s killed and runs none.bazel run //apps/desktop/gpucheck:gpu_checkchecks on a machine with a GPU that WebKit draws with it (manual verification §37). Drawing in software setsWEBKIT_DISABLE_DMABUF_RENDERER=1(unless the environment already sets it), the usual fix for a blank or garbled window on NVIDIA’s drivers and in VMs. The window’s class isblitz-desktop(GTK’s program name, the launcher’sStartupWMClass), so the dock or taskbar groups it under the launcher’s icon however the binary was started, and the window carries the app icon (256 px,windowicon.png) for a taskbar that doesn’t find the launcher’s. The chat input’s ring turns only while the agent works: turning, it repaints every frame (an animated registered property the compositor can’t take), a core’s worth in software. - DSK-101 2026-10-13 (the owner’s request). macOS’s menu bar is the app’s own (since v0.7.0 the Electron app’s,
setMenuinapps/electron/src/main.ts, EL-13; until thenoptions.App.Menu,appmenu.go): the app menu has About Blitz, Settings… (⌘,), Hide Blitz (⌘H), Hide Others (⌥⌘H), Show All and Quit Blitz (⌘Q, asking first about unsaved changes as closing does), then the Edit and Window menus. About and Settings send the pagemenu:aboutandmenu:settings, which open Settings › About and Settings › Appearance. The page sends the labels in its language (SetMenu) as it starts and when the language changes; until then they’re English. Linux and Windows have no native menu bar: the page’s is the only one. In full screen on macOS, Escape stays the page’s (closing a dialog or popover); ⌃⌘F, F11 or the window’s button leave it. - DSK-102 2026-10-14 (the owner’s request: the window had 18 of the terminal’s 49 commands). The window’s slash commands match the terminal’s (
slashCommands.ts, one registry the composer’s menu, the command palette and/helpshare). Every command the REPL’s/helplists is a built-in here, in the same seven groups (Session, Agents and models, Planning and long work, Workspace and files, Permissions and safety, Extensions, Settings), each sorted by name, answering with a notice, a turn or the window’s own UI through the service’s API: among them/status,/copy,/export(a workspace file),/fork,/rewind,/style,/pin_model,/unpin,/model_settings,/goal(the terminal’s prompt),/loop(timers in the window while it’s open; Workers outlive it),/tasks,/attach,/init,/memory,/trust(the project review when settings wait),/approvals,/hooks,/permissions,/sandbox,/envs,/mcp,/skills(andupdate),/tools,/config(or/set; no arguments opens Settings › Settings file,--savewrites the user’s file),/locale(the reply language) and/theme./clear,/exitand/quit,/paste,/cdand!<command>stay the terminal’s;/helpnames them with the window’s way instead./helplists the custom commands by source after the built-ins.slashParity.test.tsreads the REPL’shelpGroupsand fails when a terminal command is neither here nor terminal-only. - DSK-103 2026-10-14 (the owner’s request: the docs can change over time). The app carries the user docs of its own release: the home page, Getting started, Tutorials, Guide, Products and Releases of
docs/content, and their picture (//docs:user_docs, bundled into the page as its own chunk bysrc/docs/content.ts; the specifications, development and architecture pages are left to the website).src/docs/index.tsreads them as the site does (front mattertitleandweightfor the order; relative.mdlinks,_index.mdthe section) and resolves a page’s links: to another page the app carries, a heading, or the website for anything else; its search ranks title, then headings, then text. The viewer (DocsDrawer.tsx, Settings’ layout) has the navigation, search, a breadcrumb, back and forward (⌘[ ⌘]) and Open on the web, and renders with the page’s Markdown (Mermaid, tables, math) under a docs link context. It opens where it was left, page and scroll (apps/desktop/web/src/docs/place.ts, the window’s storage), and on Tutorials the first time; Help’s section links, the palette and/docs topicopen their page. It fits narrow windows (container queries on the viewer): under 700 px the navigation folds behind Contents; on a page under 680 px a table’s rows stack as cards (the first cell as the heading, the others labelled with their column), and a table of four or more columns stacks under 900 px; wider, every cell is at least 8em (a crowded table scrolls) and code in a cell stays on one line. Help › User guide (where it was left), a Help link per section (Getting started, Tutorials, Products, Guide, Releases;apps/desktop/web/src/docs/sections.ts, checked against the docs),/docs [topic](the best match) and a Docs group in the ⌘K palette open it (openDocs). The website stays published.docs.test.tschecks every carried page and link;docsCommands.test.tsthat every commandproducts/cli.mddocuments exists in the terminal or among the bundled commands. 2026-10-15: the release shortcode ({{< release >}}, in install snippets) is the app’s own release there (withRelease,src/docs/index.ts), orvX.Y.Zin a build that isn’t a release. - DSK-12 The page is built by Bazel (
//apps/desktop/web:page, Vite) for the host platform: since v0.7.0 carried in the Electron app’spage/(EL-03) and embedded in the Windows tray (apps/tray/page.go); until then embedded in the Wails binary. Nothing is written into the source tree.
- DSK-20 A web view can’t open a Unix socket, so the handler serving the page (since v0.7.0
blitz-tray --serve, SVC-53a; until then the Wails asset server’s) forwards every request whose path starts with/blitz.v1.to the service socket ($BLITZ_SOCKETor~/.blitz/run/blitz.sock, resolved at start), rewriting the URL to the service’sBaseURL. The page’s Connect client useswindow.location.originas its base URL, so it talks to its own origin and needs no CORS. - DSK-21 Responses stream as they arrive (
FlushInterval: -1): turn events and worker-run events must appear as they happen. WebKitGTK (Linux, the Wails app) streamed them, but sometimes held back the last data written before a pause until more arrives (found 2026-09-28: an approval request, after which the service waits, stayed unseen and the turn showed “Working…”). So a Connect stream (application/connect+jsonor+proto) goes throughkeepalive: the service’s messages pass whole, and after 250 ms of quiet an empty message ({}, or no bytes in protobuf) follows, never inside a message or after the end-of-stream one; the page skips messages without an event. - DSK-22 If the service can’t be reached, the proxy answers
503with Connect’s JSON error shape{"code":"unavailable","message":"the Blitz service isn't answering"}, which the page’s client reports like any API error. - DSK-23 Any other path the handler receives is
404, apart from the page’s own files and the host’s/host/calls (since v0.7.0 both served byblitz-tray --serve; until then Wails served the page from the embedded assets). - DSK-24 Security: the socket is owner-only (spec_service_021 SVC-02); the proxy listens only on a loopback address behind a one-time link and a cookie (WIN-11–13; the Wails app’s opened no listener), so nothing new is reachable from the network. Only the embedded page runs in the web view. Model and tool output becomes React elements (Markdown through
react-markdown, raw HTML skipped; everything else escaped), never HTML; its links open only in the system browser throughOpenURL(web and mail schemes only), and its images are never fetched from the web (a picture written into it as adata:URL, or in a document one in the workspace, is shown: spec_files_029 FIL-65), so output can’t navigate the web view, inject script or make the app load anything.
Since v0.7.0 the Electron window answers ChooseWorkspace, Notify, OpenURL, PrintPDF and the window’s own calls (EL-20), and the host answers the rest through /host/ (pkg/pagehost, EL-21); until then they were Go methods bound to the Wails window.
| Method | Behaviour |
|---|---|
ServiceStatus() → {running, installed, socket, service} |
running: the socket answers (1 s dial). installed: the login item file exists (~/Library/LaunchAgents/dev.blitz.service.plist on macOS, ~/.config/systemd/user/blitz.service on Linux; always false elsewhere). service: the blitzd this app manages — beside its executable, in its Bazel runfiles, or on PATH — else "". |
InstallService() |
Installs service as the login item and starts it now (pkg/loginitem), replacing a previous item. Fails if no blitzd is found. |
StopService(pid), RestartService(pid) |
Stop the running service (its login item, else process pid), and for restart install and start this app’s blitzd (DSK-51a). |
SetTray(on) |
Runs blitz-tray --install or --uninstall (the one beside the app, in its runfiles, or on PATH); ServiceStatus also says which blitz-tray it found and whether it starts at login (SVC-54). |
OpenDocument(path) |
Opens an image or PDF (by extension, a regular file) in the system’s viewer; anything else is refused (FIL-54). |
OpenFolder(dir) |
Shows an existing directory in the file manager (open, xdg-open); anything else is refused (the logs’ folder, DSK-60). |
RevealPath(path) |
Shows an existing file or folder selected in the file manager (open -R, explorer /select,, D-Bus FileManager1.ShowItems; else its folder); nothing is run (the tree’s Show in …, FIL-58). |
FileManager() |
The file manager’s name for Show in …: Finder, File Explorer, or on Linux the one that opens folders (Dolphin, Files, …), "" when unknown. |
Version(), ProgramExists(path) |
The app’s version (DSK-01); whether a file exists (the running service’s program, DSK-51a). |
ChooseWorkspace(title) → dir |
Native directory dialog with the page’s title (may create directories); "" when cancelled. |
GetPrefs() → Prefs |
The window’s settings (§7), defaults when none are saved. An unreadable file is renamed desktop.json.damaged and reported once; the defaults are returned. |
SavePrefs(Prefs) → Prefs |
Normalizes (§7) and writes them atomically, owner-only (0600, directory 0700); returns what was saved. |
Notify(title, body, dir) |
A system notification (Electron’s Notification; until v0.6.5 Wails’s; macOS’s needs the bundle identifier). The first call asks the user’s permission; without it, or where notifications aren’t available, it does nothing. A click on the notification unminimises and shows the window and emits notification:open with dir, which shows that workspace. |
License(which) |
notice, full or third-party: the NOTICE, the Apache License or the third-party notices, embedded (pkg/legal). |
OpenURL(url) |
Opens http/https links (with a host) and mailto: links in the system browser; anything else (file:, javascript:, custom schemes, relative) is refused. Never navigates the window. |
- DSK-30 The page calls the Electron window for what it answers (
window.blitzElectron, EL-40), else asks the program serving it which calls it answers (/host/info:blitz-tray --serve, or the tray on Windows, spec_windows_036 WIN-14–15) and calls those there. For the rest (and in a browser in development) it uses stand-ins: preferences inlocalStorage, the status from a call to the service through the dev server’s proxy, a prompt for the directory, links in a new tab. Calling one that nothing answers throws “not running inside the Blitz desktop app”.
- DSK-35 Material 3 tokens (
m3.css): the colour roles (primary, secondary, tertiary, error, warning containers, and surface / surface-container-lowest…highest, outline, outline-variant, inverse, scrim, diff colours) in a light and a dark scheme derived from Google blue#0B57D0, with AI Studio-like neutral surfaces (dark surface#131314); shape (4–28 px, full), elevation 1–3, type scale (display, headline, title, label, body) in Google Sans/Roboto with system fallbacks and a monospace stack. Components use only tokens: buttons (filled, tonal, outlined, text, danger), icon buttons, chips, segmented buttons, outlined fields and selects, switches, sliders, lists, cards, a menu bar with a search bar, menus (an item may carry a button at its end,MenuItem.action, shown on hover or focus; right-click menus are dense: 28 px rows, 16 px icons, key hints on the right), dialogs, a snackbar, progress indicators and badges, all with M3 state layers. Since 2026-10-04 (the owner’s request) the controls share the chat input’s ring’s colours, an accent:--accent-ato--accent-b,--accent-grad(light: a brighter blue than the primary,#1a73e8, to teal#00838f; dark: the primary#a8c7fa45% toward the tertiary, teal rather than green). Filled buttons and filled icon buttons are that gradient at 135° with a soft accent shadow, a hairline highlight on top and, on hover, a deeper shadow and a 1 px lift (none with reduced motion); disabled, they go flat (surface-container-highest, no shadow). Danger stays solid red. Tonal buttons and everything selected (icon buttons such as Files’ hidden-files eye, chips, segmented buttons, list items, menu items, the command palette’s, search hits, the file tree’s row, a table’s focused row) are a tint of the same gradient (--accent-tint: light, 18% of each end over white; dark, each end 30% over a deep blue and a deep teal), so they read as a lighter version of the filled colour; segmented buttons and outlined buttons have accent-tinted borders, and the editor’s open tab the gradient along its top. Every field-like box (inputs, text areas, selects, the find bar, the settings and CSS editors) has one radius,--field-radius(8 px), and theoutlineborder; fields have their own surface (nearly opaque under glass) and, focused, the accent’s border with a 3 px halo (red when invalid); focus rings, checked switches, sliders and progress bars are the accent. Button text keeps 4.5:1 or more on both ends of the gradient (light: white, 4.5:1; dark:on-primary, 7.3:1). Icons are Material Design icons as inline SVG paths (@mdi/js, tree-shaken), not a font. - DSK-36 Theme: the user chooses System (the default), Light or Dark; System follows the OS setting live (
prefers-color-scheme). The choice is applied ashtml[data-theme]; density (Comfortable or Compact) ashtml[data-density]. Motion is reduced when the OS asks for it. - DSK-37 Narrower windows: the view switcher shows icons only under 1100 px. Since 2026-10-04 (the owner’s request) no panel ever floats over another: the Files shelf keeps its column at every width (it floated over the editor under 1100 px, hiding the chat’s tiles at center stage), and in a narrow window the panels give way to their minimums — the shelf down to 180 px, the editor to 160 px, the chat to 320 px — the shelf minimized to the rail when you want the room. Opening a file no longer minimizes it. A Changes pane under 760 px wide lists its files and turns above the diff (a container query). The composer’s key hint hides when the composer is under 560 px wide, and a long file path in the editor gives way from its folders, keeping the file’s name. The snackbar sits bottom left, above the status bar, clear of the composer. 2026-10-14 (#37): it sits at the top center, just below the top bar (following it down when a banner shows), clear of the composer. 2026-10-15 (the owner’s request): it sits below the panes’ headers too (the editor’s tabs and toolbar, the Files and side panels’ heads, wherever they’re under its 640 px), which it covered just under the top bar. Since v0.6.3 it has rounded corners (16 px) and, with glass on, is a pane of glass like the others: nearly opaque (
--glass-strong), the page’s ink and accent, not Material’s inverse slab. - DSK-38 Glass (
glass.css): with Settings › Appearance › Transparency on (the default;transparencyin the preferences) and the system not asking for reduced transparency,html[data-glass="on"]makes the surface roles translucent, the top bar, panels, editor and status bar blur what’s behind them (backdrop-filter), and dialogs, menus, the composer and the snackbar get a light edge and a soft shadow. Since 2026-10-02 light glass is frosted rather than clear, so it stays usable: surfaces 78–93% opaque (they were 50–80%), borders and outlines a grey that shows on white (--glass-borderrgb(110 122 145 / .28),--md-outline-variantat 45%), a stronger shadow and scrim, and a backdrop a step deeper (#e3e8f1, with more saturated glows) so the panes lift off it; text and muted text keep 13:1 and 7.7:1 or more on every surface, the ring’s colours 5:1 or more. Dark glass is unchanged. The page’s backdrop is a soft gradient; in the macOS app the window itself is translucent (Electron’svibrancy, EL-12;html[data-native-glass]), so the desktop shows through. The workspace’s areas (Files, the editor or center, the chat) float as rounded glass panes 8 px apart, and the top bar sits on the backdrop itself. A dialog’s scrim blurs the window behind it. Since 2026-10-04, the macOS window’s blur takes the page’s theme, not the system’s: a light page in a dark system had its top bar, gaps and buttons on a dark blur, all but unreadable.applyThemecallsSetAppearancewith the user’s choice (NSApp.appearance: Aqua for Light, Dark Aqua for Dark, and for System none, so the window follows the system again — it must, since the page’sprefers-color-schemereports the window’s appearance, and a window held light would hide the system’s switch to dark; on the main thread; nothing on other systems), and underdata-native-glassthe body is the backdrop’s glows, translucent, over a base in the theme (--backdrop-native; lightrgb(227 232 241 / .68), darkrgb(11 14 21 / .55)) rather than clear: the gradient reaches the top bar and the gaps between the panes, as in VS Code, and the ink stays on its own theme even where the blur doesn’t follow. Light glass also has the menus inon-surface, muted text#3c4043, the text and outlined buttons’ blue#0842a0, and disabled buttons at 50% (38% elsewhere). What floats over other text (dialogs, menus) is nearly opaque, so it stays readable where the blur isn’t drawn. Off: the solid Material surfaces. Since v0.6.3 (the owner’s request) warnings are glass too: the banner over the window (the service lost or out of date) and warning cards (.card.warn: a model unavailable, a setting’s problem) are nearly opaque panes (--glass-strong) with the warning colour on their left edge and icon, not a solid yellow block; the window’s banner floats with the panes’ gap and corners. Since v0.7.0 the drawer (DSK-65c) is one more pane: the same border, corners, blur and shadow, 8 px from the window’s edges, but opaque (--glass-solid, the nearly opaque glass’s colour in full): it covers the whole middle, and the panes faintly showing through only got in the way. - DSK-38a Since v0.7.0 (the owner’s request: “not everyone will like what I like”). Colour themes, chosen as VS Code’s preferred light and dark themes are: Settings › Appearance › Light theme (Blitz, One Light) and Dark theme (Blitz, One Dark Pro), beside Theme (System, Light, Dark), which picks between them;
light_themeanddark_themein the preferences (blitzby default; anything else, or a dark theme for light, is read asblitz). The page setshtml[data-palette]to the one in effect (paletteOf,themes.ts);themes.cssgives each other theme the M3 roles’ colours, the highlighting’s (--hl-*, so the editor, code blocks and diffs follow) and the accent’s, taken from the themes themselves (Atom’s One Light UI and syntax; Binaryify’s One Dark Pro; MIT, credited in the third-party notices fromthird_party/themes). They’re solid: glass (DSK-38) is Blitz’s own theme’s, so Transparency is off, and says why, while another is in effect. - DSK-37a Since v0.7.0 (the owner’s request: “when a large amount of output is shown, the narrow view is not readable”). With files open, the chat’s header and the editor’s tab bar each have Expand (
Workspace.tsxwide): the one expanded takes the middle of the window and the other is hidden, kept as it was (the editor’s tabs, views and web pages; the chat’s conversation, the composer still at the window’s foot). The hidden one leaves a rail on its side, as the Files shelf’s (PaneRail): its icon and name, the number of files open (the editor’s) or a dot while the agent waits for you (the chat’s), so it reads as shrunk, not closed; clicking it, or Shrink, puts the two side by side again, and opening a file or a web page shows the editor. - DSK-39 Status bar (
StatusBar.tsx): a 26 px strip under the whole window, glass with Transparency on, shown on every view including the welcome screen. Left: the service (connected, or reconnecting; its version on hover), the workspace in view (its colour and name), and its git branch (the short commit when detached) with the files that differ from HEAD, untracked included, and since v0.6.3 the commits to pull (↓n) and push (↑n) as of the last fetch, with a merge icon while a pull’s merge or rebase waits (WorkspaceService.GetGitStatus, asked on showing, 0.5 s after a tool touches files, on focus and every 10 s while visible; git runs read-only with the repository’s filters and fsmonitor off; nothing outside a repository) — clicking shows Changes. Right: a turn running, or waiting for you (clicking it brings the approval or question into view and focuses it; one also comes into view as it arrives), the background runs going or waiting (opening the inbox), the workspace’s failed worker runs (showing Workers), the editor’s line, column, selection and language while a file is shown, the agent and model (a menu of the agents, as/agentlists them: each with its description and pinned model, the one working on the conversation marked; choosing one isSetAgent, as/agent <name>, so it takes over mid-conversation from the next turn, the history kept; at its foot Workspace settings…, Settings › Workspaces), the permission mode (a menu of the modes, with their meaning;bypassin red; a refusal because the sandbox is off is explained), the reasoning effort (a menu: Auto, Minimal … Max), the context in tokens with a meter against the compaction threshold (red past 85%), and the session’s cost with the last turn’s usage on hover. The parts of the page that know each fact publish it (status.ts: the workspace its settings, the conversation its usage, the editor its cursor). The line of usage under the composer is gone. Below 900 px the labels drop, keeping icons and numbers; below 640 px the cursor and workspace too.
- DSK-40
~/.blitz/desktop.json(Prefs):theme(system|light|dark),density(comfortable|compact),width(full, the default: the conversation fills its panel;readable: a column of--content-width, 860 px),notifications(on|off),language(systemor a language tag),files(the Files shelf expanded, else a rail),show_hidden,chat_width(0: a third of the window),files_width(0: 272 px),show_thoughts,active(a directory), andworkspaces: every workspace the window has opened, each{dir, name, description, color, open}. Normalization: unknown values fall back to defaults; directories are cleaned, must be absolute, and appear once; open workspaces come first in tab order, then closed ones (at most 20, most recent first);activemust be an open workspace, else the first open one. - DSK-41 The rules for the list are pure functions (
prefs.ts), unit-tested: opening a known workspace keeps its name, description and colour and shows it (opening an open one just shows it); closing moves it to the front of the recent ones and shows the tab after it (else before); a closed workspace can be forgotten (open ones can’t); open ones can be reordered. A workspace is shown by its name, else its directory’s last element. - DSK-43 The host (
pkg/pagehostPrefsStore) always sendsworkspacesas a list ([], nevernull), and the page normalizes whatever it loads (normalizePrefs, unit-tested: every field of the right type, else its default; workspaces without a directory dropped). An error while drawing the page shows “Something went wrong” with the details, Reload the window and Copy the details (an error boundary), never a frozen or blank window. - DSK-42 Saving happens on every change, one save after another so the file ends with the latest.
- DSK-50 On start the page loads the preferences and asks
ServiceStatus, showing a splash meanwhile. If the service isn’t running it explains that the service holds workspaces and runs scheduled workers and keeps running after the window closes, and offers Install and start the service (or Start the service when the login item exists); it then polls the status every 2.5 s until the service answers. Without a CLI it says to install Blitz’s CLI and runblitz service install. - DSK-51 When any call fails with
unavailable(the proxy’s answer when the socket doesn’t answer, or the service shutting down), a banner “The Blitz service isn’t answering. Reconnecting…” appears and the status is polled every 2.5 s; when the service answers again every open workspace reloads (sessions, settings) and a snackbar says “Reconnected”. The open workspaces and the one shown are kept throughout. - DSK-51a The window talks to whatever service holds the socket, which may be an older install or one whose program has since been removed or replaced. Once the service answers (and after each reconnection) the page asks it
GetServiceInfo(spec_service_021 SVC-06) and checks, withProgramExists, that its program is still there. The service is stale when it’s too old to answer (“older than this app”), when its program is gone or was replaced since it started (replaced: a new build or package installed over it, which also tells development builds apart, alldev), or when its version differs from the app’s (Version()). A stale service gets a banner with Restart the service (RestartService: stop it, then install this app’sblitzdas the login item and start it; it waits up to 30 s for the expected version). Settings › Service shows the service’s version (since v0.7.1 only the service’s; the app’s is in About, and both are named only in the stale warning), the service program, and Restart and Stop while it runs.StopService(pid)stops the login item’s service (launchdbootout, systemdstop), else — or if it still answers — the process GetServiceInfo reported (SIGTERM, 15 s). The app manages the service itself throughpkg/loginitem, with no CLI: it findsblitzdbeside its executable (Blitz.app, the .deb), in its Bazel runfiles (bazel run), or onPATH; without one the buttons are off and the window says to reinstall Blitz. Rules inserviceVersion.ts, unit-tested; stopping a realblitzdby its process is tested. - DSK-51b 2026-10-18 (a workflow run lost to a restart). Stopping the service stops the workflow runs going in it, so nothing stops it unasked: Restart (the stale-service banner, Settings), Stop and Reinstall first ask when runs are going (
useStopGuard: how many, and which), and do nothing unless told to stop them. Start on the service-down screen looks again first and leaves a service that answers alone. Installing the same login item again (loginitem.Install: the same plist and loaded, or the same unit) only starts the service if it isn’t running (launchctl kickstart,systemctl --user start), never stops it; only another program in its place replaces it. - DSK-52 Replaced 2026-10-02 by the Workspaces menu (DSK-65a). The workspace dropdown at the left of the top bar (2026-09-27 to 2026-10-02): the shown workspace’s avatar and name, its open and recent workspaces, Edit details, Close, Forget and Open workspace….
- DSK-53 With no open workspace: a welcome page with Open a workspace and up to six recent workspaces as cards, under a top bar with the app’s name, the workspace dropdown and Settings.
- DSK-54 Open workspaces stay mounted while another is shown (hidden, not destroyed), so a running turn keeps streaming and its approval waits.
- DSK-55 Closing a workspace: if a turn from this window runs in it, a dialog asks “Stop the turn and close?”; stopping aborts the turn and waits for it to end. The tab then closes (it moves to Recent), the service is asked to
CloseWorkspace(aTURN_RUNNINGrefusal — another client’s turn — is ignored: the tab still closes and the service keeps the workspace), and a snackbar offers Undo (reopen). - DSK-56 Replaced 2026-10-02. The workspace details dialog (name, description, colour, folder; Save/Cancel) is gone: those fields (
WorkspaceFields.tsx) are the Workspace card of a workspace’s settings (DSK-85a), in its panel and in Settings › Workspaces, saved as they’re changed.
-
DSK-36a Full screen: F11, ⌃⌘F on macOS, or Full screen in the command palette, toggles the window (the Electron window’s
ToggleFullscreen; in a browser, the page). The window has no maximum size (the Wails app setMaxWidth/MaxHeight16384, since Wails capped a Linux window at one monitor’s size). -
DSK-60 Settings (⌘, or Ctrl+, opens it) in the drawer (DSK-65c; since v0.7.0, a dialog over most of the window before): ✕, or Escape with the focus in it unless something inside used the key (such as the editor’s completion list), closes it, and settings apply as they’re changed, so there’s no Done. The sections are a list at its left, and a rail of their icons (their names in tooltips) when the drawer is under 600 px; a setting’s control goes under its description when the pane is under 520 px. Since v0.7.1 (the owner’s request) the sections are, in order: Providers & keys, Appearance, Agents, Skills, Workspaces, Permissions, Service, Logs, About, and Settings opens on Providers & keys (the gear, ⌘,). Its title bar holds Show advanced (a labelled switch, DSK-60a; its words hidden under 600 px) and, with advanced settings, Show the settings file (
</>): Settings turns over, a quarter turn out and back in (at once with reduced motion), to the settings file (DSK-61) across its whole width, as a page’s source, and</>turns it back; what’s typed in the file is kept while turning back and forth, until Settings closes. The file is no longer a section, nor is Documents. Segmented choices in Appearance and Documents are all as wide as the widest (Segmentedequal), and Appearance’s menus (the light and dark themes, the language) share a width. In sections: Appearance — theme (segmented System/Light/Dark with icons), Interface language (System, or one of the shipped catalogs by its own name), density, Conversation width (Full or Readable), Show thinking, Continue after background tasks, and Documents (Document styles…, a dialog: CSS for Markdown previews, per theme, spec_files_029 FIL-57); Providers & keys and the settings file (DSK-61); Permissions — the global permission rules and the built-in read-only rules (the form DSK-85 describes); Workspaces — every workspace the window knows with its avatar, name and directory, Forget for closed ones, and a pencil (Settings for …) that shows its settings in place, with a back arrow: for an open workspace the workspace settings form (WorkspaceSettingsForm, the panel’s, DSK-85a: every card, changes told to the window withconfigChanged, and Edit the settings file turning Settings over to its file); for a closed one how it’s shown (name, description, colour, folder) and Open (since 2026-10-02; project settings are reviewed from the form’s Project settings card, the bar above the conversation, or when a workspace opens with settings waiting for trust, spec_project_config_031 PRJ-33); Service — running or not, whether it starts at login (Install/Reinstall), the socket and the CLI path, Show in the system tray (a switch:SetTray, which runs theblitz-traybeside the app with--installor--uninstall, SVC-54; off, with a note, when there’s none); OS sandbox (Linux only: whether bubblewrap runs,SandboxStatus; Allow bubblewrap when AppArmor’s restriction stops it,FixSandbox, spec_shell_007 SH-20b, with the system’s password dialog, then the service restarted (RestartService, waiting for it) so what it opens next is sandboxed; the commands to run by hand when that fails). A workspace that didn’t open for want of the sandbox (its settings fail withSANDBOX_UNAVAILABLE) shows the same above its panes: why, Allow bubblewrap when AppArmor is the cause (the fix, then the restart, then the workspace reloaded), else how to install bubblewrap or setauto(SandboxNotice.tsx, sharinguseSandboxFixwith the row); Command line (theblitzbeside the app and whether a new terminal finds it:CLIStatusasks the login shell,$SHELL -i -l -c, for its PATH, since the app’s own isn’t the terminal’s; when noblitzis on it, Install (InstallCLI) links the app’s into the first of~/.local/bin,~/binor a writable/usr/local/binon that PATH, else into~/.local/bin, adding it to PATH in the shell’s startup file:.zshrc(under$ZDOTDIRif set),.bash_profileon macOS and.bashrcon Linux, fish’sconf.d/blitz.fish, else.profile; it replaces a stale link there but never a file; when ablitzis on PATH, this app’s or another’s, it says where, and since 2026-10-04 (the owner’s request) Reinstall (ReinstallCLI), for developers’ machines with links to old builds: it removes the deadblitzlinks in PATH’s folders, points theblitzlink a terminal finds at this app’s (a new link renamed over the old, so there’s always one), and with none left links it as Install does; ablitzthat’s a program, not a link, is left alone with a message; the note lists the dead links it removed; since v0.7.0, on Windows (the tray’s page and the Electron app), the app’s folder goes on the user’s PATH instead, worded so, spec_windows_036 WIN-14); Logs — the service’s log (LogViewer.tsx, SVC-06a): a day (Newest or one of the days there are), a level (All, Info, Warnings, Errors: that level and above), a search (every word, any field, as typing pauses), Refresh, Delete for a past day (canDeleteinlogs.ts: today’s is being written; a dialog confirms, thenDeleteLogDay, SVC-06a, and the view goes back to the newest day), and in the app Open the folder (OpenFolder: only an existing directory, in the file manager); records newest first, each its time to the millisecond, level (errors tinted), message and fields, selectable; the file’s path above, and “the newest n of m” when the limit cuts them; “the log is off” whenlog.levelis off; after installing, restarting or stopping it waits for the service to be running (or stopped), up to 15 s (“Waiting for the service…”, then an error if it doesn’t settle;waitForService), the version check gives up after 5 s, and the section refreshes every 5 s while open; About — the app’s version (Version(), the release’s tag), where its settings live, and Licenses / Third-party notices, which open the Licenses dialog (Notice, Apache License 2.0, Third-party notices;/license [full|third-party]in the composer opens it too). The agent’s settings are per workspace, in Workspaces (§13). -
DSK-60a 2026-10-02 (the owner’s request). Simple and advanced. The window starts simple (
advancedin the preferences,falseby default, so existing windows start simple too): it shows what a newcomer needs, and Show advanced (a switch in Settings’ title bar; until v0.7.1 the first row of Appearance) shows the rest. Hidden settings keep working with their defaults or what they’re set to: hiding never resets anything. What’s advanced (the larger choices insimpleMode.ts, unit-tested; the smaller where they’re drawn, withuseAdvanced):- Settings sections: Documents, Permissions, Agents, Skills (spec_skills_013 SK-96, 2026-10-14), Settings file, Service and Logs (simple keeps Appearance, Providers & keys, Workspaces and About; a hidden section asked for opens Appearance). Since v0.7.1 the sections are in four groups, each under a heading in the side list (
sectionGroups,shownGroups; a thin rule when the list is icons only; no headings in simple mode): Getting started (Providers & keys, Appearance), Global setup (Agents, Skills, Workers, MCP servers, Secrets: Studio’s order), Workspaces and access (Workspaces, Permissions) and System (Service, Logs, About). - Appearance: Chat input, Density, Transparency, Conversation width, Show thinking and Continue after background tasks (simple keeps the switch, Theme, Light theme, Dark theme, Interface language and Notifications).
- Providers & keys: the sign-in choice (Google Cloud, an Anthropic account) with its fields, and Base URL, unless the provider already signs in some other way than an API key (
showsSignIn). - A workspace’s settings: Behaviour’s Agency and Output style, and every card under Advanced settings (Thinking, Generation, API keys, Permission rules, Standing approvals, Project settings, Settings file, Context); simple keeps Workspace, Agent and model, and Behaviour’s Permission mode and Replies in.
- Menu bar: the Studio menu (the Agents and Workers menus until v0.7.1, spec_studio_040 STU-01), and Changes’ Version control source (simple’s Changes is a button for this session’s).
- Conversation: a prompt’s Rewind menu (Edit stays), Copy as Markdown and Copy the whole answer, Always allow on approvals, and a tool’s arguments and result.
- Files and editor: the shelf’s Refresh, Collapse all and Show hidden files, the context menu’s Git actions and Copy relative path, and the editor’s Wrap long lines.
- Changes: the This session / Git switch and Refresh.
- Command palette: Show the workers.
The status bar is the same in both.
- Settings sections: Documents, Permissions, Agents, Skills (spec_skills_013 SK-96, 2026-10-14), Settings file, Service and Logs (simple keeps Appearance, Providers & keys, Workspaces and About; a hidden section asked for opens Appearance). Since v0.7.1 the sections are in four groups, each under a heading in the side list (
-
DSK-61 Settings and keys, through
ConfigService(spec_service_021 SVC-35; the page never sees a key). Providers & keys (global) and a workspace’s API keys (its own, DSK-85) share one form (ProviderSettings.tsx): where keys are kept; the provider (global: the one set, Gemini when none is, as the service does; workspace: “Use the global setting” or one), default model and how the provider signs in are one change, on a card laid out a row at a time (provider and model; the sign-in; the sign-in’s fields; one column in a workspace’s settings) — Sign in with (segmented; a menu in a workspace’s settings) for Gemini (API key, Google Cloud ADC, or since 2026-10-04 Google account (OAuth): project and location fields for both) and Anthropic (API key, Anthropic account OAuth: anantprofile field, or Google Cloud ADC: Claude on Vertex AI, project and location fields), a note saying the sign-in happens on the service’s machine (gcloud auth application-default login,ant auth login) and, since 2026-10-04, above it the sign-in itself (SignInPanel.tsx; which one,signin.ts:adcand Claude on Vertex Google,oauthAnthropic, Bedrock AWS, Azure Entra ID, also shown when one of the cloud providers is configured): Signed in to … with its detail, or Not signed in, fromGetSignIn; Sign in runsSignIn, streaming the tool’s progress (“Finish signing in in your browser”, Open the sign-in page for its URL, a device code in large type) with Cancel, then refreshes the model check; Sign out; when the tool isn’t installed, which one, and Get … (its install page). Signing in adds to the API key, which stays a choice, else its API key (a password field, “Leave empty to keep the current key” when there is one): nothing is saved until Save or Enter (SetProvider), Revert puts back what’s saved. The chosen provider’s form holds everything about it — there are no separate sections per provider, and no list of the other providers’ keys: with an API key, the key’s source as a chip — Keychain, Plain text and Obfuscated (in the error colour, with Move to keychain), Environment, Global (a workspace inheriting), Not set, Missing (a reference to a secret that isn’t there) — with a sentence saying what it means, and Remove for a key in this scope’s file; for Anthropic and OpenAI, the base URL (not in a workspace’s settings). The cloud providers (bedrock, azure, vertex-anthropic) are set in the settings file. Settings file (settingsfile/): the global file or a known workspace’s, in the Files view’s CodeMirror editor with TOML highlighting (⌘S saves, Revert), and its path. Completion comes from the settings reference, not the file’s words (settingsCompletions): in a header, the tables ([) or arrays of tables ([[); at a line’s start, the current table’s settings not yet set, each with its type, default and description, inserted askey =and going on to the values; after=,trueandfalsefor a boolean, else the default. A panel beside the editor describes the setting or table at the cursor (key, type, default, description; below it on narrow windows). Validate checks the text without saving (CheckConfigFile) and lists the problems below the editor, errors in the error colour, each with its line (a click goes there), and marks their lines in the editor, tinted, with a ✕ (error) or ! (warning) in a gutter beside the line numbers (the messages on hover); the first error’s line is shown. Save validates first and saves only without errors; warnings (unknown settings, plain-text keys, skill policy) don’t stop it. After an edit the list says it may be out of date. Hovering a key or a table’s name shows the reference’s entry (GetSettingsReference: key, type, default, description;findSettingmatches quoted names to<model>-style keys and[[array]]tables toname[]), and Every setting (n) lists the whole reference, searchable by key and description (searchSettings). After any change open workspaces ask again whether their model works (configChanged→GetModel), and a model still unavailable is said in the form, until the next change; the conversation’s “model isn’t available” note has Check again, and while it shows, the window coming back into focus asks again (a sign-in made in a terminal); the conversation’s “model isn’t available” note says where to set a key.
-
DSK-65 Laid out like an IDE (the owner’s request, 2026-09-27). The top bar is the window’s title bar (a drag region, inset for macOS’s window buttons): the app’s mark and name (⚡ Blitz) over the Files shelf, then the menu bar (DSK-65a) from the center panel’s left edge. Below it, left to right: the Files shelf (minimized to a rail with Show files and Go to file, which opens the command palette), the middle (the editor; since 2026-10-02 the changes, an agent or a worker open over it in a dialog, DSK-65b), the chat (always shown; its left edge drags its width, saved). With the editor chosen and no file open, the chat takes the middle (center stage, the owner’s request); opening a file moves it back to the side, and closing the last one brings it back — the same conversation, so a running turn carries on. Settings (agent, model, mode, effort) are loaded on open and after any change, turn or reconnection.
-
DSK-65a 2026-10-02 (the owner’s request). The top bar is a menu bar (
MenuBar.tsx), replacing the workspace dropdown, the view switch and the icon buttons. The brand sits over the Files shelf (.topbar-lead, after macOS’s window buttons, at least 120 px: the mark alone when narrower than 180 px), and the menus start at the center panel’s left edge (the editor’s, or the chat’s at center stage), measured as the shelf opens, closes, floats or is resized (Workspace.tsx, aResizeObserver). From the left: Workspaces (below), Changes › Session (this session’s changes) or Version control (the Changes dialog on that source); Agents › Add agent, then the workspace’s agents (a click edits one in the agent dialog; a remove button, or Delete, asks to delete it); Workers › Add worker, Background runs (the inbox dialog, with how many go on or wait), then the workspace’s workers, each with its state and schedule (a click opens its dialog) and buttons on its row: Run and Disable for an enabled one, else Review and enable (shows it, its permissions and Enable: never enabled unseen), Edit (its dialog) and Delete (asks, thenDeleteWorker) (workerActions,MenuItem.actions), its button badged with the runs going on (red while one waits) and a red dot for failed worker runs; Workspaces (first, since 2026-10-02) › Open workspace…, the open workspaces (each folder icon in its workspace’s colour,MenuItem.iconColor; the shown one marked, with “Waiting for you”, “Working” or failed runs; a click shows one; on the row, Settings for … (a pencil: Settings › Workspaces on it, DSK-85b) and Close), then up to eight recent ones (a click reopens, Forget), a red dot when another workspace waits; Help › User guide (the published guide, in the browser), Set up the agent harness, About Blitz. At the right: the search bar (DSK-78e) and Settings, an icon button for the global settings (⌘,; the dialog on Appearance). A workspace’s own settings are its workspace settings panel (DSK-85a). An open menu is in the highest surface. Lists are asked for when the workspace opens and each time their menu opens. The Agents and Workers menus ask their views through an intent (intent.ts: add, show, edit or delete an item, run or disable a worker, by path or name), and the dialogs have no list of their own. (Settings › Agents, which has no menu, keeps its list.) Under 1100 px the search bar folds to its icon; under 800 px the menus drop their chevrons. The welcome screen’s bar has the brand, Workspaces, Help, the search bar and Settings. A workspace’s settings also open from the status bar (the agent and model) and the command palette (Workspace settings). -
DSK-65b 2026-10-02 (the owner’s request). No views: the middle is always the editor (with no file open, the chat at center stage), and what the menus open slides over it in the drawer (DSK-65c; before v0.7.0, over it in a dialog that a click outside closed), so working on them never switches away from the editor. Changes (
ChangesDrawer, edge to edge). Forms in dialogs share their look: labels in the same style whetherField’s or the worker form’s, and their buttons in the dialog’s footer (.dialog-actions): the Changes view (DSK-90), starting on the source the menu chose. An agent (AgentEditorwithdrawer, in the drawer since v0.7.0, laid out as the worker form: the drawer’s padding and scroll, Name and Description side by side, its buttons in the footer: where it’s saved, Delete, Revert or Cancel, Save): the agent form (DSK-91b), titled with its name, “Unsaved changes” beside the title; closing with unsaved edits, or another view taking the drawer, asks first (Discard); deleting it, or cancelling a new one, closes it. A worker (WorkerDrawer, in the drawer): two views in one drawer. The read view (what DSK-91 shows for a worker: its description, schedule, permissions, limits and runs; Run now, Enable/Disable, Edit, Delete; its state as a chip beside the title) and the edit view (the worker form,WorkerEditor, its Cancel and Save or Create in the dialog’s footer), switching in place in the same drawer (no second one opens); with unsaved changes in the form, ✕, Escape or another view taking the drawer asks first (Discard, or Cancel to keep editing): Edit opens the form, Cancel or saving goes back to the read view; Add worker starts on the form, and saving shows the new worker’s read view; Delete asks, then closes; deleting from the menu only asks. Run and disable from the menu open the read view and do it there (a run shows live). Opening a run’s conversation closes the dialog. The status bar’s changed files and failed worker runs, the command palette’s Show changes and Show workers, and a turn’s link to its changes open the same dialogs (showView: “changes” or “workers”; “editor” closes what’s open); without a name, the worker dialog shows the first worker whose last run failed, else the first. Opening a worker sees the workspace’s failed runs (BL-WK-11). -
DSK-65c Since v0.7.0 (the owner’s request: a click meant for the window, such as moving it by its title bar, closed Settings; and the forms wanted room). Panels stay put. The drawer (
ui/Drawer.tsx): the views (Settings, the user guide, Changes, History, an agent, a worker, the background runs’ inbox, Licenses) slide down from under the top menu as a sheet over the window’s whole middle (the Files shelf, the editor, the chat and the pinned input, which steps aside while it’s up) to the status bar, clipped at the top menu’s edge so they come out from under it. The title bar and the status bar stay: the menus work (opening another view from them replaces the sheet’s), the title bar moves the window, and nothing outside the sheet closes it. Its header has the view’s icon, title, actions and ✕; its footer the view’s buttons. ✕, or Escape with the focus inside it and no dialog over it, closes it. Forms keep to a measure in it (Settings’ sections at the left and its pane up to 960 px; an agent, a worker, the inbox and Licenses up to 1,080 px, centred); Changes, History and the guide take its width. One at a time (DrawerClaims, unit-tested): a view that opens closes the one it replaces, or, when that one has unsaved changes (an agent’s or a worker’s form), asks first: Discard closes it, Cancel closes the new one and shows the old one again as it was (it stays mounted, hidden, while asked). Glass on, it’s a pane 8 px from the window’s edges, opaque (DSK-38). It focuses what asks for it (or its first field) when it opens and gives the focus back when it closes. Dialogs stay for short forms and questions (committing, a new branch or a merge, Rewrite with AI, the project’s settings, confirmations): Escape or their own buttons close them, never a click on the scrim, and a strip over the title bar (.scrim-drag) keeps moving the window while one is open. The command palette and search still close on a click beside them (pickers, like menus), and have the same strip. -
DSK-66 Files (spec_files_029 §5–6): the Files shelf on the left (minimizes to a rail), Go to file (⌘P, the rail’s search button, or the top bar’s search: the command palette’s files, DSK-78e), and the editor in the middle. Paths in tool calls, and inline code in answers naming a workspace file, open it (switching the middle to the editor).
-
DSK-66a 2026-10-01. Welcome tiles and setup. A new chat’s welcome screen asks
GetSuggestionsand shows at most four tiles, worded in the window’s language (suggestions.ts, unit-tested): Set up this workspace for agents first and highlighted when the harness is missing (spec_memory_012 MEM-23); The specification changed: update the skills and agents when a spec is newer than them (MEM-44; sends/skills update); Continue “” (opens the latest conversation); Review my N uncommitted changes and propose a commit message; Find out why the worker X failed (with the run’s error); then the ideas a model wrote from the recent conversations (MEM-24). With nothing to show, the four canned tiles. While ideas are being written (pending) it says “Learning from your recent chats…” and asks again every 3 s, up to ten times. Setup is also the top bar’s wand (Set up the agent harness, beside Go to file) and a command palette entry; each starts a new chat running/setup(startSetup, a freshcompose). -
DSK-66e 2026-10-01. Tool calls in the conversation (
groupTools,latestTurninturns.ts, unit-tested): every run of calls, one or more, is one item keyed by its first call, so as calls arrive it only grows and nothing already drawn is replaced (a lone call becoming a group used to be redrawn, a visible flash). One call shows as its row alone; from two, a head (“Used 3 tools”) sits above the same rows. A group stays open while its turn runs (or when a call failed) and folds when the turn ends, not between calls; rows are keyed by call ID. -
DSK-66f 2026-10-18 (a live pass). A reopened chat shows what a live one did: its tool calls, each with its result, back after the prompt they ran under (
SessionService.ListSessionTools, read from the session’s event log, long strings cut at 4 KiB;withToolsinturns.tsplaces each by its prompt’s text, else by count, and leaves out one whose prompt can’t be found), and the images a prompt was sent with as named chips (splitAttachments: the transcript’s[images: …]line). Notices (version control, warnings) aren’t in the log and stay live-only. -
DSK-66b 2026-10-01. Model fields (
ModelInput.tsx, rules inmodels.ts, unit-tested): a workspace’s model, the agent editor’s default model, the worker dialog’s model and Settings › Providers’ model share one field. It never capitalizes or corrects what’s typed, suggests every configured provider’s models (ListModels, spec_service_021 SVC-55; asked once per workspace per window, again after a provider or key change) with the pinned models and those with settings, and lowercases gemini, anthropic and vertex-anthropic names when it’s left, as the service does. A name with spaces, or a provider prefix with no name, is an error: a workspace’s settings don’t apply it, and the forms’ Save is off. A name its provider’s list doesn’t include is a warning (“gemini doesn’t list X: it will be sent as written”), which saves: providers don’t list every name they take. No warning when a provider couldn’t be listed. All single-line text fields have autocapitalize and autocorrect off (ui/inputs.ts, on focus, unless markeddata-prose); multi-line fields such as the composer keep the system’s behaviour. -
DSK-66c 2026-10-01; the default since 2026-10-02. Settings › Appearance › Chat input (advanced, DSK-60a): Pinned to the bottom (the default) or With the conversation (
composerin the window’s preferences:pinnedunless it’spanel; adesktop.jsonthat already sayspanel, as every one saved before did, keeps it). Pinned, the shown workspace’s composer and task list render into a band across the window’s foot, above the status bar and under every panel, on every view (composerDock.ts: a React portal, so the conversation still owns drafts, steering,@completion, attachments and stop). Pinned, it is part of the window’s frame, not a card: flush and square, the status bar’s tone, a 1 px top border where the panels end; one line at rest with a › prompt mark, growing upward (pushing the panels up, to a third of the window); the text capped at 1,100 px from the left, the attach button borderless and stop and send round (since 2026-10-04: send the accent gradient with its shadow, glowing in step with the ring while a turn runs, flat while there’s nothing to send; stop the accent tint) at the right edge, 30 px, the same height as a line of text so one line and the icons share a centre; the chat’s New chat button is the same filled accent; focus and a running turn show on its ring (DSK-77b); background tasks a flat row above it; the / and @ menus at most 760 px wide. ⌘L / Ctrl+L focuses the shown workspace’s composer from anywhere, in either place. -
DSK-66c-glass 2026-10-01. With glass on (the default), the pinned input is one more pane: under the Files, viewer and chat panes and apart from the status bar, with their 8 px gap, 16 px corners, border, blur and shadow (
glass.css); its border is the ring (DSK-77b). With glass off, where the panes are flat and edge to edge, it stays the flat band above the status bar, the ring drawn square around it. -
DSK-66d 2026-10-01. The composer’s keys (Enter to send, Shift+Enter for a new line, /plan) are a keyboard icon beside send, in either place, whose tooltip lists them: on hover after a moment, and on keyboard focus (
KeyHint, a CSS tooltip, so not the native title’s delay).
API used: SessionService.GetActiveSession, NewSession, ListSessions, LoadSession, RenameSession, RunTurn, Steer, Approve, Answer, GetUsage, Rewind; WorkspaceService.SetPermissionMode, SetSetting.
- DSK-70 On opening, show the workspace’s active session, or start one. A session bar shows its title (click to rename; Enter saves, Escape cancels; “New chat” until named), History (a menu of this workspace’s sessions with message count and last update; snapshots marked 📸; each row but the open chat’s has a delete button, shown on hover or focus, or Delete on the row: a dialog confirms, then
DeleteSession, SES-23, and a snackbar says so. 2026-10-04 (the owner’s request): the menu (HistoryMenu.tsx, rules inhistory.ts, unit-tested) opens with the focus in a search field, which keeps the sessions whose title, snapshot name or agent has every word, in any case; All, Chats and Workers (each with its count) filter by origin, SES-24, a detached run counting as a chat. Every row but the open chat’s has a checkbox, and Select all shown ticks or clears the rows shown; with any ticked, a bar at the bottom says how many and offers Clear and Delete n, which closes the menu and confirms in a dialog listing them; they’re deleted one by one, and one snackbar says how many went and which couldn’t, with why: a worker’s run still going isSESSION_OPEN) and New chat; both are disabled while a turn runs. - DSK-71 The conversation is a centred column (max 860 px). Prompts are right-aligned bubbles; messages sent while the agent worked are dashed bubbles “Sent while working”; a stop hook’s request and an approved plan’s go-ahead show as small notices, not bubbles (message
kind). Model text renders as Markdown (§11a). A sub-agent’s text is labelled with its name. - DSK-72 Hovering a prompt shows Copy, Edit (rewind code and conversation to before it and put the prompt back in the composer) and a rewind menu: Code and conversation, Conversation only, Code only, Summarize from here, Summarize up to here, Copy into the composer. Rewinds need the prompt’s transcript index, known for saved prompts and learnt for new ones after each turn (the n-th prompt shown is the n-th saved;
assignPromptIndices). A conflict (UNDO_CONFLICT) asks “Files changed since” — Keep them or Overwrite them (force). Rewinds are unavailable while a turn runs. - DSK-72a Hovering a finished answer (or focusing its actions) shows Copy (what the answer shows: its text as laid out, and HTML for rich editors, without code blocks’ controls; text only where the clipboard refuses HTML), Copy as Markdown (its source) and, when the turn’s answer came in parts (between tool calls or steering), Copy the whole answer (the parts’ Markdown, in order;
turnAnswers). Each says so in a snackbar, or why it couldn’t copy. - DSK-73 Turn rendering (
turns.ts, unit-tested): streamed text grows one open entry and its final repeat closes it; final text that wasn’t streamed is its own entry; a whitespace-only final text (the separator between runs of one turn) is added to the previous answer; thinking becomes thought entries by the same rules (shown only with Show thinking, folded: “Thoughts”, or “Thinking…” while streaming); streamed copies of tool calls are left out; a tool result completes its call (by ID, else by name);finishedcloses open text and adds an error notice if the turn failed. Anoticeevent is a notice entry (error or information), closing the answer before it. - DSK-74 Tool calls show as rows with an icon for their kind (read, search, edit, shell, web, agent, task list, question, plan), the name, the argument summary (first of
path,command,url,query,question,pattern,agent,reason, cut to 80), and a pending/✓/✗ status with the error; clicking expands the arguments and result as JSON (cut at 20 000 characters). Two or more consecutive calls fold into Used N tools (with their icons and the number that failed), open while one runs or one failed, folded otherwise. - DSK-75 “Working…” shows while a turn runs and nothing waits for the user. The view follows new output unless the user has scrolled up more than 80 px.
- DSK-76 Task list (
TurnEvent.tasks): a card above the composer, Tasks with “n of m done”, a progress bar and the items (done struck through and ticked, in progress pulsing); it folds itself when all are done and can be hidden after the turn. A new session clears it. - DSK-77 Composer: a rounded field that grows up to 280 px; Enter sends, Shift+Enter adds a line (IME composition respected); images to attach; a hint line; the hint line names
/plan(the agent plans and asks for approval before changing anything). The permission mode and reasoning effort are set from the status bar (DSK-39), not the composer; Send (disabled when empty), and while a turn runs Stop — the text then steers the agent (Steer), shown as a dashed bubble. An empty session shows “What are we working on in?” with four suggestions that fill the composer. - DSK-77b 2026-10-02 (the owner’s request). The chat input’s border, in the conversation or pinned (the dock carries it), with glass on or off, is a ring in the welcome title’s gradient (
.gradient-text: the theme’s primary to its tertiary; dark light blue#a8c7fato green#6dd58c, light deep blue#0b57d0to deep green#146c2e;--ring-a,--ring-band their oklab mix; since 2026-10-04--ring-aand the mix are the accent’s--accent-aand--accent-b, DSK-35, so the ring and the buttons can’t drift apart), blue to green and back around it (a conic gradient on::after, masked to the border’s width,app.css). It turns: its angle is a registered@property, 8 s a turn at rest and 2.5 s while a turn runs (which replaces the input’s running tint and the dock’s sweeping line); at 75% at rest, full and with a soft green glow when focused or while a turn runs; still with reduced motion, and still where@propertyisn’t supported. It replaces the wheel of four colours the pinned input had (DSK-66c-glass) and the plain border that turned grey on focus. - DSK-77a @ mentions: typing
@in the composer (at the start or after a space;@"for paths with spaces) lists the workspace’s files and folders matching what follows (FindFileswithfolders, 12 at most, as typing pauses), with their icons; ↑/↓ choose, Tab or Enter puts the path in place of what was typed (@path, quoted when it has a space; a folder keeps the caret after its slash, to go on into it), Escape closes the list. An image chosen is also attached (LoadImage). The service adds what they name to the prompt (WS-21a). Pure rules inmentions.ts, unit-tested. - DSK-78 After a turn: “This turn:
in · out · context · $ ” plus the session total (“session tokens · $ ”, from GetUsageand each turn’safter). Stop aborts the turn (“Stopped.”); otherwise leftover steer messages are sent at once as the next turn, marked accepted. Errors show as a dismissable card with the service’s message;unavailableerrors also start reconnecting (DSK-51).
- DSK-79 Model text, questions and summaries render with
react-markdownand GitHub-flavoured Markdown (tables, task lists, strikethrough) to React elements; raw HTML is skipped, never rendered. Links show their target on hover and open withOpenURLafter a click (never in the window). Images are never loaded: they show as links. Fenced code blocks show their language and a Copy button and are syntax-highlighted (§11b); inline code is tinted; wide tables scroll.
- DSK-79a Code blocks and diff lines are highlighted with highlight.js grammars through
lowlight(the common set, about 35 languages), which returns an element tree rendered as React elements — never an HTML string. A block’s grammar comes from its fence’s language (with aliases:sh→bash,ts→typescript,yml→yaml,golang→go,proto→protobuf, …); a diff’s from the file name (extension, orMakefile). Unknown languages, and code over 100 000 characters, stay plain text. Diff lines are highlighted one at a time after their+/−/space sign (shown in its own column), over the added/removed tint. Token colours are theme tokens (--hl-*) with a light and a dark set.
- DSK-78c A composer line that is a command (
/name args; names are letters, digits,-,_,:,.—/usr/bin …is text) runs in the window instead of going to the agent. Built-in commands, through the same API calls as the REPL:/plan <goal>(a plan turn),/btw <question>(an aside turn, shown as “Side question: not kept”),/search web <terms>(SearchWeb; the links show as a notice, then a read-only turn reads them with fetch grants) and/search session <terms>(SearchSession, then a read-only turn),/undo [--force],/checkpoints,/diff(opens Changes),/costand/context(GetUsage),/compact [focus],/agent [name],/model [ref],/mode [name],/effort [level],/session save <name> [--force](SaveSnapshot),/rename <title>,/new,/help(every command, the workspace’s too). The workspace’s custom commands and skills (ListCommands; a built-in name wins) run as command turns (Turn.command). Unknown commands are refused (“Unknown command /x”), never sent. Commands that start or change a turn wait while one runs; the others work during a turn. Results show as notices, as Markdown where they’re tables or lists. - DSK-78d Typing
/name(before a space; the whole message, leading spaces and new lines aside, as the command line is trimmed) opens a menu above the composer of up to 12 matching commands — names starting with the text first, then names containing it, then (from three letters) descriptions — with their arguments, description and source; ↑/↓ choose, Tab or Enter completes (Enter runs a command whose name is already whole), Esc closes (until the text changes: the same text typed again opens it again). - DSK-78e Cmd/Ctrl+K, Cmd/Ctrl+P or the top bar’s search bar opens the command palette: once something is typed that isn’t a command (
/…), the active workspace’s files first (FindFiles, up to 8, as it ranks them;name:12opens at line 12,paletteResultsandsearchesFilesincommands.ts), then the active workspace’s commands (a command without required arguments runs at once; others go to the composer to finish), views (chat, changes, workers), its recent chats, the open and recent workspaces, “Open a workspace…”, Settings, the theme, Workspace settings (Settings › Workspaces on the workspace in view) and menu toggles. Typing filters (every word in label, detail or group); label starts rank first, then labels with every word, then the rest; ↑/↓, Enter, Esc. Parsing, matching,/helpand filtering are pure (commands.ts), unit-tested.
- DSK-78b Images can be pasted into the composer, dropped anywhere on the conversation (an overlay “Drop images to attach them” shows while dragging files), or chosen with the attach button. Only images up to 20 MB are taken (others are refused in a snackbar); each uploads at once (
AddImage) and shows as a chip with its thumbnail (a local object URL), name, and the service’s dimensions and size (“scaled down” when the service resized it) or its error; chips can be removed. Send waits until uploads finish; the turn carries the uploaded images’ IDs (Turn.image_ids), and the prompt’s bubble shows them. With images turned off for the workspace (imagesEnabledfalse) the button is hidden and pasting or dropping explains why. Files dropped elsewhere in the window are ignored, so the web view never navigates to a dropped file. Rules inattachments.ts, unit-tested.
- DSK-78f Every string the page shows comes from the terminal’s catalogs (
pkg/i18n/locales, embedded in the page at build time) under keys starting withdesktop., throught(key, params)andtn(key, count)(i18n.ts): placeholders{name}, plurals.one/.otherby the language’s CLDR rules (Intl.PluralRules), and the terminal’s lookup chain (fr-CA→fr→en-US; a barefrborrowsfr-CA; a missing key shows as the key). The language is the window’s own setting (DSK-40),systemby default: the first of the system’s languages with a catalog, else English. Changing it re-renders at once. Dates and numbers use the same language. The pseudo-localeen-XA(setlanguageindesktop.json) shows every catalog string accented and bracketed. Text from the service or the model (tool details, approval scopes, errors, command results) isn’t translated by the page. The dialog to choose a directory gets its title from the page. - DSK-78g Two page tests keep it that way (the page’s I18N-06): one reads the sources with the TypeScript compiler and fails on English written into the code — JSX text, text attributes (
title,placeholder,aria-label,label,alt, …),snack()/setError()/notify()-style calls and menu fields (label,detail,heading, …) — with file and line; the other fails on at()/tn()key missing fromen-US.json. The Go catalog checks (I18N-05) then require every key, with its placeholders, in every shipped catalog.
- DSK-78a With Notifications on (the default; Settings › Appearance;
notificationsindesktop.json), the window notifies when an approval or a question waits (": approval needed" with the request, “ : the agent asks” with the question), or when a turn that took at least 10 s finishes (" : done" with the start of the answer, or “the turn failed” with the error) — but only while the user isn’t looking at that conversation: the window isn’t focused, or another workspace or view is shown ( shouldNotify, unit-tested). Bodies are cut to 180 characters. In a browser (development) the web’s Notification API stands in.
- DSK-80 An approval request shows as a card: “Allow this?”,
<tool>wants to:, the diff coloured per file (§14), and Allow once (focused, filled), Allow this session , Always allow(when the request has a scope), Deny (outlined, red). 2026-10-15 (the owner’s report: a turn hung after its first answer): requests and questions that arrive together (the parallel tool calls of one model response, each waiting for its own answer) are queued in order ( waits.ts) and shown one card at a time, the next as each is answered; the turn ending clears them. One slot kept only the last, and the calls whose requests it replaced waited until the turn was stopped. - DSK-80c Failed worker runs (BL-WK-11): a worker whose latest run (
Worker.last_run, whichListWorkersnow carries) failed or hit a limit after its workspace’s Workers view was last shown (workers_seenin the workspace’s preferences, set whenever the view shows) counts: the workspace’s row in the Workspaces menu (DSK-65a; it was a dropdown) gets a red dot and “1 worker failed”, the menu button a red dot when another workspace has some, and the Workers button a count, “Workers (1)”. One poll (ListWorkersfor each open workspace, every minute, and at once when the open workspaces or their seen times change) serves them all (workerFailures.ts). - DSK-78h The window’s System language follows the terminal’s:
ConfigService.GetInterfaceLanguagegives the[ui] localethe user’s settings set (empty when they don’t) and their catalogs from[ui] locales_dir(checked as the terminal loads them:i18n.CheckCatalog; at most 20, 1 MiB each). The page adds those catalogs (new languages join the setting’s list; a catalog for a built-in language overrides its translations) and System tries that locale before the system’s languages (applyUserLanguage, when the service is up). An older service: the system’s language, as before (BL-DSK-41). - DSK-80d The Workers view asks for the workspace’s workers every 5 s while it’s open, so a
WORKER.mdadded or edited elsewhere shows up. Each run shows the files it changed (“Changed: …”) and Open its conversation, which loads the run’s session into the chat (BL-DSK-50). - DSK-80b The inbox of background runs (spec_background_agents_032 BGA-67): a button in the top bar (beside go-to-file, and on the welcome page) with a badge counting the runs going on, red with the count waiting for an answer when any do. It opens a dialog listing every workspace’s runs (state, prompt, ID, workspace, age, cost, error) with Open and, while one goes on, Stop. Open opens its workspace (as a tab) and loads its session, once the workspace is there if it’s opening (
loadSession,takePendingLoad). A chat whose session a run is working in follows the run as a turn from its start (WatchBackground): its approval requests and questions show as the turn’s cards, and Stop stops the run. One poll (ListBackground, every 4 s while an inbox button is shown) serves every button. - DSK-80a Background tasks (spec_background_agents_032 BGA-33, BGA-41): a background
invoke_agentshows a card under its tool group (agent, task, state, runtime, cost, the prompt; Show for its latest events and result, Stop while it runs), refreshed every 2 s until it ends. While a chat is open its tasks are followed (WatchTasks, again after 3 s when the stream breaks): their approval requests and questions show as the usual cards, labelled “qa · task-3 asks, from the background”, not focused, beside the turn’s own; Ctrl+J moves to the next. A task that ends adds a notice; with Settings › Continue after background tasks (task_continue, off by default) it also starts a turn about its result when nothing runs. Reopened chats show ended tasks as notices (the transcript’staskmessages). - DSK-81 A question shows as a card “The agent asks” with the question as Markdown (up to half the window, scrolling), one button per option (the first filled; since v0.6.3 a long answer wraps onto more lines, with rounded corners, rather than being cut off at the card’s edge), and a field for another answer (“feedback on a plan, say”). A plan review (
exit_plan_mode) is such a question: the plan renders as Markdown and typed feedback makes the agent revise it. - DSK-82 One request shows at a time (the latest); it disappears once answered or when the turn ends. Late answers fail with the service’s
UNKNOWN_REQUEST.
API used: ConfigService (DSK-61); WorkspaceService.ListAgents, SetAgent, SetModel, GetModelSettings, UpdateModelSettings, SetSetting, SetPermissionMode, ListLocales, SetLocale, ListPermissionRules, AddPermissionRule, RemovePermissionRule, ListApprovals, RevokeApprovals; SessionService.GetUsage, Compact.
- DSK-85 Until 2026-10-02 a panel; now the form in Settings › Workspaces (DSK-85b). A panel beside the conversation (§6 for narrow windows), 420 px unless dragged wider or narrower at its left edge (340–720 px, remembered as
run_settings_width; double-click puts back the default), with collapsible sections, folded until unfolded (the window remembers which, in its local storage): first the everyday ones (Workspace, Agent and model, Behaviour), then under an Advanced settings divider Thinking, Generation, API keys, Permission rules, Standing approvals, Project settings, Settings file and Context; each a card with its icon and title, where what it sets is kept when that’s one place (This workspace: API keys, Permission rules; This model: Generation; This session: Context), and when folded a summary of what’s set (the agent and model, the provider, the effort, the temperature and top P, the mode and agency, the context’s size); fields go two to a row where the panel is wide enough (Thinking, Behaviour): Agent and model — agent (with pinned models), model (a field with suggestions: pinned models and models with settings; applied on Enter or leaving the field); Thinking — reasoning effort for the session, thinking budget for the model; Generation ·— temperature and top P (slider with a number, “default” when unset), max output tokens (placeholder: the global value), Reset to defaults (settings the provider ignores are reported); Behaviour — permission mode (with its meaning), agency, the output style (with its description; ListStyles,SetSetting style), and the language the model replies in; Permission rules — the workspace’s own rules (PermissionSettings.tsx, as in Settings, Permissions, for the global ones:ConfigServicepermissions calls), each with its effect (coloured) and remove; 2026-10-03: rules left out when the settings last changed (ListPermissionRules.problem) show above them as a warning naming them, saying they aren’t enforced and that the workspace won’t open with them after a restart; a card to add one — effect, rule, and Try it on a sample — checked as it’s typed (CheckPermission: the canonical rule, what it matches in words, whether it applies to the sample, and that a redirection still asks), Add only for a valid rule; the built-in read-only rules with their commands and exceptions (a switch in Settings; in a workspace, use the global setting, on or off); in a workspace, the global rules it inherits (read-only) and the session’s own (from/permissions, removable); the section’s count leaves out the built-in rules; Standing approvals — each with its kind, subject and scope, revoke one or all; API keys (collapsed; DSK-61, for this workspace); Context — context size, tokens sent and received, cost, and Compact the context. Every change reports failure in a snackbar and reloads the panel. - DSK-85a 2026-10-02 (the owner’s request). One form for a workspace’s settings (
WorkspaceSettingsForm, in the panel and in Settings › Workspaces): the panel is Workspace settings (the run settings panel, renamed in every string), its head the workspace’s avatar, “Workspace settings” and its name, and it takes in what was elsewhere. Workspace (first): name, description, colour and folder (copy), the fields of the details dialog (WorkspaceFields), saved as they’re changed: a name or description when its field is left, a colour as it’s picked; folded, the description or the folder. Project settings (Project): the trust state (“3 wait for your trust” folded), the files, and Review (the project settings dialog, PRJ-33). Settings file (This workspace): what it’s for, its path (GetConfigFile), and Edit the settings file, which opens Settings › Settings file on this workspace’s (SettingsDrawerscope). Settings › This workspace (DSK-65a) is gone: everything in it is here. - DSK-85b 2026-10-02 (the owner’s request). No panel: a workspace’s settings are only Settings › Workspaces (DSK-60), the same form (
WorkspaceSettingsForm.tsx, every card of DSK-85 and DSK-85a). Its pencil in the Workspaces menu, Workspace settings… at the foot of the status bar’s agent menu (DSK-39), and the palette’s Workspace settings open the dialog there, on that workspace (SettingsDrawerinitial="workspaces",scope), without switching the window to it. Gone with the panel: its width (run_settings_width) and shown state (run_settings) indesktop.json(old values are ignored), Appearance’s Run settings panel switch, the palette’s show and hide, and its floating over the chat under 1280 px; the editor keeps its room beside the chat. - DSK-86 Panel widths follow the window (
useWindowWidth: resized, or moved to another display, where the width can change without the page reloading): the chat and the Files shelf are kept to what fits (panelWidth), so a width saved on a larger display shrinks on a smaller one and comes back on the larger; the chat leaves the editor at least 360 px beside the panels that sit inline. A banner above the top bar (the service lost or stale) starts clear of the macOS window buttons. Menus and context menus float over the whole window (Menu,ContextMenu, and the workspace switcher’s dropdown, throughuseAnchoredMenuandFloatingLayer: drawn in the page’s body, fixed to the window, so no panel clips them): a menu opens beside its button on the side asked for, or the other when that has under 200 px and the other more, no taller than the room there, and moves inside the window once its size is known (menuPosition,nudge); clicking its button again closes it, as does a click elsewhere, Escape or the window changing size. Arrow keys, Home and End move through a menu’s items (it opens on the chosen one, else the first), and closing it gives the focus back to its button. A dialog keeps the focus: Tab and Shift+Tab move through everything inside it, buttons too (the web view skips them otherwise), wrapping at its ends; an editor inside may keep Tab; only the topmost dialog answers Escape; it opens on what asks for the focus (Settings: the section shown), else its first field, and gives the focus back when it closes. The command palette and Go to file behave the same way (useModal).
- DSK-90 Changes view: This session (the agent’s changes,
GetDiff) or Version control (GetDiff git: every change since the last commit, staged or not, and new untracked files, 2026-10-04; outside a repository, whichGetGitStatustells, instead “This folder isn’t under version control” and Turn on version control,InitGitRepo, which runsgit initthere; without git on the service’s PATH it says git isn’t installed; a repository with no commit yet is one, on its branch); a count of files and lines added and removed; Refresh; Undo last turn (a conflict asks Keep them / Undo anyway). A side column lists the files (name, path, +/−) and the turns that changed files (a timeline fromListCheckpoints); the main pane shows the agent’s latest summary of its work (its last answer, as Markdown, foldable — the walkthrough) above the selected file’s diff. Diffs are parsed per file (parseDiff, unit-tested: file headers, git’sdiff --githeaders so binary, renamed and mode-only files still list, a--statsummary skipped,/dev/nullfor created and deleted files, hunks, “too large” notes) and coloured: added and removed lines on tinted backgrounds, hunk headers in the secondary colour.
DSK-90a 2026-10-04 (the owner’s request). Commit dialog (CommitDialog.tsx, rules in commit.ts, unit-tested), from Changes › Commit changes… in the menu bar, Commit changes… in the Files tree’s Version control menu, and Commit changes… in the Changes view’s toolbar on the Version control source. It asks DraftCommit as it opens: the staged files (git’s status letter in its colour, and the path) and a message the model drafted from the staged diff on the user’s behalf, which is theirs to edit (a monospace field, the first line’s length against 72 shown, red past it). Redraft asks again. With nothing staged it says so and offers Include all changes (DraftCommit with stage_all: git add --all, then the draft; the tree’s marks refresh). A model that fails leaves the field empty with the reason under it (its first line, redacted, as ModelErrorSummary gives it: never the client’s settings); the user writes the message. Commit n files (⌘Enter / Ctrl+Enter in the field) is Commit, enabled with something staged, a message, and no call under way; afterwards a snackbar says “Committed
DSK-90b Since v0.6.3 (the owner’s request). The workspace’s remote, on the Changes view’s Version control source (RemoteSync.tsx, wording in gitRemote.ts, unit-tested). With a remote, the toolbar has Pull and Push, each with a count badge: the commits to pull and to push as of the last fetch (GetGitStatus ahead, behind); their tooltip says it (“origin/main: 2 commits to pull, 1 commit to push”). The view fetches once as it opens (FetchRemote; quietly: offline or not signed in, the counts stay as they were). Pull (Pull) follows the repository’s git config (pull.rebase, pull.ff; a merge when neither is set) and keeps uncommitted changes (--autostash); a snackbar says what it did (“Pulled 3 commits from origin/main and merged them with yours”, “Already up to date…”). A branch without an upstream offers Publish branch instead of Push (Push sets the upstream: origin, or the only remote), and Pull is off. A rejected push says to pull first; a remote that wants a sign-in says to sign in with git once in a terminal (git never prompts). A pull that stops on conflicts shows a banner, “Merging origin/main” (or “Rebasing onto…”), with how many files are in conflict, Ask the agent to resolve (the chat, with a prompt naming the files: settle each, remove the markers, check it builds, git add it, and leave completing the merge to the user; a rebase’s prompt explains that git’s “ours” is the upstream), Abort merge (after a confirmation that settled conflicts are lost: the branch and the uncommitted changes go back as they were) and Complete merge / Continue rebase, enabled once no file is in conflict. Conflicted files are marked Conflict in the list, and the selected one has Open in editor, Keep mine, Take theirs and Mark resolved (refused while conflict markers remain) above its diff. Pull, Push and Commit wait while a merge with conflicts is in progress. The service runs these with the repository’s own git config checked: a setting there that runs a command (ssh command, credential helper, filter, merge driver, …) stops the action and names it (spec_service_021 SVC-36).
DSK-90c 2026-10-16 (the owner’s request). The workspace’s branches, on the Changes view’s Version control source (Branches.tsx, wording in gitRemote.ts, unit-tested). The toolbar’s first button is the current branch; it opens a menu (loaded as it opens, ListBranches): Branches (the local ones, the current one checked) and On the remote (the remotes’ branches no local branch tracks), each with its last commit’s subject. Choosing a branch switches to it (SwitchBranch; a remote’s makes a local branch that tracks it), and a snackbar says so (“Switched to main.”); uncommitted changes that would be overwritten stop it ("…commit them first"). Each other branch’s row has a merge button. Below, New branch… (a dialog asking the name, “Starts at main’s latest commit. Your uncommitted changes come along.”; Create and switch, CreateBranch; a taken or invalid name is said under the field) and Merge into current… (a dialog to pick the branch, saying what merging does; Merge, MergeBranch; the snackbar says what it did: “Brought 2 commits from feature into main.”, “Merged…”, “main already has everything in feature.”). A merge that stops on conflicts waits in DSK-90b’s banner, which names the branch (“Merging feature”, from GetGitStatus merging; a pull’s still names the upstream); Ask the agent to resolve names both branches, and Abort merge says the branch goes back to before the merge. The button is off while a merge or rebase waits. The status bar’s branch opens Changes on the Version control source (on this session’s in simple mode, which has no Version control source) (spec_service_021 SVC-37).
DSK-90d 2026-10-16 (the owner’s request). Plain words for version control, so the app reaches people who aren’t developers: the page says what a git action does, not git’s name for it. Version control, not “Git”, names the Changes source, the Changes menu’s item, the Files tree’s menu heading and the command palette’s row; “This folder isn’t under version control” and Turn on version control outside a repository. Staging is including: Include in commit and Leave out of commit (were Stage and Unstage), Include all changes, “3 files included”, a file’s state “Included in the next commit” (or not, or partly). Do not version and Version it again (were Add to and Remove from .gitignore), View history… and “History of file” (were View revisions… and Revisions of), Commit changes… (was Commit…). Branch, commit, merge, pull and push stay: they’re version control’s words, not git’s. Git is named only where the user acts on git itself: installing it (“Version control needs Git, which isn’t installed”), signing in with it in a terminal, and a setting in the repository’s git config. The prompts sent to the agent keep git’s terms (git add), which it runs. In every language (en-US, es, fr-CA).
-
DSK-91 Workers view (behaviour as before): a list with state and schedule; the selected worker’s description, state chip, schedule (with next run when enabled), agent, model, permissions as chips, limits, content hash and problems; Enable as shown (enables the displayed hash;
HASH_MISMATCHif edited since), Run now (with the run’s events live), Disable; the latest 20 runs with an icon per status, cost, time, session, refusals and error. Since 2026-10-02 the list is the top bar’s Workers menu (DSK-65a); the view shows the chosen worker across the pane, with Delete beside Edit. Since v0.7.1 the list and the view are in Studio › Workers (spec_studio_040 STU-03). -
DSK-91a Since v0.6.3 (the owner’s request). Every worker run going now can be seen and stopped. Running now (
runningWorkers.ts, unit-tested;ListRunningWorkerRuns): each row is the worker, with its workspace when it’s another one and when it started, and a Stop button (StopWorkerRun; a snackbar says “Stopping…” and the row goes). Since v0.7.1 it heads Studio › Workers (the workspace’s runs) and Settings › Workers (every workspace’s), instead of the Workers menu. In a worker’s view, a run going now has Stop in the run list, and the run started there has it in its live panel; a stopped run shows as Stopped with a stop icon and “stopped by the user”, not as an error. -
DSK-91b Since v0.6.3 (the owner’s request). Under the worker form’s instructions (Workflow) is Rewrite with AI, a small text button with the sparkle icon, as the commit dialog’s Redraft (enabled with instructions; “Rewriting…”, and a progress bar under the label, while it asks
RewriteWorkerwith the form as it is). The suggestion opens in a review: what changed, the instructions (read-only), and, when they differ, the permissions it would add (+) and drop (−) under Also use the permissions they need (checked). Use these puts the instructions (and the permissions, when checked) in the form; Keep mine leaves it. Nothing is saved until Save. A model that fails says why in a snackbar and the form stays as it was. Rules inworkerForm.ts(applyRewrite,permissionChanges), unit-tested. -
DSK-91a New worker (Workers › Add worker in the menu bar until v0.7.1, then New worker in Studio › Workers; and the empty list): a dialog with a field for each frontmatter setting — what it does (the name follows it until typed:
workerName), name (checked as typed), schedule (with the phrases it takes), time zone (the machine’s), agent (the workspace’s, or the default), model, the workflow, permissions (onekind:patterna line), the limits (empty: the policy’s), missed runs (Skip / Run once). Create is off until name, schedule and workflow are there; the footer says where it will be saved.CreateWorker(WK-01a): its problems are listed in the dialog (scrolled into view) and nothing is written; created, the dialog closes and the worker is selected,new, to review and enable. Rules inworkerForm.ts, unit-tested. -
DSK-91b 2026-10-01. Agents view and Settings › Agents (
AgentEditor.tsx,agentForm.ts; spec_agents_014 AG-14): the view shows the workspace’s agents (.agents/agents; since 2026-10-02 listed in the top bar’s Agents menu, not beside the form, DSK-65a), Settings lists the user’s (~/.blitz/agents), fromListAgentFiles, asked again every 5 s so files added or edited elsewhere show up (unsaved edits are kept); a file that doesn’t load shows why. The editor has every frontmatter field and the prompt. On top: Name (the display name; the file name follows it until typed), Description, System instructions (the prompt) and Run in the background. A folded Advanced section (“Advanced (n set)” counts what’s set there,advancedCount) holds the rest: the File name (the agent’s name), default model with the known models offered, agency, permission mode, max turns, isolation, tools as checkboxes fromListTools, and Model settings (effort, thinking budget, temperature, top_p, max_tokens; empty leaves the model’s). It opens itself when a field in it is wrong. The layout answers to its pane, not the window (container queries): under 720 px the list goes above the form, under 480 px the form’s pairs are one column; its dialogs are drawn over the whole window. New agent, Save (SaveAgentFile; a rename passes the previous name; problems shown in the form, nothing written), Revert and Delete (confirmed). -
DSK-91c 2026-10-01. Edit on the selected worker (also an invalid one) opens the worker dialog (
WorkerEditor.tsx, which New worker also uses) filled fromGetWorkerSpec, titled “Edit”, with the name read-only and Save ( UpdateWorkerat the loaded hash, WK-01b). Problems show as for Create; a file changed on disk since it was loaded (HASH_MISMATCH) is said at the top of the form, Save is off, and Reload loads the file again (confirmed when the form has edits). The dialog’s rules are inworkerForm.ts(formFromWorker,savePauses,canSave), unit-tested. Saving an enabled worker says first that it pauses it until it’s enabled again; afterwards it stays selected,changed, for Enable as shown. -
DSK-91d 2026-10-16 (the owner’s request). Agents › Reload agent files (under Add agent, with “Read the agents, skills, instructions and rules in .agents again. Each message already picks up what changed.”) and
/reloadread them again (ReloadAgentFiles): a snackbar (/reload: a chat note listing the files) says what was loaded (“Reloaded 2 agents, 1 skill and 4 instruction files and rules.”,reload.ts, unit-tested), and an error one what didn’t load; the Agents menu lists the agents again (spec_memory_012 MEM-25). -
DSK-92 Deep links (PAR-INT-03):
blitz://open?dir=<absolute folder>&prompt=<text>(alsoblitz:open?…) opens the folder as a workspace, brings the window forward, and fills the composer with the prompt (a snackbar says it’s ready to check and send); it is never sent. The app registers the scheme (since v0.7.0 the Electron app, EL-10 and EL-22: macOSCFBundleURLTypesin Blitz.app’sInfo.plist(mac.bzl), handed over as Electron’sopen-url; Linux:MimeType=x-scheme-handler/blitzandExec=blitz-desktop %uin the.desktopfile, the link an argument) and holds a single-instance lock (requestSingleInstanceLock), so a second start gives its links to the running app. A link names an existing absolute folder (~expanded) and a prompt of at most 20,000 characters, else it’s dropped and logged. Links wait until the page asks for them (PendingLinks), then arrive asdeeplink:openevents; text for a composer not mounted yet waits for it (takePendingCompose). There’s no terminal handler.
- DSK-93 Export as PDF (a button in the editor’s head, for a Markdown file, in simple mode too; only in the app): the file’s preview, as it shows (switching to it first, and waiting until it stops changing as Mermaid diagrams finish), printed by the window’s web engine (Chromium’s, EL-20; until v0.6.5 the system’s) and written beside the file as
<name>.pdf(files/printPage.ts,FileService.WriteBinaryFile). The page is a copy of the preview’s document with each picture put in (the preview shows them as links, DSK-79: a workspace file read withReadPreviewand inlined, a web URL as it is, others left as links), under the window’s CSS in the light theme (not the user’s document CSS for the theme in effect), the user’s light document CSS (FIL-57), and rules for paper (a white page, no anchors or copy buttons, nothing cut across pages, no heading last on a page); from a dark window, each Mermaid diagram is drawn again in Mermaid’s light default theme. Paper is Letter for a system locale whose country uses it, A4 otherwise. An existing PDF is replaced only after Replace? ; a snackbar says it’s saved, with Open; a failure says why. 2026-10-15 (the owner’s testing: pictures missing from PDFs of documents with maths): the Wails app’s printer (printpdf, macOS and Linux) waited, after the page loaded, for its fonts and every picture to be ready to draw (BLITZ_PRINT_READY, at most 10 s) before printing: printing straight away while a font still loaded (KaTeX’s, inlined withfont-display: block) printed the pictures as blank space. The wait ran as the app’s script; the page’s own never ran (WebKitGTK:enable-javascript-markupoff). Electron’s printer does the same since v0.7.0 (printPDFinapps/electron/src/main.ts,printLoadScript,printCheckScriptandwaitForPrintinlogic.ts): a script in a world of its own starts every font and picture loading, and the main process asks every 50 ms whether they’re ready (the page’s scripts, and with them its timers, are off), at most 10 s, then gives the page 150 ms to paint. - DSK-93a Removed in v0.7.0 (the Wails app; see spec_electron_038). Electron prints with
printToPDFin a hidden window, its scripts off: EL-20. What follows was the Wails binding.PrintPDF(html, pageSize)(the Wails binding; the Wails app’sprintpdfpackage) prints a whole page offscreen and returns the PDF, within a minute: on macOS aWKWebView(a non-persistent store, JavaScript off) in a borderless window that’s never shown loads it, thenNSPrintOperationprints it to a temporary file, paginated and fitted to the width, with 15 mm margins on A4 or Letter; on Linux a WebKitGTK view (an ephemeral context, JavaScript and hardware acceleration off, sinceGtkOffscreenWindowhas no OpenGL context) in an offscreen window loads it, thenWebKitPrintOperationprints it through GTK’s “Print to File” printer (by the name GTK gives it, translated). After the page loads, it navigates nowhere. The engine runs on the UI thread (Wails’s; a test runs its own loop,InitUIandRunMainLoop), and a print that doesn’t finish in time is abandoned. CI runs its tests on macOS and, under Xvfb, on Linux, where they must not skip. - DSK-93b 2026-10-14 (#38). The editor head’s PDF button becomes Export (a menu): Export as PDF (DSK-93, in the app only) and Export as Word, which sends the editor’s text (unsaved edits included) to
FileService.RenderDocx(FS-75, its images read beside the file), with its```mermaiddiagrams drawn in the page (drawAsPicture: Mermaid’s light default theme, labels as SVG text, which Word and a canvas can draw, as a PNG at twice their size; one that won’t draw stays as its code) and writes<name>.docxbeside the file withWriteBinaryFile, asking Replace? first when one exists. It works in a browser too./export <name>.docxwrites the conversation as a Word document. - DSK-94 Sound plays in the editor:
.wav,.mp3,.m4a,.aac,.ogg,.opusand.flacfiles open as a player (<audio controls>, fromReadPreview’s bytes, up to 32 MB) with Open in the system viewer; the Files shelf shows them, and PDFs, with their own icons. The conversation showsexport_pdf,view_documentandgenerate_audiocalls with icons of their own. - DSK-94a Speech model in a workspace’s Agent and model card (shown in simple mode too, since audio overviews depend on it): the
[audio] modelfor this workspace, typed or chosen from suggestions (Gemini’s and OpenAI’s speech models), saved on Enter or leaving the field withConfigService.SetConfigValue(audio.model; empty turns speech off); the workspace reloads it at once (MDL-90), and why it can’t be used shows under the field. - DSK-94b PDFs attach as pictures do (spec_images_011 IMG-55): the attach button, paste and drop take PDFs up to 30 MB, the chip shows the PDF icon with its pages and size, an
@paper.pdfmention attaches it, and the sent prompt shows its name. - DSK-94c Attachments follow the active agent’s model (
GetSettingsaccepted_media; spec_images_011 IMG-63): the file picker offers its types and extensions only, and paste and drop refuse other files with “can’t take ”; a file over the model’s limit, or over 30 MB (a service request), says so, and a large one that the model takes from the workspace says to put it there and attach it from there. Chips have an icon per kind (image, PDF, text, audio, video) and show a length for audio and video; @mentions attach audio and video too. Video opens in the editor as a player (previewKindvideo: MP4, QuickTime and WebM, which the window plays; AVI, WMV, FLV, MPEG and 3GPP say they play in the system’s player), and Open in the system viewer takes sound and video.
- DSK-95 Built by Bazel (spec_monorepo_028): the page is
//apps/desktop/web:page; since v0.7.0 the app and its packages are the Electron app’s (//:desktop,//:mac-app,//:deb,//:pacman,//:desktop-tar,//:windows-zip; EL-05, EL-80). Until v0.6.5//apps/desktop:blitz-desktopand//apps/desktop/packaging:Blitz.app(universal) or:debbuilt the Wails app. - DSK-96
bazel test //apps/desktop/...: the page’s vitest (:test) and TypeScript check (:typecheck); the host’s Go tests arepkg/pagehost’s andpkg/pageserver’s. CI runs them with everything else. - DSK-97 Tests: the proxy against a real service (
apps/service/servicetest) (API calls, a streamed turn and its approval, theunavailableerror); the preferences file (defaults, normalization, owner-only atomic writes, a damaged file set aside) andOpenURL’s scheme check; the page’s pure logic (turn events, message kinds, prompt indices, task lists, diff parsing, the workspace list rules, theme resolution, colours, the language chain and plurals) and the two text checks (DSK-78g). - DSK-98 Development:
bazel run //:desktopruns the Electron app on this machine’s service (EL-05).bazel run //:desktop-web(//apps/desktop/web:dev) serves the page (Vite, with the generated API code) and proxies/blitz.v1.*to the service socket ($BLITZ_SOCKETor~/.blitz/run/blitz.sock).?fakein a development build uses an in-page fake service (dev/fake.ts, left out of production builds) whose turns script every state — streamed Markdown, thinking, tool calls, a task list, an approval, a plan review — for working on the page and taking screenshots without a service or a model. - DSK-99 Dependencies are pinned: React 19, Connect-ES 2, protobuf-es 2 (
protoc-gen-esgeneratessrc/genat build time,//apps/desktop/web:api_ts), react-markdown 10 with remark-gfm 4, Mermaid 11 (a chunk loaded the first time a diagram shows; FIL-55), lowlight 3 with hast-util-to-jsx-runtime 2,@mdi/js7, Vite 8, TypeScript 5.9, vitest 5; pnpm 10.24 viapackageManager. CodeMirror 6 (@codemirror/*6,@lezer/highlight1) for the editor. The page is a main bundle (about 1.4 MB, 440 kB compressed, of which the three catalogs are about 150 kB and CodeMirror about 500 kB) and a chunk per editor language, loaded when a file in it is first opened, all from the app.
Not implemented; each is tracked in spec_backlog_026:
- Sessions from other workspaces, and opening a snapshot by name (the History menu lists this workspace’s).
- Localisation: the window doesn’t follow the terminal’s
/locale([ui] locale) or load catalogs from~/.blitz/locales; it has its own language setting and the shipped catalogs (BL-DSK-41).