Merge remote-tracking branch 'origin/master' into fix/platform-login-theme
No files matched your search
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md
|
||||
2026-07-08-tool-output-spill-files.md: fa6697aad63c97b0ef580fe1ca46e50541e3d682
|
||||
2026-07-08-tool-output-spill-files.zh.md: a995eafc7878b342a2164c5116da1f43ada791a5
|
||||
2026-07-08-tool-output-spill-files.md: e32aec72f423dff33a29d9012c0f71c9cfbd24fb
|
||||
2026-07-08-tool-output-spill-files.zh.md: 2941fc516723e72b5d5670e674ce612f3cf581cc
|
||||
@@ -66,36 +66,7 @@ interface SpillRef {
|
||||
|
||||
### Spill policy
|
||||
|
||||
`dsh-spill-policy` is a `tools/post-execute` result transformer with one configuration knob:
|
||||
|
||||
```ts ignore-check
|
||||
interface Config {
|
||||
/** Omitted means no automatic spill policy. Present means apply to oversized plain text tool results. */
|
||||
maxInlineBytes?: number
|
||||
}
|
||||
```
|
||||
|
||||
When `maxInlineBytes` is omitted the plugin registers nothing (a true no-op). When set, it applies a default policy to final plain-text tool results:
|
||||
|
||||
1. Let the tool run normally, delegating via `next()` so a downstream listener settles the result first.
|
||||
2. Flatten the accepted final `ContentBlock[]` only when it is entirely plain text; a result with any non-text block is left untouched.
|
||||
3. If its UTF-8 byte size is at or below `maxInlineBytes`, leave it unchanged.
|
||||
4. If it is larger, call `ctx.spillStore.saveText()` with the full final text.
|
||||
5. Replace the model-facing result with a retained head/tail preview plus the spill reference.
|
||||
|
||||
The preview is an implementation default owned by the policy: a head/tail split of `maxInlineBytes` via the retention library's `TextRetainer`. Future config can expose preview sizing only after a second deployment needs it.
|
||||
|
||||
The replacement text is intentionally generic because the policy only knows the final formatted tool result, not the tool's internal resource:
|
||||
|
||||
```text
|
||||
<retained preview>
|
||||
|
||||
(Omitted N bytes. Full formatted result stored at: /.../session-.../....txt. Use read with offset/limit, or grep this path to search within it.)
|
||||
```
|
||||
|
||||
If `ctx.spillStore.saveText()` fails (permissions, ENOSPC, backend unavailable), or the call has no session owner, or no backend is loaded, the plugin logs the reason and returns the original result unchanged. Spill failure never turns a successful tool call into an `isError` result or hides the inline result.
|
||||
|
||||
The policy skips `read` to avoid a circular `read -> spill file -> read again` loop. Additional opt-out configuration is deferred until a real second tool needs it.
|
||||
`dsh-spill-policy` retains ordered text/image ends under `maxInlineTokens` after post-execute policy accepts the result. `saveText()` stores the complete result with readable image paths; image bytes remain in attachment storage. The [multimodal retention decision](../../implemented/bug-fix/2026-09-21-multimodal-tool-result-retention.md) owns image projection order, atomic image omission, and route pricing. An omitted budget installs no listeners, recovery failures keep the original result, and model-facing `read` results skip retention to avoid a read/spill loop.
|
||||
|
||||
## Showcase: web_fetch
|
||||
|
||||
@@ -129,7 +100,7 @@ With `dsh-spill-policy` configured, a large formatted fetch result is automatica
|
||||
- id: spill-policy
|
||||
name: '@deepseek-ai/dsh-spill-policy'
|
||||
config:
|
||||
maxInlineBytes: 50000
|
||||
maxInlineTokens: 12500
|
||||
```
|
||||
|
||||
This separation is important. `web-fetch-http` still owns resource caps (`maxResponseBytes`, `maxBodyChars`) to protect network, memory, and decoding work. `spill-policy` owns only the model-facing context cap after the result already exists. If the provider already returned `truncated: true`, the spill file contains the full formatted result the tool returned, not the full original webpage; the policy does not claim otherwise.
|
||||
@@ -186,11 +157,11 @@ The local-backend value proposition depends on the existing `read`/`grep` tools
|
||||
|
||||
**Snapshot gap.** No ACP snapshot scenario covers the transcript-visible `web_fetch` spill notice yet. The ACP snapshot harness replays keyless and cannot hit the live web, and a `web_fetch` spill requires a real over-cap HTTP body; a deterministic scenario would need a seeded loopback fetch target the replay tree does not currently wire (the examples do not load `tool-web` at all). The behavior is covered instead by the `dsh-tool-web` integration test against a loopback server. Closing the gap is follow-up work: wire `tool-web` + a seeded fetch target into the ACP example, then record a `web-fetch-spill` scenario.
|
||||
|
||||
The policy can become too large if it starts owning tool-specific semantics. It stays narrow: plain-text final results only. Tool-owned early spill remains future work.
|
||||
The policy can become too large if it starts owning tool-specific semantics. It handles accepted text/image sequences without interpreting tool-specific text. Tool-owned early spill remains future work.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Require each tool to opt in with a retention declaration.** Rejected: the goal is a default behavior similar to Claude Code's generic tool-result persistence. A single `maxInlineBytes` deployment knob is enough to prove the shape.
|
||||
**Require each tool to opt in with a retention declaration.** Rejected: the goal is a default behavior similar to Claude Code's generic tool-result persistence. A single `maxInlineTokens` deployment knob is enough to prove the shape.
|
||||
|
||||
**Make `tool-results` a broad tool-result platform.** Rejected: a broad package name invites retention policy, result replacement, preview wording, search, and early spill into one seam. The shared storage part is smaller: save text and return a locator plus retrieval hint.
|
||||
|
||||
|
||||
@@ -66,36 +66,7 @@ interface SpillRef {
|
||||
|
||||
### spill 策略
|
||||
|
||||
`dsh-spill-policy` 是一个 `tools/post-execute` 结果转换器,只提供一个配置项:
|
||||
|
||||
```ts ignore-check
|
||||
interface Config {
|
||||
/** Omitted means no automatic spill policy. Present means apply to oversized plain text tool results. */
|
||||
maxInlineBytes?: number
|
||||
}
|
||||
```
|
||||
|
||||
省略 `maxInlineBytes` 时,插件不会注册任何内容,是真正的无操作。设置该值后,它会对最终的纯文本工具结果应用默认策略:
|
||||
|
||||
1. 让工具正常运行,通过 `next()` 委托,使下游监听器先结算结果。
|
||||
2. 仅当已接受的最终 `ContentBlock[]` 全部是纯文本时,才将其展平;含任何非文本块的结果保持不变。
|
||||
3. 如果 UTF-8 字节大小不超过 `maxInlineBytes`,保持不变。
|
||||
4. 如果超出上限,使用完整的最终文本调用 `ctx.spillStore.saveText()`。
|
||||
5. 把模型可见结果替换为保留的首尾预览和 spill 引用。
|
||||
|
||||
预览属于策略所有的实现默认值:以 `maxInlineBytes` 为上限,使用保留库的 `TextRetainer` 进行首尾分割。只有第二个部署证明有此需求后,未来配置才会公开预览大小。
|
||||
|
||||
替换文本刻意保持通用,因为策略只知道最终格式化的工具结果,不了解工具的内部资源:
|
||||
|
||||
```text
|
||||
<retained preview>
|
||||
|
||||
(Omitted N bytes. Full formatted result stored at: /.../session-.../....txt. Use read with offset/limit, or grep this path to search within it.)
|
||||
```
|
||||
|
||||
如果 `ctx.spillStore.saveText()` 失败(权限、ENOSPC、后端不可用),或调用没有会话所有者,或未加载后端,插件会记录原因并原样返回结果。spill 失败绝不会把成功的工具调用变为 `isError` 结果,也不会隐藏内联结果。
|
||||
|
||||
策略跳过 `read`,以避免形成 `read -> spill file -> read again` 循环。额外的选择退出配置要等确实出现第二个有此需求的工具后再引入。
|
||||
`dsh-spill-policy` 在执行后策略接受结果之后,按 `maxInlineTokens` 对文字和图片共同计量并保留首尾。完整结果仍通过 `saveText()` 保存,图片本体留在附件存储,结果文件记录可读取路径。图片投影顺序、整图省略和模型计量由[图文结果保留决策](../../implemented/bug-fix/2026-09-21-multimodal-tool-result-retention.zh.md)负责。省略配置时不安装监听器,存储失败时保留原结果;`read` 的模型可见结果跳过自动省略,避免重复读取产生循环。
|
||||
|
||||
## 示例:web_fetch
|
||||
|
||||
@@ -129,7 +100,7 @@ ctx.tools.register(defineTool({
|
||||
- id: spill-policy
|
||||
name: '@deepseek-ai/dsh-spill-policy'
|
||||
config:
|
||||
maxInlineBytes: 50000
|
||||
maxInlineTokens: 12500
|
||||
```
|
||||
|
||||
这项分离很重要。`web-fetch-http` 仍负责资源上限(`maxResponseBytes`、`maxBodyChars`),用来保护网络、内存和解码工作。`spill-policy` 只负责结果已经存在后针对模型上下文的上限。如果提供方已经返回 `truncated: true`,spill 文件包含的是工具返回的完整格式化结果,而不是原始网页全文;策略不会做出其他承诺。
|
||||
@@ -186,11 +157,11 @@ ctx.tools.register(defineTool({
|
||||
|
||||
**快照缺口。** 目前没有 ACP 快照场景覆盖 transcript(文本记录)可见的 `web_fetch` spill 提示。ACP 快照 harness 在无密钥环境中回放,无法访问实时 web,而 `web_fetch` spill 需要一个真实的超上限 HTTP 正文;确定性场景需要一个预置的 loopback fetch 目标,但当前回放树尚未接线(示例根本没有加载 `tool-web`)。该行为改由 `dsh-tool-web` 针对 loopback server 的集成测试覆盖。弥补该缺口属于后续工作:把 `tool-web` 和预置 fetch 目标接入 ACP 示例,然后录制 `web-fetch-spill` 场景。
|
||||
|
||||
如果策略开始负责工具专用语义,就会膨胀得过大。它的范围保持狭窄:只处理纯文本最终结果。由工具负责的提前 spill 仍留作未来工作。
|
||||
如果策略开始负责工具专用语义,就会膨胀得过大。它处理已接受的图文序列,不解释工具专用的文字语义。由工具负责的提前 spill 仍留作未来工作。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**要求每个工具通过保留声明选择加入。**不予采纳,因为目标是实现类似 Claude Code 通用工具结果持久化的默认行为。只需一个 `maxInlineBytes` 部署配置项即可验证该形态。
|
||||
**要求每个工具通过保留声明选择加入。**不予采纳,因为目标是实现类似 Claude Code 通用工具结果持久化的默认行为。只需一个 `maxInlineTokens` 部署配置项即可验证该形态。
|
||||
|
||||
**把 `tool-results` 建成宽泛的工具结果平台。** 不予采纳:宽泛的包名会诱使系统把保留策略、结果替换、预览措辞、搜索和提前 spill 合并进一个 seam。可共享的存储部分更小:保存文本,并返回定位符与检索提示。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-14-deepseek-account-login.md
|
||||
2026-09-14-deepseek-account-login.md: 65405b293cdea14a231d8d700589f9c3e7ef6db7
|
||||
2026-09-14-deepseek-account-login.zh.md: 0aca011826006a0fb8f0557464187b73da377238
|
||||
2026-09-14-deepseek-account-login.md: 9d9853617a3b7e288084c93eac4e150f3c180999
|
||||
2026-09-14-deepseek-account-login.zh.md: d9cdbed25b0ae79caa4c61b310383cc52d8b586a
|
||||
@@ -46,7 +46,7 @@ Profile and recharge-wallet queries use independent getProfile and getBalance op
|
||||
|
||||
Deployment authentication uses explicit Host-only requestHeaders on the configured Platform origin. The provider rejects redirects and reserved-header overrides so deployment cookies cannot replace account authorization or follow a browser destination. Environment-specific authentication protocols remain outside the account provider.
|
||||
|
||||
The Host sends login_source (desktop or web) in auth_init so Platform can choose the completion experience. Web failures close the authorization tab while the original tab receives Host state; no Web UI return URL is used. The backend accepts localhost callbacks. DSH preserves the browser-supplied localhost hostname and port without DNS resolution or conversion to an IP literal.
|
||||
The bundled account UI is Desktop-only: its preload bridge enables the account launcher, settings and sign-in onboarding. Plain Web retains API-key onboarding and the standard Settings launcher without account subscription or login. The Host protocol accepts login_source (desktop or web) in auth_init; the bundled UI sends desktop. The backend accepts localhost callbacks. DSH preserves the browser-supplied localhost hostname and port without DNS resolution or conversion to an IP literal.
|
||||
|
||||
The macOS development launcher registers an isolated, ad-hoc-signed application bundle for `dsh://open`. The bundle retains the workspace entry and development paths for Launch Services cold starts without copying credentials or changing the package-manager-owned Electron application. Protocol registration targets the latest launched development or packaged application.
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ DSH 授权通过 x-dsh-auth-token 请求头鉴权 Platform、推理和 Files 请
|
||||
|
||||
开发环境认证通过配置的平台来源上的显式 Host 请求头 requestHeaders 完成。提供者拒绝重定向和保留请求头覆盖,防止开发环境 Cookie 替换账号授权或跟随浏览器跳转地址。特定环境的认证协议不属于账号提供者。
|
||||
|
||||
Host 在 auth_init 中发送 login_source(desktop 或 web),供 Platform 选择完成页交互。Web 失败时关闭授权标签页,原标签页接收 Host 状态,不使用 Web UI 返回地址。后端接受 localhost 回调。DSH 保留浏览器提供的 localhost 主机名和端口,不做 DNS 解析或 IP 字面量转换。
|
||||
内置账号界面仅用于 Desktop:preload 桥接启用账号入口、设置和登录引导。普通 Web 保留 API Key 引导及标准设置入口,不订阅账号状态或显示登录。Host 协议在 auth_init 中接受 login_source(desktop 或 web);内置界面发送 desktop。后端接受 localhost 回调。DSH 保留浏览器提供的 localhost 主机名和端口,不做 DNS 解析或 IP 字面量转换。
|
||||
|
||||
macOS 开发启动器为 `dsh://open` 注册独立、经临时签名的应用包。该应用包保留工作区入口和开发路径,使 Launch Services 能够冷启动;凭证不会被复制,也不修改包管理器安装的 Electron 应用。协议注册指向最近启动的开发版或打包版应用。
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-21-multimodal-tool-result-retention.md
|
||||
2026-09-21-multimodal-tool-result-retention.md: 41470724911d3086878d206f82c76ad9a4993146
|
||||
2026-09-21-multimodal-tool-result-retention.zh.md: 8e6d4953f7380d45235ac13f9f245e2e005f1abf
|
||||
@@ -0,0 +1,31 @@
|
||||
# Agent Note: Multimodal tool-result retention
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-21-multimodal-tool-result-retention.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
MCP prepares durable images asynchronously while its pure renderer emits text placeholders. Installing prepared content after post-execute policy makes text retention invalidate image replacement. A byte-only limit also cannot compare text with visual input costs.
|
||||
|
||||
## Decision
|
||||
|
||||
`ToolDefinition.projectContent` installs execution-prepared content before post-execute policy. The final callback retains its separate role for terminal and job limits. Policy blocks and replacements remain authoritative.
|
||||
|
||||
The spill policy prices ordered text/image blocks in estimated tokens, reusing the text estimator and the active route image calculator, including descriptor text. It reserves omission notices, divides remaining tokens between contiguous ends, and omits the middle. Text can split between code points; images remain indivisible at their original positions.
|
||||
|
||||
The complete spill file contains accepted text and readable attachment paths in order. Attachments own image bytes. Missing image pricing or a readable recovery path preserves the original result with a diagnostic. PTC program values remain complete; forwarding and logs retain a recovery notice even when every image is omitted.
|
||||
|
||||
The [spill storage decision](../architecture/2026-07-08-tool-output-spill-files.md) still owns backend separation and secure local files. The [MCP client decision](../feature/2026-07-07-mcp-client-plugin.md) still owns canonical values and image admission.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Restore images after policy edits.** This can resurrect blocked images or discarded text. Preparing content before policy lets transformations inspect the actual images.
|
||||
|
||||
**Keep images outside the budget or charge a fixed maximum.** Images must share the result budget with text. The existing route calculator already prices projected dimensions and descriptor text, so it owns that cost.
|
||||
|
||||
**Embed image bytes in spill text.** Base64 inflates the artifact and duplicates attachment storage. Readable attachment paths preserve retrieval and ordering without copying bytes.
|
||||
|
||||
## Consequences
|
||||
|
||||
The shipped result budget is 12,500 estimated tokens, replacing the former 50,000-byte setting. Text estimation remains heuristic; provider usage is authoritative. Unused space at an image boundary stays unused to preserve contiguous ends. Other block types pass through.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Agent Note: 工具结果的图文保留
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-21-multimodal-tool-result-retention.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
MCP 异步准备持久化图片,而纯渲染器输出文字占位。在执行后策略完成后才安装已准备内容,会让文字保留使图片替换失效。按字节限制也无法比较文字与视觉输入的成本。
|
||||
|
||||
## Decision
|
||||
|
||||
`ToolDefinition.projectContent` 在执行后策略之前安装执行期间准备的内容。最终回调保留其对终端和任务结果设定上限的独立职责。策略阻止和替换仍然生效。
|
||||
|
||||
spill 策略以估算 token 对有序图文块计量,复用文字估算和当前模型图片计算器,并计入说明文字。它预留省略提示,将剩余 token 平分给连续两端,省略中间。文字可在码点之间切分;图片不可切分,并保持原位置。
|
||||
|
||||
完整结果文件按顺序保存已接受文字和可读取的附件路径。附件拥有图片字节。缺少图片计量或可读取的恢复路径时,保留原结果并记录诊断。PTC 程序返回值保持完整;全部图片被省略时,转发内容和日志仍保留读取提示。
|
||||
|
||||
[spill 存储决策](../architecture/2026-07-08-tool-output-spill-files.zh.md)继续负责后端拆分和安全的本地文件。[MCP 客户端决策](../feature/2026-07-07-mcp-client-plugin.zh.md)继续负责规范值和图片准入。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**在策略修改后恢复图片。** 这可能恢复已阻止图片或已丢弃文字。在策略之前准备内容,让转换能够检查真实图片。
|
||||
|
||||
**图片不计入预算或按固定上限计费。** 图片必须与文字共享结果预算。现有模型计算器已经衡量请求尺寸和说明文字,因此由它负责成本。
|
||||
|
||||
**把图片字节嵌入 spill 文本。** Base64 会增大产物并重复附件存储。可读取的附件路径在不复制字节的情况下保留读取方式和顺序。
|
||||
|
||||
## Consequences
|
||||
|
||||
随产品提供的结果预算为 12,500 个估算 token,替代此前的 50,000 字节配置。文字计量仍为估算,提供方用量具有权威性。图片分界处剩余空间保持空闲,以维持连续的首尾内容。其他内容块类型原样通过。
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-07-mcp-client-plugin.md
|
||||
2026-07-07-mcp-client-plugin.md: a4aa2cf216d9f28afedebf82f0659db70606bbf1
|
||||
2026-07-07-mcp-client-plugin.zh.md: 5903e4ef976317602527d9f335d640bc86939f72
|
||||
2026-07-07-mcp-client-plugin.md: dd793b67bf2043fa88377d50837df40faadf3fbf
|
||||
2026-07-07-mcp-client-plugin.zh.md: 5056118df0185f1e7ec2b1ea06818ed920d2af26
|
||||
@@ -145,7 +145,7 @@ A unified `execute` handler for all tools from one MCP server:
|
||||
1. Call the SDK's `callTool` with the closed-over `rawName`, model arguments, `exec.signal`, the configured timeout, and the complete discovered `toolDefinition` — the public name is never sent to the server.
|
||||
2. Preserve canonical success as `{ content: JsonValue[], structuredContent? }`; complete MCP JSON blocks remain the programmatic/PTC mode value. `isError: true` throws before any image persistence so the registry owns the failure path.
|
||||
3. Prepare a separate ordered Native projection. Text runs join with `'\n'`; resource links preserve name and URI as text; audio and embedded resources become explicit diagnostics. The SDK rejects malformed wire results. If any image exists, the bridge strictly decodes the complete batch, resolves the calling agent's latest exact route, requires an attachment store plus explicit model image input, and delegates all-member validation and ordered persistence to `AttachmentStore.saveImages()`. Any decode, capability, or storage refusal renders every image as diagnostic text and returns no partial references.
|
||||
4. Keep `output.render` synchronous and pure. The executor stages its richer projection in a generation-local `WeakMap` keyed by the exact execution; `finalizeContent` installs it only when the registry's post-execute result still has the original canonical value and fallback content. A policy block, value replacement, or content replacement remains authoritative, and a re-sync cannot let an older generation consume new execution state.
|
||||
4. The executor preserves the complete canonical MCP value and prepares ordered text/image content. `projectContent` installs that content before `tools/post-execute`, so retention sees real images. Subsequent content replacement, value replacement, and blocking remain authoritative.
|
||||
5. PTC mode receives the untouched canonical value. Its generic dispatch bridge defers a successful final content sequence containing an image through the outer `run_code` result, so MCP requires no private parent-token special case.
|
||||
6. Cancellation: `exec.signal` (from the agent loop's cancel) is passed through to the MCP SDK's `callTool`, exact-model lookup, and the pre-storage gate.
|
||||
|
||||
@@ -157,6 +157,8 @@ Build the child environment from the subprocess seam's shared `scrubbedParentEnv
|
||||
|
||||
A per-instance connection supervisor reconnects automatically after a lost connection with bounded exponential backoff and a per-outage attempt budget, re-running discovery on success; exhaustion unregisters the server's tools and stops until reload. The [auto-reconnect Agent Note](../../archived/feature/2026-08-06-mcp-client-auto-reconnect.md) owns that decision, including the `reconnect` config block and the `reconnect.enabled: false` opt-out that restores manual HMR/restart recovery.
|
||||
|
||||
The [multimodal retention decision](../../implemented/bug-fix/2026-09-21-multimodal-tool-result-retention.md) explains why prepared images enter content before result policies.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
### MCP Server side (expose harness tools to external MCP clients)
|
||||
|
||||
@@ -145,7 +145,7 @@ SDK 接受符合协议的工具,并执行现代 HTTP header 声明检查。注
|
||||
1. 调用 SDK 的 `callTool`,传入闭包持有的 `rawName`、模型参数、`exec.signal`、配置的超时时间与完整的已发现 `toolDefinition`——公开名称永远不发送给服务器。
|
||||
2. 把规范成功值保留为 `{ content: JsonValue[], structuredContent? }`;完整 MCP JSON 块仍是程序化调用/PTC mode 值。`isError: true` 会在持久化任何图片前抛出,使失败路径归注册表所有。
|
||||
3. 另行准备有序 Native 投影。连续文本块以 `'\n'` 连接;资源链接以文本保留名称和 URI;音频和嵌入资源成为明确诊断。SDK 拒绝格式错误的协议结果。只要存在图片,桥接层就严格解码完整批次,解析调用 agent 的最新确切路由,要求附件存储以及模型明确支持图片输入,再把全成员校验和有序持久化委托给 `AttachmentStore.saveImages()`。任何解码、能力或存储拒绝都会把全部图片渲染为诊断文本,且不返回部分引用。
|
||||
4. 保持 `output.render` 同步且纯净。执行器把更丰富的投影暂存在按同步世代创建、以确切执行为键的 `WeakMap` 中;只有注册表的 post-execute 结果仍保留原规范值和兜底内容时,`finalizeContent` 才安装该投影。策略阻止、值替换或内容替换仍具有权威性,重新同步也无法让旧世代消费新执行状态。
|
||||
4. 执行器保留完整的规范 MCP 返回值,并准备有序图文内容。`projectContent` 在 `tools/post-execute` 之前安装已准备的内容,让省略策略看到真实图片块。后续策略的内容替换、值替换和阻止均保持生效。
|
||||
5. PTC mode 接收未改动的规范值。其通用分发桥接层会把包含图片的成功最终内容序列经外层 `run_code` 结果延后,因此 MCP 无需私有父 token 特例。
|
||||
6. 取消:`exec.signal`(来自 agent loop 的取消)透传给 MCP SDK 的 `callTool`、确切模型查询和存储前门禁。
|
||||
|
||||
@@ -157,6 +157,8 @@ SDK 接受符合协议的工具,并执行现代 HTTP header 声明检查。注
|
||||
|
||||
每个实例的连接监督器在连接丢失后以有界指数退避和单次故障尝试预算自动重连,成功后重新执行发现流程;尝试耗尽则注销该服务器的工具并停止,直到重新加载。[自动重连 Agent Note](../../archived/feature/2026-08-06-mcp-client-auto-reconnect.md) 拥有该决策,包括 `reconnect` 配置块和恢复手动 HMR/重启恢复的 `reconnect.enabled: false` opt-out。
|
||||
|
||||
图片在结果策略之前进入内容的原因见[图文结果保留决策](../../implemented/bug-fix/2026-09-21-multimodal-tool-result-retention.zh.md)。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
### MCP Server 端(将 harness 工具暴露给外部 MCP 客户端)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh",
|
||||
"description": "dsh CLI: profile launch, plugin management, and configuration inspection",
|
||||
"version": "0.1.6-alpha.2",
|
||||
"version": "0.1.7-alpha.1",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-desktop-host",
|
||||
"description": "Private Node-mode host process for the Electron desktop application",
|
||||
"version": "0.1.6-alpha.2",
|
||||
"version": "0.1.7-alpha.1",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
@@ -119,6 +119,8 @@ static LRESULT CALLBACK ProgressProc(HWND window, UINT message, WPARAM wparam, L
|
||||
graphics.ScaleTransform(page->dpi / 96.0f, page->dpi / 96.0f);
|
||||
graphics.Clear(page->dark ? Color(255, 21, 21, 23) : Color(255, 255, 255, 255));
|
||||
graphics.SetSmoothingMode(SmoothingModeAntiAlias);
|
||||
graphics.SetInterpolationMode(InterpolationModeHighQualityBicubic);
|
||||
graphics.SetPixelOffsetMode(PixelOffsetModeHalf);
|
||||
graphics.DrawImage(page->brand, Rect(0, 174, 600, 196));
|
||||
const int stage = static_cast<int>(reinterpret_cast<INT_PTR>(GetPropW(GetParent(window), L"HarnessInstaller.Stage")));
|
||||
const double fraction = reinterpret_cast<UINT_PTR>(GetPropW(GetParent(window), L"HarnessInstaller.ExtractProgress")) / 100.0;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-desktop",
|
||||
"description": "Electron desktop shell for a bundled dsh runtime and external plugins",
|
||||
"version": "0.1.6-alpha.2",
|
||||
"version": "0.1.7-alpha.1",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
|
Before Width: | Height: | Size: 325 KiB After Width: | Height: | Size: 126 KiB |
@@ -1,8 +1,8 @@
|
||||
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(32 32) scale(0.9375) translate(-40 -32)"><g filter="url(#filter0_d_1637_371)">
|
||||
<rect rx="44" width="1024" height="1024" transform="translate(40 32)" fill="#FBFBFB"/>
|
||||
<rect rx="44" width="1024" height="1024" transform="translate(40 32)" fill="url(#paint0_linear_1637_371)" fill-opacity="0.4"/>
|
||||
<rect rx="44" width="1024" height="1024" transform="translate(40 32)" fill="url(#paint1_linear_1637_371)" fill-opacity="0.8"/>
|
||||
<rect rx="192" width="1024" height="1024" transform="translate(40 32)" fill="#FBFBFB"/>
|
||||
<rect rx="192" width="1024" height="1024" transform="translate(40 32)" fill="url(#paint0_linear_1637_371)" fill-opacity="0.4"/>
|
||||
<rect rx="192" width="1024" height="1024" transform="translate(40 32)" fill="url(#paint1_linear_1637_371)" fill-opacity="0.8"/>
|
||||
<g filter="url(#filter1_ddi_1637_371)">
|
||||
<path d="M968.002 326.025C959.5 321.85 955.842 329.804 950.865 333.835C949.162 335.143 947.722 336.834 946.283 338.393C933.859 351.673 919.336 360.407 900.376 359.363C872.649 357.804 848.963 366.525 828.035 387.759C823.586 361.583 808.812 345.951 786.313 335.936C774.536 330.729 762.64 325.51 754.401 314.186C748.645 306.112 747.074 297.127 744.195 288.274C742.36 282.936 740.538 277.465 734.385 276.554C727.718 275.51 725.103 281.112 722.476 285.803C712.006 304.95 707.952 326.039 708.349 347.405C709.26 395.463 729.54 433.742 769.823 460.962C774.404 464.094 775.579 467.212 774.14 471.771C771.394 481.153 768.119 490.257 765.241 499.639C763.406 505.624 760.66 506.933 754.256 504.329C732.154 495.08 713.049 481.417 696.175 464.874C667.524 437.138 641.633 406.536 609.325 382.566C601.733 376.964 594.154 371.758 586.298 366.802C553.33 334.773 590.616 308.464 599.251 305.333C608.282 302.082 602.393 290.877 573.214 291.009C544.048 291.141 517.364 300.906 483.353 313.935C478.389 315.891 473.147 317.318 467.786 318.494C436.917 312.64 404.873 311.332 371.377 315.111C308.332 322.141 257.975 351.964 220.953 402.876C176.471 464.081 166.014 533.624 178.835 606.153C192.302 682.594 231.291 745.887 291.194 795.372C353.328 846.68 424.876 871.813 506.498 866.99C556.076 864.122 611.279 857.489 673.532 804.74C689.23 812.55 705.708 815.681 733.052 818.02C754.111 819.976 774.391 816.976 790.076 813.726C814.674 808.519 812.971 785.726 804.072 781.564C731.995 747.962 747.826 761.638 733.435 750.565C770.06 707.198 825.263 662.139 846.85 516.156C848.553 504.567 847.114 497.273 846.85 487.892C846.718 482.157 848.025 479.95 854.574 479.303C872.623 477.215 890.156 472.273 906.238 463.42C952.938 437.892 971.765 395.965 976.215 345.7C976.875 338.01 976.083 330.069 967.976 326.039L968.002 326.025ZM561.067 778.432C491.222 723.477 457.343 705.374 443.347 706.154C430.263 706.933 432.626 721.918 435.505 731.682C438.515 741.315 442.436 747.962 447.929 756.418C451.718 762.021 454.346 770.345 444.14 776.609C421.641 790.549 382.533 771.918 380.698 771.006C335.174 744.182 297.109 708.757 270.293 660.316C244.389 613.698 229.35 563.685 226.868 510.302C226.208 497.406 230.01 492.847 242.831 490.508C259.704 487.376 277.106 486.729 293.98 489.2C365.264 499.625 425.959 531.523 476.844 582.052C505.878 610.831 527.861 645.213 550.491 678.815C574.561 714.492 600.452 748.49 633.42 776.358C645.066 786.122 654.348 793.548 663.247 799.019C636.431 802.018 591.698 802.666 561.08 778.445L561.067 778.432ZM594.55 562.905C594.55 557.171 599.132 552.625 604.888 552.625C606.195 552.625 607.371 552.876 608.414 553.273C609.853 553.801 611.16 554.581 612.203 555.744C614.038 557.567 615.081 560.17 615.081 562.905C615.081 568.64 610.5 573.185 604.743 573.185C598.987 573.185 594.537 568.627 594.537 562.905H594.55ZM698.539 616.301C691.871 619.037 685.19 621.375 678.787 621.64C668.845 622.155 657.992 618.125 652.103 613.17C642.953 605.479 636.404 601.185 633.658 587.773C632.483 582.039 633.13 573.185 634.186 568.111C636.536 557.171 633.922 550.141 626.212 543.759C619.927 538.553 611.952 537.112 603.185 537.112C599.911 537.112 596.914 535.685 594.682 534.509C591.025 532.686 588.015 528.127 590.893 522.525C591.804 520.701 596.254 516.275 597.297 515.495C609.206 508.716 622.937 510.936 635.625 516.01C647.403 520.833 656.288 529.686 669.109 542.186C682.193 557.289 684.543 561.465 692.003 572.789C697.892 581.642 703.252 590.759 706.909 601.185C709.128 607.699 706.249 613.038 698.539 616.288V616.301Z" fill="url(#paint2_linear_1637_371)" fill-opacity="0.9"/>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
@@ -3,6 +3,8 @@
|
||||
export const en = {
|
||||
application: 'Application',
|
||||
aboutMenu: 'About DeepSeek Harness',
|
||||
aboutProduct: 'DeepSeek Harness',
|
||||
aboutVersion: 'Version V{version}',
|
||||
hideApplication: 'Hide DeepSeek Harness',
|
||||
hideOtherApplications: 'Hide Others',
|
||||
showAllApplications: 'Show All',
|
||||
@@ -88,7 +90,7 @@ export const en = {
|
||||
updateTechnicalDetails: 'View technical details',
|
||||
updateTitle: 'DeepSeek Harness Update',
|
||||
updateAvailable: 'An update is available',
|
||||
updateDetail: 'DeepSeek Harness {version}\n\nThis release includes its matching dsh version. The application will restart after installation.',
|
||||
updateDetail: 'DeepSeek Harness {version}\n\nDeepSeek Harness will restart to complete the update.',
|
||||
installAndRestart: 'Install and Restart',
|
||||
later: 'Later',
|
||||
updateFailedTitle: 'Update Failed',
|
||||
@@ -130,6 +132,8 @@ export type DesktopMessages = { readonly [Key in keyof typeof en]: string }
|
||||
export const zh = {
|
||||
application: '应用',
|
||||
aboutMenu: '关于 DeepSeek Harness',
|
||||
aboutProduct: 'DeepSeek Harness',
|
||||
aboutVersion: '版本 V{version}',
|
||||
hideApplication: '隐藏 DeepSeek Harness',
|
||||
hideOtherApplications: '隐藏其他',
|
||||
showAllApplications: '显示全部',
|
||||
@@ -215,7 +219,7 @@ export const zh = {
|
||||
updateTechnicalDetails: '查看技术详情',
|
||||
updateTitle: 'DeepSeek Harness 更新',
|
||||
updateAvailable: '发现可用更新',
|
||||
updateDetail: 'DeepSeek Harness {version}\n\n新版本绑定匹配的 dsh,安装后将重新启动。',
|
||||
updateDetail: 'DeepSeek Harness {version}\n\nDeepSeek Harness 将重启以完成更新。',
|
||||
installAndRestart: '安装并重启',
|
||||
later: '稍后',
|
||||
updateFailedTitle: '更新失败',
|
||||
|
||||
@@ -290,6 +290,13 @@ async function main(): Promise<void> {
|
||||
}
|
||||
finally { ordinaryDialogs.delete(controller) }
|
||||
}
|
||||
// Copy comes from the same locale as the update prompts so the dialog
|
||||
// chrome and its content never mix languages.
|
||||
const showAbout = async (): Promise<void> => {
|
||||
await ordinaryMessageBox({ type: 'info', title: locale.messages.aboutMenu, message: locale.messages.aboutProduct,
|
||||
detail: formatDesktopMessage(locale.messages.aboutVersion, { version: app.getVersion() }),
|
||||
buttons: [locale.messages.updateAcknowledge], cancelId: 0 })
|
||||
}
|
||||
const appPreload = fileURLToPath(new URL('./preload-app.cjs', import.meta.url))
|
||||
const applicationUrl = `${SCHEME}://app/`
|
||||
let hostUrl: string | undefined
|
||||
@@ -767,7 +774,12 @@ async function main(): Promise<void> {
|
||||
{ role: 'unhide', label: currentDesktopLocale().messages.showAllApplications }, { type: 'separator' }]
|
||||
: []
|
||||
const applicationItems = (): MenuItemConstructorOptions[] => [
|
||||
{ label: currentDesktopLocale().messages.aboutMenu, role: 'about' },
|
||||
// Windows has no system About panel; Electron's fallback is a plain
|
||||
// message box, so the shell shows its own dimmed dialog instead.
|
||||
process.platform === 'win32'
|
||||
? { label: currentDesktopLocale().messages.aboutMenu,
|
||||
click: () => { void showAbout().catch((error: unknown) => { console.error(error) }) } }
|
||||
: { label: currentDesktopLocale().messages.aboutMenu, role: 'about' },
|
||||
{ type: 'separator' },
|
||||
{ label: currentDesktopLocale().messages.checkUpdatesMenu, click: () => { void openUpdatePrompt(true) } },
|
||||
...development ? [
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"en-US": {
|
||||
"darwin:en-US": {
|
||||
"menu": [
|
||||
{ "label": "About DeepSeek Harness", "role": "about" },
|
||||
{ "type": "separator" }
|
||||
@@ -12,7 +12,7 @@
|
||||
"iconPath": "<app icon>"
|
||||
}
|
||||
},
|
||||
"zh-CN": {
|
||||
"darwin:zh-CN": {
|
||||
"menu": [
|
||||
{ "label": "关于 DeepSeek Harness", "role": "about" },
|
||||
{ "type": "separator" }
|
||||
@@ -24,5 +24,31 @@
|
||||
"copyright": "",
|
||||
"iconPath": "<app icon>"
|
||||
}
|
||||
},
|
||||
"win32:zh-CN": {
|
||||
"menu": [
|
||||
{ "label": "关于 DeepSeek Harness" },
|
||||
{ "type": "separator" }
|
||||
],
|
||||
"options": {
|
||||
"applicationName": "DeepSeek Harness",
|
||||
"applicationVersion": "1.0.0",
|
||||
"version": "",
|
||||
"copyright": "",
|
||||
"iconPath": "<app icon>"
|
||||
}
|
||||
},
|
||||
"win32:en-US": {
|
||||
"menu": [
|
||||
{ "label": "About DeepSeek Harness" },
|
||||
{ "type": "separator" }
|
||||
],
|
||||
"options": {
|
||||
"applicationName": "DeepSeek Harness",
|
||||
"applicationVersion": "1.0.0",
|
||||
"version": "",
|
||||
"copyright": "",
|
||||
"iconPath": "<app icon>"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -405,7 +405,7 @@ describe('desktop main startup', () => {
|
||||
['darwin', false, 'zh-CN'],
|
||||
['win32', true, 'zh-CN'],
|
||||
['win32', false, 'en-US'],
|
||||
] as const)('offers the native About panel before other commands on %s (packaged=%s, locale=%s)', async (platform, packaged, locale) => {
|
||||
] as const)('offers the About command before other commands on %s (packaged=%s, locale=%s)', async (platform, packaged, locale) => {
|
||||
vi.stubGlobal('process', { ...process, platform })
|
||||
harness.app.isPackaged = packaged
|
||||
vi.spyOn(harness.app, 'getLocale').mockReturnValue(locale)
|
||||
@@ -415,9 +415,26 @@ describe('desktop main startup', () => {
|
||||
const submenu = applicationMenuItems()
|
||||
const options = harness.app.setAboutPanelOptions.mock.calls[0]![0]
|
||||
const expected = JSON.parse(readFileSync(new URL('./expected/about-panel.json', import.meta.url), 'utf8')) as Record<string, unknown>
|
||||
expect({ menu: submenu.slice(0, 2), options: { ...options, iconPath: '<app icon>' } }).toEqual(expected[locale])
|
||||
const [about, separator] = submenu
|
||||
expect({ menu: [{ label: about!.label, role: about!.role }, separator], options: { ...options, iconPath: '<app icon>' } })
|
||||
.toEqual(expected[`${platform}:${locale}`])
|
||||
expect(options.iconPath).toBe(packaged ? join('desktop-test-resources', 'icon.png')
|
||||
: join('desktop-test-app', 'resources', 'icon-windows.png'))
|
||||
if (platform !== 'win32') { expect(about!.click).toBeUndefined(); return }
|
||||
// Windows reuses the dimmed update dialog because Electron's fallback is a bare message box.
|
||||
harness.dialog.showMessageBox.mockResolvedValueOnce({ response: 0 })
|
||||
;(about!.click as () => void)()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
const zh = locale === 'zh-CN'
|
||||
expect(harness.dialog.showMessageBox).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||
type: 'info', title: zh ? '关于 DeepSeek Harness' : 'About DeepSeek Harness', message: 'DeepSeek Harness',
|
||||
detail: zh ? '版本 V1.0.0' : 'Version V1.0.0', buttons: [zh ? '确定' : 'OK'], cancelId: 0,
|
||||
}))
|
||||
// A dialog that cannot open is logged, not surfaced as an unhandled rejection.
|
||||
harness.dialog.showMessageBox.mockRejectedValueOnce(new Error('overlay unavailable'))
|
||||
;(about!.click as () => void)()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(console.error).toHaveBeenLastCalledWith(expect.objectContaining({ message: 'overlay unavailable' }))
|
||||
})
|
||||
|
||||
it('shows one explained startup login before Host readiness and joins concurrent checks without reopening it', async () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-web-frontend",
|
||||
"description": "Web application entry: vite build over the @deepseek-ai/dsh-client-web shell library; dist/ served by apps/cli's dsh web",
|
||||
"version": "0.1.6-alpha.2",
|
||||
"version": "0.1.7-alpha.1",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { Browser, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import { captureStableAria, compareOrRefreshGolden, launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold } from './scaffold.ts'
|
||||
import { openSettingsFromAccountMenu, ZH_BROWSER_LOCALE, connectFreshWorkspaceZh, saveFailureShot } from './support.ts'
|
||||
import { openSettings, ZH_BROWSER_LOCALE, connectFreshWorkspaceZh, saveFailureShot } from './support.ts'
|
||||
|
||||
const EXPECTED = fileURLToPath(new URL('./expected/agent-preset-authoring', import.meta.url))
|
||||
const mode = webSnapshotMode()
|
||||
@@ -21,7 +21,7 @@ describe('web e2e: preset roster guidance', () => {
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE })
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
|
||||
await openSettingsFromAccountMenu(page, 'zh')
|
||||
await openSettings(page, 'zh')
|
||||
await page.getByRole('dialog', { name: '设置' }).getByRole('button', { name: 'Agent 预设' }).click()
|
||||
await page.getByRole('heading', { name: 'Agent 预设' }).waitFor()
|
||||
}, 120_000)
|
||||
@@ -61,7 +61,7 @@ describe('web e2e: preset roster guidance', () => {
|
||||
// so the flow needs a connected workspace to enter.
|
||||
await page.getByRole('dialog', { name: '设置' }).getByRole('button', { name: '关闭', exact: true }).last().click()
|
||||
await connectFreshWorkspaceZh(page, scaffold.workspaceCwd)
|
||||
await openSettingsFromAccountMenu(page, 'zh')
|
||||
await openSettings(page, 'zh')
|
||||
const settings = page.getByRole('dialog', { name: '设置' })
|
||||
await settings.getByRole('button', { name: 'Agent 预设' }).click()
|
||||
await settings.getByRole('button', { name: '让 Agent 帮我创建预设模式', exact: true }).click()
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
captureStableAria, compareOrRefreshGolden, launchWebScaffold, seedSession, watchConsole,
|
||||
webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { openSettingsFromAccountMenu,
|
||||
import { openSettings,
|
||||
connectFreshWorkspace, newEnglishPage, saveFailureShot, writeComposerDraft,
|
||||
} from './support.ts'
|
||||
|
||||
@@ -254,7 +254,7 @@ describe('web e2e: agent-preset selection', () => {
|
||||
await connectFreshWorkspace(page, scaffold.workspaceCwd)
|
||||
await page.getByRole('button', { name: 'Standard mode', exact: true }).waitFor({ timeout: 10_000 })
|
||||
|
||||
await openSettingsFromAccountMenu(page, 'en')
|
||||
await openSettings(page, 'en')
|
||||
const dialog = page.getByRole('dialog', { name: 'Settings' })
|
||||
await dialog.getByRole('button', { name: 'Agent presets' }).click()
|
||||
const toggle = dialog.getByRole('switch', { name: 'Choose a mode for new tasks' })
|
||||
@@ -347,7 +347,7 @@ describe('web e2e: agent-preset selection', () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-agent-preset-disabled'))
|
||||
await expect.poll(() => livePreset(scaffold), { timeout: 15_000 }).toBe('standard')
|
||||
|
||||
await openSettingsFromAccountMenu(page, 'en')
|
||||
await openSettings(page, 'en')
|
||||
const dialog = page.getByRole('dialog', { name: 'Settings' })
|
||||
await dialog.getByRole('button', { name: 'Agent presets' }).click()
|
||||
await dialog.getByRole('button', { name: 'Set as new task default: Minimal mode' }).click()
|
||||
@@ -364,7 +364,7 @@ describe('web e2e: agent-preset selection', () => {
|
||||
|
||||
// The switch controls availability only: re-enabling restores the saved
|
||||
// default and aligns this same still-blank task with it.
|
||||
await openSettingsFromAccountMenu(page, 'en')
|
||||
await openSettings(page, 'en')
|
||||
const reopened = page.getByRole('dialog', { name: 'Settings' })
|
||||
await reopened.getByRole('button', { name: 'Agent presets' }).click()
|
||||
const reopenedToggle = reopened.getByRole('switch', { name: 'Choose a mode for new tasks' })
|
||||
|
||||
@@ -158,7 +158,7 @@ describe('web e2e: Agent Teams panel', () => {
|
||||
expect(await trigger.evaluate(element => element === document.activeElement)).toBe(true)
|
||||
await page.keyboard.press('Enter')
|
||||
await panel.waitFor()
|
||||
const outside = page.getByRole('button', { name: 'Account menu', exact: true })
|
||||
const outside = page.getByRole('button', { name: 'Settings', exact: true })
|
||||
await outside.focus()
|
||||
await panel.waitFor({ state: 'detached' })
|
||||
expect(await outside.evaluate(element => element === document.activeElement)).toBe(true)
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
fixtureUserPrompts, launchWebScaffold, recordFixture, watchConsole,
|
||||
webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { openSettingsFromAccountMenu, connectFreshWorkspaceZh, ZH_BROWSER_LOCALE } from './support.ts'
|
||||
import { openSettings, connectFreshWorkspaceZh, ZH_BROWSER_LOCALE } from './support.ts'
|
||||
|
||||
const DIR = fileURLToPath(new URL('../../../snapshots/web/changed-files-turn', import.meta.url))
|
||||
const FIXTURE = join(DIR, 'session.v3.jsonl')
|
||||
@@ -136,7 +136,7 @@ describe('web e2e: a git workspace turn ends with its changed files', () => {
|
||||
expect(await header.getByRole('tablist').count()).toBe(0)
|
||||
const compactHeight = await header.evaluate(element => element.getBoundingClientRect().height)
|
||||
expect(compactHeight).toBeLessThan(60)
|
||||
await openSettingsFromAccountMenu(page, 'zh')
|
||||
await openSettings(page, 'zh')
|
||||
const settings = page.getByRole('dialog', { name: '设置' })
|
||||
await settings.getByRole('switch', { name: '开发者工具' }).click()
|
||||
await expect.poll(() => settings.getByRole('switch', { name: '开发者工具' }).getAttribute('aria-checked')).toBe('true')
|
||||
@@ -482,6 +482,54 @@ describe('web e2e: a git workspace turn ends with its changed files', () => {
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
})
|
||||
|
||||
it.skipIf(MODE === 'record')('keeps the file menu aligned while dragging the floating review', async () => {
|
||||
const column = page.locator('[data-rightbar-col]')
|
||||
const tab = column.locator('[data-dockkit-tab]').filter({ hasText: '第 1 轮改动' })
|
||||
const tabBox = await tab.boundingBox()
|
||||
if (tabBox === null) throw new Error('review tab is not rendered')
|
||||
await page.mouse.move(tabBox.x + 6, tabBox.y + tabBox.height / 2)
|
||||
await page.mouse.down()
|
||||
try {
|
||||
await page.mouse.move(360, 140, { steps: 8 })
|
||||
} finally {
|
||||
await page.mouse.up()
|
||||
}
|
||||
const floating = page.locator('[data-dockkit-float]').filter({ has: page.locator('[data-changes-review]') })
|
||||
await floating.waitFor({ state: 'visible' })
|
||||
const selector = floating.getByRole('button', { name: '选择要查看的文件' })
|
||||
await selector.click()
|
||||
const menu = page.getByRole('menu')
|
||||
await menu.waitFor({ state: 'visible' })
|
||||
const grip = floating.locator('[data-dockkit-float-grip]')
|
||||
for (const delta of [{ x: 120, y: 60 }, { x: -60, y: -30 }]) {
|
||||
const handle = await grip.boundingBox()
|
||||
const before = await selector.boundingBox()
|
||||
if (handle === null || before === null) throw new Error('floating review controls are not rendered')
|
||||
const start = { x: handle.x + 12, y: handle.y + handle.height / 2 }
|
||||
await page.mouse.move(start.x, start.y)
|
||||
await page.mouse.down()
|
||||
try {
|
||||
await page.mouse.move(start.x + delta.x, start.y + delta.y, { steps: 8 })
|
||||
await expect.poll(async () => {
|
||||
const anchor = await selector.boundingBox()
|
||||
const list = await menu.boundingBox()
|
||||
if (anchor === null || list === null) throw new Error('review file menu disappeared during drag')
|
||||
return Math.max(
|
||||
Math.abs(anchor.x - before.x - delta.x), Math.abs(anchor.y - before.y - delta.y),
|
||||
Math.abs(list.x - anchor.x), Math.abs(list.y - anchor.y - anchor.height - 4),
|
||||
)
|
||||
}).toBeLessThanOrEqual(1)
|
||||
} finally {
|
||||
await page.mouse.up()
|
||||
}
|
||||
}
|
||||
await page.keyboard.press('Escape')
|
||||
await floating.locator('[data-dockkit-float-dock]').click()
|
||||
await floating.waitFor({ state: 'detached' })
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
})
|
||||
|
||||
it('keeps edge gestures inside either diff column and accepts reverse scrolling', async () => {
|
||||
const review = page.locator('[data-changes-review]')
|
||||
await review.getByRole('button', { name: '自动换行' }).click()
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
webSnapshotMode,
|
||||
type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { openSettingsFromAccountMenu, expandTurnProcesses, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
import { openSettings, expandTurnProcesses, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/clickable-links-gallery', import.meta.url))
|
||||
const UI_EXPECTED = fileURLToPath(new URL('./expected/clickable-links-gallery/ui.expected.md', import.meta.url))
|
||||
@@ -453,7 +453,7 @@ describe('web e2e: clickable links gallery', () => {
|
||||
await markdown.locator(`a[href="${HTTP_URL}"]`).click()
|
||||
await expect.poll(() => browserAddress.inputValue()).toBe(HTTP_URL)
|
||||
|
||||
await openSettingsFromAccountMenu(page, 'en')
|
||||
await openSettings(page, 'en')
|
||||
await page.getByRole('button', { name: 'Built-in browser', exact: true }).click()
|
||||
await page.getByRole('menuitem', { name: 'New browser tab', exact: true }).click()
|
||||
await expect.poll(() => scaffold.ctx.settings.describe().find(row => row.ns === 'ui-chat')?.value).toMatchObject({ linkOpening: 'new-tab' })
|
||||
@@ -470,7 +470,7 @@ describe('web e2e: clickable links gallery', () => {
|
||||
}
|
||||
|
||||
await page.reload()
|
||||
await openSettingsFromAccountMenu(page, 'en')
|
||||
await openSettings(page, 'en')
|
||||
await page.getByRole('button', { name: 'New browser tab', exact: true }).click()
|
||||
await page.getByRole('menuitem', { name: 'Built-in browser', exact: true }).click()
|
||||
await expect.poll(() => scaffold.ctx.settings.describe().find(row => row.ns === 'ui-chat')?.value).toMatchObject({ linkOpening: 'sidebar' })
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, launchWebScaffold,
|
||||
seedSession, watchConsole, webSnapshotMode,
|
||||
} from './scaffold.ts'
|
||||
import { openSettingsFromAccountMenu, newEnglishPage, saveFailureShot, ZH_BROWSER_LOCALE } from './support.ts'
|
||||
import { openSettings, newEnglishPage, saveFailureShot, ZH_BROWSER_LOCALE } from './support.ts'
|
||||
|
||||
const FIXTURE = fileURLToPath(new URL('./fixtures/plugins/fixture-live-client', import.meta.url))
|
||||
const EXPECTED = fileURLToPath(new URL('./expected/client-plugin-live', import.meta.url))
|
||||
@@ -19,7 +19,7 @@ const SESSION_TITLE = 'Session action extension'
|
||||
|
||||
async function openInventory(page: Page, url: string) {
|
||||
await page.goto(url, { waitUntil: 'load' })
|
||||
await openSettingsFromAccountMenu(page, 'zh')
|
||||
await openSettings(page, 'zh')
|
||||
const dialog = page.getByRole('dialog', { name: '设置' })
|
||||
await dialog.getByRole('button', { name: '内置插件', exact: true }).click()
|
||||
await dialog.getByRole('searchbox', { name: '搜索插件' }).waitFor()
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
captureStableAria, compareOrRefreshGolden, launchWebScaffold,
|
||||
watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { openSettingsFromAccountMenu, connectFreshWorkspaceZh, saveFailureShot, ZH_BROWSER_LOCALE } from './support.ts'
|
||||
import { openSettings, connectFreshWorkspaceZh, saveFailureShot, ZH_BROWSER_LOCALE } from './support.ts'
|
||||
|
||||
const EXPECTED = fileURLToPath(new URL('./expected/deepseek-messages-settings/', import.meta.url))
|
||||
|
||||
@@ -39,13 +39,11 @@ describe.skipIf(webSnapshotMode() === 'record')('web e2e: DeepSeek Messages sett
|
||||
expect(scaffold.ctx.llm.listProviders()).toContainEqual({ id: 'deepseek-official', name: 'DeepSeek' })
|
||||
expect(scaffold.ctx.llm.listProviders().filter(provider => provider.id === 'deepseek-official')).toHaveLength(1)
|
||||
expect(scaffold.ctx.agentDefaultModel.currentSelection()).toEqual({ provider: 'deepseek-official', model: 'deepseek-flash' })
|
||||
await page.getByRole('dialog', { name: '开始你的创作' })
|
||||
.getByRole('button', { name: '添加 API Key', exact: true }).click()
|
||||
const onboarding = page.getByRole('dialog', { name: '添加一个 API Key 开始使用' })
|
||||
await onboarding.getByLabel('API 密钥', { exact: true }).fill('sk-messages-onboarding')
|
||||
await onboarding.getByRole('button', { name: '保存并继续' }).click()
|
||||
await onboarding.waitFor({ state: 'detached' })
|
||||
await openSettingsFromAccountMenu(page, 'zh')
|
||||
await openSettings(page, 'zh')
|
||||
const dialog = page.getByRole('dialog', { name: '设置', exact: true })
|
||||
await dialog.getByRole('button', { name: '模型', exact: true }).click()
|
||||
await dialog.getByText('DeepSeek', { exact: true }).waitFor()
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
|
||||
launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { openSettingsFromAccountMenu, saveFailureShot } from './support.ts'
|
||||
import { openSettings, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/desktop-locale', import.meta.url))
|
||||
const MODE = webSnapshotMode()
|
||||
@@ -53,7 +53,7 @@ describe.skipIf(MODE === 'record')('web e2e: native and Client locale preference
|
||||
it('uses OS languages without saving them, then shares an explicit choice across reloads', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-desktop-locale'))
|
||||
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
|
||||
await openSettingsFromAccountMenu(page, 'zh')
|
||||
await openSettings(page, 'zh')
|
||||
const zhDialog = page.getByRole('dialog', { name: '设置', exact: true })
|
||||
await zhDialog.getByRole('button', { name: '中文', exact: true }).waitFor()
|
||||
await expect.poll(() => reported).toEqual(['zh'])
|
||||
@@ -73,7 +73,7 @@ describe.skipIf(MODE === 'record')('web e2e: native and Client locale preference
|
||||
reported.length = 0
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
acknowledgeReloadConnectionLoss(tripwire, warningsBefore)
|
||||
await openSettingsFromAccountMenu(page, 'en')
|
||||
await openSettings(page, 'en')
|
||||
await enDialog.getByRole('button', { name: 'English', exact: true }).waitFor()
|
||||
await expect.poll(() => reported).toEqual(['en'])
|
||||
expect(await page.evaluate(() => document.documentElement.lang)).toBe('en')
|
||||
|
||||
@@ -73,7 +73,7 @@ describe('web e2e: Desktop update workspace chrome', () => {
|
||||
|
||||
const progress = presentDesktopUpdate({ phase: 'downloading', version, percent: 58 }, en)
|
||||
await publish(progress)
|
||||
const downloading = page.getByRole('button', { name: '58%…', exact: true })
|
||||
const downloading = page.getByRole('button', { name: '58%', exact: true })
|
||||
await downloading.waitFor()
|
||||
expect(await downloading.getAttribute('aria-disabled')).toBe('true')
|
||||
expect(await downloading.locator('svg').count()).toBe(1)
|
||||
@@ -86,20 +86,20 @@ describe('web e2e: Desktop update workspace chrome', () => {
|
||||
const expand = locale === 'zh-CN' ? '打开侧边栏' : 'Open sidebar'
|
||||
await page.getByRole('button', { name: collapse, exact: true }).click()
|
||||
const toggle = page.getByRole('button', { name: expand, exact: true })
|
||||
const badge = toggle.getByRole('img', { name: '58%…', exact: true })
|
||||
const badge = toggle.getByRole('img', { name: '58%', exact: true })
|
||||
await badge.waitFor()
|
||||
await expect.poll(() => downloading.count()).toBe(0)
|
||||
expect(await page.getByRole('img', { name: '58%…', exact: true }).count()).toBe(1)
|
||||
const blue = await badge.evaluate(element => getComputedStyle(element).backgroundColor)
|
||||
expect(await page.getByRole('img', { name: '58%', exact: true }).count()).toBe(1)
|
||||
const updateBackground = await badge.evaluate(element => getComputedStyle(element).backgroundColor)
|
||||
await page.screenshot({ path: join(evidence, 'collapsed.png') })
|
||||
|
||||
const error = presentDesktopUpdate({ phase: 'error', version, failedOperation: 'download', message: 'HTTP 503' }, en)
|
||||
await publish(error)
|
||||
const errorBadge = toggle.getByRole('img', { name: retryLabel, exact: true })
|
||||
await errorBadge.waitFor()
|
||||
expect(await errorBadge.getAttribute('data-error')).toBe('true')
|
||||
const red = await errorBadge.evaluate(element => getComputedStyle(element).backgroundColor)
|
||||
expect(red).not.toBe(blue)
|
||||
// A failure keeps the update palette; only the label distinguishes it.
|
||||
const failedBackground = await errorBadge.evaluate(element => getComputedStyle(element).backgroundColor)
|
||||
expect(failedBackground).toBe(updateBackground)
|
||||
await errorBadge.hover()
|
||||
const tooltip = page.getByRole('tooltip', { name: errorDetail, exact: true })
|
||||
await expect.poll(() => tooltip.evaluateAll(elements => elements.map(element => getComputedStyle(element).opacity))).toEqual(['1'])
|
||||
@@ -132,7 +132,7 @@ describe('web e2e: Desktop update workspace chrome', () => {
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
expect(await page.evaluate(() => (window as FixtureWindow).updateFixture.listeners.size)).toBe(1)
|
||||
await writeFile(join(evidence, 'result.json'), JSON.stringify({ locale, geometry, blue, red,
|
||||
await writeFile(join(evidence, 'result.json'), JSON.stringify({ locale, geometry, updateBackground, failedBackground,
|
||||
passed: true, explicitActions: 3, carrier: 'substituted', host: 'real Web composition',
|
||||
electron: false, installerExecuted: false }, null, 2) + '\n')
|
||||
console.log(`Desktop workspace chrome evidence: ${evidence}`)
|
||||
|
||||
@@ -4,7 +4,7 @@ import { join } from 'node:path'
|
||||
import { chromium } from 'playwright'
|
||||
import { expect, it, onTestFinished } from 'vitest'
|
||||
import { launchWebScaffold } from './scaffold.ts'
|
||||
import { openSettingsFromAccountMenu, newEnglishPage } from './support.ts'
|
||||
import { openSettings, newEnglishPage } from './support.ts'
|
||||
|
||||
it('persists developer tools in the Host settings document and restores the accepted choice', async () => {
|
||||
const scaffold = await launchWebScaffold()
|
||||
@@ -13,7 +13,7 @@ it('persists developer tools in the Host settings document and restores the acce
|
||||
onTestFinished(() => browser.close())
|
||||
const page = await newEnglishPage(browser)
|
||||
await page.goto(scaffold.authenticatedUrl)
|
||||
await openSettingsFromAccountMenu(page, 'en')
|
||||
await openSettings(page, 'en')
|
||||
const toggle = page.getByRole('switch', { name: 'Developer tools' })
|
||||
expect(await toggle.getAttribute('aria-checked')).toBe('true')
|
||||
expect(await readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')).not.toMatch(/id: ui-settings(?:\r?\n|$)/)
|
||||
@@ -21,7 +21,7 @@ it('persists developer tools in the Host settings document and restores the acce
|
||||
await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('false')
|
||||
expect(await readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')).toContain('enabled: false')
|
||||
await page.reload()
|
||||
await openSettingsFromAccountMenu(page, 'en')
|
||||
await openSettings(page, 'en')
|
||||
await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('false')
|
||||
|
||||
await page.getByRole('button', { name: 'Agent presets', exact: true }).click()
|
||||
|
||||
@@ -14,7 +14,7 @@ import { excelDrawingFixture } from '../../../packages/client/ui-sidebar-documen
|
||||
import { xlsFixture } from '../../../packages/client/ui-sidebar-documentpreview/tests/xls-fixture.ts'
|
||||
import { pdfFixture, selectionPdfFixture } from '../../../packages/client/ui-sidebar-documentpreview/tests/pdf-fixture.ts'
|
||||
import { assertFixtureInventory, compareOrRefreshGolden, launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold } from './scaffold.ts'
|
||||
import { openSettingsFromAccountMenu, connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
import { openSettings, connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const FIXTURE = fileURLToPath(new URL('../../../snapshots/web/lifecycle-chrome/session.v3.jsonl', import.meta.url))
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/document-preview', import.meta.url))
|
||||
@@ -425,7 +425,7 @@ else process.exit(1);
|
||||
`- Local script: ${await basicHtml.locator('#local-result').innerText()}`,
|
||||
`- Network requests: ${previewNetworkRequests}`,
|
||||
].join('\n'))
|
||||
await openSettingsFromAccountMenu(page, 'en')
|
||||
await openSettings(page, 'en')
|
||||
const settings = page.getByRole('dialog', { name: 'Settings' })
|
||||
await settings.getByRole('switch', { name: 'Developer tools' }).click()
|
||||
await expect.poll(() => settings.getByRole('switch', { name: 'Developer tools' }).getAttribute('aria-checked')).toBe('true')
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
- dialog "开始你的创作":
|
||||
- heading "开始你的创作" [level=2]
|
||||
- button "关闭"
|
||||
- paragraph: 登录后即可创建、编辑和分享你的设计项目,所有内容在本地保存。
|
||||
- button "添加 API Key"
|
||||
- button "登录"
|
||||
@@ -1,5 +1,5 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy": /user-invoke-demo @"meeting notes.md" an
|
||||
- navigation "Session hierarchy": /user-invoke-demo @"meeting notes-this-i
|
||||
- text: Standard mode
|
||||
- button "More actions"
|
||||
- button "Open right sidebar"
|
||||
@@ -7,7 +7,7 @@
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- button "/user-invoke-demo"
|
||||
- button "meeting notes.md"
|
||||
- button "meeting notes-this-is-a-very-long-filename-for-testing-user-message-file-references-and-preview-layout.md"
|
||||
- text: and confirm the fixture wiring {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy": /user-invoke-demo @"meeting notes.md" an
|
||||
- navigation "Session hierarchy": /user-invoke-demo @"meeting notes-this-i
|
||||
- text: Standard mode
|
||||
- button "More actions"
|
||||
- button "Open right sidebar"
|
||||
@@ -7,7 +7,7 @@
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- button "/user-invoke-demo"
|
||||
- button "meeting notes.md"
|
||||
- button "meeting notes-this-is-a-very-long-filename-for-testing-user-message-file-references-and-preview-layout.md"
|
||||
- text: and confirm the fixture wiring {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, parseSeedFixture, renderSeedFixture, seedSession, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { openSettingsFromAccountMenu, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
import { openSettings, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/message-actions', import.meta.url))
|
||||
// Borrowed read-only: this scenario needs any settled user+assistant pair, not
|
||||
@@ -321,7 +321,7 @@ describe('web e2e: message IconActions and clocks on settled history', () => {
|
||||
it.skipIf(MODE === 'record')('persists performance detail and hides statistics in Compact', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-performance-usage'))
|
||||
const stats = page.locator('[data-composer-stats]')
|
||||
await openSettingsFromAccountMenu(page, 'en')
|
||||
await openSettings(page, 'en')
|
||||
const dialog = page.getByRole('dialog', { name: 'Settings', exact: true })
|
||||
const row = dialog.getByText('Performance & usage', { exact: true }).locator('../..')
|
||||
await row.getByRole('button', { name: 'Detailed', exact: true }).click()
|
||||
@@ -336,7 +336,7 @@ describe('web e2e: message IconActions and clocks on settled history', () => {
|
||||
await compareOrRefreshGolden(join(SNAPSHOT_DIR, 'compact.expected.md'), await captureStableAria(page, '[data-composer-stats]', scaffold.workspaceCwd), MODE)
|
||||
const warningStart = tripwire.warnings.length
|
||||
await page.reload()
|
||||
await openSettingsFromAccountMenu(page, 'en')
|
||||
await openSettings(page, 'en')
|
||||
await row.getByRole('button', { name: 'Compact', exact: true }).waitFor()
|
||||
acknowledgeReloadConnectionLoss(tripwire, warningStart)
|
||||
await row.getByRole('button', { name: 'Compact', exact: true }).click()
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
captureStableAria, compareOrRefreshGolden, launchWebScaffold,
|
||||
watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { openSettingsFromAccountMenu, saveFailureShot, ZH_BROWSER_LOCALE } from './support.ts'
|
||||
import { openSettings, saveFailureShot, ZH_BROWSER_LOCALE } from './support.ts'
|
||||
|
||||
const EXPECTED = fileURLToPath(new URL('./expected/models-settings-recovery/stored-error.expected.md', import.meta.url))
|
||||
const FAILURE = 'llm-pi-ai: provider "openrouter" model "111" needs an api; '
|
||||
@@ -37,7 +37,7 @@ describe('web e2e: repairs a stored provider after catalog drift', () => {
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE })
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
|
||||
await openSettingsFromAccountMenu(page, 'zh')
|
||||
await openSettings(page, 'zh')
|
||||
const dialog = page.getByRole('dialog', { name: '设置' })
|
||||
await dialog.getByRole('button', { name: '模型', exact: true }).click()
|
||||
await dialog.getByText(FAILURE, { exact: true }).waitFor()
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
|
||||
launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { openSettingsFromAccountMenu, ZH_BROWSER_LOCALE, saveFailureShot } from './support.ts'
|
||||
import { openSettings, ZH_BROWSER_LOCALE, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/models-settings', import.meta.url))
|
||||
const EMPTY_EXPECTED = join(SNAPSHOT_DIR, 'empty.expected.md')
|
||||
@@ -72,7 +72,7 @@ describe('web e2e: Models settings page configures a dormant provider', () => {
|
||||
|
||||
it('opens the add card over the dormant directory vocabulary', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-models-empty'))
|
||||
await openSettingsFromAccountMenu(page, 'zh')
|
||||
await openSettings(page, 'zh')
|
||||
const dialog = page.getByRole('dialog', { name: '设置' })
|
||||
await dialog.waitFor({ timeout: 10_000 })
|
||||
await dialog.getByRole('button', { name: '模型', exact: true }).click()
|
||||
|
||||
@@ -16,11 +16,10 @@ import {
|
||||
WELCOME_NOTICE_ACK_FIELD, WELCOME_NOTICE_COPY, WELCOME_NOTICE_SETTINGS_NAMESPACE,
|
||||
WELCOME_NOTICE_VERSION,
|
||||
} from './scaffold.ts'
|
||||
import { openSettingsFromAccountMenu, ZH_BROWSER_LOCALE, connectFreshWorkspaceZh, saveFailureShot } from './support.ts'
|
||||
import { openSettings, ZH_BROWSER_LOCALE, connectFreshWorkspaceZh, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/onboarding-deepseek-config', import.meta.url))
|
||||
const WELCOME_EXPECTED = join(SNAPSHOT_DIR, 'welcome.expected.md')
|
||||
const SIGN_IN_EXPECTED = join(SNAPSHOT_DIR, 'sign-in.expected.md')
|
||||
const MISSING_EXPECTED = join(SNAPSHOT_DIR, 'missing.expected.md')
|
||||
const MODELS_EXPECTED = join(SNAPSHOT_DIR, 'models.expected.md')
|
||||
const DEFAULT_MODELS_EXPECTED = join(SNAPSHOT_DIR, 'default-models.expected.md')
|
||||
@@ -73,13 +72,10 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
|
||||
await welcome.getByRole('button', { name: WELCOME_NOTICE_COPY.zh.continueLabel }).click()
|
||||
await welcome.waitFor({ state: 'detached', timeout: 15_000 })
|
||||
|
||||
const signIn = page.getByRole('dialog', { name: '开始你的创作' })
|
||||
await signIn.waitFor({ timeout: 15_000 })
|
||||
await compareOrRefreshGolden(SIGN_IN_EXPECTED,
|
||||
await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd), MODE)
|
||||
await signIn.getByRole('button', { name: '添加 API Key', exact: true }).click()
|
||||
const credentialStep = page.getByRole('dialog', { name: '添加一个 API Key 开始使用' })
|
||||
await credentialStep.waitFor({ timeout: 15_000 })
|
||||
expect(await page.getByRole('dialog', { name: '开始你的创作' }).count()).toBe(0)
|
||||
expect(await page.getByRole('button', { name: '账号菜单', exact: true }).count()).toBe(0)
|
||||
const keyInput = credentialStep.getByLabel('API 密钥', { exact: true })
|
||||
await keyInput.waitFor({ timeout: 10_000 })
|
||||
const initial = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd)
|
||||
@@ -102,7 +98,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
|
||||
|
||||
// The ordinary Models surface reuses the refreshed join and exposes the
|
||||
// configured write-only placeholder without a reload.
|
||||
await openSettingsFromAccountMenu(page, 'zh')
|
||||
await openSettings(page, 'zh')
|
||||
const settings = page.getByRole('dialog', { name: '设置' })
|
||||
await settings.waitFor({ timeout: 10_000 })
|
||||
await settings.getByRole('button', { name: '模型', exact: true }).click()
|
||||
@@ -203,7 +199,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-onboarding-deepseek-models'))
|
||||
// Opened here rather than inherited: the credential test reloads the page
|
||||
// after configuring the key, so nothing carries an open dialog across.
|
||||
await openSettingsFromAccountMenu(page, 'zh')
|
||||
await openSettings(page, 'zh')
|
||||
const settings = page.getByRole('dialog', { name: '设置' })
|
||||
await settings.waitFor({ timeout: 10_000 })
|
||||
await settings.getByRole('button', { name: '模型', exact: true }).click()
|
||||
@@ -292,7 +288,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
|
||||
it('keeps the fixture inventory closed', async () => {
|
||||
await assertFixtureInventory(
|
||||
SNAPSHOT_DIR,
|
||||
['welcome.expected.md', 'sign-in.expected.md', 'missing.expected.md', 'models.expected.md', 'default-models.expected.md'],
|
||||
['welcome.expected.md', 'missing.expected.md', 'models.expected.md', 'default-models.expected.md'],
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
|
||||
launchWebScaffold, watchConsole, webSnapshotMode, WELCOME_NOTICE_COPY, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { openSettingsFromAccountMenu, ZH_BROWSER_LOCALE, saveFailureShot } from './support.ts'
|
||||
import { openSettings, ZH_BROWSER_LOCALE, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/onboarding-native', import.meta.url))
|
||||
const MODE = webSnapshotMode()
|
||||
@@ -52,7 +52,14 @@ describe.skipIf(MODE === 'record').each([false, true])('web e2e: native credenti
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
acknowledgeReloadConnectionLoss(tripwire, warningsBefore)
|
||||
}
|
||||
await openSettingsFromAccountMenu(page, 'zh')
|
||||
const accountMenu = page.getByRole('button', { name: '账号菜单', exact: true })
|
||||
if (desktop) await accountMenu.waitFor()
|
||||
else {
|
||||
await page.getByRole('button', { name: '设置', exact: true }).waitFor()
|
||||
expect(await accountMenu.count()).toBe(0)
|
||||
expect(await page.getByRole('dialog', { name: '开始你的创作' }).count()).toBe(0)
|
||||
}
|
||||
await openSettings(page, 'zh')
|
||||
const settings = page.getByRole('dialog', { name: '设置', exact: true })
|
||||
await settings.getByRole('button', { name: '模型', exact: true }).click()
|
||||
await settings.getByLabel('API 密钥', { exact: true }).waitFor()
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
|
||||
launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { openSettingsFromAccountMenu, ZH_BROWSER_LOCALE, saveFailureShot } from './support.ts'
|
||||
import { openSettings, ZH_BROWSER_LOCALE, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/onboarding-usable-provider', import.meta.url))
|
||||
const DISMISSED_EXPECTED = join(SNAPSHOT_DIR, 'dismissed.expected.md')
|
||||
@@ -44,15 +44,14 @@ describe.skipIf(MODE === 'record')('web e2e: another usable provider ends first-
|
||||
|
||||
it('closes the setup card without discarding the add card beside it', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-onboarding-setup-card-cancel'))
|
||||
const signIn = page.getByRole('dialog', { name: '开始你的创作' })
|
||||
await signIn.waitFor({ timeout: 15_000 })
|
||||
await signIn.getByRole('button', { name: '添加 API Key', exact: true }).click()
|
||||
const credentialStep = page.getByRole('dialog', { name: CREDENTIAL_STEP })
|
||||
await credentialStep.waitFor({ timeout: 15_000 })
|
||||
expect(await page.getByRole('dialog', { name: '开始你的创作' }).count()).toBe(0)
|
||||
expect(await page.getByRole('button', { name: '账号菜单', exact: true }).count()).toBe(0)
|
||||
await credentialStep.getByRole('button', { name: '稍后配置' }).click()
|
||||
await credentialStep.waitFor({ state: 'detached', timeout: 15_000 })
|
||||
|
||||
await openSettingsFromAccountMenu(page, 'zh')
|
||||
await openSettings(page, 'zh')
|
||||
const settings = page.getByRole('dialog', { name: '设置' })
|
||||
await settings.waitFor({ timeout: 10_000 })
|
||||
// Dismissing the onboarding step leaves Settings closed, so enter the
|
||||
@@ -116,7 +115,7 @@ describe.skipIf(MODE === 'record')('web e2e: another usable provider ends first-
|
||||
|
||||
// The Models page agrees: DeepSeek stays a row rather than reopening its
|
||||
// setup card over a user who already has somewhere to send a request.
|
||||
await openSettingsFromAccountMenu(page, 'zh')
|
||||
await openSettings(page, 'zh')
|
||||
await settings.waitFor({ timeout: 10_000 })
|
||||
await settings.getByRole('button', { name: '模型', exact: true }).click()
|
||||
await settings.getByRole('button', { name: '编辑 DeepSeek (deepseek-official)' }).waitFor({ timeout: 10_000 })
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
SCAFFOLD_DEFAULTS_BUNDLE, assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
|
||||
launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { ZH_BROWSER_LOCALE, connectFreshWorkspaceZh, openSettingsFromAccountMenu, saveFailureShot } from './support.ts'
|
||||
import { ZH_BROWSER_LOCALE, connectFreshWorkspaceZh, openSettings, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/plugin-manager', import.meta.url))
|
||||
const MANAGER_EXPECTED = join(SNAPSHOT_DIR, 'manager.expected.md')
|
||||
@@ -64,7 +64,7 @@ describe('web e2e: plugin manager', () => {
|
||||
const source = language === 'en' ? '中文' : 'English'
|
||||
const target = language === 'en' ? 'English' : '中文'
|
||||
if (await page.getByRole('dialog', { name: settings }).count() === 0) {
|
||||
await openSettingsFromAccountMenu(page, language === 'en' ? 'zh' : 'en')
|
||||
await openSettings(page, language === 'en' ? 'zh' : 'en')
|
||||
}
|
||||
await page.getByRole('dialog', { name: settings }).getByRole('button', { name: source }).click()
|
||||
await page.getByRole('menuitem', { name: target }).click()
|
||||
|
||||
@@ -316,7 +316,6 @@ async function bootPreview(origin: string, browser: Browser): Promise<void> {
|
||||
const continueButton = page.getByRole('button', { name: 'Continue' })
|
||||
await continueButton.waitFor({ timeout: HERO_TIMEOUT_MS })
|
||||
await continueButton.click()
|
||||
await page.getByRole('button', { name: 'Add API Key', exact: true }).click()
|
||||
const configureLater = page.getByRole('button', { name: 'Configure later' })
|
||||
await configureLater.waitFor({ timeout: 30_000 })
|
||||
await configureLater.click()
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
|
||||
launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { openSettingsFromAccountMenu, ZH_BROWSER_LOCALE, saveFailureShot } from './support.ts'
|
||||
import { openSettings, ZH_BROWSER_LOCALE, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/settings-chrome', import.meta.url))
|
||||
const DIALOG_EXPECTED = join(SNAPSHOT_DIR, 'dialog.expected.md')
|
||||
@@ -59,15 +59,13 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
|
||||
it('opens the settings dialog, switches sections, and closes by every path', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-shell'))
|
||||
const trigger = page.getByRole('button', { name: '账号菜单', exact: true })
|
||||
expect(await trigger.getAttribute('aria-haspopup')).toBe('menu')
|
||||
const trigger = page.getByRole('button', { name: '设置', exact: true })
|
||||
expect(await trigger.getAttribute('aria-haspopup')).toBe('dialog')
|
||||
expect(await trigger.getAttribute('aria-expanded')).toBe('false')
|
||||
await trigger.click()
|
||||
expect(await trigger.getAttribute('aria-expanded')).toBe('true')
|
||||
await page.getByRole('menuitem', { name: '设置', exact: true }).click()
|
||||
const dialog = page.getByRole('dialog', { name: '设置' })
|
||||
await dialog.waitFor({ timeout: 10_000 })
|
||||
expect(await trigger.getAttribute('aria-expanded')).toBe('false')
|
||||
expect(await trigger.getAttribute('aria-expanded')).toBe('true')
|
||||
// General is active by default; Permission, Language and Appearance are functional.
|
||||
expect(await dialog.getByRole('button', { name: '通用设置' }).getAttribute('aria-current')).toBe('true')
|
||||
await dialog.getByRole('button', { name: '工作区内修改' }).waitFor({ timeout: 10_000 })
|
||||
@@ -186,7 +184,7 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
await expect.poll(() => page.getByRole('dialog', { name: '设置' }).count(), { timeout: 5_000 }).toBe(0)
|
||||
expect(await trigger.getAttribute('aria-expanded')).toBe('false')
|
||||
// Close path 2: the header close button (focus lands there on open).
|
||||
await openSettingsFromAccountMenu(page, 'zh')
|
||||
await openSettings(page, 'zh')
|
||||
await page.getByRole('dialog', { name: '设置' }).getByRole('button', { name: '关闭' }).click()
|
||||
await expect.poll(() => page.getByRole('dialog', { name: '设置' }).count(), { timeout: 5_000 }).toBe(0)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
@@ -198,7 +196,7 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
onTestFailed(() => saveFailureShot(overflowPage, 'web-e2e-settings-navigation'))
|
||||
const console = watchConsole(overflowPage)
|
||||
await overflowPage.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
|
||||
await openSettingsFromAccountMenu(overflowPage, 'zh')
|
||||
await openSettings(overflowPage, 'zh')
|
||||
const dialog = overflowPage.getByRole('dialog', { name: '设置', exact: true })
|
||||
const navigation = dialog.getByRole('navigation')
|
||||
const list = navigation.locator('[class*="_navList"]')
|
||||
@@ -242,7 +240,7 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
expect(existing.snapshotEvents().find(event => event.type === 'permission/preset')?.data)
|
||||
.toEqual({ preset: 'workspace-write' })
|
||||
|
||||
await openSettingsFromAccountMenu(page, 'zh')
|
||||
await openSettings(page, 'zh')
|
||||
const dialog = page.getByRole('dialog', { name: '设置' })
|
||||
await dialog.waitFor({ timeout: 10_000 })
|
||||
const selector = dialog.getByRole('button', { name: '工作区内修改' })
|
||||
@@ -309,7 +307,7 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
it('uses the persisted dark preference while plugins are still loading', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-boot-theme'))
|
||||
await page.emulateMedia({ colorScheme: 'light' })
|
||||
await openSettingsFromAccountMenu(page, 'zh')
|
||||
await openSettings(page, 'zh')
|
||||
const initialDialog = page.getByRole('dialog', { name: '设置' })
|
||||
const darkCube = initialDialog.getByRole('button', { name: '深色' })
|
||||
await selectTheme(darkCube, 'dark')
|
||||
@@ -355,7 +353,7 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
acknowledgeReloadConnectionLoss(tripwire, warningStart)
|
||||
await openSettingsFromAccountMenu(page, 'zh')
|
||||
await openSettings(page, 'zh')
|
||||
const restoredDialog = page.getByRole('dialog', { name: '设置' })
|
||||
const systemCube = restoredDialog.getByRole('button', { name: '跟随系统' })
|
||||
// The boot palette precedes the settings mirror's saved preference.
|
||||
@@ -409,7 +407,7 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
expect(light.faviconPaths).toEqual(['/favicon.svg'])
|
||||
expectThemeColorSynchronized(light)
|
||||
|
||||
await openSettingsFromAccountMenu(page, 'zh')
|
||||
await openSettings(page, 'zh')
|
||||
const dialog = page.getByRole('dialog', { name: '设置' })
|
||||
await dialog.waitFor({ timeout: 10_000 })
|
||||
const darkCube = dialog.getByRole('button', { name: '深色' })
|
||||
@@ -463,7 +461,7 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
}
|
||||
|
||||
// `system` follows the emulated OS scheme (dark stays dark, light clears).
|
||||
await openSettingsFromAccountMenu(page, 'zh')
|
||||
await openSettings(page, 'zh')
|
||||
const systemCube = page.getByRole('dialog', { name: '设置' }).getByRole('button', { name: '跟随系统' })
|
||||
await selectTheme(systemCube, 'system')
|
||||
await expect.poll(() => systemCube.getAttribute('aria-pressed'), { timeout: 5_000 }).toBe('true')
|
||||
@@ -525,7 +523,7 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
}
|
||||
expect(await readFontSize()).toBe('14px')
|
||||
expect(await readSecondaryFontSize()).toBe('13px')
|
||||
await openSettingsFromAccountMenu(page, 'zh')
|
||||
await openSettings(page, 'zh')
|
||||
const dialog = page.getByRole('dialog', { name: '设置' })
|
||||
await dialog.waitFor({ timeout: 10_000 })
|
||||
// The stepper reveals its arrows on hover; the up arrow steps 14 → 15 → 16.
|
||||
@@ -550,7 +548,7 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
expect(await readSecondaryFontSize()).toBe('14px')
|
||||
|
||||
// Restore the default for the specs that follow (and the dialog golden).
|
||||
await openSettingsFromAccountMenu(page, 'zh')
|
||||
await openSettings(page, 'zh')
|
||||
const restored = page.getByRole('dialog', { name: '设置' })
|
||||
await restored.waitFor({ timeout: 10_000 })
|
||||
await restored.getByText('16', { exact: true }).hover()
|
||||
@@ -565,7 +563,7 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
['detailed', '详细'], ['expanded', '完全展开'],
|
||||
] as const)('persists the %s work-details mode across reload', async (mode, label) => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-transcript-view'))
|
||||
await openSettingsFromAccountMenu(page, 'zh')
|
||||
await openSettings(page, 'zh')
|
||||
const dialog = page.getByRole('dialog', { name: '设置' })
|
||||
await dialog.waitFor({ timeout: 10_000 })
|
||||
await dialog.getByText('工作过程展示', { exact: true }).waitFor({ timeout: 10_000 })
|
||||
@@ -581,7 +579,7 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
acknowledgeReloadConnectionLoss(tripwire, warningStart)
|
||||
await openSettingsFromAccountMenu(page, 'zh')
|
||||
await openSettings(page, 'zh')
|
||||
const reloaded = page.getByRole('dialog', { name: '设置' })
|
||||
const restoredDetails = reloaded.getByText('工作过程展示', { exact: true }).locator('../..')
|
||||
await restoredDetails.getByRole('button', { name: label, exact: true }).waitFor({ timeout: 10_000 })
|
||||
@@ -597,7 +595,7 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
|
||||
it('persists the busy-state Enter behavior across reload and a distinct port', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-enter-behavior'))
|
||||
await openSettingsFromAccountMenu(page, 'zh')
|
||||
await openSettings(page, 'zh')
|
||||
const dialog = page.getByRole('dialog', { name: '设置' })
|
||||
await dialog.waitFor({ timeout: 10_000 })
|
||||
await dialog.getByRole('button', { name: '排队发送' }).click()
|
||||
@@ -612,7 +610,7 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
acknowledgeReloadConnectionLoss(tripwire, warningStart)
|
||||
await openSettingsFromAccountMenu(page, 'zh')
|
||||
await openSettings(page, 'zh')
|
||||
const reloaded = page.getByRole('dialog', { name: '设置' })
|
||||
await reloaded.getByRole('button', { name: '插话发送' }).waitFor({ timeout: 10_000 })
|
||||
|
||||
@@ -623,7 +621,7 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
expect(second.baseUrl).not.toBe(scaffold.baseUrl)
|
||||
await secondPage.goto(second.authenticatedUrl, { waitUntil: 'load' })
|
||||
await secondPage.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
await openSettingsFromAccountMenu(secondPage, 'zh')
|
||||
await openSettings(secondPage, 'zh')
|
||||
await secondPage.getByRole('dialog', { name: '设置' })
|
||||
.getByRole('button', { name: '插话发送' }).waitFor({ timeout: 10_000 })
|
||||
expect(await secondPage.evaluate(() => localStorage.getItem('dsh.conversation.busyEnter'))).toBeNull()
|
||||
@@ -646,7 +644,7 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
|
||||
it('persists the settings language across reload and a distinct port', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-language'))
|
||||
await openSettingsFromAccountMenu(page, 'zh')
|
||||
await openSettings(page, 'zh')
|
||||
const zhDialog = page.getByRole('dialog', { name: '设置' })
|
||||
await zhDialog.waitFor({ timeout: 10_000 })
|
||||
// The document language follows the active locale in the assembled app, not
|
||||
@@ -678,7 +676,7 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
acknowledgeReloadConnectionLoss(tripwire, warningStart)
|
||||
const enTrigger = page.getByRole('button', { name: 'Account menu' })
|
||||
const enTrigger = page.getByRole('button', { name: 'Settings' })
|
||||
await enTrigger.waitFor({ timeout: 10_000 })
|
||||
|
||||
// A Chinese browser on another port still receives the explicit English
|
||||
@@ -690,7 +688,7 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
expect(second.baseUrl).not.toBe(scaffold.baseUrl)
|
||||
await secondPage.goto(second.authenticatedUrl, { waitUntil: 'load' })
|
||||
await secondPage.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
await openSettingsFromAccountMenu(secondPage, 'en')
|
||||
await openSettings(secondPage, 'en')
|
||||
await secondPage.getByRole('dialog', { name: 'Settings' })
|
||||
.getByRole('button', { name: 'English' }).waitFor({ timeout: 10_000 })
|
||||
expect(await secondPage.evaluate(() => localStorage.getItem('dsh.locale'))).toBeNull()
|
||||
@@ -701,7 +699,7 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
await second.close()
|
||||
}
|
||||
|
||||
await openSettingsFromAccountMenu(page, 'en')
|
||||
await openSettings(page, 'en')
|
||||
await page.getByRole('dialog', { name: 'Settings' }).getByRole('button', { name: 'English' }).click()
|
||||
await page.getByRole('menuitem', { name: '中文' }).click()
|
||||
await page.getByRole('dialog', { name: '设置' }).waitFor({ timeout: 10_000 })
|
||||
@@ -725,7 +723,7 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
await enPage.goto(fresh.authenticatedUrl, { waitUntil: 'load' })
|
||||
await enPage.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
expect(await enPage.evaluate(() => localStorage.getItem('dsh.locale'))).toBeNull()
|
||||
await openSettingsFromAccountMenu(enPage, 'en')
|
||||
await openSettings(enPage, 'en')
|
||||
const dialog = enPage.getByRole('dialog', { name: 'Settings' })
|
||||
await dialog.waitFor({ timeout: 10_000 })
|
||||
await dialog.getByRole('button', { name: 'English' }).waitFor({ timeout: 10_000 })
|
||||
@@ -764,7 +762,7 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
await frPage.goto(fresh.authenticatedUrl, { waitUntil: 'load' })
|
||||
await frPage.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
expect(await frPage.evaluate(() => localStorage.getItem('dsh.locale'))).toBeNull()
|
||||
await openSettingsFromAccountMenu(frPage, 'en')
|
||||
await openSettings(frPage, 'en')
|
||||
const dialog = frPage.getByRole('dialog', { name: 'Settings' })
|
||||
await dialog.waitFor({ timeout: 10_000 })
|
||||
await dialog.getByRole('button', { name: 'English' }).waitFor({ timeout: 10_000 })
|
||||
@@ -798,7 +796,7 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
onTestFinished(() => withoutBrowser.close())
|
||||
const browserConsole = watchConsole(withoutBrowser)
|
||||
await withoutBrowser.goto(fresh.authenticatedUrl, { waitUntil: 'load' })
|
||||
await openSettingsFromAccountMenu(withoutBrowser, 'en')
|
||||
await openSettings(withoutBrowser, 'en')
|
||||
const dialog = withoutBrowser.getByRole('dialog', { name: 'Settings' })
|
||||
await dialog.getByRole('button', { name: 'Detailed', exact: true }).waitFor()
|
||||
expect(await dialog.getByText('Open chat links in', { exact: true }).count()).toBe(0)
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {} from '@deepseek-ai/dsh-api-terminal-controller'
|
||||
import type { SubprocessTerminalHandle } from '@deepseek-ai/dsh-subprocess'
|
||||
import { createProcessInspector, type ProcessIdentity } from '@deepseek-ai/dsh-subprocess-local/src/process-inspector.ts'
|
||||
import { compareOrRefreshGolden, launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold } from './scaffold.ts'
|
||||
import { openSettingsFromAccountMenu, connectFreshWorkspace, saveFailureShot } from './support.ts'
|
||||
import { openSettings, connectFreshWorkspace, saveFailureShot } from './support.ts'
|
||||
|
||||
const expected = fileURLToPath(new URL('./expected/sidebar-terminal/running.expected.md', import.meta.url))
|
||||
const shots = fileURLToPath(new URL('../../../.artifacts/screenshots/sidebar-terminal/', import.meta.url))
|
||||
@@ -47,7 +47,7 @@ async function controlTransport(page: Page) {
|
||||
}
|
||||
|
||||
async function selectTerminalTheme(page: Page, name: string): Promise<void> {
|
||||
await openSettingsFromAccountMenu(page, 'en')
|
||||
await openSettings(page, 'en')
|
||||
const dialog = page.getByRole('dialog', { name: 'Settings' })
|
||||
const [response] = await Promise.all([
|
||||
page.waitForResponse(candidate => new URL(candidate.url()).pathname === '/api/settings/mutate' && candidate.request().method() === 'POST'),
|
||||
|
||||
@@ -30,7 +30,8 @@ const UI_EXPANDED_EXPECTED = join(SNAPSHOT_DIR, 'ui-expanded.expected.md')
|
||||
const MODE = webSnapshotMode()
|
||||
|
||||
const SKILL_NAME = 'user-invoke-demo'
|
||||
const ARGS_TEXT = '@"meeting notes.md" and confirm the fixture wiring'
|
||||
const FILE_NAME = 'meeting notes-this-is-a-very-long-filename-for-testing-user-message-file-references-and-preview-layout.md'
|
||||
const ARGS_TEXT = `@"${FILE_NAME}" and confirm the fixture wiring`
|
||||
const REPLY = 'USER_INVOKE_REPLY acknowledged; following the injected skill.'
|
||||
|
||||
async function seedUserOnlySkill(workspaceCwd: string): Promise<void> {
|
||||
@@ -78,7 +79,7 @@ describe.skipIf(MODE === 'record')('web e2e: user-explicit skill invocation thro
|
||||
paceMs: 10,
|
||||
})
|
||||
await seedUserOnlySkill(scaffold.workspaceCwd)
|
||||
await writeFile(join(scaffold.workspaceCwd, 'workspace', 'meeting notes.md'), '# Meeting notes\n\nSent reference preview.\n')
|
||||
await writeFile(join(scaffold.workspaceCwd, 'workspace', FILE_NAME), '# Meeting notes\n\nSent reference preview.\n')
|
||||
browser = await chromium.launch()
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
@@ -162,6 +163,19 @@ describe.skipIf(MODE === 'record')('web e2e: user-explicit skill invocation thro
|
||||
await skill.click()
|
||||
await expect.poll(() => preview.textContent(), { timeout: 10_000 }).toContain('Reply with the fixture acknowledgement line.')
|
||||
const file = page.locator('[data-chat-flow-kind="user"] [data-ref-chip="file"]').first()
|
||||
const bounds = await file.evaluate((element) => {
|
||||
const bubble = element.closest('[class*="bubble"]')
|
||||
if (bubble === null) throw new Error('file reference has no user bubble')
|
||||
const label = element.getBoundingClientRect()
|
||||
const container = bubble.getBoundingClientRect()
|
||||
return {
|
||||
left: label.left - container.left, right: container.right - label.right,
|
||||
height: label.height, lineHeight: Number.parseFloat(getComputedStyle(element).lineHeight),
|
||||
}
|
||||
})
|
||||
expect(bounds.left).toBeGreaterThanOrEqual(0)
|
||||
expect(bounds.right).toBeGreaterThanOrEqual(0)
|
||||
expect(bounds.height).toBeGreaterThan(bounds.lineHeight)
|
||||
await file.hover()
|
||||
expect(await file.evaluate(el => getComputedStyle(el).textDecorationStyle)).toBe('dotted')
|
||||
await file.click()
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { openSettingsFromAccountMenu, connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
import { openSettings, connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/steering', import.meta.url))
|
||||
const FIXTURE = join(SNAPSHOT_DIR, 'session.v3.jsonl')
|
||||
@@ -309,7 +309,7 @@ describe('web e2e: composer shortcut follows the swapped busy behavior', () => {
|
||||
|
||||
it.skipIf(MODE === 'record')('queues Cmd+Enter when plain Enter is configured to Steer', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-composer-swapped-shortcut'))
|
||||
await openSettingsFromAccountMenu(page, 'en')
|
||||
await openSettings(page, 'en')
|
||||
const dialog = page.getByRole('dialog', { name: 'Settings' })
|
||||
await dialog.getByRole('button', { name: 'Queue' }).click()
|
||||
await page.getByRole('menuitem', { name: 'Steer' }).click()
|
||||
|
||||
@@ -226,11 +226,16 @@ export function conversationContextKey(kind: string, id: string): string {
|
||||
return `${kind.length}:${kind}${id}`
|
||||
}
|
||||
|
||||
/** Open Settings through the sidebar account menu.
|
||||
/** Open Settings through the Web gear or Desktop account menu.
|
||||
* @param page - browser page with the mounted sidebar.
|
||||
* @param locale - current UI language.
|
||||
*/
|
||||
export async function openSettingsFromAccountMenu(page: Page, locale: 'en' | 'zh'): Promise<void> {
|
||||
await page.getByRole('button', { name: locale === 'zh' ? '账号菜单' : 'Account menu', exact: true }).click()
|
||||
await page.getByRole('menuitem', { name: locale === 'zh' ? '设置' : 'Settings', exact: true }).click()
|
||||
export async function openSettings(page: Page, locale: 'en' | 'zh'): Promise<void> {
|
||||
const label = locale === 'zh' ? '设置' : 'Settings'
|
||||
if (await page.evaluate(() => 'dshDesktop' in globalThis)) {
|
||||
await page.getByRole('button', { name: locale === 'zh' ? '账号菜单' : 'Account menu', exact: true }).click()
|
||||
await page.getByRole('menuitem', { name: label, exact: true }).click()
|
||||
} else {
|
||||
await page.getByRole('button', { name: label, exact: true }).click()
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { openSettingsFromAccountMenu, connectFreshWorkspace, expandOwningTurnProcess, newEnglishPage, saveFailureShot, WEB_FIXTURE_TIME } from './support.ts'
|
||||
import { openSettings, connectFreshWorkspace, expandOwningTurnProcess, newEnglishPage, saveFailureShot, WEB_FIXTURE_TIME } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/turn-tail-actions', import.meta.url))
|
||||
const FIXTURE = join(SNAPSHOT_DIR, 'session.v3.jsonl')
|
||||
@@ -256,7 +256,7 @@ describe('web e2e: assistant IconActions wait for the turn to end', () => {
|
||||
expect(await process.getAttribute('aria-expanded')).toBe('false')
|
||||
expect(await tool.isVisible()).toBe(false)
|
||||
|
||||
await openSettingsFromAccountMenu(page, 'en')
|
||||
await openSettings(page, 'en')
|
||||
const dialog = page.getByRole('dialog', { name: 'Settings' })
|
||||
await dialog.getByText('Work details', { exact: true }).locator('../..')
|
||||
.getByRole('button', { name: 'Compact', exact: true }).click()
|
||||
@@ -279,7 +279,7 @@ describe('web e2e: assistant IconActions wait for the turn to end', () => {
|
||||
expect(await tool.isVisible()).toBe(true)
|
||||
await process.click()
|
||||
|
||||
await openSettingsFromAccountMenu(page, 'en')
|
||||
await openSettings(page, 'en')
|
||||
const restored = page.getByRole('dialog', { name: 'Settings' })
|
||||
await restored.getByText('Work details', { exact: true }).locator('../..')
|
||||
.getByRole('button', { name: label, exact: true }).click()
|
||||
|
||||
@@ -556,7 +556,7 @@ describe('web e2e: workspace management (create / rename / grouping / hover affo
|
||||
expect(await copied.isVisible()).toBe(true)
|
||||
expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(rowTitle)
|
||||
// Leaving anchor and card together closes it after the grace.
|
||||
await page.getByRole('button', { name: 'Account menu' }).hover()
|
||||
await page.getByRole('button', { name: 'Settings' }).hover()
|
||||
await expect.poll(() => card.count(), { timeout: 5_000 }).toBe(0)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
@@ -586,7 +586,7 @@ describe('web e2e: workspace management (create / rename / grouping / hover affo
|
||||
await page.waitForTimeout(POINTER_HOLD_MS)
|
||||
expect(await page.getByRole('menuitem', { name: 'Rename' }).count()).toBe(1)
|
||||
// Pointer-leave dismissal still applies once the pointer genuinely leaves.
|
||||
await page.getByRole('button', { name: 'Account menu' }).hover()
|
||||
await page.getByRole('button', { name: 'Settings' }).hover()
|
||||
await expect.poll(() => page.getByRole('menuitem', { name: 'Rename' }).count(), { timeout: 5_000 }).toBe(0)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/config-catalog.md
|
||||
config-catalog.md: 573d16f37b9ce3c123c55c366d8ee6424feb3d64
|
||||
config-catalog.zh.md: 9d827a4c65ec198239d7d1ef29735747a39838b9
|
||||
config-catalog.md: f86f02fbec949c0212c3c038f47d1fdf50d239fe
|
||||
config-catalog.zh.md: d56d5bdef41e76ce7a41e3d582a1a8afde18766b
|
||||
@@ -679,7 +679,7 @@ Requires: `credentials` · `authorization`
|
||||
export interface Config {
|
||||
/** Platform origin serving auth-api and browser pages. */
|
||||
platformOrigin?: string
|
||||
/** Native desktop identity for Host API and embedded Platform requests; null omits the client platform header. */
|
||||
/** Native desktop identity for Host API and embedded Platform requests; null identifies the client as web. */
|
||||
desktopPlatform?: 'darwin' | 'win32' | null
|
||||
/** Optional frontend deployment selector for embedded Usage and Top-up pages. */
|
||||
embeddedPageDist?: string
|
||||
@@ -2781,18 +2781,14 @@ Source: [`packages/spill/spill-local/src/index.ts:31`](../packages/spill/spill-l
|
||||
Requires: `tools`
|
||||
|
||||
```ts config-catalog
|
||||
/** Plugin config. */
|
||||
/** Optional result-retention budget. */
|
||||
export interface Config {
|
||||
/**
|
||||
* The model-facing context cap for a plain-text tool result, in UTF-8 bytes.
|
||||
* Omitted disables the policy entirely (no-op). When set, a result larger than
|
||||
* this is spilled and replaced with a preview derived from this same budget.
|
||||
*/
|
||||
maxInlineBytes?: number
|
||||
/** Maximum estimated tokens in a retained result, including image descriptors and omission notices. Omitted disables retention. */
|
||||
maxInlineTokens?: number
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/spill/spill-policy/src/index.ts:61`](../packages/spill/spill-policy/src/index.ts)
|
||||
Source: [`packages/spill/spill-policy/src/index.ts:25`](../packages/spill/spill-policy/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-ssh"></a>
|
||||
|
||||
@@ -3808,7 +3804,7 @@ export interface Config {
|
||||
export type ToolPresentationMode = 'native' | 'ptc' | 'both'
|
||||
```
|
||||
|
||||
Source: [`packages/core/tools/src/index.ts:663`](../packages/core/tools/src/index.ts)
|
||||
Source: [`packages/core/tools/src/index.ts:673`](../packages/core/tools/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-typert-loader"></a>
|
||||
|
||||
|
||||
@@ -681,7 +681,7 @@ export interface Config {
|
||||
export interface Config {
|
||||
/** Platform origin serving auth-api and browser pages. */
|
||||
platformOrigin?: string
|
||||
/** Native desktop identity for Host API and embedded Platform requests; null omits the client platform header. */
|
||||
/** Native desktop identity for Host API and embedded Platform requests; null identifies the client as web. */
|
||||
desktopPlatform?: 'darwin' | 'win32' | null
|
||||
/** Optional frontend deployment selector for embedded Usage and Top-up pages. */
|
||||
embeddedPageDist?: string
|
||||
@@ -2783,18 +2783,14 @@ export interface Config {
|
||||
需要: `tools`
|
||||
|
||||
```ts config-catalog
|
||||
/** Plugin config. */
|
||||
/** Optional result-retention budget. */
|
||||
export interface Config {
|
||||
/**
|
||||
* The model-facing context cap for a plain-text tool result, in UTF-8 bytes.
|
||||
* Omitted disables the policy entirely (no-op). When set, a result larger than
|
||||
* this is spilled and replaced with a preview derived from this same budget.
|
||||
*/
|
||||
maxInlineBytes?: number
|
||||
/** Maximum estimated tokens in a retained result, including image descriptors and omission notices. Omitted disables retention. */
|
||||
maxInlineTokens?: number
|
||||
}
|
||||
```
|
||||
|
||||
来源: [`packages/spill/spill-policy/src/index.ts:61`](../packages/spill/spill-policy/src/index.ts)
|
||||
来源: [`packages/spill/spill-policy/src/index.ts:25`](../packages/spill/spill-policy/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-ssh"></a>
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/module-graph.md
|
||||
module-graph.md: fc5c06727e58705ac6d9b44470b1b37010dc3223
|
||||
module-graph.zh.md: 6911d9820932569c7a666b47a8bd53696fdb6393
|
||||
module-graph.md: bfe3e5faa7168b03848ac76d2dd2584d4aa31f5e
|
||||
module-graph.zh.md: bfa0f83e14cbcf89599fa207ec919195128db49e
|
||||
@@ -806,11 +806,6 @@ flowchart TD
|
||||
pkg_tool_web --> pkg_system_prompt
|
||||
pkg_tool_web --> pkg_tools
|
||||
pkg_tool_web --> pkg_web
|
||||
pkg_spill_policy --> pkg_llm
|
||||
pkg_spill_policy --> pkg_output_retention
|
||||
pkg_spill_policy --> pkg_session
|
||||
pkg_spill_policy --> pkg_spill
|
||||
pkg_spill_policy --> pkg_tools
|
||||
pkg_tool_todo --> pkg_agent
|
||||
pkg_tool_todo --> pkg_invariants
|
||||
pkg_tool_todo --> pkg_session
|
||||
@@ -1101,6 +1096,14 @@ flowchart TD
|
||||
pkg_subagent --> pkg_user_approval
|
||||
pkg_subagent --> pkg_util_time
|
||||
pkg_subagent --> pkg_workspace
|
||||
pkg_spill_policy --> pkg_attachment
|
||||
pkg_spill_policy --> pkg_fs
|
||||
pkg_spill_policy --> pkg_llm
|
||||
pkg_spill_policy --> pkg_output_retention
|
||||
pkg_spill_policy --> pkg_session
|
||||
pkg_spill_policy --> pkg_spill
|
||||
pkg_spill_policy --> pkg_token_meter
|
||||
pkg_spill_policy --> pkg_tools
|
||||
pkg_session_query_sqlite --> pkg_session
|
||||
pkg_session_query_sqlite --> pkg_session_persistence
|
||||
pkg_session_query_sqlite --> pkg_session_query
|
||||
@@ -1566,7 +1569,6 @@ flowchart TD
|
||||
| [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) |
|
||||
| [`tool-workspace-dependencies`](../packages/skill/tool-workspace-dependencies) | `skill` | [`tools`](../packages/core/tools) |
|
||||
| [`tool-web`](../packages/web/tool-web) | `web` | [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`web`](../packages/web/web) |
|
||||
| [`spill-policy`](../packages/spill/spill-policy) | `spill` | [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`tools`](../packages/core/tools) |
|
||||
| [`tool-todo`](../packages/todo/tool-todo) | `todo` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) |
|
||||
| [`plan-mode`](../packages/plan/plan-mode) | `plan` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| [`hooks-codex`](../packages/hooks/hooks-codex) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) |
|
||||
@@ -1619,6 +1621,7 @@ flowchart TD
|
||||
| [`agent-loop-testkit`](../packages/test-support/agent-loop-testkit) | `test-support` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
|
||||
| [`webhook`](../packages/webhook/webhook) | `webhook` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval), [`util-time`](../packages/util/time), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`spill-policy`](../packages/spill/spill-policy) | `spill` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools) |
|
||||
| [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) |
|
||||
| [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
|
||||
| [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`mcp-client`](../packages/mcp/mcp-client), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`token-meter`](../packages/llm/token-meter), [`user-approval`](../packages/interaction/user-approval) |
|
||||
|
||||
@@ -808,11 +808,6 @@ flowchart TD
|
||||
pkg_tool_web --> pkg_system_prompt
|
||||
pkg_tool_web --> pkg_tools
|
||||
pkg_tool_web --> pkg_web
|
||||
pkg_spill_policy --> pkg_llm
|
||||
pkg_spill_policy --> pkg_output_retention
|
||||
pkg_spill_policy --> pkg_session
|
||||
pkg_spill_policy --> pkg_spill
|
||||
pkg_spill_policy --> pkg_tools
|
||||
pkg_tool_todo --> pkg_agent
|
||||
pkg_tool_todo --> pkg_invariants
|
||||
pkg_tool_todo --> pkg_session
|
||||
@@ -1103,6 +1098,14 @@ flowchart TD
|
||||
pkg_subagent --> pkg_user_approval
|
||||
pkg_subagent --> pkg_util_time
|
||||
pkg_subagent --> pkg_workspace
|
||||
pkg_spill_policy --> pkg_attachment
|
||||
pkg_spill_policy --> pkg_fs
|
||||
pkg_spill_policy --> pkg_llm
|
||||
pkg_spill_policy --> pkg_output_retention
|
||||
pkg_spill_policy --> pkg_session
|
||||
pkg_spill_policy --> pkg_spill
|
||||
pkg_spill_policy --> pkg_token_meter
|
||||
pkg_spill_policy --> pkg_tools
|
||||
pkg_session_query_sqlite --> pkg_session
|
||||
pkg_session_query_sqlite --> pkg_session_persistence
|
||||
pkg_session_query_sqlite --> pkg_session_query
|
||||
@@ -1568,7 +1571,6 @@ flowchart TD
|
||||
| [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) |
|
||||
| [`tool-workspace-dependencies`](../packages/skill/tool-workspace-dependencies) | `skill` | [`tools`](../packages/core/tools) |
|
||||
| [`tool-web`](../packages/web/tool-web) | `web` | [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`web`](../packages/web/web) |
|
||||
| [`spill-policy`](../packages/spill/spill-policy) | `spill` | [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`tools`](../packages/core/tools) |
|
||||
| [`tool-todo`](../packages/todo/tool-todo) | `todo` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) |
|
||||
| [`plan-mode`](../packages/plan/plan-mode) | `plan` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| [`hooks-codex`](../packages/hooks/hooks-codex) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) |
|
||||
@@ -1621,6 +1623,7 @@ flowchart TD
|
||||
| [`agent-loop-testkit`](../packages/test-support/agent-loop-testkit) | `test-support` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
|
||||
| [`webhook`](../packages/webhook/webhook) | `webhook` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval), [`util-time`](../packages/util/time), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`spill-policy`](../packages/spill/spill-policy) | `spill` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools) |
|
||||
| [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) |
|
||||
| [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
|
||||
| [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`mcp-client`](../packages/mcp/mcp-client), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`token-meter`](../packages/llm/token-meter), [`user-approval`](../packages/interaction/user-approval) |
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/persistence-catalog.md
|
||||
persistence-catalog.md: bb3ee2c998189533ed87e149422eae8123fb07b1
|
||||
persistence-catalog.zh.md: 3dabca40d2975e090935d471cd187def826ea4df
|
||||
persistence-catalog.md: 022944bea076f49e054e7194e6b475cf6dfca2bc
|
||||
persistence-catalog.zh.md: 096bf746085d329427dfb5516db4c6387f0974fe
|
||||
@@ -4929,7 +4929,7 @@ Sources: [`packages/llm/llm/src/types.ts:127`](../packages/llm/llm/src/types.ts)
|
||||
|
||||
SHA-256: `3cfc3a56502da1f8c6153c2c56657bab4b749056968634dd0991d325ec1c919f`
|
||||
|
||||
Sources: [`packages/core/tools/src/index.ts:689`](../packages/core/tools/src/index.ts)
|
||||
Sources: [`packages/core/tools/src/index.ts:699`](../packages/core/tools/src/index.ts)
|
||||
|
||||
| Property | Presence | Type |
|
||||
|---|---|---|
|
||||
|
||||
@@ -4931,7 +4931,7 @@ SHA-256: `43e311235aa537df1bacdd92d144df8a952337a76e9ac892cdf670f1a797bad8`
|
||||
|
||||
SHA-256: `3cfc3a56502da1f8c6153c2c56657bab4b749056968634dd0991d325ec1c919f`
|
||||
|
||||
来源:[`packages/core/tools/src/index.ts:689`](../packages/core/tools/src/index.ts)
|
||||
来源:[`packages/core/tools/src/index.ts:699`](../packages/core/tools/src/index.ts)
|
||||
|
||||
| 属性 | 存在性 | 类型 |
|
||||
|---|---|---|
|
||||
|
||||
@@ -36275,7 +36275,7 @@
|
||||
"packages/core/tools/src/index.ts#ToolRestriction"
|
||||
],
|
||||
"sources": [
|
||||
"packages/core/tools/src/index.ts:689"
|
||||
"packages/core/tools/src/index.ts:699"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/subsystems/spill.md
|
||||
spill.md: 577f3ff14bade2b0c03a0aeaa975d12ae96803b8
|
||||
spill.zh.md: 1bc238b8cdc0ef81e966bc5ec9c11b2dd31b24da
|
||||
spill.md: 5a6eb91c8dc3932ab56bd7afc4a4cc60b509180d
|
||||
spill.zh.md: 0eebf93d041a2689e17c70053d34b93b34b51c2f
|
||||
@@ -89,7 +89,7 @@ type SpillLocator = Branded<'SpillLocator'>
|
||||
|
||||
`SpillStore` (`ctx.spillStore`, defined in [`packages/spill/spill/src/index.ts`](../../packages/spill/spill/src/index.ts)) is a one-method abstract service: `saveText(input) → Promise<SpillRef>`. It persists the FULL `content` and REJECTS on a real storage failure (permissions, ENOSPC, backend unavailable). The seam owns storage only: no retention policy, no tool-result replacement, no retrieval/search API.
|
||||
|
||||
The local backend ([dsh-spill-local](../../packages/spill/spill-local)) writes under `<root>/session-<hash>/<random>-<safeName>` — a configured or lazily-created private (0700) root, a `sha256(sessionId)` session subdir, and an exclusive owner-only (`open(path, 'wx', 0o600)`) write so a planted symlink cannot redirect it. Its `locator` is the local path and its `retrievalHint` tells the model to use `read` or `grep` on that path. The policy consumer ([dsh-spill-policy](../../packages/spill/spill-policy)) replaces an over-`maxInlineBytes` plain-text final result with a retention-library head/tail preview plus the spill reference, best-effort: a save failure keeps the original inline result rather than turning a successful call into an `isError`.
|
||||
The local backend ([dsh-spill-local](../../packages/spill/spill-local)) writes under `<root>/session-<hash>/<random>-<safeName>` — a configured or lazily-created private (0700) root, a `sha256(sessionId)` session subdir, and an exclusive owner-only (`open(path, 'wx', 0o600)`) write so a planted symlink cannot redirect it. Its `locator` is the local path and its `retrievalHint` tells the model to use `read` or `grep` on that path. The policy consumer ([dsh-spill-policy](../../packages/spill/spill-policy)) replaces an over-`maxInlineTokens` text/image result with ordered head/tail content and a spill address, best-effort: a save failure keeps the original inline result rather than turning a successful call into an `isError`.
|
||||
|
||||
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ type SpillLocator = Branded<'SpillLocator'>
|
||||
|
||||
`SpillStore`(`ctx.spillStore`,定义于 [`packages/spill/spill/src/index.ts`](../../packages/spill/spill/src/index.ts))是只有一个方法的抽象服务:`saveText(input) → Promise<SpillRef>`。它持久保存完整的 `content`,并在实际存储失败(权限、ENOSPC、后端不可用)时拒绝。该 seam 只负责存储:不负责保留策略、工具结果替换或检索/搜索 API。
|
||||
|
||||
本地后端([dsh-spill-local](../../packages/spill/spill-local))写入 `<root>/session-<hash>/<random>-<safeName>`:根目录是已配置或延迟创建的私有(0700)目录,会话子目录采用 `sha256(sessionId)`,并通过排他且仅所有者可访问的写入(`open(path, 'wx', 0o600)`)防止预先植入的符号链接重定向写入。其 `locator` 是本地路径,`retrievalHint` 则告知模型在该路径上使用 `read` 或 `grep`。策略消费方([dsh-spill-policy](../../packages/spill/spill-policy))会把超过 `maxInlineBytes` 的纯文本最终结果替换为保留库生成的首尾预览和 spill 引用;该过程尽力而为:保存失败时保留原始内联结果,而不会把成功的调用变成 `isError`。
|
||||
本地后端([dsh-spill-local](../../packages/spill/spill-local))写入 `<root>/session-<hash>/<random>-<safeName>`:根目录是已配置或延迟创建的私有(0700)目录,会话子目录采用 `sha256(sessionId)`,并通过排他且仅所有者可访问的写入(`open(path, 'wx', 0o600)`)防止预先植入的符号链接重定向写入。其 `locator` 是本地路径,`retrievalHint` 则告知模型在该路径上使用 `read` 或 `grep`。策略消费方([dsh-spill-policy](../../packages/spill/spill-policy))会把超过 `maxInlineTokens` 的图文结果替换为按原顺序保留的首尾内容和 spill 地址;该过程尽力而为:保存失败时保留原始内联结果,而不会把成功的调用变成 `isError`。
|
||||
|
||||
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/subsystems/tools.md
|
||||
tools.md: 396deae037f9026c56102d080b61a3f3fbde7a05
|
||||
tools.zh.md: ae39bade0f9bb7c4b0008a0f115d97ed2e033189
|
||||
tools.md: 348cda65733b1bf60c5e6e44d14507b7c91ecbd6
|
||||
tools.zh.md: ae928e573fa4d914713357ca2de94470fc8a2c7c
|
||||
@@ -8,7 +8,7 @@ Source: [`packages/core/tools/src/index.ts`](../../packages/core/tools/src/index
|
||||
|
||||
## `ToolDefinition` — a registered tool
|
||||
|
||||
A `ToolSchema` (the model-facing fields) plus a mandatory canonical output declaration, the `execute` function, host-only scheduler metadata, an optional final-content callback, and optional UI presenters. The registry holds these; the loop dispatches calls through them. The registry's `schemas()` builds the model-facing `ToolSchema[]` by an explicit allowlist — `output`/`execute`/`finalizeContent`/`timeoutMs`/`isConcurrencySafe`/`presentCall`/`presentResult` must never leak into a model request.
|
||||
A `ToolSchema` (the model-facing fields) plus a mandatory canonical output declaration, the `execute` function, host-only scheduler metadata, an optional final-content callback, and optional UI presenters. The registry holds these; the loop dispatches calls through them. The registry's `schemas()` builds the model-facing `ToolSchema[]` by an explicit allowlist — `output`/`execute`/`projectContent`/`finalizeContent`/`timeoutMs`/`isConcurrencySafe`/`presentCall`/`presentResult` must never leak into a model request.
|
||||
|
||||
```ts type-equiv
|
||||
/** Tool-owned canonical output contract used after the body returns a JSON value. */
|
||||
@@ -38,6 +38,16 @@ interface ToolDefinition extends ToolSchema {
|
||||
* @returns the canonical value declared by `output.schema`.
|
||||
*/
|
||||
execute(args: unknown, exec: ToolRunContext): Promise<unknown>
|
||||
/**
|
||||
* Install execution-prepared content before `tools/post-execute` policies.
|
||||
* The callback is captured when the call starts and runs once for a
|
||||
* normalized outcome entering post-execute. Policy replacements remain
|
||||
* authoritative; pipeline failures that bypass post-execute skip projection.
|
||||
* @param exec - immutable execution identity and arguments.
|
||||
* @param result - normalized result before post-execute policy.
|
||||
* @returns replacement content, or undefined to preserve the renderer output.
|
||||
*/
|
||||
projectContent?(exec: Readonly<ToolExecution>, result: Readonly<ToolExecutionResult>): ContentBlock[] | undefined
|
||||
/**
|
||||
* Synchronous last-mile transform for model-facing content. The registry
|
||||
* snapshots this callback when execution starts and invokes it exactly once
|
||||
@@ -169,7 +179,7 @@ interface ToolRestriction {
|
||||
|
||||
## Execution: extensible waterfalls plus monotonic policy
|
||||
|
||||
`ctx.tools.execute()` accepts a caller-owned `ToolExecutionInput` with a required readonly `signal`, materializes its parsed JSON arguments once into a pipeline-owned `ToolExecution`, and runs that call through `tools/pre-execute` (the reorderable allow/deny/ask waterfall) → registered monotonic guards → `tools/execute` (around-dispatch wrappers) → `tools/post-execute` (inspect/replace the result) → optional definition-owned `finalizeContent` → `tools/result` (the immutable authoritative outcome). Only the `tools/execute` view may replace the required signal. The outcome is a `ToolExecutionResult`.
|
||||
`ctx.tools.execute()` accepts a caller-owned `ToolExecutionInput` with a required readonly `signal`, materializes its parsed JSON arguments once into a pipeline-owned `ToolExecution`, and runs that call through `tools/pre-execute` (the reorderable allow/deny/ask waterfall) → registered monotonic guards → `tools/execute` (around-dispatch wrappers) → `projectContent` → `tools/post-execute` (inspect/replace the result) → optional definition-owned `finalizeContent` → `tools/result` (the immutable authoritative outcome). Only the `tools/execute` view may replace the required signal. The outcome is a `ToolExecutionResult`.
|
||||
|
||||
```ts type-equiv
|
||||
/** Opaque call identity that permits correlation without exposing mutable execution state. */
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
## `ToolDefinition` — 一个已注册的工具
|
||||
|
||||
由一个 `ToolSchema`(面向模型的字段)、必需的规范输出声明、`execute` 函数、仅供宿主使用的调度器元数据、可选的最终内容回调和可选 UI 展示函数组成。注册表持有这些定义,循环通过它们分派调用。注册表的 `schemas()` 通过显式允许列表构建面向模型的 `ToolSchema[]`;`output`/`execute`/`finalizeContent`/`timeoutMs`/`isConcurrencySafe`/`presentCall`/`presentResult` 绝不能泄漏到模型请求中。
|
||||
由一个 `ToolSchema`(面向模型的字段)、必需的规范输出声明、`execute` 函数、仅供宿主使用的调度器元数据、可选的最终内容回调和可选 UI 展示函数组成。注册表持有这些定义,循环通过它们分派调用。注册表的 `schemas()` 通过显式允许列表构建面向模型的 `ToolSchema[]`;`output`/`execute`/`projectContent`/`finalizeContent`/`timeoutMs`/`isConcurrencySafe`/`presentCall`/`presentResult` 绝不能泄漏到模型请求中。
|
||||
|
||||
```ts type-equiv
|
||||
/** Tool-owned canonical output contract used after the body returns a JSON value. */
|
||||
@@ -38,6 +38,16 @@ interface ToolDefinition extends ToolSchema {
|
||||
* @returns the canonical value declared by `output.schema`.
|
||||
*/
|
||||
execute(args: unknown, exec: ToolRunContext): Promise<unknown>
|
||||
/**
|
||||
* Install execution-prepared content before `tools/post-execute` policies.
|
||||
* The callback is captured when the call starts and runs once for a
|
||||
* normalized outcome entering post-execute. Policy replacements remain
|
||||
* authoritative; pipeline failures that bypass post-execute skip projection.
|
||||
* @param exec - immutable execution identity and arguments.
|
||||
* @param result - normalized result before post-execute policy.
|
||||
* @returns replacement content, or undefined to preserve the renderer output.
|
||||
*/
|
||||
projectContent?(exec: Readonly<ToolExecution>, result: Readonly<ToolExecutionResult>): ContentBlock[] | undefined
|
||||
/**
|
||||
* Synchronous last-mile transform for model-facing content. The registry
|
||||
* snapshots this callback when execution starts and invokes it exactly once
|
||||
@@ -169,7 +179,7 @@ interface ToolRestriction {
|
||||
|
||||
## 执行:可扩展的 waterfall(瀑布式事件)加单调策略
|
||||
|
||||
`ctx.tools.execute()` 接受由调用方拥有且包含必需 readonly `signal` 的 `ToolExecutionInput`,将其解析后的 JSON 参数一次性物化为流水线拥有的 `ToolExecution`,然后让调用依次经过 `tools/pre-execute`(可重排的 allow/deny/ask waterfall)→ 已注册的单调 guard → `tools/execute`(环绕分派包装层)→ `tools/post-execute`(检查/替换结果)→ 可选且由定义拥有的 `finalizeContent` → `tools/result`(不可变的权威结果)。只有 `tools/execute` 视图可以替换必需的 signal。最终产出为 `ToolExecutionResult`。
|
||||
`ctx.tools.execute()` 接受由调用方拥有且包含必需 readonly `signal` 的 `ToolExecutionInput`,将其解析后的 JSON 参数一次性物化为流水线拥有的 `ToolExecution`,然后让调用依次经过 `tools/pre-execute`(可重排的 allow/deny/ask waterfall)→ 已注册的单调 guard → `tools/execute`(环绕分派包装层)→ `projectContent` → `tools/post-execute`(检查/替换结果)→ 可选且由定义拥有的 `finalizeContent` → `tools/result`(不可变的权威结果)。只有 `tools/execute` 视图可以替换必需的 signal。最终产出为 `ToolExecutionResult`。
|
||||
|
||||
```ts type-equiv
|
||||
/** Opaque call identity that permits correlation without exposing mutable execution state. */
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/tool-execution-pipeline.md
|
||||
tool-execution-pipeline.md: f9d3d145bb3d7c271882942b4adc100f53a55290
|
||||
tool-execution-pipeline.zh.md: 3bfa6f358ee8482feaa9d26541e495b86a02e515
|
||||
tool-execution-pipeline.md: 53b7481183145289d2b2a2502ef71b5a1b7a3796
|
||||
tool-execution-pipeline.zh.md: 89d2ba505e454c50a67095d10ef3014d26009e6c
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
# Tool Execution Pipeline
|
||||
|
||||
This graph shows where policy, hooks, sandboxing, filesystem guards, result rewriting, final-outcome observation, and UI rendering run without changing the loop. The `tools/pre-execute` waterfall runs first, monotonic guards run next, and the `tools/execute` and `tools/post-execute` waterfalls follow; the three waterfalls may transform a call. Definition-owned `finalizeContent` and `tools/result` run afterward.
|
||||
This graph shows where policy, hooks, sandboxing, filesystem guards, result rewriting, final-outcome observation, and UI rendering run without changing the loop. The `tools/pre-execute` waterfall runs first, monotonic guards run next, and the `tools/execute` and `tools/post-execute` waterfalls follow; the three waterfalls may transform a call. Definition-owned `projectContent` installs prepared content before post-execute; `finalizeContent` and `tools/result` run afterward.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
@@ -18,6 +18,7 @@ flowchart TD
|
||||
toolBody["Registered tool execute() body"]
|
||||
fsGate["<code>fs/write-intent</code> or <code>fs/edit-intent</code><br/>tool-fs mutations only"]
|
||||
owned["Tool-owned session events<br/><code>todo/write</code>, <code>fs/observed</code>, <code>hook/invoked</code>, <code>hook/result</code>, <code>tool/ptc-dispatch</code>"]
|
||||
project["ToolDefinition.projectContent<br/>execution-prepared text and images"]
|
||||
post["<code>tools/post-execute</code> waterfall<br/>accept, block, replace, add context"]
|
||||
normalized["Registry outer normalization<br/>pipeline/result snapshot throws become isError"]
|
||||
finalize["ToolDefinition.finalizeContent<br/>last content-only invariant"]
|
||||
@@ -39,13 +40,15 @@ flowchart TD
|
||||
approval -->|allowed-once| guards
|
||||
approval -->|rejected, cancelled, unavailable| denied
|
||||
approval -.->|throw| normalized
|
||||
denied --> post
|
||||
denied --> project
|
||||
pre -.->|throw| normalized
|
||||
toolBody --> fsGate
|
||||
fsGate --> toolBody
|
||||
toolBody --> owned
|
||||
toolBody --> around
|
||||
around --> post
|
||||
around --> project
|
||||
project --> post
|
||||
project -.->|throw| normalized
|
||||
around -.->|wrapper throws| normalized
|
||||
post -.->|throw| normalized
|
||||
post --> finalize
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
[English](tool-execution-pipeline.md) | 中文
|
||||
|
||||
此图展示策略、钩子、沙箱、文件系统守卫、结果重写、最终结果观察和 UI 渲染在不改变循环的情况下何时运行。`tools/pre-execute` waterfall(瀑布式事件)首先运行,随后是单调守卫,然后运行 `tools/execute` 和 `tools/post-execute` waterfall;这三个 waterfall 可以改写一次调用。由定义自身控制的 `finalizeContent` 和 `tools/result` 在此之后运行。
|
||||
此图展示策略、钩子、沙箱、文件系统守卫、结果重写、最终结果观察和 UI 渲染在不改变循环的情况下何时运行。`tools/pre-execute` waterfall(瀑布式事件)首先运行,随后是单调守卫,然后运行 `tools/execute` 和 `tools/post-execute` waterfall;这三个 waterfall 可以改写一次调用。由定义自身控制的 `projectContent` 在执行后策略之前安装已准备内容;`finalizeContent` 和 `tools/result` 在此之后运行。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
@@ -20,6 +20,7 @@ flowchart TD
|
||||
toolBody["Registered tool execute() body"]
|
||||
fsGate["<code>fs/write-intent</code> or <code>fs/edit-intent</code><br/>tool-fs mutations only"]
|
||||
owned["Tool-owned session events<br/><code>todo/write</code>, <code>fs/observed</code>, <code>hook/invoked</code>, <code>hook/result</code>, <code>tool/ptc-dispatch</code>"]
|
||||
project["ToolDefinition.projectContent<br/>execution-prepared text and images"]
|
||||
post["<code>tools/post-execute</code> waterfall<br/>accept, block, replace, add context"]
|
||||
normalized["Registry outer normalization<br/>pipeline/result snapshot throws become isError"]
|
||||
finalize["ToolDefinition.finalizeContent<br/>last content-only invariant"]
|
||||
@@ -41,13 +42,15 @@ flowchart TD
|
||||
approval -->|allowed-once| guards
|
||||
approval -->|rejected, cancelled, unavailable| denied
|
||||
approval -.->|throw| normalized
|
||||
denied --> post
|
||||
denied --> project
|
||||
pre -.->|throw| normalized
|
||||
toolBody --> fsGate
|
||||
fsGate --> toolBody
|
||||
toolBody --> owned
|
||||
toolBody --> around
|
||||
around --> post
|
||||
around --> project
|
||||
project --> post
|
||||
project -.->|throw| normalized
|
||||
around -.->|wrapper throws| normalized
|
||||
post -.->|throw| normalized
|
||||
post --> finalize
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/user/guide/providers.md
|
||||
providers.md: 81ccba8b0665fa1f7666b5724aab4a8b3d183bbf
|
||||
providers.zh.md: 1afb7e7bf616206290e5cc4a90ef45b4250a9596
|
||||
providers.md: 773ff6722a8558720a458f7574940972cf976958
|
||||
providers.zh.md: 6d618ed1e9eec17e8c4a000d00cc6987d0a6af14
|
||||
@@ -20,7 +20,7 @@ Providers that sign in with OAuth, such as Codex, are not supported here yet.
|
||||
|
||||
## Add a custom model API
|
||||
|
||||
Switch the card to **Custom model API** for a relay, a company gateway, a self-hosted server, or any provider absent from the installed catalog. Supply a lowercase Provider ID, base URL, API protocol, credential, and at least one model. The **API protocol** must be the one your gateway speaks, and the picker offers three: OpenAI Chat Completions, OpenAI Responses, and Anthropic Messages, stored in `settings.yaml` as `openai-completions`, `openai-responses`, and `anthropic-messages`. A provider speaks one protocol, so a gateway that serves two needs two providers.
|
||||
Switch the card to **Custom model API** for a relay, a company gateway, a self-hosted server, or any provider absent from the installed catalog. Supply a lowercase Provider ID, base URL, API protocol, credential, and at least one model. The **API protocol** must be the one your gateway speaks, and the picker offers three: OpenAI Chat Completions, OpenAI Responses, and Anthropic Messages, stored in the active profile's `cordis.patch.yml` as `openai-completions`, `openai-responses`, and `anthropic-messages`. A provider speaks one protocol, so a gateway that serves two needs two providers.
|
||||
|
||||

|
||||
|
||||
@@ -44,6 +44,8 @@ The generated [plugin configuration catalog](../../config-catalog.md) lists ever
|
||||
|
||||
::: tip Additional settings
|
||||
The Models page exposes the API key, display name, base URL, API protocol, and each model's id, display name, context window, max output tokens, and input types. Configure reasoning effort levels, request-compatibility switches, headers, timeouts, and retry policy in `$DSH_HOME/profiles/<profile>/cordis.patch.yml`, the same document the page writes. Edit it directly, or, when the browser runs on the same machine as the server, open it with **Open configuration file** in the Settings header; the adapters re-read it on the next request, so nothing needs a restart. The subsections below cover the fields most gateways need.
|
||||
|
||||
For the standard Web UI launch with `dsh web`, `<profile>` is `web`, so the path is `$DSH_HOME/profiles/web/cordis.patch.yml`. If you launch a custom profile, use the name selected at startup instead.
|
||||
:::
|
||||
|
||||
### Image input
|
||||
@@ -132,14 +134,14 @@ Each key is a level the menu offers, and its value is the spelling sent on the w
|
||||
An `off` left empty sends nothing, which only stops a model that thinks on request; an `off` given a value sends that value as `reasoning_effort` instead. A model that thinks unless told not to — DeepSeek V4 behind an OpenAI-compatible gateway, for example — needs `compat.thinkingFormat: deepseek`, which makes `off` send `thinking: {type: disabled}` and every other level send `thinking: {type: enabled}` beside the effort:
|
||||
|
||||
```yaml
|
||||
models:
|
||||
- id: deepseek-v4-pro
|
||||
compat:
|
||||
thinkingFormat: deepseek
|
||||
reasoningEfforts:
|
||||
off:
|
||||
high: high
|
||||
max: max
|
||||
models:
|
||||
- id: deepseek-v4-pro
|
||||
compat:
|
||||
thinkingFormat: deepseek
|
||||
reasoningEfforts:
|
||||
off:
|
||||
high: high
|
||||
max: max
|
||||
```
|
||||
|
||||
A built-in provider's model whose gateway does not reason loses its levels with `reasoningEfforts: false` under `modelOverrides`; selecting an effort for it is then refused as `UNSUPPORTED_REASONING_EFFORT`. DeepSeek's own route needs none of this: its models already offer `off`, `low`, `high`, and `max`, and `llm-deepseek.reasoningEffort` sets the default the picker starts from:
|
||||
@@ -174,11 +176,11 @@ Two account for most of it. A model that declares reasoning has its system promp
|
||||
A route's `compat` is the default for its models, and a model's own wins field by field, so one model can be corrected without restating the route:
|
||||
|
||||
```yaml
|
||||
models:
|
||||
- id: my-model
|
||||
- id: my-reasoner
|
||||
compat:
|
||||
thinkingFormat: deepseek
|
||||
models:
|
||||
- id: my-model
|
||||
- id: my-reasoner
|
||||
compat:
|
||||
thinkingFormat: deepseek
|
||||
```
|
||||
|
||||
What neither sets keeps the installed catalog's value for that model, and what the catalog does not describe falls to pi-ai's detection. Give every switch you name a value: a key left empty (`supportsDeveloperRole:`) is refused rather than ignored, because an empty value would erase what the catalog knows while saying nothing in its place. A name no protocol accepts is refused too, and the message lists the ones that are available.
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
## 添加自定义模型 API
|
||||
|
||||
对于中转站、公司网关、自建服务器或已安装目录中不存在的提供商,把卡片切换到**自定义模型 API**。提供小写 Provider ID、基础 URL、API 协议、凭据和至少一个模型。**API 协议**必须选网关实际使用的那一种,选择框提供三种:OpenAI Chat Completions、OpenAI Responses 和 Anthropic Messages,在 `settings.yaml` 中分别存为 `openai-completions`、`openai-responses` 和 `anthropic-messages`。一个提供商只使用一种协议,网关同时提供两种时需要建两个提供商。
|
||||
对于中转站、公司网关、自建服务器或已安装目录中不存在的提供商,把卡片切换到**自定义模型 API**。提供小写 Provider ID、基础 URL、API 协议、凭据和至少一个模型。**API 协议**必须选网关实际使用的那一种,选择框提供三种:OpenAI Chat Completions、OpenAI Responses 和 Anthropic Messages,在当前 profile 的 `cordis.patch.yml` 中分别存为 `openai-completions`、`openai-responses` 和 `anthropic-messages`。一个提供商只使用一种协议,网关同时提供两种时需要建两个提供商。
|
||||
|
||||

|
||||
|
||||
@@ -44,6 +44,8 @@ Provider ID 是永久的,因为请求、已保存会话、模型默认值和
|
||||
|
||||
::: tip 其他设置
|
||||
模型页提供 API 密钥、显示名称、API 地址、API 协议,以及每个模型的 ID、显示名称、上下文窗口、最大输出 token 数和输入类型。推理等级、请求兼容性开关、请求头、超时和重试策略在 `$DSH_HOME/profiles/<profile>/cordis.patch.yml` 中设置,也就是模型页写入的同一份文档。可以直接编辑它;浏览器与服务器在同一台机器时,也可以点击设置页顶部的**打开配置文件**打开它。适配器会在下一次请求时重新读取,无需重启任何东西。下面各小节介绍多数网关会用到的字段。
|
||||
|
||||
按常规方式通过 `dsh web` 启动 Web UI 时,`<profile>` 就是 `web`,完整路径为 `$DSH_HOME/profiles/web/cordis.patch.yml`。如果使用自定义 profile,请替换为启动时指定的名称。
|
||||
:::
|
||||
|
||||
### 图片输入
|
||||
@@ -132,14 +134,14 @@ DeepSeek 将省略的 `inputModalities` 视为纯文本,并拒绝空列表。
|
||||
留空的 `off` 什么都不发送,这只能让「按请求才思考」的模型停下来;给 `off` 一个值,则会把该值作为 `reasoning_effort` 发送。对于「不明确关闭就会思考」的模型——例如 OpenAI 兼容网关后面的 DeepSeek V4——需要 `compat.thinkingFormat: deepseek`:它让 `off` 发送 `thinking: {type: disabled}`,其他每个等级则在 effort 之外再发送 `thinking: {type: enabled}`:
|
||||
|
||||
```yaml
|
||||
models:
|
||||
- id: deepseek-v4-pro
|
||||
compat:
|
||||
thinkingFormat: deepseek
|
||||
reasoningEfforts:
|
||||
off:
|
||||
high: high
|
||||
max: max
|
||||
models:
|
||||
- id: deepseek-v4-pro
|
||||
compat:
|
||||
thinkingFormat: deepseek
|
||||
reasoningEfforts:
|
||||
off:
|
||||
high: high
|
||||
max: max
|
||||
```
|
||||
|
||||
网关并不提供推理能力的内置提供商模型,可在 `modelOverrides` 下用 `reasoningEfforts: false` 去掉其等级;之后再为它选择等级会被拒绝并报 `UNSUPPORTED_REASONING_EFFORT`。DeepSeek 自身的路由不需要以上任何配置:其模型已经提供 `off`、`low`、`high` 和 `max`,`llm-deepseek.reasoningEffort` 设置选择器的起始默认值:
|
||||
@@ -174,11 +176,11 @@ DeepSeek 将省略的 `inputModalities` 视为纯文本,并拒绝空列表。
|
||||
路由的 `compat` 是其模型的默认值,模型自身的则逐字段胜出,因此更正某一个模型无需重述整条路由:
|
||||
|
||||
```yaml
|
||||
models:
|
||||
- id: my-model
|
||||
- id: my-reasoner
|
||||
compat:
|
||||
thinkingFormat: deepseek
|
||||
models:
|
||||
- id: my-model
|
||||
- id: my-reasoner
|
||||
compat:
|
||||
thinkingFormat: deepseek
|
||||
```
|
||||
|
||||
两者都未设置的字段,沿用已安装 catalog 为该模型记录的值;catalog 也未描述的,落到 pi-ai 的检测。凡是写下的开关都要给值:冒号后留空的键(`supportsDeveloperRole:`)会被拒绝而不是被忽略,因为空值会抹掉 catalog 已知的信息,却又没有给出任何替代。任何协议都不接受的名字同样会被拒绝,报错会列出可用的那些。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-root",
|
||||
"version": "0.1.6-alpha.2",
|
||||
"version": "0.1.7-alpha.1",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
@@ -214,6 +214,7 @@
|
||||
"@deepseek-ai/dsh-experimental-computer-use-cua-driver-mcp": "workspace:^",
|
||||
"@deepseek-ai/dsh-experimental-computer-use-cua-driver-native": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm-replay": "workspace:^",
|
||||
"@deepseek-ai/dsh-mcp-client": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-session-query": "workspace:^",
|
||||
"@deepseek-ai/dsh-web-fetch-http": "workspace:^",
|
||||
"@stylistic/eslint-plugin": "^5.10.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-acp",
|
||||
"description": "Automation-only Agent Client Protocol server for driving DeepSeek Harness agents over JSON-RPC stdio",
|
||||
"version": "0.1.6-alpha.2",
|
||||
"version": "0.1.7-alpha.1",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-api-account-controller",
|
||||
"description": "Expose safe account operations over authenticated Remote",
|
||||
"version": "0.1.6-alpha.2",
|
||||
"version": "0.1.7-alpha.1",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-api-gateway",
|
||||
"description": "Typert Remote Host dispatcher and Client API endpoint",
|
||||
"version": "0.1.6-alpha.2",
|
||||
"version": "0.1.7-alpha.1",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-api-job-controller",
|
||||
"description": "Job Remote observation stream and the reference-counted client job-output service",
|
||||
"version": "0.1.6-alpha.2",
|
||||
"version": "0.1.7-alpha.1",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-api-remotes",
|
||||
"description": "Remote BFF assembly for application-selected Host capabilities",
|
||||
"version": "0.1.6-alpha.2",
|
||||
"version": "0.1.7-alpha.1",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-api-session-controller",
|
||||
"description": "Session Remote commands, cold reads, and live control transport",
|
||||
"version": "0.1.6-alpha.2",
|
||||
"version": "0.1.7-alpha.1",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-api-settings-controller",
|
||||
"description": "Remote owner for the configuration surfaces over the settings-domain seams",
|
||||
"version": "0.1.6-alpha.2",
|
||||
"version": "0.1.7-alpha.1",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-api-terminal-controller",
|
||||
"description": "Session-owned interactive terminals with shell discovery, screen recovery and typed Remote control",
|
||||
"version": "0.1.6-alpha.2",
|
||||
"version": "0.1.7-alpha.1",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-api-workspace-controller",
|
||||
"description": "Workspace Remote commands and reconnect-safe state transport",
|
||||
"version": "0.1.6-alpha.2",
|
||||
"version": "0.1.7-alpha.1",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-api-workspace-files",
|
||||
"description": "Workspace file service and Client resource provider: bounded reads, directory listing, and live metadata over the workspaceFiles Remote namespace",
|
||||
"version": "0.1.6-alpha.2",
|
||||
"version": "0.1.7-alpha.1",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-attachment-local",
|
||||
"description": "Private content-addressed DSH_HOME attachment storage",
|
||||
"version": "0.1.6-alpha.2",
|
||||
"version": "0.1.7-alpha.1",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-attachment",
|
||||
"description": "Durable immutable attachment storage seam for the DeepSeek Harness",
|
||||
"version": "0.1.6-alpha.2",
|
||||
"version": "0.1.7-alpha.1",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-app-boot",
|
||||
"description": "Shared boot glue for the app bins: .env loading, fail-loud Loader guards, snapshot-aware config resolution, and the Loader boot sequence",
|
||||
"version": "0.1.6-alpha.2",
|
||||
"version": "0.1.7-alpha.1",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-cmdline",
|
||||
"description": "Immutable command-line handoff from a dsh launcher to any app plugin that injects cmdlineArgs",
|
||||
"version": "0.1.6-alpha.2",
|
||||
"version": "0.1.7-alpha.1",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-config-editor",
|
||||
"description": "Persist plugin configuration through profile patches and Loader reconciliation",
|
||||
"version": "0.1.6-alpha.2",
|
||||
"version": "0.1.7-alpha.1",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-hmr",
|
||||
"description": "Coordinated module and profile configuration hot reload",
|
||||
"version": "0.1.6-alpha.2",
|
||||
"version": "0.1.7-alpha.1",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-plugin-manager",
|
||||
"description": "Current-profile plugin and bundle management shared by dsh CLI, Web and agent tools",
|
||||
"version": "0.1.6-alpha.2",
|
||||
"version": "0.1.7-alpha.1",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-browser-use",
|
||||
"description": "Exclusive named browser-use provider registration",
|
||||
"version": "0.1.6-alpha.2",
|
||||
"version": "0.1.7-alpha.1",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-acp-app",
|
||||
"description": "The dsh ACP profile bundle: automation-only JSON-RPC stdio and process lifecycle over dsh-base",
|
||||
"version": "0.1.6-alpha.2",
|
||||
"version": "0.1.7-alpha.1",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -406,7 +406,7 @@
|
||||
- id: spill-policy
|
||||
name: '@deepseek-ai/dsh-spill-policy'
|
||||
config:
|
||||
maxInlineBytes: 50000
|
||||
maxInlineTokens: 12500
|
||||
|
||||
# Durability checkpoints before each model request and top-level dispatch.
|
||||
- id: session-checkpoint-policy
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-base",
|
||||
"description": "The shared dsh core as a profile bundle: the first patch layer of base-backed profiles, inserting core rows over the empty profile root",
|
||||
"version": "0.1.6-alpha.2",
|
||||
"version": "0.1.7-alpha.1",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||