From 58a08cf4f933a9af0457b22066d421322e3e1f0b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 20:15:00 +0000 Subject: [PATCH] feat: support DSH 0.2.0-rc.2 - Bump all DSH client packages to 0.2.0-rc.2 (cordis ^4.0.4) and replace the removed dsh-client-runtime with the split Workspace Controller, Session Controller and ui-workspace services. - Detect the open Session through its `mainView` reference and wait for the Workspace list `phase` before sweeping leftovers. - Serve the Host endpoints as exact Fetch routes on the authenticated `/api` channel; `connection.rpc.handle` cannot mount third-party channels in this release. - Mirror DSH's hero directory-flow occupant into the picker's own hole, since the built-in picker now exclusively declares that hole. - Use the renamed `*Regular` primitive icons. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Pa42ksDaPgAVgL5uPB4uA1 --- CHANGELOG.md | 18 ++ CONTRIBUTING.md | 5 +- README.md | 23 ++- SECURITY.md | 5 +- lib/client.js | 105 +++++++--- lib/client.js.map | 6 +- lib/index.js | 88 ++++++--- lib/index.js.map | 6 +- package-lock.json | 446 ++++++++++++++++++++++++++++-------------- package.json | 49 +++-- scripts/build.mjs | 1 - src/client/index.tsx | 126 +++++++++--- src/client/session.ts | 40 ++-- src/index.ts | 117 +++++++---- src/shared/rpc.ts | 15 ++ tests/host.test.ts | 75 +++++++ tests/session.test.ts | 27 +-- 17 files changed, 839 insertions(+), 313 deletions(-) create mode 100644 src/shared/rpc.ts create mode 100644 tests/host.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c7c015..cab44c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## 0.6.0 - 2026-09-29 + +- Update all DSH client packages for DeepSeek Harness `0.2.0-rc.2`. +- Replace the removed `dsh-client-runtime` faces with the split + `ctx.workspaces` (Workspace Controller), `ctx.sessions` (Session + Controller) and `ctx.uiWorkspace` (blank-Session connection, navigation and + archival) services. +- Detect the open Session through its `mainView` reference now that the + Session list no longer carries a `current` field, and wait for the Workspace + list `phase` before sweeping leftovers. +- Use the renamed `*Regular` primitive icons. +- Serve the Host endpoints as exact Fetch routes on the authenticated `/api` + channel: DSH `0.2.0-rc.2`'s `connection.rpc.handle` resolves `webServer` on + the Connection plugin's own fiber and cannot mount third-party channels. +- Mirror DSH's hero directory-flow occupant into the picker's own hole, since + a hole can now be declared by only one entry and the built-in picker owns + `conversation.hero.workspace.directoryFlow`. + ## 0.5.0 - 2026-09-14 - Update all DSH client packages for DeepSeek Harness `0.1.5-rc.2`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bb30f90..f041d38 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,5 +24,6 @@ For UI changes, verify these transitions in an isolated DSH Web profile: 5. Clicking “无工作区会话” without sending, then switching away, removes the temporary Workspace from the picker and deletes its empty directory. -Do not weaken the loopback authority on the filesystem RPC without adding a -real authentication and authorization boundary. +Keep the filesystem endpoints on DSH's authenticated `/api` Connection channel; +do not expose them on a route that skips its Host/Origin fence and browser +authentication. diff --git a/README.md b/README.md index 98508fe..f91c224 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,13 @@ complete English and Simplified Chinese dictionaries. ### How it works -DSH `0.1.5-rc.2` disables its native blank-session composer when a Session has +DSH `0.2.0-rc.2` disables its native blank-session composer when a Session has no registered Workspace. To retain the complete native composer—including agent preset, commands and attachments, access mode, model selection, and the send button—the plugin uses this lifecycle: -1. The Host creates a directory through a loopback-only DSH Connection RPC. +1. The Host creates a directory through an authenticated DSH Connection + endpoint on the shared `/api` channel. 2. The client registers that directory as a temporary Workspace. 3. DSH creates and opens a Session in the temporary Workspace, so the standard blank-session UI remains fully available. @@ -46,7 +47,7 @@ Finder metadata such as `.DS_Store` does not keep the directory. ### Compatibility -- DeepSeek Harness `0.1.5-rc.2` +- DeepSeek Harness `0.2.0-rc.2` - Node.js `22.19` or later - Local Web/Desktop profile @@ -87,8 +88,10 @@ dsh plugin --profile web remove dsh-projectless-session - Keeps the Session resumable under **Ungrouped** across DSH restarts. - Switches plugin copy live between English and Simplified Chinese with the DSH Language setting. -- Uses DSH's authenticated Connection channel for filesystem RPC calls and - unregisters the channel cleanly when the plugin unloads. +- Uses DSH's authenticated `/api` Connection channel for filesystem calls and + removes its routes cleanly when the plugin unloads. +- Keeps **Add workspace…** on DSH's composed directory flow (native chooser or + in-app browser) by mirroring it into the plugin picker's own slot. - Restores DSH's built-in priority `0` Workspace picker automatically when the plugin is removed. @@ -131,11 +134,11 @@ Workspaces, and rollback when Session creation fails. ### 工作方式 -在 DSH `0.1.5-rc.2` 中,没有已注册 Workspace 的空白 Session 会禁用原生 +在 DSH `0.2.0-rc.2` 中,没有已注册 Workspace 的空白 Session 会禁用原生 编辑器。为了完整保留 Agent 预设、命令与附件、访问模式、模型选择和发送 按钮,插件采用以下生命周期: -1. Host 通过仅允许 loopback 调用的 DSH Connection RPC 创建目录。 +1. Host 通过共享 `/api` 通道上已认证的 DSH Connection 接口创建目录。 2. 客户端把该目录注册为临时 Workspace。 3. DSH 在临时 Workspace 中创建并打开 Session,因此空白会话继续使用完整 的原生界面。 @@ -153,7 +156,7 @@ Workspaces, and rollback when Session creation fails. ### 兼容性 -- DeepSeek Harness `0.1.5-rc.2` +- DeepSeek Harness `0.2.0-rc.2` - Node.js `22.19` 或更高版本 - 本机 Web/Desktop profile @@ -191,7 +194,9 @@ dsh plugin --profile web remove dsh-projectless-session - 若未发送消息,会注销仍空白的无工作区 Workspace。 - Session 进入**未分组**,重启 DSH 后仍可恢复并继续。 - 跟随 DSH Language 设置即时切换英文或简体中文。 -- 文件系统 RPC 使用 DSH 的已认证 Connection 通道,并在插件卸载时干净注销。 +- 文件系统调用使用 DSH 已认证的 `/api` Connection 通道,并在插件卸载时干净移除路由。 +- **添加工作区…** 继续使用 DSH 组合的目录选择流程(原生选择器或应用内浏览器), + 插件会把它镜像到自身选择器的插槽中。 - 卸载插件后,DSH 内置优先级 `0` 的 Workspace 选择器自动恢复。 ### 自定义根目录 diff --git a/SECURITY.md b/SECURITY.md index f1a32f1..ff0a4a2 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -12,7 +12,8 @@ in an issue. ## Security boundary -The Host operation that creates directories is exposed through a dedicated -DSH Connection RPC channel with `loopback` authority. It accepts no path from +The Host operation that creates directories is exposed as exact Fetch routes +under DSH's shared `/api` Connection channel, behind its Host/Origin fence and +browser authentication. It accepts no path from the browser: the root is Host configuration and all generated child names are fixed-format date/time values plus cryptographic randomness. diff --git a/lib/client.js b/lib/client.js index f50af69..383692c 100644 --- a/lib/client.js +++ b/lib/client.js @@ -52,7 +52,18 @@ function isProjectlessPath(path) { return dateName !== void 0 && sessionName !== void 0 && DATE_DIRECTORY.test(dateName) && SESSION_DIRECTORY.test(sessionName); } +// src/shared/rpc.ts +var PROJECTLESS_RPC_CHANNEL = "/api"; +var PROJECTLESS_RPC_PREFIX = "projectless-session"; +function projectlessEndpoint(endpoint) { + return `${PROJECTLESS_RPC_PREFIX}/${endpoint}`; +} + // src/client/session.ts +var MAIN_VIEW_SOURCE = "mainView"; +function isCurrentSession(sessions, sessionId) { + return (sessions.byId[sessionId]?.retainedBy?.[MAIN_VIEW_SOURCE] ?? 0) > 0; +} function createAbandonClaim() { const claimed = /* @__PURE__ */ new Set(); return { @@ -103,20 +114,20 @@ function expectPathObject(value, key, label) { } async function requestProjectlessDirectory(rpc) { return expectPathObject( - rpcValue(await rpc.call("/projectless-session", "create-directory", {}), "projectless session Host"), + rpcValue(await rpc.call(PROJECTLESS_RPC_CHANNEL, projectlessEndpoint("create-directory"), {}), "projectless session Host"), "path", "projectless session Host" ); } async function requestProjectlessRoot(rpc) { return expectPathObject( - rpcValue(await rpc.call("/projectless-session", "get-root", {}), "projectless session Host"), + rpcValue(await rpc.call(PROJECTLESS_RPC_CHANNEL, projectlessEndpoint("get-root"), {}), "projectless session Host"), "root", "projectless session Host" ); } async function requestRemoveProjectlessDirectory(rpc, path) { - rpcValue(await rpc.call("/projectless-session", "remove-directory", { path }), "projectless session Host"); + rpcValue(await rpc.call(PROJECTLESS_RPC_CHANNEL, projectlessEndpoint("remove-directory"), { path }), "projectless session Host"); } async function createAndOpenProjectlessSession(workspaces, sessions, provisionDirectory, registry, removeDirectory = async () => { }, pending) { @@ -148,7 +159,7 @@ function isAbandonedProjectlessWorkspace(workspace, sessions, root, skip = /* @_ if (skip.has(workspace.workspaceId) || !isManagedSessionPath(workspace.path, root)) return false; if (workspace.sessionIds.length === 0) return false; for (const sessionId of workspace.sessionIds) { - if (sessions.current === sessionId) return false; + if (isCurrentSession(sessions, sessionId)) return false; const row = sessions.byId[sessionId]; if (row === void 0 || row.blank !== true) return false; } @@ -195,7 +206,7 @@ function watchTemporaryWorkspace(workspaces, sessions, receipt, removeDirectory, }); return; } - if (snapshot.current === receipt.sessionId) return; + if (isCurrentSession(snapshot, receipt.sessionId)) return; abandon(); }; unsubscribe = sessions.list.subscribe(reconcile); @@ -217,7 +228,7 @@ function sweepAbandonedProjectlessWorkspaces(workspaces, sessions, root, removeD const reconcile = () => { if (!active) return; const workspaceState = workspaces.list.getSnapshot(); - if (workspaceState.baselinesReady === false) return; + if (workspaceState.phase === "pending") return; for (const leftover of findAbandonedProjectlessWorkspaces( workspaceState.items, sessions.list.getSnapshot(), @@ -262,6 +273,8 @@ var import_jsx_runtime = require("react/jsx-runtime"); var PACKAGE_ID = "dsh-projectless-session"; var PROJECTLESS = PROJECTLESS_ENTRY_ID; var ADD_WORKSPACE = "::add-workspace"; +var DSH_DIRECTORY_FLOW = "conversation.hero.workspace.directoryFlow"; +var DIRECTORY_FLOW = "dsh-projectless-session.directoryFlow"; function errorMessage(reason) { return reason instanceof Error ? reason.message : String(reason); } @@ -295,7 +308,7 @@ function ProjectlessWorkspacePicker({ const workspaceItems = selection.projects.map((workspace) => ({ id: workspace.workspaceId, label: workspace.title, - icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.IconFolderClose16, { size: 16 }), + icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.IconFolderCloseRegular, { size: 16 }), disabled: busy })); const flowAvailable = useDirectoryFlow((occupied) => occupied); @@ -306,7 +319,7 @@ function ProjectlessWorkspacePicker({ { id: PROJECTLESS, label: t("picker.projectless"), - icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.IconNewChatOutline16, { size: 16 }), + icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.IconNewChatOutlineRegular, { size: 16 }), disabled: busy }, ...flowAvailable ? [ @@ -314,7 +327,7 @@ function ProjectlessWorkspacePicker({ { id: ADD_WORKSPACE, label: t("picker.addWorkspace"), - icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.IconPlusOutline16, { size: 16 }), + icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.IconPlusOutlineRegular, { size: 16 }), disabled: busy } ] : [] @@ -381,7 +394,7 @@ function ProjectlessWorkspacePicker({ getAnchorRect } ), - renderSlot("conversation.hero.workspace.directoryFlow", directoryFlowOwner), + renderSlot(DIRECTORY_FLOW, directoryFlowOwner), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( import_dsh_client_ui_primitives.Modal, { @@ -416,9 +429,56 @@ function installStyles() { style.remove(); }; } +function mirrorDirectoryFlow(slots) { + const register = slots.register.bind(slots); + let mirrored; + let disposers = []; + const release = () => { + for (const dispose of disposers) dispose(); + disposers = []; + }; + const sync = () => { + const entries = slots.entries(DSH_DIRECTORY_FLOW); + if (entries === mirrored) return; + mirrored = entries; + release(); + disposers = entries.map((entry) => register({ + name: DIRECTORY_FLOW, + ...entry.options.priority === void 0 ? {} : { priority: entry.options.priority }, + ...entry.inject === void 0 ? {} : { inject: entry.inject }, + ...entry.locale === void 0 ? {} : { locale: entry.locale } + }, entry.component)); + }; + const unsubscribe = slots.subscribe(DSH_DIRECTORY_FLOW, sync); + sync(); + return () => { + unsubscribe(); + release(); + }; +} var name = PACKAGE_ID; -var inject = ["connection", "locale", "slots", "sessions", "workspaces"]; +var inject = ["connection", "locale", "slots", "sessions", "workspaces", "uiWorkspace"]; +function projectlessHost(ctx) { + const workspaces = ctx.workspaces; + const ui = ctx.uiWorkspace; + const sessions = { + list: ctx.sessions.list, + open: (sessionId) => { + ui.openSession(sessionId); + } + }; + const host = { + list: workspaces.list, + create: (input) => workspaces.create(input), + delete: (workspaceId) => workspaces.delete(workspaceId), + rename: (workspaceId, title) => workspaces.rename(workspaceId, title), + connectWorkspace: (workspaceId) => ui.connectWorkspace(workspaceId), + archiveSession: (sessionId) => ui.archiveSession(sessionId) + }; + return { host, sessions }; +} function apply(ctx) { + const { host, sessions } = projectlessHost(ctx); ctx.effect(installStyles, `${PACKAGE_ID}: styles`); ctx.effect( () => ctx.locale.register(PROJECTLESS_LOCALE_NS, projectlessLocales), @@ -431,8 +491,8 @@ function apply(ctx) { const removeDirectory = (path) => requestRemoveProjectlessDirectory(rpc, path); const isProjectlessWorkspace = (workspace) => registry.has(workspace.workspaceId) || isProjectlessPath(workspace.path); const directoryFlow = { - getSnapshot: () => ctx.slots.entries("conversation.hero.workspace.directoryFlow").length > 0, - subscribe: (listener) => ctx.slots.subscribe("conversation.hero.workspace.directoryFlow", listener) + getSnapshot: () => ctx.slots.entries(DIRECTORY_FLOW).length > 0, + subscribe: (listener) => ctx.slots.subscribe(DIRECTORY_FLOW, listener) }; ctx.effect(() => { let disposed = false; @@ -441,8 +501,8 @@ function apply(ctx) { void requestProjectlessRoot(rpc).then((root) => { if (disposed) return; stopSweep = sweepAbandonedProjectlessWorkspaces( - ctx.workspaces, - ctx.sessions, + host, + sessions, root, removeDirectory, pendingWorkspaceIds, @@ -455,13 +515,13 @@ function apply(ctx) { }; }, `${PACKAGE_ID}: sweep leftover unused workspaces`); const actions = () => ({ - createWorkspace: (input) => ctx.workspaces.create(input), + createWorkspace: (input) => host.create(input), isProjectlessWorkspace, hooks: { directoryFlow }, createProjectlessSession: async () => { const receipt = await createAndOpenProjectlessSession( - ctx.workspaces, - ctx.sessions, + host, + sessions, // The published Connection package augments the same Cordis key with // Host and Client faces; this file is bundled only for the Client face. () => requestProjectlessDirectory(rpc), @@ -470,13 +530,13 @@ function apply(ctx) { pendingWorkspaceIds ); const translate = ctx.locale.bind(PROJECTLESS_LOCALE_NS); - await ctx.workspaces.rename(receipt.workspaceId, translate("picker.projectless")).catch(() => { + await host.rename(receipt.workspaceId, translate("picker.projectless")).catch(() => { }); ctx.effect( () => { const stop = watchTemporaryWorkspace( - ctx.workspaces, - ctx.sessions, + host, + sessions, receipt, removeDirectory, claim @@ -494,13 +554,14 @@ function apply(ctx) { name: "conversation.hero.workspace", priority: -1, children: { - "conversation.hero.workspace.directoryFlow": { kind: "single", scope: "root" } + [DIRECTORY_FLOW]: { kind: "single", scope: "root" } }, inject: actions, locale: PROJECTLESS_LOCALE_NS }, ProjectlessWorkspacePicker )); + ctx.slots.inject(DIRECTORY_FLOW, () => mirrorDirectoryFlow(ctx.slots)); } return module.exports; } }); //# sourceMappingURL=client.js.map diff --git a/lib/client.js.map b/lib/client.js.map index 6d4f09f..08f5409 100644 --- a/lib/client.js.map +++ b/lib/client.js.map @@ -1,7 +1,7 @@ { "version": 3, - "sources": ["../src/client/index.tsx", "../src/shared/paths.ts", "../src/client/session.ts", "../src/client/locales.ts"], - "sourcesContent": ["import { useCallback, useEffect, useState } from 'react'\nimport type { RefObject } from 'react'\nimport type { ClientContext, SessionId, WorkspaceId, WorkspaceView } from '@deepseek-ai/dsh-client-runtime/client'\nimport type { ClientConnectionRpc } from '@deepseek-ai/dsh-client-connection/client'\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\nimport type {\n HostObservable,\n PropsHooks,\n PropsLocale,\n PropsRenderSlots,\n PropsRuntime,\n} from '@deepseek-ai/dsh-client-ui-slots'\nimport type { DirectoryFlowOwnerProps } from '@deepseek-ai/dsh-client-ui-workspace/client'\nimport {\n Button,\n IconFolderClose16,\n IconNewChatOutline16,\n IconPlusOutline16,\n Menu,\n Modal,\n type MenuEntry,\n} from '@deepseek-ai/dsh-client-ui-primitives'\nimport {\n PROJECTLESS_ENTRY_ID,\n createAbandonClaim,\n createAndOpenProjectlessSession,\n createProjectlessRegistry,\n isProjectlessPath,\n requestProjectlessDirectory,\n requestProjectlessRoot,\n requestRemoveProjectlessDirectory,\n resolvePickerSelection,\n sweepAbandonedProjectlessWorkspaces,\n watchTemporaryWorkspace,\n} from './session.ts'\nimport { PROJECTLESS_LOCALE_NS, projectlessLocales } from './locales.ts'\n\nconst PACKAGE_ID = 'dsh-projectless-session'\nconst PROJECTLESS = PROJECTLESS_ENTRY_ID\nconst ADD_WORKSPACE = '::add-workspace'\n\ninterface PickerActions {\n createWorkspace(input: { path: string }): Promise\n createProjectlessSession(): Promise\n isProjectlessWorkspace(workspace: WorkspaceView): boolean\n hooks: {\n directoryFlow: HostObservable\n }\n}\n\ntype PickerProps =\n PropsRuntime<'conversation.hero.workspace'>\n & PropsRenderSlots<'conversation.hero.workspace.directoryFlow'>\n & Omit\n & PropsHooks\n & PropsLocale\n\nfunction errorMessage(reason: unknown): string {\n return reason instanceof Error ? reason.message : String(reason)\n}\n\n/** Shadow-compatible replacement for the built-in WorkspacePicker. */\nfunction ProjectlessWorkspacePicker({\n open,\n anchorRef,\n selectedId,\n onPick,\n onClose,\n useWorkspaces,\n createWorkspace,\n createProjectlessSession,\n isProjectlessWorkspace,\n useDirectoryFlow,\n renderSlot,\n t,\n}: PickerProps) {\n const workspaceState = useWorkspaces((state: { items: readonly WorkspaceView[] }) => state)\n const [busy, setBusy] = useState(false)\n const [flowOpen, setFlowOpen] = useState(false)\n const [modalError, setModalError] = useState(null)\n const getAnchorRect = useCallback(\n () => (anchorRef as RefObject | undefined)?.current?.getBoundingClientRect() ?? null,\n [anchorRef],\n )\n\n const selection = resolvePickerSelection(\n workspaceState.items,\n selectedId,\n isProjectlessWorkspace,\n )\n const workspaceItems: MenuEntry[] = selection.projects.map(workspace => ({\n id: workspace.workspaceId,\n label: workspace.title,\n icon: ,\n disabled: busy,\n }))\n const flowAvailable = useDirectoryFlow((occupied: boolean) => occupied)\n useEffect(() => {\n if (flowOpen && !flowAvailable) setFlowOpen(false)\n }, [flowOpen, flowAvailable])\n const footer: MenuEntry[] = [\n {\n id: PROJECTLESS,\n label: t('picker.projectless'),\n icon: ,\n disabled: busy,\n },\n ...flowAvailable ? [\n { type: 'separator' as const, id: 'projectless-separator' },\n {\n id: ADD_WORKSPACE,\n label: t('picker.addWorkspace'),\n icon: ,\n disabled: busy,\n },\n ] : [],\n ]\n\n const run = (operation: () => Promise): void => {\n if (busy) return\n setBusy(true)\n void operation().catch((reason: unknown) => {\n setModalError(errorMessage(reason))\n }).finally(() => {\n setBusy(false)\n })\n }\n\n const handleSelect = (id: string): void => {\n if (id === PROJECTLESS) {\n onClose()\n run(async () => { await createProjectlessSession() })\n return\n }\n if (id === ADD_WORKSPACE) {\n onClose()\n setModalError(null)\n setFlowOpen(true)\n return\n }\n onPick(id as WorkspaceId)\n }\n\n const directoryFlowOwner: DirectoryFlowOwnerProps = {\n open: flowOpen,\n busy,\n onPicked: path => {\n run(async () => {\n try {\n const workspace = await createWorkspace({ path })\n onPick(workspace.workspaceId)\n } finally {\n setFlowOpen(false)\n }\n })\n },\n onCancel: () => { setFlowOpen(false) },\n onError: message => {\n setFlowOpen(false)\n setModalError(message)\n },\n }\n\n return (\n <>\n \n {renderSlot('conversation.hero.workspace.directoryFlow', directoryFlowOwner)}\n { setModalError(null) }}\n closeLabel={t('modal.close')}\n title={t('modal.createFailed')}\n footer={(\n \n )}\n >\n
{modalError}
\n \n \n )\n}\n\nfunction installStyles(): () => void {\n const style = document.createElement('style')\n style.dataset.plugin = PACKAGE_ID\n style.textContent = `\n .dsh-projectless-session-error {\n margin-top: 8px;\n color: var(--dsw-alias-state-error-primary);\n font-size: 12px;\n line-height: 18px;\n overflow-wrap: anywhere;\n }\n `\n document.head.appendChild(style)\n return () => { style.remove() }\n}\n\nexport const name = PACKAGE_ID\nexport const inject = ['connection', 'locale', 'slots', 'sessions', 'workspaces']\n\n/** Install a priority -1 picker; DSH's built-in priority 0 picker remains the automatic fallback. */\nexport function apply(ctx: ClientContext): void {\n ctx.effect(installStyles, `${PACKAGE_ID}: styles`)\n ctx.effect(\n () => ctx.locale.register(PROJECTLESS_LOCALE_NS, projectlessLocales),\n `${PACKAGE_ID}: dictionaries`,\n )\n const registry = createProjectlessRegistry()\n const pendingWorkspaceIds = new Set()\n const claim = createAbandonClaim()\n const rpc = ctx.connection.rpc as unknown as ClientConnectionRpc\n const removeDirectory = (path: string) => requestRemoveProjectlessDirectory(rpc, path)\n const isProjectlessWorkspace = (workspace: WorkspaceView): boolean => (\n registry.has(workspace.workspaceId) || isProjectlessPath(workspace.path)\n )\n const directoryFlow: HostObservable = {\n getSnapshot: () => ctx.slots.entries('conversation.hero.workspace.directoryFlow').length > 0,\n subscribe: (listener: () => void) => ctx.slots.subscribe('conversation.hero.workspace.directoryFlow', listener),\n }\n ctx.effect(() => {\n let disposed = false\n let stopSweep = (): void => {}\n void requestProjectlessRoot(rpc).then(root => {\n if (disposed) return\n stopSweep = sweepAbandonedProjectlessWorkspaces(\n ctx.workspaces,\n ctx.sessions,\n root,\n removeDirectory,\n pendingWorkspaceIds,\n claim,\n )\n }).catch(console.error)\n return () => {\n disposed = true\n stopSweep()\n }\n }, `${PACKAGE_ID}: sweep leftover unused workspaces`)\n const actions = (): PickerActions => ({\n createWorkspace: input => ctx.workspaces.create(input),\n isProjectlessWorkspace,\n hooks: { directoryFlow },\n createProjectlessSession: async () => {\n const receipt = await createAndOpenProjectlessSession(\n ctx.workspaces,\n ctx.sessions,\n // The published Connection package augments the same Cordis key with\n // Host and Client faces; this file is bundled only for the Client face.\n () => requestProjectlessDirectory(rpc),\n registry,\n removeDirectory,\n pendingWorkspaceIds,\n )\n const translate = ctx.locale.bind(PROJECTLESS_LOCALE_NS)\n await ctx.workspaces.rename(receipt.workspaceId, translate('picker.projectless')).catch(() => {})\n ctx.effect(\n () => {\n const stop = watchTemporaryWorkspace(\n ctx.workspaces,\n ctx.sessions,\n receipt,\n removeDirectory,\n claim,\n )\n pendingWorkspaceIds.delete(receipt.workspaceId)\n return stop\n },\n `${PACKAGE_ID}: watch ${receipt.sessionId} temporary workspace`,\n )\n return receipt.sessionId\n },\n })\n\n ctx.slots.inject('conversation.hero.workspace', () => ctx.slots.register(\n {\n name: 'conversation.hero.workspace',\n priority: -1,\n children: {\n 'conversation.hero.workspace.directoryFlow': { kind: 'single', scope: 'root' },\n },\n inject: actions,\n locale: PROJECTLESS_LOCALE_NS,\n },\n ProjectlessWorkspacePicker,\n ))\n}\n", "export const DATE_DIRECTORY = /^\\d{4}-\\d{2}-\\d{2}$/\nexport const SESSION_DIRECTORY = /^session-\\d{2}-\\d{2}-\\d{2}-[0-9a-f]{8}$/\n\nconst IGNORABLE_DIRECTORY_ENTRIES = new Set([\n '.DS_Store',\n '.localized',\n 'Thumbs.db',\n 'desktop.ini',\n])\n\n/** Finder/Explorer metadata that must not keep an otherwise empty scratch directory. */\nexport function isIgnorableDirectoryEntry(name: string): boolean {\n return IGNORABLE_DIRECTORY_ENTRIES.has(name) || name.startsWith('._')\n}\n\n/** Normalize separators so Host and browser path checks agree. */\nexport function normalizeFsPath(path: string): string {\n return path.replace(/\\\\/g, '/').replace(/\\/+$/, '')\n}\n\nfunction relativeParts(target: string, root: string): string[] | undefined {\n const path = normalizeFsPath(target)\n const base = normalizeFsPath(root)\n if (base === '' || path === base || !path.startsWith(`${base}/`)) return undefined\n return path.slice(base.length + 1).split('/')\n}\n\n/** True for ROOT/YYYY-MM-DD. */\nexport function isManagedDateDirectoryPath(target: string, root: string): boolean {\n const parts = relativeParts(target, root)\n return parts !== undefined && parts.length === 1 && DATE_DIRECTORY.test(parts[0] ?? '')\n}\n\n/** True for ROOT/YYYY-MM-DD/session-HH-mm-ss-hex. */\nexport function isManagedSessionPath(target: string, root: string): boolean {\n const parts = relativeParts(target, root)\n return parts !== undefined\n && parts.length === 2\n && DATE_DIRECTORY.test(parts[0] ?? '')\n && SESSION_DIRECTORY.test(parts[1] ?? '')\n}\n\n/**\n * Recognize a plugin-provisioned scratch directory by the two-level\n * `YYYY-MM-DD/session-HH-mm-ss-random` shape. Used as a cross-reload\n * fallback when the in-memory registry was never populated.\n */\nexport function isProjectlessPath(path: string): boolean {\n const segments = normalizeFsPath(path).split('/').filter(segment => segment.length > 0)\n const dateName = segments.at(-2)\n const sessionName = segments.at(-1)\n return dateName !== undefined\n && sessionName !== undefined\n && DATE_DIRECTORY.test(dateName)\n && SESSION_DIRECTORY.test(sessionName)\n}\n", "import type { SessionId, WorkspaceId, WorkspaceView } from '@deepseek-ai/dsh-client-runtime/client'\nimport type { ClientConnectionRpc } from '@deepseek-ai/dsh-client-connection/client'\nimport { isManagedSessionPath, isProjectlessPath } from '../shared/paths.ts'\n\nexport { isProjectlessPath }\n\n/** Public DSH faces required to turn a directory into an ungrouped Session. */\nexport interface ProjectlessSessionHost {\n create(input: { path: string }): Promise\n connectWorkspace(workspaceId: WorkspaceId): Promise\n delete(workspaceId: WorkspaceId): Promise\n archiveSession(sessionId: SessionId): Promise\n}\n\nexport interface SessionListSlice {\n current?: SessionId | undefined\n byId: Partial>\n}\n\nexport interface SessionNavigator {\n open(sessionId: SessionId): void\n list: {\n getSnapshot(): SessionListSlice\n subscribe(listener: () => void): () => void\n }\n}\n\nexport interface WorkspaceListSlice {\n items: readonly Pick[]\n baselinesReady?: boolean | undefined\n}\n\nexport interface WorkspaceNavigator {\n list: {\n getSnapshot(): WorkspaceListSlice\n subscribe(listener: () => void): () => void\n }\n}\n\nexport interface ProjectlessSessionReceipt {\n sessionId: SessionId\n workspaceId: WorkspaceId\n path: string\n}\n\nexport interface AbandonedProjectlessWorkspace {\n workspaceId: WorkspaceId\n path: string\n sessionIds: readonly SessionId[]\n}\n\n/** Prevents watch + sweep from both tearing down the same unused Workspace. */\nexport interface AbandonClaim {\n tryClaim(workspaceId: WorkspaceId): boolean\n}\n\nexport function createAbandonClaim(): AbandonClaim {\n const claimed = new Set()\n return {\n tryClaim(workspaceId) {\n if (claimed.has(workspaceId)) return false\n claimed.add(workspaceId)\n return true\n },\n }\n}\n\n/** Menu entry id for the projectless action; also its selected-state key. */\nexport const PROJECTLESS_ENTRY_ID = '::projectless-session'\n\n/** Workspaces this page provisioned, authoritative over the path heuristic. */\nexport interface ProjectlessRegistry {\n remember(workspaceId: WorkspaceId): void\n has(workspaceId: WorkspaceId): boolean\n}\n\nexport function createProjectlessRegistry(): ProjectlessRegistry {\n const ids = new Set()\n return {\n remember(workspaceId) { ids.add(workspaceId) },\n has(workspaceId) { return ids.has(workspaceId) },\n }\n}\n\nexport interface PickerSelection {\n /** Rows a user may meaningfully pick as a project. */\n projects: readonly T[]\n /** Value handed to the Menu: the projectless entry wins when it is active. */\n selectedId: string | undefined\n projectlessActive: boolean\n}\n\n/**\n * Hide temporary scratch workspaces from the picker and move the selected\n * state onto the projectless entry, so the active projectless session reads as\n * \"Session without workspace\" instead of exposing its generated directory.\n */\nexport function resolvePickerSelection(\n items: readonly T[],\n selectedId: WorkspaceId | undefined,\n isProjectless: (row: T) => boolean,\n): PickerSelection {\n const projects: T[] = []\n let projectlessActive = false\n for (const row of items) {\n if (isProjectless(row)) {\n if (selectedId !== undefined && row.workspaceId === selectedId) projectlessActive = true\n continue\n }\n projects.push(row)\n }\n return {\n projects,\n selectedId: projectlessActive ? PROJECTLESS_ENTRY_ID : selectedId,\n projectlessActive,\n }\n}\n\nfunction rpcValue(result: { ok: boolean, value?: unknown, error?: { message: string } }, label: string): unknown {\n if (!result.ok) throw new Error(result.error?.message ?? `${label} failed`)\n return result.value\n}\n\nfunction expectPathObject(value: unknown, key: 'path' | 'root', label: string): string {\n if (typeof value !== 'object' || value === null || typeof (value as Record)[key] !== 'string') {\n throw new Error(`${label} returned an invalid directory response`)\n }\n return (value as Record)[key]\n}\n\n/** Call the plugin Host half and validate its intentionally tiny response. */\nexport async function requestProjectlessDirectory(rpc: ClientConnectionRpc): Promise {\n return expectPathObject(\n rpcValue(await rpc.call('/projectless-session', 'create-directory', {}), 'projectless session Host'),\n 'path',\n 'projectless session Host',\n )\n}\n\nexport async function requestProjectlessRoot(rpc: ClientConnectionRpc): Promise {\n return expectPathObject(\n rpcValue(await rpc.call('/projectless-session', 'get-root', {}), 'projectless session Host'),\n 'root',\n 'projectless session Host',\n )\n}\n\nexport async function requestRemoveProjectlessDirectory(rpc: ClientConnectionRpc, path: string): Promise {\n rpcValue(await rpc.call('/projectless-session', 'remove-directory', { path }), 'projectless session Host')\n}\n\n/**\n * Create and open a Session while retaining its temporary Workspace long\n * enough for DSH's native blank-session composer to accept the first prompt.\n */\nexport async function createAndOpenProjectlessSession(\n workspaces: ProjectlessSessionHost,\n sessions: SessionNavigator,\n provisionDirectory: () => Promise,\n registry?: Pick,\n removeDirectory: (path: string) => Promise = async () => {},\n pending?: Set,\n): Promise {\n const path = await provisionDirectory()\n const workspace = await workspaces.create({ path })\n registry?.remember(workspace.workspaceId)\n pending?.add(workspace.workspaceId)\n try {\n const sessionId = await workspaces.connectWorkspace(workspace.workspaceId)\n sessions.open(sessionId)\n return { sessionId, workspaceId: workspace.workspaceId, path }\n } catch (reason) {\n pending?.delete(workspace.workspaceId)\n // A failed Session creation must not leave a temporary Workspace behind.\n await workspaces.delete(workspace.workspaceId).catch(() => {})\n await removeDirectory(path).catch(() => {})\n throw reason\n }\n}\n\nexport async function abandonUnusedProjectlessWorkspace(\n workspaces: Pick,\n target: AbandonedProjectlessWorkspace,\n removeDirectory: (path: string) => Promise,\n onError: (reason: unknown) => void = console.error,\n): Promise {\n for (const sessionId of target.sessionIds) {\n await workspaces.archiveSession(sessionId).catch(onError)\n }\n await workspaces.delete(target.workspaceId).catch(onError)\n await removeDirectory(target.path).catch(onError)\n}\n\n/**\n * A leftover projectless Workspace is unused when every accounted Session is\n * still blank and none of them is the current Session.\n */\nexport function isAbandonedProjectlessWorkspace(\n workspace: Pick,\n sessions: SessionListSlice,\n root: string,\n skip: ReadonlySet = new Set(),\n): boolean {\n if (skip.has(workspace.workspaceId) || !isManagedSessionPath(workspace.path, root)) return false\n // A just-created Workspace has no sessions yet. Sweeping that empty row\n // races create\u2192connectWorkspace and surfaces workspace-not-found.\n if (workspace.sessionIds.length === 0) return false\n for (const sessionId of workspace.sessionIds) {\n if (sessions.current === sessionId) return false\n const row = sessions.byId[sessionId]\n if (row === undefined || row.blank !== true) return false\n }\n return true\n}\n\nexport function findAbandonedProjectlessWorkspaces(\n workspaces: readonly Pick[],\n sessions: SessionListSlice,\n root: string,\n skip: ReadonlySet = new Set(),\n): AbandonedProjectlessWorkspace[] {\n return workspaces\n .filter(workspace => isAbandonedProjectlessWorkspace(workspace, sessions, root, skip))\n .map(workspace => ({\n workspaceId: workspace.workspaceId,\n path: workspace.path,\n sessionIds: workspace.sessionIds,\n }))\n}\n\nfunction unusedReceipt(receipt: ProjectlessSessionReceipt): AbandonedProjectlessWorkspace {\n return {\n workspaceId: receipt.workspaceId,\n path: receipt.path,\n sessionIds: [receipt.sessionId],\n }\n}\n\n/**\n * Keep the temporary Workspace while the blank Session is current. Detach the\n * registration after the first accepted prompt; abandon it if the user leaves\n * without sending, or if this watcher is disposed while still blank.\n */\nexport function watchTemporaryWorkspace(\n workspaces: Pick,\n sessions: SessionNavigator,\n receipt: ProjectlessSessionReceipt,\n removeDirectory: (path: string) => Promise,\n claim: AbandonClaim = createAbandonClaim(),\n onError: (reason: unknown) => void = console.error,\n): () => void {\n let active = true\n let unsubscribe = (): void => {}\n\n const finish = (work: () => Promise): void => {\n if (!active) return\n active = false\n unsubscribe()\n void work().catch(onError)\n }\n\n const abandon = (): void => {\n if (!claim.tryClaim(receipt.workspaceId)) {\n active = false\n unsubscribe()\n return\n }\n finish(() => abandonUnusedProjectlessWorkspace(workspaces, unusedReceipt(receipt), removeDirectory, onError))\n }\n\n const reconcile = (): void => {\n if (!active) return\n const snapshot = sessions.list.getSnapshot()\n if (snapshot.byId[receipt.sessionId]?.blank === false) {\n finish(async () => { await workspaces.delete(receipt.workspaceId) })\n return\n }\n if (snapshot.current === receipt.sessionId) return\n abandon()\n }\n\n unsubscribe = sessions.list.subscribe(reconcile)\n reconcile()\n return () => {\n if (!active) return\n const snapshot = sessions.list.getSnapshot()\n if (snapshot.byId[receipt.sessionId]?.blank === false) {\n finish(async () => { await workspaces.delete(receipt.workspaceId) })\n return\n }\n abandon()\n }\n}\n\n/**\n * Remove only the Workspace registration once DSH reports the first prompt\n * accepted (`blank === false`). The live/cold Session, cwd, log, and directory\n * remain authoritative under DSH's Workspace deletion contract.\n */\nexport function detachWorkspaceAfterFirstPrompt(\n workspaces: Pick,\n sessions: SessionNavigator,\n receipt: ProjectlessSessionReceipt,\n onError: (reason: unknown) => void = console.error,\n): () => void {\n let active = true\n let unsubscribe = (): void => {}\n const reconcile = (): void => {\n if (!active || sessions.list.getSnapshot().byId[receipt.sessionId]?.blank !== false) return\n active = false\n unsubscribe()\n void workspaces.delete(receipt.workspaceId).catch(onError)\n }\n unsubscribe = sessions.list.subscribe(reconcile)\n reconcile()\n return () => {\n active = false\n unsubscribe()\n }\n}\n\n/** Remove leftover unused projectless Workspaces after a previous DSH run. */\nexport function sweepAbandonedProjectlessWorkspaces(\n workspaces: Pick & WorkspaceNavigator,\n sessions: SessionNavigator,\n root: string,\n removeDirectory: (path: string) => Promise,\n skip: ReadonlySet,\n claim: AbandonClaim,\n onError: (reason: unknown) => void = console.error,\n): () => void {\n let active = true\n const reconcile = (): void => {\n if (!active) return\n const workspaceState = workspaces.list.getSnapshot()\n if (workspaceState.baselinesReady === false) return\n for (const leftover of findAbandonedProjectlessWorkspaces(\n workspaceState.items,\n sessions.list.getSnapshot(),\n root,\n skip,\n )) {\n if (!claim.tryClaim(leftover.workspaceId)) continue\n void abandonUnusedProjectlessWorkspace(workspaces, leftover, removeDirectory, onError)\n }\n }\n const unsubscribeWorkspaces = workspaces.list.subscribe(reconcile)\n const unsubscribeSessions = sessions.list.subscribe(reconcile)\n reconcile()\n return () => {\n active = false\n unsubscribeWorkspaces()\n unsubscribeSessions()\n }\n}\n", "import type { LocaleDictOf } from '@deepseek-ai/dsh-client-ui-slots'\n\nexport const PROJECTLESS_LOCALE_NS = 'projectless-session'\n\nexport const zh = {\n 'picker.projectless': '\u65E0\u5DE5\u4F5C\u533A\u4F1A\u8BDD',\n 'picker.addWorkspace': '\u6DFB\u52A0\u5DE5\u4F5C\u533A\u2026',\n 'modal.createFailed': '\u65E0\u6CD5\u521B\u5EFA\u65E0\u5DE5\u4F5C\u533A\u4F1A\u8BDD',\n 'modal.close': '\u77E5\u9053\u4E86',\n} as const\n\nexport type ProjectlessLocaleKey = keyof typeof zh\n\nexport const en: Record = {\n 'picker.projectless': 'Session without workspace',\n 'picker.addWorkspace': 'Add workspace\u2026',\n 'modal.createFailed': 'Could not create session without workspace',\n 'modal.close': 'Got it',\n}\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** Projectless-session picker and error copy. */\n 'projectless-session': ProjectlessLocaleKey\n }\n}\n\n/** Typed bilingual dictionaries consumed by the DSH locale runtime. */\nexport const projectlessLocales: Record<'zh' | 'en', LocaleDictOf> = {\n zh,\n en,\n}\n"], - "mappings": "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", + "sources": ["../src/client/index.tsx", "../src/shared/paths.ts", "../src/shared/rpc.ts", "../src/client/session.ts", "../src/client/locales.ts"], + "sourcesContent": ["import { useCallback, useEffect, useState } from 'react'\nimport type { RefObject } from 'react'\nimport type { Context } from '@deepseek-ai/cordis'\nimport type { SessionId } from '@deepseek-ai/dsh-session/types'\nimport type { WorkspaceId, WorkspaceView } from '@deepseek-ai/dsh-api-workspace-controller/client'\nimport type {} from '@deepseek-ai/dsh-api-session-controller/client'\nimport type { ClientConnectionRpc } from '@deepseek-ai/dsh-client-connection/client'\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\nimport type { SlotRegistry } from '@deepseek-ai/dsh-client-ui-renderer/client'\nimport type {\n HostObservable,\n PropsHooks,\n PropsLocale,\n PropsRenderSlots,\n PropsRuntime,\n StoredEntry,\n} from '@deepseek-ai/dsh-client-ui-slots'\nimport type { DirectoryFlowOwnerProps, UiWorkspace } from '@deepseek-ai/dsh-client-ui-workspace/client'\nimport {\n Button,\n IconFolderCloseRegular,\n IconNewChatOutlineRegular,\n IconPlusOutlineRegular,\n Menu,\n Modal,\n type MenuEntry,\n} from '@deepseek-ai/dsh-client-ui-primitives'\nimport {\n PROJECTLESS_ENTRY_ID,\n createAbandonClaim,\n createAndOpenProjectlessSession,\n createProjectlessRegistry,\n isProjectlessPath,\n requestProjectlessDirectory,\n requestProjectlessRoot,\n requestRemoveProjectlessDirectory,\n resolvePickerSelection,\n sweepAbandonedProjectlessWorkspaces,\n watchTemporaryWorkspace,\n} from './session.ts'\nimport { PROJECTLESS_LOCALE_NS, projectlessLocales } from './locales.ts'\n\nconst PACKAGE_ID = 'dsh-projectless-session'\nconst PROJECTLESS = PROJECTLESS_ENTRY_ID\nconst ADD_WORKSPACE = '::add-workspace'\nconst DSH_DIRECTORY_FLOW = 'conversation.hero.workspace.directoryFlow'\nconst DIRECTORY_FLOW = 'dsh-projectless-session.directoryFlow'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface SlotMap {\n /** This picker's own directory-flow hole, mirrored from DSH's hero hole. */\n 'dsh-projectless-session.directoryFlow': {\n kind: 'single'\n scope: 'root'\n owner: DirectoryFlowOwnerProps\n }\n }\n}\n\ninterface PickerActions {\n createWorkspace(input: { path: string }): Promise\n createProjectlessSession(): Promise\n isProjectlessWorkspace(workspace: WorkspaceView): boolean\n hooks: {\n directoryFlow: HostObservable\n }\n}\n\ntype PickerProps =\n PropsRuntime<'conversation.hero.workspace'>\n & PropsRenderSlots\n & Omit\n & PropsHooks\n & PropsLocale\n\nfunction errorMessage(reason: unknown): string {\n return reason instanceof Error ? reason.message : String(reason)\n}\n\n/** Shadow-compatible replacement for the built-in WorkspacePicker. */\nfunction ProjectlessWorkspacePicker({\n open,\n anchorRef,\n selectedId,\n onPick,\n onClose,\n useWorkspaces,\n createWorkspace,\n createProjectlessSession,\n isProjectlessWorkspace,\n useDirectoryFlow,\n renderSlot,\n t,\n}: PickerProps) {\n const workspaceState = useWorkspaces((state: { items: readonly WorkspaceView[] }) => state)\n const [busy, setBusy] = useState(false)\n const [flowOpen, setFlowOpen] = useState(false)\n const [modalError, setModalError] = useState(null)\n const getAnchorRect = useCallback(\n () => (anchorRef as RefObject | undefined)?.current?.getBoundingClientRect() ?? null,\n [anchorRef],\n )\n\n const selection = resolvePickerSelection(\n workspaceState.items,\n selectedId,\n isProjectlessWorkspace,\n )\n const workspaceItems: MenuEntry[] = selection.projects.map(workspace => ({\n id: workspace.workspaceId,\n label: workspace.title,\n icon: ,\n disabled: busy,\n }))\n const flowAvailable = useDirectoryFlow((occupied: boolean) => occupied)\n useEffect(() => {\n if (flowOpen && !flowAvailable) setFlowOpen(false)\n }, [flowOpen, flowAvailable])\n const footer: MenuEntry[] = [\n {\n id: PROJECTLESS,\n label: t('picker.projectless'),\n icon: ,\n disabled: busy,\n },\n ...flowAvailable ? [\n { type: 'separator' as const, id: 'projectless-separator' },\n {\n id: ADD_WORKSPACE,\n label: t('picker.addWorkspace'),\n icon: ,\n disabled: busy,\n },\n ] : [],\n ]\n\n const run = (operation: () => Promise): void => {\n if (busy) return\n setBusy(true)\n void operation().catch((reason: unknown) => {\n setModalError(errorMessage(reason))\n }).finally(() => {\n setBusy(false)\n })\n }\n\n const handleSelect = (id: string): void => {\n if (id === PROJECTLESS) {\n onClose()\n run(async () => { await createProjectlessSession() })\n return\n }\n if (id === ADD_WORKSPACE) {\n onClose()\n setModalError(null)\n setFlowOpen(true)\n return\n }\n onPick(id as WorkspaceId)\n }\n\n const directoryFlowOwner: DirectoryFlowOwnerProps = {\n open: flowOpen,\n busy,\n onPicked: path => {\n run(async () => {\n try {\n const workspace = await createWorkspace({ path })\n onPick(workspace.workspaceId)\n } finally {\n setFlowOpen(false)\n }\n })\n },\n onCancel: () => { setFlowOpen(false) },\n onError: message => {\n setFlowOpen(false)\n setModalError(message)\n },\n }\n\n return (\n <>\n \n {renderSlot(DIRECTORY_FLOW, directoryFlowOwner)}\n { setModalError(null) }}\n closeLabel={t('modal.close')}\n title={t('modal.createFailed')}\n footer={(\n \n )}\n >\n
{modalError}
\n \n \n )\n}\n\nfunction installStyles(): () => void {\n const style = document.createElement('style')\n style.dataset.plugin = PACKAGE_ID\n style.textContent = `\n .dsh-projectless-session-error {\n margin-top: 8px;\n color: var(--dsw-alias-state-error-primary);\n font-size: 12px;\n line-height: 18px;\n overflow-wrap: anywhere;\n }\n `\n document.head.appendChild(style)\n return () => { style.remove() }\n}\n\ntype LooseRegister = (options: Record, component: unknown) => () => void\n\n/**\n * DSH 0.2 lets exactly one entry declare a hole and render only holes it\n * declared, and the built-in picker already declares the hero directory-flow\n * hole. Re-register whatever occupies that hole (the native chooser or the\n * in-app browser) into this picker's own hole so \"Add workspace\u2026\" keeps the\n * composed picking interaction.\n */\nfunction mirrorDirectoryFlow(slots: SlotRegistry): () => void {\n const register = slots.register.bind(slots) as unknown as LooseRegister\n let mirrored: readonly StoredEntry[] | undefined\n let disposers: (() => void)[] = []\n const release = (): void => {\n for (const dispose of disposers) dispose()\n disposers = []\n }\n const sync = (): void => {\n const entries = slots.entries(DSH_DIRECTORY_FLOW)\n if (entries === mirrored) return\n mirrored = entries\n release()\n disposers = entries.map(entry => register({\n name: DIRECTORY_FLOW,\n ...entry.options.priority === undefined ? {} : { priority: entry.options.priority },\n ...entry.inject === undefined ? {} : { inject: entry.inject },\n ...entry.locale === undefined ? {} : { locale: entry.locale },\n }, entry.component))\n }\n const unsubscribe = slots.subscribe(DSH_DIRECTORY_FLOW, sync)\n sync()\n return () => {\n unsubscribe()\n release()\n }\n}\n\nexport const name = PACKAGE_ID\nexport const inject = ['connection', 'locale', 'slots', 'sessions', 'workspaces', 'uiWorkspace']\n\n/**\n * DSH 0.2 split the old combined Workspace face: registrations live on\n * `ctx.workspaces`, blank-Session connection, navigation and archival on\n * `ctx.uiWorkspace`.\n */\nfunction projectlessHost(ctx: Context) {\n const workspaces = ctx.workspaces\n const ui: UiWorkspace = ctx.uiWorkspace\n const sessions = {\n list: ctx.sessions.list,\n open: (sessionId: SessionId) => { ui.openSession(sessionId) },\n }\n const host = {\n list: workspaces.list,\n create: (input: { path: string }) => workspaces.create(input),\n delete: (workspaceId: WorkspaceId) => workspaces.delete(workspaceId),\n rename: (workspaceId: WorkspaceId, title: string) => workspaces.rename(workspaceId, title),\n connectWorkspace: (workspaceId: WorkspaceId) => ui.connectWorkspace(workspaceId),\n archiveSession: (sessionId: SessionId) => ui.archiveSession(sessionId),\n }\n return { host, sessions }\n}\n\n/** Install a priority -1 picker; DSH's built-in priority 0 picker remains the automatic fallback. */\nexport function apply(ctx: Context): void {\n const { host, sessions } = projectlessHost(ctx)\n ctx.effect(installStyles, `${PACKAGE_ID}: styles`)\n ctx.effect(\n () => ctx.locale.register(PROJECTLESS_LOCALE_NS, projectlessLocales),\n `${PACKAGE_ID}: dictionaries`,\n )\n const registry = createProjectlessRegistry()\n const pendingWorkspaceIds = new Set()\n const claim = createAbandonClaim()\n const rpc = ctx.connection.rpc as unknown as ClientConnectionRpc\n const removeDirectory = (path: string) => requestRemoveProjectlessDirectory(rpc, path)\n const isProjectlessWorkspace = (workspace: WorkspaceView): boolean => (\n registry.has(workspace.workspaceId) || isProjectlessPath(workspace.path)\n )\n const directoryFlow: HostObservable = {\n getSnapshot: () => ctx.slots.entries(DIRECTORY_FLOW).length > 0,\n subscribe: (listener: () => void) => ctx.slots.subscribe(DIRECTORY_FLOW, listener),\n }\n ctx.effect(() => {\n let disposed = false\n let stopSweep = (): void => {}\n void requestProjectlessRoot(rpc).then(root => {\n if (disposed) return\n stopSweep = sweepAbandonedProjectlessWorkspaces(\n host,\n sessions,\n root,\n removeDirectory,\n pendingWorkspaceIds,\n claim,\n )\n }).catch(console.error)\n return () => {\n disposed = true\n stopSweep()\n }\n }, `${PACKAGE_ID}: sweep leftover unused workspaces`)\n const actions = (): PickerActions => ({\n createWorkspace: input => host.create(input),\n isProjectlessWorkspace,\n hooks: { directoryFlow },\n createProjectlessSession: async () => {\n const receipt = await createAndOpenProjectlessSession(\n host,\n sessions,\n // The published Connection package augments the same Cordis key with\n // Host and Client faces; this file is bundled only for the Client face.\n () => requestProjectlessDirectory(rpc),\n registry,\n removeDirectory,\n pendingWorkspaceIds,\n )\n const translate = ctx.locale.bind(PROJECTLESS_LOCALE_NS)\n await host.rename(receipt.workspaceId, translate('picker.projectless')).catch(() => {})\n ctx.effect(\n () => {\n const stop = watchTemporaryWorkspace(\n host,\n sessions,\n receipt,\n removeDirectory,\n claim,\n )\n pendingWorkspaceIds.delete(receipt.workspaceId)\n return stop\n },\n `${PACKAGE_ID}: watch ${receipt.sessionId} temporary workspace`,\n )\n return receipt.sessionId\n },\n })\n\n ctx.slots.inject('conversation.hero.workspace', () => ctx.slots.register(\n {\n name: 'conversation.hero.workspace',\n priority: -1,\n children: {\n [DIRECTORY_FLOW]: { kind: 'single', scope: 'root' },\n },\n inject: actions,\n locale: PROJECTLESS_LOCALE_NS,\n },\n ProjectlessWorkspacePicker,\n ))\n ctx.slots.inject(DIRECTORY_FLOW, () => mirrorDirectoryFlow(ctx.slots))\n}\n", "export const DATE_DIRECTORY = /^\\d{4}-\\d{2}-\\d{2}$/\nexport const SESSION_DIRECTORY = /^session-\\d{2}-\\d{2}-\\d{2}-[0-9a-f]{8}$/\n\nconst IGNORABLE_DIRECTORY_ENTRIES = new Set([\n '.DS_Store',\n '.localized',\n 'Thumbs.db',\n 'desktop.ini',\n])\n\n/** Finder/Explorer metadata that must not keep an otherwise empty scratch directory. */\nexport function isIgnorableDirectoryEntry(name: string): boolean {\n return IGNORABLE_DIRECTORY_ENTRIES.has(name) || name.startsWith('._')\n}\n\n/** Normalize separators so Host and browser path checks agree. */\nexport function normalizeFsPath(path: string): string {\n return path.replace(/\\\\/g, '/').replace(/\\/+$/, '')\n}\n\nfunction relativeParts(target: string, root: string): string[] | undefined {\n const path = normalizeFsPath(target)\n const base = normalizeFsPath(root)\n if (base === '' || path === base || !path.startsWith(`${base}/`)) return undefined\n return path.slice(base.length + 1).split('/')\n}\n\n/** True for ROOT/YYYY-MM-DD. */\nexport function isManagedDateDirectoryPath(target: string, root: string): boolean {\n const parts = relativeParts(target, root)\n return parts !== undefined && parts.length === 1 && DATE_DIRECTORY.test(parts[0] ?? '')\n}\n\n/** True for ROOT/YYYY-MM-DD/session-HH-mm-ss-hex. */\nexport function isManagedSessionPath(target: string, root: string): boolean {\n const parts = relativeParts(target, root)\n return parts !== undefined\n && parts.length === 2\n && DATE_DIRECTORY.test(parts[0] ?? '')\n && SESSION_DIRECTORY.test(parts[1] ?? '')\n}\n\n/**\n * Recognize a plugin-provisioned scratch directory by the two-level\n * `YYYY-MM-DD/session-HH-mm-ss-random` shape. Used as a cross-reload\n * fallback when the in-memory registry was never populated.\n */\nexport function isProjectlessPath(path: string): boolean {\n const segments = normalizeFsPath(path).split('/').filter(segment => segment.length > 0)\n const dateName = segments.at(-2)\n const sessionName = segments.at(-1)\n return dateName !== undefined\n && sessionName !== undefined\n && DATE_DIRECTORY.test(dateName)\n && SESSION_DIRECTORY.test(sessionName)\n}\n", "/**\n * Wire address of the Host half. DSH 0.2's `connection.rpc.handle` resolves\n * `webServer` from the Connection plugin's own fiber, so third-party channels\n * cannot mount; the endpoints ride the already-authenticated shared `/api`\n * channel as exact Fetch routes instead.\n */\nexport const PROJECTLESS_RPC_CHANNEL = '/api'\nexport const PROJECTLESS_RPC_PREFIX = 'projectless-session'\nexport const PROJECTLESS_ENDPOINTS = ['create-directory', 'get-root', 'remove-directory'] as const\nexport type ProjectlessEndpoint = typeof PROJECTLESS_ENDPOINTS[number]\n\n/** Channel-relative endpoint passed to `ClientConnectionRpc.call`. */\nexport function projectlessEndpoint(endpoint: ProjectlessEndpoint): string {\n return `${PROJECTLESS_RPC_PREFIX}/${endpoint}`\n}\n", "import type { SessionId } from '@deepseek-ai/dsh-session/types'\nimport type { WorkspaceId, WorkspaceView } from '@deepseek-ai/dsh-api-workspace-controller/client'\nimport type { ClientConnectionRpc } from '@deepseek-ai/dsh-client-connection/client'\nimport { isManagedSessionPath, isProjectlessPath } from '../shared/paths.ts'\nimport { PROJECTLESS_RPC_CHANNEL, projectlessEndpoint } from '../shared/rpc.ts'\n\nexport { isProjectlessPath }\n\n/**\n * Public DSH faces required to turn a directory into an ungrouped Session:\n * Workspace registration from `ctx.workspaces`, blank-Session connection and\n * archival from `ctx.uiWorkspace`.\n */\nexport interface ProjectlessSessionHost {\n create(input: { path: string }): Promise\n connectWorkspace(workspaceId: WorkspaceId): Promise\n delete(workspaceId: WorkspaceId): Promise\n archiveSession(sessionId: SessionId): Promise\n}\n\n/** Reference source DSH's main Conversation view retains its Session under. */\nexport const MAIN_VIEW_SOURCE = 'mainView'\n\nexport interface SessionRowSlice {\n blank: boolean\n retainedBy?: Readonly>> | undefined\n}\n\nexport interface SessionListSlice {\n byId: Partial>\n}\n\nexport interface SessionNavigator {\n open(sessionId: SessionId): void\n list: {\n getSnapshot(): SessionListSlice\n subscribe(listener: () => void): () => void\n }\n}\n\nexport interface WorkspaceListSlice {\n items: readonly Pick[]\n phase?: 'pending' | 'ready' | undefined\n}\n\n/** DSH 0.2 has no `current` field; the main view holds a `mainView` reference instead. */\nexport function isCurrentSession(sessions: SessionListSlice, sessionId: SessionId): boolean {\n return (sessions.byId[sessionId]?.retainedBy?.[MAIN_VIEW_SOURCE] ?? 0) > 0\n}\n\nexport interface WorkspaceNavigator {\n list: {\n getSnapshot(): WorkspaceListSlice\n subscribe(listener: () => void): () => void\n }\n}\n\nexport interface ProjectlessSessionReceipt {\n sessionId: SessionId\n workspaceId: WorkspaceId\n path: string\n}\n\nexport interface AbandonedProjectlessWorkspace {\n workspaceId: WorkspaceId\n path: string\n sessionIds: readonly SessionId[]\n}\n\n/** Prevents watch + sweep from both tearing down the same unused Workspace. */\nexport interface AbandonClaim {\n tryClaim(workspaceId: WorkspaceId): boolean\n}\n\nexport function createAbandonClaim(): AbandonClaim {\n const claimed = new Set()\n return {\n tryClaim(workspaceId) {\n if (claimed.has(workspaceId)) return false\n claimed.add(workspaceId)\n return true\n },\n }\n}\n\n/** Menu entry id for the projectless action; also its selected-state key. */\nexport const PROJECTLESS_ENTRY_ID = '::projectless-session'\n\n/** Workspaces this page provisioned, authoritative over the path heuristic. */\nexport interface ProjectlessRegistry {\n remember(workspaceId: WorkspaceId): void\n has(workspaceId: WorkspaceId): boolean\n}\n\nexport function createProjectlessRegistry(): ProjectlessRegistry {\n const ids = new Set()\n return {\n remember(workspaceId) { ids.add(workspaceId) },\n has(workspaceId) { return ids.has(workspaceId) },\n }\n}\n\nexport interface PickerSelection {\n /** Rows a user may meaningfully pick as a project. */\n projects: readonly T[]\n /** Value handed to the Menu: the projectless entry wins when it is active. */\n selectedId: string | undefined\n projectlessActive: boolean\n}\n\n/**\n * Hide temporary scratch workspaces from the picker and move the selected\n * state onto the projectless entry, so the active projectless session reads as\n * \"Session without workspace\" instead of exposing its generated directory.\n */\nexport function resolvePickerSelection(\n items: readonly T[],\n selectedId: WorkspaceId | undefined,\n isProjectless: (row: T) => boolean,\n): PickerSelection {\n const projects: T[] = []\n let projectlessActive = false\n for (const row of items) {\n if (isProjectless(row)) {\n if (selectedId !== undefined && row.workspaceId === selectedId) projectlessActive = true\n continue\n }\n projects.push(row)\n }\n return {\n projects,\n selectedId: projectlessActive ? PROJECTLESS_ENTRY_ID : selectedId,\n projectlessActive,\n }\n}\n\nfunction rpcValue(result: { ok: boolean, value?: unknown, error?: { message: string } }, label: string): unknown {\n if (!result.ok) throw new Error(result.error?.message ?? `${label} failed`)\n return result.value\n}\n\nfunction expectPathObject(value: unknown, key: 'path' | 'root', label: string): string {\n if (typeof value !== 'object' || value === null || typeof (value as Record)[key] !== 'string') {\n throw new Error(`${label} returned an invalid directory response`)\n }\n return (value as Record)[key]\n}\n\n/** Call the plugin Host half and validate its intentionally tiny response. */\nexport async function requestProjectlessDirectory(rpc: ClientConnectionRpc): Promise {\n return expectPathObject(\n rpcValue(await rpc.call(PROJECTLESS_RPC_CHANNEL, projectlessEndpoint('create-directory'), {}), 'projectless session Host'),\n 'path',\n 'projectless session Host',\n )\n}\n\nexport async function requestProjectlessRoot(rpc: ClientConnectionRpc): Promise {\n return expectPathObject(\n rpcValue(await rpc.call(PROJECTLESS_RPC_CHANNEL, projectlessEndpoint('get-root'), {}), 'projectless session Host'),\n 'root',\n 'projectless session Host',\n )\n}\n\nexport async function requestRemoveProjectlessDirectory(rpc: ClientConnectionRpc, path: string): Promise {\n rpcValue(await rpc.call(PROJECTLESS_RPC_CHANNEL, projectlessEndpoint('remove-directory'), { path }), 'projectless session Host')\n}\n\n/**\n * Create and open a Session while retaining its temporary Workspace long\n * enough for DSH's native blank-session composer to accept the first prompt.\n */\nexport async function createAndOpenProjectlessSession(\n workspaces: ProjectlessSessionHost,\n sessions: SessionNavigator,\n provisionDirectory: () => Promise,\n registry?: Pick,\n removeDirectory: (path: string) => Promise = async () => {},\n pending?: Set,\n): Promise {\n const path = await provisionDirectory()\n const workspace = await workspaces.create({ path })\n registry?.remember(workspace.workspaceId)\n pending?.add(workspace.workspaceId)\n try {\n const sessionId = await workspaces.connectWorkspace(workspace.workspaceId)\n sessions.open(sessionId)\n return { sessionId, workspaceId: workspace.workspaceId, path }\n } catch (reason) {\n pending?.delete(workspace.workspaceId)\n // A failed Session creation must not leave a temporary Workspace behind.\n await workspaces.delete(workspace.workspaceId).catch(() => {})\n await removeDirectory(path).catch(() => {})\n throw reason\n }\n}\n\nexport async function abandonUnusedProjectlessWorkspace(\n workspaces: Pick,\n target: AbandonedProjectlessWorkspace,\n removeDirectory: (path: string) => Promise,\n onError: (reason: unknown) => void = console.error,\n): Promise {\n for (const sessionId of target.sessionIds) {\n await workspaces.archiveSession(sessionId).catch(onError)\n }\n await workspaces.delete(target.workspaceId).catch(onError)\n await removeDirectory(target.path).catch(onError)\n}\n\n/**\n * A leftover projectless Workspace is unused when every accounted Session is\n * still blank and none of them is the current Session.\n */\nexport function isAbandonedProjectlessWorkspace(\n workspace: Pick,\n sessions: SessionListSlice,\n root: string,\n skip: ReadonlySet = new Set(),\n): boolean {\n if (skip.has(workspace.workspaceId) || !isManagedSessionPath(workspace.path, root)) return false\n // A just-created Workspace has no sessions yet. Sweeping that empty row\n // races create\u2192connectWorkspace and surfaces workspace-not-found.\n if (workspace.sessionIds.length === 0) return false\n for (const sessionId of workspace.sessionIds) {\n if (isCurrentSession(sessions, sessionId)) return false\n const row = sessions.byId[sessionId]\n if (row === undefined || row.blank !== true) return false\n }\n return true\n}\n\nexport function findAbandonedProjectlessWorkspaces(\n workspaces: readonly Pick[],\n sessions: SessionListSlice,\n root: string,\n skip: ReadonlySet = new Set(),\n): AbandonedProjectlessWorkspace[] {\n return workspaces\n .filter(workspace => isAbandonedProjectlessWorkspace(workspace, sessions, root, skip))\n .map(workspace => ({\n workspaceId: workspace.workspaceId,\n path: workspace.path,\n sessionIds: workspace.sessionIds,\n }))\n}\n\nfunction unusedReceipt(receipt: ProjectlessSessionReceipt): AbandonedProjectlessWorkspace {\n return {\n workspaceId: receipt.workspaceId,\n path: receipt.path,\n sessionIds: [receipt.sessionId],\n }\n}\n\n/**\n * Keep the temporary Workspace while the blank Session is current. Detach the\n * registration after the first accepted prompt; abandon it if the user leaves\n * without sending, or if this watcher is disposed while still blank.\n */\nexport function watchTemporaryWorkspace(\n workspaces: Pick,\n sessions: SessionNavigator,\n receipt: ProjectlessSessionReceipt,\n removeDirectory: (path: string) => Promise,\n claim: AbandonClaim = createAbandonClaim(),\n onError: (reason: unknown) => void = console.error,\n): () => void {\n let active = true\n let unsubscribe = (): void => {}\n\n const finish = (work: () => Promise): void => {\n if (!active) return\n active = false\n unsubscribe()\n void work().catch(onError)\n }\n\n const abandon = (): void => {\n if (!claim.tryClaim(receipt.workspaceId)) {\n active = false\n unsubscribe()\n return\n }\n finish(() => abandonUnusedProjectlessWorkspace(workspaces, unusedReceipt(receipt), removeDirectory, onError))\n }\n\n const reconcile = (): void => {\n if (!active) return\n const snapshot = sessions.list.getSnapshot()\n if (snapshot.byId[receipt.sessionId]?.blank === false) {\n finish(async () => { await workspaces.delete(receipt.workspaceId) })\n return\n }\n if (isCurrentSession(snapshot, receipt.sessionId)) return\n abandon()\n }\n\n unsubscribe = sessions.list.subscribe(reconcile)\n reconcile()\n return () => {\n if (!active) return\n const snapshot = sessions.list.getSnapshot()\n if (snapshot.byId[receipt.sessionId]?.blank === false) {\n finish(async () => { await workspaces.delete(receipt.workspaceId) })\n return\n }\n abandon()\n }\n}\n\n/**\n * Remove only the Workspace registration once DSH reports the first prompt\n * accepted (`blank === false`). The live/cold Session, cwd, log, and directory\n * remain authoritative under DSH's Workspace deletion contract.\n */\nexport function detachWorkspaceAfterFirstPrompt(\n workspaces: Pick,\n sessions: SessionNavigator,\n receipt: ProjectlessSessionReceipt,\n onError: (reason: unknown) => void = console.error,\n): () => void {\n let active = true\n let unsubscribe = (): void => {}\n const reconcile = (): void => {\n if (!active || sessions.list.getSnapshot().byId[receipt.sessionId]?.blank !== false) return\n active = false\n unsubscribe()\n void workspaces.delete(receipt.workspaceId).catch(onError)\n }\n unsubscribe = sessions.list.subscribe(reconcile)\n reconcile()\n return () => {\n active = false\n unsubscribe()\n }\n}\n\n/** Remove leftover unused projectless Workspaces after a previous DSH run. */\nexport function sweepAbandonedProjectlessWorkspaces(\n workspaces: Pick & WorkspaceNavigator,\n sessions: SessionNavigator,\n root: string,\n removeDirectory: (path: string) => Promise,\n skip: ReadonlySet,\n claim: AbandonClaim,\n onError: (reason: unknown) => void = console.error,\n): () => void {\n let active = true\n const reconcile = (): void => {\n if (!active) return\n const workspaceState = workspaces.list.getSnapshot()\n if (workspaceState.phase === 'pending') return\n for (const leftover of findAbandonedProjectlessWorkspaces(\n workspaceState.items,\n sessions.list.getSnapshot(),\n root,\n skip,\n )) {\n if (!claim.tryClaim(leftover.workspaceId)) continue\n void abandonUnusedProjectlessWorkspace(workspaces, leftover, removeDirectory, onError)\n }\n }\n const unsubscribeWorkspaces = workspaces.list.subscribe(reconcile)\n const unsubscribeSessions = sessions.list.subscribe(reconcile)\n reconcile()\n return () => {\n active = false\n unsubscribeWorkspaces()\n unsubscribeSessions()\n }\n}\n", "import type { LocaleDictOf } from '@deepseek-ai/dsh-client-ui-slots'\n\nexport const PROJECTLESS_LOCALE_NS = 'projectless-session'\n\nexport const zh = {\n 'picker.projectless': '\u65E0\u5DE5\u4F5C\u533A\u4F1A\u8BDD',\n 'picker.addWorkspace': '\u6DFB\u52A0\u5DE5\u4F5C\u533A\u2026',\n 'modal.createFailed': '\u65E0\u6CD5\u521B\u5EFA\u65E0\u5DE5\u4F5C\u533A\u4F1A\u8BDD',\n 'modal.close': '\u77E5\u9053\u4E86',\n} as const\n\nexport type ProjectlessLocaleKey = keyof typeof zh\n\nexport const en: Record = {\n 'picker.projectless': 'Session without workspace',\n 'picker.addWorkspace': 'Add workspace\u2026',\n 'modal.createFailed': 'Could not create session without workspace',\n 'modal.close': 'Got it',\n}\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** Projectless-session picker and error copy. */\n 'projectless-session': ProjectlessLocaleKey\n }\n}\n\n/** Typed bilingual dictionaries consumed by the DSH locale runtime. */\nexport const projectlessLocales: Record<'zh' | 'en', LocaleDictOf> = {\n zh,\n en,\n}\n"], + "mappings": "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", "names": [] } diff --git a/lib/index.js b/lib/index.js index 6f5af54..e952110 100644 --- a/lib/index.js +++ b/lib/index.js @@ -140,6 +140,14 @@ async function removeUnusedProjectlessDirectory(root, requestedPath) { return "removed"; } +// src/shared/rpc.ts +var PROJECTLESS_RPC_CHANNEL = "/api"; +var PROJECTLESS_RPC_PREFIX = "projectless-session"; +var PROJECTLESS_ENDPOINTS = ["create-directory", "get-root", "remove-directory"]; +function projectlessEndpoint(endpoint) { + return `${PROJECTLESS_RPC_PREFIX}/${endpoint}`; +} + // src/index.ts var name = "dsh-projectless-session"; var inject = ["connection"]; @@ -168,36 +176,66 @@ function internalError(message) { } }; } +async function dispatchProjectlessEndpoint(root, endpoint, payload) { + try { + if (endpoint === "create-directory") { + return { ok: true, value: { path: await createProjectlessDirectory(root) } }; + } + if (endpoint === "get-root") { + return { ok: true, value: { root: await resolveProjectlessRoot(root) } }; + } + const path = pathPayload(payload); + if (path === void 0) return badRequest("remove-directory requires { path }"); + return { ok: true, value: { result: await removeUnusedProjectlessDirectory(root, path) } }; + } catch (reason) { + const message = reason instanceof Error ? reason.message : String(reason); + if (message.includes("absolute path") || message.includes("not a projectless")) { + return badRequest(message); + } + return internalError(message); + } +} +function envelope(rpcId, result) { + return Response.json({ type: "server-response", rpcId, result }); +} +function createProjectlessFetch(root, endpoint) { + const method = projectlessEndpoint(endpoint); + return async (request) => { + if (request.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() !== "application/json") { + return new Response("content type must be application/json", { status: 415 }); + } + let body; + try { + body = await request.json(); + } catch { + return new Response("body is not JSON", { status: 400 }); + } + const message = body; + if (typeof message !== "object" || message === null || message.type !== "client-request" || typeof message.rpcId !== "string") { + return new Response("invalid client-request envelope", { status: 400 }); + } + if (message.method !== method) { + return envelope(message.rpcId, badRequest(`method ${JSON.stringify(message.method)} does not match endpoint ${JSON.stringify(method)}`)); + } + return envelope(message.rpcId, await dispatchProjectlessEndpoint(root, endpoint, message.payload)); + }; +} function apply(ctx, config = {}) { const root = config.root ?? join2(homedir(), "Documents", "DSH"); - ctx.effect(() => ctx.connection.rpc.handle( - "/projectless-session", - async (endpoint, payload) => { - try { - if (endpoint === "create-directory") { - return { ok: true, value: { path: await createProjectlessDirectory(root) } }; - } - if (endpoint === "get-root") { - return { ok: true, value: { root: await resolveProjectlessRoot(root) } }; - } - if (endpoint === "remove-directory") { - const path = pathPayload(payload); - if (path === void 0) return badRequest("remove-directory requires { path }"); - return { ok: true, value: { result: await removeUnusedProjectlessDirectory(root, path) } }; - } - return badRequest(`unknown projectless-session endpoint ${JSON.stringify(endpoint)}`); - } catch (reason) { - const message = reason instanceof Error ? reason.message : String(reason); - if (message.includes("absolute path") || message.includes("not a projectless")) { - return badRequest(message); - } - return internalError(message); - } - } - ), "dsh-projectless-session: rpc"); + for (const endpoint of PROJECTLESS_ENDPOINTS) { + const path = `${PROJECTLESS_RPC_CHANNEL}/${projectlessEndpoint(endpoint)}`; + ctx.effect(() => ctx.connection.fetch.register({ + path, + methods: ["POST"], + requestBody: "buffered", + fetch: createProjectlessFetch(root, endpoint) + }), `dsh-projectless-session: ${path}`); + } } export { apply, + createProjectlessFetch, + dispatchProjectlessEndpoint, inject, name }; diff --git a/lib/index.js.map b/lib/index.js.map index 9d38d07..8cf1285 100644 --- a/lib/index.js.map +++ b/lib/index.js.map @@ -1,7 +1,7 @@ { "version": 3, - "sources": ["../src/index.ts", "../src/host/directories.ts", "../src/shared/paths.ts"], - "sourcesContent": ["import { homedir } from 'node:os'\nimport { join } from 'node:path'\nimport type { Context } from '@deepseek-ai/cordis'\nimport type {} from '@deepseek-ai/dsh-client-connection'\nimport {\n createProjectlessDirectory,\n removeUnusedProjectlessDirectory,\n resolveProjectlessRoot,\n} from './host/directories.ts'\n\n/** Host half: provides an authenticated DSH RPC for filesystem provisioning. */\nexport const name = 'dsh-projectless-session'\nexport const inject = ['connection']\n\nexport interface Config {\n /** Absolute parent for date folders; defaults to ~/Documents/DSH. */\n root?: string\n}\n\nfunction pathPayload(payload: unknown): string | undefined {\n if (typeof payload !== 'object' || payload === null) return undefined\n const path = (payload as { path?: unknown }).path\n return typeof path === 'string' ? path : undefined\n}\n\nfunction badRequest(message: string) {\n return {\n ok: false as const,\n error: {\n code: 'bad-request' as const,\n message,\n details: { issues: [] },\n },\n }\n}\n\nfunction internalError(message: string) {\n return {\n ok: false as const,\n error: {\n code: 'internal' as const,\n message,\n details: {},\n },\n }\n}\n\n/** Register the least-privilege channel used by the browser half. */\nexport function apply(ctx: Context, config: Config = {}): void {\n const root = config.root ?? join(homedir(), 'Documents', 'DSH')\n ctx.effect(() => ctx.connection.rpc.handle(\n '/projectless-session',\n async (endpoint, payload) => {\n try {\n if (endpoint === 'create-directory') {\n return { ok: true, value: { path: await createProjectlessDirectory(root) } }\n }\n if (endpoint === 'get-root') {\n return { ok: true, value: { root: await resolveProjectlessRoot(root) } }\n }\n if (endpoint === 'remove-directory') {\n const path = pathPayload(payload)\n if (path === undefined) return badRequest('remove-directory requires { path }')\n return { ok: true, value: { result: await removeUnusedProjectlessDirectory(root, path) } }\n }\n return badRequest(`unknown projectless-session endpoint ${JSON.stringify(endpoint)}`)\n } catch (reason) {\n const message = reason instanceof Error ? reason.message : String(reason)\n if (message.includes('absolute path') || message.includes('not a projectless')) {\n return badRequest(message)\n }\n return internalError(message)\n }\n },\n ), 'dsh-projectless-session: rpc')\n}\n", "import { randomBytes } from 'node:crypto'\nimport { lstat, mkdir, readdir, realpath, rmdir, unlink } from 'node:fs/promises'\nimport { dirname, isAbsolute, join } from 'node:path'\nimport {\n isIgnorableDirectoryEntry,\n isManagedDateDirectoryPath,\n isManagedSessionPath,\n} from '../shared/paths.ts'\n\n/** Use the Host's local calendar fields because it owns the target filesystem. */\nexport function localDateName(now: Date): string {\n const year = String(now.getFullYear()).padStart(4, '0')\n const month = String(now.getMonth() + 1).padStart(2, '0')\n const day = String(now.getDate()).padStart(2, '0')\n return `${year}-${month}-${day}`\n}\n\n/** Human-sortable prefix plus entropy, avoiding predictable or colliding session directories. */\nexport function sessionDirectoryName(now: Date, suffix: string): string {\n const hour = String(now.getHours()).padStart(2, '0')\n const minute = String(now.getMinutes()).padStart(2, '0')\n const second = String(now.getSeconds()).padStart(2, '0')\n return `session-${hour}-${minute}-${second}-${suffix}`\n}\n\nfunction randomSuffix(): string {\n return randomBytes(4).toString('hex')\n}\n\n/** Create ROOT/YYYY-MM-DD/session-HH-mm-ss-random and return its absolute path. */\nexport async function createProjectlessDirectory(\n root: string,\n now: Date = new Date(),\n suffix: string = randomSuffix(),\n): Promise {\n if (!isAbsolute(root)) throw new Error('projectless session root must be an absolute path')\n const dateDirectory = join(root, localDateName(now))\n await mkdir(dateDirectory, { recursive: true, mode: 0o700 })\n const sessionDirectory = join(dateDirectory, sessionDirectoryName(now, suffix))\n await mkdir(sessionDirectory, { mode: 0o700 })\n return sessionDirectory\n}\n\nfunction isErrno(reason: unknown, code: string): boolean {\n return typeof reason === 'object' && reason !== null && 'code' in reason\n && (reason as { code: unknown }).code === code\n}\n\n/** Prefer the Host-resolved root so Workspace realpaths still match. */\nexport async function resolveProjectlessRoot(root: string): Promise {\n if (!isAbsolute(root)) throw new Error('projectless session root must be an absolute path')\n try {\n return await realpath(root)\n } catch (reason) {\n if (isErrno(reason, 'ENOENT')) return root\n throw reason\n }\n}\n\nfunction isOwnedSessionPath(target: string, root: string, resolvedRoot: string): boolean {\n return isManagedSessionPath(target, root) || isManagedSessionPath(target, resolvedRoot)\n}\n\nfunction isOwnedDatePath(target: string, root: string, resolvedRoot: string): boolean {\n return isManagedDateDirectoryPath(target, root) || isManagedDateDirectoryPath(target, resolvedRoot)\n}\n\nexport type UnusedDirectoryRemoval = 'removed' | 'absent' | 'retained'\n\n/** True when the directory exists and has no user files. `.DS_Store` and similar OS junk are ignored. */\nexport async function directoryHasNoUserEntries(path: string): Promise {\n try {\n const entries = await readdir(path)\n return entries.every(isIgnorableDirectoryEntry)\n } catch (reason) {\n if (isErrno(reason, 'ENOENT')) return false\n throw reason\n }\n}\n\nasync function unlinkIgnorableFiles(directory: string, names: string[]): Promise {\n for (const name of names) {\n const target = join(directory, name)\n try {\n const info = await lstat(target)\n if (!info.isFile()) return false\n await unlink(target)\n } catch (reason) {\n if (isErrno(reason, 'ENOENT')) continue\n return false\n }\n }\n return true\n}\n\nasync function removeEmptyDirectory(path: string): Promise {\n let entries: string[]\n try {\n entries = await readdir(path)\n } catch (reason) {\n if (isErrno(reason, 'ENOENT')) return 'absent'\n throw reason\n }\n const junk = entries.filter(isIgnorableDirectoryEntry)\n if (junk.length !== entries.length) return 'retained'\n if (junk.length > 0 && !await unlinkIgnorableFiles(path, junk)) return 'retained'\n try {\n await rmdir(path)\n return 'removed'\n } catch (reason) {\n if (isErrno(reason, 'ENOENT')) return 'absent'\n if (isErrno(reason, 'ENOTEMPTY') || isErrno(reason, 'EEXIST')) return 'retained'\n throw reason\n }\n}\n\n/**\n * Remove an unused projectless session directory only after confirming it has\n * no user files. OS metadata such as `.DS_Store` is stripped first; any other\n * content keeps the directory.\n */\nexport async function removeUnusedProjectlessDirectory(\n root: string,\n requestedPath: string,\n): Promise {\n if (!isAbsolute(root) || !isAbsolute(requestedPath)) {\n throw new Error('projectless session path must be an absolute path')\n }\n const resolvedRoot = await resolveProjectlessRoot(root)\n if (!isOwnedSessionPath(requestedPath, root, resolvedRoot)) {\n throw new Error('path is not a projectless session directory')\n }\n\n let canonical: string\n try {\n canonical = await realpath(requestedPath)\n } catch (reason) {\n if (isErrno(reason, 'ENOENT')) return 'absent'\n throw reason\n }\n if (!isOwnedSessionPath(canonical, root, resolvedRoot)) {\n throw new Error('path is not a projectless session directory')\n }\n\n const result = await removeEmptyDirectory(canonical)\n if (result !== 'removed') return result\n\n const parent = dirname(canonical)\n if (isOwnedDatePath(parent, root, resolvedRoot)) {\n await removeEmptyDirectory(parent)\n }\n return 'removed'\n}\n", "export const DATE_DIRECTORY = /^\\d{4}-\\d{2}-\\d{2}$/\nexport const SESSION_DIRECTORY = /^session-\\d{2}-\\d{2}-\\d{2}-[0-9a-f]{8}$/\n\nconst IGNORABLE_DIRECTORY_ENTRIES = new Set([\n '.DS_Store',\n '.localized',\n 'Thumbs.db',\n 'desktop.ini',\n])\n\n/** Finder/Explorer metadata that must not keep an otherwise empty scratch directory. */\nexport function isIgnorableDirectoryEntry(name: string): boolean {\n return IGNORABLE_DIRECTORY_ENTRIES.has(name) || name.startsWith('._')\n}\n\n/** Normalize separators so Host and browser path checks agree. */\nexport function normalizeFsPath(path: string): string {\n return path.replace(/\\\\/g, '/').replace(/\\/+$/, '')\n}\n\nfunction relativeParts(target: string, root: string): string[] | undefined {\n const path = normalizeFsPath(target)\n const base = normalizeFsPath(root)\n if (base === '' || path === base || !path.startsWith(`${base}/`)) return undefined\n return path.slice(base.length + 1).split('/')\n}\n\n/** True for ROOT/YYYY-MM-DD. */\nexport function isManagedDateDirectoryPath(target: string, root: string): boolean {\n const parts = relativeParts(target, root)\n return parts !== undefined && parts.length === 1 && DATE_DIRECTORY.test(parts[0] ?? '')\n}\n\n/** True for ROOT/YYYY-MM-DD/session-HH-mm-ss-hex. */\nexport function isManagedSessionPath(target: string, root: string): boolean {\n const parts = relativeParts(target, root)\n return parts !== undefined\n && parts.length === 2\n && DATE_DIRECTORY.test(parts[0] ?? '')\n && SESSION_DIRECTORY.test(parts[1] ?? '')\n}\n\n/**\n * Recognize a plugin-provisioned scratch directory by the two-level\n * `YYYY-MM-DD/session-HH-mm-ss-random` shape. Used as a cross-reload\n * fallback when the in-memory registry was never populated.\n */\nexport function isProjectlessPath(path: string): boolean {\n const segments = normalizeFsPath(path).split('/').filter(segment => segment.length > 0)\n const dateName = segments.at(-2)\n const sessionName = segments.at(-1)\n return dateName !== undefined\n && sessionName !== undefined\n && DATE_DIRECTORY.test(dateName)\n && SESSION_DIRECTORY.test(sessionName)\n}\n"], - "mappings": "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", + "sources": ["../src/index.ts", "../src/host/directories.ts", "../src/shared/paths.ts", "../src/shared/rpc.ts"], + "sourcesContent": ["import { homedir } from 'node:os'\nimport { join } from 'node:path'\nimport type { Context } from '@deepseek-ai/cordis'\nimport type {} from '@deepseek-ai/dsh-client-connection'\nimport {\n createProjectlessDirectory,\n removeUnusedProjectlessDirectory,\n resolveProjectlessRoot,\n} from './host/directories.ts'\nimport {\n PROJECTLESS_ENDPOINTS,\n PROJECTLESS_RPC_CHANNEL,\n projectlessEndpoint,\n type ProjectlessEndpoint,\n} from './shared/rpc.ts'\n\n/** Host half: provides authenticated DSH endpoints for filesystem provisioning. */\nexport const name = 'dsh-projectless-session'\nexport const inject = ['connection']\n\nexport interface Config {\n /** Absolute parent for date folders; defaults to ~/Documents/DSH. */\n root?: string\n}\n\ntype RpcResult =\n | { ok: true, value: unknown }\n | { ok: false, error: { code: string, message: string, details: Record } }\n\nfunction pathPayload(payload: unknown): string | undefined {\n if (typeof payload !== 'object' || payload === null) return undefined\n const path = (payload as { path?: unknown }).path\n return typeof path === 'string' ? path : undefined\n}\n\nfunction badRequest(message: string): RpcResult {\n return {\n ok: false,\n error: {\n code: 'bad-request',\n message,\n details: { issues: [] },\n },\n }\n}\n\nfunction internalError(message: string): RpcResult {\n return {\n ok: false,\n error: {\n code: 'internal',\n message,\n details: {},\n },\n }\n}\n\n/** Run one decoded endpoint against the configured root. */\nexport async function dispatchProjectlessEndpoint(\n root: string,\n endpoint: ProjectlessEndpoint,\n payload: unknown,\n): Promise {\n try {\n if (endpoint === 'create-directory') {\n return { ok: true, value: { path: await createProjectlessDirectory(root) } }\n }\n if (endpoint === 'get-root') {\n return { ok: true, value: { root: await resolveProjectlessRoot(root) } }\n }\n const path = pathPayload(payload)\n if (path === undefined) return badRequest('remove-directory requires { path }')\n return { ok: true, value: { result: await removeUnusedProjectlessDirectory(root, path) } }\n } catch (reason) {\n const message = reason instanceof Error ? reason.message : String(reason)\n if (message.includes('absolute path') || message.includes('not a projectless')) {\n return badRequest(message)\n }\n return internalError(message)\n }\n}\n\nfunction envelope(rpcId: string, result: RpcResult): Response {\n return Response.json({ type: 'server-response', rpcId, result })\n}\n\n/**\n * Decode Connection's `client-request` envelope for one exact endpoint and\n * answer with its `server-response` envelope, so the browser half keeps using\n * `ctx.connection.rpc.call`.\n */\nexport function createProjectlessFetch(root: string, endpoint: ProjectlessEndpoint) {\n const method = projectlessEndpoint(endpoint)\n return async (request: Request): Promise => {\n if (request.headers.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase() !== 'application/json') {\n return new Response('content type must be application/json', { status: 415 })\n }\n let body: unknown\n try {\n body = await request.json()\n } catch {\n return new Response('body is not JSON', { status: 400 })\n }\n const message = body as { type?: unknown, rpcId?: unknown, method?: unknown, payload?: unknown } | null\n if (typeof message !== 'object' || message === null || message.type !== 'client-request' || typeof message.rpcId !== 'string') {\n return new Response('invalid client-request envelope', { status: 400 })\n }\n if (message.method !== method) {\n return envelope(message.rpcId, badRequest(`method ${JSON.stringify(message.method)} does not match endpoint ${JSON.stringify(method)}`))\n }\n return envelope(message.rpcId, await dispatchProjectlessEndpoint(root, endpoint, message.payload))\n }\n}\n\n/** Register the least-privilege endpoints used by the browser half. */\nexport function apply(ctx: Context, config: Config = {}): void {\n const root = config.root ?? join(homedir(), 'Documents', 'DSH')\n for (const endpoint of PROJECTLESS_ENDPOINTS) {\n const path = `${PROJECTLESS_RPC_CHANNEL}/${projectlessEndpoint(endpoint)}`\n ctx.effect(() => ctx.connection.fetch.register({\n path,\n methods: ['POST'],\n requestBody: 'buffered',\n fetch: createProjectlessFetch(root, endpoint),\n }), `dsh-projectless-session: ${path}`)\n }\n}\n", "import { randomBytes } from 'node:crypto'\nimport { lstat, mkdir, readdir, realpath, rmdir, unlink } from 'node:fs/promises'\nimport { dirname, isAbsolute, join } from 'node:path'\nimport {\n isIgnorableDirectoryEntry,\n isManagedDateDirectoryPath,\n isManagedSessionPath,\n} from '../shared/paths.ts'\n\n/** Use the Host's local calendar fields because it owns the target filesystem. */\nexport function localDateName(now: Date): string {\n const year = String(now.getFullYear()).padStart(4, '0')\n const month = String(now.getMonth() + 1).padStart(2, '0')\n const day = String(now.getDate()).padStart(2, '0')\n return `${year}-${month}-${day}`\n}\n\n/** Human-sortable prefix plus entropy, avoiding predictable or colliding session directories. */\nexport function sessionDirectoryName(now: Date, suffix: string): string {\n const hour = String(now.getHours()).padStart(2, '0')\n const minute = String(now.getMinutes()).padStart(2, '0')\n const second = String(now.getSeconds()).padStart(2, '0')\n return `session-${hour}-${minute}-${second}-${suffix}`\n}\n\nfunction randomSuffix(): string {\n return randomBytes(4).toString('hex')\n}\n\n/** Create ROOT/YYYY-MM-DD/session-HH-mm-ss-random and return its absolute path. */\nexport async function createProjectlessDirectory(\n root: string,\n now: Date = new Date(),\n suffix: string = randomSuffix(),\n): Promise {\n if (!isAbsolute(root)) throw new Error('projectless session root must be an absolute path')\n const dateDirectory = join(root, localDateName(now))\n await mkdir(dateDirectory, { recursive: true, mode: 0o700 })\n const sessionDirectory = join(dateDirectory, sessionDirectoryName(now, suffix))\n await mkdir(sessionDirectory, { mode: 0o700 })\n return sessionDirectory\n}\n\nfunction isErrno(reason: unknown, code: string): boolean {\n return typeof reason === 'object' && reason !== null && 'code' in reason\n && (reason as { code: unknown }).code === code\n}\n\n/** Prefer the Host-resolved root so Workspace realpaths still match. */\nexport async function resolveProjectlessRoot(root: string): Promise {\n if (!isAbsolute(root)) throw new Error('projectless session root must be an absolute path')\n try {\n return await realpath(root)\n } catch (reason) {\n if (isErrno(reason, 'ENOENT')) return root\n throw reason\n }\n}\n\nfunction isOwnedSessionPath(target: string, root: string, resolvedRoot: string): boolean {\n return isManagedSessionPath(target, root) || isManagedSessionPath(target, resolvedRoot)\n}\n\nfunction isOwnedDatePath(target: string, root: string, resolvedRoot: string): boolean {\n return isManagedDateDirectoryPath(target, root) || isManagedDateDirectoryPath(target, resolvedRoot)\n}\n\nexport type UnusedDirectoryRemoval = 'removed' | 'absent' | 'retained'\n\n/** True when the directory exists and has no user files. `.DS_Store` and similar OS junk are ignored. */\nexport async function directoryHasNoUserEntries(path: string): Promise {\n try {\n const entries = await readdir(path)\n return entries.every(isIgnorableDirectoryEntry)\n } catch (reason) {\n if (isErrno(reason, 'ENOENT')) return false\n throw reason\n }\n}\n\nasync function unlinkIgnorableFiles(directory: string, names: string[]): Promise {\n for (const name of names) {\n const target = join(directory, name)\n try {\n const info = await lstat(target)\n if (!info.isFile()) return false\n await unlink(target)\n } catch (reason) {\n if (isErrno(reason, 'ENOENT')) continue\n return false\n }\n }\n return true\n}\n\nasync function removeEmptyDirectory(path: string): Promise {\n let entries: string[]\n try {\n entries = await readdir(path)\n } catch (reason) {\n if (isErrno(reason, 'ENOENT')) return 'absent'\n throw reason\n }\n const junk = entries.filter(isIgnorableDirectoryEntry)\n if (junk.length !== entries.length) return 'retained'\n if (junk.length > 0 && !await unlinkIgnorableFiles(path, junk)) return 'retained'\n try {\n await rmdir(path)\n return 'removed'\n } catch (reason) {\n if (isErrno(reason, 'ENOENT')) return 'absent'\n if (isErrno(reason, 'ENOTEMPTY') || isErrno(reason, 'EEXIST')) return 'retained'\n throw reason\n }\n}\n\n/**\n * Remove an unused projectless session directory only after confirming it has\n * no user files. OS metadata such as `.DS_Store` is stripped first; any other\n * content keeps the directory.\n */\nexport async function removeUnusedProjectlessDirectory(\n root: string,\n requestedPath: string,\n): Promise {\n if (!isAbsolute(root) || !isAbsolute(requestedPath)) {\n throw new Error('projectless session path must be an absolute path')\n }\n const resolvedRoot = await resolveProjectlessRoot(root)\n if (!isOwnedSessionPath(requestedPath, root, resolvedRoot)) {\n throw new Error('path is not a projectless session directory')\n }\n\n let canonical: string\n try {\n canonical = await realpath(requestedPath)\n } catch (reason) {\n if (isErrno(reason, 'ENOENT')) return 'absent'\n throw reason\n }\n if (!isOwnedSessionPath(canonical, root, resolvedRoot)) {\n throw new Error('path is not a projectless session directory')\n }\n\n const result = await removeEmptyDirectory(canonical)\n if (result !== 'removed') return result\n\n const parent = dirname(canonical)\n if (isOwnedDatePath(parent, root, resolvedRoot)) {\n await removeEmptyDirectory(parent)\n }\n return 'removed'\n}\n", "export const DATE_DIRECTORY = /^\\d{4}-\\d{2}-\\d{2}$/\nexport const SESSION_DIRECTORY = /^session-\\d{2}-\\d{2}-\\d{2}-[0-9a-f]{8}$/\n\nconst IGNORABLE_DIRECTORY_ENTRIES = new Set([\n '.DS_Store',\n '.localized',\n 'Thumbs.db',\n 'desktop.ini',\n])\n\n/** Finder/Explorer metadata that must not keep an otherwise empty scratch directory. */\nexport function isIgnorableDirectoryEntry(name: string): boolean {\n return IGNORABLE_DIRECTORY_ENTRIES.has(name) || name.startsWith('._')\n}\n\n/** Normalize separators so Host and browser path checks agree. */\nexport function normalizeFsPath(path: string): string {\n return path.replace(/\\\\/g, '/').replace(/\\/+$/, '')\n}\n\nfunction relativeParts(target: string, root: string): string[] | undefined {\n const path = normalizeFsPath(target)\n const base = normalizeFsPath(root)\n if (base === '' || path === base || !path.startsWith(`${base}/`)) return undefined\n return path.slice(base.length + 1).split('/')\n}\n\n/** True for ROOT/YYYY-MM-DD. */\nexport function isManagedDateDirectoryPath(target: string, root: string): boolean {\n const parts = relativeParts(target, root)\n return parts !== undefined && parts.length === 1 && DATE_DIRECTORY.test(parts[0] ?? '')\n}\n\n/** True for ROOT/YYYY-MM-DD/session-HH-mm-ss-hex. */\nexport function isManagedSessionPath(target: string, root: string): boolean {\n const parts = relativeParts(target, root)\n return parts !== undefined\n && parts.length === 2\n && DATE_DIRECTORY.test(parts[0] ?? '')\n && SESSION_DIRECTORY.test(parts[1] ?? '')\n}\n\n/**\n * Recognize a plugin-provisioned scratch directory by the two-level\n * `YYYY-MM-DD/session-HH-mm-ss-random` shape. Used as a cross-reload\n * fallback when the in-memory registry was never populated.\n */\nexport function isProjectlessPath(path: string): boolean {\n const segments = normalizeFsPath(path).split('/').filter(segment => segment.length > 0)\n const dateName = segments.at(-2)\n const sessionName = segments.at(-1)\n return dateName !== undefined\n && sessionName !== undefined\n && DATE_DIRECTORY.test(dateName)\n && SESSION_DIRECTORY.test(sessionName)\n}\n", "/**\n * Wire address of the Host half. DSH 0.2's `connection.rpc.handle` resolves\n * `webServer` from the Connection plugin's own fiber, so third-party channels\n * cannot mount; the endpoints ride the already-authenticated shared `/api`\n * channel as exact Fetch routes instead.\n */\nexport const PROJECTLESS_RPC_CHANNEL = '/api'\nexport const PROJECTLESS_RPC_PREFIX = 'projectless-session'\nexport const PROJECTLESS_ENDPOINTS = ['create-directory', 'get-root', 'remove-directory'] as const\nexport type ProjectlessEndpoint = typeof PROJECTLESS_ENDPOINTS[number]\n\n/** Channel-relative endpoint passed to `ClientConnectionRpc.call`. */\nexport function projectlessEndpoint(endpoint: ProjectlessEndpoint): string {\n return `${PROJECTLESS_RPC_PREFIX}/${endpoint}`\n}\n"], + "mappings": "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", "names": ["join", "name", "name", "join"] } diff --git a/package-lock.json b/package-lock.json index fc050db..924464a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,25 @@ { "name": "dsh-projectless-session", - "version": "0.5.0", + "version": "0.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dsh-projectless-session", - "version": "0.5.0", + "version": "0.6.0", "license": "MIT", "devDependencies": { - "@deepseek-ai/cordis": "^4.0.2", - "@deepseek-ai/dsh-client-connection": "0.1.5-rc.2", - "@deepseek-ai/dsh-client-locale": "0.1.5-rc.2", - "@deepseek-ai/dsh-client-runtime": "0.1.1-rc.2", - "@deepseek-ai/dsh-client-ui-conversation": "0.1.5-rc.2", - "@deepseek-ai/dsh-client-ui-primitives": "0.1.5-rc.2", - "@deepseek-ai/dsh-client-ui-slots": "0.1.5-rc.2", - "@deepseek-ai/dsh-client-ui-workspace": "0.1.5-rc.2", + "@deepseek-ai/cordis": "^4.0.4", + "@deepseek-ai/dsh-api-session-controller": "0.2.0-rc.2", + "@deepseek-ai/dsh-api-workspace-controller": "0.2.0-rc.2", + "@deepseek-ai/dsh-client-connection": "0.2.0-rc.2", + "@deepseek-ai/dsh-client-locale": "0.2.0-rc.2", + "@deepseek-ai/dsh-client-ui-conversation": "0.2.0-rc.2", + "@deepseek-ai/dsh-client-ui-primitives": "0.2.0-rc.2", + "@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.2", + "@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.2", + "@deepseek-ai/dsh-client-ui-workspace": "0.2.0-rc.2", + "@deepseek-ai/dsh-session": "0.2.0-rc.2", "@types/node": "^22.0.0", "@types/react": "^18.3.0", "esbuild": "^0.25.0", @@ -28,27 +31,33 @@ "node": ">=22.19" }, "peerDependencies": { - "@deepseek-ai/cordis": "^4.0.2", - "@deepseek-ai/dsh-client-connection": "^0.1.5-rc.2", - "@deepseek-ai/dsh-client-locale": "^0.1.5-rc.2", - "@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.2", - "@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-rc.2", - "@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.2", - "@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.2", - "@deepseek-ai/dsh-client-ui-workspace": "^0.1.5-rc.2", + "@deepseek-ai/cordis": "^4.0.4", + "@deepseek-ai/dsh-api-session-controller": "^0.2.0-rc.2", + "@deepseek-ai/dsh-api-workspace-controller": "^0.2.0-rc.2", + "@deepseek-ai/dsh-client-connection": "^0.2.0-rc.2", + "@deepseek-ai/dsh-client-locale": "^0.2.0-rc.2", + "@deepseek-ai/dsh-client-ui-conversation": "^0.2.0-rc.2", + "@deepseek-ai/dsh-client-ui-primitives": "^0.2.0-rc.2", + "@deepseek-ai/dsh-client-ui-renderer": "^0.2.0-rc.2", + "@deepseek-ai/dsh-client-ui-slots": "^0.2.0-rc.2", + "@deepseek-ai/dsh-client-ui-workspace": "^0.2.0-rc.2", + "@deepseek-ai/dsh-session": "^0.2.0-rc.2", "react": "^18.2.0" }, "peerDependenciesMeta": { "@deepseek-ai/cordis": { "optional": true }, - "@deepseek-ai/dsh-client-connection": { + "@deepseek-ai/dsh-api-session-controller": { "optional": true }, - "@deepseek-ai/dsh-client-locale": { + "@deepseek-ai/dsh-api-workspace-controller": { "optional": true }, - "@deepseek-ai/dsh-client-runtime": { + "@deepseek-ai/dsh-client-connection": { + "optional": true + }, + "@deepseek-ai/dsh-client-locale": { "optional": true }, "@deepseek-ai/dsh-client-ui-conversation": { @@ -57,33 +66,39 @@ "@deepseek-ai/dsh-client-ui-primitives": { "optional": true }, + "@deepseek-ai/dsh-client-ui-renderer": { + "optional": true + }, "@deepseek-ai/dsh-client-ui-slots": { "optional": true }, "@deepseek-ai/dsh-client-ui-workspace": { "optional": true }, + "@deepseek-ai/dsh-session": { + "optional": true + }, "react": { "optional": true } } }, "node_modules/@deepseek-ai/cordis": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@deepseek-ai/cordis/-/cordis-4.0.2.tgz", - "integrity": "sha512-asOnXP1TzFSFQlHb1iegDZp0z/8WD1c7YNrwJR/Tx2bzNuMXfcekE/I67Iv6SQXeLB4csxqCngzQKANP7gdw0g==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@deepseek-ai/cordis/-/cordis-4.0.4.tgz", + "integrity": "sha512-obgyxqWAmFn3Re8kvsuUnyW+ihrz6eJCnJO4fh1cQzDtmPYz/zzVeUkH9R94I0OwSVOocK67Kgakm04j/oQXzg==", "dev": true, "license": "MIT", "dependencies": { - "@deepseek-ai/cosmokit": "^1.8.3", + "@deepseek-ai/cosmokit": "~1.8.5", "@standard-schema/spec": "^1.1.0" }, "bin": { "cordis": "bin.js" }, "peerDependencies": { - "@deepseek-ai/cordis-plugin-include": "^1.0.7", - "@deepseek-ai/cordis-plugin-loader": "^1.0.3" + "@deepseek-ai/cordis-plugin-include": "~1.0.9", + "@deepseek-ai/cordis-plugin-loader": "~1.0.5" }, "peerDependenciesMeta": { "@deepseek-ai/cordis-plugin-include": { @@ -95,144 +110,304 @@ } }, "node_modules/@deepseek-ai/cosmokit": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@deepseek-ai/cosmokit/-/cosmokit-1.8.3.tgz", - "integrity": "sha512-qBo+ronVM6Eu2WNVJXi8JcMiqZ19T9BRIpV+5qJUFPXjGH/Z0QKcQMC/IZJ7L394YTOtJgcovbk9qP0w2GsBXQ==", + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@deepseek-ai/cosmokit/-/cosmokit-1.8.5.tgz", + "integrity": "sha512-LXsrlem9z8dq4sLflj2yuCuYX7KqhdzF5hZly3eZyuTo8d6oDSOXuEgC5DbQWKW+lksm6zmOutQLsP/ma6wR6A==", "dev": true, "license": "MIT" }, - "node_modules/@deepseek-ai/dsh-brand": { - "version": "0.1.5-rc.2", - "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-brand/-/dsh-brand-0.1.5-rc.2.tgz", - "integrity": "sha512-/+3TzQRYT4M8NINZ9OrsL5VWNyQSXof8mFLx3txiosWA7Bt0H6UcxUdKGXVwj6gCXhsQ74mzEHm2ucsflG0mYA==", + "node_modules/@deepseek-ai/dsh-api-session-controller": { + "version": "0.2.0-rc.2", + "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-api-session-controller/-/dsh-api-session-controller-0.2.0-rc.2.tgz", + "integrity": "sha512-WDmk1aWOHbzIKRRt7rtHDxSupw/XLB+BgMyKHlzB7QGinXrK/Dnht6JIdgE1iSShnf1RNANGAGh2G94VzDpl5Q==", "dev": true, "license": "MIT", + "dependencies": { + "@deepseek-ai/dsh-brand": "0.2.0-rc.2", + "@deepseek-ai/dsh-deque": "0.2.0-rc.2", + "@deepseek-ai/schemastery": "~3.18.4", + "mime-types": "^3.0.2", + "zod": "^4.4.3" + }, "peerDependencies": { - "@deepseek-ai/cordis": "^4.0.2" + "@deepseek-ai/cordis": "~4.0.4", + "@deepseek-ai/dsh-agent": "0.2.0-rc.2", + "@deepseek-ai/dsh-agent-default-model": "0.2.0-rc.2", + "@deepseek-ai/dsh-agent-preset-registry": "0.2.0-rc.2", + "@deepseek-ai/dsh-api-gateway": "0.2.0-rc.2", + "@deepseek-ai/dsh-attachment": "0.2.0-rc.2", + "@deepseek-ai/dsh-client-connection": "0.2.0-rc.2", + "@deepseek-ai/dsh-client-file-upload": "0.2.0-rc.2", + "@deepseek-ai/dsh-commands": "0.2.0-rc.2", + "@deepseek-ai/dsh-credentials": "0.2.0-rc.2", + "@deepseek-ai/dsh-file-reference": "0.2.0-rc.2", + "@deepseek-ai/dsh-fs": "0.2.0-rc.2", + "@deepseek-ai/dsh-llm": "0.2.0-rc.2", + "@deepseek-ai/dsh-native-command": "0.2.0-rc.2", + "@deepseek-ai/dsh-scope": "0.2.0-rc.2", + "@deepseek-ai/dsh-session": "0.2.0-rc.2", + "@deepseek-ai/dsh-session-persistence": "0.2.0-rc.2", + "@deepseek-ai/dsh-session-projection": "0.2.0-rc.2", + "@deepseek-ai/dsh-session-projection-cache": "0.2.0-rc.2", + "@deepseek-ai/dsh-session-query": "0.2.0-rc.2", + "@deepseek-ai/dsh-session-title": "0.2.0-rc.2", + "@deepseek-ai/dsh-settings": "0.2.0-rc.2", + "@deepseek-ai/dsh-skill": "0.2.0-rc.2", + "@deepseek-ai/dsh-subagent": "0.2.0-rc.2", + "@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.2", + "@deepseek-ai/dsh-typert-registry": "0.2.0-rc.2", + "@deepseek-ai/dsh-util-time": "0.2.0-rc.2", + "@deepseek-ai/dsh-util-values": "0.2.0-rc.2", + "@deepseek-ai/dsh-util-workspace-path": "0.2.0-rc.2", + "@deepseek-ai/dsh-workspace": "0.2.0-rc.2" + }, + "peerDependenciesMeta": { + "@deepseek-ai/dsh-session-persistence": { + "optional": true + }, + "@deepseek-ai/dsh-session-projection-cache": { + "optional": true + }, + "@deepseek-ai/dsh-settings": { + "optional": true + } } }, - "node_modules/@deepseek-ai/dsh-client-connection": { - "version": "0.1.5-rc.2", - "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-client-connection/-/dsh-client-connection-0.1.5-rc.2.tgz", - "integrity": "sha512-W0GAZX01hAfrfjoZbtwEJ5ik3dG20Hy/0TFYJ6dnvwxtcacGYNIwi3f2oHpZgLcEw+PWwq1Y9IV1iKG+yDOytg==", + "node_modules/@deepseek-ai/dsh-api-workspace-controller": { + "version": "0.2.0-rc.2", + "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-api-workspace-controller/-/dsh-api-workspace-controller-0.2.0-rc.2.tgz", + "integrity": "sha512-3CoKQUgGxJ8yH++j/HurB4ZTrVLMZXS0cMm8Fb3ZaMB1dTseroaMoepvnBtzJF1hQx6YArNu8XvXO5qPRA6Fgg==", "dev": true, "license": "MIT", "dependencies": { - "@deepseek-ai/dsh-credentials": "^0.1.5-rc.2", - "@deepseek-ai/schemastery": "^3.18.2", + "@deepseek-ai/dsh-deque": "0.2.0-rc.2", + "@deepseek-ai/dsh-native-command": "0.2.0-rc.2", + "@deepseek-ai/schemastery": "~3.18.4", "zod": "^4.4.3" }, "peerDependencies": { - "@deepseek-ai/cordis": "^4.0.2" + "@deepseek-ai/cordis": "~4.0.4", + "@deepseek-ai/dsh-api-gateway": "0.2.0-rc.2", + "@deepseek-ai/dsh-client-connection": "0.2.0-rc.2", + "@deepseek-ai/dsh-host-directory-picker": "0.2.0-rc.2", + "@deepseek-ai/dsh-session": "0.2.0-rc.2", + "@deepseek-ai/dsh-storage-domain": "0.2.0-rc.2", + "@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.2", + "@deepseek-ai/dsh-workspace": "0.2.0-rc.2" } }, - "node_modules/@deepseek-ai/dsh-client-locale": { - "version": "0.1.5-rc.2", - "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-client-locale/-/dsh-client-locale-0.1.5-rc.2.tgz", - "integrity": "sha512-kv56ki/WQWagsHt94wJAPzsiKnxa8KlmM33bZGgl2SgF4atlQ3iRCdkwJ3fQARXF4Ocnfutivoj5fu5usks1Sw==", + "node_modules/@deepseek-ai/dsh-brand": { + "version": "0.2.0-rc.2", + "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-brand/-/dsh-brand-0.2.0-rc.2.tgz", + "integrity": "sha512-A5XlN4tIgP0qObkYFhcZ7Oz/tM2oESEZqLwS0Qq+sM1ivGx6o4AMPgJdfKQlwLzqk0cMLNtAaVBHfiYoElt4lw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "~4.0.4" + } + }, + "node_modules/@deepseek-ai/dsh-client-connection": { + "version": "0.2.0-rc.2", + "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-client-connection/-/dsh-client-connection-0.2.0-rc.2.tgz", + "integrity": "sha512-eL66YSZ0+HlWRzAPGU8jtCLd322WXBoMGN1g1OsM5T7Skqbbo9hSwV1FO3Lm0IcMZR3Y07sGPQBwZwf/l66T3w==", "dev": true, "license": "MIT", "dependencies": { - "@deepseek-ai/schemastery": "^3.18.2" + "@deepseek-ai/dsh-credentials": "0.2.0-rc.2", + "@deepseek-ai/schemastery": "~3.18.4", + "zod": "^4.4.3" }, "peerDependencies": { - "@deepseek-ai/cordis": "^4.0.2" + "@deepseek-ai/cordis": "~4.0.4", + "@deepseek-ai/dsh-scope": "0.2.0-rc.2" } }, - "node_modules/@deepseek-ai/dsh-client-runtime": { - "version": "0.1.1-rc.2", - "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-client-runtime/-/dsh-client-runtime-0.1.1-rc.2.tgz", - "integrity": "sha512-o1FH7Rlns0Xaxh4SBOWZ1wpa0ViGw6DXWNm5NFpsBTGYD94RGdIrud3QxgrfzmQKLzu33gvS8JL/IjqbzWyYsg==", + "node_modules/@deepseek-ai/dsh-client-locale": { + "version": "0.2.0-rc.2", + "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-client-locale/-/dsh-client-locale-0.2.0-rc.2.tgz", + "integrity": "sha512-JdZPMv/8hLOQNxHk1dM6YbNbChB5S6lGaGAPtraP0670i9Tb4dX+yKOAUgkS7nSoO5TYZztXgI1kgcEregRPJA==", "dev": true, "license": "MIT", "dependencies": { - "immer": "^10.1.1", - "zustand": "~4.4.7" + "@deepseek-ai/schemastery": "~3.18.4" }, "peerDependencies": { - "@deepseek-ai/cordis": "^4.0.1", - "@deepseek-ai/dsh-agent": "^0.1.1-rc.2", - "@deepseek-ai/dsh-api-remotes": "^0.1.1-rc.2", - "@deepseek-ai/dsh-attachment": "^0.1.1-rc.2", - "@deepseek-ai/dsh-client-connection": "^0.1.1-rc.2", - "@deepseek-ai/dsh-commands": "^0.1.1-rc.2", - "@deepseek-ai/dsh-host-apiproxy": "^0.1.1-rc.2", - "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2", - "@deepseek-ai/dsh-llm": "^0.1.1-rc.2", - "@deepseek-ai/dsh-llm-retry": "^0.1.1-rc.2", - "@deepseek-ai/dsh-session": "^0.1.1-rc.2", - "@deepseek-ai/dsh-session-projection": "^0.1.1-rc.2", - "@deepseek-ai/dsh-session-title": "^0.1.1-rc.2", - "@deepseek-ai/dsh-tools": "^0.1.1-rc.2", - "@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.2", - "@deepseek-ai/dsh-typert-registry": "^0.1.1-rc.2" + "@deepseek-ai/cordis": "~4.0.4" } }, "node_modules/@deepseek-ai/dsh-client-ui-conversation": { - "version": "0.1.5-rc.2", - "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-client-ui-conversation/-/dsh-client-ui-conversation-0.1.5-rc.2.tgz", - "integrity": "sha512-VnZ0VrmI7+1JH/iMYV6+FaxCsZrVk4CZIuG+k1HkeevHgUPHe3ghLo8u1Qt9cx00MEH5i3bLOY5Nka3w4mbA6g==", + "version": "0.2.0-rc.2", + "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-client-ui-conversation/-/dsh-client-ui-conversation-0.2.0-rc.2.tgz", + "integrity": "sha512-Y1eWKVeh84cbB0fcV0MFOMUHBeptlzBCzsw6xslZS7p1a8l9PeFcjx5ZVTkKFuFMCfwEHWTeV9HcYqWLYH0bhw==", "dev": true, "license": "MIT", "dependencies": { - "@deepseek-ai/schemastery": "^3.18.2" + "@deepseek-ai/schemastery": "~3.18.4" }, "peerDependencies": { - "@deepseek-ai/cordis": "^4.0.2" + "@deepseek-ai/cordis": "~4.0.4" } }, "node_modules/@deepseek-ai/dsh-client-ui-primitives": { - "version": "0.1.5-rc.2", - "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-client-ui-primitives/-/dsh-client-ui-primitives-0.1.5-rc.2.tgz", - "integrity": "sha512-APWho69VlEmbLGqzBLiybTtjun93tEbxMMJlm+Q129Cx7ZGJak3DNfQtW3OPzBCf3Gzs2PGEz8fKbunTpea2jQ==", + "version": "0.2.0-rc.2", + "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-client-ui-primitives/-/dsh-client-ui-primitives-0.2.0-rc.2.tgz", + "integrity": "sha512-Zx/MRT8NH6rYEvPnk4FQaHrADM6Dh1PQCprieqdvKj6yFhD/CJUVjSN08suGF6FPQekfVTJtS0WxY1XbQ5Ed9Q==", "dev": true, "license": "MIT", "peerDependencies": { - "@deepseek-ai/cordis": "^4.0.2" + "@deepseek-ai/cordis": "~4.0.4" + } + }, + "node_modules/@deepseek-ai/dsh-client-ui-renderer": { + "version": "0.2.0-rc.2", + "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-client-ui-renderer/-/dsh-client-ui-renderer-0.2.0-rc.2.tgz", + "integrity": "sha512-vQ/LH4A53K1J/UeF5fQs1Dj8km5cnVos7vO0rmj+pJROhE6Vs7pAAKGz5P2e02t2/2to6+iMDu2+SSITrcYWTQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "~4.0.4" } }, "node_modules/@deepseek-ai/dsh-client-ui-slots": { - "version": "0.1.5-rc.2", - "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-client-ui-slots/-/dsh-client-ui-slots-0.1.5-rc.2.tgz", - "integrity": "sha512-uUKIHBcNzWmJFtTlPkgn7hprsE5uU8lJf8Fw7jKwUouZWIwGnLdxuunrm1LOZWr7G+b4UfjVwD4LV12rhF/WLg==", + "version": "0.2.0-rc.2", + "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-client-ui-slots/-/dsh-client-ui-slots-0.2.0-rc.2.tgz", + "integrity": "sha512-zfILCyG3ijHT7S8+WDXFz4RMxBJtG9A3ngtDBnqcLbD5wzKz8H7E+HC657USa6RgG13C2RGSs73sfKcSs4WtWQ==", "dev": true, "license": "MIT", "peerDependencies": { - "@deepseek-ai/cordis": "^4.0.2" + "@deepseek-ai/cordis": "~4.0.4" } }, "node_modules/@deepseek-ai/dsh-client-ui-workspace": { - "version": "0.1.5-rc.2", - "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-client-ui-workspace/-/dsh-client-ui-workspace-0.1.5-rc.2.tgz", - "integrity": "sha512-BRe/RDIJJblCECYLwVroy8h4+cXrf3eXSfLjJ6BdpnW2fI3CBJuEKlFaBPGtKAFTTain0rQ+dP6SscZNrXm5Gw==", + "version": "0.2.0-rc.2", + "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-client-ui-workspace/-/dsh-client-ui-workspace-0.2.0-rc.2.tgz", + "integrity": "sha512-dGKLOFJXBiGDKl+qWMQ0cYF0AT485JMzSk+AUM6/81tZPapJuirxP7Yf5ZFMfsmgQFv6m4jjYlnOc6KcIfUUZQ==", "dev": true, "license": "MIT", "peerDependencies": { - "@deepseek-ai/cordis": "^4.0.2" + "@deepseek-ai/cordis": "~4.0.4" } }, "node_modules/@deepseek-ai/dsh-credentials": { - "version": "0.1.5-rc.2", - "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-credentials/-/dsh-credentials-0.1.5-rc.2.tgz", - "integrity": "sha512-TfX5MYLlyw0BFERj3dGxVfP9QGcKZUE5eXDSaY7ZfJbfQgK51VfxV5tP899OxIDRD3Bw94K5Rzx+dbUTSWJBtA==", + "version": "0.2.0-rc.2", + "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-credentials/-/dsh-credentials-0.2.0-rc.2.tgz", + "integrity": "sha512-TQlvP0jxXYGy1ZNzh4nVsiCzb8FR9iGarD6BfIZ3LwJC8lDtjSxQHWE8//NYjpn10TIMxmaR4s6niHuJ3JkTOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@deepseek-ai/dsh-brand": "0.2.0-rc.2" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "~4.0.4", + "@deepseek-ai/dsh-invariants": "0.2.0-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-deque": { + "version": "0.2.0-rc.2", + "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-deque/-/dsh-deque-0.2.0-rc.2.tgz", + "integrity": "sha512-XwZvafZAxyDm2TRnQetEizuczZ1UG7RhO8vmmIUbAdjbRbMA15NOTlJ3d24uw+I8gcSKKkmWl/jwoKYKl/qo/A==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "~4.0.4" + } + }, + "node_modules/@deepseek-ai/dsh-llm": { + "version": "0.2.0-rc.2", + "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-llm/-/dsh-llm-0.2.0-rc.2.tgz", + "integrity": "sha512-6QFrQn/h0iNqCfPpgQidnHtLSpA99A7ZND/uYKaGSd/4BGP8KBhuRd0w63e9s4u+hfW8jc3j29WKcNLIOeUfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@deepseek-ai/dsh-brand": "0.2.0-rc.2", + "@deepseek-ai/dsh-timeout": "0.2.0-rc.2", + "@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.2", + "@deepseek-ai/dsh-util-crypto": "0.2.0-rc.2", + "@deepseek-ai/dsh-util-values": "0.2.0-rc.2", + "@deepseek-ai/schemastery": "~3.18.4", + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "~4.0.4" + } + }, + "node_modules/@deepseek-ai/dsh-native-command": { + "version": "0.2.0-rc.2", + "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-native-command/-/dsh-native-command-0.2.0-rc.2.tgz", + "integrity": "sha512-RgjkI2avXHxJFwI7tjb42WTHHd4xL6Ybbg5I12T8qbuW4ph6gzAGcl8UKv7tSFM59yD85i2rnULTiZOzKKSdqw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "~4.0.4" + } + }, + "node_modules/@deepseek-ai/dsh-session": { + "version": "0.2.0-rc.2", + "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-session/-/dsh-session-0.2.0-rc.2.tgz", + "integrity": "sha512-wj+6MeqCYbDcbEvKH3puHyEGdjyu51JwBoN6Ua2tz6griLufDgLM5HpSU40L5xgpbOfrYVhXZs4vRgOrMlFY2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@deepseek-ai/dsh-brand": "0.2.0-rc.2", + "@deepseek-ai/dsh-llm": "0.2.0-rc.2", + "@deepseek-ai/dsh-util-values": "0.2.0-rc.2" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "~4.0.4", + "@deepseek-ai/dsh-scope": "0.2.0-rc.2" + } + }, + "node_modules/@deepseek-ai/dsh-timeout": { + "version": "0.2.0-rc.2", + "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-timeout/-/dsh-timeout-0.2.0-rc.2.tgz", + "integrity": "sha512-NDob8b8DJDB/sDPz3P7xhrkxOh1O9FbN1EZRKsxP4fuTfp4hrQU7aekYS5qZENFKoR8dBJMTh/zcEBw4m31glQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "~4.0.4" + } + }, + "node_modules/@deepseek-ai/dsh-typert-protocol": { + "version": "0.2.0-rc.2", + "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-typert-protocol/-/dsh-typert-protocol-0.2.0-rc.2.tgz", + "integrity": "sha512-95RvIDcVac2BWdv1IxN6yKlRV+8eEvkofqSxV+d0r8x1kx2EIWEsbdxz6C+coYIm+8e03MiN+rwOt8J7sLfe7Q==", "dev": true, "license": "MIT", "dependencies": { - "@deepseek-ai/dsh-brand": "^0.1.5-rc.2" + "@deepseek-ai/dsh-brand": "0.2.0-rc.2" }, "peerDependencies": { - "@deepseek-ai/cordis": "^4.0.2", - "@deepseek-ai/dsh-invariants": "^0.1.5-rc.2" + "@deepseek-ai/cordis": "~4.0.4" + } + }, + "node_modules/@deepseek-ai/dsh-util-crypto": { + "version": "0.2.0-rc.2", + "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-util-crypto/-/dsh-util-crypto-0.2.0-rc.2.tgz", + "integrity": "sha512-5OvvmepY69uXTagUMlnoHqg7euI4ZMifLgfdV8xZO20OslWGGiRCkJbZRyDtcJG9GsXWz9UimfWmUu52zKXJ5w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "~4.0.4" + } + }, + "node_modules/@deepseek-ai/dsh-util-values": { + "version": "0.2.0-rc.2", + "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-util-values/-/dsh-util-values-0.2.0-rc.2.tgz", + "integrity": "sha512-qgpsbgiZABuE9UvgNaVo9b2HiWwLeU93CTl9PFXqE0jN8ap1ALxzQsLGvr/q2MJuWDYqnFBg60XnqnY8ZxPWEg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "~4.0.4" } }, "node_modules/@deepseek-ai/schemastery": { - "version": "3.18.2", - "resolved": "https://registry.npmjs.org/@deepseek-ai/schemastery/-/schemastery-3.18.2.tgz", - "integrity": "sha512-njDtZsznjYxok7KLLlHOPyuv2efdWVbSflAHgztSfbMsg+CVraEoRe2DjOCgClYv3ZCSm7WXoaUkbB/+RY7tWQ==", + "version": "3.18.4", + "resolved": "https://registry.npmjs.org/@deepseek-ai/schemastery/-/schemastery-3.18.4.tgz", + "integrity": "sha512-SSXO6tYuyrIqKVbmOnIq0s+riUywYzouFMcnBluHF9n4KMo2G8HvEzhCYj6pj2617/glOjbJuVInJ9hfONsjkg==", "dev": true, "license": "MIT", "dependencies": { - "@deepseek-ai/cosmokit": "^1.8.3", + "@deepseek-ai/cosmokit": "~1.8.5", "@standard-schema/spec": "^1.1.0" } }, @@ -777,17 +952,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/immer": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", - "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -808,6 +972,33 @@ "loose-envify": "cli.js" } }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -1345,16 +1536,6 @@ "dev": true, "license": "MIT" }, - "node_modules/use-sync-external-store": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", - "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/zod": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", @@ -1364,35 +1545,6 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } - }, - "node_modules/zustand": { - "version": "4.4.7", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.7.tgz", - "integrity": "sha512-QFJWJMdlETcI69paJwhSMJz7PPWjVP8Sjhclxmxmxv/RYI7ZOvR5BHX+ktH0we9gTWQMxcne8q1OY8xxz604gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "use-sync-external-store": "1.2.0" - }, - "engines": { - "node": ">=12.7.0" - }, - "peerDependencies": { - "@types/react": ">=16.8", - "immer": ">=9.0", - "react": ">=16.8" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - } - } } } } diff --git a/package.json b/package.json index 6e4170c..bf67537 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dsh-projectless-session", - "version": "0.5.0", + "version": "0.6.0", "description": "DeepSeek Harness Web plugin for date-organized projectless sessions under Documents/DSH", "keywords": [ "deepseek", @@ -36,10 +36,12 @@ }, "client": { "inject": [ + "@deepseek-ai/dsh-api-session-controller", + "@deepseek-ai/dsh-api-workspace-controller", "@deepseek-ai/dsh-client-connection", "@deepseek-ai/dsh-client-locale", - "@deepseek-ai/dsh-client-runtime", "@deepseek-ai/dsh-client-ui-conversation", + "@deepseek-ai/dsh-client-ui-renderer", "@deepseek-ai/dsh-client-ui-workspace" ], "platform": "web" @@ -63,36 +65,45 @@ "node": ">=22.19" }, "peerDependencies": { - "@deepseek-ai/cordis": "^4.0.2", - "@deepseek-ai/dsh-client-connection": "^0.1.5-rc.2", - "@deepseek-ai/dsh-client-locale": "^0.1.5-rc.2", - "@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.2", - "@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-rc.2", - "@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.2", - "@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.2", - "@deepseek-ai/dsh-client-ui-workspace": "^0.1.5-rc.2", + "@deepseek-ai/cordis": "^4.0.4", + "@deepseek-ai/dsh-api-session-controller": "^0.2.0-rc.2", + "@deepseek-ai/dsh-api-workspace-controller": "^0.2.0-rc.2", + "@deepseek-ai/dsh-client-connection": "^0.2.0-rc.2", + "@deepseek-ai/dsh-client-locale": "^0.2.0-rc.2", + "@deepseek-ai/dsh-client-ui-conversation": "^0.2.0-rc.2", + "@deepseek-ai/dsh-client-ui-primitives": "^0.2.0-rc.2", + "@deepseek-ai/dsh-client-ui-renderer": "^0.2.0-rc.2", + "@deepseek-ai/dsh-client-ui-slots": "^0.2.0-rc.2", + "@deepseek-ai/dsh-client-ui-workspace": "^0.2.0-rc.2", + "@deepseek-ai/dsh-session": "^0.2.0-rc.2", "react": "^18.2.0" }, "peerDependenciesMeta": { "@deepseek-ai/cordis": { "optional": true }, + "@deepseek-ai/dsh-api-session-controller": { "optional": true }, + "@deepseek-ai/dsh-api-workspace-controller": { "optional": true }, "@deepseek-ai/dsh-client-connection": { "optional": true }, "@deepseek-ai/dsh-client-locale": { "optional": true }, - "@deepseek-ai/dsh-client-runtime": { "optional": true }, "@deepseek-ai/dsh-client-ui-conversation": { "optional": true }, "@deepseek-ai/dsh-client-ui-primitives": { "optional": true }, + "@deepseek-ai/dsh-client-ui-renderer": { "optional": true }, "@deepseek-ai/dsh-client-ui-slots": { "optional": true }, "@deepseek-ai/dsh-client-ui-workspace": { "optional": true }, + "@deepseek-ai/dsh-session": { "optional": true }, "react": { "optional": true } }, "devDependencies": { - "@deepseek-ai/cordis": "^4.0.2", - "@deepseek-ai/dsh-client-connection": "0.1.5-rc.2", - "@deepseek-ai/dsh-client-locale": "0.1.5-rc.2", - "@deepseek-ai/dsh-client-runtime": "0.1.1-rc.2", - "@deepseek-ai/dsh-client-ui-conversation": "0.1.5-rc.2", - "@deepseek-ai/dsh-client-ui-primitives": "0.1.5-rc.2", - "@deepseek-ai/dsh-client-ui-slots": "0.1.5-rc.2", - "@deepseek-ai/dsh-client-ui-workspace": "0.1.5-rc.2", + "@deepseek-ai/cordis": "^4.0.4", + "@deepseek-ai/dsh-api-session-controller": "0.2.0-rc.2", + "@deepseek-ai/dsh-api-workspace-controller": "0.2.0-rc.2", + "@deepseek-ai/dsh-client-connection": "0.2.0-rc.2", + "@deepseek-ai/dsh-client-locale": "0.2.0-rc.2", + "@deepseek-ai/dsh-client-ui-conversation": "0.2.0-rc.2", + "@deepseek-ai/dsh-client-ui-primitives": "0.2.0-rc.2", + "@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.2", + "@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.2", + "@deepseek-ai/dsh-client-ui-workspace": "0.2.0-rc.2", + "@deepseek-ai/dsh-session": "0.2.0-rc.2", "@types/node": "^22.0.0", "@types/react": "^18.3.0", "esbuild": "^0.25.0", diff --git a/scripts/build.mjs b/scripts/build.mjs index 793668f..a8c3244 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -7,7 +7,6 @@ const external = [ 'react/jsx-runtime', '@deepseek-ai/cordis', '@deepseek-ai/dsh-client-connection/client', - '@deepseek-ai/dsh-client-runtime/client', '@deepseek-ai/dsh-client-ui-slots', '@deepseek-ai/dsh-client-ui-primitives', ] diff --git a/src/client/index.tsx b/src/client/index.tsx index 2734d7c..e596fa1 100644 --- a/src/client/index.tsx +++ b/src/client/index.tsx @@ -1,22 +1,27 @@ import { useCallback, useEffect, useState } from 'react' import type { RefObject } from 'react' -import type { ClientContext, SessionId, WorkspaceId, WorkspaceView } from '@deepseek-ai/dsh-client-runtime/client' +import type { Context } from '@deepseek-ai/cordis' +import type { SessionId } from '@deepseek-ai/dsh-session/types' +import type { WorkspaceId, WorkspaceView } from '@deepseek-ai/dsh-api-workspace-controller/client' +import type {} from '@deepseek-ai/dsh-api-session-controller/client' import type { ClientConnectionRpc } from '@deepseek-ai/dsh-client-connection/client' import type {} from '@deepseek-ai/dsh-client-locale/client' import type {} from '@deepseek-ai/dsh-client-ui-conversation/client' +import type { SlotRegistry } from '@deepseek-ai/dsh-client-ui-renderer/client' import type { HostObservable, PropsHooks, PropsLocale, PropsRenderSlots, PropsRuntime, + StoredEntry, } from '@deepseek-ai/dsh-client-ui-slots' -import type { DirectoryFlowOwnerProps } from '@deepseek-ai/dsh-client-ui-workspace/client' +import type { DirectoryFlowOwnerProps, UiWorkspace } from '@deepseek-ai/dsh-client-ui-workspace/client' import { Button, - IconFolderClose16, - IconNewChatOutline16, - IconPlusOutline16, + IconFolderCloseRegular, + IconNewChatOutlineRegular, + IconPlusOutlineRegular, Menu, Modal, type MenuEntry, @@ -39,6 +44,19 @@ import { PROJECTLESS_LOCALE_NS, projectlessLocales } from './locales.ts' const PACKAGE_ID = 'dsh-projectless-session' const PROJECTLESS = PROJECTLESS_ENTRY_ID const ADD_WORKSPACE = '::add-workspace' +const DSH_DIRECTORY_FLOW = 'conversation.hero.workspace.directoryFlow' +const DIRECTORY_FLOW = 'dsh-projectless-session.directoryFlow' + +declare module '@deepseek-ai/dsh-client-ui-slots' { + interface SlotMap { + /** This picker's own directory-flow hole, mirrored from DSH's hero hole. */ + 'dsh-projectless-session.directoryFlow': { + kind: 'single' + scope: 'root' + owner: DirectoryFlowOwnerProps + } + } +} interface PickerActions { createWorkspace(input: { path: string }): Promise @@ -51,7 +69,7 @@ interface PickerActions { type PickerProps = PropsRuntime<'conversation.hero.workspace'> - & PropsRenderSlots<'conversation.hero.workspace.directoryFlow'> + & PropsRenderSlots & Omit & PropsHooks & PropsLocale @@ -92,7 +110,7 @@ function ProjectlessWorkspacePicker({ const workspaceItems: MenuEntry[] = selection.projects.map(workspace => ({ id: workspace.workspaceId, label: workspace.title, - icon: , + icon: , disabled: busy, })) const flowAvailable = useDirectoryFlow((occupied: boolean) => occupied) @@ -103,7 +121,7 @@ function ProjectlessWorkspacePicker({ { id: PROJECTLESS, label: t('picker.projectless'), - icon: , + icon: , disabled: busy, }, ...flowAvailable ? [ @@ -111,7 +129,7 @@ function ProjectlessWorkspacePicker({ { id: ADD_WORKSPACE, label: t('picker.addWorkspace'), - icon: , + icon: , disabled: busy, }, ] : [], @@ -176,7 +194,7 @@ function ProjectlessWorkspacePicker({ portal getAnchorRect={getAnchorRect} /> - {renderSlot('conversation.hero.workspace.directoryFlow', directoryFlowOwner)} + {renderSlot(DIRECTORY_FLOW, directoryFlowOwner)} { setModalError(null) }} @@ -210,11 +228,72 @@ function installStyles(): () => void { return () => { style.remove() } } +type LooseRegister = (options: Record, component: unknown) => () => void + +/** + * DSH 0.2 lets exactly one entry declare a hole and render only holes it + * declared, and the built-in picker already declares the hero directory-flow + * hole. Re-register whatever occupies that hole (the native chooser or the + * in-app browser) into this picker's own hole so "Add workspace…" keeps the + * composed picking interaction. + */ +function mirrorDirectoryFlow(slots: SlotRegistry): () => void { + const register = slots.register.bind(slots) as unknown as LooseRegister + let mirrored: readonly StoredEntry[] | undefined + let disposers: (() => void)[] = [] + const release = (): void => { + for (const dispose of disposers) dispose() + disposers = [] + } + const sync = (): void => { + const entries = slots.entries(DSH_DIRECTORY_FLOW) + if (entries === mirrored) return + mirrored = entries + release() + disposers = entries.map(entry => register({ + name: DIRECTORY_FLOW, + ...entry.options.priority === undefined ? {} : { priority: entry.options.priority }, + ...entry.inject === undefined ? {} : { inject: entry.inject }, + ...entry.locale === undefined ? {} : { locale: entry.locale }, + }, entry.component)) + } + const unsubscribe = slots.subscribe(DSH_DIRECTORY_FLOW, sync) + sync() + return () => { + unsubscribe() + release() + } +} + export const name = PACKAGE_ID -export const inject = ['connection', 'locale', 'slots', 'sessions', 'workspaces'] +export const inject = ['connection', 'locale', 'slots', 'sessions', 'workspaces', 'uiWorkspace'] + +/** + * DSH 0.2 split the old combined Workspace face: registrations live on + * `ctx.workspaces`, blank-Session connection, navigation and archival on + * `ctx.uiWorkspace`. + */ +function projectlessHost(ctx: Context) { + const workspaces = ctx.workspaces + const ui: UiWorkspace = ctx.uiWorkspace + const sessions = { + list: ctx.sessions.list, + open: (sessionId: SessionId) => { ui.openSession(sessionId) }, + } + const host = { + list: workspaces.list, + create: (input: { path: string }) => workspaces.create(input), + delete: (workspaceId: WorkspaceId) => workspaces.delete(workspaceId), + rename: (workspaceId: WorkspaceId, title: string) => workspaces.rename(workspaceId, title), + connectWorkspace: (workspaceId: WorkspaceId) => ui.connectWorkspace(workspaceId), + archiveSession: (sessionId: SessionId) => ui.archiveSession(sessionId), + } + return { host, sessions } +} /** Install a priority -1 picker; DSH's built-in priority 0 picker remains the automatic fallback. */ -export function apply(ctx: ClientContext): void { +export function apply(ctx: Context): void { + const { host, sessions } = projectlessHost(ctx) ctx.effect(installStyles, `${PACKAGE_ID}: styles`) ctx.effect( () => ctx.locale.register(PROJECTLESS_LOCALE_NS, projectlessLocales), @@ -229,8 +308,8 @@ export function apply(ctx: ClientContext): void { registry.has(workspace.workspaceId) || isProjectlessPath(workspace.path) ) const directoryFlow: HostObservable = { - getSnapshot: () => ctx.slots.entries('conversation.hero.workspace.directoryFlow').length > 0, - subscribe: (listener: () => void) => ctx.slots.subscribe('conversation.hero.workspace.directoryFlow', listener), + getSnapshot: () => ctx.slots.entries(DIRECTORY_FLOW).length > 0, + subscribe: (listener: () => void) => ctx.slots.subscribe(DIRECTORY_FLOW, listener), } ctx.effect(() => { let disposed = false @@ -238,8 +317,8 @@ export function apply(ctx: ClientContext): void { void requestProjectlessRoot(rpc).then(root => { if (disposed) return stopSweep = sweepAbandonedProjectlessWorkspaces( - ctx.workspaces, - ctx.sessions, + host, + sessions, root, removeDirectory, pendingWorkspaceIds, @@ -252,13 +331,13 @@ export function apply(ctx: ClientContext): void { } }, `${PACKAGE_ID}: sweep leftover unused workspaces`) const actions = (): PickerActions => ({ - createWorkspace: input => ctx.workspaces.create(input), + createWorkspace: input => host.create(input), isProjectlessWorkspace, hooks: { directoryFlow }, createProjectlessSession: async () => { const receipt = await createAndOpenProjectlessSession( - ctx.workspaces, - ctx.sessions, + host, + sessions, // The published Connection package augments the same Cordis key with // Host and Client faces; this file is bundled only for the Client face. () => requestProjectlessDirectory(rpc), @@ -267,12 +346,12 @@ export function apply(ctx: ClientContext): void { pendingWorkspaceIds, ) const translate = ctx.locale.bind(PROJECTLESS_LOCALE_NS) - await ctx.workspaces.rename(receipt.workspaceId, translate('picker.projectless')).catch(() => {}) + await host.rename(receipt.workspaceId, translate('picker.projectless')).catch(() => {}) ctx.effect( () => { const stop = watchTemporaryWorkspace( - ctx.workspaces, - ctx.sessions, + host, + sessions, receipt, removeDirectory, claim, @@ -291,11 +370,12 @@ export function apply(ctx: ClientContext): void { name: 'conversation.hero.workspace', priority: -1, children: { - 'conversation.hero.workspace.directoryFlow': { kind: 'single', scope: 'root' }, + [DIRECTORY_FLOW]: { kind: 'single', scope: 'root' }, }, inject: actions, locale: PROJECTLESS_LOCALE_NS, }, ProjectlessWorkspacePicker, )) + ctx.slots.inject(DIRECTORY_FLOW, () => mirrorDirectoryFlow(ctx.slots)) } diff --git a/src/client/session.ts b/src/client/session.ts index 5a6e4c2..4dd3fd4 100644 --- a/src/client/session.ts +++ b/src/client/session.ts @@ -1,10 +1,16 @@ -import type { SessionId, WorkspaceId, WorkspaceView } from '@deepseek-ai/dsh-client-runtime/client' +import type { SessionId } from '@deepseek-ai/dsh-session/types' +import type { WorkspaceId, WorkspaceView } from '@deepseek-ai/dsh-api-workspace-controller/client' import type { ClientConnectionRpc } from '@deepseek-ai/dsh-client-connection/client' import { isManagedSessionPath, isProjectlessPath } from '../shared/paths.ts' +import { PROJECTLESS_RPC_CHANNEL, projectlessEndpoint } from '../shared/rpc.ts' export { isProjectlessPath } -/** Public DSH faces required to turn a directory into an ungrouped Session. */ +/** + * Public DSH faces required to turn a directory into an ungrouped Session: + * Workspace registration from `ctx.workspaces`, blank-Session connection and + * archival from `ctx.uiWorkspace`. + */ export interface ProjectlessSessionHost { create(input: { path: string }): Promise connectWorkspace(workspaceId: WorkspaceId): Promise @@ -12,9 +18,16 @@ export interface ProjectlessSessionHost { archiveSession(sessionId: SessionId): Promise } +/** Reference source DSH's main Conversation view retains its Session under. */ +export const MAIN_VIEW_SOURCE = 'mainView' + +export interface SessionRowSlice { + blank: boolean + retainedBy?: Readonly>> | undefined +} + export interface SessionListSlice { - current?: SessionId | undefined - byId: Partial> + byId: Partial> } export interface SessionNavigator { @@ -27,7 +40,12 @@ export interface SessionNavigator { export interface WorkspaceListSlice { items: readonly Pick[] - baselinesReady?: boolean | undefined + phase?: 'pending' | 'ready' | undefined +} + +/** DSH 0.2 has no `current` field; the main view holds a `mainView` reference instead. */ +export function isCurrentSession(sessions: SessionListSlice, sessionId: SessionId): boolean { + return (sessions.byId[sessionId]?.retainedBy?.[MAIN_VIEW_SOURCE] ?? 0) > 0 } export interface WorkspaceNavigator { @@ -131,7 +149,7 @@ function expectPathObject(value: unknown, key: 'path' | 'root', label: string): /** Call the plugin Host half and validate its intentionally tiny response. */ export async function requestProjectlessDirectory(rpc: ClientConnectionRpc): Promise { return expectPathObject( - rpcValue(await rpc.call('/projectless-session', 'create-directory', {}), 'projectless session Host'), + rpcValue(await rpc.call(PROJECTLESS_RPC_CHANNEL, projectlessEndpoint('create-directory'), {}), 'projectless session Host'), 'path', 'projectless session Host', ) @@ -139,14 +157,14 @@ export async function requestProjectlessDirectory(rpc: ClientConnectionRpc): Pro export async function requestProjectlessRoot(rpc: ClientConnectionRpc): Promise { return expectPathObject( - rpcValue(await rpc.call('/projectless-session', 'get-root', {}), 'projectless session Host'), + rpcValue(await rpc.call(PROJECTLESS_RPC_CHANNEL, projectlessEndpoint('get-root'), {}), 'projectless session Host'), 'root', 'projectless session Host', ) } export async function requestRemoveProjectlessDirectory(rpc: ClientConnectionRpc, path: string): Promise { - rpcValue(await rpc.call('/projectless-session', 'remove-directory', { path }), 'projectless session Host') + rpcValue(await rpc.call(PROJECTLESS_RPC_CHANNEL, projectlessEndpoint('remove-directory'), { path }), 'projectless session Host') } /** @@ -206,7 +224,7 @@ export function isAbandonedProjectlessWorkspace( // races create→connectWorkspace and surfaces workspace-not-found. if (workspace.sessionIds.length === 0) return false for (const sessionId of workspace.sessionIds) { - if (sessions.current === sessionId) return false + if (isCurrentSession(sessions, sessionId)) return false const row = sessions.byId[sessionId] if (row === undefined || row.blank !== true) return false } @@ -275,7 +293,7 @@ export function watchTemporaryWorkspace( finish(async () => { await workspaces.delete(receipt.workspaceId) }) return } - if (snapshot.current === receipt.sessionId) return + if (isCurrentSession(snapshot, receipt.sessionId)) return abandon() } @@ -333,7 +351,7 @@ export function sweepAbandonedProjectlessWorkspaces( const reconcile = (): void => { if (!active) return const workspaceState = workspaces.list.getSnapshot() - if (workspaceState.baselinesReady === false) return + if (workspaceState.phase === 'pending') return for (const leftover of findAbandonedProjectlessWorkspaces( workspaceState.items, sessions.list.getSnapshot(), diff --git a/src/index.ts b/src/index.ts index 3ff3e3e..e5a654c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,8 +7,14 @@ import { removeUnusedProjectlessDirectory, resolveProjectlessRoot, } from './host/directories.ts' +import { + PROJECTLESS_ENDPOINTS, + PROJECTLESS_RPC_CHANNEL, + projectlessEndpoint, + type ProjectlessEndpoint, +} from './shared/rpc.ts' -/** Host half: provides an authenticated DSH RPC for filesystem provisioning. */ +/** Host half: provides authenticated DSH endpoints for filesystem provisioning. */ export const name = 'dsh-projectless-session' export const inject = ['connection'] @@ -17,60 +23,105 @@ export interface Config { root?: string } +type RpcResult = + | { ok: true, value: unknown } + | { ok: false, error: { code: string, message: string, details: Record } } + function pathPayload(payload: unknown): string | undefined { if (typeof payload !== 'object' || payload === null) return undefined const path = (payload as { path?: unknown }).path return typeof path === 'string' ? path : undefined } -function badRequest(message: string) { +function badRequest(message: string): RpcResult { return { - ok: false as const, + ok: false, error: { - code: 'bad-request' as const, + code: 'bad-request', message, details: { issues: [] }, }, } } -function internalError(message: string) { +function internalError(message: string): RpcResult { return { - ok: false as const, + ok: false, error: { - code: 'internal' as const, + code: 'internal', message, details: {}, }, } } -/** Register the least-privilege channel used by the browser half. */ +/** Run one decoded endpoint against the configured root. */ +export async function dispatchProjectlessEndpoint( + root: string, + endpoint: ProjectlessEndpoint, + payload: unknown, +): Promise { + try { + if (endpoint === 'create-directory') { + return { ok: true, value: { path: await createProjectlessDirectory(root) } } + } + if (endpoint === 'get-root') { + return { ok: true, value: { root: await resolveProjectlessRoot(root) } } + } + const path = pathPayload(payload) + if (path === undefined) return badRequest('remove-directory requires { path }') + return { ok: true, value: { result: await removeUnusedProjectlessDirectory(root, path) } } + } catch (reason) { + const message = reason instanceof Error ? reason.message : String(reason) + if (message.includes('absolute path') || message.includes('not a projectless')) { + return badRequest(message) + } + return internalError(message) + } +} + +function envelope(rpcId: string, result: RpcResult): Response { + return Response.json({ type: 'server-response', rpcId, result }) +} + +/** + * Decode Connection's `client-request` envelope for one exact endpoint and + * answer with its `server-response` envelope, so the browser half keeps using + * `ctx.connection.rpc.call`. + */ +export function createProjectlessFetch(root: string, endpoint: ProjectlessEndpoint) { + const method = projectlessEndpoint(endpoint) + return async (request: Request): Promise => { + if (request.headers.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase() !== 'application/json') { + return new Response('content type must be application/json', { status: 415 }) + } + let body: unknown + try { + body = await request.json() + } catch { + return new Response('body is not JSON', { status: 400 }) + } + const message = body as { type?: unknown, rpcId?: unknown, method?: unknown, payload?: unknown } | null + if (typeof message !== 'object' || message === null || message.type !== 'client-request' || typeof message.rpcId !== 'string') { + return new Response('invalid client-request envelope', { status: 400 }) + } + if (message.method !== method) { + return envelope(message.rpcId, badRequest(`method ${JSON.stringify(message.method)} does not match endpoint ${JSON.stringify(method)}`)) + } + return envelope(message.rpcId, await dispatchProjectlessEndpoint(root, endpoint, message.payload)) + } +} + +/** Register the least-privilege endpoints used by the browser half. */ export function apply(ctx: Context, config: Config = {}): void { const root = config.root ?? join(homedir(), 'Documents', 'DSH') - ctx.effect(() => ctx.connection.rpc.handle( - '/projectless-session', - async (endpoint, payload) => { - try { - if (endpoint === 'create-directory') { - return { ok: true, value: { path: await createProjectlessDirectory(root) } } - } - if (endpoint === 'get-root') { - return { ok: true, value: { root: await resolveProjectlessRoot(root) } } - } - if (endpoint === 'remove-directory') { - const path = pathPayload(payload) - if (path === undefined) return badRequest('remove-directory requires { path }') - return { ok: true, value: { result: await removeUnusedProjectlessDirectory(root, path) } } - } - return badRequest(`unknown projectless-session endpoint ${JSON.stringify(endpoint)}`) - } catch (reason) { - const message = reason instanceof Error ? reason.message : String(reason) - if (message.includes('absolute path') || message.includes('not a projectless')) { - return badRequest(message) - } - return internalError(message) - } - }, - ), 'dsh-projectless-session: rpc') + for (const endpoint of PROJECTLESS_ENDPOINTS) { + const path = `${PROJECTLESS_RPC_CHANNEL}/${projectlessEndpoint(endpoint)}` + ctx.effect(() => ctx.connection.fetch.register({ + path, + methods: ['POST'], + requestBody: 'buffered', + fetch: createProjectlessFetch(root, endpoint), + }), `dsh-projectless-session: ${path}`) + } } diff --git a/src/shared/rpc.ts b/src/shared/rpc.ts new file mode 100644 index 0000000..47afd10 --- /dev/null +++ b/src/shared/rpc.ts @@ -0,0 +1,15 @@ +/** + * Wire address of the Host half. DSH 0.2's `connection.rpc.handle` resolves + * `webServer` from the Connection plugin's own fiber, so third-party channels + * cannot mount; the endpoints ride the already-authenticated shared `/api` + * channel as exact Fetch routes instead. + */ +export const PROJECTLESS_RPC_CHANNEL = '/api' +export const PROJECTLESS_RPC_PREFIX = 'projectless-session' +export const PROJECTLESS_ENDPOINTS = ['create-directory', 'get-root', 'remove-directory'] as const +export type ProjectlessEndpoint = typeof PROJECTLESS_ENDPOINTS[number] + +/** Channel-relative endpoint passed to `ClientConnectionRpc.call`. */ +export function projectlessEndpoint(endpoint: ProjectlessEndpoint): string { + return `${PROJECTLESS_RPC_PREFIX}/${endpoint}` +} diff --git a/tests/host.test.ts b/tests/host.test.ts new file mode 100644 index 0000000..1069b94 --- /dev/null +++ b/tests/host.test.ts @@ -0,0 +1,75 @@ +import assert from 'node:assert/strict' +import { mkdtemp, rm, stat } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import test from 'node:test' +import { apply, createProjectlessFetch } from '../src/index.ts' + +function clientRequest(method: string, payload: unknown, rpcId = 'rpc-1'): Request { + return new Request('http://dsh.internal/api/' + method, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ type: 'client-request', rpcId, method, payload }), + }) +} + +test('answers the Connection client-request envelope with a server-response', async () => { + const root = await mkdtemp(join(tmpdir(), 'dsh-projectless-host-')) + try { + const response = await createProjectlessFetch(root, 'create-directory')( + clientRequest('projectless-session/create-directory', {}, 'rpc-create'), + ) + const body = await response.json() as { type: string, rpcId: string, result: { ok: boolean, value: { path: string } } } + assert.equal(body.type, 'server-response') + assert.equal(body.rpcId, 'rpc-create') + assert.equal(body.result.ok, true) + assert.equal(body.result.value.path.startsWith(root), true) + assert.equal((await stat(body.result.value.path)).isDirectory(), true) + } finally { + await rm(root, { recursive: true, force: true }) + } +}) + +test('rejects a mismatched method and a missing remove-directory path', async () => { + const root = '/tmp/dsh-projectless-host-unused' + const mismatched = await (await createProjectlessFetch(root, 'get-root')( + clientRequest('projectless-session/create-directory', {}), + )).json() as { result: { ok: boolean, error: { code: string } } } + assert.equal(mismatched.result.ok, false) + assert.equal(mismatched.result.error.code, 'bad-request') + + const missing = await (await createProjectlessFetch(root, 'remove-directory')( + clientRequest('projectless-session/remove-directory', {}), + )).json() as { result: { ok: boolean, error: { message: string } } } + assert.equal(missing.result.ok, false) + assert.match(missing.result.error.message, /requires \{ path \}/) + + const wrongType = await createProjectlessFetch(root, 'get-root')(new Request('http://dsh.internal/api/x', { + method: 'POST', + headers: { 'content-type': 'text/plain' }, + body: '{}', + })) + assert.equal(wrongType.status, 415) +}) + +test('registers one exact /api Fetch route per endpoint', () => { + const routes: { path: string, methods: readonly string[] }[] = [] + const ctx = { + effect(execute: () => unknown) { execute() }, + connection: { + fetch: { + register(route: { path: string, methods: readonly string[] }) { + routes.push(route) + return async () => {} + }, + }, + }, + } + apply(ctx as never, { root: '/tmp/dsh-projectless-host-unused' }) + assert.deepEqual(routes.map(route => route.path), [ + '/api/projectless-session/create-directory', + '/api/projectless-session/get-root', + '/api/projectless-session/remove-directory', + ]) + assert.deepEqual(routes.map(route => route.methods), [['POST'], ['POST'], ['POST']]) +}) diff --git a/tests/session.test.ts b/tests/session.test.ts index 2f45a39..82f52b5 100644 --- a/tests/session.test.ts +++ b/tests/session.test.ts @@ -1,10 +1,7 @@ import assert from 'node:assert/strict' import test from 'node:test' -import type { - SessionId, - WorkspaceId, - WorkspaceView, -} from '@deepseek-ai/dsh-client-runtime/client' +import type { SessionId } from '@deepseek-ai/dsh-session/types' +import type { WorkspaceId, WorkspaceView } from '@deepseek-ai/dsh-api-workspace-controller/client' import { PROJECTLESS_ENTRY_ID, createAbandonClaim, @@ -19,6 +16,9 @@ import { type ProjectlessSessionHost, } from '../src/client/session.ts' +/** How DSH 0.2 marks the Session shown in the main Conversation view. */ +const MAIN_VIEW = { mainView: 1 } + test('keeps the blank Session usable, then detaches after the first accepted prompt', async () => { const operations: string[] = [] const workspaceId = 'workspace-1' as WorkspaceId @@ -62,7 +62,7 @@ test('keeps the blank Session usable, then detaches after the first accepted pro operations.push('session:open') }, list: { - getSnapshot: () => ({ current: sessionId, byId: { [sessionId]: { blank } } }), + getSnapshot: () => ({ byId: { [sessionId]: { blank, retainedBy: MAIN_VIEW } } }), subscribe(listener: () => void) { listeners.add(listener) return () => { listeners.delete(listener) } @@ -205,7 +205,7 @@ test('watchTemporaryWorkspace detaches after the first accepted prompt without r const sessions = { open() {}, list: { - getSnapshot: () => ({ current: sessionId, byId: { [sessionId]: { blank } } }), + getSnapshot: () => ({ byId: { [sessionId]: { blank, retainedBy: MAIN_VIEW } } }), subscribe(listener: () => void) { listeners.add(listener) return () => { listeners.delete(listener) } @@ -244,7 +244,9 @@ test('watchTemporaryWorkspace abandons a still-blank Session after the user leav const sessions = { open() {}, list: { - getSnapshot: () => ({ current, byId: { [sessionId]: { blank: true } } }), + getSnapshot: () => ({ + byId: { [sessionId]: { blank: true, retainedBy: current === sessionId ? MAIN_VIEW : {} } }, + }), subscribe(listener: () => void) { listeners.add(listener) return () => { listeners.delete(listener) } @@ -288,7 +290,7 @@ test('watchTemporaryWorkspace abandons a still-blank Session when the watcher is const sessions = { open() {}, list: { - getSnapshot: () => ({ current: sessionId, byId: { [sessionId]: { blank: true } } }), + getSnapshot: () => ({ byId: { [sessionId]: { blank: true, retainedBy: MAIN_VIEW } } }), subscribe: () => () => {}, }, } @@ -317,7 +319,7 @@ test('watchTemporaryWorkspace does not abandon while the blank Session is curren const sessions = { open() {}, list: { - getSnapshot: () => ({ current: sessionId, byId: { [sessionId]: { blank: true } } }), + getSnapshot: () => ({ byId: { [sessionId]: { blank: true, retainedBy: MAIN_VIEW } } }), subscribe: () => () => {}, }, } @@ -357,10 +359,9 @@ test('findAbandonedProjectlessWorkspaces skips the current Session and unmanaged workspaceView(realId, '/Users/test/codespace/app', [usedSession]), ], { - current: currentSession, byId: { [leftoverSession]: { blank: true }, - [currentSession]: { blank: true }, + [currentSession]: { blank: true, retainedBy: MAIN_VIEW }, [usedSession]: { blank: false }, }, }, @@ -378,7 +379,7 @@ test('sweepAbandonedProjectlessWorkspaces removes leftover unused registrations' const items = [workspaceView(leftoverId, leftoverPath, [leftoverSession])] const workspaces = { list: { - getSnapshot: () => ({ items, baselinesReady: true }), + getSnapshot: () => ({ items, phase: 'ready' as const }), subscribe: () => () => {}, }, async delete(id: WorkspaceId) { operations.push(`delete:${id}`) },