Merge remote-tracking branch 'origin/master' into worktree/semantic-session-checkpoints

# Conflicts:
#	docs/event-producer-consumer.md
#	docs/module-graph.md
#	examples/tui-agent/tests/tui-keyless-smoke.e2e.ts
#	packages/examples/acp-demo/package.json
#	packages/examples/cli-demo/tsconfig.json
#	packages/support/invariants/src/index.ts
#	packages/support/invariants/tests/invariants.spec.ts
#	pnpm-lock.yaml
This commit is contained in:
Yichen Jiang
2026-07-21 20:09:37 +08:00
634 changed files with 21549 additions and 3328 deletions

View File

@@ -10,7 +10,7 @@ pnpm run demo:tui
The command needs `DEEPSEEK_API_KEY` in the environment or gitignored repository-root `.env`. Set `RESUME_SESSION_ID` to reopen a persisted conversation under `./.sessions`.
The TUI renders Markdown history, reasoning, tool-owned terminal/diff/generic cards, token totals, and the latest todo list. Enter submits or steers while the agent runs; Ctrl+O expands cards, Ctrl+R toggles reasoning, Escape cancels, and `/help` lists commands. `ask_user_question` opens a keyboard-driven overlay.
The TUI renders Markdown history, reasoning, tool-owned terminal/diff/generic cards, token totals, and the latest todo list. Long tool bodies keep a head/tail preview; Ctrl+O expands or collapses every card. Enter submits or steers while the agent runs, Ctrl+R toggles reasoning, Escape cancels, and `/help` lists commands. `/model` opens a keyboard selector for the current provider catalog; use Up/Down and Enter, or `/model <model>` and `/model <provider>/<model>` for direct selection. `ask_user_question` opens a wide bottom-left keyboard panel with batch progress and numbered options.
Run `pnpm run demo:code-mode tui` for the Code Mode overlay.

View File

@@ -19,7 +19,7 @@
welcome: 'TUI Code Mode ready. Give it a multi-tool task.'
ui:
showReasoning: true
maxToolOutputLines: 12
maxToolOutputLines: 6
persona: |
You are a coding agent powered by the {{model}} model.

View File

@@ -31,7 +31,7 @@
welcome: 'TUI agent ready. Give it a coding task.'
ui:
showReasoning: true
maxToolOutputLines: 12
maxToolOutputLines: 6
persona: |
You are a coding agent powered by the {{model}} model.

View File

