Add standalone .pi/components preview harness#281
Conversation
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
npm run dev:components boots a real ProcessTerminal + TUI and shows a
gallery of every registered .pi/components entry - no seeded workbench,
session, or DB required, since these components are render-only with
injectable theme/props.
Each registry entry mirrors its component's real production presentation
contract (overlay-with-options vs. inline editor-swap), reimplementing
ExtensionUIContext.custom's documented calling shape rather than assuming
every component is an overlay. Theme is a real pi-coding-agent Theme
instance, not a duck-typed stand-in.
- src/dev/component-preview/{theme,custom-ui,registry,gallery-component}.ts
- src/dev/component-preview.ts (public entry) + src/dev/index.ts export
- scripts/dev-components.ts, package.json dev:components[:watch]
- src/dev/TOPOLOGY.md + src/.pi/components/TOPOLOGY.md updated
- memory/PLAN.md: component-preview-harness (Parallel / Low-Conflict,
tooling, no Linear/branch by AGENTS.md convention)
- memory/cards/tooling--component-preview-harness.md: design rationale
Named but not fixed here: runtime-posture-axis-picker.harness.test.ts
wraps the picker in showOverlay when production uses inline-swap;
tui-lab's slash command remains unwired in pi-extensions.ts.
The harness test wrapped the runtime mode picker in tui.showOverlay(...), but production (commands/index.ts's openModePicker -> ctx.ui.custom with no overlay options) presents it inline via tui.addChild + tui.setFocus. Drift called out in the component-preview-harness card. Switching presentation modes surfaced a second, related bug: the active-segment assertions checked for a doubled *trailing* space after the label, which only held because the overlay box's fixed-width fill padded the line further. In the real inline-swap render the last track segment gets no such fill, so the doubled-trailing-space check was a false invariant. Replaced with a doubled-*leading*-space check, which the badge + separator/padding stacking produces reliably regardless of a segment's position in the track. Also tightens src/.pi/components/TOPOLOGY.md's harness-tier description, which generalized "drive through overlay" for all harness tests -- not true for inline-swap components like this one.
…s reference registerBrunchTuiLab (the /brunch:tui-style-lab command) never entered the product extension bundle (proven by its own test suite) and was inert even under Pi's ambient .pi/extensions/ directory scan, since ambient loading calls default exports with no options argument and the registrar's own enabled guard defaulted to false. It has had no working invocation path since it landed in FE-845. TuiStyleLabComponent (the actual render-only Component) moves to .pi/components/tui-lab/style-lab-component.ts, matching the directory's own ownership rule (components/ owns render-only Pi TUI components, extensions/ owns wiring). It stays previewable and visible as a design reference via 'npm run dev:components -- tui-lab', with no production call site. Split the extension-side test file: palette-rendering assertions move to components/__tests__/tui-lab-style.test.ts; the cycle-demo component test already lived under components/ and just gets its import path fixed. Registration-behavior tests (enabled-gate, product-bundle-exclusion) are dropped since the registrar they proved no longer exists. Resolves the second component-preview-harness carried-forward finding.
Audited every .pi/extensions/* registrar (all 19 wired to src/app/ pi-extensions.ts) and every .pi/components/* export (all consumed) against real call sites -- tui-lab was the only orphan, already retired. Cross-checked .pi/extensions/TOPOLOGY.md and src/dev/TOPOLOGY.md layout/path claims against the real directory shape -- both already accurate. One drift remained: .pi/components/TOPOLOGY.md's layout sketch still named runtime-posture/strategy-picker.ts, retired in the prompt-axis retirement (fc1b016). Only axis-picker.ts remains under runtime-posture/.
The harness previously only covered ctx.ui.custom (overlay/inline). Two more real presentation contracts existed in .pi/components/ with no preview path: - pi.registerMessageRenderer (alternatives-card-set): a pure (message, options, theme) => Component function with no done() callback. static-preview.ts's captureMessageRenderer() feeds registerBrunchAlternatives a minimal fake ExtensionAPI slice to capture the real renderer closure -- same technique registry.test.ts already uses to assert registration, just taken one step further into actually rendering the output. - ui.setHeader (BrunchStartupHeader): turned out not to need new plumbing at all -- it's already a plain Component with a public constructor, so it slots into the same inline-swap machinery as any other static content. Both mount via previewStaticComponent(), which wraps the static component with a dismiss-on-any-key handler -- a preview-only affordance, since neither lane has a real dismissal in production (a transcript message persists; chrome stays mounted for the session). Footer (ui.setFooter) is deliberately deferred: it's driven by live token/model/coherence state rather than static layout, and rides the scope the user wants to refine later. Smoke-tested both new entries end-to-end via npm run dev:components (gallery listing, deep-link, and gallery round-trip) using a real PTY, not just the VirtualTerminal-backed unit tests.
…chrome Design exploration for the component-dx frontier (design it twice via three parallel divergent proposals, synthesized): a CustomEditor subclass that overrides only render(), wrapping the inherited output in a │-bordered box (the default Editor has no side borders at all) with runtime-state labels baked into the border corners, matching the user's sketch. projectBorderedChrome is a pure, domain-ignorant function (pre-formatted label strings in, decorated lines out) so it stays reusable for the request_* question-form pickers the user wants to refine next, not just this editor. Because a real Editor's bottom border is not reliably the last rendered line -- autocomplete dropdown rows are appended after it -- the bottom border is found by scanning backward for an Editor-shaped border line, not a fixed index. Verified against the real (non-compiled) pi-tui Editor source at ~/.pi/pi-mono/packages/tui, not just its .d.ts. Registered as an [experimental] preview-harness entry only; not wired into src/.pi/extensions/chrome/index.ts. This also forced component-preview.ts's keybindings stub to become a real pi-tui KeybindingsManager, since CustomEditor.handleInput needs .matches() for escape/ctrl+d -- a gap manual PTY smoke-testing caught that the first unit tests missed (fixed and back-filled with onEscape/onCtrlD harness tests). Smoke-tested end-to-end via a real PTY: typing, box layout, escape-dismiss, and gallery round-trip all confirmed working.
…es, clickable URL
- Rounded corners (╭╮╰╯) instead of square, matching CardComponent's
existing box style (cards.ts).
- The box is now at least 2 content rows tall even when the editor is
empty: padContentToMinimum() inserts blank rows just before the detected
bottom border (never after, where autocomplete rows may live), shifting
the tracked bottom index accordingly.
- belowLines get a 1-column left indent.
- belowLines now accept a { text, url } union alongside plain strings,
rendered as a clickable OSC 8 hyperlink via pi-tui's own hyperlink() +
getCapabilities().hyperlinks gate (the same pattern pi-tui's Markdown
renderer already uses), falling back to plain text in terminals that
don't report hyperlink support. Kept as a generic union rather than a
dedicated 'url' field so belowLines stays usable for any future bordered
component, not just this editor's sidecar URL.
Smoke-tested end-to-end via a real PTY.
…wing + thumb, preview demo
Adds a pure scroll-window primitive, sibling to projectBorderedChrome:
projectScrollViewport(content, height, keepVisible?) => { lines, offset, isThumbRow }
Converged shape after cross-checking pi-tui's own Editor/SelectList windowing
against glyph, opentui, and lazygit/gocui — all five arrive at the same model
independently. Thumb math (size/position) mirrors the gocui/glyph proportional
formula; the thumb is reported per-row so a caller can fold it into whatever
border character it already draws (gocui/lazygit's approach, rather than a
separate column.
Wired into WorkspaceDialogComponent, fixing a real (not hypothetical) gap: its
specList/sessionList option rendering had no windowing at all, so once an
option list exceeded the overlay's resolved maxHeight, pi-tui's
compositeOverlays() silently truncated it (slice(0, maxHeight), no offset, no
indicator) and arrow-down could select an option already clipped off screen.
Height is a fixed WORKSPACE_DIALOG_MAX_VISIBLE_OPTIONS constant, not
terminal-rows-derived — the component holds no TUI reference today and
Component.render(width) never receives height; SelectList's own fixed
maxVisible is the precedented fit for this shape. #selectedIndex already
existed, so selection-follow windowing needs no new persisted scroll state.
Demoed in the component-preview harness via a new workspace-dialog-scroll
registry entry (20-spec fixture, same ctx.ui.custom overlay mounting path,
no new preview lane or wrapper component needed). Confirmed live through
agent-tui in a real terminal: window follows selection past both boundaries,
thumb tracks size/position top -> middle -> bottom correctly.
Deliberately not built this slice: wheel-scroll passthrough (needs a single
owner for the terminal mouse-mode toggle) and pointer-hover hit-testing
(pi-tui has no per-render row->component ownership map at all -- an upstream
change, not a component). Both named in memory/PLAN.md's component-dx entry.
Tests: scroll-viewport.test.ts (9 direct unit cases) + 3 new
workspace-dialog.test.ts cases (windowing, selection-follow past the edge,
thumb presence/absence scoped to option rows to avoid a false match against
the brand logo's own ANSI block art).
EOF
)
…ted frontiers memory/cards/component-dx--component-preview-harness.md and memory/cards/component-dx--scroll-viewport.md are deleted (the latter was never committed -- created and reconciled within this session). Both fully exhausted: their durable rationale now lives redundantly in code (scroll-viewport.ts's docstring, workspace-dialog/component.ts's inline comments), src/.pi/components/TOPOLOGY.md, src/dev/TOPOLOGY.md, and this commit's memory/PLAN.md paragraph. Fixed the dangling "see memory/cards/..." pointers left behind (plus two pre-existing stale pointers in component-preview.ts/registry.ts that referenced a card by the wrong prefix, predating this session) in the prior commit. memory/PLAN.md's component-dx entry: "Current execution pointer" no longer names now-deleted cards; states the direct-build-and-reconcile pattern going forward. Folded in the two forward-looking facts the deleted scroll-viewport card carried so they aren't lost: wheel-scroll passthrough is buildable without forking pi-tui but needs a single owner for the terminal mouse-mode toggle before it's built; true pointer-hover hit-testing is out of scope for any brunch component (pi-tui has no per-render row->component ownership map at all -- an upstream change). elicitor-project (FE-1085) and structured-exchange-affordance (FE-1108), both done since 2026-06-30, had full frontier definitions in PLAN.md duplicating their existing Recently Completed one-liners with nothing downstream citing the extra detail. Archived both full definitions to docs/archive/PLAN_HISTORY.md under a new 2026-07-01 Sync archive section, per the file's established per-frontier archive format. Deleted the stale untracked HANDOFF.md (never committed this session): the gap it named -- no scope card for the scroll-viewport work -- was resolved twice over (a card was created, then its content fully reconciled into canonical homes).
…no pi-tui fork needed Spike question: can Brunch receive/parse real terminal mouse-wheel escape sequences through pi-tui's existing input pipeline without forking it, and is enabling mouse-tracking mode safe for components that don't know about it? Verdict: yes, confirmed empirically via a throwaway probe (deleted, .fixtures/scratch/mouse-spike/) run live under agent-tui. tui.terminal.write enables SGR mouse mode; stdin-buffer.ts's existing SGR-sequence framing delivers a wheel event byte-intact and unsplit to both addInputListener and a focused component's handleInput; an unaware Editor-based component safely ignores it today (ESC-prefixed, matches neither decodePrintableKey decode path nor the >=32 printable-insertion guard). Sub-finding: agent-tui's scroll command is a keystroke-emulation shim (sends plain ArrowDown, not a real SGR mouse event) -- had to inject the literal sequence via agent-tui type instead. Not resolved (named, not a blocker): the native-text-selection UX tradeoff of enabling mouse mode, and who owns the global enable/disable lifecycle -- both design/ownership questions for a future ln-scope, not technical unknowns. No memory/SPEC.md change -- component-dx carries no SPEC traceability for tooling/component-level work, and there was no prior assumption entry about mouse support to update.
…ds/component-dx--wheel-scroll-passthrough.md) Full scope card for the next component-dx slice after the 2026-07-01 ln-spike confirmed the mechanism works. Scopes a single, minimal tracer: - parseWheelEvent(data) -- new pure fn (.pi/components/mouse-wheel.ts) recognizing the SGR wheel-up/wheel-down shape, ignoring click/motion/ non-mouse input. - ComponentPreviewCustomOptions.wheelScroll?: boolean on the shared showComponentPreview shim (custom-ui.ts) -- opt-in per entry, mirroring the existing overlay boolean's shape. When set, owns enable/disable of SGR mouse mode for exactly that entry's open/close lifecycle and synthesizes the equivalent ArrowUp/ArrowDown keypress into the already- created component's existing handleInput -- no new component-level API. - Wired into the workspace-dialog-scroll entry as the first real consumer. Deliberately scoped as harness-only (no production wiring, no new component-level API) with two named residuals: a real-terminal smoke test (agent-tui cannot exercise a real physical wheel event -- confirmed by the prior spike) and the production lifecycle-ownership question (this slice's per-entry opt-in does not answer who owns it for a real long-lived TUI session). memory/PLAN.md's component-dx execution pointer now names this card.
…l preview
Implements memory/cards/component-dx--wheel-scroll-passthrough.md (deleted,
consumed):
- .pi/components/mouse-wheel.ts: parseWheelEvent(data) decodes the SGR
wheel-up/wheel-down shape (bit 64), rejecting motion (bit 32),
release ('m'), and non-wheel button codes (clicks, horizontal wheel).
Pure, unit-tested in isolation.
- dev/component-preview/custom-ui.ts: ComponentPreviewCustomOptions gains
an opt-in wheelScroll?: boolean, mirroring the existing overlay boolean's
shape. When set, showComponentPreview owns SGR mouse enable/disable for
exactly that entry's open/close lifecycle and rewrites a recognized wheel
event's raw bytes to the equivalent ArrowUp/ArrowDown sequence via the
addInputListener data-rewrite path -- so TUI's own dispatch delivers it to
whichever component currently has focus, rather than a hardcoded
reference to the originally-created component. No new component-level
API; WorkspaceDialogComponent is unchanged.
- registry.ts: workspace-dialog-scroll opts in ({ wheelScroll: true }); no
other entry is affected (regression-tested).
- virtual-terminal.ts: additive "writes" log for asserting DECSET
enable/disable sequences in tests; no existing test behavior changed.
Test coverage across all three tiers named in the card: parseWheelEvent
unit tests; custom-ui.test.ts proving the opt-in enable/disable lifecycle
and wheel-to-arrow-key routing generically (including a regression case
that no entry gets mouse mode by default); and a harness test driving the
*real* workspace-dialog-scroll registry entry through a VirtualTerminal,
proving wheel-down reaches the identical rendered state as an equivalent
ArrowDown keypress.
Fixed one placement issue found in review (my own scoping miss, not the
builder's): the real-entry harness test was originally added to
.pi/components/__tests__/workspace-dialog.test.ts, importing
src/dev/component-preview/registry.js -- inverting the established
dependency direction (no existing .harness.test.ts under .pi/components/
imports from src/dev/; they all construct their component directly).
Moved it to src/dev/component-preview/__tests__/workspace-dialog-scroll.harness.test.ts,
matching precedent and the .harness.test.ts naming convention.
workspace-dialog.test.ts is now byte-identical to its prior committed state.
memory/PLAN.md's component-dx entry and both TOPOLOGY.md homes (.pi/components,
dev) are reconciled, naming the residuals honestly: a manual real-terminal
(iTerm2/Kitty/Ghostty) smoke test to confirm physical wheel emission matches
the injected SGR shape proved in harness, and production lifecycle ownership
of the mouse-mode toggle (this slice's per-entry opt-in deliberately does
not answer that for a real long-lived TUI session).
Left memory/cards/orchestrator-tool-port--plan-check-tool.md and
docs/design/CUELOOP_PATTERN_LIFTOUT.md untouched -- concurrent work from
another agent in this worktree, outside this session's manifest.
Adds docs/design/STRUCTURED_EXCHANGE_ANSWERING_PATHS.md, verified against pi-coding-agent internals (ExtensionMode/bindExtensions/hasUI(), rpc-mode.ts's ExtensionUIContext), not just the exchanges/ policy prose that already existed: - ctx.hasUI/ctx.ui.custom availability is a process-boot-time fact (Brunch always binds mode: "tui") -- not a per-caller/per-connection one. - Three structurally distinct answering paths: local-TUI tool execution, Brunch's own session.submitExchangeResponse (bypasses ctx.ui entirely), and the live web-driver broker (session.answerExchange, answer/free-text only today -- a pre-existing, independently tracked gap for choice/choices/review). - Per-response-kind coverage matrix showing a column-A (local-TUI) UI swap for choice/review is safe with respect to columns B and C. Corrects an over-strong claim from earlier this session (that ctx.ui.custom's lack of RPC relay was a hard blocker for restyling choice/review) -- Brunch's real RPC surface never reaches ctx.ui at all, so it's unaffected either way. Includes a re-verification checklist and an explicit public-API-vs-internal- implementation distinction, since pi-coding-agent is tracked continuously (D67-L) and most of what's cited here is the latter. Adds one-line pointer bullets to exchanges/, rpc/, and session/'s TOPOLOGY.md files rather than duplicating the content in three places.
…rds/component-dx--rounded-box-primitive.md) Mode: slices, earned posture (the interface was already fully designed and agreed in conversation before this file was written -- consolidation, not exploration). Consolidates three independent hand-rolled bordered-box implementations (brunch-editor.ts's projectBorderedChrome, workspace-dialog/component.ts's renderFrame + border helpers, cards.ts's CardComponent) into one canonical projectRoundedBox primitive, sibling to scroll-viewport.ts. Four slices: build the primitive, then migrate each of the three existing callers with a byte-identical-output regression test as the safety net. Scoping finding worth naming: CardComponent has no test today at all (neither it nor its one consumer, alternatives.ts, covers rendered card output) -- slice 4's acceptance criteria require writing that baseline test first, not skipping the safety net because none currently exists. Explicitly out of this sequence: giving MultiChoicePickerComponent a border + scroll-viewport wiring (the actual next request_* picker work this unblocks), and restyling choice/review response kinds -- both named, not built here. memory/PLAN.md's component-dx execution pointer now names this file.
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The builder's stop finding was correct but conflated two independently-sized
problems -- traced precisely against both cards.ts and rounded-box.ts:
1. Content-row side-border coloring (glyph+adjacent-space grouped vs bare
glyph) is a pure byte-representation difference with zero visual effect,
and rounded-box.ts's bare-glyph convention already matches 2 of 3 original
implementations (brunch-editor, workspace-dialog), proven byte-identical
in slices 2/3. No primitive change needed -- slice 4b re-baselines
cards.test.ts to the already-canonical convention, named explicitly.
2. Top-border label alignment (cards.ts left-aligns its title; the primitive
right-aligns, matching brunch-editor) is a real, visible difference and a
genuine scoping gap -- the original feature-coverage matrix incorrectly
recorded "label in top border" as one shared feature without checking
alignment. This does need a narrow, targeted primitive widening.
Un-supersedes the card, splits slice 4 into:
- 4a: RoundedBoxOptions gains labelAlign?: 'left'|'right' (default 'right',
non-breaking); border-line-with-label construction switches from
per-glyph color scanning to two contiguous colored runs -- simpler than
the current scanner, not more complex, and matches what all three
original implementations actually did.
- 4b: migrate cards.ts against the widened primitive, re-baselining only
the content-row coloring (named in the test/commit, not silent).
No code changed this commit -- scope only. Status flipped back to active.
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Not part of the ln-* skill family (check:skills validates 19 ln-* skills, unaffected by this) and not referenced anywhere else in the repo (docs/praxis/ ln-skills.md never listed it). Per user instruction: no longer necessary -- planning-doc edits stay on the active feature branch by default, which was already this skill's own recommended default in the common case; a dedicated advisory skill for the rare separate-planning-PR case wasn't earning its keep. .claude/skills/ is a directory symlink to .agents/skills/, so this single deletion removes it from both paths.
component-dx (FE-1115) is paused: the preview harness plus three shared
presentation primitives (projectScrollViewport, mouse-wheel.ts's wheel
decoder, projectRoundedBox) all shipped as either preview-harness-only work
or behavior-preserving refactors of an already-shipped component -- zero
production UX change throughout. Wiring BrunchEditorComponent into real
production surfaces and rebuilding the request_* response components is a
different risk category (the first genuine production UX change either
primitive will drive), so it splits into its own frontier rather than
continuing as more component-dx slices.
New frontier: bordered-chrome-production (Certainty: proving; no Linear
issue/branch yet). Its Objective deliberately separates three threads rather
than bundling them under one editor-installation framing, correcting an
implicit conflation from earlier in this session:
1. Main editor: wire BrunchEditorComponent via ctx.ui.setEditorComponent
(persistent input editor, D22-L/D35-L chrome territory) -- carries a
real, unverified assumption about the editor region's height budget.
2. answer (free-text): give request_response's collectAnswerFromSources
path a ctx.ui.custom-backed bordered component -- independent of #1;
ctx.ui.setEditorComponent and ctx.ui.editor(...) are structurally
distinct mechanisms in pi-coding-agent (confirmed against source), so
wiring the main editor does not touch request_response's answer dialog
at all.
3. choice/review: same ctx.ui.custom-backed treatment for
collectChoiceFromUi/collectReviewFromUi -- most ready-to-build of the
three per docs/design/STRUCTURED_EXCHANGE_ANSWERING_PATHS.md's coverage
matrix (already proven not to regress session.submitExchangeResponse).
Also trimmed memory/PLAN.md's Recently Completed list from 8 to 3 entries
(elicitation-gap-guidance plus the two component-dx completions directly
relevant to this split), archiving the other 7 one-liners to
docs/archive/PLAN_HISTORY.md per ln-plan's own step-7 maintenance procedure --
mechanical move, no re-summarization needed since the one-liners were already
well-formed.
Verified: check:skills OK (19 ln-* skills), check:markdown-links clean.
b6adbbb to
e293ee7
Compare
PR SummaryLow Risk Overview Introduces shared layout primitives Retires the unwired Reviewed by Cursor Bugbot for commit e293ee7. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
Pull request overview
Adds a standalone real-terminal preview harness for .pi/components (no workspace/session/DB), expands preview coverage to additional presentation lanes (message renderers + persistent chrome), and lands reusable bordered-presentation primitives (rounded box + scroll viewport + wheel decoding) that are exercised in both the harness and existing components.
Changes:
- Introduces
npm run dev:componentsharness (gallery + per-entry registry) with a realThemeandKeybindingsManager, plus dev-facing docs/topology updates. - Adds shared presentation primitives in
.pi/components/(projectRoundedBox,projectScrollViewport,parseWheelEvent) and refactorscards+workspace-dialogto use them. - Retires the unwired
tui-labextension registrar while keepingTuiStyleLabComponentas a previewable reference component; adds/updates tests across the new primitives and harness tiers.
Reviewed changes
Copilot reviewed 43 out of 44 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/session/TOPOLOGY.md | Documents exchange-broker scope and links to the structured answering-paths mechanism doc. |
| src/rpc/TOPOLOGY.md | Clarifies structural split between RPC submit vs live broker answering. |
| src/dev/TOPOLOGY.md | Adds dev-tooling ownership and documents the component preview harness. |
| src/dev/index.ts | Exports the component preview runner from the dev entry surface. |
| src/dev/component-preview/theme.ts | Creates a real Theme + EditorTheme adapter for preview rendering. |
| src/dev/component-preview/static-preview.ts | Adds static preview helpers for message-renderers and persistent chrome components. |
| src/dev/component-preview/registry.ts | Registers preview entries and mirrors production presentation contracts per component. |
| src/dev/component-preview/gallery-component.ts | Implements the interactive gallery menu component for choosing preview entries. |
| src/dev/component-preview/custom-ui.ts | Implements ctx.ui.custom-like preview wrapper including optional wheel-scroll translation. |
| src/dev/component-preview/tests/workspace-dialog-scroll.harness.test.ts | End-to-end harness test for the real workspace-dialog-scroll registry entry. |
| src/dev/component-preview/tests/theme.test.ts | Verifies preview theme emits expected 256-color ANSI and is usable by a real component. |
| src/dev/component-preview/tests/static-preview.test.ts | Tests static preview mounting + renderer capture for transcript message lane. |
| src/dev/component-preview/tests/custom-ui.test.ts | Tests overlay/inline behavior and wheel-scroll lifecycle + routing. |
| src/dev/component-preview.ts | Public harness entry: real terminal + TUI boot, deep-linking, and gallery loop. |
| src/.pi/extensions/TOPOLOGY.md | Removes tui-lab registrar from extensions inventory and explains retirement. |
| src/.pi/extensions/exchanges/TOPOLOGY.md | Links to the new structured answering paths mechanism doc. |
| src/.pi/components/workspace-dialog/index.ts | Re-exports WORKSPACE_DIALOG_MAX_VISIBLE_OPTIONS from the component module. |
| src/.pi/components/workspace-dialog/component.ts | Adds scroll-windowing + thumb rendering via new primitives; introduces fixed max-visible constant. |
| src/.pi/components/tui-lab/style-lab-component.ts | Removes registrar wiring; keeps TuiStyleLabComponent as reference-only component. |
| src/.pi/components/tui-lab/index.ts | Exposes TuiStyleLabComponent from the tui-lab public seam. |
| src/.pi/components/TOPOLOGY.md | Updates components layout + test tier descriptions; notes harness home under src/dev/. |
| src/.pi/components/scroll-viewport.ts | Adds pure scroll-viewport/windowing primitive with thumb-row reporting. |
| src/.pi/components/rounded-box.ts | Adds pure rounded-box projection primitive with label + thumb-row support. |
| src/.pi/components/mouse-wheel.ts | Adds pure SGR wheel-event decoder for preview harness wheel passthrough. |
| src/.pi/components/cards.ts | Refactors card border rendering to delegate to projectRoundedBox. |
| src/.pi/components/brunch-editor.ts | Adds bordered editor chrome projection + experimental BrunchEditorComponent. |
| src/.pi/components/tests/workspace-dialog.test.ts | Adds tests proving windowing, selection-follow, and thumb folding for long lists. |
| src/.pi/components/tests/tui-lab-style.test.ts | Keeps palette/primitive tests; drops registrar/bundle-exclusion tests after retirement. |
| src/.pi/components/tests/tui-lab-cycle.test.ts | Fixes imports after moving TuiStyleLabComponent under components. |
| src/.pi/components/tests/scroll-viewport.test.ts | Unit tests for projectScrollViewport behavior and thumb math. |
| src/.pi/components/tests/runtime-posture-axis-picker.harness.test.ts | Aligns harness presentation with production inline-swap behavior; updates assertions. |
| src/.pi/components/tests/rounded-box.test.ts | Unit tests for projectRoundedBox borders, labels, truncation, padding, and thumb rows. |
| src/.pi/components/tests/mouse-wheel.test.ts | Unit tests for SGR wheel decoding behavior. |
| src/.pi/components/tests/cards.test.ts | Updates/locks card rendering expectations after rounded-box refactor. |
| src/.pi/components/tests/brunch-editor.test.ts | Unit tests for bordered chrome projection, padding, autocomplete trailing rows, and links. |
| src/.pi/components/tests/brunch-editor.harness.test.ts | Harness tests for editor behavior through real TUI input routing and keybindings. |
| src/.pi/tests/support/virtual-terminal.ts | Adds a writes log to assert terminal write side-effects (mouse DECSET toggles). |
| scripts/dev-components.ts | Adds the script entrypoint for npm run dev:components. |
| package.json | Adds dev:components script. |
| package-lock.json | Adjusts pi-ai bin path to ./dist/cli.js. |
| memory/PLAN.md | Updates planning state: completes component-dx slices, introduces bordered-chrome-production frontier. |
| docs/design/STRUCTURED_EXCHANGE_ANSWERING_PATHS.md | New mechanism doc describing the three exchange answering paths and coverage matrix. |
| docs/archive/PLAN_HISTORY.md | Archives trimmed PLAN history entries and retired frontier-definition detail. |
| .agents/skills/planning-pr/SKILL.md | Removes the local planning-pr skill definition file. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| '', | ||
| this.theme.fg('dim', '\u2191/\u2193 or j/k move \u00b7 enter opens \u00b7 q quits'), | ||
| ]; | ||
| return lines.map((line) => line.slice(0, safeWidth)); |
| import type { KeybindingsManager, Theme } from '@earendil-works/pi-coding-agent'; | ||
| import { type Component, type TUI } from '@earendil-works/pi-tui'; |
| `npm run dev:components` (or `npm run dev:components:watch` for a `tsx watch`-backed edit loop) boots a | ||
| real `ProcessTerminal` + `TUI` and shows a gallery of every registered `.pi/components` entry | ||
| (`src/dev/component-preview/registry.ts`) — no seeded workbench, session, or DB required, since these | ||
| components are render-only with injectable `theme`/props. |
| tui.start(); | ||
| await entry.open(tui, theme, keybindings); | ||
| tui.stop(); | ||
| return; |

Add standalone .pi/components preview harness
npm run dev:components boots a real ProcessTerminal + TUI and shows a
gallery of every registered .pi/components entry - no seeded workbench,
session, or DB required, since these components are render-only with
injectable theme/props.
Each registry entry mirrors its component's real production presentation
contract (overlay-with-options vs. inline editor-swap), reimplementing
ExtensionUIContext.custom's documented calling shape rather than assuming
every component is an overlay. Theme is a real pi-coding-agent Theme
instance, not a duck-typed stand-in.
tooling, no Linear/branch by AGENTS.md convention)
Named but not fixed here: runtime-posture-axis-picker.harness.test.ts
wraps the picker in showOverlay when production uses inline-swap;
tui-lab's slash command remains unwired in pi-extensions.ts.
Promote component-preview-harness to component-dx frontier (FE-1115)
Fix axis-picker harness test to match real inline-swap presentation
The harness test wrapped the runtime mode picker in tui.showOverlay(...),
but production (commands/index.ts's openModePicker -> ctx.ui.custom with
no overlay options) presents it inline via tui.addChild + tui.setFocus.
Drift called out in the component-preview-harness card.
Switching presentation modes surfaced a second, related bug: the
active-segment assertions checked for a doubled trailing space after
the label, which only held because the overlay box's fixed-width fill
padded the line further. In the real inline-swap render the last track
segment gets no such fill, so the doubled-trailing-space check was a
false invariant. Replaced with a doubled-leading-space check, which
the badge + separator/padding stacking produces reliably regardless of
a segment's position in the track.
Also tightens src/.pi/components/TOPOLOGY.md's harness-tier description,
which generalized "drive through overlay" for all harness tests -- not
true for inline-swap components like this one.
Retire unwired tui-lab extension; keep TuiStyleLabComponent as harness reference
registerBrunchTuiLab (the /brunch:tui-style-lab command) never entered the
product extension bundle (proven by its own test suite) and was inert even
under Pi's ambient .pi/extensions/ directory scan, since ambient loading
calls default exports with no options argument and the registrar's own
enabled guard defaulted to false. It has had no working invocation path
since it landed in FE-845.
TuiStyleLabComponent (the actual render-only Component) moves to
.pi/components/tui-lab/style-lab-component.ts, matching the directory's
own ownership rule (components/ owns render-only Pi TUI components,
extensions/ owns wiring). It stays previewable and visible as a design
reference via 'npm run dev:components -- tui-lab', with no production
call site.
Split the extension-side test file: palette-rendering assertions move to
components/tests/tui-lab-style.test.ts; the cycle-demo component test
already lived under components/ and just gets its import path fixed.
Registration-behavior tests (enabled-gate, product-bundle-exclusion) are
dropped since the registrar they proved no longer exists.
Resolves the second component-preview-harness carried-forward finding.
Sweep component-dx topology drift + wiring
Audited every .pi/extensions/* registrar (all 19 wired to src/app/
pi-extensions.ts) and every .pi/components/* export (all consumed) against
real call sites -- tui-lab was the only orphan, already retired. Cross-checked
.pi/extensions/TOPOLOGY.md and src/dev/TOPOLOGY.md layout/path claims against
the real directory shape -- both already accurate.
One drift remained: .pi/components/TOPOLOGY.md's layout sketch still named
runtime-posture/strategy-picker.ts, retired in the prompt-axis retirement
(fc1b016). Only axis-picker.ts remains under runtime-posture/.
Extend preview harness to the transcript-message and chrome lanes
The harness previously only covered ctx.ui.custom (overlay/inline). Two more
real presentation contracts existed in .pi/components/ with no preview path:
pi.registerMessageRenderer (alternatives-card-set): a pure
(message, options, theme) => Component function with no done() callback.
static-preview.ts's captureMessageRenderer() feeds registerBrunchAlternatives
a minimal fake ExtensionAPI slice to capture the real renderer closure --
same technique registry.test.ts already uses to assert registration, just
taken one step further into actually rendering the output.
ui.setHeader (BrunchStartupHeader): turned out not to need new plumbing at
all -- it's already a plain Component with a public constructor, so it
slots into the same inline-swap machinery as any other static content.
Both mount via previewStaticComponent(), which wraps the static component
with a dismiss-on-any-key handler -- a preview-only affordance, since neither
lane has a real dismissal in production (a transcript message persists;
chrome stays mounted for the session).
Footer (ui.setFooter) is deliberately deferred: it's driven by live
token/model/coherence state rather than static layout, and rides the scope
the user wants to refine later.
Smoke-tested both new entries end-to-end via npm run dev:components (gallery
listing, deep-link, and gallery round-trip) using a real PTY, not just the
VirtualTerminal-backed unit tests.
Add experimental BrunchEditorComponent: bordered editor with runtime chrome
Design exploration for the component-dx frontier (design it twice via three
parallel divergent proposals, synthesized): a CustomEditor subclass that
overrides only render(), wrapping the inherited output in a │-bordered box
(the default Editor has no side borders at all) with runtime-state labels
baked into the border corners, matching the user's sketch.
projectBorderedChrome is a pure, domain-ignorant function (pre-formatted
label strings in, decorated lines out) so it stays reusable for the
request_* question-form pickers the user wants to refine next, not just
this editor.
Because a real Editor's bottom border is not reliably the last rendered
line -- autocomplete dropdown rows are appended after it -- the bottom
border is found by scanning backward for an Editor-shaped border line, not
a fixed index. Verified against the real (non-compiled) pi-tui Editor
source at ~/.pi/pi-mono/packages/tui, not just its .d.ts.
Registered as an [experimental] preview-harness entry only; not wired into
src/.pi/extensions/chrome/index.ts. This also forced component-preview.ts's
keybindings stub to become a real pi-tui KeybindingsManager, since
CustomEditor.handleInput needs .matches() for escape/ctrl+d -- a gap manual
PTY smoke-testing caught that the first unit tests missed (fixed and
back-filled with onEscape/onCtrlD harness tests).
Smoke-tested end-to-end via a real PTY: typing, box layout, escape-dismiss,
and gallery round-trip all confirmed working.
brunch-editor: rounded corners, min 2-line height, indented below-lines, clickable URL
existing box style (cards.ts).
empty: padContentToMinimum() inserts blank rows just before the detected
bottom border (never after, where autocomplete rows may live), shifting
the tracked bottom index accordingly.
rendered as a clickable OSC 8 hyperlink via pi-tui's own hyperlink() +
getCapabilities().hyperlinks gate (the same pattern pi-tui's Markdown
renderer already uses), falling back to plain text in terminals that
don't report hyperlink support. Kept as a generic union rather than a
dedicated 'url' field so belowLines stays usable for any future bordered
component, not just this editor's sidecar URL.
Smoke-tested end-to-end via a real PTY.
remove the "watch" component dev script
component-dx: projectScrollViewport primitive, workspace-dialog windowing + thumb, preview demo
Adds a pure scroll-window primitive, sibling to projectBorderedChrome:
projectScrollViewport(content, height, keepVisible?) => { lines, offset, isThumbRow }
Converged shape after cross-checking pi-tui's own Editor/SelectList windowing
against glyph, opentui, and lazygit/gocui — all five arrive at the same model
independently. Thumb math (size/position) mirrors the gocui/glyph proportional
formula; the thumb is reported per-row so a caller can fold it into whatever
border character it already draws (gocui/lazygit's approach, rather than a
separate column.
Wired into WorkspaceDialogComponent, fixing a real (not hypothetical) gap: its
specList/sessionList option rendering had no windowing at all, so once an
option list exceeded the overlay's resolved maxHeight, pi-tui's
compositeOverlays() silently truncated it (slice(0, maxHeight), no offset, no
indicator) and arrow-down could select an option already clipped off screen.
Height is a fixed WORKSPACE_DIALOG_MAX_VISIBLE_OPTIONS constant, not
terminal-rows-derived — the component holds no TUI reference today and
Component.render(width) never receives height; SelectList's own fixed
maxVisible is the precedented fit for this shape. #selectedIndex already
existed, so selection-follow windowing needs no new persisted scroll state.
Demoed in the component-preview harness via a new workspace-dialog-scroll
registry entry (20-spec fixture, same ctx.ui.custom overlay mounting path,
no new preview lane or wrapper component needed). Confirmed live through
agent-tui in a real terminal: window follows selection past both boundaries,
thumb tracks size/position top -> middle -> bottom correctly.
Deliberately not built this slice: wheel-scroll passthrough (needs a single
owner for the terminal mouse-mode toggle) and pointer-hover hit-testing
(pi-tui has no per-render row->component ownership map at all -- an upstream
change, not a component). Both named in memory/PLAN.md's component-dx entry.
Tests: scroll-viewport.test.ts (9 direct unit cases) + 3 new
workspace-dialog.test.ts cases (windowing, selection-follow past the edge,
thumb presence/absence scoped to option rows to avoid a false match against
the brand logo's own ANSI block art).
EOF
)
component-dx: ln-sync -- retire exhausted scope cards, archive completed frontiers
memory/cards/component-dx--component-preview-harness.md and
memory/cards/component-dx--scroll-viewport.md are deleted (the latter was
never committed -- created and reconciled within this session). Both fully
exhausted: their durable rationale now lives redundantly in code
(scroll-viewport.ts's docstring, workspace-dialog/component.ts's inline
comments), src/.pi/components/TOPOLOGY.md, src/dev/TOPOLOGY.md, and this
commit's memory/PLAN.md paragraph. Fixed the dangling "see memory/cards/..."
pointers left behind (plus two pre-existing stale pointers in
component-preview.ts/registry.ts that referenced a card by the wrong prefix,
predating this session) in the prior commit.
memory/PLAN.md's component-dx entry: "Current execution pointer" no longer
names now-deleted cards; states the direct-build-and-reconcile pattern going
forward. Folded in the two forward-looking facts the deleted scroll-viewport
card carried so they aren't lost: wheel-scroll passthrough is buildable
without forking pi-tui but needs a single owner for the terminal mouse-mode
toggle before it's built; true pointer-hover hit-testing is out of scope for
any brunch component (pi-tui has no per-render row->component ownership map
at all -- an upstream change).
elicitor-project (FE-1085) and structured-exchange-affordance (FE-1108), both
done since 2026-06-30, had full frontier definitions in PLAN.md duplicating
their existing Recently Completed one-liners with nothing downstream citing
the extra detail. Archived both full definitions to
docs/archive/PLAN_HISTORY.md under a new 2026-07-01 Sync archive section,
per the file's established per-frontier archive format.
Deleted the stale untracked HANDOFF.md (never committed this session): the
gap it named -- no scope card for the scroll-viewport work -- was resolved
twice over (a card was created, then its content fully reconciled into
canonical homes).
component-dx: ln-spike -- wheel-scroll passthrough confirmed viable, no pi-tui fork needed
Spike question: can Brunch receive/parse real terminal mouse-wheel escape
sequences through pi-tui's existing input pipeline without forking it, and
is enabling mouse-tracking mode safe for components that don't know about it?
Verdict: yes, confirmed empirically via a throwaway probe (deleted,
.fixtures/scratch/mouse-spike/) run live under agent-tui. tui.terminal.write
enables SGR mouse mode; stdin-buffer.ts's existing SGR-sequence framing
delivers a wheel event byte-intact and unsplit to both addInputListener and
a focused component's handleInput; an unaware Editor-based component safely
ignores it today (ESC-prefixed, matches neither decodePrintableKey decode
path nor the >=32 printable-insertion guard).
Sub-finding: agent-tui's scroll command is a keystroke-emulation shim (sends
plain ArrowDown, not a real SGR mouse event) -- had to inject the literal
sequence via agent-tui type instead.
Not resolved (named, not a blocker): the native-text-selection UX tradeoff of
enabling mouse mode, and who owns the global enable/disable lifecycle -- both
design/ownership questions for a future ln-scope, not technical unknowns.
No memory/SPEC.md change -- component-dx carries no SPEC traceability for
tooling/component-level work, and there was no prior assumption entry about
mouse support to update.
component-dx: ln-scope -- wheel-scroll passthrough tracer (memory/cards/component-dx--wheel-scroll-passthrough.md)
Full scope card for the next component-dx slice after the 2026-07-01
ln-spike confirmed the mechanism works. Scopes a single, minimal tracer:
recognizing the SGR wheel-up/wheel-down shape, ignoring click/motion/
non-mouse input.
showComponentPreview shim (custom-ui.ts) -- opt-in per entry, mirroring
the existing overlay boolean's shape. When set, owns enable/disable of
SGR mouse mode for exactly that entry's open/close lifecycle and
synthesizes the equivalent ArrowUp/ArrowDown keypress into the already-
created component's existing handleInput -- no new component-level API.
Deliberately scoped as harness-only (no production wiring, no new
component-level API) with two named residuals: a real-terminal smoke test
(agent-tui cannot exercise a real physical wheel event -- confirmed by the
prior spike) and the production lifecycle-ownership question (this slice's
per-entry opt-in does not answer who owns it for a real long-lived TUI
session).
memory/PLAN.md's component-dx execution pointer now names this card.
component-dx: wheel-scroll passthrough for the workspace-dialog-scroll preview
Implements memory/cards/component-dx--wheel-scroll-passthrough.md (deleted,
consumed):
wheel-up/wheel-down shape (bit 64), rejecting motion (bit 32),
release ('m'), and non-wheel button codes (clicks, horizontal wheel).
Pure, unit-tested in isolation.
an opt-in wheelScroll?: boolean, mirroring the existing overlay boolean's
shape. When set, showComponentPreview owns SGR mouse enable/disable for
exactly that entry's open/close lifecycle and rewrites a recognized wheel
event's raw bytes to the equivalent ArrowUp/ArrowDown sequence via the
addInputListener data-rewrite path -- so TUI's own dispatch delivers it to
whichever component currently has focus, rather than a hardcoded
reference to the originally-created component. No new component-level
API; WorkspaceDialogComponent is unchanged.
other entry is affected (regression-tested).
enable/disable sequences in tests; no existing test behavior changed.
Test coverage across all three tiers named in the card: parseWheelEvent
unit tests; custom-ui.test.ts proving the opt-in enable/disable lifecycle
and wheel-to-arrow-key routing generically (including a regression case
that no entry gets mouse mode by default); and a harness test driving the
real workspace-dialog-scroll registry entry through a VirtualTerminal,
proving wheel-down reaches the identical rendered state as an equivalent
ArrowDown keypress.
Fixed one placement issue found in review (my own scoping miss, not the
builder's): the real-entry harness test was originally added to
.pi/components/tests/workspace-dialog.test.ts, importing
src/dev/component-preview/registry.js -- inverting the established
dependency direction (no existing .harness.test.ts under .pi/components/
imports from src/dev/; they all construct their component directly).
Moved it to src/dev/component-preview/tests/workspace-dialog-scroll.harness.test.ts,
matching precedent and the .harness.test.ts naming convention.
workspace-dialog.test.ts is now byte-identical to its prior committed state.
memory/PLAN.md's component-dx entry and both TOPOLOGY.md homes (.pi/components,
dev) are reconciled, naming the residuals honestly: a manual real-terminal
(iTerm2/Kitty/Ghostty) smoke test to confirm physical wheel emission matches
the injected SGR shape proved in harness, and production lifecycle ownership
of the mouse-mode toggle (this slice's per-entry opt-in deliberately does
not answer that for a real long-lived TUI session).
Left memory/cards/orchestrator-tool-port--plan-check-tool.md and
docs/design/CUELOOP_PATTERN_LIFTOUT.md untouched -- concurrent work from
another agent in this worktree, outside this session's manifest.