|
32 | 32 | import { |
33 | 33 | processVideo, |
34 | 34 | claudeSessions, |
| 35 | + git, |
35 | 36 | gitCheckpointCapture, |
36 | 37 | gitCheckpointRestore, |
37 | 38 | type Op |
38 | 39 | } from '$lib/protocol'; |
| 40 | + import { buildModelRows } from '$lib/composer/modelRows'; |
39 | 41 | import { dispatch } from '$lib/backends/router'; |
40 | 42 | import { browser } from '$lib/browser.svelte'; |
41 | 43 | import { prefs } from '$lib/prefs.svelte'; |
|
171 | 173 | // yet (an optimistic push counts) and not a resumed conversation. |
172 | 174 | const backendLocked = $derived(!!session.restored || chat.userTurns > 0); |
173 | 175 |
|
174 | | - // Effort switch is debounced: reflect the pick immediately on the slider |
175 | | - // (optimistic chat.effort) so the handle stays put, but only send the actual |
176 | | - // `/model` command once the user settles — rapid drags/clicks don't race a |
177 | | - // half-dozen switches through the engine. |
178 | | - let effortTimer: ReturnType<typeof setTimeout> | undefined; |
179 | | - function chooseEffort(ef: string) { |
180 | | - chat.effort = ef; |
181 | | - const model = chat.model; |
182 | | - clearTimeout(effortTimer); |
183 | | - effortTimer = setTimeout(() => { |
184 | | - send({ op: 'command', input: `/model ${model} ${ef}` }); |
185 | | - }, 350); |
186 | | - } |
| 176 | + // Current git branch for the composer's footer strip, refetched when the |
| 177 | + // working directory changes. A detached HEAD reads "detached"; a failed |
| 178 | + // probe (not a git repo) hides the chip. |
| 179 | + let gitBranch = $state(''); |
| 180 | + $effect(() => { |
| 181 | + const cwd = project?.path || chat.cwd; |
| 182 | + gitBranch = ''; |
| 183 | + if (!cwd) return; |
| 184 | + git(['branch', '--show-current'], cwd) |
| 185 | + .then((out) => { |
| 186 | + if (cwd === (project?.path || chat.cwd)) gitBranch = out.trim() || 'detached'; |
| 187 | + }) |
| 188 | + .catch(() => {}); |
| 189 | + }); |
187 | 190 |
|
188 | 191 | // Open the model picker as a popover. If we already have a cached catalog, |
189 | 192 | // show it instantly and refresh in the background; otherwise fetch first. |
|
237 | 240 | findIdx = 0; |
238 | 241 | }); |
239 | 242 |
|
240 | | - const fmtTokens = (n: number) => (n >= 1000 ? `${(n / 1000).toFixed(1)}k` : `${n}`); |
241 | 243 | const isImage = (p: string) => /\.(png|jpe?g|gif|webp|bmp)$/i.test(p); |
242 | 244 | const base = (p: string) => p.replace(/\/+$/, '').split('/').pop() || p; |
243 | 245 | // Engine subagent lifecycle status → localized label (falls back to the raw value). |
|
277 | 279 | return p.items.map((it) => ({ id: it.id, label: it.label, detail: it.detail, active: it.active, command: `/resume ${it.id}`, depth: nil })); |
278 | 280 | if (p.kind === 'checkpoint') |
279 | 281 | return p.items.map((it) => ({ id: it.id, label: it.label, detail: it.detail, active: it.active, command: `/rewind ${it.id}`, depth: nil })); |
280 | | - // Model picker. The active provider's rows come from the engine's model_view |
281 | | - // (already filtered — e.g. jucode hides unsupported models — and flagged with |
282 | | - // the active one); other providers come from the client-side config list so |
283 | | - // you can switch to any of them. Same-provider picks use /model (instant); |
284 | | - // cross-provider picks switch via @switch (config rewrite + engine restart). |
285 | | - const cur = chat.provider ?? ''; |
286 | | - // Mirror the engine's jucode allow-list so we don't offer a model it rejects. |
287 | | - const jucodeOk = (n: string) => |
288 | | - ['gpt-5.5', 'gpt-5.4', 'gpt-5.4-mini', 'gpt-5.3-codex', 'gpt-5.2'].includes(n) || n.startsWith('claude-'); |
289 | | - const groups = { |
290 | | - codex: t('shell.modelGroup.codex'), |
291 | | - claude: t('shell.modelGroup.claude'), |
292 | | - jucode: t('shell.modelGroup.jucode'), |
293 | | - byok: t('shell.modelGroup.byok') |
294 | | - }; |
295 | | - const activeGroup = |
296 | | - chat.backendId === 'codex' |
297 | | - ? groups.codex |
298 | | - : chat.backendId === 'claude' |
299 | | - ? groups.claude |
300 | | - : cur === 'jucode' |
301 | | - ? groups.jucode |
302 | | - : groups.byok; |
303 | | - const activeRows = p.models.map((m) => ({ |
304 | | - id: `${cur}::${m.model}`, |
305 | | - label: m.label || m.model, |
306 | | - vendor: m.vendor || m.model, |
307 | | - detail: m.context_window ? `${cur} · ${fmtTokens(m.context_window)}` : cur, |
308 | | - active: m.active, |
309 | | - command: `/model ${m.model}`, |
310 | | - depth: nil, |
311 | | - group: activeGroup |
312 | | - })); |
313 | | - // Provider switching rewrites the native engine's global config and |
314 | | - // restarts it — meaningful for jucode sessions only. Other backends' |
315 | | - // pickers list just their own engine's model_view catalog. |
316 | | - const otherRows = (chat.backendId !== 'jucode' ? [] : providersList) |
317 | | - .filter((pv) => pv.id !== cur) |
318 | | - .flatMap((pv) => |
319 | | - pv.models |
320 | | - .filter((m) => pv.id !== 'jucode' || jucodeOk(m.name)) |
321 | | - .map((m) => ({ |
322 | | - id: `${pv.id}::${m.name}`, |
323 | | - label: m.name, |
324 | | - vendor: m.name, |
325 | | - detail: `${pv.id}${providers.includes(pv.id) ? '' : ` · ${t('shell.notConfigured')}`} · ${fmtTokens(m.context_window ?? 0)}`, |
326 | | - active: false, |
327 | | - command: `@switch ${pv.id} ${m.name}`, |
328 | | - depth: nil, |
329 | | - group: pv.id === 'jucode' ? groups.jucode : groups.byok |
330 | | - })) |
331 | | - ); |
332 | | - const groupOrder = [groups.codex, groups.claude, groups.jucode, groups.byok]; |
333 | | - return [...activeRows, ...otherRows].sort( |
334 | | - (a, b) => groupOrder.indexOf(a.group) - groupOrder.indexOf(b.group) |
335 | | - ); |
| 282 | + // Model picker rows (pure packing in $lib/composer/modelRows): the active |
| 283 | + // provider's models from the engine's model_view plus, for jucode |
| 284 | + // sessions, every other configured provider's catalog — each row carrying |
| 285 | + // its model's reasoning-effort options for the popover's hover chips. |
| 286 | + return buildModelRows({ |
| 287 | + models: p.models, |
| 288 | + backendId: chat.backendId, |
| 289 | + provider: chat.provider ?? '', |
| 290 | + providersList, |
| 291 | + configured: providers, |
| 292 | + groups: { |
| 293 | + codex: t('shell.modelGroup.codex'), |
| 294 | + claude: t('shell.modelGroup.claude'), |
| 295 | + jucode: t('shell.modelGroup.jucode'), |
| 296 | + byok: t('shell.modelGroup.byok') |
| 297 | + }, |
| 298 | + notConfigured: t('shell.notConfigured') |
| 299 | + }); |
336 | 300 | }); |
337 | 301 |
|
338 | 302 | // Whether to offer a filter box (history and other long lists). |
|
696 | 660 | }); |
697 | 661 | onDestroy(() => { |
698 | 662 | if (findDebounce != null) clearTimeout(findDebounce); |
699 | | - clearTimeout(effortTimer); |
700 | 663 | // Moving the tile to another leaf remounts the pane — stash the draft so |
701 | 664 | // the composer text survives the drag (pendingFill restores it). |
702 | 665 | if (input.trim()) chat.pendingFill = input; |
|
789 | 752 | onPick={pickFiles} |
790 | 753 | onModel={openModelPicker} |
791 | 754 | onModelSelect={selectRow} |
792 | | - onModelEffort={setEffort} |
793 | 755 | onModelClose={() => chat.closePicker()} |
794 | 756 | modelRows={filteredRows} |
795 | | - modelActive={activeModel} |
796 | 757 | modelTitle={pickerTitle} |
797 | 758 | modelSearch={showPickerSearch} |
798 | 759 | {backendLocked} |
| 760 | + {gitBranch} |
799 | 761 | onBackend={(b, acpAgent) => store.switchBackend(session.id, b, acpAgent)} |
800 | 762 | bind:pickerQuery |
801 | 763 | bind:pickerSelIdx={selIdx} |
802 | | - onEffort={chooseEffort} |
803 | 764 | onApproval={setApprovalMode} |
804 | 765 | /> |
805 | 766 | </div> |
|
0 commit comments