From cd8d03bc06398b856f6db734aef11df4e2afe15a Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Fri, 2 Oct 2026 04:26:00 -0700 Subject: [PATCH] fix(dsh): recognize 0.2 profiles and open workspace composer (#24589) --- docs/reference/dsh-harness-integration.md | 44 +++++++++++++++++++ src/shared/dsh-launch-command.test.ts | 24 ++++++++++ src/shared/dsh-launch-command.ts | 45 +++++++------------ src/shared/dsh-real-cli.test.ts | 53 +++++++++++++++++++++++ src/shared/dsh-startup.test.ts | 21 +++++++++ src/shared/tui-agent-config.ts | 13 ++---- 6 files changed, 160 insertions(+), 40 deletions(-) create mode 100644 docs/reference/dsh-harness-integration.md create mode 100644 src/shared/dsh-real-cli.test.ts create mode 100644 src/shared/dsh-startup.test.ts diff --git a/docs/reference/dsh-harness-integration.md b/docs/reference/dsh-harness-integration.md new file mode 100644 index 00000000000..9a850e06900 --- /dev/null +++ b/docs/reference/dsh-harness-integration.md @@ -0,0 +1,44 @@ +# DeepSeek Harness integration + +Orca detects the community `@deepseek-harness-tui/dsh-tui` launcher (`dsh-tui`, alias +`dst`) and requires the official `@deepseek-ai/dsh` executable too. The launcher +boots the `dsh-tui` profile; Orca passes `.` to select the current workspace and +reach its composer on the first launch. The official Harness does not bundle this community TUI. +DSH Console and DeepSeek Build are separate products and are not interchangeable +with this launch contract. + +The official DSH 0.2 CLI accepts both `dsh --profile headless` and `dsh headless`. +Orca excludes the known `web`, `headless`, `sdk`, `sdk-minimal`, `acp`, and `desktop` +profiles from interactive process recognition, along with plugin management and +configuration dumps. Custom profile names remain eligible because profiles are +user configurable. Only launcher arguments are inspected; app prompts, resume IDs, +and patch filenames cannot change the selected profile's identity. + +Status hooks use the official `@deepseek-ai/dsh-hooks-claude-code` plugin, installed +as an owned block in `$DSH_HOME/cordis.patch.yml`. User entries outside the block are +preserved. Local installation respects `DSH_HOME`; the existing SSH installer uses +the execution host's default `~/.dsh` because SFTP cannot read its environment. +Hooks report session start, prompt submission, tool start/end, and stopping through +Orca's host status store. Approval has no dedicated hook; it is not inferred from +an uncaptured screen. Subagent lifecycle events are ignored for parent-pane status. + +DSH 0.2 still emits an empty `transcript_path` in Claude-compatible hooks. Its +session persistence defaults to compressed JSONL under `$DSH_HOME/sessions`. +Orca can resume a hook-associated session through `dsh-tui --resume `, but +currently does not discover DSH logs in Agent Session History. Resume support alone +does not establish transcript-history support. + +## Reproduce the official launcher check + +Install `@deepseek-ai/dsh@0.2.0-rc.2` into a disposable prefix, then run: + +```sh +ORCA_BACKGROUND_LAUNCH=1 ORCA_REAL_DSH_CLI=/path/to/prefix/node_modules/.bin/dsh \ + pnpm test src/shared/dsh-real-cli.test.ts +``` + +The opt-in test checks published version, composed profile configurations, and +headless help in an isolated home and working folder without a model request. +Interactive readiness is separately pinned to the captured community TUI transcript +in `src/main/runtime/__fixtures__/dsh-tui-ready-no-key.txt`; that older capture is +not proof of current TUI compatibility or paid generation. diff --git a/src/shared/dsh-launch-command.test.ts b/src/shared/dsh-launch-command.test.ts index 6585318cc98..f8bf718a211 100644 --- a/src/shared/dsh-launch-command.test.ts +++ b/src/shared/dsh-launch-command.test.ts @@ -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) => { diff --git a/src/shared/dsh-launch-command.ts b/src/shared/dsh-launch-command.ts index 60100a9823b..4c44fbf9c4d 100644 --- a/src/shared/dsh-launch-command.ts +++ b/src/shared/dsh-launch-command.ts @@ -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) { diff --git a/src/shared/dsh-real-cli.test.ts b/src/shared/dsh-real-cli.test.ts new file mode 100644 index 00000000000..6422b9884fa --- /dev/null +++ b/src/shared/dsh-real-cli.test.ts @@ -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) +}) diff --git a/src/shared/dsh-startup.test.ts b/src/shared/dsh-startup.test.ts new file mode 100644 index 00000000000..0a99d2cc67b --- /dev/null +++ b/src/shared/dsh-startup.test.ts @@ -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') + } + ) +}) diff --git a/src/shared/tui-agent-config.ts b/src/shared/tui-agent-config.ts index d9fdabb14c3..a7be259df2c 100644 --- a/src/shared/tui-agent-config.ts +++ b/src/shared/tui-agent-config.ts @@ -290,21 +290,14 @@ const TUI_AGENT_CONFIG_SOURCE: Record = { 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: {