Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-09-15
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
## Context

The Home TUI paints colored surfaces by wrapping `TextChunk`s in a background:

- `highlighted()` in `src/home-tui.ts` wraps every chunk that does **not** already carry a background with the accent fill, and leaves chunks that do (the state rail/dot) untouched.
- `filledLines()` in `src/home-tui.ts` wraps every chunk with a fill **unconditionally**; it backs the detail's identity zone (accent) and the new-worktree input well.

Facts are built with foreground-only chunks. Both the fold's `fact()` and the zone's `zoneFact()` colour a warning value with `theme.yellow`. On an accent fill that ink measures ~1.26:1 (dark), ~1.21:1 (light), ~1.03:1 (neutral) — unreadable — whereas the same yellow as ink on the canvas is 9.63:1. Palette definitions live in `src/tui-theme.ts`; `chipText` is documented as "text drawn on top of colored chips". Motivation is in `proposal.md`.

The two fill paths disagree on one rule: `highlighted` already lets a chunk keep its own background, `filledLines` does not. A chip that must survive a fill therefore only works in the fold today.

## Goals / Non-Goals

**Goals:**

- Warnings on filled surfaces render as a filled chip (fill + contrasting ink), preserving the warning signal while making the value legible.
- A dedicated warning fill/ink pair per palette that guarantees the ink contrasts with the fill, and the fill stays distinguishable from the accent fill.
- One shared mechanism so the fold and the detail zone render warnings identically.

**Non-Goals:**

