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
12 changes: 12 additions & 0 deletions apps/mobile/src/lib/threadActivity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1094,6 +1094,18 @@ describe("buildThreadFeed", () => {
expect(activity?.getCopyText().split("\n")[0]).toBe("Read a T3 thread");
});

it("uses the CUA action title in the mobile feed", () => {
const item: OrchestrationV2TurnItem = {
...base("cua", "2026-09-23T20:20:00.000Z", 1),
type: "dynamic_tool",
toolName: "cua_repl.js",
input: { code: "await game.getAXStateAndScreenshot();", title: "Inspect Saga music screen" },
};
const feed = buildThreadFeed([projected(item, 0)]);
const activity = feed[0]?.type === "activity-group" ? feed[0].activities[0] : null;
expect(activity?.summary).toBe("Inspect Saga music screen");
});

it("uses canonical T3 orchestration summaries in compact work groups", () => {
const rows = [
projected(command("2026-06-20T00:00:01.000Z"), 0),
Expand Down
5 changes: 4 additions & 1 deletion apps/mobile/src/lib/threadActivity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import type {
import { ThreadId } from "@t3tools/contracts";
import { formatDuration } from "@t3tools/shared/orchestrationTiming";
import { compactDynamicToolOutput } from "@t3tools/shared/toolOutput";
import { computerUseToolTitle } from "@t3tools/shared/toolActivity";
import * as DateTime from "effect/DateTime";

export type PendingApproval = ThreadPendingApproval;
Expand Down Expand Up @@ -471,7 +472,9 @@ function itemSummary(
if (item.type === "notification") return item.summary;
if (item.type === "system_notice") return item.message;
if (item.type === "compaction") return contextCompactionLabel(item);
const title = item.title?.trim();
const title =
(item.type === "dynamic_tool" ? computerUseToolTitle(item.toolName, item.input) : undefined) ??
item.title?.trim();
if (item.type === "subagent") return formatSubagentDisplayTitle(title || "Subagent");
if (title) return toolPresentation?.displayName ?? capitalizePhrase(title);
switch (item.type) {
Expand Down
21 changes: 21 additions & 0 deletions apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,27 @@ describe("CodexAdapterV2 process spawning", () => {
});

describe("CodexAdapterV2 dynamic tool projection", () => {
it("uses the CUA call title while leaving other MCP titles as tool arguments", () => {
const call = {
type: "mcpToolCall" as const,
id: "inspect",
server: "cua_repl",
tool: "js",
status: "completed" as const,
arguments: {
code: "await game.getAXStateAndScreenshot();",
title: "Inspect Saga music screen",
},
result: { content: [] },
};
assert.equal(projectCodexDynamicToolItem(call).title, "Inspect Saga music screen");
assert.equal(
projectCodexDynamicToolItem({ ...call, arguments: { title: " " } }).title,
undefined,
);
assert.equal(projectCodexDynamicToolItem({ ...call, server: "github" }).title, undefined);
});

it("preserves native browser and app icons alongside MCP tool output", () => {
const browser = projectCodexDynamicToolItem({
type: "mcpToolCall",
Expand Down
6 changes: 5 additions & 1 deletion apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
ProviderDriverKind,
} from "@t3tools/contracts";
import { HostProcessEnvironment } from "@t3tools/shared/hostProcess";
import { computerUseToolTitle } from "@t3tools/shared/toolActivity";
import { getModelSelectionStringOptionValue, modelSelectionsEqual } from "@t3tools/shared/model";
import { resolveSpawnCommand } from "@t3tools/shared/shell";
import type {
Expand Down Expand Up @@ -428,6 +429,7 @@ export function codexBackgroundCommandDetail(item: {

export interface CodexDynamicToolProjection extends McpToolPresentation {
readonly toolName: string;
readonly title?: string;
readonly input: unknown;
readonly output?: unknown;
readonly status: OrchestrationV2TurnItem["status"];
Expand Down Expand Up @@ -469,9 +471,11 @@ export function projectCodexDynamicToolItem(
item.type === "mcpToolCall"
? `${item.server}.${item.tool}`
: [trimText(item.namespace), item.tool].filter(Boolean).join(".");
const title = computerUseToolTitle(toolName, item.arguments);
const projection: CodexDynamicToolProjection = {
...(item.type === "mcpToolCall" ? mcpToolPresentation(item) : {}),
toolName,
...(title ? { title } : {}),
input: item.arguments,
status: codexItemStatus(item.status).turnItem,
};
Expand Down Expand Up @@ -3168,7 +3172,7 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
nativeItemRef: codexNativeItemRef(item.id),
parentItemId: null,
ordinal,
title: null,
title: projection.title ?? null,
startedAt: context.startedAt,
completedAt,
updatedAt,
Expand Down
31 changes: 31 additions & 0 deletions apps/web/src/components/chat/MessagesTimeline.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,37 @@ describe("resolveAssistantMessageCopyState", () => {
});

describe("deriveMessagesTimelineRows", () => {
it("shows the CUA action title for a retained tool item", () => {
const fixture = makeStreamingTimelineFixture();
const source = fixture.visibleTurnItems.find((row) => row.item.type === "dynamic_tool")!;
if (source.item.type !== "dynamic_tool") throw new Error("Expected tool fixture");
const item = {
...source.item,
title: null,
toolName: "cua_repl.js",
input: { code: "await game.getAXStateAndScreenshot();", title: "Inspect Saga music screen" },
};
const entries = deriveTimelineEntriesFromVisibleTurnItems({
visibleTurnItems: [{ ...source, item }],
optimisticMessages: [],
});
const work = entries.find((entry) => entry.kind === "work");
expect(work?.kind === "work" && workEntryDisplayLabel(work.entry, undefined)).toBe(
"Inspect Saga music screen",
);
const rows = deriveMessagesTimelineRows({
timelineEntries: entries,
isWorking: false,
runningRunId: fixture.runId,
activeTurnStartedAt: fixture.time(0),
turnDiffSummaries: [],
supportsConversationRollback: false,
});
expect(rows.find((row) => row.kind === "work")).toMatchObject({
displayLabel: "Inspect Saga music screen",
});
});

it("presents project MCP calls and summarizes successful clones through the web timeline", () => {
const fixture = makeStreamingTimelineFixture();
const source = fixture.visibleTurnItems.find((row) => row.item.type === "dynamic_tool")!;
Expand Down
10 changes: 9 additions & 1 deletion apps/web/src/components/chat/MessagesTimeline.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
type T3McpToolPresentation,
} from "@t3tools/shared/t3McpToolPresentation";
import { compactDynamicToolOutput } from "@t3tools/shared/toolOutput";
import { computerUseToolTitle } from "@t3tools/shared/toolActivity";
import { formatWorkspaceRelativePath } from "../../filePathDisplay";

function timelineEntryRunId(entry: TimelineEntry): RunId | null {
Expand Down Expand Up @@ -75,7 +76,10 @@ function singleToolCallLabel(entry: WorkLogEntry): string {
if (toolPresentation) return toolPresentation.displayName;
const command = entry.command?.trim();
if (command) return commandDisplayText(command);
const heading = normalizeCompactToolLabel(entry.toolTitle || entry.label);
const item = entry.structuredPayload;
const title =
item?.type === "dynamic_tool" ? computerUseToolTitle(item.toolName, item.input) : null;
const heading = normalizeCompactToolLabel(title || entry.toolTitle || entry.label);
return `${heading.charAt(0).toUpperCase()}${heading.slice(1)}`;
}

Expand All @@ -88,6 +92,10 @@ export function workEntryDisplayLabel(entry: WorkLogEntry, workspaceRoot: string
// the retained message instead of a generic error heading.
const providerRetry =
entry.projectedItem?.item.type === "error" && entry.projectedItem.item.retry !== undefined;
const item = entry.structuredPayload;
const title =
item?.type === "dynamic_tool" ? computerUseToolTitle(item.toolName, item.input) : null;
if (title) return title;
if (entry.detail && !providerRetry) return entry.detail;
const [firstPath] = entry.changedFiles ?? [];
if (firstPath) {
Expand Down
14 changes: 10 additions & 4 deletions apps/web/src/components/chat/MessagesTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3482,9 +3482,14 @@ function LiveActivityContent({
icon={
iconName ? (
<span
className={
failed ? failedToolIconClassName : highlighted ? "text-foreground" : "text-icon-muted"
}
className={cn(
"flex size-4 items-center justify-center",
failed
? failedToolIconClassName
: highlighted
? "text-foreground"
: "text-icon-muted",
)}
role={announceFailure ? "img" : undefined}
aria-label={announceFailure ? "Tool call failed" : undefined}
>
Expand Down Expand Up @@ -4565,7 +4570,7 @@ function ToolActivityIconView(props: {
<NativeAppToolActivityIcon
app={props.icon.app}
fallbackName={props.fallbackName}
className={props.className}
className={cn(props.className, "size-5")}
muted={props.muted}
/>
);
Expand Down Expand Up @@ -5008,6 +5013,7 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: {
const canExpandProjectedItem = canExpand || workEntry.projectedItem !== undefined;
// Reserve destructive row styling for severe failures, not routine tool errors.
const iconWrapperClass = cn(
"flex size-4 items-center justify-center",
showWarningIndicator
? "text-warning"
: showDestructiveRowStyle
Expand Down
9 changes: 9 additions & 0 deletions packages/shared/src/toolActivity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ function asTrimmedString(value: unknown): string | undefined {
return trimmed.length > 0 ? trimmed : undefined;
}

/** CUA's `title` describes the action shown in the activity log. */
export function computerUseToolTitle(
toolName: string | null | undefined,
input: unknown,
): string | undefined {
if (toolName !== "cua_repl.js") return undefined;
return asTrimmedString(asRecord(input)?.title);
}

function normalizeCommandValue(value: unknown): string | undefined {
const direct = asTrimmedString(value);
if (direct) {
Expand Down
Loading