From 0c7e7ceae36930e87fc30993f18e30cf54888295 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Mon, 22 Jun 2026 15:14:31 +0900 Subject: feat: consume context window + percentage-based compact handoff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Real context window: GET /models now returns modelInfo[model].contextWindow. The Composer uses this instead of the hardcoded MAX_CONTEXT = 1,000,000. Falls back to 1M when modelInfo is absent or the model has no contextWindow. 2. Percentage-based auto-compact: the compact-threshold endpoint is renamed to compact-percent. The CompactionView now shows a percent input (0-100, default 85, 0 = manual) instead of a token count input. Types renamed: CompactThresholdResponse → CompactPercentResponse, SetCompactThresholdRequest → SetCompactPercentRequest. Note: the field name in the backend types is still 'threshold' (not 'percent') — the FE maps between them. Re-mirrored .dispatch/transport-contract.reference.md. 686 tests green. 0 svelte-check errors + warnings. --- src/features/chat/ui/CompactionView.svelte | 101 +++++++++++++++-------------- src/features/chat/ui/Composer.svelte | 10 +-- 2 files changed, 57 insertions(+), 54 deletions(-) (limited to 'src/features/chat/ui') diff --git a/src/features/chat/ui/CompactionView.svelte b/src/features/chat/ui/CompactionView.svelte index ce2a0a0..7bec984 100644 --- a/src/features/chat/ui/CompactionView.svelte +++ b/src/features/chat/ui/CompactionView.svelte @@ -3,54 +3,54 @@ | { readonly ok: true; readonly messagesSummarized: number; readonly messagesKept: number } | { readonly ok: false; readonly error: string }; - export type SaveCompactThresholdResult = - | { readonly ok: true; readonly threshold: number } + export type SaveCompactPercentResult = + | { readonly ok: true; readonly percent: number } | { readonly ok: false; readonly error: string }; let { - threshold, + percent, canCompact, compactNow, - saveThreshold, + savePercent, }: { - /** The conversation's auto-compact threshold, or null when not yet fetched. 0 = disabled. */ - threshold: number | null; + /** The conversation's auto-compact percent (0-100), or null when not yet fetched. 0 = disabled. */ + percent: number | null; /** Whether a real conversation is focused (a draft has nothing to compact). */ canCompact: boolean; compactNow: () => Promise; - saveThreshold: (threshold: number) => Promise; + savePercent: (percent: number) => Promise; } = $props(); - const DEFAULT_THRESHOLD = 350000; + const DEFAULT_PERCENT = 85; let compacting = $state(false); let compactError = $state(null); let compactResult = $state<{ summarized: number; kept: number } | null>(null); - let thresholdInput = $state(""); - let savingThreshold = $state(false); - let thresholdError = $state(null); - let thresholdSaved = $state(false); + let percentInput = $state(""); + let savingPercent = $state(false); + let percentError = $state(null); + let percentSaved = $state(false); // Sync the input from the prop when it changes (focus switch / initial load). - let lastThreshold = $state(null); + let lastPercent = $state(null); $effect(() => { - if (threshold !== lastThreshold) { - lastThreshold = threshold; - thresholdInput = threshold !== null ? String(threshold) : ""; - thresholdError = null; - thresholdSaved = false; + if (percent !== lastPercent) { + lastPercent = percent; + percentInput = percent !== null ? String(percent) : ""; + percentError = null; + percentSaved = false; } }); - const thresholdLabel = $derived( - threshold == null + const percentLabel = $derived( + percent == null ? "Loading…" - : threshold === 0 + : percent === 0 ? "Disabled (manual only)" - : threshold === DEFAULT_THRESHOLD - ? `${threshold.toLocaleString("en-US")} (default)` - : threshold.toLocaleString("en-US"), + : percent === DEFAULT_PERCENT + ? `${percent}% (default)` + : `${percent}%`, ); async function handleCompact() { @@ -68,22 +68,22 @@ } } - async function handleSaveThreshold() { - const value = Number.parseInt(thresholdInput, 10); - if (Number.isNaN(value) || value < 0) { - thresholdError = "Must be a non-negative number"; + async function handleSavePercent() { + const value = Number.parseInt(percentInput, 10); + if (Number.isNaN(value) || value < 0 || value > 100) { + percentError = "Must be 0-100"; return; } - savingThreshold = true; - thresholdError = null; - thresholdSaved = false; - const result = await saveThreshold(value); - savingThreshold = false; + savingPercent = true; + percentError = null; + percentSaved = false; + const result = await savePercent(value); + savingPercent = false; if (result === null) return; if (result.ok) { - thresholdSaved = true; + percentSaved = true; } else { - thresholdError = result.error; + percentError = result.error; } } @@ -120,33 +120,34 @@ {/if} - +
- Auto-compact threshold + Auto-compact percent
- tokens - {#if savingThreshold} + % + {#if savingPercent} {/if}

- Current: {thresholdLabel} + Current: {percentLabel}
- 0 disables auto-compact. Default is {DEFAULT_THRESHOLD.toLocaleString("en-US")}. + 0 disables auto-compact. Default is {DEFAULT_PERCENT}%.

- {#if thresholdError} -

{thresholdError}

- {:else if thresholdSaved} + {#if percentError} +

{percentError}

+ {:else if percentSaved}

Saved.

{/if}
diff --git a/src/features/chat/ui/Composer.svelte b/src/features/chat/ui/Composer.svelte index 7030153..fe9ea94 100644 --- a/src/features/chat/ui/Composer.svelte +++ b/src/features/chat/ui/Composer.svelte @@ -1,9 +1,7 @@