@deepseek-ai/dsh-client-connection 0.1.0-rc.7 → 0.1.1-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/client/connection/README.md
5
- README.md: f0a707cc5f3a962c7852f7323c727d0a39a57b10
6
- README.zh.md: b529eebf9af93e36b6b92c19964678b7f5a04ea1
5
+ README.md: a7562b9dac57930b1abc0b76b9079a6865a38b35
6
+ README.zh.md: 24c56e598ebd4b5ca39e433c5782399909f528b8
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  English | [中文](README.zh.md)
4
4
 
5
- Wire consumer layer: the client plugin's apply mounts `ctx.connection` (shared api client + current-page loopback state + observable generation-scoped `hostDescription` + single-consumer stream-loop starter); the export face carries the wire contract types, the `AbstractApiClient` abstraction, and the loop's sink/config types. Each successful readiness handshake publishes the exact `host.describe` value before `onConnected`; generation loss and explicit stop clear it, so native-capability consumers never retain a disconnected answer. The browser carrier uses HTTP POST for unary and respond operations and opens one downlink-only WebSocket each for `events.mux` and `events.host`; the in-process carrier satisfies the same two-stream abstraction. The Host half owns the single `/api` route and its Fetch bridge; a registered Typert interceptor claims its Remote endpoints before the API Proxy fallback. Loopback hostname classification stays package-internal: the `/api` Host fence and WebSocket upgrades use it directly, while other client plugins consume the derived `ctx.connection.isLoopback` state. The node half's `/api` route pins the privileged method set (`host.pickDirectory`, `host.openPath`, and the whole configuration plane — `settings.describe`/`openDocument`/`update`/`replace`/`mutate` and `credentials.describe`/`set`/`unset`; reads and native actions included, since describing returns the exposed configuration, opening acts on the Host desktop, and probing an arbitrary reference reports where a credential comes from — and the agent-preset authoring plane, `agentPreset.read`/`copy`/`openDocument`/`remove`, since a composition names the plugins a session runs, so reading one is reconnaissance, and copy/remove/openDocument manage the roster and drive the host desktop (authoring is copy-only, so none of them accepts composition text or a path); `agentPreset.list` and `agentPreset.select` stay out — the roster carries only ids and trust, and choosing a preset grants nothing `session.create`'s own `agentPreset` did not, over a default that already carries bash) to loopback by passing the trust fence with an empty trust list — a declared `trustedHosts` authority reaches every other method, while these stay loopback-local until a real authentication layer exists. The platform carriers and ConnectionController loop are package-internal; apply selects and drives them. The downlink boundary is documented in the [WebSocket downlink carrier Agent Note](../../../.agents/notes/implemented/architecture/2026-08-04-websocket-downlink-carrier.md).
5
+ Wire consumer layer: the client plugin's apply mounts `ctx.connection` (shared api client + current-page loopback state + observable generation-scoped `hostDescription` + single-consumer stream-loop starter); the export face carries the wire contract types, the `AbstractApiClient` abstraction, and the loop's sink/config types. Each successful readiness handshake publishes the exact `host.describe` value before `onConnected`; generation loss and explicit stop clear it, so native-capability consumers never retain a disconnected answer. The browser carrier uses HTTP POST for unary and respond operations and opens one downlink-only WebSocket each for `events.mux` and `events.host`; the in-process carrier satisfies the same two-stream abstraction. The exported `ClientTransportHooks` names the page global `__DSH_TRANSPORT__` that replaces the browser carrier wholesale: the served web app leaves it unset and gets HTTP + WebSocket, while a shell owning a different physical transport (the worker preview's postMessage tunnel) provides `createApiClient` and `fetch` — plus `loadBundle` when it also owns bundle bytes — instead of forking the plugin. The Host half owns the single `/api` route and its Fetch bridge; a registered Typert interceptor claims its Remote endpoints before the API Proxy fallback. Loopback hostname classification stays package-internal: the `/api` Host fence and WebSocket upgrades use it directly, while other client plugins consume the derived `ctx.connection.isLoopback` state. The node half's `/api` route pins the privileged method set (`host.pickDirectory`, `host.openPath`, and the whole configuration plane — `settings.describe`/`openDocument`/`update`/`replace`/`mutate` and `credentials.describe`/`set`/`unset`; reads and native actions included, since describing returns the exposed configuration, opening acts on the Host desktop, and probing an arbitrary reference reports where a credential comes from — and the agent-preset authoring plane, `agentPreset.read`/`copy`/`openDocument`/`remove`, since a composition names the plugins a session runs, so reading one is reconnaissance, and copy/remove/openDocument manage the roster and drive the host desktop (authoring is copy-only, so none of them accepts composition text or a path); `agentPreset.list` and `agentPreset.select` stay out — the roster carries only ids and trust, and choosing a preset grants nothing `session.create`'s own `agentPreset` did not, over a default that already carries bash) to loopback by passing the trust fence with an empty trust list — a declared `trustedHosts` authority reaches every other method, while these stay loopback-local until a real authentication layer exists. The platform carriers and ConnectionController loop are package-internal; apply selects and drives them. The downlink boundary is documented in the [WebSocket downlink carrier Agent Note](../../../.agents/notes/implemented/architecture/2026-08-04-websocket-downlink-carrier.md).
6
6
 
7
7
  ## /api browser-trust fence
8
8
 
package/README.zh.md CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  [English](README.md) | 中文
4
4
 
5
- 协议消费层:客户端插件的 apply 会挂载 `ctx.connection`(共享 API 客户端 + 当前页面的 loopback 状态 + 可观察且按 generation 生效的 `hostDescription` + 单消费方流循环启动器);导出表层携带协议约定类型、`AbstractApiClient` 抽象,以及循环的 sink/配置类型。每次就绪握手成功后,都会在 `onConnected` 之前发布完整的 `host.describe` 值;generation 失效或显式 stop 会清空它,因此原生能力消费者不会保留已经断线的判断。浏览器载体以 HTTP POST 发送 unary/respond,并为 `events.mux` 与 `events.host` 各开一条只下行的 WebSocket;进程内载体满足同一双流抽象。Host half 持有唯一 `/api` route 及其 Fetch bridge;已注册的 Typert interceptor 会先认领自己的 Remote endpoint,未认领请求再回退 API Proxy。Loopback hostname 判定逻辑留在包内部:`/api` Host fence 与 WebSocket upgrade 会直接使用它,其他客户端插件则消费派生的 `ctx.connection.isLoopback` 状态。node 半侧的 `/api` 路由让特权方法集(`host.pickDirectory`、`host.openPath`,以及整个配置面——`settings.describe`/`openDocument`/`update`/`replace`/`mutate` 与 `credentials.describe`/`set`/`unset`;读取与原生操作也在内,因为 describe 会返回已暴露的配置、打开操作会作用于 Host 桌面,而探测任意引用会报出某条凭据来自何处——以及 agent(智能体) preset 的创作面 `agentPreset.read`/`copy`/`openDocument`/`remove`,因为组装指明了一个会话所运行的插件,读取它是侦察,而 copy/remove/openDocument 管理名单并驱动宿主桌面(创作只有复制一种写入,因此这些方法都不接收组装文本或路径);`agentPreset.list` 与 `agentPreset.select` 不在其中——名单只携带 id 与信任级别,而选择一个 preset 并不比 `session.create` 自带的 `agentPreset` 多给任何能力,何况默认 preset 本就带着 bash)以空信任表过信任 fence,从而钉在回环——已声明的 `trustedHosts` 授权可达其余全部方法,而这些方法在真正的认证层出现之前仍只限回环本机。平台载体与 ConnectionController 循环属于包内部;apply 负责选择并驱动它们。下行边界见 [WebSocket 下行载体 Agent Note](../../../.agents/notes/implemented/architecture/2026-08-04-websocket-downlink-carrier.md)。
5
+ 协议消费层:客户端插件的 apply 会挂载 `ctx.connection`(共享 API 客户端 + 当前页面的 loopback 状态 + 可观察且按 generation 生效的 `hostDescription` + 单消费方流循环启动器);导出表层携带协议约定类型、`AbstractApiClient` 抽象,以及循环的 sink/配置类型。每次就绪握手成功后,都会在 `onConnected` 之前发布完整的 `host.describe` 值;generation 失效或显式 stop 会清空它,因此原生能力消费者不会保留已经断线的判断。浏览器载体以 HTTP POST 发送 unary/respond,并为 `events.mux` 与 `events.host` 各开一条只下行的 WebSocket;进程内载体满足同一双流抽象。导出的 `ClientTransportHooks` 命名了整体替换浏览器载体的页面全局量 `__DSH_TRANSPORT__`:served web app 不设置它、走 HTTP + WebSocket;拥有另一种物理传输的壳(worker 预览的 postMessage 隧道)则在此提供 `createApiClient` 与 `fetch`——当它同时持有 bundle 字节时再加 `loadBundle`——而不必 fork 本插件。Host half 持有唯一 `/api` route 及其 Fetch bridge;已注册的 Typert interceptor 会先认领自己的 Remote endpoint,未认领请求再回退 API Proxy。Loopback hostname 判定逻辑留在包内部:`/api` Host fence 与 WebSocket upgrade 会直接使用它,其他客户端插件则消费派生的 `ctx.connection.isLoopback` 状态。node 半侧的 `/api` 路由让特权方法集(`host.pickDirectory`、`host.openPath`,以及整个配置面——`settings.describe`/`openDocument`/`update`/`replace`/`mutate` 与 `credentials.describe`/`set`/`unset`;读取与原生操作也在内,因为 describe 会返回已暴露的配置、打开操作会作用于 Host 桌面,而探测任意引用会报出某条凭据来自何处——以及 agent(智能体) preset 的创作面 `agentPreset.read`/`copy`/`openDocument`/`remove`,因为组装指明了一个会话所运行的插件,读取它是侦察,而 copy/remove/openDocument 管理名单并驱动宿主桌面(创作只有复制一种写入,因此这些方法都不接收组装文本或路径);`agentPreset.list` 与 `agentPreset.select` 不在其中——名单只携带 id 与信任级别,而选择一个 preset 并不比 `session.create` 自带的 `agentPreset` 多给任何能力,何况默认 preset 本就带着 bash)以空信任表过信任 fence,从而钉在回环——已声明的 `trustedHosts` 授权可达其余全部方法,而这些方法在真正的认证层出现之前仍只限回环本机。平台载体与 ConnectionController 循环属于包内部;apply 负责选择并驱动它们。下行边界见 [WebSocket 下行载体 Agent Note](../../../.agents/notes/implemented/architecture/2026-08-04-websocket-downlink-carrier.zh.md)。
6
6
 
7
7
  ## /api 浏览器信任栅栏
8
8
 
9
- node 半侧在桥接或 upgrade 前守卫 `/api` 下的每个入口(`src/api-request-trust.ts`)。每个请求——无论是否带浏览器标记——`Host` 都必须是回环地址权威,或与某个 `trustedHosts` 条目匹配:带端口的 `host:port` 条目精确匹配,不带端口的条目匹配任意端口,两侧均经 WHATWG 归一化后比较(DNS rebinding 防御)。刻意不为无浏览器标记的 HTTP 请求开捷径:明文 HTTP 下浏览器的图片与导航读取既不带 `Origin` 也不带 Fetch-Metadata,因此无标记请求仍可能是被重绑页面发起的、响应可被读走的读取,而 Host 是重绑唯一伪造不了的请求头;WebSocket 浏览器握手会带 `Origin` 并通过同一道比较。非浏览器客户端经由回环地址、部署推导的 LAN IP 字面量或已声明的权威通过同一道栅栏。当标记存在时,如附带 `Origin`,则它必须与 Host 权威完全一致;显式的 `sec-fetch-site: cross-site` 标记一律拒绝。不是纯的、规范形 `host[:port]` 权威的 `trustedHosts` 条目——即 WHATWG 解析读回后与原文不完全一致的——会让插件加载明确报错:否则解析会悄悄授权 `harness.internal/path` 这类笔误里的 hostname,或把悬空冒号、补零端口放大成任意端口授权。HTTP 失败在任何 RPC 分发之前以纯 403 应答,upgrade 失败在启动任何事件流前拒绝握手。非回环组合必须显式信任其服务权威:Web 运行时从全接口服务器配置推导 LAN IP 字面量,cordis.yml 中的 `trustedHosts` 与 CLI(命令行界面)的 `--trusted-host` flag 则声明具名权威。`dsh web --host 0.0.0.0` 在远程访问具备认证层之前有意不受支持。这道栅栏是可达性策略,而不是认证;Web 载体不提供认证层。决策记录:[api 浏览器信任边界 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-28-api-browser-trust-boundary.md)。
9
+ node 半侧在桥接或 upgrade 前守卫 `/api` 下的每个入口(`src/api-request-trust.ts`)。每个请求——无论是否带浏览器标记——`Host` 都必须是回环地址权威,或与某个 `trustedHosts` 条目匹配:带端口的 `host:port` 条目精确匹配,不带端口的条目匹配任意端口,两侧均经 WHATWG 归一化后比较(DNS rebinding 防御)。刻意不为无浏览器标记的 HTTP 请求开捷径:明文 HTTP 下浏览器的图片与导航读取既不带 `Origin` 也不带 Fetch-Metadata,因此无标记请求仍可能是被重绑页面发起的、响应可被读走的读取,而 Host 是重绑唯一伪造不了的请求头;WebSocket 浏览器握手会带 `Origin` 并通过同一道比较。非浏览器客户端经由回环地址、部署推导的 LAN IP 字面量或已声明的权威通过同一道栅栏。当标记存在时,如附带 `Origin`,则它必须与 Host 权威完全一致;显式的 `sec-fetch-site: cross-site` 标记一律拒绝。不是纯的、规范形 `host[:port]` 权威的 `trustedHosts` 条目——即 WHATWG 解析读回后与原文不完全一致的——会让插件加载明确报错:否则解析会悄悄授权 `harness.internal/path` 这类笔误里的 hostname,或把悬空冒号、补零端口放大成任意端口授权。HTTP 失败在任何 RPC 分发之前以纯 403 应答,upgrade 失败在启动任何事件流前拒绝握手。非回环组合必须显式信任其服务权威:Web 运行时从全接口服务器配置推导 LAN IP 字面量,cordis.yml 中的 `trustedHosts` 与 CLI(命令行界面)的 `--trusted-host` flag 则声明具名权威。`dsh web --host 0.0.0.0` 在远程访问具备认证层之前有意不受支持。这道栅栏是可达性策略,而不是认证;Web 载体不提供认证层。决策记录:[api 浏览器信任边界 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-28-api-browser-trust-boundary.zh.md)。
10
10
 
11
11
  ## `/api` WebSocket 下行
12
12
 
package/lib/client.js CHANGED
@@ -5263,8 +5263,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5263
5263
  workspaceId: workspaceIdSchema.optional(),
5264
5264
  cwd: string().optional(),
5265
5265
  sessionId: sessionIdSchema.optional(),
5266
- agentPreset: string().optional()
5267
- }).refine((payload) => payload.workspaceId === void 0 || payload.cwd === void 0, { message: "session.create accepts workspaceId or cwd, not both" });
5266
+ agentPreset: string().optional(),
5267
+ reuseWorkspaceBlank: literal(true).optional()
5268
+ }).refine((payload) => payload.workspaceId === void 0 || payload.cwd === void 0, { message: "session.create accepts workspaceId or cwd, not both" }).refine((payload) => payload.reuseWorkspaceBlank !== true || payload.workspaceId !== void 0 && payload.sessionId !== void 0, { message: "session.create reuseWorkspaceBlank requires workspaceId and sessionId" });
5268
5269
  /** session.create response value. */
