mirror of
https://github.com/stablyai/orca.git
synced 2026-10-03 08:02:12 +00:00
fix(dsh): recognize 0.2 profiles and open workspace composer (#24589)
This commit is contained in:
@@ -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 <id>`, 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.
|
||||
@@ -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