mirror of
https://github.com/stablyai/orca.git
synced 2026-10-07 00:02:29 +00:00
fix(dsh): recognize 0.2 profiles and open workspace composer (#24589)
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
@@ -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')
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user