refactor(cli)!: one shared base config with per-surface overlays

`dsh` shipped two config trees that were 43 rows the same: apps/cli/cordis.yml
composed web as 74 flat rows, while the TUI booted examples/tui-agent/cordis.yml
whose single `@deepseek-ai/dsh-tui-demo` row mounted twelve plugins behind a
twenty-key pass-through Config. Neither file was what its location claimed —
apps/cli hardcoded the "example" as the product default and the "demo" bundle
was the application — and every capability change had to be made twice.

- apps/cli/base.cordis.yml holds the 43 shared rows; tui.cordis.yml and
  web.cordis.yml are patch lists stating only what differs per surface
- overlays apply as SIBLING patch lists at one include level, because include
  patches never cross an include boundary. Precedence: base < surface <
  (--config | personal ~/.dsh/config.yaml) < launcher flag/profile patches
- `--config` now applies an overlay INSTEAD OF the personal one, so a demo or
  test tree never inherits the user's route; new `--config-replace` boots a file
  as the entire tree (the old `--config` behaviour). Both survive /resume
- vendor/include: index each `insert`ed row as it is added so a later patch can
  configure or disable it. Upstream built the id index once before the patch
  loop, leaving every surface-only row — the whole TUI front door — silently
  unpatchable from user config. Logged as local modification 8
- session identity moves to dsh-agent-loop's CONFIGURED_AGENT_IDENTITIES_KEY;
  dsh-tui's MAIN_SESSION_ID_KEY is deleted (only the bundle read it)
- delete examples/tui-agent, examples/cordis-agent, packages/examples/tui-demo;
  TUI tests → apps/cli/tests, cordis e2e → packages/cordis/tool-cordis/tests,
  examples/code-mode survives as an overlay leaf
- `dsh web` gains --config, threaded into AppCLIEntry as an extra overlay

Three latent defects surfaced and are fixed here: the TUI captured the optional
sessionQuery service once at construction and could permanently disable /resume
when it won the mount race; the session-store root silently reverted to a
project-local ./.sessions; --config-replace was dropped by the resume handoff.

Verified by booting each tree through the real Loader (TUI 55 entries, web 75,
zero unsettled) rather than reading YAML. All eight terminal snapshots replay
byte-identically; 14/14 PTY smoke, 112/112 snapshots, 25/25 doc-sync, hygiene
and lint clean.
This commit is contained in:
Turtle
2026-07-29 13:58:21 +08:00
parent e7c0a5b794
commit f290a8b851
182 changed files with 1659 additions and 2400 deletions

View File

