@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.
- package/package.json +3 -3
- package/src/chat/ApprovalCard.svelte +8 -2
- package/src/chat/ChatComposer.svelte +319 -0
- package/src/chat/ChatMessage.svelte +262 -0
- package/src/chat/ChatPanel.css +386 -0
- package/src/chat/ChatPanel.svelte +276 -1880
- package/src/chat/ChatPanelApprovals.svelte +28 -0
- package/src/chat/ChatPanelComposer.svelte +336 -0
- package/src/chat/ChatPanelDialogs.svelte +242 -0
- package/src/chat/ChatPanelHeader.svelte +108 -0
- package/src/chat/ChatPanelMessages.svelte +166 -0
- package/src/chat/ChatPanelMode.svelte +190 -0
- package/src/chat/ChatPanelState.svelte.ts +1 -0
- package/src/chat/ChatPanelToolbar.svelte +436 -0
- package/src/chat/ChatSurface.svelte +168 -0
- package/src/chat/ContextUsageStatus.svelte +97 -0
- package/src/chat/MessageItem.svelte +111 -214
- package/src/chat/ModelEffortPicker.svelte +326 -0
- package/src/chat/PickerEffort.svelte +71 -0
- package/src/chat/ReasoningControl.svelte +184 -0
- package/src/chat/ReasoningSlider.svelte +174 -0
- package/src/chat/SkillPicker.svelte +14 -6
- package/src/chat/UsageControl.svelte +223 -0
- package/src/chat/UsageDetails.svelte +44 -90
- package/src/chat/chat-attachments.svelte.ts +78 -0
- package/src/chat/chat-panel-history.svelte.ts +430 -0
- package/src/chat/chat-panel-models.svelte.ts +97 -0
- package/src/chat/chat-panel-props.ts +38 -0
- package/src/chat/context-status.ts +101 -0
- package/src/chat/icons/LICENSE-REMIX +186 -0
- package/src/chat/icons/LICENSE-SIMPLE-ICONS +30 -0
- package/src/chat/icons/PathIcon.svelte +8 -0
- package/src/chat/icons/effort-icons.ts +65 -0
- package/src/chat/icons/status-icons.ts +105 -0
- package/src/chat/index.ts +25 -1
- package/src/chat/model-effort.ts +177 -0
- package/src/chat/run-session-history.ts +56 -0
- package/src/chat/run-session-types.ts +69 -0
- package/src/chat/run-session.svelte.ts +73 -135
- package/src/chat/test-support/chat-fixture.ts +194 -0
- package/src/chat/test-support/conversation-fixtures.ts +32 -0
- package/src/chat/test-support/run-session-fixture.ts +112 -0
- package/src/dev/Demo.svelte +3 -1
- package/src/i18n/en-chat.d.ts +1 -0
- package/src/i18n/en-chat.js +201 -0
- package/src/i18n/en.ts +58 -180
- package/src/i18n/index.ts +7 -2
- package/src/i18n/zh-CN-chat.d.ts +1 -0
- package/src/i18n/zh-CN-chat.js +190 -0
- package/src/i18n/zh-CN.ts +8 -153
- package/src/logs/LogPanel.svelte +9 -3
- package/src/logs/LogSettings.svelte +3 -1
- package/src/mcp/McpClientEditor.svelte +204 -0
- package/src/mcp/McpClientSettings.svelte +57 -130
- package/src/mcp/McpServerSettings.svelte +47 -11
- package/src/mcp/input.ts +2 -1
- package/src/ports/contract.ts +11 -2
- package/src/ports/fake-optional.ts +81 -0
- package/src/ports/fake.ts +90 -86
- package/src/ports/search.ts +11 -2
- package/src/ports/test-support/fake-fixture.ts +63 -0
- package/src/providers/EndpointKeysDialog.svelte +27 -13
- package/src/providers/EndpointModels.svelte +311 -0
- package/src/providers/EndpointPanel.svelte +33 -258
- package/src/providers/ModelPickerDialog.svelte +59 -16
- package/src/providers/ModelPricing.svelte +125 -0
- package/src/providers/ModelSelect.svelte +22 -8
- package/src/providers/ModelSettingsDialog.svelte +61 -126
- package/src/providers/ProviderDialogs.svelte +120 -0
- package/src/providers/ProviderList.svelte +159 -0
- package/src/providers/ProvidersSettings.svelte +67 -207
- package/src/providers/RequestConfigDialog.svelte +40 -12
- package/src/providers/SecretInput.svelte +11 -3
- package/src/providers/draft.ts +39 -13
- package/src/providers/edit-session.ts +31 -7
- package/src/providers/reasoning.ts +18 -4
- package/src/providers/revision.svelte.ts +3 -1
- package/src/providers/test-support/providers-fixture.ts +120 -0
- package/src/search/SearchAdvancedTools.svelte +182 -0
- package/src/search/SearchProviderCard.svelte +195 -0
- package/src/search/SearchSettings.svelte +62 -265
- package/src/search/settings-flow.ts +8 -3
- package/src/skills/SkillAdd.svelte +51 -18
- package/src/skills/SkillAuthoring.svelte +69 -20
- package/src/skills/SkillEditor.svelte +13 -3
- package/src/skills/SkillPreviewDialog.svelte +19 -5
- package/src/skills/SkillsSettings.svelte +11 -4
- package/src/skills/test-support/skills-fixture.ts +139 -0
- package/src/tauri/index.ts +23 -7
- 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 {
|
|
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 = {
|
|
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)
|
|
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)
|
|
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 (
|
|
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)
|
|
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 (
|
|
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')
|
|
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))
|
|
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 = {
|
|
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: () =>
|
|
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: () =>
|
|
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', {
|
|
299
|
-
|
|
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 (
|
|
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 =
|
|
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)
|
|
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)
|
|
384
|
+
if (!retry && !translation && selectedId && !request.replyToId)
|
|
385
|
+
getTurn(conversationId, selectedId)
|
|
338
386
|
const model =
|
|
339
387
|
request.model ??
|
|
340
|
-
(source?.endpointId && source.modelId
|
|
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 (
|
|
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: '
|
|
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: () =>
|
|
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
|
-
|
|
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
|
-
|
|
456
|
-
result.
|
|
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
|
}
|
package/src/ports/search.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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) =>
|
|
77
|
-
|
|
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(
|
|
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"
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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) =>
|
|
284
|
-
|
|
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))}
|
|
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) =>
|
|
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">
|
|
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}
|