Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
}

window.APP_DATA = {
apiUrl: "http://localhost:5002",
apiUrl: "",
}
</script>
<script type="module" src="/src/frontend/main.tsx"></script>
Expand Down
3 changes: 0 additions & 3 deletions src/frontend/components/ChatMessagesView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -542,9 +542,6 @@ export function AIMessageRenderer({ message, pendingInterrupt, onInterruptResume
toolCall={toolCall as any}
messageId={message.id ?? ''}
index={idx}
pendingInterrupt={pendingInterrupt}
onInterruptResume={onInterruptResume}
onAlwaysAllow={onAlwaysAllow}
/>
))}

Expand Down
1 change: 0 additions & 1 deletion src/frontend/components/InterruptBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ export function InterruptBanner({ interrupt, onResume, onDismiss }: InterruptBan
if (!mcpAuth) return undefined;

const handler = (event: MessageEvent) => {
if (event.origin !== window.location.origin) return;
const data = event.data as { type?: string; mcp_name?: string } | null;
if (data?.type === 'mcp_oauth_done' && data.mcp_name === mcpAuth.mcp_name) {
void verifyAndSetReady(mcpAuth.mcp_name);
Expand Down
85 changes: 11 additions & 74 deletions src/frontend/components/SubAgentIndicator.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
import { useState, useEffect } from 'react';
import { useState } from 'react';
import {
Card,
CardBody,
CardHeader,
CardTitle,
Label,
} from '@patternfly/react-core';
import { Bot, Check, CheckCircle, ChevronDown, ChevronRight, Loader2, AlertCircle, ShieldCheck } from 'lucide-react';
import type { ToolCallWithContent, InterruptInfo } from '../types/deep-agent';
import { Bot, CheckCircle, ChevronDown, ChevronRight, Loader2, AlertCircle } from 'lucide-react';
import type { ToolCallWithContent } from '../types/deep-agent';
import { extractSubAgentName, extractDelegationText } from '../types/deep-agent';

interface SubAgentIndicatorProps {
readonly toolCall: ToolCallWithContent;
readonly messageId: string;
readonly index: number;
readonly pendingInterrupt?: InterruptInfo | null;
readonly onInterruptResume?: (decisions: Array<{ type: 'approve' | 'reject'; message?: string }>) => void;
readonly onAlwaysAllow?: (toolNames: string[]) => void;
}

type VisualStatus = 'delegating' | 'complete' | 'error';
Expand Down Expand Up @@ -44,40 +41,24 @@ const STATUS_CONFIG: Record<VisualStatus, {
error: { label: 'Error', color: 'red', icon: AlertCircle, animate: false },
};

export function SubAgentIndicator({ toolCall, messageId, index, pendingInterrupt, onInterruptResume, onAlwaysAllow }: SubAgentIndicatorProps) {
export function SubAgentIndicator({ toolCall, messageId, index }: SubAgentIndicatorProps) {
const [expanded, setExpanded] = useState(false);
const [isApproving, setIsApproving] = useState(false);
const name = extractSubAgentName(toolCall);
const delegationText = extractDelegationText(toolCall);
const status = deriveStatus(toolCall);
const config = STATUS_CONFIG[status];
const StatusIcon = config.icon;

const interruptValue = pendingInterrupt?.value;
const needsApproval = !!(
typeof interruptValue === 'object'
&& interruptValue !== null
&& 'action_requests' in interruptValue
&& interruptValue.action_requests?.some((r) => r.name === 'task' || r.name === name)
) && toolCall.content == null;

useEffect(() => {
if (needsApproval) {
setIsApproving(false);
setExpanded(true);
}
}, [needsApproval]);

return (
<div className="flex items-start gap-3">
<div
className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center ${needsApproval ? 'bg-yellow-500/15 border border-yellow-500/40' : 'bg-blue-500/15 border border-blue-500/30'}`}
className="flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center bg-blue-500/15 border border-blue-500/30"
aria-hidden="true"
>
<Bot className={`w-4 h-4 ${needsApproval ? 'text-yellow-500' : 'text-blue-500'}`} />
<Bot className="w-4 h-4 text-blue-500" />
</div>
<div className="flex-1 min-w-0">
<Card isCompact className={`shadow-card ${needsApproval ? 'border-yellow-500/60' : ''}`}>
<Card isCompact className="shadow-card">
<CardHeader
className="cursor-pointer"
onClick={() => setExpanded((v) => !v)}
Expand All @@ -96,15 +77,15 @@ export function SubAgentIndicator({ toolCall, messageId, index, pendingInterrupt
</CardTitle>
<Label
isCompact
color={needsApproval ? 'yellow' : config.color}
color={config.color}
icon={
<StatusIcon
className={`w-3 h-3 ${config.animate && !needsApproval ? 'animate-spin' : ''}`}
className={`w-3 h-3 ${config.animate ? 'animate-spin' : ''}`}
aria-hidden="true"
/>
}
>
{needsApproval ? 'Approval required' : config.label}
{config.label}
</Label>
</div>
<span aria-hidden="true">
Expand Down Expand Up @@ -144,56 +125,12 @@ export function SubAgentIndicator({ toolCall, messageId, index, pendingInterrupt
</pre>
</div>
)}
{status === 'delegating' && !needsApproval && (
{status === 'delegating' && (
<div className="flex items-center gap-2 text-xs text-muted-foreground pb-3">
<div className="w-1.5 h-1.5 rounded-full bg-blue-500 animate-pulse" />
Sub-agent is processing&hellip;
</div>
)}

{needsApproval && !isApproving && onInterruptResume && (
<div role="alert" aria-live="assertive" aria-label={`Sub-agent ${name} requires approval`} className="flex items-center gap-2 py-3 border-t border-yellow-500/30 bg-yellow-500/5 -mx-4 px-4 flex-wrap rounded-b-lg">
<button
type="button"
autoFocus
onClick={() => {
setIsApproving(true);
onInterruptResume([{ type: 'approve' }]);
}}
aria-label={`Approve sub-agent action: ${name}`}
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors"
style={{ backgroundColor: 'var(--chart-3)', color: 'var(--background)' }}
>
<Check className="w-3 h-3" aria-hidden="true" />
Approve
</button>
<button
type="button"
onClick={() => {
setIsApproving(true);
onInterruptResume([{ type: 'reject', message: 'User rejected this action.' }]);
}}
aria-label={`Reject sub-agent action: ${name}`}
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium hover:opacity-90 transition-colors"
style={{ backgroundColor: 'var(--destructive)', color: 'var(--background)' }}
>
Reject
</button>
<button
type="button"
onClick={() => {
setIsApproving(true);
onAlwaysAllow?.([name]);
onInterruptResume([{ type: 'approve' }]);
}}
aria-label={`Always allow sub-agent: ${name}`}
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border border-border bg-muted text-foreground hover:bg-muted/70 transition-colors"
>
<ShieldCheck className="w-3 h-3" aria-hidden="true" />
Always allow
</button>
</div>
)}
</CardBody>
)}
</Card>
Expand Down
36 changes: 0 additions & 36 deletions src/frontend/components/__tests__/accessibility.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -378,42 +378,6 @@ describe('SubAgentIndicator — accessibility', () => {
expect(header).toHaveAttribute('aria-expanded', 'true');
});

it('approval alert uses role="alert" when approval is needed', () => {
const interruptValue = {
action_requests: [{ name: 'data_analyst' }],
};
render(
<SubAgentIndicator
toolCall={baseToolCall}
messageId="msg-1"
index={0}
pendingInterrupt={{ value: interruptValue } as any}
onInterruptResume={() => {}}
onAlwaysAllow={() => {}}
/>,
);
const alert = document.querySelector('[role="alert"]');
expect(alert).toBeInTheDocument();
});

it('approve/reject/always-allow buttons have descriptive aria-labels', () => {
const interruptValue = {
action_requests: [{ name: 'data_analyst' }],
};
render(
<SubAgentIndicator
toolCall={baseToolCall}
messageId="msg-1"
index={0}
pendingInterrupt={{ value: interruptValue } as any}
onInterruptResume={() => {}}
onAlwaysAllow={() => {}}
/>,
);
expect(screen.getByRole('button', { name: /approve sub-agent action/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /reject sub-agent action/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /always allow sub-agent/i })).toBeInTheDocument();
});
});

// ---------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/lib/streaming/StreamingManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export class StreamingManager {
try {
const body: Record<string, unknown> = {
message: request.resume
? { decisions: request.resumeDecisions ?? [] }
? (request.resumeDecisions?.length ? { decisions: request.resumeDecisions } : request.message)
: request.message,
thread_id: request.threadId || 'default-thread',
session_id: request.threadId || 'default-session',
Expand Down
2 changes: 1 addition & 1 deletion src/server/utils/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,7 @@ export function getSettings(): UISettings {

if (explicitPath && !existsSync(configPath)) {
throw new Error(
`Config file not found: ${configPath}. Mount config/ui at /opt/app-root/src/config`,
`Config file not found: ${configPath}. Ensure a ConfigMap or volume is mounted at ${dirname(configPath)}`,
);
}

Expand Down
6 changes: 3 additions & 3 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,15 @@ export default defineConfig({
server: {
proxy: {
"/api": {
target: "http://127.0.0.1:8080",
target: "http://127.0.0.1:5003",
changeOrigin: true,
},
"/auth": {
target: "http://127.0.0.1:8080",
target: "http://127.0.0.1:5003",
changeOrigin: true,
},
"/login": {
target: "http://127.0.0.1:8080",
target: "http://127.0.0.1:5003",
changeOrigin: true,
},
},
Expand Down
Loading