@@ -0,0 +1,171 @@
import type { Context } from 'cordis'
import type {
GenerateOptions,
LlmModelInfo,
LlmResolvedModelInfo,
StreamChunk,
} from '@deepseek-ai/dsh-llm'
import { CallId, LlmAdapter, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
const CONTROL_PROBE = '\u001b]2;MODEL_CONTROLLED\u0007\u001b[999CMODEL_CURSOR\u009b31mMODEL_C1'
const INITIAL_TEXT = `I need one decision before I continue. ${CONTROL_PROBE}`
const FINAL_TEXT = 'Decision received. Scripted TUI run complete.'
const DEFAULT_MODE_PROBE = 'Confirm the scripted run left plan mode.'
const DEFAULT_MODE_TEXT = 'Default mode confirmed.'
// The `skill` scenario types `/skill:scripted-skill`; the manual-invocation front
// door delivers the loaded skill as a user turn wrapped in `<skill name="…">`. The
// body marker below lives in the fixture skill, so echoing it back proves the whole
// block (name attribute plus body) reached the model, not just the command text.
const SKILL_BLOCK_OPEN = '<skill name="scripted-skill">'
const SKILL_BODY_MARKER = 'SCRIPTED SKILL BODY MARKER'
const SKILL_RECEIVED_TEXT = 'Scripted skill body received.'
const TITLE_TEXT = 'scripted session title'
// The failing-bash scenario proves the terminal card reports a non-zero exit
// exactly once: the model-facing result carries the `[exit code: N]` marker, and
// the card turns it into its own `[exit N]` pill instead of showing both.
const BASH_FAILURE_PROBE = 'Run the failing scripted command.'
const BASH_FAILURE_COMMAND = 'printf "SCRIPTED_BASH_FAILED\\n"; exit 3'
const BASH_FAILURE_TEXT = 'Scripted bash failure observed.'
const BASH_FAILURE_CALL_ID = CallId('call-bash-failure')
function textChunks(text: string): StreamChunk[] {
return [
{ type: 'block-start', index: 0, blockType: 'text' },
...Array.from(text, (char): StreamChunk => ({ type: 'text-delta', index: 0, text: char })),
{ type: 'block-end', index: 0, block: { type: 'text', text } },
{ type: 'usage', usage: { inputTokens: 20, outputTokens: text.length } },
{ type: 'finish', reason: { kind: 'stop' } },
]
}
/** Keyless adapter for the real-PTY TUI tests: the two-step conversation and the `/skill:` round-trip. */
class ScriptedTuiAdapter extends LlmAdapter {
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 resolveModel(
provider: string,
model: string,
): Promise<LlmResolvedModelInfo> {
return Promise.resolve({
provider,
id: model,
name: model === 'tui-scripted-model-pro' ? 'Scripted Pro' : 'Scripted Base',
context: { contextWindow: 128_000 },
...model !== 'tui-scripted-model-pro'
? {}
: {
reasoning: {
efforts: [
{ id: ReasoningEffortId('off'), name: 'Off' },
{ id: ReasoningEffortId('high'), name: 'High' },
{ id: ReasoningEffortId('max'), name: 'Max' },
],
defaultEffort: ReasoningEffortId('high'),
},
},
})
}
override async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
// The session-title provider's auxiliary request carries no tool schemas,
// unlike every agent turn; answer it with a fixed title so the PTY test can
// assert the logged title reaches the terminal window title.
if ((options.tools?.length ?? 0) === 0) {
for (const chunk of textChunks(TITLE_TEXT)) yield chunk
return
}
if (
options.model !== 'tui-scripted-model-pro'
|| !options.system?.includes('tui-scripted-model-pro')
|| options.reasoningEffort !== ReasoningEffortId('max')
) {
throw new Error('the scripted TUI request did not apply the selected model and reasoning effort')
}
const lastMessage = options.messages.at(-1)
// The loop appends plugin-sourced context (the plan-mode notice, the
// tool-skill catalog) AFTER the admitted prompt, so the scripted trigger
// may sit one or more user messages back: scan the whole trailing run of
// user-role messages since the last assistant message.
const trailingUserTexts: string[] = []
for (let index = options.messages.length - 1; index >= 0; index--) {
const message = options.messages[index]
if (message?.role !== 'user') break
for (const block of message.content) {
if (block.type === 'text') trailingUserTexts.push(block.text)
}
}
const lastText = trailingUserTexts.join('\n')
if (lastText.includes(DEFAULT_MODE_PROBE)) {
if (options.system?.includes('Stay in plan mode for this scripted TUI test.')) {
throw new Error('the scripted TUI request retained plan guidance after /plan off')
}
for (const chunk of textChunks(DEFAULT_MODE_TEXT)) yield chunk
return
}
if (lastText.includes(SKILL_BLOCK_OPEN)) {
const ack = lastText.includes(SKILL_BODY_MARKER)
? SKILL_RECEIVED_TEXT
: 'Scripted skill block arrived without its body.'
for (const chunk of textChunks(ack)) yield chunk
return
}
const blocks = lastMessage?.content ?? []
if (blocks.some(block => block.type === 'tool-result')) {
const answered = blocks.some(block => block.type === 'tool-result' && block.toolCallId === BASH_FAILURE_CALL_ID)
for (const chunk of textChunks(answered ? BASH_FAILURE_TEXT : FINAL_TEXT)) yield chunk
return
}
if (lastText.includes(BASH_FAILURE_PROBE)) {
const bashArgs = JSON.stringify({ command: BASH_FAILURE_COMMAND, description: 'Run the failing scripted command' })
yield { type: 'block-start', index: 0, blockType: 'tool-call' }
yield { type: 'tool-call-delta', index: 0, id: BASH_FAILURE_CALL_ID, name: 'bash', argumentsDelta: bashArgs }
yield {
type: 'block-end',
index: 0,
block: { type: 'tool-call', id: BASH_FAILURE_CALL_ID, name: 'bash', arguments: bashArgs },
}
yield { type: 'usage', usage: { inputTokens: 20, outputTokens: 10 } }
yield { type: 'finish', reason: { kind: 'tool-calls' } }
return
}
const args = JSON.stringify({
questions: [{
id: 'mode',
header: 'Execution mode',
question: 'How should the scripted run proceed?',
options: [
{ label: 'Safe', description: 'Use the guarded path.' },
{ label: 'Fast', description: 'Use the shorter path.' },
],
}],
})
const callId = CallId('call-ask-mode')
yield { type: 'block-start', index: 0, blockType: 'text' }
for (const char of INITIAL_TEXT) yield { type: 'text-delta', index: 0, text: char }
yield { type: 'block-end', index: 0, block: { type: 'text', text: INITIAL_TEXT } }
yield { type: 'block-start', index: 1, blockType: 'tool-call' }
yield { type: 'tool-call-delta', index: 1, id: callId, name: 'ask_user_question', argumentsDelta: args }
yield {
type: 'block-end',
index: 1,
block: { type: 'tool-call', id: callId, name: 'ask_user_question', arguments: args },
}
yield { type: 'usage', usage: { inputTokens: 20, outputTokens: 10 } }
yield { type: 'finish', reason: { kind: 'tool-calls' } }
}
}
export const name = 'tui-scripted-llm'
export const inject = ['llm']
/** Register the network-free adapter used by the PTY fixture. */
export function apply(ctx: Context): void {
ctx.llm.registerAdapter(['tui-scripted'], new ScriptedTuiAdapter())
}