5269
5270
  const sessionCreateValueSchema = object({
5270
5271
  sessionId: sessionIdSchema,
@@ -5360,6 +5361,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5360
5361
  maxImagesPerMessage: number().int().positive(),
5361
5362
  maxMessageImageBytes: number().int().positive(),
5362
5363
  maxImagePixels: number().int().positive(),
5364
+ maxImageDimension: number().int().positive(),
5363
5365
  mediaTypes: array(string())
5364
5366
  });
5365
5367
  /** session.history response value (projections rides the tail page only). */
@@ -5707,6 +5709,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5707
5709
  provider: string().optional(),
5708
5710
  model: string().optional(),
5709
5711
  attachedSessions: number().int().nonnegative(),
5712
+ home: string(),
5710
5713
  canOpenPath: boolean()
5711
5714
  });
5712
5715
  object({});
@@ -7084,7 +7087,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7084
7087
  toolTurn(67, "grep", "{\"pattern\":\"SEARCH_MAX_LINES\",\"path\":\"packages/client\"}", SEARCH_MATCHES_TEXT);
7085
7088
  toolTurn(68, "glob", "{\"pattern\":\"**/SearchBlock*\",\"path\":\"packages/client\"}", SEARCH_PATHS_TEXT);
7086
7089
  toolTurn(69, "read", `{"file_path":${JSON.stringify(READ_SAMPLE_PATH)},"offset":${READ_SAMPLE_FIRST_LINE}}`, READ_SAMPLE_TEXT);
