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
11 changes: 6 additions & 5 deletions specs/STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ _This file is a **card, not a ledger**: an entry that is done and no longer
guides the work gets **deleted**, not archived. History lives in git and PR
bodies; measured facts live in the spec they verify._

_Last updated: 2026-08-07 (spec 12 settings layer built: settings.json + the
engine-owned store + two additive wire messages + the TUI `/settings` pane;
seven writable intents, everything hot)_
_Last updated: 2026-08-07 (spec 10 §6.1 art direction landed: the
quiet-constellation skin — night-ground palette, wide-terminal sky panel with
braille mist + floating pet, narrow fallback unchanged; by-eye tuning stays
issue #79)_

## Where we are

Expand All @@ -34,7 +35,8 @@ how it got there:
retired 2026-08-07 — spec 07 status note).
- spec 10 TUI — the wire + `IpcHost` + OpenTUI client (#71); the visualizer
feed + pixel pet + warmth kit (#74). Redesign decisions: #70. The TUI is now
the DEFAULT front-end (spec 10 §6), falling back to plain without bun.
the DEFAULT front-end (spec 10 §6), falling back to plain without bun. The
§6.1 quiet-constellation skin landed 2026-08-07 (spec 10 §6.1 for its terms).
- spec 03-03 §7 conversational onboarding — the shell preflight demoted to a
reporter, one aggregated setup offer per boot with a ledger-recorded decline,
and the guide-written voice endpoint (#88).
Expand Down Expand Up @@ -63,7 +65,6 @@ hand: CI fails if this section points at an issue that is already closed.
- **#83** (watch) Enter during an uncommitted IME composition may submit the line.
- **#98** (eng) Steer tool-choice eval (Ollama) owed — the smoke is on-demand only; spec 11 §5.
- **#99** (by-ear) Spec 11 acceptance pass — handover feel, slow-pick cover, two-phase off.
- **#111** (by-ear) Settings pane acceptance pass — focus handoff, hot knobs by ear; spec 12 §5.7.

## Pinned — do not relitigate

Expand Down
58 changes: 47 additions & 11 deletions specs/spec10/10-tui.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
> item, not a blocker: whether Enter pressed *during* an uncommitted composition
> commits the candidate or submits the line (§5.1's note below). Slice 2
> (engine FFT tap + `viz` feed, §3.6) and slice 3 (pet substrate + warmth kit,
> §3.7) landed 2026-07-30; what remains open is sensory, not mechanical — the
> §5.11 human pass and the §6.1 art-direction session, which restyles the
> substrate without reopening these contracts.
> §3.7) landed 2026-07-30. The §6.1 art-direction session chose and landed the
> **quiet-constellation** composition (2026-08-07): the palette discipline,
> the wide-terminal sky panel (§3.3), and the mist rendering of the viz feed
> (§3.6) — contracts untouched. What remains open is sensory: the §5.11 human
> pass and the by-eye tuning of the new skin (issue #79), plus the pet's
> identity, which stays a §6.1 question.
> **Part**: Front-end refinement — replaces the CLI Host's plain print/stdin
> with a real TUI. The **single richer front-end murmur ever gets**: there is
> no GUI, no menu-bar, no web surface (master [`../DESIGN.md`](../DESIGN.md)
Expand Down Expand Up @@ -344,6 +347,16 @@ Esc. The permanent-focus rule defends against *radio output* stealing focus
mid-keystroke; a mode the listener opened is the listener spending their own
focus, and the broadcast never pauses under it.

**As built (2026-08-07, §6.1 quiet-constellation): composition has one
breakpoint.** At ≥ 96 columns the alive band recomposes as a **sky panel**
beside the log: the program log keeps the left with one blank line between
entries, the panel takes the right — a seeded starfield, the visualizer's
bins as a floating braille particle mist (§3.6), the pet drifting inside it,
and now-playing as a dim `♪` line under the panel. Below 96 columns the
classic bottom band stands unchanged, and now-playing stays in the status
strip. Same four regions either way; only the composition moves, which is
exactly the §6.1 licence.

**As built (2026-08-06, issue #95): the pet is optional.** `MURMUR_TUI_PET=0`
(also `off` / `false` / `no`) drops the creature from the alive band, and the
gutter that separated it from the bars goes with it — the spectrum spans the
Expand Down Expand Up @@ -403,6 +416,14 @@ only the client — one shutdown path.
the same strip can render the voice's envelope — the radio visibly
*speaks*; on the bed it breathes low. (What ships in v1 is a §6.1 styling
call; the feed contract is the same.)
- **As built (2026-08-07, §6.1)**: the bar strip is the narrow composition;
wide terminals re-render the SAME feed as the constellation mist —
braille-cell particles climbing from a floating baseline, density following
the bin level, a sparse fallout drifting below it. The sky paints on the
client's own 12fps clock and viz frames only feed the smoother, so stars
twinkle and the pet animates even when the engine is silent. All of it is
client-side arithmetic (`tui/src/constellation.ts`); the feed contract is
untouched.

### 3.7 The warmth kit (techniques adopted from the case research)

Expand Down Expand Up @@ -575,11 +596,26 @@ TUI work is built on the framework choice.
- **Bun→Node exit**: revisit when OpenTUI's Node FFI support leaves
experimental status.

### 6.1 The creative session (deferred, unchanged in spirit)

Art direction remains its own working session with the user: palette and
overall warmth, the pet's identity and personality (what it is, how it
reacts, what it does when you're gone), visualizer styling, whether typing
deserves a steering affordance (§3.2-A), and how the four regions compose
into something that feels alive. That session styles what this spec builds;
it does not reopen the contracts above.
### 6.1 The creative session

Art direction is its own working track with the user; it styles what this
spec builds and does not reopen the contracts above.

**Decided (2026-08-07, concept session over `scratch/ui-concepts/logo-pet`):
the skin is `04-quiet-constellation`.** Its terms:

- **Palette discipline**: one shared deep blue-charcoal night ground; each
scene is a single near-monochrome accent family on it (dawn cream /
daylight sage / ember / moonlight) — the hour changes the warmth of the
light, never the room. Values live in `tui/src/palette.ts` and stay by-eye
knobs.
- **Composition**: the one-breakpoint sky-panel layout (§3.3) and the mist
rendering of the viz feed (§3.6).
- **Rejected**: the analog-radio dial chrome (01), the framed scene panel
(02 — per-scene hand art, cost without a daily payoff), the zine poster
(03 — display type has no CJK form and no small-terminal degradation).

Still open, still this session's property: the pet's **identity** (the
whisper-figure logo motif and the stitched texture of concept 03 are the
reference material; today's creature holds the slot), and the by-eye tuning
of all of the above in a real terminal (issue #79).
126 changes: 126 additions & 0 deletions test/tui-constellation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// The constellation panel's client half (spec 10 §3.6 / §6.1 quiet-constellation
// art direction): a seeded starfield, the FFT bins re-rendered as a braille
// particle mist, and the pet floating inside it.
//
// Like bars.ts, the module is free of OpenTUI and React on purpose: the frame a
// terminal paints cannot be unit-asserted (§3.9), but "which dots for this
// level" can — and determinism (seeded stars, hashed jitter) is itself part of
// the contract, because a resize must not reshuffle the sky.

import { describe, expect, it } from 'vitest'

import { Constellation, panelWidth, WIDE_MIN } from '../tui/src/constellation.ts'
import { accentFor } from '../tui/src/palette.ts'

const ACCENT = accentFor('late-night')

const BRAILLE_START = 0x28_00
const BRAILLE_END = 0x28_ff

function isBraille(char: string): boolean {
const code = char.codePointAt(0)!
return code >= BRAILLE_START && code <= BRAILLE_END
}

// Total lit sub-pixels in a frame: each braille cell carries its dots in the
// low byte of the code point.
function litDots(rows: { text: string }[][]): number {
let dots = 0
for (const row of rows) {
for (const run of row) {
for (const char of run.text) {
if (isBraille(char)) {
let bits = char.codePointAt(0)! - BRAILLE_START
while (bits > 0) {
dots += bits & 1
bits >>= 1
}
}
}
}
}
return dots
}

function rowText(row: { text: string }[]): string {
return row.map((run) => run.text).join('')
}

describe('Constellation (§6.1: starfield + particle mist)', () => {
it('renders exactly height rows of exactly width cells', () => {
const sky = new Constellation(40, 12, 7)
const rows = sky.frame([0.2, 0.8, 0.5], ACCENT, null)
expect(rows).toHaveLength(12)
for (const row of rows) expect([...rowText(row)]).toHaveLength(40)
})

it('draws the same sky for the same seed — a resize must not reshuffle it', () => {
const a = new Constellation(40, 12, 7).frame([], ACCENT, null)
const b = new Constellation(40, 12, 7).frame([], ACCENT, null)
expect(a).toEqual(b)
})

it('draws a different sky for a different seed', () => {
const a = new Constellation(40, 12, 1).frame([], ACCENT, null)
const b = new Constellation(40, 12, 2).frame([], ACCENT, null)
expect(a).not.toEqual(b)
})

it('has stars but no particles in silence', () => {
const sky = new Constellation(40, 12, 7)
const rows = sky.frame([0, 0, 0], ACCENT, null)
expect(litDots(rows)).toBe(0)
const glyphs = rows.flatMap((row) => [...rowText(row)]).filter((char) => char !== ' ')
expect(glyphs.length).toBeGreaterThan(0)
})

it('lights more dots the louder the frame — the mist follows the music', () => {
const quiet = litDots(new Constellation(40, 12, 7).frame([0.2, 0.2, 0.2], ACCENT, null))
const loud = litDots(new Constellation(40, 12, 7).frame([1, 1, 1], ACCENT, null))
expect(quiet).toBeGreaterThan(0)
expect(loud).toBeGreaterThan(quiet)
})

it('keeps every particle braille and every star a known glyph', () => {
const sky = new Constellation(40, 12, 7)
const rows = sky.frame([1, 0.5, 1], ACCENT, null)
for (const char of rows.flatMap((row) => [...rowText(row)])) {
expect(isBraille(char) || [' ', '·', '+', '✦'].includes(char)).toBe(true)
}
})

it('blits the pet into the middle of the sky, background and all', () => {
const pet = [
[
{ fg: '#111111', bg: '#222222' },
{ fg: '#111111', bg: '#222222' },
],
]
const rows = new Constellation(40, 12, 7).frame([], ACCENT, pet)
const withPet = rows.flat().filter((run) => run.bg === '#222222')
expect(withPet.length).toBeGreaterThan(0)
})

it('survives hostile bins and degenerate panels', () => {
const sky = new Constellation(2, 2, 7)
for (const frame of [[], [Number.NaN], [2, -1], [0.5]]) {
const rows = sky.frame(frame, ACCENT, null)
expect(rows).toHaveLength(2)
for (const row of rows) expect([...rowText(row)]).toHaveLength(2)
}
})
})

describe('panelWidth (the one §6.1 breakpoint)', () => {
it('declines a panel below the wide minimum', () => {
expect(panelWidth(WIDE_MIN - 1)).toBeNull()
expect(panelWidth(20)).toBeNull()
})

it('grants a panel from the minimum up, clamped to a readable band', () => {
const atMin = panelWidth(WIDE_MIN)
expect(atMin).not.toBeNull()
expect(atMin!).toBeGreaterThanOrEqual(30)
expect(panelWidth(400)!).toBeLessThanOrEqual(64)
})
})
Loading
Loading