@@ -1,5 +1,5 @@
import type { Context } from 'cordis'
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, LlmModelContext, LlmModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm'
import { CallId, LlmAdapter } from '@deepseek-ai/dsh-llm'
const CONTROL_PROBE = '\u001b]2;MODEL_CONTROLLED\u0007\u001b[999CMODEL_CURSOR\u009b31mMODEL_C1'
@@ -18,7 +18,21 @@ function textChunks(text: string): StreamChunk[] {
/** Keyless two-step adapter for the real-PTY TUI conversation test. */
class ScriptedTuiAdapter extends LlmAdapter {
async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
override listModels(provider: string): Promise<readonly LlmModelInfo[]> {
return Promise.resolve([
{ provider, id: 'tui-scripted-model', name: 'Scripted Base' },
{ provider, id: 'tui-scripted-model-pro', name: 'Scripted Pro' },
])
}
override resolveModelContext(_provider: string, _model: string): Promise<LlmModelContext> {
return Promise.resolve({ contextWindow: 128_000 })
}
override async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
if (options.model !== 'tui-scripted-model-pro' || !options.system?.includes('tui-scripted-model-pro')) {
throw new Error('the scripted TUI request did not apply the selected model to routing and prompt variables')
}
const hasToolResult = options.messages.at(-1)?.content.some(block => block.type === 'tool-result') ?? false
if (hasToolResult) {
for (const chunk of textChunks(FINAL_TEXT)) yield chunk

View File

@@ -12,6 +12,9 @@
config:
cwd: !!js process.cwd()
- id: token-meter
name: '@deepseek-ai/dsh-token-meter'
- id: tui-agent
name: '@deepseek-ai/dsh-tui-demo'
config:
@@ -21,5 +24,6 @@
workspaceContext:
maxBytes: 65536
welcome: 'scripted TUI ready.'
persona: 'Scripted model {{model}}.'
ui:
showReasoning: true

View File

@@ -67,7 +67,7 @@ buffer
style 1-1 inverse
28| "────────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-99 dim
29| "/workspace/project ↑3.0k ↓115 idle reasoning:on tools:compact"
style 0-58 dim
style 67-99 dim
29| "/tmp/dsh-tui-snapshot-bash-te ↑3.0k ↓115 3% context tools:compact deepseek-v4-flash(reasoning:on)"
style 0-28 dim
style 42-99 dim
30-35| <blank>

View File

@@ -73,7 +73,7 @@ buffer
style 1-1 inverse
30| "────────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-99 dim
31| "/workspace/project ↑3.1k ↓158 idle reasoning:on tools:compact"
style 0-49 dim
style 67-99 dim
31| "/tmp/dsh-tui-snapshot-code-mo ↑3.1k ↓158 3% context tools:compact deepseek-v4-flash(reasoning:on)"
style 0-28 dim
style 42-99 dim
32-35| <blank>

View File

@@ -111,6 +111,6 @@ buffer
style 1-1 inverse
48| "────────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-99 dim
49| "/workspace/project ↑18 ↓18 idle reasoning:on tools:compact"
style 0-61 dim
style 67-99 dim
49| "/tmp/dsh-tui-snapshot-cordis-dyn ↑18 ↓18 7% context tools:compact deepseek-v4-flash(reasoning:on)"
style 0-31 dim
style 42-99 dim

View File

@@ -101,6 +101,6 @@ buffer
style 1-1 inverse
45| "────────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-99 dim
46| "/workspace/project ↑3.5k ↓227 idle reasoning:on tools:compact"
style 0-56 dim
style 67-99 dim
46| "/tmp/dsh-tui-snapshot-dynamic ↑3.5k ↓227 3% context tools:compact deepseek-v4-flash(reasoning:on)"
style 0-28 dim
style 42-99 dim

View File

@@ -64,7 +64,7 @@ buffer
style 1-1 inverse
29| "────────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-99 dim
30| "/workspace/project ↑2.9k ↓41 idle reasoning:on tools:compact"
style 0-62 dim
style 67-99 dim
30| "/tmp/dsh-tui-snapshot-multi-tu ↑2.9k ↓41 3% context tools:compact deepseek-v4-flash(reasoning:on)"
style 0-29 dim
style 42-99 dim
31-35| <blank>

View File

@@ -86,6 +86,6 @@ buffer
style 1-1 inverse
37| "────────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-99 dim
38| "/workspace/project ↑20 ↓6 idle reasoning:on tools:compact"
style 0-55 dim
style 67-99 dim
38| "/tmp/dsh-tui-snapshot-parallel-fi ↑20 ↓6 3% context tools:compact deepseek-v4-flash(reasoning:on)"
style 0-32 dim
style 42-99 dim

View File

@@ -76,6 +76,6 @@ buffer
style 1-1 inverse
34| "────────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-99 dim
35| "/workspace/project ↑3.1k ↓145 idle reasoning:on tools:compact"
style 0-49 dim
style 67-99 dim
35| "/tmp/dsh-tui-snapshot-todo-pl ↑3.1k ↓145 3% context tools:compact deepseek-v4-flash(reasoning:on)"
style 0-28 dim
style 42-99 dim

View File

@@ -24,7 +24,7 @@ describe('tui-agent keyless smoke (real Loader tree in a PTY)', () => {
expect(output).toContain('\u001B[?2004l')
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
it('streams a response, answers a user-question dialog, completes the tool round-trip, and exits cleanly', async () => {
it('switches models, streams a response, answers a user-question dialog, and exits cleanly', async () => {
const output = await runTuiPtySmoke({
label: 'tui-agent conversation',
tempDirPrefix: 'tui-agent-conversation-',
@@ -32,11 +32,13 @@ describe('tui-agent keyless smoke (real Loader tree in a PTY)', () => {
configPath: scriptedConfigPath,
tsconfigPath,
actions: [
{ waitFor: 'scripted TUI ready.', send: 'exercise the TUI\r' },
{ waitFor: 'scripted TUI ready.', send: '/model\r' },
{ waitFor: 'Select model', send: '\x1b[B\r' },
{ waitFor: 'Model selected: tui-scripted/tui-scripted-model-pro.', send: 'exercise the TUI\r' },
// The question text first appears in the streamed tool-call card. Wait
// for the dialog's input legend so Enter cannot arrive before it owns
// terminal input when pre-dispatch policy yields.
{ waitFor: '↑↓ navigate • Enter select', send: '\r' },
{ waitFor: 'Tab custom answer • ↑/↓ navigate • Enter submit • Esc interrupt', send: '\r' },
{ waitFor: 'Decision received. Scripted TUI run complete.', send: '/exit\r' },
],
})

View File

@@ -15,6 +15,7 @@ import * as FsPolicy from '@deepseek-ai/dsh-fs-policy'
import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
import { installLlmReplay, parseSessionLog } from '@deepseek-ai/dsh-llm-replay'
import TokenMeterService from '@deepseek-ai/dsh-token-meter'
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
import { SessionId } from '@deepseek-ai/dsh-session'
import SubagentService from '@deepseek-ai/dsh-subagent'
@@ -32,7 +33,7 @@ import { HeadlessTerminal } from '../../../packages/ui/tui/tests/headless-termin
const SNAPSHOTS_DIR = join(dirname(fileURLToPath(import.meta.url)), 'snapshots')
// Keep pre-normalization layout widths identical across macOS and Linux.
const SNAPSHOT_TMP_ROOT = process.platform === 'win32' ? tmpdir() : '/tmp'
const PROVIDERS = [{ id: 'deepseek', models: [{ id: 'deepseek-v4-flash' }] }]
const PROVIDERS = [{ id: 'deepseek', models: [{ id: 'deepseek-v4-flash', contextWindow: 128_000 }] }]
const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi
type SnapshotMode = 'replay' | 'record' | 'refresh'
@@ -170,6 +171,7 @@ async function mountScenarioContext(
tools: { mode: scenario.composition === 'code' ? 'code' : scenario.composition === 'advanced' ? 'both' : 'native' },
skills: { local: { agentsHome: join(cwd, '.agents') } },
})
await ctx.plugin(TokenMeterService)
await ctx.plugin(LocalBashExecutor, { cwd, timeoutMs: 30_000 })
await ctx.plugin(LocalFileSystem, { cwd: '/' })
await ctx.plugin(FsPolicy)