feat(session-anchors): configurable budget scale + research-backed truncation fix

Remove 500-char assistant text truncation in anchor serializer — research
consensus (JetBrains NeurIPS 2025, SWE-agent, OpenDev ACC) is that agent
reasoning must never be truncated; only tool outputs get observation-masked.

Add AnchorBudgetScale type with 4 presets (Small=2K, Medium=6K, Large=12K,
Full=20K) and per-project range slider in SettingsTab. Remove Ollama-specific
warning toast — budget slider handles context limits generically.
This commit is contained in:
Hibryda 2026-03-11 03:03:53 +01:00
parent 64e040ebfe
commit 0d9c473a06
9 changed files with 104 additions and 23 deletions

View file

@ -9,15 +9,16 @@
removeAnchor,
changeAnchorType,
} from '../../stores/anchors.svelte';
import { DEFAULT_ANCHOR_SETTINGS, MAX_ANCHOR_TOKEN_BUDGET } from '../../types/anchors';
import type { SessionAnchor, AnchorType } from '../../types/anchors';
import { ANCHOR_BUDGET_SCALE_MAP, MAX_ANCHOR_TOKEN_BUDGET } from '../../types/anchors';
import type { SessionAnchor, AnchorType, AnchorBudgetScale } from '../../types/anchors';
interface Props {
sessionId: string | null;
projectId?: string;
anchorBudgetScale?: AnchorBudgetScale;
}
let { sessionId, projectId }: Props = $props();
let { sessionId, projectId, anchorBudgetScale }: Props = $props();
// Reactive session data
let session = $derived(sessionId ? getAgentSession(sessionId) : undefined);
@ -193,7 +194,7 @@
let anchors = $derived(projectId ? getProjectAnchors(projectId) : []);
let injectableAnchors = $derived(projectId ? getInjectableAnchors(projectId) : []);
let anchorTokens = $derived(projectId ? getInjectableTokenCount(projectId) : 0);
let anchorBudget = $derived(DEFAULT_ANCHOR_SETTINGS.anchorTokenBudget);
let anchorBudget = $derived(ANCHOR_BUDGET_SCALE_MAP[anchorBudgetScale ?? 'medium']);
let anchorBudgetPct = $derived(anchorBudget > 0 ? Math.min((anchorTokens / anchorBudget) * 100, 100) : 0);
function anchorTypeLabel(t: AnchorType): string {

View file

@ -157,7 +157,7 @@
<ProjectFiles cwd={project.cwd} projectName={project.name} />
</div>
<div class="content-pane" style:display={activeTab === 'context' ? 'flex' : 'none'}>
<ContextTab sessionId={mainSessionId} projectId={project.id} />
<ContextTab sessionId={mainSessionId} projectId={project.id} anchorBudgetScale={project.anchorBudgetScale} />
</div>
<!-- PERSISTED-LAZY: mount on first activation, then toggle via display -->

View file

@ -20,6 +20,7 @@
import { invoke } from '@tauri-apps/api/core';
import { getProviders } from '../../providers/registry.svelte';
import type { ProviderId, ProviderSettings } from '../../providers/types';
import { ANCHOR_BUDGET_SCALES, ANCHOR_BUDGET_SCALE_LABELS, type AnchorBudgetScale } from '../../types/anchors';
const PROJECT_ICONS = [
'📁', '🚀', '🤖', '🌐', '🔧', '🎮', '📱', '💻',
@ -771,6 +772,27 @@
</div>
{/if}
<div class="card-field">
<span class="card-field-label">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20V10"/><path d="M18 20V4"/><path d="M6 20v-4"/></svg>
Anchor Budget
</span>
<div class="scale-slider">
<input
type="range"
min="0"
max={ANCHOR_BUDGET_SCALES.length - 1}
step="1"
value={ANCHOR_BUDGET_SCALES.indexOf(project.anchorBudgetScale ?? 'medium')}
oninput={(e) => {
const idx = parseInt((e.target as HTMLInputElement).value);
updateProject(activeGroupId, project.id, { anchorBudgetScale: ANCHOR_BUDGET_SCALES[idx] });
}}
/>
<span class="scale-label">{ANCHOR_BUDGET_SCALE_LABELS[project.anchorBudgetScale ?? 'medium']}</span>
</div>
</div>
<div class="card-footer">
<button class="btn-remove" onclick={() => removeProject(activeGroupId, project.id)}>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/></svg>
@ -1203,6 +1225,40 @@
font-family: var(--term-font-family, monospace);
}
/* Anchor budget scale slider */
.scale-slider {
display: flex;
align-items: center;
gap: 0.5rem;
}
.scale-slider input[type="range"] {
flex: 1;
height: 0.25rem;
appearance: none;
background: var(--ctp-surface1);
border-radius: 0.125rem;
outline: none;
cursor: pointer;
}
.scale-slider input[type="range"]::-webkit-slider-thumb {
appearance: none;
width: 0.875rem;
height: 0.875rem;
border-radius: 50%;
background: var(--ctp-blue);
border: 2px solid var(--ctp-base);
cursor: pointer;
}
.scale-label {
font-size: 0.75rem;
color: var(--ctp-subtext0);
white-space: nowrap;
min-width: 5.5em;
}
/* CWD input: left-ellipsis */
.cwd-input {
direction: rtl;