}) {
@@ -3951,7 +3947,7 @@ function LiveActivityContent({
highlighted?: boolean;
}) {
const showTrailingFailureMark =
- failed && iconName !== undefined && !toolIconAcceptsTint(iconName, toolIcon);
+ failed && iconName !== undefined && !toolIconAcceptsTint(toolIcon);
return (
;
case "browser":
- return ;
case "computer":
- return ;
+ return ;
+ case "code":
+ return ;
case "device":
return ;
case "t3-code":
@@ -5382,6 +5380,11 @@ function workEntryIconName(workEntry: TimelineWorkEntry): WorkEntryIconName {
return "message-circle";
}
if (workEntry.toolSurface) return workEntry.toolSurface;
+ if (
+ workEntry.structuredPayload?.type === "dynamic_tool" &&
+ isJavaScriptReplTool(workEntry.structuredPayload.toolName)
+ )
+ return "code";
const toolPresentation = resolveWorkEntryToolPresentation(workEntry);
if (toolPresentation) return toolPresentation.icon;
const action = toolGroupAction(workEntry);
@@ -5794,7 +5797,7 @@ function WorkEntryLogRow(props: WorkEntryRowProps) {
) : null}
{showFailedIndicator &&
!showDestructiveRowStyle &&
- !toolIconAcceptsTint(entryIconName, entryToolIcon) ? (
+ !toolIconAcceptsTint(entryToolIcon) ? (
) : null}
diff --git a/apps/web/src/components/chat/V2ItemInspector.tsx b/apps/web/src/components/chat/V2ItemInspector.tsx
index 93befc5c75a53a86438eff0262f9e7c82f1f5cd8..2bc30383254826803fe88a31f9cdb2d935983675 100644
--- a/apps/web/src/components/chat/V2ItemInspector.tsx
+++ b/apps/web/src/components/chat/V2ItemInspector.tsx
@@ -12,6 +12,7 @@ import {
turnItemOutputText,
} from "@t3tools/client-runtime/work-log/item-detail";
import { formatDiffCount } from "@t3tools/client-runtime/diff-count";
+import { isJavaScriptReplTool } from "@t3tools/shared/toolActivity";
import { ExternalLinkIcon, GitBranchIcon, RotateCcwIcon } from "lucide-react";
import { memo, Suspense, use, useMemo } from "react";
@@ -43,13 +44,22 @@ interface V2ItemInspectorProps {
readonly onImageExpand?: ((preview: ExpandedImagePreview) => void) | undefined;
}
-function JsonTokens({ text }: { readonly text: string }) {
+function SyntaxTokens({
+ text,
+ language,
+}: {
+ readonly text: string;
+ readonly language: "json" | "typescript";
+}) {
const { resolvedTheme } = useTheme();
- const highlighter = use(getSyntaxHighlighterPromise("json"));
+ const highlighter = use(getSyntaxHighlighterPromise(language));
const { tokens } = useMemo(
() =>
- highlighter.codeToTokens(text, { lang: "json", theme: resolveDiffThemeName(resolvedTheme) }),
- [highlighter, text, resolvedTheme],
+ highlighter.codeToTokens(text, {
+ lang: language,
+ theme: resolveDiffThemeName(resolvedTheme),
+ }),
+ [highlighter, text, language, resolvedTheme],
);
return tokens.flatMap((line, lineIndex) => [
lineIndex > 0 ? "\n" : "",
@@ -67,9 +77,11 @@ const monoClassName =
function StructuredValue({
value,
highlightJson = false,
+ language,
}: {
readonly value: unknown;
readonly highlightJson?: boolean;
+ readonly language?: "typescript";
}) {
const text = typeof value === "string" ? value : JSON.stringify(value, null, 2);
const isJson = useMemo(() => {
@@ -84,10 +96,10 @@ function StructuredValue({
if (!text) return null;
return (
- {isJson ? (
+ {language || isJson ? (
-
+
) : (
@@ -194,6 +206,7 @@ function ToolCallBody(
props: ToolOutputState & {
readonly command?: string;
readonly args?: unknown;
+ readonly toolName?: string | null;
readonly exitCode?: number | undefined;
},
) {
@@ -206,7 +219,11 @@ function ToolCallBody(
{call.args.map(([key, value]) => (
{key}
- {value}
+ {key === "code" && isJavaScriptReplTool(props.toolName) ? (
+
+ ) : (
+ value
+ )}
))}
@@ -349,7 +366,12 @@ export const V2ItemInspector = memo(function V2ItemInspector(props: V2ItemInspec
) : null}
{item.type === "dynamic_tool" ? (
-
+
) : null}
{item.type === "approval_request" ? : null}
diff --git a/apps/web/src/components/chat/WorkLog.tsx b/apps/web/src/components/chat/WorkLog.tsx
index 1c588937813ad916a000dfc41c765826f72aaef4..378387c4261ecea2b11f818ed0f25eaa73956f06 100644
--- a/apps/web/src/components/chat/WorkLog.tsx
+++ b/apps/web/src/components/chat/WorkLog.tsx
@@ -49,20 +49,27 @@ function WorkLogLine({ icon, label, trailing, wrapLabel }: RowContent) {
- {icon ? (
-
{icon}
- ) : null}
-
- {label}
+
+ {icon ? (
+ // Align with the first line's capital letters. Centering line boxes drifts
+ // when font ascent/descent rounding changes at fractional browser zoom.
+
+
+ {icon}
+
+
+ ) : null}
+
+ {label}
+
{trailing}
diff --git a/packages/client-runtime/src/work-log/presentation.test.ts b/packages/client-runtime/src/work-log/presentation.test.ts
index 241215310aad7f5f2a8b8deed9c297b6700bf53d..4b0078fdb73760d822df6cf20312e7b1534fb771 100644
--- a/packages/client-runtime/src/work-log/presentation.test.ts
+++ b/packages/client-runtime/src/work-log/presentation.test.ts
@@ -253,6 +253,28 @@ describe("summarizeToolGroup", () => {
).toBe("Used Chrome integration and ran 1 command");
});
+ it.each([
+ [["Foo", "Foo"], "Used Foo integration and ran 1 command"],
+ [["Foo", "Bar", "Foo"], "Used Foo and Bar integrations and ran 1 command"],
+ ] as const)(
+ "uses the displayed source names for integration plurality: %j",
+ (names, expected) => {
+ expect(
+ summarizeToolGroup([
+ ...names.map((name, index) =>
+ entry(`integration-${index}`, {
+ toolSource: { key: `integration-${index}`, name, kind: "integration" },
+ }),
+ ),
+ entry("command", {
+ itemType: "command_execution",
+ command: "git status",
+ }),
+ ]).summary,
+ ).toBe(expected);
+ },
+ );
+
it("omits the integration suffix for special browser and computer sources", () => {
expect(
summarizeToolGroup([
@@ -267,6 +289,25 @@ describe("summarizeToolGroup", () => {
]).summary,
).toBe("Used Browser and Computer Use");
});
+
+ it("names Computer Use once when native-app and fallback sources share its label", () => {
+ expect(
+ summarizeToolGroup([
+ entry("inspect-app", {
+ label: "Inspect app",
+ toolSource: {
+ key: "native-app:com.t3tools.t3code",
+ name: "Computer Use",
+ kind: "computer",
+ },
+ }),
+ entry("failed-sky", {
+ label: "Restrict the breakpoint to Git actions",
+ toolSource: { key: "computer-use", name: "Computer Use", kind: "computer" },
+ }),
+ ]).summary,
+ ).toBe("Used Computer Use");
+ });
});
describe("resolveWorkEntryToolPresentation", () => {
diff --git a/packages/client-runtime/src/work-log/presentation.ts b/packages/client-runtime/src/work-log/presentation.ts
index 90c33b789a64fd57910815ab2a9e2ff572031f53..cb5ac00b0e9645d29c0741b31e7773d12b4d4eb5 100644
--- a/packages/client-runtime/src/work-log/presentation.ts
+++ b/packages/client-runtime/src/work-log/presentation.ts
@@ -697,7 +697,7 @@ export function summarizeToolGroup(entries: ReadonlyArray
label);
if (sources.size > 0) {
const sourceValues = [...sources.values()];
- const sourceNames = sourceValues.map((source) => source.name);
+ const sourceNames = [...new Set(sourceValues.map((source) => source.name))];
const formattedNames =
sourceNames.length < 2
? sourceNames[0]!
@@ -706,7 +706,7 @@ export function summarizeToolGroup(entries: ReadonlyArray source.kind === "integration");
labels.unshift(
- `Used ${formattedNames}${allIntegrations ? ` ${sources.size === 1 ? "integration" : "integrations"}` : ""}`,
+ `Used ${formattedNames}${allIntegrations ? ` ${sourceNames.length === 1 ? "integration" : "integrations"}` : ""}`,
);
}
const sourcedCount = entries.filter(
diff --git a/packages/client-runtime/src/work-log/toolPresentation.test.ts b/packages/client-runtime/src/work-log/toolPresentation.test.ts
index a794678a372148158c9337d8827fede1b9ce70fb..37733eca4c88b60efa3f46607b75405e88212708 100644
--- a/packages/client-runtime/src/work-log/toolPresentation.test.ts
+++ b/packages/client-runtime/src/work-log/toolPresentation.test.ts
@@ -3,6 +3,49 @@ import { describe, expect, it } from "@effect/vitest";
import { extractToolActivityPresentation } from "./toolPresentation.ts";
describe("extractToolActivityPresentation", () => {
+ it("recovers computer-use presentation for retained calls without result metadata", () => {
+ const item = {
+ type: "dynamic_tool",
+ toolName: "js",
+ input: { code: "await sky.get_app_state(app);" },
+ };
+ expect(extractToolActivityPresentation(item)).toEqual({
+ toolSurface: "computer",
+ toolSource: { key: "computer-use", name: "Computer Use", kind: "computer" },
+ });
+ expect(extractToolActivityPresentation({ ...item, toolSurface: "browser" })).toEqual({
+ toolSurface: "browser",
+ });
+ expect(
+ extractToolActivityPresentation({ ...item, input: { code: "await fs.readFile(path);" } }),
+ ).toEqual({});
+ });
+
+ it("replaces a failed Sky call's generic node REPL source", () => {
+ const source = { key: "mcp:node_repl", name: "node repl", kind: "integration" };
+ const item = {
+ type: "dynamic_tool",
+ toolName: "node_repl.js",
+ title: "js",
+ status: "failed",
+ toolSource: source,
+ input: {
+ title: "Restrict the breakpoint to Git actions",
+ code: "await sky.type_text({app:'T3 Code (Nightly)',text:'debug(ga.run)'});",
+ },
+ };
+ expect(extractToolActivityPresentation(item)).toEqual({
+ toolSurface: "computer",
+ toolSource: { key: "computer-use", name: "Computer Use", kind: "computer" },
+ });
+ expect(
+ extractToolActivityPresentation({
+ ...item,
+ input: { code: "await fetch('http://127.0.0.1/json/list');" },
+ }),
+ ).toEqual({ toolSource: source });
+ });
+
it("retains valid image paths independently of redacted output", () => {
expect(
extractToolActivityPresentation({ viewedImagePath: " /workspace/reference.webp " }),
diff --git a/packages/client-runtime/src/work-log/toolPresentation.ts b/packages/client-runtime/src/work-log/toolPresentation.ts
index 95ad72cfcca1c76694250a79530ec17b46f22a04..f69ddbd3c65162cd14fce623b2eb893009b5ab53 100644
--- a/packages/client-runtime/src/work-log/toolPresentation.ts
+++ b/packages/client-runtime/src/work-log/toolPresentation.ts
@@ -1,4 +1,5 @@
import { isWorkspaceImagePreviewPath } from "@t3tools/shared/filePreview";
+import { COMPUTER_USE_TOOL_SOURCE, dynamicToolSurface } from "@t3tools/shared/toolActivity";
import type {
ToolActivityIcon,
ToolActivityNativeAppReference,
@@ -111,12 +112,23 @@ export function extractToolActivityPresentation(
payloadValue: unknown,
): ExtractedToolActivityPresentation {
const payload = asRecord(payloadValue);
+ const retainedSource = activitySource(payload?.toolSource);
const toolSurface =
payload?.toolSurface === "browser" || payload?.toolSurface === "computer"
? payload.toolSurface
- : undefined;
+ : retainedSource?.kind === "computer" || retainedSource?.kind === "browser"
+ ? retainedSource.kind
+ : payload?.type === "dynamic_tool"
+ ? dynamicToolSurface(
+ typeof payload.toolName === "string" ? payload.toolName : undefined,
+ payload.input,
+ )
+ : undefined;
+ const toolSource =
+ toolSurface === "computer" && retainedSource?.kind !== "computer"
+ ? COMPUTER_USE_TOOL_SOURCE
+ : retainedSource;
const toolIcon = activityIcon(payload?.toolIcon);
- const toolSource = activitySource(payload?.toolSource);
const viewedImagePath = trimmedString(payload?.viewedImagePath, 4096);
return {
...(toolSurface ? { toolSurface } : {}),
diff --git a/packages/shared/src/toolActivity.test.ts b/packages/shared/src/toolActivity.test.ts
index fe19052fa5bc005aecaaf07213fb45cfce76293a..e18eeda8ca80b9b5ee1b20f2e53c282ca0329c90 100644
--- a/packages/shared/src/toolActivity.test.ts
+++ b/packages/shared/src/toolActivity.test.ts
@@ -6,6 +6,7 @@ import {
collectToolFilePaths,
deriveToolActivityPresentation,
dynamicToolTitle,
+ dynamicToolSurface,
formatReadToolLabel,
formatSearchToolLabel,
mergeToolActivityData,
@@ -123,6 +124,44 @@ describe("toolActivity", () => {
).toEqual({ rawInput: { path: "src/a.ts", startLine: 4 } });
});
+ it.each(["cua_repl.js", "node_repl.js", "js"])("uses the action title for %s", (toolName) => {
+ expect(dynamicToolTitle(toolName, { title: " Read the current T3 desktop window " })).toBe(
+ "Read the current T3 desktop window",
+ );
+ const fallback = toolName === "cua_repl.js" ? "Computer use" : "Run JavaScript";
+ expect(dynamicToolTitle(toolName, { title: " " })).toBe(fallback);
+ expect(dynamicToolTitle(toolName, { title: 123 })).toBe(fallback);
+ expect(dynamicToolTitle(toolName, {})).toBe(fallback);
+ expect(dynamicToolTitle(toolName, { code: "await sky.get_app_state(app);" })).toBe(
+ "Computer use",
+ );
+ });
+
+ it("keeps a retained action title but replaces raw REPL names", () => {
+ expect(dynamicToolTitle("js", {}, "Read the window")).toBe("Read the window");
+ expect(dynamicToolTitle("js", { title: "Inspect the window" }, "Read the window")).toBe(
+ "Inspect the window",
+ );
+ expect(dynamicToolTitle("js", { code: "await sky.click(app);" }, "Js")).toBe("Computer use");
+ expect(dynamicToolTitle("node_repl.js", {}, "Node_repl.js")).toBe("Run JavaScript");
+ });
+
+ it("recognizes computer-use REPL calls before result metadata exists", () => {
+ expect(dynamicToolSurface("cua_repl.js", {})).toBe("computer");
+ for (const name of ["node_repl.js", "js"]) {
+ expect(dynamicToolSurface(name, { code: "await sky.get_app_state(app);" })).toBe("computer");
+ expect(dynamicToolSurface(name, { code: "const sky = await import('@oai/sky');" })).toBe(
+ "computer",
+ );
+ expect(dynamicToolSurface(name, { code: "await fs.readFile(path);" })).toBeUndefined();
+ }
+ expect(dynamicToolSurface("github.js", { code: "await sky.click(app);" })).toBeUndefined();
+ });
+
+ it("leaves ordinary tool titles in their arguments", () => {
+ expect(dynamicToolTitle("github.create_issue", { title: "Issue title" })).toBeUndefined();
+ });
+
it("titles Claude skill calls with the skill they load", () => {
expect(dynamicToolTitle("Skill", { skill: "full-send" })).toBe("Skill: full-send");
expect(claudeSkillInvocation("Skill", { skill: "claude-api", args: " pricing " })).toEqual({
diff --git a/packages/shared/src/toolActivity.ts b/packages/shared/src/toolActivity.ts
index d281462cb08d32a0bfab6b74ae82b2957abff83a..d93c5fd3e1bc7aa1eea6d882ffe35455053bf691 100644
--- a/packages/shared/src/toolActivity.ts
+++ b/packages/shared/src/toolActivity.ts
@@ -25,15 +25,48 @@ export function claudeSkillInvocation(
return name === undefined ? undefined : { name, args: asTrimmedString(record?.args) };
}
+export function isJavaScriptReplTool(toolName: string | null | undefined): boolean {
+ return toolName === "js" || toolName === "cua_repl.js" || toolName === "node_repl.js";
+}
+
+export const COMPUTER_USE_TOOL_SOURCE = {
+ key: "computer-use",
+ name: "Computer Use",
+ kind: "computer",
+} as const;
+
+/** Computer-use calls may fail or still be running before result metadata is available. */
+export function dynamicToolSurface(toolName: string | null | undefined, input: unknown) {
+ if (toolName === "cua_repl.js") return "computer" as const;
+ const code = asRecord(input)?.code;
+ return isJavaScriptReplTool(toolName) &&
+ typeof code === "string" &&
+ (/\bsky\s*\./u.test(code) || /["']@oai\/sky["']/u.test(code))
+ ? ("computer" as const)
+ : undefined;
+}
+
/**
- * Activity log heading a dynamic tool derives from its input: CUA's `title`,
+ * Activity log heading a dynamic tool derives from its input: a REPL's `title`,
* or the skill a Claude `Skill` call loads.
*/
export function dynamicToolTitle(
toolName: string | null | undefined,
input: unknown,
+ retainedTitle?: string | null,
): string | undefined {
- if (toolName === "cua_repl.js") return asTrimmedString(asRecord(input)?.title);
+ if (isJavaScriptReplTool(toolName)) {
+ const title = asTrimmedString(retainedTitle);
+ const meaningfulTitle =
+ title && title.toLowerCase() !== "js" && title.toLowerCase() !== toolName?.toLowerCase()
+ ? title
+ : undefined;
+ return (
+ asTrimmedString(asRecord(input)?.title) ??
+ meaningfulTitle ??
+ (dynamicToolSurface(toolName, input) === "computer" ? "Computer use" : "Run JavaScript")
+ );
+ }
const skill = claudeSkillInvocation(toolName, input);
return skill === undefined ? undefined : `Skill: ${skill.name}`;
}