@ouraihub/aether-ui 0.1.0 → 0.1.2

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 (90) hide show
  1. package/package.json +3 -3
  2. package/src/chat/ApprovalCard.svelte +8 -2
  3. package/src/chat/ChatComposer.svelte +319 -0
  4. package/src/chat/ChatMessage.svelte +262 -0
  5. package/src/chat/ChatPanel.css +386 -0
  6. package/src/chat/ChatPanel.svelte +276 -1880
  7. package/src/chat/ChatPanelApprovals.svelte +28 -0
  8. package/src/chat/ChatPanelComposer.svelte +336 -0
  9. package/src/chat/ChatPanelDialogs.svelte +242 -0
  10. package/src/chat/ChatPanelHeader.svelte +108 -0
  11. package/src/chat/ChatPanelMessages.svelte +166 -0
  12. package/src/chat/ChatPanelMode.svelte +190 -0
  13. package/src/chat/ChatPanelState.svelte.ts +1 -0
  14. package/src/chat/ChatPanelToolbar.svelte +436 -0
  15. package/src/chat/ChatSurface.svelte +168 -0
  16. package/src/chat/ContextUsageStatus.svelte +97 -0
  17. package/src/chat/MessageItem.svelte +111 -214
  18. package/src/chat/ModelEffortPicker.svelte +326 -0
  19. package/src/chat/PickerEffort.svelte +71 -0
  20. package/src/chat/ReasoningControl.svelte +184 -0
  21. package/src/chat/ReasoningSlider.svelte +174 -0
  22. package/src/chat/SkillPicker.svelte +14 -6
  23. package/src/chat/UsageControl.svelte +223 -0
  24. package/src/chat/UsageDetails.svelte +44 -90
  25. package/src/chat/chat-attachments.svelte.ts +78 -0
  26. package/src/chat/chat-panel-history.svelte.ts +430 -0
  27. package/src/chat/chat-panel-models.svelte.ts +97 -0
  28. package/src/chat/chat-panel-props.ts +38 -0
  29. package/src/chat/context-status.ts +101 -0
  30. package/src/chat/icons/LICENSE-REMIX +186 -0
  31. package/src/chat/icons/LICENSE-SIMPLE-ICONS +30 -0
  32. package/src/chat/icons/PathIcon.svelte +8 -0
  33. package/src/chat/icons/effort-icons.ts +65 -0
  34. package/src/chat/icons/status-icons.ts +105 -0
  35. package/src/chat/index.ts +25 -1
  36. package/src/chat/model-effort.ts +177 -0
  37. package/src/chat/run-session-history.ts +56 -0
  38. package/src/chat/run-session-types.ts +69 -0
  39. package/src/chat/run-session.svelte.ts +73 -135
  40. package/src/chat/test-support/chat-fixture.ts +194 -0
  41. package/src/chat/test-support/conversation-fixtures.ts +32 -0
  42. package/src/chat/test-support/run-session-fixture.ts +112 -0
  43. package/src/dev/Demo.svelte +3 -1
  44. package/src/i18n/en-chat.d.ts +1 -0
  45. package/src/i18n/en-chat.js +201 -0
  46. package/src/i18n/en.ts +58 -180
  47. package/src/i18n/index.ts +7 -2
  48. package/src/i18n/zh-CN-chat.d.ts +1 -0
  49. package/src/i18n/zh-CN-chat.js +190 -0
  50. package/src/i18n/zh-CN.ts +8 -153
  51. package/src/logs/LogPanel.svelte +9 -3
  52. package/src/logs/LogSettings.svelte +3 -1
  53. package/src/mcp/McpClientEditor.svelte +204 -0
  54. package/src/mcp/McpClientSettings.svelte +57 -130
  55. package/src/mcp/McpServerSettings.svelte +47 -11
  56. package/src/mcp/input.ts +2 -1
  57. package/src/ports/contract.ts +11 -2
  58. package/src/ports/fake-optional.ts +81 -0
  59. package/src/ports/fake.ts +90 -86
  60. package/src/ports/search.ts +11 -2
  61. package/src/ports/test-support/fake-fixture.ts +63 -0
  62. package/src/providers/EndpointKeysDialog.svelte +27 -13
  63. package/src/providers/EndpointModels.svelte +311 -0
  64. package/src/providers/EndpointPanel.svelte +33 -258
  65. package/src/providers/ModelPickerDialog.svelte +59 -16
  66. package/src/providers/ModelPricing.svelte +125 -0
  67. package/src/providers/ModelSelect.svelte +22 -8
  68. package/src/providers/ModelSettingsDialog.svelte +61 -126
  69. package/src/providers/ProviderDialogs.svelte +120 -0
  70. package/src/providers/ProviderList.svelte +159 -0
  71. package/src/providers/ProvidersSettings.svelte +67 -207
  72. package/src/providers/RequestConfigDialog.svelte +40 -12
  73. package/src/providers/SecretInput.svelte +11 -3
  74. package/src/providers/draft.ts +39 -13
  75. package/src/providers/edit-session.ts +31 -7
  76. package/src/providers/reasoning.ts +18 -4
  77. package/src/providers/revision.svelte.ts +3 -1
  78. package/src/providers/test-support/providers-fixture.ts +120 -0
  79. package/src/search/SearchAdvancedTools.svelte +182 -0
  80. package/src/search/SearchProviderCard.svelte +195 -0
  81. package/src/search/SearchSettings.svelte +62 -265
  82. package/src/search/settings-flow.ts +8 -3
  83. package/src/skills/SkillAdd.svelte +51 -18
  84. package/src/skills/SkillAuthoring.svelte +69 -20
  85. package/src/skills/SkillEditor.svelte +13 -3
  86. package/src/skills/SkillPreviewDialog.svelte +19 -5
  87. package/src/skills/SkillsSettings.svelte +11 -4
  88. package/src/skills/test-support/skills-fixture.ts +139 -0
  89. package/src/tauri/index.ts +23 -7
  90. package/src/web/index.ts +90 -25
