@ouraihub/aether-ui 0.1.1 → 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 +2 -2
- package/src/chat/ChatComposer.svelte +47 -5
- package/src/chat/ChatPanel.css +380 -380
- package/src/chat/ChatPanel.svelte +48 -51
- package/src/chat/ChatPanelComposer.svelte +44 -25
- package/src/chat/ChatPanelHeader.svelte +1 -25
- package/src/chat/ChatPanelMode.svelte +3 -3
- package/src/chat/ChatPanelToolbar.svelte +14 -3
- package/src/chat/ContextUsageStatus.svelte +97 -0
- package/src/chat/ModelEffortPicker.svelte +326 -0
- package/src/chat/PickerEffort.svelte +71 -0
- package/src/chat/ReasoningControl.svelte +10 -112
- package/src/chat/ReasoningSlider.svelte +174 -0
- package/src/chat/SkillPicker.svelte +1 -0
- package/src/chat/UsageControl.svelte +12 -5
- package/src/chat/UsageDetails.svelte +19 -3
- package/src/chat/chat-attachments.svelte.ts +9 -5
- package/src/chat/chat-panel-models.svelte.ts +97 -0
- package/src/chat/chat-panel-props.ts +6 -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 +20 -0
- package/src/chat/model-effort.ts +177 -0
- package/src/chat/test-support/chat-fixture.ts +16 -0
- package/src/i18n/en-chat.d.ts +1 -0
- package/src/i18n/en-chat.js +201 -0
- package/src/i18n/en.ts +24 -174
- 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 +2 -158
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
isKitClientError,
|
|
6
6
|
type ModelRef,
|
|
7
7
|
type SkillRef,
|
|
8
|
-
type ModelOption,
|
|
9
8
|
type ReasoningEffort,
|
|
10
9
|
type PermissionMode,
|
|
11
10
|
} from '../ports'
|
|
@@ -21,6 +20,8 @@
|
|
|
21
20
|
import ChatPanelComposer from './ChatPanelComposer.svelte'
|
|
22
21
|
import ChatPanelDialogs from './ChatPanelDialogs.svelte'
|
|
23
22
|
import type { ChatPanelProps } from './chat-panel-props'
|
|
23
|
+
import { ChatPanelModels } from './chat-panel-models.svelte'
|
|
24
|
+
import { modelKey, type PickerValue } from './model-effort'
|
|
24
25
|
|
|
25
26
|
let {
|
|
26
27
|
purpose = 'chat',
|
|
@@ -42,6 +43,9 @@
|
|
|
42
43
|
onToolbarChange,
|
|
43
44
|
reasoningEffort: reasoningEffortProp,
|
|
44
45
|
onReasoningEffortChange,
|
|
46
|
+
defaultModel = null,
|
|
47
|
+
defaultReasoningEffort = 'default',
|
|
48
|
+
onDefaultModelChange,
|
|
45
49
|
permissionModes = ['default', 'plan', 'accept_edits'],
|
|
46
50
|
permissionMode = $bindable('default'),
|
|
47
51
|
}: ChatPanelProps = $props()
|
|
@@ -52,15 +56,18 @@
|
|
|
52
56
|
const session = ports.chat ? new RunSession(ports.chat, history) : null
|
|
53
57
|
// svelte-ignore state_referenced_locally
|
|
54
58
|
let picked = $state<ModelRef | null>(model)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
const errorText = (error: unknown) =>
|
|
60
|
+
t(`errors.${isKitClientError(error) ? error.code : 'internal'}`)
|
|
61
|
+
const models = new ChatPanelModels(ports.providers, i18n, errorText)
|
|
62
|
+
const modelOptions = $derived(models.options)
|
|
63
|
+
const modelError = $derived(models.error)
|
|
58
64
|
const capability = $derived(
|
|
59
65
|
modelOptions?.find(
|
|
60
66
|
(item) => item.endpointId === picked?.endpointId && item.modelId === picked?.modelId,
|
|
61
67
|
) ?? null,
|
|
62
68
|
)
|
|
63
|
-
// The saved choice survives model switches; each model sends its nearest allowed effort
|
|
69
|
+
// The saved choice survives model switches; each model sends its nearest allowed effort
|
|
70
|
+
// (docs/28 RT3).
|
|
64
71
|
// svelte-ignore state_referenced_locally
|
|
65
72
|
let chosenEffort = $state<ReasoningEffort>(reasoningEffortProp ?? 'default')
|
|
66
73
|
const allowedEfforts = $derived(
|
|
@@ -111,29 +118,10 @@
|
|
|
111
118
|
}
|
|
112
119
|
let reasoningOpen = $state(false)
|
|
113
120
|
const noModels = $derived(modelOptions !== null && modelOptions.length === 0)
|
|
114
|
-
const errorText = (error: unknown) =>
|
|
115
|
-
t(`errors.${isKitClientError(error) ? error.code : 'internal'}`)
|
|
116
121
|
$effect(() => {
|
|
117
|
-
|
|
122
|
+
// A provider save swaps options in place: nulling them would reset the chosen effort.
|
|
118
123
|
void providersRevision.value
|
|
119
|
-
|
|
120
|
-
// A provider save swaps options in place: nulling them would reset the chosen effort (docs/41 §3.5).
|
|
121
|
-
modelError = null
|
|
122
|
-
if (!ports.providers) {
|
|
123
|
-
modelOptions = []
|
|
124
|
-
return
|
|
125
|
-
}
|
|
126
|
-
void ports.providers.modelOptions({ modelType: 'chat' }).then(
|
|
127
|
-
(list) => {
|
|
128
|
-
if (alive) modelOptions = list
|
|
129
|
-
},
|
|
130
|
-
(error: unknown) => {
|
|
131
|
-
if (alive) modelError = errorText(error)
|
|
132
|
-
},
|
|
133
|
-
)
|
|
134
|
-
return () => {
|
|
135
|
-
alive = false
|
|
136
|
-
}
|
|
124
|
+
untrack(() => void models.load())
|
|
137
125
|
})
|
|
138
126
|
$effect(() => {
|
|
139
127
|
// A model that turns out not to take tools cannot keep a pending generate_image request.
|
|
@@ -143,6 +131,20 @@
|
|
|
143
131
|
picked = next
|
|
144
132
|
session?.resetContext()
|
|
145
133
|
}
|
|
134
|
+
const pickerValue = $derived(
|
|
135
|
+
picked ? { key: modelKey(picked.endpointId, picked.modelId), effort: reasoningEffort } : null,
|
|
136
|
+
)
|
|
137
|
+
const pickerDefault = $derived(
|
|
138
|
+
defaultModel
|
|
139
|
+
? {
|
|
140
|
+
key: modelKey(defaultModel.endpointId, defaultModel.modelId),
|
|
141
|
+
effort: defaultReasoningEffort,
|
|
142
|
+
}
|
|
143
|
+
: null,
|
|
144
|
+
)
|
|
145
|
+
function pickValue(value: PickerValue) {
|
|
146
|
+
if (!session?.running) models.select(value, picked, chosenEffort, pickModel, chooseEffort)
|
|
147
|
+
}
|
|
146
148
|
|
|
147
149
|
let draft = $state('')
|
|
148
150
|
let draftRevision = 0
|
|
@@ -170,7 +172,8 @@
|
|
|
170
172
|
generateImage: purpose === 'chat' && generateImage && paintingModel,
|
|
171
173
|
context: activeContext ? { kind: activeContext.kind, id: activeContext.id } : null,
|
|
172
174
|
})
|
|
173
|
-
// Consume before awaiting run acceptance, including retries and translations.
|
|
175
|
+
// Consume before awaiting run acceptance, including retries and translations.
|
|
176
|
+
// Never persist full access.
|
|
174
177
|
function consumeBase() {
|
|
175
178
|
const request = { ...base }
|
|
176
179
|
fullAccess = false
|
|
@@ -337,7 +340,11 @@
|
|
|
337
340
|
|
|
338
341
|
{#if session}
|
|
339
342
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions (Esc bubbles from controls) -->
|
|
340
|
-
<section
|
|
343
|
+
<section
|
|
344
|
+
class="flex min-h-0 min-w-0 flex-1 flex-col gap-3 h-full [container-type:size]"
|
|
345
|
+
aria-label={t('chat.title')}
|
|
346
|
+
{onkeydown}
|
|
347
|
+
>
|
|
341
348
|
<ChatSurface
|
|
342
349
|
label={t('chat.title')}
|
|
343
350
|
messagesLabel={t('chat.messages')}
|
|
@@ -354,17 +361,12 @@
|
|
|
354
361
|
historyBusy={historyState.historyBusy}
|
|
355
362
|
historyOpen={historyState.historyOpen}
|
|
356
363
|
historyError={historyState.historyError}
|
|
357
|
-
{modelOptions}
|
|
358
|
-
{modelError}
|
|
359
364
|
onHistory={() => {
|
|
360
365
|
historyState.historyOpen = true
|
|
361
366
|
void historyState.refresh()
|
|
362
367
|
}}
|
|
363
368
|
{onClose}
|
|
364
369
|
onRetryHistory={() => void historyState.retryHistory()}
|
|
365
|
-
{picked}
|
|
366
|
-
onPickModel={pickModel}
|
|
367
|
-
{onOpenSettings}
|
|
368
370
|
/>
|
|
369
371
|
{/snippet}
|
|
370
372
|
<ChatPanelMessages
|
|
@@ -379,7 +381,7 @@
|
|
|
379
381
|
{modelError}
|
|
380
382
|
{capability}
|
|
381
383
|
{noModels}
|
|
382
|
-
onRetryModels={() =>
|
|
384
|
+
onRetryModels={() => void models.load()}
|
|
383
385
|
{onOpenSettings}
|
|
384
386
|
{onInsert}
|
|
385
387
|
{onSaveNote}
|
|
@@ -432,10 +434,18 @@
|
|
|
432
434
|
{mode}
|
|
433
435
|
{allowedModes}
|
|
434
436
|
{chooseMode}
|
|
435
|
-
{reasoningEffort}
|
|
436
|
-
{allowedEfforts}
|
|
437
437
|
bind:reasoningOpen
|
|
438
|
-
{
|
|
438
|
+
pickerModels={models.picker}
|
|
439
|
+
{pickerValue}
|
|
440
|
+
{pickerDefault}
|
|
441
|
+
pickerRefreshing={models.refreshing}
|
|
442
|
+
pickerNotice={models.notice}
|
|
443
|
+
pickerDisabled={modelError !== null}
|
|
444
|
+
onPick={pickValue}
|
|
445
|
+
onSetDefault={onDefaultModelChange
|
|
446
|
+
? (value) => void models.saveDefault(value, onDefaultModelChange)
|
|
447
|
+
: undefined}
|
|
448
|
+
onRefresh={(force) => void models.load(force)}
|
|
439
449
|
{currentContext}
|
|
440
450
|
{picked}
|
|
441
451
|
{onOpenSettings}
|
|
@@ -469,7 +479,7 @@
|
|
|
469
479
|
{noModels}
|
|
470
480
|
validReplyModel={historyState.validReplyModel}
|
|
471
481
|
submitReply={historyState.submitReply}
|
|
472
|
-
reloadModels={() =>
|
|
482
|
+
reloadModels={() => void models.load()}
|
|
473
483
|
bind:historyOpen={historyState.historyOpen}
|
|
474
484
|
bind:renaming={historyState.renaming}
|
|
475
485
|
bind:renameTitle={historyState.renameTitle}
|
|
@@ -481,16 +491,3 @@
|
|
|
481
491
|
/>
|
|
482
492
|
</section>
|
|
483
493
|
{/if}
|
|
484
|
-
|
|
485
|
-
<style>
|
|
486
|
-
.chat-panel {
|
|
487
|
-
display: flex;
|
|
488
|
-
flex: 1;
|
|
489
|
-
flex-direction: column;
|
|
490
|
-
gap: 0.75rem;
|
|
491
|
-
min-width: 0;
|
|
492
|
-
min-height: 0;
|
|
493
|
-
height: 100%;
|
|
494
|
-
container-type: size;
|
|
495
|
-
}
|
|
496
|
-
</style>
|
|
@@ -9,17 +9,17 @@
|
|
|
9
9
|
ModelOption,
|
|
10
10
|
ModelRef,
|
|
11
11
|
PermissionMode,
|
|
12
|
-
ReasoningEffort,
|
|
13
12
|
SkillRef,
|
|
14
13
|
SkillsPort,
|
|
15
14
|
} from '../ports'
|
|
16
|
-
import { settingsTarget } from '../providers/revision.svelte'
|
|
17
15
|
import ChatComposer from './ChatComposer.svelte'
|
|
18
16
|
import ChatPanelMode from './ChatPanelMode.svelte'
|
|
19
17
|
import ChatPanelToolbar from './ChatPanelToolbar.svelte'
|
|
20
|
-
import
|
|
18
|
+
import ModelEffortPicker from './ModelEffortPicker.svelte'
|
|
21
19
|
import SkillPicker from './SkillPicker.svelte'
|
|
22
20
|
import UsageDetails from './UsageDetails.svelte'
|
|
21
|
+
import type { PickerModel, PickerValue } from './model-effort'
|
|
22
|
+
import { settingsTarget } from '../providers/revision.svelte'
|
|
23
23
|
import type { RunSession } from './run-session.svelte'
|
|
24
24
|
|
|
25
25
|
type Attached = ImagePart & { preview: string }
|
|
@@ -62,10 +62,16 @@
|
|
|
62
62
|
mode,
|
|
63
63
|
allowedModes,
|
|
64
64
|
chooseMode,
|
|
65
|
-
reasoningEffort,
|
|
66
|
-
allowedEfforts,
|
|
67
65
|
reasoningOpen = $bindable(false),
|
|
68
|
-
|
|
66
|
+
pickerModels,
|
|
67
|
+
pickerValue,
|
|
68
|
+
pickerDefault,
|
|
69
|
+
pickerRefreshing,
|
|
70
|
+
pickerNotice,
|
|
71
|
+
pickerDisabled,
|
|
72
|
+
onPick,
|
|
73
|
+
onSetDefault,
|
|
74
|
+
onRefresh,
|
|
69
75
|
currentContext,
|
|
70
76
|
picked,
|
|
71
77
|
onOpenSettings,
|
|
@@ -105,10 +111,16 @@
|
|
|
105
111
|
mode: PermissionMode
|
|
106
112
|
allowedModes: PermissionMode[]
|
|
107
113
|
chooseMode: (mode: PermissionMode) => Promise<void>
|
|
108
|
-
reasoningEffort: ReasoningEffort
|
|
109
|
-
allowedEfforts: ReasoningEffort[]
|
|
110
114
|
reasoningOpen?: boolean
|
|
111
|
-
|
|
115
|
+
pickerModels: PickerModel[]
|
|
116
|
+
pickerValue: PickerValue | null
|
|
117
|
+
pickerDefault: PickerValue | null
|
|
118
|
+
pickerRefreshing: boolean
|
|
119
|
+
pickerNotice: string
|
|
120
|
+
pickerDisabled: boolean
|
|
121
|
+
onPick: (value: PickerValue) => void
|
|
122
|
+
onSetDefault?: ((value: PickerValue) => void) | undefined
|
|
123
|
+
onRefresh: (force: boolean) => void
|
|
112
124
|
currentContext: ContextUsage | null
|
|
113
125
|
picked: ModelRef | null
|
|
114
126
|
onOpenSettings?: (() => void) | undefined
|
|
@@ -217,28 +229,35 @@
|
|
|
217
229
|
onresolved={onSkillResolved}
|
|
218
230
|
/>
|
|
219
231
|
</div>{/if}
|
|
232
|
+
<ModelEffortPicker
|
|
233
|
+
models={pickerModels}
|
|
234
|
+
value={pickerValue}
|
|
235
|
+
defaultValue={pickerDefault}
|
|
236
|
+
disabled={session.running || pickerDisabled}
|
|
237
|
+
refreshing={pickerRefreshing}
|
|
238
|
+
notice={pickerNotice}
|
|
239
|
+
{i18n}
|
|
240
|
+
bind:open={reasoningOpen}
|
|
241
|
+
onselect={onPick}
|
|
242
|
+
onsetdefault={onSetDefault}
|
|
243
|
+
onrefresh={onRefresh}
|
|
244
|
+
onopensettings={onOpenSettings && picked
|
|
245
|
+
? () => {
|
|
246
|
+
settingsTarget.value = { endpointId: picked.endpointId, modelId: picked.modelId }
|
|
247
|
+
onOpenSettings()
|
|
248
|
+
}
|
|
249
|
+
: undefined}
|
|
250
|
+
/>
|
|
220
251
|
{/snippet}
|
|
221
252
|
{#snippet actions()}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
<ReasoningControl
|
|
225
|
-
value={reasoningEffort}
|
|
226
|
-
efforts={allowedEfforts}
|
|
227
|
-
disabled={session.running}
|
|
228
|
-
bind:open={reasoningOpen}
|
|
229
|
-
{i18n}
|
|
230
|
-
onchange={chooseEffort}
|
|
231
|
-
onOpenSettings={onOpenSettings
|
|
232
|
-
? () => {
|
|
233
|
-
settingsTarget.value = { endpointId: picked.endpointId, modelId: picked.modelId }
|
|
234
|
-
onOpenSettings()
|
|
235
|
-
}
|
|
236
|
-
: undefined}
|
|
237
|
-
/>
|
|
253
|
+
{#if purpose !== 'chat'}
|
|
254
|
+
<ChatPanelMode {i18n} {mode} {allowedModes} running={session.running} {chooseMode} />
|
|
238
255
|
{/if}
|
|
239
256
|
<UsageDetails
|
|
240
257
|
context={currentContext}
|
|
258
|
+
contextWindow={pickerModels.find((model) => model.key === pickerValue?.key)?.contextWindow}
|
|
241
259
|
modelName={capability?.name ?? capability?.modelId ?? ''}
|
|
260
|
+
turns={session.turns}
|
|
242
261
|
usage={session.turns.at(-1)?.usage ?? null}
|
|
243
262
|
total={session.cumulativeUsage}
|
|
244
263
|
cost={session.turns.at(-1)?.cost ?? null}
|
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
import { Button } from '#lib/components/ui/button/index.js'
|
|
5
5
|
import * as Alert from '#lib/components/ui/alert/index.js'
|
|
6
6
|
import type { KitI18n } from '../i18n'
|
|
7
|
-
import type { ConversationClient
|
|
8
|
-
import ModelSelect from '../providers/ModelSelect.svelte'
|
|
7
|
+
import type { ConversationClient } from '../ports'
|
|
9
8
|
import type { RunSession } from './run-session.svelte'
|
|
10
9
|
|
|
11
10
|
let {
|
|
@@ -16,14 +15,9 @@
|
|
|
16
15
|
historyBusy,
|
|
17
16
|
historyOpen,
|
|
18
17
|
historyError,
|
|
19
|
-
modelOptions,
|
|
20
|
-
modelError,
|
|
21
18
|
onHistory,
|
|
22
19
|
onClose,
|
|
23
20
|
onRetryHistory,
|
|
24
|
-
picked,
|
|
25
|
-
onPickModel,
|
|
26
|
-
onOpenSettings,
|
|
27
21
|
}: {
|
|
28
22
|
i18n: KitI18n
|
|
29
23
|
title: string
|
|
@@ -32,14 +26,9 @@
|
|
|
32
26
|
historyBusy: boolean
|
|
33
27
|
historyOpen: boolean
|
|
34
28
|
historyError: string | null
|
|
35
|
-
modelOptions: ModelOption[] | null
|
|
36
|
-
modelError: string | null
|
|
37
29
|
onHistory: () => void
|
|
38
30
|
onClose?: (() => void) | undefined
|
|
39
31
|
onRetryHistory: () => void
|
|
40
|
-
picked: ModelRef | null
|
|
41
|
-
onPickModel: (model: ModelRef) => void
|
|
42
|
-
onOpenSettings?: (() => void) | undefined
|
|
43
32
|
} = $props()
|
|
44
33
|
const t = $derived(i18n.t)
|
|
45
34
|
</script>
|
|
@@ -63,16 +52,6 @@
|
|
|
63
52
|
onclick={onClose}><XIcon /></Button
|
|
64
53
|
>{/if}
|
|
65
54
|
</div>
|
|
66
|
-
<div class="model-row">
|
|
67
|
-
<ModelSelect
|
|
68
|
-
value={picked}
|
|
69
|
-
options={modelOptions}
|
|
70
|
-
filter={{ modelType: 'chat' }}
|
|
71
|
-
disabled={session.running || modelError !== null}
|
|
72
|
-
onchange={onPickModel}
|
|
73
|
-
{...onOpenSettings ? { onOpenSettings } : {}}
|
|
74
|
-
/>
|
|
75
|
-
</div>
|
|
76
55
|
</header>
|
|
77
56
|
{#if historyError && !historyOpen}
|
|
78
57
|
<Alert.Root variant="destructive">
|
|
@@ -126,7 +105,4 @@
|
|
|
126
105
|
.title-row :global(button) {
|
|
127
106
|
flex-shrink: 0;
|
|
128
107
|
}
|
|
129
|
-
.model-row {
|
|
130
|
-
min-width: 0;
|
|
131
|
-
}
|
|
132
108
|
</style>
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
size="sm"
|
|
39
39
|
class={mode === 'full_access' ? 'mode-danger' : undefined}
|
|
40
40
|
aria-label={`${t('chat.mode.label')}: ${t(`chat.mode.${mode}`)}`}
|
|
41
|
-
><ShieldIcon data-icon="inline-start"
|
|
42
|
-
|
|
43
|
-
/></Button
|
|
41
|
+
><ShieldIcon data-icon="inline-start" /><span data-composer-mode-label>
|
|
42
|
+
{t(`chat.mode.${mode}`)}</span
|
|
43
|
+
><ChevronDownIcon data-composer-mode-label data-icon="inline-end" /></Button
|
|
44
44
|
>{/snippet}
|
|
45
45
|
</DropdownMenu.Trigger>
|
|
46
46
|
<DropdownMenu.Content side="top" align="end" class="mode-menu">
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { tick } from 'svelte'
|
|
2
|
+
import { onMount, tick } from 'svelte'
|
|
3
3
|
import { Popover } from 'bits-ui'
|
|
4
4
|
import PlusIcon from '@lucide/svelte/icons/plus'
|
|
5
5
|
import GlobeIcon from '@lucide/svelte/icons/globe'
|
|
@@ -55,7 +55,17 @@
|
|
|
55
55
|
const DEFAULT_TOOLBAR = ['new-conversation', 'web-search']
|
|
56
56
|
// svelte-ignore state_referenced_locally
|
|
57
57
|
let pinned = $state<string[]>(toolbar ?? DEFAULT_TOOLBAR)
|
|
58
|
-
|
|
58
|
+
let compact = $state(false)
|
|
59
|
+
const visiblePinned = $derived(compact ? [] : pinned.filter((id) => tools.includes(id)))
|
|
60
|
+
onMount(() => {
|
|
61
|
+
const shell = fileInput?.closest('.composer-shell')
|
|
62
|
+
if (!shell) return
|
|
63
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
64
|
+
if (entry && entry.contentRect.width > 0) compact = entry.contentRect.width < 400
|
|
65
|
+
})
|
|
66
|
+
observer.observe(shell)
|
|
67
|
+
return () => observer.disconnect()
|
|
68
|
+
})
|
|
59
69
|
/** Tools usable here; unavailable pinned ids stay stored but hidden (docs/38 §3.2). */
|
|
60
70
|
const tools = $derived(
|
|
61
71
|
['new-conversation', 'web-search', 'attachment', 'generate-image'].filter(
|
|
@@ -286,7 +296,8 @@
|
|
|
286
296
|
</Popover.Content>
|
|
287
297
|
</Popover.Portal>
|
|
288
298
|
</Popover.Root>
|
|
289
|
-
{#if generateImage && paintingModel && purpose === 'chat' && !pinned.includes('generate-image')}
|
|
299
|
+
{#if generateImage && paintingModel && purpose === 'chat' && !pinned.includes('generate-image')}
|
|
300
|
+
<Button
|
|
290
301
|
type="button"
|
|
291
302
|
variant="secondary"
|
|
292
303
|
size="sm"
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Inline context status beside Send (docs/31 §4.3); opens the shared usage popover.
|
|
3
|
+
import type { Snippet } from 'svelte'
|
|
4
|
+
import { cn } from '#lib/utils.js'
|
|
5
|
+
import type { KitI18n } from '../i18n'
|
|
6
|
+
import type { ContextUsage } from '../ports'
|
|
7
|
+
import UsageControl from './UsageControl.svelte'
|
|
8
|
+
import PathIcon from './icons/PathIcon.svelte'
|
|
9
|
+
import {
|
|
10
|
+
detectPlatform,
|
|
11
|
+
pressure,
|
|
12
|
+
spending,
|
|
13
|
+
summary as contextSummary,
|
|
14
|
+
type Platform,
|
|
15
|
+
type SpendingTurn,
|
|
16
|
+
} from './context-status'
|
|
17
|
+
import { applePath, linuxPath, wandPath, windowsPath } from './icons/status-icons'
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
context,
|
|
21
|
+
contextWindow = null,
|
|
22
|
+
turns,
|
|
23
|
+
modelName = '',
|
|
24
|
+
i18n,
|
|
25
|
+
platform = detectPlatform(),
|
|
26
|
+
settings,
|
|
27
|
+
details,
|
|
28
|
+
open = $bindable(false),
|
|
29
|
+
}: {
|
|
30
|
+
context: ContextUsage | null
|
|
31
|
+
/** Fallback capacity, e.g. the selected model's window, when the usage has none. */
|
|
32
|
+
contextWindow?: number | null
|
|
33
|
+
turns: readonly SpendingTurn[]
|
|
34
|
+
modelName?: string
|
|
35
|
+
i18n: KitI18n
|
|
36
|
+
platform?: Platform
|
|
37
|
+
settings?: Snippet | undefined
|
|
38
|
+
details?: Snippet | undefined
|
|
39
|
+
open?: boolean
|
|
40
|
+
} = $props()
|
|
41
|
+
const marks: Record<Platform, { name: string; path: string } | null> = {
|
|
42
|
+
windows: { name: 'Windows', path: windowsPath },
|
|
43
|
+
macos: { name: 'macOS', path: applePath },
|
|
44
|
+
linux: { name: 'Linux', path: linuxPath },
|
|
45
|
+
other: null,
|
|
46
|
+
}
|
|
47
|
+
const capacity = $derived(context?.contextWindow ?? contextWindow)
|
|
48
|
+
const text = $derived(contextSummary(context, capacity))
|
|
49
|
+
const spent = $derived(spending(turns))
|
|
50
|
+
const level = $derived(pressure(context, capacity))
|
|
51
|
+
const mark = $derived(marks[platform])
|
|
52
|
+
const label = $derived(
|
|
53
|
+
i18n
|
|
54
|
+
.t('chat.status.label', { platform: mark?.name ?? '', summary: text, spending: spent })
|
|
55
|
+
.trim(),
|
|
56
|
+
)
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<UsageControl
|
|
60
|
+
bind:open
|
|
61
|
+
{i18n}
|
|
62
|
+
{settings}
|
|
63
|
+
{details}
|
|
64
|
+
summaryLabel={label}
|
|
65
|
+
context={{
|
|
66
|
+
tokens: context?.tokens ?? null,
|
|
67
|
+
contextWindow: capacity,
|
|
68
|
+
modelName,
|
|
69
|
+
source: context?.source ?? 'unknown',
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
{#snippet summary()}
|
|
73
|
+
<span
|
|
74
|
+
class={cn(
|
|
75
|
+
'inline-flex min-w-0 items-center gap-1.5 font-mono text-xs tabular-nums',
|
|
76
|
+
level === 'normal' && 'text-muted-foreground',
|
|
77
|
+
level === 'warning' && 'text-warning',
|
|
78
|
+
level === 'high' &&
|
|
79
|
+
'text-[color-mix(in_oklch,var(--color-warning)_45%,var(--color-destructive))]',
|
|
80
|
+
level === 'critical' && 'text-destructive',
|
|
81
|
+
)}
|
|
82
|
+
data-pressure={level}
|
|
83
|
+
>
|
|
84
|
+
{#if mark}<PathIcon
|
|
85
|
+
data-composer-capacity
|
|
86
|
+
d={mark.path}
|
|
87
|
+
class="size-3 shrink-0 text-foreground"
|
|
88
|
+
/>{/if}
|
|
89
|
+
<span class="shrink-0 whitespace-nowrap"
|
|
90
|
+
>{text.split('/')[0]}<span data-composer-capacity>/{text.split('/')[1]}</span></span
|
|
91
|
+
>
|
|
92
|
+
<PathIcon d={wandPath} class="size-3 shrink-0" />
|
|
93
|
+
<span data-composer-capacity class="mx-1.5 shrink-0 text-border" aria-hidden="true">·</span>
|
|
94
|
+
<span data-composer-capacity class="min-w-0 truncate text-muted-foreground">{spent}</span>
|
|
95
|
+
</span>
|
|
96
|
+
{/snippet}
|
|
97
|
+
</UsageControl>
|