fix(tui): resolve context capacity for active model

This commit is contained in:
Tianyi Cui
2026-07-21 18:45:27 +08:00
parent 78f8a3f5d9
commit 1cee25fdea
4 changed files with 69 additions and 8 deletions
+6 -2
View File
@@ -1,7 +1,7 @@
import { Context } from 'cordis'
import type { Terminal } from '@earendil-works/pi-tui'
import AgentRegistry, { type Agent, type AgentOptions, type AgentStatus } from '@deepseek-ai/dsh-agent'
import type { ContentBlock, LlmModelInfo, LlmProviderInfo } from '@deepseek-ai/dsh-llm'
import type { ContentBlock, LlmModelContext, LlmModelInfo, LlmProviderInfo } from '@deepseek-ai/dsh-llm'
import CommandService from '@deepseek-ai/dsh-commands'
import SessionStore, { SessionId, type Session } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
@@ -31,6 +31,7 @@ export interface TuiHarnessOptions {
providers: LlmProviderInfo[]
models: LlmModelInfo[]
listModels?: (provider: string) => Promise<LlmModelInfo[]>
resolveModelContext?: (provider: string, model: string) => Promise<LlmModelContext | undefined>
}
}
@@ -75,9 +76,12 @@ export async function createTuiTestHarness<TerminalType extends Terminal, Exit e
return catalog.listModels?.(provider)
?? Promise.resolve(catalog.models.filter(model => model.provider === provider).map(model => ({ ...model })))
},
resolveModelContext(provider: string, model: string) {
return catalog.resolveModelContext?.(provider, model)
?? Promise.resolve({ contextWindow: options.contextWindow ?? 128_000 })
},
} as never)
ctx.provide('tokenMeter', {
contextWindow: options.contextWindow ?? 128_000,
measure() {
return { totalTokens: options.contextTokens ?? 0 }
},
+28 -2
View File
@@ -115,7 +115,6 @@ async function dispose(setupResult: Awaited<ReturnType<typeof setup>>): Promise<
function provideTokenMeter(ctx: Context): void {
ctx.provide('tokenMeter', {
contextWindow: 128_000,
measure() {
return { totalTokens: 0 }
},
@@ -537,8 +536,10 @@ describe('pi-tui chat lifecycle and transcript', () => {
})
it('opens a keyboard selector and switches the session model without sending slash text to the agent', async () => {
const initialContext = Promise.withResolvers<{ contextWindow: number }>()
const result = await setup({
agentOptions: { provider: 'alpha', model: 'a1' },
contextTokens: 50,
catalog: {
providers: [{ id: 'alpha', name: 'Alpha' }, { id: 'beta', name: 'Beta' }],
models: [
@@ -547,6 +548,9 @@ describe('pi-tui chat lifecycle and transcript', () => {
{ provider: 'beta', id: 'b1', name: 'Beta One' },
{ provider: 'beta', id: 'shared', name: 'Beta Shared' },
],
resolveModelContext: (provider, model) => provider === 'alpha' && model === 'a1'
? initialContext.promise
: Promise.resolve({ contextWindow: 200 }),
},
})
@@ -574,6 +578,9 @@ describe('pi-tui chat lifecycle and transcript', () => {
expect(result.terminal.output).toContain('Model selected: beta/b1')
expect(result.agent.sent).toEqual([])
expect(result.agent.steered).toEqual([])
initialContext.resolve({ contextWindow: 100 })
await tick()
expect(result.terminal.output).not.toContain('50% context tools:compact b1(reasoning:on)')
result.terminal.send('/model')
result.terminal.send('\r')
@@ -584,7 +591,7 @@ describe('pi-tui chat lifecycle and transcript', () => {
result.agent.status = 'idle'
result.ctx.emit('agent/status', result.agent, 'idle')
await tick()
expect(result.terminal.output).toContain('tools:compact b1(reasoning:on)')
expect(result.terminal.output).toContain('25% context tools:compact b1(reasoning:on)')
const assembly = await result.ctx.systemPrompt.assemble(assembleContextFor(result.agent))
expect(assembly.variables).toMatchObject({ provider: 'beta', model: 'b1' })
@@ -620,6 +627,7 @@ describe('pi-tui chat lifecycle and transcript', () => {
catalog: {
providers: [{ id: 'alpha', name: 'Alpha' }],
models: [{ provider: 'alpha', id: 'a1', name: 'Alpha One' }],
resolveModelContext: () => Promise.resolve(undefined),
},
})
unset.terminal.send('/model')
@@ -628,6 +636,7 @@ describe('pi-tui chat lifecycle and transcript', () => {
unset.terminal.send('\r')
await tick()
expect(unset.terminal.output).toContain('Model selected: alpha/a1')
expect(unset.terminal.output).toContain('context unknown tools:compact a1(reasoning:on)')
await dispose(unset)
const empty = await setup({ agentOptions: {}, catalog: { providers: [], models: [] } })
@@ -649,12 +658,14 @@ describe('pi-tui chat lifecycle and transcript', () => {
providers: [{ id: 'deepseek', name: 'DeepSeek' }],
models: [],
listModels: () => Promise.reject(new Error('catalog offline')),
resolveModelContext: () => Promise.reject(new Error('capacity offline')),
},
})
failed.terminal.send('/model')
failed.terminal.send('\r')
await tick()
expect(failed.terminal.output).toContain('Could not read the model catalog: catalog offline')
expect(failed.terminal.output).toContain('Could not resolve model context: capacity offline')
await dispose(failed)
})
@@ -690,6 +701,21 @@ describe('pi-tui chat lifecycle and transcript', () => {
await tick()
expect(rejectedResult.terminal.output).not.toContain('late catalog failure')
await rejectedResult.ctx.fiber.dispose()
const context = Promise.withResolvers<{ contextWindow: number }>()
const contextResult = await setup({
contextTokens: 99,
catalog: {
providers: [{ id: 'deepseek', name: 'DeepSeek' }],
models: [],
resolveModelContext: () => context.promise,
},
})
await contextResult.controller.dispose()
context.resolve({ contextWindow: 100 })
await tick()
expect(contextResult.terminal.output).not.toContain('99% context')
await contextResult.ctx.fiber.dispose()
})
it('discovers and executes plugin commands, then removes TUI-local commands on disposal', async () => {