@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
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ContextRef,
|
|
3
|
+
ContextUsage,
|
|
4
|
+
Cost,
|
|
5
|
+
ImageRef,
|
|
6
|
+
ModelRef,
|
|
7
|
+
PermissionMode,
|
|
8
|
+
ReasoningEffort,
|
|
9
|
+
SearchHit,
|
|
10
|
+
SkillRef,
|
|
11
|
+
TokenUsage,
|
|
12
|
+
ToolOutcome,
|
|
13
|
+
TurnStatus,
|
|
14
|
+
} from '../ports'
|
|
15
|
+
|
|
16
|
+
/** One tool call of a turn, as shown by ToolCallItem / ApprovalCard. */
|
|
17
|
+
export interface ToolItem {
|
|
18
|
+
callId: string
|
|
19
|
+
name: string
|
|
20
|
+
server: string | null
|
|
21
|
+
pending: string | null
|
|
22
|
+
outcome: ToolOutcome | null
|
|
23
|
+
summary: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ChatTurn {
|
|
27
|
+
id: string
|
|
28
|
+
persisted: boolean
|
|
29
|
+
parentId: string | null
|
|
30
|
+
questionId: string
|
|
31
|
+
supersedesId: string | null
|
|
32
|
+
kind: 'reply' | 'translation'
|
|
33
|
+
endpointId: string | null
|
|
34
|
+
modelId: string | null
|
|
35
|
+
input: string
|
|
36
|
+
images: ImageRef[]
|
|
37
|
+
generated: ImageRef[]
|
|
38
|
+
text: string
|
|
39
|
+
reasoningText: string
|
|
40
|
+
reasoningEffort: ReasoningEffort
|
|
41
|
+
usage: TokenUsage | null
|
|
42
|
+
cost: Cost | null
|
|
43
|
+
contextUsage: ContextUsage | null
|
|
44
|
+
status: TurnStatus
|
|
45
|
+
error: string | null
|
|
46
|
+
tools: ToolItem[]
|
|
47
|
+
exhausted: boolean
|
|
48
|
+
search: SearchState
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface SearchState {
|
|
52
|
+
status: 'idle' | 'searching' | 'complete' | 'failed'
|
|
53
|
+
query: string | null
|
|
54
|
+
provider: string | null
|
|
55
|
+
hits: SearchHit[]
|
|
56
|
+
error: string | null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface RunBase {
|
|
60
|
+
purpose: string
|
|
61
|
+
scope: string | null
|
|
62
|
+
language: string
|
|
63
|
+
model: ModelRef | null
|
|
64
|
+
skills: SkillRef[]
|
|
65
|
+
context: ContextRef | null
|
|
66
|
+
reasoningEffort?: ReasoningEffort
|
|
67
|
+
generateImage?: boolean
|
|
68
|
+
permissionMode?: PermissionMode
|
|
69
|
+
}
|
|
@@ -1,86 +1,24 @@
|
|
|
1
1
|
// One conversation in the panel: turns, the active run and its abort (docs/16 §4.2, §7 trap 4).
|
|
2
2
|
import { SvelteMap } from 'svelte/reactivity'
|
|
3
|
-
import {
|
|
3
|
+
import type { ChatTurn, RunBase, SearchState, ToolItem } from './run-session-types'
|
|
4
|
+
import { cumulativeCost, cumulativeUsage, fromHistory } from './run-session-history'
|
|
4
5
|
import type {
|
|
5
|
-
RunClient,
|
|
6
6
|
ConversationClient,
|
|
7
|
-
|
|
7
|
+
ContextUsage,
|
|
8
|
+
Cost,
|
|
8
9
|
ImagePart,
|
|
9
10
|
ImageRef,
|
|
10
11
|
KitError,
|
|
11
12
|
ModelRef,
|
|
13
|
+
RunClient,
|
|
12
14
|
RunEvent,
|
|
13
15
|
RunRequest,
|
|
14
|
-
|
|
15
|
-
SkillRef,
|
|
16
|
-
ToolOutcome,
|
|
16
|
+
TokenUsage,
|
|
17
17
|
Turn,
|
|
18
18
|
TurnStatus,
|
|
19
19
|
} from '../ports'
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
export interface ToolItem {
|
|
23
|
-
callId: string
|
|
24
|
-
name: string
|
|
25
|
-
/** MCP server name; absent for host tools. */
|
|
26
|
-
server: string | null
|
|
27
|
-
/** Arguments preview while the call waits for the user. */
|
|
28
|
-
pending: string | null
|
|
29
|
-
outcome: ToolOutcome | null
|
|
30
|
-
summary: string
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
import type { ContextUsage, Cost, TokenUsage, ReasoningEffort, PermissionMode } from '../ports'
|
|
34
|
-
|
|
35
|
-
export interface ChatTurn {
|
|
36
|
-
id: string
|
|
37
|
-
persisted: boolean
|
|
38
|
-
parentId: string | null
|
|
39
|
-
questionId: string
|
|
40
|
-
supersedesId: string | null
|
|
41
|
-
kind: 'reply' | 'translation'
|
|
42
|
-
endpointId: string | null
|
|
43
|
-
modelId: string | null
|
|
44
|
-
input: string
|
|
45
|
-
images: ImageRef[]
|
|
46
|
-
/** Images `generate_image` produced in this reply (docs/20). */
|
|
47
|
-
generated: ImageRef[]
|
|
48
|
-
text: string
|
|
49
|
-
reasoningText: string
|
|
50
|
-
reasoningEffort: ReasoningEffort
|
|
51
|
-
usage: TokenUsage | null
|
|
52
|
-
/** Priced at run time; later price changes never recompute it (docs/41 §3.7). */
|
|
53
|
-
cost: Cost | null
|
|
54
|
-
contextUsage: ContextUsage | null
|
|
55
|
-
status: TurnStatus
|
|
56
|
-
error: string | null
|
|
57
|
-
tools: ToolItem[]
|
|
58
|
-
exhausted: boolean
|
|
59
|
-
search: SearchState
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export interface SearchState {
|
|
63
|
-
status: 'idle' | 'searching' | 'complete' | 'failed'
|
|
64
|
-
query: string | null
|
|
65
|
-
provider: string | null
|
|
66
|
-
hits: SearchHit[]
|
|
67
|
-
error: string | null
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export interface RunBase {
|
|
71
|
-
purpose: string
|
|
72
|
-
scope: string | null
|
|
73
|
-
language: string
|
|
74
|
-
model: ModelRef | null
|
|
75
|
-
skills: SkillRef[]
|
|
76
|
-
context: ContextRef | null
|
|
77
|
-
reasoningEffort?: ReasoningEffort
|
|
78
|
-
/** Offer the `generate_image` tool (docs/20). */
|
|
79
|
-
generateImage?: boolean
|
|
80
|
-
/** Permission mode of the run (docs/16 §4.2); omitted is `default`. */
|
|
81
|
-
permissionMode?: PermissionMode
|
|
82
|
-
}
|
|
83
|
-
|
|
20
|
+
import { isKitClientError } from '../ports'
|
|
21
|
+
export type { ChatTurn, RunBase, SearchState, ToolItem } from './run-session-types'
|
|
84
22
|
export class RunSession {
|
|
85
23
|
turns = $state<ChatTurn[]>([])
|
|
86
24
|
conversationId = $state<string | null>(null)
|
|
@@ -89,7 +27,13 @@ export class RunSession {
|
|
|
89
27
|
historyRefreshError = $state<string | null>(null)
|
|
90
28
|
running = $state(false)
|
|
91
29
|
contextUsage = $state<ContextUsage | null>(null)
|
|
92
|
-
search = $state<SearchState>({
|
|
30
|
+
search = $state<SearchState>({
|
|
31
|
+
status: 'idle',
|
|
32
|
+
query: null,
|
|
33
|
+
provider: null,
|
|
34
|
+
hits: [],
|
|
35
|
+
error: null,
|
|
36
|
+
})
|
|
93
37
|
/** Id of a selected skill the backend rejected as `skill_changed`; the user has to confirm it again. */
|
|
94
38
|
skillChanged = $state<string | null>(null)
|
|
95
39
|
#abort: AbortController | null = null
|
|
@@ -108,36 +52,30 @@ export class RunSession {
|
|
|
108
52
|
}
|
|
109
53
|
|
|
110
54
|
get cumulativeUsage(): TokenUsage {
|
|
111
|
-
|
|
112
|
-
inputTokens: null,
|
|
113
|
-
outputTokens: null,
|
|
114
|
-
cacheReadTokens: null,
|
|
115
|
-
cacheWriteTokens: null,
|
|
116
|
-
reasoningTokens: null,
|
|
117
|
-
}
|
|
118
|
-
for (const turn of this.turns)
|
|
119
|
-
for (const key of Object.keys(total) as (keyof TokenUsage)[]) {
|
|
120
|
-
const value = turn.usage?.[key]
|
|
121
|
-
if (value != null) total[key] = (total[key] ?? 0) + value
|
|
122
|
-
}
|
|
123
|
-
return total
|
|
55
|
+
return cumulativeUsage(this.turns)
|
|
124
56
|
}
|
|
125
57
|
|
|
126
58
|
/** Known turn costs summed per currency, same scope as `cumulativeUsage` (no exchange rates). */
|
|
127
59
|
get cumulativeCost(): Cost[] {
|
|
128
|
-
|
|
129
|
-
for (const { cost } of this.turns) {
|
|
130
|
-
if (!cost) continue
|
|
131
|
-
const found = totals.find((item) => item.currency === cost.currency)
|
|
132
|
-
if (found) found.amount += cost.amount
|
|
133
|
-
else totals.push({ ...cost })
|
|
134
|
-
}
|
|
135
|
-
return totals
|
|
60
|
+
return cumulativeCost(this.turns)
|
|
136
61
|
}
|
|
137
62
|
|
|
138
|
-
send(
|
|
63
|
+
send(
|
|
64
|
+
base: RunBase,
|
|
65
|
+
input: string,
|
|
66
|
+
webSearch = false,
|
|
67
|
+
images: ImagePart[] = [],
|
|
68
|
+
): Promise<boolean> {
|
|
139
69
|
if (this.running || input.trim() === '') return Promise.resolve(false)
|
|
140
|
-
return this.#append(
|
|
70
|
+
return this.#append(
|
|
71
|
+
base,
|
|
72
|
+
input,
|
|
73
|
+
this.selectedTurnId,
|
|
74
|
+
null,
|
|
75
|
+
'reply',
|
|
76
|
+
images.length ? { images } : {},
|
|
77
|
+
webSearch,
|
|
78
|
+
)
|
|
141
79
|
}
|
|
142
80
|
|
|
143
81
|
retry(base: RunBase, turnId: string, model?: ModelRef | null): Promise<boolean> {
|
|
@@ -161,7 +99,9 @@ export class RunSession {
|
|
|
161
99
|
const storedSource = this.#stored && source.persisted
|
|
162
100
|
// Without stored history the host cannot read the source images back, so resend the local data URLs.
|
|
163
101
|
const images = source.images.flatMap((image) =>
|
|
164
|
-
image.url?.startsWith('data:')
|
|
102
|
+
image.url?.startsWith('data:')
|
|
103
|
+
? [{ mime: image.mime, data: image.url.slice(image.url.indexOf(',') + 1) }]
|
|
104
|
+
: [],
|
|
165
105
|
)
|
|
166
106
|
return this.#append(
|
|
167
107
|
snapshot,
|
|
@@ -190,7 +130,9 @@ export class RunSession {
|
|
|
190
130
|
reasoningEffort: source.reasoningEffort,
|
|
191
131
|
model:
|
|
192
132
|
base.model ??
|
|
193
|
-
(source.endpointId && source.modelId
|
|
133
|
+
(source.endpointId && source.modelId
|
|
134
|
+
? { endpointId: source.endpointId, modelId: source.modelId }
|
|
135
|
+
: null),
|
|
194
136
|
}
|
|
195
137
|
return this.#append(
|
|
196
138
|
snapshot,
|
|
@@ -198,7 +140,9 @@ export class RunSession {
|
|
|
198
140
|
source.id,
|
|
199
141
|
null,
|
|
200
142
|
'translation',
|
|
201
|
-
this.#stored && source.persisted
|
|
143
|
+
this.#stored && source.persisted
|
|
144
|
+
? { translateTurnId: source.id, targetLanguage: language.trim() }
|
|
145
|
+
: {},
|
|
202
146
|
)
|
|
203
147
|
}
|
|
204
148
|
|
|
@@ -236,7 +180,10 @@ export class RunSession {
|
|
|
236
180
|
modelId: base.model?.modelId ?? null,
|
|
237
181
|
input,
|
|
238
182
|
images:
|
|
239
|
-
action.images?.map((image) => ({
|
|
183
|
+
action.images?.map((image) => ({
|
|
184
|
+
mime: image.mime,
|
|
185
|
+
url: `data:${image.mime};base64,${image.data}`,
|
|
186
|
+
})) ??
|
|
240
187
|
source?.images ??
|
|
241
188
|
[],
|
|
242
189
|
generated: [],
|
|
@@ -262,7 +209,8 @@ export class RunSession {
|
|
|
262
209
|
if (this.#runId && !this.#ended) {
|
|
263
210
|
const abort = this.#abort
|
|
264
211
|
void this.#port.cancel(this.#runId).catch((error: unknown) => {
|
|
265
|
-
if (this.#abort === abort && this.running)
|
|
212
|
+
if (this.#abort === abort && this.running)
|
|
213
|
+
this.actionError = isKitClientError(error) ? error.code : 'internal'
|
|
266
214
|
})
|
|
267
215
|
}
|
|
268
216
|
}
|
|
@@ -291,7 +239,9 @@ export class RunSession {
|
|
|
291
239
|
|
|
292
240
|
show(conversationId: string | null, turns: readonly Turn[] = [], selectedTurnId?: string | null) {
|
|
293
241
|
const previous =
|
|
294
|
-
conversationId === this.conversationId
|
|
242
|
+
conversationId === this.conversationId
|
|
243
|
+
? new SvelteMap(this.turns.map((turn) => [turn.id, turn]))
|
|
244
|
+
: null
|
|
295
245
|
this.stop()
|
|
296
246
|
this.#abort = null
|
|
297
247
|
this.#runId = null
|
|
@@ -306,7 +256,7 @@ export class RunSession {
|
|
|
306
256
|
this.contextUsage = turns.find((turn) => turn.id === this.selectedTurnId)?.contextUsage ?? null
|
|
307
257
|
this.search = { status: 'idle', query: null, provider: null, hits: [], error: null }
|
|
308
258
|
this.turns = turns.map((turn) => {
|
|
309
|
-
const restored =
|
|
259
|
+
const restored = fromHistory(turn)
|
|
310
260
|
const current = previous?.get(turn.id)
|
|
311
261
|
if (
|
|
312
262
|
current &&
|
|
@@ -322,34 +272,12 @@ export class RunSession {
|
|
|
322
272
|
})
|
|
323
273
|
}
|
|
324
274
|
|
|
325
|
-
#
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
supersedesId: turn.supersedesId,
|
|
332
|
-
kind: turn.kind,
|
|
333
|
-
endpointId: turn.endpointId,
|
|
334
|
-
modelId: turn.modelId,
|
|
335
|
-
input: turn.input,
|
|
336
|
-
images: turn.images,
|
|
337
|
-
generated: turn.generated,
|
|
338
|
-
text: turn.text,
|
|
339
|
-
reasoningText: turn.reasoningText ?? '',
|
|
340
|
-
reasoningEffort: turn.reasoningEffort ?? 'default',
|
|
341
|
-
usage: turn.usage ?? null,
|
|
342
|
-
cost: turn.cost ?? null,
|
|
343
|
-
contextUsage: turn.contextUsage ?? null,
|
|
344
|
-
status: turn.status,
|
|
345
|
-
error: turn.errorCode,
|
|
346
|
-
tools: [],
|
|
347
|
-
exhausted: false,
|
|
348
|
-
search: { status: 'idle', query: null, provider: null, hits: [], error: null },
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
async #start(base: RunBase, input: string, action: Partial<RunRequest>, webSearch: boolean): Promise<boolean> {
|
|
275
|
+
async #start(
|
|
276
|
+
base: RunBase,
|
|
277
|
+
input: string,
|
|
278
|
+
action: Partial<RunRequest>,
|
|
279
|
+
webSearch: boolean,
|
|
280
|
+
): Promise<boolean> {
|
|
353
281
|
// The proxied element, so streamed writes are reactive.
|
|
354
282
|
const turn = this.turns[this.turns.length - 1]
|
|
355
283
|
if (!turn) return Promise.resolve(false)
|
|
@@ -381,15 +309,22 @@ export class RunSession {
|
|
|
381
309
|
const current = live.get(stored.id)
|
|
382
310
|
live.delete(stored.id)
|
|
383
311
|
return {
|
|
384
|
-
...
|
|
312
|
+
...fromHistory(stored),
|
|
385
313
|
tools: current?.tools ?? [],
|
|
386
|
-
search: current?.search ?? {
|
|
314
|
+
search: current?.search ?? {
|
|
315
|
+
status: 'idle',
|
|
316
|
+
query: null,
|
|
317
|
+
provider: null,
|
|
318
|
+
hits: [],
|
|
319
|
+
error: null,
|
|
320
|
+
},
|
|
387
321
|
exhausted: current?.exhausted ?? false,
|
|
388
322
|
}
|
|
389
323
|
})
|
|
390
324
|
this.turns.push(...live.values())
|
|
391
325
|
this.selectedTurnId = conversation.summary.selectedTurnId
|
|
392
|
-
this.contextUsage =
|
|
326
|
+
this.contextUsage =
|
|
327
|
+
this.turns.find((item) => item.id === this.selectedTurnId)?.contextUsage ?? null
|
|
393
328
|
}
|
|
394
329
|
} catch (error: unknown) {
|
|
395
330
|
if (this.#abort !== abort) return
|
|
@@ -476,7 +411,8 @@ export class RunSession {
|
|
|
476
411
|
}
|
|
477
412
|
case 'tool_result': {
|
|
478
413
|
const item = tool(event.callId)
|
|
479
|
-
if (item)
|
|
414
|
+
if (item)
|
|
415
|
+
Object.assign(item, { pending: null, outcome: event.outcome, summary: event.summary })
|
|
480
416
|
const painted = (event.payload as { images?: ImageRef[] } | undefined)?.images
|
|
481
417
|
if (event.outcome === 'ok' && Array.isArray(painted)) turn.generated.push(...painted)
|
|
482
418
|
break
|
|
@@ -520,7 +456,8 @@ export class RunSession {
|
|
|
520
456
|
if (!ended) void this.#port.cancel(result.runId).catch(() => undefined)
|
|
521
457
|
return true
|
|
522
458
|
}
|
|
523
|
-
if (abort.signal.aborted && !ended)
|
|
459
|
+
if (abort.signal.aborted && !ended)
|
|
460
|
+
void this.#port.cancel(result.runId).catch(() => undefined)
|
|
524
461
|
accepted = true
|
|
525
462
|
this.#runId = result.runId
|
|
526
463
|
if (result.conversationId) this.conversationId = result.conversationId
|
|
@@ -538,7 +475,8 @@ export class RunSession {
|
|
|
538
475
|
const code = isKitClientError(error) ? error.code : 'internal'
|
|
539
476
|
if (isKitClientError(error)) this.#checkSkill(error)
|
|
540
477
|
finish(code === 'cancelled' ? 'cancelled' : 'failed', code === 'cancelled' ? null : code)
|
|
541
|
-
if ((turn.supersedesId !== null || turn.kind === 'translation') && code !== 'cancelled')
|
|
478
|
+
if ((turn.supersedesId !== null || turn.kind === 'translation') && code !== 'cancelled')
|
|
479
|
+
this.actionError = code
|
|
542
480
|
if (turn.supersedesId !== null || turn.kind === 'translation')
|
|
543
481
|
this.turns = this.turns.filter((item) => item !== turn)
|
|
544
482
|
this.#abort = null
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/svelte'
|
|
2
|
+
import type { ComponentProps } from 'svelte'
|
|
3
|
+
import { expect, vi } from 'vitest'
|
|
4
|
+
import { createI18n } from '../../i18n'
|
|
5
|
+
import {
|
|
6
|
+
createFakePorts,
|
|
7
|
+
KitClientError,
|
|
8
|
+
type ConversationClient,
|
|
9
|
+
type Conversation,
|
|
10
|
+
type RunClient,
|
|
11
|
+
type RunEvent,
|
|
12
|
+
type RunRequest,
|
|
13
|
+
type ProvidersPort,
|
|
14
|
+
type SearchPort,
|
|
15
|
+
type SkillsPort,
|
|
16
|
+
} from '../../ports'
|
|
17
|
+
import type ChatPanel from '../ChatPanel.svelte'
|
|
18
|
+
import Harness from '../Harness.test.svelte'
|
|
19
|
+
|
|
20
|
+
export const model = { endpointId: 'e1', modelId: 'm1' }
|
|
21
|
+
|
|
22
|
+
export function required<T>(value: T | null | undefined): T {
|
|
23
|
+
if (value == null) throw new Error('Expected chat fixture or control is missing')
|
|
24
|
+
return value
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface Run {
|
|
28
|
+
request: RunRequest
|
|
29
|
+
turnId: string
|
|
30
|
+
emit: (event: RunEvent) => void
|
|
31
|
+
signal: AbortSignal | undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface Extra {
|
|
35
|
+
props?: Partial<ComponentProps<typeof ChatPanel>>
|
|
36
|
+
history?: Partial<ConversationClient>
|
|
37
|
+
providers?: Partial<ProvidersPort>
|
|
38
|
+
search?: Partial<SearchPort>
|
|
39
|
+
skills?: Partial<SkillsPort>
|
|
40
|
+
chat?: Partial<RunClient>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function setup(
|
|
44
|
+
withHistory: boolean,
|
|
45
|
+
context: { kind: string; id: string; label: string } | null = null,
|
|
46
|
+
extra: Extra = {},
|
|
47
|
+
) {
|
|
48
|
+
const runs: Run[] = []
|
|
49
|
+
const stored: Conversation = {
|
|
50
|
+
summary: {
|
|
51
|
+
id: 'c1',
|
|
52
|
+
title: 'Test',
|
|
53
|
+
scope: null,
|
|
54
|
+
selectedTurnId: null,
|
|
55
|
+
createdAt: 0,
|
|
56
|
+
updatedAt: 0,
|
|
57
|
+
},
|
|
58
|
+
turns: [],
|
|
59
|
+
}
|
|
60
|
+
const approve = vi.fn<(runId: string, callId: string, allow: boolean) => Promise<void>>(() =>
|
|
61
|
+
Promise.resolve(),
|
|
62
|
+
)
|
|
63
|
+
const ports = createFakePorts({
|
|
64
|
+
providers: {
|
|
65
|
+
modelOptions: () =>
|
|
66
|
+
Promise.resolve([
|
|
67
|
+
{
|
|
68
|
+
...model,
|
|
69
|
+
label: 'Local · main · m1',
|
|
70
|
+
modelType: 'chat',
|
|
71
|
+
name: null,
|
|
72
|
+
supportsTools: null,
|
|
73
|
+
reasoning: null,
|
|
74
|
+
nativeWebSearch: false,
|
|
75
|
+
canVision: false,
|
|
76
|
+
},
|
|
77
|
+
]),
|
|
78
|
+
...extra.providers,
|
|
79
|
+
},
|
|
80
|
+
...(extra.search ? { search: extra.search } : {}),
|
|
81
|
+
...(extra.skills ? { skills: extra.skills } : {}),
|
|
82
|
+
chat: {
|
|
83
|
+
start: (request, emit, options) => {
|
|
84
|
+
const id = crypto.randomUUID()
|
|
85
|
+
const source = stored.turns.find(
|
|
86
|
+
(turn) => turn.id === (request.retryTurnId ?? request.translateTurnId),
|
|
87
|
+
)
|
|
88
|
+
const turn: Conversation['turns'][number] = {
|
|
89
|
+
id,
|
|
90
|
+
parentId: request.retryTurnId
|
|
91
|
+
? (source?.parentId ?? null)
|
|
92
|
+
: (request.translateTurnId ?? request.replyToId ?? stored.summary.selectedTurnId),
|
|
93
|
+
questionId: request.retryTurnId ? (source?.questionId ?? id) : id,
|
|
94
|
+
supersedesId: request.retryTurnId ?? null,
|
|
95
|
+
kind: request.translateTurnId ? 'translation' : 'reply',
|
|
96
|
+
input: source?.input ?? request.input,
|
|
97
|
+
images: source
|
|
98
|
+
? source.images
|
|
99
|
+
: (request.images ?? []).map((image) => ({
|
|
100
|
+
mime: image.mime,
|
|
101
|
+
url: `data:${image.mime};base64,${image.data}`,
|
|
102
|
+
})),
|
|
103
|
+
generated: [],
|
|
104
|
+
text: '',
|
|
105
|
+
status: 'running',
|
|
106
|
+
errorCode: null,
|
|
107
|
+
endpointId: request.model?.endpointId ?? null,
|
|
108
|
+
modelId: request.model?.modelId ?? null,
|
|
109
|
+
reasoningEffort: request.reasoningEffort ?? 'default',
|
|
110
|
+
reasoningText: null,
|
|
111
|
+
}
|
|
112
|
+
stored.turns.push(turn)
|
|
113
|
+
const wrapped = (event: RunEvent) => {
|
|
114
|
+
if (event.kind === 'text_delta') turn.text += event.text
|
|
115
|
+
if (event.kind === 'reasoning_delta')
|
|
116
|
+
turn.reasoningText = (turn.reasoningText ?? '') + event.text
|
|
117
|
+
if (event.kind === 'usage') {
|
|
118
|
+
turn.usage = {
|
|
119
|
+
inputTokens: event.inputTokens,
|
|
120
|
+
outputTokens: event.outputTokens,
|
|
121
|
+
cacheReadTokens: event.cacheReadTokens,
|
|
122
|
+
cacheWriteTokens: event.cacheWriteTokens,
|
|
123
|
+
reasoningTokens: event.reasoningTokens,
|
|
124
|
+
}
|
|
125
|
+
turn.cost = event.cost
|
|
126
|
+
}
|
|
127
|
+
if (event.kind === 'context') turn.contextUsage = event.usage
|
|
128
|
+
if (event.kind === 'end') {
|
|
129
|
+
turn.status = event.status
|
|
130
|
+
turn.errorCode = event.error?.code ?? null
|
|
131
|
+
if (event.status === 'completed' && !request.retryTurnId)
|
|
132
|
+
stored.summary.selectedTurnId = id
|
|
133
|
+
}
|
|
134
|
+
emit(event)
|
|
135
|
+
}
|
|
136
|
+
runs.push({ request, turnId: id, emit: wrapped, signal: options?.signal })
|
|
137
|
+
options?.signal?.addEventListener('abort', () =>
|
|
138
|
+
wrapped({ kind: 'end', status: 'cancelled' }),
|
|
139
|
+
)
|
|
140
|
+
return Promise.resolve({
|
|
141
|
+
runId: `r${runs.length}`,
|
|
142
|
+
conversationId: withHistory ? (request.conversationId ?? 'c1') : null,
|
|
143
|
+
turnId: withHistory ? id : null,
|
|
144
|
+
})
|
|
145
|
+
},
|
|
146
|
+
approve,
|
|
147
|
+
...extra.chat,
|
|
148
|
+
},
|
|
149
|
+
})
|
|
150
|
+
if (ports.conversations)
|
|
151
|
+
Object.assign(
|
|
152
|
+
ports.conversations,
|
|
153
|
+
{
|
|
154
|
+
get: (id: string) =>
|
|
155
|
+
id === stored.summary.id
|
|
156
|
+
? Promise.resolve(structuredClone(stored))
|
|
157
|
+
: Promise.reject(
|
|
158
|
+
new KitClientError({ code: 'not_found', message: 'conversation not found' }),
|
|
159
|
+
),
|
|
160
|
+
},
|
|
161
|
+
extra.history,
|
|
162
|
+
)
|
|
163
|
+
if (!withHistory) delete ports.conversations
|
|
164
|
+
const notify = { toast: vi.fn(), confirm: vi.fn(() => Promise.resolve(true)) }
|
|
165
|
+
const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }
|
|
166
|
+
const kit = { ports, i18n: createI18n('en'), notify, logger }
|
|
167
|
+
const view = render(Harness, { props: { kit, model, context, ...extra.props } })
|
|
168
|
+
return { runs, view, approve, notify }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function send(text: string) {
|
|
172
|
+
await fireEvent.input(screen.getByRole('textbox', { name: 'Message' }), {
|
|
173
|
+
target: { value: text },
|
|
174
|
+
})
|
|
175
|
+
const button = screen.getByRole('button', { name: 'Send' })
|
|
176
|
+
await waitFor(() => expect(button.hasAttribute('disabled')).toBe(false))
|
|
177
|
+
await fireEvent.click(button)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const SEARCH = 'Search models, optionally with a level'
|
|
181
|
+
|
|
182
|
+
/** The composer model/effort pill; its name carries the current model and effort. */
|
|
183
|
+
export const pickerTrigger = () => screen.findByRole('button', { name: /^Model and reasoning:/ })
|
|
184
|
+
|
|
185
|
+
/** Opens the picker, searches so every group unfolds, picks a row, then closes it like a user. */
|
|
186
|
+
export async function pickModel(search: string, name: RegExp) {
|
|
187
|
+
const trigger = await pickerTrigger()
|
|
188
|
+
if (trigger.getAttribute('aria-expanded') !== 'true') await fireEvent.click(trigger)
|
|
189
|
+
const input = await screen.findByLabelText(SEARCH)
|
|
190
|
+
await fireEvent.input(input, { target: { value: search } })
|
|
191
|
+
await fireEvent.click(await screen.findByRole('option', { name }))
|
|
192
|
+
await fireEvent.keyDown(input, { key: 'Escape' })
|
|
193
|
+
await waitFor(() => expect(screen.queryByLabelText(SEARCH)).toBeNull())
|
|
194
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Conversation } from '../../ports'
|
|
2
|
+
import { model } from './chat-fixture'
|
|
3
|
+
|
|
4
|
+
export function candidates(): Conversation {
|
|
5
|
+
const reply = (id: string, text: string): Conversation['turns'][number] => ({
|
|
6
|
+
id,
|
|
7
|
+
parentId: null,
|
|
8
|
+
questionId: 'q',
|
|
9
|
+
supersedesId: null,
|
|
10
|
+
kind: 'reply',
|
|
11
|
+
...model,
|
|
12
|
+
input: 'Shared question',
|
|
13
|
+
text,
|
|
14
|
+
status: 'completed',
|
|
15
|
+
errorCode: null,
|
|
16
|
+
reasoningEffort: 'default',
|
|
17
|
+
reasoningText: null,
|
|
18
|
+
images: [],
|
|
19
|
+
generated: [],
|
|
20
|
+
})
|
|
21
|
+
return {
|
|
22
|
+
summary: {
|
|
23
|
+
id: 'saved',
|
|
24
|
+
title: 'Candidates',
|
|
25
|
+
scope: null,
|
|
26
|
+
selectedTurnId: 'a',
|
|
27
|
+
createdAt: 0,
|
|
28
|
+
updatedAt: 0,
|
|
29
|
+
},
|
|
30
|
+
turns: [reply('a', 'Original **reply**'), reply('b', 'Alternative reply')],
|
|
31
|
+
}
|
|
32
|
+
}
|