diff --git a/dashboard/src/api/memory.ts b/dashboard/src/api/memory.ts new file mode 100644 index 000000000..390aa8e9c --- /dev/null +++ b/dashboard/src/api/memory.ts @@ -0,0 +1,55 @@ +import client from './client'; + +export type MemoryLayer = 'WORKING' | 'EPISODIC' | 'SEMANTIC' | 'PROCEDURAL'; + +export type MemoryType = + | 'DECISION' + | 'CONSTRAINT' + | 'FAILURE' + | 'FIX' + | 'PREFERENCE' + | 'PROJECT_FACT' + | 'TASK_STATE' + | 'COMMAND_RESULT'; + +export interface RelevantMemoryItem { + id: string; + layer: MemoryLayer | null; + type: MemoryType | null; + title: string | null; + content: string | null; + scope: string | null; + tags: string[]; + source: string | null; + confidence: number | null; + salience: number | null; + ttlDays: number | null; + createdAt: string | null; + updatedAt: string | null; + lastAccessedAt: string | null; + references: string[]; + referenceCount: number; +} + +export interface RelevantMemoryResponse { + items: RelevantMemoryItem[]; + sessionId: string; + queryText: string; +} + +export interface RelevantMemoryParams { + sessionId: string; + query?: string; + limit?: number; +} + +export async function getRelevantMemories(params: RelevantMemoryParams): Promise { + const { data } = await client.get('/memory/relevant', { + params: { + sessionId: params.sessionId, + query: params.query, + limit: params.limit, + }, + }); + return data; +} diff --git a/dashboard/src/components/agentRun/ApprovalRequestCard.tsx b/dashboard/src/components/agentRun/ApprovalRequestCard.tsx new file mode 100644 index 000000000..686811c07 --- /dev/null +++ b/dashboard/src/components/agentRun/ApprovalRequestCard.tsx @@ -0,0 +1,36 @@ +import { FiCheck, FiX } from 'react-icons/fi'; +import type { ApprovalRequestViewModel } from './types'; +import { formatTimeOfDay } from './agentRunFormat'; + +interface ApprovalRequestCardProps { + approval: ApprovalRequestViewModel; + onApprove?: () => void; + onReject?: () => void; +} + +export default function ApprovalRequestCard({ approval, onApprove, onReject }: ApprovalRequestCardProps) { + return ( +
+
+ {approval.title} + {formatTimeOfDay(approval.createdAt)} +
+

{approval.description}

+
{approval.prompt}
+
+ {onApprove != null && ( + + )} + {onReject != null && ( + + )} +
+
+ ); +} diff --git a/dashboard/src/components/agentRun/ArtifactCard.tsx b/dashboard/src/components/agentRun/ArtifactCard.tsx new file mode 100644 index 000000000..d29e4cbd0 --- /dev/null +++ b/dashboard/src/components/agentRun/ArtifactCard.tsx @@ -0,0 +1,33 @@ +import { FiDownload, FiFileText } from 'react-icons/fi'; +import type { ArtifactViewModel } from './types'; + +interface ArtifactCardProps { + artifact: ArtifactViewModel; + onOpen?: (id: string) => void; +} + +export default function ArtifactCard({ artifact, onOpen }: ArtifactCardProps) { + return ( +
+ +
+ {artifact.name} + {artifact.description != null && ( + {artifact.description} + )} +
+ {artifact.href != null && ( + + + )} + {artifact.href == null && onOpen != null && ( + + )} +
+ ); +} diff --git a/dashboard/src/components/agentRun/HarnessChatLayout.tsx b/dashboard/src/components/agentRun/HarnessChatLayout.tsx new file mode 100644 index 000000000..024f144de --- /dev/null +++ b/dashboard/src/components/agentRun/HarnessChatLayout.tsx @@ -0,0 +1,31 @@ +import { useState, type ReactNode } from 'react'; +import TaskHeader from './TaskHeader'; +import { useChatRunSummary } from './useChatRunSummary'; + +interface HarnessChatLayoutProps { + children: ReactNode; +} + +export default function HarnessChatLayout({ children }: HarnessChatLayoutProps) { + const summary = useChatRunSummary(); + const [titleOverride, setTitleOverride] = useState(null); + const title = titleOverride ?? summary.title; + + return ( +
+
+ setTitleOverride(next)} + /> +
+
+ {children} +
+
+ ); +} diff --git a/dashboard/src/components/agentRun/IncidentCard.test.tsx b/dashboard/src/components/agentRun/IncidentCard.test.tsx new file mode 100644 index 000000000..dd73446eb --- /dev/null +++ b/dashboard/src/components/agentRun/IncidentCard.test.tsx @@ -0,0 +1,39 @@ +import { renderToStaticMarkup } from 'react-dom/server'; +import { describe, expect, it } from 'vitest'; +import IncidentCard from './IncidentCard'; +import type { IncidentViewModel } from './types'; + +const incident: IncidentViewModel = { + id: 'inc-1', + runId: 'run-1', + severity: 'error', + title: 'LLM provider is temporarily unavailable', + message: 'Your task has been saved and will retry automatically in 5 minutes.', + code: 'llm.provider.circuit.open', + createdAt: '2026-04-29T10:43:00.000Z', + retryCountdownSeconds: 299, + taskSaved: true, + actions: [ + { id: 'retry_now', label: 'Retry now', kind: 'primary' }, + { id: 'switch_model', label: 'Switch model', kind: 'secondary' }, + { id: 'continue_manually', label: 'Continue manually', kind: 'secondary' }, + { id: 'open_logs', label: 'Open logs', kind: 'secondary' }, + ], +}; + +describe('IncidentCard', () => { + it('renders the title, message and code', () => { + const html = renderToStaticMarkup(); + expect(html).toContain('LLM provider is temporarily unavailable'); + expect(html).toContain('llm.provider.circuit.open'); + expect(html).toContain('Task saved automatically'); + }); + + it('renders all recovery actions', () => { + const html = renderToStaticMarkup(); + expect(html).toContain('Retry now'); + expect(html).toContain('Switch model'); + expect(html).toContain('Continue manually'); + expect(html).toContain('Open logs'); + }); +}); diff --git a/dashboard/src/components/agentRun/IncidentCard.tsx b/dashboard/src/components/agentRun/IncidentCard.tsx new file mode 100644 index 000000000..56e49ebed --- /dev/null +++ b/dashboard/src/components/agentRun/IncidentCard.tsx @@ -0,0 +1,98 @@ +import { useEffect, useState } from 'react'; +import { FiAlertTriangle, FiInfo, FiAlertOctagon } from 'react-icons/fi'; +import type { IncidentActionViewModel, IncidentSeverity, IncidentViewModel } from './types'; +import { formatCountdown, formatTimeOfDay } from './agentRunFormat'; + +type IncidentActionHandler = (actionId: IncidentActionViewModel['id']) => void; + +interface IncidentCardProps { + incident: IncidentViewModel; + onAction?: IncidentActionHandler; +} + +const SEVERITY_CLASS: Record = { + info: 'incident-card--info', + warning: 'incident-card--warning', + error: '', + critical: '', +}; + +function SeverityIcon({ severity }: { severity: IncidentSeverity }) { + if (severity === 'info') { + return