diff --git a/.agents/notes/implemented/architecture/2026-09-15-guided-plugin-installation.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-15-guided-plugin-installation.i18n.yaml index 5c2d10151d..a5b7444619 100644 --- a/.agents/notes/implemented/architecture/2026-09-15-guided-plugin-installation.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-15-guided-plugin-installation.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-15-guided-plugin-installation.md -2026-09-15-guided-plugin-installation.md: 26ad870856ce948e1ebb736a07004e41af2a17be -2026-09-15-guided-plugin-installation.zh.md: 36e76c8ace0ae990ede14436b65fa00752f86bbe +2026-09-15-guided-plugin-installation.md: 451b1f316eebfdce9c9efcffbf48202177d04343 +2026-09-15-guided-plugin-installation.zh.md: 0c228695828238b4dbfdae7d2153e0358e92360c diff --git a/.agents/notes/implemented/architecture/2026-09-15-guided-plugin-installation.md b/.agents/notes/implemented/architecture/2026-09-15-guided-plugin-installation.md index 26ad870856..451b1f316e 100644 --- a/.agents/notes/implemented/architecture/2026-09-15-guided-plugin-installation.md +++ b/.agents/notes/implemented/architecture/2026-09-15-guided-plugin-installation.md @@ -16,6 +16,8 @@ The install dialog put a spec straight into `pnpm add` and showed pnpm's termina **A failed run is classified where the facts are.** `classifyInstallFailure` reads how the run ended and what pnpm printed — its `ERR_PNPM_*` codes and Node's errno names — into `packageResult.kind`. The client shows the kind as one line and folds pnpm's output behind the details. Parsing the log is confined to this one function with a fixture-driven test. +**GitHub connection failures have a short budget before installation.** The Host runs a read-only `git ls-remote` check with the profile's Git and proxy configuration and a default `githubConnectionTimeoutMs` of 5000 ms. Credential helpers and prompts are disabled for this check, so authentication cannot consume the connection budget. Only network failures and timeouts prevent pnpm from starting; pnpm still owns authentication, HTTPS-to-SSH fallback, ref resolution and the full installation. The budget does not limit downloads or builds. The existing failure kind and `failedAt: 'spec-host'` let the Client show a connection error or timeout and offer a domestic registry for a replacement package spec. Cancellation and manager disposal terminate the check and its descendants before settling. + **Cancellation is the manager's, not the signal's.** The dialog generates a request id for each run; the Host streams the run's output and phases under it, and `cancelInstall` answers only after pnpm exited and the files are back. The dialog waits for that answer before offering the spec again; an aborted RPC or a lost connection is not a confirmation. Only the check takes a trailing `AbortSignal`: going back or closing drops a registry lookup, whose settlement nothing waits for. **Enabling comes after the fact.** The run installs with `enabled: false`; the finished screen offers **Enable now** for the bundle it added, and the dialog closes and the list scrolls to it. Nothing is enabled before the person has seen what was installed. @@ -36,8 +38,10 @@ The install dialog put a spec straight into `pnpm add` and showed pnpm's termina ## Consequences -`inspect`, `cancelInstall`, and the `plugin-manager/changed`, `plugin-manager/install-log`, and `plugin-manager/install-state` events join the manager's Remote; `listBundles` carries titles, rows, and overrides; `ChangeResult` gains `cancelled`, `bundle`, and `packageResult.kind`; the config gains `pnpmCommand` and `inspectTimeoutMs`. The dialog is four screens over one subject card. +`inspect`, `cancelInstall`, and the `plugin-manager/changed`, `plugin-manager/install-log`, and `plugin-manager/install-state` events join the manager's Remote; `listBundles` carries titles, rows, and overrides; `ChangeResult` gains `cancelled`, `bundle`, and `packageResult.kind`; the config gains `pnpmCommand`, `inspectTimeoutMs` and `githubConnectionTimeoutMs`. The dialog is four screens over one subject card. ## Testing `packages/boot/plugin-manager/tests/install-spec.spec.ts` pins the spec forms and the failure classifier's inputs; `manager.spec.ts` drives `inspect` against a stubbed registry lookup and a real directory, streams a run, stops one and checks the restored files, and checks the change events; `operations.spec.ts` covers the registry lookup. `packages/client/ui-plugin-manager/tests` cover the store's phases, Host-confirmed cancellation, post-install enabling, toasts, and the page's four screens; `apps/web/tests/plugin-manager.e2e.ts` refuses an installed name, a missing path, and a bad name through the real Host and switches a bundle and one of its rows live, and `plugin-install-cancel.e2e.ts` stops a real child from the dialog, checks the restored files, and installs on the second try, and `plugin-install-approve.e2e.ts` leaves a script undecided through the fake pnpm, allows it from the dialog, and installs on the retry. + +`github-connection.spec.ts` uses real Git against owned loopback transports to cover immediate failure, timeout, cancellation and noninteractive authentication. `manager.spec.ts` also installs a local repository through real Git and pnpm for HTTPS, GitHub shorthand and SSH specs, isolating pnpm's HTTPS probe through an owned proxy and verifying its SSH fallback. `plugin-install-github.e2e.ts` checks the connection error and timeout dialogs, child cleanup, and switching to a mirror without retrying the old GitHub address. diff --git a/.agents/notes/implemented/architecture/2026-09-15-guided-plugin-installation.zh.md b/.agents/notes/implemented/architecture/2026-09-15-guided-plugin-installation.zh.md index 36e76c8ace..0c22869582 100644 --- a/.agents/notes/implemented/architecture/2026-09-15-guided-plugin-installation.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-15-guided-plugin-installation.zh.md @@ -16,6 +16,8 @@ Status: implemented **失败在事实所在处分类。** `classifyInstallFailure` 依据运行的结束方式和 pnpm 的输出——它的 `ERR_PNPM_*` 码和 Node 的 errno 名——给出 `packageResult.kind`。客户端把 kind 显示成一句话,把 pnpm 输出折叠进详情。对日志的解析只存在于这一个函数,用夹具驱动的测试钉住。 +**GitHub 连接失败在安装前有单独的短时限。** 宿主通过只读的 `git ls-remote` 检查连接,沿用 profile 的 Git 与代理配置,`githubConnectionTimeoutMs` 默认为 5000 毫秒。这次检查禁用凭据助手和认证提示,避免认证过程占用连接预算。只有网络失败与超时会阻止 pnpm 启动;认证、HTTPS 到 SSH 的回退、ref 解析及完整安装仍由 pnpm 负责。时限不约束下载或构建。已有的失败类型与 `failedAt: 'spec-host'` 让客户端显示连接失败或超时,并提供国内镜像供用户输入另一个包名。取消安装或销毁管理器会先终止检查及其子进程,再结束操作。 + **取消属于管理器,不属于信号。** 对话框为每次运行生成一个 request id;宿主在它之下流式转发运行的输出与阶段,`cancelInstall` 只在 pnpm 退出且文件恢复后才答复。对话框等到这个答复才把 spec 重新交回;中止的 RPC 或断开的连接都不算确认。只有检查末尾接受 `AbortSignal`:返回编辑或关闭会丢掉一次注册表查询,没有人等它的结果。 **启用在事后。** 运行以 `enabled: false` 安装;完成画面为它新增的组合包提供**立即启用**,对话框关闭,列表滚动到它。在人看到装了什么之前,不会启用任何东西。 @@ -36,8 +38,10 @@ Status: implemented ## 后果 -`inspect`、`cancelInstall` 以及 `plugin-manager/changed`、`plugin-manager/install-log`、`plugin-manager/install-state` 事件加入管理器的 Remote;`listBundles` 携带标题、行与覆盖项;`ChangeResult` 增加 `cancelled`、`bundle` 与 `packageResult.kind`;配置增加 `pnpmCommand` 与 `inspectTimeoutMs`。对话框是围绕同一张主题卡的四个画面。 +`inspect`、`cancelInstall` 以及 `plugin-manager/changed`、`plugin-manager/install-log`、`plugin-manager/install-state` 事件加入管理器的 Remote;`listBundles` 携带标题、行与覆盖项;`ChangeResult` 增加 `cancelled`、`bundle` 与 `packageResult.kind`;配置增加 `pnpmCommand`、`inspectTimeoutMs` 与 `githubConnectionTimeoutMs`。对话框是围绕同一张主题卡的四个画面。 ## 测试 `packages/boot/plugin-manager/tests/install-spec.spec.ts` 钉住 spec 形式与失败分类器的输入;`manager.spec.ts` 用桩住的注册表查询和真实目录驱动 `inspect`,流式转发一次运行,停下一次运行并检查恢复后的文件,并检查变更事件;`operations.spec.ts` 覆盖注册表查询。`packages/client/ui-plugin-manager/tests` 覆盖 store 的阶段、经宿主确认的取消、装后启用、toast 与页面的四个画面;`apps/web/tests/plugin-manager.e2e.ts` 经真实宿主拒绝已装名字、不存在的路径和坏名字,并实时切换一个组合包及其中一行,`plugin-install-cancel.e2e.ts` 从对话框停下一个真实子进程、检查恢复后的文件,并在第二次尝试时装成,`plugin-install-approve.e2e.ts` 用假 pnpm 把脚本留作未决、从对话框允许后在重试中装上。 + +`github-connection.spec.ts` 用真实 Git 和测试自有的本机网络端点覆盖立即失败、超时、取消与非交互认证。`manager.spec.ts` 还通过真实 Git 与 pnpm 安装本地仓库,覆盖 HTTPS、GitHub 简写和 SSH 地址,用自有代理隔离 pnpm 的 HTTPS 请求并验证其 SSH 回退。`plugin-install-github.e2e.ts` 检查连接失败及超时弹窗、子进程清理,以及切换镜像时不重试原 GitHub 地址。 diff --git a/.agents/notes/implemented/feature/2026-08-05-agent-teams.i18n.yaml b/.agents/notes/implemented/feature/2026-08-05-agent-teams.i18n.yaml index a4f2c8968a..4eb9df8cb0 100644 --- a/.agents/notes/implemented/feature/2026-08-05-agent-teams.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-05-agent-teams.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-05-agent-teams.md -2026-08-05-agent-teams.md: 45c7e599e03bb26dc9224c74a6d3bc0ab8a74dcc -2026-08-05-agent-teams.zh.md: dbb2931bdc40da5ddf2c8091dd8c401029e0445f +2026-08-05-agent-teams.md: 6333e511ba1e71823cfa55c27ba46b2665d1ef15 +2026-08-05-agent-teams.zh.md: 3fed1302146406f71d63c7ff2df8f031a91c9804 diff --git a/.agents/notes/implemented/feature/2026-08-05-agent-teams.md b/.agents/notes/implemented/feature/2026-08-05-agent-teams.md index 45c7e599e0..6333e511ba 100644 --- a/.agents/notes/implemented/feature/2026-08-05-agent-teams.md +++ b/.agents/notes/implemented/feature/2026-08-05-agent-teams.md @@ -54,6 +54,14 @@ All members use the same cwd and observe writes immediately. The policy tells me Worktree isolation is not a harness runtime behavior. A deployment or prompt may arrange separate worktrees, but the Team domain does not infer branches, merge changes, or silently change cwd. This preserves the existing same-world subagent and sandbox contracts. +## Web projection + +The Web panel reads the Lead Session's `agentTeam` wire projection because the existing projection stream keeps browser state current without a separate Team read API. Both Lead and teammate conversations select values from the shared cross-Session store. The panel owns no projection reads: Session loading, cached list values, and control baselines supply the data. + +The wire view carries durable roster phases, non-deleted task views with shared readiness and overlap derivation, and `failure` beside the last valid state. Live activity comes from Session status. Model selection has its own durable projection; the panel displays the shared store's available values without opening member conversations. A missing cached value remains absent until Session loading or a live update supplies it. + +Publication requires `apply` to return a new state object per applied Team event and replace only the touched collection. Mailbox-only events retain the view reference and publish nothing. The existing carrier sends each changed complete view to every connected browser; the package README records that cost. + ## Alternatives considered **Extend direct-child subagent tools with peer ids.** Rejected because parent/child authority and Team peer membership are different domains. Adding peer access to the continuation seam would weaken its exact-parent authorization and still leave roster and tasks without a persistence owner. diff --git a/.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md b/.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md index dbb2931bdc..3fed130214 100644 --- a/.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md +++ b/.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md @@ -54,6 +54,14 @@ Peer 通讯使用 Lead 日志 mailbox。投递前先追加并 flush `team/messag Worktree isolation 不是 harness runtime 行为。deployment 或 prompt 可以安排独立 worktree,但 Team 领域不会推断 branch、merge 变更或静默改变 cwd。这样保留既有 same-world subagent 与 sandbox 契约。 +## Web projection + +Web panel 读取 Lead Session 的 `agentTeam` 传输投影,因为现有投影流能持续更新浏览器状态,无需独立的 Team 读取 API。Lead 与 teammate 会话均从共享的跨 Session store 选择值。面板不负责投影读取:Session 加载、列表缓存值与控制基线提供数据。 + +传输视图携带持久 roster phase、使用共享就绪与重叠派生逻辑的未删除任务视图,以及最后有效状态旁的 `failure`。实时活动来自 Session 状态。模型选择有自己的持久投影;面板展示共享 store 中可用的值,不打开成员会话。缺少的缓存值保持缺失,直到 Session 加载或实时更新提供它。 + +发布要求 `apply` 对每个已应用的 Team 事件返回新的 state 对象,并只替换被触及的集合。仅邮箱的事件保留视图引用,不发布 frame。现有传输层将每个变化后的完整视图发送给所有已连接浏览器;包 README 记录了这项开销。 + ## Alternatives considered **用 peer id 扩展 direct-child subagent tool。** 拒绝,因为 parent/child 权限与 Team peer membership 是不同领域。向 continuation seam 增加 peer access 会削弱 exact-parent authorization,仍无法为 roster 与 task 提供持久 owner。 diff --git a/.agents/notes/implemented/feature/2026-09-14-composer-selection-keyboard.i18n.yaml b/.agents/notes/implemented/feature/2026-09-14-composer-selection-keyboard.i18n.yaml index 3476259680..9c3ae26c74 100644 --- a/.agents/notes/implemented/feature/2026-09-14-composer-selection-keyboard.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-09-14-composer-selection-keyboard.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-14-composer-selection-keyboard.md -2026-09-14-composer-selection-keyboard.md: fb353a7072e7c2245aeeb22af9018edd9a6009f0 -2026-09-14-composer-selection-keyboard.zh.md: f51e5ca5096785ae6d059e3c8497292483490437 +2026-09-14-composer-selection-keyboard.md: 675e7239108604c51f4e9a82b98e164833ff9173 +2026-09-14-composer-selection-keyboard.zh.md: 2b296a609cdd76a407fd4ddcc5855eef2fcfa047 diff --git a/.agents/notes/implemented/feature/2026-09-14-composer-selection-keyboard.md b/.agents/notes/implemented/feature/2026-09-14-composer-selection-keyboard.md index fb353a7072..675e723910 100644 --- a/.agents/notes/implemented/feature/2026-09-14-composer-selection-keyboard.md +++ b/.agents/notes/implemented/feature/2026-09-14-composer-selection-keyboard.md @@ -20,6 +20,8 @@ The model seat's dropdown (`ModelSelect`, whose Effort row opens the reasoning-l `ModelSelect` keeps `↑`/`↓` as its focus walk over the rows of the shown pane (wrapping; a step taken while the trigger still holds focus enters at the near end). Tab settles: it activates the row the keyboard is on, and with focus still on the trigger it enters the menu at the row of the value in use instead. Escape and Shift+Tab leave a drilled pane first and otherwise close back to the trigger. A pane switch replaces the row that had focus, so each switch names where the keyboard lands: drilling focuses the row of the value in use (its checked row, or the first row when none is marked), and returning to the root pane focuses the cell that opened the pane left. Without that handoff the focus the unmounted row left on the page body sits outside the card's subtree, where its key handling never sees a keystroke. +Mouse presses on the `ModelSelect` trigger or portaled menu buttons prevent the browser’s default focus transfer: WebKit can otherwise blur the focused row and close the menu before `click`. Native `click` still owns selection and cancellation. Opening the menu or closing it through the trigger puts focus on the trigger. Selection requests return focus there before disabling the option rows, so a rejected request leaves keyboard navigation available. + `Menu`, the shared anchored dropdown behind the composer's permission seat, the preset chip, file cards, and the settings rows, carries the same pairing: while its list is open, Tab settles the focused row — from the anchor's button, Tab enters the list instead — and Escape or Shift+Tab close it and return focus to the anchor's first button. Only a keyboard already on the anchor or inside the list is intercepted, so Tab presses elsewhere on the page keep the browser's traversal even while a menu is open. The row's focus fill is the row's own indication: the browser's default ring would double it, and the fill is the same one the row shows under the pointer, so the visual language stays one per state. Selecting a row hands the keyboard back to the anchor unless the owner moved it itself — a card that focuses its own preview keeps it. The arrows, Home, and End walk the list whether or not `autoFocus` is set — that option now decides only whether opening focuses the first row — the row the keyboard is on carries the same fill a hovered row gets, and closing on Escape or Shift+Tab returns focus to the anchor whenever the menu held it. A dismissed menu stays dismissed: `InputTriggerController` records the identity of the hit the user closed — Escape or Shift+Tab, a pointer dismissal, or a settling pick — and a re-track of that same token with the same query leaves the menu closed, so restoring the caret after a dismissal (or a settled command closing the surface it opened) cannot revive it. A new query or another token re-arms it. @@ -42,7 +44,7 @@ The row contracts are unchanged. Producers already mark their current row `activ ## Verification -[Seat browser evidence](../../../../apps/web/tests/declared-reasoning.e2e.ts) drives a real Chromium against the shipped bundles: it opens the drilled level pane, walks it with `↑`, settles a level with Tab and asserts the saved effort, then reopens the pane and asserts the keyboard lands on that level rather than the top of the list before Shift+Tab walks back to the drilled cell and closes. [Seat keyboard tests](../../../../packages/client/ui-model-selection/tests/model-select.client.spec.tsx) cover the drilled-pane handoff onto the value in use (and onto the first row when no row is marked), the handoff back onto the cell, the `↑`/`↓` walk with both wraps, Tab settling a focused row and entering the menu from the trigger, Shift+Tab leaving a pane then closing, and Tab staying native while the menu is closed. [Popup view tests](../../../../packages/client/ui-commands/tests/popup-view.client.spec.tsx) cover Tab settling the parked highlight with consumption and refocus, Shift+Tab dismissing without settling, Tab consumed while rows load without leaving the card, the arrows staying distinct from the native left/right caret, and the wrap on `↑`/`↓`. [Trigger tests](../../../../packages/client/ui-input-trigger/tests/service.client.spec.ts) cover a dismissal sticking to its hit (Escape, a pointer dismissal, and a settling pick), the same hit re-tracking closed, and a changed query re-arming the menu. [Menu tests](../../../../packages/client/ui-primitives/tests/atoms.client.spec.tsx) cover the arrow walk without `autoFocus` (entering at the near end, wrapping, and stepping over a disabled row), Escape returning focus to the anchor, Tab settling the focused row, Shift+Tab closing back to the anchor, Tab entering the list from the anchor, and an unrelated Tab staying native while the list is open. [Popup controller tests](../../../../packages/client/ui-commands/tests/popup.client.spec.ts) cover the parked highlight, its top-row fallback without a marked row, the re-park inside a search retained across a retry, and the rebase to the top row on typing. The popup panel's Tab has unit coverage only: no Web scenario drives `/model` or `/permission` yet. +[Seat browser evidence](../../../../apps/web/tests/declared-reasoning.e2e.ts) drives real Chromium and WebKit against the shipped bundles: both verify native mouse selection, cancellation, focus after rejected requests, and keyboard navigation. Each opens the drilled level pane, walks it with `↑`, settles a level with Tab and asserts the saved effort, then reopens the pane and asserts the keyboard lands on that level rather than the top of the list before Shift+Tab walks back to the drilled cell and closes. [Seat keyboard tests](../../../../packages/client/ui-model-selection/tests/model-select.client.spec.tsx) cover the drilled-pane handoff onto the value in use (and onto the first row when no row is marked), the handoff back onto the cell, the `↑`/`↓` walk with both wraps, Tab settling a focused row and entering the menu from the trigger, Shift+Tab leaving a pane then closing, and Tab staying native while the menu is closed. [Popup view tests](../../../../packages/client/ui-commands/tests/popup-view.client.spec.tsx) cover Tab settling the parked highlight with consumption and refocus, Shift+Tab dismissing without settling, Tab consumed while rows load without leaving the card, the arrows staying distinct from the native left/right caret, and the wrap on `↑`/`↓`. [Trigger tests](../../../../packages/client/ui-input-trigger/tests/service.client.spec.ts) cover a dismissal sticking to its hit (Escape, a pointer dismissal, and a settling pick), the same hit re-tracking closed, and a changed query re-arming the menu. [Menu tests](../../../../packages/client/ui-primitives/tests/atoms.client.spec.tsx) cover the arrow walk without `autoFocus` (entering at the near end, wrapping, and stepping over a disabled row), Escape returning focus to the anchor, Tab settling the focused row, Shift+Tab closing back to the anchor, Tab entering the list from the anchor, and an unrelated Tab staying native while the list is open. [Popup controller tests](../../../../packages/client/ui-commands/tests/popup.client.spec.ts) cover the parked highlight, its top-row fallback without a marked row, the re-park inside a search retained across a retry, and the rebase to the top row on typing. The popup panel's Tab has unit coverage only: no Web scenario drives `/model` or `/permission` yet. ## Consequences diff --git a/.agents/notes/implemented/feature/2026-09-14-composer-selection-keyboard.zh.md b/.agents/notes/implemented/feature/2026-09-14-composer-selection-keyboard.zh.md index f51e5ca509..2b296a609c 100644 --- a/.agents/notes/implemented/feature/2026-09-14-composer-selection-keyboard.zh.md +++ b/.agents/notes/implemented/feature/2026-09-14-composer-selection-keyboard.zh.md @@ -20,6 +20,8 @@ composer 内有两个选择界面在打开期间持有焦点,两者都把 Tab `ModelSelect` 的 `↑`/`↓` 仍是在所显示面板行间移动焦点(循环;焦点仍在触发器上时从近端进入)。Tab 执行接受:它激活键盘所在的那一行;焦点仍在触发器上时,改为进入菜单并把焦点放在正在使用的那一行。Escape 与 Shift+Tab 都先退出已下钻的面板,否则关闭并回到触发器。面板切换会替换持有焦点的那一行,因此每次切换都指明键盘的落点:下钻落在正在使用的值那一行(其选中行;没有标记行时为首行),返回根面板则落在打开该面板的那个格子上。没有这一步,已卸载的行留在页面 body 上的焦点就位于卡片自身子树之外,挂在那里的按键处理永远收不到键盘事件。 +在 `ModelSelect` 触发按钮或 portal 菜单按钮上按下鼠标时,阻止浏览器的默认焦点转移:否则 WebKit 可能先让聚焦行失焦,并在 `click` 前关闭菜单。选择和取消仍由原生 `click` 决定。打开菜单或通过触发按钮关闭菜单时,焦点落在触发按钮上。选择请求在禁用选项行前将焦点交回该按钮,因此请求被拒绝后仍可使用键盘导航。 + `Menu`——权限档位、预设芯片、文件卡与设置行背后的共享锚定下拉——沿用同一组配对:列表打开期间,Tab 选定聚焦行(键盘在锚点按钮上时,Tab 改为进入列表),Escape 或 Shift+Tab 关闭并把焦点还给锚点的第一个按钮。只拦截已经位于锚点或列表内的键盘,因此页面其它位置按下的 Tab 即使在菜单打开时也仍归浏览器。行的焦点填充就是行自己的焦点提示:浏览器默认轮廓会与之叠加,而这一填充与指针悬停时用的是同一个,视觉语言因此一态一义。选定一行会把键盘还给锚点——除非拥有者自己移动了焦点(例如文件卡把自己的预览按钮设为焦点)。`↑`/`↓` 与 Home/End 无论是否设置 `autoFocus` 都在列表中走位——该选项如今只决定打开时是否聚焦首行——键盘所在的那一行使用与悬停行相同的填充(否则键盘走位看不出当前行),而 Escape 或 Shift+Tab 关闭时,只要键盘原本在菜单里就把焦点归还锚点。 被关闭的菜单保持关闭:`InputTriggerController` 记下用户关闭的那个命中(Escape/Shift+Tab、指针关闭或一次落定选定)的身份,同一 token 以同一查询重新 track 时菜单保持关闭,因此关闭后恢复光标、或已选定命令关闭它打开的界面,都无法把它召回来;新的查询或另一个 token 才会重新武装。 @@ -42,7 +44,7 @@ composer 内有两个选择界面在打开期间持有焦点,两者都把 Tab ## 验证 -[模型位浏览器证据](../../../../apps/web/tests/declared-reasoning.e2e.ts) 用真实 Chromium 驱动已发布的 bundle:打开已下钻的等级面板,用 `↑` 走位,用 Tab 选定一档并断言保存下来的推理等级,随后重新打开该面板、断言键盘落在那一档而不是列表顶部,再用 Shift+Tab 退回被下钻的格子并关闭。[模型位键盘测试](../../../../packages/client/ui-model-selection/tests/model-select.client.spec.tsx) 覆盖下钻交接落在正在使用的值上(没有标记行时落在首行)、交回给该格子、`↑`/`↓` 的双向循环走位、Tab 选定聚焦行以及从触发器进入菜单、Shift+Tab 先退层再关闭,以及菜单关闭时 Tab 保持原生。[弹窗视图测试](../../../../packages/client/ui-commands/tests/popup-view.client.spec.tsx) 覆盖 Tab 接受停放的高亮并完成消费与焦点归还、Shift+Tab 关闭且不接受、行仍在加载时 Tab 被消费且焦点不离开卡片、方向键与原生左右光标互不干扰,以及 `↑`/`↓` 的循环。[触发流水线测试](../../../../packages/client/ui-input-trigger/tests/service.client.spec.ts) 覆盖关闭粘在它自己的命中上(Escape、指针关闭、落定选定三种)、同一命中重新 track 时保持关闭,以及查询改变后菜单重新武装。[Menu 测试](../../../../packages/client/ui-primitives/tests/atoms.client.spec.tsx) 覆盖不带 `autoFocus` 的方向键走位(从近端进入、两端循环、跳过禁用行)、Escape 把焦点还给锚点、Tab 选定聚焦行、Shift+Tab 关闭并把焦点还给锚点、Tab 从锚点进入列表,以及列表打开期间无关位置的 Tab 保持原生。[弹窗控制器测试](../../../../packages/client/ui-commands/tests/popup.client.spec.ts) 覆盖高亮的停放、没有标记行时退回首行、重试时在保留的搜索内重新停放,以及输入时重定到首行。弹窗面板的 Tab 只有单元覆盖:目前还没有 Web 场景驱动 `/model` 或 `/permission`。 +[模型位浏览器证据](../../../../apps/web/tests/declared-reasoning.e2e.ts) 用真实 Chromium 与 WebKit 驱动已发布的 bundle:两者都验证原生鼠标选择、取消、请求被拒绝后的焦点以及键盘导航。每种引擎都会打开已下钻的等级面板,用 `↑` 走位,用 Tab 选定一档并断言保存下来的推理等级,随后重新打开该面板、断言键盘落在那一档而不是列表顶部,再用 Shift+Tab 退回被下钻的格子并关闭。[模型位键盘测试](../../../../packages/client/ui-model-selection/tests/model-select.client.spec.tsx) 覆盖下钻交接落在正在使用的值上(没有标记行时落在首行)、交回给该格子、`↑`/`↓` 的双向循环走位、Tab 选定聚焦行以及从触发器进入菜单、Shift+Tab 先退层再关闭,以及菜单关闭时 Tab 保持原生。[弹窗视图测试](../../../../packages/client/ui-commands/tests/popup-view.client.spec.tsx) 覆盖 Tab 接受停放的高亮并完成消费与焦点归还、Shift+Tab 关闭且不接受、行仍在加载时 Tab 被消费且焦点不离开卡片、方向键与原生左右光标互不干扰,以及 `↑`/`↓` 的循环。[触发流水线测试](../../../../packages/client/ui-input-trigger/tests/service.client.spec.ts) 覆盖关闭粘在它自己的命中上(Escape、指针关闭、落定选定三种)、同一命中重新 track 时保持关闭,以及查询改变后菜单重新武装。[Menu 测试](../../../../packages/client/ui-primitives/tests/atoms.client.spec.tsx) 覆盖不带 `autoFocus` 的方向键走位(从近端进入、两端循环、跳过禁用行)、Escape 把焦点还给锚点、Tab 选定聚焦行、Shift+Tab 关闭并把焦点还给锚点、Tab 从锚点进入列表,以及列表打开期间无关位置的 Tab 保持原生。[弹窗控制器测试](../../../../packages/client/ui-commands/tests/popup.client.spec.ts) 覆盖高亮的停放、没有标记行时退回首行、重试时在保留的搜索内重新停放,以及输入时重定到首行。弹窗面板的 Tab 只有单元覆盖:目前还没有 Web 场景驱动 `/model` 或 `/permission`。 ## 影响 diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml index 23d245c8c7..1be4833f09 100644 --- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md -2026-07-26-ci-failover-runbook.md: c2fd2fa6cf673ec4d202702c16a9a85a3a609b8d -2026-07-26-ci-failover-runbook.zh.md: 7fa5887bb552afd98cebd7502202fb5e9f37b1ee +2026-07-26-ci-failover-runbook.md: d46f9d140860580011e06aa3b51bb8618a23a114 +2026-07-26-ci-failover-runbook.zh.md: a229d46f16d959356e9108842edd557420e699ae diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md index c2fd2fa6cf..d46f9d1408 100644 --- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md +++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md @@ -20,7 +20,7 @@ The [superseded-CI cancellation policy](2026-09-09-cancel-superseded-ci.md) gove ### What the in-house pool is -`vm-backup`: one shared VM with multiple always-on systemd-managed runner instances. Registrations share its CPU, memory, and disk; their count is not a count of independent machines. Its image must preinstall Playwright Chromium's Linux system packages; CI downloads the lockfile-selected browser but never runs `apt` on this persistent shared host. Check the latest `serial / linux (self-hosted standby)` run before switching: its aggregate includes browser replay, so a green standby verifies both ordinary capacity and this browser prerequisite. +`vm-backup`: one shared VM with multiple always-on systemd-managed runner instances. Registrations share its CPU, memory, and disk; their count is not a count of independent machines. Its image must preinstall the Linux system packages for Playwright Chromium and WebKit; CI downloads the lockfile-selected browsers but never runs `apt` on this persistent shared host. Check the latest `serial / linux (self-hosted standby)` run before switching: its aggregate includes browser replay, so a green standby verifies both ordinary capacity and this browser prerequisite. #### Windows pool diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md index 7fa5887bb5..a229d46f16 100644 --- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md +++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md @@ -20,7 +20,7 @@ Status: implemented ### 自有池是什么 -`vm-backup`:一台共享虚拟机,运行多个常驻 systemd 管理的运行器实例。注册实例共享 CPU、内存和磁盘;实例数量不代表独立机器数量。其镜像必须预装 Playwright Chromium 的 Linux 系统软件包;CI 会下载锁文件选定的浏览器,但绝不在这台持久化共享主机上运行 `apt`。切换前先看 `serial / linux (self-hosted standby)` 最近一次运行:其聚合流程包含浏览器回放,因此绿色热备同时验证常规容量和这项浏览器先决条件。 +`vm-backup`:一台共享虚拟机,运行多个常驻 systemd 管理的运行器实例。注册实例共享 CPU、内存和磁盘;实例数量不代表独立机器数量。其镜像必须预装 Playwright Chromium 和 WebKit 所需的 Linux 系统软件包;CI 会下载锁文件选定的浏览器,但绝不在这台持久化共享主机上运行 `apt`。切换前先看 `serial / linux (self-hosted standby)` 最近一次运行:其聚合流程包含浏览器回放,因此绿色热备同时验证常规容量和这项浏览器先决条件。 #### Windows 池 diff --git a/.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.i18n.yaml b/.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.i18n.yaml index dbe621f214..ac15111eb8 100644 --- a/.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.i18n.yaml +++ b/.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md -2026-07-24-web-gui-browser-e2e-lane.md: b08d99a7d750bdeabac3dcbb7007e922d32dbdcd -2026-07-24-web-gui-browser-e2e-lane.zh.md: fbb92a094bbd14042d33e5ebd20d2d3ffd490d43 +2026-07-24-web-gui-browser-e2e-lane.md: eb79591e7b94fccb8eb92aec4024738825d214fc +2026-07-24-web-gui-browser-e2e-lane.zh.md: f715f4ca93b202f72e632e5ddae0d13559a927a7 diff --git a/.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md b/.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md index b08d99a7d7..eb79591e7b 100644 --- a/.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md +++ b/.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md @@ -10,7 +10,7 @@ The web GUI ships as a real assembled chain — chromium page → client plugin ## Decision -`pnpm run test:web` carries a keyless, deterministic browser e2e lane under `apps/web/tests/`: recorded session-log fixtures replay through `@deepseek-ai/dsh-llm-replay` against the real in-process web composition, with normalized aria goldens for user-visible states and in-process assertions for durable world state. The supporting product contracts are `dsh-llm-replay` pacing, consumption checks, and validated indexed override patches; cross-package `dsh-llm` failures retain validated provider facts through own data properties; and the shipped web composition mounts `llm-retry` for transient model failures. +`pnpm run test:web` carries a keyless, deterministic browser e2e lane under `apps/web/tests/`: recorded session-log fixtures replay through `@deepseek-ai/dsh-llm-replay` against the real in-process web composition, with normalized aria goldens for user-visible states and in-process assertions for durable world state. Chromium runs the full lane; the [model and reasoning picker scenario](../../../../apps/web/tests/declared-reasoning.e2e.ts) also runs in WebKit to cover native mouse focus behavior. The supporting product contracts are `dsh-llm-replay` pacing, consumption checks, and validated indexed override patches; cross-package `dsh-llm` failures retain validated provider facts through own data properties; and the shipped web composition mounts `llm-retry` for transient model failures. ### Scaffold: `apps/web/tests/scaffold.ts` @@ -32,7 +32,7 @@ No single-shot transient-DOM assertions: every hop from replay yield to React co Pagination drivers wait for the interactive load row to leave its pending state and record the pre-request row count before scrolling. An immediately committed resident page therefore remains observable instead of becoming the baseline for a request that the scroll gesture does not repeat. -Every scenario fails on any pageerror and on the client's connection-loss/gap-repair console warnings: the reconnect machine plus history resync would otherwise self-heal a dead SSE path and the suite would certify a broken wire. Scaffold `close()` calls the `ReplayHandle.assertConsumed()` teardown check (every recorded script bound, every cursor drained), converting silent underruns and shifted bindings into crisp diagnostics. No vitest retry on the lane; one chromium per file, fresh context per scenario, one host per scenario; viewport pinned; interaction selectors anchor on roles, `data-*` attributes, and visible text, while the frame and conversation-region captures use the existing CSS-module local-name anchors. Standard scenarios open an `en-US` browser so localized role locators and goldens use one explicit language; scenarios asserting Chinese copy open a `zh-CN` browser instead, because the client derives its provisional locale from `navigator` when the Host settings document has no explicit preference ([browser-derived initial locale](../../archived/feature/2026-07-31-browser-derived-initial-locale.md)). `settings-chrome.e2e.ts` additionally covers both switch directions, a fresh English-browser default, and preference persistence across distinct ports sharing one DSH home. +Every scenario fails on any pageerror and on the client's connection-loss/gap-repair console warnings: the reconnect machine plus history resync would otherwise self-heal a dead SSE path and the suite would certify a broken wire. Scaffold `close()` calls the `ReplayHandle.assertConsumed()` teardown check (every recorded script bound, every cursor drained), converting silent underruns and shifted bindings into crisp diagnostics. No vitest retry on the lane; one browser per file and engine, fresh context per scenario, one host per scenario; viewport pinned; interaction selectors anchor on roles, `data-*` attributes, and visible text, while the frame and conversation-region captures use the existing CSS-module local-name anchors. Standard scenarios open an `en-US` browser so localized role locators and goldens use one explicit language; scenarios asserting Chinese copy open a `zh-CN` browser instead, because the client derives its provisional locale from `navigator` when the Host settings document has no explicit preference ([browser-derived initial locale](../../archived/feature/2026-07-31-browser-derived-initial-locale.md)). `settings-chrome.e2e.ts` additionally covers both switch directions, a fresh English-browser default, and preference persistence across distinct ports sharing one DSH home. ### Expected outputs @@ -50,7 +50,7 @@ The lane covers three behavior families. Live-turn scenarios pin ordinary tool e ### CI stance -The lane is a required compare-only gate for Linux pull requests under the [browser snapshot CI decision](2026-07-30-web-browser-snapshot-ci-gate.md). The `node 24 / snapshots and artifacts` consumer job owns the [single Linux build](../../archived/process/2026-07-30-independent-ci-consumer-build.md), installs the lockfile-selected Chromium, restores its OS-and-lockfile-keyed cache, and runs the lane with `DSH_SNAPSHOT=replay`. This is an intentional plane split: the host and specs use the [tsx source-launch contract](../architecture/2026-07-29-dsh-source-launch-tsx-esm.md), while the browser consumes `apps/web/dist` and package `lib/client.js` artifacts, so the gate depends on `built-package-invariants` for those client artifacts. The self-hosted default-branch Linux serial standby runs the same gate; there is no hosted Linux serial producer of the browser cache consumed by pull requests, and the persistent self-hosted pool needs no hosted cache. CI never records or refreshes goldens. Scenarios remain POSIX-oriented and stay outside the Windows and macOS matrices. +The lane is a required compare-only gate for Linux pull requests under the [browser snapshot CI decision](2026-07-30-web-browser-snapshot-ci-gate.md). The `node 24 / snapshots and artifacts` consumer job owns the [single Linux build](../../archived/process/2026-07-30-independent-ci-consumer-build.md), installs the lockfile-selected Chromium and WebKit, restores their OS-and-lockfile-keyed cache, and runs the lane with `DSH_SNAPSHOT=replay`. This is an intentional plane split: the host and specs use the [tsx source-launch contract](../architecture/2026-07-29-dsh-source-launch-tsx-esm.md), while the browser consumes `apps/web/dist` and package `lib/client.js` artifacts, so the gate depends on `built-package-invariants` for those client artifacts. The self-hosted default-branch Linux serial standby runs the same gate; there is no hosted Linux serial producer of the browser cache consumed by pull requests, and the persistent self-hosted pool needs no hosted cache. CI never records or refreshes goldens. Scenarios remain POSIX-oriented and stay outside the Windows and macOS matrices. High-cardinality performance diagnostics use the separate opt-in `apps/web/tests/**/*.perf.ts` inventory selected only by `vitest.web.perf.config.ts`. The isolated `complex-history.perf.ts` cases reuse the real scaffold: the workspace case seeds 1,000 compact sessions plus one 500-turn history containing 500 tool calls, exhausts and remounts that history in Chat, and reports Chromium main-thread, DOM, listener, heap, paging, search, and Trajectory measurements. Two continuation cases seed the same long history but compare the default 24-turn Chat window with all 500 turns expanded before each continues eight identical turns through the real composer, agent loop, SSE wire, tools, and persistence; two turns execute a real `bash` call and assert its durable result, while the final turn fills an 8,232-character mixed-language prompt and replays 120 paced text deltas. A separate soak case starts from a blank session, drives 100 consecutive real composer turns with a `bash` call and result every tenth turn, forces GC every ten turns, and reports ten-turn latency windows plus retained browser state. It then submits a 101st text-only turn with a trusted browser click and measures browser-clock send-to-transcript-DOM and send-to-post-paint latency, excluding the composer's draft mirrors, separately from full-turn completion. Per-turn diagnostics cover composer fill, click-to-user-echo, click-to-first-chunk, completion, browser mutations, persisted chunks, and tool events; the synthetic replay model has enough context capacity to keep fixture cardinality stable instead of consuming scripted calls through compaction. Structural assertions pin the intended load, stream, and tool shapes, but timing remains threshold-free because machine speed is not a correctness contract. The required `vitest.web.config.ts` inventory remains limited to `*.e2e.ts` and `*.snapshot.ts`, so neither `test:web:built` nor its CI gate collects performance cases. @@ -95,4 +95,4 @@ Surveyed AI-chat/agent web UIs and mocking layers (LibreChat, vercel/ai-chatbot ## Consequences -The web surface gains its record-once/replay-forever tier: the real chromium → SSE → apiproxy → loop → tools → persistence chain runs keylessly in ~10-30s, deterministic across repeat runs, with fixtures owned and re-recordable by the lane itself. Costs accepted: every intentional conversation-UI change ends with a keyless `DSH_SNAPSHOT=refresh` (golden churn is reviewed diff, anchors keep semantic green); the aria format is Playwright-owned — the one committed snapshot format the repo does not control — so playwright version bumps must be deliberate bump-and-refresh commits (the dependency floats `^1.49.0` in `apps/web/package.json`; pin exactly if churn bites); replay's first-call-order binding constrains scenarios to one prompting session each, with the consumption assertion as the tripwire; `compaction-basic` shares the session's replay cursor and stays inert only under the published 128k catalog window; and the required consumer job pays for Chromium provisioning and one browser run so the PR that changes the assembled UI owns its expected-output diff. The opt-in performance lane preserves a repeatable, threshold-free diagnostic workload whose measurements require manual interpretation. The separate required [frontend performance benchmarks](2026-09-06-frontend-performance-budgets.md) enforce calibrated budgets in the isolated benchmark CI job; they do not add thresholds to the manual inventory. +The web surface gains its record-once/replay-forever tier: the real chromium → SSE → apiproxy → loop → tools → persistence chain runs keylessly in ~10-30s, deterministic across repeat runs, with fixtures owned and re-recordable by the lane itself. Costs accepted: every intentional conversation-UI change ends with a keyless `DSH_SNAPSHOT=refresh` (golden churn is reviewed diff, anchors keep semantic green); the aria format is Playwright-owned — the one committed snapshot format the repo does not control — so playwright version bumps must be deliberate bump-and-refresh commits (the dependency floats `^1.49.0` in `apps/web/package.json`; pin exactly if churn bites); replay's first-call-order binding constrains scenarios to one prompting session each, with the consumption assertion as the tripwire; `compaction-basic` shares the session's replay cursor and stays inert only under the published 128k catalog window; and the required consumer job pays for Chromium and WebKit provisioning and the browser lane so the PR that changes the assembled UI owns its expected-output diff. The opt-in performance lane preserves a repeatable, threshold-free diagnostic workload whose measurements require manual interpretation. The separate required [frontend performance benchmarks](2026-09-06-frontend-performance-budgets.md) enforce calibrated budgets in the isolated benchmark CI job; they do not add thresholds to the manual inventory. diff --git a/.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.zh.md b/.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.zh.md index fbb92a094b..f715f4ca93 100644 --- a/.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.zh.md +++ b/.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.zh.md @@ -10,9 +10,7 @@ Web GUI 以一条真实组装链交付——chromium 页面 → client 插件 bu ## 决策 -布局断言先等待字体加载、框架过渡完成及 Conversation 宽度发布,再读取尺寸;触发合成 resize 后,还需等待其调度的 React 更新,才能测量通过 portal 挂载的面板。目录树的子目录和根目录读取使用相同的 Remote 等待预算。工作区重载场景在打开另一个路径编辑器前,等待 Session 选择恢复及输入框获得焦点,因为迟到的聚焦会取消路径草稿。响应式文件标签场景将实测通道宽度放在容器查询档位内部,并为平台字体度量保留明确余量。 - -`pnpm run test:web` 携带 `apps/web/tests/` 下的无密钥、确定性浏览器 e2e 车道:录制的会话日志 fixture 经 `@deepseek-ai/dsh-llm-replay` 对真实进程内 web 组合回放;用户可见状态使用规范化的 aria 预期输出,持久化的世界状态则使用进程内断言。配套的产品约定包括 `dsh-llm-replay` 的节奏控制、消费检查与已校验的索引式覆写 patch;跨包的 `dsh-llm` 失败通过自有数据属性保留经校验的提供方信息;已交付的 web 组合挂载 `llm-retry`,以处理瞬态模型失败。 +`pnpm run test:web` 携带 `apps/web/tests/` 下的无密钥、确定性浏览器 e2e 车道:录制的会话日志 fixture 经 `@deepseek-ai/dsh-llm-replay` 对真实进程内 web 组合回放;用户可见状态使用规范化的 aria 预期输出,持久化的世界状态则使用进程内断言。Chromium 运行整个车道;[模型与推理强度选择场景](../../../../apps/web/tests/declared-reasoning.e2e.ts) 还在 WebKit 中运行,以覆盖原生鼠标焦点行为。配套的产品约定包括 `dsh-llm-replay` 的节奏控制、消费检查与已校验的索引式覆写 patch;跨包的 `dsh-llm` 失败通过自有数据属性保留经校验的提供方信息;已交付的 web 组合挂载 `llm-retry`,以处理瞬态模型失败。 ### Scaffold:`apps/web/tests/scaffold.ts` @@ -28,11 +26,13 @@ Web GUI 以一条真实组装链交付——chromium 页面 → client 插件 bu 回放模式下浏览器断言的屏障栈,按序:(1)host 侧 `await agent.whenIdle()` 加超时,以进程内 `turn/end` 为锚——空闲翻转发生在持久化落盘之后,一次等待同时覆盖轮次完成与持久性;(2)浏览器安定轮询(流式输出节点已卸载、最终文本可见)。录制模式下,日志采收在 `whenIdle()` 之后、scaffold 释放之前进行,此时运行中的会话仍然可用。单独监听进程内 `turn/end` 是错误屏障(它先于 SSE 帧到达浏览器、先于 fsync 触发);禁止轮询持久化文件来充当轮次完成或持久性屏障(NFS 上慢,且被 `whenIdle` 取代),但工具控制的临时就绪标记可以仅作为该完成屏障之前的交互门控进行轮询;`networkidle` 被彻底禁止(SSE 流保持打开时它永不解析)。导航断言会在页面加载前同时监听 `session.list` 和 `workspace.list` 的初始响应,随后等待播种数据投影到 DOM;仅凭 shell 已挂载不能判定就绪,因为较晚完成的 bootstrap 可能替换受控状态。 +布局断言先等待字体加载、框架过渡完成及 Conversation 宽度发布,再读取尺寸;触发合成 resize 后,还需等待其调度的 React 更新,才能测量通过 portal 挂载的面板。目录树的子目录和根目录读取使用相同的 Remote 等待预算。工作区重载场景在打开另一个路径编辑器前,等待 Session 选择恢复及输入框获得焦点,因为迟到的聚焦会取消路径草稿。响应式文件标签场景将实测通道宽度放在容器查询档位内部,并为平台字体度量保留明确余量。 + 不做单次瞬态 DOM 断言:从 replay yield 到 React commit 的每一跳都可能合并 chunk,采样 `[data-streaming]` 天然就是竞态。流式增量性通过有序 `agent/assistant-stream` follow path 断言,最终持久 `assistant/message` 或 `assistant/attempt` 则嵌入 replay 使用的精确 stream。`dsh-llm-replay` 的可选 `paceMs`(默认缺省 = burst)只是让浏览器观察到真正增量 SSE 的真实感旋钮;正确性绝不依赖它,且 pace wait 期间 abort 会即时取消。 分页驱动会等待交互式加载行退出 pending 状态,并在滚动前记录请求前的行数。因此,即时提交的常驻页仍然可观测,不会变成一次滚动手势不会重复触发的请求基线。 -每个场景都会因任何 pageerror 或客户端的连接丢失/间隙修复控制台警告而失败:否则重连机制加历史重同步会把一条死掉的 SSE 通路自愈掉,套件反而认证了坏 wire。Scaffold 的 `close()` 调用 `ReplayHandle.assertConsumed()` 收尾检查(每个已录脚本都被绑定、每个游标都耗尽),把静默的少放与错绑变成清晰诊断。车道不设 vitest 重试;每文件一个 chromium、每场景一个新 context、每场景一个 host;视口固定;交互选择器锚定 role、`data-*` 属性和可见文本,而 frame 与会话区采集则使用既有的 CSS 模块局部类名锚点。常规场景开启 `en-US` 浏览器,使本地化的 role 定位器和预期输出统一采用明确指定的语言;断言中文文案的场景则开启 `zh-CN` 浏览器,因为 Host settings 文档没有显式偏好时,客户端的暂定 locale 由 `navigator` 推导([由浏览器推导初始 locale](../../archived/feature/2026-07-31-browser-derived-initial-locale.md))。`settings-chrome.e2e.ts` 还额外覆盖双向切换、全新英文浏览器默认态,以及共享同一 DSH home 的不同端口之间的偏好持久化。 +每个场景都会因任何 pageerror 或客户端的连接丢失/间隙修复控制台警告而失败:否则重连机制加历史重同步会把一条死掉的 SSE 通路自愈掉,套件反而认证了坏 wire。Scaffold 的 `close()` 调用 `ReplayHandle.assertConsumed()` 收尾检查(每个已录脚本都被绑定、每个游标都耗尽),把静默的少放与错绑变成清晰诊断。车道不设 vitest 重试;每个文件中的每种引擎各启动一个浏览器、每场景一个新 context、每场景一个 host;视口固定;交互选择器锚定 role、`data-*` 属性和可见文本,而 frame 与会话区采集则使用既有的 CSS 模块局部类名锚点。常规场景开启 `en-US` 浏览器,使本地化的 role 定位器和预期输出统一采用明确指定的语言;断言中文文案的场景则开启 `zh-CN` 浏览器,因为 Host settings 文档没有显式偏好时,客户端的暂定 locale 由 `navigator` 推导([由浏览器推导初始 locale](../../archived/feature/2026-07-31-browser-derived-initial-locale.md))。`settings-chrome.e2e.ts` 还额外覆盖双向切换、全新英文浏览器默认态,以及共享同一 DSH home 的不同端口之间的偏好持久化。 ### 预期输出 @@ -50,7 +50,7 @@ Web GUI 以一条真实组装链交付——chromium 页面 → client 插件 bu ### CI 立场 -根据[浏览器快照 CI 决策](2026-07-30-web-browser-snapshot-ci-gate.zh.md),该车道是 Linux 拉取请求必需的只比较门禁。`node 24 / snapshots and artifacts` 消费方任务在[消费方独立构建](../../archived/process/2026-07-30-independent-ci-consumer-build.md)中负责唯一一次 Linux 构建,安装锁文件选定的 Chromium,恢复以操作系统和锁文件为键的缓存,并用 `DSH_SNAPSHOT=replay` 运行该车道。这是有意的平面切分:host 与 spec 使用 [tsx 源码启动约定](../architecture/2026-07-29-dsh-source-launch-tsx-esm.zh.md),浏览器则消费 `apps/web/dist` 和包的 `lib/client.js` 产物,因此门禁依赖 `built-package-invariants` 提供这些客户端产物。自托管的默认分支 Linux 串行热备运行同一门禁;不存在托管 Linux 串行生产者生成供 PR 消费的浏览器缓存,持久化自托管池则不需要托管侧缓存。CI 从不录制或刷新预期输出。场景仍面向 POSIX,并继续置于 Windows 和 macOS 矩阵之外。 +根据[浏览器快照 CI 决策](2026-07-30-web-browser-snapshot-ci-gate.zh.md),该车道是 Linux 拉取请求必需的只比较门禁。`node 24 / snapshots and artifacts` 消费方任务在[消费方独立构建](../../archived/process/2026-07-30-independent-ci-consumer-build.md)中负责唯一一次 Linux 构建,安装锁文件选定的 Chromium 和 WebKit,恢复以操作系统和锁文件为键的缓存,并用 `DSH_SNAPSHOT=replay` 运行该车道。这是有意的平面切分:host 与 spec 使用 [tsx 源码启动约定](../architecture/2026-07-29-dsh-source-launch-tsx-esm.zh.md),浏览器则消费 `apps/web/dist` 和包的 `lib/client.js` 产物,因此门禁依赖 `built-package-invariants` 提供这些客户端产物。自托管的默认分支 Linux 串行热备运行同一门禁;不存在托管 Linux 串行生产者生成供 PR 消费的浏览器缓存,持久化自托管池则不需要托管侧缓存。CI 从不录制或刷新预期输出。场景仍面向 POSIX,并继续置于 Windows 和 macOS 矩阵之外。 高基数性能诊断使用单独按需启用的 `apps/web/tests/**/*.perf.ts` 清单,并且只由 `vitest.web.perf.config.ts` 选中。`complex-history.perf.ts` 的隔离用例复用真实 scaffold:工作区用例播种 1,000 个紧凑会话以及一份包含 500 次工具调用的 500 轮次历史,在 Chat 中穷尽并重新挂载该历史,并报告 Chromium 主线程、DOM、监听器、堆内存、分页、搜索和 Trajectory 测量结果。两个续聊用例播种同一份长历史,但比较默认的 24 轮次 Chat 窗口与展开全部 500 轮次的状态,然后各自通过真实输入框、agent loop、SSE wire、工具和持久化继续进行 8 个相同轮次;其中两轮执行真实 `bash` 调用并断言其持久化结果,最后一轮则填入一条包含 8,232 个字符的混合语言提示词,并回放 120 个带节奏的文本增量。一个单独的 soak 用例从空白会话开始,通过真实输入框连续驱动 100 轮,每第 10 轮执行一次 `bash` 调用并产生结果,每 10 轮强制执行一次 GC,并报告每 10 轮的延迟窗口及保留的浏览器状态。随后它通过受信任的浏览器点击提交第 101 个纯文本轮次,并使用浏览器时钟分别测量发送到 transcript DOM 和发送到绘制后的延迟,排除输入框的草稿镜像,并与完整轮次完成时间分开。逐轮诊断涵盖输入框填入、点击到用户消息回显、点击到首个分片、完成、浏览器变更、持久化分片和工具事件;合成回放模型拥有足够的上下文容量,可使 fixture 基数保持稳定,而不会因压缩(compaction)消耗脚本化调用。结构性断言钉住预期的负载、流和工具形状,但时间仍不设阈值,因为机器速度不属于正确性约定。必需的 `vitest.web.config.ts` 清单仍仅限 `*.e2e.ts` 和 `*.snapshot.ts`,因此 `test:web:built` 及其 CI 门禁都不会收集性能用例。 @@ -95,4 +95,4 @@ Web GUI 以一条真实组装链交付——chromium 页面 → client 插件 bu ## 后果 -Web 表面获得了录制一次/永久回放的层级:真实 chromium → SSE → apiproxy → 循环 → 工具 → 持久化的链路以约 10-30 秒无密钥运行,重复运行结果确定,fixture 由车道自身持有并可重录。接受的成本:每次有意的会话 UI 变更都以一次无密钥 `DSH_SNAPSHOT=refresh` 收尾(预期输出变动是受评审的 diff,锚断言保住语义绿色);aria 格式归 Playwright 所有——仓库唯一不受自己控制的提交快照格式——因此 playwright 版本升级必须是刻意的升级加刷新提交(依赖在 `apps/web/package.json` 中浮动为 `^1.49.0`;若变动伤人则改为精确锁定);回放的首次调用顺序绑定把每个场景限制为至多一个发起提示的会话,消费断言是绊线;`compaction-basic` 与会话共享回放游标,仅在目录中发布的 128k 上下文窗口下保持闲置;必需的消费方任务承担 Chromium 供给与一次浏览器运行的成本,使改动组装后 UI 的 PR(Pull Request)持有相应的预期输出 diff。按需启用的性能车道保留可重复、无阈值的诊断工作负载,其测量需要人工解读。独立的必需[前端性能基准](2026-09-06-frontend-performance-budgets.zh.md)在隔离的基准 CI job 中执行经校准的预算;它们不向手动清单添加阈值。 +Web 表面获得了录制一次/永久回放的层级:真实 chromium → SSE → apiproxy → 循环 → 工具 → 持久化的链路以约 10-30 秒无密钥运行,重复运行结果确定,fixture 由车道自身持有并可重录。接受的成本:每次有意的会话 UI 变更都以一次无密钥 `DSH_SNAPSHOT=refresh` 收尾(预期输出变动是受评审的 diff,锚断言保住语义绿色);aria 格式归 Playwright 所有——仓库唯一不受自己控制的提交快照格式——因此 playwright 版本升级必须是刻意的升级加刷新提交(依赖在 `apps/web/package.json` 中浮动为 `^1.49.0`;若变动伤人则改为精确锁定);回放的首次调用顺序绑定把每个场景限制为至多一个发起提示的会话,消费断言是绊线;`compaction-basic` 与会话共享回放游标,仅在目录中发布的 128k 上下文窗口下保持闲置;必需的消费方任务承担 Chromium 与 WebKit 供给及浏览器车道的成本,使改动组装后 UI 的 PR(Pull Request)持有相应的预期输出 diff。按需启用的性能车道保留可重复、无阈值的诊断工作负载,其测量需要人工解读。独立的必需[前端性能基准](2026-09-06-frontend-performance-budgets.zh.md)在隔离的基准 CI job 中执行经校准的预算;它们不向手动清单添加阈值。 diff --git a/.agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.i18n.yaml b/.agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.i18n.yaml index f3df1f5527..aeb64a1a11 100644 --- a/.agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.i18n.yaml +++ b/.agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.md -2026-07-30-web-browser-snapshot-ci-gate.md: d1c8dcdd859d0515804cc6aed0d2f3341beb6dd0 -2026-07-30-web-browser-snapshot-ci-gate.zh.md: 9d642819e09abc63384f4f4eb105d13261a9eb95 +2026-07-30-web-browser-snapshot-ci-gate.md: 352072bca6e1a9e1d5b72973d2c3c9e96bc58ad9 +2026-07-30-web-browser-snapshot-ci-gate.zh.md: cd04f33a5db6ddb3c88a235fb34ea31da0e5a869 diff --git a/.agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.md b/.agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.md index d1c8dcdd85..352072bca6 100644 --- a/.agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.md +++ b/.agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.md @@ -12,13 +12,13 @@ The [keyless web browser e2e lane](2026-07-24-web-gui-browser-e2e-lane.md) runs For Linux PRs, the `node 24 / snapshots and artifacts` job must run the full web browser replay/compare suite. When `DSH_WEB_SNAPSHOT_WORKERS` is configured, `scripts/run-gates.ts` registers `test:web:ci` as the `ci-consumers` gate and explicitly injects `DSH_SNAPSHOT=replay`; CI never runs in `record` or `refresh` mode, so when the committed goldens disagree with the currently assembled application, the tests fail directly instead of silently rewriting them on the runner and then passing. -The consumer job owns the [single Linux build](../../archived/process/2026-07-30-independent-ci-consumer-build.md), so `apps/web/dist` and the package `lib/` directories remain in its workspace for the browser suite. On hosted runners, CI installs Chromium and its system dependencies at the Playwright version in the lockfile. On the persistent failover VM, the image owns the Linux system packages and CI installs only Chromium, avoiding per-run `apt` mutation. Pull requests restore the operating-system-and-lockfile-keyed browser cache without paying compression and upload on the required path, with an operating-system prefix fallback across lockfile changes. No master job produces these hosted caches, so restores hit archived entries until they evict. The self-hosted standby runs the same comparison without hosted cache actions. +The consumer job owns the [single Linux build](../../archived/process/2026-07-30-independent-ci-consumer-build.md), so `apps/web/dist` and the package `lib/` directories remain in its workspace for the browser suite. Chromium runs the full suite; the model and reasoning picker also runs in WebKit to cover native mouse focus behavior. On hosted runners, CI installs both engines and their system dependencies at the Playwright version in the lockfile. On the persistent failover VM, the image owns both engines’ Linux system packages and CI installs only the browser binaries, avoiding per-run `apt` mutation. Pull requests restore the operating-system-and-lockfile-keyed browser cache without paying compression and upload on the required path, with an operating-system prefix fallback across lockfile changes. No master job produces these hosted caches, so restores hit archived entries until they evict. The self-hosted standby runs the same comparison without hosted cache actions. Local `pnpm run test:web` continues to build first and then run the full browser suite serially; `test:web:built` is the serial entry point for existing build artifacts. Developers explicitly run `DSH_SNAPSHOT=refresh pnpm run test:web` only after confirming that user-visible output changed intentionally, review every expected-output diff, and then verify again in replay mode that no files are written. CI’s `scripts/run-web-snapshots.ts` runs `hmr-live.e2e.ts` separately because it mutates built workspace state. One six-worker Vitest pool then runs every remaining file, including historical Cordis-card rendering. Every child inherits stdio, and `run-gates` streams its output. -For pull requests, the gate runs only in the Linux consumer job: these scenarios target POSIX, and the other PR jobs do not provision Chromium. The self-hosted default-branch Linux serial standby also includes the comparison, while the macOS and Windows serial jobs remain browser-free (there is no hosted Linux serial aggregate). A PR's `all checks passed` verdict already depends on the consumer job, so a browser compare failure blocks the merge without requiring a new branch-protection check name. +For pull requests, the gate runs only in the Linux consumer job: these scenarios target POSIX, and the other PR jobs do not provision both engines. The self-hosted default-branch Linux serial standby also includes the comparison, while the macOS and Windows serial jobs remain browser-free (there is no hosted Linux serial aggregate). A PR's `all checks passed` verdict already depends on the consumer job, so a browser compare failure blocks the merge without requiring a new branch-protection check name. Completed local replays measured the six-worker browser command at about 65–71 seconds. A twelve-worker comparison completed in about 50 seconds, so halving the browser worker budget adds about 15–20 seconds rather than doubling wall time. The gate scheduler starts browser snapshots as soon as `built-package-invariants` succeeds and runs independent gates concurrently, so it needs neither a dedicated job timeout nor a manual YAML ordering rule. @@ -36,4 +36,4 @@ Completed local replays measured the six-worker browser command at about 65–71 ## Consequences -Before merge, every PR proves that the current web assembly matches all committed browser expected outputs; a missing refresh fails in the same PR that changes the assembly. The cost is Chromium provisioning, one serial scenario, and one bounded six-worker pool in the consumer job; the consumer-owned build and browser cache avoid duplicate builds and downloads on reruns. Parallel-file failures stream immediately, but a worker-budget change still requires a completed end-to-end measurement rather than an elapsed-time guess. The gate makes no claim of cross-platform browser consistency, and if a Playwright/Chromium upgrade changes the ARIA format, the upgrade PR must explicitly refresh the expected outputs and review the churn. +Before merge, every PR proves that the current web assembly matches all committed browser expected outputs; a missing refresh fails in the same PR that changes the assembly. The cost is Chromium and WebKit provisioning, one serial scenario, and one bounded six-worker pool in the consumer job; the consumer-owned build and browser cache avoid duplicate builds and downloads on reruns. Parallel-file failures stream immediately, but a worker-budget change still requires a completed end-to-end measurement rather than an elapsed-time guess. The gate makes no claim of cross-platform browser consistency, and if a Playwright or browser upgrade changes the ARIA format, the upgrade PR must explicitly refresh the expected outputs and review the churn. diff --git a/.agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.zh.md b/.agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.zh.md index 9d642819e0..cd04f33a5d 100644 --- a/.agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.zh.md +++ b/.agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.zh.md @@ -12,13 +12,13 @@ Status: implemented Linux PR 的 `node 24 / snapshots and artifacts` 必须运行完整 Web 浏览器 replay/compare。配置 `DSH_WEB_SNAPSHOT_WORKERS` 后,`scripts/run-gates.ts` 把 `test:web:ci` 登记为 `ci-consumers` 门禁,并显式注入 `DSH_SNAPSHOT=replay`;CI 永不以 `record` 或 `refresh` 模式运行,因此提交的预期输出与当前组装应用不一致时测试直接失败,不会在 runner 内静默改写后通过。 -消费方 job 在[消费方独立构建](../../archived/process/2026-07-30-independent-ci-consumer-build.md)中负责唯一一次 Linux 构建,因此 `apps/web/dist` 和包的 `lib/` 目录会保留在其工作区中,供浏览器套件使用。在托管运行器上,CI 按锁文件中的 Playwright 版本安装 Chromium 及其系统依赖。在持久化故障切换 VM 上,镜像负责预装 Linux 系统软件包,CI 只安装 Chromium,避免每次运行都通过 `apt` 改动系统。PR 恢复以操作系统和锁文件为键的浏览器缓存,使必需路径无需承担压缩和上传开销,并可在锁文件变化时按操作系统前缀回退。没有任何 master 作业生成这些 hosted 缓存,因此恢复只能命中仍有归档的旧条目,直至其被逐出。自托管热备运行相同的比较,但不执行托管缓存操作。 +消费方 job 在[消费方独立构建](../../archived/process/2026-07-30-independent-ci-consumer-build.md)中负责唯一一次 Linux 构建,因此 `apps/web/dist` 和包的 `lib/` 目录会保留在其工作区中,供浏览器套件使用。Chromium 运行完整套件;模型与推理强度选择场景还在 WebKit 中运行,以覆盖原生鼠标焦点行为。在托管运行器上,CI 按锁文件中的 Playwright 版本安装这两种引擎及其系统依赖。在持久化故障切换 VM 上,镜像负责预装两种引擎的 Linux 系统软件包,CI 只安装浏览器程序,避免每次运行都通过 `apt` 改动系统。PR 恢复以操作系统和锁文件为键的浏览器缓存,使必需路径无需承担压缩和上传开销,并可在锁文件变化时按操作系统前缀回退。没有任何 master 作业生成这些 hosted 缓存,因此恢复只能命中仍有归档的旧条目,直至其被逐出。自托管热备运行相同的比较,但不执行托管缓存操作。 本地 `pnpm run test:web` 仍先构建,再串行运行完整浏览器套件;`test:web:built` 是已有构建产物的串行执行入口。开发者只在确认用户可见输出有意变化后显式运行 `DSH_SNAPSHOT=refresh pnpm run test:web`,评审每一处预期输出 diff,再以 replay 模式复验不再写文件。 CI 的 `scripts/run-web-snapshots.ts` 单独运行会修改构建产物的 `hmr-live.e2e.ts`。随后由一个六 worker Vitest 池运行其余文件,包括历史 Cordis 卡片渲染。所有子进程继承 stdio,由 `run-gates` 流式转发输出。 -对 PR 而言,门禁仅在 Linux 消费方 job 中运行:这些场景面向 POSIX,其他 PR job 不安装 Chromium。自托管的默认分支 Linux 串行热备也包含该比较,而 macOS 和 Windows 串行 job 仍不使用浏览器(不存在托管的 Linux 串行聚合)。PR 的 `all checks passed` 已依赖消费方 job,因此浏览器比较失败会阻止合并,无需新增 branch-protection check 名称。 +对 PR 而言,门禁仅在 Linux 消费方 job 中运行:这些场景面向 POSIX,其他 PR job 不会同时安装这两种引擎。自托管的默认分支 Linux 串行热备也包含该比较,而 macOS 和 Windows 串行 job 仍不使用浏览器(不存在托管的 Linux 串行聚合)。PR 的 `all checks passed` 已依赖消费方 job,因此浏览器比较失败会阻止合并,无需新增 branch-protection check 名称。 完整本地 replay 中,6-worker 浏览器命令耗时约 65–71 秒。12-worker 对比约为 50 秒,因此把浏览器 worker 预算减半只增加约 15–20 秒,而不是让墙钟时间翻倍。门禁调度器会在 `built-package-invariants` 成功后立即启动浏览器快照,并发运行彼此独立的门禁,因此既不需要专用 job 超时,也不需要手动制定 YAML 顺序规则。 @@ -36,4 +36,4 @@ CI 的 `scripts/run-web-snapshots.ts` 单独运行会修改构建产物的 `hmr- ## 后果 -每个 PR 都在合并前证明当前 Web 组装与所有已提交的浏览器预期输出一致;漏刷会在改变该组装的同一个 PR 中失败。成本是消费方 job 需要安装 Chromium、串行运行 2 个场景并执行 1 个有界 6-worker 池;消费方独立构建与浏览器缓存避免重跑时重复构建和下载。并行文件的失败会立即流式显示,但 worker 预算的任何变化仍需要完整端到端测量,而不能依据运行中耗时猜测。门禁不声称跨平台浏览器一致性,Playwright/Chromium 升级若改变 ARIA 格式,升级 PR 必须显式 refresh 并评审 churn。 +每个 PR 都在合并前证明当前 Web 组装与所有已提交的浏览器预期输出一致;漏刷会在改变该组装的同一个 PR 中失败。成本是消费方 job 需要安装 Chromium 和 WebKit、串行运行 1 个场景并执行 1 个有界 6-worker 池;消费方独立构建与浏览器缓存避免重跑时重复构建和下载。并行文件的失败会立即流式显示,但 worker 预算的任何变化仍需要完整端到端测量,而不能依据运行中耗时猜测。门禁不声称跨平台浏览器一致性,Playwright 或浏览器升级若改变 ARIA 格式,升级 PR 必须显式 refresh 并评审 churn。 diff --git a/.agents/skills/dsh-client-ui-ux/SKILL.md b/.agents/skills/dsh-client-ui-ux/SKILL.md new file mode 100644 index 0000000000..de63792f72 --- /dev/null +++ b/.agents/skills/dsh-client-ui-ux/SKILL.md @@ -0,0 +1,62 @@ +--- +name: dsh-client-ui-ux +description: Design and review DeepSeek Harness client UI changes — visual token discipline, reuse-before-adding, feedback surfaces (toast vs in-place notice vs empty state), overlay and menu safety, platform window adaptation, loading states, and error copy placement. Use when adding or changing product-user-visible GUI behavior in packages/client, or when reviewing such a PR. +--- + +# DeepSeek Harness Client UI/UX + +This skill is guidance, not a complete checklist. It covers judgment calls that lint, typecheck, and the i18n gate cannot make: where feedback appears, what gets reused, and how surfaces behave at window edges and across platforms. Copy quality itself follows [dsh-prose-standard](../dsh-prose-standard/SKILL.md); locale ownership follows the [locale-owned copy decision](../../notes/implemented/architecture/2026-08-23-locale-owned-client-ui-copy.md); styling ownership and token rules live in [docs/web-styling.md](../../../docs/web-styling.md) and the [ui-primitives component catalog](../../../packages/client/ui-primitives/README.md#component-catalog) — this skill adds judgment on top of those rules, not a second copy of them. + +## Visual foundation + +- **Use existing tokens for color, radius, and spacing.** Before introducing a literal value, search the target page and its nearest established sibling (the Plugins page is the usual reference for full-page surfaces) for the token or value already in use, and adopt it. The same applies to whole treatments — toasts, menus, in-message artifact previews, sidebar tab layout, colors, clearance rules, page padding: reuse the established component or rule wherever one exists instead of inventing a parallel one. +- **Verify every color in both light and dark mode.** An ink/background pair must stay readable under both themes before it ships. Dimmed tones are the standing counter-example: they suit decorative surfaces, but on text they routinely fall below readability in one theme — never put a dimmed color on text without checking both. +- **Font weight tops out at 500 in feature CSS you add or change.** Do not use 600+ for emphasis; differentiate with size or ink instead. The markdown typography tokens owned by ui-theme are the standing exception. +- **Pick font sizes from the theme typography variables when an existing role matches, otherwise from the page's existing scale.** Parallel content shares one size; never invent a size for a single element, and keep the number of distinct sizes on a page small (13px is the established row/detail size). When the page itself has no precedent, borrow the size and spacing ratios of the most similar existing page or element. +- **Icons, text, and neighbouring elements stay proportionate and aligned.** Every element lines up with its siblings along a deliberate axis (baseline, vertical center, or shared edge), and an icon's size stays in proportion to the text beside it; never ship an element that aligns to nothing or visibly outweighs its neighbours. + +## Reuse before adding + +- **Extend an existing component, container, or interaction before creating a new one.** A new capability that fits an existing surface (an extra menu item, a new prop on an existing primitive) beats a parallel element stacked beside it. +- **Icons come from the existing icon library.** Pick the semantically closest existing icon; a new glyph requires designer approval before it lands — when reviewing a PR that adds one, suggest requesting a designer review. +- **Right-sidebar content registers a slot in the existing sidebar**, never a separate sidebar. Every sidebar tab declares a tab icon matching its meaning. +- **Icon-only actions whose meaning is not obvious get a Tooltip.** Informational content the pointer must rest on or select uses HoverCard instead. Prefer these shared primitives over ad-hoc title attributes. + +## Feedback surfaces + +Choose the surface by the lifetime of the message relative to the surface that produced it: + +- **Transient operation outcomes use the app-wide Toast primitive**, not in-place notices, and the toast's state and rendering live in a host that outlives the reporting surface (a `shell.overlay` entry, as RowActionToast does) — a toast rendered by the panel itself unmounts with that panel. A deletion can close the panel or tab that requested it; only a toast held outside that surface survives the close. Report success and failure; do not report still-pending states. +- **A failed operation keeps the data visible.** A failed deletion keeps its row; the toast announces the failure. Never blank content to show an error for a transient operation. +- **In-place notices are for states tied to the surface itself**: a query failure with its Retry action, field validation on the form that owns the field. A populated panel keeps a compact notice beside its retained content; only an empty panel centers a full error state. +- **Error copy is plain language, and short.** No internal jargon or technical nouns unless an error code must be exposed for debugging. Prefer one clear sentence; anything explainable in two sentences must not grow into paragraphs. A Chinese notice of at most two sentences omits the trailing 句号(。). The notice must not break the existing layout — reserve its space or overlay it; never shift neighbouring elements. + +## Loading states + +- Lists use their skeleton; every other page-level load centers a bare spinner. Never place a spinner in a corner of the page. +- One loading treatment per page/component — concurrent regions must not each flash their own style. +- No loading text unless the context needs it (narrow sidebars almost never do). + +## Overlays and menus + +Every menu, popover, and tooltip must be verified for all three before merge: + +- **Dismissable**: outside click (and Escape where focusable) closes it. +- **Viewport-fitting**: it slides or flips to stay inside the window with an edge margin, and flips only into a side that actually fits (no oscillation when neither side fits). +- **Unclipped**: it escapes `overflow` ancestors and containing blocks (portal to body where needed) and is never covered by neighbouring elements. + +Scrollbars stay inside their container: use the shared scrollbar styles rather than component-specific selectors, and verify the bar neither overflows the container nor gets clipped by its rounded corners or edges — keep a small inset instead of letting it sit flush against the container edge. + +## Platform window adaptation + +- Any new header element or full page must account for macOS and Windows window chrome. On macOS the titlebar clearance is `--dsh-frame-top-clearance`, consumed by Menu, Modal, and overlay primitives as their top safe margin — **never change the global variable to fix one page**. Add a scoped `[data-platform='darwin']` override on the page's own inset instead, and assert the override in the page's stylesheet spec. +- State the intended total offset (clearance + page inset) when reviewing, so two pages aligning to "the same top spacing" agree on the sum, not just one addend. + +## Spacing review + +- Review every changed region for gaps: nothing sits flush against its neighbour without an intentional gap, and adjacent icons and elements keep the region's established spacing. +- Prefer symmetry for parallel elements; an unexplained one-off gap or offset usually signals a missed shared value. + +## Design review escalation + +- A PR with extensive UI changes, or any clearly user-perceivable change to UI or interaction, must get a designer or product review. When no approver holds that role, suggest requesting one from the design/product reviewers (roster as of 2026-09: yx zhang, yifffan, zhangziya, gaokaige; update this list here when membership changes). diff --git a/.agents/skills/dsh-code-review/SKILL.md b/.agents/skills/dsh-code-review/SKILL.md index 7453e110ef..bc145e212a 100644 --- a/.agents/skills/dsh-code-review/SKILL.md +++ b/.agents/skills/dsh-code-review/SKILL.md @@ -14,6 +14,7 @@ description: Use when reviewing a pull request in the deepseek-harness repo — - [docs/AGENTS.md](../../../docs/AGENTS.md): documentation placement and prose discipline. - [dsh-prose-standard](../dsh-prose-standard/SKILL.md): required coverage and editorial judgment for comments, docs, prompts, and visible strings. - [dsh-ci-test-reliability](../dsh-ci-test-reliability/SKILL.md): isolation and regression-proof rules for resource-owning, asynchronous, or flaky tests and fixtures. +- [dsh-client-ui-ux](../dsh-client-ui-ux/SKILL.md): feedback-surface choice, overlay safety, platform window adaptation, and visual token discipline for product-user-visible GUI changes. - [docs/testing.md](../../../docs/testing.md) and the [quality-gates Agent Note](../../notes/implemented/process/2026-06-11-quality-gates.md): required test tiers and gates. - [Agent Notes](../../notes/README.md): design rationale. Treat disagreement with an Agent Note as a design discussion, not an automatic veto. - For bilingual changes, read [translation-rules.md](../../../docs/i18n/translation-rules.md) and [terminology.md](../../../docs/i18n/terminology.md); the extended translation skill is outside automatic review and runs only on explicit user invocation. diff --git a/.github/workflows/ci-master.yml b/.github/workflows/ci-master.yml index be96b7effa..75410ad410 100644 --- a/.github/workflows/ci-master.yml +++ b/.github/workflows/ci-master.yml @@ -165,8 +165,8 @@ jobs: # The persistent VM image owns Playwright's Linux system packages; this # step also proves that browser provisioning remains usable for failover. - - name: Install Playwright Chromium - run: pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install chromium + - name: Install Playwright Chromium and WebKit + run: pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install chromium webkit - name: Prepare bubblewrap (unrestrict userns) run: bash scripts/prepare-ci-bubblewrap.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c2077159b..9c8d801864 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -344,18 +344,18 @@ jobs: r'https://\1/', source.read_text())) PY - - name: Install Playwright Chromium and hosted dependencies + - name: Install Playwright Chromium, WebKit, and hosted dependencies if: vars.DSH_CI_FAILOVER_LINUX != 'selfhosted' || github.event.pull_request.user.login == 'dependabot[bot]' # Ephemeral runner images can carry Ubuntu mirrors whose HTTP endpoints are unreachable. run: | sudo find /etc/apt -maxdepth 2 -type f \( -name '*.list' -o -name '*.sources' \) -exec sed -i -E 's@http://([a-z]{2}\.)?archive\.ubuntu\.com/ubuntu@https://archive.ubuntu.com/ubuntu@g; s@http://security\.ubuntu\.com/ubuntu@https://security.ubuntu.com/ubuntu@g' {} + - pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install --with-deps chromium + pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install --with-deps chromium webkit # The persistent VM image owns Playwright's Linux system packages; do # not mutate the shared host with apt on every failover run. - - name: Install Playwright Chromium on the failover VM + - name: Install Playwright Chromium and WebKit on the failover VM if: vars.DSH_CI_FAILOVER_LINUX == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' - run: pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install chromium + run: pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install chromium webkit - name: Build and execute the Linux Office runtime run: CI=true pnpm run prepare:primary-runtime --target linux-x64 --output "$RUNNER_TEMP/office-runtime" --cache "$RUNNER_TEMP/office-runtime-downloads" diff --git a/apps/cli/package.json b/apps/cli/package.json index d3ced6a684..b433c2407f 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh", "description": "dsh CLI: profile launch, plugin management, and configuration inspection", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/apps/desktop-host/package.json b/apps/desktop-host/package.json index 705921ba0d..5d9628bdee 100644 --- a/apps/desktop-host/package.json +++ b/apps/desktop-host/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-desktop-host", "description": "Private Node-mode host process for the Electron desktop application", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "private": true, "license": "MIT", "type": "module", diff --git a/apps/desktop/package.json b/apps/desktop/package.json index ba33a3fb3e..db69834af2 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-desktop", "description": "Electron desktop shell for a bundled dsh runtime and external plugins", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "private": true, "license": "MIT", "type": "module", diff --git a/apps/web/package.json b/apps/web/package.json index 87752f4bb5..f1c27b66c1 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-web-frontend", "description": "Web application entry: vite build over the @deepseek-ai/dsh-client-web shell library; dist/ served by apps/cli's dsh web", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/apps/web/tests/README.i18n.yaml b/apps/web/tests/README.i18n.yaml index e48e24302b..6828bcc5ae 100644 --- a/apps/web/tests/README.i18n.yaml +++ b/apps/web/tests/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write apps/web/tests/README.md -README.md: 2744c40c7ff6d90e2d1d6cefadfcf6b55acb7e1b -README.zh.md: e7dd1a23b96e5b3eff055e49deec8c86d8e91727 +README.md: 1dce900ab2ffa16850fe50573f9177088c8201db +README.zh.md: fd14377bc01897e7d330d36603601f9dce16e295 diff --git a/apps/web/tests/README.md b/apps/web/tests/README.md index 2744c40c7f..1dce900ab2 100644 --- a/apps/web/tests/README.md +++ b/apps/web/tests/README.md @@ -2,7 +2,15 @@ English | [中文](README.zh.md) -These tests boot the real web composition in-process and drive it with a real Chromium over real HTTP. The lane's mechanics — modes, fixtures, goldens, and the deliberate composition divergences from `dsh web` — are documented in [`scaffold.ts`](scaffold.ts) and the [browser e2e Agent Note](../../../.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md). +These tests boot the real web composition in-process and drive it with real browsers over real HTTP. Chromium runs the full lane; the [model and reasoning picker scenario](declared-reasoning.e2e.ts) also runs in WebKit to cover native mouse focus behavior. The lane's mechanics — modes, fixtures, goldens, and the deliberate composition divergences from `dsh web` — are documented in [`scaffold.ts`](scaffold.ts) and the [browser e2e Agent Note](../../../.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md). + +After installing workspace dependencies, install the browsers and their system dependencies from the repository root: + +```sh +pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install --with-deps chromium webkit +``` + +On Linux, `--with-deps` installs dependencies through the system package manager. The persistent CI VM must provide these dependencies through image maintenance; CI installs only the browser binaries, as required by the [failover runbook](../../../.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md). Ordinary scenarios begin with no registered Workspace or Session and a durable marker recording a removed default Workspace, so explicit folder-selection scenarios retain control of their cwd. `launchWebScaffold({ firstUse: true })` leaves initialization eligible for startup scenarios. diff --git a/apps/web/tests/README.zh.md b/apps/web/tests/README.zh.md index e7dd1a23b9..fd14377bc0 100644 --- a/apps/web/tests/README.zh.md +++ b/apps/web/tests/README.zh.md @@ -2,7 +2,15 @@ [English](README.md) | 中文 -这些测试在进程内启动真实的 web 组合,并用真实 Chromium 通过真实 HTTP 驱动它。该 lane 的运行机制——模式、fixture(测试前置数据)、golden,以及与 `dsh web` 之间刻意保留的组合差异——记录在 [`scaffold.ts`](scaffold.ts) 和 [浏览器 e2e Agent Note](../../../.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.zh.md) 中。 +这些测试在进程内启动真实的 web 组合,并用真实浏览器通过真实 HTTP 驱动它。Chromium 运行整个 lane;[模型与推理强度选择场景](declared-reasoning.e2e.ts) 还在 WebKit 中运行,以覆盖原生鼠标焦点行为。该 lane 的运行机制——模式、fixture(测试前置数据)、golden,以及与 `dsh web` 之间刻意保留的组合差异——记录在 [`scaffold.ts`](scaffold.ts) 和 [浏览器 e2e Agent Note](../../../.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.zh.md) 中。 + +安装工作区依赖后,在仓库根目录安装浏览器及其系统依赖: + +```sh +pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install --with-deps chromium webkit +``` + +在 Linux 上,`--with-deps` 会通过系统包管理器安装依赖。持久化 CI VM 必须通过镜像维护提供这些依赖,CI 只安装浏览器程序,遵循[故障切换手册](../../../.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md)的要求。 普通场景以没有已登记 Workspace 或 Session、但持久化标记记录默认 Workspace 已被删除的状态启动,使显式文件夹选择场景自行决定 cwd。`launchWebScaffold({ firstUse: true })` 保留初始化资格,供启动场景使用。 diff --git a/apps/web/tests/agent-team-panel.e2e.ts b/apps/web/tests/agent-team-panel.e2e.ts index 3e5ad5e0c2..cf3fe29662 100644 --- a/apps/web/tests/agent-team-panel.e2e.ts +++ b/apps/web/tests/agent-team-panel.e2e.ts @@ -1,11 +1,11 @@ // Keyless assembled-browser coverage for the opt-in Agent Teams bundle -// over the real Host Typert Remote flow. +// over the real Host projection frame flow. import { fileURLToPath } from 'node:url' import { join } from 'node:path' import { readFileSync } from 'node:fs' import type { Browser, Page } from 'playwright' import { chromium } from 'playwright' -import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' +import { afterAll, beforeAll, describe, expect, it, onTestFailed, onTestFinished, vi } from 'vitest' import * as yaml from 'js-yaml' import { entryListSchema } from '@deepseek-ai/cordis-plugin-include' import type {} from '@deepseek-ai/dsh-experimental-agent-team' @@ -80,26 +80,29 @@ describe('web e2e: Agent Teams panel', () => { await scaffold?.close() }) - it('displays agent-owned tasks through a read-only board', async () => { + it('displays agent-owned task changes through a read-only board without a refresh action', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-agent-team-panel')) + const projectionReads = vi.spyOn(scaffold.ctx.sessionController, 'projections') + onTestFinished(() => { projectionReads.mockRestore() }) await page.locator('[data-team-action]').getByRole('button', { name: /Agent Team/iu }).click() const action = page.getByRole('dialog', { name: 'Agent Team', exact: true }) await action.getByText('No shared tasks yet').waitFor() await action.getByText('lead').waitFor() + expect(projectionReads).not.toHaveBeenCalled() expect(await action.getByRole('button', { name: 'New task' }).count()).toBe(0) + expect(await action.getByRole('button', { name: /Refresh/u }).count()).toBe(0) + // Host-side Team mutations reach the open panel through projection frames only. const agent = scaffold.ctx.agents.list()[0]! const task = await scaffold.ctx.agentTeams.createTask(agent, { subject: 'Agent task', description: 'Created by the Team Lead', writeScopes: ['src/web'], }) - await action.getByRole('button', { name: 'Refresh Team' }).click() await action.getByText('Agent task', { exact: true }).waitFor() await action.getByText('Owner: Unowned', { exact: true }).waitFor() await scaffold.ctx.agentTeams.updateTask(agent, { taskId: task.id, expectedRevision: task.revision, action: 'claim', }) - await action.getByRole('button', { name: 'Refresh Team' }).click() await action.getByText('In progress', { exact: true }).waitFor() await action.getByText('Owner: lead', { exact: true }).waitFor() expect(await action.getByRole('button', { name: /^(New task|Edit|Complete|Reopen|Delete)$/u }).count()).toBe(0) @@ -107,6 +110,7 @@ describe('web e2e: Agent Teams panel', () => { const snapshot = await captureStableAria(page, '[data-team-panel]', scaffold.workspaceCwd) await compareOrRefreshGolden(PANEL_EXPECTED, snapshot, MODE) + expect(projectionReads).not.toHaveBeenCalled() expect(tripwire.pageErrors).toEqual([]) expect(tripwire.warnings).toEqual([]) await action.getByRole('button', { name: 'Close' }).click() @@ -141,9 +145,6 @@ describe('web e2e: Agent Teams panel', () => { await panel.getByRole('heading', { name: 'Shared tasks' }).waitFor() expect(await panel.evaluate(element => element === document.activeElement)).toBe(true) await page.keyboard.press('Tab') - expect(await panel.getByRole('button', { name: 'Refresh Team' }) - .evaluate(element => element === document.activeElement)).toBe(true) - await page.keyboard.press('Tab') expect(await panel.getByRole('button', { name: 'Close', exact: true }) .evaluate(element => element === document.activeElement)).toBe(true) await page.keyboard.press('Escape') @@ -152,7 +153,6 @@ describe('web e2e: Agent Teams panel', () => { await page.keyboard.press('Enter') await panel.waitFor() await page.keyboard.press('Tab') - await page.keyboard.press('Tab') await page.keyboard.press('Enter') await panel.waitFor({ state: 'detached' }) expect(await trigger.evaluate(element => element === document.activeElement)).toBe(true) diff --git a/apps/web/tests/declared-reasoning.e2e.ts b/apps/web/tests/declared-reasoning.e2e.ts index 897e192cab..6079e187bf 100644 --- a/apps/web/tests/declared-reasoning.e2e.ts +++ b/apps/web/tests/declared-reasoning.e2e.ts @@ -6,9 +6,9 @@ import { readFile } from 'node:fs/promises' import { fileURLToPath } from 'node:url' import { join } from 'node:path' -import type { Browser, Page } from 'playwright' -import { chromium } from 'playwright' -import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' +import type { Browser, Page, Request } from 'playwright' +import { chromium, webkit } from 'playwright' +import { afterAll, beforeAll, describe, expect, it, onTestFailed, onTestFinished } from 'vitest' import { assertFixtureInventory, captureStableAria, compareOrRefreshGolden, launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold, @@ -19,9 +19,13 @@ import { ZH_BROWSER_LOCALE, connectFreshWorkspaceZh, saveFailureShot } from './s const OVERLAY = fileURLToPath(new URL('./declared-reasoning.overlay.yml', import.meta.url)) const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/declared-reasoning', import.meta.url)) const UI_EXPECTED = fileURLToPath(new URL('./expected/declared-reasoning/ui.expected.md', import.meta.url)) +const POINTER_EXPECTED = fileURLToPath(new URL('./expected/declared-reasoning/pointer-menu.expected.md', import.meta.url)) const MODE = webSnapshotMode() -describe.skipIf(MODE === 'record')('web e2e: declared reasoning efforts reach the composer', () => { +describe.skipIf(MODE === 'record').each([ + { name: 'Chromium', engine: chromium }, + { name: 'WebKit', engine: webkit }, +])('web e2e: declared reasoning efforts reach the composer ($name)', ({ engine }) => { let scaffold: WebScaffold let browser: Browser let page: Page @@ -39,15 +43,17 @@ describe.skipIf(MODE === 'record')('web e2e: declared reasoning efforts reach th displayName: 'Acme Gateway', api: 'openai-completions', baseURL: 'https://gateway.acme.example/v1', - models: [{ - id: 'acme-think', - name: 'Acme Think', + models: [ + { id: 'acme-think', name: 'Acme Think' }, + { id: 'acme-swift', name: 'Acme Swift' }, + ].map(model => ({ + ...model, reasoningEfforts: { off: null, high: 'high', max: 'ultra' }, - }], + })), }, }, }) - browser = await chromium.launch() + browser = await engine.launch() page = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE }) tripwire = watchConsole(page) await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) @@ -56,12 +62,15 @@ describe.skipIf(MODE === 'record')('web e2e: declared reasoning efforts reach th }, 120_000) afterAll(async () => { - await browser?.close() - await scaffold?.close() + try { + await browser?.close() + } finally { + await scaffold?.close() + } }) it('offers exactly the declared levels and records the picked one', async () => { - onTestFailed(() => saveFailureShot(page, 'web-e2e-declared-reasoning')) + onTestFailed(() => saveFailureShot(page, `web-e2e-declared-reasoning-${engine.name()}`)) const trigger = page.getByRole('button', { name: /^选择模型/ }) await trigger.waitFor({ timeout: 15_000 }) await trigger.click() @@ -125,7 +134,117 @@ describe.skipIf(MODE === 'record')('web e2e: declared reasoning efforts reach th expect(tripwire.pageErrors).toEqual([]) }, 60_000) + it('opens from the pointer with keyboard focus and closes from the trigger in every pane', async () => { + onTestFailed(() => saveFailureShot(page, `web-e2e-model-trigger-${engine.name()}`)) + const trigger = page.getByRole('button', { name: /^选择模型/ }) + const menu = page.getByRole('menu') + for (const pane of ['root', 'model', 'effort']) { + await page.locator('[data-composer-input][contenteditable="true"]').focus() + await trigger.click() + await expect.poll(() => trigger.evaluate(element => element === document.activeElement)).toBe(true) + if (pane === 'root') { + await page.keyboard.press('ArrowDown') + await expect.poll(() => page.getByRole('menuitem', { name: /^模型/ }) + .evaluate(element => element === document.activeElement)).toBe(true) + } else { + await page.getByRole('menuitem', { name: pane === 'model' ? /^模型/ : /推理等级/ }).click() + await expect.poll(() => page.locator('[role="menuitemradio"][aria-checked="true"]') + .evaluate(element => element === document.activeElement)).toBe(true) + } + await trigger.click() + await menu.waitFor({ state: 'detached' }) + await expect.poll(() => trigger.evaluate(element => element === document.activeElement)).toBe(true) + } + expect(tripwire.pageErrors).toEqual([]) + }) + + it('selects model and effort by mouse and keeps keyboard control after cancelled or rejected clicks', async () => { + onTestFailed(() => saveFailureShot(page, `web-e2e-model-pointer-${engine.name()}`)) + let selections = 0 + const countSelection = (request: Request): void => { + if (new URL(request.url()).pathname.endsWith('/session/selectModel')) selections++ + } + page.on('request', countSelection) + onTestFinished(() => { page.off('request', countSelection) }) + const trigger = page.getByRole('button', { name: /^选择模型/ }) + const menu = page.getByRole('menu') + await trigger.click() + await page.getByRole('menuitem', { name: /^模型/ }).click() + const current = page.getByRole('menuitemradio', { name: 'Acme Think', exact: true }) + const target = page.getByRole('menuitemradio', { name: 'Acme Swift', exact: true }) + await expect.poll(() => current.evaluate(element => element === document.activeElement)).toBe(true) + + // Native mousedown must not blur the focused row and unmount the menu before click in WebKit. + await target.getByText('Acme Swift', { exact: true }).hover() + await page.mouse.down() + try { + await expect.poll(() => menu.count()).toBe(1) + await expect.poll(() => current.evaluate(element => element === document.activeElement)).toBe(true) + await page.getByText('Acme Gateway', { exact: true }).hover() + } finally { + await page.mouse.up() + } + // Later selection counts also include any request from this cancelled press. + expect(selections).toBe(0) + await page.keyboard.press('ArrowDown') + await expect.poll(() => target.evaluate(element => element === document.activeElement)).toBe(true) + await page.keyboard.press('Escape') + await page.keyboard.press('Shift+Tab') + await menu.waitFor({ state: 'detached' }) + + await trigger.click() + await page.getByRole('menuitem', { name: /^模型/ }).click() + await target.getByText('Acme Swift', { exact: true }).click() + await menu.waitFor({ state: 'detached' }) + expect(selections).toBe(1) + expect(scaffold.ctx.agentDefaultModel.currentSelection()?.model).toBe('acme-swift') + + await trigger.click() + await page.getByRole('menuitem', { name: /推理等级/ }).click() + await page.getByRole('menuitemradio', { name: 'Max', exact: true }).click() + await menu.waitFor({ state: 'detached' }) + expect(selections).toBe(2) + expect(scaffold.ctx.agentDefaultModel.currentSelection()?.reasoningEffort).toBe('max') + + await page.route('**/api/session/selectModel', async (route) => { + const envelope = route.request().postDataJSON() as { rpcId: string } + await route.fulfill({ + json: { + type: 'server-response', rpcId: envelope.rpcId, + result: { + ok: false, + error: { code: 'session/writer-held', message: 'writer held', details: { sessionId: 'held-session' } }, + }, + }, + }) + }, { times: 1 }) + await trigger.click() + await page.getByRole('menuitem', { name: /^模型/ }).click() + await expect.poll(() => target.evaluate(element => element === document.activeElement)).toBe(true) + await current.click() + await page.getByRole('alert').waitFor() + expect(selections).toBe(3) + await compareOrRefreshGolden(POINTER_EXPECTED, await captureStableAria(page, '[role="menu"]', scaffold.workspaceCwd), MODE) + await expect.poll(() => trigger.evaluate(element => element === document.activeElement)).toBe(true) + await page.keyboard.press('Tab') + await expect.poll(() => target.evaluate(element => element === document.activeElement)).toBe(true) + await page.keyboard.press('ArrowUp') + await expect.poll(() => current.evaluate(element => element === document.activeElement)).toBe(true) + await page.keyboard.press('Escape') + await page.keyboard.press('Escape') + await menu.waitFor({ state: 'detached' }) + + await trigger.click() + await page.getByRole('menuitem', { name: /^模型/ }).click() + await page.locator('[data-composer-input][contenteditable="true"]').focus() + await menu.waitFor({ state: 'detached' }) + await trigger.click() + await page.mouse.click(0, 0) + await menu.waitFor({ state: 'detached' }) + expect(tripwire.pageErrors).toEqual([]) + }) + it('keeps its snapshot inventory closed', async () => { - await assertFixtureInventory(SNAPSHOT_DIR, ['ui.expected.md']) + await assertFixtureInventory(SNAPSHOT_DIR, ['ui.expected.md', 'pointer-menu.expected.md']) }) }) diff --git a/apps/web/tests/expected/declared-reasoning/pointer-menu.expected.md b/apps/web/tests/expected/declared-reasoning/pointer-menu.expected.md new file mode 100644 index 0000000000..21a27082cd --- /dev/null +++ b/apps/web/tests/expected/declared-reasoning/pointer-menu.expected.md @@ -0,0 +1,9 @@ +- menu "模型与推理等级": + - group "DeepSeek": + - text: DeepSeek + - menuitemradio "DeepSeek-V4-Flash" + - menuitemradio "DeepSeek-V4-Flash-Vision-Exp" + - group "Acme Gateway": + - text: Acme Gateway + - menuitemradio "Acme Think" + - menuitemradio "Acme Swift" [checked] diff --git a/apps/web/tests/expected/plugin-install-github/failed.expected.md b/apps/web/tests/expected/plugin-install-github/failed.expected.md new file mode 100644 index 0000000000..74395524dc --- /dev/null +++ b/apps/web/tests/expected/plugin-install-github/failed.expected.md @@ -0,0 +1,6 @@ +- dialog "无法访问 GitHub": + - heading "无法访问 GitHub" [level=2] + - button "关闭" + - paragraph: 请尝试其他安装来源。 + - button "取消" + - button "改用国内镜像" diff --git a/apps/web/tests/expected/plugin-install-github/mirror.expected.md b/apps/web/tests/expected/plugin-install-github/mirror.expected.md new file mode 100644 index 0000000000..fbdc9a5116 --- /dev/null +++ b/apps/web/tests/expected/plugin-install-github/mirror.expected.md @@ -0,0 +1,8 @@ +- dialog "添加插件": + - heading "添加插件" [level=2] + - button "关闭" + - textbox "插件包名": + - /placeholder: 例如 @deepseek-ai/dsh-experimental-auto-review + - button "插件安装引导和示例" + - button "安装源 中国大陆镜像源" + - button "安装" [disabled] diff --git a/apps/web/tests/expected/plugin-install-github/timeout.expected.md b/apps/web/tests/expected/plugin-install-github/timeout.expected.md new file mode 100644 index 0000000000..0d76ddbe98 --- /dev/null +++ b/apps/web/tests/expected/plugin-install-github/timeout.expected.md @@ -0,0 +1,6 @@ +- dialog "连接 GitHub 超时": + - heading "连接 GitHub 超时" [level=2] + - button "关闭" + - paragraph: 请尝试其他安装来源。 + - button "取消" + - button "改用国内镜像" diff --git a/apps/web/tests/expected/voice-download-fallback.expected.md b/apps/web/tests/expected/voice-download-fallback.expected.md new file mode 100644 index 0000000000..898f471215 --- /dev/null +++ b/apps/web/tests/expected/voice-download-fallback.expected.md @@ -0,0 +1,4 @@ +- alert: + - paragraph: "Cannot download model.int8.onnx: the download service returned HTTP 502." + - paragraph: Confirm that the download address is available and the proxy can reach the download service, or retry later. + - text: "Download source: https://fallback-model-source.example" diff --git a/apps/web/tests/expected/voice-source-auto.expected.md b/apps/web/tests/expected/voice-source-auto.expected.md new file mode 100644 index 0000000000..a64c41e080 --- /dev/null +++ b/apps/web/tests/expected/voice-source-auto.expected.md @@ -0,0 +1,19 @@ +- strong: SenseVoiceSmall (INT8) +- paragraph: Local models will be downloaded to the machine running DSH. No Python or compiler is required. +- term: Disk space +- definition: Allow about 1 GB for models, runtime, and download caches +- term: Memory +- definition: About 1 GB with the model loaded; recognition may use more +- term: First setup +- definition: Allow roughly 1–10 minutes, depending on the network and machine +- text: These are estimates; slow networks may take longer. Downloads are reused; idle models release memory by default. +- button "First use downloads the local recognition models.": + - text: First use downloads the local recognition models. + - status +- text: Model download source +- combobox "Model download source": + - option "Automatic (recommended)" [selected] + - option "Hugging Face" + - option "HF-Mirror (China mirror)" +- text: Prefer the first responding source and try another if a download fails. +- button "Download and prepare" diff --git a/apps/web/tests/expected/voice-source-manual.expected.md b/apps/web/tests/expected/voice-source-manual.expected.md new file mode 100644 index 0000000000..1867a5bb80 --- /dev/null +++ b/apps/web/tests/expected/voice-source-manual.expected.md @@ -0,0 +1,19 @@ +- strong: SenseVoiceSmall (INT8) +- paragraph: Local models will be downloaded to the machine running DSH. No Python or compiler is required. +- term: Disk space +- definition: Allow about 1 GB for models, runtime, and download caches +- term: Memory +- definition: About 1 GB with the model loaded; recognition may use more +- term: First setup +- definition: Allow roughly 1–10 minutes, depending on the network and machine +- text: These are estimates; slow networks may take longer. Downloads are reused; idle models release memory by default. +- button "First use downloads the local recognition models.": + - text: First use downloads the local recognition models. + - status +- text: Model download source +- combobox "Model download source": + - option "Automatic (recommended)" + - option "Hugging Face" + - option "HF-Mirror (China mirror)" [selected] +- text: Download only from the selected source. Verified downloaded files are reused. +- button "Download and prepare" diff --git a/apps/web/tests/goal-bar.e2e.ts b/apps/web/tests/goal-bar.e2e.ts index 787b69e959..a93c9d7c13 100644 --- a/apps/web/tests/goal-bar.e2e.ts +++ b/apps/web/tests/goal-bar.e2e.ts @@ -13,7 +13,7 @@ import { assertFixtureInventory, captureStableAria, compareOrRefreshGolden, launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold, } from './scaffold.ts' -import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts' +import { connectFreshWorkspace, expectTooltipOnTop, newEnglishPage, saveFailureShot } from './support.ts' const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/goal-bar', import.meta.url)) const ACTIVE_EXPECTED = join(SNAPSHOT_DIR, 'active.expected.md') @@ -58,6 +58,9 @@ describe('web e2e: goal bar clear convergence', () => { await pause.hover() const pauseTooltip = page.getByRole('tooltip', { name: 'Pause goal', exact: true }) await pauseTooltip.waitFor() + // The strip rides directly above the input card, which paints later; the + // bubble must escape the strip's stacking context instead of landing under it. + await expectTooltipOnTop(pauseTooltip) const tooltipGeometry = await page.evaluate(() => { const element = document.querySelector('[role="tooltip"]') if (element === null) return null diff --git a/apps/web/tests/models-settings.e2e.ts b/apps/web/tests/models-settings.e2e.ts index 3d9fd95906..68857956f3 100644 --- a/apps/web/tests/models-settings.e2e.ts +++ b/apps/web/tests/models-settings.e2e.ts @@ -397,7 +397,7 @@ describe('web e2e: Models settings page configures a dormant provider', () => { const picker = page.getByRole('dialog', { name: '选择要添加的模型' }) await picker.getByRole('button', { name: '取消全选' }).click() await picker.getByRole('searchbox', { name: '搜索模型' }).fill('gpt-6-astra') - await picker.getByRole('checkbox', { name: 'GPT-6 Astra', exact: true }).check() + await picker.getByRole('checkbox', { name: 'gpt-6-astra', exact: true }).check() await picker.getByRole('button', { name: '添加所选' }).click() await dialog.getByRole('button', { name: '模型选项 1' }).click() expect(await image.isChecked()).toBe(true) diff --git a/apps/web/tests/plugin-install-github.e2e.ts b/apps/web/tests/plugin-install-github.e2e.ts new file mode 100644 index 0000000000..1926b6f4e2 --- /dev/null +++ b/apps/web/tests/plugin-install-github.e2e.ts @@ -0,0 +1,105 @@ +// Real Git reaches a controlled proxy failure or stall; the browser +// offers a mirror for a replacement spec without retrying the failed address. +import { once } from 'node:events' +import { mkdtemp, readFile, writeFile, rm } from 'node:fs/promises' +import { createServer, type Socket } from 'node:net' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { chromium } from 'playwright' +import { expect, it, onTestFinished } from 'vitest' +import { launchWebScaffold, captureStableAria, compareOrRefreshGolden, webSnapshotMode, watchConsole } from './scaffold.ts' +import { ZH_BROWSER_LOCALE } from './support.ts' + +it.each(['network', 'timeout'] as const)('offers a mirror after a GitHub %s and waits for replacement input', async (failure) => { + const scratch = await mkdtemp(join(tmpdir(), 'dsh-install-github-')) + onTestFinished(() => rm(scratch, { recursive: true, force: true })) + const sockets = new Set() + let connections = 0 + const proxy = createServer((socket) => { + sockets.add(socket) + socket.on('error', () => {}) + socket.once('close', () => { sockets.delete(socket) }) + socket.once('data', () => { connections++; if (failure === 'network') socket.destroy() }) + }) + onTestFinished(async () => { + for (const socket of sockets) socket.destroy() + await new Promise((resolve, reject) => { proxy.close((error) => { if (error) reject(error); else resolve() }) }) + }) + proxy.listen(0, '127.0.0.1') + await once(proxy, 'listening') + const address = proxy.address() + if (address === null || typeof address === 'string') throw new Error('proxy did not bind a TCP port') + const gitConfig = join(scratch, 'git.config') + await writeFile(gitConfig, `[http "https://github.com/"]\n proxy = http://127.0.0.1:${String(address.port)}\n`) + const overlay = join(scratch, 'cordis.patch.yml') + await writeFile(overlay, '- id: ui-plugin-manager\n config: { registryProbeEnabled: false }\n') + const scaffold = await launchWebScaffold({ + profile: { packages: [], packageManager: { command: process.execPath, args: [], env: { GIT_CONFIG_GLOBAL: gitConfig, GIT_CONFIG_NOSYSTEM: '1' } } }, + extraOverlayPath: overlay, + }) + onTestFinished(() => scaffold.close()) + const browser = await chromium.launch() + onTestFinished(() => browser.close()) + const profile = join(scaffold.harnessHome, 'profiles', 'scaffold') + const manifestPath = join(profile, 'package.json') + const manifestBefore = await readFile(manifestPath, 'utf8') + await writeFile(join(profile, 'config'), 'console.log("https://registry.npmjs.org/")\n') + await writeFile(join(profile, '.attempts'), '') + await writeFile(join(profile, 'add'), ` + const fs = require('node:fs'); + fs.appendFileSync('.attempts', JSON.stringify(process.argv) + '\\n'); + console.error('pnpm must not start after the GitHub connection check fails'); + process.exitCode = 1; + `) + const page = await browser.newPage({ viewport: { width: 1280, height: 800 }, locale: ZH_BROWSER_LOCALE }) + const tripwire = watchConsole(page) + await page.goto(scaffold.authenticatedUrl) + await page.waitForSelector('[class*="frame"]') + if (await page.getByRole('dialog', { name: '设置' }).count() > 0) await page.keyboard.press('Escape') + await page.getByRole('navigation', { name: '全局面板' }).getByRole('button', { name: '插件', exact: true }).click() + await page.getByRole('button', { name: '添加插件', exact: true }).click() + const spec = 'https://github.com/example/dsh-plugin.git' + const title = failure === 'timeout' ? '连接 GitHub 超时' : '无法访问 GitHub' + let dialog = page.getByRole('dialog', { name: '添加插件', exact: true }) + await dialog.getByRole('button', { name: '安装源 默认安装源', exact: true }).waitFor() + await dialog.getByRole('textbox', { name: '包名或地址' }).fill(spec) + expect(await page.getByText(title, { exact: true }).count()).toBe(0) + await dialog.getByRole('button', { name: '安装', exact: true }).click() + dialog = page.getByRole('dialog', { name: title, exact: true }) + await dialog.waitFor() + expect(await page.getByRole('dialog').count()).toBe(1) + expect(await dialog.getByText('请尝试其他安装来源。', { exact: true }).count()).toBe(1) + await compareOrRefreshGolden( + fileURLToPath(new URL(`./expected/plugin-install-github/${failure === 'timeout' ? 'timeout' : 'failed'}.expected.md`, import.meta.url)), + await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd), webSnapshotMode(), + ) + const attempts = await readFile(join(profile, '.attempts'), 'utf8') + expect(attempts).toBe('') + expect(connections).toBeGreaterThan(0) + await expect.poll(() => sockets.size).toBe(0) + const checked = connections + await dialog.getByRole('button', { name: '改用国内镜像', exact: true }).click() + dialog = page.getByRole('dialog', { name: '添加插件', exact: true }) + const input = dialog.getByRole('textbox', { name: '插件包名', exact: true }) + await input.waitFor() + expect(await input.inputValue()).toBe('') + expect(await input.evaluate(element => element === document.activeElement)).toBe(true) + expect(await dialog.getByRole('button', { name: '安装', exact: true }).isDisabled()).toBe(true) + await dialog.getByRole('button', { name: '安装源 中国大陆镜像源', exact: true }).waitFor() + await compareOrRefreshGolden( + fileURLToPath(new URL('./expected/plugin-install-github/mirror.expected.md', import.meta.url)), + await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd), webSnapshotMode(), + ) + expect(await readFile(join(profile, '.attempts'), 'utf8')).toBe(attempts) + expect(connections).toBe(checked) + expect(await readFile(manifestPath, 'utf8')).toBe(manifestBefore) + await input.fill(spec) + await dialog.getByRole('button', { name: '安装', exact: true }).click() + dialog = page.getByRole('dialog', { name: title, exact: true }) + await dialog.getByRole('button', { name: '取消', exact: true }).click() + await dialog.waitFor({ state: 'detached' }) + expect(await readFile(join(profile, '.attempts'), 'utf8')).toBe('') + expect(connections).toBeGreaterThan(checked) + expect(tripwire.pageErrors).toEqual([]) +}) diff --git a/apps/web/tests/plugin-manager.e2e.ts b/apps/web/tests/plugin-manager.e2e.ts index 19a9c96e4d..05ff6010ab 100644 --- a/apps/web/tests/plugin-manager.e2e.ts +++ b/apps/web/tests/plugin-manager.e2e.ts @@ -280,6 +280,9 @@ describe('web e2e: plugin manager', () => { await action.waitFor({ timeout: 20_000 }) await action.getByRole('button', { name: /Agent Team/iu }).click() const teamPanel = teamPage.getByRole('dialog', { name: 'Agent Team', exact: true }) + await teamPanel.getByText('Team 暂不可用', { exact: true }).waitFor() + await teamPage.reload({ waitUntil: 'load' }) + await action.getByRole('button', { name: /Agent Team/iu }).click() await teamPanel.getByText('还没有共享任务').waitFor() await teamPanel.getByText('lead', { exact: true }).waitFor() const manifest = JSON.parse(await homeFile('profiles', 'scaffold', 'package.json')) as { diff --git a/apps/web/tests/queue-actions.e2e.ts b/apps/web/tests/queue-actions.e2e.ts index c5317cb3fb..9892033837 100644 --- a/apps/web/tests/queue-actions.e2e.ts +++ b/apps/web/tests/queue-actions.e2e.ts @@ -16,7 +16,7 @@ import { assertFixtureInventory, captureExpandedTurnProcessAria, captureStableAria, compareOrRefreshGolden, launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold, } from './scaffold.ts' -import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts' +import { connectFreshWorkspace, expectTooltipOnTop, newEnglishPage, saveFailureShot } from './support.ts' const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/queue-actions', import.meta.url)) const FIXTURE = fileURLToPath(new URL('../../../snapshots/web/live-interactions/session.v3.jsonl', import.meta.url)) @@ -196,6 +196,9 @@ describe('web e2e: queue row actions', () => { await save.hover() const saveTooltip = page.getByRole('tooltip', { name: 'Save queued message', exact: true }) await saveTooltip.waitFor() + // The dock tucks under the input card, which paints later; the bubble must + // escape the panel's stacking context instead of landing behind it. + await expectTooltipOnTop(saveTooltip) const tooltipGeometry = await page.evaluate(() => { const element = document.querySelector('[role="tooltip"]') if (element === null) return null @@ -222,7 +225,9 @@ describe('web e2e: queue row actions', () => { const remainingEdit = page.getByRole('button', { name: 'Edit queued message', exact: true }) await expect.poll(() => remainingEdit.isEnabled(), { timeout: 10_000 }).toBe(true) await remainingEdit.hover() - await page.getByRole('tooltip', { name: 'Edit queued message', exact: true }).waitFor() + const editTooltip = page.getByRole('tooltip', { name: 'Edit queued message', exact: true }) + await editTooltip.waitFor() + await expectTooltipOnTop(editTooltip) const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd) await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE) diff --git a/apps/web/tests/scaffold.ts b/apps/web/tests/scaffold.ts index 6e18a9c3ed..9fde2ea346 100644 --- a/apps/web/tests/scaffold.ts +++ b/apps/web/tests/scaffold.ts @@ -334,6 +334,8 @@ export interface LaunchOptions { */ profile?: { hmr?: boolean + /** Launcher-owned invocation and environment for profile package operations. */ + packageManager?: ProfileContext['packageManager'] packages: { dir: string; enabled?: boolean }[] /** Additional selected names, including bundles unavailable after an upgrade. */ bundles?: readonly string[] @@ -772,6 +774,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise layer.packageName), overlays: processOverlays, telemetryDisabledEnv: undefined, diff --git a/apps/web/tests/sidebar-terminal.e2e.ts b/apps/web/tests/sidebar-terminal.e2e.ts index aac87b9f54..75ee794ebe 100644 --- a/apps/web/tests/sidebar-terminal.e2e.ts +++ b/apps/web/tests/sidebar-terminal.e2e.ts @@ -475,11 +475,16 @@ describe.skipIf(process.platform === 'win32')('Web sidebar terminal', () => { await page.context().setOffline(false) await page.reload({ waitUntil: 'load' }) await page.locator('[data-sidebar-right-expand]').click() - await expect.poll(() => page.getByRole('alert').innerText()).toContain('no longer exists') + const terminal = page.locator('[data-sidebar-terminal]') + let unavailableSnapshot = '' + // Mounting can refresh a failed recovered view; readiness and comparison use the same DOM sample. + await expect.poll(async () => { + unavailableSnapshot = await terminal.ariaSnapshot() + return unavailableSnapshot + }).toContain('no longer exists') expect(handles).toHaveLength(1) const unavailable = fileURLToPath(new URL('./expected/sidebar-terminal/unavailable.expected.md', import.meta.url)) - const terminal = page.locator('[data-sidebar-terminal]') - await compareOrRefreshGolden(unavailable, await terminal.ariaSnapshot(), webSnapshotMode()) + await compareOrRefreshGolden(unavailable, unavailableSnapshot, webSnapshotMode()) await terminal.screenshot({ path: `${shots}/unavailable.png`, animations: 'disabled' }) const tabCount = await page.locator('[data-dockkit-tab]').count() const create = terminal.getByRole('button', { name: 'New terminal', exact: true }) diff --git a/apps/web/tests/snapshots/agent-team-panel/task.expected.md b/apps/web/tests/snapshots/agent-team-panel/task.expected.md index 5184f24d29..d0a286ce9a 100644 --- a/apps/web/tests/snapshots/agent-team-panel/task.expected.md +++ b/apps/web/tests/snapshots/agent-team-panel/task.expected.md @@ -1,9 +1,8 @@ - dialog "Agent Team": - strong: Agent Team - - button "Refresh Team" - button "Close" - heading "Members" [level=3] - - 'button "lead Inactive · Model: deepseek-v4-flash" [disabled]' + - button "lead Inactive" [disabled] - heading "Shared tasks" [level=3] - article: - strong: Agent task diff --git a/apps/web/tests/support.ts b/apps/web/tests/support.ts index 9faa3d8f76..884f160418 100644 --- a/apps/web/tests/support.ts +++ b/apps/web/tests/support.ts @@ -225,6 +225,30 @@ export async function saveFailureShot(page: Page, name: string): Promise { } } +/** + * Assert a visible tooltip paints above the element a user would read through + * it, at the bubble's center and bottom edge. The bubble ignores pointer events + * by design, so the measurement enables them for its own duration; each probe + * reports the bubble or the covering element, so a failure names its cover. + * @param tooltip - locator for the visible `[role="tooltip"]` bubble. + */ +export async function expectTooltipOnTop(tooltip: Locator): Promise { + const probes = await tooltip.evaluate((bubble) => { + const rect = bubble.getBoundingClientRect() + const previous = bubble.style.pointerEvents + bubble.style.pointerEvents = 'auto' + const probe = (y: number): string => { + const hit = document.elementFromPoint(rect.left + rect.width / 2, y) + if (hit === null) return 'none' + return bubble.contains(hit) ? 'tooltip' : `${hit.tagName}.${hit.classList.value}`.slice(0, 120) + } + const probes = { center: probe(rect.top + rect.height / 2), bottom: probe(rect.bottom - 1) } + bubble.style.pointerEvents = previous + return probes + }) + expect(probes).toEqual({ center: 'tooltip', bottom: 'tooltip' }) +} + /** * The conversation engine's Context key format, restated here rather than * imported: these specs live in the Host compiler aggregate, which must not diff --git a/apps/web/tests/voice-download.e2e.ts b/apps/web/tests/voice-download.e2e.ts index 32161bceb3..0acd3489bc 100644 --- a/apps/web/tests/voice-download.e2e.ts +++ b/apps/web/tests/voice-download.e2e.ts @@ -1,7 +1,7 @@ /** The real local provider publishes actionable download failures through the speech Remote to bundle details. */ import { once } from 'node:events' import { mkdtemp, rm, writeFile } from 'node:fs/promises' -import { createServer } from 'node:http' +import { createServer, type ServerResponse } from 'node:http' import { tmpdir } from 'node:os' import { join } from 'node:path' import { fileURLToPath } from 'node:url' @@ -45,7 +45,7 @@ it('shows the failed asset, actual download source and recovery advice, then ret await page.getByRole('button', { name: 'Plugins', exact: true }).click() await page.locator('[data-plugin-package="@deepseek-ai/dsh-experimental-voice-input-bundle"]').getByRole('button').click() await page.getByRole('button', { name: 'Download and prepare', exact: true }).click() - const alert = page.getByRole('alert').filter({ hasText: 'Cannot download model.int8.onnx' }) + const alert = page.getByRole('alert') await alert.waitFor() expect(await alert.textContent()).toContain('HTTP 503') expect(await alert.textContent()).toContain(`Download source: ${origin}`) @@ -59,3 +59,66 @@ it('shows the failed asset, actual download source and recovery advice, then ret expect(requests).toHaveLength(2) expect(tripwire.pageErrors).toEqual([]) }) + +it.each([{ winner: 0, manual: false }, { winner: 1, manual: false }, { winner: 0, manual: true }, { winner: 1, manual: true }])( + 'uses source $winner with manual selection $manual through the real preparation UI', async ({ winner, manual }) => { + const root = await mkdtemp(join(tmpdir(), 'dsh-voice-mirrors-')) + onTestFinished(async () => { await rm(root, { recursive: true, force: true }) }) + const resources: { scaffold?: WebScaffold; browser?: Browser } = {} + const probes = new Map(), downloads: number[] = [], requestedPaths: string[] = [] + const servers = [0, 1].map(index => createServer((request, response) => { + requestedPaths.push(request.url!) + if (request.method === 'HEAD') { + probes.set(index, response) + // Release the winner only after both real Host requests overlap. + if (probes.size === 2) { const selected = probes.get(winner)!; selected.writeHead(200); selected.end() } + } else { + downloads.push(index) + response.writeHead(index === winner ? 503 : 502); response.end() + } + })) + onTestFinished(async () => { + try { await resources.browser?.close() } finally { + try { await resources.scaffold?.close() } finally { + await Promise.all(servers.map(async (server) => { + const closed = new Promise((resolve, reject) => { server.close((error) => { if (error) reject(error); else resolve() }) }) + server.closeAllConnections(); await closed + })) + } + } + }) + const origins = await Promise.all(servers.map(async (server) => { + server.listen(0, '127.0.0.1'); await once(server, 'listening') + const address = server.address() + if (!address || typeof address === 'string') throw new Error('Expected a loopback listener') + return `http://127.0.0.1:${address.port}` + })) + const overlay = join(root, 'voice.patch.yml') + await writeFile(overlay, `- id: speech-to-text-sensevoice\n config: ${JSON.stringify({ + dataRoot: join(root, 'models'), modelOrigins: origins, modelProbeTimeoutMs: 10_000, + })}\n`) + const scaffold = await launchWebScaffold({ profile: { packages: [{ dir: bundle, enabled: true }] }, extraOverlayPath: overlay }) + resources.scaffold = scaffold + const browser = await chromium.launch(); resources.browser = browser + const page = await newEnglishPage(browser), tripwire = watchConsole(page) + await page.goto(scaffold.authenticatedUrl) + await page.getByRole('button', { name: 'Plugins', exact: true }).click() + await page.locator('[data-plugin-package="@deepseek-ai/dsh-experimental-voice-input-bundle"]').getByRole('button').click() + const picker = page.getByLabel('Model download source', { exact: true }) + await picker.waitFor() + if (manual) await picker.selectOption(origins[winner]!) + await page.getByRole('button', { name: 'Download and prepare', exact: true }).click() + const alert = page.getByRole('alert') + await alert.waitFor() + expect(downloads).toEqual(manual ? [winner] : [winner, 1 - winner]) + expect(requestedPaths).toHaveLength(manual ? 1 : 4) + expect(requestedPaths.every(path => path === '/csukuangfj/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17/resolve/2365baeacb507f821a0c8120fcee3d484dba7a07/model.int8.onnx')).toBe(true) + const failedOrigin = origins[manual ? winner : 1 - winner]! + expect(await picker.inputValue()).toBe(manual ? origins[winner] : '') + expect(await alert.textContent()).toContain(manual ? 'HTTP 503' : 'HTTP 502') + expect(await alert.textContent()).toContain(`Download source: ${failedOrigin}`) + await compareOrRefreshGolden(manual ? expected : fileURLToPath(new URL('./expected/voice-download-fallback.expected.md', import.meta.url)), + (await captureStableAria(page, '[role="alert"]', scaffold.workspaceCwd)).replaceAll(failedOrigin, manual ? 'https://model-mirror.example' : 'https://fallback-model-source.example'), + webSnapshotMode()) + expect(tripwire.pageErrors).toEqual([]) + }) diff --git a/apps/web/tests/voice-setup.e2e.ts b/apps/web/tests/voice-setup.e2e.ts index 93eb86b5c5..0d594f59e0 100644 --- a/apps/web/tests/voice-setup.e2e.ts +++ b/apps/web/tests/voice-setup.e2e.ts @@ -40,5 +40,14 @@ it('guides a newly enabled voice plugin to installation and lets the user postpo await page.getByRole('button', { name: 'Download and prepare', exact: true }).waitFor() expect(await dialog.count()).toBe(0) expect(await page.getByText('Local models will be downloaded to the machine running DSH. No Python or compiler is required.', { exact: true }).count()).toBe(1) + expect(await page.getByText('Audio is recognized on the machine running DSH. If models need downloading, that machine must be able to reach the selected source and its file services. Configure a proxy on that machine if needed.', { exact: true }).count()).toBe(1) + const source = page.getByLabel('Model download source', { exact: true }) + expect(await source.inputValue()).toBe('') + expect(await source.getByRole('option').allTextContents()).toEqual(['Automatic (recommended)', 'Hugging Face', 'HF-Mirror (China mirror)']) + await compareOrRefreshGolden(fileURLToPath(new URL('./expected/voice-source-auto.expected.md', import.meta.url)), + await captureStableAria(page, '[data-speech-provider]', scaffold.workspaceCwd), webSnapshotMode()) + await source.selectOption('https://hf-mirror.com') + await compareOrRefreshGolden(fileURLToPath(new URL('./expected/voice-source-manual.expected.md', import.meta.url)), + await captureStableAria(page, '[data-speech-provider]', scaffold.workspaceCwd), webSnapshotMode()) expect(tripwire.pageErrors).toEqual([]) }) diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index bf3f8a08e8..1db54896f7 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -63,6 +63,7 @@ "tests/plugin-install-cancel.e2e.ts", "tests/plugin-install-approve.e2e.ts", "tests/plugin-install-registry.e2e.ts", + "tests/plugin-install-github.e2e.ts", "tests/client-plugin-live.e2e.ts", "tests/settings-chrome.e2e.ts", "tests/desktop-locale.e2e.ts", diff --git a/docs/capability-seams.i18n.yaml b/docs/capability-seams.i18n.yaml index ea2247432b..13405e808b 100644 --- a/docs/capability-seams.i18n.yaml +++ b/docs/capability-seams.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/capability-seams.md -capability-seams.md: 6550250f0cc89ea5ca1c67c07894bc04385f93f6 -capability-seams.zh.md: f71f021a32f07eec129e892f8d65445ad7b8d264 +capability-seams.md: 2914231ceaaa243b0c918a9477e554aa1641ad91 +capability-seams.zh.md: a1223a351c26d668705df327102ac5e7e521f9ec diff --git a/docs/capability-seams.md b/docs/capability-seams.md index 6550250f0c..2914231cea 100644 --- a/docs/capability-seams.md +++ b/docs/capability-seams.md @@ -232,7 +232,6 @@ flowchart LR pkg_experimental_agent_team["experimental-agent-team"] svc_agentTeams["ctx.agentTeams
Agent Teams coordination domain"] pkg_experimental_tool_agent_team["experimental-tool-agent-team"] - pkg_experimental_client_ui_agent_team["experimental-client-ui-agent-team"] pkg_inspector["inspector"] svc_inspector["ctx.inspector
Cross-realm runtime inspection"] pkg_jobs["jobs"] @@ -419,7 +418,6 @@ flowchart LR svc_agentDefaultModel --> pkg_headless svc_agentLoop --> pkg_base svc_agentLoop --> pkg_sdk_minimal - svc_agentTeams --> pkg_experimental_client_ui_agent_team svc_agentTeams --> pkg_experimental_tool_agent_team svc_agents --> pkg_acp svc_agents --> pkg_agent_loop @@ -643,7 +641,7 @@ flowchart LR | `ctx.compaction` | `seam` | [`compaction`](../packages/compaction/compaction) | [`compaction-basic`](../packages/compaction/compaction-basic) | [`compaction-basic`](../packages/compaction/compaction-basic) | - | The basic backend consumes post-step pressure and request-error recovery events; there is no model-facing compact tool. | | `ctx.subagents` | `seam` | [`subagent`](../packages/subagent/subagent) | [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process), [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-claude-code`](../packages/subagent/subagent-claude-code), [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-subagent-control`](../packages/subagent/tool-subagent-control), [`tool-ralph`](../packages/workflow/tool-ralph) | - | Providers implement transports; the service also owns optional Activation-based continuation orchestration, tool-subagent selects one-shot or continuable delegation, tool-subagent-control delivers follow-ups, and tool-ralph requires one fresh structured-output route. | | `ctx.speechToText` | `seam` | [`experimental-speech-to-text`](../packages/experimental/speech-to-text) | [`experimental-speech-to-text-sensevoice`](../packages/experimental/speech-to-text-sensevoice) | [`experimental-api-speech-to-text`](../packages/experimental/api-speech-to-text) | - | Routes explicit recognizers; the browser uses the authenticated Remote and keeps transcripts in the draft until submission. | -| `ctx.agentTeams` | `core` | [`experimental-agent-team`](../packages/experimental/agent-team) | - | [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team), [`experimental-client-ui-agent-team`](../packages/experimental/client-ui-agent-team) | - | Owns the implicit-root roster, durable peer mailbox, shared task DAG, continuable-child lifecycle, and generated Team Remote methods; tool-agent-team contributes model controls and client-ui-agent-team mounts the browser contribution. | +| `ctx.agentTeams` | `core` | [`experimental-agent-team`](../packages/experimental/agent-team) | - | [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team) | - | Owns the implicit-root roster, durable peer mailbox, shared task DAG, and continuable-child lifecycle; tool-agent-team contributes model controls. | | `ctx.inspector` | `core` | `inspector` | - | - | - | Owns the Worker-hosted CDP target and the transport-independent Host and Client observation and Cordis-tree query API. | | `ctx.jobs` | `seam` | [`jobs`](../packages/jobs/jobs) | [`jobs-local`](../packages/jobs/jobs-local) | [`tool-bash`](../packages/shell/tool-bash), [`tool-pwsh`](../packages/shell/tool-pwsh), [`tool-terminal`](../packages/terminal/tool-terminal), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`api-job-controller`](../packages/api/job-controller) | - | Producers (background bash/pwsh, PTY sends, and subagent delegations) register running work; record-declaring jobs additionally stream raw output for non-consuming observers; tool-jobs is the model-facing controller that reads, lists, and kills it; jobs-local is the process-local registry. | | `ctx.web` | `seam` | [`web`](../packages/web/web) | [`web-search-exa`](../packages/web/web-search-exa), [`web-search-perplexity`](../packages/web/web-search-perplexity), [`web-search-deepseek`](../packages/web/web-search-deepseek), [`web-fetch-http`](../packages/web/web-fetch-http) | [`tool-web`](../packages/web/tool-web) | - | Search and fetch providers register into one ctx.web seam; tool-web owns the stable model-facing names. | diff --git a/docs/capability-seams.zh.md b/docs/capability-seams.zh.md index f71f021a32..a1223a351c 100644 --- a/docs/capability-seams.zh.md +++ b/docs/capability-seams.zh.md @@ -234,7 +234,6 @@ flowchart LR pkg_experimental_agent_team["experimental-agent-team"] svc_agentTeams["ctx.agentTeams
Agent Teams coordination domain"] pkg_experimental_tool_agent_team["experimental-tool-agent-team"] - pkg_experimental_client_ui_agent_team["experimental-client-ui-agent-team"] pkg_inspector["inspector"] svc_inspector["ctx.inspector
Cross-realm runtime inspection"] pkg_jobs["jobs"] @@ -421,7 +420,6 @@ flowchart LR svc_agentDefaultModel --> pkg_headless svc_agentLoop --> pkg_base svc_agentLoop --> pkg_sdk_minimal - svc_agentTeams --> pkg_experimental_client_ui_agent_team svc_agentTeams --> pkg_experimental_tool_agent_team svc_agents --> pkg_acp svc_agents --> pkg_agent_loop @@ -645,7 +643,7 @@ flowchart LR | `ctx.compaction` | `seam` | [`compaction`](../packages/compaction/compaction) | [`compaction-basic`](../packages/compaction/compaction-basic) | [`compaction-basic`](../packages/compaction/compaction-basic) | - | 基础后端消费步骤后的压力事件和请求错误恢复事件;不存在面向模型的压缩工具。 | | `ctx.subagents` | `seam` | [`subagent`](../packages/subagent/subagent) | [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process), [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-claude-code`](../packages/subagent/subagent-claude-code), [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-subagent-control`](../packages/subagent/tool-subagent-control), [`tool-ralph`](../packages/workflow/tool-ralph) | - | 提供方实现传输;该服务还负责可选的、基于 Activation 的延续编排,tool-subagent 选择一次性或可延续委派,tool-subagent-control 传递后续消息,而 tool-ralph 要求一条全新的结构化输出路由。 | | `ctx.speechToText` | `seam` | [`experimental-speech-to-text`](../packages/experimental/speech-to-text) | [`experimental-speech-to-text-sensevoice`](../packages/experimental/speech-to-text-sensevoice) | [`experimental-api-speech-to-text`](../packages/experimental/api-speech-to-text) | - | 路由显式选择的识别器;浏览器使用带认证的 Remote,并在提交前将转写保留在草稿中。 | -| `ctx.agentTeams` | `core` | [`experimental-agent-team`](../packages/experimental/agent-team) | - | [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team), [`experimental-client-ui-agent-team`](../packages/experimental/client-ui-agent-team) | - | 负责隐式 Root roster、持久 peer mailbox、共享任务 DAG、continuable child 生命周期与生成式 Team Remote method;tool-agent-team 提供模型控制工具,client-ui-agent-team 挂载浏览器 contribution。 | +| `ctx.agentTeams` | `core` | [`experimental-agent-team`](../packages/experimental/agent-team) | - | [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team) | - | 负责隐式 Root roster、持久 peer mailbox、共享任务 DAG 与 continuable child 生命周期;tool-agent-team 提供模型控制工具。 | | `ctx.inspector` | `core` | `inspector` | - | - | - | 负责 Worker 托管的 CDP target,以及独立于传输的 Host 和 Client observation 与 Cordis tree query API。 | | `ctx.jobs` | `seam` | [`jobs`](../packages/jobs/jobs) | [`jobs-local`](../packages/jobs/jobs-local) | [`tool-bash`](../packages/shell/tool-bash), [`tool-pwsh`](../packages/shell/tool-pwsh), [`tool-terminal`](../packages/terminal/tool-terminal), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`api-job-controller`](../packages/api/job-controller) | - | 生产方(后台 bash/pwsh、PTY 发送和 subagent 委派)登记正在运行的工作;声明 record 的 job 还为非消费观察者流式提供原始输出;tool-jobs 是面向模型的控制器,用于读取、列出和终止这些工作;jobs-local 是进程本地注册表。 | | `ctx.web` | `seam` | [`web`](../packages/web/web) | [`web-search-exa`](../packages/web/web-search-exa), [`web-search-perplexity`](../packages/web/web-search-perplexity), [`web-search-deepseek`](../packages/web/web-search-deepseek), [`web-fetch-http`](../packages/web/web-fetch-http) | [`tool-web`](../packages/web/tool-web) | - | 搜索和抓取提供方注册到同一个 ctx.web seam;tool-web 负责稳定的面向模型名称。 | diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index 106db59c55..76d9465922 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/config-catalog.md -config-catalog.md: b9e75f6679d43aef41808a30ed370e26070c1331 -config-catalog.zh.md: d76c88fb806998abee414347822a3d11c3b2b7de +config-catalog.md: 146a7e58138ec4f1f92cc27dd2dc76632aaff45c +config-catalog.zh.md: 2e67775709d5f5c0e5b0c465be8c1b901fe6ad97 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index b9e75f6679..146a7e5813 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -746,7 +746,7 @@ export interface Config { } ``` -Source: [`packages/experimental/agent-team/src/types.ts:130`](../packages/experimental/agent-team/src/types.ts) +Source: [`packages/experimental/agent-team/src/types.ts:152`](../packages/experimental/agent-team/src/types.ts) @@ -1033,8 +1033,12 @@ export interface Config { vadModelPath?: string | undefined /** Weight precision; INT8 minimizes first-use download and model storage. */ precision: 'int8' | 'fp32' - /** Hugging Face-compatible origin for pinned model URLs, including private mirrors. */ - modelOrigin: string + /** Explicit Hugging Face-compatible origin; bypasses automatic selection and public fallback. */ + modelOrigin?: string | undefined + /** Hugging Face-compatible origins compared before downloading each missing asset. */ + modelOrigins: string[] + /** Deadline for concurrent HEAD probes, including redirects to the actual asset. */ + modelProbeTimeoutMs: number /** CPU intra-operation thread count. */ threads: number /** Maximum speech segment length passed to the recognizer. */ @@ -2165,16 +2169,18 @@ Source: [`packages/plan/plan-mode/src/index.ts:70`](../packages/plan/plan-mode/s Requires: `loader` · `profileContext` ```ts config-catalog -/** The pnpm executable, the registries asked, and the limits for package diagnostics and registry lookups. */ +/** The pnpm executable, registries, and limits for diagnostics, lookups and connection checks. */ export interface Config { /** The pnpm executable name or path; resolved through `PATH` like the `dsh plugin` command. */ pnpmCommand?: string - /** Maximum retained pnpm diagnostic bytes per operation. */ + /** Maximum retained package-operation diagnostic bytes. */ outputBytes?: number /** Maximum time to wait for another process's profile package operation. */ lockWaitMs?: number /** Bound on one registry lookup an inspection runs, in milliseconds. */ inspectTimeoutMs?: number + /** Maximum duration of the GitHub repository connection check before installation, in milliseconds. */ + githubConnectionTimeoutMs?: number /** The registry lookups and installations ask first, as an http(s) URL; absent, the one pnpm's own configuration names. */ registry?: string /** @@ -2186,7 +2192,7 @@ export interface Config { } ``` -Source: [`packages/boot/plugin-manager/src/index.ts:37`](../packages/boot/plugin-manager/src/index.ts) +Source: [`packages/boot/plugin-manager/src/index.ts:38`](../packages/boot/plugin-manager/src/index.ts) diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index d76c88fb80..2e67775709 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -748,7 +748,7 @@ export interface Config { } ``` -来源: [`packages/experimental/agent-team/src/types.ts:130`](../packages/experimental/agent-team/src/types.ts) +来源: [`packages/experimental/agent-team/src/types.ts:152`](../packages/experimental/agent-team/src/types.ts) @@ -1035,8 +1035,12 @@ export interface Config { vadModelPath?: string | undefined /** Weight precision; INT8 minimizes first-use download and model storage. */ precision: 'int8' | 'fp32' - /** Hugging Face-compatible origin for pinned model URLs, including private mirrors. */ - modelOrigin: string + /** Explicit Hugging Face-compatible origin; bypasses automatic selection and public fallback. */ + modelOrigin?: string | undefined + /** Hugging Face-compatible origins compared before downloading each missing asset. */ + modelOrigins: string[] + /** Deadline for concurrent HEAD probes, including redirects to the actual asset. */ + modelProbeTimeoutMs: number /** CPU intra-operation thread count. */ threads: number /** Maximum speech segment length passed to the recognizer. */ @@ -2167,16 +2171,18 @@ export interface PlanModeConfig { 需要: `loader` · `profileContext` ```ts config-catalog -/** The pnpm executable, the registries asked, and the limits for package diagnostics and registry lookups. */ +/** The pnpm executable, registries, and limits for diagnostics, lookups and connection checks. */ export interface Config { /** The pnpm executable name or path; resolved through `PATH` like the `dsh plugin` command. */ pnpmCommand?: string - /** Maximum retained pnpm diagnostic bytes per operation. */ + /** Maximum retained package-operation diagnostic bytes. */ outputBytes?: number /** Maximum time to wait for another process's profile package operation. */ lockWaitMs?: number /** Bound on one registry lookup an inspection runs, in milliseconds. */ inspectTimeoutMs?: number + /** Maximum duration of the GitHub repository connection check before installation, in milliseconds. */ + githubConnectionTimeoutMs?: number /** The registry lookups and installations ask first, as an http(s) URL; absent, the one pnpm's own configuration names. */ registry?: string /** @@ -2188,7 +2194,7 @@ export interface Config { } ``` -来源: [`packages/boot/plugin-manager/src/index.ts:37`](../packages/boot/plugin-manager/src/index.ts) +来源: [`packages/boot/plugin-manager/src/index.ts:38`](../packages/boot/plugin-manager/src/index.ts) diff --git a/docs/module-graph.i18n.yaml b/docs/module-graph.i18n.yaml index 6ae367c64a..a1c8765cd8 100644 --- a/docs/module-graph.i18n.yaml +++ b/docs/module-graph.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/module-graph.md -module-graph.md: bfe3e5faa7168b03848ac76d2dd2584d4aa31f5e -module-graph.zh.md: bfa0f83e14cbcf89599fa207ec919195128db49e +module-graph.md: ef70bdf4a002d764a1fca6ffd5e67163e93027aa +module-graph.zh.md: 729b76c88e6d5f3dc1c2abcea0c5852d1b39348f diff --git a/docs/module-graph.md b/docs/module-graph.md index bfe3e5faa7..ef70bdf4a0 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -1249,7 +1249,6 @@ flowchart TD pkg_experimental_agent_team --> pkg_session_persistence pkg_experimental_agent_team --> pkg_session_projection pkg_experimental_agent_team --> pkg_subagent - pkg_experimental_agent_team --> pkg_typert_protocol pkg_sdk_protocol --> pkg_llm pkg_sdk_protocol --> pkg_session pkg_sdk_protocol --> pkg_subagent @@ -1274,7 +1273,6 @@ flowchart TD pkg_subagent_fork_in_process --> pkg_subagent_in_process_driver pkg_subagent_spawn_in_process --> pkg_subagent pkg_subagent_spawn_in_process --> pkg_subagent_in_process_driver - pkg_experimental_client_ui_agent_team --> pkg_api_remotes pkg_experimental_client_ui_agent_team --> pkg_api_session_controller pkg_experimental_client_ui_agent_team --> pkg_client_locale pkg_experimental_client_ui_agent_team --> pkg_client_ui_conversation @@ -1285,7 +1283,6 @@ flowchart TD pkg_experimental_client_ui_agent_team --> pkg_client_ui_workspace pkg_experimental_client_ui_agent_team --> pkg_experimental_agent_team pkg_experimental_client_ui_agent_team --> pkg_session - pkg_experimental_client_ui_agent_team --> pkg_typert_protocol pkg_experimental_client_ui_voice_input --> pkg_api_gateway pkg_experimental_client_ui_voice_input --> pkg_api_remotes pkg_experimental_client_ui_voice_input --> pkg_api_session_controller @@ -1640,13 +1637,13 @@ flowchart TD | [`api-session-controller`](../packages/api/session-controller) | `api` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`api-gateway`](../packages/api/gateway), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-file-upload`](../packages/client/file-upload), [`commands`](../packages/interaction/commands), [`file-reference`](../packages/context/file-reference), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`native-command`](../packages/util/native-command), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`skill`](../packages/skill/skill), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry), [`util-time`](../packages/util/time), [`util-values`](../packages/util/values), [`util-workspace-path`](../packages/util/workspace-path), [`workspace`](../packages/workspace/workspace) | | [`compaction-basic`](../packages/compaction/compaction-basic) | `compaction` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | | [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`spill`](../packages/spill/spill), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol) | -| [`experimental-agent-team`](../packages/experimental/agent-team) | `experimental` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol) | +| [`experimental-agent-team`](../packages/experimental/agent-team) | `experimental` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent) | | [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | | [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | | [`workflow-ptc`](../packages/workflow/workflow-ptc) | `workflow` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | | [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process) | `subagent` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) | | [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process) | `subagent` | [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) | -| [`experimental-client-ui-agent-team`](../packages/experimental/client-ui-agent-team) | `experimental` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`experimental-agent-team`](../packages/experimental/agent-team), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) | +| [`experimental-client-ui-agent-team`](../packages/experimental/client-ui-agent-team) | `experimental` | [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`experimental-agent-team`](../packages/experimental/agent-team), [`session`](../packages/core/session) | | [`experimental-client-ui-voice-input`](../packages/experimental/client-ui-voice-input) | `experimental` | [`api-gateway`](../packages/api/gateway), [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-store`](../packages/client/store), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-plugin-manager`](../packages/client/ui-plugin-manager), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`experimental-api-speech-to-text`](../packages/experimental/api-speech-to-text), [`experimental-speech-to-text`](../packages/experimental/speech-to-text), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) | | [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team) | `experimental` | [`agent`](../packages/core/agent), [`experimental-agent-team`](../packages/experimental/agent-team), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`sdk-client`](../packages/sdk/client) | `sdk` | [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index bfa0f83e14..729b76c88e 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -1251,7 +1251,6 @@ flowchart TD pkg_experimental_agent_team --> pkg_session_persistence pkg_experimental_agent_team --> pkg_session_projection pkg_experimental_agent_team --> pkg_subagent - pkg_experimental_agent_team --> pkg_typert_protocol pkg_sdk_protocol --> pkg_llm pkg_sdk_protocol --> pkg_session pkg_sdk_protocol --> pkg_subagent @@ -1276,7 +1275,6 @@ flowchart TD pkg_subagent_fork_in_process --> pkg_subagent_in_process_driver pkg_subagent_spawn_in_process --> pkg_subagent pkg_subagent_spawn_in_process --> pkg_subagent_in_process_driver - pkg_experimental_client_ui_agent_team --> pkg_api_remotes pkg_experimental_client_ui_agent_team --> pkg_api_session_controller pkg_experimental_client_ui_agent_team --> pkg_client_locale pkg_experimental_client_ui_agent_team --> pkg_client_ui_conversation @@ -1287,7 +1285,6 @@ flowchart TD pkg_experimental_client_ui_agent_team --> pkg_client_ui_workspace pkg_experimental_client_ui_agent_team --> pkg_experimental_agent_team pkg_experimental_client_ui_agent_team --> pkg_session - pkg_experimental_client_ui_agent_team --> pkg_typert_protocol pkg_experimental_client_ui_voice_input --> pkg_api_gateway pkg_experimental_client_ui_voice_input --> pkg_api_remotes pkg_experimental_client_ui_voice_input --> pkg_api_session_controller @@ -1642,13 +1639,13 @@ flowchart TD | [`api-session-controller`](../packages/api/session-controller) | `api` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`api-gateway`](../packages/api/gateway), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-file-upload`](../packages/client/file-upload), [`commands`](../packages/interaction/commands), [`file-reference`](../packages/context/file-reference), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`native-command`](../packages/util/native-command), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`skill`](../packages/skill/skill), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry), [`util-time`](../packages/util/time), [`util-values`](../packages/util/values), [`util-workspace-path`](../packages/util/workspace-path), [`workspace`](../packages/workspace/workspace) | | [`compaction-basic`](../packages/compaction/compaction-basic) | `compaction` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | | [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`spill`](../packages/spill/spill), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol) | -| [`experimental-agent-team`](../packages/experimental/agent-team) | `experimental` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol) | +| [`experimental-agent-team`](../packages/experimental/agent-team) | `experimental` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent) | | [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | | [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | | [`workflow-ptc`](../packages/workflow/workflow-ptc) | `workflow` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | | [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process) | `subagent` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) | | [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process) | `subagent` | [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) | -| [`experimental-client-ui-agent-team`](../packages/experimental/client-ui-agent-team) | `experimental` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`experimental-agent-team`](../packages/experimental/agent-team), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) | +| [`experimental-client-ui-agent-team`](../packages/experimental/client-ui-agent-team) | `experimental` | [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`experimental-agent-team`](../packages/experimental/agent-team), [`session`](../packages/core/session) | | [`experimental-client-ui-voice-input`](../packages/experimental/client-ui-voice-input) | `experimental` | [`api-gateway`](../packages/api/gateway), [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-store`](../packages/client/store), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-plugin-manager`](../packages/client/ui-plugin-manager), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`experimental-api-speech-to-text`](../packages/experimental/api-speech-to-text), [`experimental-speech-to-text`](../packages/experimental/speech-to-text), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) | | [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team) | `experimental` | [`agent`](../packages/core/agent), [`experimental-agent-team`](../packages/experimental/agent-team), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`sdk-client`](../packages/sdk/client) | `sdk` | [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) | diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index 207b82e845..32d3d4d57d 100644 --- a/docs/persistence-catalog.i18n.yaml +++ b/docs/persistence-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/persistence-catalog.md -persistence-catalog.md: 022944bea076f49e054e7194e6b475cf6dfca2bc -persistence-catalog.zh.md: 096bf746085d329427dfb5516db4c6387f0974fe +persistence-catalog.md: 200e7156f65c4cba6fab6808a84326e85df69ddd +persistence-catalog.zh.md: fa48684d4456c6338b67d58bc051c3672d6ee2c9 diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index 022944bea0..200e7156f6 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -975,7 +975,7 @@ Source: [`packages/core/session/src/types.ts:330`](../packages/core/session/src/ Types: [TeamId](subsystems/agent-team.md) · [TeamMemberSnapshot](subsystems/agent-team.md) -Source: [`packages/experimental/agent-team/src/types.ts:210`](../packages/experimental/agent-team/src/types.ts) +Source: [`packages/experimental/agent-team/src/types.ts:232`](../packages/experimental/agent-team/src/types.ts) @@ -993,7 +993,7 @@ Source: [`packages/experimental/agent-team/src/types.ts:210`](../packages/experi Types: [TeamId](subsystems/agent-team.md) · [TeamMessageId](subsystems/agent-team.md) -Source: [`packages/experimental/agent-team/src/types.ts:216`](../packages/experimental/agent-team/src/types.ts) +Source: [`packages/experimental/agent-team/src/types.ts:238`](../packages/experimental/agent-team/src/types.ts) @@ -1006,7 +1006,7 @@ Source: [`packages/experimental/agent-team/src/types.ts:216`](../packages/experi Types: [TeamId](subsystems/agent-team.md) · [TeamMessageSnapshot](subsystems/agent-team.md) -Source: [`packages/experimental/agent-team/src/types.ts:214`](../packages/experimental/agent-team/src/types.ts) +Source: [`packages/experimental/agent-team/src/types.ts:236`](../packages/experimental/agent-team/src/types.ts) @@ -1019,7 +1019,7 @@ Source: [`packages/experimental/agent-team/src/types.ts:214`](../packages/experi Types: [TeamId](subsystems/agent-team.md) · [TeamTaskSnapshot](subsystems/agent-team.md) -Source: [`packages/experimental/agent-team/src/types.ts:212`](../packages/experimental/agent-team/src/types.ts) +Source: [`packages/experimental/agent-team/src/types.ts:234`](../packages/experimental/agent-team/src/types.ts) ### `todo/*` @@ -4713,7 +4713,7 @@ Sources: [`packages/experimental/agent-team/src/types.ts:47`](../packages/experi SHA-256: `2eafefdc6f79a4922d68a787a09eaf4dd2b2369c3777fba4c05e800b5678ac8e` -Sources: [`packages/experimental/agent-team/src/types.ts:106`](../packages/experimental/agent-team/src/types.ts) +Sources: [`packages/experimental/agent-team/src/types.ts:128`](../packages/experimental/agent-team/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -4733,7 +4733,7 @@ Sources: [`packages/experimental/agent-team/src/types.ts:106`](../packages/exper SHA-256: `2152caf1a71e588e4df2358a806dea24936c1c47131134f01a12939d12cdc0c0` -Sources: [`packages/experimental/agent-team/src/types.ts:115`](../packages/experimental/agent-team/src/types.ts) +Sources: [`packages/experimental/agent-team/src/types.ts:137`](../packages/experimental/agent-team/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -7373,7 +7373,7 @@ Sources: [`packages/web/web-search-deepseek/src/provider.ts:61`](../packages/web SHA-256: `b1ebd5cda47d90d980bb05bffc3f970c51537b9aff15b3bc3d639811727741b1` -Sources: [`packages/experimental/agent-team/src/types.ts:210`](../packages/experimental/agent-team/src/types.ts) +Sources: [`packages/experimental/agent-team/src/types.ts:232`](../packages/experimental/agent-team/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -7401,7 +7401,7 @@ Sources: [`packages/core/session/src/types.ts:330`](../packages/core/session/src SHA-256: `ea08ec1df0c33108fd03345b9af199cf04dea450aa045d3c683f216311403403` -Sources: [`packages/experimental/agent-team/src/types.ts:214`](../packages/experimental/agent-team/src/types.ts) +Sources: [`packages/experimental/agent-team/src/types.ts:236`](../packages/experimental/agent-team/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -7415,7 +7415,7 @@ Sources: [`packages/experimental/agent-team/src/types.ts:214`](../packages/exper SHA-256: `0bcfaf5566d271028429ac7b562cb59053c9d78b8ae14708c35fac835108d1b1` -Sources: [`packages/experimental/agent-team/src/types.ts:216`](../packages/experimental/agent-team/src/types.ts) +Sources: [`packages/experimental/agent-team/src/types.ts:238`](../packages/experimental/agent-team/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -7561,7 +7561,7 @@ Sources: [`packages/compaction/compaction-image-offload/src/projection.ts:25`](. SHA-256: `a40d12070f6f4a124f32fb3cc86e7857554702e32f0eda3080a25c4b9ac9b18c` -Sources: [`packages/experimental/agent-team/src/types.ts:212`](../packages/experimental/agent-team/src/types.ts) +Sources: [`packages/experimental/agent-team/src/types.ts:234`](../packages/experimental/agent-team/src/types.ts) | Property | Presence | Type | |---|---|---| diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index 096bf74608..fa48684d44 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -977,7 +977,7 @@ export type SessionEvent = { 类型:[TeamId](subsystems/agent-team.zh.md) · [TeamMemberSnapshot](subsystems/agent-team.zh.md) -来源:[`packages/experimental/agent-team/src/types.ts:210`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/experimental/agent-team/src/types.ts:232`](../packages/experimental/agent-team/src/types.ts) @@ -995,7 +995,7 @@ export type SessionEvent = { 类型:[TeamId](subsystems/agent-team.zh.md) · [TeamMessageId](subsystems/agent-team.zh.md) -来源:[`packages/experimental/agent-team/src/types.ts:216`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/experimental/agent-team/src/types.ts:238`](../packages/experimental/agent-team/src/types.ts) @@ -1008,7 +1008,7 @@ export type SessionEvent = { 类型:[TeamId](subsystems/agent-team.zh.md) · [TeamMessageSnapshot](subsystems/agent-team.zh.md) -来源:[`packages/experimental/agent-team/src/types.ts:214`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/experimental/agent-team/src/types.ts:236`](../packages/experimental/agent-team/src/types.ts) @@ -1021,7 +1021,7 @@ export type SessionEvent = { 类型:[TeamId](subsystems/agent-team.zh.md) · [TeamTaskSnapshot](subsystems/agent-team.zh.md) -来源:[`packages/experimental/agent-team/src/types.ts:212`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/experimental/agent-team/src/types.ts:234`](../packages/experimental/agent-team/src/types.ts) ### `todo/*` @@ -4715,7 +4715,7 @@ SHA-256: `6598cd6da253948d6b207c6374a45335804c79b576866d6215649c2d1bc85e25` SHA-256: `2eafefdc6f79a4922d68a787a09eaf4dd2b2369c3777fba4c05e800b5678ac8e` -来源:[`packages/experimental/agent-team/src/types.ts:106`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/experimental/agent-team/src/types.ts:128`](../packages/experimental/agent-team/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -4735,7 +4735,7 @@ SHA-256: `2eafefdc6f79a4922d68a787a09eaf4dd2b2369c3777fba4c05e800b5678ac8e` SHA-256: `2152caf1a71e588e4df2358a806dea24936c1c47131134f01a12939d12cdc0c0` -来源:[`packages/experimental/agent-team/src/types.ts:115`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/experimental/agent-team/src/types.ts:137`](../packages/experimental/agent-team/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -7375,7 +7375,7 @@ SHA-256: `930a6567a10bb62ddd157bd8abdf4b182810c8c3b49d91309b3b009a5fed9731` SHA-256: `b1ebd5cda47d90d980bb05bffc3f970c51537b9aff15b3bc3d639811727741b1` -来源:[`packages/experimental/agent-team/src/types.ts:210`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/experimental/agent-team/src/types.ts:232`](../packages/experimental/agent-team/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -7403,7 +7403,7 @@ SHA-256: `6bc8e51124d0874bc89e1d0ceff381a9c3cd0fc240667d69f841ce4f60f65fe5` SHA-256: `ea08ec1df0c33108fd03345b9af199cf04dea450aa045d3c683f216311403403` -来源:[`packages/experimental/agent-team/src/types.ts:214`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/experimental/agent-team/src/types.ts:236`](../packages/experimental/agent-team/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -7417,7 +7417,7 @@ SHA-256: `ea08ec1df0c33108fd03345b9af199cf04dea450aa045d3c683f216311403403` SHA-256: `0bcfaf5566d271028429ac7b562cb59053c9d78b8ae14708c35fac835108d1b1` -来源:[`packages/experimental/agent-team/src/types.ts:216`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/experimental/agent-team/src/types.ts:238`](../packages/experimental/agent-team/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -7563,7 +7563,7 @@ SHA-256: `477614bca595a9e7adb5a1870be446b9a8684f3ba8950259d6289f2d3b74f028` SHA-256: `a40d12070f6f4a124f32fb3cc86e7857554702e32f0eda3080a25c4b9ac9b18c` -来源:[`packages/experimental/agent-team/src/types.ts:212`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/experimental/agent-team/src/types.ts:234`](../packages/experimental/agent-team/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| diff --git a/docs/persistence-schema.json b/docs/persistence-schema.json index 4695717a2d..211f6c6fe6 100644 --- a/docs/persistence-schema.json +++ b/docs/persistence-schema.json @@ -22575,7 +22575,7 @@ }, "names": [], "sources": [ - "packages/experimental/agent-team/src/types.ts:216" + "packages/experimental/agent-team/src/types.ts:238" ] }, { @@ -31770,7 +31770,7 @@ "packages/experimental/agent-team/src/types.ts#TeamMessageSource" ], "sources": [ - "packages/experimental/agent-team/src/types.ts:115" + "packages/experimental/agent-team/src/types.ts:137" ] }, { @@ -33739,7 +33739,7 @@ "packages/experimental/agent-team/src/types.ts#TeamMessageSnapshot" ], "sources": [ - "packages/experimental/agent-team/src/types.ts:106" + "packages/experimental/agent-team/src/types.ts:128" ] }, { @@ -69181,7 +69181,7 @@ }, "names": [], "sources": [ - "packages/experimental/agent-team/src/types.ts:212" + "packages/experimental/agent-team/src/types.ts:234" ] }, { @@ -71163,7 +71163,7 @@ }, "names": [], "sources": [ - "packages/experimental/agent-team/src/types.ts:210" + "packages/experimental/agent-team/src/types.ts:232" ] }, { @@ -83593,7 +83593,7 @@ }, "names": [], "sources": [ - "packages/experimental/agent-team/src/types.ts:214" + "packages/experimental/agent-team/src/types.ts:236" ] }, { diff --git a/docs/subsystems/agent-team.i18n.yaml b/docs/subsystems/agent-team.i18n.yaml index 6a04e1377d..03abbfe5b9 100644 --- a/docs/subsystems/agent-team.i18n.yaml +++ b/docs/subsystems/agent-team.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/agent-team.md -agent-team.md: 2f49e164645e6b66bd0da4946c477a09c412ddba -agent-team.zh.md: 24d7d73fdace11ad5e00eb3fb4c90a4abde33810 +agent-team.md: 6cb503d130727b4c4694877f83c02f2e0983c8af +agent-team.zh.md: 083b9fafb6f45cd09ded84807da3c8602193b3d4 diff --git a/docs/subsystems/agent-team.md b/docs/subsystems/agent-team.md index 2f49e16464..6cb503d130 100644 --- a/docs/subsystems/agent-team.md +++ b/docs/subsystems/agent-team.md @@ -2,7 +2,7 @@ English | [中文](agent-team.zh.md) -Types shared by the experimental implicit-root Team domain, model tools, and host adapters. The [Agent Teams Agent Note](../../.agents/notes/implemented/feature/2026-08-05-agent-teams.md) owns identity, mailbox, task, and shared-checkout decisions; this page records the literal durable forms from [`packages/experimental/agent-team/src/types.ts`](../../packages/experimental/agent-team/src/types.ts). +Types shared by the experimental implicit-root Team domain, model tools, and host adapters. The [Agent Teams Agent Note](../../.agents/notes/implemented/feature/2026-08-05-agent-teams.md) owns identity, mailbox, task, and shared-checkout decisions; this page records the durable and client-visible forms from [`packages/experimental/agent-team/src/types.ts`](../../packages/experimental/agent-team/src/types.ts). ## Identity and roster @@ -73,9 +73,56 @@ interface TeamTaskSnapshot { `pending` is unstarted or released, `in_progress` carries an owner, `completed` satisfies blockers, and `deleted` is a retained tombstone. Views add owner name, readiness, and write-scope overlap warnings without changing the durable snapshot. + + +## Web projection + +The Lead Session publishes `SessionProjectionMap.agentTeam` with durable roster rows and non-deleted task views. `failure` reports a rejected persisted record beside the last valid state. Member activity comes from Session status; model labels come from each member's `modelSelection` projection. + +```ts type-equiv +/** One durable roster row published through the `agentTeam` Session projection. */ +interface TeamMemberProjection { + readonly id: SessionId + readonly name: string + readonly role: 'lead' | 'teammate' + /** Durable lifecycle; the Lead row is always `active`. Turn activity comes from Session status. */ + readonly phase: TeamMemberPhase + readonly error?: string +} +``` + +```ts type-equiv +/** Runtime-enriched task view returned to tools and hosts. */ +interface TeamTaskView { + readonly id: TeamTaskId + readonly revision: number + readonly subject: string + readonly description: string + readonly status: TeamTaskStatus + readonly blockedBy: TeamTaskId[] + readonly writeScopes: string[] + readonly ownerName?: string + readonly ready: boolean + readonly writeScopeWarnings: string[] +} +``` + +```ts type-equiv +/** + * Durable Team state published to browser clients through the Lead Session's + * `agentTeam` projection. `failure` names the first rejected persisted Team + * record; members and tasks then stay at the last valid state. + */ +interface TeamProjection { + readonly members: TeamMemberProjection[] + readonly tasks: TeamTaskView[] + readonly failure?: string +} +``` + ## Replay -`foldTeam()` replays one root Session into the roster, task board, and queued-minus-delivered mailbox that every Team operation reads. It selects records by `TeamId`, so events inherited by an ordinary fork retain the ancestor id and never enter the new root's state. Session event `seq` and `time` remain the ordering and timing record; Team snapshots do not duplicate them. Roster and task reads reach callers as views; pending mail stays internal to delivery and recovery. The package [README](../../packages/experimental/agent-team/README.md) owns operation, authorization, recovery, and limit behavior. +The `agentTeam` Session projection replays one root Session into the roster, task board, and queued-minus-delivered mailbox that every Team operation reads. It selects records by `TeamId`, so events inherited by an ordinary fork retain the ancestor id and never enter the new root's state. Session event `seq` and `time` remain the ordering and timing record; Team snapshots do not duplicate them. Roster and task reads reach callers as views; pending mail stays internal to delivery and recovery. The package [README](../../packages/experimental/agent-team/README.md) owns operation, authorization, recovery, and limit behavior. @@ -176,13 +223,6 @@ interrupt(caller: Agent, targetName: string): { previousStatus: 'running' | 'ina * @returns Team membership, or undefined for non-Team subagents and stale identities. */ tryMembership(agent: Agent): TeamMembership | undefined - -/** - * Read the current roster and non-deleted task board through the generated Remote API. - * @param agent - exact live Team member used as the authority credential. - * @returns detached current roster and task views. - */ -@Remote('view') remoteView(agent: Agent): TeamView ``` Types: [Agent](core.md) diff --git a/docs/subsystems/agent-team.zh.md b/docs/subsystems/agent-team.zh.md index 24d7d73fda..083b9fafb6 100644 --- a/docs/subsystems/agent-team.zh.md +++ b/docs/subsystems/agent-team.zh.md @@ -2,7 +2,7 @@ [English](agent-team.md) | 中文 -实验性隐式 Root Team 领域、模型工具与宿主适配器共享的类型。[Agent Teams Agent Note](../../.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md)负责身份、mailbox、task 与共享 checkout 决策;本页记录 [`packages/experimental/agent-team/src/types.ts`](../../packages/experimental/agent-team/src/types.ts) 中的字面持久形式。 +实验性隐式 Root Team 领域、模型工具与宿主适配器共享的类型。[Agent Teams Agent Note](../../.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md)负责身份、mailbox、task 与共享 checkout 决策;本页记录 [`packages/experimental/agent-team/src/types.ts`](../../packages/experimental/agent-team/src/types.ts) 中的持久与客户端可见形式。 ## 身份与 roster @@ -73,9 +73,56 @@ interface TeamTaskSnapshot { `pending` 表示尚未开始或已经释放,`in_progress` 携带 owner,`completed` 满足 blocker,`deleted` 是保留的 tombstone。view 会添加 owner name、readiness 和 write-scope 重叠警告,但不会改变持久快照。 + + +## Web 投影 + +Lead Session 通过 `SessionProjectionMap.agentTeam` 发布持久 roster 行与未删除任务视图。`failure` 在最后有效状态旁报告被拒绝的持久记录。成员活动来自 Session 状态;模型标签来自各成员的 `modelSelection` 投影。 + +```ts type-equiv +/** One durable roster row published through the `agentTeam` Session projection. */ +interface TeamMemberProjection { + readonly id: SessionId + readonly name: string + readonly role: 'lead' | 'teammate' + /** Durable lifecycle; the Lead row is always `active`. Turn activity comes from Session status. */ + readonly phase: TeamMemberPhase + readonly error?: string +} +``` + +```ts type-equiv +/** Runtime-enriched task view returned to tools and hosts. */ +interface TeamTaskView { + readonly id: TeamTaskId + readonly revision: number + readonly subject: string + readonly description: string + readonly status: TeamTaskStatus + readonly blockedBy: TeamTaskId[] + readonly writeScopes: string[] + readonly ownerName?: string + readonly ready: boolean + readonly writeScopeWarnings: string[] +} +``` + +```ts type-equiv +/** + * Durable Team state published to browser clients through the Lead Session's + * `agentTeam` projection. `failure` names the first rejected persisted Team + * record; members and tasks then stay at the last valid state. + */ +interface TeamProjection { + readonly members: TeamMemberProjection[] + readonly tasks: TeamTaskView[] + readonly failure?: string +} +``` + ## 回放 -`foldTeam()` 把一个 Root Session 回放成每个 Team 操作所读取的 roster、任务板与 queued-minus-delivered mailbox。它按 `TeamId` 选取记录,因此普通 fork 继承的 event 保留 ancestor id,绝不会进入新 Root 的状态。Session event 的 `seq` 与 `time` 继续负责顺序和时间记录,Team snapshot 不再重复保存它们。roster 与 task 读取以 view 形式到达调用方,而 pending 邮件仅供投递与恢复内部使用。包 [README](../../packages/experimental/agent-team/README.zh.md)负责 operation、authorization、recovery 和限制行为。 +`agentTeam` Session 投影把一个 Root Session 回放成每个 Team 操作所读取的 roster、任务板与 queued-minus-delivered mailbox。它按 `TeamId` 选取记录,因此普通 fork 继承的 event 保留 ancestor id,绝不会进入新 Root 的状态。Session event 的 `seq` 与 `time` 继续负责顺序和时间记录,Team snapshot 不再重复保存它们。roster 与 task 读取以 view 形式到达调用方,而 pending 邮件仅供投递与恢复内部使用。包 [README](../../packages/experimental/agent-team/README.zh.md)负责 operation、authorization、recovery 和限制行为。 @@ -176,13 +223,6 @@ interrupt(caller: Agent, targetName: string): { previousStatus: 'running' | 'ina * @returns Team membership, or undefined for non-Team subagents and stale identities. */ tryMembership(agent: Agent): TeamMembership | undefined - -/** - * Read the current roster and non-deleted task board through the generated Remote API. - * @param agent - exact live Team member used as the authority credential. - * @returns detached current roster and task views. - */ -@Remote('view') remoteView(agent: Agent): TeamView ``` Types: [Agent](core.zh.md) diff --git a/docs/subsystems/boot.i18n.yaml b/docs/subsystems/boot.i18n.yaml index 00cd046aaa..4df5918665 100644 --- a/docs/subsystems/boot.i18n.yaml +++ b/docs/subsystems/boot.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/boot.md -boot.md: a1e8917adaf80315d871081740e5a76e5cd1e6dd -boot.zh.md: 1ba455d6ba9ff0053ff9d6f6d1313e7ae871a2a1 +boot.md: 6ec2204ffe04fe91acb89fe16fa8f8d3332299e7 +boot.zh.md: f5fde00f0cd858450ecd7c75ca42b64e2f4348e4 diff --git a/docs/subsystems/boot.md b/docs/subsystems/boot.md index a1e8917ada..6ec2204ffe 100644 --- a/docs/subsystems/boot.md +++ b/docs/subsystems/boot.md @@ -129,7 +129,9 @@ Manage profile files and apply their declared reload lifecycle. @Remote setBundleEnabled(name: string, enabled: boolean): Promise /** - * Install a package using the same pnpm implementation as dsh plugin. A run + * Install a package using the same pnpm implementation as dsh plugin. GitHub + * repositories get a connection check bounded by githubConnectionTimeoutMs before pnpm starts; + * only network failures or timeouts stop installation, while pnpm owns authentication and transport fallback. A run * that fails, is cancelled, or adds a package without a bundle patch restores * `package.json` and `pnpm-lock.yaml` as they were; downloaded files can stay. * @param spec One package spec, including local paths relative to the invocation directory. @@ -148,7 +150,7 @@ Manage profile files and apply their declared reload lifecycle. /** Stop an installation this manager owns and wait until its files are back. * @param requestId The id the installation was started with. - * @returns `cancelled` once pnpm exited and the files are restored, `too-late` once the bundle is being + * @returns `cancelled` once the Git check or pnpm exited and the files are restored, `too-late` once the bundle is being * applied, `not-running` for any other id. */ @Remote async cancelInstall(requestId: PluginInstallRequestId): Promise diff --git a/docs/subsystems/boot.zh.md b/docs/subsystems/boot.zh.md index 1ba455d6ba..f5fde00f0c 100644 --- a/docs/subsystems/boot.zh.md +++ b/docs/subsystems/boot.zh.md @@ -129,7 +129,9 @@ Manage profile files and apply their declared reload lifecycle. @Remote setBundleEnabled(name: string, enabled: boolean): Promise /** - * Install a package using the same pnpm implementation as dsh plugin. A run + * Install a package using the same pnpm implementation as dsh plugin. GitHub + * repositories get a connection check bounded by githubConnectionTimeoutMs before pnpm starts; + * only network failures or timeouts stop installation, while pnpm owns authentication and transport fallback. A run * that fails, is cancelled, or adds a package without a bundle patch restores * `package.json` and `pnpm-lock.yaml` as they were; downloaded files can stay. * @param spec One package spec, including local paths relative to the invocation directory. @@ -148,7 +150,7 @@ Manage profile files and apply their declared reload lifecycle. /** Stop an installation this manager owns and wait until its files are back. * @param requestId The id the installation was started with. - * @returns `cancelled` once pnpm exited and the files are restored, `too-late` once the bundle is being + * @returns `cancelled` once the Git check or pnpm exited and the files are restored, `too-late` once the bundle is being * applied, `not-running` for any other id. */ @Remote async cancelInstall(requestId: PluginInstallRequestId): Promise diff --git a/docs/subsystems/voice-input.i18n.yaml b/docs/subsystems/voice-input.i18n.yaml index c1b12085c1..c26921adf8 100644 --- a/docs/subsystems/voice-input.i18n.yaml +++ b/docs/subsystems/voice-input.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/voice-input.md -voice-input.md: 1c128b268c8fbbcabc13f4ca52daaa62534a6366 -voice-input.zh.md: 5ce0626f4dc769cb12a842b7ad296eb915cb94ea +voice-input.md: 1d85e8226afddad187533cfe5b14b6750d46c158 +voice-input.zh.md: 78a70ab9c265df86c1d99aa863a4be0ae1abbd9e diff --git a/docs/subsystems/voice-input.md b/docs/subsystems/voice-input.md index 1c128b268c..1d85e8226a 100644 --- a/docs/subsystems/voice-input.md +++ b/docs/subsystems/voice-input.md @@ -18,6 +18,8 @@ The input facade captures a revision-bearing selection before recording. `InputA ## Preparation and settings +`SpeechProviderInfo.downloadSources` advertises preparation origins. `SpeechPreparationOptions.downloadSource` selects one for a task; omission preserves provider policy. SenseVoice validates the advertised choice, pins a manual source without fallback, and rejects source changes during active preparation. The UI keeps a choice for retries in the current card; it is not a persisted recognition preference. + The Host provider owns one preparation task across page and Session changes. The Client shares one `follow()` subscription across the composer, setup prompt and bundle details. Optional `SpeechSetupEstimate` metadata supplies provider-specific planning hints, separate from measured progress. `SpeechPreparationStepKind` identifies ordered resource operations; `SpeechPreparationStep` records each status and start time. The complete `SpeechPreparationState` retains these steps after cancellation or failure. The collapsed UI shows the current operation; expanding lists all steps, with byte progress or elapsed time only for the running step. Closing an observer never cancels preparation. Verified files survive retries and idle worker reclamation. Failed preparation can include `SpeechDownloadFailure` with the asset, source origin, classified reason and optional diagnostic code or HTTP status. The Client localizes recovery advice; raw download causes remain on the Host. @@ -66,8 +68,9 @@ Speech calls never activate or submit to an Agent. /** * Start or join one Host-owned preparation task. * @param providerId - selected recognizer. + * @param options - task-local source selection validated by the provider. */ -@Remote prepare(providerId: SpeechProviderId): void +@Remote prepare(providerId: SpeechProviderId, options?: SpeechPreparationOptions): void /** * Explicitly cancel resource preparation. @@ -130,8 +133,9 @@ async configure(patch: SpeechSelectionPatch): Promise /** * Start or join provider-owned preparation. * @param id - exact registered provider identity. + * @param options - task-local source selection validated by the provider. */ -prepare(id: SpeechProviderId): void +prepare(id: SpeechProviderId, options?: SpeechPreparationOptions): void /** * Explicitly cancel provider preparation without tying it to a browser connection. diff --git a/docs/subsystems/voice-input.zh.md b/docs/subsystems/voice-input.zh.md index 5ce0626f4d..78a70ab9c2 100644 --- a/docs/subsystems/voice-input.zh.md +++ b/docs/subsystems/voice-input.zh.md @@ -18,6 +18,8 @@ ## 准备与设置 +`SpeechProviderInfo.downloadSources` 公布准备阶段的源。`SpeechPreparationOptions.downloadSource` 为单次任务选择一个源;省略时保留提供方策略。SenseVoice 校验公布的选项,固定使用手动指定的源而不回退,并拒绝在准备期间改源。UI 为当前卡片中的重试保留选择,但不会将其保存为识别偏好。 + Host Provider 在页面和 Session 变化期间拥有同一个准备任务。Client 在输入框、安装引导弹窗、Bundle 详情之间共享一份 `follow()` 订阅。可选 `SpeechSetupEstimate` 元数据提供各 Provider 的资源预期,与实测进度分开。`SpeechPreparationStepKind` 标识有序资源操作;`SpeechPreparationStep` 记录各步骤状态与开始时间。完整 `SpeechPreparationState` 在取消或失败后保留这些步骤。折叠 UI 显示当前操作,展开后列出所有步骤,仅进行中步骤显示字节进度或等待时间。关闭观察者不会取消准备。已校验文件在重试及空闲进程回收后继续复用。 准备失败时可包含 `SpeechDownloadFailure`,提供文件、下载源、原因分类以及可选错误码或 HTTP 状态。Client 将恢复建议本地化;原始下载错误留在 Host。 @@ -66,8 +68,9 @@ Speech calls never activate or submit to an Agent. /** * Start or join one Host-owned preparation task. * @param providerId - selected recognizer. + * @param options - task-local source selection validated by the provider. */ -@Remote prepare(providerId: SpeechProviderId): void +@Remote prepare(providerId: SpeechProviderId, options?: SpeechPreparationOptions): void /** * Explicitly cancel resource preparation. @@ -130,8 +133,9 @@ async configure(patch: SpeechSelectionPatch): Promise /** * Start or join provider-owned preparation. * @param id - exact registered provider identity. + * @param options - task-local source selection validated by the provider. */ -prepare(id: SpeechProviderId): void +prepare(id: SpeechProviderId, options?: SpeechPreparationOptions): void /** * Explicitly cancel provider preparation without tying it to a browser connection. diff --git a/docs/testing.i18n.yaml b/docs/testing.i18n.yaml index a044baf3b1..583a4c1f73 100644 --- a/docs/testing.i18n.yaml +++ b/docs/testing.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/testing.md -testing.md: 95badfd66ba49151a64dad3f5959a12265449f97 -testing.zh.md: a715fb0a6f285a908857a90e848213f42cc040cf +testing.md: c726270b796f6de125a762436ccd5bc7c74ba3bc +testing.zh.md: 977960c76d6455389a4058ebac305e6690342278 diff --git a/docs/testing.md b/docs/testing.md index 95badfd66b..c726270b79 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -12,7 +12,7 @@ How this repo tests, tier by tier, and the rules that keep a green suite meaning - **Owner-local expected output** (`pnpm run test:expected`): keyless assembled CLI/process expectations without a recorded-session round trip. Drivers use `*.expected.e2e.ts` beside `tests/expected/`; CI runs built exports. Package/script expectations use `test`, while browser expectations use `test:web`. - **Performance benchmarks** (`pnpm run test:bench`; required Linux PR gate `node 24 / benchmarks`): `benchmarks/` groups user-path gates. It builds libraries and workers; timed code runs under plain Node, never TSX. Synthetic inputs enforce time, heap, and scaling budgets; package-local `.perf.ts` stays diagnostic ([rules](../.agents/notes/implemented/testing/2026-09-04-session-open-performance-gate.md)). - **Snapshot** (`pnpm run test:snapshot`): a top-level scenario's highest recorded parent generation supplies user input and model replay, then serves as the expected persisted result. Parent filenames are `session[.vN].jsonl`; child roles are `session.[.vN].jsonl`; v0 omits `.v0`, positive versions require lowercase `.vN`, and each filename must agree with its header. Process scenarios start through `dsh`: headless owns one-shot behavior, the SDK owns persistent control, ACP owns automation-protocol behavior, and Web retains browser/ARIA evidence beside the same Session. `snapshot.yml` declares the profile, composition/header class, recording policy, exceptional replay or input metadata, and workspace facts. Typed tokens preserve parent/child identity relationships; only header pins own prompt/schema sidecars. A mutating scenario independently compares the complete `workspace.expected/` tree, which record and refresh never rewrite. Use `test:snapshot:record` when a model transcript changes and `test:snapshot:refresh` when replay input remains valid; review every resulting diff. -- **Web browser snapshot** (`pnpm run test:web`; required Linux PR gate): Chromium compares session-driven output under `snapshots/web/` and UI-only output under `apps/web/tests/expected/`. CI forces read-only `DSH_SNAPSHOT=replay`, never writing expected outputs; record/refresh stay local and every diff is reviewed ([web e2e lane](../.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md), [CI gate decision](../.agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.md)). `test:web` builds first for plugin CSS. +- **Web browser snapshot** (`pnpm run test:web`; required Linux PR gate): The command builds plugin CSS, then compares all session-driven `snapshots/web/` and UI-only `apps/web/tests/expected/` output in Chromium; the model/reasoning picker also runs in WebKit. CI enforces read-only `DSH_SNAPSHOT=replay`; record/refresh stay local, with every diff reviewed ([web lane](../.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md), [CI decision](../.agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.md)). Session fixtures retain headers and payloads but omit body sequence/time envelopes; replay synthesizes them. Replay, record, and refresh select each parent/child role's highest generation. Current fixtures use the [writer format](session-format-status.md) in their filenames and headers, one row per event, and embedded compact Assistant streams. Historical fixtures retain their released representation; explicit `sessionFormat` owners preserve migration coverage. Follow the [format-version cookbook](cookbook/adding-a-session-format-version.md#snapshot-successors) to add successors without changing predecessors. diff --git a/docs/testing.zh.md b/docs/testing.zh.md index a715fb0a6f..977960c76d 100644 --- a/docs/testing.zh.md +++ b/docs/testing.zh.md @@ -12,7 +12,7 @@ - **所属位置的预期输出**(`pnpm run test:expected`):无录制会话往返的无密钥组装 CLI/进程预期。驱动使用 `*.expected.e2e.ts`,并与 `tests/expected/` 同属一处;CI 针对构建产物运行。包/脚本预期使用 `test`,浏览器预期使用 `test:web`。 - **性能基准**(`pnpm run test:bench`;必需的 Linux PR gate `node 24 / benchmarks`):`benchmarks/` 按用户路径组织门禁。它先构建 library 和 worker;被计时代码在纯 Node 下运行,不使用 TSX。合成输入执行耗时、堆和缩放预算;包内 `.perf.ts` 保留为诊断([规则](../.agents/notes/implemented/testing/2026-09-04-session-open-performance-gate.zh.md))。 - **快照**(`pnpm run test:snapshot`):顶层场景数值最高的已录制 parent generation 同时提供用户输入和模型回放,并作为持久化结果的预期值。parent 文件名是 `session[.vN].jsonl`;child 角色使用 `session.[.vN].jsonl`;v0 省略 `.v0`,正版本必须使用小写 `.vN`,且每个文件名必须与其 header 一致。进程级场景都通过 `dsh` 启动:headless 负责一次性行为,SDK 负责持久控制,ACP 负责自动化协议行为,Web 在同一 Session 旁保留浏览器与 ARIA 证据。`snapshot.yml` 声明 profile、组合与请求头类别、录制策略、例外回放或输入元数据以及 workspace 事实。带类型的 token 保留父子身份关系;只有请求头 pin 拥有 prompt/schema sidecar。变更 workspace 的场景会独立比较完整的 `workspace.expected/` 目录,record 与 refresh 绝不改写该目录。当模型 transcript(文本记录)变化时使用 `test:snapshot:record`,回放输入仍有效时使用 `test:snapshot:refresh`;请审查所有结果差异。 -- **Web 浏览器快照**(`pnpm run test:web`;必需的 Linux PR(Pull Request)门禁):Chromium 比较 `snapshots/web/` 下由会话驱动的输出,以及 `apps/web/tests/expected/` 下仅含 UI 的输出。CI 强制只读的 `DSH_SNAPSHOT=replay`,绝不写入预期输出;record/refresh 留在本地,每处 diff 都须评审([web e2e 车道](../.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.zh.md)、[CI 门禁决策](../.agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.zh.md))。`test:web` 会先构建以交付插件 CSS。 +- **Web 浏览器快照**(`pnpm run test:web`;必需的 Linux PR(Pull Request)门禁):命令先构建插件 CSS,再用 Chromium 比较 `snapshots/web/` 下全部会话驱动输出和 `apps/web/tests/expected/` 下全部纯 UI 输出;模型与推理强度选择场景还在 WebKit 中运行。CI 强制只读的 `DSH_SNAPSHOT=replay`;record/refresh 留在本地,每处 diff 都须评审([web 车道](../.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.zh.md)、[CI 决策](../.agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.zh.md))。 Session fixture 保留 header 与 payload,但省略正文 seq/time envelope;replay 会合成这些 envelope。Replay、record 与 refresh 会选择每个 parent/child 角色的最高 generation。当前 fixture 在文件名与 header 中使用[写入格式](session-format-status.zh.md),每个事件一行,并嵌入紧凑 Assistant stream。历史 fixture 保留其已发布表示;显式 `sessionFormat` 所有者保留迁移覆盖。按照[格式版本实操手册](cookbook/adding-a-session-format-version.zh.md#snapshot-successors)添加后继代际,不改动前代。 diff --git a/package.json b/package.json index 13e90f90e4..986af9f481 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-root", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "license": "MIT", "private": true, "type": "module", diff --git a/packages/acp/acp/package.json b/packages/acp/acp/package.json index a3a0d70e95..086843b9b5 100644 --- a/packages/acp/acp/package.json +++ b/packages/acp/acp/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-acp", "description": "Automation-only Agent Client Protocol server for driving DeepSeek Harness agents over JSON-RPC stdio", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/api/account-controller/package.json b/packages/api/account-controller/package.json index fbbafa2fc8..714c2c4e40 100644 --- a/packages/api/account-controller/package.json +++ b/packages/api/account-controller/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-account-controller", "description": "Expose safe account operations over authenticated Remote", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/api/gateway/package.json b/packages/api/gateway/package.json index 814c3aa7e2..48dccab35f 100644 --- a/packages/api/gateway/package.json +++ b/packages/api/gateway/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-gateway", "description": "Typert Remote Host dispatcher and Client API endpoint", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/api/job-controller/package.json b/packages/api/job-controller/package.json index 9af939effc..7cf32571b1 100644 --- a/packages/api/job-controller/package.json +++ b/packages/api/job-controller/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-job-controller", "description": "Job Remote observation stream and the reference-counted client job-output service", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/api/remotes/package.json b/packages/api/remotes/package.json index 537fb57354..dc33e2f2c0 100644 --- a/packages/api/remotes/package.json +++ b/packages/api/remotes/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-remotes", "description": "Remote BFF assembly for application-selected Host capabilities", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/api/session-controller/package.json b/packages/api/session-controller/package.json index bb77590743..08704ed09d 100644 --- a/packages/api/session-controller/package.json +++ b/packages/api/session-controller/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-session-controller", "description": "Session Remote commands, cold reads, and live control transport", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/api/settings-controller/package.json b/packages/api/settings-controller/package.json index 0249f259bc..e228f2cbeb 100644 --- a/packages/api/settings-controller/package.json +++ b/packages/api/settings-controller/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-settings-controller", "description": "Remote owner for the configuration surfaces over the settings-domain seams", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/api/terminal-controller/package.json b/packages/api/terminal-controller/package.json index eaeee722f4..c26390e3d6 100644 --- a/packages/api/terminal-controller/package.json +++ b/packages/api/terminal-controller/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-terminal-controller", "description": "Session-owned interactive terminals with shell discovery, screen recovery and typed Remote control", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/api/workspace-controller/package.json b/packages/api/workspace-controller/package.json index c7fa53a013..dfa24fae41 100644 --- a/packages/api/workspace-controller/package.json +++ b/packages/api/workspace-controller/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-workspace-controller", "description": "Workspace Remote commands and reconnect-safe state transport", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/api/workspace-files/package.json b/packages/api/workspace-files/package.json index 993a1283bb..a5e73318e6 100644 --- a/packages/api/workspace-files/package.json +++ b/packages/api/workspace-files/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-workspace-files", "description": "Workspace file service and Client resource provider: bounded reads, directory listing, and live metadata over the workspaceFiles Remote namespace", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/attachment/attachment-local/package.json b/packages/attachment/attachment-local/package.json index 4cc47b0077..aa3af60e31 100644 --- a/packages/attachment/attachment-local/package.json +++ b/packages/attachment/attachment-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-attachment-local", "description": "Private content-addressed DSH_HOME attachment storage", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/attachment/attachment/package.json b/packages/attachment/attachment/package.json index 6c9a9b52e2..c377716886 100644 --- a/packages/attachment/attachment/package.json +++ b/packages/attachment/attachment/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-attachment", "description": "Durable immutable attachment storage seam for the DeepSeek Harness", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/boot/app-boot/package.json b/packages/boot/app-boot/package.json index 52117cb7d3..051c79d0bd 100644 --- a/packages/boot/app-boot/package.json +++ b/packages/boot/app-boot/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-app-boot", "description": "Shared boot glue for the app bins: .env loading, fail-loud Loader guards, snapshot-aware config resolution, and the Loader boot sequence", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/boot/cmdline/package.json b/packages/boot/cmdline/package.json index b1e91919be..2a74de5479 100644 --- a/packages/boot/cmdline/package.json +++ b/packages/boot/cmdline/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-cmdline", "description": "Immutable command-line handoff from a dsh launcher to any app plugin that injects cmdlineArgs", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/boot/config-editor/package.json b/packages/boot/config-editor/package.json index 138a6bd16c..1a69c93b6d 100644 --- a/packages/boot/config-editor/package.json +++ b/packages/boot/config-editor/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-config-editor", "description": "Persist plugin configuration through profile patches and Loader reconciliation", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/boot/hmr/package.json b/packages/boot/hmr/package.json index 415f143316..d57ac71584 100644 --- a/packages/boot/hmr/package.json +++ b/packages/boot/hmr/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-hmr", "description": "Coordinated module and profile configuration hot reload", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/boot/plugin-manager/README.i18n.yaml b/packages/boot/plugin-manager/README.i18n.yaml index 8dbbf1d7b8..a27c97fc81 100644 --- a/packages/boot/plugin-manager/README.i18n.yaml +++ b/packages/boot/plugin-manager/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/boot/plugin-manager/README.md -README.md: 1de9c9d78e421d44eb006f1ab7fd82656f006a2a -README.zh.md: f7502aab64738ad1feccf58b84ff945213df2eff +README.md: 803566c8650df7186c5344945ea0fe2359e8e70c +README.zh.md: 657e4587e2d813dd53b44fdb15a23f905f2c0dca diff --git a/packages/boot/plugin-manager/README.md b/packages/boot/plugin-manager/README.md index 1de9c9d78e..803566c865 100644 --- a/packages/boot/plugin-manager/README.md +++ b/packages/boot/plugin-manager/README.md @@ -45,11 +45,13 @@ A selected bundle that cannot load remains in `listBundles` with an `error`; `en `inspect(spec, options)` reads what a spec names before anything installs: a registry name is asked of the registry through `pnpm view`, run in the profile directory so the same proxy and authentication settings apply as to the install; an absolute path has its `package.json` read; a git address or tarball answers only its form and the `host` it is fetched from. The answer carries the name, version, description, whether the package declares a bundle, and the `registry` that answered, or a `problem`: `invalid-spec`, `already-installed`, `not-found`, `not-a-package`, `not-a-bundle`, `network`, or `unknown`, with the `registries` asked. A caller's `signal` or `inspectTimeoutMs` ends the lookup. +`installBundle` checks GitHub repositories with `git ls-remote` before starting pnpm, using the profile directory and the installer's Git and proxy configuration. `githubConnectionTimeoutMs` defaults to 5000 ms and limits only this check, not package download or builds. The check disables credential helpers and prompts; only network failures and timeouts stop installation, reporting `failedAt: 'spec-host'` with the existing failure kind and diagnostic log. Authentication, repository lookup and other failures are left to pnpm, including its HTTPS-to-SSH fallback. Cancellation and manager disposal stop the check and its descendants. Registry packages, paths, tarballs and other Git hosts skip this check. A reachable repository can still fail during download or bundle validation. + Registries are asked in turn. The plan starts at `options.registry`, else the configured `registry` (`null` is the one pnpm's own configuration names), and continues through `fallbackRegistries` while a registry is unreachable, times out, or answers that it has no such package or version, which a mirror not yet synced does. A registry outside the configured set is asked alone, so a private registry never falls through to a public one; pnpm's own registry counts as part of the set only while what it names, read through `pnpm config get registry` before each plan, is npm's own registry or one of the fallbacks, and is otherwise asked alone as a private one. A registry pnpm's own configuration already names is asked once. The lookup runs `pnpm view` with `--registry` and without pnpm's own retries, so a dead registry is reported within `inspectTimeoutMs` and the next one is asked; pnpm prints its refusal as JSON on stdout, which is read like stderr. The install keeps pnpm's retry settings. `registries()` answers the configured set and what pnpm names, for a picker. The [registry Agent Note](../../../.agents/notes/implemented/architecture/2026-09-18-plugin-install-registries.md) owns the rationale. The browser-safe `@deepseek-ai/dsh-plugin-manager/registry` entry exports `OFFICIAL_NPM_REGISTRY` and `NPMMIRROR_REGISTRY` for consumers that identify those public registries. -`installBundle` accepts a caller-generated `requestId`, under which `plugin-manager/install-log` streams each pnpm run's output and `plugin-manager/install-state` announces `installing`, `cancelling`, and `applying`; `installing` is announced once per registry asked, with the `attempt`'s registry, position, and the plan's length. The install asks the same plan as the lookup, from `options.registry`, running `pnpm add` with `--registry` and restoring the profile files between attempts; it moves on for the failures another registry can change and stops at any other, and at a failure whose error line names the host a git or tarball spec is fetched from, which no registry stands in for; `failedAt` says which of the two the last failed run could not reach. `cancelInstall(requestId)` stops the run and answers `cancelled` only after pnpm exited and the files are back, `too-late` once the bundle is being applied, and `not-running` for any other id; the install call then reports `application: 'cancelled'`. A run that fails, is cancelled, or adds a package without a bundle patch restores `package.json` and `pnpm-lock.yaml` as they were; `packageResult.kind` classifies the last run from its exit and output, `registries` names every registry asked, and `bundle` names the package a finished run added. `listBundles` carries each bundle's one-liner (the package `description`), the rows its patch declares with their live entries, and the built-in rows it overrides; it lists the profile's own bundles, the bundles the installation supplies, and a selected name without a bundle patch as a `not-bundle` problem, while an unselected plain dependency is left out. A bundle the launcher's `OPTIONAL_BUNDLES` names is `optional`: shipped switched off for the person to turn on, never removable, and selected by no shipped template ([rationale](../../../.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.md)). Every completed operation emits `plugin-manager/changed`; a patch generation applied outside the manager, by HMR's watcher after a CLI or hand edit, announces nothing, so the page learns of it on its next read. +`installBundle` accepts a caller-generated `requestId`, under which `plugin-manager/install-log` streams each pnpm run's output and `plugin-manager/install-state` announces `installing`, `cancelling`, and `applying`; `installing` is announced once per registry asked, with the `attempt`'s registry, position, and the plan's length. The install asks the same plan as the lookup, from `options.registry`, running `pnpm add` with `--registry` and restoring the profile files between attempts; it moves on for the failures another registry can change and stops at any other, and at a failure whose error line names the host a git or tarball spec is fetched from, which no registry stands in for; `failedAt` says which of the two the last failed run could not reach. `cancelInstall(requestId)` stops the run and answers `cancelled` only after the Git check or pnpm exited and the files are back, `too-late` once the bundle is being applied, and `not-running` for any other id; the install call then reports `application: 'cancelled'`. A run that fails, is cancelled, or adds a package without a bundle patch restores `package.json` and `pnpm-lock.yaml` as they were; `packageResult.kind` classifies the last run from its exit and output, `registries` names every registry asked, and `bundle` names the package a finished run added. `listBundles` carries each bundle's one-liner (the package `description`), the rows its patch declares with their live entries, and the built-in rows it overrides; it lists the profile's own bundles, the bundles the installation supplies, and a selected name without a bundle patch as a `not-bundle` problem, while an unselected plain dependency is left out. A bundle the launcher's `OPTIONAL_BUNDLES` names is `optional`: shipped switched off for the person to turn on, never removable, and selected by no shipped template ([rationale](../../../.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.md)). Every completed operation emits `plugin-manager/changed`; a patch generation applied outside the manager, by HMR's watcher after a CLI or hand edit, announces nothing, so the page learns of it on its next read. `waitForInstall(requestId)` lets a client recover a lost response by waiting for the active installation, including its non-cancellable application phase. It returns the same outcome as the original call, or `null` if the request is not active. Completed results are not retained; `null` establishes neither success nor cancellation. @@ -61,6 +63,7 @@ When pnpm 11 blocks dependency scripts, the failed installation reports every pe |---|---|---| | `pnpmCommand` | `pnpm` | The pnpm executable name or path, resolved through `PATH` like the `dsh plugin` command. | | `inspectTimeoutMs` | `20000` | Bound on one registry lookup an inspection runs, in milliseconds. | +| `githubConnectionTimeoutMs` | `5000` | Deadline for the GitHub repository check before installation, in milliseconds. | | `registry` | pnpm's own | The registry lookups and installations ask first, as an http(s) URL; absent, the one pnpm's own configuration names. | | `fallbackRegistries` | `['https://registry.npmmirror.com/']` | Registries asked in turn, as http(s) URLs, while the one before is unreachable or holds no copy of the package; pnpm's own registry joins the order only while it names npm's own registry or one of these. | | `outputBytes` | `16384` | Maximum pnpm diagnostic bytes returned per operation; the full output remains in the returned log path. | diff --git a/packages/boot/plugin-manager/README.zh.md b/packages/boot/plugin-manager/README.zh.md index f7502aab64..657e4587e2 100644 --- a/packages/boot/plugin-manager/README.zh.md +++ b/packages/boot/plugin-manager/README.zh.md @@ -45,11 +45,13 @@ kind: "package-reference" `inspect(spec, options)` 在任何东西安装之前读出 spec 指向什么:注册表包名通过 `pnpm view` 询问注册表,在 profile 目录中运行,因而与安装使用同样的代理与认证设置;绝对路径读取其 `package.json`;git 地址或 tarball 只答复自己的形式和它被拉取的 `host`。答复携带名称、版本、描述、该包是否声明组合包,以及作答的 `registry`,否则给出 `problem`:`invalid-spec`、`already-installed`、`not-found`、`not-a-package`、`not-a-bundle`、`network` 或 `unknown`,并附上问过的 `registries`。调用方的 `signal` 或 `inspectTimeoutMs` 会结束查询。 +`installBundle` 在启动 pnpm 前通过 `git ls-remote` 检查 GitHub 仓库,使用 profile 目录及安装器的 Git 与代理配置。`githubConnectionTimeoutMs` 默认为 5000 毫秒,只限制这次检查,不限制包下载或构建。检查禁用凭据助手和认证提示;只有网络失败与超时会停止安装,通过现有失败类型与诊断日志返回,并标记 `failedAt: 'spec-host'`。认证、仓库查找及其他失败继续交给 pnpm,包括其 HTTPS 到 SSH 的回退。取消安装或销毁管理器会停止检查及其子进程。注册表包、本地路径、压缩包和其他 Git 主机跳过此检查。仓库可达后,下载或组合包验证仍可能失败。 + 注册表按顺序询问。计划从 `options.registry` 开始,否则从配置的 `registry`(`null` 即 pnpm 自身配置指定的那个)开始,并在一个注册表不可达、超时或答复没有这个包或版本(尚未同步的镜像会如此)时继续问 `fallbackRegistries`。配置集合之外的注册表只问它自己,因而私有源永远不会落到公共源;pnpm 自身的注册表只在它指向的地址(每次做计划前经 `pnpm config get registry` 读出)是 npm 官方源或某个备选源时才算集合成员,否则视为私有源只问它自己。pnpm 自身配置已经指向的注册表只问一次。查询以 `--registry` 运行 `pnpm view` 且不带 pnpm 自身的重试,所以死掉的注册表会在 `inspectTimeoutMs` 内报告并转问下一个;pnpm 把拒绝以 JSON 打印在 stdout,读法与 stderr 相同。安装保留 pnpm 的重试设置。`registries()` 把配置集合和 pnpm 指向的地址答复给选择器。[注册表 Agent Note](../../../.agents/notes/implemented/architecture/2026-09-18-plugin-install-registries.zh.md) 拥有理由。 可在浏览器使用的 `@deepseek-ai/dsh-plugin-manager/registry` 入口导出 `OFFICIAL_NPM_REGISTRY` 和 `NPMMIRROR_REGISTRY`,供消费方识别这两个公共注册表。 -`installBundle` 接受调用方生成的 `requestId`,`plugin-manager/install-log` 在其下流式转发每次 pnpm 运行的输出,`plugin-manager/install-state` 通告 `installing`、`cancelling` 与 `applying`;每问一个注册表通告一次 `installing`,`attempt` 带上这次的注册表、序号和计划长度。安装沿用查询的计划,从 `options.registry` 开始,以 `--registry` 运行 `pnpm add`,两次尝试之间恢复 profile 文件;换一个注册表能改变的失败才转问下一个,其他失败即停,错误行点名 git 或 tarball spec 自身拉取主机的失败也停,因为没有注册表能替代那台主机;`failedAt` 说明最后一次失败的运行连不上的是二者中的哪一个。`cancelInstall(requestId)` 停止运行,只在 pnpm 退出且文件恢复后答复 `cancelled`,组合包已在应用时答复 `too-late`,其他 id 答复 `not-running`;安装调用随后报告 `application: 'cancelled'`。失败、被取消或装入了没有组合包 patch 的包的运行,会把 `package.json` 与 `pnpm-lock.yaml` 恢复原样;`packageResult.kind` 按退出方式与输出对最后一次运行分类,`registries` 列出问过的每个注册表,`bundle` 给出完成的运行新增的包。`listBundles` 携带每个组合包的一句话简介(包的 `description`)、其 patch 声明的行及其存活条目,以及它覆盖的内置行;它列出 profile 自己的组合包、安装提供的组合包,以及被选中却没有组合包 patch 的名字(作为 `not-bundle` 问题),未选中的普通依赖不列出。启动器的 `OPTIONAL_BUNDLES` 点名的组合包是 `optional`:随安装提供、默认关闭、由用户开启,永不可卸载,也不被任何随附模板选中([理由](../../../.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.zh.md))。每个完成的操作都会发出 `plugin-manager/changed`;在管理器之外应用的一代 patch(HMR 监视到 CLI 或手工编辑后)不发通知,页面要到下一次读取才知道。 +`installBundle` 接受调用方生成的 `requestId`,`plugin-manager/install-log` 在其下流式转发每次 pnpm 运行的输出,`plugin-manager/install-state` 通告 `installing`、`cancelling` 与 `applying`;每问一个注册表通告一次 `installing`,`attempt` 带上这次的注册表、序号和计划长度。安装沿用查询的计划,从 `options.registry` 开始,以 `--registry` 运行 `pnpm add`,两次尝试之间恢复 profile 文件;换一个注册表能改变的失败才转问下一个,其他失败即停,错误行点名 git 或 tarball spec 自身拉取主机的失败也停,因为没有注册表能替代那台主机;`failedAt` 说明最后一次失败的运行连不上的是二者中的哪一个。`cancelInstall(requestId)` 停止运行,只在 Git 检查或 pnpm 退出且文件恢复后答复 `cancelled`,组合包已在应用时答复 `too-late`,其他 id 答复 `not-running`;安装调用随后报告 `application: 'cancelled'`。失败、被取消或装入了没有组合包 patch 的包的运行,会把 `package.json` 与 `pnpm-lock.yaml` 恢复原样;`packageResult.kind` 按退出方式与输出对最后一次运行分类,`registries` 列出问过的每个注册表,`bundle` 给出完成的运行新增的包。`listBundles` 携带每个组合包的一句话简介(包的 `description`)、其 patch 声明的行及其存活条目,以及它覆盖的内置行;它列出 profile 自己的组合包、安装提供的组合包,以及被选中却没有组合包 patch 的名字(作为 `not-bundle` 问题),未选中的普通依赖不列出。启动器的 `OPTIONAL_BUNDLES` 点名的组合包是 `optional`:随安装提供、默认关闭、由用户开启,永不可卸载,也不被任何随附模板选中([理由](../../../.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.zh.md))。每个完成的操作都会发出 `plugin-manager/changed`;在管理器之外应用的一代 patch(HMR 监视到 CLI 或手工编辑后)不发通知,页面要到下一次读取才知道。 `waitForInstall(requestId)` 让客户端在响应丢失后等待活动安装的结果,包括不可取消的应用阶段。它返回与原调用相同的结果,请求不在活动中时返回 `null`。已完成的结果不保留;`null` 不表示成功或已取消。 @@ -61,6 +63,7 @@ pnpm 11 拦下依赖脚本时,失败的安装在 `pendingBuilds` 里报告 pro |---|---|---| | `pnpmCommand` | `pnpm` | pnpm 可执行文件名或路径,与 `dsh plugin` 命令一样通过 `PATH` 解析。 | | `inspectTimeoutMs` | `20000` | 单次检查所做注册表查询的上限,单位毫秒。 | +| `githubConnectionTimeoutMs` | `5000` | 安装前 GitHub 仓库连接检查的时限,单位毫秒。 | | `registry` | pnpm 自身配置 | 查询与安装首先询问的注册表,http(s) URL;缺省为 pnpm 自身配置指定的那个。 | | `fallbackRegistries` | `['https://registry.npmmirror.com/']` | 前一个注册表不可达或没有该包副本时依次询问的注册表,http(s) URL;pnpm 自身的注册表只在它指向 npm 官方源或这里的某一个时才进入顺序。 | | `outputBytes` | `16384` | 每次操作返回的 pnpm 诊断字节上限;完整输出保留在返回的日志路径中。 | diff --git a/packages/boot/plugin-manager/package.json b/packages/boot/plugin-manager/package.json index c3c59a10af..c96579daf0 100644 --- a/packages/boot/plugin-manager/package.json +++ b/packages/boot/plugin-manager/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-plugin-manager", "description": "Current-profile plugin and bundle management shared by dsh CLI, Web and agent tools", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/boot/plugin-manager/src/github-connection.ts b/packages/boot/plugin-manager/src/github-connection.ts new file mode 100644 index 0000000000..99b64fd73c --- /dev/null +++ b/packages/boot/plugin-manager/src/github-connection.ts @@ -0,0 +1,68 @@ +/** Bounded GitHub repository checks using the installer's Git configuration and environment. */ +import { mkdir, mkdtemp, open } from 'node:fs/promises' +import { join } from 'node:path' +import { execa } from 'execa' +import { scrubbedParentEnv } from '@deepseek-ai/dsh-subprocess' +import { classifyInstallFailure } from './install-failure.ts' +import type { ParsedInstallSpec } from './install-spec.ts' +import type { PackageResult } from './types.ts' + +/** Deadline, diagnostics and cancellation for one read-only GitHub check. */ +export interface GithubConnectionOptions { + timeoutMs: number + outputBytes: number + signal: AbortSignal + /** The application-owned package manager's environment, also used by its Git children. */ + env?: Readonly> +} + +/** + * Check a GitHub repository before pnpm starts, without downloading or building its package. + * Git reads the profile's Git and proxy configuration without invoking credential helpers or prompting. + * Timeout and cancellation terminate its descendants too; pnpm owns authentication and transport fallback. + * @param spec The parsed installation address; npm packages, local paths and other hosts are not checked. + * @param dir The profile directory where installation runs. + * @param options The connection deadline, output bound and operation cancellation. + * @returns A failed check with bounded output and a complete log at logPath, or undefined for a reachable repository or an unhandled spec. + */ +export async function checkGithubConnection( + spec: ParsedInstallSpec, dir: string, options: GithubConnectionOptions, +): Promise { + if (spec.kind !== 'git') return undefined + const host = spec.host.toLowerCase().replace(/:.*$/, '') + if (host !== 'github.com' && !host.endsWith('.github.com')) return undefined + const repository = spec.spec.replace(/#.*$/s, '') + .replace(/^github:/i, 'https://github.com/') + .replace(/^gist:/i, 'https://gist.github.com/') + .replace(/^git\+/i, '') + const logRoot = join(dir, '.plugin-manager', 'logs') + await mkdir(logRoot, { recursive: true, mode: 0o700 }) + const logDir = await mkdtemp(join(logRoot, 'github-connection-')) + const logPath = join(logDir, 'git.log') + const log = await open(logPath, 'ax+', 0o600) + try { + const result = await execa('git', ['-c', 'credential.helper=', 'ls-remote', '--', repository, 'HEAD'], { + cwd: dir, + env: { + ...scrubbedParentEnv(), ...options.env, LC_ALL: 'C', GIT_TERMINAL_PROMPT: '0', + GIT_ASKPASS: '', SSH_ASKPASS: '', SSH_ASKPASS_REQUIRE: 'never', + }, + extendEnv: false, + stdin: 'ignore', stdout: 'ignore', stderr: { file: logPath, append: true }, buffer: false, reject: false, + timeout: options.timeoutMs, cancelSignal: options.signal, killDescendants: true, killSignal: 'SIGKILL', + }) + if (!result.failed) return undefined + if (result.timedOut) await log.write(`dsh: connection to ${spec.host} timed out after ${String(options.timeoutMs)}ms\n`) + if ((await log.stat()).size === 0) await log.write(result.shortMessage ?? 'GitHub connection check failed') + const { size } = await log.stat() + const bytes = Buffer.alloc(Math.min(size, options.outputBytes)) + await log.read(bytes, 0, bytes.length, size - bytes.length) + const output = bytes.toString('utf8') + return { + exitCode: result.exitCode ?? (result.code === 'ENOENT' ? 127 : 1), output, truncated: size > options.outputBytes, logPath, + kind: classifyInstallFailure({ log: output, timedOut: result.timedOut }), + } + } finally { + await log.close() + } +} diff --git a/packages/boot/plugin-manager/src/index.ts b/packages/boot/plugin-manager/src/index.ts index 876adf9750..93d0287b9e 100644 --- a/packages/boot/plugin-manager/src/index.ts +++ b/packages/boot/plugin-manager/src/index.ts @@ -24,6 +24,7 @@ import { attributeFailure, normalizeRegistry, NPMMIRROR_REGISTRY, registryPlan } import { writePluginEnabled } from './patch.ts' import { ManagementFailure } from './failure.ts' import { approveBuilds, readPendingBuilds } from './build-approval.ts' +import { checkGithubConnection } from './github-connection.ts' import type { BundleInfo, BundleRowInfo, ChangeResult, InspectOptions, InstallBundleOptions, ManagementError, PackageResult, PluginChange, PluginEntryId, PluginInfo, PluginInspectProblem, PluginInstallCancellation, PluginInstallProgress, PluginInstallRequestId, @@ -33,16 +34,18 @@ export type * from './types.ts' export { classifyInstallFailure, type InstallFailureFacts } from './install-failure.ts' export { InvalidInstallSpecError, parseInstallSpec, type ParsedInstallSpec } from './install-spec.ts' -/** The pnpm executable, the registries asked, and the limits for package diagnostics and registry lookups. */ +/** The pnpm executable, registries, and limits for diagnostics, lookups and connection checks. */ export interface Config { /** The pnpm executable name or path; resolved through `PATH` like the `dsh plugin` command. */ pnpmCommand?: string - /** Maximum retained pnpm diagnostic bytes per operation. */ + /** Maximum retained package-operation diagnostic bytes. */ outputBytes?: number /** Maximum time to wait for another process's profile package operation. */ lockWaitMs?: number /** Bound on one registry lookup an inspection runs, in milliseconds. */ inspectTimeoutMs?: number + /** Maximum duration of the GitHub repository connection check before installation, in milliseconds. */ + githubConnectionTimeoutMs?: number /** The registry lookups and installations ask first, as an http(s) URL; absent, the one pnpm's own configuration names. */ registry?: string /** @@ -172,6 +175,7 @@ export class PluginManager extends TypertRemoteService { outputBytes: z.number().step(1).min(1).default(16384), lockWaitMs: z.number().step(1).min(0).default(120000), inspectTimeoutMs: z.number().step(1).min(1000).default(20000), + githubConnectionTimeoutMs: z.number().step(1).min(1000).default(5000), registry: z.string().pattern(REGISTRY_URL), fallbackRegistries: z.array(z.string().pattern(REGISTRY_URL)).default([NPMMIRROR_REGISTRY]), }) @@ -183,6 +187,7 @@ export class PluginManager extends TypertRemoteService { private readonly outputBytes: number private readonly lockWaitMs: number private readonly inspectTimeoutMs: number + private readonly githubConnectionTimeoutMs: number private readonly pnpmCommand: string private readonly configuredRegistries: Omit private readonly ownerContext: Context @@ -199,6 +204,7 @@ export class PluginManager extends TypertRemoteService { this.outputBytes = (config as Required).outputBytes this.lockWaitMs = (config as Required).lockWaitMs this.inspectTimeoutMs = (config as Required).inspectTimeoutMs + this.githubConnectionTimeoutMs = (config as Required).githubConnectionTimeoutMs this.pnpmCommand = (config as Required).pnpmCommand this.configuredRegistries = { registry: config.registry === undefined ? null : normalizeRegistry(config.registry), @@ -406,7 +412,9 @@ export class PluginManager extends TypertRemoteService { } /** - * Install a package using the same pnpm implementation as dsh plugin. A run + * Install a package using the same pnpm implementation as dsh plugin. GitHub + * repositories get a connection check bounded by githubConnectionTimeoutMs before pnpm starts; + * only network failures or timeouts stop installation, while pnpm owns authentication and transport fallback. A run * that fails, is cancelled, or adds a package without a bundle patch restores * `package.json` and `pnpm-lock.yaml` as they were; downloaded files can stay. * @param spec One package spec, including local paths relative to the invocation directory. @@ -418,7 +426,7 @@ export class PluginManager extends TypertRemoteService { installBundle(spec: string, options?: InstallBundleOptions): Promise { const requestId = options?.requestId const control: InstallControl = { abort: new AbortController(), phase: 'installing', result: Promise.resolve(null) } - const stopped = (): boolean => control.abort.signal.aborted + const stopped = (): boolean => control.abort.signal.aborted || this.abort.signal.aborted if (requestId !== undefined) this.installs.set(requestId, control) const announce = (phase: PluginInstallProgress['phase'], attempt?: PluginInstallProgress['attempt']): void => { if (requestId !== undefined) this.ownerContext.emit('plugin-manager/install-state', { requestId, phase, ...attempt === undefined ? {} : { attempt } }) @@ -432,12 +440,27 @@ export class PluginManager extends TypertRemoteService { } const files = await this.readRestoredFiles() const before = readProfileManifest('dsh', this.profile.dir).dependencies ?? {} - const plan = registryPlan(options?.registry, await this.registries()) let name: string try { - // The last run is the result's; the registries asked stay listed whatever the outcome. - let run: PackageResult | undefined result.registries = [] + const connection = checkGithubConnection(parsedForRegistry(spec), this.profile.dir, { + timeoutMs: this.githubConnectionTimeoutMs, outputBytes: this.outputBytes, + signal: AbortSignal.any([this.abort.signal, control.abort.signal]), + ...this.profile.packageManager?.env === undefined ? {} : { env: this.profile.packageManager.env }, + }) + this.packageOperations.add(connection) + let connectionFailure: PackageResult | undefined + try { connectionFailure = await connection } + finally { this.packageOperations.delete(connection) } + if (stopped()) throw new InstallCancelledError() + if (connectionFailure?.kind === 'network' || connectionFailure?.kind === 'timeout') { + result.packageResult = connectionFailure + result.failedAt = 'spec-host' + throw new Error(connectionFailure.output) + } + // The last run is the result's; the registries asked stay listed whatever the outcome. + const plan = registryPlan(options?.registry, await this.registries()) + let run: PackageResult | undefined for (const [index, registry] of plan.entries()) { if (index > 0) await this.restoreFiles(files) // A stop that landed while the files went back, or before the first run, starts no run with a dead signal. @@ -509,7 +532,7 @@ export class PluginManager extends TypertRemoteService { /** Stop an installation this manager owns and wait until its files are back. * @param requestId The id the installation was started with. - * @returns `cancelled` once pnpm exited and the files are restored, `too-late` once the bundle is being + * @returns `cancelled` once the Git check or pnpm exited and the files are restored, `too-late` once the bundle is being * applied, `not-running` for any other id. */ @Remote diff --git a/packages/boot/plugin-manager/src/install-failure.ts b/packages/boot/plugin-manager/src/install-failure.ts index 47a68c920a..009eeb1ad9 100644 --- a/packages/boot/plugin-manager/src/install-failure.ts +++ b/packages/boot/plugin-manager/src/install-failure.ts @@ -25,6 +25,7 @@ const LOG_KINDS: readonly [PluginInstallFailureKind, RegExp][] = [ ['disk-full', /\bENOSPC\b|no space left on device/i], ['permission', /\bEACCES\b|\bEPERM\b|permission denied/i], ['integrity', /ERR_PNPM_TARBALL_INTEGRITY|ERR_PNPM_BAD_TARBALL_SIZE|\bEINTEGRITY\b/], + ['unknown', /The requested URL returned error: 40[134]\b/], ['network', /\bENOTFOUND\b|\bECONNRESET\b|\bETIMEDOUT\b|\bECONNREFUSED\b|\bEAI_AGAIN\b|ERR_PNPM_META_FETCH_FAIL|ERR_PNPM_FETCH_5\d\d|ERR_PNPM_FETCH_TIMEOUT|\bFETCH_ERROR\b|socket hang up|Could not resolve host|unable to access/], ] diff --git a/packages/boot/plugin-manager/src/types.ts b/packages/boot/plugin-manager/src/types.ts index 7743b4f35f..4eb78b96f6 100644 --- a/packages/boot/plugin-manager/src/types.ts +++ b/packages/boot/plugin-manager/src/types.ts @@ -69,7 +69,7 @@ export interface PluginRegistries { readonly resolved: string | null } -/** How a pnpm run failed, read off how it ended and what it printed. */ +/** How a package operation failed, read off how it ended and what it printed. */ export type PluginInstallFailureKind = | 'pnpm-missing' | 'timeout' @@ -82,7 +82,7 @@ export type PluginInstallFailureKind = | 'integrity' | 'unknown' -/** Pnpm completion, including a retrieval path for unabridged diagnostics. */ +/** Package operation completion, including Git checks and a retrieval path for unabridged diagnostics. */ export interface PackageResult { exitCode: number output: string diff --git a/packages/boot/plugin-manager/tests/github-connection.spec.ts b/packages/boot/plugin-manager/tests/github-connection.spec.ts new file mode 100644 index 0000000000..673fb7c733 --- /dev/null +++ b/packages/boot/plugin-manager/tests/github-connection.spec.ts @@ -0,0 +1,183 @@ +/** Real Git transport deadlines, process cleanup, and bounded connection diagnostics. */ +import { once } from 'node:events' +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { appendFileSync } from 'node:fs' +import { createServer, type Socket } from 'node:net' +import { createServer as createHttpServer } from 'node:http' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { pathToFileURL } from 'node:url' +import * as subprocess from 'execa' +import { expect, it, onTestFinished, vi } from 'vitest' +import { checkGithubConnection } from '../src/github-connection.ts' +import { parseInstallSpec } from '../src/install-spec.ts' + +vi.mock('execa', async (original) => { + const module = await original() + return { ...module, execa: vi.fn(module.execa) } +}) + +async function fixture() { + const dir = await mkdtemp(join(tmpdir(), 'dsh-github-check-')) + onTestFinished(() => rm(dir, { recursive: true, force: true })) + const controller = new AbortController() + const tasks: Promise[] = [] + onTestFinished(async () => { controller.abort(); await Promise.allSettled(tasks) }) + const config = join(dir, 'git.config') + await writeFile(config, '') + const env = { GIT_CONFIG_GLOBAL: config, GIT_CONFIG_NOSYSTEM: '1' } + const check = (spec: string, timeoutMs = 3000, outputBytes = 4096) => { + const task = checkGithubConnection(parseInstallSpec(spec), dir, { + timeoutMs, outputBytes, signal: controller.signal, + env, + }) + tasks.push(task) + return task + } + return { dir, config, controller, check, env } +} + +async function proxy(config: string, fail: boolean) { + const sockets = new Set() + const connected = Promise.withResolvers() + const disconnected = Promise.withResolvers() + const server = createServer((socket) => { + sockets.add(socket) + socket.on('error', () => {}) + socket.once('close', () => { sockets.delete(socket); disconnected.resolve(undefined) }) + socket.once('data', () => { connected.resolve(undefined); if (fail) socket.destroy() }) + }) + onTestFinished(async () => { + for (const socket of sockets) socket.destroy() + await new Promise((resolve, reject) => { server.close((error) => { if (error) reject(error); else resolve() }) }) + }) + server.listen(0, '127.0.0.1') + await once(server, 'listening') + const address = server.address() + if (address === null || typeof address === 'string') throw new Error('proxy did not bind a TCP port') + await writeFile(config, `[http "https://github.com/"]\n proxy = http://127.0.0.1:${String(address.port)}\n`) + return { connected: connected.promise, disconnected: disconnected.promise, sockets } +} + +it('leaves registry packages and other Git hosts to the installer', async () => { + const { check, dir } = await fixture() + expect(await check('@acme/plugin')).toBeUndefined() + expect(await check('https://gitlab.com/acme/plugin.git')).toBeUndefined() + await expect(readFile(join(dir, '.plugin-manager', 'logs'))).rejects.toMatchObject({ code: 'ENOENT' }) +}) + +it.each([ + ['https://github.com/acme/plugin.git#main', 'https://github.com/'], + ['github:acme/plugin#main', 'https://github.com/'], + ['git+https://github.com/acme/plugin.git#main', 'https://github.com/'], + ['git+ssh://git@github.com:22/acme/plugin.git#main', 'ssh://git@github.com:22/'], + ['git@github.com:acme/plugin.git#main', 'git@github.com:'], + ['gist:acme/plugin#main', 'https://gist.github.com/'], +])('checks %s through the profile Git configuration without downloading a package', async (spec, prefix) => { + const { check, dir, config } = await fixture() + const repository = join(dir, 'empty.git') + await subprocess.execa('git', ['init', '--bare', repository]) + // Match both forms exactly; a fragment reaching Git would fail to resolve this repository. + await writeFile(config, `[url "${pathToFileURL(repository).href}"]\n insteadOf = ${prefix}acme/plugin.git\n insteadOf = ${prefix}acme/plugin\n`) + expect(await check(spec)).toBeUndefined() +}) + +it('reports an immediate transport failure without waiting for the deadline', async () => { + const { check, config } = await fixture() + const transport = await proxy(config, true) + const result = await check('https://github.com/acme/plugin.git') + await transport.connected + expect(result).toMatchObject({ exitCode: 128, kind: 'network', truncated: false }) + expect(await readFile(result!.logPath, 'utf8')).toBe(result!.output) + await transport.disconnected + expect(transport.sockets.size).toBe(0) +}) + +it('ends a silent proxy connection and its Git child at the connection deadline', async () => { + const { check, config } = await fixture() + const transport = await proxy(config, false) + const running = check('https://github.com/acme/plugin.git') + await transport.connected + const result = await running + expect(result?.kind).toBe('timeout') + expect(result!.output).toContain('connection to github.com timed out after 3000ms') + await transport.disconnected + expect(transport.sockets.size).toBe(0) +}) + +it('cancels a connected Git probe and closes its transport before the deadline', async () => { + const { check, config, controller } = await fixture() + const transport = await proxy(config, false) + const running = check('https://github.com/acme/plugin.git', 60000) + await transport.connected + controller.abort() + const result = await running + expect(result?.kind).toBe('unknown') + await transport.disconnected + expect(transport.sockets.size).toBe(0) +}) + +it.each([undefined, 'Git could not start'])('retains a diagnostic when Git writes no stderr (%s)', async (shortMessage) => { + const { check } = await fixture() + const run = vi.spyOn(subprocess, 'execa').mockResolvedValue({ failed: true, shortMessage } as Awaited>) + onTestFinished(() => { run.mockRestore() }) + const result = await check('github:acme/plugin') + expect(result).toMatchObject({ exitCode: 1, kind: 'unknown', output: shortMessage ?? 'GitHub connection check failed' }) + expect(await readFile(result!.logPath, 'utf8')).toBe(result!.output) +}) + +it('bounds returned diagnostics while retaining Git stderr in the operation log', async () => { + const { check } = await fixture() + const run = vi.spyOn(subprocess, 'execa').mockImplementation((...args: unknown[]) => { + const options = args[2] as { stderr: { file: string } } + appendFileSync(options.stderr.file, '0123456789') + return Promise.resolve({ failed: true, exitCode: 128 }) as ReturnType + }) + onTestFinished(() => { run.mockRestore() }) + const result = await check('github:acme/plugin', 3000, 4) + expect(result).toMatchObject({ output: '6789', truncated: true }) + expect(await readFile(result!.logPath, 'utf8')).toBe('0123456789') +}) + +it('reports a missing Git executable without classifying it as a network failure', async () => { + const { check } = await fixture() + const run = vi.spyOn(subprocess, 'execa').mockResolvedValue({ failed: true, code: 'ENOENT', shortMessage: 'spawn git ENOENT' } as Awaited>) + onTestFinished(() => { run.mockRestore() }) + expect(await check('github:acme/plugin')).toMatchObject({ exitCode: 127, kind: 'unknown', output: 'spawn git ENOENT' }) +}) + +it('does not invoke configured credential helpers or askpass for an authentication challenge', async () => { + const { check, dir, config, env } = await fixture() + const server = createHttpServer((_request, response) => { + response.writeHead(401, { 'WWW-Authenticate': 'Basic realm="fixture"' }) + response.end() + }) + onTestFinished(async () => { + server.closeAllConnections() + await new Promise((resolve, reject) => { server.close((error) => { if (error) reject(error); else resolve() }) }) + }) + server.listen(0, '127.0.0.1') + await once(server, 'listening') + const address = server.address() + if (address === null || typeof address === 'string') throw new Error('server did not bind a TCP port') + const marker = join(dir, 'prompted') + const helper = join(dir, 'credential-helper.cjs') + await writeFile(helper, `require('node:fs').appendFileSync(${JSON.stringify(marker)}, process.argv.slice(2).join(' ') + '\\n')`) + const helperCommand = `"${process.execPath.replaceAll('\\', '/')}" "${helper.replaceAll('\\', '/')}"` + const askpass = join(dir, 'askpass.sh') + await writeFile(askpass, `#!/bin/sh\nexec ${helperCommand} "$@"\n`, { mode: 0o700 }) + await writeFile(config, `[url "http://127.0.0.1:${String(address.port)}/"]\n insteadOf = https://github.com/\n[http]\n proxy =\n`) + await subprocess.execa('git', ['config', '--file', config, 'credential.helper', `!${helperCommand}`]) + await subprocess.execa('git', ['config', '--file', config, 'core.askPass', askpass]) + Object.assign(env, { GIT_ASKPASS: askpass, SSH_ASKPASS: askpass }) + // The same Git configuration really prompts without the probe's noninteractive overrides. + await subprocess.execa('git', ['ls-remote', '--', 'https://github.com/acme/private.git', 'HEAD'], { + cwd: dir, env: { ...env, GIT_TERMINAL_PROMPT: '0' }, reject: false, + }) + const prompted = await readFile(marker, 'utf8') + expect(prompted).toContain('get') + expect(prompted).toContain('Username') + await rm(marker) + expect(await check('github:acme/private.git')).toMatchObject({ exitCode: 128, kind: 'unknown' }) + await expect(readFile(marker)).rejects.toMatchObject({ code: 'ENOENT' }) +}) diff --git a/packages/boot/plugin-manager/tests/install-spec.spec.ts b/packages/boot/plugin-manager/tests/install-spec.spec.ts index c45d461e67..10a4a76398 100644 --- a/packages/boot/plugin-manager/tests/install-spec.spec.ts +++ b/packages/boot/plugin-manager/tests/install-spec.spec.ts @@ -56,6 +56,10 @@ describe('parseInstallSpec', () => { }) describe('classifyInstallFailure', () => { + it.each([401, 403, 404])('leaves Git HTTP %s responses out of connection recovery', (status) => { + expect(classifyInstallFailure({ log: `fatal: unable to access 'https://github.com/acme/private.git/': The requested URL returned error: ${String(status)}` })).toBe('unknown') + }) + it('names the run\'s end before reading its output, then the most specific code in the output', () => { expect(classifyInstallFailure({ log: 'ENOSPC', timedOut: true })).toBe('timeout') expect(classifyInstallFailure({ log: '', cause: Object.assign(new Error('spawn pnpm ENOENT'), { code: 'ENOENT' }) })).toBe('pnpm-missing') diff --git a/packages/boot/plugin-manager/tests/manager.spec.ts b/packages/boot/plugin-manager/tests/manager.spec.ts index 16fbed9650..5c372a07a8 100644 --- a/packages/boot/plugin-manager/tests/manager.spec.ts +++ b/packages/boot/plugin-manager/tests/manager.spec.ts @@ -1,9 +1,13 @@ /** Persistent manager behavior through a real profile Include and Loader. */ import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { realpath } from 'node:fs/promises' +import { once } from 'node:events' +import { createServer } from 'node:http' +import type { Socket } from 'node:net' import { join } from 'node:path' import { tmpdir } from 'node:os' import { fileURLToPath, pathToFileURL } from 'node:url' +import { execa } from 'execa' import type { Context } from '@deepseek-ai/cordis' import { expect, it, onTestFinished, vi } from 'vitest' import { @@ -17,6 +21,7 @@ import Timer from '@deepseek-ai/cordis-plugin-timer' import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include' import { Group } from '@deepseek-ai/cordis-plugin-loader' import * as operations from '../src/operations.ts' +import * as githubConnection from '../src/github-connection.ts' import { parse, parseDocument } from 'yaml' async function fixture(reload: 'live' | 'startup' = 'live', overlay = false, prepare?: (ctx: Context) => void, config: Config = {}, packageManager?: ProfileContext['packageManager'], prepareFiles?: (dir: string) => void) { @@ -60,7 +65,8 @@ async function fixture(reload: 'live' | 'startup' = 'live', overlay = false, pre }) // What pnpm's own configuration names is read from pnpm before every registry plan; the fixture answers npm's own registry. const registry = vi.spyOn(operations, 'readProfileRegistry').mockResolvedValue('https://registry.npmjs.org/') - onTestFinished(() => { registry.mockRestore() }) + const connection = vi.spyOn(githubConnection, 'checkGithubConnection').mockResolvedValue(undefined) + onTestFinished(() => { registry.mockRestore(); connection.mockRestore() }) let stopHmr = async () => {} if (reload === 'live') { await ctx.plugin(Timer) @@ -68,9 +74,155 @@ async function fixture(reload: 'live' | 'startup' = 'live', overlay = false, pre stopHmr = () => owner.dispose() await ctx.hmr.runExclusive(async () => {}) } - return { ctx, dir, manager: ctx.pluginManager, bundle, profile, stopHmr, overlays } + return { ctx, dir, manager: ctx.pluginManager, bundle, profile, stopHmr, overlays, connection } } +it.each(['network', 'timeout'] as const)('stops a GitHub %s before pnpm and attributes it to the repository', async (kind) => { + const { manager, dir, connection } = await fixture() + const before = readFileSync(join(dir, 'package.json'), 'utf8') + const failure = { exitCode: 1, output: 'GitHub check failed', truncated: false, logPath: join(dir, 'git.log'), kind } + connection.mockResolvedValue(failure) + const pnpm = vi.spyOn(operations, 'runProfilePnpm') + onTestFinished(() => { pnpm.mockRestore() }) + expect(await manager.installBundle('https://github.com/acme/dsh-plugin.git')).toMatchObject({ + application: 'failed', changed: false, stage: 'install', failedAt: 'spec-host', packageResult: failure, + }) + expect(pnpm).not.toHaveBeenCalled() + expect(connection).toHaveBeenCalledWith(expect.objectContaining({ host: 'github.com' }), dir, expect.objectContaining({ timeoutMs: 5000 })) + expect(readFileSync(join(dir, 'package.json'), 'utf8')).toBe(before) +}) + +it('leaves non-network GitHub errors to pnpm and forwards profile Git environment and deadline', async () => { + const env = { GIT_CONFIG_GLOBAL: '/application/git.config' } + const { manager, connection } = await fixture('live', false, undefined, { githubConnectionTimeoutMs: 8000 }, { command: 'pnpm', args: [], env }) + connection.mockResolvedValue({ exitCode: 128, output: 'fatal: Authentication failed', truncated: false, logPath: 'git.log', kind: 'unknown' }) + const failure = { exitCode: 1, output: 'pnpm owns authentication', truncated: false, logPath: 'pnpm.log', kind: 'unknown' as const } + const pnpm = vi.spyOn(operations, 'runProfilePnpm').mockResolvedValue(failure) + onTestFinished(() => { pnpm.mockRestore() }) + const result = await manager.installBundle('github:acme/private-plugin') + expect(result.application).toBe('failed') + expect(result.packageResult).toEqual(failure) + expect(pnpm).toHaveBeenCalledOnce() + expect(result.failedAt).toBeUndefined() + expect(connection).toHaveBeenCalledWith(expect.anything(), expect.anything(), expect.objectContaining({ timeoutMs: 8000, env })) +}) + +it('cancels an active GitHub check before starting pnpm', async () => { + const { manager, connection } = await fixture() + const entered = Promise.withResolvers() + connection.mockImplementation(async (_spec, _dir, options) => { + entered.resolve(undefined) + await new Promise((resolve) => { options.signal.addEventListener('abort', () => { resolve() }, { once: true }) }) + return { exitCode: 1, output: 'cancelled', truncated: false, logPath: 'git.log', kind: 'unknown' } + }) + const pnpm = vi.spyOn(operations, 'runProfilePnpm') + onTestFinished(() => { pnpm.mockRestore() }) + const requestId = '824103ec-bc45-489d-bb85-5b4fe0aefc78' as PluginInstallRequestId + const installing = manager.installBundle('github:acme/dsh-plugin', { requestId }) + await entered.promise + expect(await manager.cancelInstall(requestId)).toEqual({ status: 'cancelled' }) + expect(await installing).toMatchObject({ application: 'cancelled', changed: false }) + expect(pnpm).not.toHaveBeenCalled() +}) + +it('waits for the GitHub check before installing and activating the bundle', async () => { + const { manager, dir, bundle, connection } = await fixture() + const entered = Promise.withResolvers() + const release = Promise.withResolvers() + connection.mockImplementation(async () => { entered.resolve(undefined); await release.promise; return undefined }) + const pnpm = vi.spyOn(operations, 'runProfilePnpm').mockImplementation(async () => { + bundle('addon', []) + const manifest = readProfileManifest('test', dir) + manifest.dependencies = { ...manifest.dependencies, addon: '1.0.0' } + writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest)) + return { exitCode: 0, output: 'installed', truncated: false, logPath: join(dir, 'pnpm.log') } + }) + const installing = manager.installBundle('github:acme/addon') + onTestFinished(async () => { release.resolve(undefined); await installing; pnpm.mockRestore() }) + await entered.promise + expect(pnpm).not.toHaveBeenCalled() + release.resolve(undefined) + expect(await installing).toMatchObject({ application: 'applied', changed: true, bundle: 'addon' }) + expect(readProfileManifest('test', dir).dsh?.profile?.bundles).toContain('addon') +}) + +it('disposal waits for an active GitHub check to stop', async () => { + const { ctx, manager, connection } = await fixture('startup') + const entered = Promise.withResolvers() + const released = Promise.withResolvers() + connection.mockImplementation(async (_spec, _dir, options) => { + entered.resolve(undefined) + await new Promise((resolve) => { options.signal.addEventListener('abort', () => { resolve() }, { once: true }) }) + released.resolve(undefined) + return { exitCode: 1, output: 'cancelled', truncated: false, logPath: 'git.log', kind: 'unknown' } + }) + const pnpm = vi.spyOn(operations, 'runProfilePnpm') + onTestFinished(() => { pnpm.mockRestore() }) + const installing = manager.installBundle('github:acme/addon') + await entered.promise + await ctx.fiber.dispose() + await released.promise + expect(await installing).toMatchObject({ application: 'cancelled', changed: false }) + expect(pnpm).not.toHaveBeenCalled() +}) + +it.each(['github:acme/connected', 'https://github.com/acme/connected.git', 'git+ssh://git@github.com/acme/connected.git'])( + 'installs %s through real Git and pnpm with private repository SSH fallback', async (spec) => { + const pnpm = fileURLToPath(new URL('../../../../apps/desktop/node_modules/pnpm/bin/pnpm.mjs', import.meta.url)) + const env = { GIT_CONFIG_GLOBAL: '', GIT_CONFIG_NOSYSTEM: '1' } + const { manager, dir, connection } = await fixture('startup', false, undefined, {}, { + command: process.execPath, args: ['--expose-internals', pnpm], env, + }) + connection.mockRestore() + const repository = join(dir, 'repository') + mkdirSync(repository) + env.GIT_CONFIG_GLOBAL = join(dir, 'git.config') + writeFileSync(env.GIT_CONFIG_GLOBAL, '') + const git = (args: string[]) => execa('git', args, { cwd: repository, env }) + await git(['init', '--initial-branch=main']) + const name = '@test/github-connected' + writeFileSync(join(repository, 'package.json'), JSON.stringify({ name, version: '1.0.0', dsh: { bundle: { patch: './cordis.patch.yml' } } })) + writeFileSync(join(repository, 'cordis.patch.yml'), '[]\n') + await git(['add', '.']) + await git(['-c', 'user.name=Fixture', '-c', 'user.email=fixture@example.invalid', '-c', 'commit.gpgsign=false', 'commit', '-m', 'fixture bundle']) + const requests: string[] = [] + const sockets = new Set() + const proxy = createServer((_request, response) => { + response.writeHead(401, { 'WWW-Authenticate': 'Basic realm="fixture"' }) + response.end() + }) + proxy.on('connection', (socket) => { + sockets.add(socket) + socket.once('close', () => { sockets.delete(socket) }) + }) + proxy.on('connect', (request, socket) => { + requests.push(request.url ?? '') + socket.end('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n') + }) + onTestFinished(async () => { + for (const socket of sockets) socket.destroy() + await new Promise((resolve, reject) => { proxy.close((error) => { if (error) reject(error); else resolve() }) }) + }) + proxy.listen(0, '127.0.0.1') + await once(proxy, 'listening') + const address = proxy.address() + if (address === null || typeof address === 'string') throw new Error('proxy did not bind a TCP port') + const proxyUrl = `http://127.0.0.1:${String(address.port)}` + writeFileSync(env.GIT_CONFIG_GLOBAL, `[url "${pathToFileURL(repository).href}"]\n insteadOf = ssh://git@github.com/acme/connected.git\n insteadOf = git@github.com:acme/connected.git\n[url "${proxyUrl}/"]\n insteadOf = https://github.com/\n[http]\n proxy =\n`) + // pnpm probes HTTPS with Node before falling back to SSH, even for an SSH spec. + // Route that HTTP request to our proxy too; Git's URL rewrite alone cannot isolate it. + const manifest = readProfileManifest('test', dir) + delete manifest.dependencies + writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest)) + writeFileSync(join(dir, '.npmrc'), `store-dir=${join(dir, 'store').replaceAll('\\', '/')}\nhttps-proxy=${proxyUrl}\nproxy=${proxyUrl}\nnoproxy=\n`) + const result = await manager.installBundle(spec, { enabled: false }) + expect(result.error).toBeUndefined() + expect(result).toMatchObject({ changed: true, bundle: name, packageResult: { exitCode: 0 } }) + expect(readProfileManifest('test', dir).dependencies).toHaveProperty(name) + expect(requests).toContain('github.com:443') + }, +) + it.each(['missing package', 'invalid manifest', 'not a bundle', 'missing patch', 'invalid patch'])( 'boots with %s, lists its error, and permits deselection without enabling the broken bundle', async (failure) => { const { manager, dir, ctx } = await fixture('live', false, undefined, {}, undefined, (dir) => { diff --git a/packages/browser-use/browser-use/package.json b/packages/browser-use/browser-use/package.json index b24eef351a..02974247ef 100644 --- a/packages/browser-use/browser-use/package.json +++ b/packages/browser-use/browser-use/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-browser-use", "description": "Exclusive named browser-use provider registration", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/acp-app/package.json b/packages/bundle/acp-app/package.json index eeba4e1a6f..fd79e0747f 100644 --- a/packages/bundle/acp-app/package.json +++ b/packages/bundle/acp-app/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-acp-app", "description": "The dsh ACP profile bundle: automation-only JSON-RPC stdio and process lifecycle over dsh-base", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/base/package.json b/packages/bundle/base/package.json index 39fdcf615b..9bac1d952d 100644 --- a/packages/bundle/base/package.json +++ b/packages/bundle/base/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-base", "description": "The shared dsh core as a profile bundle: the first patch layer of base-backed profiles, inserting core rows over the empty profile root", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/headless/package.json b/packages/bundle/headless/package.json index 6f00ed63b9..01c6559068 100644 --- a/packages/bundle/headless/package.json +++ b/packages/bundle/headless/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-headless", "description": "The dsh one-shot bundle: a direct core Agent/Session runner over dsh-base with no Host, HTTP, or browser layer", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/sdk-app/package.json b/packages/bundle/sdk-app/package.json index b2b892d218..dc76a874fa 100644 --- a/packages/bundle/sdk-app/package.json +++ b/packages/bundle/sdk-app/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sdk-app", "description": "The dsh SDK profile bundle: stdio JSON-RPC serving and process lifecycle over dsh-base", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/sdk-minimal/package.json b/packages/bundle/sdk-minimal/package.json index d52ce98483..d62fa60d58 100644 --- a/packages/bundle/sdk-minimal/package.json +++ b/packages/bundle/sdk-minimal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sdk-minimal", "description": "The standalone minimal SDK profile bundle: JSON-RPC, one DeepSeek adapter, persistent shell, and JSONL sessions", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/web-app/package.json b/packages/bundle/web-app/package.json index 63e3b62b92..291cc0e58f 100644 --- a/packages/bundle/web-app/package.json +++ b/packages/bundle/web-app/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-web-app", "description": "The dsh browser-surface bundle: the web patch layer over dsh-base plus the runtime glue plugin (frontend dist serving, web-surface prompt, bash runtime variables, URL line)", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/AGENTS.md b/packages/client/AGENTS.md index 1a729d3fa5..68b17f21c8 100644 --- a/packages/client/AGENTS.md +++ b/packages/client/AGENTS.md @@ -110,7 +110,7 @@ One UI feature = one plugin package (`src/client/` browser half). A multi-domain ## Styling and localization -[docs/web-styling.md](../../docs/web-styling.md) is authoritative. Shared `--dsw-*` tokens and global sheets live in `ui-theme/src/styles/`; feature components consume semantic aliases through CSS Modules and `clsx`, with no literal colors, component library, or Tailwind. Code comments are English. +[docs/web-styling.md](../../docs/web-styling.md) is authoritative. Shared `--dsw-*` tokens and global sheets live in `ui-theme/src/styles/`; feature components consume semantic aliases through CSS Modules and `clsx`, with no literal colors, component library, or Tailwind. Code comments are English. Design judgment beyond those rules — feedback surfaces, loading states, overlay safety, platform window adaptation — follows [dsh-client-ui-ux](../../.agents/skills/dsh-client-ui-ux/SKILL.md). Every product-visible string—including text, accessibility names, tooltips, placeholders, status/unit formatters, and primitive chrome—lives in a typed locale dictionary and reaches components through the standard `t` seat or an already-localized prop. Cordis-free primitives require complete label props and own no fallback copy. Keep user/model/wire data and code tokens verbatim; internal matching uses discriminants or stable ids, never localized text. `pnpm run verify-client-ui-i18n` enforces source ownership ([decision](../../.agents/notes/implemented/architecture/2026-08-23-locale-owned-client-ui-copy.md)). diff --git a/packages/client/connection/package.json b/packages/client/connection/package.json index fa72603fd9..572fc80588 100644 --- a/packages/client/connection/package.json +++ b/packages/client/connection/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-connection", "description": "Authenticated RPC transport and generation lifecycle", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/file-upload/package.json b/packages/client/file-upload/package.json index 5864ad1351..c46f823fdf 100644 --- a/packages/client/file-upload/package.json +++ b/packages/client/file-upload/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-file-upload", "description": "Agent-scoped browser file upload, streaming intake, and staged receipt service", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/hmr/package.json b/packages/client/hmr/package.json index 424309561b..7a94aabed6 100644 --- a/packages/client/hmr/package.json +++ b/packages/client/hmr/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-hmr", "description": "Web client graph synchronization and rebuilt-bundle reload transport", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/locale/package.json b/packages/client/locale/package.json index d735544f58..a57fdc884f 100644 --- a/packages/client/locale/package.json +++ b/packages/client/locale/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-locale", "description": "Locale plugin: Host-backed preference, extensible language catalog, browser fallback, and typed built-in dictionaries", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/modules/package.json b/packages/client/modules/package.json index 575a24a18c..d9cd2df64a 100644 --- a/packages/client/modules/package.json +++ b/packages/client/modules/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-modules", "description": "Client module system, dual-face: node half composes the __DSH_BOOT__ entry graph (incremental dsh.client scan, bundle route, index tap, webPlugins service); browser half is the lazy-CJS module table the vendored cordis Loader consumes as its internal seam", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/resources/package.json b/packages/client/resources/package.json index d9e90b0775..cf6c67bcd0 100644 --- a/packages/client/resources/package.json +++ b/packages/client/resources/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-resources", "description": "Unified client resource model: protocol-registered providers turn URL addresses into live values, consumed through the useResource global standard hook", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/store/package.json b/packages/client/store/package.json index 52ff624a38..3a8a609385 100644 --- a/packages/client/store/package.json +++ b/packages/client/store/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-store", "description": "React-free observable and snapshot-store contracts with the shared Zustand/Immer engine", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-agent-preset/package.json b/packages/client/ui-agent-preset/package.json index 64a3623375..785c41cbe5 100644 --- a/packages/client/ui-agent-preset/package.json +++ b/packages/client/ui-agent-preset/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-agent-preset", "description": "Agent-preset surfaces: the default for later sessions, this session's seat, and the composition editor", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-approval/package.json b/packages/client/ui-approval/package.json index 2ef38180c0..2159fa5af9 100644 --- a/packages/client/ui-approval/package.json +++ b/packages/client/ui-approval/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-approval", "description": "Approval composer takeover over the scoped Remote Event waterfall", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-attachment/package.json b/packages/client/ui-attachment/package.json index 4bf338ddee..ec5cab12d3 100644 --- a/packages/client/ui-attachment/package.json +++ b/packages/client/ui-attachment/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-attachment", "description": "Dynamic attachment presentation plugin for conversation input, message-image, and trajectory image slots", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-brand-official/package.json b/packages/client/ui-brand-official/package.json index 87bca8ebff..822b612aff 100644 --- a/packages/client/ui-brand-official/package.json +++ b/packages/client/ui-brand-official/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-brand-official", "description": "Official DeepSeek Harness brand occupants for the Web client's sidebar slots", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-chat/package.json b/packages/client/ui-chat/package.json index d7aeb15a14..61174be32d 100644 --- a/packages/client/ui-chat/package.json +++ b/packages/client/ui-chat/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-chat", "description": "Chat Conversation target, node definitions, renderers, and details surface", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-commands/package.json b/packages/client/ui-commands/package.json index b6f845f7d5..b2eb3b8b28 100644 --- a/packages/client/ui-commands/package.json +++ b/packages/client/ui-commands/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-commands", "description": "Client command surface: global directory cache, '/' source, three command UI kinds, popupSelect registry", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-conversation/package.json b/packages/client/ui-conversation/package.json index 4b950d9b3d..7641d94c74 100644 --- a/packages/client/ui-conversation/package.json +++ b/packages/client/ui-conversation/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-conversation", "description": "Target-neutral Conversation assembly, shell, composer, queue, and view navigation", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-conversation/src/client/queue/QueueDock.tsx b/packages/client/ui-conversation/src/client/queue/QueueDock.tsx index f8c7154ddd..2aa9056341 100644 --- a/packages/client/ui-conversation/src/client/queue/QueueDock.tsx +++ b/packages/client/ui-conversation/src/client/queue/QueueDock.tsx @@ -285,7 +285,7 @@ export function QueueDock({ useSession, useProjection, updateQueue, notify, load {editing?.id === row.id ? ( <> - + - + - + - + - + )} {showResume && ( - + )} - + - + diff --git a/packages/client/ui-goal/tests/goalbar.client.spec.tsx b/packages/client/ui-goal/tests/goalbar.client.spec.tsx index c321ccf5ff..d73443d67c 100644 --- a/packages/client/ui-goal/tests/goalbar.client.spec.tsx +++ b/packages/client/ui-goal/tests/goalbar.client.spec.tsx @@ -151,6 +151,20 @@ describe('GoalBar', () => { expect(actions.onResume).toHaveBeenCalledTimes(1) }) + it('portals an action tooltip out of the strip, where the input card cannot cover it', () => { + vi.useFakeTimers() + try { + render() + fireEvent.mouseEnter(screen.getByRole('button', { name: '暂停目标' })) + act(() => { vi.advanceTimersByTime(500) }) + const tooltip = screen.getByRole('tooltip') + expect(tooltip.textContent).toBe('暂停目标') + expect(tooltip.parentElement).toBe(document.body) + } finally { + vi.useRealTimers() + } + }) + it('a new goal identity drops the edit form (no stale draft over the new goal)', () => { const actions = makeActions() const { rerender } = render() diff --git a/packages/client/ui-input-trigger/package.json b/packages/client/ui-input-trigger/package.json index deb1a83286..6267aff57c 100644 --- a/packages/client/ui-input-trigger/package.json +++ b/packages/client/ui-input-trigger/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-input-trigger", "description": "Input trigger pipeline: '/' and '@' detection, candidate menu, pick routing to registered sources", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-jobs/package.json b/packages/client/ui-jobs/package.json index 6c60248fba..021b4d2ac6 100644 --- a/packages/client/ui-jobs/package.json +++ b/packages/client/ui-jobs/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-jobs", "description": "Session-header background-job list with on-demand streaming record panels", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "type": "module", "main": "lib/index.js", "types": "lib/types/index.d.ts", diff --git a/packages/client/ui-layout/package.json b/packages/client/ui-layout/package.json index 40f9feb045..cf4b95f168 100644 --- a/packages/client/ui-layout/package.json +++ b/packages/client/ui-layout/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-layout", "description": "Shell plugin: three-column AppFrame with drag handles, ctx.layout viewing-state service (navigation + panels)", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-message-feedback/package.json b/packages/client/ui-message-feedback/package.json index fc4c3c4647..b60cdadcfd 100644 --- a/packages/client/ui-message-feedback/package.json +++ b/packages/client/ui-message-feedback/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-message-feedback", "description": "The Web feedback surface: per-message Like/Dislike in the assistant-message action strip and the feedback dialog behind both ratings and /feedback, backed by the messageFeedback and sessionFeedback Host Remotes", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-model-selection/README.i18n.yaml b/packages/client/ui-model-selection/README.i18n.yaml index 10481b46f8..9dccc32faa 100644 --- a/packages/client/ui-model-selection/README.i18n.yaml +++ b/packages/client/ui-model-selection/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-model-selection/README.md -README.md: 37253cf04242275f22d646da0e65cfd44f4fefbf -README.zh.md: 4e339e6a6c6db8c3125e6ccc19c0fc3af9947f88 +README.md: 60a1491779cda03daac53cecf3fe1597e6930414 +README.zh.md: b1d297877bfe4f112e07e357f836b4cb06608077 diff --git a/packages/client/ui-model-selection/README.md b/packages/client/ui-model-selection/README.md index 37253cf042..60a1491779 100644 --- a/packages/client/ui-model-selection/README.md +++ b/packages/client/ui-model-selection/README.md @@ -27,6 +27,8 @@ The Web GUI lets users switch the model and reasoning effort for an existing ses Mount this plugin alongside `ui-conversation` and the commands package; the composer then shows the model seat next to the pending indicator, and `/model` opens the same directory as a popup. While the seat's menu is open, `↑`/`↓` move focus across the rows of the shown pane, Tab settles the focused row, and Escape and `Shift+Tab` leave a drilled pane first and otherwise close back to the trigger. Drilling lands on the row of the value in use, and going back lands on the cell that opened the pane left. Both surfaces show the host-reported current selection when the exact provider/model pair remains in the advertised groups; a missing catalog row leaves the routable selection intact while the trigger prompts `Select model`. +Mouse selection uses native browser clicks, including their cancellation behavior; a press alone never selects. Opening the menu focuses its trigger, and clicking the trigger again closes the menu and returns focus there. While a selection is pending, focus stays on the trigger; a rejected selection leaves the menu open, and Tab returns to the current row. + ### Model and effort Models stay grouped by provider. The composer menu shows model and effort names only. The `/model` popup shows provider names and catalog descriptions; it localizes the two built-in DeepSeek descriptions and leaves external provider descriptions verbatim. The popup applies the selected model's default effort; the composer can then choose any advertised effort. An adapter without reasoning metadata leaves the Effort row absent; there is no arbitrary effort input. diff --git a/packages/client/ui-model-selection/README.zh.md b/packages/client/ui-model-selection/README.zh.md index 4e339e6a6c..b1d297877b 100644 --- a/packages/client/ui-model-selection/README.zh.md +++ b/packages/client/ui-model-selection/README.zh.md @@ -27,6 +27,8 @@ Web GUI 允许用户通过 `/model` 弹窗或 composer 模型控件切换既有 与 `ui-conversation` 及命令包一起挂载本插件;composer 随即在待处理指示器旁显示模型位,`/model` 则以弹窗打开同一份目录。模型位菜单打开期间,`↑`/`↓` 在所显示面板的行间移动焦点,`Tab` 选定聚焦行,Escape 与 `Shift+Tab` 先退出已下钻的面板,否则关闭并回到触发器。下钻落在正在使用的那一行,返回则落在打开该面板的格子上。当确切提供方/模型对仍在已公布分组中时,两个界面都显示 Host 报告的当前选择;目录行缺席时,可路由的选择保持不变,触发器提示 `Select model`。 +鼠标选择沿用浏览器原生点击及其取消行为;仅按下按钮不会选定。打开菜单时聚焦触发按钮,再次点击触发按钮会关闭菜单并把焦点还给它。等待选择结果时,焦点停在触发按钮;选择被拒绝后菜单保持打开,Tab 可回到当前选中的行。 + ### 模型与推理强度 模型按提供方分组。composer 菜单只显示模型与推理强度名称。`/model` 弹窗显示提供方名称与目录说明;其中两个内置 DeepSeek 模型的说明使用当前语言,外部提供方说明保持原文。弹窗应用所选模型的默认推理强度;composer 随后可以选择任一已公布的推理强度。适配器没有推理元数据时不显示 Effort 行;不存在任意推理强度输入。 diff --git a/packages/client/ui-model-selection/package.json b/packages/client/ui-model-selection/package.json index 9344d5c997..15c9158526 100644 --- a/packages/client/ui-model-selection/package.json +++ b/packages/client/ui-model-selection/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-model-selection", "description": "Model selection over the shared model catalog, Session projection, and session.selectModel", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-model-selection/src/client/ModelSelect.tsx b/packages/client/ui-model-selection/src/client/ModelSelect.tsx index b9c8993563..c7db6f1c0b 100644 --- a/packages/client/ui-model-selection/src/client/ModelSelect.tsx +++ b/packages/client/ui-model-selection/src/client/ModelSelect.tsx @@ -190,6 +190,7 @@ export function ModelSelect( if (!available) return null const show = (): void => { + triggerRef.current?.focus() setPane('root') setOpen(true) reload() @@ -291,13 +292,19 @@ export function ModelSelect( }) } + const submit = (selection: ModelSelection): void => { + lastActionRef.current = 'select' + // Disabled option rows cannot retain focus while a selection is pending. + triggerRef.current?.focus() + void select(selection).then(settleSelection) + } + const choose = (selection: ModelSelection): void => { if (state.current?.provider === selection.provider && state.current.model === selection.model) { close(true) return } - lastActionRef.current = 'select' - void select(selection).then(settleSelection) + submit(selection) } const chooseEffort = (effort: string | undefined): void => { @@ -311,8 +318,7 @@ export function ModelSelect( model: state.current.model, ...effort === undefined ? {} : { reasoningEffort: effort }, } - lastActionRef.current = 'select' - void select(selection).then(settleSelection) + submit(selection) } const waiting = state.current === null && state.status === 'loading' @@ -336,7 +342,16 @@ export function ModelSelect( } return ( -
+
{ + // WebKit blurs a focused row before click unless the button's mousedown keeps focus. + if (event.target instanceof Element && event.target.closest('button') !== null) event.preventDefault() + }} + > + + + )} + /> + ) + } if (phase === 'idle' || phase === 'checking') { const checking = phase === 'checking' const empty = install.spec.trim() === '' @@ -799,7 +817,7 @@ function InstallDialog({ onClose={onClose} title={t('installTitle')} closeLabel={t('close')} - description={t('installDescription')} + {...install.mirrorRecovery ? {} : { description: t('installDescription') }} className={css.installDialog as string} contentClassName={css.installContent as string} footer={( @@ -813,10 +831,11 @@ function InstallDialog({
{ onEditSpec(event.currentTarget.value) }} @@ -1321,6 +1340,7 @@ export function PluginManagerPage(props: PluginManagerPageProps): ReactNode { onToggleRegistry={props.toggleRegistryOptions} onChooseRegistry={props.chooseRegistry} onChangeRegistry={props.changeRegistry} + onUseGithubMirror={props.useGithubMirror} /> {state.confirm === null ? null diff --git a/packages/client/ui-plugin-manager/src/client/locales.ts b/packages/client/ui-plugin-manager/src/client/locales.ts index 30116aa2bf..97e7309c77 100644 --- a/packages/client/ui-plugin-manager/src/client/locales.ts +++ b/packages/client/ui-plugin-manager/src/client/locales.ts @@ -87,6 +87,11 @@ export const zh = { installingTitle: '插件安装中…', installedTitle: '已安装', installFailedTitle: '插件安装失败', + installGithubFailedTitle: '无法访问 GitHub', + installGithubTimeoutTitle: '连接 GitHub 超时', + installGithubFailedDescription: '请尝试其他安装来源。', + installUseGithubMirror: '改用国内镜像', + installPackageLabel: '插件包名', installEdit: '编辑', installEditAria: '返回编辑', installCancelAndEdit: '取消安装并返回编辑', @@ -271,6 +276,11 @@ export const en = { installingTitle: 'Installing the plugin…', installedTitle: 'Installed', installFailedTitle: 'The plugin could not be installed', + installGithubFailedTitle: 'Cannot access GitHub', + installGithubTimeoutTitle: 'GitHub connection timed out', + installGithubFailedDescription: 'Try another installation source.', + installUseGithubMirror: 'Use mainland China mirror', + installPackageLabel: 'Plugin package name', installEdit: 'Edit', installEditAria: 'Back to editing', installCancelAndEdit: 'Cancel installation and return to editing', diff --git a/packages/client/ui-plugin-manager/src/client/manager-store.ts b/packages/client/ui-plugin-manager/src/client/manager-store.ts index 6b55628588..4fe666b5b6 100644 --- a/packages/client/ui-plugin-manager/src/client/manager-store.ts +++ b/packages/client/ui-plugin-manager/src/client/manager-store.ts @@ -147,6 +147,8 @@ export interface InstallState { readonly open: boolean /** The package spec as typed. */ readonly spec: string + /** The spec form was reopened after switching away from a failed GitHub address. */ + readonly mirrorRecovery?: boolean /** The registries the Host configured, read when the dialog opens; null until the Host answered. */ readonly registries: PluginRegistries | null /** The registry this install asks first: the one last used, else the Host's first. */ @@ -204,6 +206,20 @@ export function isInstallPending(phase: InstallState['phase']): boolean { return phase === 'starting' || phase === 'running' || phase === 'cancelling' || phase === 'applying' || phase === 'unconfirmed' } +/** + * Offer the configured mainland mirror after a confirmed GitHub connection failure. + * @param install - the installation and the Host's failure attribution. + * @returns the offered mirror URL, or undefined when this recovery does not apply. + */ +export function githubRecoveryRegistry(install: InstallState): string | undefined { + if (install.phase !== 'failed' || install.failure?.failedAt !== 'spec-host' + || (install.failure.kind !== 'network' && install.failure.kind !== 'timeout')) return undefined + const host = install.subject?.host?.toLowerCase().split(':')[0] + if (host !== 'github.com' && !host?.endsWith('.github.com')) return undefined + return offeredRegistries(install.registries).find((registry): registry is string => + registry !== null && new URL(registry).hostname === 'registry.npmmirror.com') +} + /** A destructive action waiting for the user's confirmation: a package's uninstall. */ export interface ConfirmState { readonly action: 'uninstall' @@ -254,6 +270,8 @@ export interface PluginManagerFace { chooseRegistry: (choice: RegistryChoice) => void /** From the failed screen: back to the spec with the registry options unfolded. */ changeRegistry: () => void + /** Return from a GitHub connection failure to an empty spec with the offered mainland mirror selected. */ + useGithubMirror: () => void /** Allow the install scripts the failed run left pending, saved for this profile, and run the same spec again. */ approveBuildsAndRetry: () => void /** Leave the check or the failed screen for the spec, or ask the Host to stop the run and wait for its cleanup. */ @@ -374,8 +392,8 @@ const IDLE_INSTALL: InstallState = { /** The dialog back at its spec: the run and its outcome forgotten, the spec and the registries kept. */ function specAgain(install: InstallState): InstallState { - const { open, spec, registries, registry } = install - return { ...IDLE_INSTALL, open, spec, registries, registry } + const { open, spec, registries, registry, mirrorRecovery } = install + return { ...IDLE_INSTALL, open, spec, registries, registry, ...mirrorRecovery === undefined ? {} : { mirrorRecovery } } } /** @@ -515,6 +533,14 @@ export class PluginManagerController { const install = this.getSnapshot().install if (install.phase === 'failed') this.patch({ install: { ...specAgain(install), registryOpen: true } }) }, + useGithubMirror: () => { + const install = this.getSnapshot().install + const mirror = githubRecoveryRegistry(install) + if (mirror === undefined) return + const registry: RegistryChoice = { kind: 'offered', registry: mirror } + this.registryMemory.set(registry) + this.patch({ install: { ...specAgain(install), spec: '', mirrorRecovery: true, registry } }) + }, approveBuildsAndRetry: () => { void this.approveBuildsAndRetry() }, cancelInstall: () => { void this.cancelInstall() }, reconcileInstall: () => { void this.reconcileInstall() }, diff --git a/packages/client/ui-plugin-manager/tests/components.client.spec.tsx b/packages/client/ui-plugin-manager/tests/components.client.spec.tsx index 4e13674dd1..906578bc85 100644 --- a/packages/client/ui-plugin-manager/tests/components.client.spec.tsx +++ b/packages/client/ui-plugin-manager/tests/components.client.spec.tsx @@ -106,6 +106,7 @@ function renderTab( toggleRegistryOptions: vi.fn(), chooseRegistry: vi.fn(), changeRegistry: vi.fn(), + useGithubMirror: vi.fn(), approveBuildsAndRetry: vi.fn(), enableInstalled: vi.fn(), clearHighlight: vi.fn(), @@ -1056,6 +1057,63 @@ describe('PluginManagerPage', () => { expect(screen.getByText(en.installSubjectTarball)).toBeTruthy() }) + it.each(['network', 'timeout'] as const)('shows GitHub recovery only after a %s failure and returns to package input', (kind) => { + const subject = { spec: 'github:a/b', status: 'accepted', kind: 'git', bundle: null, registry: null, host: 'github.com' } as const + const failed: InstallState = { + ...IDLE_INSTALL, open: true, spec: subject.spec, registries: REGISTRIES, phase: 'failed', subject, + failure: { reason: 'GitHub connection failed', kind, failedAt: 'spec-host' }, + } + const { actions, set, setLanguage } = renderTab({ install: { ...failed, phase: 'idle', failure: null } }) + expect(screen.queryByText(en.installGithubFailedTitle)).toBeNull() + set({ install: failed }) + const dialog = screen.getByRole('dialog', { name: kind === 'timeout' ? en.installGithubTimeoutTitle : en.installGithubFailedTitle }) + expect(within(dialog).getByText(en.installGithubFailedDescription)).toBeTruthy() + expect(screen.queryByRole('button', { name: en.installRetry })).toBeNull() + fireEvent.click(within(dialog).getByRole('button', { name: en.installUseGithubMirror })) + expect(actions.useGithubMirror).toHaveBeenCalledOnce() + expect(actions.runInstall).not.toHaveBeenCalled() + fireEvent.click(within(dialog).getByRole('button', { name: en.cancel })) + expect(actions.closeInstall).toHaveBeenCalledOnce() + setLanguage(zh) + expect(screen.getByRole('dialog', { name: kind === 'timeout' ? '连接 GitHub 超时' : '无法访问 GitHub' })).toBeTruthy() + expect(screen.getByText('请尝试其他安装来源。')).toBeTruthy() + set({ install: { ...IDLE_INSTALL, open: true, mirrorRecovery: true, registries: REGISTRIES, registry: { kind: 'offered', registry: MIRROR } } }) + const form = within(screen.getByRole('dialog', { name: zh.installTitle })) + expect(form.queryByText(zh.installDescription)).toBeNull() + expect(form.queryByRole('textbox', { name: zh.installSpecLabel })).toBeNull() + const input = screen.getByRole('textbox', { name: zh.installPackageLabel }) + expect(input).toHaveProperty('value', '') + expect(document.activeElement).toBe(input) + expect(screen.getByRole('button', { name: zh.installRun })).toHaveProperty('disabled', true) + expect(form.getAllByRole('button')).toEqual([ + form.getByRole('button', { name: zh.close }), + form.getByRole('button', { name: zh.installGuideToggle }), + form.getByRole('button', { name: '安装源 中国大陆镜像源' }), + form.getByRole('button', { name: zh.installRun }), + ]) + }) + + it('keeps the ordinary failure view for registry errors, other hosts, and unavailable mirrors', () => { + const failed: InstallState = { + ...IDLE_INSTALL, open: true, registries: REGISTRIES, phase: 'failed', + subject: { spec: 'github:a/b', status: 'accepted', kind: 'git', bundle: null, registry: null, host: 'github.com' }, + failure: { reason: 'failed', kind: 'network', failedAt: 'registry' }, + } + const { set } = renderTab({ install: failed }) + expect(screen.getByRole('button', { name: en.installChangeRegistry })).toBeTruthy() + for (const patch of [ + { subject: { ...failed.subject!, host: 'gitlab.com' } }, + { subject: null }, + { registries: { ...REGISTRIES, fallbackRegistries: [] } }, + { registries: null }, + { failure: { reason: 'not found', kind: 'not-found' as const, failedAt: 'spec-host' as const } }, + ]) { + set({ install: { ...failed, failure: { reason: 'failed', kind: 'network', failedAt: 'spec-host' }, ...patch } }) + expect(screen.queryByRole('dialog', { name: en.installGithubFailedTitle })).toBeNull() + expect(screen.getByRole('button', { name: en.installRetry })).toBeTruthy() + } + }) + it('offers the registries under the spec, folded by default, and picks or types one', () => { const open = { ...IDLE_INSTALL, open: true, registries: REGISTRIES } const { actions, set } = renderTab({ install: open }) diff --git a/packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts b/packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts index ac022135ef..829d8b4fd4 100644 --- a/packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts +++ b/packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts @@ -1155,6 +1155,48 @@ describe('PluginManagerController', () => { face.changeRegistry() expect(state().install).toMatchObject({ phase: 'idle', spec: 'dsh-new', registryOpen: true, runs: [], failure: null }) }) + + it.each(['network', 'timeout'] as const)('recovers a GitHub %s failure without retrying its URL through a mirror', async (kind) => { + const spec = 'https://github.com/example/dsh-plugin.git' + const inspect = vi.fn().mockResolvedValueOnce(ok({ status: 'accepted', kind: 'git', bundle: null, registry: null, host: 'github.com' })) + .mockResolvedValue(ok({ ...INSPECTED, registry: MIRROR })) + const { face, state, plugins } = bench({ + inspect, + installBundle: vi.fn().mockResolvedValueOnce(ok({ + ...failed(undefined, { exitCode: 1, output: 'Could not resolve host: github.com', truncated: false, logPath: '/l', kind }), + failedAt: 'spec-host', + })).mockResolvedValueOnce(ok({ + ...failed(undefined, { exitCode: 1, output: 'Registry connection failed', truncated: false, logPath: '/l', kind: 'network' }), + failedAt: 'registry', + })).mockResolvedValue(ok({ ...APPLIED, bundle: 'dsh-new' })), + }) + face.openInstall() + await vi.waitFor(() => { expect(state().install.registries).toEqual(REGISTRIES) }) + face.editInstallSpec(spec) + face.useGithubMirror() + expect(state().install.spec).toBe(spec) + face.runInstall() + await vi.waitFor(() => { expect(state().install.phase).toBe('failed') }) + face.useGithubMirror() + expect(state().install).toMatchObject({ + phase: 'idle', open: true, spec: '', mirrorRecovery: true, registry: { kind: 'offered', registry: MIRROR }, + registryOpen: false, failure: null, runs: [], + }) + expect(plugins.installBundle).toHaveBeenCalledTimes(1) + face.editInstallSpec('dsh-new') + face.runInstall() + await vi.waitFor(() => { expect(state().install.phase).toBe('failed') }) + face.changeRegistry() + expect(state().install).toMatchObject({ phase: 'idle', spec: 'dsh-new', mirrorRecovery: true, registryOpen: true }) + face.runInstall() + await vi.waitFor(() => { expect(state().install.phase).toBe('done') }) + expect(plugins.inspect).toHaveBeenLastCalledWith('dsh-new', { registry: MIRROR }, expect.any(AbortSignal)) + expect(plugins.installBundle).toHaveBeenLastCalledWith('dsh-new', expect.objectContaining({ registry: MIRROR })) + face.closeInstall() + face.openInstall() + await vi.waitFor(() => { expect(state().install.registries).toEqual(REGISTRIES) }) + expect(state().install.registry).toEqual({ kind: 'offered', registry: MIRROR }) + }) }) describe('Host registry response recommendation', () => { diff --git a/packages/client/ui-primitives/README.i18n.yaml b/packages/client/ui-primitives/README.i18n.yaml index 7d548fb479..5cc185f648 100644 --- a/packages/client/ui-primitives/README.i18n.yaml +++ b/packages/client/ui-primitives/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-primitives/README.md -README.md: 5e4ce23cca1b602722c0a4db13f7d10d6ae95f65 -README.zh.md: 79b27a914a3c02b9546fb0af9ff40e8d923cf373 +README.md: ab2f0116cf2a92ac68a899b42242fb8de6473f67 +README.zh.md: b6b26f68afbf470e863104f54401343ccfe3b2fd diff --git a/packages/client/ui-primitives/README.md b/packages/client/ui-primitives/README.md index 5e4ce23cca..ab2f0116cf 100644 --- a/packages/client/ui-primitives/README.md +++ b/packages/client/ui-primitives/README.md @@ -52,7 +52,7 @@ Check this table before writing a control in a feature package. A plugin cannot | `Modal` | Centered dialog over a page mask. | | `RiskConfirmation` | Sensitive action gated behind an explicit checkbox. | | `OnboardingSurface` | First-run stage that holds the application root inert. | -| `Tooltip` | Hover text anchored to a cloned child; optional `portal` rendering escapes clipping containers. | +| `Tooltip` | Hover text anchored to a cloned child; optional `portal` rendering escapes clipping containers and ancestor stacking contexts that cap the bubble's z-index. | | `HoverCard` | Hover preview the pointer can rest on and select from; optional copy button. | | `Toast` | Transient top-center banner held for the owner's `holdMs`. | | `SettingsForm`, `SettingsValueField`, `SettingsSecretField` | The frame and the controls of a plugin's settings page: the frame takes its copy as `labels`, saves only on its button, and discards on unmount; a value field shows staged text with the overridden badge and reset; a secret field starts blank and reports only whether a value is configured. | diff --git a/packages/client/ui-primitives/README.zh.md b/packages/client/ui-primitives/README.zh.md index 79b27a914a..b6b26f68af 100644 --- a/packages/client/ui-primitives/README.zh.md +++ b/packages/client/ui-primitives/README.zh.md @@ -52,7 +52,7 @@ kind: "package-library" | `Modal` | 页面遮罩之上的居中对话框。 | | `RiskConfirmation` | 以显式复选框把关的敏感操作确认。 | | `OnboardingSurface` | 首次运行的引导舞台,期间保持应用根节点 inert。 | -| `Tooltip` | 锚定在克隆子元素上的悬停文本;可通过 `portal` 渲染到外层,避免被容器裁剪。 | +| `Tooltip` | 锚定在克隆子元素上的悬停文本;可通过 `portal` 渲染到外层,避免被容器裁剪,或受祖先层叠上下文限制其 z-index。 | | `HoverCard` | 指针可停留、可选中的悬停预览;可选带复制按钮。 | | `Toast` | 顶部居中的瞬时横幅,保持时长由所有者的 `holdMs` 决定。 | | `SettingsForm`、`SettingsValueField`、`SettingsSecretField` | 插件设置页的框架与控件:框架以 `labels` 接收文案,只在按钮点击时保存,卸载即丢弃;值字段显示暂存文本以及已覆盖标签和重置;密文字段每次为空,只报告是否已配置。 | diff --git a/packages/client/ui-primitives/package.json b/packages/client/ui-primitives/package.json index 0e4d01a714..52e29beb42 100644 --- a/packages/client/ui-primitives/package.json +++ b/packages/client/ui-primitives/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-primitives", "description": "Pure React atoms for the dsh web UI: controls, icons, markdown, and JSON inspectors (zero cordis)", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-primitives/src/Tooltip.tsx b/packages/client/ui-primitives/src/Tooltip.tsx index 93b06322ca..6ebd60dbd4 100644 --- a/packages/client/ui-primitives/src/Tooltip.tsx +++ b/packages/client/ui-primitives/src/Tooltip.tsx @@ -1,4 +1,4 @@ -/** Anchor-preserving tooltips with optional body portals for clipping containers. */ +/** Anchor-preserving tooltips; an optional body portal escapes clipping containers and stacking contexts that cap the bubble's z-index. */ import { cloneElement, createContext, useCallback, useContext, useEffect, useRef, useState } from 'react' import type { FocusEventHandler, MouseEventHandler, MutableRefObject, ReactElement, Ref } from 'react' @@ -46,7 +46,8 @@ if (typeof window !== 'undefined') { * @param props.align - horizontal anchor-edge alignment for 'bottom'/'top' bubbles: 'end' pins * the bubble's right edge to the anchor's (for anchors beside other hover surfaces the centered * bubble would overlap); default 'center'. Ignored for side 'right'. - * @param props.portal - render the bubble under document.body to escape containing blocks and clipping ancestors. + * @param props.portal - render the bubble under document.body, so an ancestor's clipping or its + * stacking context (which confines the bubble's z-index to that context) cannot hide it. * @param props.delayMs - hover delay in milliseconds; keyboard focus remains immediate. * @param props.disabled - suppress the bubble while true; the anchor renders identically so * toggling never remounts it (which would cut its CSS transitions). diff --git a/packages/client/ui-reference/package.json b/packages/client/ui-reference/package.json index 3d4a484a18..f3fb02b449 100644 --- a/packages/client/ui-reference/package.json +++ b/packages/client/ui-reference/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-reference", "description": "Unified Web @file and @session reference source", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-renderer/package.json b/packages/client/ui-renderer/package.json index 29e69c24df..77852c6550 100644 --- a/packages/client/ui-renderer/package.json +++ b/packages/client/ui-renderer/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-renderer", "description": "Browser UI renderer: React slot bindings, ctx.uiRenderer, and the assembled application root", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-schedule/package.json b/packages/client/ui-schedule/package.json index f032329d21..fd23490a2d 100644 --- a/packages/client/ui-schedule/package.json +++ b/packages/client/ui-schedule/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-schedule", "description": "Read-only active Schedule catalog in the Web Session header", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "type": "module", "main": "lib/index.js", "types": "lib/types/index.d.ts", diff --git a/packages/client/ui-session/package.json b/packages/client/ui-session/package.json index fe83d2616e..40d1884750 100644 --- a/packages/client/ui-session/package.json +++ b/packages/client/ui-session/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-session", "description": "Session Controller adapter for React and session-scoped slots", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-account/README.i18n.yaml b/packages/client/ui-settings-account/README.i18n.yaml index a6b62d6056..826301b3aa 100644 --- a/packages/client/ui-settings-account/README.i18n.yaml +++ b/packages/client/ui-settings-account/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-settings-account/README.md -README.md: be38bcd68ab7affe4aa840499ef79009ba25b779 -README.zh.md: 9429d21cbd988a538c18320acbbfcb65dd6acfc0 +README.md: 6155d54643f456395f1d2d377e0446377033df43 +README.zh.md: c9dd2f3627214972842924225bb2a08bbb505a7e diff --git a/packages/client/ui-settings-account/README.md b/packages/client/ui-settings-account/README.md index be38bcd68a..6155d54643 100644 --- a/packages/client/ui-settings-account/README.md +++ b/packages/client/ui-settings-account/README.md @@ -28,7 +28,7 @@ The section registers through settings.section and uses the account Remote names Balances use Platform Web currency formatting: two decimal places and digit grouping, positive amounts truncated to cents, positive sub-cent amounts shown as <0.01, and negative amounts rounded with a minimum displayed magnitude of 0.01. Raw Host balance strings remain unchanged. -Desktop usage and top-up actions open an isolated native Platform view below a 48px return bar. Back destroys the view and preserves the Account settings page. Loading failures retain the return action and show a centered localized error with Retry. Retry reloads the same destination; renderer commands never receive the account token. +Desktop usage and top-up actions open an isolated native Platform view below a 48px return bar. On Windows Electron the return bar and the native view start below the Desktop caption strip, so the Application and Edit menu text cannot cover the Back to DeepSeek Harness label. Back destroys the view and preserves the Account settings page. Loading failures retain the return action and show a centered localized error with Retry. Retry reloads the same destination; renderer commands never receive the account token. The account menu's Feedback entry opens the Feishu questionnaire in the system browser. It supplies the available build version, UI locale and physical screen resolution as prefill_* parameters, with hide_*=1 for every context field; the account UID, tokens and contact details are excluded. Configure contactFormUrl on the ui-settings-account plugin to select another HTTPS form. contactSource defaults to empty until the questionnaire supports a Harness source option; OS and device fields remain unfilled, matching the Web implementation. @@ -67,6 +67,8 @@ No model request prefix changes. - Profile and recharge-wallet balances use the existing Platform Web endpoints through Host getProfile / getBalance. The page refreshes when opened and after login or reconnect, preserves server-masked contact data, and shows query failures independently without manufacturing a zero balance. Usage and top-up use Host-provided links derived from platformOrigin and the browser’s own login; the links never carry a DSH token. +- The Platform view’s dialog marks every other document child inert, including the Desktop-owned caption menu host, so the Application and Edit menu stays visible but cannot be operated until the view closes. Exempting that host needs an inertness contract owned by the Desktop. + ### Dev Note diff --git a/packages/client/ui-settings-account/README.zh.md b/packages/client/ui-settings-account/README.zh.md index 9429d21cbd..c9dd2f3627 100644 --- a/packages/client/ui-settings-account/README.zh.md +++ b/packages/client/ui-settings-account/README.zh.md @@ -28,7 +28,7 @@ Desktop 设置中的账号页面显示 DeepSeek 登录状态,并提供浏览 余额沿用 Platform Web 的金额格式:两位小数和千分位分组,正金额截断至分,小于一分的正金额显示为 <0.01,负金额按舍入规则处理且显示绝对值至少为 0.01。Host 返回的原始余额字符串保持不变。 -Desktop 的用量和充值操作在 48px 返回栏下方打开隔离的原生 Platform 视图。返回操作销毁视图并保留 Account 设置页。加载失败时保留返回操作,居中显示本地化错误提示和“重试”按钮。重试重新加载同一目标页面;渲染进程命令不接收账号 token。 +Desktop 的用量和充值操作在 48px 返回栏下方打开隔离的原生 Platform 视图。在 Windows Electron 中,返回栏与原生视图都从 Desktop 顶栏下方开始,Application 和 Edit 菜单文字不会盖住“返回 DeepSeek Harness”。返回操作销毁视图并保留 Account 设置页。加载失败时保留返回操作,居中显示本地化错误提示和“重试”按钮。重试重新加载同一目标页面;渲染进程命令不接收账号 token。 账号菜单的“意见反馈”入口在系统浏览器中打开飞书问卷。链接通过 prefill_* 参数填写已有的构建版本、界面语言和屏幕物理分辨率,并为所有上下文字段设置 hide_*=1;不传账号 UID、token 或联系方式。可在 ui-settings-account 插件配置 contactFormUrl,切换到另一个 HTTPS 问卷。问卷支持 Harness 来源选项前,contactSource 默认为空;OS 和设备字段沿用 Web 实现,保持未填写。 @@ -67,6 +67,8 @@ Desktop 的用量和充值操作在 48px 返回栏下方打开隔离的原生 Pl - 资料和充值钱包余额通过 Host getProfile / getBalance 复用已有 Platform Web 接口。页面在打开、登录及重连后刷新,原样显示服务端脱敏后的联系方式,并分别处理查询失败,不将失败显示为零余额。用量和充值使用 Host 按 platformOrigin 派生的链接,并沿用浏览器自身登录态,不携带 DSH token。 +- Platform 视图的弹窗会把文档内其它子节点设为 inert,其中包括 Desktop 拥有的顶栏菜单 host,因此 Application 和 Edit 菜单保持可见,但在视图关闭前无法操作。放行该 host 需要 Desktop 侧提供 inert 契约。 + ### 开发备注 diff --git a/packages/client/ui-settings-account/package.json b/packages/client/ui-settings-account/package.json index 03a42eaff5..8dd7ccb9db 100644 --- a/packages/client/ui-settings-account/package.json +++ b/packages/client/ui-settings-account/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-account", "description": "Manage DeepSeek login and open Platform billing pages", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-account/src/client/PlatformOverlay.module.css b/packages/client/ui-settings-account/src/client/PlatformOverlay.module.css index 62084cf12f..39f0da5deb 100644 --- a/packages/client/ui-settings-account/src/client/PlatformOverlay.module.css +++ b/packages/client/ui-settings-account/src/client/PlatformOverlay.module.css @@ -7,6 +7,12 @@ background: var(--dsw-alias-bg-base); } +/* Windows Electron: the Desktop caption menu owns the window's top strip, so + the return bar and the native Platform view start below it. */ +:global([data-windows-titlebar]) .overlay { + padding-top: var(--dsh-windows-titlebar-height); +} + .header { box-sizing: border-box; height: 48px; diff --git a/packages/client/ui-settings-account/tests/platform-overlay-caption.client.spec.ts b/packages/client/ui-settings-account/tests/platform-overlay-caption.client.spec.ts new file mode 100644 index 0000000000..5df486ca8b --- /dev/null +++ b/packages/client/ui-settings-account/tests/platform-overlay-caption.client.spec.ts @@ -0,0 +1,24 @@ +/** + * The Windows caption clearance as CSS text. jsdom has no layout, so the + * Account specs pin the return bar's DOM but cannot show where it lands; this + * reads the declaration that keeps the Desktop-owned Application and Edit menu + * from covering the return bar. + */ +import { readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { expect, it } from 'vitest' + +const css = readFileSync(fileURLToPath(new URL('../src/client/PlatformOverlay.module.css', import.meta.url)), 'utf8') + +function declarations(selector: string): string[] { + const rule = new RegExp(`(?:^|[{}])\\s*${selector.replace(/[.[\]():*+^$\\]/g, '\\$&')}\\s*\\{([^{}]*)\\}`).exec(css.replace(/\/\*[\s\S]*?\*\//g, ' ')) + if (rule === null) throw new Error(`no \`${selector}\` rule`) + return (rule[1] ?? '').split(';').map(part => part.trim()).filter(Boolean) +} + +it('starts the return bar below the Desktop caption strip on Windows', () => { + // The strip and its clearance token both come from the Desktop preload's + // caption marker; without the offset the 48px return bar renders inside the + // strip that the fixed caption menu host also occupies. + expect(declarations(':global([data-windows-titlebar]) .overlay')).toContain('padding-top: var(--dsh-windows-titlebar-height)') +}) diff --git a/packages/client/ui-settings-agent-loop/package.json b/packages/client/ui-settings-agent-loop/package.json index 88b949811d..3d97598629 100644 --- a/packages/client/ui-settings-agent-loop/package.json +++ b/packages/client/ui-settings-agent-loop/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-agent-loop", "description": "Settings page of the agent loop on the dsh web client's Plugins page: the parallel tool-call cap of the agent-loop namespace", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-general/package.json b/packages/client/ui-settings-general/package.json index a00f40a4c9..c8430e7058 100644 --- a/packages/client/ui-settings-general/package.json +++ b/packages/client/ui-settings-general/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-general", "description": "Settings ownerless-copy and product onboarding plugin: the General section, shell trigger/header chrome content, settings dictionaries, and the versioned welcome notice", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-models/README.i18n.yaml b/packages/client/ui-settings-models/README.i18n.yaml index 8dcae71776..29a2070e75 100644 --- a/packages/client/ui-settings-models/README.i18n.yaml +++ b/packages/client/ui-settings-models/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-settings-models/README.md -README.md: e0accf787aba9d2857887773b4f1a3d703fc1fc1 -README.zh.md: 39b443c2293a9da0f7a82bc70d10539a2d153aa8 +README.md: 52a8e91c158db912a5c7a4f84ff2892de2bd4a7d +README.zh.md: 9470c9c0f1e80136e31e6e656b09c5fd17f7c691 diff --git a/packages/client/ui-settings-models/README.md b/packages/client/ui-settings-models/README.md index e0accf787a..52a8e91c15 100644 --- a/packages/client/ui-settings-models/README.md +++ b/packages/client/ui-settings-models/README.md @@ -25,7 +25,7 @@ English | [中文](README.zh.md) ## Use this package -Open the Models page from the Settings navigation to see every configured provider as a row. A whole-section provider whose key is not configured anywhere renders as its open setup card instead, but only in the first-run posture and only until the user closes that card. Each card kind owns its own open state, so closing one never discards a draft in another. +Open the Models page from the Settings navigation to see every configured provider as a row. The official DeepSeek provider (`deepseek-official`) always appears first; other providers retain their directory order. A whole-section provider whose key is not configured anywhere renders as its open setup card instead, but only in the first-run posture and only until the user closes that card. Each card kind owns its own open state, so closing one never discards a draft in another. A provider with a stored catalog error remains visible with its diagnostic and edit/delete actions. Add actions are offered only for registered settings namespaces, so an unavailable namespace cannot leave a button that opens no editor. A rejected save leaves the editor open and displays the Host diagnostic. @@ -45,7 +45,7 @@ Expand **Customized settings → Model options** to edit each model. Both provid ### Adding and deleting providers -**Add model provider** is the one entry to the add card, offered while any editor-backed settings namespace is mounted and enabled while either mode below can proceed. The card opens on a segmented mode switch with a one-line purpose beneath it: **Third-party model provider** carries the dormant-directory provider select over the provider editor — a bare-mounted `llm-pi-ai` offers its whole installed catalog before any route exists — and **Custom model API** carries the create form for a route pi-ai does not ship: a relay, a self-hosted server, or any other OpenAI- or Anthropic-compatible endpoint. A mode is offered only while its namespace is mounted; with one mode the card shows that form alone under the mode's name, and a mode whose namespace has nothing left to adopt or no protocol to declare is disabled with the reason as its hover text. A panel mounts the first time its mode is shown and stays mounted, hidden, while the card is open and its mode stays offered, so switching modes discards neither draft; the switch locks while either panel has a write or an endpoint interrogation in flight, since a switch underneath one would orphan its answer. Each tab controls its panel through ids, and closing either panel forgets the catalog draft's target. The custom form asks for a unique **Provider ID**, an endpoint, a protocol, and at least one uniquely-identified model, because nothing can default those. The protocol picker names each protocol by its product name — OpenAI Chat Completions, OpenAI Responses, Anthropic Messages — while the stored value stays the schema identifier; a protocol the adapter adds before this page names it shows that identifier. The endpoint placeholder follows the selected protocol: OpenAI Chat Completions and Responses show `https://gateway.example/v1`, while Anthropic Messages shows `https://gateway.example` because its SDK appends `/v1/messages`. Changing the protocol preserves any address already entered. The endpoint must be a parseable HTTP or HTTPS URL; localhost, IPv4 and IPv6 literals, and custom ports remain valid. A syntax error blocks both discovery and creation at the field, while a request failure remains a separate provider error. **Fetch available models** asks the `llm/discoverModels` Remote about the endpoint the form shows, so adding a provider is one pass instead of save-then-return; the reply opens a searchable picker rather than being written, and nothing is written until **Add selected**. Each candidate displays its name, falling back to its id when no name is supplied. Rows stay on one line; hovering reveals the full text when it is truncated. Selection and configuration retain the original model id. Each selected candidate copies its id, display name, context window, output-token cap, and disclosed input types into the editable row when disclosed, while an existing row retains its user-tuned values. Search matches model ids and optional display names without clearing hidden selections. **Select all** adds the visible results, while **Deselect all** clears the entire selection so hidden results cannot be adopted accidentally. A row is deletable only when the user layer alone carries it (removal restores the composition base), and its confirmation dialog names the provider. +**Add model provider** is the one entry to the add card, offered while any editor-backed settings namespace is mounted and enabled while either mode below can proceed. The card opens on a segmented mode switch with a one-line purpose beneath it: **Third-party model provider** carries the dormant-directory provider select over the provider editor — a bare-mounted `llm-pi-ai` offers its whole installed catalog before any route exists — and **Custom model API** carries the create form for a route pi-ai does not ship: a relay, a self-hosted server, or any other OpenAI- or Anthropic-compatible endpoint. A mode is offered only while its namespace is mounted; with one mode the card shows that form alone under the mode's name, and a mode whose namespace has nothing left to adopt or no protocol to declare is disabled with the reason as its hover text. A panel mounts the first time its mode is shown and stays mounted, hidden, while the card is open and its mode stays offered, so switching modes discards neither draft; the switch locks while either panel has a write or an endpoint interrogation in flight, since a switch underneath one would orphan its answer. Each tab controls its panel through ids, and closing either panel forgets the catalog draft's target. The custom form asks for a unique **Provider ID**, an endpoint, a protocol, and at least one uniquely-identified model, because nothing can default those. The protocol picker names each protocol by its product name — OpenAI Chat Completions, OpenAI Responses, Anthropic Messages — while the stored value stays the schema identifier; a protocol the adapter adds before this page names it shows that identifier. The endpoint placeholder follows the selected protocol: OpenAI Chat Completions and Responses show `https://gateway.example/v1`, while Anthropic Messages shows `https://gateway.example` because its SDK appends `/v1/messages`. Changing the protocol preserves any address already entered. The endpoint must be a parseable HTTP or HTTPS URL; localhost, IPv4 and IPv6 literals, and custom ports remain valid. A syntax error blocks both discovery and creation at the field, while a request failure remains a separate provider error. **Fetch available models** asks the `llm/discoverModels` Remote about the endpoint the form shows, so adding a provider is one pass instead of save-then-return; the reply opens a searchable picker rather than being written, and nothing is written until **Add selected**. Each candidate displays its original model id in a monospace font to distinguish models with the same name. Rows stay on one line; hovering reveals the model name, falling back to the full id when no name is supplied. Selection and configuration retain the original model id. Each selected candidate copies its id, display name, context window, output-token cap, and disclosed input types into the editable row when disclosed, while an existing row retains its user-tuned values. Search matches model ids and optional display names without clearing hidden selections. **Select all** adds the visible results, while **Deselect all** clears the entire selection so hidden results cannot be adopted accidentally. A row is deletable only when the user layer alone carries it (removal restores the composition base), and its confirmation dialog names the provider. ### First-run dialogs diff --git a/packages/client/ui-settings-models/README.zh.md b/packages/client/ui-settings-models/README.zh.md index 39b443c229..9470c9c0f1 100644 --- a/packages/client/ui-settings-models/README.zh.md +++ b/packages/client/ui-settings-models/README.zh.md @@ -25,7 +25,7 @@ kind: "package-reference" ## 使用本包 -从设置导航打开 Models 页面,即可看到每个已配置的提供商都有一行。其配置键未在任何位置配置的整分节提供商会渲染为其展开的设置卡片而非一行,但仅限首次运行姿态,且仅持续到用户关闭该卡片为止。每一类卡片各自持有自己的展开状态,因此关掉其中一张绝不会丢弃另一张里的草稿。 +从设置导航打开 Models 页面,即可看到每个已配置的提供商都有一行。官方 DeepSeek 提供商(`deepseek-official`)始终排在第一位,其他提供商保持目录中的原有顺序。其配置键未在任何位置配置的整分节提供商会渲染为其展开的设置卡片而非一行,但仅限首次运行姿态,且仅持续到用户关闭该卡片为止。每一类卡片各自持有自己的展开状态,因此关掉其中一张绝不会丢弃另一张里的草稿。 存在已存储目录错误的提供商仍显示诊断以及编辑、删除入口。添加操作只面向已注册的 settings 命名空间,因此不可用的命名空间不会留下无法打开编辑器的按钮。保存被拒绝时,编辑器保持打开并展示 Host 诊断。 @@ -45,7 +45,7 @@ Host 配置 `credentialOnboarding` 默认为 `true`。Electron preload 标记会 ### 新增与删除提供商 -**添加模型提供商**是新增卡片的唯一入口:只要任一带编辑器的设置 namespace 已挂载就会出现,下述两种方式任一可继续时才可点击。卡片打开后顶部是分段式的方式切换,下方一行小字说明所选方式的用途:**第三方模型提供商**承载休眠目录提供商选择框与提供商编辑器——裸挂载的 `llm-pi-ai` 在任何路由存在之前就能提供其完整的已安装 catalog;**自定义模型 API**承载为 pi-ai 不提供的路由(中转站、自部署服务或其他兼容 OpenAI / Anthropic 协议的接口)准备的创建表单。某种方式只在其 namespace 已挂载时提供;只剩一种方式时卡片以该方式为标题直接显示表单;namespace 已无可采用的提供商或没有可声明的协议时,对应方式会被禁用并把原因作为悬停提示。面板在其方式首次显示时挂载,之后在卡片打开且该方式仍被提供期间保持挂载但隐藏,因此切换方式不会丢掉任何一边的草稿;任一面板有写入或端点探测进行中时滑块锁定,因为此时切换会让结果落到看不见的面板上。每个 tab 通过 id 控制其面板,关闭任一面板都会忘记目录草稿的目标。自定义表单会索要唯一的 **Provider ID**、端点、协议与至少一个可唯一识别的模型,因为没有东西能为它们兜底。协议选择框以产品名称显示各协议——OpenAI Chat Completions、OpenAI Responses、Anthropic Messages——存储的值仍是 schema 标识符;适配器新增而本页尚未命名的协议直接显示其标识符。端点占位示例随所选协议变化:OpenAI Chat Completions 与 Responses 显示 `https://gateway.example/v1`,Anthropic Messages 显示 `https://gateway.example`,因为其 SDK 会追加 `/v1/messages`。切换协议会保留已输入的地址。端点必须是可解析的 HTTP 或 HTTPS URL;localhost、IPv4 与 IPv6 字面地址以及自定义端口仍然有效。语法错误会在字段处阻止询问与创建,请求失败则继续作为独立的提供商错误显示。**获取可用模型**通过 `llm/discoverModels` Remote 查询表单显示的端点,因此新增提供商一次即可完成,而非先保存再返回;回复打开的是可搜索选择器而非直接写入,只有点击**添加所选**才会写入。候选项显示模型名称,未提供名称时回退到 id。每行保持单行,文字截断时可悬停查看完整内容。勾选与配置仍使用原始模型 id。每个选中候选会在提供商公布相应信息时,把 id、显示名、上下文窗口、最大输出 token 数和已公布的输入类型复制进可编辑行;已经存在的行保留用户调整过的值。搜索会匹配模型 id 与可选显示名称,且不会清除隐藏项的勾选状态。**全选**会加入可见结果,而**取消全选**会清空全部勾选,以免意外采用隐藏结果。只有用户层单独携带某行时,该行才可删除(删除会恢复组合基线),其确认对话框会指名该提供商。 +**添加模型提供商**是新增卡片的唯一入口:只要任一带编辑器的设置 namespace 已挂载就会出现,下述两种方式任一可继续时才可点击。卡片打开后顶部是分段式的方式切换,下方一行小字说明所选方式的用途:**第三方模型提供商**承载休眠目录提供商选择框与提供商编辑器——裸挂载的 `llm-pi-ai` 在任何路由存在之前就能提供其完整的已安装 catalog;**自定义模型 API**承载为 pi-ai 不提供的路由(中转站、自部署服务或其他兼容 OpenAI / Anthropic 协议的接口)准备的创建表单。某种方式只在其 namespace 已挂载时提供;只剩一种方式时卡片以该方式为标题直接显示表单;namespace 已无可采用的提供商或没有可声明的协议时,对应方式会被禁用并把原因作为悬停提示。面板在其方式首次显示时挂载,之后在卡片打开且该方式仍被提供期间保持挂载但隐藏,因此切换方式不会丢掉任何一边的草稿;任一面板有写入或端点探测进行中时滑块锁定,因为此时切换会让结果落到看不见的面板上。每个 tab 通过 id 控制其面板,关闭任一面板都会忘记目录草稿的目标。自定义表单会索要唯一的 **Provider ID**、端点、协议与至少一个可唯一识别的模型,因为没有东西能为它们兜底。协议选择框以产品名称显示各协议——OpenAI Chat Completions、OpenAI Responses、Anthropic Messages——存储的值仍是 schema 标识符;适配器新增而本页尚未命名的协议直接显示其标识符。端点占位示例随所选协议变化:OpenAI Chat Completions 与 Responses 显示 `https://gateway.example/v1`,Anthropic Messages 显示 `https://gateway.example`,因为其 SDK 会追加 `/v1/messages`。切换协议会保留已输入的地址。端点必须是可解析的 HTTP 或 HTTPS URL;localhost、IPv4 与 IPv6 字面地址以及自定义端口仍然有效。语法错误会在字段处阻止询问与创建,请求失败则继续作为独立的提供商错误显示。**获取可用模型**通过 `llm/discoverModels` Remote 查询表单显示的端点,因此新增提供商一次即可完成,而非先保存再返回;回复打开的是可搜索选择器而非直接写入,只有点击**添加所选**才会写入。候选项使用等宽字体显示原始模型 id,以区分同名模型。每行保持单行,悬停可查看模型名称,未提供名称时回退到完整 id。勾选与配置仍使用原始模型 id。每个选中候选会在提供商公布相应信息时,把 id、显示名、上下文窗口、最大输出 token 数和已公布的输入类型复制进可编辑行;已经存在的行保留用户调整过的值。搜索会匹配模型 id 与可选显示名称,且不会清除隐藏项的勾选状态。**全选**会加入可见结果,而**取消全选**会清空全部勾选,以免意外采用隐藏结果。只有用户层单独携带某行时,该行才可删除(删除会恢复组合基线),其确认对话框会指名该提供商。 ### 首次运行弹窗 diff --git a/packages/client/ui-settings-models/package.json b/packages/client/ui-settings-models/package.json index 60d2f7ac60..93959d0734 100644 --- a/packages/client/ui-settings-models/package.json +++ b/packages/client/ui-settings-models/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-models", "description": "Models settings and shared product-onboarding dialogs over existing settings and credential joins", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-models/src/client/ModelListEditor.tsx b/packages/client/ui-settings-models/src/client/ModelListEditor.tsx index cfab83e8d9..3c02030b6b 100644 --- a/packages/client/ui-settings-models/src/client/ModelListEditor.tsx +++ b/packages/client/ui-settings-models/src/client/ModelListEditor.tsx @@ -447,8 +447,8 @@ export function ModelListEditor(props: ModelListEditorProps): ReactNode { checked={picked.has(candidate.id)} onChange={() => { toggle(candidate.id) }} /> - - {candidate.name ?? candidate.id} + + {candidate.id} diff --git a/packages/client/ui-settings-models/src/client/ModelsSection.module.css b/packages/client/ui-settings-models/src/client/ModelsSection.module.css index a21446f6ff..be41c27c18 100644 --- a/packages/client/ui-settings-models/src/client/ModelsSection.module.css +++ b/packages/client/ui-settings-models/src/client/ModelsSection.module.css @@ -705,9 +705,10 @@ select.input { cursor: pointer; } -.candidateName { +.candidateId { flex: 1 1 auto; min-width: 0; + font-family: var(--ds-font-family-code); font-size: 13px; overflow: hidden; text-overflow: ellipsis; diff --git a/packages/client/ui-settings-models/src/client/ModelsSection.tsx b/packages/client/ui-settings-models/src/client/ModelsSection.tsx index 7ae8118762..d5743645b2 100644 --- a/packages/client/ui-settings-models/src/client/ModelsSection.tsx +++ b/packages/client/ui-settings-models/src/client/ModelsSection.tsx @@ -1,7 +1,8 @@ /** * Models settings section: the provider rows joined from the configurable * directory, settings namespaces, and credential states, with one editor - * card at a time. Rows expose only confirmed API-key state through accessible + * card at a time. The official DeepSeek provider appears first; other rows + * retain directory order. Rows expose only confirmed API-key state through accessible * solid configured or missing dots. A whole-section provider without a * configured key renders as its open setup card instead of a row, but only in * the first-run posture — no provider on the page can serve requests yet — and @@ -335,7 +336,9 @@ function Loaded({ injected, renderSlot }: { injected: ModelsSectionFace; renderS // One fact decides both first-run postures on this page and the onboarding // step: whether the user already has a provider to talk to. const anyUsable = state.rows.some(providerUsable) - const configured = state.rows.filter(row => row.configured) + const configured = state.rows.filter(row => row.configured).sort((left, right) => ( + Number(right.entry.provider === 'deepseek-official') - Number(left.entry.provider === 'deepseek-official') + )) const configurable = state.rows.filter(row => state.namespaces.has(row.entry.settingsNs)) const addable: AddableRow[] = state.rows.flatMap((row) => { const namespace = state.namespaces.get(row.entry.settingsNs) diff --git a/packages/client/ui-settings-models/tests/provider-form.client.spec.tsx b/packages/client/ui-settings-models/tests/provider-form.client.spec.tsx index 5140c7a427..16af464c77 100644 --- a/packages/client/ui-settings-models/tests/provider-form.client.spec.tsx +++ b/packages/client/ui-settings-models/tests/provider-form.client.spec.tsx @@ -609,8 +609,8 @@ describe('endpoint interrogation', () => { fireEvent.click(screen.getByText(en.fetchModels)) await screen.findByText(en.fetchTitle) - expect(screen.getByRole('checkbox', { name: 'Fresh' }).checked).toBe(true) - expect(screen.queryByRole('checkbox', { name: 'fresh' })).toBeNull() + expect(screen.getByRole('checkbox', { name: 'fresh' }).checked).toBe(true) + expect(screen.queryByRole('checkbox', { name: 'Fresh' })).toBeNull() // The already-configured row starts unchecked; the new one starts checked. const boxes = [...document.querySelectorAll('input[type="checkbox"]')] expect(boxes.map(box => box.checked)).toEqual([false, true]) @@ -750,8 +750,8 @@ describe('endpoint interrogation', () => { fireEvent.click(screen.getByText(en.fetchModels)) const dialog = await screen.findByRole('dialog') const search = screen.getByLabelText(en.fetchSearch) - expect(dialog.textContent).toContain('Beta Display') - expect(dialog.textContent).not.toContain('opaque-id') + expect(dialog.textContent).toContain('opaque-id') + expect(dialog.textContent).not.toContain('Beta Display') expect([...dialog.querySelectorAll('input[type="checkbox"]')] .map(box => box.checked)).toEqual([true, true, true]) @@ -760,11 +760,11 @@ describe('endpoint interrogation', () => { expect(dialog.textContent).not.toContain('opaque-id') fireEvent.change(search, { target: { value: 'beta' } }) - expect(dialog.textContent).toContain('Beta Display') + expect(dialog.textContent).toContain('opaque-id') expect(dialog.textContent).not.toContain('alpha') fireEvent.change(search, { target: { value: 'opaque' } }) - expect(dialog.textContent).toContain('Beta Display') + expect(dialog.textContent).toContain('opaque-id') expect(dialog.textContent).not.toContain('alpha') fireEvent.click(within_(dialog, en.fetchDeselectAll)) diff --git a/packages/client/ui-settings-plugin-inventory/package.json b/packages/client/ui-settings-plugin-inventory/package.json index 4922cf9343..0948468ce4 100644 --- a/packages/client/ui-settings-plugin-inventory/package.json +++ b/packages/client/ui-settings-plugin-inventory/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-plugin-inventory", "description": "Read-only Cordis Loader inventory tab in Web Plugins settings", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-plugins/package.json b/packages/client/ui-settings-plugins/package.json index 03d92c3ea9..745ff32976 100644 --- a/packages/client/ui-settings-plugins/package.json +++ b/packages/client/ui-settings-plugins/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-plugins", "description": "Built-in plugins settings section for the dsh web client: the Settings navigation entry and the tab chrome feature-owned tabs register into", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-shell/package.json b/packages/client/ui-settings-shell/package.json index e6b7859680..e3551daa21 100644 --- a/packages/client/ui-settings-shell/package.json +++ b/packages/client/ui-settings-shell/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-shell", "description": "Settings page of the shell executor on the dsh web client's Plugins page: the command timeout and the per-stream output cap of the shell namespace", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-subagent/package.json b/packages/client/ui-settings-subagent/package.json index d0da1ba3ca..4c2fd80f37 100644 --- a/packages/client/ui-settings-subagent/package.json +++ b/packages/client/ui-settings-subagent/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-subagent", "description": "Settings page of Subagent delegation on the dsh web client's Plugins page: recursion depth, parallel capacity, and the models agents may choose for subagents", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-web-search/package.json b/packages/client/ui-settings-web-search/package.json index 3409e17e65..e21ca4aaa7 100644 --- a/packages/client/ui-settings-web-search/package.json +++ b/packages/client/ui-settings-web-search/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-web-search", "description": "Settings page of the DeepSeek web-search provider on the dsh web client's Plugins page: its API key, endpoint, and per-request search budget", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings/package.json b/packages/client/ui-settings/package.json index 655a2ac47c..90ae1eb92b 100644 --- a/packages/client/ui-settings/package.json +++ b/packages/client/ui-settings/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings", "description": "Settings domain base plugin: shared configuration forms and the canonical settings slot-type contract", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-sidebar-browser/package.json b/packages/client/ui-sidebar-browser/package.json index b8523a5b04..276edca457 100644 --- a/packages/client/ui-sidebar-browser/package.json +++ b/packages/client/ui-sidebar-browser/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-sidebar-browser", "description": "Sandboxed Web browser tabs for the right Sidebar", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-sidebar-documentpreview/package.json b/packages/client/ui-sidebar-documentpreview/package.json index ad0c6fe49b..bae13db1d5 100644 --- a/packages/client/ui-sidebar-documentpreview/package.json +++ b/packages/client/ui-sidebar-documentpreview/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-sidebar-documentpreview", "description": "Extensible Sidebar previews for Office documents, spreadsheets, Markdown, code, images, PDF, HTML, and plain text", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-sidebar-files/package.json b/packages/client/ui-sidebar-files/package.json index d5b0a8f803..492f3a1129 100644 --- a/packages/client/ui-sidebar-files/package.json +++ b/packages/client/ui-sidebar-files/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-sidebar-files", "description": "Workspace file tree tab type for the right Sidebar: lazy directory listing over the workspaceFiles Remote namespace, opening files into the Sidebar", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-sidebar-right/package.json b/packages/client/ui-sidebar-right/package.json index b8c92a7ae3..b36613d81e 100644 --- a/packages/client/ui-sidebar-right/package.json +++ b/packages/client/ui-sidebar-right/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-sidebar-right", "description": "Right Sidebar: the docking surface's session-bound state, its panel and header expand control, and the navigation service over it", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-sidebar-terminal/package.json b/packages/client/ui-sidebar-terminal/package.json index e75cbb959d..1bf7b868b9 100644 --- a/packages/client/ui-sidebar-terminal/package.json +++ b/packages/client/ui-sidebar-terminal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-sidebar-terminal", "description": "Interactive shell tabs for the right Sidebar", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-sidebar/package.json b/packages/client/ui-sidebar/package.json index 076c7e3d3f..cf9d1a2893 100644 --- a/packages/client/ui-sidebar/package.json +++ b/packages/client/ui-sidebar/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-sidebar", "description": "Sidebar plugin: session multi-level tree, search, grouping, state dots", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-skill/package.json b/packages/client/ui-skill/package.json index 54f5b19f79..49bee1c402 100644 --- a/packages/client/ui-skill/package.json +++ b/packages/client/ui-skill/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-skill", "description": "Web skill references and the dedicated skill tool row", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-slots/package.json b/packages/client/ui-slots/package.json index 464072b246..40ddbe4970 100644 --- a/packages/client/ui-slots/package.json +++ b/packages/client/ui-slots/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-slots", "description": "Slot registry pure core: typed ordinary Slots and reusable Component Factories, derived props, Store seats, and renderer installation", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-subagent/package.json b/packages/client/ui-subagent/package.json index d5c87a7518..89a118ddad 100644 --- a/packages/client/ui-subagent/package.json +++ b/packages/client/ui-subagent/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-subagent", "description": "Subagent conversation catalog, continuation routing UI, and '@' reference source", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-theme/package.json b/packages/client/ui-theme/package.json index e63647fa62..8fdbec3862 100644 --- a/packages/client/ui-theme/package.json +++ b/packages/client/ui-theme/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-theme", "description": "Theme plugin: Host bootstrap for the pre-plugin palette; DOM-free ThemeRuntime for light/dark/system state; --dsw-* token styles and Appearance settings row", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-tool/package.json b/packages/client/ui-tool/package.json index d75d8e49e0..1f36739781 100644 --- a/packages/client/ui-tool/package.json +++ b/packages/client/ui-tool/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-tool", "description": "Client Tool call-tree renderer and keyed per-tool presentation slot", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-trajectory/package.json b/packages/client/ui-trajectory/package.json index 742e2b7846..b9a31b6307 100644 --- a/packages/client/ui-trajectory/package.json +++ b/packages/client/ui-trajectory/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-trajectory", "description": "Trajectory event ledger with an interactive timing overview: pure-consumer plugin registering into the conversation ViewMap (no service)", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-user-questions/package.json b/packages/client/ui-user-questions/package.json index e8d377742b..af7574df7c 100644 --- a/packages/client/ui-user-questions/package.json +++ b/packages/client/ui-user-questions/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-user-questions", "description": "Web ask_user_question composer takeover and plan-review presentation UI", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-workflow-run/package.json b/packages/client/ui-workflow-run/package.json index 1c59474572..95243a24ea 100644 --- a/packages/client/ui-workflow-run/package.json +++ b/packages/client/ui-workflow-run/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-workflow-run", "description": "Durable workflow-run Conversation Node and nested member disclosure for dsh web", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-workspace/package.json b/packages/client/ui-workspace/package.json index 9fec8c9c93..b604aee9ff 100644 --- a/packages/client/ui-workspace/package.json +++ b/packages/client/ui-workspace/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-workspace", "description": "Workspace picker plugin: one WorkspacePicker registered into the sidebar and empty-state workspace slots", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/web/package.json b/packages/client/web/package.json index 023c0ac544..03c4707cd8 100644 --- a/packages/client/web/package.json +++ b/packages/client/web/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-web", "description": "Web boot kernel: static module table, Cordis loader, framework-free boot page, and UI-renderer handoff", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/compaction/command-compact/package.json b/packages/compaction/command-compact/package.json index 53dedea10d..f1124114e6 100644 --- a/packages/compaction/command-compact/package.json +++ b/packages/compaction/command-compact/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-command-compact", "description": "Human-facing slash command for explicit session compaction", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/compaction/compaction-basic/package.json b/packages/compaction/compaction-basic/package.json index 076d88f18b..028c88f678 100644 --- a/packages/compaction/compaction-basic/package.json +++ b/packages/compaction/compaction-basic/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-compaction-basic", "description": "Token-meter-driven compaction policy and LLM summarization backend for the DeepSeek Harness", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/compaction/compaction-image-offload/package.json b/packages/compaction/compaction-image-offload/package.json index a77e71796a..1053f6cbd4 100644 --- a/packages/compaction/compaction-image-offload/package.json +++ b/packages/compaction/compaction-image-offload/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-compaction-image-offload", "description": "Durable image offload for image-capable routes: replace over-budget request images with placeholders and retry", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/compaction/compaction-tool-result-pruner/package.json b/packages/compaction/compaction-tool-result-pruner/package.json index 051e88585e..7d30cded68 100644 --- a/packages/compaction/compaction-tool-result-pruner/package.json +++ b/packages/compaction/compaction-tool-result-pruner/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-compaction-tool-result-pruner", "description": "Replay-safe model-free head/middle/tail pruning for tool-result surface nodes", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/compaction/compaction/package.json b/packages/compaction/compaction/package.json index 430219d464..067a42d1a1 100644 --- a/packages/compaction/compaction/package.json +++ b/packages/compaction/compaction/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-compaction", "description": "Abstract compaction service seam (ctx.compaction) for the DeepSeek Harness", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/computer-use/computer-use/package.json b/packages/computer-use/computer-use/package.json index 275924505d..5892b9f69a 100644 --- a/packages/computer-use/computer-use/package.json +++ b/packages/computer-use/computer-use/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-computer-use", "description": "Exclusive named computer-use provider registration", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/context/agent-instructions/package.json b/packages/context/agent-instructions/package.json index 5da6d7e009..90cfff97ab 100644 --- a/packages/context/agent-instructions/package.json +++ b/packages/context/agent-instructions/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent-instructions", "description": "Workspace context loader for AGENTS.md/CLAUDE.md instruction files", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/context/file-reference-local/package.json b/packages/context/file-reference-local/package.json index b4a3997116..a42d833f65 100644 --- a/packages/context/file-reference-local/package.json +++ b/packages/context/file-reference-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-file-reference-local", "description": "Local-filesystem ctx.fileReferences provider with bounded fuzzy indexes", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/context/file-reference/package.json b/packages/context/file-reference/package.json index 88d1410c2a..6b7f48d3e5 100644 --- a/packages/context/file-reference/package.json +++ b/packages/context/file-reference/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-file-reference", "description": "File-reference discovery contract and shared @file grammar", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/context/session-reference/package.json b/packages/context/session-reference/package.json index f2453f9239..3427d2a847 100644 --- a/packages/context/session-reference/package.json +++ b/packages/context/session-reference/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-reference", "description": "Cross-session snapshot references and durable untrusted model context (ctx.sessionReferenceResolver)", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/context/time-context/package.json b/packages/context/time-context/package.json index d22ffa7366..85b9acf6d3 100644 --- a/packages/context/time-context/package.json +++ b/packages/context/time-context/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-time-context", "description": "Opt-in durable per-step context with the current time and elapsed time", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/context/tmux-context/package.json b/packages/context/tmux-context/package.json index bcd67705fc..40638291d6 100644 --- a/packages/context/tmux-context/package.json +++ b/packages/context/tmux-context/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tmux-context", "description": "Opt-in durable per-step context with this agent's tmux pane and window location", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/core/agent-default-model/package.json b/packages/core/agent-default-model/package.json index c4a00536ce..e27550b897 100644 --- a/packages/core/agent-default-model/package.json +++ b/packages/core/agent-default-model/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent-default-model", "description": "Default model selection shared by Agent entry points", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/core/agent-loop/package.json b/packages/core/agent-loop/package.json index 30aea4c21a..ddfb2645ac 100644 --- a/packages/core/agent-loop/package.json +++ b/packages/core/agent-loop/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent-loop", "description": "The concrete agent loop plugin for the DeepSeek Harness", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/core/agent-tool-presentation/package.json b/packages/core/agent-tool-presentation/package.json index d50567c802..c0ad0084a0 100644 --- a/packages/core/agent-tool-presentation/package.json +++ b/packages/core/agent-tool-presentation/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent-tool-presentation", "description": "Agent-plane presentation selector: composes one agent's tools as PTC mode, native, or both", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/core/agent/package.json b/packages/core/agent/package.json index 99d7fece31..0e39e03c71 100644 --- a/packages/core/agent/package.json +++ b/packages/core/agent/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent", "description": "Agent interface, registry, initiator scope, and event vocabulary for the DeepSeek Harness", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/core/scope/package.json b/packages/core/scope/package.json index 805b64c1cf..56ec1246c6 100644 --- a/packages/core/scope/package.json +++ b/packages/core/scope/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-scope", "description": "Scoped-context registration primitive (scope tags, scope-filtered event dispatch) for the DeepSeek Harness", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/core/session/package.json b/packages/core/session/package.json index 26cc9ee9ea..3cce03e091 100644 --- a/packages/core/session/package.json +++ b/packages/core/session/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session", "description": "Event-sourced session store for the DeepSeek Harness", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/core/system-prompt/package.json b/packages/core/system-prompt/package.json index 9cc4976a07..0ab266f3a6 100644 --- a/packages/core/system-prompt/package.json +++ b/packages/core/system-prompt/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-system-prompt", "description": "System prompt assembly registry for the DeepSeek Harness", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/core/tools/package.json b/packages/core/tools/package.json index 8e97f843cd..b5839d2e50 100644 --- a/packages/core/tools/package.json +++ b/packages/core/tools/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tools", "description": "Tool registry and execution pipeline for the DeepSeek Harness", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/credentials/authorization/package.json b/packages/credentials/authorization/package.json index 2eaf6b40af..6e8c3ddc7b 100644 --- a/packages/credentials/authorization/package.json +++ b/packages/credentials/authorization/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-authorization", "description": "Authorization seam (ctx.authorization): plugin-owned flows that obtain a credential through a conversation with the human", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/credentials/credentials-local/package.json b/packages/credentials/credentials-local/package.json index 541415dde0..c69efa42a0 100644 --- a/packages/credentials/credentials-local/package.json +++ b/packages/credentials/credentials-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-credentials-local", "description": "File-backed credentials provider ($DSH_HOME/.env under the live process environment) for the DeepSeek Harness", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/credentials/credentials/package.json b/packages/credentials/credentials/package.json index 68e33c0ca9..65fe3cb106 100644 --- a/packages/credentials/credentials/package.json +++ b/packages/credentials/credentials/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-credentials", "description": "Abstract credential seam (ctx.credentials): settings carry references to secrets, providers own the values", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/credentials/deepseek-account-platform/package.json b/packages/credentials/deepseek-account-platform/package.json index 14b6a698df..abf1170a15 100644 --- a/packages/credentials/deepseek-account-platform/package.json +++ b/packages/credentials/deepseek-account-platform/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-deepseek-account-platform", "description": "Authorize DeepSeek accounts through browser PKCE", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/credentials/deepseek-account/package.json b/packages/credentials/deepseek-account/package.json index 9199faca05..b0f00e6a7c 100644 --- a/packages/credentials/deepseek-account/package.json +++ b/packages/credentials/deepseek-account/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-deepseek-account", "description": "Read account state and resolve official API credentials", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/deliverables/tool-present/package.json b/packages/deliverables/tool-present/package.json index f15a4d5209..a91f72803a 100644 --- a/packages/deliverables/tool-present/package.json +++ b/packages/deliverables/tool-present/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-present", "description": "Explicit workspace file delivery declarations for the DeepSeek Harness", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/deliverables/workspace-changes/package.json b/packages/deliverables/workspace-changes/package.json index 9a5b84c082..a062496179 100644 --- a/packages/deliverables/workspace-changes/package.json +++ b/packages/deliverables/workspace-changes/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-workspace-changes", "description": "Per-turn workspace file changes recorded from git working-tree snapshots and whole-file captures, with per-file comparisons, for the DeepSeek Harness", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/document/office-to-pdf/package.json b/packages/document/office-to-pdf/package.json index 6f1c45502f..660db725e5 100644 --- a/packages/document/office-to-pdf/package.json +++ b/packages/document/office-to-pdf/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-office-to-pdf", "description": "Shared Office-to-PDF conversion with bounded queues and caching", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/agent-team-profile/package.json b/packages/experimental/agent-team-profile/package.json index 3c58e78ecf..ba73fb30ce 100644 --- a/packages/experimental/agent-team-profile/package.json +++ b/packages/experimental/agent-team-profile/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-agent-team-profile", "description": "Agent Teams collaboration, tools, and Web UI in one experimental bundle", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "icon": "./icon.svg", "publishConfig": { "access": "public" diff --git a/packages/experimental/agent-team/README.i18n.yaml b/packages/experimental/agent-team/README.i18n.yaml index 52019ce198..be20145568 100644 --- a/packages/experimental/agent-team/README.i18n.yaml +++ b/packages/experimental/agent-team/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/experimental/agent-team/README.md -README.md: 6ae7d82214ca9e041324e353057ba6850675c043 -README.zh.md: 89176c285a8bd9d5a1875618dffb0b2eb4890a3e +README.md: 6d2aa6041039022e6508541fdb738181bbb54045 +README.zh.md: 9ea2977a976583aac08ca8bc53d61ff4812e1766 diff --git a/packages/experimental/agent-team/README.md b/packages/experimental/agent-team/README.md index 6ae7d82214..6d2aa60410 100644 --- a/packages/experimental/agent-team/README.md +++ b/packages/experimental/agent-team/README.md @@ -118,7 +118,8 @@ The [Agent Teams Agent Note](../../../.agents/notes/implemented/feature/2026-08- | [`src/mailbox.ts`](src/mailbox.ts) | Durable queue, target-local dispatch, acknowledgement, and recovery | | [`src/task-board.ts`](src/task-board.ts) | Task CAS commands, DAG validation, and derived views | | [`src/journal.ts`](src/journal.ts) | Serialized Lead-log transactions and commit notification | -| [`src/projection.ts`](src/projection.ts) | Strict replay projection that decodes and validates Team events | +| [`src/projection.ts`](src/projection.ts) | Strict replay projection that decodes and validates Team events and publishes the `agentTeam` client view | +| [`src/task-view.ts`](src/task-view.ts) | Pure task readiness, owner-name, and write-overlap derivation shared by the task board and the client view | | [`src/activity.ts`](src/activity.ts) | One-shot change waiters and disposal release | | [`src/lifecycle.ts`](src/lifecycle.ts) | Shared admission cutoff and bounded settlement | | [`src/invariant.ts`](src/invariant.ts) | Invariant companion that replays candidate events before append | @@ -171,9 +172,11 @@ Read these pages when the package-level contract is not enough. They move from t -### Browser Remote +### Browser projection -`TeamService` exposes the read-only `agentTeams/view` Remote method for browser clients. Task creation and updates belong to Team agents through the service and model tools. The `./remote` export supplies the Client contribution mounted by the Web UI, while `./client` exports browser-safe roster and task views. +The `agentTeam` Session projection publishes the Lead Session's durable roster identities and phases, member errors, non-deleted task views, and any `failure` beside the last valid state. Its `apply` replaces only the touched collection; mailbox-only changes retain the client view reference and produce no frame. The [subsystem reference](../../../docs/subsystems/agent-team.md#web-projection) defines the wire types. + +The [Web UI](../client-ui-agent-team/README.md) reads the shared Session projections and overlays activity from Session status. Task creation and updates belong to Team agents through the service and model tools. The `./client` export supplies browser-safe roster, task, and projection types. ## Model Experience @@ -198,6 +201,7 @@ Peer messages append after the target's reusable history prefix. Cold resume reu These limits describe what a team cannot do yet or what needs special operational care. They are current package constraints, not a comparison with other coordination mechanisms. +- **Whole-view broadcasts** — each roster or task change sends the complete roster and non-deleted task board, including descriptions, to every connected browser, even when it is viewing another Session. - **Experimental prototype with no stability promise** — the package is public, but its contracts can change freely while it incubates. - **One process and one shared checkout** — members share cwd and observe edits immediately; this package provides no worktree, remote member, merge, or filesystem lock. - **Advisory write scopes** — Bash, formatters, code generators, and direct external writers can bypass filesystem version checks; Leads must coordinate ownership and review the final diff. diff --git a/packages/experimental/agent-team/README.zh.md b/packages/experimental/agent-team/README.zh.md index 89176c285a..9ea2977a97 100644 --- a/packages/experimental/agent-team/README.zh.md +++ b/packages/experimental/agent-team/README.zh.md @@ -118,7 +118,8 @@ Lead 可以停止 teammate 的当前轮次,而不会删除其排队的消息 | [`src/mailbox.ts`](src/mailbox.ts) | 持久队列、目标本地投递、确认与恢复 | | [`src/task-board.ts`](src/task-board.ts) | 任务 CAS 命令、DAG 校验与派生视图 | | [`src/journal.ts`](src/journal.ts) | 串行化的 Lead 日志事务与提交通知 | -| [`src/projection.ts`](src/projection.ts) | 解码并校验 Team 事件的严格回放投影 | +| [`src/projection.ts`](src/projection.ts) | 解码并校验 Team 事件、发布 `agentTeam` 客户端视图的严格回放投影 | +| [`src/task-view.ts`](src/task-view.ts) | 任务板与客户端视图共用的纯任务派生:就绪状态、owner 名称与写入范围重叠 | | [`src/activity.ts`](src/activity.ts) | 一次性变更等待者与 dispose(资源释放)时的等待解除 | | [`src/lifecycle.ts`](src/lifecycle.ts) | 共享准入截止与有界结算 | | [`src/invariant.ts`](src/invariant.ts) | 在 append 前回放候选事件的不变式伴生插件 | @@ -171,9 +172,11 @@ dispose 会关闭准入、中止并等待已获准的创建与 mailbox dispatch -### 浏览器 Remote +### 浏览器投影 -`TeamService` 向浏览器客户端公开只读的 `agentTeams/view` Remote method。任务创建与更新由 Team agent 通过服务和模型工具执行。`./remote` 导出由 Web UI 挂载的 Client contribution,`./client` 导出可供浏览器使用的成员与任务视图。 +`agentTeam` Session 投影发布 Lead Session 的持久成员身份与阶段、成员错误、未删除任务视图,以及最后有效状态旁的 `failure`。其 `apply` 只替换被触及的集合;仅邮箱的变化保留客户端视图引用,不产生 frame。[子系统参考](../../../docs/subsystems/agent-team.zh.md#web-projection) 定义传输类型。 + +[Web UI](../client-ui-agent-team/README.zh.md) 读取共享 Session 投影,并从 Session 状态叠加活动信息。任务创建与更新由 Team agent 通过服务和模型工具完成。`./client` 导出可供浏览器使用的 roster、任务与投影类型。 ## 模型体验 @@ -198,6 +201,7 @@ Peer 消息追加在 target 可复用历史前缀之后。冷恢复会先复用 这些限制说明一支团队目前不能做什么、或哪些方面需要特别的运维关注。它们是当前包约束,不是与其他协作机制的对比。 +- **完整视图广播** — 每次 roster 或任务变化都会把完整 roster 和未删除任务板(含描述)发给所有已连接浏览器,即使它正在查看其他 Session。 - **实验原型,无稳定性承诺**——本包公开发布,但孵化期间约定仍可自由变更。 - **单进程、共享 checkout**——成员共享 cwd,修改立即可见;本包不提供 worktree、远端成员、merge 或文件锁。 - **write scope 仅作提示**——Bash、formatter、代码生成器与直接外部写入可以绕过文件版本检查;Lead 必须协调 owner 并检查最终 diff。 diff --git a/packages/experimental/agent-team/package.json b/packages/experimental/agent-team/package.json index 119af0da93..680ea5c4c4 100644 --- a/packages/experimental/agent-team/package.json +++ b/packages/experimental/agent-team/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-agent-team", "description": "Implicit-root Agent Teams roster, durable peer mailbox, and shared task DAG", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, @@ -30,14 +30,6 @@ "types": "./lib/types/types.d.ts", "default": "./lib/types/types.js" }, - "./typert": { - "types": "./lib/typert.host.d.ts", - "default": "./lib/typert.host.js" - }, - "./remote": { - "types": "./lib/typert.remote-client.d.ts", - "default": "./lib/typert.remote-client.js" - }, "./locale/*.json": "./locale/*.json", "./src/*": "./src/*", "./package.json": "./package.json" @@ -47,11 +39,7 @@ "lib/index.js", "lib/invariant.js", "lib/types/**/*.js", - "lib/types/**/*.d.ts", - "lib/typert.host.js", - "lib/typert.host.d.ts", - "lib/typert.remote-client.js", - "lib/typert.remote-client.d.ts" + "lib/types/**/*.d.ts" ], "license": "MIT", "dependencies": { @@ -67,8 +55,7 @@ "@deepseek-ai/dsh-session": "workspace:*", "@deepseek-ai/dsh-session-persistence": "workspace:*", "@deepseek-ai/dsh-session-projection": "workspace:*", - "@deepseek-ai/dsh-subagent": "workspace:*", - "@deepseek-ai/dsh-typert-protocol": "workspace:*" + "@deepseek-ai/dsh-subagent": "workspace:*" }, "devDependencies": { "@deepseek-ai/cordis": "workspace:~", @@ -84,7 +71,6 @@ "@deepseek-ai/dsh-session-query": "workspace:*", "@deepseek-ai/dsh-subagent": "workspace:*", "@deepseek-ai/dsh-subagent-fork-in-process": "workspace:*", - "@deepseek-ai/dsh-subagent-spawn-in-process": "workspace:*", - "@deepseek-ai/dsh-typert-protocol": "workspace:*" + "@deepseek-ai/dsh-subagent-spawn-in-process": "workspace:*" } } diff --git a/packages/experimental/agent-team/src/client.ts b/packages/experimental/agent-team/src/client.ts index b1fd173d10..c56076fa7c 100644 --- a/packages/experimental/agent-team/src/client.ts +++ b/packages/experimental/agent-team/src/client.ts @@ -1,9 +1,11 @@ -/** Client-safe Agent Teams roster and task views. */ +/** Client-safe Agent Teams roster and task views plus the `agentTeam` projection vocabulary. */ export type { + TeamMemberPhase, + TeamMemberProjection, TeamMemberView, + TeamProjection, TeamTaskId, TeamTaskStatus, TeamTaskView, - TeamView, } from './types.ts' diff --git a/packages/experimental/agent-team/src/index.ts b/packages/experimental/agent-team/src/index.ts index 03ddbc7a48..96a79bea16 100644 --- a/packages/experimental/agent-team/src/index.ts +++ b/packages/experimental/agent-team/src/index.ts @@ -1,10 +1,9 @@ /** Agent Teams service façade over roster, mailbox, task, and runtime lifecycle owners. */ -import { Context } from '@deepseek-ai/cordis' +import { Context, Service } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' import type { Agent } from '@deepseek-ai/dsh-agent' import type {} from '@deepseek-ai/dsh-session-persistence' -import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol' import { TeamActivity } from './activity.ts' import { errorMessage, TeamError } from './error.ts' import { TeamJournal } from './journal.ts' @@ -24,7 +23,6 @@ import type { SpawnTeammateResult, TeamMemberView, TeamTaskView, - TeamView, TeamWaitResult, UpdateTeamTaskRequest, } from './types.ts' @@ -55,7 +53,7 @@ function positiveLimit(name: string, value: number): number { } /** Agent Teams service backed by the exact live Lead Session log. */ -export class TeamService extends TypertRemoteService { +export class TeamService extends Service { static inject = ['agents', 'sessions', 'sessionPersistence', 'sessionProjections', 'subagents'] static Config: z = z.object({ @@ -233,19 +231,6 @@ export class TeamService extends TypertRemoteService { return this.roster.tryMembership(agent) } - /** - * Read the current roster and non-deleted task board through the generated Remote API. - * @param agent - exact live Team member used as the authority credential. - * @returns detached current roster and task views. - */ - @Remote('view') - remoteView(agent: Agent): TeamView { - return { - members: this.listMembers(agent), - tasks: this.listTasks(agent), - } - } - /** Queue one contained recovery pass after publication has unwound. */ private scheduleRecovery(agent: Agent): void { queueMicrotask(() => { diff --git a/packages/experimental/agent-team/src/invariant.ts b/packages/experimental/agent-team/src/invariant.ts index dc5fd3ddc1..442df6a6e6 100644 --- a/packages/experimental/agent-team/src/invariant.ts +++ b/packages/experimental/agent-team/src/invariant.ts @@ -16,7 +16,7 @@ export const name = 'team-invariant' /** Invariant registry required by the companion. */ export const inject = ['invariants'] -/** Validate candidate Team events against the projected committed prefix. */ +/** Validate candidate Team events against the projected committed prefix; `apply` never mutates the committed state. */ const install: InvariantInstaller = Object.assign((ctx: Context, fail: InvariantFailure) => { ctx.on('internal/dispatch', (_mode, eventName, args) => { if (eventName !== 'session/event') return @@ -24,7 +24,7 @@ const install: InvariantInstaller = Object.assign((ctx: Context, fail: Invariant /* v8 ignore next -- non-Team Session events have no Agent Teams invariant. */ if (!isTeamEvent(event)) return const state = ctx.sessionProjections.stateOf(session, 'agentTeam') as TeamProjectionState - const candidate = teamProjectionDefinition.apply(structuredClone(state), event) + const candidate = teamProjectionDefinition.apply(state, event) if (candidate.failure !== undefined) { fail(`session event ${event.seq} violates the Agent Teams stream: ${candidate.failure}`) } diff --git a/packages/experimental/agent-team/src/projection.ts b/packages/experimental/agent-team/src/projection.ts index e4ade61e9e..bc9239d3d8 100644 --- a/packages/experimental/agent-team/src/projection.ts +++ b/packages/experimental/agent-team/src/projection.ts @@ -1,4 +1,4 @@ -/** Host-only Team state projected incrementally from committed Session events. */ +/** Team state projected incrementally from committed Session events, with a durable-only client view. */ import { z } from 'zod' import { brandString } from '@deepseek-ai/dsh-brand' @@ -7,10 +7,13 @@ import type { SessionEvent, SessionEventMap, SessionId } from '@deepseek-ai/dsh- import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection' import type { TeamId, + TeamMemberProjection, TeamMemberSnapshot, TeamMessageId, TeamMessageSnapshot, + TeamProjection, TeamTaskSnapshot, + TeamTaskView, } from './types.ts' import { TeamId as toTeamId, @@ -18,6 +21,7 @@ import { TeamTaskId as toTeamTaskId, } from './types.ts' import { assertTaskGraphCandidate } from './task-graph.ts' +import { projectTaskView } from './task-view.ts' const nonNegativeSafeInteger = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER) const positiveSafeInteger = nonNegativeSafeInteger.min(1) @@ -119,20 +123,24 @@ const teamMessageDeliveredEventSchema = z.object({ targetId: sessionIdSchema, }).strict() as z.ZodType -/** Current Team state selected by durable Team identity. */ +/** + * Current Team state selected by durable Team identity. Every applied Team + * event produces a new state object and replaces only the collection it + * touched; untouched collections keep their references. + */ export interface TeamState { readonly id: TeamId - readonly members: TeamMemberSnapshot[] - readonly tasks: TeamTaskSnapshot[] - readonly messages: TeamMessageSnapshot[] - readonly delivered: TeamMessageId[] - nextTaskNumber: number + readonly members: readonly TeamMemberSnapshot[] + readonly tasks: readonly TeamTaskSnapshot[] + readonly messages: readonly TeamMessageSnapshot[] + readonly delivered: readonly TeamMessageId[] + readonly nextTaskNumber: number } /** * Construct empty state for one Team identity. * @param rootId - root Session identity. - * @returns mutable empty Team state. + * @returns empty Team state. */ export function emptyTeamState(rootId: SessionId): TeamProjectionState { return { @@ -147,7 +155,7 @@ export function emptyTeamState(rootId: SessionId): TeamProjectionState { /** Checkpoint-safe state for the Team owned by the projected Session. */ export interface TeamProjectionState extends TeamState { - failure?: string + readonly failure?: string } declare module '@deepseek-ai/dsh-session-projection/types' { @@ -214,23 +222,30 @@ function parseCurrentTeamEvent(event: TeamSessionEvent): TeamSessionEvent { } } -function applyProjectionEvent(state: TeamProjectionState, event: SessionEvent): void { - if (state.failure !== undefined) return - if (!isTeamEvent(event)) return +function applyProjectionEvent(state: TeamProjectionState, event: SessionEvent): TeamProjectionState { + if (state.failure !== undefined) return state + if (!isTeamEvent(event)) return state try { const selector = parsePersisted(event.type, teamEventSelectorSchema, event.data) - if (selector.teamId !== state.id) return + if (selector.teamId !== state.id) return state if (selector.version !== 2) { throw new Error(`unsupported Agent Teams event version ${String(selector.version)}`) } - applyCurrentTeamEvent(state, parseCurrentTeamEvent(event)) + return applyCurrentTeamEvent(state, parseCurrentTeamEvent(event)) } catch (error: unknown) { /* v8 ignore next -- the owned Team transition throws Error instances. */ - state.failure = error instanceof Error ? error.message : String(error) + return { ...state, failure: error instanceof Error ? error.message : String(error) } } } -function applyCurrentTeamEvent(state: TeamState, event: TeamSessionEvent): void { +function replaceAt(items: readonly T[], index: number, item: T): T[] { + const next = [...items] + if (index < 0) next.push(item) + else next[index] = item + return next +} + +function applyCurrentTeamEvent(state: TeamProjectionState, event: TeamSessionEvent): TeamProjectionState { switch (event.type) { case 'team/member': { const member = event.data.member @@ -250,9 +265,7 @@ function applyCurrentTeamEvent(state: TeamState, event: TeamSessionEvent): void throw new Error(`teammate "${member.name}" has an invalid ${prior.phase} -> ${member.phase} transition`) } } - if (index < 0) state.members.push(member) - else state.members[index] = member - break + return { ...state, members: replaceAt(state.members, index, member) } } case 'team/task': { const task = event.data.task @@ -265,48 +278,117 @@ function applyCurrentTeamEvent(state: TeamState, event: TeamSessionEvent): void throw new Error(`team task "${task.id}" revision is not contiguous`) } assertTaskGraphCandidate(state.tasks, task) + let nextTaskNumber = state.nextTaskNumber const match = numericTaskIdPattern.exec(task.id) if (match !== null) { const number = Number(match[1]) - state.nextTaskNumber = Math.max( - state.nextTaskNumber, + nextTaskNumber = Math.max( + nextTaskNumber, number === Number.MAX_SAFE_INTEGER ? number : number + 1, ) } - if (index < 0) state.tasks.push(task) - else state.tasks[index] = task - break + return { ...state, tasks: replaceAt(state.tasks, index, task), nextTaskNumber } } case 'team/message/queued': { const message = event.data.message if (state.messages.some(candidate => candidate.id === message.id)) { throw new Error(`team message "${message.id}" was queued twice`) } - state.messages.push(message) - break + return { ...state, messages: [...state.messages, message] } } case 'team/message/delivered': { const queued = state.messages.find(message => message.id === event.data.messageId) if (queued === undefined) throw new Error(`team message "${event.data.messageId}" was delivered before queueing`) if (queued.targetId !== event.data.targetId) throw new Error(`team message "${event.data.messageId}" target changed`) if (state.delivered.includes(event.data.messageId)) throw new Error(`team message "${event.data.messageId}" was delivered twice`) - state.delivered.push(event.data.messageId) - break + return { ...state, delivered: [...state.delivered, event.data.messageId] } } /* v8 ignore next 2 -- TeamEventType is closed and every member is handled above. */ default: - return + return state } } -/** Host-only Team projection selected by the projected Session identity. */ +const teamMemberProjectionSchema = z.object({ + id: sessionIdSchema, + name: z.string(), + role: z.enum(['lead', 'teammate']), + phase: z.enum(['provisioning', 'active', 'failed']), + error: z.string().optional(), +}).strict() as z.ZodType + +const teamTaskViewSchema = z.object({ + id: teamTaskIdSchema, + revision: positiveSafeInteger, + subject: z.string(), + description: z.string(), + status: z.enum(['pending', 'in_progress', 'completed', 'deleted']), + blockedBy: z.array(teamTaskIdSchema), + writeScopes: z.array(z.string()), + ownerName: z.string().optional(), + ready: z.boolean(), + writeScopeWarnings: z.array(z.string()), +}).strict() as z.ZodType + +const teamProjectionSchema = z.object({ + members: z.array(teamMemberProjectionSchema), + tasks: z.array(teamTaskViewSchema), + failure: z.string().optional(), +}).strict() as z.ZodType + +/** Client views keyed by the member and task collections they were derived from. */ +const teamProjectionViews = new WeakMap>() + +function buildTeamProjection(state: TeamProjectionState): TeamProjection { + const rootId = brandString(state.id) + const members: TeamMemberProjection[] = [{ id: rootId, name: 'lead', role: 'lead', phase: 'active' }] + for (const member of state.members) { + members.push({ + id: member.id, + name: member.name, + role: 'teammate', + phase: member.phase, + ...member.error === undefined ? {} : { error: member.error }, + }) + } + return { + members, + tasks: state.tasks + .filter(task => task.status !== 'deleted') + .map(task => projectTaskView(state, task)), + ...state.failure === undefined ? {} : { failure: state.failure }, + } +} + +/** + * Durable client view of one Team state. Mailbox-only state changes reuse the + * previous view reference, so the live drive publishes nothing for them. + * A failure is terminal: later events retain the failed state reference and + * do not republish its view. + * @param state - current Team state. + * @returns the roster and non-deleted task board, plus any projection failure. + */ +export function teamProjectionView(state: TeamProjectionState): TeamProjection { + if (state.failure !== undefined) return buildTeamProjection(state) + let byTasks = teamProjectionViews.get(state.members) + if (byTasks === undefined) { + byTasks = new WeakMap() + teamProjectionViews.set(state.members, byTasks) + } + let view = byTasks.get(state.tasks) + if (view === undefined) { + view = buildTeamProjection(state) + byTasks.set(state.tasks, view) + } + return view +} + +/** Team projection selected by the projected Session identity; the wire view carries durable roster and task state only. */ export const teamProjectionDefinition = { key: 'agentTeam', stateVersion: 4, stateSchema: teamProjectionEntrySchema, init: header => emptyTeamState(header.id), - apply: (state, event) => { - applyProjectionEvent(state, event) - return state - }, + apply: applyProjectionEvent, + wire: { viewSchema: teamProjectionSchema, view: teamProjectionView }, } satisfies ProjectionDefinition<'agentTeam', TeamProjectionState> diff --git a/packages/experimental/agent-team/src/task-board.ts b/packages/experimental/agent-team/src/task-board.ts index cc7064ceb6..4b6132526a 100644 --- a/packages/experimental/agent-team/src/task-board.ts +++ b/packages/experimental/agent-team/src/task-board.ts @@ -15,13 +15,9 @@ import type { TeamTaskView, UpdateTeamTaskRequest, } from './types.ts' +import { projectTaskView, taskReady } from './task-view.ts' import { requiredText, writeScope } from './validation.ts' -/** Whether two normalized file or directory prefixes overlap on path components. */ -function scopesOverlap(left: string, right: string): boolean { - return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`) -} - const TASK_GRAPH_ERROR_CODES: Record = { missing: 'TEAM_TASK_NOT_FOUND', duplicate: 'TEAM_INVALID_ARGUMENT', @@ -68,7 +64,7 @@ export class TeamTaskBoard { } this.assertTaskGraph(state, task) await this.journal.appendAndFlush(root, 'team/task', { version: 2, teamId: TeamId(root.id), task }) - return this.taskView(root, state, task) + return projectTaskView(state, task) }) } @@ -83,7 +79,7 @@ export class TeamTaskBoard { const state = this.journal.state(root) const task = state.tasks.find(candidate => candidate.id === id) if (task === undefined) throw new TeamError(`team task "${id}" not found`, 'TEAM_TASK_NOT_FOUND') - return this.taskView(root, state, task) + return projectTaskView(state, task) } /** @@ -96,7 +92,7 @@ export class TeamTaskBoard { const state = this.journal.state(root) return state.tasks .filter(task => task.status !== 'deleted') - .map(task => this.taskView(root, state, task)) + .map(task => projectTaskView(state, task)) } /** @@ -134,7 +130,7 @@ export class TeamTaskBoard { if (current.ownerId !== undefined && current.ownerId !== caller.id) { throw new TeamError(`team task "${current.id}" is owned by another member`, 'TEAM_TASK_ALREADY_CLAIMED') } - if (current.status !== 'pending' || !this.taskReady(state, current)) { + if (current.status !== 'pending' || !taskReady(state, current)) { throw new TeamError(`team task "${current.id}" is not ready to claim`, 'TEAM_TASK_BLOCKED') } next = { ...current, status: 'in_progress', ownerId: caller.id } @@ -185,7 +181,7 @@ export class TeamTaskBoard { next = this.withoutOwner({ ...current, status: 'pending' }) break } - if (!this.taskReady(state, current)) throw new TeamError(`team task "${current.id}" is blocked`, 'TEAM_TASK_BLOCKED') + if (!taskReady(state, current)) throw new TeamError(`team task "${current.id}" is blocked`, 'TEAM_TASK_BLOCKED') const assignee = resolveActiveMember(root, state, request.owner) next = { ...current, status: 'in_progress', ownerId: assignee.id } break @@ -210,7 +206,7 @@ export class TeamTaskBoard { } this.assertTaskGraph(state, task) await this.journal.appendAndFlush(root, 'team/task', { version: 2, teamId: TeamId(root.id), task }) - return this.taskView(root, state, task) + return projectTaskView(state, task) }) } @@ -251,47 +247,9 @@ export class TeamTaskBoard { } } - /** Whether all current blockers completed. */ - private taskReady(state: TeamState, task: TeamTaskSnapshot): boolean { - return task.blockedBy.every(id => state.tasks.find(candidate => candidate.id === id)?.status === 'completed') - } - /** Remove an optional owner field under exactOptionalPropertyTypes. */ private withoutOwner(task: TeamTaskSnapshot): TeamTaskSnapshot { const { ownerId: _ownerId, ...without } = task return without } - - /** - * Build one task view with owner name, readiness, and advisory write overlaps. - * A committing caller may pass its pre-append state because `task` supplies the - * new value explicitly; owner names, blocker readiness, and other task scopes - * do not change when that snapshot is appended. - */ - private taskView(root: Agent, state: TeamState, task: TeamTaskSnapshot): TeamTaskView { - const ownerName = task.ownerId === undefined - ? undefined - : task.ownerId === root.id - ? 'lead' - : state.members.find(member => member.id === task.ownerId)?.name - const warnings = new Set() - for (const other of state.tasks) { - if (other.id === task.id || other.status !== 'in_progress') continue - if (task.writeScopes.some(left => other.writeScopes.some(right => scopesOverlap(left, right)))) { - warnings.add(`write scopes overlap with ${other.id}`) - } - } - return { - id: task.id, - revision: task.revision, - subject: task.subject, - description: task.description, - status: task.status, - blockedBy: structuredClone(task.blockedBy), - writeScopes: structuredClone(task.writeScopes), - ...ownerName === undefined ? {} : { ownerName }, - ready: task.status === 'pending' && this.taskReady(state, task), - writeScopeWarnings: [...warnings], - } - } } diff --git a/packages/experimental/agent-team/src/task-view.ts b/packages/experimental/agent-team/src/task-view.ts new file mode 100644 index 0000000000..569ff38261 --- /dev/null +++ b/packages/experimental/agent-team/src/task-view.ts @@ -0,0 +1,62 @@ +/** Pure task-view derivation shared by the task board and the client projection. */ + +import { brandString } from '@deepseek-ai/dsh-brand' +import type { SessionId } from '@deepseek-ai/dsh-session' +import type { TeamState } from './projection.ts' +import type { TeamTaskSnapshot, TeamTaskView } from './types.ts' + +/** + * Whether two normalized file or directory prefixes overlap on path components. + * @param left - normalized write scope. + * @param right - normalized write scope. + * @returns whether either scope contains the other. + */ +function scopesOverlap(left: string, right: string): boolean { + return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`) +} + +/** + * Whether every current blocker of one task completed. + * @param state - Team state supplying sibling tasks. + * @param task - durable task snapshot to test. + * @returns whether the task has no incomplete blockers. + */ +export function taskReady(state: TeamState, task: TeamTaskSnapshot): boolean { + return task.blockedBy.every(id => state.tasks.find(candidate => candidate.id === id)?.status === 'completed') +} + +/** + * Derive one task view with owner name, readiness, and advisory write overlaps. + * A committing caller may pass its pre-append state because `task` supplies the + * new value explicitly; owner names, blocker readiness, and other task scopes + * do not change when that snapshot is appended. + * @param state - Team state supplying members and sibling tasks. + * @param task - durable task snapshot to view. + * @returns a detached task view. + */ +export function projectTaskView(state: TeamState, task: TeamTaskSnapshot): TeamTaskView { + const ownerName = task.ownerId === undefined + ? undefined + : task.ownerId === brandString(state.id) + ? 'lead' + : state.members.find(member => member.id === task.ownerId)?.name + const warnings = new Set() + for (const other of state.tasks) { + if (other.id === task.id || other.status !== 'in_progress') continue + if (task.writeScopes.some(left => other.writeScopes.some(right => scopesOverlap(left, right)))) { + warnings.add(`write scopes overlap with ${other.id}`) + } + } + return { + id: task.id, + revision: task.revision, + subject: task.subject, + description: task.description, + status: task.status, + blockedBy: [...task.blockedBy], + writeScopes: [...task.writeScopes], + ...ownerName === undefined ? {} : { ownerName }, + ready: task.status === 'pending' && taskReady(state, task), + writeScopeWarnings: [...warnings], + } +} diff --git a/packages/experimental/agent-team/src/types.ts b/packages/experimental/agent-team/src/types.ts index 77aa6fe620..9921886f6f 100644 --- a/packages/experimental/agent-team/src/types.ts +++ b/packages/experimental/agent-team/src/types.ts @@ -96,10 +96,32 @@ export interface TeamTaskView { readonly writeScopeWarnings: string[] } -/** Point-in-time roster and task-board projection returned to browser clients. */ -export interface TeamView { - readonly members: TeamMemberView[] +/** One durable roster row published through the `agentTeam` Session projection. */ +export interface TeamMemberProjection { + readonly id: SessionId + readonly name: string + readonly role: 'lead' | 'teammate' + /** Durable lifecycle; the Lead row is always `active`. Turn activity comes from Session status. */ + readonly phase: TeamMemberPhase + readonly error?: string +} + +/** + * Durable Team state published to browser clients through the Lead Session's + * `agentTeam` projection. `failure` names the first rejected persisted Team + * record; members and tasks then stay at the last valid state. + */ +export interface TeamProjection { + readonly members: TeamMemberProjection[] readonly tasks: TeamTaskView[] + readonly failure?: string +} + +declare module '@deepseek-ai/dsh-session-projection/types' { + interface SessionProjectionMap { + /** Durable roster and non-deleted task board of the Team rooted at the projected Session. */ + agentTeam: TeamProjection + } } /** One peer message retained until its target Session records it. */ diff --git a/packages/experimental/agent-team/tests/built-lib.e2e.ts b/packages/experimental/agent-team/tests/built-lib.e2e.ts index 4e2d762fd6..d171624d80 100644 --- a/packages/experimental/agent-team/tests/built-lib.e2e.ts +++ b/packages/experimental/agent-team/tests/built-lib.e2e.ts @@ -1,4 +1,4 @@ -/** Plain-Node smoke for the built Agent Teams service and Remote contribution. */ +/** Plain-Node smoke for the built Agent Teams service. */ import { execFile } from 'node:child_process' import { existsSync } from 'node:fs' @@ -13,21 +13,17 @@ const artifactUrl = (path: string): string => pathToFileURL(artifact(path)).href const requiredArtifacts = [ 'packages/experimental/agent-team/lib/index.js', - 'packages/experimental/agent-team/lib/typert.remote-client.js', ].every(path => existsSync(artifact(path))) describe.skipIf(!requiredArtifacts)('Agent Teams built LIB service', () => { - it('loads the Host service and its generated browser contribution under plain Node', async () => { + it('loads the Host service under plain Node', async () => { const urls = { host: artifactUrl('packages/experimental/agent-team/lib/index.js'), - remote: artifactUrl('packages/experimental/agent-team/lib/typert.remote-client.js'), } const script = ` const host = await import(${JSON.stringify(urls.host)}) - const remote = await import(${JSON.stringify(urls.remote)}) console.log(JSON.stringify({ className: host.default.name, - methods: remote.default.descriptors.map(descriptor => descriptor.id), })) ` @@ -35,13 +31,9 @@ describe.skipIf(!requiredArtifacts)('Agent Teams built LIB service', () => { expect(result.exitCode, `stderr:\n${result.stderr}`).toBe(0) const output = JSON.parse(result.stdout.trim().split('\n').at(-1) ?? '{}') as { className: string - methods: string[] } expect(output).toEqual({ className: 'TeamService', - methods: [ - '@deepseek-ai/dsh-experimental-agent-team#agentTeams/view', - ], }) }) }) diff --git a/packages/experimental/agent-team/tests/persistence.spec.ts b/packages/experimental/agent-team/tests/persistence.spec.ts index 5237fc0301..f7ee5d41cb 100644 --- a/packages/experimental/agent-team/tests/persistence.spec.ts +++ b/packages/experimental/agent-team/tests/persistence.spec.ts @@ -26,9 +26,9 @@ const contexts = new Set() /** Detached durable Team read through the same projection definition as the service. */ function durable(agent: Agent): { - members: TeamMemberSnapshot[] - tasks: TeamTaskSnapshot[] - pendingMessages: TeamMessageSnapshot[] + members: readonly TeamMemberSnapshot[] + tasks: readonly TeamTaskSnapshot[] + pendingMessages: readonly TeamMessageSnapshot[] } { let projected = teamProjectionDefinition.init(agent.session.header) for (const event of agent.session.snapshotEvents()) projected = teamProjectionDefinition.apply(projected, event) diff --git a/packages/experimental/agent-team/tests/projection-events.spec.ts b/packages/experimental/agent-team/tests/projection-events.spec.ts index 333f157eb0..2266b41483 100644 --- a/packages/experimental/agent-team/tests/projection-events.spec.ts +++ b/packages/experimental/agent-team/tests/projection-events.spec.ts @@ -4,7 +4,7 @@ import { Context } from '@deepseek-ai/cordis' import SessionStore, { SESSION_FORMAT_VERSION, SessionId, SessionLogOffset, SessionSeq } from '@deepseek-ai/dsh-session' import type { SessionEvent, SessionEventMap, SessionEventType } from '@deepseek-ai/dsh-session' import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' -import { teamProjectionDefinition } from '../src/projection.ts' +import { teamProjectionDefinition, teamProjectionView } from '../src/projection.ts' import type { TeamProjectionState, TeamState } from '../src/projection.ts' import { TeamId, TeamMessageId, TeamTaskId } from '../src/types.ts' import type { TeamMemberSnapshot, TeamMessageSnapshot, TeamTaskSnapshot } from '../src/types.ts' @@ -424,4 +424,161 @@ describe('Agent Teams projection events', () => { } as unknown as SessionEvent expect(isEmptyState(projectTeam(ROOT, [inherited]))).toBe(true) }) + + describe('copy-on-write state and client view', () => { + const memberEvent = event('team/member', { version: 2, teamId: TEAM, member: member() }, SessionSeq(0)) + const taskEvent = event('team/task', { version: 2, teamId: TEAM, task: task() }, SessionSeq(1)) + const queuedEvent = event('team/message/queued', { version: 2, teamId: TEAM, message: message() }, SessionSeq(2)) + const deliveredEvent = event('team/message/delivered', { + version: 2, teamId: TEAM, messageId: TeamMessageId('message-1'), targetId: CHILD, + }, SessionSeq(3)) + + it('returns the same state for unrelated events and a new state that replaces only the touched collection', () => { + const initial = project(ROOT, []) + expect(teamProjectionDefinition.apply(initial, { type: 'turn/start', data: { turn: 1 }, seq: 0, time: 0 } as SessionEvent)).toBe(initial) + expect(teamProjectionDefinition.apply(initial, event('team/member', { + version: 2, teamId: TeamId('ancestor'), member: member(), + }, SessionSeq(0)))).toBe(initial) + + const withMember = teamProjectionDefinition.apply(initial, memberEvent) + expect(withMember).not.toBe(initial) + expect(initial.members).toEqual([]) + expect(withMember.members).toHaveLength(1) + expect(withMember.tasks).toBe(initial.tasks) + + const withTask = teamProjectionDefinition.apply(withMember, taskEvent) + expect(withTask.members).toBe(withMember.members) + expect(withTask.tasks).not.toBe(withMember.tasks) + expect(withMember.tasks).toEqual([]) + + const withMail = teamProjectionDefinition.apply(withTask, queuedEvent) + expect(withMail.members).toBe(withTask.members) + expect(withMail.tasks).toBe(withTask.tasks) + expect(withMail.messages).not.toBe(withTask.messages) + expect(withTask.messages).toEqual([]) + + const withReceipt = teamProjectionDefinition.apply(withMail, deliveredEvent) + expect(withReceipt.delivered).not.toBe(withMail.delivered) + expect(withMail.delivered).toEqual([]) + expect(teamProjectionDefinition.apply(withReceipt, { ...deliveredEvent, seq: SessionSeq(4) })).not.toBe(withReceipt) + }) + + it('enters failure through a new state that keeps the last valid collections', () => { + const valid = project(ROOT, [memberEvent, taskEvent]) + const failed = teamProjectionDefinition.apply(valid, event('team/task', { + version: 1 as 2, teamId: TEAM, task: task(), + }, SessionSeq(2))) + expect(failed).not.toBe(valid) + expect(valid.failure).toBeUndefined() + expect(failed.failure).toMatch(/unsupported Agent Teams event version 1/) + expect(failed.members).toBe(valid.members) + expect(failed.tasks).toBe(valid.tasks) + expect(teamProjectionDefinition.apply(failed, queuedEvent)).toBe(failed) + }) + + it('views durable roster and non-deleted derived tasks and reuses the view across mailbox-only changes', () => { + const activeMember = event('team/member', { version: 2, teamId: TEAM, member: member({ phase: 'active' }) }, SessionSeq(1)) + const claimed = event('team/task', { + version: 2, teamId: TEAM, task: task({ revision: 2, status: 'in_progress', ownerId: CHILD, writeScopes: ['src'] }), + }, SessionSeq(2)) + const blocked = event('team/task', { + version: 2, teamId: TEAM, task: task({ id: TeamTaskId('task-2'), blockedBy: [TeamTaskId('task-1')], writeScopes: ['src/web'] }), + }, SessionSeq(3)) + const leadOwned = event('team/task', { + version: 2, teamId: TEAM, task: task({ id: TeamTaskId('task-3'), revision: 1, status: 'in_progress', ownerId: ROOT }), + }, SessionSeq(4)) + const deleted = event('team/task', { + version: 2, teamId: TEAM, task: task({ id: TeamTaskId('task-3'), revision: 2, status: 'deleted', ownerId: ROOT }), + }, SessionSeq(5)) + const state = project(ROOT, [memberEvent, activeMember, taskEvent, claimed, blocked, leadOwned, deleted]) + const view = teamProjectionView(state) + expect(teamProjectionDefinition.wire.viewSchema.parse(view)).toEqual(view) + expect(view).toEqual({ + members: [ + { id: ROOT, name: 'lead', role: 'lead', phase: 'active' }, + { id: CHILD, name: 'worker-a', role: 'teammate', phase: 'active' }, + ], + tasks: [ + { + id: 'task-1', revision: 2, subject: 'subject', description: 'description', status: 'in_progress', + blockedBy: [], writeScopes: ['src'], ownerName: 'worker-a', ready: false, writeScopeWarnings: [], + }, + { + id: 'task-2', revision: 1, subject: 'subject', description: 'description', status: 'pending', + blockedBy: ['task-1'], writeScopes: ['src/web'], ready: false, writeScopeWarnings: ['write scopes overlap with task-1'], + }, + ], + }) + expect(view).not.toHaveProperty('failure') + expect(teamProjectionView(state)).toBe(view) + + const withMail = teamProjectionDefinition.apply(state, { ...queuedEvent, seq: SessionSeq(6) }) + expect(teamProjectionView(withMail)).toBe(view) + const withMember = teamProjectionDefinition.apply(withMail, event('team/member', { + version: 2, teamId: TEAM, member: member({ id: SessionId('child-b'), name: 'worker-b' }), + }, SessionSeq(7))) + const memberView = teamProjectionView(withMember) + expect(memberView).not.toBe(view) + expect(memberView.members.at(-1)).toEqual({ + id: 'child-b', name: 'worker-b', role: 'teammate', phase: 'provisioning', + }) + + const failedMember = event('team/member', { + version: 2, teamId: TEAM, member: member({ id: SessionId('child-b'), name: 'worker-b', phase: 'failed', error: 'provider failed' }), + }, SessionSeq(8)) + expect(teamProjectionView(teamProjectionDefinition.apply(withMember, failedMember)).members.at(-1)) + .toMatchObject({ phase: 'failed', error: 'provider failed' }) + }) + + it('publishes the failure with the last valid roster and tasks', () => { + const state = project(ROOT, [memberEvent, taskEvent, event('team/task', { + version: 1 as 2, teamId: TEAM, task: task(), + }, SessionSeq(2))]) + const view = teamProjectionView(state) + expect(view.failure).toMatch(/unsupported Agent Teams event version 1/) + expect(view.members).toHaveLength(2) + expect(view.tasks).toHaveLength(1) + expect(teamProjectionDefinition.wire.viewSchema.parse(view)).toEqual(view) + }) + + it('drives live agentTeam publications per Session through the projection registry', async () => { + const ctx = new Context() + onTestFinished(() => ctx.fiber.dispose()) + await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) + ctx.effect(() => ctx.sessionProjections.register(teamProjectionDefinition)) + const published: Array<{ session: SessionId; key: string; value: unknown }> = [] + ctx.sessionProjections.onChanged((session, key, value) => { + published.push({ session: session.id, key, value }) + }) + const lead = ctx.sessions.create(ROOT) + const other = ctx.sessions.create(SessionId('other-root')) + expect(ctx.sessionProjections.snapshot(lead).values['agentTeam']).toEqual({ + members: [{ id: ROOT, name: 'lead', role: 'lead', phase: 'active' }], + tasks: [], + }) + + lead.append('team/task', taskEvent.data) + expect(published).toHaveLength(1) + expect(published[0]).toMatchObject({ + session: ROOT, + key: 'agentTeam', + value: { tasks: [{ id: 'task-1', ready: true }] }, + }) + + lead.append('team/message/queued', queuedEvent.data) + lead.append('turn/start', { turn: 1 }) + expect(published).toHaveLength(1) + + other.append('team/task', { version: 2, teamId: TeamId(other.id), task: task() }) + expect(published).toHaveLength(2) + expect(published[1]).toMatchObject({ + session: other.id, + value: { members: [{ id: other.id, name: 'lead', role: 'lead', phase: 'active' }] }, + }) + expect(ctx.sessionProjections.snapshot(lead).values['agentTeam']).toMatchObject({ + tasks: [{ id: 'task-1' }], + }) + }) + }) }) diff --git a/packages/experimental/agent-team/tests/team.spec.ts b/packages/experimental/agent-team/tests/team.spec.ts index 87d63353ed..af939b0108 100644 --- a/packages/experimental/agent-team/tests/team.spec.ts +++ b/packages/experimental/agent-team/tests/team.spec.ts @@ -22,17 +22,19 @@ import { TestSessionQuery } from './test-session-query.ts' const SIGNAL = new AbortController().signal const roots: string[] = [] +const contexts: Context[] = [] -afterEach(() => { +afterEach(async () => { vi.useRealTimers() + for (const ctx of contexts.splice(0).reverse()) await ctx.fiber.dispose() for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }) }) /** Detached durable Team read through the same projection definition as the service. */ function durable(agent: Agent): { - members: TeamMemberSnapshot[] - tasks: TeamTaskSnapshot[] - pendingMessages: TeamMessageSnapshot[] + members: readonly TeamMemberSnapshot[] + tasks: readonly TeamTaskSnapshot[] + pendingMessages: readonly TeamMessageSnapshot[] } { let projected = teamProjectionDefinition.init(agent.session.header) for (const event of agent.session.snapshotEvents()) projected = teamProjectionDefinition.apply(projected, event) @@ -60,6 +62,7 @@ async function setup( config: ConstructorParameters[1] = {}, ) { const ctx = new Context() + contexts.push(ctx) await mountAgentLoopTestDependencies(ctx) const storageRoot = mkdtempSync(join(tmpdir(), 'dsh-team-')) roots.push(storageRoot) @@ -167,6 +170,7 @@ describe('Team identity and provisioning', () => { it('supports direct-constructor defaults and recovers roots that already exist', async () => { const ctx = new Context() + contexts.push(ctx) await mountAgentLoopTestDependencies(ctx) const storageRoot = mkdtempSync(join(tmpdir(), 'dsh-team-direct-')) roots.push(storageRoot) @@ -873,24 +877,6 @@ describe('Team shared task DAG', () => { }) }) -describe('Team Remote API', () => { - it('reads tasks created and updated by Team agents', async () => { - const { ctx, lead } = await setup([]) - expect(ctx.agentTeams.typertRemote).toMatchObject({ serviceKey: 'agentTeams', namespace: 'agentTeams' }) - expect(ctx.agentTeams.remoteView(lead)).toEqual({ - members: [expect.objectContaining({ name: 'lead', role: 'lead', status: 'inactive' })], - tasks: [], - }) - const created = await ctx.agentTeams.createTask(lead, { - subject: 'Agent task', description: 'Created by the Team Lead', - }) - const updated = await ctx.agentTeams.updateTask(lead, { - taskId: created.id, expectedRevision: 1, action: 'claim', - }) - expect(ctx.agentTeams.remoteView(lead).tasks).toEqual([updated]) - }) -}) - describe('Team mailbox and waiting', () => { it('steers a message addressed to the Lead and checkpoints its receipt', async () => { const { ctx, lead } = await setup(['hang']) @@ -1364,6 +1350,7 @@ describe('Team mailbox and waiting', () => { it('waits for one change, supports cancellation, times out, and releases waiters on HMR disposal', async () => { const ctx = new Context() + contexts.push(ctx) await mountAgentLoopTestDependencies(ctx) const storageRoot = mkdtempSync(join(tmpdir(), 'dsh-team-wait-')) roots.push(storageRoot) diff --git a/packages/experimental/agent-team/tsconfig.json b/packages/experimental/agent-team/tsconfig.json index 862b4b5836..669a476e44 100644 --- a/packages/experimental/agent-team/tsconfig.json +++ b/packages/experimental/agent-team/tsconfig.json @@ -13,7 +13,6 @@ { "path": "../../llm/llm" }, { "path": "../../core/session" }, { "path": "../../core/agent" }, - { "path": "../../typert/protocol" }, { "path": "../../subagent/subagent" }, { "path": "../../session/session-persistence" }, { "path": "../../session/session-projection" }, diff --git a/packages/experimental/api-speech-to-text/README.i18n.yaml b/packages/experimental/api-speech-to-text/README.i18n.yaml index 54e9523ced..454b3e5dc8 100644 --- a/packages/experimental/api-speech-to-text/README.i18n.yaml +++ b/packages/experimental/api-speech-to-text/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/experimental/api-speech-to-text/README.md -README.md: ecc83c15b4217323c074733ae77259261598803c -README.zh.md: 46ac6a16c902c32f2effb23a98141b1afbdccd37 +README.md: 113c1f94459329051182ab2503b5220ffa9ad50b +README.zh.md: bc853bd00da978a42d56dcbefcd98109efc78491 diff --git a/packages/experimental/api-speech-to-text/README.md b/packages/experimental/api-speech-to-text/README.md index ecc83c15b4..113c1f9445 100644 --- a/packages/experimental/api-speech-to-text/README.md +++ b/packages/experimental/api-speech-to-text/README.md @@ -27,6 +27,8 @@ The `speech` Remote connects browser recordings to `ctx.speechToText`. It expose Compose with the speech Service Definition and Typert. `maxAudioBytes` and `maxDurationSeconds` limit accepted recordings. The browser UI mounts this package’s generated `/remote` contribution when enabled. +`prepare(providerId, { downloadSource })` forwards one advertised source to the provider; omission keeps its configured policy. The catalog carries `downloadSources` for the picker. The provider rejects unavailable choices and changes to an active task’s source. + ----- diff --git a/packages/experimental/api-speech-to-text/README.zh.md b/packages/experimental/api-speech-to-text/README.zh.md index 46ac6a16c9..bc853bd00d 100644 --- a/packages/experimental/api-speech-to-text/README.zh.md +++ b/packages/experimental/api-speech-to-text/README.zh.md @@ -27,6 +27,8 @@ kind: "package-reference" 与语音服务定义和 Typert 组合。`maxAudioBytes` 与 `maxDurationSeconds` 限制接收的录音。浏览器 UI 启用时挂载本包生成的 `/remote` 贡献。 +`prepare(providerId, { downloadSource })` 将公布的一个下载源转发给提供方,省略时保留其配置策略。目录通过 `downloadSources` 提供选择器选项。提供方拒绝不可用的源,以及对进行中任务的源变更。 + ----- diff --git a/packages/experimental/api-speech-to-text/package.json b/packages/experimental/api-speech-to-text/package.json index 80c3b31208..2b6235edb2 100644 --- a/packages/experimental/api-speech-to-text/package.json +++ b/packages/experimental/api-speech-to-text/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-api-speech-to-text", "description": "Authenticated experimental speech transcription for browser clients", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/api-speech-to-text/src/index.ts b/packages/experimental/api-speech-to-text/src/index.ts index ce53084e4b..4d19d7c5af 100644 --- a/packages/experimental/api-speech-to-text/src/index.ts +++ b/packages/experimental/api-speech-to-text/src/index.ts @@ -3,7 +3,7 @@ import { Context } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' import { Remote, RemoteError, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol' import type {} from '@deepseek-ai/dsh-experimental-speech-to-text' -import type { SpeechProviderId, SpeechSelectionPatch, Transcript } from '@deepseek-ai/dsh-experimental-speech-to-text/types' +import type { SpeechPreparationOptions, SpeechProviderId, SpeechSelectionPatch, Transcript } from '@deepseek-ai/dsh-experimental-speech-to-text/types' import type { SpeechCatalog, TranscriptionRequest } from './types.ts' import { validateWave } from '@deepseek-ai/dsh-experimental-speech-to-text/wave' @@ -66,9 +66,10 @@ export default class SpeechController extends TypertRemoteService { /** * Start or join one Host-owned preparation task. * @param providerId - selected recognizer. + * @param options - task-local source selection validated by the provider. */ @Remote - prepare(providerId: SpeechProviderId): void { this.ctx.speechToText.prepare(providerId) } + prepare(providerId: SpeechProviderId, options?: SpeechPreparationOptions): void { this.ctx.speechToText.prepare(providerId, options) } /** * Explicitly cancel resource preparation. diff --git a/packages/experimental/api-speech-to-text/tests/controller.spec.ts b/packages/experimental/api-speech-to-text/tests/controller.spec.ts index 0cf59f5449..caa2145ea2 100644 --- a/packages/experimental/api-speech-to-text/tests/controller.spec.ts +++ b/packages/experimental/api-speech-to-text/tests/controller.spec.ts @@ -39,6 +39,20 @@ it('lists limits without inference and forwards explicit provider and language', expect(recognize).toHaveBeenLastCalledWith(expect.objectContaining({ language: 'auto' }), expect.any(AbortSignal)) }) +it('forwards an explicit download source to the registered preparation owner', async () => { + const ctx = new Context(); roots.push(ctx) + const speech = new SpeechToText(ctx, SpeechToText.Config({ defaultProvider: id })) + const prepare = vi.fn() + speech.register({ info: { id, name: 'local', location: 'host-local', languages: ['auto'], downloadSources: ['https://hf-mirror.com'] }, + preparation: { snapshot: () => ({ phase: 'unprepared' }), subscribe: () => () => {}, prepare, cancel: async () => {} }, + transcribe: async () => ({ text: '', audioSeconds: 0, inferenceSeconds: 0 }), + }) + const api = new SpeechController(ctx, { maxAudioBytes: 32044, maxDurationSeconds: 1 }) + expect(api.catalog().providers[0]?.downloadSources).toEqual(['https://hf-mirror.com']) + api.prepare(id, { downloadSource: 'https://hf-mirror.com' }) + expect(prepare).toHaveBeenCalledWith({ downloadSource: 'https://hf-mirror.com' }) +}) + it('rejects oversized, noncanonical, malformed and overlong audio without inference', async () => { const { api, recognize } = fixture() for (const audioBase64 of ['x'.repeat(50000), 'AAAA?', '', 'QQ==', recording() + '\n']) { diff --git a/packages/experimental/auto-review/package.json b/packages/experimental/auto-review/package.json index 1e844007f6..c6cbbd994a 100644 --- a/packages/experimental/auto-review/package.json +++ b/packages/experimental/auto-review/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-auto-review", "description": "Per-tool LLM authorization review for the DeepSeek Harness Auto permission preset", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "repository": { "type": "git", "url": "git+https://github.com/deepseek-ai/deepseek-harness.git", diff --git a/packages/experimental/browser-use-chrome-devtools-mcp/package.json b/packages/experimental/browser-use-chrome-devtools-mcp/package.json index 95b7555b2e..73317d1ead 100644 --- a/packages/experimental/browser-use-chrome-devtools-mcp/package.json +++ b/packages/experimental/browser-use-chrome-devtools-mcp/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-browser-use-chrome-devtools-mcp", "description": "Experimental per-Session Chromium browser tools through chrome-devtools-mcp", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/browser-use-playwright-mcp/package.json b/packages/experimental/browser-use-playwright-mcp/package.json index eea3441ee8..963c8cbdf3 100644 --- a/packages/experimental/browser-use-playwright-mcp/package.json +++ b/packages/experimental/browser-use-playwright-mcp/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-browser-use-playwright-mcp", "description": "Experimental per-Session Chromium browser tools through @playwright/mcp", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/browser-use-runtime/package.json b/packages/experimental/browser-use-runtime/package.json index c4e5ee7196..2628535f4f 100644 --- a/packages/experimental/browser-use-runtime/package.json +++ b/packages/experimental/browser-use-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-browser-use-runtime", "description": "Session-owned browser resource lifecycles and MCP integration for experimental providers", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/browser-use-stagehand-native/package.json b/packages/experimental/browser-use-stagehand-native/package.json index c1af40b350..cd9880acbb 100644 --- a/packages/experimental/browser-use-stagehand-native/package.json +++ b/packages/experimental/browser-use-stagehand-native/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-browser-use-stagehand-native", "description": "Experimental Stagehand browser tools with separately configured native models", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/client-ui-agent-team/README.i18n.yaml b/packages/experimental/client-ui-agent-team/README.i18n.yaml index 5965f254f5..c819bea01e 100644 --- a/packages/experimental/client-ui-agent-team/README.i18n.yaml +++ b/packages/experimental/client-ui-agent-team/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/experimental/client-ui-agent-team/README.md -README.md: 253c243a680a989988ea65e553c14eff9a699155 -README.zh.md: 143c855b8b8a45a7d3777700aefa6a378401f20d +README.md: 364388796442ed71e128ee97b08a39b41982b41b +README.zh.md: 71ad88a6f8f8ecb203af733cd66b5f41ed863b03 diff --git a/packages/experimental/client-ui-agent-team/README.md b/packages/experimental/client-ui-agent-team/README.md index 253c243a68..3643887964 100644 --- a/packages/experimental/client-ui-agent-team/README.md +++ b/packages/experimental/client-ui-agent-team/README.md @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -This package adds an Agent Teams action to the Web conversation header, where a user can inspect the current roster, inspect the shared task board, and navigate into a teammate's conversation. It reads authoritative Team state through the generated `ctx.remote.agentTeams` contribution and keeps ordinary child-history navigation on the stable addressed-subagent path. Choose it through the published experimental Agent Teams bundle. The browser projection does not extend the stable API Proxy, store Team state, or register model-facing input. +This package adds an Agent Teams action to the Web conversation header, where a user can inspect the current roster, inspect the shared task board, and navigate into a teammate's conversation. It reads the Lead Session's `agentTeam` projection from the shared Session store, where Host projection frames keep it current without a refresh control, and keeps ordinary child-history navigation on the stable addressed-subagent path. Choose it through the published experimental Agent Teams bundle. The browser projection does not extend the stable API Proxy, store Team state, or register model-facing input. ## Table of Contents @@ -29,13 +29,15 @@ Enable this package through [`@deepseek-ai/dsh-experimental-agent-team-profile`] ### Inspect and navigate the roster -Opening the panel calls `agentTeams/view`. Roster rows show durable names, turn availability, model, and diagnostics. Provisioning and running members use the shared ongoing loader, inactive members use idle, and failed members use error. Selecting a healthy teammate opens the ordinary `{ parentSessionId, childSessionId, mode: 'continuable' }` address from its Lead and roster identity without refreshing or checking the parent catalog. The Host validates the parent, child, and mode when history opens. History and later human prompts continue through the stable addressed-subagent conversation path; this package adds no Team-specific address field. +The trigger shows the teammate count, and the panel shows the Lead Session's roster and task board from the shared Session store. Task and roster updates appear while the panel stays open. Opening the panel performs no projection requests. The panel shows a loading notice while the conversation or Session list is loading, and an unavailable notice when no Team value is present afterward. + +Roster rows show durable names and phases. Live Session status supplies running activity; the shared `modelSelection` projection supplies a model when available. Selecting an active teammate opens its ordinary continuable child address. The Host validates the parent, child, and mode when history opens; later human prompts use the same addressed-subagent conversation. ### Inspect the task board Ready pending tasks use idle, blocked pending tasks use warning, in-progress tasks use ongoing, and completed tasks use done. -The read-only task board shows task identity, owner, blockers, readiness, advisory write scopes, and overlap warnings. Team agents create and update tasks through their tools; the panel provides no task mutation controls. +The read-only task board shows task identity, owner, blockers, readiness, advisory write scopes, and overlap warnings. Team agents create and update tasks through their tools; the panel provides no task mutation controls. When the projection reports a rejected persisted Team record, the panel shows that failure above the last valid roster and tasks. ----- @@ -45,14 +47,14 @@ The read-only task board shows task identity, owner, blockers, readiness, adviso
Implementation internals — click to expand -The Client export mounts the generated `ctx.remote.agentTeams` contribution from [`@deepseek-ai/dsh-experimental-agent-team/remote`](../agent-team/README.md), then registers its locale dictionaries and one conversation-header slot through Cordis effects. Disposing the plugin fiber removes both registrations. +The Client export registers its locale dictionaries and one conversation-header slot through Cordis effects; it mounts no Remote namespace. Disposing the plugin fiber removes both registrations. -The panel renders outside the conversation container and stays within the viewport. Opening moves focus into the panel; Escape or Close returns focus to its trigger. Clicking outside or moving focus outside the panel and trigger closes it without moving focus back. Opening or refreshing the panel reads the complete Team view. Overlapping refreshes keep the newest response, and responses for a previous conversation are ignored. +The panel renders outside the conversation container and stays within the viewport. Opening moves focus into the panel; Escape or Close returns focus to its trigger. Clicking outside or moving focus outside the panel and trigger closes it without moving focus back. `useSession` supplies the Lead identity; `useSessions` selects its Team and each member's model from the shared store. Each roster row selects its own running state. The only injected callback opens a teammate using the current and child Session ids. Switching conversations closes the panel and clears a navigation failure. | File | Role | |---|---| -| [`src/client/mount.ts`](src/client/mount.ts) | Generated Remote, locale, navigation, and slot registrations | -| [`src/client/TeamAction.tsx`](src/client/TeamAction.tsx) | Roster and task-board interaction state | +| [`src/client/mount.ts`](src/client/mount.ts) | Locale, navigation, and slot registrations | +| [`src/client/TeamAction.tsx`](src/client/TeamAction.tsx) | Projection-derived roster and task board with panel interaction state | | [`src/client/locales.ts`](src/client/locales.ts) | English and Chinese panel copy | | [`src/index.ts`](src/index.ts) | Inert Host entry | @@ -64,7 +66,7 @@ The panel renders outside the conversation container and stays within the viewpo ## Further Exploration - [Agent Teams bundle](../agent-team-profile/README.md) — the published opt-in bundle that mounts this Client plugin. -- [Agent Teams service](../agent-team/README.md) — authoritative roster, task, and Remote behavior. +- [Agent Teams service](../agent-team/README.md) — authoritative roster, task, and projection behavior. - [Conversation UI](../../client/ui-conversation/README.md) — the stable header slot and addressed-subagent navigation surface. - [Experimental packages](../README.md) — incubation status and publication policy. @@ -83,7 +85,9 @@ No direct effect; the Team tools and ordinary conversation submission own any la -- **Snapshot refresh** — the panel refreshes on open and explicit refresh; it has no live event subscription or mailbox timeline. +- **No mailbox timeline** — the projection view carries roster and tasks only; peer messages are not shown. +- **Late plugin activation** — after enabling Agent Teams in an already-open conversation, reload the page to receive its Team projection. +- **Model availability** — a model appears only when the shared store has a durable selection or request for that member. Missing cold-cache values stay absent until normal Session loading or a live update supplies them. - **Ordinary child continuation** — a human message sent after navigation uses the stable addressed-subagent prompt path, not the Team peer mailbox. - **No lifecycle or workspace controls** — the panel cannot spawn, rename, delete, or interrupt teammates, and write scopes remain advisory metadata. @@ -97,4 +101,4 @@ None.
-**Runtime invariant:** No companion is published. RPC is authoritative and the package owns only one disposable slot registration. +**Runtime invariant:** No companion is published. The Host projection is authoritative and the package owns only one disposable slot registration. diff --git a/packages/experimental/client-ui-agent-team/README.zh.md b/packages/experimental/client-ui-agent-team/README.zh.md index 143c855b8b..71ad88a6f8 100644 --- a/packages/experimental/client-ui-agent-team/README.zh.md +++ b/packages/experimental/client-ui-agent-team/README.zh.md @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -本包向 Web 会话页头添加 Agent Teams action,让用户检查当前 roster、查看共享任务板并导航到 teammate 会话。它通过生成的 `ctx.remote.agentTeams` contribution 读取权威 Team 状态,并让普通 child history 导航继续使用稳定的 addressed-subagent 路径。通过公开发布的实验性 Agent Teams bundle 选择本包。这个浏览器 projection 不扩展稳定 API Proxy、不存储 Team 状态,也不注册面向模型的输入。 +本包向 Web 会话页头添加 Agent Teams action,让用户检查当前 roster、查看共享任务板并导航到 teammate 会话。它从共享 Session store 读取 Lead Session 的 `agentTeam` 投影,Host 投影 frame 使其保持最新而无需刷新控件,并让普通 child history 导航继续使用稳定的 addressed-subagent 路径。通过公开发布的实验性 Agent Teams bundle 选择本包。这个浏览器 projection 不扩展稳定 API Proxy、不存储 Team 状态,也不注册面向模型的输入。 ## 目录 @@ -29,13 +29,15 @@ kind: "package-reference" ### 检查并导航 roster -打开 panel 会调用 `agentTeams/view`。Roster row 展示持久 name、轮次可用状态、model 与 diagnostics。provisioning 和 running 成员使用共享 ongoing loading,inactive 成员使用 idle 灰点,failed 成员使用 error 红点。选择健康 teammate 时,系统直接根据其 Lead 与 roster 身份打开普通的 `{ parentSessionId, childSessionId, mode: 'continuable' }` address,不刷新或检查 parent catalog。Host 在打开历史时校验 parent、child 与 mode。History 与后续人类提示词继续使用稳定 addressed-subagent 会话路径;本包不会添加 Team 专用 address 字段。 +触发按钮显示 teammate 数量,面板从共享 Session store 展示 Lead Session 的 roster 与任务板。面板保持打开时,任务和成员更新会直接出现。打开面板不发起投影请求。会话或 Session 列表正在加载时,面板显示加载提示;加载结束后仍无 Team 值时,显示不可用提示。 + +Roster 行展示持久名称与阶段。实时 Session 状态提供运行活动;共享 `modelSelection` 投影在可用时提供模型。选择 active teammate 会打开其普通 continuable 子会话地址。Host 在打开历史时校验 parent、child 与 mode;后续人类提示词使用同一 addressed-subagent 会话。 ### 查看任务板 可开始的 pending 任务使用 idle 灰点,被依赖阻塞的 pending 任务使用 warning 橙点,in-progress 任务使用 ongoing loading,completed 任务使用 done 绿点。 -只读任务板展示任务标识、负责人、依赖、就绪状态、提示性写入范围与重叠警告。Team agent 通过工具创建和更新任务;面板不提供任务修改控件。 +只读任务板展示任务标识、负责人、依赖、就绪状态、提示性写入范围与重叠警告。Team agent 通过工具创建和更新任务;面板不提供任务修改控件。当投影报告某条持久 Team 记录被拒绝时,面板在最后有效的 roster 与任务上方显示该失败。 ----- @@ -45,14 +47,14 @@ kind: "package-reference"
实现细节——点击展开 -Client export 挂载来自 [`@deepseek-ai/dsh-experimental-agent-team/remote`](../agent-team/README.zh.md) 的生成的 `ctx.remote.agentTeams` contribution,然后通过 Cordis effect 注册 locale dictionary 与一个 conversation-header slot。Dispose plugin fiber 会移除这两项 registration。 +Client export 通过 Cordis effect 注册 locale dictionary 与一个 conversation-header slot;它不挂载任何 Remote namespace。Dispose plugin fiber 会移除这两项 registration。 -面板渲染在会话容器外,并保持在视口范围内。打开时焦点移入面板;按 Escape 或选择关闭按钮时,焦点返回触发按钮。点击外部或将焦点移出面板与触发按钮时,面板关闭,但不会将焦点移回。打开或刷新面板会读取完整 Team view。并行刷新只保留最新响应,属于上一个会话的响应会被忽略。 +面板渲染在会话容器外,并保持在视口范围内。打开时焦点移入面板;按 Escape 或选择关闭按钮时,焦点返回触发按钮。点击外部或将焦点移出面板与触发按钮时,面板关闭,但不会将焦点移回。`useSession` 提供 Lead 身份;`useSessions` 从共享 store 选择其 Team 及每个成员的模型。每个 roster 行只选择自己的运行状态。唯一的注入回调通过当前与子 Session 的 id 打开 teammate。切换会话会关闭面板并清除导航失败。 | 文件 | 职责 | |---|---| -| [`src/client/mount.ts`](src/client/mount.ts) | 生成的 Remote、locale、导航与 slot registration | -| [`src/client/TeamAction.tsx`](src/client/TeamAction.tsx) | Roster 与任务板交互状态 | +| [`src/client/mount.ts`](src/client/mount.ts) | locale、导航与 slot registration | +| [`src/client/TeamAction.tsx`](src/client/TeamAction.tsx) | 由投影派生的 roster 与任务板及面板交互状态 | | [`src/client/locales.ts`](src/client/locales.ts) | 中英文 panel 文案 | | [`src/index.ts`](src/index.ts) | 不执行行为的 Host entry | @@ -64,7 +66,7 @@ Client export 挂载来自 [`@deepseek-ai/dsh-experimental-agent-team/remote`](. ## 进一步探索 - [Agent Teams bundle](../agent-team-profile/README.zh.md)——挂载本 Client plugin 的公开 opt-in bundle。 -- [Agent Teams service](../agent-team/README.zh.md)——权威 roster、task 与 Remote 行为。 +- [Agent Teams service](../agent-team/README.zh.md)——权威 roster、task 与投影行为。 - [会话 UI](../../client/ui-conversation/README.zh.md)——稳定 header slot 与 addressed-subagent 导航表层。 - [实验性包](../README.zh.md)——孵化状态与发布规则。 @@ -83,7 +85,9 @@ Client export 挂载来自 [`@deepseek-ai/dsh-experimental-agent-team/remote`](. -- **Snapshot refresh**——panel 会在打开和显式 refresh 时刷新;它没有实时事件订阅或 mailbox timeline。 +- **没有 mailbox timeline**——投影视图只承载 roster 与任务;不显示 peer 消息。 +- **晚启用插件** — 在已经打开的会话中启用 Agent Teams 后,需要刷新页面才能接收其 Team 投影。 +- **模型可用性** — 仅当共享 store 包含该成员的持久选择或请求时才显示模型。冷缓存中缺少的值会保持缺失,直到正常 Session 加载或实时更新提供它们。 - **普通 child continuation**——导航后发送的人类消息使用稳定 addressed-subagent 提示词路径,而不是 Team peer mailbox。 - **没有 lifecycle 或 workspace control**——panel 不能 spawn、rename、delete 或 interrupt teammate,write scope 仍只是提示性 metadata。 @@ -97,4 +101,4 @@ Client export 挂载来自 [`@deepseek-ai/dsh-experimental-agent-team/remote`](.
-**运行时不变式:** 不发布伴生入口。RPC 是权威来源,本包只持有一个可释放的 slot 注册。 +**运行时不变式:** 不发布伴生入口。Host 投影是权威来源,本包只持有一个可释放的 slot 注册。 diff --git a/packages/experimental/client-ui-agent-team/package.json b/packages/experimental/client-ui-agent-team/package.json index 242d368795..ef6657b6d7 100644 --- a/packages/experimental/client-ui-agent-team/package.json +++ b/packages/experimental/client-ui-agent-team/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-client-ui-agent-team", "description": "Web Agent Teams roster, task board, and teammate navigation", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, @@ -29,7 +29,6 @@ "dsh": { "client": { "inject": [ - "@deepseek-ai/dsh-api-remotes", "@deepseek-ai/dsh-api-session-controller", "@deepseek-ai/dsh-client-locale", "@deepseek-ai/dsh-client-ui-conversation", @@ -45,7 +44,6 @@ }, "license": "MIT", "peerDependencies": { - "@deepseek-ai/dsh-api-remotes": "workspace:*", "@deepseek-ai/dsh-api-session-controller": "workspace:*", "@deepseek-ai/dsh-client-locale": "workspace:*", "@deepseek-ai/dsh-client-ui-conversation": "workspace:*", @@ -56,13 +54,12 @@ "@deepseek-ai/dsh-client-ui-workspace": "workspace:*", "@deepseek-ai/dsh-experimental-agent-team": "workspace:*", "@deepseek-ai/dsh-session": "workspace:*", - "@deepseek-ai/dsh-typert-protocol": "workspace:*", "@deepseek-ai/cordis": "workspace:~" }, "devDependencies": { - "@deepseek-ai/dsh-api-remotes": "workspace:*", "@deepseek-ai/dsh-api-session-controller": "workspace:*", "@deepseek-ai/dsh-client-locale": "workspace:*", + "@deepseek-ai/dsh-client-store": "workspace:*", "@deepseek-ai/dsh-client-test-runtime": "workspace:*", "@deepseek-ai/dsh-client-ui-conversation": "workspace:*", "@deepseek-ai/dsh-client-ui-primitives": "workspace:*", @@ -72,7 +69,6 @@ "@deepseek-ai/dsh-client-ui-workspace": "workspace:*", "@deepseek-ai/dsh-experimental-agent-team": "workspace:*", "@deepseek-ai/dsh-session": "workspace:*", - "@deepseek-ai/dsh-typert-protocol": "workspace:*", "@testing-library/react": "^16.1.0", "@types/react": "~18.3.1", "@types/react-dom": "~18.3.0", diff --git a/packages/experimental/client-ui-agent-team/src/client/TeamAction.tsx b/packages/experimental/client-ui-agent-team/src/client/TeamAction.tsx index bce5aa56e0..5a4dd88b65 100644 --- a/packages/experimental/client-ui-agent-team/src/client/TeamAction.tsx +++ b/packages/experimental/client-ui-agent-team/src/client/TeamAction.tsx @@ -1,14 +1,13 @@ -import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react' +import { useEffect, useLayoutEffect, useRef, useState } from 'react' import { createPortal } from 'react-dom' import type { SessionId } from '@deepseek-ai/dsh-session/types' import type { - TeamMemberView as TeamRosterMember, + TeamMemberProjection, TeamTaskView as TeamTask, - TeamView, } from '@deepseek-ai/dsh-experimental-agent-team/client' -import type { RemoteResult } from '@deepseek-ai/dsh-api-remotes/client' +import type {} from '@deepseek-ai/dsh-api-session-controller/client' import { - IconCloseOutlineRegular, IconRefreshOutlineRegular, IconUserOutlineRegular, StateDot, + IconCloseOutlineRegular, IconUserOutlineRegular, StateDot, useAnchoredPosition, useDismissOnOutsidePointer, type StateDotState, } from '@deepseek-ai/dsh-client-ui-primitives' import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' @@ -16,34 +15,30 @@ import type {} from '@deepseek-ai/dsh-client-ui-conversation/client' import { NS, type TeamKey } from './locales.ts' import css from './TeamAction.module.css' -/** Generated Remote result consumed directly by the Team UI. */ -export type TeamActionResult = RemoteResult - /** Business actions injected by the browser plugin. */ export interface TeamActionInjected { - load: (sessionId: SessionId) => Promise> - openTeammate: (sessionId: SessionId, member: TeamRosterMember) => void + /** Open a continuable child from the current conversation. */ + openTeammate: (sessionId: SessionId, childSessionId: SessionId) => void } /** Full props of the Team conversation-header action. */ export type TeamActionProps = PropsRuntime<'conversation.session.header.actions'> & TeamActionInjected & PropsLocale -function failureText(error: { readonly code: string; readonly message: string }): string { - return `${error.message} (${error.code})` -} +/** Durable lifecycle overlaid with the member Session's live turn activity. */ +type MemberStatus = 'running' | 'inactive' | 'provisioning' | 'failed' function statusKey(status: TeamTask['status']): TeamKey { switch (status) { case 'pending': return 'status.pending' case 'in_progress': return 'status.in_progress' case 'completed': return 'status.completed' - /* v8 ignore next -- Team views omit deleted task tombstones. */ + /* v8 ignore next -- the Team projection omits deleted task tombstones. */ case 'deleted': return 'status.completed' } } -function memberStatusKey(status: TeamRosterMember['status']): TeamKey { +function memberStatusKey(status: MemberStatus): TeamKey { switch (status) { case 'running': return 'memberStatus.running' case 'inactive': return 'memberStatus.inactive' @@ -52,7 +47,7 @@ function memberStatusKey(status: TeamRosterMember['status']): TeamKey { } } -function memberDotState(status: TeamRosterMember['status']): StateDotState { +function memberDotState(status: MemberStatus): StateDotState { switch (status) { case 'running': case 'provisioning': return 'ongoing' @@ -66,18 +61,57 @@ function taskDotState(task: TeamTask): StateDotState { case 'pending': return task.ready ? 'idle' : 'warning' case 'in_progress': return 'ongoing' case 'completed': return 'done' - /* v8 ignore next -- Team views omit deleted task tombstones. */ + /* v8 ignore next -- the Team projection omits deleted task tombstones. */ case 'deleted': return 'idle' } } -/** Render the Team roster and read-only task board. */ +type TeamMemberRowProps = Pick & { + member: TeamMemberProjection + onError: (message: string) => void +} + +function TeamMemberRow({ + member, sessionId, useSessions, useSessionStatus, openTeammate, onError, t, +}: TeamMemberRowProps) { + const model = useSessions(state => state.projectionsBySession[member.id]?.values.modelSelection?.next?.model) + const running = useSessionStatus(state => state.get(member.id)?.running) + const summaryRunning = useSessions(state => state.byId[member.id]?.running) + const status: MemberStatus = member.phase === 'active' + ? (running ?? summaryRunning) === true ? 'running' : 'inactive' + : member.phase + + return ( + + ) +} + +/** Render the Team roster and read-only task board from the Lead Session's `agentTeam` projection. */ export function TeamAction({ - sessionId, load, openTeammate, t, + sessionId, useSession, useSessions, useSessionStatus, openTeammate, t, }: TeamActionProps) { const [open, setOpen] = useState(false) - const [loading, setLoading] = useState(false) - const [view, setView] = useState(null) const [error, setError] = useState(null) const rootRef = useRef(null) const triggerRef = useRef(null) @@ -87,15 +121,14 @@ export function TeamAction({ }) const positioned = position !== null useDismissOnOutsidePointer(rootRef, open, setOpen, panelRef) - const sessionRef = useRef(sessionId) - const refreshGeneration = useRef(0) - sessionRef.current = sessionId + + const leadSessionId = useSession(snapshot => snapshot.subagent?.address.parentSessionId) ?? sessionId + const team = useSessions(state => state.projectionsBySession[leadSessionId]?.values.agentTeam) + const opening = useSession(snapshot => snapshot.openState === 'loading') + const listing = useSessions(state => state.phase === 'pending') useEffect(() => { - refreshGeneration.current += 1 setOpen(false) - setLoading(false) - setView(null) setError(null) }, [sessionId]) @@ -108,22 +141,7 @@ export function TeamAction({ triggerRef.current?.focus() } - const refresh = useCallback(async (): Promise => { - const requestedSession = sessionId - const generation = ++refreshGeneration.current - setLoading(true) - const result = await load(requestedSession) - if (sessionRef.current !== requestedSession || refreshGeneration.current !== generation) return - setLoading(false) - if (result.ok) { - setView(result.value) - setError(null) - } else { - setError(failureText(result.error)) - } - }, [load, sessionId]) - - const teammates = view?.members.filter(member => member.role === 'teammate') ?? [] + const teammates = team?.members.filter(member => member.role === 'teammate') ?? [] return (
{ @@ -142,11 +160,7 @@ export function TeamAction({ className={css.trigger} aria-haspopup="dialog" aria-expanded={open} - onClick={() => { - const next = !open - setOpen(next) - if (next) void refresh() - }} + onClick={() => { setOpen(!open) }} > {t('trigger')} @@ -165,12 +179,6 @@ export function TeamAction({
{t('trigger')} - {loading && view !== null && ( - - )} - @@ -178,44 +186,39 @@ export function TeamAction({ {error !== null && (
{error}
)} - {loading && view === null && ( -
{t('loading')}
+ {team === undefined && ( +
+ + {t(opening || listing ? 'loading' : 'unavailable')} +
)} - {view !== null && ( + {team !== undefined && ( <> + {team.failure !== undefined && ( +
{t('failure', { message: team.failure })}
+ )}

{t('roster')}

- {view.members.map(member => ( - + member={member} + sessionId={sessionId} + useSessions={useSessions} + useSessionStatus={useSessionStatus} + openTeammate={openTeammate} + onError={setError} + t={t} + /> ))}

{t('tasks')}

- {view.tasks.length === 0 &&
{t('empty')}
} + {team.tasks.length === 0 &&
{t('empty')}
}
- {view.tasks.map(task => ( + {team.tasks.map(task => (
{task.subject} diff --git a/packages/experimental/client-ui-agent-team/src/client/index.ts b/packages/experimental/client-ui-agent-team/src/client/index.ts index 5e2093105e..a4896c82af 100644 --- a/packages/experimental/client-ui-agent-team/src/client/index.ts +++ b/packages/experimental/client-ui-agent-team/src/client/index.ts @@ -1,14 +1,16 @@ -/** Browser entry binding the generated Team Remote artifact to its Client UI. */ +/** Browser entry registering the Agent Teams conversation-header action. */ -import agentTeamsRemote from '@deepseek-ai/dsh-experimental-agent-team/remote' import type { Context as ClientContext } from '@deepseek-ai/cordis' -import { mountAgentTeamUi } from './mount.ts' +import { registerAgentTeamUi } from './mount.ts' export { inject } from './mount.ts' -export type { TeamActionInjected, TeamActionProps, TeamActionResult } from './TeamAction.tsx' +export type { TeamActionInjected, TeamActionProps } from './TeamAction.tsx' export type { TeamKey } from './locales.ts' -/** Mount the generated Team Remote contribution and its browser UI. */ -export async function apply(ctx: ClientContext): Promise<() => Promise> { - return await mountAgentTeamUi(ctx, agentTeamsRemote) +/** + * Register the Team locale dictionaries and header action on the Client Context. + * @param ctx - Client Context with the declared `inject` services available. + */ +export function apply(ctx: ClientContext): void { + registerAgentTeamUi(ctx) } diff --git a/packages/experimental/client-ui-agent-team/src/client/locales.ts b/packages/experimental/client-ui-agent-team/src/client/locales.ts index 9cef05d89c..14d1eb0ed7 100644 --- a/packages/experimental/client-ui-agent-team/src/client/locales.ts +++ b/packages/experimental/client-ui-agent-team/src/client/locales.ts @@ -6,9 +6,10 @@ export const NS = 'agent-team' /** Simplified Chinese dictionary and key source. */ export const zh = { trigger: 'Agent Team', - refresh: '刷新 Team', close: '关闭', loading: '正在加载 Team…', + unavailable: 'Team 暂不可用', + failure: 'Team 持久记录无效:{message}', empty: '还没有共享任务', roster: '成员', tasks: '共享任务', @@ -35,9 +36,10 @@ export type TeamKey = keyof typeof zh /** English dictionary checked against the Chinese key set. */ export const en = { trigger: 'Agent Team', - refresh: 'Refresh Team', close: 'Close', loading: 'Loading Team…', + unavailable: 'Team is unavailable', + failure: 'Invalid persisted Team record: {message}', empty: 'No shared tasks yet', roster: 'Members', tasks: 'Shared tasks', diff --git a/packages/experimental/client-ui-agent-team/src/client/mount.ts b/packages/experimental/client-ui-agent-team/src/client/mount.ts index ad9648e322..4ca1b5e04c 100644 --- a/packages/experimental/client-ui-agent-team/src/client/mount.ts +++ b/packages/experimental/client-ui-agent-team/src/client/mount.ts @@ -1,22 +1,13 @@ -/** Source-safe Agent Teams browser registration and Remote mount lifecycle. */ +/** Source-safe Agent Teams browser registration. */ -import type { - TeamMemberView as TeamRosterMember, - TeamView, -} from '@deepseek-ai/dsh-experimental-agent-team/client' -import type {} from '@deepseek-ai/dsh-experimental-agent-team/remote' import type { Context as ClientContext } from '@deepseek-ai/cordis' -import type {} from '@deepseek-ai/dsh-api-remotes/client' import type {} from '@deepseek-ai/dsh-api-session-controller/client' import type { SessionId } from '@deepseek-ai/dsh-session/types' import type {} from '@deepseek-ai/dsh-client-ui-conversation/client' import type {} from '@deepseek-ai/dsh-client-locale/client' import type {} from '@deepseek-ai/dsh-client-ui-renderer/client' import type {} from '@deepseek-ai/dsh-client-ui-workspace/client' -import type { TypertRemoteContribution } from '@deepseek-ai/dsh-typert-protocol' -import { - TeamAction, type TeamActionInjected, type TeamActionResult, -} from './TeamAction.tsx' +import { TeamAction, type TeamActionInjected } from './TeamAction.tsx' import { en, NS, zh, type TeamKey } from './locales.ts' declare module '@deepseek-ai/dsh-client-ui-slots' { @@ -26,10 +17,16 @@ declare module '@deepseek-ai/dsh-client-ui-slots' { } } -/** Required browser services for RPC, navigation, slots, and localized copy. */ -export const inject = ['sessions', 'uiWorkspace', 'remote', 'slots', 'locale'] +/** Required browser services for navigation, slots, and localized copy. */ +export const inject = ['sessions', 'uiWorkspace', 'slots', 'locale'] -function registerUi(ctx: ClientContext): void { +/** + * Register the Team locale dictionaries and the conversation-header action. + * The panel reads the Lead Session's `agentTeam` projection from the shared + * Session store; this registration performs no Team RPC. + * @param ctx - Client Context carrying the injected navigation, locale, slot, and Session services. + */ +export function registerAgentTeamUi(ctx: ClientContext): void { ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'client-ui-agent-team: dictionaries') const sessions = ctx.sessions const leadSessionId = (sessionId: SessionId): SessionId => { @@ -38,16 +35,12 @@ function registerUi(ctx: ClientContext): void { } const actions: TeamActionInjected = { - async load(sessionId): Promise> { - return await ctx.remote.agentTeams.view(leadSessionId(sessionId)) - }, - openTeammate(sessionId: SessionId, member: TeamRosterMember): void { - if (member.role !== 'teammate') return + openTeammate(sessionId: SessionId, childSessionId: SessionId): void { const parentSessionId = leadSessionId(sessionId) if ((sessions.retainInfo(sessionId).getSnapshot().retainedBy.mainView ?? 0) === 0) return ctx.uiWorkspace.openSession({ parentSessionId, - childSessionId: member.id, + childSessionId, mode: 'continuable', }) }, @@ -64,28 +57,3 @@ function registerUi(ctx: ClientContext): void { }, TeamAction), ) } - -/** - * Mount one generated Team Remote contribution, then register its browser UI. - * @param ctx - Client Context carrying navigation, locale, slot, and Remote services. - * @param contribution - generated Team descriptors selected by the browser entry. - * @returns disposer for both the UI registrations and Remote namespace. - */ -export async function mountAgentTeamUi( - ctx: ClientContext, - contribution: TypertRemoteContribution, -): Promise<() => Promise> { - const disposeRemote = await ctx.remote.$mount(contribution) - const ui = ctx.inject(['sessions', 'uiWorkspace', 'remote.agentTeams', 'slots', 'locale'], registerUi) - try { - await ui - } catch (error) { - await ui.dispose() - await disposeRemote() - throw error - } - return async () => { - await ui.dispose() - await disposeRemote() - } -} diff --git a/packages/experimental/client-ui-agent-team/tests/browser-plugin.client.spec.ts b/packages/experimental/client-ui-agent-team/tests/browser-plugin.client.spec.ts index a153ec2870..abc786646f 100644 --- a/packages/experimental/client-ui-agent-team/tests/browser-plugin.client.spec.ts +++ b/packages/experimental/client-ui-agent-team/tests/browser-plugin.client.spec.ts @@ -1,78 +1,20 @@ -import { Context, Service } from '@deepseek-ai/cordis' -import { describe, expect, it, vi } from 'vitest' +import { Context } from '@deepseek-ai/cordis' +import { describe, expect, it } from 'vitest' import type { SessionId } from '@deepseek-ai/dsh-session/types' import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client' import { SlotRegistry } from '@deepseek-ai/dsh-client-ui-renderer/client' -import type { TeamMemberView as TeamRosterMember } from '@deepseek-ai/dsh-experimental-agent-team/client' -import type {} from '@deepseek-ai/dsh-experimental-agent-team/remote' -import { RemoteError } from '@deepseek-ai/dsh-client-test-runtime' -import type { TypertRemoteContribution } from '@deepseek-ai/dsh-typert-protocol' import { TeamAction, type TeamActionInjected } from '../src/client/TeamAction.tsx' -import { inject, mountAgentTeamUi } from '../src/client/mount.ts' +import { apply, inject } from '../src/client/index.ts' import { apply as nodeApply } from '../src/index.ts' const SESSION = 'team-session' as SessionId const CHILD = 'team-child' as SessionId -const REMOTE: TypertRemoteContribution = { - package: '@deepseek-ai/dsh-experimental-agent-team', - descriptors: [], -} -async function bench(options: { - addressed?: boolean - registrationFailure?: boolean - remoteFailure?: boolean - catalog?: 'missing' | 'empty' -} = {}) { +async function bench(options: { addressed?: boolean } = {}) { const ctx = new Context() - const calls: { method: string; args: unknown[] }[] = [] - const task = { - id: 'task-1', - revision: 1, subject: 'Task', description: 'Description', status: 'pending' as const, - blockedBy: [], writeScopes: [], ready: true, writeScopeWarnings: [], - } - class RemoteService extends Service { - readonly disposeMount = vi.fn(() => Promise.resolve()) - readonly mount = vi.fn((_contribution: unknown) => Promise.resolve(this.disposeMount)) - - constructor(serviceCtx: Context) { - super(serviceCtx, 'remote') - } - - $mount(contribution: unknown): Promise<() => Promise> { - return this.mount(contribution) - } - } - const remote = new RemoteService(ctx) - const failure = { - ok: false as const, - error: new RemoteError('gateway/internal', 'offline', {}), - } - const view = { - members: [{ - id: SESSION, name: 'lead', role: 'lead' as const, status: 'inactive' as const, diagnostics: [], - }], tasks: [task], - } - ctx.provide('remote.agentTeams', { - view: (...args: unknown[]) => { - calls.push({ method: 'agentTeams/view', args }) - return Promise.resolve(options.remoteFailure - ? failure - : { ok: true as const, value: view }) - }, - }) const navigation: unknown[] = [] let mainSessionId = options.addressed === true ? CHILD : SESSION - const projectionsBySession = options.catalog === 'missing' - ? {} - : { - [SESSION]: { state: 'ready' as const, error: null, values: { subagentCatalog: options.catalog === 'empty' ? [] : [{ createdAt: 1, id: CHILD, - mode: 'continuable' as const, - label: 'worker' as const, - }] } }, - } ctx.provide('sessions', { - list: { getSnapshot: () => ({ projectionsBySession }) }, binding: (id: SessionId) => options.addressed === true && id === CHILD ? { session: { getSnapshot: () => ({ subagent: { @@ -106,153 +48,72 @@ async function bench(options: { name: 'root', children: { 'conversation.session.header.actions': { kind: 'list', scope: 'session' } }, } as never, () => null) - if (options.registrationFailure === true) { - vi.spyOn(ctx.slots, 'inject').mockImplementationOnce(() => { throw new Error('slot registration failed') }) - } - const fiber = options.registrationFailure === true - ? ctx.plugin({ apply() {} }) - : ctx.plugin({ inject: [...inject], apply: clientCtx => mountAgentTeamUi(clientCtx, REMOTE) }) - const activation: Promise = options.registrationFailure === true - ? mountAgentTeamUi(ctx, REMOTE).catch((error: unknown) => error) - : fiber.await() - if (options.registrationFailure !== true) { - await activation - } else { - await fiber.await() - } + const fiber = ctx.plugin({ inject: [...inject], apply }) + await fiber.await() const entry = () => ctx.slots.entries('conversation.session.header.actions') .find(candidate => candidate.component === TeamAction) + const actions = (): TeamActionInjected => { + const injected = entry()!.inject!() + const { openTeammate } = injected + if (typeof openTeammate !== 'function') { + throw new Error('Team header action lacks its injected callbacks') + } + return { + openTeammate: openTeammate as TeamActionInjected['openTeammate'], + } + } return { ctx, fiber, - activation, - calls, navigation, - remote, entry, + actions, collapseHeader, select: (sessionId: SessionId) => { mainSessionId = sessionId }, } } describe('ui-team browser plugin', () => { - it('registers one disposable header action with a read-only RPC-backed task board', async () => { + it('registers one disposable header action without a Team Remote namespace', async () => { const b = await bench() - expect(inject).toEqual(['sessions', 'uiWorkspace', 'remote', 'slots', 'locale']) + expect(inject).toEqual(['sessions', 'uiWorkspace', 'slots', 'locale']) expect(b.entry()).toMatchObject({ options: { id: 'agent-team', order: 20 }, locale: 'agent-team', }) - expect(b.remote.mount).toHaveBeenCalledOnce() - expect(b.remote.mount).toHaveBeenCalledWith(REMOTE) - const actions = (b.entry()!.inject as unknown as () => TeamActionInjected)() - expect((await actions.load(SESSION)).ok).toBe(true) - expect(b.calls).toEqual([{ method: 'agentTeams/view', args: [SESSION] }]) + const t = b.ctx.locale.bind('agent-team') + expect(t('trigger')).toBe('Agent Team') - actions.openTeammate(SESSION, { - id: SESSION, - name: 'lead', - role: 'lead', - status: 'inactive', - diagnostics: [], - }) expect(b.navigation).toEqual([]) await b.fiber.dispose() expect(b.entry()).toBeUndefined() - expect(b.remote.disposeMount).toHaveBeenCalledOnce() + expect(t('empty')).toBe('empty') }) - it('unmounts the Remote contribution when later Client registration fails', async () => { - const b = await bench({ registrationFailure: true }) - await expect(b.activation).resolves.toMatchObject({ message: 'slot registration failed' }) - expect(b.remote.mount).toHaveBeenCalledOnce() - expect(b.remote.disposeMount).toHaveBeenCalledOnce() - }) - - it('returns Remote carrier failures unchanged', async () => { - const view = await bench({ remoteFailure: true }) - const viewActions = (view.entry()!.inject as unknown as () => TeamActionInjected)() - await expect(viewActions.load(SESSION)).resolves.toMatchObject({ - ok: false, - error: { code: 'gateway/internal', message: 'offline' }, - }) - - }) - - it('opens a continuable teammate address without refreshing the parent catalog', async () => { + it('opens a continuable teammate address without touching the parent catalog', async () => { const b = await bench() - const actions = (b.entry()!.inject as unknown as () => TeamActionInjected)() - const member: TeamRosterMember = { - id: CHILD, - name: 'worker', - role: 'teammate', - status: 'inactive', - diagnostics: [], - } - actions.openTeammate(SESSION, member) + b.actions().openTeammate(SESSION, CHILD) expect(b.navigation).toEqual([ - ['open', { - parentSessionId: SESSION, - childSessionId: CHILD, - mode: 'continuable', - }], + ['open', { parentSessionId: SESSION, childSessionId: CHILD, mode: 'continuable' }], ]) }) - it('routes Team actions from an addressed teammate conversation back through its Lead', async () => { + it('routes teammate navigation from an addressed teammate conversation back through its Lead', async () => { const b = await bench({ addressed: true }) - const actions = (b.entry()!.inject as unknown as () => TeamActionInjected)() - await actions.load(CHILD) - actions.openTeammate(CHILD, { - id: CHILD, - name: 'worker', - role: 'teammate', - status: 'inactive', - diagnostics: [], - }) - expect(b.calls[0]).toEqual({ method: 'agentTeams/view', args: [SESSION] }) + b.actions().openTeammate(CHILD, CHILD) expect(b.navigation).toEqual([ - ['open', { - parentSessionId: SESSION, - childSessionId: CHILD, - mode: 'continuable', - }], + ['open', { parentSessionId: SESSION, childSessionId: CHILD, mode: 'continuable' }], ]) }) it('does not open a teammate from a conversation outside the main view', async () => { const b = await bench() - const actions = (b.entry()!.inject as unknown as () => TeamActionInjected)() b.select('other-session' as SessionId) - actions.openTeammate(SESSION, { - id: CHILD, - name: 'worker', - role: 'teammate', - status: 'inactive', - diagnostics: [], - }) + b.actions().openTeammate(SESSION, CHILD) expect(b.navigation).toEqual([]) }) - it('opens a teammate when the parent catalog is missing or empty', async () => { - for (const catalog of ['missing', 'empty'] as const) { - const b = await bench({ catalog }) - const actions = (b.entry()!.inject as unknown as () => TeamActionInjected)() - actions.openTeammate(SESSION, { - id: CHILD, - name: 'worker', - role: 'teammate', - status: 'inactive', - diagnostics: [], - }) - expect(b.navigation).toEqual([ - ['open', { parentSessionId: SESSION, childSessionId: CHILD, mode: 'continuable' }], - ]) - await b.fiber.dispose() - } - }) - it('re-registers after the conversation header slot is collapsed and declared again', async () => { const b = await bench() expect(b.entry()).toBeDefined() diff --git a/packages/experimental/client-ui-agent-team/tests/team-action.client.spec.tsx b/packages/experimental/client-ui-agent-team/tests/team-action.client.spec.tsx index 82a90c7e02..3aca5c5cc8 100644 --- a/packages/experimental/client-ui-agent-team/tests/team-action.client.spec.tsx +++ b/packages/experimental/client-ui-agent-team/tests/team-action.client.spec.tsx @@ -1,22 +1,26 @@ // @vitest-environment jsdom +import { Profiler } from 'react' import { afterEach, describe, expect, it, vi } from 'vitest' -import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' +import { act, cleanup, fireEvent, render, screen } from '@testing-library/react' import type { SessionId } from '@deepseek-ai/dsh-session/types' import type { - TeamTaskId, TeamTaskView as TeamTask, TeamView, + TeamMemberProjection, TeamProjection, TeamTaskId, TeamTaskView as TeamTask, } from '@deepseek-ai/dsh-experimental-agent-team/client' -import { makeTranslate, RemoteError } from '@deepseek-ai/dsh-client-test-runtime' +import type { SessionListState, SessionSnapshot, SessionSummary, UseProjection } from '@deepseek-ai/dsh-api-session-controller/client' +import type { SessionStatusSnapshot } from '@deepseek-ai/dsh-client-ui-session/client' +import { createSnapshotStore } from '@deepseek-ai/dsh-client-store' +import { bindSnapshotSelector, makeTranslate } from '@deepseek-ai/dsh-client-test-runtime' import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts' -import { - TeamAction, type TeamActionInjected, type TeamActionProps, type TeamActionResult, -} from '../src/client/TeamAction.tsx' +import { TeamAction, type TeamActionInjected, type TeamActionProps } from '../src/client/TeamAction.tsx' import { zh } from '../src/client/locales.ts' afterEach(cleanup) const SESSION = 'lead' as SessionId +const WORKER = 'worker-id' as SessionId const TASK_1 = 'task-1' as TeamTaskId +const TASK_2 = 'task-2' as TeamTaskId const task: TeamTask = { id: TASK_1, revision: 1, @@ -29,214 +33,306 @@ const task: TeamTask = { ready: false, writeScopeWarnings: ['write scopes overlap with task-2'], } -const view: TeamView = { - members: [ - { id: SESSION, name: 'lead', role: 'lead', status: 'inactive', model: 'model-a', diagnostics: [] }, - { - id: 'worker-id' as SessionId, - name: 'worker', - role: 'teammate', - status: 'inactive', - model: 'model-a', - diagnostics: [], - }, - ], - tasks: [task], +const lead: TeamMemberProjection = { id: SESSION, name: 'lead', role: 'lead', phase: 'active' } +const worker: TeamMemberProjection = { + id: WORKER, name: 'worker', role: 'teammate', phase: 'active', +} +const team: TeamProjection = { members: [lead, worker], tasks: [task] } + +type Projections = SessionListState['projectionsBySession'] + +function summary(id: SessionId, running: boolean): SessionSummary { + return { id, displayTitle: id, running, retainedBy: {}, blank: false, updatedAt: 0 } } -function remoteFailure(message: string): TeamActionResult { - return { ok: false, error: new RemoteError('gateway/internal', message, {}) } -} - -function props(actions: TeamActionInjected, sessionId: SessionId = SESSION): TeamActionProps { - return { +function bench(options: { + projections?: Projections + sessionId?: SessionId + parentSessionId?: SessionId + openState?: SessionSnapshot['openState'] + statuses?: SessionStatusSnapshot + running?: Record +} = {}) { + const sessionId = options.sessionId ?? SESSION + const byId: Record = {} + for (const [id, running] of Object.entries(options.running ?? {}) as [SessionId, boolean][]) byId[id] = summary(id, running) + const sessions = createSnapshotStore({ + ids: Object.keys(byId) as SessionId[], byId, phase: 'ready', + projectionsBySession: options.projections ?? { [SESSION]: { state: 'ready', error: null, values: { agentTeam: team } } }, + }) + const statuses = createSnapshotStore(options.statuses ?? new Map()) + const session = createSnapshotStore({ sessionId, - ...actions, + pendingSubmissions: [], + running: false, + subagent: options.parentSessionId === undefined + ? null + : { address: { parentSessionId: options.parentSessionId, childSessionId: sessionId, mode: 'continuable' } }, + removed: false, + openState: options.openState ?? 'open', + openError: null, + hasMore: false, + loadingOlder: false, + promptError: null, + blank: false, + lastAgentError: null, + promptAttempted: false, + awaitingFirstTurn: false, + }) + const useSessions = bindSnapshotSelector(sessions) + const injected: TeamActionInjected = { openTeammate: vi.fn() } + const props: TeamActionProps = { + sessionId, + useSession: bindSnapshotSelector(session), + useProjection: ((key: string, select?: (value: unknown) => unknown) => { + const value = useSessions(state => state.projectionsBySession[sessionId]?.values[ + key as keyof SessionListState['projectionsBySession'][SessionId]['values'] + ]) + return select === undefined ? value : select(value) + }) as UseProjection, + useSessions, + useSessionStatus: bindSnapshotSelector(statuses), + ...injected, t: makeTranslate(zh, commonZh), - } as unknown as TeamActionProps + } as TeamActionProps + return { props, injected, sessions, statuses, session } } -function actions(overrides: Partial = {}): TeamActionInjected { - return { - load: () => Promise.resolve({ ok: true, value: view }), - openTeammate: () => {}, - ...overrides, - } +function openPanel(): void { + fireEvent.click(screen.getByRole('button', { name: /Agent Team/u })) +} + +function setProjectionSnapshot( + sessions: ReturnType['sessions'], + sessionId: SessionId, + snapshot: Projections[SessionId], +): void { + act(() => { + const current = sessions.getSnapshot() + sessions.set({ ...current, projectionsBySession: { ...current.projectionsBySession, [sessionId]: snapshot } }) + }) +} + +function setProjection(sessions: ReturnType['sessions'], sessionId: SessionId, value: TeamProjection): void { + setProjectionSnapshot(sessions, sessionId, { state: 'ready', error: null, values: { agentTeam: value } }) } describe('TeamAction', () => { - it('ignores a stale Team load after the conversation switches sessions', async () => { - const nextSession = 'next-lead' as SessionId - const firstLoad = Promise.withResolvers<{ ok: true; value: TeamView }>() - const nextView: TeamView = { - ...view, - members: [{ id: nextSession, name: 'lead', role: 'lead', status: 'inactive', diagnostics: [] }], - tasks: [{ ...task, id: 'task-next' as TeamTaskId, subject: 'Next session task' }], - } - const load = vi.fn((sessionId: SessionId) => sessionId === SESSION - ? firstLoad.promise - : Promise.resolve({ ok: true as const, value: nextView })) - const injected = actions({ load }) - const rendered = render() - fireEvent.click(screen.getByRole('button', { name: /Agent Team/u })) - await waitFor(() => { expect(load).toHaveBeenCalledWith(SESSION) }) - - rendered.rerender() - fireEvent.click(screen.getByRole('button', { name: /Agent Team/u })) - expect(await screen.findByText('Next session task')).toBeTruthy() - firstLoad.resolve({ ok: true, value: view }) - await Promise.resolve() - - await waitFor(() => { - expect(screen.getByText('Next session task')).toBeTruthy() - expect(screen.queryByText('Implement runtime')).toBeNull() - }) - }) - - it('loads roster/task diagnostics on open and navigates a healthy teammate', async () => { - const openTeammate = vi.fn() - render() - fireEvent.click(screen.getByRole('button', { name: /Agent Team/u })) - const worker = await screen.findByRole('button', { name: /worker/u }) + it('renders the Lead projection and applies later projection frames without any user action', async () => { + const b = bench() + render() + expect(screen.getByRole('button', { name: /Agent Team/u }).textContent).toContain('1') + openPanel() + expect(await screen.findByText('Implement runtime')).toBeTruthy() expect(screen.getByText('write scopes overlap with task-2')).toBeTruthy() - fireEvent.click(worker) - await waitFor(() => { expect(openTeammate).toHaveBeenCalledWith(SESSION, view.members[1]) }) + expect(screen.queryByRole('button', { name: /刷新|Refresh/u })).toBeNull() + + setProjection(b.sessions, SESSION, { + members: [lead, worker, { id: 'worker-b' as SessionId, name: 'worker-b', role: 'teammate', phase: 'provisioning' }], + tasks: [task, { ...task, id: TASK_2, subject: 'Pushed task', status: 'pending', ready: true, writeScopeWarnings: [] }], + }) + expect(screen.getByText('Pushed task')).toBeTruthy() + expect(screen.getByRole('button', { name: /worker-b/u })).toHaveProperty('disabled', true) + expect(screen.getByRole('button', { name: /Agent Team/u }).textContent).toContain('2') }) - it('keeps only the newest overlapping refresh for one session', async () => { - const older = Promise.withResolvers>() - const newer = Promise.withResolvers>() - const newestView = { - ...view, - tasks: [{ ...task, id: 'newest-task' as TeamTaskId, subject: 'Newest task' }], - } - const load = vi.fn() - .mockResolvedValueOnce({ ok: true, value: view }) - .mockImplementationOnce(() => older.promise) - .mockImplementationOnce(() => newer.promise) - render() - fireEvent.click(screen.getByRole('button', { name: /Agent Team/u })) - await screen.findByText('Implement runtime') + it('overlays live Session status and the durable model selection on roster rows', () => { + const statuses: SessionStatusSnapshot = new Map([[WORKER, { running: true, pendingInteraction: undefined, completionUnread: false }]]) + const b = bench({ + statuses, + running: { [SESSION]: true }, + projections: { + [SESSION]: { + state: 'ready', error: null, + values: { agentTeam: team, modelSelection: { lastUsed: null, next: { provider: 'p', model: 'lead-model' } } }, + }, + [WORKER]: { + state: 'ready', error: null, + values: { modelSelection: { lastUsed: { provider: 'p', model: 'worker-model' }, next: { provider: 'p', model: 'worker-model' } } }, + }, + }, + }) + render() + openPanel() + expect(screen.getByRole('button', { name: `lead${zh['memberStatus.running']} · ${zh.model}: lead-model` })).toBeTruthy() + const row = screen.getByRole('button', { name: `worker${zh['memberStatus.running']} · ${zh.model}: worker-model` }) + expect(row.querySelector('[data-state="ongoing"]')).not.toBeNull() - const refresh = screen.getByRole('button', { name: zh.refresh }) - fireEvent.click(refresh) - expect(screen.getByRole('status', { name: zh.loading })).toBeTruthy() - fireEvent.click(refresh) - newer.resolve({ ok: true, value: newestView }) - expect(await screen.findByText('Newest task')).toBeTruthy() - older.resolve({ ok: true, value: view }) - await Promise.resolve() + act(() => { b.statuses.set(new Map([[WORKER, { running: false, pendingInteraction: undefined, completionUnread: false }]])) }) + expect(screen.getByRole('button', { name: /^worker未运行/u })).toBeTruthy() - expect(screen.getByText('Newest task')).toBeTruthy() - expect(screen.queryByText('Implement runtime')).toBeNull() + act(() => { + b.statuses.set(new Map()) + b.sessions.update((draft) => { draft.byId[WORKER] = summary(WORKER, true) }) + }) + expect(screen.getByRole('button', { name: /^worker运行中/u })).toBeTruthy() }) - it('renders roster/task state variants and contains navigation, refresh, and close actions', async () => { + it('reads the Lead projection from an addressed teammate conversation', () => { + const b = bench({ sessionId: WORKER, parentSessionId: SESSION }) + render() + openPanel() + expect(screen.getByText('Implement runtime')).toBeTruthy() + fireEvent.click(screen.getByRole('button', { name: /^worker/u })) + expect(b.injected.openTeammate).toHaveBeenCalledWith(WORKER, WORKER) + }) + + it.each([false, true])('accepts shared baselines and late capability updates (teammate page: %s)', (addressed) => { + const b = bench({ + ...(addressed ? { sessionId: WORKER, parentSessionId: SESSION } : {}), + projections: {}, openState: 'loading', + }) + render() + openPanel() + expect(screen.getByRole('status').textContent).toBe(zh.loading) + act(() => { b.session.set({ ...b.session.getSnapshot(), openState: 'open' }) }) + expect(screen.getByRole('status').textContent).toBe(zh.unavailable) + setProjectionSnapshot(b.sessions, SESSION, { state: 'idle', error: null, values: { agentTeam: team } }) + expect(screen.getByText('Implement runtime')).toBeTruthy() + expect(screen.queryByRole('status')).toBeNull() + setProjectionSnapshot(b.sessions, SESSION, { state: 'idle', error: null, values: {} }) + expect(screen.getByRole('status').textContent).toBe(zh.unavailable) + setProjection(b.sessions, SESSION, { members: [lead], tasks: [] }) + expect(screen.getByText(zh.empty)).toBeTruthy() + }) + + it('waits for the shared Session list and accepts cached projections without an explicit read', () => { + const b = bench({ projections: {} }) + b.sessions.update((draft) => { draft.phase = 'pending' }) + render() + openPanel() + expect(screen.getByRole('status').textContent).toBe(zh.loading) + act(() => { b.sessions.set({ + ...b.sessions.getSnapshot(), phase: 'ready', + projectionsBySession: { [SESSION]: { state: 'idle', error: null, values: { agentTeam: team } } }, + }) }) + expect(screen.getByText('Implement runtime')).toBeTruthy() + expect(screen.queryByRole('status')).toBeNull() + }) + + it.each([false, true])('ignores unrelated Session updates (teammate page: %s)', (addressed) => { + const b = bench(addressed ? { sessionId: WORKER, parentSessionId: SESSION } : {}) + const onRender = vi.fn() + render() + openPanel() + onRender.mockClear() + + act(() => { + const current = b.sessions.getSnapshot() + const projectionsBySession = Object.fromEntries(Object.entries(current.projectionsBySession) + .map(([id, snapshot]) => [id, { ...snapshot }])) + b.sessions.set({ + ...current, + byId: { ...current.byId, ['unrelated' as SessionId]: summary('unrelated' as SessionId, true) }, + projectionsBySession: { + ...projectionsBySession, + ['unrelated' as SessionId]: { state: 'ready', error: null, values: { agentTeam: { members: [], tasks: [] } } }, + }, + }) + b.statuses.set(new Map([['unrelated' as SessionId, { running: true, pendingInteraction: undefined, completionUnread: false }]])) + }) + expect(onRender).not.toHaveBeenCalled() + + setProjectionSnapshot(b.sessions, WORKER, { + state: 'ready', error: null, + values: { modelSelection: { lastUsed: null, next: { provider: 'p', model: 'updated-worker-model' } } }, + }) + expect(screen.getByRole('button', { name: /worker.*updated-worker-model/u })).toBeTruthy() + setProjection(b.sessions, SESSION, { ...team, tasks: [{ ...task, subject: 'Updated parent task' }] }) + expect(screen.getByText('Updated parent task')).toBeTruthy() + }) + + it('shows capability absence after a successful read instead of loading forever', () => { + const b = bench({ projections: { [SESSION]: { state: 'ready', error: null, values: {} } } }) + render() + openPanel() + expect(screen.queryByText(zh.loading)).toBeNull() + expect(screen.getByRole('status').textContent).toBe('Team 暂不可用') + }) + + it('surfaces a Team projection failure beside the last valid state', () => { + const b = bench({ + projections: { [SESSION]: { state: 'ready', error: null, values: { agentTeam: { ...team, failure: 'revision is not contiguous' } } } }, + }) + render() + openPanel() + expect(screen.getByRole('alert').textContent).toBe('Team 持久记录无效:revision is not contiguous') + expect(screen.getByText('Implement runtime')).toBeTruthy() + }) + + it('renders roster/task state variants and reports navigation failures', () => { const { ownerName: _ownerName, ...unownedTask } = task - const richView: TeamView = { - ...view, - members: [ - view.members[0]!, - { ...view.members[1]!, status: 'running' }, - { - id: 'failed-id' as SessionId, - name: 'failed-worker', - role: 'teammate', - status: 'failed', - diagnostics: ['provider failed'], + const b = bench({ + projections: { + [SESSION]: { + state: 'ready', error: null, + values: { + agentTeam: { + members: [ + lead, + worker, + { id: 'failed-id' as SessionId, name: 'failed-worker', role: 'teammate', phase: 'failed', error: 'provider failed' }, + { id: 'provisioning-id' as SessionId, name: 'provisioning-worker', role: 'teammate', phase: 'provisioning' }, + ], + tasks: [ + { ...unownedTask, id: 'ready-task' as TeamTaskId, status: 'pending', ready: true }, + { ...unownedTask, id: 'blocked-task' as TeamTaskId, status: 'pending', ready: false, blockedBy: [TASK_1] }, + { ...task, id: 'completed-task' as TeamTaskId, status: 'completed', ownerName: 'worker' }, + ], + }, + }, }, - { - id: 'provisioning-id' as SessionId, - name: 'provisioning-worker', - role: 'teammate', - status: 'provisioning', - diagnostics: [], - }, - ], - tasks: [ - { ...unownedTask, id: 'ready-task' as TeamTaskId, status: 'pending', ready: true }, - { ...unownedTask, id: 'blocked-task' as TeamTaskId, status: 'pending', ready: false, blockedBy: [TASK_1] }, - { ...task, id: 'completed-task' as TeamTaskId, status: 'completed' }, - ], - } - const load = vi.fn(() => Promise.resolve({ ok: true as const, value: richView })) - const openTeammate = vi.fn(() => { throw new Error('navigation failed') }) - render() - fireEvent.click(screen.getByRole('button', { name: /Agent Team/u })) - expect(await screen.findByText('provider failed')).toBeTruthy() + }, + }) + b.injected.openTeammate = vi.fn(() => { throw new Error('navigation failed') }) + render() + openPanel() + expect(screen.getByText('provider failed')).toBeTruthy() expect(screen.getByText(zh.ready)).toBeTruthy() expect(screen.getByText(zh.blocked)).toBeTruthy() + expect(screen.getAllByText('Owner: 未分配')).toHaveLength(2) + expect(screen.getByText('Owner: worker')).toBeTruthy() const failedMember = screen.getByRole('button', { name: /failed-worker/u }) const provisioningMember = screen.getByRole('button', { name: /provisioning-worker/u }) expect(failedMember.disabled).toBe(true) expect(failedMember.querySelector('[data-state="error"]')).not.toBeNull() expect(provisioningMember.disabled).toBe(true) expect(provisioningMember.querySelector('[data-state="ongoing"]')).not.toBeNull() + expect(screen.getByRole('button', { name: /^lead/u }).disabled).toBe(true) const tasks = [...document.querySelectorAll('article')] expect(tasks.map(card => card.querySelector('[data-state]')?.getAttribute('data-state'))) .toEqual(['idle', 'warning', 'done']) + for (const card of tasks) expect(card.querySelector('button, input, select, textarea')).toBeNull() - fireEvent.click(screen.getByRole('button', { name: /^worker运行中/u })) - expect(await screen.findByText('Error: navigation failed')).toBeTruthy() - fireEvent.click(screen.getByRole('button', { name: zh.refresh })) - await waitFor(() => { expect(load).toHaveBeenCalledTimes(2) }) - fireEvent.click(screen.getByRole('button', { name: /Agent Team/u })) - expect(screen.queryByRole('dialog')).toBeNull() - fireEvent.click(screen.getByRole('button', { name: /Agent Team/u })) - await screen.findByRole('dialog') + fireEvent.click(screen.getByRole('button', { name: /^worker/u })) + expect(screen.getByRole('alert').textContent).toBe('Error: navigation failed') fireEvent.click(screen.getByRole('button', { name: zh.close })) expect(screen.queryByRole('dialog')).toBeNull() expect(document.activeElement).toBe(screen.getByRole('button', { name: /Agent Team/u })) }) - it('shows load failures', async () => { - render( Promise.resolve(remoteFailure('load failed')), - }))} />) - fireEvent.click(screen.getByRole('button', { name: /Agent Team/u })) - expect(await screen.findByRole('alert')).toHaveProperty('textContent', 'load failed (gateway/internal)') + it('closes the panel and clears a navigation failure when the conversation switches sessions', () => { + const b = bench() + b.injected.openTeammate = vi.fn(() => { throw new Error('navigation failed') }) + const rendered = render() + openPanel() + fireEvent.click(screen.getByRole('button', { name: /^worker/u })) + expect(screen.getByRole('alert')).toBeTruthy() + + const next = bench({ sessionId: 'next-lead' as SessionId, projections: {}, openState: 'loading' }) + rendered.rerender() + expect(screen.queryByRole('dialog')).toBeNull() + openPanel() + expect(screen.queryByRole('alert')).toBeNull() + expect(screen.getByRole('status').textContent).toBe(zh.loading) }) - it('shows an empty task board without a create action', async () => { - render( Promise.resolve({ ok: true, value: { members: [], tasks: [] } }), - }))} />) - fireEvent.click(screen.getByRole('button', { name: /Agent Team/u })) - expect(await screen.findByText(zh.empty)).toBeTruthy() - expect(screen.queryByRole('button', { name: /新建任务/u })).toBeNull() - expect(screen.queryByRole('textbox')).toBeNull() - }) - - it('displays task ownership and status without mutation controls', async () => { - const { ownerName: _ownerName, ...unowned } = task - const tasks: TeamTask[] = [ - { ...unowned, status: 'pending', ready: true }, - { ...task, id: 'task-2' as TeamTaskId }, - { ...task, id: 'task-3' as TeamTaskId, status: 'completed', ownerName: 'worker' }, - ] - render( Promise.resolve({ ok: true, value: { ...view, tasks } }), - }))} />) - fireEvent.click(screen.getByRole('button', { name: /Agent Team/u })) - await screen.findAllByRole('article') - expect(screen.getByText('Owner: 未分配')).toBeTruthy() - expect(screen.getByText('Owner: lead')).toBeTruthy() - expect(screen.getByText('Owner: worker')).toBeTruthy() - expect(screen.getByText(zh['status.pending'])).toBeTruthy() - expect(screen.getByText(zh['status.in_progress'])).toBeTruthy() - expect(screen.getByText(zh['status.completed'])).toBeTruthy() - expect(screen.queryByRole('button', { name: /新建任务|编辑|完成|重开|删除/u })).toBeNull() - expect(screen.queryByRole('combobox')).toBeNull() - expect(screen.queryByRole('textbox')).toBeNull() - for (const card of screen.getAllByRole('article')) { - expect(card.querySelector('button, input, select, textarea')).toBeNull() - } - }) - it('keeps panel interactions open and dismisses on outside pointer or Escape', async () => { - const rendered = render() + it('keeps panel interactions open and dismisses on outside pointer or Escape', () => { + const b = bench() + const rendered = render() const trigger = screen.getByRole('button', { name: /Agent Team/u }) fireEvent.click(trigger) - const panel = await screen.findByRole('dialog') + const panel = screen.getByRole('dialog') expect(rendered.container.contains(panel)).toBe(false) expect(document.activeElement).toBe(panel) fireEvent.pointerDown(panel) @@ -246,22 +342,22 @@ describe('TeamAction', () => { fireEvent.pointerDown(document.body) expect(screen.queryByRole('dialog')).toBeNull() fireEvent.click(trigger) - await screen.findByText('Implement runtime') - fireEvent.keyDown(screen.getByRole('button', { name: zh.refresh }), { key: 'Enter' }) + fireEvent.keyDown(screen.getByRole('button', { name: zh.close }), { key: 'Enter' }) expect(screen.queryByRole('dialog')).not.toBeNull() - fireEvent.keyDown(screen.getByRole('button', { name: zh.refresh }), { key: 'Escape' }) + fireEvent.keyDown(screen.getByRole('button', { name: zh.close }), { key: 'Escape' }) expect(screen.queryByRole('dialog')).toBeNull() expect(document.activeElement).toBe(trigger) fireEvent.keyDown(trigger, { key: 'Escape' }) expect(screen.queryByRole('dialog')).toBeNull() }) - it('keeps focus within the trigger or panel and closes when focus moves elsewhere', async () => { - render(<>) + it('keeps focus within the trigger or panel and closes when focus moves elsewhere', () => { + const b = bench() + render(<>) const trigger = screen.getByRole('button', { name: /Agent Team/u }) fireEvent.click(trigger) - const panel = await screen.findByRole('dialog') - fireEvent.blur(panel, { relatedTarget: screen.getByRole('button', { name: zh.refresh }) }) + const panel = screen.getByRole('dialog') + fireEvent.blur(panel, { relatedTarget: screen.getByRole('button', { name: zh.close }) }) expect(screen.getByRole('dialog')).toBe(panel) fireEvent.blur(panel, { relatedTarget: trigger }) expect(screen.getByRole('dialog')).toBe(panel) @@ -270,5 +366,4 @@ describe('TeamAction', () => { fireEvent.blur(panel, { relatedTarget: screen.getByRole('button', { name: 'Outside' }) }) expect(screen.queryByRole('dialog')).toBeNull() }) - }) diff --git a/packages/experimental/client-ui-agent-team/tsconfig.json b/packages/experimental/client-ui-agent-team/tsconfig.json index 188f578e84..cf196ce4c6 100644 --- a/packages/experimental/client-ui-agent-team/tsconfig.json +++ b/packages/experimental/client-ui-agent-team/tsconfig.json @@ -8,16 +8,15 @@ "references": [ { "path": "../../../vendor/cordis" }, { "path": "../agent-team" }, - { "path": "../../api/remotes/tsconfig.client.json" }, { "path": "../../api/session-controller/tsconfig.client.json" }, { "path": "../../client/locale/tsconfig.client.json" }, + { "path": "../../client/store" }, { "path": "../../client/ui-conversation/tsconfig.client.json" }, { "path": "../../client/ui-primitives" }, { "path": "../../client/ui-renderer" }, { "path": "../../client/ui-session" }, { "path": "../../client/ui-slots" }, { "path": "../../client/ui-workspace" }, - { "path": "../../core/session" }, - { "path": "../../typert/protocol" } + { "path": "../../core/session" } ] } diff --git a/packages/experimental/client-ui-voice-input/README.i18n.yaml b/packages/experimental/client-ui-voice-input/README.i18n.yaml index c81fd5bd73..50b00dd654 100644 --- a/packages/experimental/client-ui-voice-input/README.i18n.yaml +++ b/packages/experimental/client-ui-voice-input/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/experimental/client-ui-voice-input/README.md -README.md: 4d859f4d9837c249c564abfbba0682f1493502d9 -README.zh.md: 84a02e4f620feb35706de5b3bc10ec291f32c423 +README.md: 764962ff496c9fdd91073c2aa391ebceeec23608 +README.zh.md: 105cb641684eb2920c04294e74f1b5389f564c0c diff --git a/packages/experimental/client-ui-voice-input/README.md b/packages/experimental/client-ui-voice-input/README.md index 4d859f4d98..764962ff49 100644 --- a/packages/experimental/client-ui-voice-input/README.md +++ b/packages/experimental/client-ui-voice-input/README.md @@ -27,6 +27,8 @@ This optional browser plugin adds an outline microphone icon between the model s Enable the [voice input bundle](../voice-input-bundle/README.md) from Plugins. After cache inspection, missing models trigger a setup prompt with Go to setup and Later actions. Go to setup opens bundle details; Download and prepare starts installation there. Intact cached models need no prompt or repeated installation. Before preparation, local providers can show their estimated disk, memory and setup-time requirements. The preparation summary starts collapsed; expand it to inspect completed, current and pending steps. Once ready, click the microphone, allow access and click Stop to transcribe. Cancel or Escape discards capture. Recognition feedback stays inside the toolbar. Provider and language preferences are saved from bundle details through the Settings service. Browser microphone access requires HTTPS or loopback and operating-system permission. +Before downloading or retrying, **Model download source** offers Automatic and the origins advertised by the Host. SenseVoice normally offers Hugging Face and HF-Mirror; a fixed private deployment exposes only its configured origin. Manual selection uses only that source, while Automatic preserves the provider’s comparison and fallback policy. The choice is retained for retries in the current card, without changing deployment configuration or recognition preferences. Source controls are unavailable during preparation and hidden when resources are ready. + ----- diff --git a/packages/experimental/client-ui-voice-input/README.zh.md b/packages/experimental/client-ui-voice-input/README.zh.md index 84a02e4f62..105cb64168 100644 --- a/packages/experimental/client-ui-voice-input/README.zh.md +++ b/packages/experimental/client-ui-voice-input/README.zh.md @@ -27,6 +27,8 @@ kind: "package-reference" 在插件管理页启用[语音输入 Bundle](../voice-input-bundle/README.zh.md)。缓存检查后,缺少模型时弹出安装引导,可选择“前往安装”或“稍后”。“前往安装”打开 Bundle 详情,再点击“下载并准备”才开始安装。缓存完整时不弹引导,也无需重复安装。准备前,本地 Provider 可展示磁盘、内存与安装时间的估算。准备摘要默认折叠;展开后可查看已完成、当前和未开始的步骤。就绪后点击麦克风,允许访问,再点击停止以转写。取消或 Escape 丢弃录音。识别反馈留在工具栏内部。Provider 和语言偏好在 Bundle 详情中通过 Settings 服务保存。浏览器麦克风访问需要 HTTPS 或环回地址,以及操作系统权限。 +下载或重试前,**模型下载源**提供自动选择及 Host 公布的源。SenseVoice 默认提供 Hugging Face 和 HF-Mirror;固定私有部署仅显示配置的源。手动选择只使用指定源,自动选择沿用提供方的比较与回退策略。当前卡片中的重试会保留选择,不修改部署配置或识别偏好。准备期间不能改源,资源就绪后隐藏源选择器。 + ----- diff --git a/packages/experimental/client-ui-voice-input/package.json b/packages/experimental/client-ui-voice-input/package.json index bf775ee89d..8759866c9d 100644 --- a/packages/experimental/client-ui-voice-input/package.json +++ b/packages/experimental/client-ui-voice-input/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-client-ui-voice-input", "description": "Record speech and insert editable text into the conversation draft", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/client-ui-voice-input/src/client/PreparationCard.tsx b/packages/experimental/client-ui-voice-input/src/client/PreparationCard.tsx index 61a81b4586..e67edfb23f 100644 --- a/packages/experimental/client-ui-voice-input/src/client/PreparationCard.tsx +++ b/packages/experimental/client-ui-voice-input/src/client/PreparationCard.tsx @@ -44,6 +44,11 @@ function PreparationFailure({ state, t }: { state: Extract step.status === 'running' || step.status === 'failed' || step.status === 'cancelled') const preparing = preparationTone(state) === 'ongoing' const startedAt = current?.startedAt ?? ('startedAt' in state ? state.startedAt : undefined) @@ -53,8 +58,9 @@ export function PreparationCard({ provider, connected, prepare, cancelPreparatio return () => { clearInterval(timer) } }, [preparing, startedAt]) const run = async (action: () => Promise): Promise => { - setError('') + setError(''); setSubmitting(true) try { await action() } catch (failure) { setError(failure instanceof Error ? failure.message : String(failure)) } + finally { setSubmitting(false) } } const metric = state.phase === 'downloading' ? byteText(state, t) : preparing && startedAt !== undefined ? t('elapsed', { seconds: String(Math.max(0, Math.floor((now - startedAt) / 1000))) }) : '' @@ -100,10 +106,21 @@ export function PreparationCard({ provider, connected, prepare, cancelPreparatio {!expanded && } {state.phase === 'failed' && } + {canPrepare && sources.length > 0 &&
+ + {t(selectedSource === '' ? 'sourceAutoHelp' : 'sourceManualHelp')} +
}
- {['unprepared', 'cancelled', 'failed'].includes(state.phase) && } - {preparing && state.phase !== 'waking' && } + {preparing && state.phase !== 'waking' && }
{error &&

{t('failed', { message: error })}

} diff --git a/packages/experimental/client-ui-voice-input/src/client/VoiceInput.module.css b/packages/experimental/client-ui-voice-input/src/client/VoiceInput.module.css index 436b2d5286..978295ec8b 100644 --- a/packages/experimental/client-ui-voice-input/src/client/VoiceInput.module.css +++ b/packages/experimental/client-ui-voice-input/src/client/VoiceInput.module.css @@ -25,8 +25,9 @@ .stepProgress { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; } .stepProgress small { font-size: 11px; color: var(--dsw-alias-label-tertiary); overflow-wrap: anywhere; } .preferences { display: grid; gap: 12px; font-size: 13px; margin: 18px 0; } -.preferences label { display: grid; grid-template-columns: 100px minmax(120px, 280px); align-items: center; gap: 12px; } -.preferences select { border: 0.5px solid var(--dsw-alias-border-l1); border-radius: 6px; padding: 6px 10px; background: transparent; color: inherit; font: inherit; } +.preferences label, .sourceChoice label { display: grid; grid-template-columns: 100px minmax(120px, 280px); align-items: center; gap: 12px; } +.preferences select, .sourceChoice select { border: 0.5px solid var(--dsw-alias-border-l1); border-radius: 6px; padding: 6px 10px; background: transparent; color: inherit; font: inherit; } +.sourceChoice { display: grid; gap: 6px; } .preferences p { margin: 0; } .settingsCard { list-style: none; padding: 16px; border: 0.5px solid var(--dsw-alias-border-l1); border-radius: 10px; } .settingsCard > p { font-size: 13px; color: var(--dsw-alias-label-secondary); margin: 8px 0 0; } diff --git a/packages/experimental/client-ui-voice-input/src/client/VoiceInput.tsx b/packages/experimental/client-ui-voice-input/src/client/VoiceInput.tsx index 3610267fe3..dfef2387ee 100644 --- a/packages/experimental/client-ui-voice-input/src/client/VoiceInput.tsx +++ b/packages/experimental/client-ui-voice-input/src/client/VoiceInput.tsx @@ -3,7 +3,7 @@ import { useEffect, useLayoutEffect, useRef, useState } from 'react' import type { HostObservable, InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' import type { TokenSpan } from '@deepseek-ai/dsh-client-ui-conversation/client' import type { TranscriptionRequest } from '@deepseek-ai/dsh-experimental-api-speech-to-text/types' -import type { SpeechProviderId, SpeechSelection, SpeechSelectionPatch, Transcript } from '@deepseek-ai/dsh-experimental-speech-to-text/types' +import type { SpeechPreparationOptions, SpeechProviderId, SpeechSelection, SpeechSelectionPatch, Transcript } from '@deepseek-ai/dsh-experimental-speech-to-text/types' import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol' import { RecordingError, audioBase64, type Recording } from './audio.ts' import type { SpeechReadiness } from './readiness.ts' @@ -17,7 +17,7 @@ export interface VoiceInputActions { /** @returns one microphone operation owned by the plugin lifecycle. */ createRecording: () => Recording transcribe: (request: TranscriptionRequest, signal: AbortSignal) => Promise> - prepare: (providerId: SpeechProviderId) => Promise + prepare: (providerId: SpeechProviderId, options?: SpeechPreparationOptions) => Promise cancelPreparation: (providerId: SpeechProviderId) => Promise configure: (patch: SpeechSelectionPatch) => Promise } diff --git a/packages/experimental/client-ui-voice-input/src/client/locales.ts b/packages/experimental/client-ui-voice-input/src/client/locales.ts index f275ad72d6..4d145cc35d 100644 --- a/packages/experimental/client-ui-voice-input/src/client/locales.ts +++ b/packages/experimental/client-ui-voice-input/src/client/locales.ts @@ -57,6 +57,12 @@ export const zh = { 'downloadAdvice.storage': '请检查运行 DSH 的机器的剩余磁盘空间,以及模型目录的写入权限后重试。', 'downloadAdvice.unknown': '请检查运行 DSH 的机器的网络、磁盘空间和模型目录权限后重试。', downloadSource: '下载来源:{source}', + sourceChoice: '模型下载源', + sourceAuto: '自动选择(推荐)', + sourceHuggingFace: 'Hugging Face', + sourceMirror: 'HF-Mirror(国内镜像)', + sourceAutoHelp: '优先使用响应较快的源,下载失败时自动尝试其他源。', + sourceManualHelp: '仅从所选源下载;已下载并通过校验的文件会复用。', downloadCode: '错误码:{code}', 'reconnecting': '正在连接语音服务…', 'preparation.unprepared': '首次使用需要下载本地识别模型。', @@ -77,7 +83,7 @@ export const zh = { start: '开始录音', stop: '停止并识别', cancel: '取消', insert: '插入文字', loading: '正在读取识别服务…', requesting: '请允许使用麦克风…', recording: '正在录音…', interrupted: '录音中断,请重试。', - local: '音频在运行 DSH 的机器上识别。首次准备默认从 Hugging Face 下载模型,该机器需要能访问 Hugging Face 及其模型下载服务。如需代理,请在该机器上配置。', + local: '音频在运行 DSH 的机器上识别。需要下载模型时,请确保该机器能访问所选下载源及其文件服务。如需代理,请在该机器上配置。', cloud: '音频将发送至所选云端服务。', empty: '未识别到语音', cancelled: '已取消语音输入。', conflict: '草稿已被修改。识别文字已保留,可在当前光标位置插入。', @@ -144,6 +150,12 @@ export const en: Record = { 'downloadAdvice.storage': 'Check available disk space and write permission for the model directory on the machine running DSH, then retry.', 'downloadAdvice.unknown': 'Check the network, disk space and model directory permissions on the machine running DSH, then retry.', downloadSource: 'Download source: {source}', + sourceChoice: 'Model download source', + sourceAuto: 'Automatic (recommended)', + sourceHuggingFace: 'Hugging Face', + sourceMirror: 'HF-Mirror (China mirror)', + sourceAutoHelp: 'Prefer the first responding source and try another if a download fails.', + sourceManualHelp: 'Download only from the selected source. Verified downloaded files are reused.', downloadCode: 'Error code: {code}', 'reconnecting': 'Connecting to speech recognition…', 'preparation.unprepared': 'First use downloads the local recognition models.', @@ -164,7 +176,7 @@ export const en: Record = { start: 'Start recording', stop: 'Stop and transcribe', cancel: 'Cancel', insert: 'Insert text', loading: 'Loading recognition services…', requesting: 'Allow microphone access to continue…', recording: 'Recording…', interrupted: 'Recording was interrupted. Please try again.', - local: 'Audio is recognized on the machine running DSH. First setup downloads models from Hugging Face by default, so that machine must be able to access Hugging Face and its model download services. If a proxy is needed, configure it on that machine.', + local: 'Audio is recognized on the machine running DSH. If models need downloading, that machine must be able to reach the selected source and its file services. Configure a proxy on that machine if needed.', cloud: 'Audio will be sent to the selected cloud service.', empty: 'No speech recognized', cancelled: 'Voice input cancelled.', conflict: 'Your draft changed. The transcript is preserved and can be inserted at the current cursor.', diff --git a/packages/experimental/client-ui-voice-input/src/client/mount.ts b/packages/experimental/client-ui-voice-input/src/client/mount.ts index cff12b511b..a3f0f2c2ae 100644 --- a/packages/experimental/client-ui-voice-input/src/client/mount.ts +++ b/packages/experimental/client-ui-voice-input/src/client/mount.ts @@ -31,7 +31,9 @@ function registerUi(ctx: Context): void { }, transcribe: async (request, signal) => await ctx.remote.speech.transcribe(request, signal), configure: async (patch) => { const result = await ctx.remote.speech.configure(patch); if (!result.ok) throw result.error }, - prepare: async (providerId) => { const result = await ctx.remote.speech.prepare(providerId); if (!result.ok) throw result.error }, + prepare: async (providerId, options) => { + const result = await ctx.remote.speech.prepare(providerId, options); if (!result.ok) throw result.error + }, cancelPreparation: async (providerId) => { const result = await ctx.remote.speech.cancelPreparation(providerId); if (!result.ok) throw result.error }, diff --git a/packages/experimental/client-ui-voice-input/tests/mount.client.spec.ts b/packages/experimental/client-ui-voice-input/tests/mount.client.spec.ts index 6f3904b972..638b5d913b 100644 --- a/packages/experimental/client-ui-voice-input/tests/mount.client.spec.ts +++ b/packages/experimental/client-ui-voice-input/tests/mount.client.spec.ts @@ -77,6 +77,8 @@ it('withdraws its Remote, localized slot and microphone captures on disposal', a const dispose = vi.spyOn(pending, 'dispose') await actions.configure({ language: 'zh' }) await actions.prepare('local' as SpeechProviderId) + await actions.prepare('local' as SpeechProviderId, { downloadSource: 'https://hf-mirror.com' }) + expect(b.prepare).toHaveBeenLastCalledWith('local', { downloadSource: 'https://hf-mirror.com' }) await actions.cancelPreparation('local' as SpeechProviderId) for (const slot of ['plugins.bundle.config', 'plugins.bundle.activation'] as const) { const item = b.ctx.slots.entries(slot)[0]! diff --git a/packages/experimental/client-ui-voice-input/tests/preparation.client.spec.tsx b/packages/experimental/client-ui-voice-input/tests/preparation.client.spec.tsx index 759bb1f994..d3b9a22ac9 100644 --- a/packages/experimental/client-ui-voice-input/tests/preparation.client.spec.tsx +++ b/packages/experimental/client-ui-voice-input/tests/preparation.client.spec.tsx @@ -70,6 +70,7 @@ it('starts preparation and offers retry without showing progress for completed o expect(b.prepare).toHaveBeenCalledWith(id) b.rerender() + await waitFor(() => { expect(screen.getByRole('button', { name: zh.retryPrepare }).disabled).toBe(false) }) b.prepare.mockRejectedValueOnce(new Error('offline')) fireEvent.click(screen.getByRole('button', { name: zh.retryPrepare })) await screen.findByText('语音识别失败:offline') @@ -155,3 +156,56 @@ it('shows provider-specific installation estimates before preparation and remove b.rerender() expect(screen.queryByText(zh['setup.local'])).toBeNull() }) + +it('offers Host sources, submits a manual choice, and retains it for retry', async () => { + const b = fixture({ phase: 'unprepared' }) + const provider = { ...b.props.provider, downloadSources: ['https://huggingface.co', 'https://hf-mirror.com'] } + b.rerender() + const picker = screen.getByLabelText(zh.sourceChoice) + expect(picker.value).toBe('') + expect(within(picker).getAllByRole('option').map(option => option.textContent)).toEqual([zh.sourceAuto, zh.sourceHuggingFace, zh.sourceMirror]) + expect(screen.getByText(zh.sourceAutoHelp)).toBeTruthy() + fireEvent.change(picker, { target: { value: 'https://hf-mirror.com' } }) + expect(screen.getByText(zh.sourceManualHelp)).toBeTruthy() + fireEvent.click(screen.getByRole('button', { name: zh.prepare })) + await waitFor(() => { expect(b.prepare).toHaveBeenCalledWith(id, { downloadSource: 'https://hf-mirror.com' }) }) + b.rerender() + expect(screen.getByLabelText(zh.sourceChoice).value).toBe('https://hf-mirror.com') + fireEvent.change(picker, { target: { value: 'https://huggingface.co' } }) + fireEvent.click(screen.getByRole('button', { name: zh.retryPrepare })) + await waitFor(() => { expect(b.prepare).toHaveBeenLastCalledWith(id, { downloadSource: 'https://huggingface.co' }) }) + fireEvent.change(picker, { target: { value: '' } }) + fireEvent.click(screen.getByRole('button', { name: zh.retryPrepare })) + await waitFor(() => { expect(b.prepare).toHaveBeenLastCalledWith(id) }) +}) + +it('respects a fixed private source and removes a choice that the Host withdraws', async () => { + const b = fixture({ phase: 'unprepared' }) + b.rerender() + fireEvent.change(screen.getByLabelText(zh.sourceChoice), { target: { value: 'https://hf-mirror.com' } }) + b.rerender() + const picker = screen.getByLabelText(zh.sourceChoice) + expect(picker.value).toBe('https://private.example') + expect(picker.disabled).toBe(true) + expect(within(picker).getAllByRole('option')).toHaveLength(1) + fireEvent.click(screen.getByRole('button', { name: zh.prepare })) + await waitFor(() => { expect(b.prepare).toHaveBeenCalledWith(id, { downloadSource: 'https://private.example' }) }) + b.rerender() + expect(screen.queryByLabelText(zh.sourceChoice)).toBeNull() +}) + +it('locks source changes while a preparation request is pending and hides them during active work', async () => { + const b = fixture({ phase: 'unprepared' }), pending = Promise.withResolvers() + b.prepare.mockImplementation(async () => { await pending.promise }) + const provider = { ...b.props.provider, downloadSources: ['https://huggingface.co', 'https://hf-mirror.com'] } + b.rerender() + fireEvent.click(screen.getByRole('button', { name: zh.prepare })) + try { + expect(screen.getByLabelText(zh.sourceChoice).disabled).toBe(true) + expect(screen.getByRole('button', { name: zh.prepare }).disabled).toBe(true) + } finally { await act(async () => { pending.resolve(undefined); await pending.promise }) } + b.rerender() + expect(screen.getByLabelText(zh.sourceChoice).disabled).toBe(true) + b.rerender() + expect(screen.queryByLabelText(zh.sourceChoice)).toBeNull() +}) diff --git a/packages/experimental/computer-use-cua-driver-mcp/package.json b/packages/experimental/computer-use-cua-driver-mcp/package.json index 194f3d75e8..172690c42f 100644 --- a/packages/experimental/computer-use-cua-driver-mcp/package.json +++ b/packages/experimental/computer-use-cua-driver-mcp/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-computer-use-cua-driver-mcp", "description": "Experimental computer use through an installed Cua Driver MCP executable", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/computer-use-cua-driver-native/package.json b/packages/experimental/computer-use-cua-driver-native/package.json index cac34e5873..80b7447d8a 100644 --- a/packages/experimental/computer-use-cua-driver-native/package.json +++ b/packages/experimental/computer-use-cua-driver-native/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-computer-use-cua-driver-native", "description": "Experimental computer-use provider embedding the Cua Driver native npm SDK", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/inspector/package.json b/packages/experimental/inspector/package.json index 86ad829569..4e1325661b 100644 --- a/packages/experimental/inspector/package.json +++ b/packages/experimental/inspector/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-inspector", "description": "Experimental cross-realm CDP hub for Host debugging and Client Runtime inspection", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/ptc-runtime-python/package.json b/packages/experimental/ptc-runtime-python/package.json index 973e6aeb0b..a938a45f2c 100644 --- a/packages/experimental/ptc-runtime-python/package.json +++ b/packages/experimental/ptc-runtime-python/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-ptc-runtime-python", "description": "CPython subprocess implementation of the DeepSeek Harness PTC execution seam", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "repository": { "type": "git", "url": "git+https://github.com/deepseek-ai/deepseek-harness.git", diff --git a/packages/experimental/speech-to-text-sensevoice/README.i18n.yaml b/packages/experimental/speech-to-text-sensevoice/README.i18n.yaml index a6f7e23754..017646ce74 100644 --- a/packages/experimental/speech-to-text-sensevoice/README.i18n.yaml +++ b/packages/experimental/speech-to-text-sensevoice/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/experimental/speech-to-text-sensevoice/README.md -README.md: 412b179d03e8b6a9eff143a5884bfb393996249b -README.zh.md: b9a00d7ba9cb454fab9b1153bb1855107b08a34a +README.md: 3d553d2a235346e8d51bf9c8c137bc824b0f3d48 +README.zh.md: e4ab23cc48df0d93431eb22f6c17afcebfd78916 diff --git a/packages/experimental/speech-to-text-sensevoice/README.md b/packages/experimental/speech-to-text-sensevoice/README.md index 412b179d03..3d553d2a23 100644 --- a/packages/experimental/speech-to-text-sensevoice/README.md +++ b/packages/experimental/speech-to-text-sensevoice/README.md @@ -25,7 +25,11 @@ This provider recognizes speech with SenseVoiceSmall ONNX and Silero VAD on the ## Use this package -The [bundle](../voice-input-bundle/README.md) supplies an absolute `dataRoot` under the DSH home. Preparation downloads revision-pinned model files, verifies their sizes and SHA-256 hashes, then loads them. `precision` defaults to `int8`; `fp32` selects the larger reference weights. `modelDirectory` supplies an existing absolute directory containing the selected ONNX file and `tokens.txt`; `vadModelPath` selects an existing Silero ONNX file. `modelOrigin` selects a Hugging Face-compatible download origin while preserving pinned paths and checksums. Verified completed files remain reusable after cancellation or failure. +The [bundle](../voice-input-bundle/README.md) supplies an absolute `dataRoot` under the DSH home. Preparation downloads revision-pinned model files, verifies their sizes and SHA-256 hashes, then loads them. `precision` defaults to `int8`; `fp32` selects the larger reference weights. `modelDirectory` supplies an existing absolute directory containing the selected ONNX file and `tokens.txt`; `vadModelPath` selects an existing Silero ONNX file. Verified completed files remain reusable after cancellation or failure. + +Before each missing asset is downloaded, the Host compares the Hugging Face-compatible `modelOrigins`, which default to `https://huggingface.co` and [HF-Mirror](https://hf-mirror.com). Concurrent HEAD requests follow the pinned file path and redirects through the Host's fetch proxy; the first 2xx response is tried first. `modelProbeTimeoutMs` defaults to 3000 ms; when every probe fails, downloads use the configured order. Network, HTTP, certificate and integrity failures try the remaining sources; cancellation, storage failures and unclassified failures stop preparation. Pinned revisions, sizes and SHA-256 hashes apply to every source. Response latency does not measure download throughput; the preparation deadline still bounds the complete download. + +An explicit `modelOrigin` uses only that origin, without probing or public fallback. A single-entry `modelOrigins` also skips probing. Verified caches and explicit offline paths require no source requests. The voice UI offers the advertised sources before preparation or retry. A manual choice overrides selection for that task only, uses one source without fallback, and cannot replace an active task’s source. The Host rejects choices outside its configured origins; fully offline deployments advertise no download sources. Download failures identify the asset and source origin, with a classified cause and HTTP status or diagnostic code when available. The public state omits URL credentials, query strings and raw cause messages. Retry reuses verified files; partial downloads restart. diff --git a/packages/experimental/speech-to-text-sensevoice/README.zh.md b/packages/experimental/speech-to-text-sensevoice/README.zh.md index b9a00d7ba9..e4ab23cc48 100644 --- a/packages/experimental/speech-to-text-sensevoice/README.zh.md +++ b/packages/experimental/speech-to-text-sensevoice/README.zh.md @@ -25,7 +25,11 @@ kind: "package-reference" ## 使用此包 -[Bundle](../voice-input-bundle/README.zh.md) 在 DSH 主目录下提供绝对 `dataRoot`。准备会下载固定修订的模型文件,校验大小与 SHA-256,再加载模型。`precision` 默认为 `int8`,`fp32` 选择较大的参考权重。`modelDirectory` 提供包含所选 ONNX 文件和 `tokens.txt` 的已有绝对目录,`vadModelPath` 选择已有 Silero ONNX 文件。`modelOrigin` 选择兼容 Hugging Face 的下载源,固定路径和校验值保持不变。取消或失败后复用已完成并通过校验的文件。 +[Bundle](../voice-input-bundle/README.zh.md) 在 DSH 主目录下提供绝对 `dataRoot`。准备会下载固定修订的模型文件,校验大小与 SHA-256,再加载模型。`precision` 默认为 `int8`,`fp32` 选择较大的参考权重。`modelDirectory` 提供包含所选 ONNX 文件和 `tokens.txt` 的已有绝对目录,`vadModelPath` 选择已有 Silero ONNX 文件。取消或失败后复用已完成并通过校验的文件。 + +下载每个缺失文件前,Host 会比较兼容 Hugging Face 的 `modelOrigins`,默认为 `https://huggingface.co` 和 [HF-Mirror](https://hf-mirror.com)。并发 HEAD 请求沿用固定文件路径,通过 Host 的 fetch 代理跟随重定向,先返回 2xx 的源优先下载。`modelProbeTimeoutMs` 默认为 3000 毫秒;所有探测均失败时按配置顺序下载。网络、HTTP、证书或完整性校验失败会尝试其他源;取消、存储错误和未分类错误会停止准备。所有源都必须符合固定修订、大小和 SHA-256 校验值。响应延迟不代表下载吞吐量,完整下载仍受准备阶段的总超时约束。 + +显式 `modelOrigin` 仅使用指定源,不探测也不回退到公共源。只有一个地址的 `modelOrigins` 同样跳过探测。已校验的缓存和显式离线路径无需请求下载源。语音 UI 在准备或重试前提供已公布的下载源。手动选择仅覆盖本次任务,只使用指定源而不回退,且不能替换进行中任务的源。Host 拒绝配置之外的源;完全离线部署不公布下载源。 下载失败会标明文件与下载源,并提供原因分类,以及可用的 HTTP 状态或错误码。公开状态不包含 URL 凭据、查询参数和原始底层错误消息。重试复用已校验文件,未完成的文件重新下载。 diff --git a/packages/experimental/speech-to-text-sensevoice/package.json b/packages/experimental/speech-to-text-sensevoice/package.json index 1feb884e68..65acfd37de 100644 --- a/packages/experimental/speech-to-text-sensevoice/package.json +++ b/packages/experimental/speech-to-text-sensevoice/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-speech-to-text-sensevoice", "description": "Local SenseVoice ONNX transcription with a managed sherpa-onnx process", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/speech-to-text-sensevoice/src/config.ts b/packages/experimental/speech-to-text-sensevoice/src/config.ts index 33729f2dd6..c3c24232d8 100644 --- a/packages/experimental/speech-to-text-sensevoice/src/config.ts +++ b/packages/experimental/speech-to-text-sensevoice/src/config.ts @@ -14,8 +14,12 @@ export interface Config { vadModelPath?: string | undefined /** Weight precision; INT8 minimizes first-use download and model storage. */ precision: 'int8' | 'fp32' - /** Hugging Face-compatible origin for pinned model URLs, including private mirrors. */ - modelOrigin: string + /** Explicit Hugging Face-compatible origin; bypasses automatic selection and public fallback. */ + modelOrigin?: string | undefined + /** Hugging Face-compatible origins compared before downloading each missing asset. */ + modelOrigins: string[] + /** Deadline for concurrent HEAD probes, including redirects to the actual asset. */ + modelProbeTimeoutMs: number /** CPU intra-operation thread count. */ threads: number /** Maximum speech segment length passed to the recognizer. */ @@ -53,7 +57,10 @@ export const Config: z, Config> = z.object({ modelDirectory: z.union([z.string().min(1), z.const(undefined)]), vadModelPath: z.union([z.string().min(1), z.const(undefined)]), precision: z.union(['int8', 'fp32']).default('int8'), - modelOrigin: z.string().pattern(/^https?:\/\/[^/\s?#@]+\/?$/).default('https://huggingface.co'), + modelOrigin: z.union([z.string().pattern(/^https?:\/\/[^/\s?#@]+\/?$/), z.const(undefined)]), + modelOrigins: z.array(z.string().pattern(/^https?:\/\/[^/\s?#@]+\/?$/)).min(1) + .default(['https://huggingface.co', 'https://hf-mirror.com']), + modelProbeTimeoutMs: z.natural().min(1).max(MAX_TIMER_DELAY_MS).default(3000), threads: z.natural().min(1).default(2), segmentSeconds: z.number().min(1).max(120).default(30), vadThreshold: z.number().min(0).max(1).default(0.5), diff --git a/packages/experimental/speech-to-text-sensevoice/src/index.ts b/packages/experimental/speech-to-text-sensevoice/src/index.ts index 987f0443ed..f714a022c7 100644 --- a/packages/experimental/speech-to-text-sensevoice/src/index.ts +++ b/packages/experimental/speech-to-text-sensevoice/src/index.ts @@ -21,12 +21,12 @@ export function apply(ctx: Context, config: Config): void { for (const path of [config.dataRoot, config.modelDirectory, config.vadModelPath]) { if (path !== undefined && !isAbsolute(path)) throw new Error(`SenseVoice paths must be absolute: ${path}`) } - new URL(config.modelOrigin) + for (const origin of config.modelOrigin === undefined ? config.modelOrigins : [config.modelOrigin]) new URL(origin) const worker = new SenseVoiceWorker(ctx, config) const estimatedBytes = config.precision === 'int8' ? 1_000_000_000 : 2_000_000_000 ctx.effect(() => { const unregister = ctx.speechToText.register({ - info: { id: config.providerId as SpeechProviderId, name: `SenseVoiceSmall (${config.precision.toUpperCase()})`, location: 'host-local', languages, + info: { id: config.providerId as SpeechProviderId, name: `SenseVoiceSmall (${config.precision.toUpperCase()})`, location: 'host-local', languages, downloadSources: worker.downloadSources, setupEstimate: { recommendedDiskBytes: estimatedBytes, expectedMemoryBytes: estimatedBytes, minimumMinutes: 1, maximumMinutes: 10 } }, preparation: worker, diff --git a/packages/experimental/speech-to-text-sensevoice/src/model-sources.ts b/packages/experimental/speech-to-text-sensevoice/src/model-sources.ts new file mode 100644 index 0000000000..e321dedbfa --- /dev/null +++ b/packages/experimental/speech-to-text-sensevoice/src/model-sources.ts @@ -0,0 +1,41 @@ +/** Host-side response comparison for pinned model files on Hugging Face-compatible origins. */ +import { deadline } from '@deepseek-ai/dsh-timeout' + +/** + * Prefer the first successful HEAD response while retaining other sources for download fallback. + * All probes settle before returning; if every probe fails, the configured order is preserved. + * @param assetUrl - revision-pinned upstream file URL. + * @param origins - nonempty configured origins, or one explicit deployment origin. + * @param timeoutMs - maximum probe duration, including redirects. + * @param signal - preparation cancellation or deadline. + * @returns deduplicated download URLs with the first responding source first; a single source needs no probe. + */ +export async function orderModelSources(assetUrl: string, origins: readonly string[], timeoutMs: number, + signal: AbortSignal): Promise { + signal.throwIfAborted() + const path = new URL(assetUrl).pathname + const urls = [...new Set(origins.map(origin => new URL(path, origin).href))] + if (urls.length === 1) return urls + const finished = new AbortController() + using timeout = deadline(signal, timeoutMs, 'SPEECH_SOURCE_PROBE_TIMEOUT') + const probing = AbortSignal.any([timeout.signal, finished.signal]) + const requests = urls.map(async url => ({ url, response: await fetch(url, { method: 'HEAD', signal: probing }) })) + let preferred: string | undefined + try { + preferred = await Promise.any(requests.map(async (request) => { + const { url, response } = await request + if (!response.ok) throw new Error(`Model source probe returned HTTP ${response.status}`) + return url + })) + } catch (_unavailableSources) { + // Downloads can still work when an origin refuses HEAD or exceeds the probe deadline. + } finally { + finished.abort() + const settled = await Promise.allSettled(requests) + await Promise.allSettled(settled.map(async (result) => { + if (result.status === 'fulfilled') await result.value.response.body?.cancel() + })) + } + signal.throwIfAborted() + return preferred === undefined ? urls : [preferred, ...urls.filter(url => url !== preferred)] +} diff --git a/packages/experimental/speech-to-text-sensevoice/src/recognizer.ts b/packages/experimental/speech-to-text-sensevoice/src/recognizer.ts index fefd02acac..a74e204b6d 100644 --- a/packages/experimental/speech-to-text-sensevoice/src/recognizer.ts +++ b/packages/experimental/speech-to-text-sensevoice/src/recognizer.ts @@ -3,7 +3,7 @@ import { randomBytes } from 'node:crypto' import { mkdir } from 'node:fs/promises' import type { Context } from '@deepseek-ai/cordis' import type { SubprocessHandle } from '@deepseek-ai/dsh-subprocess' -import type { SpeechInput, SpeechPreparationState, SpeechPreparationStep, SpeechPreparationStepKind, Transcript } from '@deepseek-ai/dsh-experimental-speech-to-text/types' +import type { SpeechPreparationOptions, SpeechInput, SpeechPreparationState, SpeechPreparationStep, SpeechPreparationStepKind, Transcript } from '@deepseek-ai/dsh-experimental-speech-to-text/types' import { deadline } from '@deepseek-ai/dsh-timeout' import { z } from 'zod' import type { Config } from './config.ts' @@ -109,9 +109,13 @@ export class SenseVoiceWorker { private state: SpeechPreparationState & { readonly steps: readonly SpeechPreparationStep[] } private readonly listeners = new Set<() => void>() private lastProgressAt = 0 - private preparing: { abort: AbortController; settled: Promise; completed: boolean } | undefined + /** Configured origins available for explicit downloads; offline deployments expose no choices. */ + readonly downloadSources: readonly string[] + private preparing: { abort: AbortController; settled: Promise; completed: boolean; downloadSource: string | undefined } | undefined constructor(private readonly ctx: Context, private readonly config: Config) { + this.downloadSources = config.modelDirectory !== undefined && config.vadModelPath !== undefined ? [] + : [...new Set((config.modelOrigin === undefined ? config.modelOrigins : [config.modelOrigin]).map(origin => new URL(origin).origin))] const kinds: SpeechPreparationStepKind[] = ['check'] if (config.modelDirectory === undefined) kinds.push('model') if (config.vadModelPath === undefined) kinds.push('vad') @@ -172,16 +176,23 @@ export class SenseVoiceWorker { }) } - /** Start one Host-owned preparation task; repeated callers join it. */ - prepare(): void { - this.runPreparation(async (signal) => { await this.start(signal) }) + /** + * Start or join one Host-owned preparation task with a fixed download source. + * @param options - omitted source uses deployment policy; a manual source must be advertised and disables fallback. + */ + prepare(options: SpeechPreparationOptions = {}): void { + const source = options.downloadSource + if (source !== undefined && !this.downloadSources.includes(source)) throw new Error('Speech download source is unavailable') + const config = source === undefined ? this.config : Object.assign({}, this.config, { modelOrigin: source }) + this.runPreparation(async (signal) => { await this.start(signal, config) }, source) } - private runPreparation(run: (signal: AbortSignal) => Promise): void { + private runPreparation(run: (signal: AbortSignal) => Promise, downloadSource?: string): void { + if (this.preparing && this.preparing.downloadSource !== downloadSource) throw new Error('Cancel preparation before changing its download source') if (this.preparing || this.worker) return this.lifetime.signal.throwIfAborted() const abort = new AbortController() - const task = { abort, settled: Promise.resolve(), completed: false } + const task = { abort, settled: Promise.resolve(), completed: false, downloadSource } this.preparing = task task.settled = this.enqueue(run, abort.signal) .catch((error: unknown) => { @@ -236,13 +247,13 @@ export class SenseVoiceWorker { return await job } - private async start(signal: AbortSignal): Promise { + private async start(signal: AbortSignal, preparationConfig: Config = this.config): Promise { if (this.worker?.closed) await this.stop() if (this.worker) return this.worker using setup = deadline(signal, this.config.prepareTimeoutMs, 'SPEECH_PREPARE_TIMEOUT') const cached = this.runtime !== undefined if (!cached) this.publish({ phase: 'checking', step: 'check', startedAt: Date.now() }) - const runtime = this.runtime ?? await prepareRuntime(this.ctx, this.config, setup.signal, (state) => { this.publish(state) }) + const runtime = this.runtime ?? await prepareRuntime(this.ctx, preparationConfig, setup.signal, (state) => { this.publish(state) }) this.runtime = runtime setup.signal.throwIfAborted() this.publish({ phase: cached ? 'waking' : 'loading', step: 'load', startedAt: Date.now() }) diff --git a/packages/experimental/speech-to-text-sensevoice/src/runtime.ts b/packages/experimental/speech-to-text-sensevoice/src/runtime.ts index e67adfe240..67da743073 100644 --- a/packages/experimental/speech-to-text-sensevoice/src/runtime.ts +++ b/packages/experimental/speech-to-text-sensevoice/src/runtime.ts @@ -13,6 +13,7 @@ import type { Config } from './config.ts' import type { SpeechPreparationState } from '@deepseek-ai/dsh-experimental-speech-to-text/types' import { timeoutOf } from '@deepseek-ai/dsh-timeout' import { classifyDownloadFailure, SpeechDownloadError } from './download-error.ts' +import { orderModelSources } from './model-sources.ts' /** Release-pinned downloadable file. */ export interface Asset { @@ -113,6 +114,7 @@ export async function downloadAsset(asset: Asset, root: string, signal: AbortSig const response = await fetch(asset.url, { signal }) source = new URL(response.url || asset.url).origin if (!response.ok || !response.body) { + await response.body?.cancel() throw new SpeechDownloadError({ resource: asset.name, source, reason: 'http', status: response.status }) } const digest = createHash('sha256') @@ -148,7 +150,7 @@ export async function downloadAsset(asset: Asset, root: string, signal: AbortSig /** * Resolve the bundled native runtime and prepare verified ONNX models on demand. * @param _ctx - Host context owning the preparation task. - * @param config - model paths, precision, and download origin. + * @param config - model paths, precision, and download source policy. * @param signal - preparation cancellation or deadline. * @param report - Host-owned progress publisher. * @returns verified model and worker paths. @@ -157,9 +159,18 @@ export async function prepareRuntime(_ctx: Context, config: Config, signal: Abor report: (state: SpeechPreparationState) => void = () => {}): Promise { const { lock, modelRoot, paths } = resolveRuntime(config) const download = async (asset: Asset, root: string, step: 'model' | 'vad'): Promise => { - const url = new URL(asset.url) - const pinned = { ...asset, url: `${config.modelOrigin.replace(/\/$/, '')}${url.pathname}` } - await downloadAsset(pinned, root, signal, (state) => { report({ ...state, step }) }) + if (await matchesAsset(join(root, asset.name), asset, signal)) return + const origins = config.modelOrigin === undefined ? config.modelOrigins : [config.modelOrigin] + const urls = await orderModelSources(asset.url, origins, config.modelProbeTimeoutMs, signal) + for (const [index, url] of urls.entries()) { + try { + await downloadAsset({ ...asset, url }, root, signal, (state) => { report({ ...state, step }) }) + return + } catch (error) { + if (signal.aborted || !(error instanceof SpeechDownloadError) || error.download.reason === 'storage' + || error.download.reason === 'unknown' || index === urls.length - 1) throw error + } + } } if (config.modelDirectory === undefined) { report({ phase: 'checking', step: 'model', startedAt: Date.now() }) diff --git a/packages/experimental/speech-to-text-sensevoice/tests/installer.spec.ts b/packages/experimental/speech-to-text-sensevoice/tests/installer.spec.ts index 0e3b532f70..c6059e06a5 100644 --- a/packages/experimental/speech-to-text-sensevoice/tests/installer.spec.ts +++ b/packages/experimental/speech-to-text-sensevoice/tests/installer.spec.ts @@ -1,7 +1,7 @@ /** ONNX preparation reuses verified files and honors explicit offline deployments. */ import { writeFileSync } from 'node:fs' import { createHash } from 'node:crypto' -import { access, mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises' +import { access, mkdtemp, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { Context } from '@deepseek-ai/cordis' @@ -19,7 +19,7 @@ const asset = (name: string): Asset => ({ name, url: `https://huggingface.co/own const fake = vi.hoisted(() => ({ lock: {} })) vi.mock('node:fs/promises', async (importOriginal) => { const actual = await importOriginal() - return { ...actual, access: vi.fn(actual.access) } + return { ...actual, access: vi.fn(actual.access), rename: vi.fn(actual.rename) } }) vi.mock('node:fs', async (importOriginal) => { const actual = await importOriginal() @@ -92,7 +92,7 @@ it('reports filesystem access errors instead of treating unreadable caches as ab async function fixture() { const root = await mkdtemp(join(tmpdir(), 'dsh-speech-onnx-')); roots.push(root) fake.lock = { models: { int8: asset('model.int8.onnx'), fp32: asset('model.onnx') }, tokens: asset('tokens.txt'), vad: asset('silero_vad.onnx') } - const fetcher = vi.fn(async (_url: string) => new Response(bytes)); vi.stubGlobal('fetch', fetcher) + const fetcher = vi.fn(async (_url: string, _init?: RequestInit) => new Response(bytes)); vi.stubGlobal('fetch', fetcher) return { root, fetcher } } it.each(['darwin-arm64', 'darwin-x64', 'linux-arm64', 'linux-x64', 'win32-x64'])('prepares only model files on %s', async (target) => { @@ -143,3 +143,61 @@ it('rejects cached files changed before the final verification stage', async () if (state.step === 'verify') writeFileSync(join(root, 'models', 'sensevoice-onnx', 'model.int8.onnx'), 'changed') })).rejects.toThrow('verification failed') }) + +it('downloads missing assets from the first responsive mirror and reuses verified files without probes', async () => { + const { root, fetcher } = await fixture() + fetcher.mockImplementation(async (url, init) => init?.method === 'HEAD' + ? new Response(null, { status: url.startsWith('https://hf-mirror.com/') ? 200 : 503 }) : new Response(bytes)) + const config = Config({ dataRoot: root }) + await prepareRuntime(new Context(), config, new AbortController().signal) + const downloads = fetcher.mock.calls.filter(([, init]) => init?.method !== 'HEAD').map(([url]) => url) + expect(downloads).toEqual(['model.int8.onnx', 'tokens.txt', 'silero_vad.onnx'] + .map(name => `https://hf-mirror.com/owner/model/resolve/revision/${name}`)) + fetcher.mockClear() + await prepareRuntime(new Context(), config, new AbortController().signal) + expect(fetcher).not.toHaveBeenCalled() +}) + +it.each(['http', 'network', 'integrity'] as const)('falls back after a %s failure without accepting unverified bytes', async (reason) => { + const { root, fetcher } = await fixture() + fetcher.mockImplementation(async (url, init) => { + const mirror = url.startsWith('https://hf-mirror.com/') + if (init?.method === 'HEAD') return new Response(null, { status: mirror ? 200 : 503 }) + if (!mirror) return new Response(bytes) + if (reason === 'network') throw new TypeError('fetch failed') + return reason === 'http' ? new Response('unavailable', { status: 503 }) : new Response('corrupt') + }) + const paths = await prepareRuntime(new Context(), Config({ dataRoot: root }), new AbortController().signal) + expect(await readFile(paths.model)).toEqual(bytes) + const downloads = fetcher.mock.calls.filter(([, init]) => init?.method !== 'HEAD').map(([url]) => new URL(url).origin) + expect(downloads).toEqual(['https://hf-mirror.com', 'https://huggingface.co', + 'https://hf-mirror.com', 'https://huggingface.co', 'https://hf-mirror.com', 'https://huggingface.co']) +}) + +it.each(['EACCES', 'unclassified'])('does not switch sources for a local publication failure (%s)', async (code) => { + const { root, fetcher } = await fixture() + vi.mocked(rename).mockRejectedValueOnce(Object.assign(new Error('cannot publish'), { code })) + await expect(prepareRuntime(new Context(), Config({ dataRoot: root }), new AbortController().signal)).rejects.toMatchObject({ + download: { reason: code === 'EACCES' ? 'storage' : 'unknown' }, + }) + expect(fetcher.mock.calls.filter(([, init]) => init?.method !== 'HEAD')).toHaveLength(1) +}) + +it('does not switch sources after cancellation during a download', async () => { + const { root, fetcher } = await fixture(), abort = new AbortController(), reason = new Error('cancelled') + fetcher.mockImplementation(async (_url, init) => { + if (init?.method === 'HEAD') return new Response(null) + abort.abort(reason); throw reason + }) + await expect(prepareRuntime(new Context(), Config({ dataRoot: root }), abort.signal)).rejects.toBe(reason) + expect(fetcher.mock.calls.filter(([, init]) => init?.method !== 'HEAD')).toHaveLength(1) +}) + +it('reports the final source when every download fails', async () => { + const { root, fetcher } = await fixture() + fetcher.mockResolvedValue(new Response(null, { status: 503 })) + await expect(prepareRuntime(new Context(), Config({ dataRoot: root }), new AbortController().signal)).rejects.toMatchObject({ + download: { source: 'https://hf-mirror.com', reason: 'http', status: 503 }, + }) + expect(fetcher.mock.calls.filter(([, init]) => init?.method !== 'HEAD')).toHaveLength(2) +}) diff --git a/packages/experimental/speech-to-text-sensevoice/tests/model-sources.spec.ts b/packages/experimental/speech-to-text-sensevoice/tests/model-sources.spec.ts new file mode 100644 index 0000000000..43cc295d8c --- /dev/null +++ b/packages/experimental/speech-to-text-sensevoice/tests/model-sources.spec.ts @@ -0,0 +1,92 @@ +/** Source races are controlled by response barriers and share preparation cancellation. */ +import { afterEach, expect, it, onTestFinished, vi } from 'vitest' +import { orderModelSources } from '../src/model-sources.ts' + +const origins = ['https://huggingface.co', 'https://hf-mirror.com'] as const +const path = '/owner/model/resolve/pinned/model.onnx' +afterEach(() => { vi.unstubAllGlobals(); vi.useRealTimers() }) + +function race() { + const requests = new Map> & { signal: AbortSignal }>() + const fetcher = vi.fn((url: string, init: RequestInit) => { + const signal = init.signal! + const pending = Promise.withResolvers() + const abort = () => { pending.reject(signal.reason) } + signal.addEventListener('abort', abort, { once: true }) + requests.set(url, { ...pending, signal }) + return pending.promise.finally(() => { signal.removeEventListener('abort', abort) }) + }) + vi.stubGlobal('fetch', fetcher) + const cancel = new AbortController() + const result = orderModelSources(origins[0] + path, origins, 50, cancel.signal) + onTestFinished(async () => { cancel.abort(); await result.catch(() => {}) }) + const request = (origin: string) => requests.get(origin + path)! + return { request, fetcher, cancel, result } +} + +it.each(origins)('prefers %s after its first successful response and joins the other request', async (winner) => { + vi.useFakeTimers() + const { request, fetcher, result } = race() + expect(fetcher).toHaveBeenCalledTimes(2) + expect(fetcher).toHaveBeenCalledWith(winner + path, expect.objectContaining({ method: 'HEAD' })) + request(winner).resolve(new Response(null)) + expect(await result).toEqual([winner + path, ...origins.filter(origin => origin !== winner).map(origin => origin + path)]) + expect(origins.every(origin => request(origin).signal.aborted)).toBe(true) + expect(vi.getTimerCount()).toBe(0) +}) + +it('ignores a faster HTTP error and releases any returned bodies', async () => { + const { request, result } = race() + const cancel = vi.fn() + request(origins[0]).resolve(new Response(new ReadableStream({ cancel }), { status: 503 })) + request(origins[1]).resolve(new Response(null)) + expect(await result).toEqual([origins[1] + path, origins[0] + path]) + expect(cancel).toHaveBeenCalledOnce() +}) + +it('preserves configured order if every probe fails', async () => { + const { request, result } = race() + request(origins[0]).reject(new TypeError('offline')) + request(origins[1]).resolve(new Response(null, { status: 405 })) + expect(await result).toEqual(origins.map(origin => origin + path)) +}) + +it('bounds probes without cancelling the following download', async () => { + vi.useFakeTimers() + const { request, result, cancel } = race() + await vi.advanceTimersByTimeAsync(50) + expect(await result).toEqual(origins.map(origin => origin + path)) + expect(origins.every(origin => request(origin).signal.aborted)).toBe(true) + expect(cancel.signal.aborted).toBe(false) + expect(vi.getTimerCount()).toBe(0) +}) + +it('rejects preparation cancellation after joining both probes', async () => { + const { request, result, cancel } = race() + const reason = new Error('cancelled') + const rejected = expect(result).rejects.toBe(reason) + cancel.abort(reason) + await rejected + expect(origins.every(origin => request(origin).signal.aborted)).toBe(true) +}) + +it('uses a single deduplicated origin without probing and rejects already cancelled preparation', async () => { + const fetcher = vi.fn(); vi.stubGlobal('fetch', fetcher) + expect(await orderModelSources(origins[0] + path, ['https://private.example', 'https://private.example/'], 50, + new AbortController().signal)).toEqual(['https://private.example' + path]) + await expect(orderModelSources(origins[0] + path, origins, 50, AbortSignal.abort(new Error('cancelled')))).rejects.toThrow('cancelled') + expect(fetcher).not.toHaveBeenCalled() +}) + +it('waits for body cleanup without changing the winning source', async () => { + const { request, result } = race() + const cleanup = Promise.withResolvers(), cancelling = Promise.withResolvers(), settled = vi.fn() + void result.then(settled) + request(origins[1]).resolve(new Response(new ReadableStream({ cancel() { cancelling.resolve(undefined); return cleanup.promise } }))) + try { + await cancelling.promise + expect(settled).not.toHaveBeenCalled() + cleanup.resolve(undefined) + expect(await result).toEqual([origins[1] + path, origins[0] + path]) + } finally { cleanup.resolve(undefined); await result } +}) diff --git a/packages/experimental/speech-to-text-sensevoice/tests/provider.spec.ts b/packages/experimental/speech-to-text-sensevoice/tests/provider.spec.ts index a2fef1fd18..ed05490aa2 100644 --- a/packages/experimental/speech-to-text-sensevoice/tests/provider.spec.ts +++ b/packages/experimental/speech-to-text-sensevoice/tests/provider.spec.ts @@ -14,7 +14,9 @@ it.each(['int8', 'fp32'] as const)('registers %s lazily and joins inference befo await ctx.plugin(LocalSubprocess) await ctx.plugin(SpeechToText, { defaultProvider: 'sensevoice-local', language: 'auto' }) const transcribe = vi.spyOn(SenseVoiceWorker.prototype, 'transcribe') - const fiber = ctx.plugin(Provider, { dataRoot: process.cwd(), precision }) + const fiber = ctx.plugin(Provider, { dataRoot: process.cwd(), precision, + ...precision === 'fp32' ? { modelOrigin: 'https://private.example' } : {}, + }) await fiber expect(transcribe).not.toHaveBeenCalled() const speech = ctx.get('speechToText')! @@ -38,3 +40,10 @@ it('refuses relative runtime, model and cache paths before registration', () => } expect(() => Provider.Config({ dataRoot: process.cwd(), threads: 0 })).toThrow() }) + +it.each([ + { modelOrigins: [] }, { modelOrigins: ['https://example.com/path'] }, { modelOrigins: ['https://user:secret@example.com'] }, + { modelOrigin: 'file:///models' }, { modelProbeTimeoutMs: 0 }, { modelProbeTimeoutMs: 2_147_483_648 }, +])('rejects invalid download source configuration %j', (config) => { + expect(() => Provider.Config({ dataRoot: process.cwd(), ...config })).toThrow() +}) diff --git a/packages/experimental/speech-to-text-sensevoice/tests/worker.spec.ts b/packages/experimental/speech-to-text-sensevoice/tests/worker.spec.ts index 48313474d6..5ee007e489 100644 --- a/packages/experimental/speech-to-text-sensevoice/tests/worker.spec.ts +++ b/packages/experimental/speech-to-text-sensevoice/tests/worker.spec.ts @@ -63,6 +63,41 @@ it('keeps one worker warm across recordings and joins its exit on disposal', asy await expect(worker.transcribe({ audio, language: 'en' }, signal())).rejects.toThrow('disposed') }) +it('pins a manual source to one preparation without changing deployment defaults', async () => { + const { worker } = await fixture() + expect(worker.downloadSources).toEqual(['https://huggingface.co', 'https://hf-mirror.com']) + worker.prepare({ downloadSource: 'https://hf-mirror.com' }) + await vi.waitFor(() => { expect(worker.snapshot().phase).toBe('ready') }) + expect(prepareRuntime).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ modelOrigin: 'https://hf-mirror.com' }), + expect.any(AbortSignal), expect.any(Function)) +}) + +it('rejects unadvertised sources and preserves private and offline deployments', async () => { + const { worker } = await fixture({ modelOrigin: 'https://private.example/' }) + expect(worker.downloadSources).toEqual(['https://private.example']) + expect(() => { worker.prepare({ downloadSource: 'https://hf-mirror.com' }) }).toThrow('unavailable') + expect(prepareRuntime).not.toHaveBeenCalled() + const offline = await fixture({ modelDirectory: process.cwd(), vadModelPath: process.cwd() }) + expect(offline.worker.downloadSources).toEqual([]) + expect(() => { offline.worker.prepare({ downloadSource: 'https://huggingface.co' }) }).toThrow('unavailable') +}) + +it('joins matching requests and refuses to change the source of active preparation', async () => { + const { worker } = await fixture(), entered = Promise.withResolvers() + vi.mocked(prepareRuntime).mockImplementation(async (_ctx, _config, signal) => { + entered.resolve(undefined) + await new Promise((_resolve, reject) => { signal.addEventListener('abort', () => { reject(new Error('preparation cancelled')) }, { once: true }) }) + throw new Error('unreachable') + }) + worker.prepare({ downloadSource: 'https://hf-mirror.com' }) + await entered.promise + worker.prepare({ downloadSource: 'https://hf-mirror.com' }) + expect(() => { worker.prepare({ downloadSource: 'https://huggingface.co' }) }).toThrow('Cancel preparation') + expect(prepareRuntime).toHaveBeenCalledOnce() + await worker.cancel() + expect(worker.snapshot().phase).toBe('cancelled') +}) + it('wakes verified caches on the first recording without preparing them again', async () => { const { worker, spawn, root } = await fixture() const paths = { tokens: root, model: root, vad: root, worker: fileURLToPath(new URL('./worker.fixture.mjs', import.meta.url)) } diff --git a/packages/experimental/speech-to-text/README.i18n.yaml b/packages/experimental/speech-to-text/README.i18n.yaml index 855300b8f1..a6fbe06934 100644 --- a/packages/experimental/speech-to-text/README.i18n.yaml +++ b/packages/experimental/speech-to-text/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/experimental/speech-to-text/README.md -README.md: 3dafb5c7cfb9c3946be329e6e67b876ddebfb322 -README.zh.md: d665e5c05f7c220ecd838108b97ae886af7d952a +README.md: e96d3c09d8bea6a2ae7f19fd3ef195efbc8d6214 +README.zh.md: 23a6a4850a540ecaafbd1a4ca6380caed32f8b91 diff --git a/packages/experimental/speech-to-text/README.md b/packages/experimental/speech-to-text/README.md index 3dafb5c7cf..e96d3c09d8 100644 --- a/packages/experimental/speech-to-text/README.md +++ b/packages/experimental/speech-to-text/README.md @@ -27,6 +27,8 @@ This Service Definition selects named speech recognizers through `ctx.speechToTe Load through the [voice input bundle](../voice-input-bundle/README.md), or compose the service with a provider and consumer. `defaultProvider` is required and selects an exact registered id; the bundle supplies `sensevoice-local`. `language` supplies the omitted language hint. A missing or duplicate provider fails explicitly. +Providers advertise preparation origins through `downloadSources`. `prepare(id, options)` forwards an optional `downloadSource` for one task; the provider validates the choice and refuses source changes during active preparation. Omission retains provider policy. Source choices are not persisted recognition preferences. + ----- diff --git a/packages/experimental/speech-to-text/README.zh.md b/packages/experimental/speech-to-text/README.zh.md index d665e5c05f..23a6a4850a 100644 --- a/packages/experimental/speech-to-text/README.zh.md +++ b/packages/experimental/speech-to-text/README.zh.md @@ -27,6 +27,8 @@ kind: "package-reference" 通过[语音输入 Bundle](../voice-input-bundle/README.zh.md)加载,或组合此服务、Provider 与消费者。`defaultProvider` 为必填项,选择确切的注册 id;Bundle 提供 `sensevoice-local`。`language` 提供省略的语言提示。缺失或重复的 Provider 会明确报错。 +提供方通过 `downloadSources` 公布准备阶段可用的源。`prepare(id, options)` 为单次任务转发可选的 `downloadSource`;提供方校验选择,并拒绝在准备期间改源。省略时沿用提供方策略。下载源选择不属于持久化的识别偏好。 + ----- diff --git a/packages/experimental/speech-to-text/package.json b/packages/experimental/speech-to-text/package.json index 93a5dddd54..a27c6d5f9b 100644 --- a/packages/experimental/speech-to-text/package.json +++ b/packages/experimental/speech-to-text/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-speech-to-text", "description": "Experimental speech recognition with independently selectable providers", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/speech-to-text/src/index.ts b/packages/experimental/speech-to-text/src/index.ts index ba4bb4fc9a..d88bf7aa39 100644 --- a/packages/experimental/speech-to-text/src/index.ts +++ b/packages/experimental/speech-to-text/src/index.ts @@ -5,7 +5,7 @@ import z from '@deepseek-ai/schemastery' // entry and `loader/volatile-update` merges. import type {} from '@deepseek-ai/dsh-settings' import type {} from '@deepseek-ai/cordis-plugin-loader' -import type { SpeechProvider, SpeechProviderId, SpeechProviderInfo, SpeechSnapshot, SpeechSelectionPatch, SpeechRequest, SpeechSpec, Transcript } from './types.ts' +import type { SpeechPreparationOptions, SpeechProvider, SpeechProviderId, SpeechProviderInfo, SpeechSnapshot, SpeechSelectionPatch, SpeechRequest, SpeechSpec, Transcript } from './types.ts' export type * from './types.ts' @@ -153,11 +153,12 @@ export default class SpeechToText extends Service { /** * Start or join provider-owned preparation. * @param id - exact registered provider identity. + * @param options - task-local source selection validated by the provider. */ - prepare(id: SpeechProviderId): void { + prepare(id: SpeechProviderId, options?: SpeechPreparationOptions): void { const registration = this.providers.get(id) if (!registration) throw new Error(`Speech provider is unavailable: ${id}`) - registration.provider.preparation?.prepare() + registration.provider.preparation?.prepare(options) } /** diff --git a/packages/experimental/speech-to-text/src/types.ts b/packages/experimental/speech-to-text/src/types.ts index a7f885fd69..1500aabe3e 100644 --- a/packages/experimental/speech-to-text/src/types.ts +++ b/packages/experimental/speech-to-text/src/types.ts @@ -20,6 +20,8 @@ export interface SpeechProviderInfo { /** Accepted language hints, including automatic detection when supported. */ readonly languages: readonly string[] readonly setupEstimate?: SpeechSetupEstimate + /** Origins offered for an explicit preparation download; omitted or empty when no choice applies. */ + readonly downloadSources?: readonly string[] } /** Ordered resource-preparation operations understood by the speech UI. Providers omit operations they do not need. */ @@ -75,14 +77,23 @@ export interface SpeechSnapshot { readonly selection: SpeechSelection } +/** Download choice for one preparation task; omission keeps the provider's configured selection policy. */ +export interface SpeechPreparationOptions { + /** One advertised origin; providers reject unavailable sources and use a manual choice without fallback. */ + readonly downloadSource?: string +} + /** Optional resource preparation controls. Providers without them are immediately usable. */ export interface SpeechPreparation { /** @returns the latest Host-owned state. */ snapshot(): SpeechPreparationState /** @param listener - state invalidation callback. @returns subscription disposer. */ subscribe(listener: () => void): () => void - /** Start or join the current preparation task; page and transport lifetimes do not own it. */ - prepare(): void + /** + * Start or join the current preparation task; page and transport lifetimes do not own it. + * @param options - task-local download selection; providers reject changing the source of active work. + */ + prepare(options?: SpeechPreparationOptions): void /** Cancel preparation. @returns after its resources settle. */ cancel(): Promise } diff --git a/packages/experimental/speech-to-text/tests/service.spec.ts b/packages/experimental/speech-to-text/tests/service.spec.ts index 60dfe50bd1..bf09c0af5f 100644 --- a/packages/experimental/speech-to-text/tests/service.spec.ts +++ b/packages/experimental/speech-to-text/tests/service.spec.ts @@ -35,6 +35,8 @@ it('shares Host preparation across observers and does not cancel work when obser expect((await stream.next()).value).toMatchObject({ providers: [{ id, preparation: { phase: 'unprepared' } }] }) service.prepare(id) expect(prepare).toHaveBeenCalledOnce() + service.prepare(id, { downloadSource: 'https://mirror.example' }) + expect(prepare).toHaveBeenLastCalledWith({ downloadSource: 'https://mirror.example' }) const next = stream.next() state = { phase: 'downloading', resource: 'model', completedBytes: 638, totalBytes: 936 }; notify() expect((await next).value).toMatchObject({ providers: [{ preparation: state }] }) diff --git a/packages/experimental/tool-agent-team/package.json b/packages/experimental/tool-agent-team/package.json index de51f63edb..fe2455915a 100644 --- a/packages/experimental/tool-agent-team/package.json +++ b/packages/experimental/tool-agent-team/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-tool-agent-team", "description": "Scoped model-facing Agent Teams tools over ctx.agentTeams", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/voice-input-bundle/package.json b/packages/experimental/voice-input-bundle/package.json index e84a1589e8..b228379ab3 100644 --- a/packages/experimental/voice-input-bundle/package.json +++ b/packages/experimental/voice-input-bundle/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-voice-input-bundle", "description": "Experimental voice input with local SenseVoice; downloads its runtime on first use", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "icon": "./icon.svg", "publishConfig": { "access": "public" diff --git a/packages/experimental/webworker-packer/package.json b/packages/experimental/webworker-packer/package.json index 3485fd6d61..952cbb8a9b 100644 --- a/packages/experimental/webworker-packer/package.json +++ b/packages/experimental/webworker-packer/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-webworker-packer", "description": "Build-time packer for the browser runtime's base VFS image and ordered data-overlay archives", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/webworker-runtime/package.json b/packages/experimental/webworker-runtime/package.json index f535fa1a06..9328a73218 100644 --- a/packages/experimental/webworker-runtime/package.json +++ b/packages/experimental/webworker-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-webworker-runtime", "description": "Browser-only harness runtime: in-memory VFS, module transform and loader, postMessage tunnel, and the dedicated Web Worker assembly, with the Node-compatibility layer that lets the host tree run unchanged", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/extensions/cordis-client-runner/package.json b/packages/extensions/cordis-client-runner/package.json index 24e98d9b8b..939e310bf0 100644 --- a/packages/extensions/cordis-client-runner/package.json +++ b/packages/extensions/cordis-client-runner/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-cordis-client-runner", "description": "Browser half of dynamic dual-half plugin packages: event subscription, closure evaluation, guard facade, and loader entries", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/extensions/cordis-host-runner/package.json b/packages/extensions/cordis-host-runner/package.json index 75935f2799..167baa0017 100644 --- a/packages/extensions/cordis-host-runner/package.json +++ b/packages/extensions/cordis-host-runner/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-cordis-host-runner", "description": "Dynamic package definition registry, host-half sandbox lifecycle, and invoke handler table for model-mounted dual-half packages", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/extensions/tool-cordis/package.json b/packages/extensions/tool-cordis/package.json index 7691ec1e29..3a09adac07 100644 --- a/packages/extensions/tool-cordis/package.json +++ b/packages/extensions/tool-cordis/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-cordis", "description": "Read-only runtime API inspection for Harness plugin development", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index 4bf22eef0d..726f3b9f74 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -376,12 +376,6 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ parameters: [{ name: 'agent', description: 'candidate exact live Agent.' }], returns: 'Team membership, or undefined for non-Team subagents and stale identities.', }, - { - signature: '@Remote(\'view\') remoteView(agent: Agent): TeamView', - description: 'Read the current roster and non-deleted task board through the generated Remote API.', - parameters: [{ name: 'agent', description: 'exact live Team member used as the authority credential.' }], - returns: 'detached current roster and task views.', - }, ], }, { @@ -1629,7 +1623,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ }, { signature: '@Remote installBundle(spec: string, options?: InstallBundleOptions): Promise', - description: 'Install a package using the same pnpm implementation as dsh plugin. A run that fails, is cancelled, or adds a package without a bundle patch restores `package.json` and `pnpm-lock.yaml` as they were; downloaded files can stay.', + description: 'Install a package using the same pnpm implementation as dsh plugin. GitHub repositories get a connection check bounded by githubConnectionTimeoutMs before pnpm starts; only network failures or timeouts stop installation, while pnpm owns authentication and transport fallback. A run that fails, is cancelled, or adds a package without a bundle patch restores `package.json` and `pnpm-lock.yaml` as they were; downloaded files can stay.', parameters: [{ name: 'spec', description: 'One package spec, including local paths relative to the invocation directory.' }, { name: 'options', description: 'Whether to activate the installed bundle (defaults to true), the request id a cancellation names, the pending build scripts to allow for this profile before pnpm runs, and the registry asked first.' }], returns: 'Package-manager diagnostics, the registries asked, and the observed activation outcome.', }, @@ -1643,7 +1637,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ signature: '@Remote async cancelInstall(requestId: PluginInstallRequestId): Promise', description: 'Stop an installation this manager owns and wait until its files are back.', parameters: [{ name: 'requestId', description: 'The id the installation was started with.' }], - returns: '`cancelled` once pnpm exited and the files are restored, `too-late` once the bundle is being applied, `not-running` for any other id.', + returns: '`cancelled` once the Git check or pnpm exited and the files are restored, `too-late` once the bundle is being applied, `not-running` for any other id.', }, { signature: '@Remote removeBundle(name: string): Promise', @@ -2549,9 +2543,9 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ returns: 'after preferences are saved.', }, { - signature: '@Remote prepare(providerId: SpeechProviderId): void', + signature: '@Remote prepare(providerId: SpeechProviderId, options?: SpeechPreparationOptions): void', description: 'Start or join one Host-owned preparation task.', - parameters: [{ name: 'providerId', description: 'selected recognizer.' }], + parameters: [{ name: 'providerId', description: 'selected recognizer.' }, { name: 'options', description: 'task-local source selection validated by the provider.' }], }, { signature: '@Remote cancelPreparation(providerId: SpeechProviderId): Promise', @@ -2603,9 +2597,9 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ returns: 'after the profile write and the live update it applies.', }, { - signature: 'prepare(id: SpeechProviderId): void', + signature: 'prepare(id: SpeechProviderId, options?: SpeechPreparationOptions): void', description: 'Start or join provider-owned preparation.', - parameters: [{ name: 'id', description: 'exact registered provider identity.' }], + parameters: [{ name: 'id', description: 'exact registered provider identity.' }, { name: 'options', description: 'task-local source selection validated by the provider.' }], }, { signature: 'async cancelPreparation(id: SpeechProviderId): Promise', @@ -6735,7 +6729,11 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'SpeechPreparation', - declaration: 'export interface SpeechPreparation {\n snapshot(): SpeechPreparationState;\n subscribe(listener: () => void): () => void;\n prepare(): void;\n cancel(): Promise;\n}', + declaration: 'export interface SpeechPreparation {\n snapshot(): SpeechPreparationState;\n subscribe(listener: () => void): () => void;\n prepare(options?: SpeechPreparationOptions): void;\n cancel(): Promise;\n}', + }, + { + name: 'SpeechPreparationOptions', + declaration: 'export interface SpeechPreparationOptions {\n readonly downloadSource?: string;\n}', }, { name: 'SpeechPreparationState', @@ -6759,7 +6757,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'SpeechProviderInfo', - declaration: 'export interface SpeechProviderInfo {\n readonly id: SpeechProviderId;\n readonly name: string;\n readonly location: \'host-local\' | \'cloud\';\n readonly languages: readonly string[];\n readonly setupEstimate?: SpeechSetupEstimate;\n}', + declaration: 'export interface SpeechProviderInfo {\n readonly id: SpeechProviderId;\n readonly name: string;\n readonly location: \'host-local\' | \'cloud\';\n readonly languages: readonly string[];\n readonly setupEstimate?: SpeechSetupEstimate;\n readonly downloadSources?: readonly string[];\n}', }, { name: 'SpeechProviderView', @@ -7049,10 +7047,6 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'TeamTaskView', declaration: 'export interface TeamTaskView {\n readonly id: TeamTaskId;\n readonly revision: number;\n readonly subject: string;\n readonly description: string;\n readonly status: TeamTaskStatus;\n readonly blockedBy: TeamTaskId[];\n readonly writeScopes: string[];\n readonly ownerName?: string;\n readonly ready: boolean;\n readonly writeScopeWarnings: string[];\n}', }, - { - name: 'TeamView', - declaration: 'export interface TeamView {\n readonly members: TeamMemberView[];\n readonly tasks: TeamTaskView[];\n}', - }, { name: 'TeamWaitResult', declaration: 'export interface TeamWaitResult {\n readonly timedOut: boolean;\n}', diff --git a/packages/extensions/ui-cordis/package.json b/packages/extensions/ui-cordis/package.json index ab85f1e314..4ecb8b6c7e 100644 --- a/packages/extensions/ui-cordis/package.json +++ b/packages/extensions/ui-cordis/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-cordis", "description": "Cordis dynamic-plugin definition card: the keyed cordis_define tool row with its run/stop switch", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/feedback/command-feedback/package.json b/packages/feedback/command-feedback/package.json index 75107af4aa..c05ba51ed8 100644 --- a/packages/feedback/command-feedback/package.json +++ b/packages/feedback/command-feedback/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-command-feedback", "description": "Log-only session feedback: the record event, the sessionFeedback Host Remote, and the human-facing slash command", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/feedback/message-feedback/package.json b/packages/feedback/message-feedback/package.json index b235d59266..f322034362 100644 --- a/packages/feedback/message-feedback/package.json +++ b/packages/feedback/message-feedback/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-message-feedback", "description": "Canonical Session-log ratings and notes for finalized assistant messages", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/fs/fs-local/package.json b/packages/fs/fs-local/package.json index 43ff5238f7..d7457b3a54 100644 --- a/packages/fs/fs-local/package.json +++ b/packages/fs/fs-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-fs-local", "description": "Local-filesystem implementation of the DeepSeek Harness filesystem seam (ctx.fs)", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/fs/fs-observation-policy/package.json b/packages/fs/fs-observation-policy/package.json index 3fda7e35f7..208db70821 100644 --- a/packages/fs/fs-observation-policy/package.json +++ b/packages/fs/fs-observation-policy/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-fs-observation-policy", "description": "File-context policy plugin for the DeepSeek Harness — observed-state, read-before-edit, and version-guarded write/edit added over the ctx.fs provider seam through the fs/* event gate (no service API)", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/fs/fs-sandbox/package.json b/packages/fs/fs-sandbox/package.json index a0c9d6c4e4..fde7ebbc67 100644 --- a/packages/fs/fs-sandbox/package.json +++ b/packages/fs/fs-sandbox/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-fs-sandbox", "description": "Sandbox-enforcing implementation of the DeepSeek Harness filesystem seam: fences write/edit by the per-call sandbox mode (read-only denies mutation, workspace-write contains it to the workspace + temp roots) while reads pass through", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/fs/fs/package.json b/packages/fs/fs/package.json index 0689f950e5..edad0a0c7e 100644 --- a/packages/fs/fs/package.json +++ b/packages/fs/fs/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-fs", "description": "Abstract filesystem capability seam (ctx.fs) for the DeepSeek Harness — vocabulary types, the FileSystem service (text IO + optional version-guarded atomic mutations), and the fs/* policy event vocabulary", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/fs/tool-fs-search/package.json b/packages/fs/tool-fs-search/package.json index dc2dd213fa..706af4f1c2 100644 --- a/packages/fs/tool-fs-search/package.json +++ b/packages/fs/tool-fs-search/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-fs-search", "description": "Model-facing filesystem discovery tools (glob, grep) backed by the packaged ripgrep binary (@vscode/ripgrep)", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/fs/tool-fs/package.json b/packages/fs/tool-fs/package.json index b5d0973f01..bce844f3ee 100644 --- a/packages/fs/tool-fs/package.json +++ b/packages/fs/tool-fs/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-fs", "description": "Model-facing filesystem tools (read, write, edit) over the DeepSeek Harness filesystem seam (ctx.fs)", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/fs/tool-str-replace-editor/package.json b/packages/fs/tool-str-replace-editor/package.json index 133124e89a..bcd55926be 100644 --- a/packages/fs/tool-str-replace-editor/package.json +++ b/packages/fs/tool-str-replace-editor/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-str-replace-editor", "description": "Model-facing view, create, literal replace, and line insert tool over the Harness filesystem service", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/goal/command-goal/package.json b/packages/goal/command-goal/package.json index 7af99b23d8..f721307c43 100644 --- a/packages/goal/command-goal/package.json +++ b/packages/goal/command-goal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-command-goal", "description": "Human-facing slash command for persisted same-session goals", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/goal/goal-round-driver/package.json b/packages/goal/goal-round-driver/package.json index a229bfdc4a..52507494b8 100644 --- a/packages/goal/goal-round-driver/package.json +++ b/packages/goal/goal-round-driver/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-goal-round-driver", "description": "Race-fenced same-session goal-round driver", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/goal/goal/package.json b/packages/goal/goal/package.json index 4d3ad76314..6426b2245b 100644 --- a/packages/goal/goal/package.json +++ b/packages/goal/goal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-goal", "description": "Event-sourced same-session goal state and lifecycle service for the DeepSeek Harness", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/goal/tool-goal/package.json b/packages/goal/tool-goal/package.json index 64f4be5c5d..bf2afbd7b6 100644 --- a/packages/goal/tool-goal/package.json +++ b/packages/goal/tool-goal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-goal", "description": "Model-facing same-session goal tools with execution-time authority checks", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/guard/repeat-tool-reminder/package.json b/packages/guard/repeat-tool-reminder/package.json index 8f60013995..44ea2e583d 100644 --- a/packages/guard/repeat-tool-reminder/package.json +++ b/packages/guard/repeat-tool-reminder/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-repeat-tool-reminder", "description": "Repeat-tool-call guard plugin: advisory reminders when an agent loops on identical tool calls", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/guard/timeout-policy/package.json b/packages/guard/timeout-policy/package.json index 50ddad5f5f..dca83b9971 100644 --- a/packages/guard/timeout-policy/package.json +++ b/packages/guard/timeout-policy/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-call-timeout-policy", "description": "Tool-call timeout policy: a tools/execute wrapper that arms a per-tool deadline on exec.signal and returns TOOL_TIMEOUT when it wins", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/hooks/hook-protocol/package.json b/packages/hooks/hook-protocol/package.json index 60793f3889..695fe9b0c3 100644 --- a/packages/hooks/hook-protocol/package.json +++ b/packages/hooks/hook-protocol/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-hook-protocol", "description": "Shared Claude Code / Codex hook wire protocol: matcher engine, stdin/exit-code/stdout codec, multi-hook merge, and hook/* session events", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/hooks/hooks-claude-code/package.json b/packages/hooks/hooks-claude-code/package.json index f95f4b58e8..3a23f57137 100644 --- a/packages/hooks/hooks-claude-code/package.json +++ b/packages/hooks/hooks-claude-code/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-hooks-claude-code", "description": "Bridge plugin: run a Claude Code hooks.json / settings hook config on the DeepSeek Harness interception seams", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/hooks/hooks-codex/package.json b/packages/hooks/hooks-codex/package.json index aab639e436..d3bfcddd5b 100644 --- a/packages/hooks/hooks-codex/package.json +++ b/packages/hooks/hooks-codex/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-hooks-codex", "description": "Bridge plugin: run a Codex hooks.json hook config on the DeepSeek Harness interception seams", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/host/directory-picker-auto/package.json b/packages/host/directory-picker-auto/package.json index 96a2b18e35..b085694711 100644 --- a/packages/host/directory-picker-auto/package.json +++ b/packages/host/directory-picker-auto/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-directory-picker-auto", "description": "Adaptive chooser of the directory-picker seam: resolves the host situation at boot and mounts the native or browse backend for the DeepSeek Harness web GUI host", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/host/directory-picker-browse/package.json b/packages/host/directory-picker-browse/package.json index 47f10696d0..96c4835118 100644 --- a/packages/host/directory-picker-browse/package.json +++ b/packages/host/directory-picker-browse/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-directory-picker-browse", "description": "In-app browsing backend of the directory-picker seam (listing/creation primitives over the host filesystem)", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/host/directory-picker-native/package.json b/packages/host/directory-picker-native/package.json index 875af145c6..bb60ec57cd 100644 --- a/packages/host/directory-picker-native/package.json +++ b/packages/host/directory-picker-native/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-directory-picker-native", "description": "Native-OS-chooser backend of the directory-picker seam for the DeepSeek Harness web GUI host", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/host/directory-picker/package.json b/packages/host/directory-picker/package.json index b7b77204ce..a6cf71a9dc 100644 --- a/packages/host/directory-picker/package.json +++ b/packages/host/directory-picker/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-directory-picker", "description": "Abstract workspace-directory picking seam (ctx.directoryPicker) for the DeepSeek Harness web GUI host", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/host/frontend-static/package.json b/packages/host/frontend-static/package.json index 20654e868c..edc585600e 100644 --- a/packages/host/frontend-static/package.json +++ b/packages/host/frontend-static/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-frontend-static", "description": "SPA dist server for the Web shell: owns the webserver fallback seat, serving explicit index entries and static assets with traversal rejection and 404 misses", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/host/open-in-app/package.json b/packages/host/open-in-app/package.json index 6473170e4b..89b3a4ab6d 100644 --- a/packages/host/open-in-app/package.json +++ b/packages/host/open-in-app/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-open-in-app", "description": "Host half of open-in-app: resolved application catalog, icons, and the launch endpoint as three webServer routes", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/host/plugin-inventory/package.json b/packages/host/plugin-inventory/package.json index 136a9eab59..1a8befbb71 100644 --- a/packages/host/plugin-inventory/package.json +++ b/packages/host/plugin-inventory/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-plugin-inventory", "description": "Read-only Remote projection of current Cordis Loader plugin state", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/host/product-telemetry-otel/package.json b/packages/host/product-telemetry-otel/package.json index a480455594..621f4aa930 100644 --- a/packages/host/product-telemetry-otel/package.json +++ b/packages/host/product-telemetry-otel/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-product-telemetry-otel", "description": "Explicit product usage events exported through OpenTelemetry HTTP logs", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/host/webserver/package.json b/packages/host/webserver/package.json index 9df6eaa1e3..faeb2ff4f7 100644 --- a/packages/host/webserver/package.json +++ b/packages/host/webserver/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-webserver", "description": "Web route-registration plugin: HTTP and upgrade routes, index transform taps, and static dist fallback; knows no harness concepts", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/identity/anonymous-user-id/package.json b/packages/identity/anonymous-user-id/package.json index 457c4c470e..eff50c0cdb 100644 --- a/packages/identity/anonymous-user-id/package.json +++ b/packages/identity/anonymous-user-id/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-anonymous-user-id", "description": "Shared anonymous user identity for DeepSeek Harness telemetry and feedback correlation", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/interaction/commands/package.json b/packages/interaction/commands/package.json index d92c2189fb..e317c58444 100644 --- a/packages/interaction/commands/package.json +++ b/packages/interaction/commands/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-commands", "description": "Plugin-owned human command registry for DeepSeek Harness UIs", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/interaction/permission-presets/package.json b/packages/interaction/permission-presets/package.json index fba00823fd..c5289ab201 100644 --- a/packages/interaction/permission-presets/package.json +++ b/packages/interaction/permission-presets/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-permission-presets", "description": "User-facing permission presets (ctx.permissionPresets) for the DeepSeek Harness: one product-level Permissions select bundling the sandbox-mode and approval-policy knobs, written through to their own session events", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/interaction/tool-ask-user/package.json b/packages/interaction/tool-ask-user/package.json index bebe7f81c4..1262994e62 100644 --- a/packages/interaction/tool-ask-user/package.json +++ b/packages/interaction/tool-ask-user/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-ask-user", "description": "Model-facing ask_user_question tool over the ctx.userQuestions seam", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/interaction/user-approval/package.json b/packages/interaction/user-approval/package.json index 029719c966..e154730a57 100644 --- a/packages/interaction/user-approval/package.json +++ b/packages/interaction/user-approval/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-user-approval", "description": "User-approval seam (ctx.approval) for the DeepSeek Harness: one-shot permission decisions dispatched to composed answerers over the approval/request waterfall, fail-closed by default", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/interaction/user-questions/package.json b/packages/interaction/user-questions/package.json index 02c0e6cd2e..d5c02e8825 100644 --- a/packages/interaction/user-questions/package.json +++ b/packages/interaction/user-questions/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-user-questions", "description": "Abstract user-questions seam (ctx.userQuestions) for asking the human during agent runs", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/jobs/jobs-local/package.json b/packages/jobs/jobs-local/package.json index fdbadaa52c..a96b1f1efc 100644 --- a/packages/jobs/jobs-local/package.json +++ b/packages/jobs/jobs-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-jobs-local", "description": "Process-local implementation of the DeepSeek Harness background job registry seam", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/jobs/jobs/package.json b/packages/jobs/jobs/package.json index 295dff59f5..f5e4a36603 100644 --- a/packages/jobs/jobs/package.json +++ b/packages/jobs/jobs/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-jobs", "description": "Background job registry (ctx.jobs) for the DeepSeek Harness — shared ids, owner isolation, polling, cancellation, and completion listeners for long-running tool work", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/jobs/tool-jobs/package.json b/packages/jobs/tool-jobs/package.json index 15e568fe26..4f0515959f 100644 --- a/packages/jobs/tool-jobs/package.json +++ b/packages/jobs/tool-jobs/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-jobs", "description": "Model-facing background job control tools (job_output, job_list, job_kill) over the ctx.jobs registry", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/llm/deepseek-llm-api-extensions/package.json b/packages/llm/deepseek-llm-api-extensions/package.json index f6a1a21b16..a17c6b6763 100644 --- a/packages/llm/deepseek-llm-api-extensions/package.json +++ b/packages/llm/deepseek-llm-api-extensions/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-deepseek-llm-api-extensions", "description": "Additive request-field registry for the official DeepSeek LLM API adapter", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/llm/llm-deepseek/package.json b/packages/llm/llm-deepseek/package.json index aea25af057..ff34eb4349 100644 --- a/packages/llm/llm-deepseek/package.json +++ b/packages/llm/llm-deepseek/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-llm-deepseek", "description": "DeepSeek Messages adapter", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/llm/llm-pi-ai/package.json b/packages/llm/llm-pi-ai/package.json index 83e9e1178d..d582217754 100644 --- a/packages/llm/llm-pi-ai/package.json +++ b/packages/llm/llm-pi-ai/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-llm-pi-ai", "description": "pi-ai-backed DeepSeek adapter for the DeepSeek Harness LLM seam (design-verification twin of dsh-llm-deepseek)", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/llm/llm-retry/package.json b/packages/llm/llm-retry/package.json index 041814f8d8..5f66b839e6 100644 --- a/packages/llm/llm-retry/package.json +++ b/packages/llm/llm-retry/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-llm-retry", "description": "Provider-routed LLM request retry policy for the DeepSeek Harness", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/llm/llm/package.json b/packages/llm/llm/package.json index f01f55036f..be3d84ae98 100644 --- a/packages/llm/llm/package.json +++ b/packages/llm/llm/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-llm", "description": "Provider-neutral LLM service interface for the DeepSeek Harness", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/llm/plugin-package-inventory-deepseek/package.json b/packages/llm/plugin-package-inventory-deepseek/package.json index 8b4cd60bc2..350d7599c4 100644 --- a/packages/llm/plugin-package-inventory-deepseek/package.json +++ b/packages/llm/plugin-package-inventory-deepseek/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-plugin-package-inventory-deepseek", "description": "Active Loader-backed plugin package inventory for official DeepSeek LLM API requests", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/llm/token-meter/package.json b/packages/llm/token-meter/package.json index 99fe7d05e5..3a3be58bc1 100644 --- a/packages/llm/token-meter/package.json +++ b/packages/llm/token-meter/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-token-meter", "description": "Replay-aware token measurement service (ctx.tokenMeter) for the DeepSeek Harness", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/lsp/lsp-stdio/package.json b/packages/lsp/lsp-stdio/package.json index 559825ffdf..821604f72e 100644 --- a/packages/lsp/lsp-stdio/package.json +++ b/packages/lsp/lsp-stdio/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-lsp-stdio", "description": "Generic stdio language-server provider for the DeepSeek Harness LSP capability seam (ctx.lsp) — spawns configured servers, translates JSON-RPC, and serves transient-open goToDefinition/findReferences/goToImplementation/hover queries in the host filesystem namespace", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/lsp/lsp/package.json b/packages/lsp/lsp/package.json index 0a202bb16a..d03c14ec99 100644 --- a/packages/lsp/lsp/package.json +++ b/packages/lsp/lsp/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-lsp", "description": "Abstract LSP capability seam (ctx.lsp) for the DeepSeek Harness — language-server provider registry keyed by branded id and extension mapping, order-independent per-query selection, normalized definition/references/implementation/hover requests and results, and the LspError taxonomy", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/lsp/tool-lsp/package.json b/packages/lsp/tool-lsp/package.json index 5c0c40fd05..7eec9023f4 100644 --- a/packages/lsp/tool-lsp/package.json +++ b/packages/lsp/tool-lsp/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-lsp", "description": "Model-facing lsp tool over the DeepSeek Harness LSP capability seam (ctx.lsp) — one read-only tool with goToDefinition/findReferences/goToImplementation/hover operations, one-based UTF-16 cursor coordinates, bounded location rendering, and hover normalization", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/mcp/mcp-client/package.json b/packages/mcp/mcp-client/package.json index 00f9f2deb1..2ac261cb38 100644 --- a/packages/mcp/mcp-client/package.json +++ b/packages/mcp/mcp-client/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-mcp-client", "description": "MCP client bridge: connects to MCP servers and registers their tools on ctx.tools", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/mcp/mcp-resources/package.json b/packages/mcp/mcp-resources/package.json index 3b8b682296..b3ae9d6f5c 100644 --- a/packages/mcp/mcp-resources/package.json +++ b/packages/mcp/mcp-resources/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-mcp-resources", "description": "Scoped MCP resource discovery and reading through shared model tools", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/plan/plan-mode/package.json b/packages/plan/plan-mode/package.json index 1976d9e39e..ab235817a1 100644 --- a/packages/plan/plan-mode/package.json +++ b/packages/plan/plan-mode/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-plan-mode", "description": "Logged per-agent plan mode with deployment guidance, a direct slash command, and a user-reviewed exit", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/preset/agent-preset-registry/package.json b/packages/preset/agent-preset-registry/package.json index d5f324378f..4472149061 100644 --- a/packages/preset/agent-preset-registry/package.json +++ b/packages/preset/agent-preset-registry/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent-preset-registry", "description": "Declarative Agent preset registry and profile-backed editing", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/preset/agent-preset/package.json b/packages/preset/agent-preset/package.json index 306a2808fc..2fba417ecc 100644 --- a/packages/preset/agent-preset/package.json +++ b/packages/preset/agent-preset/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent-preset", "description": "Declare an Agent capability composition in Cordis YAML", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/preset/persona/package.json b/packages/preset/persona/package.json index 2ddf6bcd2c..c20b4006bf 100644 --- a/packages/preset/persona/package.json +++ b/packages/preset/persona/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-persona", "description": "Composition-authored deployment persona section for the DeepSeek Harness", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/ptc-runtime/ptc-runtime-node/package.json b/packages/ptc-runtime/ptc-runtime-node/package.json index d729da5920..c8219b1743 100644 --- a/packages/ptc-runtime/ptc-runtime-node/package.json +++ b/packages/ptc-runtime/ptc-runtime-node/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-ptc-runtime-node", "description": "Sandboxed Node process implementation of the DeepSeek Harness PTC execution capability", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/ptc-runtime/ptc-runtime/package.json b/packages/ptc-runtime/ptc-runtime/package.json index 67b85ec6b9..40a0d8d234 100644 --- a/packages/ptc-runtime/ptc-runtime/package.json +++ b/packages/ptc-runtime/ptc-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-ptc-runtime", "description": "Abstract PTC execution seam (ctx.ptcRuntime) for the DeepSeek Harness", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/runtime-diagnostics/invariants/package.json b/packages/runtime-diagnostics/invariants/package.json index 73c9f3b99d..7a6179a606 100644 --- a/packages/runtime-diagnostics/invariants/package.json +++ b/packages/runtime-diagnostics/invariants/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-invariants", "description": "Registry service for package-owned DeepSeek Harness runtime invariants", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/sandbox/sandbox-local/package.json b/packages/sandbox/sandbox-local/package.json index a1439ff5b6..1f7cd12f59 100644 --- a/packages/sandbox/sandbox-local/package.json +++ b/packages/sandbox/sandbox-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sandbox-local", "description": "Local process-sandbox backends for the DeepSeek Harness sandbox seam: bwrap, the npm-distributed landlock-run launcher, macOS Seatbelt, or the Windows ACL restricted-token runner — functionally probed, fail-closed", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/sandbox/sandbox-policy/package.json b/packages/sandbox/sandbox-policy/package.json index 623517f836..a66942a07a 100644 --- a/packages/sandbox/sandbox-policy/package.json +++ b/packages/sandbox/sandbox-policy/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sandbox-policy", "description": "Per-call sandbox policy resolver and current model context: deployment fallbacks plus each session's mode and workspace root, shared by every enforcing capability family", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/sandbox/sandbox-windows-acl/package.json b/packages/sandbox/sandbox-windows-acl/package.json index 2d4cd72e2a..716bbc0f7d 100644 --- a/packages/sandbox/sandbox-windows-acl/package.json +++ b/packages/sandbox/sandbox-windows-acl/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sandbox-windows-acl", "description": "Windows ACL write-restriction sandbox backend (restricted-token spawn with capability-SID write allowlist) for the DeepSeek Harness sandbox seam", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/sandbox/sandbox/package.json b/packages/sandbox/sandbox/package.json index 00ab1ce45e..b07bfc9b18 100644 --- a/packages/sandbox/sandbox/package.json +++ b/packages/sandbox/sandbox/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sandbox", "description": "Abstract process-sandbox seam (ctx.sandbox) for the DeepSeek Harness: same-world confinement vocabulary and the SandboxProvider contract", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/schedule/schedule/package.json b/packages/schedule/schedule/package.json index 493dc1e0c2..d729668e9c 100644 --- a/packages/schedule/schedule/package.json +++ b/packages/schedule/schedule/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-schedule", "description": "Agent-scoped durable after, at, and fixed-rate reminders over the session event log", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/sdk/client/package.json b/packages/sdk/client/package.json index a79827058d..432d7420d0 100644 --- a/packages/sdk/client/package.json +++ b/packages/sdk/client/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sdk-client", "description": "TypeScript client SDK for driving a DeepSeek Harness runtime subprocess over stdio JSON-RPC: the DeepSeekHarness high-level turns API and the lower-level HarnessClient", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/sdk/protocol/package.json b/packages/sdk/protocol/package.json index 3905a88fd5..e5eb68077a 100644 --- a/packages/sdk/protocol/package.json +++ b/packages/sdk/protocol/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sdk-protocol", "description": "Shared wire protocol for the DeepSeek Harness SDK runtime: the newline-delimited JSON-RPC stdio transport and the named request, result, and notification types spoken between the runtime server and SDK clients", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/sdk/server/package.json b/packages/sdk/server/package.json index cd999821ae..dd9581f36b 100644 --- a/packages/sdk/server/package.json +++ b/packages/sdk/server/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sdk-jsonrpc-server", "description": "Stdio JSON-RPC server plugin for out-of-process DeepSeek Harness SDK clients", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session-query/session-log-export/package.json b/packages/session-query/session-log-export/package.json index 8bb2af84cc..6812223661 100644 --- a/packages/session-query/session-log-export/package.json +++ b/packages/session-query/session-log-export/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-log-export", "description": "Web Session-log export command and shared download dialog", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session-query/session-query-sqlite/package.json b/packages/session-query/session-query-sqlite/package.json index 9aa5c02749..567f044369 100644 --- a/packages/session-query/session-query-sqlite/package.json +++ b/packages/session-query/session-query-sqlite/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-query-sqlite", "description": "Concrete ctx.sessionQuery backend with SQLite FTS5 search", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session-query/session-query/package.json b/packages/session-query/session-query/package.json index 032ded62a8..1efe866c2c 100644 --- a/packages/session-query/session-query/package.json +++ b/packages/session-query/session-query/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-query", "description": "Combined session query service contract with concrete reads, traces, and filters", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session-query/tool-session-query/package.json b/packages/session-query/tool-session-query/package.json index 2575f54d59..d28b8eee61 100644 --- a/packages/session-query/tool-session-query/package.json +++ b/packages/session-query/tool-session-query/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-session-query", "description": "Workspace-authorized model-facing session history search, trace, and event read tools", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-checkpoint-policy/package.json b/packages/session/session-checkpoint-policy/package.json index ef4d24ddce..377144a816 100644 --- a/packages/session/session-checkpoint-policy/package.json +++ b/packages/session/session-checkpoint-policy/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-checkpoint-policy", "description": "Semantic session durability checkpoints before model requests and tool side effects", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-format-catalog/package.json b/packages/session/session-format-catalog/package.json index 804218446e..857071b04c 100644 --- a/packages/session/session-format-catalog/package.json +++ b/packages/session/session-format-catalog/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-format-catalog", "description": "Build-static first-party Session format codec and migration catalog", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-format-v0-to-v1/package.json b/packages/session/session-format-v0-to-v1/package.json index fb80e3eda1..8b156a585b 100644 --- a/packages/session/session-format-v0-to-v1/package.json +++ b/packages/session/session-format-v0-to-v1/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-format-v0-to-v1", "description": "Frozen released-v0 Session codec and identity migration to v1", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-format-v1-to-v2/package.json b/packages/session/session-format-v1-to-v2/package.json index 6bed58e59e..bf9ed47550 100644 --- a/packages/session/session-format-v1-to-v2/package.json +++ b/packages/session/session-format-v1-to-v2/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-format-v1-to-v2", "description": "Frozen released-v1 Session codec and assistant-stream migration to v2", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-format-v2-to-v3/package.json b/packages/session/session-format-v2-to-v3/package.json index f78321bb27..03fa527a4d 100644 --- a/packages/session/session-format-v2-to-v3/package.json +++ b/packages/session/session-format-v2-to-v3/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-format-v2-to-v3", "description": "Streaming system-prompt, canonical-envelope and PTC migration into V3", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-format-v3-to-v4/package.json b/packages/session/session-format-v3-to-v4/package.json index 1cfef2bc64..f07d74f053 100644 --- a/packages/session/session-format-v3-to-v4/package.json +++ b/packages/session/session-format-v3-to-v4/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-format-v3-to-v4", "description": "Streaming tool-role migration and delivery validation from Session V3 to V4", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-format/package.json b/packages/session/session-format/package.json index 8db03f9766..4fecebb4e6 100644 --- a/packages/session/session-format/package.json +++ b/packages/session/session-format/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-format", "description": "Streaming adjacent Session format migration machinery", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-log-deepseek/package.json b/packages/session/session-log-deepseek/package.json index 111701f607..45ac212aa5 100644 --- a/packages/session/session-log-deepseek/package.json +++ b/packages/session/session-log-deepseek/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-log-deepseek", "description": "Incremental lossless session-log request extension for the official DeepSeek LLM API", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-persistence-jsonl/package.json b/packages/session/session-persistence-jsonl/package.json index cb3d7e132e..5d4ff97192 100644 --- a/packages/session/session-persistence-jsonl/package.json +++ b/packages/session/session-persistence-jsonl/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-persistence-jsonl", "description": "JSONL durable session persistence backend for the DeepSeek Harness", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-persistence/package.json b/packages/session/session-persistence/package.json index fc4e33d735..73e9e596a5 100644 --- a/packages/session/session-persistence/package.json +++ b/packages/session/session-persistence/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-persistence", "description": "Abstract durable session persistence seam (ctx.sessionPersistence) for the DeepSeek Harness", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-projection-cache/package.json b/packages/session/session-projection-cache/package.json index 90e0daa021..2d9ae15d79 100644 --- a/packages/session/session-projection-cache/package.json +++ b/packages/session/session-projection-cache/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-projection-cache", "description": "Persisted projection cache (ctx.sessionProjectionCache): durable per-session checkpoint records on the session_projcache storage domain (per-record layout), throttled write-behind, and the cached listing read", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-projection/package.json b/packages/session/session-projection/package.json index 795f3b7483..68fec6c9ec 100644 --- a/packages/session/session-projection/package.json +++ b/packages/session/session-projection/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-projection", "description": "Session-projection seam: the merge-extensible projection type table, the provider contract, and the ctx.sessionProjections registry serving whole current values of log-derived per-session state", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-stats/package.json b/packages/session/session-stats/package.json index d73bd076d9..1310119bae 100644 --- a/packages/session/session-stats/package.json +++ b/packages/session/session-stats/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-stats", "description": "Whole-log conversation counts and wall times projection (sessionStats) for the DeepSeek Harness", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-telemetry-otel/package.json b/packages/session/session-telemetry-otel/package.json index 830d0a0c8b..d422d86902 100644 --- a/packages/session/session-telemetry-otel/package.json +++ b/packages/session/session-telemetry-otel/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-telemetry-otel", "description": "OpenTelemetry backend for the DeepSeek Harness telemetry seam: hands captured session records to the OTel JS SDK's log pipeline", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-telemetry/package.json b/packages/session/session-telemetry/package.json index 7b08adb208..fcd35a2203 100644 --- a/packages/session/session-telemetry/package.json +++ b/packages/session/session-telemetry/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-telemetry", "description": "SessionTelemetryBackend seam for the DeepSeek Harness: session-event capture, projection, redaction, and handoff to a reporting backend", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-title-all-prompts-llm/package.json b/packages/session/session-title-all-prompts-llm/package.json index e8daebcad7..bca9d289ab 100644 --- a/packages/session/session-title-all-prompts-llm/package.json +++ b/packages/session/session-title-all-prompts-llm/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-title-all-prompts-llm", "description": "All-user-messages LLM provider plugin for DeepSeek Harness session titles", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-title-first-prompt-llm/package.json b/packages/session/session-title-first-prompt-llm/package.json index 4d6531b458..9867aef734 100644 --- a/packages/session/session-title-first-prompt-llm/package.json +++ b/packages/session/session-title-first-prompt-llm/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-title-first-prompt-llm", "description": "First-message LLM provider plugin for DeepSeek Harness session titles", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-title-llm/package.json b/packages/session/session-title-llm/package.json index 690ea38f8a..617fd4bd7c 100644 --- a/packages/session/session-title-llm/package.json +++ b/packages/session/session-title-llm/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-title-llm", "description": "Shared LLM generation policy for DeepSeek Harness session-title providers", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-title/package.json b/packages/session/session-title/package.json index 9923bb55d8..7b74eaf69a 100644 --- a/packages/session/session-title/package.json +++ b/packages/session/session-title/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-title", "description": "Log-backed session title service and provider registry for the DeepSeek Harness", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-turn-outline/package.json b/packages/session/session-turn-outline/package.json index 2228de992c..623265c4f8 100644 --- a/packages/session/session-turn-outline/package.json +++ b/packages/session/session-turn-outline/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-turn-outline", "description": "Whole-log turn outline projection (turnOutline) for the DeepSeek Harness", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/settings/settings/package.json b/packages/settings/settings/package.json index 6a111055f2..ab7777ba69 100644 --- a/packages/settings/settings/package.json +++ b/packages/settings/settings/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-settings", "description": "Abstract user-settings seam (ctx.settings) for the DeepSeek Harness", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/shell/bash-local/package.json b/packages/shell/bash-local/package.json index 78524471de..f0ca7a1a10 100644 --- a/packages/shell/bash-local/package.json +++ b/packages/shell/bash-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-bash-local", "description": "Local-subprocess implementation of the DeepSeek Harness bash executor seam", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/shell/bash-sandbox/package.json b/packages/shell/bash-sandbox/package.json index 9091c75a9b..fdabe47048 100644 --- a/packages/shell/bash-sandbox/package.json +++ b/packages/shell/bash-sandbox/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-bash-sandbox", "description": "Sandbox-consuming implementation of the DeepSeek Harness bash executor seam (confines every command via ctx.sandbox, reports denial/enforcement result facts)", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/shell/pwsh-local/package.json b/packages/shell/pwsh-local/package.json index b100b5255c..a7e16bde9a 100644 --- a/packages/shell/pwsh-local/package.json +++ b/packages/shell/pwsh-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-pwsh-local", "description": "Local PowerShell implementation of the DeepSeek Harness bash executor seam", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/shell/pwsh-sandbox/package.json b/packages/shell/pwsh-sandbox/package.json index 16ad2cedcf..4753cd5b59 100644 --- a/packages/shell/pwsh-sandbox/package.json +++ b/packages/shell/pwsh-sandbox/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-pwsh-sandbox", "description": "Sandbox-consuming implementation of the DeepSeek Harness PowerShell executor seam (confines every command via ctx.sandbox, reports denial/enforcement result facts)", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/shell/shell-env/package.json b/packages/shell/shell-env/package.json index 5c60401877..3956bb1fbe 100644 --- a/packages/shell/shell-env/package.json +++ b/packages/shell/shell-env/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-shell-env", "description": "Tool-independent managed DSH_* shell environment registry", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/shell/shell/package.json b/packages/shell/shell/package.json index 8856e3cb90..3ac7c3ffb6 100644 --- a/packages/shell/shell/package.json +++ b/packages/shell/shell/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-shell", "description": "Abstract bash executor seam (ctx.shell) for the DeepSeek Harness", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/shell/tool-bash-persistent/package.json b/packages/shell/tool-bash-persistent/package.json index 406fd49cae..082fd6d29b 100644 --- a/packages/shell/tool-bash-persistent/package.json +++ b/packages/shell/tool-bash-persistent/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-bash-persistent", "description": "Model-facing owner-scoped persistent Bash tool backed by the Harness PTY service", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/shell/tool-bash/package.json b/packages/shell/tool-bash/package.json index 07d2e42663..a397e3f60b 100644 --- a/packages/shell/tool-bash/package.json +++ b/packages/shell/tool-bash/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-bash", "description": "Model-facing bash tool with optional generic background-job and sandbox-escalation support", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/shell/tool-pwsh-persistent/package.json b/packages/shell/tool-pwsh-persistent/package.json index 0e48e7c21e..5758b6520b 100644 --- a/packages/shell/tool-pwsh-persistent/package.json +++ b/packages/shell/tool-pwsh-persistent/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-pwsh-persistent", "description": "Model-facing owner-scoped persistent PowerShell tool backed by the Harness PTY service", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/shell/tool-pwsh/package.json b/packages/shell/tool-pwsh/package.json index 35c799e10d..c814405b91 100644 --- a/packages/shell/tool-pwsh/package.json +++ b/packages/shell/tool-pwsh/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-pwsh", "description": "Model-facing pwsh tool over the bash executor seam", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/skill/skill-badge/package.json b/packages/skill/skill-badge/package.json index 0a76f7ca23..f7eab8c85a 100644 --- a/packages/skill/skill-badge/package.json +++ b/packages/skill/skill-badge/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-skill-badge", "description": "Bundled dsh badge skill provider for DeepSeek Harness", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/skill/skill-filesystem/package.json b/packages/skill/skill-filesystem/package.json index 7de677b050..78aa78881e 100644 --- a/packages/skill/skill-filesystem/package.json +++ b/packages/skill/skill-filesystem/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-skill-filesystem", "description": "Local filesystem skill provider for the DeepSeek Harness", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/skill/skill-office/package.json b/packages/skill/skill-office/package.json index d1db374ce5..803f1c5d1c 100644 --- a/packages/skill/skill-office/package.json +++ b/packages/skill/skill-office/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-skill-office", "description": "Bundled Word, PowerPoint, and Excel workflows and structural checks", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/skill/skill/package.json b/packages/skill/skill/package.json index ede9e67d2f..211a20fa73 100644 --- a/packages/skill/skill/package.json +++ b/packages/skill/skill/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-skill", "description": "Agent skill provider registry for the DeepSeek Harness", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/skill/tool-skill/package.json b/packages/skill/tool-skill/package.json index 12e1d688e3..49b6495630 100644 --- a/packages/skill/tool-skill/package.json +++ b/packages/skill/tool-skill/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-skill", "description": "Model-facing skill loading tool for the DeepSeek Harness", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/skill/tool-workspace-dependencies/package.json b/packages/skill/tool-workspace-dependencies/package.json index 7b8e2bdb79..019644ea31 100644 --- a/packages/skill/tool-workspace-dependencies/package.json +++ b/packages/skill/tool-workspace-dependencies/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-workspace-dependencies", "description": "The load_workspace_dependencies tool: absolute paths into a bundled Python, Node.js, and pnpm payload", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/spill/spill-local/package.json b/packages/spill/spill-local/package.json index 28eb81b63d..eb8e15b99d 100644 --- a/packages/spill/spill-local/package.json +++ b/packages/spill/spill-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-spill-local", "description": "Local-filesystem implementation of the DeepSeek Harness spill storage seam (private session-scoped files)", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/spill/spill-policy/package.json b/packages/spill/spill-policy/package.json index 4d443913ed..ecf5eb5ce6 100644 --- a/packages/spill/spill-policy/package.json +++ b/packages/spill/spill-policy/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-spill-policy", "description": "Token-budgeted tool-result retention with recoverable text and image paths", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/spill/spill/package.json b/packages/spill/spill/package.json index 806bcf9a83..3466cd5f43 100644 --- a/packages/spill/spill/package.json +++ b/packages/spill/spill/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-spill", "description": "Abstract spill storage seam (ctx.spillStore) for the DeepSeek Harness — save oversized tool text and return a retrieval locator", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/ssh/fs-ssh/package.json b/packages/ssh/fs-ssh/package.json index 7638d2b381..e3e5455a40 100644 --- a/packages/ssh/fs-ssh/package.json +++ b/packages/ssh/fs-ssh/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-fs-ssh", "description": "Filesystem provider over the shared POSIX SSH helper", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/ssh/sandbox-ssh/package.json b/packages/ssh/sandbox-ssh/package.json index 6ee7ec70eb..f61b6fbda6 100644 --- a/packages/ssh/sandbox-ssh/package.json +++ b/packages/ssh/sandbox-ssh/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sandbox-ssh", "description": "Remote POSIX sandbox argv provider over the shared SSH helper", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/ssh/ssh/package.json b/packages/ssh/ssh/package.json index 80be73316a..1cc2289b57 100644 --- a/packages/ssh/ssh/package.json +++ b/packages/ssh/ssh/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-ssh", "description": "Shared OpenSSH connection and versioned POSIX remote helper", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/ssh/subprocess-ssh/package.json b/packages/ssh/subprocess-ssh/package.json index 4561b0ed89..90e24535de 100644 --- a/packages/ssh/subprocess-ssh/package.json +++ b/packages/ssh/subprocess-ssh/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subprocess-ssh", "description": "Subprocess and terminal provider over the shared POSIX SSH helper", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/storage/storage-domain/package.json b/packages/storage/storage-domain/package.json index 2c70a4a387..a83948781d 100644 --- a/packages/storage/storage-domain/package.json +++ b/packages/storage/storage-domain/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-storage-domain", "description": "Domain data form (ctx.storage.domain): schema-validated, event-emitting KV domains over storage backends for the DeepSeek Harness", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/storage/storage-json/package.json b/packages/storage/storage-json/package.json index 8a5d7c9fb1..7efc811c36 100644 --- a/packages/storage/storage-json/package.json +++ b/packages/storage/storage-json/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-storage-json", "description": "JSON file KV storage backend for the DeepSeek Harness storage hub", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/storage/storage-sqlite/package.json b/packages/storage/storage-sqlite/package.json index fd4872d1ac..3b57ffd8c0 100644 --- a/packages/storage/storage-sqlite/package.json +++ b/packages/storage/storage-sqlite/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-storage-sqlite", "description": "SQLite storage backend (kv facet) for the DeepSeek Harness storage hub", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/storage/storage/package.json b/packages/storage/storage/package.json index 6ab7c7e3a0..9c41ca9ee4 100644 --- a/packages/storage/storage/package.json +++ b/packages/storage/storage/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-storage", "description": "Storage hub (ctx.storage): named backend registry plus mounted data-form facilities for the DeepSeek Harness", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent-acp/package.json b/packages/subagent/subagent-acp/package.json index 69adc7a68d..c16896a5ff 100644 --- a/packages/subagent/subagent-acp/package.json +++ b/packages/subagent/subagent-acp/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent-acp", "description": "Out-of-process ACP subagent backend: drives a child agent in a spawned subprocess over the Agent Client Protocol", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent-claude-code/package.json b/packages/subagent/subagent-claude-code/package.json index 8a5f5784e2..6216690b4a 100644 --- a/packages/subagent/subagent-claude-code/package.json +++ b/packages/subagent/subagent-claude-code/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent-claude-code", "description": "One-shot Claude Code subagent provider over the official Agent SDK", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent-codex/package.json b/packages/subagent/subagent-codex/package.json index b6f8de79e7..a44170636a 100644 --- a/packages/subagent/subagent-codex/package.json +++ b/packages/subagent/subagent-codex/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent-codex", "description": "One-shot Codex subagent provider over the official app-server protocol", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent-dsh-sdk/package.json b/packages/subagent/subagent-dsh-sdk/package.json index 318c11b0c4..ebb7f50ce7 100644 --- a/packages/subagent/subagent-dsh-sdk/package.json +++ b/packages/subagent/subagent-dsh-sdk/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent-dsh-sdk", "description": "Out-of-process SDK subagent backend: drives a child DeepSeek Harness runtime subprocess over stdio JSON-RPC through the TypeScript SDK client", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent-fork-in-process/package.json b/packages/subagent/subagent-fork-in-process/package.json index c90e0e68d9..f9b342dd01 100644 --- a/packages/subagent/subagent-fork-in-process/package.json +++ b/packages/subagent/subagent-fork-in-process/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent-fork-in-process", "description": "In-process fork subagent backend: runs a child agent seeded with a prefix of the parent's log", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent-in-process-driver/package.json b/packages/subagent/subagent-in-process-driver/package.json index 967d7eb635..066987a7e4 100644 --- a/packages/subagent/subagent-in-process-driver/package.json +++ b/packages/subagent/subagent-in-process-driver/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent-in-process-driver", "description": "Shared in-process subagent run driver: drives a child agent on ctx.agents (used by the spawn and fork backends)", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent-spawn-in-process/package.json b/packages/subagent/subagent-spawn-in-process/package.json index 5a5a52bb1d..744fb203c6 100644 --- a/packages/subagent/subagent-spawn-in-process/package.json +++ b/packages/subagent/subagent-spawn-in-process/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent-spawn-in-process", "description": "In-process spawn subagent backend: runs a fresh child agent on ctx.agents", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent/package.json b/packages/subagent/subagent/package.json index f36c870f22..38fcf39a5f 100644 --- a/packages/subagent/subagent/package.json +++ b/packages/subagent/subagent/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent", "description": "Abstract subagent seam (ctx.subagents): named-provider registry for delegating to child agents", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/tool-subagent-control/package.json b/packages/subagent/tool-subagent-control/package.json index eda62c7c96..c7573a393e 100644 --- a/packages/subagent/tool-subagent-control/package.json +++ b/packages/subagent/tool-subagent-control/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-subagent-control", "description": "Globally named send_message, interrupt_agent, and list_agents tools over ctx.subagents continuations", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/tool-subagent/package.json b/packages/subagent/tool-subagent/package.json index a8f961d042..eb16118d7b 100644 --- a/packages/subagent/tool-subagent/package.json +++ b/packages/subagent/tool-subagent/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-subagent", "description": "Model-facing subagent delegation tool over the ctx.subagents seam", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/subprocess/subprocess-local/package.json b/packages/subprocess/subprocess-local/package.json index 50d307859b..85b2055512 100644 --- a/packages/subprocess/subprocess-local/package.json +++ b/packages/subprocess/subprocess-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subprocess-local", "description": "Local-subprocess implementation of the DeepSeek Harness subprocess seam", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/subprocess/subprocess/package.json b/packages/subprocess/subprocess/package.json index 74f248df7d..84e1c0fbbc 100644 --- a/packages/subprocess/subprocess/package.json +++ b/packages/subprocess/subprocess/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subprocess", "description": "Subprocess seam (ctx.subprocess) for the DeepSeek Harness \u2014 managed process groups, bounded spill-backed output, and escalated kills behind one abstract service", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/subprocess/win32-process/package.json b/packages/subprocess/win32-process/package.json index 713fefb2c3..514f9f7ada 100644 --- a/packages/subprocess/win32-process/package.json +++ b/packages/subprocess/win32-process/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-win32-process", "description": "Shared low-level Win32 process, stdio, and Job Object primitives", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/terminal/terminal-bash/package.json b/packages/terminal/terminal-bash/package.json index 49d644e3fa..62a0fae23e 100644 --- a/packages/terminal/terminal-bash/package.json +++ b/packages/terminal/terminal-bash/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-terminal-bash", "description": "Persistent shell PTY backend over the DeepSeek Harness subprocess terminal primitive", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/terminal/terminal/package.json b/packages/terminal/terminal/package.json index 71496630bb..697e9d4ebd 100644 --- a/packages/terminal/terminal/package.json +++ b/packages/terminal/terminal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-terminal", "description": "Persistent PTY session seam for the DeepSeek Harness — owner-scoped ids, backend registry, interactive sends, reads, signals, and awaited cleanup", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/terminal/tool-terminal/package.json b/packages/terminal/tool-terminal/package.json index 8a4cc1a868..1688847288 100644 --- a/packages/terminal/tool-terminal/package.json +++ b/packages/terminal/tool-terminal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-terminal", "description": "Six model-facing persistent PTY tools with owner isolation and generic background-job integration", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/test-support/agent-loop-testkit/package.json b/packages/test-support/agent-loop-testkit/package.json index 6cd1e31817..e41a535c86 100644 --- a/packages/test-support/agent-loop-testkit/package.json +++ b/packages/test-support/agent-loop-testkit/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent-loop-testkit", "description": "Prerequisite mounting, production AgentLoop drivers, and Inbox stubs for tests", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/test-support/client-runtime/package.json b/packages/test-support/client-runtime/package.json index 22bfcf7d16..b4e5a20033 100644 --- a/packages/test-support/client-runtime/package.json +++ b/packages/test-support/client-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-test-runtime", "description": "Browser test runtimes: a jsdom slot bench with test-owned Session and Workspace doubles, and a whole-client tier that boots the web roster through the production bootClient over an endpoint-named Remote mock", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/test-support/llm-mock-server/package.json b/packages/test-support/llm-mock-server/package.json index adb49fc64b..e7c1a25aef 100644 --- a/packages/test-support/llm-mock-server/package.json +++ b/packages/test-support/llm-mock-server/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-llm-mock-server", "description": "Scriptable Messages HTTP/SSE fault server for LLM recovery tests", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/test-support/llm-replay/package.json b/packages/test-support/llm-replay/package.json index f064cc30c6..9e7e1b69fe 100644 --- a/packages/test-support/llm-replay/package.json +++ b/packages/test-support/llm-replay/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-llm-replay", "description": "Replay LLM plugin: short-circuits llm/stream with model chunks reconstructed from a recorded session JSONL (keyless snapshot tests)", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/test-support/loader-smoke/package.json b/packages/test-support/loader-smoke/package.json index 197dce5c07..18c6d0a9f2 100644 --- a/packages/test-support/loader-smoke/package.json +++ b/packages/test-support/loader-smoke/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-loader-smoke", "description": "Shared subprocess and direct-agent harness for keyless real-Loader example smoke tests", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/test-support/remote-mock/package.json b/packages/test-support/remote-mock/package.json index 64dfb2617c..a3186958b2 100644 --- a/packages/test-support/remote-mock/package.json +++ b/packages/test-support/remote-mock/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-remote-mock", "description": "Endpoint-named mock for Typert Remote traffic: unary answers and stream scripts per /, live stream control, a log, and the Connection carrier face whole-client specs install", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/test-support/session-snapshot/package.json b/packages/test-support/session-snapshot/package.json index 76df2219f1..9364e43e0e 100644 --- a/packages/test-support/session-snapshot/package.json +++ b/packages/test-support/session-snapshot/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-snapshot", "description": "Session-log snapshot core with an ACP protocol adapter, expected-output normalization, and fixture invariants", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/todo/tool-todo/package.json b/packages/todo/tool-todo/package.json index 19e93ed48b..bbd0fed447 100644 --- a/packages/todo/tool-todo/package.json +++ b/packages/todo/tool-todo/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-todo", "description": "Model-facing todo_write tool over the DeepSeek Harness event-sourced session log", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/typert/generator/package.json b/packages/typert/generator/package.json index 1c26c186c4..94055dde80 100644 --- a/packages/typert/generator/package.json +++ b/packages/typert/generator/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-typert-generator", "description": "TypeScript project analyzer and model-driven Typert artifact generator", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/typert/loader/package.json b/packages/typert/loader/package.json index 91f7ff9162..38323ae71d 100644 --- a/packages/typert/loader/package.json +++ b/packages/typert/loader/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-typert-loader", "description": "Loader integration for generated Typert package contributions", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/typert/protocol/package.json b/packages/typert/protocol/package.json index a74f2c9cb9..c4c81b1377 100644 --- a/packages/typert/protocol/package.json +++ b/packages/typert/protocol/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-typert-protocol", "description": "Compiler-independent Remote metadata and Typert provider protocols", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/typert/registry/package.json b/packages/typert/registry/package.json index 2cdfde73b2..2fa54b854f 100644 --- a/packages/typert/registry/package.json +++ b/packages/typert/registry/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-typert-registry", "description": "Runtime registry for generated package reflection and Zod schemas", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/atomic-write/package.json b/packages/util/atomic-write/package.json index 885ddde92a..44227e213d 100644 --- a/packages/util/atomic-write/package.json +++ b/packages/util/atomic-write/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-atomic-write", "description": "Zero-dependency atomic file replacement: exclusive-create random-suffix temp + rename carrying the caller-stated permissions (writeFileAtomic)", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/brand/package.json b/packages/util/brand/package.json index ec8c426c39..d5127c46c1 100644 --- a/packages/util/brand/package.json +++ b/packages/util/brand/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-brand", "description": "Stateless branded primitive types for the DeepSeek Harness", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/chunked-list/package.json b/packages/util/chunked-list/package.json index 5448935e79..3ae771f041 100644 --- a/packages/util/chunked-list/package.json +++ b/packages/util/chunked-list/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-chunked-list", "description": "Persistent append-only chunked lists with bounded copying and JSON checkpoint validation", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/crypto/package.json b/packages/util/crypto/package.json index 219d72ac38..1c83ac57f6 100644 --- a/packages/util/crypto/package.json +++ b/packages/util/crypto/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-util-crypto", "description": "Zero-dependency browser-safe UUID and byte-encoding helpers", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/deque/package.json b/packages/util/deque/package.json index b3b940941b..53f4e535a0 100644 --- a/packages/util/deque/package.json +++ b/packages/util/deque/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-deque", "description": "Zero-dependency circular deque with amortized constant-time end operations and bounded vacant storage", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/home-paths/package.json b/packages/util/home-paths/package.json index 029ccae080..23af68a4a2 100644 --- a/packages/util/home-paths/package.json +++ b/packages/util/home-paths/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-home-paths", "description": "Shared filesystem path helpers for the DeepSeek Harness", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/http-proxy/package.json b/packages/util/http-proxy/package.json index c0b5c711d3..5cdc0c5179 100644 --- a/packages/util/http-proxy/package.json +++ b/packages/util/http-proxy/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-http-proxy", "description": "Process-wide outbound HTTP proxy policy for DeepSeek Harness: resolve it from the launch environment and install it as undici's global dispatcher", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/launch-environment/package.json b/packages/util/launch-environment/package.json index 47f2bf1c86..77ce691086 100644 --- a/packages/util/launch-environment/package.json +++ b/packages/util/launch-environment/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-launch-environment", "description": "Immutable DeepSeek Harness launch environment that records which layer supplied each value", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/lazy-require/package.json b/packages/util/lazy-require/package.json index 38e75afcc6..4f1dcafdc8 100644 --- a/packages/util/lazy-require/package.json +++ b/packages/util/lazy-require/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-lazy-require", "description": "Caller-relative, success-cached lazy loading for CommonJS-compatible Host dependencies", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/native-command/package.json b/packages/util/native-command/package.json index ebf73e5f73..8fa10e9d7a 100644 --- a/packages/util/native-command/package.json +++ b/packages/util/native-command/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-native-command", "description": "Host-native command and path-opening utilities with shell-free execution, cancellation, desktop detection, and WSL handoff", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/output-retention/package.json b/packages/util/output-retention/package.json index b7147ccc68..b1c35111d7 100644 --- a/packages/util/output-retention/package.json +++ b/packages/util/output-retention/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-output-retention", "description": "Zero-dependency bounded-retention primitive: ItemRetainer/TextRetainer + neutral notice helpers (what did we keep, what did we omit)", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/package-manifest/package.json b/packages/util/package-manifest/package.json index d3e8b2d7ec..c60a771c8c 100644 --- a/packages/util/package-manifest/package.json +++ b/packages/util/package-manifest/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-package-manifest", "description": "Shared type declarations for package.json.dsh configuration fields", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/time/package.json b/packages/util/time/package.json index 93dbad155d..1b22e958bb 100644 --- a/packages/util/time/package.json +++ b/packages/util/time/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-util-time", "description": "Zero-dependency time vocabulary shared by wire boundaries: canonicalClientTimeZone (IANA zone validation and canonicalization only, no formatting)", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/timeout/package.json b/packages/util/timeout/package.json index 7c2c6ddd6b..5d15a2d582 100644 --- a/packages/util/timeout/package.json +++ b/packages/util/timeout/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-timeout", "description": "Zero-dependency timeout/deadline primitive: clampTimeout, deadline, timeoutOf, TimeoutReason (timing + classification only, no termination)", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/values/package.json b/packages/util/values/package.json index 35a646a817..3404c228bc 100644 --- a/packages/util/values/package.json +++ b/packages/util/values/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-util-values", "description": "Duplicate-install-safe value primitives for the DeepSeek Harness", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/util/workspace-path/package.json b/packages/util/workspace-path/package.json index 0f1f31f391..6cdf54003c 100644 --- a/packages/util/workspace-path/package.json +++ b/packages/util/workspace-path/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-util-workspace-path", "description": "Browser-safe Workspace path and display helpers", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/web/tool-web/package.json b/packages/web/tool-web/package.json index 40feb58f66..e2c40b37dc 100644 --- a/packages/web/tool-web/package.json +++ b/packages/web/tool-web/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-web", "description": "Model-facing web tools (web_search, web_fetch) over the DeepSeek Harness web capability seam (ctx.web)", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/web/web-fetch-http/package.json b/packages/web/web-fetch-http/package.json index 1809552282..aa31af13ec 100644 --- a/packages/web/web-fetch-http/package.json +++ b/packages/web/web-fetch-http/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-web-fetch-http", "description": "Anonymous public HTTP(S) fetch provider for the DeepSeek Harness web capability seam (ctx.web)", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/web/web-search-deepseek/package.json b/packages/web/web-search-deepseek/package.json index 3c28bf7f41..f92130d3d8 100644 --- a/packages/web/web-search-deepseek/package.json +++ b/packages/web/web-search-deepseek/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-web-search-deepseek", "description": "DeepSeek-backed search provider (native web_search via the Anthropic-compatible API) for the DeepSeek Harness web capability seam (ctx.web)", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/web/web-search-exa/package.json b/packages/web/web-search-exa/package.json index d82912919f..2816290bf3 100644 --- a/packages/web/web-search-exa/package.json +++ b/packages/web/web-search-exa/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-web-search-exa", "description": "Exa-backed search provider for the DeepSeek Harness web capability seam (ctx.web)", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/web/web-search-perplexity/package.json b/packages/web/web-search-perplexity/package.json index 178a9f549c..89ddfc7b6f 100644 --- a/packages/web/web-search-perplexity/package.json +++ b/packages/web/web-search-perplexity/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-web-search-perplexity", "description": "Perplexity-backed search provider for the DeepSeek Harness web capability seam (ctx.web)", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/web/web/package.json b/packages/web/web/package.json index a78b06e117..010eb41dec 100644 --- a/packages/web/web/package.json +++ b/packages/web/web/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-web", "description": "Abstract web access capability seam (ctx.web) for the DeepSeek Harness — search/fetch provider registry, registration-order-independent selection, request/result vocabulary, and the WebError taxonomy", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/webhook/webhook-github/package.json b/packages/webhook/webhook-github/package.json index 875b02eac2..ad6ce72720 100644 --- a/packages/webhook/webhook-github/package.json +++ b/packages/webhook/webhook-github/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-webhook-github", "description": "Signed GitHub HTTP webhook adapter for the DeepSeek Harness webhook runtime", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/webhook/webhook/package.json b/packages/webhook/webhook/package.json index 4d0b2d1b10..28b02bd05b 100644 --- a/packages/webhook/webhook/package.json +++ b/packages/webhook/webhook/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-webhook", "description": "Fire-and-forget webhook rule runtime that creates Workspace-backed DeepSeek Harness Sessions", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/workflow/tool-ralph/package.json b/packages/workflow/tool-ralph/package.json index 1306414d4c..d2e76f3364 100644 --- a/packages/workflow/tool-ralph/package.json +++ b/packages/workflow/tool-ralph/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-ralph", "description": "Model-facing fresh-agent Ralph loop over the workflow and subagent seams", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/workflow/tool-workflow/package.json b/packages/workflow/tool-workflow/package.json index 5a9c0fcd9f..ac3d65e9e0 100644 --- a/packages/workflow/tool-workflow/package.json +++ b/packages/workflow/tool-workflow/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-workflow", "description": "Model-facing workflow tool: run a JavaScript orchestration script over ctx.workflowEngine", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/workflow/workflow-ptc/package.json b/packages/workflow/workflow-ptc/package.json index d5c2543721..fe776b7bb7 100644 --- a/packages/workflow/workflow-ptc/package.json +++ b/packages/workflow/workflow-ptc/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-workflow-ptc", "description": "Workflow orchestration in the shared sandboxed Node PTC runtime", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/workflow/workflow/package.json b/packages/workflow/workflow/package.json index 8a11473eb9..7ffbb48324 100644 --- a/packages/workflow/workflow/package.json +++ b/packages/workflow/workflow/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-workflow", "description": "Workflow capability seam: ctx.workflowEngine service, run vocabulary, and workflow/* events", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/workspace/workspace/package.json b/packages/workspace/workspace/package.json index 9fc100415d..eab12aee36 100644 --- a/packages/workspace/workspace/package.json +++ b/packages/workspace/workspace/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-workspace", "description": "Workspace entity registry (ctx.workspaceRegistry): durable workspace records with validated session attachment over the domain data form for the DeepSeek Harness", - "version": "0.1.7-alpha.1", + "version": "0.1.7-alpha.2", "publishConfig": { "access": "public" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 232cfc6a3a..9b12381c71 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6871,9 +6871,6 @@ importers: '@deepseek-ai/dsh-subagent-spawn-in-process': specifier: workspace:* version: link:../../subagent/subagent-spawn-in-process - '@deepseek-ai/dsh-typert-protocol': - specifier: workspace:* - version: link:../../typert/protocol packages/experimental/agent-team-profile: dependencies: @@ -7232,15 +7229,15 @@ importers: '@deepseek-ai/cordis': specifier: workspace:~ version: link:../../../vendor/cordis - '@deepseek-ai/dsh-api-remotes': - specifier: workspace:* - version: link:../../api/remotes '@deepseek-ai/dsh-api-session-controller': specifier: workspace:* version: link:../../api/session-controller '@deepseek-ai/dsh-client-locale': specifier: workspace:* version: link:../../client/locale + '@deepseek-ai/dsh-client-store': + specifier: workspace:* + version: link:../../client/store '@deepseek-ai/dsh-client-test-runtime': specifier: workspace:* version: link:../../test-support/client-runtime @@ -7268,9 +7265,6 @@ importers: '@deepseek-ai/dsh-session': specifier: workspace:* version: link:../../core/session - '@deepseek-ai/dsh-typert-protocol': - specifier: workspace:* - version: link:../../typert/protocol '@testing-library/react': specifier: ^16.1.0 version: 16.3.2(@testing-library/dom@10.4.2)(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) diff --git a/scripts/gen-cordis-catalog.ts b/scripts/gen-cordis-catalog.ts index 042e19d444..644f7073b2 100644 --- a/scripts/gen-cordis-catalog.ts +++ b/scripts/gen-cordis-catalog.ts @@ -623,6 +623,7 @@ export const LINK_MAP: Readonly> = { JobSettleCause: 'jobs.md', SpeechDownloadFailure: 'voice-input.md', SpeechPreparationState: 'voice-input.md', + SpeechPreparationOptions: 'voice-input.md', SpeechPreparationStep: 'voice-input.md', SpeechPreparationStepKind: 'voice-input.md', SpeechProviderView: 'voice-input.md', @@ -650,7 +651,8 @@ export const LINK_MAP: Readonly> = { TeamMembership: 'agent-team.md', TeamTaskId: 'agent-team.md', TeamTaskView: 'agent-team.md', - TeamView: 'agent-team.md', + TeamProjection: 'agent-team.md', + TeamMemberProjection: 'agent-team.md', TeamWaitResult: 'agent-team.md', UpdateTeamTaskRequest: 'agent-team.md', TokenMeasurement: 'token-meter.md', diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts index bddc726f3c..f2195887e9 100644 --- a/scripts/gen-doc-graphs.ts +++ b/scripts/gen-doc-graphs.ts @@ -716,8 +716,8 @@ const SERVICE_ROLES: ServiceRole[] = [ pkg: 'experimental-agent-team', title: 'Agent Teams coordination domain', mode: 'core', - consumers: ['experimental-tool-agent-team', 'experimental-client-ui-agent-team'], - note: 'Owns the implicit-root roster, durable peer mailbox, shared task DAG, continuable-child lifecycle, and generated Team Remote methods; tool-agent-team contributes model controls and client-ui-agent-team mounts the browser contribution.', + consumers: ['experimental-tool-agent-team'], + note: 'Owns the implicit-root roster, durable peer mailbox, shared task DAG, and continuable-child lifecycle; tool-agent-team contributes model controls.', }, { key: 'inspector', diff --git a/scripts/no-unknown-casts.baseline.json b/scripts/no-unknown-casts.baseline.json index 5bc8b14c95..1a590ad1b9 100644 --- a/scripts/no-unknown-casts.baseline.json +++ b/scripts/no-unknown-casts.baseline.json @@ -1143,13 +1143,6 @@ "019c354b579e6d5cfa63e106e3b3047817fe5a37eee532abd7ab4aa80d28676a": 1, "4777453719b1a12ac03d7405a7cc1662ac1873b8eb0016a08ccd11f27076e803": 1 }, - "packages/experimental/client-ui-agent-team/tests/browser-plugin.client.spec.ts": { - "22baf2573ca146a7904b9ffc8b34a43692c4e042f01f43016d85443828bc88b1": 1, - "87ef7d60538b938bd8b5748cf4c0a5956ecedb860826c57d9ec789daea2f0355": 5 - }, - "packages/experimental/client-ui-agent-team/tests/team-action.client.spec.tsx": { - "032fa1f91833ea216b76d2724f073741e70ad3eafebe4fbfcb166b950618e415": 1 - }, "packages/experimental/computer-use-cua-driver-mcp/tests/composition.spec.ts": { "154da39139a0074bad3d13a624e5fa62d1f7ce19dc8b9c28a73af3263cd7f10e": 1 }, diff --git a/scripts/type-equiv.manifest.json b/scripts/type-equiv.manifest.json index 13b995afc4..48d3b928d7 100644 --- a/scripts/type-equiv.manifest.json +++ b/scripts/type-equiv.manifest.json @@ -337,6 +337,21 @@ "symbol": "ScheduleView", "source": "packages/schedule/schedule/src/types.ts" }, + { + "doc": "docs/subsystems/agent-team.md", + "symbol": "TeamMemberProjection", + "source": "packages/experimental/agent-team/src/types.ts" + }, + { + "doc": "docs/subsystems/agent-team.md", + "symbol": "TeamTaskView", + "source": "packages/experimental/agent-team/src/types.ts" + }, + { + "doc": "docs/subsystems/agent-team.md", + "symbol": "TeamProjection", + "source": "packages/experimental/agent-team/src/types.ts" + }, { "doc": "docs/subsystems/agent-team.md", "symbol": "TeamMemberSnapshot", diff --git a/snapshots/web/queue-actions/editing.expected.md b/snapshots/web/queue-actions/editing.expected.md index 0e02090d39..d375969b1e 100644 --- a/snapshots/web/queue-actions/editing.expected.md +++ b/snapshots/web/queue-actions/editing.expected.md @@ -21,7 +21,6 @@ - listitem: - textbox "Edit queued message": Edited queue item - button "Save queued message" - - tooltip "Save queued message" - button "Cancel editing" - textbox "Cmd/Ctrl+Enter steers all queued messages" - button "Add files or run commands" diff --git a/snapshots/web/queue-actions/ui.expected.md b/snapshots/web/queue-actions/ui.expected.md index 6107949766..6dc03fa797 100644 --- a/snapshots/web/queue-actions/ui.expected.md +++ b/snapshots/web/queue-actions/ui.expected.md @@ -15,7 +15,6 @@ - listitem: - text: Edited queue item - button "Edit queued message" - - tooltip "Edit queued message" - button "Remove queued message" - button "Steer queued message" - textbox "Cmd/Ctrl+Enter steers all queued messages" diff --git a/tsconfig.host.json b/tsconfig.host.json index c551469419..2d31d0df31 100644 --- a/tsconfig.host.json +++ b/tsconfig.host.json @@ -58,6 +58,7 @@ "apps/web/tests/plugin-install-cancel.e2e.ts", "apps/web/tests/plugin-install-approve.e2e.ts", "apps/web/tests/plugin-install-registry.e2e.ts", + "apps/web/tests/plugin-install-github.e2e.ts", "apps/web/tests/client-plugin-live.e2e.ts", "apps/web/tests/settings-chrome.e2e.ts", "apps/web/tests/desktop-locale.e2e.ts", diff --git a/vendor/cordis/package.json b/vendor/cordis/package.json index 35fe271452..abeac30e1c 100644 --- a/vendor/cordis/package.json +++ b/vendor/cordis/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/cordis", "description": "Meta-Framework for Modern JavaScript Applications", - "version": "4.0.3", + "version": "4.0.4", "publishConfig": { "access": "public" }, diff --git a/vendor/cosmokit/package.json b/vendor/cosmokit/package.json index 0e3aa0692f..b1dcebc8a8 100644 --- a/vendor/cosmokit/package.json +++ b/vendor/cosmokit/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/cosmokit", "description": "A collection of common utilities", - "version": "1.8.4", + "version": "1.8.5", "publishConfig": { "access": "public" }, diff --git a/vendor/group/package.json b/vendor/group/package.json index 0b09a6a6fe..780e89e4c5 100644 --- a/vendor/group/package.json +++ b/vendor/group/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/cordis-plugin-group", "description": "Nested plugin group for cordis", - "version": "1.0.3", + "version": "1.0.4", "publishConfig": { "access": "public" }, diff --git a/vendor/hmr/package.json b/vendor/hmr/package.json index 762a3ac0bc..76624ca522 100644 --- a/vendor/hmr/package.json +++ b/vendor/hmr/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/cordis-plugin-hmr", "description": "Hot Module Replacement Plugin for Cordis", - "version": "1.0.18", + "version": "1.0.19", "publishConfig": { "access": "public" }, diff --git a/vendor/include/package.json b/vendor/include/package.json index 4768e23f39..3592ebec9a 100644 --- a/vendor/include/package.json +++ b/vendor/include/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/cordis-plugin-include", "description": "Include files in cordis configurations", - "version": "1.0.8", + "version": "1.0.9", "publishConfig": { "access": "public" }, diff --git a/vendor/loader/package.json b/vendor/loader/package.json index d32e003efc..1c96ff3708 100644 --- a/vendor/loader/package.json +++ b/vendor/loader/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/cordis-plugin-loader", "description": "Plugin loader for cordis", - "version": "1.0.4", + "version": "1.0.5", "publishConfig": { "access": "public" }, diff --git a/vendor/logger-console/package.json b/vendor/logger-console/package.json index efc444afcc..03f12f9ac6 100644 --- a/vendor/logger-console/package.json +++ b/vendor/logger-console/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/cordis-plugin-logger-console", "description": "Console logger exporter for cordis", - "version": "1.0.3", + "version": "1.0.4", "publishConfig": { "access": "public" }, diff --git a/vendor/schemastery/package.json b/vendor/schemastery/package.json index b8a6465592..81d8be7c1b 100644 --- a/vendor/schemastery/package.json +++ b/vendor/schemastery/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/schemastery", "description": "Type driven schema validator", - "version": "3.18.3", + "version": "3.18.4", "publishConfig": { "access": "public" }, diff --git a/vendor/timer/package.json b/vendor/timer/package.json index 381299be6e..ca6d50cf11 100644 --- a/vendor/timer/package.json +++ b/vendor/timer/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/cordis-plugin-timer", "description": "Timer service for cordis", - "version": "1.1.5", + "version": "1.1.6", "publishConfig": { "access": "public" },