@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
package/src/ports/fake.ts CHANGED
@@ -1,6 +1,19 @@
1
1
  // In-memory RunClient, ConversationClient and desktop actions for tests and demos (docs/16, docs/22).
2
2
  // Seed storage with initialConversations; override clients independently or omit a capability.
3
3
  import { KitClientError } from './contract'
4
+ import {
5
+ mcpClientDefaults,
6
+ mcpServerDefaults,
7
+ searchDefaults,
8
+ skillSourceDefaults,
9
+ } from './fake-optional'
10
+
11
+ const clientError = (
12
+ code: ConstructorParameters<typeof KitClientError>[0]['code'],
13
+ message: string,
14
+ details: Record<string, string> = {},
15
+ ) => new KitClientError({ code, message, details })
16
+
4
17
  import type {
5
18
  ConversationClient,
6
19
  DesktopActions,
@@ -12,7 +25,13 @@ import type {
12
25
  ProvidersPort,
13
26
  SkillsPort,
14
27
  } from './contract'
15
- import type { Conversation, LogLevel, RunEvent, RunSnapshot, Turn } from '@ouraihub/aether-contracts'
28
+ import type {
29
+ Conversation,
30
+ LogLevel,
31
+ RunEvent,
32
+ RunSnapshot,
33
+ Turn,
34
+ } from '@ouraihub/aether-contracts'
16
35
  import type { SearchPort } from './search'
17
36
  import type { SkillSourcesPort } from './skill-sources'
18
37
 
@@ -35,13 +54,6 @@ export interface FakePortsOptions {
35
54
  omit?: readonly PortName[]
36
55
  }
37
56
 
38
- const STOPPED = { running: false, port: 0, url: '', toolCount: 0 }
39
- const clientError = (
40
- code: ConstructorParameters<typeof KitClientError>[0]['code'],
41
- message: string,
42
- details: Record<string, string> = {},
43
- ) => new KitClientError({ code, message, details })
44
-
45
57
  export function createFakePorts(options: FakePortsOptions = {}): KitPorts {
46
58
  let level: LogLevel = 'INFO'
47
59
  let sequence = 0
@@ -95,7 +107,11 @@ export function createFakePorts(options: FakePortsOptions = {}): KitPorts {
95
107
  turn.status = run.status
96
108
  if (run.status === 'cancelled') turn.text = ''
97
109
  if (select && run.status === 'completed')
98
- conversation.summary = { ...conversation.summary, selectedTurnId: turn.id, updatedAt: Date.now() }
110
+ conversation.summary = {
111
+ ...conversation.summary,
112
+ selectedTurnId: turn.id,
113
+ updatedAt: Date.now(),
114
+ }
99
115
  }
100
116
  signal?.removeEventListener('abort', cancel)
101
117
  terminals.push(run.runId)
@@ -111,7 +127,8 @@ export function createFakePorts(options: FakePortsOptions = {}): KitPorts {
111
127
  const snapshot = (conversation: Conversation): Conversation => structuredClone(conversation)
112
128
  const getConversation = (id: string) => {
113
129
  const conversation = conversations.get(id)
114
- if (!conversation) throw clientError('not_found', `conversation ${id}`, { entity: 'conversation' })
130
+ if (!conversation)
131
+ throw clientError('not_found', `conversation ${id}`, { entity: 'conversation' })
115
132
  if (conversation.turns.some((turn) => turn.status === 'running'))
116
133
  throw clientError('conflict', 'conversation is active', { reason: 'active_run' })
117
134
  return conversation
@@ -130,7 +147,8 @@ export function createFakePorts(options: FakePortsOptions = {}): KitPorts {
130
147
  ),
131
148
  get: async (id: string) => {
132
149
  const conversation = conversations.get(id)
133
- if (!conversation) throw clientError('not_found', `conversation ${id}`, { entity: 'conversation' })
150
+ if (!conversation)
151
+ throw clientError('not_found', `conversation ${id}`, { entity: 'conversation' })
134
152
  return snapshot(conversation)
135
153
  },
136
154
  remove: async (id: string) => {
@@ -142,13 +160,23 @@ export function createFakePorts(options: FakePortsOptions = {}): KitPorts {
142
160
  conversation.summary = { ...conversation.summary, title, updatedAt: Date.now() }
143
161
  return { ...conversation.summary }
144
162
  },
145
- editTurn: async (conversationId: string, turnId: string, text: string, expectedText: string) => {
163
+ editTurn: async (
164
+ conversationId: string,
165
+ turnId: string,
166
+ text: string,
167
+ expectedText: string,
168
+ ) => {
146
169
  const conversation = getConversation(conversationId)
147
170
  const turn = getTurn(conversationId, turnId)
148
- if (turn.text !== expectedText) throw clientError('conflict', 'turn changed', { reason: 'text_changed' })
171
+ if (turn.text !== expectedText)
172
+ throw clientError('conflict', 'turn changed', { reason: 'text_changed' })
149
173
  if (!text.trim() || Array.from(text).length > 24_000)
150
174
  throw clientError('bad_request', 'invalid text', { field: 'text' })
151
- if (conversation.turns.some((candidate) => candidate.parentId === turnId || candidate.supersedesId === turnId))
175
+ if (
176
+ conversation.turns.some(
177
+ (candidate) => candidate.parentId === turnId || candidate.supersedesId === turnId,
178
+ )
179
+ )
152
180
  throw clientError('conflict', 'turn has children', { reason: 'has_children' })
153
181
  turn.text = text
154
182
  turn.reasoningText = null
@@ -160,7 +188,8 @@ export function createFakePorts(options: FakePortsOptions = {}): KitPorts {
160
188
  // Same rule as both hosts (AutoNote docs/35 §3.3): reparent onto the successor, then delete.
161
189
  const conversation = getConversation(conversationId)
162
190
  const turn = getTurn(conversationId, turnId)
163
- if (turn.status === 'running') throw clientError('conflict', 'turn running', { reason: 'turn_running' })
191
+ if (turn.status === 'running')
192
+ throw clientError('conflict', 'turn running', { reason: 'turn_running' })
164
193
  // Turns are stored in creation order, which stands in for `seq`.
165
194
  const at = conversation.turns.indexOf(turn)
166
195
  const group = conversation.turns.filter(
@@ -202,7 +231,8 @@ export function createFakePorts(options: FakePortsOptions = {}): KitPorts {
202
231
  const seen = new Set<string>()
203
232
  let cursor: Turn | undefined = target
204
233
  while (cursor) {
205
- if (seen.has(cursor.id)) throw clientError('conflict', 'history cycle', { reason: 'history_cycle' })
234
+ if (seen.has(cursor.id))
235
+ throw clientError('conflict', 'history cycle', { reason: 'history_cycle' })
206
236
  seen.add(cursor.id)
207
237
  chain.unshift(cursor)
208
238
  if (!cursor.parentId) break
@@ -247,7 +277,11 @@ export function createFakePorts(options: FakePortsOptions = {}): KitPorts {
247
277
  selectTurn: async (conversationId: string, turnId: string) => {
248
278
  const conversation = getConversation(conversationId)
249
279
  getTurn(conversationId, turnId)
250
- conversation.summary = { ...conversation.summary, selectedTurnId: turnId, updatedAt: Date.now() }
280
+ conversation.summary = {
281
+ ...conversation.summary,
282
+ selectedTurnId: turnId,
283
+ updatedAt: Date.now(),
284
+ }
251
285
  return snapshot(conversation)
252
286
  },
253
287
  }
@@ -270,7 +304,8 @@ export function createFakePorts(options: FakePortsOptions = {}): KitPorts {
270
304
  providers: {
271
305
  revealNeedsPassword: false,
272
306
  keyStrategies: false,
273
- revealKey: () => Promise.reject(clientError('internal', 'Configure the fake provider reveal method')),
307
+ revealKey: () =>
308
+ Promise.reject(clientError('internal', 'Configure the fake provider reveal method')),
274
309
  list: () => Promise.resolve([]),
275
310
  setEnabled: () => Promise.resolve([]),
276
311
  save: () => Promise.resolve([]),
@@ -278,7 +313,8 @@ export function createFakePorts(options: FakePortsOptions = {}): KitPorts {
278
313
  removeKey: () => Promise.resolve([]),
279
314
  remove: () => Promise.resolve([]),
280
315
  modelOptions: () => Promise.resolve([]),
281
- probe: () => Promise.resolve({ probeUrl: '', models: [], error: null, suggestedProtocol: null }),
316
+ probe: () =>
317
+ Promise.resolve({ probeUrl: '', models: [], error: null, suggestedProtocol: null }),
282
318
  ...options.providers,
283
319
  },
284
320
  chat: {
@@ -295,10 +331,19 @@ export function createFakePorts(options: FakePortsOptions = {}): KitPorts {
295
331
  )
296
332
  throw clientError('bad_request', 'invalid graph request', { field: 'action' })
297
333
  if (request.images?.length && (retry || translation))
298
- throw clientError('bad_request', 'images on action', { field: 'images', reason: 'action_conflict' })
299
- if (request.purpose === 'writing' && (request.replyToId != null || action || request.targetLanguage != null))
334
+ throw clientError('bad_request', 'images on action', {
335
+ field: 'images',
336
+ reason: 'action_conflict',
337
+ })
338
+ if (
339
+ request.purpose === 'writing' &&
340
+ (request.replyToId != null || action || request.targetLanguage != null)
341
+ )
300
342
  throw clientError('bad_request', 'writing graph fields', { field: 'action' })
301
- if ((retry || translation || request.replyToId != null) && (!request.conversationId || !storesConversations))
343
+ if (
344
+ (retry || translation || request.replyToId != null) &&
345
+ (!request.conversationId || !storesConversations)
346
+ )
302
347
  throw clientError('bad_request', 'conversation required', { field: 'conversationId' })
303
348
  const language = request.targetLanguage?.trim() ?? ''
304
349
  if (translation && (!language || Array.from(language).length > 80))
@@ -330,18 +375,28 @@ export function createFakePorts(options: FakePortsOptions = {}): KitPorts {
330
375
  conversation.summary.selectedTurnId ??
331
376
  [...conversation.turns].reverse().find((turn) => turn.status === 'completed')?.id ??
332
377
  null
333
- const sourceId = request.retryTurnId ?? request.translateTurnId ?? (retry ? selectedId : null)
378
+ const sourceId =
379
+ request.retryTurnId ?? request.translateTurnId ?? (retry ? selectedId : null)
334
380
  const source = sourceId ? getTurn(conversationId, sourceId) : undefined
335
- if ((retry || translation) && !source) throw clientError('not_found', 'source turn', { entity: 'turn' })
381
+ if ((retry || translation) && !source)
382
+ throw clientError('not_found', 'source turn', { entity: 'turn' })
336
383
  if (request.replyToId) getTurn(conversationId, request.replyToId)
337
- if (!retry && !translation && selectedId && !request.replyToId) getTurn(conversationId, selectedId)
384
+ if (!retry && !translation && selectedId && !request.replyToId)
385
+ getTurn(conversationId, selectedId)
338
386
  const model =
339
387
  request.model ??
340
- (source?.endpointId && source.modelId ? { endpointId: source.endpointId, modelId: source.modelId } : null)
388
+ (source?.endpointId && source.modelId
389
+ ? { endpointId: source.endpointId, modelId: source.modelId }
390
+ : null)
341
391
  if (source && !model) throw clientError('needs_config', 'source model unavailable')
342
392
  if (source && model && options.providers?.modelOptions) {
343
393
  const models = await ports.providers.modelOptions()
344
- if (!models.some((option) => option.endpointId === model.endpointId && option.modelId === model.modelId))
394
+ if (
395
+ !models.some(
396
+ (option) =>
397
+ option.endpointId === model.endpointId && option.modelId === model.modelId,
398
+ )
399
+ )
345
400
  throw clientError('needs_config', 'source model unavailable')
346
401
  }
347
402
  if (callOptions?.signal?.aborted) throw clientError('cancelled', 'cancelled')
@@ -400,7 +455,7 @@ export function createFakePorts(options: FakePortsOptions = {}): KitPorts {
400
455
  },
401
456
  approve: async (runId) => {
402
457
  if (!runs.has(runId)) throw clientError('not_found', `run ${runId}`, { entity: 'run' })
403
- throw clientError('conflict', 'no pending approval', { reason: 'approval_expired' })
458
+ throw clientError('conflict', 'no pending approval', { reason: 'approval_not_pending' })
404
459
  },
405
460
  getSnapshot: async (runId) => {
406
461
  const run = runs.get(runId)
@@ -412,7 +467,8 @@ export function createFakePorts(options: FakePortsOptions = {}): KitPorts {
412
467
  skills: {
413
468
  list: () => Promise.resolve([]),
414
469
  get: (id) => Promise.reject(clientError('not_found', `skill ${id}`, { entity: 'skill' })),
415
- preview: () => Promise.reject(clientError('bad_request', 'no package', { field: 'SKILL.md' })),
470
+ preview: () =>
471
+ Promise.reject(clientError('bad_request', 'no package', { field: 'SKILL.md' })),
416
472
  install: () => Promise.resolve([]),
417
473
  save: () => Promise.resolve([]),
418
474
  duplicate: () => Promise.resolve([]),
@@ -420,65 +476,13 @@ export function createFakePorts(options: FakePortsOptions = {}): KitPorts {
420
476
  remove: () => Promise.resolve([]),
421
477
  ...options.skills,
422
478
  },
423
- mcpClient: {
424
- list: () => Promise.resolve([]),
425
- save: () => Promise.resolve([]),
426
- remove: () => Promise.resolve([]),
427
- refresh: () => Promise.resolve([]),
428
- setToolApproval: () => Promise.resolve(),
429
- ...options.mcpClient,
430
- },
431
- mcpServer: {
432
- status: () => Promise.resolve(STOPPED),
433
- tokens: () => Promise.resolve([]),
434
- start: () => Promise.resolve(STOPPED),
435
- stop: () => Promise.resolve(STOPPED),
436
- setPort: (port) => Promise.resolve({ ...STOPPED, port }),
437
- issueToken: (name, scopes) =>
438
- Promise.resolve({
439
- token: 'kit_fake',
440
- id: 'fake-token',
441
- name,
442
- scopes: [...scopes],
443
- createdAt: 0,
444
- lastUsedAt: null,
445
- revokedAt: null,
446
- }),
447
- revokeToken: () => Promise.resolve(),
448
- scopes: () => Promise.resolve([]),
449
- setScopes: () => Promise.resolve([]),
450
- ...options.mcpServer,
451
- },
479
+ mcpClient: mcpClientDefaults(options.mcpClient ?? {}),
480
+ mcpServer: mcpServerDefaults(options.mcpServer ?? {}),
452
481
  }
453
482
  const result: KitPorts = { ...ports }
454
- if (options.search) {
455
- const unavailable = () => Promise.reject(clientError('internal', 'Configure the fake search method'))
456
- result.search = {
457
- settings: unavailable,
458
- save: unavailable,
459
- setDefault: unavailable,
460
- savePreferences: unavailable,
461
- saveKey: unavailable,
462
- removeKey: unavailable,
463
- reveal: unavailable,
464
- check: unavailable,
465
- search: unavailable,
466
- fetch: unavailable,
467
- ...options.search,
468
- }
469
- }
470
- if (options.skillSources) {
471
- const unavailable = () => Promise.reject(clientError('internal', 'Configure the fake skill source method'))
472
- result.skillSources = {
473
- search: unavailable,
474
- prepare: unavailable,
475
- discover: unavailable,
476
- prepareSystem: unavailable,
477
- pickDirectory: unavailable,
478
- pickSearchDirectory: unavailable,
479
- ...options.skillSources,
480
- }
481
- }
483
+ if (options.search) result.search = searchDefaults(options.search, clientError)
484
+ if (options.skillSources)
485
+ result.skillSources = skillSourceDefaults(options.skillSources, clientError)
482
486
  for (const name of options.omit ?? []) delete result[name]
483
487
  return result
484
488
  }
@@ -24,7 +24,11 @@ export interface SearchPort {
24
24
  revealNeedsPassword?: boolean
25
25
  settings(capability: SearchCap, options?: CallOptions): Promise<SearchSettingsDto>
26
26
  save(input: SearchSave, options?: CallOptions): Promise<SearchSettingsDto>
27
- setDefault(capability: SearchCap, provider: string, options?: CallOptions): Promise<SearchSettingsDto>
27
+ setDefault(
28
+ capability: SearchCap,
29
+ provider: string,
30
+ options?: CallOptions,
31
+ ): Promise<SearchSettingsDto>
28
32
  savePreferences(input: SearchPreferences, options?: CallOptions): Promise<SearchPreferences>
29
33
  saveKey(
30
34
  capability: SearchCap,
@@ -33,7 +37,12 @@ export interface SearchPort {
33
37
  keyId?: string,
34
38
  options?: CallOptions,
35
39
  ): Promise<SearchSettingsDto>
36
- removeKey(capability: SearchCap, provider: string, keyId: string, options?: CallOptions): Promise<SearchSettingsDto>
40
+ removeKey(
41
+ capability: SearchCap,
42
+ provider: string,
43
+ keyId: string,
44
+ options?: CallOptions,
45
+ ): Promise<SearchSettingsDto>
37
46
  reveal(
38
47
  capability: SearchCap,
39
48
  provider: string,
@@ -0,0 +1,63 @@
1
+ import { isKitClientError } from '../index'
2
+ import type { Conversation, RunRequest, Turn } from '../index'
3
+
4
+ export function required<T>(value: T | null | undefined): T {
5
+ if (value == null) throw new Error('Expected fixture is missing')
6
+ return value
7
+ }
8
+
9
+ export const model = { endpointId: 'endpoint', modelId: 'model' }
10
+
11
+ export const request: RunRequest = {
12
+ purpose: 'chat',
13
+ skills: [],
14
+ input: 'Question',
15
+ regenerate: false,
16
+ language: 'en',
17
+ model,
18
+ }
19
+
20
+ export const turn = (id: string, overrides: Partial<Turn> = {}): Turn => ({
21
+ id,
22
+ parentId: null,
23
+ questionId: id,
24
+ supersedesId: null,
25
+ kind: 'reply',
26
+ ...model,
27
+ status: 'completed',
28
+ input: 'Question',
29
+ text: 'Original answer',
30
+ errorCode: null,
31
+ reasoningText: 'Thinking',
32
+ images: [],
33
+ generated: [],
34
+ usage: {
35
+ inputTokens: 100,
36
+ outputTokens: 60,
37
+ cacheReadTokens: 20,
38
+ cacheWriteTokens: null,
39
+ reasoningTokens: 4,
40
+ },
41
+ contextUsage: { tokens: 160, contextWindow: 1000, source: 'reported', ...model },
42
+ ...overrides,
43
+ })
44
+
45
+ export const stored = (
46
+ turns: Turn[],
47
+ selectedTurnId: string | null = turns[0]?.id ?? null,
48
+ ): Conversation => ({
49
+ summary: {
50
+ id: 'stored',
51
+ title: 'Stored',
52
+ scope: 'scope',
53
+ createdAt: 1,
54
+ updatedAt: 2,
55
+ selectedTurnId,
56
+ },
57
+ turns,
58
+ })
59
+
60
+ export async function code(promise: Promise<unknown>) {
61
+ const error: unknown = await promise.catch((caught: unknown) => caught)
62
+ return isKitClientError(error) ? error.code : null
63
+ }
@@ -73,8 +73,10 @@
73
73
  const enabled = $derived(endpoint.keys.filter((k) => k.enabled).length)
74
74
  const name = (key: EndpointKey) => key.label || maskedText(key, t)
75
75
  /** Set aside by fill-first and not yet recovered; the host clears it lazily, so the time decides. */
76
- const paused = (key: EndpointKey) => key.exhaustedUntil !== null && key.exhaustedUntil > Date.now()
77
- const until = (ms: number) => new Date(ms).toLocaleString(i18n.locale, { dateStyle: 'short', timeStyle: 'short' })
76
+ const paused = (key: EndpointKey) =>
77
+ key.exhaustedUntil !== null && key.exhaustedUntil > Date.now()
78
+ const until = (ms: number) =>
79
+ new Date(ms).toLocaleString(i18n.locale, { dateStyle: 'short', timeStyle: 'short' })
78
80
 
79
81
  async function run(action: (port: ProvidersPort) => Promise<Provider[]>): Promise<boolean> {
80
82
  if (!ports.providers || busy || !open) return false
@@ -123,7 +125,10 @@
123
125
  return text
124
126
  } catch (error) {
125
127
  if (session.isCurrent(token))
126
- notify.toast('error', error instanceof Error ? error.message : t('providers.key_reveal_failed'))
128
+ notify.toast(
129
+ 'error',
130
+ error instanceof Error ? error.message : t('providers.key_reveal_failed'),
131
+ )
127
132
  return null
128
133
  } finally {
129
134
  if (session.isCurrent(token)) {
@@ -268,11 +273,14 @@
268
273
  onclick={() => void copy(key)}
269
274
  >
270
275
  {#if key.label}<span class="truncate text-sm">{key.label}</span>{/if}
271
- <span class="truncate font-mono text-xs text-muted-foreground">{maskedText(key, t)}</span>
272
- {#if paused(key) && key.exhaustedUntil !== null}<span class="text-warning truncate text-xs"
273
- >{t('providers.key_exhausted', { time: until(key.exhaustedUntil) })}{key.exhaustedReason
274
- ? ` · ${key.exhaustedReason}`
275
- : ''}</span
276
+ <span class="truncate font-mono text-xs text-muted-foreground"
277
+ >{maskedText(key, t)}</span
278
+ >
279
+ {#if paused(key) && key.exhaustedUntil !== null}<span
280
+ class="text-warning truncate text-xs"
281
+ >{t('providers.key_exhausted', {
282
+ time: until(key.exhaustedUntil),
283
+ })}{key.exhaustedReason ? ` · ${key.exhaustedReason}` : ''}</span
276
284
  >{/if}
277
285
  </button>
278
286
  {#if paused(key)}<Button
@@ -280,8 +288,9 @@
280
288
  size="xs"
281
289
  disabled={busy}
282
290
  onclick={() =>
283
- void run((port) => port.saveKey({ endpointId: endpoint.id, id: key.id, restore: true }))}
284
- >{t('providers.key_restore')}</Button
291
+ void run((port) =>
292
+ port.saveKey({ endpointId: endpoint.id, id: key.id, restore: true }),
293
+ )}>{t('providers.key_restore')}</Button
285
294
  >{/if}
286
295
  <Button
287
296
  variant="ghost"
@@ -306,7 +315,8 @@
306
315
  title={t('providers.key_delete')}
307
316
  aria-label={t('providers.key_delete')}
308
317
  disabled={busy}
309
- onclick={() => void run((port) => port.removeKey(endpoint.id, key.id))}><TrashIcon /></Button
318
+ onclick={() => void run((port) => port.removeKey(endpoint.id, key.id))}
319
+ ><TrashIcon /></Button
310
320
  >
311
321
  <Switch
312
322
  size="sm"
@@ -315,14 +325,18 @@
315
325
  checked={key.enabled}
316
326
  disabled={busy}
317
327
  onCheckedChange={(next) =>
318
- void run((port) => port.saveKey({ endpointId: endpoint.id, id: key.id, enabled: next }))}
328
+ void run((port) =>
329
+ port.saveKey({ endpointId: endpoint.id, id: key.id, enabled: next }),
330
+ )}
319
331
  />
320
332
  </div>
321
333
  {/if}
322
334
  </li>
323
335
  {:else}
324
336
  {#if edit?.id !== null}
325
- <li class="text-muted-foreground py-6 text-center text-kit-sm">{t('providers.keys_empty')}</li>
337
+ <li class="text-muted-foreground py-6 text-center text-kit-sm">
338
+ {t('providers.keys_empty')}
339
+ </li>
326
340
  {/if}
327
341
  {/each}
328
342
  {#if edit?.id === null}<li class="min-w-0">{@render editRow(true)}</li>{/if}