@@ -6,14 +6,11 @@
6
6
  import { onDestroy } from 'svelte'
7
7
  import { Alert, AlertDescription } from '#lib/components/ui/alert/index.js'
8
8
  import { Button } from '#lib/components/ui/button/index.js'
9
- import * as Dialog from '#lib/components/ui/dialog/index.js'
10
9
  import * as Empty from '#lib/components/ui/empty/index.js'
11
10
  import * as Field from '#lib/components/ui/field/index.js'
12
- import { Input } from '#lib/components/ui/input/index.js'
13
- import CopyInput from '../CopyInput.svelte'
14
11
  import * as Select from '#lib/components/ui/select/index.js'
15
12
  import { Switch } from '#lib/components/ui/switch/index.js'
16
- import { Textarea } from '#lib/components/ui/textarea/index.js'
13
+ import McpClientEditor from './McpClientEditor.svelte'
17
14
  import { getKitContext } from '../context'
18
15
  import { isKitClientError } from '../ports'
19
16
  import type { Approval, McpTransport, ServerDto, ServerSave } from '../ports'
@@ -21,8 +18,6 @@
21
18
  import { mapMcpInput } from './input'
22
19
 
23
20
  const APPROVALS: Approval[] = ['auto', 'ask', 'deny']
24
- const KINDS: McpTransport['kind'][] = ['http', 'stdio']
25
- const ARGS_PLACEHOLDER = '-y\n@modelcontextprotocol/server-filesystem'
26
21
 
27
22
  const { ports, i18n, notify, logger } = getKitContext()
28
23
  const client = ports.mcpClient
@@ -130,7 +125,9 @@
130
125
 
131
126
  /** One-line location shown in the list: the URL, or the command line. */
132
127
  function location(transport: McpTransport): string {
133
- return transport.kind === 'http' ? transport.url : [transport.command, ...transport.args].join(' ')
128
+ return transport.kind === 'http'
129
+ ? transport.url
130
+ : [transport.command, ...transport.args].join(' ')
134
131
  }
135
132
 
136
133
  const update = (next: ServerSave) => act(async () => void (servers = await need().save(next)))
@@ -277,7 +274,9 @@
277
274
  >{server.name} <span class="text-muted-foreground">({server.slug})</span></span
278
275
  >
279
276
  <span class="text-muted-foreground truncate font-mono text-xs"
280
- >{t(`mcp.client.transport.${server.transport.kind}`)} · {location(server.transport)}</span
277
+ >{t(`mcp.client.transport.${server.transport.kind}`)} · {location(
278
+ server.transport,
279
+ )}</span
281
280
  >
