Merge remote-tracking branch 'origin/master' into xtr/react-loop-simplification

# Conflicts:
#	.agents/notes/archived/feature/2026-07-28-dsh-guided-skill-session-commands.i18n.yaml
#	.agents/notes/archived/manifest.json
#	.agents/notes/implemented/bug-fix/2026-07-29-human-transcript-append-origin.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-05-skill-system.i18n.yaml
#	apps/cli/tests/snapshots/multi-turn-conversation/terminal.expected.txt
#	apps/cli/tests/snapshots/queued-manual-compact/terminal.expected.txt
#	apps/cli/tests/tui-keyless-smoke.e2e.ts
#	apps/cli/tests/tui.snapshot.ts
#	docs/architecture.i18n.yaml
#	docs/architecture.md
#	docs/architecture.zh.md
#	docs/cookbook/extension-cookbook.i18n.yaml
#	docs/event-producer-consumer.md
#	packages/ui/tui/README.i18n.yaml
#	packages/ui/tui/README.md
#	packages/ui/tui/README.zh.md
#	packages/ui/tui/src/index.ts
#	packages/ui/tui/tests/harness.ts
#	packages/ui/tui/tests/session-reference.snapshot.ts
#	packages/ui/tui/tests/snapshots/conversation-streaming.expected.txt
#	packages/ui/tui/tests/snapshots/details-command.expected.txt
#	packages/ui/tui/tests/snapshots/details-selector.expected.txt
#	packages/ui/tui/tests/snapshots/disposed-terminal.expected.txt
#	packages/ui/tui/tests/snapshots/errors-and-help.expected.txt
#	packages/ui/tui/tests/snapshots/status-diagnostics-narrow.expected.txt
#	packages/ui/tui/tests/snapshots/status-diagnostics.expected.txt
#	packages/ui/tui/tests/snapshots/surface-after-compaction-narrow.expected.txt
#	packages/ui/tui/tests/snapshots/surface-after-compaction-wide.expected.txt
#	packages/ui/tui/tests/snapshots/surface-before-compaction.expected.txt
#	packages/ui/tui/tests/snapshots/surface-replayed-compaction.expected.txt
#	packages/ui/tui/tests/snapshots/todo-plan-cleared.expected.txt
#	packages/ui/tui/tests/snapshots/tool-cards-hidden-folded.expected.txt
#	packages/ui/tui/tests/snapshots/untrusted-controls.expected.txt
#	packages/ui/tui/tests/tui.snapshot.ts
#	packages/ui/tui/tests/tui.spec.ts
This commit is contained in:
_Kerman
2026-08-04 14:19:07 +08:00
431 changed files with 1452 additions and 28868 deletions

View File

@@ -61,7 +61,7 @@ function createProgram(): Command {
.option('--base-url <url>')
.option('--api-key <key>')
.option('--model <name>')
.addOption(new Option('--interface <name>').choices(['acp', 'tui', 'embed']))
.addOption(new Option('--interface <name>').choices(['acp', 'embed']))
.addOption(new Option('--pm <name>').choices(['npm', 'pnpm', 'yarn']))
.addOption(new Option('--install').default(undefined))
.addOption(new Option('--no-install').default(undefined))

View File

@@ -169,10 +169,9 @@ const PROJECT_QUESTION_STEPS: readonly WizardStep<ProjectAnswerState>[] = [
message: 'Run interface',
options: [
{ value: 'acp', label: 'ACP automation server' },
{ value: 'tui', label: 'Terminal TUI' },
{ value: 'embed', label: 'Embedded context' },
],
initialValue: 'tui',
initialValue: 'acp',
}),
prefilled: state => state.args.runInterface,
apply: (state, value) => { state.runInterface = value },

View File

@@ -6,7 +6,7 @@ Options:
--base-url <url>
--api-key <key>
--model <name>
--interface <acp|tui|embed>
--interface <acp|embed>
--pm <npm|pnpm|yarn>
--install / --no-install
--config <path>

View File

@@ -179,12 +179,11 @@ describe.skipIf(process.platform === 'win32')('create-sdk terminal contract', ()
"message": "DeepSeek API key",
},
{
"initialValue": "tui",
"initialValue": "acp",
"kind": "select",
"message": "Run interface",
"options": [
"ACP automation server",
"Terminal TUI",
"Embedded context",
],
},

View File

