Spaces:
Running
Running
Commit Β·
472f63c
1
Parent(s): 0b3071f
feat: beautiful research sub-agent UI with inline progress steps
Browse files- ToolCallGroup: renders research sub-steps inline below the tool row
with contextual icons (π finding examples, π reading file, π docs)
and a spinner on the active step
- ActivityStatusBar: shows current research step in the shimmer bar
("Exploring docs(trl)..." instead of generic "Running research...")
- Store: tracks researchSteps per-session, cleared on each new research call
- useAgentChat: routes research tool_log events to both the step list
and activity status
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
frontend/src/components/Chat/ActivityStatusBar.tsx
CHANGED
|
@@ -17,6 +17,7 @@ const TOOL_LABELS: Record<string, string> = {
|
|
| 17 |
hf_inspect_dataset: 'Inspecting dataset',
|
| 18 |
hf_search: 'Searching',
|
| 19 |
plan_tool: 'Planning',
|
|
|
|
| 20 |
};
|
| 21 |
|
| 22 |
function statusLabel(status: ActivityStatus): string {
|
|
|
|
| 17 |
hf_inspect_dataset: 'Inspecting dataset',
|
| 18 |
hf_search: 'Searching',
|
| 19 |
plan_tool: 'Planning',
|
| 20 |
+
research: 'Researching',
|
| 21 |
};
|
| 22 |
|
| 23 |
function statusLabel(status: ActivityStatus): string {
|
frontend/src/components/Chat/ToolCallGroup.tsx
CHANGED
|
@@ -31,6 +31,74 @@ interface ToolCallGroupProps {
|
|
| 31 |
approveTools: (approvals: Array<{ tool_call_id: string; approved: boolean; feedback?: string | null; edited_script?: string | null }>) => Promise<boolean>;
|
| 32 |
}
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
// ---------------------------------------------------------------------------
|
| 35 |
// Hardware pricing ($/hr) β from HF Spaces & Jobs pricing
|
| 36 |
// ---------------------------------------------------------------------------
|
|
@@ -327,6 +395,10 @@ function InlineApproval({
|
|
| 327 |
|
| 328 |
export default function ToolCallGroup({ tools, approveTools }: ToolCallGroupProps) {
|
| 329 |
const { setPanel, lockPanel, getJobUrl, getEditedScript } = useAgentStore();
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
const { setRightPanelOpen, setLeftSidebarOpen } = useLayoutStore();
|
| 331 |
|
| 332 |
// ββ Batch approval state ββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -367,6 +439,12 @@ export default function ToolCallGroup({ tools, approveTools }: ToolCallGroupProp
|
|
| 367 |
displayMap[id] = 'hf_jobs';
|
| 368 |
}
|
| 369 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 370 |
return { scriptLabelMap: scriptMap, toolDisplayMap: displayMap };
|
| 371 |
}, [tools]);
|
| 372 |
|
|
@@ -704,6 +782,13 @@ export default function ToolCallGroup({ tools, approveTools }: ToolCallGroupProp
|
|
| 704 |
)}
|
| 705 |
</Stack>
|
| 706 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 707 |
|
| 708 |
{/* Per-tool approval: undecided */}
|
| 709 |
{isPending && !localDecision && !isSubmitting && (
|
|
|
|
| 31 |
approveTools: (approvals: Array<{ tool_call_id: string; approved: boolean; feedback?: string | null; edited_script?: string | null }>) => Promise<boolean>;
|
| 32 |
}
|
| 33 |
|
| 34 |
+
// ---------------------------------------------------------------------------
|
| 35 |
+
// Research sub-steps (inline under the research tool row)
|
| 36 |
+
// ---------------------------------------------------------------------------
|
| 37 |
+
|
| 38 |
+
/** Pretty labels for research sub-agent tool calls */
|
| 39 |
+
function formatResearchStep(step: string): { icon: string; label: string } {
|
| 40 |
+
if (step === 'Starting research sub-agent...') return { icon: 'π', label: 'Starting research' };
|
| 41 |
+
if (step === 'Research complete.') return { icon: 'β', label: 'Research complete' };
|
| 42 |
+
if (step.startsWith('github_find_examples')) return { icon: 'π', label: step.replace('github_find_examples', 'Finding examples') };
|
| 43 |
+
if (step.startsWith('github_read_file')) {
|
| 44 |
+
const path = step.match(/\(([^)]+)\)/)?.[1] || '';
|
| 45 |
+
const filename = path.split('/').pop() || path;
|
| 46 |
+
return { icon: 'π', label: `Reading ${filename}` };
|
| 47 |
+
}
|
| 48 |
+
if (step.startsWith('explore_hf_docs')) return { icon: 'π', label: step.replace('explore_hf_docs', 'Exploring docs') };
|
| 49 |
+
if (step.startsWith('fetch_hf_docs')) return { icon: 'π', label: step.replace('fetch_hf_docs', 'Fetching docs') };
|
| 50 |
+
if (step.startsWith('hf_inspect_dataset')) return { icon: 'ποΈ', label: step.replace('hf_inspect_dataset', 'Inspecting dataset') };
|
| 51 |
+
if (step.startsWith('hf_papers')) return { icon: 'π', label: 'Searching papers' };
|
| 52 |
+
if (step.startsWith('find_hf_api')) return { icon: 'π', label: 'Finding API endpoints' };
|
| 53 |
+
if (step.startsWith('hf_repo_files')) return { icon: 'π', label: 'Reading repo files' };
|
| 54 |
+
return { icon: 'β', label: step };
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
function ResearchSteps({ steps, isRunning }: { steps: string[]; isRunning: boolean }) {
|
| 58 |
+
// Filter out the "Starting..." and "complete" meta-steps for the list
|
| 59 |
+
const toolSteps = steps.filter(
|
| 60 |
+
s => s !== 'Starting research sub-agent...' && s !== 'Research complete.',
|
| 61 |
+
);
|
| 62 |
+
if (toolSteps.length === 0) return null;
|
| 63 |
+
|
| 64 |
+
return (
|
| 65 |
+
<Box sx={{ pl: 4.5, pr: 1.5, pb: 1, pt: 0.25 }}>
|
| 66 |
+
{toolSteps.map((step, i) => {
|
| 67 |
+
const { icon, label } = formatResearchStep(step);
|
| 68 |
+
const isLast = i === toolSteps.length - 1;
|
| 69 |
+
return (
|
| 70 |
+
<Stack
|
| 71 |
+
key={i}
|
| 72 |
+
direction="row"
|
| 73 |
+
alignItems="center"
|
| 74 |
+
spacing={0.75}
|
| 75 |
+
sx={{ py: 0.2 }}
|
| 76 |
+
>
|
| 77 |
+
<Typography sx={{ fontSize: '0.65rem', lineHeight: 1, width: 14, textAlign: 'center', flexShrink: 0 }}>
|
| 78 |
+
{isLast && isRunning ? '' : icon}
|
| 79 |
+
</Typography>
|
| 80 |
+
{isLast && isRunning && (
|
| 81 |
+
<CircularProgress size={10} thickness={5} sx={{ color: 'var(--accent-yellow)', flexShrink: 0 }} />
|
| 82 |
+
)}
|
| 83 |
+
<Typography
|
| 84 |
+
sx={{
|
| 85 |
+
fontFamily: '"JetBrains Mono", ui-monospace, SFMono-Regular, monospace',
|
| 86 |
+
fontSize: '0.68rem',
|
| 87 |
+
color: isLast && isRunning ? 'var(--text)' : 'var(--muted-text)',
|
| 88 |
+
overflow: 'hidden',
|
| 89 |
+
textOverflow: 'ellipsis',
|
| 90 |
+
whiteSpace: 'nowrap',
|
| 91 |
+
}}
|
| 92 |
+
>
|
| 93 |
+
{label}
|
| 94 |
+
</Typography>
|
| 95 |
+
</Stack>
|
| 96 |
+
);
|
| 97 |
+
})}
|
| 98 |
+
</Box>
|
| 99 |
+
);
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
// ---------------------------------------------------------------------------
|
| 103 |
// Hardware pricing ($/hr) β from HF Spaces & Jobs pricing
|
| 104 |
// ---------------------------------------------------------------------------
|
|
|
|
| 395 |
|
| 396 |
export default function ToolCallGroup({ tools, approveTools }: ToolCallGroupProps) {
|
| 397 |
const { setPanel, lockPanel, getJobUrl, getEditedScript } = useAgentStore();
|
| 398 |
+
const researchSteps = useAgentStore(s => {
|
| 399 |
+
const activeId = s.activeSessionId;
|
| 400 |
+
return activeId ? (s.sessionStates[activeId]?.researchSteps ?? []) : [];
|
| 401 |
+
});
|
| 402 |
const { setRightPanelOpen, setLeftSidebarOpen } = useLayoutStore();
|
| 403 |
|
| 404 |
// ββ Batch approval state ββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 439 |
displayMap[id] = 'hf_jobs';
|
| 440 |
}
|
| 441 |
}
|
| 442 |
+
// Pretty name for research tool
|
| 443 |
+
for (const t of tools) {
|
| 444 |
+
if (t.toolName === 'research') {
|
| 445 |
+
displayMap[t.toolCallId] = 'research';
|
| 446 |
+
}
|
| 447 |
+
}
|
| 448 |
return { scriptLabelMap: scriptMap, toolDisplayMap: displayMap };
|
| 449 |
}, [tools]);
|
| 450 |
|
|
|
|
| 782 |
)}
|
| 783 |
</Stack>
|
| 784 |
|
| 785 |
+
{/* Research sub-agent steps */}
|
| 786 |
+
{tool.toolName === 'research' && researchSteps.length > 0 && (
|
| 787 |
+
<ResearchSteps
|
| 788 |
+
steps={researchSteps}
|
| 789 |
+
isRunning={state === 'input-streaming' || state === 'input-available'}
|
| 790 |
+
/>
|
| 791 |
+
)}
|
| 792 |
|
| 793 |
{/* Per-tool approval: undecided */}
|
| 794 |
{isPending && !localDecision && !isSubmitting && (
|
frontend/src/hooks/useAgentChat.ts
CHANGED
|
@@ -86,6 +86,17 @@ export function useAgentChat({ sessionId, isActive, onReady, onError, onSessionD
|
|
| 86 |
}
|
| 87 |
},
|
| 88 |
onToolLog: (tool: string, log: string) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
const STREAMABLE_TOOLS = new Set(['hf_jobs', 'sandbox', 'bash']);
|
| 90 |
if (!STREAMABLE_TOOLS.has(tool)) return;
|
| 91 |
|
|
@@ -221,7 +232,12 @@ export function useAgentChat({ sessionId, isActive, onReady, onError, onSessionD
|
|
| 221 |
updateSession(sessionId, { activityStatus: { type: 'streaming' } });
|
| 222 |
},
|
| 223 |
onToolRunning: (toolName: string, description?: string) => {
|
| 224 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
},
|
| 226 |
onInterrupted: () => { /* no-op β handled by stop() caller */ },
|
| 227 |
}),
|
|
|
|
| 86 |
}
|
| 87 |
},
|
| 88 |
onToolLog: (tool: string, log: string) => {
|
| 89 |
+
// Research sub-agent: accumulate steps + update activity status
|
| 90 |
+
if (tool === 'research') {
|
| 91 |
+
const sessState = useAgentStore.getState().getSessionState(sessionId);
|
| 92 |
+
const steps = [...sessState.researchSteps, log];
|
| 93 |
+
updateSession(sessionId, {
|
| 94 |
+
researchSteps: steps,
|
| 95 |
+
activityStatus: { type: 'tool', toolName: 'research', description: log },
|
| 96 |
+
});
|
| 97 |
+
return;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
const STREAMABLE_TOOLS = new Set(['hf_jobs', 'sandbox', 'bash']);
|
| 101 |
if (!STREAMABLE_TOOLS.has(tool)) return;
|
| 102 |
|
|
|
|
| 232 |
updateSession(sessionId, { activityStatus: { type: 'streaming' } });
|
| 233 |
},
|
| 234 |
onToolRunning: (toolName: string, description?: string) => {
|
| 235 |
+
const updates: Partial<import('@/store/agentStore').PerSessionState> = {
|
| 236 |
+
activityStatus: { type: 'tool', toolName, description },
|
| 237 |
+
};
|
| 238 |
+
// Clear research steps when a new research call starts
|
| 239 |
+
if (toolName === 'research') updates.researchSteps = [];
|
| 240 |
+
updateSession(sessionId, updates);
|
| 241 |
},
|
| 242 |
onInterrupted: () => { /* no-op β handled by stop() caller */ },
|
| 243 |
}),
|
frontend/src/store/agentStore.ts
CHANGED
|
@@ -60,6 +60,8 @@ export interface PerSessionState {
|
|
| 60 |
panelView: PanelView;
|
| 61 |
panelEditable: boolean;
|
| 62 |
plan: PlanItem[];
|
|
|
|
|
|
|
| 63 |
}
|
| 64 |
|
| 65 |
const defaultSessionState: PerSessionState = {
|
|
@@ -69,6 +71,7 @@ const defaultSessionState: PerSessionState = {
|
|
| 69 |
panelView: 'script',
|
| 70 |
panelEditable: false,
|
| 71 |
plan: [],
|
|
|
|
| 72 |
};
|
| 73 |
|
| 74 |
interface AgentStore {
|
|
@@ -233,6 +236,7 @@ export const useAgentStore = create<AgentStore>()((set, get) => ({
|
|
| 233 |
panelView: state.panelView,
|
| 234 |
panelEditable: state.panelEditable,
|
| 235 |
plan: state.plan,
|
|
|
|
| 236 |
};
|
| 237 |
}
|
| 238 |
|
|
|
|
| 60 |
panelView: PanelView;
|
| 61 |
panelEditable: boolean;
|
| 62 |
plan: PlanItem[];
|
| 63 |
+
/** Steps completed by the research sub-agent (tool_log events). */
|
| 64 |
+
researchSteps: string[];
|
| 65 |
}
|
| 66 |
|
| 67 |
const defaultSessionState: PerSessionState = {
|
|
|
|
| 71 |
panelView: 'script',
|
| 72 |
panelEditable: false,
|
| 73 |
plan: [],
|
| 74 |
+
researchSteps: [],
|
| 75 |
};
|
| 76 |
|
| 77 |
interface AgentStore {
|
|
|
|
| 236 |
panelView: state.panelView,
|
| 237 |
panelEditable: state.panelEditable,
|
| 238 |
plan: state.plan,
|
| 239 |
+
researchSteps: state.sessionStates[state.activeSessionId]?.researchSteps ?? [],
|
| 240 |
};
|
| 241 |
}
|
| 242 |
|