fix(cua): merge master and address selection review feedback
This commit is contained in:
commit
1d246ff9af
710 files changed
+21461
-4961
No files matched your search
+4
-4
@@ -9,11 +9,11 @@
|
||||
en: d94bb81479bfa4a1
|
||||
zh: af44ceaecf84ee51
|
||||
/agent-note-one-ordering-for-configuration-sources-and-what-a-discovered-file-may-not-decide/decision:
|
||||
en: aea80ece13f2905b
|
||||
zh: a880bc1a7839cf12
|
||||
en: f2e0559e9cf0eddd
|
||||
zh: 704138e08a93979d
|
||||
/agent-note-one-ordering-for-configuration-sources-and-what-a-discovered-file-may-not-decide/consequences:
|
||||
en: 41ebe22077560090
|
||||
zh: da3475e4c196c3f9
|
||||
en: 15d57cfa8c37ddc9
|
||||
zh: e16becb173d87579
|
||||
/agent-note-one-ordering-for-configuration-sources-and-what-a-discovered-file-may-not-decide/alternatives-considered:
|
||||
en: ac43499cfe7b8495
|
||||
zh: e5ab599e7d3ed753
|
||||
@@ -42,9 +42,9 @@ inherited process environment (read-only, wins)
|
||||
|
||||
The launching environment wins because `DEEPSEEK_API_KEY=… dsh`, a CI secret, and a container `-e` are the one override an operator must be able to apply per run without editing machine state, and because it cannot be edited from inside it must be *visibly* read-only. Configuration is meant to carry only the *reference* — which name to resolve — and that name follows the non-secret ordering above.
|
||||
|
||||
**The project the harness is launched in is trusted, by default and without a prompt.** A checkout may carry its own endpoint, its own ordinary variables, and its own key; the key ranks below the managed store, so a key stored through the Models page is never displaced by one a checkout happens to contain. `LaunchEnvironmentSnapshot.getFrom(name, sources)` still searches only the layers a caller names, and omitting one is a refusal rather than a demotion — the mechanism exists for decisions where a layer must be unreachable; this decision includes the project layer.
|
||||
**The project the harness is launched in is trusted, by default and without a prompt.** A checkout may carry its own ordinary variables and its own key, but not the launch-only names the next paragraph lists, which include the DeepSeek endpoints; the key ranks below the managed store, so a key stored through the Models page is never displaced by one a checkout happens to contain. `LaunchEnvironmentSnapshot.getFrom(name, sources)` still searches only the layers a caller names, and omitting one is a refusal rather than a demotion — the mechanism exists for decisions where a layer must be unreachable; this decision includes the project layer.
|
||||
|
||||
**Trust does not extend to changing the harness itself.** `loadLayeredEnv` rejects, at load and before anything is materialized, any `.env` that sets a variable governing how a process launches (`PATH`, `SHELL`, `NODE_OPTIONS`, `LD_PRELOAD`), which ambient program handles an operation (`EDITOR`, `PAGER`, `BROWSER`), what code a runtime executes before the program it was asked to run (`BASH_ENV`, `PERL5OPT`, `PYTHONSTARTUP`, `RUBYOPT`, `JAVA_TOOL_OPTIONS`, the Git hook commands), where model-visible instructions load from (the whole `DSH_*` namespace, `HOME`, `XDG_*`), or how the network is reached and trusted (proxy and CA variables). Matching is case-insensitive, so `https_proxy` is not a bypass. One exemption, recorded in [the proxy policy note](2026-08-27-outbound-proxy-policy.md): the four proxy names are accepted from `$DSH_HOME/.env`, which no `.env` can relocate, and still refused from the invoking directory's file.
|
||||
**Trust does not extend to changing the harness itself.** `loadLayeredEnv` rejects, at load and before anything is materialized, any `.env` that sets a variable governing how a process launches (`PATH`, `SHELL`, `NODE_OPTIONS`, `LD_PRELOAD`), which ambient program handles an operation (`EDITOR`, `PAGER`, `BROWSER`), what code a runtime executes before the program it was asked to run (`BASH_ENV`, `PERL5OPT`, `PYTHONSTARTUP`, `RUBYOPT`, `JAVA_TOOL_OPTIONS`, the Git hook commands), where model-visible instructions load from (the whole `DSH_*` namespace, `HOME`, `XDG_*`), or how the network is reached and trusted (proxy, CA, and DeepSeek endpoint variables). Matching is case-insensitive, so `https_proxy` is not a bypass. Two exemptions accept names from `$DSH_HOME/.env`, which no `.env` can relocate, and still refuse them from the invoking directory's file: the four proxy names, recorded in [the proxy policy note](2026-08-27-outbound-proxy-policy.md), and `DEEPSEEK_BASE_URL` and `DEEPSEEK_SEARCH_BASE_URL`. The endpoint names choose where the model and search requests that carry the user's DeepSeek credential go. The profile patches in the same home can already set those endpoints as plugin `baseURL` fields, so accepting the names from the home file gives a writer of that directory nothing it lacks.
|
||||
|
||||
The line is that these take effect with no user action, before any turn, outside the permission policy and the sandbox. `DSH_PERMISSION_MODE` would switch off the approvals that make trusting a project meaningful at all, and `BASH_ENV` runs a file of the project's choosing on every single `bash -c` the bash tool issues — the project's code running under the agent's policy is the deal; the project rewriting that policy is not. Enumerating these is a losing game one variable at a time, which is why the whole `DSH_*` namespace is denied rather than an audited subset, and why the list is organised by what a variable *does* rather than by which runtime owns it. There is no opt-out: an escape hatch would have to be readable from somewhere, and anything a discovered file could set is the hole itself.
|
||||
|
||||
@@ -55,7 +55,7 @@ The line is that these take effect with no user action, before any turn, outside
|
||||
## Consequences
|
||||
|
||||
- The web credential form now takes effect against an older key in the user's `.env`; only a key exported in the launching shell still makes it read-only, and the diagnostic says so.
|
||||
- A `.env` holding `DSH_*`, `PATH`, `BROWSER`, or — in the invoking directory — a proxy variable fails the launch instead of being applied. Developers keeping switches in a repository `.env` move them to their shell — a deliberate, loud break.
|
||||
- A `.env` holding `DSH_*`, `PATH`, `BROWSER`, or — in the invoking directory — a proxy or DeepSeek endpoint variable fails the launch instead of being applied. Developers keeping switches in a repository `.env` move them to their shell — a deliberate, loud break.
|
||||
- Composition is no longer overridable by a stale shell endpoint. It is still overridable by a user's stored `settings.yaml`, which is the settings seam's layering and not something this note changes; the product CLI offers no flag above it, so a deployment that must win against stored settings owns its own bin or loader tree.
|
||||
- Not solved: the layers are still materialized into `process.env`, so ordinary project variables continue to reach child processes under the subprocess scrub. Bootstrap variables cannot come from a file at all; the environment package records the remaining subprocess reach as a limitation.
|
||||
|
||||
|
||||
+3
-3
@@ -43,9 +43,9 @@ inherited process environment (read-only, wins)
|
||||
|
||||
继承环境优先,因为 `DEEPSEEK_API_KEY=… dsh`、CI 机密与容器 `-e` 是运维必须能按次施加、且无需改动机器状态的那一种覆盖;而它无法从进程内部修改,就必须*可见地*只读。配置本应只携带*引用*——解析哪个名字——该名字本身遵循上面的非机密顺序。
|
||||
|
||||
**harness 被启动于其中的项目默认可信,且不做询问。** 一个 checkout 可以携带自己的 endpoint、自己的普通变量和自己的密钥;密钥排在受管存储之下,因此通过 Models 页存下的密钥绝不会被 checkout 中恰好带有的那一个顶掉。`LaunchEnvironmentSnapshot.getFrom(name, sources)` 仍然只搜索调用方点名的层,省略某层仍是拒绝而不是降级——该机制供要求某一层不可达的决策使用;本决策包含项目层。
|
||||
**harness 被启动于其中的项目默认可信,且不做询问。** 一个 checkout 可以携带自己的普通变量和自己的密钥,但不能设置下一段列出的启动专用变量名,其中包括 DeepSeek 端点;密钥排在受管存储之下,因此通过 Models 页存下的密钥绝不会被 checkout 中恰好带有的那一个顶掉。`LaunchEnvironmentSnapshot.getFrom(name, sources)` 仍然只搜索调用方点名的层,省略某层仍是拒绝而不是降级——该机制供要求某一层不可达的决策使用;本决策包含项目层。
|
||||
|
||||
**信任不延伸到改变 harness 本身。** `loadLayeredEnv` 会在加载时、且在物化任何内容之前,拒绝任何设置了下列变量的 `.env`:决定进程如何启动的(`PATH`、`SHELL`、`NODE_OPTIONS`、`LD_PRELOAD`)、决定由哪个环境程序处理一项操作的(`EDITOR`、`PAGER`、`BROWSER`)、决定运行时在执行被要求运行的程序之前先执行哪些代码的(`BASH_ENV`、`PERL5OPT`、`PYTHONSTARTUP`、`RUBYOPT`、`JAVA_TOOL_OPTIONS`、Git 的钩子命令)、决定模型可见指令从哪里加载的(整个 `DSH_*` 命名空间、`HOME`、`XDG_*`),以及决定网络如何访问以及如何建立信任的(proxy 与 CA 变量)。匹配不区分大小写,因此 `https_proxy` 不是绕过手段。唯一的豁免记录在[代理策略笔记](2026-08-27-outbound-proxy-policy.zh.md)中:四个代理名可从 `$DSH_HOME/.env` 接受——没有任何 `.env` 能挪动该文件——但仍拒绝来自调用目录文件的同名变量。
|
||||
**信任不延伸到改变 harness 本身。** `loadLayeredEnv` 会在加载时、且在物化任何内容之前,拒绝任何设置了下列变量的 `.env`:决定进程如何启动的(`PATH`、`SHELL`、`NODE_OPTIONS`、`LD_PRELOAD`)、决定由哪个环境程序处理一项操作的(`EDITOR`、`PAGER`、`BROWSER`)、决定运行时在执行被要求运行的程序之前先执行哪些代码的(`BASH_ENV`、`PERL5OPT`、`PYTHONSTARTUP`、`RUBYOPT`、`JAVA_TOOL_OPTIONS`、Git 的钩子命令)、决定模型可见指令从哪里加载的(整个 `DSH_*` 命名空间、`HOME`、`XDG_*`),以及决定网络如何访问以及如何建立信任的(proxy、CA 与 DeepSeek 端点变量)。匹配不区分大小写,因此 `https_proxy` 不是绕过手段。有两项豁免从 `$DSH_HOME/.env` 接受变量名——没有任何 `.env` 能挪动该文件——但仍拒绝来自调用目录文件的同名变量:四个代理名,记录在[代理策略笔记](2026-08-27-outbound-proxy-policy.zh.md)中;以及 `DEEPSEEK_BASE_URL` 与 `DEEPSEEK_SEARCH_BASE_URL`。这两个端点变量名决定携带用户 DeepSeek 凭据的模型请求与搜索请求发往何处。同一 home 中的 profile patch 本就能以插件 `baseURL` 字段设置这些端点,因此从 home 文件接受它们,不会给能写入该目录的一方任何它原本没有的能力。
|
||||
|
||||
这条界线在于:它们无需任何用户动作、在任何轮次开始之前、且在权限策略与沙箱之外就生效。`DSH_PERMISSION_MODE` 会关掉让「信任项目」根本成立的那道审批,而 `BASH_ENV` 会在 bash 工具每次发出 `bash -c` 时执行项目指定的文件——项目的代码在 agent(智能体)的策略下运行是约定,项目改写那份策略不是。一个变量一个变量地枚举是必输的游戏,所以整个 `DSH_*` 命名空间被拒绝而不是只拒绝一份经审查的子集,也所以这份清单是按变量*做什么*而不是按哪个运行时拥有它来组织的。不设逃生门:逃生门本身总得从某处读取,而任何被发现的文件能设置的东西,就是那个漏洞本身。
|
||||
|
||||
@@ -56,7 +56,7 @@ inherited process environment (read-only, wins)
|
||||
## Consequences
|
||||
|
||||
- Web 凭据表单现在能压过用户 `.env` 里更旧的密钥;只有在启动 shell 里 export 的密钥才会让它变成只读,诊断信息也会这么说。
|
||||
- 含 `DSH_*`、`PATH`、`BROWSER` 或(在调用目录中)proxy 变量的 `.env` 会导致启动失败而不是被应用。把开关放在仓库 `.env` 里的开发者需要改放到 shell——这是一次刻意且响亮的破坏。
|
||||
- 含 `DSH_*`、`PATH`、`BROWSER` 或(在调用目录中)proxy 或 DeepSeek 端点变量的 `.env` 会导致启动失败而不是被应用。把开关放在仓库 `.env` 里的开发者需要改放到 shell——这是一次刻意且响亮的破坏。
|
||||
- composition 不再会被陈旧的 shell endpoint 覆盖。但它仍然会被用户已存的 `settings.yaml` 覆盖,这是 settings seam 的分层方式,本 Note 不改变它;产品 CLI 没有高于它的标志,因此需要压过已存 settings 的部署方要自带 bin 或 loader 配置树。
|
||||
- 未解决的:各层仍然会被物化进 `process.env`,因此普通项目变量继续按子进程清洗规则抵达子进程。bootstrap 变量完全不能来自文件;环境包将其余变量仍可抵达子进程这一点记录为一项限制。
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
en: 6379f0a2522f4c46
|
||||
zh: c45ffeb5089be274
|
||||
/agent-note-one-outbound-proxy-policy-installed-before-anything-can-request/decision:
|
||||
en: 6435c336ab53ca0f
|
||||
zh: 57070799a7061f85
|
||||
en: 5168301af05473ed
|
||||
zh: 30b840cb41998bee
|
||||
/agent-note-one-outbound-proxy-policy-installed-before-anything-can-request/alternatives-considered:
|
||||
en: df6c078f239a11f5
|
||||
zh: 914da26836aa373b
|
||||
|
||||
@@ -16,7 +16,7 @@ That sentence could not have worked anyway, for three measured reasons. `NODE_US
|
||||
|
||||
**One policy, resolved once from the launch environment, installed as the global dispatcher.** `packages/util/http-proxy` resolves a `ProxyPolicy` and installs it in `runProfile` immediately after the environment snapshot is provided and before any entry mounts. Node's `fetch` resolves undici's global dispatcher, so every plain `fetch()` and every SDK that reaches `globalThis.fetch` is covered without touching its code — nine call sites at the time of writing, and every future one for free. `loadLayeredEnv` has exactly one caller and `apps/web` ships no bin, so this single site covers every profile including `sdk-minimal`, which does not layer over `base`.
|
||||
|
||||
Resolution reads the launcher's snapshot rather than `process.env`, which is what makes a proxy in `$DSH_HOME/.env` work — the capability the environment-variable approach cannot have. Only that file: `loadLayeredEnv` refuses a proxy name in the project `.env` exactly as it refuses `PATH` or `NODE_OPTIONS` there, because that file arrives with a clone and must not choose the harness's route. The home file is exempt for the four proxy names alone, and `DSH_HOME` is itself bootstrap-only, so no `.env` can point the exemption at a directory a repository controls.
|
||||
Resolution reads the launcher's snapshot rather than `process.env`, which is what makes a proxy in `$DSH_HOME/.env` work — the capability the environment-variable approach cannot have. Only that file: `loadLayeredEnv` refuses a proxy name in the project `.env` exactly as it refuses `PATH` or `NODE_OPTIONS` there, because that file arrives with a clone and must not choose the harness's route. The home file is exempt for the four proxy names, and `DSH_HOME` is itself bootstrap-only, so no `.env` can point the exemption at a directory a repository controls.
|
||||
|
||||
**A library in `util/`, not a plugin.** Transport policy has one answer per process: nothing to swap, and no scope narrower than the process to give one. The package exports functions and mounts nothing — `boot`, `web`, `subprocess`, and `workflow` all consume it, and `util/` is the group every other group may depend on.
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ Node 内置的 `fetch` 会忽略 `HTTP_PROXY` 与 `HTTPS_PROXY`。开发者运
|
||||
|
||||
**一份策略,从启动环境解析一次,装为全局 dispatcher。** `packages/util/http-proxy` 解析出 `ProxyPolicy`,并在 `runProfile` 中于环境快照提供之后、任何 entry 挂载之前完成安装。Node 的 `fetch` 解析的正是 undici 的全局 dispatcher,因此每一处普通 `fetch()` 以及每一个最终落到 `globalThis.fetch` 的 SDK 都无需改动即被覆盖——撰写时是九个调用点,未来新增的也自动覆盖。`loadLayeredEnv` 只有一个调用方,且 `apps/web` 不提供 bin,因此这一处即覆盖全部 profile,包括不叠加 `base` 的 `sdk-minimal`。
|
||||
|
||||
解析读取的是启动器的快照而非 `process.env`,这正是让 `$DSH_HOME/.env` 中的代理生效的原因——也是环境变量方案不可能具备的能力。仅限该文件:`loadLayeredEnv` 拒绝项目 `.env` 里的代理名,正如它在那里拒绝 `PATH` 或 `NODE_OPTIONS`,因为那个文件随 clone 一起到来,不得替 Harness 选择路由。home 文件仅对这四个代理名豁免,而 `DSH_HOME` 本身是 bootstrap-only,因此没有任何 `.env` 能把这份豁免指向仓库控制的目录。
|
||||
解析读取的是启动器的快照而非 `process.env`,这正是让 `$DSH_HOME/.env` 中的代理生效的原因——也是环境变量方案不可能具备的能力。仅限该文件:`loadLayeredEnv` 拒绝项目 `.env` 里的代理名,正如它在那里拒绝 `PATH` 或 `NODE_OPTIONS`,因为那个文件随 clone 一起到来,不得替 Harness 选择路由。home 文件对这四个代理名豁免,而 `DSH_HOME` 本身是 bootstrap-only,因此没有任何 `.env` 能把这份豁免指向仓库控制的目录。
|
||||
|
||||
**放在 `util/` 的库,而非插件。** 传输策略每个进程只有一个答案:没有可替换的实现,也没有比进程更窄的作用域可赋予。因此本包只导出函数、不挂载任何东西——`boot`、`web`、`subprocess` 与 `workflow` 都消费它,而 `util/` 正是其他所有组都可以依赖的那一组。
|
||||
|
||||
|
||||
+2
-2
@@ -9,8 +9,8 @@
|
||||
en: ab63c8127d68d294
|
||||
zh: d3f99489c96ff1d4
|
||||
/agent-note-open-the-session-when-migration-is-required/decision:
|
||||
en: 32627d744a6e7410
|
||||
zh: 2836a95ee5390111
|
||||
en: 310ff84cece51901
|
||||
zh: a9a29e3ba30e03a8
|
||||
/agent-note-open-the-session-when-migration-is-required/alternatives-considered:
|
||||
en: baea781018a49252
|
||||
zh: bcc452029bf229fd
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ Other fork callers omit that option and allow automatic migration.
|
||||
|
||||
The `formatStatus` returned by `stat` carries the migration library's existing classification; `header` remains the current logical format. It describes the stored format, not migration-task progress or prepared in-memory history.
|
||||
|
||||
When migration is required and `subagentCatalog` is absent, Web marks the Session's row “Migration required” and keeps its history-opening actions without descendant expansion or changing its subagent mode. Opening the Session supplies its catalog through history and control projections. Available catalogs remain usable, including empty catalogs. Session-list lineage resolves navigation addresses, not missing catalog membership. Ordinary loading, cancellation, and storage errors do not become this migration branch.
|
||||
When migration is required and `subagentCatalog` is absent, Web keeps the Session row's history-opening actions without a format notice, descendant expansion, or changing its subagent mode. Opening the Session supplies its catalog through history and control projections. Available catalogs remain usable, including empty catalogs. Session-list lineage resolves navigation addresses, not missing catalog membership. Ordinary loading, cancellation, and storage errors do not become this migration branch.
|
||||
|
||||
Create/adopt, full-text search, and ordinary history opening allow automatic migration.
|
||||
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ WorkspaceBrowser 行菜单的 fork 请求携带 `allowMigration: false`,由 Ho
|
||||
|
||||
`stat` 返回的 `formatStatus` 携带迁移库已有的分类;`header` 仍是当前逻辑格式。它描述存储格式,不描述迁移任务进度或内存准备缓存。
|
||||
|
||||
在需要迁移且缺少 `subagentCatalog` 时,Web 将该会话行标为“需要迁移”,保留打开历史的操作,但不能展开下级,也不改变子会话的 mode。打开会话后,历史与 control 投影提供其目录。已有目录仍可使用,包括空目录。会话列表的谱系用于解析导航地址,不用于补齐缺失的目录成员。普通加载、取消和存储错误不转换成这一迁移分支。
|
||||
在需要迁移且缺少 `subagentCatalog` 时,Web 保留该会话行打开历史的操作,不显示格式提示,不展开下级,也不改变子会话的 mode。打开会话后,历史与 control 投影提供其目录。已有目录仍可使用,包括空目录。会话列表的谱系用于解析导航地址,不用于补齐缺失的目录成员。普通加载、取消和存储错误不转换成这一迁移分支。
|
||||
|
||||
create/adopt、全文搜索和普通历史打开允许自动迁移。
|
||||
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
en: cdc04139c8d69415
|
||||
zh: a3fe3e78ada08d95
|
||||
/agent-note-continuous-cua-hover-without-cross-call-ownership/decision:
|
||||
en: 125c860c1c568960
|
||||
zh: 8a2ece1101358200
|
||||
en: 193acf8c23672457
|
||||
zh: e614884d52585b16
|
||||
/agent-note-continuous-cua-hover-without-cross-call-ownership/alternatives-considered:
|
||||
en: 18d917ee2e909b93
|
||||
en: df67e547bce2f289
|
||||
zh: 5b5ad0530f138cda
|
||||
/agent-note-continuous-cua-hover-without-cross-call-ownership/consequences:
|
||||
en: 714789d0a472a308
|
||||
|
||||
@@ -10,7 +10,7 @@ Hover interactions span movement, delayed content, observation and input inside
|
||||
|
||||
## Decision
|
||||
|
||||
An explicit Window exposes `mouseMove` for coordinates or an observed element ID. The result reports `hoverRoute` and `systemPointerMoved`. It never presses a mouse button; disabled controls can use system-pointer movement with `systemPointerHover` enabled. Completion, AX reads and screenshots do not restore a previous pointer position. Successive calls can visit intermediate points before entering a popup. Clicks deliver button events at their target without an off-window priming click. The [input-routing decision](2026-10-13-cua-hover-routes-and-coordinates.md) selects AX alternate UI before allowing opt-in system movement with foreground and mouse-hit checks. It never activates or raises the app implicitly. Other input or window changes can race that check.
|
||||
An explicit Window exposes `mouseMove` for coordinates or an observed element ID. The result reports `hoverRoute` and `systemPointerMoved`. It never presses a mouse button. With session approval, system-pointer movement also works on disabled controls. Completion, AX reads and screenshots do not restore a previous pointer position. Successive calls can visit intermediate points before entering a popup. Clicks deliver button events at their target without an off-window priming click. The [input-routing decision](2026-10-13-cua-hover-routes-and-coordinates.md) requires foreground and mouse-hit checks for system movement. It never activates or raises the app implicitly. Other input or window changes can race that check.
|
||||
|
||||
The [shared helper](2026-10-09-cua-shared-native-helper.md) serializes complete individual operations. It grants no application ownership across calls. Earlier arrival, a retained app object and Session lifetime do not establish task priority. Waiting for the model does not reserve the application. Explicit task coordination determines any required execution order.
|
||||
|
||||
@@ -20,7 +20,7 @@ The [shared helper](2026-10-09-cua-shared-native-helper.md) serializes complete
|
||||
|
||||
**Click or drag as hover** introduces button input that can activate a control, select text or dismiss a popup. Hover requires a separate unpressed movement operation.
|
||||
|
||||
**PID-directed virtual movement** can display Chromium hover content after prior interaction, but a fresh window can ignore it, and later pointer checks can dismiss the content. Native AppKit tracking also requires real pointer movement in the measured fixture. The system-pointer fallback avoids reporting this unreliable event route as continuous hover.
|
||||
**PID-directed virtual movement** can display Chromium hover content after prior interaction, but a fresh window can ignore it, and later pointer checks can dismiss the content. Native AppKit tracking also requires real pointer movement in the measured fixture. The approved system-pointer route avoids reporting this unreliable event route as continuous hover.
|
||||
|
||||
## Consequences
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
显式窗口提供 `mouseMove`,接受窗口坐标或已观察元素的字符串 ID,结果用 `hoverRoute` 和 `systemPointerMoved` 报告执行路径。悬停不按下鼠标按钮,启用 `systemPointerHover` 后可通过系统鼠标悬停禁用控件。调用结束、AX 读取和截图不恢复先前的鼠标位置。进入浮层前可连续调用并经过中间位置。点击只在目标位置发送按键事件,不先在窗口外点击。 [输入路径决定](2026-10-13-cua-hover-routes-and-coordinates.zh.md)优先选择 AX 替代界面,只有启用该配置时才回退到经过前台和鼠标命中检查的系统移动,不隐式激活或抬起应用。其他输入或窗口变化仍可能与检查交错。
|
||||
显式窗口提供 `mouseMove`,接受窗口坐标或已观察元素的字符串 ID,结果用 `hoverRoute` 和 `systemPointerMoved` 报告执行路径。悬停不按下鼠标按钮,用户授权当前会话移动鼠标后,也可悬停在禁用的控件上。调用结束、AX 读取和截图不恢复先前的鼠标位置。进入浮层前可连续调用并经过中间位置。点击只在目标位置发送按键事件,不先在窗口外点击。 [输入路径决定](2026-10-13-cua-hover-routes-and-coordinates.zh.md)要求移动系统鼠标前检查前台应用和鼠标命中的窗口,不隐式激活或抬起应用。其他输入或窗口变化仍可能与检查交错。
|
||||
|
||||
[共享 helper](2026-10-09-cua-shared-native-helper.zh.md)串行执行完整的单次操作,不分配跨调用的应用所有权。先发起请求、持有 app 对象和 Session 寿命均不代表任务优先级。等待模型期间不占用应用。需要确定执行顺序时,由明确的任务协调决定。
|
||||
|
||||
|
||||
+2
-2
@@ -9,8 +9,8 @@
|
||||
en: f96bf1ebc0719fb0
|
||||
zh: e9a1144f0e76572d
|
||||
/agent-note-background-hover-and-screenshot-coordinates/decision:
|
||||
en: bdf4d3386521ce00
|
||||
zh: 96db93177ad96a76
|
||||
en: 36188669cfa9af11
|
||||
zh: f1f0cba1ebb18f49
|
||||
/agent-note-background-hover-and-screenshot-coordinates/alternatives-considered:
|
||||
en: 5fc4beaf79761c50
|
||||
zh: 4948333b0020be28
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ Hover-only controls can sometimes be revealed without moving the user's pointer.
|
||||
|
||||
## Decision
|
||||
|
||||
Native hover first uses a same-window element or ancestor advertising both `AXShowAlternateUI` and `AXShowDefaultUI`. Entering its revealed descendants preserves the parent UI; leaving restores it. An unavailable route returns `HOVER_UNAVAILABLE` by default. Enabling `systemPointerHover` permits the real system pointer fallback. An AX action failure propagates because received effects may be uncertain. The result records the route and whether the system pointer moved. System movement requires the selected foreground app and AppKit's mouse hit test, which excludes input-transparent overlays. No route grants [cross-call ownership](2026-10-13-cua-continuous-hover.md).
|
||||
By default, native hover uses a same-window element or ancestor advertising both `AXShowAlternateUI` and `AXShowDefaultUI`. Entering its revealed descendants preserves the parent UI; leaving restores it. An unavailable route returns `HOVER_UNAVAILABLE` by default. With `requireSystemPointerApproval` enabled, the first explicit `mouseMove(target, { useSystemPointer: true })` asks the user. Each physical move requires an explicit request, even after approval. The live session remembers the decision until the session closes or DSH restarts. This lets a caller reach pointer-tracking controls even when the same target also advertises AX alternate UI. The Host selects real input only for a move that requests it and meets the approval policy. Physical movement leaves the visibility of previously revealed controls to the application, so switching routes cannot dismiss a target before dispatch. An AX action failure propagates because received effects may be uncertain. The result records the route and whether the system pointer moved. System movement requires the selected foreground app and AppKit's mouse hit test, which excludes input-transparent overlays. No route grants [cross-call ownership](2026-10-13-cua-continuous-hover.md).
|
||||
|
||||
Model-facing coordinate input defaults to pixels in the returned screenshot. `relative: true` accepts integer thousandths from 0 to 999 for `click`, `mouseMove`, `scroll` and `drag`; it does not apply to opaque element IDs. The Host owns the mapping to native window points. An application-wide observation refreshes its inventory; a scoped observation preserves other windows' image mappings. Window resizing invalidates the old image, while movement preserves local coordinates. Relative endpoints remain inside the window and do not depend on screenshot resolution.
|
||||
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
原生悬停优先寻找同一窗口内同时声明 `AXShowAlternateUI` 和 `AXShowDefaultUI` 的元素或祖先。进入其展开区域的子控件时保持父控件展开,离开后恢复。没有可用路径时默认返回 `HOVER_UNAVAILABLE`。启用 `systemPointerHover` 后才允许移动系统光标。AX 动作失败直接返回,因为已产生的效果可能不确定。结果记录实际路径及是否移动系统鼠标。系统移动要求所选应用处于前台,并使用 AppKit 的鼠标命中查询排除不接收输入的覆盖窗口。所有路径都遵守[跨调用不分配所有权](2026-10-13-cua-continuous-hover.zh.md)的决定。
|
||||
默认情况下,原生悬停会寻找同一窗口内同时声明 `AXShowAlternateUI` 和 `AXShowDefaultUI` 的元素或祖先。进入其展开区域的子控件时保持父控件展开,离开后恢复。没有可用路径时默认返回 `HOVER_UNAVAILABLE`。启用 `requireSystemPointerApproval` 时,首次显式调用 `mouseMove(target, { useSystemPointer: true })` 会询问用户。即使用户已授权,每次移动系统光标仍需显式请求。会话记住授权决定,直到关闭会话或重启 DSH。有些控件既支持 AX 备用界面,又跟踪真实鼠标。只有本次调用显式请求系统鼠标并满足授权条件时,Host 才使用真实鼠标触发跟踪。切换到系统鼠标后,由应用决定是否收起先前展开的控件,以免派发前目标消失。AX 动作失败直接返回,因为已产生的效果可能不确定。结果记录实际路径及是否移动系统鼠标。系统移动要求所选应用处于前台,并使用 AppKit 的鼠标命中查询排除不接收输入的覆盖窗口。所有路径都遵守[跨调用不分配所有权](2026-10-13-cua-continuous-hover.zh.md)的决定。
|
||||
|
||||
面向模型的坐标默认使用返回截图中的像素。`click`、`mouseMove`、`scroll` 和 `drag` 传入 `relative: true` 时接受 0~999 的千分比整数,仅用于坐标目标。Host 负责换算原生窗口 point。应用级观察刷新窗口清单,单窗口观察保留其他窗口的图片映射。窗口尺寸变化使旧图片失效,移动窗口保留局部坐标。相对坐标的端点保持在窗口内,不随截图分辨率改变。
|
||||
|
||||
|
||||
+2
-2
@@ -15,5 +15,5 @@
|
||||
en: 32c970ae153e0f6c
|
||||
zh: 8431be0a10ffed15
|
||||
/agent-note-cua-scoped-surfaces-and-separated-input-capabilities/consequences:
|
||||
en: 5c6ec60b77f1bff0
|
||||
zh: e1ae568ef3501b7b
|
||||
en: c4b12ca80865e670
|
||||
zh: 396426fad0105eaa
|
||||
@@ -34,6 +34,6 @@ Within one API instance, the object facade keeps one current accessibility snaps
|
||||
|
||||
Element IDs belong to the application's current accessibility snapshot. Reading one window replaces the facade’s current snapshot for that application, so callers observe again and confirm the IDs they use. The new read does not guarantee that an old string is rejected or that an ID changes; callers must not assume an older ID still identifies the same control. Pointer input and screenshots require an explicit window object; callers enumerate windows first.
|
||||
|
||||
Keyboard behavior follows the provider's supported scopes. A caller that uses `sendKeyEvent` owns the down/up pairing, because the API never sends the opposite event. Keyboard modifier names are provider-defined, so the same string may have different support across providers.
|
||||
Keyboard behavior follows the provider's supported scopes. A caller that uses `sendKeyEvent` owns the down/up pairing, during execution. macOS releases that run’s remaining keys when the program ends or the user pauses it. Keyboard modifier names are provider-defined, so the same string may have different support across providers.
|
||||
|
||||
Provider guidance owns the list of supported actions. Adding a method to the shared schema does not claim that every provider implements it, and the merged `getCapabilities` result remains the runtime source for permissions and session lock.
|
||||
+1
-1
@@ -34,6 +34,6 @@ Provider 的启动能力是静态的。平台、支持的方法、应用级辅
|
||||
|
||||
元素 ID 属于应用当前的辅助功能快照。读取一个窗口会替换门面对该应用的当前快照,因此调用方需要重新观察并确认要使用的 ID。新的读取不保证拒绝旧字符串,也不保证 ID 会改变;调用方不得假定旧 ID 仍指向同一控件。指针输入和截图都要求显式的窗口对象;调用方先枚举窗口。
|
||||
|
||||
键盘行为遵循 provider 支持的作用域。使用 `sendKeyEvent` 的调用方负责按下与抬起的配对,因为 API 从不发送相反事件。键盘修饰键名称由 provider 定义,同一字符串在不同 provider 上可能支持不同。
|
||||
键盘行为遵循 provider 支持的作用域。使用 `sendKeyEvent` 的调用方负责按下与抬起的配对,在 macOS 上,程序结束或用户暂停时会释放本次执行仍按着的键。键盘修饰键名称由 provider 定义,同一字符串在不同 provider 上可能支持不同。
|
||||
|
||||
Provider 指导文本负责支持动作的清单。把方法加入共享 schema 并不表示每个 provider 都已实现它;合并后的 `getCapabilities` 结果仍是权限和会话锁定状态的运行时来源。
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Bilingual-pair consistency record for 2026-11-07-forge-changes-view-responsiveness.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-11-07-forge-changes-view-responsiveness.md
|
||||
/agent-note-forge-changes-view-responsiveness:
|
||||
en: e809ffea0d880a4b
|
||||
zh: 6725e6a9bd1c3bd1
|
||||
/agent-note-forge-changes-view-responsiveness/problem:
|
||||
en: 77fab27f3d4a4a59
|
||||
zh: 46e0fa8b9ef978a7
|
||||
/agent-note-forge-changes-view-responsiveness/decision:
|
||||
en: 40550cf129ab4c0c
|
||||
zh: 2e9a3597d03ce185
|
||||
/agent-note-forge-changes-view-responsiveness/alternatives-considered:
|
||||
en: c9833999164e8b67
|
||||
zh: 9459a95f307db854
|
||||
/agent-note-forge-changes-view-responsiveness/consequences:
|
||||
en: 2d74147e306ed174
|
||||
zh: f1369020f27f92c0
|
||||
@@ -0,0 +1,41 @@
|
||||
# Agent Note: Forge changes view responsiveness
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-11-07-forge-changes-view-responsiveness.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The forge changes view of a large change request froze the browser. Opening #7985 (366 files, 37,563 drawn lines) kept the main thread busy for about five seconds, "expand all" for fifteen, and every later state change, such as a poll result, a tool result, a file jump, or a keystroke in the file filter, re-rendered every file section. While the view was visible, the whole app slowed down: two subject-less `:has()` rules in `ui-sidebar-files` made each DOM change restyle the entire document, and the view's hundred thousand nodes made each restyle cost 170–260 ms, so a streaming Agent turn kept the main thread saturated.
|
||||
|
||||
## Decision
|
||||
|
||||
**Scoped `:has()` rules.** The navigator's covering rules name their row through `data-files-navigator-row`, which both hosts of `sidebar.files.navigator` set, so a DOM change outside such a row schedules no document-wide invalidation.
|
||||
|
||||
**Sections draw what changed and what the reader reaches.** `ChangesTab` passes each memoized `DiffFile` values whose identity survives its renders: one handler object, one comment-actions object, and per-file strings and booleans. Tab visibility is a prop of every section, so a visibility change redraws them all; leaving the view measured +57 ms against reading it from a React context, which the client packages forbid business components to do. The scrollport's reach tracker travels in the handler object. An expanded file holds a 22 px row per drawn line until its section first comes within 1200 px of the scrollport, then draws its lines and keeps them. After a jump, the browser's scroll anchoring keeps the target in place while the sections above it draw.
|
||||
|
||||
**Highlighting in the drawing render.** A section highlights its lines in the same render that draws them, as before, so the drawn text is always the file's current text and a native selection keeps its text nodes.
|
||||
|
||||
**Fewer serial reads.** A panel state equal to the published one reaches no follower. `changeRequestDiff` reads the patch and `.gitattributes` together. Snapshots carry the base commit as `baseSha`; `ForgeService.diff` passes it to the provider, and forge-github starts the compare read while its GraphQL read confirms the head.
|
||||
|
||||
**Pixel measurement.** Whether a section is near the scrollport decides whether React renders its lines at all, which CSS cannot express: `content-visibility` only skips layout and paint of nodes that already exist. The client packages offer no injected measurement hook, so one `IntersectionObserver` on the scrollport measures it off the main thread.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**`content-visibility: auto` alone** cut style work as much as scoping the rules did, but kept the React render and Shiki tokenization of every expanded file at open, about 3 s of script, and its placeholder sizes would need estimates for every collapsed and expanded section.
|
||||
|
||||
**Full virtualization that also unmounts far sections** would bound the DOM after long scrolls, but a section holds a comment draft and a text selection, which unmounting loses.
|
||||
|
||||
**Deferred idle-time highlighting** drew plain text and applied tokens once the page was idle and the scrollport still. It kept fast scrolling smooth, but tokens carry their own text, so a file whose content changed at the same path showed its previous text until the next pass, and replacing the plain text nodes collapsed a selection made in the meantime. Against it, highlighting while drawing raised the blocking time of scrolling all of #7985 from 22 ms to 1,634 ms and its longest frame gap from 34 ms to 817 ms; other operations stayed within 0.1 s.
|
||||
|
||||
**The CSS Custom Highlight API** would paint token colors over unchanged text nodes and keep deferral without either defect, but it cannot apply the themes' bold and italic, and its cost over tens of thousands of ranges is unmeasured.
|
||||
|
||||
**Re-aligning a jumped-to section every frame for a second** kept the last file flush with the end of the list, but it also pulled back any scroll it did not start, such as one from find in page; scroll anchoring already holds every other target at the top.
|
||||
|
||||
**Highlighting in a Web Worker** removes tokenization from the main thread entirely, including a heavy grammar's first use, but the shared highlighter in `ui-primitives` owns Shiki; moving it is a separate change for every consumer.
|
||||
|
||||
**A provider-private cache of base commits** would avoid the optional `baseSha` field, but it adds retained state without an owner that knows when a change request stops being watched.
|
||||
|
||||
## Consequences
|
||||
|
||||
Opening #7985 now takes 0.5 s of main-thread work instead of 5.2 s, and file jumps, filtering, polling, and Agent turns no longer produce long tasks. In exchange, the browser's find in page does not see lines of files the reader has not approached, and scrolling onto files in a slow grammar's language, such as C++, highlights them during the scroll, up to about 0.8 s of main-thread work when that grammar is first used. A diff read costs a second compare request when the base commit changed after the latest status.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Agent Note: Forge 变更视图的响应速度
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-11-07-forge-changes-view-responsiveness.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
大型变更请求的 forge 变更视图会让浏览器卡住。打开 #7985(366 个文件、37,563 个绘制行)时主线程忙约 5 秒,“全部展开”约 15 秒;之后的每次状态变化,例如轮询结果、工具结果、跳转文件或在文件筛选框中按键,都会重渲染所有文件段。视图可见时整个应用都变慢:`ui-sidebar-files` 中两条没有主体的 `:has()` 规则让每次 DOM 变化都重算整个文档的样式,而视图的十万个节点让每次重算耗时 170–260 ms,所以 Agent 流式输出的回合会让主线程一直饱和。
|
||||
|
||||
## Decision
|
||||
|
||||
**限定 `:has()` 规则。** 导航器的遮盖规则通过 `data-files-navigator-row` 指定所在行,`sidebar.files.navigator` 的两个宿主都会设置它,因此这类行之外的 DOM 变化不再触发全文档失效。
|
||||
|
||||
**文件段只绘制变化的部分和读者接近的部分。** `ChangesTab` 传给每个 memo 化的 `DiffFile` 的值在多次渲染之间保持同一标识:一个处理函数对象、一个评论操作对象,以及每个文件的字符串和布尔值。标签页可见性作为每个文件段的参数传入,可见性变化时全部文件段都会重绘;离开视图实测比从 React context 读取多 57 ms,而客户端包禁止业务组件读取 context。滚动区域的接近跟踪对象放在处理函数对象中传递。展开的文件在它的段第一次进入滚动区域 1200 px 范围之前,按每个绘制行 22 px 占位,之后绘制各行并一直保留。跳转后,上方文件段绘制时,浏览器的滚动锚定让目标保持原位。
|
||||
|
||||
**在绘制的渲染中高亮。** 文件段与修复前一样,在绘制各行的同一次渲染中完成高亮,所以画出的始终是文件当前的文本,原生选区所在的文本节点也不会被替换。
|
||||
|
||||
**减少串行读取。** 与已发布状态相同的面板状态不会通知任何关注方。`changeRequestDiff` 同时读取补丁和 `.gitattributes`。快照把基准提交作为 `baseSha` 携带;`ForgeService.diff` 把它传给提供方,forge-github 在 GraphQL 读取确认 head 的同时就开始 compare 读取。
|
||||
|
||||
**像素测量。** 文件段是否接近滚动区域,决定 React 是否渲染它的各行,这一点 CSS 无法表达:`content-visibility` 只能跳过已有节点的布局与绘制。客户端包没有可注入的测量 hook,所以在滚动区域上用一个 `IntersectionObserver`,在主线程之外测量。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**只用 `content-visibility: auto`** 减少的样式工作与限定规则相当,但打开时仍要对每个展开文件做 React 渲染和 Shiki 分词,约 3 秒脚本时间,而且每个折叠和展开的段都需要估计占位尺寸。
|
||||
|
||||
**同时卸载远处文件段的完整虚拟化** 可以在长距离滚动后限制 DOM 规模,但文件段持有评论草稿和文本选区,卸载会丢失它们。
|
||||
|
||||
**空闲时延迟高亮** 先画纯文本,等页面空闲且滚动区域静止后再应用 token。它让快速滚动保持流畅,但 token 自带文本,同一路径内容变化后会显示旧文本直到下一轮高亮完成;替换纯文本节点还会清掉期间做出的选区。与它相比,绘制时高亮让滚动整个 #7985 的阻塞时间从 22 ms 增到 1,634 ms,最长帧间隔从 34 ms 增到 817 ms;其他操作的差异在 0.1 秒以内。
|
||||
|
||||
**CSS Custom Highlight API** 可以在不变的文本节点上绘制 token 颜色,保留延迟高亮又没有上述两个问题,但它无法应用主题中的粗体和斜体,几万个范围的绘制成本也没有测过。
|
||||
|
||||
**跳转后一秒内每帧重新对齐** 能让最后一个文件贴紧列表末端,但它也会把不是它发起的滚动拉回去,例如页内查找引起的滚动;而对其他目标,滚动锚定已经能让它们保持在顶部。
|
||||
|
||||
**在 Web Worker 中高亮** 能把分词完全移出主线程,包括重型语法的首次使用,但 Shiki 由 `ui-primitives` 中的共享高亮器持有;迁移它是影响所有使用方的另一项改动。
|
||||
|
||||
**由提供方私有缓存基准提交** 可以省去可选的 `baseSha` 字段,但会引入保留状态,而且没有一个知道变更请求何时不再被 watch 的所有者。
|
||||
|
||||
## Consequences
|
||||
|
||||
打开 #7985 的主线程工作从 5.2 秒降到 0.5 秒,跳转文件、筛选、轮询和 Agent 回合不再产生长任务。代价是:浏览器的页内查找看不到读者尚未接近的文件中的行;滚动到慢语法(例如 C++)的文件时会在滚动中完成高亮,该语法首次使用时最多约 0.8 秒的主线程工作。最新状态之后基准提交有变化时,一次 diff 读取会多一次 compare 请求。
|
||||
@@ -18,11 +18,11 @@
|
||||
en: 16c4ee08b65ead23
|
||||
zh: df80d6e49982615b
|
||||
/agent-note-parallel-tool-call-execution-by-per-call-safety/configuration-and-declarations:
|
||||
en: e112957ed8a48039
|
||||
zh: 37d63f9ea4968fb2
|
||||
en: 082e5a02b092b656
|
||||
zh: ca674a08323d8cc1
|
||||
/agent-note-parallel-tool-call-execution-by-per-call-safety/verification:
|
||||
en: 224b6838130c113f
|
||||
zh: f3d7eb68ab5932fb
|
||||
en: 20d0e1724ebc1247
|
||||
zh: baa89694cea135c8
|
||||
/agent-note-parallel-tool-call-execution-by-per-call-safety/alternatives-considered:
|
||||
en: 17becc038bb7df7c
|
||||
zh: 673d9a9bdfa39edf
|
||||
|
||||
@@ -60,13 +60,13 @@ Any shared state touched during execution must be concurrency-safe. This include
|
||||
|
||||
`maxParallelToolCalls` is a positive AgentLoop deployment cap shared by every agent the factory creates. It defaults to `10`; `1` preserves serial execution. Exact fields and defaults live in the generated [configuration catalog](../../../../docs/config-catalog.md).
|
||||
|
||||
The shipped declarations are conservative. Web search, web fetch, filesystem read, the session-query trace/read tools, and subagent delegation opt in — delegation because a child works in its own session and its run never mutates the parent session, with sibling workspace coordination owned by the model ([parallel subagent Agent Note](../../archived/feature/2026-08-09-parallel-subagent-delegations.md)). Filesystem writes and edits, bash tools, the session-query search tools, workflow, user interaction, todo mutation, PTC mode, and Cordis mutation tools remain exclusive. Bash has no proven input-sensitive classifier and remains exclusive.
|
||||
The shipped declarations are conservative. Web search, web fetch, filesystem read, glob, grep, the session-query trace/read tools, and subagent delegation opt in — delegation because a child works in its own session and its run never mutates the parent session, with sibling workspace coordination owned by the model ([parallel subagent Agent Note](../../archived/feature/2026-08-09-parallel-subagent-delegations.md)). Read-only queries also opt in: the background job list, the subagent tree and model listings, scheduled task and Team task reads, Cordis Inspect queries, and MCP resource reads. Filesystem writes and edits, bash tools, `job_output` because it consumes a read cursor, the session-query search tools, workflow, user interaction, todo mutation, PTC mode, and Cordis mutation tools remain exclusive. Bash has no proven input-sensitive classifier and remains exclusive. Read-only `lsp` stays exclusive because its language-server pool already serializes queries per workspace, so overlap would gain nothing, and the goal tools stay exclusive under the [goal tools Agent Note](2026-07-19-model-facing-goal-tools.md).
|
||||
|
||||
Filesystem read relies on a narrow recorder exception: its synchronous observation updates may settle out of order, but write and edit re-check the observed version before mutation, so stale state only produces `FS_STALE_VERSION`.
|
||||
Filesystem read relies on a narrow recorder exception: its synchronous observation updates may settle out of order, but write and edit re-check the observed version before mutation, so stale state only produces `FS_STALE_VERSION`. Filesystem read, glob, and grep also share a working-directory recovery exception: when the Session's recorded directory is unset or missing, the body's `ctx.workingDirectory.ensure()` appends `working-directory/change` and, when it restores a missing directory, injects a model-visible notice; the working-directory service serializes that recovery per Session.
|
||||
|
||||
## Verification
|
||||
|
||||
Unit coverage pins fail-closed classification, typed argument validation, grouping, barriers, rejection of unstarted calls after registry replacement, the rolling cap, distinct execution objects, middleware order, ordered results and context, abort draining, and scheduler-failure quiescence. First-party tests pin each parallel declaration.
|
||||
Unit coverage pins fail-closed classification, typed argument validation, grouping, barriers, rejection of unstarted calls after registry replacement, the rolling cap, distinct execution objects, middleware order, ordered results and context, abort draining, and scheduler-failure quiescence. First-party tests pin each parallel declaration, and the working-directory service tests pin per-Session serialization of concurrent directory changes.
|
||||
|
||||
Snapshot coverage pins the visible multi-call transcript: pending calls may overlap while completed results remain model-ordered. PTC mode coverage pins its serial boundary. No provider-backed e2e is required because scheduling is deterministic loop behavior.
|
||||
|
||||
|
||||
@@ -60,13 +60,13 @@ Status: implemented
|
||||
|
||||
`maxParallelToolCalls` 是 AgentLoop 的正整数部署上限,由工厂创建的所有 agent(智能体)共享。默认值为 `10`;`1` 保持串行执行。字段和默认值的精确定义见生成的[配置目录](../../../../docs/config-catalog.zh.md)。
|
||||
|
||||
当前实现中的声明保持保守。Web 搜索、Web 获取、文件系统读取、会话查询的 trace/read 工具和 subagent 委派选择并行;委派之所以并行,是因为子 agent 在自己的会话中工作,其运行绝不变更父会话,并列委派间的工作区协调由模型负责([并行 subagent Agent Note](../../archived/feature/2026-08-09-parallel-subagent-delegations.md))。文件系统写入与编辑、bash 工具、会话查询的 search 工具、工作流、用户交互、todo 变更、PTC mode 以及 Cordis 变更工具仍按独占方式执行。Bash 没有已证明的输入敏感分类器,因此仍按独占方式执行。
|
||||
当前实现中的声明保持保守。Web 搜索、Web 获取、文件系统读取、glob、grep、会话查询的 trace/read 工具和 subagent 委派选择并行;委派之所以并行,是因为子 agent 在自己的会话中工作,其运行绝不变更父会话,并列委派间的工作区协调由模型负责([并行 subagent Agent Note](../../archived/feature/2026-08-09-parallel-subagent-delegations.md))。只读查询也选择并行:后台任务列表、subagent 树与模型列表、定时任务与 Team 任务读取、Cordis Inspect 查询以及 MCP 资源读取。文件系统写入与编辑、bash 工具、`job_output`(因为它会消费读取游标)、会话查询的 search 工具、工作流、用户交互、todo 变更、PTC mode 以及 Cordis 变更工具仍按独占方式执行。Bash 没有已证明的输入敏感分类器,因此仍按独占方式执行。只读的 `lsp` 仍按独占方式执行,因为其语言服务器池已按工作区串行化查询,重叠执行不会带来收益;goal 工具依照 [goal 工具 Agent Note](2026-07-19-model-facing-goal-tools.zh.md) 仍按独占方式执行。
|
||||
|
||||
文件系统读取依赖一个范围很窄的记录器例外:其同步观察更新可以不按顺序结算,但写入和编辑在变更前会重新检查已观察的版本,因此陈旧状态只会导致 `FS_STALE_VERSION`。
|
||||
文件系统读取依赖一个范围很窄的记录器例外:其同步观察更新可以不按顺序结算,但写入和编辑在变更前会重新检查已观察的版本,因此陈旧状态只会导致 `FS_STALE_VERSION`。文件系统读取、glob 和 grep 还共享一个工作目录恢复例外:当会话记录的目录未设置或不存在时,工具主体中的 `ctx.workingDirectory.ensure()` 会追加 `working-directory/change`,并在恢复不存在的目录时注入一条模型可见的通知;工作目录服务按会话串行化该恢复过程。
|
||||
|
||||
## 验证
|
||||
|
||||
单元测试固定了按安全侧原则进行的分类、类型化参数验证、分组、屏障、注册表替换后拒绝尚未开始的调用、滚动上限、独立执行对象、中间件顺序、有序结果与上下文、中止排空,以及调度器故障后的完全停稳。第一方测试固定了每项并行声明。
|
||||
单元测试固定了按安全侧原则进行的分类、类型化参数验证、分组、屏障、注册表替换后拒绝尚未开始的调用、滚动上限、独立执行对象、中间件顺序、有序结果与上下文、中止排空,以及调度器故障后的完全停稳。第一方测试固定了每项并行声明,工作目录服务测试固定了按会话串行化并发目录变更。
|
||||
|
||||
快照测试固定了可见的多调用 transcript(文本记录):待处理调用可以重叠执行,已完成结果仍按模型顺序排列。PTC mode 测试固定了其串行边界。此调度属于确定性循环行为,因此无需依赖提供方的 e2e 测试。
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
en: 12ab239ab0c58e31
|
||||
zh: c32874442ce66041
|
||||
/agent-note-explicit-worktree-creation/decision:
|
||||
en: 92782d5964d1a021
|
||||
zh: 37b385836e9ae2b9
|
||||
en: 571a2d4c5352170b
|
||||
zh: aeb12b849c5f8d31
|
||||
/agent-note-explicit-worktree-creation/alternatives-considered:
|
||||
en: 72d42e892a37b94c
|
||||
zh: 92a14a51c6a89c53
|
||||
|
||||
@@ -18,7 +18,7 @@ Git and directory creation use the mounted subprocess and sandbox providers with
|
||||
|
||||
Leaving uses the shared working-directory operation and retains the checkout and branch. Failed setup can retain partial artifacts and reports their location. The service does not own a branch merge policy or a task association database.
|
||||
|
||||
The service records each creation as a `plugin:worktree/created` record and owns archive-triggered cleanup of those checkouts, as the job and Agent registries own their archive-admission families. A Session's inherited prefix decides whether it created a checkout or merely refers to one; cwd alone never grants cleanup ownership. Admission registers the attempt inside the Workspace write queue and returns; inspection reads cold Sessions through the projection cache or their log, without starting an Agent, and runs afterwards. Only a clean, unreferenced checkout is removed, without force and without deleting its branch; tracked changes and untracked files retain it, ignored files do not, matching Git's own non-force refusal. Processes and terminals are not consulted. A volatile `retainOnArchive` setting, read at admission, turns the cleanup off without unmounting the service; the Forge client edits it on the bundle's worktree row of the Plugins page because the service package has no Client half. Unarchiving cancels an unadmitted removal and waits for an admitted one, and directory entry into a checkout being removed waits for that removal. No startup sweep deletes previously archived worktrees.
|
||||
The service records each creation as a `plugin:worktree/created` record and owns archive-triggered cleanup of those checkouts, as the job and Agent registries own their archive-admission families. A Session's inherited prefix decides whether it created a checkout or merely refers to one; cwd alone never grants cleanup ownership. Admission registers the attempt inside the Workspace write queue and returns; inspection reads cold Sessions through the projection cache or their log, without starting an Agent, and runs afterwards. Only a clean, unreferenced checkout is removed, without force and without deleting its branch; tracked changes and untracked files retain it, ignored files do not, matching Git's own non-force refusal. Inspection trusts `git status` alone, as peer agent products do: edits to skip-worktree or assume-unchanged files are discarded with the checkout, because sparse checkout sets skip-worktree on every file outside its cone and inspecting those bits would retain every sparse checkout. A checkout whose administrative directory holds submodule repositories is retained; a committed embedded repository is not inspected, and Git's refusal of that removal settles the attempt as failed. Processes and terminals are not consulted. A volatile `retainOnArchive` setting, read at admission, turns the cleanup off without unmounting the service; the Forge client edits it on the bundle's worktree row of the Plugins page because the service package has no Client half. Unarchiving cancels an unadmitted removal and waits for an admitted one, and directory entry into a checkout being removed waits for that removal. No startup sweep deletes previously archived worktrees.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ Git 与目录创建使用挂载的子进程及沙箱提供方,不扩大调用
|
||||
|
||||
离开使用共享工作目录操作,保留检出目录与分支。初始化失败可能留下部分产物,并报告其位置。该服务不拥有分支合并策略或任务关联数据库。
|
||||
|
||||
服务把每次创建记录为 `plugin:worktree/created` Session 事件,并像 job 与 Agent 注册表各自拥有归档准入家族那样,拥有这些检出目录的归档触发清理。Session 的继承前缀决定它创建了某个检出目录还是仅引用它;当前工作目录本身不授予清理所有权。准入在 Workspace 写队列内登记尝试并返回;检查通过投影缓存或日志读取冷 Session,不启动 Agent,并在之后执行。只删除干净且无人引用的检出目录,不带 force 也不删除分支;跟踪文件改动与未跟踪文件保留目录,被忽略的文件不保留,与 Git 自身不带 force 时的拒绝条件一致。不咨询进程与终端。volatile 的 `retainOnArchive` 设置在准入时读取,无需卸载服务即可关闭清理;由于服务包没有 Client 半部,Forge 客户端在插件页该套件的 worktree 行上编辑它。取消归档会取消尚未获准的删除并等待已获准的删除,进入正在删除的检出目录会等待该删除结束。不在启动时扫描删除此前已归档的工作树。
|
||||
服务把每次创建记录为 `plugin:worktree/created` Session 事件,并像 job 与 Agent 注册表各自拥有归档准入家族那样,拥有这些检出目录的归档触发清理。Session 的继承前缀决定它创建了某个检出目录还是仅引用它;当前工作目录本身不授予清理所有权。准入在 Workspace 写队列内登记尝试并返回;检查通过投影缓存或日志读取冷 Session,不启动 Agent,并在之后执行。只删除干净且无人引用的检出目录,不带 force 也不删除分支;跟踪文件改动与未跟踪文件保留目录,被忽略的文件不保留,与 Git 自身不带 force 时的拒绝条件一致。检查与同类 Agent 产品一样只依据 `git status`:skip-worktree 或 assume-unchanged 文件的改动随检出目录一起丢弃,因为 sparse checkout 会给稀疏范围之外的每个文件设置 skip-worktree,检查这些标记会保留每个稀疏检出目录。管理目录中含子模块仓库的检出目录会保留;已提交的内嵌仓库不在检查范围内,Git 拒绝这类删除,本次尝试以失败结束。不咨询进程与终端。volatile 的 `retainOnArchive` 设置在准入时读取,无需卸载服务即可关闭清理;由于服务包没有 Client 半部,Forge 客户端在插件页该套件的 worktree 行上编辑它。取消归档会取消尚未获准的删除并等待已获准的删除,进入正在删除的检出目录会等待该删除结束。不在启动时扫描删除此前已归档的工作树。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
en: 344bcc7e9825bbc8
|
||||
zh: c1d0f4b54f5b3786
|
||||
/agent-note-change-request-panel-and-worktree-sessions/decision:
|
||||
en: 66bde24e7c601eb6
|
||||
zh: 5091591f3dc1ac85
|
||||
en: 4695a1e4f8aa7a25
|
||||
zh: e17be0e36ea3eaa3
|
||||
/agent-note-change-request-panel-and-worktree-sessions/deferred:
|
||||
en: 32ef8af85f4d2f66
|
||||
zh: b82b67f94346399e
|
||||
|
||||
@@ -28,7 +28,7 @@ The panel does not move a Session between existing worktrees. Removing a change
|
||||
|
||||
The panel also shows the changes behind a Session's work in a changes view, a resource tab type of this package. The Remote parses every diff on the Host into files and hunks, so the browser draws them without a patch parser, and bounds each read with `maxDiffBytes` and `maxUntrackedFiles`. Local changes come from `git diff` using the Session's permissions and execution world, in a scope the user picks: the branch against its fork point by default, or the uncommitted, unstaged, or staged changes; untracked files are read through the Session's filesystem and shown as new files. A change request's changes come from its code host at the head of the binding's latest status, through two provider reads the forge seam gained, `diff` and `fileText`. The Host keeps the patch it read last for each change request, keyed by the head commit and base branch and bounded by `maxCachedDiffBytes`, so reopening, refreshing, and navigating at the same head download nothing again; a host or provider that refuses an answer over its size limit reports the forge failure `too-large`, and the view then offers the change request's web page instead of a retry. Unchanged lines between hunks expand from the file's new side, which is the working tree, the index, or the head commit, and only while that text still matches the hunks. Each inspection also sizes the branch's changes for a local changes card, shown while no open change request carries the checked-out branch; its Create change request sends the Session a user message, so the Agent creates the change request with its own tools and the binding follows from `gh pr create` as for any other.
|
||||
|
||||
Review comments capture the file, side, range, complete hunk, and reviewed head when the user selects code. Agent references enter the existing composer quote mechanism, so the user controls batching and the sent message records exactly what the model receives. GitHub writes belong to the optional forge provider operation: preflight checks the current head, the write addresses that commit, and an ambiguous outcome blocks another write until the user reconciles it. Review drafts and inline records belong to a browser tab occurrence, not to the Session log or a GitHub pending review. Switching Sessions retains that occurrence; closing it discards the local state and rejects late write completions. Published GitHub comments remain external records.
|
||||
Review comments capture the file, side, range, complete hunk, and reviewed head when the user selects code. Agent references enter the existing composer quote mechanism, so the user controls batching and the sent message records exactly what the model receives. GitHub writes belong to the optional forge provider operation: preflight checks the current head, the write addresses that commit, and an ambiguous outcome blocks another write until the user reconciles it. Inline records of Agent references and published comments, and the review summary, belong to a browser tab occurrence, not to the Session log; a pull request comment kept for a later review goes to the code host's pending review, as [the pending review decision](2026-11-05-forge-pending-review-drafts.md) records. Switching Sessions retains that occurrence; closing it discards the local state and rejects late write completions. Published GitHub comments remain external records.
|
||||
|
||||
## Deferred
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ Session 所在的 worktree 可能在 Session 之外被删除。每次检查都
|
||||
|
||||
面板还通过变更视图展示 Session 工作背后的变更;变更视图是本包的一个资源标签页类型。Remote 在宿主上把每个 diff 解析成文件与 hunk,因此浏览器无需补丁解析器即可绘制,每次读取都受 `maxDiffBytes` 与 `maxUntrackedFiles` 约束。本地变更来自使用 Session 权限与执行环境的 `git diff`,范围由用户选择:默认是分支相对于分叉点的变更,也可以是未提交、未暂存或已暂存的变更;未跟踪文件经 Session 的文件系统读取,显示为新文件。变更请求的变更来自代码托管平台,取自绑定最新状态中的 head,经 forge seam 新增的两个提供方读取操作 `diff` 与 `fileText` 获得。Host 为每个变更请求保留最近读到的补丁,按 head 提交与目标分支区分,总量受 `maxCachedDiffBytes` 约束,因此在同一 head 下重新打开、刷新与导航都不会再次下载;托管平台或提供方因结果超过其大小上限而拒绝时报告 forge 失败 `too-large`,视图随之提供变更请求的网页而不是重试。hunk 之间未变更的行从文件的新版本展开,即工作树、暂存区或 head 提交,并且只在该文本仍与 hunk 吻合时展开。每次检查还会为变更卡片统计分支的变更规模;没有打开中的变更请求以当前检出分支为源分支时才显示该卡片。卡片上的“创建变更请求”向 Session 发送一条用户消息,由 Agent 用自己的工具创建变更请求,绑定像其他变更请求一样来自 `gh pr create`。
|
||||
|
||||
评审评论在用户选择代码时固定文件、侧、行范围、完整 hunk 与所评审的 head。Agent 引用进入既有输入框引用机制,由用户决定批量发送,发送后的消息记录模型实际收到的内容。GitHub 写入属于 forge 提供方的可选操作:预检核对当前 head,写入明确指向该提交;结果不明时阻止再次写入,直到用户核对。评审草稿与行内记录归浏览器页签实例所有,不进入 Session 日志,也不创建 GitHub 待提交评审。切换 Session 保留该实例;关闭时丢弃本地状态并拒绝迟到的写入结果。已发布的 GitHub 评论仍保留在外部平台。
|
||||
评审评论在用户选择代码时固定文件、侧、行范围、完整 hunk 与所评审的 head。Agent 引用进入既有输入框引用机制,由用户决定批量发送,发送后的消息记录模型实际收到的内容。GitHub 写入属于 forge 提供方的可选操作:预检核对当前 head,写入明确指向该提交;结果不明时阻止再次写入,直到用户核对。Agent 引用与已发布评论的行内记录以及评审总结归浏览器页签实例所有,不进入 Session 日志;留待之后一起提交的 PR 评论写入代码托管平台的待提交评审,见[待提交评审决策](2026-11-05-forge-pending-review-drafts.zh.md)。切换 Session 保留该实例;关闭时丢弃本地状态并拒绝迟到的写入结果。已发布的 GitHub 评论仍保留在外部平台。
|
||||
|
||||
## Deferred
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Bilingual-pair consistency record for 2026-11-05-forge-pending-review-drafts.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-11-05-forge-pending-review-drafts.md
|
||||
/agent-note-pull-request-comment-drafts-in-the-code-host-s-pending-review:
|
||||
en: 25b4da2d12f15b82
|
||||
zh: 6d9fec68e8bb0baa
|
||||
/agent-note-pull-request-comment-drafts-in-the-code-host-s-pending-review/problem:
|
||||
en: 2674866671bd0d4b
|
||||
zh: e60d41a24d20d7bf
|
||||
/agent-note-pull-request-comment-drafts-in-the-code-host-s-pending-review/decision:
|
||||
en: 3df55965423b6d33
|
||||
zh: 2e8f0fbb4f7a311f
|
||||
/agent-note-pull-request-comment-drafts-in-the-code-host-s-pending-review/alternatives-considered:
|
||||
en: ef67e05cb9442f05
|
||||
zh: d4433ddd090a9cec
|
||||
/agent-note-pull-request-comment-drafts-in-the-code-host-s-pending-review/consequences:
|
||||
en: 198f6cf5a2bc2230
|
||||
zh: 92eb0a349356f100
|
||||
@@ -0,0 +1,31 @@
|
||||
# Agent Note: Pull request comment drafts in the code host's pending review
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-11-05-forge-pending-review-drafts.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The [forge panel](2026-10-09-forge-panel.md) kept a pull request's comment drafts in a browser tab occurrence, published them as one review, and recorded what it published. Once the changes view shows the code host's review threads and lets the user reply to them, browser drafts leave four gaps. A reply cannot join a later review, because the drafts and the threads live in different places. A review the user started on the GitHub page, or in another browser, does not appear, and GitHub refuses a review submitted while the user has one. Drafts are lost with the tab and invisible on another device. And GitHub keeps one pending review per user and pull request, so two stores of one review would disagree.
|
||||
|
||||
## Decision
|
||||
|
||||
A pull request comment that the user keeps for a later review goes to the user's pending review on the code host through the provider's optional `writeReview`. The first such comment or reply starts the pending review at the head the user saw; later ones join it. The changes view reads the pending review with the threads, shows its comments as threads tagged Pending, and Submit review submits or discards that review with the summary and decision. Replies, resolving and unresolving, and editing and deleting the user's own comments use the same write, which re-checks the head and is never retried.
|
||||
|
||||
The browser keeps what the code host does not hold: the review summary and decision until submission, the records of Agent references and published comments, and the outgoing direct write whose outcome needs confirmation. Comments on local changes never had a host destination; they go to the main input as before. Comment drafts an earlier panel kept in the browser stay readable: they show beside their code as browser drafts, can move into the pending review, and keep Submit review disabled while a pending review exists; without one, Submit review publishes them as one review as before.
|
||||
|
||||
GitHub measurements on a private test repository decided three rules. While the user has a pending review, GitHub refuses a directly created review and a single comment (HTTP 422, "can only have one pending review per pull request"), so the view offers neither; a reply sent without the review's id still joins the review, so the view offers no reply at once and the provider refuses one. GitHub refuses Approve and Request changes on the user's own pull request, so the dialog offers only Comment there.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keeping drafts in the browser tab.** It needs no host write until submission, but replies cannot join the same review, a review started elsewhere blocks submission without explanation, and the drafts do not follow the user to another device.
|
||||
|
||||
**Keeping both stores.** Mirroring browser drafts into the pending review or the reverse gives one review two owners whose contents can diverge, and every edit would need reconciliation of two writes.
|
||||
|
||||
**Recording drafts in the Session log.** The log records what reaches the model; review drafts are host state the model never sees, and other Sessions on the same pull request would not see them.
|
||||
|
||||
**Retrying a write after an unknown outcome.** A lost response does not show whether GitHub applied the mutation. A submission of the pending review is kept as the write to reconcile, as a direct submission is, and blocks another submission until the user checks GitHub; a reply, resolution, or comment change reads the threads again and asks the user to check them before trying again.
|
||||
|
||||
## Consequences
|
||||
|
||||
A comment kept for later is visible on the GitHub page to its author before submission, and is lost only when the user discards the review. Each comment, reply, or thread change costs one GraphQL query for the head check and one mutation. Resolving a thread does not change the pull request's update time, so every write from the panel drops the panel's kept threads; another view of the same pull request shows the change at its next status or after `reviewRefreshMs`. A provider without `writeReview` reports no write permissions, so its threads stay read-only and its pull request comments are published at once or added to the input. Comment drafts from an earlier panel remain until the user moves, publishes, or deletes them, or closes their tab.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Agent Note: PR 评论草稿保存在代码托管平台的待提交评审中
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-11-05-forge-pending-review-drafts.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
[forge 面板](2026-10-09-forge-panel.zh.md)原先把 PR 的评论草稿保存在浏览器页签实例中,作为一次评审发布,并记录已发布的内容。变更视图显示代码托管平台的评审讨论并允许用户回复之后,浏览器草稿留下四个缺口。回复无法加入之后的评审,因为草稿与讨论保存在不同的地方。用户在 GitHub 页面或另一个浏览器中开始的评审不会显示,而用户已有待提交评审时,GitHub 会拒绝直接提交的评审。草稿随页签丢失,在其他设备上也看不到。GitHub 每个用户在每个 PR 上只保留一个待提交评审,两处保存同一个评审会出现不一致。
|
||||
|
||||
## Decision
|
||||
|
||||
用户留待之后一起提交的 PR 评论,通过提供方的可选能力 `writeReview` 写入代码托管平台上该用户的待提交评审。第一条这样的评论或回复在用户看到的 head 上开始待提交评审,之后的评论或回复加入其中。变更视图随讨论一起读取待提交评审,把其中的评论显示为带“待提交”标签的讨论;“提交评审”带着总结和评审决定提交或丢弃这个评审。回复、解决与取消解决、编辑与删除用户自己的评论也使用同一写操作;写操作会重新检查 head,且从不重试。
|
||||
|
||||
浏览器只保存代码托管平台不保存的内容:提交前的评审总结与评审决定、Agent 引用与已发布评论的记录,以及结果有待确认的直接写入。本地变更上的评论本来就没有托管平台上的去处,仍像以前一样进入主输入框。早期面板保存在浏览器中的评论草稿仍可读取:它们作为浏览器草稿显示在对应代码旁,可以加入待提交评审;存在待提交评审时,它们会让“提交评审”保持不可用;没有待提交评审时,“提交评审”像以前一样把它们作为一次评审发布。
|
||||
|
||||
在一个私有测试仓库上对 GitHub 的实测确定了三条规则。用户已有待提交评审时,GitHub 拒绝直接创建的评审和单条评论(HTTP 422,“每个 PR 只能有一个待提交评审”),因此视图两者都不提供;不带评审 id 发送的回复仍会加入该评审,因此视图不提供立即回复,提供方也拒绝这种回复。GitHub 拒绝用户批准或请求更改自己的 PR,因此在这种 PR 上,弹窗只提供“评论”。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**把草稿保存在浏览器页签中。** 提交之前不需要写入托管平台,但回复无法加入同一个评审,在别处开始的评审会无说明地阻止提交,草稿也无法随用户换到另一台设备。
|
||||
|
||||
**两处同时保存。** 把浏览器草稿同步到待提交评审,或反过来同步,会让一个评审有两个所有者,内容可能分歧,每次编辑都要核对两次写入。
|
||||
|
||||
**把草稿记录在 Session 日志中。** 日志记录的是到达模型的内容;评审草稿是模型永远看不到的托管平台状态,同一 PR 上的其他 Session 也看不到它们。
|
||||
|
||||
**结果不明时自动重试写操作。** 响应丢失并不能说明 GitHub 是否执行了 mutation。提交待提交评审时,与直接提交一样,这次提交会作为待核对的写入保留下来,在用户到 GitHub 核对之前阻止再次提交;回复、解决讨论或修改评论时,视图重新读取讨论,并请用户核对后再重试。
|
||||
|
||||
## Consequences
|
||||
|
||||
留待之后提交的评论,在提交前其作者就能在 GitHub 页面上看到,只有用户丢弃评审时才会丢失。每条评论、回复或讨论状态的修改,都消耗一次用于检查 head 的 GraphQL 查询和一个 mutation。解决讨论不会改变 PR 的更新时间,因此面板发出的每个写操作都会丢弃面板保留的讨论;同一 PR 的其他视图在收到下一次状态时,或在 `reviewRefreshMs` 之后显示这一变化。未实现 `writeReview` 的提供方不报告任何写入权限,因此它的讨论保持只读,它的 PR 评论只能立即发布或添加到输入框。早期面板留下的评论草稿会一直保留,直到用户把它们加入评审、发布、删除,或关闭所在页签。
|
||||
@@ -16,3 +16,12 @@ docs/persistence-changes/historical-formats/*.schema.json linguist-generated=tru
|
||||
|
||||
# Unified patch blank context lines require their leading space.
|
||||
patches/*.patch whitespace=-blank-at-eol
|
||||
|
||||
# Review layers of the forge changes view (dsh-review-layer), for this repository's own conventions:
|
||||
# plugin locale dictionaries and the session snapshot tree hide with test files, generated bilingual
|
||||
# pairing records join them, and a package's types-only module is part of its interface.
|
||||
packages/*/*/locale/*.json dsh-review-layer=test
|
||||
locales.ts dsh-review-layer=test
|
||||
snapshots/** dsh-review-layer=test
|
||||
*.i18n.yaml dsh-review-layer=test
|
||||
packages/*/*/src/types.ts dsh-review-layer=interface
|
||||
+2
-2
@@ -17,8 +17,8 @@
|
||||
{"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Delegate the write probe to a subagent."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"<system-reminder>\nThe following information describes your current session:\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names.\n</system-reminder>"}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","reasoningEffort":"low"},"tools":["apply_patch","bash","create_goal","edit","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","wait_agent","web_fetch","web_search","workflow","working_directory","write"]},"reason":"resume","startsSeries":true}}
|
||||
{"type":"developer/message","data":{"turn":2,"step":1,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"apply_patch"},{"type":"tool-addition","toolName":"bash"},{"type":"tool-addition","toolName":"create_goal"},{"type":"tool-addition","toolName":"edit"},{"type":"tool-addition","toolName":"get_goal"},{"type":"tool-addition","toolName":"glob"},{"type":"tool-addition","toolName":"grep"},{"type":"tool-addition","toolName":"interrupt_agent"},{"type":"tool-addition","toolName":"job_kill"},{"type":"tool-addition","toolName":"job_list"},{"type":"tool-addition","toolName":"job_output"},{"type":"tool-addition","toolName":"list_agents"},{"type":"tool-addition","toolName":"read"},{"type":"tool-addition","toolName":"read_image"},{"type":"tool-addition","toolName":"send_message"},{"type":"tool-addition","toolName":"skill"},{"type":"tool-addition","toolName":"subagent"},{"type":"tool-addition","toolName":"subagent_fork"},{"type":"tool-addition","toolName":"todo_write"},{"type":"tool-addition","toolName":"update_goal"},{"type":"tool-addition","toolName":"wait_agent"},{"type":"tool-addition","toolName":"web_fetch"},{"type":"tool-addition","toolName":"web_search"},{"type":"tool-addition","toolName":"workflow"},{"type":"tool-addition","toolName":"working_directory"},{"type":"tool-addition","toolName":"write"}],"role":"developer","id":"{{sessionId}}"},"headerSeq":18},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","reasoningEffort":"low"},"tools":["apply_patch","bash","create_goal","edit","get_goal","glob","grep","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","wait_agent","web_fetch","web_search","workflow","working_directory","write"]},"reason":"resume","startsSeries":true}}
|
||||
{"type":"developer/message","data":{"turn":2,"step":1,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"apply_patch"},{"type":"tool-addition","toolName":"bash"},{"type":"tool-addition","toolName":"create_goal"},{"type":"tool-addition","toolName":"edit"},{"type":"tool-addition","toolName":"get_goal"},{"type":"tool-addition","toolName":"glob"},{"type":"tool-addition","toolName":"grep"},{"type":"tool-addition","toolName":"job_kill"},{"type":"tool-addition","toolName":"job_list"},{"type":"tool-addition","toolName":"job_output"},{"type":"tool-addition","toolName":"list_agents"},{"type":"tool-addition","toolName":"read"},{"type":"tool-addition","toolName":"read_image"},{"type":"tool-addition","toolName":"send_message"},{"type":"tool-addition","toolName":"skill"},{"type":"tool-addition","toolName":"subagent"},{"type":"tool-addition","toolName":"subagent_fork"},{"type":"tool-addition","toolName":"todo_write"},{"type":"tool-addition","toolName":"update_goal"},{"type":"tool-addition","toolName":"wait_agent"},{"type":"tool-addition","toolName":"web_fetch"},{"type":"tool-addition","toolName":"web_search"},{"type":"tool-addition","toolName":"workflow"},{"type":"tool-addition","toolName":"working_directory"},{"type":"tool-addition","toolName":"write"}],"role":"developer","id":"{{sessionId}}"},"headerSeq":18},"surfaceOp":"append"}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"session/title","data":{"title":"Tighten this session to read-only.","messageSeqs":[3],"source":{"kind":"fallback"}}}
|
||||
{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"delegate-write","name":"subagent","arguments":"{\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"delegate-write","name":"subagent","args":["{\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"delegate-write","name":"subagent","arguments":"{\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
|
||||
|
||||
@@ -289,6 +289,55 @@ describe('the shipped Web composition', () => {
|
||||
expect(ctx.agentPresets.defaultId).toBe('standard')
|
||||
})
|
||||
|
||||
it.each(['standard', 'cordis', 'ptc'] as const)('keeps interruption disabled in %s with Team mode off or on', async (preset) => {
|
||||
const handle = await ctx.agents.create({
|
||||
sessionId: SessionId(`preset-interrupt-${preset}`),
|
||||
setup: agentCtx => ctx.agentPresets.mount(agentCtx, preset).then(() => undefined),
|
||||
})
|
||||
try {
|
||||
for (const enabled of [false, true]) {
|
||||
await ctx.agentTeams.setEnabled(handle.agent, enabled)
|
||||
const names = toolNames(ctx, handle.agent)
|
||||
expect(names).not.toContain('interrupt_agent')
|
||||
expect(names).toEqual(expect.arrayContaining(['send_message', 'list_agents', 'wait_agent']))
|
||||
}
|
||||
} finally {
|
||||
await handle.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
it.each(['standard', 'cordis', 'ptc'] as const)('restores interruption through a complete %s declaration override', async (preset) => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-preset-interrupt-'))
|
||||
onTestFinished(() => rm(root, { recursive: true, force: true }))
|
||||
const declaration = composeEntries([webPatches('interrupt migration')])
|
||||
.find(row => row.id === `preset-${preset}`)!.config as {
|
||||
id: string
|
||||
order: number
|
||||
plugins: import('@deepseek-ai/cordis-plugin-loader').EntryOptions[]
|
||||
}
|
||||
const configured = await bootWeb(root, [{
|
||||
id: `preset-${preset}`,
|
||||
config: {
|
||||
...declaration,
|
||||
plugins: composeEntries([[{ insert: declaration.plugins }, {
|
||||
id: 'tool-subagent-control', config: { interrupt: true },
|
||||
}]]),
|
||||
},
|
||||
}])
|
||||
try {
|
||||
const handle = await configured.agents.create({
|
||||
sessionId: SessionId(`preset-interrupt-enabled-${preset}`),
|
||||
setup: agentCtx => configured.agentPresets.mount(agentCtx, preset).then(() => undefined),
|
||||
})
|
||||
try {
|
||||
expect(toolNames(configured)).not.toContain('interrupt_agent')
|
||||
expect(toolNames(configured, handle.agent)).toEqual(expect.arrayContaining([
|
||||
'interrupt_agent', 'send_message', 'list_agents', 'wait_agent',
|
||||
]))
|
||||
} finally { await handle.dispose() }
|
||||
} finally { await configured.fiber.dispose() }
|
||||
})
|
||||
|
||||
it('composes the full agent from `standard`', async () => {
|
||||
const handle = await ctx.agents.create({
|
||||
sessionId: SessionId('preset-standard'),
|
||||
@@ -302,7 +351,7 @@ describe('the shipped Web composition', () => {
|
||||
// depend on ripgrep being present on the machine.
|
||||
expect(toolNames(ctx, handle.agent).filter(name => name !== 'glob' && name !== 'grep')).toEqual([
|
||||
'ask_user_question', 'bash', 'create_goal', 'edit', 'exit_plan_mode',
|
||||
'get_goal', 'interrupt_agent', 'job_kill', 'job_list', 'job_output', 'list_agents', 'list_subagent_models', 'office_convert', 'office_recalculate', 'office_render', 'plugin_manager', 'present', 'read', 'read_image',
|
||||
'get_goal', 'job_kill', 'job_list', 'job_output', 'list_agents', 'list_subagent_models', 'office_convert', 'office_recalculate', 'office_render', 'plugin_manager', 'present', 'read', 'read_image',
|
||||
'schedule_create', 'schedule_edit', 'schedule_list', 'schedule_view',
|
||||
'send_message', 'skill',
|
||||
'subagent', 'subagent_fork', 'team_task_create', 'team_task_get', 'team_task_list', 'team_task_update', 'todo_write', 'update_goal', 'wait_agent', 'web_fetch', 'web_search',
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
- img "变更请求状态:需要处理"
|
||||
- text: "Add the pull request panel #7"
|
||||
- button "查看的提交:全部提交": 全部提交
|
||||
- button "提交评审"
|
||||
- button "在浏览器中打开"
|
||||
- button "全部 2 个文件"
|
||||
- button "分层透镜:全部": 全部
|
||||
- switch "隐藏测试文件"
|
||||
- progressbar "已审阅的文件": 已审阅 0 / 2
|
||||
- text: +4 −2
|
||||
- button "刷新变更"
|
||||
- button "全部折叠"
|
||||
@@ -11,6 +15,7 @@
|
||||
- region "全部 2 个文件":
|
||||
- button "收起 panel.md" [expanded]
|
||||
- text: docs/panel.md 新文件 +2 −0
|
||||
- checkbox "将 panel.md 标记为已审阅"
|
||||
- button "panel.md 的更多操作"
|
||||
- button "选择第 R1 行": "1"
|
||||
- text: "+ # Panel"
|
||||
@@ -18,6 +23,7 @@
|
||||
- text: + Shows the change requests of a Session.
|
||||
- button "收起 panel.ts" [expanded]
|
||||
- text: src/panel.ts +2 −2
|
||||
- checkbox "将 panel.ts 标记为已审阅"
|
||||
- button "panel.ts 的更多操作"
|
||||
- button "选择第 L1 行": "1"
|
||||
- text: export const n1 = 1
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
- dialog "提交评审":
|
||||
- heading "提交评审" [level=2]
|
||||
- button "关闭"
|
||||
- text: 待提交评论(1)
|
||||
- group "丢弃评审":
|
||||
- text: 丢弃待提交的评审及其中的 1 条评论?此操作无法撤销
|
||||
- button "取消"
|
||||
- button "丢弃评审"
|
||||
- textbox "评审总结…"
|
||||
- status
|
||||
- radio "评论" [checked]
|
||||
- text: 评论
|
||||
- radio "批准"
|
||||
- text: 批准
|
||||
- radio "请求更改"
|
||||
- text: 请求更改
|
||||
- button "丢弃评审" [disabled]
|
||||
- button "取消"
|
||||
- button "提交评审" [disabled]
|
||||
@@ -1,8 +1,16 @@
|
||||
- img "变更请求状态:需要处理"
|
||||
- text: "Add the pull request panel #7"
|
||||
- button "查看的提交:全部提交": 全部提交
|
||||
- button "提交评审"
|
||||
- button "在浏览器中打开"
|
||||
- radiogroup "分层透镜":
|
||||
- radio "全部 2" [checked]
|
||||
- radio "接口 0"
|
||||
- radio "实现 2"
|
||||
- radio "测试 0"
|
||||
- button "全部 2 个文件"
|
||||
- switch "隐藏测试文件"
|
||||
- progressbar "已审阅的文件": 已审阅 0 / 2
|
||||
- text: +4 −2
|
||||
- button "刷新变更"
|
||||
- button "全部折叠"
|
||||
@@ -12,6 +20,7 @@
|
||||
- region "全部 2 个文件":
|
||||
- button "收起 panel.md" [expanded]
|
||||
- text: docs/panel.md 新文件 +2 −0
|
||||
- checkbox "将 panel.md 标记为已审阅"
|
||||
- button "panel.md 的更多操作"
|
||||
- button "选择第 R1 行": "1"
|
||||
- text: "+ # Panel"
|
||||
@@ -19,6 +28,7 @@
|
||||
- text: + Shows the change requests of a Session.
|
||||
- button "收起 panel.ts" [expanded]
|
||||
- text: src/panel.ts +2 −2
|
||||
- checkbox "将 panel.ts 标记为已审阅"
|
||||
- button "panel.ts 的更多操作"
|
||||
- button "选择第 L1 行": "1"
|
||||
- text: export const n1 = 1
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
- text: main
|
||||
- button "基准分支:origin/main": origin/main
|
||||
- text: 包含未提交的修改
|
||||
- button "查看的提交:全部提交": 全部提交
|
||||
- button "创建变更请求"
|
||||
- button "分支"
|
||||
- button "全部 0 个文件" [disabled]
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
- img "变更请求状态:需要处理"
|
||||
- text: "Add the pull request panel #7"
|
||||
- button "查看的提交:全部提交": 全部提交
|
||||
- button "提交评审"
|
||||
- button "在浏览器中打开"
|
||||
- radiogroup "分层透镜":
|
||||
- radio "全部 3" [checked]
|
||||
- radio "接口 1"
|
||||
- radio "实现 2"
|
||||
- radio "测试" [disabled]
|
||||
- button "全部 3 个文件"
|
||||
- switch "隐藏测试文件" [checked]
|
||||
- progressbar "已审阅的文件": 已审阅 2 / 3
|
||||
- text: +5 −2
|
||||
- button "刷新变更"
|
||||
- button "全部折叠"
|
||||
- button "左右对比" [pressed]
|
||||
- button "自动换行" [pressed]
|
||||
- button "将文件树移到右侧" [expanded]
|
||||
- region "全部 3 个文件":
|
||||
- button "展开 panel.md"
|
||||
- text: docs/panel.md 新文件 +2 −0
|
||||
- checkbox "将 panel.md 标记为已审阅" [checked]
|
||||
- button "panel.md 的更多操作"
|
||||
- button "展开 panel.ts"
|
||||
- text: src/panel.ts +2 −2
|
||||
- checkbox "将 panel.ts 标记为已审阅" [checked]
|
||||
- button "panel.ts 的更多操作"
|
||||
- button "收起 package.json" [expanded]
|
||||
- text: package.json 新文件 +1 −0
|
||||
- checkbox "将 package.json 标记为已审阅"
|
||||
- button "package.json 的更多操作"
|
||||
- button "选择第 R1 行": "1"
|
||||
- text: "+ { \"name\": \"demo\" }"
|
||||
- separator "调整文件树宽度"
|
||||
- navigation "全部 3 个文件":
|
||||
- textbox "筛选文件…"
|
||||
- button "docs" [expanded]
|
||||
- button "docs/panel.md": panel.md +2 −0
|
||||
- button "src" [expanded]
|
||||
- button "src/panel.ts": panel.ts +2 −2
|
||||
- button "package.json": package.json +1 −0
|
||||
@@ -1,4 +1,8 @@
|
||||
- region "评论 R2–4":
|
||||
- text: 你的评论 R2–4 已发布到 GitHub 请核对边界条件
|
||||
- region "R2–4":
|
||||
- text: R2–4 · 在 aaaaaaa 上 lin 1 个月前
|
||||
- button "lin 的评论操作"
|
||||
- paragraph: 请核对边界条件
|
||||
- button "回复…"
|
||||
- button "添加到输入框"
|
||||
- button "在 GitHub 查看"
|
||||
- button "在 GitHub 中查看"
|
||||
- button "解决讨论"
|
||||
@@ -10,5 +10,6 @@
|
||||
- text: 批准
|
||||
- radio "请求更改" [checked]
|
||||
- text: 请求更改
|
||||
- button "丢弃评审"
|
||||
- button "取消"
|
||||
- button "提交评审" [disabled]
|
||||
@@ -0,0 +1,15 @@
|
||||
- complementary "讨论列表,共 5 个":
|
||||
- text: 讨论 5
|
||||
- button "关闭讨论列表"
|
||||
- radiogroup "筛选讨论":
|
||||
- radio "未解决 4"
|
||||
- radio "已解决 1"
|
||||
- radio "全部 5" [checked]
|
||||
- heading "docs/panel.md" [level=3]
|
||||
- button "文件级讨论 Add a screenshot."
|
||||
- heading "src/panel.ts" [level=3]
|
||||
- button "R2 · 1 条回复 Why two?"
|
||||
- button "L15 The old fifteen was a number."
|
||||
- button "R15 Settled. 已解决"
|
||||
- button "原在 R9 · 在 fffffff 上 This line moved. 已过期"
|
||||
- button "在 GitHub 中查看"
|
||||
@@ -0,0 +1,19 @@
|
||||
> ## Review Thread 1
|
||||
> File: src/panel.ts
|
||||
> Side: R
|
||||
> Lines: 2
|
||||
> Commit: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
> State: unresolved
|
||||
> Source: https://github.com/octo/demo/pull/7#discussion_on-line_0
|
||||
> Diff hunk:
|
||||
> ```diff
|
||||
> @@ -1,5 +1,5 @@
|
||||
> export const n1 = 1
|
||||
> -export const n2 = 2
|
||||
> +export const n2 = 'two'
|
||||
> ```
|
||||
> Comments:
|
||||
> @lin (2026-10-09T00:00:00Z):
|
||||
> > Why **two**?
|
||||
> @octo (2026-10-09T00:00:00Z):
|
||||
> > It reads better.
|
||||
@@ -0,0 +1,16 @@
|
||||
- region "R2":
|
||||
- text: R2 · 在 aaaaaaa 上 lin 2 小时前
|
||||
- button "lin 的评论操作"
|
||||
- paragraph:
|
||||
- text: Why
|
||||
- strong: two
|
||||
- text: "?"
|
||||
- text: octo 2 小时前 作者
|
||||
- paragraph: It reads better.
|
||||
- textbox "回复…": 已改为字符串
|
||||
- button "取消"
|
||||
- button "回复"
|
||||
- button "回复选项"
|
||||
- button "添加到输入框"
|
||||
- button "在 GitHub 中查看"
|
||||
- button "解决讨论"
|
||||
@@ -0,0 +1,91 @@
|
||||
- button "收起 panel.ts" [expanded]
|
||||
- text: src/panel.ts
|
||||
- button "panel.ts 有 3 条未解决的讨论,跳到下一条": 3 条未解决
|
||||
- text: +2 −2
|
||||
- checkbox "将 panel.ts 标记为已审阅"
|
||||
- button "panel.ts 的更多操作"
|
||||
- button "选择第 L1 行": "1"
|
||||
- text: export const n1 = 1
|
||||
- button "选择第 R1 行": "1"
|
||||
- text: export const n1 = 1
|
||||
- button "选择第 L2 行": "2"
|
||||
- text: export const n2 = 2
|
||||
- button "选择第 R2 行": "2"
|
||||
- text: export const n2 = 'two'
|
||||
- region "R2":
|
||||
- text: R2 · 在 aaaaaaa 上 lin 2 小时前
|
||||
- button "lin 的评论操作"
|
||||
- paragraph:
|
||||
- text: Why
|
||||
- strong: two
|
||||
- text: "?"
|
||||
- text: octo 2 小时前 作者
|
||||
- paragraph: It reads better.
|
||||
- button "回复…"
|
||||
- button "添加到输入框"
|
||||
- button "在 GitHub 中查看"
|
||||
- button "解决讨论"
|
||||
- button "选择第 L3 行": "3"
|
||||
- text: export const n3 = 3
|
||||
- button "选择第 R3 行": "3"
|
||||
- text: export const n3 = 3
|
||||
- button "选择第 L4 行": "4"
|
||||
- text: export const n4 = 4
|
||||
- button "选择第 R4 行": "4"
|
||||
- text: export const n4 = 4
|
||||
- button "选择第 L5 行": "5"
|
||||
- text: export const n5 = 5
|
||||
- button "选择第 R5 行": "5"
|
||||
- text: export const n5 = 5
|
||||
- button "6 行未变更"
|
||||
- button "选择第 L12 行": "12"
|
||||
- text: export const n12 = 12
|
||||
- button "选择第 R12 行": "12"
|
||||
- text: export const n12 = 12
|
||||
- button "选择第 L13 行": "13"
|
||||
- text: export const n13 = 13
|
||||
- button "选择第 R13 行": "13"
|
||||
- text: export const n13 = 13
|
||||
- button "选择第 L14 行": "14"
|
||||
- text: export const n14 = 14
|
||||
- button "选择第 R14 行": "14"
|
||||
- text: export const n14 = 14
|
||||
- button "选择第 L15 行": "15"
|
||||
- text: export const n15 = 15
|
||||
- button "选择第 R15 行": "15"
|
||||
- text: export const n15 = 'fifteen'
|
||||
- region "L15":
|
||||
- text: L15 · 在 aaaaaaa 上 lin 2 小时前
|
||||
- button "lin 的评论操作"
|
||||
- paragraph: The old fifteen was a number.
|
||||
- button "回复…"
|
||||
- button "添加到输入框"
|
||||
- button "在 GitHub 中查看"
|
||||
- button "解决讨论"
|
||||
- text: 已解决的讨论 · lin
|
||||
- button "展开"
|
||||
- button "选择第 L16 行": "16"
|
||||
- text: export const n16 = 16
|
||||
- button "选择第 R16 行": "16"
|
||||
- text: export const n16 = 16
|
||||
- button "选择第 L17 行": "17"
|
||||
- text: export const n17 = 17
|
||||
- button "选择第 R17 行": "17"
|
||||
- text: export const n17 = 17
|
||||
- button "选择第 L18 行": "18"
|
||||
- text: export const n18 = 18
|
||||
- button "选择第 R18 行": "18"
|
||||
- text: export const n18 = 18
|
||||
- button "其余未变更的行"
|
||||
- text: 1 条讨论已过期:所在代码已被后续提交修改
|
||||
- button "收起" [expanded]
|
||||
- region "原在 R9":
|
||||
- text: 原在 R9 · 在 fffffff 上 已过期
|
||||
- button "查看当时的文件"
|
||||
- text: "@@ -1,5 +1,5 @@ export const n1 = 1 -export const n2 = 2 +export const n2 = 'two' lin 2 小时前"
|
||||
- button "lin 的评论操作"
|
||||
- paragraph: This line moved.
|
||||
- button "回复…"
|
||||
- button "添加到输入框"
|
||||
- button "在 GitHub 中查看"
|
||||
- button "解决讨论"
|
||||
@@ -10,5 +10,6 @@
|
||||
- text: 批准
|
||||
- radio "请求更改" [checked]
|
||||
- text: 请求更改
|
||||
- button "丢弃评审"
|
||||
- button "取消"
|
||||
- button "提交评审"
|
||||
@@ -1,13 +1,13 @@
|
||||
# Before opening the child
|
||||
|
||||
- tree "Subagent sessions":
|
||||
- treeitem "historical researcher migration-catalog-child · Migration required · not running" [level=1]:
|
||||
- text: historical researcher migration-catalog-child Migration required
|
||||
- treeitem "historical researcher migration-catalog-child · continuable · not running" [level=1]:
|
||||
- text: historical researcher migration-catalog-child
|
||||
- button "Open historical researcher in sidebar"
|
||||
|
||||
# After opening the child
|
||||
|
||||
- tree "Subagent sessions":
|
||||
- treeitem "historical editor migration-catalog-grandchild · Migration required · not running" [level=1]:
|
||||
- text: historical editor migration-catalog-grandchild Migration required
|
||||
- treeitem "historical editor migration-catalog-grandchild · continuable · not running" [level=1]:
|
||||
- text: historical editor migration-catalog-grandchild
|
||||
- button "Open historical editor in sidebar"
|
||||
@@ -1,5 +1,6 @@
|
||||
/** Worktree Sessions and the change request panel through the real optional bundle, a Git checkout, and a scripted GitHub CLI. */
|
||||
import { execFile } from 'node:child_process'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { access, chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { homedir, tmpdir } from 'node:os'
|
||||
import { basename, join } from 'node:path'
|
||||
@@ -44,11 +45,19 @@ const run = promisify(execFile)
|
||||
* required review, and pull request 9, an approved draft whose checks passed.
|
||||
* `gh pr ready 9` and the REST merge of pull request 9 change what later
|
||||
* queries report, through `state.json`, which can also move the head of pull
|
||||
* request 9. The compare API between a pull request's recorded base and its
|
||||
* head prints `pr-N.diff`, or, when `pr-N.refused` exists, fails with that JSON
|
||||
* error body and the matching diagnostic as GitHub's HTTP 406 refusal. The raw
|
||||
* contents API serves `contents.json`, keyed by `path@commit`. Every invocation
|
||||
* is appended to `calls.jsonl`.
|
||||
* request 9. The compare API serves `compare.json`, keyed by `base...head`, and
|
||||
* otherwise, between a pull request's recorded base and its head, prints
|
||||
* `pr-N.diff`, or, when `pr-N.refused` exists, fails with that JSON error body
|
||||
* and the matching diagnostic as GitHub's HTTP 406 refusal. The raw contents API
|
||||
* serves `contents.json`, keyed by `path@commit`, and the commit listing of pull
|
||||
* request 7 serves `commits.json`, oldest first. The review thread read serves
|
||||
* `threads.json` as its one page of threads, with the viewer `lin`, who may write
|
||||
* to the repository and did not open either pull request, or no threads without it. Review mutations, which arrive as
|
||||
* JSON on stdin, change what later reads report through `state.json`: replies,
|
||||
* which join the pending review whenever one exists, resolution, comment edits and
|
||||
* deletions, and the pending review of `lin` with its new threads, which a
|
||||
* submission publishes. Each mutation is appended to
|
||||
* `mutations.jsonl`, and every invocation to `calls.jsonl`.
|
||||
*/
|
||||
const GH = `#!/usr/bin/env node
|
||||
import { appendFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
@@ -93,6 +102,11 @@ const BASE = 'e'.repeat(40)
|
||||
const heads = () => ({ 7: 'a'.repeat(40), 9: state.head ?? 'b'.repeat(40) })
|
||||
if (args[0] === 'api' && args.includes('Accept: application/vnd.github.diff')) {
|
||||
const [, range] = args.at(-1).split('/compare/')
|
||||
const chosen = existsSync(join(here, 'compare.json')) ? JSON.parse(readFileSync(join(here, 'compare.json'), 'utf8'))[range] : undefined
|
||||
if (chosen !== undefined) {
|
||||
process.stdout.write(chosen)
|
||||
process.exit(0)
|
||||
}
|
||||
const [base, head] = range.split('...')
|
||||
const number = Object.entries(heads()).find(([, oid]) => oid === head)?.[0]
|
||||
if (base !== BASE || number === undefined) {
|
||||
@@ -124,6 +138,56 @@ if (args[0] === 'auth' && args[1] === 'status') {
|
||||
process.stdout.write('github.com\\n Logged in to github.com account octo\\n')
|
||||
process.exit(0)
|
||||
}
|
||||
const review = () => ({ pending: null, published: [], replies: {}, resolved: {}, bodies: {}, deleted: [], ...state.review })
|
||||
/** The signed-in reviewer, who did not open the pull requests. */
|
||||
const VIEWER = 'lin'
|
||||
/** One review comment node by \`login\`, which the viewer may change when it is the viewer's own. */
|
||||
const commentNode = (id, body, login, pending) => ({ id, state: pending ? 'PENDING' : 'SUBMITTED', createdAt: '2026-10-09T01:00:00Z',
|
||||
url: pending ? null : 'https://github.com/octo/demo/pull/7#discussion_' + id, body,
|
||||
viewerCanUpdate: login === VIEWER, viewerCanDelete: login === VIEWER, author: { __typename: 'User', login, avatarUrl: null } })
|
||||
if (args[0] === 'api' && args[1] === 'graphql' && args.includes('--input')) {
|
||||
const { query, variables } = JSON.parse(readFileSync(0, 'utf8'))
|
||||
const field = query.match(/\\{\\s*(\\w+)\\(input:/)[1]
|
||||
appendFileSync(join(here, 'mutations.jsonl'), JSON.stringify({ field, variables }) + '\\n')
|
||||
const current = review()
|
||||
const draft = thread => ({ id: 'new-' + (current.published.length + (current.pending?.threads.length ?? 0) + 1),
|
||||
path: thread.path, side: thread.side, line: thread.line, startLine: thread.startLine ?? null, body: thread.body })
|
||||
let payload
|
||||
if (field === 'addPullRequestReview') {
|
||||
current.pending = { id: 'review-1', commit: variables.commit, threads: [] }
|
||||
for (const thread of variables.threads ?? []) current.pending.threads.push(draft(thread))
|
||||
payload = { pullRequestReview: { id: 'review-1' } }
|
||||
} else if (field === 'addPullRequestReviewThread') {
|
||||
current.pending.threads.push(draft(variables))
|
||||
payload = { thread: { id: current.pending.threads.at(-1).id } }
|
||||
} else if (field === 'addPullRequestReviewThreadReply') {
|
||||
const replies = current.replies[variables.thread] ??= []
|
||||
// As on GitHub, a reply joins the pending review whenever one exists.
|
||||
replies.push({ id: variables.thread + '-reply-' + replies.length, body: variables.body, pending: variables.review !== null || current.pending !== null })
|
||||
payload = { comment: { id: replies.at(-1).id } }
|
||||
} else if (field === 'resolveReviewThread' || field === 'unresolveReviewThread') {
|
||||
current.resolved[variables.thread] = field === 'resolveReviewThread'
|
||||
payload = { thread: { id: variables.thread } }
|
||||
} else if (field === 'updatePullRequestReviewComment') {
|
||||
current.bodies[variables.comment] = variables.body
|
||||
payload = { pullRequestReviewComment: { id: variables.comment } }
|
||||
} else if (field === 'deletePullRequestReviewComment') {
|
||||
current.deleted.push(variables.comment)
|
||||
payload = { clientMutationId: null }
|
||||
} else if (field === 'submitPullRequestReview' || field === 'deletePullRequestReview') {
|
||||
if (field === 'submitPullRequestReview') current.published.push(...current.pending.threads)
|
||||
for (const replies of Object.values(current.replies)) {
|
||||
for (const reply of replies) if (reply.pending && field === 'deletePullRequestReview') current.deleted.push(reply.id)
|
||||
for (const reply of replies) reply.pending = false
|
||||
}
|
||||
current.pending = null
|
||||
payload = { pullRequestReview: { id: variables.review } }
|
||||
}
|
||||
state.review = current
|
||||
save()
|
||||
process.stdout.write(JSON.stringify({ data: { [field]: payload } }))
|
||||
process.exit(0)
|
||||
}
|
||||
if (args[0] === 'api' && args[1] === 'graphql') {
|
||||
const query = args.find(arg => arg.startsWith('query=')).slice('query='.length)
|
||||
const check = (name, conclusion, completedAt, databaseId = 101) => ({
|
||||
@@ -134,7 +198,7 @@ if (args[0] === 'api' && args[1] === 'graphql') {
|
||||
const pulls = {
|
||||
7: {
|
||||
id: 'pull-7', number: 7, url: 'https://github.com/octo/demo/pull/7', title: 'Add the pull request panel', state: 'OPEN', isDraft: false,
|
||||
updatedAt: '2026-10-09T00:00:00Z', closedAt: null, author: { login: 'octo' }, headRefName: 'feat', headRefOid: 'a'.repeat(40),
|
||||
updatedAt: '2026-10-09T00:00:00Z', closedAt: null, author: { login: 'octo' }, headRefName: 'feat', headRefOid: 'a'.repeat(40), baseRefOid: BASE,
|
||||
baseRefName: 'main', additions: 128, deletions: 14, mergeable: 'MERGEABLE', mergeStateStatus: 'BLOCKED', reviewDecision: 'REVIEW_REQUIRED',
|
||||
reviewThreads: { pageInfo: { hasNextPage: false, endCursor: null }, nodes: [{ isResolved: false }] },
|
||||
commits: { nodes: [{ commit: { statusCheckRollup: { id: 'rollup-7', state: 'FAILURE', contexts: {
|
||||
@@ -145,7 +209,7 @@ if (args[0] === 'api' && args[1] === 'graphql') {
|
||||
9: {
|
||||
id: 'pull-9', number: 9, url: 'https://github.com/octo/demo/pull/9', title: 'Add the merge button', state: state.mergedAt === null ? 'OPEN' : 'MERGED',
|
||||
isDraft: !state.ready, updatedAt: '2026-10-09T00:00:00Z', closedAt: state.mergedAt, author: { login: 'octo' },
|
||||
headRefName: 'feat/merge', headRefOid: heads()[9], baseRefName: 'main', additions: 30, deletions: 2, mergeable: 'MERGEABLE',
|
||||
headRefName: 'feat/merge', headRefOid: heads()[9], baseRefName: 'main', baseRefOid: BASE, additions: 30, deletions: 2, mergeable: 'MERGEABLE',
|
||||
mergeStateStatus: state.ready ? 'CLEAN' : 'BLOCKED', reviewDecision: 'APPROVED',
|
||||
reviewThreads: { pageInfo: { hasNextPage: false, endCursor: null }, nodes: [] },
|
||||
commits: { nodes: [{ commit: { statusCheckRollup: { id: 'rollup-9', state: 'SUCCESS', contexts: {
|
||||
@@ -153,6 +217,52 @@ if (args[0] === 'api' && args[1] === 'graphql') {
|
||||
} } } }] },
|
||||
},
|
||||
}
|
||||
if (query.includes('commits(first: $first')) {
|
||||
const commits = existsSync(join(here, 'commits.json')) ? JSON.parse(readFileSync(join(here, 'commits.json'), 'utf8')) : []
|
||||
process.stdout.write(JSON.stringify({ data: { repository: { pullRequest: { headRefOid: 'a'.repeat(40), commits: {
|
||||
totalCount: commits.length, pageInfo: { hasNextPage: false, endCursor: null },
|
||||
nodes: commits.map(commit => ({ commit: {
|
||||
oid: commit.sha, messageHeadline: commit.title, authoredDate: '2026-10-08T00:00:00Z',
|
||||
author: { name: 'Octo', user: { login: 'octo' } }, parents: { nodes: [{ oid: commit.parent }] },
|
||||
} })),
|
||||
} } } } }))
|
||||
process.exit(0)
|
||||
}
|
||||
if (query.includes('viewer { login }')) {
|
||||
const number = Number(args.find(arg => arg.startsWith('number=')).slice('number='.length))
|
||||
const current = review()
|
||||
const page = nodes => ({ pageInfo: { hasNextPage: false, endCursor: null }, nodes })
|
||||
const written = (thread, pending) => ({
|
||||
id: thread.id, isOutdated: false, isResolved: false, path: thread.path, diffSide: thread.side, startDiffSide: thread.startLine === null ? null : thread.side,
|
||||
line: thread.line, startLine: thread.startLine, originalLine: thread.line, originalStartLine: thread.startLine, subjectType: 'LINE',
|
||||
viewerCanReply: true, viewerCanResolve: true, viewerCanUnresolve: false,
|
||||
root: { nodes: [{ diffHunk: '@@ -1,5 +1,5 @@', commit: { oid: pulls[number].headRefOid }, originalCommit: { oid: pulls[number].headRefOid } }] },
|
||||
comments: page([commentNode(thread.id + '-0', thread.body, VIEWER, pending)]),
|
||||
})
|
||||
const stored = existsSync(join(here, 'threads.json')) ? JSON.parse(readFileSync(join(here, 'threads.json'), 'utf8')) : []
|
||||
const threads = [...stored, ...current.published.map(thread => written(thread, false)),
|
||||
...(current.pending?.threads ?? []).map(thread => written(thread, true))].map((thread) => {
|
||||
const replies = (current.replies[thread.id] ?? []).map(reply => commentNode(reply.id, reply.body, VIEWER, reply.pending))
|
||||
const comments = [...thread.comments.nodes, ...replies].filter(comment => !current.deleted.includes(comment.id))
|
||||
.map(comment => ({ ...comment, body: current.bodies[comment.id] ?? comment.body }))
|
||||
return { ...thread, isResolved: current.resolved[thread.id] ?? thread.isResolved, comments: page(comments) }
|
||||
}).filter(thread => thread.comments.nodes.length > 0)
|
||||
const pendingComments = threads.flatMap(thread => thread.comments.nodes).filter(comment => comment.state === 'PENDING').length
|
||||
process.stdout.write(JSON.stringify({ data: { viewer: { login: VIEWER }, repository: { isArchived: false, viewerPermission: 'WRITE',
|
||||
pullRequest: { id: 'pull-' + number, headRefOid: pulls[number].headRefOid, locked: false,
|
||||
reviews: { nodes: current.pending === null ? [] : [{ id: current.pending.id, viewerDidAuthor: true, comments: { totalCount: pendingComments } }] },
|
||||
reviewThreads: page(threads) } } } }))
|
||||
process.exit(0)
|
||||
}
|
||||
if (query.includes('reviews(states: PENDING')) {
|
||||
const number = Number(args.find(arg => arg.startsWith('number=')).slice('number='.length))
|
||||
const pending = review().pending
|
||||
// Every thread and comment the double serves belongs to the pull request it was read from.
|
||||
const target = args.some(arg => arg.startsWith('target=')) ? { target: { pullRequest: { id: 'pull-' + number } } } : {}
|
||||
process.stdout.write(JSON.stringify({ data: { repository: { pullRequest: { id: 'pull-' + number, headRefOid: pulls[number].headRefOid,
|
||||
reviews: { nodes: pending === null ? [] : [{ id: pending.id, viewerDidAuthor: true }] } } }, ...target } }))
|
||||
process.exit(0)
|
||||
}
|
||||
if (query.includes('pullRequest(number: $number)')) {
|
||||
const number = Number(args.find(arg => arg.startsWith('number=')).slice('number='.length))
|
||||
process.stdout.write(JSON.stringify({ data: { repository: { pullRequest: { headRefOid: pulls[number].headRefOid, baseRefOid: BASE } } } }))
|
||||
@@ -224,6 +334,53 @@ const PR7_DIFF = [
|
||||
'',
|
||||
].join('\n')
|
||||
|
||||
/** A patch section that adds `lines` as the new file `path`. */
|
||||
function addedSection(path: string, lines: readonly string[]): string[] {
|
||||
return [`diff --git a/${path} b/${path}`, 'new file mode 100644', 'index 0000000..4444444', '--- /dev/null', `+++ b/${path}`,
|
||||
`@@ -0,0 +1,${lines.length} @@`, ...lines.map(line => `+${line}`)]
|
||||
}
|
||||
|
||||
/** Commits of pull request 7 in the review fixture, oldest first; the last is its head. */
|
||||
const REVIEW_COMMITS = [
|
||||
{ sha: '1'.repeat(40), parent: 'f'.repeat(40), title: 'Add the pull request panel' },
|
||||
{ sha: '2'.repeat(40), parent: '1'.repeat(40), title: 'Test the panel and translate it' },
|
||||
{ sha: HEAD7, parent: '2'.repeat(40), title: 'Publish the panel schema' },
|
||||
] as const
|
||||
|
||||
/** New files of each review fixture commit after the first, which changes `src/panel.ts` and adds `docs/panel.md`. */
|
||||
const REVIEW_SECTIONS = {
|
||||
test: [
|
||||
...addedSection('src/panel.spec.ts', ['it(\'shows the panel\', () => {})']),
|
||||
...addedSection('locale/zh.json', ['{ "title": "面板" }']),
|
||||
],
|
||||
schema: [
|
||||
...addedSection('package.json', ['{ "name": "demo" }']),
|
||||
...addedSection('docs/schema.json', ['{ "type": "object" }']),
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull request 7 with review layers: `src/panel.ts` and `docs/panel.md` are implementation, `package.json` is
|
||||
* interface, and the spec, the locale dictionary, which `.gitattributes` places in the test layer, and
|
||||
* `docs/schema.json`, which it marks generated, are tests.
|
||||
*/
|
||||
const REVIEW_DIFF = [PR7_DIFF.trimEnd(), ...REVIEW_SECTIONS.test, ...REVIEW_SECTIONS.schema, ''].join('\n')
|
||||
|
||||
/** Write the review fixture of pull request 7: its patch, commits, commit ranges, and root `.gitattributes`. */
|
||||
async function writeReviewFixture(root: string): Promise<void> {
|
||||
await writeFile(join(root, 'pr-7.diff'), REVIEW_DIFF)
|
||||
await writeFile(join(root, 'commits.json'), JSON.stringify(REVIEW_COMMITS))
|
||||
await writeFile(join(root, 'compare.json'), JSON.stringify({
|
||||
[`${'1'.repeat(40)}...${'2'.repeat(40)}`]: [...REVIEW_SECTIONS.test, ''].join('\n'),
|
||||
[`${'1'.repeat(40)}...${HEAD7}`]: [...REVIEW_SECTIONS.test, ...REVIEW_SECTIONS.schema, ''].join('\n'),
|
||||
}))
|
||||
const contents = JSON.parse(await readFile(join(root, 'contents.json'), 'utf8')) as Record<string, string>
|
||||
for (const commit of ['2'.repeat(40), HEAD7]) {
|
||||
contents[`.gitattributes@${commit}`] = 'docs/schema.json linguist-generated\nlocale/*.json dsh-review-layer=test\n'
|
||||
}
|
||||
await writeFile(join(root, 'contents.json'), JSON.stringify(contents))
|
||||
}
|
||||
|
||||
async function git(cwd: string, ...args: string[]): Promise<string> {
|
||||
const { stdout } = await run('git', args, { cwd, env: { ...process.env, GIT_CONFIG_NOSYSTEM: '1', GIT_TERMINAL_PROMPT: '0' } })
|
||||
return stdout.trim()
|
||||
@@ -482,10 +639,14 @@ it.skipIf(MODE === 'record')('automatically shows a numbered PR when gh JSON omi
|
||||
await change.getByText('等待评审 · 1 条未解决讨论', { exact: true }).waitFor()
|
||||
await compareOrRefreshGolden(join(EXPECTED, 'reference.expected.md'),
|
||||
await captureStableAria(page, `[data-forge-change="${PULL}"]`, scaffold.workspaceCwd), MODE)
|
||||
expect(events.flatMap((event) => {
|
||||
const record = pluginRecordOf(event)
|
||||
return record?.type === 'plugin:forge-session/binding' ? [record.data] : []
|
||||
})).toEqual([{ action: 'reference', url: PULL }])
|
||||
await expect.poll(async () => {
|
||||
await scaffold.ctx.sessionPersistence.flush()
|
||||
const boundEvents = await readPersistedEvents(scaffold, sessionId)
|
||||
return boundEvents.flatMap((event) => {
|
||||
const record = pluginRecordOf(event)
|
||||
return record?.type === 'plugin:forge-session/binding' ? [record.data] : []
|
||||
})
|
||||
}).toEqual([{ action: 'reference', url: PULL }])
|
||||
const calls = (await readFile(join(project, '.fixture-bin', 'calls.jsonl'), 'utf8')).trim().split('\n').map(line => JSON.parse(line) as string[])
|
||||
expect(calls).toContainEqual(['pr', 'view', '7', '--repo', 'octo/demo', '--json', 'url'])
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
@@ -952,7 +1113,10 @@ it.skipIf(MODE === 'record')('shows the local changes of the Session by scope an
|
||||
expect(narrowComment.x + narrowComment.width).toBeLessThanOrEqual(390)
|
||||
await page.setViewportSize({ width: 1680, height: 1000 })
|
||||
await panelFile.getByRole('button', { name: '开始评审', exact: true }).click()
|
||||
await panelFile.locator('[data-forge-review-thread="pending"]').getByText('请核对边界条件', { exact: true }).waitFor()
|
||||
// The comment starts the pending review on GitHub, and the threads read again show it there.
|
||||
const pendingThread = panelFile.locator('[data-forge-thread="new-1"]')
|
||||
await pendingThread.getByText('请核对边界条件', { exact: true }).waitFor({ timeout: 20_000 })
|
||||
await pendingThread.getByText('待提交', { exact: true }).waitFor()
|
||||
await page.setViewportSize({ width: 1680, height: 500 })
|
||||
const changesScroll = view.locator('[data-forge-changes-scroll]')
|
||||
await changesScroll.evaluate((element) => { element.scrollTop = element.scrollHeight })
|
||||
@@ -983,11 +1147,16 @@ it.skipIf(MODE === 'record')('shows the local changes of the Session by scope an
|
||||
await captureStableAria(page, '[role="dialog"][aria-label="提交评审"]', scaffold.workspaceCwd), MODE)
|
||||
await reviewDialog.getByRole('button', { name: '提交评审', exact: true }).click()
|
||||
await page.getByRole('alert').filter({ hasText: '已提交评审' }).waitFor()
|
||||
await panelFile.locator('[data-forge-review-thread="published"]').getByText('请核对边界条件', { exact: true }).waitFor()
|
||||
await compareOrRefreshGolden(join(EXPECTED, 'review-record.expected.md'), await captureStableAria(page, '[data-forge-review-thread="published"]', scaffold.workspaceCwd), MODE)
|
||||
const published: unknown = JSON.parse((await readFile(join(root, 'reviews.jsonl'), 'utf8')).trim())
|
||||
expect(published).toEqual({ endpoint: 'repos/octo/demo/pulls/7/reviews', body: { commit_id: HEAD7, event: 'REQUEST_CHANGES', body: '需要补充边界检查',
|
||||
comments: [{ path: 'src/panel.ts', side: 'RIGHT', line: 4, start_line: 2, start_side: 'RIGHT', body: '请核对边界条件' }] } })
|
||||
await expect.poll(() => pendingThread.getByText('待提交', { exact: true }).count(), { timeout: 20_000 }).toBe(0)
|
||||
await compareOrRefreshGolden(join(EXPECTED, 'review-record.expected.md'),
|
||||
await captureStableAria(page, '[data-forge-thread="new-1"]', scaffold.workspaceCwd), MODE)
|
||||
const mutations = (await readFile(join(root, 'mutations.jsonl'), 'utf8')).trim().split('\n').map((line): unknown => JSON.parse(line))
|
||||
expect(mutations).toEqual([
|
||||
{ field: 'addPullRequestReview', variables: { pull: 'pull-7', commit: HEAD7,
|
||||
threads: [{ path: 'src/panel.ts', body: '请核对边界条件', line: 4, side: 'RIGHT', startLine: 2, startSide: 'RIGHT' }] } },
|
||||
{ field: 'submitPullRequestReview', variables: { review: 'review-1', event: 'REQUEST_CHANGES', body: '需要补充边界检查' } },
|
||||
])
|
||||
expect(existsSync(join(root, 'reviews.jsonl'))).toBe(false)
|
||||
await page.getByRole('button', { name: '退出全屏', exact: true }).click()
|
||||
await panelFile.getByRole('button', { name: '6 行未变更', exact: true }).click()
|
||||
await panelFile.getByText('export const n6 = 6', { exact: true }).first().waitFor({ timeout: 20_000 })
|
||||
@@ -1011,7 +1180,7 @@ it.skipIf(MODE === 'record')('shows the local changes of the Session by scope an
|
||||
expect(await panelFile.evaluate((element, original) => element === original, originalFile)).toBe(true)
|
||||
await expect.poll(() => docsFile.getAttribute('data-expanded')).toBe(null)
|
||||
await expect.poll(() => scrollport.evaluate(element => element.scrollTop)).toBe(position)
|
||||
expect(await panelFile.locator('[data-forge-review-thread="published"]').count()).toBe(1)
|
||||
expect(await panelFile.locator('[data-forge-thread="new-1"]').count()).toBe(1)
|
||||
await page.setViewportSize({ width: 1680, height: 1000 })
|
||||
await page.locator('[data-dockkit-tab]').filter({ hasText: '#7 的变更' }).locator('[data-dockkit-tab-close]').click()
|
||||
await expect.poll(() => view.count()).toBe(0)
|
||||
@@ -1106,6 +1275,8 @@ it.skipIf(MODE === 'record')('retains a wrapped code line and tree width across
|
||||
return { path: element.getAttribute('data-forge-diff-file'), line: line.dataset.newLine,
|
||||
offset: line.getBoundingClientRect().top - top, scrollTop: viewport.scrollTop }
|
||||
})
|
||||
// An expanded file draws its lines once the reader scrolls near it.
|
||||
await file.scrollIntoViewIfNeeded()
|
||||
await code.waitFor()
|
||||
for (const width of [240, 320]) {
|
||||
if (width === 320) {
|
||||
@@ -1183,6 +1354,11 @@ it.skipIf(MODE === 'record')('uses horizontal arrows to navigate files while pre
|
||||
expect(await scroll.evaluate(element => document.activeElement === element)).toBe(true)
|
||||
await page.keyboard.press('ArrowRight')
|
||||
await expect.poll(() => tree.getByRole('button', { name: 'README.md', exact: true }).getAttribute('aria-current')).toBe('true')
|
||||
// The last file draws its lines after the jump, and scroll anchoring adjusts the position at the next layout.
|
||||
await c.locator('[data-review-code]').first().waitFor()
|
||||
await scroll.evaluate(() => new Promise<void>((resolve) => {
|
||||
requestAnimationFrame(() => { requestAnimationFrame(() => { resolve() }) })
|
||||
}))
|
||||
const lastPosition = await scroll.evaluate(element => element.scrollTop)
|
||||
const lastBox = await c.boundingBox(), viewport = await scroll.boundingBox()
|
||||
if (lastBox === null || viewport === null) throw new Error('The last file and scroll viewport must be rendered')
|
||||
@@ -1643,6 +1819,351 @@ it.skipIf(MODE === 'record').each([120, 4000])('types a review comment beside %i
|
||||
expect(await file.getByRole('button', { name: '开始评审', exact: true }).isDisabled()).toBe(true)
|
||||
await input.fill('检查边界条件')
|
||||
await file.getByRole('button', { name: '开始评审', exact: true }).click()
|
||||
await file.locator('[data-forge-review-thread="pending"]').getByText('检查边界条件', { exact: true }).waitFor()
|
||||
await file.locator('[data-forge-thread]').getByText('检查边界条件', { exact: true }).waitFor({ timeout: 20_000 })
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
})
|
||||
|
||||
/** Open the change request view of pull request 7 in full screen, its tree on the left. */
|
||||
async function openReview(page: Page, scaffold: WebScaffold): Promise<{ readonly view: ReturnType<Page['locator']>; readonly sessionId: SessionId }> {
|
||||
const { settled } = await sendPrompt(page, scaffold)
|
||||
const sessionId = await settled
|
||||
const session = scaffold.ctx.sessions.get(sessionId)
|
||||
if (session === undefined) throw new Error('Session must stay loaded')
|
||||
scaffold.ctx.forgeSession.track(session, PULL)
|
||||
await page.locator('[data-forge-header]').click()
|
||||
await page.locator(`[data-forge-change="${PULL}"]`).getByRole('button', { name: '查看 #7 的变更:新增 128 行,删除 14 行', exact: true }).click()
|
||||
await page.getByRole('button', { name: '全屏', exact: true }).click()
|
||||
const view = page.locator('[data-forge-changes="change"]')
|
||||
await view.locator('[data-forge-diff-file="src/panel.ts"]').waitFor({ timeout: 20_000 })
|
||||
return { view, sessionId }
|
||||
}
|
||||
|
||||
/** Paths of the diff sections a changes view shows, in order. */
|
||||
async function shownPaths(view: ReturnType<Page['locator']>): Promise<(string | null)[]> {
|
||||
return await view.locator('[data-forge-diff-file]').evaluateAll(sections => sections.map(section => section.getAttribute('data-forge-diff-file')))
|
||||
}
|
||||
|
||||
it.skipIf(MODE === 'record')('reviews a change request by layer, hides test files, marks files viewed, and views chosen commits', async () => {
|
||||
const { scaffold, page, root, tripwire } = await launchPanel('web-e2e-forge-review-layers')
|
||||
await writeReviewFixture(root)
|
||||
const { view } = await openReview(page, scaffold)
|
||||
const lens = view.getByRole('radiogroup', { name: '分层透镜', exact: true })
|
||||
const tree = view.getByRole('navigation')
|
||||
await expect.poll(() => lens.getByRole('radio').allTextContents()).toEqual(['全部6', '接口1', '实现2', '测试3'])
|
||||
// The lens spans the file tree with the filter field's insets, and its divider continues the tree's border.
|
||||
const [lensBox, fieldBox, cellBox, treeBox] = await Promise.all([
|
||||
lens.boundingBox(), tree.locator('input').locator('xpath=..').boundingBox(), lens.locator('xpath=..').boundingBox(), tree.locator('xpath=..').boundingBox(),
|
||||
])
|
||||
if (lensBox === null || fieldBox === null || cellBox === null || treeBox === null) throw new Error('Lens and tree must be visible')
|
||||
expect([lensBox.x, lensBox.x + lensBox.width]).toEqual([fieldBox.x, fieldBox.x + fieldBox.width])
|
||||
expect(cellBox.x + cellBox.width).toBe(treeBox.x + treeBox.width)
|
||||
|
||||
await lens.getByRole('radio', { name: /^接口/ }).click()
|
||||
await expect.poll(() => shownPaths(view)).toEqual(['package.json'])
|
||||
await lens.getByRole('radio', { name: /^测试/ }).click()
|
||||
await expect.poll(() => shownPaths(view)).toEqual(['docs/schema.json', 'locale/zh.json', 'src/panel.spec.ts'])
|
||||
await view.getByRole('switch', { name: '隐藏测试文件', exact: true }).click()
|
||||
await expect.poll(() => shownPaths(view)).toEqual(['docs/panel.md', 'src/panel.ts', 'package.json'])
|
||||
expect(await tree.getByRole('button', { name: 'src/panel.spec.ts', exact: true }).count()).toBe(0)
|
||||
expect(await lens.getByRole('radio', { name: /^测试/ }).isDisabled()).toBe(true)
|
||||
|
||||
const progress = view.getByRole('progressbar', { name: '已审阅的文件', exact: true })
|
||||
await expect.poll(() => progress.getAttribute('aria-valuetext')).toBe('已审阅 0 / 3')
|
||||
const docs = view.locator('[data-forge-diff-file="docs/panel.md"]')
|
||||
await docs.getByRole('checkbox', { name: '将 panel.md 标记为已审阅', exact: true }).check()
|
||||
await expect.poll(() => docs.getAttribute('data-expanded')).toBe(null)
|
||||
await expect.poll(() => progress.getAttribute('aria-valuetext')).toBe('已审阅 1 / 3')
|
||||
const panel = view.locator('[data-forge-diff-file="src/panel.ts"]')
|
||||
await panel.getByText('export const n2 = \'two\'', { exact: true }).first().click()
|
||||
await page.keyboard.press('v')
|
||||
await expect.poll(() => panel.getAttribute('data-viewed')).toBe('viewed')
|
||||
await expect.poll(() => progress.getAttribute('aria-valuetext')).toBe('已审阅 2 / 3')
|
||||
await compareOrRefreshGolden(join(EXPECTED, 'review-layers.expected.md'),
|
||||
await captureStableAria(page, '[data-forge-changes="change"]', scaffold.workspaceCwd), MODE)
|
||||
|
||||
await view.getByRole('switch', { name: '隐藏测试文件', exact: true }).click()
|
||||
const commits = view.getByRole('button', { name: '查看的提交:全部提交', exact: true })
|
||||
await commits.click()
|
||||
const menu = page.locator('[data-forge-commit-menu]')
|
||||
await expect.poll(() => menu.getByRole('menuitem').evaluateAll(rows => rows.map(row => row.querySelector('[data-forge-commit]')?.getAttribute('data-forge-commit') ?? 'all')))
|
||||
.toEqual(['all', '1'.repeat(40), '2'.repeat(40), HEAD7])
|
||||
await menu.locator(`[data-forge-commit="${'2'.repeat(40)}"]`).click()
|
||||
await expect.poll(() => shownPaths(view), { timeout: 20_000 }).toEqual(['locale/zh.json', 'src/panel.spec.ts'])
|
||||
expect(await view.getByRole('button', { name: '提交评审', exact: true }).isDisabled()).toBe(true)
|
||||
await view.getByRole('button', { name: '查看的提交:2222222', exact: true }).click()
|
||||
await page.locator(`[data-forge-commit="${HEAD7}"]`).click({ modifiers: ['Shift'] })
|
||||
await view.getByRole('button', { name: `查看的提交:2222222…${HEAD7.slice(0, 7)}`, exact: true }).waitFor()
|
||||
await expect.poll(() => shownPaths(view), { timeout: 20_000 }).toEqual(['docs/schema.json', 'locale/zh.json', 'src/panel.spec.ts', 'package.json'])
|
||||
await expect.poll(() => lens.getByRole('radio').allTextContents()).toEqual(['全部4', '接口1', '实现0', '测试3'])
|
||||
const calls = (await readFile(join(root, 'calls.jsonl'), 'utf8')).split('\n').filter(line => line.length > 0).map(line => JSON.parse(line) as string[])
|
||||
// The whole pull request is read from its recorded base; chosen commits are read from the first one's parent.
|
||||
expect(calls.filter(args => args[0] === 'api' && args.includes('Accept: application/vnd.github.diff')).map(args => args.at(-1))
|
||||
.filter(path => path?.includes(`/compare/${'e'.repeat(40)}...`) === false)).toEqual([
|
||||
`repos/octo/demo/compare/${'1'.repeat(40)}...${'2'.repeat(40)}`, `repos/octo/demo/compare/${'1'.repeat(40)}...${HEAD7}`,
|
||||
])
|
||||
await view.getByRole('button', { name: `查看的提交:2222222…${HEAD7.slice(0, 7)}`, exact: true }).click()
|
||||
await page.locator('[data-forge-commit-menu]').getByRole('menuitem', { name: /^全部提交/ }).click()
|
||||
await expect.poll(() => shownPaths(view), { timeout: 20_000 }).toHaveLength(6)
|
||||
await expect.poll(() => docs.getAttribute('data-viewed')).toBe('viewed')
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
})
|
||||
|
||||
it.skipIf(MODE === 'record')('keeps local viewed marks across reloads, clears a mark when the file changes again, and views branch commits', async () => {
|
||||
const { scaffold, page, project, tripwire } = await launchPanel('web-e2e-forge-review-local', 'normal', { branch: 'feat' })
|
||||
await git(project, 'update-ref', 'refs/remotes/origin/main', 'main')
|
||||
await git(project, 'symbolic-ref', 'refs/remotes/origin/HEAD', 'refs/remotes/origin/main')
|
||||
await mkdir(join(project, 'src'))
|
||||
for (const [path, text, message] of [
|
||||
['src/app.ts', 'export const app = 1\n', 'Add the app'],
|
||||
['src/app.spec.ts', 'it(\'runs\', () => {})\n', 'Test the app'],
|
||||
] as const) {
|
||||
await writeFile(join(project, path), text)
|
||||
await git(project, 'add', path)
|
||||
await git(project, 'commit', '--quiet', '-m', message)
|
||||
}
|
||||
await writeFile(join(project, 'notes.md'), 'note\n')
|
||||
const { settled } = await sendPrompt(page, scaffold)
|
||||
await settled
|
||||
await page.getByRole('button', { name: '打开右侧边栏', exact: true }).click()
|
||||
await page.locator('[data-sidebar-right-guide]').getByRole('button', { name: /^代码审查和变更/ }).click()
|
||||
await page.locator('[data-forge-panel]').getByRole('button', { name: '刷新状态', exact: true }).click()
|
||||
const card = page.locator('[data-forge-local]')
|
||||
await card.getByRole('button', { name: /^查看本会话的变更:3 个文件/ }).click({ timeout: 20_000 })
|
||||
const local = page.locator('[data-forge-changes="local"]')
|
||||
const app = local.locator('[data-forge-diff-file="src/app.ts"]')
|
||||
await app.getByRole('checkbox', { name: '将 app.ts 标记为已审阅', exact: true }).check({ timeout: 20_000 })
|
||||
await expect.poll(() => app.getAttribute('data-expanded')).toBe(null)
|
||||
|
||||
await page.reload()
|
||||
await page.locator('[data-forge-changes="local"] [data-forge-diff-file="src/app.ts"]').waitFor({ timeout: 20_000 })
|
||||
await expect.poll(() => app.getAttribute('data-viewed')).toBe('viewed')
|
||||
expect(await app.getAttribute('data-expanded')).toBe(null)
|
||||
await writeFile(join(project, 'src', 'app.ts'), 'export const app = 2\n')
|
||||
await local.getByRole('button', { name: '刷新变更', exact: true }).click()
|
||||
await expect.poll(() => app.getAttribute('data-viewed'), { timeout: 20_000 }).toBe('changed')
|
||||
await app.getByText('有新改动', { exact: true }).waitFor()
|
||||
await expect.poll(() => app.getAttribute('data-expanded')).toBe('')
|
||||
expect(await app.getByRole('checkbox', { name: '将 app.ts 标记为已审阅', exact: true }).isChecked()).toBe(false)
|
||||
|
||||
await local.getByRole('button', { name: '查看的提交:全部提交', exact: true }).click()
|
||||
const menu = page.locator('[data-forge-commit-menu]')
|
||||
await menu.getByRole('menuitem', { name: /Test the app/ }).click()
|
||||
await expect.poll(() => shownPaths(local), { timeout: 20_000 }).toEqual(['src/app.spec.ts'])
|
||||
// Without the file tree beside the diff, the toolbar offers the lens as a menu.
|
||||
await local.getByRole('button', { name: '分层透镜:全部', exact: true }).click()
|
||||
await expect.poll(() => page.getByRole('menu').getByRole('menuitem').allTextContents()).toEqual(['全部1', '接口0', '实现0', '测试1'])
|
||||
await page.keyboard.press('Escape')
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
})
|
||||
|
||||
/**
|
||||
* One review thread node of pull request 7 as the GraphQL thread read returns it, its comments by `lin`, the viewer, then
|
||||
* `octo`, the pull request's author; the viewer may reply, resolve, and change their own comments.
|
||||
*/
|
||||
function threadNode(
|
||||
id: string, fields: Record<string, unknown>, bodies: readonly string[], commit: string = HEAD7,
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
id, isOutdated: false, isResolved: false, path: 'src/panel.ts', diffSide: 'RIGHT', startDiffSide: null, line: null, startLine: null,
|
||||
originalLine: null, originalStartLine: null, subjectType: 'LINE', viewerCanReply: true, viewerCanResolve: true, viewerCanUnresolve: true, ...fields,
|
||||
root: { nodes: [{ diffHunk: '@@ -1,5 +1,5 @@\n export const n1 = 1\n-export const n2 = 2\n+export const n2 = \'two\'',
|
||||
commit: { oid: commit }, originalCommit: { oid: commit } }] },
|
||||
comments: { pageInfo: { hasNextPage: false, endCursor: null }, nodes: bodies.map((body, index) => ({
|
||||
id: `${id}-${index}`, state: 'SUBMITTED', createdAt: '2026-10-09T00:00:00Z', url: `${PULL}#discussion_${id}_${index}`, body,
|
||||
viewerCanUpdate: index === 0, viewerCanDelete: index === 0, author: { __typename: 'User', login: index === 0 ? 'lin' : 'octo', avatarUrl: null },
|
||||
})) },
|
||||
}
|
||||
}
|
||||
|
||||
/** Threads of pull request 7: on new line 2, on old line 15, resolved on new line 15, outdated, and on the whole of `docs/panel.md`. */
|
||||
const THREADS = [
|
||||
threadNode('on-line', { line: 2, originalLine: 2 }, ['Why **two**?', 'It reads better.']),
|
||||
threadNode('old-side', { diffSide: 'LEFT', line: 15, originalLine: 15 }, ['The old fifteen was a number.']),
|
||||
threadNode('resolved', { isResolved: true, line: 15, originalLine: 15 }, ['Settled.']),
|
||||
threadNode('outdated', { isOutdated: true, originalLine: 9 }, ['This line moved.'], 'f'.repeat(40)),
|
||||
threadNode('whole-file', { path: 'docs/panel.md', subjectType: 'FILE' }, ['Add a screenshot.']),
|
||||
]
|
||||
|
||||
it.skipIf(MODE === 'record')('overlays the review threads of a change request and opens them from the thread list', async () => {
|
||||
const { scaffold, page, root, tripwire } = await launchPanel('web-e2e-forge-review-threads', 'follow-up')
|
||||
await writeFile(join(root, 'threads.json'), JSON.stringify(THREADS))
|
||||
// Comments were written two hours before the browser's clock, so their relative times stay fixed.
|
||||
await page.clock.setFixedTime(Date.parse('2026-10-09T02:00:00Z'))
|
||||
const { view, sessionId } = await openReview(page, scaffold)
|
||||
const panel = view.locator('[data-forge-diff-file="src/panel.ts"]')
|
||||
const onLine = panel.locator('[data-forge-thread="on-line"]')
|
||||
await onLine.getByText('two', { exact: true }).waitFor({ timeout: 20_000 })
|
||||
// The thread follows the row of its last line, under the new side.
|
||||
expect(await onLine.evaluate(card => card.previousElementSibling?.querySelector('[data-new-line]')?.textContent)).toBe('export const n2 = \'two\'')
|
||||
expect(await panel.locator('[data-forge-thread="old-side"]').evaluate(card => card.previousElementSibling?.querySelector('[data-old-line]')?.textContent))
|
||||
.toBe('export const n15 = 15')
|
||||
await panel.locator('[data-forge-thread="resolved"]').getByText('已解决的讨论 · lin', { exact: true }).waitFor()
|
||||
await panel.locator('[data-forge-outdated-threads]').getByText('1 条讨论已过期:所在代码已被后续提交修改', { exact: true }).waitFor()
|
||||
await view.locator('[data-forge-diff-file="docs/panel.md"] [data-forge-thread="whole-file"]').getByText('文件级讨论', { exact: true }).waitFor()
|
||||
const count = panel.getByRole('button', { name: 'panel.ts 有 3 条未解决的讨论,跳到下一条', exact: true })
|
||||
await count.waitFor()
|
||||
await view.getByRole('navigation').getByTitle('3 条未解决的讨论', { exact: true }).waitFor()
|
||||
await compareOrRefreshGolden(join(EXPECTED, 'review-threads.expected.md'),
|
||||
await captureStableAria(page, '[data-forge-diff-file="src/panel.ts"]', scaffold.workspaceCwd), MODE)
|
||||
|
||||
// The count steps through the file's unresolved threads in display order: R2, L15, then the outdated one.
|
||||
const centered = async (id: string): Promise<boolean> => await panel.locator(`[data-forge-thread="${id}"]`).evaluate((card) => {
|
||||
const viewport = card.closest('[data-forge-changes-scroll]')!.getBoundingClientRect()
|
||||
const box = card.getBoundingClientRect()
|
||||
return box.top + box.height / 2 > viewport.top && box.top + box.height / 2 < viewport.bottom
|
||||
})
|
||||
for (const id of ['on-line', 'old-side', 'outdated']) {
|
||||
await count.click()
|
||||
await expect.poll(() => centered(id)).toBe(true)
|
||||
}
|
||||
expect(await view.locator('[data-forge-thread-list]').count()).toBe(0)
|
||||
|
||||
await view.getByRole('button', { name: '讨论列表,共 5 个', exact: true }).click()
|
||||
const list = view.getByRole('complementary', { name: '讨论列表,共 5 个' })
|
||||
await expect.poll(() => list.getByRole('radio', { checked: true }).allTextContents()).toEqual(['未解决4'])
|
||||
await expect.poll(() => list.locator('[data-forge-thread-item]').evaluateAll(items => items.map(item => item.getAttribute('data-forge-thread-item'))))
|
||||
.toEqual(['whole-file', 'on-line', 'old-side', 'outdated'])
|
||||
await list.getByRole('radio', { name: /^已解决/ }).click()
|
||||
await list.locator('[data-forge-thread-item="resolved"]').click()
|
||||
// Opening a resolved thread unfolds it inside its file, and the docked list stays.
|
||||
await panel.locator('[data-forge-thread="resolved"]').getByText('Settled.', { exact: true }).waitFor()
|
||||
await list.getByRole('radio', { name: /^全部/ }).click()
|
||||
await expect.poll(() => list.locator('[data-forge-thread-item]').count()).toBe(5)
|
||||
await compareOrRefreshGolden(join(EXPECTED, 'review-thread-list.expected.md'),
|
||||
await captureStableAria(page, '[data-forge-thread-list]', scaffold.workspaceCwd), MODE)
|
||||
// A view narrower than 480px gives the list the files' whole width and closes it before opening a thread.
|
||||
await view.evaluate((element) => { element.style.width = '420px'; element.style.flex = 'none' })
|
||||
await expect.poll(() => list.evaluate(element => [getComputedStyle(element).position, element.getBoundingClientRect().width]))
|
||||
.toEqual(['absolute', 420])
|
||||
await list.locator('[data-forge-thread-item="on-line"]').click()
|
||||
await list.waitFor({ state: 'detached' })
|
||||
await view.evaluate((element) => { element.style.width = ''; element.style.flex = '' })
|
||||
|
||||
await onLine.getByRole('button', { name: '添加到输入框', exact: true }).click()
|
||||
await page.locator('[data-composer-input]').first().getByText('panel.ts R2', { exact: true }).first().waitFor()
|
||||
const sent = scaffold.whenTurnSettled()
|
||||
await page.locator('[data-composer-input]').first().press('Enter')
|
||||
await sent
|
||||
await page.getByText(FOLLOW_UP_REPLY, { exact: true }).waitFor()
|
||||
// The quoted thread reaches the model only inside the ordinary logged user message.
|
||||
const quotes = (await readPersistedEvents(scaffold, sessionId)).flatMap(event => event.type === 'user/message'
|
||||
? event.data.content.flatMap(block => block.type === 'text' && block.text.includes('## Review Thread') ? [block.text] : []) : [])
|
||||
expect(quotes).toHaveLength(1)
|
||||
await compareOrRefreshGolden(join(EXPECTED, 'review-thread-message.expected.md'), quotes.join('').trimEnd(), MODE)
|
||||
const calls = (await readFile(join(root, 'calls.jsonl'), 'utf8')).split('\n').filter(line => line.length > 0).map(line => JSON.parse(line) as string[])
|
||||
const reads = calls.filter(args => args.some(arg => arg.startsWith('query=') && arg.includes('viewer { login }')))
|
||||
expect(reads.length).toBeGreaterThan(0)
|
||||
expect(reads[0]).toEqual(expect.arrayContaining(['owner=octo', 'name=demo', 'number=7', 'first=100']))
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
})
|
||||
|
||||
it.skipIf(MODE === 'record')('replies to, edits, deletes, and resolves review threads, and discards a pending review started from a reply', async () => {
|
||||
const { scaffold, page, root, tripwire } = await launchPanel('web-e2e-forge-review-writes')
|
||||
await writeFile(join(root, 'threads.json'), JSON.stringify(THREADS))
|
||||
await page.clock.setFixedTime(Date.parse('2026-10-09T02:00:00Z'))
|
||||
const { view } = await openReview(page, scaffold)
|
||||
const panel = view.locator('[data-forge-diff-file="src/panel.ts"]')
|
||||
const onLine = panel.locator('[data-forge-thread="on-line"]')
|
||||
await onLine.getByText('two', { exact: true }).waitFor({ timeout: 20_000 })
|
||||
|
||||
await onLine.getByRole('button', { name: '回复…', exact: true }).click()
|
||||
await onLine.getByRole('textbox', { name: '回复…', exact: true }).fill('已改为字符串')
|
||||
await compareOrRefreshGolden(join(EXPECTED, 'review-thread-reply.expected.md'),
|
||||
await captureStableAria(page, '[data-forge-thread="on-line"]', scaffold.workspaceCwd), MODE)
|
||||
await onLine.getByRole('button', { name: '回复', exact: true }).click()
|
||||
await onLine.getByText('已改为字符串', { exact: true }).waitFor({ timeout: 20_000 })
|
||||
|
||||
await onLine.getByRole('button', { name: 'lin 的评论操作', exact: true }).first().click()
|
||||
await page.getByRole('menuitem', { name: '编辑', exact: true }).click()
|
||||
await onLine.getByRole('textbox', { name: '编辑评论', exact: true }).fill('Why a **word**?')
|
||||
await onLine.getByRole('button', { name: '保存', exact: true }).click()
|
||||
await onLine.getByText('word', { exact: true }).waitFor({ timeout: 20_000 })
|
||||
|
||||
await onLine.getByRole('button', { name: 'lin 的评论操作', exact: true }).last().click()
|
||||
await page.getByRole('menuitem', { name: '删除', exact: true }).click()
|
||||
await onLine.getByRole('group', { name: '删除这条评论?', exact: true }).getByRole('button', { name: '删除', exact: true }).click()
|
||||
await expect.poll(() => onLine.getByText('已改为字符串', { exact: true }).count(), { timeout: 20_000 }).toBe(0)
|
||||
|
||||
await onLine.getByRole('button', { name: '解决讨论', exact: true }).click()
|
||||
await onLine.getByText('已解决的讨论 · lin', { exact: true }).waitFor({ timeout: 20_000 })
|
||||
|
||||
const oldSide = panel.locator('[data-forge-thread="old-side"]')
|
||||
await oldSide.getByRole('button', { name: '回复…', exact: true }).click()
|
||||
await oldSide.getByRole('textbox', { name: '回复…', exact: true }).fill('稍后一起提交')
|
||||
await oldSide.getByRole('button', { name: '回复选项', exact: true }).click()
|
||||
await page.getByRole('menuitem', { name: '开始评审', exact: true }).click()
|
||||
await oldSide.getByText('待提交', { exact: true }).waitFor({ timeout: 20_000 })
|
||||
await view.getByRole('button', { name: '提交评审 (1)', exact: true }).click()
|
||||
const dialog = page.getByRole('dialog', { name: '提交评审', exact: true })
|
||||
await dialog.getByRole('button', { name: '丢弃评审', exact: true }).click()
|
||||
await compareOrRefreshGolden(join(EXPECTED, 'discard-review.expected.md'),
|
||||
await captureStableAria(page, '[role="dialog"][aria-label="提交评审"]', scaffold.workspaceCwd), MODE)
|
||||
await dialog.getByRole('group', { name: '丢弃评审', exact: true }).getByRole('button', { name: '丢弃评审', exact: true }).click()
|
||||
await page.getByRole('alert').filter({ hasText: '已丢弃评审' }).waitFor()
|
||||
await expect.poll(() => oldSide.getByText('稍后一起提交', { exact: true }).count(), { timeout: 20_000 }).toBe(0)
|
||||
await view.getByRole('button', { name: '提交评审', exact: true }).waitFor()
|
||||
|
||||
const mutations = (await readFile(join(root, 'mutations.jsonl'), 'utf8')).trim().split('\n').map((line): unknown => JSON.parse(line))
|
||||
expect(mutations).toEqual([
|
||||
{ field: 'addPullRequestReviewThreadReply', variables: { thread: 'on-line', body: '已改为字符串', review: null } },
|
||||
{ field: 'updatePullRequestReviewComment', variables: { comment: 'on-line-0', body: 'Why a **word**?' } },
|
||||
{ field: 'deletePullRequestReviewComment', variables: { comment: 'on-line-reply-0' } },
|
||||
{ field: 'resolveReviewThread', variables: { thread: 'on-line' } },
|
||||
{ field: 'addPullRequestReview', variables: { pull: 'pull-7', commit: HEAD7, threads: null } },
|
||||
{ field: 'addPullRequestReviewThreadReply', variables: { thread: 'old-side', body: '稍后一起提交', review: 'review-1' } },
|
||||
{ field: 'deletePullRequestReview', variables: { review: 'review-1' } },
|
||||
])
|
||||
// Every write checked the head of pull request 7 first.
|
||||
const calls = (await readFile(join(root, 'calls.jsonl'), 'utf8')).split('\n').filter(line => line.length > 0).map(line => JSON.parse(line) as string[])
|
||||
expect(calls.filter(args => args.some(arg => arg.startsWith('query=') && arg.includes('reviews(states: PENDING') && !arg.includes('viewer { login }'))))
|
||||
.toHaveLength(mutations.length - 1)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
})
|
||||
|
||||
it.skipIf(MODE === 'record')('keeps a narrow file header\'s viewed mark and menu inside its card and its name under the pointer', async () => {
|
||||
const { scaffold, page, project, tripwire } = await launchPanel('web-e2e-forge-narrow-header', 'normal', { branch: 'feat' })
|
||||
const from = 'src/original-review-module-with-a-long-name.ts'
|
||||
const to = 'src/renamed-review-module-with-an-even-longer-name.ts'
|
||||
const body = Array.from({ length: 20 }, (_, index) => `export const value${index} = ${index}\n`).join('')
|
||||
await mkdir(join(project, 'src'))
|
||||
await writeFile(join(project, from), body)
|
||||
await git(project, 'add', from)
|
||||
await git(project, 'commit', '--quiet', '-m', 'Add the module')
|
||||
// The base holds the old path, so the branch shows a rename.
|
||||
await git(project, 'update-ref', 'refs/remotes/origin/main', 'HEAD')
|
||||
await git(project, 'symbolic-ref', 'refs/remotes/origin/HEAD', 'refs/remotes/origin/main')
|
||||
await git(project, 'mv', from, to)
|
||||
await writeFile(join(project, to), `${body}export const extra = 1\n`)
|
||||
await git(project, 'commit', '--quiet', '-am', 'Rename the module')
|
||||
const { settled } = await sendPrompt(page, scaffold)
|
||||
await settled
|
||||
await page.getByRole('button', { name: '打开右侧边栏', exact: true }).click()
|
||||
await page.locator('[data-sidebar-right-guide]').getByRole('button', { name: /^代码审查和变更/ }).click()
|
||||
await page.locator('[data-forge-panel]').getByRole('button', { name: '刷新状态', exact: true }).click()
|
||||
await page.locator('[data-forge-local]').getByRole('button', { name: /^查看本会话的变更/ }).click({ timeout: 20_000 })
|
||||
const local = page.locator('[data-forge-changes="local"]')
|
||||
const name = 'renamed-review-module-with-an-even-longer-name.ts'
|
||||
const file = local.locator(`[data-forge-diff-file="${to}"]`)
|
||||
const viewed = file.getByRole('checkbox', { name: `将 ${name} 标记为已审阅`, exact: true })
|
||||
await viewed.check({ timeout: 20_000 })
|
||||
await writeFile(join(project, to), `${body}export const extra = 2\n`)
|
||||
await local.getByRole('button', { name: '刷新变更', exact: true }).click()
|
||||
await expect.poll(() => file.getAttribute('data-viewed'), { timeout: 20_000 }).toBe('changed')
|
||||
await file.getByText('由 original-review-module-with-a-long-name.ts 重命名', { exact: true }).waitFor()
|
||||
|
||||
// The changed tag, the rename badge, and the long path compete for one narrow header with the counts and actions.
|
||||
await page.setViewportSize({ width: 390, height: 780 })
|
||||
const label = file.locator('[data-forge-file-label]')
|
||||
const [card, mark, menu, labelBox] = await Promise.all([
|
||||
file.boundingBox(), viewed.boundingBox(), file.getByRole('button', { name: `${name} 的更多操作`, exact: true }).boundingBox(), label.boundingBox(),
|
||||
])
|
||||
if (card === null || mark === null || menu === null || labelBox === null) throw new Error('The header controls must be rendered')
|
||||
expect(mark.x + mark.width).toBeLessThanOrEqual(card.x + card.width)
|
||||
expect(menu.x + menu.width).toBeLessThanOrEqual(card.x + card.width)
|
||||
expect(labelBox.width).toBeGreaterThan(40)
|
||||
await label.hover()
|
||||
await page.getByRole('tooltip').filter({ hasText: `${from} → ${to}` }).waitFor()
|
||||
await viewed.check()
|
||||
await expect.poll(() => file.getAttribute('data-viewed')).toBe('viewed')
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
})
|
||||
@@ -0,0 +1,119 @@
|
||||
/** Reasoning rows of one Assistant message keep the 4px process-row gap, and display-empty reasoning adds none. */
|
||||
import { chromium } from 'playwright'
|
||||
import { expect, it, onTestFinished } from 'vitest'
|
||||
import { SESSION_FORMAT_VERSION } from '@deepseek-ai/dsh-session'
|
||||
import { launchWebScaffold, seedSession, watchConsole } from './scaffold.ts'
|
||||
import { newEnglishPage } from './support.ts'
|
||||
|
||||
type Block = readonly ['reasoning' | 'signature' | 'text' | 'bash', string]
|
||||
|
||||
/** Turn 1: reasoning around a signature-only block. Turn 2: signature-only blocks before and after reasoning. */
|
||||
const TURNS: readonly (readonly (readonly Block[])[])[] = [
|
||||
[[['reasoning', 'a'], ['signature', 's1'], ['reasoning', 'b'], ['bash', 'k1']], [['text', 'Answer one']]],
|
||||
[
|
||||
[['reasoning', 'c'], ['bash', 'k2']],
|
||||
[['signature', 's2'], ['signature', 's3'], ['reasoning', 'd'], ['bash', 'k3']],
|
||||
[['reasoning', 'e'], ['signature', 's4'], ['signature', 's5'], ['bash', 'k4']],
|
||||
[['text', 'Answer two']],
|
||||
],
|
||||
]
|
||||
|
||||
function bashArguments(label: string): string {
|
||||
return JSON.stringify({ command: `echo ${label}`, description: `Run ${label}` })
|
||||
}
|
||||
|
||||
function content([kind, label]: Block): Record<string, string> {
|
||||
switch (kind) {
|
||||
case 'reasoning': return { type: 'reasoning', text: `Thinking ${label}\n\nDetails for ${label}.` }
|
||||
case 'signature': return { type: 'reasoning', text: `<signature>${label}</signature>` }
|
||||
case 'text': return { type: 'text', text: label }
|
||||
case 'bash': return {
|
||||
type: 'tool-call', id: `call_${label}`, name: 'bash', arguments: bashArguments(label),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** One closed Session whose Assistant messages carry the block layouts above. */
|
||||
function sessionLog(): string {
|
||||
const header = {
|
||||
type: 'session', version: SESSION_FORMAT_VERSION, id: '{{sessionId}}', createdAt: 0, cwd: '{{cwd}}',
|
||||
isSeeded: false, delegationDepth: 0,
|
||||
}
|
||||
const events: Record<string, unknown>[] = []
|
||||
let message = 0
|
||||
const id = (): string => `{{message:${String(++message)}}}`
|
||||
TURNS.forEach((steps, index) => {
|
||||
const turn = index + 1
|
||||
events.push({ type: 'turn/start', data: { turn } })
|
||||
steps.forEach((blocks, stepIndex) => {
|
||||
const step = stepIndex + 1
|
||||
events.push({ type: 'step/start', data: { turn, step } })
|
||||
if (step === 1) {
|
||||
events.push({
|
||||
type: 'user/message', surfaceOp: 'append',
|
||||
data: { content: [{ type: 'text', text: `Turn ${String(turn)}` }], source: { kind: 'user' }, role: 'user', id: id() },
|
||||
})
|
||||
}
|
||||
events.push({
|
||||
type: 'assistant/message', surfaceOp: 'append',
|
||||
data: {
|
||||
turn, step, stream: [],
|
||||
message: {
|
||||
role: 'assistant', content: blocks.map(content), id: id(),
|
||||
source: { kind: 'model', provider: 'deepseek-official', model: 'deepseek-v4-flash' },
|
||||
},
|
||||
},
|
||||
})
|
||||
for (const [kind, label] of blocks) {
|
||||
if (kind !== 'bash') continue
|
||||
const callId = `call_${label}`
|
||||
events.push({ type: 'tool/call', data: { turn, step, callId, name: 'bash', arguments: bashArguments(label) } })
|
||||
events.push({
|
||||
type: 'tool/result', surfaceOp: 'append', sourceEventSeqs: [events.length - 1],
|
||||
data: {
|
||||
turn, step, meta: { cwd: '{{cwd}}' },
|
||||
message: {
|
||||
role: 'tool', source: { kind: 'tool', callId }, toolCallId: callId,
|
||||
content: [{ type: 'text', text: `${label}\n` }], isError: false, id: id(),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
events.push({ type: 'step/end', data: { turn, step } })
|
||||
})
|
||||
events.push({ type: 'turn/end', data: { turn, reason: { kind: 'completed' } } })
|
||||
})
|
||||
return [header, ...events].map(line => JSON.stringify(line)).join('\n')
|
||||
}
|
||||
|
||||
it('keeps 4px between reasoning and tool rows around display-empty reasoning blocks', async () => {
|
||||
const scaffold = await launchWebScaffold({})
|
||||
onTestFinished(() => scaffold.close())
|
||||
await scaffold.ctx.settings.update('ui-chat', { transcriptView: 'verbose' })
|
||||
await seedSession(scaffold, sessionLog(), 'reasoning-spacing')
|
||||
const browser = await chromium.launch()
|
||||
onTestFinished(() => browser.close())
|
||||
const page = await newEnglishPage(browser)
|
||||
const console = watchConsole(page)
|
||||
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
|
||||
await page.locator('[role="treeitem"]').first().click()
|
||||
await page.locator('[role="treeitem"]').nth(1).click()
|
||||
await page.getByText('Answer two', { exact: true }).waitFor()
|
||||
|
||||
// Rows in reading order; signature-only reasoning renders no row of its own.
|
||||
const rows = await page.evaluate(() => [...document.querySelectorAll('[data-variant="think"], [data-chat-call-id]')]
|
||||
.filter(row => row.getBoundingClientRect().height > 0)
|
||||
.map((row) => {
|
||||
const { top, bottom } = row.getBoundingClientRect()
|
||||
const name = row.matches('[data-variant="think"]')
|
||||
? /Thinking (\w+)/.exec(row.textContent ?? '')?.[1]
|
||||
: row.getAttribute('data-chat-call-id')?.replace('call_', '')
|
||||
return { name: name ?? '?', top, bottom }
|
||||
})
|
||||
.sort((left, right) => left.top - right.top))
|
||||
expect(rows.map(row => row.name)).toEqual(['a', 'b', 'k1', 'c', 'k2', 'd', 'k3', 'e', 'k4'])
|
||||
const gaps = Object.fromEntries(rows.slice(1).map((row, index) =>
|
||||
[`${rows[index]!.name}→${row.name}`, Math.round(row.top - rows[index]!.bottom)]))
|
||||
expect(gaps).toMatchObject({ 'a→b': 4, 'b→k1': 4, 'c→k2': 4, 'k2→d': 4, 'd→k3': 4, 'k3→e': 4, 'e→k4': 4 })
|
||||
expect(console.pageErrors).toEqual([])
|
||||
})
|
||||
@@ -124,14 +124,24 @@ describe('web e2e: fresh round trip through the real assembly', () => {
|
||||
let accepted = false
|
||||
try {
|
||||
await input.fill(PROMPT)
|
||||
// Hold the prompt POST at the browser boundary so the Host cannot accept
|
||||
// the message before the pre-accept snapshot is read.
|
||||
const promptSent = Promise.withResolvers<undefined>()
|
||||
const releasePrompt = Promise.withResolvers<undefined>()
|
||||
const holdPrompt = async (route: Route): Promise<void> => {
|
||||
promptSent.resolve(undefined)
|
||||
await releasePrompt.promise
|
||||
await route.continue()
|
||||
}
|
||||
await page.route('**/api/session/prompt', holdPrompt, { times: 1 })
|
||||
const admitted = page.waitForResponse('**/api/session/prompt')
|
||||
const [echoSnapshot, response] = await Promise.all([
|
||||
input.evaluate(async (element, prompt) => {
|
||||
try {
|
||||
const echoCapture = input.evaluate(async (element, prompt) => {
|
||||
element.dispatchEvent(new KeyboardEvent('keydown', {
|
||||
key: 'Enter', code: 'Enter', bubbles: true, cancelable: true,
|
||||
}))
|
||||
// sendSession registered its paint yield first. This frame observes the
|
||||
// composer holding the message while the Host has not accepted it yet.
|
||||
// sendSession registered its paint yield first; this frame observes the
|
||||
// composer holding the message while the POST is still held.
|
||||
await new Promise<void>((resolve) => { requestAnimationFrame(() => { resolve() }) })
|
||||
const echo = document.querySelector<HTMLElement>('[data-submission-echo]')
|
||||
return [
|
||||
@@ -139,12 +149,15 @@ describe('web e2e: fresh round trip through the real assembly', () => {
|
||||
`composer: ${JSON.stringify(element.textContent ?? '')}`,
|
||||
`contenteditable: ${element.getAttribute('contenteditable')}`,
|
||||
].join('\n')
|
||||
}, PROMPT),
|
||||
admitted,
|
||||
])
|
||||
}, PROMPT)
|
||||
await promptSent.promise
|
||||
await compareOrRefreshGolden(ECHO_EXPECTED, await echoCapture, MODE)
|
||||
} finally {
|
||||
releasePrompt.resolve(undefined)
|
||||
}
|
||||
const response = await admitted
|
||||
expect(await response.json()).toMatchObject({ result: { ok: true, value: { accepted: true } } })
|
||||
accepted = true
|
||||
await compareOrRefreshGolden(ECHO_EXPECTED, echoSnapshot, MODE)
|
||||
// Acceptance publishes the echo and clears the composer; maintenance keeps the Turn from starting.
|
||||
await page.locator('[data-submission-echo]', { hasText: PROMPT }).waitFor()
|
||||
await expect.poll(() => input.textContent()).toBe('')
|
||||
|
||||
@@ -479,7 +479,6 @@ const EXPECTED_TOOLS = [
|
||||
'edit',
|
||||
'exit_plan_mode',
|
||||
'get_goal',
|
||||
'interrupt_agent',
|
||||
'job_kill',
|
||||
'job_list',
|
||||
'job_output',
|
||||
|
||||
@@ -365,6 +365,61 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
|
||||
if (failures.length > 1) throw new AggregateError(failures, 'subagent Web teardown failed')
|
||||
})
|
||||
|
||||
it('retries a failed cold catalog and discovers descendants without reopening the menu', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-subagent-unread-catalog'))
|
||||
const reads = apiCalls.filter(path => path === '/api/session/projections').length
|
||||
const observe = vi.spyOn(scaffold.ctx.sessionQuery, 'observeSession')
|
||||
const projections = vi.spyOn(scaffold.ctx.sessionController, 'projections')
|
||||
try {
|
||||
const trigger = page.getByRole('button', { name: '2 subagents', exact: true })
|
||||
await page.locator('[data-composer-input]').first().hover()
|
||||
await trigger.hover()
|
||||
const tree = page.getByRole('tree', { name: 'Subagent sessions' })
|
||||
const child = tree.getByRole('treeitem', { name: new RegExp(LABEL) })
|
||||
await child.waitFor()
|
||||
expect(await child.getAttribute('aria-expanded')).toBeNull()
|
||||
expect(await tree.getByRole('button', { name: /^(Expand|Collapse) /u }).count()).toBe(0)
|
||||
expect(await tree.getByText(/Loading subagents|unknown/u).count()).toBe(0)
|
||||
expect(apiCalls.filter(path => path === '/api/session/projections')).toHaveLength(reads)
|
||||
expect(observe).not.toHaveBeenCalled()
|
||||
const parentUrl = page.url()
|
||||
await page.route('**/api/session/projections', route => route.abort('failed'), { times: 1 })
|
||||
await trigger.click()
|
||||
const retry = tree.getByRole('button', { name: 'Retry', exact: true })
|
||||
await retry.waitFor()
|
||||
expect(await tree.getByRole('button', { name: /^(Expand|Collapse) /u }).count()).toBe(0)
|
||||
expect(apiCalls.filter(path => path === '/api/session/projections')).toHaveLength(reads + 1)
|
||||
expect(projections).not.toHaveBeenCalled()
|
||||
await retry.click()
|
||||
await retry.waitFor({ state: 'detached' })
|
||||
const expand = page.getByRole('button', { name: `Expand ${LABEL} descendants`, exact: true })
|
||||
await expand.waitFor()
|
||||
await expect.poll(() => projections.mock.calls.map(([request]) => request.sessionId)).toEqual([parentId, childId, oneShotId])
|
||||
expect(observe.mock.calls.some(([id]) => id === grandchildId)).toBe(false)
|
||||
const lastRead = page.waitForResponse('**/api/session/projections')
|
||||
await expand.click()
|
||||
await (await lastRead).finished()
|
||||
await expect.poll(() => projections.mock.calls.map(([request]) => request.sessionId))
|
||||
.toEqual([parentId, childId, oneShotId, grandchildId])
|
||||
const grandchild = tree.getByRole('treeitem', { name: new RegExp(NESTED_LABEL) })
|
||||
await grandchild.waitFor()
|
||||
expect(await grandchild.getAttribute('aria-expanded')).toBeNull()
|
||||
await compareOrRefreshGolden(
|
||||
TREE_EXPECTED,
|
||||
await captureStableAria(page, '[role="tree"][aria-label="Subagent sessions"]', scaffold.workspaceCwd), MODE,
|
||||
)
|
||||
expect(page.url()).toBe(parentUrl)
|
||||
expect(scaffold.ctx.agents.get(childId)).toBeUndefined()
|
||||
expect(scaffold.ctx.agents.get(grandchildId)).toBeUndefined()
|
||||
await page.keyboard.press('Escape')
|
||||
await page.locator('[data-subagent-panel]').waitFor({ state: 'detached' })
|
||||
} finally {
|
||||
await page.unroute('**/api/session/projections')
|
||||
observe.mockRestore()
|
||||
projections.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
it('lists direct subagents by label in the reference menu', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-subagent-reference-title'))
|
||||
expect(liveReferenceOptions.some(text => text.includes(LABEL))).toBe(true)
|
||||
@@ -424,7 +479,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
|
||||
}
|
||||
expect(await row.getAttribute('aria-label')).toContain(`${LABEL} ${CHILD_NAME} `)
|
||||
expect(await row.getByRole('button', { name: /description/u }).count()).toBe(0)
|
||||
expect(await row.getByRole('button', { name: `Expand ${LABEL} descendants`, exact: true }).count()).toBe(1)
|
||||
await row.getByRole('button', { name: `Expand ${LABEL} descendants`, exact: true }).waitFor()
|
||||
expect(await row.getByRole('button', { name: `Open ${LABEL} in sidebar`, exact: true }).count()).toBe(1)
|
||||
expect(page.url()).toBe(originalUrl)
|
||||
expect(await page.getByRole('tooltip').count()).toBe(0)
|
||||
@@ -438,9 +493,34 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
|
||||
}
|
||||
})
|
||||
|
||||
it('recovers a current-format cold child catalog cache miss without activating either level', async () => {
|
||||
it('expands locally available cold catalogs without requesting or activating either level', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'screenshots/non-migrating-session-reads/web-e2e-subagent-tree'))
|
||||
await page.getByRole('button', { name: '2 subagents' }).hover()
|
||||
const warningStart = tripwire.warnings.length
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
const trigger = page.getByRole('button', { name: '2 subagents', exact: true })
|
||||
await trigger.waitFor()
|
||||
acknowledgeReloadConnectionLoss(tripwire, warningStart)
|
||||
const siblingsRead = page.waitForResponse((response) => {
|
||||
if (!response.url().endsWith('/api/session/projections')) return false
|
||||
const body = response.request().postDataJSON() as { payload: { args: { request: { sessionId: SessionId } } } }
|
||||
return body.payload.args.request.sessionId === oneShotId
|
||||
})
|
||||
await trigger.click()
|
||||
await (await siblingsRead).finished()
|
||||
const expand = page.getByRole('button', { name: `Expand ${LABEL} descendants`, exact: true })
|
||||
await expand.waitFor()
|
||||
const descendantsRead = page.waitForResponse((response) => {
|
||||
if (!response.url().endsWith('/api/session/projections')) return false
|
||||
const body = response.request().postDataJSON() as { payload: { args: { request: { sessionId: SessionId } } } }
|
||||
return body.payload.args.request.sessionId === grandchildId
|
||||
})
|
||||
await expand.click()
|
||||
await (await descendantsRead).finished()
|
||||
await page.keyboard.press('Escape')
|
||||
await page.locator('[data-subagent-panel]').waitFor({ state: 'detached' })
|
||||
const catalogReads = apiCalls.filter(path => path === '/api/session/projections').length
|
||||
await page.locator('[data-composer-input]').first().hover()
|
||||
await trigger.hover()
|
||||
const catalogTree = page.getByRole('tree', { name: 'Subagent sessions' })
|
||||
expect(await catalogTree.evaluate((element) => {
|
||||
const rect = element.getBoundingClientRect()
|
||||
@@ -453,6 +533,10 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
|
||||
name: `Expand ${ONE_SHOT_LABEL} descendants`,
|
||||
})
|
||||
expect(await oneShotDisclosure.count()).toBe(0)
|
||||
await compareOrRefreshGolden(
|
||||
fileURLToPath(new URL('../../../snapshots/web/subagent-conversation/disclosures.expected.md', import.meta.url)),
|
||||
await captureStableAria(page, '[role="tree"][aria-label="Subagent sessions"]', scaffold.workspaceCwd), MODE,
|
||||
)
|
||||
const oneShotRow = page.getByRole('treeitem', { name: new RegExp(ONE_SHOT_LABEL) })
|
||||
expect(await oneShotRow.locator('[data-state="done"]').count()).toBe(1)
|
||||
expect(await oneShotRow.getByText('~6mo 12d', { exact: true }).count()).toBe(1)
|
||||
@@ -501,18 +585,13 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
|
||||
expect(await page.getByText(PARENT_PROMPT, { exact: true }).isVisible()).toBe(true)
|
||||
expect(await page.locator('[data-sidebar-chat]').count()).toBe(0)
|
||||
}
|
||||
const observe = vi.spyOn(scaffold.ctx.sessionQuery, 'observeSession')
|
||||
try {
|
||||
const projections = page.waitForResponse(response => new URL(response.url()).pathname === '/api/session/projections')
|
||||
await clickDisclosureGutter(3)
|
||||
await projections
|
||||
await page.getByRole('treeitem', { name: new RegExp(NESTED_LABEL) }).waitFor({ timeout: 15_000 })
|
||||
expect(await sectionHeader.count()).toBe(0)
|
||||
expect(observe.mock.calls.some(([id]) => id === childId)).toBe(true)
|
||||
expect(observe.mock.calls.some(([id]) => id === grandchildId)).toBe(false)
|
||||
} finally {
|
||||
observe.mockRestore()
|
||||
}
|
||||
await clickDisclosureGutter(3)
|
||||
const grandchildRow = page.getByRole('treeitem', { name: new RegExp(NESTED_LABEL) })
|
||||
await grandchildRow.waitFor({ timeout: 15_000 })
|
||||
expect(await catalogTree.getByText('Loading subagents…', { exact: true }).count()).toBe(0)
|
||||
expect(await grandchildRow.getAttribute('aria-expanded')).toBeNull()
|
||||
expect(await grandchildRow.getByRole('button', { name: `Expand ${NESTED_LABEL} descendants` }).count()).toBe(0)
|
||||
expect(await sectionHeader.count()).toBe(0)
|
||||
await clickDisclosureGutter(26)
|
||||
await page.getByRole('treeitem', { name: new RegExp(NESTED_LABEL) }).waitFor({ state: 'detached' })
|
||||
expect(await childRow.getAttribute('aria-expanded')).toBe('false')
|
||||
@@ -571,6 +650,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
|
||||
scaffold.workspaceCwd,
|
||||
)
|
||||
await compareOrRefreshGolden(TREE_EXPECTED, snapshot, MODE)
|
||||
expect(apiCalls.filter(path => path === '/api/session/projections')).toHaveLength(catalogReads)
|
||||
await page.getByRole('treeitem', { name: new RegExp(NESTED_LABEL) }).getByText(grandchildId, { exact: true }).click()
|
||||
await page.getByText(NESTED_PROMPT).waitFor()
|
||||
const hierarchy = page.getByRole('navigation', { name: 'Session hierarchy' })
|
||||
@@ -994,10 +1074,9 @@ describe('web e2e: migration-required subagent catalog', () => {
|
||||
const tree = page.getByRole('tree', { name: 'Subagent sessions' })
|
||||
const childRow = tree.getByRole('treeitem', { name: new RegExp(childLabel) })
|
||||
await childRow.waitFor()
|
||||
await expect.poll(() => childRow.textContent()).toContain('Migration required')
|
||||
await expect.poll(() => childRow.getByRole('button', { name: `Expand ${childLabel} descendants`, exact: true }).count()).toBe(0)
|
||||
expect(await childRow.getAttribute('aria-expanded')).toBeNull()
|
||||
expect(await childRow.textContent()).toContain('Migration required')
|
||||
expect(await childRow.textContent()).not.toContain('Migration required')
|
||||
expect(await childRow.textContent()).not.toContain('unknown mode')
|
||||
expect(await childRow.getByRole('button', { name: `Open ${childLabel} in sidebar`, exact: true }).isEnabled()).toBe(true)
|
||||
expect(await tree.getByRole('treeitem').count()).toBe(1)
|
||||
@@ -1015,7 +1094,7 @@ describe('web e2e: migration-required subagent catalog', () => {
|
||||
await page.getByRole('button', { name: '1 subagent', exact: true }).hover()
|
||||
const grandchildRow = tree.getByRole('treeitem', { name: new RegExp(grandchildLabel) })
|
||||
await grandchildRow.waitFor()
|
||||
expect(await grandchildRow.textContent()).toContain('Migration required')
|
||||
expect(await grandchildRow.textContent()).not.toContain('Migration required')
|
||||
expect(await grandchildRow.getAttribute('aria-expanded')).toBeNull()
|
||||
expect(await grandchildRow.getByRole('button', { name: `Expand ${grandchildLabel} descendants`, exact: true }).count()).toBe(0)
|
||||
expect(projections).not.toHaveBeenCalled()
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
"tests/trajectory-virtualization.e2e.ts",
|
||||
"tests/lifecycle-chrome.e2e.ts",
|
||||
"tests/reasoning-preview.e2e.ts",
|
||||
"tests/reasoning-spacing.e2e.ts",
|
||||
"tests/details-session-lifecycle.e2e.ts",
|
||||
"tests/sessionless-header.e2e.ts",
|
||||
"tests/document-preview.e2e.ts",
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
en: 20ef882e44b06a10
|
||||
zh: c10c3934ed5570e9
|
||||
/deepseek-harness-architecture/events:
|
||||
en: ddebac7cfd41ec73
|
||||
zh: 51e6d0866a9e7839
|
||||
en: 0d4481070a0ba352
|
||||
zh: 026be77a9f3db68e
|
||||
/deepseek-harness-architecture/turn-flow:
|
||||
en: 1d496f1893bdbb8d
|
||||
zh: f16d162a803fc791
|
||||
|
||||
@@ -74,7 +74,7 @@ Events provide extension points in three domains.
|
||||
|
||||
Subagent Activations own runtime tree membership.
|
||||
|
||||
AgentLoop rolls back failed initialization and awaits serial `agent/created` before queued work; unload retains resources through teardown ([ordering](../packages/core/agent-loop/README.md#understand-the-implementation)).
|
||||
AgentLoop rolls back failed initialization and awaits serial `agent/created` before queued work; shutdown cancels and awaits active work despite queue-cleanup failures before releasing resources ([ordering](../packages/core/agent-loop/README.md#understand-the-implementation)).
|
||||
|
||||
[Event producers and consumers](event-producer-consumer.md).
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ Electron 使用 Electron Node 模式启动私有 Desktop Host。Host 调用共
|
||||
|
||||
Subagent Activation 拥有运行时树成员关系。
|
||||
|
||||
AgentLoop 回滚失败的初始化,并在启动已排队工作前等待串行 `agent/created`;卸载在 teardown 期间保留所需资源([顺序](../packages/core/agent-loop/README.zh.md#understand-the-implementation))。
|
||||
AgentLoop 回滚失败的初始化,并在启动已排队工作前等待串行 `agent/created`;关闭时即使队列清理失败,也会取消并等待当前工作,再释放资源([顺序](../packages/core/agent-loop/README.zh.md#understand-the-implementation))。
|
||||
|
||||
[事件生产者与消费者](event-producer-consumer.zh.md)。
|
||||
|
||||
|
||||
+42
-16
@@ -1081,7 +1081,7 @@ export interface CotTranslationPreferences {
|
||||
## `@deepseek-ai/dsh-experimental-client-ui-forge`
|
||||
|
||||
- `inject`: `forge` · `forgeSession` · `sessionProjections` · `workingDirectory` · `fs` · `subprocess` · `sandbox` · `sandboxPolicy` · `agents`
|
||||
- `source`: [`packages/experimental/client-ui-forge/src/types.ts:345`](../packages/experimental/client-ui-forge/src/types.ts)
|
||||
- `source`: [`packages/experimental/client-ui-forge/src/types.ts:381`](../packages/experimental/client-ui-forge/src/types.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Host settings of the forge panel Remote. */
|
||||
@@ -1109,6 +1109,25 @@ export interface Config {
|
||||
maxUntrackedFiles?: number
|
||||
/** Most UTF-8 bytes of change request diffs kept in memory for later reads at the same head commit and base branch. */
|
||||
maxCachedDiffBytes?: number
|
||||
/** Most commits of the `branch` scope listed; a longer branch lists its newest ones. */
|
||||
maxCommits?: number
|
||||
/**
|
||||
* Globs of files in the `test` layer, which the changes view can hide, beside the built-in rules and checked before
|
||||
* them; matched against repository-relative paths with dot files included. A repository's `.gitattributes` overrides both.
|
||||
*/
|
||||
testPaths?: string[]
|
||||
/**
|
||||
* Globs of files in the `interface` layer beside the built-in rules, matched like `testPaths`; checked after
|
||||
* `testPaths` and before the built-in rules.
|
||||
*/
|
||||
interfacePaths?: string[]
|
||||
/** Most UTF-8 bytes, as JSON, of change request review threads kept in memory for later reads at the same head commit and update time. */
|
||||
maxCachedReviewBytes?: number
|
||||
/**
|
||||
* Age in milliseconds at which review threads kept in memory are read from the code host again; a GitHub pull
|
||||
* request's update time does not change when a thread is resolved.
|
||||
*/
|
||||
reviewRefreshMs?: number
|
||||
}
|
||||
```
|
||||
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-experimental-client-ui-forge -->
|
||||
@@ -1151,8 +1170,6 @@ export interface Config {
|
||||
export interface Config extends CuaConfig {
|
||||
/** Interval in milliseconds between permission and position checks while the native guide is open. */
|
||||
permissionPollIntervalMs: number
|
||||
/** Allow mouseMove to move the visible system pointer when AX hover is unavailable. Off by default to avoid interrupting the user. */
|
||||
systemPointerHover: boolean
|
||||
/**
|
||||
* Source-build signing: `ad-hoc` (default) binds grants to the code hash;
|
||||
* an object selects an existing certificate in an explicitly configured keychain.
|
||||
@@ -1176,7 +1193,7 @@ export type SourceSigning = 'ad-hoc' | {
|
||||
|
||||
## `@deepseek-ai/dsh-experimental-forge`
|
||||
|
||||
- `source`: [`packages/experimental/forge/src/types.ts:239`](../packages/experimental/forge/src/types.ts)
|
||||
- `source`: [`packages/experimental/forge/src/types.ts:432`](../packages/experimental/forge/src/types.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Poll cadence and fetch deadline for watched change requests. */
|
||||
@@ -1210,10 +1227,15 @@ export interface Config {
|
||||
hosts?: string[]
|
||||
/** Deadline in milliseconds for one `gh` invocation. */
|
||||
timeoutMs?: number
|
||||
/** Maximum captured bytes per `gh` output stream and combined JSON bytes of one complete check read. */
|
||||
/** Maximum captured bytes per `gh` output stream and combined JSON bytes of one complete check read or review read. */
|
||||
maxOutputBytes?: number
|
||||
/** Most review threads counted per pull request in one status read; a pull request with more reports a lower bound. */
|
||||
/**
|
||||
* Most review threads counted per pull request in one status read, which reports a lower bound past it, and read in
|
||||
* one review read, which reports itself truncated past it.
|
||||
*/
|
||||
maxReviewThreads?: number
|
||||
/** Most commits one pull request commit listing returns; GitHub lists at most 250 commits of a pull request. */
|
||||
maxCommits?: number
|
||||
}
|
||||
```
|
||||
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-experimental-forge-github -->
|
||||
@@ -1547,7 +1569,7 @@ export type PaidTranslationProvider = 'deepseek-account' | 'deepseek-official'
|
||||
|
||||
- `inject`: `workingDirectory` · `fs` · `subprocess` · `sandbox` · `sandboxPolicy` · `sessionProjections`
|
||||
- `refs`: `Volatile` (`@deepseek-ai/cordis`)
|
||||
- `source`: [`packages/experimental/worktree/src/index.ts:35`](../packages/experimental/worktree/src/index.ts)
|
||||
- `source`: [`packages/experimental/worktree/src/index.ts:36`](../packages/experimental/worktree/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Worktree location, generated names, executables, subprocess resource limits, and the archive cleanup switch. */
|
||||
@@ -1566,7 +1588,11 @@ export interface Config {
|
||||
timeoutMs?: number
|
||||
/** Subprocess termination grace in milliseconds. */
|
||||
graceMs?: number
|
||||
/** Maximum captured bytes per subprocess output stream. */
|
||||
/**
|
||||
* Maximum captured bytes per subprocess output stream; archive inspection
|
||||
* streams `git status`, and inspection and restoration stream the worktree
|
||||
* listing, so it bounds only their stderr.
|
||||
*/
|
||||
maxOutputBytes?: number
|
||||
}
|
||||
```
|
||||
@@ -2377,7 +2403,7 @@ export type Config = Readonly<Record<string, never>>
|
||||
## `@deepseek-ai/dsh-lsp-stdio`
|
||||
|
||||
- `inject`: `fs` · `lsp` · `subprocess`
|
||||
- `source`: [`packages/lsp/lsp-stdio/src/index.ts:82`](../packages/lsp/lsp-stdio/src/index.ts)
|
||||
- `source`: [`packages/lsp/lsp-stdio/src/index.ts:83`](../packages/lsp/lsp-stdio/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Plugin configuration: provider id → local language-server configuration. */
|
||||
@@ -2974,7 +3000,7 @@ export interface Config {
|
||||
|
||||
## `@deepseek-ai/dsh-sandbox-local`
|
||||
|
||||
- `source`: [`packages/sandbox/sandbox-local/src/index.ts:45`](../packages/sandbox/sandbox-local/src/index.ts)
|
||||
- `source`: [`packages/sandbox/sandbox-local/src/index.ts:63`](../packages/sandbox/sandbox-local/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Plugin config. All optional — `static Config` supplies the defaults. */
|
||||
@@ -3398,7 +3424,7 @@ export interface Config extends SessionTitleLlmConfig {
|
||||
|
||||
## `@deepseek-ai/dsh-shell-env`
|
||||
|
||||
- `source`: [`packages/shell/shell-env/src/index.ts:31`](../packages/shell/shell-env/src/index.ts)
|
||||
- `source`: [`packages/shell/shell-env/src/index.ts:34`](../packages/shell/shell-env/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Plugin config (all optional — the built-in facts resolve without defaults). */
|
||||
@@ -4119,7 +4145,7 @@ export interface Config {
|
||||
## `@deepseek-ai/dsh-tool-bash`
|
||||
|
||||
- `inject`: `tools` · `shell` · `systemPrompt` · `shellEnv` · `workingDirectory`
|
||||
- `source`: [`packages/shell/tool-bash/src/index.ts:37`](../packages/shell/tool-bash/src/index.ts)
|
||||
- `source`: [`packages/shell/tool-bash/src/index.ts:40`](../packages/shell/tool-bash/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Configuration for the bash tool. */
|
||||
@@ -4149,7 +4175,7 @@ export interface Config {
|
||||
## `@deepseek-ai/dsh-tool-bash-persistent`
|
||||
|
||||
- `inject`: `tools` · `terminals` · `workingDirectory`
|
||||
- `source`: [`packages/shell/tool-bash-persistent/src/index.ts:447`](../packages/shell/tool-bash-persistent/src/index.ts)
|
||||
- `source`: [`packages/shell/tool-bash-persistent/src/index.ts:448`](../packages/shell/tool-bash-persistent/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Configuration for the persistent Bash tool. */
|
||||
@@ -4384,7 +4410,7 @@ export interface Config {
|
||||
## `@deepseek-ai/dsh-tool-pwsh`
|
||||
|
||||
- `inject`: `tools` · `shell` · `systemPrompt` · `shellEnv` · `workingDirectory`
|
||||
- `source`: [`packages/shell/tool-pwsh/src/index.ts:54`](../packages/shell/tool-pwsh/src/index.ts)
|
||||
- `source`: [`packages/shell/tool-pwsh/src/index.ts:57`](../packages/shell/tool-pwsh/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Configuration for the pwsh tool. */
|
||||
@@ -4496,7 +4522,7 @@ export interface Config {
|
||||
## `@deepseek-ai/dsh-tool-str-replace-editor`
|
||||
|
||||
- `inject`: `tools` · `fs`
|
||||
- `source`: [`packages/fs/tool-str-replace-editor/src/index.ts:506`](../packages/fs/tool-str-replace-editor/src/index.ts)
|
||||
- `source`: [`packages/fs/tool-str-replace-editor/src/index.ts:511`](../packages/fs/tool-str-replace-editor/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Configuration for the string-replacement editor tool. */
|
||||
@@ -4978,7 +5004,7 @@ export interface Config {
|
||||
## `@deepseek-ai/dsh-working-directory`
|
||||
|
||||
- `inject`: `fs` · `sessionProjections` · `systemPrompt`
|
||||
- `source`: [`packages/session/working-directory/src/index.ts:67`](../packages/session/working-directory/src/index.ts)
|
||||
- `source`: [`packages/session/working-directory/src/index.ts:70`](../packages/session/working-directory/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Deployment default for Sessions without an original project directory. */
|
||||
|
||||
+42
-16
@@ -1083,7 +1083,7 @@ export interface CotTranslationPreferences {
|
||||
## `@deepseek-ai/dsh-experimental-client-ui-forge`
|
||||
|
||||
- `inject`: `forge` · `forgeSession` · `sessionProjections` · `workingDirectory` · `fs` · `subprocess` · `sandbox` · `sandboxPolicy` · `agents`
|
||||
- `source`: [`packages/experimental/client-ui-forge/src/types.ts:345`](../packages/experimental/client-ui-forge/src/types.ts)
|
||||
- `source`: [`packages/experimental/client-ui-forge/src/types.ts:381`](../packages/experimental/client-ui-forge/src/types.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Host settings of the forge panel Remote. */
|
||||
@@ -1111,6 +1111,25 @@ export interface Config {
|
||||
maxUntrackedFiles?: number
|
||||
/** Most UTF-8 bytes of change request diffs kept in memory for later reads at the same head commit and base branch. */
|
||||
maxCachedDiffBytes?: number
|
||||
/** Most commits of the `branch` scope listed; a longer branch lists its newest ones. */
|
||||
maxCommits?: number
|
||||
/**
|
||||
* Globs of files in the `test` layer, which the changes view can hide, beside the built-in rules and checked before
|
||||
* them; matched against repository-relative paths with dot files included. A repository's `.gitattributes` overrides both.
|
||||
*/
|
||||
testPaths?: string[]
|
||||
/**
|
||||
* Globs of files in the `interface` layer beside the built-in rules, matched like `testPaths`; checked after
|
||||
* `testPaths` and before the built-in rules.
|
||||
*/
|
||||
interfacePaths?: string[]
|
||||
/** Most UTF-8 bytes, as JSON, of change request review threads kept in memory for later reads at the same head commit and update time. */
|
||||
maxCachedReviewBytes?: number
|
||||
/**
|
||||
* Age in milliseconds at which review threads kept in memory are read from the code host again; a GitHub pull
|
||||
* request's update time does not change when a thread is resolved.
|
||||
*/
|
||||
reviewRefreshMs?: number
|
||||
}
|
||||
```
|
||||
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-experimental-client-ui-forge -->
|
||||
@@ -1153,8 +1172,6 @@ export interface Config {
|
||||
export interface Config extends CuaConfig {
|
||||
/** Interval in milliseconds between permission and position checks while the native guide is open. */
|
||||
permissionPollIntervalMs: number
|
||||
/** Allow mouseMove to move the visible system pointer when AX hover is unavailable. Off by default to avoid interrupting the user. */
|
||||
systemPointerHover: boolean
|
||||
/**
|
||||
* Source-build signing: `ad-hoc` (default) binds grants to the code hash;
|
||||
* an object selects an existing certificate in an explicitly configured keychain.
|
||||
@@ -1178,7 +1195,7 @@ export type SourceSigning = 'ad-hoc' | {
|
||||
|
||||
## `@deepseek-ai/dsh-experimental-forge`
|
||||
|
||||
- `source`: [`packages/experimental/forge/src/types.ts:239`](../packages/experimental/forge/src/types.ts)
|
||||
- `source`: [`packages/experimental/forge/src/types.ts:432`](../packages/experimental/forge/src/types.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Poll cadence and fetch deadline for watched change requests. */
|
||||
@@ -1212,10 +1229,15 @@ export interface Config {
|
||||
hosts?: string[]
|
||||
/** Deadline in milliseconds for one `gh` invocation. */
|
||||
timeoutMs?: number
|
||||
/** Maximum captured bytes per `gh` output stream and combined JSON bytes of one complete check read. */
|
||||
/** Maximum captured bytes per `gh` output stream and combined JSON bytes of one complete check read or review read. */
|
||||
maxOutputBytes?: number
|
||||
/** Most review threads counted per pull request in one status read; a pull request with more reports a lower bound. */
|
||||
/**
|
||||
* Most review threads counted per pull request in one status read, which reports a lower bound past it, and read in
|
||||
* one review read, which reports itself truncated past it.
|
||||
*/
|
||||
maxReviewThreads?: number
|
||||
/** Most commits one pull request commit listing returns; GitHub lists at most 250 commits of a pull request. */
|
||||
maxCommits?: number
|
||||
}
|
||||
```
|
||||
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-experimental-forge-github -->
|
||||
@@ -1549,7 +1571,7 @@ export type PaidTranslationProvider = 'deepseek-account' | 'deepseek-official'
|
||||
|
||||
- `inject`: `workingDirectory` · `fs` · `subprocess` · `sandbox` · `sandboxPolicy` · `sessionProjections`
|
||||
- `refs`: `Volatile` (`@deepseek-ai/cordis`)
|
||||
- `source`: [`packages/experimental/worktree/src/index.ts:35`](../packages/experimental/worktree/src/index.ts)
|
||||
- `source`: [`packages/experimental/worktree/src/index.ts:36`](../packages/experimental/worktree/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Worktree location, generated names, executables, subprocess resource limits, and the archive cleanup switch. */
|
||||
@@ -1568,7 +1590,11 @@ export interface Config {
|
||||
timeoutMs?: number
|
||||
/** Subprocess termination grace in milliseconds. */
|
||||
graceMs?: number
|
||||
/** Maximum captured bytes per subprocess output stream. */
|
||||
/**
|
||||
* Maximum captured bytes per subprocess output stream; archive inspection
|
||||
* streams `git status`, and inspection and restoration stream the worktree
|
||||
* listing, so it bounds only their stderr.
|
||||
*/
|
||||
maxOutputBytes?: number
|
||||
}
|
||||
```
|
||||
@@ -2379,7 +2405,7 @@ export type Config = Readonly<Record<string, never>>
|
||||
## `@deepseek-ai/dsh-lsp-stdio`
|
||||
|
||||
- `inject`: `fs` · `lsp` · `subprocess`
|
||||
- `source`: [`packages/lsp/lsp-stdio/src/index.ts:82`](../packages/lsp/lsp-stdio/src/index.ts)
|
||||
- `source`: [`packages/lsp/lsp-stdio/src/index.ts:83`](../packages/lsp/lsp-stdio/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Plugin configuration: provider id → local language-server configuration. */
|
||||
@@ -2976,7 +3002,7 @@ export interface Config {
|
||||
|
||||
## `@deepseek-ai/dsh-sandbox-local`
|
||||
|
||||
- `source`: [`packages/sandbox/sandbox-local/src/index.ts:45`](../packages/sandbox/sandbox-local/src/index.ts)
|
||||
- `source`: [`packages/sandbox/sandbox-local/src/index.ts:63`](../packages/sandbox/sandbox-local/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Plugin config. All optional — `static Config` supplies the defaults. */
|
||||
@@ -3400,7 +3426,7 @@ export interface Config extends SessionTitleLlmConfig {
|
||||
|
||||
## `@deepseek-ai/dsh-shell-env`
|
||||
|
||||
- `source`: [`packages/shell/shell-env/src/index.ts:31`](../packages/shell/shell-env/src/index.ts)
|
||||
- `source`: [`packages/shell/shell-env/src/index.ts:34`](../packages/shell/shell-env/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Plugin config (all optional — the built-in facts resolve without defaults). */
|
||||
@@ -4121,7 +4147,7 @@ export interface Config {
|
||||
## `@deepseek-ai/dsh-tool-bash`
|
||||
|
||||
- `inject`: `tools` · `shell` · `systemPrompt` · `shellEnv` · `workingDirectory`
|
||||
- `source`: [`packages/shell/tool-bash/src/index.ts:37`](../packages/shell/tool-bash/src/index.ts)
|
||||
- `source`: [`packages/shell/tool-bash/src/index.ts:40`](../packages/shell/tool-bash/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Configuration for the bash tool. */
|
||||
@@ -4151,7 +4177,7 @@ export interface Config {
|
||||
## `@deepseek-ai/dsh-tool-bash-persistent`
|
||||
|
||||
- `inject`: `tools` · `terminals` · `workingDirectory`
|
||||
- `source`: [`packages/shell/tool-bash-persistent/src/index.ts:447`](../packages/shell/tool-bash-persistent/src/index.ts)
|
||||
- `source`: [`packages/shell/tool-bash-persistent/src/index.ts:448`](../packages/shell/tool-bash-persistent/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Configuration for the persistent Bash tool. */
|
||||
@@ -4386,7 +4412,7 @@ export interface Config {
|
||||
## `@deepseek-ai/dsh-tool-pwsh`
|
||||
|
||||
- `inject`: `tools` · `shell` · `systemPrompt` · `shellEnv` · `workingDirectory`
|
||||
- `source`: [`packages/shell/tool-pwsh/src/index.ts:54`](../packages/shell/tool-pwsh/src/index.ts)
|
||||
- `source`: [`packages/shell/tool-pwsh/src/index.ts:57`](../packages/shell/tool-pwsh/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Configuration for the pwsh tool. */
|
||||
@@ -4498,7 +4524,7 @@ export interface Config {
|
||||
## `@deepseek-ai/dsh-tool-str-replace-editor`
|
||||
|
||||
- `inject`: `tools` · `fs`
|
||||
- `source`: [`packages/fs/tool-str-replace-editor/src/index.ts:506`](../packages/fs/tool-str-replace-editor/src/index.ts)
|
||||
- `source`: [`packages/fs/tool-str-replace-editor/src/index.ts:511`](../packages/fs/tool-str-replace-editor/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Configuration for the string-replacement editor tool. */
|
||||
@@ -4980,7 +5006,7 @@ export interface Config {
|
||||
## `@deepseek-ai/dsh-working-directory`
|
||||
|
||||
- `inject`: `fs` · `sessionProjections` · `systemPrompt`
|
||||
- `source`: [`packages/session/working-directory/src/index.ts:67`](../packages/session/working-directory/src/index.ts)
|
||||
- `source`: [`packages/session/working-directory/src/index.ts:70`](../packages/session/working-directory/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Deployment default for Sessions without an original project directory. */
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
en: 16d67911ecaa50d3
|
||||
zh: f5259b0fbf022db3
|
||||
/development-guide/contributor-reference/environment-variables:
|
||||
en: dba2c72ee6ffa930
|
||||
zh: b65c27fbb9129ec8
|
||||
en: 9a0b2f1a520cfc68
|
||||
zh: 62c9ec565909f605
|
||||
/development-guide/contributor-reference/git-integrations:
|
||||
en: 075658ecfc10da20
|
||||
zh: 69e664417bbde87e
|
||||
|
||||
+1
-1
@@ -133,7 +133,7 @@ DEEPSEEK_API_KEY=sk-...
|
||||
DEEPSEEK_BASE_URL=https://... # optional
|
||||
```
|
||||
|
||||
`DEEPSEEK_BASE_URL` is optional and defaults to the public API. Never commit real credentials. The real-API e2e suites self-skip when `DEEPSEEK_API_KEY` is not set.
|
||||
`DEEPSEEK_BASE_URL` is optional and defaults to the public API. The e2e and Web Vitest configurations, and the snapshot configuration when `DSH_SNAPSHOT=record`, load the root `.env` themselves; `pnpm dsh` and `pnpm run demo:ptc` launched from the repository root read it as a project `.env`, which may not set `DEEPSEEK_BASE_URL`, so export that variable or put it in `$DSH_HOME/.env` for those launches. Never commit real credentials. The real-API e2e suites self-skip when `DEEPSEEK_API_KEY` is not set.
|
||||
|
||||
### Git integrations
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ DEEPSEEK_API_KEY=sk-...
|
||||
DEEPSEEK_BASE_URL=https://... # optional
|
||||
```
|
||||
|
||||
`DEEPSEEK_BASE_URL` 可选,默认为公开 API。请勿提交真实凭证。未设置 `DEEPSEEK_API_KEY` 时,真实 API 的 e2e 套件会自动跳过。
|
||||
`DEEPSEEK_BASE_URL` 可选,默认为公开 API。e2e 与 Web 的 Vitest 配置,以及 `DSH_SNAPSHOT=record` 时的快照配置,会自行加载根目录 `.env`;而从仓库根目录启动的 `pnpm dsh` 和 `pnpm run demo:ptc` 会把它当作项目 `.env` 读取,项目 `.env` 不能设置 `DEEPSEEK_BASE_URL`,因此这些启动需要导出该变量,或把它写入 `$DSH_HOME/.env`。请勿提交真实凭证。未设置 `DEEPSEEK_API_KEY` 时,真实 API 的 e2e 套件会自动跳过。
|
||||
|
||||
### Git 集成
|
||||
|
||||
|
||||
@@ -12,17 +12,17 @@ Application listeners are outside this package matrix. Desktop Host subscribes t
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:198`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
|
||||
| `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:66`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | [`cua`](../packages/experimental/cua), `remotes` |
|
||||
| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:399`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
|
||||
| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | [`agent-team`](../packages/team/agent-team), `browser-use-runtime`, `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`cua`](../packages/experimental/cua), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), `liveshare`, [`loader-smoke`](../packages/test-support/loader-smoke), [`permission-presets`](../packages/interaction/permission-presets), `session-controller`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`action-cards`](../packages/experimental/action-cards), [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller`, [`subagent`](../packages/subagent/subagent), [`tool-office`](../packages/document/tool-office), [`tool-subagent-control`](../packages/subagent/tool-subagent-control) |
|
||||
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:429`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), `server`, `session-controller`, [`session-telemetry`](../packages/session/session-telemetry), [`subagent`](../packages/subagent/subagent) |
|
||||
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:328`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:336`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:317`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | `client-ui-forge`, [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`tool-subagent-control`](../packages/subagent/tool-subagent-control) |
|
||||
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:350`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`action-cards`](../packages/experimental/action-cards), [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, `client-ui-forge`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), `liveshare`, [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`subagent`](../packages/subagent/subagent), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) |
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:389`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:309`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` |
|
||||
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:417`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:400`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
|
||||
| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:291`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | [`agent-team`](../packages/team/agent-team), `browser-use-runtime`, `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`cua`](../packages/experimental/cua), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), `liveshare`, [`loader-smoke`](../packages/test-support/loader-smoke), [`permission-presets`](../packages/interaction/permission-presets), `session-controller`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:300`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`action-cards`](../packages/experimental/action-cards), [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller`, [`subagent`](../packages/subagent/subagent), [`tool-office`](../packages/document/tool-office), [`tool-subagent-control`](../packages/subagent/tool-subagent-control) |
|
||||
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:430`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), `server`, `session-controller`, [`session-telemetry`](../packages/session/session-telemetry), [`subagent`](../packages/subagent/subagent) |
|
||||
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:329`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:337`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:318`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | `client-ui-forge`, [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`tool-subagent-control`](../packages/subagent/tool-subagent-control) |
|
||||
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:351`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`action-cards`](../packages/experimental/action-cards), [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, `client-ui-forge`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), `liveshare`, [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`subagent`](../packages/subagent/subagent), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) |
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:390`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:310`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` |
|
||||
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:418`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:705`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:685`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:712`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
@@ -47,11 +47,11 @@ Application listeners are outside this package matrix. Desktop Host subscribes t
|
||||
| `deepseek-account/signed-out` | `emit` | [`packages/credentials/deepseek-account/src/index.ts:29`](../packages/credentials/deepseek-account/src/index.ts) | [`deepseek-account`](../packages/credentials/deepseek-account) (`emit`) | [`deepseek-account`](../packages/credentials/deepseek-account) |
|
||||
| `domain/changed` | `emit` | [`packages/storage/storage-domain/src/events.ts:46`](../packages/storage/storage-domain/src/events.ts) | [`storage-domain`](../packages/storage/storage-domain) (`emit`) | `workspace-controller` |
|
||||
| `feedback/committed` | `parallel` | [`packages/feedback/message-feedback/src/index.ts:58`](../packages/feedback/message-feedback/src/index.ts) | [`message-feedback`](../packages/feedback/message-feedback) (`parallel`) | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) |
|
||||
| `forge/providers-changed` | `parallel` | [`packages/experimental/forge/src/index.ts:27`](../packages/experimental/forge/src/index.ts) | `forge` (`parallel`) | `forge-session` |
|
||||
| `fs/delete-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:82`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:74`](../packages/fs/fs/src/index.ts) | `liveshare` (`waterfall`), [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:92`](../packages/fs/fs/src/index.ts) | `liveshare` (`emit`), [`tool-fs`](../packages/fs/tool-fs) (`emit`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`emit`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`skill-filesystem`](../packages/skill/skill-filesystem), `workspace-files` |
|
||||
| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:66`](../packages/fs/fs/src/index.ts) | `liveshare` (`waterfall`), [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `forge/providers-changed` | `parallel` | [`packages/experimental/forge/src/index.ts:28`](../packages/experimental/forge/src/index.ts) | `forge` (`parallel`) | `forge-session` |
|
||||
| `fs/delete-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:83`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:75`](../packages/fs/fs/src/index.ts) | `liveshare` (`waterfall`), [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:93`](../packages/fs/fs/src/index.ts) | `liveshare` (`emit`), [`tool-fs`](../packages/fs/tool-fs) (`emit`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`emit`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`skill-filesystem`](../packages/skill/skill-filesystem), `workspace-files` |
|
||||
| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:67`](../packages/fs/fs/src/index.ts) | `liveshare` (`waterfall`), [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `goal/activation-changed` | `emit` | [`packages/goal/goal/src/types.ts:147`](../packages/goal/goal/src/types.ts) | [`goal`](../packages/goal/goal) (`emit`) | `remotes`, `server` |
|
||||
| `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
|
||||
| `hmr/change` | `emit` | [`packages/boot/hmr/src/index.ts:35`](../packages/boot/hmr/src/index.ts) | [`hmr`](../packages/boot/hmr) (`emit`) | - |
|
||||
@@ -73,8 +73,8 @@ Application listeners are outside this package matrix. Desktop Host subscribes t
|
||||
| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:44`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
|
||||
| `session/created` | `emit` | [`packages/core/session/src/index.ts:56`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `server`, `session-controller`, [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:65`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `file-upload`, `forge-session`, [`office-kit`](../packages/document/office-kit), `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), `ui-trajectory`, [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `claude-code-mods`, `client-ui-forge`, [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, `forge-session`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`loader-smoke`](../packages/test-support/loader-smoke), `product-analytics`, [`ptc-runtime-node`](../packages/ptc-runtime/ptc-runtime-node), `server`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter), [`tool-executor-ptc`](../packages/core/tool-executor-ptc), [`tool-jobs`](../packages/jobs/tool-jobs), [`tool-office`](../packages/document/tool-office), `ui-trajectory`, [`user-questions`](../packages/interaction/user-questions), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:84`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
| `session/event` | `emit` | [`packages/core/session/src/index.ts:79`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `claude-code-mods`, `client-ui-forge`, [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, `forge-session`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`loader-smoke`](../packages/test-support/loader-smoke), `product-analytics`, [`ptc-runtime-node`](../packages/ptc-runtime/ptc-runtime-node), `server`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter), [`tool-executor-ptc`](../packages/core/tool-executor-ptc), [`tool-jobs`](../packages/jobs/tool-jobs), [`tool-office`](../packages/document/tool-office), `ui-trajectory`, [`user-questions`](../packages/interaction/user-questions), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:87`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:75`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`emit`) | `remotes` |
|
||||
| `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:296`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - |
|
||||
| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:166`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), `server` |
|
||||
@@ -84,7 +84,7 @@ Application listeners are outside this package matrix. Desktop Host subscribes t
|
||||
| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | `browser-use-runtime`, [`working-directory`](../packages/session/working-directory) |
|
||||
| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
|
||||
| `tools/change` | `emit` | [`packages/core/tools/src/events.ts:56`](../packages/core/tools/src/events.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime` |
|
||||
| `tools/execute` | `waterfall` | [`packages/core/tools/src/events.ts:29`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `browser-use-runtime`, `browser-use-stagehand-native`, `claude-code-mods`, `computer-use-cua-driver-native`, [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` |
|
||||
| `tools/execute` | `waterfall` | [`packages/core/tools/src/events.ts:29`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `browser-use-runtime`, `browser-use-stagehand-native`, `claude-code-mods`, `computer-use-cua-driver-native`, `cua-macos`, [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` |
|
||||
| `tools/post-execute` | `waterfall` | [`packages/core/tools/src/events.ts:41`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `claude-code-mods`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) |
|
||||
| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/events.ts:18`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), `liveshare`, [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tool-executor-ptc/src/runtime-types.ts:46`](../packages/core/tool-executor-ptc/src/runtime-types.ts) | [`tool-executor-ptc`](../packages/core/tool-executor-ptc) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
|
||||
@@ -98,7 +98,7 @@ Application listeners are outside this package matrix. Desktop Host subscribes t
|
||||
| `workflow/log` | `emit` | [`packages/workflow/workflow/src/index.ts:58`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow) |
|
||||
| `workflow/phase` | `emit` | [`packages/workflow/workflow/src/index.ts:51`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow) |
|
||||
| `workflow/start` | `emit` | [`packages/workflow/workflow/src/index.ts:43`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | - |
|
||||
| `working-directory/access` | `waterfall` | [`packages/session/working-directory/src/index.ts:29`](../packages/session/working-directory/src/index.ts) | [`working-directory`](../packages/session/working-directory) (`waterfall`) | `worktree` |
|
||||
| `working-directory/access` | `waterfall` | [`packages/session/working-directory/src/index.ts:32`](../packages/session/working-directory/src/index.ts) | [`working-directory`](../packages/session/working-directory) (`waterfall`) | `worktree` |
|
||||
| `workspace/session-activity` | `waterfall` | [`packages/workspace/workspace/src/index.ts:129`](../packages/workspace/workspace/src/index.ts) | [`workspace`](../packages/workspace/workspace) (`waterfall`) | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`schedule`](../packages/schedule/schedule), [`subagent`](../packages/subagent/subagent) |
|
||||
| `workspace/session-archived` | `parallel` | [`packages/workspace/workspace/src/index.ts:156`](../packages/workspace/workspace/src/index.ts) | [`workspace`](../packages/workspace/workspace) (`parallel`) | `worktree` |
|
||||
| `workspace/session-stop` | `parallel` | [`packages/workspace/workspace/src/index.ts:147`](../packages/workspace/workspace/src/index.ts) | [`workspace`](../packages/workspace/workspace) (`parallel`) | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`schedule`](../packages/schedule/schedule), [`subagent`](../packages/subagent/subagent) |
|
||||
|
||||
@@ -14,17 +14,17 @@
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:198`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
|
||||
| `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:66`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | [`cua`](../packages/experimental/cua), `remotes` |
|
||||
| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:399`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
|
||||
| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | [`agent-team`](../packages/team/agent-team), `browser-use-runtime`, `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`cua`](../packages/experimental/cua), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), `liveshare`, [`loader-smoke`](../packages/test-support/loader-smoke), [`permission-presets`](../packages/interaction/permission-presets), `session-controller`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`action-cards`](../packages/experimental/action-cards), [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller`, [`subagent`](../packages/subagent/subagent), [`tool-office`](../packages/document/tool-office), [`tool-subagent-control`](../packages/subagent/tool-subagent-control) |
|
||||
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:429`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), `server`, `session-controller`, [`session-telemetry`](../packages/session/session-telemetry), [`subagent`](../packages/subagent/subagent) |
|
||||
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:328`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:336`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:317`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | `client-ui-forge`, [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`tool-subagent-control`](../packages/subagent/tool-subagent-control) |
|
||||
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:350`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`action-cards`](../packages/experimental/action-cards), [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, `client-ui-forge`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), `liveshare`, [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`subagent`](../packages/subagent/subagent), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) |
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:389`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:309`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` |
|
||||
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:417`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:400`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
|
||||
| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:291`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | [`agent-team`](../packages/team/agent-team), `browser-use-runtime`, `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`cua`](../packages/experimental/cua), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), `liveshare`, [`loader-smoke`](../packages/test-support/loader-smoke), [`permission-presets`](../packages/interaction/permission-presets), `session-controller`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:300`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`action-cards`](../packages/experimental/action-cards), [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller`, [`subagent`](../packages/subagent/subagent), [`tool-office`](../packages/document/tool-office), [`tool-subagent-control`](../packages/subagent/tool-subagent-control) |
|
||||
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:430`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), `server`, `session-controller`, [`session-telemetry`](../packages/session/session-telemetry), [`subagent`](../packages/subagent/subagent) |
|
||||
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:329`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:337`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:318`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | `client-ui-forge`, [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`tool-subagent-control`](../packages/subagent/tool-subagent-control) |
|
||||
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:351`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`action-cards`](../packages/experimental/action-cards), [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, `client-ui-forge`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), `liveshare`, [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`subagent`](../packages/subagent/subagent), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) |
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:390`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:310`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` |
|
||||
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:418`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:705`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:685`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:712`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
@@ -49,11 +49,11 @@
|
||||
| `deepseek-account/signed-out` | `emit` | [`packages/credentials/deepseek-account/src/index.ts:29`](../packages/credentials/deepseek-account/src/index.ts) | [`deepseek-account`](../packages/credentials/deepseek-account) (`emit`) | [`deepseek-account`](../packages/credentials/deepseek-account) |
|
||||
| `domain/changed` | `emit` | [`packages/storage/storage-domain/src/events.ts:46`](../packages/storage/storage-domain/src/events.ts) | [`storage-domain`](../packages/storage/storage-domain) (`emit`) | `workspace-controller` |
|
||||
| `feedback/committed` | `parallel` | [`packages/feedback/message-feedback/src/index.ts:58`](../packages/feedback/message-feedback/src/index.ts) | [`message-feedback`](../packages/feedback/message-feedback) (`parallel`) | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) |
|
||||
| `forge/providers-changed` | `parallel` | [`packages/experimental/forge/src/index.ts:27`](../packages/experimental/forge/src/index.ts) | `forge` (`parallel`) | `forge-session` |
|
||||
| `fs/delete-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:82`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:74`](../packages/fs/fs/src/index.ts) | `liveshare` (`waterfall`), [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:92`](../packages/fs/fs/src/index.ts) | `liveshare` (`emit`), [`tool-fs`](../packages/fs/tool-fs) (`emit`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`emit`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`skill-filesystem`](../packages/skill/skill-filesystem), `workspace-files` |
|
||||
| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:66`](../packages/fs/fs/src/index.ts) | `liveshare` (`waterfall`), [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `forge/providers-changed` | `parallel` | [`packages/experimental/forge/src/index.ts:28`](../packages/experimental/forge/src/index.ts) | `forge` (`parallel`) | `forge-session` |
|
||||
| `fs/delete-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:83`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:75`](../packages/fs/fs/src/index.ts) | `liveshare` (`waterfall`), [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:93`](../packages/fs/fs/src/index.ts) | `liveshare` (`emit`), [`tool-fs`](../packages/fs/tool-fs) (`emit`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`emit`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`skill-filesystem`](../packages/skill/skill-filesystem), `workspace-files` |
|
||||
| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:67`](../packages/fs/fs/src/index.ts) | `liveshare` (`waterfall`), [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `goal/activation-changed` | `emit` | [`packages/goal/goal/src/types.ts:147`](../packages/goal/goal/src/types.ts) | [`goal`](../packages/goal/goal) (`emit`) | `remotes`, `server` |
|
||||
| `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
|
||||
| `hmr/change` | `emit` | [`packages/boot/hmr/src/index.ts:35`](../packages/boot/hmr/src/index.ts) | [`hmr`](../packages/boot/hmr) (`emit`) | - |
|
||||
@@ -75,8 +75,8 @@
|
||||
| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:44`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
|
||||
| `session/created` | `emit` | [`packages/core/session/src/index.ts:56`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `server`, `session-controller`, [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:65`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `file-upload`, `forge-session`, [`office-kit`](../packages/document/office-kit), `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), `ui-trajectory`, [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `claude-code-mods`, `client-ui-forge`, [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, `forge-session`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`loader-smoke`](../packages/test-support/loader-smoke), `product-analytics`, [`ptc-runtime-node`](../packages/ptc-runtime/ptc-runtime-node), `server`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter), [`tool-executor-ptc`](../packages/core/tool-executor-ptc), [`tool-jobs`](../packages/jobs/tool-jobs), [`tool-office`](../packages/document/tool-office), `ui-trajectory`, [`user-questions`](../packages/interaction/user-questions), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:84`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
| `session/event` | `emit` | [`packages/core/session/src/index.ts:79`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `claude-code-mods`, `client-ui-forge`, [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, `forge-session`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`loader-smoke`](../packages/test-support/loader-smoke), `product-analytics`, [`ptc-runtime-node`](../packages/ptc-runtime/ptc-runtime-node), `server`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter), [`tool-executor-ptc`](../packages/core/tool-executor-ptc), [`tool-jobs`](../packages/jobs/tool-jobs), [`tool-office`](../packages/document/tool-office), `ui-trajectory`, [`user-questions`](../packages/interaction/user-questions), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:87`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:75`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`emit`) | `remotes` |
|
||||
| `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:296`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - |
|
||||
| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:166`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), `server` |
|
||||
@@ -86,7 +86,7 @@
|
||||
| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | `browser-use-runtime`, [`working-directory`](../packages/session/working-directory) |
|
||||
| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
|
||||
| `tools/change` | `emit` | [`packages/core/tools/src/events.ts:56`](../packages/core/tools/src/events.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime` |
|
||||
| `tools/execute` | `waterfall` | [`packages/core/tools/src/events.ts:29`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `browser-use-runtime`, `browser-use-stagehand-native`, `claude-code-mods`, `computer-use-cua-driver-native`, [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` |
|
||||
| `tools/execute` | `waterfall` | [`packages/core/tools/src/events.ts:29`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `browser-use-runtime`, `browser-use-stagehand-native`, `claude-code-mods`, `computer-use-cua-driver-native`, `cua-macos`, [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` |
|
||||
| `tools/post-execute` | `waterfall` | [`packages/core/tools/src/events.ts:41`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `claude-code-mods`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) |
|
||||
| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/events.ts:18`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), `liveshare`, [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tool-executor-ptc/src/runtime-types.ts:46`](../packages/core/tool-executor-ptc/src/runtime-types.ts) | [`tool-executor-ptc`](../packages/core/tool-executor-ptc) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
|
||||
@@ -100,7 +100,7 @@
|
||||
| `workflow/log` | `emit` | [`packages/workflow/workflow/src/index.ts:58`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow) |
|
||||
| `workflow/phase` | `emit` | [`packages/workflow/workflow/src/index.ts:51`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow) |
|
||||
| `workflow/start` | `emit` | [`packages/workflow/workflow/src/index.ts:43`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | - |
|
||||
| `working-directory/access` | `waterfall` | [`packages/session/working-directory/src/index.ts:29`](../packages/session/working-directory/src/index.ts) | [`working-directory`](../packages/session/working-directory) (`waterfall`) | `worktree` |
|
||||
| `working-directory/access` | `waterfall` | [`packages/session/working-directory/src/index.ts:32`](../packages/session/working-directory/src/index.ts) | [`working-directory`](../packages/session/working-directory) (`waterfall`) | `worktree` |
|
||||
| `workspace/session-activity` | `waterfall` | [`packages/workspace/workspace/src/index.ts:129`](../packages/workspace/workspace/src/index.ts) | [`workspace`](../packages/workspace/workspace) (`waterfall`) | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`schedule`](../packages/schedule/schedule), [`subagent`](../packages/subagent/subagent) |
|
||||
| `workspace/session-archived` | `parallel` | [`packages/workspace/workspace/src/index.ts:156`](../packages/workspace/workspace/src/index.ts) | [`workspace`](../packages/workspace/workspace) (`parallel`) | `worktree` |
|
||||
| `workspace/session-stop` | `parallel` | [`packages/workspace/workspace/src/index.ts:147`](../packages/workspace/workspace/src/index.ts) | [`workspace`](../packages/workspace/workspace) (`parallel`) | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`schedule`](../packages/schedule/schedule), [`subagent`](../packages/subagent/subagent) |
|
||||
|
||||
+20
-10
@@ -490,6 +490,7 @@ flowchart TD
|
||||
pkg_session --> pkg_scope
|
||||
pkg_api_remotes --> pkg_scope
|
||||
pkg_attachment --> pkg_brand
|
||||
pkg_attachment --> pkg_error
|
||||
pkg_app_boot --> pkg_home_paths
|
||||
pkg_app_boot --> pkg_launch_environment
|
||||
pkg_browser_use --> pkg_brand
|
||||
@@ -505,7 +506,9 @@ flowchart TD
|
||||
pkg_storage_domain --> pkg_storage
|
||||
pkg_storage_json --> pkg_storage
|
||||
pkg_storage_sqlite --> pkg_storage
|
||||
pkg_subprocess --> pkg_error
|
||||
pkg_subprocess --> pkg_http_proxy
|
||||
pkg_win32_process --> pkg_error
|
||||
pkg_remote_mock --> pkg_typert_protocol
|
||||
pkg_typert_loader --> pkg_typert_registry
|
||||
pkg_util_ripgrep --> pkg_subprocess
|
||||
@@ -540,6 +543,7 @@ flowchart TD
|
||||
pkg_host_frontend_static --> pkg_host_webserver
|
||||
pkg_lsp --> pkg_brand
|
||||
pkg_lsp --> pkg_llm
|
||||
pkg_sandbox --> pkg_error
|
||||
pkg_sandbox --> pkg_llm
|
||||
pkg_sandbox --> pkg_session
|
||||
pkg_session_format_catalog --> pkg_session
|
||||
@@ -650,6 +654,7 @@ flowchart TD
|
||||
pkg_hook_protocol --> pkg_session
|
||||
pkg_hook_protocol --> pkg_shell
|
||||
pkg_lsp_stdio --> pkg_brand
|
||||
pkg_lsp_stdio --> pkg_error
|
||||
pkg_lsp_stdio --> pkg_fs
|
||||
pkg_lsp_stdio --> pkg_llm
|
||||
pkg_lsp_stdio --> pkg_lsp
|
||||
@@ -740,6 +745,7 @@ flowchart TD
|
||||
pkg_session_title --> pkg_session
|
||||
pkg_session_title --> pkg_session_projection
|
||||
pkg_working_directory --> pkg_agent
|
||||
pkg_working_directory --> pkg_error
|
||||
pkg_working_directory --> pkg_fs
|
||||
pkg_working_directory --> pkg_llm
|
||||
pkg_working_directory --> pkg_session
|
||||
@@ -859,6 +865,7 @@ flowchart TD
|
||||
pkg_tool_goal --> pkg_system_prompt
|
||||
pkg_tool_goal --> pkg_tools
|
||||
pkg_tool_fs --> pkg_attachment
|
||||
pkg_tool_fs --> pkg_error
|
||||
pkg_tool_fs --> pkg_fs
|
||||
pkg_tool_fs --> pkg_llm
|
||||
pkg_tool_fs --> pkg_sandbox
|
||||
@@ -1057,6 +1064,7 @@ flowchart TD
|
||||
pkg_tool_working_directory --> pkg_tools
|
||||
pkg_tool_working_directory --> pkg_working_directory
|
||||
pkg_shell_env --> pkg_app_boot
|
||||
pkg_shell_env --> pkg_error
|
||||
pkg_shell_env --> pkg_home_paths
|
||||
pkg_shell_env --> pkg_shell
|
||||
pkg_shell_env --> pkg_tools
|
||||
@@ -1174,6 +1182,7 @@ flowchart TD
|
||||
pkg_tool_bash --> pkg_sandbox_policy
|
||||
pkg_tool_bash --> pkg_shell
|
||||
pkg_tool_bash --> pkg_shell_env
|
||||
pkg_tool_bash --> pkg_subprocess
|
||||
pkg_tool_bash --> pkg_system_prompt
|
||||
pkg_tool_bash --> pkg_tools
|
||||
pkg_tool_bash --> pkg_user_approval
|
||||
@@ -1185,6 +1194,7 @@ flowchart TD
|
||||
pkg_tool_pwsh --> pkg_sandbox_policy
|
||||
pkg_tool_pwsh --> pkg_shell
|
||||
pkg_tool_pwsh --> pkg_shell_env
|
||||
pkg_tool_pwsh --> pkg_subprocess
|
||||
pkg_tool_pwsh --> pkg_system_prompt
|
||||
pkg_tool_pwsh --> pkg_tools
|
||||
pkg_tool_pwsh --> pkg_user_approval
|
||||
@@ -1682,7 +1692,6 @@ flowchart TD
|
||||
| [`session-format-v3-to-v4`](../packages/session/session-format-v3-to-v4) | `session` | — |
|
||||
| [`ssh-helper-runtime`](../packages/ssh/ssh-helper-runtime) | `ssh` | — |
|
||||
| [`storage`](../packages/storage/storage) | `storage` | — |
|
||||
| [`win32-process`](../packages/subprocess/win32-process) | `subprocess` | — |
|
||||
| [`llm-mock-server`](../packages/test-support/llm-mock-server) | `test-support` | — |
|
||||
| [`typert-generator`](../packages/typert/generator) | `typert` | — |
|
||||
| [`typert-protocol`](../packages/typert/protocol) | `typert` | — |
|
||||
@@ -1690,7 +1699,7 @@ flowchart TD
|
||||
| [`llm`](../packages/llm/llm) | `llm` | [`error`](../packages/util/error) |
|
||||
| [`session`](../packages/core/session) | `core` | [`scope`](../packages/core/scope) |
|
||||
| [`api-remotes`](../packages/api/remotes) | `api` | [`scope`](../packages/core/scope) |
|
||||
| [`attachment`](../packages/attachment/attachment) | `attachment` | [`brand`](../packages/util/brand) |
|
||||
| [`attachment`](../packages/attachment/attachment) | `attachment` | [`brand`](../packages/util/brand), [`error`](../packages/util/error) |
|
||||
| [`app-boot`](../packages/boot/app-boot) | `boot` | [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment) |
|
||||
| [`browser-use`](../packages/browser-use/browser-use) | `browser-use` | [`brand`](../packages/util/brand) |
|
||||
| [`client-connection`](../packages/client/connection) | `client` | [`scope`](../packages/core/scope) |
|
||||
@@ -1701,7 +1710,8 @@ flowchart TD
|
||||
| [`storage-domain`](../packages/storage/storage-domain) | `storage` | [`storage`](../packages/storage/storage) |
|
||||
| [`storage-json`](../packages/storage/storage-json) | `storage` | [`storage`](../packages/storage/storage) |
|
||||
| [`storage-sqlite`](../packages/storage/storage-sqlite) | `storage` | [`storage`](../packages/storage/storage) |
|
||||
| [`subprocess`](../packages/subprocess/subprocess) | `subprocess` | [`http-proxy`](../packages/util/http-proxy) |
|
||||
| [`subprocess`](../packages/subprocess/subprocess) | `subprocess` | [`error`](../packages/util/error), [`http-proxy`](../packages/util/http-proxy) |
|
||||
| [`win32-process`](../packages/subprocess/win32-process) | `subprocess` | [`error`](../packages/util/error) |
|
||||
| [`remote-mock`](../packages/test-support/remote-mock) | `test-support` | [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`typert-loader`](../packages/typert/loader) | `typert` | [`typert-registry`](../packages/typert/registry) |
|
||||
| [`util-ripgrep`](../packages/util/ripgrep) | `util` | [`subprocess`](../packages/subprocess/subprocess) |
|
||||
@@ -1719,7 +1729,7 @@ flowchart TD
|
||||
| [`host-directory-picker-auto`](../packages/host/directory-picker-auto) | `host` | [`client-connection`](../packages/client/connection), [`client-ui-directory-picker-browse`](../packages/client/ui-directory-picker-browse), [`client-ui-directory-picker-native`](../packages/client/ui-directory-picker-native), [`host-directory-picker-browse`](../packages/host/directory-picker-browse), [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-webserver`](../packages/host/webserver) |
|
||||
| [`host-frontend-static`](../packages/host/frontend-static) | `host` | [`client-connection`](../packages/client/connection), [`host-webserver`](../packages/host/webserver) |
|
||||
| [`lsp`](../packages/lsp/lsp) | `lsp` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm) |
|
||||
| [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
|
||||
| [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`error`](../packages/util/error), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
|
||||
| [`session-format-catalog`](../packages/session/session-format-catalog) | `session` | [`session`](../packages/core/session) |
|
||||
| [`session-log-deepseek`](../packages/session/session-log-deepseek) | `session` | [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`session`](../packages/core/session) |
|
||||
| [`session-persistence`](../packages/session/session-persistence) | `session` | [`brand`](../packages/util/brand), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
|
||||
@@ -1761,7 +1771,7 @@ flowchart TD
|
||||
| [`experimental-client-ui-cot-translation`](../packages/experimental/client-ui-cot-translation) | `experimental` | [`experimental-translator`](../packages/experimental/translator) |
|
||||
| [`experimental-speech-to-text-sensevoice`](../packages/experimental/speech-to-text-sensevoice) | `experimental` | [`experimental-speech-to-text`](../packages/experimental/speech-to-text), [`subprocess`](../packages/subprocess/subprocess) |
|
||||
| [`hook-protocol`](../packages/experimental/hook-protocol) | `experimental` | [`session`](../packages/core/session), [`shell`](../packages/shell/shell) |
|
||||
| [`lsp-stdio`](../packages/lsp/lsp-stdio) | `lsp` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||
| [`lsp-stdio`](../packages/lsp/lsp-stdio) | `lsp` | [`brand`](../packages/util/brand), [`error`](../packages/util/error), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||
| [`bash-local`](../packages/shell/bash-local) | `shell` | [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||
| [`pwsh-local`](../packages/shell/pwsh-local) | `shell` | [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||
| [`llm-deepseek-api-key`](../packages/llm/llm-deepseek-api-key) | `llm` | [`credentials`](../packages/credentials/credentials), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek) |
|
||||
@@ -1780,7 +1790,7 @@ flowchart TD
|
||||
| [`sandbox-policy`](../packages/sandbox/sandbox-policy) | `sandbox` | [`agent`](../packages/core/agent), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt) |
|
||||
| [`session-telemetry`](../packages/session/session-telemetry) | `session` | [`agent`](../packages/core/agent), [`session`](../packages/core/session) |
|
||||
| [`session-title`](../packages/session/session-title) | `session` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
|
||||
| [`working-directory`](../packages/session/working-directory) | `session` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt) |
|
||||
| [`working-directory`](../packages/session/working-directory) | `session` | [`agent`](../packages/core/agent), [`error`](../packages/util/error), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt) |
|
||||
| [`terminal`](../packages/terminal/terminal) | `terminal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`error`](../packages/util/error) |
|
||||
| [`loader-smoke`](../packages/test-support/loader-smoke) | `test-support` | [`agent`](../packages/core/agent), [`http-proxy`](../packages/util/http-proxy), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
|
||||
| [`workflow`](../packages/workflow/workflow) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
|
||||
@@ -1804,7 +1814,7 @@ flowchart TD
|
||||
| [`tool-executor-native`](../packages/core/tool-executor-native) | `core` | [`tools`](../packages/core/tools) |
|
||||
| [`tool-executor-ptc`](../packages/core/tool-executor-ptc) | `core` | [`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), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`tool-goal`](../packages/goal/tool-goal) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
|
||||
| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`attachment`](../packages/attachment/attachment), [`error`](../packages/util/error), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) | `fs` | [`fs`](../packages/fs/fs), [`output-retention`](../packages/util/output-retention), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`tools`](../packages/core/tools) |
|
||||
| [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
|
||||
@@ -1843,7 +1853,7 @@ flowchart TD
|
||||
| [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy) | `session` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`tools`](../packages/core/tools) |
|
||||
| [`session-title-first-prompt-llm`](../packages/session/session-title-first-prompt-llm) | `session` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) |
|
||||
| [`tool-working-directory`](../packages/session/tool-working-directory) | `session` | [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`shell-env`](../packages/shell/shell-env) | `shell` | [`app-boot`](../packages/boot/app-boot), [`home-paths`](../packages/util/home-paths), [`shell`](../packages/shell/shell), [`tools`](../packages/core/tools) |
|
||||
| [`shell-env`](../packages/shell/shell-env) | `shell` | [`app-boot`](../packages/boot/app-boot), [`error`](../packages/util/error), [`home-paths`](../packages/util/home-paths), [`shell`](../packages/shell/shell), [`tools`](../packages/core/tools) |
|
||||
| [`tool-bash-persistent`](../packages/shell/tool-bash-persistent) | `shell` | [`agent`](../packages/core/agent), [`output-retention`](../packages/util/output-retention), [`terminal`](../packages/terminal/terminal), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`tool-pwsh-persistent`](../packages/shell/tool-pwsh-persistent) | `shell` | [`agent`](../packages/core/agent), [`output-retention`](../packages/util/output-retention), [`terminal`](../packages/terminal/terminal), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`ssh`](../packages/ssh/ssh) | `ssh` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`fs-sandbox`](../packages/fs/fs-sandbox), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-local`](../packages/sandbox/sandbox-local), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session-projection`](../packages/session/session-projection), [`subprocess`](../packages/subprocess/subprocess), [`subprocess-local`](../packages/subprocess/subprocess-local), [`util-values`](../packages/util/values) |
|
||||
@@ -1863,8 +1873,8 @@ flowchart TD
|
||||
| [`mcp-client`](../packages/mcp/mcp-client) | `mcp` | [`attachment`](../packages/attachment/attachment), [`credentials`](../packages/credentials/credentials), [`llm`](../packages/llm/llm), [`mcp-resources`](../packages/mcp/mcp-resources), [`scope`](../packages/core/scope), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
|
||||
| [`agent-preset`](../packages/preset/agent-preset) | `preset` | [`agent-preset-registry`](../packages/preset/agent-preset-registry) |
|
||||
| [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | `session` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`command-feedback`](../packages/feedback/command-feedback), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`otel`](../packages/telemetry/otel), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
| [`tool-bash`](../packages/shell/tool-bash) | `shell` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`tool-pwsh`](../packages/shell/tool-pwsh) | `shell` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`tool-bash`](../packages/shell/tool-bash) | `shell` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`tool-pwsh`](../packages/shell/tool-pwsh) | `shell` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`fs-ssh`](../packages/ssh/fs-ssh) | `ssh` | [`fs`](../packages/fs/fs), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`ssh`](../packages/ssh/ssh) |
|
||||
| [`sandbox-ssh`](../packages/ssh/sandbox-ssh) | `ssh` | [`sandbox`](../packages/sandbox/sandbox), [`ssh`](../packages/ssh/ssh) |
|
||||
| [`subprocess-ssh`](../packages/ssh/subprocess-ssh) | `ssh` | [`ssh`](../packages/ssh/ssh), [`subprocess`](../packages/subprocess/subprocess), [`subprocess-local`](../packages/subprocess/subprocess-local) |
|
||||
|
||||
+20
-10
@@ -492,6 +492,7 @@ flowchart TD
|
||||
pkg_session --> pkg_scope
|
||||
pkg_api_remotes --> pkg_scope
|
||||
pkg_attachment --> pkg_brand
|
||||
pkg_attachment --> pkg_error
|
||||
pkg_app_boot --> pkg_home_paths
|
||||
pkg_app_boot --> pkg_launch_environment
|
||||
pkg_browser_use --> pkg_brand
|
||||
@@ -507,7 +508,9 @@ flowchart TD
|
||||
pkg_storage_domain --> pkg_storage
|
||||
pkg_storage_json --> pkg_storage
|
||||
pkg_storage_sqlite --> pkg_storage
|
||||
pkg_subprocess --> pkg_error
|
||||
pkg_subprocess --> pkg_http_proxy
|
||||
pkg_win32_process --> pkg_error
|
||||
pkg_remote_mock --> pkg_typert_protocol
|
||||
pkg_typert_loader --> pkg_typert_registry
|
||||
pkg_util_ripgrep --> pkg_subprocess
|
||||
@@ -542,6 +545,7 @@ flowchart TD
|
||||
pkg_host_frontend_static --> pkg_host_webserver
|
||||
pkg_lsp --> pkg_brand
|
||||
pkg_lsp --> pkg_llm
|
||||
pkg_sandbox --> pkg_error
|
||||
pkg_sandbox --> pkg_llm
|
||||
pkg_sandbox --> pkg_session
|
||||
pkg_session_format_catalog --> pkg_session
|
||||
@@ -652,6 +656,7 @@ flowchart TD
|
||||
pkg_hook_protocol --> pkg_session
|
||||
pkg_hook_protocol --> pkg_shell
|
||||
pkg_lsp_stdio --> pkg_brand
|
||||
pkg_lsp_stdio --> pkg_error
|
||||
pkg_lsp_stdio --> pkg_fs
|
||||
pkg_lsp_stdio --> pkg_llm
|
||||
pkg_lsp_stdio --> pkg_lsp
|
||||
@@ -742,6 +747,7 @@ flowchart TD
|
||||
pkg_session_title --> pkg_session
|
||||
pkg_session_title --> pkg_session_projection
|
||||
pkg_working_directory --> pkg_agent
|
||||
pkg_working_directory --> pkg_error
|
||||
pkg_working_directory --> pkg_fs
|
||||
pkg_working_directory --> pkg_llm
|
||||
pkg_working_directory --> pkg_session
|
||||
@@ -861,6 +867,7 @@ flowchart TD
|
||||
pkg_tool_goal --> pkg_system_prompt
|
||||
pkg_tool_goal --> pkg_tools
|
||||
pkg_tool_fs --> pkg_attachment
|
||||
pkg_tool_fs --> pkg_error
|
||||
pkg_tool_fs --> pkg_fs
|
||||
pkg_tool_fs --> pkg_llm
|
||||
pkg_tool_fs --> pkg_sandbox
|
||||
@@ -1059,6 +1066,7 @@ flowchart TD
|
||||
pkg_tool_working_directory --> pkg_tools
|
||||
pkg_tool_working_directory --> pkg_working_directory
|
||||
pkg_shell_env --> pkg_app_boot
|
||||
pkg_shell_env --> pkg_error
|
||||
pkg_shell_env --> pkg_home_paths
|
||||
pkg_shell_env --> pkg_shell
|
||||
pkg_shell_env --> pkg_tools
|
||||
@@ -1176,6 +1184,7 @@ flowchart TD
|
||||
pkg_tool_bash --> pkg_sandbox_policy
|
||||
pkg_tool_bash --> pkg_shell
|
||||
pkg_tool_bash --> pkg_shell_env
|
||||
pkg_tool_bash --> pkg_subprocess
|
||||
pkg_tool_bash --> pkg_system_prompt
|
||||
pkg_tool_bash --> pkg_tools
|
||||
pkg_tool_bash --> pkg_user_approval
|
||||
@@ -1187,6 +1196,7 @@ flowchart TD
|
||||
pkg_tool_pwsh --> pkg_sandbox_policy
|
||||
pkg_tool_pwsh --> pkg_shell
|
||||
pkg_tool_pwsh --> pkg_shell_env
|
||||
pkg_tool_pwsh --> pkg_subprocess
|
||||
pkg_tool_pwsh --> pkg_system_prompt
|
||||
pkg_tool_pwsh --> pkg_tools
|
||||
pkg_tool_pwsh --> pkg_user_approval
|
||||
@@ -1684,7 +1694,6 @@ flowchart TD
|
||||
| [`session-format-v3-to-v4`](../packages/session/session-format-v3-to-v4) | `session` | — |
|
||||
| [`ssh-helper-runtime`](../packages/ssh/ssh-helper-runtime) | `ssh` | — |
|
||||
| [`storage`](../packages/storage/storage) | `storage` | — |
|
||||
| [`win32-process`](../packages/subprocess/win32-process) | `subprocess` | — |
|
||||
| [`llm-mock-server`](../packages/test-support/llm-mock-server) | `test-support` | — |
|
||||
| [`typert-generator`](../packages/typert/generator) | `typert` | — |
|
||||
| [`typert-protocol`](../packages/typert/protocol) | `typert` | — |
|
||||
@@ -1692,7 +1701,7 @@ flowchart TD
|
||||
| [`llm`](../packages/llm/llm) | `llm` | [`error`](../packages/util/error) |
|
||||
| [`session`](../packages/core/session) | `core` | [`scope`](../packages/core/scope) |
|
||||
| [`api-remotes`](../packages/api/remotes) | `api` | [`scope`](../packages/core/scope) |
|
||||
| [`attachment`](../packages/attachment/attachment) | `attachment` | [`brand`](../packages/util/brand) |
|
||||
| [`attachment`](../packages/attachment/attachment) | `attachment` | [`brand`](../packages/util/brand), [`error`](../packages/util/error) |
|
||||
| [`app-boot`](../packages/boot/app-boot) | `boot` | [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment) |
|
||||
| [`browser-use`](../packages/browser-use/browser-use) | `browser-use` | [`brand`](../packages/util/brand) |
|
||||
| [`client-connection`](../packages/client/connection) | `client` | [`scope`](../packages/core/scope) |
|
||||
@@ -1703,7 +1712,8 @@ flowchart TD
|
||||
| [`storage-domain`](../packages/storage/storage-domain) | `storage` | [`storage`](../packages/storage/storage) |
|
||||
| [`storage-json`](../packages/storage/storage-json) | `storage` | [`storage`](../packages/storage/storage) |
|
||||
| [`storage-sqlite`](../packages/storage/storage-sqlite) | `storage` | [`storage`](../packages/storage/storage) |
|
||||
| [`subprocess`](../packages/subprocess/subprocess) | `subprocess` | [`http-proxy`](../packages/util/http-proxy) |
|
||||
| [`subprocess`](../packages/subprocess/subprocess) | `subprocess` | [`error`](../packages/util/error), [`http-proxy`](../packages/util/http-proxy) |
|
||||
| [`win32-process`](../packages/subprocess/win32-process) | `subprocess` | [`error`](../packages/util/error) |
|
||||
| [`remote-mock`](../packages/test-support/remote-mock) | `test-support` | [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`typert-loader`](../packages/typert/loader) | `typert` | [`typert-registry`](../packages/typert/registry) |
|
||||
| [`util-ripgrep`](../packages/util/ripgrep) | `util` | [`subprocess`](../packages/subprocess/subprocess) |
|
||||
@@ -1721,7 +1731,7 @@ flowchart TD
|
||||
| [`host-directory-picker-auto`](../packages/host/directory-picker-auto) | `host` | [`client-connection`](../packages/client/connection), [`client-ui-directory-picker-browse`](../packages/client/ui-directory-picker-browse), [`client-ui-directory-picker-native`](../packages/client/ui-directory-picker-native), [`host-directory-picker-browse`](../packages/host/directory-picker-browse), [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-webserver`](../packages/host/webserver) |
|
||||
| [`host-frontend-static`](../packages/host/frontend-static) | `host` | [`client-connection`](../packages/client/connection), [`host-webserver`](../packages/host/webserver) |
|
||||
| [`lsp`](../packages/lsp/lsp) | `lsp` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm) |
|
||||
| [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
|
||||
| [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`error`](../packages/util/error), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
|
||||
| [`session-format-catalog`](../packages/session/session-format-catalog) | `session` | [`session`](../packages/core/session) |
|
||||
| [`session-log-deepseek`](../packages/session/session-log-deepseek) | `session` | [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`session`](../packages/core/session) |
|
||||
| [`session-persistence`](../packages/session/session-persistence) | `session` | [`brand`](../packages/util/brand), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
|
||||
@@ -1763,7 +1773,7 @@ flowchart TD
|
||||
| [`experimental-client-ui-cot-translation`](../packages/experimental/client-ui-cot-translation) | `experimental` | [`experimental-translator`](../packages/experimental/translator) |
|
||||
| [`experimental-speech-to-text-sensevoice`](../packages/experimental/speech-to-text-sensevoice) | `experimental` | [`experimental-speech-to-text`](../packages/experimental/speech-to-text), [`subprocess`](../packages/subprocess/subprocess) |
|
||||
| [`hook-protocol`](../packages/experimental/hook-protocol) | `experimental` | [`session`](../packages/core/session), [`shell`](../packages/shell/shell) |
|
||||
| [`lsp-stdio`](../packages/lsp/lsp-stdio) | `lsp` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||
| [`lsp-stdio`](../packages/lsp/lsp-stdio) | `lsp` | [`brand`](../packages/util/brand), [`error`](../packages/util/error), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||
| [`bash-local`](../packages/shell/bash-local) | `shell` | [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||
| [`pwsh-local`](../packages/shell/pwsh-local) | `shell` | [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||
| [`llm-deepseek-api-key`](../packages/llm/llm-deepseek-api-key) | `llm` | [`credentials`](../packages/credentials/credentials), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek) |
|
||||
@@ -1782,7 +1792,7 @@ flowchart TD
|
||||
| [`sandbox-policy`](../packages/sandbox/sandbox-policy) | `sandbox` | [`agent`](../packages/core/agent), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt) |
|
||||
| [`session-telemetry`](../packages/session/session-telemetry) | `session` | [`agent`](../packages/core/agent), [`session`](../packages/core/session) |
|
||||
| [`session-title`](../packages/session/session-title) | `session` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
|
||||
| [`working-directory`](../packages/session/working-directory) | `session` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt) |
|
||||
| [`working-directory`](../packages/session/working-directory) | `session` | [`agent`](../packages/core/agent), [`error`](../packages/util/error), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt) |
|
||||
| [`terminal`](../packages/terminal/terminal) | `terminal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`error`](../packages/util/error) |
|
||||
| [`loader-smoke`](../packages/test-support/loader-smoke) | `test-support` | [`agent`](../packages/core/agent), [`http-proxy`](../packages/util/http-proxy), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
|
||||
| [`workflow`](../packages/workflow/workflow) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
|
||||
@@ -1806,7 +1816,7 @@ flowchart TD
|
||||
| [`tool-executor-native`](../packages/core/tool-executor-native) | `core` | [`tools`](../packages/core/tools) |
|
||||
| [`tool-executor-ptc`](../packages/core/tool-executor-ptc) | `core` | [`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), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`tool-goal`](../packages/goal/tool-goal) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
|
||||
| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`attachment`](../packages/attachment/attachment), [`error`](../packages/util/error), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) | `fs` | [`fs`](../packages/fs/fs), [`output-retention`](../packages/util/output-retention), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`tools`](../packages/core/tools) |
|
||||
| [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
|
||||
@@ -1845,7 +1855,7 @@ flowchart TD
|
||||
| [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy) | `session` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`tools`](../packages/core/tools) |
|
||||
| [`session-title-first-prompt-llm`](../packages/session/session-title-first-prompt-llm) | `session` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) |
|
||||
| [`tool-working-directory`](../packages/session/tool-working-directory) | `session` | [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`shell-env`](../packages/shell/shell-env) | `shell` | [`app-boot`](../packages/boot/app-boot), [`home-paths`](../packages/util/home-paths), [`shell`](../packages/shell/shell), [`tools`](../packages/core/tools) |
|
||||
| [`shell-env`](../packages/shell/shell-env) | `shell` | [`app-boot`](../packages/boot/app-boot), [`error`](../packages/util/error), [`home-paths`](../packages/util/home-paths), [`shell`](../packages/shell/shell), [`tools`](../packages/core/tools) |
|
||||
| [`tool-bash-persistent`](../packages/shell/tool-bash-persistent) | `shell` | [`agent`](../packages/core/agent), [`output-retention`](../packages/util/output-retention), [`terminal`](../packages/terminal/terminal), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`tool-pwsh-persistent`](../packages/shell/tool-pwsh-persistent) | `shell` | [`agent`](../packages/core/agent), [`output-retention`](../packages/util/output-retention), [`terminal`](../packages/terminal/terminal), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`ssh`](../packages/ssh/ssh) | `ssh` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`fs-sandbox`](../packages/fs/fs-sandbox), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-local`](../packages/sandbox/sandbox-local), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session-projection`](../packages/session/session-projection), [`subprocess`](../packages/subprocess/subprocess), [`subprocess-local`](../packages/subprocess/subprocess-local), [`util-values`](../packages/util/values) |
|
||||
@@ -1865,8 +1875,8 @@ flowchart TD
|
||||
| [`mcp-client`](../packages/mcp/mcp-client) | `mcp` | [`attachment`](../packages/attachment/attachment), [`credentials`](../packages/credentials/credentials), [`llm`](../packages/llm/llm), [`mcp-resources`](../packages/mcp/mcp-resources), [`scope`](../packages/core/scope), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
|
||||
| [`agent-preset`](../packages/preset/agent-preset) | `preset` | [`agent-preset-registry`](../packages/preset/agent-preset-registry) |
|
||||
| [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | `session` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`command-feedback`](../packages/feedback/command-feedback), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`otel`](../packages/telemetry/otel), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
| [`tool-bash`](../packages/shell/tool-bash) | `shell` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`tool-pwsh`](../packages/shell/tool-pwsh) | `shell` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`tool-bash`](../packages/shell/tool-bash) | `shell` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`tool-pwsh`](../packages/shell/tool-pwsh) | `shell` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`fs-ssh`](../packages/ssh/fs-ssh) | `ssh` | [`fs`](../packages/fs/fs), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`ssh`](../packages/ssh/ssh) |
|
||||
| [`sandbox-ssh`](../packages/ssh/sandbox-ssh) | `ssh` | [`sandbox`](../packages/sandbox/sandbox), [`ssh`](../packages/ssh/ssh) |
|
||||
| [`subprocess-ssh`](../packages/ssh/subprocess-ssh) | `ssh` | [`ssh`](../packages/ssh/ssh), [`subprocess`](../packages/subprocess/subprocess), [`subprocess-local`](../packages/subprocess/subprocess-local) |
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
en: 35ffb6107a5fbae2
|
||||
zh: 06ce2128075e157b
|
||||
/session-persistence-event-catalog/event-envelope:
|
||||
en: 56414ffd00cdc32a
|
||||
zh: 6dc11eb013e95e41
|
||||
en: 189352d7a3bba620
|
||||
zh: 5361f42035034672
|
||||
/session-persistence-event-catalog/events:
|
||||
en: f4776ba3d0fd53ce
|
||||
zh: e86769f50a69055a
|
||||
@@ -300,8 +300,8 @@
|
||||
en: e8dd0cebbb930eb7
|
||||
zh: e8dd0cebbb930eb7
|
||||
/session-persistence-event-catalog/events/working-directory/working-directory-change-log-only:
|
||||
en: fc893ed0d93765df
|
||||
zh: 2df365d9d0ac6f6e
|
||||
en: 27f0761cbdd6fe2f
|
||||
zh: 33528f333498132d
|
||||
/session-persistence-event-catalog/events/workspace:
|
||||
en: 63ee1a85e4b59ae3
|
||||
zh: 63ee1a85e4b59ae3
|
||||
@@ -1203,8 +1203,8 @@
|
||||
en: 1f691c223f54ab2c
|
||||
zh: f88fef3ee9745c91
|
||||
/session-persistence-event-catalog/resolved-persistence-types/sandboxmode:
|
||||
en: c9fa5343ea99733b
|
||||
zh: d6de41351c57a73f
|
||||
en: 5adc2bf6745ccc67
|
||||
zh: 566cb9aaa4a177c9
|
||||
/session-persistence-event-catalog/resolved-persistence-types/schedulechange:
|
||||
en: b20a355fde48f699
|
||||
zh: 5d0fd858e2dbff06
|
||||
@@ -1494,8 +1494,8 @@
|
||||
en: bef51e0442ebb903
|
||||
zh: 240ebeb160533d8c
|
||||
/session-persistence-event-catalog/resolved-persistence-types/cwd:
|
||||
en: 35feea0430c0f14b
|
||||
zh: d5aaf1d2ca80114a
|
||||
en: 2809c921c6996791
|
||||
zh: 80b15177561766d7
|
||||
/session-persistence-event-catalog/resolved-persistence-types/decision-durationms-exitcode-handlerid:
|
||||
en: a99ac57bbec0370f
|
||||
zh: 9073329e324a66e0
|
||||
@@ -1929,8 +1929,8 @@
|
||||
en: 0b74295f7c5c8f11
|
||||
zh: 5f1213cec87f6be0
|
||||
/session-persistence-event-catalog/resolved-persistence-types/kind-working-directory:
|
||||
en: 626ea3180b86e5b6
|
||||
zh: 4b6e0dc1312b3c5a
|
||||
en: b28ca6b37f5a338d
|
||||
zh: 4c4f2f05721b8b62
|
||||
/session-persistence-event-catalog/resolved-persistence-types/max-tokens-messages-model-thinking:
|
||||
en: 3216b23c131d0070
|
||||
zh: 73ca171c6014f16e
|
||||
|
||||
@@ -161,7 +161,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
|
||||
}[T]
|
||||
```
|
||||
|
||||
Sources: [`packages/core/session/src/types.ts:439`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:447`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:470`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:501`](../packages/core/session/src/types.ts)
|
||||
Sources: [`packages/core/session/src/types.ts:439`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:447`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:470`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:506`](../packages/core/session/src/types.ts)
|
||||
|
||||
## Events
|
||||
|
||||
@@ -1329,7 +1329,7 @@ Source: [`packages/web/web-search-deepseek/src/provider.ts:89`](../packages/web/
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/session/working-directory/src/index.ts:52`](../packages/session/working-directory/src/index.ts)
|
||||
Source: [`packages/session/working-directory/src/index.ts:55`](../packages/session/working-directory/src/index.ts)
|
||||
|
||||
### `workspace/*`
|
||||
|
||||
@@ -4619,7 +4619,7 @@ One of:
|
||||
|
||||
SHA-256: `fab2bb09bc5e3b9db0685e8f5500b029af1a2b33e70bc703c1d5d0f596fbc1fd`
|
||||
|
||||
Sources: [`packages/sandbox/sandbox/src/index.ts:30`](../packages/sandbox/sandbox/src/index.ts)
|
||||
Sources: [`packages/sandbox/sandbox/src/index.ts:38`](../packages/sandbox/sandbox/src/index.ts)
|
||||
|
||||
One of:
|
||||
|
||||
@@ -6209,7 +6209,7 @@ Sources: [`packages/compaction/compaction/src/types.ts:24`](../packages/compacti
|
||||
|
||||
SHA-256: `da60ed58a8fa23d7aa4920ab5c1f4e3d28109cb0797ef9e5c8b125721b847582`
|
||||
|
||||
Sources: [`packages/session/working-directory/src/index.ts:52`](../packages/session/working-directory/src/index.ts)
|
||||
Sources: [`packages/session/working-directory/src/index.ts:55`](../packages/session/working-directory/src/index.ts)
|
||||
|
||||
| Property | Presence | Type |
|
||||
|---|---|---|
|
||||
@@ -7949,7 +7949,7 @@ Sources: [`packages/experimental/webhook/src/types.ts:74`](../packages/experimen
|
||||
|
||||
SHA-256: `2235d957bf4b7c6cf959231d480f0ba1f3a6d72d82981493fb6c7c963021d3d5`
|
||||
|
||||
Sources: [`packages/session/working-directory/src/index.ts:45`](../packages/session/working-directory/src/index.ts)
|
||||
Sources: [`packages/session/working-directory/src/index.ts:48`](../packages/session/working-directory/src/index.ts)
|
||||
|
||||
| Property | Presence | Type |
|
||||
|---|---|---|
|
||||
|
||||
@@ -163,7 +163,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
|
||||
}[T]
|
||||
```
|
||||
|
||||
来源:[`packages/core/session/src/types.ts:439`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:447`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:470`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:501`](../packages/core/session/src/types.ts)
|
||||
来源:[`packages/core/session/src/types.ts:439`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:447`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:470`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:506`](../packages/core/session/src/types.ts)
|
||||
|
||||
## 事件
|
||||
|
||||
@@ -1331,7 +1331,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
|
||||
}
|
||||
```
|
||||
|
||||
来源:[`packages/session/working-directory/src/index.ts:52`](../packages/session/working-directory/src/index.ts)
|
||||
来源:[`packages/session/working-directory/src/index.ts:55`](../packages/session/working-directory/src/index.ts)
|
||||
|
||||
### `workspace/*`
|
||||
|
||||
@@ -4621,7 +4621,7 @@ SHA-256: `530da6c0267813bb4511af72883cc518834e624e3b3220a7062955fd57a788a6`
|
||||
|
||||
SHA-256: `fab2bb09bc5e3b9db0685e8f5500b029af1a2b33e70bc703c1d5d0f596fbc1fd`
|
||||
|
||||
来源:[`packages/sandbox/sandbox/src/index.ts:30`](../packages/sandbox/sandbox/src/index.ts)
|
||||
来源:[`packages/sandbox/sandbox/src/index.ts:38`](../packages/sandbox/sandbox/src/index.ts)
|
||||
|
||||
以下类型之一:
|
||||
|
||||
@@ -6211,7 +6211,7 @@ SHA-256: `3e4e7683b0192c9f0e6a75c59076108f017892cdb302a98032c2632fd79c356f`
|
||||
|
||||
SHA-256: `da60ed58a8fa23d7aa4920ab5c1f4e3d28109cb0797ef9e5c8b125721b847582`
|
||||
|
||||
来源:[`packages/session/working-directory/src/index.ts:52`](../packages/session/working-directory/src/index.ts)
|
||||
来源:[`packages/session/working-directory/src/index.ts:55`](../packages/session/working-directory/src/index.ts)
|
||||
|
||||
| 属性 | 存在性 | 类型 |
|
||||
|---|---|---|
|
||||
@@ -7951,7 +7951,7 @@ SHA-256: `9edc162949abf93bd5ff049191b345454ba104f583937ce5580fb50f067d2b94`
|
||||
|
||||
SHA-256: `2235d957bf4b7c6cf959231d480f0ba1f3a6d72d82981493fb6c7c963021d3d5`
|
||||
|
||||
来源:[`packages/session/working-directory/src/index.ts:45`](../packages/session/working-directory/src/index.ts)
|
||||
来源:[`packages/session/working-directory/src/index.ts:48`](../packages/session/working-directory/src/index.ts)
|
||||
|
||||
| 属性 | 存在性 | 类型 |
|
||||
|---|---|---|
|
||||
|
||||
Generated
+3
-3
@@ -159,7 +159,7 @@
|
||||
{"digest":"2142e391b9ccb60d487afcf373ce74db98d952f69e8289eddb352a7e16e996c5","names":[],"sources":[]},
|
||||
{"digest":"2152caf1a71e588e4df2358a806dea24936c1c47131134f01a12939d12cdc0c0","names":["packages/team/agent-team/src/types.ts#TeamMessageSource"],"sources":["packages/team/agent-team/src/types.ts:121"]},
|
||||
{"digest":"21fb6a90d5068f6a0003b7ab316ed2f56342477146a65c00db0f13c4d8df667d","names":[],"sources":[]},
|
||||
{"digest":"2235d957bf4b7c6cf959231d480f0ba1f3a6d72d82981493fb6c7c963021d3d5","names":[],"sources":["packages/session/working-directory/src/index.ts:45"]},
|
||||
{"digest":"2235d957bf4b7c6cf959231d480f0ba1f3a6d72d82981493fb6c7c963021d3d5","names":[],"sources":["packages/session/working-directory/src/index.ts:48"]},
|
||||
{"digest":"224aa11d4d5f88c450822d01b99e6ca5e3a9c7f26a5ede10bae33384a09ef4dc","names":[],"sources":[]},
|
||||
{"digest":"234142ee4f996f38cc9fa3b8b3e3de29b03c9aefe5ebf6c224eddd564505aa85","names":[],"sources":[]},
|
||||
{"digest":"237c004c73b6b288246816e0d7fde16e09c63a47aa3b1227383d7afe58666163","names":[],"sources":[]},
|
||||
@@ -607,7 +607,7 @@
|
||||
{"digest":"d9196086e41e4dd5c2ce87f5c25529c8112a1731d258cf0728747d41a9ee3241","names":[],"sources":[]},
|
||||
{"digest":"d9c462b9f62fdd3efc76a44b9524dc8781f6b91cd9c7e584b17d971defafebab","names":["packages/llm/llm/src/message.ts#ToolMessageSource"],"sources":["packages/llm/llm/src/message.ts:29"]},
|
||||
{"digest":"d9edf1862e4471d6ad8a106a3f4160e0643874368707d50ecd8e27e5ee7a2567","names":[],"sources":[]},
|
||||
{"digest":"da60ed58a8fa23d7aa4920ab5c1f4e3d28109cb0797ef9e5c8b125721b847582","names":[],"sources":["packages/session/working-directory/src/index.ts:52"]},
|
||||
{"digest":"da60ed58a8fa23d7aa4920ab5c1f4e3d28109cb0797ef9e5c8b125721b847582","names":[],"sources":["packages/session/working-directory/src/index.ts:55"]},
|
||||
{"digest":"dacf3d5f3e4ff619c1320e64a48dec5e7ecd7870bece65fbd449a0e63adfa521","names":[],"sources":[]},
|
||||
{"digest":"db06ef35b27a38425046b2228fa6bce9582abe140d781d363137eb2f4d233a1c","names":[],"sources":[]},
|
||||
{"digest":"db6c5246911d47a7969f6b4cb47c7d0dbbde75c402710b22ace3672fa97bebe0","names":[],"sources":[]},
|
||||
@@ -692,7 +692,7 @@
|
||||
{"digest":"f97f47b0eb87905ed77cc27c5e4ed75ef805cb372c98905253709de1aab9b63c","names":[],"sources":[]},
|
||||
{"digest":"f9cfc89e8fe085b4fcc9cd32b74b6f103608c54d7d8cd97b47c01de34f252ba3","names":[],"sources":[]},
|
||||
{"digest":"fa87537f4a9c1e25602990a8bf686b383e9e00352ec8946e16ae24ba7c1625be","names":["packages/llm/llm/src/message.ts#AssistantMessage"],"sources":["packages/llm/llm/src/message.ts:167"]},
|
||||
{"digest":"fab2bb09bc5e3b9db0685e8f5500b029af1a2b33e70bc703c1d5d0f596fbc1fd","names":["packages/sandbox/sandbox/src/index.ts#SandboxMode"],"sources":["packages/sandbox/sandbox/src/index.ts:30"]},
|
||||
{"digest":"fab2bb09bc5e3b9db0685e8f5500b029af1a2b33e70bc703c1d5d0f596fbc1fd","names":["packages/sandbox/sandbox/src/index.ts#SandboxMode"],"sources":["packages/sandbox/sandbox/src/index.ts:38"]},
|
||||
{"digest":"fab35c4520883c0d69e42d079c9f543fc5ac7a2d15b537b5572fcd99059c7018","names":[],"sources":[]},
|
||||
{"digest":"fb5e0517050d2c6d40e5b8145db1f7c5ca7044d5d50fada6ff80e0b22c6ca973","names":[],"sources":[]},
|
||||
{"digest":"fb8b6a2e53cb9cf44c551176de0fb77d1ddc60037b99a891ff8a860b6d5c29a0","names":[],"sources":[]},
|
||||
|
||||
@@ -78,6 +78,7 @@ interface Agent {
|
||||
* Clear queued and steering work — unless `keepInbox` — and abort the active
|
||||
* turn or between-turn task. The first cause wins for that activity. With no
|
||||
* active activity, cancellation is a no-op and does not arm later work.
|
||||
* Queue-write failures still abort the activity and propagate to the caller.
|
||||
* @param cause - the stable caller intent carried by the active operation signal.
|
||||
* @param options - cancellation options; `keepInbox` preserves pending work.
|
||||
*/
|
||||
|
||||
@@ -82,6 +82,7 @@ interface Agent {
|
||||
* Clear queued and steering work — unless `keepInbox` — and abort the active
|
||||
* turn or between-turn task. The first cause wins for that activity. With no
|
||||
* active activity, cancellation is a no-op and does not arm later work.
|
||||
* Queue-write failures still abort the activity and propagate to the caller.
|
||||
* @param cause - the stable caller intent carried by the active operation signal.
|
||||
* @param options - cancellation options; `keepInbox` preserves pending work.
|
||||
*/
|
||||
|
||||
@@ -30,8 +30,8 @@
|
||||
en: 7b462db42ab89b10
|
||||
zh: 968219ded2b55d69
|
||||
/filesystem/error-taxonomy-provider-contract:
|
||||
en: ca206569d4e2528d
|
||||
zh: e36d70ad39a73570
|
||||
en: dbab88fbb60fcb8f
|
||||
zh: f90be1352b1e05f8
|
||||
/filesystem/no-timeouts-on-file-io:
|
||||
en: ae18af906f3f7e50
|
||||
zh: 24a1aa6f903fe08d
|
||||
|
||||
@@ -375,7 +375,7 @@ import { FS_ERROR_CODES } from '@deepseek-ai/dsh-fs'
|
||||
type FsErrorCode = typeof FS_ERROR_CODES[number]
|
||||
```
|
||||
|
||||
`FS_NOT_DIRECTORY`, `FS_PERMISSION_DENIED`, and `FS_IO_ERROR` are used by directory listing to distinguish an existing non-directory target, a denied listing, and an unexpected backend I/O failure. `FS_SANDBOX_DENIED` is a POLICY refusal from a sandbox-enforcing backend (`dsh-fs-sandbox`) — the mode fence denied a mutation — distinct from `FS_PERMISSION_DENIED` (the host kernel refusing). `FS_NOT_OBSERVED` means the policy plugin has no prior-observation record for this owner (or a `createIfAbsent` hit an existing file). `FS_NOT_FOUND` also represents an edit rejected from confirmed absence. `FS_STALE_VERSION` means the backend version no longer matches the observed one (or the provider itself receives an edit for a missing target). Freshness authorization has no partial/full distinction, so there is no `FS_PARTIAL_OBSERVATION`.
|
||||
`FS_NOT_DIRECTORY`, `FS_PERMISSION_DENIED`, and `FS_IO_ERROR` are used by directory listing to distinguish an existing non-directory target, a denied listing, and an unexpected backend I/O failure. `FS_SANDBOX_DENIED` is a POLICY refusal from a sandbox-enforcing backend (`dsh-fs-sandbox`) — the mode fence denied a mutation — distinct from `FS_PERMISSION_DENIED` (the host kernel refusing). `FS_NOT_OBSERVED` means the policy plugin has no prior-observation record for this owner (or a `createIfAbsent` hit an existing file). `FS_NOT_FOUND` also represents an edit rejected from confirmed absence. `FS_STALE_VERSION` means the backend version no longer matches the observed one (or the provider itself receives an edit for a missing target). Freshness authorization has no partial/full distinction, so there is no `FS_PARTIAL_OBSERVATION`. `FS_ALREADY_EXISTS` means a create-only tool command found an existing target; the caller chooses another path or edits the existing file. `dsh-fs-local` and the `dsh-fs-sandbox` containment check classify errno failures from resolution, metadata, reads, and listing with the `dsh-fs` function `fsErrorFromErrno`: `ENOENT` and `ENOTDIR` report `FS_NOT_FOUND`, `EACCES` and `EPERM` report `FS_PERMISSION_DENIED`, `EISDIR` reports `FS_NOT_REGULAR_FILE`, and other errno values report `FS_IO_ERROR`, each with the Node error as `cause`.
|
||||
|
||||
## No timeouts on file IO
|
||||
|
||||
|
||||
@@ -375,7 +375,7 @@ import { FS_ERROR_CODES } from '@deepseek-ai/dsh-fs'
|
||||
type FsErrorCode = typeof FS_ERROR_CODES[number]
|
||||
```
|
||||
|
||||
目录列表使用 `FS_NOT_DIRECTORY`、`FS_PERMISSION_DENIED` 与 `FS_IO_ERROR` 区分已存在但并非目录的目标、被拒绝的列表操作和意外的后端 I/O 失败。`FS_SANDBOX_DENIED` 是强制执行沙箱的后端(`dsh-fs-sandbox`)所作的策略拒绝——模式边界拒绝了变更——与 `FS_PERMISSION_DENIED`(宿主内核拒绝)不同。`FS_NOT_OBSERVED` 表示策略插件没有此所有者的先前观测记录(或 `createIfAbsent` 遇到了现有文件)。`FS_NOT_FOUND` 也表示策略因确认缺失而拒绝 edit。`FS_STALE_VERSION` 表示后端版本不再与观测到的版本匹配(或提供方本身收到针对缺失目标的 edit)。新鲜度授权没有部分/完整之分,因此不存在 `FS_PARTIAL_OBSERVATION`。
|
||||
目录列表使用 `FS_NOT_DIRECTORY`、`FS_PERMISSION_DENIED` 与 `FS_IO_ERROR` 区分已存在但并非目录的目标、被拒绝的列表操作和意外的后端 I/O 失败。`FS_SANDBOX_DENIED` 是强制执行沙箱的后端(`dsh-fs-sandbox`)所作的策略拒绝——模式边界拒绝了变更——与 `FS_PERMISSION_DENIED`(宿主内核拒绝)不同。`FS_NOT_OBSERVED` 表示策略插件没有此所有者的先前观测记录(或 `createIfAbsent` 遇到了现有文件)。`FS_NOT_FOUND` 也表示策略因确认缺失而拒绝 edit。`FS_STALE_VERSION` 表示后端版本不再与观测到的版本匹配(或提供方本身收到针对缺失目标的 edit)。新鲜度授权没有部分/完整之分,因此不存在 `FS_PARTIAL_OBSERVATION`。`FS_ALREADY_EXISTS` 表示仅创建的工具命令遇到了已存在的目标;调用方应换一个路径或编辑现有文件。`dsh-fs-local` 与 `dsh-fs-sandbox` 的包含性检查用 `dsh-fs` 的函数 `fsErrorFromErrno` 分类解析、元数据、读取与目录列表中的 errno 失败:`ENOENT` 与 `ENOTDIR` 报告 `FS_NOT_FOUND`,`EACCES` 与 `EPERM` 报告 `FS_PERMISSION_DENIED`,`EISDIR` 报告 `FS_NOT_REGULAR_FILE`,其他 errno 报告 `FS_IO_ERROR`,均以 Node 错误为 `cause`。
|
||||
|
||||
## 文件 IO 不设超时
|
||||
|
||||
|
||||
@@ -6,17 +6,20 @@
|
||||
en: 3990a17e12c66a23
|
||||
zh: 4170c33e49248f8f
|
||||
/forges/providers-and-change-requests:
|
||||
en: 4ec7d1892b4067db
|
||||
zh: 6bb70d21ae3d7a89
|
||||
en: e5d807b29f553751
|
||||
zh: e0b25d9e964f988e
|
||||
/forges/status-polling:
|
||||
en: 26ccfa2e342e3e67
|
||||
zh: fe6cc2a622757c48
|
||||
/forges/change-request-actions:
|
||||
en: 87d2c81d9514267f
|
||||
zh: 1daf31c5b0127a1c
|
||||
en: ae735fbdbdf2d870
|
||||
zh: 9903fcc571104ad8
|
||||
/forges/review-threads:
|
||||
en: af729af6e59e915f
|
||||
zh: 62ddae4cfcd4431f
|
||||
/forges/session-bindings:
|
||||
en: 13dfd36f2166bba0
|
||||
zh: a369434f52f04d57
|
||||
/forges/panel:
|
||||
en: 1d3400c14260e62c
|
||||
zh: 191e9bae2d753244
|
||||
en: 4020149c26661a2d
|
||||
zh: 56842b057d4f662e
|
||||
+420
-28
@@ -47,6 +47,18 @@ interface ForgeProvider {
|
||||
readonly forge: string
|
||||
/** Publish a comment or completed review. Omission declares that this provider cannot write reviews. */
|
||||
submitReview?(ref: ChangeRequestRef, request: ForgeReviewRequest, signal: AbortSignal): Promise<ForgeReviewResult>
|
||||
/**
|
||||
* Read the review threads of a change request at head commit `headSha`. The threads are returned only when the
|
||||
* read names `headSha` as the head; once the head is another commit, the read resolves to a `stale` failure.
|
||||
* Omission declares that this provider cannot read reviews.
|
||||
*/
|
||||
readReview?(ref: ChangeRequestRef, headSha: string, signal: AbortSignal): Promise<ForgeReviewData | ForgeFailure>
|
||||
/**
|
||||
* Write to the review threads or the pending review of a change request while its head is `write.headSha`; once the
|
||||
* head is another commit, the write resolves to `stale` without writing. A write is never retried. Omission declares
|
||||
* that this provider cannot write reviews, and its `readReview` then reports no viewer permission.
|
||||
*/
|
||||
writeReview?(ref: ChangeRequestRef, write: ForgeReviewWrite, signal: AbortSignal): Promise<ForgeReviewWriteResult>
|
||||
/** Map a Git remote URL to its repository, or undefined when this provider does not serve it. */
|
||||
repositoryFromRemote(url: string): ForgeRepository | undefined
|
||||
/** Map a change request web URL to its reference, or undefined when this provider does not serve it. */
|
||||
@@ -71,16 +83,57 @@ interface ForgeProvider {
|
||||
/**
|
||||
* Read the changes of a change request at head commit `headSha` as the host computes them, as one unified
|
||||
* patch with `a/` and `b/` path prefixes. A patch is returned only when it describes `headSha`; once the
|
||||
* head is another commit, the read resolves to a `stale` failure.
|
||||
* head is another commit, the read resolves to a `stale` failure. `base`, when given, is the `baseSha` the
|
||||
* latest status recorded with `headSha`; a provider may start reading from it before it confirms the head.
|
||||
*/
|
||||
diff(ref: ChangeRequestRef, headSha: string, signal: AbortSignal): Promise<string | ForgeFailure>
|
||||
diff(ref: ChangeRequestRef, headSha: string, signal: AbortSignal, base?: string): Promise<string | ForgeFailure>
|
||||
/** Read the text of `path` at `commit` in the change request's repository. */
|
||||
fileText(ref: ChangeRequestRef, commit: string, path: string, signal: AbortSignal): Promise<string | ForgeFailure>
|
||||
/** Build the web page of `path` at `commit` in the change request's repository. */
|
||||
fileUrl(ref: ChangeRequestRef, commit: string, path: string): string
|
||||
/**
|
||||
* List the commits of a change request at its current head. A provider implements `commits` and `compare`
|
||||
* together; omitting either declares that the change request can be read only as a whole.
|
||||
*/
|
||||
commits?(ref: ChangeRequestRef, signal: AbortSignal): Promise<ForgeCommitList | ForgeFailure>
|
||||
/**
|
||||
* Read the changes from commit `base` to commit `head` in the change request's repository, as one unified patch
|
||||
* with `a/` and `b/` path prefixes. Both are full object names; `head` descends from `base`.
|
||||
*/
|
||||
compare?(ref: ChangeRequestRef, base: string, head: string, signal: AbortSignal): Promise<string | ForgeFailure>
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** One commit of a change request. */
|
||||
interface ForgeCommit {
|
||||
/** Full commit object name. */
|
||||
readonly sha: string
|
||||
/** Full object name of the first parent, or null for a root commit. */
|
||||
readonly parent: string | null
|
||||
/** First line of the commit message. */
|
||||
readonly title: string
|
||||
/** Author name or login as the host reports it, or null when it reports none. */
|
||||
readonly author: string | null
|
||||
/** ISO 8601 time the commit was authored. */
|
||||
readonly authoredAt: string
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** The commits of a change request at one head commit. */
|
||||
interface ForgeCommitList {
|
||||
/** Head commit the list was read at. */
|
||||
readonly headSha: string
|
||||
/** Commits oldest first, ending with the head commit unless `truncated`. */
|
||||
readonly commits: readonly ForgeCommit[]
|
||||
/** Whether the change request has more commits than the provider lists; the listed ones are the oldest. */
|
||||
readonly truncated: boolean
|
||||
}
|
||||
```
|
||||
|
||||
`ctx.forge.commits` and `ctx.forge.compare` delegate to a provider that implements both; otherwise `commits` resolves to undefined and `compare` to an `unavailable` failure, so the forge panel offers its commit picker only where the provider reads commits.
|
||||
|
||||
```ts type-equiv
|
||||
/** Whether a provider can reach one host with the current credentials. */
|
||||
interface ForgeReadiness {
|
||||
@@ -159,6 +212,11 @@ interface ChangeRequestSnapshot {
|
||||
readonly headSha: string
|
||||
/** Branch the change request merges into. */
|
||||
readonly baseBranch: string
|
||||
/**
|
||||
* Base commit the host recorded with `headSha`, whose merge base with the head starts the change request's diff;
|
||||
* omitted when the provider does not report it.
|
||||
*/
|
||||
readonly baseSha?: string
|
||||
/** Lines the change request adds. */
|
||||
readonly additions: number
|
||||
/** Lines the change request deletes. */
|
||||
@@ -198,9 +256,9 @@ interface ForgeFailure {
|
||||
* `not-found`: the host does not expose it; `unauthenticated`: credentials are missing or rejected;
|
||||
* `too-large`: the host or the provider refuses an answer over its size limit, so reading again fails
|
||||
* the same way; `stale`: the head is no longer the commit the read named, so reading again at the head of
|
||||
* a later status can succeed; `unavailable`: any other failure.
|
||||
* a later status can succeed; `unsupported`: the provider does not offer the read; `unavailable`: any other failure.
|
||||
*/
|
||||
readonly code: 'not-found' | 'unauthenticated' | 'too-large' | 'stale' | 'unavailable'
|
||||
readonly code: 'not-found' | 'unauthenticated' | 'too-large' | 'stale' | 'unsupported' | 'unavailable'
|
||||
/** Human-readable cause. */
|
||||
readonly message: string
|
||||
}
|
||||
@@ -257,7 +315,7 @@ interface ForgeActionFailure {
|
||||
}
|
||||
```
|
||||
|
||||
`ctx.forge.submitReview` delegates an explicit comment or completed review to the provider. A provider that omits this capability returns unsupported. Consumers retain pending drafts and reconcile unknown write outcomes before resubmitting.
|
||||
`ctx.forge.submitReview` delegates an explicit comment or completed review to the provider. A provider that omits this capability returns unsupported. Consumers reconcile unknown write outcomes before resubmitting; comments kept for a later review go to the code host's pending review through `ctx.forge.writeReview`.
|
||||
|
||||
```ts type-equiv
|
||||
/** One inline comment addressed to the old or new side of a pull request diff. */
|
||||
@@ -277,17 +335,188 @@ type ForgeReviewRequest = {
|
||||
readonly headSha: string
|
||||
} & (
|
||||
| { readonly kind: 'comment'; readonly comment: ForgeLineComment }
|
||||
| { readonly kind: 'review'; readonly event: 'comment' | 'approve' | 'request-changes'; readonly body: string; readonly comments: readonly ForgeLineComment[] }
|
||||
| { readonly kind: 'review'; readonly event: ForgeReviewEvent; readonly body: string; readonly comments: readonly ForgeLineComment[] }
|
||||
)
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Result of a review write; unknown outcomes must be checked on the host before another submission. */
|
||||
/** Result of a review submission; unknown outcomes must be checked on the host before another submission. */
|
||||
type ForgeReviewResult =
|
||||
| { readonly ok: true; readonly url: string }
|
||||
| { readonly ok: false; readonly code: 'unsupported' | 'stale' | 'failed' | 'unknown'; readonly message: string }
|
||||
```
|
||||
|
||||
## Review threads
|
||||
|
||||
`ctx.forge.readReview(ref, headSha, signal)` reads the review threads of a change request at one head commit. The provider returns `ForgeReviewData` only when the read names `headSha` as the head; otherwise it returns `stale`, which polls a watched change request again at once, as a stale diff read does. For a provider without `readReview` the service returns `unsupported`, so consumers show no threads instead of a failure. A line thread has two positions: `original` on the commit it was written on, and `current` on the head of the read, which is null once the host no longer places the thread on that commit. A consumer that shows the head's diff places a thread at `current` and shows the others with their `diffHunk`; reading the file at `original.commit` through `fileText` shows the whole file the thread was written on. The [GitHub provider README](../../packages/experimental/forge-github/README.md#use-this-package) owns how GitHub's fields map onto these positions.
|
||||
|
||||
```ts type-equiv
|
||||
/** Opaque host identity of one review thread. */
|
||||
type ForgeThreadId = Branded<'ForgeThreadId'>
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Opaque host identity of one review comment. */
|
||||
type ForgeCommentId = Branded<'ForgeCommentId'>
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Consecutive lines on one side of a diff whose new side is one commit. */
|
||||
interface ForgeAnchor {
|
||||
/**
|
||||
* Full object name of the commit the diff's new side shows: new-side (`R`) lines number this commit's file, and
|
||||
* old-side (`L`) lines number the file at the diff's base.
|
||||
*/
|
||||
readonly commit: string
|
||||
/** First line, inclusive. */
|
||||
readonly startLine: number
|
||||
/** Last line, inclusive. */
|
||||
readonly endLine: number
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Account that wrote a review comment. */
|
||||
interface ForgeCommentAuthor {
|
||||
/** Login as the host spells it. */
|
||||
readonly login: string
|
||||
/** Avatar image URL, or null when the host reports none. */
|
||||
readonly avatarUrl: string | null
|
||||
/** Whether the account is an app or bot rather than a person. */
|
||||
readonly bot: boolean
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** One comment of a review thread. */
|
||||
interface ForgeReviewComment {
|
||||
/** Host identity of the comment. */
|
||||
readonly id: ForgeCommentId
|
||||
/** Author, or null when the account no longer exists. */
|
||||
readonly author: ForgeCommentAuthor | null
|
||||
/** Markdown source as the author wrote it; it may contain raw HTML, which readers must not render as HTML. */
|
||||
readonly body: string
|
||||
/** ISO 8601 time the comment was created. */
|
||||
readonly createdAt: string
|
||||
/** Whether the comment belongs to the signed-in user's review that is not submitted yet, which only that user sees. */
|
||||
readonly pending: boolean
|
||||
/** Web page of the comment, or null when the host reports none. */
|
||||
readonly url: string | null
|
||||
/** Whether the signed-in user can replace the comment's text through `writeReview`; false for a provider without it. */
|
||||
readonly viewerCanEdit: boolean
|
||||
/** Whether the signed-in user can delete the comment through `writeReview`; false for a provider without it. */
|
||||
readonly viewerCanDelete: boolean
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* One review thread on a file of a change request. A line thread has two positions: `original` on the commit it was
|
||||
* written on, and `current` on the head commit of the read. `current` is null once the host no longer places the
|
||||
* thread on the head commit, as after later commits changed its lines.
|
||||
*/
|
||||
interface ForgeReviewThread {
|
||||
/** Host identity of the thread. */
|
||||
readonly id: ForgeThreadId
|
||||
/** Repository-relative path the thread is on. */
|
||||
readonly path: string
|
||||
/** Old (`L`) or new (`R`) side of the diff the lines number. */
|
||||
readonly side: 'L' | 'R'
|
||||
/** Whether the thread is on lines or on the whole file. */
|
||||
readonly subject: 'line' | 'file'
|
||||
/** Lines in the diff that ends at the head commit of the read, whose `commit` is that head; null for an outdated or file thread. */
|
||||
readonly current: ForgeAnchor | null
|
||||
/** Lines in the diff that ended at the commit the thread was written on; null for a file thread. */
|
||||
readonly original: ForgeAnchor | null
|
||||
/** Diff hunk the host recorded when the thread was written, ending at its last line; null when it recorded none. */
|
||||
readonly diffHunk: string | null
|
||||
/** Whether the thread is marked resolved. */
|
||||
readonly resolved: boolean
|
||||
/** Whether the signed-in user can reply to the thread through `writeReview`; false for a provider without it. */
|
||||
readonly viewerCanReply: boolean
|
||||
/**
|
||||
* Whether the signed-in user can mark the thread resolved, or unresolved when `resolved`, through `writeReview`; false
|
||||
* for a provider without it.
|
||||
*/
|
||||
readonly viewerCanResolve: boolean
|
||||
/** Comments oldest first; the first starts the thread. */
|
||||
readonly comments: readonly ForgeReviewComment[]
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Review threads of a change request read at one head commit. */
|
||||
interface ForgeReviewData {
|
||||
/** Head commit the threads were read at; `current` positions number its files. */
|
||||
readonly headSha: string
|
||||
/** Login of the signed-in user the provider read as, or null when the host does not name one. */
|
||||
readonly viewer: string | null
|
||||
/** Threads in host order, at most the provider's limit. */
|
||||
readonly threads: readonly ForgeReviewThread[]
|
||||
/** Whether the change request has more threads than `threads` holds. */
|
||||
readonly truncated: boolean
|
||||
/** The signed-in user's pending review, or null without one. */
|
||||
readonly pendingReview: ForgePendingReview | null
|
||||
/**
|
||||
* Whether the signed-in user can add comments to a pending review and submit or discard it through `writeReview`;
|
||||
* false for a provider without it.
|
||||
*/
|
||||
readonly viewerCanReview: boolean
|
||||
}
|
||||
```
|
||||
|
||||
`ctx.forge.writeReview(ref, write, signal)` writes to the review threads and the signed-in user's pending review: a reply, at once or with `pending` inside the pending review; resolving or unresolving a thread; editing or deleting a comment; a line comment at the head added to the pending review; and submitting or discarding the pending review. A pending reply or comment starts a pending review when the user has none. Every write carries the head commit the user saw; the provider checks it first and returns `stale` without writing once the head moved, which polls a watched change request as a stale read does, and the service polls a watched change request at once after any other result. Writes are never retried, and an `unknown` result must be checked on the host before the write is tried again. For a provider without `writeReview` the service returns `unsupported`. `ForgeReviewData` carries the pending review and the user's permissions for each thread, comment, and the pending review, which a provider without `writeReview` reports as absent, so consumers offer only the writes that can succeed. The [GitHub provider README](../../packages/experimental/forge-github/README.md#use-this-package) owns the GraphQL mutations and how their outcomes map onto these results.
|
||||
|
||||
```ts type-equiv
|
||||
/** Opaque host identity of one review. */
|
||||
type ForgeReviewId = Branded<'ForgeReviewId'>
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** The signed-in user's review of a change request that is not submitted yet, which only that user sees. */
|
||||
interface ForgePendingReview {
|
||||
/** Host identity of the review. */
|
||||
readonly id: ForgeReviewId
|
||||
/** Comments the review holds. */
|
||||
readonly comments: number
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Decision a submitted review records. */
|
||||
type ForgeReviewEvent = 'comment' | 'approve' | 'request-changes'
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* One write to the review threads and the pending review of a change request. `pending` replies and `add-pending`
|
||||
* comments join the signed-in user's pending review, which is started when the user has none. A host that adds every
|
||||
* reply to an existing pending review, as GitHub does, refuses a reply with `pending: false` while one exists.
|
||||
*/
|
||||
type ForgeReviewWrite = {
|
||||
/** Head commit the user saw; providers check it before writing and refuse with `stale` once the head is another commit. */
|
||||
readonly headSha: string
|
||||
} & (
|
||||
| { readonly kind: 'reply'; readonly thread: ForgeThreadId; readonly body: string; readonly pending: boolean }
|
||||
| { readonly kind: 'resolve'; readonly thread: ForgeThreadId; readonly resolved: boolean }
|
||||
| { readonly kind: 'edit-comment'; readonly comment: ForgeCommentId; readonly body: string }
|
||||
/** Deleting the only comment of a thread deletes the thread. */
|
||||
| { readonly kind: 'delete-comment'; readonly comment: ForgeCommentId }
|
||||
/** A comment on lines of `headSha`. */
|
||||
| { readonly kind: 'add-pending'; readonly comment: ForgeLineComment }
|
||||
| { readonly kind: 'submit-pending'; readonly review: ForgeReviewId; readonly event: ForgeReviewEvent; readonly body: string }
|
||||
/** Delete the pending review with every comment it holds. */
|
||||
| { readonly kind: 'discard-pending'; readonly review: ForgeReviewId }
|
||||
)
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Result of a review write; an `unknown` outcome must be checked on the host before the write is tried again. */
|
||||
type ForgeReviewWriteResult =
|
||||
| { readonly ok: true }
|
||||
| { readonly ok: false; readonly code: 'unsupported' | 'stale' | 'failed' | 'unknown'; readonly message: string }
|
||||
```
|
||||
|
||||
## Session bindings
|
||||
|
||||
`ctx.forgeSession` derives each Session's bindings from its own log: `gh pr create` results bind the URLs they print, `gh pr view`, `gh pr edit`, and `gh pr checkout` calls bind the change request they act on, and `git push` results trigger head-branch discovery through `ctx.forge`; the panel also reports the branch its checkout tracks for the same discovery. Track, dismiss, and discovery actions are `plugin:forge-session/binding` records, which stay outside the model-visible conversation. The `forgeChangeRequests` projection's client view, which the Session summary carries, lists the bindings without their status. The [Session binding README](../../packages/experimental/forge-session/README.md#use-this-package) owns the recognition and discovery rules.
|
||||
@@ -575,7 +804,24 @@ type ForgeChangeRequestStatesFrame =
|
||||
| { readonly type: 'changed'; readonly states: readonly ForgeChangeRequestListState[] }
|
||||
```
|
||||
|
||||
The changes view reads through the same Remote. `localDiff` reads one `ForgeLocalScope` of the Session's checkout, and `changeRequestDiff` reads a bound change request at the head of its latest status through the provider's `diff`, which returns a patch only for that head commit; both return files that the Host parsed into `ForgeDiffFile` hunks. `localText` and `changeRequestText` read the new side of one file, which supplies the unchanged lines between hunks. Each inspection also sizes the `branch` scope as `ForgePanelState.local` for the panel's local changes card, always against the automatic fork point. A `branch` scope whose `base` names a ref compares with the merge base of HEAD and that branch instead; `diffBases` lists the branches the view offers, with the automatic fork point. When the head moved past the latest status, the provider returns `stale` and `ctx.forge.diff` polls the change request at once, once per head commit; `changeRequestDiff` rejects with `forgePanel/stale-head` and caches nothing, and the changes view keeps loading until a later status arrives, then reads again at that status's head. Review comments therefore always address the commit whose lines the view shows.
|
||||
The changes view reads through the same Remote. `localDiff` reads one `ForgeLocalScope` of the Session's checkout, and `changeRequestDiff` reads a bound change request at the head of its latest status through the provider's `diff`, which returns a patch only for that head commit, or the `ForgeCommitRange` it names through the provider's `compare`; both return files that the Host parsed into `ForgeDiffFile` hunks, each with its review layer and change digest. `localCommits` and `changeRequestCommits` list the commits a range can name, the latter as null when the provider does not read commits. `localText` and `changeRequestText` read the new side of one file, which supplies the unchanged lines between hunks. Each inspection also sizes the `branch` scope as `ForgePanelState.local` for the panel's local changes card, always against the automatic fork point. A `branch` scope whose `base` names a ref compares with the merge base of HEAD and that branch instead; `diffBases` lists the branches the view offers, with the automatic fork point. When the head moved past the latest status, the provider returns `stale` and `ctx.forge.diff` polls the change request at once, once per head commit; `changeRequestDiff` rejects with `forgePanel/stale-head` and caches nothing, and the changes view keeps loading until a later status arrives, then reads again at that status's head. Review comments therefore always address the commit whose lines the view shows.
|
||||
|
||||
`changeRequestReview(session, url, headSha, fresh)` reads the threads of a bound change request at the head of the diff the view shows and refuses another head with `forgePanel/stale-head`, `forgePanel/review-unsupported` for a provider without review reads, and `forgePanel/review-failed` otherwise. It keeps one read per change request in memory, keyed by head commit and the status's update time and bounded by `maxCachedReviewBytes`, and reads again once the kept read is `reviewRefreshMs` old or when `fresh` asks for it, as the first read after the reader refreshes the view does: on GitHub a new comment or reply changes a pull request's update time, but resolving a thread does not. The changes view asks again at every status it receives while visible, and overlays the threads on the files the [panel README](../../packages/experimental/client-ui-forge/README.md#use-this-package) describes.
|
||||
|
||||
`changeRequestReviewWrite(session, url, write)` passes one `ForgeReviewWrite` for a bound change request to `ctx.forge.writeReview` after checking its head commit, ids, text, and line range, and returns the `ForgeReviewWriteResult`. Once the provider answers, whatever the answer, it drops the kept threads of that change request and every read that started before, so the next `changeRequestReview` reads the host; `submitReview` drops them as well. The changes view sends one write at a time and reads the threads again after each write that may have changed them.
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Consecutive commits a changes view shows instead of its whole comparison, named by full object names in the order
|
||||
* the commit list shows them, oldest first; the view shows the changes from the first commit's first parent to the last.
|
||||
*/
|
||||
interface ForgeCommitRange {
|
||||
/** First, oldest commit shown. */
|
||||
readonly from: string
|
||||
/** Last, newest commit shown; equal to `from` for one commit. */
|
||||
readonly to: string
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Which local changes a diff covers; each reads the Session's checkout through Git. */
|
||||
@@ -583,9 +829,10 @@ type ForgeLocalScope =
|
||||
/**
|
||||
* The working tree and untracked files against a fork point: the merge base of HEAD with the
|
||||
* `base` ref, or while `base` is null, the base commit of the worktree the Session created,
|
||||
* else the merge base of HEAD with the default branch of the code host remote.
|
||||
* else the merge base of HEAD with the default branch of the code host remote. With `commits`, only the changes
|
||||
* of those commits between the fork point and HEAD, without uncommitted or untracked files.
|
||||
*/
|
||||
| { readonly kind: 'branch'; readonly base: string | null }
|
||||
| { readonly kind: 'branch'; readonly base: string | null; readonly commits: ForgeCommitRange | null }
|
||||
/** The working tree and untracked files against HEAD. */
|
||||
| { readonly kind: 'uncommitted' }
|
||||
/** The working tree and untracked files against the index. */
|
||||
@@ -647,6 +894,15 @@ interface ForgeDiffHunk {
|
||||
type ForgeDiffStatus = 'added' | 'deleted' | 'modified' | 'renamed'
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Review layer of a changed file: the layer the repository's root `.gitattributes` names with `dsh-review-layer`, else
|
||||
* `test` for a file it marks `linguist-generated`, else the first of `testPaths`, `interfacePaths`, and the built-in
|
||||
* test and interface rules that the path matches, else `implementation`.
|
||||
*/
|
||||
type ForgeFileLayer = 'interface' | 'implementation' | 'test'
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** One changed file of a diff. */
|
||||
interface ForgeDiffFile {
|
||||
@@ -668,6 +924,13 @@ interface ForgeDiffFile {
|
||||
readonly deletions: number
|
||||
/** Hunks in file order; none for a binary file or a change of mode or name only. */
|
||||
readonly hunks: readonly ForgeDiffHunk[]
|
||||
/** Review layer the path falls in. */
|
||||
readonly layer: ForgeFileLayer
|
||||
/**
|
||||
* Digest of the file's change: its paths, status, and hunks, plus the blob names of a binary change, or the content
|
||||
* of an untracked binary file. Equal digests describe the same change; reading the same comparison again keeps it.
|
||||
*/
|
||||
readonly digest: string
|
||||
}
|
||||
```
|
||||
|
||||
@@ -684,22 +947,34 @@ interface ForgeLocalDiff {
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Changes of a change request as its code host computes them. */
|
||||
/** Changes of a change request, or of consecutive commits of it, as its code host computes them. */
|
||||
interface ForgeChangeRequestDiff {
|
||||
/** Head commit the diff describes. */
|
||||
/** Commit whose new side the diff describes: the head, or the last chosen commit. */
|
||||
readonly headSha: string
|
||||
/** Changed files in the host's order. */
|
||||
readonly files: readonly ForgeDiffFile[]
|
||||
/**
|
||||
* Commits the checkout's branch has beyond the head when that branch is the change request's head
|
||||
* branch and the head commit is present locally; null otherwise.
|
||||
* branch and the head commit is present locally; null otherwise and for chosen commits.
|
||||
*/
|
||||
readonly localAhead: number | null
|
||||
/** Web page of each changed file at the head commit, by path. */
|
||||
/** Web page of each changed file at `headSha`, by path. */
|
||||
readonly fileUrls: Readonly<Record<string, string>>
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Commits of the `branch` scope at one HEAD commit, which the changes view can show one or several of. */
|
||||
interface ForgeLocalCommits {
|
||||
/** HEAD commit the list was read at. */
|
||||
readonly headSha: string
|
||||
/** Commits oldest first, ending with HEAD's commit. */
|
||||
readonly commits: readonly ForgeCommit[]
|
||||
/** Whether the branch has more commits than `maxCommits`; the listed ones are the newest. */
|
||||
readonly truncated: boolean
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** One file the `branch` scope changes, without its hunks. */
|
||||
interface ForgeChangedFile {
|
||||
@@ -825,7 +1100,8 @@ async fetchChecks(ref: ChangeRequestRef, signal: AbortSignal): Promise<ChangeReq
|
||||
* computes them. The first `stale` failure at a head commit polls a watched
|
||||
* change request again at once, so its next status names the current head;
|
||||
* later stale reads at the same commit wait for the regular poll, which
|
||||
* bounds the reads while the host's answers disagree.
|
||||
* bounds the reads while the host's answers disagree. When the latest status of a watched change request names
|
||||
* `headSha`, its `baseSha` goes to the provider, which may then read the patch while it confirms the head.
|
||||
* @param ref - change request served by a registered provider.
|
||||
* @param headSha - head commit the patch must describe, usually the head of the latest status.
|
||||
* @param signal - cancellation of the read.
|
||||
@@ -834,6 +1110,51 @@ async fetchChecks(ref: ChangeRequestRef, signal: AbortSignal): Promise<ChangeReq
|
||||
*/
|
||||
async diff(ref: ChangeRequestRef, headSha: string, signal: AbortSignal): Promise<string | ForgeFailure>
|
||||
|
||||
/**
|
||||
* Read the review threads of a change request at one head commit. A
|
||||
* `stale` failure polls a watched change request again as {@link ForgeService.diff} does.
|
||||
* @param ref - change request served by a registered provider.
|
||||
* @param headSha - head commit the threads must be read at, usually the head of the displayed diff.
|
||||
* @param signal - cancellation of the read.
|
||||
* @returns the threads read at `headSha`, or why the host did not provide them; `unsupported` when the provider cannot read reviews.
|
||||
* @throws when no provider serves the change request's forge, or with `signal.reason` when it aborts.
|
||||
*/
|
||||
async readReview(ref: ChangeRequestRef, headSha: string, signal: AbortSignal): Promise<ForgeReviewData | ForgeFailure>
|
||||
|
||||
/**
|
||||
* Write to the review threads or the pending review of a change request while its head is still `write.headSha`, then
|
||||
* poll the change request at once when it is watched, unless the provider cannot write reviews. A `stale` result polls
|
||||
* it as a stale read does.
|
||||
* @param ref - change request served by a registered provider.
|
||||
* @param write - the write and the head commit the user saw.
|
||||
* @param signal - cancellation of the write.
|
||||
* @returns whether the host took the write; `unsupported` when the provider cannot write reviews, and `unknown` when the
|
||||
* outcome must be checked on the host before the write is tried again.
|
||||
* @throws when no provider serves the change request's forge, or with `signal.reason` when it aborts.
|
||||
*/
|
||||
async writeReview(ref: ChangeRequestRef, write: ForgeReviewWrite, signal: AbortSignal): Promise<ForgeReviewWriteResult>
|
||||
|
||||
/**
|
||||
* List the commits of a change request at its current head.
|
||||
* @param ref - change request served by a registered provider.
|
||||
* @param signal - cancellation of the read.
|
||||
* @returns the commits, why the host did not provide them, or undefined when the provider does not read commits.
|
||||
* @throws when no provider serves the change request's forge, or with `signal.reason` when it aborts.
|
||||
*/
|
||||
async commits(ref: ChangeRequestRef, signal: AbortSignal): Promise<ForgeCommitList | ForgeFailure | undefined>
|
||||
|
||||
/**
|
||||
* Read the changes between two commits of a change request's repository.
|
||||
* @param ref - change request served by a registered provider.
|
||||
* @param base - full object name of the commit compared from.
|
||||
* @param head - full object name of a commit that descends from `base`.
|
||||
* @param signal - cancellation of the read.
|
||||
* @returns one unified patch with `a/` and `b/` path prefixes, or why the host did not provide it; a provider that
|
||||
* does not read commits returns `unavailable`.
|
||||
* @throws when no provider serves the change request's forge, or with `signal.reason` when it aborts.
|
||||
*/
|
||||
async compare(ref: ChangeRequestRef, base: string, head: string, signal: AbortSignal): Promise<string | ForgeFailure>
|
||||
|
||||
/**
|
||||
* Publish one comment or a completed review, then refresh the watched status.
|
||||
* @param ref - change request served by a registered provider.
|
||||
@@ -987,21 +1308,37 @@ Serves the forge panel. Following a Session inspects the checkout of its current
|
||||
* @returns the files, the base of the `branch` scope, and the untracked files left out.
|
||||
* @throws `forgePanel/not-a-checkout` outside a checkout; `forgePanel/base-missing` when the base branch
|
||||
* chosen for the `branch` scope does not exist; `forgePanel/no-fork-point` for the `branch` scope without
|
||||
* a fork point; `forgePanel/filter-config-failed` when filter configuration cannot be read completely, before
|
||||
* a fork point; `forgePanel/commits-missing` when chosen commits are not between the fork point and HEAD in order;
|
||||
* `forgePanel/commits-not-linear` when a chosen commit's first parent is not the commit listed before it;
|
||||
* `forgePanel/filter-config-failed` when filter configuration cannot be read completely, before
|
||||
* any comparison; `forgePanel/diff-too-large` over `maxDiffBytes`; the Git error when Git fails.
|
||||
*/
|
||||
@Remote async localDiff(session: Session, scope: ForgeLocalScope, signal: AbortSignal): Promise<ForgeLocalDiff>
|
||||
|
||||
/**
|
||||
* List the commits of the `branch` scope of the Session's current checkout, which the changes view can show one or
|
||||
* several of instead of the whole scope.
|
||||
* @param session - Session whose checkout is read.
|
||||
* @param base - the base ref of the `branch` scope, or null for its automatic fork point.
|
||||
* @param signal - request cancellation.
|
||||
* @returns the newest `maxCommits` commits between the fork point and HEAD, oldest first, and HEAD's commit.
|
||||
* @throws `forgePanel/not-a-checkout` outside a checkout; `forgePanel/base-missing` when the base branch does not
|
||||
* exist; `forgePanel/no-fork-point` without a fork point; the Git error when Git fails or the listing exceeds `maxDiffBytes`.
|
||||
*/
|
||||
@Remote async localCommits(session: Session, base: string | null, signal: AbortSignal): Promise<ForgeLocalCommits>
|
||||
|
||||
/**
|
||||
* Read the new side of one changed file in a scope, which supplies the
|
||||
* unchanged lines between its hunks: the index for `staged`, else the working tree.
|
||||
* unchanged lines between its hunks: the index for `staged`, the last chosen commit for chosen commits of
|
||||
* the `branch` scope, else the working tree.
|
||||
* @param session - Session whose checkout is read.
|
||||
* @param scope - the scope the diff was read in.
|
||||
* @param path - path relative to the checkout's top-level directory, as the diff names it.
|
||||
* @param signal - request cancellation.
|
||||
* @returns the text.
|
||||
* @throws `forgePanel/not-a-checkout` outside a checkout; `forgePanel/diff-too-large` over `maxDiffBytes`;
|
||||
* an error for a path outside the checkout, binary content, or a failed read.
|
||||
* `forgePanel/commits-missing` for a chosen commit that is not a full object name; an error for a path outside
|
||||
* the checkout, binary content, or a failed read.
|
||||
*/
|
||||
@Remote async localText(session: Session, scope: ForgeLocalScope, path: string, signal: AbortSignal): Promise<string>
|
||||
|
||||
@@ -1018,20 +1355,39 @@ Serves the forge panel. Following a Session inspects the checkout of its current
|
||||
@Remote async changeRequestChecks(session: Session, url: string, headSha: string, signal: AbortSignal): Promise<ChangeRequestSnapshot>
|
||||
|
||||
/**
|
||||
* Read the changes of a change request of a Session as its code host
|
||||
* computes them, at the head commit of its latest status. A patch already
|
||||
* read at the same head commit and base branch is reused from memory, for
|
||||
* any Session, until `maxCachedDiffBytes` evicts it.
|
||||
* List the commits of a change request of a Session at its current head, which the changes view can show one or
|
||||
* several of instead of the whole change request.
|
||||
* @param session - Session the change request is bound to.
|
||||
* @param url - binding URL of the change request.
|
||||
* @param signal - request cancellation.
|
||||
* @returns the files, the head commit, each file's web page, and the commits the checkout has beyond the head.
|
||||
* @returns the commits oldest first with the head they were read at, or null when the provider does not read commits.
|
||||
* @throws `forgePanel/not-bound` unless the URL is a visible binding of the Session that a provider serves;
|
||||
* `forgePanel/stale-head` when the head moved past the latest status, which is then polled again;
|
||||
* `forgePanel/diff-too-large` when the host or its provider refuses the diff over a size limit;
|
||||
* `forgePanel/diff-failed` before its status was read and when the host does not provide the diff otherwise.
|
||||
* `forgePanel/diff-failed` when the host does not list them.
|
||||
*/
|
||||
@Remote async changeRequestDiff(session: Session, url: string, signal: AbortSignal): Promise<ForgeChangeRequestDiff>
|
||||
@Remote async changeRequestCommits(session: Session, url: string, signal: AbortSignal): Promise<ForgeCommitList | null>
|
||||
|
||||
/**
|
||||
* Read the changes of a change request of a Session as its code host
|
||||
* computes them, with each file's layer following the root `.gitattributes`
|
||||
* of the commit the diff ends at. Without `commits` the diff covers the change
|
||||
* request at the head commit of its latest status; a patch already read at the
|
||||
* same head commit and base branch is reused from memory with its attributes, for
|
||||
* any Session, until `maxCachedDiffBytes` evicts it. With `commits`, it covers those
|
||||
* commits, from the first one's first parent to the last, and is read every time.
|
||||
* @param session - Session the change request is bound to.
|
||||
* @param url - binding URL of the change request.
|
||||
* @param commits - consecutive commits of the change request, or null for all of it.
|
||||
* @param signal - request cancellation.
|
||||
* @returns the files, the commit they end at, each file's web page, and the commits the checkout has beyond the head.
|
||||
* @throws `forgePanel/not-bound` unless the URL is a visible binding of the Session that a provider serves;
|
||||
* `forgePanel/commits-missing` when chosen commits are not among the change request's commits in order;
|
||||
* `forgePanel/commits-not-linear` when a chosen commit's first parent is not the commit listed before it;
|
||||
* `forgePanel/stale-head` without `commits` when the head moved past the latest status, which is then polled again;
|
||||
* `forgePanel/diff-too-large` when the host or its provider refuses the diff over a size limit;
|
||||
* `forgePanel/diff-failed` before its status was read, for a first chosen commit without a parent, and when the host
|
||||
* does not provide the commits, the diff, or an existing `.gitattributes` otherwise.
|
||||
*/
|
||||
@Remote async changeRequestDiff( session: Session, url: string, commits: ForgeCommitRange | null, signal: AbortSignal, ): Promise<ForgeChangeRequestDiff>
|
||||
|
||||
/**
|
||||
* Read one file of a change request's repository at a commit, which
|
||||
@@ -1049,7 +1405,28 @@ Serves the forge panel. Following a Session inspects the checkout of its current
|
||||
@Remote async changeRequestText(session: Session, url: string, commit: string, path: string, signal: AbortSignal): Promise<string>
|
||||
|
||||
/**
|
||||
* Publish a line comment or completed review for a visible Session binding.
|
||||
* Read the review threads of a change request of a Session at the head commit of its displayed diff. Threads read
|
||||
* at the same head commit and update time are reused from memory, for any Session, until they are
|
||||
* `reviewRefreshMs` old, `maxCachedReviewBytes` evicts them, or a review write through this panel drops them; a read
|
||||
* again after that age, or a `fresh` read, shows threads resolved elsewhere since, which do not change a GitHub pull
|
||||
* request's update time.
|
||||
* @param session - Session the change request is bound to.
|
||||
* @param url - binding URL of the change request.
|
||||
* @param headSha - head commit of the displayed diff.
|
||||
* @param fresh - whether to read from the host even when threads at this head and update time are kept, as after the
|
||||
* reader refreshes the view; the result replaces the kept threads.
|
||||
* @param signal - request cancellation.
|
||||
* @returns the threads at `headSha`, with `current` positions numbering the files of that diff.
|
||||
* @throws `forgePanel/not-bound` unless the URL is a visible binding of the Session that a provider serves;
|
||||
* `forgePanel/stale-head` when the latest status or the host names another head, which a host answer polls again;
|
||||
* `forgePanel/review-unsupported` when the provider cannot read reviews; `forgePanel/review-failed` before the
|
||||
* status was read and when the host does not provide the threads otherwise.
|
||||
*/
|
||||
@Remote async changeRequestReview(session: Session, url: string, headSha: string, fresh: boolean, signal: AbortSignal): Promise<ForgeReviewData>
|
||||
|
||||
/**
|
||||
* Publish a line comment or completed review for a visible Session binding. The threads kept for the change request
|
||||
* are dropped once the host answers, so the next read shows the comments.
|
||||
* @param session - Session retaining the change request.
|
||||
* @param url - visible binding URL.
|
||||
* @param request - reviewed head, ranges, plain text, and explicit review decision.
|
||||
@@ -1058,6 +1435,21 @@ Serves the forge panel. Following a Session inspects the checkout of its current
|
||||
*/
|
||||
@Remote async submitReview(session: Session, url: string, request: ForgeReviewRequest, signal: AbortSignal): Promise<ForgeReviewResult>
|
||||
|
||||
/**
|
||||
* Write to the review threads or the pending review of a change request of a Session while its head is still the
|
||||
* head the user saw. The threads kept for the change request are dropped once the host answers, so the next read
|
||||
* shows the write, which a resolve does not show through a GitHub pull request's update time.
|
||||
* @param session - Session the change request is bound to.
|
||||
* @param url - binding URL of the change request.
|
||||
* @param write - the write and the head commit of the threads the user saw.
|
||||
* @param signal - request cancellation.
|
||||
* @returns whether the host took the write; `stale` once the head moved, which polls the change request again, and
|
||||
* `unknown` when the outcome must be checked on the host before the write is tried again.
|
||||
* @throws `forgePanel/not-bound` unless the URL is a visible binding of the Session that a provider serves; an error
|
||||
* for a write without text, a head that is not a commit object name, or an invalid line range.
|
||||
*/
|
||||
@Remote async changeRequestReviewWrite( session: Session, url: string, write: ForgeReviewWrite, signal: AbortSignal, ): Promise<ForgeReviewWriteResult>
|
||||
|
||||
/**
|
||||
* Mark a draft change request of a Session ready for review; the change
|
||||
* request is polled again once the host answers.
|
||||
|
||||
+420
-28
@@ -47,6 +47,18 @@ interface ForgeProvider {
|
||||
readonly forge: string
|
||||
/** Publish a comment or completed review. Omission declares that this provider cannot write reviews. */
|
||||
submitReview?(ref: ChangeRequestRef, request: ForgeReviewRequest, signal: AbortSignal): Promise<ForgeReviewResult>
|
||||
/**
|
||||
* Read the review threads of a change request at head commit `headSha`. The threads are returned only when the
|
||||
* read names `headSha` as the head; once the head is another commit, the read resolves to a `stale` failure.
|
||||
* Omission declares that this provider cannot read reviews.
|
||||
*/
|
||||
readReview?(ref: ChangeRequestRef, headSha: string, signal: AbortSignal): Promise<ForgeReviewData | ForgeFailure>
|
||||
/**
|
||||
* Write to the review threads or the pending review of a change request while its head is `write.headSha`; once the
|
||||
* head is another commit, the write resolves to `stale` without writing. A write is never retried. Omission declares
|
||||
* that this provider cannot write reviews, and its `readReview` then reports no viewer permission.
|
||||
*/
|
||||
writeReview?(ref: ChangeRequestRef, write: ForgeReviewWrite, signal: AbortSignal): Promise<ForgeReviewWriteResult>
|
||||
/** Map a Git remote URL to its repository, or undefined when this provider does not serve it. */
|
||||
repositoryFromRemote(url: string): ForgeRepository | undefined
|
||||
/** Map a change request web URL to its reference, or undefined when this provider does not serve it. */
|
||||
@@ -71,16 +83,57 @@ interface ForgeProvider {
|
||||
/**
|
||||
* Read the changes of a change request at head commit `headSha` as the host computes them, as one unified
|
||||
* patch with `a/` and `b/` path prefixes. A patch is returned only when it describes `headSha`; once the
|
||||
* head is another commit, the read resolves to a `stale` failure.
|
||||
* head is another commit, the read resolves to a `stale` failure. `base`, when given, is the `baseSha` the
|
||||
* latest status recorded with `headSha`; a provider may start reading from it before it confirms the head.
|
||||
*/
|
||||
diff(ref: ChangeRequestRef, headSha: string, signal: AbortSignal): Promise<string | ForgeFailure>
|
||||
diff(ref: ChangeRequestRef, headSha: string, signal: AbortSignal, base?: string): Promise<string | ForgeFailure>
|
||||
/** Read the text of `path` at `commit` in the change request's repository. */
|
||||
fileText(ref: ChangeRequestRef, commit: string, path: string, signal: AbortSignal): Promise<string | ForgeFailure>
|
||||
/** Build the web page of `path` at `commit` in the change request's repository. */
|
||||
fileUrl(ref: ChangeRequestRef, commit: string, path: string): string
|
||||
/**
|
||||
* List the commits of a change request at its current head. A provider implements `commits` and `compare`
|
||||
* together; omitting either declares that the change request can be read only as a whole.
|
||||
*/
|
||||
commits?(ref: ChangeRequestRef, signal: AbortSignal): Promise<ForgeCommitList | ForgeFailure>
|
||||
/**
|
||||
* Read the changes from commit `base` to commit `head` in the change request's repository, as one unified patch
|
||||
* with `a/` and `b/` path prefixes. Both are full object names; `head` descends from `base`.
|
||||
*/
|
||||
compare?(ref: ChangeRequestRef, base: string, head: string, signal: AbortSignal): Promise<string | ForgeFailure>
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** One commit of a change request. */
|
||||
interface ForgeCommit {
|
||||
/** Full commit object name. */
|
||||
readonly sha: string
|
||||
/** Full object name of the first parent, or null for a root commit. */
|
||||
readonly parent: string | null
|
||||
/** First line of the commit message. */
|
||||
readonly title: string
|
||||
/** Author name or login as the host reports it, or null when it reports none. */
|
||||
readonly author: string | null
|
||||
/** ISO 8601 time the commit was authored. */
|
||||
readonly authoredAt: string
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** The commits of a change request at one head commit. */
|
||||
interface ForgeCommitList {
|
||||
/** Head commit the list was read at. */
|
||||
readonly headSha: string
|
||||
/** Commits oldest first, ending with the head commit unless `truncated`. */
|
||||
readonly commits: readonly ForgeCommit[]
|
||||
/** Whether the change request has more commits than the provider lists; the listed ones are the oldest. */
|
||||
readonly truncated: boolean
|
||||
}
|
||||
```
|
||||
|
||||
`ctx.forge.commits` 与 `ctx.forge.compare` 委托给同时实现两者的提供方;否则 `commits` 返回 undefined,`compare` 返回 `unavailable` 失败,因此 forge 面板只在提供方能读取提交时提供提交选择器。
|
||||
|
||||
```ts type-equiv
|
||||
/** Whether a provider can reach one host with the current credentials. */
|
||||
interface ForgeReadiness {
|
||||
@@ -159,6 +212,11 @@ interface ChangeRequestSnapshot {
|
||||
readonly headSha: string
|
||||
/** Branch the change request merges into. */
|
||||
readonly baseBranch: string
|
||||
/**
|
||||
* Base commit the host recorded with `headSha`, whose merge base with the head starts the change request's diff;
|
||||
* omitted when the provider does not report it.
|
||||
*/
|
||||
readonly baseSha?: string
|
||||
/** Lines the change request adds. */
|
||||
readonly additions: number
|
||||
/** Lines the change request deletes. */
|
||||
@@ -198,9 +256,9 @@ interface ForgeFailure {
|
||||
* `not-found`: the host does not expose it; `unauthenticated`: credentials are missing or rejected;
|
||||
* `too-large`: the host or the provider refuses an answer over its size limit, so reading again fails
|
||||
* the same way; `stale`: the head is no longer the commit the read named, so reading again at the head of
|
||||
* a later status can succeed; `unavailable`: any other failure.
|
||||
* a later status can succeed; `unsupported`: the provider does not offer the read; `unavailable`: any other failure.
|
||||
*/
|
||||
readonly code: 'not-found' | 'unauthenticated' | 'too-large' | 'stale' | 'unavailable'
|
||||
readonly code: 'not-found' | 'unauthenticated' | 'too-large' | 'stale' | 'unsupported' | 'unavailable'
|
||||
/** Human-readable cause. */
|
||||
readonly message: string
|
||||
}
|
||||
@@ -257,7 +315,7 @@ interface ForgeActionFailure {
|
||||
}
|
||||
```
|
||||
|
||||
`ctx.forge.submitReview` 将明确指定的评论或完整评审交给 provider;未实现此能力的 provider 返回不支持。消费者保存待提交草稿,并在再次提交前核对结果不明的写操作。
|
||||
`ctx.forge.submitReview` 将明确指定的评论或完整评审交给 provider;未实现此能力的 provider 返回不支持。消费者在再次提交前核对结果不明的写操作;留待之后一起提交的评论通过 `ctx.forge.writeReview` 写入代码托管平台的待提交评审。
|
||||
|
||||
```ts type-equiv
|
||||
/** One inline comment addressed to the old or new side of a pull request diff. */
|
||||
@@ -277,17 +335,188 @@ type ForgeReviewRequest = {
|
||||
readonly headSha: string
|
||||
} & (
|
||||
| { readonly kind: 'comment'; readonly comment: ForgeLineComment }
|
||||
| { readonly kind: 'review'; readonly event: 'comment' | 'approve' | 'request-changes'; readonly body: string; readonly comments: readonly ForgeLineComment[] }
|
||||
| { readonly kind: 'review'; readonly event: ForgeReviewEvent; readonly body: string; readonly comments: readonly ForgeLineComment[] }
|
||||
)
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Result of a review write; unknown outcomes must be checked on the host before another submission. */
|
||||
/** Result of a review submission; unknown outcomes must be checked on the host before another submission. */
|
||||
type ForgeReviewResult =
|
||||
| { readonly ok: true; readonly url: string }
|
||||
| { readonly ok: false; readonly code: 'unsupported' | 'stale' | 'failed' | 'unknown'; readonly message: string }
|
||||
```
|
||||
|
||||
## 评审讨论
|
||||
|
||||
`ctx.forge.readReview(ref, headSha, signal)` 读取变更请求在某个 head 提交处的评审讨论。只有当读取时的 head 就是 `headSha` 时,提供方才返回 `ForgeReviewData`;否则返回 `stale`,并像 diff 读取遇到 `stale` 时一样,立即重新轮询被关注的变更请求。提供方没有实现 `readReview` 时,服务返回 `unsupported`,使用方据此不显示讨论,也不报错。行级讨论有两套位置:`original` 是写下讨论时所在提交上的行,`current` 是本次读取的 head 上的行;托管平台不再把讨论放在该提交上时,`current` 为 null。显示 head diff 的使用方把讨论放在 `current` 位置,其余讨论用 `diffHunk` 展示;用 `fileText` 读取 `original.commit` 上的文件,可以看到写下讨论时的完整文件。GitHub 各字段如何映射到这两套位置,由 [GitHub 提供方 README](../../packages/experimental/forge-github/README.zh.md#use-this-package) 说明。
|
||||
|
||||
```ts type-equiv
|
||||
/** Opaque host identity of one review thread. */
|
||||
type ForgeThreadId = Branded<'ForgeThreadId'>
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Opaque host identity of one review comment. */
|
||||
type ForgeCommentId = Branded<'ForgeCommentId'>
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Consecutive lines on one side of a diff whose new side is one commit. */
|
||||
interface ForgeAnchor {
|
||||
/**
|
||||
* Full object name of the commit the diff's new side shows: new-side (`R`) lines number this commit's file, and
|
||||
* old-side (`L`) lines number the file at the diff's base.
|
||||
*/
|
||||
readonly commit: string
|
||||
/** First line, inclusive. */
|
||||
readonly startLine: number
|
||||
/** Last line, inclusive. */
|
||||
readonly endLine: number
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Account that wrote a review comment. */
|
||||
interface ForgeCommentAuthor {
|
||||
/** Login as the host spells it. */
|
||||
readonly login: string
|
||||
/** Avatar image URL, or null when the host reports none. */
|
||||
readonly avatarUrl: string | null
|
||||
/** Whether the account is an app or bot rather than a person. */
|
||||
readonly bot: boolean
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** One comment of a review thread. */
|
||||
interface ForgeReviewComment {
|
||||
/** Host identity of the comment. */
|
||||
readonly id: ForgeCommentId
|
||||
/** Author, or null when the account no longer exists. */
|
||||
readonly author: ForgeCommentAuthor | null
|
||||
/** Markdown source as the author wrote it; it may contain raw HTML, which readers must not render as HTML. */
|
||||
readonly body: string
|
||||
/** ISO 8601 time the comment was created. */
|
||||
readonly createdAt: string
|
||||
/** Whether the comment belongs to the signed-in user's review that is not submitted yet, which only that user sees. */
|
||||
readonly pending: boolean
|
||||
/** Web page of the comment, or null when the host reports none. */
|
||||
readonly url: string | null
|
||||
/** Whether the signed-in user can replace the comment's text through `writeReview`; false for a provider without it. */
|
||||
readonly viewerCanEdit: boolean
|
||||
/** Whether the signed-in user can delete the comment through `writeReview`; false for a provider without it. */
|
||||
readonly viewerCanDelete: boolean
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* One review thread on a file of a change request. A line thread has two positions: `original` on the commit it was
|
||||
* written on, and `current` on the head commit of the read. `current` is null once the host no longer places the
|
||||
* thread on the head commit, as after later commits changed its lines.
|
||||
*/
|
||||
interface ForgeReviewThread {
|
||||
/** Host identity of the thread. */
|
||||
readonly id: ForgeThreadId
|
||||
/** Repository-relative path the thread is on. */
|
||||
readonly path: string
|
||||
/** Old (`L`) or new (`R`) side of the diff the lines number. */
|
||||
readonly side: 'L' | 'R'
|
||||
/** Whether the thread is on lines or on the whole file. */
|
||||
readonly subject: 'line' | 'file'
|
||||
/** Lines in the diff that ends at the head commit of the read, whose `commit` is that head; null for an outdated or file thread. */
|
||||
readonly current: ForgeAnchor | null
|
||||
/** Lines in the diff that ended at the commit the thread was written on; null for a file thread. */
|
||||
readonly original: ForgeAnchor | null
|
||||
/** Diff hunk the host recorded when the thread was written, ending at its last line; null when it recorded none. */
|
||||
readonly diffHunk: string | null
|
||||
/** Whether the thread is marked resolved. */
|
||||
readonly resolved: boolean
|
||||
/** Whether the signed-in user can reply to the thread through `writeReview`; false for a provider without it. */
|
||||
readonly viewerCanReply: boolean
|
||||
/**
|
||||
* Whether the signed-in user can mark the thread resolved, or unresolved when `resolved`, through `writeReview`; false
|
||||
* for a provider without it.
|
||||
*/
|
||||
readonly viewerCanResolve: boolean
|
||||
/** Comments oldest first; the first starts the thread. */
|
||||
readonly comments: readonly ForgeReviewComment[]
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Review threads of a change request read at one head commit. */
|
||||
interface ForgeReviewData {
|
||||
/** Head commit the threads were read at; `current` positions number its files. */
|
||||
readonly headSha: string
|
||||
/** Login of the signed-in user the provider read as, or null when the host does not name one. */
|
||||
readonly viewer: string | null
|
||||
/** Threads in host order, at most the provider's limit. */
|
||||
readonly threads: readonly ForgeReviewThread[]
|
||||
/** Whether the change request has more threads than `threads` holds. */
|
||||
readonly truncated: boolean
|
||||
/** The signed-in user's pending review, or null without one. */
|
||||
readonly pendingReview: ForgePendingReview | null
|
||||
/**
|
||||
* Whether the signed-in user can add comments to a pending review and submit or discard it through `writeReview`;
|
||||
* false for a provider without it.
|
||||
*/
|
||||
readonly viewerCanReview: boolean
|
||||
}
|
||||
```
|
||||
|
||||
`ctx.forge.writeReview(ref, write, signal)` 写入评审讨论和当前登录用户的待提交评审:回复讨论,立即发布,或在 `pending` 时加入待提交评审;把讨论标记为已解决或取消解决;编辑或删除评论;在 head 上添加一条行内评论到待提交评审;以及提交或丢弃待提交评审。用户还没有待提交评审时,待提交的回复或评论会先开始一个评审。每个写操作都带有用户看到的 head 提交;提供方先检查它,head 已变化时不写入,直接返回 `stale`,并像读取遇到 `stale` 时那样重新轮询被关注的变更请求;其他结果之后,服务都会立即重新轮询被关注的变更请求。写操作从不重试;结果为 `unknown` 时,必须先在托管平台上核对,再重试。提供方没有实现 `writeReview` 时,服务返回 `unsupported`。`ForgeReviewData` 带有待提交评审,以及用户对每个讨论、每条评论和待提交评审的权限;未实现 `writeReview` 的提供方把这些权限报告为没有,使用方据此只提供能够成功的写操作。GraphQL mutation 以及它们的结果如何对应到这些返回值,由 [GitHub 提供方 README](../../packages/experimental/forge-github/README.zh.md#use-this-package) 说明。
|
||||
|
||||
```ts type-equiv
|
||||
/** Opaque host identity of one review. */
|
||||
type ForgeReviewId = Branded<'ForgeReviewId'>
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** The signed-in user's review of a change request that is not submitted yet, which only that user sees. */
|
||||
interface ForgePendingReview {
|
||||
/** Host identity of the review. */
|
||||
readonly id: ForgeReviewId
|
||||
/** Comments the review holds. */
|
||||
readonly comments: number
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Decision a submitted review records. */
|
||||
type ForgeReviewEvent = 'comment' | 'approve' | 'request-changes'
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* One write to the review threads and the pending review of a change request. `pending` replies and `add-pending`
|
||||
* comments join the signed-in user's pending review, which is started when the user has none. A host that adds every
|
||||
* reply to an existing pending review, as GitHub does, refuses a reply with `pending: false` while one exists.
|
||||
*/
|
||||
type ForgeReviewWrite = {
|
||||
/** Head commit the user saw; providers check it before writing and refuse with `stale` once the head is another commit. */
|
||||
readonly headSha: string
|
||||
} & (
|
||||
| { readonly kind: 'reply'; readonly thread: ForgeThreadId; readonly body: string; readonly pending: boolean }
|
||||
| { readonly kind: 'resolve'; readonly thread: ForgeThreadId; readonly resolved: boolean }
|
||||
| { readonly kind: 'edit-comment'; readonly comment: ForgeCommentId; readonly body: string }
|
||||
/** Deleting the only comment of a thread deletes the thread. */
|
||||
| { readonly kind: 'delete-comment'; readonly comment: ForgeCommentId }
|
||||
/** A comment on lines of `headSha`. */
|
||||
| { readonly kind: 'add-pending'; readonly comment: ForgeLineComment }
|
||||
| { readonly kind: 'submit-pending'; readonly review: ForgeReviewId; readonly event: ForgeReviewEvent; readonly body: string }
|
||||
/** Delete the pending review with every comment it holds. */
|
||||
| { readonly kind: 'discard-pending'; readonly review: ForgeReviewId }
|
||||
)
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Result of a review write; an `unknown` outcome must be checked on the host before the write is tried again. */
|
||||
type ForgeReviewWriteResult =
|
||||
| { readonly ok: true }
|
||||
| { readonly ok: false; readonly code: 'unsupported' | 'stale' | 'failed' | 'unknown'; readonly message: string }
|
||||
```
|
||||
|
||||
## Session 绑定
|
||||
|
||||
`ctx.forgeSession` 从每个 Session 自己的日志派生其绑定:`gh pr create` 的结果绑定其输出的 URL,`gh pr view`、`gh pr edit` 与 `gh pr checkout` 调用绑定它们所操作的变更请求,`git push` 的结果通过 `ctx.forge` 触发按 head 分支的发现;面板也会报告其检出所跟踪的分支,触发同样的发现。track、dismiss 与发现操作都是 `plugin:forge-session/binding` 插件记录,位于模型可见对话之外。`forgeChangeRequests` 投影的客户端视图随 Session 摘要下发,列出绑定但不含其状态。识别与发现规则见 [Session 绑定 README](../../packages/experimental/forge-session/README.zh.md#use-this-package)。
|
||||
@@ -575,7 +804,24 @@ type ForgeChangeRequestStatesFrame =
|
||||
| { readonly type: 'changed'; readonly states: readonly ForgeChangeRequestListState[] }
|
||||
```
|
||||
|
||||
变更视图经同一个 Remote 读取。`localDiff` 读取 Session 检出的某个 `ForgeLocalScope`;`changeRequestDiff` 经提供方的 `diff` 读取已绑定变更请求在其最新状态 head 处的变更,提供方只返回描述该 head 提交的补丁;两者返回的文件都已在宿主上解析为 `ForgeDiffFile` 的 hunk。`localText` 与 `changeRequestText` 读取一个文件的新版本,用来补出 hunk 之间未变更的行。每次检查还会把 `branch` 范围的规模统计为 `ForgePanelState.local`,供面板的变更卡片使用,始终以自动分叉点为准。`base` 指定了引用的 `branch` 范围改为与 HEAD 和该分支的合并基准比较;`diffBases` 列出视图可选的分支以及自动分叉点。head 已越过最新状态时,提供方返回 `stale`,`ctx.forge.diff` 立即重新轮询该变更请求(每个 head 提交一次);`changeRequestDiff` 以 `forgePanel/stale-head` 拒绝且不缓存任何内容,变更视图保持加载,直到更新的状态到达后按该状态的 head 重新读取。因此评审评论始终指向视图所显示行的那个提交。
|
||||
变更视图经同一个 Remote 读取。`localDiff` 读取 Session 检出的某个 `ForgeLocalScope`;`changeRequestDiff` 经提供方的 `diff` 读取已绑定变更请求在其最新状态 head 处的变更(提供方只返回描述该 head 提交的补丁),或经提供方的 `compare` 读取它指定的 `ForgeCommitRange`;两者返回的文件都已在宿主上解析为 `ForgeDiffFile` 的 hunk,并带有分层与改动摘要。`localCommits` 与 `changeRequestCommits` 列出范围可以指定的提交,提供方不读取提交时后者返回 null。`localText` 与 `changeRequestText` 读取一个文件的新版本,用来补出 hunk 之间未变更的行。每次检查还会把 `branch` 范围的规模统计为 `ForgePanelState.local`,供面板的变更卡片使用,始终以自动分叉点为准。`base` 指定了引用的 `branch` 范围改为与 HEAD 和该分支的合并基准比较;`diffBases` 列出视图可选的分支以及自动分叉点。head 已越过最新状态时,提供方返回 `stale`,`ctx.forge.diff` 立即重新轮询该变更请求(每个 head 提交一次);`changeRequestDiff` 以 `forgePanel/stale-head` 拒绝且不缓存任何内容,变更视图保持加载,直到更新的状态到达后按该状态的 head 重新读取。因此评审评论始终指向视图所显示行的那个提交。
|
||||
|
||||
`changeRequestReview(session, url, headSha, fresh)` 在视图所显示 diff 的 head 处读取已绑定变更请求的讨论;head 不同时以 `forgePanel/stale-head` 拒绝,提供方不支持读取讨论时返回 `forgePanel/review-unsupported`,其他失败返回 `forgePanel/review-failed`。每个变更请求在内存里保留一次读取结果,以 head 提交和状态的更新时间为键、受 `maxCachedReviewBytes` 约束,保留时间达到 `reviewRefreshMs` 后,或 `fresh` 要求时(读者刷新视图后的第一次读取就是这样)重新读取:在 GitHub 上,新评论和回复会改变 PR 的更新时间,解决讨论则不会。变更视图可见时,每收到一次状态就重新请求一次,并按[面板 README](../../packages/experimental/client-ui-forge/README.zh.md#use-this-package) 所述把讨论叠加到文件上。
|
||||
|
||||
`changeRequestReviewWrite(session, url, write)` 检查已绑定变更请求的一个 `ForgeReviewWrite` 的 head 提交、id、文字和行范围后,交给 `ctx.forge.writeReview`,并返回 `ForgeReviewWriteResult`。提供方给出答复后,无论结果如何,都会丢弃该变更请求保留的讨论,以及在此之前开始的所有读取,使下一次 `changeRequestReview` 从托管平台读取;`submitReview` 同样如此。变更视图一次只发送一个写操作,每个可能改变讨论的写操作之后都会重新读取讨论。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Consecutive commits a changes view shows instead of its whole comparison, named by full object names in the order
|
||||
* the commit list shows them, oldest first; the view shows the changes from the first commit's first parent to the last.
|
||||
*/
|
||||
interface ForgeCommitRange {
|
||||
/** First, oldest commit shown. */
|
||||
readonly from: string
|
||||
/** Last, newest commit shown; equal to `from` for one commit. */
|
||||
readonly to: string
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Which local changes a diff covers; each reads the Session's checkout through Git. */
|
||||
@@ -583,9 +829,10 @@ type ForgeLocalScope =
|
||||
/**
|
||||
* The working tree and untracked files against a fork point: the merge base of HEAD with the
|
||||
* `base` ref, or while `base` is null, the base commit of the worktree the Session created,
|
||||
* else the merge base of HEAD with the default branch of the code host remote.
|
||||
* else the merge base of HEAD with the default branch of the code host remote. With `commits`, only the changes
|
||||
* of those commits between the fork point and HEAD, without uncommitted or untracked files.
|
||||
*/
|
||||
| { readonly kind: 'branch'; readonly base: string | null }
|
||||
| { readonly kind: 'branch'; readonly base: string | null; readonly commits: ForgeCommitRange | null }
|
||||
/** The working tree and untracked files against HEAD. */
|
||||
| { readonly kind: 'uncommitted' }
|
||||
/** The working tree and untracked files against the index. */
|
||||
@@ -647,6 +894,15 @@ interface ForgeDiffHunk {
|
||||
type ForgeDiffStatus = 'added' | 'deleted' | 'modified' | 'renamed'
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Review layer of a changed file: the layer the repository's root `.gitattributes` names with `dsh-review-layer`, else
|
||||
* `test` for a file it marks `linguist-generated`, else the first of `testPaths`, `interfacePaths`, and the built-in
|
||||
* test and interface rules that the path matches, else `implementation`.
|
||||
*/
|
||||
type ForgeFileLayer = 'interface' | 'implementation' | 'test'
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** One changed file of a diff. */
|
||||
interface ForgeDiffFile {
|
||||
@@ -668,6 +924,13 @@ interface ForgeDiffFile {
|
||||
readonly deletions: number
|
||||
/** Hunks in file order; none for a binary file or a change of mode or name only. */
|
||||
readonly hunks: readonly ForgeDiffHunk[]
|
||||
/** Review layer the path falls in. */
|
||||
readonly layer: ForgeFileLayer
|
||||
/**
|
||||
* Digest of the file's change: its paths, status, and hunks, plus the blob names of a binary change, or the content
|
||||
* of an untracked binary file. Equal digests describe the same change; reading the same comparison again keeps it.
|
||||
*/
|
||||
readonly digest: string
|
||||
}
|
||||
```
|
||||
|
||||
@@ -684,22 +947,34 @@ interface ForgeLocalDiff {
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Changes of a change request as its code host computes them. */
|
||||
/** Changes of a change request, or of consecutive commits of it, as its code host computes them. */
|
||||
interface ForgeChangeRequestDiff {
|
||||
/** Head commit the diff describes. */
|
||||
/** Commit whose new side the diff describes: the head, or the last chosen commit. */
|
||||
readonly headSha: string
|
||||
/** Changed files in the host's order. */
|
||||
readonly files: readonly ForgeDiffFile[]
|
||||
/**
|
||||
* Commits the checkout's branch has beyond the head when that branch is the change request's head
|
||||
* branch and the head commit is present locally; null otherwise.
|
||||
* branch and the head commit is present locally; null otherwise and for chosen commits.
|
||||
*/
|
||||
readonly localAhead: number | null
|
||||
/** Web page of each changed file at the head commit, by path. */
|
||||
/** Web page of each changed file at `headSha`, by path. */
|
||||
readonly fileUrls: Readonly<Record<string, string>>
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Commits of the `branch` scope at one HEAD commit, which the changes view can show one or several of. */
|
||||
interface ForgeLocalCommits {
|
||||
/** HEAD commit the list was read at. */
|
||||
readonly headSha: string
|
||||
/** Commits oldest first, ending with HEAD's commit. */
|
||||
readonly commits: readonly ForgeCommit[]
|
||||
/** Whether the branch has more commits than `maxCommits`; the listed ones are the newest. */
|
||||
readonly truncated: boolean
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** One file the `branch` scope changes, without its hunks. */
|
||||
interface ForgeChangedFile {
|
||||
@@ -825,7 +1100,8 @@ async fetchChecks(ref: ChangeRequestRef, signal: AbortSignal): Promise<ChangeReq
|
||||
* computes them. The first `stale` failure at a head commit polls a watched
|
||||
* change request again at once, so its next status names the current head;
|
||||
* later stale reads at the same commit wait for the regular poll, which
|
||||
* bounds the reads while the host's answers disagree.
|
||||
* bounds the reads while the host's answers disagree. When the latest status of a watched change request names
|
||||
* `headSha`, its `baseSha` goes to the provider, which may then read the patch while it confirms the head.
|
||||
* @param ref - change request served by a registered provider.
|
||||
* @param headSha - head commit the patch must describe, usually the head of the latest status.
|
||||
* @param signal - cancellation of the read.
|
||||
@@ -834,6 +1110,51 @@ async fetchChecks(ref: ChangeRequestRef, signal: AbortSignal): Promise<ChangeReq
|
||||
*/
|
||||
async diff(ref: ChangeRequestRef, headSha: string, signal: AbortSignal): Promise<string | ForgeFailure>
|
||||
|
||||
/**
|
||||
* Read the review threads of a change request at one head commit. A
|
||||
* `stale` failure polls a watched change request again as {@link ForgeService.diff} does.
|
||||
* @param ref - change request served by a registered provider.
|
||||
* @param headSha - head commit the threads must be read at, usually the head of the displayed diff.
|
||||
* @param signal - cancellation of the read.
|
||||
* @returns the threads read at `headSha`, or why the host did not provide them; `unsupported` when the provider cannot read reviews.
|
||||
* @throws when no provider serves the change request's forge, or with `signal.reason` when it aborts.
|
||||
*/
|
||||
async readReview(ref: ChangeRequestRef, headSha: string, signal: AbortSignal): Promise<ForgeReviewData | ForgeFailure>
|
||||
|
||||
/**
|
||||
* Write to the review threads or the pending review of a change request while its head is still `write.headSha`, then
|
||||
* poll the change request at once when it is watched, unless the provider cannot write reviews. A `stale` result polls
|
||||
* it as a stale read does.
|
||||
* @param ref - change request served by a registered provider.
|
||||
* @param write - the write and the head commit the user saw.
|
||||
* @param signal - cancellation of the write.
|
||||
* @returns whether the host took the write; `unsupported` when the provider cannot write reviews, and `unknown` when the
|
||||
* outcome must be checked on the host before the write is tried again.
|
||||
* @throws when no provider serves the change request's forge, or with `signal.reason` when it aborts.
|
||||
*/
|
||||
async writeReview(ref: ChangeRequestRef, write: ForgeReviewWrite, signal: AbortSignal): Promise<ForgeReviewWriteResult>
|
||||
|
||||
/**
|
||||
* List the commits of a change request at its current head.
|
||||
* @param ref - change request served by a registered provider.
|
||||
* @param signal - cancellation of the read.
|
||||
* @returns the commits, why the host did not provide them, or undefined when the provider does not read commits.
|
||||
* @throws when no provider serves the change request's forge, or with `signal.reason` when it aborts.
|
||||
*/
|
||||
async commits(ref: ChangeRequestRef, signal: AbortSignal): Promise<ForgeCommitList | ForgeFailure | undefined>
|
||||
|
||||
/**
|
||||
* Read the changes between two commits of a change request's repository.
|
||||
* @param ref - change request served by a registered provider.
|
||||
* @param base - full object name of the commit compared from.
|
||||
* @param head - full object name of a commit that descends from `base`.
|
||||
* @param signal - cancellation of the read.
|
||||
* @returns one unified patch with `a/` and `b/` path prefixes, or why the host did not provide it; a provider that
|
||||
* does not read commits returns `unavailable`.
|
||||
* @throws when no provider serves the change request's forge, or with `signal.reason` when it aborts.
|
||||
*/
|
||||
async compare(ref: ChangeRequestRef, base: string, head: string, signal: AbortSignal): Promise<string | ForgeFailure>
|
||||
|
||||
/**
|
||||
* Publish one comment or a completed review, then refresh the watched status.
|
||||
* @param ref - change request served by a registered provider.
|
||||
@@ -987,21 +1308,37 @@ Serves the forge panel. Following a Session inspects the checkout of its current
|
||||
* @returns the files, the base of the `branch` scope, and the untracked files left out.
|
||||
* @throws `forgePanel/not-a-checkout` outside a checkout; `forgePanel/base-missing` when the base branch
|
||||
* chosen for the `branch` scope does not exist; `forgePanel/no-fork-point` for the `branch` scope without
|
||||
* a fork point; `forgePanel/filter-config-failed` when filter configuration cannot be read completely, before
|
||||
* a fork point; `forgePanel/commits-missing` when chosen commits are not between the fork point and HEAD in order;
|
||||
* `forgePanel/commits-not-linear` when a chosen commit's first parent is not the commit listed before it;
|
||||
* `forgePanel/filter-config-failed` when filter configuration cannot be read completely, before
|
||||
* any comparison; `forgePanel/diff-too-large` over `maxDiffBytes`; the Git error when Git fails.
|
||||
*/
|
||||
@Remote async localDiff(session: Session, scope: ForgeLocalScope, signal: AbortSignal): Promise<ForgeLocalDiff>
|
||||
|
||||
/**
|
||||
* List the commits of the `branch` scope of the Session's current checkout, which the changes view can show one or
|
||||
* several of instead of the whole scope.
|
||||
* @param session - Session whose checkout is read.
|
||||
* @param base - the base ref of the `branch` scope, or null for its automatic fork point.
|
||||
* @param signal - request cancellation.
|
||||
* @returns the newest `maxCommits` commits between the fork point and HEAD, oldest first, and HEAD's commit.
|
||||
* @throws `forgePanel/not-a-checkout` outside a checkout; `forgePanel/base-missing` when the base branch does not
|
||||
* exist; `forgePanel/no-fork-point` without a fork point; the Git error when Git fails or the listing exceeds `maxDiffBytes`.
|
||||
*/
|
||||
@Remote async localCommits(session: Session, base: string | null, signal: AbortSignal): Promise<ForgeLocalCommits>
|
||||
|
||||
/**
|
||||
* Read the new side of one changed file in a scope, which supplies the
|
||||
* unchanged lines between its hunks: the index for `staged`, else the working tree.
|
||||
* unchanged lines between its hunks: the index for `staged`, the last chosen commit for chosen commits of
|
||||
* the `branch` scope, else the working tree.
|
||||
* @param session - Session whose checkout is read.
|
||||
* @param scope - the scope the diff was read in.
|
||||
* @param path - path relative to the checkout's top-level directory, as the diff names it.
|
||||
* @param signal - request cancellation.
|
||||
* @returns the text.
|
||||
* @throws `forgePanel/not-a-checkout` outside a checkout; `forgePanel/diff-too-large` over `maxDiffBytes`;
|
||||
* an error for a path outside the checkout, binary content, or a failed read.
|
||||
* `forgePanel/commits-missing` for a chosen commit that is not a full object name; an error for a path outside
|
||||
* the checkout, binary content, or a failed read.
|
||||
*/
|
||||
@Remote async localText(session: Session, scope: ForgeLocalScope, path: string, signal: AbortSignal): Promise<string>
|
||||
|
||||
@@ -1018,20 +1355,39 @@ Serves the forge panel. Following a Session inspects the checkout of its current
|
||||
@Remote async changeRequestChecks(session: Session, url: string, headSha: string, signal: AbortSignal): Promise<ChangeRequestSnapshot>
|
||||
|
||||
/**
|
||||
* Read the changes of a change request of a Session as its code host
|
||||
* computes them, at the head commit of its latest status. A patch already
|
||||
* read at the same head commit and base branch is reused from memory, for
|
||||
* any Session, until `maxCachedDiffBytes` evicts it.
|
||||
* List the commits of a change request of a Session at its current head, which the changes view can show one or
|
||||
* several of instead of the whole change request.
|
||||
* @param session - Session the change request is bound to.
|
||||
* @param url - binding URL of the change request.
|
||||
* @param signal - request cancellation.
|
||||
* @returns the files, the head commit, each file's web page, and the commits the checkout has beyond the head.
|
||||
* @returns the commits oldest first with the head they were read at, or null when the provider does not read commits.
|
||||
* @throws `forgePanel/not-bound` unless the URL is a visible binding of the Session that a provider serves;
|
||||
* `forgePanel/stale-head` when the head moved past the latest status, which is then polled again;
|
||||
* `forgePanel/diff-too-large` when the host or its provider refuses the diff over a size limit;
|
||||
* `forgePanel/diff-failed` before its status was read and when the host does not provide the diff otherwise.
|
||||
* `forgePanel/diff-failed` when the host does not list them.
|
||||
*/
|
||||
@Remote async changeRequestDiff(session: Session, url: string, signal: AbortSignal): Promise<ForgeChangeRequestDiff>
|
||||
@Remote async changeRequestCommits(session: Session, url: string, signal: AbortSignal): Promise<ForgeCommitList | null>
|
||||
|
||||
/**
|
||||
* Read the changes of a change request of a Session as its code host
|
||||
* computes them, with each file's layer following the root `.gitattributes`
|
||||
* of the commit the diff ends at. Without `commits` the diff covers the change
|
||||
* request at the head commit of its latest status; a patch already read at the
|
||||
* same head commit and base branch is reused from memory with its attributes, for
|
||||
* any Session, until `maxCachedDiffBytes` evicts it. With `commits`, it covers those
|
||||
* commits, from the first one's first parent to the last, and is read every time.
|
||||
* @param session - Session the change request is bound to.
|
||||
* @param url - binding URL of the change request.
|
||||
* @param commits - consecutive commits of the change request, or null for all of it.
|
||||
* @param signal - request cancellation.
|
||||
* @returns the files, the commit they end at, each file's web page, and the commits the checkout has beyond the head.
|
||||
* @throws `forgePanel/not-bound` unless the URL is a visible binding of the Session that a provider serves;
|
||||
* `forgePanel/commits-missing` when chosen commits are not among the change request's commits in order;
|
||||
* `forgePanel/commits-not-linear` when a chosen commit's first parent is not the commit listed before it;
|
||||
* `forgePanel/stale-head` without `commits` when the head moved past the latest status, which is then polled again;
|
||||
* `forgePanel/diff-too-large` when the host or its provider refuses the diff over a size limit;
|
||||
* `forgePanel/diff-failed` before its status was read, for a first chosen commit without a parent, and when the host
|
||||
* does not provide the commits, the diff, or an existing `.gitattributes` otherwise.
|
||||
*/
|
||||
@Remote async changeRequestDiff( session: Session, url: string, commits: ForgeCommitRange | null, signal: AbortSignal, ): Promise<ForgeChangeRequestDiff>
|
||||
|
||||
/**
|
||||
* Read one file of a change request's repository at a commit, which
|
||||
@@ -1049,7 +1405,28 @@ Serves the forge panel. Following a Session inspects the checkout of its current
|
||||
@Remote async changeRequestText(session: Session, url: string, commit: string, path: string, signal: AbortSignal): Promise<string>
|
||||
|
||||
/**
|
||||
* Publish a line comment or completed review for a visible Session binding.
|
||||
* Read the review threads of a change request of a Session at the head commit of its displayed diff. Threads read
|
||||
* at the same head commit and update time are reused from memory, for any Session, until they are
|
||||
* `reviewRefreshMs` old, `maxCachedReviewBytes` evicts them, or a review write through this panel drops them; a read
|
||||
* again after that age, or a `fresh` read, shows threads resolved elsewhere since, which do not change a GitHub pull
|
||||
* request's update time.
|
||||
* @param session - Session the change request is bound to.
|
||||
* @param url - binding URL of the change request.
|
||||
* @param headSha - head commit of the displayed diff.
|
||||
* @param fresh - whether to read from the host even when threads at this head and update time are kept, as after the
|
||||
* reader refreshes the view; the result replaces the kept threads.
|
||||
* @param signal - request cancellation.
|
||||
* @returns the threads at `headSha`, with `current` positions numbering the files of that diff.
|
||||
* @throws `forgePanel/not-bound` unless the URL is a visible binding of the Session that a provider serves;
|
||||
* `forgePanel/stale-head` when the latest status or the host names another head, which a host answer polls again;
|
||||
* `forgePanel/review-unsupported` when the provider cannot read reviews; `forgePanel/review-failed` before the
|
||||
* status was read and when the host does not provide the threads otherwise.
|
||||
*/
|
||||
@Remote async changeRequestReview(session: Session, url: string, headSha: string, fresh: boolean, signal: AbortSignal): Promise<ForgeReviewData>
|
||||
|
||||
/**
|
||||
* Publish a line comment or completed review for a visible Session binding. The threads kept for the change request
|
||||
* are dropped once the host answers, so the next read shows the comments.
|
||||
* @param session - Session retaining the change request.
|
||||
* @param url - visible binding URL.
|
||||
* @param request - reviewed head, ranges, plain text, and explicit review decision.
|
||||
@@ -1058,6 +1435,21 @@ Serves the forge panel. Following a Session inspects the checkout of its current
|
||||
*/
|
||||
@Remote async submitReview(session: Session, url: string, request: ForgeReviewRequest, signal: AbortSignal): Promise<ForgeReviewResult>
|
||||
|
||||
/**
|
||||
* Write to the review threads or the pending review of a change request of a Session while its head is still the
|
||||
* head the user saw. The threads kept for the change request are dropped once the host answers, so the next read
|
||||
* shows the write, which a resolve does not show through a GitHub pull request's update time.
|
||||
* @param session - Session the change request is bound to.
|
||||
* @param url - binding URL of the change request.
|
||||
* @param write - the write and the head commit of the threads the user saw.
|
||||
* @param signal - request cancellation.
|
||||
* @returns whether the host took the write; `stale` once the head moved, which polls the change request again, and
|
||||
* `unknown` when the outcome must be checked on the host before the write is tried again.
|
||||
* @throws `forgePanel/not-bound` unless the URL is a visible binding of the Session that a provider serves; an error
|
||||
* for a write without text, a head that is not a commit object name, or an invalid line range.
|
||||
*/
|
||||
@Remote async changeRequestReviewWrite( session: Session, url: string, write: ForgeReviewWrite, signal: AbortSignal, ): Promise<ForgeReviewWriteResult>
|
||||
|
||||
/**
|
||||
* Mark a draft change request of a Session ready for review; the change
|
||||
* request is polled again once the host answers.
|
||||
|
||||
@@ -15,5 +15,8 @@
|
||||
en: d260facb7ad22440
|
||||
zh: f4868aae5be01eb6
|
||||
/lsp-navigation/provider-and-service:
|
||||
en: 754b4f1a0c80e332
|
||||
zh: aaacbc912dd6337b
|
||||
en: 9977196522f2d7e5
|
||||
zh: f971ba2c60beda68
|
||||
/lsp-navigation/failures:
|
||||
en: 50959870fd65acce
|
||||
zh: 9f9e0c2b04572235
|
||||
+25
-1
@@ -162,7 +162,31 @@ interface LspService {
|
||||
}
|
||||
```
|
||||
|
||||
`LspProviderId` is the seam's branded id (`Branded<'LspProviderId'>` from [dsh-brand](../../packages/util/brand)); `LspError` extends `HarnessError` with stable codes such as `LSP_INVALID_PROVIDER`, `LSP_CONFLICT`, `LSP_UNAVAILABLE`, `LSP_DISPOSED`, `LSP_UNSUPPORTED_OPERATION`, and `LSP_MALFORMED_RESPONSE`, which callers route on instead of parsing `message`.
|
||||
`LspProviderId` is the seam's branded id (`Branded<'LspProviderId'>` from [dsh-brand](../../packages/util/brand)); `LspError` extends `HarnessError` with the stable codes listed under [Failures](#failures), which callers route on instead of parsing `message`.
|
||||
|
||||
<a id="failures"></a>
|
||||
## Failures
|
||||
|
||||
Every expected failure of an `lsp` tool call is a `HarnessError` whose `name` and `code` the tool result keeps in `error.info`. `LspError` sets `name` to `LspError`; its `code` stays open because providers and consumers add codes, so consumers tolerate codes they do not know. Invalid tool arguments fail as `ToolArgsError` `INVALID_ARGS`, and caller cancellation as `AbortError`. Workspace and source reads keep the filesystem provider's `FsError` code, such as `FS_NOT_FOUND`, `FS_NOT_REGULAR_FILE`, or `FS_NOT_TEXT`, and prefix the LSP operand to its message; the stdio provider adds `FS_NOT_DIRECTORY` for a non-directory workspace and `FS_TOO_LARGE` for a source over `maxDocumentBytes`. A coded query failure whose server teardown also fails keeps the query failure's `name` and `code` and appends the teardown failure to its message; a query failure without a code, such as a cancellation, surfaces with the teardown failure as an `AggregateError` without `error.info`.
|
||||
|
||||
| Code | Raised by | Meaning | Consumer action |
|
||||
|---|---|---|---|
|
||||
| `LSP_INVALID_PROVIDER` | Seam | A registration has an empty id or language id, no extensions, or an invalid extension | Fix the registration |
|
||||
| `LSP_CONFLICT` | Seam | The provider id or an extension is already registered | Remove the duplicate registration |
|
||||
| `LSP_UNAVAILABLE` | Seam | No provider handles the file's extension | Query a supported file or add a provider |
|
||||
| `LSP_WORKSPACE_REQUIRED` | `lsp` tool | The call has no Agent Session workspace | Call the tool from an Agent Session |
|
||||
| `LSP_DISPOSED` | Provider | The provider or its server instance was disposed | Retry after the provider is mounted again |
|
||||
| `LSP_UNSUPPORTED_OPERATION` | Provider | The server lacks the operation or transient open/close synchronization | Choose another operation or server |
|
||||
| `LSP_MALFORMED_RESPONSE` | Provider | The server's `initialize` or query result violates the protocol | Report the server defect |
|
||||
| `LSP_FILE_OUTSIDE_WORKSPACE` | Stdio provider | The source resolves outside the canonical workspace | Query a file inside the workspace |
|
||||
| `LSP_UNSUPPORTED_ENCODING` | Stdio provider | The server negotiated a position encoding other than UTF-16 | Use a server that supports UTF-16 |
|
||||
| `LSP_SPAWN_FAILED` | Stdio provider | The subprocess provider failed to launch or run the server; a provider `HarnessError` keeps its own `name` and `code` instead | Fix the server command or installation |
|
||||
| `LSP_SERVER_EXITED` | Stdio provider | The server exited or closed its stdin; after an exit, the message carries the retained stderr tail | Retry later; inspect the stderr tail when it repeats |
|
||||
| `LSP_PROTOCOL_ERROR` | Stdio provider | The server's output broke base-protocol framing or JSON | Report the server defect |
|
||||
| `LSP_MESSAGE_TOO_LARGE` | Stdio provider | A server message exceeded `maxMessageBytes` | Narrow the query or raise `maxMessageBytes` |
|
||||
| `LSP_SERVER_ERROR` | Stdio provider | The server answered with a JSON-RPC error; the message carries its numeric code and text | Act on the server's message |
|
||||
|
||||
After a transport failure (`LSP_SPAWN_FAILED`, `LSP_SERVER_EXITED`, `LSP_PROTOCOL_ERROR`, or `LSP_MESSAGE_TOO_LARGE`), the stdio provider retries the read-only query once on a fresh process when the failed process tears down cleanly.
|
||||
|
||||
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
|
||||
|
||||
|
||||
@@ -162,7 +162,31 @@ interface LspService {
|
||||
}
|
||||
```
|
||||
|
||||
`LspProviderId` 是该 seam 的品牌化 id(来自 [dsh-brand](../../packages/util/brand) 的 `Branded<'LspProviderId'>`);`LspError` 扩展 `HarnessError`,提供 `LSP_INVALID_PROVIDER`、`LSP_CONFLICT`、`LSP_UNAVAILABLE`、`LSP_DISPOSED`、`LSP_UNSUPPORTED_OPERATION` 和 `LSP_MALFORMED_RESPONSE` 等稳定错误码,调用方应按错误码路由,而不是解析 `message`。
|
||||
`LspProviderId` 是该 seam 的品牌化 id(来自 [dsh-brand](../../packages/util/brand) 的 `Branded<'LspProviderId'>`);`LspError` 扩展 `HarnessError`,提供[失败](#failures)一节列出的稳定错误码,调用方应按错误码路由,而不是解析 `message`。
|
||||
|
||||
<a id="failures"></a>
|
||||
## 失败
|
||||
|
||||
`lsp` 工具调用的每个预期失败都是 `HarnessError`,工具结果会把它的 `name` 和 `code` 保留在 `error.info` 中。`LspError` 把 `name` 设为 `LspError`;由于提供方和消费方会添加错误码,它的 `code` 保持开放,消费方须容忍未知错误码。无效的工具参数以 `ToolArgsError` `INVALID_ARGS` 失败,调用方取消以 `AbortError` 失败。工作区与源文件读取保留文件系统提供方的 `FsError` 错误码,例如 `FS_NOT_FOUND`、`FS_NOT_REGULAR_FILE` 或 `FS_NOT_TEXT`,并在其消息前加上 LSP 操作对象;stdio 提供方对非目录工作区使用 `FS_NOT_DIRECTORY`,对超过 `maxDocumentBytes` 的源文件使用 `FS_TOO_LARGE`。如果带错误码的查询失败后服务器拆除也失败,错误保留查询失败的 `name` 和 `code`,并把拆除失败追加到消息中;不带错误码的查询失败(例如取消)与拆除失败一起以 `AggregateError` 呈现,不带 `error.info`。
|
||||
|
||||
| 错误码 | 抛出方 | 含义 | 消费方操作 |
|
||||
|---|---|---|---|
|
||||
| `LSP_INVALID_PROVIDER` | Seam | 注册的 id 或语言 id 为空、没有扩展名,或扩展名无效 | 修正注册 |
|
||||
| `LSP_CONFLICT` | Seam | 提供方 id 或某个扩展名已被注册 | 移除重复注册 |
|
||||
| `LSP_UNAVAILABLE` | Seam | 没有提供方处理该文件扩展名 | 查询受支持的文件,或添加提供方 |
|
||||
| `LSP_WORKSPACE_REQUIRED` | `lsp` 工具 | 调用没有 Agent Session 工作区 | 从 Agent Session 调用该工具 |
|
||||
| `LSP_DISPOSED` | 提供方 | 提供方或其服务器实例已被释放 | 在提供方重新挂载后重试 |
|
||||
| `LSP_UNSUPPORTED_OPERATION` | 提供方 | 服务器不支持该操作或临时打开/关闭同步 | 选择其他操作或服务器 |
|
||||
| `LSP_MALFORMED_RESPONSE` | 提供方 | 服务器的 `initialize` 或查询结果违反协议 | 报告服务器缺陷 |
|
||||
| `LSP_FILE_OUTSIDE_WORKSPACE` | stdio 提供方 | 源文件解析到规范工作区之外 | 查询工作区内的文件 |
|
||||
| `LSP_UNSUPPORTED_ENCODING` | stdio 提供方 | 服务器协商了 UTF-16 以外的位置编码 | 使用支持 UTF-16 的服务器 |
|
||||
| `LSP_SPAWN_FAILED` | stdio 提供方 | 子进程提供方未能启动或运行服务器;提供方的 `HarnessError` 则保留其自身的 `name` 和 `code` | 修正服务器命令或安装 |
|
||||
| `LSP_SERVER_EXITED` | stdio 提供方 | 服务器已退出或关闭了 stdin;进程退出时消息携带保留的 stderr 尾部 | 稍后重试;反复出现时检查 stderr 尾部 |
|
||||
| `LSP_PROTOCOL_ERROR` | stdio 提供方 | 服务器输出破坏了基础协议分帧或 JSON | 报告服务器缺陷 |
|
||||
| `LSP_MESSAGE_TOO_LARGE` | stdio 提供方 | 某条服务器消息超过 `maxMessageBytes` | 缩小查询范围或调大 `maxMessageBytes` |
|
||||
| `LSP_SERVER_ERROR` | stdio 提供方 | 服务器以 JSON-RPC 错误应答;消息携带其数字错误码与文本 | 按服务器消息处理 |
|
||||
|
||||
传输失败(`LSP_SPAWN_FAILED`、`LSP_SERVER_EXITED`、`LSP_PROTOCOL_ERROR` 或 `LSP_MESSAGE_TOO_LARGE`)后,如果失败的进程干净地完成拆除,stdio 提供方会在新进程上把只读查询重试一次。
|
||||
|
||||
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
|
||||
|
||||
|
||||
@@ -86,7 +86,9 @@ type McpResourceRequest =
|
||||
/** One configured server's resource access, owned by its MCP connection plugin. */
|
||||
interface McpResourceProvider {
|
||||
/**
|
||||
* Run an operation against one live connection generation.
|
||||
* Run an operation against one live connection generation. The resource tools
|
||||
* dispatch in parallel, so one provider may receive several concurrent requests;
|
||||
* each must settle independently.
|
||||
* @param request - MCP resource method and parameters.
|
||||
* @param exec - caller identity and cancellation for this invocation.
|
||||
* @returns the protocol result as lossless JSON.
|
||||
|
||||
@@ -86,7 +86,9 @@ type McpResourceRequest =
|
||||
/** One configured server's resource access, owned by its MCP connection plugin. */
|
||||
interface McpResourceProvider {
|
||||
/**
|
||||
* Run an operation against one live connection generation.
|
||||
* Run an operation against one live connection generation. The resource tools
|
||||
* dispatch in parallel, so one provider may receive several concurrent requests;
|
||||
* each must settle independently.
|
||||
* @param request - MCP resource method and parameters.
|
||||
* @param exec - caller identity and cancellation for this invocation.
|
||||
* @returns the protocol result as lossless JSON.
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
en: 2ba8930762e92e40
|
||||
zh: 220f64da1a9e5d57
|
||||
/process-sandbox/provider-and-fail-closed-errors:
|
||||
en: a90a38fe4b11737c
|
||||
zh: 2397768e48712617
|
||||
en: a1f9649b3cde0aca
|
||||
zh: 6708348a70eb2451
|
||||
@@ -161,7 +161,7 @@ The [local provider](../../packages/sandbox/sandbox-local/README.md) owns operat
|
||||
|
||||
## Provider and fail-closed errors
|
||||
|
||||
`await ctx.sandbox.confine(argv, policy, signal)` resolves policy paths and returns a `ConfinedArgv` from the execution world, or rejects with `SandboxUnavailableError` and code `SANDBOX_UNAVAILABLE` when no usable backend exists. The optional signal cancels resolution before launch. Consumers may also classify a failure while spawning or observing the returned argv; that attribution belongs to the consumer contract. Silent unconfined passthrough is never legal for a confined policy.
|
||||
`await ctx.sandbox.confine(argv, policy, signal)` resolves policy paths and returns a `ConfinedArgv` from the execution world, or rejects with `SandboxUnavailableError` and code `SANDBOX_UNAVAILABLE` when no usable backend exists, or with `SandboxPreparationError` (`SANDBOX_PREPARATION_FAILED`, `SANDBOX_INVALID_TEMP_ROOT`) when the provider cannot prepare host state for the mode. The optional signal cancels resolution before launch. Consumers may also classify a failure while spawning or observing the returned argv; that attribution belongs to the consumer contract. Silent unconfined passthrough is never legal for a confined policy.
|
||||
|
||||
Provider selection, probing, caching, and backend-specific enforcement reports belong to the [local provider](../../packages/sandbox/sandbox-local/README.md).
|
||||
|
||||
@@ -213,6 +213,8 @@ The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment default mo
|
||||
* snapshotted together; existing operations and processes keep their policy.
|
||||
* @param request - optional session and approved mode override.
|
||||
* @returns the fully resolved per-call mode and absolute workspace root.
|
||||
* @throws `SandboxInvalidRootError` (`SANDBOX_INVALID_ROOT`) when the session's recorded cwd is
|
||||
* relative or contains a NUL byte.
|
||||
*/
|
||||
resolve(request: SandboxPolicyRequest = {}): SandboxExecutionPolicy
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@ interface ConfinedArgv {
|
||||
|
||||
## 提供方与 fail-closed 错误
|
||||
|
||||
`await ctx.sandbox.confine(argv, policy, signal)` 在执行环境中解析策略路径并返回 `ConfinedArgv`,没有可用后端时以 `SandboxUnavailableError`(错误码 `SANDBOX_UNAVAILABLE`)拒绝。可选信号可在启动前取消解析。消费方也可以在 spawn 或观察所返回的 argv 时对失败进行分类;该归因属于消费方约定。对于受限策略,静默的无隔离透传永远不合法。
|
||||
`await ctx.sandbox.confine(argv, policy, signal)` 在执行环境中解析策略路径并返回 `ConfinedArgv`,没有可用后端时以 `SandboxUnavailableError`(错误码 `SANDBOX_UNAVAILABLE`)拒绝,提供方无法为该模式准备主机状态时以 `SandboxPreparationError`(`SANDBOX_PREPARATION_FAILED`、`SANDBOX_INVALID_TEMP_ROOT`)拒绝。可选信号可在启动前取消解析。消费方也可以在 spawn 或观察所返回的 argv 时对失败进行分类;该归因属于消费方约定。对于受限策略,静默的无隔离透传永远不合法。
|
||||
|
||||
提供方选择、探测、缓存和后端特定的强制执行报告归[本地提供方](../../packages/sandbox/sandbox-local/README.zh.md)所有。
|
||||
|
||||
@@ -213,6 +213,8 @@ The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment default mo
|
||||
* snapshotted together; existing operations and processes keep their policy.
|
||||
* @param request - optional session and approved mode override.
|
||||
* @returns the fully resolved per-call mode and absolute workspace root.
|
||||
* @throws `SandboxInvalidRootError` (`SANDBOX_INVALID_ROOT`) when the session's recorded cwd is
|
||||
* relative or contains a NUL byte.
|
||||
*/
|
||||
resolve(request: SandboxPolicyRequest = {}): SandboxExecutionPolicy
|
||||
|
||||
|
||||
@@ -39,8 +39,11 @@
|
||||
en: 13820e0611b2e141
|
||||
zh: 2aafb1e5e3dc42f6
|
||||
/sessions/session-public-api:
|
||||
en: 04e9982948d9560d
|
||||
zh: 221961cadebc6fb1
|
||||
en: 2dbf1890d8c38701
|
||||
zh: 9948971a092b3243
|
||||
/sessions/session-public-api/sessioneventinput-one-proposed-event:
|
||||
en: 90c3989856a301dc
|
||||
zh: 839a494d5b2b1fa4
|
||||
/sessions/derived-history-derivemessages-and-deriveeventmessage:
|
||||
en: 45d1a9ee6d610ac5
|
||||
zh: 79c061252fde4ceb
|
||||
|
||||
@@ -447,6 +447,17 @@ interface SurfaceFoldResult {
|
||||
|
||||
## `Session` public API
|
||||
|
||||
### `SessionEventInput` — one proposed event
|
||||
|
||||
```ts type-equiv
|
||||
/** One positional event call proposed for append, without assigned sequence or time. */
|
||||
type SessionEventInput = {
|
||||
[K in SessionEventType]: readonly [type: K, data: SessionEventMap[K], ...opts: K extends SurfaceEventType ? [opts: SurfaceIntent<K>] : []]
|
||||
}[SessionEventType]
|
||||
```
|
||||
|
||||
Each array member keeps its event type paired with its payload and requires surface intent only for message-producing events.
|
||||
|
||||
The body-stripped declaration keeps the plain class's detached factory, state accessors, append method, and history projections synchronized with source. Store operations remain in the generated [`ctx.sessions` section](#ctxsessions--sessionstore).
|
||||
|
||||
```ts public-api
|
||||
@@ -573,6 +584,18 @@ declare class Session {
|
||||
isOwnSeq(seq: SessionSeq): boolean;
|
||||
/** The next event's sequence number — always the log length (the `seq = log.length` contiguity contract). */
|
||||
get seq(): SessionLogOffset;
|
||||
/** Current event while synchronously notifying a batch's observers. */
|
||||
get currentBatchPublication(): SessionEvent | undefined;
|
||||
/**
|
||||
* Append consecutive events together after validating every candidate and dispatch.
|
||||
* Observers read the complete committed log and final message surface.
|
||||
* Listener registration changes during publication take effect on the next append.
|
||||
* Persistence listeners retain their own durability rules.
|
||||
* @param inputs - event payloads and surface intents in commit order. An empty array records no events.
|
||||
* @returns immutable committed events in the same order.
|
||||
* @throws if the Session cannot accept writes, including an empty append, or any candidate or synchronous dispatch validation fails.
|
||||
*/
|
||||
append(inputs: readonly SessionEventInput[]): readonly SessionEvent[];
|
||||
/**
|
||||
* Append one typed event to the log and synchronously notify observers via
|
||||
* the store-owned, module-private publication hooks. The hot path never blocks
|
||||
@@ -1384,13 +1407,16 @@ Source: [`packages/core/session/src/index.ts`](../../packages/core/session/src/i
|
||||
|
||||
#### `session/event` — emit
|
||||
|
||||
Post-commit, fire-and-forget append feed. The listener snapshot resolves before the log push, but callbacks run after it; observer failures are logged and contained without making the committed append fail. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only events from sessions entered through that agent's context.
|
||||
Post-commit, fire-and-forget append feed. The listener snapshot resolves before the log push, but callbacks run after it; observer failures are logged and contained without making the committed append fail. Array append commits every member before publishing them in order; `session.seq` and `session.surface` already include the complete batch. `session.currentBatchPublication` identifies the member being delivered. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only events from sessions entered through that agent's context.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Post-commit, fire-and-forget append feed. The listener snapshot resolves
|
||||
* before the log push, but callbacks run after it; observer failures are
|
||||
* logged and contained without making the committed append fail.
|
||||
* Array append commits every member before publishing them in order;
|
||||
* `session.seq` and `session.surface` already include the complete batch.
|
||||
* `session.currentBatchPublication` identifies the member being delivered.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners
|
||||
* receive only events from sessions entered through that agent's context.
|
||||
* @param session - the session whose log grew.
|
||||
|
||||
@@ -449,6 +449,17 @@ interface SurfaceFoldResult {
|
||||
|
||||
## `Session` 公共 API
|
||||
|
||||
### `SessionEventInput`:一个候选事件
|
||||
|
||||
```ts type-equiv
|
||||
/** One positional event call proposed for append, without assigned sequence or time. */
|
||||
type SessionEventInput = {
|
||||
[K in SessionEventType]: readonly [type: K, data: SessionEventMap[K], ...opts: K extends SurfaceEventType ? [opts: SurfaceIntent<K>] : []]
|
||||
}[SessionEventType]
|
||||
```
|
||||
|
||||
数组中的每个成员将事件类型与对应载荷配对,且仅对产生消息的事件要求surface意图。
|
||||
|
||||
去除方法体的声明与源码中的普通类保持同步,覆盖其脱离态工厂、状态访问器、append 方法和历史投影。存储操作仍由生成的 [`ctx.sessions` 小节](#ctxsessions--sessionstore)记录。
|
||||
|
||||
```ts public-api
|
||||
@@ -575,6 +586,18 @@ declare class Session {
|
||||
isOwnSeq(seq: SessionSeq): boolean;
|
||||
/** The next event's sequence number — always the log length (the `seq = log.length` contiguity contract). */
|
||||
get seq(): SessionLogOffset;
|
||||
/** Current event while synchronously notifying a batch's observers. */
|
||||
get currentBatchPublication(): SessionEvent | undefined;
|
||||
/**
|
||||
* Append consecutive events together after validating every candidate and dispatch.
|
||||
* Observers read the complete committed log and final message surface.
|
||||
* Listener registration changes during publication take effect on the next append.
|
||||
* Persistence listeners retain their own durability rules.
|
||||
* @param inputs - event payloads and surface intents in commit order. An empty array records no events.
|
||||
* @returns immutable committed events in the same order.
|
||||
* @throws if the Session cannot accept writes, including an empty append, or any candidate or synchronous dispatch validation fails.
|
||||
*/
|
||||
append(inputs: readonly SessionEventInput[]): readonly SessionEvent[];
|
||||
/**
|
||||
* Append one typed event to the log and synchronously notify observers via
|
||||
* the store-owned, module-private publication hooks. The hot path never blocks
|
||||
@@ -1388,13 +1411,16 @@ Source: [`packages/core/session/src/index.ts`](../../packages/core/session/src/i
|
||||
|
||||
#### `session/event` — emit
|
||||
|
||||
Post-commit, fire-and-forget append feed. The listener snapshot resolves before the log push, but callbacks run after it; observer failures are logged and contained without making the committed append fail. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only events from sessions entered through that agent's context.
|
||||
Post-commit, fire-and-forget append feed. The listener snapshot resolves before the log push, but callbacks run after it; observer failures are logged and contained without making the committed append fail. Array append commits every member before publishing them in order; `session.seq` and `session.surface` already include the complete batch. `session.currentBatchPublication` identifies the member being delivered. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only events from sessions entered through that agent's context.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Post-commit, fire-and-forget append feed. The listener snapshot resolves
|
||||
* before the log push, but callbacks run after it; observer failures are
|
||||
* logged and contained without making the committed append fail.
|
||||
* Array append commits every member before publishing them in order;
|
||||
* `session.seq` and `session.surface` already include the complete batch.
|
||||
* `session.currentBatchPublication` identifies the member being delivered.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners
|
||||
* receive only events from sessions entered through that agent's context.
|
||||
* @param session - the session whose log grew.
|
||||
|
||||
@@ -31,7 +31,7 @@ A backend owns how one registered type starts and detects readiness. `TerminalSe
|
||||
interface TerminalBackend {
|
||||
/** Stable type selected by {@link TerminalSpawnRequest.type}. */
|
||||
readonly type: string
|
||||
/** Create an unpublished session or reject after cleaning partial resources; cleanup failure uses {@link TerminalBackendCleanupError}. */
|
||||
/** Create an unpublished session or reject after cleaning partial resources; cleanup failure uses `TerminalBackendCleanupError`. */
|
||||
spawn(spec: TerminalBackendSpawnSpec): Promise<TerminalBackendSession>
|
||||
}
|
||||
```
|
||||
|
||||
@@ -31,7 +31,7 @@ type TerminalSessionStatus =
|
||||
interface TerminalBackend {
|
||||
/** Stable type selected by {@link TerminalSpawnRequest.type}. */
|
||||
readonly type: string
|
||||
/** Create an unpublished session or reject after cleaning partial resources; cleanup failure uses {@link TerminalBackendCleanupError}. */
|
||||
/** Create an unpublished session or reject after cleaning partial resources; cleanup failure uses `TerminalBackendCleanupError`. */
|
||||
spawn(spec: TerminalBackendSpawnSpec): Promise<TerminalBackendSession>
|
||||
}
|
||||
```
|
||||
|
||||
@@ -42,7 +42,8 @@ get(session: Session): string
|
||||
* @param signal - cancellation for filesystem inspection.
|
||||
* @returns the existing directory; recovery is committed before fulfillment.
|
||||
* A notice failure is warned without reverting the committed state.
|
||||
* @throws when the original project is also unavailable, or when a `working-directory/access` listener rejects.
|
||||
* @throws {@link WorkingDirectoryError} `WORKING_DIRECTORY_UNAVAILABLE` when the original project is also
|
||||
* unavailable or the service is disposed, or the rejection of a `working-directory/access` listener.
|
||||
*/
|
||||
ensure(agent: Agent, signal?: AbortSignal): Promise<string>
|
||||
|
||||
@@ -53,7 +54,9 @@ ensure(agent: Agent, signal?: AbortSignal): Promise<string>
|
||||
* @param signal - cancellation before the durable change.
|
||||
* @returns the canonical absolute directory, committed before fulfillment.
|
||||
* A notice failure is warned; the next request still receives the committed directory.
|
||||
* @throws when the requested path is not an existing directory.
|
||||
* @throws {@link WorkingDirectoryError} `WORKING_DIRECTORY_INVALID` for an empty path and
|
||||
* `WORKING_DIRECTORY_NOT_FOUND` when the path is not an existing directory, or the rejection of a
|
||||
* `working-directory/access` listener.
|
||||
*/
|
||||
set(agent: Agent, path: string, signal?: AbortSignal): Promise<string>
|
||||
```
|
||||
|
||||
@@ -42,7 +42,8 @@ get(session: Session): string
|
||||
* @param signal - cancellation for filesystem inspection.
|
||||
* @returns the existing directory; recovery is committed before fulfillment.
|
||||
* A notice failure is warned without reverting the committed state.
|
||||
* @throws when the original project is also unavailable, or when a `working-directory/access` listener rejects.
|
||||
* @throws {@link WorkingDirectoryError} `WORKING_DIRECTORY_UNAVAILABLE` when the original project is also
|
||||
* unavailable or the service is disposed, or the rejection of a `working-directory/access` listener.
|
||||
*/
|
||||
ensure(agent: Agent, signal?: AbortSignal): Promise<string>
|
||||
|
||||
@@ -53,7 +54,9 @@ ensure(agent: Agent, signal?: AbortSignal): Promise<string>
|
||||
* @param signal - cancellation before the durable change.
|
||||
* @returns the canonical absolute directory, committed before fulfillment.
|
||||
* A notice failure is warned; the next request still receives the committed directory.
|
||||
* @throws when the requested path is not an existing directory.
|
||||
* @throws {@link WorkingDirectoryError} `WORKING_DIRECTORY_INVALID` for an empty path and
|
||||
* `WORKING_DIRECTORY_NOT_FOUND` when the path is not an existing directory, or the rejection of a
|
||||
* `working-directory/access` listener.
|
||||
*/
|
||||
set(agent: Agent, path: string, signal?: AbortSignal): Promise<string>
|
||||
```
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
en: bc309764ab7b8b98
|
||||
zh: 20fdfbce76706025
|
||||
/worktree-cleanup-on-archive/change:
|
||||
en: 05f27e166f2dbb40
|
||||
zh: a320ac6252fd2e38
|
||||
en: 990690039f918e72
|
||||
zh: 8e23fbabf88a777f
|
||||
/worktree-cleanup-on-archive/migration:
|
||||
en: c9b2fca77d02e07f
|
||||
zh: a4228d7f22fb7e35
|
||||
en: 05f29b760cb99ae9
|
||||
zh: 33f181ac2a582bdf
|
||||
@@ -9,13 +9,13 @@ English | [中文](guide.zh.md)
|
||||
|
||||
## Change
|
||||
|
||||
Archiving a Session previously retained its checkout. The worktree service now records each checkout it creates as a `plugin:worktree/created` record and, when the Session that created one is archived, removes it with a non-force `git worktree remove` when no unarchived Session refers to it, its creator's policy is not read-only, and Git finds no tracked changes, untracked files, hidden index flags, submodules, locks, or detached HEAD. Ignored files such as `node_modules` do not retain it. Branches and commits remain. The service's `retainOnArchive` setting, off by default, keeps every checkout instead. Checkouts created before this release have no such event and are never swept, nor are Sessions archived earlier. See the [worktree service](../../../../packages/experimental/worktree/README.md).
|
||||
Archiving a Session previously retained its checkout. The worktree service now records each checkout it creates as a `plugin:worktree/created` record and, when the Session that created one is archived, removes it with a non-force `git worktree remove` when no unarchived Session refers to it, its creator's policy is not read-only, and Git finds no tracked changes, untracked files, initialized submodules, locks, or detached HEAD. Ignored files such as `node_modules` and edits to skip-worktree or assume-unchanged files, which `git status` hides, do not retain it; Git discards them. Git refuses to remove a checkout holding a committed embedded repository, so that checkout stays and the archive notice reports a failed cleanup. Branches and commits remain. The service's `retainOnArchive` setting, off by default, keeps every checkout instead. Checkouts created before this release have no such event and are never swept, nor are Sessions archived earlier. See the [worktree service](../../../../packages/experimental/worktree/README.md).
|
||||
|
||||
The Forge bundle mounts one `worktree` service for both the panel and the `create_worktree` tool. The `forge-worktree` row and the `optional-forge-panel` group that isolated a second service are gone; the panel is the top-level `forge-panel` row.
|
||||
|
||||
## Migration
|
||||
|
||||
1. If a checkout must remain available outside DSH, keep its Session unarchived, lock it with `git worktree lock <path>` before archiving, or turn on **Keep worktrees when archiving** on the Forge bundle's worktree row of the Plugins page (`retainOnArchive`), which keeps every checkout. Ignored files are not kept.
|
||||
1. If a checkout must remain available outside DSH, keep its Session unarchived, lock it with `git worktree lock <path>` before archiving, or turn on **Keep worktrees when archiving** on the Forge bundle's worktree row of the Plugins page (`retainOnArchive`), which keeps every checkout. Ignored files are not kept, nor are edits to skip-worktree or assume-unchanged files: commit those edits, or clear the flags with `git update-index --no-skip-worktree` or `--no-assume-unchanged`, before archiving.
|
||||
2. To resume an archived Session whose checkout was removed, unarchive it and use **Restore worktree**. The retained branch is checked out at the recorded path.
|
||||
3. In a profile's `cordis.patch.yml`, move any `forge-worktree` override to the `worktree` row and any `optional-forge-panel` override to the `forge-panel` row; a `disabled: true` on `forge-panel` turns off the panel alone.
|
||||
4. Confirm the branch and commits are present with `git branch --list` and that **Restore worktree** makes the recorded checkout available. Session logs require no migration.
|
||||
@@ -9,13 +9,13 @@ description: "归档 Session 可以删除其干净的工作树并保留分支,
|
||||
|
||||
## 变更
|
||||
|
||||
归档 Session 以前会保留检出目录。工作树服务现在把它创建的每个检出目录记录为 `plugin:worktree/created` Session 事件;当创建它的 Session 被归档时,若没有未归档的 Session 引用该目录、创建者的策略不是只读,且 Git 未发现跟踪文件改动、未跟踪文件、隐藏的索引标记、子模块、锁或分离 HEAD,就用不带 force 的 `git worktree remove` 删除它。`node_modules` 之类被忽略的文件不会保留目录。分支和提交仍会保留。服务的 `retainOnArchive` 设置默认关闭,开启后改为保留所有检出目录。本版本之前创建的检出目录没有该事件,不会被扫描;更早归档的 Session 也不会。参见[工作树服务](../../../../packages/experimental/worktree/README.zh.md)。
|
||||
归档 Session 以前会保留检出目录。工作树服务现在把它创建的每个检出目录记录为 `plugin:worktree/created` Session 事件;当创建它的 Session 被归档时,若没有未归档的 Session 引用该目录、创建者的策略不是只读,且 Git 未发现跟踪文件改动、未跟踪文件、已初始化的子模块、锁或分离 HEAD,就用不带 force 的 `git worktree remove` 删除它。`node_modules` 之类被忽略的文件,以及 `git status` 不报告的 skip-worktree 或 assume-unchanged 文件改动,都不会保留目录,Git 会丢弃它们。含已提交内嵌仓库的检出目录会被 Git 拒绝删除,因此保留,归档提示显示清理失败。分支和提交仍会保留。服务的 `retainOnArchive` 设置默认关闭,开启后改为保留所有检出目录。本版本之前创建的检出目录没有该事件,不会被扫描;更早归档的 Session 也不会。参见[工作树服务](../../../../packages/experimental/worktree/README.zh.md)。
|
||||
|
||||
Forge bundle 为面板和 `create_worktree` 工具挂载同一个 `worktree` 服务。隔离第二个服务的 `forge-worktree` 行与 `optional-forge-panel` 组已移除;面板是顶层的 `forge-panel` 行。
|
||||
|
||||
## 迁移
|
||||
|
||||
1. 如果检出目录必须供 DSH 之外继续使用,请保持其 Session 未归档,在归档前用 `git worktree lock <path>` 锁定它,或在插件页 Forge 套件的 worktree 行上开启**归档时不删除工作树**(`retainOnArchive`),它会保留所有检出目录。被忽略的文件不会保留。
|
||||
1. 如果检出目录必须供 DSH 之外继续使用,请保持其 Session 未归档,在归档前用 `git worktree lock <path>` 锁定它,或在插件页 Forge 套件的 worktree 行上开启**归档时不删除工作树**(`retainOnArchive`),它会保留所有检出目录。被忽略的文件不会保留,skip-worktree 或 assume-unchanged 文件的改动也不会:归档前请提交这些改动,或用 `git update-index --no-skip-worktree` 或 `--no-assume-unchanged` 清除标记。
|
||||
2. 要继续使用已归档且检出目录已删除的 Session,请取消归档并使用**恢复工作树**。保留的分支会在记录的路径重新检出。
|
||||
3. 在 profile 的 `cordis.patch.yml` 中,把针对 `forge-worktree` 的覆盖迁到 `worktree` 行,把针对 `optional-forge-panel` 的覆盖迁到 `forge-panel` 行;对 `forge-panel` 设置 `disabled: true` 可以只关闭面板。
|
||||
4. 用 `git branch --list` 确认分支与提交仍然存在,并确认**恢复工作树**使记录的检出目录重新可用。Session 日志无需迁移。
|
||||
@@ -0,0 +1,16 @@
|
||||
# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.2-rc.1/attachment-error-codes/guide.md
|
||||
/:
|
||||
en: 8a0a1ff4ca3ad446
|
||||
zh: 4bd6cd51cbd791c2
|
||||
/attachment-failures-keep-their-codes:
|
||||
en: 4f2b2f0db0646d3b
|
||||
zh: 9467f41336a65c48
|
||||
/attachment-failures-keep-their-codes/change:
|
||||
en: 0f8c05c401f15a7d
|
||||
zh: adf5976a05802bf1
|
||||
/attachment-failures-keep-their-codes/migration:
|
||||
en: 8599ead7a17bc7eb
|
||||
zh: 1120fd02065567f5
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "AttachmentError now extends HarnessError, so tool and model-request failures record attachment codes instead of losing them or reporting UNKNOWN."
|
||||
---
|
||||
|
||||
# Attachment failures keep their codes
|
||||
|
||||
English | [中文](guide.zh.md)
|
||||
|
||||
## Change
|
||||
|
||||
`AttachmentError` from `@deepseek-ai/dsh-attachment` now extends `HarnessError` from `@deepseek-ai/dsh-error`. Its `name` (`AttachmentError`) and every `ATTACHMENT_*` and image admission code are unchanged. Two persisted results change:
|
||||
|
||||
- When a model request fails because an adapter could not read an image attachment, the failed turn's `turn/end` error previously recorded `code: 'UNKNOWN'`. It now records the `AttachmentError` code, such as `ATTACHMENT_NOT_FOUND`, `ATTACHMENT_CORRUPT`, `ATTACHMENT_READ_FAILED`, or an image admission code such as `INVALID_IMAGE` or `IMAGE_TOO_LARGE`.
|
||||
- A tool whose body lets an `AttachmentError` escape now records `error: { name: 'AttachmentError', code }` on its `tool/result`; previously the result had no `error`. `read_image` also records the `AttachmentError` code and names the file and the next step in the message.
|
||||
|
||||
`instanceof HarnessError` now matches `AttachmentError`. `isAttachmentError` is unchanged.
|
||||
|
||||
## Migration
|
||||
|
||||
1. If analytics or retry rules treat `UNKNOWN` model failures as a catch-all, add the codes of the exported `AttachmentErrorCode` type; in code, identify these failures with `isAttachmentError`. The codes do not share one prefix.
|
||||
2. If code catches `HarnessError` before `AttachmentError`, move the `AttachmentError` check first wherever the two need different handling.
|
||||
3. Earlier Session logs keep `UNKNOWN`; readers that compare old and new logs should treat both as the same failure class for attachment reads.
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "AttachmentError 现在继承 HarnessError,工具失败与模型请求失败会记录附件错误码,不再丢失或报告为 UNKNOWN。"
|
||||
---
|
||||
|
||||
# 附件失败保留其错误码
|
||||
|
||||
[English](guide.md) | 中文
|
||||
|
||||
## 变更
|
||||
|
||||
`@deepseek-ai/dsh-attachment` 的 `AttachmentError` 现在继承 `@deepseek-ai/dsh-error` 中的 `HarnessError`。其 `name`(`AttachmentError`)以及所有 `ATTACHMENT_*` 与图像准入错误码均保持不变。两类持久化结果会变化:
|
||||
|
||||
- 适配器无法读取图像附件而导致模型请求失败时,失败 turn 的 `turn/end` 错误过去记录 `code: 'UNKNOWN'`。现在记录 `AttachmentError` 的错误码,例如 `ATTACHMENT_NOT_FOUND`、`ATTACHMENT_CORRUPT`、`ATTACHMENT_READ_FAILED`,或 `INVALID_IMAGE`、`IMAGE_TOO_LARGE` 等图像准入错误码。
|
||||
- 工具主体让 `AttachmentError` 逸出时,其 `tool/result` 现在记录 `error: { name: 'AttachmentError', code }`;过去结果中没有 `error`。`read_image` 同样记录 `AttachmentError` 的错误码,并在消息中写明文件与下一步。
|
||||
|
||||
`instanceof HarnessError` 现在会匹配 `AttachmentError`。`isAttachmentError` 不变。
|
||||
|
||||
## 迁移
|
||||
|
||||
1. 如果分析或重试规则把 `UNKNOWN` 模型失败当作兜底类别,请加入导出类型 `AttachmentErrorCode` 中的错误码;代码中用 `isAttachmentError` 识别这些失败。这些错误码没有统一前缀。
|
||||
2. 如果代码先捕获 `HarnessError` 再捕获 `AttachmentError`,在两者需要不同处理的地方把 `AttachmentError` 检查移到前面。
|
||||
3. 较早的 Session 日志仍保留 `UNKNOWN`;对比新旧日志的读取方应把附件读取的这两种记录视为同一类失败。
|
||||
Loaded 100 of 710 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user