diff --git a/.agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.i18n.yaml
index 617d6b07db..c316742ec1 100644
--- a/.agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.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 .agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.md
-2026-08-10-unary-apiproxy-remote-migration.md: 027376cbf772043cce21f487c94288cad6bece1c
-2026-08-10-unary-apiproxy-remote-migration.zh.md: f7507959a992dd17ca60883ada7769c7196fdc3a
+2026-08-10-unary-apiproxy-remote-migration.md: 11254099556113d921da502f0150522886a718f3
+2026-08-10-unary-apiproxy-remote-migration.zh.md: 50b303876863e992566f6ed6fb0bd0a89326344f
diff --git a/.agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.md b/.agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.md
index 027376cbf7..1125409955 100644
--- a/.agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.md
+++ b/.agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.md
@@ -8,7 +8,7 @@ English | [中文](2026-08-10-unary-apiproxy-remote-migration.zh.md)
The Host API Proxy duplicated simple unary operations across business Services, API Proxy interfaces, Zod schemas, route tables, client stubs, and Client callers. [Typert Remote calls](2026-08-02-typert-remote-method-calls.md) already let a business package own this class of call, but moving an endpoint without its lifecycle and projection policy could change observable behavior.
-Agent-bound calls require particular care. Shared lookup policy reuses live Agents, resumes ordinary cold Sessions with their recorded presets, deduplicates concurrent resumes, and rejects subagent-owned identities. Skill listing instead must inspect a Session without activating its Agent. Native desktop operations must keep the browser from choosing an arbitrary Host path.
+Agent-bound calls require particular care. Shared lookup policy reuses live Agents, resumes ordinary cold Sessions with their recorded presets, deduplicates concurrent resumes, and rejects subagent-owned identities. Skill listing instead must inspect a Session without activating its Agent. Settings and preset operations keep their Host-owned document paths out of browser requests; Session file links preserve their caller-resolved path behavior.
## Decision
@@ -30,11 +30,11 @@ Simple unary operations live on their natural business Remote owner. The busines
| `workspace.list`, `workspace.insertSessionBefore`, `workspace.archiveSession` | Equivalent `workspace/*` methods | The Workspace registry owns detached snapshots and serialized mutations. |
| `skill.list` | `skills/list` | `SessionSkillCatalog` observes the Session and its recorded preset, uses a live Agent only when one already exists, and never activates an Agent for listing. |
| `fileReferences/list` | `fileReferences/list` | `SessionFileReferences` supplies the Session Controller's established Agent lookup to the provider; cold lookup behavior remains unchanged. |
-| `host.openPath` | `session/openWorkspacePath` | `SessionController` resolves the path against the addressed Session's workspace before native opening. |
+| `host.openPath` | `session/openWorkspacePath` | The Session-aware Client resolves relative paths against the known workspace before `SessionController` hands them to the native opener. |
The shared Agent and Session resolver remains the authority for endpoints that accept those objects. It provides the same live reuse, cold restoration, concurrent deduplication, preset setup, persistence failures, and subagent ownership fence that legacy API Proxy calls used. `TypertLookupFailure` preserves resolver-owned RPC errors instead of collapsing them into `internal`.
-The native path implementation lives in `@deepseek-ai/dsh-native-command`. Session and Settings controllers select the target; the utility only performs platform detection, WSL translation, browser preference, text-editor intent, and shell-free command execution.
+The native path implementation lives in `@deepseek-ai/dsh-native-command`. Settings controllers select Host-owned targets, while Session-aware Clients resolve workspace paths before calling `SessionController`; the utility only performs platform detection, WSL translation, browser preference, text-editor intent, and shell-free command execution.
## Browser authentication
@@ -50,7 +50,7 @@ Focused Host and Client tests cover Remote calls, lookup and no-activation polic
**Move every unary operation.** Rejected because `host.describe` combines deployment facts and Connection readiness, while Session export is a streamed download rather than a unary business method.
-**Put native opening in one controller.** Rejected because Session, Settings, and the retained Host description consume the same platform operation. A Host utility avoids controller-to-controller imports without making the browser authoritative for filesystem targets.
+**Put native opening in one controller.** Rejected because Session, Settings, and the retained Host description consume the same platform operation. A Host utility avoids controller-to-controller imports and duplicated platform logic.
## Consequences
diff --git a/.agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.zh.md b/.agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.zh.md
index f7507959a9..50b3038768 100644
--- a/.agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.zh.md
+++ b/.agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.zh.md
@@ -8,7 +8,7 @@ Status: implemented
Host API Proxy 曾在业务 Service、API Proxy interface、Zod schema、路由表、Client stub 与 Client 调用方之间重复定义简单一元操作。[Typert Remote 调用](2026-08-02-typert-remote-method-calls.zh.md)已经允许业务包持有这类调用,但如果迁移 endpoint 时没有一并保留生命周期与投影策略,就会改变可观察行为。
-与 Agent 绑定的调用需要格外谨慎。共享 lookup 策略会复用 live Agent、用记录的 preset 恢复普通冷 Session、对并发恢复去重,并拒绝由 subagent 持有的 identity。skill 列表则必须检查 Session 而不激活 Agent。原生桌面操作必须避免让浏览器选择任意 Host 路径。
+与 Agent 绑定的调用需要格外谨慎。共享 lookup 策略会复用 live Agent、用记录的 preset 恢复普通冷 Session、对并发恢复去重,并拒绝由 subagent 持有的 identity。skill 列表则必须检查 Session 而不激活 Agent。Settings 与 preset 操作不会把 Host 持有的文档路径放进浏览器请求;Session 文件链接保留由调用方解析路径的行为。
## 决策
@@ -30,11 +30,11 @@ Host API Proxy 曾在业务 Service、API Proxy interface、Zod schema、路由
| `workspace.list`、`workspace.insertSessionBefore`、`workspace.archiveSession` | 对应的 `workspace/*` 方法 | Workspace registry 持有脱离可变对象的 snapshot 与串行 mutation。 |
| `skill.list` | `skills/list` | `SessionSkillCatalog` 观察 Session 及其记录的 preset,仅在 live Agent 已存在时使用它,列表查询绝不激活 Agent。 |
| `fileReferences/list` | `fileReferences/list` | `SessionFileReferences` 向 provider 提供 Session Controller 的既有 Agent lookup;冷 lookup 行为保持不变。 |
-| `host.openPath` | `session/openWorkspacePath` | `SessionController` 先基于目标 Session 的 workspace 解析路径,再执行原生打开。 |
+| `host.openPath` | `session/openWorkspacePath` | Session-aware Client 先基于已知 workspace 解析相对路径,再由 `SessionController` 交给原生打开器。 |
共享 Agent 与 Session resolver 仍是接收这些对象的 endpoint 的权威。它提供与旧 API Proxy 调用相同的 live 复用、冷恢复、并发去重、preset setup、持久化失败与 subagent ownership fence。`TypertLookupFailure` 保留 resolver 持有的 RPC error,而不把它们归并为 `internal`。
-原生路径实现在 `@deepseek-ai/dsh-native-command` 中。Session 与 Settings controller 选择目标;该工具仅负责平台探测、WSL 转换、浏览器偏好、文本编辑器意图与无 shell 命令执行。
+原生路径实现在 `@deepseek-ai/dsh-native-command` 中。Settings controller 选择 Host 持有的目标,Session-aware Client 则在调用 `SessionController` 前解析 workspace 路径;该工具仅负责平台探测、WSL 转换、浏览器偏好、文本编辑器意图与无 shell 命令执行。
## 浏览器认证
@@ -50,7 +50,7 @@ Connection 在选择 Typert interceptor 或 API Proxy fallback 前认证完整
**迁移每一个一元操作。** 否决,因为 `host.describe` 组合部署事实与 Connection readiness,而 Session export 是流式下载,不是一元业务方法。
-**把原生打开操作放入某个 controller。** 否决,因为 Session、Settings 与保留的 Host 描述都会消费同一平台操作。Host 工具可以避免 controller 间导入,同时不让浏览器成为文件系统目标的权威。
+**把原生打开操作放入某个 controller。** 否决,因为 Session、Settings 与保留的 Host 描述都会消费同一平台操作。Host 工具可以避免 controller 间导入与重复的平台逻辑。
## 后果
diff --git a/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.i18n.yaml b/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.i18n.yaml
index e97143a911..fa703bf41d 100644
--- a/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.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 .agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.md
-2026-07-28-tool-call-file-open-in-os.md: c8ede5c5c2fbdc9797edd9bf80673442c39873c2
-2026-07-28-tool-call-file-open-in-os.zh.md: 1e51dd4dced25bd14272358199baef82f396635a
+2026-07-28-tool-call-file-open-in-os.md: a8d5bd116b3f4cf1434d44b643dad3d883763a82
+2026-07-28-tool-call-file-open-in-os.zh.md: b486ec0972356419c2df5abbc148dc57a4586920
diff --git a/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.md b/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.md
index c8ede5c5c2..a8d5bd116b 100644
--- a/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.md
+++ b/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.md
@@ -10,7 +10,7 @@ Chat tool rows treated the whole summary line as a click target that opened the
## Decision
-File-tool path summaries (`read` / `write` / `edit` args carrying `path` or `file_path`) render as links underlined at rest with a pointer cursor. Clicking the path calls `session/openWorkspacePath` through the chat view's `openFile` injection; the Host resolves relative paths against the addressed Session's cwd. File-link rows disable args expand (leading icon is inert); whole-row click, row hover fill, and the click-to-open-details gesture are removed from tool rows (including bash and todo registrations). The details panel and its inject surface remain for programmatic selection; rows no longer drive them.
+File-tool path summaries (`read` / `write` / `edit` args carrying `path` or `file_path`) render as links underlined at rest with a pointer cursor. Clicking the path calls `session/openWorkspacePath` through the chat view's `openFile` injection; the chat view resolves relative paths against the addressed Session's cwd when it is known. File-link rows disable args expand (leading icon is inert); whole-row click, row hover fill, and the click-to-open-details gesture are removed from tool rows (including bash and todo registrations). The details panel and its inject surface remain for programmatic selection; rows no longer drive them.
`session/openWorkspacePath` uses the authenticated Remote carrier, while the product UI offers the gesture only on a loopback page whose `host.describe.canOpenPath` is true. Platform adapters open without a shell: `open` on macOS, PowerShell `Invoke-Item` on Windows, and `xdg-open` on desktop Linux; browser-renderable documents prefer the named default browser on macOS and desktop Linux. WSL is a separate host shape despite Node reporting `linux`: the adapter recognizes its environment or Microsoft kernel release, translates the Linux path with `wslpath -w`, and passes the resulting Windows/UNC path to the same PowerShell handoff. The opener's platform facts and command runner are injectable for tests. URL-only read args (`web_fetch`) are not file links.
diff --git a/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.zh.md b/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.zh.md
index 1e51dd4dce..b486ec0972 100644
--- a/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.zh.md
+++ b/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.zh.md
@@ -10,7 +10,7 @@ Status: implemented
## 决策
-文件工具的路径摘要(`read`/`write`/`edit` 参数中的 `path` 或 `file_path`)渲染为静止状态下即带下划线的链接,并使用 pointer 光标。点击路径会经聊天视图的 `openFile` injection 调用 `session/openWorkspacePath`;Host 以目标 Session 的 cwd 为基准解析相对路径。带文件链接的行关闭参数展开(左侧图标不可点);工具行(含 bash 与 todo 注册)去掉整行点击、整行悬停底色,以及点击打开 details 的手势。details 面板及其 inject 面仍保留供程序化选择;工具行不再驱动它们。
+文件工具的路径摘要(`read`/`write`/`edit` 参数中的 `path` 或 `file_path`)渲染为静止状态下即带下划线的链接,并使用 pointer 光标。点击路径会经聊天视图的 `openFile` injection 调用 `session/openWorkspacePath`;聊天视图会在目标 Session 的 cwd 已知时据此解析相对路径。带文件链接的行关闭参数展开(左侧图标不可点);工具行(含 bash 与 todo 注册)去掉整行点击、整行悬停底色,以及点击打开 details 的手势。details 面板及其 inject 面仍保留供程序化选择;工具行不再驱动它们。
`session/openWorkspacePath` 使用经过认证的 Remote carrier,而产品 UI 只在 loopback 页面且 `host.describe.canOpenPath` 为 true 时提供该手势。平台适配器不经 shell 打开:macOS 为 `open`,Windows 为 PowerShell `Invoke-Item`,桌面 Linux 为 `xdg-open`;浏览器可渲染的文档会在 macOS 与桌面 Linux 上优先使用指定的默认浏览器。尽管 Node 将 WSL 报告为 `linux`,WSL 仍是一种独立的宿主形态:适配器根据其环境或 Microsoft 内核 release 识别它,用 `wslpath -w` 转换 Linux 路径,并将所得 Windows/UNC 路径交给同一 PowerShell 交接。打开器的平台信息和命令运行器可在测试中注入。仅含 URL 的 read 参数(`web_fetch`)不是文件链接。
diff --git a/apps/web/tests/produced-files.e2e.ts b/apps/web/tests/produced-files.e2e.ts
index cdc3296693..0761639de5 100644
--- a/apps/web/tests/produced-files.e2e.ts
+++ b/apps/web/tests/produced-files.e2e.ts
@@ -158,7 +158,7 @@ describe('web e2e: a finished turn ends with the files it produced', () => {
])
expect(response.status()).toBe(200)
expect(openPath).toHaveBeenCalledTimes(1)
- expect(openPath.mock.calls[0]![0]).toMatchObject({ path: '.' })
+ expect(openPath.mock.calls[0]![0]).toMatchObject({ path: `${scaffold.workspaceCwd}/.` })
} finally {
openPath.mockRestore()
}
diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml
index 2416c74af9..438a3ef9c8 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: 0a08455a27fa94f1bb69628b61dd8eb23d318ded
-config-catalog.zh.md: ad6a9c0481750ec41c0701d3a9e3989fd0e9ebf4
+config-catalog.md: 05bd17a600869782409038188457db6d362f07a4
+config-catalog.zh.md: 1095b31530a28af2c4a23520fd5e68e067b46c11
diff --git a/docs/config-catalog.md b/docs/config-catalog.md
index 0a08455a27..05bd17a600 100644
--- a/docs/config-catalog.md
+++ b/docs/config-catalog.md
@@ -304,7 +304,7 @@ export interface Config {
}
```
-Source: [`packages/api/session-controller/src/index.ts:69`](../packages/api/session-controller/src/index.ts)
+Source: [`packages/api/session-controller/src/index.ts:67`](../packages/api/session-controller/src/index.ts)
diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md
index ad6a9c0481..1095b31530 100644
--- a/docs/config-catalog.zh.md
+++ b/docs/config-catalog.zh.md
@@ -306,7 +306,7 @@ export interface Config {
}
```
-来源:[`packages/api/session-controller/src/index.ts:69`](../packages/api/session-controller/src/index.ts)
+来源:[`packages/api/session-controller/src/index.ts:67`](../packages/api/session-controller/src/index.ts)
diff --git a/docs/event-producer-consumer.i18n.yaml b/docs/event-producer-consumer.i18n.yaml
index 468327d243..749e1e0695 100644
--- a/docs/event-producer-consumer.i18n.yaml
+++ b/docs/event-producer-consumer.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/event-producer-consumer.md
-event-producer-consumer.md: 2c443095b796eb274fa099b4b7d91b4454311a37
-event-producer-consumer.zh.md: b35e1c56d7f51d3aed7f3f81aec228708cb952e3
+event-producer-consumer.md: 8c30d5a945e2abb21c1e8dad5d6aa4e472c18b23
+event-producer-consumer.zh.md: 29304d9ac855a36ffc8a5e6766bedc841cf2738c
diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md
index 2c443095b7..8c30d5a945 100644
--- a/docs/event-producer-consumer.md
+++ b/docs/event-producer-consumer.md
@@ -21,11 +21,11 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:224`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | `agent-team`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:185`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` |
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:285`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
-| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:538`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
-| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:518`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
-| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:545`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
-| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:524`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
-| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:531`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:537`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:517`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:544`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:523`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:530`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:85`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` |
| `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
| `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:80`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `remotes` |
diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md
index b35e1c56d7..29304d9ac8 100644
--- a/docs/event-producer-consumer.zh.md
+++ b/docs/event-producer-consumer.zh.md
@@ -23,11 +23,11 @@
| `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:224`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | `agent-team`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:185`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` |
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:285`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
-| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:538`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
-| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:518`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
-| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:545`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
-| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:524`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
-| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:531`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:537`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:517`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:544`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:523`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:530`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:85`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` |
| `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
| `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:80`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `remotes` |
diff --git a/docs/subsystems/session.i18n.yaml b/docs/subsystems/session.i18n.yaml
index 423b9a6668..f917871371 100644
--- a/docs/subsystems/session.i18n.yaml
+++ b/docs/subsystems/session.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/session.md
-session.md: fe3f71de6aeb6b5d9924fa88c94688f7eac8ff0a
-session.zh.md: 844fd7a057c853fcbda6add875b997069ab024de
+session.md: 919a8eff583886610b56b34294ae1c13a06493b0
+session.zh.md: 8ffc7c8256311328c9e56e63625f3fbfcb5241a7
diff --git a/docs/subsystems/session.md b/docs/subsystems/session.md
index fe3f71de6a..919a8eff58 100644
--- a/docs/subsystems/session.md
+++ b/docs/subsystems/session.md
@@ -581,7 +581,7 @@ The backends that consume this contract are on [persistence.md](persistence.md).
`ModelCatalog` is the Host-generation model directory returned by `session/modelCatalog`: it carries the deployment default, routable provider ids, successful provider groups, and isolated provider failures. It is not derived from one Session and remains separate from Session projections.
-`SessionOpenWorkspacePathRequest` carries a `sessionId` and an absolute or Session-workspace-relative `path`. `SessionOpenWorkspacePathValue` confirms that the Host accepted the native handoff. The controller inspects the Session without activating its Agent, resolves a relative path against the recorded cwd, and reports missing Sessions, cancellation, and opener failures through the Session Remote error vocabulary.
+`SessionOpenWorkspacePathRequest` carries an absolute or workspace-resolved `path`. `SessionOpenWorkspacePathValue` confirms that the Host accepted the native handoff. A Session-aware Client resolves relative paths against its current Session cwd when known; the controller hands the path to the opener unchanged and reports invalid requests, cancellation, and opener failures through the Session Remote error vocabulary.
@@ -650,11 +650,11 @@ inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<{ meta: SessionH
@Remote('modelCatalog') modelCatalog(): Promise
/**
- * Open a path resolved against one Session's workspace on the Host desktop.
- * @param request - Session identity and absolute or workspace-relative path.
- * @param signal - caller lifetime; abort terminates inspection or the native command.
+ * Open one path prepared by a Session-aware caller on the Host desktop.
+ * @param request - path after best-effort Session workspace resolution.
+ * @param signal - caller lifetime; abort terminates the native command.
* @returns confirmation after the native opener accepts the path.
- * @throws TypertRemoteFailure when the request is invalid, the Session is missing, or the opener fails.
+ * @throws TypertRemoteFailure when the request is invalid, cancelled, or the opener fails.
*/
@Remote('openWorkspacePath') async openWorkspacePath( request: SessionOpenWorkspacePathRequest, signal: AbortSignal, ): Promise
diff --git a/docs/subsystems/session.zh.md b/docs/subsystems/session.zh.md
index 844fd7a057..8ffc7c8256 100644
--- a/docs/subsystems/session.zh.md
+++ b/docs/subsystems/session.zh.md
@@ -585,7 +585,7 @@ interface TurnEndReasonMap {
`ModelCatalog` 是 `session/modelCatalog` 返回的 Host generation 模型目录:它携带部署默认值、可路由 provider id、成功的 provider 分组与相互隔离的 provider 失败。它不由某个 Session 派生,因此与 Session projection 分开保存。
-`SessionOpenWorkspacePathRequest` 携带 `sessionId` 与绝对路径或相对于 Session workspace 的 `path`。`SessionOpenWorkspacePathValue` 确认 Host 已接受原生交接。controller 在不激活 Agent 的前提下检查 Session,基于记录的 cwd 解析相对路径,并通过 Session Remote 错误词汇表报告 Session 缺失、取消与打开器失败。
+`SessionOpenWorkspacePathRequest` 携带绝对路径或已按 workspace 解析的 `path`。`SessionOpenWorkspacePathValue` 确认 Host 已接受原生交接。Session-aware Client 会在已知当前 Session cwd 时据此解析相对路径;controller 将路径原样交给打开器,并通过 Session Remote 错误词汇表报告无效请求、取消与打开器失败。
@@ -654,11 +654,11 @@ inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<{ meta: SessionH
@Remote('modelCatalog') modelCatalog(): Promise
/**
- * Open a path resolved against one Session's workspace on the Host desktop.
- * @param request - Session identity and absolute or workspace-relative path.
- * @param signal - caller lifetime; abort terminates inspection or the native command.
+ * Open one path prepared by a Session-aware caller on the Host desktop.
+ * @param request - path after best-effort Session workspace resolution.
+ * @param signal - caller lifetime; abort terminates the native command.
* @returns confirmation after the native opener accepts the path.
- * @throws TypertRemoteFailure when the request is invalid, the Session is missing, or the opener fails.
+ * @throws TypertRemoteFailure when the request is invalid, cancelled, or the opener fails.
*/
@Remote('openWorkspacePath') async openWorkspacePath( request: SessionOpenWorkspacePathRequest, signal: AbortSignal, ): Promise
diff --git a/knip.json b/knip.json
index b22ef3caaa..a1c247f0d5 100644
--- a/knip.json
+++ b/knip.json
@@ -460,11 +460,6 @@
"tests/**/*.ts"
]
},
- "packages/context/file-reference": {
- "ignoreDependencies": [
- "zod"
- ]
- },
"packages/context/session-reference": {
"ignoreDependencies": [
"zod"
diff --git a/packages/api/session-controller/src/index.ts b/packages/api/session-controller/src/index.ts
index dadf1db607..b58cdfcbad 100644
--- a/packages/api/session-controller/src/index.ts
+++ b/packages/api/session-controller/src/index.ts
@@ -7,9 +7,7 @@ import { openNativePath } from '@deepseek-ai/dsh-native-command'
import type { SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session'
import type { SessionObservation } from '@deepseek-ai/dsh-session-query'
import { Remote, TypertRemoteFailure, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
-import { resolveWorkspacePath } from '@deepseek-ai/dsh-util-workspace-path'
import {
- ApiSessionNotFound,
ApiSessionAgentController,
inspectApiSession,
type ApiSessionAgentResult,
@@ -245,11 +243,11 @@ export class SessionController extends TypertRemoteService {
}
/**
- * Open a path resolved against one Session's workspace on the Host desktop.
- * @param request - Session identity and absolute or workspace-relative path.
- * @param signal - caller lifetime; abort terminates inspection or the native command.
+ * Open one path prepared by a Session-aware caller on the Host desktop.
+ * @param request - path after best-effort Session workspace resolution.
+ * @param signal - caller lifetime; abort terminates the native command.
* @returns confirmation after the native opener accepts the path.
- * @throws TypertRemoteFailure when the request is invalid, the Session is missing, or the opener fails.
+ * @throws TypertRemoteFailure when the request is invalid, cancelled, or the opener fails.
*/
@Remote('openWorkspacePath')
async openWorkspacePath(
@@ -264,30 +262,8 @@ export class SessionController extends TypertRemoteService {
})
}
signal.throwIfAborted()
- let cwd: string | undefined
try {
- cwd = (await this.inspect(request.sessionId, signal)).meta.cwd
- } catch (error: unknown) {
- if (signal.aborted) {
- throw new TypertRemoteFailure({
- code: 'cancelled', message: 'path open was aborted', details: {},
- })
- }
- if (error instanceof ApiSessionNotFound) {
- throw new TypertRemoteFailure({
- code: 'session-not-found',
- message: error.message,
- details: { sessionId: request.sessionId },
- })
- }
- throw new TypertRemoteFailure({
- code: 'internal',
- message: `session "${request.sessionId}" could not be inspected: ${String(error)}`,
- details: {},
- })
- }
- try {
- await this.openPath(resolveWorkspacePath(cwd, request.path), signal)
+ await this.openPath(request.path, signal)
return { opened: true }
} catch (error: unknown) {
if (signal.aborted) {
diff --git a/packages/api/session-controller/src/types.ts b/packages/api/session-controller/src/types.ts
index 24e9232d07..167937e3d3 100644
--- a/packages/api/session-controller/src/types.ts
+++ b/packages/api/session-controller/src/types.ts
@@ -362,10 +362,9 @@ export interface SessionCancelValue {
readonly accepted: true
}
-/** Session-addressed request to open one workspace path on the Host desktop. */
+/** Request to open one path prepared by a Session-aware caller on the Host desktop. */
export interface SessionOpenWorkspacePathRequest {
- readonly sessionId: SessionId
- /** Absolute or Session-workspace-relative path. */
+ /** Path after best-effort Session workspace resolution, in Host filesystem syntax. */
readonly path: string
}
diff --git a/packages/api/session-controller/tests/session-open-workspace-path.host.spec.ts b/packages/api/session-controller/tests/session-open-workspace-path.host.spec.ts
index 2727fb96c2..ee24947227 100644
--- a/packages/api/session-controller/tests/session-open-workspace-path.host.spec.ts
+++ b/packages/api/session-controller/tests/session-open-workspace-path.host.spec.ts
@@ -1,11 +1,10 @@
import { Context } from '@deepseek-ai/cordis'
import AgentRegistry from '@deepseek-ai/dsh-agent'
-import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
+import SessionStore from '@deepseek-ai/dsh-session'
import { describe, expect, it, vi } from 'vitest'
import {
createSessionTestController,
createSessionTestRemote,
- testSessionPersistence,
} from './test-remote.ts'
async function context(): Promise {
@@ -16,10 +15,8 @@ async function context(): Promise {
}
describe('session/openWorkspacePath', () => {
- it('resolves a relative path against the attached Session cwd', async () => {
+ it('hands a Client-resolved workspace path to the Host opener unchanged', async () => {
const ctx = await context()
- const sessionId = SessionId('open-relative')
- ctx.sessions.create(sessionId, { meta: { cwd: '/workspace/project' } })
const openPath = vi.fn((_path: string, _signal: AbortSignal) => Promise.resolve())
const remote = createSessionTestRemote(ctx, {
defaultModelSelection: () => ({ provider: 'p', model: 'm' }),
@@ -28,18 +25,14 @@ describe('session/openWorkspacePath', () => {
})
const signal = new AbortController().signal
- await expect(remote.openWorkspacePath({ sessionId, path: 'src/a.ts' }, signal))
+ await expect(remote.openWorkspacePath({ path: '/workspace/project/src/a.ts' }, signal))
.resolves.toEqual({ ok: true, value: { opened: true } })
expect(openPath).toHaveBeenCalledWith('/workspace/project/src/a.ts', signal)
expect(ctx.agents.list()).toEqual([])
})
- it('preserves absolute paths and cwd-less Session paths', async () => {
+ it('preserves relative and absolute Host-resolvable paths', async () => {
const ctx = await context()
- const withCwd = SessionId('open-absolute')
- const withoutCwd = SessionId('open-without-cwd')
- ctx.sessions.create(withCwd, { meta: { cwd: '/workspace/project' } })
- ctx.sessions.create(withoutCwd)
const openPath = vi.fn((_path: string, _signal: AbortSignal) => Promise.resolve())
const remote = createSessionTestRemote(ctx, {
defaultModelSelection: () => ({ provider: 'p', model: 'm' }),
@@ -47,18 +40,13 @@ describe('session/openWorkspacePath', () => {
openPath,
})
- await remote.openWorkspacePath({ sessionId: withCwd, path: '/tmp/result.html' })
- await remote.openWorkspacePath({ sessionId: withoutCwd, path: 'result.html' })
+ await remote.openWorkspacePath({ path: '/tmp/result.html' })
+ await remote.openWorkspacePath({ path: 'result.html' })
expect(openPath.mock.calls.map(call => call[0])).toEqual(['/tmp/result.html', 'result.html'])
})
- it('rejects empty paths and missing Sessions before opening anything', async () => {
+ it('rejects empty paths before opening anything', async () => {
const ctx = await context()
- const sessionId = SessionId('open-validation')
- ctx.provide('sessionPersistence', testSessionPersistence(ctx, {
- list: () => Promise.resolve([]),
- inspect: () => Promise.resolve(undefined),
- }) as never)
const openPath = vi.fn((_path: string, _signal: AbortSignal) => Promise.resolve())
const remote = createSessionTestRemote(ctx, {
defaultModelSelection: () => ({ provider: 'p', model: 'm' }),
@@ -66,17 +54,13 @@ describe('session/openWorkspacePath', () => {
openPath,
})
- await expect(remote.openWorkspacePath({ sessionId, path: '' }))
+ await expect(remote.openWorkspacePath({ path: '' }))
.resolves.toMatchObject({ ok: false, error: { code: 'bad-request' } })
- await expect(remote.openWorkspacePath({ sessionId, path: 'result.html' }))
- .resolves.toMatchObject({ ok: false, error: { code: 'session-not-found' } })
expect(openPath).not.toHaveBeenCalled()
})
it('preserves native opener failure and cancellation results', async () => {
const ctx = await context()
- const sessionId = SessionId('open-failure')
- ctx.sessions.create(sessionId, { meta: { cwd: '/workspace/project' } })
const openPath = vi.fn((_path: string, _signal: AbortSignal) =>
Promise.reject(new Error('desktop unavailable')))
const remote = createSessionTestRemote(ctx, {
@@ -85,7 +69,7 @@ describe('session/openWorkspacePath', () => {
openPath,
})
- await expect(remote.openWorkspacePath({ sessionId, path: 'result.html' }))
+ await expect(remote.openWorkspacePath({ path: 'result.html' }))
.resolves.toMatchObject({
ok: false,
error: { code: 'internal', message: 'path open failed: desktop unavailable' },
@@ -93,38 +77,12 @@ describe('session/openWorkspacePath', () => {
const aborted = new AbortController()
aborted.abort(new Error('cancelled'))
- await expect(remote.openWorkspacePath({ sessionId, path: 'result.html' }, aborted.signal))
+ await expect(remote.openWorkspacePath({ path: 'result.html' }, aborted.signal))
.resolves.toMatchObject({ ok: false, error: { code: 'cancelled' } })
})
- it('classifies inspection cancellation and non-session failures', async () => {
- const ctx = await context()
- const controller = createSessionTestController(ctx, {
- defaultModelSelection: () => ({ provider: 'p', model: 'm' }),
- cwd: '/default',
- })
- const inspect = vi.spyOn(controller, 'inspect')
- const aborted = new AbortController()
- inspect.mockImplementationOnce(async () => {
- aborted.abort(new Error('cancelled'))
- throw new Error('inspection stopped')
- })
- await expect(controller.openWorkspacePath({
- sessionId: SessionId('inspection-cancelled'), path: 'result.html',
- }, aborted.signal)).rejects.toMatchObject({ failure: { code: 'cancelled' } })
-
- inspect.mockRejectedValueOnce('storage offline')
- const failed = controller.openWorkspacePath({
- sessionId: SessionId('inspection-failed'), path: 'result.html',
- }, new AbortController().signal)
- await expect(failed).rejects.toMatchObject({ failure: { code: 'internal' } })
- await expect(failed).rejects.toThrow('storage offline')
- })
-
it('classifies opener cancellation and non-Error failures', async () => {
const ctx = await context()
- const sessionId = SessionId('open-error-kinds')
- ctx.sessions.create(sessionId, { meta: { cwd: '/workspace/project' } })
const aborted = new AbortController()
const openPath = vi.fn()
.mockImplementationOnce(async () => {
@@ -138,10 +96,10 @@ describe('session/openWorkspacePath', () => {
openPath,
})
- await expect(controller.openWorkspacePath({ sessionId, path: 'first.html' }, aborted.signal))
+ await expect(controller.openWorkspacePath({ path: 'first.html' }, aborted.signal))
.rejects.toMatchObject({ failure: { code: 'cancelled' } })
await expect(controller.openWorkspacePath({
- sessionId, path: 'second.html',
+ path: 'second.html',
}, new AbortController().signal)).rejects.toMatchObject({
failure: { code: 'internal', message: 'path open failed: desktop unavailable' },
})
diff --git a/packages/api/session-controller/tsconfig.host.json b/packages/api/session-controller/tsconfig.host.json
index e3f40c885d..bea21672b7 100644
--- a/packages/api/session-controller/tsconfig.host.json
+++ b/packages/api/session-controller/tsconfig.host.json
@@ -44,7 +44,6 @@
{ "path": "../../subagent/subagent" },
{ "path": "../../typert/protocol" },
{ "path": "../../typert/registry" },
- { "path": "../../util/workspace-path" },
{ "path": "../../workspace/workspace" }
]
}
diff --git a/packages/client/connection/src/client/fixture.ts b/packages/client/connection/src/client/fixture.ts
index e39c39db01..a2a7617a7e 100644
--- a/packages/client/connection/src/client/fixture.ts
+++ b/packages/client/connection/src/client/fixture.ts
@@ -3501,9 +3501,7 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
})
}
case 'session/openWorkspacePath': {
- const pathRequest = request as { readonly sessionId: SessionId; readonly path: string }
- const missing = requireRemoteSession(pathRequest)
- return missing ?? sessionOk({ opened: true as const })
+ return sessionOk({ opened: true as const })
}
case 'session/modelCatalog': return Promise.resolve({
ok: true,
diff --git a/packages/client/ui-chat/package.json b/packages/client/ui-chat/package.json
index fcafb63b8b..10382a63bf 100644
--- a/packages/client/ui-chat/package.json
+++ b/packages/client/ui-chat/package.json
@@ -74,7 +74,8 @@
"@deepseek-ai/dsh-session-stats": "workspace:^",
"@deepseek-ai/dsh-settings": "workspace:^",
"@deepseek-ai/dsh-token-meter": "workspace:^",
- "@deepseek-ai/dsh-tools": "workspace:^"
+ "@deepseek-ai/dsh-tools": "workspace:^",
+ "@deepseek-ai/dsh-util-workspace-path": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/cordis": "workspace:^",
@@ -104,6 +105,7 @@
"@deepseek-ai/dsh-settings": "workspace:^",
"@deepseek-ai/dsh-token-meter": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
+ "@deepseek-ai/dsh-util-workspace-path": "workspace:^",
"@types/react": "~18.3.1",
"react": "^18.2.0"
},
diff --git a/packages/client/ui-chat/src/client/apply.ts b/packages/client/ui-chat/src/client/apply.ts
index 829b36bd0f..be061dff13 100644
--- a/packages/client/ui-chat/src/client/apply.ts
+++ b/packages/client/ui-chat/src/client/apply.ts
@@ -5,6 +5,7 @@ import type {} from '@deepseek-ai/dsh-api-remotes/client'
import type { SessionBinding } from '@deepseek-ai/dsh-api-session-controller/client'
import type { BoundActions, ObservableSnapshot } from '@deepseek-ai/dsh-client-store'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
+import { resolveWorkspacePath } from '@deepseek-ai/dsh-util-workspace-path'
// Type-only service and declaration merges used by the apply world.
import type {} from '@deepseek-ai/dsh-client-locale/client'
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
@@ -117,7 +118,10 @@ export function apply(ctx: Context): void {
},
fileMentions: (owner: TurnTailOwnerProps) => ctx.get('chatFileMentions')?.forClosing(owner),
openFile: async (path) => {
- const result = await ctx.remote.session.openWorkspacePath({ sessionId, path })
+ const cwd = ctx.sessions.list.getSnapshot().byId[sessionId]?.cwd
+ const result = await ctx.remote.session.openWorkspacePath({
+ path: resolveWorkspacePath(cwd, path),
+ })
if (!result.ok) throw new Error(`path open failed: ${result.error.message}`)
},
loadOlder: () => { void session.loadOlder() },
diff --git a/packages/client/ui-chat/tests/apply-inject.client.spec.tsx b/packages/client/ui-chat/tests/apply-inject.client.spec.tsx
index 2a0d9b045c..9ceef64dd7 100644
--- a/packages/client/ui-chat/tests/apply-inject.client.spec.tsx
+++ b/packages/client/ui-chat/tests/apply-inject.client.spec.tsx
@@ -123,7 +123,7 @@ describe('Chat inject API', () => {
const b = await bench()
const { injected } = b.chatViewApi(ROOT)
await injected.openFile('src/a.ts')
- expect(b.openWorkspacePath).toHaveBeenCalledWith({ sessionId: ROOT, path: 'src/a.ts' })
+ expect(b.openWorkspacePath).toHaveBeenCalledWith({ path: '/proj/src/a.ts' })
b.openWorkspacePath.mockResolvedValueOnce({
ok: false,
diff --git a/packages/client/ui-chat/tsconfig.json b/packages/client/ui-chat/tsconfig.json
index 0800260fbb..4d42320885 100644
--- a/packages/client/ui-chat/tsconfig.json
+++ b/packages/client/ui-chat/tsconfig.json
@@ -50,6 +50,9 @@
{
"path": "../../runtime-diagnostics/invariants"
},
+ {
+ "path": "../../util/workspace-path"
+ },
{
"path": "../../session/session-stats"
},
diff --git a/packages/client/ui-tool/tests/chat-code-subcalls.client.spec.tsx b/packages/client/ui-tool/tests/chat-code-subcalls.client.spec.tsx
index be9061fbc2..a23e003800 100644
--- a/packages/client/ui-tool/tests/chat-code-subcalls.client.spec.tsx
+++ b/packages/client/ui-tool/tests/chat-code-subcalls.client.spec.tsx
@@ -223,7 +223,7 @@ describe('run_code sub-calls through the real chat machinery', () => {
view.getByText('notes/demo.txt').click()
expect(b.layout.openDetails).not.toHaveBeenCalled()
await vi.waitFor(() => {
- expect(b.openWorkspacePath).toHaveBeenCalledWith({ sessionId: SID, path: 'notes/demo.txt' })
+ expect(b.openWorkspacePath).toHaveBeenCalledWith({ path: 'notes/demo.txt' })
})
view.getByText('List notes').click()
expect(b.layout.openDetails).not.toHaveBeenCalled()
diff --git a/packages/client/ui-tool/tests/toolview-slot.client.spec.tsx b/packages/client/ui-tool/tests/toolview-slot.client.spec.tsx
index ad5eb90a10..719dbafb1a 100644
--- a/packages/client/ui-tool/tests/toolview-slot.client.spec.tsx
+++ b/packages/client/ui-tool/tests/toolview-slot.client.spec.tsx
@@ -137,7 +137,7 @@ describe('keyed toolview hole through the real machinery', () => {
view.getByText('src/a.ts').click()
expect(b.layout.openDetails).not.toHaveBeenCalled()
await vi.waitFor(() => {
- expect(b.openWorkspacePath).toHaveBeenCalledWith({ sessionId: SID, path: 'src/a.ts' })
+ expect(b.openWorkspacePath).toHaveBeenCalledWith({ path: 'src/a.ts' })
})
await b.runtime.dispose()
})
diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts
index eb1992f66d..37d29ce704 100644
--- a/packages/extensions/tool-cordis/src/api-catalog.ts
+++ b/packages/extensions/tool-cordis/src/api-catalog.ts
@@ -1384,10 +1384,10 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
},
{
signature: '@Remote(\'openWorkspacePath\') async openWorkspacePath( request: SessionOpenWorkspacePathRequest, signal: AbortSignal, ): Promise',
- description: 'Open a path resolved against one Session\'s workspace on the Host desktop.',
- parameters: [{ name: 'request', description: 'Session identity and absolute or workspace-relative path.' }, { name: 'signal', description: 'caller lifetime; abort terminates inspection or the native command.' }],
+ description: 'Open one path prepared by a Session-aware caller on the Host desktop.',
+ parameters: [{ name: 'request', description: 'path after best-effort Session workspace resolution.' }, { name: 'signal', description: 'caller lifetime; abort terminates the native command.' }],
returns: 'confirmation after the native opener accepts the path.',
- throws: ['TypertRemoteFailure when the request is invalid, the Session is missing, or the opener fails.'],
+ throws: ['TypertRemoteFailure when the request is invalid, cancelled, or the opener fails.'],
},
{
signature: '@Remote(\'rename\') rename(request: SessionRenameRequest): Promise',
@@ -4960,7 +4960,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
},
{
name: 'SessionOpenWorkspacePathRequest',
- declaration: 'export interface SessionOpenWorkspacePathRequest {\n readonly sessionId: SessionId;\n readonly path: string;\n}',
+ declaration: 'export interface SessionOpenWorkspacePathRequest {\n readonly path: string;\n}',
},
{
name: 'SessionOpenWorkspacePathValue',
diff --git a/packages/host/apiproxy/src/api-proxy.ts b/packages/host/apiproxy/src/api-proxy.ts
index 29ebd6b732..6e502172f7 100644
--- a/packages/host/apiproxy/src/api-proxy.ts
+++ b/packages/host/apiproxy/src/api-proxy.ts
@@ -34,9 +34,7 @@ export interface ApiProxyDefaults {
/** Validated DEFLATE level for session-log ZIP entries; defaults to 6. */
sessionExportCompressionLevel?: SessionLogCompressionLevel
/**
- * Whether handing a path to the native opener can work at all — the
- * `hasDocument` capability the preset roster reports, and the switch
- * between opening a preset directory and answering its path as text.
+ * Whether `host.describe` reports that the Client may offer native path actions.
* Absent, platform detection decides ({@link canOpenNativePath}).
*/
canOpenPath?: () => boolean
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4eab46efb1..5ac1fcc288 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2065,6 +2065,9 @@ importers:
'@deepseek-ai/dsh-tools':
specifier: workspace:^
version: link:../../core/tools
+ '@deepseek-ai/dsh-util-workspace-path':
+ specifier: workspace:^
+ version: link:../../util/workspace-path
'@types/react':
specifier: ~18.3.1
version: 18.3.31