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/ChatPanel.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/ChatPanel.svelte')
| -rw-r--r-- | packages/frontend/src/lib/components/ChatPanel.svelte | 27 |
1 files changed, 23 insertions, 4 deletions
diff --git a/packages/frontend/src/lib/components/ChatPanel.svelte b/packages/frontend/src/lib/components/ChatPanel.svelte index abdec17..ee1b8ef 100644 --- a/packages/frontend/src/lib/components/ChatPanel.svelte +++ b/packages/frontend/src/lib/components/ChatPanel.svelte @@ -10,6 +10,11 @@ let isLoadingMore = $state(false); const renderGroups = $derived(tabStore.activeTab?.renderGroups ?? []); const activeTabId = $derived(tabStore.activeTab?.id); +// Compaction placeholder state for the active tab. `compactingSource` is set on +// a transient placeholder tab while a conversation is being compacted; +// `compactionError` is set if it failed. +const compactingSource = $derived(tabStore.activeTab?.compactingSource ?? null); +const compactionError = $derived(tabStore.activeTab?.compactionError ?? null); // Stable, turn-scoped render keys. A bubble's identity is `${turnId}:${role}:${n}` // (n = its index among same-(turn,role) messages) rather than the underlying @@ -138,14 +143,28 @@ $effect(() => { {#if isLoadingMore} <div class="text-center text-xs text-base-content/40 py-2">Loading earlier messages...</div> {/if} - {#if renderGroups.length === 0} + {#if compactingSource || compactionError} + <div class="flex flex-col items-center justify-center h-full gap-4 px-6 text-center"> + {#if compactionError} + <div class="text-2xl font-semibold text-error">Compaction failed</div> + <div class="text-base text-base-content/70 max-w-md">{compactionError}</div> + <div class="text-sm text-base-content/50">Close this tab to dismiss — your conversation was not changed.</div> + {:else} + <span class="loading loading-spinner loading-lg text-primary"></span> + <div class="text-2xl font-semibold text-base-content">Please wait, compacting conversation…</div> + <div class="text-sm text-base-content/50">You can cancel by closing this tab.</div> + {/if} + </div> + {:else if renderGroups.length === 0} <div class="flex items-center justify-center h-full text-base-content/40 text-sm"> Send a message to start a conversation </div> {/if} - {#each keyedMessages as { m, key } (key)} - <ChatMessageComponent message={m} tabId={activeTabId} /> - {/each} + {#if !compactingSource && !compactionError} + {#each keyedMessages as { m, key } (key)} + <ChatMessageComponent message={m} tabId={activeTabId} /> + {/each} + {/if} </div> <!-- Scroll-to-bottom button --> |
