@neurocode-ai/session-ui 1.18.8
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/package.json +71 -0
- package/src/components/apply-patch-file.test.ts +43 -0
- package/src/components/apply-patch-file.ts +78 -0
- package/src/components/basic-tool.css +358 -0
- package/src/components/basic-tool.stories.tsx +133 -0
- package/src/components/basic-tool.tsx +343 -0
- package/src/components/dock-prompt.stories.tsx +62 -0
- package/src/components/dock-prompt.tsx +23 -0
- package/src/components/file-media.tsx +291 -0
- package/src/components/file-search.tsx +72 -0
- package/src/components/file-ssr.tsx +197 -0
- package/src/components/file.css +46 -0
- package/src/components/file.tsx +1202 -0
- package/src/components/line-comment-annotations.tsx +637 -0
- package/src/components/line-comment-styles.ts +292 -0
- package/src/components/line-comment.stories.tsx +115 -0
- package/src/components/line-comment.tsx +439 -0
- package/src/components/markdown-cache.tsx +78 -0
- package/src/components/markdown-code-state.test.ts +32 -0
- package/src/components/markdown-code-state.ts +22 -0
- package/src/components/markdown-inline-code-kind.test.ts +46 -0
- package/src/components/markdown-inline-code-kind.ts +1915 -0
- package/src/components/markdown-preload.test.ts +18 -0
- package/src/components/markdown-shiki.worker.ts +104 -0
- package/src/components/markdown-stream.test.ts +194 -0
- package/src/components/markdown-stream.ts +110 -0
- package/src/components/markdown-worker-protocol.test.ts +81 -0
- package/src/components/markdown-worker-protocol.ts +48 -0
- package/src/components/markdown-worker-queue.test.ts +49 -0
- package/src/components/markdown-worker-queue.ts +64 -0
- package/src/components/markdown-worker-transport.test.ts +56 -0
- package/src/components/markdown-worker-transport.ts +41 -0
- package/src/components/markdown-worker.ts +122 -0
- package/src/components/markdown.css +377 -0
- package/src/components/markdown.stories.tsx +53 -0
- package/src/components/markdown.tsx +678 -0
- package/src/components/message-file.test.ts +63 -0
- package/src/components/message-file.ts +34 -0
- package/src/components/message-nav.css +127 -0
- package/src/components/message-nav.stories.tsx +7 -0
- package/src/components/message-nav.tsx +102 -0
- package/src/components/message-part-text.ts +3 -0
- package/src/components/message-part.css +1569 -0
- package/src/components/message-part.stories.tsx +7 -0
- package/src/components/message-part.test.ts +28 -0
- package/src/components/message-part.tsx +2662 -0
- package/src/components/session-diff.test.ts +135 -0
- package/src/components/session-diff.ts +145 -0
- package/src/components/session-retry.tsx +74 -0
- package/src/components/session-review.css +247 -0
- package/src/components/session-review.stories.tsx +7 -0
- package/src/components/session-review.tsx +657 -0
- package/src/components/session-turn.css +241 -0
- package/src/components/session-turn.stories.tsx +7 -0
- package/src/components/session-turn.tsx +543 -0
- package/src/components/shell-submessage-motion.stories.tsx +346 -0
- package/src/components/shell-submessage.css +23 -0
- package/src/components/timeline-playground.stories.tsx +2090 -0
- package/src/components/tool-count-label.css +57 -0
- package/src/components/tool-count-label.tsx +58 -0
- package/src/components/tool-count-summary.css +102 -0
- package/src/components/tool-count-summary.stories.tsx +238 -0
- package/src/components/tool-count-summary.tsx +52 -0
- package/src/components/tool-error-card.css +91 -0
- package/src/components/tool-error-card.stories.tsx +92 -0
- package/src/components/tool-error-card.tsx +157 -0
- package/src/components/tool-status-title.css +89 -0
- package/src/components/tool-status-title.tsx +133 -0
- package/src/context/data.tsx +64 -0
- package/src/context/index.ts +1 -0
- package/src/pierre/comment-hover.ts +83 -0
- package/src/pierre/commented-lines.ts +91 -0
- package/src/pierre/diff-selection.ts +71 -0
- package/src/pierre/file-find.ts +485 -0
- package/src/pierre/file-runtime.ts +114 -0
- package/src/pierre/file-selection.ts +85 -0
- package/src/pierre/index.ts +186 -0
- package/src/pierre/media.ts +110 -0
- package/src/pierre/selection-bridge.ts +132 -0
- package/src/pierre/virtualizer.ts +100 -0
- package/src/pierre/worker.ts +52 -0
- package/src/styles/index.css +14 -0
- package/src/v2/components/attachment-card-v2.css +70 -0
- package/src/v2/components/attachment-card-v2.tsx +33 -0
- package/src/v2/components/basic-tool-v2.css +163 -0
- package/src/v2/components/basic-tool-v2.stories.tsx +137 -0
- package/src/v2/components/basic-tool-v2.tsx +139 -0
- package/src/v2/components/comment-card-v2.tsx +64 -0
- package/src/v2/components/line-comment-annotations-v2.tsx +220 -0
- package/src/v2/components/prompt-input/attachments.ts +266 -0
- package/src/v2/components/prompt-input/index.tsx +706 -0
- package/src/v2/components/prompt-input/interaction.ts +482 -0
- package/src/v2/components/prompt-input/machine.test.ts +164 -0
- package/src/v2/components/prompt-input/machine.ts +261 -0
- package/src/v2/components/prompt-input/prompt-input.stories.tsx +221 -0
- package/src/v2/components/prompt-input/store.test.ts +116 -0
- package/src/v2/components/prompt-input/store.ts +152 -0
- package/src/v2/components/prompt-input/types.ts +106 -0
- package/src/v2/components/session-file-panel-v2.tsx +43 -0
- package/src/v2/components/session-progress-indicator-v2.css +875 -0
- package/src/v2/components/session-progress-indicator-v2.stories.tsx +66 -0
- package/src/v2/components/session-progress-indicator-v2.tsx +32 -0
- package/src/v2/components/session-review-empty-changes-v2.tsx +17 -0
- package/src/v2/components/session-review-empty-no-git-v2.tsx +28 -0
- package/src/v2/components/session-review-file-preview-v2-virtualize.test.ts +13 -0
- package/src/v2/components/session-review-file-preview-v2-virtualize.ts +5 -0
- package/src/v2/components/session-review-file-preview-v2.tsx +290 -0
- package/src/v2/components/session-review-v2.css +458 -0
- package/src/v2/components/session-review-v2.tsx +340 -0
- package/src/v2/components/tool-error-card-v2.css +200 -0
- package/src/v2/components/tool-error-card-v2.stories.tsx +91 -0
- package/src/v2/components/tool-error-card-v2.tsx +166 -0
- package/sst-env.d.ts +10 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,2662 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
createEffect,
|
|
4
|
+
createMemo,
|
|
5
|
+
createSignal,
|
|
6
|
+
For,
|
|
7
|
+
Match,
|
|
8
|
+
onMount,
|
|
9
|
+
Show,
|
|
10
|
+
Switch,
|
|
11
|
+
onCleanup,
|
|
12
|
+
Index,
|
|
13
|
+
type JSX,
|
|
14
|
+
type ComponentProps,
|
|
15
|
+
} from "solid-js"
|
|
16
|
+
import { createStore } from "solid-js/store"
|
|
17
|
+
import stripAnsi from "strip-ansi"
|
|
18
|
+
import { Dynamic } from "solid-js/web"
|
|
19
|
+
import {
|
|
20
|
+
AgentPart,
|
|
21
|
+
AssistantMessage,
|
|
22
|
+
FilePart,
|
|
23
|
+
Message as MessageType,
|
|
24
|
+
Part as PartType,
|
|
25
|
+
ReasoningPart,
|
|
26
|
+
Session,
|
|
27
|
+
TextPart,
|
|
28
|
+
ToolPart,
|
|
29
|
+
UserMessage,
|
|
30
|
+
Todo,
|
|
31
|
+
QuestionAnswer,
|
|
32
|
+
QuestionInfo,
|
|
33
|
+
} from "@neurocode-ai/sdk/v2"
|
|
34
|
+
import { useData } from "../context"
|
|
35
|
+
import { useFileComponent } from "@neurocode-ai/ui/context/file"
|
|
36
|
+
import { useDialog } from "@neurocode-ai/ui/context/dialog"
|
|
37
|
+
import { type UiI18n, useI18n } from "@neurocode-ai/ui/context/i18n"
|
|
38
|
+
import { BasicTool, GenericTool } from "./basic-tool"
|
|
39
|
+
import { Accordion } from "@neurocode-ai/ui/accordion"
|
|
40
|
+
import { StickyAccordionHeader } from "@neurocode-ai/ui/sticky-accordion-header"
|
|
41
|
+
import { Collapsible } from "@neurocode-ai/ui/collapsible"
|
|
42
|
+
import { FileIcon } from "@neurocode-ai/ui/file-icon"
|
|
43
|
+
import { Icon } from "@neurocode-ai/ui/icon"
|
|
44
|
+
import { ToolErrorCard } from "./tool-error-card"
|
|
45
|
+
import { Checkbox } from "@neurocode-ai/ui/checkbox"
|
|
46
|
+
import { DiffChanges } from "@neurocode-ai/ui/diff-changes"
|
|
47
|
+
import { Markdown } from "./markdown"
|
|
48
|
+
import { ImagePreview } from "@neurocode-ai/ui/image-preview"
|
|
49
|
+
import { getDirectory as _getDirectory, getFilename } from "@neurocode-ai/core/util/path"
|
|
50
|
+
import { AttachmentCardV2 } from "../v2/components/attachment-card-v2"
|
|
51
|
+
import { CommentCardV2 } from "../v2/components/comment-card-v2"
|
|
52
|
+
import { checksum } from "@neurocode-ai/core/util/encode"
|
|
53
|
+
import { Tooltip } from "@neurocode-ai/ui/tooltip"
|
|
54
|
+
import { IconButton } from "@neurocode-ai/ui/icon-button"
|
|
55
|
+
import { Icon as IconV2 } from "@neurocode-ai/ui/v2/icon"
|
|
56
|
+
import { IconButtonV2 } from "@neurocode-ai/ui/v2/icon-button-v2"
|
|
57
|
+
import { ButtonV2 } from "@neurocode-ai/ui/v2/button-v2"
|
|
58
|
+
import { TooltipV2 } from "@neurocode-ai/ui/v2/tooltip-v2"
|
|
59
|
+
import { Spinner } from "@neurocode-ai/ui/spinner"
|
|
60
|
+
import { TextShimmer } from "@neurocode-ai/ui/text-shimmer"
|
|
61
|
+
import { AnimatedCountList } from "./tool-count-summary"
|
|
62
|
+
import { ToolStatusTitle } from "./tool-status-title"
|
|
63
|
+
import { patchFiles } from "./apply-patch-file"
|
|
64
|
+
import { animate } from "motion"
|
|
65
|
+
import { useLocation } from "@solidjs/router"
|
|
66
|
+
import { attached, inline, kind, typeLabel } from "./message-file"
|
|
67
|
+
import { readPartText } from "./message-part-text"
|
|
68
|
+
import { SessionProgressIndicatorV2 } from "../v2/components/session-progress-indicator-v2"
|
|
69
|
+
|
|
70
|
+
async function writeClipboard(text: string): Promise<boolean> {
|
|
71
|
+
const body = typeof document === "undefined" ? undefined : document.body
|
|
72
|
+
if (body) {
|
|
73
|
+
const textarea = document.createElement("textarea")
|
|
74
|
+
textarea.value = text
|
|
75
|
+
textarea.setAttribute("readonly", "")
|
|
76
|
+
textarea.style.position = "fixed"
|
|
77
|
+
textarea.style.opacity = "0"
|
|
78
|
+
textarea.style.pointerEvents = "none"
|
|
79
|
+
body.appendChild(textarea)
|
|
80
|
+
textarea.select()
|
|
81
|
+
const copied = document.execCommand("copy")
|
|
82
|
+
body.removeChild(textarea)
|
|
83
|
+
if (copied) return true
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const clipboard = typeof navigator === "undefined" ? undefined : navigator.clipboard
|
|
87
|
+
if (!clipboard?.writeText) return false
|
|
88
|
+
return clipboard.writeText(text).then(
|
|
89
|
+
() => true,
|
|
90
|
+
() => false,
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function ShellSubmessage(props: { text: string; animate?: boolean }) {
|
|
95
|
+
let widthRef: HTMLSpanElement | undefined
|
|
96
|
+
let valueRef: HTMLSpanElement | undefined
|
|
97
|
+
|
|
98
|
+
onMount(() => {
|
|
99
|
+
if (!props.animate) return
|
|
100
|
+
requestAnimationFrame(() => {
|
|
101
|
+
if (widthRef) {
|
|
102
|
+
animate(widthRef, { width: "auto" }, { type: "spring", visualDuration: 0.25, bounce: 0 })
|
|
103
|
+
}
|
|
104
|
+
if (valueRef) {
|
|
105
|
+
animate(valueRef, { opacity: 1, filter: "blur(0px)" }, { duration: 0.32, ease: [0.16, 1, 0.3, 1] })
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<span data-component="shell-submessage">
|
|
112
|
+
<span ref={widthRef} data-slot="shell-submessage-width" style={{ width: props.animate ? "0px" : undefined }}>
|
|
113
|
+
<span data-slot="basic-tool-tool-subtitle">
|
|
114
|
+
<span
|
|
115
|
+
ref={valueRef}
|
|
116
|
+
data-slot="shell-submessage-value"
|
|
117
|
+
style={props.animate ? { opacity: 0, filter: "blur(2px)" } : undefined}
|
|
118
|
+
>
|
|
119
|
+
{props.text}
|
|
120
|
+
</span>
|
|
121
|
+
</span>
|
|
122
|
+
</span>
|
|
123
|
+
</span>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
interface Diagnostic {
|
|
128
|
+
range: {
|
|
129
|
+
start: { line: number; character: number }
|
|
130
|
+
end: { line: number; character: number }
|
|
131
|
+
}
|
|
132
|
+
message: string
|
|
133
|
+
severity?: number
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function getDiagnostics(
|
|
137
|
+
diagnosticsByFile: Record<string, Diagnostic[]> | undefined,
|
|
138
|
+
filePath: string | undefined,
|
|
139
|
+
): Diagnostic[] {
|
|
140
|
+
if (!diagnosticsByFile || !filePath) return []
|
|
141
|
+
const diagnostics = diagnosticsByFile[filePath] ?? []
|
|
142
|
+
return diagnostics.filter((d) => d.severity === 1).slice(0, 3)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function DiagnosticsDisplay(props: { diagnostics: Diagnostic[] }): JSX.Element {
|
|
146
|
+
const i18n = useI18n()
|
|
147
|
+
return (
|
|
148
|
+
<Show when={props.diagnostics.length > 0}>
|
|
149
|
+
<div data-component="diagnostics">
|
|
150
|
+
<For each={props.diagnostics}>
|
|
151
|
+
{(diagnostic) => (
|
|
152
|
+
<div data-slot="diagnostic">
|
|
153
|
+
<span data-slot="diagnostic-label">{i18n.t("ui.messagePart.diagnostic.error")}</span>
|
|
154
|
+
<span data-slot="diagnostic-location">
|
|
155
|
+
[{diagnostic.range.start.line + 1}:{diagnostic.range.start.character + 1}]
|
|
156
|
+
</span>
|
|
157
|
+
<span data-slot="diagnostic-message">{diagnostic.message}</span>
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
160
|
+
</For>
|
|
161
|
+
</div>
|
|
162
|
+
</Show>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface MessageProps {
|
|
167
|
+
message: MessageType
|
|
168
|
+
parts: PartType[]
|
|
169
|
+
actions?: UserActions
|
|
170
|
+
showAssistantCopyPartID?: string | null
|
|
171
|
+
showReasoningSummaries?: boolean
|
|
172
|
+
useV2Actions?: boolean
|
|
173
|
+
comments?: UserMessageComment[]
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export type SessionAction = (input: { sessionID: string; messageID: string }) => Promise<void> | void
|
|
177
|
+
|
|
178
|
+
export type UserActions = {
|
|
179
|
+
fork?: SessionAction
|
|
180
|
+
revert?: SessionAction
|
|
181
|
+
openAttachment?: (file: FilePart) => void
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export type UserMessageComment = {
|
|
185
|
+
path: string
|
|
186
|
+
comment: string
|
|
187
|
+
selection?: {
|
|
188
|
+
startLine: number
|
|
189
|
+
endLine: number
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export interface MessagePartProps {
|
|
194
|
+
part: PartType
|
|
195
|
+
message: MessageType
|
|
196
|
+
hideDetails?: boolean
|
|
197
|
+
defaultOpen?: boolean
|
|
198
|
+
toolOpen?: boolean
|
|
199
|
+
onToolOpenChange?: (open: boolean) => void
|
|
200
|
+
deferToolContent?: boolean
|
|
201
|
+
virtualizeDiff?: boolean
|
|
202
|
+
onContentRendered?: () => void
|
|
203
|
+
showAssistantCopyPartID?: string | null
|
|
204
|
+
turnDurationMs?: number
|
|
205
|
+
useV2Actions?: boolean
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function MessageActionButton(
|
|
209
|
+
props: Pick<ComponentProps<"button">, "disabled" | "onMouseDown" | "onClick" | "aria-label"> & {
|
|
210
|
+
icon: "check" | "copy" | "reset"
|
|
211
|
+
label: JSX.Element
|
|
212
|
+
useV2?: boolean
|
|
213
|
+
},
|
|
214
|
+
) {
|
|
215
|
+
const icon = () => (props.icon === "copy" ? "outline-copy" : props.icon)
|
|
216
|
+
return (
|
|
217
|
+
<Show
|
|
218
|
+
when={props.useV2}
|
|
219
|
+
fallback={
|
|
220
|
+
<Tooltip value={props.label} placement="top" gutter={4}>
|
|
221
|
+
<IconButton
|
|
222
|
+
icon={props.icon}
|
|
223
|
+
size="normal"
|
|
224
|
+
variant="ghost"
|
|
225
|
+
disabled={props.disabled}
|
|
226
|
+
onMouseDown={props.onMouseDown}
|
|
227
|
+
onClick={props.onClick}
|
|
228
|
+
aria-label={props["aria-label"]}
|
|
229
|
+
/>
|
|
230
|
+
</Tooltip>
|
|
231
|
+
}
|
|
232
|
+
>
|
|
233
|
+
<TooltipV2 value={props.label} placement="top" gutter={4}>
|
|
234
|
+
<IconButtonV2
|
|
235
|
+
icon={<IconV2 name={icon()} size="small" />}
|
|
236
|
+
size="normal"
|
|
237
|
+
variant="ghost-muted"
|
|
238
|
+
disabled={props.disabled}
|
|
239
|
+
onMouseDown={props.onMouseDown}
|
|
240
|
+
onClick={props.onClick}
|
|
241
|
+
aria-label={props["aria-label"]}
|
|
242
|
+
/>
|
|
243
|
+
</TooltipV2>
|
|
244
|
+
</Show>
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export type PartComponent = Component<MessagePartProps>
|
|
249
|
+
|
|
250
|
+
export const PART_MAPPING: Record<string, PartComponent | undefined> = {}
|
|
251
|
+
|
|
252
|
+
const TEXT_RENDER_PACE_MS = 24
|
|
253
|
+
const TEXT_RENDER_IMMEDIATE = 512
|
|
254
|
+
const TEXT_RENDER_SNAP = /[\s.,!?;:)\]]/
|
|
255
|
+
|
|
256
|
+
function step(size: number) {
|
|
257
|
+
if (size <= 12) return 2
|
|
258
|
+
if (size <= 48) return 4
|
|
259
|
+
if (size <= 96) return 8
|
|
260
|
+
return Math.min(256, Math.ceil(size / 4))
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function next(text: string, start: number) {
|
|
264
|
+
const end = Math.min(text.length, start + step(text.length - start))
|
|
265
|
+
const max = Math.min(text.length, end + 8)
|
|
266
|
+
for (let i = end; i < max; i++) {
|
|
267
|
+
if (TEXT_RENDER_SNAP.test(text[i] ?? "")) return i + 1
|
|
268
|
+
}
|
|
269
|
+
return end
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function createPacedValue(getValue: () => string, live?: () => boolean) {
|
|
273
|
+
const [value, setValue] = createSignal(getValue())
|
|
274
|
+
let shown = getValue()
|
|
275
|
+
let timeout: ReturnType<typeof setTimeout> | undefined
|
|
276
|
+
|
|
277
|
+
const clear = () => {
|
|
278
|
+
if (!timeout) return
|
|
279
|
+
clearTimeout(timeout)
|
|
280
|
+
timeout = undefined
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const sync = (text: string) => {
|
|
284
|
+
shown = text
|
|
285
|
+
setValue(text)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const run = () => {
|
|
289
|
+
timeout = undefined
|
|
290
|
+
const text = getValue()
|
|
291
|
+
if (!live?.()) {
|
|
292
|
+
sync(text)
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
if (!text.startsWith(shown) || text.length <= shown.length) {
|
|
296
|
+
sync(text)
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
if (text.length - shown.length <= TEXT_RENDER_IMMEDIATE) {
|
|
300
|
+
sync(text)
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
const end = next(text, shown.length)
|
|
304
|
+
sync(text.slice(0, end))
|
|
305
|
+
if (end < text.length) timeout = setTimeout(run, TEXT_RENDER_PACE_MS)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
createEffect(() => {
|
|
309
|
+
const text = getValue()
|
|
310
|
+
if (!live?.()) {
|
|
311
|
+
clear()
|
|
312
|
+
sync(text)
|
|
313
|
+
return
|
|
314
|
+
}
|
|
315
|
+
if (!text.startsWith(shown) || text.length < shown.length) {
|
|
316
|
+
clear()
|
|
317
|
+
sync(text)
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
if (text.length - shown.length <= TEXT_RENDER_IMMEDIATE) {
|
|
321
|
+
clear()
|
|
322
|
+
sync(text)
|
|
323
|
+
return
|
|
324
|
+
}
|
|
325
|
+
if (text.length === shown.length || timeout) return
|
|
326
|
+
timeout = setTimeout(run, TEXT_RENDER_PACE_MS)
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
onCleanup(() => {
|
|
330
|
+
clear()
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
return value
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function PacedMarkdown(props: { text: string; cacheKey: string; streaming: boolean }) {
|
|
337
|
+
const value = createPacedValue(
|
|
338
|
+
() => props.text,
|
|
339
|
+
() => props.streaming,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
return (
|
|
343
|
+
<Show when={value()}>
|
|
344
|
+
<Markdown text={value()} cacheKey={props.cacheKey} streaming={props.streaming} />
|
|
345
|
+
</Show>
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function relativizeProjectPath(path: string, directory?: string) {
|
|
350
|
+
if (!path) return ""
|
|
351
|
+
if (!directory) return path
|
|
352
|
+
if (directory === "/") return path
|
|
353
|
+
if (directory === "\\") return path
|
|
354
|
+
if (path === directory) return ""
|
|
355
|
+
|
|
356
|
+
const separator = directory.includes("\\") ? "\\" : "/"
|
|
357
|
+
const prefix = directory.endsWith(separator) ? directory : directory + separator
|
|
358
|
+
if (!path.startsWith(prefix)) return path
|
|
359
|
+
return path.slice(directory.length)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function getDirectory(path: string | undefined) {
|
|
363
|
+
const data = useData()
|
|
364
|
+
return relativizeProjectPath(_getDirectory(path), data.directory)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
import type { IconProps } from "@neurocode-ai/ui/icon"
|
|
368
|
+
import { normalize, resolveFileDiff } from "./session-diff"
|
|
369
|
+
|
|
370
|
+
export type ToolInfo = {
|
|
371
|
+
icon: IconProps["name"]
|
|
372
|
+
title: string
|
|
373
|
+
subtitle?: string
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function agentTitle(i18n: UiI18n, type?: string) {
|
|
377
|
+
if (!type) return i18n.t("ui.tool.agent.default")
|
|
378
|
+
return i18n.t("ui.tool.agent", { type })
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const agentTones: Record<string, string> = {
|
|
382
|
+
ask: "var(--icon-agent-ask-base)",
|
|
383
|
+
build: "var(--icon-agent-build-base)",
|
|
384
|
+
docs: "var(--icon-agent-docs-base)",
|
|
385
|
+
plan: "var(--icon-agent-plan-base)",
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const v2AgentTones: Record<string, string> = {
|
|
389
|
+
build: "var(--v2-agent-build-solid)",
|
|
390
|
+
explore: "var(--v2-agent-explore-solid)",
|
|
391
|
+
plan: "var(--v2-agent-plan-solid)",
|
|
392
|
+
review: "var(--v2-agent-review-solid)",
|
|
393
|
+
writer: "var(--v2-agent-writer-solid)",
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const agentThemeColors: Record<string, string> = {
|
|
397
|
+
primary: "var(--text-interactive-base)",
|
|
398
|
+
secondary: "var(--text-base)",
|
|
399
|
+
accent: "var(--icon-info-base)",
|
|
400
|
+
success: "var(--icon-success-base)",
|
|
401
|
+
warning: "var(--icon-warning-base)",
|
|
402
|
+
error: "var(--icon-critical-base)",
|
|
403
|
+
info: "var(--icon-info-base)",
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const v2AgentThemeColors: Record<string, string> = {
|
|
407
|
+
primary: "var(--v2-text-text-accent)",
|
|
408
|
+
secondary: "var(--v2-text-text-muted)",
|
|
409
|
+
accent: "var(--v2-icon-icon-accent)",
|
|
410
|
+
success: "var(--v2-state-fg-success)",
|
|
411
|
+
warning: "var(--v2-state-fg-warning)",
|
|
412
|
+
error: "var(--v2-state-fg-danger)",
|
|
413
|
+
info: "var(--v2-state-fg-info)",
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const agentPalette = [
|
|
417
|
+
"var(--icon-agent-ask-base)",
|
|
418
|
+
"var(--icon-agent-build-base)",
|
|
419
|
+
"var(--icon-agent-docs-base)",
|
|
420
|
+
"var(--icon-agent-plan-base)",
|
|
421
|
+
"var(--syntax-info)",
|
|
422
|
+
"var(--syntax-success)",
|
|
423
|
+
"var(--syntax-warning)",
|
|
424
|
+
"var(--syntax-property)",
|
|
425
|
+
"var(--syntax-constant)",
|
|
426
|
+
"var(--text-diff-add-base)",
|
|
427
|
+
"var(--text-diff-delete-base)",
|
|
428
|
+
"var(--icon-warning-base)",
|
|
429
|
+
]
|
|
430
|
+
|
|
431
|
+
function tone(name: string) {
|
|
432
|
+
let hash = 0
|
|
433
|
+
for (const char of name) hash = (hash * 31 + char.charCodeAt(0)) >>> 0
|
|
434
|
+
return agentPalette[hash % agentPalette.length]
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function taskAgent(
|
|
438
|
+
raw: unknown,
|
|
439
|
+
list?: readonly { name: string; color?: string }[],
|
|
440
|
+
): { name?: string; color?: string; v2Color?: string } {
|
|
441
|
+
if (typeof raw !== "string" || !raw) return {}
|
|
442
|
+
const key = raw.toLowerCase()
|
|
443
|
+
const item = list?.find((entry) => entry.name === raw || entry.name.toLowerCase() === key)
|
|
444
|
+
const v2Tone = item?.color ? undefined : v2AgentTones[key]
|
|
445
|
+
const color = agentColor(item?.color, agentThemeColors) ?? agentTones[key] ?? tone(key)
|
|
446
|
+
const v2Color = agentColor(item?.color, v2AgentThemeColors) ?? v2Tone ?? color
|
|
447
|
+
return {
|
|
448
|
+
name: item?.name ?? `${raw[0]!.toUpperCase()}${raw.slice(1)}`,
|
|
449
|
+
color,
|
|
450
|
+
v2Color,
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function agentColor(value: string | undefined, themeColors: Record<string, string>) {
|
|
455
|
+
if (!value) return
|
|
456
|
+
return themeColors[value] ?? value
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function newLayout() {
|
|
460
|
+
return typeof document !== "undefined" && document.body.hasAttribute("data-new-layout")
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function webSearchProviderLabel(provider: unknown) {
|
|
464
|
+
if (provider === "parallel") return "Parallel Web Search"
|
|
465
|
+
if (provider === "exa") return "Exa Web Search"
|
|
466
|
+
return "Web Search"
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export function getToolInfo(
|
|
470
|
+
tool: string,
|
|
471
|
+
input: any = {},
|
|
472
|
+
metadata: Record<string, unknown> | undefined = {},
|
|
473
|
+
): ToolInfo {
|
|
474
|
+
const i18n = useI18n()
|
|
475
|
+
switch (tool) {
|
|
476
|
+
case "read":
|
|
477
|
+
return {
|
|
478
|
+
icon: "glasses",
|
|
479
|
+
title: i18n.t("ui.tool.read"),
|
|
480
|
+
subtitle: input.filePath ? getFilename(input.filePath) : undefined,
|
|
481
|
+
}
|
|
482
|
+
case "list":
|
|
483
|
+
return {
|
|
484
|
+
icon: "bullet-list",
|
|
485
|
+
title: i18n.t("ui.tool.list"),
|
|
486
|
+
subtitle: input.path ? getFilename(input.path) : undefined,
|
|
487
|
+
}
|
|
488
|
+
case "glob":
|
|
489
|
+
return {
|
|
490
|
+
icon: "magnifying-glass-menu",
|
|
491
|
+
title: i18n.t("ui.tool.glob"),
|
|
492
|
+
subtitle: input.pattern,
|
|
493
|
+
}
|
|
494
|
+
case "grep":
|
|
495
|
+
return {
|
|
496
|
+
icon: "magnifying-glass-menu",
|
|
497
|
+
title: i18n.t("ui.tool.grep"),
|
|
498
|
+
subtitle: input.pattern,
|
|
499
|
+
}
|
|
500
|
+
case "webfetch":
|
|
501
|
+
return {
|
|
502
|
+
icon: "window-cursor",
|
|
503
|
+
title: i18n.t("ui.tool.webfetch"),
|
|
504
|
+
subtitle: input.url,
|
|
505
|
+
}
|
|
506
|
+
case "websearch":
|
|
507
|
+
return {
|
|
508
|
+
icon: "window-cursor",
|
|
509
|
+
title: webSearchProviderLabel(metadata?.provider),
|
|
510
|
+
subtitle: input.query,
|
|
511
|
+
}
|
|
512
|
+
case "task": {
|
|
513
|
+
const type =
|
|
514
|
+
typeof input.subagent_type === "string" && input.subagent_type
|
|
515
|
+
? input.subagent_type[0]!.toUpperCase() + input.subagent_type.slice(1)
|
|
516
|
+
: undefined
|
|
517
|
+
return {
|
|
518
|
+
icon: "task",
|
|
519
|
+
title: agentTitle(i18n, type),
|
|
520
|
+
subtitle: input.description,
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
case "bash":
|
|
524
|
+
case "shell":
|
|
525
|
+
return {
|
|
526
|
+
icon: "console",
|
|
527
|
+
title: i18n.t("ui.tool.shell"),
|
|
528
|
+
subtitle: input.command,
|
|
529
|
+
}
|
|
530
|
+
case "edit":
|
|
531
|
+
return {
|
|
532
|
+
icon: "code-lines",
|
|
533
|
+
title: i18n.t("ui.messagePart.title.edit"),
|
|
534
|
+
subtitle: input.filePath ? getFilename(input.filePath) : undefined,
|
|
535
|
+
}
|
|
536
|
+
case "write":
|
|
537
|
+
return {
|
|
538
|
+
icon: "code-lines",
|
|
539
|
+
title: i18n.t("ui.messagePart.title.write"),
|
|
540
|
+
subtitle: input.filePath ? getFilename(input.filePath) : undefined,
|
|
541
|
+
}
|
|
542
|
+
case "patch":
|
|
543
|
+
case "apply_patch":
|
|
544
|
+
return {
|
|
545
|
+
icon: "code-lines",
|
|
546
|
+
title: i18n.t("ui.tool.patch"),
|
|
547
|
+
subtitle: input.files?.length
|
|
548
|
+
? `${input.files.length} ${i18n.t(input.files.length > 1 ? "ui.common.file.other" : "ui.common.file.one")}`
|
|
549
|
+
: undefined,
|
|
550
|
+
}
|
|
551
|
+
case "todowrite":
|
|
552
|
+
return {
|
|
553
|
+
icon: "checklist",
|
|
554
|
+
title: i18n.t("ui.tool.todos"),
|
|
555
|
+
}
|
|
556
|
+
case "question":
|
|
557
|
+
return {
|
|
558
|
+
icon: "bubble-5",
|
|
559
|
+
title: i18n.t("ui.tool.questions"),
|
|
560
|
+
}
|
|
561
|
+
case "skill":
|
|
562
|
+
return {
|
|
563
|
+
icon: "brain",
|
|
564
|
+
title: input.name || i18n.t("ui.tool.skill"),
|
|
565
|
+
}
|
|
566
|
+
default:
|
|
567
|
+
return {
|
|
568
|
+
icon: "mcp",
|
|
569
|
+
title: tool,
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function urls(text: string | undefined) {
|
|
575
|
+
if (!text) return []
|
|
576
|
+
const seen = new Set<string>()
|
|
577
|
+
return [...text.matchAll(/https?:\/\/[^\s<>"'`)\]]+/g)]
|
|
578
|
+
.map((item) => item[0].replace(/[),.;:!?]+$/g, ""))
|
|
579
|
+
.filter((item) => {
|
|
580
|
+
if (seen.has(item)) return false
|
|
581
|
+
seen.add(item)
|
|
582
|
+
return true
|
|
583
|
+
})
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function sessionLink(id: string | undefined, path: string, href?: (id: string) => string | undefined) {
|
|
587
|
+
if (!id) return
|
|
588
|
+
|
|
589
|
+
const direct = href?.(id)
|
|
590
|
+
if (direct) return direct
|
|
591
|
+
|
|
592
|
+
const idx = path.indexOf("/session")
|
|
593
|
+
if (idx === -1) return
|
|
594
|
+
return `${path.slice(0, idx)}/session/${id}`
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function currentSession(path: string) {
|
|
598
|
+
return path.match(/\/session\/([^/?#]+)/)?.[1]
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function taskSession(
|
|
602
|
+
input: Record<string, any>,
|
|
603
|
+
path: string,
|
|
604
|
+
sessions: Session[] | undefined,
|
|
605
|
+
agents?: readonly { name: string; color?: string }[],
|
|
606
|
+
) {
|
|
607
|
+
const parentID = currentSession(path)
|
|
608
|
+
if (!parentID) return
|
|
609
|
+
const description = typeof input.description === "string" ? input.description : ""
|
|
610
|
+
const agent = taskAgent(input.subagent_type, agents).name
|
|
611
|
+
return (sessions ?? [])
|
|
612
|
+
.filter((session) => session.parentID === parentID && !session.time?.archived)
|
|
613
|
+
.filter((session) => (description ? session.title.startsWith(description) : true))
|
|
614
|
+
.filter((session) => (agent ? session.title.includes(`@${agent}`) : true))
|
|
615
|
+
.sort((a, b) => (b.time.created ?? 0) - (a.time.created ?? 0))[0]?.id
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const CONTEXT_GROUP_TOOLS = new Set(["read", "glob", "grep", "list"])
|
|
619
|
+
const HIDDEN_TOOLS = new Set(["todowrite"])
|
|
620
|
+
|
|
621
|
+
function list<T>(value: T[] | undefined | null, fallback: T[]) {
|
|
622
|
+
if (Array.isArray(value)) return value
|
|
623
|
+
return fallback
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function same<T>(a: readonly T[] | undefined, b: readonly T[] | undefined) {
|
|
627
|
+
if (a === b) return true
|
|
628
|
+
if (!a || !b) return false
|
|
629
|
+
if (a.length !== b.length) return false
|
|
630
|
+
return a.every((x, i) => x === b[i])
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
export type PartRef = {
|
|
634
|
+
messageID: string
|
|
635
|
+
partID: string
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
export type PartGroup =
|
|
639
|
+
| {
|
|
640
|
+
key: string
|
|
641
|
+
type: "part"
|
|
642
|
+
ref: PartRef
|
|
643
|
+
}
|
|
644
|
+
| {
|
|
645
|
+
key: string
|
|
646
|
+
type: "context"
|
|
647
|
+
refs: PartRef[]
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function sameRef(a: PartRef, b: PartRef) {
|
|
651
|
+
return a.messageID === b.messageID && a.partID === b.partID
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function sameGroup(a: PartGroup, b: PartGroup) {
|
|
655
|
+
if (a === b) return true
|
|
656
|
+
if (a.key !== b.key) return false
|
|
657
|
+
if (a.type !== b.type) return false
|
|
658
|
+
if (a.type === "part") {
|
|
659
|
+
if (b.type !== "part") return false
|
|
660
|
+
return sameRef(a.ref, b.ref)
|
|
661
|
+
}
|
|
662
|
+
if (b.type !== "context") return false
|
|
663
|
+
if (a.refs.length !== b.refs.length) return false
|
|
664
|
+
return a.refs.every((ref, i) => sameRef(ref, b.refs[i]!))
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export function sameGroups(a: readonly PartGroup[] | undefined, b: readonly PartGroup[] | undefined) {
|
|
668
|
+
if (a === b) return true
|
|
669
|
+
if (!a || !b) return false
|
|
670
|
+
if (a.length !== b.length) return false
|
|
671
|
+
return a.every((item, i) => sameGroup(item, b[i]!))
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
export function groupParts(parts: { messageID: string; part: PartType }[]) {
|
|
675
|
+
const result: PartGroup[] = []
|
|
676
|
+
let start = -1
|
|
677
|
+
|
|
678
|
+
const flush = (end: number) => {
|
|
679
|
+
if (start < 0) return
|
|
680
|
+
const first = parts[start]
|
|
681
|
+
const last = parts[end]
|
|
682
|
+
if (!first || !last) {
|
|
683
|
+
start = -1
|
|
684
|
+
return
|
|
685
|
+
}
|
|
686
|
+
result.push({
|
|
687
|
+
key: `context:${first.part.id}`,
|
|
688
|
+
type: "context",
|
|
689
|
+
refs: parts.slice(start, end + 1).map((item) => ({
|
|
690
|
+
messageID: item.messageID,
|
|
691
|
+
partID: item.part.id,
|
|
692
|
+
})),
|
|
693
|
+
})
|
|
694
|
+
start = -1
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
parts.forEach((item, index) => {
|
|
698
|
+
if (isContextGroupTool(item.part)) {
|
|
699
|
+
if (start < 0) start = index
|
|
700
|
+
return
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
flush(index - 1)
|
|
704
|
+
result.push({
|
|
705
|
+
key: `part:${item.messageID}:${item.part.id}`,
|
|
706
|
+
type: "part",
|
|
707
|
+
ref: {
|
|
708
|
+
messageID: item.messageID,
|
|
709
|
+
partID: item.part.id,
|
|
710
|
+
},
|
|
711
|
+
})
|
|
712
|
+
})
|
|
713
|
+
|
|
714
|
+
flush(parts.length - 1)
|
|
715
|
+
return result
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function index<T extends { id: string }>(items: readonly T[]) {
|
|
719
|
+
return new Map(items.map((item) => [item.id, item] as const))
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
export function renderable(part: PartType, showReasoningSummaries = true) {
|
|
723
|
+
if (part.type === "tool") {
|
|
724
|
+
if (HIDDEN_TOOLS.has(part.tool)) return false
|
|
725
|
+
if (part.tool === "question") return part.state.status !== "pending" && part.state.status !== "running"
|
|
726
|
+
return true
|
|
727
|
+
}
|
|
728
|
+
if (part.type === "text") return !!part.text?.trim()
|
|
729
|
+
if (part.type === "reasoning") return showReasoningSummaries && !!part.text?.trim()
|
|
730
|
+
return !!PART_MAPPING[part.type]
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function toolDefaultOpen(tool: string, shell = false, edit = false) {
|
|
734
|
+
if (tool === "bash" || tool === "shell") return shell
|
|
735
|
+
if (tool === "edit" || tool === "write" || tool === "patch" || tool === "apply_patch") return edit
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
export function partDefaultOpen(part: PartType, shell = false, edit = false) {
|
|
739
|
+
if (part.type !== "tool") return
|
|
740
|
+
return toolDefaultOpen(part.tool, shell, edit)
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
export function AssistantParts(props: {
|
|
744
|
+
messages: AssistantMessage[]
|
|
745
|
+
showAssistantCopyPartID?: string | null
|
|
746
|
+
turnDurationMs?: number
|
|
747
|
+
useV2Actions?: boolean
|
|
748
|
+
working?: boolean
|
|
749
|
+
showReasoningSummaries?: boolean
|
|
750
|
+
shellToolDefaultOpen?: boolean
|
|
751
|
+
editToolDefaultOpen?: boolean
|
|
752
|
+
}) {
|
|
753
|
+
const data = useData()
|
|
754
|
+
const emptyParts: PartType[] = []
|
|
755
|
+
const emptyTools: ToolPart[] = []
|
|
756
|
+
const msgs = createMemo(() => index(props.messages))
|
|
757
|
+
const part = createMemo(
|
|
758
|
+
() =>
|
|
759
|
+
new Map(
|
|
760
|
+
props.messages.map((message) => [message.id, index(list(data.store.part?.[message.id], emptyParts))] as const),
|
|
761
|
+
),
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
const grouped = createMemo(
|
|
765
|
+
() =>
|
|
766
|
+
groupParts(
|
|
767
|
+
props.messages.flatMap((message) =>
|
|
768
|
+
list(data.store.part?.[message.id], emptyParts)
|
|
769
|
+
.filter((part) => renderable(part, props.showReasoningSummaries ?? true))
|
|
770
|
+
.map((part) => ({
|
|
771
|
+
messageID: message.id,
|
|
772
|
+
part,
|
|
773
|
+
})),
|
|
774
|
+
),
|
|
775
|
+
),
|
|
776
|
+
[] as PartGroup[],
|
|
777
|
+
{ equals: sameGroups },
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
const last = createMemo(() => grouped().at(-1)?.key)
|
|
781
|
+
|
|
782
|
+
return (
|
|
783
|
+
<Index each={grouped()}>
|
|
784
|
+
{(entryAccessor) => {
|
|
785
|
+
const entryType = createMemo(() => entryAccessor().type)
|
|
786
|
+
|
|
787
|
+
return (
|
|
788
|
+
<Switch>
|
|
789
|
+
<Match when={entryType() === "context"}>
|
|
790
|
+
{(() => {
|
|
791
|
+
const parts = createMemo(
|
|
792
|
+
() => {
|
|
793
|
+
const entry = entryAccessor()
|
|
794
|
+
if (entry.type !== "context") return emptyTools
|
|
795
|
+
return entry.refs
|
|
796
|
+
.map((ref) => part().get(ref.messageID)?.get(ref.partID))
|
|
797
|
+
.filter((part): part is ToolPart => !!part && isContextGroupTool(part))
|
|
798
|
+
},
|
|
799
|
+
emptyTools,
|
|
800
|
+
{ equals: same },
|
|
801
|
+
)
|
|
802
|
+
const busy = createMemo(() => props.working && last() === entryAccessor().key)
|
|
803
|
+
|
|
804
|
+
return (
|
|
805
|
+
<Show when={parts().length > 0}>
|
|
806
|
+
<ContextToolGroup parts={parts()} busy={busy()} />
|
|
807
|
+
</Show>
|
|
808
|
+
)
|
|
809
|
+
})()}
|
|
810
|
+
</Match>
|
|
811
|
+
<Match when={entryType() === "part"}>
|
|
812
|
+
{(() => {
|
|
813
|
+
const message = createMemo(() => {
|
|
814
|
+
const entry = entryAccessor()
|
|
815
|
+
if (entry.type !== "part") return
|
|
816
|
+
return msgs().get(entry.ref.messageID)
|
|
817
|
+
})
|
|
818
|
+
const item = createMemo(() => {
|
|
819
|
+
const entry = entryAccessor()
|
|
820
|
+
if (entry.type !== "part") return
|
|
821
|
+
return part().get(entry.ref.messageID)?.get(entry.ref.partID)
|
|
822
|
+
})
|
|
823
|
+
|
|
824
|
+
return (
|
|
825
|
+
<Show when={message()}>
|
|
826
|
+
<Show when={item()}>
|
|
827
|
+
<Part
|
|
828
|
+
part={item()!}
|
|
829
|
+
message={message()!}
|
|
830
|
+
showAssistantCopyPartID={props.showAssistantCopyPartID}
|
|
831
|
+
turnDurationMs={props.turnDurationMs}
|
|
832
|
+
useV2Actions={props.useV2Actions}
|
|
833
|
+
defaultOpen={partDefaultOpen(item()!, props.shellToolDefaultOpen, props.editToolDefaultOpen)}
|
|
834
|
+
/>
|
|
835
|
+
</Show>
|
|
836
|
+
</Show>
|
|
837
|
+
)
|
|
838
|
+
})()}
|
|
839
|
+
</Match>
|
|
840
|
+
</Switch>
|
|
841
|
+
)
|
|
842
|
+
}}
|
|
843
|
+
</Index>
|
|
844
|
+
)
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function isContextGroupTool(part: PartType): part is ToolPart {
|
|
848
|
+
return part.type === "tool" && CONTEXT_GROUP_TOOLS.has(part.tool)
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function contextToolDetail(part: ToolPart): string | undefined {
|
|
852
|
+
const info = getToolInfo(
|
|
853
|
+
part.tool,
|
|
854
|
+
part.state.input ?? {},
|
|
855
|
+
"metadata" in part.state ? part.state.metadata : undefined,
|
|
856
|
+
)
|
|
857
|
+
if (info.subtitle) return info.subtitle
|
|
858
|
+
if (part.state.status === "error") return part.state.error
|
|
859
|
+
if ((part.state.status === "running" || part.state.status === "completed") && part.state.title)
|
|
860
|
+
return part.state.title
|
|
861
|
+
const description = part.state.input?.description
|
|
862
|
+
if (typeof description === "string") return description
|
|
863
|
+
return undefined
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function contextToolTrigger(part: ToolPart, i18n: ReturnType<typeof useI18n>) {
|
|
867
|
+
const input = (part.state.input ?? {}) as Record<string, unknown>
|
|
868
|
+
const path = typeof input.path === "string" ? input.path : "/"
|
|
869
|
+
const filePath = typeof input.filePath === "string" ? input.filePath : undefined
|
|
870
|
+
const pattern = typeof input.pattern === "string" ? input.pattern : undefined
|
|
871
|
+
const include = typeof input.include === "string" ? input.include : undefined
|
|
872
|
+
const offset = typeof input.offset === "number" ? input.offset : undefined
|
|
873
|
+
const limit = typeof input.limit === "number" ? input.limit : undefined
|
|
874
|
+
|
|
875
|
+
switch (part.tool) {
|
|
876
|
+
case "read": {
|
|
877
|
+
const args: string[] = []
|
|
878
|
+
if (offset !== undefined) args.push("offset=" + offset)
|
|
879
|
+
if (limit !== undefined) args.push("limit=" + limit)
|
|
880
|
+
return {
|
|
881
|
+
title: i18n.t("ui.tool.read"),
|
|
882
|
+
subtitle: filePath ? getFilename(filePath) : "",
|
|
883
|
+
args,
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
case "list":
|
|
887
|
+
return {
|
|
888
|
+
title: i18n.t("ui.tool.list"),
|
|
889
|
+
subtitle: getDirectory(path),
|
|
890
|
+
}
|
|
891
|
+
case "glob":
|
|
892
|
+
return {
|
|
893
|
+
title: i18n.t("ui.tool.glob"),
|
|
894
|
+
subtitle: getDirectory(path),
|
|
895
|
+
args: pattern ? ["pattern=" + pattern] : [],
|
|
896
|
+
}
|
|
897
|
+
case "grep": {
|
|
898
|
+
const args: string[] = []
|
|
899
|
+
if (pattern) args.push("pattern=" + pattern)
|
|
900
|
+
if (include) args.push("include=" + include)
|
|
901
|
+
return {
|
|
902
|
+
title: i18n.t("ui.tool.grep"),
|
|
903
|
+
subtitle: getDirectory(path),
|
|
904
|
+
args,
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
default: {
|
|
908
|
+
const info = getToolInfo(part.tool, input, "metadata" in part.state ? part.state.metadata : undefined)
|
|
909
|
+
return {
|
|
910
|
+
title: info.title,
|
|
911
|
+
subtitle: info.subtitle || contextToolDetail(part),
|
|
912
|
+
args: [],
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function contextToolSummary(parts: ToolPart[]) {
|
|
919
|
+
const read = parts.filter((part) => part.tool === "read").length
|
|
920
|
+
const search = parts.filter((part) => part.tool === "glob" || part.tool === "grep").length
|
|
921
|
+
const list = parts.filter((part) => part.tool === "list").length
|
|
922
|
+
return { read, search, list }
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function ExaOutput(props: { output?: string }) {
|
|
926
|
+
const links = createMemo(() => urls(props.output))
|
|
927
|
+
|
|
928
|
+
return (
|
|
929
|
+
<Show when={links().length > 0}>
|
|
930
|
+
<div data-component="exa-tool-output">
|
|
931
|
+
<div data-slot="exa-tool-links">
|
|
932
|
+
<For each={links()}>
|
|
933
|
+
{(url) => (
|
|
934
|
+
<a
|
|
935
|
+
data-slot="exa-tool-link"
|
|
936
|
+
href={url}
|
|
937
|
+
target="_blank"
|
|
938
|
+
rel="noopener noreferrer"
|
|
939
|
+
onClick={(event) => event.stopPropagation()}
|
|
940
|
+
>
|
|
941
|
+
{url}
|
|
942
|
+
</a>
|
|
943
|
+
)}
|
|
944
|
+
</For>
|
|
945
|
+
</div>
|
|
946
|
+
</div>
|
|
947
|
+
</Show>
|
|
948
|
+
)
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
export function registerPartComponent(type: string, component: PartComponent) {
|
|
952
|
+
PART_MAPPING[type] = component
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
export function Message(props: MessageProps) {
|
|
956
|
+
return (
|
|
957
|
+
<Switch>
|
|
958
|
+
<Match when={props.message.role === "user" && props.message}>
|
|
959
|
+
{(userMessage) => (
|
|
960
|
+
<UserMessageDisplay
|
|
961
|
+
message={userMessage() as UserMessage}
|
|
962
|
+
parts={props.parts}
|
|
963
|
+
actions={props.actions}
|
|
964
|
+
useV2Actions={props.useV2Actions}
|
|
965
|
+
comments={props.comments}
|
|
966
|
+
/>
|
|
967
|
+
)}
|
|
968
|
+
</Match>
|
|
969
|
+
<Match when={props.message.role === "assistant" && props.message}>
|
|
970
|
+
{(assistantMessage) => (
|
|
971
|
+
<AssistantMessageDisplay
|
|
972
|
+
message={assistantMessage() as AssistantMessage}
|
|
973
|
+
parts={props.parts}
|
|
974
|
+
showAssistantCopyPartID={props.showAssistantCopyPartID}
|
|
975
|
+
showReasoningSummaries={props.showReasoningSummaries}
|
|
976
|
+
useV2Actions={props.useV2Actions}
|
|
977
|
+
/>
|
|
978
|
+
)}
|
|
979
|
+
</Match>
|
|
980
|
+
</Switch>
|
|
981
|
+
)
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
export function AssistantMessageDisplay(props: {
|
|
985
|
+
message: AssistantMessage
|
|
986
|
+
parts: PartType[]
|
|
987
|
+
showAssistantCopyPartID?: string | null
|
|
988
|
+
showReasoningSummaries?: boolean
|
|
989
|
+
useV2Actions?: boolean
|
|
990
|
+
}) {
|
|
991
|
+
const emptyTools: ToolPart[] = []
|
|
992
|
+
const part = createMemo(() => index(props.parts))
|
|
993
|
+
const grouped = createMemo(
|
|
994
|
+
() =>
|
|
995
|
+
groupParts(
|
|
996
|
+
props.parts
|
|
997
|
+
.filter((part) => renderable(part, props.showReasoningSummaries ?? true))
|
|
998
|
+
.map((part) => ({
|
|
999
|
+
messageID: props.message.id,
|
|
1000
|
+
part,
|
|
1001
|
+
})),
|
|
1002
|
+
),
|
|
1003
|
+
[] as PartGroup[],
|
|
1004
|
+
{ equals: sameGroups },
|
|
1005
|
+
)
|
|
1006
|
+
|
|
1007
|
+
return (
|
|
1008
|
+
<Index each={grouped()}>
|
|
1009
|
+
{(entryAccessor) => {
|
|
1010
|
+
const entryType = createMemo(() => entryAccessor().type)
|
|
1011
|
+
|
|
1012
|
+
return (
|
|
1013
|
+
<Switch>
|
|
1014
|
+
<Match when={entryType() === "context"}>
|
|
1015
|
+
{(() => {
|
|
1016
|
+
const parts = createMemo(
|
|
1017
|
+
() => {
|
|
1018
|
+
const entry = entryAccessor()
|
|
1019
|
+
if (entry.type !== "context") return emptyTools
|
|
1020
|
+
return entry.refs
|
|
1021
|
+
.map((ref) => part().get(ref.partID))
|
|
1022
|
+
.filter((part): part is ToolPart => !!part && isContextGroupTool(part))
|
|
1023
|
+
},
|
|
1024
|
+
emptyTools,
|
|
1025
|
+
{ equals: same },
|
|
1026
|
+
)
|
|
1027
|
+
|
|
1028
|
+
return (
|
|
1029
|
+
<Show when={parts().length > 0}>
|
|
1030
|
+
<ContextToolGroup parts={parts()} />
|
|
1031
|
+
</Show>
|
|
1032
|
+
)
|
|
1033
|
+
})()}
|
|
1034
|
+
</Match>
|
|
1035
|
+
<Match when={entryType() === "part"}>
|
|
1036
|
+
{(() => {
|
|
1037
|
+
const item = createMemo(() => {
|
|
1038
|
+
const entry = entryAccessor()
|
|
1039
|
+
if (entry.type !== "part") return
|
|
1040
|
+
return part().get(entry.ref.partID)
|
|
1041
|
+
})
|
|
1042
|
+
|
|
1043
|
+
return (
|
|
1044
|
+
<Show when={item()}>
|
|
1045
|
+
<Part
|
|
1046
|
+
part={item()!}
|
|
1047
|
+
message={props.message}
|
|
1048
|
+
showAssistantCopyPartID={props.showAssistantCopyPartID}
|
|
1049
|
+
useV2Actions={props.useV2Actions}
|
|
1050
|
+
/>
|
|
1051
|
+
</Show>
|
|
1052
|
+
)
|
|
1053
|
+
})()}
|
|
1054
|
+
</Match>
|
|
1055
|
+
</Switch>
|
|
1056
|
+
)
|
|
1057
|
+
}}
|
|
1058
|
+
</Index>
|
|
1059
|
+
)
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
export function ContextToolGroup(props: {
|
|
1063
|
+
parts: ToolPart[]
|
|
1064
|
+
busy?: boolean
|
|
1065
|
+
open?: boolean
|
|
1066
|
+
onOpenChange?: (open: boolean) => void
|
|
1067
|
+
onSizeChange?: () => void
|
|
1068
|
+
}) {
|
|
1069
|
+
const i18n = useI18n()
|
|
1070
|
+
const [localOpen, setLocalOpen] = createSignal(false)
|
|
1071
|
+
const open = () => props.open ?? localOpen()
|
|
1072
|
+
const pending = createMemo(
|
|
1073
|
+
() =>
|
|
1074
|
+
!!props.busy || props.parts.some((part) => part.state.status === "pending" || part.state.status === "running"),
|
|
1075
|
+
)
|
|
1076
|
+
const summary = createMemo(() => contextToolSummary(props.parts))
|
|
1077
|
+
const handleOpenChange = (value: boolean) => {
|
|
1078
|
+
if (props.open === undefined) setLocalOpen(value)
|
|
1079
|
+
props.onOpenChange?.(value)
|
|
1080
|
+
props.onSizeChange?.()
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
return (
|
|
1084
|
+
<Collapsible
|
|
1085
|
+
open={open()}
|
|
1086
|
+
onOpenChange={handleOpenChange}
|
|
1087
|
+
variant="ghost"
|
|
1088
|
+
class="tool-collapsible"
|
|
1089
|
+
data-timeline-part-ids={props.parts.map((part) => part.id).join(",")}
|
|
1090
|
+
>
|
|
1091
|
+
<Collapsible.Trigger>
|
|
1092
|
+
<div data-component="context-tool-group-trigger">
|
|
1093
|
+
<span
|
|
1094
|
+
data-slot="context-tool-group-title"
|
|
1095
|
+
class="min-w-0 flex items-center gap-2 text-14-medium text-text-strong"
|
|
1096
|
+
>
|
|
1097
|
+
<span data-slot="context-tool-group-label" class="shrink-0">
|
|
1098
|
+
<ToolStatusTitle
|
|
1099
|
+
active={pending()}
|
|
1100
|
+
activeText={i18n.t("ui.sessionTurn.status.gatheringContext")}
|
|
1101
|
+
doneText={i18n.t("ui.sessionTurn.status.gatheredContext")}
|
|
1102
|
+
split={false}
|
|
1103
|
+
/>
|
|
1104
|
+
</span>
|
|
1105
|
+
<span
|
|
1106
|
+
data-slot="context-tool-group-summary"
|
|
1107
|
+
class="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap font-normal text-text-base"
|
|
1108
|
+
>
|
|
1109
|
+
<AnimatedCountList
|
|
1110
|
+
items={[
|
|
1111
|
+
{
|
|
1112
|
+
key: "read",
|
|
1113
|
+
count: summary().read,
|
|
1114
|
+
one: i18n.t("ui.messagePart.context.read.one"),
|
|
1115
|
+
other: i18n.t("ui.messagePart.context.read.other"),
|
|
1116
|
+
},
|
|
1117
|
+
{
|
|
1118
|
+
key: "search",
|
|
1119
|
+
count: summary().search,
|
|
1120
|
+
one: i18n.t("ui.messagePart.context.search.one"),
|
|
1121
|
+
other: i18n.t("ui.messagePart.context.search.other"),
|
|
1122
|
+
},
|
|
1123
|
+
{
|
|
1124
|
+
key: "list",
|
|
1125
|
+
count: summary().list,
|
|
1126
|
+
one: i18n.t("ui.messagePart.context.list.one"),
|
|
1127
|
+
other: i18n.t("ui.messagePart.context.list.other"),
|
|
1128
|
+
},
|
|
1129
|
+
]}
|
|
1130
|
+
fallback=""
|
|
1131
|
+
/>
|
|
1132
|
+
</span>
|
|
1133
|
+
</span>
|
|
1134
|
+
<Collapsible.Arrow />
|
|
1135
|
+
</div>
|
|
1136
|
+
</Collapsible.Trigger>
|
|
1137
|
+
<Collapsible.Content>
|
|
1138
|
+
<div data-component="context-tool-group-list">
|
|
1139
|
+
<Index each={props.parts}>
|
|
1140
|
+
{(partAccessor) => {
|
|
1141
|
+
const trigger = createMemo(() => contextToolTrigger(partAccessor(), i18n))
|
|
1142
|
+
const running = createMemo(
|
|
1143
|
+
() => partAccessor().state.status === "pending" || partAccessor().state.status === "running",
|
|
1144
|
+
)
|
|
1145
|
+
return (
|
|
1146
|
+
<div data-slot="context-tool-group-item">
|
|
1147
|
+
<div data-component="tool-trigger">
|
|
1148
|
+
<div data-slot="basic-tool-tool-trigger-content">
|
|
1149
|
+
<div data-slot="basic-tool-tool-info">
|
|
1150
|
+
<div data-slot="basic-tool-tool-info-structured">
|
|
1151
|
+
<div data-slot="basic-tool-tool-info-main">
|
|
1152
|
+
<span data-slot="basic-tool-tool-title">
|
|
1153
|
+
<TextShimmer text={trigger().title} active={running()} />
|
|
1154
|
+
</span>
|
|
1155
|
+
<Show when={!running() && trigger().subtitle}>
|
|
1156
|
+
<span data-slot="basic-tool-tool-subtitle">{trigger().subtitle}</span>
|
|
1157
|
+
</Show>
|
|
1158
|
+
<Show when={!running() && trigger().args?.length}>
|
|
1159
|
+
<For each={trigger().args}>
|
|
1160
|
+
{(arg) => <span data-slot="basic-tool-tool-arg">{arg}</span>}
|
|
1161
|
+
</For>
|
|
1162
|
+
</Show>
|
|
1163
|
+
</div>
|
|
1164
|
+
</div>
|
|
1165
|
+
</div>
|
|
1166
|
+
</div>
|
|
1167
|
+
</div>
|
|
1168
|
+
</div>
|
|
1169
|
+
)
|
|
1170
|
+
}}
|
|
1171
|
+
</Index>
|
|
1172
|
+
</div>
|
|
1173
|
+
</Collapsible.Content>
|
|
1174
|
+
</Collapsible>
|
|
1175
|
+
)
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function UserMessageComments(props: { comments: UserMessageComment[]; bounded: boolean }) {
|
|
1179
|
+
const i18n = useI18n()
|
|
1180
|
+
const [state, setState] = createStore({ expanded: false })
|
|
1181
|
+
const comments = createMemo(() => (props.bounded && !state.expanded ? props.comments.slice(0, 5) : props.comments))
|
|
1182
|
+
|
|
1183
|
+
return (
|
|
1184
|
+
<div data-slot="user-message-comments" data-bounded={props.bounded ? "true" : undefined}>
|
|
1185
|
+
<For each={comments()}>
|
|
1186
|
+
{(comment) => (
|
|
1187
|
+
<CommentCardV2
|
|
1188
|
+
comment={comment.comment}
|
|
1189
|
+
path={comment.path}
|
|
1190
|
+
selection={comment.selection}
|
|
1191
|
+
title={comment.comment}
|
|
1192
|
+
tooltip
|
|
1193
|
+
wide
|
|
1194
|
+
/>
|
|
1195
|
+
)}
|
|
1196
|
+
</For>
|
|
1197
|
+
<Show when={props.bounded && props.comments.length > 5 && !state.expanded}>
|
|
1198
|
+
<ButtonV2 size="small" variant="ghost-muted" onClick={() => setState("expanded", true)}>
|
|
1199
|
+
{i18n.t("ui.common.showMore")}
|
|
1200
|
+
</ButtonV2>
|
|
1201
|
+
</Show>
|
|
1202
|
+
</div>
|
|
1203
|
+
)
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
export function UserMessageDisplay(props: {
|
|
1207
|
+
message: UserMessage
|
|
1208
|
+
parts: PartType[]
|
|
1209
|
+
actions?: UserActions
|
|
1210
|
+
useV2Actions?: boolean
|
|
1211
|
+
comments?: UserMessageComment[]
|
|
1212
|
+
}) {
|
|
1213
|
+
const data = useData()
|
|
1214
|
+
const dialog = useDialog()
|
|
1215
|
+
const i18n = useI18n()
|
|
1216
|
+
const [state, setState] = createStore({
|
|
1217
|
+
copied: false,
|
|
1218
|
+
busy: false,
|
|
1219
|
+
})
|
|
1220
|
+
const copied = () => state.copied
|
|
1221
|
+
const busy = () => state.busy
|
|
1222
|
+
|
|
1223
|
+
const textPart = createMemo(
|
|
1224
|
+
() => props.parts?.find((p) => p.type === "text" && !(p as TextPart).synthetic) as TextPart | undefined,
|
|
1225
|
+
)
|
|
1226
|
+
|
|
1227
|
+
const text = createMemo(() => textPart()?.text || "")
|
|
1228
|
+
|
|
1229
|
+
const files = createMemo(() => (props.parts?.filter((p) => p.type === "file") as FilePart[]) ?? [])
|
|
1230
|
+
|
|
1231
|
+
const attachments = createMemo(() => files().filter(attached))
|
|
1232
|
+
|
|
1233
|
+
const messageComments = createMemo(() => (newLayout() ? (props.comments ?? []) : []))
|
|
1234
|
+
|
|
1235
|
+
const inlineFiles = createMemo(() => files().filter(inline))
|
|
1236
|
+
|
|
1237
|
+
const agents = createMemo(() => (props.parts?.filter((p) => p.type === "agent") as AgentPart[]) ?? [])
|
|
1238
|
+
|
|
1239
|
+
const model = createMemo(() => {
|
|
1240
|
+
const providerID = props.message.model?.providerID
|
|
1241
|
+
const modelID = props.message.model?.modelID
|
|
1242
|
+
if (!providerID || !modelID) return ""
|
|
1243
|
+
const match = data.store.provider?.all?.get(providerID)
|
|
1244
|
+
return match?.models?.[modelID]?.name ?? modelID
|
|
1245
|
+
})
|
|
1246
|
+
const timefmt = createMemo(() => new Intl.DateTimeFormat(i18n.locale(), { timeStyle: "short" }))
|
|
1247
|
+
|
|
1248
|
+
const stamp = createMemo(() => {
|
|
1249
|
+
const created = props.message.time?.created
|
|
1250
|
+
if (typeof created !== "number") return ""
|
|
1251
|
+
return timefmt().format(created)
|
|
1252
|
+
})
|
|
1253
|
+
|
|
1254
|
+
const metaHead = createMemo(() => {
|
|
1255
|
+
const agent = props.message.agent
|
|
1256
|
+
const items = [agent ? agent[0]?.toUpperCase() + agent.slice(1) : "", model()]
|
|
1257
|
+
return items.filter((x) => !!x).join("\u00A0\u00B7\u00A0")
|
|
1258
|
+
})
|
|
1259
|
+
|
|
1260
|
+
const metaTail = stamp
|
|
1261
|
+
|
|
1262
|
+
const openImagePreview = (url: string, alt?: string) => {
|
|
1263
|
+
dialog.show(() => <ImagePreview src={url} alt={alt} />)
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
const handleCopy = async () => {
|
|
1267
|
+
const content = text()
|
|
1268
|
+
if (!content) return
|
|
1269
|
+
if (await writeClipboard(content)) {
|
|
1270
|
+
setState("copied", true)
|
|
1271
|
+
setTimeout(() => setState("copied", false), 2000)
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
const revert = () => {
|
|
1276
|
+
const act = props.actions?.revert
|
|
1277
|
+
if (!act || busy()) return
|
|
1278
|
+
setState("busy", true)
|
|
1279
|
+
void Promise.resolve()
|
|
1280
|
+
.then(() =>
|
|
1281
|
+
act({
|
|
1282
|
+
sessionID: props.message.sessionID,
|
|
1283
|
+
messageID: props.message.id,
|
|
1284
|
+
}),
|
|
1285
|
+
)
|
|
1286
|
+
.finally(() => setState("busy", false))
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
const renderAttachments = () => (
|
|
1290
|
+
<Show when={attachments().length > 0}>
|
|
1291
|
+
<div data-slot="user-message-attachments">
|
|
1292
|
+
<For each={attachments()}>
|
|
1293
|
+
{(file) => {
|
|
1294
|
+
const type = kind(file)
|
|
1295
|
+
const name = file.filename ?? i18n.t("ui.message.attachment.alt")
|
|
1296
|
+
|
|
1297
|
+
return (
|
|
1298
|
+
<Show
|
|
1299
|
+
when={newLayout() && type === "file"}
|
|
1300
|
+
fallback={
|
|
1301
|
+
<div
|
|
1302
|
+
data-slot="user-message-attachment"
|
|
1303
|
+
data-type={type}
|
|
1304
|
+
data-clickable={type === "image" ? "true" : undefined}
|
|
1305
|
+
title={type === "file" ? name : undefined}
|
|
1306
|
+
onClick={() => {
|
|
1307
|
+
if (type === "image") openImagePreview(file.url, name)
|
|
1308
|
+
}}
|
|
1309
|
+
>
|
|
1310
|
+
<Show
|
|
1311
|
+
when={type === "image"}
|
|
1312
|
+
fallback={
|
|
1313
|
+
<div data-slot="user-message-attachment-file">
|
|
1314
|
+
<FileIcon node={{ path: name, type: "file" }} />
|
|
1315
|
+
<span data-slot="user-message-attachment-name">{name}</span>
|
|
1316
|
+
</div>
|
|
1317
|
+
}
|
|
1318
|
+
>
|
|
1319
|
+
<img data-slot="user-message-attachment-image" src={file.url} alt={name} />
|
|
1320
|
+
</Show>
|
|
1321
|
+
</div>
|
|
1322
|
+
}
|
|
1323
|
+
>
|
|
1324
|
+
<AttachmentCardV2
|
|
1325
|
+
title={getFilename(name)}
|
|
1326
|
+
hover={name}
|
|
1327
|
+
clickable={!!props.actions?.openAttachment}
|
|
1328
|
+
onClick={() => props.actions?.openAttachment?.(file)}
|
|
1329
|
+
>
|
|
1330
|
+
{typeLabel(name, file.mime)}
|
|
1331
|
+
</AttachmentCardV2>
|
|
1332
|
+
</Show>
|
|
1333
|
+
)
|
|
1334
|
+
}}
|
|
1335
|
+
</For>
|
|
1336
|
+
</div>
|
|
1337
|
+
</Show>
|
|
1338
|
+
)
|
|
1339
|
+
|
|
1340
|
+
return (
|
|
1341
|
+
<div data-component="user-message" data-timeline-part-id={textPart()?.id}>
|
|
1342
|
+
<Show when={!props.useV2Actions}>{renderAttachments()}</Show>
|
|
1343
|
+
<Show
|
|
1344
|
+
when={text()}
|
|
1345
|
+
fallback={
|
|
1346
|
+
<Show when={messageComments().length > 0}>
|
|
1347
|
+
<UserMessageComments comments={messageComments()} bounded={false} />
|
|
1348
|
+
</Show>
|
|
1349
|
+
}
|
|
1350
|
+
>
|
|
1351
|
+
<div data-slot="user-message-body">
|
|
1352
|
+
<div data-slot="user-message-text" data-comments={messageComments().length > 0 ? "true" : undefined}>
|
|
1353
|
+
<HighlightedText text={text()} references={inlineFiles()} agents={agents()} />
|
|
1354
|
+
<Show when={messageComments().length > 0}>
|
|
1355
|
+
<UserMessageComments comments={messageComments()} bounded />
|
|
1356
|
+
</Show>
|
|
1357
|
+
</div>
|
|
1358
|
+
</div>
|
|
1359
|
+
</Show>
|
|
1360
|
+
<Show when={props.useV2Actions}>{renderAttachments()}</Show>
|
|
1361
|
+
<Show when={text() || (props.useV2Actions && messageComments().length > 0)}>
|
|
1362
|
+
<div data-slot="user-message-copy-wrapper">
|
|
1363
|
+
<Show when={metaHead() || metaTail()}>
|
|
1364
|
+
<span data-slot="user-message-meta-wrap">
|
|
1365
|
+
<Show when={metaHead()}>
|
|
1366
|
+
<span data-slot="user-message-meta" class="text-12-regular text-text-weak cursor-default">
|
|
1367
|
+
{metaHead()}
|
|
1368
|
+
</span>
|
|
1369
|
+
</Show>
|
|
1370
|
+
<Show when={metaHead() && metaTail()}>
|
|
1371
|
+
<span data-slot="user-message-meta-sep" class="text-12-regular text-text-weak cursor-default">
|
|
1372
|
+
{"\u00A0\u00B7\u00A0"}
|
|
1373
|
+
</span>
|
|
1374
|
+
</Show>
|
|
1375
|
+
<Show when={metaTail()}>
|
|
1376
|
+
<span data-slot="user-message-meta-tail" class="text-12-regular text-text-weak cursor-default">
|
|
1377
|
+
{metaTail()}
|
|
1378
|
+
</span>
|
|
1379
|
+
</Show>
|
|
1380
|
+
</span>
|
|
1381
|
+
</Show>
|
|
1382
|
+
<Show when={props.actions?.revert}>
|
|
1383
|
+
<MessageActionButton
|
|
1384
|
+
icon="reset"
|
|
1385
|
+
label={i18n.t("ui.message.revertMessage")}
|
|
1386
|
+
useV2={props.useV2Actions}
|
|
1387
|
+
disabled={!!busy()}
|
|
1388
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
1389
|
+
onClick={(event) => {
|
|
1390
|
+
event.stopPropagation()
|
|
1391
|
+
revert()
|
|
1392
|
+
}}
|
|
1393
|
+
aria-label={i18n.t("ui.message.revertMessage")}
|
|
1394
|
+
/>
|
|
1395
|
+
</Show>
|
|
1396
|
+
<Show when={text()}>
|
|
1397
|
+
<MessageActionButton
|
|
1398
|
+
icon={copied() ? "check" : "copy"}
|
|
1399
|
+
label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copyMessage")}
|
|
1400
|
+
useV2={props.useV2Actions}
|
|
1401
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
1402
|
+
onClick={(event) => {
|
|
1403
|
+
event.stopPropagation()
|
|
1404
|
+
void handleCopy()
|
|
1405
|
+
}}
|
|
1406
|
+
aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copyMessage")}
|
|
1407
|
+
/>
|
|
1408
|
+
</Show>
|
|
1409
|
+
</div>
|
|
1410
|
+
</Show>
|
|
1411
|
+
</div>
|
|
1412
|
+
)
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
type HighlightSegment = { text: string; type?: "file" | "agent" }
|
|
1416
|
+
|
|
1417
|
+
function HighlightedText(props: { text: string; references: FilePart[]; agents: AgentPart[] }) {
|
|
1418
|
+
const segments = createMemo(() => {
|
|
1419
|
+
const text = props.text
|
|
1420
|
+
|
|
1421
|
+
const allRefs: { start: number; end: number; type: "file" | "agent" }[] = [
|
|
1422
|
+
...props.references
|
|
1423
|
+
.filter((r) => r.source?.text?.start !== undefined && r.source?.text?.end !== undefined)
|
|
1424
|
+
.map((r) => ({ start: r.source!.text!.start, end: r.source!.text!.end, type: "file" as const })),
|
|
1425
|
+
...props.agents
|
|
1426
|
+
.filter((a) => a.source?.start !== undefined && a.source?.end !== undefined)
|
|
1427
|
+
.map((a) => ({ start: a.source!.start, end: a.source!.end, type: "agent" as const })),
|
|
1428
|
+
].sort((a, b) => a.start - b.start)
|
|
1429
|
+
|
|
1430
|
+
const result: HighlightSegment[] = []
|
|
1431
|
+
let lastIndex = 0
|
|
1432
|
+
|
|
1433
|
+
for (const ref of allRefs) {
|
|
1434
|
+
if (ref.start < lastIndex) continue
|
|
1435
|
+
|
|
1436
|
+
if (ref.start > lastIndex) {
|
|
1437
|
+
result.push({ text: text.slice(lastIndex, ref.start) })
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
result.push({ text: text.slice(ref.start, ref.end), type: ref.type })
|
|
1441
|
+
lastIndex = ref.end
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
if (lastIndex < text.length) {
|
|
1445
|
+
result.push({ text: text.slice(lastIndex) })
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
return result
|
|
1449
|
+
})
|
|
1450
|
+
|
|
1451
|
+
return <For each={segments()}>{(segment) => <span data-highlight={segment.type}>{segment.text}</span>}</For>
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
export function Part(props: MessagePartProps) {
|
|
1455
|
+
const component = createMemo(() => PART_MAPPING[props.part.type])
|
|
1456
|
+
return (
|
|
1457
|
+
<Show when={component()}>
|
|
1458
|
+
<Dynamic
|
|
1459
|
+
component={component()}
|
|
1460
|
+
part={props.part}
|
|
1461
|
+
message={props.message}
|
|
1462
|
+
hideDetails={props.hideDetails}
|
|
1463
|
+
defaultOpen={props.defaultOpen}
|
|
1464
|
+
toolOpen={props.toolOpen}
|
|
1465
|
+
onToolOpenChange={props.onToolOpenChange}
|
|
1466
|
+
deferToolContent={props.deferToolContent}
|
|
1467
|
+
virtualizeDiff={props.virtualizeDiff}
|
|
1468
|
+
onContentRendered={props.onContentRendered}
|
|
1469
|
+
showAssistantCopyPartID={props.showAssistantCopyPartID}
|
|
1470
|
+
turnDurationMs={props.turnDurationMs}
|
|
1471
|
+
useV2Actions={props.useV2Actions}
|
|
1472
|
+
/>
|
|
1473
|
+
</Show>
|
|
1474
|
+
)
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
export interface ToolProps {
|
|
1478
|
+
input: Record<string, any>
|
|
1479
|
+
metadata: Record<string, any>
|
|
1480
|
+
tool: string
|
|
1481
|
+
sessionID?: string
|
|
1482
|
+
output?: string
|
|
1483
|
+
status?: string
|
|
1484
|
+
hideDetails?: boolean
|
|
1485
|
+
defaultOpen?: boolean
|
|
1486
|
+
open?: boolean
|
|
1487
|
+
onOpenChange?: (open: boolean) => void
|
|
1488
|
+
deferContent?: boolean
|
|
1489
|
+
virtualizeDiff?: boolean
|
|
1490
|
+
onContentRendered?: () => void
|
|
1491
|
+
forceOpen?: boolean
|
|
1492
|
+
locked?: boolean
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
export type ToolComponent = Component<ToolProps>
|
|
1496
|
+
|
|
1497
|
+
const state: Record<
|
|
1498
|
+
string,
|
|
1499
|
+
{
|
|
1500
|
+
name: string
|
|
1501
|
+
render?: ToolComponent
|
|
1502
|
+
}
|
|
1503
|
+
> = {}
|
|
1504
|
+
|
|
1505
|
+
export function registerTool(input: { name: string; render?: ToolComponent }) {
|
|
1506
|
+
state[input.name] = input
|
|
1507
|
+
return input
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
export function getTool(name: string) {
|
|
1511
|
+
return state[name === "apply_patch" ? "patch" : name === "bash" ? "shell" : name]?.render
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
export const ToolRegistry = {
|
|
1515
|
+
register: registerTool,
|
|
1516
|
+
render: getTool,
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
function ToolFileAccordion(props: { path: string; actions?: JSX.Element; children: JSX.Element }) {
|
|
1520
|
+
const value = createMemo(() => props.path || "tool-file")
|
|
1521
|
+
|
|
1522
|
+
return (
|
|
1523
|
+
<Accordion
|
|
1524
|
+
multiple
|
|
1525
|
+
data-scope="apply-patch"
|
|
1526
|
+
style={{ "--sticky-accordion-offset": "calc(32px + var(--tool-content-gap))" }}
|
|
1527
|
+
defaultValue={[value()]}
|
|
1528
|
+
>
|
|
1529
|
+
<Accordion.Item value={value()}>
|
|
1530
|
+
<StickyAccordionHeader>
|
|
1531
|
+
<Accordion.Trigger>
|
|
1532
|
+
<div data-slot="apply-patch-trigger-content">
|
|
1533
|
+
<div data-slot="apply-patch-file-info">
|
|
1534
|
+
<FileIcon node={{ path: props.path, type: "file" }} />
|
|
1535
|
+
<div data-slot="apply-patch-file-name-container">
|
|
1536
|
+
<Show when={props.path.includes("/")}>
|
|
1537
|
+
<span data-slot="apply-patch-directory">{`\u202A${getDirectory(props.path)}\u202C`}</span>
|
|
1538
|
+
</Show>
|
|
1539
|
+
<span data-slot="apply-patch-filename">{getFilename(props.path)}</span>
|
|
1540
|
+
</div>
|
|
1541
|
+
</div>
|
|
1542
|
+
<div data-slot="apply-patch-trigger-actions">
|
|
1543
|
+
{props.actions}
|
|
1544
|
+
<Icon name="chevron-grabber-vertical" size="small" />
|
|
1545
|
+
</div>
|
|
1546
|
+
</div>
|
|
1547
|
+
</Accordion.Trigger>
|
|
1548
|
+
</StickyAccordionHeader>
|
|
1549
|
+
<Accordion.Content>{props.children}</Accordion.Content>
|
|
1550
|
+
</Accordion.Item>
|
|
1551
|
+
</Accordion>
|
|
1552
|
+
)
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
PART_MAPPING["tool"] = function ToolPartDisplay(props) {
|
|
1556
|
+
const data = useData()
|
|
1557
|
+
const i18n = useI18n()
|
|
1558
|
+
const part = () => props.part as ToolPart
|
|
1559
|
+
if (part().tool === "todowrite") return null
|
|
1560
|
+
|
|
1561
|
+
const hideQuestion = createMemo(
|
|
1562
|
+
() => part().tool === "question" && (part().state.status === "pending" || part().state.status === "running"),
|
|
1563
|
+
)
|
|
1564
|
+
|
|
1565
|
+
const emptyInput: Record<string, any> = {}
|
|
1566
|
+
const emptyMetadata: Record<string, any> = {}
|
|
1567
|
+
|
|
1568
|
+
const input = () => part().state?.input ?? emptyInput
|
|
1569
|
+
// @ts-expect-error
|
|
1570
|
+
const partMetadata = () => part().state?.metadata ?? emptyMetadata
|
|
1571
|
+
const taskId = createMemo(() => {
|
|
1572
|
+
if (part().tool !== "task") return
|
|
1573
|
+
const value = partMetadata().sessionId
|
|
1574
|
+
if (typeof value === "string" && value) return value
|
|
1575
|
+
})
|
|
1576
|
+
const taskHref = createMemo(() => {
|
|
1577
|
+
if (part().tool !== "task") return
|
|
1578
|
+
return sessionLink(taskId(), useLocation().pathname, data.sessionHref)
|
|
1579
|
+
})
|
|
1580
|
+
const taskSubtitle = createMemo(() => {
|
|
1581
|
+
if (part().tool !== "task") return undefined
|
|
1582
|
+
const value = input().description
|
|
1583
|
+
if (typeof value === "string" && value) return value
|
|
1584
|
+
return taskId()
|
|
1585
|
+
})
|
|
1586
|
+
|
|
1587
|
+
const render = createMemo(() => ToolRegistry.render(part().tool) ?? GenericTool)
|
|
1588
|
+
const controlledOpen = () => (props.onToolOpenChange ? (props.toolOpen ?? props.defaultOpen) : undefined)
|
|
1589
|
+
const handleToolOpenChange = (open: boolean) => props.onToolOpenChange?.(open)
|
|
1590
|
+
|
|
1591
|
+
return (
|
|
1592
|
+
<Show when={!hideQuestion()}>
|
|
1593
|
+
<div data-component="tool-part-wrapper" data-timeline-part-id={part().id}>
|
|
1594
|
+
<Switch>
|
|
1595
|
+
<Match when={part().state.status === "error" && (part().state as any).error}>
|
|
1596
|
+
{(error) => {
|
|
1597
|
+
const cleaned = error().replace("Error: ", "")
|
|
1598
|
+
if (part().tool === "question" && cleaned.includes("dismissed this question")) {
|
|
1599
|
+
return (
|
|
1600
|
+
<div style="width: 100%; display: flex; justify-content: flex-end;">
|
|
1601
|
+
<span class="text-13-regular text-text-weak cursor-default">
|
|
1602
|
+
{i18n.t("ui.messagePart.questions.dismissed")}
|
|
1603
|
+
</span>
|
|
1604
|
+
</div>
|
|
1605
|
+
)
|
|
1606
|
+
}
|
|
1607
|
+
return (
|
|
1608
|
+
<ToolErrorCard
|
|
1609
|
+
tool={part().tool}
|
|
1610
|
+
error={error()}
|
|
1611
|
+
title={part().tool === "websearch" ? webSearchProviderLabel(partMetadata().provider) : undefined}
|
|
1612
|
+
defaultOpen={props.defaultOpen}
|
|
1613
|
+
open={controlledOpen()}
|
|
1614
|
+
onOpenChange={props.onToolOpenChange ? handleToolOpenChange : undefined}
|
|
1615
|
+
subtitle={taskSubtitle()}
|
|
1616
|
+
href={taskHref()}
|
|
1617
|
+
/>
|
|
1618
|
+
)
|
|
1619
|
+
}}
|
|
1620
|
+
</Match>
|
|
1621
|
+
<Match when={true}>
|
|
1622
|
+
<Dynamic
|
|
1623
|
+
component={render()}
|
|
1624
|
+
input={input()}
|
|
1625
|
+
tool={part().tool}
|
|
1626
|
+
sessionID={part().sessionID}
|
|
1627
|
+
metadata={partMetadata()}
|
|
1628
|
+
// @ts-expect-error
|
|
1629
|
+
output={part().state.output}
|
|
1630
|
+
status={part().state.status}
|
|
1631
|
+
hideDetails={props.hideDetails}
|
|
1632
|
+
defaultOpen={props.defaultOpen}
|
|
1633
|
+
open={controlledOpen()}
|
|
1634
|
+
onOpenChange={props.onToolOpenChange ? handleToolOpenChange : undefined}
|
|
1635
|
+
deferContent={props.deferToolContent}
|
|
1636
|
+
virtualizeDiff={props.virtualizeDiff}
|
|
1637
|
+
onContentRendered={props.onContentRendered}
|
|
1638
|
+
/>
|
|
1639
|
+
</Match>
|
|
1640
|
+
</Switch>
|
|
1641
|
+
</div>
|
|
1642
|
+
</Show>
|
|
1643
|
+
)
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
export function MessageDivider(props: { label: string }) {
|
|
1647
|
+
return (
|
|
1648
|
+
<div data-component="compaction-part">
|
|
1649
|
+
<div data-slot="compaction-part-divider">
|
|
1650
|
+
<span data-slot="compaction-part-line" />
|
|
1651
|
+
<span data-slot="compaction-part-label" class="text-12-regular text-text-weak">
|
|
1652
|
+
{props.label}
|
|
1653
|
+
</span>
|
|
1654
|
+
<span data-slot="compaction-part-line" />
|
|
1655
|
+
</div>
|
|
1656
|
+
</div>
|
|
1657
|
+
)
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
PART_MAPPING["compaction"] = function CompactionPartDisplay() {
|
|
1661
|
+
const i18n = useI18n()
|
|
1662
|
+
return <MessageDivider label={i18n.t("ui.messagePart.compaction")} />
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
PART_MAPPING["text"] = function TextPartDisplay(props) {
|
|
1666
|
+
const data = useData()
|
|
1667
|
+
const i18n = useI18n()
|
|
1668
|
+
const numfmt = createMemo(() => new Intl.NumberFormat(i18n.locale()))
|
|
1669
|
+
const part = () => props.part as TextPart
|
|
1670
|
+
const interrupted = createMemo(
|
|
1671
|
+
() =>
|
|
1672
|
+
props.message.role === "assistant" && (props.message as AssistantMessage).error?.name === "MessageAbortedError",
|
|
1673
|
+
)
|
|
1674
|
+
|
|
1675
|
+
const model = createMemo(() => {
|
|
1676
|
+
if (props.message.role !== "assistant") return ""
|
|
1677
|
+
const message = props.message as AssistantMessage
|
|
1678
|
+
const match = data.store.provider?.all?.get(message.providerID)
|
|
1679
|
+
return match?.models?.[message.modelID]?.name ?? message.modelID
|
|
1680
|
+
})
|
|
1681
|
+
|
|
1682
|
+
const duration = createMemo(() => {
|
|
1683
|
+
if (props.message.role !== "assistant") return ""
|
|
1684
|
+
const message = props.message as AssistantMessage
|
|
1685
|
+
const completed = message.time.completed
|
|
1686
|
+
const ms =
|
|
1687
|
+
typeof props.turnDurationMs === "number"
|
|
1688
|
+
? props.turnDurationMs
|
|
1689
|
+
: typeof completed === "number"
|
|
1690
|
+
? completed - message.time.created
|
|
1691
|
+
: -1
|
|
1692
|
+
if (!(ms >= 0)) return ""
|
|
1693
|
+
const total = Math.round(ms / 1000)
|
|
1694
|
+
if (total < 60) return i18n.t("ui.message.duration.seconds", { count: numfmt().format(total) })
|
|
1695
|
+
const minutes = Math.floor(total / 60)
|
|
1696
|
+
const seconds = total % 60
|
|
1697
|
+
return i18n.t("ui.message.duration.minutesSeconds", {
|
|
1698
|
+
minutes: numfmt().format(minutes),
|
|
1699
|
+
seconds: numfmt().format(seconds),
|
|
1700
|
+
})
|
|
1701
|
+
})
|
|
1702
|
+
|
|
1703
|
+
const meta = createMemo(() => {
|
|
1704
|
+
if (props.message.role !== "assistant") return ""
|
|
1705
|
+
const agent = (props.message as AssistantMessage).agent
|
|
1706
|
+
const items = [
|
|
1707
|
+
agent ? agent[0]?.toUpperCase() + agent.slice(1) : "",
|
|
1708
|
+
model(),
|
|
1709
|
+
duration(),
|
|
1710
|
+
interrupted() ? i18n.t("ui.message.interrupted") : "",
|
|
1711
|
+
]
|
|
1712
|
+
return items.filter((x) => !!x).join(" \u00B7 ")
|
|
1713
|
+
})
|
|
1714
|
+
|
|
1715
|
+
const streaming = createMemo(
|
|
1716
|
+
() => props.message.role === "assistant" && typeof (props.message as AssistantMessage).time.completed !== "number",
|
|
1717
|
+
)
|
|
1718
|
+
const text = () => readPartText(data.store.part_text_accum_delta, part())
|
|
1719
|
+
const isLastTextPart = createMemo(() => {
|
|
1720
|
+
const last = (data.store.part?.[props.message.id] ?? [])
|
|
1721
|
+
.filter((item): item is TextPart => item?.type === "text" && !!item.text?.trim())
|
|
1722
|
+
.at(-1)
|
|
1723
|
+
return last?.id === part().id
|
|
1724
|
+
})
|
|
1725
|
+
const showCopy = createMemo(() => {
|
|
1726
|
+
if (props.message.role !== "assistant") return isLastTextPart()
|
|
1727
|
+
if (props.showAssistantCopyPartID === null) return false
|
|
1728
|
+
if (typeof props.showAssistantCopyPartID === "string") return props.showAssistantCopyPartID === part().id
|
|
1729
|
+
return isLastTextPart()
|
|
1730
|
+
})
|
|
1731
|
+
const [copied, setCopied] = createSignal(false)
|
|
1732
|
+
|
|
1733
|
+
const handleCopy = async () => {
|
|
1734
|
+
const content = text()
|
|
1735
|
+
if (!content) return
|
|
1736
|
+
if (await writeClipboard(content)) {
|
|
1737
|
+
setCopied(true)
|
|
1738
|
+
setTimeout(() => setCopied(false), 2000)
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
return (
|
|
1743
|
+
<Show when={text()}>
|
|
1744
|
+
<div data-component="text-part" data-timeline-part-id={part().id}>
|
|
1745
|
+
<div data-slot="text-part-body">
|
|
1746
|
+
<Show when={streaming()} fallback={<Markdown text={text()} cacheKey={part().id} streaming={false} />}>
|
|
1747
|
+
<PacedMarkdown text={text()} cacheKey={part().id} streaming={streaming()} />
|
|
1748
|
+
</Show>
|
|
1749
|
+
</div>
|
|
1750
|
+
<Show when={showCopy()}>
|
|
1751
|
+
<div data-slot="text-part-copy-wrapper" data-interrupted={interrupted() ? "" : undefined}>
|
|
1752
|
+
<MessageActionButton
|
|
1753
|
+
icon={copied() ? "check" : "copy"}
|
|
1754
|
+
label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copyResponse")}
|
|
1755
|
+
useV2={props.useV2Actions}
|
|
1756
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
1757
|
+
onClick={handleCopy}
|
|
1758
|
+
aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copyResponse")}
|
|
1759
|
+
/>
|
|
1760
|
+
<Show when={meta()}>
|
|
1761
|
+
<span data-slot="text-part-meta" class="text-12-regular text-text-weak cursor-default">
|
|
1762
|
+
{meta()}
|
|
1763
|
+
</span>
|
|
1764
|
+
</Show>
|
|
1765
|
+
</div>
|
|
1766
|
+
</Show>
|
|
1767
|
+
</div>
|
|
1768
|
+
</Show>
|
|
1769
|
+
)
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props) {
|
|
1773
|
+
const data = useData()
|
|
1774
|
+
const part = () => props.part as ReasoningPart
|
|
1775
|
+
const streaming = createMemo(
|
|
1776
|
+
() => props.message.role === "assistant" && typeof (props.message as AssistantMessage).time.completed !== "number",
|
|
1777
|
+
)
|
|
1778
|
+
const text = () => readPartText(data.store.part_text_accum_delta, part())
|
|
1779
|
+
|
|
1780
|
+
return (
|
|
1781
|
+
<Show when={text()}>
|
|
1782
|
+
<div data-component="reasoning-part" data-timeline-part-id={part().id}>
|
|
1783
|
+
<Show when={streaming()} fallback={<Markdown text={text()} cacheKey={part().id} streaming={false} />}>
|
|
1784
|
+
<PacedMarkdown text={text()} cacheKey={part().id} streaming={streaming()} />
|
|
1785
|
+
</Show>
|
|
1786
|
+
</div>
|
|
1787
|
+
</Show>
|
|
1788
|
+
)
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
ToolRegistry.register({
|
|
1792
|
+
name: "read",
|
|
1793
|
+
render(props) {
|
|
1794
|
+
const data = useData()
|
|
1795
|
+
const i18n = useI18n()
|
|
1796
|
+
const args: string[] = []
|
|
1797
|
+
if (props.input.offset) args.push("offset=" + props.input.offset)
|
|
1798
|
+
if (props.input.limit) args.push("limit=" + props.input.limit)
|
|
1799
|
+
const loaded = createMemo(() => {
|
|
1800
|
+
if (props.status !== "completed") return []
|
|
1801
|
+
const value = props.metadata.loaded
|
|
1802
|
+
if (!value || !Array.isArray(value)) return []
|
|
1803
|
+
return value.filter((p): p is string => typeof p === "string")
|
|
1804
|
+
})
|
|
1805
|
+
return (
|
|
1806
|
+
<>
|
|
1807
|
+
<BasicTool
|
|
1808
|
+
{...props}
|
|
1809
|
+
icon="glasses"
|
|
1810
|
+
trigger={{
|
|
1811
|
+
title: i18n.t("ui.tool.read"),
|
|
1812
|
+
subtitle: props.input.filePath ? getFilename(props.input.filePath) : "",
|
|
1813
|
+
args,
|
|
1814
|
+
}}
|
|
1815
|
+
/>
|
|
1816
|
+
<For each={loaded()}>
|
|
1817
|
+
{(filepath) => (
|
|
1818
|
+
<div data-component="tool-loaded-file">
|
|
1819
|
+
<Icon name="enter" size="small" />
|
|
1820
|
+
<span>
|
|
1821
|
+
{i18n.t("ui.tool.loaded")} {relativizeProjectPath(filepath, data.directory)}
|
|
1822
|
+
</span>
|
|
1823
|
+
</div>
|
|
1824
|
+
)}
|
|
1825
|
+
</For>
|
|
1826
|
+
</>
|
|
1827
|
+
)
|
|
1828
|
+
},
|
|
1829
|
+
})
|
|
1830
|
+
|
|
1831
|
+
ToolRegistry.register({
|
|
1832
|
+
name: "list",
|
|
1833
|
+
render(props) {
|
|
1834
|
+
const i18n = useI18n()
|
|
1835
|
+
return (
|
|
1836
|
+
<BasicTool
|
|
1837
|
+
{...props}
|
|
1838
|
+
icon="bullet-list"
|
|
1839
|
+
trigger={{ title: i18n.t("ui.tool.list"), subtitle: getDirectory(props.input.path || "/") }}
|
|
1840
|
+
>
|
|
1841
|
+
<Show when={props.output}>
|
|
1842
|
+
<div
|
|
1843
|
+
data-component="tool-output"
|
|
1844
|
+
data-scrollable
|
|
1845
|
+
tabIndex={0}
|
|
1846
|
+
role="region"
|
|
1847
|
+
aria-label={i18n.t("ui.scrollView.ariaLabel")}
|
|
1848
|
+
>
|
|
1849
|
+
<Markdown text={props.output!} />
|
|
1850
|
+
</div>
|
|
1851
|
+
</Show>
|
|
1852
|
+
</BasicTool>
|
|
1853
|
+
)
|
|
1854
|
+
},
|
|
1855
|
+
})
|
|
1856
|
+
|
|
1857
|
+
ToolRegistry.register({
|
|
1858
|
+
name: "glob",
|
|
1859
|
+
render(props) {
|
|
1860
|
+
const i18n = useI18n()
|
|
1861
|
+
return (
|
|
1862
|
+
<BasicTool
|
|
1863
|
+
{...props}
|
|
1864
|
+
icon="magnifying-glass-menu"
|
|
1865
|
+
trigger={{
|
|
1866
|
+
title: i18n.t("ui.tool.glob"),
|
|
1867
|
+
subtitle: getDirectory(props.input.path || "/"),
|
|
1868
|
+
args: props.input.pattern ? ["pattern=" + props.input.pattern] : [],
|
|
1869
|
+
}}
|
|
1870
|
+
>
|
|
1871
|
+
<Show when={props.output}>
|
|
1872
|
+
<div
|
|
1873
|
+
data-component="tool-output"
|
|
1874
|
+
data-scrollable
|
|
1875
|
+
tabIndex={0}
|
|
1876
|
+
role="region"
|
|
1877
|
+
aria-label={i18n.t("ui.scrollView.ariaLabel")}
|
|
1878
|
+
>
|
|
1879
|
+
<Markdown text={props.output!} />
|
|
1880
|
+
</div>
|
|
1881
|
+
</Show>
|
|
1882
|
+
</BasicTool>
|
|
1883
|
+
)
|
|
1884
|
+
},
|
|
1885
|
+
})
|
|
1886
|
+
|
|
1887
|
+
ToolRegistry.register({
|
|
1888
|
+
name: "grep",
|
|
1889
|
+
render(props) {
|
|
1890
|
+
const i18n = useI18n()
|
|
1891
|
+
const args: string[] = []
|
|
1892
|
+
if (props.input.pattern) args.push("pattern=" + props.input.pattern)
|
|
1893
|
+
if (props.input.include) args.push("include=" + props.input.include)
|
|
1894
|
+
return (
|
|
1895
|
+
<BasicTool
|
|
1896
|
+
{...props}
|
|
1897
|
+
icon="magnifying-glass-menu"
|
|
1898
|
+
trigger={{
|
|
1899
|
+
title: i18n.t("ui.tool.grep"),
|
|
1900
|
+
subtitle: getDirectory(props.input.path || "/"),
|
|
1901
|
+
args,
|
|
1902
|
+
}}
|
|
1903
|
+
>
|
|
1904
|
+
<Show when={props.output}>
|
|
1905
|
+
<div
|
|
1906
|
+
data-component="tool-output"
|
|
1907
|
+
data-scrollable
|
|
1908
|
+
tabIndex={0}
|
|
1909
|
+
role="region"
|
|
1910
|
+
aria-label={i18n.t("ui.scrollView.ariaLabel")}
|
|
1911
|
+
>
|
|
1912
|
+
<Markdown text={props.output!} />
|
|
1913
|
+
</div>
|
|
1914
|
+
</Show>
|
|
1915
|
+
</BasicTool>
|
|
1916
|
+
)
|
|
1917
|
+
},
|
|
1918
|
+
})
|
|
1919
|
+
|
|
1920
|
+
ToolRegistry.register({
|
|
1921
|
+
name: "webfetch",
|
|
1922
|
+
render(props) {
|
|
1923
|
+
const i18n = useI18n()
|
|
1924
|
+
const pending = createMemo(() => props.status === "pending" || props.status === "running")
|
|
1925
|
+
const url = createMemo(() => {
|
|
1926
|
+
const value = props.input.url
|
|
1927
|
+
if (typeof value !== "string") return ""
|
|
1928
|
+
return value
|
|
1929
|
+
})
|
|
1930
|
+
return (
|
|
1931
|
+
<BasicTool
|
|
1932
|
+
{...props}
|
|
1933
|
+
hideDetails
|
|
1934
|
+
icon="window-cursor"
|
|
1935
|
+
trigger={
|
|
1936
|
+
<div data-slot="basic-tool-tool-info-structured">
|
|
1937
|
+
<div data-slot="basic-tool-tool-info-main">
|
|
1938
|
+
<span data-slot="basic-tool-tool-title">
|
|
1939
|
+
<TextShimmer text={i18n.t("ui.tool.webfetch")} active={pending()} />
|
|
1940
|
+
</span>
|
|
1941
|
+
<Show when={!pending() && url()}>
|
|
1942
|
+
<a
|
|
1943
|
+
data-slot="basic-tool-tool-subtitle"
|
|
1944
|
+
class="clickable subagent-link"
|
|
1945
|
+
href={url()}
|
|
1946
|
+
target="_blank"
|
|
1947
|
+
rel="noopener noreferrer"
|
|
1948
|
+
onClick={(event) => event.stopPropagation()}
|
|
1949
|
+
>
|
|
1950
|
+
{url()}
|
|
1951
|
+
</a>
|
|
1952
|
+
</Show>
|
|
1953
|
+
</div>
|
|
1954
|
+
<Show when={!pending() && url()}>
|
|
1955
|
+
<div data-component="tool-action">
|
|
1956
|
+
<Icon name="square-arrow-top-right" size="small" />
|
|
1957
|
+
</div>
|
|
1958
|
+
</Show>
|
|
1959
|
+
</div>
|
|
1960
|
+
}
|
|
1961
|
+
/>
|
|
1962
|
+
)
|
|
1963
|
+
},
|
|
1964
|
+
})
|
|
1965
|
+
|
|
1966
|
+
ToolRegistry.register({
|
|
1967
|
+
name: "websearch",
|
|
1968
|
+
render(props) {
|
|
1969
|
+
const query = createMemo(() => {
|
|
1970
|
+
const value = props.input.query
|
|
1971
|
+
if (typeof value !== "string") return ""
|
|
1972
|
+
return value
|
|
1973
|
+
})
|
|
1974
|
+
const title = createMemo(() => webSearchProviderLabel(props.metadata.provider))
|
|
1975
|
+
|
|
1976
|
+
return (
|
|
1977
|
+
<BasicTool
|
|
1978
|
+
{...props}
|
|
1979
|
+
icon="window-cursor"
|
|
1980
|
+
trigger={{
|
|
1981
|
+
title: title(),
|
|
1982
|
+
subtitle: query(),
|
|
1983
|
+
subtitleClass: "exa-tool-query",
|
|
1984
|
+
}}
|
|
1985
|
+
>
|
|
1986
|
+
<ExaOutput output={props.output} />
|
|
1987
|
+
</BasicTool>
|
|
1988
|
+
)
|
|
1989
|
+
},
|
|
1990
|
+
})
|
|
1991
|
+
|
|
1992
|
+
ToolRegistry.register({
|
|
1993
|
+
name: "task",
|
|
1994
|
+
render(props) {
|
|
1995
|
+
const data = useData()
|
|
1996
|
+
const i18n = useI18n()
|
|
1997
|
+
const location = useLocation()
|
|
1998
|
+
const childSessionId = createMemo(() => {
|
|
1999
|
+
const value = props.metadata.sessionId
|
|
2000
|
+
if (typeof value === "string" && value) return value
|
|
2001
|
+
return taskSession(props.input, location.pathname, data.store.session, data.store.agent)
|
|
2002
|
+
})
|
|
2003
|
+
const agent = createMemo(() => taskAgent(props.input.subagent_type, data.store.agent))
|
|
2004
|
+
const title = createMemo(() => agent().name ?? i18n.t("ui.tool.agent.default"))
|
|
2005
|
+
const tone = createMemo(() => agent().color)
|
|
2006
|
+
const v2Tone = createMemo(() => agent().v2Color)
|
|
2007
|
+
const subtitle = createMemo(() => {
|
|
2008
|
+
const value =
|
|
2009
|
+
typeof props.input.description === "string" && props.input.description
|
|
2010
|
+
? props.input.description
|
|
2011
|
+
: childSessionId()
|
|
2012
|
+
if (!value) return value
|
|
2013
|
+
if (props.metadata.background === true) return `${value} (background)`
|
|
2014
|
+
return value
|
|
2015
|
+
})
|
|
2016
|
+
const running = createMemo(() => props.status === "pending" || props.status === "running")
|
|
2017
|
+
|
|
2018
|
+
const href = createMemo(() => sessionLink(childSessionId(), location.pathname, data.sessionHref))
|
|
2019
|
+
const clickable = createMemo(() => !!(childSessionId() && (data.navigateToSession || href())))
|
|
2020
|
+
|
|
2021
|
+
const open = () => {
|
|
2022
|
+
const id = childSessionId()
|
|
2023
|
+
if (!id) return
|
|
2024
|
+
if (data.navigateToSession) {
|
|
2025
|
+
data.navigateToSession(id)
|
|
2026
|
+
return
|
|
2027
|
+
}
|
|
2028
|
+
const value = href()
|
|
2029
|
+
if (value) window.location.assign(value)
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
const navigate = (event: MouseEvent) => {
|
|
2033
|
+
if (!data.navigateToSession) return
|
|
2034
|
+
if (event.button !== 0 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return
|
|
2035
|
+
event.preventDefault()
|
|
2036
|
+
open()
|
|
2037
|
+
}
|
|
2038
|
+
const navigateKey = (event: KeyboardEvent) => {
|
|
2039
|
+
if (!clickable() || href()) return
|
|
2040
|
+
if (event.key !== "Enter" && event.key !== " ") return
|
|
2041
|
+
event.preventDefault()
|
|
2042
|
+
open()
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
const trigger = () => (
|
|
2046
|
+
<div
|
|
2047
|
+
data-component="task-tool-card"
|
|
2048
|
+
style={{
|
|
2049
|
+
"--task-agent-color": v2Tone(),
|
|
2050
|
+
"--task-agent-legacy-color": tone(),
|
|
2051
|
+
}}
|
|
2052
|
+
>
|
|
2053
|
+
<div data-component="task-tool-surface">
|
|
2054
|
+
<div data-slot="basic-tool-tool-info-structured">
|
|
2055
|
+
<div data-slot="basic-tool-tool-info-main">
|
|
2056
|
+
<Show
|
|
2057
|
+
when={running()}
|
|
2058
|
+
fallback={
|
|
2059
|
+
<Show when={newLayout()}>
|
|
2060
|
+
<span data-component="task-tool-icon">
|
|
2061
|
+
<Icon name="subagent" size="small" />
|
|
2062
|
+
</span>
|
|
2063
|
+
</Show>
|
|
2064
|
+
}
|
|
2065
|
+
>
|
|
2066
|
+
<span data-component="task-tool-spinner" style={{ color: tone() ?? "var(--icon-interactive-base)" }}>
|
|
2067
|
+
<Show when={newLayout()} fallback={<Spinner />}>
|
|
2068
|
+
<SessionProgressIndicatorV2
|
|
2069
|
+
style={{ color: v2Tone() ?? "light-dark(var(--v2-text-text-base), #ffffff)" }}
|
|
2070
|
+
/>
|
|
2071
|
+
</Show>
|
|
2072
|
+
</span>
|
|
2073
|
+
</Show>
|
|
2074
|
+
<span data-component="task-tool-title">{title()}</span>
|
|
2075
|
+
<Show when={subtitle()}>
|
|
2076
|
+
<span data-slot="basic-tool-tool-subtitle">{subtitle()}</span>
|
|
2077
|
+
</Show>
|
|
2078
|
+
</div>
|
|
2079
|
+
</div>
|
|
2080
|
+
</div>
|
|
2081
|
+
<Show when={clickable()}>
|
|
2082
|
+
<div data-component="task-tool-action">
|
|
2083
|
+
<Icon name="square-arrow-top-right" size="small" />
|
|
2084
|
+
</div>
|
|
2085
|
+
</Show>
|
|
2086
|
+
</div>
|
|
2087
|
+
)
|
|
2088
|
+
|
|
2089
|
+
return (
|
|
2090
|
+
<BasicTool
|
|
2091
|
+
icon="task"
|
|
2092
|
+
status={props.status}
|
|
2093
|
+
trigger={trigger()}
|
|
2094
|
+
hideDetails
|
|
2095
|
+
triggerAsLink
|
|
2096
|
+
triggerHref={href()}
|
|
2097
|
+
clickable={clickable()}
|
|
2098
|
+
onTriggerClick={navigate}
|
|
2099
|
+
onTriggerKeyDown={navigateKey}
|
|
2100
|
+
/>
|
|
2101
|
+
)
|
|
2102
|
+
},
|
|
2103
|
+
})
|
|
2104
|
+
|
|
2105
|
+
ToolRegistry.register({
|
|
2106
|
+
name: "shell",
|
|
2107
|
+
render(props) {
|
|
2108
|
+
const i18n = useI18n()
|
|
2109
|
+
const pending = () => props.status === "pending" || props.status === "running"
|
|
2110
|
+
const sawPending = pending()
|
|
2111
|
+
const text = createMemo(() => {
|
|
2112
|
+
const cmd = props.input.command ?? props.metadata.command ?? ""
|
|
2113
|
+
const out = stripAnsi(props.output || props.metadata.output || "").replace(/\r\n?/g, "\n")
|
|
2114
|
+
return `$ ${cmd}${out ? "\n\n" + out : ""}`
|
|
2115
|
+
})
|
|
2116
|
+
const [copied, setCopied] = createSignal(false)
|
|
2117
|
+
|
|
2118
|
+
const handleCopy = async () => {
|
|
2119
|
+
const content = text()
|
|
2120
|
+
if (!content) return
|
|
2121
|
+
if (await writeClipboard(content)) {
|
|
2122
|
+
setCopied(true)
|
|
2123
|
+
setTimeout(() => setCopied(false), 2000)
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
return (
|
|
2128
|
+
<BasicTool
|
|
2129
|
+
{...props}
|
|
2130
|
+
icon="console"
|
|
2131
|
+
allowOpenWhilePending
|
|
2132
|
+
trigger={(open) => (
|
|
2133
|
+
<div data-slot="basic-tool-tool-info-structured">
|
|
2134
|
+
<div data-slot="basic-tool-tool-info-main">
|
|
2135
|
+
<span data-slot="basic-tool-tool-title">
|
|
2136
|
+
<TextShimmer text={i18n.t("ui.tool.shell")} active={pending()} />
|
|
2137
|
+
</span>
|
|
2138
|
+
<Show when={!open() && props.input.command}>
|
|
2139
|
+
<ShellSubmessage text={props.input.command} animate={sawPending} />
|
|
2140
|
+
</Show>
|
|
2141
|
+
</div>
|
|
2142
|
+
</div>
|
|
2143
|
+
)}
|
|
2144
|
+
>
|
|
2145
|
+
<div data-component="bash-output">
|
|
2146
|
+
<div data-slot="bash-copy">
|
|
2147
|
+
<TooltipV2 value={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")} placement="top">
|
|
2148
|
+
<IconButtonV2
|
|
2149
|
+
icon={<IconV2 name={copied() ? "check" : "outline-copy"} size="small" />}
|
|
2150
|
+
size="normal"
|
|
2151
|
+
variant="ghost-muted"
|
|
2152
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
2153
|
+
onClick={handleCopy}
|
|
2154
|
+
aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")}
|
|
2155
|
+
/>
|
|
2156
|
+
</TooltipV2>
|
|
2157
|
+
</div>
|
|
2158
|
+
<div
|
|
2159
|
+
data-slot="bash-scroll"
|
|
2160
|
+
data-scrollable
|
|
2161
|
+
tabIndex={0}
|
|
2162
|
+
role="region"
|
|
2163
|
+
aria-label={i18n.t("ui.scrollView.ariaLabel")}
|
|
2164
|
+
>
|
|
2165
|
+
<pre data-slot="bash-pre">
|
|
2166
|
+
<code>{text()}</code>
|
|
2167
|
+
</pre>
|
|
2168
|
+
</div>
|
|
2169
|
+
</div>
|
|
2170
|
+
</BasicTool>
|
|
2171
|
+
)
|
|
2172
|
+
},
|
|
2173
|
+
})
|
|
2174
|
+
|
|
2175
|
+
ToolRegistry.register({
|
|
2176
|
+
name: "edit",
|
|
2177
|
+
render(props) {
|
|
2178
|
+
const i18n = useI18n()
|
|
2179
|
+
const fileComponent = useFileComponent()
|
|
2180
|
+
const diagnostics = createMemo(() => getDiagnostics(props.metadata.diagnostics, props.input.filePath))
|
|
2181
|
+
const path = createMemo(() => props.metadata?.filediff?.file || props.input.filePath || "")
|
|
2182
|
+
const filename = () => getFilename(props.input.filePath ?? "")
|
|
2183
|
+
const pending = () => props.status === "pending" || props.status === "running"
|
|
2184
|
+
const diffSource = createMemo(
|
|
2185
|
+
() => {
|
|
2186
|
+
const filediff = props.metadata?.filediff
|
|
2187
|
+
if (!filediff) return
|
|
2188
|
+
return {
|
|
2189
|
+
file: filediff.file || props.input.filePath || "",
|
|
2190
|
+
patch: typeof filediff.patch === "string" ? filediff.patch : undefined,
|
|
2191
|
+
before: typeof filediff.before === "string" ? filediff.before : undefined,
|
|
2192
|
+
after: typeof filediff.after === "string" ? filediff.after : undefined,
|
|
2193
|
+
}
|
|
2194
|
+
},
|
|
2195
|
+
undefined,
|
|
2196
|
+
{
|
|
2197
|
+
equals: (a, b) =>
|
|
2198
|
+
a?.file === b?.file && a?.patch === b?.patch && a?.before === b?.before && a?.after === b?.after,
|
|
2199
|
+
},
|
|
2200
|
+
)
|
|
2201
|
+
|
|
2202
|
+
const fileCompProps = createMemo(() => {
|
|
2203
|
+
try {
|
|
2204
|
+
const source = diffSource()
|
|
2205
|
+
if (source) {
|
|
2206
|
+
const fileDiff = resolveFileDiff(source)
|
|
2207
|
+
if (fileDiff) return { fileDiff, hunkSeparators: fileDiff.isPartial ? "simple" : "line-info-basic" }
|
|
2208
|
+
}
|
|
2209
|
+
} catch {}
|
|
2210
|
+
|
|
2211
|
+
return {
|
|
2212
|
+
before: {
|
|
2213
|
+
name: props.metadata?.filediff?.file || props.input.filePath,
|
|
2214
|
+
contents: props.metadata?.filediff?.before || props.input.oldString || "",
|
|
2215
|
+
},
|
|
2216
|
+
after: {
|
|
2217
|
+
name: props.metadata?.filediff?.file || props.input.filePath,
|
|
2218
|
+
contents: props.metadata?.filediff?.after || props.input.newString || "",
|
|
2219
|
+
},
|
|
2220
|
+
}
|
|
2221
|
+
})
|
|
2222
|
+
|
|
2223
|
+
return (
|
|
2224
|
+
<div data-component="edit-tool">
|
|
2225
|
+
<BasicTool
|
|
2226
|
+
{...props}
|
|
2227
|
+
icon="code-lines"
|
|
2228
|
+
defer={props.deferContent !== false}
|
|
2229
|
+
trigger={
|
|
2230
|
+
<div data-component="edit-trigger">
|
|
2231
|
+
<div data-slot="message-part-title-area">
|
|
2232
|
+
<div data-slot="message-part-title">
|
|
2233
|
+
<span data-slot="message-part-title-text">
|
|
2234
|
+
<TextShimmer text={i18n.t("ui.messagePart.title.edit")} active={pending()} />
|
|
2235
|
+
</span>
|
|
2236
|
+
<Show when={!pending()}>
|
|
2237
|
+
<span data-slot="message-part-title-filename">{filename()}</span>
|
|
2238
|
+
</Show>
|
|
2239
|
+
</div>
|
|
2240
|
+
<Show when={!pending() && props.input.filePath?.includes("/")}>
|
|
2241
|
+
<div data-slot="message-part-path">
|
|
2242
|
+
<span data-slot="message-part-directory">{getDirectory(props.input.filePath!)}</span>
|
|
2243
|
+
</div>
|
|
2244
|
+
</Show>
|
|
2245
|
+
</div>
|
|
2246
|
+
<div data-slot="message-part-actions">
|
|
2247
|
+
<Show when={!pending() && props.metadata.filediff}>
|
|
2248
|
+
<DiffChanges changes={props.metadata.filediff} />
|
|
2249
|
+
</Show>
|
|
2250
|
+
</div>
|
|
2251
|
+
</div>
|
|
2252
|
+
}
|
|
2253
|
+
>
|
|
2254
|
+
<Show when={path()}>
|
|
2255
|
+
<ToolFileAccordion
|
|
2256
|
+
path={path()}
|
|
2257
|
+
actions={
|
|
2258
|
+
<Show when={!pending() && props.metadata.filediff}>
|
|
2259
|
+
<DiffChanges changes={props.metadata.filediff!} />
|
|
2260
|
+
</Show>
|
|
2261
|
+
}
|
|
2262
|
+
>
|
|
2263
|
+
<div data-component="edit-content">
|
|
2264
|
+
<Dynamic
|
|
2265
|
+
component={fileComponent}
|
|
2266
|
+
mode="diff"
|
|
2267
|
+
virtualize={props.virtualizeDiff}
|
|
2268
|
+
onRendered={props.onContentRendered}
|
|
2269
|
+
{...fileCompProps()}
|
|
2270
|
+
/>
|
|
2271
|
+
</div>
|
|
2272
|
+
</ToolFileAccordion>
|
|
2273
|
+
</Show>
|
|
2274
|
+
<DiagnosticsDisplay diagnostics={diagnostics()} />
|
|
2275
|
+
</BasicTool>
|
|
2276
|
+
</div>
|
|
2277
|
+
)
|
|
2278
|
+
},
|
|
2279
|
+
})
|
|
2280
|
+
|
|
2281
|
+
ToolRegistry.register({
|
|
2282
|
+
name: "write",
|
|
2283
|
+
render(props) {
|
|
2284
|
+
const i18n = useI18n()
|
|
2285
|
+
const fileComponent = useFileComponent()
|
|
2286
|
+
const diagnostics = createMemo(() => getDiagnostics(props.metadata.diagnostics, props.input.filePath))
|
|
2287
|
+
const path = createMemo(() => props.input.filePath || "")
|
|
2288
|
+
const filename = () => getFilename(props.input.filePath ?? "")
|
|
2289
|
+
const pending = () => props.status === "pending" || props.status === "running"
|
|
2290
|
+
return (
|
|
2291
|
+
<div data-component="write-tool">
|
|
2292
|
+
<BasicTool
|
|
2293
|
+
{...props}
|
|
2294
|
+
icon="code-lines"
|
|
2295
|
+
defer={props.deferContent !== false}
|
|
2296
|
+
trigger={
|
|
2297
|
+
<div data-component="write-trigger">
|
|
2298
|
+
<div data-slot="message-part-title-area">
|
|
2299
|
+
<div data-slot="message-part-title">
|
|
2300
|
+
<span data-slot="message-part-title-text">
|
|
2301
|
+
<TextShimmer text={i18n.t("ui.messagePart.title.write")} active={pending()} />
|
|
2302
|
+
</span>
|
|
2303
|
+
<Show when={!pending()}>
|
|
2304
|
+
<span data-slot="message-part-title-filename">{filename()}</span>
|
|
2305
|
+
</Show>
|
|
2306
|
+
</div>
|
|
2307
|
+
<Show when={!pending() && props.input.filePath?.includes("/")}>
|
|
2308
|
+
<div data-slot="message-part-path">
|
|
2309
|
+
<span data-slot="message-part-directory">{getDirectory(props.input.filePath!)}</span>
|
|
2310
|
+
</div>
|
|
2311
|
+
</Show>
|
|
2312
|
+
</div>
|
|
2313
|
+
<div data-slot="message-part-actions">{/* <DiffChanges diff={diff} /> */}</div>
|
|
2314
|
+
</div>
|
|
2315
|
+
}
|
|
2316
|
+
>
|
|
2317
|
+
<Show when={props.input.content && path()}>
|
|
2318
|
+
<ToolFileAccordion path={path()}>
|
|
2319
|
+
<div data-component="write-content">
|
|
2320
|
+
<Dynamic
|
|
2321
|
+
component={fileComponent}
|
|
2322
|
+
mode="text"
|
|
2323
|
+
file={{
|
|
2324
|
+
name: props.input.filePath,
|
|
2325
|
+
contents: props.input.content,
|
|
2326
|
+
cacheKey: checksum(props.input.content),
|
|
2327
|
+
}}
|
|
2328
|
+
overflow="scroll"
|
|
2329
|
+
onRendered={props.onContentRendered}
|
|
2330
|
+
/>
|
|
2331
|
+
</div>
|
|
2332
|
+
</ToolFileAccordion>
|
|
2333
|
+
</Show>
|
|
2334
|
+
<DiagnosticsDisplay diagnostics={diagnostics()} />
|
|
2335
|
+
</BasicTool>
|
|
2336
|
+
</div>
|
|
2337
|
+
)
|
|
2338
|
+
},
|
|
2339
|
+
})
|
|
2340
|
+
|
|
2341
|
+
ToolRegistry.register({
|
|
2342
|
+
name: "patch",
|
|
2343
|
+
render(props) {
|
|
2344
|
+
const i18n = useI18n()
|
|
2345
|
+
const fileComponent = useFileComponent()
|
|
2346
|
+
const files = createMemo(() => patchFiles(props.metadata.files))
|
|
2347
|
+
const pending = createMemo(() => props.status === "pending" || props.status === "running")
|
|
2348
|
+
const single = createMemo(() => {
|
|
2349
|
+
const list = files()
|
|
2350
|
+
if (list.length !== 1) return
|
|
2351
|
+
return list[0]
|
|
2352
|
+
})
|
|
2353
|
+
const [expanded, setExpanded] = createSignal<string[]>([])
|
|
2354
|
+
let seeded = false
|
|
2355
|
+
|
|
2356
|
+
createEffect(() => {
|
|
2357
|
+
const list = files()
|
|
2358
|
+
if (list.length === 0) return
|
|
2359
|
+
if (seeded) return
|
|
2360
|
+
seeded = true
|
|
2361
|
+
setExpanded(list.filter((f) => f.type !== "delete").map((f) => f.filePath))
|
|
2362
|
+
})
|
|
2363
|
+
|
|
2364
|
+
const subtitle = createMemo(() => {
|
|
2365
|
+
const count = files().length
|
|
2366
|
+
if (count === 0) return ""
|
|
2367
|
+
return `${count} ${i18n.t(count > 1 ? "ui.common.file.other" : "ui.common.file.one")}`
|
|
2368
|
+
})
|
|
2369
|
+
|
|
2370
|
+
return (
|
|
2371
|
+
<Show
|
|
2372
|
+
when={single()}
|
|
2373
|
+
fallback={
|
|
2374
|
+
<div data-component="apply-patch-tool">
|
|
2375
|
+
<BasicTool
|
|
2376
|
+
{...props}
|
|
2377
|
+
icon="code-lines"
|
|
2378
|
+
defer={props.deferContent !== false}
|
|
2379
|
+
trigger={{
|
|
2380
|
+
title: i18n.t("ui.tool.patch"),
|
|
2381
|
+
subtitle: subtitle(),
|
|
2382
|
+
}}
|
|
2383
|
+
>
|
|
2384
|
+
<Show when={files().length > 0}>
|
|
2385
|
+
<Accordion
|
|
2386
|
+
multiple
|
|
2387
|
+
data-scope="apply-patch"
|
|
2388
|
+
style={{ "--sticky-accordion-offset": "calc(32px + var(--tool-content-gap))" }}
|
|
2389
|
+
value={expanded()}
|
|
2390
|
+
onChange={(value) => setExpanded(Array.isArray(value) ? value : value ? [value] : [])}
|
|
2391
|
+
>
|
|
2392
|
+
<For each={files()}>
|
|
2393
|
+
{(file) => {
|
|
2394
|
+
const active = createMemo(() => expanded().includes(file.filePath))
|
|
2395
|
+
const [visible, setVisible] = createSignal(false)
|
|
2396
|
+
|
|
2397
|
+
createEffect(() => {
|
|
2398
|
+
if (!active()) {
|
|
2399
|
+
setVisible(false)
|
|
2400
|
+
return
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
requestAnimationFrame(() => {
|
|
2404
|
+
if (!active()) return
|
|
2405
|
+
setVisible(true)
|
|
2406
|
+
})
|
|
2407
|
+
})
|
|
2408
|
+
|
|
2409
|
+
return (
|
|
2410
|
+
<Accordion.Item value={file.filePath} data-type={file.type}>
|
|
2411
|
+
<StickyAccordionHeader>
|
|
2412
|
+
<Accordion.Trigger>
|
|
2413
|
+
<div data-slot="apply-patch-trigger-content">
|
|
2414
|
+
<div data-slot="apply-patch-file-info">
|
|
2415
|
+
<FileIcon node={{ path: file.relativePath, type: "file" }} />
|
|
2416
|
+
<div data-slot="apply-patch-file-name-container">
|
|
2417
|
+
<Show when={file.relativePath.includes("/")}>
|
|
2418
|
+
<span data-slot="apply-patch-directory">{`\u202A${getDirectory(file.relativePath)}\u202C`}</span>
|
|
2419
|
+
</Show>
|
|
2420
|
+
<span data-slot="apply-patch-filename">{getFilename(file.relativePath)}</span>
|
|
2421
|
+
</div>
|
|
2422
|
+
</div>
|
|
2423
|
+
<div data-slot="apply-patch-trigger-actions">
|
|
2424
|
+
<Switch>
|
|
2425
|
+
<Match when={file.type === "add"}>
|
|
2426
|
+
<span data-slot="apply-patch-change" data-type="added">
|
|
2427
|
+
{i18n.t("ui.patch.action.created")}
|
|
2428
|
+
</span>
|
|
2429
|
+
</Match>
|
|
2430
|
+
<Match when={file.type === "delete"}>
|
|
2431
|
+
<span data-slot="apply-patch-change" data-type="removed">
|
|
2432
|
+
{i18n.t("ui.patch.action.deleted")}
|
|
2433
|
+
</span>
|
|
2434
|
+
</Match>
|
|
2435
|
+
<Match when={file.type === "move"}>
|
|
2436
|
+
<span data-slot="apply-patch-change" data-type="modified">
|
|
2437
|
+
{i18n.t("ui.patch.action.moved")}
|
|
2438
|
+
</span>
|
|
2439
|
+
</Match>
|
|
2440
|
+
<Match when={true}>
|
|
2441
|
+
<DiffChanges changes={{ additions: file.additions, deletions: file.deletions }} />
|
|
2442
|
+
</Match>
|
|
2443
|
+
</Switch>
|
|
2444
|
+
<Icon name="chevron-grabber-vertical" size="small" />
|
|
2445
|
+
</div>
|
|
2446
|
+
</div>
|
|
2447
|
+
</Accordion.Trigger>
|
|
2448
|
+
</StickyAccordionHeader>
|
|
2449
|
+
<Accordion.Content>
|
|
2450
|
+
<Show when={props.deferContent === false || visible()}>
|
|
2451
|
+
<div data-component="apply-patch-file-diff">
|
|
2452
|
+
<Dynamic
|
|
2453
|
+
component={fileComponent}
|
|
2454
|
+
mode="diff"
|
|
2455
|
+
virtualize={props.virtualizeDiff}
|
|
2456
|
+
fileDiff={file.view.fileDiff}
|
|
2457
|
+
hunkSeparators={file.view.fileDiff.isPartial ? "simple" : "line-info-basic"}
|
|
2458
|
+
onRendered={props.onContentRendered}
|
|
2459
|
+
/>
|
|
2460
|
+
</div>
|
|
2461
|
+
</Show>
|
|
2462
|
+
</Accordion.Content>
|
|
2463
|
+
</Accordion.Item>
|
|
2464
|
+
)
|
|
2465
|
+
}}
|
|
2466
|
+
</For>
|
|
2467
|
+
</Accordion>
|
|
2468
|
+
</Show>
|
|
2469
|
+
</BasicTool>
|
|
2470
|
+
</div>
|
|
2471
|
+
}
|
|
2472
|
+
>
|
|
2473
|
+
<div data-component="apply-patch-tool">
|
|
2474
|
+
<BasicTool
|
|
2475
|
+
{...props}
|
|
2476
|
+
icon="code-lines"
|
|
2477
|
+
defer={props.deferContent !== false}
|
|
2478
|
+
trigger={
|
|
2479
|
+
<div data-component="edit-trigger">
|
|
2480
|
+
<div data-slot="message-part-title-area">
|
|
2481
|
+
<div data-slot="message-part-title">
|
|
2482
|
+
<span data-slot="message-part-title-text">
|
|
2483
|
+
<TextShimmer text={i18n.t("ui.tool.patch")} active={pending()} />
|
|
2484
|
+
</span>
|
|
2485
|
+
<Show when={!pending()}>
|
|
2486
|
+
<span data-slot="message-part-title-filename">{getFilename(single()!.relativePath)}</span>
|
|
2487
|
+
</Show>
|
|
2488
|
+
</div>
|
|
2489
|
+
<Show when={!pending() && single()!.relativePath.includes("/")}>
|
|
2490
|
+
<div data-slot="message-part-path">
|
|
2491
|
+
<span data-slot="message-part-directory">{getDirectory(single()!.relativePath)}</span>
|
|
2492
|
+
</div>
|
|
2493
|
+
</Show>
|
|
2494
|
+
</div>
|
|
2495
|
+
<div data-slot="message-part-actions">
|
|
2496
|
+
<Show when={!pending()}>
|
|
2497
|
+
<DiffChanges changes={{ additions: single()!.additions, deletions: single()!.deletions }} />
|
|
2498
|
+
</Show>
|
|
2499
|
+
</div>
|
|
2500
|
+
</div>
|
|
2501
|
+
}
|
|
2502
|
+
>
|
|
2503
|
+
<ToolFileAccordion
|
|
2504
|
+
path={single()!.relativePath}
|
|
2505
|
+
actions={
|
|
2506
|
+
<Switch>
|
|
2507
|
+
<Match when={single()!.type === "add"}>
|
|
2508
|
+
<span data-slot="apply-patch-change" data-type="added">
|
|
2509
|
+
{i18n.t("ui.patch.action.created")}
|
|
2510
|
+
</span>
|
|
2511
|
+
</Match>
|
|
2512
|
+
<Match when={single()!.type === "delete"}>
|
|
2513
|
+
<span data-slot="apply-patch-change" data-type="removed">
|
|
2514
|
+
{i18n.t("ui.patch.action.deleted")}
|
|
2515
|
+
</span>
|
|
2516
|
+
</Match>
|
|
2517
|
+
<Match when={single()!.type === "move"}>
|
|
2518
|
+
<span data-slot="apply-patch-change" data-type="modified">
|
|
2519
|
+
{i18n.t("ui.patch.action.moved")}
|
|
2520
|
+
</span>
|
|
2521
|
+
</Match>
|
|
2522
|
+
<Match when={true}>
|
|
2523
|
+
<DiffChanges changes={{ additions: single()!.additions, deletions: single()!.deletions }} />
|
|
2524
|
+
</Match>
|
|
2525
|
+
</Switch>
|
|
2526
|
+
}
|
|
2527
|
+
>
|
|
2528
|
+
<div data-component="apply-patch-file-diff">
|
|
2529
|
+
<Dynamic
|
|
2530
|
+
component={fileComponent}
|
|
2531
|
+
mode="diff"
|
|
2532
|
+
virtualize={props.virtualizeDiff}
|
|
2533
|
+
fileDiff={single()!.view.fileDiff}
|
|
2534
|
+
onRendered={props.onContentRendered}
|
|
2535
|
+
/>
|
|
2536
|
+
</div>
|
|
2537
|
+
</ToolFileAccordion>
|
|
2538
|
+
</BasicTool>
|
|
2539
|
+
</div>
|
|
2540
|
+
</Show>
|
|
2541
|
+
)
|
|
2542
|
+
},
|
|
2543
|
+
})
|
|
2544
|
+
|
|
2545
|
+
ToolRegistry.register({
|
|
2546
|
+
name: "todowrite",
|
|
2547
|
+
render(props) {
|
|
2548
|
+
const i18n = useI18n()
|
|
2549
|
+
const todos = createMemo(() => {
|
|
2550
|
+
const meta = props.metadata?.todos
|
|
2551
|
+
if (Array.isArray(meta)) return meta
|
|
2552
|
+
|
|
2553
|
+
const input = props.input.todos
|
|
2554
|
+
if (Array.isArray(input)) return input
|
|
2555
|
+
|
|
2556
|
+
return []
|
|
2557
|
+
})
|
|
2558
|
+
|
|
2559
|
+
const subtitle = createMemo(() => {
|
|
2560
|
+
const list = todos()
|
|
2561
|
+
if (list.length === 0) return ""
|
|
2562
|
+
return `${list.filter((t: Todo) => t.status === "completed").length}/${list.length}`
|
|
2563
|
+
})
|
|
2564
|
+
|
|
2565
|
+
return (
|
|
2566
|
+
<BasicTool
|
|
2567
|
+
{...props}
|
|
2568
|
+
defaultOpen
|
|
2569
|
+
icon="checklist"
|
|
2570
|
+
trigger={{
|
|
2571
|
+
title: i18n.t("ui.tool.todos"),
|
|
2572
|
+
subtitle: subtitle(),
|
|
2573
|
+
}}
|
|
2574
|
+
>
|
|
2575
|
+
<Show when={todos().length}>
|
|
2576
|
+
<div data-component="todos">
|
|
2577
|
+
<For each={todos()}>
|
|
2578
|
+
{(todo: Todo) => (
|
|
2579
|
+
<Checkbox readOnly checked={todo.status === "completed"}>
|
|
2580
|
+
<span
|
|
2581
|
+
data-slot="message-part-todo-content"
|
|
2582
|
+
data-completed={todo.status === "completed" ? "completed" : undefined}
|
|
2583
|
+
>
|
|
2584
|
+
{todo.content}
|
|
2585
|
+
</span>
|
|
2586
|
+
</Checkbox>
|
|
2587
|
+
)}
|
|
2588
|
+
</For>
|
|
2589
|
+
</div>
|
|
2590
|
+
</Show>
|
|
2591
|
+
</BasicTool>
|
|
2592
|
+
)
|
|
2593
|
+
},
|
|
2594
|
+
})
|
|
2595
|
+
|
|
2596
|
+
ToolRegistry.register({
|
|
2597
|
+
name: "question",
|
|
2598
|
+
render(props) {
|
|
2599
|
+
const i18n = useI18n()
|
|
2600
|
+
const questions = createMemo(() => (props.input.questions ?? []) as QuestionInfo[])
|
|
2601
|
+
const answers = createMemo(() => (props.metadata.answers ?? []) as QuestionAnswer[])
|
|
2602
|
+
const completed = createMemo(() => answers().length > 0)
|
|
2603
|
+
|
|
2604
|
+
const subtitle = createMemo(() => {
|
|
2605
|
+
const count = questions().length
|
|
2606
|
+
if (count === 0) return ""
|
|
2607
|
+
if (completed()) return i18n.t("ui.question.subtitle.answered", { count })
|
|
2608
|
+
return `${count} ${i18n.t(count > 1 ? "ui.common.question.other" : "ui.common.question.one")}`
|
|
2609
|
+
})
|
|
2610
|
+
|
|
2611
|
+
return (
|
|
2612
|
+
<BasicTool
|
|
2613
|
+
{...props}
|
|
2614
|
+
defaultOpen={completed()}
|
|
2615
|
+
icon="bubble-5"
|
|
2616
|
+
trigger={{
|
|
2617
|
+
title: i18n.t("ui.tool.questions"),
|
|
2618
|
+
subtitle: subtitle(),
|
|
2619
|
+
}}
|
|
2620
|
+
>
|
|
2621
|
+
<Show when={completed()}>
|
|
2622
|
+
<div data-component="question-answers">
|
|
2623
|
+
<For each={questions()}>
|
|
2624
|
+
{(q, i) => {
|
|
2625
|
+
const answer = () => answers()[i()] ?? []
|
|
2626
|
+
return (
|
|
2627
|
+
<div data-slot="question-answer-item">
|
|
2628
|
+
<div data-slot="question-text">{q.question}</div>
|
|
2629
|
+
<div data-slot="answer-text">{answer().join(", ") || i18n.t("ui.question.answer.none")}</div>
|
|
2630
|
+
</div>
|
|
2631
|
+
)
|
|
2632
|
+
}}
|
|
2633
|
+
</For>
|
|
2634
|
+
</div>
|
|
2635
|
+
</Show>
|
|
2636
|
+
</BasicTool>
|
|
2637
|
+
)
|
|
2638
|
+
},
|
|
2639
|
+
})
|
|
2640
|
+
|
|
2641
|
+
ToolRegistry.register({
|
|
2642
|
+
name: "skill",
|
|
2643
|
+
render(props) {
|
|
2644
|
+
const i18n = useI18n()
|
|
2645
|
+
const title = createMemo(() => props.input.name || i18n.t("ui.tool.skill"))
|
|
2646
|
+
const running = createMemo(() => props.status === "pending" || props.status === "running")
|
|
2647
|
+
|
|
2648
|
+
const titleContent = () => <TextShimmer text={title()} active={running()} />
|
|
2649
|
+
|
|
2650
|
+
const trigger = () => (
|
|
2651
|
+
<div data-slot="basic-tool-tool-info-structured">
|
|
2652
|
+
<div data-slot="basic-tool-tool-info-main">
|
|
2653
|
+
<span data-slot="basic-tool-tool-title" class="capitalize agent-title">
|
|
2654
|
+
{titleContent()}
|
|
2655
|
+
</span>
|
|
2656
|
+
</div>
|
|
2657
|
+
</div>
|
|
2658
|
+
)
|
|
2659
|
+
|
|
2660
|
+
return <BasicTool icon="brain" status={props.status} trigger={trigger()} hideDetails />
|
|
2661
|
+
},
|
|
2662
|
+
})
|