spacevibe-deck AGENTS.md

A project instruction file for SpaceVibe Deck, a desktop terminal that runs several coding-agent command-line tools side by side. It documents the app's architecture, current direction, commands, layout, and verification rules.

In plain words
What is it for?
Use it when modifying SpaceVibe Deck, especially its Tauri or Electron hosts, user interface, releases, auto-updates, or verification process.
Why use it?
It gives coding agents the project context and boundaries they need before changing code. This helps prevent edits in sibling repositories and avoids adding features to the retired host.

Instructions file for CodexOpenCode

Install

Getting it into your agent

One page per mod, every tool's command on it. A separate URL per tool would split the same page into five that compete with each other.

agentmods
npx agentmods add instructions/mxrsv/spacevibe-deck/agents-md
Clone the repo
git clone --depth 1 https://github.com/mxrsv/spacevibe-deck

Made for: Codex, OpenCode.

Per session 28,400 This file is loaded in full into every session.
When invoked 28,400 The same file — it is already loaded in full.
Security scan A 0 findings. Scan, not verified.
Origin original No closer match found in the catalogue.
Token cost

What it costs to keep this loaded

Counted locally with the o200k_base tokenizer, which is exact for GPT models; Claude uses its own tokenizer and its counts differ. Treat this as one consistent yardstick across the catalogue rather than a bill. Prices are per million input tokens.

ModelPer sessionOnce invoked
Fable 5 $0.28400 $0.28400
Opus 5 $0.14200 $0.14200
Sonnet 5 $0.05680 $0.05680
Haiku 4.5 $0.02840 $0.02840

Measured 2d ago against content hash ca4cad095c69, method: parsed. Prices are Anthropic first-party input rates as of 2026-08-30, from the pricing page.

Security

Grade A, and why

spacevibe-deck AGENTS.md scanned grade A with 0 findings against 26 rules in 11 categories — prompt injection, anti-refusal, data exfiltration, privilege escalation, supply chain, agent snooping, system-prompt leakage, SSRF and excessive agency — measured 2d ago.

A static scan of the body, not an audit. Every finding is printed with the line that produced it so you can judge whether it matters here. A mod is markdown that instructs an agent; that is exactly why what it instructs is worth reading.

Nothing flagged

None of the 26 patterns this scan looks for appear in this file: no shell pipes, no recursive deletes, no credential paths, no hidden text, no instruction-override or anti-refusal phrasing, no agent-config snooping. That is not a guarantee, it is the absence of the things that are checkable.

AGENTS.md · 1,120 lines

How it starts

The opening of the file, as written. The whole thing — 1,120 lines — stays where its author put it; the contents beside it link to each section on GitHub.

AGENTS.md — SpaceVibe Deck

Boundary: standalone desktop app; no shared DB or API with the SpaceVibe web repos. Do not edit sibling repos from this session. Workspace map: ../AGENTS.md current.

Deck is a terminal for running many agent CLIs side by side. main carries two hosts: the Tauri 2 + Rust host that every release still builds, and the Electron host in electron/ that is meant to replace it. The renderer is Preact + xterm.js and reaches whichever host it runs under through the facades in src/host/. Everything in this repo — UI strings, comments, docs, and commits — is English only.

Project state: docs/CONTEXT.md current; architecture: docs/ARCHITECTURE.md current; visual rules: docs/DESIGN-LANGUAGE.md current.

