@neocode-ai/web 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -0
- package/astro.config.mjs +145 -0
- package/config.mjs +14 -0
- package/package.json +41 -0
- package/public/robots.txt +6 -0
- package/public/theme.json +183 -0
- package/src/assets/lander/check.svg +2 -0
- package/src/assets/lander/copy.svg +2 -0
- package/src/assets/lander/screenshot-github.png +0 -0
- package/src/assets/lander/screenshot-splash.png +0 -0
- package/src/assets/lander/screenshot-vscode.png +0 -0
- package/src/assets/lander/screenshot.png +0 -0
- package/src/assets/logo-dark.svg +20 -0
- package/src/assets/logo-light.svg +20 -0
- package/src/assets/logo-ornate-dark.svg +18 -0
- package/src/assets/logo-ornate-light.svg +18 -0
- package/src/assets/web/web-homepage-active-session.png +0 -0
- package/src/assets/web/web-homepage-new-session.png +0 -0
- package/src/assets/web/web-homepage-see-servers.png +0 -0
- package/src/components/Head.astro +50 -0
- package/src/components/Header.astro +128 -0
- package/src/components/Hero.astro +11 -0
- package/src/components/Lander.astro +713 -0
- package/src/components/Share.tsx +634 -0
- package/src/components/SiteTitle.astro +59 -0
- package/src/components/icons/custom.tsx +87 -0
- package/src/components/icons/index.tsx +4454 -0
- package/src/components/share/common.tsx +77 -0
- package/src/components/share/content-bash.module.css +85 -0
- package/src/components/share/content-bash.tsx +67 -0
- package/src/components/share/content-code.module.css +26 -0
- package/src/components/share/content-code.tsx +32 -0
- package/src/components/share/content-diff.module.css +153 -0
- package/src/components/share/content-diff.tsx +231 -0
- package/src/components/share/content-error.module.css +64 -0
- package/src/components/share/content-error.tsx +24 -0
- package/src/components/share/content-markdown.module.css +154 -0
- package/src/components/share/content-markdown.tsx +75 -0
- package/src/components/share/content-text.module.css +63 -0
- package/src/components/share/content-text.tsx +37 -0
- package/src/components/share/copy-button.module.css +30 -0
- package/src/components/share/copy-button.tsx +28 -0
- package/src/components/share/part.module.css +428 -0
- package/src/components/share/part.tsx +780 -0
- package/src/components/share.module.css +832 -0
- package/src/content/docs/1-0.mdx +67 -0
- package/src/content/docs/acp.mdx +156 -0
- package/src/content/docs/agents.mdx +720 -0
- package/src/content/docs/cli.mdx +597 -0
- package/src/content/docs/commands.mdx +323 -0
- package/src/content/docs/config.mdx +683 -0
- package/src/content/docs/custom-tools.mdx +170 -0
- package/src/content/docs/ecosystem.mdx +76 -0
- package/src/content/docs/enterprise.mdx +170 -0
- package/src/content/docs/formatters.mdx +130 -0
- package/src/content/docs/github.mdx +321 -0
- package/src/content/docs/gitlab.mdx +195 -0
- package/src/content/docs/ide.mdx +48 -0
- package/src/content/docs/index.mdx +359 -0
- package/src/content/docs/keybinds.mdx +191 -0
- package/src/content/docs/lsp.mdx +188 -0
- package/src/content/docs/mcp-servers.mdx +511 -0
- package/src/content/docs/models.mdx +223 -0
- package/src/content/docs/modes.mdx +331 -0
- package/src/content/docs/network.mdx +57 -0
- package/src/content/docs/permissions.mdx +237 -0
- package/src/content/docs/plugins.mdx +362 -0
- package/src/content/docs/providers.mdx +1889 -0
- package/src/content/docs/rules.mdx +180 -0
- package/src/content/docs/sdk.mdx +391 -0
- package/src/content/docs/server.mdx +286 -0
- package/src/content/docs/share.mdx +128 -0
- package/src/content/docs/skills.mdx +220 -0
- package/src/content/docs/themes.mdx +369 -0
- package/src/content/docs/tools.mdx +345 -0
- package/src/content/docs/troubleshooting.mdx +300 -0
- package/src/content/docs/tui.mdx +390 -0
- package/src/content/docs/web.mdx +136 -0
- package/src/content/docs/windows-wsl.mdx +113 -0
- package/src/content/docs/zen.mdx +251 -0
- package/src/content.config.ts +7 -0
- package/src/pages/[...slug].md.ts +18 -0
- package/src/pages/s/[id].astro +113 -0
- package/src/styles/custom.css +405 -0
- package/src/types/lang-map.d.ts +27 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,780 @@
|
|
|
1
|
+
import map from "lang-map"
|
|
2
|
+
import { DateTime } from "luxon"
|
|
3
|
+
import { For, Show, Match, Switch, type JSX, createMemo, createSignal, type ParentProps } from "solid-js"
|
|
4
|
+
import {
|
|
5
|
+
IconHashtag,
|
|
6
|
+
IconSparkles,
|
|
7
|
+
IconGlobeAlt,
|
|
8
|
+
IconDocument,
|
|
9
|
+
IconPaperClip,
|
|
10
|
+
IconQueueList,
|
|
11
|
+
IconUserCircle,
|
|
12
|
+
IconCommandLine,
|
|
13
|
+
IconCheckCircle,
|
|
14
|
+
IconChevronDown,
|
|
15
|
+
IconChevronRight,
|
|
16
|
+
IconDocumentPlus,
|
|
17
|
+
IconPencilSquare,
|
|
18
|
+
IconRectangleStack,
|
|
19
|
+
IconMagnifyingGlass,
|
|
20
|
+
IconDocumentMagnifyingGlass,
|
|
21
|
+
} from "../icons"
|
|
22
|
+
import { IconMeta, IconRobot, IconOpenAI, IconGemini, IconAnthropic, IconBrain } from "../icons/custom"
|
|
23
|
+
import { ContentCode } from "./content-code"
|
|
24
|
+
import { ContentDiff } from "./content-diff"
|
|
25
|
+
import { ContentText } from "./content-text"
|
|
26
|
+
import { ContentBash } from "./content-bash"
|
|
27
|
+
import { ContentError } from "./content-error"
|
|
28
|
+
import { formatDuration } from "../share/common"
|
|
29
|
+
import { ContentMarkdown } from "./content-markdown"
|
|
30
|
+
import type { MessageV2 } from "neocode/session/message-v2"
|
|
31
|
+
import type { Diagnostic } from "vscode-languageserver-types"
|
|
32
|
+
|
|
33
|
+
import styles from "./part.module.css"
|
|
34
|
+
|
|
35
|
+
const MIN_DURATION = 2000
|
|
36
|
+
|
|
37
|
+
export interface PartProps {
|
|
38
|
+
index: number
|
|
39
|
+
message: MessageV2.Info
|
|
40
|
+
part: MessageV2.Part
|
|
41
|
+
last: boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function Part(props: PartProps) {
|
|
45
|
+
const [copied, setCopied] = createSignal(false)
|
|
46
|
+
const id = createMemo(() => props.message.id + "-" + props.index)
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
class={styles.root}
|
|
51
|
+
id={id()}
|
|
52
|
+
data-component="part"
|
|
53
|
+
data-type={props.part.type}
|
|
54
|
+
data-role={props.message.role}
|
|
55
|
+
data-copied={copied() ? true : undefined}
|
|
56
|
+
>
|
|
57
|
+
<div data-component="decoration">
|
|
58
|
+
<div data-slot="anchor" title="Link to this message">
|
|
59
|
+
<a
|
|
60
|
+
href={`#${id()}`}
|
|
61
|
+
onClick={(e) => {
|
|
62
|
+
e.preventDefault()
|
|
63
|
+
const anchor = e.currentTarget
|
|
64
|
+
const hash = anchor.getAttribute("href") || ""
|
|
65
|
+
const { origin, pathname, search } = window.location
|
|
66
|
+
navigator.clipboard
|
|
67
|
+
.writeText(`${origin}${pathname}${search}${hash}`)
|
|
68
|
+
.catch((err) => console.error("Copy failed", err))
|
|
69
|
+
|
|
70
|
+
setCopied(true)
|
|
71
|
+
setTimeout(() => setCopied(false), 3000)
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
<Switch>
|
|
75
|
+
<Match when={props.message.role === "user" && props.part.type === "text"}>
|
|
76
|
+
<IconUserCircle width={18} height={18} />
|
|
77
|
+
</Match>
|
|
78
|
+
<Match when={props.message.role === "user" && props.part.type === "file"}>
|
|
79
|
+
<IconPaperClip width={18} height={18} />
|
|
80
|
+
</Match>
|
|
81
|
+
<Match
|
|
82
|
+
when={props.part.type === "step-start" && props.message.role === "assistant" && props.message.modelID}
|
|
83
|
+
>
|
|
84
|
+
{(model) => <ProviderIcon model={model()} size={18} />}
|
|
85
|
+
</Match>
|
|
86
|
+
<Match when={props.part.type === "reasoning" && props.message.role === "assistant"}>
|
|
87
|
+
<IconBrain width={18} height={18} />
|
|
88
|
+
</Match>
|
|
89
|
+
<Match when={props.part.type === "tool" && props.part.tool === "todowrite"}>
|
|
90
|
+
<IconQueueList width={18} height={18} />
|
|
91
|
+
</Match>
|
|
92
|
+
<Match when={props.part.type === "tool" && props.part.tool === "todoread"}>
|
|
93
|
+
<IconQueueList width={18} height={18} />
|
|
94
|
+
</Match>
|
|
95
|
+
<Match when={props.part.type === "tool" && props.part.tool === "bash"}>
|
|
96
|
+
<IconCommandLine width={18} height={18} />
|
|
97
|
+
</Match>
|
|
98
|
+
<Match when={props.part.type === "tool" && props.part.tool === "edit"}>
|
|
99
|
+
<IconPencilSquare width={18} height={18} />
|
|
100
|
+
</Match>
|
|
101
|
+
<Match when={props.part.type === "tool" && props.part.tool === "write"}>
|
|
102
|
+
<IconDocumentPlus width={18} height={18} />
|
|
103
|
+
</Match>
|
|
104
|
+
<Match when={props.part.type === "tool" && props.part.tool === "read"}>
|
|
105
|
+
<IconDocument width={18} height={18} />
|
|
106
|
+
</Match>
|
|
107
|
+
<Match when={props.part.type === "tool" && props.part.tool === "grep"}>
|
|
108
|
+
<IconDocumentMagnifyingGlass width={18} height={18} />
|
|
109
|
+
</Match>
|
|
110
|
+
<Match when={props.part.type === "tool" && props.part.tool === "list"}>
|
|
111
|
+
<IconRectangleStack width={18} height={18} />
|
|
112
|
+
</Match>
|
|
113
|
+
<Match when={props.part.type === "tool" && props.part.tool === "glob"}>
|
|
114
|
+
<IconMagnifyingGlass width={18} height={18} />
|
|
115
|
+
</Match>
|
|
116
|
+
<Match when={props.part.type === "tool" && props.part.tool === "webfetch"}>
|
|
117
|
+
<IconGlobeAlt width={18} height={18} />
|
|
118
|
+
</Match>
|
|
119
|
+
<Match when={props.part.type === "tool" && props.part.tool === "task"}>
|
|
120
|
+
<IconRobot width={18} height={18} />
|
|
121
|
+
</Match>
|
|
122
|
+
<Match when={true}>
|
|
123
|
+
<IconSparkles width={18} height={18} />
|
|
124
|
+
</Match>
|
|
125
|
+
</Switch>
|
|
126
|
+
<IconHashtag width={18} height={18} />
|
|
127
|
+
<IconCheckCircle width={18} height={18} />
|
|
128
|
+
</a>
|
|
129
|
+
<span data-slot="tooltip">Copied!</span>
|
|
130
|
+
</div>
|
|
131
|
+
<div data-slot="bar"></div>
|
|
132
|
+
</div>
|
|
133
|
+
<div data-component="content">
|
|
134
|
+
{props.message.role === "user" && props.part.type === "text" && (
|
|
135
|
+
<div data-component="user-text">
|
|
136
|
+
<ContentText text={props.part.text} expand={props.last} />
|
|
137
|
+
</div>
|
|
138
|
+
)}
|
|
139
|
+
{props.message.role === "assistant" && props.part.type === "text" && (
|
|
140
|
+
<div data-component="assistant-text">
|
|
141
|
+
<div data-component="assistant-text-markdown">
|
|
142
|
+
<ContentMarkdown expand={props.last} text={props.part.text} />
|
|
143
|
+
</div>
|
|
144
|
+
{props.last && props.message.role === "assistant" && props.message.time.completed && (
|
|
145
|
+
<Footer
|
|
146
|
+
title={DateTime.fromMillis(props.message.time.completed).toLocaleString(
|
|
147
|
+
DateTime.DATETIME_FULL_WITH_SECONDS,
|
|
148
|
+
)}
|
|
149
|
+
>
|
|
150
|
+
{DateTime.fromMillis(props.message.time.completed).toLocaleString(DateTime.DATETIME_MED)}
|
|
151
|
+
</Footer>
|
|
152
|
+
)}
|
|
153
|
+
</div>
|
|
154
|
+
)}
|
|
155
|
+
{props.message.role === "assistant" && props.part.type === "reasoning" && (
|
|
156
|
+
<div data-component="tool">
|
|
157
|
+
<div data-component="tool-title">
|
|
158
|
+
<span data-slot="name">Thinking</span>
|
|
159
|
+
</div>
|
|
160
|
+
<Show when={props.part.text}>
|
|
161
|
+
<div data-component="assistant-reasoning">
|
|
162
|
+
<ResultsButton showCopy="Show details" hideCopy="Hide details">
|
|
163
|
+
<div data-component="assistant-reasoning-markdown">
|
|
164
|
+
<ContentMarkdown expand text={props.part.text || "Thinking..."} />
|
|
165
|
+
</div>
|
|
166
|
+
</ResultsButton>
|
|
167
|
+
</div>
|
|
168
|
+
</Show>
|
|
169
|
+
</div>
|
|
170
|
+
)}
|
|
171
|
+
{props.message.role === "user" && props.part.type === "file" && (
|
|
172
|
+
<div data-component="attachment">
|
|
173
|
+
<div data-slot="copy">Attachment</div>
|
|
174
|
+
<div data-slot="filename">{props.part.filename}</div>
|
|
175
|
+
</div>
|
|
176
|
+
)}
|
|
177
|
+
{props.message.role === "user" && props.part.type === "file" && (
|
|
178
|
+
<div data-component="attachment">
|
|
179
|
+
<div data-slot="copy">Attachment</div>
|
|
180
|
+
<div data-slot="filename">{props.part.filename}</div>
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
{props.part.type === "step-start" && props.message.role === "assistant" && (
|
|
184
|
+
<div data-component="step-start">
|
|
185
|
+
<div data-slot="provider">{props.message.providerID}</div>
|
|
186
|
+
<div data-slot="model">{props.message.modelID}</div>
|
|
187
|
+
</div>
|
|
188
|
+
)}
|
|
189
|
+
{props.part.type === "tool" && props.part.state.status === "error" && (
|
|
190
|
+
<div data-component="tool" data-tool="error">
|
|
191
|
+
<ContentError>{formatErrorString(props.part.state.error)}</ContentError>
|
|
192
|
+
<Spacer />
|
|
193
|
+
</div>
|
|
194
|
+
)}
|
|
195
|
+
{props.part.type === "tool" &&
|
|
196
|
+
props.part.state.status === "completed" &&
|
|
197
|
+
props.message.role === "assistant" && (
|
|
198
|
+
<>
|
|
199
|
+
<div data-component="tool" data-tool={props.part.tool}>
|
|
200
|
+
<Switch>
|
|
201
|
+
<Match when={props.part.tool === "grep"}>
|
|
202
|
+
<GrepTool
|
|
203
|
+
message={props.message}
|
|
204
|
+
id={props.part.id}
|
|
205
|
+
tool={props.part.tool}
|
|
206
|
+
state={props.part.state}
|
|
207
|
+
/>
|
|
208
|
+
</Match>
|
|
209
|
+
<Match when={props.part.tool === "glob"}>
|
|
210
|
+
<GlobTool
|
|
211
|
+
message={props.message}
|
|
212
|
+
id={props.part.id}
|
|
213
|
+
tool={props.part.tool}
|
|
214
|
+
state={props.part.state}
|
|
215
|
+
/>
|
|
216
|
+
</Match>
|
|
217
|
+
<Match when={props.part.tool === "list"}>
|
|
218
|
+
<ListTool
|
|
219
|
+
message={props.message}
|
|
220
|
+
id={props.part.id}
|
|
221
|
+
tool={props.part.tool}
|
|
222
|
+
state={props.part.state}
|
|
223
|
+
/>
|
|
224
|
+
</Match>
|
|
225
|
+
<Match when={props.part.tool === "read"}>
|
|
226
|
+
<ReadTool
|
|
227
|
+
message={props.message}
|
|
228
|
+
id={props.part.id}
|
|
229
|
+
tool={props.part.tool}
|
|
230
|
+
state={props.part.state}
|
|
231
|
+
/>
|
|
232
|
+
</Match>
|
|
233
|
+
<Match when={props.part.tool === "write"}>
|
|
234
|
+
<WriteTool
|
|
235
|
+
message={props.message}
|
|
236
|
+
id={props.part.id}
|
|
237
|
+
tool={props.part.tool}
|
|
238
|
+
state={props.part.state}
|
|
239
|
+
/>
|
|
240
|
+
</Match>
|
|
241
|
+
<Match when={props.part.tool === "edit"}>
|
|
242
|
+
<EditTool
|
|
243
|
+
message={props.message}
|
|
244
|
+
id={props.part.id}
|
|
245
|
+
tool={props.part.tool}
|
|
246
|
+
state={props.part.state}
|
|
247
|
+
/>
|
|
248
|
+
</Match>
|
|
249
|
+
<Match when={props.part.tool === "bash"}>
|
|
250
|
+
<BashTool
|
|
251
|
+
id={props.part.id}
|
|
252
|
+
tool={props.part.tool}
|
|
253
|
+
state={props.part.state}
|
|
254
|
+
message={props.message}
|
|
255
|
+
/>
|
|
256
|
+
</Match>
|
|
257
|
+
<Match when={props.part.tool === "todowrite"}>
|
|
258
|
+
<TodoWriteTool
|
|
259
|
+
message={props.message}
|
|
260
|
+
id={props.part.id}
|
|
261
|
+
tool={props.part.tool}
|
|
262
|
+
state={props.part.state}
|
|
263
|
+
/>
|
|
264
|
+
</Match>
|
|
265
|
+
<Match when={props.part.tool === "webfetch"}>
|
|
266
|
+
<WebFetchTool
|
|
267
|
+
message={props.message}
|
|
268
|
+
id={props.part.id}
|
|
269
|
+
tool={props.part.tool}
|
|
270
|
+
state={props.part.state}
|
|
271
|
+
/>
|
|
272
|
+
</Match>
|
|
273
|
+
<Match when={props.part.tool === "task"}>
|
|
274
|
+
<TaskTool
|
|
275
|
+
id={props.part.id}
|
|
276
|
+
tool={props.part.tool}
|
|
277
|
+
message={props.message}
|
|
278
|
+
state={props.part.state}
|
|
279
|
+
/>
|
|
280
|
+
</Match>
|
|
281
|
+
<Match when={true}>
|
|
282
|
+
<FallbackTool
|
|
283
|
+
message={props.message}
|
|
284
|
+
id={props.part.id}
|
|
285
|
+
tool={props.part.tool}
|
|
286
|
+
state={props.part.state}
|
|
287
|
+
/>
|
|
288
|
+
</Match>
|
|
289
|
+
</Switch>
|
|
290
|
+
</div>
|
|
291
|
+
<ToolFooter
|
|
292
|
+
time={DateTime.fromMillis(props.part.state.time.end)
|
|
293
|
+
.diff(DateTime.fromMillis(props.part.state.time.start))
|
|
294
|
+
.toMillis()}
|
|
295
|
+
/>
|
|
296
|
+
</>
|
|
297
|
+
)}
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
type ToolProps = {
|
|
304
|
+
id: MessageV2.ToolPart["id"]
|
|
305
|
+
tool: MessageV2.ToolPart["tool"]
|
|
306
|
+
state: MessageV2.ToolStateCompleted
|
|
307
|
+
message: MessageV2.Assistant
|
|
308
|
+
isLastPart?: boolean
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
interface Todo {
|
|
312
|
+
id: string
|
|
313
|
+
content: string
|
|
314
|
+
status: "pending" | "in_progress" | "completed"
|
|
315
|
+
priority: "low" | "medium" | "high"
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function stripWorkingDirectory(filePath?: string, workingDir?: string) {
|
|
319
|
+
if (filePath === undefined || workingDir === undefined) return filePath
|
|
320
|
+
|
|
321
|
+
const prefix = workingDir.endsWith("/") ? workingDir : workingDir + "/"
|
|
322
|
+
|
|
323
|
+
if (filePath === workingDir) {
|
|
324
|
+
return ""
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (filePath.startsWith(prefix)) {
|
|
328
|
+
return filePath.slice(prefix.length)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return filePath
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function getShikiLang(filename: string) {
|
|
335
|
+
const ext = filename.split(".").pop()?.toLowerCase() ?? ""
|
|
336
|
+
const langs = map.languages(ext)
|
|
337
|
+
const type = langs?.[0]?.toLowerCase()
|
|
338
|
+
|
|
339
|
+
const overrides: Record<string, string> = {
|
|
340
|
+
conf: "shellscript",
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return type ? (overrides[type] ?? type) : "plaintext"
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function getDiagnostics(diagnosticsByFile: Record<string, Diagnostic[]>, currentFile: string): JSX.Element[] {
|
|
347
|
+
const result: JSX.Element[] = []
|
|
348
|
+
|
|
349
|
+
if (diagnosticsByFile === undefined || diagnosticsByFile[currentFile] === undefined) return result
|
|
350
|
+
|
|
351
|
+
for (const diags of Object.values(diagnosticsByFile)) {
|
|
352
|
+
for (const d of diags) {
|
|
353
|
+
if (d.severity !== 1) continue
|
|
354
|
+
|
|
355
|
+
const line = d.range.start.line + 1
|
|
356
|
+
const column = d.range.start.character + 1
|
|
357
|
+
|
|
358
|
+
result.push(
|
|
359
|
+
<pre>
|
|
360
|
+
<span data-color="red" data-marker="label">
|
|
361
|
+
Error
|
|
362
|
+
</span>
|
|
363
|
+
<span data-color="dimmed" data-separator>
|
|
364
|
+
[{line}:{column}]
|
|
365
|
+
</span>
|
|
366
|
+
<span>{d.message}</span>
|
|
367
|
+
</pre>,
|
|
368
|
+
)
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return result
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function formatErrorString(error: string): JSX.Element {
|
|
376
|
+
const errorMarker = "Error: "
|
|
377
|
+
const startsWithError = error.startsWith(errorMarker)
|
|
378
|
+
|
|
379
|
+
return startsWithError ? (
|
|
380
|
+
<pre>
|
|
381
|
+
<span data-color="red" data-marker="label" data-separator>
|
|
382
|
+
Error
|
|
383
|
+
</span>
|
|
384
|
+
<span>{error.slice(errorMarker.length)}</span>
|
|
385
|
+
</pre>
|
|
386
|
+
) : (
|
|
387
|
+
<pre>
|
|
388
|
+
<span data-color="dimmed">{error}</span>
|
|
389
|
+
</pre>
|
|
390
|
+
)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export function TodoWriteTool(props: ToolProps) {
|
|
394
|
+
const priority: Record<Todo["status"], number> = {
|
|
395
|
+
in_progress: 0,
|
|
396
|
+
pending: 1,
|
|
397
|
+
completed: 2,
|
|
398
|
+
}
|
|
399
|
+
const todos = createMemo(() =>
|
|
400
|
+
((props.state.input?.todos ?? []) as Todo[]).slice().sort((a, b) => priority[a.status] - priority[b.status]),
|
|
401
|
+
)
|
|
402
|
+
const starting = () => todos().every((t: Todo) => t.status === "pending")
|
|
403
|
+
const finished = () => todos().every((t: Todo) => t.status === "completed")
|
|
404
|
+
|
|
405
|
+
return (
|
|
406
|
+
<>
|
|
407
|
+
<div data-component="tool-title">
|
|
408
|
+
<span data-slot="name">
|
|
409
|
+
<Switch fallback="Updating plan">
|
|
410
|
+
<Match when={starting()}>Creating plan</Match>
|
|
411
|
+
<Match when={finished()}>Completing plan</Match>
|
|
412
|
+
</Switch>
|
|
413
|
+
</span>
|
|
414
|
+
</div>
|
|
415
|
+
<Show when={todos().length > 0}>
|
|
416
|
+
<ul data-component="todos">
|
|
417
|
+
<For each={todos()}>
|
|
418
|
+
{(todo) => (
|
|
419
|
+
<li data-slot="item" data-status={todo.status}>
|
|
420
|
+
<span></span>
|
|
421
|
+
{todo.content}
|
|
422
|
+
</li>
|
|
423
|
+
)}
|
|
424
|
+
</For>
|
|
425
|
+
</ul>
|
|
426
|
+
</Show>
|
|
427
|
+
</>
|
|
428
|
+
)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export function GrepTool(props: ToolProps) {
|
|
432
|
+
return (
|
|
433
|
+
<>
|
|
434
|
+
<div data-component="tool-title">
|
|
435
|
+
<span data-slot="name">Grep</span>
|
|
436
|
+
<span data-slot="target">“{props.state.input.pattern}”</span>
|
|
437
|
+
</div>
|
|
438
|
+
<div data-component="tool-result">
|
|
439
|
+
<Switch>
|
|
440
|
+
<Match when={props.state.metadata?.matches && props.state.metadata?.matches > 0}>
|
|
441
|
+
<ResultsButton
|
|
442
|
+
showCopy={props.state.metadata?.matches === 1 ? "1 match" : `${props.state.metadata?.matches} matches`}
|
|
443
|
+
>
|
|
444
|
+
<ContentText expand compact text={props.state.output} />
|
|
445
|
+
</ResultsButton>
|
|
446
|
+
</Match>
|
|
447
|
+
<Match when={props.state.output}>
|
|
448
|
+
<ContentText expand compact text={props.state.output} data-size="sm" data-color="dimmed" />
|
|
449
|
+
</Match>
|
|
450
|
+
</Switch>
|
|
451
|
+
</div>
|
|
452
|
+
</>
|
|
453
|
+
)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export function ListTool(props: ToolProps) {
|
|
457
|
+
const path = createMemo(() =>
|
|
458
|
+
props.state.input?.path !== props.message.path.cwd
|
|
459
|
+
? stripWorkingDirectory(props.state.input?.path, props.message.path.cwd)
|
|
460
|
+
: props.state.input?.path,
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
return (
|
|
464
|
+
<>
|
|
465
|
+
<div data-component="tool-title">
|
|
466
|
+
<span data-slot="name">LS</span>
|
|
467
|
+
<span data-slot="target" title={props.state.input?.path}>
|
|
468
|
+
{path()}
|
|
469
|
+
</span>
|
|
470
|
+
</div>
|
|
471
|
+
<div data-component="tool-result">
|
|
472
|
+
<Switch>
|
|
473
|
+
<Match when={props.state.output}>
|
|
474
|
+
<ResultsButton>
|
|
475
|
+
<ContentText expand compact text={props.state.output} />
|
|
476
|
+
</ResultsButton>
|
|
477
|
+
</Match>
|
|
478
|
+
</Switch>
|
|
479
|
+
</div>
|
|
480
|
+
</>
|
|
481
|
+
)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export function WebFetchTool(props: ToolProps) {
|
|
485
|
+
return (
|
|
486
|
+
<>
|
|
487
|
+
<div data-component="tool-title">
|
|
488
|
+
<span data-slot="name">Fetch</span>
|
|
489
|
+
<span data-slot="target">{props.state.input.url}</span>
|
|
490
|
+
</div>
|
|
491
|
+
<div data-component="tool-result">
|
|
492
|
+
<Switch>
|
|
493
|
+
<Match when={props.state.metadata?.error}>
|
|
494
|
+
<ContentError>{formatErrorString(props.state.output)}</ContentError>
|
|
495
|
+
</Match>
|
|
496
|
+
<Match when={props.state.output}>
|
|
497
|
+
<ResultsButton>
|
|
498
|
+
<ContentCode lang={props.state.input.format || "text"} code={props.state.output} />
|
|
499
|
+
</ResultsButton>
|
|
500
|
+
</Match>
|
|
501
|
+
</Switch>
|
|
502
|
+
</div>
|
|
503
|
+
</>
|
|
504
|
+
)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export function ReadTool(props: ToolProps) {
|
|
508
|
+
const filePath = createMemo(() => stripWorkingDirectory(props.state.input?.filePath, props.message.path.cwd))
|
|
509
|
+
|
|
510
|
+
return (
|
|
511
|
+
<>
|
|
512
|
+
<div data-component="tool-title">
|
|
513
|
+
<span data-slot="name">Read</span>
|
|
514
|
+
<span data-slot="target" title={props.state.input?.filePath}>
|
|
515
|
+
{filePath()}
|
|
516
|
+
</span>
|
|
517
|
+
</div>
|
|
518
|
+
<div data-component="tool-result">
|
|
519
|
+
<Switch>
|
|
520
|
+
<Match when={props.state.metadata?.error}>
|
|
521
|
+
<ContentError>{formatErrorString(props.state.output)}</ContentError>
|
|
522
|
+
</Match>
|
|
523
|
+
<Match when={typeof props.state.metadata?.preview === "string"}>
|
|
524
|
+
<ResultsButton showCopy="Show preview" hideCopy="Hide preview">
|
|
525
|
+
<ContentCode lang={getShikiLang(filePath() || "")} code={props.state.metadata?.preview} />
|
|
526
|
+
</ResultsButton>
|
|
527
|
+
</Match>
|
|
528
|
+
<Match when={typeof props.state.metadata?.preview !== "string" && props.state.output}>
|
|
529
|
+
<ResultsButton>
|
|
530
|
+
<ContentText expand compact text={props.state.output} />
|
|
531
|
+
</ResultsButton>
|
|
532
|
+
</Match>
|
|
533
|
+
</Switch>
|
|
534
|
+
</div>
|
|
535
|
+
</>
|
|
536
|
+
)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export function WriteTool(props: ToolProps) {
|
|
540
|
+
const filePath = createMemo(() => stripWorkingDirectory(props.state.input?.filePath, props.message.path.cwd))
|
|
541
|
+
const diagnostics = createMemo(() => getDiagnostics(props.state.metadata?.diagnostics, props.state.input.filePath))
|
|
542
|
+
|
|
543
|
+
return (
|
|
544
|
+
<>
|
|
545
|
+
<div data-component="tool-title">
|
|
546
|
+
<span data-slot="name">Write</span>
|
|
547
|
+
<span data-slot="target" title={props.state.input?.filePath}>
|
|
548
|
+
{filePath()}
|
|
549
|
+
</span>
|
|
550
|
+
</div>
|
|
551
|
+
<Show when={diagnostics().length > 0}>
|
|
552
|
+
<ContentError>{diagnostics()}</ContentError>
|
|
553
|
+
</Show>
|
|
554
|
+
<div data-component="tool-result">
|
|
555
|
+
<Switch>
|
|
556
|
+
<Match when={props.state.metadata?.error}>
|
|
557
|
+
<ContentError>{formatErrorString(props.state.output)}</ContentError>
|
|
558
|
+
</Match>
|
|
559
|
+
<Match when={props.state.input?.content}>
|
|
560
|
+
<ResultsButton showCopy="Show contents" hideCopy="Hide contents">
|
|
561
|
+
<ContentCode lang={getShikiLang(filePath() || "")} code={props.state.input?.content} />
|
|
562
|
+
</ResultsButton>
|
|
563
|
+
</Match>
|
|
564
|
+
</Switch>
|
|
565
|
+
</div>
|
|
566
|
+
</>
|
|
567
|
+
)
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
export function EditTool(props: ToolProps) {
|
|
571
|
+
const filePath = createMemo(() => stripWorkingDirectory(props.state.input.filePath, props.message.path.cwd))
|
|
572
|
+
const diagnostics = createMemo(() => getDiagnostics(props.state.metadata?.diagnostics, props.state.input.filePath))
|
|
573
|
+
|
|
574
|
+
return (
|
|
575
|
+
<>
|
|
576
|
+
<div data-component="tool-title">
|
|
577
|
+
<span data-slot="name">Edit</span>
|
|
578
|
+
<span data-slot="target" title={props.state.input?.filePath}>
|
|
579
|
+
{filePath()}
|
|
580
|
+
</span>
|
|
581
|
+
</div>
|
|
582
|
+
<div data-component="tool-result">
|
|
583
|
+
<Switch>
|
|
584
|
+
<Match when={props.state.metadata?.error}>
|
|
585
|
+
<ContentError>{formatErrorString(props.state.metadata?.message || "")}</ContentError>
|
|
586
|
+
</Match>
|
|
587
|
+
<Match when={props.state.metadata?.diff}>
|
|
588
|
+
<div data-component="diff">
|
|
589
|
+
<ContentDiff diff={props.state.metadata?.diff} lang={getShikiLang(filePath() || "")} />
|
|
590
|
+
</div>
|
|
591
|
+
</Match>
|
|
592
|
+
</Switch>
|
|
593
|
+
</div>
|
|
594
|
+
<Show when={diagnostics().length > 0}>
|
|
595
|
+
<ContentError>{diagnostics()}</ContentError>
|
|
596
|
+
</Show>
|
|
597
|
+
</>
|
|
598
|
+
)
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
export function BashTool(props: ToolProps) {
|
|
602
|
+
return (
|
|
603
|
+
<ContentBash
|
|
604
|
+
command={props.state.input.command}
|
|
605
|
+
output={props.state.metadata.output ?? props.state.metadata?.stdout}
|
|
606
|
+
description={props.state.metadata.description}
|
|
607
|
+
/>
|
|
608
|
+
)
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
export function GlobTool(props: ToolProps) {
|
|
612
|
+
return (
|
|
613
|
+
<>
|
|
614
|
+
<div data-component="tool-title">
|
|
615
|
+
<span data-slot="name">Glob</span>
|
|
616
|
+
<span data-slot="target">“{props.state.input.pattern}”</span>
|
|
617
|
+
</div>
|
|
618
|
+
<Switch>
|
|
619
|
+
<Match when={props.state.metadata?.count && props.state.metadata?.count > 0}>
|
|
620
|
+
<div data-component="tool-result">
|
|
621
|
+
<ResultsButton
|
|
622
|
+
showCopy={props.state.metadata?.count === 1 ? "1 result" : `${props.state.metadata?.count} results`}
|
|
623
|
+
>
|
|
624
|
+
<ContentText expand compact text={props.state.output} />
|
|
625
|
+
</ResultsButton>
|
|
626
|
+
</div>
|
|
627
|
+
</Match>
|
|
628
|
+
<Match when={props.state.output}>
|
|
629
|
+
<ContentText expand text={props.state.output} data-size="sm" data-color="dimmed" />
|
|
630
|
+
</Match>
|
|
631
|
+
</Switch>
|
|
632
|
+
</>
|
|
633
|
+
)
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
interface ResultsButtonProps extends ParentProps {
|
|
637
|
+
showCopy?: string
|
|
638
|
+
hideCopy?: string
|
|
639
|
+
}
|
|
640
|
+
function ResultsButton(props: ResultsButtonProps) {
|
|
641
|
+
const [show, setShow] = createSignal(false)
|
|
642
|
+
|
|
643
|
+
return (
|
|
644
|
+
<>
|
|
645
|
+
<button type="button" data-component="button-text" data-more onClick={() => setShow((e) => !e)}>
|
|
646
|
+
<span>{show() ? props.hideCopy || "Hide results" : props.showCopy || "Show results"}</span>
|
|
647
|
+
<span data-slot="icon">
|
|
648
|
+
<Show when={show()} fallback={<IconChevronRight width={11} height={11} />}>
|
|
649
|
+
<IconChevronDown width={11} height={11} />
|
|
650
|
+
</Show>
|
|
651
|
+
</span>
|
|
652
|
+
</button>
|
|
653
|
+
<Show when={show()}>{props.children}</Show>
|
|
654
|
+
</>
|
|
655
|
+
)
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
export function Spacer() {
|
|
659
|
+
return <div data-component="spacer"></div>
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function Footer(props: ParentProps<{ title: string }>) {
|
|
663
|
+
return (
|
|
664
|
+
<div data-component="content-footer" title={props.title}>
|
|
665
|
+
{props.children}
|
|
666
|
+
</div>
|
|
667
|
+
)
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function ToolFooter(props: { time: number }) {
|
|
671
|
+
return props.time > MIN_DURATION && <Footer title={`${props.time}ms`}>{formatDuration(props.time)}</Footer>
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function TaskTool(props: ToolProps) {
|
|
675
|
+
return (
|
|
676
|
+
<>
|
|
677
|
+
<div data-component="tool-title">
|
|
678
|
+
<span data-slot="name">Task</span>
|
|
679
|
+
<span data-slot="target">{props.state.input.description}</span>
|
|
680
|
+
</div>
|
|
681
|
+
<div data-component="tool-input">“{props.state.input.prompt}”</div>
|
|
682
|
+
<ResultsButton showCopy="Show output" hideCopy="Hide output">
|
|
683
|
+
<div data-component="tool-output">
|
|
684
|
+
<ContentMarkdown expand text={props.state.output} />
|
|
685
|
+
</div>
|
|
686
|
+
</ResultsButton>
|
|
687
|
+
</>
|
|
688
|
+
)
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
export function FallbackTool(props: ToolProps) {
|
|
692
|
+
return (
|
|
693
|
+
<>
|
|
694
|
+
<div data-component="tool-title">
|
|
695
|
+
<span data-slot="name">{props.tool}</span>
|
|
696
|
+
</div>
|
|
697
|
+
<div data-component="tool-args">
|
|
698
|
+
<For each={flattenToolArgs(props.state.input)}>
|
|
699
|
+
{(arg) => (
|
|
700
|
+
<>
|
|
701
|
+
<div></div>
|
|
702
|
+
<div>{arg[0]}</div>
|
|
703
|
+
<div>{arg[1]}</div>
|
|
704
|
+
</>
|
|
705
|
+
)}
|
|
706
|
+
</For>
|
|
707
|
+
</div>
|
|
708
|
+
<Switch>
|
|
709
|
+
<Match when={props.state.output}>
|
|
710
|
+
<div data-component="tool-result">
|
|
711
|
+
<ResultsButton>
|
|
712
|
+
<ContentText expand compact text={props.state.output} data-size="sm" data-color="dimmed" />
|
|
713
|
+
</ResultsButton>
|
|
714
|
+
</div>
|
|
715
|
+
</Match>
|
|
716
|
+
</Switch>
|
|
717
|
+
</>
|
|
718
|
+
)
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Converts nested objects/arrays into [path, value] pairs.
|
|
722
|
+
// E.g. {a:{b:{c:1}}, d:[{e:2}, 3]} => [["a.b.c",1], ["d[0].e",2], ["d[1]",3]]
|
|
723
|
+
function flattenToolArgs(obj: any, prefix: string = ""): Array<[string, any]> {
|
|
724
|
+
const entries: Array<[string, any]> = []
|
|
725
|
+
|
|
726
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
727
|
+
const path = prefix ? `${prefix}.${key}` : key
|
|
728
|
+
|
|
729
|
+
if (value !== null && typeof value === "object") {
|
|
730
|
+
if (Array.isArray(value)) {
|
|
731
|
+
value.forEach((item, index) => {
|
|
732
|
+
const arrayPath = `${path}[${index}]`
|
|
733
|
+
if (item !== null && typeof item === "object") {
|
|
734
|
+
entries.push(...flattenToolArgs(item, arrayPath))
|
|
735
|
+
} else {
|
|
736
|
+
entries.push([arrayPath, item])
|
|
737
|
+
}
|
|
738
|
+
})
|
|
739
|
+
} else {
|
|
740
|
+
entries.push(...flattenToolArgs(value, path))
|
|
741
|
+
}
|
|
742
|
+
} else {
|
|
743
|
+
entries.push([path, value])
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
return entries
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function getProvider(model: string) {
|
|
751
|
+
const lowerModel = model.toLowerCase()
|
|
752
|
+
|
|
753
|
+
if (/claude|anthropic/.test(lowerModel)) return "anthropic"
|
|
754
|
+
if (/gpt|o[1-4]|codex|openai/.test(lowerModel)) return "openai"
|
|
755
|
+
if (/gemini|palm|bard|google/.test(lowerModel)) return "gemini"
|
|
756
|
+
if (/llama|meta/.test(lowerModel)) return "meta"
|
|
757
|
+
|
|
758
|
+
return "any"
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
export function ProviderIcon(props: { model: string; size?: number }) {
|
|
762
|
+
const provider = getProvider(props.model)
|
|
763
|
+
const size = props.size || 16
|
|
764
|
+
return (
|
|
765
|
+
<Switch fallback={<IconSparkles width={size} height={size} />}>
|
|
766
|
+
<Match when={provider === "openai"}>
|
|
767
|
+
<IconOpenAI width={size} height={size} />
|
|
768
|
+
</Match>
|
|
769
|
+
<Match when={provider === "anthropic"}>
|
|
770
|
+
<IconAnthropic width={size} height={size} />
|
|
771
|
+
</Match>
|
|
772
|
+
<Match when={provider === "gemini"}>
|
|
773
|
+
<IconGemini width={size} height={size} />
|
|
774
|
+
</Match>
|
|
775
|
+
<Match when={provider === "meta"}>
|
|
776
|
+
<IconMeta width={size} height={size} />
|
|
777
|
+
</Match>
|
|
778
|
+
</Switch>
|
|
779
|
+
)
|
|
780
|
+
}
|