Merge pull request #603 from omdsh-dev/feat/dsh-0.1.5-alpha-compat

feat(dsh): 适配 DSH 0.1.5-alpha.1(阶段一:基线 + 契约修复)
This commit is contained in:
Atlas Zeng
2026-09-09 23:51:27 +08:00
committed by GitHub
59 changed files with 1163 additions and 1011 deletions
+2 -2
View File
@@ -131,10 +131,10 @@ jobs:
# Pin the DSH CLI the lane mounts into; the profile's bundle stack
# (dsh-base / dsh-web-app) resolves from this installation. Keep in
# step with the plugin's @deepseek-ai/* peer range when bumping.
# DSH 0.1.2-rc.1 is published under the `next` dist-tag; the lanes
# DSH 0.1.5-alpha.1 is published under the `alpha` dist-tag; the lanes
# speak the slash-only RPC dialect (tests/e2e/host-protocol.ts).
- name: Install dsh CLI (pinned)
run: npm install -g @deepseek-ai/dsh@0.1.2-rc.1
run: npm install -g @deepseek-ai/dsh@0.1.5-alpha.1
# lib/ is gitignored; the tarball is produced from this build.
- name: Build and pack the npm tarball
+6 -6
View File
@@ -19,27 +19,27 @@
「npm 打包 → 真实挂载 → 无头渲染」门禁(证明打包产物在真实 DSH 挂载后不 crash):`pnpm build && pnpm pack` 产 tarball → `scripts/e2e-mount.sh` 装进全新 scratch profile(`dsh plugin --profile web add file:<tarball>`)并启动真实 `dsh web`(keyless,`--port 0`)→ `tests/e2e/mount.e2e.ts`(Playwright)断言 `[data-dsh-better-sidebar]` 挂载、无错误条/pageerror/console 错误,经「+ 菜单」逐个打开内置 tab(含终端懒加载 chunk),再经文件树打开 seed 文件强制加载 editor chunk(`client-editor.js`)。
本地:`pnpm build && pnpm pack && pnpm exec playwright install chromium && pnpm test:mount`。CI 钉 `@deepseek-ai/dsh@0.1.2-rc.1`(`next` dist-tag;peer 下限 `^0.1.2-rc.1`)。e2e spec 命名 `*.e2e.ts` + vitest `exclude` 双保险;**改 `exclude` 必须保留默认排除项**(exclude 整体替换默认值)。
本地:`pnpm build && pnpm pack && pnpm exec playwright install chromium && pnpm test:mount`。CI 钉 `@deepseek-ai/dsh@0.1.5-alpha.1`(`alpha` dist-tag;peer 下限 `^0.1.5-alpha.1`)。e2e spec 命名 `*.e2e.ts` + vitest `exclude` 双保险;**改 `exclude` 必须保留默认排除项**(exclude 整体替换默认值)。
---
## 3. DSH 0.1.2 适配要点(0.1.2-rc.1+ 基线)
## 3. DSH 0.1.5 适配要点(0.1.5-alpha.1+ 基线)
**v0.18.0 起插件毕业正式版(npm `latest`),仅支持 DSH 0.1.2-rc.1+**(peer 下限 `^0.1.2-rc.1`,CI 钉 `@deepseek-ai/dsh@0.1.2-rc.1`,DSH rc.x 发在 `next` dist-tag)。v0.18.0-alpha.0 进入 alpha 通道时已删除对 0.1.0-rc.8 ~ 0.1.1-rc.2 的兼容层——双方言 RPC(点分回退)、MarkdownText 双形状 labels、`dsh-client-runtime` externals/inject 残留行;stable DSH 用户请固定装 v0.17.1(npm 上的旧 latest,0.1.2-alpha.x 宿主继续用 npm `alpha` 的 v0.18.0-alpha.0)。发版:release.yml 按版本号是否含 `-` 自动选 `alpha`/`latest` dist-tag。
**v0.19.0-alpha.0 起仅支持 DSH 0.1.5-alpha.1+**(peer 下限 `^0.1.5-alpha.1`,CI 钉 `@deepseek-ai/dsh@0.1.5-alpha.1`,DSH alpha 线发在 `alpha` dist-tag)。0.1.2-rc.1 稳定线用户继续用 v0.18.x(npm `latest`)——0.1.5 的会话事件模型与文件打开漏斗都变了,插件不再对 0.1.2 做运行时兼容。发版:release.yml 按版本号是否含 `-` 自动选 `alpha`/`latest` dist-tag。
宿主契约(均经真机挂载冒烟 14/14 验证):
1. **一次性 token 鉴权**:就绪行 `dsh web: http://127.0.0.1:<port>/?token=<43字符>`(导航换签名 cookie,干净 URL 401)。`e2e-mount.sh` 的 URL grep 必须延伸到空白(`[^ ]*`,在 `/` 截断丢 token);e2e 统一走 `tests/e2e/host.ts`(token 必选:`parseLaunchUrl` 对裸 origin 直接抛错),带 stamps 导航走 `gotoPage()`(先 addCookies 再直达——token 换 cookie 的 303 会丢弃同 URL 其它 query 参数)。插件 `/sidebar/*` 路由不受影响,同源 fetch 照旧。
2. **Remote gateway 斜杠 RPC(唯一方言)**:`POST /api/workspace/create`,payload 恰为 `{args: {...}}`,**args 按控制器 TS 参数名包装**(`workspace/create`、`session/create` → `{args:{request:{...}}}`;`session/list` 参数名 `_request` **不可省略**——`{}` 也被拒 `args fields do not match the descriptor`);envelope `method` 与路径一致,点分路径 404。请求由 `tests/e2e/host-protocol.ts` 的 `rpcAttempt` 构造、`tests/e2e-host-protocol.spec.ts` 锁定;要调新方法先在真机验参数名再进 `RPC_ARGS_KEY`。
3. **`MarkdownText` labels 嵌套契约**:必填 `labels: { code: { copyLabel, copiedLabel }, footnotes }`(漏传回退硬编码中文)。四个渲染点(mermaid.tsx / MarkdownHtml.tsx / TextEditor.tsx / SideChatView.tsx)统一走 `src/client/markdown-labels.tsx` 的 `markdownTextProps()`。
4. **侧边对话转录走自有路由,不碰客户端宿主 RPC**:`ctx.connection.api`(含 `sessions.history`)在 alpha.1 整体移除,继任 `session/follow|page` 又对 `origin:'subagent'` 会话强制 subagent 地址(普通 `{kind:'session'}` 被 `agent-busy` 拒)且分页 `throughSeq` 不得超当前游标。转录因此由 **`sidechat.events`** 插件路由供给(`src/sidechat-routes.ts`:live 读 `agent.session.snapshotEvents()`(0.1.2-alpha.4 起的按需读 API,前身 `Session.events` 属性已删)、冷读 `sessionPersistence.inspect`,服务端 `session/end-seed` 切割 + `afterSeq` 增量);`ctx.connection` 镜像与 inject 已删。设计见 [docs/plans/2026-08-20-sidechat-tab-design.md](docs/plans/2026-08-20-sidechat-tab-design.md) §10。
4. **侧边对话转录走自有路由,不碰客户端宿主 RPC**:`ctx.connection.api`(含 `sessions.history`)在 alpha.1 整体移除,继任 `session/follow|page` 又对 `origin:'subagent'` 会话强制 subagent 地址(普通 `{kind:'session'}` 被 `agent-busy` 拒)且分页 `throughSeq` 不得超当前游标。转录因此由 **`sidechat.events`** 插件路由供给(`src/sidechat-routes.ts`:live 读 `agent.session.snapshotEvents()`、冷读 `sessionPersistence.inspect`,服务端 `session/end-seed` 切割 + `afterSeq` 增量);`ctx.connection` 镜像与 inject 已删。**0.1.5 起实时增量不在日志里**:`assistant/chunk` 事件被删除,进行中的模型增量改由 `agent/assistant-stream` 瞬时帧发布(`start` / `chunk` / `end`,不入会话日志),结算时才落 `assistant/message`(内嵌 `stream`)或 `assistant/attempt`(失败尝试,内嵌 `stream`)。插件在宿主侧 `src/assistant-live.ts` 折叠这些帧成有界缓冲,`sidechat.events` 的 `live` 字段按「当前 attempt 全量、每次轮询替换」下发(不是增量),转录映射与继承快照都读它。设计见 [docs/plans/2026-08-20-sidechat-tab-design.md](docs/plans/2026-08-20-sidechat-tab-design.md) §10。
5. **`dsh-settings` 无运行时 `settingsNamespace`**:命名空间合法性校验转为编译期模板字面量 `SettingsNamespaceInput`(小写字母开头 + `[a-z0-9-]` 尾部),`'dsh-better-sidebar'` 字面量直接过——宿主侧直接传常量(`src/index.ts` 的 settings inject)。
6. **`dsh-subagent` 的 `SUBAGENT_DESCRIPTOR_VERSION` 2 → 3**:sidechat 种子的 `subagent/descriptor` 版本由宿主包盖章,插件不硬编码;测试断言跟随常量(`tests/sidechat-routes.spec.ts`),勿钉字面量。
7. **`@deepseek-ai/dsh-client-runtime` 包已消亡**(继任 seed 是裸名 `dsh-client-store`,无 `/client` 子路径):peerDependencies、devDependencies、`dsh.client.inject`、chunk externals 白名单(`src/client/chunk-loader.ts` / `tsdown.config.ts` / `tests/chunk-loader.spec.ts` / `tests/manifest-consistency.spec.ts` 四处同步)均已无该条目。
8. **e2e scratch profile 的 `minimumReleaseAgeExclude` 含 `'@deepseek-ai/*'`**(`scripts/e2e-mount.sh` / `e2e-aggregate-mount.sh`,与仓库根 `pnpm-workspace.yaml` 同策):alpha 版本常在发布后 24h 内跑 lane,pnpm 11 的 `minimumReleaseAge` 默认会拒装新鲜包。
9. **插件开发树的 dsh-* 传递 peer 需提升为 devDependencies**(alpha.3 首见):`dsh-subagent` 等 npm 包把 `dsh-attachment` 等 dsh-* 姊妹包全部声明为 peerDependencies(由宿主 bundle 树统一提供,宿主侧无此问题),插件仓库若只直接依赖其中一部分,其余 peer 在 pnpm 下会解析到树上残留的旧版——如 `dsh-attachment@0.1.1-rc.1` 缺 `admitPromptContent` 导出、`dsh-subagent` 产物 import 它时测试加载即崩。因此 devDependencies 需涵盖 dev 树实际触达的全部 peer(attachment / code-runtime / scope / session-projection / system-prompt / user-approval / util-time 七个即为此提升,与直接依赖同款精确钉版);`@deepseek-ai/cordis` peer 自 alpha.3 起要求 `^4.0.2`(上游全线 peer 已升)。适配新 alpha 版本时先跑 `pnpm peers check`,把新失配的传递 peer 一并提升进 devDependencies。例外:**`dsh-client-locale` 的 peer/devDep 允许落后于基线**(alpha.5 时上游停在 0.1.2-alpha.3 未发新版)——peer 下限 `^0.1.2-alpha.3` 天然容纳同 tuple 的 alpha.5 运行时,此时保持旧钉版并在 `pnpm-workspace.yaml` 注明,待上游发版再追平;rc.1 起上游恢复发版,peer/devDep 已与基线一并钉 `0.1.2-rc.1`,该例外消除。
10. **聊天文件打开漏斗是 `remote.session.openWorkspacePath`**(`ctx.workspaces.openPath` 已随 alpha 删除,`IWorkspaces` 无此方法):「聊天区文件在侧边栏打开」拦截在 `ctx.inject(['remote.session'], …)` 内以 **defineProperty 数据属性遮蔽**该命名空间方法(gateway client 的方法是 accessor 属性、异步挂载、contribution 重挂载时服务重建——inject 回调重跑即自愈);实现见 `src/client/openpath-intercept.ts`,设计见 [docs/plans/2026-08-31-openpath-intercept-alpha-design.md](docs/plans/2026-08-31-openpath-intercept-alpha-design.md)。
11. **自定义种子必须带 fork 标记对(`meta.isSeeded: true` + `inheritedEventCount`)**:dsh-session 契约「只给 seed 不标 isSeeded,种子算重放历史而非继承前缀」——缺标记时 `Session.ownEvents()` 含整个种子,子会话 `Inbox` 构造器重放种子里的 `agent/inbox/spliced`,**继承父会话切割时刻未领取的 inbox 输入**(排队用户消息 next-turn、长回合中工具结果上下文/steering next-step——后者即「上下文很长时侧边对话先把之前的 User msg 发出去」的根因,幽灵消息排在 boundary 之前最先发给模型)。宿主 `session.fork`(api-session-controller)的调用即规范形态;回归由 `tests/sidechat-seed-validation.spec.ts`(真实 `Session.create` + `Inbox`)守护。0.1.2-rc.1 复评:无更优雅的 sidechat 宿主 API(`ContinuableStartSpec` 仍无 seed 字段),`AgentRegistry.create` 接缝补齐标记即为规范用法。
10. **聊天文件打开漏斗已改为原生右侧栏**:0.1.5 客户端不再有任何 `remote.session.openWorkspacePath` 调用——聊天里一切文件打开(工具行 / 产物行 / 正文提及 / 行内代码路径)统一走 `ctx.sidebarRight.openResource(fileAddressFor(sessionId, cwd, path))`(`packages/client/ui-chat/src/client/apply.ts` 是唯一调用点)。插件此前的 `openpath-intercept`(defineProperty 遮蔽 `remote.session` 方法)随之删除:0.1.5 上它是死代码,且会劫持宿主的「在外部应用打开」语义。文件当前落到原生栏的内置 `text` 预览;阶段二由插件注册 `dsh-resource://file/**` 类型(默认 `extension` 带,压过 `text` 的 `fallback`)接管。
11. **自定义种子必须带 fork 标记对(`meta.isSeeded: true` + `inheritedEventCount`)**:dsh-session 契约「只给 seed 不标 isSeeded,种子算重放历史而非继承前缀」——缺标记时 `Session.ownEvents()` 含整个种子,子会话的 inbox 折(`inboxProjectionDefinition`,0.1.5 起 `@deepseek-ai/dsh-agent` 只导出 `Inbox` 接口,实现是该投影)重放种子里的 `agent/inbox/spliced`,**继承父会话切割时刻未领取的 inbox 输入**(排队用户消息 next-turn、长回合中工具结果上下文/steering next-step——后者即「上下文很长时侧边对话先把之前的 User msg 发出去」的根因,幽灵消息排在 boundary 之前最先发给模型)。宿主 `session.fork`(api-session-controller)的调用即规范形态;回归由 `tests/sidechat-seed-validation.spec.ts`(真实 `Session.create` + 对 `ownEvents()` 跑 inbox 折)守护。0.1.5 复评:无更优雅的 sidechat 宿主 API(`ContinuableStartSpec` 仍无 seed 字段),`AgentRegistry.create` 接缝补齐标记即为规范用法;`Session.create` 的 header `version` 必须用 `SESSION_FORMAT_VERSION`(0.1.5 是 `3` 字面量类型),勿钉 `0`。
---
+2 -2
View File
@@ -2,7 +2,7 @@
> 面向 **消费插件开发者**:如何让你的插件向 better-sidebar 注册新的侧边栏页面(tab)和文件类型预览器。
>
> 适用版本:**v0.4.0+**(`ctx.betterSidebar` 服务);声明式设置 **v0.4.1+**;text/number 设置行 **v0.11.0+**;badge/生命周期/定向打开/插件设置/版本探测 **v0.12.0+**;select 设置行(`settingSelect`)与外链认领(`urlTarget`)**v0.13.0+**;统一 `@deepseek-ai/cordis` 类型基底 **v0.15.2+**;自由窗口(`floatWindows`)**v0.16.0+**;终端固定(pin)**v0.17.0+**。当前版本 **v0.18.1**(正式版,仅支持 DSH 0.1.2-rc.1+;旧宿主 stable 线为 v0.17.1)。
> 适用版本:**v0.4.0+**(`ctx.betterSidebar` 服务);声明式设置 **v0.4.1+**;text/number 设置行 **v0.11.0+**;badge/生命周期/定向打开/插件设置/版本探测 **v0.12.0+**;select 设置行(`settingSelect`)与外链认领(`urlTarget`)**v0.13.0+**;统一 `@deepseek-ai/cordis` 类型基底 **v0.15.2+**;自由窗口(`floatWindows`)**v0.16.0+**;终端固定(pin)**v0.17.0+**。当前版本 **v0.19.0-alpha.0**(alpha 通道,仅支持 DSH **0.1.5-alpha.1+**;0.1.2-rc.1 稳定线请用 v0.18.x)。
> 权威代码:`src/client/service.ts`(服务实现)、`src/client/builtins/`(内置 8 tab + 6 viewer 参考实现)、`lib/types/client/service.d.ts`(类型声明)。
> 仓库开发规则(硬约束 / CI / 发版)见 [AGENTS.md](../AGENTS.md)。
@@ -730,7 +730,7 @@ ctx.effect(() =>
| **i18n 跟随** | 文案跟随 DSH `ctx.locale`(词典在 `betterSidebar` 命名空间;Host-backed `locale.preference` 优先于浏览器语言并实时切换;缺失回退浏览器)。消费插件**不要**依赖内部 `t()`——标题传字符串或 `() => string` |
| **第三语言覆盖(ja 等)** | 可选 peer `@huanlin/dsh-plugin-better-locale`(optional)提供 ja/ko 覆盖,**借用 DSH 英文槽位**(仅 DSH=en 时生效,zh 下惰性)。经 `ctx.get('betterLocale')` 注入 `t()`;未安装整段 no-op |
| **懒加载 chunk** | 重依赖(xterm/CodeMirror)在独立 bundle(`lib/client-<name>.js`),经 `/sidebar/bundle` 按需下发;factory 赋到 `globalThis.__dshChunks__[<name>]`,由 `src/client/chunk-loader.ts` 物化,**不经** `__ModuleLoader__`。对消费插件透明 |
| **聊天文件打开漏斗(alpha 宿主)** | 聊天里一切文件打开(工具行 / 产物行 / 正文提及 / 行内代码路径)汇入 `ctx.remote.session.openWorkspacePath`(Typert remote 命名空间,cordis 服务 key `remote.session`,方法为 **accessor 属性**、异步挂载)。better-sidebar 的「聊天区文件在侧边栏打开」即在 `ctx.inject(['remote.session'], …)` 内以 defineProperty 遮蔽该方法(`src/client/openpath-intercept.ts`)。你的插件若要观测/旁路聊天文件打开,走同一服务;不要假设 pre-alpha 的 `ctx.workspaces.openPath` 存在(alpha 的 `IWorkspaces` 已无此方法) |
| **聊天文件打开漏斗(0.1.5 宿主)** | 聊天里一切文件打开(工具行 / 产物行 / 正文提及 / 行内代码路径)统一走 DSH 原生右侧栏的 `ctx.sidebarRight.openResource(fileAddressFor(sessionId, cwd, path))`(`packages/client/ui-chat/src/client/apply.ts` 是唯一调用点);`remote.session.openWorkspacePath` 在 0.1.5 客户端**已无调用者**,better-sidebar 的 openpath 拦截随之删除。你的插件若要观测/旁路聊天文件打开,注册原生 tab 类型认领 `dsh-resource://file/**`(见 [AGENTS.md §3](AGENTS.md) 第 10 条);不要假设 `ctx.workspaces.openPath`(已删除)或旧 `remote.session` 漏斗存在 |
---
+2 -2
View File
@@ -1,10 +1,10 @@
{
"id": "dsh-external/dsh-better-sidebar",
"version": "0.18.1",
"version": "0.19.0-alpha.0",
"main": "./lib/index.js",
"description": "VSCode 风格右侧侧边栏:文件资源管理器 / 编辑器 / 终端 / Git / 浏览器,按会话隔离。暴露服务供其他插件注册侧边栏页面和文件预览器",
"engines": {
"dsh": ">=0.1.2-rc.1"
"dsh": ">=0.1.5-alpha.1"
},
"contributes": {
"tools": [],
+41 -34
View File
@@ -1,6 +1,6 @@
{
"name": "dsh-better-sidebar",
"version": "0.18.1",
"version": "0.19.0-alpha.0",
"description": "DSH web plugin: a VSCode-like right sidebar (explorer / editor / terminal / git / browser), isolated per conversation session. Exposes a service for other plugins to register sidebar tabs and file viewers.",
"type": "module",
"repository": {
@@ -49,6 +49,7 @@
"@deepseek-ai/dsh-client-locale",
"@deepseek-ai/dsh-client-ui-slots",
"@deepseek-ai/dsh-client-ui-conversation",
"@deepseek-ai/dsh-client-ui-sidebar-right",
"@deepseek-ai/dsh-client-modules"
],
"platform": "web"
@@ -88,19 +89,20 @@
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/cordis": "^4.0.2",
"@deepseek-ai/dsh-agent": "^0.1.2-rc.1",
"@deepseek-ai/dsh-client-locale": "^0.1.2-rc.1",
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.2-rc.1",
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.2-rc.1",
"@deepseek-ai/dsh-client-ui-settings": "^0.1.2-rc.1",
"@deepseek-ai/dsh-client-ui-slots": "^0.1.2-rc.1",
"@deepseek-ai/dsh-host-webserver": "^0.1.2-rc.1",
"@deepseek-ai/dsh-invariants": "^0.1.2-rc.1",
"@deepseek-ai/dsh-llm": "^0.1.2-rc.1",
"@deepseek-ai/dsh-session": "^0.1.2-rc.1",
"@deepseek-ai/dsh-settings": "^0.1.2-rc.1",
"@deepseek-ai/dsh-subagent": "^0.1.2-rc.1",
"@deepseek-ai/dsh-tools": "^0.1.2-rc.1",
"@deepseek-ai/dsh-agent": "^0.1.5-alpha.1",
"@deepseek-ai/dsh-client-locale": "^0.1.5-alpha.1",
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-alpha.1",
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-alpha.1",
"@deepseek-ai/dsh-client-ui-settings": "^0.1.5-alpha.1",
"@deepseek-ai/dsh-client-ui-sidebar-right": "^0.1.5-alpha.1",
"@deepseek-ai/dsh-client-ui-slots": "^0.1.5-alpha.1",
"@deepseek-ai/dsh-host-webserver": "^0.1.5-alpha.1",
"@deepseek-ai/dsh-invariants": "^0.1.5-alpha.1",
"@deepseek-ai/dsh-llm": "^0.1.5-alpha.1",
"@deepseek-ai/dsh-session": "^0.1.5-alpha.1",
"@deepseek-ai/dsh-settings": "^0.1.5-alpha.1",
"@deepseek-ai/dsh-subagent": "^0.1.5-alpha.1",
"@deepseek-ai/dsh-tools": "^0.1.5-alpha.1",
"@huanlin/dsh-plugin-better-locale": "^0.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
@@ -108,6 +110,9 @@
"peerDependenciesMeta": {
"@huanlin/dsh-plugin-better-locale": {
"optional": true
},
"@deepseek-ai/dsh-client-ui-sidebar-right": {
"optional": true
}
},
"dependencies": {
@@ -144,26 +149,28 @@
},
"devDependencies": {
"@cordisjs/plugin-loader": "^1.0.0-rc.5",
"@deepseek-ai/dsh-agent": "0.1.2-rc.1",
"@deepseek-ai/dsh-attachment": "0.1.2-rc.1",
"@deepseek-ai/dsh-client-locale": "0.1.2-rc.1",
"@deepseek-ai/dsh-client-ui-conversation": "0.1.2-rc.1",
"@deepseek-ai/dsh-client-ui-primitives": "0.1.2-rc.1",
"@deepseek-ai/dsh-client-ui-settings": "0.1.2-rc.1",
"@deepseek-ai/dsh-client-ui-slots": "0.1.2-rc.1",
"@deepseek-ai/dsh-code-runtime": "0.1.2-rc.1",
"@deepseek-ai/dsh-host-webserver": "0.1.2-rc.1",
"@deepseek-ai/dsh-invariants": "0.1.2-rc.1",
"@deepseek-ai/dsh-llm": "0.1.2-rc.1",
"@deepseek-ai/dsh-scope": "0.1.2-rc.1",
"@deepseek-ai/dsh-session": "0.1.2-rc.1",
"@deepseek-ai/dsh-session-projection": "0.1.2-rc.1",
"@deepseek-ai/dsh-settings": "0.1.2-rc.1",
"@deepseek-ai/dsh-subagent": "0.1.2-rc.1",
"@deepseek-ai/dsh-system-prompt": "0.1.2-rc.1",
"@deepseek-ai/dsh-tools": "0.1.2-rc.1",
"@deepseek-ai/dsh-user-approval": "0.1.2-rc.1",
"@deepseek-ai/dsh-util-time": "0.1.2-rc.1",
"@deepseek-ai/dsh-agent": "0.1.5-alpha.1",
"@deepseek-ai/dsh-agent-loop": "0.1.5-alpha.1",
"@deepseek-ai/dsh-attachment": "0.1.5-alpha.1",
"@deepseek-ai/dsh-client-locale": "0.1.5-alpha.1",
"@deepseek-ai/dsh-client-ui-conversation": "0.1.5-alpha.1",
"@deepseek-ai/dsh-client-ui-primitives": "0.1.5-alpha.1",
"@deepseek-ai/dsh-client-ui-settings": "0.1.5-alpha.1",
"@deepseek-ai/dsh-client-ui-sidebar-right": "0.1.5-alpha.1",
"@deepseek-ai/dsh-client-ui-slots": "0.1.5-alpha.1",
"@deepseek-ai/dsh-code-runtime": "0.1.5-alpha.1",
"@deepseek-ai/dsh-host-webserver": "0.1.5-alpha.1",
"@deepseek-ai/dsh-invariants": "0.1.5-alpha.1",
"@deepseek-ai/dsh-llm": "0.1.5-alpha.1",
"@deepseek-ai/dsh-scope": "0.1.5-alpha.1",
"@deepseek-ai/dsh-session": "0.1.5-alpha.1",
"@deepseek-ai/dsh-session-projection": "0.1.5-alpha.1",
"@deepseek-ai/dsh-settings": "0.1.5-alpha.1",
"@deepseek-ai/dsh-subagent": "0.1.5-alpha.1",
"@deepseek-ai/dsh-system-prompt": "0.1.5-alpha.1",
"@deepseek-ai/dsh-tools": "0.1.5-alpha.1",
"@deepseek-ai/dsh-user-approval": "0.1.5-alpha.1",
"@deepseek-ai/dsh-util-time": "0.1.5-alpha.1",
"@huanlin/dsh-plugin-better-locale": "^0.1.0",
"@playwright/test": "^1.62.1",
"@types/node": "^24.0.0",
+301 -230
View File
@@ -106,71 +106,77 @@ importers:
specifier: ^1.0.0-rc.5
version: 1.0.0-rc.5(cordis@4.0.0-rc.8)
'@deepseek-ai/dsh-agent':
specifier: 0.1.2-rc.1
version: 0.1.2-rc.1(6f833cf90b9dd3f0d529840b1aebfb48)
specifier: 0.1.5-alpha.1
version: 0.1.5-alpha.1(288d2f4271bc6bef491bb5a5c084f12c)
'@deepseek-ai/dsh-agent-loop':
specifier: 0.1.5-alpha.1
version: 0.1.5-alpha.1(3d62ae7d0bb4adb238d8dfb4da37e073)
'@deepseek-ai/dsh-attachment':
specifier: 0.1.2-rc.1
version: 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-brand@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))
specifier: 0.1.5-alpha.1
version: 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-brand@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))
'@deepseek-ai/dsh-client-locale':
specifier: 0.1.2-rc.1
version: 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
specifier: 0.1.5-alpha.1
version: 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-client-ui-conversation':
specifier: 0.1.2-rc.1
version: 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(typescript@5.6.2)
specifier: 0.1.5-alpha.1
version: 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(typescript@5.6.2)
'@deepseek-ai/dsh-client-ui-primitives':
specifier: 0.1.2-rc.1
version: 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
specifier: 0.1.5-alpha.1
version: 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-client-ui-settings':
specifier: 0.1.2-rc.1
version: 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
specifier: 0.1.5-alpha.1
version: 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-client-ui-sidebar-right':
specifier: 0.1.5-alpha.1
version: 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-client-ui-slots':
specifier: 0.1.2-rc.1
version: 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
specifier: 0.1.5-alpha.1
version: 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-code-runtime':
specifier: 0.1.2-rc.1
version: 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
specifier: 0.1.5-alpha.1
version: 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-host-webserver':
specifier: 0.1.2-rc.1
version: 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
specifier: 0.1.5-alpha.1
version: 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-invariants':
specifier: 0.1.2-rc.1
version: 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
specifier: 0.1.5-alpha.1
version: 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-llm':
specifier: 0.1.2-rc.1
version: 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
specifier: 0.1.5-alpha.1
version: 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-scope':
specifier: 0.1.2-rc.1
version: 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))
specifier: 0.1.5-alpha.1
version: 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))
'@deepseek-ai/dsh-session':
specifier: 0.1.2-rc.1
version: 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)))
specifier: 0.1.5-alpha.1
version: 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)))
'@deepseek-ai/dsh-session-projection':
specifier: 0.1.2-rc.1
version: 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-session@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))))
specifier: 0.1.5-alpha.1
version: 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-session@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))))
'@deepseek-ai/dsh-settings':
specifier: 0.1.2-rc.1
version: 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-brand@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-session@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))))(@deepseek-ai/schemastery@3.18.2)
specifier: 0.1.5-alpha.1
version: 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-brand@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-session@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))))(@deepseek-ai/schemastery@3.18.2)
'@deepseek-ai/dsh-subagent':
specifier: 0.1.2-rc.1
version: 0.1.2-rc.1(6241f70df87ac98c48965b657a1f7a60)
specifier: 0.1.5-alpha.1
version: 0.1.5-alpha.1(0904ccd91d8f9ab5907c34d0b1c82b43)
'@deepseek-ai/dsh-system-prompt':
specifier: 0.1.2-rc.1
version: 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-llm@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)))
specifier: 0.1.5-alpha.1
version: 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-llm@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)))
'@deepseek-ai/dsh-tools':
specifier: 0.1.2-rc.1
version: 0.1.2-rc.1(de5e1ff2d608fbe22667925341ad1dfc)
specifier: 0.1.5-alpha.1
version: 0.1.5-alpha.1(239eb910b4462b7c15b6b44661457a15)
'@deepseek-ai/dsh-user-approval':
specifier: 0.1.2-rc.1
version: 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-agent@0.1.2-rc.1(6f833cf90b9dd3f0d529840b1aebfb48))(@deepseek-ai/dsh-brand@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-llm@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)))(@deepseek-ai/dsh-session@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))))(@deepseek-ai/dsh-system-prompt@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-llm@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))))
specifier: 0.1.5-alpha.1
version: 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-agent@0.1.5-alpha.1(288d2f4271bc6bef491bb5a5c084f12c))(@deepseek-ai/dsh-brand@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-llm@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)))(@deepseek-ai/dsh-session@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))))(@deepseek-ai/dsh-system-prompt@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-llm@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))))
'@deepseek-ai/dsh-util-time':
specifier: 0.1.2-rc.1
version: 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
specifier: 0.1.5-alpha.1
version: 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@eslint/js':
specifier: ^10.0.1
version: 10.0.1(eslint@10.9.1)
'@huanlin/dsh-plugin-better-locale':
specifier: ^0.1.0
version: 0.1.0(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-client-locale@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-client-ui-primitives@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-client-ui-settings@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-client-ui-slots@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
version: 0.1.0(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-client-locale@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-client-ui-primitives@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-client-ui-settings@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-client-ui-slots@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@playwright/test':
specifier: ^1.62.1
version: 1.62.1
@@ -458,124 +464,153 @@ packages:
'@deepseek-ai/cosmokit@1.8.3':
resolution: {integrity: sha512-qBo+ronVM6Eu2WNVJXi8JcMiqZ19T9BRIpV+5qJUFPXjGH/Z0QKcQMC/IZJ7L394YTOtJgcovbk9qP0w2GsBXQ==}
'@deepseek-ai/dsh-agent@0.1.2-rc.1':
resolution: {integrity: sha512-lfaqN34vUCWvbn1kJVHrhfJ6Dvt1HDHCm33ZCpmKkl07/5q6FxWqVv6rOdVX5QnM/9xz/uYiN0nQwUtBg4+Skg==}
'@deepseek-ai/dsh-agent-loop@0.1.5-alpha.1':
resolution: {integrity: sha512-FpfSUfeyONRfVbmIC82dYGPL4G5T+9sJDd0OIqMqv8gH2M7smbILE/HOnKo/eHC29ce/YDSvL3etWr6g0PGc8w==}
peerDependencies:
'@deepseek-ai/cordis': ^4.0.2
'@deepseek-ai/dsh-invariants': ^0.1.2-rc.1
'@deepseek-ai/dsh-llm': ^0.1.2-rc.1
'@deepseek-ai/dsh-scope': ^0.1.2-rc.1
'@deepseek-ai/dsh-session': ^0.1.2-rc.1
'@deepseek-ai/dsh-session-projection': ^0.1.2-rc.1
'@deepseek-ai/dsh-system-prompt': ^0.1.2-rc.1
'@deepseek-ai/dsh-typert-protocol': ^0.1.2-rc.1
'@deepseek-ai/dsh-agent': ^0.1.5-alpha.1
'@deepseek-ai/dsh-invariants': ^0.1.5-alpha.1
'@deepseek-ai/dsh-llm': ^0.1.5-alpha.1
'@deepseek-ai/dsh-scope': ^0.1.5-alpha.1
'@deepseek-ai/dsh-session': ^0.1.5-alpha.1
'@deepseek-ai/dsh-session-persistence': ^0.1.5-alpha.1
'@deepseek-ai/dsh-session-projection': ^0.1.5-alpha.1
'@deepseek-ai/dsh-settings': ^0.1.5-alpha.1
'@deepseek-ai/dsh-system-prompt': ^0.1.5-alpha.1
'@deepseek-ai/dsh-tools': ^0.1.5-alpha.1
'@deepseek-ai/dsh-attachment@0.1.2-rc.1':
resolution: {integrity: sha512-QISKUjEITusLvAkqPLRn70xDUt+GjjY41pISjtPKYcZ4EGmayzhgZ3VJNYEcC+rbaF44hs2o/0VhOwHA7lyxjw==}
'@deepseek-ai/dsh-agent@0.1.5-alpha.1':
resolution: {integrity: sha512-Klimoo970OFP2/5sk6MaycY5l4VHeK8dN5jEIL3l9EBerr4QnoDOZMmLVkrx3cSk9OCm4KfQWrv8Gkm1HJ3fUg==}
peerDependencies:
'@deepseek-ai/cordis': ^4.0.2
'@deepseek-ai/dsh-brand': ^0.1.2-rc.1
'@deepseek-ai/dsh-invariants': ^0.1.5-alpha.1
'@deepseek-ai/dsh-llm': ^0.1.5-alpha.1
'@deepseek-ai/dsh-scope': ^0.1.5-alpha.1
'@deepseek-ai/dsh-session': ^0.1.5-alpha.1
'@deepseek-ai/dsh-session-projection': ^0.1.5-alpha.1
'@deepseek-ai/dsh-system-prompt': ^0.1.5-alpha.1
'@deepseek-ai/dsh-typert-protocol': ^0.1.5-alpha.1
'@deepseek-ai/dsh-util-values': ^0.1.5-alpha.1
'@deepseek-ai/dsh-brand@0.1.2-rc.1':
resolution: {integrity: sha512-y2RHIag3kzLdY8kTBuSUSTsKzWPscJrysNObXTiNM7stXLpa3jXpshsB1caqGzNx50vYcmHeN1gFVECgx3NZHg==}
'@deepseek-ai/dsh-attachment@0.1.5-alpha.1':
resolution: {integrity: sha512-2XBsE+J4zSFEC09OhCXVD1BPWR3KIdVlM7lKcaHBEuUHwqewS/g16DbM/yAmFUaigjXS9zuQFThCAv7QYb1JhA==}
peerDependencies:
'@deepseek-ai/cordis': ^4.0.2
'@deepseek-ai/dsh-brand': ^0.1.5-alpha.1
'@deepseek-ai/dsh-brand@0.1.5-alpha.1':
resolution: {integrity: sha512-R7CADJl81Q5PMeIKAQfzP9ZRauLw7niiSZDUVqZfxAkNMlGbKIqdXtUIMuxOv1TQUW8fuqqqPVSDo7m+QtKQxA==}
peerDependencies:
'@deepseek-ai/cordis': ^4.0.2
'@deepseek-ai/dsh-client-locale@0.1.2-rc.1':
resolution: {integrity: sha512-p34i5WcZ0+b77RKEatGLc6Rh4OgJJmc15kWbBqtCtSI1BxpwZxjDzoNIMTTS3jDGUQqfEd9X7Js9D4uNJDUewg==}
'@deepseek-ai/dsh-client-locale@0.1.5-alpha.1':
resolution: {integrity: sha512-Dz7VSN8nr59xLR+rvIlYXlP1A47l1XOgFFAHpNXnxIXupI8VdXb1GIByI5WdEdrZ+J4DmoBJZggNkIrVbjXqRg==}
peerDependencies:
'@deepseek-ai/cordis': ^4.0.2
'@deepseek-ai/dsh-client-ui-conversation@0.1.2-rc.1':
resolution: {integrity: sha512-PDrV7twPfmZE5TjdvdVVZMHOla36wCojRC8GJBUp1cdXsWUP+Omk10gxYbH1g5TQPdAWgSL+GktdjondSXMSNQ==}
'@deepseek-ai/dsh-client-ui-conversation@0.1.5-alpha.1':
resolution: {integrity: sha512-0UiN8TEuVKrnr10lIz8T8pkp9wK/gfMap6HEnP040KB50mceTTO3y00umCsz3CeS61gYWS6DwkaNBY9th3wq8Q==}
peerDependencies:
'@deepseek-ai/cordis': ^4.0.2
'@deepseek-ai/dsh-client-ui-primitives@0.1.2-rc.1':
resolution: {integrity: sha512-4gZS+tXAHbSxAjP91+lENpxKb49XVKcO80m7ATNNgD6lJHOwbuWVGv9IwEnmQ5ebZvI8Yrp8C1J+jDe1pB9jXw==}
'@deepseek-ai/dsh-client-ui-primitives@0.1.5-alpha.1':
resolution: {integrity: sha512-vF+hNd/Mxrhpbm5qcFVUFb7Db0JqkLnSdu+V2USu8dtQwcFIb0C5Es6v1+EB6Pq42YFs/lIg90JUTuXvF0OxBg==}
peerDependencies:
'@deepseek-ai/cordis': ^4.0.2
'@deepseek-ai/dsh-client-ui-settings@0.1.2-rc.1':
resolution: {integrity: sha512-/S16tlS84fggURMqO94HjYbE0xIH82aRJ2E4UupgM9acD3yJ6SCVam17tMcWKnqA6UWJK9llEHhl+KN9JyI/0Q==}
'@deepseek-ai/dsh-client-ui-settings@0.1.5-alpha.1':
resolution: {integrity: sha512-+3TVwum4UVY10jzEG2Pt7LfcJAjs1PlYb6kr49v3Yq6ZCvs3bsrEJ/yAN7EDJ7jtcUsgooSSneUlvBMXVdOCrQ==}
peerDependencies:
'@deepseek-ai/cordis': ^4.0.2
'@deepseek-ai/dsh-client-ui-slots@0.1.2-rc.1':
resolution: {integrity: sha512-wcqn8xwb87RjurSQ0zPoI4K9eYhDyoBjQqg4bByDq8CNynvayHoBPnNWfiAQ8ngyOPE20teL3DMS0gbcfCTqbA==}
'@deepseek-ai/dsh-client-ui-sidebar-right@0.1.5-alpha.1':
resolution: {integrity: sha512-3msAiVyymMsaBFKKSTsPO4q/x5isVcIYJvhQtuTCH1rEI11FFjm7/FUkOLGlBJsmIrxlubyTQ5lxhZKv+d1X5w==}
peerDependencies:
'@deepseek-ai/cordis': ^4.0.2
'@deepseek-ai/dsh-code-runtime@0.1.2-rc.1':
resolution: {integrity: sha512-dv5UszPJb/AFcarZc2c6h5z/X80bsrSd3bfIOugwNVclSI6P+L3ixseoZKgyks7T0Ap0h6ngXGk2WeoUkmYxnQ==}
'@deepseek-ai/dsh-client-ui-slots@0.1.5-alpha.1':
resolution: {integrity: sha512-Z7GXSKUeKlYrV0yRMU5auRSsTI0ORQ5zsr/FSojzdCI6+qQi27wMwRHvy5enIYMq/R/PfvSnH7igefzxu7OL5w==}
peerDependencies:
'@deepseek-ai/cordis': ^4.0.2
'@deepseek-ai/dsh-host-webserver@0.1.2-rc.1':
resolution: {integrity: sha512-QVcaf4qnIa1t215Y8TRiRhqkEz4Lu5/F+KqvWT+4sHOiyWmZr8g1JndqlwP6MBnMQEPmp0I/EKYJ7PWMV32gkA==}
'@deepseek-ai/dsh-code-runtime@0.1.5-alpha.1':
resolution: {integrity: sha512-WsHQyfhZlhy6E/E5FOg0O1nKguGTdxTQUOcE9Ixu0u9LD6kjnuNtwbJasKNCAVSRGRIMJ/LQ22OorCOVTbP+Ag==}
peerDependencies:
'@deepseek-ai/cordis': ^4.0.2
'@deepseek-ai/dsh-invariants@0.1.2-rc.1':
resolution: {integrity: sha512-zSjuFJYcEB94kPwn7SicqAV1dfVWZkjt1I8luDboxtjfp5CN6ziwJhLv3KaB+Dh9tgkyGY4mvo886W/HhE9L9g==}
'@deepseek-ai/dsh-host-webserver@0.1.5-alpha.1':
resolution: {integrity: sha512-0jjj2skOsThDukKE48WVE/GUtKXza4yTUrfZ7B0MwVrNbVhQPlrbMVlIyjR60OQCwNwDI8PC7Tbd6BIBAfetGw==}
peerDependencies:
'@deepseek-ai/cordis': ^4.0.2
'@deepseek-ai/dsh-llm@0.1.2-rc.1':
resolution: {integrity: sha512-7VYsha5AXsVLnsAwYJffWXz9bwUbElw8i5N8tlTSdai9Bupk3sMbsotzPf8ZbsuGAxQYErahMwQwgGEu4qZO6g==}
'@deepseek-ai/dsh-invariants@0.1.5-alpha.1':
resolution: {integrity: sha512-8Ote6DMq12u/X7eu5OfjlXPI1jxbEFUEm1jLIu6X03XZJT37O6S+wHvf5XCF0Kq43Ch7r5sibT3xcgTZpWRkNQ==}
peerDependencies:
'@deepseek-ai/cordis': ^4.0.2
'@deepseek-ai/dsh-scope@0.1.2-rc.1':
resolution: {integrity: sha512-GHwbzWrGbmE0ZGzAZsXBudpyCM9ENCpXxlKODIaPFbIWhRePh6nH2jW47TAOqCIsPPedWC2p3w8Tx5WAa3I/2A==}
'@deepseek-ai/dsh-llm@0.1.5-alpha.1':
resolution: {integrity: sha512-ncbGiWdvOG9v3K4b/LluSnrrG1w0lHKGkDsjztz1rfekvp2meLeQwbf59VCQRG68N9hdqNcr8dm0GFLEpGTE4w==}
peerDependencies:
'@deepseek-ai/cordis': ^4.0.2
'@deepseek-ai/dsh-invariants': ^0.1.2-rc.1
'@deepseek-ai/dsh-session-projection@0.1.2-rc.1':
resolution: {integrity: sha512-qVWlIzPm4u9vzQ5G/QtE6jeSJ3b3b42hWVJj664BurEZlHbAfdqkhYc1QsoBU+5k5kUEJFHh07z8HxuXA9kTnQ==}
'@deepseek-ai/dsh-scope@0.1.5-alpha.1':
resolution: {integrity: sha512-rY/5BQAiP0hPBArwPB2uf5kXdWfmSHcOXprB7hrDeyZ6EwciYfUgOX8n5CY/FT7e2ypd3Lwqqe90fibUxU3xGg==}
peerDependencies:
'@deepseek-ai/cordis': ^4.0.2
'@deepseek-ai/dsh-session': ^0.1.2-rc.1
'@deepseek-ai/dsh-invariants': ^0.1.5-alpha.1
'@deepseek-ai/dsh-session@0.1.2-rc.1':
resolution: {integrity: sha512-jRGNPTbQcvIx1F2MVmmkoiHLgpC3Btqo1wkl/3JDLlOWRVWKOuaC+5fQiMrFPOYfno6YiX/c2UvmA0td8qB92w==}
'@deepseek-ai/dsh-session-persistence@0.1.5-alpha.1':
resolution: {integrity: sha512-9bWMlbGsY/IEeAr0fKHdIxC9X/FMVmFGjJFn1HzqGsLKYDtrWmU3F5jug3ZN+baIfcjhiIzyP6FVbNvkFrVQag==}
peerDependencies:
'@deepseek-ai/cordis': ^4.0.2
'@deepseek-ai/dsh-scope': ^0.1.2-rc.1
'@deepseek-ai/dsh-brand': ^0.1.5-alpha.1
'@deepseek-ai/dsh-session': ^0.1.5-alpha.1
'@deepseek-ai/dsh-timeout': ^0.1.5-alpha.1
'@deepseek-ai/dsh-settings@0.1.2-rc.1':
resolution: {integrity: sha512-wMmJ2w5S6I7hgmgPppuAVemltEcTmAa8gvii7fk4T2KGVCuBYgig8xPwQ8Lp2ukLZSzzE6ZVCQxhdYZyJX/USA==}
'@deepseek-ai/dsh-session-projection@0.1.5-alpha.1':
resolution: {integrity: sha512-V68REQFndPMxtbwsX+MOPkrBF6nvSWC5L5cbFheX5EcgIqe2gJthRH4j0aQyTt3HOHeu3VRlNDXeE/Ys6pEejg==}
peerDependencies:
'@deepseek-ai/cordis': ^4.0.2
'@deepseek-ai/dsh-brand': ^0.1.2-rc.1
'@deepseek-ai/dsh-invariants': ^0.1.2-rc.1
'@deepseek-ai/dsh-session': ^0.1.2-rc.1
'@deepseek-ai/dsh-session': ^0.1.5-alpha.1
'@deepseek-ai/dsh-session@0.1.5-alpha.1':
resolution: {integrity: sha512-15+FxQFoVDDG1Lh0wA8dqVpVz7O4iUiXtVYM+ETg/ce6+gITPafXTEDd5i8HTIMnPRjZxEwX0BUlDDsChsjjNw==}
peerDependencies:
'@deepseek-ai/cordis': ^4.0.2
'@deepseek-ai/dsh-scope': ^0.1.5-alpha.1
'@deepseek-ai/dsh-settings@0.1.5-alpha.1':
resolution: {integrity: sha512-nO2fAvijWIHyNvAQsWPpIVuFnz03q1adB94l7HRbfWfMi0fJwi1u96f28SM8BJuOOqLLOka7kRNmljoqc/1bDw==}
peerDependencies:
'@deepseek-ai/cordis': ^4.0.2
'@deepseek-ai/dsh-brand': ^0.1.5-alpha.1
'@deepseek-ai/dsh-invariants': ^0.1.5-alpha.1
'@deepseek-ai/dsh-session': ^0.1.5-alpha.1
'@deepseek-ai/schemastery': ^3.18.2
'@deepseek-ai/dsh-subagent@0.1.2-rc.1':
resolution: {integrity: sha512-GhRsymKb1uYbc7OwHh8WTdSXqAcx/KHFJwYX4BYJk90dsOkVx9ccBq6QoJfDYja34s1KCyv2UO/8MUtrzBsITw==}
'@deepseek-ai/dsh-subagent@0.1.5-alpha.1':
resolution: {integrity: sha512-wXjiTD9t230lI2AywTjEBfa6B5Q22eVkZqrAE8z1pUrd7/CPoyMSLmySEobfb9ugX1LcjLn8lGTngO8k+Gj8DQ==}
peerDependencies:
'@deepseek-ai/cordis': ^4.0.2
'@deepseek-ai/dsh-agent': ^0.1.2-rc.1
'@deepseek-ai/dsh-agent-presets': ^0.1.2-rc.1
'@deepseek-ai/dsh-attachment': ^0.1.2-rc.1
'@deepseek-ai/dsh-invariants': ^0.1.2-rc.1
'@deepseek-ai/dsh-jobs': ^0.1.2-rc.1
'@deepseek-ai/dsh-llm': ^0.1.2-rc.1
'@deepseek-ai/dsh-sandbox': ^0.1.2-rc.1
'@deepseek-ai/dsh-sandbox-policy': ^0.1.2-rc.1
'@deepseek-ai/dsh-scope': ^0.1.2-rc.1
'@deepseek-ai/dsh-session': ^0.1.2-rc.1
'@deepseek-ai/dsh-session-persistence': ^0.1.2-rc.1
'@deepseek-ai/dsh-session-projection': ^0.1.2-rc.1
'@deepseek-ai/dsh-session-projection-cache': ^0.1.2-rc.1
'@deepseek-ai/dsh-session-query': ^0.1.2-rc.1
'@deepseek-ai/dsh-system-prompt': ^0.1.2-rc.1
'@deepseek-ai/dsh-tools': ^0.1.2-rc.1
'@deepseek-ai/dsh-typert-protocol': ^0.1.2-rc.1
'@deepseek-ai/dsh-user-approval': ^0.1.2-rc.1
'@deepseek-ai/dsh-util-time': ^0.1.2-rc.1
'@deepseek-ai/dsh-agent': ^0.1.5-alpha.1
'@deepseek-ai/dsh-agent-presets': ^0.1.5-alpha.1
'@deepseek-ai/dsh-attachment': ^0.1.5-alpha.1
'@deepseek-ai/dsh-invariants': ^0.1.5-alpha.1
'@deepseek-ai/dsh-jobs': ^0.1.5-alpha.1
'@deepseek-ai/dsh-llm': ^0.1.5-alpha.1
'@deepseek-ai/dsh-sandbox': ^0.1.5-alpha.1
'@deepseek-ai/dsh-sandbox-policy': ^0.1.5-alpha.1
'@deepseek-ai/dsh-scope': ^0.1.5-alpha.1
'@deepseek-ai/dsh-session': ^0.1.5-alpha.1
'@deepseek-ai/dsh-session-persistence': ^0.1.5-alpha.1
'@deepseek-ai/dsh-session-projection': ^0.1.5-alpha.1
'@deepseek-ai/dsh-session-projection-cache': ^0.1.5-alpha.1
'@deepseek-ai/dsh-session-query': ^0.1.5-alpha.1
'@deepseek-ai/dsh-system-prompt': ^0.1.5-alpha.1
'@deepseek-ai/dsh-tools': ^0.1.5-alpha.1
'@deepseek-ai/dsh-typert-protocol': ^0.1.5-alpha.1
'@deepseek-ai/dsh-user-approval': ^0.1.5-alpha.1
'@deepseek-ai/dsh-util-time': ^0.1.5-alpha.1
peerDependenciesMeta:
'@deepseek-ai/dsh-agent-presets':
optional: true
@@ -596,61 +631,61 @@ packages:
'@deepseek-ai/dsh-user-approval':
optional: true
'@deepseek-ai/dsh-system-prompt@0.1.2-rc.1':
resolution: {integrity: sha512-7W93PZKIk4CHvjZGgLIxrrEKpk+6t8nQXje1vKR5vG73dfXH0t1MJ5WWH/fngadYc7c6KzM84ihQ66tJqJqyNQ==}
'@deepseek-ai/dsh-system-prompt@0.1.5-alpha.1':
resolution: {integrity: sha512-sQk/K05ZOFn2xbDbbCTfDN1Jr5lMBz+dPVreqkVsXbt72QxzTGM4yw8JmDJEvUMVnOKBsKyWpLrvBpcYN8Unbw==}
peerDependencies:
'@deepseek-ai/cordis': ^4.0.2
'@deepseek-ai/dsh-invariants': ^0.1.2-rc.1
'@deepseek-ai/dsh-llm': ^0.1.2-rc.1
'@deepseek-ai/dsh-scope': ^0.1.2-rc.1
'@deepseek-ai/dsh-invariants': ^0.1.5-alpha.1
'@deepseek-ai/dsh-llm': ^0.1.5-alpha.1
'@deepseek-ai/dsh-scope': ^0.1.5-alpha.1
'@deepseek-ai/dsh-timeout@0.1.2-rc.1':
resolution: {integrity: sha512-llz11yxWeJB8suKCa6hRLjNBWVaPn3Fbd0XoBS0bkZLhPFzO1mDorx/XMca2GhI7XF9A86CURQcDt+aGjjvqJA==}
'@deepseek-ai/dsh-timeout@0.1.5-alpha.1':
resolution: {integrity: sha512-QTcHWuKD1/LuHyXTvwX85LzWhgcqUeIza0FjajekIDYG7+jW1K6T6gzRYaOger0pAzJMylUz8iERU16cHR1c8A==}
peerDependencies:
'@deepseek-ai/cordis': ^4.0.2
'@deepseek-ai/dsh-tools@0.1.2-rc.1':
resolution: {integrity: sha512-W9kUio00s7WbM8kEwniyd4hfb3CeUxVczsjXbOcKtakfiTywNLeyRWkpx2fvwRAH2rBfg3qCgczVfS0DOw1csg==}
'@deepseek-ai/dsh-tools@0.1.5-alpha.1':
resolution: {integrity: sha512-TYvlnJlFZtenuoqe/SvTSrscfo6Y8NgwwV2zgaF+1rsOZBBiGPZNsUn0WG53JMs8M8wpkq7HIzIUUs2TOdThMw==}
peerDependencies:
'@deepseek-ai/cordis': ^4.0.2
'@deepseek-ai/dsh-agent': ^0.1.2-rc.1
'@deepseek-ai/dsh-code-runtime': ^0.1.2-rc.1
'@deepseek-ai/dsh-invariants': ^0.1.2-rc.1
'@deepseek-ai/dsh-llm': ^0.1.2-rc.1
'@deepseek-ai/dsh-scope': ^0.1.2-rc.1
'@deepseek-ai/dsh-session': ^0.1.2-rc.1
'@deepseek-ai/dsh-system-prompt': ^0.1.2-rc.1
'@deepseek-ai/dsh-user-approval': ^0.1.2-rc.1
'@deepseek-ai/dsh-agent': ^0.1.5-alpha.1
'@deepseek-ai/dsh-code-runtime': ^0.1.5-alpha.1
'@deepseek-ai/dsh-invariants': ^0.1.5-alpha.1
'@deepseek-ai/dsh-llm': ^0.1.5-alpha.1
'@deepseek-ai/dsh-scope': ^0.1.5-alpha.1
'@deepseek-ai/dsh-session': ^0.1.5-alpha.1
'@deepseek-ai/dsh-system-prompt': ^0.1.5-alpha.1
'@deepseek-ai/dsh-user-approval': ^0.1.5-alpha.1
'@deepseek-ai/dsh-typert-protocol@0.1.2-rc.1':
resolution: {integrity: sha512-vBWD0NpriPd96ltbWaAw+iKeWFIEjiAcCWYtmqEcV1Ms+FaV02usjjm7LCmhSQhMZpBSBc7EZUX0spGlpjI1bA==}
'@deepseek-ai/dsh-typert-protocol@0.1.5-alpha.1':
resolution: {integrity: sha512-TYUDD047W5uxXK+8QX8lR8z/cAUqQBBy2mzU2kX7i7STtjzzeIqf+29JzANDFDFIXjwKmszk6VjeX7pwyjinIw==}
peerDependencies:
'@deepseek-ai/cordis': ^4.0.2
'@deepseek-ai/dsh-user-approval@0.1.2-rc.1':
resolution: {integrity: sha512-oYkLE4a/TwqVNi299pUf/QP1Yku6KScdUCTUyYbjaRBcN+/pXPpHikl2aoE2thH9D0uT/Kj6T2kz+wDDcFd9Xg==}
'@deepseek-ai/dsh-user-approval@0.1.5-alpha.1':
resolution: {integrity: sha512-xCuH17yYnuCpDMJyrNzHfL/wYs9iEGPGFOqnc2NWyFhNMsja+UGe3cDjqhCRvpYsXImM9AKxdAtjM18bDda0Lw==}
peerDependencies:
'@deepseek-ai/cordis': ^4.0.2
'@deepseek-ai/dsh-agent': ^0.1.2-rc.1
'@deepseek-ai/dsh-brand': ^0.1.2-rc.1
'@deepseek-ai/dsh-invariants': ^0.1.2-rc.1
'@deepseek-ai/dsh-llm': ^0.1.2-rc.1
'@deepseek-ai/dsh-scope': ^0.1.2-rc.1
'@deepseek-ai/dsh-session': ^0.1.2-rc.1
'@deepseek-ai/dsh-system-prompt': ^0.1.2-rc.1
'@deepseek-ai/dsh-agent': ^0.1.5-alpha.1
'@deepseek-ai/dsh-brand': ^0.1.5-alpha.1
'@deepseek-ai/dsh-invariants': ^0.1.5-alpha.1
'@deepseek-ai/dsh-llm': ^0.1.5-alpha.1
'@deepseek-ai/dsh-scope': ^0.1.5-alpha.1
'@deepseek-ai/dsh-session': ^0.1.5-alpha.1
'@deepseek-ai/dsh-system-prompt': ^0.1.5-alpha.1
'@deepseek-ai/dsh-util-crypto@0.1.2-rc.1':
resolution: {integrity: sha512-gE8FznQ1hkknw9QMrHNSlm3UEyRYuj3MLVvlJ6NyH1kmOsdjnbrVbSs+Pn72k2oIV4Fiyu3Umg0sLznUEcsiXw==}
'@deepseek-ai/dsh-util-crypto@0.1.5-alpha.1':
resolution: {integrity: sha512-lTTpyRKowbzlvrwHJr7leMDt/Iul8cLmlrwn+JLyKid5YzX1NDK7MgkrEYivqGplX5Jd64GRcXU1bGpz9phULA==}
peerDependencies:
'@deepseek-ai/cordis': ^4.0.2
'@deepseek-ai/dsh-util-time@0.1.2-rc.1':
resolution: {integrity: sha512-OoudDLoYothbZKDbjO6kFu3NLgAnglIS8NFkg5fjnBtz8LGwplN8w7kRURLIrZtH38HknYhpDsdJh8LZai3U6A==}
'@deepseek-ai/dsh-util-time@0.1.5-alpha.1':
resolution: {integrity: sha512-Fry6C7D9d4pwaDzAMc0Mg7PUjHitvLZ6UxzLfHP4sm7/43aps9RWeKAdRz+2OMf2Ml51zn8jUMRrhfaQoB3Ghg==}
peerDependencies:
'@deepseek-ai/cordis': ^4.0.2
'@deepseek-ai/dsh-util-values@0.1.2-rc.1':
resolution: {integrity: sha512-FQdgoK+KYVR0pBQb9fTsajvGb3gzUxEzJQEYnFOt+Qdwc4nkxJOPwJJdIvXuqvZaQ33DaOoqpnpcr2p5HpLu4g==}
'@deepseek-ai/dsh-util-values@0.1.5-alpha.1':
resolution: {integrity: sha512-slx5c7zAg1VAwvfkCNe24+NTu93FlQuI5vHLx0rb/S7HIVeCqI5Lm5158WCKjPTvHxXXOpACy7qY9L17lwCsFw==}
peerDependencies:
'@deepseek-ai/cordis': ^4.0.2
@@ -3506,32 +3541,51 @@ snapshots:
'@deepseek-ai/cosmokit@1.8.3': {}
'@deepseek-ai/dsh-agent@0.1.2-rc.1(6f833cf90b9dd3f0d529840b1aebfb48)':
'@deepseek-ai/dsh-agent-loop@0.1.5-alpha.1(3d62ae7d0bb4adb238d8dfb4da37e073)':
dependencies:
'@deepseek-ai/cordis': 4.0.2
'@deepseek-ai/dsh-invariants': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-llm': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-scope': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))
'@deepseek-ai/dsh-session': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)))
'@deepseek-ai/dsh-session-projection': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-session@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))))
'@deepseek-ai/dsh-system-prompt': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-llm@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)))
'@deepseek-ai/dsh-typert-protocol': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-agent': 0.1.5-alpha.1(288d2f4271bc6bef491bb5a5c084f12c)
'@deepseek-ai/dsh-brand': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-invariants': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-llm': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-scope': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))
'@deepseek-ai/dsh-session': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)))
'@deepseek-ai/dsh-session-persistence': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-brand@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-session@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))))(@deepseek-ai/dsh-timeout@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))
'@deepseek-ai/dsh-session-projection': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-session@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))))
'@deepseek-ai/dsh-settings': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-brand@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-session@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))))(@deepseek-ai/schemastery@3.18.2)
'@deepseek-ai/dsh-system-prompt': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-llm@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)))
'@deepseek-ai/dsh-tools': 0.1.5-alpha.1(239eb910b4462b7c15b6b44661457a15)
'@deepseek-ai/dsh-util-values': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/schemastery': 3.18.2
zod: 4.4.3
'@deepseek-ai/dsh-attachment@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-brand@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))':
'@deepseek-ai/dsh-agent@0.1.5-alpha.1(288d2f4271bc6bef491bb5a5c084f12c)':
dependencies:
'@deepseek-ai/cordis': 4.0.2
'@deepseek-ai/dsh-brand': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-invariants': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-llm': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-scope': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))
'@deepseek-ai/dsh-session': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)))
'@deepseek-ai/dsh-session-projection': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-session@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))))
'@deepseek-ai/dsh-system-prompt': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-llm@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)))
'@deepseek-ai/dsh-typert-protocol': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-util-values': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-brand@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)':
'@deepseek-ai/dsh-attachment@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-brand@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))':
dependencies:
'@deepseek-ai/cordis': 4.0.2
'@deepseek-ai/dsh-brand': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-brand@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)':
dependencies:
'@deepseek-ai/cordis': 4.0.2
'@deepseek-ai/dsh-client-locale@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)':
'@deepseek-ai/dsh-client-locale@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)':
dependencies:
'@deepseek-ai/cordis': 4.0.2
'@deepseek-ai/schemastery': 3.18.2
'@deepseek-ai/dsh-client-ui-conversation@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(typescript@5.6.2)':
'@deepseek-ai/dsh-client-ui-conversation@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(typescript@5.6.2)':
dependencies:
'@deepseek-ai/cordis': 4.0.2
'@deepseek-ai/schemastery': 3.18.2
@@ -3544,7 +3598,7 @@ snapshots:
transitivePeerDependencies:
- typescript
'@deepseek-ai/dsh-client-ui-primitives@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)':
'@deepseek-ai/dsh-client-ui-primitives@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)':
dependencies:
'@deepseek-ai/cordis': 4.0.2
'@shikijs/langs': 4.4.3
@@ -3570,19 +3624,27 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@deepseek-ai/dsh-client-ui-settings@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)':
'@deepseek-ai/dsh-client-ui-settings@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)':
dependencies:
'@deepseek-ai/cordis': 4.0.2
'@deepseek-ai/dsh-client-ui-slots@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)':
'@deepseek-ai/dsh-client-ui-sidebar-right@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)':
dependencies:
'@deepseek-ai/cordis': 4.0.2
clsx: 2.1.1
picomatch: 4.0.5
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
'@deepseek-ai/dsh-client-ui-slots@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)':
dependencies:
'@deepseek-ai/cordis': 4.0.2
'@deepseek-ai/dsh-code-runtime@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)':
'@deepseek-ai/dsh-code-runtime@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)':
dependencies:
'@deepseek-ai/cordis': 4.0.2
'@deepseek-ai/dsh-host-webserver@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)':
'@deepseek-ai/dsh-host-webserver@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)':
dependencies:
'@deepseek-ai/cordis': 4.0.2
'@deepseek-ai/schemastery': 3.18.2
@@ -3591,122 +3653,131 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)':
'@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)':
dependencies:
'@deepseek-ai/cordis': 4.0.2
'@deepseek-ai/schemastery': 3.18.2
'@deepseek-ai/dsh-llm@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)':
'@deepseek-ai/dsh-llm@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)':
dependencies:
'@deepseek-ai/cordis': 4.0.2
'@deepseek-ai/dsh-brand': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-timeout': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-typert-protocol': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-util-crypto': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-util-values': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-brand': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-timeout': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-typert-protocol': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-util-crypto': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-util-values': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/schemastery': 3.18.2
zod: 4.4.3
'@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))':
'@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))':
dependencies:
'@deepseek-ai/cordis': 4.0.2
'@deepseek-ai/dsh-invariants': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-invariants': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-session-projection@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-session@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))))':
'@deepseek-ai/dsh-session-persistence@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-brand@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-session@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))))(@deepseek-ai/dsh-timeout@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))':
dependencies:
'@deepseek-ai/cordis': 4.0.2
'@deepseek-ai/dsh-session': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)))
'@deepseek-ai/dsh-brand': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-session': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)))
'@deepseek-ai/dsh-timeout': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-util-values': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-session-projection@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-session@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))))':
dependencies:
'@deepseek-ai/cordis': 4.0.2
'@deepseek-ai/dsh-session': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)))
zod: 4.4.3
'@deepseek-ai/dsh-session@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)))':
'@deepseek-ai/dsh-session@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)))':
dependencies:
'@deepseek-ai/cordis': 4.0.2
'@deepseek-ai/dsh-brand': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-llm': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-scope': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))
'@deepseek-ai/dsh-util-values': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-brand': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-llm': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-scope': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))
'@deepseek-ai/dsh-util-values': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-settings@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-brand@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-session@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))))(@deepseek-ai/schemastery@3.18.2)':
'@deepseek-ai/dsh-settings@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-brand@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-session@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))))(@deepseek-ai/schemastery@3.18.2)':
dependencies:
'@deepseek-ai/cordis': 4.0.2
'@deepseek-ai/dsh-brand': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-invariants': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-session': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)))
'@deepseek-ai/dsh-util-values': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-brand': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-invariants': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-session': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)))
'@deepseek-ai/dsh-util-values': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/schemastery': 3.18.2
'@deepseek-ai/dsh-subagent@0.1.2-rc.1(6241f70df87ac98c48965b657a1f7a60)':
'@deepseek-ai/dsh-subagent@0.1.5-alpha.1(0904ccd91d8f9ab5907c34d0b1c82b43)':
dependencies:
'@deepseek-ai/cordis': 4.0.2
'@deepseek-ai/dsh-agent': 0.1.2-rc.1(6f833cf90b9dd3f0d529840b1aebfb48)
'@deepseek-ai/dsh-attachment': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-brand@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))
'@deepseek-ai/dsh-brand': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-invariants': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-llm': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-scope': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))
'@deepseek-ai/dsh-session': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)))
'@deepseek-ai/dsh-system-prompt': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-llm@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)))
'@deepseek-ai/dsh-tools': 0.1.2-rc.1(de5e1ff2d608fbe22667925341ad1dfc)
'@deepseek-ai/dsh-typert-protocol': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-util-time': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-util-values': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-agent': 0.1.5-alpha.1(288d2f4271bc6bef491bb5a5c084f12c)
'@deepseek-ai/dsh-attachment': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-brand@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))
'@deepseek-ai/dsh-brand': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-invariants': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-llm': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-scope': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))
'@deepseek-ai/dsh-session': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)))
'@deepseek-ai/dsh-system-prompt': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-llm@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)))
'@deepseek-ai/dsh-tools': 0.1.5-alpha.1(239eb910b4462b7c15b6b44661457a15)
'@deepseek-ai/dsh-typert-protocol': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-util-time': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-util-values': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
zod: 4.4.3
optionalDependencies:
'@deepseek-ai/dsh-session-projection': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-session@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))))
'@deepseek-ai/dsh-user-approval': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-agent@0.1.2-rc.1(6f833cf90b9dd3f0d529840b1aebfb48))(@deepseek-ai/dsh-brand@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-llm@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)))(@deepseek-ai/dsh-session@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))))(@deepseek-ai/dsh-system-prompt@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-llm@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))))
'@deepseek-ai/dsh-session-persistence': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-brand@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-session@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))))(@deepseek-ai/dsh-timeout@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))
'@deepseek-ai/dsh-session-projection': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-session@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))))
'@deepseek-ai/dsh-user-approval': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-agent@0.1.5-alpha.1(288d2f4271bc6bef491bb5a5c084f12c))(@deepseek-ai/dsh-brand@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-llm@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)))(@deepseek-ai/dsh-session@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))))(@deepseek-ai/dsh-system-prompt@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-llm@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))))
'@deepseek-ai/dsh-system-prompt@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-llm@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)))':
'@deepseek-ai/dsh-system-prompt@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-llm@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)))':
dependencies:
'@deepseek-ai/cordis': 4.0.2
'@deepseek-ai/dsh-invariants': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-llm': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-scope': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))
'@deepseek-ai/dsh-invariants': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-llm': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-scope': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))
'@deepseek-ai/schemastery': 3.18.2
'@deepseek-ai/dsh-timeout@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)':
'@deepseek-ai/dsh-timeout@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)':
dependencies:
'@deepseek-ai/cordis': 4.0.2
'@deepseek-ai/dsh-tools@0.1.2-rc.1(de5e1ff2d608fbe22667925341ad1dfc)':
'@deepseek-ai/dsh-tools@0.1.5-alpha.1(239eb910b4462b7c15b6b44661457a15)':
dependencies:
'@deepseek-ai/cordis': 4.0.2
'@deepseek-ai/dsh-agent': 0.1.2-rc.1(6f833cf90b9dd3f0d529840b1aebfb48)
'@deepseek-ai/dsh-brand': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-code-runtime': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-invariants': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-llm': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-scope': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))
'@deepseek-ai/dsh-session': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)))
'@deepseek-ai/dsh-system-prompt': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-llm@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)))
'@deepseek-ai/dsh-user-approval': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-agent@0.1.2-rc.1(6f833cf90b9dd3f0d529840b1aebfb48))(@deepseek-ai/dsh-brand@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-llm@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)))(@deepseek-ai/dsh-session@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))))(@deepseek-ai/dsh-system-prompt@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-llm@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))))
'@deepseek-ai/dsh-util-values': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-agent': 0.1.5-alpha.1(288d2f4271bc6bef491bb5a5c084f12c)
'@deepseek-ai/dsh-brand': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-code-runtime': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-invariants': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-llm': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-scope': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))
'@deepseek-ai/dsh-session': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)))
'@deepseek-ai/dsh-system-prompt': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-llm@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)))
'@deepseek-ai/dsh-user-approval': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-agent@0.1.5-alpha.1(288d2f4271bc6bef491bb5a5c084f12c))(@deepseek-ai/dsh-brand@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-llm@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)))(@deepseek-ai/dsh-session@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))))(@deepseek-ai/dsh-system-prompt@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-llm@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))))
'@deepseek-ai/dsh-util-values': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/schemastery': 3.18.2
'@deepseek-ai/dsh-typert-protocol@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)':
'@deepseek-ai/dsh-typert-protocol@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)':
dependencies:
'@deepseek-ai/cordis': 4.0.2
'@deepseek-ai/dsh-user-approval@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-agent@0.1.2-rc.1(6f833cf90b9dd3f0d529840b1aebfb48))(@deepseek-ai/dsh-brand@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-llm@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)))(@deepseek-ai/dsh-session@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))))(@deepseek-ai/dsh-system-prompt@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-llm@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))))':
'@deepseek-ai/dsh-user-approval@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-agent@0.1.5-alpha.1(288d2f4271bc6bef491bb5a5c084f12c))(@deepseek-ai/dsh-brand@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-llm@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)))(@deepseek-ai/dsh-session@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))))(@deepseek-ai/dsh-system-prompt@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-llm@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))))':
dependencies:
'@deepseek-ai/cordis': 4.0.2
'@deepseek-ai/dsh-agent': 0.1.2-rc.1(6f833cf90b9dd3f0d529840b1aebfb48)
'@deepseek-ai/dsh-brand': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-invariants': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-llm': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-scope': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))
'@deepseek-ai/dsh-session': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)))
'@deepseek-ai/dsh-system-prompt': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-llm@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-scope@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)))
'@deepseek-ai/dsh-agent': 0.1.5-alpha.1(288d2f4271bc6bef491bb5a5c084f12c)
'@deepseek-ai/dsh-brand': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-invariants': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-llm': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-scope': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))
'@deepseek-ai/dsh-session': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)))
'@deepseek-ai/dsh-system-prompt': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-llm@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-scope@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)))
'@deepseek-ai/schemastery': 3.18.2
'@deepseek-ai/dsh-util-crypto@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)':
'@deepseek-ai/dsh-util-crypto@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)':
dependencies:
'@deepseek-ai/cordis': 4.0.2
'@deepseek-ai/dsh-util-time@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)':
'@deepseek-ai/dsh-util-time@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)':
dependencies:
'@deepseek-ai/cordis': 4.0.2
'@deepseek-ai/dsh-util-values@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)':
'@deepseek-ai/dsh-util-values@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)':
dependencies:
'@deepseek-ai/cordis': 4.0.2
@@ -3767,15 +3838,15 @@ snapshots:
'@exodus/bytes@1.15.1': {}
'@huanlin/dsh-plugin-better-locale@0.1.0(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-client-locale@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-client-ui-primitives@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-client-ui-settings@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-client-ui-slots@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-invariants@0.1.2-rc.1(@deepseek-ai/cordis@4.0.2))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
'@huanlin/dsh-plugin-better-locale@0.1.0(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-client-locale@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-client-ui-primitives@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-client-ui-settings@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-client-ui-slots@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-invariants@0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@deepseek-ai/cordis': 4.0.2
optionalDependencies:
'@deepseek-ai/dsh-client-locale': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-client-ui-primitives': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-client-ui-settings': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-client-ui-slots': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-invariants': 0.1.2-rc.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-client-locale': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-client-ui-primitives': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-client-ui-settings': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-client-ui-slots': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
'@deepseek-ai/dsh-invariants': 0.1.5-alpha.1(@deepseek-ai/cordis@4.0.2)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
+140
View File
@@ -0,0 +1,140 @@
/**
* Process-local live assistant stream buffer (DSH 0.1.5+).
*
* 0.1.2 appended a durable `assistant/chunk` session event for every model
* delta, so the side-chat transcript and the inherited in-progress snapshot
* could read streaming text straight out of the session log. 0.1.5 removed
* that event: an in-flight attempt now publishes `agent/assistant-stream`
* frames (start / chunk / end) that are NOT part of the log, and the durable
* record lands only at settlement — `assistant/message` (with the exact
* `stream` embedded) or `assistant/attempt` (a failed attempt that committed
* no message, also with its `stream`).
*
* This module folds those frames into a bounded per-session buffer of
* normalized chunks — the same information the old `assistant/chunk` events
* carried — so the plugin keeps a live transcript and an honest in-progress
* snapshot. The buffer is cleared when an attempt ends, so a settled step
* never duplicates its durable message.
*/
import type { Context } from './context-types.ts'
/** One normalized live delta, keyed by its attempt and dense position. */
export interface AssistantLiveChunk {
/** The attempt these chunks belong to (DSH `LlmAttemptId`). */
readonly attemptId: string
/** The turn the attempt belongs to. */
readonly turn: number
/** The step the attempt belongs to. */
readonly step: number
/** Dense zero-based position within the attempt (DSH's own index). */
readonly index: number
/** Safe-integer timestamp of the frame (reused by the durable stream). */
readonly time: number
/** The raw model stream chunk (`text-delta` / `reasoning-delta` / …). */
readonly chunk: Record<string, unknown>
}
/** The plugin's read face over the live frames of every session. */
export interface AssistantLiveBuffer {
/**
* The live chunks of one session's active attempt, in index order.
* @param sessionId - the session whose attempt is streaming.
* @returns the chunks; empty when nothing is streaming for that session.
*/
chunksFor(sessionId: string): readonly AssistantLiveChunk[]
/** Stop observing the agent stream. */
dispose(): void
}
/** Per-attempt buffer ceiling; beyond it the oldest deltas are dropped. */
export const LIVE_CHUNK_CAP = 4000
/** How many sessions may hold a live buffer at once (the oldest is dropped). */
export const LIVE_SESSION_CAP = 64
/** One session's active attempt as it accumulates. */
interface Attempt {
readonly attemptId: string
readonly turn: number
readonly step: number
readonly chunks: AssistantLiveChunk[]
/** The next dense frame index this attempt accepts (independent of the
* window below, which drops the oldest chunks once the cap is reached). */
nextIndex: number
}
/** A frame's `chunk` payload must be a JSON record to be worth buffering. */
function chunkOf(value: unknown): Record<string, unknown> | undefined {
return typeof value === 'object' && value !== null && !Array.isArray(value)
? value as Record<string, unknown>
: undefined
}
/** A finite non-negative integer, or undefined. */
function countOf(value: unknown): number | undefined {
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : undefined
}
/**
* Fold `agent/assistant-stream` frames into per-session live buffers.
*
* Frames arrive for every attached agent, so the buffer is keyed by the
* emitting session and ignores anything it cannot identify. An out-of-order
* or mismatched chunk drops the attempt rather than splicing a gap: a
* transcript with a hole is worse than one that settles at the next durable
* event.
* @param ctx - host plugin context (its `on` subscribes the session feed).
* @param cap - per-attempt chunk ceiling.
* @returns the read face and a disposer unbinding the listener.
*/
export function createAssistantLiveBuffer(ctx: Context, cap: number = LIVE_CHUNK_CAP): AssistantLiveBuffer {
const attempts = new Map<string, Attempt>()
const off = ctx.on('agent/assistant-stream', (payload) => {
const record = payload as { agent?: unknown; frame?: unknown } | null
const frame = record?.frame as Record<string, unknown> | undefined
if (frame === undefined) return
const session = (record?.agent as { session?: { id?: unknown } } | undefined)?.session
const sessionId = typeof session?.id === 'string' ? session.id : undefined
if (sessionId === undefined) return
const type = frame['type']
if (type === 'end') {
attempts.delete(sessionId)
return
}
const attemptId = typeof frame['attemptId'] === 'string' ? frame.attemptId : undefined
if (attemptId === undefined) return
if (type === 'start') {
const turn = countOf(frame['turn'])
const step = countOf(frame['step'])
if (turn === undefined || step === undefined) return
const known = attempts.has(sessionId)
attempts.delete(sessionId)
// Only a NEW session may evict the oldest tracked one.
if (!known && attempts.size >= LIVE_SESSION_CAP) {
const oldest = attempts.keys().next().value
if (oldest !== undefined) attempts.delete(oldest)
}
attempts.set(sessionId, { attemptId, turn, step, chunks: [], nextIndex: 0 })
return
}
if (type !== 'chunk') return
const attempt = attempts.get(sessionId)
if (attempt === undefined || attempt.attemptId !== attemptId) return
const index = countOf(frame['index'])
const chunk = chunkOf(frame['chunk'])
// Dense positions only: a gap means the buffer missed frames (a
// replacement attempt, a reconnect) and its text would be wrong.
if (index === undefined || chunk === undefined || index !== attempt.nextIndex) {
attempts.delete(sessionId)
return
}
attempt.nextIndex = index + 1
const time = countOf(frame['time']) ?? 0
attempt.chunks.push({ attemptId, turn: attempt.turn, step: attempt.step, index, time, chunk })
if (attempt.chunks.length > cap) attempt.chunks.splice(0, attempt.chunks.length - cap)
})
return {
chunksFor: sessionId => attempts.get(sessionId)?.chunks ?? [],
dispose: () => { off() },
}
}
-11
View File
@@ -901,17 +901,6 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
<span className={css.suffix}>{t('settingsWidthSuffix')}</span>
</span>
</div>
<div className={css.row}>
<span className={css.rowText}>
<span className={css.title}>{t('settingsOpenPathTitle')}</span>
<span className={css.desc}>{t('settingsOpenPathDesc')}</span>
</span>
<Switch
label={t('settingsOpenPathTitle')}
checked={prefs.interceptOpenPath}
onChange={(next) => { applyPref({ interceptOpenPath: next }) }}
/>
</div>
<div className={css.row}>
<span className={css.rowText}>
<span className={css.title}>{t('settingsOpenToolsTitle')}</span>
+28 -5
View File
@@ -58,6 +58,7 @@ import {
sideThreadRows,
threadHasCompletedTurn,
threadTrailingPending,
type SidechatLiveEvent,
type SidechatThreadInfo,
} from '../sidechat-core.ts'
import {
@@ -112,6 +113,8 @@ const inFlightStarts = new Set<string>()
* the afterSeq delta and never re-download what they already hold). */
interface ThreadCache {
entries: SidebarHistoryEntry[]
/** The CURRENT attempt's live deltas (replaced every pull, never merged). */
live: SidechatLiveEvent[]
}
/** Row-render labels (locale-dependent, memoized once per mount). */
@@ -383,7 +386,7 @@ export function SideChatView(props: {
const [info, setInfo] = useState<SidechatThreadInfo | null>(null)
const [menuOpen, setMenuOpen] = useState(false)
const cacheRef = useRef<ThreadCache>({ entries: [] })
const cacheRef = useRef<ThreadCache>({ entries: [], live: [] })
// The previous poll's rows (see the mapping's reuse pass below).
const prevRowsRef = useRef<SidechatTranscriptRow[]>([])
const controllerRef = useRef<AbortController | null>(null)
@@ -457,14 +460,18 @@ export function SideChatView(props: {
try {
const cache = cacheRef.current
const afterSeq = cache.entries.at(-1)?.event.seq
const { events } = await api.sidechatEvents(childId, afterSeq, controller.signal)
const { events, live } = await api.sidechatEvents(childId, afterSeq, controller.signal)
// The live set is the CURRENT attempt, so it replaces whatever the
// previous pull held; a settled step drops out of the host buffer and
// its durable assistant/message takes over in the mapping.
cache.live = live ?? []
if (events.length > 0) {
// Wire events arrive as parsed JSON; the mirror narrows data to the
// record the mapping reads.
const incoming = events.map(event => ({ event: event as SidebarSessionEvent }))
cache.entries = mergeBySeq(cache.entries, incoming)
setRevision(value => value + 1)
}
setRevision(value => value + 1)
} catch {
// Aborted by a newer pull or a wire failure: keep the last rows.
}
@@ -482,7 +489,7 @@ export function SideChatView(props: {
// Reset the transcript cache whenever the binding changes, then focus
// the composer — it owns the first message of a fresh thread.
useEffect(() => {
cacheRef.current = { entries: [] }
cacheRef.current = { entries: [], live: [] }
prevRowsRef.current = []
controllerRef.current?.abort()
setError(null)
@@ -532,8 +539,24 @@ export function SideChatView(props: {
// The previous poll's rows ride into the mapping so unchanged rows keep
// their object identity (see reuseRows): the 2s poll re-renders only the
// changed tail instead of re-parsing markdown for the whole transcript.
// The live deltas are appended AFTER the durable events (they are the
// in-flight tail), and a delta whose step already settled durably is
// dropped: the durable message is authoritative and would otherwise render
// twice.
const rows = useMemo(() => {
const next = threadId === undefined ? [] : transcriptRows(cacheRef.current.entries, prevRowsRef.current)
const cache = cacheRef.current
const settled = new Set(
cache.entries
.filter(entry => entry.event.type === 'assistant/message')
.map(entry => {
const data = entry.event.data as { turn?: unknown; step?: unknown }
return `${String(data.turn)}:${String(data.step)}`
}),
)
const live = cache.live
.filter(event => !settled.has(`${String(event.data.turn)}:${String(event.data.step)}`))
.map(event => ({ event: event as SidebarSessionEvent }))
const next = threadId === undefined ? [] : transcriptRows([...cache.entries, ...live], prevRowsRef.current)
prevRowsRef.current = next
return next
},
+6 -3
View File
@@ -8,7 +8,7 @@
*/
import { encodeHtmlUrl } from '../html-route.ts'
import type { LastActivity } from '../subagent-activity.ts'
import type { SidechatLogEvent, SidechatThreadInfo } from '../sidechat-core.ts'
import type { SidechatLiveEvent, SidechatLogEvent, SidechatThreadInfo } from '../sidechat-core.ts'
import type { SidebarSessionEvent } from '../context-types.ts'
import type { BrowserProbeResult } from './browser.ts'
@@ -390,9 +390,12 @@ export const api = {
call<SidechatThreadInfo>('sidechat.info', { childId }),
/** One transcript pull of a Side Chat thread: the thread's OWN events
* (the inherited seed is cut host-side and never crosses the wire).
* `afterSeq` narrows the response to the delta beyond it (poll tail). */
* `afterSeq` narrows the response to the delta beyond it (poll tail).
* `live` is the thread's in-flight model deltas (DSH 0.1.5 publishes them
* outside the session log) — the CURRENT attempt on every pull, never a
* delta, so the caller replaces its live set instead of appending. */
sidechatEvents: (childId: string, afterSeq?: number, signal?: AbortSignal) =>
call<{ events: SidechatLogEvent[] }>('sidechat.events', {
call<{ events: SidechatLogEvent[]; live: SidechatLiveEvent[] }>('sidechat.events', {
childId,
...(afterSeq !== undefined ? { afterSeq } : {}),
}, signal),
+1 -13
View File
@@ -17,7 +17,7 @@ import { revalidateChunksOnReactivate, setChunkModuleSystem } from './chunk-load
import { registerBuiltins } from './builtins/index.ts'
import { Sidebar } from './Sidebar.tsx'
import { RenderBoundary } from './RenderBoundary.tsx'
import { registerOpenPathInterception, registerTurnTailInterception } from './intercept.tsx'
import { registerTurnTailInterception } from './intercept.tsx'
import { registerLinkInterception } from './link-intercept.ts'
import { registerImeGuard } from './ime-guard.ts'
import { registerSettingsNavIcon } from './settings-nav-icon.ts'
@@ -342,18 +342,6 @@ export function apply(ctx: Context): void {
'dsh-better-sidebar: turn-tail interception',
)
ctx.effect(
() => {
try {
return registerOpenPathInterception(ctx, sidebarStore)
} catch (error) {
fail('interception', error)
return () => {}
}
},
'dsh-better-sidebar: open-path interception',
)
ctx.effect(
() => {
try {
+1 -46
View File
@@ -11,7 +11,6 @@ import type { Context } from '../context-types.ts'
import { firstLeaf, revealPaths, togglePanel, type SidebarStore } from './state.ts'
import { t } from './locales.ts'
import { resolveSidebarPath, selectProducedFiles } from './produced-files.ts'
import { wrapOpenWorkspacePath, type OpenWorkspacePathService } from './openpath-intercept.ts'
import css from './sidebar.module.css'
/** Open a file in the sidebar's editor (used by the intercepted row and the explorer). */
@@ -25,13 +24,6 @@ export function openSidebarFile(ctx: Context, store: SidebarStore, sessionId: st
ctx.get('betterSidebar')?.openTab({ type: 'editor', title, path: absolute, id: `editor:${absolute}` })
}
/**
* The produced files the turn-tail selector last matched for the visible
* session. The "Show in folder" gesture carries no file path of its own
* (`'.'`), so the reveal highlights exactly these rows when available.
*/
let lastProduced: readonly string[] = []
/**
* Reveal the produced files in the sidebar explorer: expand their parent
* directories, highlight the rows, and focus the explorer tab (expanding the
@@ -137,9 +129,7 @@ export function registerTurnTailInterception(ctx: Context, store: SidebarStore):
select: (owner) => {
if (store.getSuspended()) return null
if (store.getPrefs().tabsEnabled['editor'] === false) return null
const matched = selectProducedFiles(owner)
if (matched !== null) lastProduced = matched
return matched
return selectProducedFiles(owner)
},
priority: -1,
registrant: 'dsh-better-sidebar',
@@ -149,38 +139,3 @@ export function registerTurnTailInterception(ctx: Context, store: SidebarStore):
}),
}, SidebarProducedFiles))
}
/**
* Register the chat file-open interception: shadows
* `remote.session.openWorkspacePath` — the single funnel every chat-side
* file open goes through on alpha hosts (tool-row path links, the
* produced-files row, prose mentions, inline-code paths) — so opens land in
* the sidebar editor instead of the Host OS. The folder-reveal gesture
* ("Show in folder" passes `'.'`) is the one exception: it is routed to the
* explorer. Gated by BOTH the `interceptOpenPath` pref and the editor tab's
* enable switch; declined opens fall through to the original remote call.
*
* The `remote.session` namespace service mounts asynchronously (the gateway
* client creates it when the session-controller contribution arrives) and
* is recreated on contribution remounts, so the wrapper installs through
* `ctx.inject`: the callback runs once the service exists and re-runs after
* every remount, re-applying the shadow on the fresh instance. Returns the
* disposer (disposes the inject fiber, which restores the original method
* descriptor — HMR-safe).
*/
export function registerOpenPathInterception(ctx: Context, store: SidebarStore): () => void {
const fiber = ctx.inject(['remote.session'], (fctx) => {
fctx.effect(() => {
const service = fctx.get('remote.session') as OpenWorkspacePathService
return wrapOpenWorkspacePath(service, {
takeoverEnabled: () => !store.getSuspended()
&& store.getPrefs().interceptOpenPath !== false
&& store.getPrefs().tabsEnabled['editor'] !== false,
currentSessionId: () => ctx.sessions.list.getSnapshot().current,
openInSidebar: (path, sessionId) => { openSidebarFile(ctx, store, sessionId, path) },
revealInExplorer: (_path, sessionId) => { revealInExplorer(ctx, store, sessionId, lastProduced) },
})
}, 'dsh-better-sidebar: open-path interception wrap')
})
return () => { void fiber.dispose() }
}
-2
View File
@@ -238,8 +238,6 @@ export const ar: Record<string, string> = {
settingsWidthTitle: 'الحصة الافتراضية للعرض',
settingsWidthDesc: 'الحصة الافتراضية للبطاقة الجانبية من عرض النافذة للمحادثات الجديدة (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'فتح ملفات المحادثة في الشريط الجانبي',
settingsOpenPathDesc: 'فتح روابط الملفات في المحادثة (صفوف الأدوات، الملفات الناتجة، الإشارات) في محرّر الشريط الجانبي بدلاً من التطبيق الافتراضي للنظام',
settingsOpenToolsTitle: 'حقن أداة فتح الشريط الجانبي للنموذج',
settingsOpenToolsDesc: 'عند التفعيل، يمكن للنموذج فتح الملفات والمجلدات وصفحات HTTP(S) في الشريط الجانبي عبر أداة sidebar_open (معطّل افتراضياً)',
settingsTitleBarTitle: 'وضع توافق الموضع',
-2
View File
@@ -223,8 +223,6 @@ export const de: Record<string, string> = {
settingsWidthTitle: 'Standardbreite',
settingsWidthDesc: 'Standardanteil der Seitenkarte an der Fensterbreite für neue Unterhaltungen (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'Chat-Dateien in der Seitenleiste öffnen',
settingsOpenPathDesc: 'Dateilinks im Chat (Werkzeugzeilen, erstellte Dateien, Erwähnungen) werden im Seitenleisten-Editor geöffnet statt in der System-Standardanwendung',
settingsOpenToolsTitle: 'Seitenleisten-Öffnungswerkzeug für das Modell bereitstellen',
settingsOpenToolsDesc: 'Wenn aktiviert, kann das Modell über das sidebar_open-Werkzeug Dateien, Ordner und HTTP(S)-Seiten in der Seitenleiste öffnen (standardmäßig deaktiviert)',
settingsTitleBarTitle: 'Kompatibilitätsmodus der Position',
-2
View File
@@ -230,8 +230,6 @@ export const fr: Record<string, string> = {
settingsWidthTitle: 'Part de largeur par défaut',
settingsWidthDesc: 'Pourcentage de la largeur de la fenêtre occupé par la carte latérale pour les nouvelles sessions (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'Ouvrir les fichiers de discussion dans la barre latérale',
settingsOpenPathDesc: 'À la place de l’application par défaut du système, ouvrir dans l’éditeur de la barre latérale les liens vers les fichiers du chat (lignes d’outils, listes de produits, mentions de fichiers)',
settingsOpenToolsTitle: 'Injecter l\'outil d\'ouverture latérale pour le modèle',
settingsOpenToolsDesc: 'Une fois activé, le modèle peut ouvrir des fichiers, dossiers et pages HTTP(S) dans la barre latérale via l\'outil sidebar_open (désactivé par défaut)',
settingsTitleBarTitle: 'Mode de compatibilité de position',
-2
View File
@@ -237,8 +237,6 @@ export const hi: Record<string, string> = {
settingsWidthTitle: 'डिफ़ॉल्ट चौड़ाई हिस्सा',
settingsWidthDesc: 'नए वार्तालाप के लिए साइड कार्ड की विंडो चौड़ाई का डिफ़ॉल्ट हिस्सा (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'चैट फ़ाइलें साइडबार में खोलें',
settingsOpenPathDesc: 'चैट में फ़ाइल लिंक (टूल पंक्ति, उत्पादित फ़ाइलें, उल्लेख) क्लिक करने पर सिस्टम डिफ़ॉल्ट ऐप के बजाय साइडबार एडिटर में खोलें',
settingsOpenToolsTitle: 'मॉडल के लिए साइडबार ओपन टूल इंजेक्ट करें',
settingsOpenToolsDesc: 'चालू होने पर, मॉडल sidebar_open टूल से साइडबार में फ़ाइलें, फ़ोल्डर और HTTP(S) पेज खोल सकता है (डिफ़ॉल्ट रूप से बंद)',
settingsTitleBarTitle: 'स्थिति संगतता मोड',
-2
View File
@@ -235,8 +235,6 @@ export const id: Record<string, string> = {
settingsWidthTitle: 'Porsi lebar default',
settingsWidthDesc: 'Porsi lebar jendela default kartu samping untuk obrolan baru (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'Buka berkas obrolan di sidebar',
settingsOpenPathDesc: 'Buka tautan berkas di obrolan (baris alat, berkas yang dihasilkan, mention) di editor sidebar alih-alih aplikasi default sistem',
settingsOpenToolsTitle: 'Suntik alat buka sidebar untuk model',
settingsOpenToolsDesc: 'Saat diaktifkan, model dapat membuka file, folder, dan halaman HTTP(S) di sidebar melalui alat sidebar_open (nonaktif secara default)',
settingsTitleBarTitle: 'Mode kompatibilitas posisi',
-2
View File
@@ -228,8 +228,6 @@ export const it: Record<string, string> = {
settingsWidthTitle: 'Quota di larghezza predefinita',
settingsWidthDesc: 'La quota predefinita della scheda laterale sulla larghezza della finestra per le nuove conversazioni (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'Apri i file della chat nella barra laterale',
settingsOpenPathDesc: 'Apre i collegamenti ai file nella chat (righe di strumenti, file prodotti, menzioni) nell’editor della barra laterale invece dell’app predefinita di sistema',
settingsOpenToolsTitle: 'Iniettare lo strumento di apertura della barra laterale per il modello',
settingsOpenToolsDesc: 'Se attivato, il modello può aprire file, cartelle e pagine HTTP(S) nella barra laterale tramite lo strumento sidebar_open (disattivato per impostazione predefinita)',
settingsTitleBarTitle: 'Modalità di compatibilità della posizione',
-2
View File
@@ -237,8 +237,6 @@ export const ja: Record<string, string> = {
settingsWidthTitle: 'デフォルト幅の比率',
settingsWidthDesc: '新規会話時のサイドカードがウィンドウ幅に占める割合 (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'チャット内ファイルをサイドバーで開く',
settingsOpenPathDesc: 'チャット内のファイルリンク(ツール行、産物リスト、ファイル言及)クリック時に、システム既定アプリではなくサイドバーエディターで開く',
settingsOpenToolsTitle: 'モデルにサイドバー開くツールを注入',
settingsOpenToolsDesc: 'オンにすると、モデルは sidebar_open ツールでサイドバーにファイル・フォルダー・HTTP(S) ページを開ける(デフォルトオフ)',
settingsTitleBarTitle: '位置互換モード',
-2
View File
@@ -229,8 +229,6 @@ export const ko: Record<string, string> = {
settingsWidthTitle: '기본 너비 비율',
settingsWidthDesc: '새 대화에서 사이드 카드가 창 너비에서 차지하는 비율(%) (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: '채팅 영역 파일을 사이드바에서 열기',
settingsOpenPathDesc: '채팅에서 파일 링크(도구 행, 산출물 목록, 파일 언급)를 클릭하면 사이드바 편집기에서 열고, 시스템 기본 앱은 호출하지 않습니다',
settingsOpenToolsTitle: '모델에 사이드바 열기 도구 주입',
settingsOpenToolsDesc: '켜면 모델이 sidebar_open 도구로 사이드바에서 파일·폴더·HTTP(S) 페이지를 열 수 있습니다(기본 꺼짐)',
settingsTitleBarTitle: '위치 호환 모드',
-2
View File
@@ -235,8 +235,6 @@ export const nl: Record<string, string> = {
settingsWidthTitle: 'Standaard breedte-aandeel',
settingsWidthDesc: 'Het standaard breedte-aandeel van de zijkaart voor nieuwe conversaties (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'Chatbestanden in de zijbalk openen',
settingsOpenPathDesc: 'Bestandslinks in de chat (toolrijen, geproduceerde bestanden, vermeldingen) openen in de zijbalk-editor in plaats van de standaard systeemapp',
settingsOpenToolsTitle: 'Zijbalk-openen-tool voor het model injecteren',
settingsOpenToolsDesc: 'Indien ingeschakeld kan het model bestanden, mappen en HTTP(S)-pagina\'s in de zijbalk openen via de sidebar_open-tool (standaard uit)',
settingsTitleBarTitle: 'Positiecompatibiliteitsmodus',
-2
View File
@@ -239,8 +239,6 @@ export const pl: Record<string, string> = {
settingsWidthTitle: 'Domyślny udział szerokości',
settingsWidthDesc: 'Domyślny udział szerokości okna dla karty bocznej w nowych rozmowach (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'Otwieraj pliki czatu w panelu bocznym',
settingsOpenPathDesc: 'Otwieraj linki plików na czacie (wiersze narzędzi, wyprodukowane pliki, wzmianki) w edytorze panelu bocznego zamiast w domyślnej aplikacji systemu',
settingsOpenToolsTitle: 'Wstrzyknij narzędzie otwierania panelu bocznego dla modelu',
settingsOpenToolsDesc: 'Po włączeniu model może otwierać pliki, foldery i strony HTTP(S) w panelu bocznym za pomocą narzędzia sidebar_open (domyślnie wyłączone)',
settingsTitleBarTitle: 'Tryb zgodności pozycji',
-2
View File
@@ -220,8 +220,6 @@ export const pt: Record<string, string> = {
settingsWidthTitle: 'Participação padrão na largura',
settingsWidthDesc: 'Participação padrão do cartão lateral na largura da janela em novas conversas (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'Abrir arquivos do chat na barra lateral',
settingsOpenPathDesc: 'Abrir links de arquivos no chat (linhas de ferramentas, arquivos produzidos, menções) no editor da barra lateral em vez do aplicativo padrão do sistema',
settingsOpenToolsTitle: 'Injetar a ferramenta de abertura na barra lateral para o modelo',
settingsOpenToolsDesc: 'Quando ativado, o modelo pode abrir arquivos, pastas e páginas HTTP(S) na barra lateral pela ferramenta sidebar_open (desativado por padrão)',
settingsTitleBarTitle: 'Modo de compatibilidade de posição',
-2
View File
@@ -234,8 +234,6 @@ export const ru: Record<string, string> = {
settingsWidthTitle: 'Доля ширины по умолчанию',
settingsWidthDesc: 'Доля боковой карточки в ширине окна для новых сессий, в процентах (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'Открывать файлы чата в боковой панели',
settingsOpenPathDesc: 'При щелчке по ссылке на файл в чате (строки инструментов, списки результатов, упоминания) файл открывается в редакторе боковой панели, а не в системном приложении по умолчанию',
settingsOpenToolsTitle: 'Внедрить инструмент открытия в боковой панели для модели',
settingsOpenToolsDesc: 'Если включено, модель может открывать файлы, папки и HTTP(S)-страницы в боковой панели с помощью инструмента sidebar_open (по умолчанию выключено)',
settingsTitleBarTitle: 'Режим совместимости заголовка',
-2
View File
@@ -220,8 +220,6 @@ export const sv: Record<string, string> = {
settingsWidthTitle: 'Standardbreddandel',
settingsWidthDesc: 'Sidokortets standardandel av fönsterbredden för nya konversationer (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'Öppna chattfiler i sidopanelen',
settingsOpenPathDesc: 'Öppna fillänkar i chatten (verktygsrader, producerade filer, omnämnanden) i sidopanelens editor i stället för systemets standardapp',
settingsOpenToolsTitle: 'Injicera sidopanel-öppningsverktyg för modellen',
settingsOpenToolsDesc: 'När aktiverat kan modellen öppna filer, mappar och HTTP(S)-sidor i sidopanelen via sidebar_open-verktyget (av som standard)',
settingsTitleBarTitle: 'Positions kompatibilitetsläge',
-2
View File
@@ -237,8 +237,6 @@ export const th: Record<string, string> = {
settingsWidthTitle: 'สัดส่วนความกว้างเริ่มต้น',
settingsWidthDesc: 'สัดส่วนความกว้างเริ่มต้นของการ์ดด้านข้างจากความกว้างหน้าต่างสำหรับแชทใหม่ (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'เปิดไฟล์แชทในแถบด้านข้าง',
settingsOpenPathDesc: 'เปิดลิงก์ไฟล์ในแชท (แถวเครื่องมือ, ไฟล์ที่สร้าง, การกล่าวถึง) ในตัวแก้ไขแถบด้านข้างแทนแอปเริ่มต้นของระบบ',
settingsOpenToolsTitle: 'ฉีดเครื่องมือเปิดแถบด้านข้างสำหรับโมเดล',
settingsOpenToolsDesc: 'เมื่อเปิดใช้ โมเดลสามารถเปิดไฟล์ โฟลเดอร์ และหน้า HTTP(S) ในแถบด้านข้างผ่านเครื่องมือ sidebar_open (ปิดเป็นค่าเริ่มต้น)',
settingsTitleBarTitle: 'โหมดความเข้ากันได้ของตำแหน่ง',
-2
View File
@@ -237,8 +237,6 @@ export const tr: Record<string, string> = {
settingsWidthTitle: 'Varsayılan genişlik payı',
settingsWidthDesc: 'Yeni sohbetler için yan kartın pencere genişliğindeki varsayılan payı (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'Sohbet dosyalarını kenar çubuğunda aç',
settingsOpenPathDesc: 'Sohbetteki dosya bağlantılarını (araç satırları, üretilen dosyalar, anılmalar) sistem varsayılan uygulaması yerine kenar çubuğu düzenleyicisinde aç',
settingsOpenToolsTitle: 'Model için kenar çubuğu açma aracı enjekte et',
settingsOpenToolsDesc: 'Etkinleştirildiğinde model, sidebar_open aracıyla kenar çubuğunda dosyaları, klasörleri ve HTTP(S) sayfalarını açabilir (varsayılan kapalı)',
settingsTitleBarTitle: 'Konum uyumluluk modu',
-2
View File
@@ -237,8 +237,6 @@ export const vi: Record<string, string> = {
settingsWidthTitle: 'Tỷ lệ chiều rộng mặc định',
settingsWidthDesc: 'Tỷ lệ phần trăm chiều rộng cửa sổ mà thẻ bên chiếm cho phiên mới (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'Mở tệp chat trong thanh bên',
settingsOpenPathDesc: 'Khi nhấp link tệp trong chat (dòng công cụ, danh sách sản phẩm, nhắc tệp), mở trong trình soạn thảo thanh bên thay vì ứng dụng mặc định hệ thống',
settingsOpenToolsTitle: 'Tiêm công cụ mở thanh bên cho mô hình',
settingsOpenToolsDesc: 'Khi bật, mô hình có thể mở tệp, thư mục và trang HTTP(S) trong thanh bên qua công cụ sidebar_open (mặc định tắt)',
settingsTitleBarTitle: 'Chế độ tương thích thanh tiêu đề',
-2
View File
@@ -252,8 +252,6 @@ export const zhHK: Record<string, string> = {
settingsWidthTitle: '預設寬度佔比',
settingsWidthDesc: '新增工作階段時側邊卡片佔視窗寬度的百分比 (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: '聊天區檔案在側邊欄開啟',
settingsOpenPathDesc: '在聊天裡點擊檔案連結(工具行、產物列表、檔案提及)時,在側邊欄編輯器中開啟,不再呼叫系統預設應用程式',
settingsOpenToolsTitle: '為模型注入側邊欄開啟工具',
settingsOpenToolsDesc: '開啟後,模型可透過 sidebar_open 工具在側邊欄主動開啟檔案、資料夾和 HTTP(S) 網頁(預設關閉)',
settingsTitleBarTitle: '位置相容模式',
-2
View File
@@ -252,8 +252,6 @@ export const zhMO: Record<string, string> = {
settingsWidthTitle: '預設寬度佔比',
settingsWidthDesc: '新增工作階段時側邊卡片佔視窗寬度的百分比 (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: '聊天區檔案在側邊欄開啟',
settingsOpenPathDesc: '在聊天裡點擊檔案連結(工具行、產物列表、檔案提及)時,在側邊欄編輯器中開啟,不再呼叫系統預設應用程式',
settingsOpenToolsTitle: '為模型注入側邊欄開啟工具',
settingsOpenToolsDesc: '開啟後,模型可透過 sidebar_open 工具在側邊欄主動開啟檔案、資料夾和 HTTP(S) 網頁(預設關閉)',
settingsTitleBarTitle: '位置相容模式',
-2
View File
@@ -252,8 +252,6 @@ export const zhTW: Record<string, string> = {
settingsWidthTitle: '預設寬度佔比',
settingsWidthDesc: '新增工作階段時側邊卡片佔視窗寬度的百分比 (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: '聊天區檔案在側邊欄開啟',
settingsOpenPathDesc: '在聊天裡點擊檔案連結(工具行、產物列表、檔案提及)時,在側邊欄編輯器中開啟,不再呼叫系統預設應用程式',
settingsOpenToolsTitle: '為模型注入側邊欄開啟工具',
settingsOpenToolsDesc: '開啟後,模型可透過 sidebar_open 工具在側邊欄主動開啟檔案、資料夾和 HTTP(S) 網頁(預設關閉)',
settingsTitleBarTitle: '位置相容模式',
-4
View File
@@ -249,8 +249,6 @@ export const zh = {
settingsWidthTitle: '默认宽度占比',
settingsWidthDesc: '新建会话时侧边卡片占窗口宽度的百分比 (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: '聊天区文件在侧边栏打开',
settingsOpenPathDesc: '在聊天里点击文件链接(工具行、产物列表、文件提及)时,在侧边栏编辑器中打开,不再调用系统默认应用',
settingsOpenToolsTitle: '为模型注入侧边栏打开工具',
settingsOpenToolsDesc: '开启后,模型可通过 sidebar_open 工具在侧边栏主动打开文件、文件夹和 HTTP(S) 网页(默认关闭)',
settingsTitleBarTitle: '位置兼容模式',
@@ -699,8 +697,6 @@ export const en: Record<keyof typeof zh, string> = {
settingsWidthTitle: 'Default width share',
settingsWidthDesc: 'The side card\'s default share of the window width for new conversations (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'Open chat files in the sidebar',
settingsOpenPathDesc: 'Open file links in the chat (tool rows, produced files, mentions) in the sidebar editor instead of the system default app',
settingsOpenToolsTitle: 'Inject the sidebar-open tool for the model',
settingsOpenToolsDesc: 'When enabled, the model can actively open files, folders, and HTTP(S) pages in the sidebar through the sidebar_open tool (off by default)',
settingsTitleBarTitle: 'Position compatibility mode',
-144
View File
@@ -1,144 +0,0 @@
/**
* Interception of the chat's file-open funnel (DSH 0.1.2-alpha hosts).
*
* The alpha funnel is the Typert remote namespace `remote.session`: every
* chat-side file open (tool-row path links, the produced-files row, prose
* file mentions, inline-code paths) funnels through ChatView's injected
* `openFile`, which calls `ctx.remote.session.openWorkspacePath({ path })`
* with the path ALREADY resolved against the session cwd (verified against
* the DSH source: `packages/client/ui-chat/src/client/apply.ts` is the only
* production caller). The host side (`api/session-controller`) hands the
* path to the OS's default application (`openNativePath` → xdg-open on
* Linux). The pre-alpha funnel `ctx.workspaces.openPath` is gone — the
* alpha `IWorkspaces` has no openPath at all.
*
* `remote.session` is a cordis service (the gateway client's
* RemoteNamespaceService, key `remote.session`) whose methods are installed
* as ACCESSOR properties (`Object.defineProperty(service, method, {
* configurable: true, get })`) — the getter reads the method table at
* ACCESS time and returns the invocation closure. A plain assignment would
* not stick (no setter), so the wrapper shadows the method with a data
* property and restores the captured accessor descriptor on dispose.
*
* The namespace service appears asynchronously (the session-controller
* contribution mounts after the connection is up) and is disposed/recreated
* on contribution remounts; the caller therefore enters through
* `ctx.inject(['remote.session'], …)`, which re-fires on every remount.
*
* The wrapper is dependency-free by design (no React / ui-primitives), so
* the takeover logic is unit-testable and the file stays importable from
* the test runtime.
*/
/** The one request shape the funnel method takes (mirror of the host's
* SessionOpenWorkspacePathRequest). */
export interface OpenWorkspacePathRequest {
/** Absolute path (the chat caller already resolved it against the cwd). */
path: string
}
/**
* What the funnel method resolves to (mirror of the typert `RemoteResult`
* envelope): EVERY remote method folds its business value into
* `{ ok: true, value }` / `{ ok: false, error }` — the chat caller branches
* on `result.ok`, so a bare business value would be misread as a failure
* (`result.error.message` reads `message` off undefined).
*/
export type OpenWorkspacePathResult =
| { readonly ok: true; readonly value: { opened: boolean } }
| { readonly ok: false; readonly error: { readonly code: string; readonly message: string; readonly details: object } }
/** The namespace slice the wrapper shadows (mirror of the gateway client's
* RemoteNamespaceService for the `session` namespace). */
export interface OpenWorkspacePathService {
openWorkspacePath(request: OpenWorkspacePathRequest, signal?: AbortSignal): Promise<OpenWorkspacePathResult>
}
/** Per-call decisions the wrapper needs (wired to the store + ctx in the client half). */
export interface OpenPathInterceptDeps {
/**
* Whether to take over this call: the `interceptOpenPath` pref AND the
* editor tab's own enable switch must both be on (an editor that cannot
* open must not swallow opens — they fall through to the Host).
*/
takeoverEnabled(): boolean
/** The session whose scope the sidebar editor loads the file in (current session). */
currentSessionId(): string | undefined
/** Route the open into the sidebar editor (the established openSidebarFile). */
openInSidebar(path: string, sessionId: string): void
/** Route a folder-reveal gesture ("Show in folder" passes '.') into the sidebar explorer. */
revealInExplorer(path: string, sessionId: string): void
}
/**
* Whether a path is the "Show in folder" folder-reveal gesture. The stock
* ui-deliverables row passes `'.'` (the session workspace root, resolved by
* the chat view to `"<cwd>/."`); any path whose final segment is `.` is the
* same gesture. A directory has no editor content, so these opens must reach
* the explorer instead of an editor tab.
*/
export function isFolderRevealPath(path: string): boolean {
if (path === '.' || path === './') return true
const trimmed = path.replace(/[\\/]+$/, '')
return trimmed === '.' || /[\\/]\.$/.test(trimmed)
}
/**
* Shadow `remote.session.openWorkspacePath`: intercepted calls open the file
* in the sidebar editor and resolve with the remote SUCCESS ENVELOPE
* (`{ ok: true, value: { opened: true } }`, so ChatView shows no error
* dialog); anything that declines falls through to the captured original
* closure (the host OS's default application) untouched.
* The one exception is the folder-reveal gesture, which is routed to
* {@link OpenPathInterceptDeps.revealInExplorer} instead.
*
* The original closure is captured by ACCESSING the accessor once at wrap
* time — it invokes whatever method records are mounted at that moment. If
* the contribution remounts its methods while the shadow is installed, the
* captured closure points at the old records; the session namespace is
* effectively permanent in practice, so this is accepted and the shadow is
* re-applied anyway when the remount recreates the service (the caller's
* `ctx.inject` re-fires).
*
* @param service - the `remote.session` namespace service.
* @param deps - per-call takeover decisions.
* @returns the disposer restoring the original accessor descriptor (HMR-safe).
*/
export function wrapOpenWorkspacePath(
service: OpenWorkspacePathService,
deps: OpenPathInterceptDeps,
): () => void {
const KEY = 'openWorkspacePath'
const target = service as object
const descriptor = Object.getOwnPropertyDescriptor(target, KEY)
// Invoke the accessor once to capture the CURRENT invocation closure.
const original = service.openWorkspacePath
// The gateway installs the namespace's whole method group in the same
// synchronous window as the service registration, so an inject-observed
// service always has the method — but a hand-rolled composition might not;
// wrapping nothing would turn fall-throughs into crashes, so decline.
if (typeof original !== 'function') return () => {}
const wrapped = (request: OpenWorkspacePathRequest, signal?: AbortSignal): Promise<OpenWorkspacePathResult> => {
if (deps.takeoverEnabled()) {
const sessionId = deps.currentSessionId()
if (sessionId !== undefined) {
if (isFolderRevealPath(request.path)) deps.revealInExplorer(request.path, sessionId)
else deps.openInSidebar(request.path, sessionId)
return Promise.resolve({ ok: true, value: { opened: true } })
}
}
return original.call(service, request, signal)
}
Object.defineProperty(target, KEY, {
configurable: true,
enumerable: true,
writable: true,
value: wrapped,
})
return () => {
// Restore the exact original property (the gateway's accessor) so a
// chain of wrappers keeps working across disposals in any order.
if (descriptor !== undefined) Object.defineProperty(target, KEY, descriptor)
else Reflect.deleteProperty(target, KEY)
}
}
-3
View File
@@ -76,9 +76,6 @@ export function parsePrefs(value: unknown): SidebarPrefs {
terminalFontSize: typeof record.terminalFontSize === 'number' && Number.isFinite(record.terminalFontSize)
? clampTerminalFontSize(record.terminalFontSize)
: SIDEBAR_PREFS_DEFAULTS.terminalFontSize,
interceptOpenPath: typeof record.interceptOpenPath === 'boolean'
? record.interceptOpenPath
: SIDEBAR_PREFS_DEFAULTS.interceptOpenPath,
editorExplorer: typeof record.editorExplorer === 'boolean'
? record.editorExplorer
: SIDEBAR_PREFS_DEFAULTS.editorExplorer,
+1 -1
View File
@@ -465,7 +465,7 @@ export function matchUrlTarget(tabs: readonly TabDescriptor[], url: URL): TabDes
* The plugin version this service instance reports. Keep in lockstep with
* `package.json`'s version — `tests/service.spec.ts` asserts the pair.
*/
export const SIDEBAR_SERVICE_VERSION = '0.18.1'
export const SIDEBAR_SERVICE_VERSION = '0.19.0-alpha.0'
/**
* Monotonic capability list consumers use to gate new API usage (features
+8 -6
View File
@@ -13,9 +13,10 @@
* shows only the thread's own conversation.
*
* Live streaming: `assistant/message` events only land when a step
* completes, but `assistant/chunk` events stream token-level text and
* reasoning deltas. The mapping accumulates both per block and supersedes
* them with the assembled message once it lands (settled rows).
* completes, but the host's `assistant/live-chunk` rows stream token-level
* text and reasoning deltas (DSH 0.1.5 publishes them outside the session
* log — see assistant-live.ts). The mapping accumulates both per block and
* supersedes them with the assembled message once it lands (settled rows).
*/
import type { DiffHunk, ReadBlockLine } from '@deepseek-ai/dsh-client-ui-primitives'
import type { SidebarHistoryEntry } from '../context-types.ts'
@@ -299,12 +300,13 @@ function resultCard(
/**
* Map a thread child's history rows onto compact transcript rows: the
* inherited fork seed is cut at the last `session/end-seed`, context
* injections map onto a collapsible injection row, `assistant/chunk`
* injections map onto a collapsible injection row, `assistant/live-chunk`
* deltas accumulate into streaming rows per (turn, step, block) and are
* superseded by the assembled `assistant/message`, and tool invocations
* render one expandable line each (arguments, paired result text, failure
* marker; a still-executing call is marked until its result lands).
* @param entries - history rows (event + host-computed view) in seq order.
* @param entries - history rows (event + host-computed view) in seq order,
* the live deltas of the in-flight attempt appended after the durable tail.
* @returns display rows in log order.
*/
export function transcriptRows(entries: readonly SidebarHistoryEntry[], prev?: readonly SidechatTranscriptRow[]): SidechatTranscriptRow[] {
@@ -374,7 +376,7 @@ export function transcriptRows(entries: readonly SidebarHistoryEntry[], prev?: r
rows.push({ kind: 'user', seq: event.seq, text })
break
}
case 'assistant/chunk': {
case 'assistant/live-chunk': {
const chunk = data.chunk as { type?: unknown; text?: unknown } | undefined
if (chunk === null || typeof chunk !== 'object') break
const kind = chunk.type === 'text-delta' ? 'assistant' : chunk.type === 'reasoning-delta' ? 'reasoning' : null
-1
View File
@@ -124,7 +124,6 @@ export const PrefsSchema: z<SidebarPrefs> = z.object({
bottomPanelAutoTerminal: z.boolean().default(true),
terminalFontFamily: z.string().default(''),
terminalFontSize: z.number().step(1).min(TERMINAL_FONT_SIZE_MIN).max(TERMINAL_FONT_SIZE_MAX).default(TERMINAL_FONT_SIZE_DEFAULT),
interceptOpenPath: z.boolean().default(true),
editorExplorer: z.boolean().default(false),
changesDiffFloat: z.boolean().default(true),
workspaceFence: z.boolean().default(true),
+34 -31
View File
@@ -296,14 +296,34 @@ export interface SidebarSessionTitleService {
rename(session: unknown, title: string): { title: string; eventSeq: number }
}
/** The host session-persistence face (mirror of the sessionPersistence
* service): detached inspection of a persisted session, used to compose the
* recorded preset when a Side Chat thread cold-resumes. */
/**
* The host session-persistence face (mirror of the `sessionPersistence`
* service): durable, handle-addressed session storage.
*
* DSH 0.1.5 replaced the detached `inspect(id)` call with an explicit read
* handle: `open(id, 'read')` never takes write ownership and works while
* another process owns the session, `handle.read()` returns one contiguous
* slice of the log, and `close()` releases it. Every cold read in this plugin
* goes through {@link readPersistedSession} so the handle is always closed.
*/
export interface SidebarSessionPersistenceService {
inspect(sessionId: string): Promise<{
meta: { cwd?: string; agentPreset?: string }
events: readonly SidebarSessionEvent[]
}>
open(sessionId: string, access: 'read' | 'write'): Promise<SidebarSessionHandle>
}
/** One open channel onto a stored session (the fields this plugin reads). */
export interface SidebarSessionHandle {
/** Immutable stored header (cwd / agentPreset live here). */
readonly header: { cwd?: string; agentPreset?: string } & Record<string, unknown>
/** Exact fork-inherited prefix length stored with the log. */
readonly inheritedEventCount?: number
/**
* Read a slice of the valid contiguous log.
* @param offset - first logical seq to include (defaults to 0).
* @param length - maximum events (defaults to the rest of the log).
*/
read(offset?: number, length?: number): Promise<{ events: readonly SidebarSessionEvent[] }>
/** Release the handle (idempotent). */
close(): Promise<void>
}
/** The client session list snapshot the sidebar subscribes to. */
@@ -408,30 +428,6 @@ export interface SidebarConversation {
}
}
/**
* The client `remote.session` namespace face (mirror of the gateway client's
* RemoteNamespaceService for the session-controller contribution on alpha
* hosts). The chat's file-open funnel is `openWorkspacePath`: the caller
* resolves the path against the session cwd, and the host hands it to the
* OS's default application. Namespace methods are accessor properties (see
* `client/openpath-intercept.ts` for how the interception shadows them).
*/
export interface SidebarRemoteSessionService {
/**
* Open an absolute path with the Host operating system's default
* application. Resolves with the typert `RemoteResult` envelope
* (`{ ok: true, value: { opened } }` / `{ ok: false, error }`), like every
* remote method — callers branch on `result.ok`.
*/
openWorkspacePath(
request: { path: string },
signal?: AbortSignal,
): Promise<
| { readonly ok: true; readonly value: { opened: boolean } }
| { readonly ok: false; readonly error: { readonly code: string; readonly message: string; readonly details: object } }
>
}
/**
* The invariant service face (mirror of @deepseek-ai/dsh-invariants'
* InvariantRegistry). The upstream augmentation does not reach this Context
@@ -564,6 +560,13 @@ export interface SidebarContextShape {
* LIVE Session instance that appended it.
*/
on(event: string, listener: (session: unknown, event: SidebarSessionEvent) => void): () => void
/**
* The agent's process-local assistant stream (DSH 0.1.5+): one payload per
* `start` / `chunk` / `end` frame, carrying the emitting agent and the
* frame. These frames are NOT session events — see
* {@link ./assistant-live.ts} for why the plugin needs them.
*/
on(event: 'agent/assistant-stream', listener: (payload: { agent?: unknown; frame?: unknown }) => void): () => void
}
/**
+15 -5
View File
@@ -54,7 +54,9 @@ import { AgentOpenRegistry, registerOpenTool, type AgentOpenRequest } from './ag
import { buildJobsApi, type SidebarJobsRoutes } from './jobs-routes.ts'
import { buildSubagentLiveApi, type SidebarSubagentLiveRoutes } from './subagent-live-route.ts'
import { buildSidechatApi } from './sidechat-routes.ts'
import { createAssistantLiveBuffer, type AssistantLiveBuffer } from './assistant-live.ts'
import { readJsonBody, requireString, SidebarError, writeError, writeJson, writeOk } from './wire.ts'
import { readPersistedSession } from './session-store.ts'
export { Config }
export type { SidebarConfig, ResolvedSidebarConfig }
@@ -129,8 +131,8 @@ async function sessionCwdOf(ctx: Context, sessionId: string, clientCwd?: string)
}
const persistence = ctx.get('sessionPersistence')
if (persistence !== undefined) {
const inspected = await persistence.inspect(sessionId)
const metaCwd = inspected.meta.cwd
const persisted = await readPersistedSession(persistence, sessionId)
const metaCwd = persisted.header.cwd
if (metaCwd !== undefined && metaCwd !== '') {
try {
return requireAbsolute(metaCwd)
@@ -298,6 +300,7 @@ function buildApi(
resolved: ResolvedSidebarConfig,
terminalShell: string,
getSettings: () => SidebarSettingsFace | undefined,
assistantLive: AssistantLiveBuffer,
): Record<string, ApiMethod> {
const cwdOf = async (payload: unknown): Promise<{ sessionId: string; cwd: string }> => {
const sessionId = requireString(payload, 'sessionId')
@@ -507,7 +510,7 @@ function buildApi(
const persistence = ctx.get('sessionPersistence')
if (persistence !== undefined) {
try {
events = (await persistence.inspect(sessionId)).events
events = (await readPersistedSession(persistence, sessionId)).events
} catch {
// Cold read unavailable (session never persisted): an empty
// window is the honest answer, not a wire error.
@@ -694,7 +697,7 @@ function buildApi(
// identities are fenced from the generic session RPCs (agent-lookup
// ownership), and the thread is created with a CUSTOM seed the stock
// fork APIs cannot express.
...buildSidechatApi(ctx),
...buildSidechatApi(ctx, assistantLive),
}
}
@@ -859,7 +862,14 @@ export function apply(ctx: Context, config?: SidebarConfig): void {
})
// ── JSON API ────────────────────────────────────────────────────────────
const api = buildApi(ctx, ptyManager, agentPtyRegistry, resolved, terminalShell, () => settingsFace)
// The live assistant stream buffer: DSH 0.1.5 publishes in-flight model
// deltas as process-local `agent/assistant-stream` frames instead of the
// durable `assistant/chunk` events 0.1.2 logged, so the side-chat
// transcript and the inherited in-progress snapshot read them here. The
// effect releases the listener on fiber disposal.
const assistantLive = createAssistantLiveBuffer(ctx)
ctx.effect(() => () => { assistantLive.dispose() }, 'dsh-better-sidebar: live assistant stream buffer')
const api = buildApi(ctx, ptyManager, agentPtyRegistry, resolved, terminalShell, () => settingsFace, assistantLive)
ctx.effect(() => ctx.webServer.register({
kind: 'prefix',
path: '/sidebar/api',
-9
View File
@@ -63,14 +63,6 @@ export interface SidebarPrefs {
* row in the Side card settings.
*/
bottomPanelAutoTerminal: boolean
/**
* Whether chat-side file opens (tool-row path links, the produced-files
* row, prose file mentions — every path that funnels through the client
* runtime's `remote.session.openWorkspacePath`) open in the sidebar editor
* instead of the Host OS's default application. On by default; the editor
* tab's own enable switch gates it too (both must be on for the takeover).
*/
interceptOpenPath: boolean
/**
* Whether the editor tab runs in merged mode: a path input replaces the
* plain header and a toggleable file-tree panel (with a global name
@@ -276,7 +268,6 @@ export const SIDEBAR_PREFS_DEFAULTS: SidebarPrefs = {
bottomPanelAutoTerminal: true,
terminalFontFamily: '',
terminalFontSize: TERMINAL_FONT_SIZE_DEFAULT,
interceptOpenPath: true,
editorExplorer: false,
changesDiffFloat: true,
workspaceFence: true,
+65
View File
@@ -0,0 +1,65 @@
/**
* Cold-session reads over the host `sessionPersistence` service.
*
* DSH 0.1.5 removed the detached `inspect(id)` call: a persisted session is
* read through an explicit handle — `open(id, 'read')` never takes write
* ownership (so it works while the session is live in another process),
* `handle.read()` returns one contiguous slice of the log, and `close()`
* releases it. Every cold read in this plugin goes through
* {@link readPersistedSession} so the handle is always released, even when
* the read throws.
*/
import type { Context, SidebarSessionEvent, SidebarSessionPersistenceService } from './context-types.ts'
/** One persisted session as this plugin reads it. */
export interface PersistedSession {
/** Immutable stored header (cwd / agentPreset live here). */
readonly header: { cwd?: string; agentPreset?: string } & Record<string, unknown>
/** The full stored event log. */
readonly events: readonly SidebarSessionEvent[]
/** Exact fork-inherited prefix length (0 when the header is not seeded). */
readonly inheritedEventCount: number
}
/**
* Read one persisted session's header and full event log.
* @param persistence - the live `sessionPersistence` service.
* @param sessionId - the stored session to read.
* @returns the header and log; rejects when the session does not exist.
*/
export async function readPersistedSession(
persistence: SidebarSessionPersistenceService,
sessionId: string,
): Promise<PersistedSession> {
const handle = await persistence.open(sessionId, 'read')
try {
const { events } = await handle.read()
return {
header: handle.header,
events,
inheritedEventCount: handle.inheritedEventCount ?? 0,
}
} finally {
await handle.close()
}
}
/**
* Resolve the service and read one persisted session, or `undefined` when
* the service is absent or the session cannot be read.
* @param ctx - host plugin context.
* @param sessionId - the stored session to read.
* @returns the persisted session, or `undefined`.
*/
export async function readPersistedSessionOf(
ctx: Context,
sessionId: string,
): Promise<PersistedSession | undefined> {
const persistence = ctx.get('sessionPersistence')
if (persistence === undefined) return undefined
try {
return await readPersistedSession(persistence, sessionId)
} catch {
return undefined
}
}
+143 -12
View File
@@ -17,6 +17,7 @@
* snapshot inside the boundary prompt.
*/
import type { SidebarHistoryEntry, SidebarSessionSummary } from './context-types.ts'
import type { AssistantLiveChunk } from './assistant-live.ts'
/** The durable thread-label prefix (also the row filter in the client list). */
export const SIDE_LABEL_PREFIX = 'Side: '
@@ -81,6 +82,55 @@ export interface SidechatLogEvent {
data: unknown
}
/**
* One live assistant delta on the plugin's wire, mirroring DSH 0.1.5's
* client-only `assistant/live-chunk` presentation row.
*
* DSH 0.1.5 no longer logs `assistant/chunk`: an in-flight attempt's deltas
* are process-local frames (`agent/assistant-stream`, folded by
* {@link ./assistant-live.ts}) and reach the transcript through the plugin's
* own `sidechat.events` route as these rows. They are NOT durable — the
* route returns the current attempt's rows on every poll and the client
* replaces its live set each time; the durable `assistant/message` settles
* them by `turn:step`.
*/
export interface SidechatLiveEvent {
type: 'assistant/live-chunk'
/** Ordering key among live rows only; durable seqs stay authoritative. */
seq: number
time: number
data: {
attemptId: string
turn: number
step: number
/** Dense zero-based position within the attempt. */
index: number
/** The raw model stream chunk. */
chunk: Record<string, unknown>
}
}
/**
* Project buffered live chunks into wire rows.
* @param chunks - the session's active-attempt chunks, in index order.
* @param tailSeq - the session's last durable seq (live rows order after it).
* @returns the rows to append to the transcript feed.
*/
export function liveEventsOf(chunks: readonly AssistantLiveChunk[], tailSeq: number): SidechatLiveEvent[] {
return chunks.map((delta, position) => ({
type: 'assistant/live-chunk',
seq: tailSeq + 1 + position,
time: delta.time,
data: {
attemptId: delta.attemptId,
turn: delta.turn,
step: delta.step,
index: delta.index,
chunk: delta.chunk,
},
}))
}
/** The result of cutting a parent log into a side-thread inheritance. */
export interface SidechatInheritance {
/** The child seed: contiguous from seq 0, ends outside any open turn. */
@@ -210,8 +260,14 @@ const SNAPSHOT_TOTAL_CAP = 8000
/**
* Build the side-thread inheritance for one parent log: the full event log
* up to the click moment, honestly closed when it ends inside an open turn.
* @param events - the parent's log (live or persisted).
* @param live - the parent's in-flight stream chunks (DSH 0.1.5+ publishes
* them outside the log); used only by the snapshot fallback.
*/
export function buildSidechatInheritance(events: readonly SidechatLogEvent[]): SidechatInheritance {
export function buildSidechatInheritance(
events: readonly SidechatLogEvent[],
live: readonly AssistantLiveChunk[] = [],
): SidechatInheritance {
if (events.length === 0) return { seed: [], snapshot: null }
const boundary = lastTurnBoundary(events)
if (boundary < 0 || events[boundary]?.type === 'turn/end') {
@@ -225,7 +281,7 @@ export function buildSidechatInheritance(events: readonly SidechatLogEvent[]): S
// the structured snapshot to the boundary prompt instead.
return {
seed: copyEvents(events.slice(0, boundary)),
snapshot: buildOpenTurnSnapshot(events),
snapshot: buildOpenTurnSnapshot(events, live),
}
}
const seed = copyEvents(events)
@@ -245,16 +301,79 @@ export function buildSidechatInheritance(events: readonly SidechatLogEvent[]): S
return { seed, snapshot: null }
}
/** One assistant content block reduced to the text the snapshot shows. */
function messageTexts(message: unknown): { text: string; reasoning: string } {
const content = (message as { content?: unknown } | undefined)?.content
let text = ''
let reasoning = ''
if (!Array.isArray(content)) return { text, reasoning }
for (const block of content) {
if (block === null || typeof block !== 'object') continue
const candidate = block as { type?: unknown; text?: unknown }
if (typeof candidate.text !== 'string' || candidate.text === '') continue
if (candidate.type === 'text') text += candidate.text
else if (candidate.type === 'reasoning') reasoning += candidate.text
}
return { text, reasoning }
}
/**
* Expand one attempt's compact `AssistantStreamRecord[]` (the durable stream
* DSH 0.1.5 embeds in `assistant/message.stream` and `assistant/attempt.stream`)
* into the text/reasoning it carried. Tool-call records contribute no text.
*/
function streamTexts(stream: unknown): { text: string; reasoning: string } {
let text = ''
let reasoning = ''
if (!Array.isArray(stream)) return { text, reasoning }
for (const record of stream) {
if (record === null || typeof record !== 'object') continue
const entry = record as { type?: unknown; texts?: unknown; chunk?: unknown }
if (entry.type === 'text-chunks' || entry.type === 'reasoning-chunks') {
if (!Array.isArray(entry.texts)) continue
const joined = entry.texts.filter((part): part is string => typeof part === 'string').join('')
if (entry.type === 'text-chunks') text += joined
else reasoning += joined
continue
}
if (entry.type === 'chunk') {
const chunk = entry.chunk as { type?: unknown; text?: unknown } | undefined
if (chunk === null || typeof chunk !== 'object' || typeof chunk.text !== 'string') continue
if (chunk.type === 'text-delta') text += chunk.text
else if (chunk.type === 'reasoning-delta') reasoning += chunk.text
}
}
return { text, reasoning }
}
/** One live delta's contribution to the snapshot. */
function liveTexts(chunk: Record<string, unknown>): { text: string; reasoning: string } {
if (typeof chunk.text !== 'string' || chunk.text === '') return { text: '', reasoning: '' }
if (chunk.type === 'text-delta') return { text: chunk.text, reasoning: '' }
if (chunk.type === 'reasoning-delta') return { text: '', reasoning: chunk.text }
return { text: '', reasoning: '' }
}
/**
* Structured text snapshot of the parent's OPEN turn (from its `turn/start`
* to the log tail): the accumulated assistant/reasoning output verbatim
* (code blocks ride the raw deltas) and the tool activity — executed tools
* with their result text, the still-executing one marked. Returns null when
* there is no open turn or nothing to show.
* to the log tail): the assistant/reasoning output so far and the tool
* activity — executed tools with their result text, the still-executing one
* marked. Returns null when there is no open turn or nothing to show.
*
* The in-flight step's text is NOT in the log on DSH 0.1.5 (the model stream
* is process-local until it settles), so it comes from `live`; settled steps
* read their durable `assistant/message` content, and a failed attempt reads
* its embedded `assistant/attempt.stream`.
* @param events - the parent's log.
* @param live - the parent's in-flight stream chunks, in index order.
*/
export function buildOpenTurnSnapshot(events: readonly SidechatLogEvent[]): string | null {
export function buildOpenTurnSnapshot(
events: readonly SidechatLogEvent[],
live: readonly AssistantLiveChunk[] = [],
): string | null {
const boundary = lastTurnBoundary(events)
if (boundary < 0 || events[boundary]?.type !== 'turn/start') return null
const openTurn = numberAt(dataOf(events[boundary]!), 'turn')
let text = ''
let reasoning = ''
const tools: string[] = []
@@ -267,11 +386,16 @@ export function buildOpenTurnSnapshot(events: readonly SidechatLogEvent[]): stri
pendingCalls.clear()
continue
}
if (event.type === 'assistant/chunk') {
const chunk = data.chunk as { type?: unknown; text?: unknown } | undefined
if (chunk === null || typeof chunk !== 'object') continue
if (chunk.type === 'text-delta' && typeof chunk.text === 'string') text += chunk.text
else if (chunk.type === 'reasoning-delta' && typeof chunk.text === 'string') reasoning += chunk.text
if (event.type === 'assistant/message') {
const settled = messageTexts(data.message)
text += settled.text
reasoning += settled.reasoning
continue
}
if (event.type === 'assistant/attempt') {
const attempt = streamTexts(data.stream)
text += attempt.text
reasoning += attempt.reasoning
continue
}
if (event.type === 'tool/call') {
@@ -304,6 +428,13 @@ export function buildOpenTurnSnapshot(events: readonly SidechatLogEvent[]): stri
const line = `- \`${call.name}\` (executing) — arguments: \`${call.args}\``
tools.push(line)
}
// The in-flight step: its deltas never reached the log.
for (const delta of live) {
if (delta.turn !== openTurn) continue
const contribution = liveTexts(delta.chunk)
text += contribution.text
reasoning += contribution.reasoning
}
const sections: string[] = []
if (text.trim() !== '') sections.push(`Assistant output so far:\n\n${text}`)
if (reasoning.trim() !== '') sections.push(`Reasoning so far:\n\n${reasoning}`)
+26 -10
View File
@@ -37,17 +37,21 @@ import type {
import {
boundaryDelivered,
buildSidechatInheritance,
liveEventsOf,
resolvePresetId,
SIDE_BOUNDARY_PROMPT,
SIDE_INJECTION_PLUGIN,
SIDE_NEW_THREAD_TITLE,
sideLabel,
type SeedEvent,
type SidechatLiveEvent,
type SidechatLogEvent,
type SidechatThreadInfo,
threadOwnLogEvents,
} from './sidechat-core.ts'
import type { AssistantLiveBuffer } from './assistant-live.ts'
import { requireString, SidebarError } from './wire.ts'
import { readPersistedSession } from './session-store.ts'
/** The six Side Chat routes of the sidebar API (wire method names). */
export interface SidechatRoutes {
@@ -66,8 +70,10 @@ export interface SidechatRoutes {
'sidechat.info'(payload: unknown): Promise<SidechatThreadInfo>
/** The thread's OWN transcript events, seed-cut host-side (the inherited
* parent log never crosses the wire); `afterSeq` narrows the response to
* the delta beyond it (poll tail). */
'sidechat.events'(payload: unknown): Promise<{ events: SidechatLogEvent[] }>
* the delta beyond it (poll tail). `live` carries the thread's in-flight
* model deltas, which DSH 0.1.5 publishes outside the session log — it is
* the CURRENT attempt's rows on every poll, never a delta. */
'sidechat.events'(payload: unknown): Promise<{ events: SidechatLogEvent[]; live: SidechatLiveEvent[] }>
}
/** Timeout guarding the create call (the registry detaches it before the
@@ -117,8 +123,8 @@ async function composePersistedSetup(
if (persistence === undefined) {
return () => Promise.resolve()
}
const inspected = await persistence.inspect(childId)
const presetId = resolvePresetId(inspected.meta, inspected.events)
const inspected = await readPersistedSession(persistence, childId)
const presetId = resolvePresetId(inspected.header, inspected.events)
const presets = ctx.get('agentPresets') as SidebarAgentPresetsService | undefined
if (presets === undefined || presetId === undefined) {
return () => Promise.resolve()
@@ -186,7 +192,7 @@ async function threadLogEvents(ctx: Context, childId: string): Promise<readonly
throw new SidebarError('sidechat-error', 'the session persistence service is unavailable', 503)
}
try {
const inspected = await persistence.inspect(childId)
const inspected = await readPersistedSession(persistence, childId)
return inspected.events as unknown as readonly SidechatLogEvent[]
} catch (error: unknown) {
throw new SidebarError(
@@ -199,8 +205,11 @@ async function threadLogEvents(ctx: Context, childId: string): Promise<readonly
/** Build the Side Chat routes (all optional services degrade to a wire
* error the tab surfaces inline). The record keys are the FULL wire method
* names the /sidebar/api dispatcher looks up (`api[method]`). */
export function buildSidechatApi(ctx: Context): SidechatRoutes {
* names the /sidebar/api dispatcher looks up (`api[method]`).
* @param ctx - host plugin context.
* @param live - the live assistant stream buffer; absent only in tests that
* never exercise streaming (then every `live` response is empty). */
export function buildSidechatApi(ctx: Context, live?: AssistantLiveBuffer): SidechatRoutes {
return {
'sidechat.start': async (payload: unknown) => {
const sessionId = requireString(payload, 'sessionId')
@@ -213,6 +222,7 @@ export function buildSidechatApi(ctx: Context): SidechatRoutes {
const parentSession = parent.session
const inheritance = buildSidechatInheritance(
parentSession.snapshotEvents() as unknown as readonly SidechatLogEvent[],
live?.chunksFor(sessionId) ?? [],
)
const { agentPreset, setup } = await composeChildSetup(
ctx,
@@ -388,8 +398,8 @@ export function buildSidechatApi(ctx: Context): SidechatRoutes {
const persistence = ctx.get('sessionPersistence') as SidebarSessionPersistenceService | undefined
if (persistence !== undefined) {
try {
const inspected = await persistence.inspect(childId)
const preset = resolvePresetId(inspected.meta, inspected.events)
const inspected = await readPersistedSession(persistence, childId)
const preset = resolvePresetId(inspected.header, inspected.events)
return { live: false, ...(preset === undefined ? {} : { preset }) }
} catch {
// Unknown/gone session: report a bare cold info.
@@ -408,7 +418,13 @@ export function buildSidechatApi(ctx: Context): SidechatRoutes {
const events = await threadLogEvents(ctx, childId)
const own = threadOwnLogEvents(events)
const fresh = rawAfter === undefined ? own : own.filter(event => event.seq > rawAfter)
return { events: fresh.length > EVENTS_CAP ? fresh.slice(fresh.length - EVENTS_CAP) : fresh }
const tailSeq = own.at(-1)?.seq ?? -1
return {
events: fresh.length > EVENTS_CAP ? fresh.slice(fresh.length - EVENTS_CAP) : fresh,
// The live rows ride every poll: they are process-local, so there is
// no durable seq to page them by. An idle thread has none.
live: liveEventsOf(live?.chunksFor(childId) ?? [], tailSeq),
}
},
}
}
+5 -3
View File
@@ -39,9 +39,11 @@ export interface LastActivity {
/**
* Fold a session event log into the last text output + last tool call (each
* is the LAST occurrence in event order). Lifecycle events and raw
* `assistant/chunk` rows are ignored — the card shows what the subagent is
* doing right now, not its plumbing. The scan runs BACKWARD from the newest
* is the LAST occurrence in event order). Lifecycle events and raw stream
* rows are ignored — the card shows what the subagent is doing right now,
* not its plumbing. (DSH 0.1.5 publishes the raw deltas as process-local
* frames instead of log events, so only the assembled `assistant/message`
* reaches this scan.) The scan runs BACKWARD from the newest
* event and stops once both fields are found, so a long history costs only
* the recent tail in the common case.
* @param events - the session's append-only event log (oldest → newest).
+132
View File
@@ -0,0 +1,132 @@
/**
* Unit tests for the live assistant stream buffer (src/assistant-live.ts):
* the fold that replaces DSH 0.1.2's durable `assistant/chunk` events with
* the process-local `agent/assistant-stream` frames 0.1.5 publishes instead.
*/
import { describe, expect, it, vi } from 'vitest'
import { createAssistantLiveBuffer, LIVE_CHUNK_CAP, LIVE_SESSION_CAP } from '../src/assistant-live.ts'
import type { Context } from '../src/context-types.ts'
/** A context whose `on` records the stream listener the buffer installs. */
function harness(): {
ctx: Context
emit: (payload: unknown) => void
off: ReturnType<typeof vi.fn>
} {
let listener: ((payload: unknown) => void) | undefined
const off = vi.fn()
const ctx = {
on: (event: string, fn: (payload: unknown) => void) => {
expect(event).toBe('agent/assistant-stream')
listener = fn
return off
},
} as unknown as Context
return { ctx, emit: payload => { listener?.(payload) }, off }
}
/** One frame as DSH emits it, for the session named by `sessionId`. */
function frame(sessionId: string, frame: Record<string, unknown>): Record<string, unknown> {
return { agent: { session: { id: sessionId } }, frame }
}
describe('createAssistantLiveBuffer', () => {
it('folds start + chunk frames into ordered per-session chunks', () => {
const { ctx, emit } = harness()
const live = createAssistantLiveBuffer(ctx)
emit(frame('s1', { type: 'start', attemptId: 'a1', revision: 1, turn: 2, step: 1 }))
emit(frame('s1', { type: 'chunk', attemptId: 'a1', revision: 1, index: 0, time: 10, chunk: { type: 'text-delta', index: 0, text: 'he' } }))
emit(frame('s1', { type: 'chunk', attemptId: 'a1', revision: 1, index: 1, time: 11, chunk: { type: 'reasoning-delta', index: 1, text: 'hmm' } }))
const chunks = live.chunksFor('s1')
expect(chunks.map(chunk => [chunk.turn, chunk.step, chunk.index, chunk.time])).toEqual([
[2, 1, 0, 10],
[2, 1, 1, 11],
])
expect(chunks[1]?.chunk).toEqual({ type: 'reasoning-delta', index: 1, text: 'hmm' })
expect(live.chunksFor('other')).toEqual([])
})
it('ignores payloads it cannot identify', () => {
const { ctx, emit } = harness()
const live = createAssistantLiveBuffer(ctx)
emit(undefined)
emit({})
emit({ agent: {}, frame: { type: 'chunk', attemptId: 'a1', index: 0, chunk: {} } })
emit({ agent: { session: { id: 's1' } }, frame: { type: 'chunk', attemptId: 'a1', index: 0, chunk: {} } })
// A chunk with no start is not an attempt this buffer knows.
expect(live.chunksFor('s1')).toEqual([])
emit(frame('s1', { type: 'start', attemptId: 'a1', turn: 1, step: 1 }))
emit(frame('s1', { type: 'chunk', attemptId: 'a1', index: 0, chunk: 'not-a-record' }))
expect(live.chunksFor('s1')).toEqual([])
})
it('drops the attempt on a gap rather than splicing incomplete text', () => {
const { ctx, emit } = harness()
const live = createAssistantLiveBuffer(ctx)
emit(frame('s1', { type: 'start', attemptId: 'a1', turn: 1, step: 1 }))
emit(frame('s1', { type: 'chunk', attemptId: 'a1', index: 0, chunk: { type: 'text-delta', text: 'a' } }))
emit(frame('s1', { type: 'chunk', attemptId: 'a1', index: 2, chunk: { type: 'text-delta', text: 'c' } }))
expect(live.chunksFor('s1')).toEqual([])
})
it('drops the attempt when a chunk names another attempt', () => {
const { ctx, emit } = harness()
const live = createAssistantLiveBuffer(ctx)
emit(frame('s1', { type: 'start', attemptId: 'a1', turn: 1, step: 1 }))
emit(frame('s1', { type: 'chunk', attemptId: 'a2', index: 0, chunk: { type: 'text-delta', text: 'a' } }))
expect(live.chunksFor('s1')).toEqual([])
})
it('clears the buffer when the attempt ends', () => {
const { ctx, emit } = harness()
const live = createAssistantLiveBuffer(ctx)
emit(frame('s1', { type: 'start', attemptId: 'a1', turn: 1, step: 1 }))
emit(frame('s1', { type: 'chunk', attemptId: 'a1', index: 0, chunk: { type: 'text-delta', text: 'a' } }))
emit(frame('s1', { type: 'end', attemptId: 'a1', index: 1, outcome: { kind: 'abandoned' } }))
expect(live.chunksFor('s1')).toEqual([])
})
it('a new start replaces the previous attempt of the same session', () => {
const { ctx, emit } = harness()
const live = createAssistantLiveBuffer(ctx)
emit(frame('s1', { type: 'start', attemptId: 'a1', turn: 1, step: 1 }))
emit(frame('s1', { type: 'chunk', attemptId: 'a1', index: 0, chunk: { type: 'text-delta', text: 'old' } }))
emit(frame('s1', { type: 'start', attemptId: 'a2', turn: 1, step: 2 }))
expect(live.chunksFor('s1')).toEqual([])
emit(frame('s1', { type: 'chunk', attemptId: 'a2', index: 0, chunk: { type: 'text-delta', text: 'new' } }))
expect(live.chunksFor('s1').map(chunk => chunk.chunk['text'])).toEqual(['new'])
})
it('caps one attempt at LIVE_CHUNK_CAP chunks (oldest dropped)', () => {
const { ctx, emit } = harness()
const live = createAssistantLiveBuffer(ctx)
emit(frame('s1', { type: 'start', attemptId: 'a1', turn: 1, step: 1 }))
for (let index = 0; index < LIVE_CHUNK_CAP + 3; index++) {
emit(frame('s1', { type: 'chunk', attemptId: 'a1', index, chunk: { type: 'text-delta', text: String(index) } }))
}
const chunks = live.chunksFor('s1')
expect(chunks).toHaveLength(LIVE_CHUNK_CAP)
expect(chunks[0]?.index).toBe(3)
expect(chunks.at(-1)?.index).toBe(LIVE_CHUNK_CAP + 2)
})
it('caps tracked sessions at LIVE_SESSION_CAP (oldest dropped)', () => {
const { ctx, emit } = harness()
const live = createAssistantLiveBuffer(ctx)
for (let index = 0; index < LIVE_SESSION_CAP + 1; index++) {
emit(frame(`s${index}`, { type: 'start', attemptId: 'a1', turn: 1, step: 1 }))
}
expect(live.chunksFor('s0')).toEqual([])
expect(live.chunksFor(`s${LIVE_SESSION_CAP}`)).toEqual([])
// The most recent session is the only live one, and it has no chunks yet.
emit(frame(`s${LIVE_SESSION_CAP}`, { type: 'chunk', attemptId: 'a1', index: 0, chunk: { type: 'text-delta', text: 'x' } }))
expect(live.chunksFor(`s${LIVE_SESSION_CAP}`)).toHaveLength(1)
})
it('dispose unbinds the stream listener', () => {
const { ctx, off } = harness()
const live = createAssistantLiveBuffer(ctx)
live.dispose()
expect(off).toHaveBeenCalledTimes(1)
})
})
+40 -24
View File
@@ -2,14 +2,16 @@
import { mkdirSync } from 'node:fs'
import { homedir } from 'node:os'
import { join } from 'node:path'
import { expect, test, type Page } from '@playwright/test'
import { PAGE_URL } from './host'
import { expect, test, type APIRequestContext, type Page } from '@playwright/test'
import { PAGE_URL, createHostApi, hostRpc } from './host'
const WORKSPACE_PATH = process.env.DSH_E2E_DESKTOP_WORKSPACE
?? (process.env.DSH_E2E_WORKSPACE === undefined
? join(homedir(), 'dsh-e2e-desktop-layout-workspace')
: `${process.env.DSH_E2E_WORKSPACE}-desktop-layout`)
let api: APIRequestContext
async function dismissOnboarding(page: Page): Promise<void> {
await expect
.poll(() => page.getByRole('button', { name: /^(Continue|Configure later|继续|稍后配置)$/ }).count(), { timeout: 60_000 })
@@ -28,8 +30,20 @@ async function dismissOnboarding(page: Page): Promise<void> {
throw new Error('onboarding takeovers did not settle after visible dismissal')
}
test.beforeAll(() => {
test.beforeAll(async () => {
mkdirSync(WORKSPACE_PATH, { recursive: true })
// Seed one workspace + one session through the host's own RPC surface (the
// calls the UI makes) instead of driving the workspace picker: DSH 0.1.5
// ships BOTH a browser directory dialog and an OS-native chooser and picks
// one by platform, so a UI-driven flow only works on hosts whose chooser
// renders in the page.
api = await createHostApi()
const workspace = await hostRpc<{ workspace: { workspaceId: string } }>(api, 'workspace.create', { path: WORKSPACE_PATH })
await hostRpc(api, 'session.create', { workspaceId: workspace.value.workspace.workspaceId })
})
test.afterAll(async () => {
await api?.dispose()
})
test('right panel keeps desktop session actions in their header positions', async ({ page }) => {
@@ -45,38 +59,37 @@ test('right panel keeps desktop session actions in their header positions', asyn
await page.goto(PAGE_URL, { waitUntil: 'domcontentloaded' })
await dismissOnboarding(page)
await page.getByRole('button', { name: /^(Choose workspace|选择工作区)$/ }).click()
const picker = page.getByRole('dialog', { name: /^(Select Workspace Directory|选择工作区目录)$/ })
await expect(picker).toBeVisible()
await picker.getByRole('button', { name: /^(Edit path|编辑路径)$/ }).click()
const pathInput = picker.getByRole('textbox', { name: /^(Edit path|编辑路径)$/ })
await pathInput.fill(WORKSPACE_PATH)
await pathInput.press('Enter')
await expect(pathInput).toBeHidden()
await picker.getByRole('button', { name: /^(Open|打开)$/ }).click()
// Target the ACTIVE composer by its accessible name. DSH 0.1.2-alpha hosts
// keep an INERT `data-composer-input` "Choose workspace" textbox (with
// contenteditable="false") later in DOM order than the real composer — a
// bare `getByRole('textbox').last()` resolved to that one and fill() threw
// ("not an <input>, <textarea> or [contenteditable] element").
// The seeded blank session opens with its workspace already attached (the
// chip shows the workspace title), so the hero composer is live without
// touching the workspace picker at all. One message gives the header its
// real actions, which this spec measures.
const composer = page.getByRole('textbox', { name: /^(Describe what you want to build|描述你想要构建)/ })
await expect(composer).toBeVisible()
await composer.fill('Create a desktop side-card layout test session.')
await page.getByRole('button', { name: /^(Send message|发送消息)$/ }).click()
// DSH 0.1.5's composer is a Lexical contenteditable: fill() writes the DOM
// but not the editor model, so the send button would stay disabled — type
// real keys instead.
await composer.click()
await page.keyboard.type('Create a desktop side-card layout test session.')
const send = page.getByRole('button', { name: /^(Send message|发送消息)$/ })
await expect(send).toBeEnabled({ timeout: 30_000 })
await send.click()
const sessionLog = page.getByRole('button', { name: 'Session log', exact: true })
const sessionLogLabel = sessionLog.getByText('Session log', { exact: true })
await expect(sessionLogLabel, 'the desktop session-log action starts as a text button').toBeVisible({ timeout: 30_000 })
const root = page.locator('#root')
const frame = page.locator('#root [data-dsh-frame], #root > [data-slot="root"] > div').first()
const appSidebarExpanded = root.getByRole('button', { name: /^(Collapse sidebar|收起侧边栏)$/ })
const sidebar = page.locator('[data-dsh-better-sidebar]')
const sessionLog = page.getByRole('button', { name: 'Session log', exact: true })
const sessionLogLabel = sessionLog.getByText('Session log', { exact: true })
await expect(sessionLogLabel, 'the desktop session-log action starts as a text button').toBeVisible({ timeout: 30_000 })
await expect(appSidebarExpanded, 'the desktop frame starts with its app sidebar expanded').toBeVisible()
const beforeRoot = await root.boundingBox()
const beforeFrame = await frame.boundingBox()
expect(beforeRoot).not.toBeNull()
expect(beforeFrame).not.toBeNull()
// The header action's own width is the baseline: the assertion below is
// "the panel does not squeeze it", not a host-styling constant.
const beforeSessionLog = await sessionLog.boundingBox()
expect(beforeSessionLog).not.toBeNull()
await sidebar.getByRole('button', { name: /^(Expand sidebar|展开侧边栏)$/ }).click()
const panel = page.locator('[data-dsh-panel]:not([data-dsh-bottom-panel])')
@@ -103,7 +116,10 @@ test('right panel keeps desktop session actions in their header positions', asyn
const [sessionLogBox, panelBox] = await Promise.all([sessionLog.boundingBox(), panel.boundingBox()])
expect(sessionLogBox).not.toBeNull()
expect(panelBox).not.toBeNull()
expect(sessionLogBox!.width, 'the desktop session-log action must keep its full button width').toBeGreaterThanOrEqual(110)
expect(
sessionLogBox!.width,
'the desktop session-log action must keep its full button width',
).toBeGreaterThanOrEqual(beforeSessionLog!.width - 1)
expect(sessionLogBox!.x + sessionLogBox!.width, 'the session-log action must not overlap the plugin panel chrome').toBeLessThanOrEqual(panelBox!.x - 8)
expect(pageErrors).toEqual([])
expect(consoleErrors).toEqual([])
-281
View File
@@ -1,281 +0,0 @@
import { describe, expect, it } from 'vitest'
import { registerOpenPathInterception } from '../src/client/intercept.tsx'
import {
isFolderRevealPath,
wrapOpenWorkspacePath,
type OpenPathInterceptDeps,
type OpenWorkspacePathService,
} from '../src/client/openpath-intercept.ts'
import { createSidebarStore } from '../src/client/state.ts'
import type { Context } from '../src/context-types.ts'
/**
* A fake of the gateway client's RemoteNamespaceService: the method is an
* ACCESSOR property (getter returning the invocation closure), exactly like
* `Object.defineProperty(service, method, { configurable: true, get })` in
* the real implementation — the wrapper must shadow it with a data property
* and restore the accessor on dispose.
*/
function fakeNamespaceService(opened: string[]): OpenWorkspacePathService {
const target = {} as Record<string, unknown>
Object.defineProperty(target, 'openWorkspacePath', {
configurable: true,
enumerable: true,
get() {
return async (request: { path: string }) => {
opened.push(request.path)
return { ok: true, value: { opened: true } } as const
}
},
})
return target as unknown as OpenWorkspacePathService
}
describe('open-path interception (wrapOpenWorkspacePath)', () => {
const deps = (overrides: Partial<OpenPathInterceptDeps> = {}): OpenPathInterceptDeps & {
sidebar: string[]
revealed: string[]
} => {
const sidebar: string[] = []
const revealed: string[] = []
return {
sidebar,
revealed,
takeoverEnabled: () => true,
currentSessionId: () => 's1',
openInSidebar: (path, sessionId) => { sidebar.push(`${sessionId}:${path}`) },
revealInExplorer: (path, sessionId) => { revealed.push(`${sessionId}:${path}`) },
...overrides,
}
}
it('routes an intercepted open into the sidebar and resolves with the remote success envelope', async () => {
const opened: string[] = []
const service = fakeNamespaceService(opened)
const d = deps()
const restore = wrapOpenWorkspacePath(service, d)
// The caller (ChatView's openFile) branches on `result.ok` and reads
// `result.error.message` on falsy — a bare business value would surface
// as a bogus "cannot open" dialog.
await expect(service.openWorkspacePath({ path: '/abs/a.ts' }))
.resolves.toEqual({ ok: true, value: { opened: true } })
expect(opened).toEqual([])
expect(d.sidebar).toEqual(['s1:/abs/a.ts'])
restore()
})
it('falls through to the original when the takeover is disabled', async () => {
const opened: string[] = []
const service = fakeNamespaceService(opened)
const d = deps({ takeoverEnabled: () => false })
const restore = wrapOpenWorkspacePath(service, d)
await service.openWorkspacePath({ path: '/abs/a.ts' })
expect(opened).toEqual(['/abs/a.ts'])
expect(d.sidebar).toEqual([])
restore()
})
it('falls through when no session is current (nothing to scope the editor load to)', async () => {
const opened: string[] = []
const service = fakeNamespaceService(opened)
const d = deps({ currentSessionId: () => undefined })
const restore = wrapOpenWorkspacePath(service, d)
await service.openWorkspacePath({ path: '/abs/a.ts' })
expect(opened).toEqual(['/abs/a.ts'])
expect(d.sidebar).toEqual([])
restore()
})
it('passes the current session into the sidebar opener', async () => {
const opened: string[] = []
const service = fakeNamespaceService(opened)
let current = 's1'
const d = deps({ currentSessionId: () => current })
const restore = wrapOpenWorkspacePath(service, d)
await service.openWorkspacePath({ path: '/abs/a.ts' })
current = 's2'
await service.openWorkspacePath({ path: '/abs/b.ts' })
expect(d.sidebar).toEqual(['s1:/abs/a.ts', 's2:/abs/b.ts'])
restore()
})
it('routes the folder-reveal gesture to the explorer, not the editor', async () => {
const opened: string[] = []
const service = fakeNamespaceService(opened)
const d = deps()
const restore = wrapOpenWorkspacePath(service, d)
for (const path of ['.', './', '/w/.', 'C:\\w\\.']) {
await service.openWorkspacePath({ path })
}
expect(opened).toEqual([])
expect(d.sidebar).toEqual([])
expect(d.revealed).toEqual(['s1:.', 's1:./', 's1:/w/.', 's1:C:\\w\\.'])
restore()
})
it('restores the original accessor on dispose (HMR-safe)', async () => {
const opened: string[] = []
const service = fakeNamespaceService(opened)
const before = Object.getOwnPropertyDescriptor(service, 'openWorkspacePath')
expect(typeof before?.get).toBe('function')
const d = deps()
const restore = wrapOpenWorkspacePath(service, d)
// While wrapped, the property is our data-property shadow.
const during = Object.getOwnPropertyDescriptor(service, 'openWorkspacePath')
expect(during?.get).toBeUndefined()
expect(typeof during?.value).toBe('function')
restore()
const after = Object.getOwnPropertyDescriptor(service, 'openWorkspacePath')
expect(typeof after?.get).toBe('function')
await service.openWorkspacePath({ path: '/abs/a.ts' })
expect(opened).toEqual(['/abs/a.ts'])
expect(d.sidebar).toEqual([])
})
it('propagates a rejection from the original (no swallowing)', async () => {
const target = {} as Record<string, unknown>
Object.defineProperty(target, 'openWorkspacePath', {
configurable: true,
enumerable: true,
get: () => async () => { throw new Error('host refused') },
})
const service = target as unknown as OpenWorkspacePathService
const d = deps({ takeoverEnabled: () => false })
const restore = wrapOpenWorkspacePath(service, d)
await expect(service.openWorkspacePath({ path: '/abs/a.ts' })).rejects.toThrow('host refused')
restore()
})
it('declines to wrap when the method is not installed (hand-rolled composition)', async () => {
const service = {} as OpenWorkspacePathService
const d = deps()
const restore = wrapOpenWorkspacePath(service, d)
expect(service.openWorkspacePath).toBeUndefined()
expect(d.sidebar).toEqual([])
restore()
})
})
describe('isFolderRevealPath', () => {
it('recognizes the dot gestures on both separator styles', () => {
expect(isFolderRevealPath('.')).toBe(true)
expect(isFolderRevealPath('./')).toBe(true)
expect(isFolderRevealPath('/w/.')).toBe(true)
expect(isFolderRevealPath('/w/./')).toBe(true)
expect(isFolderRevealPath('C:\\w\\.')).toBe(true)
expect(isFolderRevealPath('/w/a.ts')).toBe(false)
expect(isFolderRevealPath('/w/.hidden')).toBe(false)
})
})
describe('open-path interception wiring', () => {
/**
* A client-context fake whose `inject` mimics cordis: the callback runs
* once the dependency appears (here: when `mount()` is called) and its
* effects dispose with the fiber.
*/
function fakeCtx(opened: Array<Record<string, unknown>>, remoteSession: OpenWorkspacePathService) {
let effectDisposer: (() => void) | undefined
let injectCallback: ((fctx: unknown) => void) | undefined
const fctx = {
get: (name: string) => (name === 'remote.session' ? remoteSession : undefined),
effect: (fn: () => () => void) => { effectDisposer = fn() },
}
const ctx = {
sessions: {
list: { getSnapshot: () => ({ current: 's1', byId: { s1: { cwd: '/w' } } }) },
},
get: (name: string) => name === 'betterSidebar'
? { openTab: (seed: unknown) => { opened.push(seed as Record<string, unknown>) } }
: undefined,
inject: (_deps: string[], callback: (fctx: unknown) => void) => {
injectCallback = callback
return {
dispose: async () => { effectDisposer?.() },
}
},
/** Test helper: simulate the namespace service appearing. */
mount: () => { injectCallback?.(fctx) },
}
return ctx
}
it('registers through ctx.inject and routes chat opens into the editor tab', async () => {
const opened: Array<Record<string, unknown>> = []
const hostOpened: string[] = []
const remoteSession = fakeNamespaceService(hostOpened)
const ctx = fakeCtx(opened, remoteSession)
const store = createSidebarStore()
const restore = registerOpenPathInterception(ctx as unknown as Context, store)
// Before the namespace mounts, nothing is wrapped.
expect(Object.getOwnPropertyDescriptor(remoteSession, 'openWorkspacePath')?.get).toBeDefined()
ctx.mount()
// Default prefs: the takeover routes the open into the sidebar editor
// with the session-scoped absolute path (chat already resolved it).
await remoteSession.openWorkspacePath({ path: '/w/src/a.ts' })
expect(opened).toEqual([{
type: 'editor',
title: 'a.ts',
path: '/w/src/a.ts',
id: 'editor:/w/src/a.ts',
}])
expect(hostOpened).toEqual([])
// The interceptOpenPath pref off → the original funnel runs untouched.
store.setPrefs({ ...store.getPrefs(), interceptOpenPath: false })
await remoteSession.openWorkspacePath({ path: '/w/src/b.ts' })
expect(hostOpened).toEqual(['/w/src/b.ts'])
expect(opened).toHaveLength(1)
// The editor tab disabled → falls through too (an editor that cannot
// open must not swallow opens).
store.setPrefs({ ...store.getPrefs(), interceptOpenPath: true, tabsEnabled: { editor: false } })
await remoteSession.openWorkspacePath({ path: '/w/src/c.ts' })
expect(hostOpened).toEqual(['/w/src/b.ts', '/w/src/c.ts'])
// Disposal restores the accessor (HMR-safe).
restore()
const descriptor = Object.getOwnPropertyDescriptor(remoteSession, 'openWorkspacePath')
expect(typeof descriptor?.get).toBe('function')
})
it('re-applies the shadow when the namespace service is remounted', async () => {
const opened: Array<Record<string, unknown>> = []
const hostOpened: string[] = []
// A ctx whose inject can fire multiple times (service recreate).
let effectDisposer: (() => void) | undefined
let injectCallback: ((fctx: unknown) => void) | undefined
let current = fakeNamespaceService(hostOpened)
const ctx = {
sessions: { list: { getSnapshot: () => ({ current: 's1', byId: { s1: { cwd: '/w' } } }) } },
get: (name: string) => name === 'betterSidebar'
? { openTab: (seed: unknown) => { opened.push(seed as Record<string, unknown>) } }
: undefined,
inject: (_deps: string[], callback: (fctx: unknown) => void) => {
injectCallback = callback
return { dispose: async () => { effectDisposer?.() } }
},
remount: () => {
effectDisposer?.()
current = fakeNamespaceService(hostOpened)
injectCallback?.({
get: (name: string) => (name === 'remote.session' ? current : undefined),
effect: (fn: () => () => void) => { effectDisposer = fn() },
})
},
}
const store = createSidebarStore()
registerOpenPathInterception(ctx as unknown as Context, store)
ctx.remount()
await current.openWorkspacePath({ path: '/w/src/a.ts' })
expect(opened).toHaveLength(1)
// A remount (dispose + fresh service) must not leak the old shadow and
// must wrap the fresh instance.
ctx.remount()
await current.openWorkspacePath({ path: '/w/src/b.ts' })
expect(opened).toHaveLength(2)
expect(hostOpened).toEqual([])
})
})
+1 -1
View File
@@ -97,6 +97,6 @@ describe('dsh-better-sidebar plugin export shape', () => {
const overridden = (PrefsSchema as unknown as {
(input: Record<string, unknown> | undefined): Record<string, unknown>
})({ openByDefault: false, defaultWidthPercent: 45 })
expect(overridden).toEqual({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, editorExplorer: false, workspaceFence: true, terminalShell: '', terminalShellArgs: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: '', changesDiffFloat: true, tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} })
expect(overridden).toEqual({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, editorExplorer: false, workspaceFence: true, terminalShell: '', terminalShellArgs: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: '', changesDiffFloat: true, tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} })
})
})
+5 -18
View File
@@ -41,7 +41,6 @@ describe('side card preferences', () => {
bottomPanelAutoTerminal: true,
terminalFontFamily: '',
terminalFontSize: 13,
interceptOpenPath: true,
editorExplorer: false, changesDiffFloat: true,
workspaceFence: true,
terminalShell: '',
@@ -75,7 +74,6 @@ describe('side card preferences', () => {
bottomPanelAutoTerminal: true,
terminalFontFamily: '',
terminalFontSize: 13,
interceptOpenPath: true,
editorExplorer: false, changesDiffFloat: true,
workspaceFence: true,
terminalShell: '',
@@ -109,7 +107,6 @@ describe('side card preferences', () => {
bottomPanelAutoTerminal: true,
terminalFontFamily: '',
terminalFontSize: 13,
interceptOpenPath: true,
editorExplorer: false, changesDiffFloat: true,
workspaceFence: true,
terminalShell: '',
@@ -153,16 +150,6 @@ describe('side card preferences', () => {
.toBe(false)
})
it('defaults interceptOpenPath to true; only an explicit false turns the takeover off', async () => {
// Absent or malformed → on (the takeover is the safe default).
expect((await loadPrefs(wire({}))).interceptOpenPath).toBe(true)
expect((await loadPrefs(wire({ interceptOpenPath: 'yes' }))).interceptOpenPath).toBe(true)
expect((await loadPrefs(wire({ interceptOpenPath: 0 }))).interceptOpenPath).toBe(true)
// Explicit booleans survive verbatim.
expect((await loadPrefs(wire({ interceptOpenPath: false }))).interceptOpenPath).toBe(false)
expect((await loadPrefs(wire({ interceptOpenPath: true }))).interceptOpenPath).toBe(true)
})
it('defaults editorExplorer to false; only an explicit true enables the merged editor-explorer', async () => {
// Absent or malformed → off (separate file windows are the default).
expect((await loadPrefs(wire({}))).editorExplorer).toBe(false)
@@ -282,9 +269,9 @@ describe('side card preferences', () => {
const store = createSidebarStore()
// Node environment: no window → the width falls back to PANEL_DEFAULT,
// while the open flag still follows the preference.
store.setPrefs({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, editorExplorer: true, changesDiffFloat: true, workspaceFence: true, terminalShell: '', terminalShellArgs: '', titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: "", tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} })
store.setPrefs({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, editorExplorer: true, changesDiffFloat: true, workspaceFence: true, terminalShell: '', terminalShellArgs: '', titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: "", tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} })
store.setSession('fresh-session')
expect(store.getPrefs()).toEqual({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, editorExplorer: true, changesDiffFloat: true, workspaceFence: true, terminalShell: '', terminalShellArgs: '', titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: "", tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} })
expect(store.getPrefs()).toEqual({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, editorExplorer: true, changesDiffFloat: true, workspaceFence: true, terminalShell: '', terminalShellArgs: '', titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: "", tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} })
const snapshot = store.getSnapshot()
expect(snapshot.sessionId).toBe('fresh-session')
expect(snapshot.state?.panelOpen).toBe(false)
@@ -320,7 +307,7 @@ describe('side card preferences', () => {
it('skips the default seed tab when the editor (files window) type is disabled', () => {
const store = createSidebarStore()
store.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, editorExplorer: true, changesDiffFloat: true, workspaceFence: true, terminalShell: '', terminalShellArgs: '', titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: "", tabsEnabled: { editor: false }, viewersEnabled: {}, pluginSettings: {} })
store.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, editorExplorer: true, changesDiffFloat: true, workspaceFence: true, terminalShell: '', terminalShellArgs: '', titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: "", tabsEnabled: { editor: false }, viewersEnabled: {}, pluginSettings: {} })
store.setSession('no-editor')
const state = store.getSnapshot().state!
const tabs = allLeaves(state.splits).flatMap(leaf => leaf.tabs)
@@ -330,7 +317,7 @@ describe('side card preferences', () => {
// editorExplorer modes.
for (const editorExplorer of [true, false]) {
const openStore = createSidebarStore()
openStore.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, editorExplorer, changesDiffFloat: true, workspaceFence: true, terminalShell: '', terminalShellArgs: '', titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: "", tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} })
openStore.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, editorExplorer, changesDiffFloat: true, workspaceFence: true, terminalShell: '', terminalShellArgs: '', titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: "", tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} })
openStore.setSession(`with-editor-${editorExplorer}`)
const openTabs = allLeaves(openStore.getSnapshot().state!.splits).flatMap(leaf => leaf.tabs)
expect(openTabs.map(tab => tab.type)).toEqual(['editor'])
@@ -340,7 +327,7 @@ describe('side card preferences', () => {
it('seeds the empty editor home tab (files window) in both editorExplorer modes', () => {
for (const editorExplorer of [true, false]) {
const store = createSidebarStore()
store.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, editorExplorer, changesDiffFloat: true, workspaceFence: true, terminalShell: '', terminalShellArgs: '', titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: "", tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} })
store.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, editorExplorer, changesDiffFloat: true, workspaceFence: true, terminalShell: '', terminalShellArgs: '', titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: "", tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} })
store.setSession(`fresh-${editorExplorer}`)
const tabs = allLeaves(store.getSnapshot().state!.splits).flatMap(leaf => leaf.tabs)
expect(tabs).toHaveLength(1)
+24
View File
@@ -0,0 +1,24 @@
/**
* Vitest global setup.
*
* Node ≥ 22 defines a `localStorage` accessor on globalThis that resolves to
* `undefined` unless the process was started with `--localstorage-file`
* ("localStorage is not available because --localstorage-file was not
* provided"). Because that key already exists, the jsdom environment's
* global population never installs the document's own store, so a test
* calling the bare global `localStorage` throws "Cannot read properties of
* undefined" even though jsdom has a working one.
*
* Bridge the two: point the global at the jsdom window's Storage. The jsdom
* environment exposes its instance as `globalThis.jsdom`; node-only suites
* (no jsdom global) keep Node's accessor untouched.
*/
const jsdomWindow = (globalThis as { jsdom?: { window?: Window } }).jsdom?.window
const storage = jsdomWindow?.localStorage ?? (typeof window === 'undefined' ? undefined : window.localStorage)
if (storage !== undefined) {
Object.defineProperty(globalThis, 'localStorage', {
value: storage,
configurable: true,
writable: true,
})
}
+10 -11
View File
@@ -90,14 +90,14 @@ describe('SideCardSection declarative inventory', () => {
expect(html).toContain('>explorer<')
expect(html).toContain('data-icon="subagent"')
expect(html).toContain('>Subagents<')
// Default prefs: only the interceptOpenPath switch is checked (openByDefault
// now defaults off), and both tabs + the image viewer cards pressed
// Default prefs: no general switch is on (openByDefault and agentOpenTools
// both default off), and both tabs + the image viewer cards pressed
// (3 aria-pressed cards).
// The nested auto-open toggle is NOT an inline card (it lives in the popup).
expect(pressedCount(html, 'true')).toBe(3)
expect(pressedCount(html, 'false')).toBe(0)
// The general toggles are custom switches (real checkboxes, one checked).
expect(html.match(/checked=""/g)?.length).toBe(1)
// The general toggles are custom switches (real checkboxes, none checked).
expect(html.match(/checked=""/g)?.length ?? 0).toBe(0)
expect(html).not.toContain('Auto-open Subagents')
})
@@ -153,9 +153,9 @@ describe('SideCardSection declarative inventory', () => {
expect(html).toContain('>Subagents<')
expect(html).toContain('>Image<')
expect(pressedCount(html, 'false')).toBe(2)
// The explorer card stays pressed; the one default-on general switch stays checked.
// The explorer card stays pressed; no general switch is on by default.
expect(pressedCount(html, 'true')).toBe(1)
expect(html.match(/checked=""/g)?.length).toBe(1)
expect(html.match(/checked=""/g)?.length ?? 0).toBe(0)
})
it('hides the gear of a disabled feature (its related settings are dormant)', () => {
@@ -178,11 +178,10 @@ describe('SideCardSection declarative inventory', () => {
expect(html).toContain('Pick the title-bar compatibility scheme: auto-detect (default, conservative) / DSH official web / known desktop shells / custom (shift distance + custom CSS)')
expect(html).not.toContain('<select')
expect(html).toContain('>Auto-detect<')
// Three general-row switches remain (openByDefault + interceptOpenPath
// + agentOpenTools), only interceptOpenPath checked by default — the
// scheme row is a dropdown, not a switch.
expect(html.match(/type="checkbox"/g)?.length).toBe(3)
expect(html.match(/checked=""/g)?.length).toBe(1)
// Two general-row switches remain (openByDefault + agentOpenTools), neither
// on by default — the scheme row is a dropdown, not a switch.
expect(html.match(/type="checkbox"/g)?.length).toBe(2)
expect(html.match(/checked=""/g)?.length ?? 0).toBe(0)
// Auto (default) needs no further settings → no gear.
expect(html).not.toContain('Position compatibility mode Feature settings')
+33 -12
View File
@@ -7,6 +7,7 @@
*/
import { describe, expect, it } from 'vitest'
import type { SidebarHistoryEntry, SidebarSessionEvent, SidebarSessionSummary } from '../src/context-types.ts'
import type { AssistantLiveChunk } from '../src/assistant-live.ts'
import {
boundaryDelivered,
buildOpenTurnSnapshot,
@@ -35,6 +36,24 @@ function ev(type: string, seq: number, data: Record<string, unknown> = {}): Side
return event
}
/** One live delta as the 0.1.5 stream buffer produces it: `index` is the
* dense frame position, the chunk carries its own block index. */
function live(
turn: number,
step: number,
text: string,
over: { kind?: 'text' | 'reasoning'; index?: number; block?: number } = {},
): AssistantLiveChunk {
return {
attemptId: 'a1',
turn,
step,
index: over.index ?? 0,
time: 0,
chunk: { type: over.kind === 'reasoning' ? 'reasoning-delta' : 'text-delta', index: over.block ?? 0, text },
}
}
/** A completed user→assistant turn block (turn T, step 1, optional tools). */
function completedTurn(seq: number, turn: number, over: {
text?: string
@@ -136,7 +155,6 @@ describe('buildSidechatInheritance', () => {
ev('user/message', 6, { content: [{ type: 'text', text: 'next?' }], source: { kind: 'user' } }),
ev('turn/start', 7, { turn: 2 }),
ev('step/start', 8, { turn: 2, step: 1 }),
ev('assistant/chunk', 9, { turn: 2, step: 1, chunk: { type: 'text-delta', index: 0, text: 'partial answer' } }),
]
const { seed, snapshot } = buildSidechatInheritance(events)
expectContiguous(seed)
@@ -145,10 +163,12 @@ describe('buildSidechatInheritance', () => {
const turnEnd = seed.at(-1)!
expect(stepEnd).toMatchObject({ type: 'step/end', data: { turn: 2, step: 1 } })
expect(turnEnd).toMatchObject({ type: 'turn/end', data: { turn: 2, reason: { kind: 'interrupted' } } })
// The pending user message and the partial chunk survive as REAL events.
// The pending user message survives as a REAL event. The in-flight
// step's deltas are process-local on 0.1.5, so they are NOT seed events —
// the snapshot fallback is the only path that carries them.
expect(seed.some(event => event.type === 'user/message'
&& (event.data.content as Array<{ text: string }>)[0]?.text === 'next?')).toBe(true)
expect(seed.some(event => event.type === 'assistant/chunk')).toBe(true)
expect(seed.every(event => event.type !== 'assistant/live-chunk')).toBe(true)
expect(snapshot).toBeNull()
})
@@ -166,7 +186,7 @@ describe('buildSidechatInheritance', () => {
content: [{ type: 'tool-result', toolCallId: 'c1', content: [{ type: 'text', text: 'hit' }] }],
},
}),
ev('assistant/chunk', 5, { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'found ' } }),
ev('assistant/message', 5, { turn: 1, step: 1, message: { content: [{ type: 'text', text: 'found it' }] }, stream: [] }),
]
const { seed, snapshot } = buildSidechatInheritance(events)
expectContiguous(seed)
@@ -180,11 +200,12 @@ describe('buildSidechatInheritance', () => {
ev('user/message', 0, { content: [{ type: 'text', text: 'q' }], source: { kind: 'user' } }),
ev('turn/start', 1, { turn: 1 }),
ev('step/start', 2, { turn: 1, step: 1 }),
ev('assistant/chunk', 3, { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'running ' } }),
ev('tool/call', 4, { turn: 1, step: 1, callId: 'c1', name: 'bash', arguments: '{"cmd":"sleep 9"}' }),
]
expect(hasDanglingToolCall(events, 1)).toBe(true)
const { seed, snapshot } = buildSidechatInheritance(events)
// The in-flight text is process-local on 0.1.5: it reaches the snapshot
// through the live buffer, not through the log.
const { seed, snapshot } = buildSidechatInheritance(events, [live(1, 1, 'running ')])
expectContiguous(seed)
// The seed stops BEFORE the open turn; the pending user message stays.
expect(seed.at(-1)?.type).toBe('user/message')
@@ -200,10 +221,9 @@ describe('buildSidechatInheritance', () => {
ev('user/message', 0, { content: [{ type: 'text', text: 'q' }], source: { kind: 'user' } }),
ev('turn/start', 1, { turn: 1 }),
ev('step/start', 2, { turn: 1, step: 1 }),
ev('assistant/message', 3, { turn: 1, step: 1, message: { content: [{ type: 'text', text: 'first' }] } }),
ev('assistant/message', 3, { turn: 1, step: 1, message: { content: [{ type: 'text', text: 'first' }] }, stream: [] }),
ev('step/end', 4, { turn: 1, step: 1 }),
ev('step/start', 5, { turn: 1, step: 2 }),
ev('assistant/chunk', 6, { turn: 1, step: 2, chunk: { type: 'text-delta', index: 0, text: 'second' } }),
]
const { seed } = buildSidechatInheritance(events)
expectContiguous(seed)
@@ -222,9 +242,6 @@ describe('buildOpenTurnSnapshot', () => {
const events = [
ev('turn/start', 0, { turn: 1 }),
ev('step/start', 1, { turn: 1, step: 1 }),
ev('assistant/chunk', 2, { turn: 1, step: 1, chunk: { type: 'reasoning-delta', index: 0, text: 'think' } }),
ev('assistant/chunk', 3, { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: '```js\ncode' } }),
ev('assistant/chunk', 4, { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: '\n```' } }),
ev('tool/call', 5, { turn: 1, step: 1, callId: 'c1', name: 'read', arguments: '{"path":"a.txt"}' }),
ev('tool/result', 6, {
turn: 1,
@@ -236,7 +253,11 @@ describe('buildOpenTurnSnapshot', () => {
}),
ev('tool/call', 7, { turn: 1, step: 1, callId: 'c2', name: 'bash', arguments: '{"cmd":"long"}' }),
]
const snapshot = buildOpenTurnSnapshot(events)
const snapshot = buildOpenTurnSnapshot(events, [
live(1, 1, 'think', { kind: 'reasoning', index: 0, block: 0 }),
live(1, 1, '```js\ncode', { index: 1, block: 1 }),
live(1, 1, '\n```', { index: 2, block: 1 }),
])
expect(snapshot).not.toBeNull()
expect(snapshot).toContain('```js\ncode\n```')
expect(snapshot).toContain('Reasoning so far')
+31 -16
View File
@@ -48,18 +48,29 @@ function happyServices(parent: AgentLike | undefined, child: AgentLike) {
const rename = vi.fn((_session: unknown, _title: string) => ({ title: 'x', eventSeq: 1 }))
const resolve = vi.fn(async (_id?: string) => ({ id: 'preset-a' }))
const mount = vi.fn(async () => {})
const inspect = vi.fn(async (_id?: string): Promise<{ meta: Record<string, unknown>; events: unknown[] }> => ({ meta: { agentPreset: 'preset-a' }, events: [] }))
/** The 0.1.5 persistence face: open(id, 'read') -> handle.read() -> close(). */
const open = vi.fn(async (_id?: string): Promise<{
header: { agentPreset?: string; cwd?: string }
inheritedEventCount: number
read: () => Promise<{ events: unknown[] }>
close: () => Promise<void>
}> => ({
header: { agentPreset: 'preset-a' },
inheritedEventCount: 0,
read: async () => ({ events: [] }),
close: async () => {},
}))
return {
agents: { get, create, resume },
agentPresets: { resolve, mount },
sessionTitle: { rename },
sessionPersistence: { inspect },
sessionPersistence: { open },
create,
resume,
get,
rename,
mount,
inspect,
open,
}
}
@@ -170,7 +181,6 @@ describe('sidechat.start', () => {
ev('user/message', 0, { content: [{ type: 'text', text: 'q' }], source: { kind: 'user' } }),
ev('turn/start', 1, { turn: 1 }),
ev('step/start', 2, { turn: 1, step: 1 }),
ev('assistant/chunk', 3, { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'streaming' } }),
],
})
const child = agent('child')
@@ -181,7 +191,7 @@ describe('sidechat.start', () => {
const options = services.create.mock.calls[0]![0] as { seed: Array<{ type: string; data: Record<string, unknown> }> }
expect(options.seed.map(event => event.type)).toEqual([
'user/message', 'turn/start', 'step/start', 'assistant/chunk', 'step/end', 'turn/end',
'user/message', 'turn/start', 'step/start', 'step/end', 'turn/end',
'subagent/descriptor',
])
expect(options.seed.at(-2)?.data).toEqual({ turn: 1, reason: { kind: 'interrupted' } })
@@ -306,7 +316,7 @@ describe('sidechat.prompt', () => {
const resumeOptions = services.resume.mock.calls[0]![0] as { resumeSessionId: string; setup: unknown }
expect(resumeOptions.resumeSessionId).toBe('child')
expect(typeof resumeOptions.setup).toBe('function')
expect(services.inspect).toHaveBeenCalledWith('child')
expect(services.open).toHaveBeenCalledWith('child', 'read')
expect(child.followup).toHaveBeenCalledTimes(1)
})
@@ -368,14 +378,14 @@ describe('sidechat.info', () => {
const api = buildSidechatApi(ctxWith(services))
const info = await api['sidechat.info']({ childId: 'child' })
expect(info).toEqual({ live: false, preset: 'preset-a' })
expect(services.inspect).toHaveBeenCalledWith('child')
expect(services.open).toHaveBeenCalledWith('child', 'read')
})
it('degrades to a bare cold info when the session is gone', async () => {
const services = happyServices(undefined, agent('child'))
services.agents.get = vi.fn((_id: unknown) => undefined)
services.inspect = vi.fn(async () => { throw new Error('unknown session') })
services.sessionPersistence = { inspect: services.inspect }
services.open = vi.fn(async () => { throw new Error('unknown session') })
services.sessionPersistence = { open: services.open }
const api = buildSidechatApi(ctxWith(services))
await expect(api['sidechat.info']({ childId: 'ghost' })).resolves.toEqual({ live: false })
})
@@ -391,8 +401,8 @@ function threadLog(): Array<ReturnType<typeof ev>> {
ev('subagent/descriptor', 3, { mode: 'continuable' }),
ev('user/message', 4, { content: [{ type: 'text', text: 'Side conversation boundary.' }], source: { kind: 'plugin', plugin: 'dsh-better-sidebar' } }),
ev('user/message', 5, { content: [{ type: 'text', text: 'the side question' }], source: { kind: 'user' } }),
ev('assistant/chunk', 6, { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'an' } }),
ev('assistant/chunk', 7, { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'swer' } }),
ev('assistant/attempt', 6, { turn: 1, step: 1, stream: [] }),
ev('assistant/attempt', 7, { turn: 1, step: 1, stream: [] }),
]
}
@@ -404,7 +414,7 @@ describe('sidechat.events', () => {
const result = await api['sidechat.events']({ childId: 'child' })
expect(result.events.map(event => event.seq)).toEqual([3, 4, 5, 6, 7])
expect(result.events[0]).toMatchObject({ type: 'subagent/descriptor', seq: 3 })
expect(services.inspect).not.toHaveBeenCalled()
expect(services.open).not.toHaveBeenCalled()
})
it('narrows to the afterSeq delta on polls (and never resurrects the seed)', async () => {
@@ -422,11 +432,16 @@ describe('sidechat.events', () => {
const child = agent('child')
const services = happyServices(undefined, child)
services.agents.get = vi.fn((_id: unknown) => undefined)
services.inspect.mockImplementation(async () => ({ meta: { agentPreset: 'preset-a' }, events: threadLog() }))
services.open.mockImplementation(async () => ({
header: { agentPreset: 'preset-a' },
inheritedEventCount: 0,
read: async () => ({ events: threadLog() }),
close: async () => {},
}))
const api = buildSidechatApi(ctxWith(services))
const result = await api['sidechat.events']({ childId: 'child' })
expect(result.events.map(event => event.seq)).toEqual([3, 4, 5, 6, 7])
expect(services.inspect).toHaveBeenCalledWith('child')
expect(services.open).toHaveBeenCalledWith('child', 'read')
})
it('returns a marker-less legacy log whole', async () => {
@@ -443,7 +458,7 @@ describe('sidechat.events', () => {
})
it('caps a pathological response at its tail (8000 events)', async () => {
const events = Array.from({ length: 8_003 }, (_, index) => ev('assistant/chunk', index, { turn: 1, step: 1 }))
const events = Array.from({ length: 8_003 }, (_, index) => ev('assistant/attempt', index, { turn: 1, step: 1, stream: [] }))
const child = agent('child', { events })
const services = happyServices(undefined, child)
const api = buildSidechatApi(ctxWith(services))
@@ -463,7 +478,7 @@ describe('sidechat.events', () => {
.rejects.toMatchObject({ code: 'bad-request' })
services.agents.get = vi.fn((_id: unknown) => undefined)
services.inspect.mockImplementation(async () => { throw new Error('no such session') })
services.open.mockImplementation(async () => { throw new Error('no such session') })
await expect(api['sidechat.events']({ childId: 'ghost' }))
.rejects.toMatchObject({ code: 'not-found', status: 404 })
})
+30 -14
View File
@@ -10,9 +10,9 @@
* not a mock, so the class of bug cannot silently return.
*/
import { describe, expect, it } from 'vitest'
import { Session, SessionLogOffset } from '@deepseek-ai/dsh-session'
import { Session, SessionLogOffset, SESSION_FORMAT_VERSION } from '@deepseek-ai/dsh-session'
import type { SessionId } from '@deepseek-ai/dsh-session'
import { Inbox } from '@deepseek-ai/dsh-agent'
import type { InboxState } from '@deepseek-ai/dsh-agent'
import { snapshotSubagentDescriptor } from '@deepseek-ai/dsh-subagent'
import type { SidebarSessionEvent } from '../src/context-types.ts'
import { buildSidechatInheritance } from '../src/sidechat-core.ts'
@@ -42,19 +42,20 @@ function assistantMessage(text: string): Record<string, unknown> {
}
/** A parent log with a completed turn, a pending question, and an open
* in-progress turn (the exact shape a mid-stream thread creation sees). */
* in-progress turn (the exact shape a mid-stream thread creation sees).
* DSH 0.1.5 logs nothing for the in-flight step: its deltas are
* process-local `agent/assistant-stream` frames, not session events. */
function parentLog(): SidebarSessionEvent[] {
return [
ev('user/message', 0, userMessage('first question')),
ev('turn/start', 1, { turn: 1 }),
ev('step/start', 2, { turn: 1, step: 1 }),
ev('assistant/message', 3, { turn: 1, step: 1, message: assistantMessage('first answer') }),
ev('assistant/message', 3, { turn: 1, step: 1, message: assistantMessage('first answer'), stream: [] }),
ev('step/end', 4, { turn: 1, step: 1 }),
ev('turn/end', 5, { turn: 1, reason: { kind: 'completed' } }),
ev('user/message', 6, userMessage('pending question')),
ev('turn/start', 7, { turn: 2 }),
ev('step/start', 8, { turn: 2, step: 1 }),
ev('assistant/chunk', 9, { turn: 2, step: 1, chunk: { type: 'text-delta', index: 0, text: 'in-progress' } }),
]
}
@@ -68,7 +69,7 @@ describe('sidechat seed against the real dsh-session validator', () => {
const types = child.snapshotEvents().map(event => event.type)
expect(types).toEqual([
'user/message', 'turn/start', 'step/start', 'assistant/message', 'step/end', 'turn/end',
'user/message', 'turn/start', 'step/start', 'assistant/chunk', 'step/end', 'turn/end',
'user/message', 'turn/start', 'step/start', 'step/end', 'turn/end',
'session/end-seed',
])
// The synthetic close is honest: the frozen turn ends interrupted.
@@ -113,18 +114,24 @@ describe('sidechat seed against the real dsh-session validator', () => {
})
})
describe('sidechat seed fork markers vs the real dsh-agent Inbox replay', () => {
describe('sidechat seed fork markers vs the reconstructed inbox', () => {
/**
* Regression: the thread-create call originally passed `seed` WITHOUT the
* fork-marker pair (`meta.isSeeded: true` + `inheritedEventCount`). A seed
* without them is REPLAY history, not an inherited prefix, so the child's
* ownEvents() includes the whole seed and its Inbox constructor replays the
* ownEvents() includes the whole seed and its inbox fold replays the
* parent's `agent/inbox/spliced` events — inheriting whatever input sat
* UNCLAIMED in the parent at the click moment (a queued follow-up, or a
* tool-result context spliced into next-step between step boundaries of a
* long-running turn). The first side prompt then claimed and SENT that stale
* message before the boundary + question. These tests run the REAL
* dsh-session Session and dsh-agent Inbox.
* message before the boundary + question.
*
* The fold below is the standard inbox projection's rule
* (`inboxProjectionDefinition` in `@deepseek-ai/dsh-agent-loop/inbox`,
* which 0.1.5 does not re-export from the package root): pending input is
* reconstructed from the session's OWN splice events, so the
* `inheritedEventCount` the marker pair supplies is exactly what keeps the
* inherited prefix out of it.
*/
const pendingMessage = (id: string, kind: string, text: string) => ({
id,
@@ -133,15 +140,24 @@ describe('sidechat seed fork markers vs the real dsh-agent Inbox replay', () =>
source: { kind },
})
const inboxOver = (session: Session): { nextTurn: number; nextStep: number } => {
const inbox = new Inbox(session, { inserted() {}, discarded() {}, claimed() {} })
return { nextTurn: inbox.nextTurn.length, nextStep: inbox.nextStep.length }
let state: InboxState = { 'next-turn': [], 'next-step': [] }
for (const event of session.ownEvents()) {
if (event.type !== 'agent/inbox/spliced') continue
const splice = event.data
const list = state[splice.target]
state = {
...state,
[splice.target]: list.toSpliced(splice.start, splice.removedCount ?? 0, ...splice.inserted),
}
}
return { nextTurn: state['next-turn'].length, nextStep: state['next-step'].length }
}
/** The shape the routes pass since the fix: isSeeded + inheritedEventCount. */
const forkMarkedSession = (id: string, seed: readonly SidebarSessionEvent[]): Session =>
Session.create(
id as SessionId,
seed as never,
{ version: 0, id: id as SessionId, createdAt: Date.now(), isSeeded: true },
{ version: SESSION_FORMAT_VERSION, id: id as SessionId, createdAt: Date.now(), isSeeded: true },
SessionLogOffset(seed.length),
)
/** Real loop order: inbox insert → turn/start → claim deletion → step/start
@@ -152,7 +168,7 @@ describe('sidechat seed fork markers vs the real dsh-agent Inbox replay', () =>
ev('agent/inbox/spliced', 2, { target: 'next-turn', start: 0, removedCount: 1, inserted: [] }),
ev('step/start', 3, { turn: 1, step: 1 }),
ev('user/message', 4, pendingMessage('m-q', 'user', 'q')),
ev('assistant/message', 5, { turn: 1, step: 1, message: assistantMessage('a') }),
ev('assistant/message', 5, { turn: 1, step: 1, message: assistantMessage('a'), stream: [] }),
ev('step/end', 6, { turn: 1, step: 1 }),
ev('turn/end', 7, { turn: 1, reason: { kind: 'completed' } }),
]
+5 -5
View File
@@ -80,15 +80,15 @@ describe('transcriptRows', () => {
])
})
it('accumulates chunk deltas per block and supersedes them on settle', () => {
it('accumulates live deltas per block and supersedes them on settle', () => {
const entries = [
entry(ev('session/end-seed', 0)),
entry(ev('user/message', 1, { content: textBlocks('q'), source: { kind: 'user' } })),
entry(ev('turn/start', 2, { turn: 1 })),
entry(ev('step/start', 3, { turn: 1, step: 1 })),
entry(ev('assistant/chunk', 4, { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'Hel' } })),
entry(ev('assistant/chunk', 5, { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'lo' } })),
entry(ev('assistant/chunk', 6, { turn: 1, step: 1, chunk: { type: 'reasoning-delta', index: 1, text: 'think' } })),
entry(ev('assistant/live-chunk', 4, { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'Hel' } })),
entry(ev('assistant/live-chunk', 5, { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'lo' } })),
entry(ev('assistant/live-chunk', 6, { turn: 1, step: 1, chunk: { type: 'reasoning-delta', index: 1, text: 'think' } })),
]
const rows = transcriptRows(entries)
const assistant = rows.find(row => row.kind === 'assistant') as Extract<SidechatTranscriptRow, { kind: 'assistant' }>
@@ -104,7 +104,7 @@ describe('transcriptRows', () => {
entry(ev('user/message', 1, { content: textBlocks('q'), source: { kind: 'user' } })),
entry(ev('turn/start', 2, { turn: 1 })),
entry(ev('step/start', 3, { turn: 1, step: 1 })),
entry(ev('assistant/chunk', 4, { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'par' } })),
entry(ev('assistant/live-chunk', 4, { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'par' } })),
entry(ev('assistant/message', 5, { turn: 1, step: 1, message: { content: textBlocks('final answer') } })),
]
const rows = transcriptRows(entries)
+19 -6
View File
@@ -40,6 +40,8 @@ interface FakeContext {
sessions: { get: (id: string) => { header: { cwd?: string } } | undefined }
tools: { register: (tool: unknown) => () => void }
effect: (fn: () => void | (() => void), label?: string) => void
/** The session/agent event feeds: nothing emits in these tests. */
on: (event: string, listener: (payload: never) => void) => () => void
/** The settings service never appears in the smoke context: the inject
* callback must never run (mirror of cordis' service-less inject). */
inject: (deps: readonly string[], callback: (sctx: never) => void) => () => void
@@ -100,6 +102,7 @@ describe('host plugin smoke', () => {
// No settings service in the smoke context: the registration callback
// never runs (cordis' service-less inject behaves the same).
inject: () => () => {},
on: () => () => {},
// No jobs/agents services: the jobs routes degrade to a 503.
get: () => undefined,
}
@@ -136,6 +139,7 @@ describe('host plugin smoke', () => {
if (typeof cleanup === 'function') effects.push(cleanup)
},
inject: () => () => {},
on: () => () => {},
get: () => undefined,
}
try {
@@ -492,7 +496,7 @@ describe('git destructive operations (scratch repository)', () => {
describe('session cwd resolution over the API route', () => {
interface CtxOverrides {
sessions?: { get: (id: string) => { header: { cwd?: string } } | undefined }
sessionPersistence?: { inspect: (id: string) => Promise<{ meta: { cwd?: string } }> }
sessionPersistence?: { open: (id: string, access: 'read' | 'write') => Promise<{ header: { cwd?: string }; read: () => Promise<{ events: never[] }>; close: () => Promise<void> }> }
}
const mountAll = (overrides: CtxOverrides = {}): SidebarWebRoute[] => {
@@ -509,6 +513,8 @@ describe('session cwd resolution over the API route', () => {
effect: (fn: () => void | (() => void)) => { fn() },
// No settings service: the namespace registration never runs.
inject: () => () => {},
// The session/agent event feeds: nothing emits in these tests.
on: () => () => {},
// No jobs/agents services in the smoke context: the routes degrade.
get: (key: string) => key === 'sessionPersistence' ? overrides.sessionPersistence : undefined,
}
@@ -575,8 +581,10 @@ describe('session cwd resolution over the API route', () => {
const coldCwd = resolvePath('/cold-project-cwd')
const route = mount({
sessionPersistence: {
inspect: async (id) => ({
meta: id === 's-cold' ? { cwd: coldCwd } : {},
open: async (id) => ({
header: id === 's-cold' ? { cwd: coldCwd } : {},
read: async () => ({ events: [] }),
close: async () => {},
}),
},
})
@@ -592,7 +600,7 @@ describe('session cwd resolution over the API route', () => {
// potentially recreate the original "outside workspace" misclassification.
const route = mount({
sessionPersistence: {
inspect: async () => ({ meta: { cwd: 'relative/path' } }),
open: async () => ({ header: { cwd: 'relative/path' }, read: async () => ({ events: [] }), close: async () => {} }),
},
})
const result = await invoke(route, 'session.cwd', { sessionId: 's-bad' })
@@ -603,7 +611,7 @@ describe('session cwd resolution over the API route', () => {
it('falls back to the process cwd when persistence has no cwd for the session', async () => {
const route = mount({
sessionPersistence: {
inspect: async () => ({ meta: {} }),
open: async () => ({ header: {}, read: async () => ({ events: [] }), close: async () => {} }),
},
})
const result = await invoke(route, 'session.cwd', { sessionId: 's-blank' })
@@ -877,6 +885,8 @@ describe('side card settings routes', () => {
if (deps.includes('settings') && settings !== undefined) callback({ settings })
return () => {}
},
// The session/agent event feeds: nothing emits in these tests.
on: () => () => {},
// No jobs/agents services: the jobs routes degrade to a 503.
get: () => undefined,
}
@@ -990,7 +1000,6 @@ describe('side card settings routes', () => {
bottomPanelAutoTerminal: true,
terminalFontFamily: '',
terminalFontSize: 13,
interceptOpenPath: true,
editorExplorer: false,
workspaceFence: true,
terminalShell: '',
@@ -1170,6 +1179,8 @@ describe('agent terminal tool gating', () => {
if (deps.includes('settings')) callback({ settings })
return () => {}
},
// The session/agent event feeds: nothing emits in these tests.
on: () => () => {},
// No jobs/agents services: the jobs routes degrade to a 503.
get: () => undefined,
}
@@ -1227,6 +1238,8 @@ describe('agent sidebar-open tool gating', () => {
if (deps.includes('settings')) callback({ settings })
return () => {}
},
// The session/agent event feeds: nothing emits in these tests.
on: () => () => {},
get: () => undefined,
}
apply(ctx as never)
+2 -2
View File
@@ -46,11 +46,11 @@ describe('subagent activity summary parser', () => {
])).toEqual({ text: 'ok' })
})
it('lastActivity ignores lifecycle events, chunks, and text-less messages', () => {
it('lastActivity ignores lifecycle events, stream rows, and text-less messages', () => {
const live = lastActivity([
entry('turn/end', { turn: 1, reason: 'success' }),
entry('step/start', { turn: 1, step: 1 }),
entry('assistant/chunk', { turn: 1, step: 1, chunk: { type: 'text', delta: 'x' } }),
entry('assistant/live-chunk', { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'x' } }),
entry('assistant/message', { turn: 1, step: 1, message: { content: [{ type: 'tool_use', name: 'bash' }] } }),
])
expect(live).toEqual({})
+2
View File
@@ -77,6 +77,8 @@ function mount(initialTrustedHosts: readonly string[] = []): {
if (typeof cleanup === 'function') effects.push(cleanup)
},
inject: () => () => {},
// The session/agent event feeds: nothing emits in these tests.
on: () => () => {},
get: () => undefined,
}
apply(ctx as never)
+2
View File
@@ -14,6 +14,8 @@ import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
// Bridge Node's `localStorage` accessor to jsdom's store (see the file).
setupFiles: ['tests/setup.ts'],
server: {
deps: {
inline: [/@deepseek-ai\/dsh-client-ui-primitives/],