Merge branch 'master' into claude/unified-environment-credentials-c8841a

Master removed the TUI package, the `meta` and `upgrade` subcommands, and
`--config-replace`, and made raw `dsh` require a `--config` overlay. Resolved
onto that shape:

- Dropped this branch's TUI edits with the surface itself, including
  `tui.cordis.yml`, `runTui`, and the TUI keyless PTY smoke.
- Dropped the `--config-replace` plumbing rather than reintroducing a flag
  master deliberately removed. The gap this branch fixed remains: `dsh -p`
  still could not name its composition, so it keeps `--config`.
- Kept this branch's deletion of the personal `$DSH_HOME/config.yaml` layer,
  which master still carried, and provided the environment snapshot in the new
  raw `runConfig` surface alongside web and headless.
- Ported the headless shutdown PTY test off the personal overlay onto a named
  `--config` file, which is what proves that flag now exists on `-p`.
This commit is contained in:
Yichen Jiang
2026-08-04 17:51:44 +08:00
668 changed files with 10527 additions and 30408 deletions

View File

@@ -254,10 +254,6 @@ class SingleExeBuild {
'--config.node-linker=hoisted',
'--config.auto-install-peers=false',
'--config.link-workspace-packages=true',
// The production closure intentionally omits the patched dev-only
// @earendil-works/pi-tui package. The root frozen install still validates
// every patch; this exception is scoped only to the production deploy.
'--config.allow-unused-patches=true',
this.staging,
])
if (this.cli.dryRun) {

View File

@@ -97,7 +97,6 @@ function workspaceManifests(): WorkspaceManifest[] {
const packageFileExtras: Readonly<Record<string, readonly string[]>> = {
'@deepseek-ai/dsh-helper': ['lib/assets'],
'@deepseek-ai/dsh-tui': ['lib/prompt.js'],
'@deepseek-ai/dsh-scripts': [
'lib/dev/tsdown-config.js',
'lib/local-plugin-loader-hooks.js',

View File

@@ -14,6 +14,10 @@ interface CssModulePlugin {
load?: (this: { addWatchFile: (id: string) => void }, id: string) => Promise<unknown>
}
function clientSourceMapPath(packagePath: string): string {
return fileURLToPath(new URL(`../packages/${packagePath}/lib/client.js.map`, import.meta.url))
}
function purityResolveId(): ResolveId {
// libEntry is spelled at every call site (no default) so the
// package-invariants text check can see the invariant entry per package.
@@ -74,6 +78,54 @@ describe('client bundle purity gate', () => {
})
})
describe('client bundle debug artifacts', () => {
it('emits source maps for plugin TS and TSX outside the Vite module graph', () => {
const configs = clientBundle('@deepseek-ai/dsh-client-test', ['lib/types/index.js', 'lib/types/invariant.js'])
expect(configs[1]?.sourcemap).toBe(true)
})
it('maps first-party sources to their repository package paths', () => {
const configs = clientBundle('@deepseek-ai/dsh-client-ui-goal', ['lib/types/index.js', 'lib/types/invariant.js'])
const outputOptions = configs[1]?.outputOptions
if (typeof outputOptions !== 'object' || outputOptions === null) throw new Error('client output options missing')
const transform = outputOptions.sourcemapPathTransform
if (transform === undefined) throw new Error('client sourcemap path transform missing')
const source = transform('../src/client/GoalBar.tsx', clientSourceMapPath('client/ui-goal'))
expect(source).toBe('../../../packages/client/ui-goal/src/client/GoalBar.tsx')
const resolved = new URL(source, 'https://dsh.test/plugins/@deepseek-ai/dsh-client-ui-goal/client.js.map')
expect(resolved.pathname).toBe('/packages/client/ui-goal/src/client/GoalBar.tsx')
})
it('maps dual-face host sources to the host package group', () => {
const configs = clientBundle('@deepseek-ai/dsh-host-directory-picker-native', ['lib/types/index.js'])
const outputOptions = configs[1]?.outputOptions
if (typeof outputOptions !== 'object' || outputOptions === null) throw new Error('client output options missing')
const transform = outputOptions.sourcemapPathTransform
if (transform === undefined) throw new Error('client sourcemap path transform missing')
const source = transform('../src/client/index.ts', clientSourceMapPath('host/directory-picker-native'))
expect(source).toBe('../../../packages/host/directory-picker-native/src/client/index.ts')
})
it('maps inlined workspace sources to packages and leaves dependencies outside it unchanged', () => {
const configs = clientBundle('@deepseek-ai/dsh-client-connection', ['lib/types/index.js'])
const outputOptions = configs[1]?.outputOptions
if (typeof outputOptions !== 'object' || outputOptions === null) throw new Error('client output options missing')
const transform = outputOptions.sourcemapPathTransform
if (transform === undefined) throw new Error('client sourcemap path transform missing')
const sourceMapPath = clientSourceMapPath('client/connection')
const workspaceSource = transform('../../../host/apiproxy/src/api/rpc.ts', sourceMapPath)
expect(workspaceSource).toBe('../../../packages/host/apiproxy/src/api/rpc.ts')
const resolved = new URL(workspaceSource, 'https://dsh.test/plugins/@deepseek-ai/dsh-client-connection/client.js.map')
expect(resolved.pathname).toBe('/packages/host/apiproxy/src/api/rpc.ts')
const dependencySource = '../../../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/index.js'
expect(transform(dependencySource, sourceMapPath)).toBe(dependencySource)
})
})
describe('client bundle CSS Modules watch graph', () => {
it('registers the physical stylesheet read behind a virtual module', async () => {
const plugin = cssModulePlugin()

View File

@@ -279,8 +279,6 @@ export const TYPE_LINK_EXEMPTIONS: Readonly<Record<string, string>> = {
WebRoute: 'route registration contract is owned by packages/host/webserver/src/index.ts',
ThemeTokens: 'service-local token dictionary is owned by packages/client/ui-theme/src/index.ts',
Translate: 'service-local bound translator is owned by packages/client/i18n/src/index.ts',
TuiOverlayRequest: 'service-local extension contract is owned by packages/ui/tui/README.md',
TuiOverlaySession: 'service-local extension contract is owned by packages/ui/tui/README.md',
InvariantRegistration: 'service-local lifecycle handle is owned by packages/support/invariants/README.md',
PresetOption: 'deployment menu metadata is owned by packages/ui/permission/README.md',
PresetSpec: 'deployment preset composition is owned by packages/ui/permission/README.md',

View File

@@ -218,7 +218,6 @@ const SERVICE_ROLES: ServiceRole[] = [
pkg: 'session-reference',
title: 'Cross-session snapshot preparation',
mode: 'core',
consumers: ['tui'],
note: 'Projects bounded current-surface conversation snapshots into durable untrusted message context; host adapters own mention syntax.',
},
{
@@ -250,8 +249,7 @@ const SERVICE_ROLES: ServiceRole[] = [
pkg: 'user-interaction',
title: 'Human question/answer seam',
mode: 'seam',
implementations: ['tui'],
consumers: ['tool-ask-user', 'tui'],
consumers: ['tool-ask-user'],
note: 'UI front doors provide the active human-answer provider; tool-ask-user pauses a tool call on the provider-neutral ask() promise.',
},
{
@@ -266,8 +264,7 @@ const SERVICE_ROLES: ServiceRole[] = [
pkg: 'commands',
title: 'Human command registry',
mode: 'core',
consumers: ['tui'],
note: 'Plugins register direct human commands; TUI consumes the effective per-agent catalog without sending invocations to the model.',
note: 'Plugins register direct human commands without sending invocations to the model.',
},
{
key: 'sessionProjections',
@@ -285,13 +282,6 @@ const SERVICE_ROLES: ServiceRole[] = [
consumers: ['host-apiproxy'],
note: 'Durably checkpoints projection unit states per session (throttled + turn/end/detach mandatory points) and serves the cold-read ladder: cache row + persistence tail replay, so listings never load full logs.',
},
{
key: 'tui',
pkg: 'tui',
title: 'Mounted-terminal interaction service',
mode: 'bundle',
note: 'One TUI front door provides a FIFO overlay host; injected plugins receive caller-fiber ownership without access to pi-tui or terminal lifecycle state.',
},
{
key: 'skills',
pkg: 'skill',
@@ -306,7 +296,7 @@ const SERVICE_ROLES: ServiceRole[] = [
pkg: 'agent',
title: 'Agent service',
mode: 'core',
consumers: ['agent-loop', 'acp', 'cli-demo', 'subagent-inprocess', 'tui-demo'],
consumers: ['agent-loop', 'acp', 'cli-demo', 'subagent-inprocess'],
note: 'Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation.',
},
{
@@ -626,12 +616,12 @@ function stripYamlScalar(value: string): string {
const APP_EXAMPLES = [
{
id: 'tui',
id: 'dsh_base',
rel: 'apps/cli/composition.md',
title: 'TUI Agent App Composition',
label: 'apps/cli/config',
title: 'DSH Base Composition',
label: 'apps/cli/config/base.cordis.yml',
config: 'apps/cli/config/base.cordis.yml',
summary: 'The TUI surface combines the shared CLI base with its surface overlay and full-screen terminal package.',
summary: 'The raw CLI applies one required caller-selected patch list over this shared base; Web and headless apply their own shipped overlays.',
},
{
id: 'headless',
@@ -658,9 +648,7 @@ function renderAppExpansion(lines: string[], appNode: string, pluginName: string
const jsonl = nodeId('bundle', 'jsonl')
lines.push(` ${appNode} --> ${agentCore}["@deepseek-ai/dsh-agent-spine-demo"]`)
lines.push(` ${appNode} --> ${jsonl}["@deepseek-ai/dsh-session-persistence-jsonl"]`)
if (pluginName === '@deepseek-ai/dsh-tui-demo') {
lines.push(` ${appNode} --> ${nodeId('frontdoor', 'tui')}["@deepseek-ai/dsh-tui<br/>pre-created main agent"]`)
} else if (pluginName === '@deepseek-ai/dsh-cli-demo') {
if (pluginName === '@deepseek-ai/dsh-cli-demo') {
lines.push(` ${appNode} --> ${nodeId('frontdoor', 'cli')}["one-shot driver<br/>format-pure stdout<br/>fresh top-level agent"]`)
} else if (pluginName === '@deepseek-ai/dsh-acp-demo') {
lines.push(` ${appNode} --> ${nodeId('frontdoor', 'acp')}["@deepseek-ai/dsh-acp<br/>automation-only JSON-RPC stdio<br/>fresh sessions created by client"]`)
@@ -688,7 +676,7 @@ function renderAppComposition(example: AppExample): string {
const pluginNode = nodeId(`plugin_${example.id}`, plugin.id)
lines.push(` ${pluginNode}["${escLabel(plugin.id)}<br/>${escLabel(plugin.name)}"]`)
lines.push(` cfg --> ${pluginNode}`)
if (plugin.name === '@deepseek-ai/dsh-tui-demo' || plugin.name === '@deepseek-ai/dsh-cli-demo' || plugin.name === '@deepseek-ai/dsh-acp-demo') {
if (plugin.name === '@deepseek-ai/dsh-cli-demo' || plugin.name === '@deepseek-ai/dsh-acp-demo') {
renderAppExpansion(lines, pluginNode, plugin.name)
}
}
@@ -1303,8 +1291,8 @@ function renderDocs(): GraphDoc[] {
function renderIndex(docs: GraphDoc[]): string {
const labels: Record<string, string> = {
'docs/capability-seams.md': 'capability seams and core services',
'apps/cli/composition.md': 'dsh shared base composition',
'examples/headless-agent/composition.md': 'headless-agent app composition',
'examples/tui-agent/composition.md': 'tui-agent app composition',
'examples/cordis-agent/composition.md': 'cordis-agent app composition',
'examples/acp-agent/composition.md': 'acp-agent app composition',
'docs/event-producer-consumer.md': 'event producer/consumer matrix',
@@ -1313,8 +1301,8 @@ function renderIndex(docs: GraphDoc[]): string {
}
const modes: Record<string, string> = {
'docs/capability-seams.md': 'hybrid generated',
'apps/cli/composition.md': 'hybrid generated',
'examples/headless-agent/composition.md': 'hybrid generated',
'examples/tui-agent/composition.md': 'hybrid generated',
'examples/cordis-agent/composition.md': 'hybrid generated',
'examples/acp-agent/composition.md': 'hybrid generated',
'docs/event-producer-consumer.md': 'hybrid generated',

View File

@@ -1,7 +1,8 @@
import { readdirSync, readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { tmpdir } from 'node:os'
import { describe, expect, it } from 'vitest'
import { collectPythonDependencies, isPermissive, type Manifest, manifestPatterns, parsePyprojectRequirements, parseVendoredRows, render, tierExternalDeps } from './gen-third-party-notices.ts'
import { collectPythonDependencies, isPermissive, type Manifest, manifestPatterns, parsePyprojectRequirements, parseVendoredRows, render, tierExternalDeps, virtualManifest } from './gen-third-party-notices.ts'
const root = resolve(import.meta.dirname, '..')
@@ -63,6 +64,56 @@ describe('tierExternalDeps', () => {
})
})
describe('virtualManifest', () => {
it('resolves a manifest from an ordinary prefix-matching store directory', () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-notices-prefix-'))
try {
const name = '@scope/pkg'
const version = '1.0.0'
const store = join(root, 'store')
const manifestDir = join(store, `${name.replace('/', '+')}@${version}`, 'node_modules', name)
mkdirSync(manifestDir, { recursive: true })
writeFileSync(join(manifestDir, 'package.json'), JSON.stringify({ name, version, license: 'MIT' }))
expect(virtualManifest(store, name)).toMatchObject({ name, version, license: 'MIT' })
} finally {
rmSync(root, { recursive: true, force: true })
}
})
it('falls back to a content scan when pnpm 11 truncates the store directory name', () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-notices-truncated-'))
try {
const name = '@scope/pkg'
const version = '2.0.0'
const store = join(root, 'store')
// The truncated name no longer starts with `@scope+pkg@`, so only the
// whole-store content scan can find the package.
const manifestDir = join(store, '@scope+pkg_9f1c2d3e4a5b6c7d8e9f0a1b2c3d4e5f', 'node_modules', name)
mkdirSync(manifestDir, { recursive: true })
writeFileSync(join(manifestDir, 'package.json'), JSON.stringify({ name, version, license: 'Apache-2.0' }))
expect(virtualManifest(store, name)).toMatchObject({ name, version, license: 'Apache-2.0' })
} finally {
rmSync(root, { recursive: true, force: true })
}
})
it('returns undefined when neither the prefix nor the content scan finds the package', () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-notices-miss-'))
try {
const store = join(root, 'store')
const other = join(store, 'other-pkg@1.0.0', 'node_modules', 'other-pkg')
mkdirSync(other, { recursive: true })
writeFileSync(join(other, 'package.json'), JSON.stringify({ name: 'other-pkg', version: '1.0.0' }))
expect(virtualManifest(store, '@scope/missing')).toBeUndefined()
} finally {
rmSync(root, { recursive: true, force: true })
}
})
})
describe('parseVendoredRows', () => {
it('reads the committed vendor manifest table', () => {
const rows = parseVendoredRows(readFileSync(resolve(root, 'vendor/README.md'), 'utf8'))

View File

@@ -141,15 +141,22 @@ function workspaceMembers(rel: string): string[] {
return declared.map(member => String(member))
}
/** Every workspace manifest, keyed by path, plus the set of workspace package names. */
/**
* Every workspace manifest, keyed by repository-relative path, plus the set of
* workspace package names. Paths are normalized to `/` at ingestion: Node's
* `fs.globSync` returns OS-native separators, and the area matching in
* `tierExternalDeps` compares `/`-suffixed prefixes, so Windows backslashes
* would silently push dev-area manifests into the runtime tier.
*/
function loadWorkspaceManifests(): { manifests: Map<string, Manifest>; names: Set<string> } {
const patterns = manifestPatterns(workspaceMembers('pnpm-workspace.yaml'), workspaceMembers('native/landlock-run/pnpm-workspace.yaml'))
const manifests = new Map<string, Manifest>()
const names = new Set<string>()
for (const pattern of patterns) {
for (const path of globSync(pattern, { cwd: root })) {
const manifest = readManifest(path)
manifests.set(path, manifest)
const normalized = path.replaceAll('\\', '/')
const manifest = readManifest(normalized)
manifests.set(normalized, manifest)
if (manifest.name !== undefined) names.add(manifest.name)
}
}
@@ -157,6 +164,35 @@ function loadWorkspaceManifests(): { manifests: Map<string, Manifest>; names: Se
return { manifests, names }
}
type VirtualManifest = Manifest & { license?: string; repository?: string | { url?: string }; homepage?: string }
/**
* Resolve one package's manifest inside a pnpm virtual store. The prefix scan
* matches ordinary `@scope+name@version` directory names; pnpm 11 truncates
* long names (a peer-suffixed name past the length limit becomes
* `<prefix>_<hash>`), so a content scan falls back over the whole store when
* the prefix misses.
*
* @param virtual - the `.pnpm` virtual store directory to scan.
* @param name - the external package name, exactly as `node_modules` spells it.
* @returns the parsed manifest, or `undefined` when neither the prefix match
* nor the content scan finds the package's `package.json`.
*/
export function virtualManifest(virtual: string, name: string): VirtualManifest | undefined {
const prefix = `${name.replace('/', '+')}@`
const entry = readdirSync(virtual).find(dir => dir.startsWith(prefix))
if (entry !== undefined) {
return JSON.parse(readFileSync(resolve(virtual, entry, 'node_modules', name, 'package.json'), 'utf8')) as VirtualManifest
}
for (const dir of readdirSync(virtual)) {
const candidate = resolve(virtual, dir, 'node_modules', name, 'package.json')
if (existsSync(candidate)) {
return JSON.parse(readFileSync(candidate, 'utf8')) as VirtualManifest
}
}
return undefined
}
/** License and repository URL for an installed external package, from the pnpm store. */
function installedMetadata(name: string): { license: string; repo: string } {
const override = OVERRIDES[name]
@@ -171,11 +207,8 @@ function installedMetadata(name: string): { license: string; repo: string } {
}
const virtual = resolve(root, store, '.pnpm')
if (!existsSync(virtual)) continue
const prefix = `${name.replace('/', '+')}@`
const entry = readdirSync(virtual).find(dir => dir.startsWith(prefix))
if (entry === undefined) continue
manifest = JSON.parse(readFileSync(resolve(virtual, entry, 'node_modules', name, 'package.json'), 'utf8')) as typeof manifest
break
manifest = virtualManifest(virtual, name)
if (manifest !== undefined) break
}
const license = override?.license ?? manifest?.license
const rawRepo = typeof manifest?.repository === 'string' ? manifest.repository : manifest?.repository?.url ?? manifest?.homepage

View File

@@ -18,8 +18,6 @@ import SessionQuerySqlite from '@deepseek-ai/dsh-session-query-sqlite'
import GoalService from '@deepseek-ai/dsh-goal'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { type Config as ToolsConfig } from '@deepseek-ai/dsh-tools'
import { BashExecutor } from '@deepseek-ai/dsh-bash'
import type { BashExecRequest, BashExecSpec, BashProcess, BashRunResult } from '@deepseek-ai/dsh-bash'
import LocalBashExecutor from '@deepseek-ai/dsh-bash-local'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
@@ -60,44 +58,6 @@ import * as ToolWorkflow from '@deepseek-ai/dsh-tool-workflow'
const root = resolve(import.meta.dirname, '..')
const OUT = 'docs/tool-catalog.md'
const CATALOG_RG_PROBE_COMMAND = 'command -v rg >/dev/null 2>&1'
/**
* Minimal bash service for harvesting `dsh-tool-fs-search` schemas. The search
* plugin now probes `rg` at registration time, but the generated catalog must
* remain independent of the host PATH and never execute a real search.
*/
class CatalogSearchBashExecutor extends BashExecutor {
override resolve(request: BashExecRequest): BashExecSpec {
return {
command: request.command,
workdir: request.workdir ?? root,
timeoutMs: request.timeoutMs ?? 60_000,
stdoutMaxBytes: request.stdoutMaxBytes ?? 64_000,
signal: request.signal,
sandboxPolicy: request.sandboxPolicy,
}
}
override run(spec: BashExecSpec): Promise<BashRunResult> {
if (spec.command !== CATALOG_RG_PROBE_COMMAND) {
throw new Error(`gen-tool-catalog: unexpected search bash command during schema harvest: ${spec.command}`)
}
return Promise.resolve({
exitCode: 0,
signal: null,
timedOut: false,
aborted: false,
timeoutMs: spec.timeoutMs,
stdout: { text: '', truncated: false },
stderr: { text: '', truncated: false },
})
}
override start(): BashProcess {
throw new Error('gen-tool-catalog: search schema harvest must not start background processes')
}
}
/**
* Register the descriptor needed to mount schema-producing consumers. Declares
@@ -297,19 +257,19 @@ const TOOL_PACKAGES: ToolPackage[] = [
pkg: '@deepseek-ai/dsh-tool-fs-search',
dir: 'tool-fs-search',
source: 'packages/fs/tool-fs-search/src/index.ts',
requires: ['ctx.tools', 'ctx.bash', 'ctx.systemPrompt'],
requires: ['ctx.tools', 'ctx.subprocess', 'ctx.systemPrompt'],
writes: ['tool/call', 'tool/result'],
async mount(ctx) {
// The tools inject `bash` (search executes fixed `rg` commands through
// the executor seam, not ctx.fs). Use a catalog-only executor so the
// registration-time `rg` probe stays deterministic and the generator
// never depends on the host PATH. `ctx.spillStore` is optional (read via
// ctx.get) and does not affect the schemas, so no spill backend is mounted.
await ctx.plugin(CatalogSearchBashExecutor)
// The tools inject `subprocess` (search spawns the packaged ripgrep
// binary through the seam, not ctx.fs); registration itself never
// spawns, so the real local service is inert here. `ctx.spillStore` is
// optional (read via ctx.get) and does not affect the schemas, so no
// spill backend is mounted.
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(ToolFsSearch, { sampleOverCapGlobResults: true })
},
note:
'glob and grep are conditional bash-backed discovery tools: they register only when ctx.bash can find `rg`, then run fixed ripgrep commands through ctx.bash as ordinary foreground calls (never background tasks). The catalog uses `sampleOverCapGlobResults: true`; deployments must choose that behavior explicitly. Capped results save the complete formatted list through the optional ctx.spillStore backend; returned locators are follow-up-readable/searchable when the backend exposes local paths in co-located deployments.',
'glob and grep are unconditional discovery tools that spawn the packaged ripgrep binary (`@vscode/ripgrep`) through ctx.subprocess as ordinary foreground calls (never background tasks) — no host `rg` install and no shell layer. The catalog uses `sampleOverCapGlobResults: true`; deployments must choose that behavior explicitly. Capped results save the complete formatted list through the optional ctx.spillStore backend; returned locators are follow-up-readable/searchable when the backend exposes local paths in co-located deployments.',
},
{
pkg: '@deepseek-ai/dsh-tool-pty',

View File

@@ -10,9 +10,8 @@
# `pnpm install`, points the stable `~/.dsh/source/current` symlink
# at that staging worktree and symlinks `dsh` onto PATH at `current/bin/dsh`,
# records your API credentials in the Harness home (`~/.dsh`) dsh reads at boot,
# and lets you launch the Web UI or TUI. The Web choice builds the repository
# artifacts first; the TUI runs directly from TypeScript source through the
# repo's own tsx. Keeping every checkout under ~/.dsh/source keeps successive
# builds the repository artifacts, and launches the Web UI. Keeping every
# checkout under ~/.dsh/source keeps successive
# upgrades in one place instead of scattered sibling clones, and lets staging
# worktrees share the master clone's object store. The PATH symlink resolves through
# `current`, so an upgrade repoints one stable symlink instead of relinking PATH:
@@ -414,33 +413,15 @@ if [ "${SKIP_CREDS:-0}" != 1 ]; then
fi
fi
# --- 6. choose and launch an interface -----------------------------------------
# --- 6. build and launch the Web interface -------------------------------------
step "Done"
if [ "$HAS_TTY" = 1 ]; then
printf ' 1) Web UI (recommended)\n'
printf ' 2) TUI\n'
while :; do
LAUNCH_INTERFACE=$(ask "Choose an interface [1/2]:" 1)
case "$LAUNCH_INTERFACE" in
1|web|Web|WEB)
step "Building DeepSeek Harness for Web UI"
( cd "$DSH_STAGING" && pnpm run build )
info "launching Web UI — run 'dsh web' anytime to start again"
exec "$DSH_BIN_DIR/dsh" web </dev/tty
;;
2|tui|Tui|TUI)
info "launching TUI — run 'dsh' anytime to start again"
exec "$DSH_BIN_DIR/dsh" </dev/tty
;;
*)
warn "choose 1 for Web UI or 2 for TUI"
;;
esac
done
step "Building DeepSeek Harness for Web UI"
( cd "$DSH_STAGING" && pnpm run build )
info "launching Web UI — run 'dsh web' anytime to start again"
exec "$DSH_BIN_DIR/dsh" web </dev/tty
else
info "install complete. Build and start the Web UI with:"
printf ' (cd %s && pnpm run build)\n' "$DSH_STAGING"
printf ' %s web\n' "$DSH_BIN_DIR/dsh"
info "or start the TUI with:"
printf ' %s\n' "$DSH_BIN_DIR/dsh"
fi

View File

@@ -595,7 +595,7 @@ function builtBinSmokeGate(needs: string[] = ['build']): Gate {
'--config',
'vitest.e2e.config.ts',
'examples/headless-agent/tests/keyless-smoke.e2e.ts',
'apps/cli/tests/tui-keyless-smoke.e2e.ts',
'apps/cli/tests/built-bin.e2e.ts',
'packages/examples/cli-demo/tests/built-bin.e2e.ts',
'packages/examples/acp-demo/tests/built-bin.e2e.ts',
'packages/ui/jsonrpc/tests/built-scope-carrier.e2e.ts',

File diff suppressed because one or more lines are too long