Skip to main content
Blitz
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage
Edit page

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)

1. Purpose and scope

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.

2. Identity and packaging

  • DSK-01 Product name Blitz; macOS bundle Blitz.app, bundle identifier dev.blitz.desktop, executable blitz-desktop on Linux; version stamped into release builds from the git tag (//build:version, spec_monorepo_028 MR-20), dev otherwise; 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 as vN.N.N (shownVersion, serviceVersion.ts), as the tags, releases and docs write it; dev and a commit stay as they are. The stamped version has no v (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, and LSMinimumSystemVersion in Info.plist/Info.dev.plist says the same.
  • DSK-04 The CLI and the service (blitz, blitzd) are packaged with the app (since v0.7.0 in the app’s bin/ with blitz-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 install finds blitzd beside 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.zst for 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.

3. Architecture

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/pagehost in blitz-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 uses pkg/socket (the socket’s path, BaseURL and HTTP client), pkg/loginitem, pkg/config and pkg/legal; its tests run a real service through apps/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 are no-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 Go App value bound to the page as window.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=off or the gpu preference (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.WebviewGpuPolicy on 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 (or on) in the environment overrides the setting, for a driver that keeps the app from starting at all. On NVIDIA (/dev/nvidiactl present) WebKitGTK leaves its DMA-BUF renderer off and draws in software whatever the policy says, so with the GPU on Blitz sets WEBKIT_FORCE_DMABUF_RENDERER=1 and WEBKIT_DMABUF_RENDERER_FORCE_SHM=1 (each unless already set; neither when WEBKIT_DISABLE_DMABUF_RENDERER is 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’s webprocess package): 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_check checks on a machine with a GPU that WebKit draws with it (manual verification §37). Drawing in software sets WEBKIT_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 is blitz-desktop (GTK’s program name, the launcher’s StartupWMClass), 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, setMenu in apps/electron/src/main.ts, EL-13; until then options.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 page menu:about and menu: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 /help share). Every command the REPL’s /help lists 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 (and update), /tools, /config (or /set; no arguments opens Settings › Settings file, --save writes the user’s file), /locale (the reply language) and /theme. /clear, /exit and /quit, /paste, /cd and !<command> stay the terminal’s; /help names them with the window’s way instead. /help lists the custom commands by source after the built-ins. slashParity.test.ts reads the REPL’s helpGroups and 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 by src/docs/content.ts; the specifications, development and architecture pages are left to the website). src/docs/index.ts reads them as the site does (front matter title and weight for the order; relative .md links, _index.md the 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 topic open 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.ts checks every carried page and link; docsCommands.test.ts that every command products/cli.md documents 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), or vX.Y.Z in 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’s page/ (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.

4. The service bridge (proxy)

  • 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_SOCKET or ~/.blitz/run/blitz.sock, resolved at start), rewriting the URL to the service’s BaseURL. The page’s Connect client uses window.location.origin as 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+json or +proto) goes through keepalive: 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 503 with 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 by blitz-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 through OpenURL (web and mail schemes only), and its images are never fetched from the web (a picture written into it as a data: 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.

5. The page’s calls to its window and host

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 in localStorage, 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”.

6. Design system

  • 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-a to --accent-b, --accent-grad (light: a brighter blue than the primary, #1a73e8, to teal #00838f; dark: the primary #a8c7fa 45% 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 the outline border; 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 as html[data-theme]; density (Comfortable or Compact) as html[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; transparency in 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-border rgb(110 122 145 / .28), --md-outline-variant at 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’s vibrancy, 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. applyTheme calls SetAppearance with 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’s prefers-color-scheme reports 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 under data-native-glass the body is the backdrop’s glows, translucent, over a base in the theme (--backdrop-native; light rgb(227 232 241 / .68), dark rgb(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 in on-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_theme and dark_theme in the preferences (blitz by default; anything else, or a dark theme for light, is read as blitz). The page sets html[data-palette] to the one in effect (paletteOf, themes.ts); themes.css gives 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 from third_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.tsx wide): 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 /agent lists them: each with its description and pinned model, the one working on the conversation marked; choosing one is SetAgent, 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; bypass in 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.

7. Window settings and workspaces

  • 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 (system or 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), and workspaces: 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); active must 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/pagehost PrefsStore) always sends workspaces as a list ([], never null), 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.

