mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
feat(host): directory-picker capability seam with dialog and browse backends
The web GUI's folder picking was hardwired to one interaction: a native OS chooser compiled into the gateway, unusable for remote deployments and swappable only by editing apiproxy source. Directory picking becomes a three-package capability seam in packages/host: ctx.directoryPicker returns a discriminated capability — dialog (the extracted native chooser; host-display only) or browse (new: one-level listing + child creation over Node stdlib, hidden flags host-stamped, symlinks followed, ancestry crumbs; remote-capable). The gateway injects the seam, advertises the kind via host.describe.directoryPicker, serves host.listDirectory / host.createDirectory under browse, and answers directory-picker-unavailable across kinds. cordis.yml is the swap point; apps/cli keeps dialog mounted, so behavior is unchanged until the in-app browser PR flips the default. The connection fixture serves a deterministic browse tree; WorkspacesService gains the browse calls the browser UI will drive. Decision record: .agents/notes/implemented/architecture/2026-07-28-directory-picker-capability-seam.md
This commit is contained in:
@@ -39,7 +39,7 @@ import type {
|
||||
AskUserQuestionAnswer, AskUserQuestionItem, AskUserQuestionRequest,
|
||||
} from '@deepseek-ai/dsh-user-interaction'
|
||||
import { UserInteractionError } from '@deepseek-ai/dsh-user-interaction'
|
||||
import { pickNativeDirectory } from './native-directory-picker.ts'
|
||||
import { DirectoryPickerError } from '@deepseek-ai/dsh-host-directory-picker'
|
||||
|
||||
/** Page size when history is called without maxMessages. */
|
||||
const DEFAULT_MAX_MESSAGES = 50
|
||||
@@ -193,6 +193,14 @@ async function summarizeCold(persistence: SessionPersistence, meta: SessionHeade
|
||||
}
|
||||
}
|
||||
|
||||
/** Map a browse-primitive failure onto the wire error vocabulary (unknown throws stay internal). */
|
||||
function directoryError(error: unknown): RpcError {
|
||||
if (error instanceof DirectoryPickerError) {
|
||||
return { code: error.code, message: error.message, details: { path: error.path } }
|
||||
}
|
||||
return { code: 'internal', message: error instanceof Error ? error.message : String(error), details: {} }
|
||||
}
|
||||
|
||||
/** Resolved Host routing and project-directory defaults consumed by the API implementation. */
|
||||
export interface ApiProxyDefaults {
|
||||
provider: string
|
||||
@@ -201,8 +209,6 @@ export interface ApiProxyDefaults {
|
||||
cwd: string
|
||||
/** Parent directory for name-created workspaces. */
|
||||
workspaceRoot: string
|
||||
/** Native single-directory picker; injectable for carrier tests. */
|
||||
pickDirectory?: (signal: AbortSignal) => Promise<string | null>
|
||||
}
|
||||
|
||||
/** The tool/call payload fields the presenter path reads. */
|
||||
@@ -990,12 +996,21 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
||||
provider: defaults.provider,
|
||||
model: defaults.model,
|
||||
attachedSessions: ctx.agents.list().length,
|
||||
directoryPicker: ctx.directoryPicker.capability().kind,
|
||||
}))
|
||||
},
|
||||
|
||||
async pickDirectory(request, signal) {
|
||||
const capability = ctx.directoryPicker.capability()
|
||||
if (capability.kind !== 'dialog') {
|
||||
return err(request, {
|
||||
code: 'directory-picker-unavailable',
|
||||
message: `host.pickDirectory needs the dialog capability; the composed picker serves "${capability.kind}"`,
|
||||
details: { capability: capability.kind },
|
||||
})
|
||||
}
|
||||
try {
|
||||
const path = await (defaults.pickDirectory ?? pickNativeDirectory)(signal)
|
||||
const path = await capability.pick(signal)
|
||||
return ok(request, { path })
|
||||
} catch (error: unknown) {
|
||||
if (signal.aborted) {
|
||||
@@ -1012,6 +1027,38 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
async listDirectory(request) {
|
||||
const capability = ctx.directoryPicker.capability()
|
||||
if (capability.kind !== 'browse') {
|
||||
return err(request, {
|
||||
code: 'directory-picker-unavailable',
|
||||
message: `host.listDirectory needs the browse capability; the composed picker serves "${capability.kind}"`,
|
||||
details: { capability: capability.kind },
|
||||
})
|
||||
}
|
||||
try {
|
||||
return ok(request, await capability.list(request.payload.path))
|
||||
} catch (error: unknown) {
|
||||
return err(request, directoryError(error))
|
||||
}
|
||||
},
|
||||
|
||||
async createDirectory(request) {
|
||||
const capability = ctx.directoryPicker.capability()
|
||||
if (capability.kind !== 'browse') {
|
||||
return err(request, {
|
||||
code: 'directory-picker-unavailable',
|
||||
message: `host.createDirectory needs the browse capability; the composed picker serves "${capability.kind}"`,
|
||||
details: { capability: capability.kind },
|
||||
})
|
||||
}
|
||||
try {
|
||||
return ok(request, { path: await capability.createDirectory(request.payload.path, request.payload.name) })
|
||||
} catch (error: unknown) {
|
||||
return err(request, directoryError(error))
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
commands: {
|
||||
|
||||
Reference in New Issue
Block a user