282
281
  {#if server.secretKeys.length}
283
282
  <span class="text-muted-foreground text-xs">
@@ -291,7 +290,12 @@
291
290
  <Button size="sm" variant="outline" disabled={busy} onclick={() => edit(server)}>
292
291
  {t('mcp.client.edit')}
293
292
  </Button>
294
- <Button size="sm" variant="destructive" disabled={busy} onclick={() => void remove(server)}>
293
+ <Button
294
+ size="sm"
295
+ variant="destructive"
296
+ disabled={busy}
297
+ onclick={() => void remove(server)}
298
+ >
295
299
  {t('mcp.client.delete')}
296
300
  </Button>
297
301
  </div>
@@ -303,7 +307,9 @@
303
307
  disabled={busy}
304
308
  onCheckedChange={(on) => void update({ ...base(server), enabled: on })}
305
309
  />
306
- <Field.Label for={`kit-mcp-enabled-${server.id}`}>{t('mcp.client.enabled')}</Field.Label>
310
+ <Field.Label for={`kit-mcp-enabled-${server.id}`}
311
+ >{t('mcp.client.enabled')}</Field.Label
312
+ >
307
313
  </Field.Field>
308
314
  <Field.Field orientation="horizontal" class="w-auto">
309
315
  <Switch
@@ -312,7 +318,9 @@
312
318
  disabled={busy}
313
319
  onCheckedChange={(on) => void setTrusted(server, on)}
314
320
  />
315
- <Field.Label for={`kit-mcp-trusted-${server.id}`}>{t('mcp.client.trusted')}</Field.Label>
321
+ <Field.Label for={`kit-mcp-trusted-${server.id}`}
322
+ >{t('mcp.client.trusted')}</Field.Label
323
+ >
316
324
  </Field.Field>
317
325
  {#if server.transport.kind === 'http'}
318
326
  <Field.Field orientation="horizontal" class="w-auto">
@@ -322,13 +330,19 @@
322
330
  disabled={busy}
323
331
  onCheckedChange={(on) => void setPrivate(server, on)}
324
332
  />
325
- <Field.Label for={`kit-mcp-private-${server.id}`}>{t('mcp.client.allowPrivate')}</Field.Label>
333
+ <Field.Label for={`kit-mcp-private-${server.id}`}
334
+ >{t('mcp.client.allowPrivate')}</Field.Label
335
+ >
326
336
  </Field.Field>
327
337
  {/if}
328
338
  </div>
329
339
  {#if testErrors[server.id]}
330
340
  <Alert variant="destructive">
331
- <AlertDescription>{t('mcp.client.testFailed', { error: testErrors[server.id] ?? '' })}</AlertDescription>
341
+ <AlertDescription
342
+ >{t('mcp.client.testFailed', {
343
+ error: testErrors[server.id] ?? '',
344
+ })}</AlertDescription
345
+ >
332
346
  </Alert>
333
347
  {/if}
334
348
  {#if server.tools.length}
@@ -345,7 +359,8 @@
345
359
  <Select.Root
346
360
  type="single"
347
361
  value={tool.approval}
348
- onValueChange={(value) => void setApproval(server, tool.name, value as Approval)}
362
+ onValueChange={(value) =>
363
+ void setApproval(server, tool.name, value as Approval)}
349
364
  >
350
365
  <Select.Trigger
351
366
  size="sm"
@@ -357,7 +372,10 @@
357
372
  <Select.Content>
358
373
  <Select.Group>
359
374
  {#each APPROVALS as approval (approval)}
360
- <Select.Item value={approval} label={t(`mcp.client.approval.${approval}`)} />
375
+ <Select.Item
376
+ value={approval}
377
+ label={t(`mcp.client.approval.${approval}`)}
378
+ />
361
379
  {/each}
362
380
  </Select.Group>
363
381
  </Select.Content>
@@ -373,119 +391,28 @@
373
391
  </ul>
374
392
  </section>
375
393
 
376
- <Dialog.Root
377
- open={editing}
378
- onOpenChange={(open) => {
379
- if (!open) closeEditor()
380
- }}
381
- >
382
- <Dialog.Content class="sm:max-w-xl" oninput={() => editRevision++}>
383
- <Dialog.Header>
384
- <Dialog.Title>{editId ? t('mcp.client.editTitle') : t('mcp.client.add')}</Dialog.Title>
385
- </Dialog.Header>
386
- <Field.Group>
387
- <div class="flex gap-3">
388
- <Field.Field class="flex-1">
389
- <Field.Label for="kit-mcp-name">{t('mcp.client.name')}</Field.Label>
390
- <Input id="kit-mcp-name" maxlength={200} bind:value={name} />
391
- </Field.Field>
392
- <Field.Field class="w-40">
393
- <Field.Label for="kit-mcp-slug">{t('mcp.client.slug')}</Field.Label>
394
- <Input id="kit-mcp-slug" class="font-mono" maxlength={24} bind:value={slug} />
395
- </Field.Field>
396
- </div>
397
- <Field.Field>
398
- <Field.Label for="kit-mcp-transport">{t('mcp.client.transport')}</Field.Label>
399
- <Select.Root type="single" value={kind} onValueChange={(value) => changeKind(value as McpTransport['kind'])}>
400
- <Select.Trigger id="kit-mcp-transport" class="w-full">
401
- {t(`mcp.client.transport.${kind}`)}
402
- </Select.Trigger>
403
- <Select.Content>
404
- <Select.Group>
405
- {#each KINDS as option (option)}
406
- <Select.Item value={option} label={t(`mcp.client.transport.${option}`)} />
407
- {/each}
408
- </Select.Group>
409
- </Select.Content>
410
- </Select.Root>
411
- </Field.Field>
412
- {#if kind === 'http'}
413
- <Field.Field>
414
- <Field.Label for="kit-mcp-url">{t('mcp.client.url')}</Field.Label>
415
- <CopyInput id="kit-mcp-url" class="font-mono" placeholder="https://example.com/mcp" bind:value={url} />
416
- <Field.Description>{t('mcp.client.urlHint')}</Field.Description>
417
- </Field.Field>
418
- {:else}
419
- <Field.Field>
420
- <Field.Label for="kit-mcp-command">{t('mcp.client.command')}</Field.Label>
421
- <Input id="kit-mcp-command" class="font-mono" maxlength={1024} placeholder="npx" bind:value={command} />
422
- </Field.Field>
423
- <Field.Field>
424
- <Field.Label for="kit-mcp-args">{t('mcp.client.args')}</Field.Label>
425
- <Textarea id="kit-mcp-args" class="font-mono text-xs" placeholder={ARGS_PLACEHOLDER} bind:value={args} />
426
- <Field.Description>{t('mcp.client.argsHint')}</Field.Description>
427
- </Field.Field>
428
- <Field.Field>
429
- <Field.Label for="kit-mcp-cwd">{t('mcp.client.cwd')}</Field.Label>
430
- <Input id="kit-mcp-cwd" class="font-mono" bind:value={cwd} />
431
- <Field.Description>{t('mcp.client.cwdHint')}</Field.Description>
432
- </Field.Field>
433
- {/if}
434
- {#if editId}
435
- <Field.Field orientation="horizontal">
436
- <Switch id="kit-mcp-replace" checked={replaceSecret} onCheckedChange={changeReplacement} />
437
- <Field.Label for="kit-mcp-replace">
438
- {t('mcp.client.replaceSecret', { keys: currentKeys.join(', ') || '—' })}
439
- </Field.Label>
440
- </Field.Field>
441
- {/if}
442
- {#if replaceSecret && kind === 'http'}
443
- <Field.Field>
444
- <Field.Label for="kit-mcp-headers">{t('mcp.client.headers')}</Field.Label>
445
- <Textarea
446
- id="kit-mcp-headers"
447
- class="font-mono text-xs"
448
- placeholder="Authorization: Bearer …"
449
- bind:value={headers}
450
- oninput={() => secretRevision++}
451
- />
452
- </Field.Field>
453
- <Field.Field>
454
- <Field.Label for="kit-mcp-query">{t('mcp.client.query')}</Field.Label>
455
- <Textarea
456
- id="kit-mcp-query"
457
- class="font-mono text-xs"
458
- placeholder="ak=…"
459
- bind:value={query}
460
- oninput={() => secretRevision++}
461
- />
462
- <Field.Description>{t('mcp.client.secretHint')}</Field.Description>
463
- </Field.Field>
464
- {:else if replaceSecret}
465
- <Field.Field>
466
- <Field.Label for="kit-mcp-env">{t('mcp.client.env')}</Field.Label>
467
- <Textarea
468
- id="kit-mcp-env"
469
- class="font-mono text-xs"
470
- placeholder="GITHUB_TOKEN=…"
471
- bind:value={env}
472
- oninput={() => secretRevision++}
473
- />
474
- <Field.Description>{t('mcp.client.envHint')}</Field.Description>
475
- </Field.Field>
476
- {/if}
477
- </Field.Group>
478
- <Dialog.Footer>
479
- <Button variant="outline" onclick={closeEditor}>{t('common.cancel')}</Button>
480
- <Button
481
- disabled={busy ||
482
- editorSaving ||
483
- !name.trim() ||
484
- !slug.trim() ||
485
- !(kind === 'http' ? url.trim() : command.trim())}
486
- onclick={save}>{t('mcp.client.save')}</Button
487
- >
488
- </Dialog.Footer>
489
- </Dialog.Content>
490
- </Dialog.Root>
394
+ <McpClientEditor
395
+ bind:open={editing}
396
+ {editId}
397
+ bind:name
398
+ bind:slug
399
+ bind:kind
400
+ bind:url
401
+ bind:command
402
+ bind:args
403
+ bind:cwd
404
+ bind:replaceSecret
405
+ bind:headers
406
+ bind:query
407
+ bind:env
408
+ {currentKeys}
409
+ {editorSaving}
410
+ {busy}
411
+ onclose={closeEditor}
412
+ onsave={save}
413
+ onchangeKind={changeKind}
414
+ onchangeReplacement={changeReplacement}
415
+ oninputRevision={() => editRevision++}
416
+ onsecretInput={() => secretRevision++}
417
+ />
491
418
  {/if}
@@ -101,7 +101,11 @@
101
101
 
102
102
  async function load() {
103
103
  if (!server) return
104
- const [nextStatus, nextScopes, nextTokens] = await Promise.all([server.status(), server.scopes(), server.tokens()])
104
+ const [nextStatus, nextScopes, nextTokens] = await Promise.all([
105
+ server.status(),
106
+ server.scopes(),
107
+ server.tokens(),
108
+ ])
105
109
  setStatus(nextStatus)
106
110
  scopes = nextScopes
107
111
  tokens = nextTokens
@@ -111,7 +115,8 @@
111
115
  void act(load)
112
116
  })
113
117
 
114
- const toggleRunning = () => act(async () => setStatus(status?.running ? await need().stop() : await need().start()))
118
+ const toggleRunning = () =>
119
+ act(async () => setStatus(status?.running ? await need().stop() : await need().start()))
115
120
 
116
121
  const savePort = () => act(async () => setStatus(await need().setPort(Number(port))))
117
122
 
@@ -143,7 +148,8 @@
143
148
  const revision = tokenRevision
144
149
  const name = tokenName
145
150
  const grants = [...tokenScopes]
146
- const current = () => tokenSession.isCurrent(token) && targetName === serverName && targetUrl === status?.url
151
+ const current = () =>
152
+ tokenSession.isCurrent(token) && targetName === serverName && targetUrl === status?.url
147
153
  issuing = true
148
154
  try {
149
155
  const next = await need().issueToken(name, grants)
@@ -204,7 +210,9 @@
204
210
  <Field.Content>
205
211
  <Field.Label>{t('mcp.server.title')}</Field.Label>
206
212
  <Field.Description>
207
- {status.running ? t('mcp.server.running', { url: status.url }) : t('mcp.server.stopped')}
213
+ {status.running
214
+ ? t('mcp.server.running', { url: status.url })
215
+ : t('mcp.server.stopped')}
208
216
  · {t('mcp.server.toolCount', { count: status.toolCount })}
209
217
  </Field.Description>
210
218
  </Field.Content>
@@ -218,7 +226,14 @@
218
226
  </Field.Field>
219
227
  <Field.Field orientation="horizontal">
220
228
  <Field.Label for="kit-mcp-port">{t('mcp.server.port')}</Field.Label>
221
- <Input id="kit-mcp-port" class="w-32" type="number" min="1024" max="65535" bind:value={port} />
229
+ <Input
230
+ id="kit-mcp-port"
231
+ class="w-32"
232
+ type="number"
233
+ min="1024"
234
+ max="65535"
235
+ bind:value={port}
236
+ />
222
237
  <Button
223
238
  disabled={busy || issuing || port === String(status.port)}
224
239
  variant="outline"
@@ -240,7 +255,9 @@
240
255
  bind:checked={() => scope.enabled, (on) => void toggleScope(scope, on)}
241
256
  disabled={busy || issuing}
242
257
  />
243
- <Field.Label for={`kit-mcp-scope-${scope.id}`}>{t(scope.labelKey)} ({scope.id})</Field.Label>
258
+ <Field.Label for={`kit-mcp-scope-${scope.id}`}
259
+ >{t(scope.labelKey)} ({scope.id})</Field.Label
260
+ >
244
261
  </Field.Field>
245
262
  {/each}
246
263
  </Field.Group>
@@ -251,7 +268,12 @@
251
268
  <Field.Group>
252
269
  <Field.Field>
253
270
  <Field.Label for="kit-mcp-token-name">{t('mcp.server.tokenName')}</Field.Label>
254
- <Input id="kit-mcp-token-name" bind:value={tokenName} maxlength={200} oninput={() => tokenRevision++} />
271
+ <Input
272
+ id="kit-mcp-token-name"
273
+ bind:value={tokenName}
274
+ maxlength={200}
275
+ oninput={() => tokenRevision++}
276
+ />
255
277
  </Field.Field>
256
278
  {#each scopes as scope (scope.id)}
257
279
  <Field.Field orientation="horizontal">
@@ -260,7 +282,9 @@
260
282
  checked={tokenScopes.includes(scope.id)}
261
283
  onCheckedChange={(on) => toggleTokenScope(scope.id, on)}
262
284
  />
263
- <Field.Label for={`kit-mcp-grant-${scope.id}`}>{t(scope.labelKey)} ({scope.id})</Field.Label>
285
+ <Field.Label for={`kit-mcp-grant-${scope.id}`}
286
+ >{t(scope.labelKey)} ({scope.id})</Field.Label
287
+ >
264
288
  </Field.Field>
265
289
  {/each}
266
290
  <Field.Field orientation="horizontal">
@@ -289,10 +313,17 @@
289
313
  >
290
314
  {t('mcp.server.copyClaude')}
291
315
  </Button>
292
- <Button size="sm" variant="outline" disabled={!configInput} onclick={() => void copy('json')}>
316
+ <Button
317
+ size="sm"
318
+ variant="outline"
319
+ disabled={!configInput}
320
+ onclick={() => void copy('json')}
321
+ >
293
322
  {t('mcp.server.copyJson')}
294
323
  </Button>
295
- <Button size="sm" variant="ghost" onclick={clearIssued}>{t('mcp.server.dismiss')}</Button>
324
+ <Button size="sm" variant="ghost" onclick={clearIssued}
325
+ >{t('mcp.server.dismiss')}</Button
326
+ >
296
327
  </div>
297
328
  </AlertDescription>
298
329
  </Alert>
@@ -313,7 +344,12 @@
313
344
  </span>
314
345
  </div>
315
346
  {#if token.revokedAt === null}
316
- <Button size="sm" variant="destructive" disabled={busy || issuing} onclick={() => void revoke(token)}>
347
+ <Button
348
+ size="sm"
349
+ variant="destructive"
350
+ disabled={busy || issuing}
351
+ onclick={() => void revoke(token)}
352
+ >
317
353
  {t('mcp.server.revoke')}
318
354
  </Button>
319
355
  {:else}
package/src/mcp/input.ts CHANGED
@@ -15,7 +15,8 @@ export interface McpInput {
15
15
  env: string
16
16
  }
17
17
 
18
- export type McpInputResult = { ok: true; value: ServerSave } | { ok: false; error: 'secretLine' | 'transportChanged' }
18
+ export type McpInputResult =
19
+ { ok: true; value: ServerSave } | { ok: false; error: 'secretLine' | 'transportChanged' }
19
20
 
20
21
  /** Split only at the first separator; values may themselves contain separators. */
21
22
  function pairs(text: string, separator: string): Record<string, string> | null {
@@ -86,7 +86,12 @@ export interface ConversationClient {
86
86
  get(id: string): Promise<Conversation>
87
87
  remove(id: string): Promise<void>
88
88
  rename(id: string, title: string): Promise<ConversationSummary>
89
- editTurn(conversationId: string, turnId: string, text: string, expectedText: string): Promise<Conversation>
89
+ editTurn(
90
+ conversationId: string,
91
+ turnId: string,
92
+ text: string,
93
+ expectedText: string,
94
+ ): Promise<Conversation>
90
95
  removeTurn(conversationId: string, turnId: string): Promise<Conversation>
91
96
  fork(conversationId: string, turnId: string): Promise<Conversation>
92
97
  selectTurn(conversationId: string, turnId: string): Promise<Conversation>
@@ -94,7 +99,11 @@ export interface ConversationClient {
94
99
 
95
100
  export interface RunClient {
96
101
  /** Resolves once accepted; events may arrive before acceptance. */
97
- start(request: RunRequest, onEvent: (event: RunEvent) => void, options?: CallOptions): Promise<RunAccepted>
102
+ start(
103
+ request: RunRequest,
104
+ onEvent: (event: RunEvent) => void,
105
+ options?: CallOptions,
106
+ ): Promise<RunAccepted>
98
107
  cancel(runId: string): Promise<void>
99
108
  approve(runId: string, callId: string, allow: boolean): Promise<void>
100
109
  getSnapshot(runId: string): Promise<RunSnapshot>
@@ -0,0 +1,81 @@
1
+ import type { KitClientError } from './contract'
2
+ import type { McpClientPort, McpServerPort } from './contract'
3
+ import type { SearchPort } from './search'
4
+ import type { SkillSourcesPort } from './skill-sources'
5
+
6
+ export type FakeError = (
7
+ code: ConstructorParameters<typeof KitClientError>[0]['code'],
8
+ message: string,
9
+ details?: Record<string, string>,
10
+ ) => KitClientError
11
+
12
+ const STOPPED = { running: false, port: 0, url: '', toolCount: 0 }
13
+
14
+ export function mcpClientDefaults(options: Partial<McpClientPort>): McpClientPort {
15
+ return {
16
+ list: () => Promise.resolve([]),
17
+ save: () => Promise.resolve([]),
18
+ remove: () => Promise.resolve([]),
19
+ refresh: () => Promise.resolve([]),
20
+ setToolApproval: () => Promise.resolve(),
21
+ ...options,
22
+ }
23
+ }
24
+
25
+ export function mcpServerDefaults(options: Partial<McpServerPort>): McpServerPort {
26
+ return {
27
+ status: () => Promise.resolve(STOPPED),
28
+ tokens: () => Promise.resolve([]),
29
+ start: () => Promise.resolve(STOPPED),
30
+ stop: () => Promise.resolve(STOPPED),
31
+ setPort: (port) => Promise.resolve({ ...STOPPED, port }),
32
+ issueToken: (name, scopes) =>
33
+ Promise.resolve({
34
+ token: 'kit_fake',
35
+ id: 'fake-token',
36
+ name,
37
+ scopes: [...scopes],
38
+ createdAt: 0,
39
+ lastUsedAt: null,
40
+ revokedAt: null,
41
+ }),
42
+ revokeToken: () => Promise.resolve(),
43
+ scopes: () => Promise.resolve([]),
44
+ setScopes: () => Promise.resolve([]),
45
+ ...options,
46
+ }
47
+ }
48
+
49
+ export function searchDefaults(options: Partial<SearchPort>, error: FakeError): SearchPort {
50
+ const unavailable = () => Promise.reject(error('internal', 'Configure the fake search method'))
51
+ return {
52
+ settings: unavailable,
53
+ save: unavailable,
54
+ setDefault: unavailable,
55
+ savePreferences: unavailable,
56
+ saveKey: unavailable,
57
+ removeKey: unavailable,
58
+ reveal: unavailable,
59
+ check: unavailable,
60
+ search: unavailable,
61
+ fetch: unavailable,
62
+ ...options,
63
+ }
64
+ }
65
+
66
+ export function skillSourceDefaults(
67
+ options: Partial<SkillSourcesPort>,
68
+ error: FakeError,
69
+ ): SkillSourcesPort {
70
+ const unavailable = () =>
71
+ Promise.reject(error('internal', 'Configure the fake skill source method'))
72
+ return {
73
+ search: unavailable,
74
+ prepare: unavailable,
75
+ discover: unavailable,
76
+ prepareSystem: unavailable,
77
+ pickDirectory: unavailable,
78
+ pickSearchDirectory: unavailable,
79
+ ...options,
80
+ }
81
+ }