Skip to content

feat(tui): list preview pane at wide terminals#1749

Merged
pedrofuentes merged 4 commits into
mainfrom
feature/tui-preview-pane
Jun 26, 2026
Merged

feat(tui): list preview pane at wide terminals#1749
pedrofuentes merged 4 commits into
mainfrom
feature/tui-preview-pane

Conversation

@pedrofuentes

Copy link
Copy Markdown
Owner

Summary

ListViewport gains an optional renderPreview shown beside the list at ≥120 cols; Panels/Experts/Sessions/Chats provide a selected-item preview; narrow-width rendering unchanged; Phase 10 TUI overhaul — PR-17

Changes

ListViewport

  • Add PREVIEW_MIN_COLS = 120 const
  • Add optional props: renderPreview?: (selectedId: string) => React.ReactNode and stdout?: ResizableStdout (for test injection)
  • When renderPreview is provided and terminal columns >= 120, renders a two-column layout: existing list on the left, bordered preview pane on the right showing renderPreview(selectedItemId)
  • Preview updates as the cursor moves (safeCursor drives selectedId)
  • Narrow widths or absent renderPreview → falls through to unchanged single-column layout

Screen migrations (PanelsScreen, ExpertsScreen, SessionsScreen, ChatsScreen)

  • Each gains readonly stdout?: ResizableStdout | undefined prop (threaded to ListViewport for test injection)
  • Each passes a renderPreview that builds a sanitized short summary from the loaded list data:
    • Panels: name · memberCount members · source
    • Experts: displayName · role [kind] · panelCount panels
    • Sessions: status symbol · panelName · debate/turn counts · topic
    • Chats: target symbol · targetSlug · title · status · when
  • All preview strings pass through toSingleLineDisplay (sanitized)

