diff --git a/packages/api/session-controller/tests/fake-api.client.ts b/packages/api/session-controller/tests/fake-api.client.ts index 1625ac65e7..98d7196358 100644 --- a/packages/api/session-controller/tests/fake-api.client.ts +++ b/packages/api/session-controller/tests/fake-api.client.ts @@ -167,23 +167,9 @@ export class FakeApiClient implements IApiClient { () => Promise.resolve(ok({ version: '0-fake', cwd: '/f', attachedSessions: 0, home: '/h', canOpenPath: true, })) - onPickDirectory: (payload: unknown) => Promise> = - () => Promise.resolve(ok({ path: null })) onOpenPath: (payload: unknown) => Promise> = () => Promise.resolve(ok({ opened: true as const })) - onListDirectory: (payload: unknown) => Promise> = - () => Promise.resolve(ok({ path: '/home/fake', home: '/home/fake', crumbs: [{ name: '/', path: '/', hidden: false }], entries: [], truncated: false })) - - onCreateDirectory: (payload: unknown) => Promise> = - () => Promise.resolve(ok({ path: '/home/fake/new' })) - private readonly followConns = new Map[]>() private readonly controlConns: ValueStreamConn[] = [] private readonly workspaceConns: ValueStreamConn[] = [] @@ -210,9 +196,6 @@ export class FakeApiClient implements IApiClient { readonly host: IApiClient['host'] = { describe: (payload: unknown) => this.record('host.describe', payload, this.onDescribe(payload)), - pickDirectory: (payload: unknown) => this.record('host.pickDirectory', payload, this.onPickDirectory(payload)), - listDirectory: (payload: unknown) => this.record('host.listDirectory', payload, this.onListDirectory(payload)), - createDirectory: (payload: unknown) => this.record('host.createDirectory', payload, this.onCreateDirectory(payload)), openPath: (payload: unknown) => this.record('host.openPath', payload, this.onOpenPath(payload)), } diff --git a/packages/client/connection/src/client/api.ts b/packages/client/connection/src/client/api.ts index 3a3c78dfa5..113e72212c 100644 --- a/packages/client/connection/src/client/api.ts +++ b/packages/client/connection/src/client/api.ts @@ -7,7 +7,6 @@ export type { ApiProxy, HostApi, - DirectoryEntry, DirectoryListing, ResponseValue, SkillsApi, SkillEntry, ModelCatalog, ModelCatalogFailure, ModelCatalogModel, ModelProviderGroup, ModelReasoning, diff --git a/packages/client/connection/src/client/fixture.ts b/packages/client/connection/src/client/fixture.ts index 0e0eab03e5..c3bb6b503d 100644 --- a/packages/client/connection/src/client/fixture.ts +++ b/packages/client/connection/src/client/fixture.ts @@ -3314,42 +3314,6 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld { describe: request => ok(request, { version: '0.0.0-fixture', cwd: '/tmp/fixture', attachedSessions, home: FIXTURE_HOME, canOpenPath: true, }), - // Deterministic native pick: the keyless lanes drive the full - // pick-then-adopt path without an OS chooser (design-mock content, - // same tree the browse primitives serve). - pickDirectory: request => ok(request, { path: `${FIXTURE_HOME}/Documents/project` }), - listDirectory: (request) => { - const target = request.payload.path ?? FIXTURE_HOME - const children = childrenOf(target) - if (children === undefined) { - return err(request, { code: 'directory-unreadable', message: `cannot list ${target}: not in the fixture tree`, details: { path: target } }) - } - return ok(request, { - path: target, - home: FIXTURE_HOME, - crumbs: crumbsOf(target), - entries: [...children].sort((a, b) => a.localeCompare(b)) - .map(name => ({ name, path: target === '/' ? `/${name}` : `${target}/${name}`, hidden: name.startsWith('.') })), - // The fixture tree is tiny; no level ever reaches a backend bound. - truncated: false, - }) - }, - createDirectory: (request) => { - const parent = request.payload.path - const children = childrenOf(parent) - if (children === undefined) { - return err(request, { code: 'directory-create-failed', message: `missing parent ${parent}`, details: { path: parent } }) - } - // Same root special case as listDirectory's entry paths: a plain join - // under '/' would mint '//name' and fork the tree's identity. - const target = parent === '/' ? `/${request.payload.name}` : `${parent}/${request.payload.name}` - if (children.includes(request.payload.name)) { - return err(request, { code: 'directory-exists', message: `${target} already exists`, details: { path: target } }) - } - directoryTree.set(parent, [...children, request.payload.name]) - directoryTree.set(target, []) - return ok(request, { path: target }) - }, openPath: request => ok(request, { opened: true as const }), }, agentPresets: { @@ -3652,9 +3616,6 @@ export class FixtureApiClient extends AbstractApiClient { ): Promise> { switch (method) { case 'host.describe': return this.api.host.describe(request) - case 'host.pickDirectory': return this.api.host.pickDirectory(request, new AbortController().signal) - case 'host.listDirectory': return this.api.host.listDirectory(request, new AbortController().signal) - case 'host.createDirectory': return this.api.host.createDirectory(request) case 'host.openPath': return this.api.host.openPath(request, new AbortController().signal) case 'skill.list': return this.api.skills.list(request) case 'agentPreset.openDocument': return this.api.agentPresets.openDocument(request, new AbortController().signal) diff --git a/packages/client/connection/src/client/index.ts b/packages/client/connection/src/client/index.ts index 358b2f41cc..8136254f1c 100644 --- a/packages/client/connection/src/client/index.ts +++ b/packages/client/connection/src/client/index.ts @@ -31,7 +31,6 @@ declare module '@deepseek-ai/cordis' { // ---- Contract re-exports (browser-safe apiproxy channels + core types) ---- export type { ApiProxy, HostApi, - DirectoryEntry, DirectoryListing, SkillsApi, SkillEntry, ModelCatalog, ModelCatalogFailure, ModelCatalogModel, ModelProviderGroup, ModelReasoning, MessageId, ModelReasoningEffort, ModelSelection, diff --git a/packages/client/connection/tests/fake-api.client.ts b/packages/client/connection/tests/fake-api.client.ts index 93ab3af3b0..c4bd0d03ff 100644 --- a/packages/client/connection/tests/fake-api.client.ts +++ b/packages/client/connection/tests/fake-api.client.ts @@ -50,30 +50,13 @@ export class FakeApiClient implements IApiClient { () => Promise.resolve(ok({ version: '0-fake', cwd: '/f', attachedSessions: 0, home: '/h', canOpenPath: true, })) - onPickDirectory: (payload: unknown) => Promise> = - () => Promise.resolve(ok({ path: null })) onOpenPath: (payload: unknown) => Promise> = () => Promise.resolve(ok({ opened: true as const })) - onListDirectory: (payload: unknown) => Promise> = - () => Promise.resolve(ok({ path: '/home/fake', home: '/home/fake', crumbs: [{ name: '/', path: '/', hidden: false }], entries: [], truncated: false })) - - onCreateDirectory: (payload: unknown) => Promise> = - () => Promise.resolve(ok({ path: '/home/fake/new' })) - private readonly generationConns: StreamConn[] = [] readonly host: IApiClient['host'] = { describe: payload => this.record('host.describe', payload, this.onDescribe(payload)), - pickDirectory: payload => this.record('host.pickDirectory', payload, this.onPickDirectory(payload)), - listDirectory: payload => this.record('host.listDirectory', payload, this.onListDirectory(payload)), - createDirectory: payload => this.record('host.createDirectory', payload, this.onCreateDirectory(payload)), openPath: payload => this.record('host.openPath', payload, this.onOpenPath(payload)), } diff --git a/packages/client/connection/tests/node-half.host.spec.ts b/packages/client/connection/tests/node-half.host.spec.ts index e1cb7abe83..1cead76b09 100644 --- a/packages/client/connection/tests/node-half.host.spec.ts +++ b/packages/client/connection/tests/node-half.host.spec.ts @@ -174,7 +174,7 @@ describe('connection node half', () => { it('requires the same browser session for every method on every trusted authority', async () => { const { routes, connection, dispose } = await mounted({ trustedHosts: ['harness.example'] }) const methods = [ - 'host.pickDirectory', 'host.openPath', + 'host.openPath', 'settings.describe', 'settings.update', 'credentials.describe', 'credentials.set', 'llm.discoverModels', 'llm.models', 'agentPreset.openDocument', ] @@ -503,7 +503,7 @@ describe('connection node half over a real HTTP server', () => { const methods = [ 'settings.describe', 'settings.openDocument', 'settings.update', 'settings.replace', 'settings.mutate', 'credentials.describe', 'credentials.set', 'credentials.unset', - 'host.pickDirectory', 'host.openPath', + 'host.openPath', 'llm.discoverModels', 'agentPreset.openDocument', 'llm.providers', 'llm.models', diff --git a/packages/host/apiproxy/src/api-proxy.ts b/packages/host/apiproxy/src/api-proxy.ts index 3c3f8210d6..fc94e58035 100644 --- a/packages/host/apiproxy/src/api-proxy.ts +++ b/packages/host/apiproxy/src/api-proxy.ts @@ -41,7 +41,6 @@ import type { SettingsDescriptor, SettingsNamespace, SettingsPathOp } from '@dee import { credentialRef } from '@deepseek-ai/dsh-credentials' import type { ScopeKey } from '@deepseek-ai/dsh-scope' import type { RpcError, RpcRequest, RpcResponse } from './api/rpc.ts' -import { DirectoryPickerError } from '@deepseek-ai/dsh-host-directory-picker' import { canOpenNativePath, openNativePath, openNativeTextFile } from './native-path-opener.ts' /** Read live abort state across awaits without treating it as synchronously immutable. */ @@ -59,14 +58,6 @@ function err(request: RpcRequest, error: RpcError): RpcResponse { return { rpcId: request.rpcId, result: { ok: false, error } } } -/** 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: {} } -} - /** Deployment metadata and Host integrations consumed by the API implementation. */ export interface ApiProxyDefaults { /** Current deployment model selection reported by `host.describe`. */ @@ -290,72 +281,6 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro })) }, - async pickDirectory(request, signal) { - const capability = ctx.directoryPicker.capability() - if (capability.kind !== 'native') { - return err(request, { - code: 'directory-picker-unavailable', - message: `host.pickDirectory needs the native capability; the composed picker serves "${capability.kind}"`, - details: { capability: capability.kind }, - }) - } - try { - const path = await capability.pick(signal) - return ok(request, { path }) - } catch (error: unknown) { - if (signal.aborted) { - return err(request, { - code: 'cancelled', - message: 'directory picker was aborted', - details: {}, - }) - } - return err(request, { - code: 'internal', - message: `directory picker failed: ${error instanceof Error ? error.message : String(error)}`, - details: {}, - }) - } - }, - - async listDirectory(request, signal) { - 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 { - // The carrier's signal follows the caller: a disconnect or timeout - // stops the backend's directory scan instead of outliving it. - return ok(request, await capability.list(request.payload.path, signal)) - } catch (error: unknown) { - // An abort is the caller's own timeout/disconnect, not a server failure. - if (signal.aborted) { - return err(request, { code: 'cancelled', message: 'directory listing was aborted', details: {} }) - } - 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)) - } - }, - async openPath(request, signal) { return openPath(request, request.payload.path, signal) }, diff --git a/packages/host/apiproxy/src/api/host.schema.ts b/packages/host/apiproxy/src/api/host.schema.ts index 1a520f1e30..6735e55639 100644 --- a/packages/host/apiproxy/src/api/host.schema.ts +++ b/packages/host/apiproxy/src/api/host.schema.ts @@ -3,7 +3,6 @@ */ import { z } from 'zod' -import type { DirectoryEntry } from './host.ts' import type { RequestPayload, ResponseValue } from './rpc-map.ts' import type { Wire } from './rpc.schema.ts' @@ -21,49 +20,6 @@ export const hostDescribeValueSchema = z.object({ canOpenPath: z.boolean(), }) satisfies z.ZodType>> -/** host.pickDirectory request payload (empty object literal). */ -export const hostPickDirectoryRequestSchema = z.object({}) satisfies z.ZodType>> - -/** host.pickDirectory response value; null means the user cancelled. */ -export const hostPickDirectoryValueSchema = z.object({ - path: z.string().nullable(), -}) satisfies z.ZodType>> - -/** Directory row shared by listing entries and breadcrumb crumbs. */ -export const directoryEntrySchema = z.object({ - name: z.string(), - path: z.string(), - hidden: z.boolean(), -}) satisfies z.ZodType> - -/** host.listDirectory request payload; an absent path lists the home directory. */ -export const hostListDirectoryRequestSchema = z.object({ - path: z.string().optional(), -}) satisfies z.ZodType>> - -/** host.listDirectory response value. */ -export const hostListDirectoryValueSchema = z.object({ - path: z.string(), - home: z.string(), - crumbs: z.array(directoryEntrySchema), - entries: z.array(directoryEntrySchema), - truncated: z.boolean(), -}) satisfies z.ZodType>> - -/** host.createDirectory request payload: name must be one plain path segment. */ -export const hostCreateDirectoryRequestSchema = z.object({ - path: z.string(), - name: z.string(), -}).refine( - payload => payload.name.trim() !== '' && payload.name !== '.' && payload.name !== '..' - && !/[/\\]/.test(payload.name), - { message: 'host.createDirectory requires a single non-blank path segment name' }, -) satisfies z.ZodType>> - -/** host.createDirectory response value: the created directory's absolute path. */ -export const hostCreateDirectoryValueSchema = z.object({ - path: z.string(), -}) satisfies z.ZodType>> /** host.openPath request payload. */ export const hostOpenPathRequestSchema = z.object({ path: z.string().min(1), diff --git a/packages/host/apiproxy/src/api/host.ts b/packages/host/apiproxy/src/api/host.ts index e700b86db3..ceb7998d2b 100644 --- a/packages/host/apiproxy/src/api/host.ts +++ b/packages/host/apiproxy/src/api/host.ts @@ -5,33 +5,6 @@ import type { RpcRequest, RpcResponse } from './rpc.ts' -/** One directory row of a listing: a child entry or a breadcrumb ancestor. */ -export interface DirectoryEntry { - /** Base name shown in a browser row (a root crumb carries its full path). */ - name: string - /** Absolute host path — the client never joins path segments itself. */ - path: string - /** Hidden by the host platform's convention (dot-prefixed on POSIX); the client owns whether to show it. */ - hidden: boolean -} - -/** host.listDirectory response value: one directory level plus its ancestry. */ -export interface DirectoryListing { - /** Absolute path of the listed directory. */ - path: string - /** The host account's home directory (breadcrumb "Home" rooting). */ - home: string - /** - * Ancestor chain from the filesystem root to the listed directory - * inclusive; every crumb is a jump target (crumb `hidden` is always false). - */ - crumbs: DirectoryEntry[] - /** Direct child directories, name-sorted; symlinks to directories included. */ - entries: DirectoryEntry[] - /** True when the backend cut `entries` at its complete-result bound (the name-sorted tail is absent). */ - truncated: boolean -} - /** Host-level unary methods. */ export interface HostApi { /** @@ -54,37 +27,6 @@ export interface HostApi { canOpenPath: boolean }>> - /** - * Open the operating system's single-directory picker; cancellation returns - * null. Only served under the `native` capability. - */ - pickDirectory( - request: RpcRequest<{}>, - signal: AbortSignal, - ): Promise> - - /** - * List one directory level for the in-app browser; an absent path lists the - * host account's home directory. Only served under the `browse` capability; - * unreadable or missing targets fail with `directory-unreadable`. The - * carrier's request signal follows the caller, stopping the backend's scan - * on disconnect or timeout. - */ - listDirectory( - request: RpcRequest<{ path?: string }>, - signal: AbortSignal, - ): Promise> - - /** - * Create one child directory under an existing parent (the browser's - * "New folder"). Only served under the `browse` capability; an existing - * child fails with `directory-exists`, every other filesystem failure with - * `directory-create-failed`. - */ - createDirectory( - request: RpcRequest<{ path: string; name: string }>, - ): Promise> - /** * Open a filesystem path with the operating system's default application * (Finder / Explorer / xdg-open hand-off). The browser carrier's diff --git a/packages/host/apiproxy/src/api/index.ts b/packages/host/apiproxy/src/api/index.ts index 434c362e1a..b151677137 100644 --- a/packages/host/apiproxy/src/api/index.ts +++ b/packages/host/apiproxy/src/api/index.ts @@ -29,7 +29,7 @@ export type { ModelCatalog, ModelCatalogFailure, ModelCatalogModel, ModelProviderGroup, ModelReasoning, ModelReasoningEffort, ModelSelection, } from '@deepseek-ai/dsh-api-session-controller/types' -export type { DirectoryEntry, DirectoryListing, HostApi } from './host.ts' +export type { HostApi } from './host.ts' export type { SkillsApi, SkillEntry } from './skills.ts' export type { AgentPresetsApi } from './agent-presets.ts' export type { SettingsApi, SettingsNamespaceView, SettingsPathOpView, SettingsSecretView } from './settings.ts' diff --git a/packages/host/apiproxy/src/api/rpc-map.ts b/packages/host/apiproxy/src/api/rpc-map.ts index d636963e82..2590b4cec7 100644 --- a/packages/host/apiproxy/src/api/rpc-map.ts +++ b/packages/host/apiproxy/src/api/rpc-map.ts @@ -18,9 +18,6 @@ import type { RpcResponse } from './rpc.ts' */ export interface RpcMethodMap { 'host.describe': HostApi['describe'] - 'host.pickDirectory': HostApi['pickDirectory'] - 'host.listDirectory': HostApi['listDirectory'] - 'host.createDirectory': HostApi['createDirectory'] 'host.openPath': HostApi['openPath'] 'skill.list': SkillsApi['list'] 'agentPreset.openDocument': AgentPresetsApi['openDocument'] diff --git a/packages/host/apiproxy/src/api/rpc.schema.ts b/packages/host/apiproxy/src/api/rpc.schema.ts index 498c7c3a1f..57ca87bcf3 100644 --- a/packages/host/apiproxy/src/api/rpc.schema.ts +++ b/packages/host/apiproxy/src/api/rpc.schema.ts @@ -36,10 +36,6 @@ export const rpcErrorSchema: z.ZodType = z.discriminatedUnion('code', z.object({ code: z.literal('cancelled'), message: z.string(), details: z.object({}) }), z.object({ code: z.literal('session-not-found'), message: z.string(), details: z.object({ sessionId: z.string() }) }), z.object({ code: z.literal('invalid-time-zone'), message: z.string(), details: z.object({ value: z.string() }) }), - z.object({ code: z.literal('directory-unreadable'), message: z.string(), details: z.object({ path: z.string() }) }), - z.object({ code: z.literal('directory-exists'), message: z.string(), details: z.object({ path: z.string() }) }), - z.object({ code: z.literal('directory-create-failed'), message: z.string(), details: z.object({ path: z.string() }) }), - z.object({ code: z.literal('directory-picker-unavailable'), message: z.string(), details: z.object({ capability: z.string() }) }), z.object({ code: z.literal('agent-preset-read-only'), message: z.string(), details: z.object({ agentPreset: z.string(), reason: z.string() }) }), z.object({ code: z.literal('agent-preset-locked'), message: z.string(), details: z.object({ sessionId: z.string(), agentPreset: z.string() }) }), z.object({ code: z.literal('agent-preset-not-found'), message: z.string(), details: z.object({ agentPreset: z.string(), available: z.array(z.string()) }) }), diff --git a/packages/host/apiproxy/src/api/rpc.ts b/packages/host/apiproxy/src/api/rpc.ts index b154a5e14a..7448ef669c 100644 --- a/packages/host/apiproxy/src/api/rpc.ts +++ b/packages/host/apiproxy/src/api/rpc.ts @@ -31,10 +31,6 @@ export interface RpcErrorDetailsMap { 'cancelled': {} 'session-not-found': { sessionId: SessionId } 'invalid-time-zone': { value: string } - 'directory-unreadable': { path: string } - 'directory-exists': { path: string } - 'directory-create-failed': { path: string } - 'directory-picker-unavailable': { capability: string } 'agent-preset-read-only': { agentPreset: string; reason: string } 'agent-preset-locked': { sessionId: SessionId; agentPreset: string } 'agent-preset-not-found': { agentPreset: string; available: readonly string[] } diff --git a/packages/host/apiproxy/src/fetch/client.ts b/packages/host/apiproxy/src/fetch/client.ts index f65d5fafff..4a567f8638 100644 --- a/packages/host/apiproxy/src/fetch/client.ts +++ b/packages/host/apiproxy/src/fetch/client.ts @@ -13,8 +13,7 @@ import { RpcId } from '../api/rpc.ts' import type { Wire } from '../api/rpc.schema.ts' import { serverResponseSchema } from '../api/rpc.schema.ts' import { - hostCreateDirectoryValueSchema, hostDescribeValueSchema, - hostListDirectoryValueSchema, hostOpenPathValueSchema, hostPickDirectoryValueSchema, + hostDescribeValueSchema, hostOpenPathValueSchema, } from '../api/host.schema.ts' import { skillListValueSchema } from '../api/skills.schema.ts' import { @@ -44,9 +43,6 @@ import { llmDiscoverModelsValueSchema, llmModelsValueSchema, llmProvidersValueSc export interface IApiClient { host: { describe(payload: RequestPayload<'host.describe'>, signal?: AbortSignal): Promise>> - pickDirectory(payload: RequestPayload<'host.pickDirectory'>, signal?: AbortSignal): Promise>> - listDirectory(payload: RequestPayload<'host.listDirectory'>, signal?: AbortSignal): Promise>> - createDirectory(payload: RequestPayload<'host.createDirectory'>, signal?: AbortSignal): Promise>> openPath(payload: RequestPayload<'host.openPath'>, signal?: AbortSignal): Promise>> } skills: { @@ -80,9 +76,6 @@ export interface IApiClient { */ const UNARY_VALUE_SCHEMAS: { [K in keyof RpcMethodMap]: z.ZodType>> } = { 'host.describe': hostDescribeValueSchema, - 'host.pickDirectory': hostPickDirectoryValueSchema, - 'host.listDirectory': hostListDirectoryValueSchema, - 'host.createDirectory': hostCreateDirectoryValueSchema, 'host.openPath': hostOpenPathValueSchema, 'skill.list': skillListValueSchema, 'agentPreset.openDocument': agentPresetOpenDocumentValueSchema, @@ -102,9 +95,6 @@ const UNARY_VALUE_SCHEMAS: { [K in keyof RpcMethodMap]: z.ZodType void>() - /** @param timeoutMs - timeout for bounded unary calls; user-paced calls do not use it. */ + /** @param timeoutMs - timeout for unary calls. */ constructor(protected readonly timeoutMs: number = DEFAULT_TIMEOUT_MS) {} /** Transport aspect: browser fetch, injected handler.fetch, IPC bridge, ... */ @@ -178,24 +168,21 @@ export abstract class AbstractApiClient implements IApiClient { /** * Shared POST leg of unary calls: JSON body, - * optional default timeout merged with the caller's external signal, non-2xx → transport throw. + * default timeout merged with the caller's external signal, non-2xx → transport throw. */ private async postJson( path: string, body: ClientRequest, signal: AbortSignal | undefined, - timeoutPolicy: UnaryTimeoutPolicy = 'default', ): Promise { - const requestSignal = timeoutPolicy === 'default' - ? signal === undefined - ? AbortSignal.timeout(this.timeoutMs) - : AbortSignal.any([AbortSignal.timeout(this.timeoutMs), signal]) - : signal + const requestSignal = signal === undefined + ? AbortSignal.timeout(this.timeoutMs) + : AbortSignal.any([AbortSignal.timeout(this.timeoutMs), signal]) const response = await this.doFetch(new URL(path, this.resolveBase()), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), - ...requestSignal === undefined ? {} : { signal: requestSignal }, + signal: requestSignal, }) if (!response.ok) throw new Error(`transport failure for ${path}: HTTP ${response.status}`) return response @@ -210,11 +197,10 @@ export abstract class AbstractApiClient implements IApiClient { method: K, payload: RequestPayload, signal?: AbortSignal, - timeoutPolicy: UnaryTimeoutPolicy = 'default', ): Promise>> { const message: ClientRequest = { type: 'client-request', rpcId: this.mintRpcId(), method, payload } this.onEnvelope(message) - const response = await this.postJson(`/api/${method}`, message, signal, timeoutPolicy) + const response = await this.postJson(`/api/${method}`, message, signal) const full = serverResponseSchema.parse(await response.json()) this.onEnvelope(full) if (full.rpcId !== message.rpcId) throw new Error(`rpcId mismatch for ${method}: sent ${message.rpcId}, got ${full.rpcId}`) @@ -229,13 +215,6 @@ export abstract class AbstractApiClient implements IApiClient { readonly host: IApiClient['host'] = { describe: (payload, signal) => this.callUnary('host.describe', payload, signal), - // A native system dialog is user-paced and may legitimately stay open - // longer than the normal unary deadline. Caller/connection aborts remain. - pickDirectory: (payload, signal) => this.callUnary( - 'host.pickDirectory', payload, signal, 'caller-signal-only', - ), - listDirectory: (payload, signal) => this.callUnary('host.listDirectory', payload, signal), - createDirectory: (payload, signal) => this.callUnary('host.createDirectory', payload, signal), openPath: (payload, signal) => this.callUnary('host.openPath', payload, signal), } diff --git a/packages/host/apiproxy/src/fetch/handler.ts b/packages/host/apiproxy/src/fetch/handler.ts index 6b3a7d3b13..d4f3ea58ed 100644 --- a/packages/host/apiproxy/src/fetch/handler.ts +++ b/packages/host/apiproxy/src/fetch/handler.ts @@ -15,9 +15,7 @@ import { RpcId } from '../api/rpc.ts' import type { Wire } from '../api/rpc.schema.ts' import { clientRequestSchema } from '../api/rpc.schema.ts' import { - hostCreateDirectoryRequestSchema, hostDescribeRequestSchema, - hostListDirectoryRequestSchema, hostOpenPathRequestSchema, - hostPickDirectoryRequestSchema, + hostDescribeRequestSchema, hostOpenPathRequestSchema, } from '../api/host.schema.ts' import { skillListRequestSchema } from '../api/skills.schema.ts' import { @@ -50,9 +48,6 @@ type UnaryRoutes = { const UNARY_ROUTES: UnaryRoutes = { 'host.describe': { schema: hostDescribeRequestSchema, invoke: (api, r) => api.host.describe(r) }, - 'host.pickDirectory': { schema: hostPickDirectoryRequestSchema, invoke: (api, r, signal) => api.host.pickDirectory(r, signal) }, - 'host.listDirectory': { schema: hostListDirectoryRequestSchema, invoke: (api, r, signal) => api.host.listDirectory(r, signal) }, - 'host.createDirectory': { schema: hostCreateDirectoryRequestSchema, invoke: (api, r) => api.host.createDirectory(r) }, 'host.openPath': { schema: hostOpenPathRequestSchema, invoke: (api, r, signal) => api.host.openPath(r, signal) }, 'skill.list': { schema: skillListRequestSchema, invoke: (api, r) => api.skills.list(r) }, 'agentPreset.openDocument': { schema: agentPresetOpenDocumentRequestSchema, invoke: (api, r, signal) => api.agentPresets.openDocument(r, signal) }, diff --git a/packages/host/apiproxy/src/index.ts b/packages/host/apiproxy/src/index.ts index aab8e952d2..98a0e091c8 100644 --- a/packages/host/apiproxy/src/index.ts +++ b/packages/host/apiproxy/src/index.ts @@ -15,6 +15,7 @@ import { Context, Service } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' import type {} from '@deepseek-ai/dsh-agent-default-model' import type {} from '@deepseek-ai/dsh-api-session-controller' +import type {} from '@deepseek-ai/dsh-host-directory-picker' import type { ApiProxy } from './api/index.ts' import { createApiProxy } from './api-proxy.ts' import { diff --git a/packages/host/apiproxy/tests/api-proxy-host.spec.ts b/packages/host/apiproxy/tests/api-proxy-host.spec.ts index 74c068150f..681f2d0ed4 100644 --- a/packages/host/apiproxy/tests/api-proxy-host.spec.ts +++ b/packages/host/apiproxy/tests/api-proxy-host.spec.ts @@ -2,8 +2,6 @@ import { homedir } from 'node:os' import { afterEach, describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' import AgentRegistry from '@deepseek-ai/dsh-agent' -import { DirectoryPickerError } from '@deepseek-ai/dsh-host-directory-picker' -import type { DirectoryPickerCapability } from '@deepseek-ai/dsh-host-directory-picker' import type { RpcRequest } from '@deepseek-ai/dsh-host-apiproxy/api/rpc' import { RpcId } from '@deepseek-ai/dsh-host-apiproxy/api/rpc' import { createApiProxy } from '../src/api-proxy.ts' @@ -26,7 +24,6 @@ function expectOk(response: { readonly result: { readonly ok: true; readonly } async function harness( - picker: DirectoryPickerCapability = { kind: 'native', pick: async () => null }, extras: { openPath?: (path: string, signal: AbortSignal) => Promise canOpenPath?: () => boolean @@ -35,7 +32,6 @@ async function harness( const ctx = new Context() contexts.push(ctx) await ctx.plugin(AgentRegistry) - ctx.provide('directoryPicker', { capability: () => picker } as never) const api = createApiProxy(ctx, { defaultModelSelection: () => ({ provider: 'test', model: 'test-model' }), cwd: '/tmp/dsh-apiproxy-host', @@ -45,135 +41,10 @@ async function harness( return { api } } -describe('host.pickDirectory', () => { - it('returns a selected path or explicit cancellation from the native capability', async () => { - const selected = await harness({ kind: 'native', pick: async () => '/tmp/project' }) - expect((await selected.api.host.pickDirectory(request({}), new AbortController().signal)).result) - .toEqual({ ok: true, value: { path: '/tmp/project' } }) - - const cancelled = await harness({ kind: 'native', pick: async () => null }) - expect((await cancelled.api.host.pickDirectory(request({}), new AbortController().signal)).result) - .toEqual({ ok: true, value: { path: null } }) - }) - - it('propagates abort into the native capability as a cancelled RPC error', async () => { - const { api } = await harness({ - kind: 'native', - pick: signal => new Promise((_resolve, reject) => { - signal.addEventListener('abort', () => { reject(new Error('aborted')) }, { once: true }) - }), - }) - const abort = new AbortController() - const pending = api.host.pickDirectory(request({}), abort.signal) - abort.abort() - expect((await pending).result).toMatchObject({ ok: false, error: { code: 'cancelled' } }) - }) - - it('folds a non-abort native-chooser failure into an internal error', async () => { - const { api } = await harness({ - kind: 'native', - pick: async () => { throw new Error('no chooser installed') }, - }) - const response = await api.host.pickDirectory(request({}), new AbortController().signal) - expect(response.result).toMatchObject({ ok: false, error: { code: 'internal' } }) - }) - - it('refuses the native RPC under a browse composition', async () => { - const { api } = await harness(BROWSE_STUB) - const response = await api.host.pickDirectory(request({}), new AbortController().signal) - expect(response.result).toMatchObject({ - ok: false, - error: { code: 'directory-picker-unavailable', details: { capability: 'browse' } }, - }) - }) -}) - -const BROWSE_STUB: DirectoryPickerCapability = { - kind: 'browse', - list: async (path) => { - if (path === '/denied') { - throw new DirectoryPickerError('directory-unreadable', '/denied', 'cannot list /denied') - } - const target = path ?? '/home/user' - return { - path: target, - home: '/home/user', - crumbs: [{ name: '/', path: '/', hidden: false }], - entries: [{ name: 'projects', path: `${target}/projects`, hidden: false }], - truncated: false, - } - }, - createDirectory: async (path, name) => { - if (name === 'taken') { - throw new DirectoryPickerError('directory-exists', `${path}/${name}`, 'already exists') - } - if (name === 'unwritable') throw new Error('disk detached') - return `${path}/${name}` - }, -} - -describe('host.listDirectory / host.createDirectory', () => { - it('serves listings and creation through the browse capability, defaulting to home', async () => { - const { api } = await harness(BROWSE_STUB) - const home = await api.host.listDirectory(request({}), new AbortController().signal) - expect(home.result).toMatchObject({ ok: true, value: { path: '/home/user', home: '/home/user' } }) - const listed = await api.host.listDirectory( - request({ path: '/home/user/projects' }), - new AbortController().signal, - ) - expect(listed.result).toMatchObject({ ok: true, value: { path: '/home/user/projects' } }) - const created = await api.host.createDirectory(request({ path: '/home/user', name: 'fresh' })) - expect(created.result).toEqual({ ok: true, value: { path: '/home/user/fresh' } }) - }) - - it('maps typed picker failures onto wire errors and folds unknown throws to internal', async () => { - const { api } = await harness(BROWSE_STUB) - expect((await api.host.listDirectory( - request({ path: '/denied' }), - new AbortController().signal, - )).result).toMatchObject({ - ok: false, - error: { code: 'directory-unreadable', details: { path: '/denied' } }, - }) - expect((await api.host.createDirectory(request({ path: '/home/user', name: 'taken' }))).result) - .toMatchObject({ ok: false, error: { code: 'directory-exists' } }) - expect((await api.host.createDirectory(request({ path: '/home/user', name: 'unwritable' }))).result) - .toMatchObject({ ok: false, error: { code: 'internal' } }) - }) - - it('reports an aborted listing as cancelled', async () => { - const { api } = await harness({ - kind: 'browse', - list: (_path, signal) => new Promise((_resolve, reject) => { - signal?.addEventListener('abort', () => { reject(new Error('scan aborted')) }, { once: true }) - }), - createDirectory: async () => '/never', - }) - const abort = new AbortController() - const pending = api.host.listDirectory(request({}), abort.signal) - abort.abort() - expect((await pending).result).toMatchObject({ ok: false, error: { code: 'cancelled' } }) - }) - - it('refuses the browse RPCs under a native composition', async () => { - const { api } = await harness() - expect((await api.host.listDirectory(request({}), new AbortController().signal)).result) - .toMatchObject({ - ok: false, - error: { code: 'directory-picker-unavailable', details: { capability: 'native' } }, - }) - expect((await api.host.createDirectory(request({ path: '/x', name: 'y' }))).result) - .toMatchObject({ - ok: false, - error: { code: 'directory-picker-unavailable', details: { capability: 'native' } }, - }) - }) -}) - describe('host.openPath', () => { it('describes whether the deployment can reach a native desktop', async () => { - const visible = await harness(undefined, { canOpenPath: () => true }) - const headless = await harness(undefined, { canOpenPath: () => false }) + const visible = await harness({ canOpenPath: () => true }) + const headless = await harness({ canOpenPath: () => false }) expect(expectOk(await visible.api.host.describe(request({}))).canOpenPath).toBe(true) expect(expectOk(await headless.api.host.describe(request({}))).canOpenPath).toBe(false) expect(expectOk(await visible.api.host.describe(request({}))).home).toBe(homedir()) @@ -181,7 +52,7 @@ describe('host.openPath', () => { it('opens through the injected native boundary', async () => { const opened: string[] = [] - const { api } = await harness(undefined, { + const { api } = await harness({ openPath: async (path) => { opened.push(path) }, }) expect((await api.host.openPath( @@ -192,7 +63,7 @@ describe('host.openPath', () => { }) it('propagates abort into the native boundary as a cancelled RPC error', async () => { - const { api } = await harness(undefined, { + const { api } = await harness({ openPath: (_path, signal) => new Promise((_resolve, reject) => { signal.addEventListener('abort', () => { reject(new Error('aborted')) }, { once: true }) }), diff --git a/packages/host/apiproxy/tests/client-handler.spec.ts b/packages/host/apiproxy/tests/client-handler.spec.ts index d6be6bf619..45b6d153ce 100644 --- a/packages/host/apiproxy/tests/client-handler.spec.ts +++ b/packages/host/apiproxy/tests/client-handler.spec.ts @@ -29,9 +29,6 @@ function scriptedApi(overrides: { describe: r => ok(r, { version: '0-test', cwd: '/t', attachedSessions: 0, home: '/h', canOpenPath: true, }), - pickDirectory: r => ok(r, { path: null }), - listDirectory: r => ok(r, { path: '/t', home: '/t', crumbs: [], entries: [], truncated: false }), - createDirectory: r => ok(r, { path: '/t/new' }), openPath: r => ok(r, { opened: true as const }), ...overrides.host, }, diff --git a/packages/host/apiproxy/tests/fetch-carrier.spec.ts b/packages/host/apiproxy/tests/fetch-carrier.spec.ts index b56fa89d82..9bbd19640b 100644 --- a/packages/host/apiproxy/tests/fetch-carrier.spec.ts +++ b/packages/host/apiproxy/tests/fetch-carrier.spec.ts @@ -18,15 +18,6 @@ function fakeApi(overrides: Partial<{ crashOn: string }> = {}): ApiProxy { }, } }, - async pickDirectory(request) { - return { rpcId: request.rpcId, result: { ok: true, value: { path: null } } } - }, - async listDirectory(request) { - return { rpcId: request.rpcId, result: { ok: true, value: { path: '/w', home: '/w', crumbs: [{ name: '/', path: '/', hidden: false }], entries: [], truncated: false } } } - }, - async createDirectory(request) { - return { rpcId: request.rpcId, result: { ok: true, value: { path: '/w/new' } } } - }, async openPath(request) { return { rpcId: request.rpcId, result: { ok: true, value: { opened: true as const } } } }, @@ -124,29 +115,6 @@ describe('unary round trip (handler ⇄ client, no network)', () => { .toEqual({ ok: true, value: { opened: true } }) }) - it('round-trips the native picker without the default unary timeout', async () => { - const api = fakeApi() - api.host.pickDirectory = async (request) => { - await new Promise(resolve => setTimeout(resolve, 15)) - return { rpcId: request.rpcId, result: { ok: true, value: { path: '/tmp/project' } } } - } - const response = await client(api, 1).host.pickDirectory({}) - expect(response.result).toEqual({ ok: true, value: { path: '/tmp/project' } }) - }) - - it('round-trips the browse listing and creation calls through the wire form', async () => { - const c = client() - const listed = await c.host.listDirectory({ path: '/w' }) - expect(listed.result).toEqual({ - ok: true, - value: { path: '/w', home: '/w', crumbs: [{ name: '/', path: '/', hidden: false }], entries: [], truncated: false }, - }) - const home = await c.host.listDirectory({}) - expect(home.result).toMatchObject({ ok: true, value: { home: '/w' } }) - const created = await c.host.createDirectory({ path: '/w', name: 'fresh' }) - expect(created.result).toEqual({ ok: true, value: { path: '/w/new' } }) - }) - it('round-trips host.openPath through the wire form', async () => { const api = fakeApi() let opened: string | undefined @@ -165,41 +133,10 @@ describe('unary round trip (handler ⇄ client, no network)', () => { expect(skills.result).toEqual({ ok: true, value: { skills: [{ name: 'commit-helper', description: 'Git commits', modelInvocable: true }] } }) }) - it('lets host.pickDirectory finish after the 30-second default unary deadline', async () => { - vi.useFakeTimers() - const timeoutSpy = vi.spyOn(AbortSignal, 'timeout').mockImplementation((milliseconds) => { - const controller = new AbortController() - setTimeout(() => { - controller.abort(new DOMException('The operation was aborted due to timeout', 'TimeoutError')) - }, milliseconds) - return controller.signal - }) - try { - const api = fakeApi() - api.host.pickDirectory = async (request) => { - await new Promise(resolve => setTimeout(resolve, 30_001)) - return { rpcId: request.rpcId, result: { ok: true, value: { path: '/tmp/slow' } } } - } - const execution = client(api).host.pickDirectory({}) - const assertion = expect(execution).resolves.toMatchObject({ - result: { ok: true, value: { path: '/tmp/slow' } }, - }) - - await Promise.all([ - vi.advanceTimersByTimeAsync(30_001), - assertion, - ]) - expect(timeoutSpy).not.toHaveBeenCalled() - } finally { - timeoutSpy.mockRestore() - vi.useRealTimers() - } - }) - - it('keeps caller and connection aborts on a deadline-exempt unary', async () => { + it('keeps caller and connection aborts on a signal-taking unary', async () => { const api = fakeApi() const started = Promise.withResolvers() - api.host.pickDirectory = async (request, signal) => { + api.host.openPath = async (request, signal) => { started.resolve(signal) if (!signal.aborted) { await new Promise((resolve) => { @@ -212,7 +149,7 @@ describe('unary round trip (handler ⇄ client, no network)', () => { } } const controller = new AbortController() - const execution = client(api).host.pickDirectory({}, controller.signal) + const execution = client(api).host.openPath({ path: '/tmp/a.txt' }, controller.signal) const handlerSignal = await started.promise controller.abort(new Error('connection closed')) @@ -221,9 +158,9 @@ describe('unary round trip (handler ⇄ client, no network)', () => { expect(handlerSignal.aborted).toBe(true) }) - it('propagates the carrier Request signal into host.pickDirectory', async () => { + it('propagates the carrier Request signal into host.openPath', async () => { const api = fakeApi() - api.host.pickDirectory = async (request, signal) => { + api.host.openPath = async (request, signal) => { if (!signal.aborted) { await new Promise((resolve) => { signal.addEventListener('abort', () => { resolve() }, { once: true }) @@ -236,8 +173,8 @@ describe('unary round trip (handler ⇄ client, no network)', () => { } const handler = toFetchHandler(api) const controller = new AbortController() - const body = JSON.stringify({ type: 'client-request', rpcId: 'r-picker', method: 'host.pickDirectory', payload: {} }) - const pending = handler.fetch(new Request('http://x/api/host.pickDirectory', { + const body = JSON.stringify({ type: 'client-request', rpcId: 'r-opener', method: 'host.openPath', payload: { path: '/tmp/a.txt' } }) + const pending = handler.fetch(new Request('http://x/api/host.openPath', { method: 'POST', headers: { 'content-type': 'application/json' }, body, signal: controller.signal, })) controller.abort() diff --git a/packages/host/apiproxy/tests/rpc-schemas.spec.ts b/packages/host/apiproxy/tests/rpc-schemas.spec.ts index 0a803154b8..429e31361f 100644 --- a/packages/host/apiproxy/tests/rpc-schemas.spec.ts +++ b/packages/host/apiproxy/tests/rpc-schemas.spec.ts @@ -5,11 +5,7 @@ import { rpcResultSchema, serverResponseSchema, } from '../src/api/rpc.schema.ts' import { z } from 'zod' -import { - hostCreateDirectoryRequestSchema, hostCreateDirectoryValueSchema, - hostDescribeRequestSchema, hostDescribeValueSchema, - hostListDirectoryRequestSchema, hostListDirectoryValueSchema, -} from '../src/api/host.schema.ts' +import { hostDescribeRequestSchema, hostDescribeValueSchema } from '../src/api/host.schema.ts' import { skillEntrySchema, skillListRequestSchema, skillListValueSchema } from '../src/api/skills.schema.ts' import { agentPresetOpenDocumentValueSchema } from '../src/api/agent-presets.schema.ts' @@ -36,10 +32,6 @@ describe('rpcErrorSchema', () => { expect(rpcErrorSchema.parse({ code: 'cancelled', message: 'm', details: {} }).code).toBe('cancelled') expect(rpcErrorSchema.parse({ code: 'session-not-found', message: 'm', details: { sessionId: 's' } }).code).toBe('session-not-found') expect(rpcErrorSchema.parse({ code: 'invalid-time-zone', message: 'm', details: { value: 'CST' } }).code).toBe('invalid-time-zone') - expect(rpcErrorSchema.parse({ code: 'directory-unreadable', message: 'm', details: { path: '/x' } }).code).toBe('directory-unreadable') - expect(rpcErrorSchema.parse({ code: 'directory-exists', message: 'm', details: { path: '/x' } }).code).toBe('directory-exists') - expect(rpcErrorSchema.parse({ code: 'directory-create-failed', message: 'm', details: { path: '/x' } }).code).toBe('directory-create-failed') - expect(rpcErrorSchema.parse({ code: 'directory-picker-unavailable', message: 'm', details: { capability: 'none' } }).code).toBe('directory-picker-unavailable') expect(rpcErrorSchema.parse({ code: 'agent-preset-read-only', message: 'm', details: { agentPreset: 'p', reason: 'system' } }).code).toBe('agent-preset-read-only') expect(rpcErrorSchema.parse({ code: 'agent-preset-locked', message: 'm', details: { sessionId: 's', agentPreset: 'p' } }).code).toBe('agent-preset-locked') expect(rpcErrorSchema.parse({ code: 'agent-preset-not-found', message: 'm', details: { agentPreset: 'p', available: [] } }).code).toBe('agent-preset-not-found') @@ -55,7 +47,6 @@ describe('rpcErrorSchema', () => { it('rejects a known code with missing details', () => { expect(() => rpcErrorSchema.parse({ code: 'agent-busy', message: 'm', details: {} })).toThrow() - expect(() => rpcErrorSchema.parse({ code: 'directory-unreadable', message: 'm', details: {} })).toThrow() expect(() => rpcErrorSchema.parse({ code: 'internal', message: 'm' })).toThrow() expect(() => rpcErrorSchema.parse({ code: 'nope', message: 'm', details: {} })).toThrow() }) @@ -109,26 +100,6 @@ describe('host domain schemas', () => { version: '1', cwd: '/x', attachedSessions: 0, canOpenPath: true, })).toThrow() }) - - it('validates the browse listing/creation payloads', () => { - expect(hostListDirectoryRequestSchema.parse({})).toEqual({}) - expect(hostListDirectoryRequestSchema.parse({ path: '/x' })).toEqual({ path: '/x' }) - const listing = hostListDirectoryValueSchema.parse({ - path: '/home/u/p', - home: '/home/u', - crumbs: [{ name: '/', path: '/', hidden: false }, { name: 'p', path: '/home/u/p', hidden: false }], - entries: [{ name: '.dot', path: '/home/u/p/.dot', hidden: true }], - truncated: false, - }) - expect(listing.entries[0]?.hidden).toBe(true) - // The flag is part of the wire value, not an optional decoration. - expect(() => hostListDirectoryValueSchema.parse({ path: '/x', home: '/x', crumbs: [], entries: [] })).toThrow() - expect(hostCreateDirectoryRequestSchema.parse({ path: '/x', name: 'new' })).toEqual({ path: '/x', name: 'new' }) - for (const name of ['', ' ', '.', '..', 'a/b', 'a\\b']) { - expect(() => hostCreateDirectoryRequestSchema.parse({ path: '/x', name })).toThrow() - } - expect(hostCreateDirectoryValueSchema.parse({ path: '/x/new' })).toEqual({ path: '/x/new' }) - }) }) describe('skills domain schemas', () => {