Current direction

  • Auto-update is a core requirement. A release is not complete if distribution falls back to manual-download-only. Release claims require platform-specific runtime evidence.
  • Tauri is feature-frozen except hotfixes and release support. New product features land on Electron so they are not implemented twice.
  • Tags ship Electron on both platforms now; Tauri is retired from tag triggers (2026-08-20). electron-release.yml current is four jobs (prepare → mac + windows → promote): one build/vX.Y.Z (stable) or build/vX.Y.Z-electron.N (prerelease) tag — its commit must sit on main — produces a draft that goes public only with all six updater assets present, and a stable release publishes the tagged commit's CHANGELOG.md section as its notes (missing section = the release stays a draft); release.yml is workflow_dispatch-only for Tauri hotfixes. The stable is 1.0.0 — the owner named it a V1 (2026-08-20). Gate A is CLOSED for macOS — owner-verified auto-update against v0.12.5-electron.2, 2026-08-19. Windows ships unsigned and runtime-unverified by owner decision (Gate C stays open), and an updating Windows Deck Electron preview.2 becomes a side-by-side SpaceVibe Deck install with fresh userData (fork F1's identity). No macOS preview ever shipped publicly, so the stable is the first public macOS release. SpaceVibe Deck 1.0.0 is PUBLIC and is releases/latest since 2026-08-20 — run 32383647050, all four jobs green, eight assets served; the maiden run before it died in promote on a transitive-needs empty output and published NOTHING, which is the fail-closed design working. See spec decided and plan building.
  • The Electron cutover is a clean install with no settings/workspace migration. The final Tauri release must explain the manual transition and old data location. “No Electron” must stop being a proof point at cutover; “no accounts” remains valid. “No telemetry” is retired, and analytics is ON by default (decided 2026-08-23, committed 2026-08-24 as cdc07a0): the 2026-08-22 opt-in model was built, never released, and reversed by the owner the next day — no consent question is asked, declined (the Settings → Privacy switch) is the only state never inferred away, an unreadable state file still fails closed to off, and public copy says "on by default, no code, paths or prompts" and never "anonymous". USAGE_CONSENT_ASKED in usage-notice.ts current is the whole reversal switch; the consent modal stays in the tree behind it. Rollout consequence: every install of the next release POSTs, so the Worker and the privacy page are prerequisites, not follow-ups. See the usage analytics spec decided (amended 2026-08-24).
  • Electron process classification must use the measured ps snapshot path, not node-pty.process; the latter returned version/executable strings instead of argv0.
  • Pane detach Phase A exists on Tauri, including IPC contract tests; remaining native manual checks live in docs/CONTEXT.md. Phase B is Electron-only and still gated by a real Windows pointer-capture check.
  • The browser is a tab on the stage strip, not a docked column (2026-08-15). One chip in the strip's second segment (globe + page title); its surface covers the stage like the document editor does (new DL-18.8), and the docked right column, its resize drag and the browserWidth setting are gone. composeSurfaceStrip current folds it into TabManager's SurfaceStrip seam, so ⌘W, tab cycling and "last surface, not last tab" reach it without touching R4 seams. The WebContentsView itself, react-grab Inspect and electron/browser/ are unchanged. Electron only; verified by suite/build only — no native electron:dev pass or owner eye review yet. No Tauri implementation exists; its behaviour under npm run tauri dev is unverified.
  • A grab stops at the clipboard and no longer reaches a pane (2026-08-16, temporary). GRAB_PASTE_DISABLED current short-circuits deliverGrab, so react-grab's own copy is the whole delivery — the clipboard carries the snippet WITHOUT formatGrab's Page: <url> line, which only ever existed on the paste path. GrabTarget, the paste seam, its wiring in App and every gate in electron/browser/ are untouched: reverting is flipping that one constant and restoring grabSummary's two strings. Verified by the browser suite only — no full npm test, no build, no native pass. See docs/CONTEXT.md current.
  • Gate M is retired as a current acceptance gate (2026-08-23). Its packaged 6/6 run on the owner's verification Mac (2026-08-14) remains historical evidence for the pre-reshape explorer only. The maintained packaged Monaco smoke current keeps the useful regression seam — Monaco, its worker/assets, edit/save and Monaco↔xterm focus inside an unsigned local package — without claiming the current explorer layout passed. The renamed smoke itself is packaged-verified: the universal build completed and electron:verify:monaco-smoke current passed twice back-to-back on 2026-08-23, including WebGL-safe xterm input/output assertions and process-group cleanup. Pending: owner eye review (DL §9.6), the packaged both-layout manual pass and native macOS sign-off. Adding a CSP later requires rerunning the smoke. Electron only; no Tauri implementation exists. Historical detail remains in docs/CONTEXT.md current.
  • The tabs are one strip on the stage's own frame-row half, and the document renders on the stage (2026-08-14). TabStrip current is the chips; TabBar is top-tab mode's frame around it and .stage__strip is sidebar mode's mount (DL-18.6). The editor left ExplorerPanel's preview block for .stage__surface, which covers the terminal grid rather than replacing it, and RepositoryRail stopped listing file tabs entirely. Since 2026-08-15, the sidebar mount shows only terminal tabs belonging to its selected worktree and restores that worktree's last selected tab; top-tab mode remains global. Verified by suite/build only: no packaged or native acceptance covers this shape — the packaged both-layout manual pass (plan T35) and the owner eye review are owed on the new picture. See docs/CONTEXT.md current.
  • The token usage dashboard is landed, ported, and its owner-machine acceptance table has run (2026-08-14). The branch merged over main during the redesign's phase 5; its Rust backend has an Electron port in electron/usage/ gated by a Rust-produced golden-fixture parity test. docs/DESIGN-LANGUAGE.md's §15/§16 now hold its sections, §20/§21/§23 are written, and §22 stays reserved — take the next free number above §23 rather than filling a gap. The §6.1.8 acceptance table ran against this machine's real ~/.claude/~/.codex corpus, all 7 rows pass — and the gap that run surfaced is fixed (2026-08-18): discoverClaude current walks subagents/ recursively (capped at MAX_WALK_DEPTH), so subagents/workflows/<id>/*.jsonl (~25% of this machine's Claude corpus) counts; the Rust twin got the same walk to keep the parity gate honest, and a nested-file case pins both. Windows corpus behaviour is unverified (Gate C). The branch's owner-local dirty tree remains owed.
  • The open board is one center surface with three views (home/config/worktree), and create-worktree is an Electron-only flow reached from home (2026-08-14). The board's own second sidebar is retired — the app's own WorkspaceSidebar is the one sidebar now. git worktree add runs main-process side via execFile argv (never a shell string) behind a flat worktree_add IPC channel; Windows is unverified (Gate C). Details in docs/CONTEXT.md current.
  • The tab strip's +/⌘T opens AgentQuickPicker, not the Open board, since 2026-08-14. AgentQuickPicker current is a .modal-scrim genre alongside PresetEditor/SavePresetDialog (same "modal" tier in openOverlayRanks()): pick an agent chip (click or digit key 1-9/0) and TabManager.openQuickAgent spawns a single pane in the active tab's live cwd, carrying its workspace tag, no workspace/preset step. The Open board's full flow did not go away — RepositoryRail's "Open workspace" footer row now opens it directly (onOpenWorkspace, renamed from onNewTab; WorkspaceSidebar got the identical rename to keep the two prop-identical for the one-line revert). Verified by suite/build only — no native npm run electron:dev click-through or owner eye review of the wired flow yet, only of the gallery specimen it was built from. See docs/CONTEXT.md current.
  • On a dark theme the side columns rise off the stage now (2026-08-19). DL-18.7 amended; DL-2.2 gained one exception. --sidebar-bg was the DARKEST surface in the window (bg mixed 24% toward black) and is #272D31 on deck-dark — lighter than the stage — so the terminal is the deepest plane and every chrome surface stands above it. The whole dark ladder moved with it: --chrome-1/--chrome-2/--tab-active-bg are measured from --sidebar-bg rather than from --bg (deriveChromeColors current), because at the old offsets a raised sidebar landed BETWEEN chrome-1 and chrome-2 and a popover read as a smudge of the column behind it. The dark steps are 3/6/10 against light's 5/9/15 — at 4/8/14 One Dark's active row falls to 7.13:1 against white, under DL-3.5's 8:1 floor, which would flatten every chrome tone to white and start rejecting imports Deck accepts today. --input-bg sinks from the sidebar back toward the stage instead of climbing, and --seam-raised joined the ladder because from --bg it fell below --chrome-2. #272d31 is a LITERAL — it is not reachable by mixing #17181c toward white — pinned on the background, not the preset id, so all four deriveChromeColors callers agree and a background override drops the pin. Light themes are untouched. Renderer-only, so it reaches BOTH hosts; verified by a colour-relationship smoke only — no npm test, no build, no typecheck, no native pass, no owner eye review. See docs/CONTEXT.md current.
  • Appearance shows Light and Dark, and Settings reads as a document (2026-08-19). ThemeModeSelector current — a new DL-6.5 binary radio group over deck-light/deck-dark — replaced the theme gallery, the import row, the themes-folder row and the four colour overrides in one step. None of that was deleted: every module and parser still builds and still passes its own tests, they are imported by nothing in Settings, so a legacy themeId keeps resolving and the reversal is re-mounting one component (DESIGN-LANGUAGE §24 carries a retirement banner, not a deletion). Opening Settings writes NOTHING; a legacy theme is described by whichever segment its resolved background belongs to (themeModeOf current), and a click is the explicit conversion — which also clears colorOverrides, after a confirmation when an imported selection or non-empty overrides would go. New installs default to deck-dark, and getPreset's fallback moved with it. The section side gained a title/description/grouped-surface hierarchy (new DL-11.6), an icon rail below 720px (new DL-11.7), achromatic chrome (new DL-3.7), a Tab focus trap, an Escape that a dirty draft claims first, and a fieldset that is disabled until the settings snapshot lands. Two owner follow-ons the same day: the rail is text only — DL-11.3 retired, settings-nav-icons.tsx and its test DELETED, SettingsCategory.Icon gone, and DL-11.7's compact rail re-specified from a 54px icon rail to 132px of truncating text — and Settings now covers the whole window, frame row included (position: fixed, DL-11.1 amended), leaving by a Back control or Escape. That exemption is Settings-only: the rule it reverses exists so a surface cannot strand the user, and the Open board (which can be uncancellable) still stops below the strip. Reset became an ordinary category too (DL-11.5 amended) — the pinned rail foot is deleted and reset is the last registry entry, because position was never what made it safe (the native confirm is) and a destructive config row pinned in a 220px rail had to stack to fit. Renderer-only, so it reaches BOTH hosts; verified by targeted suites only — the full-suite and build gates are currently red on OTHER sessions' in-flight work, and there is no native electron:dev pass, no tauri dev pass, and no owner eye review of the running app. See docs/CONTEXT.md current.
  • The theme setting was a gallery of cards, and custom themes are imported files (2026-08-15). Superseded above as a SURFACE on 2026-08-19; the machinery below is unchanged and still loaded. ThemeGallery deprecated replaced the cycle pill inside the appearance category; each card is a miniature of Deck painted with that theme's own derived colours (new DESIGN-LANGUAGE §24, a §5 fork like §12/§13). Custom themes are files in <userData>/themes — a native picker copies them in, the folder is rescanned on mount, and deleting a file is how a theme is removed. Four formats parse in the renderer with no new dependency: Windows Terminal JSON, iTerm2 .itermcolors, Ghostty, Alacritty TOML. VS Code themes are out on purpose. Electron only; verified by suite/build only, so owner eye review and a native electron:dev pass are owed. See docs/CONTEXT.md current. Since 2026-08-16 the cards are thumbnails — the track caps at 132px instead of stretching on 1fr — and the colors rail category is gone: its four rows are a Colors group inside appearance, under the gallery that clears them (ColorOverrides current). No DL rule changed.
  • Session restore reopens Deck's tabs and resumes each pane's agent conversation on launch, since 2026-08-15. A debounced session-journal current mirrors every window's live tabs into session.json, with a per-workspace archive backing the rail's now-resumable rows. Boot restore (session-restore.ts current) runs under a crash-loop marker, drops dead cwds by a liveness pass, and resolves each built-in pane's exact session id through one batched resume_lookup IPC call before typing the resume command via the widened MaterializeIntent.paneCommands and AgentLauncher.arm(entries). Precision: claude/codex/opencode get an exact id; gemini always answers --resume latest; agy is a best-effort byte-scan with a --continue fallback; custom agents relaunch their declared command unchanged. Quit flushes the journal; a deliberate window close clears its record instead, so a closing window's own tabs cannot resurrect as ghost tabs on the next boot. Settings.restoreSessions (default on) is the kill switch. Electron only, and reverses the earlier no-restore decision. See docs/CONTEXT.md current. Verified by suite/build only (npm test 2619 green) — native macOS pass, owner eye review of the rail row, and Windows (Gate C) are all owed.
  • Both docked edges resize by drag and close by dragging past their floor, and hiding the sidebar hides it completely (2026-08-16). New DL-18.9; DL-19.4 amended. resolvePanelDrag current is the one threshold both seams use. The sidebar had no seam at all before this — SidebarGrip current is new, as are sidebarWidth/ sidebarCollapsed. Hidden means width 0: rail, frame row and seam all go, and the stage strip carries the traffic-light inset instead. That was only possible after the frame row was reduced to window controls — traffic lights plus SidebarToggle current beside them — with the feature toolbar moved to the stage strip's trailing end. Renderer-only, so it reaches BOTH hosts; verified by suite/build plus a browser measurement, with the native pass and owner eye review owed on each. See docs/CONTEXT.md current.
  • The tab strip is one row of one chip shape, ordered by when things were opened (2026-08-16). New DL-18.10; DL-18.6/18.8 amended. The two segments and the .tabbar__sep hairline between them are gone: a terminal tab, a document and the browser now share a shape and differ only by their glyph — an agent brand mark (or SquareTerminal for a plain shell), a file-type icon, a globe. A chip says what is open and nothing else: the owner then removed the colour dot, the agent attention mark and the rename popover from the strip — agent state is the rail's job, and a click on the active chip is now inert. Nothing was deleted that day (dotColor, AgentAttentionMark and TabPopover were all left standing), but later the same day TabPopover, the rename/logo features and ⌘⇧R were deleted outright — so the recorded "⌘⇧R reaches nothing in top-tab mode" consequence is moot: the chord is gone from both keymaps. Every chip now has a resting wash (--tab-rest-bg, 3% of --tone, new DL-21.7) and the selected one adds a neutral 1px --hair-strong frame (a scoped exception in DL-21.1) — a chip floats alone on the stage's --bg, so "no wash" read as "nothing here" rather than "not selected". The strip also closes with the --seam-recessed hairline .tabbar always had (DL-18.6 amended), so both layouts separate chrome from the work area the same way. The strip's close control hovers on the neutral wash now, not red (the rail's and sidebar's close buttons still do; out of scope). Order comes from one window-wide clock (open-sequence.ts current) merged by mergeStripOrder current, which TabManager and TabStrip both walk — so ⌘⇧[/], ⌘1–9 and ⌘9 count chips, and ⌘2 can land on a document (this reverses the earlier digits-stay-terminal-only rule). The R4 seam held: SurfaceStrip gained one optional method, orderKey, and TabManager still knows nothing about files. Renderer-only, so it reaches BOTH hosts; verified by suite/build plus a gallery screenshot of the merged strip — no native electron:dev pass and no owner eye review of the running app yet. See docs/CONTEXT.md current.
  • Every modal is one shell now, and the scrim closes it (2026-08-16). New DESIGN-LANGUAGE §29; DL-1.3 amended. Modal current owns the scrim, the role="dialog" frame, focus-on-mount and both ways out; AgentQuickPicker, SavePresetDialog and PresetEditor supply only a class and a body. None of the three could be dismissed by clicking outside before this, because each had hand-rolled its own wrapper. Dismissal reads the pointer press, not the click, so a drag out of the panel cannot close it, and PresetEditor withdraws it entirely (dismissOnScrim={false}) — its draft exists nowhere else. The scrim now blurs: backdrop-filter is DL-1.3's one sanctioned exception, scoped to that selector, with the wash dropped 65% → 42%. Two follow-ons rode along: the .achip digit badges came off in BOTH mounts (the keys still pick), and agentQuickPickerOpen joined panelObscured() — ⌘T over an open browser tab used to draw the picker underneath the WebContentsView. All three panels then took --sidebar-bg, the recessed plane the rail and dock already stand on (DL-29.6) — one step off --bg read as a smudge of the blurred stage rather than an object. Renderer-only, so it reaches BOTH hosts; verified by suite/build plus gallery measurements — no native electron:dev pass and no owner eye review. See docs/CONTEXT.md current.
  • AgentQuickPicker states a worktree once, then lists agents as rows (2026-08-16). New DL-29.7. A §5 config row at the top of the panel carries the destination as a menu value (folder · branch); below it the agents are a COLUMN, not the open board's wrapped grid. Worktree and branch are one choice, because git makes them one — a worktree is checked out on exactly one branch — so picking a branch independently (a git checkout into a possibly-dirty tree with agents running in it) is deliberately NOT offered; the open board's create-worktree flow stays the way to reach a branch with no worktree. worktree-destinations.ts current is the pure half; no new IPC, since git_repository already reports every worktree with its branch and repositories-store already caches the scan for the rail. openQuickAgent took a second argument — a destination overrides BOTH cwd and workspace tag, null keeps the old behaviour — which is the one materialization seam that moved (fork approved by the owner). git_repository is Electron-only, so on Tauri the row is omitted entirely. Suite/build plus a gallery specimen; no native pass, no worktree actually opened into.
  • The open board is home plus the worktree form; picking a workspace opens it (2026-08-16). The Layout + Agent config view is DELETED, not hidden: a click on a recents row, a folder from the picker, or a freshly created worktree goes straight to onOpen with the combo that workspace was last opened with (lastPresetId + lastAgent, including a remembered null = Shell), and an unknown folder takes the last-used preset and the first detected agent. Choosing an agent per open is AgentQuickPicker's job (⌘T) — the board no longer offers one, and renamePreset/deletePreset lost their only call sites with the layout cards, so a preset can be created (⌘⇧N / menu) but no longer renamed or deleted anywhere in the app. Two consequences carried on purpose: a remembered agent whose binary has left $PATH now falls back to the first detected one silently (the footer that used to warn is gone), and the open path AWAITS the detect_agents probe, because a click landing before it answered would otherwise resolve against an empty list and quietly spawn a Shell. The board's one failure line moved to home (.board-home__notice, role="status") — it is the only place a failed spawn or a missing folder is ever said. Renderer-only, so it reaches BOTH hosts; npx tsc --noEmit is clean, but no suite run, no bundle, no native pass. See docs/CONTEXT.md current.
  • Every rail row says what its agent just said; state no longer dims it (2026-08-17, amended 2026-08-19). New DL-27.15; DL-27.11's "only asked/failed may spend a second line" is superseded. The sentence is read off the agent's own session log by session-tail.ts current over a new flat session_tail channel, and asked for by session-tail-store.ts current — debounced on tabViews, never on a timer, and only for panes that have actually run something, so a fresh pane cannot wear yesterday's session's sentence. claude, codex and — since the same day — opencode produce a real tail; gemini, agy and custom agents answer null. Two frozen decisions were overridden on the owner's explicit ask that day: the rail spec's §2.6 ("a message line is exceptional") and its §10 sequencing gate ("tier 1 native pass before tier 3 starts"). Electron only — on Tauri the rail degrades to the fallback. Verified by suite/build plus a gallery pass on the real AgentRail; the native electron:dev pass and the owner eye review are owed. See docs/CONTEXT.md current. On 2026-08-19 the owner withdrew the quiet-row treatment because live, clickable agents read as disabled. Every row now keeps full legibility. The trailing state vocabulary also collapsed to one static 9px dot: red failed, yellow asked, neutral working; done and idle paint nothing. The project header now reads folder → name → trailing caret; its name and folder are 2px larger, and the redundant Workspace caption is gone. Targeted rail suite only; no build, native pass, or owner eye review of the running result. See docs/CONTEXT.md current.
  • The turn TAKES the agent's name, and the strip's chips say it too (2026-08-17). DL-27.15 amended hours after it landed; DL-18.10 amended; DL-20.1 gained a fourth radius role. Every rail row is ONE line — the sentence stands where the agent name stood, because the brand glyph beside it already said that word and three claude rows in one project were told apart by nothing else. A name the USER typed still wins and the turn follows it on the same line; a pane that has said nothing keeps its agent name, so no row is blank. RailPaneRow.message is the tail or empty — the custom-name fallback is gone — and RailTabRow gained named. The tab strip prints the SAME sentence through the same precedence (tabTail current), paying for the longer text with --radius-flat (2px), --type-meta and max-width: 210px; the chip still reports no agent STATE — what 2026-08-16 took off it stays off. Renderer-only, so it reaches BOTH hosts; verified by the rail/strip/design-language suites, npm run build and gallery screenshots — no native pass, no owner eye review. See docs/CONTEXT.md current.
  • opencode moved to SQLite, and Deck was reading a dead store (2026-08-17). Deck's opencode scanner walked ~/.local/share/opencode/storage/, a json tree that opencode 1.18 stopped writing: everything now lives in opencode.db beside it, ids and json shapes unchanged. Nothing failed loudly — the old tree is still on disk, so the scan just returned stale sessions, which silently broke BOTH the rail's session_tail (no sentence) and resolveResume (session restore resuming the wrong conversation, or none). opencode-db.ts current reads it through node:sqlite, Node's own driver — no npm dependency, no native rebuild, no packaging/signing consequence (owner-approved fork; better-sqlite3 was the rejected alternative). Verified present in the Node that Electron 43 embeds (24.18.1). opencode.ts merges both layouts, database first, deduping by id — the migration kept ids, and two copies of one session would defeat resolve.ts's greedy dedup and hand two panes the same conversation. resolve.ts itself did not change: it still calls opencode.candidates. Sub-agent sessions (parent_id IS NOT NULL) are excluded — they share their parent's directory, and quoting one shows a delegated task's turn as the pane's own. The tail is one statement whose two json_extract predicates are the file walk's rules in SQL: role = 'assistant' skips the user, type = 'text' skips reasoning (which carries a text field of its own — matching the field prints private thinking on the rail). Electron only. Evidence: electron/resume suites 45/45 (opencode-db, session-tail, resolve), tsc -p tsconfig.electron.json clean, and a tsx smoke against the owner's real opencode.db resolving the live spacevibe-api pane to its own session id and its own sentence. No full suite, no bundle, no native electron:dev pass, no owner eye review.
  • Chrome ink is neutral gray now, and so are the hairlines (2026-08-17). New DL-3.6; DL-2.3's hairline carve-out closed. deriveChromeColors builds the whole --text-* ladder out of the theme's foreground, so three built-in palettes' blue-violet ink (Tokyo Night #c0caf5, 73% saturated; Catppuccin #cdd6f4, 64%) was tinting every label, path and menu item in the app. Each built-in foreground in THEME_PRESETS current became the gray of matching WCAG luminance — every contrast ratio moves by under 0.06, so DL-3.5's floors did not move and only the hue is gone. The ANSI sixteen are untouched; a cursor follows only where the palette already had it equal to foreground. DL-3.6 binds the four built-ins ONLY — an imported theme keeps its file's foreground, so chrome under a tinted import is still tinted. --hair/--hair-strong were the last tokens mixing from --fg and now mix from --tone like the seams. Renderer-only plus a data change, so it reaches BOTH hosts; verified by suite/build plus a gallery browser pass — no native pass and no owner eye review, which is the weakest evidence class for a colour change. See docs/CONTEXT.md current.
  • The quick picker answers the keyboard from anywhere, and every project has its own + (2026-08-19). New DL-27.18 and DL-29.8; DL-27.17 and DL-29.5 amended. Modal current reads Escape at the DOCUMENT in the capture phase now — on the panel it only answered while focus was still inside the dialog, so one click on the scrim left the modal on screen with Escape travelling into the terminal behind it. That fix reaches all three modals without any of them changing. AgentQuickPicker gained roving focus over its rows (ArrowUp/Down/Home/End, Enter as the native press; focus still STARTS on the panel per DL-29.2, so a reflexive Enter after ⌘T still does nothing), one --text-faint line naming the keys — the digits kept working after the badges came off on 2026-08-16 and nothing said so — and a missing row that opens Settings instead of spawning command not found. The rail's project header is a row of TWO controls now (AgentRail current): .asr-cluster__toggle keeps folder → name → caret, and .asr-cluster__add opens the picker with quickPickerWorkspace current pinned to that project. That signal lives beside agentQuickPickerOpen because newTab() has to CLEAR it — otherwise the next ⌘T inherits the rail's target. A folder git does not know is stated by plainFolderDestination rather than by the panel's "Runs in this workspace" line, which would be a lie about a project the user pressed. Renderer-only, so it reaches BOTH hosts; verified by targeted suites and npx tsc --noEmit only — no npm test, no npm run build, no native pass, no owner eye review. See docs/CONTEXT.md current.
  • Settings → Agents is the agent catalog, and the commands ship with the app (2026-08-19). Every agent Deck knows is a row stating the command it will launch with, and that command comes from the CATALOG, not from a setting: BuiltinAgent current gained defaultCommand and url, so a fresh install shows claude --dangerously-skip-permissions immediately rather than a bare binary waiting for someone to type a flag. Flags are verbatim from each CLI's own --help on the owner's machine; opencode ships bare because its --auto is opt-in per session. A user preset replaces the shipped command for that agent — nothing merges. The list splits on what the discovery probe found: Installed with a count and a Refresh, then Available to install, so "can Deck run X" is answered on screen. Two settings fields carry the row's controls: disabledAgents, because a built-in cannot be deleted (the probe finds it again) and the switch is the only thing that takes one out of the pickers; and defaultAgent, offered on installed rows ONLY, since starring a binary that is not on $PATH names a default that cannot run. A preset is a command line, not a set of options. launch-profile.ts current stores the STRING and one text field adds it. Two earlier builds the same day stored semantic options per agent and composed them; neither could express a flag nobody had modelled, and both put four controls between the user and a command they already knew. The safety rule that made the enums attractive is enforced directly instead: AgentLauncher.arm writes this string VERBATIM into a live interactive shell, so commandProblem current refuses separators, substitution, redirects, quotes and newlines and says why — a pipeline belongs in a wrapper script declared as a custom agent. An agent is derived from a command's first word, never stored beside it. The journal stores the COMMAND, not a preset id, so editing or removing a preset cannot rewrite a running session; on restore only claude is re-flagged, its flags sitting beside --resume where codex resume and opencode take theirs in positions this repo does not model. cursor-agent is the sixth built-in, appended LAST so every existing digit key keeps its agent; no Cursor session scanner exists, so resume_lookup answers null for a cursor pane and it relaunches bare. Not built: drag-to-reorder and the per-row expand caret, both of which the owner's reference shows. Renderer-only plus a catalog change, so it reaches BOTH hosts; verified by npm test (3250 passed, 0 failed), npm run build and a gallery pass on the REAL component — no native electron:dev or tauri dev pass. See docs/CONTEXT.md current.
  • The panes of one tab stand inside a frame (2026-08-20). New DL-27.19. A tab running several agents listed its panes as rows that said nothing about belonging together, ever since DL-27.13's parent row and elbow guides went behind PANE_TREE_HIDDEN; a rounded --hair-strong hairline at --radius-control now closes each such block. It is CSS onlythe headless-item rule current is exactly "several panes, no parent row", so the frame needed no markup, no model change and no R4 seam. The frame is DL-1.3's inset hairline (box-shadow: inset 0 0 0 1px), and it took two wrong shapes to get there — the owner caught both within minutes. A border bled back by margin: 0 -1px put 255px of content in the 254px .asr-rail__list, and overflow-x: hidden hides that bar without removing the scroll container (Known traps: a 1px overflow moves chrome once focus lands in it). An outline then paints on the 1px OUTSIDE the block, which that same hidden clips — the left stroke simply vanished. An inset hairline paints inside: no layout, no overflow, nothing to clip. The colour stays neutral: the drawn alternative wore the tab's TabView.dotColor (gallery column B4) and was turned down, since red and yellow are the status dot's words and nothing has been able to SET that field since TabPopover was deleted. Electron-only in effect — the seam needs showAgentPresence — and it rides PANE_TREE_HIDDEN, so restoring the tree takes named multi-agent tabs back out of the frame. Verified by the design-language and agent-rail suites plus a gallery measurement and screenshot of the REAL rail — no full npm test, no build, no native pass, no owner eye review. See docs/CONTEXT.md current.
  • ⌘+click on a path an agent printed opens it in Deck (2026-08-20). New DL-14.7 and DL-23.11. A path inside a workspace this window already has open lands in Deck's own editor as a preview tab, revealed at its line; anything else goes to the app selected on a new split-button beside More. Containment is answered main-process side by workspaceForPath current through the explorer's own resolveInsideRoot guard, and it answers the root as the renderer spelled it — a realpath'd root is a key no file-surface lookup knows. Detection gained four grammars (tsc's (340,15), quoted paths and Python's , line N, git's a/ prefix stripped renderer-side into the same resolve batch, and ESLint's cross-line rows, whose header text is part of the provider's cache key). The external apps are a ten-entry catalog mirrored across src/lib/external-app-catalog.ts current and electron/external-apps.ts current; installed = the bundle exists, the icon is read off that bundle at runtime, and launching is execFile argv, never a shell string. One setting replaced two: externalAppId where editorId/editorCommand were, which costs the custom editor command — a real loss, in the drift table below. resolve_paths and open_editor are UNCHANGED, so the Rust twin stays valid. Detection is renderer-only and reaches both hosts; routing and the button are Electron-only. Tauri keeps today's behaviour because a host that cannot ANSWER is a third state, not an empty machine: available current (the __deckHost presence flag worktree-host.ts already uses) makes an unanswered host take the selection at its word — editor selections keep their template, anything else falls back to VS Code's — and hides the button entirely. Verified by suite and build only — no native electron:dev pass, no owner eye review, Windows is Gate C. See docs/CONTEXT.md current.
  • The landing's window mock draws the shipped app, not the July one (2026-08-20). Marketing only — no src/ or electron/ file changed, and no DL rule moved: DL binds app chrome and this is a drawing of it. The hero is one still .a-appwin in deck-dark's plane order — an agent rail of project clusters and per-pane sentence rows, one unified tab strip (terminal + file + browser chip), a frame row of traffic lights + sidebar toggle + New, three streaming panes, and no status bar and no dock. The tour's grid panel is cut and its whole render chain deleted; four panels are rebuilt and two are new (Usage → Overview, Settings → Agents). All ten --sg-* tokens are re-derived from deck-dark and eleven added, each named for the app token it mirrors. A pane's script now carries tail/state per step, which mountStageStream current writes to every [data-tail]/[data-dot] node the pane owns — querySelectorAll, never querySelector, and scoped to the root it was handed. Two knowing divergences: the active chip echoes the FOCUSED pane, where the app's tabTail prints the LOUDEST; and the marketing video keeps drawing the July shell by choice (stage-driver.js hard-requires [data-ws-avatar]), so stageSidebar / renderStageSidebar / renderStageStatus survive as video-only and nothing was removed from marketing/stage/. marketing/** has NO lint signal at all — it is in .prettierignore AND in oxlint's ignorePatterns, so every "prettier clean" claim over this tree is vacuous. Verified by build:landing, vitest run marketing/ 159/159 and a 42-image headless capture at 1440/768/390 in both motion modes; owner eye review and frontend-design-bar are owed, and marketing/video/out/ is now stale in colour as well as shape. See docs/CONTEXT.md current. Same day, owner-asked (the onorca.dev pattern, adapted): the hero rail densified to six clusters — a red failed row, a second remembered header, and all six built-in agents including the cursor-agent monogram — and the hero's stage region cycles through four scenes on a timer (HERO_SCENES current) behind the ONE live rail: agents (14s) / restore / surfaces / usage (9s each), the last three being the panels' own bodies re-mounted, never redrawn. It began the day as a row of click pills; the owner replaced them hours later with the automatic cycle ("the workspaces run one after another"), which knowingly amends the 2026-08-19 no-decorative-loops line: this one timer shows work, it is the page's only one, and reduced motion holds the hero still on the agents frame. The same review made the window NATIVE: pane grids are flush over a 1px --sg-seam-divider (no card border, radius, gap, or focus ring — 06-stage-panes.css is the reference), and the transcript inks went neutral — t-tool purple and t-ok green died, the codex header blue became dim bold — because the real CLIs print plain foreground. The scene animations' gate widened to :is(.panel, .a-appwin__stage).is-revealed — one class, two writers (the panels' IntersectionObserver and the cycle) — and the var()-carrying ones moved to animation LONGHANDS — a shorthand holding var() is stored pending-substitution and Chromium restarts it on any global style recalc, which is what had Playwright's own screenshots catching two restore panes at width 0. Verified by vitest run marketing/ 165/165, build:landing, the capture gate, and scene screenshots.
  • A chord that cannot do anything no longer eats the key, and Ctrl+C copies or interrupts on Windows (2026-08-20). handleShortcut consumed the keystroke the moment matchBinding returned an action and only then let overlayBlocksAction decide whether the action could run — so every scope: "pane" chord was swallowed and then blocked over a file surface, and Ctrl+Shift+C on an open document copied nothing AND denied Chromium's own copy. isActionPerformable current is asked BEFORE preventDefault(), and a false answer leaves the event alone so it reaches whatever holds focus. The predicate is keyed on the ACTION, not the binding, because user overrides replace an action's whole chord set — so copy-or-interrupt current is a second action (Ctrl+C, WINDOWS_KEYMAP only, no menu field) rather than a flag on copy-selection. It consumes only while a terminal owns the stage AND holds a selection, and it CLEARS that selection so the next press interrupts; Deck writes no \x03 — not consuming is what lets xterm encode the interrupt. Cancelling after a selection therefore takes two presses, accepted. macOS is untouched: ⌘C stays the native Cocoa Copy role. Renderer-only, so it reaches BOTH hosts. Verified by npm test (3375 passed / 10 failed, all ten reproduced on a pristine HEAD worktree and attributed to other sessions), both typechecks, npm run build and generate:menu:checkthe Ctrl+C keystroke has never been pressed on Windows (Gate C), and there is no host run and no owner eye review. Known gap carried on purpose: macOS menu-bound chords (find, clear-buffer, zoom-*…) still die over a file surface, because Cocoa consumes their accelerators before the webview and no renderer-side reorder can reach them. See docs/CONTEXT.md current.
  • Tauri users are told the build has stopped updating itself (2026-08-21). New DL §30. migration-notice.ts current holds the switch (MIGRATION_NOTICE_ENABLED, the GRAB_PASTE_DISABLED precedent) and a pure shouldShowNotice, so the case that matters — Electron host → false — is proven without mounting anything; MigrationBanner current is the row. It became true rather than defensive when 1.0.0 took releases/latest: the endpoint tauri.macos.conf.json serves, releases/latest/download/latest.json, now answers 404, so a deployed Tauri client's update check FAILS. The row sits BENEATH the tab strip (the strip is top: 0 and a hidden sidebar puts the traffic lights there), costs --notice-h of stage height rather than floating over the panes, and its dismissal is window-scoped and unpersisted — two windows dismiss twice, accepted. No file under src-tauri/ and no workflow step changed (spec §2: the notice must never become a second way to reach download-failed). npm test 3529/0, npm run build, generate:menu:check and the design-language gate are green, plus a gallery screenshot of the real component — no tauri dev pass, no electron:dev pass proving it stays hidden, no owner eye review. See spec decided.
  • A project cluster goes where the user drags it, and stays there (2026-08-22). New DL-27.20. The rail's clusters sat where their oldest tab put them and the remembered tier below them sat in MRU order; the header is the whole cluster's drag handle now, and the position survives the case the owner named — the cluster's last tab closing. That works because RailStreamGroup.orderKey current is the un-prefixed key both tiers produce, where key carries remembered: and therefore changes. rail-order.ts current is the pure half: pinned clusters first in stored order, everything else in exactly today's order, and an empty railOrder returns the assembled array ITSELF. A pinned cluster ignores the live/remembered boundary — a knowing break with 2026-08-20, because the owner asked for a position, not a position within a tier — and a drop pins every cluster above it, or slot 1's open order would push slot 2 around. plain:<path> entries written before a scan lands match AND are rewritten to the repository key on the next write, so the list canonicalizes instead of holding two spellings. Only the CLUSTER drags (owner: not a tab row, not a pane row, not between clusters), which is what keeps this off the tab strip: the shared order key is about TABS and no openedAt is rewritten. Settings are app-level, so a drag reorders every window's rail. Renderer-only, so it reaches BOTH hosts; targeted suites, typecheck, lint and the DL gate are green over every file it touches, but the full suite, the build, a host pass and the owner eye review are all owed — no cluster has been dragged in a running app. See spec decided.
  • Every rail row closes what it names, and the window outlives its last agent (2026-08-22). New DL-27.21. The rail drew AGENTS and closed TABS: a single-agent row's ✕ said Close tab, a multi-agent tab's rows had no ✕ at all (DL-27.13's parent row is behind PANE_TREE_HIDDEN, so closing one of three agents from the rail was impossible), and a project header's ✕ existed only on a REMEMBERED cluster, where it forgot a folder. One rule replaces the three: the control closes the thing its row names. An agent row closes that pane — ⌘W's own contract, so the tab follows only when the pane was its last (closePaneAt current, deciding from manager.paneCount(), never from the rail's agent-row count); a row with no agent is a shell tab and closes the tab. A tab holding one agent beside a plain shell now survives that agent's close — carried on purpose, since the alternative kills a shell nobody asked about. A project header closes every tab of the repository, secondary worktrees included, under ONE busy dialog over every pane (closeTabs current, which pins entries by identity before the first dispose and answers false for a decline, so a cancelled close cannot forget a project whose tabs are all still open) and THEN takes the project off the rail — RailStreamGroup.historyPaths is populated for LIVE clusters now, by prefix attach AND by project key, because a worktree of the same repository with nothing open in it is attached to no live path and would rebuild the header under its own orderKey. disposeTab stopped closing the window: the last agent leaves the window standing on the Open board with its project headers intact, and flushSettingsSave went with the close it existed for. removeEmptyTab's closeWindow (the pane-MOVED path) is deliberately unchanged, and no pty exit reaches disposeTab at all — a tab's last pane exiting prints [Session ended] and removes nothing. ⌘⇧W is untouched. A leaf became a DL-27.1 container plus hit layer, since a button cannot hold one. Renderer-only apart from that one disposeTab branch, so it reaches BOTH hosts. npx tsc --noEmit clean and the six affected suites green (183 tests) after a medium code review caught three real defects in this work — an unused flushSettingsSave import that broke the build, an ES2022 Array.at in a test under an ES2020 lib, and closePaneAt routing on a STALE index before checking pane membership, which could close an unrelated single-pane tab silently. Still owed: full npm test, npm run build, the design-language gate, a host pass and an owner eye review; no agent has been closed from a leaf and no project from a header in a running app. See spec decided.
  • A rail row quotes its OWN session now, because the pairing is remembered (2026-08-22). Three rows in one cluster printed the identical sentence, each stamped now. It was said once and copied: the tail request carried (agent, cwd, lastSeenAt) and nothing else, so the pane→session pairing was re-guessed by mtime proximity every 300ms and permuted; merged kept the previous sentence on a null answer; and null was the COMMON answer for a working pane, since only the last 64 KiB was read and an agent's own tool traffic fills it (486 of the 616 records past the window were user:tool_result, measured). So a pane kept a sentence while its session was released to the next pane, which kept it too. A request may now carry preferredId and resolveSessionTails current runs two passes — every pin honoured before anything is ranked, because one pass in request order lets an earlier unpinned pane take a later pane's pinned session and the churn resumes. The answer became { id, tail }: only the id separates "same conversation, nothing new to quote" (keep the row) from "different conversation" (drop it, empty or not). findCandidateById current skips the 30-day cutoff and the ranking on purpose — both exist to guess at what a pin states. Two review findings shaped the rest. A pairing must not outlive its agent generation: a pane id outlives its occupants (claude → shell → claude), and a surviving pairing kept being sent as preferredId, kept being honoured, and pinned the new agent's row to the old agent's sentence for the life of the pane — worse than the drift this change fixes, since a drift self-corrects and a pin does not. The forget reads two tells already on PaneView (agent label changed, or hasRun went true → false), and fingerprintOf gained hasRun and now covers EVERY pane so a generation change cannot be skipped as a repeat. And a mark says "ask for this pane", never "this pane is running session X": noteResumedPane briefly carried the resolved session id so a restored pane would start out pinned, and that was BUILT AND WITHDRAWN the same day — a mark is keyed by (workspace, agent) with no causal link to a pane, is claimed by whichever matching pane the poll recognizes first (refs [none, B] leave one mark the FRESH pane takes), and is left as soon as materialize resolves while the command is only armed. Doing it properly needs a mark bound to a pane id — the tab-materialization seam, a fork. Also from review: the host facade walks the REQUESTS not the reply, resetSessionTailStore bumps an epoch an in-flight answer checks before merging, and the growing window lost its short-read early exit (one readSync may legally return short; that is not EOF). Deliberately not fixed: a fresh pane's FIRST pairing is still ranked (birthtime is the honest anchor and is unread), the request still carries the TAB's cwd, and the 300-file scan cap is global and pre-cwd. npm test 3673/8 with every failure proven to belong to other sessions on a pristine HEAD worktree (this change alone there: 1019/1019), both typechecks, npm run build, npm run electron:build and Prettier clean — no electron:dev pass, no owner eye review, and no rail watched for the minutes of live traffic the bug needs. See docs/CONTEXT.md current and the plan current.
  • Markdown opens rendered, and ⌘⇧V flips it to source (2026-08-23). New DL §31. Opening a .md/.markdown file from the tree shows the RENDERED document instead of Monaco; .mdx opens as source, because its JSX renders as broken prose. The view is a read-only picture of the LIVE BUFFER — saved or dirty — so it rides the external-change silent-reload path for free, debounced 150ms so an agent streaming a file cannot thrash layout. markdown-render.ts current is synchronous and pure: fenced blocks, mermaid fences and local images come out as data-md-* placeholders and markdown-enhance.ts current fills them in against the mounted node, which is what makes the whole policy assertable as strings with no jsdom, no Monaco and no mermaid. Fenced code is tokenized by Monaco's own colorizer against the enumerated EDITOR_LANGUAGES set (no new dependency, and .md already lazy-loads Monaco); mermaid is imported ONLY when a document actually holds a fence, and a diagram that will not parse keeps its code block plus the error — never a blank hole. The feature needs no CSP, by design: raw HTML is escaped and shown verbatim, a link carries no href at all (the decision rides in data-md-target), javascript:/data:/every unhandled scheme and anything resolving outside the workspace root render as PLAIN TEXT, http(s) goes out through shell_open_url, an in-workspace relative link raises the same requestPathOpen a ⌘+click does, and images are local-only with no network fetch, ever. One departure from the spec, named: images resolve containment through workspace_for_path and read through read_image_as_data_url rather than read_file, which looksBinary refuses for every PNG — still two EXISTING channels, so no new IPC and no contract moves. No R4 seam moved: SurfaceStrip gained two optional methods (canToggleView/toggleView) beside orderKey and runEditCommand. toggle-markdown-view is the 54th registry action, performable-gated so ⌘⇧V passes through untouched anywhere else, and macOS-only — Ctrl+Shift+V is already paste, and a declined performable action does not fall through to a second binding. Electron-only in effect by inheritance (the file surface has no Tauri implementation). npm test 3755/8 with all eight proven to belong to other sessions on a pristine HEAD worktree, both typechecks, npm run build and generate:menu:check clean, and both new dependencies confirmed as their own lazy chunks — but no native electron:dev pass and no owner eye review: no diagram has been drawn, no image read off disk and no link clicked in a running app. See spec decided, plan building and docs/CONTEXT.md current.
  • The rail marks the agent holding the keyboard (2026-08-23). New DL-27.22. A multi-agent tab renders headless (DL-27.13), so DL-27.8's row wash had no row to land on and the rail could show a whole column with NOTHING selected — the owner reported exactly that, from a screenshot of framed rows. The focused leaf now wears DL-21.1's own --tab-active-bg, so a one-agent tab marks its ROW and a several-agent tab marks its LEAF: one signifier, at most one washed row. The fact had to be plumbed, because nothing carried it: PaneView.focused current is activePaneId() projected per pane in syncViews, and onActivePaneChange current — fired from setActive, where every focus path converges and a repeat already early-returns — is what tells the tab layer focus moved. onPaneFocus could not do it: it is suppressed while focusPane drives the focus (so a rail click never reached it), gated on the window being foreground, and syncs only when an attention ACK changed something, which is null for a pane with nothing latched. The AND with the tab's own active lives in paneRows current, where "at most one focused row in the whole rail" is assertable. A document or the browser on the stage does NOT clear the mark — the active pane is unchanged, and the row then reads as where the keyboard returns to. Renderer-only apart from that one callback, so it reaches BOTH hosts; verified by npx tsc --noEmit, npm run build, the five affected suites (156 tests) and a gallery pass on the real rail — no native electron:dev pass and no owner eye review of the running app. See plan building and docs/CONTEXT.md current.
  • Chrome gallery is current: gallery.html mounts real components through src/gallery/; run npm run prototype:gallery. Gallery code must never enter the shipping bundle. Its window-chrome section is narrowed to the one selected direction on purpose; parked comparison specimens stay in the tree but out of the registry.

Read the full file on GitHub · 1,120 lines

Changes

What this file has done since we first saw it

Hashed on every crawl. A supply-chain change to an agent config is a question of when, not whether, so the history is kept rather than the latest state alone.

  1. 2d ago First seen · 1,120 lines · 28,400 tokens per session scan A ca4cad095c69

Subscribe to this mod's changes

spacevibe-deck AGENTS.md is an instructions file published in the GitHub repository mxrsv/spacevibe-deck (10 stars, last pushed 9d ago), licensed MIT. It adds 28,400 tokens to every session, about $0.1420 per session on Opus 5. A static security scan graded it A with 0 findings. No closer match exists in the catalogue, so it is treated as the original; first seen 2026-08-31.