From fe9c3fceeeb650e44afe9f7ceae2eecb0555c8d0 Mon Sep 17 00:00:00 2001 From: Hoang Nguyen Date: Fri, 3 Jul 2026 11:29:03 +0200 Subject: [PATCH] feat(agent): show memory list in console --- ...ature-view-memory-list-in-agent-console.md | 161 ++++++++++++++++++ ...ature-view-memory-list-in-agent-console.md | 108 ++++++++++++ ...ature-view-memory-list-in-agent-console.md | 131 ++++++++++++++ ...ature-view-memory-list-in-agent-console.md | 84 +++++++++ ...ature-view-memory-list-in-agent-console.md | 115 +++++++++++++ .../tui/console/ConsoleApp.memory.test.ts | 12 ++ .../__tests__/tui/console/HelpPane.test.ts | 5 + .../tui/console/MemoryListPane.test.ts | 104 +++++++++++ .../tui/console/hooks/useMemoryList.test.ts | 62 +++++++ .../tui/console/render/formatStatus.test.ts | 17 ++ .../cli/src/__tests__/util/object.test.ts | 14 ++ packages/cli/src/__tests__/util/text.test.ts | 13 +- packages/cli/src/tui/console/ConsoleApp.tsx | 19 ++- packages/cli/src/tui/console/HelpPane.tsx | 3 +- .../cli/src/tui/console/MemoryListPane.tsx | 124 ++++++++++++++ .../src/tui/console/hooks/useMemoryList.ts | 90 ++++++++++ .../src/tui/console/render/formatStatus.tsx | 6 +- packages/cli/src/tui/console/rightPaneMode.ts | 5 + packages/cli/src/tui/console/types.ts | 17 ++ 19 files changed, 1085 insertions(+), 5 deletions(-) create mode 100644 docs/ai/design/2026-07-03-feature-view-memory-list-in-agent-console.md create mode 100644 docs/ai/implementation/2026-07-03-feature-view-memory-list-in-agent-console.md create mode 100644 docs/ai/planning/2026-07-03-feature-view-memory-list-in-agent-console.md create mode 100644 docs/ai/requirements/2026-07-03-feature-view-memory-list-in-agent-console.md create mode 100644 docs/ai/testing/2026-07-03-feature-view-memory-list-in-agent-console.md create mode 100644 packages/cli/src/__tests__/tui/console/ConsoleApp.memory.test.ts create mode 100644 packages/cli/src/__tests__/tui/console/MemoryListPane.test.ts create mode 100644 packages/cli/src/__tests__/tui/console/hooks/useMemoryList.test.ts create mode 100644 packages/cli/src/__tests__/tui/console/render/formatStatus.test.ts create mode 100644 packages/cli/src/__tests__/util/object.test.ts create mode 100644 packages/cli/src/tui/console/MemoryListPane.tsx create mode 100644 packages/cli/src/tui/console/hooks/useMemoryList.ts create mode 100644 packages/cli/src/tui/console/rightPaneMode.ts diff --git a/docs/ai/design/2026-07-03-feature-view-memory-list-in-agent-console.md b/docs/ai/design/2026-07-03-feature-view-memory-list-in-agent-console.md new file mode 100644 index 00000000..9646eee5 --- /dev/null +++ b/docs/ai/design/2026-07-03-feature-view-memory-list-in-agent-console.md @@ -0,0 +1,161 @@ +--- +phase: design +title: System Design & Architecture +description: Define the technical architecture, components, and data models +--- + +# System Design & Architecture + +## Architecture Overview +**What is the high-level system structure?** + +```mermaid +graph TD + User[User presses M] + Console[ConsoleAppShell] + Mode[RightPaneMode memory-list] + Pane[MemoryListPane] + Hook[useMemoryList] + Config[ConfigManager getMemoryDbPath] + Api[memoryListCommand] + MemoryPkg["@ai-devkit/memory listKnowledge"] + Db[(SQLite memory.db)] + Help[HelpPane and StatusFooter] + + User --> Console + Console --> Mode + Mode --> Pane + Pane --> Hook + Hook --> Config + Hook --> Api + Api --> MemoryPkg + MemoryPkg --> Db + Console --> Help +``` + +The console adds a read-only memory-list right-pane workspace. `ConsoleAppShell` handles the new shortcut only when global console shortcuts are active, sets `RightPaneMode` to a memory-list state, and renders `MemoryListPane` using the same replacement-pane model as help, start agent, rename, and channel selection. + +The memory pane loads bounded recent items through a small CLI-side hook that resolves the configured memory database path with existing config behavior and calls `@ai-devkit/memory` list APIs. The TUI does not parse SQLite files directly. Loading starts when the memory pane is opened and can run asynchronously from the Ink render path so existing agent polling and selection remain responsive. + +## Data Models +**What data do we need to manage?** + +```typescript +interface ConsoleMemoryItem { + id: string; + title: string; + scope: string; + tags: string[]; + updatedAt: string; +} + +type RightPaneMode = + | ExistingRightPaneMode + | { type: 'memory-list' }; + +interface MemoryListState { + items: ConsoleMemoryItem[]; + total: number; + isLoading: boolean; + error: string | null; + lastUpdated: Date | null; +} +``` + +- Core source data comes from `KnowledgeItem` returned by `memoryListCommand` / `listKnowledge`. +- The pane projects memory data to terminal-friendly rows: title, scope, tags, and updated time. +- `total` comes from `ListKnowledgeResult.total` and supports compact `+N more` overflow messaging when the pane cannot show every loaded item. +- `lastUpdated` is the pane load time, not a persisted memory property. +- No schema changes or new persistent data are required. + +## API Design +**How do components communicate?** + +- External APIs: + - No public network API is added. + - No command syntax changes outside `ai-devkit agent console` keyboard behavior. +- Internal interfaces: + - Add `memory-list` to `RightPaneMode`. + - Add `MemoryListPane` under `packages/cli/src/tui/console`. + - Add a `useMemoryList` hook or equivalent local loader. + - Reuse async `ConfigManager.getMemoryDbPath()` to resolve the configured database path. + - Reuse synchronous `memoryListCommand({ limit, sort: 'updated-desc', dbPath })` after path resolution completes. +- Request/response format: + - The pane requests a bounded list, proposed default `limit: 20`. + - The pane renders at most the rows that fit inside its height budget and keeps remaining loaded items in state for future navigation/pagination work. + - Empty result returns success with no rows and renders an empty state. + - Exceptions become pane-local error state and do not exit the console. +- Authentication/authorization: + - No new authentication. The feature reads the same local database that the CLI user can already access. + +## Component Breakdown +**What are the major building blocks?** + +| Component | Change | +|---|---| +| `packages/cli/src/tui/console/types.ts` | Add `memory-list` right-pane mode and memory item state types if needed | +| `packages/cli/src/tui/console/ConsoleApp.tsx` | Route `M` shortcut, close/toggle memory pane, and render `MemoryListPane` | +| `packages/cli/src/tui/console/MemoryListPane.tsx` | New read-only Ink pane for recent memory items, empty state, loading state, and error state | +| `packages/cli/src/tui/console/hooks/useMemoryList.ts` | Load bounded recent memory items through config-aware memory APIs | +| `packages/cli/src/tui/console/HelpPane.tsx` | Document the `M` memory shortcut | +| `packages/cli/src/tui/console/StatusFooter.tsx` | Include compact `M memory` hint | +| `packages/cli/src/__tests__/tui/console/**` | Cover rendering, shortcut behavior, and regression around existing panes | + +The memory package already exposes `memoryListCommand`, so this feature should not add new memory storage or query primitives unless implementation discovers a missing capability. + +### Load Behavior + +- Opening the memory pane starts a one-shot load. +- Reopening the pane starts a fresh load so recent changes from `ai-devkit memory store/update` can appear without a separate refresh control. +- There is no polling in this feature. Memory data changes far less frequently than agent status, and polling local memory content would add complexity without a clear user benefit. +- If the pane unmounts while a load is in flight, the hook should ignore late results rather than setting state on an inactive component. + +## Design Decisions +**Why did we choose this approach?** + +- Add a console-native pane instead of launching `memory-dashboard`. + - The agent console is a terminal supervision workflow. A native pane keeps the user in that workflow. +- Use uppercase `M`. + - Lowercase `m` already focuses message input. Uppercase keeps the existing shortcut intact. +- Start with recent list, not full search. + - Recent memory is the fastest low-friction recall surface. Search can be added later without changing the underlying pane model. +- Keep read-only behavior. + - Memory content may be sensitive project knowledge. Mutation flows need stronger confirmation and are outside this feature. +- Use existing memory APIs rather than direct database access. + - This preserves memory package ownership of schema and mapping behavior. +- Use bounded results. + - Prevents large memory databases from overwhelming the TUI. +- Refresh on open, not continuous polling. + - Keeps the view fresh enough for console use while avoiding unnecessary database reads. +- Render rows according to pane height. + - Preserves the console's fixed layout and avoids memory rows pushing into the footer or input pane. + +Alternatives considered: +- Add a `memory` tab as a persistent third column. + - Rejected because the console already has narrow-layout constraints and right-pane replacement patterns. +- Trigger `ai-devkit memory search --table` as a subprocess. + - Rejected because the TUI needs structured data, error handling, and configured path consistency without parsing human output. +- Embed dashboard graph behavior in the console. + - Rejected because graph interaction is better suited to the browser dashboard and would make the TUI harder to operate. +- Add inline search immediately. + - Deferred because requirements prioritize quick recent recall and a smaller read-only first slice. +- Poll memory continuously while the pane is open. + - Rejected because memory changes are user-driven and infrequent; refresh-on-open is simpler and predictable. + +## Non-Functional Requirements +**How should the system perform?** + +- Performance: + - Initial memory load should be bounded and asynchronous so console rendering remains responsive. + - Default list size should stay small enough for terminal scanability. +- Scalability: + - Large memory databases are handled by limiting rows; pagination/search can be added later if needed. +- Security: + - Do not print memory contents to logs. + - Do not expose memory data outside the local TUI. + - Treat configured database path errors as local errors, not crash conditions. +- Reliability: + - Missing database and zero-item database render an empty state. + - Invalid/unreadable configured path renders an error in the memory pane and leaves the rest of the console usable. + - Existing console workflows and shortcuts continue to work unchanged. + - Late async results from an unmounted memory pane must be ignored. diff --git a/docs/ai/implementation/2026-07-03-feature-view-memory-list-in-agent-console.md b/docs/ai/implementation/2026-07-03-feature-view-memory-list-in-agent-console.md new file mode 100644 index 00000000..753b8d14 --- /dev/null +++ b/docs/ai/implementation/2026-07-03-feature-view-memory-list-in-agent-console.md @@ -0,0 +1,108 @@ +--- +phase: implementation +title: Implementation Guide +description: Technical implementation notes, patterns, and code guidelines +--- + +# Implementation Guide + +## Development Setup +**How do we get started?** + +- Active worktree: `.worktrees/feature-view-memory-list-in-agent-console` +- Branch: `feature-view-memory-list-in-agent-console` +- Dependencies were installed with `npm ci` in the worktree. +- Local workspace packages needed for full CLI tests: + - `npm --workspace packages/agent-manager run build` + - `npm --workspace packages/channel-connector run build` + - `npm --workspace packages/memory run build` + +## Code Structure +**How is the code organized?** + +- `packages/cli/src/tui/console/types.ts` + - Added `ConsoleMemoryItem`, `MemoryListState`, and `memory-list` right-pane mode. +- `packages/cli/src/tui/console/hooks/useMemoryList.ts` + - New config-aware memory loader and hook. +- `packages/cli/src/tui/console/MemoryListPane.tsx` + - New read-only Ink pane for recent memory items. +- `packages/cli/src/tui/console/rightPaneMode.ts` + - Lightweight helper for `M` shortcut mode transitions. +- `packages/cli/src/tui/console/ConsoleApp.tsx` + - Wires `M` shortcut and replacement-pane rendering. +- `packages/cli/src/tui/console/HelpPane.tsx` + - Documents `M` in help and footer hints. +- Tests: + - `packages/cli/src/__tests__/tui/console/hooks/useMemoryList.test.ts` + - `packages/cli/src/__tests__/tui/console/MemoryListPane.test.ts` + - `packages/cli/src/__tests__/tui/console/ConsoleApp.memory.test.ts` + - updated `HelpPane.test.ts` + +## Implementation Notes +**Key technical details to remember:** + +### Core Features +- Memory loading: + - `loadMemoryList` resolves `ConfigManager.getMemoryDbPath()` before calling `memoryListCommand`. + - Requests are bounded with `limit: 20` and ordered by `updated-desc`. + - The hook ignores late async results after unmount. +- Memory pane: + - Renders loading, empty, error, and recent-item states. + - Shows title, scope, tags, and updated date only. + - Limits rendered rows by pane height and truncates long title/metadata strings. +- Console integration: + - Uppercase `M` toggles between preview and memory list. + - Lowercase `m` remains message input. + - The memory pane follows the existing wide right-pane and narrow replacement-pane model. + +### Patterns & Best Practices +- Keep memory data access isolated in `useMemoryList`. +- Keep shortcut mode behavior testable through a pure helper. +- Reuse existing `truncate`, `formatRelative`, `Panel`, `SectionTitle`, and `TUI_COLORS` utilities. +- Do not parse SQLite directly from console UI code. + +## Integration Points +**How do pieces connect?** + +- `ConsoleApp` renders `MemoryListPane` when `rightPaneMode.type === 'memory-list'`. +- `MemoryListPane` calls `useMemoryList`. +- `useMemoryList` resolves the configured memory database path with `ConfigManager` and reads via `@ai-devkit/memory` `memoryListCommand`. +- No new external API, database schema, or command syntax was added. + +## Error Handling +**How do we handle failures?** + +- Loader failures are converted to pane-local error text using `getErrorMessage`. +- Errors do not exit the console or stop agent polling. +- Empty databases render `No memory items yet.` +- Late async results after unmount are ignored. + +## Performance Considerations +**How do we keep it fast?** + +- Memory list loads only when the pane opens. +- No continuous polling was added. +- The read is bounded to 20 items. +- Rendering is bounded by the pane height so rows cannot push into the footer/input area. + +## Security Notes +**What security measures are in place?** + +- The feature is read-only. +- Memory content is not logged. +- The pane displays only title and metadata, not full memory content. +- The console uses the same local memory access available to the CLI user. + +## Verification Evidence + +- `npm --workspace packages/cli test -- src/__tests__/tui/console/hooks/useMemoryList.test.ts src/__tests__/tui/console/MemoryListPane.test.ts src/__tests__/tui/console/HelpPane.test.ts src/__tests__/tui/console/ConsoleApp.memory.test.ts` + - Exit 0; 4 files, 16 tests passed. +- `npm --workspace packages/cli run build` + - Exit 0; CLI package compiled 187 files and emitted declarations. +- `npm --workspace packages/cli test` + - First run failed because local workspace dependencies had unbuilt `dist` entries for `@ai-devkit/agent-manager` and `@ai-devkit/channel-connector`. + - After building `packages/agent-manager`, `packages/channel-connector`, and `packages/memory`, rerun exited 0; 75 files, 880 tests passed. + - Final testing-phase run exited 0; 77 files, 888 tests passed. +- `npm --workspace packages/cli run test:coverage` + - First run failed at branch coverage 59.61%, below the 60% global threshold. + - Final run exited 0 after adding focused branch tests; 77 files, 888 tests passed; branch coverage 60.09%. diff --git a/docs/ai/planning/2026-07-03-feature-view-memory-list-in-agent-console.md b/docs/ai/planning/2026-07-03-feature-view-memory-list-in-agent-console.md new file mode 100644 index 00000000..a0584e57 --- /dev/null +++ b/docs/ai/planning/2026-07-03-feature-view-memory-list-in-agent-console.md @@ -0,0 +1,131 @@ +--- +phase: planning +title: Project Planning & Task Breakdown +description: Break down work into actionable tasks and estimate timeline +--- + +# Project Planning & Task Breakdown + +## Milestones +**What are the major checkpoints?** + +- [x] Milestone 1: Memory loading foundation + - Outcome: console code has a typed memory-list mode and a config-aware loader that reads bounded recent memory items through `@ai-devkit/memory`. +- [x] Milestone 2: Console UI integration + - Outcome: `M` opens/closes a read-only memory list pane in wide and narrow layouts without disrupting existing console workflows. +- [x] Milestone 3: Test coverage and verification + - Outcome: targeted console tests cover the new pane, hook, shortcut, empty/error states, and help/footer hints; lifecycle lint and package tests pass. + +## Task Breakdown +**What specific work needs to be done?** + +### Phase 1: Memory Loading Foundation +- [x] Task 1.1: Add memory-list state types + - Outcome: `packages/cli/src/tui/console/types.ts` includes `RightPaneMode` support for `{ type: 'memory-list' }` and any shared memory item state types needed by the pane/hook. + - Dependencies: approved design data model. + - Validation evidence: TypeScript compile or targeted CLI test run. + - Testing scenarios: Console shortcut/right-pane mode tests. +- [x] Task 1.2: Implement `useMemoryList` + - Outcome: `packages/cli/src/tui/console/hooks/useMemoryList.ts` resolves `ConfigManager.getMemoryDbPath()`, calls `memoryListCommand({ limit: 20, sort: 'updated-desc', dbPath })`, maps `KnowledgeItem` to console row state, and ignores late results after unmount. + - Dependencies: Task 1.1. + - Validation evidence: unit tests for success, empty results, thrown errors, configured path resolution, and late-result behavior. + - Testing scenarios: `useMemoryList` unit tests; memory loader integration path. + +### Phase 2: Console UI Integration +- [x] Task 2.1: Add `MemoryListPane` + - Outcome: new Ink pane renders loading, empty, error, and recent item rows with title, scope, tags, and updated time while respecting width/height constraints. + - Dependencies: Tasks 1.1 and 1.2. + - Validation evidence: component tests for loading/empty/error/items/long text. + - Testing scenarios: `MemoryListPane` unit tests; text fit and bounded row behavior. +- [x] Task 2.2: Wire the pane into `ConsoleApp` + - Outcome: uppercase `M` toggles memory-list mode from global shortcut focus; `M` does not interfere with chat input, start, rename, channel select, help, or kill confirmation behavior. Wide layout renders in the right pane; narrow layout uses replacement-pane behavior. + - Dependencies: Task 2.1. + - Validation evidence: shortcut/mode tests or focused tests around exported helper behavior where full Ink input integration is not practical. + - Testing scenarios: `M` open/close, ignored while input or modal owns keyboard, wide/narrow rendering. +- [x] Task 2.3: Update console help and footer hints + - Outcome: `HelpPane` documents `M` and `StatusFooter` includes compact `M memory` hint without removing important existing shortcuts. + - Dependencies: Task 2.2. + - Validation evidence: `HelpPane` helper tests and snapshot/string assertions for hints. + - Testing scenarios: help/footer hint tests and shortcut regression coverage. + +### Phase 3: Verification and Documentation +- [x] Task 3.1: Add/adjust automated tests + - Outcome: tests cover all new hook, pane, shortcut, and hint behavior listed in the testing strategy. + - Dependencies: Tasks 1.1 through 2.3. + - Validation evidence: `npm --workspace packages/cli test -- --runInBand` if supported, otherwise `npm --workspace packages/cli test`; targeted Vitest paths if full package test is too slow. + - Testing scenarios: all unit and integration scenarios from the testing doc. +- [x] Task 3.2: Run broader validation + - Outcome: lifecycle lint, CLI tests, and build/type checks pass for the changed package. + - Dependencies: Task 3.1. + - Validation evidence: + - `npx ai-devkit@latest lint --feature view-memory-list-in-agent-console` + - `npm --workspace packages/cli test` + - `npm --workspace packages/cli run build` + - Testing scenarios: regression of adjacent console features and type safety. +- [x] Task 3.3: Update implementation/testing docs with evidence + - Outcome: implementation doc records changed files, key decisions, verification commands, and any deferred follow-ups. Testing doc checkboxes are updated after tests exist. + - Dependencies: Task 3.2. + - Validation evidence: feature lint after doc updates. + - Testing scenarios: documentation traceability. + +## Dependencies +**What needs to happen in what order?** + +- Task 1.1 must land before tasks that reference `memory-list` mode. +- Task 1.2 must land before UI renders live memory data. +- Task 2.1 can be developed with mocked memory state, but final integration depends on Task 1.2. +- Task 2.2 depends on `MemoryListPane` and existing `ConsoleApp` right-pane replacement behavior. +- Task 2.3 depends on the final shortcut choice remaining `M`. +- Task 3.1 and Task 3.2 depend on implementation tasks being complete. +- No external network service is required; the feature reads local SQLite memory through existing package APIs. + +## Timeline & Estimates +**When will things be done?** + +- Estimated effort: + - Memory loading foundation: small to medium. + - Console UI integration: medium, mostly around keyboard and layout regression risk. + - Test coverage and verification: medium. +- Target sequence: + - Complete Task 1.1 and Task 1.2 first. + - Complete Task 2.1 through Task 2.3 in one implementation pass. + - Complete tests and docs before implementation check/testing phases. +- Buffer: + - Reserve time for Ink test limitations; helper extraction may be needed if direct keyboard simulation is brittle. + +## Risks & Mitigation +**What could go wrong?** + +- Risk: Importing `memoryListCommand` into TUI code could make tests harder because it opens SQLite. + - Mitigation: keep loading isolated in `useMemoryList` and mock the memory API in hook tests. +- Risk: async load results could update state after the pane is closed. + - Mitigation: track active/cancelled state inside the hook and ignore late results. +- Risk: long memory metadata could break terminal layout. + - Mitigation: use existing truncation utilities and height-based row budgets. +- Risk: `M` shortcut could conflict with existing input flows. + - Mitigation: route only in global shortcut handling and add regression tests for input/modal states. +- Risk: full Ink keyboard integration tests may be brittle. + - Mitigation: test pure helpers, hook behavior, pane rendering, and mode routing as close to the existing test style as possible. + +## Resources Needed +**What do we need to succeed?** + +- Existing code: + - `packages/cli/src/tui/console/ConsoleApp.tsx` + - `packages/cli/src/tui/console/types.ts` + - `packages/cli/src/tui/console/HelpPane.tsx` + - `packages/cli/src/tui/console/StatusFooter.tsx` + - `packages/cli/src/lib/Config.ts` + - `packages/memory/src/api.ts` +- New likely files: + - `packages/cli/src/tui/console/MemoryListPane.tsx` + - `packages/cli/src/tui/console/hooks/useMemoryList.ts` + - corresponding tests under `packages/cli/src/__tests__/tui/console`. +- Commands: + - `npx ai-devkit@latest lint --feature view-memory-list-in-agent-console` + - `npm --workspace packages/cli test` + - `npm --workspace packages/cli run build` + +## Progress Summary + +All planned implementation tasks are complete. The implementation added the read-only memory list pane, config-aware loader, shortcut wiring, help/footer hints, and focused tests. Broader CLI validation passed after building local workspace package dependencies that the CLI test suite imports from `dist`. diff --git a/docs/ai/requirements/2026-07-03-feature-view-memory-list-in-agent-console.md b/docs/ai/requirements/2026-07-03-feature-view-memory-list-in-agent-console.md new file mode 100644 index 00000000..43f8eebe --- /dev/null +++ b/docs/ai/requirements/2026-07-03-feature-view-memory-list-in-agent-console.md @@ -0,0 +1,84 @@ +--- +phase: requirements +title: Requirements & Problem Understanding +description: Clarify the problem space, gather requirements, and define success criteria +--- + +# Requirements & Problem Understanding + +## Problem Statement +**What problem are we solving?** + +- Users can supervise running agents in `ai-devkit agent console`, but they cannot inspect AI DevKit memory from the same TUI. +- Users who want project recall while supervising or messaging agents must leave the console and run `ai-devkit memory search` or open `ai-devkit memory-dashboard`. +- This context switch makes memory less visible during everyday agent supervision, especially when users want to quickly see recent project decisions, conventions, or fixes before messaging an agent. + +## Goals & Objectives +**What do we want to achieve?** + +- Primary goals: + - Add a keyboard-accessible, read-only memory list inside `ai-devkit agent console`. + - Let users view recent memory items without leaving the console or interrupting agent list polling. + - Reuse the existing AI DevKit memory database path resolution and `@ai-devkit/memory` read APIs. + - Keep the console interaction model consistent with existing right-pane workspaces such as help, start agent, rename, and channel selection. +- Secondary goals: + - Show useful metadata for each item: title, scope, tags, and updated time. + - Support an empty state and error state that explain what happened without crashing the TUI. + - Document the shortcut in the help pane and footer hints. +- Non-goals: + - Creating, editing, deleting, merging, or storing memory from the console. + - Replacing `ai-devkit memory search`, `ai-devkit memory-dashboard`, or the browser graph UI. + - Full-text search UX in the first slice unless it falls out naturally from the read-only list implementation. + - Cross-project memory synchronization or remote memory access. + +## User Stories & Use Cases +**How will users interact with the solution?** + +- As a user supervising local agents, I want to open a memory list in the agent console so that I can recall project knowledge before messaging or directing an agent. +- As a user in a narrow terminal, I want the memory view to follow the console's existing narrow-layout replacement-pane behavior so that it remains readable. +- As a user with no stored memory, I want a clear empty state so that I know the feature is working and there is simply no data to show. +- As a user with a configured project memory path, I want the console to use the same memory database as `ai-devkit memory ...` commands so that the displayed items match CLI behavior. +- Key workflow: + - User runs `ai-devkit agent console`. + - User presses a new shortcut from the list/preview focus state. + - The right pane changes to a memory list. + - User scans recent memory items and presses the shortcut again or a back key to return to preview. +- Edge cases: + - Memory database does not exist yet. + - Memory database exists but has zero items. + - Configured memory database path is invalid or unreadable. + - Memory item titles, tags, or scopes are longer than the available terminal width. + - Agent console is already focused on chat input or another modal/workspace. + +## Success Criteria +**How will we know when we're done?** + +- `ai-devkit agent console` exposes a documented shortcut for opening a read-only memory list. +- The memory list renders recent memory items from the configured memory database using existing memory package APIs, not direct SQLite reads in the TUI. +- The memory list can be opened and closed without disrupting agent polling, selection, chat input behavior, channel controls, start/rename panes, kill confirmation, or help. +- Empty and error states are covered and understandable. +- Unit tests cover shortcut routing, right-pane mode selection, memory pane rendering, empty/error states, and help/footer hint updates. +- Existing console tests continue to pass. + +## Constraints & Assumptions +**What limitations do we need to work within?** + +- Technical constraints: + - The console is an Ink TUI in `packages/cli/src/tui/console`. + - The memory database path is resolved by CLI config code; the feature should avoid duplicating config resolution rules. + - Memory content is local developer data and should remain read-only in the console for this feature. + - Keyboard handling must not steal input while the chat input or another pane owns focus. +- Assumptions: + - A default recent-items list is the right first console view because it answers "what do I know already?" with the fewest controls. + - A read-only console view is lower risk than adding mutation flows. + - The first implementation can cap results to a small number, such as 20, to preserve TUI responsiveness. + +## Questions & Open Items +**What do we still need to clarify?** + +- Requirements review decisions: + - Shortcut: use uppercase `M` to open/close the memory list. Lowercase `m` remains mapped to message input. + - List ordering: show recent memory first using `updated-desc`. + - Search/filter controls: defer interactive search and filtering to a follow-up feature. This feature only needs a bounded recent list. + - Content preview: show title and metadata only in the initial list. Full-content detail is deferred unless implementation discovers the list is unusable without a compact detail preview. +- No unresolved material requirements questions remain for the first read-only memory-list slice. diff --git a/docs/ai/testing/2026-07-03-feature-view-memory-list-in-agent-console.md b/docs/ai/testing/2026-07-03-feature-view-memory-list-in-agent-console.md new file mode 100644 index 00000000..ca4fbf12 --- /dev/null +++ b/docs/ai/testing/2026-07-03-feature-view-memory-list-in-agent-console.md @@ -0,0 +1,115 @@ +--- +phase: testing +title: Testing Strategy +description: Define testing approach, test cases, and quality assurance +--- + +# Testing Strategy + +## Test Coverage Goals +**What level of testing do we aim for?** + +- Unit test coverage target: 100% of new console memory-list components and hooks where practical. +- Integration scope: console keyboard routing, pane mode rendering, config-aware memory loading, and adjacent shortcut regressions. +- End-to-end scope: manual TUI smoke test for opening/closing the memory list inside `ai-devkit agent console`. +- Acceptance criteria alignment: every requirements success criterion should map to at least one automated or manual validation item below. + +## Unit Tests +**What individual components need testing?** + +### `MemoryListPane` +- [x] Renders loading state without layout-breaking text. +- [x] Renders empty state when the memory list has no items. +- [x] Renders item title, scope, tags, and relative updated time for recent memory items. +- [x] Truncates or wraps long title/scope/tag values so they fit within the pane. +- [x] Renders an error state without throwing. + +### `useMemoryList` +- [x] Calls `ConfigManager.getMemoryDbPath()` before loading memory. +- [x] Calls `memoryListCommand` with a bounded limit and `updated-desc` sort. +- [x] Maps `KnowledgeItem` values to console row state. +- [x] Handles missing/empty databases as an empty list. +- [x] Converts thrown errors to hook error state and keeps previous UI stable. + +### Console Shortcut and Help +- [x] `M` opens the memory-list pane from list/preview focus. +- [x] `M` toggles back to preview when memory list is already open, or otherwise provides a documented back path. +- [x] `M` is ignored while chat input, start agent, rename, channel select, or kill confirmation owns input. +- [x] `HelpPane` includes the memory shortcut. +- [x] `StatusFooter` includes a compact memory hint without overflowing common widths. + +## Integration Tests +**How do we test component interactions?** + +- [x] Console renders memory list in the right pane on wide terminals. +- [x] Console renders memory list as the replacement pane on narrow terminals. +- [x] Existing shortcuts still work: `j/k`, `s`, `r`, `c`, `C`, `o`, `i/m`, `K`, `h`, `q`. +- [x] Memory loader uses the same configured database path as `ai-devkit memory` commands. +- [x] Memory load failure is isolated to the pane and does not stop agent polling. + +## End-to-End Tests +**What user flows need validation?** + +- [ ] Start `ai-devkit agent console`, press `M`, and verify recent memory items are visible. +- [ ] Press the documented return/toggle key and verify preview/chat input behavior returns. +- [ ] Run with an empty memory database and verify the empty state. +- [ ] Run with a deliberately invalid configured memory path and verify the error state. +- [ ] Regression smoke: send a message to a selected agent after opening/closing memory list. + +## Test Data +**What data do we use for testing?** + +- Mock `memoryListCommand` results for component/hook tests. +- Use fixtures with: + - zero memory items + - one short memory item + - multiple items with tags and different scopes + - long titles/tags/scopes + - thrown loader error +- For manual validation, create temporary memory items through `ai-devkit memory store` in a disposable configured database path. + +## Test Reporting & Coverage +**How do we verify and communicate test results?** + +- Run targeted tests for console TUI and memory command areas. +- Run broader package tests before implementation is considered complete. +- Record any coverage gaps with rationale in the implementation doc. +- Capture manual TUI smoke results in the implementation/testing docs. + +Current automated evidence: +- `npm --workspace packages/cli test -- src/__tests__/tui/console/hooks/useMemoryList.test.ts src/__tests__/tui/console/MemoryListPane.test.ts src/__tests__/tui/console/HelpPane.test.ts src/__tests__/tui/console/ConsoleApp.memory.test.ts` + - Exit 0; 4 files, 16 tests passed. +- `npm --workspace packages/cli run build` + - Exit 0; 187 files compiled. +- `npm --workspace packages/cli test` + - Exit 0 after building local workspace dependencies; 75 files, 880 tests passed during implementation verification. + - Final testing-phase run exited 0; 77 files, 888 tests passed. +- `npm --workspace packages/cli run test:coverage` + - First run failed at branch coverage 59.61%, below the 60% global threshold. + - Added focused branch tests for memory pane helpers, `getErrorMessage`, `deepEqual`, and console status display. + - Final run exited 0; 77 files, 888 tests passed; branch coverage 60.09%. + +## Manual Testing +**What requires human validation?** + +- Verify keyboard-only operation. +- Verify text remains readable on wide and narrow terminals. +- Verify the pane does not overlap footer, header, list, preview, or chat input. +- Verify memory content is not logged to the terminal outside the intended pane. + +Manual TUI smoke status: +- Not executed in this non-interactive testing pass. The automated tests cover mode routing, row bounds, error/empty states, and shortcut hints, but a human terminal smoke check is still recommended before release. + +## Performance Testing +**How do we validate performance?** + +- Seed more than the default result limit and verify only bounded rows render. +- Confirm opening the memory list does not visibly freeze agent list updates. +- Confirm repeated open/close cycles do not accumulate timers or duplicate loads unexpectedly. + +## Bug Tracking +**How do we manage issues?** + +- Track defects against this feature task or follow-up tickets. +- Treat crashes, memory data leakage, or broken existing console shortcuts as blocking. +- Treat search, pagination, and mutation requests as follow-up scope unless they block the read-only list. diff --git a/packages/cli/src/__tests__/tui/console/ConsoleApp.memory.test.ts b/packages/cli/src/__tests__/tui/console/ConsoleApp.memory.test.ts new file mode 100644 index 00000000..d3ae4cf2 --- /dev/null +++ b/packages/cli/src/__tests__/tui/console/ConsoleApp.memory.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from 'vitest'; +import { getNextRightPaneModeForMemoryShortcut } from '../../../tui/console/rightPaneMode.js'; + +describe('ConsoleApp memory shortcut helpers', () => { + it('opens the memory pane from preview mode', () => { + expect(getNextRightPaneModeForMemoryShortcut({ type: 'preview' })).toEqual({ type: 'memory-list' }); + }); + + it('toggles back to preview from memory mode', () => { + expect(getNextRightPaneModeForMemoryShortcut({ type: 'memory-list' })).toEqual({ type: 'preview' }); + }); +}); diff --git a/packages/cli/src/__tests__/tui/console/HelpPane.test.ts b/packages/cli/src/__tests__/tui/console/HelpPane.test.ts index 3a346bab..c5d45720 100644 --- a/packages/cli/src/__tests__/tui/console/HelpPane.test.ts +++ b/packages/cli/src/__tests__/tui/console/HelpPane.test.ts @@ -10,6 +10,7 @@ describe('HelpPane helpers', () => { { key: 'r', action: 'Rename selected agent' }, { key: 'c', action: 'Start Telegram channel for selected agent' }, { key: 'C', action: 'Stop Telegram channel' }, + { key: 'M', action: 'Show memory list' }, { key: 'o', action: 'Open selected agent terminal' }, { key: 'i / m', action: 'Message selected agent' }, { key: 'K', action: 'Kill selected agent' }, @@ -30,4 +31,8 @@ describe('HelpPane helpers', () => { expect(getConsoleHotkeyHints()).toContain('c channel'); expect(getConsoleHotkeyHints()).toContain('C stop'); }); + + it('includes memory in footer hints', () => { + expect(getConsoleHotkeyHints()).toContain('M memory'); + }); }); diff --git a/packages/cli/src/__tests__/tui/console/MemoryListPane.test.ts b/packages/cli/src/__tests__/tui/console/MemoryListPane.test.ts new file mode 100644 index 00000000..2e5909b9 --- /dev/null +++ b/packages/cli/src/__tests__/tui/console/MemoryListPane.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, it, vi } from 'vitest'; +import { + getHiddenMemoryCount, + getMemoryListDisplayState, + getMemoryListRows, + getMemoryListStatusText, + getVisibleMemoryItems, +} from '../../../tui/console/MemoryListPane.js'; +import type { ConsoleMemoryItem } from '../../../tui/console/types.js'; + +vi.mock('@ai-devkit/memory', () => ({ + memoryListCommand: vi.fn(), +}), { virtual: true }); + +const items: ConsoleMemoryItem[] = [ + { + id: 'mem-1', + title: 'Use response DTOs for API handlers', + scope: 'project:ai-devkit', + tags: ['api', 'backend'], + updatedAt: '2026-07-02T09:30:00.000Z', + }, + { + id: 'mem-2', + title: 'Keep console panes height bounded', + scope: 'repo:codeaholicguy/ai-devkit', + tags: ['tui'], + updatedAt: '2026-07-01T08:00:00.000Z', + }, +]; + +describe('MemoryListPane helpers', () => { + it('formats memory items as terminal rows', () => { + expect(getMemoryListRows(items, 80)).toEqual([ + { + title: 'Use response DTOs for API handlers', + meta: 'project:ai-devkit · api, backend · 2026-07-02', + }, + { + title: 'Keep console panes height bounded', + meta: 'repo:codeaholicguy/ai-devkit · tui · 2026-07-01', + }, + ]); + }); + + it('limits visible rows to the pane height budget', () => { + expect(getVisibleMemoryItems(items, 5)).toEqual([items[0]]); + expect(getVisibleMemoryItems(items, 8)).toEqual(items); + expect(getVisibleMemoryItems(items, 1)).toEqual([]); + }); + + it('truncates long titles and metadata to fit the pane width', () => { + const [row] = getMemoryListRows([ + { + id: 'mem-3', + title: 'x'.repeat(100), + scope: 'project:' + 'y'.repeat(80), + tags: ['frontend', 'console', 'memory'], + updatedAt: '2026-07-03T10:00:00.000Z', + }, + ], 30); + + expect(row.title).toHaveLength(30); + expect(row.title.endsWith('...')).toBe(true); + expect(row.meta.length).toBeLessThanOrEqual(30); + }); + + it('formats invalid dates and empty tags without crashing', () => { + expect(getMemoryListRows([ + { + id: 'mem-4', + title: 'No tags', + scope: 'global', + tags: [], + updatedAt: 'not-a-date', + }, + ], 80)).toEqual([ + { + title: 'No tags', + meta: 'global · untagged · not-a-date', + }, + ]); + }); + + it('classifies loading, error, empty, and item states', () => { + expect(getMemoryListDisplayState({ isLoading: true, error: null, rowCount: 0 })).toBe('loading'); + expect(getMemoryListDisplayState({ isLoading: false, error: 'db failed', rowCount: 0 })).toBe('error'); + expect(getMemoryListDisplayState({ isLoading: false, error: null, rowCount: 0 })).toBe('empty'); + expect(getMemoryListDisplayState({ isLoading: false, error: null, rowCount: 1 })).toBe('items'); + }); + + it('returns status text for non-item display states', () => { + expect(getMemoryListStatusText({ displayState: 'loading', error: null, width: 80 })).toBe('loading...'); + expect(getMemoryListStatusText({ displayState: 'error', error: 'x'.repeat(20), width: 10 })).toBe('xxxxxxx...'); + expect(getMemoryListStatusText({ displayState: 'error', error: null, width: 80 })).toBe('Could not load memory.'); + expect(getMemoryListStatusText({ displayState: 'empty', error: null, width: 80 })).toBe('No memory items yet.'); + expect(getMemoryListStatusText({ displayState: 'items', error: null, width: 80 })).toBeNull(); + }); + + it('computes hidden memory count without going below zero', () => { + expect(getHiddenMemoryCount(10, 4)).toBe(6); + expect(getHiddenMemoryCount(2, 4)).toBe(0); + }); +}); diff --git a/packages/cli/src/__tests__/tui/console/hooks/useMemoryList.test.ts b/packages/cli/src/__tests__/tui/console/hooks/useMemoryList.test.ts new file mode 100644 index 00000000..20ee1b89 --- /dev/null +++ b/packages/cli/src/__tests__/tui/console/hooks/useMemoryList.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, it, vi } from 'vitest'; +import type { KnowledgeItem } from '@ai-devkit/memory'; +import { loadMemoryList } from '../../../../tui/console/hooks/useMemoryList.js'; + +vi.mock('@ai-devkit/memory', () => ({ + memoryListCommand: vi.fn(), +}), { virtual: true }); + +const makeItem = (overrides: Partial = {}): KnowledgeItem => ({ + id: 'mem-1', + title: 'Use response DTOs', + content: 'Return response DTOs instead of domain entities.', + tags: ['api', 'backend'], + scope: 'project:ai-devkit', + normalizedTitle: 'use response dtos', + contentHash: 'hash', + createdAt: '2026-07-01T08:00:00.000Z', + updatedAt: '2026-07-02T09:30:00.000Z', + ...overrides, +}); + +describe('loadMemoryList', () => { + it('resolves the configured database path and loads recent memory items', async () => { + const getMemoryDbPath = vi.fn().mockResolvedValue('/tmp/memory.db'); + const memoryListCommand = vi.fn().mockReturnValue({ + items: [makeItem()], + total: 1, + }); + + const result = await loadMemoryList({ getMemoryDbPath, memoryListCommand }); + + expect(getMemoryDbPath).toHaveBeenCalledOnce(); + expect(memoryListCommand).toHaveBeenCalledWith({ + dbPath: '/tmp/memory.db', + limit: 20, + sort: 'updated-desc', + }); + expect(result.items).toEqual([ + { + id: 'mem-1', + title: 'Use response DTOs', + scope: 'project:ai-devkit', + tags: ['api', 'backend'], + updatedAt: '2026-07-02T09:30:00.000Z', + }, + ]); + expect(result.total).toBe(1); + }); + + it('passes undefined dbPath when no project memory path is configured', async () => { + const getMemoryDbPath = vi.fn().mockResolvedValue(undefined); + const memoryListCommand = vi.fn().mockReturnValue({ items: [], total: 0 }); + + await loadMemoryList({ getMemoryDbPath, memoryListCommand }); + + expect(memoryListCommand).toHaveBeenCalledWith({ + dbPath: undefined, + limit: 20, + sort: 'updated-desc', + }); + }); +}); diff --git a/packages/cli/src/__tests__/tui/console/render/formatStatus.test.ts b/packages/cli/src/__tests__/tui/console/render/formatStatus.test.ts new file mode 100644 index 00000000..05f89c68 --- /dev/null +++ b/packages/cli/src/__tests__/tui/console/render/formatStatus.test.ts @@ -0,0 +1,17 @@ +import { AgentStatus } from '@ai-devkit/agent-manager'; +import { describe, expect, it } from 'vitest'; +import { TUI_STATUS_LABELS } from '../../../../tui/design-system/index.js'; +import { getStatusDisplay } from '../../../../tui/console/render/formatStatus.js'; + +describe('getStatusDisplay', () => { + it('returns display values for known statuses', () => { + expect(getStatusDisplay(AgentStatus.RUNNING)).toEqual(TUI_STATUS_LABELS.running); + expect(getStatusDisplay(AgentStatus.WAITING)).toEqual(TUI_STATUS_LABELS.waiting); + expect(getStatusDisplay(AgentStatus.IDLE)).toEqual(TUI_STATUS_LABELS.idle); + expect(getStatusDisplay(AgentStatus.UNKNOWN)).toEqual(TUI_STATUS_LABELS.unknown); + }); + + it('falls back to unknown for unsupported status values', () => { + expect(getStatusDisplay('paused' as AgentStatus)).toEqual(TUI_STATUS_LABELS.unknown); + }); +}); diff --git a/packages/cli/src/__tests__/util/object.test.ts b/packages/cli/src/__tests__/util/object.test.ts new file mode 100644 index 00000000..dacd88ff --- /dev/null +++ b/packages/cli/src/__tests__/util/object.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from 'vitest'; +import { deepEqual } from '../../util/object.js'; + +describe('deepEqual', () => { + it('compares primitive, array, and object values', () => { + expect(deepEqual('x', 'x')).toBe(true); + expect(deepEqual('x', 1)).toBe(false); + expect(deepEqual(null, {})).toBe(false); + expect(deepEqual(['a', { b: 1 }], ['a', { b: 1 }])).toBe(true); + expect(deepEqual(['a'], ['a', 'b'])).toBe(false); + expect(deepEqual({ a: 1 }, { b: 1 })).toBe(false); + expect(deepEqual({ a: { b: 1 } }, { a: { b: 2 } })).toBe(false); + }); +}); diff --git a/packages/cli/src/__tests__/util/text.test.ts b/packages/cli/src/__tests__/util/text.test.ts index 9acc402f..bcefdb4d 100644 --- a/packages/cli/src/__tests__/util/text.test.ts +++ b/packages/cli/src/__tests__/util/text.test.ts @@ -1,7 +1,18 @@ -import { truncate } from '../../util/text.js'; +import { getErrorMessage, truncate } from '../../util/text.js'; describe('text util', () => { + describe('getErrorMessage', () => { + it('returns Error messages', () => { + expect(getErrorMessage(new Error('memory failed'))).toBe('memory failed'); + }); + + it('stringifies non-Error values', () => { + expect(getErrorMessage('plain failure')).toBe('plain failure'); + expect(getErrorMessage(404)).toBe('404'); + }); + }); + describe('truncate', () => { it('returns original text when it is within maxLength', () => { expect(truncate('hello', 10)).toBe('hello'); diff --git a/packages/cli/src/tui/console/ConsoleApp.tsx b/packages/cli/src/tui/console/ConsoleApp.tsx index 391021ca..9311079b 100644 --- a/packages/cli/src/tui/console/ConsoleApp.tsx +++ b/packages/cli/src/tui/console/ConsoleApp.tsx @@ -17,9 +17,11 @@ import { StartAgentPane } from './StartAgentPane.js'; import { RenameAgentPane } from './RenameAgentPane.js'; import { ChannelSelectPane } from './ChannelSelectPane.js'; import { HelpPane } from './HelpPane.js'; +import { MemoryListPane } from './MemoryListPane.js'; import { KillConfirmDialog } from './KillConfirmDialog.js'; import type { ConsoleFocus, RightPaneMode, TransientMessage } from './types.js'; import { Panel } from '../design-system/index.js'; +import { getNextRightPaneModeForMemoryShortcut } from './rightPaneMode.js'; interface ConsoleAppProps { manager: AgentManager; @@ -75,8 +77,9 @@ const ConsoleAppShell: React.FC<{ const startPaneActive = rightPaneMode.type === 'start-agent'; const renamePaneActive = rightPaneMode.type === 'rename-agent'; const channelSelectPaneActive = rightPaneMode.type === 'channel-select'; + const memoryListPaneActive = rightPaneMode.type === 'memory-list'; const helpPaneActive = rightPaneMode.type === 'help'; - const inputFocused = focus === 'input' && !startPaneActive && !renamePaneActive && !channelSelectPaneActive && !helpPaneActive; + const inputFocused = focus === 'input' && !startPaneActive && !renamePaneActive && !channelSelectPaneActive && !memoryListPaneActive && !helpPaneActive; useEffect(() => { if (!inputFocused) setInputLines(1); @@ -224,6 +227,11 @@ const ConsoleAppShell: React.FC<{ return; } + if (input === 'M') { + setRightPaneMode(getNextRightPaneModeForMemoryShortcut); + return; + } + if (input === 's') { openStartPane(); return; @@ -285,6 +293,12 @@ const ConsoleAppShell: React.FC<{ height={contentHeight} /> ); + const memoryListPane = ( + + ); const renamePane = renamePaneActive ? ( 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', 'M memory', 'o open', 'i message', 'K kill', 'h help', 'q quit']; } interface HelpPaneProps { diff --git a/packages/cli/src/tui/console/MemoryListPane.tsx b/packages/cli/src/tui/console/MemoryListPane.tsx new file mode 100644 index 00000000..3cae1579 --- /dev/null +++ b/packages/cli/src/tui/console/MemoryListPane.tsx @@ -0,0 +1,124 @@ +import React from 'react'; +import { Box, Text } from 'ink'; +import { truncate } from '../../util/text.js'; +import { Panel, SectionTitle, TUI_COLORS } from '../design-system/index.js'; +import { formatRelative } from './render/formatRelative.js'; +import { useMemoryList } from './hooks/useMemoryList.js'; +import type { ConsoleMemoryItem } from './types.js'; + +interface MemoryListPaneProps { + width: number; + height: number; +} + +export interface MemoryListRow { + title: string; + meta: string; +} + +export type MemoryListDisplayState = 'loading' | 'error' | 'empty' | 'items'; + +export function getMemoryListDisplayState(input: { + isLoading: boolean; + error: string | null; + rowCount: number; +}): MemoryListDisplayState { + if (input.isLoading) return 'loading'; + if (input.error) return 'error'; + if (input.rowCount === 0) return 'empty'; + return 'items'; +} + +export function getMemoryListStatusText(input: { + displayState: MemoryListDisplayState; + error: string | null; + width: number; +}): string | null { + if (input.displayState === 'loading') return 'loading...'; + if (input.displayState === 'error') return truncate(input.error ?? 'Could not load memory.', input.width); + if (input.displayState === 'empty') return 'No memory items yet.'; + return null; +} + +function formatUpdatedDate(value: string): string { + const date = new Date(value); + if (Number.isNaN(date.getTime())) return value; + return date.toISOString().slice(0, 10); +} + +function buildMeta(item: ConsoleMemoryItem): string { + const tags = item.tags.length > 0 ? item.tags.join(', ') : 'untagged'; + return `${item.scope} · ${tags} · ${formatUpdatedDate(item.updatedAt)}`; +} + +export function getVisibleMemoryItems(items: ConsoleMemoryItem[], height: number): ConsoleMemoryItem[] { + const rowBudget = Math.max(0, Math.floor((height - 3) / 2)); + return items.slice(0, rowBudget); +} + +export function getHiddenMemoryCount(total: number, visibleCount: number): number { + return Math.max(0, total - visibleCount); +} + +export function getMemoryListRows(items: ConsoleMemoryItem[], width: number): MemoryListRow[] { + const contentWidth = Math.max(4, width); + return items.map(item => ({ + title: truncate(item.title, contentWidth), + meta: truncate(buildMeta(item), contentWidth), + })); +} + +export const MemoryListPane: React.FC = ({ width, height }) => { + const { items, total, isLoading, error, lastUpdated } = useMemoryList(); + const innerWidth = Math.max(4, width - 4); + const visibleItems = getVisibleMemoryItems(items, height - 2); + const rows = getMemoryListRows(visibleItems, innerWidth); + const hiddenCount = getHiddenMemoryCount(total, visibleItems.length); + const displayState = getMemoryListDisplayState({ isLoading, error, rowCount: rows.length }); + const statusText = getMemoryListStatusText({ displayState, error, width: innerWidth }); + + return ( + + + MEMORY + · recent + {lastUpdated ? ( + <> + · + updated {formatRelative(lastUpdated)} + + ) : null} + + + + {displayState === 'loading' ? ( + {statusText} + ) : displayState === 'error' ? ( + {statusText} + ) : displayState === 'empty' ? ( + {statusText} + ) : ( + rows.map((row, index) => ( + + {row.title} + {row.meta} + + )) + )} + + + {!isLoading && !error && hiddenCount > 0 ? ( + + +{hiddenCount} more + + ) : null} + + ); +}; diff --git a/packages/cli/src/tui/console/hooks/useMemoryList.ts b/packages/cli/src/tui/console/hooks/useMemoryList.ts new file mode 100644 index 00000000..1b7f39d4 --- /dev/null +++ b/packages/cli/src/tui/console/hooks/useMemoryList.ts @@ -0,0 +1,90 @@ +import { useEffect, useState } from 'react'; +import { memoryListCommand } from '@ai-devkit/memory'; +import type { KnowledgeItem, MemoryListOptions, ListKnowledgeResult } from '@ai-devkit/memory'; +import { ConfigManager } from '../../../lib/Config.js'; +import { getErrorMessage } from '../../../util/text.js'; +import type { ConsoleMemoryItem, MemoryListState } from '../types.js'; + +const DEFAULT_MEMORY_LIMIT = 20; + +export interface LoadMemoryListDeps { + getMemoryDbPath: () => Promise; + memoryListCommand: (options: MemoryListOptions) => ListKnowledgeResult; +} + +export interface LoadedMemoryList { + items: ConsoleMemoryItem[]; + total: number; +} + +function toConsoleMemoryItem(item: KnowledgeItem): ConsoleMemoryItem { + return { + id: item.id, + title: item.title, + scope: item.scope, + tags: item.tags, + updatedAt: item.updatedAt, + }; +} + +export async function loadMemoryList(deps: LoadMemoryListDeps): Promise { + const dbPath = await deps.getMemoryDbPath(); + const result = deps.memoryListCommand({ + dbPath, + limit: DEFAULT_MEMORY_LIMIT, + sort: 'updated-desc', + }); + + return { + items: result.items.map(toConsoleMemoryItem), + total: result.total, + }; +} + +export function useMemoryList(): MemoryListState { + const [state, setState] = useState({ + items: [], + total: 0, + isLoading: true, + error: null, + lastUpdated: null, + }); + + useEffect(() => { + let active = true; + setState(current => ({ + ...current, + isLoading: true, + error: null, + })); + + const configManager = new ConfigManager(); + void loadMemoryList({ + getMemoryDbPath: () => configManager.getMemoryDbPath(), + memoryListCommand, + }).then(result => { + if (!active) return; + setState({ + items: result.items, + total: result.total, + isLoading: false, + error: null, + lastUpdated: new Date(), + }); + }).catch(error => { + if (!active) return; + setState(current => ({ + ...current, + isLoading: false, + error: getErrorMessage(error), + lastUpdated: new Date(), + })); + }); + + return () => { + active = false; + }; + }, []); + + return state; +} diff --git a/packages/cli/src/tui/console/render/formatStatus.tsx b/packages/cli/src/tui/console/render/formatStatus.tsx index 2569d47a..be72d590 100644 --- a/packages/cli/src/tui/console/render/formatStatus.tsx +++ b/packages/cli/src/tui/console/render/formatStatus.tsx @@ -20,8 +20,12 @@ export interface FormatStatusProps { status: AgentStatus; } +export function getStatusDisplay(status: AgentStatus): StatusGlyph { + return STATUS_DISPLAY[status] ?? STATUS_DISPLAY[AgentStatus.UNKNOWN]; +} + const FormatStatusInner: React.FC = ({ status }) => { - const { glyph, label, color } = STATUS_DISPLAY[status] ?? STATUS_DISPLAY[AgentStatus.UNKNOWN]; + const { glyph, label, color } = getStatusDisplay(status); return {glyph} {label}; }; diff --git a/packages/cli/src/tui/console/rightPaneMode.ts b/packages/cli/src/tui/console/rightPaneMode.ts new file mode 100644 index 00000000..aa813268 --- /dev/null +++ b/packages/cli/src/tui/console/rightPaneMode.ts @@ -0,0 +1,5 @@ +import type { RightPaneMode } from './types.js'; + +export function getNextRightPaneModeForMemoryShortcut(current: RightPaneMode): RightPaneMode { + return current.type === 'memory-list' ? { type: 'preview' } : { type: 'memory-list' }; +} diff --git a/packages/cli/src/tui/console/types.ts b/packages/cli/src/tui/console/types.ts index 71cebd83..3a0ef229 100644 --- a/packages/cli/src/tui/console/types.ts +++ b/packages/cli/src/tui/console/types.ts @@ -15,11 +15,28 @@ export interface ConfiguredChannel { botUsername?: string; } +export interface ConsoleMemoryItem { + id: string; + title: string; + scope: string; + tags: string[]; + updatedAt: string; +} + +export interface MemoryListState { + items: ConsoleMemoryItem[]; + total: number; + isLoading: boolean; + error: string | null; + lastUpdated: Date | null; +} + export type RightPaneMode = | { type: 'preview' } | { type: 'start-agent' } | { type: 'rename-agent'; agentName: string } | { type: 'channel-select'; agentName: string } + | { type: 'memory-list' } | { type: 'help' }; export type TransientMessage = { kind: 'info' | 'error'; text: string };