From 88939f93381bcbe708a77d9273443ceec3312e0f Mon Sep 17 00:00:00 2001 From: whitelonng Date: Fri, 28 Aug 2026 00:05:22 +0800 Subject: [PATCH] feat(web): upload files by host path, not staged bytes Send a selected/dragged file's absolute path into the draft as an @path mention, so the agent reads the file in place with zero copy and zero context cost. - new host seam file-picker / file-picker-native (native multi-file picker + exact-basename workspace location) - gateway host.pickFiles / host.locateFiles - composer paperclip button + drag/paste image-vs-file MIME split - Windows native selection fails loud until the GetResults IFileOpenDialog binding lands --- ...26-08-27-web-file-upload-by-path.i18n.yaml | 6 + .../2026-08-27-web-file-upload-by-path.md | 38 ++++ .../2026-08-27-web-file-upload-by-path.zh.md | 38 ++++ apps/desktop/package.json | 2 + apps/web/tests/image-display.snapshot.ts | 10 +- .../snapshots/bash-abort-row/ui.expected.md | 2 + .../snapshots/code-mode-round/ui.expected.md | 2 + .../cordis-tool-round/ui.expected.md | 2 + .../feedback-command/ack.expected.md | 2 + .../snapshots/fresh-round-trip/ui.expected.md | 2 + .../goal-command-presentation/ui.expected.md | 2 + .../goal-multi-turn-actions/ui.expected.md | 2 + .../lifecycle-chrome/hero.expected.md | 2 + .../lifecycle-chrome/plan-active.expected.md | 2 + .../lifecycle-chrome/reloaded.expected.md | 2 + .../live-interactions/cancel.expected.md | 2 + .../live-interactions/error-auth.expected.md | 2 + .../live-interactions/loading.expected.md | 2 + .../retry-exhausted.expected.md | 2 + .../live-interactions/retry.expected.md | 2 + .../markdown-cjk-strong/ui.expected.md | 2 + .../snapshots/markdown-images/ui.expected.md | 2 + .../markdown-inline-code-links/ui.expected.md | 2 + .../snapshots/math-rendering/ui.expected.md | 2 + .../message-actions/delete.expected.md | 2 + .../snapshots/message-actions/ui.expected.md | 2 + .../plan-review/approved.expected.md | 2 + .../question-composer/answered.expected.md | 2 + .../queue-actions/collapsed.expected.md | 2 + .../queue-actions/editing.expected.md | 2 + .../queue-actions/layout.expected.md | 2 + .../queue-actions/preserved.expected.md | 2 + .../snapshots/queue-actions/ui.expected.md | 2 + .../reference-composer/order.expected.md | 2 + .../seeded-history/command-row.expected.md | 2 + .../seeded-history/feedback-row.expected.md | 2 + .../snapshots/seeded-history/ui.expected.md | 2 + .../snapshots/skill-tool-row/ui.expected.md | 2 + .../skill-user-invoke/ui.expected.md | 2 + .../stats-paged-history/ui.expected.md | 2 + .../snapshots/steer-all/mid-steer.expected.md | 2 + .../snapshots/steer-all/settled.expected.md | 2 + .../snapshots/steering/settled.expected.md | 2 + .../subagent-conversation/ui.expected.md | 2 + .../offline-composer.expected.md | 2 + .../turn-tail-actions/running.expected.md | 2 + .../turn-tail-actions/settled.expected.md | 2 + .../snapshots/web-search-round/ui.expected.md | 2 + docs/capability-seams.i18n.yaml | 4 +- docs/capability-seams.md | 7 + docs/capability-seams.zh.md | 7 + docs/config-catalog.i18n.yaml | 4 +- docs/config-catalog.md | 4 +- docs/config-catalog.zh.md | 4 +- docs/module-graph.i18n.yaml | 4 +- docs/module-graph.md | 9 +- docs/module-graph.zh.md | 9 +- docs/subsystems/workspace.i18n.yaml | 4 +- docs/subsystems/workspace.md | 16 ++ docs/subsystems/workspace.zh.md | 16 ++ packages/bundle/web-app/cordis.patch.yml | 5 + packages/bundle/web-app/package.json | 2 + .../client/connection/src/client/fixture.ts | 4 + .../connection/tests/fake-api.client.ts | 2 + .../runtime/src/client/contract/workspaces.ts | 13 ++ .../runtime/src/client/workspaces/service.ts | 27 +++ .../client/runtime/tests/fake-api.client.ts | 2 + packages/client/ui-conversation/package.json | 2 + .../ui-conversation/src/client/apply.ts | 4 + .../src/client/contract/slots.ts | 4 + .../ui-conversation/src/client/locales.ts | 4 + .../src/client/skeleton/InputBar.tsx | 146 +++++++++++--- .../tests/input-bar.client.spec.tsx | 82 +++++--- .../tests/input-matrix.client.spec.tsx | 2 + .../tests/input-scenarios.client.spec.tsx | 2 + .../tests/skeleton.client.spec.tsx | 2 + packages/client/ui-conversation/tsconfig.json | 3 + .../extensions/tool-cordis/src/api-catalog.ts | 35 +++- packages/host/apiproxy/src/api-proxy.ts | 73 +++++++ packages/host/apiproxy/src/api/host.schema.ts | 28 +++ packages/host/apiproxy/src/api/host.ts | 39 ++++ packages/host/apiproxy/src/api/index.ts | 2 +- packages/host/apiproxy/src/api/rpc-map.ts | 2 + packages/host/apiproxy/src/api/rpc.schema.ts | 1 + packages/host/apiproxy/src/api/rpc.ts | 1 + packages/host/apiproxy/src/fetch/client.ts | 12 +- packages/host/apiproxy/src/fetch/handler.ts | 6 +- packages/host/apiproxy/src/index.ts | 2 +- .../tests/api-proxy-workspace.spec.ts | 101 +++++++++- .../apiproxy/tests/client-handler.spec.ts | 2 + .../host/apiproxy/tests/fetch-carrier.spec.ts | 7 + packages/host/apiproxy/tsconfig.json | 3 + .../host/file-picker-native/README.i18n.yaml | 6 + packages/host/file-picker-native/README.md | 20 ++ packages/host/file-picker-native/README.zh.md | 20 ++ packages/host/file-picker-native/package.json | 46 +++++ packages/host/file-picker-native/src/index.ts | 31 +++ .../host/file-picker-native/src/invariant.ts | 25 +++ .../file-picker-native/src/native-picker.ts | 132 +++++++++++++ .../tests/native-picker.spec.ts | 187 ++++++++++++++++++ .../file-picker-native/tests/service.spec.ts | 21 ++ .../host/file-picker-native/tsconfig.json | 24 +++ packages/host/file-picker/README.i18n.yaml | 6 + packages/host/file-picker/README.md | 20 ++ packages/host/file-picker/README.zh.md | 20 ++ packages/host/file-picker/package.json | 47 +++++ packages/host/file-picker/src/index.ts | 73 +++++++ packages/host/file-picker/src/invariant.ts | 25 +++ packages/host/file-picker/src/locate.ts | 125 ++++++++++++ .../host/file-picker/tests/locate.spec.ts | 137 +++++++++++++ packages/host/file-picker/tests/seam.spec.ts | 29 +++ packages/host/file-picker/tsconfig.json | 21 ++ .../client-runtime/src/workspaces.ts | 24 +++ pnpm-lock.yaml | 41 ++++ scripts/gen-cordis-catalog.ts | 2 + scripts/gen-doc-graphs.ts | 9 + .../verify-package-readme-model-experience.ts | 2 + tsconfig.base.json | 4 + tsconfig.host.json | 2 + 119 files changed, 1908 insertions(+), 79 deletions(-) create mode 100644 .agents/notes/implemented/architecture/2026-08-27-web-file-upload-by-path.i18n.yaml create mode 100644 .agents/notes/implemented/architecture/2026-08-27-web-file-upload-by-path.md create mode 100644 .agents/notes/implemented/architecture/2026-08-27-web-file-upload-by-path.zh.md create mode 100644 packages/host/file-picker-native/README.i18n.yaml create mode 100644 packages/host/file-picker-native/README.md create mode 100644 packages/host/file-picker-native/README.zh.md create mode 100644 packages/host/file-picker-native/package.json create mode 100644 packages/host/file-picker-native/src/index.ts create mode 100644 packages/host/file-picker-native/src/invariant.ts create mode 100644 packages/host/file-picker-native/src/native-picker.ts create mode 100644 packages/host/file-picker-native/tests/native-picker.spec.ts create mode 100644 packages/host/file-picker-native/tests/service.spec.ts create mode 100644 packages/host/file-picker-native/tsconfig.json create mode 100644 packages/host/file-picker/README.i18n.yaml create mode 100644 packages/host/file-picker/README.md create mode 100644 packages/host/file-picker/README.zh.md create mode 100644 packages/host/file-picker/package.json create mode 100644 packages/host/file-picker/src/index.ts create mode 100644 packages/host/file-picker/src/invariant.ts create mode 100644 packages/host/file-picker/src/locate.ts create mode 100644 packages/host/file-picker/tests/locate.spec.ts create mode 100644 packages/host/file-picker/tests/seam.spec.ts create mode 100644 packages/host/file-picker/tsconfig.json diff --git a/.agents/notes/implemented/architecture/2026-08-27-web-file-upload-by-path.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-27-web-file-upload-by-path.i18n.yaml new file mode 100644 index 0000000000..eeba1bbb8c --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-08-27-web-file-upload-by-path.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-27-web-file-upload-by-path.md +2026-08-27-web-file-upload-by-path.md: 584d4095b00fb3f3abfc2781bb33765151c8d0bd +2026-08-27-web-file-upload-by-path.zh.md: 98251922eaa047094789c0befd967448ca71c6f5 \ No newline at end of file diff --git a/.agents/notes/implemented/architecture/2026-08-27-web-file-upload-by-path.md b/.agents/notes/implemented/architecture/2026-08-27-web-file-upload-by-path.md new file mode 100644 index 0000000000..584d4095b0 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-08-27-web-file-upload-by-path.md @@ -0,0 +1,38 @@ +# Agent Note: Web GUI file upload sends host paths, not staged bytes + +Status: implemented + +English | [中文](2026-08-27-web-file-upload-by-path.zh.md) + +## Problem + +The composer only accepted images, and only as base64 inlined into the prompt (`createDraftImages` rejected every non-`png/jpeg/webp/gif` MIME with `UnsupportedImageMediaTypeError`). A user could not hand the agent an arbitrary local file without either staging a copy on disk or inflating the conversation with its bytes. The browser can never read a dragged file's real host path — `File` exposes only `name`, `size`, and content — so "send the path and let the agent read it" has no pure-browser route. + +## Decision + +Two complementary host capabilities, plus a composer split, all path-only: + +- **`file-picker` / `file-picker-native`** (`ctx.filePicker`): a native-only capability seam whose single interaction `pickFiles({ multiple }, signal)` opens one OS chooser on the host display and returns the selected absolute paths. Native-only because a remote client has no display to open a chooser on; unlike `directory-picker` there is no browse twin. The gateway serves it as `host.pickFiles`, answering `file-picker-unavailable` under any non-native backend (fail loud, never fake a pick). macOS uses `osascript choose file`; Linux uses Zenity with a KDialog fallback; Windows fails loud until the koffi `IFileOpenDialog` file multi-select conversation is built. +- **Basename location** (`file-picker`'s `./locate`): `locateByName(root, name)` walks a tree for exact-basename matches and returns absolute paths — the zero-byte, zero-write answer for a drag, whose only input is a `name`. It accepts an optional `systemSearch` delegate for anything wider than the workspace walk (spotlight/`find`), which this increment does not wire; the gateway's `host.locateFiles` walks the session workspace only. +- **Composer split** (`InputBar`): a dropped/pasted file is classified by MIME — multi-modal images join the image rail unchanged; everything else resolves through `locateFiles` and inserts a `@path` mention (reusing `formatFileMention`) — never staged bytes. A new paperclip button drives `pickFiles` and inserts every selected path as a mention. A basename with zero or several matches is announced ("use the add-file button"), not silently dropped. + +Why these shapes won: + +- **Path-only beats staging.** Staging a copy into the workspace would satisfy "the agent reads it by path" but spends disk and risks clobbering the user's tree; a content-addressed object store (like images) sidesteps clobbering but yields no readable path at all. Returning the real path is the only option that is simultaneously zero-copy, zero-context, and agent-readable. Model-visible ⟺ logged holds trivially: the mention is ordinary prompt text. +- **Drag gets a name, not a path.** The `locateByName` exact-basename walk is precise and unambiguous at the workspace tier; the system-wide tier stays an injected delegate because its cost and coverage are the caller's policy, and this increment has no workspace-external consumer yet. +- **Native-only, discriminated anyway.** The `FilePickerCapabilities` map is declared merge-extensible from day one, mirroring `directory-picker`, so a future backend adds its shape without editing this package. The failure mode for an unsupported interaction is a typed `file-picker-unavailable` code, matching `directory-picker-unavailable`. +- **MIME decides image vs file in the composer.** The image rail's own admission keys on MIME too, so a suffix-only match would be refused there; routing on MIME alone keeps one rule and lets an unknown-type file fall through to path location (the agent picks the reading tool). + +## Alternatives considered + +- **Stage-then-mention and submit-time atomic staging** — both write bytes to disk (rejected; the operator asked for zero landing), and submit-time staging hides the path from the draft. +- **Bundle a system-wide search now** — `mdfind`/`find` per platform adds an OS-command surface and cost policy this increment has no consumer for; the `systemSearch` delegate keeps the seam ready without committing those choices. +- **Reuse `@file`'s `WorkspaceFileSearch`** — that index returns workspace-relative paths for autocomplete ranking, not absolute paths from a bare basename; a separate exact-basename walk is a different, smaller query. +- **Extend `directory-picker` with a `pickFiles` method** — rejected: the seam is named for directory selection and its browse/native contract is about levels and creation, not file selection; a sibling package keeps each contract's consumer set honest. + +## Consequences + +- Two new host packages (`file-picker`, `file-picker-native`), two gateway methods (`host.pickFiles`, `host.locateFiles`), and one error code (`file-picker-unavailable`) widen the web GUI's host surface; each mirrors the `directory-picker` seam and rides the same apiproxy/rpc/schema layers. +- The composer splits on the `image/*` MIME family, so a deployment's narrowed `imageLimits.mediaTypes` no longer changes the image-vs-file split (an `image/*` type keeps the image rail's authoritative "only PNG/JPG/WebP/GIF" refusal); an empty-MIME file always resolves by path. +- Windows file selection is not shipped: `pickNativeFiles` fails loud on win32 until the koffi `IFileOpenDialog` file multi-select conversation lands. Remote clients likewise have no display to open a chooser on. +- `locateByName` walks the workspace tree only; a wider `systemSearch` tier is an injected delegate left unwired until a workspace-external consumer needs it. Dragged basenames with zero or several matches announce "use the add-file button" rather than guessing. \ No newline at end of file diff --git a/.agents/notes/implemented/architecture/2026-08-27-web-file-upload-by-path.zh.md b/.agents/notes/implemented/architecture/2026-08-27-web-file-upload-by-path.zh.md new file mode 100644 index 0000000000..98251922ea --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-08-27-web-file-upload-by-path.zh.md @@ -0,0 +1,38 @@ +# Agent Note:Web GUI 文件上传发送宿主路径,而非暂存字节 + +Status: implemented + +[English](2026-08-27-web-file-upload-by-path.md) | 中文 + +## Problem + +对话框只接受图片,而且只以 base64 内嵌进提示文本(`createDraftImages` 对非 `png/jpeg/webp/gif` 的 MIME 一律抛 `UnsupportedImageMediaTypeError`)。用户在既不暂存磁盘副本、也不膨胀对话上下文的前提下,无法把任意本地文件交给智能体。浏览器永远读不到拖拽文件的真实宿主路径——`File` 只暴露 `name`、`size` 和内容——因此「发送路径、让智能体去读」没有纯浏览器路线。 + +## Decision + +两个互补的宿主能力,加上对话框的分流,全程只发路径: + +- **`file-picker` / `file-picker-native`**(`ctx.filePicker`):一个仅 native 的能力缝隙,其唯一交互 `pickFiles({ multiple }, signal)` 在宿主显示器上打开一个操作系统文件选择框并返回选中的绝对路径。只做 native,因为远程客户端没有可打开选择框的显示器;与 `directory-picker` 不同,没有 browse 孪生形态。网关以 `host.pickFiles` 提供它,在非 native 后端下回答 `file-picker-unavailable`(大声失败,绝不假装已选)。macOS 用 `osascript choose file`;Linux 用 Zenity、以 KDialog 回退;Windows 在建成 koffi `IFileOpenDialog` 文件多选对话之前大声失败。 +- **Basename 定位**(`file-picker` 的 `./locate`):`locateByName(root, name)` 遍历目录树做 basename 精确匹配并返回绝对路径——这是拖拽的零字节、零写入答案,其唯一输入就是 `name`。它接受可选的 `systemSearch` 委托来处理任何比工作区遍历更宽的搜索(spotlight/`find`),本增量不接线;网关的 `host.locateFiles` 只遍历会话工作区。 +- **对话框分流**(`InputBar`):对拖入/粘贴的文件按 MIME 分类——多模态图片照旧进图片轨道;其余全部经 `locateFiles` 解析并插入 `@path` 引用(复用 `formatFileMention`)——绝不暂存字节。新增的回形针按钮驱动 `pickFiles` 并把每个选中路径插为引用。basename 命中零个或多个时给出提示(「请用添加文件按钮」),而不是静默丢弃。 + +这些取舍胜出的原因: + +- **只发路径胜过暂存。** 把副本暂存进工作区能满足「智能体按路径读」,但消耗磁盘并可能污染用户的目录树;内容寻址对象存储(如图片)能避开污染,却根本拿不出可读路径。返回真实路径是唯一同时做到零复制、零上下文、且智能体可读的方案。「模型可见 ⟺ 已入日志」天然成立:引用就是普通提示文本。 +- **拖拽拿到的是名字,不是路径。** `locateByName` 的 basename 精确遍历在工作区这一层是精确且无歧义的;系统级层保持为注入式委托,因为其开销与覆盖范围属于调用方策略,而本增量尚无工作区之外的消费者。 +- **仅 native,但仍然判别式。** `FilePickerCapabilities` 映射从第一天起就声明为可合并扩展,镜像 `directory-picker`,因此未来后端无需修改本包即可增加其形态。不支持的交互以大写的 `file-picker-unavailable` 码失败,与 `directory-picker-unavailable` 一致。 +- **对话框里由 MIME 决定图片还是文件。** 图片轨道自身的准入同样以 MIME 为准,因此仅按后缀匹配会被拒;只按 MIME 路由保持单一规则,并让未知类型的文件落入路径定位(由智能体选择读取工具)。 + +## Alternatives considered + +- **先暂存再引用、以及提交时原子暂存**——两者都写字节到磁盘(被拒;操作者要求零落地),且提交时暂存会把路径藏出草稿。 +- **现在就内置系统级搜索**——按平台 `mdfind`/`find` 会引入操作系统命令面与开销策略,而本增量没有消费者;`systemSearch` 委托让缝隙保持就绪而不必现在拍板这些选择。 +- **复用 `@file` 的 `WorkspaceFileSearch`**——那个索引为自动补全排序返回工作区相对路径,而不是从裸 basename 得到绝对路径;单独的 basename 精确遍历是另一个更小的查询。 +- **给 `directory-picker` 加 `pickFiles` 方法**——被拒:该缝隙以目录选择命名,其 browse/native 契约关乎层级与创建,而非文件选择;兄弟包让每个契约的消费者集合保持诚实。 + +## Consequences + +- 新增两个宿主包(`file-picker`、`file-picker-native`)、两个网关方法(`host.pickFiles`、`host.locateFiles`)和一个错误码(`file-picker-unavailable`),扩大了 Web GUI 的宿主面;各自镜像 `directory-picker` 缝隙,并受同一套 apiproxy/rpc/schema 层覆盖。 +- 对话框按 `image/*` MIME 族分流,因此部署收窄 `imageLimits.mediaTypes` 不再改变「图片 vs 文件」的划分(`image/*` 类型保留图片轨道权威的「仅 PNG/JPG/WebP/GIF」拒绝);空 MIME 文件始终按路径解析。 +- Windows 文件选择未随此交付:在 koffi `IFileOpenDialog` 文件多选对话落地之前,`pickNativeFiles` 在 win32 上大声失败。远程客户端同样没有可打开选择框的显示器。 +- `locateByName` 只遍历工作区树;更宽的 `systemSearch` 层级是注入式委托,留待出现工作区之外消费者时再接线。basename 命中零个或多个时给出「请用添加文件按钮」提示,而非猜测。 \ No newline at end of file diff --git a/apps/desktop/package.json b/apps/desktop/package.json index f32aafeafb..078e10d85e 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -81,6 +81,8 @@ "@deepseek-ai/dsh-host-apiproxy": "workspace:^", "@deepseek-ai/dsh-host-directory-picker-browse": "workspace:^", "@deepseek-ai/dsh-host-directory-picker-native": "workspace:^", + "@deepseek-ai/dsh-host-file-picker": "workspace:^", + "@deepseek-ai/dsh-host-file-picker-native": "workspace:^", "@deepseek-ai/dsh-host-plugin-control": "workspace:^", "@deepseek-ai/dsh-host-plugin-installer": "workspace:^", "@deepseek-ai/dsh-host-plugin-inventory": "workspace:^", diff --git a/apps/web/tests/image-display.snapshot.ts b/apps/web/tests/image-display.snapshot.ts index fcdcc19f0f..5f6589c63c 100644 --- a/apps/web/tests/image-display.snapshot.ts +++ b/apps/web/tests/image-display.snapshot.ts @@ -134,19 +134,19 @@ it('accepts pasted images into the composer rail in order and removes them', asy expect(document.querySelector('[role="group"][aria-label="Pending images"]')).toBeNull() }) - // An unsupported file announces a transient toast (the inline strip is - // gone) and the banner dismisses itself after its hold-and-fade lifetime. + // A non-image file resolves by basename; an empty match announces the + // add-file fallback (the inline strip is already gone). fireEvent.paste(textarea, { clipboardData: { items: [{ kind: 'file', type: 'text/plain', getAsFile: () => new File(['x'], 'notes.txt', { type: 'text/plain' }) }], getData: () => '', }, }) - const unsupportedMessage = 'Only PNG, JPG, WebP, and GIF images are supported' - const toast = await screen.findByText(unsupportedMessage) + const locateMessage = 'Cannot locate notes.txt; use the add-file button' + const toast = await screen.findByText(locateMessage) expect(toast.closest('[role="alert"]')).not.toBeNull() await waitFor(() => { - expect(screen.queryByText(unsupportedMessage)).toBeNull() + expect(screen.queryByText(locateMessage)).toBeNull() }, { timeout: 6_000 }) }) diff --git a/apps/web/tests/snapshots/bash-abort-row/ui.expected.md b/apps/web/tests/snapshots/bash-abort-row/ui.expected.md index c6801e5725..eb87db8d96 100644 --- a/apps/web/tests/snapshots/bash-abort-row/ui.expected.md +++ b/apps/web/tests/snapshots/bash-abort-row/ui.expected.md @@ -27,6 +27,8 @@ - img - text: "Failed Bash Error: tool call aborted before dispatch" - textbox "Message the agent" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/code-mode-round/ui.expected.md b/apps/web/tests/snapshots/code-mode-round/ui.expected.md index f451d2a545..72559b5fa7 100644 --- a/apps/web/tests/snapshots/code-mode-round/ui.expected.md +++ b/apps/web/tests/snapshots/code-mode-round/ui.expected.md @@ -50,6 +50,8 @@ - img - text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s - textbox "Message the agent" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/cordis-tool-round/ui.expected.md b/apps/web/tests/snapshots/cordis-tool-round/ui.expected.md index a4f82bc4a5..275c89c15e 100644 --- a/apps/web/tests/snapshots/cordis-tool-round/ui.expected.md +++ b/apps/web/tests/snapshots/cordis-tool-round/ui.expected.md @@ -105,6 +105,8 @@ - img - text: {{clock}} Ran for {{duration}} - textbox "Message the agent" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/feedback-command/ack.expected.md b/apps/web/tests/snapshots/feedback-command/ack.expected.md index 95c15a8a16..3c783e2598 100644 --- a/apps/web/tests/snapshots/feedback-command/ack.expected.md +++ b/apps/web/tests/snapshots/feedback-command/ack.expected.md @@ -41,6 +41,8 @@ - img - text: "feedback Feedback recorded for session session-{{uuid}} Anonymous user: {{uuid}}. Session sharing is enabled." - textbox "Message the agent" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/fresh-round-trip/ui.expected.md b/apps/web/tests/snapshots/fresh-round-trip/ui.expected.md index ad04eb3f72..dfdd74d071 100644 --- a/apps/web/tests/snapshots/fresh-round-trip/ui.expected.md +++ b/apps/web/tests/snapshots/fresh-round-trip/ui.expected.md @@ -45,6 +45,8 @@ - img - text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s - textbox "Message the agent" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/goal-command-presentation/ui.expected.md b/apps/web/tests/snapshots/goal-command-presentation/ui.expected.md index 42811d29ae..b49ea38d39 100644 --- a/apps/web/tests/snapshots/goal-command-presentation/ui.expected.md +++ b/apps/web/tests/snapshots/goal-command-presentation/ui.expected.md @@ -15,6 +15,8 @@ - img - text: "goal No goal is currently set. Usage: /goal [|clear|edit |pause|resume]" - textbox "Message the agent" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/goal-multi-turn-actions/ui.expected.md b/apps/web/tests/snapshots/goal-multi-turn-actions/ui.expected.md index 631d23d357..7aafeeda26 100644 --- a/apps/web/tests/snapshots/goal-multi-turn-actions/ui.expected.md +++ b/apps/web/tests/snapshots/goal-multi-turn-actions/ui.expected.md @@ -207,6 +207,8 @@ - tooltip "Branch into a new conversation" - text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s - textbox "Message the agent" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/lifecycle-chrome/hero.expected.md b/apps/web/tests/snapshots/lifecycle-chrome/hero.expected.md index 64ff6ae8f0..71b400181a 100644 --- a/apps/web/tests/snapshots/lifecycle-chrome/hero.expected.md +++ b/apps/web/tests/snapshots/lifecycle-chrome/hero.expected.md @@ -30,6 +30,8 @@ - text: Standard mode - img - textbox "Describe what you want to build" +- button "Add files": + - img - button "Commands": - img - tooltip "Commands" diff --git a/apps/web/tests/snapshots/lifecycle-chrome/plan-active.expected.md b/apps/web/tests/snapshots/lifecycle-chrome/plan-active.expected.md index 396a9bfa88..334e2977e2 100644 --- a/apps/web/tests/snapshots/lifecycle-chrome/plan-active.expected.md +++ b/apps/web/tests/snapshots/lifecycle-chrome/plan-active.expected.md @@ -30,6 +30,8 @@ - text: Standard mode - img - textbox "Describe what you want to build" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/lifecycle-chrome/reloaded.expected.md b/apps/web/tests/snapshots/lifecycle-chrome/reloaded.expected.md index 59358bb0cb..261faca2a9 100644 --- a/apps/web/tests/snapshots/lifecycle-chrome/reloaded.expected.md +++ b/apps/web/tests/snapshots/lifecycle-chrome/reloaded.expected.md @@ -37,6 +37,8 @@ - img - text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s - textbox "Message the agent" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/live-interactions/cancel.expected.md b/apps/web/tests/snapshots/live-interactions/cancel.expected.md index 3830621454..39c874dbb0 100644 --- a/apps/web/tests/snapshots/live-interactions/cancel.expected.md +++ b/apps/web/tests/snapshots/live-interactions/cancel.expected.md @@ -34,6 +34,8 @@ - img - text: {{clock}} Ran for {{duration}} TTFT {{duration}} - textbox "Message the agent" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/live-interactions/error-auth.expected.md b/apps/web/tests/snapshots/live-interactions/error-auth.expected.md index 2c30a69826..c8011b7464 100644 --- a/apps/web/tests/snapshots/live-interactions/error-auth.expected.md +++ b/apps/web/tests/snapshots/live-interactions/error-auth.expected.md @@ -24,6 +24,8 @@ - text: This turn failedAPI key is invalid - code: AUTH - textbox "Message the agent" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/live-interactions/loading.expected.md b/apps/web/tests/snapshots/live-interactions/loading.expected.md index c1989daab6..94db806800 100644 --- a/apps/web/tests/snapshots/live-interactions/loading.expected.md +++ b/apps/web/tests/snapshots/live-interactions/loading.expected.md @@ -22,6 +22,8 @@ - paragraph: partial - status: Deep diving... - textbox "Message the agent" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/live-interactions/retry-exhausted.expected.md b/apps/web/tests/snapshots/live-interactions/retry-exhausted.expected.md index 6b88671794..2db6bfbb6e 100644 --- a/apps/web/tests/snapshots/live-interactions/retry-exhausted.expected.md +++ b/apps/web/tests/snapshots/live-interactions/retry-exhausted.expected.md @@ -26,6 +26,8 @@ - text: This turn failedupstream 503 - code: SERVER - textbox "Message the agent" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/live-interactions/retry.expected.md b/apps/web/tests/snapshots/live-interactions/retry.expected.md index dd45b60ae2..e42341b79f 100644 --- a/apps/web/tests/snapshots/live-interactions/retry.expected.md +++ b/apps/web/tests/snapshots/live-interactions/retry.expected.md @@ -39,6 +39,8 @@ - img - text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s - textbox "Message the agent" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/markdown-cjk-strong/ui.expected.md b/apps/web/tests/snapshots/markdown-cjk-strong/ui.expected.md index eb4c0a238e..6e98246363 100644 --- a/apps/web/tests/snapshots/markdown-cjk-strong/ui.expected.md +++ b/apps/web/tests/snapshots/markdown-cjk-strong/ui.expected.md @@ -52,6 +52,8 @@ - img - text: {{clock}} Ran for {{duration}} - textbox "Message the agent" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/markdown-images/ui.expected.md b/apps/web/tests/snapshots/markdown-images/ui.expected.md index cb8e2c2cae..9267839bae 100644 --- a/apps/web/tests/snapshots/markdown-images/ui.expected.md +++ b/apps/web/tests/snapshots/markdown-images/ui.expected.md @@ -31,6 +31,8 @@ - img - text: {{clock}} Ran for {{duration}} - textbox "Message the agent" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/markdown-inline-code-links/ui.expected.md b/apps/web/tests/snapshots/markdown-inline-code-links/ui.expected.md index 7780072038..86c94a646e 100644 --- a/apps/web/tests/snapshots/markdown-inline-code-links/ui.expected.md +++ b/apps/web/tests/snapshots/markdown-inline-code-links/ui.expected.md @@ -43,6 +43,8 @@ - img - text: {{clock}} Ran for {{duration}} - textbox "Message the agent" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/math-rendering/ui.expected.md b/apps/web/tests/snapshots/math-rendering/ui.expected.md index 10a1245bd1..56f47535fa 100644 --- a/apps/web/tests/snapshots/math-rendering/ui.expected.md +++ b/apps/web/tests/snapshots/math-rendering/ui.expected.md @@ -47,6 +47,8 @@ - img - text: {{clock}} Ran for {{duration}} - textbox "Message the agent" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/message-actions/delete.expected.md b/apps/web/tests/snapshots/message-actions/delete.expected.md index 5b09d7c702..e3343d4029 100644 --- a/apps/web/tests/snapshots/message-actions/delete.expected.md +++ b/apps/web/tests/snapshots/message-actions/delete.expected.md @@ -50,6 +50,8 @@ - button "Edit": - img - textbox "Message the agent" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/message-actions/ui.expected.md b/apps/web/tests/snapshots/message-actions/ui.expected.md index f1e607e038..14c173beb3 100644 --- a/apps/web/tests/snapshots/message-actions/ui.expected.md +++ b/apps/web/tests/snapshots/message-actions/ui.expected.md @@ -63,6 +63,8 @@ - img - text: 7/25 {{clock}} Ran for {{duration}} - textbox "Message the agent" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/plan-review/approved.expected.md b/apps/web/tests/snapshots/plan-review/approved.expected.md index b1d2264ef3..532ea50eb9 100644 --- a/apps/web/tests/snapshots/plan-review/approved.expected.md +++ b/apps/web/tests/snapshots/plan-review/approved.expected.md @@ -48,6 +48,8 @@ - img - text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s - textbox "Message the agent" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/question-composer/answered.expected.md b/apps/web/tests/snapshots/question-composer/answered.expected.md index fbd990ce60..3b1ba1620c 100644 --- a/apps/web/tests/snapshots/question-composer/answered.expected.md +++ b/apps/web/tests/snapshots/question-composer/answered.expected.md @@ -45,6 +45,8 @@ - img - text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s - textbox "Message the agent" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/queue-actions/collapsed.expected.md b/apps/web/tests/snapshots/queue-actions/collapsed.expected.md index eb412facc3..3baa5e1731 100644 --- a/apps/web/tests/snapshots/queue-actions/collapsed.expected.md +++ b/apps/web/tests/snapshots/queue-actions/collapsed.expected.md @@ -23,6 +23,8 @@ - status: Deep diving... - button "2 queued messages" - textbox "Cmd/Ctrl+Enter steers all queued messages" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/queue-actions/editing.expected.md b/apps/web/tests/snapshots/queue-actions/editing.expected.md index e21815c8a4..51a75241bc 100644 --- a/apps/web/tests/snapshots/queue-actions/editing.expected.md +++ b/apps/web/tests/snapshots/queue-actions/editing.expected.md @@ -39,6 +39,8 @@ - button "Cancel editing": - img - textbox "Cmd/Ctrl+Enter steers all queued messages" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/queue-actions/layout.expected.md b/apps/web/tests/snapshots/queue-actions/layout.expected.md index e7d6577325..454f1c957e 100644 --- a/apps/web/tests/snapshots/queue-actions/layout.expected.md +++ b/apps/web/tests/snapshots/queue-actions/layout.expected.md @@ -36,6 +36,8 @@ - img - button "2 queued messages" - textbox "Cmd/Ctrl+Enter steers all queued messages" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/queue-actions/preserved.expected.md b/apps/web/tests/snapshots/queue-actions/preserved.expected.md index 2c76a52704..512a0c2ba7 100644 --- a/apps/web/tests/snapshots/queue-actions/preserved.expected.md +++ b/apps/web/tests/snapshots/queue-actions/preserved.expected.md @@ -53,6 +53,8 @@ - button "Steer queued message" [disabled]: - img - textbox "Message the agent" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/queue-actions/ui.expected.md b/apps/web/tests/snapshots/queue-actions/ui.expected.md index 8890500567..40b4f386be 100644 --- a/apps/web/tests/snapshots/queue-actions/ui.expected.md +++ b/apps/web/tests/snapshots/queue-actions/ui.expected.md @@ -32,6 +32,8 @@ - button "Steer queued message": - img - textbox "Cmd/Ctrl+Enter steers all queued messages" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/reference-composer/order.expected.md b/apps/web/tests/snapshots/reference-composer/order.expected.md index 2ab961c277..8b06e38c7c 100644 --- a/apps/web/tests/snapshots/reference-composer/order.expected.md +++ b/apps/web/tests/snapshots/reference-composer/order.expected.md @@ -18,6 +18,8 @@ - img - text: Session recall Research notes - textbox "Message the agent" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/seeded-history/command-row.expected.md b/apps/web/tests/snapshots/seeded-history/command-row.expected.md index 91933249bd..700571b401 100644 --- a/apps/web/tests/snapshots/seeded-history/command-row.expected.md +++ b/apps/web/tests/snapshots/seeded-history/command-row.expected.md @@ -52,6 +52,8 @@ - img - text: permission preset read-only - textbox "Message the agent" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Read Only"': Read Only diff --git a/apps/web/tests/snapshots/seeded-history/feedback-row.expected.md b/apps/web/tests/snapshots/seeded-history/feedback-row.expected.md index a45c9a36bc..37fa94990d 100644 --- a/apps/web/tests/snapshots/seeded-history/feedback-row.expected.md +++ b/apps/web/tests/snapshots/seeded-history/feedback-row.expected.md @@ -56,6 +56,8 @@ - text: "feedback Feedback recorded for session {{seededId}} Anonymous user: {{uuid}}. Session sharing is not configured." - text: "Feedback recorded for session {{seededId}} Anonymous user: {{uuid}}. Session sharing is not configured." - textbox "Message the agent" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Read Only"': Read Only diff --git a/apps/web/tests/snapshots/seeded-history/ui.expected.md b/apps/web/tests/snapshots/seeded-history/ui.expected.md index 69c68665bc..59537d1a90 100644 --- a/apps/web/tests/snapshots/seeded-history/ui.expected.md +++ b/apps/web/tests/snapshots/seeded-history/ui.expected.md @@ -50,6 +50,8 @@ - img - text: Context injection AGENTS.md - textbox "Message the agent" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/skill-tool-row/ui.expected.md b/apps/web/tests/snapshots/skill-tool-row/ui.expected.md index 8fa0bd0d6b..f1f9d120d2 100644 --- a/apps/web/tests/snapshots/skill-tool-row/ui.expected.md +++ b/apps/web/tests/snapshots/skill-tool-row/ui.expected.md @@ -48,6 +48,8 @@ - img - text: {{date}} {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s - textbox "Message the agent" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/skill-user-invoke/ui.expected.md b/apps/web/tests/snapshots/skill-user-invoke/ui.expected.md index 4826abc84a..528cdb653a 100644 --- a/apps/web/tests/snapshots/skill-user-invoke/ui.expected.md +++ b/apps/web/tests/snapshots/skill-user-invoke/ui.expected.md @@ -37,6 +37,8 @@ - img - text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s - textbox "Message the agent" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/stats-paged-history/ui.expected.md b/apps/web/tests/snapshots/stats-paged-history/ui.expected.md index 39adba012e..28b4f571fa 100644 --- a/apps/web/tests/snapshots/stats-paged-history/ui.expected.md +++ b/apps/web/tests/snapshots/stats-paged-history/ui.expected.md @@ -461,6 +461,8 @@ - button "Back to bottom": - img - textbox "Message the agent" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/steer-all/mid-steer.expected.md b/apps/web/tests/snapshots/steer-all/mid-steer.expected.md index 32b4a6afab..ec1ae855dd 100644 --- a/apps/web/tests/snapshots/steer-all/mid-steer.expected.md +++ b/apps/web/tests/snapshots/steer-all/mid-steer.expected.md @@ -31,6 +31,8 @@ - button "Copy": - img - textbox "Message the agent" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/steer-all/settled.expected.md b/apps/web/tests/snapshots/steer-all/settled.expected.md index 6b4911146e..a5b7c81c1c 100644 --- a/apps/web/tests/snapshots/steer-all/settled.expected.md +++ b/apps/web/tests/snapshots/steer-all/settled.expected.md @@ -51,6 +51,8 @@ - img - text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s - textbox "Message the agent" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/steering/settled.expected.md b/apps/web/tests/snapshots/steering/settled.expected.md index 738c95eb43..3931a3f31b 100644 --- a/apps/web/tests/snapshots/steering/settled.expected.md +++ b/apps/web/tests/snapshots/steering/settled.expected.md @@ -50,6 +50,8 @@ - img - text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s - textbox "Message the agent" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/subagent-conversation/ui.expected.md b/apps/web/tests/snapshots/subagent-conversation/ui.expected.md index f1e9e97862..73690e6366 100644 --- a/apps/web/tests/snapshots/subagent-conversation/ui.expected.md +++ b/apps/web/tests/snapshots/subagent-conversation/ui.expected.md @@ -62,6 +62,8 @@ - img - text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s - textbox "Message the agent" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Custom"': Custom diff --git a/apps/web/tests/snapshots/subagent-interrupt/offline-composer.expected.md b/apps/web/tests/snapshots/subagent-interrupt/offline-composer.expected.md index a6b3c098f3..c0fcaa654f 100644 --- a/apps/web/tests/snapshots/subagent-interrupt/offline-composer.expected.md +++ b/apps/web/tests/snapshots/subagent-interrupt/offline-composer.expected.md @@ -24,6 +24,8 @@ - paragraph: partial - status: Deep diving... - textbox "Parent session offline; sending is unavailable but you can still stop the run" [disabled] +- button "Add files" [disabled]: + - img - button "Commands" [disabled]: - img - 'button "Access mode, current: Custom" [disabled]': Custom diff --git a/apps/web/tests/snapshots/turn-tail-actions/running.expected.md b/apps/web/tests/snapshots/turn-tail-actions/running.expected.md index ab5f0de460..d1a5de0b70 100644 --- a/apps/web/tests/snapshots/turn-tail-actions/running.expected.md +++ b/apps/web/tests/snapshots/turn-tail-actions/running.expected.md @@ -32,6 +32,8 @@ - paragraph: partial - status: Deep diving... - textbox "Message the agent" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/turn-tail-actions/settled.expected.md b/apps/web/tests/snapshots/turn-tail-actions/settled.expected.md index 27b663bf54..2e108e68f0 100644 --- a/apps/web/tests/snapshots/turn-tail-actions/settled.expected.md +++ b/apps/web/tests/snapshots/turn-tail-actions/settled.expected.md @@ -44,6 +44,8 @@ - img - text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s - textbox "Message the agent" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/snapshots/web-search-round/ui.expected.md b/apps/web/tests/snapshots/web-search-round/ui.expected.md index 6164713327..aa26953f7e 100644 --- a/apps/web/tests/snapshots/web-search-round/ui.expected.md +++ b/apps/web/tests/snapshots/web-search-round/ui.expected.md @@ -37,6 +37,8 @@ - img - text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s - textbox "Message the agent" +- button "Add files": + - img - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/docs/capability-seams.i18n.yaml b/docs/capability-seams.i18n.yaml index 046d0db9da..f40844aff3 100644 --- a/docs/capability-seams.i18n.yaml +++ b/docs/capability-seams.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/capability-seams.md -capability-seams.md: e70867e1905bef2dcb0193a7ef86b06c5b262d5a -capability-seams.zh.md: a291e7bffcc1f1a5981d7c9768a681d26be98a7e +capability-seams.md: 872f966a04af4a19b7af775948f8b0c322baf852 +capability-seams.zh.md: 9bac3e35cddb4099891148ac3f2edba271adc2cc diff --git a/docs/capability-seams.md b/docs/capability-seams.md index e70867e190..872f966a04 100644 --- a/docs/capability-seams.md +++ b/docs/capability-seams.md @@ -183,6 +183,9 @@ flowchart LR svc_directoryPicker["ctx.directoryPicker
Workspace-directory picking seam"] pkg_directory_picker_native["directory-picker-native"] pkg_directory_picker_browse["directory-picker-browse"] + pkg_file_picker["file-picker"] + svc_filePicker["ctx.filePicker
File picking seam"] + pkg_file_picker_native["file-picker-native"] pkg_webserver["webserver"] svc_webServer["ctx.webServer
HTTP route registration"] pkg_connection["connection"] @@ -229,6 +232,8 @@ flowchart LR pkg_directory_picker_browse --> svc_directoryPicker pkg_directory_picker_native --> svc_directoryPicker pkg_e2b --> svc_e2b + pkg_file_picker --> svc_filePicker + pkg_file_picker_native --> svc_filePicker pkg_file_reference --> svc_fileReferences pkg_file_reference_local --> svc_fileReferences pkg_fs --> svc_fs @@ -330,6 +335,7 @@ flowchart LR svc_dynamicCordisRunner --> pkg_tool_cordis svc_e2b --> pkg_fs_e2b svc_e2b --> pkg_subprocess_e2b + svc_filePicker --> pkg_apiproxy svc_fs --> pkg_tool_fs svc_invariants --> pkg_agent svc_invariants --> pkg_agent_loop @@ -475,6 +481,7 @@ flowchart LR | `ctx.web` | `seam` | [`web`](../packages/web/web) | [`web-search-exa`](../packages/web/web-search-exa), [`web-search-perplexity`](../packages/web/web-search-perplexity), [`web-search-deepseek`](../packages/web/web-search-deepseek), [`web-fetch-http`](../packages/web/web-fetch-http) | [`tool-web`](../packages/web/tool-web) | - | Search and fetch providers register into one ctx.web seam; tool-web owns the stable model-facing names. | | `ctx.spillStore` | `seam` | [`spill`](../packages/spill/spill) | [`spill-local`](../packages/spill/spill-local) | [`spill-policy`](../packages/spill/spill-policy) | - | The backend saves oversized tool text and returns a model-facing locator plus retrieval hint; spill-policy is the tools/post-execute consumer that decides when to spill. | | `ctx.directoryPicker` | `seam` | `directory-picker` | `directory-picker-native`, `directory-picker-browse` | `apiproxy` | - | Discriminated interaction capability: the native backend opens one OS chooser on the host display, the browse backend serves listing/creation primitives for the in-app browser; dual-face backends fill ui-workspace directory-flow slots from their browser halves (no wire advertisement). | +| `ctx.filePicker` | `seam` | `file-picker` | `file-picker-native` | `apiproxy` | - | Native-only picking seam: the native backend opens one OS chooser on the host display and returns selected absolute paths without staging bytes; the basename location helper (./locate) walks the workspace tree to resolve a dragged file name, and apiproxy serves both through the host RPC surface. | | `ctx.webServer` | `core` | `webserver` | - | `connection`, `modules`, `hmr` | - | Plain node:http carrier: named-route registry, index transform taps, and the static dist fallback; web-transport plugins register their own routes. | | `ctx.clientModules` | `core` | `modules` | - | `hmr` | - | Composes the __DSH_BOOT__ entry graph from an incremental dsh.client scan, serves plugin bundles, and notifies rebuilt/graph-changed subscribers. | | `ctx.workflowEngine` | `seam` | [`workflow`](../packages/workflow/workflow) | [`workflow-worker-thread`](../packages/workflow/workflow-worker-thread) | [`tool-workflow`](../packages/workflow/tool-workflow), [`tool-ralph`](../packages/workflow/tool-ralph) | - | One engine per context, as in bash, with no named-provider registry; the general workflow and fixed Ralph consumers start runs whose agent() calls fan out through ctx.subagents. | diff --git a/docs/capability-seams.zh.md b/docs/capability-seams.zh.md index a291e7bffc..9bac3e35cd 100644 --- a/docs/capability-seams.zh.md +++ b/docs/capability-seams.zh.md @@ -185,6 +185,9 @@ flowchart LR svc_directoryPicker["ctx.directoryPicker
Workspace-directory picking seam"] pkg_directory_picker_native["directory-picker-native"] pkg_directory_picker_browse["directory-picker-browse"] + pkg_file_picker["file-picker"] + svc_filePicker["ctx.filePicker
File picking seam"] + pkg_file_picker_native["file-picker-native"] pkg_webserver["webserver"] svc_webServer["ctx.webServer
HTTP route registration"] pkg_connection["connection"] @@ -231,6 +234,8 @@ flowchart LR pkg_directory_picker_browse --> svc_directoryPicker pkg_directory_picker_native --> svc_directoryPicker pkg_e2b --> svc_e2b + pkg_file_picker --> svc_filePicker + pkg_file_picker_native --> svc_filePicker pkg_file_reference --> svc_fileReferences pkg_file_reference_local --> svc_fileReferences pkg_fs --> svc_fs @@ -332,6 +337,7 @@ flowchart LR svc_dynamicCordisRunner --> pkg_tool_cordis svc_e2b --> pkg_fs_e2b svc_e2b --> pkg_subprocess_e2b + svc_filePicker --> pkg_apiproxy svc_fs --> pkg_tool_fs svc_invariants --> pkg_agent svc_invariants --> pkg_agent_loop @@ -477,6 +483,7 @@ flowchart LR | `ctx.web` | `seam` | [`web`](../packages/web/web) | [`web-search-exa`](../packages/web/web-search-exa), [`web-search-perplexity`](../packages/web/web-search-perplexity), [`web-search-deepseek`](../packages/web/web-search-deepseek), [`web-fetch-http`](../packages/web/web-fetch-http) | [`tool-web`](../packages/web/tool-web) | - | 搜索和抓取提供方注册到同一个 ctx.web seam;tool-web 负责稳定的面向模型名称。 | | `ctx.spillStore` | `seam` | [`spill`](../packages/spill/spill) | [`spill-local`](../packages/spill/spill-local) | [`spill-policy`](../packages/spill/spill-policy) | - | 后端保存过大的工具文本,并返回面向模型的定位信息和取回提示;spill-policy 是 tools/post-execute 消费方,负责决定何时 spill。 | | `ctx.directoryPicker` | `seam` | `directory-picker` | `directory-picker-native`, `directory-picker-browse` | `apiproxy` | - | 带判别标记的交互能力:原生后端在 Host 显示设备上打开一个操作系统选择器,浏览后端为应用内浏览器提供列表与创建原语;双端后端通过其浏览器侧填充 ui-workspace 目录流程的 slot(不通过协议发布)。 | +| `ctx.filePicker` | `seam` | `file-picker` | `file-picker-native` | `apiproxy` | - | 仅原生拾取缝隙:原生后端在 Host 显示设备上打开一个操作系统选择器并返回选中的绝对路径而不暂存字节;basename 定位辅助(./locate)遍历工作区树以解析拖拽的文件名,apiproxy 通过主机 RPC 面为两者提供服务。 | | `ctx.webServer` | `core` | `webserver` | - | `connection`, `modules`, `hmr` | - | 普通的 node:http 载体:具名路由注册表、索引转换 tap,以及静态 dist 回退;Web 传输插件注册自己的路由。 | | `ctx.clientModules` | `core` | `modules` | - | `hmr` | - | 通过增量 `dsh.client` 扫描组合 __DSH_BOOT__ 入口图,提供插件组合包,并通知重建/图变更订阅方。 | | `ctx.workflowEngine` | `seam` | [`workflow`](../packages/workflow/workflow) | [`workflow-worker-thread`](../packages/workflow/workflow-worker-thread) | [`tool-workflow`](../packages/workflow/tool-workflow), [`tool-ralph`](../packages/workflow/tool-ralph) | - | 每个上下文使用一个引擎,与 bash 相同,且没有具名提供方注册表;通用工作流与固定 Ralph 消费方启动运行,其中的 agent() 调用通过 ctx.subagents 扇出。 | diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index ec2cc08acd..1b6d6520f8 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/config-catalog.md -config-catalog.md: df24e8fe3db04c65fc5ff94c5e9dd94cce36e1c9 -config-catalog.zh.md: a88656016368d36e648bae797e4eaa7a3f9d65c8 +config-catalog.md: 38343037706c107c60f1d9fc2ecc3d3338b7e901 +config-catalog.zh.md: a668bfa870b8d5a128769312c18e80c323252814 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index df24e8fe3d..3834303770 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -795,7 +795,7 @@ Source: [`packages/hooks/hooks-codex/src/index.ts:44`](../packages/hooks/hooks-c ## `@deepseek-ai/dsh-host-apiproxy` -Requires: `agentDefaultModel` · `agents` · `attachments` · `directoryPicker` · `llm` · `sessions` · `subagents` · `sessionPersistence` · `sessionQuery` · `tools` · `userQuestions` · `workspaceRegistry` +Requires: `agentDefaultModel` · `agents` · `attachments` · `directoryPicker` · `filePicker` · `llm` · `sessions` · `subagents` · `sessionPersistence` · `sessionQuery` · `tools` · `userQuestions` · `workspaceRegistry` ```ts config-catalog /** Gateway plugin configuration. */ @@ -3415,6 +3415,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co - `@deepseek-ai/dsh-goal-round-driver` — requires `agents` · `goals` · `sessions` ([`packages/goal/goal-round-driver/src/index.ts`](../packages/goal/goal-round-driver/src/index.ts)) - `@deepseek-ai/dsh-host-directory-picker-auto` — requires `webServer` · `loader` ([`packages/host/directory-picker-auto/src/index.ts`](../packages/host/directory-picker-auto/src/index.ts)) - `@deepseek-ai/dsh-host-directory-picker-native` ([`packages/host/directory-picker-native/src/index.ts`](../packages/host/directory-picker-native/src/index.ts)) +- `@deepseek-ai/dsh-host-file-picker-native` ([`packages/host/file-picker-native/src/index.ts`](../packages/host/file-picker-native/src/index.ts)) - `@deepseek-ai/dsh-host-plugin-inventory` — requires `loader` ([`packages/host/plugin-inventory/src/index.ts`](../packages/host/plugin-inventory/src/index.ts)) - `@deepseek-ai/dsh-llm` ([`packages/llm/llm/src/index.ts`](../packages/llm/llm/src/index.ts)) - `@deepseek-ai/dsh-lsp` ([`packages/lsp/lsp/src/index.ts`](../packages/lsp/lsp/src/index.ts)) @@ -3447,6 +3448,7 @@ Abstract service classes — a deployment loads a concrete implementation packag - `@deepseek-ai/dsh-file-reference` — abstract `FileReferenceService` ([`packages/context/file-reference/src/index.ts`](../packages/context/file-reference/src/index.ts)) - `@deepseek-ai/dsh-fs` — abstract `FileSystem` ([`packages/fs/fs/src/index.ts`](../packages/fs/fs/src/index.ts)) - `@deepseek-ai/dsh-host-directory-picker` — abstract `DirectoryPicker` ([`packages/host/directory-picker/src/index.ts`](../packages/host/directory-picker/src/index.ts)) +- `@deepseek-ai/dsh-host-file-picker` — abstract `FilePicker` ([`packages/host/file-picker/src/index.ts`](../packages/host/file-picker/src/index.ts)) - `@deepseek-ai/dsh-jobs` — abstract `JobRegistry` ([`packages/jobs/jobs/src/index.ts`](../packages/jobs/jobs/src/index.ts)) - `@deepseek-ai/dsh-sandbox` — abstract `SandboxProvider` ([`packages/sandbox/sandbox/src/index.ts`](../packages/sandbox/sandbox/src/index.ts)) - `@deepseek-ai/dsh-session-persistence` — abstract `SessionPersistence` ([`packages/session/session-persistence/src/index.ts`](../packages/session/session-persistence/src/index.ts)) diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index a886560163..a668bfa870 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -797,7 +797,7 @@ export interface Config { ## `@deepseek-ai/dsh-host-apiproxy` -需要:`agentDefaultModel` · `agents` · `attachments` · `directoryPicker` · `llm` · `sessions` · `subagents` · `sessionPersistence` · `sessionQuery` · `tools` · `userQuestions` · `workspaceRegistry` +需要:`agentDefaultModel` · `agents` · `attachments` · `directoryPicker` · `filePicker` · `llm` · `sessions` · `subagents` · `sessionPersistence` · `sessionQuery` · `tools` · `userQuestions` · `workspaceRegistry` ```ts config-catalog /** Gateway plugin configuration. */ @@ -3417,6 +3417,7 @@ export interface Config { - `@deepseek-ai/dsh-goal-round-driver` — 需要 `agents` · `goals` · `sessions`([`packages/goal/goal-round-driver/src/index.ts`](../packages/goal/goal-round-driver/src/index.ts)) - `@deepseek-ai/dsh-host-directory-picker-auto` — 需要 `webServer` · `loader`([`packages/host/directory-picker-auto/src/index.ts`](../packages/host/directory-picker-auto/src/index.ts)) - `@deepseek-ai/dsh-host-directory-picker-native`([`packages/host/directory-picker-native/src/index.ts`](../packages/host/directory-picker-native/src/index.ts)) +- `@deepseek-ai/dsh-host-file-picker-native`([`packages/host/file-picker-native/src/index.ts`](../packages/host/file-picker-native/src/index.ts)) - `@deepseek-ai/dsh-host-plugin-inventory` — 需要 `loader`([`packages/host/plugin-inventory/src/index.ts`](../packages/host/plugin-inventory/src/index.ts)) - `@deepseek-ai/dsh-llm`([`packages/llm/llm/src/index.ts`](../packages/llm/llm/src/index.ts)) - `@deepseek-ai/dsh-lsp`([`packages/lsp/lsp/src/index.ts`](../packages/lsp/lsp/src/index.ts)) @@ -3449,6 +3450,7 @@ export interface Config { - `@deepseek-ai/dsh-file-reference` — 抽象 `FileReferenceService`([`packages/context/file-reference/src/index.ts`](../packages/context/file-reference/src/index.ts)) - `@deepseek-ai/dsh-fs` — 抽象 `FileSystem`([`packages/fs/fs/src/index.ts`](../packages/fs/fs/src/index.ts)) - `@deepseek-ai/dsh-host-directory-picker` — 抽象 `DirectoryPicker`([`packages/host/directory-picker/src/index.ts`](../packages/host/directory-picker/src/index.ts)) +- `@deepseek-ai/dsh-host-file-picker` — 抽象 `FilePicker`([`packages/host/file-picker/src/index.ts`](../packages/host/file-picker/src/index.ts)) - `@deepseek-ai/dsh-jobs` — 抽象 `JobRegistry`([`packages/jobs/jobs/src/index.ts`](../packages/jobs/jobs/src/index.ts)) - `@deepseek-ai/dsh-sandbox` — 抽象 `SandboxProvider`([`packages/sandbox/sandbox/src/index.ts`](../packages/sandbox/sandbox/src/index.ts)) - `@deepseek-ai/dsh-session-persistence` — 抽象 `SessionPersistence`([`packages/session/session-persistence/src/index.ts`](../packages/session/session-persistence/src/index.ts)) diff --git a/docs/module-graph.i18n.yaml b/docs/module-graph.i18n.yaml index eb89f08d43..1907b48b6c 100644 --- a/docs/module-graph.i18n.yaml +++ b/docs/module-graph.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/module-graph.md -module-graph.md: 7e41da16634c8ce2d75320dbac0864b6cc30cab8 -module-graph.zh.md: 9842e74a03d1b3e16c16b66152a6bccb90c5a9db +module-graph.md: d5a1a2f7701ad900e3746ad20bff8951553c9cd8 +module-graph.zh.md: 142f9d5abc2d62684262b14c5c51c480c6b46a8b diff --git a/docs/module-graph.md b/docs/module-graph.md index 7e41da1663..d5a1a2f770 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -222,6 +222,8 @@ flowchart TD pkg_host_directory_picker_auto["host-directory-picker-auto"] pkg_host_directory_picker_browse["host-directory-picker-browse"] pkg_host_directory_picker_native["host-directory-picker-native"] + pkg_host_file_picker["host-file-picker"] + pkg_host_file_picker_native["host-file-picker-native"] pkg_host_frontend_static["host-frontend-static"] pkg_host_plugin_control["host-plugin-control"] pkg_host_plugin_installer["host-plugin-installer"] @@ -366,6 +368,8 @@ flowchart TD pkg_host_directory_picker --> pkg_invariants pkg_host_directory_picker_browse --> pkg_invariants pkg_host_directory_picker_native --> pkg_invariants + pkg_host_file_picker --> pkg_invariants + pkg_host_file_picker_native --> pkg_invariants pkg_host_webserver --> pkg_invariants pkg_sandbox_windows_acl --> pkg_invariants pkg_storage --> pkg_invariants @@ -1339,6 +1343,7 @@ flowchart TD pkg_client_ui_conversation --> pkg_client_ui_settings pkg_client_ui_conversation --> pkg_commands pkg_client_ui_conversation --> pkg_compaction + pkg_client_ui_conversation --> pkg_file_reference pkg_client_ui_conversation --> pkg_goal pkg_client_ui_conversation --> pkg_invariants pkg_client_ui_conversation --> pkg_llm_retry @@ -1538,6 +1543,8 @@ flowchart TD | [`host-directory-picker`](../packages/host/directory-picker) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`host-directory-picker-native`](../packages/host/directory-picker-native) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) | +| [`host-file-picker`](../packages/host/file-picker) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) | +| [`host-file-picker-native`](../packages/host/file-picker-native) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`host-webserver`](../packages/host/webserver) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl) | `sandbox` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`storage`](../packages/storage/storage) | `storage` | [`invariants`](../packages/runtime-diagnostics/invariants) | @@ -1724,7 +1731,7 @@ flowchart TD | [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`client-ui-reference`](../packages/client/ui-reference) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants), [`session-reference`](../packages/context/session-reference), [`typert-protocol`](../packages/typert/protocol) | | [`cordis-client-runner`](../packages/extensions/cordis-client-runner) | `extensions` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/invariants) | -| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`agent`](../packages/core/agent), [`api-remotes`](../packages/api/remotes), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-layout`](../packages/client/ui-layout), [`client-ui-settings`](../packages/client/ui-settings), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`session-stats`](../packages/session/session-stats), [`settings`](../packages/settings/settings), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tools`](../packages/core/tools) | +| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`agent`](../packages/core/agent), [`api-remotes`](../packages/api/remotes), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-layout`](../packages/client/ui-layout), [`client-ui-settings`](../packages/client/ui-settings), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`file-reference`](../packages/context/file-reference), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`session-stats`](../packages/session/session-stats), [`settings`](../packages/settings/settings), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tools`](../packages/core/tools) | | [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-layout`](../packages/client/ui-layout), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`client-ui-agent-preset`](../packages/client/ui-agent-preset) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`client-ui-attachment`](../packages/client/ui-attachment) | `client` | [`attachment`](../packages/attachment/attachment), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`invariants`](../packages/runtime-diagnostics/invariants) | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index 9842e74a03..142f9d5abc 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -224,6 +224,8 @@ flowchart TD pkg_host_directory_picker_auto["host-directory-picker-auto"] pkg_host_directory_picker_browse["host-directory-picker-browse"] pkg_host_directory_picker_native["host-directory-picker-native"] + pkg_host_file_picker["host-file-picker"] + pkg_host_file_picker_native["host-file-picker-native"] pkg_host_frontend_static["host-frontend-static"] pkg_host_plugin_control["host-plugin-control"] pkg_host_plugin_installer["host-plugin-installer"] @@ -368,6 +370,8 @@ flowchart TD pkg_host_directory_picker --> pkg_invariants pkg_host_directory_picker_browse --> pkg_invariants pkg_host_directory_picker_native --> pkg_invariants + pkg_host_file_picker --> pkg_invariants + pkg_host_file_picker_native --> pkg_invariants pkg_host_webserver --> pkg_invariants pkg_sandbox_windows_acl --> pkg_invariants pkg_storage --> pkg_invariants @@ -1341,6 +1345,7 @@ flowchart TD pkg_client_ui_conversation --> pkg_client_ui_settings pkg_client_ui_conversation --> pkg_commands pkg_client_ui_conversation --> pkg_compaction + pkg_client_ui_conversation --> pkg_file_reference pkg_client_ui_conversation --> pkg_goal pkg_client_ui_conversation --> pkg_invariants pkg_client_ui_conversation --> pkg_llm_retry @@ -1540,6 +1545,8 @@ flowchart TD | [`host-directory-picker`](../packages/host/directory-picker) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`host-directory-picker-native`](../packages/host/directory-picker-native) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) | +| [`host-file-picker`](../packages/host/file-picker) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) | +| [`host-file-picker-native`](../packages/host/file-picker-native) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`host-webserver`](../packages/host/webserver) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl) | `sandbox` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`storage`](../packages/storage/storage) | `storage` | [`invariants`](../packages/runtime-diagnostics/invariants) | @@ -1726,7 +1733,7 @@ flowchart TD | [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`client-ui-reference`](../packages/client/ui-reference) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants), [`session-reference`](../packages/context/session-reference), [`typert-protocol`](../packages/typert/protocol) | | [`cordis-client-runner`](../packages/extensions/cordis-client-runner) | `extensions` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/invariants) | -| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`agent`](../packages/core/agent), [`api-remotes`](../packages/api/remotes), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-layout`](../packages/client/ui-layout), [`client-ui-settings`](../packages/client/ui-settings), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`session-stats`](../packages/session/session-stats), [`settings`](../packages/settings/settings), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tools`](../packages/core/tools) | +| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`agent`](../packages/core/agent), [`api-remotes`](../packages/api/remotes), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-layout`](../packages/client/ui-layout), [`client-ui-settings`](../packages/client/ui-settings), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`file-reference`](../packages/context/file-reference), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`session-stats`](../packages/session/session-stats), [`settings`](../packages/settings/settings), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tools`](../packages/core/tools) | | [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-layout`](../packages/client/ui-layout), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`client-ui-agent-preset`](../packages/client/ui-agent-preset) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`client-ui-attachment`](../packages/client/ui-attachment) | `client` | [`attachment`](../packages/attachment/attachment), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`invariants`](../packages/runtime-diagnostics/invariants) | diff --git a/docs/subsystems/workspace.i18n.yaml b/docs/subsystems/workspace.i18n.yaml index 3d989ae4cf..51584ef162 100644 --- a/docs/subsystems/workspace.i18n.yaml +++ b/docs/subsystems/workspace.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/workspace.md -workspace.md: 26d1cbe87fed65fc963a3eab167895a5174cca78 -workspace.zh.md: 3e2a25ad2f7e047b51911dcd2ab0ce3b774fd540 +workspace.md: ce1ee58782fe0ea999f078304f7fa8c4915b2b86 +workspace.zh.md: d71dfed204baf02353c1e472964ef187d49b6690 diff --git a/docs/subsystems/workspace.md b/docs/subsystems/workspace.md index 26d1cbe87f..ce1ee58782 100644 --- a/docs/subsystems/workspace.md +++ b/docs/subsystems/workspace.md @@ -149,6 +149,22 @@ abstract capability(): DirectoryPickerCapability Source: [`packages/host/directory-picker/src/index.ts`](../../packages/host/directory-picker/src/index.ts) + + +### `ctx.filePicker` — `FilePicker` (abstract seam) + +Abstract file-picking service. Subclass, implement `capability()`, and load the subclass as a plugin — it registers as `ctx.filePicker` (one implementation per context). The capability object must be stable for the service lifetime: consumers may capture it across calls. + +```ts cordis-catalog +/** + * The backend's interaction capability. + * @returns the discriminated capability consumers switch on. + */ +abstract capability(): FilePickerCapability +``` + +Source: [`packages/host/file-picker/src/index.ts`](../../packages/host/file-picker/src/index.ts) + ### `ctx.workspaceRegistry` — `WorkspaceRegistry` diff --git a/docs/subsystems/workspace.zh.md b/docs/subsystems/workspace.zh.md index 3e2a25ad2f..d71dfed204 100644 --- a/docs/subsystems/workspace.zh.md +++ b/docs/subsystems/workspace.zh.md @@ -149,6 +149,22 @@ abstract capability(): DirectoryPickerCapability Source: [`packages/host/directory-picker/src/index.ts`](../../packages/host/directory-picker/src/index.ts) + + +### `ctx.filePicker` — `FilePicker` (abstract seam) + +Abstract file-picking service. Subclass, implement `capability()`, and load the subclass as a plugin — it registers as `ctx.filePicker` (one implementation per context). The capability object must be stable for the service lifetime: consumers may capture it across calls. + +```ts cordis-catalog +/** + * The backend's interaction capability. + * @returns the discriminated capability consumers switch on. + */ +abstract capability(): FilePickerCapability +``` + +Source: [`packages/host/file-picker/src/index.ts`](../../packages/host/file-picker/src/index.ts) + ### `ctx.workspaceRegistry` — `WorkspaceRegistry` diff --git a/packages/bundle/web-app/cordis.patch.yml b/packages/bundle/web-app/cordis.patch.yml index 12f4894932..e7d6cbe62e 100644 --- a/packages/bundle/web-app/cordis.patch.yml +++ b/packages/bundle/web-app/cordis.patch.yml @@ -97,6 +97,11 @@ - id: directory-picker name: '@deepseek-ai/dsh-host-directory-picker-auto' + # Native-only multi-file chooser backing host.pickFiles (no remote browse + # twin: a remote client has no display to open a file chooser on). + - id: file-picker + name: '@deepseek-ai/dsh-host-file-picker-native' + # Read-only projection of current Loader entries for trusted client RPCs. - id: plugin-inventory name: '@deepseek-ai/dsh-host-plugin-inventory' diff --git a/packages/bundle/web-app/package.json b/packages/bundle/web-app/package.json index e507bceef1..0b3670f5a6 100644 --- a/packages/bundle/web-app/package.json +++ b/packages/bundle/web-app/package.json @@ -95,6 +95,8 @@ "@deepseek-ai/dsh-host-directory-picker-auto": "workspace:^", "@deepseek-ai/dsh-host-directory-picker-browse": "workspace:^", "@deepseek-ai/dsh-host-directory-picker-native": "workspace:^", + "@deepseek-ai/dsh-host-file-picker": "workspace:^", + "@deepseek-ai/dsh-host-file-picker-native": "workspace:^", "@deepseek-ai/dsh-host-plugin-inventory": "workspace:^", "@deepseek-ai/dsh-host-webserver": "workspace:^", "@deepseek-ai/dsh-file-reference": "workspace:^", diff --git a/packages/client/connection/src/client/fixture.ts b/packages/client/connection/src/client/fixture.ts index 1262ea572a..91c533fe93 100644 --- a/packages/client/connection/src/client/fixture.ts +++ b/packages/client/connection/src/client/fixture.ts @@ -2713,6 +2713,8 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld { return ok(request, { path: target }) }, openPath: request => ok(request, { opened: true as const }), + pickFiles: request => ok(request, { cancelled: false, paths: [`${FIXTURE_HOME}/Documents/upload.txt`] }), + locateFiles: request => ok(request, { items: request.payload.names.map(name => ({ name, paths: [] })) }), }, workspace: { list: request => ok(request, { @@ -3289,6 +3291,8 @@ export class FixtureApiClient extends AbstractApiClient { case 'host.listDirectory': return this.api.host.listDirectory(request, new AbortController().signal) case 'host.createDirectory': return this.api.host.createDirectory(request) case 'host.openPath': return this.api.host.openPath(request, new AbortController().signal) + case 'host.pickFiles': return this.api.host.pickFiles(request, new AbortController().signal) + case 'host.locateFiles': return this.api.host.locateFiles(request, new AbortController().signal) case 'workspace.list': return this.api.workspace.list(request) case 'workspace.create': return this.api.workspace.create(request) case 'workspace.rename': return this.api.workspace.rename(request) diff --git a/packages/client/connection/tests/fake-api.client.ts b/packages/client/connection/tests/fake-api.client.ts index 744e397477..7e5e468976 100644 --- a/packages/client/connection/tests/fake-api.client.ts +++ b/packages/client/connection/tests/fake-api.client.ts @@ -153,6 +153,8 @@ export class FakeApiClient implements IApiClient { listDirectory: payload => this.record('host.listDirectory', payload, this.onListDirectory(payload)), createDirectory: payload => this.record('host.createDirectory', payload, this.onCreateDirectory(payload)), openPath: payload => this.record('host.openPath', payload, this.onOpenPath(payload)), + pickFiles: payload => this.record('host.pickFiles', payload, Promise.resolve(ok({ cancelled: true, paths: [] }))), + locateFiles: payload => this.record('host.locateFiles', payload, Promise.resolve(ok({ items: payload.names.map(name => ({ name, paths: [] })) }))), } readonly workspace: IApiClient['workspace'] = { diff --git a/packages/client/runtime/src/client/contract/workspaces.ts b/packages/client/runtime/src/client/contract/workspaces.ts index b70a8c563f..80c2a75652 100644 --- a/packages/client/runtime/src/client/contract/workspaces.ts +++ b/packages/client/runtime/src/client/contract/workspaces.ts @@ -39,6 +39,19 @@ export interface IWorkspaces { * @returns the selected path, or null when the user cancelled. */ pickDirectory(): Promise + /** + * Open the Host's native multi-file picker. + * @returns cancellation plus the selected absolute paths (empty on cancel). + */ + pickFiles(): Promise<{ cancelled: boolean; paths: string[] }> + /** + * Resolve dragged file basenames to absolute paths against the target + * session's workspace. + * @param sessionId - session whose workspace roots the exact-basename walk. + * @param names - dragged file basenames (leading directories are dropped). + * @returns one item per name, its `paths` empty when no deterministic match. + */ + locateFiles(sessionId: SessionId, names: string[]): Promise> /** * List one directory level through the Host's `browse` capability. * @param path - absolute directory to list; absent lists the Host home directory. diff --git a/packages/client/runtime/src/client/workspaces/service.ts b/packages/client/runtime/src/client/workspaces/service.ts index 58fdb55451..74632815a1 100644 --- a/packages/client/runtime/src/client/workspaces/service.ts +++ b/packages/client/runtime/src/client/workspaces/service.ts @@ -214,6 +214,33 @@ export class WorkspaceRuntime implements IWorkspaces { return response.result.value.path } + /** + * Open the Host's native multi-file picker. + * @returns cancellation plus the selected absolute paths (empty on cancel). + */ + async pickFiles(): Promise<{ cancelled: boolean; paths: string[] }> { + const response = await this.api.host.pickFiles({ multiple: true }) + if (!response.result.ok) { + throw new Error(`file picker failed: ${response.result.error.message}`) + } + return response.result.value + } + + /** + * Resolve dragged file basenames to absolute paths against the target + * session's workspace. + * @param sessionId - session whose workspace roots the exact-basename walk. + * @param names - dragged file basenames. + * @returns one item per name. + */ + async locateFiles(sessionId: SessionId, names: string[]): Promise> { + const response = await this.api.host.locateFiles({ sessionId, names }) + if (!response.result.ok) { + throw new Error(`file location failed: ${response.result.error.message}`) + } + return response.result.value.items + } + /** * List one directory level through the Host's `browse` capability. * @param path - absolute directory to list; absent lists the Host home directory. diff --git a/packages/client/runtime/tests/fake-api.client.ts b/packages/client/runtime/tests/fake-api.client.ts index 7927bc2a45..f48d8e71bd 100644 --- a/packages/client/runtime/tests/fake-api.client.ts +++ b/packages/client/runtime/tests/fake-api.client.ts @@ -187,6 +187,8 @@ export class FakeApiClient implements IApiClient { listDirectory: (payload: unknown) => this.record('host.listDirectory', payload, this.onListDirectory(payload)), createDirectory: (payload: unknown) => this.record('host.createDirectory', payload, this.onCreateDirectory(payload)), openPath: (payload: unknown) => this.record('host.openPath', payload, this.onOpenPath(payload)), + pickFiles: (payload: unknown) => this.record('host.pickFiles', payload, Promise.resolve(ok({ cancelled: true, paths: [] }))), + locateFiles: (payload: unknown) => this.record('host.locateFiles', payload, Promise.resolve(ok({ items: (payload as { names: string[] }).names.map(name => ({ name, paths: [] })) }))), } // The archive-set field defaults at the binding below so list stubs keep diff --git a/packages/client/ui-conversation/package.json b/packages/client/ui-conversation/package.json index 83538eb52f..0accd97a06 100644 --- a/packages/client/ui-conversation/package.json +++ b/packages/client/ui-conversation/package.json @@ -64,6 +64,7 @@ "@deepseek-ai/dsh-client-ui-input-trigger": "workspace:^", "@deepseek-ai/dsh-commands": "workspace:^", "@deepseek-ai/dsh-compaction": "workspace:^", + "@deepseek-ai/dsh-file-reference": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm-retry": "workspace:^", "@deepseek-ai/dsh-session-stats": "workspace:^", @@ -93,6 +94,7 @@ "@deepseek-ai/dsh-client-ui-slots": "workspace:^", "@deepseek-ai/dsh-commands": "workspace:^", "@deepseek-ai/dsh-compaction": "workspace:^", + "@deepseek-ai/dsh-file-reference": "workspace:^", "@deepseek-ai/dsh-goal": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm-retry": "workspace:^", diff --git a/packages/client/ui-conversation/src/client/apply.ts b/packages/client/ui-conversation/src/client/apply.ts index b4919f2fb4..4f61edbef2 100644 --- a/packages/client/ui-conversation/src/client/apply.ts +++ b/packages/client/ui-conversation/src/client/apply.ts @@ -296,6 +296,8 @@ export function apply(ctx: Context): void { addImages: undefined, removeImage: undefined, draftImages: undefined, + pickFiles: undefined, + locateFiles: undefined, resolveSubmitMode: (running, gesture, steeringAvailable) => submissionPolicy.resolve(running, gesture, steeringAvailable), toggleCommandMenu: undefined, @@ -330,6 +332,8 @@ export function apply(ctx: Context): void { shell.removeImage(id) }, draftImages: ids => conversation.draftImages(ids), + pickFiles: () => workspaces.pickFiles(), + locateFiles: names => workspaces.locateFiles(sessionId, names), resolveSubmitMode: (running, gesture, steeringAvailable) => submissionPolicy.resolve(running, gesture, steeringAvailable), toggleCommandMenu: inputTriggers === undefined diff --git a/packages/client/ui-conversation/src/client/contract/slots.ts b/packages/client/ui-conversation/src/client/contract/slots.ts index 8bfe9e4905..7ec78a7cb5 100644 --- a/packages/client/ui-conversation/src/client/contract/slots.ts +++ b/packages/client/ui-conversation/src/client/contract/slots.ts @@ -582,6 +582,10 @@ export interface ComposerBarInjected { removeImage: ((id: DraftAttachmentId) => void) | undefined /** Resolve ordered input ids to browser-owned draft images. */ draftImages: ((ids: readonly DraftAttachmentId[]) => readonly ComposerAttachment[]) | undefined + /** Open the host's native multi-file picker; resolved absolute paths are empty when the operator cancels. */ + pickFiles: (() => Promise<{ cancelled: boolean; paths: string[] }>) | undefined + /** Resolve dragged file basenames to absolute paths inside this session's workspace (empty when no match). */ + locateFiles: ((names: string[]) => Promise>) | undefined /** Resolve one keyboard submission gesture against the current running state and persisted preference. */ resolveSubmitMode: ( running: boolean, diff --git a/packages/client/ui-conversation/src/client/locales.ts b/packages/client/ui-conversation/src/client/locales.ts index ea727af7f9..7ed4ad9ef0 100644 --- a/packages/client/ui-conversation/src/client/locales.ts +++ b/packages/client/ui-conversation/src/client/locales.ts @@ -23,6 +23,8 @@ export const zh = { 'input.commands': '命令', 'input.stop': '停止生成', 'input.send': '发送消息', + 'input.addFiles': '添加文件', + 'file.locateFailed': '无法定位文件:{names},请点击「添加文件」按钮选择', 'placeholder.steerQueue': 'Cmd/Ctrl+Enter 插话发送全部排队消息', 'input.accessMode': '访问模式,当前:{name}', 'image.dropTitle': '图片拖动到此处即可添加', @@ -210,6 +212,8 @@ export const en = { 'input.commands': 'Commands', 'input.stop': 'Stop generating', 'input.send': 'Send message', + 'input.addFiles': 'Add files', + 'file.locateFailed': 'Cannot locate {names}; use the add-file button', 'placeholder.steerQueue': 'Cmd/Ctrl+Enter steers all queued messages', 'input.accessMode': 'Access mode, current: {name}', 'image.dropTitle': 'Drag images here to add them', diff --git a/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx b/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx index 4e5d526176..e291ae608b 100644 --- a/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx +++ b/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx @@ -10,7 +10,7 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } fr import type { ChangeEvent, KeyboardEvent, MouseEvent, ReactNode } from 'react' import clsx from 'clsx' import { - IconPlusOutline16, IconWarningOutline16, Toast, Tooltip, + IconPaperclipOutline16, IconPlusOutline16, IconWarningOutline16, Toast, Tooltip, } from '@deepseek-ai/dsh-client-ui-primitives' // Type-only: the `plan` projection key merge (the TodoDock posture — the // composer reads a host-computed value; the domain owns the key). @@ -26,6 +26,7 @@ import { deriveDecorations } from '../input/decorations.ts' import type { DraftDecorations } from '../input/decorations.ts' import type { EditRange } from '../input/contract.ts' import { attachmentErrorText, imageSizeText } from '../image-labels.ts' +import { formatFileMention } from '@deepseek-ai/dsh-file-reference/grammar' import { ReferenceIcon } from '../reference/ReferenceIcon.tsx' import { ContextMeter } from './ContextMeter.tsx' import { PermissionSelect } from './PermissionSelect.tsx' @@ -35,6 +36,21 @@ import css from './InputBar.module.css' /** Decoration product of the no-session state (no machine, empty draft). */ const INERT_DECORATIONS: DraftDecorations = { token: null, chips: [], textRefs: [], hint: null } +/** + * Classify one dropped/pasted file as an image (multi-modal intake) versus a + * plain file (basename-location intake). The `image/*` MIME family decides: + * an image type joins the image rail, where the rail's own admission keeps + * its authoritative "only PNG/JPG/WebP/GIF" refusal; everything else — any + * non-image MIME or an empty MIME — resolves to a `@path` mention. Routing on + * the MIME family keeps one rule that does not drift with a deployment's + * narrowed intake list. + * @param file - the browser file. + * @returns true when the file joins the image rail, false when it is path-located. + */ +function isSupportedImage(file: File): boolean { + return file.type.startsWith('image/') +} + /** The selection and edit family a `beforeinput` recorded, with the draft length it applied to. */ interface PendingEdit { readonly start: number @@ -78,6 +94,7 @@ export type InputBarProps = ComposerBarProps export function InputBar({ useSession, useInput, inputActions, keyboard, addImages, removeImage, draftImages, + pickFiles, locateFiles, resolveSubmitMode, toggleCommandMenu, stop, command, t, renderSlot, useNotices, useLexicon, useMenuLauncher, useProjection, sessionId, variant, disabled: inert = false, blocked, @@ -502,37 +519,96 @@ export function InputBar({ keyboard.track(keyboard.snapshot.draft, caret) } - // Intake pre-check (DeepSeek Chat semantics): an addition that would break - // a projected limit is refused as a whole batch, announced immediately, and - // never enters the rail — no more submit-time failure rolling the rail - // back. The host enforces the same limits at submit for callers that bypass - // this composer. + // The asynchronous file-location/native-pick arms write the draft after a + // network round trip, so the keyboard access rides a ref; the draft itself + // comes from the machine's synchronous snapshot (never the render closure, + // which can lag one frame behind the user's latest keystroke). + const liveKeyboard = useRef(keyboard) + liveKeyboard.current = keyboard + + // Insert `@path` mentions at the caret/selection, normalizing surrounding + // whitespace. Reads the live machine snapshot so a pick/location settled + // after the user kept typing edits the draft as-is; the editRange is + // omittable (mentions are plain text, and the machine diff-scans occurrence + // offsets itself), which avoids feeding coordinates measured on a stale draft. + const insertFileMentions = (mentions: readonly string[]): void => { + const currentKeyboard = liveKeyboard.current + if (currentKeyboard === undefined || mentions.length === 0) return + const currentDraft = currentKeyboard.snapshot.draft + const el = inputRef.current + const sel = el === null ? { start: currentDraft.length, end: currentDraft.length } : selectionOf(el) + const prefix = currentDraft.slice(0, sel.start) + const suffix = currentDraft.slice(sel.end) + const inserted = `${prefix !== '' && !/\s$/u.test(prefix) ? ' ' : ''}${mentions.join(' ')}${suffix !== '' && !/^\s/u.test(suffix) ? ' ' : ''}` + const next = prefix + inserted + suffix + currentKeyboard.setDraft(next) + const caret = sel.start + inserted.length + if (el !== null) restoreCaret(el, caret) + currentKeyboard.track(currentKeyboard.snapshot.draft, caret) + } + + // Intake pre-check (DeepSeek Chat semantics for images): an image batch that + // would break a projected limit is refused whole and announced immediately. + // Non-image files never enter the rail — their basenames resolve to `@path` + // mentions against the workspace, never staged bytes. const intakeImages = useCallback((files: readonly File[]): void => { - if (addImages === undefined || files.length === 0) return - const rejected = ((): string | null => { - if (imageLimits !== undefined) { - // Format precedes limits (DeepSeek Chat's filter order): a batch with - // a non-image must announce the format problem, not a count or size - // it could never pass anyway — addImages rejects it authoritatively. - if (files.some(file => !(imageLimits.mediaTypes as readonly string[]).includes(file.type))) { - return addImages(files) - } - if (attachments.length + files.length > imageLimits.maxImagesPerMessage) { - return t('image.tooMany', { count: imageLimits.maxImagesPerMessage }) - } - if (files.some(file => file.size > imageLimits.maxImageBytes)) { - return t('image.fileTooLarge', { size: imageSizeText(imageLimits.maxImageBytes) }) + if (files.length === 0 || addImages === undefined) return + const images: File[] = [] + const others: File[] = [] + for (const file of files) { + (isSupportedImage(file) ? images : others).push(file) + } + if (images.length > 0) { + const rejected = ((): string | null => { + if (imageLimits !== undefined) { + if (attachments.length + images.length > imageLimits.maxImagesPerMessage) { + return t('image.tooMany', { count: imageLimits.maxImagesPerMessage }) + } + if (images.some(file => file.size > imageLimits.maxImageBytes)) { + return t('image.fileTooLarge', { size: imageSizeText(imageLimits.maxImageBytes) }) + } + const total = attachments.reduce((sum, attachment) => sum + attachment.file.size, 0) + + images.reduce((sum, file) => sum + file.size, 0) + if (total > imageLimits.maxMessageImageBytes) { + return t('image.totalTooLarge', { size: imageSizeText(imageLimits.maxMessageImageBytes) }) + } } - const total = attachments.reduce((sum, attachment) => sum + attachment.file.size, 0) - + files.reduce((sum, file) => sum + file.size, 0) - if (total > imageLimits.maxMessageImageBytes) { - return t('image.totalTooLarge', { size: imageSizeText(imageLimits.maxMessageImageBytes) }) + return addImages(images) + })() + if (rejected !== null) showToast(rejected) + } + if (others.length > 0 && locateFiles !== undefined) { + void locateFiles(others.map(file => file.name)).then((items) => { + const mentions: string[] = [] + const ambiguous: string[] = [] + for (const item of items) { + if (item.paths.length === 1 && item.paths[0] !== undefined) { + const mention = formatFileMention({ path: item.paths[0], kind: 'file' }, false) + if (mention !== undefined) mentions.push(mention) + } else { + ambiguous.push(item.name) + } } - } - return addImages(files) - })() - if (rejected !== null) showToast(rejected) - }, [addImages, attachments, imageLimits, showToast, t]) + if (mentions.length > 0) insertFileMentions(mentions) + if (ambiguous.length > 0) showToast(t('file.locateFailed', { names: ambiguous.join(', ') })) + }, (error: unknown) => { showToast(error instanceof Error ? error.message : String(error)) }) + } + }, [addImages, attachments, imageLimits, showToast, t, locateFiles]) + + // The native multi-file picker: selected paths become `@path` mentions. + const onAddFiles = async (): Promise => { + if (pickFiles === undefined || locked || machineBusy) return + try { + const result = await pickFiles() + if (result.cancelled || result.paths.length === 0) return + const mentions = result.paths + .map(path => formatFileMention({ path, kind: 'file' }, false)) + .filter((mention): mention is string => mention !== undefined) + insertFileMentions(mentions) + } catch (error: unknown) { + showToast(error instanceof Error ? error.message : String(error)) + } + } const canAcceptDrop = !locked && !machineBusy && addImages !== undefined @@ -769,6 +845,18 @@ export function InputBar({
+ + +