@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.
Files changed (86) hide show
  1. package/README.md +54 -0
  2. package/astro.config.mjs +145 -0
  3. package/config.mjs +14 -0
  4. package/package.json +41 -0
  5. package/public/robots.txt +6 -0
  6. package/public/theme.json +183 -0
  7. package/src/assets/lander/check.svg +2 -0
  8. package/src/assets/lander/copy.svg +2 -0
  9. package/src/assets/lander/screenshot-github.png +0 -0
  10. package/src/assets/lander/screenshot-splash.png +0 -0
  11. package/src/assets/lander/screenshot-vscode.png +0 -0
  12. package/src/assets/lander/screenshot.png +0 -0
  13. package/src/assets/logo-dark.svg +20 -0
  14. package/src/assets/logo-light.svg +20 -0
  15. package/src/assets/logo-ornate-dark.svg +18 -0
  16. package/src/assets/logo-ornate-light.svg +18 -0
  17. package/src/assets/web/web-homepage-active-session.png +0 -0
  18. package/src/assets/web/web-homepage-new-session.png +0 -0
  19. package/src/assets/web/web-homepage-see-servers.png +0 -0
  20. package/src/components/Head.astro +50 -0
  21. package/src/components/Header.astro +128 -0
  22. package/src/components/Hero.astro +11 -0
  23. package/src/components/Lander.astro +713 -0
  24. package/src/components/Share.tsx +634 -0
  25. package/src/components/SiteTitle.astro +59 -0
  26. package/src/components/icons/custom.tsx +87 -0
  27. package/src/components/icons/index.tsx +4454 -0
  28. package/src/components/share/common.tsx +77 -0
  29. package/src/components/share/content-bash.module.css +85 -0
  30. package/src/components/share/content-bash.tsx +67 -0
  31. package/src/components/share/content-code.module.css +26 -0
  32. package/src/components/share/content-code.tsx +32 -0
  33. package/src/components/share/content-diff.module.css +153 -0
  34. package/src/components/share/content-diff.tsx +231 -0
  35. package/src/components/share/content-error.module.css +64 -0
  36. package/src/components/share/content-error.tsx +24 -0
  37. package/src/components/share/content-markdown.module.css +154 -0
  38. package/src/components/share/content-markdown.tsx +75 -0
  39. package/src/components/share/content-text.module.css +63 -0
  40. package/src/components/share/content-text.tsx +37 -0
  41. package/src/components/share/copy-button.module.css +30 -0
  42. package/src/components/share/copy-button.tsx +28 -0
  43. package/src/components/share/part.module.css +428 -0
  44. package/src/components/share/part.tsx +780 -0
  45. package/src/components/share.module.css +832 -0
  46. package/src/content/docs/1-0.mdx +67 -0
  47. package/src/content/docs/acp.mdx +156 -0
  48. package/src/content/docs/agents.mdx +720 -0
  49. package/src/content/docs/cli.mdx +597 -0
  50. package/src/content/docs/commands.mdx +323 -0
  51. package/src/content/docs/config.mdx +683 -0
  52. package/src/content/docs/custom-tools.mdx +170 -0
  53. package/src/content/docs/ecosystem.mdx +76 -0
  54. package/src/content/docs/enterprise.mdx +170 -0
  55. package/src/content/docs/formatters.mdx +130 -0
  56. package/src/content/docs/github.mdx +321 -0
  57. package/src/content/docs/gitlab.mdx +195 -0
  58. package/src/content/docs/ide.mdx +48 -0
  59. package/src/content/docs/index.mdx +359 -0
  60. package/src/content/docs/keybinds.mdx +191 -0
  61. package/src/content/docs/lsp.mdx +188 -0
  62. package/src/content/docs/mcp-servers.mdx +511 -0
  63. package/src/content/docs/models.mdx +223 -0
  64. package/src/content/docs/modes.mdx +331 -0
  65. package/src/content/docs/network.mdx +57 -0
  66. package/src/content/docs/permissions.mdx +237 -0
  67. package/src/content/docs/plugins.mdx +362 -0
  68. package/src/content/docs/providers.mdx +1889 -0
  69. package/src/content/docs/rules.mdx +180 -0
  70. package/src/content/docs/sdk.mdx +391 -0
  71. package/src/content/docs/server.mdx +286 -0
  72. package/src/content/docs/share.mdx +128 -0
  73. package/src/content/docs/skills.mdx +220 -0
  74. package/src/content/docs/themes.mdx +369 -0
  75. package/src/content/docs/tools.mdx +345 -0
  76. package/src/content/docs/troubleshooting.mdx +300 -0
  77. package/src/content/docs/tui.mdx +390 -0
  78. package/src/content/docs/web.mdx +136 -0
  79. package/src/content/docs/windows-wsl.mdx +113 -0
  80. package/src/content/docs/zen.mdx +251 -0
  81. package/src/content.config.ts +7 -0
  82. package/src/pages/[...slug].md.ts +18 -0
  83. package/src/pages/s/[id].astro +113 -0
  84. package/src/styles/custom.css +405 -0
  85. package/src/types/lang-map.d.ts +27 -0
  86. 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">&ldquo;{props.state.input.pattern}&rdquo;</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">&ldquo;{props.state.input.pattern}&rdquo;</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">&ldquo;{props.state.input.prompt}&rdquo;</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
+ }