Skip to content

Commit b6efac3

Browse files
cursoragentgaoyu06
andcommitted
Unify agent sessions and redesign composer picker
Co-authored-by: Gao Yu <gaoyu06@users.noreply.github.com>
1 parent 897b0db commit b6efac3

11 files changed

Lines changed: 814 additions & 718 deletions

File tree

src/lib/ChatPane.svelte

Lines changed: 35 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,12 @@
3232
import {
3333
processVideo,
3434
claudeSessions,
35+
git,
3536
gitCheckpointCapture,
3637
gitCheckpointRestore,
3738
type Op
3839
} from '$lib/protocol';
40+
import { buildModelRows } from '$lib/composer/modelRows';
3941
import { dispatch } from '$lib/backends/router';
4042
import { browser } from '$lib/browser.svelte';
4143
import { prefs } from '$lib/prefs.svelte';
@@ -171,19 +173,20 @@
171173
// yet (an optimistic push counts) and not a resumed conversation.
172174
const backendLocked = $derived(!!session.restored || chat.userTurns > 0);
173175
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+
});
187190
188191
// Open the model picker as a popover. If we already have a cached catalog,
189192
// show it instantly and refresh in the background; otherwise fetch first.
@@ -237,7 +240,6 @@
237240
findIdx = 0;
238241
});
239242
240-
const fmtTokens = (n: number) => (n >= 1000 ? `${(n / 1000).toFixed(1)}k` : `${n}`);
241243
const isImage = (p: string) => /\.(png|jpe?g|gif|webp|bmp)$/i.test(p);
242244
const base = (p: string) => p.replace(/\/+$/, '').split('/').pop() || p;
243245
// Engine subagent lifecycle status → localized label (falls back to the raw value).
@@ -277,62 +279,24 @@
277279
return p.items.map((it) => ({ id: it.id, label: it.label, detail: it.detail, active: it.active, command: `/resume ${it.id}`, depth: nil }));
278280
if (p.kind === 'checkpoint')
279281
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+
});
336300
});
337301
338302
// Whether to offer a filter box (history and other long lists).
@@ -696,7 +660,6 @@
696660
});
697661
onDestroy(() => {
698662
if (findDebounce != null) clearTimeout(findDebounce);
699-
clearTimeout(effortTimer);
700663
// Moving the tile to another leaf remounts the pane — stash the draft so
701664
// the composer text survives the drag (pendingFill restores it).
702665
if (input.trim()) chat.pendingFill = input;
@@ -789,17 +752,15 @@
789752
onPick={pickFiles}
790753
onModel={openModelPicker}
791754
onModelSelect={selectRow}
792-
onModelEffort={setEffort}
793755
onModelClose={() => chat.closePicker()}
794756
modelRows={filteredRows}
795-
modelActive={activeModel}
796757
modelTitle={pickerTitle}
797758
modelSearch={showPickerSearch}
798759
{backendLocked}
760+
{gitBranch}
799761
onBackend={(b, acpAgent) => store.switchBackend(session.id, b, acpAgent)}
800762
bind:pickerQuery
801763
bind:pickerSelIdx={selIdx}
802-
onEffort={chooseEffort}
803764
onApproval={setApprovalMode}
804765
/>
805766
</div>

src/lib/CommandPalette.svelte

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
import {
44
Search, Plus, FolderPlus, Cpu, RotateCcw, History, Layers,
55
Gauge, Activity, Stethoscope, GitBranch, GitBranchPlus, Store, Settings as SettingsIcon,
6-
PanelLeft, LayoutGrid, SunMoon, ChevronRight, Wrench, SquareTerminal
6+
PanelLeft, LayoutGrid, SunMoon, ChevronRight, Wrench
77
} from 'lucide-svelte';
88
import type { ChatState } from '$lib/chat.svelte';
9-
import { caps, BACKEND_IDS, BACKEND_LABELS, type BackendCaps, type BackendId } from '$lib/backends';
9+
import { caps, type BackendCaps } from '$lib/backends';
1010
import { focusTrap } from '$lib/focusTrap';
1111
import { t } from '$lib/i18n';
1212
@@ -25,8 +25,7 @@
2525
onOpenPanel,
2626
onToggleSidebar,
2727
onToggleTheme,
28-
onSetup,
29-
onOpenTui
28+
onSetup
3029
}: {
3130
chat: ChatState | undefined;
3231
hasProject: boolean;
@@ -46,8 +45,6 @@
4645
onToggleSidebar: () => void;
4746
onToggleTheme: () => void;
4847
onSetup: () => void;
49-
/** Open a native TUI tile (real interactive CLI in a pty) for a backend. */
50-
onOpenTui: (backend: BackendId) => void;
5148
} = $props();
5249
5350
type Action = {
@@ -100,14 +97,6 @@
10097
keywords: `${t('shell.cmd.openPanelKw')} ${p.key} ${p.label}`,
10198
run: wrap(() => onOpenPanel(p.key))
10299
})),
103-
...BACKEND_IDS.map((b): Action => ({
104-
id: `tui-${b}`,
105-
label: t('shell.cmd.openTui', { name: BACKEND_LABELS[b] }),
106-
hint: t('shell.cmd.openTuiHint'),
107-
icon: SquareTerminal,
108-
keywords: `${t('shell.cmd.openTuiKw')} ${b}`,
109-
run: wrap(() => onOpenTui(b))
110-
})),
111100
{ id: 'settings', label: t('shell.cmd.settings'), keys: '⌘,', icon: SettingsIcon, keywords: t('shell.cmd.settingsKw'), run: wrap(onSettings) },
112101
{ id: 'setup', label: t('shell.cmd.setup'), hint: t('shell.cmd.setupHint'), icon: Wrench, keywords: t('shell.cmd.setupKw'), run: wrap(onSetup) },
113102
{ id: 'sidebar', label: t('shell.cmd.sidebar'), keys: '⌘B', icon: PanelLeft, keywords: t('shell.cmd.sidebarKw'), run: wrap(onToggleSidebar) },

0 commit comments

Comments
 (0)