feat(app): add tool call detail levels (#2031)

* feat(app): compact tool call sequences

* feat(app): summarize compact tool activity

* feat(app): add tool call detail levels

* fix(app): stabilize compact tool groups

* fix(app): address compact tool review feedback
This commit is contained in:
Matt Cowger
2026-07-14 18:55:06 +08:00
committed by GitHub
parent 3b078240a8
commit 81113d1582
16 changed files with 1488 additions and 13 deletions
+66 -7
View File
@@ -57,6 +57,8 @@ import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
import { ToolCallDetailsContent } from "@/components/tool-call-details";
import { QuestionFormCard } from "@/components/question-form-card";
import { ToolCallSheetProvider } from "@/components/tool-call-sheet";
import { ToolCallGroup } from "@/components/tool-call-group";
import { compactToolCallRuns } from "@/tool-calls/grouping";
import { type AgentStreamRenderModel, buildAgentStreamRenderModel } from "./model";
import { resolveStreamRenderStrategy } from "./strategy-resolver";
import { type StreamSegmentRenderers, type StreamViewportHandle } from "./strategy";
@@ -329,6 +331,7 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
const { t } = useTranslation();
const router = useRouter();
const autoExpandReasoning = useSettings((settings) => settings.autoExpandReasoning);
const toolCallDetailLevel = useSettings((settings) => settings.toolCallDetailLevel);
const viewportRef = useRef<StreamViewportHandle | null>(null);
const isMobile = useIsCompactFormFactor();
const streamRenderStrategy = useMemo(
@@ -343,6 +346,9 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
const [expandedInlineToolCallIds, setExpandedInlineToolCallIds] = useState<Set<string>>(
new Set(),
);
const [expandedToolCallGroupIds, setExpandedToolCallGroupIds] = useState<Set<string>>(
new Set(),
);
const openFileExplorerForCheckout = usePanelStore((state) => state.openFileExplorerForCheckout);
const setExplorerTabForCheckout = usePanelStore((state) => state.setExplorerTabForCheckout);
@@ -391,6 +397,7 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
useEffect(() => {
setIsNearBottom(true);
setExpandedInlineToolCallIds(new Set());
setExpandedToolCallGroupIds(new Set());
}, [agentId]);
const handleInlinePathPress = useStableEvent(
@@ -537,16 +544,25 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
}
const effectiveStreamItems = isActive ? streamItems : frozenStreamItemsRef.current;
const effectiveStreamHead = isActive ? streamHead : frozenStreamHeadRef.current;
const compactedToolCalls = useMemo(
() =>
compactToolCallRuns({
tail: effectiveStreamItems,
head: effectiveStreamHead ?? EMPTY_STREAM_HEAD,
enabled: toolCallDetailLevel !== "detailed",
}),
[effectiveStreamHead, effectiveStreamItems, toolCallDetailLevel],
);
const baseRenderModel = useMemo(() => {
return buildAgentStreamRenderModel({
agentStatus: context.status,
tail: effectiveStreamItems,
head: effectiveStreamHead ?? EMPTY_STREAM_HEAD,
tail: compactedToolCalls.tail,
head: compactedToolCalls.head,
platform: isWeb ? "web" : "native",
isMobileBreakpoint: isMobile,
});
}, [context.status, isMobile, effectiveStreamHead, effectiveStreamItems]);
}, [context.status, isMobile, compactedToolCalls.head, compactedToolCalls.tail]);
const streamLayout = useMemo(
() =>
layoutStream({
@@ -599,6 +615,18 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
[streamRenderStrategy],
);
const setToolCallGroupExpanded = useCallback((groupId: string, expanded: boolean) => {
setExpandedToolCallGroupIds((previous) => {
const next = new Set(previous);
if (expanded) {
next.add(groupId);
} else {
next.delete(groupId);
}
return next;
});
}, []);
const renderUserMessageItem = useCallback(
(layoutItem: StreamLayoutItem, item: Extract<StreamItem, { kind: "user_message" }>) => {
return (
@@ -662,8 +690,8 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
[autoExpandReasoning, setInlineDetailsExpanded],
);
const renderToolCallItem = useCallback(
(layoutItem: StreamLayoutItem, item: Extract<StreamItem, { kind: "tool_call" }>) => {
const renderSingleToolCallItem = useCallback(
(item: Extract<StreamItem, { kind: "tool_call" }>, isLastInSequence: boolean) => {
const { payload } = item;
if (payload.source === "agent") {
@@ -690,7 +718,7 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
detail={data.detail}
cwd={context.cwd}
metadata={data.metadata}
isLastInSequence={layoutItem.isLastInToolSequence}
isLastInSequence={isLastInSequence}
onOpenFilePath={handleToolCallOpenFile}
/>
);
@@ -705,7 +733,7 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
args={data.arguments}
result={data.result}
status={data.status}
isLastInSequence={layoutItem.isLastInToolSequence}
isLastInSequence={isLastInSequence}
onOpenFilePath={handleToolCallOpenFile}
/>
);
@@ -713,6 +741,37 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
[context.cwd, setInlineDetailsExpanded, handleToolCallOpenFile],
);
const renderToolCallItem = useCallback(
(layoutItem: StreamLayoutItem, item: Extract<StreamItem, { kind: "tool_call" }>) => {
const group = compactedToolCalls.groupsByHostId.get(item.id);
if (!group) {
return renderSingleToolCallItem(item, layoutItem.isLastInToolSequence);
}
const expanded = expandedToolCallGroupIds.has(group.id);
return (
<ToolCallGroup
group={group}
presentation={toolCallDetailLevel === "concise" ? "concise" : "overview"}
expanded={expanded}
onExpandedChange={setToolCallGroupExpanded}
>
{group.calls.map((call, index) => (
<React.Fragment key={call.id}>
{renderSingleToolCallItem(call, index === group.calls.length - 1)}
</React.Fragment>
))}
</ToolCallGroup>
);
},
[
compactedToolCalls.groupsByHostId,
expandedToolCallGroupIds,
renderSingleToolCallItem,
setToolCallGroupExpanded,
toolCallDetailLevel,
],
);
const renderStreamItemContent = useCallback(
(layoutItem: StreamLayoutItem) => {
const item = layoutItem.item;