- Changing which conditions raise a warning, or the independence of the underlying observations.
- Changing warnings on plain surfaces (the detail's remaining facts keep yellow ink).
- Introducing chips in TUI surfaces that have no warn-on-fill today (`runs-browser`, `specs-browser`).
- Reworking the accent fill, the palette at large, or the selected-row highlight design.

## Decisions

### D1: Warnings are context-sensitive — ink on plain surfaces, fill on filled surfaces

A warning keeps `yellow` **ink** where the surface is transparent (yellow on canvas is 9.63:1). Where the surface is a fill, the warning becomes a **chip**: `warning` fill + `warningInk` text. This keeps the existing "warning = yellow" vocabulary while removing the illegible combination, instead of abandoning yellow or dimming the accent.

*Alternative considered:* keep yellow ink but lighten/darken it per surface — rejected because no single ink clears 4.5:1 against both the blue accent and the canvas across palettes.

### D2: A dedicated palette pair, not the palette's `yellow`

The palette's `yellow` is tuned as ink on the canvas; as a fill on light it is a dark olive that clears contrast with nothing reasonable:

| fill | ink | ink/fill | fill/accent | verdict |
|---|---|---|---|---|
| `#E0AF68` (dark yellow) | `#0A0E1A` | 9.63 | 2.01 | satisfies both, on dark and light |
| `#8C6C3E` (light yellow) | `#000000` | 4.33 | 1.21 | fails ink/fill on light |
| `#8C6C3E` | `#E1E2E7` (`chipText` on light) | 3.75 | 1.21 | fails ink/fill on light |

So the change adds two palette fields, a warning fill and a warning ink, and chooses values per palette so the ink clears 4.5:1 against the fill and the fill stays distinguishable from the accent fill. The dark and light palettes use the amber fill `#E0AF68` with near-black ink `#0A0E1A`; neutral already passes with `#B59B3A` + `#000000` and may keep it or adopt the same pair. Because the chip's ink is **not** `chipText` (near-white on light), the new ink field is excluded from `PaletteColor` alongside `chipText` so it is never treated as a fill target.

*Alternative considered:* reuse `chipText` as the chip ink — rejected, it fails on light (3.75). *Alternative considered:* brighten the light palette's `yellow` in place — rejected, it is also used as ink elsewhere on light surfaces and would regress those.

### D3: One chip over the whole value, not per token

The whole warning value renders as one chip (the current yellow already spans the whole value: `pipeline · live · 1m 53s`). No attempt to chip only the `live`/`uncertain` token; a per-token chip would split one observation into mixed surfaces and complicate truncation.

### D4: Share the "preserve an existing background" rule between both fillers

`highlighted` already preserves a chunk's own background; `filledLines` does not. Extract that rule into one shared helper and apply it in both, so a chip survives either filler. This is additive for `filledLines`: the detail zone's rows and the input wall's rows currently carry no chunk-level backgrounds, so only the new chips change behaviour.

### D5: A single `warnChip` helper produces the chunk

One helper builds `bg(warning)(fg(warningInk)(value))` and is used by the fold's `fact()` (writer, unknown dirt, unknown changes) and the detail zone's `zoneFact()` (non-known linked PR). The plain detail facts keep passing `theme.yellow` to their existing colour parameter.

## Risks / Trade-offs

- **A saturated chip can overpower the row** → chip the value only, keep its label in the ordinary chip text, and keep the chip hue far from the accent blue; a single value per row is chipped.
- **A long `unknown (reason)` value becomes a long amber bar** → the value is already truncated to the fact column; the chip follows the truncated text and its length, so it never exceeds the row.
- **The shared escape could let unrelated backgrounds survive a fill** → verified that the only chunks in filled rows today carry foreground only, so the escape changes nothing until a chip exists.
- **The chip reverses a deliberate, test-locked contract** → the change is intentional and recorded in the spec; the affected test is updated as part of the work rather than worked around.
- **Light/neutral aesthetics** → the pair is chosen against measured contrast, not by eye, and is covered by a palette test.

## Migration Plan

None required: the change is presentation-only with no persisted data, public API, or configuration surface. Rollback restores the palette fields and the two call sites.

## Open Questions

- Whether other TUI surfaces (runs/specs browsers) should later adopt the warning chip if a warn-on-fill appears there. Deferrable; adding it there would not change this spec or approach.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
## Why

Home's selected-row fold and the worktree detail's identity zone paint a solid accent fill behind every fact. Warning values — the managed writer's live/uncertain liveness, an unknown dirt or changes observation, and an unavailable PR — keep their yellow ink on that fill, where yellow-on-accent measures about 1.26:1 in the dark palette and 1.21:1 in the light palette. The warning is signalled at the cost of the text being effectively unreadable, so an operator can see that something needs attention but cannot read what it says.

## What Changes

- A warning value rendered on a filled surface (the selected row's fold and the worktree detail's filled zone) SHALL render as a filled chip: the warning color becomes the background and a contrasting ink carries the text, so the warning signal is preserved and the value is legible.
- A warning value rendered on a plain, unfilled surface (the detail's remaining facts) SHALL keep its yellow ink, which is already legible against the terminal background. Warnings become context-sensitive: ink on plain surfaces, fill on filled surfaces.
- The warning chip's fill SHALL be guaranteed to contrast with its text across the dark, light, and neutral palettes. This requires a dedicated warning-chip color pair rather than reusing the palette's `yellow`, which is tuned as ink on a light canvas and is too dark to serve as a fill there.
- This reverses the current deliberate presentation contract that a warning "stays yellow on the accent fill".

## Capabilities

### New Capabilities

(none)

### Modified Capabilities

- `home-launcher`: warning observations shown on the accent-filled fold and the detail's filled zone render as a contrasting filled chip instead of yellow ink, preserving both the warning signal and legibility; warnings on the detail's plain facts keep their yellow ink.

## Impact

- `src/tui-theme.ts`: add a warning-chip fill/ink pair to the dark, light, and neutral palettes, chosen so the ink meets contrast against the fill (and the fill stays distinguishable from the accent fill).
- `src/home-tui.ts`: render warning values as chips in the selected-row fold (`inlineDetailLines`) and in the detail zone (`detailLines`), and let `filledLines` preserve a chunk's own background the way `highlighted` already does.
- `test/home-tui.test.ts`: replace the "stays yellow on the accent fill" expectation with the chip contract, and cover the two contexts (filled chip vs plain yellow ink).
- `test/tui-theme.test.ts`: cover the warning-chip pair's contrast in each palette.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
## ADDED Requirements

### Requirement: Warning observations stay legible on filled surfaces

A warning observation rendered on a filled surface SHALL render as a filled chip: the warning color becomes the chip's background and a contrasting ink carries its text, so the warning stays visible and its value stays legible against the surrounding fill. A warning observation rendered on a plain, unfilled surface SHALL retain its yellow ink. The warning chip's fill and ink SHALL be chosen per palette so the ink contrasts with the fill and the fill remains distinguishable from the accent fill. In particular, the selected row's inline detail (a managed writer's live or uncertain liveness, an unknown working-tree dirt observation, an unknown changes observation) and the worktree detail's filled identity zone (a linked-PR observation that is not known) SHALL use the chip, while the detail's remaining plain observations SHALL keep yellow ink. The conditions that raise a warning and the independence of the underlying observations SHALL NOT change, and no additional observation query SHALL be introduced.

#### Scenario: Live writer in the selected row's inline detail

- **WHEN** a selected worktree holds a live or uncertain managed writer claim and its inline detail renders on the accent fill
- **THEN** the writer value renders on the warning chip fill with contrasting ink, legible against the accent fill

#### Scenario: Unknown observation in the selected row's inline detail

- **WHEN** the selected worktree's working-tree dirt or changes observation is unknown
- **THEN** the unknown value renders on the warning chip fill with contrasting ink instead of yellow ink on the accent fill

#### Scenario: Unavailable linked PR in the detail's filled zone

- **WHEN** the worktree detail's filled identity zone shows a linked-PR observation that is not known
- **THEN** the value renders on the warning chip fill with contrasting ink

#### Scenario: Plain facts keep yellow ink

- **WHEN** a warning observation renders outside any filled surface, among the detail's remaining plain facts
- **THEN** it keeps its yellow ink and does not render as a chip

#### Scenario: Warning chip contrasts across palettes

- **WHEN** the warning chip renders under the dark, light, or neutral palette
- **THEN** its ink contrasts legibly with its fill and its fill remains distinguishable from the accent fill
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
## 1. Palette tokens

- [x] 1.1 Add `warning` (fill) and `warningInk` fields to the `Palette` type and to the dark, light, and neutral palettes in `src/tui-theme.ts`, excluding `warningInk` from `PaletteColor`; verify with `bun run typecheck`.
- [x] 1.2 Pick values per palette so the warning ink clears 4.5:1 against the warning fill and the fill stays distinguishable from the accent fill (dark/light: `#E0AF68` + `#0A0E1A`; neutral: `#B59B3A` + `#000000` or the same pair); verify with a contrast test added in `test/tui-theme.test.ts` (`bun test test/tui-theme.test.ts`).

## 2. Rendering

- [x] 2.1 Extract the "preserve a chunk's own background" rule into one shared helper in `src/home-tui.ts` and use it from both `highlighted()` and `filledLines()`; verify existing Home tests still pass with `bun test test/home-tui.test.ts`.
- [x] 2.2 Add a `warnChip` helper that builds the warning fill + ink chunk (`bg(warning)(fg(warningInk)(value))`); verify it via the fold tests in group 3.
- [x] 2.3 Apply the chip to the fold's warning facts in `inlineDetailLines` (managed writer, unknown working-tree dirt, unknown changes); verify the selected-row fold shows the value on the warning fill with `warningInk` text.
- [x] 2.4 Apply the chip to the detail zone's non-known linked-PR value in `detailLines`, and leave the detail's plain observations on yellow ink; verify via the tests in group 3.

## 3. Tests

- [x] 3.1 Replace the "zone's PR warning stays yellow on the accent fill" expectation in `test/home-tui.test.ts` with the chip contract (warning fill background + `warningInk` foreground); verify with `bun test test/home-tui.test.ts`.
- [x] 3.2 Add a fold scenario asserting a live/uncertain managed writer value renders on the warning chip; verify with `bun test test/home-tui.test.ts`.
- [x] 3.3 Add coverage that an unknown dirt or changes value in the fold renders on the warning chip, and that a warning on a plain detail fact keeps yellow ink; verify with `bun test test/home-tui.test.ts`.

## 4. Verification

- [x] 4.1 Run `bun run typecheck` and the full `bun test` suite; verify both pass with no failures.
- [x] 4.2 Confirm in a rendered Home frame (or captured spans) that the writer value is legible as a chip under the dark, light, and neutral palettes and that non-warning facts are unchanged.
29 changes: 29 additions & 0 deletions openspec/specs/home-launcher/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,3 +231,32 @@ Home SHALL supply its current on-demand PR evidence to the shared worktree indic

- **WHEN** the operator lands again on a previously observed merged worktree and the new lookup is pending or resolves unknown
- **THEN** Home uses the ordinary worktree color rules until it has a known merged observation again

### Requirement: Warning observations stay legible on filled surfaces

A warning observation rendered on a filled surface SHALL render as a filled chip: the warning color becomes the chip's background and a contrasting ink carries its text, so the warning stays visible and its value stays legible against the surrounding fill. A warning observation rendered on a plain, unfilled surface SHALL retain its yellow ink. The warning chip's fill and ink SHALL be chosen per palette so the ink contrasts with the fill and the fill remains distinguishable from the accent fill. In particular, the selected row's inline detail (a managed writer's live or uncertain liveness, an unknown working-tree dirt observation, an unknown changes observation) and the worktree detail's filled identity zone (a linked-PR observation that is not known) SHALL use the chip, while the detail's remaining plain observations SHALL keep yellow ink. The conditions that raise a warning and the independence of the underlying observations SHALL NOT change, and no additional observation query SHALL be introduced.

#### Scenario: Live writer in the selected row's inline detail

- **WHEN** a selected worktree holds a live or uncertain managed writer claim and its inline detail renders on the accent fill
- **THEN** the writer value renders on the warning chip fill with contrasting ink, legible against the accent fill

#### Scenario: Unknown observation in the selected row's inline detail

- **WHEN** the selected worktree's working-tree dirt or changes observation is unknown
- **THEN** the unknown value renders on the warning chip fill with contrasting ink instead of yellow ink on the accent fill

#### Scenario: Unavailable linked PR in the detail's filled zone

- **WHEN** the worktree detail's filled identity zone shows a linked-PR observation that is not known
- **THEN** the value renders on the warning chip fill with contrasting ink

#### Scenario: Plain facts keep yellow ink

- **WHEN** a warning observation renders outside any filled surface, among the detail's remaining plain facts
- **THEN** it keeps its yellow ink and does not render as a chip

#### Scenario: Warning chip contrasts across palettes

- **WHEN** the warning chip renders under the dark, light, or neutral palette
- **THEN** its ink contrasts legibly with its fill and its fill remains distinguishable from the accent fill
Loading