@saadnvd1/agent-os 0.3.749 → 0.3.750
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,21 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import type { ToolItem } from "@/lib/chat/group";
|
|
4
|
-
import {
|
|
5
|
-
import { cn } from "@/lib/utils";
|
|
6
|
-
import { StatusIcon } from "./Tools";
|
|
4
|
+
import { failedSteps } from "@/lib/chat/step-summary";
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const WORD = {
|
|
11
|
-
running: "running",
|
|
12
|
-
done: "ok",
|
|
13
|
-
error: "failed",
|
|
14
|
-
stopped: "not run",
|
|
15
|
-
} as const;
|
|
16
|
-
|
|
17
|
-
// One line per collapsed group: how each step ended, so the results aren't
|
|
18
|
-
// lost behind the fold.
|
|
6
|
+
// Under a collapsed group that had failures: which steps failed. Nothing when
|
|
7
|
+
// every step passed.
|
|
19
8
|
export function StepOutcomes({
|
|
20
9
|
tools,
|
|
21
10
|
onOpen,
|
|
@@ -23,44 +12,19 @@ export function StepOutcomes({
|
|
|
23
12
|
tools: ToolItem[];
|
|
24
13
|
onOpen: () => void;
|
|
25
14
|
}) {
|
|
26
|
-
const
|
|
15
|
+
const { names, more } = failedSteps(tools);
|
|
16
|
+
if (names.length === 0) return null;
|
|
27
17
|
return (
|
|
28
18
|
<button
|
|
29
19
|
type="button"
|
|
30
20
|
onClick={onOpen}
|
|
31
21
|
aria-label="Show steps"
|
|
32
|
-
className="flex min-h-
|
|
22
|
+
className="text-destructive flex min-h-8 w-full min-w-0 items-center gap-x-3 px-2 pb-1 text-left text-xs md:min-h-0"
|
|
33
23
|
>
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
key={t.id}
|
|
39
|
-
title={`${t.title}: ${WORD[t.status]}${code ? `, ${code}` : ""}`}
|
|
40
|
-
aria-label={`${t.title}: ${WORD[t.status]}${code ? `, ${code}` : ""}`}
|
|
41
|
-
className="flex min-w-0 items-center gap-1"
|
|
42
|
-
>
|
|
43
|
-
<StatusIcon status={t.status} />
|
|
44
|
-
{code && (
|
|
45
|
-
<span
|
|
46
|
-
className={cn(
|
|
47
|
-
"font-mono text-[11px]",
|
|
48
|
-
t.status === "error"
|
|
49
|
-
? "text-destructive"
|
|
50
|
-
: "text-muted-foreground"
|
|
51
|
-
)}
|
|
52
|
-
>
|
|
53
|
-
{code}
|
|
54
|
-
</span>
|
|
55
|
-
)}
|
|
56
|
-
</span>
|
|
57
|
-
);
|
|
58
|
-
})}
|
|
59
|
-
{tools.length > shown.length && (
|
|
60
|
-
<span className="text-muted-foreground/70 font-mono text-[11px]">
|
|
61
|
-
+{tools.length - shown.length} more
|
|
62
|
-
</span>
|
|
63
|
-
)}
|
|
24
|
+
<span className="min-w-0 truncate">
|
|
25
|
+
{names.map((n) => `✕ ${n}`).join(" ")}
|
|
26
|
+
{more > 0 && ` +${more} more`}
|
|
27
|
+
</span>
|
|
64
28
|
</button>
|
|
65
29
|
);
|
|
66
30
|
}
|
|
@@ -2,7 +2,6 @@ import { createElement } from "react";
|
|
|
2
2
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
3
3
|
import { describe, expect, it } from "vitest";
|
|
4
4
|
import type { ToolItem } from "@/lib/chat/group";
|
|
5
|
-
import { MAX_CHIPS } from "./StepOutcomes";
|
|
6
5
|
import { ToolGroup } from "./Tools";
|
|
7
6
|
|
|
8
7
|
const tool = (id: string, over: Partial<ToolItem> = {}): ToolItem => ({
|
|
@@ -20,7 +19,7 @@ const render = (tools: ToolItem[]) =>
|
|
|
20
19
|
renderToStaticMarkup(createElement(ToolGroup, { tools }));
|
|
21
20
|
|
|
22
21
|
describe("collapsed ToolGroup", () => {
|
|
23
|
-
it("
|
|
22
|
+
it("names the failed steps and counts the rest", () => {
|
|
24
23
|
const html = render([
|
|
25
24
|
tool("a", { output: "HTTP/2 200 \n" }),
|
|
26
25
|
tool("b", { status: "error", output: "Exit code 7" }),
|
|
@@ -31,22 +30,20 @@ describe("collapsed ToolGroup", () => {
|
|
|
31
30
|
expect(html).toContain("1 ok");
|
|
32
31
|
expect(html).toContain("2 failed");
|
|
33
32
|
expect(html).toContain("1 not run");
|
|
34
|
-
expect(html).toContain("check
|
|
35
|
-
expect(html).toContain("check
|
|
36
|
-
expect(html).toContain("check
|
|
37
|
-
expect(html).toContain("check d: not run");
|
|
33
|
+
expect(html).toContain("✕ check b");
|
|
34
|
+
expect(html).toContain("✕ check c");
|
|
35
|
+
expect(html).not.toContain("check a");
|
|
38
36
|
});
|
|
39
37
|
|
|
40
|
-
it("
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
);
|
|
38
|
+
it("has no outcome row when every step passed", () => {
|
|
39
|
+
const html = render([tool("a"), tool("b")]);
|
|
40
|
+
expect(html).toContain("2 steps · ran 2 commands");
|
|
41
|
+
expect(html).not.toContain("Show steps");
|
|
44
42
|
});
|
|
45
43
|
|
|
46
|
-
it("
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
it("shows no outcome row while a step runs", () => {
|
|
45
|
+
expect(render([tool("a", { status: "running" })])).not.toContain(
|
|
46
|
+
"Show steps"
|
|
49
47
|
);
|
|
50
|
-
expect(render(tools)).toContain("+3 more");
|
|
51
48
|
});
|
|
52
49
|
});
|
|
@@ -9,6 +9,7 @@ import { needsBody } from "@/lib/chat/bodyCache";
|
|
|
9
9
|
import { cn } from "@/lib/utils";
|
|
10
10
|
import { formatElapsed } from "@/lib/chat/elapsed";
|
|
11
11
|
import { stepCounts, toolOutcome } from "@/lib/chat/tool-outcome";
|
|
12
|
+
import { stepSummary } from "@/lib/chat/step-summary";
|
|
12
13
|
import { StepOutcomes } from "./StepOutcomes";
|
|
13
14
|
import { Highlighted } from "./Code";
|
|
14
15
|
import { BackgroundButton, BackgroundState, useBackground } from "./background";
|
|
@@ -227,13 +228,12 @@ function ToolRow({ tool }: { tool: ToolItem }) {
|
|
|
227
228
|
export function ToolGroup({ tools }: { tools: ToolItem[] }) {
|
|
228
229
|
const running = tools.some((t) => t.status === "running");
|
|
229
230
|
const { ok, failed, stopped } = stepCounts(tools);
|
|
230
|
-
const edits = tools.filter((t) => t.diff || t.hasDiff).length;
|
|
231
231
|
const moved = tools.filter((t) => t.backgroundTaskId).length;
|
|
232
232
|
const [open, setOpen] = useRowState(`steps:${tools[0].id}`, false);
|
|
233
233
|
const latest =
|
|
234
234
|
tools.findLast((t) => t.status === "running") ?? tools[tools.length - 1];
|
|
235
235
|
return (
|
|
236
|
-
<div className="bg-foreground/[0.025] rounded-xl px-2 py-
|
|
236
|
+
<div className="bg-foreground/[0.025] rounded-xl px-2 py-0.5">
|
|
237
237
|
<div className="flex items-center gap-1">
|
|
238
238
|
<button
|
|
239
239
|
type="button"
|
|
@@ -250,12 +250,7 @@ export function ToolGroup({ tools }: { tools: ToolItem[] }) {
|
|
|
250
250
|
<Loader2 className="text-primary h-3.5 w-3.5 shrink-0 animate-spin" />
|
|
251
251
|
) : null}
|
|
252
252
|
<span className="min-w-0 flex-1 truncate">
|
|
253
|
-
{running
|
|
254
|
-
? latest.title
|
|
255
|
-
: `${tools.length} step${tools.length === 1 ? "" : "s"}`}
|
|
256
|
-
{!running &&
|
|
257
|
-
edits > 0 &&
|
|
258
|
-
` · ${edits} edit${edits === 1 ? "" : "s"}`}
|
|
253
|
+
{running ? latest.title : stepSummary(tools)}
|
|
259
254
|
{!running && (failed > 0 || stopped > 0) && ` · ${ok} ok`}
|
|
260
255
|
</span>
|
|
261
256
|
{running && <Elapsed tool={latest} />}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ToolItem } from "./group";
|
|
2
|
+
|
|
3
|
+
export const MAX_FILES = 3;
|
|
4
|
+
export const MAX_FAILED = 3;
|
|
5
|
+
|
|
6
|
+
const plural = (n: number, word: string) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
7
|
+
|
|
8
|
+
const baseName = (p: string) => p.split("/").filter(Boolean).pop() ?? p;
|
|
9
|
+
|
|
10
|
+
const editedPath = (t: ToolItem): string | undefined => {
|
|
11
|
+
if (t.diff?.path) return t.diff.path;
|
|
12
|
+
const i = (t.input ?? {}) as Record<string, unknown>;
|
|
13
|
+
const p = i.file_path ?? i.notebook_path;
|
|
14
|
+
return typeof p === "string" ? p : undefined;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// What a finished run of steps did, after its count: "5 steps · edited a.ts,
|
|
18
|
+
// lib/b.ts · ran 2 commands". Files edited more than once are named once.
|
|
19
|
+
export function stepSummary(tools: ToolItem[]): string {
|
|
20
|
+
const parts = [plural(tools.length, "step")];
|
|
21
|
+
const edited = tools.filter((t) => t.diff || t.hasDiff);
|
|
22
|
+
if (edited.length > 0) {
|
|
23
|
+
const files = [
|
|
24
|
+
...new Set(
|
|
25
|
+
edited
|
|
26
|
+
.map(editedPath)
|
|
27
|
+
.filter((p): p is string => !!p)
|
|
28
|
+
.map(baseName)
|
|
29
|
+
),
|
|
30
|
+
];
|
|
31
|
+
if (files.length === 0) parts.push(plural(edited.length, "edit"));
|
|
32
|
+
else {
|
|
33
|
+
const more = files.length - MAX_FILES;
|
|
34
|
+
parts.push(
|
|
35
|
+
`edited ${files.slice(0, MAX_FILES).join(", ")}${more > 0 ? ` +${more}` : ""}`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const commands = tools.filter((t) => t.name === "Bash").length;
|
|
40
|
+
if (commands > 0) parts.push(`ran ${plural(commands, "command")}`);
|
|
41
|
+
return parts.join(" · ");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The failed steps' names, for the line under a group that had failures.
|
|
45
|
+
export function failedSteps(tools: ToolItem[]): {
|
|
46
|
+
names: string[];
|
|
47
|
+
more: number;
|
|
48
|
+
} {
|
|
49
|
+
const failed = tools.filter((t) => t.status === "error");
|
|
50
|
+
return {
|
|
51
|
+
names: failed.slice(0, MAX_FAILED).map((t) => t.title),
|
|
52
|
+
more: Math.max(0, failed.length - MAX_FAILED),
|
|
53
|
+
};
|
|
54
|
+
}
|