Skip to content

Add the reference terminal shell (TUI 0.1) - #11

Merged
scrothers merged 11 commits into
mainfrom
feature/tui-shell
Jul 27, 2026
Merged

Add the reference terminal shell (TUI 0.1)#11
scrothers merged 11 commits into
mainfrom
feature/tui-shell

Conversation

@scrothers

Copy link
Copy Markdown
Member

What & why

Adds the reference terminal shell — the first-party frontend provider — as five internal/tui/ packages plus a cmd/tui binary, and documents its design in docs/first-party/frontends/tui.md.

This is TUI 0.1: the shell and its visual language, so the remaining frontend work has something concrete to build against. It establishes where a widget's panel goes, what a plugin's RenderTree looks like once painted, how an overlay carries a plan/apply decision, and what the design tokens are named — the affordances a plugin author actually designs against.

The layering is theme → ui → paint → shell → cmd/tui, with region independent, and the commits follow that order so each one is reviewable on its own.

Package What it owns
internal/tui/theme Design tokens — palette, semantic roles, spacing scale, Oklab color mixing, gauge ramps
internal/tui/ui Components — Style builder, Panel, StatusLine, Fields, meters, Overlay
internal/tui/region Placement store: what each producer contributed to each of the seven regions, in protocol order
internal/tui/paint Pure RenderTree → styled rows, plus activation-target collection
internal/tui/shell The Bubble Tea model — layout solver, focus ring, keymap layers, agents, composer, status bar, overlay
cmd/tui Thin wiring: flags, slog to stderr, /dev/tty, the program

