summaryrefslogtreecommitdiffhomepage
path: root/packages/frontend/src/lib/components/SettingsPanel.svelte
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-03 01:26:16 +0900
committerAdam Malczewski <[email protected]>2026-06-03 01:26:16 +0900
commitb26821ead97b986f886065b20d3dbde8283daa64 (patch)
tree3c41419afb805d88080392e1c97d233af910b79d /packages/frontend/src/lib/components/SettingsPanel.svelte
parent4b45d33c256cf580a53054078be6fd7148fa6302 (diff)
downloaddispatch-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.svelte89
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