@@ -151,7 +151,7 @@ describe('create arguments', () => {
expect(() => parseCreateArgs(['--link-packages-workspace'])).toThrow("unknown option '--link-packages-workspace'")
expect(parseCreateArgs(['--provider=custom']).provider).toBe('custom')
expect(parseCreateArgs(['--help']).help).toBe(true)
expect(() => parseCreateArgs(['--interface=bad'])).toThrow('Allowed choices are acp, tui, embed')
expect(() => parseCreateArgs(['--interface=bad'])).toThrow('Allowed choices are acp, embed')
expect(() => parseCreateArgs(['--unknown'])).toThrow("unknown option '--unknown'")
expect(() => parseCreateArgs(['one', 'two'])).toThrow('too many arguments')
})
@@ -208,7 +208,7 @@ describe('CreateWizard and scaffolder', () => {
'--provider=deepseek-official',
'--api-key=deepseek-key',
'--model=deepseek-v4-flash',
'--interface=tui',
'--interface=acp',
'--pm=npm',
'--no-install',
'--link-workspace',
@@ -247,7 +247,7 @@ describe('CreateWizard and scaffolder', () => {
const resolved = await new CreateWizard({
args: parseCreateArgs([
'my-agent', '--description=demo', '--provider=deepseek-official', '--api-key=deepseek-key',
'--model=deepseek-v4-flash', '--interface=tui', '--pm=npm', '--no-install',
'--model=deepseek-v4-flash', '--interface=acp', '--pm=npm', '--no-install',
]),
port: new HeadlessPromptPort(),
cwd,
@@ -275,7 +275,7 @@ describe('CreateWizard and scaffolder', () => {
await expect(new CreateWizard({
args: parseCreateArgs([
'my-agent', '--description=demo', '--provider=deepseek-official', '--api-key=k',
'--model=m', '--interface=tui', '--pm=npm', '--no-install',
'--model=m', '--interface=acp', '--pm=npm', '--no-install',
]),
port: new HeadlessPromptPort(),
cwd,

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/sdk/helper/README.md
README.md: 8c6ed9e87be0a97af67849793edb7fa30ffb33ab
README.zh.md: e8844aab14dd0494144251de5e0a9f5cfcaf37be
README.md: 416fcab9815e50ca662333eb6925cc37eb0c41c4
README.zh.md: c50ec7aafa03377bd11759c50eeb2422a12fab68

View File

@@ -8,9 +8,9 @@ The package owns the builtin typed-spec catalog, provider/app behavior entities,
All business and document validation completes before commit writes any affected file. Commit detects external edits made after the session opened, but deliberately provides no cross-file rollback after writing starts.
Builtin features are provider, bash, app, persistence, HMR, filesystem, todo, skill, web, subagent, workflow, compaction, hooks, repeat-tool guard, timeout policy, and ask-user. The catalog owns feature options, required and non-default Cordis plugin config, feature requirements, resource contribution, and round-trip markers; create and config use the same registry and configurator. The ACP app option contributes only the automation bridge; interactive services belong to TUI or Web compositions.
Builtin features are provider, bash, app, persistence, HMR, filesystem, todo, skill, web, subagent, workflow, compaction, hooks, repeat-tool guard, and timeout policy. The catalog owns feature options, required and non-default Cordis plugin config, feature requirements, resource contribution, and round-trip markers; create and config use the same registry and configurator. The ACP app option contributes only the automation bridge; interactive services belong to host compositions.
`SdkProject.open()` requires only readable root `package.json` and `cordis.yml`. A Cordis config entry anchors feature installation; a package present only through a linked NPM dependency closure leaves the feature absent. Once an owned Cordis config entry exists, an incomplete resource shape is `inconsistent` and cannot be modified automatically.
`SdkProject.open()` requires only readable root `package.json` and `cordis.yml`, but rejects a config that references the removed `@deepseek-ai/dsh-tui` root or a subpath. A Cordis config entry anchors feature installation; a package present only through a linked NPM dependency closure leaves the feature absent. Once an owned Cordis config entry exists, an incomplete resource shape is `inconsistent` and cannot be modified automatically.
`.env.example` follows the currently selected features. `.env` is append-only: helper may add a missing differently named variable, but never updates or removes existing content.

View File

@@ -8,9 +8,9 @@
所有业务验证与文档验证都会在提交写入任何受影响文件前完成。提交会检测编辑会话打开后发生的外部修改,但在开始写入后,有意不提供跨文件回滚。
内置功能包括提供方、bash、app、持久化、HMR热模块替换、filesystem、todo、skill技能、web、subagent、工作流、压缩compaction、钩子、repeat-tool guardtimeout policy 和 ask-user。目录负责功能选项、必需和非默认 Cordis 插件配置、功能依赖、资源贡献与往返标记create 与 config 使用同一注册表和配置器。ACPAgent Client Protocol应用选项只贡献自动化桥交互式服务属于 TUI 或 Web 组合。
内置功能包括提供方、bash、app、持久化、HMR热模块替换、filesystem、todo、skill技能、web、subagent、工作流、压缩compaction、钩子、repeat-tool guardtimeout policy。目录负责功能选项、必需和非默认 Cordis 插件配置、功能依赖、资源贡献与往返标记create 与 config 使用同一注册表和配置器。ACPAgent Client Protocol应用选项只贡献自动化桥交互式服务属于宿主组合。
`SdkProject.open()` 只要求根目录下的 `package.json``cordis.yml` 可读。Cordis 配置项用于锚定功能安装;如果某个包只存在于链接的 NPM 依赖闭包中,则该功能仍视为不存在。一旦所属的 Cordis 配置项存在,资源结构不完整就是 `inconsistent`,无法自动修改。
`SdkProject.open()` 只要求根目录下的 `package.json``cordis.yml` 可读,但会拒绝引用已移除的 `@deepseek-ai/dsh-tui` 包根或其子路径的配置。Cordis 配置项用于锚定功能安装;如果某个包只存在于链接的 NPM 依赖闭包中,则该功能仍视为不存在。一旦所属的 Cordis 配置项存在,资源结构不完整就是 `inconsistent`,无法自动修改。
`.env.example` 跟随当前所选功能。`.env` 仅追加helper 可以补充缺失且名称不同的变量,但绝不会更新或删除现有内容。

View File

@@ -4,9 +4,8 @@
* @module @deepseek-ai/dsh-helper/features/builtin/app
*/
import { JsExpression } from '../../documents/cordis-yaml-file.ts'
import { featureId } from '../../ids.ts'
import type { ProjectProfile } from '../../project/types.ts'
import type { ProjectProfile, RunInterface } from '../../project/types.ts'
import {
createAppPackageScripts,
createAppProjectArtifacts,
@@ -18,9 +17,7 @@ import {
} from '../feature.ts'
import { ProjectContribution, type ProjectResource } from '../resources.ts'
import {
cordisConfigEntry,
npmCordisConfigEntry,
optionalString,
ownedTextFile,
packageScript,
requiredString,
@@ -30,10 +27,10 @@ const ID = featureId('app')
function appProjectResources(
profile: ProjectProfile,
runInterface: 'acp' | 'tui' | 'embed',
runInterface: RunInterface,
): readonly ProjectResource[] {
const context = createProjectTemplateContext(profile, runInterface)
const scripts = createAppPackageScripts(context)
const scripts = createAppPackageScripts()
return [
...createAppProjectArtifacts(context).map(document => (
ownedTextFile(ID, document.relativePath, document.serialize())
@@ -44,10 +41,10 @@ function appProjectResources(
}
class AppOption extends FeatureOption {
override readonly id: 'acp' | 'tui' | 'embed'
override readonly id: RunInterface
override readonly label: string
constructor(id: 'acp' | 'tui' | 'embed', label: string) {
constructor(id: RunInterface, label: string) {
super()
this.id = id
this.label = label
@@ -57,7 +54,6 @@ class AppOption extends FeatureOption {
override markerConfigEntries(): readonly { id: string; name: string }[] {
switch (this.id) {
case 'acp': return [{ id: 'acp', name: '@deepseek-ai/dsh-acp' }]
case 'tui': return [{ id: 'tui', name: '@deepseek-ai/dsh-tui' }]
case 'embed': return []
}
}
@@ -66,7 +62,7 @@ class AppOption extends FeatureOption {
override matchesConfigEntries(entries: readonly { id: string; name: string }[], profile: ProjectProfile): boolean {
if (this.id !== 'embed') return super.matchesConfigEntries(entries, profile)
return entries.some(entry => entry.id === 'agent-loop' && entry.name === '@deepseek-ai/dsh-agent-loop')
&& !entries.some(entry => entry.name === '@deepseek-ai/dsh-acp' || entry.name === '@deepseek-ai/dsh-tui')
&& !entries.some(entry => entry.name === '@deepseek-ai/dsh-acp')
}
override contribution(profile: ProjectProfile): ProjectContribution {
@@ -80,36 +76,13 @@ class AppOption extends FeatureOption {
config: { model: profile.runtime.model },
}, ['model'], config => requiredString(config, 'model')),
])
case 'tui':
return new ProjectContribution([
...appProjectResources(profile, this.id),
...npmCordisConfigEntry(ID, {
id: 'user-interaction',
name: '@deepseek-ai/dsh-user-interaction',
}),
cordisConfigEntry(ID, {
id: 'tui-prompt',
name: '@deepseek-ai/dsh-tui/prompt',
}),
...npmCordisConfigEntry(ID, {
id: 'tui',
name: '@deepseek-ai/dsh-tui',
config: {
welcome: 'TUI agent ready. Give it a coding task.',
sessionId: new JsExpression('process.env.DSH_SDK_SESSION_ID'),
},
}, ['welcome', 'sessionId'], config => [
...optionalString(config, 'welcome'),
...config.sessionId instanceof JsExpression ? [] : requiredString(config, 'sessionId'),
]),
])
case 'embed':
return new ProjectContribution(appProjectResources(profile, this.id))
}
}
}
/** Required app selection represented by ACP, TUI, or embed options. */
/** Required app selection represented by ACP or embed options. */
export class AppFeature extends ExclusiveOptionFeature {
override readonly id = ID
override readonly summary = 'Run interface'
@@ -117,7 +90,6 @@ export class AppFeature extends ExclusiveOptionFeature {
override readonly requires = [featureId('spine')]
override readonly options = [
new AppOption('acp', 'ACP automation server'),
new AppOption('tui', 'Terminal TUI'),
new AppOption('embed', 'Embedded context'),
]

View File

@@ -357,21 +357,5 @@ config:
}],
}],
},
{
id: 'ask-user',
summary: 'Ask the user from the model loop',
mode: 'single',
supportedInterfaces: ['tui'],
options: [{
id: 'default',
label: 'ask_user_question tool',
default: true,
resources: [{
kind: 'npm-cordis-config-entry',
id: 'tool-ask-user',
package: '@deepseek-ai/dsh-tool-ask-user',
}],
}],
},
]), profile)
}

View File

@@ -250,7 +250,7 @@ class DefinedFeature extends Feature {
this.required = spec.required ?? false
this.requires = (spec.requires ?? []).map(requirement => featureId(requirement.id))
this.suggests = (spec.suggests ?? []).map(featureId)
this.supportedInterfaces = spec.supportedInterfaces ?? ['acp', 'tui', 'embed']
this.supportedInterfaces = spec.supportedInterfaces ?? ['acp', 'embed']
}
override defaultOptions(): readonly string[] {

View File

@@ -113,7 +113,7 @@ export abstract class Feature {
/** Features recommended during creation. */
readonly suggests: readonly FeatureId[] = []
/** Front doors under which this feature is meaningful. */
readonly supportedInterfaces: readonly RunInterface[] = ['acp', 'tui', 'embed']
readonly supportedInterfaces: readonly RunInterface[] = ['acp', 'embed']
/**
* Options selected when installation has no override.

View File

@@ -90,6 +90,7 @@ export class ProjectEditSession implements FeatureProjectView {
this.profile = source.profile
this.documents = source.cloneDocuments()
for (const feature of registry.all()) {
/* v8 ignore next -- no current builtin is interface-specific after TUI removal */
if (!feature.isApplicable(this.profile)) continue
const installation = feature.inspect(this)
this.states.set(feature.id, {
@@ -507,6 +508,7 @@ export class ProjectEditSession implements FeatureProjectView {
const view = this.projectView(profile)
for (const feature of this.registry.all()) {
const state = this.states.get(feature.id)
/* v8 ignore next 5 -- no current builtin is interface-specific after TUI removal */
if (!feature.isApplicable(profile)) {
if (state?.state === 'enabled') {
throw new Error(`feature ${feature.id} is not available for ${profile.runInterface}`)
@@ -549,7 +551,7 @@ export class ProjectEditSession implements FeatureProjectView {
private finalProfile(): ProjectProfile {
const runInterface = this.states.get(featureId('app'))?.selection?.options[0]
if (runInterface !== 'acp' && runInterface !== 'tui' && runInterface !== 'embed') return this.profile
if (runInterface !== 'acp' && runInterface !== 'embed') return this.profile
return { ...this.profile, runInterface }
}

View File

@@ -41,8 +41,11 @@ const OPTIONAL_DOCUMENTS = [
] as const
function runInterface(entries: readonly CordisConfigEntry[]): RunInterface {
if (entries.some(entry => entry.name === '@deepseek-ai/dsh-tui'
|| entry.name.startsWith('@deepseek-ai/dsh-tui/'))) {
throw new Error('unsupported run interface: @deepseek-ai/dsh-tui has been removed')
}
if (entries.some(entry => entry.name === '@deepseek-ai/dsh-acp')) return 'acp'
if (entries.some(entry => entry.name === '@deepseek-ai/dsh-tui')) return 'tui'
return 'embed'
}
@@ -146,7 +149,7 @@ export class SdkProject {
static create(root: string, request: ProjectCreationRequest): SdkProject {
const app = request.features.find(selection => selection.id === 'app')
const selectedInterface = app?.options[0]
if (selectedInterface !== 'acp' && selectedInterface !== 'tui' && selectedInterface !== 'embed') {
if (selectedInterface !== 'acp' && selectedInterface !== 'embed') {
throw new Error('project creation requires one app feature option')
}
const profile: ProjectProfile = {
@@ -178,6 +181,7 @@ export class SdkProject {
* Load an existing project from required and SDK-managed optional files.
* @param root - existing project directory.
* @returns disk-backed project snapshot.
* @throws When the config references the removed `@deepseek-ai/dsh-tui` root or a subpath.
*/
static async open(root: string): Promise<SdkProject> {
const absolute = resolve(root)

View File

@@ -9,7 +9,7 @@ import type { LocalPluginBlueprint } from '../plugins/local-plugin-blueprint.ts'
import type { FeatureId } from '../ids.ts'
/** Runtime front door selected for a generated project. */
export type RunInterface = 'acp' | 'tui' | 'embed'
export type RunInterface = 'acp' | 'embed'
/** Values shared by the required provider and app features. */
interface ProjectRuntimeOptions {

View File

@@ -9,16 +9,10 @@ Built with the DeepSeek Harness SDK using the {{model}} model.
Run `{{packageManager}} start` and configure a programmatic ACP client to launch this project. Standard output is reserved for ACP JSON-RPC.
{{else}}
{{#if isTui}}
## Run in a terminal
Run `{{packageManager}} start` to start the interactive agent.
{{else}}
## Embed the harness
Import and call the exported `main()` from `index.ts` in your host application.
{{/if}}
{{/if}}
## Development

View File

@@ -8,47 +8,13 @@ import { startSDK, type SdkBootContext } from '@deepseek-ai/dsh-scripts'
/** Boot this project's cordis.yml when invoked by dsh-scripts. */
export async function main(boot: SdkBootContext) {
{{#if isTui}}
const model = boot.args.model
if (typeof model !== 'string' || model.length === 0) throw new Error('TUI startup requires --model=<name>')
const resume = boot.args.resume
if (resume !== undefined && (typeof resume !== 'string' || resume.length === 0)) {
throw new Error('TUI startup requires --resume=<session-id>')
}
const sessionId = SessionId(resume ?? `main-session-${randomUUID()}`)
process.env.DSH_SDK_SESSION_ID = sessionId
{{/if}}
const ctx = await startSDK(new URL('./cordis.yml', import.meta.url))
{{#if isTui}}
try {
if (resume === undefined) {
await ctx.agents.create({
sessionId,
meta: { cwd: boot.cwd },
agentOptions: { model },
})
} else {
await ctx.agents.resume({
resumeSessionId: sessionId,
agentOptions: { model },
})
}
} catch (error) {
try {
await ctx.fiber.dispose()
} catch (disposeError) {
throw new AggregateError([error, disposeError], 'TUI startup and cleanup failed')
}
throw error
}
{{else}}
{{#if isEmbed}}
await ctx.agents.create({
sessionId: SessionId(`main-session-${randomUUID()}`),
meta: { cwd: boot.cwd },
agentOptions: { model: {{modelLiteral}} },
})
{{/if}}
{{/if}}
return ctx
}

View File

@@ -20,7 +20,6 @@ export interface ProjectTemplateContext {
model: string
modelLiteral: string
isAcp: boolean
isTui: boolean
isEmbed: boolean
packageManager: PackageManagerName
installArgs: string
@@ -60,7 +59,6 @@ export function createProjectTemplateContext(
model: profile.runtime.model,
modelLiteral: JSON.stringify(profile.runtime.model),
isAcp: runInterface === 'acp',
isTui: runInterface === 'tui',
isEmbed: runInterface === 'embed',
packageManager: profile.packageManager.name,
installArgs: profile.packageManager.installCommand().join(' '),
@@ -104,10 +102,9 @@ export function createAppProjectArtifacts(
}
/** Build package scripts owned by the selected app feature option. */
export function createAppPackageScripts(context: ProjectTemplateContext): Readonly<Record<'dev' | 'start', string>> {
const modelArg = context.isTui ? ` -- --model=${JSON.stringify(context.model)}` : ''
export function createAppPackageScripts(): Readonly<Record<'dev' | 'start', string>> {
return {
dev: `dsh-sdk dev index.ts${modelArg}`,
start: `dsh-sdk start index.js${modelArg}`,
dev: 'dsh-sdk dev index.ts',
start: 'dsh-sdk start index.js',
}
}

View File

@@ -243,7 +243,7 @@ overrides:
expect(() => loadHelperTemplate('../bad.tpl')).toThrow('must not contain a directory')
expect(createBaselineProjectArtifacts({
name: 'demo', description: 'demo', releaseVersion: '0.0.1', model: 'model', modelLiteral: '"model"', packageManager: 'yarn',
isAcp: false, isTui: false, isEmbed: true,
isAcp: false, isEmbed: true,
installArgs: 'install', buildArgs: 'build',
}).map(document => document.relativePath)).toContain('.yarnrc.yml')
expect(() => new LocalPluginBlueprint('---', 'plugin')).toThrow('invalid local plugin name')

View File

@@ -51,7 +51,7 @@ function selection(id: string, options: readonly string[], secrets?: Record<stri
function request(
extra: readonly FeatureSelection[] = [],
plugins: readonly LocalPluginBlueprint[] = [],
app: 'acp' | 'tui' | 'embed' = 'tui',
app: 'acp' | 'embed' = 'embed',
bash: 'local' | 'sandbox' = 'local',
): ProjectCreationRequest {
return {
@@ -115,16 +115,23 @@ describe('SdkProject and ProjectEditSession', () => {
expect(acp.readEnvironment('.env', 'KEY')).toBe('value')
expect(() => acp.readEnvironment('.env.example', 'KEY')).not.toThrow()
expect(acp.document('tsconfig.json')).toBeInstanceOf(TextProjectFile)
const tui = await make('dsh-open-tui', {}, `- id: provider
await expect(make('dsh-open-tui', {}, `- id: provider
name: '@deepseek-ai/dsh-llm-deepseek'
config: { models: [provider-model] }
- id: tui
name: '@deepseek-ai/dsh-tui'
`)).rejects.toThrow('unsupported run interface: @deepseek-ai/dsh-tui has been removed')
await expect(make('dsh-open-tui-subpath', {}, `- id: tui-prompt
name: '@deepseek-ai/dsh-tui/prompt'
`)).rejects.toThrow('unsupported run interface: @deepseek-ai/dsh-tui has been removed')
const embedded = await make('dsh-open-embed', {}, `- id: provider
name: '@deepseek-ai/dsh-llm-deepseek'
config: { models: [provider-model] }
`, { 'yarn.lock': '' })
expect(tui.profile.runInterface).toBe('tui')
expect(tui.profile.runtime.model).toBe('provider-model')
expect(tui.profile.packageManager.name).toBe('yarn')
expect(tui.profile.name).toBe(tui.root.split('/').at(-1))
expect(embedded.profile.runInterface).toBe('embed')
expect(embedded.profile.runtime.model).toBe('provider-model')
expect(embedded.profile.packageManager.name).toBe('yarn')
expect(embedded.profile.name).toBe(embedded.root.split('/').at(-1))
const pnpm = await make('dsh-open-pnpm', { name: 'pnpm' }, '[]\n', { 'pnpm-lock.yaml': '' })
expect(pnpm.profile.packageManager.name).toBe('pnpm')
const defaults = await make('dsh-open-default', { name: 'default', packageManager: 'npm@10.0.0' }, '[]\n')
@@ -134,10 +141,7 @@ describe('SdkProject and ProjectEditSession', () => {
expect(() => SdkProject.create(defaults.root, { ...request(), features: [] })).toThrow('requires one app')
await expect(make('dsh-open-invalid-manager', { name: 'bad', packageManager: 'bad' }, '[]\n'))
.rejects.toThrow('invalid packageManager field')
const providerFallback = await make('dsh-open-provider-fallback', { name: 'fallback' }, `- id: tui
name: '@deepseek-ai/dsh-tui'
config: { model: '' }
- id: provider
const providerFallback = await make('dsh-open-provider-fallback', { name: 'fallback' }, `- id: provider
name: '@deepseek-ai/dsh-llm-deepseek'
config: { models: [fallback-model] }
`)
@@ -166,28 +170,17 @@ describe('SdkProject and ProjectEditSession', () => {
const index = await readFile(join(project.root, 'index.ts'), 'utf8')
expect(index).toContain('SdkBootContext')
expect(index).toContain('agents.create')
expect(index).toContain('boot.args.resume')
expect(index).not.toContain('boot.args.resume')
expect(index).not.toContain('AgentId')
expect(index).toContain('const sessionId = SessionId(resume ?? `main-session-${randomUUID()}`)')
expect(index).toContain('process.env.DSH_SDK_SESSION_ID = sessionId')
expect(index).toContain('resumeSessionId: sessionId')
expect(index).toContain('await ctx.fiber.dispose()')
expect(index).toContain("new AggregateError([error, disposeError], 'TUI startup and cleanup failed')")
expect(index).toContain('SessionId(`main-session-${randomUUID()}`)')
expect(project.packageManifest().scripts).toEqual({
dev: 'dsh-sdk dev index.ts -- --model="deepseek-v4-flash"',
dev: 'dsh-sdk dev index.ts',
build: 'dsh-sdk build',
typecheck: 'tsc -b',
start: 'dsh-sdk start index.js -- --model="deepseek-v4-flash"',
start: 'dsh-sdk start index.js',
config: 'dsh-sdk config',
})
expect(await readFile(join(project.root, '.env.example'), 'utf8')).toContain('EXA_API_KEY=')
expect(project.cordis.entry('tui')?.config?.sessionId).toMatchObject({
source: 'process.env.DSH_SDK_SESSION_ID',
})
expect(await readFile(join(project.root, 'cordis.yml'), 'utf8'))
.toContain('sessionId: !!js process.env.DSH_SDK_SESSION_ID')
expect(project.cordis.entry('tui')?.config).not.toHaveProperty('model')
expect(project.cordis.entry('tui-prompt')?.name).toBe('@deepseek-ai/dsh-tui/prompt')
expect(project.cordis.entry('agent-loop')?.config).toEqual({ agents: [] })
expect(project.cordis.entry('session-invariant')?.name).toBe('@deepseek-ai/dsh-session/invariant')
expect(project.cordis.entry('agent-invariant')?.name).toBe('@deepseek-ai/dsh-agent/invariant')
@@ -229,13 +222,12 @@ describe('SdkProject and ProjectEditSession', () => {
expect(app.selection).toEqual(selection('app', ['embed']))
expect(committed.cordis.entry('agent-loop')?.config).toEqual({ agents: [] })
expect(committed.cordis.entry('acp')).toBeUndefined()
expect(committed.cordis.entry('tui')).toBeUndefined()
})
it('emits the sandbox workspace-write example as inactive Cordis config', async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-sandbox-bash-'))
temporary.push(root)
const creation = request([], [], 'tui', 'sandbox')
const creation = request([], [], 'embed', 'sandbox')
const project = SdkProject.create(root, creation)
const registry = createBuiltinRegistry(project.profile)
const edit = project.edit(registry)
@@ -326,7 +318,7 @@ describe('SdkProject and ProjectEditSession', () => {
const modifiedRegistry = createBuiltinRegistry(modified.profile)
expect(() => { modified.edit(modifiedRegistry).configureFeature(
modifiedRegistry.get(featureId('app')),
selection('app', ['tui']),
selection('app', ['acp']),
) }).toThrow('feature-owned file was modified: README.md')
const manifest = PackageJsonFile.parse(await readFile(join(embed.root, 'package.json'), 'utf8'))
@@ -337,18 +329,6 @@ describe('SdkProject and ProjectEditSession', () => {
.toContain('missing package.json script dev')
})
it('rejects ask-user on non-interactive app interfaces', async () => {
const project = await createCommitted([selection('ask-user', ['default'])])
const registry = createBuiltinRegistry(project.profile)
const embed = project.edit(registry)
embed.configureFeature(registry.get(featureId('app')), selection('app', ['embed']))
await expect(embed.commit()).rejects.toThrow('feature ask-user is not available for embed')
const acp = project.edit(registry)
acp.configureFeature(registry.get(featureId('app')), selection('app', ['acp']))
await expect(acp.commit()).rejects.toThrow('feature ask-user is not available for acp')
})
it('supports disabled feature reconfiguration and rejects invalid state operations', async () => {
const project = await createCommitted([selection('todo', ['default'])])
const registry = createBuiltinRegistry(project.profile)
@@ -359,8 +339,8 @@ describe('SdkProject and ProjectEditSession', () => {
edit.disableFeature(todo)
edit.configureFeature(todo, selection('todo', ['default']))
edit.enableFeature(todo)
expect(() => { edit.enableFeature(registry.get(featureId('ask-user'))) }).toThrow('not installed')
expect(() => { edit.disableFeature(registry.get(featureId('ask-user'))) }).toThrow('not installed')
expect(() => { edit.enableFeature(registry.get(featureId('workflow'))) }).toThrow('not installed')
expect(() => { edit.disableFeature(registry.get(featureId('workflow'))) }).toThrow('not installed')
expect(() => { edit.setCustomPluginDisabled('missing', true) }).toThrow('does not exist')
const committed = await edit.commit()
expect(committed.changes.enabledFeatures).toContain('todo')
@@ -373,7 +353,7 @@ describe('SdkProject and ProjectEditSession', () => {
const edit = project.edit(registry)
edit.setCustomPluginDisabled('sample', true)
expect(edit.cordisConfigEntries().find(entry => entry.id === 'sample')?.disabled).toBe(true)
expect(() => { edit.setCustomPluginDisabled('tui', true) }).toThrow('builtin feature')
expect(() => { edit.setCustomPluginDisabled('agent-loop', true) }).toThrow('builtin feature')
const next = (await edit.commit()).project
const enable = next.edit(createBuiltinRegistry(next.profile))
enable.setCustomPluginDisabled('sample', false)
@@ -468,8 +448,8 @@ describe('SdkProject and ProjectEditSession', () => {
}
const internals = edit as unknown as Internals
const collidingEntry: ProjectResource = {
kind: 'cordis-config-entry', key: resourceKey('cordis-config-entry:tui'),
entry: { id: 'tui', name: 'other-package' }, ownedConfigKeys: [],
kind: 'cordis-config-entry', key: resourceKey('cordis-config-entry:agent-loop'),
entry: { id: 'agent-loop', name: 'other-package' }, ownedConfigKeys: [],
}
expect(() => { internals.applyResource(collidingEntry, undefined) }).toThrow('is owned by')
const existingFile: ProjectResource = {
@@ -821,7 +801,7 @@ describe('extension points', () => {
})
expect(exclusive.defaultOptions(profile)).toEqual(['one'])
expect(exclusive.isApplicable(profile)).toBe(true)
expect(exclusive.isApplicable({ ...profile, runInterface: 'tui' })).toBe(false)
expect(exclusive.isApplicable({ ...profile, runInterface: 'acp' })).toBe(false)
expect(exclusive.requirements(selection('defined', ['one']))).toEqual([
{ id: 'base' }, { id: 'option', options: ['required'] },
])
@@ -835,7 +815,7 @@ describe('extension points', () => {
expect(entry?.validateConfig?.({ nested: { value: 2 }, list: ['a', 'b'], nullable: null })).toEqual([])
expect(entry?.validateConfig?.({ nested: [], list: 'bad' })).toHaveLength(3)
expect(() => exclusive.normalizeSelection(selection('other', ['one']), profile)).toThrow('does not belong')
expect(() => exclusive.normalizeSelection(selection('defined', ['one']), { ...profile, runInterface: 'tui' }))
expect(() => exclusive.normalizeSelection(selection('defined', ['one']), { ...profile, runInterface: 'acp' }))
.toThrow('not available')
expect(() => exclusive.normalizeSelection(selection('defined', ['missing']), profile)).toThrow('unknown')
expect(() => exclusive.normalizeSelection(selection('defined', ['one', 'two']), profile)).toThrow('exactly one')
@@ -843,7 +823,7 @@ describe('extension points', () => {
id: 'fixed', summary: 'Fixed', mode: 'single', options: [option],
}])).toHaveLength(2)
expect(() => new FeatureRegistry([], profile).get(featureId('missing'))).toThrow('unknown feature')
expect(new FeatureRegistry([exclusive], profile).ownerOfPackage('one-package', { ...profile, runInterface: 'tui' }))
expect(new FeatureRegistry([exclusive], profile).ownerOfPackage('one-package', { ...profile, runInterface: 'acp' }))
.toBeUndefined()
class Unsupported extends FixedFeature {
override readonly id = featureId('unsupported')
@@ -926,12 +906,6 @@ describe('extension points', () => {
resource.kind === 'cordis-config-entry' && resource.entry.id === 'acp')
expect(acpEntry?.entry.id).toBe('acp')
expect(acpEntry?.validateConfig?.({ model: '' })).toHaveLength(1)
const tuiEntry = builtins.get(featureId('app')).contribution(selection('app', ['tui']), profile).resources
.find((resource): resource is CordisConfigEntryResource =>
resource.kind === 'cordis-config-entry' && resource.entry.id === 'tui')
expect(tuiEntry?.validateConfig?.({ welcome: 'ready', sessionId: 1 })).toEqual([
'sessionId must be a non-empty string',
])
const embedOption = app.options.find(option => option.id === 'embed')
expect(embedOption?.markerConfigEntries(profile)).toEqual([])
expect(embedOption?.contribution(profile, {}).resources.map(resource => resource.kind)).toEqual([
@@ -939,7 +913,7 @@ describe('extension points', () => {
])
expect(embedOption?.matchesConfigEntries([
{ id: 'agent-loop', name: '@deepseek-ai/dsh-agent-loop' },
{ id: 'tui', name: '@deepseek-ai/dsh-tui' },
{ id: 'acp', name: '@deepseek-ai/dsh-acp' },
], profile)).toBe(false)
const spineAgentLoop = builtins.get(featureId('spine')).contribution(selection('spine', ['default']), profile).resources
.find((resource): resource is CordisConfigEntryResource =>

View File

@@ -376,7 +376,7 @@ describe('feature configurator', () => {
name: 'demo',
description: 'demo',
runtime: { model: 'deepseek-v4-flash' },
runInterface: 'tui',
runInterface: 'embed',
packageManager: new NpmPackageManager('10.0.0'),
releaseVersion: '0.0.1',
}

View File

@@ -56,7 +56,7 @@ function targetRunInterface(
desired: ReadonlyMap<string, NestedMultiSelectValue<string, string>>,
): RunInterface {
const selected = desired.get('feature:app')?.choices[0]
return selected === 'acp' || selected === 'tui' || selected === 'embed' ? selected : current
return selected === 'acp' || selected === 'embed' ? selected : current
}
/** Reconcile one tree selection into domain commands, then review and commit once. */
@@ -135,6 +135,7 @@ export class ConfigWorkflow {
runInterface: targetRunInterface(project.profile.runInterface, desiredByTarget),
}
for (const feature of features) {
/* v8 ignore next -- no current builtin is interface-specific after TUI removal */
if (!feature.isApplicable(targetProfile)) desiredByTarget.delete(featureTarget(feature))
}

View File

@@ -84,15 +84,10 @@ Change file: package.json
"choiceMode": "exclusive",
"choices": [
{
"default": false,
"default": true,
"label": "ACP automation server",
"value": "acp",
},
{
"default": true,
"label": "Terminal TUI",
"value": "tui",
},
{
"default": false,
"label": "Embedded context",
@@ -280,16 +275,6 @@ Change file: package.json
"value": "feature:timeout-policy",
"warning": undefined,
},
{
"choiceMode": undefined,
"choices": undefined,
"default": false,
"disabled": false,
"label": "Ask the user from the model loop",
"required": false,
"value": "feature:ask-user",
"warning": undefined,
},
],
"showChanges": true,
},

View File

@@ -94,7 +94,7 @@ async function baseProject(): Promise<SdkProject> {
features: [
{ id: featureId('provider'), options: ['deepseek-official'], secrets: { apiKey: 'key' } },
{ id: featureId('bash'), options: ['local'] },
{ id: featureId('app'), options: ['tui'] },
{ id: featureId('app'), options: ['acp'] },
{ id: featureId('persistence'), options: ['jsonl'] },
],
localPlugins: [],

View File

@@ -85,7 +85,7 @@ function commandContext(cwd: string): DshSdkCommandContext & { readStdout: () =>
function creation(
extra: ProjectCreationRequest['features'] = [],
localPlugins: readonly LocalPluginBlueprint[] = [],
app: 'acp' | 'tui' | 'embed' = 'embed',
app: 'acp' | 'embed' = 'embed',
): ProjectCreationRequest {
return {
name: 'config-agent',
@@ -107,7 +107,7 @@ function creation(
async function committedProject(
extra: ProjectCreationRequest['features'] = [],
localPlugins: readonly LocalPluginBlueprint[] = [],
app: 'acp' | 'tui' | 'embed' = 'embed',
app: 'acp' | 'embed' = 'embed',
): Promise<SdkProject> {
const root = await mkdtemp(join(tmpdir(), 'dsh-config-workflow-'))
temporary.push(root)
@@ -526,7 +526,7 @@ describe('ConfigWorkflow', () => {
const workflow = new ConfigWorkflow(new QueuePort([
[
{ value: 'feature:provider', choices: ['custom'] },
{ value: 'feature:app', choices: ['tui'] },
{ value: 'feature:app', choices: ['acp'] },
{ value: 'feature:persistence', choices: ['jsonl'] },
],
'https://provider.example/v1',
@@ -537,31 +537,11 @@ describe('ConfigWorkflow', () => {
const provider = result.commit?.project.cordis.entry('llm-pi-ai')
expect(provider?.config?.apiKey).toBeDefined()
expect(provider?.config?.baseURL).toBe('https://provider.example/v1')
expect(result.commit?.project.cordis.entry('tui')).toBeDefined()
expect(result.commit?.project.cordis.entry('acp')).toBeDefined()
expect(result.commit?.project.cordis.entry('agent-loop')).toBeDefined()
expect(result.commit?.project.cordis.entry('agent-core')).toBeUndefined()
})
it('disables ask-user when switching its app interface to ACP', async () => {
const project = await committedProject([
{ id: featureId('ask-user'), options: ['default'] },
], [], 'tui')
const registry = createBuiltinRegistry(project.profile)
const output = outputBuffer()
const workflow = new ConfigWorkflow(new QueuePort([
[
{ value: 'feature:provider', choices: ['deepseek-official'] },
{ value: 'feature:app', choices: ['acp'] },
{ value: 'feature:persistence', choices: ['jsonl'] },
{ value: 'feature:ask-user', choices: ['default'] },
],
true,
]), output.stream, async () => {})
const result = await workflow.run(project, registry)
expect(result.commit?.project.profile.runInterface).toBe('acp')
expect(result.commit?.project.cordis.entry('tool-ask-user')?.disabled).toBe(true)
expect(output.read()).toContain('Disable feature: ask-user')
})
})
describe('dsh-sdk create', () => {