7087
- toolTurn(70, "web_search", "{\"query\":\"deepseek harness architecture\"}", "Search results for deepseek harness architecture.");
7090
+ toolTurn(70, "web_search", "{\"queries\":[\"deepseek harness architecture\"]}", "Search results for deepseek harness architecture.");
7088
7091
  toolTurn(71, "web_fetch", "{\"url\":\"https://www.deepseek.com/blog/harness-architecture\"}", "# Harness architecture\n\nEverything is a plugin.");
7089
7092
  push({
7090
7093
  type: "turn/start",
@@ -7264,7 +7267,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7264
7267
  };
7265
7268
  case "web_search": return {
7266
7269
  card: "generic",
7267
- title: `Search ${str(args.query)}`,
7270
+ title: `Search ${(Array.isArray(args.queries) ? args.queries.filter((query) => typeof query === "string" && query !== "") : []).join(", ")}`,
7268
7271
  kind: "search",
7269
7272
  rawInput: args
7270
7273
  };
@@ -7357,29 +7360,37 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7357
7360
  }
7358
7361
  }
7359
7362
  /**
7360
- * Fixture parallel of the plan unit's double-event fold: `command/run`
7361
- * records named `plan` with recorded input set the wanted target (`off` →
7362
- * false, else true); `plan/mode` commits and clears it. `wanted` is exposed
7363
- * for the prompt boundary (the fixture's step/start parallel).
7363
+ * Fixture parallel of the plan unit's lifecycle fold. The paired
7364
+ * `command/done` retains successful plan selections and drops failures;
7365
+ * `plan/mode` commits one. `wanted` is exposed for the prompt boundary (the
7366
+ * fixture's step/start parallel).
7364
7367
  */
