diff options
| author | Adam Malczewski <[email protected]> | 2026-06-03 01:26:16 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-03 01:26:16 +0900 |
| commit | b26821ead97b986f886065b20d3dbde8283daa64 (patch) | |
| tree | 3c41419afb805d88080392e1c97d233af910b79d /packages/frontend/src/lib/components/SettingsPanel.svelte | |
| parent | 4b45d33c256cf580a53054078be6fd7148fa6302 (diff) | |
| download | dispatch-b26821ead97b986f886065b20d3dbde8283daa64.tar.gz dispatch-b26821ead97b986f886065b20d3dbde8283daa64.zip | |
feat(compaction): add UI-driven conversation compaction
Summarize a conversation's older "head" into a structured anchored Markdown
summary while preserving the most recent turns verbatim, shrinking context size
while keeping the information needed to continue coherently. Triggered by a
"Compact conversation" button in Chat Settings (not an agent tool).
Approach informed by OpenCode's session/compaction.ts:
- Ported SUMMARY_TEMPLATE (Goal / Constraints / Progress / Key Decisions /
Next Steps / Critical Context / Relevant Files) and the anchored-summary
buildPrompt (re-summarizes a prior summary when present).
- Ported the TOOL_OUTPUT_MAX_CHARS (2000) cap on tool results in the summary
request.
- Simplified tail selection to a fixed recent-turn count (DEFAULT_TAIL_TURNS=2)
instead of OpenCode's token-budget splitTurn.
core:
- New src/compaction/ module (pure, DB-free): template, prompt builder,
head/tail selection, transcript renderer with tool-output capping, prior
summary extraction. Generic over ChatMessage so callers keep turnId/seq.
- db/chunks.ts: rekeyChunks(from,to) relocates a tab's full history to a
backup tab (reversible — nothing is deleted).
- AgentEvent: compaction-started / -complete / -error variants.
api:
- AgentManager.compactTab(tempTabId, sourceTabId): side-effect-free
resolveConnection() for the compactor model (configured compaction_model_*,
else the source tab's own key+model), one-shot tool-less summary generation
via a transient Agent, then relocate full history to a fresh backup tab and
re-seed the canonical source id with [summary turn + preserved tail]. Source
tab is locked (messages queue) during the run; queue drains afterward.
- Routes: POST /tabs/:id/compact, GET/PUT /tabs/settings/compaction-model.
frontend:
- "Compact conversation" button in ModelSelector (Chat Settings), between
Working Directory and the agent toggle; idle-gated.
- Compaction-model key+model selector in Settings, beside the title model.
- Transient placeholder tab shows a large, non-faded "Please wait, compacting
conversation…" screen; closing it cancels. Source input locked while running.
- Handle compaction-* events: reload compacted source, insert backup tab,
refocus source, discard placeholder.
tests: core compaction unit tests, rekeyChunks DB test, AgentManager.compactTab
orchestration tests, and compaction route tests. All green (713 tests), biome
clean, all typechecks pass, frontend builds.
Diffstat (limited to 'packages/frontend/src/lib/components/SettingsPanel.svelte')
| -rw-r--r-- | packages/frontend/src/lib/components/SettingsPanel.svelte | 89 |
1 files changed, 89 insertions, 0 deletions
diff --git a/packages/frontend/src/lib/components/SettingsPanel.svelte b/packages/frontend/src/lib/components/SettingsPanel.svelte index 7a810d5..8b28957 100644 --- a/packages/frontend/src/lib/components/SettingsPanel.svelte +++ b/packages/frontend/src/lib/components/SettingsPanel.svelte @@ -26,6 +26,10 @@ function selectTheme(theme: Theme): void { let titleKeyId = $state<string | null>(null); let titleModelId = $state<string | null>(null); let availableModels = $state<string[]>([]); +let compactionKeyId = $state<string | null>(null); +let compactionModelId = $state<string | null>(null); +let compactionModels = $state<string[]>([]); +let loadingCompactionModels = $state(false); let loadingModels = $state(false); let autoExpandThinking = $state(appSettings.autoExpandThinking); let localChunkLimit = $state(appSettings.chunkLimit); @@ -132,6 +136,19 @@ async function loadSettings(): Promise<void> { // ignore } try { + const res = await fetch(`${apiBase}/tabs/settings/compaction-model`); + if (res.ok) { + const data = (await res.json()) as { keyId: string | null; modelId: string | null }; + compactionKeyId = data.keyId; + if (compactionKeyId) { + await loadCompactionModelsForKey(compactionKeyId); + } + compactionModelId = data.modelId; + } + } catch { + // ignore + } + try { const res = await fetch(`${apiBase}/tabs/settings/auto-expand-thinking`); if (res.ok) { const data = (await res.json()) as { value: string | null }; @@ -319,6 +336,48 @@ async function onModelChange(e: Event): Promise<void> { saveTitleModel(); } +async function loadCompactionModelsForKey(keyId: string): Promise<void> { + loadingCompactionModels = true; + try { + const res = await fetch(`${apiBase}/models/available?keyId=${encodeURIComponent(keyId)}`); + if (!res.ok) { + compactionModels = []; + return; + } + const data = (await res.json()) as { models: string[] }; + compactionModels = data.models ?? []; + } catch { + compactionModels = []; + } finally { + loadingCompactionModels = false; + } +} + +function saveCompactionModel(): void { + fetch(`${apiBase}/tabs/settings/compaction-model`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ keyId: compactionKeyId, modelId: compactionModelId }), + }).catch(() => {}); +} + +async function onCompactionKeyChange(e: Event): Promise<void> { + const select = e.target as HTMLSelectElement; + compactionKeyId = select.value || null; + compactionModelId = null; + compactionModels = []; + if (compactionKeyId) { + await loadCompactionModelsForKey(compactionKeyId); + } + saveCompactionModel(); +} + +function onCompactionModelChange(e: Event): void { + const select = e.target as HTMLSelectElement; + compactionModelId = select.value || null; + saveCompactionModel(); +} + $effect(() => { void loadSettings(); }); @@ -374,6 +433,36 @@ $effect(() => { <div class="divider my-0"></div> + <p class="text-xs text-base-content/70">Conversation Compaction Model</p> + <p class="text-xs text-base-content/40">Used to summarize a conversation when you compact it. If unset, the tab's own key/model is used.</p> + + <label class="text-xs text-base-content/60"> + Key + <select class="select select-bordered select-sm w-full" onchange={onCompactionKeyChange} value={compactionKeyId ?? ""}> + <option value="">Select a key...</option> + {#each keys as key (key.id)} + <option value={key.id}>{key.id} ({key.provider})</option> + {/each} + </select> + </label> + + <label class="text-xs text-base-content/60"> + Model + <select + class="select select-bordered select-sm w-full" + onchange={onCompactionModelChange} + value={compactionModelId ?? ""} + disabled={!compactionKeyId || loadingCompactionModels} + > + <option value="">{loadingCompactionModels ? "Loading models..." : "Select a model..."}</option> + {#each compactionModels as model (model)} + <option value={model}>{model}</option> + {/each} + </select> + </label> + + <div class="divider my-0"></div> + <p class="text-xs text-base-content/70">Chat</p> <label class="flex items-center gap-2 cursor-pointer"> <input |
