feat(app): polish sidebar and tool call summaries

This commit is contained in:
Mohamed Boudra
2026-07-15 11:24:05 +02:00
parent 77f6069ec1
commit 38e4d9ad5d
5 changed files with 17 additions and 86 deletions

View File

@@ -779,7 +779,6 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
isLastInSequence={layoutItem.isLastInToolSequence}
onExpandedChange={setToolCallGroupExpanded}
cwd={context.cwd}
onOpenFilePath={handleToolCallOpenFile}
>
{expanded
? group.run.calls.map((call, index) => (
@@ -799,7 +798,6 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
projectedToolCalls.groupsByHostId,
context.cwd,
expandedToolCallGroupIds,
handleToolCallOpenFile,
isMobile,
renderSingleToolCallItem,
setToolCallGroupExpanded,

View File

@@ -519,6 +519,7 @@ function SidebarFooter({
icon={Home}
theme={theme}
/>
<SidebarHelpMenu />
<FooterIconButton
onPress={handleSettings}
testID="sidebar-settings"
@@ -527,7 +528,6 @@ function SidebarFooter({
shortcutKeys={settingsKeys}
theme={theme}
/>
<SidebarHelpMenu />
</View>
</View>
);

View File

@@ -21,21 +21,6 @@ export interface OverviewToolCallGroup {
isLoading: boolean;
}
export type OverviewHeader = { kind: "latest"; call: ToolCallRun["latest"] } | { kind: "summary" };
export function resolveOverviewHeader(
group: OverviewToolCallGroup,
expanded: boolean,
): OverviewHeader {
if (group.run.calls.length === 1) {
return { kind: "latest", call: group.run.latest };
}
if (expanded || group.run.isSealed) {
return { kind: "summary" };
}
return { kind: "latest", call: group.run.latest };
}
function isPaseoCall(name: string, normalizedName: string): boolean {
return isPaseoToolName(name) || normalizedName.startsWith(DIRECT_PASEO_TOOL_PREFIX);
}

View File

@@ -9,7 +9,7 @@ import { useToolCallSheet } from "@/components/tool-call-sheet";
import { buildToolCallPresentation } from "@/tool-calls/presentation";
import { resolveToolCallIcon } from "@/utils/tool-call-icon";
import { describeToolCall } from "../grouping";
import { resolveOverviewHeader, type OverviewSummary, type OverviewToolCallGroup } from "./model";
import { type OverviewSummary, type OverviewToolCallGroup } from "./model";
interface OverviewGroupProps {
group: OverviewToolCallGroup;
@@ -18,7 +18,6 @@ interface OverviewGroupProps {
isLastInSequence: boolean;
onExpandedChange: (groupId: string, expanded: boolean) => void;
cwd?: string;
onOpenFilePath?: (filePath: string) => void;
children: ReactNode;
}
@@ -66,17 +65,14 @@ export const OverviewToolCallGroupView = memo(function OverviewToolCallGroupView
isLastInSequence,
onExpandedChange,
cwd,
onOpenFilePath,
children,
}: OverviewGroupProps) {
const { t } = useTranslation();
const { openToolCall } = useToolCallSheet();
const scrollRef = useRef<ScrollView>(null);
const aggregateSummary = useOverviewSummary(group.summary);
const header = resolveOverviewHeader(group, expanded);
const latestCall = header.kind === "latest" ? header.call : group.run.latest;
const latest = useMemo(() => {
const descriptor = describeToolCall(latestCall);
const descriptor = describeToolCall(group.run.latest);
return {
detail: descriptor.detail,
presentation: buildToolCallPresentation({
@@ -89,16 +85,8 @@ export const OverviewToolCallGroupView = memo(function OverviewToolCallGroupView
resolveIcon: resolveToolCallIcon,
}),
};
}, [cwd, latestCall]);
const showsLatest = header.kind === "latest";
}, [cwd, group.run.latest]);
const opensSingleCallSheet = isCompact && group.run.calls.length === 1;
const openLatestFile = useMemo(() => {
const path = latest.presentation.openFilePath;
if (!showsLatest || !path || !onOpenFilePath) {
return undefined;
}
return () => onOpenFilePath(path);
}, [showsLatest, latest.presentation.openFilePath, onOpenFilePath]);
const failedSummary =
group.failedCount > 0 ? t("toolCallGroup.failed", { count: group.failedCount }) : undefined;
const scrollToLatest = useCallback(() => {
@@ -147,15 +135,14 @@ export const OverviewToolCallGroupView = memo(function OverviewToolCallGroupView
return (
<ExpandableBadge
testID="tool-call-group"
label={showsLatest ? latest.presentation.displayName : aggregateSummary}
secondaryLabel={showsLatest ? latest.presentation.summary : failedSummary}
icon={showsLatest ? latest.presentation.icon : Wrench}
label={aggregateSummary}
secondaryLabel={failedSummary}
icon={Wrench}
isLoading={group.isLoading}
isError={group.failedCount > 0}
isExpanded={opensSingleCallSheet ? false : expanded}
isLastInSequence={isLastInSequence}
onToggle={canExpand ? toggle : undefined}
onOpenFile={openLatestFile}
renderDetails={canExpand && !opensSingleCallSheet ? renderDetails : undefined}
borderlessWhenExpanded
/>

View File

@@ -7,7 +7,6 @@ import {
type PreparedToolCallHistory,
type ToolCallDetailLevel,
} from "./projection";
import { resolveOverviewHeader } from "./overview/model";
type AssistantMessageItem = Extract<StreamItem, { kind: "assistant_message" }>;
@@ -111,25 +110,6 @@ describe("tool call detail-level projection", () => {
});
});
it("shows the latest call while collapsed and the summary while expanded", () => {
const calls = [
toolCall("1", { type: "shell", command: "one" }),
toolCall("2", { type: "read", filePath: "/repo/a.ts" }, { status: "running" }),
];
const result = project({
level: "overview",
head: calls,
isTurnActive: true,
});
const group = result.groupsByHostId.get("1");
if (!group) {
throw new Error("Expected an overview group");
}
expect(resolveOverviewHeader(group, false)).toEqual({ kind: "latest", call: calls[1] });
expect(resolveOverviewHeader(group, true)).toEqual({ kind: "summary" });
});
it("keeps a parallel group loading while any call is still running", () => {
const calls = [
toolCall("1", { type: "shell", command: "slow" }, { status: "running" }),
@@ -140,16 +120,18 @@ describe("tool call detail-level projection", () => {
expect(result.groupsByHostId.get("1")?.isLoading).toBe(true);
});
it("keeps a one-call run presented as its tool call", () => {
const call = toolCall("1", { type: "shell", command: "one" });
const result = project({ level: "overview", head: [call] });
it("builds a loading aggregate for a one-call run", () => {
const call = toolCall("1", { type: "shell", command: "one" }, { status: "running" });
const result = project({ level: "overview", head: [call], isTurnActive: true });
const group = result.groupsByHostId.get(call.id);
if (!group) {
throw new Error("Expected an overview group");
}
expect(resolveOverviewHeader(group, false)).toEqual({ kind: "latest", call });
expect(resolveOverviewHeader(group, true)).toEqual({ kind: "latest", call });
expect(group).toMatchObject({
isLoading: true,
summary: { commandCount: 1 },
});
});
it("keeps an active overview group on its latest call until a visible boundary arrives", () => {
@@ -172,11 +154,6 @@ describe("tool call detail-level projection", () => {
mode: "overview",
run: { id: "1", latest: calls[3], isSealed: false },
});
expect(resolveOverviewHeader(activeGroup!, false)).toEqual({
kind: "latest",
call: calls[3],
});
const boundary = assistant("answer");
const sealed = project({
level: "overview",
@@ -189,12 +166,9 @@ describe("tool call detail-level projection", () => {
run: { latest: calls[3], isSealed: true },
summary: { editedFileCount: 1, readFileCount: 2, commandCount: 1 },
});
expect(resolveOverviewHeader(sealed.groupsByHostId.get("1")!, false)).toEqual({
kind: "summary",
});
});
it("keeps a running overview header live before the agent lifecycle catches up", () => {
it("keeps a running overview group live before the agent lifecycle catches up", () => {
const calls = ["1", "2", "3", "4"].map((id) =>
toolCall(id, { type: "shell", command: id }, { status: "running" }),
);
@@ -207,10 +181,8 @@ describe("tool call detail-level projection", () => {
expect(result.groupsByHostId.get("1")).toMatchObject({
run: { latest: calls[3], isSealed: false },
});
expect(resolveOverviewHeader(result.groupsByHostId.get("1")!, false)).toEqual({
kind: "latest",
call: calls[3],
isLoading: true,
summary: { commandCount: 4 },
});
});
@@ -239,22 +211,11 @@ describe("tool call detail-level projection", () => {
});
expect(betweenCalls.groupsByHostId.get("1")?.run.isSealed).toBe(false);
expect(resolveOverviewHeader(betweenCalls.groupsByHostId.get("1")!, false)).toEqual({
kind: "latest",
call: calls[3],
});
expect(continued.groupsByHostId.get("1")?.run).toMatchObject({
latest: nextCall,
isSealed: false,
});
expect(resolveOverviewHeader(continued.groupsByHostId.get("1")!, false)).toEqual({
kind: "latest",
call: nextCall,
});
expect(ended.groupsByHostId.get("1")?.run.isSealed).toBe(true);
expect(resolveOverviewHeader(ended.groupsByHostId.get("1")!, false)).toEqual({
kind: "summary",
});
});
it("builds overview summaries without category-specific presentation data", () => {