akseljoonas HF Staff Claude Opus 4.6 (1M context) commited on
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
- updateSession(sessionId, { activityStatus: { type: 'tool', toolName, description } });
 
 
 
 
 
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