Tests

  • list-viewport.test.tsx: 4 new tests — preview renders at wide width (140 cols via FakeStdout), updates on cursor move, absent at narrow (80 cols), absent without renderPreview
  • panels-screen.test.tsx: 1 new test — preview "members" label appears at wide width; raw \u001B[31m from tainted panel name does not bleed through

Out of scope

AppRouter/footer/nav/Header; detail screens; narrow-width rendering (unchanged).

pedrofuentes and others added 4 commits June 26, 2026 09:11
- Extend list-viewport.test.tsx: preview renders at wide widths (>=120
  cols), updates as cursor moves, absent at narrow widths, absent when
  renderPreview prop is not provided
- Extend panels-screen.test.tsx: panel preview summary ("members"
  label) appears beside list when stdout is wide (140 cols)

Co-authored-by: Copilot <175574315+pedrofuentes@users.noreply.github.com>
- Add PREVIEW_MIN_COLS=120 const and optional renderPreview/stdout props
  to ListViewportProps; passes stdout to useTerminalSize for injection
- At columns>=120 with renderPreview provided, render a two-column layout:
  list on the left, bordered preview pane on the right showing
  renderPreview(selectedItemId); narrows fall through to existing layout
- Preview updates as the cursor moves (safeCursor drives selectedId)
- Migrate PanelsScreen, ExpertsScreen, SessionsScreen, ChatsScreen:
  each accepts stdout? for test injection and passes a renderPreview
  rendering a sanitized short summary of the selected entity
- All preview strings pass through toSingleLineDisplay; no new imports
  of @github/copilot-sdk or restricted modules

Co-authored-by: Copilot <175574315+pedrofuentes@users.noreply.github.com>
The initial assertion not.toContain('\u001B[') was too broad: Ink's own
border and bold styling legitimately emit ANSI CSI sequences. Fix by
embedding \u001B[31m in the test-data panel name and asserting that the
specific red-ANSI sequence does NOT appear in the preview (i.e., the
renderPreview callback sanitizes user-supplied content via
toSingleLineDisplay).

Co-authored-by: Copilot <175574315+pedrofuentes@users.noreply.github.com>
Pass stdout to useTerminalSize conditionally (only when defined) to
satisfy exactOptionalPropertyTypes=true; add | undefined to all optional
stdout and renderPreview declarations in ListViewportProps and the four
screen props interfaces.

Co-authored-by: Copilot <175574315+pedrofuentes@users.noreply.github.com>
@pedrofuentes

pedrofuentes commented Jun 26, 2026

Copy link
Copy Markdown
Owner Author

Status: APPROVED

Sentinel Review Report

Ref: feature/tui-preview-pane → main
Report ID: sentinel-1749-52fa148-20260626T163300Z
Reviewed SHA: 52fa148
Sentinel ruleset: v1
Reviewed at: 2026-06-26T16:33:00Z
Mode: standard
Review depth: Tier 2 (full)
Required action: MERGE

Phase 1 — TDD / Test Evidence

  • Tests exist & meaningful: ✅ packages/cli/tests/unit/tui/list-viewport.test.tsx:294-309 asserts wide preview renders (expect(lastFrame()).toContain("PREVIEW:a")); :312-329 asserts cursor movement updates preview; :332-346 asserts narrow width hides preview; :349-362 asserts no renderPreview keeps single-column output. packages/cli/tests/unit/tui/panels-screen.test.tsx:250-269 asserts a wide panel preview renders members and strips raw \u001B[31m.
  • Red evidence verified: ✅ At test commit 4e8d731, targeted preview tests failed before implementation with AssertionError: expected '1/3\n\u001b[7m› Alpha...' to contain 'PREVIEW:a' and AssertionError: expected 'Panels 1/2\n\u001b[7m› alpha-panel...' to contain 'members'.
  • Type-fix red evidence verified: ✅ At 20d5b17, npx --yes pnpm@10.30.1 typecheck failed with error TS2379: Argument of type '{ stdout: ResizableStdout | undefined; }' is not assignable to parameter of type 'UseTerminalSizeOptions' with 'exactOptionalPropertyTypes: true'. The reviewed SHA passes typecheck.
  • Test-first history verified: ✅ git log 7c4905d..HEAD shows 4e8d731 testf332629 feat20d5b17 test52fa148 fix. git show --name-status verified impl commits modify source only (f332629, 52fa148) and test commits modify tests only (4e8d731, 20d5b17).
  • Full suite green on SHA: ✅ npx --yes pnpm@10.30.1 test completed with Test Files 356 passed | 1 skipped (357) and Tests 4487 passed | 1 skipped (4488).
  • Targeted TUI suite green on SHA: ✅ cd packages/cli && npx --yes pnpm@10.30.1 exec vitest run tests/unit/tui completed with Test Files 93 passed (93) and Tests 869 passed (869).
  • Lint green on SHA: ✅ npx --yes pnpm@10.30.1 lint exited 0 after eslint packages/cli/src packages/cli/tests.
  • Typecheck green on SHA: ✅ npx --yes pnpm@10.30.1 typecheck exited 0 after tsc --noEmit.
  • Coverage: N/A (no coverage threshold enforced/requested for this PR).

Phase 1.5 — Fast-path Evaluation

🔴 count: 0 | LOC: 132 non-test/non-lockfile changed lines (≤150: Y) | Security paths: Y (TUI renders untrusted content to terminal/UI) | New deps: N | Commit types qualify: N (feat present)
→ Fast-path eligible: NO → Phase 2

Phase 2 — Execution Log

Dim Tool Call Agent ID / Ref Status
Quick scan task(agent_type="general-purpose", name="sentinel-1749-quick", model="claude-haiku-4.5") via multi_tool_use.parallel N/A (Copilot CLI returned synchronous result, no ID exposed) ✅ 0 🔴 blockers
A1 task(agent_type="general-purpose", name="sentinel-1749-a1", model="claude-sonnet-4.6") via multi_tool_use.parallel N/A (Copilot CLI returned synchronous result, no ID exposed) ✅ No findings
A2 task(agent_type="general-purpose", name="sentinel-1749-a2", model="claude-sonnet-4.6") via multi_tool_use.parallel N/A (Copilot CLI returned synchronous result, no ID exposed) ✅ No findings
B task(agent_type="general-purpose", name="sentinel-1749-b", model="claude-sonnet-4.6") via multi_tool_use.parallel N/A (Copilot CLI returned synchronous result, no ID exposed) ✅ Reviewed; advisory false-positive omitted (empty/no-match states return before preview at ListViewport.tsx:136-157)
C task(agent_type="general-purpose", name="sentinel-1749-c", model="claude-sonnet-4.6") via multi_tool_use.parallel N/A (Copilot CLI returned synchronous result, no ID exposed) ✅ Reviewed; minor polish below materiality omitted
D task(agent_type="general-purpose", name="sentinel-1749-d", model="claude-sonnet-4.6") via multi_tool_use.parallel N/A (Copilot CLI returned synchronous result, no ID exposed) ✅ Reviewed; tests meaningful, boundary advisory below materiality
E N/A N/A ✅ N/A (no dependency/supply-chain surface changed)
F task(agent_type="general-purpose", name="sentinel-1749-f", model="claude-haiku-4.5") via multi_tool_use.parallel N/A (Copilot CLI returned synchronous result, no ID exposed) ✅ Reviewed; docs advisories below materiality omitted
Persist gh pr comment 1749 --body-file -; then gh pr comment 1749 --edit-last --body-file - #1749 (comment) ✅ Report persisted

Findings

  • 🔴 CRITICAL: 0
  • 🟡 IMPORTANT: 0 new / 0 known
  • 🟢 MINOR: 0

Details (ordered by severity)

No reportable findings.

Follow-ups & Actions

Decision rationale

  • Reviewed SHA is exactly 52fa14815e7e8069d22cee4961d2594febb04b7f; final git status --short was clean.
  • TDD ordering and red evidence were verified for the preview feature and exact-optional-property type fix.
  • Full tests, TUI tests, lint, and typecheck all pass on the reviewed SHA.
  • AppRouter is not in the PR diff; optional stdout/renderPreview props do not force router changes.
  • Preview/list user or DB content is sanitized with toSingleLineDisplay; constrained enum/computed fields are not attacker-controlled display text. useTerminalSize removes resize listeners on unmount, so no setState-after-unmount issue was introduced.

@pedrofuentes pedrofuentes merged commit 80d5806 into main Jun 26, 2026
14 of 15 checks passed
@pedrofuentes pedrofuentes deleted the feature/tui-preview-pane branch June 26, 2026 16:34
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.

1 participant