From 44445a959a01c1a2df68459d321f75c9a8b90911 Mon Sep 17 00:00:00 2001 From: mxrsv Date: Sat, 19 Sep 2026 00:48:19 +0700 Subject: [PATCH 1/3] chore(release): cut 2.0.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump the version, freeze the Unreleased notes as the `## 2.0.0` section the promote job publishes verbatim, and record the icon exceptions the code already made deliberately: the agent brand marks inlined by design review F1 so they follow the theme tone, and the parked 2026-09-18 gallery record whose `×` appears only in prose describing the DL-27.25 `×N` count. Claude-Session: https://claude.ai/code/session_01AC7TvhjEXzJFMYVk9HAVbR --- CHANGELOG.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- scripts/icon-system.test.ts | 16 ++++++++++++++-- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75376a34..d722c962 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ User-facing release notes. The release workflow's `promote` job publishes the platform-limitations header), so each section is written for users, reviewed in the release PR, and frozen at the tag — never an auto-generated commit list. -## Unreleased +## 2.0.0 ### Feedback diff --git a/package-lock.json b/package-lock.json index 8cbfda4f..b93cf230 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "spacevibe-deck", - "version": "1.2.0", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "spacevibe-deck", - "version": "1.2.0", + "version": "2.0.0", "hasInstallScript": true, "dependencies": { "@sentry/electron": "^7.18.0", diff --git a/package.json b/package.json index 83b0d26d..1548613f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "spacevibe-deck", "private": true, - "version": "1.2.0", + "version": "2.0.0", "type": "module", "scripts": { "generate:menu": "tsx scripts/generate-menu.ts", diff --git a/scripts/icon-system.test.ts b/scripts/icon-system.test.ts index 28a54673..d4dac8ba 100644 --- a/scripts/icon-system.test.ts +++ b/scripts/icon-system.test.ts @@ -32,6 +32,12 @@ const ALLOWED_SVG: ReadonlyMap = new Map([ // The agent-pending ring: ticks positioned by hand and spun by CSS. No // library icon expresses it, and it is a status visual, not an action. ["ui/workspace-spinner.tsx", 1], + // The single-colour agent marks, inlined by design review F1 (2026-09-18). + // Their assets are `fill="#fff"`, so as an `` they vanished on the light + // theme; an inline `` follows the chrome tone + // (DL-2.2) without a `filter` (DL-1.3). Brand marks, not action icons. + ["lib/agent-logos.ts", 1], + ["ui/controls/agent-glyph.tsx", 1], ]); /** @@ -80,7 +86,13 @@ const GLYPH_EXEMPT = (path: string): boolean => // reaches no bundle (R7 plus `gallery-entry.test.ts`), but the scan reads // every source file, so the record of the review needs the same exemption // as the thing it produced. - path === "gallery/sections/strip-actions-variants.tsx"; + path === "gallery/sections/strip-actions-variants.tsx" || + // The parked 2026-09-18 before/after record. Its `×` occurs only inside + // `note="..."` prose describing the DL-27.25 `×N` count ("the segment, the + // ×2 and the open row"), never as a control — the specimens draw their own + // chrome. Parked out of the registry like `strip-actions-variants.tsx` + // above, and exempted on the same grounds. + path === "gallery/sections/before-after-2026-09-18.tsx"; function sourceFiles(dir = SOURCE_ROOT): string[] { return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { @@ -112,7 +124,7 @@ describe("icon system", () => { expect(files.length).toBeGreaterThan(50); }); - it("authors no SVG outside the two documented exceptions", () => { + it("authors no SVG outside the documented exceptions", () => { const drawn = files .map(({ path, text }) => ({ path, From dd5c028949a65b27137f038082f6d6d46cc18cf1 Mon Sep 17 00:00:00 2001 From: mxrsv Date: Sat, 19 Sep 2026 01:10:01 +0700 Subject: [PATCH 2/3] feat(panes): remove the pane top-edge activity and focus motion The rail-click locator and the working-agent current both spent yellow on a pane's top edge. Neither survives: `pingPane` and its slot overlay are gone, `setPaneWorking` leaves the TerminalManager contract with the `is-agent-working` class it drove, and `--pane-focus-current-duration` goes with the animations that were its only readers. DL-18.11 keeps its number and now reads as the prohibition, so the rule cannot be re-taken; the scoped-exception counts in DL-3.1/3.2, DL-27.3 and DL-27.25 lose the member they were counting, and DL-21.2's note loses the clause that deferred to it. `design-language.test.ts` asserts the absence it used to assert the shape of. Claude-Session: https://claude.ai/code/session_01AC7TvhjEXzJFMYVk9HAVbR --- CHANGELOG.md | 4 + docs/DESIGN-LANGUAGE.md | 142 ++++++++---------- scripts/design-language.test.ts | 46 +----- src/styles/01-tokens.css | 3 - src/styles/06-stage-panes.css | 127 +--------------- src/terminal/pane-ping.test.ts | 83 ---------- src/terminal/pane-ping.ts | 23 --- .../tab-manager.tab-lifecycle.test.ts | 5 +- src/terminal/tab-manager.ts | 1 - src/terminal/terminal-manager-types.ts | 2 - src/terminal/terminal-manager.ts | 3 - src/ui/app.tsx | 6 - 12 files changed, 79 insertions(+), 366 deletions(-) delete mode 100644 src/terminal/pane-ping.test.ts delete mode 100644 src/terminal/pane-ping.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index d722c962..3db50d90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,10 @@ the release PR, and frozen at the tag — never an auto-generated commit list. ### Agents +- **Removed yellow lines above terminal panes.** The [pane overlays](src/styles/06-stage-panes.css) + no longer animate while agents work or when selecting an agent from the sidebar. + Agent status indicators and pane navigation keep their existing behavior. + - **Agent context above each terminal pane.** The [compact header](src/terminal/pane-agent-header.tsx) shows the agent logo and the same latest message as the sidebar. Claude Code panes offer an Effort diff --git a/docs/DESIGN-LANGUAGE.md b/docs/DESIGN-LANGUAGE.md index 0a8e908a..de0d9c30 100644 --- a/docs/DESIGN-LANGUAGE.md +++ b/docs/DESIGN-LANGUAGE.md @@ -36,25 +36,19 @@ constraint: **consume as few machine resources as possible.** - **DL-1.2** Animate only `transform`, `opacity`, `color`, `border-color`, `background-color`. Max duration 300ms. No infinite / looping animations. Nothing animates while the user is idle. - **The first exception this rule carried was withdrawn (2026-08-17).** From - 2026-08-16 the focus ping was allowed 1500ms as a locator (DL-27.7); the - owner then deleted the ping outright — a rail click moves focus and says - nothing else. **One new scoped exception was added 2026-08-19 by DL-18.11:** - a rail click runs one 1500ms locator, while each recognised working agent - repeats that same 1500ms current until its tracker stops. The latter is an - infinite CSS animation only while `.is-agent-working` exists — the state - removal ends it, and nothing moves while the agent is idle. - **A second scoped exception was added 2026-08-25 by DL-27.3:** the rail's + **Pane top-edge motion was removed on 2026-09-18 (DL-18.11).** Neither + agent activity nor rail selection draws a yellow line over the terminal + ([pane styles](../src/styles/06-stage-panes.css)). + **A scoped exception was added 2026-08-25 by DL-27.3:** the rail's `asked` mark radiates on a 1.8s loop for as long as that state is on the - element. It is the same shape as the one above — infinite only while a state - exists, `transform` and `opacity` only — with one difference stated rather - than glossed: "nothing animates while the user is idle" does NOT hold for it, + element. It uses only `transform` and `opacity` and loops only while that + state exists. "Nothing animates while the user is idle" does NOT hold for it, because an unread mark exists precisely while nobody is watching. That is the point of the mark and the whole cost of the exception; it was taken knowingly, over a still ring that separates the mark in a screenshot but not in the corner of an eye. The still ring is what `prefers-reduced-motion: reduce` gets. - **A third scoped exception was added 2026-08-27 by DL-27.25:** an Electron + **Another scoped exception was added 2026-08-27 by DL-27.25:** an Electron worktree-card row or strip carries three `transform: scaleY` bars while its agent is working (DECK-30, amended 2026-09-09). The 6/10/8px bars share a 900ms alternate cycle with a 150ms stagger; there is no beam. Leaving working @@ -114,11 +108,12 @@ from the active terminal theme (`--bg --fg --accent --red --green --yellow **One exception, added 2026-08-19:** a background somebody has hand-picked a sidebar for may pin it, as a literal keyed by that background ([`PINNED_SIDEBAR_BG`](../src/lib/derive-colors.ts) `current`). Deck's own - `#17181c` pins `#161b22` at the owner's request (`#272d31` until 2026-08-25), - because that gray is not reachable by mixing the background toward white — a - lift raises every channel, and this one drops red while raising blue. The pin is keyed on the background rather than on the - preset so the rule above still holds for all four callers, and so overriding - that background correctly drops the pin. + dark mode uses a neutral-black `#0a0a0a` pane and a lighter `#141414` + sidebar (2026-09-18, owner's direction); bars and popovers rise from that + sidebar through the existing chrome steps. The former `#17181c` → `#161b22` + pin remains for legacy background overrides. Each pin is keyed on the + background rather than the preset so all four callers agree, and overriding + the background correctly drops its pin. - **DL-2.3** **A boundary between two surfaces is a seam, not a hairline.** Seams mix from `--tone`, never from `--fg`: a boundary belongs to the background ladder, and mixing from the foreground let the terminal's text hue @@ -176,17 +171,11 @@ from the active terminal theme (`--bg --fg --accent --red --green --yellow - **DL-3.1** `--accent` marks **interactive or active** only: hover/focus borders, focus ring, active markers, affordance hints. Never a decorative - fill, never large areas. **DL-18.11 is one scoped exception:** pane activity - and the rail's pane locator use `--yellow`, not `--accent`; neither becomes - Deck's general focus colour. + fill, never large areas. - **DL-3.2** `--green` means only _on / enabled / success_. `--red` means only _danger / destructive / error_. `--yellow` means only _needs your eyes_ — attention a person must act on, one step below `--red`'s failure. Never decoration, none of the three. - **Amended 2026-08-19 by DL-18.11:** a working agent may spend yellow until it - stops, and a rail click may spend it on one 1500ms pane locator. These are - activity/navigation signals, not persistent focus state; buttons, fields - and every other focus ring remain accent. `--yellow` was **added 2026-08-16** with the agent status rail (DL-27.6), and **widened later the same day** (owner): it covers a finished run nobody has checked as well as a question or a permission wait — the rail's old @@ -946,7 +935,7 @@ answered here rather than re-argued per button. presentation is set: `color="currentColor"`, the weight, `aria-hidden`, `focusable="false"`, plus the unconditional `deck-icon` class the stylesheet's one icon rule hangs off. Icons are imported by name. - The rail's project `Folder` uses the surface-scoped `filled` prop in + The rail's fallback project `Folder` uses the surface-scoped `filled` prop in [both live and remembered headers](../src/ui/agent-rail.tsx) (DL-27.17). Nothing else authors an ``, and no glyph character stands in for an action — `scripts/icon-system.test.ts` enforces both. @@ -1458,14 +1447,21 @@ window's identity and its actions at the same time. crossed the edge repeatedly and the cursor flickered with it. Same figures on the docked column's seam (DL-19.4) — one gesture, one look, both edges. - **Amended 2026-08-19 (owner): `New` joins the open column's frame, directly - after the hide control.** The two controls form one compact leading cluster - ([`SidebarFrameActions`](../src/ui/sidebar-toggle.tsx) `current`); moving the - launcher out of the scrolling rail keeps the action visible regardless of - how many projects are open. It leaves with the column when the sidebar is - hidden, while the one control needed to restore that column still moves to - the stage strip. The feature toolbar remains on the stage side; this - amendment adds one sidebar action, not a second toolbar. + **Amended 2026-09-18 (owner): the hide control and Deck identity share the + top row; `New Workspace` occupies the row below.** + [`SidebarFrameActions`](../src/ui/sidebar-toggle.tsx) pairs the hide control + on the left with the canonical Deck logo, `Deck` label and running version + (`V1.2.3`) aligned to the right. The gap remains a window drag region; a + narrow sidebar truncates the version, with its full value in a tooltip. + Local Electron builds replace the version with an accent-filled `DEV` + badge, identified by `isDevelopment` in the validated + [desktop environment](../src/lib/platform.ts). + A 1px `--hair-strong` bottom border separates this row from navigation in + [the shell stylesheet](../src/styles/02-shell.css). The + [`AgentRail`](../src/ui/agent-rail.tsx) pins the existing launcher above its + project scrollport, preserving click and pane-drag behavior. Both rows leave + with the column when hidden; the restore control moves to the stage strip. + The feature toolbar remains on the stage side. **Cross-reference, 2026-09-03, WITHDRAWN 2026-09-09 (DECK-43):** the hidden state was briefly PRODUCED by a surface rather than by the user — the Agent @@ -1550,27 +1546,12 @@ window's identity and its actions at the same time. with the sentence). Documents and the browser keep their own names; they have no turn to report. -- **DL-18.11** **Rail focus is one yellow current; agent work is the same - current until stop (2026-08-19, owner).** These are two distinct triggers. - [`pingPane`](../src/terminal/pane-ping.ts) `current` is called only after an - Agent Rail row activates its exact pane: it replaces one inert `.pane-ping` - node inside that pane's positioned `.pane-slot`, so the shared current spans - only that pane's top edge and replays once for 1500ms, including for an idle - agent. Ordinary terminal focus, tab selection and attention-keyboard - navigation do not trigger that locator; no locator is anchored to the tab. - - Separately, [`syncViews`](../src/terminal/tab-manager.ts) `current` reflects - every exact pane's recognised-agent tracker phase as `.is-agent-working`. - [`.pane.is-agent-working::after`](../src/styles/06-stage-panes.css) `current` - repeats a 1500ms two-packet charge across a 2px yellow base until the tracker - removes that class; it does not depend on which pane holds focus, so multiple - running agents can each report their own activity and an idle agent or shell - cannot keep it alive. Both paths animate only `transform` and `opacity`, take - no pointer events and change no pane geometry. Under - `prefers-reduced-motion: reduce`, the click locator is absent and a working - pane keeps only its static 2px yellow state. These were DL-1.2's only motion - exceptions until 2026-08-25, when DL-27.3's unread ripple became the third; - they remain the scoped yellow exceptions recorded in DL-3.1/DL-3.2. +- **DL-18.11** **No pane top-edge activity or focus effect (2026-09-18, owner).** + The continuous working line and the one-shot rail-click locator are removed + from [pane styles](../src/styles/06-stage-panes.css). No replacement effect + is added. [Rail selection](../src/ui/app.tsx) still activates the exact pane; + [agent phase and attention](../src/terminal/tab-manager.ts) still supply the + existing rail and Board indicators. Pane geometry is unchanged by this removal. - **DL-18.12** **A pane is a card on the stage gutter (2026-09-18, owner).** `--stage-gutter` is 4px and applies to all four edges of the work area AND @@ -1762,6 +1743,9 @@ direction token rebuild §9.4 membership (owner, 2026-08-17, with the chips' turn text). Amended 2026-09-09 (DECK-39): [strip tabs and view buttons](../src/styles/05-tab-bar-toolbar.css) use `--radius-tab` (6px); other compact details retain the 2px default. + Amended 2026-09-18 (owner): `--radius-tab`'s membership now also covers the + stage's panes and document surface (DL-18.12), so the 6px corner is a shared + role rather than the strip's private number. `--radius-tight` (8px) is anything drawn inside a control or row — marks, bars, scrollbar thumbs and miniature parts. `--radius-control` (10px) is anything the pointer acts on inside a surface — rows, pills, icon buttons @@ -1771,9 +1755,6 @@ direction token rebuild §9.4 its own. The tight role joined the earlier control/surface pair on 2026-08-16, when surface came down from 16px so the closed scale reads 8/10/12. A value chosen by feel at a use site is not part of this scale; - Amended 2026-09-18 (owner): `--radius-tab`'s membership now also covers the - stage's panes and document surface (DL-18.12), so the 6px corner is a shared - role rather than the strip's private number. `border-radius: 50%` and the 999px capsule stay shapes rather than scale values. - **DL-20.2** One motion pair for chrome state change: `--duration` (150ms) and @@ -2348,9 +2329,9 @@ a 1.5s effect. The ping is the inset hairline DL-1.3 explicitly permits. read was an accent flash with no obvious cause, and the then-active-pane bar (`.pane-slot.is-active .pane::before`, superseded by DL-18.11) already said which pane held the keys permanently. The original ring was deleted at that - point. `pane-ping.ts` returned on 2026-08-19 for DL-18.11's top-edge yellow - line, not this ring, and its current behaviour is governed there. The number - is kept here so a future locator does not cite the withdrawn rule. + point. The top-edge replacement was also removed on 2026-09-18 under + DL-18.11 ([pane styles](../src/styles/06-stage-panes.css)). The number is kept + here so a future locator does not cite the withdrawn rule. - **DL-27.8** **The selection wash stops at the tab row (amended 2026-08-16).** It originally covered the whole item; with DL-27.13's pane tree inside the item, that painted the wash over the leaves and their guides, and the owner @@ -2520,17 +2501,16 @@ a 1.5s effect. The ping is the inset hairline DL-1.3 explicitly permits. guides — so the rail shows only agents and projects. This rule, the markup and the CSS all stand; restoring the tree is flipping that one constant. -- **DL-27.14** **`New` is the sidebar frame's launcher beside the hide control, - and it can also be picked up (2026-08-16; moved into the frame 2026-08-19, - owner).** It began as the rail's last row, moved to its first row on - 2026-08-17, then left the scrolling list entirely: a primary action must not - disappear because the project list grew. The rail now starts with live work, - while [`SidebarFrameActions`](../src/ui/sidebar-toggle.tsx) `current` owns - the compact `toggle → New` cluster. Clicked, `New` opens the board; dragged - onto a pane, it docks an agent pane at that pane's nearest edge - ([`new-pane-drag.ts`](../src/ui/new-pane-drag.ts) `current`). One control, - two verbs, because both answer the same question — "another one, where?" — - and a second row would have said the same word twice. The drag reuses the +- **DL-27.14** **`New Workspace` is pinned below the sidebar identity row, and it can + also be picked up (amended 2026-09-18, owner).** + [`SidebarFrameActions`](../src/ui/sidebar-toggle.tsx) owns the compact + `toggle → logo → Deck` row; [`AgentRail`](../src/ui/agent-rail.tsx) places + the same `SidebarNewButton` beneath it, outside the project scrollport, + with a 1px `--hair-strong` border in [the shell stylesheet](../src/styles/02-shell.css). + Clicked, `New Workspace` opens the board; dragged onto a pane, it docks an agent pane + at that pane's nearest edge ([`new-pane-drag.ts`](../src/ui/new-pane-drag.ts)). + One control, + two verbs, because both answer the same question — "another one, where?". The drag reuses the pane drag's own vocabulary exactly: the 5px threshold that separates a click from a grab, the `.pane-drag-ghost` label following the cursor, and the half-pane `.drop-overlay` naming the edge the drop will take. No new visual @@ -2602,12 +2582,15 @@ a 1.5s effect. The ping is the inset hairline DL-1.3 explicitly permits. route back to running work. The right dock is likewise suppressed while Open Board owns the stage, without changing its saved open tab or visibility. -- **DL-27.17** **A project header reads folder → name → trailing caret - (2026-08-19, owner).** `Folder` establishes that the group is a workspace; - the caret sits at the far edge so expansion is a predictable trailing - affordance rather than punctuation before the name. Both glyphs are +- **DL-27.17** **A project header reads favicon → name → trailing caret + (amended 2026-09-18, owner).** The workspace favicon identifies the project; + a missing, unreadable or broken image falls back to the filled `Folder`. + Favicons come from the existing [workspace scanner](../electron/images.ts), + using the repository root or the plain workspace folder. + The caret sits at the far edge so expansion is a predictable trailing + affordance rather than punctuation before the name. Both images are decorative; the button's accessible name carries the expand/collapse action. - The folder uses `FEATURE_ICON` (15px) with `filled` in + The favicon and fallback use `FEATURE_ICON` (15px), with `filled` for the folder in [the header component](../src/ui/agent-rail.tsx). Its 17px column remains separate from checkout content; the icon-to-name gap is 6px and the name is 14px (DL-27.9), set in [the stylesheet](../src/styles/04a-agent-rail.css). @@ -2772,9 +2755,8 @@ a 1.5s effect. The ping is the inset hairline DL-1.3 explicitly permits. active pane of its own, so the rail model ANDs a pane's focus with its tab's selection rather than reporting each tab's local answer, and that invariant lives in the pure model where it can be asserted. Selection outranks hover - (DL-21.2); the mark spends no `--yellow`, which DL-18.11 keeps for activity - and the 1500ms locator — a locator says _look here now_ and this says _the - keys are here_. **A document or the browser on the stage does not clear it:** + (DL-21.2); the mark spends no `--yellow` and identifies where the keyboard + is directed. **A document or the browser on the stage does not clear it:** the active pane is unchanged and the mark then reads as where the keyboard returns to, where clearing it would blink the rail on every file opened. - **DL-27.23** **The rail has three tiers: project, worktree, agent row diff --git a/scripts/design-language.test.ts b/scripts/design-language.test.ts index f74fbdd8..6bbe3af9 100644 --- a/scripts/design-language.test.ts +++ b/scripts/design-language.test.ts @@ -460,47 +460,13 @@ describe("design-language feature glyph treatment", () => { }); }); -describe("active pane focus current", () => { - it("loops the yellow current while the focused agent is working", () => { +describe("pane activity chrome", () => { + it("keeps working and rail-focus top-edge overlays removed", () => { const css = readStylesheet().replace(CSS_COMMENT, ""); - const base = css.match(/\.pane\.is-agent-working::before\s*\{([^}]*)\}/)?.[1] ?? ""; - const current = css.match(/\.pane\.is-agent-working::after\s*\{([^}]*)\}/)?.[1] ?? ""; - - expect(css).toMatch(/--pane-focus-current-duration:\s*1500ms\s*;/); - expect(css).not.toMatch(/\.is-active \.pane\.is-agent-working::before/); - expect(base).not.toMatch(/animation\s*:/); - expect(current).toMatch( - /animation:\s*pane-focus-current var\(--pane-focus-current-duration\) linear infinite\s*;/, - ); - }); - - it("runs a sidebar-click locator once for 1.5s", () => { - const css = readStylesheet().replace(CSS_COMMENT, ""); - const paneSlot = css.match(/\.pane-slot\s*\{([^}]*)\}/)?.[1] ?? ""; - const locator = css.match(/\.pane-ping\s*\{([^}]*)\}/)?.[1] ?? ""; - const locatorCurrents = [...css.matchAll(/\.pane-ping::after\s*\{([^}]*)\}/g)].map( - (match) => match[1], - ); - - expect(paneSlot).toMatch(/position:\s*relative\s*;/); - expect(locator).toMatch( - /animation:\s*pane-ping-line var\(--pane-focus-current-duration\) linear 1 both\s*;/, - ); - expect( - locatorCurrents.some((body) => - /animation:\s*pane-focus-current var\(--pane-focus-current-duration\) linear 1 both\s*;/.test( - body, - ), - ), - ).toBe(true); - }); - - it("freezes working to a static line and removes the locator for reduced motion", () => { - const css = readStylesheet().replace(CSS_COMMENT, ""); - - expect(css).toMatch( - /@media \(prefers-reduced-motion: reduce\)\s*\{[\s\S]*?\.pane\.is-agent-working::after,[\s\S]*?\.pane-ping\s*\{[^}]*animation:\s*none\s*;[^}]*opacity:\s*0\s*;/, - ); + expect(css).not.toContain(".pane.is-agent-working"); + expect(css).not.toContain(".pane-ping"); + expect(css).not.toContain("pane-focus-current"); + expect(css).not.toContain("pane-ping-line"); }); }); diff --git a/src/styles/01-tokens.css b/src/styles/01-tokens.css index d64180d0..84a4f85e 100644 --- a/src/styles/01-tokens.css +++ b/src/styles/01-tokens.css @@ -178,9 +178,6 @@ and still inside DL-1.2's 300ms ceiling. */ --duration: 150ms; --ease: cubic-bezier(0.4, 0, 0.2, 1); - /* DL-18.11: one cycle of the shared pane current. A rail-click locator runs - it once; a focused working agent repeats it until the tracker stops. */ - --pane-focus-current-duration: 1500ms; } /* Flat design rule: no drop shadows anywhere in this file. diff --git a/src/styles/06-stage-panes.css b/src/styles/06-stage-panes.css index c46e53c2..fde4cf73 100644 --- a/src/styles/06-stage-panes.css +++ b/src/styles/06-stage-panes.css @@ -461,20 +461,14 @@ /* ── Pane ────────────────────────────────────────────────── */ .pane-slot { - /* Local containing block for the rail-click locator. Without this, - `.pane-ping { position: absolute }` climbs to the tab/stage ancestor and - draws across the top of the whole tab instead of this exact pane. */ position: relative; display: flex; flex: 1 1 0; - /* Same corner as the pane it holds (DL-18.12): `pingPane` appends - `.pane-ping` to the SLOT, not to the pane, so the locator's 2px line would - otherwise draw square ends out over the gutter where the pane's curve has - already receded. */ - border-radius: var(--radius-tab); min-width: 0; min-height: 0; overflow: hidden; + /* Match the pane card corner (DL-18.12). */ + border-radius: var(--radius-tab); } .pane { @@ -486,12 +480,9 @@ flex-direction: column; overflow: hidden; background: var(--bg); -} /* 6px (owner, 2026-09-18). The existing `overflow: hidden` is what makes it visible: xterm's canvases fill the pane and would otherwise paint over the - rounded corners, and the same clip keeps `.is-agent-working`'s own - top-edge overlays inside the curve. `.pane-ping` is NOT one of them — it - hangs off `.pane-slot`, which carries the matching radius above. */ + rounded corners. */ border-radius: var(--radius-tab); /* The card's own edge, added the same day the gutter was: `--sidebar-bg` is only a few luminance units off `--bg`, so on a dark theme the gutter alone @@ -505,118 +496,6 @@ The 1px costs the terminal nothing — the grid re-fits to the padding box and a cell is never that small. */ border: 1px solid var(--seam-split); - -/* The pane's two yellow signals share one visual language but not one life: - a rail click appends `.pane-ping` for a single 1.5s locator; any pane whose - agent is working keeps the current cycling until the tracker stops. - It used to be a full inset ring, and the ring was the problem: three of its - four sides landed on top of something else — the pane bar covered the top, - the splits covered both flanks — so the only side standing on open ground - was the BOTTOM one, and a lone accent line floating above the empty tail of - a terminal reads as a rendering artefact, not as "this pane has the keys". - The owner asked about it twice before it was touched. - - Both use the TOP edge instead. They are overlays, not borders: `.pane` is a - flex child sized by the split, so a border would take pixels off the - terminal and cost a PTY resize every time a signal appeared. - - z-index 12 because xterm's own layers run to 11 inside `.pane`, and neither - `.pane-slot` nor `.pane` opens a stacking context, so an `auto` line would - paint under the terminal. */ -.pane.is-agent-working::before { - content: ""; - position: absolute; - inset: 0 0 auto 0; - height: 2px; - z-index: 12; - background: color-mix(in srgb, var(--yellow) 72%, transparent); - pointer-events: none; -} - -/* The rail-click locator exists independently of `working`: selecting an idle - agent still identifies where focus landed. It replaces its DOM node on each - click, so the one-shot animation always restarts without a timer. */ -.pane-ping { - position: absolute; - inset: 0 0 auto 0; - z-index: 13; - height: 2px; - background: color-mix(in srgb, var(--yellow) 72%, transparent); - opacity: 0; - pointer-events: none; - animation: pane-ping-line var(--pane-focus-current-duration) linear 1 both; -} - -/* Ghostty-style charge: two bright packets cross either the working line or - the one-shot locator. Only transform and opacity animate, so terminal - geometry and paint order stay fixed while the current travels. */ -.pane.is-agent-working::after, -.pane-ping::after { - content: ""; - position: absolute; - inset: 0 auto auto 0; - z-index: 13; - width: 48%; - height: 3px; - background: linear-gradient( - 90deg, - transparent 0 8%, - color-mix(in srgb, var(--yellow) 42%, transparent) 14%, - var(--yellow) 19%, - color-mix(in srgb, var(--yellow) 58%, var(--tone)) 22%, - var(--yellow) 25%, - transparent 31% 62%, - color-mix(in srgb, var(--yellow) 40%, transparent) 68%, - var(--yellow) 74%, - transparent 82% 100% - ); - pointer-events: none; -} - -.pane.is-agent-working::after { - animation: pane-focus-current var(--pane-focus-current-duration) linear infinite; -} - -.pane-ping::after { - animation: pane-focus-current var(--pane-focus-current-duration) linear 1 both; -} - -@keyframes pane-ping-line { - 0%, - 100% { - opacity: 0; - } - 7% { - opacity: 1; - } - 74% { - opacity: 0.72; - } -} - -@keyframes pane-focus-current { - 0% { - opacity: 0; - transform: translateX(-110%); - } - 6% { - opacity: 1; - } - 70% { - opacity: 0.96; - } - 100% { - opacity: 0; - transform: translateX(310%); - } -} - -@media (prefers-reduced-motion: reduce) { - .pane.is-agent-working::after, - .pane-ping { - animation: none; - opacity: 0; - } } .pane__bar { diff --git a/src/terminal/pane-ping.test.ts b/src/terminal/pane-ping.test.ts deleted file mode 100644 index bbfe5f17..00000000 --- a/src/terminal/pane-ping.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -// @vitest-environment jsdom -import { afterEach, describe, expect, it } from "vitest"; -import { pingPane } from "./pane-ping"; - -function grid(...paneIds: readonly number[]): HTMLElement { - const container = document.createElement("div"); - for (const id of paneIds) { - const slot = document.createElement("div"); - slot.className = "pane-slot"; - slot.dataset.paneId = String(id); - container.appendChild(slot); - } - return container; -} - -function slot(root: ParentNode, paneId: number): HTMLElement { - const found = root.querySelector(`.pane-slot[data-pane-id="${paneId}"]`); - if (found === null) { - throw new Error(`no slot for pane ${paneId}`); - } - return found; -} - -function pings(root: ParentNode): readonly Element[] { - return [...root.querySelectorAll(".pane-ping")]; -} - -afterEach(() => { - document.body.replaceChildren(); -}); - -describe("pingPane", () => { - it("appends one inert locator inside the named pane slot", () => { - const root = grid(1, 2); - - pingPane(2, root); - - expect(pings(slot(root, 1))).toHaveLength(0); - const ping = pings(slot(root, 2))[0]; - expect(ping.tagName).toBe("SPAN"); - expect(ping.getAttribute("aria-hidden")).toBe("true"); - }); - - it("replaces the locator so clicking the same rail agent replays it", () => { - const root = grid(1); - - pingPane(1, root); - const first = pings(slot(root, 1))[0]; - pingPane(1, root); - - const after = pings(slot(root, 1)); - expect(after).toHaveLength(1); - expect(after[0]).not.toBe(first); - }); - - it("clears a locator left in another pane", () => { - const root = grid(1, 2); - - pingPane(1, root); - pingPane(2, root); - - expect(pings(slot(root, 1))).toHaveLength(0); - expect(pings(slot(root, 2))).toHaveLength(1); - }); - - it("does nothing when the target pane disappeared", () => { - const root = grid(1); - pingPane(1, root); - const live = pings(slot(root, 1))[0]; - - expect(() => pingPane(99, root)).not.toThrow(); - expect(pings(root)).toHaveLength(1); - expect(pings(slot(root, 1))[0]).toBe(live); - }); - - it("uses the document when no root is supplied", () => { - document.body.appendChild(grid(3)); - - pingPane(3); - - expect(pings(slot(document, 3))).toHaveLength(1); - }); -}); diff --git a/src/terminal/pane-ping.ts b/src/terminal/pane-ping.ts deleted file mode 100644 index 32f6273c..00000000 --- a/src/terminal/pane-ping.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Replay the rail-click locator on one exact pane. - * - * Pane slots are owned by the imperative layout engine, so the locator is a - * small inert DOM node rather than Preact state. Replacing the node restarts - * its one-shot CSS animation even when the same agent row is pressed twice. - */ - -const PING_CLASS = "pane-ping"; - -export function pingPane(paneId: number, root: ParentNode = document): void { - const slot = root.querySelector(`.pane-slot[data-pane-id="${paneId}"]`); - if (slot === null) { - return; - } - for (const stale of root.querySelectorAll(`.${PING_CLASS}`)) { - stale.remove(); - } - const ping = document.createElement("span"); - ping.className = PING_CLASS; - ping.setAttribute("aria-hidden", "true"); - slot.appendChild(ping); -} diff --git a/src/terminal/tab-manager.tab-lifecycle.test.ts b/src/terminal/tab-manager.tab-lifecycle.test.ts index decf3ca6..19282ebc 100644 --- a/src/terminal/tab-manager.tab-lifecycle.test.ts +++ b/src/terminal/tab-manager.tab-lifecycle.test.ts @@ -255,7 +255,9 @@ describe("createTabManager workspace identity", () => { const shellPane = document.querySelector( '.pane-slot[data-pane-id="2"] > .pane__term', ); - expect(agentPane?.classList.contains("is-agent-working")).toBe(true); + expect(agentPane).not.toBeNull(); + expect(shellPane).not.toBeNull(); + expect(agentPane?.classList.contains("is-agent-working")).toBe(false); expect(shellPane?.classList.contains("is-agent-working")).toBe(false); expect(tabViews.value[0].panes).toEqual([ @@ -321,6 +323,7 @@ describe("createTabManager workspace identity", () => { expect(tabViews.value[0].panes?.[1].attention).toBe("none"); pty.emitOutput(1, "done.\x1b]9;4;0\x07"); + expect(tabViews.value[0].panes?.[0].phase).toBe("idle"); expect(agentPane?.classList.contains("is-agent-working")).toBe(false); tm.dispose(); diff --git a/src/terminal/tab-manager.ts b/src/terminal/tab-manager.ts index 56fa46b9..ad0ad8a8 100644 --- a/src/terminal/tab-manager.ts +++ b/src/terminal/tab-manager.ts @@ -532,7 +532,6 @@ export function createTabManager( const panes: readonly PaneView[] = paneIds.map((id) => { const snap = tracker.snapshot(id); const live = explicitAgent(poller.infoFor(id)); - tab.manager.setPaneWorking(id, live !== null && snap?.phase === "working"); // An agent that ENDED keeps its name on the projection until the end // is acknowledged (stage 0, 2026-09-03): the poller says "shell" the // moment the agent leaves the foreground, and a row with no agent is diff --git a/src/terminal/terminal-manager-types.ts b/src/terminal/terminal-manager-types.ts index 8fb39ce0..790241e0 100644 --- a/src/terminal/terminal-manager-types.ts +++ b/src/terminal/terminal-manager-types.ts @@ -241,8 +241,6 @@ export interface TerminalManager { paneCount(): number; /** Root element of a pane (overlay anchor for the agent picker). */ paneElement(id: number): HTMLElement | null; - /** Reflect exact per-pane agent activity for state-gated chrome. */ - setPaneWorking(id: number, working: boolean): void; /** Routed from the tab manager's single pty:output listener; ignores unowned ids. */ handleOutput(id: number, data: string): void; /** Routed from the tab manager's single pty:exit listener; ignores unowned ids. */ diff --git a/src/terminal/terminal-manager.ts b/src/terminal/terminal-manager.ts index b6c9cdf9..cfaba3e5 100644 --- a/src/terminal/terminal-manager.ts +++ b/src/terminal/terminal-manager.ts @@ -850,9 +850,6 @@ export function createTerminalManager( paneElement(id) { return life.panes.get(id)?.element ?? null; }, - setPaneWorking(id, working) { - life.panes.get(id)?.element.classList.toggle("is-agent-working", working); - }, handleOutput(id, data) { life.panes.get(id)?.write(data); }, diff --git a/src/ui/app.tsx b/src/ui/app.tsx index b3eb02c7..2ac3c78f 100644 --- a/src/ui/app.tsx +++ b/src/ui/app.tsx @@ -39,7 +39,6 @@ import { agentOptions, agentProcessMatchers, probeNames } from "../lib/agent-cat import { resolveTheme } from "../settings/themes"; import { isShortcutAction } from "../terminal/keymap"; import { createTabManager, type TabManager } from "../terminal/tab-manager"; -import { pingPane } from "../terminal/pane-ping"; import { activeTabIndex, tabViews } from "../terminal/tabs-store"; import { presetsData, savePreset } from "../presets/presets-store"; import { recordWorkspaceOpen, removeWorkspaceRecents } from "../open-board/workspaces-store"; @@ -376,10 +375,6 @@ export function App({ boot = { kind: "normal" } }: { boot?: BootMode } = {}) { * `activateForAttention`, which activates exactly this pane and acks only * it, never `focusNextAttention`, which would pick the loudest pane in the * window instead of the one that was pressed. - * - * The 1.5s locator follows the focus in the same synchronous tick: the rail - * has just dropped the user into a grid of identical panes, and without an - * answer they have to re-find the thing they asked for (DL-18.11). */ const focusRailPane = (index: number, paneId: number): void => { runAttentionFocus({ @@ -400,7 +395,6 @@ export function App({ boot = { kind: "normal" } }: { boot?: BootMode } = {}) { }, focusAttention: () => { tabsRef.current?.activateForAttention(index, paneId); - pingPane(paneId); }, }); }; From fe36ef336477904daa0ba9fb63af2fb74057b02d Mon Sep 17 00:00:00 2001 From: mxrsv Date: Sat, 19 Sep 2026 01:10:01 +0700 Subject: [PATCH 3/3] docs(dev): describe the incremental Electron dev watcher `electron:dev` has been the watcher since b3180fd, with `electron:dev:watch` an alias and `electron:dev:once` the build-and-launch path, but the three pages a contributor reads still described the old build-then-launch behaviour. States what the watcher rebuilds incrementally and that a main/preload change restarts Electron, interrupting live terminals and unsaved buffers. Claude-Session: https://claude.ai/code/session_01AC7TvhjEXzJFMYVk9HAVbR --- AGENTS.md | 5 +++-- README.md | 5 +++++ docs/operations/development.md | 14 ++++++++++++-- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b0c7084c..819cf259 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -67,8 +67,9 @@ turned down and why — a fork nobody can reconstruct is a decision that will be | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `npm run dev` | browser-only Vite preview; IPC operations fail soft | | `npm run tauri dev` | the Tauri host | -| `npm run electron:dev` | the Electron host, built and launched from `dist-electron/` | -| `npm run electron:dev:watch` | same host with hot reload: renderer on the Vite dev server, main rebuilds and relaunches on save via [`scripts/electron-dev-watch.mjs`](scripts/electron-dev-watch.mjs) | +| `npm run electron:dev` | Electron dev with renderer HMR and incremental main/preload rebuilds via [`scripts/electron-dev-watch.mjs`](scripts/electron-dev-watch.mjs) | +| `npm run electron:dev:watch` | alias for `electron:dev` | +| `npm run electron:dev:once` | build and launch once from the static renderer bundle through [`scripts/electron-dev-launch.mjs`](scripts/electron-dev-launch.mjs) | | `npm run electron:build` | typecheck and bundle the Electron main process | | `npm run electron:package` | package the Electron host as a local **unsigned** `Deck Electron.app` (arm64, `dir` target, no installer/updater/publish) into `dist-electron-app/` | | `npm run electron:smoke` | headed smoke test; needs a display server and a real PTY | diff --git a/README.md b/README.md index 669631e5..6e55bc5e 100644 --- a/README.md +++ b/README.md @@ -166,6 +166,11 @@ npm install npm run electron:dev ``` +The [development watcher](scripts/electron-dev-watch.mjs) updates the renderer through HMR +and incrementally rebuilds the Electron main process and preloads. Main/preload changes +restart the app and interrupt live terminal sessions. Use `npm run electron:dev:once` to +build and launch once from the static renderer bundle. + Create a local packaged macOS build with `npm run electron:package`. Release packaging is a separate signed and notarized workflow. diff --git a/docs/operations/development.md b/docs/operations/development.md index 3af1aa93..47ce07ef 100644 --- a/docs/operations/development.md +++ b/docs/operations/development.md @@ -16,8 +16,9 @@ whose only symptom when lost is `posix_spawnp failed`. | Command | What it does | | ------------------------------------ | -------------------------------------------------------------------------------------------------------------- | -| `npm run electron:dev` | Build the renderer and the main process, then launch Electron from `dist-electron/` | -| `npm run electron:dev:watch` | Same host with hot reload: the renderer loads the Vite dev server, main rebuilds and relaunches on save | +| `npm run electron:dev` | Renderer HMR and incremental main/preload compilation through the [development watcher](../../scripts/electron-dev-watch.mjs) | +| `npm run electron:dev:watch` | Alias for `electron:dev` | +| `npm run electron:dev:once` | Build the renderer and main process, then launch once from `dist-electron/` | | `npm run dev` | Browser-only Vite preview of the renderer; every IPC call fails soft | | `npm run tauri dev` | The frozen Tauri host | | `npm test` | The Vitest suite | @@ -40,6 +41,15 @@ whose only symptom when lost is `posix_spawnp failed`. `predev` and `prebuild` run `generate:menu`, so a plain `npm run build` always regenerates the Rust registry first. +The [Electron dev compiler](../../scripts/electron-dev-build.mjs) follows imports from main +and both preloads, including shared `src/` dependencies. It keeps output in +`dist-electron/dev/`; failed builds keep the previous app running. Renderer-only changes +stay with Vite. Main/preload or vendored browser-asset changes restart Electron, interrupting +live terminal sessions and unsaved editor buffers. The [watcher](../../scripts/electron-dev-watch.mjs) +waits for the old process to exit before launching again, and owns Vite until shutdown. +Renderer refresh does not guarantee preservation of terminal or editor state when a +component is remounted; use disposable sessions for development. + ## CI Each workflow runs on push to `main`, on pull requests and by hand, only for the paths it