Checklist

  • Local gate passes: go mod tidy is a no-op, go build ./..., go vet ./..., gofmt -l -s . prints nothing, go test -race -covermode=atomic ./..., golangci-lint run
  • Observable behavior changes update the matching docs/specifications/ document in this same PR
  • No hand edits under pkg/*/proto/v1/.proto changes made in api/ and regenerated with buf generate
  • Doc cross-references are path + heading anchor (never section numbers); renamed headings were grepped for inbound anchors
  • New internal/ packages include README.md + CLAUDE.md in the same commit
  • No compiled artifacts outside bin/

On the second box: this PR changes no protocol surface and touches no .proto, so nothing in docs/specifications/ needed updating. The new design doc lives under docs/first-party/ and is explicitly descriptive, not normative — the frontend spec leaves focus, keybindings, resize, and scrollback to each frontend on purpose, and this documents one implementation's choices rather than adding requirements. mkdocs build --strict passes with it registered in the nav.

Also ran, beyond the checklist: buf lint, buf format --diff --exit-code, govulncheck (no vulnerabilities), gosec -exclude-generated (no issues).

Coverage on the new packages: region 100%, theme 98.7%, ui 96.1%, paint 95.8%, shell 92.1%.

Notes for reviewers

demo.go is a fixture with an expiry date. No kernel-side frontend-attach path exists yet, so the shell is currently driven by a scripted demo source. It should be deleted when the real bridge lands rather than grown into a second implementation — the CLAUDE.md in that package says so too.

Three protocol gaps surfaced while building this, none of them addressed here, all worth a decision before the bridge is written:

  1. No ClientEvent carries an agent-profile selection, and a session's profile is fixed at creation — so the agent switcher keeps its selection as local state and emits an action the bridge has to interpret. Most plausibly it means "the profile for the next session", but that's a protocol question.
  2. Nothing on the wire carries the working directory or repository. They reach the shell as a message so the shell stays pure, and cmd/tui supplies them.
  3. Nothing aggregates per-tool line counts (read/added/removed). A tool provider knows them; a widget or a kernel-side rollup is the natural source.

Four things are worth reading closely, because they are constraints rather than choices, and each one is documented where it bites:

  • Content styles carry a foreground and never a background. Lip Gloss terminates every styled run with a full SGR reset, so a container that painted its own background lost it at the first styled run inside it. The application surface is set once on the Bubble Tea View.
  • ui.Overlay splices rows by hand because Lip Gloss cannot do it. Canvas.Compose draws each layer at full canvas bounds and Layer.Draw ignores its own X/Y, so composing a pane over a frame erases the frame.
  • ExpandTabs at every text leaf is a correctness fix. lipgloss.Width("\t") is 0 but a terminal advances to the next tab stop, so an unexpanded tab paints wider than it measures and corrupts every row to its right.
  • View.MouseMode is what makes the wheel scroll the transcript. Without it the wheel silently scrolls the terminal's own buffer behind the alt screen — the gesture appears to work while doing something else entirely.

Bubble Tea v2 differs from v1 in ways that will trip up a reviewer reading it as v1: the module path is charm.land/bubbletea/v2, Model.View() returns a tea.View rather than a string, and alt-screen is a property of that View — there is no tea.WithAltScreen() program option. Full-screen takeover is entirely View properties.

Layout arithmetic is deliberately centralized in Layout, because a duplicate of it already drifted once: the header grew from a single line into a bordered box, frame() accounted for it and the cursor did not, and the caret sat two rows above the text it belonged to. The frame tests assert against painted output — finding the row containing the prompt — rather than against recomputed arithmetic, so that class of mistake fails loudly.

Deferred on purpose: syntax highlighting for CodeBlockNode, and the e (corrected-input editor) binding is advertised in the hints but not yet wired.

Introduce the two-layer color system the rest of the shell is built on:
a raw Palette of ten neutrals plus semantic hues, resolved into Tokens
that name roles (Background, TextSubtle, BorderActive, Divider) rather
than values. Callers ask for a role; the active theme decides what it
looks like.

Both ramps are deliberately neutral — max channel spread 17 — because
the first attempt tinted the greys blue and a divider at 1.2:1 against
its background was invisible. Divider is its own token at 2.79:1 rather
than reusing a border color.

Mix interpolates in Oklab, not sRGB. A green-to-red gauge blended in
sRGB passes through a muddy olive with a visibly dark midpoint; Oklab
keeps perceived lightness constant across the ramp, which is what makes
the context meter read as a smooth scale.

Adds the Charm v2 dependencies. Note the module path is charm.land/...,
not github.com/charmbracelet/... — v2 moved.
The component layer: a chainable Style builder, Panel, StatusLine,
Fields, and the fill meters. Every component returns exact dimensions
so callers stack them without measuring.

Three constraints are load-bearing rather than stylistic:

Content styles carry a foreground and never a background. Lip Gloss
terminates every styled run with a full SGR reset, so a container that
painted its own background lost it at the first styled run inside it —
which is what produced the color banding. The application surface is
set once on the Bubble Tea View instead.

ExpandTabs runs at every text leaf. lipgloss.Width("\t") is 0 but a
terminal advances to the next tab stop, so an unexpanded tab paints
wider than it measures and corrupts every row to its right.

Overlay splices rows by hand because Lip Gloss cannot do it:
Canvas.Compose draws each layer at full canvas bounds and Layer.Draw
ignores its own X/Y, so composing a pane over a frame erases the frame.

StatusLine sheds segments from the right one at a time. Dropping the
right group wholesale made the line lurch on resize — a filling
segment would gain the whole group's width across a single column of
terminal, taking the meter from too-small-to-draw to enormous.
Holds what each producer contributed to each of the seven regions, and
resolves the ordering the frontend protocol specifies: ranked first,
then priority, then arrival sequence.

Backed by a fixed-length array rather than a map so iteration order is
positional and cannot vary run to run — the same determinism rule the
kernel's event path follows.
Turns a protocol RenderTree into styled terminal rows, and walks the
same tree to collect activation targets so a frontend can dispatch an
ActionNode without a second traversal.

Pure: it takes a tree and a width and returns strings. No I/O, no
state, no clock — which is what lets the shell's frame tests assert
against exact painted output.

Unknown node kinds fall back to FallbackText rather than rendering
nothing. A frontend built against an older protocol revision must
degrade to showing something, not silently drop a producer's content.
The frontend itself: layout solver, focus ring, keymap layers, agent
switcher, composer, status bar, and the overlay that carries a
plan/apply decision.

Layout arithmetic lives in exactly one place. Layout.ComposerTop is
the single source for where the bands stack, because a duplicate of it
in the cursor path already drifted once — the header grew from a line
into a bordered box, the frame accounted for it and the cursor did
not, and the caret sat two rows above its text. The frame tests assert
against painted output rather than recomputed arithmetic so that class
of drift fails loudly.

Full-screen takeover is entirely View properties in Bubble Tea v2 —
AltScreen, BackgroundColor, WindowTitle, Cursor, MouseMode — and there
are no matching program options. MouseMode is what makes the wheel
scroll the transcript; without it the wheel silently scrolls the
terminal's own buffer behind the alt screen, which looks like it works
while doing something else.

An overlay captures the keyboard entirely, and esc explicitly does not
resolve a pending decision: turning "go away" into an allow or a deny
on the operator's behalf would be indefensible.

demo.go is a fixture with an expiry date. It exists only because the
kernel-side attach path does not yet; delete it when the real bridge
lands rather than growing it into a second implementation.
Thin wiring: flags, a stderr slog handler, the terminal, the program.

It opens /dev/tty (CONOUT$ on Windows) and passes it as both input and
output rather than using the process stdio. As a go-plugin subprocess
the shell's stdout carries the handshake and is piped into the host's
logger, so painting there corrupts the handshake and reading stdin
competes with the plugin transport. The build-tagged openTTY is the
whole reason this binary is not three lines.

Currently driven by the scripted demo source, since no kernel-side
frontend-attach path exists to drive it for real yet.
Adds docs/first-party/frontends/tui.md — process shape and TTY
ownership, the design system, screen layout, where session data lives
and why, keymap layers, and what is deliberately deferred.

It is descriptive, not normative. The frontend spec leaves focus,
keybindings, resize, and scrollback to each frontend on purpose, and
this documents one implementation's choices rather than adding
requirements to the protocol.

Registers it in the mkdocs nav and llmstxt sections, and notes the
shell's current state in the project CLAUDE.md.
Copilot AI review requested due to automatic review settings July 27, 2026 14:07
@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown

Dependency Review

The following issues were found:
  • ✅ 0 vulnerable package(s)
  • ✅ 0 package(s) with incompatible licenses
  • ✅ 0 package(s) with invalid SPDX license definitions
  • ⚠️ 5 package(s) with unknown licenses.
  • ⚠️ 1 packages with OpenSSF Scorecard issues.
See the Details below.

License Issues

go.mod

PackageVersionLicenseIssue Type
github.com/charmbracelet/colorprofile0.4.3NullUnknown License
github.com/charmbracelet/x/ansi0.11.7NullUnknown License
github.com/mattn/go-runewidth0.0.24NullUnknown License
github.com/muesli/cancelreader0.2.2NullUnknown License
github.com/rivo/uniseg0.4.7NullUnknown License

OpenSSF Scorecard

Scorecard details
PackageVersionScoreDetails
gomod/charm.land/bubbletea/v2 2.0.8 🟢 5.1
Details
CheckScoreReason
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Code-Review🟢 3Found 8/23 approved changesets -- score normalized to 3
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
Packaging⚠️ -1packaging workflow not detected
Maintained🟢 109 commit(s) and 5 issue activity found in the last 90 days -- score normalized to 10
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
Binary-Artifacts🟢 10no binaries found in the repo
Fuzzing⚠️ 0project is not fuzzed
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Branch-Protection🟢 3branch protection is not maximal on development and all release branches
Security-Policy🟢 10security policy file detected
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
gomod/charm.land/lipgloss/v2 2.0.5 🟢 4.7
Details
CheckScoreReason
Code-Review🟢 3Found 7/18 approved changesets -- score normalized to 3
Packaging⚠️ -1packaging workflow not detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Binary-Artifacts🟢 10no binaries found in the repo
Maintained🟢 911 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 9
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
Fuzzing⚠️ 0project is not fuzzed
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Branch-Protection⚠️ 0branch protection not enabled on development/release branches
Security-Policy🟢 10security policy file detected
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
gomod/github.com/charmbracelet/colorprofile 0.4.3 UnknownUnknown
gomod/github.com/charmbracelet/ultraviolet 0.0.0-20260703014108-f5a850f9c2b7 UnknownUnknown
gomod/github.com/charmbracelet/x/ansi 0.11.7 UnknownUnknown
gomod/github.com/charmbracelet/x/term 0.2.2 🟢 5.5
Details
CheckScoreReason
Code-Review⚠️ 0Found 0/6 approved changesets -- score normalized to 0
Maintained🟢 1030 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Packaging⚠️ -1packaging workflow not detected
Binary-Artifacts🟢 10no binaries found in the repo
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Fuzzing🟢 10project is fuzzed
License🟢 10license file detected
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
Branch-Protection🟢 3branch protection is not maximal on development and all release branches
Signed-Releases⚠️ -1no releases found
Security-Policy🟢 10security policy file detected
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
gomod/github.com/charmbracelet/x/termios 0.1.1 🟢 5.5
Details
CheckScoreReason
Code-Review⚠️ 0Found 0/6 approved changesets -- score normalized to 0
Maintained🟢 1030 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Packaging⚠️ -1packaging workflow not detected
Binary-Artifacts🟢 10no binaries found in the repo
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Fuzzing🟢 10project is fuzzed
License🟢 10license file detected
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
Branch-Protection🟢 3branch protection is not maximal on development and all release branches
Signed-Releases⚠️ -1no releases found
Security-Policy🟢 10security policy file detected
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
gomod/github.com/charmbracelet/x/windows 0.2.2 🟢 5.5
Details
CheckScoreReason
Code-Review⚠️ 0Found 0/6 approved changesets -- score normalized to 0
Maintained🟢 1030 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Packaging⚠️ -1packaging workflow not detected
Binary-Artifacts🟢 10no binaries found in the repo
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Fuzzing🟢 10project is fuzzed
License🟢 10license file detected
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
Branch-Protection🟢 3branch protection is not maximal on development and all release branches
Signed-Releases⚠️ -1no releases found
Security-Policy🟢 10security policy file detected
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
gomod/github.com/clipperhouse/displaywidth 0.11.0 UnknownUnknown
gomod/github.com/clipperhouse/uax29/v2 2.7.0 🟢 3.4
Details
CheckScoreReason
Maintained⚠️ 00 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0
Packaging⚠️ -1packaging workflow not detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
Code-Review⚠️ 0Found 1/30 approved changesets -- score normalized to 0
Binary-Artifacts🟢 10no binaries found in the repo
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Security-Policy⚠️ 0security policy file not detected
Fuzzing🟢 10project is fuzzed
License🟢 10license file detected
Branch-Protection⚠️ 0branch protection not enabled on development/release branches
Signed-Releases⚠️ -1no releases found
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
gomod/github.com/lucasb-eyer/go-colorful 1.4.0 🟢 3.2
Details
CheckScoreReason
Packaging⚠️ -1packaging workflow not detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Maintained⚠️ 00 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
Code-Review🟢 4Found 13/30 approved changesets -- score normalized to 4
Binary-Artifacts🟢 10no binaries found in the repo
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Security-Policy⚠️ 0security policy file not detected
Fuzzing⚠️ 0project is not fuzzed
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Branch-Protection⚠️ 0branch protection not enabled on development/release branches
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
gomod/github.com/mattn/go-runewidth 0.0.24 UnknownUnknown
gomod/github.com/muesli/cancelreader 0.2.2 UnknownUnknown
gomod/github.com/rivo/uniseg 0.4.7 UnknownUnknown
gomod/github.com/xo/terminfo 0.0.0-20220910002029-abceb7e1c41e ⚠️ 2
Details
CheckScoreReason
Dangerous-Workflow⚠️ -1no workflows found
Code-Review⚠️ 0Found 1/30 approved changesets -- score normalized to 0
Maintained⚠️ 00 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0
Token-Permissions⚠️ -1No tokens found
Packaging⚠️ -1packaging workflow not detected
Binary-Artifacts🟢 10no binaries found in the repo
Pinned-Dependencies⚠️ -1no dependencies found
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Security-Policy⚠️ 0security policy file not detected
Fuzzing⚠️ 0project is not fuzzed
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Branch-Protection⚠️ 0branch protection not enabled on development/release branches
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0

Scanned Files

  • .github/workflows/security.yml
  • go.mod

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds the reference terminal shell (TUI 0.1) as a first-party frontend implementation, including its UI/token system, render-tree painter, region placement store, Bubble Tea shell model, and a cmd/tui binary to run it (currently against a scripted demo source). It also extends the first-party docs and MkDocs nav to document the reference TUI’s design and behavior.

Changes:

  • Introduces new internal/tui/{theme,ui,paint,region,shell} packages (with tests) implementing tokens/components, RenderTree painting + focus targets, region placement ordering, and the Bubble Tea model.
  • Adds cmd/tui wiring to run the shell with direct TTY I/O (safe under go-plugin) and a demo EventSource.
  • Registers new first-party frontend documentation in MkDocs navigation and updates first-party catalog index.

Reviewed changes

Copilot reviewed 54 out of 55 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
mkdocs.yml Adds frontends section to docs nav and include globs.
docs/first-party/index.md Updates first-party catalog intro + adds frontends link.
docs/first-party/frontends/README.md Adds index page for frontend implementation docs.
docs/first-party/frontends/tui.md Documents the reference TUI shell design (descriptive).
go.mod Adds Bubble Tea v2, Lip Gloss v2, and ANSI helper deps.
go.sum Records new module checksums for added dependencies.
CLAUDE.md Updates repo “Current state” to mention the new cmd/tui binary and design doc.
cmd/tui/main.go Runs the reference TUI against the demo source; opens TTY; sets up logging/emitter.
cmd/tui/tty_unix.go Opens /dev/tty for direct terminal I/O on non-Windows.
cmd/tui/tty_windows.go Opens CONOUT$ for direct console output on Windows.
internal/tui/theme/theme.go Implements palette/tokens, Oklab mixing, ramps, and derived styles.
internal/tui/theme/theme_test.go Tests token/style invariants, contrast, mixing/ramp behavior, and mappings.
internal/tui/theme/export_test.go Exposes Oklab conversion to external tests.
internal/tui/theme/doc.go Package docs for the token/design system layer.
internal/tui/theme/README.md Explains palette/tokens split, ramps, “one surface” rule, and invariants.
internal/tui/theme/CLAUDE.md Package-specific agent notes and invariants for themes/tokens.
internal/tui/ui/style.go Utility style builder + ANSI-aware sizing helpers (Fit/Clip/Overlay/ExpandTabs).
internal/tui/ui/panel.go Panel component with exact-dimension rendering + title/caption embedding.
internal/tui/ui/status.go StatusLine rendering with left/right groups, fill segment, meters, and fitting logic.
internal/tui/ui/fields.go Label/value field list renderer with aligned columns and wide-mode fallback.
internal/tui/ui/doc.go Package docs for UI utilities/components.
internal/tui/ui/README.md Overview of UI utilities/components and accuracy rules.
internal/tui/ui/CLAUDE.md Package-specific agent notes (no literals, exact dimensions, tab expansion, etc.).
internal/tui/ui/ui_test.go Unit tests for Fit/FitBlock/Overlay/Panel behavior and invariants.
internal/tui/paint/paint.go Pure RenderTree painter with per-node treatments and graceful fallback.
internal/tui/paint/walk.go Collects keyboard-reachable targets (actions/collapsibles) mirroring paint traversal.
internal/tui/paint/paint_test.go Painter behavior tests across node types, fallback, width clamping, focus styling.
internal/tui/paint/walk_test.go Tests target enumeration order, pathing, and collapsed/expanded behavior.
internal/tui/paint/doc.go Package docs for the painter’s protocol obligations and purity.
internal/tui/paint/README.md High-level explanation of painter responsibilities and node treatments.
internal/tui/paint/CLAUDE.md Package-specific agent notes (default branch load-bearing, ANSI stripping, etc.).
internal/tui/region/region.go Region placement store with deterministic ordering + streaming text buffers.
internal/tui/region/region_test.go Tests replace scoping, ordering/determinism, streams, and reset semantics.
internal/tui/region/doc.go Package docs for placement coexistence + determinism guarantees.
internal/tui/region/README.md Explains store semantics, ordering model, and determinism rationale.
internal/tui/region/CLAUDE.md Package-specific agent notes (no map iteration ordering, replace scoping, etc.).
internal/tui/shell/layout.go Layout solver with drop order/breakpoints and inner-size helpers.
internal/tui/shell/layout_test.go Tests layout breakpoints, degradation order, and geometry invariants.
internal/tui/shell/focus.go Focus ring and cycling rules.
internal/tui/shell/focus_test.go Tests focus naming, ring membership, wraparound, and recovery behavior.
internal/tui/shell/keymap.go Keymap layers, default bindings, and hint-line generation.
internal/tui/shell/keymap_test.go Tests binding matching, conflicts, layer hints, and truncation behavior.
internal/tui/shell/input.go Pure in-memory composer buffer (rune-based), history, cursor position.
internal/tui/shell/input_test.go Tests editing, history, line predicates, placeholder rendering, cursor pos.
internal/tui/shell/format.go Formatting helpers (tokens, USD, duration) + usage/edit summaries.
internal/tui/shell/format_test.go Tests for token/cost/duration/percent formatting and usage nil-safety.
internal/tui/shell/event.go Defines shell message vocabulary and emitted Action types.
internal/tui/shell/agent.go Agent roster + cycling ring for profile selection.
internal/tui/shell/agent_test.go Tests agent roster behavior and shift+tab cycling semantics.
internal/tui/shell/model.go Bubble Tea model: message handling, key routing, frame composition, overlay, cursor placement.
internal/tui/shell/model_test.go Model-level tests for interaction, modality, streaming behavior, and frame properties.
internal/tui/shell/demo.go Scripted demo EventSource to exercise the shell without kernel attach bridge.
internal/tui/shell/doc.go Package docs for shell responsibilities and design constraints.
internal/tui/shell/README.md High-level shell overview, gaps it fills, and testability notes.
internal/tui/shell/CLAUDE.md Package-specific agent notes (TTY ownership, Bubble Tea v2 specifics, invariants).

// With workspace detail out of shell chrome, the git widget is the only
// source of VCS state — which is the point: one truth, contributed by
// the plugin that owns it.
place(gitWidget, 6, renderv1.Region_REGION_SIDEBAR, true, new(int32(10)), render.Tree(render.Group(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a defect — this compiles today. Go 1.26 extended new to accept an expression, allocating and initializing in one step, and go.mod declares go 1.26 (CI runs 1.26.5). Verified directly on this toolchain:

p := new(int32(10))   // *int32, *p == 10

The whole package builds, vets and lints clean, which it could not do under the pre-1.26 rule you are describing. Keeping it: it is exactly what the func ptr[T any](v T) *T helper existed to work around, and the helper is now dead weight.

// A second widget of a different kind: the shell already reports context
// and cost itself, so a fixture that repeated them would demonstrate
// duplication rather than what widgets are for.
place(jobsWidget, 7, renderv1.Region_REGION_SIDEBAR, true, new(int32(20)), render.Tree(render.Group(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the sibling comment on line 112 — new(expr) is valid Go 1.26 and this file compiles. No change.

Comment on lines +217 to +219
r, g, b, _ := th.Tone(tone).RGBA()
key := r<<16 | g<<8 | b

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, and a real bug — fixed in c5c5fd6.

RGBA() returns 16-bit channels, so r<<16 | g<<8 | b overlapped green into red's bits and blue into green's. Two distinct tones could land on one key, which would make this test report a duplicate that does not exist or miss one that does.

Rather than shifting down to 8 bits I keyed on the channels directly (map[[3]uint32]string) — it cannot collide by construction and discards no precision, so there is no packing argument left to get wrong later.

Comment thread internal/tui/ui/README.md Outdated
actions/setup-go derives its cache key from platform, Go version, and a
hash of go.sum — and nothing else. Every Go job on the same runner OS
therefore competed for one key, and since actions/cache never overwrites
an existing key, the first job to finish won and every other job on that
OS restored whatever that job happened to have compiled.

The winner is the job that does the least work. Here it was gofmt: it
compiles nothing, finished in ten seconds, and was writing an empty
GOCACHE that Build, Test and golangci-lint then restored before
recompiling the world.

The cache list shows it plainly — six of eight Linux entries were
~7.5 KB against 150 MB for the two runs where a real job won the race,
while macOS and Windows sat at ~270 MB because only one job ever runs
there. That asymmetry is the tell: it is a key collision, not a Go
problem.

Adds .github/actions/setup-go-cached, which installs Go with setup-go's
own cache disabled and restores GOCACHE + GOMODCACHE under a key scoped
to the calling job. Wires it into build, test, proto and lint. The gofmt
job now caches nothing at all, which is both correct — it neither
resolves modules nor compiles — and removes the poisoner outright.

The key stays frozen on go.sum rather than rolling per run: compiled
dependencies dominate a cold build and are exactly what does not change
between commits, so a rolling key would re-upload hundreds of MB per run
and churn the repository's 10 GB cache budget for little gain.

Dependabot needs the composite action's directory listed explicitly;
"/" only covers .github/workflows, and the SHAs pinned inside a
composite action would otherwise go stale unnoticed.
securego/gosec is a Docker action, and a container cannot see the
runner's GOCACHE or GOMODCACHE. It was re-downloading the whole module
graph and recompiling it on every run: 2m27s, the slowest job outside
the test matrix, almost entirely work the cache already holds.

gosec type-checks the program through go/packages, so it wants exactly
the compiled export data every other Go job here already has. Installing
the pinned binary and running it on the runner lets it share that cache.

The trade is supply-chain shape rather than trust: a SHA-pinned Docker
digest becomes a version-pinned module install verified through the Go
checksum database. The version matches what the action ran, and it is
noted as a manual bump since Dependabot does not track it.

The job name is unchanged — branch protection matches required checks by
name, so renaming it would leave merges waiting on a check that no
longer reports.
It only compiles the protoc plugins from go.mod's tool directives —
about eight seconds of work — and restoring the 26 MB that produces
costs more than it saves. Measured across the two runs: 15s uncached
against 20s cached.

A job has to be big enough to profit from a cache. This one is not, and
carrying one here is complexity that buys nothing.
Two findings from review.

The tone-uniqueness test packed color channels as r<<16|g<<8|b, but
RGBA returns 16-bit channels, so green overlapped red's bits and blue
overlapped green's. Two distinct tones could collide on one key —
reporting a duplicate that does not exist, or missing one that does.
Keyed on the channels themselves now, which cannot collide by
construction and throws away no precision.

The ui package docs still described Bar, which was removed when
StatusLine superseded it, and listed Panel twice in the symbol table.
A README naming a symbol the package does not export is worse than one
that omits it: it sends a reader looking for something that was
deliberately deleted. Removed across README, CLAUDE.md, doc.go and the
design doc, and folded the duplicate Panel rows into one that mentions
the caption.
@scrothers
scrothers merged commit 13520d2 into main Jul 27, 2026
15 checks passed
@scrothers
scrothers deleted the feature/tui-shell branch July 27, 2026 17:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants