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:
73 files changed
+1536
-49
No files matched your search
@@ -13,7 +13,7 @@ export type { RootOwnerProps } from './slots.ts'
|
||||
export { SessionCreateError, SessionsService, scopeOf, workspaceTitleOf } from './sessions/service.ts'
|
||||
export { createScope } from './agents/scope.ts'
|
||||
export type { AgentScopeHandle } from './agents/scope.ts'
|
||||
export { WorkspaceCreateError, WorkspacesService } from './workspaces/service.ts'
|
||||
export { DirectoryBrowseError, WorkspaceCreateError, WorkspacesService } from './workspaces/service.ts'
|
||||
export type { Session } from './sessions/session.ts'
|
||||
export type {
|
||||
SessionBinding, SessionListState, SessionProvideContribution, SessionProvideDescriptor, SessionSummary,
|
||||
@@ -21,7 +21,9 @@ export type {
|
||||
export type { SessionListPhase } from './sessions/manager.ts'
|
||||
export type { WorkspaceListPhase } from './workspaces/manager.ts'
|
||||
export type { WorkspaceListState } from './workspaces/service.ts'
|
||||
export type { WorkspaceId, WorkspaceView } from '@deepseek-ai/dsh-client-connection/client'
|
||||
export type {
|
||||
DirectoryEntry, DirectoryListing, DirectoryPickerKind, WorkspaceId, WorkspaceView,
|
||||
} from '@deepseek-ai/dsh-client-connection/client'
|
||||
// Runtime owns the snapshot store; web-react only binds it to React.
|
||||
export { createSnapshotStore, defineStore, shallowEqual } from './contract/store.ts'
|
||||
export type {
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
import type {
|
||||
IApiClient, RpcError, SessionId, WorkspaceId, WorkspaceView,
|
||||
DirectoryListing, DirectoryPickerKind, IApiClient, RpcError,
|
||||
SessionId, WorkspaceId, WorkspaceView,
|
||||
} from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { SnapshotStore } from '../contract/store.ts'
|
||||
import { createSnapshotStore } from '../contract/store.ts'
|
||||
@@ -29,6 +30,14 @@ export class WorkspaceCreateError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
/** Structured browse failure so the directory browser can branch on Host business codes. */
|
||||
export class DirectoryBrowseError extends Error {
|
||||
constructor(readonly rpcError: RpcError) {
|
||||
super(`directory browse failed: ${rpcError.code}: ${rpcError.message}`)
|
||||
this.name = 'DirectoryBrowseError'
|
||||
}
|
||||
}
|
||||
|
||||
/** Real Workspace object layer and Host actions. */
|
||||
export class WorkspacesService {
|
||||
/** UI-facing immutable projection; the manager remains wire truth. */
|
||||
@@ -171,7 +180,7 @@ export class WorkspacesService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the Host's native directory picker.
|
||||
* Open the Host's native directory picker (the `dialog` capability).
|
||||
* @returns the selected path, or null when the user cancelled.
|
||||
*/
|
||||
async pickDirectory(): Promise<string | null> {
|
||||
@@ -182,6 +191,44 @@ export class WorkspacesService {
|
||||
return response.result.value.path
|
||||
}
|
||||
|
||||
/**
|
||||
* The directory-picking interaction the Host composed — the fact the picker
|
||||
* UI branches on (`dialog` opens the native chooser; `browse` opens the
|
||||
* in-app browser). Read per flow open: one describe round trip, no cache to
|
||||
* go stale across reconnects.
|
||||
* @returns the Host's advertised picker kind.
|
||||
*/
|
||||
async directoryPickerKind(): Promise<DirectoryPickerKind> {
|
||||
const response = await this.api.host.describe({})
|
||||
if (!response.result.ok) {
|
||||
throw new Error(`host describe failed: ${response.result.error.message}`)
|
||||
}
|
||||
return response.result.value.directoryPicker
|
||||
}
|
||||
|
||||
/**
|
||||
* List one directory level through the Host's `browse` capability.
|
||||
* @param path - absolute directory to list; absent lists the Host home directory.
|
||||
* @returns the level's listing with breadcrumb ancestry.
|
||||
*/
|
||||
async listDirectory(path?: string): Promise<DirectoryListing> {
|
||||
const response = await this.api.host.listDirectory(path === undefined ? {} : { path })
|
||||
if (!response.result.ok) throw new DirectoryBrowseError(response.result.error)
|
||||
return response.result.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Create one child directory through the Host's `browse` capability.
|
||||
* @param path - absolute existing parent directory.
|
||||
* @param name - single non-blank path segment.
|
||||
* @returns the created directory's absolute path.
|
||||
*/
|
||||
async createDirectory(path: string, name: string): Promise<string> {
|
||||
const response = await this.api.host.createDirectory({ path, name })
|
||||
if (!response.result.ok) throw new DirectoryBrowseError(response.result.error)
|
||||
return response.result.value.path
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename a Workspace.
|
||||
* @param workspaceId - target workspace.
|
||||
|
||||
Reference in New Issue
Block a user