feat: enhance tool usage tracking and UI components across services
- Refactored TeamDataService and TeamProvisioningService to replace tool usage counts with structured tool call details, improving accuracy and clarity in tool tracking. - Introduced ToolCallMeta type for better representation of tool call metadata, including name and preview. - Updated tool summary generation to utilize new tool call details, enhancing the visibility of tool interactions in messages. - Enhanced LeadThoughtsGroupRow to aggregate tool calls for improved tooltip display, providing users with clearer insights into tool usage. - Modified UI components to accommodate changes in tool summary handling, ensuring a consistent user experience.
This commit is contained in:
parent
b0211e5e08
commit
ff5e877023
8 changed files with 197 additions and 58 deletions
|
|
@ -17,7 +17,7 @@ import {
|
||||||
import { getMemberColor } from '@shared/constants/memberColors';
|
import { getMemberColor } from '@shared/constants/memberColors';
|
||||||
import { createLogger } from '@shared/utils/logger';
|
import { createLogger } from '@shared/utils/logger';
|
||||||
import { parseNumericSuffixName } from '@shared/utils/teamMemberName';
|
import { parseNumericSuffixName } from '@shared/utils/teamMemberName';
|
||||||
import { formatToolSummaryFromMap } from '@shared/utils/toolSummary';
|
import { extractToolPreview, formatToolSummaryFromCalls } from '@shared/utils/toolSummary';
|
||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
@ -60,6 +60,7 @@ import type {
|
||||||
TeamTaskStatus,
|
TeamTaskStatus,
|
||||||
TeamTaskWithKanban,
|
TeamTaskWithKanban,
|
||||||
UpdateKanbanPatch,
|
UpdateKanbanPatch,
|
||||||
|
ToolCallMeta,
|
||||||
} from '@shared/types';
|
} from '@shared/types';
|
||||||
|
|
||||||
const logger = createLogger('Service:TeamDataService');
|
const logger = createLogger('Service:TeamDataService');
|
||||||
|
|
@ -1504,9 +1505,9 @@ export class TeamDataService {
|
||||||
const combined = stripAgentBlocks(textParts.join('\n')).trim();
|
const combined = stripAgentBlocks(textParts.join('\n')).trim();
|
||||||
if (combined.length < MIN_TEXT_LENGTH) continue;
|
if (combined.length < MIN_TEXT_LENGTH) continue;
|
||||||
|
|
||||||
// Count tool_use blocks from following lines (text and tool_use are separate in JSONL).
|
// Collect tool_use details from following lines (text and tool_use are separate in JSONL).
|
||||||
// tool_result (type=user) lines are interleaved between tool_use lines — skip them.
|
// tool_result (type=user) lines are interleaved between tool_use lines — skip them.
|
||||||
const toolCounts = new Map<string, number>();
|
const toolCallsList: ToolCallMeta[] = [];
|
||||||
const lookaheadLimit = Math.min(i + 200, lines.length);
|
const lookaheadLimit = Math.min(i + 200, lines.length);
|
||||||
for (let j = i + 1; j < lookaheadLimit; j++) {
|
for (let j = i + 1; j < lookaheadLimit; j++) {
|
||||||
const tLine = lines[j]?.trim();
|
const tLine = lines[j]?.trim();
|
||||||
|
|
@ -1525,11 +1526,16 @@ export class TeamDataService {
|
||||||
if (tBlocks.some((b) => b.type === 'text')) break; // next text = stop
|
if (tBlocks.some((b) => b.type === 'text')) break; // next text = stop
|
||||||
for (const b of tBlocks) {
|
for (const b of tBlocks) {
|
||||||
if (b.type === 'tool_use' && typeof b.name === 'string' && b.name !== 'SendMessage') {
|
if (b.type === 'tool_use' && typeof b.name === 'string' && b.name !== 'SendMessage') {
|
||||||
toolCounts.set(b.name, (toolCounts.get(b.name) ?? 0) + 1);
|
const input = (b.input ?? {}) as Record<string, unknown>;
|
||||||
|
toolCallsList.push({
|
||||||
|
name: b.name as string,
|
||||||
|
preview: extractToolPreview(b.name as string, input),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const toolSummary = formatToolSummaryFromMap(toolCounts);
|
const toolCalls = toolCallsList.length > 0 ? toolCallsList : undefined;
|
||||||
|
const toolSummary = toolCalls ? formatToolSummaryFromCalls(toolCalls) : undefined;
|
||||||
|
|
||||||
// Stable messageId: timestamp + text prefix (survives tail-scan range changes)
|
// Stable messageId: timestamp + text prefix (survives tail-scan range changes)
|
||||||
const textPrefix = combined
|
const textPrefix = combined
|
||||||
|
|
@ -1550,6 +1556,7 @@ export class TeamDataService {
|
||||||
leadSessionId: config.leadSessionId,
|
leadSessionId: config.leadSessionId,
|
||||||
messageId,
|
messageId,
|
||||||
toolSummary,
|
toolSummary,
|
||||||
|
toolCalls,
|
||||||
});
|
});
|
||||||
if (textsReversed.length >= MAX_LEAD_TEXTS) break;
|
if (textsReversed.length >= MAX_LEAD_TEXTS) break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import { getMemberColor } from '@shared/constants/memberColors';
|
||||||
import { resolveLanguageName } from '@shared/utils/agentLanguage';
|
import { resolveLanguageName } from '@shared/utils/agentLanguage';
|
||||||
import { isInboxNoiseMessage } from '@shared/utils/inboxNoise';
|
import { isInboxNoiseMessage } from '@shared/utils/inboxNoise';
|
||||||
import { createLogger } from '@shared/utils/logger';
|
import { createLogger } from '@shared/utils/logger';
|
||||||
import { formatToolSummaryFromMap } from '@shared/utils/toolSummary';
|
import { extractToolPreview, formatToolSummaryFromCalls } from '@shared/utils/toolSummary';
|
||||||
import { createCliAutoSuffixNameGuard } from '@shared/utils/teamMemberName';
|
import { createCliAutoSuffixNameGuard } from '@shared/utils/teamMemberName';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
|
|
@ -50,6 +50,7 @@ import type {
|
||||||
TeamProvisioningProgress,
|
TeamProvisioningProgress,
|
||||||
TeamProvisioningState,
|
TeamProvisioningState,
|
||||||
TeamTask,
|
TeamTask,
|
||||||
|
ToolCallMeta,
|
||||||
} from '@shared/types';
|
} from '@shared/types';
|
||||||
|
|
||||||
const logger = createLogger('Service:TeamProvisioning');
|
const logger = createLogger('Service:TeamProvisioning');
|
||||||
|
|
@ -164,8 +165,8 @@ interface ProvisioningRun {
|
||||||
} | null;
|
} | null;
|
||||||
/** Monotonic counter for individual lead assistant messages. */
|
/** Monotonic counter for individual lead assistant messages. */
|
||||||
leadMsgSeq: number;
|
leadMsgSeq: number;
|
||||||
/** Accumulated tool_use counts between text messages (tool name → count). */
|
/** Accumulated tool_use details between text messages. */
|
||||||
pendingToolCounts: Map<string, number>;
|
pendingToolCalls: ToolCallMeta[];
|
||||||
/** Throttle timestamp for emitting inbox refresh events for lead text. */
|
/** Throttle timestamp for emitting inbox refresh events for lead text. */
|
||||||
lastLeadTextEmitMs: number;
|
lastLeadTextEmitMs: number;
|
||||||
/**
|
/**
|
||||||
|
|
@ -1747,7 +1748,7 @@ export class TeamProvisioningService {
|
||||||
fsPhase: 'waiting_config',
|
fsPhase: 'waiting_config',
|
||||||
leadRelayCapture: null,
|
leadRelayCapture: null,
|
||||||
leadMsgSeq: 0,
|
leadMsgSeq: 0,
|
||||||
pendingToolCounts: new Map(),
|
pendingToolCalls: [],
|
||||||
lastLeadTextEmitMs: 0,
|
lastLeadTextEmitMs: 0,
|
||||||
silentUserDmForward: null,
|
silentUserDmForward: null,
|
||||||
silentUserDmForwardClearHandle: null,
|
silentUserDmForwardClearHandle: null,
|
||||||
|
|
@ -2047,7 +2048,7 @@ export class TeamProvisioningService {
|
||||||
fsPhase: 'waiting_members',
|
fsPhase: 'waiting_members',
|
||||||
leadRelayCapture: null,
|
leadRelayCapture: null,
|
||||||
leadMsgSeq: 0,
|
leadMsgSeq: 0,
|
||||||
pendingToolCounts: new Map(),
|
pendingToolCalls: [],
|
||||||
lastLeadTextEmitMs: 0,
|
lastLeadTextEmitMs: 0,
|
||||||
silentUserDmForward: null,
|
silentUserDmForward: null,
|
||||||
silentUserDmForwardClearHandle: null,
|
silentUserDmForwardClearHandle: null,
|
||||||
|
|
@ -2924,12 +2925,11 @@ export class TeamProvisioningService {
|
||||||
run.request.members.find((m) => m.role?.toLowerCase().includes('lead'))?.name ||
|
run.request.members.find((m) => m.role?.toLowerCase().includes('lead'))?.name ||
|
||||||
'team-lead';
|
'team-lead';
|
||||||
const messageId = `lead-turn-${run.runId}-${run.leadMsgSeq}`;
|
const messageId = `lead-turn-${run.runId}-${run.leadMsgSeq}`;
|
||||||
// Attach accumulated tool counts from preceding tool_use messages, then reset.
|
// Attach accumulated tool call details from preceding tool_use messages, then reset.
|
||||||
const toolSummary =
|
const toolCalls =
|
||||||
run.pendingToolCounts.size > 0
|
run.pendingToolCalls.length > 0 ? [...run.pendingToolCalls] : undefined;
|
||||||
? formatToolSummaryFromMap(run.pendingToolCounts)
|
const toolSummary = toolCalls ? formatToolSummaryFromCalls(toolCalls) : undefined;
|
||||||
: undefined;
|
run.pendingToolCalls = [];
|
||||||
run.pendingToolCounts.clear();
|
|
||||||
const leadMsg: InboxMessage = {
|
const leadMsg: InboxMessage = {
|
||||||
from: leadName,
|
from: leadName,
|
||||||
text: cleanText,
|
text: cleanText,
|
||||||
|
|
@ -2939,6 +2939,7 @@ export class TeamProvisioningService {
|
||||||
messageId,
|
messageId,
|
||||||
source: 'lead_process',
|
source: 'lead_process',
|
||||||
toolSummary,
|
toolSummary,
|
||||||
|
toolCalls,
|
||||||
};
|
};
|
||||||
this.pushLiveLeadProcessMessage(run.teamName, leadMsg);
|
this.pushLiveLeadProcessMessage(run.teamName, leadMsg);
|
||||||
|
|
||||||
|
|
@ -2959,8 +2960,8 @@ export class TeamProvisioningService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accumulate tool_use counts from tool-only messages (text + tool_use are separate in stream-json).
|
// Accumulate tool_use details from tool-only messages (text + tool_use are separate in stream-json).
|
||||||
// These counts will be attached to the next text message as toolSummary.
|
// These details will be attached to the next text message as toolCalls/toolSummary.
|
||||||
if (run.provisioningComplete) {
|
if (run.provisioningComplete) {
|
||||||
for (const block of content ?? []) {
|
for (const block of content ?? []) {
|
||||||
if (
|
if (
|
||||||
|
|
@ -2968,10 +2969,11 @@ export class TeamProvisioningService {
|
||||||
typeof block.name === 'string' &&
|
typeof block.name === 'string' &&
|
||||||
block.name !== 'SendMessage'
|
block.name !== 'SendMessage'
|
||||||
) {
|
) {
|
||||||
run.pendingToolCounts.set(
|
const input = (block.input ?? {}) as Record<string, unknown>;
|
||||||
block.name as string,
|
run.pendingToolCalls.push({
|
||||||
(run.pendingToolCounts.get(block.name as string) ?? 0) + 1
|
name: block.name as string,
|
||||||
);
|
preview: extractToolPreview(block.name as string, input),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,19 @@ export class TeamSentMessagesStore {
|
||||||
source: typeof row.source === 'string' ? (row.source as InboxMessage['source']) : undefined,
|
source: typeof row.source === 'string' ? (row.source as InboxMessage['source']) : undefined,
|
||||||
leadSessionId: typeof row.leadSessionId === 'string' ? row.leadSessionId : undefined,
|
leadSessionId: typeof row.leadSessionId === 'string' ? row.leadSessionId : undefined,
|
||||||
toolSummary: typeof row.toolSummary === 'string' ? row.toolSummary : undefined,
|
toolSummary: typeof row.toolSummary === 'string' ? row.toolSummary : undefined,
|
||||||
|
toolCalls: Array.isArray(row.toolCalls)
|
||||||
|
? (row.toolCalls as unknown[])
|
||||||
|
.filter(
|
||||||
|
(tc): tc is { name: string; preview?: string } =>
|
||||||
|
tc != null &&
|
||||||
|
typeof tc === 'object' &&
|
||||||
|
typeof (tc as Record<string, unknown>).name === 'string'
|
||||||
|
)
|
||||||
|
.map((tc) => ({
|
||||||
|
name: tc.name,
|
||||||
|
preview: typeof tc.preview === 'string' ? tc.preview : undefined,
|
||||||
|
}))
|
||||||
|
: undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { useCallback, useState } from 'react';
|
||||||
import { useSortable } from '@dnd-kit/sortable';
|
import { useSortable } from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import { getTeamColorSet } from '@renderer/constants/teamColors';
|
import { getTeamColorSet } from '@renderer/constants/teamColors';
|
||||||
|
import { nameColorSet } from '@renderer/utils/projectColor';
|
||||||
import { useStore } from '@renderer/store';
|
import { useStore } from '@renderer/store';
|
||||||
import {
|
import {
|
||||||
Activity,
|
Activity,
|
||||||
|
|
@ -67,17 +68,17 @@ export const SortableTab = ({
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const teamColor = useStore((s) => {
|
const teamColorSet = useStore((s) => {
|
||||||
if (tab.type !== 'team' || !tab.teamName) return null;
|
if (tab.type !== 'team' || !tab.teamName) return null;
|
||||||
const team = s.teamByName[tab.teamName];
|
const team = s.teamByName[tab.teamName];
|
||||||
if (team?.color) return team.color;
|
const explicitColor =
|
||||||
// Fallback: selectedTeamData may be available before teamByName is populated
|
team?.color ??
|
||||||
if (s.selectedTeamName === tab.teamName && s.selectedTeamData?.config.color) {
|
(s.selectedTeamName === tab.teamName ? s.selectedTeamData?.config.color : undefined);
|
||||||
return s.selectedTeamData.config.color;
|
if (explicitColor) return getTeamColorSet(explicitColor);
|
||||||
}
|
// Fallback: deterministic color derived from display name
|
||||||
return null;
|
const displayName = team?.displayName ?? tab.label;
|
||||||
|
return nameColorSet(displayName);
|
||||||
});
|
});
|
||||||
const teamColorSet = teamColor ? getTeamColorSet(teamColor) : null;
|
|
||||||
|
|
||||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||||
id: tab.id,
|
id: tab.id,
|
||||||
|
|
|
||||||
|
|
@ -561,9 +561,7 @@ export const TeamListView = (): React.JSX.Element => {
|
||||||
void fetchTeams();
|
void fetchTeams();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{teamsLoading ? (
|
{teamsLoading ? <RotateCcw className="size-3.5 animate-spin" /> : null}
|
||||||
<RotateCcw className="size-3.5 animate-spin" />
|
|
||||||
) : null}
|
|
||||||
Refresh
|
Refresh
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -671,7 +669,7 @@ export const TeamListView = (): React.JSX.Element => {
|
||||||
key={team.teamName}
|
key={team.teamName}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className={`group relative cursor-pointer overflow-hidden rounded-lg border bg-[var(--color-surface)] p-4 hover:bg-[var(--color-surface-raised)] ${
|
className={`group relative flex cursor-pointer flex-col overflow-hidden rounded-lg border bg-[var(--color-surface)] p-4 hover:bg-[var(--color-surface-raised)] ${
|
||||||
matchesCurrentProject
|
matchesCurrentProject
|
||||||
? 'border-emerald-500/70 ring-1 ring-emerald-500/30'
|
? 'border-emerald-500/70 ring-1 ring-emerald-500/30'
|
||||||
: 'border-[var(--color-border)]'
|
: 'border-[var(--color-border)]'
|
||||||
|
|
@ -695,7 +693,11 @@ export const TeamListView = (): React.JSX.Element => {
|
||||||
style={{ backgroundColor: teamColorSet.badge }}
|
style={{ backgroundColor: teamColorSet.badge }}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<div className={teamColorSet ? 'relative z-10' : undefined}>
|
<div
|
||||||
|
className={
|
||||||
|
teamColorSet ? 'relative z-10 flex flex-1 flex-col' : 'flex flex-1 flex-col'
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||||
<h3 className="truncate text-sm font-semibold text-[var(--color-text)]">
|
<h3 className="truncate text-sm font-semibold text-[var(--color-text)]">
|
||||||
|
|
@ -779,6 +781,8 @@ export const TeamListView = (): React.JSX.Element => {
|
||||||
Members: {team.memberCount}
|
Members: {team.memberCount}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="mt-auto">
|
||||||
{(() => {
|
{(() => {
|
||||||
const tc = taskCountsByTeam.get(team.teamName);
|
const tc = taskCountsByTeam.get(team.teamName);
|
||||||
const pending = tc?.pending ?? 0;
|
const pending = tc?.pending ?? 0;
|
||||||
|
|
@ -831,8 +835,8 @@ export const TeamListView = (): React.JSX.Element => {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
{renderTeamRecentPaths(team, status)}
|
||||||
</div>
|
</div>
|
||||||
{renderTeamRecentPaths(team, status)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui
|
||||||
import { useStore } from '@renderer/store';
|
import { useStore } from '@renderer/store';
|
||||||
import { formatToolSummary, parseToolSummary } from '@shared/utils/toolSummary';
|
import { formatToolSummary, parseToolSummary } from '@shared/utils/toolSummary';
|
||||||
|
|
||||||
import type { InboxMessage } from '@shared/types';
|
import type { InboxMessage, ToolCallMeta } from '@shared/types';
|
||||||
|
|
||||||
export interface LeadThoughtGroup {
|
export interface LeadThoughtGroup {
|
||||||
type: 'lead-thoughts';
|
type: 'lead-thoughts';
|
||||||
|
|
@ -102,25 +102,50 @@ function isRecentTimestamp(timestamp: string): boolean {
|
||||||
return Date.now() - t <= LIVE_WINDOW_MS;
|
return Date.now() - t <= LIVE_WINDOW_MS;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToolSummaryTooltipContent({ summary }: { summary: string }): JSX.Element {
|
function ToolSummaryTooltipContent({
|
||||||
const parsed = parseToolSummary(summary);
|
toolCalls,
|
||||||
if (!parsed) return <span>{summary}</span>;
|
toolSummary,
|
||||||
|
}: {
|
||||||
const sorted = Object.entries(parsed.byName).sort((a, b) => b[1] - a[1]);
|
toolCalls?: ToolCallMeta[];
|
||||||
|
toolSummary?: string;
|
||||||
return (
|
}): JSX.Element {
|
||||||
<div className="flex flex-col gap-0.5">
|
if (toolCalls && toolCalls.length > 0) {
|
||||||
<div className="mb-0.5 text-[10px] text-text-secondary">
|
return (
|
||||||
{parsed.total} {parsed.total === 1 ? 'tool call' : 'tool calls'}
|
<div className="flex max-h-[300px] flex-col gap-0.5 overflow-y-auto">
|
||||||
</div>
|
<div className="mb-0.5 text-[10px] text-text-secondary">
|
||||||
{sorted.map(([name, count]) => (
|
{toolCalls.length} {toolCalls.length === 1 ? 'tool call' : 'tool calls'}
|
||||||
<div key={name} className="flex justify-between gap-3">
|
|
||||||
<span>{name}</span>
|
|
||||||
<span className="text-text-secondary">{count}</span>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
{toolCalls.map((tc, i) => (
|
||||||
</div>
|
<div key={i} className="flex items-baseline gap-2">
|
||||||
);
|
<span className="shrink-0 font-semibold">{tc.name}</span>
|
||||||
|
{tc.preview && <span className="truncate text-text-secondary">{tc.preview}</span>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolSummary) {
|
||||||
|
const parsed = parseToolSummary(toolSummary);
|
||||||
|
if (parsed) {
|
||||||
|
const sorted = Object.entries(parsed.byName).sort((a, b) => b[1] - a[1]);
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
|
<div className="mb-0.5 text-[10px] text-text-secondary">
|
||||||
|
{parsed.total} {parsed.total === 1 ? 'tool call' : 'tool calls'}
|
||||||
|
</div>
|
||||||
|
{sorted.map(([name, count]) => (
|
||||||
|
<div key={name} className="flex justify-between gap-3">
|
||||||
|
<span>{name}</span>
|
||||||
|
<span className="text-text-secondary">×{count}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span>{toolSummary ?? ''}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LeadThoughtsGroupRow = ({
|
export const LeadThoughtsGroupRow = ({
|
||||||
|
|
@ -170,6 +195,15 @@ export const LeadThoughtsGroupRow = ({
|
||||||
return formatToolSummary({ total, byName: merged });
|
return formatToolSummary({ total, byName: merged });
|
||||||
}, [thoughts]);
|
}, [thoughts]);
|
||||||
|
|
||||||
|
// Aggregate all toolCalls across thoughts for header tooltip
|
||||||
|
const allToolCalls = useMemo(() => {
|
||||||
|
const calls: ToolCallMeta[] = [];
|
||||||
|
for (const t of thoughts) {
|
||||||
|
if (t.toolCalls) calls.push(...t.toolCalls);
|
||||||
|
}
|
||||||
|
return calls.length > 0 ? calls : undefined;
|
||||||
|
}, [thoughts]);
|
||||||
|
|
||||||
// Live = process alive AND (lead is in active turn OR context recently updated OR fresh thought)
|
// Live = process alive AND (lead is in active turn OR context recently updated OR fresh thought)
|
||||||
const computeIsLive = useCallback(
|
const computeIsLive = useCallback(
|
||||||
() =>
|
() =>
|
||||||
|
|
@ -273,7 +307,10 @@ export const LeadThoughtsGroupRow = ({
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="bottom" className="font-mono text-[11px]">
|
<TooltipContent side="bottom" className="font-mono text-[11px]">
|
||||||
<ToolSummaryTooltipContent summary={totalToolSummary} />
|
<ToolSummaryTooltipContent
|
||||||
|
toolCalls={allToolCalls}
|
||||||
|
toolSummary={totalToolSummary}
|
||||||
|
/>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
@ -337,8 +374,11 @@ export const LeadThoughtsGroupRow = ({
|
||||||
🔧 {thought.toolSummary}
|
🔧 {thought.toolSummary}
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="top" className="font-mono text-[11px]">
|
<TooltipContent side="top" align="start" className="font-mono text-[11px]">
|
||||||
<ToolSummaryTooltipContent summary={thought.toolSummary} />
|
<ToolSummaryTooltipContent
|
||||||
|
toolCalls={thought.toolCalls}
|
||||||
|
toolSummary={thought.toolSummary}
|
||||||
|
/>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -186,6 +186,14 @@ export interface AttachmentFileData {
|
||||||
mimeType: AttachmentMediaType;
|
mimeType: AttachmentMediaType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Lightweight metadata for a single tool call (for UI display in tooltips). */
|
||||||
|
export interface ToolCallMeta {
|
||||||
|
/** Tool name, e.g. "Read", "Bash", "Grep" */
|
||||||
|
name: string;
|
||||||
|
/** Human-readable preview extracted from input args, e.g. "index.ts", "grep -r foo" */
|
||||||
|
preview?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface InboxMessage {
|
export interface InboxMessage {
|
||||||
from: string;
|
from: string;
|
||||||
to?: string;
|
to?: string;
|
||||||
|
|
@ -201,6 +209,8 @@ export interface InboxMessage {
|
||||||
leadSessionId?: string;
|
leadSessionId?: string;
|
||||||
/** Tool usage summary from assistant message, e.g. "3 tools (2 Read, Bash)" */
|
/** Tool usage summary from assistant message, e.g. "3 tools (2 Read, Bash)" */
|
||||||
toolSummary?: string;
|
toolSummary?: string;
|
||||||
|
/** Structured tool call details for tooltip display. */
|
||||||
|
toolCalls?: ToolCallMeta[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SendMessageRequest {
|
export interface SendMessageRequest {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import type { ToolCallMeta } from '@shared/types';
|
||||||
|
|
||||||
export interface ToolSummaryData {
|
export interface ToolSummaryData {
|
||||||
total: number;
|
total: number;
|
||||||
byName: Record<string, number>;
|
byName: Record<string, number>;
|
||||||
|
|
@ -55,3 +57,63 @@ export function formatToolSummaryFromMap(counts: Map<string, number>): string |
|
||||||
.join(', ');
|
.join(', ');
|
||||||
return `${total} ${total === 1 ? 'tool' : 'tools'} (${parts})`;
|
return `${total} ${total === 1 ? 'tool' : 'tools'} (${parts})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Format tool summary from an array of ToolCallMeta. */
|
||||||
|
export function formatToolSummaryFromCalls(calls: ToolCallMeta[]): string | undefined {
|
||||||
|
if (calls.length === 0) return undefined;
|
||||||
|
const counts = new Map<string, number>();
|
||||||
|
for (const c of calls) counts.set(c.name, (counts.get(c.name) ?? 0) + 1);
|
||||||
|
return formatToolSummaryFromMap(counts);
|
||||||
|
}
|
||||||
|
|
||||||
|
function baseName(filePath: string): string {
|
||||||
|
return filePath.split(/[/\\]/).pop() ?? filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function truncateStr(str: string, max: number): string {
|
||||||
|
return str.length <= max ? str : str.slice(0, max) + '...';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Extract a short human-readable preview from tool_use input arguments. */
|
||||||
|
export function extractToolPreview(
|
||||||
|
name: string,
|
||||||
|
input: Record<string, unknown>
|
||||||
|
): string | undefined {
|
||||||
|
switch (name) {
|
||||||
|
case 'Read':
|
||||||
|
case 'Edit':
|
||||||
|
case 'Write':
|
||||||
|
return typeof input.file_path === 'string' ? baseName(input.file_path) : undefined;
|
||||||
|
case 'Bash':
|
||||||
|
return typeof input.description === 'string'
|
||||||
|
? truncateStr(input.description, 60)
|
||||||
|
: typeof input.command === 'string'
|
||||||
|
? truncateStr(input.command, 60)
|
||||||
|
: undefined;
|
||||||
|
case 'Grep':
|
||||||
|
case 'Glob':
|
||||||
|
return typeof input.pattern === 'string' ? truncateStr(input.pattern, 40) : undefined;
|
||||||
|
case 'Agent':
|
||||||
|
case 'TaskCreate':
|
||||||
|
return typeof input.prompt === 'string'
|
||||||
|
? truncateStr(input.prompt, 60)
|
||||||
|
: typeof input.description === 'string'
|
||||||
|
? truncateStr(input.description, 60)
|
||||||
|
: undefined;
|
||||||
|
case 'WebFetch':
|
||||||
|
if (typeof input.url === 'string') {
|
||||||
|
try {
|
||||||
|
return new URL(input.url).hostname;
|
||||||
|
} catch {
|
||||||
|
return truncateStr(input.url, 40);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
case 'WebSearch':
|
||||||
|
return typeof input.query === 'string' ? truncateStr(input.query, 40) : undefined;
|
||||||
|
default: {
|
||||||
|
const v = input.name ?? input.path ?? input.file ?? input.query ?? input.command;
|
||||||
|
return typeof v === 'string' ? truncateStr(v, 50) : undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue