Skip to content

Add standalone .pi/components preview harness#281

Open
lunelson wants to merge 25 commits into
graphite-base/281from
ln/fe-1115-component-preview-dx
Open

Add standalone .pi/components preview harness#281
lunelson wants to merge 25 commits into
graphite-base/281from
ln/fe-1115-component-preview-dx

Conversation

@lunelson

@lunelson lunelson commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

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.

  • 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.

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

  • 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.

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:

  • 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.

component-dx: wheel-scroll passthrough for the workspace-dialog-scroll 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.

lunelson commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

lunelson and others added 25 commits July 1, 2026 17:17
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.
@lunelson lunelson changed the base branch from next to graphite-base/281 July 1, 2026 16:21
@lunelson lunelson force-pushed the ln/fe-1115-component-preview-dx branch from b6adbbb to e293ee7 Compare July 1, 2026 16:21
@lunelson lunelson changed the base branch from graphite-base/281 to ln/fe-1116-elicitation-gap-guidance July 1, 2026 16:21
@lunelson lunelson changed the base branch from ln/fe-1116-elicitation-gap-guidance to graphite-base/281 July 1, 2026 16:21
@lunelson lunelson marked this pull request as ready for review July 1, 2026 16:28
Copilot AI review requested due to automatic review settings July 1, 2026 16:28
@cursor

cursor Bot commented Jul 1, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Changes are mostly dev harness and presentation refactors; the main live TUI delta is workspace-dialog list windowing for overflow, with no auth, RPC, or production chrome wiring in this PR.

Overview
Adds npm run dev:components, a real-terminal gallery for .pi/components that mirrors each entry’s production presentation contract (ctx.ui.custom overlay vs inline swap, transcript renderers, chrome header, experimental editor slot), plus showComponentPreview wheel-scroll opt-in via SGR mouse → arrow-key translation.

Introduces shared layout primitives projectRoundedBox, projectScrollViewport, and parseWheelEvent, refactors CardComponent and WorkspaceDialogComponent to use them (long spec/session lists now window with a border-folded scroll thumb), and adds experimental BrunchEditorComponent / projectBorderedChrome for harness-only bordered-editor exploration.

Retires the unwired tui-lab extension registrar; TuiStyleLabComponent moves under .pi/components/tui-lab/ as a preview-only reference. Planning/docs: new docs/design/STRUCTURED_EXCHANGE_ANSWERING_PATHS.md, memory/PLAN.md marks component-dx paused and splits follow-on bordered-chrome-production; removes .agents/skills/planning-pr/SKILL.md and archives completed frontier text to docs/archive/PLAN_HISTORY.md.

Reviewed by Cursor Bugbot for commit e293ee7. Bugbot is set up for automated code reviews on this repo. Configure here.

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 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:components harness (gallery + per-entry registry) with a real Theme and KeybindingsManager, plus dev-facing docs/topology updates.
  • Adds shared presentation primitives in .pi/components/ (projectRoundedBox, projectScrollViewport, parseWheelEvent) and refactors cards + workspace-dialog to use them.
  • Retires the unwired tui-lab extension registrar while keeping TuiStyleLabComponent as 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));
Comment on lines +1 to +2
import type { KeybindingsManager, Theme } from '@earendil-works/pi-coding-agent';
import { type Component, type TUI } from '@earendil-works/pi-tui';
Comment thread src/dev/TOPOLOGY.md
Comment on lines +39 to +42
`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.
Comment on lines +60 to +63
tui.start();
await entry.open(tui, theme, keybindings);
tui.stop();
return;
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