@opengeni/react 1.4.4 → 1.4.5-canary.37458084490001
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -6
- package/dist/artifacts.d.ts +1 -0
- package/dist/artifacts.js +22 -15
- package/dist/artifacts.js.map +1 -1
- package/dist/{browser-viewer-5EEAS3IU.js → browser-viewer-CV23VIHB.js} +8 -6
- package/dist/chunk-3ZC6QPOM.js +174 -0
- package/dist/chunk-3ZC6QPOM.js.map +1 -0
- package/dist/{chunk-YVFRBXST.js → chunk-4BCVCJSU.js} +2 -2
- package/dist/{chunk-JHTIMBOL.js → chunk-5JSUNNQN.js} +26 -4
- package/dist/{chunk-JHTIMBOL.js.map → chunk-5JSUNNQN.js.map} +1 -1
- package/dist/{chunk-I6OY3MWC.js → chunk-6FOTSVWU.js} +2 -2
- package/dist/{chunk-IUHHPMAX.js → chunk-7RRBX5EM.js} +14 -85
- package/dist/chunk-7RRBX5EM.js.map +1 -0
- package/dist/{chunk-L2J3PU7A.js → chunk-7ZB7I3KP.js} +24 -98
- package/dist/chunk-7ZB7I3KP.js.map +1 -0
- package/dist/{chunk-LVS3MH5T.js → chunk-ATIRUHBZ.js} +128 -10
- package/dist/chunk-ATIRUHBZ.js.map +1 -0
- package/dist/chunk-BOFEQC6K.js +162 -0
- package/dist/chunk-BOFEQC6K.js.map +1 -0
- package/dist/{chunk-2OQA3CPB.js → chunk-CESBPQP5.js} +19 -5
- package/dist/chunk-CESBPQP5.js.map +1 -0
- package/dist/{chunk-IYQCWGU5.js → chunk-GHJU7XG7.js} +35 -62
- package/dist/chunk-GHJU7XG7.js.map +1 -0
- package/dist/{chunk-G6YRCIYJ.js → chunk-HNVWMDNN.js} +55 -60
- package/dist/chunk-HNVWMDNN.js.map +1 -0
- package/dist/{chunk-PHBFJCEU.js → chunk-I5EGRXXG.js} +110 -7
- package/dist/chunk-I5EGRXXG.js.map +1 -0
- package/dist/{chunk-PBCZL4JP.js → chunk-INIZSNXT.js} +2 -2
- package/dist/{chunk-XS67YG5R.js → chunk-IX3K4THH.js} +24 -3
- package/dist/chunk-IX3K4THH.js.map +1 -0
- package/dist/chunk-IZV5FTJC.js +55 -0
- package/dist/chunk-IZV5FTJC.js.map +1 -0
- package/dist/chunk-JDJJMBB3.js +1883 -0
- package/dist/chunk-JDJJMBB3.js.map +1 -0
- package/dist/{chunk-C5BF4L26.js → chunk-KHF2VRXF.js} +7 -7
- package/dist/{chunk-QMMCJGFI.js → chunk-L5OKM2VX.js} +2 -2
- package/dist/chunk-LHF6BVM4.js +83 -0
- package/dist/chunk-LHF6BVM4.js.map +1 -0
- package/dist/chunk-N7S72EXK.js +25 -0
- package/dist/chunk-N7S72EXK.js.map +1 -0
- package/dist/{chunk-CVYCLFMH.js → chunk-PEHTX3V7.js} +19 -1
- package/dist/chunk-PEHTX3V7.js.map +1 -0
- package/dist/chunk-R5R5RQQD.js +51 -0
- package/dist/chunk-R5R5RQQD.js.map +1 -0
- package/dist/chunk-RI42H5IU.js +326 -0
- package/dist/chunk-RI42H5IU.js.map +1 -0
- package/dist/{chunk-JH7V627O.js → chunk-SHLZMAJN.js} +2 -11
- package/dist/chunk-SHLZMAJN.js.map +1 -0
- package/dist/{chunk-EAOSEIXB.js → chunk-TZQSHCZN.js} +692 -1996
- package/dist/chunk-TZQSHCZN.js.map +1 -0
- package/dist/chunk-ZIB4H5LN.js +101 -0
- package/dist/chunk-ZIB4H5LN.js.map +1 -0
- package/dist/client.d.ts +9 -3
- package/dist/clipboard.js +1 -1
- package/dist/components/browser-viewer.d.ts +6 -0
- package/dist/components/composer.d.ts +3 -69
- package/dist/components/human-input-form.d.ts +3 -32
- package/dist/components/markdown.d.ts +8 -0
- package/dist/components/model-policy-picker.d.ts +4 -37
- package/dist/components/opper-mark.d.ts +7 -0
- package/dist/components/provider-recovery-notice.d.ts +15 -0
- package/dist/components/remark-soft-line-breaks.d.ts +38 -0
- package/dist/components/sandbox-workspace.d.ts +13 -0
- package/dist/components/session-chrome.d.ts +5 -3
- package/dist/components/session-conversation.d.ts +15 -3
- package/dist/composer-messages.d.ts +74 -0
- package/dist/composer-messages.js +9 -0
- package/dist/composer.js +15 -10
- package/dist/{computer-viewer-ELNLZGHI.js → computer-viewer-K4CKYRX6.js} +4 -4
- package/dist/connect-setup.d.ts +2 -0
- package/dist/connect.d.ts +1 -1
- package/dist/connect.js +21 -6
- package/dist/connect.js.map +1 -1
- package/dist/{embedded-voice-NIL4RDX3.js → embedded-voice-3ZFWWJ6O.js} +6 -4
- package/dist/{embedded-voice-NIL4RDX3.js.map → embedded-voice-3ZFWWJ6O.js.map} +1 -1
- package/dist/hooks/shared-machines-list.d.ts +4 -0
- package/dist/hooks/use-client-config-flags.d.ts +6 -0
- package/dist/hooks/use-goal.d.ts +2 -1
- package/dist/human-input-model.d.ts +40 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +116 -73
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +5 -5
- package/dist/{latest-question-2POFPYNB.js → latest-question-SWC6QEVN.js} +3 -3
- package/dist/lib/clipboard.d.ts +7 -0
- package/dist/lib/provider-recovery.d.ts +49 -0
- package/dist/machines.js +2 -2
- package/dist/model-mark-paths.d.ts +34 -0
- package/dist/model-mark-paths.js +16 -0
- package/dist/model-picker-model.d.ts +111 -0
- package/dist/model-picker-model.js +25 -0
- package/dist/model-picker-model.js.map +1 -0
- package/dist/{model-policy-picker-menu-WRNSRFQC.js → model-policy-picker-menu-NM4RPTY6.js} +6 -4
- package/dist/model-policy-picker-menu-NM4RPTY6.js.map +1 -0
- package/dist/model-policy.d.ts +10 -0
- package/dist/model-policy.js +5 -1
- package/dist/organization-model.d.ts +32 -0
- package/dist/organization-model.js +65 -0
- package/dist/organization-model.js.map +1 -0
- package/dist/{platform-activity-row-PJJVATTZ.js → platform-activity-row-NJ47YBXW.js} +7 -84
- package/dist/platform-activity-row-NJ47YBXW.js.map +1 -0
- package/dist/queue-presentation-model.d.ts +72 -0
- package/dist/queue-presentation-model.js +38 -0
- package/dist/queue-presentation-model.js.map +1 -0
- package/dist/{queue-surface-implementation-67FS7FUI.js → queue-surface-implementation-YPJFQ3O6.js} +24 -252
- package/dist/queue-surface-implementation-YPJFQ3O6.js.map +1 -0
- package/dist/realtime.js +5 -3
- package/dist/{sandbox-files-34CJJNOY.js → sandbox-files-RCVCMXG7.js} +5 -5
- package/dist/sandbox-files-RCVCMXG7.js.map +1 -0
- package/dist/sandbox-label-model.d.ts +23 -0
- package/dist/sandbox-label-model.js +28 -0
- package/dist/sandbox-label-model.js.map +1 -0
- package/dist/session-attention-model.d.ts +8 -0
- package/dist/session-attention-model.js +46 -0
- package/dist/session-attention-model.js.map +1 -0
- package/dist/session-feedback-model.d.ts +20 -0
- package/dist/session-feedback-model.js +56 -0
- package/dist/session-feedback-model.js.map +1 -0
- package/dist/session-list-model.d.ts +96 -0
- package/dist/session-list-model.js +219 -0
- package/dist/session-list-model.js.map +1 -0
- package/dist/session-status-model.d.ts +19 -0
- package/dist/session-ui.js +26 -17
- package/dist/session-ui.js.map +1 -1
- package/dist/session.js +8 -6
- package/dist/testing/session-scenarios.d.ts +10 -0
- package/dist/testing/session-scenarios.js +303 -0
- package/dist/testing/session-scenarios.js.map +1 -0
- package/dist/timeline/genie-copy.d.ts +3 -0
- package/dist/timeline/genie-loading.d.ts +11 -0
- package/dist/timeline/index.d.ts +1 -1
- package/dist/timeline/notice-presentation.d.ts +7 -0
- package/dist/timeline/patch-apply-command.d.ts +18 -0
- package/dist/timeline/platform-activity-presentation.d.ts +7 -0
- package/dist/timeline/presented-image.d.ts +6 -1
- package/dist/timeline/question-nav-model.d.ts +23 -0
- package/dist/timeline/registry.d.ts +10 -1
- package/dist/timeline/retained-loaders.d.ts +1 -1
- package/dist/timeline/tool-presentation.d.ts +172 -0
- package/dist/timeline/turn-summary-model.d.ts +90 -0
- package/dist/timeline/turn-summary.d.ts +4 -74
- package/dist/timeline/work-presentation.d.ts +42 -0
- package/dist/timeline-model.d.ts +24 -0
- package/dist/timeline-model.js +152 -0
- package/dist/timeline-model.js.map +1 -0
- package/package.json +47 -3
- package/src/artifacts.ts +1 -0
- package/src/client.ts +9 -3
- package/src/components/browser-viewer.tsx +23 -2
- package/src/components/claude-mark.tsx +2 -1
- package/src/components/composer.tsx +4 -144
- package/src/components/grok-mark.tsx +2 -1
- package/src/components/human-input-form.tsx +13 -124
- package/src/components/markdown.tsx +19 -1
- package/src/components/message-timeline.tsx +45 -99
- package/src/components/model-mark.tsx +2 -1
- package/src/components/model-policy-picker-menu.tsx +30 -85
- package/src/components/model-policy-picker.tsx +15 -148
- package/src/components/opper-mark.tsx +18 -0
- package/src/components/provider-recovery-notice.tsx +41 -0
- package/src/components/queue-surface-implementation.tsx +20 -349
- package/src/components/remark-soft-line-breaks.ts +166 -0
- package/src/components/sandbox-workspace.tsx +17 -1
- package/src/components/session-chrome.tsx +114 -99
- package/src/components/session-conversation.tsx +55 -6
- package/src/components/session-status.tsx +17 -16
- package/src/composer-messages.ts +150 -0
- package/src/connect-setup.tsx +45 -11
- package/src/connect.ts +1 -1
- package/src/hooks/shared-machines-list.ts +34 -1
- package/src/hooks/use-client-config-flags.ts +8 -0
- package/src/hooks/use-goal.ts +66 -10
- package/src/human-input-model.ts +147 -0
- package/src/index.ts +16 -0
- package/src/lib/clipboard.ts +29 -0
- package/src/lib/format.ts +7 -0
- package/src/lib/provider-recovery.ts +202 -0
- package/src/model-mark-paths.ts +53 -0
- package/src/model-picker-model.ts +330 -0
- package/src/model-policy.ts +43 -2
- package/src/organization-model.ts +118 -0
- package/src/queue-presentation-model.ts +464 -0
- package/src/sandbox-label-model.ts +41 -0
- package/src/session-attention-model.ts +59 -0
- package/src/session-feedback-model.ts +95 -0
- package/src/session-list-model.ts +327 -0
- package/src/session-status-model.ts +62 -0
- package/src/testing/session-scenarios.ts +326 -0
- package/src/timeline/activity-rail.tsx +5 -17
- package/src/timeline/genie-copy.ts +19 -0
- package/src/timeline/genie-loading.tsx +13 -18
- package/src/timeline/index.ts +1 -0
- package/src/timeline/notice-presentation.ts +22 -0
- package/src/timeline/patch-apply-command.tsx +135 -0
- package/src/timeline/platform-activity-presentation.ts +122 -0
- package/src/timeline/platform-activity-row.tsx +6 -96
- package/src/timeline/presented-image.ts +16 -2
- package/src/timeline/question-nav-model.ts +28 -0
- package/src/timeline/registry.ts +11 -1
- package/src/timeline/retained-loaders.ts +9 -4
- package/src/timeline/rolling-activity.tsx +7 -19
- package/src/timeline/tool-presentation.ts +1316 -0
- package/src/timeline/tool-renderers.tsx +226 -1025
- package/src/timeline/turn-summary-model.ts +338 -0
- package/src/timeline/turn-summary.tsx +21 -326
- package/src/timeline/work-presentation.ts +139 -0
- package/src/timeline-model.ts +108 -0
- package/styles/compiled.css +3 -0
- package/styles/connect.css +4 -0
- package/dist/chunk-2OQA3CPB.js.map +0 -1
- package/dist/chunk-CVYCLFMH.js.map +0 -1
- package/dist/chunk-EAOSEIXB.js.map +0 -1
- package/dist/chunk-G6YRCIYJ.js.map +0 -1
- package/dist/chunk-HXYOSSQO.js +0 -117
- package/dist/chunk-HXYOSSQO.js.map +0 -1
- package/dist/chunk-IUHHPMAX.js.map +0 -1
- package/dist/chunk-IYQCWGU5.js.map +0 -1
- package/dist/chunk-JH7V627O.js.map +0 -1
- package/dist/chunk-L2J3PU7A.js.map +0 -1
- package/dist/chunk-LVS3MH5T.js.map +0 -1
- package/dist/chunk-PHBFJCEU.js.map +0 -1
- package/dist/chunk-XS67YG5R.js.map +0 -1
- package/dist/platform-activity-row-PJJVATTZ.js.map +0 -1
- package/dist/queue-surface-implementation-67FS7FUI.js.map +0 -1
- /package/dist/{browser-viewer-5EEAS3IU.js.map → browser-viewer-CV23VIHB.js.map} +0 -0
- /package/dist/{chunk-YVFRBXST.js.map → chunk-4BCVCJSU.js.map} +0 -0
- /package/dist/{chunk-I6OY3MWC.js.map → chunk-6FOTSVWU.js.map} +0 -0
- /package/dist/{chunk-PBCZL4JP.js.map → chunk-INIZSNXT.js.map} +0 -0
- /package/dist/{chunk-C5BF4L26.js.map → chunk-KHF2VRXF.js.map} +0 -0
- /package/dist/{chunk-QMMCJGFI.js.map → chunk-L5OKM2VX.js.map} +0 -0
- /package/dist/{computer-viewer-ELNLZGHI.js.map → composer-messages.js.map} +0 -0
- /package/dist/{model-policy-picker-menu-WRNSRFQC.js.map → computer-viewer-K4CKYRX6.js.map} +0 -0
- /package/dist/{latest-question-2POFPYNB.js.map → latest-question-SWC6QEVN.js.map} +0 -0
- /package/dist/{sandbox-files-34CJJNOY.js.map → model-mark-paths.js.map} +0 -0
|
@@ -0,0 +1,1316 @@
|
|
|
1
|
+
import type { GitFileDiff } from "@opengeni/sdk";
|
|
2
|
+
import { stringifyPayload, tryParseJson } from "../lib/format";
|
|
3
|
+
import {
|
|
4
|
+
applyPatchOpsFromToolItem,
|
|
5
|
+
controlCaret,
|
|
6
|
+
execTruncated,
|
|
7
|
+
isExecSessionLostBanner,
|
|
8
|
+
looksBinary,
|
|
9
|
+
parseExecBannerSessionId,
|
|
10
|
+
parseToolArgs,
|
|
11
|
+
sandboxCommandExitCode,
|
|
12
|
+
stripExecBanner,
|
|
13
|
+
tailPeek,
|
|
14
|
+
unwrapMcpOutput,
|
|
15
|
+
v4aToGitFileDiff,
|
|
16
|
+
type ApplyPatchOperation,
|
|
17
|
+
} from "./parsers";
|
|
18
|
+
import { rawTypeOf } from "./registry";
|
|
19
|
+
import { mcpToolLeaf, toolDisplayName } from "./tool-display-name";
|
|
20
|
+
import type { ToolCallItem } from "./types";
|
|
21
|
+
|
|
22
|
+
/* ----------------------------------------------------------------------------
|
|
23
|
+
Tool row presentation model (renderer-neutral)
|
|
24
|
+
|
|
25
|
+
The collapsed-row decisions every tool renderer makes — icon, tint, title,
|
|
26
|
+
preview, settle chip and what the expanded body holds — expressed as plain
|
|
27
|
+
data so DOM and non-DOM renderers draw the same row from one source of truth.
|
|
28
|
+
Web renderers consume these functions; a change here changes every surface.
|
|
29
|
+
-------------------------------------------------------------------------- */
|
|
30
|
+
|
|
31
|
+
export type ToolIconKind =
|
|
32
|
+
| "terminal"
|
|
33
|
+
| "keyboard"
|
|
34
|
+
| "file-diff"
|
|
35
|
+
| "search"
|
|
36
|
+
| "question"
|
|
37
|
+
| "target"
|
|
38
|
+
| "brain"
|
|
39
|
+
| "sessions"
|
|
40
|
+
| "server"
|
|
41
|
+
| "server-cog"
|
|
42
|
+
| "calendar"
|
|
43
|
+
| "panels"
|
|
44
|
+
| "share"
|
|
45
|
+
| "message"
|
|
46
|
+
| "git"
|
|
47
|
+
| "box"
|
|
48
|
+
| "key"
|
|
49
|
+
| "file-search"
|
|
50
|
+
| "package-search"
|
|
51
|
+
| "plug"
|
|
52
|
+
| "wrench";
|
|
53
|
+
|
|
54
|
+
export type ToolIconTone = "accent" | "failed" | "running" | "muted";
|
|
55
|
+
|
|
56
|
+
export type ToolChip = {
|
|
57
|
+
tone: "ok" | "bad" | "muted" | "interrupted";
|
|
58
|
+
text: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** The collapsed-row secondary line. `running` previews carry the pulse locus. */
|
|
62
|
+
export type ToolPreview =
|
|
63
|
+
| { kind: "text"; text: string; running?: boolean | undefined }
|
|
64
|
+
| {
|
|
65
|
+
kind: "path";
|
|
66
|
+
path: string;
|
|
67
|
+
add?: number | undefined;
|
|
68
|
+
del?: number | undefined;
|
|
69
|
+
}
|
|
70
|
+
| { kind: "files"; count: number; add: number; del: number }
|
|
71
|
+
| { kind: "malformed"; name: string };
|
|
72
|
+
|
|
73
|
+
export type ToolPatchFile = {
|
|
74
|
+
key: string;
|
|
75
|
+
path: string;
|
|
76
|
+
file: GitFileDiff | null;
|
|
77
|
+
diff: string;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export type WebSearchResult = {
|
|
81
|
+
title: string;
|
|
82
|
+
domain: string;
|
|
83
|
+
snippet: string;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** What the expanded body holds; renderers draw these with their own primitives. */
|
|
87
|
+
export type ToolBody =
|
|
88
|
+
| {
|
|
89
|
+
kind: "term";
|
|
90
|
+
command: string | null;
|
|
91
|
+
workdir: string | null;
|
|
92
|
+
output: string;
|
|
93
|
+
live?: boolean | undefined;
|
|
94
|
+
failed?: boolean | undefined;
|
|
95
|
+
note?: string | undefined;
|
|
96
|
+
}
|
|
97
|
+
| { kind: "note"; text: string; tone?: "error" | undefined }
|
|
98
|
+
| {
|
|
99
|
+
kind: "payloads";
|
|
100
|
+
/** A short explanation above the payloads. */
|
|
101
|
+
note?: string | undefined;
|
|
102
|
+
blocks: Array<{ label: string; value: unknown; failed?: boolean }>;
|
|
103
|
+
}
|
|
104
|
+
| {
|
|
105
|
+
kind: "patch";
|
|
106
|
+
files: ToolPatchFile[];
|
|
107
|
+
/** A single malformed op renders its raw patch without the path caption. */
|
|
108
|
+
bare?: boolean | undefined;
|
|
109
|
+
}
|
|
110
|
+
| { kind: "web-results"; results: WebSearchResult[] | null }
|
|
111
|
+
| {
|
|
112
|
+
/** A short labelled listing (disclosed tools, search hits) above payloads. */
|
|
113
|
+
kind: "listing";
|
|
114
|
+
note?: string | undefined;
|
|
115
|
+
entries: ToolListingEntry[];
|
|
116
|
+
/** Entries beyond the visible list ("+N more"). */
|
|
117
|
+
more?: number | undefined;
|
|
118
|
+
/** Shown instead of the list when it is known to be empty. */
|
|
119
|
+
empty?: string | undefined;
|
|
120
|
+
blocks: Array<{ label: string; value: unknown; failed?: boolean }>;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export type ToolListingEntry = {
|
|
124
|
+
key: string;
|
|
125
|
+
title: string;
|
|
126
|
+
/** Quiet leading label (the tool's server). */
|
|
127
|
+
eyebrow?: string | null | undefined;
|
|
128
|
+
mono?: boolean | undefined;
|
|
129
|
+
/** Secondary text under the title (a search snippet). */
|
|
130
|
+
snippet?: string | undefined;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export type PresentedToolKind =
|
|
134
|
+
| "exec"
|
|
135
|
+
| "write_stdin"
|
|
136
|
+
| "apply_patch"
|
|
137
|
+
| "web_search"
|
|
138
|
+
| "ask"
|
|
139
|
+
| "run_on"
|
|
140
|
+
| "tool_search"
|
|
141
|
+
| "docs_search"
|
|
142
|
+
| "session_title"
|
|
143
|
+
| "generic";
|
|
144
|
+
|
|
145
|
+
export type ToolRowPresentation = {
|
|
146
|
+
tool: PresentedToolKind;
|
|
147
|
+
icon: ToolIconKind;
|
|
148
|
+
iconTone: ToolIconTone;
|
|
149
|
+
title: string;
|
|
150
|
+
titleMono?: boolean | undefined;
|
|
151
|
+
running?: boolean | undefined;
|
|
152
|
+
preview?: ToolPreview | undefined;
|
|
153
|
+
chip?: ToolChip | undefined;
|
|
154
|
+
failed?: boolean | undefined;
|
|
155
|
+
cancelled?: boolean | undefined;
|
|
156
|
+
body: ToolBody;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export type ToolPresentationContext = {
|
|
160
|
+
/** Host-supplied active compute label ("on <label> · …" preview prefix). */
|
|
161
|
+
computeLabel?: string | null | undefined;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/** Prefix a collapsed preview with the host-supplied active compute label. */
|
|
165
|
+
export function withComputePreview(label: string | null | undefined, preview: string): string {
|
|
166
|
+
if (!label) {
|
|
167
|
+
return preview;
|
|
168
|
+
}
|
|
169
|
+
return `on ${label} · ${preview}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function truncatePreview(text: string, max: number): string {
|
|
173
|
+
const cleaned = text.replace(/\s+/g, " ").trim();
|
|
174
|
+
if (!cleaned) {
|
|
175
|
+
return "";
|
|
176
|
+
}
|
|
177
|
+
return cleaned.length > max ? `${cleaned.slice(0, max - 1)}…` : cleaned;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function textPreview(text: string, running?: boolean): ToolPreview {
|
|
181
|
+
return running ? { kind: "text", text, running: true } : { kind: "text", text };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/* ---- exec_command ---------------------------------------------------------- */
|
|
185
|
+
|
|
186
|
+
export function execPresentation(
|
|
187
|
+
item: ToolCallItem,
|
|
188
|
+
context: ToolPresentationContext = {},
|
|
189
|
+
): ToolRowPresentation {
|
|
190
|
+
const args = parseToolArgs(item.arguments);
|
|
191
|
+
const cmd = typeof args.cmd === "string" ? args.cmd : "";
|
|
192
|
+
const workdir = typeof args.workdir === "string" ? args.workdir : null;
|
|
193
|
+
const out = item.output;
|
|
194
|
+
const title = `$ ${cmd}`;
|
|
195
|
+
const computeLabel = context.computeLabel ?? null;
|
|
196
|
+
const base = {
|
|
197
|
+
tool: "exec" as const,
|
|
198
|
+
icon: "terminal" as const,
|
|
199
|
+
title,
|
|
200
|
+
titleMono: true,
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// No output event ever arrived: the turn failed before the output insert —
|
|
204
|
+
// most likely a NUL byte in the command output prevented storage.
|
|
205
|
+
if (item.status === "failed" && out === undefined) {
|
|
206
|
+
return {
|
|
207
|
+
...base,
|
|
208
|
+
iconTone: "failed",
|
|
209
|
+
chip: { tone: "bad", text: "failed" },
|
|
210
|
+
preview: textPreview(
|
|
211
|
+
withComputePreview(computeLabel, "output lost — NUL byte could not be stored"),
|
|
212
|
+
),
|
|
213
|
+
body: {
|
|
214
|
+
kind: "note",
|
|
215
|
+
tone: "error",
|
|
216
|
+
text: "output contained a NUL byte and could not be stored; the turn failed on this tool's output insert — no output event ever arrived.",
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (item.status === "failed" && (out == null || out === "")) {
|
|
222
|
+
return {
|
|
223
|
+
...base,
|
|
224
|
+
iconTone: "failed",
|
|
225
|
+
chip: { tone: "bad", text: "failed" },
|
|
226
|
+
preview: textPreview(withComputePreview(computeLabel, "tool call failed")),
|
|
227
|
+
body: {
|
|
228
|
+
kind: "note",
|
|
229
|
+
tone: "error",
|
|
230
|
+
text: "the tool call failed with no output.",
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (item.status === "running") {
|
|
236
|
+
const streamed = typeof out === "string" ? stripExecBanner(out) : "";
|
|
237
|
+
const runningPreview = streamed ? `${streamed.split("\n").length} lines` : "running…";
|
|
238
|
+
return {
|
|
239
|
+
...base,
|
|
240
|
+
iconTone: "running",
|
|
241
|
+
running: true,
|
|
242
|
+
preview: textPreview(withComputePreview(computeLabel, runningPreview), true),
|
|
243
|
+
body: {
|
|
244
|
+
kind: "term",
|
|
245
|
+
command: null,
|
|
246
|
+
workdir,
|
|
247
|
+
output: streamed,
|
|
248
|
+
live: true,
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const text = typeof out === "string" ? out : stringifyPayload(out);
|
|
254
|
+
const stripped = stripExecBanner(text);
|
|
255
|
+
const bgSession = parseExecBannerSessionId(text);
|
|
256
|
+
const exitCode = sandboxCommandExitCode(text);
|
|
257
|
+
const binary = looksBinary(stripped);
|
|
258
|
+
|
|
259
|
+
// Color is spent on the exception only: a clean exit earns no chip.
|
|
260
|
+
let chip: ToolChip | undefined;
|
|
261
|
+
let iconTone: ToolIconTone = "muted";
|
|
262
|
+
if (bgSession != null) {
|
|
263
|
+
chip = { tone: "muted", text: `session ${bgSession}` };
|
|
264
|
+
} else if (exitCode != null && exitCode !== 0) {
|
|
265
|
+
chip = { tone: "bad", text: `exit ${exitCode}` };
|
|
266
|
+
iconTone = "failed";
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const peek = binary ? "binary output" : tailPeek(stripped) || "(no output)";
|
|
270
|
+
const truncated = execTruncated(text);
|
|
271
|
+
const preview = withComputePreview(computeLabel, truncated ? `⋯ truncated · ${peek}` : peek);
|
|
272
|
+
return {
|
|
273
|
+
...base,
|
|
274
|
+
iconTone,
|
|
275
|
+
...(chip ? { chip } : {}),
|
|
276
|
+
failed: item.status === "failed",
|
|
277
|
+
cancelled: item.status === "cancelled",
|
|
278
|
+
preview: textPreview(preview),
|
|
279
|
+
body: {
|
|
280
|
+
kind: "term",
|
|
281
|
+
command: null,
|
|
282
|
+
workdir,
|
|
283
|
+
output: binary ? "(binary output suppressed)" : stripped,
|
|
284
|
+
failed: item.status === "failed" || (exitCode != null && exitCode !== 0),
|
|
285
|
+
...(bgSession != null
|
|
286
|
+
? {
|
|
287
|
+
note: `↳ session ${bgSession} — a later write_stdin can target this PTY.`,
|
|
288
|
+
}
|
|
289
|
+
: {}),
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/* ---- write_stdin ----------------------------------------------------------- */
|
|
295
|
+
|
|
296
|
+
export function writeStdinPresentation(item: ToolCallItem): ToolRowPresentation {
|
|
297
|
+
const args = parseToolArgs(item.arguments);
|
|
298
|
+
const sessionId =
|
|
299
|
+
typeof args.session_id === "string" || typeof args.session_id === "number"
|
|
300
|
+
? args.session_id
|
|
301
|
+
: undefined;
|
|
302
|
+
const text = typeof item.output === "string" ? item.output : stringifyPayload(item.output);
|
|
303
|
+
const lost = isExecSessionLostBanner(text);
|
|
304
|
+
const keys = controlCaret(typeof args.chars === "string" ? args.chars : "");
|
|
305
|
+
const exitCode = sandboxCommandExitCode(text);
|
|
306
|
+
const stripped = stripExecBanner(text);
|
|
307
|
+
const title = `session ${sessionId} ← ${keys || "∅"}`;
|
|
308
|
+
const base = {
|
|
309
|
+
tool: "write_stdin" as const,
|
|
310
|
+
icon: "keyboard" as const,
|
|
311
|
+
title,
|
|
312
|
+
titleMono: true,
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
if (item.status === "running") {
|
|
316
|
+
return {
|
|
317
|
+
...base,
|
|
318
|
+
iconTone: "running",
|
|
319
|
+
running: true,
|
|
320
|
+
preview: textPreview("sending…", true),
|
|
321
|
+
body: { kind: "note", text: `sending input to session ${sessionId}…` },
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
let chip: ToolChip | undefined;
|
|
326
|
+
if (lost) {
|
|
327
|
+
chip = { tone: "bad", text: "lost" };
|
|
328
|
+
} else if (exitCode != null && exitCode !== 0) {
|
|
329
|
+
chip = { tone: "bad", text: `exit ${exitCode}` };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
...base,
|
|
334
|
+
iconTone: lost ? "failed" : "muted",
|
|
335
|
+
...(chip ? { chip } : {}),
|
|
336
|
+
failed: item.status === "failed",
|
|
337
|
+
cancelled: item.status === "cancelled",
|
|
338
|
+
preview: textPreview(lost ? `session ${sessionId} PTY vanished` : tailPeek(stripped) || "sent"),
|
|
339
|
+
body: lost
|
|
340
|
+
? { kind: "note", tone: "error", text: stripped || text }
|
|
341
|
+
: {
|
|
342
|
+
kind: "term",
|
|
343
|
+
command: `write_stdin → session ${sessionId}`,
|
|
344
|
+
workdir: null,
|
|
345
|
+
output: stripped,
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/* ---- apply_patch ----------------------------------------------------------- */
|
|
351
|
+
|
|
352
|
+
export function patchVerb(op: ApplyPatchOperation | undefined): string {
|
|
353
|
+
if (!op) {
|
|
354
|
+
return "Edited";
|
|
355
|
+
}
|
|
356
|
+
return op.type === "create_file"
|
|
357
|
+
? "Created"
|
|
358
|
+
: op.type === "delete_file"
|
|
359
|
+
? "Deleted"
|
|
360
|
+
: op.moveTo
|
|
361
|
+
? "Renamed"
|
|
362
|
+
: "Edited";
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function pathBasename(path: string): string {
|
|
366
|
+
const parts = path.split("/").filter(Boolean);
|
|
367
|
+
return parts.length ? parts[parts.length - 1]! : path;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export function pathDirname(path: string): string {
|
|
371
|
+
const idx = path.lastIndexOf("/");
|
|
372
|
+
return idx >= 0 ? path.slice(0, idx + 1) : "";
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export function safeParsePatchOp(op: ApplyPatchOperation): GitFileDiff | null {
|
|
376
|
+
try {
|
|
377
|
+
return v4aToGitFileDiff(op);
|
|
378
|
+
} catch {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function patchFiles(
|
|
384
|
+
ops: ApplyPatchOperation[],
|
|
385
|
+
parsed?: Array<GitFileDiff | null>,
|
|
386
|
+
): ToolPatchFile[] {
|
|
387
|
+
return ops.map((op, index) => ({
|
|
388
|
+
key: `${op.type}:${op.path}:${index}`,
|
|
389
|
+
path: op.path,
|
|
390
|
+
file: parsed ? (parsed[index] ?? null) : safeParsePatchOp(op),
|
|
391
|
+
diff: op.diff ?? "",
|
|
392
|
+
}));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** Returns null when a settled patch has no op (renderers fall back to generic). */
|
|
396
|
+
export function applyPatchPresentation(item: ToolCallItem): ToolRowPresentation | null {
|
|
397
|
+
const ops = applyPatchOpsFromToolItem(item);
|
|
398
|
+
const cancelled = item.status === "cancelled";
|
|
399
|
+
const firstOp = ops[0];
|
|
400
|
+
const base = { tool: "apply_patch" as const, icon: "file-diff" as const };
|
|
401
|
+
|
|
402
|
+
if (item.status === "running") {
|
|
403
|
+
// Show the patch structure from the arguments, marked clearly in-progress.
|
|
404
|
+
const fileCount = ops.length;
|
|
405
|
+
const titleVerb = firstOp ? `Applying ${pathBasename(firstOp.path)}` : "Applying patch";
|
|
406
|
+
return {
|
|
407
|
+
...base,
|
|
408
|
+
iconTone: "running",
|
|
409
|
+
title: fileCount > 1 ? `Applying ${fileCount} files` : titleVerb,
|
|
410
|
+
running: true,
|
|
411
|
+
preview: textPreview(
|
|
412
|
+
fileCount > 1 ? `${fileCount} files` : firstOp ? firstOp.path : "applying…",
|
|
413
|
+
true,
|
|
414
|
+
),
|
|
415
|
+
body: { kind: "patch", files: patchFiles(ops) },
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (item.status === "failed") {
|
|
420
|
+
return {
|
|
421
|
+
...base,
|
|
422
|
+
iconTone: "failed",
|
|
423
|
+
title: firstOp ? `${patchVerb(firstOp)} ${pathBasename(firstOp.path)}` : "apply_patch",
|
|
424
|
+
chip: { tone: "bad", text: "failed" },
|
|
425
|
+
preview: textPreview(typeof item.output === "string" ? item.output : "patch failed"),
|
|
426
|
+
body: {
|
|
427
|
+
kind: "payloads",
|
|
428
|
+
blocks: [{ label: "Error", value: item.output, failed: true }],
|
|
429
|
+
},
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Multi-file edit: magnitude stays a single muted glyph pair.
|
|
434
|
+
if (ops.length > 1) {
|
|
435
|
+
const parsed = ops.map((op) => safeParsePatchOp(op));
|
|
436
|
+
const goodFiles = parsed.filter((f): f is GitFileDiff => f !== null);
|
|
437
|
+
const add = goodFiles.reduce((n, f) => n + f.additions, 0);
|
|
438
|
+
const del = goodFiles.reduce((n, f) => n + f.deletions, 0);
|
|
439
|
+
return {
|
|
440
|
+
...base,
|
|
441
|
+
iconTone: "accent",
|
|
442
|
+
title: `Edited ${ops.length} files`,
|
|
443
|
+
cancelled,
|
|
444
|
+
preview: { kind: "files", count: ops.length, add, del },
|
|
445
|
+
body: { kind: "patch", files: patchFiles(ops, parsed) },
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (!firstOp) {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
if (firstOp.type === "delete_file") {
|
|
453
|
+
return {
|
|
454
|
+
...base,
|
|
455
|
+
iconTone: "failed",
|
|
456
|
+
title: `Deleted ${pathBasename(firstOp.path)}`,
|
|
457
|
+
cancelled,
|
|
458
|
+
preview: { kind: "path", path: firstOp.path },
|
|
459
|
+
body: { kind: "note", text: "File deleted — no diff to show." },
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const file = safeParsePatchOp(firstOp);
|
|
464
|
+
if (!file) {
|
|
465
|
+
return {
|
|
466
|
+
...base,
|
|
467
|
+
iconTone: "accent",
|
|
468
|
+
title: `${patchVerb(firstOp)} ${pathBasename(firstOp.path)}`,
|
|
469
|
+
cancelled,
|
|
470
|
+
preview: { kind: "malformed", name: pathBasename(firstOp.path) },
|
|
471
|
+
body: { kind: "patch", files: patchFiles([firstOp], [null]), bare: true },
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
...base,
|
|
477
|
+
iconTone: "accent",
|
|
478
|
+
title: `${patchVerb(firstOp)} ${pathBasename(file.path)}`,
|
|
479
|
+
cancelled,
|
|
480
|
+
preview: {
|
|
481
|
+
kind: "path",
|
|
482
|
+
path: file.path,
|
|
483
|
+
add: file.additions,
|
|
484
|
+
del: file.deletions,
|
|
485
|
+
},
|
|
486
|
+
body: { kind: "patch", files: patchFiles([firstOp], [file]) },
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/* ---- web search ------------------------------------------------------------ */
|
|
491
|
+
|
|
492
|
+
export function webSearchQueryFromArguments(args: unknown): string | null {
|
|
493
|
+
if (typeof args === "string") {
|
|
494
|
+
const trimmed = args.trim();
|
|
495
|
+
if (!trimmed) {
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
try {
|
|
499
|
+
return webSearchQueryFromArguments(JSON.parse(trimmed));
|
|
500
|
+
} catch {
|
|
501
|
+
return trimmed;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (!args || typeof args !== "object") {
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
const record = args as Record<string, unknown>;
|
|
508
|
+
if (typeof record.query === "string" && record.query.trim().length > 0) {
|
|
509
|
+
return record.query;
|
|
510
|
+
}
|
|
511
|
+
if (Array.isArray(record.queries)) {
|
|
512
|
+
const first = record.queries.find(
|
|
513
|
+
(value): value is string => typeof value === "string" && value.trim().length > 0,
|
|
514
|
+
);
|
|
515
|
+
if (first) {
|
|
516
|
+
return first;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export function webSearchPresentation(item: ToolCallItem): ToolRowPresentation {
|
|
523
|
+
const raw = (item.raw ?? {}) as {
|
|
524
|
+
providerData?: {
|
|
525
|
+
action?: {
|
|
526
|
+
type?: string;
|
|
527
|
+
query?: string;
|
|
528
|
+
queries?: string[];
|
|
529
|
+
url?: string;
|
|
530
|
+
pattern?: string;
|
|
531
|
+
};
|
|
532
|
+
};
|
|
533
|
+
};
|
|
534
|
+
const action = raw.providerData?.action ?? {};
|
|
535
|
+
const actionType = action.type ?? "search";
|
|
536
|
+
const queries = (action.queries ?? []).filter(
|
|
537
|
+
(value): value is string => typeof value === "string" && value.trim().length > 0,
|
|
538
|
+
);
|
|
539
|
+
const searchQuery =
|
|
540
|
+
(typeof action.query === "string" && action.query.trim().length > 0 ? action.query : null) ??
|
|
541
|
+
queries[0] ??
|
|
542
|
+
webSearchQueryFromArguments(item.arguments);
|
|
543
|
+
const running = item.status === "running";
|
|
544
|
+
const query =
|
|
545
|
+
actionType === "open_page"
|
|
546
|
+
? (action.url ?? "(page unavailable)")
|
|
547
|
+
: actionType === "find_in_page"
|
|
548
|
+
? action.pattern && action.url
|
|
549
|
+
? `"${action.pattern}" in ${action.url}`
|
|
550
|
+
: (action.pattern ?? action.url ?? "(page unavailable)")
|
|
551
|
+
: // Codex often emits the live card before action.query/queries land.
|
|
552
|
+
(searchQuery ?? (running ? "…" : "(query unavailable)"));
|
|
553
|
+
const variants = queries.length > 1 ? ` +${queries.length - 1} variants` : "";
|
|
554
|
+
const base = { tool: "web_search" as const, icon: "search" as const };
|
|
555
|
+
|
|
556
|
+
if (running) {
|
|
557
|
+
return {
|
|
558
|
+
...base,
|
|
559
|
+
iconTone: "running",
|
|
560
|
+
title:
|
|
561
|
+
actionType === "open_page"
|
|
562
|
+
? "Opening web page"
|
|
563
|
+
: actionType === "find_in_page"
|
|
564
|
+
? "Searching within page"
|
|
565
|
+
: "Searching the web",
|
|
566
|
+
running: true,
|
|
567
|
+
preview: textPreview(`${query}${variants}`, true),
|
|
568
|
+
body: {
|
|
569
|
+
kind: "note",
|
|
570
|
+
text: "searching… results fold into the model context (no output event).",
|
|
571
|
+
},
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const rawResults = (item.output as { results?: unknown } | undefined)?.results;
|
|
576
|
+
const results = Array.isArray(rawResults)
|
|
577
|
+
? (rawResults as unknown[]).filter((r): r is WebSearchResult => !!r && typeof r === "object")
|
|
578
|
+
: null;
|
|
579
|
+
return {
|
|
580
|
+
...base,
|
|
581
|
+
iconTone: "muted",
|
|
582
|
+
title:
|
|
583
|
+
actionType === "open_page"
|
|
584
|
+
? "Opened web page"
|
|
585
|
+
: actionType === "find_in_page"
|
|
586
|
+
? "Searched within page"
|
|
587
|
+
: "Searched the web",
|
|
588
|
+
preview: textPreview(`${query}${variants}`),
|
|
589
|
+
failed: item.status === "failed",
|
|
590
|
+
cancelled: item.status === "cancelled",
|
|
591
|
+
body: { kind: "web-results", results },
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/* ---- MCP-shaped tools: ask, run_on, generic -------------------------------- */
|
|
596
|
+
|
|
597
|
+
export function askToolPreview(args: unknown): string | null {
|
|
598
|
+
const record = args && typeof args === "object" ? (args as Record<string, unknown>) : null;
|
|
599
|
+
const questions = Array.isArray(record?.questions) ? record.questions : null;
|
|
600
|
+
if (!questions || questions.length === 0) {
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
const first = questions[0];
|
|
604
|
+
if (!first || typeof first !== "object") {
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
const q = first as Record<string, unknown>;
|
|
608
|
+
const text =
|
|
609
|
+
typeof q.label === "string" && q.label.trim()
|
|
610
|
+
? q.label.trim()
|
|
611
|
+
: typeof q.prompt === "string" && q.prompt.trim()
|
|
612
|
+
? q.prompt.trim()
|
|
613
|
+
: null;
|
|
614
|
+
if (!text) {
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
const preview = truncatePreview(text, 90);
|
|
618
|
+
return questions.length > 1 ? `${preview} · ${questions.length} questions` : preview;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
export function runOnTargetName(output: unknown): string | null {
|
|
622
|
+
const { text } = unwrapMcpOutput(output);
|
|
623
|
+
const parsed = tryParseJson(text);
|
|
624
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
625
|
+
const name = (parsed as { targetName?: unknown }).targetName;
|
|
626
|
+
if (typeof name === "string" && name.trim()) {
|
|
627
|
+
return name.trim();
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
export function runOnOpPreview(args: Record<string, unknown>): string | null {
|
|
634
|
+
const op = args.op;
|
|
635
|
+
if (!op || typeof op !== "object" || Array.isArray(op)) {
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
const record = op as Record<string, unknown>;
|
|
639
|
+
if (record.kind === "exec" && typeof record.cmd === "string" && record.cmd.trim()) {
|
|
640
|
+
return `$ ${record.cmd.trim()}`;
|
|
641
|
+
}
|
|
642
|
+
if (
|
|
643
|
+
(record.kind === "read" || record.kind === "write") &&
|
|
644
|
+
typeof record.path === "string" &&
|
|
645
|
+
record.path.trim()
|
|
646
|
+
) {
|
|
647
|
+
return truncatePreview(record.path.trim(), 72);
|
|
648
|
+
}
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
export function goalToolPreview(name: string, args: unknown): string | null {
|
|
653
|
+
const leaf = mcpToolLeaf(name);
|
|
654
|
+
if (
|
|
655
|
+
leaf !== "goal_set" &&
|
|
656
|
+
leaf !== "goal_update" &&
|
|
657
|
+
leaf !== "goal_complete" &&
|
|
658
|
+
leaf !== "goal_pause" &&
|
|
659
|
+
leaf !== "wait_for_input"
|
|
660
|
+
) {
|
|
661
|
+
return null;
|
|
662
|
+
}
|
|
663
|
+
const record = args && typeof args === "object" ? (args as Record<string, unknown>) : null;
|
|
664
|
+
if (!record) {
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
const text =
|
|
668
|
+
typeof record.text === "string"
|
|
669
|
+
? record.text
|
|
670
|
+
: typeof record.evidence === "string"
|
|
671
|
+
? record.evidence
|
|
672
|
+
: typeof record.rationale === "string"
|
|
673
|
+
? record.rationale
|
|
674
|
+
: typeof record.reason === "string"
|
|
675
|
+
? record.reason
|
|
676
|
+
: typeof record.progressNote === "string"
|
|
677
|
+
? record.progressNote
|
|
678
|
+
: null;
|
|
679
|
+
return text ? truncatePreview(text, 90) : null;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/** Explicit first-party leaf/prefix → canonical product icon kind. */
|
|
683
|
+
export function genericToolIconKind(name: string): ToolIconKind {
|
|
684
|
+
const leaf = mcpToolLeaf(name);
|
|
685
|
+
return leaf === "request_human_input"
|
|
686
|
+
? "question"
|
|
687
|
+
: leaf.startsWith("goal_")
|
|
688
|
+
? "target"
|
|
689
|
+
: leaf.startsWith("memory_") ||
|
|
690
|
+
leaf === "preference_registry_summary" ||
|
|
691
|
+
leaf === "preference_registry_get"
|
|
692
|
+
? "brain"
|
|
693
|
+
: leaf.startsWith("session_") ||
|
|
694
|
+
leaf === "sessions_list" ||
|
|
695
|
+
leaf === "set_session_title" ||
|
|
696
|
+
leaf === "set_other_session_title"
|
|
697
|
+
? "sessions"
|
|
698
|
+
: leaf.startsWith("sandbox") || leaf === "sandboxes_list" || leaf === "run_on"
|
|
699
|
+
? "server"
|
|
700
|
+
: leaf.startsWith("rig_")
|
|
701
|
+
? "server-cog"
|
|
702
|
+
: leaf.startsWith("scheduled_")
|
|
703
|
+
? "calendar"
|
|
704
|
+
: leaf.startsWith("artifacts_")
|
|
705
|
+
? "panels"
|
|
706
|
+
: leaf.startsWith("social_")
|
|
707
|
+
? "share"
|
|
708
|
+
: leaf.startsWith("slack_")
|
|
709
|
+
? "message"
|
|
710
|
+
: leaf.startsWith("github_")
|
|
711
|
+
? "git"
|
|
712
|
+
: leaf.startsWith("variable_")
|
|
713
|
+
? "box"
|
|
714
|
+
: leaf.startsWith("environment_")
|
|
715
|
+
? "key"
|
|
716
|
+
: leaf.includes("document") ||
|
|
717
|
+
leaf.includes("knowledge") ||
|
|
718
|
+
leaf === "list_document_bases"
|
|
719
|
+
? "file-search"
|
|
720
|
+
: leaf === "tool_search"
|
|
721
|
+
? "package-search"
|
|
722
|
+
: leaf.startsWith("skill_")
|
|
723
|
+
? "plug"
|
|
724
|
+
: "wrench";
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
type McpRowOptions = {
|
|
728
|
+
tool: PresentedToolKind;
|
|
729
|
+
icon: ToolIconKind;
|
|
730
|
+
title: string;
|
|
731
|
+
/** Preview while running (falls back to `runningFallback`). */
|
|
732
|
+
runningPreview: string | null;
|
|
733
|
+
runningFallback: string;
|
|
734
|
+
/** Settled preview (null → none); ask/run_on also use it as the error fallback. */
|
|
735
|
+
settledPreview: string | null;
|
|
736
|
+
/** Generic shows "Done" when no settled preview exists and skips the error fallback. */
|
|
737
|
+
settledFallback?: string | undefined;
|
|
738
|
+
/** Ask/run_on omit an empty Result block. */
|
|
739
|
+
omitEmptyResult?: boolean | undefined;
|
|
740
|
+
/** Generic shows a goal preview as plain text (no pulse) while running. */
|
|
741
|
+
plainRunningPreview?: boolean | undefined;
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
function mcpRowPresentation(item: ToolCallItem, options: McpRowOptions): ToolRowPresentation {
|
|
745
|
+
const args = parseToolArgs(item.arguments);
|
|
746
|
+
const base = { tool: options.tool, icon: options.icon, title: options.title };
|
|
747
|
+
if (item.status === "running") {
|
|
748
|
+
return {
|
|
749
|
+
...base,
|
|
750
|
+
iconTone: "running",
|
|
751
|
+
running: true,
|
|
752
|
+
preview:
|
|
753
|
+
options.runningPreview != null && options.plainRunningPreview
|
|
754
|
+
? textPreview(options.runningPreview)
|
|
755
|
+
: textPreview(options.runningPreview ?? options.runningFallback, true),
|
|
756
|
+
body: { kind: "payloads", blocks: [{ label: "Arguments", value: args }] },
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
const { text: outText, isError } = unwrapMcpOutput(item.output);
|
|
760
|
+
// Cancelled is not an error, even if the payload carries an isError flag.
|
|
761
|
+
if ((isError || item.status === "failed") && item.status !== "cancelled") {
|
|
762
|
+
const errorFallback = options.settledFallback === undefined ? options.settledPreview : null;
|
|
763
|
+
return {
|
|
764
|
+
...base,
|
|
765
|
+
iconTone: "failed",
|
|
766
|
+
chip: { tone: "bad", text: "error" },
|
|
767
|
+
preview: textPreview(truncatePreview(outText, 80) || errorFallback || "Error"),
|
|
768
|
+
body: {
|
|
769
|
+
kind: "payloads",
|
|
770
|
+
blocks: [
|
|
771
|
+
{ label: "Arguments", value: args },
|
|
772
|
+
{ label: "Error", value: outText, failed: true },
|
|
773
|
+
],
|
|
774
|
+
},
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
const settled =
|
|
778
|
+
item.status === "cancelled"
|
|
779
|
+
? null
|
|
780
|
+
: (options.settledPreview ?? options.settledFallback ?? null);
|
|
781
|
+
return {
|
|
782
|
+
...base,
|
|
783
|
+
iconTone: "muted",
|
|
784
|
+
cancelled: item.status === "cancelled",
|
|
785
|
+
...(settled != null ? { preview: textPreview(settled) } : {}),
|
|
786
|
+
body: {
|
|
787
|
+
kind: "payloads",
|
|
788
|
+
blocks:
|
|
789
|
+
options.omitEmptyResult && !outText
|
|
790
|
+
? [{ label: "Arguments", value: args }]
|
|
791
|
+
: [
|
|
792
|
+
{ label: "Arguments", value: args },
|
|
793
|
+
{ label: "Result", value: outText },
|
|
794
|
+
],
|
|
795
|
+
},
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
export function askPresentation(item: ToolCallItem): ToolRowPresentation {
|
|
800
|
+
const preview = askToolPreview(parseToolArgs(item.arguments));
|
|
801
|
+
return mcpRowPresentation(item, {
|
|
802
|
+
tool: "ask",
|
|
803
|
+
icon: "question",
|
|
804
|
+
title: "Ask",
|
|
805
|
+
runningPreview: preview,
|
|
806
|
+
runningFallback: "Waiting…",
|
|
807
|
+
settledPreview: preview,
|
|
808
|
+
omitEmptyResult: true,
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
export function runOnPresentation(item: ToolCallItem): ToolRowPresentation {
|
|
813
|
+
const targetName = runOnTargetName(item.output);
|
|
814
|
+
const opPreview = runOnOpPreview(parseToolArgs(item.arguments));
|
|
815
|
+
return mcpRowPresentation(item, {
|
|
816
|
+
tool: "run_on",
|
|
817
|
+
icon: "server",
|
|
818
|
+
title: targetName ? `Run on ${targetName}` : "Run on",
|
|
819
|
+
runningPreview: opPreview,
|
|
820
|
+
runningFallback: "Running…",
|
|
821
|
+
settledPreview: opPreview,
|
|
822
|
+
omitEmptyResult: true,
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Baseline craft for unmatched tools: family icon + title-cased leaf + honest
|
|
828
|
+
* status preview (Running… / Done / error snippet).
|
|
829
|
+
*/
|
|
830
|
+
export function genericToolPresentation(item: ToolCallItem): ToolRowPresentation {
|
|
831
|
+
const goalPreview = goalToolPreview(item.name, parseToolArgs(item.arguments));
|
|
832
|
+
if (item.status !== "running") {
|
|
833
|
+
const { text: outText, isError } = unwrapMcpOutput(item.output);
|
|
834
|
+
const base = {
|
|
835
|
+
tool: "generic" as const,
|
|
836
|
+
icon: genericToolIconKind(item.name),
|
|
837
|
+
title: toolDisplayName(item.name, item.display),
|
|
838
|
+
};
|
|
839
|
+
const args = parseToolArgs(item.arguments);
|
|
840
|
+
// A permission Block is a decision, not a tool failure: say so plainly instead
|
|
841
|
+
// of the generic "error occurred, please try again" wrapper.
|
|
842
|
+
if (isError && BLOCKED_OUTPUT.test(outText)) {
|
|
843
|
+
return {
|
|
844
|
+
...base,
|
|
845
|
+
iconTone: "muted",
|
|
846
|
+
chip: { tone: "interrupted", text: "blocked" },
|
|
847
|
+
preview: textPreview("Blocked by your permission settings"),
|
|
848
|
+
body: {
|
|
849
|
+
kind: "payloads",
|
|
850
|
+
note: "Your permission settings block this action, so it did not run. You can change this in the integration's tool permissions.",
|
|
851
|
+
blocks: [{ label: "Arguments", value: args }],
|
|
852
|
+
},
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
// An interrupted approved action may have run. Never invite a blind retry.
|
|
856
|
+
if (isError && UNCERTAIN_OUTPUT.test(outText)) {
|
|
857
|
+
return {
|
|
858
|
+
...base,
|
|
859
|
+
iconTone: "failed",
|
|
860
|
+
chip: { tone: "bad", text: "outcome unknown" },
|
|
861
|
+
preview: textPreview("It may have run"),
|
|
862
|
+
body: {
|
|
863
|
+
kind: "payloads",
|
|
864
|
+
note: "This action may have run. Check the result in the connected app before trying again.",
|
|
865
|
+
blocks: [{ label: "Arguments", value: args }],
|
|
866
|
+
},
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
return mcpRowPresentation(item, {
|
|
871
|
+
tool: "generic",
|
|
872
|
+
icon: genericToolIconKind(item.name),
|
|
873
|
+
title: toolDisplayName(item.name, item.display),
|
|
874
|
+
runningPreview: goalPreview,
|
|
875
|
+
runningFallback: "Running…",
|
|
876
|
+
settledPreview: goalPreview,
|
|
877
|
+
settledFallback: "Done",
|
|
878
|
+
plainRunningPreview: true,
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/** The runtime's exact refusal for a tool whose effective permission is Block. */
|
|
883
|
+
const BLOCKED_OUTPUT = /Connector action was not executed: blocked\b/;
|
|
884
|
+
/** The runtime's refusal to guess whether an approved action ran. */
|
|
885
|
+
const UNCERTAIN_OUTPUT = /Connector action outcome is uncertain\b/;
|
|
886
|
+
|
|
887
|
+
/* ---- tool_search (progressive MCP disclosure) ------------------------------ */
|
|
888
|
+
|
|
889
|
+
export type DisclosedTool = {
|
|
890
|
+
/** Full wire name (`server__leaf` or bare). */
|
|
891
|
+
name: string;
|
|
892
|
+
/** Server / namespace prefix before `__`, when present. */
|
|
893
|
+
source: string | null;
|
|
894
|
+
/** Leaf tool name after `__`. */
|
|
895
|
+
leaf: string;
|
|
896
|
+
};
|
|
897
|
+
|
|
898
|
+
export function splitToolWireName(name: string): DisclosedTool {
|
|
899
|
+
const boundary = name.indexOf("__");
|
|
900
|
+
if (boundary <= 0) {
|
|
901
|
+
return { name, source: null, leaf: name };
|
|
902
|
+
}
|
|
903
|
+
return {
|
|
904
|
+
name,
|
|
905
|
+
source: name.slice(0, boundary),
|
|
906
|
+
leaf: name.slice(boundary + 2),
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/** Capability query from live tool_search args (object or JSON string). */
|
|
911
|
+
export function toolSearchQuery(item: ToolCallItem): string {
|
|
912
|
+
const fromArgs = parseToolArgs(item.arguments);
|
|
913
|
+
if (typeof fromArgs.query === "string" && fromArgs.query.trim()) {
|
|
914
|
+
return fromArgs.query.trim();
|
|
915
|
+
}
|
|
916
|
+
const raw = item.raw;
|
|
917
|
+
if (raw && typeof raw === "object") {
|
|
918
|
+
const rawArgs = (raw as { arguments?: unknown }).arguments;
|
|
919
|
+
if (typeof rawArgs === "string" && rawArgs.trim()) {
|
|
920
|
+
const parsed = tryParseJson(rawArgs);
|
|
921
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
922
|
+
const query = (parsed as { query?: unknown }).query;
|
|
923
|
+
if (typeof query === "string" && query.trim()) {
|
|
924
|
+
return query.trim();
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
} else if (rawArgs && typeof rawArgs === "object" && !Array.isArray(rawArgs)) {
|
|
928
|
+
const query = (rawArgs as { query?: unknown }).query;
|
|
929
|
+
if (typeof query === "string" && query.trim()) {
|
|
930
|
+
return query.trim();
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
return "";
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* Parse disclosed tools from the runtime event shape.
|
|
939
|
+
* `normalizeSdkEvent` collapses `tool_search_output.tools[]` into text:
|
|
940
|
+
* "Disclosed tools: a, b" | "No matching tools found."
|
|
941
|
+
* Also accept a structured `tools` array when a host/enricher preserves it.
|
|
942
|
+
*/
|
|
943
|
+
export function parseDisclosedTools(output: unknown): DisclosedTool[] | null {
|
|
944
|
+
if (output && typeof output === "object" && !Array.isArray(output)) {
|
|
945
|
+
const tools = (output as { tools?: unknown }).tools;
|
|
946
|
+
if (Array.isArray(tools)) {
|
|
947
|
+
return tools
|
|
948
|
+
.map((tool) => {
|
|
949
|
+
if (typeof tool === "string" && tool.trim()) {
|
|
950
|
+
return splitToolWireName(tool.trim());
|
|
951
|
+
}
|
|
952
|
+
if (
|
|
953
|
+
tool &&
|
|
954
|
+
typeof tool === "object" &&
|
|
955
|
+
typeof (tool as { name?: unknown }).name === "string"
|
|
956
|
+
) {
|
|
957
|
+
const name = (tool as { name: string }).name.trim();
|
|
958
|
+
return name ? splitToolWireName(name) : null;
|
|
959
|
+
}
|
|
960
|
+
return null;
|
|
961
|
+
})
|
|
962
|
+
.filter((tool): tool is DisclosedTool => tool != null);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
const { text } = unwrapMcpOutput(output);
|
|
967
|
+
const trimmed = text.trim();
|
|
968
|
+
if (!trimmed) {
|
|
969
|
+
return null;
|
|
970
|
+
}
|
|
971
|
+
if (/^no matching tools found\.?$/i.test(trimmed)) {
|
|
972
|
+
return [];
|
|
973
|
+
}
|
|
974
|
+
const disclosed = trimmed.match(/^disclosed tools:\s*(.+)$/i);
|
|
975
|
+
if (disclosed?.[1]) {
|
|
976
|
+
return disclosed[1]
|
|
977
|
+
.split(",")
|
|
978
|
+
.map((part) => part.trim())
|
|
979
|
+
.filter(Boolean)
|
|
980
|
+
.map(splitToolWireName);
|
|
981
|
+
}
|
|
982
|
+
const parsed = tryParseJson(trimmed);
|
|
983
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
984
|
+
return parseDisclosedTools(parsed);
|
|
985
|
+
}
|
|
986
|
+
return null;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
export function toolSearchPreview(
|
|
990
|
+
tools: DisclosedTool[] | null,
|
|
991
|
+
cancelled: boolean,
|
|
992
|
+
): string | undefined {
|
|
993
|
+
if (cancelled) {
|
|
994
|
+
return undefined;
|
|
995
|
+
}
|
|
996
|
+
if (!tools) {
|
|
997
|
+
return "Done";
|
|
998
|
+
}
|
|
999
|
+
if (tools.length === 0) {
|
|
1000
|
+
return "No matches";
|
|
1001
|
+
}
|
|
1002
|
+
if (tools.length === 1) {
|
|
1003
|
+
return tools[0]!.leaf;
|
|
1004
|
+
}
|
|
1005
|
+
const head = tools[0]!.leaf;
|
|
1006
|
+
return `${tools.length} tools · ${truncatePreview(head, 28)}`;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
export function toolSearchPresentation(item: ToolCallItem): ToolRowPresentation {
|
|
1010
|
+
const query = toolSearchQuery(item);
|
|
1011
|
+
const queryPreview = query ? truncatePreview(query, 64) : "";
|
|
1012
|
+
const note = query ? `capability query: ${query}` : undefined;
|
|
1013
|
+
const args = parseToolArgs(item.arguments);
|
|
1014
|
+
const base = { tool: "tool_search" as const, icon: "package-search" as const };
|
|
1015
|
+
if (item.status === "running") {
|
|
1016
|
+
return {
|
|
1017
|
+
...base,
|
|
1018
|
+
iconTone: "running",
|
|
1019
|
+
title: "Looking up tools",
|
|
1020
|
+
running: true,
|
|
1021
|
+
preview: textPreview(queryPreview || "Matching capabilities…", true),
|
|
1022
|
+
body: { kind: "listing", note, entries: [], blocks: [{ label: "Arguments", value: args }] },
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
const { text: outText, isError } = unwrapMcpOutput(item.output);
|
|
1026
|
+
if ((isError || item.status === "failed") && item.status !== "cancelled") {
|
|
1027
|
+
return {
|
|
1028
|
+
...base,
|
|
1029
|
+
iconTone: "failed",
|
|
1030
|
+
title: "Tool lookup failed",
|
|
1031
|
+
failed: true,
|
|
1032
|
+
preview: textPreview(truncatePreview(outText, 80) || queryPreview || "Lookup failed"),
|
|
1033
|
+
body: {
|
|
1034
|
+
kind: "listing",
|
|
1035
|
+
note,
|
|
1036
|
+
entries: [],
|
|
1037
|
+
blocks: [
|
|
1038
|
+
{ label: "Arguments", value: args },
|
|
1039
|
+
{ label: "Error", value: outText, failed: true },
|
|
1040
|
+
],
|
|
1041
|
+
},
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
const tools = parseDisclosedTools(item.output);
|
|
1045
|
+
const preview = toolSearchPreview(tools, item.status === "cancelled");
|
|
1046
|
+
return {
|
|
1047
|
+
...base,
|
|
1048
|
+
iconTone: "muted",
|
|
1049
|
+
title: "Looked up tools",
|
|
1050
|
+
cancelled: item.status === "cancelled",
|
|
1051
|
+
...(preview ? { preview: textPreview(preview) } : {}),
|
|
1052
|
+
body: {
|
|
1053
|
+
kind: "listing",
|
|
1054
|
+
note,
|
|
1055
|
+
entries: (tools ?? []).slice(0, 12).map((tool) => ({
|
|
1056
|
+
key: tool.name,
|
|
1057
|
+
title: tool.leaf,
|
|
1058
|
+
eyebrow: tool.source,
|
|
1059
|
+
mono: true,
|
|
1060
|
+
})),
|
|
1061
|
+
more: tools && tools.length > 12 ? tools.length - 12 : undefined,
|
|
1062
|
+
empty:
|
|
1063
|
+
tools && tools.length === 0
|
|
1064
|
+
? "no deferred tools matched this capability query."
|
|
1065
|
+
: undefined,
|
|
1066
|
+
blocks:
|
|
1067
|
+
tools == null && outText
|
|
1068
|
+
? [
|
|
1069
|
+
{ label: "Arguments", value: args },
|
|
1070
|
+
{ label: "Result", value: outText },
|
|
1071
|
+
]
|
|
1072
|
+
: [{ label: "Arguments", value: args }],
|
|
1073
|
+
},
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
/* ---- docs / knowledge search ----------------------------------------------- */
|
|
1078
|
+
|
|
1079
|
+
export type SearchHit = { title: string; snippet: string };
|
|
1080
|
+
|
|
1081
|
+
export function parseSearchHits(outText: string): SearchHit[] | null {
|
|
1082
|
+
const parsed = tryParseJson(outText);
|
|
1083
|
+
if (parsed == null) {
|
|
1084
|
+
return null;
|
|
1085
|
+
}
|
|
1086
|
+
const list = Array.isArray(parsed)
|
|
1087
|
+
? parsed
|
|
1088
|
+
: parsed &&
|
|
1089
|
+
typeof parsed === "object" &&
|
|
1090
|
+
Array.isArray((parsed as { results?: unknown }).results)
|
|
1091
|
+
? (parsed as { results: unknown[] }).results
|
|
1092
|
+
: parsed && typeof parsed === "object" && Array.isArray((parsed as { hits?: unknown }).hits)
|
|
1093
|
+
? (parsed as { hits: unknown[] }).hits
|
|
1094
|
+
: null;
|
|
1095
|
+
if (!list) {
|
|
1096
|
+
return null;
|
|
1097
|
+
}
|
|
1098
|
+
return list.map((row) => {
|
|
1099
|
+
const r = row && typeof row === "object" ? (row as Record<string, unknown>) : {};
|
|
1100
|
+
const title =
|
|
1101
|
+
(typeof r.title === "string" && r.title) ||
|
|
1102
|
+
(typeof r.name === "string" && r.name) ||
|
|
1103
|
+
(typeof r.documentTitle === "string" && r.documentTitle) ||
|
|
1104
|
+
(typeof r.path === "string" && r.path) ||
|
|
1105
|
+
(typeof r.id === "string" && r.id) ||
|
|
1106
|
+
"Result";
|
|
1107
|
+
const snippet =
|
|
1108
|
+
(typeof r.snippet === "string" && r.snippet) ||
|
|
1109
|
+
(typeof r.text === "string" && r.text) ||
|
|
1110
|
+
(typeof r.content === "string" && r.content) ||
|
|
1111
|
+
"";
|
|
1112
|
+
return { title, snippet: truncatePreview(snippet, 160) };
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
export function docsSearchPresentation(item: ToolCallItem): ToolRowPresentation {
|
|
1117
|
+
const args = parseToolArgs(item.arguments);
|
|
1118
|
+
const query = typeof args.query === "string" ? args.query.trim() : "";
|
|
1119
|
+
const base = {
|
|
1120
|
+
tool: "docs_search" as const,
|
|
1121
|
+
icon: "file-search" as const,
|
|
1122
|
+
title: query
|
|
1123
|
+
? `Search \u201c${truncatePreview(query, 48)}\u201d`
|
|
1124
|
+
: toolDisplayName(item.name, item.display),
|
|
1125
|
+
};
|
|
1126
|
+
if (item.status === "running") {
|
|
1127
|
+
return {
|
|
1128
|
+
...base,
|
|
1129
|
+
iconTone: "running",
|
|
1130
|
+
running: true,
|
|
1131
|
+
preview: textPreview("Searching…", true),
|
|
1132
|
+
body: { kind: "payloads", blocks: [{ label: "Arguments", value: args }] },
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
const { text: outText, isError } = unwrapMcpOutput(item.output);
|
|
1136
|
+
if ((isError || item.status === "failed") && item.status !== "cancelled") {
|
|
1137
|
+
return {
|
|
1138
|
+
...base,
|
|
1139
|
+
iconTone: "failed",
|
|
1140
|
+
failed: true,
|
|
1141
|
+
preview: textPreview(truncatePreview(outText, 80) || "Search failed"),
|
|
1142
|
+
body: {
|
|
1143
|
+
kind: "payloads",
|
|
1144
|
+
blocks: [
|
|
1145
|
+
{ label: "Arguments", value: args },
|
|
1146
|
+
{ label: "Error", value: outText, failed: true },
|
|
1147
|
+
],
|
|
1148
|
+
},
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
const hits = parseSearchHits(outText);
|
|
1152
|
+
const preview =
|
|
1153
|
+
item.status === "cancelled"
|
|
1154
|
+
? undefined
|
|
1155
|
+
: hits
|
|
1156
|
+
? hits.length === 0
|
|
1157
|
+
? "No hits"
|
|
1158
|
+
: `${hits.length} hit${hits.length === 1 ? "" : "s"}`
|
|
1159
|
+
: "Done";
|
|
1160
|
+
return {
|
|
1161
|
+
...base,
|
|
1162
|
+
iconTone: "muted",
|
|
1163
|
+
cancelled: item.status === "cancelled",
|
|
1164
|
+
...(preview ? { preview: textPreview(preview) } : {}),
|
|
1165
|
+
body: {
|
|
1166
|
+
kind: "listing",
|
|
1167
|
+
entries: (hits ?? []).slice(0, 8).map((hit) => ({
|
|
1168
|
+
key: `${hit.title}\u0000${hit.snippet}`,
|
|
1169
|
+
title: hit.title,
|
|
1170
|
+
snippet: hit.snippet || undefined,
|
|
1171
|
+
})),
|
|
1172
|
+
blocks: [
|
|
1173
|
+
{ label: "Arguments", value: args },
|
|
1174
|
+
{ label: "Result", value: outText },
|
|
1175
|
+
],
|
|
1176
|
+
},
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
/* ---- set_session_title / set_other_session_title --------------------------- */
|
|
1181
|
+
|
|
1182
|
+
export function sessionTitlePresentation(item: ToolCallItem): ToolRowPresentation {
|
|
1183
|
+
const args = parseToolArgs(item.arguments);
|
|
1184
|
+
const titleArg = typeof args.title === "string" ? args.title.trim() : "";
|
|
1185
|
+
const previewTitle = titleArg ? truncatePreview(titleArg, 72) : "";
|
|
1186
|
+
const base = {
|
|
1187
|
+
tool: "session_title" as const,
|
|
1188
|
+
icon: "sessions" as const,
|
|
1189
|
+
title: toolDisplayName(item.name, item.display),
|
|
1190
|
+
};
|
|
1191
|
+
if (item.status === "running") {
|
|
1192
|
+
return {
|
|
1193
|
+
...base,
|
|
1194
|
+
iconTone: "running",
|
|
1195
|
+
running: true,
|
|
1196
|
+
preview: textPreview(previewTitle || "Setting title…", true),
|
|
1197
|
+
body: { kind: "payloads", blocks: [{ label: "Arguments", value: args }] },
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
const { text: outText, isError } = unwrapMcpOutput(item.output);
|
|
1201
|
+
if ((isError || item.status === "failed") && item.status !== "cancelled") {
|
|
1202
|
+
return {
|
|
1203
|
+
...base,
|
|
1204
|
+
iconTone: "failed",
|
|
1205
|
+
failed: true,
|
|
1206
|
+
preview: textPreview(truncatePreview(outText, 80) || "Rename failed"),
|
|
1207
|
+
body: {
|
|
1208
|
+
kind: "payloads",
|
|
1209
|
+
blocks: [
|
|
1210
|
+
{ label: "Arguments", value: args },
|
|
1211
|
+
{ label: "Error", value: outText, failed: true },
|
|
1212
|
+
],
|
|
1213
|
+
},
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
// Prefer the submitted title; fall back to a title field in the tool result.
|
|
1217
|
+
let settledTitle = previewTitle;
|
|
1218
|
+
if (!settledTitle) {
|
|
1219
|
+
const parsed = tryParseJson(outText);
|
|
1220
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1221
|
+
const fromResult = (parsed as { title?: unknown }).title;
|
|
1222
|
+
if (typeof fromResult === "string" && fromResult.trim()) {
|
|
1223
|
+
settledTitle = truncatePreview(fromResult.trim(), 72);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
return {
|
|
1228
|
+
...base,
|
|
1229
|
+
iconTone: "muted",
|
|
1230
|
+
cancelled: item.status === "cancelled",
|
|
1231
|
+
...(item.status !== "cancelled" && settledTitle ? { preview: textPreview(settledTitle) } : {}),
|
|
1232
|
+
body: {
|
|
1233
|
+
kind: "payloads",
|
|
1234
|
+
blocks: outText
|
|
1235
|
+
? [
|
|
1236
|
+
{ label: "Arguments", value: args },
|
|
1237
|
+
{ label: "Result", value: outText },
|
|
1238
|
+
]
|
|
1239
|
+
: [{ label: "Arguments", value: args }],
|
|
1240
|
+
},
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
/* ---- resolution ------------------------------------------------------------ */
|
|
1245
|
+
|
|
1246
|
+
const PRESENTERS_BY_RAW_TYPE: Record<string, PresentedToolKind> = {
|
|
1247
|
+
apply_patch_call: "apply_patch",
|
|
1248
|
+
// Web renders these with richer media renderers; keep them off name matches.
|
|
1249
|
+
computer_call: "generic",
|
|
1250
|
+
tool_search_call: "tool_search",
|
|
1251
|
+
};
|
|
1252
|
+
|
|
1253
|
+
const PRESENTERS_BY_NAME: Record<string, PresentedToolKind> = {
|
|
1254
|
+
exec_command: "exec",
|
|
1255
|
+
request_human_input: "ask",
|
|
1256
|
+
run_on: "run_on",
|
|
1257
|
+
write_stdin: "write_stdin",
|
|
1258
|
+
apply_patch_call: "apply_patch",
|
|
1259
|
+
apply_patch: "apply_patch",
|
|
1260
|
+
web_search_call: "web_search",
|
|
1261
|
+
tool_search: "tool_search",
|
|
1262
|
+
search_documents: "docs_search",
|
|
1263
|
+
knowledge_search: "docs_search",
|
|
1264
|
+
set_session_title: "session_title",
|
|
1265
|
+
set_other_session_title: "session_title",
|
|
1266
|
+
};
|
|
1267
|
+
|
|
1268
|
+
/**
|
|
1269
|
+
* Resolve the shared presenter for a call with the web registry's precedence
|
|
1270
|
+
* (raw.type, exact name, MCP leaf). Tools whose web renderer is richer than the
|
|
1271
|
+
* shared model (computer use, media, knowledge receipts, …) resolve to
|
|
1272
|
+
* `generic`; non-DOM renderers may layer their own presenters on top.
|
|
1273
|
+
*/
|
|
1274
|
+
export function presentedToolKind(item: ToolCallItem): PresentedToolKind {
|
|
1275
|
+
const rawType = rawTypeOf(item);
|
|
1276
|
+
if (rawType && PRESENTERS_BY_RAW_TYPE[rawType]) {
|
|
1277
|
+
return PRESENTERS_BY_RAW_TYPE[rawType];
|
|
1278
|
+
}
|
|
1279
|
+
const exact = PRESENTERS_BY_NAME[item.name];
|
|
1280
|
+
if (exact) {
|
|
1281
|
+
return exact;
|
|
1282
|
+
}
|
|
1283
|
+
const leaf = mcpToolLeaf(item.name);
|
|
1284
|
+
if (leaf !== item.name && PRESENTERS_BY_NAME[leaf]) {
|
|
1285
|
+
return PRESENTERS_BY_NAME[leaf];
|
|
1286
|
+
}
|
|
1287
|
+
return "generic";
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
export function toolRowPresentation(
|
|
1291
|
+
item: ToolCallItem,
|
|
1292
|
+
context: ToolPresentationContext = {},
|
|
1293
|
+
): ToolRowPresentation {
|
|
1294
|
+
switch (presentedToolKind(item)) {
|
|
1295
|
+
case "exec":
|
|
1296
|
+
return execPresentation(item, context);
|
|
1297
|
+
case "write_stdin":
|
|
1298
|
+
return writeStdinPresentation(item);
|
|
1299
|
+
case "apply_patch":
|
|
1300
|
+
return applyPatchPresentation(item) ?? genericToolPresentation(item);
|
|
1301
|
+
case "web_search":
|
|
1302
|
+
return webSearchPresentation(item);
|
|
1303
|
+
case "ask":
|
|
1304
|
+
return askPresentation(item);
|
|
1305
|
+
case "run_on":
|
|
1306
|
+
return runOnPresentation(item);
|
|
1307
|
+
case "tool_search":
|
|
1308
|
+
return toolSearchPresentation(item);
|
|
1309
|
+
case "docs_search":
|
|
1310
|
+
return docsSearchPresentation(item);
|
|
1311
|
+
case "session_title":
|
|
1312
|
+
return sessionTitlePresentation(item);
|
|
1313
|
+
default:
|
|
1314
|
+
return genericToolPresentation(item);
|
|
1315
|
+
}
|
|
1316
|
+
}
|