docs(19): create agent inbox phase plan

Phase 19: Agent Inbox
- 4 plans in 2 waves
- 3 parallel (Wave 1), 1 sequential (Wave 2)
- Ready for execution
This commit is contained in:
Lukas May
2026-02-04 21:47:58 +01:00
parent 9586e379bc
commit d9b6ce4748
5 changed files with 491 additions and 2 deletions

View File

@@ -294,10 +294,13 @@ Plans:
**Goal**: Message list, multi-question forms (radio/checkbox/free-text), answer submission, notifications **Goal**: Message list, multi-question forms (radio/checkbox/free-text), answer submission, notifications
**Depends on**: Phase 18 **Depends on**: Phase 18
**Research**: Unlikely (internal patterns, form handling from wireframes) **Research**: Unlikely (internal patterns, form handling from wireframes)
**Plans**: TBD **Plans**: 4 plans
Plans: Plans:
- [ ] 19-01: TBD (run /gsd:plan-phase 19 to break down) - [ ] 19-01: Backend API for Agent Questions
- [ ] 19-02: InboxList & MessageCard Components
- [ ] 19-03: QuestionForm & Input Components
- [ ] 19-04: Inbox Page Assembly
#### Phase 20: Real-time Subscriptions #### Phase 20: Real-time Subscriptions

View File

