Files
deepseek-harness/packages/sdk/helper/src/features/builtin/app.ts
2026-07-20 19:26:04 +08:00

128 lines
4.3 KiB
TypeScript

/**
* Required run-interface app feature.
*
* @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 {
createAppPackageScripts,
createAppProjectArtifacts,
createProjectTemplateContext,
} from '../../templates/project-template.ts'
import {
FeatureOption,
ExclusiveOptionFeature,
} from '../feature.ts'
import { ProjectContribution, type ProjectResource } from '../resources.ts'
import {
npmCordisConfigEntry,
optionalString,
ownedTextFile,
packageScript,
requiredString,
} from './helpers.ts'
const ID = featureId('app')
function appProjectResources(
profile: ProjectProfile,
runInterface: 'acp' | 'tui' | 'embed',
): readonly ProjectResource[] {
const context = createProjectTemplateContext(profile, runInterface)
const scripts = createAppPackageScripts(context)
return [
...createAppProjectArtifacts(context).map(document => (
ownedTextFile(ID, document.relativePath, document.serialize())
)),
packageScript(ID, 'dev', scripts.dev),
packageScript(ID, 'start', scripts.start),
]
}
class AppOption extends FeatureOption {
override readonly id: 'acp' | 'tui' | 'embed'
override readonly label: string
constructor(id: 'acp' | 'tui' | 'embed', label: string) {
super()
this.id = id
this.label = label
}
/** Identify options by their unique front door, not the shared interaction service. */
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 []
}
}
/** Embed is identified by the configured loop with no external front door. */
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')
}
override contribution(profile: ProjectProfile): ProjectContribution {
switch (this.id) {
case 'acp':
return new ProjectContribution([
...appProjectResources(profile, this.id),
...npmCordisConfigEntry(ID, {
id: 'user-interaction',
name: '@deepseek-ai/dsh-user-interaction',
}),
...npmCordisConfigEntry(ID, {
id: 'acp',
name: '@deepseek-ai/dsh-acp',
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',
}),
...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. */
export class AppFeature extends ExclusiveOptionFeature {
override readonly id = ID
override readonly summary = 'Run interface'
override readonly required = true
override readonly requires = [featureId('spine')]
override readonly options = [
new AppOption('acp', 'ACP server'),
new AppOption('tui', 'Terminal TUI'),
new AppOption('embed', 'Embedded context'),
]
/** Default to the profile's already selected front door. */
override defaultOptions(profile: ProjectProfile): readonly string[] {
return [profile.runInterface]
}
}