7365
7368
  function foldPlan(log) {
7366
7369
  let active = false;
7367
7370
  let wanted = null;
7371
+ let running = null;
7368
7372
  for (const event of log) {
7369
7373
  const item = event;
7370
7374
  if (item.type === "command/run" && item.data?.["name"] === "plan") {
7371
7375
  const args = item.data["args"];
7372
7376
  if (typeof args !== "string") continue;
7373
- wanted = args.trim() !== "off";
7377
+ running = {
7378
+ commandId: item.data["commandId"],
7379
+ wanted: args.trim() !== "off"
7380
+ };
7381
+ } else if (item.type === "command/done" && item.data !== void 0 && running !== null && item.data["commandId"] === running.commandId) {
7382
+ wanted = item.data["kind"] === "success" && running.wanted !== active ? running.wanted : null;
7383
+ running = null;
7374
7384
  } else if (item.type === "plan/mode") {
7375
7385
  active = item.data?.["active"] === true;
7376
7386
  wanted = null;
7377
7387
  }
7378
7388
  }
7389
+ const selected = running?.wanted ?? wanted;
7379
7390
  return {
7380
7391
  active,
7381
- pending: wanted !== null && wanted !== active,
7382
- wanted
7392
+ pending: selected !== null && selected !== active,
7393
+ wanted: selected
7383
7394
  };
7384
7395
  }
