Merge remote-tracking branch 'origin/master' into xtr/ptc-inspect-output
# Conflicts: # packages/experimental/cua-macos/README.i18n.yaml # packages/experimental/cua-macos/README.md # packages/experimental/cua-macos/README.zh.md # packages/experimental/cua/src/api.ts # packages/experimental/cua/src/guidance.ts # packages/experimental/cua/src/types.ts # packages/experimental/cua/tests/api.spec.ts # snapshots/session/cua-native/system-prompt.expected.md # snapshots/session/cua-ptc/system-prompt.expected.md # snapshots/session/ptc-script-retained/system-prompt.expected.md
This commit is contained in:
commit
83e3d0ebc0
436 files changed
+12453
-1706
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/` 正是其他所有组都可以依赖的那一组。
|
||||
|
||||
|
||||
@@ -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。应用级观察刷新窗口清单,单窗口观察保留其他窗口的图片映射。窗口尺寸变化使旧图片失效,移动窗口保留局部坐标。相对坐标的端点保持在窗口内,不随截图分辨率改变。
|
||||
|
||||
|
||||
+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 请求。
|
||||
@@ -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 评论只能立即发布或添加到输入框。早期面板留下的评论草稿会一直保留,直到用户把它们加入评审、发布、删除,或关闭所在页签。
|
||||
@@ -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,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,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'
|
||||
@@ -49,8 +50,14 @@ const run = promisify(execFile)
|
||||
* `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. Every invocation is appended to
|
||||
* `calls.jsonl`.
|
||||
* 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'
|
||||
@@ -131,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) => ({
|
||||
@@ -141,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: {
|
||||
@@ -152,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: {
|
||||
@@ -171,6 +228,41 @@ if (args[0] === 'api' && args[1] === 'graphql') {
|
||||
} } } } }))
|
||||
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 } } } }))
|
||||
@@ -1021,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 })
|
||||
@@ -1052,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 })
|
||||
@@ -1080,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)
|
||||
@@ -1175,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) {
|
||||
@@ -1252,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')
|
||||
@@ -1712,7 +1819,7 @@ 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([])
|
||||
})
|
||||
|
||||
@@ -1849,6 +1956,170 @@ it.skipIf(MODE === 'record')('keeps local viewed marks across reloads, clears a
|
||||
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'
|
||||
|
||||
+28
-16
@@ -1121,6 +1121,13 @@ export interface Config {
|
||||
* `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 -->
|
||||
@@ -1156,15 +1163,13 @@ export interface Config {
|
||||
|
||||
- `inject`: `computerUse` · `tools` · `systemPrompt` · `subprocess` · `agents`
|
||||
- `refs`: [`CuaConfig`](../packages/experimental/cua/src/index.ts)
|
||||
- `source`: [`packages/experimental/cua-macos/src/index.ts:34`](../packages/experimental/cua-macos/src/index.ts)
|
||||
- `source`: [`packages/experimental/cua-macos/src/index.ts:33`](../packages/experimental/cua-macos/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Native preparation, source-build signing, operation bounds, and profile-local application grants. */
|
||||
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.
|
||||
@@ -1188,7 +1193,7 @@ export type SourceSigning = 'ad-hoc' | {
|
||||
|
||||
## `@deepseek-ai/dsh-experimental-forge`
|
||||
|
||||
- `source`: [`packages/experimental/forge/src/types.ts:273`](../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. */
|
||||
@@ -1222,9 +1227,12 @@ 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
|
||||
@@ -1561,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. */
|
||||
@@ -1580,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
|
||||
}
|
||||
```
|
||||
@@ -2391,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. */
|
||||
@@ -2990,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. */
|
||||
@@ -3414,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). */
|
||||
@@ -4135,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. */
|
||||
@@ -4165,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. */
|
||||
@@ -4400,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. */
|
||||
@@ -4512,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. */
|
||||
@@ -4994,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. */
|
||||
|
||||
+28
-16
@@ -1123,6 +1123,13 @@ export interface Config {
|
||||
* `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 -->
|
||||
@@ -1158,15 +1165,13 @@ export interface Config {
|
||||
|
||||
- `inject`: `computerUse` · `tools` · `systemPrompt` · `subprocess` · `agents`
|
||||
- `refs`: [`CuaConfig`](../packages/experimental/cua/src/index.ts)
|
||||
- `source`: [`packages/experimental/cua-macos/src/index.ts:34`](../packages/experimental/cua-macos/src/index.ts)
|
||||
- `source`: [`packages/experimental/cua-macos/src/index.ts:33`](../packages/experimental/cua-macos/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Native preparation, source-build signing, operation bounds, and profile-local application grants. */
|
||||
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.
|
||||
@@ -1190,7 +1195,7 @@ export type SourceSigning = 'ad-hoc' | {
|
||||
|
||||
## `@deepseek-ai/dsh-experimental-forge`
|
||||
|
||||
- `source`: [`packages/experimental/forge/src/types.ts:273`](../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. */
|
||||
@@ -1224,9 +1229,12 @@ 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
|
||||
@@ -1563,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. */
|
||||
@@ -1582,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
|
||||
}
|
||||
```
|
||||
@@ -2393,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. */
|
||||
@@ -2992,7 +3004,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. */
|
||||
@@ -3416,7 +3428,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). */
|
||||
@@ -4137,7 +4149,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. */
|
||||
@@ -4167,7 +4179,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. */
|
||||
@@ -4402,7 +4414,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. */
|
||||
@@ -4514,7 +4526,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. */
|
||||
@@ -4996,7 +5008,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 集成
|
||||
|
||||
|
||||
@@ -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`) | - |
|
||||
@@ -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) |
|
||||
|
||||
@@ -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`) | - |
|
||||
@@ -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) |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|---|---|---|
|
||||
|
||||
@@ -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":[]},
|
||||
|
||||
@@ -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 不设超时
|
||||
|
||||
|
||||
@@ -12,11 +12,14 @@
|
||||
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: e52a5c57abc4d30e
|
||||
zh: 509a40f421cc3d33
|
||||
en: 4020149c26661a2d
|
||||
zh: 56842b057d4f662e
|
||||
+263
-9
@@ -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,9 +83,10 @@ 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. */
|
||||
@@ -199,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. */
|
||||
@@ -238,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
|
||||
}
|
||||
@@ -297,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. */
|
||||
@@ -317,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.
|
||||
@@ -617,6 +806,10 @@ type ForgeChangeRequestStatesFrame =
|
||||
|
||||
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
|
||||
@@ -907,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.
|
||||
@@ -916,6 +1110,30 @@ 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.
|
||||
@@ -1187,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.
|
||||
@@ -1196,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.
|
||||
|
||||
+263
-9
@@ -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,9 +83,10 @@ 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. */
|
||||
@@ -199,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. */
|
||||
@@ -238,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
|
||||
}
|
||||
@@ -297,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. */
|
||||
@@ -317,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)。
|
||||
@@ -617,6 +806,10 @@ type ForgeChangeRequestStatesFrame =
|
||||
|
||||
变更视图经同一个 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
|
||||
@@ -907,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.
|
||||
@@ -916,6 +1110,30 @@ 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.
|
||||
@@ -1187,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.
|
||||
@@ -1196,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 -->
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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`;对比新旧日志的读取方应把附件读取的这两种记录视为同一类失败。
|
||||
@@ -9,8 +9,8 @@
|
||||
en: 6833a7851486f10d
|
||||
zh: 22983f326fb725ce
|
||||
/macos-hover-keeps-the-system-pointer-in-place-by-default/change:
|
||||
en: 55c3070b24b4ad39
|
||||
zh: 2555d127b0f4e4a0
|
||||
en: e6d2e56ccb50e46a
|
||||
zh: a788fc19e0771a46
|
||||
/macos-hover-keeps-the-system-pointer-in-place-by-default/migration:
|
||||
en: 1f0ab7e8741e821f
|
||||
zh: f59adc3a23ebcb43
|
||||
en: 5db40025fe40c374
|
||||
zh: a5556c0e0b000d1c
|
||||
@@ -9,10 +9,10 @@ English | [中文](guide.zh.md)
|
||||
|
||||
## Change
|
||||
|
||||
On macOS, `win.mouseMove(target)` uses an enabled element or ancestor that advertises both `AXShowAlternateUI` and `AXShowDefaultUI`. Without that route, previous versions moved the visible system cursor. The default now returns `HOVER_UNAVAILABLE` before sending input.
|
||||
On macOS, `win.mouseMove(target)` uses an enabled element or ancestor that advertises both `AXShowAlternateUI` and `AXShowDefaultUI`. Without that route, previous versions moved the visible system cursor. The default now returns `HOVER_UNAVAILABLE` before sending input. By default, explicitly requesting the system pointer asks the user for this session.
|
||||
|
||||
## Migration
|
||||
|
||||
1. For an available control, use its advertised action or click it directly. Use browser tools for web content that requires page hover.
|
||||
2. To restore visible pointer movement, set `systemPointerHover: true` in the `config` of the macOS provider entry (`id: cua`) in your profile's `cordis.patch.yml`. Preserve the entry's other settings. If you set `helperPath`, rebuild the custom helper so it accepts `systemPointerHover` in the connection handshake. Restart the profile.
|
||||
3. Call `win.mouseMove` on a foreground, unobscured control without AX alternate UI. The opt-in result reports `hoverRoute: "system-pointer"` and `systemPointerMoved: true`. Keep the default to avoid disturbing the user's cursor.
|
||||
2. If your profile sets `helperPath`, rebuild the custom helper to support the per-request `useSystemPointer` field and restart the profile.
|
||||
3. When the task requires physical movement, call `win.mouseMove(target, { useSystemPointer: true })` on an unobscured foreground target. After approval this move reports `systemPointerMoved: true`. Pass `useSystemPointer: true` again for each subsequent physical move. Closing the session or restarting DSH clears approval.
|
||||
@@ -9,10 +9,10 @@ description: "macOS 的 mouseMove 默认要求控件支持 AX 备用界面,否
|
||||
|
||||
## 变更
|
||||
|
||||
在 macOS 上,`win.mouseMove(target)` 会查找已启用且同时声明 `AXShowAlternateUI` 和 `AXShowDefaultUI` 的控件或祖先。找不到时,旧版会移动系统光标,新版默认在发送输入前返回 `HOVER_UNAVAILABLE`。
|
||||
在 macOS 上,`win.mouseMove(target)` 会查找已启用且同时声明 `AXShowAlternateUI` 和 `AXShowDefaultUI` 的控件或祖先。找不到时,旧版会移动系统光标,新版默认在发送输入前返回 `HOVER_UNAVAILABLE`。默认配置下,显式请求移动系统光标时会询问用户是否授权当前会话。
|
||||
|
||||
## 迁移
|
||||
|
||||
1. 对已经可用的控件,可执行它声明的动作或直接点击。需要网页悬停时可用浏览器工具。
|
||||
2. 要恢复可见光标移动,在 profile 的 `cordis.patch.yml` 中,给 macOS 提供方条目(`id: cua`)的 `config` 添加 `systemPointerHover: true`。保留该条目的其他设置。如果指定了 `helperPath`,先重新构建自定义 helper,使其在建立连接时能读取 `systemPointerHover`,然后重启 profile。
|
||||
3. 对前台未受遮挡且不提供 AX 备用界面的控件调用 `win.mouseMove`。启用配置时,结果为 `hoverRoute: "system-pointer"` 和 `systemPointerMoved: true`。要保留用户的光标位置,使用默认配置。
|
||||
2. 如果 profile 设置了 `helperPath`,请重新构建该 helper,使其支持按请求读取 `useSystemPointer` 字段,然后重启 profile。
|
||||
3. 任务确实需要移动鼠标时,在前台未受遮挡的目标上调用 `win.mouseMove(target, { useSystemPointer: true })`。用户同意后,本次移动返回 `systemPointerMoved: true`。之后每次移动系统鼠标仍需显式传 `useSystemPointer: true`。关闭会话或重启 DSH 后需要重新授权。
|
||||
@@ -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/forge-review-reads/guide.md
|
||||
/:
|
||||
en: 53dbdc67362a5851
|
||||
zh: 9c7b2c4dd31cf666
|
||||
/forge-failures-report-unsupported-reads:
|
||||
en: 3e5488f3008886b7
|
||||
zh: 8123e954c41d559c
|
||||
/forge-failures-report-unsupported-reads/change:
|
||||
en: 77712ff12908e838
|
||||
zh: 00714f77f3b7ae80
|
||||
/forge-failures-report-unsupported-reads/migration:
|
||||
en: 698a34a7e09be2be
|
||||
zh: a32a64dc130f496f
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "ForgeFailure gains the unsupported code, which ctx.forge.readReview returns for a provider without review reads."
|
||||
---
|
||||
|
||||
# Forge failures report unsupported reads
|
||||
|
||||
English | [中文](guide.zh.md)
|
||||
|
||||
## Change
|
||||
|
||||
`ForgeFailure['code']` gains `unsupported`. `ctx.forge.readReview(ref, headSha, signal)`, new beside the optional `ForgeProvider.readReview`, returns it when the provider of the change request does not read review threads. Other reads keep their codes: a provider without `fetchChecks` still makes `ctx.forge.fetchChecks` return `unavailable`.
|
||||
|
||||
This affects code that switches exhaustively over `ForgeFailure['code']`, such as a `Record<ForgeFailure['code'], …>` lookup or a `switch` that ends in `assertNever`. All types come from `@deepseek-ai/dsh-experimental-forge`. Providers need no change: `readReview` is optional.
|
||||
|
||||
## Migration
|
||||
|
||||
1. Handle `unsupported` wherever code switches exhaustively over `ForgeFailure['code']`; treat it as a capability the provider lacks, not as a failed read to retry.
|
||||
2. Run your type check; it succeeds when no `ForgeFailure` code is unhandled.
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "ForgeFailure 新增 unsupported 代码;提供方不支持读取评审讨论时,ctx.forge.readReview 返回它。"
|
||||
---
|
||||
|
||||
# Forge 失败代码报告不支持的读取
|
||||
|
||||
[English](guide.md) | 中文
|
||||
|
||||
## 变更
|
||||
|
||||
`ForgeFailure['code']` 新增 `unsupported`。新增的 `ctx.forge.readReview(ref, headSha, signal)` 对应可选的 `ForgeProvider.readReview`;变更请求的提供方不支持读取评审讨论时,它返回 `unsupported`。其他读取的代码不变:提供方未实现 `fetchChecks` 时,`ctx.forge.fetchChecks` 仍返回 `unavailable`。
|
||||
|
||||
受影响的是对 `ForgeFailure['code']` 做穷举的代码,例如 `Record<ForgeFailure['code'], …>` 查表,或以 `assertNever` 结尾的 `switch`。所有类型都来自 `@deepseek-ai/dsh-experimental-forge`。提供方无需修改:`readReview` 是可选方法。
|
||||
|
||||
## 迁移
|
||||
|
||||
1. 在所有对 `ForgeFailure['code']` 做穷举的地方处理 `unsupported`,把它当作提供方缺少的能力,而不是需要重试的读取失败。
|
||||
2. 运行类型检查;没有未处理的 `ForgeFailure` 代码时即通过。
|
||||
@@ -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/terminal-backend-cleanup-error/guide.md
|
||||
/:
|
||||
en: 91597636f5a71894
|
||||
zh: 02d2517952d86cfd
|
||||
/terminalbackendcleanuperror-extends-terminalerror:
|
||||
en: cb11df977825904b
|
||||
zh: 080f157e78942a9d
|
||||
/terminalbackendcleanuperror-extends-terminalerror/change:
|
||||
en: bf3bd46faf910ff0
|
||||
zh: fd47d6e58f467a1e
|
||||
/terminalbackendcleanuperror-extends-terminalerror/migration:
|
||||
en: 85fe7dd354cf8657
|
||||
zh: e39dc4ce137aeb13
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "TerminalBackendCleanupError extends TerminalError instead of AggregateError and carries the CLEANUP_FAILED code."
|
||||
---
|
||||
|
||||
# `TerminalBackendCleanupError` extends `TerminalError`
|
||||
|
||||
English | [中文](guide.zh.md)
|
||||
|
||||
## Change
|
||||
|
||||
`@deepseek-ai/dsh-terminal` exports `TerminalBackendCleanupError`, which a PTY backend throws when startup failed and cleaning its partial resources also failed. It previously extended `AggregateError`. It now extends `TerminalError`, so it is a `HarnessError` with code `CLEANUP_FAILED`, and a tool result that carries it records `error.name` `TerminalBackendCleanupError` and `error.code` `CLEANUP_FAILED`. The constructor, the `name`, `spawnError`, `cleanupError`, and the `errors` list in that order are unchanged; `errors` is typed `readonly unknown[]` instead of `any[]`. `cause` is a new `AggregateError` of the same two failures. `error instanceof AggregateError` is now `false`.
|
||||
|
||||
Backends and consumers that only construct the error, read `spawnError` or `cleanupError`, or test `instanceof TerminalBackendCleanupError` are unaffected.
|
||||
|
||||
## Migration
|
||||
|
||||
1. Replace `error instanceof AggregateError` checks meant for this error with `error instanceof TerminalBackendCleanupError`, or with `error.code === 'CLEANUP_FAILED'` on a `TerminalError`.
|
||||
2. Where TypeScript code reads members of `error.errors` without narrowing, narrow each `unknown` element before use, or read `spawnError` and `cleanupError`.
|
||||
3. Confirm with `pnpm exec tsc --noEmit` in the consuming project, and check that a startup cleanup failure still reaches the handling path.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "TerminalBackendCleanupError 改为继承 TerminalError 而非 AggregateError,并带 CLEANUP_FAILED 错误码。"
|
||||
---
|
||||
|
||||
# `TerminalBackendCleanupError` 改为继承 `TerminalError`
|
||||
|
||||
[English](guide.md) | 中文
|
||||
|
||||
## 变更
|
||||
|
||||
`@deepseek-ai/dsh-terminal` 导出的 `TerminalBackendCleanupError` 由 PTY 后端在启动失败且清理部分资源也失败时抛出。它原先继承 `AggregateError`,现在继承 `TerminalError`,因此是带 code `CLEANUP_FAILED` 的 `HarnessError`,携带它的工具结果会记录 `error.name` `TerminalBackendCleanupError` 和 `error.code` `CLEANUP_FAILED`。构造函数、`name`、`spawnError`、`cleanupError` 以及按原顺序排列的 `errors` 列表不变;`errors` 的类型由 `any[]` 改为 `readonly unknown[]`。`cause` 新增为同样两个失败组成的 `AggregateError`。`error instanceof AggregateError` 现在为 `false`。
|
||||
|
||||
只构造该错误、读取 `spawnError` 或 `cleanupError`、或检查 `instanceof TerminalBackendCleanupError` 的后端与消费方不受影响。
|
||||
|
||||
## 迁移
|
||||
|
||||
1. 把针对该错误的 `error instanceof AggregateError` 检查改为 `error instanceof TerminalBackendCleanupError`,或对 `TerminalError` 检查 `error.code === 'CLEANUP_FAILED'`。
|
||||
2. TypeScript 代码未经收窄就读取 `error.errors` 元素成员时,先收窄每个 `unknown` 元素,或改为读取 `spawnError` 与 `cleanupError`。
|
||||
3. 在消费项目中运行 `pnpm exec tsc --noEmit` 确认通过,并检查启动清理失败仍会进入原有的处理路径。
|
||||
@@ -36,8 +36,8 @@
|
||||
en: 25d9c7d6c0899d23
|
||||
zh: ad8380d14810ff1c
|
||||
/deepseek-ai-dsh-attachment/understand-the-implementation/design-decisions:
|
||||
en: 6315b2d1fb12f0ca
|
||||
zh: c3f99f4c0850fb44
|
||||
en: bc4586bb4ce74464
|
||||
zh: 5ffd0a0e7261bc24
|
||||
/deepseek-ai-dsh-attachment/understand-the-implementation/service-operations:
|
||||
en: b94d13fe133d8ce6
|
||||
zh: dd4064d20d510afc
|
||||
|
||||
@@ -67,7 +67,7 @@ This section explains the design decisions behind the seam and the service opera
|
||||
- **Immutable and retention-neutral.** Objects are immutable once published; resumed and forked sessions may share them, so reference-aware garbage collection is deferred rather than tied to any one session's deletion.
|
||||
- **Verify on read.** Reads verify the immutable object against the supplied reference without consulting a Session. Optional display names and pre-normalization dimensions pass through as caller-supplied display information; the stored bytes do not verify these fields.
|
||||
- **Role-neutral image blocks.** The `ImageBlock` content block in `dsh-llm` carries an `ImageAttachmentRef`; provider adapters resolve it into deterministic request versions at an explicit route-chosen target size and byte target, while execution filesystems may map the immutable host object to a model-readable process path.
|
||||
- **Error routing by code.** `AttachmentError` re-implements the `HarnessError` shape instead of extending it because the base lives in `dsh-llm`, which depends on this package; consumers use `isAttachmentError` and route on `code`, never on the prototype chain.
|
||||
- **Error routing by code.** `AttachmentError` extends `HarnessError` from the dependency-free `dsh-error`, so tool results keep its `name` and `code`; consumers use `isAttachmentError`, which matches the code across duplicate installations, and route on `code`.
|
||||
- **Files are verbatim, images are normalized.** `saveFile` commits an existing byte array, `saveFileStream` commits bounded chunks with backpressure and cancellation, `readFileStream` verifies and returns bounded chunks, and `fileHostPath` locates the stored object for read-on-demand projection; neither file write path applies admission limits. The image path keeps its separate normalization, limits, and request-version pipeline. The `FileBlock` content block in `dsh-llm` carries a `FileAttachmentRef`, and request assembly projects it to deterministic handle text for every route.
|
||||
|
||||
### Service operations
|
||||
|
||||
@@ -67,7 +67,7 @@ kind: "package-reference"
|
||||
- **不可变且保留策略中立。** 对象一经发布即不可变;恢复和 fork 后的会话可能共享它们,因此引用感知的垃圾回收被推迟,而不是与任何单个会话的删除绑定。
|
||||
- **读取时校验。** 读取会把不可变对象与传入引用比对,无需查询 Session。可选的显示名称和规范化前尺寸作为调用方提交的显示信息原样返回;存储字节不校验这两个字段。
|
||||
- **角色无关的图片块。** `dsh-llm` 中的 `ImageBlock` 内容块携带 `ImageAttachmentRef`;提供方适配器按路由显式选定的目标尺寸与字节目标把引用解析为确定性请求版本,执行文件系统则可以把不可变宿主对象映射为模型可读的进程路径。
|
||||
- **按错误码路由。** `AttachmentError` 重新实现 `HarnessError` 的结构而不是继承它,因为基类位于 `dsh-llm`,而后者依赖本包;消费方用 `isAttachmentError` 识别错误并按 `code` 路由,绝不依赖原型链。
|
||||
- **按错误码路由。** `AttachmentError` 继承无依赖包 `dsh-error` 中的 `HarnessError`,因此工具结果保留其 `name` 与 `code`;消费方用 `isAttachmentError` 识别错误(它按错误码匹配,跨重复安装也有效),并按 `code` 路由。
|
||||
- **文件原样,图片规范化。**`saveFile` 提交已有字节数组,`saveFileStream` 以背压和取消语义提交有界分块,`readFileStream` 校验并返回有界分块,`fileHostPath` 定位存储对象供按需读取投影;两种文件写入路径都不设准入限制。图片路径保留其独立的规范化、限额与请求版本流水线。`dsh-llm` 中的 `FileBlock` 内容块承载 `FileAttachmentRef`,请求组装会为每条路由将其投影为确定性的句柄文本。
|
||||
|
||||
### 服务操作
|
||||
|
||||
@@ -23,10 +23,12 @@
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-brand": "workspace:*",
|
||||
"@deepseek-ai/dsh-error": "workspace:*",
|
||||
"@deepseek-ai/cordis": "workspace:~"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-brand": "workspace:*",
|
||||
"@deepseek-ai/dsh-error": "workspace:*",
|
||||
"@deepseek-ai/cordis": "workspace:~"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
/** Attachment failure class. @module @deepseek-ai/dsh-attachment/error */
|
||||
|
||||
import { HarnessError } from '@deepseek-ai/dsh-error'
|
||||
|
||||
const IMAGE_ADMISSION_ERROR_CODES = [
|
||||
'TOO_MANY_IMAGES',
|
||||
'IMAGES_TOO_LARGE',
|
||||
@@ -35,17 +37,14 @@ const IMAGE_ADMISSION_ERROR_CODE_SET: ReadonlySet<string> = new Set(IMAGE_ADMISS
|
||||
const ATTACHMENT_ERROR_CODE_SET: ReadonlySet<string> = new Set(ATTACHMENT_ERROR_CODES)
|
||||
|
||||
/**
|
||||
* Stable failures suitable for host RPC error mapping.
|
||||
*
|
||||
* Deliberately re-implements the `HarnessError` shape instead of extending it:
|
||||
* the base lives in `@deepseek-ai/dsh-llm`, which itself depends on this
|
||||
* package (`ImageBlock` references `ImageAttachmentRef`), so sharing the base
|
||||
* would create a dependency cycle. Consumers route on `code`, never on the
|
||||
* prototype chain, so the shapes stay interchangeable at the wire boundary.
|
||||
* Stable failures suitable for host RPC error mapping. Extends {@link HarnessError}, so a tool
|
||||
* result that fails with it records `name` `AttachmentError` and its `code`. Consumers that may
|
||||
* receive an error from a duplicate package installation identify it with {@link isAttachmentError},
|
||||
* which matches the code instead of the prototype chain.
|
||||
*/
|
||||
export class AttachmentError extends Error {
|
||||
export class AttachmentError extends HarnessError {
|
||||
/** Stable machine-routing failure code. */
|
||||
readonly code: AttachmentErrorCode
|
||||
override readonly code: AttachmentErrorCode
|
||||
|
||||
/**
|
||||
* @param message - human-readable failure description without raw bytes or host paths.
|
||||
@@ -53,7 +52,7 @@ export class AttachmentError extends Error {
|
||||
* @param options - optional chained cause.
|
||||
*/
|
||||
constructor(message: string, code: AttachmentErrorCode, options?: ErrorOptions) {
|
||||
super(message, options)
|
||||
super(message, code, options)
|
||||
this.name = 'AttachmentError'
|
||||
this.code = code
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { HarnessError } from '@deepseek-ai/dsh-error'
|
||||
import AttachmentStore, {
|
||||
AttachmentError,
|
||||
AttachmentId,
|
||||
@@ -218,6 +219,15 @@ describe('isImageAdmissionError', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('AttachmentError', () => {
|
||||
it('is a HarnessError that keeps its literal name and attachment code', () => {
|
||||
const cause = new Error('disk full')
|
||||
const error = new AttachmentError('disk failed', 'ATTACHMENT_WRITE_FAILED', { cause })
|
||||
expect(error).toBeInstanceOf(HarnessError)
|
||||
expect(error).toMatchObject({ name: 'AttachmentError', code: 'ATTACHMENT_WRITE_FAILED', message: 'disk failed', cause })
|
||||
})
|
||||
})
|
||||
|
||||
describe('isAttachmentError', () => {
|
||||
it('recognizes attachment failures from another package installation by code', () => {
|
||||
expect(isAttachmentError(new AttachmentError('bad base64', 'INVALID_FILE_BASE64'))).toBe(true)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"references": [
|
||||
{ "path": "../../../vendor/cosmokit" },
|
||||
{ "path": "../../../vendor/cordis" },
|
||||
{ "path": "../../util/brand" }
|
||||
{ "path": "../../util/brand" },
|
||||
{ "path": "../../util/error" }
|
||||
]
|
||||
}
|
||||
@@ -24,8 +24,8 @@
|
||||
en: 65bf2834b81f87da
|
||||
zh: c864ac60a3005b99
|
||||
/deepseek-ai-dsh-app-boot/use-this-package/profiles:
|
||||
en: 18c32e4ba60713e6
|
||||
zh: 4f247fce2f4056bf
|
||||
en: 781fe487a973ee6f
|
||||
zh: e95188274f75f9ad
|
||||
/deepseek-ai-dsh-app-boot/use-this-package/previewing-the-effective-configuration:
|
||||
en: 646a0ccdd7881ad1
|
||||
zh: e3696acb1573dd7b
|
||||
|
||||
@@ -61,7 +61,7 @@ A bundle may declare `dsh.bundle.platforms` as a nonempty list of Node Host plat
|
||||
|
||||
Your machine-local preferences also live in the Harness home:
|
||||
|
||||
- **`.env`** — your ordinary environment layers: the invoking directory's file outranks the Harness-home file, and both sit below the inherited environment. Variables that decide how the process starts (`PATH`, `DSH_*`, `XDG_*` and similar) are rejected from files: export them instead. The four proxy names (`HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, `NO_PROXY`) are accepted from the Harness-home file only, never from the invoking directory's, which arrives with a clone. For a non-product bin that just wants one directory's `.env`, a missing file is fine and an unloadable one prints one labelled warning line.
|
||||
- **`.env`** — your ordinary environment layers: the invoking directory's file outranks the Harness-home file, and both sit below the inherited environment. Variables that decide how the process starts (`PATH`, `DSH_*`, `XDG_*` and similar) are rejected from files: export them instead. The four proxy names (`HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, `NO_PROXY`) in any letter case and the DeepSeek endpoint names (`DEEPSEEK_BASE_URL`, `DEEPSEEK_SEARCH_BASE_URL`) in exactly that spelling are accepted from the Harness-home file only, never from the invoking directory's, which arrives with a clone. For a non-product bin that just wants one directory's `.env`, a missing file is fine and an unloadable one prints one labelled warning line.
|
||||
- **`cordis.patch.yml`** — your tweak layer, applied after every bundle layer (per-profile first, then the home-level file, which therefore outranks it): replace one entry's whole config (restating the fields you keep), insert new entries, or interpolate `!!js` expressions at boot. A patch naming an entry that does not exist prints a stderr warning; an empty or comments-only file fails boot — disable the layer with `[]` instead.
|
||||
|
||||
The enabled `dsh-hmr` plugin watches the profile manifest and both user patch files, re-reads the ordered bundle layers, and applies the [reload failure policy](#startup-and-reload-failures). [DSH HMR](../hmr/README.md) serializes these reloads with [Plugin Manager](../plugin-manager/README.md) configuration writes; package operations run outside its queue. The launcher does not install HMR or watchers; disabled or absent HMR means changes require restart.
|
||||
|
||||
@@ -61,7 +61,7 @@ profile 导入插件前,DSH 会将其声明的 DSH 版本要求与 `getDshRunt
|
||||
|
||||
你的机器本地偏好同样位于 harness home 中:
|
||||
|
||||
- **`.env`**——你的普通环境层:调用目录的文件优先于 harness home 的文件,两者都低于继承环境。在文件中设置的进程启动变量(如 `PATH`、`DSH_*`、`XDG_*`)会被拒绝:请改为导出这些变量。四个代理名(`HTTP_PROXY`、`HTTPS_PROXY`、`ALL_PROXY`、`NO_PROXY`)只从 harness home 的文件接受,绝不从调用目录的文件接受——后者随 clone 一起到来。对于只想加载某个目录 `.env` 的非产品 bin,文件缺失不影响启动,文件无法加载时输出一行带标签的警告。
|
||||
- **`.env`**——你的普通环境层:调用目录的文件优先于 harness home 的文件,两者都低于继承环境。在文件中设置的进程启动变量(如 `PATH`、`DSH_*`、`XDG_*`)会被拒绝:请改为导出这些变量。四个代理名(`HTTP_PROXY`、`HTTPS_PROXY`、`ALL_PROXY`、`NO_PROXY`,大小写不限)和 DeepSeek 端点变量名(`DEEPSEEK_BASE_URL`、`DEEPSEEK_SEARCH_BASE_URL`,须按此拼写)只从 harness home 的文件接受,绝不从调用目录的文件接受——后者随 clone 一起到来。对于只想加载某个目录 `.env` 的非产品 bin,文件缺失不影响启动,文件无法加载时输出一行带标签的警告。
|
||||
- **`cordis.patch.yml`**——你的 tweak 层,应用在所有组合包层之后(先应用逐 profile 的文件,再应用 home 级文件,因此后者优先级更高):替换某个条目的整个配置(重述你要保留的字段)、插入新条目,或在启动时插值 `!!js` 表达式。patch 指定的条目不存在时输出 stderr 警告;空文件或仅含注释的文件会导致启动失败——如需禁用该层,请改用 `[]`。
|
||||
|
||||
启用的 `dsh-hmr` 插件会监视 profile manifest 与两份用户 patch 文件,重新读取按顺序排列的组合包层,并应用[重载失败策略](#startup-and-reload-failures)。[DSH HMR](../hmr/README.zh.md) 将这些重载与[插件管理器](../plugin-manager/README.zh.md)的配置写入串行化;包操作在其队列之外执行。启动器不安装 HMR 或监视器;HMR 被禁用或不存在时,更改需要重启。
|
||||
|
||||
@@ -157,19 +157,34 @@ const BOOTSTRAP_NAMES = new Set([
|
||||
/** Name prefixes no discovered file may set. */
|
||||
const BOOTSTRAP_PREFIXES = ['DSH_', 'XDG_', 'DYLD_', 'BASH_FUNC_']
|
||||
|
||||
/**
|
||||
* The bootstrap names the Harness-home `.env` alone may set. A proxy chooses the route every
|
||||
* request takes, so the invoking directory's file — which arrives with a clone — keeps refusing
|
||||
* them; the home file is the user's own, and `DSH_HOME` is itself bootstrap-only, so no `.env` can
|
||||
* relocate this exemption. The CA and TLS names in the same group stay refused everywhere: they
|
||||
* change what is trusted, not where traffic goes.
|
||||
*/
|
||||
/** Proxy names the Harness-home `.env` may set in any letter case: proxy clients read both spellings. */
|
||||
const HOME_LAYER_PROXY_NAMES = new Set(['HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY'])
|
||||
|
||||
/**
|
||||
* DeepSeek endpoint names the Harness-home `.env` may set only as spelled here: their consumers look them up by
|
||||
* exact name on POSIX, so another spelling would load without effect.
|
||||
*/
|
||||
const HOME_LAYER_ENDPOINT_NAMES = new Set(['DEEPSEEK_BASE_URL', 'DEEPSEEK_SEARCH_BASE_URL'])
|
||||
|
||||
/**
|
||||
* Whether the Harness-home `.env` alone may set a bootstrap-only name. Each such name chooses where requests go: a
|
||||
* proxy for every request, a DeepSeek endpoint for the model or search requests that carry the user's credential.
|
||||
* The invoking directory's file arrives with a clone and keeps refusing them. The home file is the user's own, and
|
||||
* the profile patches beside it can already set these endpoints as plugin `baseURL` fields; `DSH_HOME` is itself
|
||||
* bootstrap-only, so no `.env` can relocate this exemption. The CA and TLS names in the same group stay refused
|
||||
* everywhere: they change what is trusted, not where traffic goes.
|
||||
* @param name - the variable name as the file spells it.
|
||||
* @returns true for a {@link HOME_LAYER_PROXY_NAMES} entry in any letter case or a
|
||||
* {@link HOME_LAYER_ENDPOINT_NAMES} entry in its exact spelling.
|
||||
*/
|
||||
function isHomeLayerRouteName(name: string): boolean {
|
||||
return HOME_LAYER_PROXY_NAMES.has(name.toUpperCase()) || HOME_LAYER_ENDPOINT_NAMES.has(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a variable may come only from the inherited process environment
|
||||
* because it changes process, runtime, VCS, or network bootstrap. The Harness-home
|
||||
* file is additionally allowed {@link HOME_LAYER_PROXY_NAMES}.
|
||||
* file is additionally allowed the names {@link isHomeLayerRouteName} accepts.
|
||||
* @param name - the variable name.
|
||||
* @returns true when only the inherited environment may supply it.
|
||||
*/
|
||||
@@ -184,7 +199,7 @@ function isBootstrapOnly(name: string): boolean {
|
||||
* @param binName - the diagnostic prefix on the thrown error.
|
||||
* @param dir - the directory whose `.env` to read.
|
||||
* @param warn - sink for the one-line unreadable-file diagnostic.
|
||||
* @param home - the resolved Harness home; when `dir` is it, {@link HOME_LAYER_PROXY_NAMES} are accepted.
|
||||
* @param home - the resolved Harness home; when `dir` is it, names {@link isHomeLayerRouteName} accepts are allowed.
|
||||
* @returns the parsed entries, or `undefined` when the file is absent or unreadable.
|
||||
* @throws when the file declares a name {@link isBootstrapOnly} rejects and this layer may not set.
|
||||
*/
|
||||
@@ -207,12 +222,17 @@ function readEnvLayer(
|
||||
const values = parseEnv(content) as Record<string, string>
|
||||
for (const name of Object.keys(values)) {
|
||||
if (!isBootstrapOnly(name)) continue
|
||||
const proxyName = HOME_LAYER_PROXY_NAMES.has(name.toUpperCase())
|
||||
if (isHome && proxyName) continue
|
||||
// A proxy name has a second way out that the other bootstrap names do not, so its message says so.
|
||||
const remedy = proxyName
|
||||
? `export ${name}, or put it in ${resolve(home, '.env')}, which does not travel with a repository`
|
||||
: `export ${name} instead of putting it in a .env file`
|
||||
const homeLayerName = isHomeLayerRouteName(name)
|
||||
if (isHome && homeLayerName) continue
|
||||
// A home-layer name, or an endpoint name spelled another way, has a second way out that the other bootstrap names
|
||||
// do not, so its message says so.
|
||||
const homeEnv = resolve(home, '.env')
|
||||
const upper = name.toUpperCase()
|
||||
const remedy = homeLayerName
|
||||
? `export ${name}, or put it in ${homeEnv}, which does not travel with a repository`
|
||||
: HOME_LAYER_ENDPOINT_NAMES.has(upper)
|
||||
? `spell it ${upper}, then export it or put it in ${homeEnv}, which does not travel with a repository`
|
||||
: `export ${name} instead of putting it in a .env file`
|
||||
throw new Error(
|
||||
`${binName}: ${path} sets "${name}", which only the launching environment may set`
|
||||
+ ' (it decides how this process starts, where its code and instructions load from, or how it'
|
||||
@@ -231,7 +251,7 @@ function readEnvLayer(
|
||||
* @param cwd - the invoking directory whose `.env` is the project layer.
|
||||
* @param warn - sink for the one-line misconfiguration diagnostics.
|
||||
* @returns this run's frozen environment snapshot.
|
||||
* @throws when either file declares a bootstrap-only variable, except {@link HOME_LAYER_PROXY_NAMES} in the Harness-home file.
|
||||
* @throws when either file declares a bootstrap-only variable, except names {@link isHomeLayerRouteName} accepts in the Harness-home file.
|
||||
*/
|
||||
export function loadLayeredEnv(
|
||||
binName: string, cwd: string = process.cwd(),
|
||||
|
||||
@@ -144,6 +144,9 @@ describe('loadLayeredEnv', () => {
|
||||
['a network proxy', 'HTTPS_PROXY=http://attacker.example\n'],
|
||||
['a lowercase network proxy', 'https_proxy=http://attacker.example\n'],
|
||||
['a browser command', 'BROWSER=./script\n'],
|
||||
['a DeepSeek model endpoint', 'DEEPSEEK_BASE_URL=http://attacker.example\n'],
|
||||
['a DeepSeek search endpoint', 'DEEPSEEK_SEARCH_BASE_URL=http://attacker.example\n'],
|
||||
['a lowercase DeepSeek endpoint', 'deepseek_base_url=http://attacker.example\n'],
|
||||
])('refuses to launch when a .env sets %s, before applying anything', (_case, content) => {
|
||||
const home = tmp()
|
||||
const project = tmp()
|
||||
@@ -189,12 +192,59 @@ describe('loadLayeredEnv', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('still refuses every other bootstrap name in the Harness-home .env', () => {
|
||||
it('accepts the DeepSeek endpoints from the Harness-home .env, below an exported one', () => {
|
||||
const home = tmp()
|
||||
const project = tmp()
|
||||
// A CA path sits in the same network group as the proxy names and changes what is trusted,
|
||||
// not where traffic goes; the exemption must not widen to it.
|
||||
writeFileSync(join(home, '.env'), 'SSL_CERT_FILE=/tmp/ca.pem\n')
|
||||
writeFileSync(join(home, '.env'), [
|
||||
'DEEPSEEK_BASE_URL=https://from-home.example/anthropic',
|
||||
'DEEPSEEK_SEARCH_BASE_URL=https://from-home.example/v1',
|
||||
'',
|
||||
].join('\n'))
|
||||
clear()
|
||||
vi.stubEnv('DSH_HOME', home)
|
||||
vi.stubEnv('DEEPSEEK_BASE_URL', 'https://exported.example/anthropic')
|
||||
vi.stubEnv('DEEPSEEK_SEARCH_BASE_URL', undefined)
|
||||
try {
|
||||
const snapshot = loadLayeredEnv(NAME, project, vi.fn())
|
||||
expect(snapshot.get('DEEPSEEK_SEARCH_BASE_URL'))
|
||||
.toEqual({ value: 'https://from-home.example/v1', source: 'user-env', path: join(home, '.env') })
|
||||
expect(snapshot.get('DEEPSEEK_BASE_URL')).toEqual({ value: 'https://exported.example/anthropic', source: 'process' })
|
||||
expect(process.env.DEEPSEEK_SEARCH_BASE_URL).toBe('https://from-home.example/v1')
|
||||
expect(process.env.DEEPSEEK_BASE_URL).toBe('https://exported.example/anthropic')
|
||||
} finally {
|
||||
clear()
|
||||
vi.unstubAllEnvs()
|
||||
}
|
||||
})
|
||||
|
||||
it.each(['deepseek_base_url', 'DeepSeek_Search_Base_URL'])('refuses %s from the Harness-home .env and names the exact spelling', (name) => {
|
||||
const home = tmp()
|
||||
const project = tmp()
|
||||
// Consumers look the endpoint up by its exact name on POSIX, so another spelling would load without effect.
|
||||
writeFileSync(join(home, '.env'), `${name}=https://from-home.example/v1\n`)
|
||||
clear()
|
||||
vi.stubEnv('DSH_HOME', home)
|
||||
vi.stubEnv(name, undefined)
|
||||
try {
|
||||
expect(() => loadLayeredEnv(NAME, project, vi.fn()))
|
||||
.toThrow(`spell it ${name.toUpperCase()}, then export it or put it in ${join(home, '.env')}`)
|
||||
expect(process.env[name]).toBeUndefined()
|
||||
} finally {
|
||||
clear()
|
||||
vi.unstubAllEnvs()
|
||||
}
|
||||
})
|
||||
|
||||
it.each([
|
||||
'SSL_CERT_FILE=/tmp/ca.pem\n',
|
||||
'NODE_TLS_REJECT_UNAUTHORIZED=0\n',
|
||||
'DSH_HOME=/tmp/elsewhere\n',
|
||||
])('still refuses every other bootstrap name in the Harness-home .env: %s', (content) => {
|
||||
const home = tmp()
|
||||
const project = tmp()
|
||||
// The CA and TLS names sit in the same network group as the proxy and DeepSeek endpoint names but change what
|
||||
// is trusted, not where traffic goes, and DSH_HOME would move the exemption itself; none may join it.
|
||||
writeFileSync(join(home, '.env'), content)
|
||||
clear()
|
||||
vi.stubEnv('DSH_HOME', home)
|
||||
try {
|
||||
@@ -205,16 +255,17 @@ describe('loadLayeredEnv', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('names the Harness-home file as the way out when a project .env sets a proxy', () => {
|
||||
it.each(['HTTP_PROXY', 'DEEPSEEK_BASE_URL'])('names the Harness-home file as the way out when a project .env sets %s', (name) => {
|
||||
const home = tmp()
|
||||
const project = tmp()
|
||||
writeFileSync(join(project, '.env'), 'HTTP_PROXY=http://attacker.example\n')
|
||||
writeFileSync(join(project, '.env'), `${name}=http://attacker.example\n`)
|
||||
clear(); clearProxy()
|
||||
vi.stubEnv('DSH_HOME', home)
|
||||
vi.stubEnv(name, undefined)
|
||||
try {
|
||||
expect(() => loadLayeredEnv(NAME, project, vi.fn()))
|
||||
.toThrow(`export HTTP_PROXY, or put it in ${join(home, '.env')}, which does not travel with a repository`)
|
||||
expect(process.env.HTTP_PROXY).toBeUndefined()
|
||||
.toThrow(`export ${name}, or put it in ${join(home, '.env')}, which does not travel with a repository`)
|
||||
expect(process.env[name]).toBeUndefined()
|
||||
} finally {
|
||||
clear(); clearProxy()
|
||||
vi.unstubAllEnvs()
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
en: 0dd8c3a22e28aacf
|
||||
zh: c27c11bf4b5cceba
|
||||
/deepseek-ai-dsh-client-ui-sidebar-documentpreview/what-it-registers:
|
||||
en: 5686f49d98437f49
|
||||
zh: 40c154f8cc7b8d14
|
||||
en: 691249b09395eae1
|
||||
zh: e2ae49df2ebe130e
|
||||
/deepseek-ai-dsh-client-ui-sidebar-documentpreview/addresses:
|
||||
en: 57942270e23d3a8e
|
||||
zh: 6617124e652b3f67
|
||||
|
||||
@@ -31,7 +31,7 @@ Preview files in the right Sidebar and choose among registered renderers. Markdo
|
||||
Image drag actions arrive through the image entry’s inject face from [ui-file-drag](../ui-file-drag/README.md).
|
||||
|
||||
- **The type** — `ctx.sidebarRightTabs.register(...)` with id `@deepseek-ai/dsh-client-ui-sidebar-documentpreview` (this implementation's identity in the tab system, and the key its body registers under), kind `text`, a whole-address file-resource pattern, band `fallback`. `canOpen` accepts only Session addresses, whose paths may be relative or absolute; bare `absolute` addresses are not claimed. A type registered at the `extension` or `builtin` band for a narrower pattern (say `*.png`) takes those addresses; other supported files land here. The whole address is the content identity, so two files with one name in different directories, or one path under two sessions, are two tabs; the decoded basename is the tab title, and the keyed `sidebar.right.pane.tab.title` seat places its extension-specific `FileTypeIcon` before that title.
|
||||
- **The body** — the keyed `sidebar.right.pane.tab` seat under the type's id. Its fixed header shows the Host's absolute path when available, otherwise the requested path; the shared [`PathLabel`](../ui-primitives/README.md#component-catalog) subdues directories, preserves a clipped path’s trailing characters with a left-edge fade, and exposes the full value on hover. A dropdown appears when multiple supported renderers are available. Plain text is offered only for text-compatible sources; a single renderer shows no viewer control. A known binary container suffix without a registered renderer shows the file-type icon and an unsupported-preview message under the path header, without issuing a read. Two Session-scoped list child slots hand the previewed file to other plugins once its metadata reports a Host path, both with `absolutePath` as the owner prop: `sidebar.right.tab.document.actions` renders after the header's own controls and before the navigator toggle, at the same position in every state that shows the header so it is not remounted when the state changes, and `sidebar.right.tab.document.unpreviewable` renders in the unsupported empty state and in the empty state of a readable file the preview cannot render (`not-text`, `too-large`), where Retry would otherwise stand; a carrier or unclassified read failure keeps Retry, and a missing or non-regular path offers only its explanation. The shipped occupant of both is [`ui-open-in-app`](../ui-open-in-app/README.md). The preview directly renders [`ui-sidebar-files`](../ui-sidebar-files/README.md#the-navigator)'s Session-scoped Factories: `sidebar.files.navigator` after the body in its content row with `{ useTabInfo, page: false, onOpen }`, where `onOpen(address)` calls `tab.actions.openResource(address, { replaceTab: true })`, and `sidebar.files.navigator.toggle` at the header's end with `{ useTabInfo }`. Both stay mounted from loading to loaded in docked and floating panes; unregistered Factories render nothing. The navigator owns its width within the row's positioning box and stacking context, narrowing or overlaying the body without covering the floating pane's resize handle. A renderer layer handling Escape calls `preventDefault()` to keep the navigator from also closing. A wrap toggle appears only when the selected renderer declares `wrap: true`; its glyph describes the mode the click selects, and the per-tab preference starts on. Reload stays in this header, not the Sidebar's tab strip. Automatic refresh defaults to enabled; its separate toggle is hidden, with state, labels, styles, and toggle logic retained. The body reaches every pane edge; each renderer owns its content inset and may own an inner scrollport. This intentionally differs from the file tree's 2px right-side scrollbar offset: previews keep the full pane width so edge-to-edge HTML and code scrollports end at the pane edge.
|
||||
- **The body** — the keyed `sidebar.right.pane.tab` seat under the type's id. Its fixed header shows the Host's absolute path when available, otherwise the requested path; the shared [`PathLabel`](../ui-primitives/README.md#component-catalog) subdues directories, preserves a clipped path’s trailing characters with a left-edge fade, and exposes the full value on hover. A dropdown appears when multiple supported renderers are available. Plain text is offered only for text-compatible sources; a single renderer shows no viewer control. A known binary container suffix without a registered renderer shows the file-type icon and an unsupported-preview message under the path header, without issuing a read. Two Session-scoped list child slots hand the previewed file to other plugins once its metadata reports a Host path, both with `absolutePath` as the owner prop: `sidebar.right.tab.document.actions` renders after the header's own controls and before the navigator toggle, at the same position in every state that shows the header so it is not remounted when the state changes, and `sidebar.right.tab.document.unpreviewable` renders in the unsupported empty state and in the empty state of a readable file the preview cannot render (`not-text`, `too-large`), where Retry would otherwise stand; a carrier or unclassified read failure keeps Retry, and a missing or non-regular path offers only its explanation. The shipped occupant of both is [`ui-open-in-app`](../ui-open-in-app/README.md). The preview directly renders [`ui-sidebar-files`](../ui-sidebar-files/README.md#the-navigator)'s Session-scoped Factories: `sidebar.files.navigator` after the body in its content row, which carries `data-files-navigator-row`, with `{ useTabInfo, page: false, onOpen }`, where `onOpen(address)` calls `tab.actions.openResource(address, { replaceTab: true })`, and `sidebar.files.navigator.toggle` at the header's end with `{ useTabInfo }`. Both stay mounted from loading to loaded in docked and floating panes; unregistered Factories render nothing. The navigator owns its width within the row's positioning box and stacking context, narrowing or overlaying the body without covering the floating pane's resize handle. A renderer layer handling Escape calls `preventDefault()` to keep the navigator from also closing. A wrap toggle appears only when the selected renderer declares `wrap: true`; its glyph describes the mode the click selects, and the per-tab preference starts on. Reload stays in this header, not the Sidebar's tab strip. Automatic refresh defaults to enabled; its separate toggle is hidden, with state, labels, styles, and toggle logic retained. The body reaches every pane edge; each renderer owns its content inset and may own an inner scrollport. This intentionally differs from the file tree's 2px right-side scrollbar offset: previews keep the full pane width so edge-to-edge HTML and code scrollports end at the pane edge.
|
||||
- **Shared loading and view state**, session-scoped and bucketed by tab id. The store holds accumulated pages or complete bytes, read and observed versions, pending Resource changes, automatic-refresh state, loading/failure state, renderer choice, scroll offset, wrap, and the answered navigation revision. The ordinary inject face calls Remote readers and writes through declared store actions. Reloads and loading-mode changes retire older requests; the tab's abort signal forgets its state.
|
||||
|
||||
Document implementations register metadata with `ctx.documentPreviews.register({ id, extensions, binaryExtensions?, priority, title, loading, wrap?, sourceLines? })` and a body under the same `id` in the keyed, Session-scoped `sidebar.right.tab.document` child slot. `binaryExtensions` lists suffixes in `extensions` that cannot be read as text and omit the plain-text option. `sourceLines` declares that a `text-pages` body exposes source-line anchors for [navigation](#navigation). Own both registrations with effects and wait for the child slot through `ctx.slots.inject`. Bodies receive `resourceAddress`, `content`, `wrap`, `scrollportRef`, `addResource`, `setResources`, and the standard `useTabInfo`/`useResource` hooks. Dependency membership belongs to a tab-owned `ResourceGroup`; replacing dependencies always retains the root Resource. An inner scrolling element attaches `scrollportRef`; unmounting restores the shared body as scroll owner. The registry retains all matching alternatives: `extension` (the default) ranks above `builtin`, then longer suffixes rank first, then registration order. The dropdown preserves a selected implementation while it remains available. HTML, SVG, and unmatched extensions retain the plain-text fallback independently of loading mode.
|
||||
|
||||
@@ -31,7 +31,7 @@ kind: "package-reference"
|
||||
图片拖放回调通过图片入口的注入接口从 [ui-file-drag](../ui-file-drag/README.zh.md) 获取。
|
||||
|
||||
- **类型** —— `ctx.sidebarRightTabs.register(...)`,id 为 `@deepseek-ai/dsh-client-ui-sidebar-documentpreview`(这个实现在 tab 系统中的身份,也是其正文注册所用的键),kind `text`,匹配完整文件资源地址的 pattern,档位 `fallback`。`canOpen` 只接受 Session 地址,其中路径可为相对或绝对路径;不认领裸 `absolute` 地址。在 `extension` 或 `builtin` 档以更窄 pattern(比如 `*.png`)注册的类型接走那些地址;其他受支持文件落到这里。整个地址就是内容身份,所以不同目录下同名的两个文件、或同一路径在两个会话之下,是两个 tab;解码后的 basename 是 tab 标题,keyed slot `sidebar.right.pane.tab.title` 会在标题前放置按扩展名选择的 `FileTypeIcon`。
|
||||
- **正文** —— keyed slot `sidebar.right.pane.tab`,键为类型的 id。固定头部在可用时显示 Host 的绝对路径,否则显示请求路径;共享的 [`PathLabel`](../ui-primitives/README.zh.md#component-catalog) 弱化目录颜色,以左侧渐隐保留被裁剪路径的尾部字符,并在悬停时展示完整值。有多个受支持的渲染器时才显示下拉菜单。仅文本兼容的源文件提供纯文本选项;只有一个渲染器时不显示查看器控件。已知的二进制容器后缀没有注册渲染器时,在路径头部下方显示文件类型图标和不支持预览的说明,并且不会发起读取。两个 Session 作用域的 list 子 slot 在文件元数据报出 Host 路径后把当前文件交给其他插件,owner prop 为 `absolutePath`:`sidebar.right.tab.document.actions` 在每个显示头部的状态里渲染在头部自有控件之后、导航栏开关之前,位置在各状态间相同,因此状态切换时不会重新挂载;`sidebar.right.tab.document.unpreviewable` 渲染在不支持预览的空态里,以及可读但本预览无法渲染的文件(`not-text`、`too-large`)的空态里,占据原本放重试的位置;载体或未分类的读取失败仍保留重试,不存在或非普通文件的路径只给说明。两者已发布的占位者都是 [`ui-open-in-app`](../ui-open-in-app/README.zh.md)。预览直接渲染 [`ui-sidebar-files`](../ui-sidebar-files/README.zh.md#the-navigator) 的 Session 作用域 Factory:`sidebar.files.navigator` 位于内容行的正文之后,传入 `{ useTabInfo, page: false, onOpen }`,其中 `onOpen(address)` 调用 `tab.actions.openResource(address, { replaceTab: true })`;`sidebar.files.navigator.toggle` 位于头部末端,传入 `{ useTabInfo }`。两者在停靠与浮动分栏中从加载到完成保持挂载;未注册的 Factory 不绘制内容。导航栏在内容行的定位框与层叠上下文内决定宽度,让正文变窄或覆盖正文,不遮挡浮窗缩放手柄。自行处理 Escape 的渲染器浮层需调用 `preventDefault()`,避免同时关闭导航栏。仅当所选渲染器声明 `wrap: true` 时显示换行开关;图标表示点击后切换到的模式,该偏好按 tab 保存,初始开启。重新载入仍在此头部,不放入 Sidebar 的 tab 条。自动刷新默认开启;独立开关暂时隐藏,状态、文案、样式与切换逻辑保留。正文贴合格的每条边,各渲染器自行提供内容留白,并可拥有内部滚动区。这与文件树右侧预留 2px 滚动条间距的布局有意不同:Preview 使用格的完整宽度,使贴边 HTML 与代码滚动区终止于格的边缘。
|
||||
- **正文** —— keyed slot `sidebar.right.pane.tab`,键为类型的 id。固定头部在可用时显示 Host 的绝对路径,否则显示请求路径;共享的 [`PathLabel`](../ui-primitives/README.zh.md#component-catalog) 弱化目录颜色,以左侧渐隐保留被裁剪路径的尾部字符,并在悬停时展示完整值。有多个受支持的渲染器时才显示下拉菜单。仅文本兼容的源文件提供纯文本选项;只有一个渲染器时不显示查看器控件。已知的二进制容器后缀没有注册渲染器时,在路径头部下方显示文件类型图标和不支持预览的说明,并且不会发起读取。两个 Session 作用域的 list 子 slot 在文件元数据报出 Host 路径后把当前文件交给其他插件,owner prop 为 `absolutePath`:`sidebar.right.tab.document.actions` 在每个显示头部的状态里渲染在头部自有控件之后、导航栏开关之前,位置在各状态间相同,因此状态切换时不会重新挂载;`sidebar.right.tab.document.unpreviewable` 渲染在不支持预览的空态里,以及可读但本预览无法渲染的文件(`not-text`、`too-large`)的空态里,占据原本放重试的位置;载体或未分类的读取失败仍保留重试,不存在或非普通文件的路径只给说明。两者已发布的占位者都是 [`ui-open-in-app`](../ui-open-in-app/README.zh.md)。预览直接渲染 [`ui-sidebar-files`](../ui-sidebar-files/README.zh.md#the-navigator) 的 Session 作用域 Factory:`sidebar.files.navigator` 位于内容行的正文之后(该行带有 `data-files-navigator-row`),传入 `{ useTabInfo, page: false, onOpen }`,其中 `onOpen(address)` 调用 `tab.actions.openResource(address, { replaceTab: true })`;`sidebar.files.navigator.toggle` 位于头部末端,传入 `{ useTabInfo }`。两者在停靠与浮动分栏中从加载到完成保持挂载;未注册的 Factory 不绘制内容。导航栏在内容行的定位框与层叠上下文内决定宽度,让正文变窄或覆盖正文,不遮挡浮窗缩放手柄。自行处理 Escape 的渲染器浮层需调用 `preventDefault()`,避免同时关闭导航栏。仅当所选渲染器声明 `wrap: true` 时显示换行开关;图标表示点击后切换到的模式,该偏好按 tab 保存,初始开启。重新载入仍在此头部,不放入 Sidebar 的 tab 条。自动刷新默认开启;独立开关暂时隐藏,状态、文案、样式与切换逻辑保留。正文贴合格的每条边,各渲染器自行提供内容留白,并可拥有内部滚动区。这与文件树右侧预留 2px 滚动条间距的布局有意不同:Preview 使用格的完整宽度,使贴边 HTML 与代码滚动区终止于格的边缘。
|
||||
- **共享加载与视图状态**,会话作用域、按 tab id 分桶。store 持有累计页或完整字节、读取与观察版本、待处理 Resource 变化、自动刷新状态、加载/失败状态、渲染器选择、滚动位置、换行和已响应的导航 revision。普通 inject face 调用 Remote 读取,并经声明的 store action 写入。重新载入和加载模式变化会淘汰旧请求;tab 的中止信号清理其状态。
|
||||
|
||||
文档实现在 `ctx.documentPreviews.register({ id, extensions, binaryExtensions?, priority, title, loading, wrap?, sourceLines? })` 注册元数据,并以相同 `id` 向 keyed、Session 作用域的子 slot `sidebar.right.tab.document` 注册正文。`binaryExtensions` 列出 `extensions` 中不可按文本阅读的后缀,这些后缀不提供纯文本选项。`sourceLines` 声明 `text-pages` 正文为[导航](#navigation)提供源码行锚点。两处注册都由 effect 持有,通过 `ctx.slots.inject` 等待子 slot。正文接收 `resourceAddress`、`content`、`wrap`、`scrollportRef`、`addResource`、`setResources` 和标准 `useTabInfo`/`useResource` 钩子。依赖成员由 tab 持有的 `ResourceGroup` 管理;替换依赖始终保留根 Resource。内部滚动元素挂载 `scrollportRef`;卸载时恢复共享正文的滚动职责。注册表保留所有匹配备选:`extension`(默认)优先于 `builtin`,随后按更长的后缀、再按注册顺序排列。所选实现仍可用时,下拉选择保持不变。HTML、SVG 和未匹配的扩展名保留纯文本回退,与加载方式无关。
|
||||
|
||||
@@ -73,7 +73,7 @@ export function PreviewFrame({
|
||||
{handoff}
|
||||
<NavigatorToggleSeat useTabInfo={useTabInfo} renderFactorySlot={renderFactorySlot} />
|
||||
</div>
|
||||
<div className={css.content} data-textpreview-content>
|
||||
<div className={css.content} data-textpreview-content data-files-navigator-row>
|
||||
{children}
|
||||
<NavigatorSeat useTabInfo={useTabInfo} renderFactorySlot={renderFactorySlot} />
|
||||
</div>
|
||||
|
||||
@@ -861,6 +861,8 @@ describe('TextPreview — workspace navigator Factories', () => {
|
||||
return {
|
||||
toggle: header?.lastElementChild?.getAttribute('data-navigator-factory'),
|
||||
navigator: row?.lastElementChild?.getAttribute('data-navigator-factory'),
|
||||
// The navigator's filled layout hides the body of the row this attribute names.
|
||||
navigatorRow: row?.hasAttribute('data-files-navigator-row'),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -876,7 +878,7 @@ describe('TextPreview — workspace navigator Factories', () => {
|
||||
])
|
||||
expect(view.container.querySelector('[data-textpreview-state="loading"]')).not.toBeNull()
|
||||
expect(seats(view.container)).toEqual({
|
||||
toggle: 'sidebar.files.navigator.toggle', navigator: 'sidebar.files.navigator',
|
||||
toggle: 'sidebar.files.navigator.toggle', navigator: 'sidebar.files.navigator', navigatorRow: true,
|
||||
})
|
||||
const navigator = view.container.querySelector<HTMLElement>('[data-textpreview-content] > [data-navigator-factory]')!
|
||||
const toggle = view.container.querySelector('[data-textpreview-path]')?.parentElement?.lastElementChild
|
||||
@@ -890,7 +892,7 @@ describe('TextPreview — workspace navigator Factories', () => {
|
||||
await settle()
|
||||
expect(view.container.querySelector('[data-textpreview-state="text"]')).not.toBeNull()
|
||||
expect(seats(view.container)).toEqual({
|
||||
toggle: 'sidebar.files.navigator.toggle', navigator: 'sidebar.files.navigator',
|
||||
toggle: 'sidebar.files.navigator.toggle', navigator: 'sidebar.files.navigator', navigatorRow: true,
|
||||
})
|
||||
expect(view.container.querySelector('[data-textpreview-content] > [data-navigator-factory]')).toBe(navigator)
|
||||
expect(view.container.querySelector('[data-textpreview-path]')?.parentElement?.lastElementChild).toBe(toggle)
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
en: c4b8f3a52f68bd51
|
||||
zh: 30b580320637572c
|
||||
/deepseek-ai-dsh-client-ui-sidebar-files/the-navigator:
|
||||
en: 7e3aebc6079b023b
|
||||
zh: 11911cc59fcec9b1
|
||||
en: 3424600b15bde2d2
|
||||
zh: 2cf1c99ffef2ad50
|
||||
/deepseek-ai-dsh-client-ui-sidebar-files/the-tree:
|
||||
en: 3d71e50801761213
|
||||
zh: e4e9ab19dd22288d
|
||||
|
||||
@@ -41,7 +41,7 @@ The [registration](src/client/index.ts) assembles the page, navigator, toggle, a
|
||||
<a id="the-navigator"></a>
|
||||
## The navigator
|
||||
|
||||
Render `sidebar.files.navigator` with `FileNavigatorInput`: `{ useTabInfo, page, onOpen }`. The required `onOpen: (address: string) => void` callback belongs to the containing plugin; the navigator builds the file address without choosing placement. `page` controls presentation only: `true` always fills the files page, while `false` follows the preview's pane preference and available width.
|
||||
Render `sidebar.files.navigator` with `FileNavigatorInput`: `{ useTabInfo, page, onOpen }`. The required `onOpen: (address: string) => void` callback belongs to the containing plugin; the navigator builds the file address without choosing placement. `page` controls presentation only: `true` always fills the files page, while `false` follows the preview's pane preference and available width. Render the Factory as a direct child of the row it shares with the body, and mark that row with `data-files-navigator-row`: the layout that fills the row hides the row's other children only under that attribute, so a host without it leaves the covered body reachable by keyboard.
|
||||
|
||||
The navigator is drawn below the preview's header, which spans the pane, at the right of the row it shares with the body. Beside a preview, its pane's record decides whether it shows: a pane starts with it hidden, and the choice holds for every file tab of the pane until the toggle, Escape, the workspace command, or navigation to a covered preview changes it. A pane without a record of its own, such as a floating panel a tab was dragged out into, starts hidden at the default width. A click outside the tree does not hide it, and opening content in another pane does not hide it either. On the files page the tree fills the page below the header, docked or floating, without a separator, and always shows: the page draws no navigator toggle and Escape leaves it. A file the page opens starts with the destination pane's own choice, hidden for a pane that has made none.
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ kind: "package-reference"
|
||||
<a id="the-navigator"></a>
|
||||
## 导航栏
|
||||
|
||||
用 `FileNavigatorInput` 渲染 `sidebar.files.navigator`:`{ useTabInfo, page, onOpen }`。必填的 `onOpen: (address: string) => void` 回调由容纳它的插件提供;导航栏构造文件地址,不决定落位。`page` 只控制呈现:`true` 时始终铺满文件页,`false` 时遵循预览所在分栏的偏好与可用宽度。
|
||||
用 `FileNavigatorInput` 渲染 `sidebar.files.navigator`:`{ useTabInfo, page, onOpen }`。必填的 `onOpen: (address: string) => void` 回调由容纳它的插件提供;导航栏构造文件地址,不决定落位。`page` 只控制呈现:`true` 时始终铺满文件页,`false` 时遵循预览所在分栏的偏好与可用宽度。Factory 要作为它与正文共用的那一行的直接子元素渲染,并给该行加上 `data-files-navigator-row`:铺满整行的布局只在带这个属性的行里隐藏其他子元素,宿主不加时,被盖住的正文仍可用键盘访问。
|
||||
|
||||
导航栏画在横跨分栏的预览标题行下方,位于它与正文共用的那一行的右侧。在预览旁,是否显示由其分栏的记录决定:分栏默认隐藏,这一选择对分栏内的每个文件 tab 都有效,直到开关、Escape、工作区命令,或导航到被目录树遮住的预览改变它。自身没有记录的分栏(例如 tab 拖出后所在的浮窗)从隐藏、默认宽度开始。点击目录树以外的地方不会隐藏它;在另一分栏打开内容也不会隐藏它。在文件页上,目录树铺满标题行下方的整页,停靠与浮动时都一样,不显示分隔条,且始终显示:文件页不画导航栏开关,Escape 也不关闭它。文件页打开的文件沿用落定分栏自己的选择,尚未做过选择的分栏为隐藏。
|
||||
|
||||
|
||||
@@ -41,7 +41,11 @@ export interface FileNavigatorInput {
|
||||
|
||||
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||
interface SlotFactoryMap {
|
||||
/** One pane's workspace tree with its width and show/hide state. */
|
||||
/**
|
||||
* One pane's workspace tree with its width and show/hide state. The host renders it as a direct child of
|
||||
* the content row it shares with the body, and that row carries `data-files-navigator-row`, which the
|
||||
* filled layout needs to hide the covered body.
|
||||
*/
|
||||
'sidebar.files.navigator': {
|
||||
scope: 'session'
|
||||
props: FileNavigatorInput
|
||||
|
||||
@@ -74,14 +74,16 @@
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
/* Covered content stays laid out but cannot receive keyboard focus. */
|
||||
:has(> .navigator.fill) > :not(.navigator) {
|
||||
/* Covered content stays laid out but cannot receive keyboard focus. The rule
|
||||
names its row through `data-files-navigator-row`: a `:has()` without a
|
||||
subject makes the browser restyle the whole document after any DOM change. */
|
||||
[data-files-navigator-row]:has(> .navigator.fill) > :not(.navigator) {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* navigator-layout `FULLSCREEN_QUERY`: every layout fills the row. */
|
||||
@media (width < 768px) {
|
||||
:has(> .navigator:not(.wholePage)) > :not(.navigator) {
|
||||
[data-files-navigator-row]:has(> .navigator:not(.wholePage)) > :not(.navigator) {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ export function FilesBody(props: FilesBodyProps): ReactNode {
|
||||
</Tooltip>
|
||||
{renderSlot('sidebar.right.tab.files.actions', { absolutePath: cwd })}
|
||||
</div>
|
||||
<div className={css.content} data-files-content>{navigator}</div>
|
||||
<div className={css.content} data-files-content data-files-navigator-row>{navigator}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loaded 100 of 436 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user