Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged.
`code_required` (below). A server and a computer must be upgraded together: an older computer has
no `/sign-in/code`.

### A reply's tool calls fold into one line

A Bot that searched, read two pages and checked Drive before answering drew four lines above its
answer. Adjacent connector calls and browser steps now fold into one line that says what was done,
such as "Searched the web and read 2 pages", with an icon for each kind of step. It names the step in
progress while the Bot works, and counts issues when a step was blocked or failed. Opened, it lists
each step with its query, page or connector, and each step opens to what it was given and what came
back. A single connector call keeps its own line, and approvals, connection requests and anything the
app draws itself stay outside the fold.

### Public-web research works with Exa and Firecrawl, not only Parallel

Four new catalogue entries give a Bot public-web search and page reading without its browser:
Expand Down
98 changes: 3 additions & 95 deletions app/src/components/channels/browser-activity.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
import { PendingApprovals } from "@/components/approvals/inline-approval";
import type { VisibleChatItem } from "./chat-messages";
import { ToolLine } from "./tool-line";

type BrowserStep = Extract<VisibleChatItem, { kind: "tool" }>;
export type BrowserGroup = {
kind: "browser";
id: string;
steps: BrowserStep[];
};
export type TranscriptItem = VisibleChatItem | BrowserGroup;

const labels: Record<string, string> = {
computer_navigate: "Opened page",
Expand All @@ -20,24 +12,9 @@ const labels: Record<string, string> = {
computer_scroll: "Scrolled page",
};

/** Group adjacent browsing steps without moving messages or hiding requests for human help. */
export function groupBrowserSteps(
items: readonly VisibleChatItem[],
): TranscriptItem[] {
const grouped: TranscriptItem[] = [];
for (const item of items) {
if (
item.kind !== "tool" ||
!Object.hasOwn(labels, item.toolCall.function.name)
) {
grouped.push(item);
continue;
}
const last = grouped.at(-1);
if (last?.kind === "browser") last.steps.push(item);
else grouped.push({ kind: "browser", id: item.id, steps: [item] });
}
return grouped;
/** Whether a tool call is one of the browser steps a tool group draws with its page link. */
export function isBrowserStep(item: BrowserStep): boolean {
return Object.hasOwn(labels, item.toolCall.function.name);
}

function objectOf(value: string | undefined): Record<string, unknown> {
Expand Down Expand Up @@ -83,72 +60,3 @@ export function browserStepDetails(step: BrowserStep) {
pending: step.result === undefined,
};
}

export function BrowserActivity({
group,
active,
}: {
group: BrowserGroup;
active: boolean;
}) {
const steps = group.steps.map((step) => ({
id: step.id,
...browserStepDetails(step),
}));
const pages = steps.filter((step) => step.visited).length;
const issues = steps.filter((step) => step.failed).length;
const running = active && steps.some((step) => step.pending);
// The folded line draws its own steps, so a waiting approval gets its card beside it, not inside.
return (
<>
<ToolLine
label={
running
? "Browsing…"
: pages
? `Browsed ${pages} ${pages === 1 ? "page" : "pages"}`
: "Browser activity"
}
detail={
issues ? `${issues} ${issues === 1 ? "issue" : "issues"}` : undefined
}
running={running}
>
<ol className="space-y-2 py-1">
{steps.map((step) => (
<li key={step.id} className="min-w-0">
<div className="flex min-w-0 items-baseline gap-2">
<span className="shrink-0 text-muted-foreground">
{step.label}
</span>
{step.href ? (
<a
href={step.href}
target="_blank"
rel="noopener noreferrer"
className="truncate underline decoration-muted-foreground/40 underline-offset-2 hover:decoration-current"
>
{step.title ?? step.href}
</a>
) : null}
{step.pending && (
<span className="text-muted-foreground">
{active ? "In progress" : "Not completed"}
</span>
)}
</div>
{step.failed && (
<p className="mt-0.5 text-destructive">
{step.reason ?? "This step could not be completed."}
</p>
)}
</li>
))}
</ol>
</ToolLine>
<PendingApprovals
toolCallIds={group.steps.map((step) => step.toolCall.id)}
/>
</>
);
}
12 changes: 6 additions & 6 deletions app/src/components/channels/chat-transcript.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,10 @@ import { readTeamBotConsent } from "@/lib/team-bots";
import { cn } from "@/lib/utils";
import { VOICE_CHAT_ACTIVITY, type VoiceChatEntry } from "@/lib/voice/archive";
import {
BrowserActivity,
groupBrowserSteps,
groupToolSteps,
ToolActivity,
type TranscriptItem,
} from "./browser-activity";
} from "./tool-activity";
import {
attachmentModality,
type SentAttachment,
Expand Down Expand Up @@ -1525,7 +1525,7 @@ export function ChatTranscript({
* cost was markdown parsing and chart SVGs, and those are skipped by the memoised children below,
* which is where the 25x came from. This runs per render and is not worth guarding.
*/
const items = groupBrowserSteps(toVisibleChatItems(messages));
const items = groupToolSteps(toVisibleChatItems(messages));

/*
* ONLY WHILE THERE IS NOTHING ELSE TO LOOK AT. Once a reply starts streaming, or a tool line
Expand Down Expand Up @@ -1668,12 +1668,12 @@ export function ChatTranscript({
</div>
{items.length === 0 && restoring ? <RestoringTranscript /> : null}
{items.map((item, index) =>
item.kind === "browser" ? (
item.kind === "tools" ? (
<MessageScrollerItem key={item.id} messageId={item.id}>
<Arriving
delay={delays.delayFor(item.id, index, items.length)}
>
<BrowserActivity
<ToolActivity
group={item}
active={busy && index === items.length - 1}
/>
Expand Down
Loading
Loading