7385
7396
  /** The plan projection's wire view over the full log. */
@@ -7619,6 +7630,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7619
7630
  maxImagesPerMessage: 20,
7620
7631
  maxMessageImageBytes: 100 * 1024 * 1024,
7621
7632
  maxImagePixels: 4e7,
7633
+ maxImageDimension: 2e3,
7622
7634
  mediaTypes: [
7623
7635
  "image/png",
7624
7636
  "image/jpeg",
@@ -7985,6 +7997,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7985
7997
  let attachedSessions = options.empty ? 0 : 1;
7986
7998
  const wid = (raw) => raw;
7987
7999
  const fixtureEpoch = (/* @__PURE__ */ new Date(Date.now() - 3e5)).toISOString();
8000
+ const FIXTURE_HOME = "/home/fixture";
7988
8001
  const workspaces = options.empty ? [] : [{
7989
8002
  workspaceId: wid("fx-ws-fixture"),
7990
8003
  path: "/tmp/fixture",
@@ -7996,10 +8009,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7996
8009
  ],
7997
8010
  createdAt: fixtureEpoch,
7998
8011
  updatedAt: fixtureEpoch
8012
+ }, {
8013
+ workspaceId: wid("fx-ws-home"),
8014
+ path: `${FIXTURE_HOME}/Documents/project`,
8015
+ title: "project",
8016
+ sessionIds: [],
8017
+ createdAt: fixtureEpoch,
8018
+ updatedAt: fixtureEpoch
7999
8019
  }];
8000
8020
  let nextWorkspace = 1;
8001
8021
  const archivedSessionIds = [];
8002
- const FIXTURE_HOME = "/home/fixture";
8003
8022
  const directoryTree = new Map([
8004
8023
  ["/", ["home"]],
8005
8024
  ["/home", ["fixture"]],
@@ -8227,7 +8246,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8227
8246
  {
8228
8247
  name: "goal",
8229
8248
  description: "set or view the goal for a long-running task",
8230
- input: { hint: "<objective>" }
8249
+ input: {
8250
+ hint: "<objective>",
8251
+ images: true
8252
+ }
8231
8253
  },
8232
8254
  {
8233
8255
  name: "permission",
@@ -8237,17 +8259,59 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8237
8259
  {
8238
8260
  name: "plan",
8239
8261
  description: "Enter or leave plan mode",
8240
- input: { hint: "[off|message]" }
8262
+ input: {
8263
+ hint: "[off|message]",
8264
+ images: true
8265
+ }
8241
8266
  }
8242
8267
  ]
8243
8268
  };
8244
8269
  },
