diff --git a/README.md b/README.md index 2704a29..ce639fa 100644 --- a/README.md +++ b/README.md @@ -439,6 +439,7 @@ pnpm tui:fs | `/status` | Show session info | | `/compact` | Compact conversation context | | `/usage` | Show token usage and cost | +| `/context` | Show estimated context-window usage | | `/verbose` | Toggle verbose output mode | | `/pair` | Generate/list/revoke DM pairing codes | | `/fullscreen` | Switch to fullscreen mode | diff --git a/docs/plans/state.json b/docs/plans/state.json index e89ee21..cf341dd 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -3505,7 +3505,7 @@ "status": "completed", "date": "2026-02-16", "updated": "2026-02-17", - "summary": "Implemented proactive context-window management end-to-end: orchestrator now exposes estimated context budget, emits staged context alerts, writes checkpoint summaries to memory near threshold, and can auto-compact proactively. Gateway now emits `context_warning` stream events during `agent.send`, serves `system.contextUsage` snapshots, and dashboard usage UI includes context budget visibility. Added config schema support under `compaction.proactive`, mapped runtime wiring in both WS SessionBridge and channel routing paths, and updated protocol/docs/default config examples with focused tests. Follow-up added `/context` command fast-path visibility, TUI parser/help/autocomplete + handler parity for `/context`, dedicated audit events for proactive checkpoint writes and proactive auto-compaction, and operator/docs references for those events.", + "summary": "Implemented proactive context-window management end-to-end: orchestrator now exposes estimated context budget, emits staged context alerts, writes checkpoint summaries to memory near threshold, and can auto-compact proactively. Gateway now emits `context_warning` stream events during `agent.send`, serves `system.contextUsage` snapshots, and dashboard usage UI includes context budget visibility. Added config schema support under `compaction.proactive`, mapped runtime wiring in both WS SessionBridge and channel routing paths, and updated protocol/docs/default config examples with focused tests. Follow-up added `/context` command fast-path visibility, TUI parser/help/autocomplete + handler parity for `/context`, dedicated audit events for proactive checkpoint writes and proactive auto-compaction, configurable `/context` threshold display wired from runtime `compaction.threshold_pct`, and operator/docs references for those events.", "files_modified": [ "src/context/compaction.ts", "src/backends/native/prompts.ts", @@ -3529,6 +3529,7 @@ "src/frontends/tui/commands.ts", "src/frontends/tui/commands.test.ts", "src/frontends/tui/minimal.ts", + "src/frontends/tui/minimal.test.ts", "src/frontends/tui/components/App.tsx", "src/commands/builtin/index.ts", "src/commands/types.ts", @@ -3543,7 +3544,7 @@ "config/default.yaml", "docs/plans/state.json" ], - "test_status": "pnpm test:run src/backends/native/orchestrator.test.ts src/config/schema.test.ts src/gateway/handlers/agent.test.ts src/gateway/handlers/handlers.test.ts src/gateway/protocol.test.ts src/commands/builtin/index.test.ts src/frontends/tui/commands.test.ts + pnpm typecheck passing" + "test_status": "pnpm test:run src/backends/native/orchestrator.test.ts src/config/schema.test.ts src/gateway/handlers/agent.test.ts src/gateway/handlers/handlers.test.ts src/gateway/protocol.test.ts src/commands/builtin/index.test.ts src/frontends/tui/commands.test.ts src/frontends/tui/minimal.test.ts + pnpm typecheck passing" } }, "overall_progress": { diff --git a/src/cli/tui.ts b/src/cli/tui.ts index 2bdf91f..c9a38e9 100644 --- a/src/cli/tui.ts +++ b/src/cli/tui.ts @@ -242,6 +242,7 @@ export function registerTuiCommand(program: Command): void { agent, hookEngine, modelProviderConfigs, + contextThresholdPct: config.compaction.threshold_pct, onExit: cleanup, }); } else { @@ -257,6 +258,7 @@ export function registerTuiCommand(program: Command): void { pairingManager, localProviders: config.models.local_providers, modelProviderConfigs, + contextThresholdPct: config.compaction.threshold_pct, currentLocalProvider: config.models.local?.provider, onTransfer: (target) => { if (target === 'telegram') { @@ -290,6 +292,7 @@ export function registerTuiCommand(program: Command): void { agent, hookEngine, modelProviderConfigs, + contextThresholdPct: config.compaction.threshold_pct, onExit: cleanup, }); return; diff --git a/src/frontends/tui/components/App.tsx b/src/frontends/tui/components/App.tsx index e61cbdd..cac6db4 100644 --- a/src/frontends/tui/components/App.tsx +++ b/src/frontends/tui/components/App.tsx @@ -50,6 +50,7 @@ export interface AppProps { agent?: NativeAgent; hookEngine?: HookEngine; modelProviderConfigs?: Partial>; + contextThresholdPct?: number; onExit?: () => void; } @@ -62,6 +63,7 @@ export function App({ agent, hookEngine, modelProviderConfigs, + contextThresholdPct, onExit, }: AppProps): React.ReactElement { const { exit } = useApp(); @@ -247,7 +249,7 @@ export function App({ const modelName = modelRouter?.getLabel(tier) ?? model; const window = getContextWindow(modelName); const usagePct = window > 0 ? (estimated / window) * 100 : 0; - const thresholdPct = 80; + const thresholdPct = contextThresholdPct ?? 80; const thresholdTokens = Math.floor((thresholdPct / 100) * window); const remaining = Math.max(0, window - estimated); const text = [ diff --git a/src/frontends/tui/fullscreen.ts b/src/frontends/tui/fullscreen.ts index 16c408a..88156b2 100644 --- a/src/frontends/tui/fullscreen.ts +++ b/src/frontends/tui/fullscreen.ts @@ -17,6 +17,7 @@ export interface FullscreenTuiConfig { agent?: NativeAgent; hookEngine?: HookEngine; modelProviderConfigs?: Partial>; + contextThresholdPct?: number; onExit?: () => void; } @@ -40,6 +41,7 @@ export async function startFullscreenTui(config: FullscreenTuiConfig): Promise Promise; handleModelCommand: (tier: string, providerModel?: string) => void; + handleContextCommand: () => void; handleEscapeAction: () => boolean; prompt: (text: string) => Promise; rl: { @@ -43,6 +44,7 @@ function minimalTuiPrivates(value: MinimalTui): { return value as unknown as { handleBackendCommand: (provider: string) => Promise; handleModelCommand: (tier: string, providerModel?: string) => void; + handleContextCommand: () => void; handleEscapeAction: () => boolean; prompt: (text: string) => Promise; rl: { @@ -174,6 +176,43 @@ describe('MinimalTui backend command', () => { expect(mockAgent.setModelTier).toHaveBeenCalledWith('local'); }); + it('uses configured compaction threshold in /context output', () => { + const mockSession = { + id: 'test', + getHistory: () => [{ role: 'user', content: 'x'.repeat(400) }], + addMessage: vi.fn(), + clear: vi.fn(), + replaceHistory: vi.fn(), + }; + + const mockRouter: TuiRouterStub = { + getTier: () => 'default' as const, + getAvailableTiers: () => ['default'], + setTier: vi.fn(() => true), + getLabel: () => 'gpt-4o', + getLocalProviderName: () => 'ollama', + setLocalClient: vi.fn(), + chat: vi.fn(), + getClient: vi.fn(), + }; + + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + try { + const tui = new MinimalTui({ + session: asSession(mockSession), + modelClient: asRouter(mockRouter), + modelRouter: asRouter(mockRouter), + systemPrompt: 'test', + contextThresholdPct: 67, + }); + + minimalTuiPrivates(tui).handleContextCommand(); + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('compaction threshold: 67%')); + } finally { + logSpy.mockRestore(); + } + }); + it('reuses configured provider credentials for /model ', () => { const prevOpenRouterKey = process.env.OPENROUTER_API_KEY; delete process.env.OPENROUTER_API_KEY; diff --git a/src/frontends/tui/minimal.ts b/src/frontends/tui/minimal.ts index 8afafbe..4bbc8ec 100644 --- a/src/frontends/tui/minimal.ts +++ b/src/frontends/tui/minimal.ts @@ -68,6 +68,7 @@ export interface MinimalTuiConfig { currentLocalProvider?: string; pairingManager?: PairingManager; hookEngine?: HookEngine; + contextThresholdPct?: number; } export class MinimalTui { @@ -394,7 +395,7 @@ export class MinimalTui { const modelName = this.config.modelRouter?.getLabel(tier) ?? 'unknown'; const window = getContextWindow(modelName); const usagePct = window > 0 ? (estimated / window) * 100 : 0; - const thresholdPct = 80; + const thresholdPct = this.config.contextThresholdPct ?? 80; const thresholdTokens = Math.floor((thresholdPct / 100) * window); const remaining = Math.max(0, window - estimated);