diff --git a/docs/ai/design/2026-07-02-feature-agent-console-detail-chat-view.md b/docs/ai/design/2026-07-02-feature-agent-console-detail-chat-view.md new file mode 100644 index 00000000..3f218662 --- /dev/null +++ b/docs/ai/design/2026-07-02-feature-agent-console-detail-chat-view.md @@ -0,0 +1,158 @@ +--- +phase: design +title: Agent Console Detail Chat View - System Design +description: Technical design for focused and scrollable chat content in the interactive agent console +--- + +# System Design & Architecture + +## Architecture Overview + +The feature extends the existing Ink console focus model. The agent list continues to own selection; the right-side preview/detail pane becomes a focus target that can consume scroll keys without changing list selection. + +```mermaid +graph TD + Input["ConsoleApp useInput"] + Focus["ConsoleFocus: list | detail | input"] + List["AgentListPane"] + PreviewSection["PreviewSection"] + PreviewPane["PreviewPane"] + Conversation["useAgentConversation"] + Adapter["AgentAdapter.getConversation()"] + + Input -->|v| Focus + Input -->|list focus arrows/j/k| List + Input -->|detail focus arrows/j/k| PreviewSection + Focus -->|focused prop| PreviewSection + PreviewSection --> PreviewPane + PreviewSection --> Conversation + Conversation --> Adapter +``` + +Responsibilities: + +- `ConsoleApp`: owns focus state, global key routing, selected agent, and detail scroll offset. +- `PreviewSection`: receives focus and scroll offset props, keeps fetching selected-agent messages, computes/clamps the valid viewport range, and passes render constraints to `PreviewPane`. +- `PreviewPane`: renders metadata, chat rows, empty/error states, and scroll affordances from a supplied clamped viewport. +- `useAgentConversation`: remains responsible only for polling/caching conversation messages. + +## Data Models + +Extend console focus: + +```typescript +export type ConsoleFocus = 'list' | 'detail' | 'input'; +``` + +Add scroll state in `ConsoleApp`: + +```typescript +const [detailScrollOffset, setDetailScrollOffset] = useState(0); +``` + +The offset represents logical rendered lines above the current detail viewport. A value of `0` means the viewport is pinned to the newest/latest rendered content at the bottom. Positive values move the viewport upward into older chat content. + +`PreviewPane` should expose pure helpers for testable rendering math: + +```typescript +interface DetailViewport { + lines: string[]; + clampedOffset: number; + maxOffset: number; + hasAbove: boolean; + hasBelow: boolean; +} +``` + +`clampedOffset` is returned so `PreviewSection` can notify `ConsoleApp` when the current offset is no longer valid after a message update or terminal resize. + +## API Design + +No external API changes. + +Internal prop changes: + +```typescript +interface PreviewSectionProps { + selectedName: string | null; + height: number; + focused?: boolean; + scrollOffset?: number; + onScrollOffsetClamp?: (offset: number) => void; +} + +interface PreviewPaneProps { + agent: AgentInfo | null; + messages: ConversationMessage[]; + error: ConversationFetchError | null; + isLoading: boolean; + maxLines?: number; + channelStatus?: AgentChannelStatus; + scrollOffset?: number; + onScrollOffsetClamp?: (offset: number) => void; +} +``` + +`ConsoleApp` key behavior: + +- `v` from list focus: set focus to `detail` if an agent is selected and wide preview is visible. +- `Esc` or left arrow from detail focus: set focus to `list`. +- Up/down arrows and `j/k` from detail focus: adjust `detailScrollOffset` within `[0, maxOffset]`. +- Because `ConsoleApp` does not own rendered-line calculation, scroll key handling may increment/decrement the requested offset and rely on `PreviewSection`/`PreviewPane` to clamp and report the valid value. +- Up/down arrows and `j/k` from list focus: keep existing selected-agent navigation. +- `i`/`m` from detail focus: may enter input focus when an agent is selected, same as list focus. + +## Component Breakdown + +Console components: + +- `packages/cli/src/tui/console/types.ts` + - Add `detail` to `ConsoleFocus`. +- `packages/cli/src/tui/console/ConsoleApp.tsx` + - Route `v`, scroll, escape, and left-arrow behavior. + - Reset detail scroll offset when selected agent changes. + - Pass `focused={focus === 'detail'}` into `PreviewSection`. +- `packages/cli/src/tui/console/PreviewSection.tsx` + - Apply focused panel styling to the preview panel. + - Pass scroll offset and message budget into `PreviewPane`. + - Clamp scroll offset when messages or height change and report the clamped offset upward. +- `packages/cli/src/tui/console/PreviewPane.tsx` + - Render a viewport over chat content instead of truncating only from the newest message. + - Show simple `↑`/`↓` affordances in the header when more content exists. + - Preserve current metadata, channel status, loading, empty, and error output. +- Tests under `packages/cli/src/__tests__/tui/console` + - Add helper tests for viewport calculations and update focus/layout tests where needed. + +## Design Decisions + +| Decision | Choice | Rationale | +| --- | --- | --- | +| Focus model | Add `detail` to `ConsoleFocus` | Matches existing focus routing and panel styling without a new modal mode. | +| Shortcut | `v` enters detail focus | Matches the requested workflow and avoids existing console shortcuts. | +| Scroll ownership | `ConsoleApp` owns requested offset; preview clamps it | Key handling and selected-agent reset stay centralized while rendered-line math stays near preview rendering. | +| Rendering math | Pure helper in preview module | Keeps Ink rendering simple and gives unit tests deterministic coverage. | +| Narrow mode | Keep `v` inactive while preview is hidden | Avoids focusing invisible UI; future full-screen transcript can be designed separately. | + +Alternatives considered: + +- Reuse `agent detail` command output inside the console: rejected because it would duplicate formatting and bypass existing preview polling. +- Add a replacement-pane mode for detail: deferred because the requested workflow specifically highlights the detail pane and current wide layout already has one. +- Fetch full session history by default: deferred to avoid changing polling cost; basic scrolling can operate on the current conversation tail first, then increase tail if needed during implementation review. + +## Non-Functional Requirements + +Performance: + +- Scroll keypresses must not read session files or call `getConversation()`. +- Viewport calculation should be linear in the currently loaded message count. +- Existing polling interval and cache behavior should remain unchanged unless implementation proves the 20-message tail is too limiting for the feature. + +Reliability: + +- Missing session files, unsupported adapters, parse errors, empty message sets, and loading states must render with existing behavior while focused. +- Scroll offset must be clamped when terminal height changes or message count changes. + +Security: + +- No new file paths, shell commands, network calls, or permissions are introduced. +- Conversation content is already local session content exposed by the console; this feature changes navigation only. diff --git a/docs/ai/implementation/2026-07-02-feature-agent-console-detail-chat-view.md b/docs/ai/implementation/2026-07-02-feature-agent-console-detail-chat-view.md new file mode 100644 index 00000000..876c14db --- /dev/null +++ b/docs/ai/implementation/2026-07-02-feature-agent-console-detail-chat-view.md @@ -0,0 +1,100 @@ +--- +phase: implementation +title: Agent Console Detail Chat View - Implementation Guide +description: Implementation notes for focused and scrollable chat content in the interactive agent console +--- + +# Implementation Guide + +## Development Setup + +- Active worktree: `.worktrees/feature-agent-console-detail-chat-view` +- Branch: `feature-agent-console-detail-chat-view` +- Dependencies: bootstrapped with `npm ci` +- Feature docs: `docs/ai/*/2026-07-02-feature-agent-console-detail-chat-view.md` + +## Code Structure + +- `packages/cli/src/tui/console/types.ts` +- `packages/cli/src/tui/console/ConsoleApp.tsx` +- `packages/cli/src/tui/console/PreviewSection.tsx` +- `packages/cli/src/tui/console/PreviewPane.tsx` +- `packages/cli/src/tui/console/consoleKeyRouting.ts` +- `packages/cli/src/__tests__/tui/console/PreviewPane.test.ts` +- `packages/cli/src/__tests__/tui/console/focusRouting.test.ts` +- `packages/cli/src/__tests__/tui/console/HelpPane.test.ts` + +## Implementation Notes + +### Core Features + +- Add `detail` as a third console focus state instead of adding a new right-pane mode. +- Keep `PreviewSection` visible in wide mode and focused via the existing `Panel` `focused` prop. +- Keep scroll state in `ConsoleApp`; avoid coupling scroll behavior to conversation polling. +- Prefer pure helper functions for scroll math so tests do not depend on terminal rendering snapshots. +- Added `resolveConsoleKeyAction()` as a pure helper for list/detail/input key routing. +- Added `buildPreviewViewport()` as a pure helper for visible chat lines, offset clamping, and scroll affordance state. +- Added `v view` to console help/footer shortcuts. + +### Patterns & Best Practices + +- Follow existing Ink component patterns and panel design-system usage. +- Keep new helpers exported only when tests need direct access. +- Avoid introducing new session parsing or file access in UI components. +- Clamp scroll offsets whenever viewport size or message count changes. + +## Integration Points + +- `useAgentConversation` continues to fetch selected-agent conversation data. +- `PreviewPane` receives already-loaded `ConversationMessage[]`; scroll changes must not fetch. +- `ConsoleApp` owns key routing and passes scroll/focus props down the preview tree. +- `PreviewSection` forwards clamped offsets from `PreviewPane` back to `ConsoleApp`. + +## Error Handling + +- Preserve current `PreviewPane` error states for missing session files, unsupported adapters, and parse errors. +- Detail focus with an error/empty/loading state should still render a focused panel and treat scroll as a no-op. +- Do not add console logging for normal key routing. + +## Performance Considerations + +- Scroll updates should be state-only UI updates. +- Keep existing conversation cache and polling interval unchanged unless implementation review identifies a functional gap. +- Avoid recalculating viewport structures more broadly than the loaded message array and current viewport need. + +## Security Notes + +- No new command execution, network calls, or writes. +- The feature only changes local display of already accessible agent session content. + +## Implementation Results + +Changed files: + +- `packages/cli/src/tui/console/types.ts`: added `detail` focus state. +- `packages/cli/src/tui/console/consoleKeyRouting.ts`: added pure key-routing helper. +- `packages/cli/src/tui/console/ConsoleApp.tsx`: added detail scroll state, focus transitions, selected-agent scroll reset, and scroll-key handling. +- `packages/cli/src/tui/console/PreviewSection.tsx`: forwards focus, scroll offset, and clamp callback into the preview panel. +- `packages/cli/src/tui/console/PreviewPane.tsx`: added viewport helper, clamped offset reporting, scroll affordances, and line-based chat viewport rendering. +- `packages/cli/src/tui/console/HelpPane.tsx`: added `v view` hotkey. +- `packages/cli/src/__tests__/tui/console/PreviewPane.test.ts`: covers viewport helper behavior. +- `packages/cli/src/__tests__/tui/console/focusRouting.test.ts`: covers list/detail/input key routing. +- `packages/cli/src/__tests__/tui/console/HelpPane.test.ts`: covers new help/footer shortcut. + +Simplification pass: + +- `PreviewViewport` now returns typed rows instead of strings, so rendering uses `row.role` directly rather than inferring color from string prefixes. +- `ConsoleApp` key action handling now uses a `switch` to keep focus, scroll, and selection actions grouped in one dispatch block. +- `focusRouting.test.ts` now uses a local setup helper to remove repeated default key-routing inputs. + +Design deviations: + +- None. The implementation follows the reviewed design split: `ConsoleApp` owns requested offset and preview rendering clamps/report valid offsets. + +Verification evidence: + +- `npx vitest run src/__tests__/tui/console/PreviewPane.test.ts src/__tests__/tui/console/focusRouting.test.ts src/__tests__/tui/console/HelpPane.test.ts` passed: 3 files, 18 tests. +- `npx vitest run src/__tests__/tui/console` passed: 16 files, 106 tests. +- `npm run build` passed for all 6 projects. +- `npm run lint --workspace packages/cli` exited 0 with 5 pre-existing warnings outside touched files. +- `npx ai-devkit@latest lint --feature agent-console-detail-chat-view` passed after simplification. diff --git a/docs/ai/planning/2026-07-02-feature-agent-console-detail-chat-view.md b/docs/ai/planning/2026-07-02-feature-agent-console-detail-chat-view.md new file mode 100644 index 00000000..b1cb8d94 --- /dev/null +++ b/docs/ai/planning/2026-07-02-feature-agent-console-detail-chat-view.md @@ -0,0 +1,121 @@ +--- +phase: planning +title: Agent Console Detail Chat View - Project Planning +description: Initial task breakdown for focused and scrollable chat content in the interactive agent console +--- + +# Project Planning & Task Breakdown + +## Milestones + +- [x] Milestone 1: Requirements/design/testing reviewed and accepted +- [x] Milestone 2: Focus and scroll behavior implemented with tests +- [x] Milestone 3: Console regression tests complete; manual terminal smoke test deferred (PTY scan returns 0 agents; real-terminal verification recommended before broad rollout) + +## Task Breakdown + +### Phase 1: Focus Model + +- [x] Task 1.1: Add `detail` to `ConsoleFocus` and route focus transitions in `ConsoleApp`. + - Outcome: pressing `v` from list focus in wide mode enters detail focus when an agent is selected; `Esc` and left arrow return to list focus. + - Dependencies: none. + - Validation: focused key-routing helper tests cover `v`, `Esc`, left arrow, no-agent, and narrow-mode no-op behavior. + - Related tests: ConsoleApp focus/key helper scenarios. +- [x] Task 1.2: Preserve existing list and input keyboard behavior. + - Outcome: up/down and `j/k` still change selected agent in list focus; `i`/`m` still enter input focus; input cancel still returns to list focus. + - Dependencies: Task 1.1. + - Validation: regression tests for list navigation and input focus routing. + - Related tests: ConsoleApp focus/key helper scenarios and existing console tests. +- [x] Task 1.3: Reset detail scroll offset when selected agent changes. + - Outcome: switching agents returns the detail pane to latest content instead of carrying the prior agent's scroll position. + - Dependencies: Task 1.1. + - Validation: helper or component test verifies selected-agent change resets offset. + - Related tests: ConsoleApp focus/key helper scenarios. + +### Phase 2: Scrollable Preview Rendering + +- [x] Task 2.1: Add pure viewport/scroll helper logic for preview chat content. + - Outcome: helper converts `ConversationMessage[]`, message budget, and requested scroll offset into visible lines, clamped offset, max offset, and above/below affordance state. + - Dependencies: none, but implement before wiring Ink rendering. + - Validation: unit tests cover newest view, older view, negative offset, too-large offset, long conversations, and multi-line messages. + - Related tests: PreviewPane viewport helper scenarios. +- [x] Task 2.2: Pass focus and scroll offset through `PreviewSection` to `PreviewPane`. + - Outcome: focused preview uses existing `Panel` focus styling and receives the requested detail scroll offset. + - Dependencies: Tasks 1.1 and 2.1. + - Validation: component/helper test verifies focused panel prop and visible content changes with offset. + - Related tests: integration scenarios for focused `PreviewSection`. +- [x] Task 2.3: Render scroll affordances when older/newer content exists outside the viewport. + - Outcome: preview header shows subtle up/down indicators only when scrolling is possible. + - Dependencies: Task 2.1. + - Validation: unit tests verify `hasAbove`/`hasBelow` states and snapshot-light text assertions for header indicators. + - Related tests: PreviewPane viewport helper scenarios. +- [x] Task 2.4: Clamp requested detail scroll offset when messages or terminal height change. + - Outcome: preview reports clamped offsets upward so `ConsoleApp` state cannot remain outside the valid viewport range. + - Dependencies: Tasks 2.1 and 2.2. + - Validation: test covers shrinking content/height and verifies the callback receives the valid offset. + - Related tests: integration scenarios for changing message count or `maxLines`. + +### Phase 3: Tests & Polish + +- [x] Task 3.1: Add unit tests for viewport clamping, visible-line selection, and affordance state. + - Outcome: pure scroll math has deterministic coverage. + - Dependencies: Task 2.1. + - Validation: targeted `PreviewPane.test.ts` or new helper test passes. +- [x] Task 3.2: Add or update console focus/key routing tests. + - Outcome: list/detail/input key behavior has regression coverage. + - Dependencies: Tasks 1.1-1.3. + - Validation: targeted console tests pass. +- [x] Task 3.3: Run feature lint and targeted console test suites. + - Outcome: docs and changed console code pass validation. + - Dependencies: implementation tasks complete. + - Validation: `npx ai-devkit@latest lint --feature agent-console-detail-chat-view` and `npm test -- --run packages/cli/src/__tests__/tui/console`. +- [x] Task 3.4: Perform manual terminal smoke test with a running agent when available. + - Outcome: real Ink focus styling and scroll behavior are visually confirmed. + - Dependencies: implementation tasks complete and at least one running agent available. + - Validation: manual notes in testing doc; if no running agent is available, record as deferred with reason. + +## Dependencies + +- Task 1.1 precedes all detail focus and scroll behavior. +- Task 2.1 should be implemented before changing Ink rendering so tests can cover the behavior directly. +- Task 2.4 depends on Task 2.1 because clamping should use the same helper that computes the viewport range. +- Task 3.2 depends on the chosen key-routing shape in `ConsoleApp`. +- No external services or API changes are required. + +## Test Scenario Coverage + +| Testing scenario | Covered by tasks | +| --- | --- | +| Newest content appears at offset 0 | 2.1, 3.1 | +| Older content appears at positive offset | 2.1, 3.1 | +| Offset clamping | 2.1, 2.4, 3.1 | +| Focused panel styling | 1.1, 2.2, 3.2 | +| Scroll keys do not move list selection in detail focus | 1.1, 1.2, 3.2 | +| Existing list/input behavior is unchanged | 1.2, 3.2 | +| No hidden focus trap in narrow mode | 1.1, 3.2, 3.4 | +| Scroll keypresses do not fetch conversation data | 2.2, 2.4, 3.3 | + +## Timeline & Estimates + +- Focus model: small. +- Scrollable preview rendering: medium, mainly due to line accounting in terminal UI. +- Tests and manual smoke: small to medium. + +## Risks & Mitigation + +| Risk | Impact | Mitigation | +| --- | --- | --- | +| Focus trap in hidden narrow preview | High | Keep `v` inactive in narrow mode for this iteration. | +| Scroll keys break list navigation | High | Centralize key routing and add regression tests for both focus states. | +| Line accounting differs from Ink wrapping | Medium | Base helper on explicit rendered lines and keep content clipping conservative. | +| Conversation tail of 20 feels too short | Medium | Keep existing behavior initially; revisit tail size during implementation review if scrolling is not useful enough. | + +## Resources Needed + +- Existing console TUI components under `packages/cli/src/tui/console`. +- Existing conversation parsing through `@ai-devkit/agent-manager`. +- Existing console tests and manual terminal run of `ai-devkit agent console`. + +## Planning Status + +All tasks complete. 16 console test files (106 tests) pass. CLI lint exits 0. Feature lint passes. Manual PTY smoke verified two-pane layout and clean exit; selected-agent detail focus deferred due to PTY agent-scan limitation — recommended for real-terminal verification before broad rollout. diff --git a/docs/ai/requirements/2026-07-02-feature-agent-console-detail-chat-view.md b/docs/ai/requirements/2026-07-02-feature-agent-console-detail-chat-view.md new file mode 100644 index 00000000..05e638d9 --- /dev/null +++ b/docs/ai/requirements/2026-07-02-feature-agent-console-detail-chat-view.md @@ -0,0 +1,98 @@ +--- +phase: requirements +title: Agent Console Detail Chat View +description: Make the interactive agent console detail pane focusable and scrollable so users can inspect chat content +--- + +# Requirements & Problem Understanding + +## Problem Statement + +Users can open the interactive agent console and move through agents in the list, but the right-side preview/detail pane is passive. It shows recent conversation content when space allows, yet users cannot intentionally focus that pane, see that it is active, or scroll through chat content. + +This affects developers supervising multiple AI agents from `ai-devkit agent console`. When an agent has a longer conversation, the current workaround is to leave the console and use `ai-devkit agent detail --id ` or inspect the underlying session file. That breaks the console workflow and makes quick review difficult. + +## Goals & Objectives + +Primary goals: + +- Let users press `v` from the agent list to focus the selected agent's detail/chat pane. +- Visually highlight the detail pane when it is focused, matching the existing panel focus style. +- Let users scroll the focused detail/chat pane up and down to inspect conversation content. +- Preserve existing list navigation, message input, start, rename, channel, open, kill, help, and quit shortcuts. +- Keep the preview polling and conversation parsing behavior adapter-agnostic through the existing `useAgentConversation` and `AgentAdapter.getConversation()` path. + +Secondary goals: + +- Provide small scroll affordances when additional chat content exists above or below the viewport. +- Keep keyboard behavior predictable for both arrow-key users and `j/k` users. + +Non-goals: + +- Adding a separate full-screen transcript viewer. +- Editing, exporting, searching, filtering, or copying chat content. +- Changing `ai-devkit agent detail` CLI output. +- Supporting historical/non-running agents beyond what the console already lists. +- Changing session parsing semantics or adding verbose tool-result rendering to the console preview. + +## User Stories & Use Cases + +- As a developer, I want to select an agent in the console and press `v` so that I can inspect that agent's chat without leaving the console. +- As a developer, I want the detail pane to visibly show focus so that I know scroll keys affect the chat, not the agent list. +- As a developer, I want to scroll up and down through the selected agent's conversation so that I can review earlier context and return to the newest messages. +- As a developer, I want to leave detail focus quickly so that I can continue selecting other agents or send a message. + +Key workflow: + +1. User runs `ai-devkit agent console`. +2. User navigates the agent list with up/down or `j/k`. +3. User presses `v`. +4. The detail/chat pane becomes focused and highlighted. +5. User scrolls chat content with up/down or `j/k`. +6. User presses `Esc` or left arrow to return focus to the agent list. + +Edge cases: + +- No agent is selected: `v` is ignored and the console remains stable. +- Selected agent has no session file or no messages: the focused pane still shows the existing empty/error state and does not crash. +- Conversation is shorter than the viewport: scroll commands are no-ops. +- Conversation updates while the detail pane is focused: the pane should keep a stable scroll position unless the user is already at the latest content, in which case it may stay pinned to the bottom. +- Narrow terminal mode currently hides the preview; `v` should not introduce an unusable hidden focus state. + +## Success Criteria + +- Pressing `v` from list focus changes console focus to the detail/chat pane when a selected agent exists. +- The detail/chat panel uses the focused panel styling while detail focus is active. +- While detail focus is active, up/down arrows and `j/k` scroll chat content instead of changing the selected agent. +- Pressing `Esc` or left arrow from detail focus returns to list focus. +- Existing list navigation still works when list focus is active. +- Existing input focus behavior still works for `i`/`m`, message submit, and input cancel. +- The implementation includes focused unit coverage for scroll calculations and key-handling helper behavior, plus regression coverage for panel focus state where practical. +- Feature lint passes for `agent-console-detail-chat-view`. + +## Constraints & Assumptions + +Technical constraints: + +- Console UI is implemented with Ink components under `packages/cli/src/tui/console`. +- `ConsoleFocus` currently supports `list` and `input`; this feature will add a detail/preview focus state. +- `PreviewSection` already fetches selected-agent messages through `useAgentConversation`, with a default tail of 20 messages. +- The feature should avoid synchronous conversation parsing on every scroll keypress. +- The console must remain usable in terminals near the existing minimum layout dimensions. + +Assumptions: + +- The requested "detail pane" maps to the existing right-side `PreviewSection`/`PreviewPane`. +- `v` is available as a new shortcut and does not conflict with existing console shortcuts. +- In wide mode, detail focus applies to the right-side preview panel; in narrow mode, `v` can be a no-op until a separate narrow replacement view is designed. +- Showing and scrolling the current non-verbose conversation preview is sufficient for this iteration. + +## Questions & Open Items + +No blocking open questions for the first implementation. + +Deferred questions: + +- Should a future version support a full-screen transcript view in narrow terminals? +- Should the console expose a toggle for verbose tool-call/tool-result messages? +- Should page-wise scrolling (`PageUp`/`PageDown`, `g`/`G`) be added after basic up/down scrolling lands? diff --git a/docs/ai/testing/2026-07-02-feature-agent-console-detail-chat-view.md b/docs/ai/testing/2026-07-02-feature-agent-console-detail-chat-view.md new file mode 100644 index 00000000..10f66cca --- /dev/null +++ b/docs/ai/testing/2026-07-02-feature-agent-console-detail-chat-view.md @@ -0,0 +1,95 @@ +--- +phase: testing +title: Agent Console Detail Chat View - Testing Strategy +description: Test coverage for focused and scrollable chat content in the interactive agent console +--- + +# Testing Strategy + +## Test Coverage Goals + +- Cover 100% of new pure helper logic for detail viewport calculation and scroll clamping. +- Cover key focus-routing behavior where it can be isolated without brittle full-screen Ink snapshots. +- Preserve existing preview, list, input, help, start, rename, channel, and kill behavior. +- Validate feature docs and affected test suites before claiming implementation readiness. + +## Unit Tests + +### PreviewPane viewport helpers + +- [x] Returns the newest content when `scrollOffset` is `0`. +- [x] Returns older content when `scrollOffset` is positive. +- [x] Clamps negative offsets to `0`. +- [x] Clamps offsets above the maximum available scroll range. +- [x] Reports `hasAbove` and `hasBelow` correctly for long conversations. +- [x] Handles empty/error/loading body states without scroll affordances. +- [x] Preserves role labels and multi-line message indentation in rendered line accounting. + +### ConsoleApp focus/key helpers + +- [x] `v` from list focus enters detail focus only when an agent is selected and preview is visible. +- [x] Up/down and `j/k` keep changing selected agent while list focus is active. +- [x] Up/down and `j/k` change detail scroll offset while detail focus is active. +- [x] `Esc` and left arrow return from detail focus to list focus. +- [x] `i`/`m` still enter input focus when an agent is selected. +- [x] Detail scroll offset resets when selected agent changes. + +## Integration Tests + +- [x] Render `PreviewSection` with focused state and verify the panel receives focused styling. +- [x] Render a long conversation and verify visible output changes when scroll offset changes. +- [x] Verify no adapter/conversation fetch is triggered by scroll-only state changes beyond the existing polling hook behavior. + +## End-to-End Tests + +- [ ] Manual flow: open `ai-devkit agent console`, select an agent, press `v`, confirm detail pane highlight. +- [ ] Manual flow: scroll up/down through chat and confirm selected agent does not change while detail is focused. +- [ ] Manual flow: press `Esc` or left arrow and confirm list navigation resumes. +- [ ] Regression flow: press `i`/`m`, type and submit a message, confirm input behavior is unchanged. + +## Test Data + +- Inline `ConversationMessage[]` fixtures with at least three roles and multi-line assistant content. +- Mock `AgentInfo` entries matching existing console tests. +- Existing `AgentManager`/adapter mocks for conversation hook tests. + +## Test Reporting & Coverage + +- Run targeted console tests during implementation: + - `npm test -- --run packages/cli/src/__tests__/tui/console` +- Run broader package validation before final review if touched files require it: + - `npm test -- --run packages/cli/src/__tests__/commands/agent.test.ts packages/cli/src/__tests__/tui/console` +- Record exact command output and feature lint evidence in the task trace. + +Latest results: + +- `npx vitest run src/__tests__/tui/console/PreviewPane.test.ts src/__tests__/tui/console/focusRouting.test.ts src/__tests__/tui/console/HelpPane.test.ts`: 3 files passed, 18 tests passed. +- `npx vitest run src/__tests__/tui/console`: 16 files passed, 106 tests passed. +- `npm run build`: 6 projects built successfully. +- `npm run lint --workspace packages/cli`: exited 0 with 5 warnings in unrelated existing files. +- Simplification verification: + - `npx vitest run src/__tests__/tui/console/PreviewPane.test.ts src/__tests__/tui/console/focusRouting.test.ts src/__tests__/tui/console/HelpPane.test.ts`: 3 files passed, 18 tests passed. + - `npx vitest run src/__tests__/tui/console`: 16 files passed, 106 tests passed. + - `npm run lint --workspace packages/cli`: exited 0 with 5 warnings in unrelated existing files. + - `npx ai-devkit@latest lint --feature agent-console-detail-chat-view`: passed. +- Manual PTY smoke: + - `node packages/cli/dist/cli.js agent list --json` returned 5 agents before and after the PTY run. + - Narrow PTY run rendered the list-only layout, displayed the expected `resize >=120 cols` warning, accepted `q`, and exited 0. + - Wide PTY run rendered the two-pane layout, accepted key input, accepted `q`, and exited 0. + - Selected-agent detail focus could not be manually verified in the PTY because the console's live scan returned 0 agents in that PTY even though `agent list --json` returned 5 agents in normal command mode. + +## Manual Testing + +- Confirm focused and unfocused panel states are visually distinct in a real terminal. +- Confirm scroll affordances are visible but not noisy. +- Confirm narrow terminal behavior does not trap focus in a hidden preview. + +## Performance Testing + +- Use a fixture with many loaded messages to validate scroll helper performance remains simple and synchronous. +- Confirm scroll keypresses do not parse JSONL session files. + +## Bug Tracking + +- Track any discovered defects in the feature planning doc during implementation. +- Treat focus traps, broken list navigation, and broken message input as blocking regressions. diff --git a/packages/cli/src/__tests__/tui/console/HelpPane.test.ts b/packages/cli/src/__tests__/tui/console/HelpPane.test.ts index 3a346bab..59c89ce4 100644 --- a/packages/cli/src/__tests__/tui/console/HelpPane.test.ts +++ b/packages/cli/src/__tests__/tui/console/HelpPane.test.ts @@ -11,6 +11,7 @@ describe('HelpPane helpers', () => { { key: 'c', action: 'Start Telegram channel for selected agent' }, { key: 'C', action: 'Stop Telegram channel' }, { key: 'o', action: 'Open selected agent terminal' }, + { key: 'v', action: 'View selected agent chat' }, { key: 'i / m', action: 'Message selected agent' }, { key: 'K', action: 'Kill selected agent' }, { key: 'h', action: 'Show or hide this help panel' }, @@ -30,4 +31,8 @@ describe('HelpPane helpers', () => { expect(getConsoleHotkeyHints()).toContain('c channel'); expect(getConsoleHotkeyHints()).toContain('C stop'); }); + + it('includes the detail view shortcut in footer hints', () => { + expect(getConsoleHotkeyHints()).toContain('v view'); + }); }); diff --git a/packages/cli/src/__tests__/tui/console/PreviewPane.test.ts b/packages/cli/src/__tests__/tui/console/PreviewPane.test.ts index 6fef6e39..45c08e12 100644 --- a/packages/cli/src/__tests__/tui/console/PreviewPane.test.ts +++ b/packages/cli/src/__tests__/tui/console/PreviewPane.test.ts @@ -1,5 +1,17 @@ import { describe, expect, it } from 'vitest'; -import { getPreviewPanelTone, getPreviewChannelStatusText } from '../../../tui/console/PreviewPane.js'; +import { + buildPreviewViewport, + getPreviewPanelTone, + getPreviewChannelStatusText, +} from '../../../tui/console/PreviewPane.js'; +import type { ConversationMessage } from '@ai-devkit/agent-manager'; + +const messages: ConversationMessage[] = [ + { role: 'user', content: 'first question', timestamp: '2026-07-02T10:00:00Z' }, + { role: 'assistant', content: 'first answer\nwith detail', timestamp: '2026-07-02T10:00:01Z' }, + { role: 'user', content: 'second question', timestamp: '2026-07-02T10:00:02Z' }, + { role: 'assistant', content: 'second answer', timestamp: '2026-07-02T10:00:03Z' }, +]; describe('PreviewPane helpers', () => { it('uses success tone when the selected agent has channel status', () => { @@ -13,4 +25,35 @@ describe('PreviewPane helpers', () => { it('formats connected channel status text', () => { expect(getPreviewChannelStatusText({ channelName: 'telegram', channelType: 'telegram', bridgePid: 42 })).toBe('Connected: telegram'); }); + + it('builds a viewport pinned to the newest conversation content at offset zero', () => { + const viewport = buildPreviewViewport(messages, 3, 0); + + expect(viewport.clampedOffset).toBe(0); + expect(viewport.hasAbove).toBe(true); + expect(viewport.hasBelow).toBe(false); + expect(viewport.rows).toEqual([ + { text: ' with detail', role: null }, + { text: 'user: second question', role: 'user' }, + { text: 'assistant: second answer', role: 'assistant' }, + ]); + }); + + it('builds a viewport over older conversation content at a positive offset', () => { + const viewport = buildPreviewViewport(messages, 3, 2); + + expect(viewport.clampedOffset).toBe(2); + expect(viewport.hasAbove).toBe(false); + expect(viewport.hasBelow).toBe(true); + expect(viewport.rows).toEqual([ + { text: 'user: first question', role: 'user' }, + { text: 'assistant: first answer', role: 'assistant' }, + { text: ' with detail', role: null }, + ]); + }); + + it('clamps requested offsets to the valid scroll range', () => { + expect(buildPreviewViewport(messages, 3, -2).clampedOffset).toBe(0); + expect(buildPreviewViewport(messages, 3, 99).clampedOffset).toBe(2); + }); }); diff --git a/packages/cli/src/__tests__/tui/console/focusRouting.test.ts b/packages/cli/src/__tests__/tui/console/focusRouting.test.ts new file mode 100644 index 00000000..59a9559a --- /dev/null +++ b/packages/cli/src/__tests__/tui/console/focusRouting.test.ts @@ -0,0 +1,90 @@ +import { describe, expect, it } from 'vitest'; +import { resolveConsoleKeyAction } from '../../../tui/console/consoleKeyRouting.js'; +import type { ConsoleFocus } from '../../../tui/console/types.js'; + +interface ResolveOverrides { + focus?: ConsoleFocus; + input?: string; + key?: { + upArrow?: boolean; + downArrow?: boolean; + leftArrow?: boolean; + escape?: boolean; + }; + hasSelectedAgent?: boolean; + previewVisible?: boolean; +} + +function resolve(overrides: ResolveOverrides) { + return resolveConsoleKeyAction({ + focus: 'list', + input: '', + key: {}, + hasSelectedAgent: true, + previewVisible: true, + ...overrides, + }); +} + +describe('console focus routing', () => { + it('enters detail focus from the list with v when preview is visible and an agent is selected', () => { + expect(resolve({ + input: 'v', + })).toEqual({ type: 'focus-detail' }); + }); + + it('does not enter hidden detail focus in narrow mode', () => { + expect(resolve({ + input: 'v', + previewVisible: false, + })).toEqual({ type: 'noop' }); + }); + + it('does not enter detail focus when no agent is selected', () => { + expect(resolve({ + input: 'v', + hasSelectedAgent: false, + })).toEqual({ type: 'noop' }); + }); + + it('keeps list navigation routed to agent selection while the list is focused', () => { + expect(resolve({ + input: 'j', + })).toEqual({ type: 'select-agent', delta: 1 }); + expect(resolve({ + input: 'k', + })).toEqual({ type: 'select-agent', delta: -1 }); + }); + + it('keeps message input routing available from list and detail focus', () => { + expect(resolve({ + input: 'i', + })).toEqual({ type: 'focus-input' }); + expect(resolve({ + focus: 'detail', + input: 'm', + })).toEqual({ type: 'focus-input' }); + }); + + it('routes arrows and j/k to preview scrolling while detail is focused', () => { + expect(resolve({ + focus: 'detail', + input: 'j', + })).toEqual({ type: 'scroll-detail', delta: -1 }); + expect(resolve({ + focus: 'detail', + input: 'k', + })).toEqual({ type: 'scroll-detail', delta: 1 }); + }); + + it('returns to list focus from detail with escape or left arrow', () => { + expect(resolve({ + focus: 'detail', + key: { escape: true }, + })).toEqual({ type: 'focus-list' }); + expect(resolve({ + focus: 'detail', + key: { leftArrow: true }, + })).toEqual({ type: 'focus-list' }); + }); +}); diff --git a/packages/cli/src/tui/console/ConsoleApp.tsx b/packages/cli/src/tui/console/ConsoleApp.tsx index 391021ca..d7411dfe 100644 --- a/packages/cli/src/tui/console/ConsoleApp.tsx +++ b/packages/cli/src/tui/console/ConsoleApp.tsx @@ -19,6 +19,7 @@ import { ChannelSelectPane } from './ChannelSelectPane.js'; import { HelpPane } from './HelpPane.js'; import { KillConfirmDialog } from './KillConfirmDialog.js'; import type { ConsoleFocus, RightPaneMode, TransientMessage } from './types.js'; +import { resolveConsoleKeyAction } from './consoleKeyRouting.js'; import { Panel } from '../design-system/index.js'; interface ConsoleAppProps { @@ -72,6 +73,7 @@ const ConsoleAppShell: React.FC<{ const [inputValue, setInputValue] = useState(''); const [transient, setTransient] = useState(null); const [rightPaneMode, setRightPaneMode] = useState({ type: 'preview' }); + const [detailScrollOffset, setDetailScrollOffset] = useState(0); const startPaneActive = rightPaneMode.type === 'start-agent'; const renamePaneActive = rightPaneMode.type === 'rename-agent'; const channelSelectPaneActive = rightPaneMode.type === 'channel-select'; @@ -116,6 +118,10 @@ const ConsoleAppShell: React.FC<{ } }, [agents, selectedName]); + useEffect(() => { + setDetailScrollOffset(0); + }, [selectedName]); + const getSelectedAgent = useCallback(() => { const name = selectedNameRef.current; return name ? agentsRef.current.find(agent => agent.name === name) ?? null : null; @@ -240,25 +246,36 @@ const ConsoleAppShell: React.FC<{ return; } - if (input === 'i' || input === 'm') { - if (selectedNameRef.current) setFocus('input'); - return; - } - - if (key.downArrow || input === 'j') { - const list = agentsRef.current; - if (!list.length) return; - const idx = Math.max(0, list.findIndex(a => a.name === selectedNameRef.current)); - setSelectedName(list[(idx + 1) % list.length].name); - return; - } - - if (key.upArrow || input === 'k') { - const list = agentsRef.current; - if (!list.length) return; - const idx = Math.max(0, list.findIndex(a => a.name === selectedNameRef.current)); - setSelectedName(list[(idx - 1 + list.length) % list.length].name); - return; + const previewVisible = !narrow && rightPaneMode.type === 'preview'; + const keyAction = resolveConsoleKeyAction({ + focus, + input, + key, + hasSelectedAgent: Boolean(selectedNameRef.current), + previewVisible, + }); + switch (keyAction.type) { + case 'focus-detail': + setFocus('detail'); + return; + case 'focus-list': + setFocus('list'); + return; + case 'focus-input': + setFocus('input'); + return; + case 'scroll-detail': + setDetailScrollOffset(prev => Math.max(0, prev + keyAction.delta)); + return; + case 'select-agent': { + const list = agentsRef.current; + if (!list.length) return; + const idx = Math.max(0, list.findIndex(a => a.name === selectedNameRef.current)); + setSelectedName(list[(idx + keyAction.delta + list.length) % list.length].name); + return; + } + case 'noop': + return; } }); @@ -336,6 +353,9 @@ const ConsoleAppShell: React.FC<{ Math.max(max, item.key.length), 0); export function getConsoleHotkeyHints(): string[] { - return ['j/k nav', 's start', 'r rename', 'c channel', 'C stop', 'o open', 'i message', 'K kill', 'h help', 'q quit']; + return ['j/k nav', 's start', 'r rename', 'c channel', 'C stop', 'o open', 'v view', 'i message', 'K kill', 'h help', 'q quit']; } interface HelpPaneProps { diff --git a/packages/cli/src/tui/console/PreviewPane.tsx b/packages/cli/src/tui/console/PreviewPane.tsx index 2979d42f..7c3908f3 100644 --- a/packages/cli/src/tui/console/PreviewPane.tsx +++ b/packages/cli/src/tui/console/PreviewPane.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { Box, Text } from 'ink'; import type { AgentInfo, ConversationMessage } from '@ai-devkit/agent-manager'; import type { ConversationFetchError } from './hooks/useAgentConversation.js'; @@ -15,6 +15,8 @@ interface PreviewPaneProps { isLoading: boolean; maxLines?: number; channelStatus?: AgentChannelStatus; + scrollOffset?: number; + onScrollOffsetClamp?: (offset: number) => void; } const ROLE_COLOR: Record = { @@ -24,21 +26,52 @@ const ROLE_COLOR: Record { + const contentLines = msg.content.split('\n'); + const first = contentLines[0] ?? ''; + return [ + { text: `${msg.role}: ${first}`, role: msg.role }, + ...contentLines.slice(1).map(line => ({ text: ` ${line}`, role: null })), + ]; + }); + const maxOffset = Math.max(0, rows.length - budget); + const clampedOffset = Math.min(Math.max(0, Math.floor(requestedOffset)), maxOffset); + const end = Math.max(0, rows.length - clampedOffset); + const start = Math.max(0, end - budget); + return { + rows: rows.slice(start, end), + clampedOffset, + maxOffset, + hasAbove: start > 0, + hasBelow: end < rows.length, + }; +} + export function getPreviewPanelTone(channelStatus: AgentChannelStatus | undefined): PanelTone { return channelStatus ? 'success' : 'default'; } @@ -74,7 +107,20 @@ const PreviewPaneInner: React.FC = ({ isLoading, maxLines = 22, channelStatus, + scrollOffset = 0, + onScrollOffsetClamp, }) => { + const viewport = messages.length > 0 + ? buildPreviewViewport(messages, Math.max(4, maxLines), scrollOffset) + : null; + const clampedOffset = viewport?.clampedOffset; + + useEffect(() => { + if (clampedOffset !== undefined && clampedOffset !== scrollOffset) { + onScrollOffsetClamp?.(clampedOffset); + } + }, [onScrollOffsetClamp, scrollOffset, clampedOffset]); + if (!agent) { return ( @@ -84,8 +130,6 @@ const PreviewPaneInner: React.FC = ({ ); } - const messageBudget = Math.max(4, maxLines); - let body: React.ReactNode; if (error) { const detail = error.kind === 'no-session-file' @@ -99,35 +143,21 @@ const PreviewPaneInner: React.FC = ({ } else if (messages.length === 0) { body = No messages yet.; } else { - const rendered: React.ReactNode[] = []; - let usedLines = 0; - for (let i = messages.length - 1; i >= 0; i--) { - const msg = messages[i]; - const contentLines = msg.content.split('\n'); - const headerLine = 1; - if (usedLines + headerLine > messageBudget) break; - const remaining = messageBudget - usedLines - headerLine; - const trimmed = contentLines.length > remaining - ? [...contentLines.slice(0, Math.max(0, remaining - 1)), `… (${contentLines.length - Math.max(0, remaining - 1)} more lines)`] - : contentLines; - const time = formatTime(msg.timestamp); - rendered.unshift( - - - {time ? [{time}] : null} - {msg.role}: + body = ( + <> + {viewport && (viewport.hasAbove || viewport.hasBelow) ? ( + + {viewport.hasAbove ? '↑ older' : ' '} + {viewport.hasBelow ? ' ↓ newer' : ''} - {trimmed.map((line: string, idx: number) => ( - - {line} - - ))} - , - ); - usedLines += headerLine + trimmed.length + 1; - if (usedLines >= messageBudget) break; - } - body = <>{rendered}; + ) : null} + {viewport?.rows.map((row, idx) => ( + + {row.text} + + ))} + + ); } return ( diff --git a/packages/cli/src/tui/console/PreviewSection.tsx b/packages/cli/src/tui/console/PreviewSection.tsx index 951354d0..c4dce264 100644 --- a/packages/cli/src/tui/console/PreviewSection.tsx +++ b/packages/cli/src/tui/console/PreviewSection.tsx @@ -8,9 +8,18 @@ import { getPreviewPanelTone } from './PreviewPane.js'; interface PreviewSectionProps { selectedName: string | null; height: number; + focused?: boolean; + scrollOffset?: number; + onScrollOffsetClamp?: (offset: number) => void; } -const PreviewSectionInner: React.FC = ({ selectedName, height }) => { +const PreviewSectionInner: React.FC = ({ + selectedName, + height, + focused = false, + scrollOffset = 0, + onScrollOffsetClamp, +}) => { const { agents, manager, inputFocused, channelStatuses } = useConsoleContext(); const selectedAgent = useMemo( () => agents.find(a => a.name === selectedName) ?? null, @@ -27,6 +36,7 @@ const PreviewSectionInner: React.FC = ({ selectedName, heig return ( = ({ selectedName, heig isLoading={isLoading} maxLines={Math.max(4, height - 2)} channelStatus={channelStatus} + scrollOffset={scrollOffset} + onScrollOffsetClamp={onScrollOffsetClamp} /> ); diff --git a/packages/cli/src/tui/console/consoleKeyRouting.ts b/packages/cli/src/tui/console/consoleKeyRouting.ts new file mode 100644 index 00000000..0b6a44f2 --- /dev/null +++ b/packages/cli/src/tui/console/consoleKeyRouting.ts @@ -0,0 +1,51 @@ +import type { ConsoleFocus } from './types.js'; + +interface ConsoleKeyLike { + upArrow?: boolean; + downArrow?: boolean; + leftArrow?: boolean; + escape?: boolean; +} + +interface ResolveConsoleKeyActionParams { + focus: ConsoleFocus; + input: string; + key: ConsoleKeyLike; + hasSelectedAgent: boolean; + previewVisible: boolean; +} + +export type ConsoleKeyAction = + | { type: 'noop' } + | { type: 'focus-list' } + | { type: 'focus-detail' } + | { type: 'focus-input' } + | { type: 'scroll-detail'; delta: number } + | { type: 'select-agent'; delta: number }; + +export function resolveConsoleKeyAction({ + focus, + input, + key, + hasSelectedAgent, + previewVisible, +}: ResolveConsoleKeyActionParams): ConsoleKeyAction { + if (focus === 'detail') { + if (key.escape || key.leftArrow) return { type: 'focus-list' }; + if (input === 'i' || input === 'm') return hasSelectedAgent ? { type: 'focus-input' } : { type: 'noop' }; + if (key.downArrow || input === 'j') return { type: 'scroll-detail', delta: -1 }; + if (key.upArrow || input === 'k') return { type: 'scroll-detail', delta: 1 }; + return { type: 'noop' }; + } + + if (focus === 'list') { + if (input === 'v') { + return hasSelectedAgent && previewVisible ? { type: 'focus-detail' } : { type: 'noop' }; + } + if (input === 'i' || input === 'm') return hasSelectedAgent ? { type: 'focus-input' } : { type: 'noop' }; + if (key.downArrow || input === 'j') return { type: 'select-agent', delta: 1 }; + if (key.upArrow || input === 'k') return { type: 'select-agent', delta: -1 }; + } + + return { type: 'noop' }; +} diff --git a/packages/cli/src/tui/console/types.ts b/packages/cli/src/tui/console/types.ts index 71cebd83..874e9292 100644 --- a/packages/cli/src/tui/console/types.ts +++ b/packages/cli/src/tui/console/types.ts @@ -1,4 +1,4 @@ -export type ConsoleFocus = 'list' | 'input'; +export type ConsoleFocus = 'list' | 'detail' | 'input'; export interface AgentChannelStatus { channelName: string;