diff --git a/apps/server/agent/manager.test.ts b/apps/server/agent/manager.test.ts index d9a751d..76db2b6 100644 --- a/apps/server/agent/manager.test.ts +++ b/apps/server/agent/manager.test.ts @@ -139,6 +139,7 @@ describe('MultiProviderAgentManager', () => { findByStatus: vi.fn().mockResolvedValue([mockAgent]), update: vi.fn().mockResolvedValue(mockAgent), delete: vi.fn().mockResolvedValue(undefined), + findWaitingWithContext: vi.fn().mockResolvedValue([]), }; mockProjectRepository = { diff --git a/apps/server/agent/mutex-completion.test.ts b/apps/server/agent/mutex-completion.test.ts index ec39cbe..b2a039a 100644 --- a/apps/server/agent/mutex-completion.test.ts +++ b/apps/server/agent/mutex-completion.test.ts @@ -46,7 +46,8 @@ describe('OutputHandler completion mutex', () => { async findByTaskId() { throw new Error('Not implemented'); }, async findByName() { throw new Error('Not implemented'); }, async findBySessionId() { throw new Error('Not implemented'); }, - async delete() { throw new Error('Not implemented'); } + async delete() { throw new Error('Not implemented'); }, + async findWaitingWithContext() { throw new Error('Not implemented'); } }; beforeEach(() => { diff --git a/apps/server/db/repositories/agent-repository.ts b/apps/server/db/repositories/agent-repository.ts index f4f4994..656d748 100644 --- a/apps/server/db/repositories/agent-repository.ts +++ b/apps/server/db/repositories/agent-repository.ts @@ -8,6 +8,14 @@ import type { Agent } from '../schema.js'; import type { AgentMode } from '../../agent/types.js'; +/** Agent row enriched with joined task/phase/initiative context fields. */ +export interface AgentWithContext extends Agent { + taskName: string | null; + phaseName: string | null; + initiativeName: string | null; + taskDescription: string | null; +} + /** * Agent status values. */ @@ -117,4 +125,10 @@ export interface AgentRepository { * Throws if agent not found. */ delete(id: string): Promise; + + /** + * Find all agents with status 'waiting_for_input', enriched with + * task, phase, and initiative names via LEFT JOINs. + */ + findWaitingWithContext(): Promise; } diff --git a/apps/server/db/repositories/drizzle/agent.test.ts b/apps/server/db/repositories/drizzle/agent.test.ts index e40bfaf..e43cbf2 100644 --- a/apps/server/db/repositories/drizzle/agent.test.ts +++ b/apps/server/db/repositories/drizzle/agent.test.ts @@ -277,3 +277,91 @@ describe('DrizzleAgentRepository', () => { }); }); }); + +describe('DrizzleAgentRepository.findWaitingWithContext()', () => { + let agentRepo: DrizzleAgentRepository; + let taskRepo: DrizzleTaskRepository; + let phaseRepo: DrizzlePhaseRepository; + let initiativeRepo: DrizzleInitiativeRepository; + + beforeEach(() => { + const db = createTestDatabase(); + agentRepo = new DrizzleAgentRepository(db); + taskRepo = new DrizzleTaskRepository(db); + phaseRepo = new DrizzlePhaseRepository(db); + initiativeRepo = new DrizzleInitiativeRepository(db); + }); + + it('returns empty array when no waiting agents exist', async () => { + const result = await agentRepo.findWaitingWithContext(); + expect(result).toEqual([]); + }); + + it('only returns agents with status waiting_for_input', async () => { + await agentRepo.create({ name: 'running-agent', worktreeId: 'wt1', status: 'running' }); + await agentRepo.create({ name: 'waiting-agent', worktreeId: 'wt2', status: 'waiting_for_input' }); + + const result = await agentRepo.findWaitingWithContext(); + expect(result).toHaveLength(1); + expect(result[0].name).toBe('waiting-agent'); + }); + + it('populates taskName, phaseName, initiativeName, taskDescription when FK associations exist', async () => { + const initiative = await initiativeRepo.create({ name: 'My Initiative' }); + const phase = await phaseRepo.create({ initiativeId: initiative.id, name: 'Phase 1' }); + const task = await taskRepo.create({ + phaseId: phase.id, + name: 'Implement feature', + description: 'Write the feature code', + }); + + await agentRepo.create({ + name: 'ctx-agent', + worktreeId: 'wt3', + status: 'waiting_for_input', + taskId: task.id, + initiativeId: initiative.id, + }); + + const result = await agentRepo.findWaitingWithContext(); + expect(result).toHaveLength(1); + expect(result[0].taskName).toBe('Implement feature'); + expect(result[0].phaseName).toBe('Phase 1'); + expect(result[0].initiativeName).toBe('My Initiative'); + expect(result[0].taskDescription).toBe('Write the feature code'); + }); + + it('returns null for context fields when agent has no taskId or initiativeId', async () => { + await agentRepo.create({ name: 'bare-agent', worktreeId: 'wt4', status: 'waiting_for_input' }); + + const result = await agentRepo.findWaitingWithContext(); + expect(result).toHaveLength(1); + expect(result[0].taskName).toBeNull(); + expect(result[0].phaseName).toBeNull(); + expect(result[0].initiativeName).toBeNull(); + expect(result[0].taskDescription).toBeNull(); + }); + + it('returns null phaseName when task has no phaseId', async () => { + const initiative = await initiativeRepo.create({ name: 'Orphan Init' }); + const task = await taskRepo.create({ + phaseId: null, + name: 'Orphan Task', + description: null, + }); + + await agentRepo.create({ + name: 'orphan-agent', + worktreeId: 'wt5', + status: 'waiting_for_input', + taskId: task.id, + initiativeId: initiative.id, + }); + + const result = await agentRepo.findWaitingWithContext(); + expect(result).toHaveLength(1); + expect(result[0].phaseName).toBeNull(); + expect(result[0].taskName).toBe('Orphan Task'); + expect(result[0].initiativeName).toBe('Orphan Init'); + }); +}); diff --git a/apps/server/db/repositories/drizzle/agent.ts b/apps/server/db/repositories/drizzle/agent.ts index c8f82fd..c5f9038 100644 --- a/apps/server/db/repositories/drizzle/agent.ts +++ b/apps/server/db/repositories/drizzle/agent.ts @@ -4,13 +4,14 @@ * Implements AgentRepository interface using Drizzle ORM. */ -import { eq } from 'drizzle-orm'; +import { eq, getTableColumns } from 'drizzle-orm'; import { nanoid } from 'nanoid'; import type { DrizzleDatabase } from '../../index.js'; -import { agents, type Agent } from '../../schema.js'; +import { agents, tasks, phases, initiatives, type Agent } from '../../schema.js'; import type { AgentRepository, AgentStatus, + AgentWithContext, CreateAgentData, UpdateAgentData, } from '../agent-repository.js'; @@ -116,4 +117,20 @@ export class DrizzleAgentRepository implements AgentRepository { throw new Error(`Agent not found: ${id}`); } } + + async findWaitingWithContext(): Promise { + return this.db + .select({ + ...getTableColumns(agents), + taskName: tasks.name, + phaseName: phases.name, + initiativeName: initiatives.name, + taskDescription: tasks.description, + }) + .from(agents) + .where(eq(agents.status, 'waiting_for_input')) + .leftJoin(tasks, eq(agents.taskId, tasks.id)) + .leftJoin(phases, eq(tasks.phaseId, phases.id)) + .leftJoin(initiatives, eq(agents.initiativeId, initiatives.id)); + } } diff --git a/apps/server/server/trpc-adapter.ts b/apps/server/server/trpc-adapter.ts index cc80176..5f565af 100644 --- a/apps/server/server/trpc-adapter.ts +++ b/apps/server/server/trpc-adapter.ts @@ -23,6 +23,7 @@ import type { ConversationRepository } from '../db/repositories/conversation-rep import type { ChatSessionRepository } from '../db/repositories/chat-session-repository.js'; import type { ReviewCommentRepository } from '../db/repositories/review-comment-repository.js'; import type { ErrandRepository } from '../db/repositories/errand-repository.js'; +import type { AgentRepository } from '../db/repositories/agent-repository.js'; import type { AccountCredentialManager } from '../agent/credentials/types.js'; import type { DispatchManager, PhaseDispatchManager } from '../dispatch/types.js'; import type { CoordinationManager } from '../coordination/types.js'; @@ -85,6 +86,8 @@ export interface TrpcAdapterOptions { projectSyncManager?: ProjectSyncManager; /** Errand repository for errand CRUD operations */ errandRepository?: ErrandRepository; + /** Agent repository for enriched agent queries */ + agentRepository?: AgentRepository; /** Absolute path to the workspace root (.cwrc directory) */ workspaceRoot?: string; } @@ -170,6 +173,7 @@ export function createTrpcHandler(options: TrpcAdapterOptions) { reviewCommentRepository: options.reviewCommentRepository, projectSyncManager: options.projectSyncManager, errandRepository: options.errandRepository, + agentRepository: options.agentRepository, workspaceRoot: options.workspaceRoot, }), }); diff --git a/apps/server/test/integration/crash-race-condition.test.ts b/apps/server/test/integration/crash-race-condition.test.ts index 4af02a1..e0790fc 100644 --- a/apps/server/test/integration/crash-race-condition.test.ts +++ b/apps/server/test/integration/crash-race-condition.test.ts @@ -96,7 +96,8 @@ describe('Crash marking race condition', () => { async findByTaskId() { throw new Error('Not implemented'); }, async findByName() { throw new Error('Not implemented'); }, async findBySessionId() { throw new Error('Not implemented'); }, - async delete() { throw new Error('Not implemented'); } + async delete() { throw new Error('Not implemented'); }, + async findWaitingWithContext() { throw new Error('Not implemented'); } }; outputHandler = new OutputHandler(mockRepo); diff --git a/apps/server/trpc/context.ts b/apps/server/trpc/context.ts index 3c259c3..9e27b57 100644 --- a/apps/server/trpc/context.ts +++ b/apps/server/trpc/context.ts @@ -20,6 +20,7 @@ import type { ConversationRepository } from '../db/repositories/conversation-rep import type { ChatSessionRepository } from '../db/repositories/chat-session-repository.js'; import type { ReviewCommentRepository } from '../db/repositories/review-comment-repository.js'; import type { ErrandRepository } from '../db/repositories/errand-repository.js'; +import type { AgentRepository } from '../db/repositories/agent-repository.js'; import type { AccountCredentialManager } from '../agent/credentials/types.js'; import type { DispatchManager, PhaseDispatchManager } from '../dispatch/types.js'; import type { CoordinationManager } from '../coordination/types.js'; @@ -87,6 +88,8 @@ export interface TRPCContext { projectSyncManager?: ProjectSyncManager; /** Absolute path to the workspace root (.cwrc directory) */ workspaceRoot?: string; + /** Agent repository for enriched queries (e.g., findWaitingWithContext) */ + agentRepository?: AgentRepository; } /** @@ -119,6 +122,7 @@ export interface CreateContextOptions { errandRepository?: ErrandRepository; projectSyncManager?: ProjectSyncManager; workspaceRoot?: string; + agentRepository?: AgentRepository; } /** @@ -155,5 +159,6 @@ export function createContext(options: CreateContextOptions): TRPCContext { errandRepository: options.errandRepository, projectSyncManager: options.projectSyncManager, workspaceRoot: options.workspaceRoot, + agentRepository: options.agentRepository, }; } diff --git a/apps/server/trpc/routers/_helpers.ts b/apps/server/trpc/routers/_helpers.ts index 928aac4..e4e097f 100644 --- a/apps/server/trpc/routers/_helpers.ts +++ b/apps/server/trpc/routers/_helpers.ts @@ -20,6 +20,7 @@ import type { ConversationRepository } from '../../db/repositories/conversation- import type { ChatSessionRepository } from '../../db/repositories/chat-session-repository.js'; import type { ReviewCommentRepository } from '../../db/repositories/review-comment-repository.js'; import type { ErrandRepository } from '../../db/repositories/errand-repository.js'; +import type { AgentRepository } from '../../db/repositories/agent-repository.js'; import type { DispatchManager, PhaseDispatchManager } from '../../dispatch/types.js'; import type { CoordinationManager } from '../../coordination/types.js'; import type { BranchManager } from '../../git/branch-manager.js'; @@ -236,3 +237,13 @@ export function requireErrandRepository(ctx: TRPCContext): ErrandRepository { } return ctx.errandRepository; } + +export function requireAgentRepository(ctx: TRPCContext): AgentRepository { + if (!ctx.agentRepository) { + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: 'Agent repository not available', + }); + } + return ctx.agentRepository; +} diff --git a/apps/server/trpc/routers/agent.ts b/apps/server/trpc/routers/agent.ts index 82d397d..268751a 100644 --- a/apps/server/trpc/routers/agent.ts +++ b/apps/server/trpc/routers/agent.ts @@ -11,7 +11,7 @@ import type { ProcedureBuilder } from '../trpc.js'; import type { TRPCContext } from '../context.js'; import type { AgentInfo, AgentResult, PendingQuestions } from '../../agent/types.js'; import type { AgentOutputEvent } from '../../events/types.js'; -import { requireAgentManager, requireLogChunkRepository, requireTaskRepository, requireInitiativeRepository, requireConversationRepository } from './_helpers.js'; +import { requireAgentManager, requireAgentRepository, requireLogChunkRepository, requireTaskRepository, requireInitiativeRepository, requireConversationRepository } from './_helpers.js'; export type AgentRadarRow = { id: string; @@ -191,9 +191,8 @@ export function agentProcedures(publicProcedure: ProcedureBuilder) { listWaitingAgents: publicProcedure .query(async ({ ctx }) => { - const agentManager = requireAgentManager(ctx); - const allAgents = await agentManager.list(); - return allAgents.filter(agent => agent.status === 'waiting_for_input'); + const agentRepo = requireAgentRepository(ctx); + return agentRepo.findWaitingWithContext(); }), getActiveRefineAgent: publicProcedure diff --git a/apps/web/src/components/InboxDetailPanel.test.tsx b/apps/web/src/components/InboxDetailPanel.test.tsx new file mode 100644 index 0000000..639a227 --- /dev/null +++ b/apps/web/src/components/InboxDetailPanel.test.tsx @@ -0,0 +1,262 @@ +// @vitest-environment happy-dom +import '@testing-library/jest-dom/vitest' +import { render, screen, fireEvent } from '@testing-library/react' +import { vi, describe, it, expect, beforeEach } from 'vitest' +import { InboxDetailPanel } from './InboxDetailPanel' + +// Mock trpc to control getAgentOutput +vi.mock('@/lib/trpc', () => ({ + trpc: { + getAgentOutput: { + useQuery: vi.fn(() => ({ data: [], isLoading: false })), + }, + }, +})) + +// Mock @tanstack/react-router's Link +vi.mock('@tanstack/react-router', () => ({ + Link: ({ children, params }: { children: React.ReactNode; params?: Record }) => ( + {children} + ), +})) + +// Mock AgentOutputViewer +vi.mock('./AgentOutputViewer', () => ({ + AgentOutputViewer: () =>
, +})) + +function makeAgent(overrides?: Partial>): { + id: string + name: string + status: string + taskId: string | null + taskName: string | null + phaseName: string | null + initiativeName: string | null + initiativeId: string | null + updatedAt: string +} { + return { + id: 'agent-1', + name: 'Test Agent', + status: 'waiting_for_input', + taskId: 'task-1', + taskName: 'Implement auth', + phaseName: 'Phase 1', + initiativeName: 'My Initiative', + initiativeId: 'init-1', + updatedAt: new Date().toISOString(), + ...overrides, + } +} + +function renderPanel( + agentOverrides?: Parameters[0], + propOverrides?: { + taskDescription?: string | null + isLoadingContext?: boolean + questions?: any[] + isLoadingQuestions?: boolean + message?: { id: string; content: string; requiresResponse: boolean } | null + } +) { + const agent = makeAgent(agentOverrides) + const props = { + taskDescription: null as string | null, + isLoadingContext: false, + questions: [] as any[], + isLoadingQuestions: false, + message: null as { id: string; content: string; requiresResponse: boolean } | null, + questionsError: null, + onBack: vi.fn(), + onSubmitAnswers: vi.fn(), + onDismissQuestions: vi.fn(), + onDismissMessage: vi.fn(), + isSubmitting: false, + isDismissingQuestions: false, + isDismissingMessage: false, + submitError: null, + dismissMessageError: null, + ...propOverrides, + } + return render() +} + +import * as trpcModule from '@/lib/trpc' + +describe('InboxDetailPanel', () => { + beforeEach(() => { + vi.clearAllMocks() + // Default: empty output + vi.mocked(trpcModule.trpc.getAgentOutput.useQuery).mockReturnValue({ + data: [], + isLoading: false, + } as any) + }) + + // ------------------------- + // Task name header tests + // ------------------------- + + it('task name header — shows taskName when set', () => { + renderPanel({ taskName: 'Implement auth', taskId: 'some-uuid-1234' }) + + expect(screen.getByText(/Task: Implement auth/)).toBeInTheDocument() + expect(screen.queryByText(/some-uuid-1234/)).not.toBeInTheDocument() + }) + + it('task name header — falls back to taskId when taskName is null', () => { + renderPanel({ taskName: null, taskId: 'abc-uuid' }) + + expect(screen.getByText(/Task:.*abc-uuid/)).toBeInTheDocument() + }) + + it('task name header — shows em dash when both are null', () => { + renderPanel({ taskName: null, taskId: null }) + + expect(screen.getByText(/Task:.*—/)).toBeInTheDocument() + }) + + // ------------------------- + // Context panel tests + // ------------------------- + + it('context panel — loaded, with task, shows all fields', () => { + renderPanel( + { + taskId: 'task-1', + taskName: 'Impl auth', + initiativeName: 'My Initiative', + initiativeId: 'init-1', + phaseName: 'Phase 1', + }, + { + taskDescription: 'Some description', + isLoadingContext: false, + } + ) + + expect(screen.getByText('My Initiative')).toBeInTheDocument() + // Task name in context panel + expect(screen.getAllByText(/Impl auth/).length).toBeGreaterThan(0) + expect(screen.getByText('Some description')).toBeInTheDocument() + expect(screen.getByText(/Phase 1/)).toBeInTheDocument() + }) + + it('context panel — truncates description longer than 300 chars', () => { + const longDescription = 'A'.repeat(301) + renderPanel( + { taskId: 'task-1' }, + { taskDescription: longDescription, isLoadingContext: false } + ) + + const truncated = screen.getByText(/A+…/) + const text = truncated.textContent ?? '' + expect(text.endsWith('…')).toBe(true) + expect(text.length).toBeLessThanOrEqual(301) + }) + + it('context panel — strips HTML tags from description', () => { + renderPanel( + { taskId: 'task-1' }, + { taskDescription: '

Some text

', isLoadingContext: false } + ) + + expect(screen.getByText('Some text')).toBeInTheDocument() + //

should not appear as literal text + expect(screen.queryByText(/

/)).not.toBeInTheDocument() + // The rendered content should not contain raw HTML tags + const container = document.querySelector('body')! + expect(container.innerHTML).not.toContain('<p>') + }) + + it('context panel — shows "No task context available" when taskId is null', () => { + renderPanel({ taskId: null }, { isLoadingContext: false }) + + expect(screen.getByText('No task context available')).toBeInTheDocument() + }) + + it('context panel — shows skeleton when loading', () => { + renderPanel({ taskId: 'task-1' }, { isLoadingContext: true }) + + // Skeletons are rendered by class + const skeletons = document.querySelectorAll('[class*="animate-pulse"]') + expect(skeletons.length).toBeGreaterThan(0) + + // Context content NOT shown + expect(screen.queryByText('No task context available')).not.toBeInTheDocument() + expect(screen.queryByText(/Initiative:/)).not.toBeInTheDocument() + }) + + // ------------------------- + // Logs section tests + // ------------------------- + + it('logs section — hidden when output is empty', () => { + vi.mocked(trpcModule.trpc.getAgentOutput.useQuery).mockReturnValue({ + data: [], + isLoading: false, + } as any) + + renderPanel() + + expect(screen.queryByText('Show agent logs')).not.toBeInTheDocument() + }) + + it('logs section — shows button and badge when chunks exist, clicking shows viewer', () => { + vi.mocked(trpcModule.trpc.getAgentOutput.useQuery).mockReturnValue({ + data: [{}, {}, {}], + isLoading: false, + } as any) + + renderPanel() + + const button = screen.getByText('Show agent logs') + expect(button).toBeInTheDocument() + expect(screen.getByText('3')).toBeInTheDocument() + + fireEvent.click(button) + + expect(screen.getByTestId('agent-output-viewer')).toBeInTheDocument() + }) + + it('logs section — collapses when agent id changes', () => { + vi.mocked(trpcModule.trpc.getAgentOutput.useQuery).mockReturnValue({ + data: [{}, {}, {}], + isLoading: false, + } as any) + + const { rerender } = renderPanel({ id: 'a' }) + + // Open logs + fireEvent.click(screen.getByText('Show agent logs')) + expect(screen.getByTestId('agent-output-viewer')).toBeInTheDocument() + + // Re-render with different agent id + const newAgent = makeAgent({ id: 'b' }) + rerender( + + ) + + // Logs should be collapsed + expect(screen.queryByTestId('agent-output-viewer')).not.toBeInTheDocument() + expect(screen.getByText('Show agent logs')).toBeInTheDocument() + }) +}) diff --git a/apps/web/src/components/InboxDetailPanel.tsx b/apps/web/src/components/InboxDetailPanel.tsx index 85d1666..0aafb2c 100644 --- a/apps/web/src/components/InboxDetailPanel.tsx +++ b/apps/web/src/components/InboxDetailPanel.tsx @@ -1,8 +1,18 @@ import { Link } from "@tanstack/react-router"; import { ChevronLeft } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; +import { Skeleton } from "@/components/ui/skeleton"; import { QuestionForm } from "@/components/QuestionForm"; +import { AgentOutputViewer } from "@/components/AgentOutputViewer"; import { formatRelativeTime } from "@/lib/utils"; +import { trpc } from "@/lib/trpc"; + +function processDescription(content: string | null): string | null { + if (!content) return null; + const stripped = content.replace(/<[^>]*>/g, ''); + return stripped.length > 300 ? stripped.slice(0, 300) + '…' : stripped; +} interface InboxDetailPanelProps { agent: { @@ -10,8 +20,14 @@ interface InboxDetailPanelProps { name: string; status: string; taskId: string | null; + taskName: string | null; + phaseName: string | null; + initiativeName: string | null; + initiativeId: string | null; updatedAt: string; }; + taskDescription: string | null; + isLoadingContext: boolean; message: { id: string; content: string; @@ -40,6 +56,8 @@ interface InboxDetailPanelProps { export function InboxDetailPanel({ agent, + taskDescription, + isLoadingContext, message, questions, isLoadingQuestions, @@ -54,6 +72,22 @@ export function InboxDetailPanel({ submitError, dismissMessageError, }: InboxDetailPanelProps) { + const [logsOpen, setLogsOpen] = useState(false); + const logsContainerRef = useRef(null); + const outputQuery = trpc.getAgentOutput.useQuery({ id: agent.id }); + + useEffect(() => { + setLogsOpen(false); + }, [agent.id]); + + useEffect(() => { + if (logsOpen && logsContainerRef.current) { + logsContainerRef.current.scrollTop = logsContainerRef.current.scrollHeight; + } + }, [logsOpen]); + + const processedDescription = processDescription(taskDescription); + return (

{/* Mobile back button */} @@ -82,16 +116,7 @@ export function InboxDetailPanel({

Task:{" "} - {agent.taskId ? ( - - {agent.taskId} - - ) : ( - "\u2014" - )} + {agent.taskName ?? agent.taskId ?? "—"}

{agent.taskId && ( + {/* Related Context Panel */} + {isLoadingContext ? ( +
+ + + +
+ ) : agent.taskId ? ( +
+ {agent.initiativeName && agent.initiativeId && ( +

+ Initiative:{" "} + + {agent.initiativeName} + +

+ )} + {agent.taskName && ( +

Task: {agent.taskName}

+ )} + {processedDescription && ( +

{processedDescription}

+ )} + {agent.phaseName &&

Phase: {agent.phaseName}

} +
+ ) : ( +

No task context available

+ )} + {/* Question Form or Notification Content */} {isLoadingQuestions && (
@@ -166,6 +224,31 @@ export function InboxDetailPanel({

)} + + {/* Agent Logs Section */} + {!logsOpen && (outputQuery.data?.length ?? 0) > 0 && ( +
+ +
+ )} + + {logsOpen && ( +
+ +
+ )}
); } diff --git a/apps/web/src/routes/hq.tsx b/apps/web/src/routes/hq.tsx index 368e6bf..8954b35 100644 --- a/apps/web/src/routes/hq.tsx +++ b/apps/web/src/routes/hq.tsx @@ -219,8 +219,14 @@ export function HeadquartersPage() { name: selectedAgent.name, status: selectedAgent.status, taskId: selectedAgent.taskId ?? null, + taskName: selectedAgent.taskName ?? null, + phaseName: selectedAgent.phaseName ?? null, + initiativeName: selectedAgent.initiativeName ?? null, + initiativeId: selectedAgent.initiativeId ?? null, updatedAt: String(selectedAgent.updatedAt), }} + isLoadingContext={agentsQuery.isLoading} + taskDescription={selectedAgent.taskDescription ?? null} message={ selectedMessage ? { diff --git a/docs/database.md b/docs/database.md index f877251..49c05ca 100644 --- a/docs/database.md +++ b/docs/database.md @@ -241,7 +241,7 @@ Index: `(phaseId)`. | InitiativeRepository | create, findById, findAll, findByStatus, update, delete | | PhaseRepository | + createDependency, getDependencies, getDependents, findByInitiativeId | | TaskRepository | + findByParentTaskId, findByPhaseId, createDependency | -| AgentRepository | + findByName, findByTaskId, findBySessionId, findByStatus | +| AgentRepository | + findByName, findByTaskId, findBySessionId, findByStatus, findWaitingWithContext (LEFT JOIN enriched) | | MessageRepository | + findPendingForUser, findRequiringResponse, findReplies | | PageRepository | + findRootPage, getOrCreateRootPage, findByParentPageId | | ProjectRepository | + junction ops: setInitiativeProjects (diff-based), findProjectsByInitiativeId | diff --git a/docs/server-api.md b/docs/server-api.md index 42e31b5..a32640c 100644 --- a/docs/server-api.md +++ b/docs/server-api.md @@ -68,7 +68,7 @@ Each procedure uses `require*Repository(ctx)` helpers that throw `TRPCError(INTE | getAgentPrompt | query | Assembled prompt — reads from DB (`agents.prompt`) first; falls back to `.cw/agent-logs//PROMPT.md` for pre-persistence agents (1 MB cap) | | getActiveRefineAgent | query | Active refine agent for initiative | | getActiveConflictAgent | query | Active conflict resolution agent for initiative (name starts with `conflict-`) | -| listWaitingAgents | query | Agents waiting for input | +| listWaitingAgents | query | Agents waiting for input — returns `AgentWithContext[]` enriched with `taskName`, `phaseName`, `initiativeName`, `taskDescription` via SQL LEFT JOINs | | listForRadar | query | Radar page: per-agent metrics (questionsCount, messagesCount, subagentsCount, compactionsCount) with time/status/mode/initiative filters | | getCompactionEvents | query | Compaction events for one agent: `{agentId}` → `{timestamp, sessionNumber}[]` (cap 200) | | getSubagentSpawns | query | Subagent spawn events for one agent: `{agentId}` → `{timestamp, description, promptPreview, fullPrompt}[]` (cap 200) |