fix(dsh): recognize 0.2 profiles and open workspace composer (#24589)

This commit is contained in:
Neil
2026-10-02 04:26:00 -07:00
committed by GitHub
parent 99b2628aef
commit cd8d03bc06
6 changed files with 160 additions and 40 deletions
+24
View File
@@ -9,6 +9,14 @@ function tokens(commandLine: string): string[] {
describe('isDshNonInteractiveCommand', () => {
it.each([
'dsh web',
'dsh headless "run the tests"',
'dsh sdk',
'dsh sdk-minimal',
'dsh acp',
'dsh desktop',
'dsh dsh-tui --dump-config',
'dsh dsh-tui --dump-default-config',
'dsh dsh-tui --dump-config-schema',
'dsh --profile web',
'dsh --profile=web --port 8080',
'dsh --profile headless "run the tests"',
@@ -23,6 +31,14 @@ describe('isDshNonInteractiveCommand', () => {
it.each([
'dsh --profile dsh-tui',
'dsh dsh-tui',
'dsh tui --resume web',
'dsh tui --patch web --resume sdk',
'dsh tui --patch=web --resume sdk',
'dsh --profile tui --patch headless --resume sdk',
'dsh tui --from-default-profile web --resume sdk',
'dsh tui --resume session --dump-config',
'dsh --profile tui --resume session --dump-config',
'dsh --profile=dsh-tui',
'dsh --profile dsh-tui --resume 9478e2d8-29bc-4009-ab32-657efa2bd763',
'dsh-tui',
@@ -66,6 +82,14 @@ describe('dsh foreground process recognition', () => {
it.each([
'dsh web',
'dsh headless "summarize the diff"',
'dsh sdk',
'dsh sdk-minimal',
'dsh acp',
'node /usr/local/lib/node_modules/@deepseek-ai/dsh/lib/bin.js headless "go"',
'node /usr/local/lib/node_modules/@deepseek-ai/dsh/lib/bin.js sdk',
'node --no-warnings /usr/local/lib/node_modules/@deepseek-ai/dsh/lib/bin.js sdk',
'C:\\Tools\\dsh.cmd headless go',
'dsh --profile headless "summarize the diff"',
'node /usr/local/lib/node_modules/@deepseek-ai/dsh/lib/bin.js --profile acp'
])('does not claim %s as an interactive agent pane', (commandLine) => {
+15 -30
View File
@@ -1,11 +1,6 @@
// DeepSeek Harness ships one binary, `dsh`, and boots a *profile* with it. Only the
// `dsh-tui` profile paints an interactive composer; `web` serves HTTP, `headless` answers
// one task and exits, `sdk`/`sdk-minimal`/`acp` speak JSON-RPC on stdio, and `plugin` is
// package management. All five are `dsh` in the process table, so foreground recognition
// has to read the profile to tell an agent pane from a server or a one-shot.
//
// `dsh-tui` (and its `dst` alias) forward straight to `dsh --profile dsh-tui`, so they are
// always interactive and never reach this matcher.
import { findInterpreterEntrypointToken } from './agent-command-line-entrypoint'
// DSH 0.2 accepts positional profiles; community dsh-tui/dst choose their own TUI profile.
/** Profiles that boot something other than the interactive terminal UI. */
const NON_INTERACTIVE_PROFILES = new Set([
@@ -17,9 +12,8 @@ const NON_INTERACTIVE_PROFILES = new Set([
'desktop'
])
/** Bare-word subcommands the launcher accepts: `plugin` boots no profile at all, and `web`
* is the documented alias for `--profile web`. Neither hosts an interactive pane. */
const SUBCOMMANDS = new Set(['plugin', 'web'])
/** Plugin management never boots an interactive pane. */
const SUBCOMMANDS = new Set(['plugin'])
/** Launcher flags that print a composed config and exit. */
const DUMP_FLAGS = new Set(['--dump-config', '--dump-default-config', '--dump-config-schema'])
@@ -69,30 +63,21 @@ function isLauncherToken(token: string): boolean {
* and a prompt or session id is free text that must never be read as a launcher flag.
*/
export function isDshNonInteractiveCommand(tokens: readonly string[]): boolean {
// Why first: `dsh-tui`/`dst` have already chosen the interactive profile, and everything
// after them is the TERMINAL APP's argv — a `--resume` id or a workspace target. A
// workspace folder named `web` or `plugin` is an ordinary directory name, and reading it
// as a `dsh` subcommand would mark a live agent pane non-interactive, costing it status
// hooks and prompt delivery. Index 1 as well as 0 because a node shim puts the launcher
// script path there.
if (
TUI_LAUNCHER_NAMES.has(programBasename(tokens[0])) ||
TUI_LAUNCHER_NAMES.has(programBasename(tokens[1]))
) {
const firstProgram = programBasename(tokens[0])
const entrypoint = findInterpreterEntrypointToken([...tokens], firstProgram)
const programIndex = entrypoint === null ? 0 : tokens.indexOf(entrypoint)
if (TUI_LAUNCHER_NAMES.has(programBasename(tokens[programIndex]))) {
return false
}
const indexOfArgs = programIndex + 1
let index = indexOfArgs
let profile: string | null = null
let index = 1
// Skip the leading non-flag tokens: an interpreter invocation puts the script path here.
while (index < tokens.length && !isLauncherToken(tokens[index])) {
const token = tokens[index]
if (SUBCOMMANDS.has(token)) {
const first = tokens[index]
if (first !== undefined && !first.startsWith('-')) {
if (SUBCOMMANDS.has(first)) {
return true
}
if (!token.startsWith('-') && index > 1) {
// A bare word that is neither a subcommand nor a flag: the app's arguments start here.
return false
}
profile = first
index += 1
}
for (; index < tokens.length; index += 1) {
+53
View File
@@ -0,0 +1,53 @@
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { runProcess } from './child-process/run-process'
import { recognizeAgentProcessFromCommandLine } from './agent-process-recognition'
const binary = process.env.ORCA_REAL_DSH_CLI
// No model call: published profile configuration and help establish the launcher contract.
describe.skipIf(!binary)('published DSH 0.2 CLI', () => {
it('keeps positional and explicit headless profiles outside interactive recognition', async () => {
if (!binary) {
throw new Error('Set ORCA_REAL_DSH_CLI to the official dsh executable')
}
const root = await mkdtemp(join(tmpdir(), 'orca-dsh-cli-'))
const env = { ...process.env, DSH_HOME: root, DEEPSEEK_API_KEY: '' }
try {
const version = await runProcess({ program: binary, args: ['--version'], cwd: root, env })
expect(version.code).toBe(0)
expect(version.stdout.trim()).toMatch(/^0\.2\./)
for (const profile of ['headless', 'sdk', 'sdk-minimal', 'acp']) {
for (const args of [[profile], ['--profile', profile]]) {
const result = await runProcess({
program: binary,
args: [...args, '--dump-default-config'],
cwd: root,
env,
timeoutMs: 30_000
})
expect(result.code).toBe(0)
expect(result.stdout).toContain('@deepseek-ai/dsh-')
const command = `"${binary}" ${args.join(' ')}`
expect(recognizeAgentProcessFromCommandLine(command)).toBeNull()
expect(
recognizeAgentProcessFromCommandLine(command, { includeHeadlessOneShot: true })?.agent
).toBe('dsh')
}
}
const help = await runProcess({
program: binary,
args: ['headless', '--help'],
cwd: root,
env,
timeoutMs: 30_000
})
expect(help.code).toBe(0)
expect(help.stdout).toContain('Answer one task and exit')
} finally {
await rm(root, { recursive: true, force: true })
}
}, 120_000)
})
+21
View File
@@ -0,0 +1,21 @@
import { describe, expect, it } from 'vitest'
import { buildAgentStartupPlan } from './tui-agent-startup'
import { TUI_AGENT_CONFIG } from './tui-agent-config'
describe('DSH first launch', () => {
it.each(['darwin', 'linux', 'win32'] as const)(
'selects the current workspace before pasting the task on %s',
(platform) => {
const plan = buildAgentStartupPlan({
agent: 'dsh',
prompt: 'Review this folder',
cmdOverrides: {},
platform
})
expect(plan?.launchCommand).toContain('dsh-tui .')
expect(plan?.launchCommand).not.toContain('Review this folder')
expect(TUI_AGENT_CONFIG.dsh.promptInjectionMode).toBe('stdin-after-start')
expect(TUI_AGENT_CONFIG.dsh.draftPasteReadySignal).toBe('dsh-composer-prompt')
}
)
})
+3 -10
View File
@@ -290,21 +290,14 @@ const TUI_AGENT_CONFIG_SOURCE: Record<TuiAgent, TuiAgentConfigSource> = {
promptInjectionMode: 'stdin-after-start'
},
dsh: {
// Why: DeepSeek Harness publishes one binary (`dsh`) that boots a profile, and only the
// `dsh-tui` profile paints a composer. `dsh-tui` (alias `dst`) is the launcher that
// selects it, so detect that and require `dsh` too — the launcher delegates to it and
// fails without it.
detectCmd: 'dsh-tui',
detectCmdAliases: ['dst'],
detectRequiredCommands: ['dsh'],
// Why: the launcher re-execs `dsh --profile dsh-tui`, so the pane's foreground process
// is `dsh`, never `dsh-tui`. Readiness and follow-up delivery key off this name.
// A first bare launch opens the session browser instead of the composer.
launchCmd: 'dsh-tui .',
expectedProcess: 'dsh',
// Why: the terminal app parses only `--resume`/`--continue` and a workspace target; it
// has no prompt flag, so the first prompt is pasted into the composer after startup.
promptInjectionMode: 'stdin-after-start',
// Why: DSH-TUI animates a whale intro continuously behind its composer, so the default
// quiet window never settles (the grok failure mode). See dsh-tui-ready-no-key.txt.
// The whale intro keeps repainting behind the ready composer.
draftPasteReadySignal: 'dsh-composer-prompt'
},
zcode: {