| author | |
| committer | |
| log | 43bd6677392b80cd12742f865c277c9939239543 |
| tree | b8e1de2b6abdae14a551d01cf403a3b1733d9a0f |
| parent | bb7997709dd5ed91a3aa18c091286f5f5331b0ef |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>8 files changed, 75 insertions(+), 25 deletions(-)
apps/mobile/src/lib/threadActivity.ts+2-2| ... | @@ -48,7 +48,7 @@ import { RunId, ThreadId } from "@t3tools/contracts"; | ... | @@ -48,7 +48,7 @@ import { RunId, ThreadId } from "@t3tools/contracts"; |
| 48 | import { | 48 | import { |
| 49 | classifyToolActivity, | 49 | classifyToolActivity, |
| 50 | collectToolFilePaths, | 50 | collectToolFilePaths, |
| 51 | computerUseToolTitle, | 51 | dynamicToolTitle, |
| 52 | formatReadToolLabel, | 52 | formatReadToolLabel, |
| 53 | formatSearchToolLabel, | 53 | formatSearchToolLabel, |
| 54 | } from "@t3tools/shared/toolActivity"; | 54 | } from "@t3tools/shared/toolActivity"; |
| ... | @@ -540,7 +540,7 @@ function itemSummary( | ... | @@ -540,7 +540,7 @@ function itemSummary( |
| 540 | if (item.type === "system_notice") return item.message; | 540 | if (item.type === "system_notice") return item.message; |
| 541 | if (item.type === "compaction") return contextCompactionLabel(item); | 541 | if (item.type === "compaction") return contextCompactionLabel(item); |
| 542 | const title = | 542 | const title = |
| 543 | (item.type === "dynamic_tool" ? computerUseToolTitle(item.toolName, item.input) : undefined) ?? | 543 | (item.type === "dynamic_tool" ? dynamicToolTitle(item.toolName, item.input) : undefined) ?? |
| 544 | item.title?.trim(); | 544 | item.title?.trim(); |
| 545 | if (item.type === "subagent") return formatSubagentDisplayTitle(title || "Subagent"); | 545 | if (item.type === "subagent") return formatSubagentDisplayTitle(title || "Subagent"); |
| 546 | if (title) return toolPresentation?.displayName ?? capitalizePhrase(title); | 546 | if (title) return toolPresentation?.displayName ?? capitalizePhrase(title); |
apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts+6-1| ... | @@ -2771,7 +2771,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { | ... | @@ -2771,7 +2771,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 2771 | }).pipe(Effect.scoped, Effect.provide(Layer.mergeAll(NodeServices.layer, IdAllocator.layer))), | 2771 | }).pipe(Effect.scoped, Effect.provide(Layer.mergeAll(NodeServices.layer, IdAllocator.layer))), |
| 2772 | ); | 2772 | ); |
| 2773 | 2773 | ||
| 2774 | it.effect("retains image preview paths on Claude Read tool completion", () => | 2774 | it.effect("titles Claude reads, searches, and skills on tool completion", () => |
| 2775 | Effect.gen(function* () { | 2775 | Effect.gen(function* () { |
| 2776 | const harness = yield* makeWakeHarness; | 2776 | const harness = yield* makeWakeHarness; |
| 2777 | const now = yield* DateTime.now; | 2777 | const now = yield* DateTime.now; |
| ... | @@ -2789,6 +2789,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { | ... | @@ -2789,6 +2789,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 2789 | { id: "image", name: "Read", input: { file_path: " /workspace/reference.png " } }, | 2789 | { id: "image", name: "Read", input: { file_path: " /workspace/reference.png " } }, |
| 2790 | { id: "text", name: "Read", input: { file_path: "/workspace/README.md" } }, | 2790 | { id: "text", name: "Read", input: { file_path: "/workspace/README.md" } }, |
| 2791 | { id: "search", name: "Grep", input: { pattern: "TODO", path: "/workspace/src" } }, | 2791 | { id: "search", name: "Grep", input: { pattern: "TODO", path: "/workspace/src" } }, |
| 2792 | { id: "skill", name: "Skill", input: { skill: "full-send" } }, | ||
| 2792 | { | 2793 | { |
| 2793 | id: "write", | 2794 | id: "write", |
| 2794 | name: "Write", | 2795 | name: "Write", |
| ... | @@ -2859,6 +2860,10 @@ describe("ClaudeAdapterV2 background wake turns", () => { | ... | @@ -2859,6 +2860,10 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 2859 | items.find((item) => item.nativeItemRef?.nativeId === "search")?.title, | 2860 | items.find((item) => item.nativeItemRef?.nativeId === "search")?.title, |
| 2860 | "Searched TODO in src", | 2861 | "Searched TODO in src", |
| 2861 | ); | 2862 | ); |
| 2863 | assert.equal( | ||
| 2864 | items.find((item) => item.nativeItemRef?.nativeId === "skill")?.title, | ||
| 2865 | "Skill: full-send", | ||
| 2866 | ); | ||
| 2862 | for (const item of items.filter((item) => item.nativeItemRef?.nativeId !== "image")) | 2867 | for (const item of items.filter((item) => item.nativeItemRef?.nativeId !== "image")) |
| 2863 | assert.notProperty(item, "viewedImagePath"); | 2868 | assert.notProperty(item, "viewedImagePath"); |
| 2864 | }).pipe(Effect.scoped, Effect.provide(Layer.mergeAll(NodeServices.layer, IdAllocator.layer))), | 2869 | }).pipe(Effect.scoped, Effect.provide(Layer.mergeAll(NodeServices.layer, IdAllocator.layer))), |
apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts+9-2| ... | @@ -1,7 +1,11 @@ | ... | @@ -1,7 +1,11 @@ |
| 1 | import * as NodeCrypto from "node:crypto"; | 1 | import * as NodeCrypto from "node:crypto"; |
| 2 | 2 | ||
| 3 | import { makeProviderTextDeltaCoalescer } from "./ProviderTextDeltaCoalescer.ts"; | 3 | import { makeProviderTextDeltaCoalescer } from "./ProviderTextDeltaCoalescer.ts"; |
| 4 | import { formatReadToolLabel, formatSearchToolLabel } from "@t3tools/shared/toolActivity"; | 4 | import { |
| 5 | dynamicToolTitle, | ||
| 6 | formatReadToolLabel, | ||
| 7 | formatSearchToolLabel, | ||
| 8 | } from "@t3tools/shared/toolActivity"; | ||
| 5 | import { isWorkspaceImagePreviewPath } from "@t3tools/shared/filePreview"; | 9 | import { isWorkspaceImagePreviewPath } from "@t3tools/shared/filePreview"; |
| 6 | import { normalizeClaudeTurnTokenUsage } from "../../provider/ClaudeTurnTokenUsage.ts"; | 10 | import { normalizeClaudeTurnTokenUsage } from "../../provider/ClaudeTurnTokenUsage.ts"; |
| 7 | import { | 11 | import { |
| ... | @@ -3726,7 +3730,10 @@ export function makeClaudeAdapterV2( | ... | @@ -3726,7 +3730,10 @@ export function makeClaudeAdapterV2( |
| 3726 | title: | 3730 | title: |
| 3727 | readPath !== undefined | 3731 | readPath !== undefined |
| 3728 | ? formatReadToolLabel(readPath) | 3732 | ? formatReadToolLabel(readPath) |
| 3729 | : (searchTitle ?? input.presentation?.title ?? null), | 3733 | : (searchTitle ?? |
| 3734 | dynamicToolTitle(input.toolName, nativeToolInput) ?? | ||
| 3735 | input.presentation?.title ?? | ||
| 3736 | null), | ||
| 3730 | startedAt: input.startedAt, | 3737 | startedAt: input.startedAt, |
| 3731 | completedAt, | 3738 | completedAt, |
| 3732 | updatedAt: input.updatedAt, | 3739 | updatedAt: input.updatedAt, |
apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts+2-2| ... | @@ -30,7 +30,7 @@ import { | ... | @@ -30,7 +30,7 @@ import { |
| 30 | } from "@t3tools/contracts"; | 30 | } from "@t3tools/contracts"; |
| 31 | import { SKILL_MENTION_PATTERN } from "@t3tools/shared/composerInlineTokens"; | 31 | import { SKILL_MENTION_PATTERN } from "@t3tools/shared/composerInlineTokens"; |
| 32 | import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; | 32 | import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; |
| 33 | import { computerUseToolTitle } from "@t3tools/shared/toolActivity"; | 33 | import { dynamicToolTitle } from "@t3tools/shared/toolActivity"; |
| 34 | import { getModelSelectionStringOptionValue, modelSelectionsEqual } from "@t3tools/shared/model"; | 34 | import { getModelSelectionStringOptionValue, modelSelectionsEqual } from "@t3tools/shared/model"; |
| 35 | import { resolveSpawnCommand } from "@t3tools/shared/shell"; | 35 | import { resolveSpawnCommand } from "@t3tools/shared/shell"; |
| 36 | import type { | 36 | import type { |
| ... | @@ -489,7 +489,7 @@ export function projectCodexDynamicToolItem( | ... | @@ -489,7 +489,7 @@ export function projectCodexDynamicToolItem( |
| 489 | item.type === "mcpToolCall" | 489 | item.type === "mcpToolCall" |
| 490 | ? `${item.server}.${item.tool}` | 490 | ? `${item.server}.${item.tool}` |
| 491 | : [trimText(item.namespace), item.tool].filter(Boolean).join("."); | 491 | : [trimText(item.namespace), item.tool].filter(Boolean).join("."); |
| 492 | const title = computerUseToolTitle(toolName, item.arguments); | 492 | const title = dynamicToolTitle(toolName, item.arguments); |
| 493 | const projection: CodexDynamicToolProjection = { | 493 | const projection: CodexDynamicToolProjection = { |
| 494 | ...(item.type === "mcpToolCall" ? mcpToolPresentation(item) : {}), | 494 | ...(item.type === "mcpToolCall" ? mcpToolPresentation(item) : {}), |
| 495 | toolName, | 495 | toolName, |
apps/web/src/components/chat/MessagesTimeline.logic.ts+3-5| ... | @@ -48,7 +48,7 @@ import { | ... | @@ -48,7 +48,7 @@ import { |
| 48 | type T3McpToolPresentation, | 48 | type T3McpToolPresentation, |
| 49 | } from "@t3tools/shared/t3McpToolPresentation"; | 49 | } from "@t3tools/shared/t3McpToolPresentation"; |
| 50 | import { compactDynamicToolOutput } from "@t3tools/shared/toolOutput"; | 50 | import { compactDynamicToolOutput } from "@t3tools/shared/toolOutput"; |
| 51 | import { computerUseToolTitle } from "@t3tools/shared/toolActivity"; | 51 | import { dynamicToolTitle } from "@t3tools/shared/toolActivity"; |
| 52 | import { formatWorkspaceRelativePath } from "../../filePathDisplay"; | 52 | import { formatWorkspaceRelativePath } from "../../filePathDisplay"; |
| 53 | import { | 53 | import { |
| 54 | collectToolFilePaths, | 54 | collectToolFilePaths, |
| ... | @@ -81,8 +81,7 @@ function singleToolCallLabel(entry: WorkLogEntry): string { | ... | @@ -81,8 +81,7 @@ function singleToolCallLabel(entry: WorkLogEntry): string { |
| 81 | const toolPresentation = resolveWorkEntryToolPresentation(entry, "completed"); | 81 | const toolPresentation = resolveWorkEntryToolPresentation(entry, "completed"); |
| 82 | if (toolPresentation) return toolPresentation.displayName; | 82 | if (toolPresentation) return toolPresentation.displayName; |
| 83 | const item = entry.structuredPayload; | 83 | const item = entry.structuredPayload; |
| 84 | const title = | 84 | const title = item?.type === "dynamic_tool" ? dynamicToolTitle(item.toolName, item.input) : null; |
| 85 | item?.type === "dynamic_tool" ? computerUseToolTitle(item.toolName, item.input) : null; | ||
| 86 | if (title) return title; | 85 | if (title) return title; |
| 87 | // A lone web search keeps its heading; the query stays in its detail. | 86 | // A lone web search keeps its heading; the query stays in its detail. |
| 88 | if (entry.itemType === "web_search") return entry.toolTitle ?? "Web search"; | 87 | if (entry.itemType === "web_search") return entry.toolTitle ?? "Web search"; |
| ... | @@ -142,8 +141,7 @@ export function workEntryDisplayLabel(entry: WorkLogEntry, workspaceRoot: string | ... | @@ -142,8 +141,7 @@ export function workEntryDisplayLabel(entry: WorkLogEntry, workspaceRoot: string |
| 142 | const providerRetry = | 141 | const providerRetry = |
| 143 | entry.projectedItem?.item.type === "error" && entry.projectedItem.item.retry !== undefined; | 142 | entry.projectedItem?.item.type === "error" && entry.projectedItem.item.retry !== undefined; |
| 144 | const item = entry.structuredPayload; | 143 | const item = entry.structuredPayload; |
| 145 | const title = | 144 | const title = item?.type === "dynamic_tool" ? dynamicToolTitle(item.toolName, item.input) : null; |
| 146 | item?.type === "dynamic_tool" ? computerUseToolTitle(item.toolName, item.input) : null; | ||
| 147 | if (title) return title; | 145 | if (title) return title; |
| 148 | const compactDetail = entry.detail?.trim(); | 146 | const compactDetail = entry.detail?.trim(); |
| 149 | const detailIsSearchOutput = | 147 | const detailIsSearchOutput = |
apps/web/src/components/chat/MessagesTimeline.tsx+22-9| ... | @@ -57,6 +57,7 @@ import { resolveChatListAnchoredEndSpace } from "@t3tools/shared/chatList"; | ... | @@ -57,6 +57,7 @@ import { resolveChatListAnchoredEndSpace } from "@t3tools/shared/chatList"; |
| 57 | import { toolActivityFaviconUrl } from "@t3tools/shared/favicon"; | 57 | import { toolActivityFaviconUrl } from "@t3tools/shared/favicon"; |
| 58 | import { formatDuration } from "@t3tools/shared/orchestrationTiming"; | 58 | import { formatDuration } from "@t3tools/shared/orchestrationTiming"; |
| 59 | import { getProjectFaviconCacheKey } from "@t3tools/shared/projectFavicon"; | 59 | import { getProjectFaviconCacheKey } from "@t3tools/shared/projectFavicon"; |
| 60 | import { claudeSkillInvocation } from "@t3tools/shared/toolActivity"; | ||
| 60 | import { observeVisibleAnimation } from "../../lib/visibleAnimation"; | 61 | import { observeVisibleAnimation } from "../../lib/visibleAnimation"; |
| 61 | import { | 62 | import { |
| 62 | createContext, | 63 | createContext, |
| ... | @@ -5034,8 +5035,19 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: { | ... | @@ -5034,8 +5035,19 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: { |
| 5034 | ? getQuestionAnswerPreview(workEntry.questionAnswer) | 5035 | ? getQuestionAnswerPreview(workEntry.questionAnswer) |
| 5035 | : null; | 5036 | : null; |
| 5036 | const viewedImagePath = workEntryViewedImagePath(workEntry); | 5037 | const viewedImagePath = workEntryViewedImagePath(workEntry); |
| 5037 | const isRead = toolGroupAction(workEntry) === "read"; | 5038 | const payload = workEntry.structuredPayload; |
| 5038 | const readOutput = isRead ? workEntryReadOutput(workEntry, workspaceRoot) : null; | 5039 | const skill = |
| 5040 | payload?.type === "dynamic_tool" | ||
| 5041 | ? claudeSkillInvocation(payload.toolName, payload.input) | ||
| 5042 | : undefined; | ||
| 5043 | // Reads and skills expand to plain text instead of the item inspector. A | ||
| 5044 | // skill's heading already names it, so only its arguments are left to show. | ||
| 5045 | const plainOutput = | ||
| 5046 | toolGroupAction(workEntry) === "read" | ||
| 5047 | ? workEntryReadOutput(workEntry, workspaceRoot) | ||
| 5048 | : skill | ||
| 5049 | ? (skill.args ?? null) | ||
| 5050 | : undefined; | ||
| 5039 | const viewedImage = | 5051 | const viewedImage = |
| 5040 | viewedImagePath && threadRef | 5052 | viewedImagePath && threadRef |
| 5041 | ? resolveViewedImageAsset(viewedImagePath, { | 5053 | ? resolveViewedImageAsset(viewedImagePath, { |
| ... | @@ -5055,8 +5067,8 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: { | ... | @@ -5055,8 +5067,8 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: { |
| 5055 | ); | 5067 | ); |
| 5056 | const expandedBody = | 5068 | const expandedBody = |
| 5057 | expanded && !isReasoning | 5069 | expanded && !isReasoning |
| 5058 | ? isRead | 5070 | ? plainOutput !== undefined |
| 5059 | ? readOutput | 5071 | ? plainOutput |
| 5060 | : buildToolCallExpandedBody( | 5072 | : buildToolCallExpandedBody( |
| 5061 | workEntry, | 5073 | workEntry, |
| 5062 | workspaceRoot, | 5074 | workspaceRoot, |
| ... | @@ -5064,9 +5076,10 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: { | ... | @@ -5064,9 +5076,10 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: { |
| 5064 | viewedImage ? viewedImagePath : null, | 5076 | viewedImage ? viewedImagePath : null, |
| 5065 | ) | 5077 | ) |
| 5066 | : null; | 5078 | : null; |
| 5067 | const canExpandProjectedItem = isRead | 5079 | const canExpandProjectedItem = |
| 5068 | ? Boolean(readOutput || viewedImage || workEntry.questionAnswer) | 5080 | plainOutput !== undefined |
| 5069 | : canExpand || workEntry.projectedItem !== undefined; | 5081 | ? Boolean(plainOutput || viewedImage || workEntry.questionAnswer) |
| 5082 | : canExpand || workEntry.projectedItem !== undefined; | ||
| 5070 | // Reserve destructive row styling for severe failures, not routine tool errors. | 5083 | // Reserve destructive row styling for severe failures, not routine tool errors. |
| 5071 | const iconWrapperClass = cn( | 5084 | const iconWrapperClass = cn( |
| 5072 | "flex size-4 items-center justify-center", | 5085 | "flex size-4 items-center justify-center", |
| ... | @@ -5237,9 +5250,9 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: { | ... | @@ -5237,9 +5250,9 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: { |
| 5237 | !isReasoning && | 5250 | !isReasoning && |
| 5238 | !workEntry.questionAnswer && | 5251 | !workEntry.questionAnswer && |
| 5239 | canExpandProjectedItem && | 5252 | canExpandProjectedItem && |
| 5240 | (expandedBody || (workEntry.projectedItem && !isRead)) ? ( | 5253 | (expandedBody || (workEntry.projectedItem && plainOutput === undefined)) ? ( |
| 5241 | <WorkLogDetails kind="panel"> | 5254 | <WorkLogDetails kind="panel"> |
| 5242 | {workEntry.projectedItem && !isRead ? ( | 5255 | {workEntry.projectedItem && plainOutput === undefined ? ( |
| 5243 | <V2ItemInspector | 5256 | <V2ItemInspector |
| 5244 | projectedItem={workEntry.projectedItem} | 5257 | projectedItem={workEntry.projectedItem} |
| 5245 | environmentId={ctx.activeThreadEnvironmentId} | 5258 | environmentId={ctx.activeThreadEnvironmentId} |
packages/shared/src/toolActivity.test.ts+12| ... | @@ -1,9 +1,11 @@ | ... | @@ -1,9 +1,11 @@ |
| 1 | import { describe, expect, it } from "vite-plus/test"; | 1 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | 2 | ||
| 3 | import { | 3 | import { |
| 4 | claudeSkillInvocation, | ||
| 4 | classifyToolActivity, | 5 | classifyToolActivity, |
| 5 | collectToolFilePaths, | 6 | collectToolFilePaths, |
| 6 | deriveToolActivityPresentation, | 7 | deriveToolActivityPresentation, |
| 8 | dynamicToolTitle, | ||
| 7 | formatReadToolLabel, | 9 | formatReadToolLabel, |
| 8 | formatSearchToolLabel, | 10 | formatSearchToolLabel, |
| 9 | mergeToolActivityData, | 11 | mergeToolActivityData, |
| ... | @@ -120,4 +122,14 @@ describe("toolActivity", () => { | ... | @@ -120,4 +122,14 @@ describe("toolActivity", () => { |
| 120 | mergeToolActivityData({ rawInput: { path: "src/a.ts" } }, { rawInput: { startLine: 4 } }), | 122 | mergeToolActivityData({ rawInput: { path: "src/a.ts" } }, { rawInput: { startLine: 4 } }), |
| 121 | ).toEqual({ rawInput: { path: "src/a.ts", startLine: 4 } }); | 123 | ).toEqual({ rawInput: { path: "src/a.ts", startLine: 4 } }); |
| 122 | }); | 124 | }); |
| 125 | |||
| 126 | it("titles Claude skill calls with the skill they load", () => { | ||
| 127 | expect(dynamicToolTitle("Skill", { skill: "full-send" })).toBe("Skill: full-send"); | ||
| 128 | expect(claudeSkillInvocation("Skill", { skill: "claude-api", args: " pricing " })).toEqual({ | ||
| 129 | name: "claude-api", | ||
| 130 | args: "pricing", | ||
| 131 | }); | ||
| 132 | expect(dynamicToolTitle("Skill", { skill: " " })).toBeUndefined(); | ||
| 133 | expect(dynamicToolTitle("Read", { skill: "full-send" })).toBeUndefined(); | ||
| 134 | }); | ||
| 123 | }); | 135 | }); |
packages/shared/src/toolActivity.ts+19-4| ... | @@ -14,13 +14,28 @@ function asTrimmedString(value: unknown): string | undefined { | ... | @@ -14,13 +14,28 @@ function asTrimmedString(value: unknown): string | undefined { |
| 14 | return trimmed.length > 0 ? trimmed : undefined; | 14 | return trimmed.length > 0 ? trimmed : undefined; |
| 15 | } | 15 | } |
| 16 | 16 | ||
| 17 | /** CUA's `title` describes the action shown in the activity log. */ | 17 | /** A Claude `Skill` call: the skill it loads and the arguments it passes, if any. */ |
| 18 | export function computerUseToolTitle( | 18 | export function claudeSkillInvocation( |
| 19 | toolName: string | null | undefined, | ||
| 20 | input: unknown, | ||
| 21 | ): { readonly name: string; readonly args: string | undefined } | undefined { | ||
| 22 | if (toolName !== "Skill") return undefined; | ||
| 23 | const record = asRecord(input); | ||
| 24 | const name = asTrimmedString(record?.skill); | ||
| 25 | return name === undefined ? undefined : { name, args: asTrimmedString(record?.args) }; | ||
| 26 | } | ||
| 27 | |||
| 28 | /** | ||
| 29 | * Activity log heading a dynamic tool derives from its input: CUA's `title`, | ||
| 30 | * or the skill a Claude `Skill` call loads. | ||
| 31 | */ | ||
| 32 | export function dynamicToolTitle( | ||
| 19 | toolName: string | null | undefined, | 33 | toolName: string | null | undefined, |
| 20 | input: unknown, | 34 | input: unknown, |
| 21 | ): string | undefined { | 35 | ): string | undefined { |
| 22 | if (toolName !== "cua_repl.js") return undefined; | 36 | if (toolName === "cua_repl.js") return asTrimmedString(asRecord(input)?.title); |
| 23 | return asTrimmedString(asRecord(input)?.title); | 37 | const skill = claudeSkillInvocation(toolName, input); |
| 38 | return skill === undefined ? undefined : `Skill: ${skill.name}`; | ||
| 24 | } | 39 | } |
| 25 | 40 | ||
| 26 | function recordHasKeys( | 41 | function recordHasKeys( |