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:
@@ -29,7 +29,13 @@ import type { ChannelNotice, ChatChannelDeps } from './channel.ts'
|
||||
export interface ResumeControllerDeps extends ChatChannelDeps, ChannelNotice {
|
||||
readonly agent: Agent
|
||||
readonly runtime: TuiRuntime
|
||||
readonly sessionQuery: SessionQueryService | undefined
|
||||
/**
|
||||
* The optional session-query service, re-read at each use. `sessionQuery` is
|
||||
* mounted by an independent plugin, and a flat config tree gives no ordering
|
||||
* guarantee between it and this front door, so a value captured once at
|
||||
* construction can be `undefined` even though the service arrives moments later.
|
||||
*/
|
||||
readonly sessionQuery: (this: void) => SessionQueryService | undefined
|
||||
readonly ui: TUI
|
||||
readonly editor: HintEditor
|
||||
/** Current agent status, re-read at each resume precondition point. */
|
||||
@@ -75,8 +81,9 @@ export function createResumeController(deps: ResumeControllerDeps): ResumeContro
|
||||
}
|
||||
} else {
|
||||
/* v8 ignore next -- caller checks the optional service before mapping records */
|
||||
if (sessionQuery === undefined) throw new Error('session query is unavailable')
|
||||
snapshot = await sessionQuery.readSession(record.header.id)
|
||||
const readQuery = sessionQuery()
|
||||
if (readQuery === undefined) throw new Error('session query is unavailable')
|
||||
snapshot = await readQuery.readSession(record.header.id)
|
||||
}
|
||||
return summarizeResumeCandidate(
|
||||
record,
|
||||
@@ -105,10 +112,11 @@ export function createResumeController(deps: ResumeControllerDeps): ResumeContro
|
||||
*/
|
||||
const preflightResume = async (sessionId: SessionId): Promise<{ id: SessionId; cwd: string }> => {
|
||||
/* v8 ignore next -- only showResume can call this closure, after proving the optional service exists */
|
||||
if (sessionQuery === undefined) throw new Error('Resume is unavailable: session query is not mounted.')
|
||||
const query = sessionQuery()
|
||||
if (query === undefined) throw new Error('Resume is unavailable: session query is not mounted.')
|
||||
const initialStatus = deps.agentStatus()
|
||||
if (initialStatus !== 'idle') throw new Error(`Resume requires an idle agent (status: ${initialStatus}).`)
|
||||
const record = (await sessionQuery.listSessions()).find(candidate => candidate.header.id === sessionId)
|
||||
const record = (await query.listSessions()).find(candidate => candidate.header.id === sessionId)
|
||||
if (record === undefined) throw new Error(`Session "${sessionId}" is no longer available.`)
|
||||
const candidate = await readResumeCandidate(
|
||||
record,
|
||||
@@ -177,13 +185,14 @@ export function createResumeController(deps: ResumeControllerDeps): ResumeContro
|
||||
deps.appendNotice('Resume requires the current turn to finish or be cancelled first.', 'warning')
|
||||
return
|
||||
}
|
||||
if (sessionQuery === undefined) {
|
||||
const listQuery = sessionQuery()
|
||||
if (listQuery === undefined) {
|
||||
deps.appendNotice('Resume is not available: session query is not mounted.', 'warning')
|
||||
return
|
||||
}
|
||||
const scan = ++resumeScan
|
||||
void resumeOverlay?.close()
|
||||
void sessionQuery.listSessions().then(async (records) => {
|
||||
void listQuery.listSessions().then(async (records) => {
|
||||
if (deps.isDisposed() || scan !== resumeScan) return
|
||||
// Every workspace in the store is summarized; the picker owns the
|
||||
// current-workspace/all-workspaces scope split over the whole set.
|
||||
|
||||
@@ -306,7 +306,6 @@ export function createTuiChat(
|
||||
const sessionId = SessionId(config.sessionId ?? 'main')
|
||||
const agent = ctx.agents.get(sessionId)
|
||||
if (agent === undefined) throw new Error(`ui-tui: session "${sessionId}" is not running`)
|
||||
const sessionQuery = ctx.get('sessionQuery')
|
||||
const resolved = resolveTuiConfig(config)
|
||||
const palette = createPalette(resolved.theme.color)
|
||||
const mdTheme = markdownTheme(palette)
|
||||
@@ -853,7 +852,9 @@ export function createTuiChat(
|
||||
resolved,
|
||||
palette,
|
||||
overlayManager,
|
||||
sessionQuery,
|
||||
// Optional and independently mounted: read at each use so config row order
|
||||
// cannot decide whether /resume works.
|
||||
sessionQuery: () => ctx.get('sessionQuery'),
|
||||
ui,
|
||||
editor,
|
||||
appendNotice,
|
||||
|
||||
Reference in New Issue
Block a user