8245
- execute(id, line) {
8270
+ execute(id, line, images = []) {
8246
8271
  const missing = requireGoalSession(id);
8247
8272
  if (missing !== void 0) return missing;
8248
8273
  const match = /^\/(\S+)((?:\s.*)?)$/.exec(line.trim());
8249
8274
  const name = match?.[1];
8250
8275
  const args = match?.[2] ?? "";
8276
+ if (images.length > 0 && name !== void 0 && [
8277
+ "permission",
8278
+ "goal",
8279
+ "compact",
8280
+ "echo",
8281
+ "plan"
8282
+ ].includes(name)) {
8283
+ const rejection = name !== "goal" && name !== "plan" ? `/${name} does not accept image attachments` : name === "goal" && args.trim() === "" ? "Image attachments only accompany a goal objective: /goal <objective> or /goal edit <objective>." : name === "plan" && args.trim() === "off" ? "Image attachments cannot accompany /plan off." : void 0;
8284
+ if (rejection !== void 0) {
8285
+ const commandId = `fx-cmd-${logOf(id).length}`;
8286
+ append(id, {
8287
+ type: "command/run",
8288
+ data: {
8289
+ commandId,
8290
+ name,
8291
+ args,
8292
+ source: { kind: "user" }
8293
+ }
8294
+ });
8295
+ const result = {
8296
+ kind: "error",
8297
+ text: rejection
8298
+ };
8299
+ append(id, {
8300
+ type: "command/done",
8301
+ data: {
8302
+ commandId,
8303
+ ...result
8304
+ }
8305
+ });
8306
+ return {
8307
+ ok: true,
8308
+ value: {
8309
+ commandId,
8310
+ result
8311
+ }
8312
+ };
8313
+ }
8314
+ }
8251
8315
  if (name === "permission") {
8252
8316
  const preset = args.trim();
8253
8317
  const commandId = `fx-cmd-${logOf(id).length}`;
@@ -8409,6 +8473,50 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8409
8473
  activation: projection.goal.phase === "active" ? "armed" : "disarmed"
8410
8474
  });
8411
8475
  /** Canonical fixture implementation of the generated Goal Remote contract. */
8476
+ /** Canonical fixture implementation of the generated reference-discovery Remote contracts. */
8477
+ const referenceRemotes = {
8478
+ files(id, query) {
8479
+ const missing = requireGoalSession(id);
8480
+ if (missing !== void 0) return missing;
8481
+ const needle = query.toLocaleLowerCase();
8482
+ return {
8483
+ ok: true,
8484
+ value: [
8485
+ {
8486
+ path: "notes",
8487
+ kind: "directory"
8488
+ },
8489
+ {
8490
+ path: "README.md",
8491
+ kind: "file"
8492
+ },
8493
+ {
8494
+ path: "notes/demo.txt",
8495
+ kind: "file"
8496
+ }
8497
+ ].filter((item) => item.path.toLocaleLowerCase().includes(needle))
8498
+ };
8499
+ },
8500
+ sessions(id, query) {
8501
+ const missing = requireGoalSession(id);
8502
+ if (missing !== void 0) return missing;
8503
+ const needle = query.toLocaleLowerCase();
8504
+ return {
8505
+ ok: true,
8506
+ value: sessions.filter((item) => item.sessionId !== id).filter((item) => String(item.sessionId).toLocaleLowerCase().includes(needle) || item.cwd?.toLocaleLowerCase().includes(needle) === true).map((item) => {
8507
+ const label = item.sessionId === sid("fx-beta") ? "Fixture child session" : String(item.sessionId);
8508
+ const encoded = btoa(JSON.stringify(item.sessionId)).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
8509
+ return {
8510
+ sessionId: item.sessionId,
8511
+ label,
8512
+ ...item.cwd === void 0 ? {} : { cwd: item.cwd },
8513
+ createdAt: item.updatedAt,
8514
+ mention: `@[${label}](dsh-session:${encoded})`
8515
+ };
8516
+ })
8517
+ };
8518
+ }
8519
+ };
8412
8520
  const goalRemotes = {
8413
8521
  create(id, request) {
8414
8522
  const missing = requireGoalSession(id);
@@ -9204,11 +9312,18 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
9204
9312
  message: `no session ${id}`,
9205
9313
  details: { sessionId: id }
9206
9314
  });
9207
- if (options.rejectPrompt) return err(request, {
9208
- code: "agent-busy",
9209
- message: "fixture: prompt rejected before acceptance",
9210
- details: { reason: "fixture-prompt-rejection" }
9211
- });
9315
+ if (options.rejectPrompt) {
9316
+ if (content.some((block) => block.type === "image")) return err(request, {
9317
+ code: "attachment-error",
9318
+ message: "fixture: image side exceeds the deployment limit",
9319
+ details: { reason: "IMAGE_DIMENSION_TOO_LARGE" }
9320
+ });
9321
+ return err(request, {
9322
+ code: "agent-busy",
9323
+ message: "fixture: prompt rejected before acceptance",
9324
+ details: { reason: "fixture-prompt-rejection" }
9325
+ });
9326
+ }
9212
9327
  summary.updatedAt = Date.now();
9213
9328
  summary.blank = false;
9214
9329
  const userText = content.map((b) => b.type === "text" ? b.text : "").join("");
@@ -9319,6 +9434,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
9319
9434
  version: "0.0.0-fixture",
9320
9435
  cwd: "/tmp/fixture",
9321
9436
  attachedSessions,
9437
+ home: FIXTURE_HOME,
9322
9438
  canOpenPath: true
9323
9439
  }),
9324
9440
  pickDirectory: (request) => ok(request, { path: `${FIXTURE_HOME}/Documents/project` }),
@@ -9831,7 +9947,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
9831
9947
  const sessionId = args.agentId;