@@ -0,0 +1,99 @@
---
phase: 19-agent-inbox
plan: 01
type: execute
wave: 1
depends_on: []
files_modified: [src/trpc/router.ts]
autonomous: true
---
<objective>
Add tRPC procedures to expose structured agent question data to the frontend.
Purpose: The agent inbox UI needs structured question data (options, multiSelect) to render form controls. Questions are stored in-memory on AgentManager via `getPendingQuestions(agentId)`, but no tRPC procedure exposes this. The message table `content` is a plain string and does NOT contain the structured question data. This plan bridges that gap.
Output: Two new tRPC procedures: `getAgentQuestions` and `listWaitingAgents`.
</objective>
<execution_context>
@~/.claude/get-shit-done/workflows/execute-plan.md
@~/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@src/trpc/router.ts
@src/agent/types.ts
@src/agent/schema.ts
</context>
<tasks>
<task type="auto">
<name>Task 1: Add getAgentQuestions tRPC procedure</name>
<files>src/trpc/router.ts</files>
<action>
Add a `getAgentQuestions` query procedure to the appRouter. It should:
1. Accept `agentIdentifierSchema` input (same as getAgent — name or id)
2. Resolve the agent via `resolveAgent(ctx, input)`
3. Call `agentManager.getPendingQuestions(agent.id)`
4. Return the `PendingQuestions` object (or null if no pending questions)
Place it near the existing `getAgentResult` procedure since it follows the same pattern.
Also add a `listWaitingAgents` query that filters `agentManager.list()` to only agents with `status === 'waiting_for_input'`. This lets the inbox efficiently fetch only agents that have questions without filtering client-side.
Update the JSDoc procedure list comment at the top of appRouter to include both new procedures.
</action>
<verify>npx tsc --noEmit (TypeScript compiles without errors)</verify>
<done>
- `getAgentQuestions` procedure exists and returns PendingQuestions | null
- `listWaitingAgents` procedure exists and returns AgentInfo[] filtered to waiting_for_input
- TypeScript compiles clean
</done>
</task>
<task type="auto">
<name>Task 2: Export PendingQuestions and QuestionItem types from shared package</name>
<files>packages/shared/src/types.ts</files>
<action>
The frontend needs `PendingQuestions` and `QuestionItem` types to properly type the question form props. Export these types from the shared package:
1. In `packages/shared/src/types.ts`, add re-exports for `PendingQuestions` and `QuestionItem` from `../../src/agent/types.js`
2. If the import path doesn't work with the shared package's rootDir config, define matching interfaces directly in the shared types file (same shapes as in src/agent/types.ts)
The key types needed by the frontend are:
- `QuestionItem`: `{ id: string; question: string; options?: { label: string; description?: string }[]; multiSelect?: boolean }`
- `PendingQuestions`: `{ questions: QuestionItem[] }`
</action>
<verify>npx tsc --noEmit -p packages/shared/tsconfig.json && npx tsc --noEmit -p packages/web/tsconfig.app.json</verify>
<done>
- PendingQuestions and QuestionItem available to import from @codewalk-district/shared
- Both frontend and backend packages compile
</done>
</task>
</tasks>
<verification>
Before declaring plan complete:
- [ ] `npx tsc --noEmit` passes in root
- [ ] `npm run build` succeeds
- [ ] New procedures visible in router type
</verification>
<success_criteria>
- getAgentQuestions procedure returns structured question data from AgentManager
- listWaitingAgents returns only agents in waiting_for_input status
- PendingQuestions/QuestionItem types available in shared package
- All builds pass
</success_criteria>
<output>
After completion, create `.planning/phases/19-agent-inbox/19-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,120 @@
---
phase: 19-agent-inbox
plan: 02
type: execute
wave: 1
depends_on: []
files_modified: [packages/web/src/components/InboxList.tsx, packages/web/src/components/MessageCard.tsx]
autonomous: true
---
<objective>
Build the InboxList and MessageCard components for the agent inbox message list view.
Purpose: These are the entry-point components for the inbox — showing a filterable, sortable list of agent messages. Following the wireframe spec from docs/wireframes/agent-inbox.md.
Output: InboxList and MessageCard components with filter/sort controls and empty state.
</objective>
<execution_context>
@~/.claude/get-shit-done/workflows/execute-plan.md
@~/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@docs/wireframes/agent-inbox.md
@packages/web/src/components/InitiativeList.tsx
@packages/web/src/components/InitiativeCard.tsx
@packages/web/src/components/StatusBadge.tsx
@packages/web/src/components/ui/card.tsx
@packages/web/src/components/ui/badge.tsx
@packages/web/src/components/ui/button.tsx
</context>
<tasks>
<task type="auto">
<name>Task 1: Create MessageCard component</name>
<files>packages/web/src/components/MessageCard.tsx</files>
<action>
Create MessageCard following the wireframe spec. Props:
- `agentName: string` — agent human-readable name
- `agentStatus: string` — 'waiting_for_input' | 'running' | 'stopped' | etc.
- `preview: string` — message preview text (truncated)
- `timestamp: string` — ISO date string for relative time display
- `requiresResponse: boolean` — determines filled (●) vs empty (○) indicator
- `isSelected: boolean` — highlighted state
- `onClick: () => void`
Display:
- Status indicator: ● (filled circle) when requiresResponse=true, ○ (empty) otherwise
- Agent name with status text in parentheses (e.g., "gastown (waiting)")
- Message preview truncated to ~80 chars with ellipsis
- Relative timestamp using simple helper (e.g., "2 min ago", "1h ago"). Write an inline `formatRelativeTime(isoDate: string): string` helper at the top of the file — do NOT create a separate utils file.
- Selected state: slightly different background via className toggle
Use shadcn Card as the base, Tailwind for styling. Match the visual density of InitiativeCard.
</action>
<verify>npx tsc --noEmit -p packages/web/tsconfig.app.json</verify>
<done>MessageCard renders with indicator, name, preview, timestamp, and selected state</done>
</task>
<task type="auto">
<name>Task 2: Create InboxList component</name>
<files>packages/web/src/components/InboxList.tsx</files>
<action>
Create InboxList following the wireframe spec. Props:
- `agents: Array<{ id: string; name: string; status: string; taskId: string; updatedAt: string }>` — agent data
- `messages: Array<{ id: string; senderId: string | null; content: string; requiresResponse: boolean; status: string; createdAt: string }>` — message data
- `selectedAgentId: string | null` — currently selected agent
- `onSelectAgent: (agentId: string) => void` — selection handler
- `onRefresh: () => void` — refresh handler
Internal state:
- `filter: 'all' | 'waiting' | 'completed'` — default 'all'
- `sort: 'newest' | 'oldest'` — default 'newest'
Behavior:
1. Join agents with their latest message (match message.senderId to agent.id)
2. Filter: 'waiting' = requiresResponse messages, 'completed' = responded/non-requiring messages, 'all' = everything
3. Sort by message timestamp (newest or oldest)
4. Render MessageCard for each result
5. Show header with count badge: "Agent Inbox (3)" and Refresh button
6. Filter and sort as simple button groups or select elements (not dropdowns — keep it simple)
7. Empty state when no messages match filter: "No pending messages" with subtitle text
Follow the pattern from InitiativeList for the list layout structure.
</action>
<verify>npx tsc --noEmit -p packages/web/tsconfig.app.json</verify>
<done>
- InboxList renders with filter/sort controls
- MessageCard instances rendered for each agent+message pair
- Empty state shown when no messages
- Filter and sort work correctly
</done>
</task>
</tasks>
<verification>
Before declaring plan complete:
- [ ] `npx tsc --noEmit -p packages/web/tsconfig.app.json` passes
- [ ] `npx vite build` in packages/web succeeds
</verification>
<success_criteria>
- MessageCard shows indicator, agent name, preview, relative time
- InboxList filters by waiting/completed/all
- InboxList sorts by newest/oldest
- Empty state renders when no messages match
- All builds pass
</success_criteria>
<output>
After completion, create `.planning/phases/19-agent-inbox/19-02-SUMMARY.md`
</output>

View File

@@ -0,0 +1,128 @@
---
phase: 19-agent-inbox
plan: 03
type: execute
wave: 1
depends_on: []
files_modified: [packages/web/src/components/QuestionForm.tsx, packages/web/src/components/OptionGroup.tsx, packages/web/src/components/FreeTextInput.tsx]
autonomous: true
---
<objective>
Build the question form components that render multi-question forms with mixed input types.
Purpose: Agents ask structured questions with options (radio/checkbox), multi-select, free-text, and "Other" fields. These components render the question forms inside the message detail view. Following the wireframe spec from docs/wireframes/agent-inbox.md.
Output: QuestionForm, OptionGroup, and FreeTextInput components.
</objective>
<execution_context>
@~/.claude/get-shit-done/workflows/execute-plan.md
@~/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@docs/wireframes/agent-inbox.md
@src/agent/types.ts
@packages/web/src/components/ui/input.tsx
@packages/web/src/components/ui/label.tsx
@packages/web/src/components/ui/textarea.tsx
@packages/web/src/components/ui/button.tsx
</context>
<tasks>
<task type="auto">
<name>Task 1: Create OptionGroup and FreeTextInput components</name>
<files>packages/web/src/components/OptionGroup.tsx, packages/web/src/components/FreeTextInput.tsx</files>
<action>
**OptionGroup** — renders radio buttons OR checkboxes for a question's options. Props:
- `questionId: string` — for form identification
- `options: Array<{ label: string; description?: string }>` — available choices
- `multiSelect: boolean` — checkboxes (true) or radio buttons (false)
- `value: string` — current selection (for radio: single label, for checkbox: comma-separated labels)
- `onChange: (value: string) => void` — change handler
- `allowOther?: boolean` — show "Other" free-text option (default true)
Behavior:
- Radio buttons for single-select (default). Use native HTML radio inputs with Tailwind styling.
- Checkboxes for multi-select. Use native HTML checkbox inputs with Tailwind styling.
- Each option shows label and optional description in lighter text
- "Other" option: text input that auto-selects its radio/checkbox when user types. The "Other" value is whatever the user typed.
- For multi-select, value is comma-joined selected labels. When "Other" is checked, append the typed text.
**FreeTextInput** — renders when question has NO options (pure free-text). Props:
- `questionId: string`
- `value: string`
- `onChange: (value: string) => void`
- `multiline?: boolean` — textarea vs single input (default false)
- `placeholder?: string`
Use shadcn Input for single-line, shadcn Textarea for multiline. Keep it simple.
</action>
<verify>npx tsc --noEmit -p packages/web/tsconfig.app.json</verify>
<done>
- OptionGroup renders radio or checkbox based on multiSelect prop
- "Other" field auto-selects when typed into
- FreeTextInput renders Input or Textarea based on multiline prop
</done>
</task>
<task type="auto">
<name>Task 2: Create QuestionForm component</name>
<files>packages/web/src/components/QuestionForm.tsx</files>
<action>
QuestionForm orchestrates rendering multiple questions with mixed input types. Props:
- `questions: Array<{ id: string; question: string; options?: Array<{ label: string; description?: string }>; multiSelect?: boolean }>` — structured question data from agent
- `onSubmit: (answers: Record<string, string>) => void` — submit handler with questionId→answer map
- `onCancel: () => void` — cancel handler
- `isSubmitting?: boolean` — disable form during submission
Internal state:
- `answers: Record<string, string>` — maps question ID to answer string
Behavior:
1. Render each question sequentially with "Q1:", "Q2:", etc. prefix and question text
2. For each question, determine input type:
- If `options` array exists → render OptionGroup (with multiSelect from question)
- If no options → render FreeTextInput
3. Track answers in local state via `onAnswerChange` callbacks
4. "Send Answers" button: enabled only when ALL questions have non-empty answers
5. "Cancel" button: always enabled
6. On submit: call `onSubmit(answers)` with the complete answer map
7. Button row at bottom: [Cancel] [Send Answers] — right-aligned, matching wireframe
Use shadcn Button for actions.
</action>
<verify>npx tsc --noEmit -p packages/web/tsconfig.app.json</verify>
<done>
- QuestionForm renders mixed question types from questions array
- Submit disabled until all questions answered
- onSubmit called with Record<string, string> mapping questionId to answer
</done>
</task>
</tasks>
<verification>
Before declaring plan complete:
- [ ] `npx tsc --noEmit -p packages/web/tsconfig.app.json` passes
- [ ] `npx vite build` in packages/web succeeds
</verification>
<success_criteria>
- OptionGroup handles radio (single-select) and checkbox (multi-select) with "Other" field
- FreeTextInput handles single-line and multiline inputs
- QuestionForm renders sequential questions with correct input type per question
- Submit validates all questions answered
- All builds pass
</success_criteria>
<output>
After completion, create `.planning/phases/19-agent-inbox/19-03-SUMMARY.md`
</output>

View File

@@ -0,0 +1,139 @@
---
phase: 19-agent-inbox
plan: 04
type: execute
wave: 2
depends_on: ["19-01", "19-02", "19-03"]
files_modified: [packages/web/src/routes/inbox.tsx]
autonomous: true
---
<objective>
Wire the inbox page with tRPC data fetching, message detail panel, and answer submission.
Purpose: Assemble all Phase 19 components into the working inbox route. Connects InboxList + QuestionForm to backend via tRPC, handles the full answer submission flow (submit answers → resume agent), and shows notification messages.
Output: Fully functional Agent Inbox page at /inbox route.
</objective>
<execution_context>
@~/.claude/get-shit-done/workflows/execute-plan.md
@~/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/19-agent-inbox/19-01-SUMMARY.md
@.planning/phases/19-agent-inbox/19-02-SUMMARY.md
@.planning/phases/19-agent-inbox/19-03-SUMMARY.md
@docs/wireframes/agent-inbox.md
@packages/web/src/routes/inbox.tsx
@packages/web/src/routes/initiatives/$id.tsx
@packages/web/src/lib/trpc.ts
@packages/web/src/components/InboxList.tsx
@packages/web/src/components/QuestionForm.tsx
@packages/web/src/components/MessageCard.tsx
</context>
<tasks>
<task type="auto">
<name>Task 1: Wire inbox page with data fetching, detail panel, and answer submission</name>
<files>packages/web/src/routes/inbox.tsx</files>
<action>
Replace the stub inbox page with the full implementation. The page has two sections:
1. **Left/main area**: InboxList showing agents with messages
2. **Detail panel**: appears when an agent is selected, showing message detail + question form
**Data fetching:**
- `trpc.listWaitingAgents.useQuery({})` — get agents in waiting_for_input status
- `trpc.listMessages.useQuery({})` — get all user-addressed messages
- When agent selected: `trpc.getAgentQuestions.useQuery({ id: selectedAgentId }, { enabled: !!selectedAgentId })` — get structured question data
**Page state:**
- `selectedAgentId: string | null` — which agent's detail is shown
- Track loading/error states for each query
**Message Detail section** (inline in this file, not a separate component — it's page-specific):
- Header: agent name, relative timestamp, task info (use agent.taskId to show task reference)
- If agent has pending questions: render QuestionForm with the structured questions
- If message is notification (requiresResponse=false): show content with "Dismiss" button
**Answer submission flow:**
1. User fills QuestionForm and clicks "Send Answers"
2. Call `trpc.resumeAgent.useMutation()` with `{ id: selectedAgentId, answers }`
3. On success: invalidate listWaitingAgents and listMessages queries, clear selectedAgentId
4. On error: show error message
**Notification handling:**
- For messages with `requiresResponse: false` (type='info'), show content text
- "Dismiss" button: call `trpc.respondToMessage.useMutation()` to mark as responded, then invalidate queries
**Layout:**
- Use responsive grid similar to initiative detail: `lg:grid-cols-[1fr_400px]`
- InboxList on left, detail panel on right (or stacked on mobile)
- Loading states: skeleton or spinner matching existing patterns from initiatives pages
- Error states: simple error text matching existing patterns
Follow patterns established in `initiatives/$id.tsx` for query management, loading states, and mutation/invalidation flow.
</action>
<verify>npx tsc --noEmit -p packages/web/tsconfig.app.json && cd packages/web && npx vite build</verify>
<done>
- Inbox page fetches agents and messages via tRPC
- Selecting an agent shows detail panel with questions
- Answer submission calls resumeAgent and refreshes data
- Notification messages can be dismissed
- Loading and error states handled
</done>
</task>
<task type="auto">
<name>Task 2: Full build verification and integration check</name>
<files>packages/web/src/routes/inbox.tsx</files>
<action>
Run full build verification:
1. `npx tsc --noEmit` in root — all TypeScript checks pass
2. `npm run build` in root — full monorepo build succeeds
3. `cd packages/web && npx vite build` — frontend bundle builds
4. Verify route is registered in routeTree.gen.ts for /inbox
5. Verify no unused imports or type errors
Fix any issues found during verification.
</action>
<verify>npm run build (root) succeeds with zero errors</verify>
<done>
- All TypeScript checks pass
- Monorepo build succeeds
- Frontend bundles without errors
- /inbox route properly registered
</done>
</task>
</tasks>
<verification>
Before declaring plan complete:
- [ ] `npx tsc --noEmit` passes in root
- [ ] `npm run build` succeeds in root
- [ ] `npx vite build` succeeds in packages/web
- [ ] /inbox route registered in routeTree.gen.ts
- [ ] No TypeScript errors or warnings
</verification>
<success_criteria>
- Inbox page shows list of waiting agents with their messages
- Selecting an agent shows structured question form
- Answer submission calls resumeAgent and refreshes the list
- Notification messages display content and can be dismissed
- Loading and error states work correctly
- All builds pass
</success_criteria>
<output>
After completion, create `.planning/phases/19-agent-inbox/19-04-SUMMARY.md`
</output>