{/* Full-page mode hides the transcript, so the header carries the only sign that a turn
is in flight — and the way back to the answer without leaving the mode for good. */}
{docMode && hasConversationContent && (
diff --git a/apps/desktop/src/projects/ProjectIcon.tsx b/apps/desktop/src/projects/ProjectIcon.tsx
index de5523fa..3b41e0eb 100644
--- a/apps/desktop/src/projects/ProjectIcon.tsx
+++ b/apps/desktop/src/projects/ProjectIcon.tsx
@@ -61,7 +61,7 @@ export function ProjectIcon({
data-project-icon
aria-hidden="true"
className={cn(
- "rounded-control bg-foreground/[0.055] text-muted-foreground ring-foreground/10 flex shrink-0 items-center justify-center overflow-hidden ring-1",
+ "rounded-control text-muted-foreground flex shrink-0 items-center justify-center overflow-hidden",
className
)}
style={{ width: size, height: size }}
diff --git a/apps/desktop/src/session/Composer.tsx b/apps/desktop/src/session/Composer.tsx
index 55f2ac25..9b1f2065 100644
--- a/apps/desktop/src/session/Composer.tsx
+++ b/apps/desktop/src/session/Composer.tsx
@@ -253,7 +253,7 @@ export function CheckoutBar({
aria-expanded={open}
>
-
+
{modeLabel}
+
{/* Document mode is the app's own feature — it deserves a control you can see, not just a
@@ -2022,7 +2036,7 @@ export function Composer({
>
{/* Title owns the row. Routine controls appear on demand. */}
}
@@ -1763,7 +1764,7 @@ export function SessionRail({
title={t(open ? "rail.hideProject" : "rail.showProject", {
name: project.name,
})}
- className="rounded-control text-ui focus-visible:focus-ring-inset flex min-w-0 flex-1 items-center gap-2 px-2 leading-4 outline-none hover:bg-transparent dark:hover:bg-transparent"
+ className="rounded-control text-body focus-visible:focus-ring-inset flex min-w-0 flex-1 items-center gap-2 leading-4 outline-none hover:bg-transparent dark:hover:bg-transparent"
>
diff --git a/apps/desktop/src/styles.css b/apps/desktop/src/styles.css
index c7332b40..06db3cb5 100644
--- a/apps/desktop/src/styles.css
+++ b/apps/desktop/src/styles.css
@@ -1793,10 +1793,6 @@ pre {
}
@container session-header (max-width: 36rem) {
- .session-header-actions {
- gap: var(--ds-space-surface-inset);
- }
-
.session-header-action-label {
display: none;
}
@@ -1814,7 +1810,6 @@ pre {
}
@container session-header (max-width: 28rem) {
- .session-header-project-icon,
.session-header-project-context,
.session-header [data-plugin-ui-slot="session.header"] {
display: none;
@@ -1823,10 +1818,6 @@ pre {
.session-header-title {
min-width: 3rem;
}
-
- .session-header-actions {
- gap: 0.25rem;
- }
}
@container dock (max-width: 640px) {
diff --git a/apps/desktop/tests/sessionHeaderActionsRendered.test.tsx b/apps/desktop/tests/sessionHeaderActionsRendered.test.tsx
index fe7bfed5..7dc4a2ae 100644
--- a/apps/desktop/tests/sessionHeaderActionsRendered.test.tsx
+++ b/apps/desktop/tests/sessionHeaderActionsRendered.test.tsx
@@ -111,7 +111,8 @@ describe("SessionHeaderActions", () => {
activateDom();
const { view } = renderActions();
const group = view.container.querySelector(".session-header-actions");
- expect(group?.classList.contains("gap-2")).toBe(true);
+ expect(group?.classList.contains("gap-inline")).toBe(true);
+ expect(group?.classList.contains("gap-2")).toBe(false);
expect(group?.classList.contains("rounded-control")).toBe(false);
expect(group?.classList.contains("p-0.5")).toBe(false);
diff --git a/apps/desktop/tests/sessionRailRendered.test.tsx b/apps/desktop/tests/sessionRailRendered.test.tsx
index 1a14feb1..1f38fa67 100644
--- a/apps/desktop/tests/sessionRailRendered.test.tsx
+++ b/apps/desktop/tests/sessionRailRendered.test.tsx
@@ -374,17 +374,15 @@ describe("SessionRail row layout", () => {
'[data-session-id="meaningful"]'
);
expect(groupedRow?.className).not.toContain("ml-6");
- expect(toggle?.className).toContain("px-2");
+ expect(toggle?.getAttribute("data-size")).toBe("row");
+ expect(toggle?.className).toContain("min-h-navigation-row");
+ expect(toggle?.className).toContain("px-module-inset");
expect(groupedRow?.className).toContain("px-2");
+ // One content column: the project mark, the nav marks and the session text all sit on the
+ // rail's single 16px edge, so the session content adds no padding of its own.
expect(
groupedRow?.querySelector("[data-session-content]")?.className
- ).toContain("pl-1.5");
- expect(
- groupedRow?.querySelector("[data-session-content]")?.className
- ).not.toContain("pl-2");
- expect(
- groupedRow?.querySelector("[data-session-content]")?.className
- ).not.toContain("pl-6");
+ ).not.toContain("pl-");
click(toggle);
await waitFor(() => {
@@ -1085,15 +1083,12 @@ describe("SessionRail row layout", () => {
).toBeTruthy();
expect(row?.querySelectorAll("[data-session-line]")).toHaveLength(2);
expect(row?.textContent).not.toContain("repo");
- expect(row?.querySelector("[data-session-content]")?.className).toContain(
- "pl-1.5"
- );
- expect(
- row?.querySelector("[data-session-content]")?.className
- ).not.toContain("pl-2");
expect(
row?.querySelector("[data-session-content]")?.className
- ).not.toContain("pl-6");
+ ).not.toContain("pl-");
+ expect(row?.querySelector("[data-session-content]")?.className).toContain(
+ "z-10"
+ );
view.unmount();
});
@@ -1158,18 +1153,41 @@ describe("SessionRail row layout", () => {
).toContain("Checkout");
await waitFor(() => {
- expect(
- view.container.querySelector(
- '[data-session-id="isolated"] [data-session-pull-request="merged"]'
- )?.textContent
- ).toContain("#84 Merged");
- expect(
- view.container.querySelector(
- '[data-session-id="regular"] [data-session-pull-request="ci_failed"]'
- )?.textContent
- ).toContain("#83 CI failed");
+ const merged = view.container.querySelector(
+ '[data-session-id="isolated"] [data-session-pull-request="merged"]'
+ );
+ const failed = view.container.querySelector(
+ '[data-session-id="regular"] [data-session-pull-request="ci_failed"]'
+ );
+ // The state travels by colour and the shared tooltip/accessible name, not by visible words.
+ expect(merged?.textContent).toContain("#84");
+ expect(merged?.textContent).not.toContain("Merged");
+ expect(merged?.getAttribute("title")).toContain("Merged");
+ expect(merged?.getAttribute("aria-label")).toContain("Merged");
+ expect(failed?.textContent).toContain("#83");
+ expect(failed?.textContent).not.toContain("CI failed");
+ expect(failed?.getAttribute("title")).toContain("CI failed");
});
+ // The provenance marks are inline meta, not pills: no fill and no pill padding, so their text
+ // edge matches the row title's edge on the shared content container.
+ for (const selector of [
+ '[data-session-id="isolated"] [data-session-checkout-kind="worktree"]',
+ '[data-session-id="regular"] [data-session-pull-request="ci_failed"]',
+ ]) {
+ const badge = view.container.querySelector(selector);
+ expect(badge?.classList.contains("bg-fill-quiet")).toBe(false);
+ expect(badge?.classList.contains("rounded-micro")).toBe(false);
+ expect(badge?.classList.contains("px-1")).toBe(false);
+ }
+ const row = view.container.querySelector('[data-session-id="regular"]');
+ const titleLine = row?.querySelector('[data-session-line="title"]');
+ const workspaceLine = row?.querySelector('[data-session-line="workspace"]');
+ expect(titleLine?.parentElement).toBe(workspaceLine?.parentElement);
+ expect(titleLine?.parentElement?.hasAttribute("data-session-content")).toBe(
+ true
+ );
+
view.unmount();
});
diff --git a/apps/desktop/tests/shellChromeContract.test.ts b/apps/desktop/tests/shellChromeContract.test.ts
new file mode 100644
index 00000000..20388b1e
--- /dev/null
+++ b/apps/desktop/tests/shellChromeContract.test.ts
@@ -0,0 +1,112 @@
+import { describe, expect, test } from "bun:test";
+import { readFileSync } from "node:fs";
+
+const normalizeSource = (source: string) => source.replaceAll(/\r\n?/gu, "\n");
+const readSource = (relativePath: string) =>
+ normalizeSource(
+ readFileSync(new URL(relativePath, import.meta.url), "utf-8")
+ );
+
+const app = readSource("../src/App.tsx");
+const headerActions = readSource("../src/session/SessionHeaderActions.tsx");
+const styles = readSource("../src/styles.css");
+const projectIcon = readSource("../src/projects/ProjectIcon.tsx");
+const rail = readSource("../src/sidebar/SessionRail.tsx");
+const composer = readSource("../src/session/Composer.tsx");
+
+describe("shell chrome contract", () => {
+ test("normalizes source contracts across platform line endings", () => {
+ expect(normalizeSource("first\r\nsecond\rthird")).toBe(
+ "first\nsecond\nthird"
+ );
+ });
+
+ test("keeps one titlebar gap across controls and clusters", () => {
+ const toolbarClass = /className="(session-header-toolbar [^"]+)"/u.exec(
+ app
+ )?.[1];
+ expect(toolbarClass?.split(" ")).toContain("gap-inline");
+ expect(toolbarClass).not.toContain("gap-4");
+ const actionsClass = /className="(session-header-actions [^"]+)"/u.exec(
+ headerActions
+ )?.[1];
+ expect(actionsClass?.split(" ")).toContain("gap-inline");
+ expect(actionsClass).not.toContain("gap-2");
+ expect(styles).not.toMatch(/\.session-header-actions\s*{[^}]*gap:/s);
+ });
+
+ test("renders the project mark without a tile fill or ring", () => {
+ expect(projectIcon).not.toContain("bg-foreground/[0.055]");
+ expect(projectIcon).not.toContain("ring-1");
+ expect(projectIcon).toContain(
+ '"rounded-control text-muted-foreground flex shrink-0 items-center justify-center overflow-hidden"'
+ );
+ });
+
+ test("renders workspace badges unfilled on the row's content edge", () => {
+ const checkout = rail.slice(
+ rail.indexOf("const checkoutBadge"),
+ rail.indexOf("const pullRequestBadge")
+ );
+ const pullRequest = rail.slice(
+ rail.indexOf("const pullRequestBadge"),
+ rail.indexOf("const provenanceInSummary")
+ );
+ for (const badge of [checkout, pullRequest]) {
+ expect(badge).not.toContain("bg-fill-quiet");
+ expect(badge).not.toContain("rounded-micro");
+ expect(badge).not.toContain("px-1");
+ }
+ expect(checkout).toContain("text-fine text-foreground/55");
+ expect(pullRequest).toContain("text-fine flex shrink-0 items-center");
+ expect(rail).toContain('className="pointer-events-none relative z-10"');
+ });
+
+ test("keeps one content edge across the rail's rows", () => {
+ // Every rail row's leading mark and text share the 16px column: the nav rows and the
+ // all-projects toggle reach it through the shared row size, the project header used to sit at
+ // 20 because an unsized Button fell back to the default control padding, and the session
+ // content added 6px on top of the row inset.
+ expect(rail).toContain('className="pointer-events-none relative z-10"');
+ expect(rail).not.toContain("pl-1.5");
+ expect(rail).toMatch(/variant="ghost"\s+size="row"\s+focusStyle="inset"/u);
+ expect(rail).toContain(
+ 'className="rounded-control text-body focus-visible:focus-ring-inset flex min-w-0 flex-1 items-center gap-2 leading-4 outline-none hover:bg-transparent dark:hover:bg-transparent"'
+ );
+ expect(rail).not.toContain("text-ui");
+ });
+
+ test("carries the pull-request state by colour, not by words", () => {
+ const badge = rail.slice(
+ rail.indexOf("const pullRequestBadge"),
+ rail.indexOf("const provenanceInSummary")
+ );
+ expect(badge).toContain("#{pullRequest.number}");
+ expect(badge).not.toContain("{pullRequestLabel}");
+ expect(badge).toContain("pullRequestTone");
+ expect(badge).toContain(
+ "aria-label={`#${pullRequest.number} · ${pullRequestLabel}`}"
+ );
+ });
+
+ test("drops the leading project mark and the rail selection bar", () => {
+ expect(app).not.toContain("session-header-project-icon");
+ expect(app).not.toContain('from "./projects/ProjectIcon"');
+ expect(styles).not.toContain(".session-header-project-icon");
+ expect(rail).not.toContain("before:inset-y-3");
+ expect(rail).not.toContain("before:rounded-full");
+ expect(rail).toContain("bg-fill-selected hover:bg-fill-selected-hover");
+ });
+
+ test("keeps one composer control row with the session chips in it", () => {
+ const spacer = composer.indexOf('');
+ const chips = composer.indexOf(" {
'"session-header window-titlebar electrobun-webkit-app-region-drag flex min-w-0 shrink-0 items-center gap-2 pr-4"'
);
expect(appSource).toContain(
- 'className="session-header-toolbar [&_svg]:text-muted-foreground flex min-w-0 shrink-0 items-center gap-4"'
+ 'className="session-header-toolbar [&_svg]:text-muted-foreground gap-inline flex min-w-0 shrink-0 items-center"'
);
expect(sessionHeaderActionsSource).toContain(
- 'className="session-header-actions flex shrink-0 items-center gap-2"'
+ 'className="session-header-actions gap-inline flex shrink-0 items-center"'
);
expect(sessionHeaderActionsSource).toContain(
"session-header-action-main bg-fill-rest text-foreground hover:bg-fill-hover hover:text-foreground"
diff --git a/docs/sdlc/changes/2026-09-16-quiet-shell-chrome/intent.md b/docs/sdlc/changes/2026-09-16-quiet-shell-chrome/intent.md
new file mode 100644
index 00000000..bd3c71fd
--- /dev/null
+++ b/docs/sdlc/changes/2026-09-16-quiet-shell-chrome/intent.md
@@ -0,0 +1,75 @@
+---
+id: 2026-09-16-quiet-shell-chrome
+schema: 5
+stage: intent
+status: accepted
+owner: chenli
+created: 2026-09-16
+source: user
+risk: medium
+approved_by: chenli
+approved_at: 2026-09-16
+approval_source: "Direct request: a screenshot of the running dev build with five red annotations (统一间距 / 去掉背景 / 对齐问题 / 去掉背景 / composer config row), confirmed through the follow-up questions for the ambiguous marks."
+next_trigger: chenli reviews the verified work.
+---
+
+# Intent: Quiet the app shell chrome
+
+## Intent
+
+The user reviewed the running dev build and marked five chrome problems in one annotated screenshot,
+then confirmed the two ambiguous marks in a follow-up exchange:
+
+1. the titlebar's right action cluster mixes 16px, 8px and 17px gaps and mixes filled and unfilled
+ controls ("统一间距");
+2. the project mark in the titlebar carries a filled tile with a ring ("去掉背景");
+3. the session row's workspace badge line does not left-align with the row title ("对齐问题");
+4. the two workspace badges render as filled pills ("去掉背景");
+5. the composer's session-config chips occupy their own row above the `+ / voice / send` row, and
+ the user wants them moved onto that row.
+
+Measured before the change (renderer at 1728px, empty shell): `.session-header-toolbar` `gap: 16px`,
+the session action group `gap: 8px`, and a 17px gap before the trailing layout control;
+`ProjectIcon` renders `bg-foreground/[0.055]` plus `ring-1 ring-foreground/10`; the checkout and
+pull-request badges render `bg-fill-quiet` with `px-1` pill padding; the composer footer stacks
+`SessionControls` above the `controls` row.
+
+Outcome: one 4px gap across the titlebar toolbar (the documented rhythm for independent 28px
+controls), a plain project mark, unfilled workspace badges whose text starts at the title's edge, and
+a composer whose config chips share the single control row with `+ / voice / send`.
+
+Follow-up review in the same session added four more marks on the running build:
+
+6. the rail's pull-request badge should carry its state by colour instead of the words
+ ("用颜色表示合并状态,不要文字了"); the checkout badge and the numbers stay;
+7. the session header's leading project mark should go entirely ("去掉前面的icon");
+8. the selected rail row's left selection bar should go ("去掉这个组件前面的线"); the selected fill
+ remains as the selection state;
+9. the checkout bar's chips should use the quiet grey label ("改成输入框里面的按钮灰色字");
+10. the sidebar still reads as misaligned ("明显对齐还有问题"), and the user asked for the check to run
+ against the real app through the web server instead of a screenshot ("对齐的用webserver检查").
+
+Measured on the web server against a copy of the running dev data dir (rail 288px, 1728px viewport):
+
+| element | before | after |
+| --- | --- | --- |
+| feature nav rows (icon) | 16 | 16 |
+| "All projects" label | 16 | 16 |
+| project header (folder / name) | 20 / 44 | 16 / 40 |
+| session title, summary, badges | 22 | 16 |
+
+The project header's Button carried no `size`, so it fell back to the default control padding whose
+`has-[>svg]:px-3` (12px) beat the local `px-2`; the session content added `pl-1.5` (6px) on top of the
+row's own inset. Outcome: one 16px content edge for every rail mark and every row's text, with the
+labels of the two header rows sharing the 40px text column.
+
+Constraints: no control is added, removed, or demoted — this is spacing, plane, and structure only.
+Keep every aria-label, tooltip, popover, and keyboard path, the rail's drag/context-menu behaviour,
+and both composer shapes (compact and document). No token, theme, or color value changes.
+
+Non-goals: the action-budget reduction (demoting low-frequency actions into overflow), the status
+vocabulary convergence, and the global spacing-vocabulary cleanup stay in their own records.
+
+## Non-goals
+
+No behavior, data, copy, or navigation change; no new component; no change to which controls exist.
diff --git a/docs/sdlc/changes/2026-09-16-quiet-shell-chrome/plan.md b/docs/sdlc/changes/2026-09-16-quiet-shell-chrome/plan.md
new file mode 100644
index 00000000..dc95e74a
--- /dev/null
+++ b/docs/sdlc/changes/2026-09-16-quiet-shell-chrome/plan.md
@@ -0,0 +1,63 @@
+---
+id: 2026-09-16-quiet-shell-chrome
+schema: 5
+stage: plan
+status: accepted
+owner: chenli
+created: 2026-09-16
+based_on: spec.md
+scope: apps/desktop/src/App.tsx, apps/desktop/src/session/SessionHeaderActions.tsx, apps/desktop/src/session/Composer.tsx, apps/desktop/src/sidebar/SessionRail.tsx, apps/desktop/src/projects/ProjectIcon.tsx, apps/desktop/src/styles.css, apps/desktop/tests/shellChromeContract.test.ts, apps/desktop/tests/sessionRailRendered.test.tsx, apps/desktop/tests/sessionHeaderActionsRendered.test.tsx, apps/desktop/tests/windowChromeContract.test.ts, apps/desktop/tests/composerGeometryContract.test.ts, docs/sdlc/changes/2026-09-16-quiet-shell-chrome
+---
+
+# Plan: Quiet the app shell chrome
+
+## Plan
+
+1. `apps/desktop/src/App.tsx` — `.session-header-toolbar` `gap-4` → `gap-inline`. (AC-1)
+2. `apps/desktop/src/session/SessionHeaderActions.tsx` — the action group `gap-2` → `gap-inline`;
+ the overflow trigger keeps its `size-7` control so the group stays on one 28px baseline. (AC-1)
+3. `apps/desktop/src/styles.css` — remove the two `@container session-header` gap overrides so the
+ toolbar rhythm never widens with the window; keep the label-collapse rules. (AC-1)
+4. `apps/desktop/src/projects/ProjectIcon.tsx` — drop the tile fill and the ring. (AC-2)
+5. `apps/desktop/src/sidebar/SessionRail.tsx` — the checkout and pull-request badges drop
+ `bg-fill-quiet`, `rounded-micro`, and `px-1`. (AC-3)
+6. `apps/desktop/src/session/Composer.tsx` — move `` into the `controls` fragment
+ before the flex spacer and collapse the footer to one `flex items-center` row. (AC-4)
+7. `apps/desktop/tests/shellChromeContract.test.ts` — new source contract for the four fixes.
+8. `apps/desktop/tests/sessionRailRendered.test.tsx` — extend the provenance case to assert the
+ badges carry no fill/pill classes and that the workspace line shares `data-session-content` with
+ the title line. (AC-3)
+9. `apps/desktop/tests/sessionHeaderActionsRendered.test.tsx` and
+ `apps/desktop/tests/composerGeometryContract.test.ts` — update only if their existing assertions
+ cover the changed classes.
+10. Review follow-ups, all inside the paths above plus `shellChromeContract`:
+ `apps/desktop/src/sidebar/SessionRail.tsx` carries the pull-request state by colour only and
+ drops the selected row's `before:` bar (AC-6, AC-8);
+ `apps/desktop/src/App.tsx` and `apps/desktop/src/styles.css` drop the header's leading project
+ mark and its narrow-window rule (AC-7); `apps/desktop/src/session/Composer.tsx` moves the
+ checkout and branch chip labels to the muted foreground (AC-9).
+
+11. Review follow-up (AC-10): `apps/desktop/src/sidebar/SessionRail.tsx` gives the project trigger
+ the shared `row` size and drops the session content's extra left padding, with the two rail tests
+ updated to the one-edge contract. Verified through the web server (see the checks below).
+
+Checks by risk and affected behavior:
+
+- Desktop: `bun run lint`, `bunx tsc --noEmit`, `bun test`, `bun run build:renderer` from
+ `apps/desktop`.
+- Rendered (AC-1, AC-2, AC-4, AC-5): the renderer runs from this worktree (`bun run dev:renderer`,
+ port 1420) and the agent's collaborative browser measures the toolbar gaps, the project-mark
+ styles, and the composer's control rows before and after. The rail's badge alignment is structural
+ (same container, no badge padding) and asserted through the rendered rail test because the
+ Core-less renderer has no session fixture for the rail; the running dev window the user opened
+ carries the same code and is left for their visual check.
+- Repository: `bun script/verify/sdlc.ts --worktree` and `bun script/verify/docs.ts` before handoff.
+
+Temporary resources: the task-owned renderer log at
+`/var/folders/nl/47s4vtc92m74_j8pmm7d0chh0000gn/T/opencode/renderer-1420.log`, the Vite dev server on
+port 1420, and the ignored `apps/desktop/dist/` output from the build check. The server is stopped and
+the log removed before handoff; screenshots are retained as Verification evidence. The user's running
+dev window (C2-dev, data dir `dev.codetwo.app.dev`) is theirs and is left running.
+
+Rollback: `git revert` of the single commit; every edit is a class, markup, or CSS change with no
+data, protocol, or persistence surface.
diff --git a/docs/sdlc/changes/2026-09-16-quiet-shell-chrome/spec.md b/docs/sdlc/changes/2026-09-16-quiet-shell-chrome/spec.md
new file mode 100644
index 00000000..4fe54599
--- /dev/null
+++ b/docs/sdlc/changes/2026-09-16-quiet-shell-chrome/spec.md
@@ -0,0 +1,72 @@
+---
+id: 2026-09-16-quiet-shell-chrome
+schema: 5
+stage: spec
+status: accepted
+owner: chenli
+created: 2026-09-16
+based_on: intent.md
+---
+
+# Spec: Quiet the app shell chrome
+
+## Design
+
+1. **One titlebar gap.** `.session-header-toolbar` drops `gap-4` (16px) and the session action group
+ drops `gap-2` (8px) for the semantic `gap-inline` (4px), matching `docs/design/system.md`
+ ("Independent 28px controls use a 4px gap"). The two `@container session-header` gap overrides
+ (`12px` below 36rem, `4px` below 28rem) are removed because they made narrow windows *wider* than
+ the base, which is where the uneven reading came from. Control heights, fills, and labels are
+ unchanged.
+2. **Plain project mark.** `ProjectIcon` drops the `bg-foreground/[0.055]` tile and the
+ `ring-1 ring-foreground/10` ring. The 12px `rounded-control` box stays so an uploaded icon image
+ still clips to the same shape; without one, the folder glyph renders on the app plane. All three
+ call sites (titlebar, worktree list, project picker) share the change.
+3. **Unfilled workspace badges.** The checkout and pull-request badges drop `bg-fill-quiet`,
+ `rounded-micro`, and the `px-1` pill padding, becoming inline icon+label meta in their existing
+ tone (`text-foreground/55`, `text-success`, `text-destructive`, `text-warning`,
+ `text-muted-foreground`). Because they are the only element on the workspace line that added a
+ left offset, this also puts their text edge on the row title's edge: both lines are children of
+ the same `data-session-content` container, which owns the row's single left inset.
+4. **One composer control row.** `SessionControls` moves from its own line in the footer into the
+ `controls` fragment before the flex spacer, so the compact composer renders one row:
+ `[+] [collaboration] [goal] [statusline] [chips] … [expand] [voice] [run]`. The footer keeps a
+ single `flex items-center` row; the session-options popover still opens beneath the chips, and the
+ document-mode floating bar keeps its surface and position.
+5. **Pull-request state by colour.** The rail badge keeps its state icon and `#number` and drops the
+ visible state word; the tone still separates merged (success), failing/conflicting (destructive),
+ running (warning) and the rest (muted), and the tooltip and `aria-label` keep the words for hover
+ and assistive technology.
+6. **No leading project mark.** The session header's breadcrumb loses the project icon span, its
+ `ProjectIcon`/`Folder` conditional and the now-unused imports; the project name and `/` separator
+ stay, and the narrow-window rule that hid the mark goes with it.
+7. **No rail selection bar.** The selected rail row keeps `bg-fill-selected` and drops the
+ `before:` vertical bar; nothing else changes and the row's other states are untouched.
+8. **Quiet checkout-bar labels.** The checkout chip and the branch chip switch their label colour
+ from `text-foreground/85` and `text-foreground/80` to the muted foreground.
+9. **One rail content edge.** Every rail row's leading mark and text sit on the same 16px column:
+ the project header's trigger takes the shared `row` size (its unsized Button had fallen back to
+ the default control padding, whose `has-[>svg]:px-3` beat the local `px-2`), and the session
+ content drops the extra `pl-1.5` so it inherits the row's own inset. Header labels keep their
+ 40px text column, which the nav rows already use.
+
+## Acceptance criteria
+
+- [x] AC-1: The titlebar toolbar renders one 4px gap across its controls and clusters; no 16px or 8px
+ toolbar gap and no widening container-query override remains.
+- [x] AC-2: The project mark renders with no tile fill and no ring at all three call sites.
+- [x] AC-3: The workspace badges render with no background or pill padding, and the workspace line
+ shares the row content container with the title line so their text edges agree.
+- [x] AC-4: The compact composer renders the chips in the same row as `+ / voice / run`; the footer
+ keeps one control row and the options popover still opens under the chips.
+- [x] AC-5: Desktop lint, type, tests, and the renderer build pass; rendered captures show the
+ titlebar gap, the plain project mark, the unfilled badges, and the single composer row.
+- [x] AC-6: The rail's pull-request badge renders the state icon and `#number` only, coloured by
+ state, with the state word still present in its `title` and `aria-label` and absent from the
+ visible text.
+- [x] AC-7: The session header renders the project name and `/` without a leading project mark, and
+ no `ProjectIcon`/`Folder` fallback remains in that header.
+- [x] AC-8: The selected rail row renders its selected fill with no leading bar.
+- [x] AC-9: The checkout chip and the branch chip render their labels in the muted foreground.
+- [x] AC-10: Every rail row's leading mark and text share one 16px content edge, and the header rows'
+ labels share the 40px column.
diff --git a/docs/sdlc/changes/2026-09-16-quiet-shell-chrome/verification.md b/docs/sdlc/changes/2026-09-16-quiet-shell-chrome/verification.md
new file mode 100644
index 00000000..86b31311
--- /dev/null
+++ b/docs/sdlc/changes/2026-09-16-quiet-shell-chrome/verification.md
@@ -0,0 +1,70 @@
+---
+id: 2026-09-16-quiet-shell-chrome
+schema: 5
+stage: verification
+status: passed
+owner: chenli
+created: 2026-09-16
+based_on: plan.md
+revision: "8d1f32d7 (origin/main) with the quiet-shell-chrome change applied"
+verification_mode: owner
+verified_by: chenli
+verified_at: 2026-09-16
+release_target: none
+cleanup_status: complete
+---
+
+# Verification: Quiet the app shell chrome
+
+## Verification
+
+- AC-1: PASS — In the renderer (`bun run dev:renderer`, port 1420, 1728px) the toolbar computed `gap: 4px` with measured gaps `[4, 4]` across the environment, action and layout clusters, and the action group computed `gap: 4px` with measured `[4, 4]`; before the change the same points measured `16px`, `8px`, `8px` and a 17px trailing gap. `shellChromeContract` and the updated `windowChromeContract` assert the semantic gap and the absence of the container-query overrides.
+- AC-2: PASS — `ProjectIcon` no longer emits `bg-foreground/[0.055]` or `ring-1`, asserted by `shellChromeContract`; the capture cannot show it because the empty shell has no selected project, and the settings picker and worktree list share the same component.
+- AC-3: PASS — The checkout and pull-request badges render as `text-fine` inline meta with no `bg-fill-quiet`, `rounded-micro`, or `px-1`, and `sessionRailRendered` asserts that the title line and the workspace line share the same `data-session-content` parent, so removing the badge padding restores the title's text edge.
+- AC-4: PASS — The compact composer renders the chips in the single control row: the footer computed `flex min-w-0 items-center gap-0.5 p-2`, and the measured row holds `+` at x=488, the plugin and scene/model chips at x=518–883, then the expand, voice and send controls at x=1144–1240 on one line (the 32px send control centres 2px above the 28px icons).
+- AC-5: PASS — From `apps/desktop`: `bun run lint`, `bunx tsc --noEmit` (inside `build:renderer`), `bun test` (947 pass, 3 skip, 0 fail) and `bun run build:renderer`. Rendered captures `browser-artifacts/browser-screenshot-localhost-mu3u2676-4aa21431.png` and `browser-artifacts/browser-screenshot-localhost-mu3urkcu-a8e0e136.png` show the unified titlebar spacing and the single composer row.
+- AC-6: PASS — The rail badge renders only the state icon and `#{pullRequest.number}`, with `pullRequestTone` carrying the state colour; `sessionRailRendered` asserts `#84` is visible while `Merged`/`CI failed` appear only in `title` and `aria-label`, and `shellChromeContract` asserts the source keeps the tone and the accessible words.
+- AC-7: PASS — `shellChromeContract` asserts `App.tsx` no longer renders `session-header-project-icon`, no longer imports `ProjectIcon`, and `styles.css` no longer hides that mark; the rendered shell reports `headerHasProjectMark: false`.
+- AC-8: PASS — `shellChromeContract` asserts the selected rail row keeps `bg-fill-selected` and no longer emits `before:inset-y-3`/`before:rounded-full`; the rendered rail test still passes with the fill as the only selection state.
+- AC-9: PASS — In the renderer the checkout chip's label computed `oklch(0.49583 0.00272918 none)` (the muted foreground) instead of the former `text-foreground/85`, and `shellChromeContract` covers the source change for both chips.
+
+- AC-10: PASS — Measured through the web server against a copy of the running dev data dir (rail 288px, 1728px viewport): feature nav icons and the "All projects" label already sat on 16; the project header's folder moved 20 → 16 and its name 44 → 40 (the trigger takes the shared `row` size, whose `px-module-inset` replaces the default padding's `has-[>svg]:px-3`), and the session title, summary and badges moved 22 → 16 (the content drops `pl-1.5`, inheriting the row's own inset). Capture `browser-artifacts/browser-screenshot-127-0-0-1-mu3vzusk-255a6c42.png`; `shellChromeContract` and the updated `sessionRailRendered` assertions hold the one-edge contract.
+
+Verdict: verified.
+Residual risk: the rail rows, the project mark and the branch chip need session/project data that the
+Core-less renderer does not have, so their evidence is structural plus the rendered rail/header
+tests; the user's running dev window carries the same source for the visual check. The 4px toolbar
+rhythm is now flat across every window width, and the narrow-window label collapse is unchanged. The
+composer's chips can still wrap inside their own measure when the composer is narrow, by design. The
+shared `selectable` button variant keeps its own leading mark for navigation rows; only the rail
+session row lost it.
+
+## Cleanup
+
+Removed: the task-owned Vite dev server on port 1420 and its log at
+`/var/folders/nl/47s4vtc92m74_j8pmm7d0chh0000gn/T/opencode/renderer-1420.log`, plus the ignored
+`apps/desktop/dist/` renderer output from the build check.
+Retained: the after screenshot in `/Users/chenli/.t3/userdata/browser-artifacts/` as AC-5 evidence,
+and the user's running `C2-dev` window with its build output under `apps/desktop/build/` and the
+Rust `target/` tree.
+Retention owner: chenli.
+Cleanup trigger: remove the screenshot with the next browser-artifact cleanup after review; the dev
+window and its build output go when the user stops or restarts their instance.
+Processes: the task-owned Vite server on port 1420 was stopped and
+`lsof -nP -iTCP:1420 -sTCP:LISTEN` plus `pgrep -fl "vite --port 1420"` match nothing. The user's
+`C2-dev` app (launcher 26364, app 26365, core 26368 on data dir `dev.codetwo.app.dev`) and the
+installed `C2 Nightly` app were left untouched.
+Evidence: `git status --porcelain` lists only the intended source and test files plus the record
+bundle.
+
+## Review and release
+
+Approval: implementation and PR delivery were requested directly by the user (the annotated review
+and the four follow-up marks); merge and release are not authorized.
+Rollback: See plan.md.
+Release: No release requested; merge and external actions require their own authorization.
+Review: [PR #238](https://github.com/IchenDEV/codeTwo/pull/238) carries this change on branch
+t3code/quiet-shell-chrome, based on main 833ecb8b; the hosted CI Validate job passed (run
+35075609711, job 104727185571, 2m44s) and again on the rail-alignment follow-up (run 35084873013,
+job 104757219623, 3m12s).
+Feedback: Link an Incident and regression Eval when a real failure occurs.