From 3811b7e1e9ae2de080d42cab02f45b5b3f12f0b5 Mon Sep 17 00:00:00 2001
From: Phelipe Lorran <112786693+httpsphl@users.noreply.github.com>
Date: Sat, 3 Oct 2026 23:35:07 -0300
Subject: [PATCH 1/3] feat(chat): fold a run's tool calls into one line
While the bot works, the line names the call it is on, with a light
sweeping across it, and changes with each new call. Once it is done,
the line sums the calls up by kind, with the failures at the end. The
arrow opens every call. A single finished call keeps its own line.
---
app/src/activity.css | 40 ++++++++++
app/src/features/browser/browser.test.tsx | 3 +
app/src/features/chat/BotRun.tsx | 14 +++-
app/src/features/chat/ToolGroup.tsx | 80 ++++++++++++++++++++
app/src/features/chat/chat.test.tsx | 6 +-
app/src/features/chat/toolGroup.test.tsx | 90 +++++++++++++++++++++++
app/src/features/chat/toolSummary.ts | 47 ++++++++++++
app/src/features/files/files.test.tsx | 3 +
app/src/features/screens/screens.test.tsx | 1 +
app/src/i18n/en/chat.ts | 14 ++++
app/src/i18n/es/chat.ts | 12 +++
app/src/i18n/pt-BR/chat.ts | 11 +++
app/src/index.css | 1 +
docs/spec.md | 2 +-
14 files changed, 317 insertions(+), 7 deletions(-)
create mode 100644 app/src/activity.css
create mode 100644 app/src/features/chat/ToolGroup.tsx
create mode 100644 app/src/features/chat/toolGroup.test.tsx
create mode 100644 app/src/features/chat/toolSummary.ts
diff --git a/app/src/activity.css b/app/src/activity.css
new file mode 100644
index 00000000..470560fd
--- /dev/null
+++ b/app/src/activity.css
@@ -0,0 +1,40 @@
+/* What a bot is doing right now (features/chat/ToolGroup.tsx): a light
+ sweeps across the name of the call it is on. */
+
+.activity-shine {
+ background:
+ linear-gradient(
+ 90deg,
+ var(--muted) 0%,
+ var(--muted) 35%,
+ var(--ink) 50%,
+ var(--muted) 65%,
+ var(--muted) 100%
+ )
+ 0 0 / 250% 100%;
+ background-clip: text;
+ color: transparent;
+ animation: activity-shine 2.2s linear infinite;
+}
+
+@keyframes activity-shine {
+ from {
+ background-position: 100% 0;
+ }
+ to {
+ background-position: 0% 0;
+ }
+}
+
+/* Without motion, the name stands still in plain ink. */
+@media (prefers-reduced-motion: reduce) {
+ .activity-shine {
+ background: none;
+ color: var(--ink);
+ }
+}
+
+:root[data-motion="less"] .activity-shine {
+ background: none;
+ color: var(--ink);
+}
diff --git a/app/src/features/browser/browser.test.tsx b/app/src/features/browser/browser.test.tsx
index 39750efa..065ce76c 100644
--- a/app/src/features/browser/browser.test.tsx
+++ b/app/src/features/browser/browser.test.tsx
@@ -233,6 +233,9 @@ describe("browser panel", () => {
});
fake.chat.tool(scout.id, "Bash", { summary: "ls", status: "done" });
await openScout(fake);
+ fireEvent.click(
+ screen.getByRole("button", { name: "Took 1 step in the browser, ran 1 command" }),
+ );
expect(screen.getAllByRole("button", { name: /Watch in browser/ })).toHaveLength(1);
fireEvent.click(screen.getByRole("button", { name: "Watch in browser: Open a page" }));
expect(await screen.findByRole("complementary", { name: "Scout's browser" })).toBeDefined();
diff --git a/app/src/features/chat/BotRun.tsx b/app/src/features/chat/BotRun.tsx
index 5e844c27..443dfe86 100644
--- a/app/src/features/chat/BotRun.tsx
+++ b/app/src/features/chat/BotRun.tsx
@@ -15,7 +15,7 @@ import { Markdown } from "./Markdown";
import { runParts, splitDraft } from "./rows";
import { SharedFiles } from "./SharedFiles";
import { isSharedFiles } from "./shared";
-import { ToolLines } from "./ToolLines";
+import { ToolGroup } from "./ToolGroup";
export interface Live {
/** Text of the reply being written. */
@@ -108,6 +108,12 @@ function BotRunView({
live?: Live | undefined;
}) {
const first = items[0];
+ const parts = runParts(items);
+ // The bot is still on the last group of calls until it writes or stops.
+ const busy = Boolean(live?.working && !live.draft);
+ const tail = parts.at(-1)?.[0];
+ // Its line already says it is working.
+ const onCalls = tail?.body.kind === "tool" && !isSharedFiles(tail);
return (
// A turn shows up as the bot starts it, still empty; what it does then
// arrives inside, so a run with items never animates as a whole.
@@ -123,14 +129,14 @@ function BotRunView({
)}
- {runParts(items).map((part) => {
+ {parts.map((part, index) => {
const head = part[0] as ChatItem;
switch (head.body.kind) {
case "tool":
return isSharedFiles(head) ? (
- {message.body} -
+ {(!routine || open) && ( ++ {message.body} +
+ )} {delivery && delivery.state !== "sent" && (