Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
142e9a4870 | ||
|
|
e5c17606d0 |
@@ -19,7 +19,9 @@
|
|||||||
"Bash(git checkout:*)",
|
"Bash(git checkout:*)",
|
||||||
"Bash(git -C /home/naps62/tea/maestro merge grazioso-2 --no-edit)",
|
"Bash(git -C /home/naps62/tea/maestro merge grazioso-2 --no-edit)",
|
||||||
"Bash(git -C /home/naps62/tea/maestro push)",
|
"Bash(git -C /home/naps62/tea/maestro push)",
|
||||||
"Bash(curl:*)"
|
"Bash(curl:*)",
|
||||||
|
"Bash(nix build:*)",
|
||||||
|
"Bash(cargo fmt:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,15 +194,12 @@ choices, not daemon ones.
|
|||||||
appear *between* tool rows in the same turn are absorbed into the group
|
appear *between* tool rows in the same turn are absorbed into the group
|
||||||
(intermediate narration); the last `AgentText` with no subsequent tool stays
|
(intermediate narration); the last `AgentText` with no subsequent tool stays
|
||||||
outside as the prominent final answer. While a turn is still active
|
outside as the prominent final answer. While a turn is still active
|
||||||
(responding or thinking), the most recent trailing text or thought row is
|
(responding or thinking), the most recent intermediate `AgentText` or
|
||||||
kept outside as visible narration — the operator always sees the agent's
|
`AgentThought` in the group is kept outside as visible narration — replaced
|
||||||
latest reasoning without opening the group. It is replaced by the next text
|
by the next text row when one arrives, and absorbed into the group when the
|
||||||
row when one arrives, and absorbed into the group when the turn ends (unless
|
turn ends. The header still reads "N tool calls" (counting only tool rows).
|
||||||
it is `AgentText`, which stays outside as the final answer). The collapsed
|
Opening the group reveals tools and intermediate text in chronological
|
||||||
header reads "N tool calls · Label 'arg'" where the label and argument come
|
order, with the text rendered in a subdued style.
|
||||||
from the last tool row's `splitToolTitle` — the operator sees which command
|
|
||||||
is current at a glance. Opening the group reveals tools and intermediate
|
|
||||||
text in chronological order, with the text rendered in a subdued style.
|
|
||||||
- The web client's user, agent, thought, tool-detail, and notice text blocks
|
- The web client's user, agent, thought, tool-detail, and notice text blocks
|
||||||
copy their complete source text when clicked or keyboard-activated.
|
copy their complete source text when clicked or keyboard-activated.
|
||||||
- The web client consumes `available_commands_update` notices as the live
|
- The web client consumes `available_commands_update` notices as the live
|
||||||
|
|||||||
@@ -148,7 +148,7 @@
|
|||||||
pnpmDeps = (pkgs.fetchPnpmDeps.override { pnpm = pkgs.pnpm_10; }) {
|
pnpmDeps = (pkgs.fetchPnpmDeps.override { pnpm = pkgs.pnpm_10; }) {
|
||||||
inherit (finalAttrs) pname version src;
|
inherit (finalAttrs) pname version src;
|
||||||
fetcherVersion = 3;
|
fetcherVersion = 3;
|
||||||
hash = "sha256-AzF/rrhqbvl613hNsC18YSylBSVxtatg7fEiUEF1u2o=";
|
hash = "sha256-FEWOAtSFvOWHeCNR0RK8JQ/ssW31qmShu1YD2GuqioQ=";
|
||||||
};
|
};
|
||||||
|
|
||||||
buildPhase = ''
|
buildPhase = ''
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ describe("tool rows and activity groups", () => {
|
|||||||
chunk("agent:2", "agent_text", "the answer"),
|
chunk("agent:2", "agent_text", "the answer"),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const head = screen.getByText(/^2 tool calls/).closest("button")!;
|
const head = screen.getByText("2 tool calls").closest("button")!;
|
||||||
expect(head.getAttribute("aria-expanded")).toBe("false");
|
expect(head.getAttribute("aria-expanded")).toBe("false");
|
||||||
// The final answer stays outside the group; the intermediate text is in it.
|
// The final answer stays outside the group; the intermediate text is in it.
|
||||||
expect(document.querySelector(".st-tool-group")!.contains(screen.getByText("the answer"))).toBe(false);
|
expect(document.querySelector(".st-tool-group")!.contains(screen.getByText("the answer"))).toBe(false);
|
||||||
|
|||||||
@@ -354,8 +354,6 @@ export const StructuredPane = forwardRef<StructuredPaneHandle, StructuredPanePro
|
|||||||
key={item.id}
|
key={item.id}
|
||||||
id={item.id}
|
id={item.id}
|
||||||
toolCount={item.group.toolCount}
|
toolCount={item.group.toolCount}
|
||||||
lastToolLabel={item.group.lastToolLabel}
|
|
||||||
lastToolArg={item.group.lastToolArg}
|
|
||||||
expanded={expandedGroups.has(item.id)}
|
expanded={expandedGroups.has(item.id)}
|
||||||
onExpandedChange={(expanded) => setGroupExpanded(item.id, expanded)}
|
onExpandedChange={(expanded) => setGroupExpanded(item.id, expanded)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,27 +1,19 @@
|
|||||||
// A turn's collapsed activity block: two or more tool rows after the same
|
// A turn's collapsed activity block: two or more tool rows after the same
|
||||||
// `User` row, plus the narration absorbed between them. The header counts tool
|
// `User` row, plus the narration absorbed between them. The header counts tool
|
||||||
// rows only (docs/STRUCTURED-VIEW.md) and shows the last/current tool label so
|
// rows only (docs/STRUCTURED-VIEW.md); opening it reveals tools and text in
|
||||||
// the operator sees activity at a glance without expanding. Opening it reveals
|
// chronological order.
|
||||||
// tools and text in chronological order.
|
|
||||||
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
export interface ToolGroupProps {
|
export interface ToolGroupProps {
|
||||||
id: string;
|
id: string;
|
||||||
toolCount: number;
|
toolCount: number;
|
||||||
/** Label of the last (or running) tool, e.g. "Execute". */
|
|
||||||
lastToolLabel: string;
|
|
||||||
/** Argument of the last tool, e.g. "`git status`". */
|
|
||||||
lastToolArg: string;
|
|
||||||
expanded: boolean;
|
expanded: boolean;
|
||||||
onExpandedChange(expanded: boolean): void;
|
onExpandedChange(expanded: boolean): void;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToolGroup({ id, toolCount, lastToolLabel, lastToolArg, expanded, onExpandedChange, children }: ToolGroupProps) {
|
export function ToolGroup({ id, toolCount, expanded, onExpandedChange, children }: ToolGroupProps) {
|
||||||
const summary = lastToolArg
|
|
||||||
? `${toolCount} tool calls · ${lastToolLabel} \u2018${lastToolArg}\u2019`
|
|
||||||
: `${toolCount} tool calls`;
|
|
||||||
return (
|
return (
|
||||||
<div className="st-tool-group" data-tool-group-id={id}>
|
<div className="st-tool-group" data-tool-group-id={id}>
|
||||||
<button
|
<button
|
||||||
@@ -33,7 +25,7 @@ export function ToolGroup({ id, toolCount, lastToolLabel, lastToolArg, expanded,
|
|||||||
<span className="st-tool-group-glyph" aria-hidden="true">
|
<span className="st-tool-group-glyph" aria-hidden="true">
|
||||||
›
|
›
|
||||||
</span>
|
</span>
|
||||||
<span className="st-tool-group-title">{summary}</span>
|
<span className="st-tool-group-title">{`${toolCount} tool calls`}</span>
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
|
|||||||
@@ -151,33 +151,6 @@ describe("activity grouping", () => {
|
|||||||
expect(ended.rows.map((r) => r.id)).toEqual(["tool:1", "tool:2", "thought:1", "thought:2"]);
|
expect(ended.rows.map((r) => r.id)).toEqual(["tool:1", "tool:2", "thought:1", "thought:2"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps the newest agent_text outside while the turn is still live", () => {
|
|
||||||
const rows = [
|
|
||||||
text("user:1", "user", "go"),
|
|
||||||
tool("tool:1"),
|
|
||||||
text("agent:1", "agent_text", "working on it"),
|
|
||||||
tool("tool:2"),
|
|
||||||
text("agent:2", "agent_text", "almost done"),
|
|
||||||
];
|
|
||||||
const live = activityGroupsByTurn(rows, null, true).get("turn:user:1")!;
|
|
||||||
// "almost done" stays outside as visible narration while the turn is live
|
|
||||||
expect(live.rows.map((r) => r.id)).toEqual(["tool:1", "agent:1", "tool:2"]);
|
|
||||||
const ended = activityGroupsByTurn(rows, null, false).get("turn:user:1")!;
|
|
||||||
// Once ended, the final agent_text stays outside as the answer
|
|
||||||
expect(ended.rows.map((r) => r.id)).toEqual(["tool:1", "agent:1", "tool:2"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("tracks the last tool's label and argument for the collapsed header", () => {
|
|
||||||
const rows = [
|
|
||||||
text("user:1", "user", "go"),
|
|
||||||
row("tool:1", "tool_call", { title: "Read(src/main.ts)", status: "completed" }),
|
|
||||||
row("tool:2", "tool_call", { title: "Execute(git status)", kind: "execute", status: "completed" }),
|
|
||||||
];
|
|
||||||
const group = activityGroupsByTurn(rows, null, false).get("turn:user:1")!;
|
|
||||||
expect(group.lastToolLabel).toBe("Execute");
|
|
||||||
expect(group.lastToolArg).toBe("git status");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("absorbs a resolved approval, never the pending one", () => {
|
it("absorbs a resolved approval, never the pending one", () => {
|
||||||
const rows = [
|
const rows = [
|
||||||
text("user:1", "user", "go"),
|
text("user:1", "user", "go"),
|
||||||
|
|||||||
@@ -148,9 +148,6 @@ export interface ActivityGroup {
|
|||||||
toolCount: number;
|
toolCount: number;
|
||||||
/** Tools plus absorbed intermediate text/approvals, in transcript order. */
|
/** Tools plus absorbed intermediate text/approvals, in transcript order. */
|
||||||
rows: TranscriptRow[];
|
rows: TranscriptRow[];
|
||||||
/** `splitToolTitle` of the last tool row — the collapsed header shows it. */
|
|
||||||
lastToolLabel: string;
|
|
||||||
lastToolArg: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -161,10 +158,6 @@ export interface ActivityGroup {
|
|||||||
* the currently pending one) are absorbed the same way. The *last* agent text
|
* the currently pending one) are absorbed the same way. The *last* agent text
|
||||||
* with no subsequent tool stays standalone (the prominent final answer).
|
* with no subsequent tool stays standalone (the prominent final answer).
|
||||||
*
|
*
|
||||||
* While a turn is still live, the most recent text or thought row trailing the
|
|
||||||
* group also stays outside as visible narration — the operator always sees the
|
|
||||||
* agent's latest reasoning without opening the group.
|
|
||||||
*
|
|
||||||
* Returns groups keyed by `turn:<userId>`.
|
* Returns groups keyed by `turn:<userId>`.
|
||||||
*/
|
*/
|
||||||
export function activityGroupsByTurn(
|
export function activityGroupsByTurn(
|
||||||
@@ -204,36 +197,23 @@ export function activityGroupsByTurn(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (row.kind === "tool_call" || row.kind === "tool_diff") {
|
if (row.kind === "tool_call" || row.kind === "tool_diff") {
|
||||||
const group = groups.get(currentGid) ?? { toolCount: count, rows: [], lastToolLabel: "", lastToolArg: "" };
|
const group = groups.get(currentGid) ?? { toolCount: count, rows: [] };
|
||||||
// Flush any pending rows — a tool follows, so they're intermediate.
|
// Flush any pending rows — a tool follows, so they're intermediate.
|
||||||
group.rows.push(...pending);
|
group.rows.push(...pending);
|
||||||
pending = [];
|
pending = [];
|
||||||
group.rows.push(row);
|
group.rows.push(row);
|
||||||
const tool = (row.data ?? {}) as { title?: string; name?: string | null; kind?: string };
|
|
||||||
const { label, arg } = splitToolTitle(tool.title, tool.name ?? undefined, tool.kind);
|
|
||||||
group.lastToolLabel = label;
|
|
||||||
group.lastToolArg = arg;
|
|
||||||
groups.set(currentGid, group);
|
groups.set(currentGid, group);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Trailing text/thought rows after the last tool in this turn.
|
// A final agent text is the answer, so it stays outside. Thought rows are
|
||||||
// While the turn is still live, the newest one stays outside as visible
|
// working commentary, though: when a turn already has a disclosure they
|
||||||
// narration — the operator always sees the agent's latest reasoning.
|
// belong with its activity even if the next tool call has not arrived yet.
|
||||||
// A final agent_text (the "answer") stays outside regardless.
|
// While the turn is still live the most recent one stays outside as visible
|
||||||
|
// narration (docs/STRUCTURED-VIEW.md), and is absorbed when the turn ends.
|
||||||
const lastGroup = groups.get(currentGid);
|
const lastGroup = groups.get(currentGid);
|
||||||
if (lastGroup && pending.length > 0) {
|
if (lastGroup && pending.length > 0 && pending.every((row) => row.kind === "agent_thought")) {
|
||||||
const allThoughts = pending.every((row) => row.kind === "agent_thought");
|
const absorb = turnActive ? pending.slice(0, -1) : pending;
|
||||||
if (allThoughts) {
|
lastGroup.rows.push(...absorb);
|
||||||
// Pure thoughts: absorb all when done, keep the newest outside while live.
|
|
||||||
const absorb = turnActive ? pending.slice(0, -1) : pending;
|
|
||||||
lastGroup.rows.push(...absorb);
|
|
||||||
} else if (turnActive) {
|
|
||||||
// Mixed or agent_text trailing: keep the very last row outside while live,
|
|
||||||
// absorb the rest.
|
|
||||||
lastGroup.rows.push(...pending.slice(0, -1));
|
|
||||||
}
|
|
||||||
// When the turn ends with agent_text last, nothing extra is absorbed — the
|
|
||||||
// text stays outside as the final answer.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return groups;
|
return groups;
|
||||||
|
|||||||
Reference in New Issue
Block a user