8. Start-up, workspaces and closing

  • 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 run blitz 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, with ProgramExists, 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, all dev), 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’s blitzd as 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 (launchd bootout, systemd stop), else — or if it still answers — the process GetServiceInfo reported (SIGTERM, 15 s). The app manages the service itself through pkg/loginitem, with no CLI: it finds blitzd beside its executable (Blitz.app, the .deb), in its Bazel runfiles (bazel run), or on PATH; without one the buttons are off and the window says to reinstall Blitz. Rules in serviceVersion.ts, unit-tested; stopping a real blitzd by 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 (a TURN_RUNNING refusal — 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.

9. Settings

  • 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 set MaxWidth/MaxHeight 16384, 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 (Segmented equal), 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 with configChanged, 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 the blitz-tray beside the app with --install or --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 with SANDBOX_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 set auto (SandboxNotice.tsx, sharing useSandboxFix with the row); Command line (the blitz beside the app and whether a new terminal finds it: CLIStatus asks the login shell, $SHELL -i -l -c, for its PATH, since the app’s own isn’t the terminal’s; when no blitz is on it, Install (InstallCLI) links the app’s into the first of ~/.local/bin, ~/bin or a writable /usr/local/bin on that PATH, else into ~/.local/bin, adding it to PATH in the shell’s startup file: .zshrc (under $ZDOTDIR if set), .bash_profile on macOS and .bashrc on Linux, fish’s conf.d/blitz.fish, else .profile; it replaces a stale link there but never a file; when a blitz is 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 dead blitz links in PATH’s folders, points the blitz link 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; a blitz that’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 (canDelete in logs.ts: today’s is being written; a dialog confirms, then DeleteLogDay, 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” when log.level is 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 (advanced in the preferences, false by 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 in simpleMode.ts, unit-tested; the smaller where they’re drawn, with useAdvanced):

    • 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.

  • 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: an ant profile 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: adc and Claude on Vertex Google, oauth Anthropic, 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, from GetSignIn; Sign in runs SignIn, 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 as key = and going on to the values; after =, true and false for 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; findSetting matches quoted names to <model>-style keys and [[array]] tables to name[]), 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.

10. Workspace view

  • 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, a ResizeObserver). 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, then DeleteWorker) (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 whether Field’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 (AgentEditor with drawer, 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 GetSuggestions and 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 “”</strong> (opens the latest conversation); <strong>Review my N uncommitted changes and propose a commit message</strong>; <strong>Find out why the worker X failed</strong> (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 (<code>pending</code>) 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 (<strong>Set up the agent harness</strong>, beside Go to file) and a command palette entry; each starts a new chat running <code>/setup</code> (<code>startSetup</code>, a fresh <code>compose</code>).</p> </li> <li> <p><strong>DSK-66e</strong> <em>2026-10-01.</em> Tool calls in the conversation (<code>groupTools</code>, <code>latestTurn</code> in <code>turns.ts</code>, 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.</p> </li> <li> <p><strong>DSK-66f</strong> <em>2026-10-18 (a live pass).</em> A reopened chat shows what a live one did: its tool calls, each with its result, back after the prompt they ran under (<code>SessionService.ListSessionTools</code>, read from the session’s event log, long strings cut at 4 KiB; <code>withTools</code> in <code>turns.ts</code> places 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 (<code>splitAttachments</code>: the transcript’s <code>[images: …]</code> line). Notices (version control, warnings) aren’t in the log and stay live-only.</p> </li> <li> <p><strong>DSK-66b</strong> <em>2026-10-01.</em> Model fields (<code>ModelInput.tsx</code>, rules in <code>models.ts</code>, 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 (<code>ListModels</code>, <a class="gdoc-markdown__link" href="/blitz/about/specs/spec_service_021/" >spec_service_021</a> 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’ <strong>Save</strong> 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 (<code>ui/inputs.ts</code>, on focus, unless marked <code>data-prose</code>); multi-line fields such as the composer keep the system’s behaviour.</p> </li> <li> <p><strong>DSK-66c</strong> <em>2026-10-01; the default since 2026-10-02.</em> <strong>Settings › Appearance › Chat input</strong> (advanced, DSK-60a): <strong>Pinned to the bottom</strong> (the default) or <strong>With the conversation</strong> (<code>composer</code> in the window’s preferences: <code>pinned</code> unless it’s <code>panel</code>; a <code>desktop.json</code> that already says <code>panel</code>, 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 (<code>composerDock.ts</code>: a React portal, so the conversation still owns drafts, steering, <code>@</code> 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 (<em>since 2026-10-04</em>: 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 <strong>New chat</strong> 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.</p> </li> <li> <p><strong>DSK-66c-glass</strong> <em>2026-10-01.</em> 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 (<code>glass.css</code>); 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.</p> </li> <li> <p><strong>DSK-66d</strong> <em>2026-10-01.</em> 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 (<code>KeyHint</code>, a CSS tooltip, so not the native title’s delay).</p> </li> </ul> <div class="flex align-center gdoc-page__anchorwrap"> <h2 id="11-conversation" > 11. Conversation </h2> <a data-clipboard-text="https://retail-cortex.github.io/blitz/about/specs/spec_desktop_024/#11-conversation" class="gdoc-page__anchor clip flex align-center" title="Anchor to: 11. Conversation" aria-label="Anchor to: 11. Conversation" href="#11-conversation"> <svg class="gdoc-icon gdoc_link"><use xlink:href="#gdoc_link"></use></svg> </a> </div> <p>API used: <code>SessionService.GetActiveSession</code>, <code>NewSession</code>, <code>ListSessions</code>, <code>LoadSession</code>, <code>RenameSession</code>, <code>RunTurn</code>, <code>Steer</code>, <code>Approve</code>, <code>Answer</code>, <code>GetUsage</code>, <code>Rewind</code>; <code>WorkspaceService.SetPermissionMode</code>, <code>SetSetting</code>.</p> <ul> <li><strong>DSK-70</strong> 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), <strong>History</strong> (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 <code>DeleteSession</code>, SES-23, and a snackbar says so. <em>2026-10-04 (the owner’s request):</em> the menu (<code>HistoryMenu.tsx</code>, rules in <code>history.ts</code>, 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; <strong>All</strong>, <strong>Chats</strong> and <strong>Workers</strong> (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 <strong>Select all shown</strong> ticks or clears the rows shown; with any ticked, a bar at the bottom says how many and offers <strong>Clear</strong> and <strong>Delete n</strong>, 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 is <code>SESSION_OPEN</code>) and <strong>New chat</strong>; both are disabled while a turn runs.</li> <li><strong>DSK-71</strong> 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 <code>kind</code>). Model text renders as Markdown (§11a). A sub-agent’s text is labelled with its name.</li> <li><strong>DSK-72</strong> Hovering a prompt shows <strong>Copy</strong>, <strong>Edit</strong> (rewind code and conversation to before it and put the prompt back in the composer) and a rewind menu: <strong>Code and conversation</strong>, <strong>Conversation only</strong>, <strong>Code only</strong>, <strong>Summarize from here</strong>, <strong>Summarize up to here</strong>, <strong>Copy into the composer</strong>. 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; <code>assignPromptIndices</code>). A conflict (<code>UNDO_CONFLICT</code>) asks “Files changed since” — <strong>Keep them</strong> or <strong>Overwrite them</strong> (force). Rewinds are unavailable while a turn runs.</li> <li><strong>DSK-72a</strong> Hovering a finished answer (or focusing its actions) shows <strong>Copy</strong> (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), <strong>Copy as Markdown</strong> (its source) and, when the turn’s answer came in parts (between tool calls or steering), <strong>Copy the whole answer</strong> (the parts’ Markdown, in order; <code>turnAnswers</code>). Each says so in a snackbar, or why it couldn’t copy.</li> <li><strong>DSK-73</strong> Turn rendering (<code>turns.ts</code>, 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 <strong>Show thinking</strong>, 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); <code>finished</code> closes open text and adds an error notice if the turn failed. A <code>notice</code> event is a notice entry (error or information), closing the answer before it.</li> <li><strong>DSK-74</strong> 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 <code>path</code>, <code>command</code>, <code>url</code>, <code>query</code>, <code>question</code>, <code>pattern</code>, <code>agent</code>, <code>reason</code>, 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 <strong>Used N tools</strong> (with their icons and the number that failed), open while one runs or one failed, folded otherwise.</li> <li><strong>DSK-75</strong> “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.</li> <li><strong>DSK-76</strong> Task list (<code>TurnEvent.tasks</code>): a card above the composer, <strong>Tasks</strong> 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.</li> <li><strong>DSK-77</strong> 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 <code>/plan</code> (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; <strong>Send</strong> (disabled when empty), and while a turn runs <strong>Stop</strong> — the text then <strong>steers</strong> the agent (<code>Steer</code>), shown as a dashed bubble. An empty session shows “What are we working on in <workspace>?” with four suggestions that fill the composer.</li> <li><strong>DSK-77b</strong> <em>2026-10-02 (the owner’s request).</em> 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 (<code>.gradient-text</code>: the theme’s primary to its tertiary; dark light blue <code>#a8c7fa</code> to green <code>#6dd58c</code>, light deep blue <code>#0b57d0</code> to deep green <code>#146c2e</code>; <code>--ring-a</code>, <code>--ring-b</code> and their oklab mix; <em>since 2026-10-04</em> <code>--ring-a</code> and the mix are the accent’s <code>--accent-a</code> and <code>--accent-b</code>, DSK-35, so the ring and the buttons can’t drift apart), blue to green and back around it (a conic gradient on <code>::after</code>, masked to the border’s width, <code>app.css</code>). It turns: its angle is a registered <code>@property</code>, 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 <code>@property</code> isn’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.</li> <li><strong>DSK-77a</strong> @ mentions: typing <code>@</code> in the composer (at the start or after a space; <code>@"</code> for paths with spaces) lists the workspace’s files and folders matching what follows (<code>FindFiles</code> with <code>folders</code>, 12 at most, as typing pauses), with their icons; ↑/↓ choose, Tab or Enter puts the path in place of what was typed (<code>@path </code>, 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 (<code>LoadImage</code>). The service adds what they name to the prompt (WS-21a). Pure rules in <code>mentions.ts</code>, unit-tested.</li> <li><strong>DSK-78</strong> After a turn: “This turn: <in> in · <out> out · context <ctx> · $<turn>” plus the session total (“session <tokens> tokens · $<total>”, from <code>GetUsage</code> and each turn’s <code>after</code>). <strong>Stop</strong> 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; <code>unavailable</code> errors also start reconnecting (DSK-51).</li> </ul> <div class="flex align-center gdoc-page__anchorwrap"> <h3 id="11a-markdown" > 11a. Markdown </h3> <a data-clipboard-text="https://retail-cortex.github.io/blitz/about/specs/spec_desktop_024/#11a-markdown" class="gdoc-page__anchor clip flex align-center" title="Anchor to: 11a. Markdown" aria-label="Anchor to: 11a. Markdown" href="#11a-markdown"> <svg class="gdoc-icon gdoc_link"><use xlink:href="#gdoc_link"></use></svg> </a> </div> <ul> <li><strong>DSK-79</strong> Model text, questions and summaries render with <code>react-markdown</code> and GitHub-flavoured Markdown (tables, task lists, strikethrough) to React elements; raw HTML is skipped, never rendered. Links show their target on hover and open with <code>OpenURL</code> after a click (never in the window). Images are never loaded: they show as links. Fenced code blocks show their language and a <strong>Copy</strong> button and are syntax-highlighted (§11b); inline code is tinted; wide tables scroll.</li> </ul> <div class="flex align-center gdoc-page__anchorwrap"> <h3 id="11b-syntax-highlighting" > 11b. Syntax highlighting </h3> <a data-clipboard-text="https://retail-cortex.github.io/blitz/about/specs/spec_desktop_024/#11b-syntax-highlighting" class="gdoc-page__anchor clip flex align-center" title="Anchor to: 11b. Syntax highlighting" aria-label="Anchor to: 11b. Syntax highlighting" href="#11b-syntax-highlighting"> <svg class="gdoc-icon gdoc_link"><use xlink:href="#gdoc_link"></use></svg> </a> </div> <ul> <li><strong>DSK-79a</strong> Code blocks and diff lines are highlighted with highlight.js grammars through <code>lowlight</code> (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: <code>sh</code>→bash, <code>ts</code>→typescript, <code>yml</code>→yaml, <code>golang</code>→go, <code>proto</code>→protobuf, …); a diff’s from the file name (extension, or <code>Makefile</code>). Unknown languages, and code over 100 000 characters, stay plain text. Diff lines are highlighted one at a time after their <code>+</code>/<code>−</code>/space sign (shown in its own column), over the added/removed tint. Token colours are theme tokens (<code>--hl-*</code>) with a light and a dark set.</li> </ul> <div class="flex align-center gdoc-page__anchorwrap"> <h3 id="11e-slash-commands-and-the-command-palette" > 11e. Slash commands and the command palette </h3> <a data-clipboard-text="https://retail-cortex.github.io/blitz/about/specs/spec_desktop_024/#11e-slash-commands-and-the-command-palette" class="gdoc-page__anchor clip flex align-center" title="Anchor to: 11e. Slash commands and the command palette" aria-label="Anchor to: 11e. Slash commands and the command palette" href="#11e-slash-commands-and-the-command-palette"> <svg class="gdoc-icon gdoc_link"><use xlink:href="#gdoc_link"></use></svg> </a> </div> <ul> <li><strong>DSK-78c</strong> A composer line that is a command (<code>/name args</code>; names are letters, digits, <code>-</code>, <code>_</code>, <code>:</code>, <code>.</code> — <code>/usr/bin …</code> is text) runs in the window instead of going to the agent. Built-in commands, through the same API calls as the REPL: <code>/plan <goal></code> (a plan turn), <code>/btw <question></code> (an aside turn, shown as “Side question: not kept”), <code>/search web <terms></code> (<code>SearchWeb</code>; the links show as a notice, then a read-only turn reads them with fetch grants) and <code>/search session <terms></code> (<code>SearchSession</code>, then a read-only turn), <code>/undo [--force]</code>, <code>/checkpoints</code>, <code>/diff</code> (opens Changes), <code>/cost</code> and <code>/context</code> (<code>GetUsage</code>), <code>/compact [focus]</code>, <code>/agent [name]</code>, <code>/model [ref]</code>, <code>/mode [name]</code>, <code>/effort [level]</code>, <code>/session save <name> [--force]</code> (<code>SaveSnapshot</code>), <code>/rename <title></code>, <code>/new</code>, <code>/help</code> (every command, the workspace’s too). The workspace’s custom commands and skills (<code>ListCommands</code>; a built-in name wins) run as command turns (<code>Turn.command</code>). 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.</li> <li><strong>DSK-78d</strong> Typing <code>/name</code> (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).</li> <li><strong>DSK-78e</strong> <strong>Cmd/Ctrl+K</strong>, <strong>Cmd/Ctrl+P</strong> or the top bar’s search bar opens the command palette: once something is typed that isn’t a command (<code>/…</code>), the active workspace’s files first (<code>FindFiles</code>, up to 8, as it ranks them; <code>name:12</code> opens at line 12, <code>paletteResults</code> and <code>searchesFiles</code> in <code>commands.ts</code>), 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, <strong>Workspace settings</strong> (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, <code>/help</code> and filtering are pure (<code>commands.ts</code>), unit-tested.</li> </ul> <div class="flex align-center gdoc-page__anchorwrap"> <h3 id="11d-images" > 11d. Images </h3> <a data-clipboard-text="https://retail-cortex.github.io/blitz/about/specs/spec_desktop_024/#11d-images" class="gdoc-page__anchor clip flex align-center" title="Anchor to: 11d. Images" aria-label="Anchor to: 11d. Images" href="#11d-images"> <svg class="gdoc-icon gdoc_link"><use xlink:href="#gdoc_link"></use></svg> </a> </div> <ul> <li><strong>DSK-78b</strong> 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 (<code>AddImage</code>) 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. <strong>Send</strong> waits until uploads finish; the turn carries the uploaded images’ IDs (<code>Turn.image_ids</code>), and the prompt’s bubble shows them. With images turned off for the workspace (<code>imagesEnabled</code> false) 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 in <code>attachments.ts</code>, unit-tested.</li> </ul> <div class="flex align-center gdoc-page__anchorwrap"> <h3 id="11f-localisation" > 11f. Localisation </h3> <a data-clipboard-text="https://retail-cortex.github.io/blitz/about/specs/spec_desktop_024/#11f-localisation" class="gdoc-page__anchor clip flex align-center" title="Anchor to: 11f. Localisation" aria-label="Anchor to: 11f. Localisation" href="#11f-localisation"> <svg class="gdoc-icon gdoc_link"><use xlink:href="#gdoc_link"></use></svg> </a> </div> <ul> <li><strong>DSK-78f</strong> Every string the page shows comes from the terminal’s catalogs (<code>pkg/i18n/locales</code>, embedded in the page at build time) under keys starting with <code>desktop.</code>, through <code>t(key, params)</code> and <code>tn(key, count)</code> (<code>i18n.ts</code>): placeholders <code>{name}</code>, plurals <code>.one</code>/<code>.other</code> by the language’s CLDR rules (<code>Intl.PluralRules</code>), and the terminal’s lookup chain (<code>fr-CA</code> → <code>fr</code> → <code>en-US</code>; a bare <code>fr</code> borrows <code>fr-CA</code>; a missing key shows as the key). The language is the window’s own setting (DSK-40), <code>system</code> by 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-locale <code>en-XA</code> (set <code>language</code> in <code>desktop.json</code>) 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.</li> <li><strong>DSK-78g</strong> 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 (<code>title</code>, <code>placeholder</code>, <code>aria-label</code>, <code>label</code>, <code>alt</code>, …), <code>snack()</code>/<code>setError()</code>/<code>notify()</code>-style calls and menu fields (<code>label</code>, <code>detail</code>, <code>heading</code>, …) — with file and line; the other fails on a <code>t()</code>/<code>tn()</code> key missing from <code>en-US.json</code>. The Go catalog checks (I18N-05) then require every key, with its placeholders, in every shipped catalog.</li> </ul> <div class="flex align-center gdoc-page__anchorwrap"> <h3 id="11c-notifications" > 11c. Notifications </h3> <a data-clipboard-text="https://retail-cortex.github.io/blitz/about/specs/spec_desktop_024/#11c-notifications" class="gdoc-page__anchor clip flex align-center" title="Anchor to: 11c. Notifications" aria-label="Anchor to: 11c. Notifications" href="#11c-notifications"> <svg class="gdoc-icon gdoc_link"><use xlink:href="#gdoc_link"></use></svg> </a> </div> <ul> <li><strong>DSK-78a</strong> With <strong>Notifications</strong> on (the default; Settings › Appearance; <code>notifications</code> in <code>desktop.json</code>), the window notifies when an approval or a question waits ("<workspace>: approval needed" with the request, “<workspace>: the agent asks” with the question), or when a turn that took at least 10 s finishes ("<workspace>: 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 (<code>shouldNotify</code>, unit-tested). Bodies are cut to 180 characters. In a browser (development) the web’s Notification API stands in.</li> </ul> <div class="flex align-center gdoc-page__anchorwrap"> <h2 id="12-approvals-questions-and-plans" > 12. Approvals, questions and plans </h2> <a data-clipboard-text="https://retail-cortex.github.io/blitz/about/specs/spec_desktop_024/#12-approvals-questions-and-plans" class="gdoc-page__anchor clip flex align-center" title="Anchor to: 12. Approvals, questions and plans" aria-label="Anchor to: 12. Approvals, questions and plans" href="#12-approvals-questions-and-plans"> <svg class="gdoc-icon gdoc_link"><use xlink:href="#gdoc_link"></use></svg> </a> </div> <ul> <li><strong>DSK-80</strong> An approval request shows as a card: “Allow this?”, <code><tool></code> wants to: <detail>, the diff coloured per file (§14), and <strong>Allow once</strong> (focused, filled), <strong>Allow <scope> this session</strong>, <strong>Always allow <scope></strong> (when the request has a scope), <strong>Deny</strong> (outlined, red). <em>2026-10-15 (the owner’s report: a turn hung after its first answer):</em> requests and questions that arrive together (the parallel tool calls of one model response, each waiting for its own answer) are queued in order (<code>waits.ts</code>) 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.</li> <li><strong>DSK-80c</strong> Failed worker runs (BL-WK-11): a worker whose latest run (<code>Worker.last_run</code>, which <code>ListWorkers</code> now carries) failed or hit a limit after its workspace’s Workers view was last shown (<code>workers_seen</code> in the workspace’s preferences, set whenever the view shows) counts: the workspace’s row in the <strong>Workspaces</strong> 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 (<code>ListWorkers</code> for each open workspace, every minute, and at once when the open workspaces or their seen times change) serves them all (<code>workerFailures.ts</code>).</li> <li><strong>DSK-78h</strong> The window’s <strong>System</strong> language follows the terminal’s: <code>ConfigService.GetInterfaceLanguage</code> gives the <code>[ui] locale</code> the user’s settings set (empty when they don’t) and their catalogs from <code>[ui] locales_dir</code> (checked as the terminal loads them: <code>i18n.CheckCatalog</code>; 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 (<code>applyUserLanguage</code>, when the service is up). An older service: the system’s language, as before (BL-DSK-41).</li> <li><strong>DSK-80d</strong> The Workers view asks for the workspace’s workers every 5 s while it’s open, so a <code>WORKER.md</code> added or edited elsewhere shows up. Each run shows the files it changed (“Changed: …”) and <strong>Open its conversation</strong>, which loads the run’s session into the chat (BL-DSK-50).</li> <li><strong>DSK-80b</strong> The inbox of background runs (<a class="gdoc-markdown__link" href="/blitz/about/specs/spec_background_agents_032/" >spec_background_agents_032</a> 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 <strong>Open</strong> and, while one goes on, <strong>Stop</strong>. <strong>Open</strong> opens its workspace (as a tab) and loads its session, once the workspace is there if it’s opening (<code>loadSession</code>, <code>takePendingLoad</code>). A chat whose session a run is working in follows the run as a turn from its start (<code>WatchBackground</code>): its approval requests and questions show as the turn’s cards, and <strong>Stop</strong> stops the run. One poll (<code>ListBackground</code>, every 4 s while an inbox button is shown) serves every button.</li> <li><strong>DSK-80a</strong> Background tasks (<a class="gdoc-markdown__link" href="/blitz/about/specs/spec_background_agents_032/" >spec_background_agents_032</a> BGA-33, BGA-41): a background <code>invoke_agent</code> shows a card under its tool group (agent, task, state, runtime, cost, the prompt; <strong>Show</strong> for its latest events and result, <strong>Stop</strong> while it runs), refreshed every 2 s until it ends. While a chat is open its tasks are followed (<code>WatchTasks</code>, 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; <strong>Ctrl+J</strong> moves to the next. A task that ends adds a notice; with Settings › <strong>Continue after background tasks</strong> (<code>task_continue</code>, off by default) it also starts a turn about its result when nothing runs. Reopened chats show ended tasks as notices (the transcript’s <code>task</code> messages).</li> <li><strong>DSK-81</strong> 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; <em>since v0.6.3</em> 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 (<code>exit_plan_mode</code>) is such a question: the plan renders as Markdown and typed feedback makes the agent revise it.</li> <li><strong>DSK-82</strong> One request shows at a time (the latest); it disappears once answered or when the turn ends. Late answers fail with the service’s <code>UNKNOWN_REQUEST</code>.</li> </ul> <div class="flex align-center gdoc-page__anchorwrap"> <h2 id="13-workspace-settings" > 13. Workspace settings </h2> <a data-clipboard-text="https://retail-cortex.github.io/blitz/about/specs/spec_desktop_024/#13-workspace-settings" class="gdoc-page__anchor clip flex align-center" title="Anchor to: 13. Workspace settings" aria-label="Anchor to: 13. Workspace settings" href="#13-workspace-settings"> <svg class="gdoc-icon gdoc_link"><use xlink:href="#gdoc_link"></use></svg> </a> </div> <p>API used: <code>ConfigService</code> (DSK-61); <code>WorkspaceService.ListAgents</code>, <code>SetAgent</code>, <code>SetModel</code>, <code>GetModelSettings</code>, <code>UpdateModelSettings</code>, <code>SetSetting</code>, <code>SetPermissionMode</code>, <code>ListLocales</code>, <code>SetLocale</code>, <code>ListPermissionRules</code>, <code>AddPermissionRule</code>, <code>RemovePermissionRule</code>, <code>ListApprovals</code>, <code>RevokeApprovals</code>; <code>SessionService.GetUsage</code>, <code>Compact</code>.</p> <ul> <li><strong>DSK-85</strong> <em>Until 2026-10-02 a panel; now the form in Settings › Workspaces (DSK-85b).</em> 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 <code>run_settings_width</code>; 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 <strong>Advanced settings</strong> 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 (<strong>This workspace</strong>: API keys, Permission rules; <strong>This model</strong>: Generation; <strong>This session</strong>: 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): <strong>Agent and model</strong> — agent (with pinned models), model (a field with suggestions: pinned models and models with settings; applied on Enter or leaving the field); <strong>Thinking</strong> — reasoning effort for the session, thinking budget for the model; <strong>Generation · <model></strong> — temperature and top P (slider with a number, “default” when unset), max output tokens (placeholder: the global value), <strong>Reset to defaults</strong> (settings the provider ignores are reported); <strong>Behaviour</strong> — permission mode (with its meaning), agency, the output style (with its description; <code>ListStyles</code>, <code>SetSetting style</code>), and the language the model replies in; <strong>Permission rules</strong> — the workspace’s own rules (<code>PermissionSettings.tsx</code>, as in Settings, <strong>Permissions</strong>, for the global ones: <code>ConfigService</code> permissions calls), each with its effect (coloured) and remove; <em>2026-10-03:</em> rules left out when the settings last changed (<code>ListPermissionRules.problem</code>) 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 <strong>Try it on</strong> a sample — checked as it’s typed (<code>CheckPermission</code>: the canonical rule, what it matches in words, whether it applies to the sample, and that a redirection still asks), <strong>Add</strong> 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 <code>/permissions</code>, removable); the section’s count leaves out the built-in rules; <strong>Standing approvals</strong> — each with its kind, subject and scope, revoke one or all; <strong>API keys</strong> (collapsed; DSK-61, for this workspace); <strong>Context</strong> — context size, tokens sent and received, cost, and <strong>Compact the context</strong>. Every change reports failure in a snackbar and reloads the panel.</li> <li><strong>DSK-85a</strong> <em>2026-10-02 (the owner’s request).</em> One form for a workspace’s settings (<code>WorkspaceSettingsForm</code>, in the panel and in Settings › Workspaces): the panel is <strong>Workspace settings</strong> (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. <strong>Workspace</strong> (first): name, description, colour and folder (copy), the fields of the details dialog (<code>WorkspaceFields</code>), 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. <strong>Project settings</strong> (<em>Project</em>): the trust state (“3 wait for your trust” folded), the files, and <strong>Review</strong> (the project settings dialog, PRJ-33). <strong>Settings file</strong> (<em>This workspace</em>): what it’s for, its path (<code>GetConfigFile</code>), and <strong>Edit the settings file</strong>, which opens Settings › Settings file on this workspace’s (<code>SettingsDrawer</code> <code>scope</code>). Settings › This workspace (DSK-65a) is gone: everything in it is here.</li> <li><strong>DSK-85b</strong> <em>2026-10-02 (the owner’s request).</em> No panel: a workspace’s settings are only Settings › Workspaces (DSK-60), the same form (<code>WorkspaceSettingsForm.tsx</code>, every card of DSK-85 and DSK-85a). Its pencil in the Workspaces menu, <strong>Workspace settings…</strong> at the foot of the status bar’s agent menu (DSK-39), and the palette’s <strong>Workspace settings</strong> open the dialog there, on that workspace (<code>SettingsDrawer</code> <code>initial="workspaces"</code>, <code>scope</code>), without switching the window to it. Gone with the panel: its width (<code>run_settings_width</code>) and shown state (<code>run_settings</code>) in <code>desktop.json</code> (old values are ignored), Appearance’s <strong>Run settings panel</strong> switch, the palette’s show and hide, and its floating over the chat under 1280 px; the editor keeps its room beside the chat.</li> <li><strong>DSK-86</strong> Panel widths follow the window (<code>useWindowWidth</code>: 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 (<code>panelWidth</code>), 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 (<code>Menu</code>, <code>ContextMenu</code>, and the workspace switcher’s dropdown, through <code>useAnchoredMenu</code> and <code>FloatingLayer</code>: 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 (<code>menuPosition</code>, <code>nudge</code>); 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 (<code>useModal</code>).</li> </ul> <div class="flex align-center gdoc-page__anchorwrap"> <h2 id="14-changes-and-workers" > 14. Changes and workers </h2> <a data-clipboard-text="https://retail-cortex.github.io/blitz/about/specs/spec_desktop_024/#14-changes-and-workers" class="gdoc-page__anchor clip flex align-center" title="Anchor to: 14. Changes and workers" aria-label="Anchor to: 14. Changes and workers" href="#14-changes-and-workers"> <svg class="gdoc-icon gdoc_link"><use xlink:href="#gdoc_link"></use></svg> </a> </div> <ul> <li><strong>DSK-90</strong> Changes view: <strong>This session</strong> (the agent’s changes, <code>GetDiff</code>) or <strong>Version control</strong> (<code>GetDiff git</code>: every change since the last commit, staged or not, and new untracked files, <em>2026-10-04</em>; outside a repository, which <code>GetGitStatus</code> tells, instead “This folder isn’t under version control” and <strong>Turn on version control</strong>, <code>InitGitRepo</code>, which runs <code>git init</code> there; 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; <strong>Refresh</strong>; <strong>Undo last turn</strong> (a conflict asks <strong>Keep them</strong> / <strong>Undo anyway</strong>). A side column lists the files (name, path, +/−) and the turns that changed files (a timeline from <code>ListCheckpoints</code>); 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 (<code>parseDiff</code>, unit-tested: file headers, git’s <code>diff --git</code> headers so binary, renamed and mode-only files still list, a <code>--stat</code> summary skipped, <code>/dev/null</code> for created and deleted files, hunks, “too large” notes) and coloured: added and removed lines on tinted backgrounds, hunk headers in the secondary colour.</li> </ul> <p><strong>DSK-90a</strong> <em>2026-10-04 (the owner’s request).</em> Commit dialog (<code>CommitDialog.tsx</code>, rules in <code>commit.ts</code>, unit-tested), from <strong>Changes › Commit changes…</strong> in the menu bar, <strong>Commit changes…</strong> in the Files tree’s Version control menu, and <strong>Commit changes…</strong> in the Changes view’s toolbar on the Version control source. It asks <code>DraftCommit</code> 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). <strong>Redraft</strong> asks again. With nothing staged it says so and offers <strong>Include all changes</strong> (<code>DraftCommit</code> with <code>stage_all</code>: <code>git add --all</code>, 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 <code>ModelErrorSummary</code> gives it: never the client’s settings); the user writes the message. <strong>Commit n files</strong> (⌘Enter / Ctrl+Enter in the field) is <code>Commit</code>, enabled with something staged, a message, and no call under way; afterwards a snackbar says “Committed <hash>: <subject>”, the tree and the Changes view refresh, and the dialog closes; a failure (a hook’s, say) stays in the dialog.</p> <p><strong>DSK-90b</strong> <em>Since v0.6.3 (the owner’s request).</em> The workspace’s remote, on the Changes view’s Version control source (<code>RemoteSync.tsx</code>, wording in <code>gitRemote.ts</code>, unit-tested). With a remote, the toolbar has <strong>Pull</strong> and <strong>Push</strong>, each with a count badge: the commits to pull and to push as of the last fetch (<code>GetGitStatus</code> <code>ahead</code>, <code>behind</code>); their tooltip says it (“origin/main: 2 commits to pull, 1 commit to push”). The view fetches once as it opens (<code>FetchRemote</code>; quietly: offline or not signed in, the counts stay as they were). <strong>Pull</strong> (<code>Pull</code>) follows the repository’s git config (<code>pull.rebase</code>, <code>pull.ff</code>; a merge when neither is set) and keeps uncommitted changes (<code>--autostash</code>); 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 <strong>Publish branch</strong> instead of Push (<code>Push</code> 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, <strong>Ask the agent to resolve</strong> (the chat, with a prompt naming the files: settle each, remove the markers, check it builds, <code>git add</code> it, and leave completing the merge to the user; a rebase’s prompt explains that git’s “ours” is the upstream), <strong>Abort merge</strong> (after a confirmation that settled conflicts are lost: the branch and the uncommitted changes go back as they were) and <strong>Complete merge</strong> / <strong>Continue rebase</strong>, enabled once no file is in conflict. Conflicted files are marked <strong>Conflict</strong> in the list, and the selected one has <strong>Open in editor</strong>, <strong>Keep mine</strong>, <strong>Take theirs</strong> and <strong>Mark resolved</strong> (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).</p> <p><strong>DSK-90c</strong> <em>2026-10-16 (the owner’s request).</em> The workspace’s branches, on the Changes view’s Version control source (<code>Branches.tsx</code>, wording in <code>gitRemote.ts</code>, unit-tested). The toolbar’s first button is the current branch; it opens a menu (loaded as it opens, <code>ListBranches</code>): <strong>Branches</strong> (the local ones, the current one checked) and <strong>On the remote</strong> (the remotes’ branches no local branch tracks), each with its last commit’s subject. Choosing a branch switches to it (<code>SwitchBranch</code>; 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, <strong>New branch…</strong> (a dialog asking the name, “Starts at main’s latest commit. Your uncommitted changes come along.”; <strong>Create and switch</strong>, <code>CreateBranch</code>; a taken or invalid name is said under the field) and <strong>Merge into <em>current</em>…</strong> (a dialog to pick the branch, saying what merging does; <strong>Merge</strong>, <code>MergeBranch</code>; 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 <code>GetGitStatus</code> <code>merging</code>; a pull’s still names the upstream); <strong>Ask the agent to resolve</strong> names both branches, and <strong>Abort merge</strong> 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).</p> <p><strong>DSK-90d</strong> <em>2026-10-16 (the owner’s request).</em> 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. <strong>Version control</strong>, 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 <strong>Turn on version control</strong> outside a repository. Staging is including: <strong>Include in commit</strong> and <strong>Leave out of commit</strong> (were Stage and Unstage), <strong>Include all changes</strong>, “3 files included”, a file’s state “Included in the next commit” (or not, or partly). <strong>Do not version</strong> and <strong>Version it again</strong> (were Add to and Remove from .gitignore), <strong>View history…</strong> and “History of <em>file</em>” (were View revisions… and Revisions of), <strong>Commit changes…</strong> (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 (<code>git add</code>), which it runs. In every language (<code>en-US</code>, <code>es</code>, <code>fr-CA</code>).</p> <ul> <li> <p><strong>DSK-91</strong> 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; <strong>Enable as shown</strong> (enables the displayed hash; <code>HASH_MISMATCH</code> if edited since), <strong>Run now</strong> (with the run’s events live), <strong>Disable</strong>; the latest 20 runs with an icon per status, cost, time, session, refusals and error. <em>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 <strong>Delete</strong> beside <strong>Edit</strong>. Since v0.7.1 the list and the view are in Studio › Workers (<a class="gdoc-markdown__link" href="/blitz/about/specs/spec_studio_040/" >spec_studio_040</a> STU-03).</em></p> </li> <li> <p><strong>DSK-91a</strong> <em>Since v0.6.3 (the owner’s request).</em> Every worker run going now can be seen and stopped. <strong>Running now</strong> (<code>runningWorkers.ts</code>, unit-tested; <code>ListRunningWorkerRuns</code>): each row is the worker, with its workspace when it’s another one and when it started, and a <strong>Stop</strong> button (<code>StopWorkerRun</code>; a snackbar says “Stopping <name>…” and the row goes). <em>Since v0.7.1</em> 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 <strong>Stop</strong> in the run list, and the run started there has it in its live panel; a stopped run shows as <strong>Stopped</strong> with a stop icon and “stopped by the user”, not as an error.</p> </li> <li> <p><strong>DSK-91b</strong> <em>Since v0.6.3 (the owner’s request).</em> Under the worker form’s instructions (Workflow) is <strong>Rewrite with AI</strong>, 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 <code>RewriteWorker</code> with 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 <strong>Also use the permissions they need</strong> (checked). <strong>Use these</strong> puts the instructions (and the permissions, when checked) in the form; <strong>Keep mine</strong> leaves it. Nothing is saved until Save. A model that fails says why in a snackbar and the form stays as it was. Rules in <code>workerForm.ts</code> (<code>applyRewrite</code>, <code>permissionChanges</code>), unit-tested.</p> </li> <li> <p><strong>DSK-91a</strong> <strong>New worker</strong> (<strong>Workers › Add worker</strong> in the menu bar until v0.7.1, then <strong>New worker</strong> 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: <code>workerName</code>), 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 (one <code>kind:pattern</code> a line), the limits (empty: the policy’s), missed runs (<strong>Skip</strong> / <strong>Run once</strong>). <strong>Create</strong> is off until name, schedule and workflow are there; the footer says where it will be saved. <code>CreateWorker</code> (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, <code>new</code>, to review and enable. Rules in <code>workerForm.ts</code>, unit-tested.</p> </li> <li> <p><strong>DSK-91b</strong> <em>2026-10-01.</em> Agents view and <strong>Settings › Agents</strong> (<code>AgentEditor.tsx</code>, <code>agentForm.ts</code>; <a class="gdoc-markdown__link" href="/blitz/about/specs/spec_agents_014/" >spec_agents_014</a> AG-14): the view shows the workspace’s agents (<code>.agents/agents</code>; since 2026-10-02 listed in the top bar’s Agents menu, not beside the form, DSK-65a), Settings lists the user’s (<code>~/.blitz/agents</code>), from <code>ListAgentFiles</code>, 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: <strong>Name</strong> (the display name; the file name follows it until typed), <strong>Description</strong>, <strong>System instructions</strong> (the prompt) and <strong>Run in the background</strong>. A folded <strong>Advanced</strong> section (“Advanced (n set)” counts what’s set there, <code>advancedCount</code>) holds the rest: the <strong>File name</strong> (the agent’s name), default model with the known models offered, agency, permission mode, max turns, isolation, tools as checkboxes from <code>ListTools</code>, and <strong>Model settings</strong> (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. <strong>New agent</strong>, <strong>Save</strong> (<code>SaveAgentFile</code>; a rename passes the previous name; problems shown in the form, nothing written), <strong>Revert</strong> and <strong>Delete</strong> (confirmed).</p> </li> <li> <p><strong>DSK-91c</strong> <em>2026-10-01.</em> <strong>Edit</strong> on the selected worker (also an invalid one) opens the worker dialog (<code>WorkerEditor.tsx</code>, which <strong>New worker</strong> also uses) filled from <code>GetWorkerSpec</code>, titled “Edit <name>”, with the name read-only and <strong>Save</strong> (<code>UpdateWorker</code> at the loaded hash, WK-01b). Problems show as for <strong>Create</strong>; a file changed on disk since it was loaded (<code>HASH_MISMATCH</code>) is said at the top of the form, <strong>Save</strong> is off, and <strong>Reload</strong> loads the file again (confirmed when the form has edits). The dialog’s rules are in <code>workerForm.ts</code> (<code>formFromWorker</code>, <code>savePauses</code>, <code>canSave</code>), unit-tested. Saving an enabled worker says first that it pauses it until it’s enabled again; afterwards it stays selected, <code>changed</code>, for <strong>Enable as shown</strong>.</p> </li> <li> <p><strong>DSK-91d</strong> <em>2026-10-16 (the owner’s request).</em> <strong>Agents › Reload agent files</strong> (under <strong>Add agent</strong>, with “Read the agents, skills, instructions and rules in .agents again. Each message already picks up what changed.”) and <code>/reload</code> read them again (<code>ReloadAgentFiles</code>): a snackbar (<code>/reload</code>: a chat note listing the files) says what was loaded (“Reloaded 2 agents, 1 skill and 4 instruction files and rules.”, <code>reload.ts</code>, unit-tested), and an error one what didn’t load; the Agents menu lists the agents again (spec_memory_012 MEM-25).</p> </li> <li> <p><strong>DSK-92</strong> Deep links (PAR-INT-03): <code>blitz://open?dir=<absolute folder>&prompt=<text></code> (also <code>blitz:open?…</code>) 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 (<em>since v0.7.0</em> the Electron app, EL-10 and EL-22: macOS <code>CFBundleURLTypes</code> in Blitz.app’s <code>Info.plist</code> (<code>mac.bzl</code>), handed over as Electron’s <code>open-url</code>; Linux: <code>MimeType=x-scheme-handler/blitz</code> and <code>Exec=blitz-desktop %u</code> in the <code>.desktop</code> file, the link an argument) and holds a single-instance lock (<code>requestSingleInstanceLock</code>), so a second start gives its links to the running app. A link names an existing absolute folder (<code>~</code> expanded) and a prompt of at most 20,000 characters, else it’s dropped and logged. Links wait until the page asks for them (<code>PendingLinks</code>), then arrive as <code>deeplink:open</code> events; text for a composer not mounted yet waits for it (<code>takePendingCompose</code>). There’s no terminal handler.</p> </li> </ul> <div class="flex align-center gdoc-page__anchorwrap"> <h2 id="14a-pdfs-and-sound" > 14a. PDFs and sound </h2> <a data-clipboard-text="https://retail-cortex.github.io/blitz/about/specs/spec_desktop_024/#14a-pdfs-and-sound" class="gdoc-page__anchor clip flex align-center" title="Anchor to: 14a. PDFs and sound" aria-label="Anchor to: 14a. PDFs and sound" href="#14a-pdfs-and-sound"> <svg class="gdoc-icon gdoc_link"><use xlink:href="#gdoc_link"></use></svg> </a> </div> <ul> <li><strong>DSK-93</strong> <strong>Export as PDF</strong> (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 <code><name>.pdf</code> (<code>files/printPage.ts</code>, <code>FileService.WriteBinaryFile</code>). 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 with <code>ReadPreview</code> and 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 <strong>Replace <name>?</strong>; a snackbar says it’s saved, with <strong>Open</strong>; a failure says why. <em>2026-10-15 (the owner’s testing: pictures missing from PDFs of documents with maths):</em> the Wails app’s printer (<code>printpdf</code>, macOS and Linux) waited, after the page loaded, for its fonts and every picture to be ready to draw (<code>BLITZ_PRINT_READY</code>, at most 10 s) before printing: printing straight away while a font still loaded (KaTeX’s, inlined with <code>font-display: block</code>) printed the pictures as blank space. The wait ran as the app’s script; the page’s own never ran (WebKitGTK: <code>enable-javascript-markup</code> off). Electron’s printer does the same since v0.7.0 (<code>printPDF</code> in <code>apps/electron/src/main.ts</code>, <code>printLoadScript</code>, <code>printCheckScript</code> and <code>waitForPrint</code> in <code>logic.ts</code>): 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.</li> <li><strong>DSK-93a</strong> <em>Removed in v0.7.0 (the Wails app; see <a class="gdoc-markdown__link" href="/blitz/about/specs/spec_electron_038/" >spec_electron_038</a>).</em> Electron prints with <code>printToPDF</code> in a hidden window, its scripts off: EL-20. What follows was the Wails binding. <code>PrintPDF(html, pageSize)</code> (the Wails binding; the Wails app’s <code>printpdf</code> package) prints a whole page offscreen and returns the PDF, within a minute: on macOS a <code>WKWebView</code> (a non-persistent store, JavaScript off) in a borderless window that’s never shown loads it, then <code>NSPrintOperation</code> prints 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, since <code>GtkOffscreenWindow</code> has no OpenGL context) in an offscreen window loads it, then <code>WebKitPrintOperation</code> prints 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, <code>InitUI</code> and <code>RunMainLoop</code>), 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.</li> <li><strong>DSK-93b</strong> <em>2026-10-14 (#38).</em> The editor head’s PDF button becomes <strong>Export</strong> (a menu): <strong>Export as PDF</strong> (DSK-93, in the app only) and <strong>Export as Word</strong>, which sends the editor’s text (unsaved edits included) to <code>FileService.RenderDocx</code> (FS-75, its images read beside the file), with its <code>```mermaid</code> diagrams drawn in the page (<code>drawAsPicture</code>: 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 <code><name>.docx</code> beside the file with <code>WriteBinaryFile</code>, asking <strong>Replace <name>?</strong> first when one exists. It works in a browser too. <code>/export <name>.docx</code> writes the conversation as a Word document.</li> <li><strong>DSK-94</strong> Sound plays in the editor: <code>.wav</code>, <code>.mp3</code>, <code>.m4a</code>, <code>.aac</code>, <code>.ogg</code>, <code>.opus</code> and <code>.flac</code> files open as a player (<code><audio controls></code>, from <code>ReadPreview</code>’s bytes, up to 32 MB) with <strong>Open in the system viewer</strong>; the Files shelf shows them, and PDFs, with their own icons. The conversation shows <code>export_pdf</code>, <code>view_document</code> and <code>generate_audio</code> calls with icons of their own.</li> <li><strong>DSK-94a</strong> <strong>Speech model</strong> in a workspace’s <strong>Agent and model</strong> card (shown in simple mode too, since audio overviews depend on it): the <code>[audio] model</code> for this workspace, typed or chosen from suggestions (Gemini’s and OpenAI’s speech models), saved on Enter or leaving the field with <code>ConfigService.SetConfigValue</code> (<code>audio.model</code>; empty turns speech off); the workspace reloads it at once (MDL-90), and why it can’t be used shows under the field.</li> <li><strong>DSK-94b</strong> 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 <code>@paper.pdf</code> mention attaches it, and the sent prompt shows its name.</li> <li><strong>DSK-94c</strong> Attachments follow the active agent’s model (<code>GetSettings</code> <code>accepted_media</code>; spec_images_011 IMG-63): the file picker offers its types and extensions only, and paste and drop refuse other files with “<model> can’t take <name>”; 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; <code>@</code> mentions attach audio and video too. Video opens in the editor as a player (<code>previewKind</code> <code>video</code>: MP4, QuickTime and WebM, which the window plays; AVI, WMV, FLV, MPEG and 3GPP say they play in the system’s player), and <strong>Open in the system viewer</strong> takes sound and video.</li> </ul> <div class="flex align-center gdoc-page__anchorwrap"> <h2 id="15-build-test-and-ci" > 15. Build, test and CI </h2> <a data-clipboard-text="https://retail-cortex.github.io/blitz/about/specs/spec_desktop_024/#15-build-test-and-ci" class="gdoc-page__anchor clip flex align-center" title="Anchor to: 15. Build, test and CI" aria-label="Anchor to: 15. Build, test and CI" href="#15-build-test-and-ci"> <svg class="gdoc-icon gdoc_link"><use xlink:href="#gdoc_link"></use></svg> </a> </div> <ul> <li><strong>DSK-95</strong> Built by Bazel (<a class="gdoc-markdown__link" href="/blitz/about/specs/spec_monorepo_028/" >spec_monorepo_028</a>): the page is <code>//apps/desktop/web:page</code>; <em>since v0.7.0</em> the app and its packages are the Electron app’s (<code>//:desktop</code>, <code>//:mac-app</code>, <code>//:deb</code>, <code>//:pacman</code>, <code>//:desktop-tar</code>, <code>//:windows-zip</code>; EL-05, EL-80). Until v0.6.5 <code>//apps/desktop:blitz-desktop</code> and <code>//apps/desktop/packaging:Blitz.app</code> (universal) or <code>:deb</code> built the Wails app.</li> <li><strong>DSK-96</strong> <code>bazel test //apps/desktop/...</code>: the page’s vitest (<code>:test</code>) and TypeScript check (<code>:typecheck</code>); the host’s Go tests are <code>pkg/pagehost</code>’s and <code>pkg/pageserver</code>’s. CI runs them with everything else.</li> <li><strong>DSK-97</strong> Tests: the proxy against a real service (<code>apps/service/servicetest</code>) (API calls, a streamed turn and its approval, the <code>unavailable</code> error); the preferences file (defaults, normalization, owner-only atomic writes, a damaged file set aside) and <code>OpenURL</code>’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).</li> <li><strong>DSK-98</strong> Development: <code>bazel run //:desktop</code> runs the Electron app on this machine’s service (EL-05). <code>bazel run //:desktop-web</code> (<code>//apps/desktop/web:dev</code>) serves the page (Vite, with the generated API code) and proxies <code>/blitz.v1.*</code> to the service socket (<code>$BLITZ_SOCKET</code> or <code>~/.blitz/run/blitz.sock</code>). <code>?fake</code> in a development build uses an in-page fake service (<code>dev/fake.ts</code>, 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.</li> <li><strong>DSK-99</strong> Dependencies are pinned: React 19, Connect-ES 2, protobuf-es 2 (<code>protoc-gen-es</code> generates <code>src/gen</code> at build time, <code>//apps/desktop/web:api_ts</code>), 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, <code>@mdi/js</code> 7, Vite 8, TypeScript 5.9, vitest 5; pnpm 10.24 via <code>packageManager</code>. CodeMirror 6 (<code>@codemirror/*</code> 6, <code>@lezer/highlight</code> 1) 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.</li> </ul> <div class="flex align-center gdoc-page__anchorwrap"> <h2 id="16-known-gaps" > 16. Known gaps </h2> <a data-clipboard-text="https://retail-cortex.github.io/blitz/about/specs/spec_desktop_024/#16-known-gaps" class="gdoc-page__anchor clip flex align-center" title="Anchor to: 16. Known gaps" aria-label="Anchor to: 16. Known gaps" href="#16-known-gaps"> <svg class="gdoc-icon gdoc_link"><use xlink:href="#gdoc_link"></use></svg> </a> </div> <p>Not implemented; each is tracked in <a class="gdoc-markdown__link" href="/blitz/about/specs/spec_backlog_026/" >spec_backlog_026</a>:</p> <ul> <li>Sessions from other workspaces, and opening a snapshot by name (the History menu lists this workspace’s).</li> <li><strong>Localisation</strong>: the window doesn’t follow the terminal’s <code>/locale</code> (<code>[ui] locale</code>) or load catalogs from <code>~/.blitz/locales</code>; it has its own language setting and the shipped catalogs (BL-DSK-41).</li> </ul> </article> <div class="gdoc-page__footer flex flex-wrap justify-between"> <span class="gdoc-page__nav"> <a class="gdoc-page__nav--prev flex align-center" href="/blitz/about/specs/spec_workers_023/" title="023 · Workers" > <i class="gdoc-icon">gdoc_arrow_left_alt</i> 023 · Workers </a> </span> <span class="gdoc-page__nav"> <a class="gdoc-page__nav--next flex align-center" href="/blitz/about/specs/spec_release_025/" title="025 · Release" > 025 · Release <i class="gdoc-icon">gdoc_arrow_right_alt</i> </a> </span> </div> </div> </main> <footer class="gdoc-footer"> <nav class="container flex"> <div> <section class="flex flex-wrap align-center"> <span class="gdoc-footer__item gdoc-footer__item--row"> Built with <a href="https://gohugo.io/" class="gdoc-footer__link">Hugo</a> and <svg class="gdoc-icon gdoc_heart"><use xlink:href="#gdoc_heart"></use></svg> </span> </section> <section class="flex flex-wrap align-center"> <span class="gdoc-footer__item"> Content licensed under <a href="https://www.apache.org/licenses/LICENSE-2.0" class="gdoc-footer__link no-wrap">Apache License 2.0</a> </span> </section> </div> <div class="flex flex-25 justify-end"> <span class="gdoc-footer__item text-right"> <a class="gdoc-footer__link fake-link" href="#" aria-label="Back to top"> <svg class="gdoc-icon gdoc_keyboard_arrow_up"> <use xlink:href="#gdoc_keyboard_arrow_up"></use> </svg> <span class="hidden-mobile">Back to top</span> </a> </span> </div> </nav> </footer> </div> </body> </html>