9832
9948
  switch (endpoint) {
9833
9949
  case "commands/list": return Promise.resolve(commandRemotes.list(sessionId));
9834
- case "commands/execute": return Promise.resolve(commandRemotes.execute(sessionId, args.line));
9950
+ case "commands/execute": return Promise.resolve(commandRemotes.execute(sessionId, args.line, args.images ?? []));
9951
+ case "fileReferences/list": return Promise.resolve(referenceRemotes.files(sessionId, args.query ?? ""));
9952
+ case "sessionReferenceResolver/candidates": return Promise.resolve(referenceRemotes.sessions(sessionId, args.query ?? ""));
9835
9953
  case "goals/create": return Promise.resolve(goalRemotes.create(sessionId, {
9836
9954
  objective: args.request?.objective,
9837
9955
  ...args.request?.maxGoalRounds === void 0 ? {} : { maxGoalRounds: args.request.maxGoalRounds }
@@ -10083,9 +10201,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
10083
10201
  const ENDPOINT_SEGMENT_PATTERN = /^[A-Za-z0-9_$.-]+$/;
10084
10202
  /**
10085
10203
  * Create the browser-backed generic RPC caller.
10204
+ * @param doFetch - transport override; defaults to the page's global fetch.
10086
10205
  * @returns caller that owns request correlation and response-envelope validation.
10087
10206
  */
10088
- function createWebConnectionRpc() {
10207
+ function createWebConnectionRpc(doFetch) {
10208
+ const send = doFetch ?? ((input, init) => globalThis.fetch(input, init));
10089
10209
  return { async call(channel, endpoint, payload, signal) {
10090
10210
  assertTarget(channel, endpoint);
10091
10211
  const rpcId = RpcId(randomUuid());
@@ -10095,7 +10215,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
10095
10215
  method: endpoint,
10096
10216
  payload
10097
10217
  };
10098
- const response = await globalThis.fetch(new URL(`${channel}/${endpoint}`, resolveBase()), {
10218
+ const response = await send(new URL(`${channel}/${endpoint}`, resolveBase()), {
10099
10219
  method: "POST",
10100
10220
  headers: { "content-type": "application/json" },
10101
10221
  body: JSON.stringify(message),
@@ -10143,8 +10263,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
10143
10263
  function apply(ctx) {
10144
10264
  const pageLocation = typeof location === "undefined" ? void 0 : location;
10145
10265
  const fixtureClient = pageLocation !== void 0 && new URLSearchParams(pageLocation.search).has("fixture") ? new FixtureApiClient() : void 0;
10146
- const api = fixtureClient ?? new WebApiClient();
10147
- const rpc = fixtureClient?.rpc ?? createWebConnectionRpc();
10266
+ const transport = globalThis.__DSH_TRANSPORT__;
10267
+ const api = fixtureClient ?? transport?.createApiClient() ?? new WebApiClient();
10268
+ const rpc = fixtureClient?.rpc ?? createWebConnectionRpc(transport?.fetch);
10148
10269
  let started = false;
10149
10270
  let description;
10150
10271
  const descriptionListeners = /* @__PURE__ */ new Set();
@@ -6,11 +6,13 @@
6
6
  import type { Context } from '@deepseek-ai/cordis';
7
7
  import type { HostDescription, IApiClient } from './api.ts';
8
8
  import { type ConnectionConfig, type ConnectionSinks, type ConnectionState } from './connection.ts';
9
+ import { type RpcFetch } from './rpc.ts';
9
10
  import type { ClientConnectionRpc } from '../rpc.ts';
10
11
  export type { ApiProxy, SessionsApi, SessionSearchItem, SessionSummary, PromptContentPart, HostApi, EventsApi, MuxFrame, HostFrame, ApprovalResponsePayload, QuestionResponsePayload, HistoryEntry, ToolEventView, DirectoryEntry, DirectoryListing, ToolCallView, ToolResultView, WorkspaceApi, WorkspaceId, WorkspaceView, SkillsApi, SkillEntry, ModelCatalogFailure, ModelCatalogModel, ModelProviderGroup, ModelReasoning, MessageId, ModelReasoningEffort, ModelSelection, QueueAction, QueuedInboxItem, SessionModels, SubagentsApi, SubagentAddress, SubagentCatalog, SubagentListEntry, SubagentPromptReceipt, JobView, RpcRequest, RpcResponse, RpcResult, RpcError, RpcErrorCode, ClientRequest, ServerResponse, ServerRequest, ClientResponse, RpcMessage, RpcReceipt, HostDescription, IApiClient, SessionId, SessionEvent, ContentBlock, StreamChunk, GoalsApi, GoalRef, SettingsApi, SettingsNamespaceView, SettingsPathOpView, SettingsSecretView, CredentialsApi, CredentialView, ConfigurableProviderView, DiscoveredModelView, LlmApi, } from './api.ts';
11
12
  export { RpcId, AbstractApiClient, transportError, } from './api.ts';
12
13
  export type { ConnectionConfig, ConnectionSinks, ConnectionState };
13
14
  export type { ClientConnectionRpc } from '../rpc.ts';
15
+ export type { RpcFetch } from './rpc.ts';
14
16
  /** Observable Host description published by each completed connection handshake. */
15
17
  export interface HostDescriptionSource {
16
18
  /** Latest connected-generation description; absent before connect and while reconnecting. */
@@ -20,6 +22,24 @@ export interface HostDescriptionSource {
20
22
  }
21
23
  /** Required services (none — this is the wire root). */
22
24
  export declare const inject: string[];
25
+ /**
26
+ * Carrier override installed on the page global before plugin boot. The served
27
+ * web app leaves it unset and gets HTTP + WebSocket; a shell that owns a
28
+ * different physical transport (the worker preview's postMessage tunnel)
29
+ * provides both halves here instead of forking this plugin.
30
+ */
31
+ export interface ClientTransportHooks {
32
+ /** Build the API carrier: unary calls plus the two downstream event streams. */
33
+ createApiClient(): IApiClient;
34
+ /** Transport for generic unary RPC channels (the Typert gateway). */
35
+ fetch: RpcFetch;
36
+ /**
37
+ * Bundle transport for the module system, present when the carrier also owns
38
+ * bundle bytes (the worker tunnel). Absent in the served web app, whose
39
+ * bundles load over HTTP.
40
+ */
41
+ loadBundle?(url: string): Promise<void>;
42
+ }
23
43
  /**
24
44
  * The ctx.connection service API: the API client plus a one-shot
25
45
  * controller starter (the runtime plugin supplies sinks when its object layer
@@ -30,7 +50,7 @@ export interface ConnectionHandle {
30
50
  readonly api: IApiClient;
31
51
  /** Whether the current page authority is loopback; non-browser contexts default to true. */
32
52
  readonly isLoopback: boolean;
33
- /** Generation-scoped Host facts, including native path-open capability. */
53
+ /** Generation-scoped Host facts, including the account home and native path-open capability. */
34
54
  readonly hostDescription: HostDescriptionSource;
35
55
  /** Generic logical RPC channels over the same Connection transport. */
36
56
  readonly rpc: ClientConnectionRpc;
@@ -1,8 +1,11 @@
1
1
  /** Browser caller for generic Connection unary RPC channels. */
2
2
  import type { ClientConnectionRpc } from '../rpc.ts';
3
+ /** Transport this caller posts through; same signature as the global `fetch`. */
4
+ export type RpcFetch = (input: URL, init: RequestInit) => Promise<Response>;
3
5
  /**
4
6
  * Create the browser-backed generic RPC caller.
7
+ * @param doFetch - transport override; defaults to the page's global fetch.
5
8
  * @returns caller that owns request correlation and response-envelope validation.
6
9
  */
7
- export declare function createWebConnectionRpc(): ClientConnectionRpc;
10
+ export declare function createWebConnectionRpc(doFetch?: RpcFetch): ClientConnectionRpc;
8
11
  //# sourceMappingURL=rpc.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-client-connection",
3
3
  "description": "Wire consumer layer: HTTP-up/WebSocket-down client, ConnectionController dual streams with reconnect, and fixture api",
4
- "version": "0.1.0-rc.7",
4
+ "version": "0.1.1-rc.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -39,13 +39,7 @@
39
39
  "license": "MIT",
40
40
  "dependencies": {
41
41
  "ws": "^8.21.0",
42
- "@deepseek-ai/dsh-attachment": "^0.1.0-rc.7",
43
- "@deepseek-ai/dsh-host-apiproxy": "^0.1.0-rc.7",
44
- "@deepseek-ai/dsh-commands": "^0.1.0-rc.7",
45
- "@deepseek-ai/dsh-session": "^0.1.0-rc.7",
46
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.7",
47
- "@deepseek-ai/schemastery": "^3.18.1",
48
- "@deepseek-ai/dsh-llm": "^0.1.0-rc.7"
42
+ "@deepseek-ai/schemastery": "^3.18.1"
49
43
  },
50
44
  "files": [
51
45
  "lib/index.js",
@@ -54,14 +48,26 @@
54
48
  "lib/types/**/*.d.ts"
55
49
  ],
56
50
  "peerDependencies": {
57
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.7",
58
- "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.7",
59
- "@deepseek-ai/cordis": "^4.0.1"
51
+ "@deepseek-ai/dsh-host-webserver": "^0.1.1-rc.1",
52
+ "@deepseek-ai/dsh-invariants": "^0.1.1-rc.1",
53
+ "@deepseek-ai/cordis": "^4.0.1",
54
+ "@deepseek-ai/dsh-attachment": "^0.1.1-rc.1",
55
+ "@deepseek-ai/dsh-host-apiproxy": "^0.1.1-rc.1",
56
+ "@deepseek-ai/dsh-commands": "^0.1.1-rc.1",
57
+ "@deepseek-ai/dsh-llm": "^0.1.1-rc.1",
58
+ "@deepseek-ai/dsh-tools": "^0.1.1-rc.1",
59
+ "@deepseek-ai/dsh-session": "^0.1.1-rc.1"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/ws": "^8.18.1",
63
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.7",
63
+ "@deepseek-ai/dsh-host-webserver": "^0.1.1-rc.1",
64
+ "@deepseek-ai/dsh-invariants": "^0.1.1-rc.1",
64
65
  "@deepseek-ai/cordis": "^4.0.1",
65
- "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.7"
66
+ "@deepseek-ai/dsh-attachment": "^0.1.1-rc.1",
67
+ "@deepseek-ai/dsh-host-apiproxy": "^0.1.1-rc.1",
68
+ "@deepseek-ai/dsh-llm": "^0.1.1-rc.1",
69
+ "@deepseek-ai/dsh-session": "^0.1.1-rc.1",
70
+ "@deepseek-ai/dsh-tools": "^0.1.1-rc.1",
71
+ "@deepseek-ai/dsh-commands": "^0.1.1-rc.1"
66
72
  }
67
73
  }