View File

@@ -0,0 +1,71 @@
# Overlay for the keyless conversational PTY test: the shipped composition with
# only the model replaced, so the terminal interaction is deterministic and
# network-free while the agent/TUI/user-question stack stays the production one.
#
# Passed as `--config`, so the launcher includes `base.cordis.yml`, applies
# `tui.cordis.yml`, then this file — all sibling patch lists at one include
# level. A patch replaces the targeted row's whole `config`, so each row below
# restates every key it owns.
# The scripted adapter replaces the DeepSeek one: no key, no network. A patch's
# `name` is an assertion rather than a replacement, so the base row is disabled
# and the adapter inserted. Relative specifiers resolve against the INCLUDED
# file's directory (apps/cli), because the include moves baseUrl to that tree.
- id: llm-deepseek
disabled: true
- insert:
- id: scripted-llm
name: './tests/fixtures/tui-scripted-llm.ts'
- id: agent-loop
config:
agents:
- id: main
provider: tui-scripted
model: tui-scripted-model
# `cwd` scopes the session to this workspace, which is what `/resume`
# filters on; dropping it would hide the seeded session.
cwd: !!js process.cwd()
- id: system-prompt
config:
persona: 'Scripted model {{model}}.'
# The smoke's log inspection reads plain `.jsonl` under the workspace, so this
# fixture pins a project-local root instead of the launcher's shared store, and
# keeps the artifacts uncompressed like the other snapshot-facing configs.
- id: session-persistence-jsonl
config:
root: './.sessions'
compression: none
# The derived index must sit under the same root as the logs it indexes; this
# fixture pins both to the workspace instead of the launcher's shared store.
- id: session-query-sqlite
config:
path: './.sessions/session-query.db'
- id: plan-mode
config:
section: 'Stay in plan mode for this scripted TUI test.'
# The scripted adapter answers the tool-less title request with a fixed string,
# so the PTY test can assert the logged title reaches the terminal window title.
- id: session-title-llm
config:
targetWords: 5
targetCjkCharacters: 10
maxInputBytes: 4096
maxOutputTokens: 64
timeoutMs: 10000
- id: tui
config:
sessionId: !!js configuredAgentIdentities?.main?.id ?? 'main'
welcome: 'scripted TUI ready.'
showReasoning: true
# HMR watches the repository; a PTY subprocess test must not start a watcher.
- id: hmr
disabled: true