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
65 lines
2.6 KiB
TypeScript
65 lines
2.6 KiB
TypeScript
/**
|
|
* host domain zod schemas (names derived from map keys).
|
|
*/
|
|
|
|
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'
|
|
|
|
/** host.describe request payload (empty object literal). */
|
|
export const hostDescribeRequestSchema = z.object({}) satisfies z.ZodType<Wire<RequestPayload<'host.describe'>>>
|
|
|
|
/** host.describe response value. */
|
|
export const hostDescribeValueSchema = z.object({
|
|
version: z.string(),
|
|
cwd: z.string(),
|
|
provider: z.string().optional(),
|
|
model: z.string().optional(),
|
|
attachedSessions: z.number().int().nonnegative(),
|
|
directoryPicker: z.union([z.literal('dialog'), z.literal('browse')]),
|
|
}) satisfies z.ZodType<Wire<ResponseValue<'host.describe'>>>
|
|
|
|
/** host.pickDirectory request payload (empty object literal). */
|
|
export const hostPickDirectoryRequestSchema = z.object({}) satisfies z.ZodType<Wire<RequestPayload<'host.pickDirectory'>>>
|
|
|
|
/** host.pickDirectory response value; null means the user cancelled. */
|
|
export const hostPickDirectoryValueSchema = z.object({
|
|
path: z.string().nullable(),
|
|
}) satisfies z.ZodType<Wire<ResponseValue<'host.pickDirectory'>>>
|
|
|
|
/** 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<Wire<DirectoryEntry>>
|
|
|
|
/** host.listDirectory request payload; an absent path lists the home directory. */
|
|
export const hostListDirectoryRequestSchema = z.object({
|
|
path: z.string().optional(),
|
|
}) satisfies z.ZodType<Wire<RequestPayload<'host.listDirectory'>>>
|
|
|
|
/** host.listDirectory response value. */
|
|
export const hostListDirectoryValueSchema = z.object({
|
|
path: z.string(),
|
|
home: z.string(),
|
|
crumbs: z.array(directoryEntrySchema),
|
|
entries: z.array(directoryEntrySchema),
|
|
}) satisfies z.ZodType<Wire<ResponseValue<'host.listDirectory'>>>
|
|
|
|
/** 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<Wire<RequestPayload<'host.createDirectory'>>>
|
|
|
|
/** host.createDirectory response value: the created directory's absolute path. */
|
|
export const hostCreateDirectoryValueSchema = z.object({
|
|
path: z.string(),
|
|
}) satisfies z.ZodType<Wire<ResponseValue<'host.createDirectory'>>>
|