Merge remote-tracking branch 'origin/master' into turtle/fix-windows-cursor-schedule-07061c1d6f38
Co-authored-by: winewill <[email protected]>
This commit is contained in:
commit
5c5af84f94
817 files changed
+14590
-1672
No files matched your search
+2
-2
@@ -9,8 +9,8 @@
|
||||
en: 2948b4f40ebe9d3b
|
||||
zh: a59676148531470f
|
||||
/agent-note-user-terminal-permissions/decision:
|
||||
en: 23aad535d7f8d41e
|
||||
zh: 543ecefce61bee7f
|
||||
en: de3d0ca17e212126
|
||||
zh: 55c32a2688d1533d
|
||||
/agent-note-user-terminal-permissions/alternatives-considered:
|
||||
en: a4f8d5ac653db322
|
||||
zh: 9671639c5a2c746b
|
||||
|
||||
@@ -12,7 +12,7 @@ Users need to run commands themselves while keeping the Agent restricted. Sharin
|
||||
|
||||
The Web sidebar terminal runs directly through the Session's subprocess provider with the execution environment's system-user permissions. It neither confines the shell through the Agent sandbox nor requests Agent approval. Operating-system permissions, container isolation and the provider's credential-environment scrubbing continue to apply. Session identity owns access, process cleanup and the initial directory; sandbox policy supplies only the configured directory fallback when the Session has no cwd.
|
||||
|
||||
Agent permission changes leave user terminals running. Agent-owned shell and terminal tools retain their own sandbox enforcement. User terminal input and output create no model input or Session events.
|
||||
Agent permission changes leave user terminals running. Agent-owned shell and terminal tools retain their own sandbox enforcement. User terminal input and output create no model input or Session events, except screens the experimental `user_terminal_read` tool of the [Agent terminal panel decision](../feature/2026-10-12-agent-terminal-panel.md) returns as tool results.
|
||||
|
||||
This decision supersedes only the shared sandbox policy and mode-switch restriction in the [Web sidebar terminal decision](../feature/2026-09-09-web-sidebar-terminal.md). That note remains active for process ownership, transport, screen recovery and shell selection. OpenCode's `packages/core/src/pty.ts` and `packages/core/src/pty/pty.node.ts` provide adjacent evidence: its interactive terminal creates a PTY directly with the selected shell and working directory.
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Status: implemented
|
||||
|
||||
Web 侧栏终端直接通过 Session 的 subprocess provider 运行,使用执行环境中系统用户的权限。它不通过 Agent 沙箱限制 shell,也不请求 Agent 审批。操作系统权限、容器隔离和 provider 对环境凭据的清除仍然生效。Session 标识负责访问范围、进程清理和初始目录;sandbox policy 仅在 Session 没有 cwd 时提供配置的默认目录。
|
||||
|
||||
改变 Agent 权限时,用户终端继续运行。Agent 使用的 shell 和 terminal 工具保留各自的沙箱限制。用户终端的输入和输出不产生模型输入或 Session 事件。
|
||||
改变 Agent 权限时,用户终端继续运行。Agent 使用的 shell 和 terminal 工具保留各自的沙箱限制。用户终端的输入和输出不产生模型输入或 Session 事件;[Agent 终端面板决策](../feature/2026-10-12-agent-terminal-panel.zh.md)中的实验工具 `user_terminal_read` 以工具结果返回的屏幕是例外。
|
||||
|
||||
本决策仅取代 [Web 侧栏终端决策](../feature/2026-09-09-web-sidebar-terminal.zh.md)中的共享沙箱策略和模式切换限制。原记录继续负责进程所有权、传输、屏幕恢复和 shell 选择。OpenCode 的 `packages/core/src/pty.ts` 和 `packages/core/src/pty/pty.node.ts` 提供相邻实现依据:其交互式终端使用选定的 shell 和工作目录直接创建 PTY。
|
||||
|
||||
|
||||
+10
-10
@@ -9,17 +9,17 @@
|
||||
en: e4a1c339c31fb1b5
|
||||
zh: 0e9db2a9ba74e6c7
|
||||
/agent-note-window-drag-coverage-as-a-contract/decision:
|
||||
en: 1cef50cc0c973afb
|
||||
zh: c97495c70292f32c
|
||||
en: 81dfb36a4657e6ef
|
||||
zh: a45de8e3e2eb9b1e
|
||||
/agent-note-window-drag-coverage-as-a-contract/alternatives-considered:
|
||||
en: d3c08cbcee308605
|
||||
zh: 93bc631a31c16855
|
||||
en: aeed84530a2bd3f2
|
||||
zh: c6700d1df49e342a
|
||||
/agent-note-window-drag-coverage-as-a-contract/consequences:
|
||||
en: d312ce6aad9dc358
|
||||
zh: 96905d1acb95ed7c
|
||||
en: 3fd5dae1e0e941df
|
||||
zh: 3d08c93ebef37e09
|
||||
/agent-note-window-drag-coverage-as-a-contract/testing:
|
||||
en: bac710df115cac90
|
||||
zh: 236874d8df1caa27
|
||||
en: a1ef0400a5661962
|
||||
zh: 93f508fc38e0e83c
|
||||
/agent-note-window-drag-coverage-as-a-contract/manual-state-matrix:
|
||||
en: 8b86cf80458055b1
|
||||
zh: 55cece531251548a
|
||||
en: 20a0ef140cf73bb5
|
||||
zh: e66b97049abe7364
|
||||
@@ -12,9 +12,9 @@ The macOS desktop window's draggable area is authored twice. The layout decides
|
||||
|
||||
Coverage becomes a contract with four executed parts:
|
||||
|
||||
1. ui-web's `window-drag/regions.ts` publishes the composition rule as an executable model — a point is draggable when the last collected app-region box containing it declares `drag`, the geometry-in-DOM-order composition the native window applies — together with `INTERACTIVE_SELECTOR`, the single source for the interactive subtraction ui-web base.css declares. The base-styles spec asserts the sheet declares exactly that list, so the two cannot drift.
|
||||
1. ui-web's `window-drag/regions.ts` publishes the composition rule as an executable model — a point is draggable when the last collected app-region box containing it declares `drag`, the geometry-in-DOM-order composition the native window applies — together with `INTERACTIVE_SELECTOR`, the source for control exclusions inside `data-window-drag` rows and `data-window-drag-controls` scopes. The shell resets inherited regions on every element. A `data-window-drag-exclude` viewport without nested drag rows excludes only its own box, while covering overlays and containers with nested drag rows declare their regions separately. The base-styles spec holds the selector, scopes, and reset declarations together.
|
||||
2. The shell declares the darwin drag surface exactly once: `html[data-platform='darwin'] [data-window-drag]` in ui-web base.css. Every chrome row marks its own element in markup, so the row's box is the window's draggable geometry and no fixed band has to match any row's height. The ui-theme app-region gate holds a manifest that pairs each row's markup mark with its sheet, its class selector and its authored height, and refuses a mark on any element the manifest does not name — so a row that stops marking itself, an unmarked row added to the composition, and a mark on a content container all fail the same gate.
|
||||
3. The browser lane gains `window-drag-coverage`, which boots the shipped composition in Chromium with `data-platform='darwin'` and asserts, through the model, that no interactive box lies inside the drag surface, plus per-row probes for the chrome rows and a page whose head row marks itself while its scrollable body stays content.
|
||||
3. The browser lane gains `window-drag-coverage`, which boots the shipped composition in Chromium with `data-platform='darwin'` and probes clickable controls independently of the boxes that contribute native regions. It asserts that controls stay outside the drag surface, checks each chrome row, and covers a page whose head row marks itself while its scrollable body stays content.
|
||||
4. The shell owns the one recollection watcher — ui-web's `window-drag/recall.ts`, installed by the boot kernel. It measures every marked row once per frame while the surface can be moving, sets `data-window-drag-recall` on the body for each frame whose geometry changed, and clears it once the geometry holds still through a short grace window, so a report that arrives before a CSS transition moves anything cannot end the loop on its first unchanged sample. It reports every reason the surface can move: a DOM change touching a marked row or the container holding one, a marked row's box resizing, and a transition or animation starting on an element that holds a marked row. No chrome row carries a pulse of its own.
|
||||
|
||||
The native half — whether a press really drags or reaches the page — stays a checklist, because no keyless CI lane reaches the window server: the state matrix below is run for any change to this layer.
|
||||
@@ -25,15 +25,17 @@ The native half — whether a press really drags or reaches the page — stays a
|
||||
|
||||
**A global background drag (the `movableByWindowBackground` reading).** Dragging anywhere that is not a control would break text selection and blank-area clicks.
|
||||
|
||||
**Global interactive `no-drag` exclusions.** Native region collection uses full element boxes without `overflow` or `clip-path` clipping. A focusable table or code block scrolled out of its viewport would therefore subtract a header it no longer visibly covers. In engines that inherit the property, descendants can extend their owner's exclusion beyond its visible box. Explicit owners, per-element resets, and scoped controls keep clipped content out of the region calculation.
|
||||
|
||||
**One drag declaration per row in each row's own sheet.** That was this work's first shape, with a manifest pinning each row's height and the band over it. Every sheet then restates the window's geometry, and a row whose box moves in layout alone still has to be re-declared in CSS; one shell rule over a markup mark keeps the declaration count at one, and the manifest pairs the marks with the geometry that the gate can still check.
|
||||
|
||||
**Pinning current coverage as one golden snapshot.** That would freeze the two failure classes next to the correct behaviour; the manifest separates the invariant (no swallowed control) from the gaps (a budget that only shrinks).
|
||||
|
||||
## Consequences
|
||||
|
||||
- A new control needs no drag declaration: the interactive selector subtracts it. A new chrome row marks its element `data-window-drag` and adds a manifest entry pinning its height, so no band arithmetic and no other sheet changes.
|
||||
- A row's own box is the whole draggable geometry, and a content container is never one. The plugin manager's detail views mark the head row they share, not the detail container they are rendered in: marking the container would drag the window over its text and form labels, and the lane samples the body below that head to keep it so.
|
||||
- No coverage gap remains: every chrome row owns its run, and the gate asserts the whole package tree declares the drag surface in exactly two places — the shell's mark rule and the Windows caption row. The plugin manager's page head and its detail views' shared head row include the 48px clearance inside the row, so the entry-page gap closed with them.
|
||||
- Controls inside marked drag rows or explicit controls scopes need no local declaration: the interactive selector subtracts them. Controls outside those scopes receive no automatic exclusion; an overlay covering a drag row must exclude its own box. A new chrome row marks its element `data-window-drag` and adds a manifest entry pinning its height, so no band arithmetic and no other sheet changes.
|
||||
- A row's own box is the whole draggable geometry, and a content container is never one. The plugin manager's detail views mark the head row they share, not the detail container they are rendered in: marking the container would drag the window over its text and form labels, and the lane samples the body below that head to keep it so. A shrinking sticky header keeps its resting flow height in an unmarked wrapper and uses the painted height for its marked row. Clipping a larger marked box does not constrain its native drag geometry.
|
||||
- Every declared chrome row owns its run, and the gate asserts the whole package tree declares the drag surface in exactly two places — the shell's mark rule and the Windows caption row. The plugin manager's page head and its detail views' shared head row include the 48px clearance inside the row, so the entry-page gap closed with them.
|
||||
- The settings overlay portals beside `#root` (like the Modal primitive) and dropped its per-sheet `no-drag` rule: a covering surface inside the root precedes the columns' chrome, so a drag row declared later would override its subtraction, and ui-web base.css's `body > :not(#root)` rule subtracts a portalled one wherever it overlaps a drag row. The browser lane asserts that placement with the overlay open. Because a step's onboarding overlay marks only `#root` inert, the panel closes when an onboarding step mounts rather than staying focusable behind it.
|
||||
- The Platform overlay's return bar is a darwin drag row only. Its former per-sheet `app-region: drag` carried no platform scope, so Windows dragged the window from that 48px bar; under the one scoped rule the bar declares drag on macOS alone, where it is the window's top strip. Windows keeps the native caption row as its drag surface, and the shell ships no Linux desktop target. The manual state matrix gains a cell for this bar.
|
||||
- A row that slides across a hidden/visible edge cannot rely on the layout change alone: Electron recollects the window's drag rects only when a computed app-region value changes (electron#32341), and Blink skips hidden boxes when it collects them. The watcher closes that gap for every row at once — it re-arms on a DOM change that touches a marked row or on a marked row's box resizing, then pulses each frame in which a box moved — so the right panel carries no pulse of its own.
|
||||
@@ -41,11 +43,13 @@ The native half — whether a press really drags or reaches the page — stays a
|
||||
## Testing
|
||||
|
||||
- `packages/client/web/tests/window-drag-regions.client.spec.ts` — the composition rule, including order sensitivity and box edges.
|
||||
- `packages/client/web/tests/base-styles.client.spec.ts` — base.css declares the model's interactive selector, the one darwin drag rule, and the recall mark's subtraction.
|
||||
- `packages/client/web/tests/base-styles.client.spec.ts` — base.css declares the model's interactive selector and control scopes, element-level reset, viewport exclusion, the one darwin drag rule, and the recall mark's subtraction.
|
||||
- `packages/client/ui-theme/tests/app-region-styles.client.spec.ts` — drag ownership, the row manifest, and each row's mark; a mark outside the manifest or a changed row height fails.
|
||||
- `packages/client/ui-dockkit/tests/app-region-styles.client.spec.ts` — the strip row's own subtraction; its drag is the markup mark's, so the sheet declares none.
|
||||
- `packages/client/ui-dockkit/tests/app-region-styles.client.spec.ts` — panes, strip-end chrome, floats, and dividers exclude their boxes; a pane's exclusion covers its body. Strip-row drag belongs to the markup mark, so the sheet declares no drag.
|
||||
- `packages/client/web/tests/window-drag-recall.client.spec.ts` — the watcher through its two seams: what re-arms it, what it ignores, the settle loop, the shell defaults, and disposal.
|
||||
- `apps/web/tests/window-drag-coverage.e2e.ts` — real Chromium: no swallowed control, per-row probes, a detail page whose head row drags while its body does not, and the recall pulse the shell fires while the right panel slides.
|
||||
- `apps/web/tests/window-drag-coverage.e2e.ts` — real Chromium: controls are probed separately from region owners, clipped transcript and tab content cannot subtract headers, fullscreen right panels with floating panes retain their intended exclusions, and inert retained panels do not affect foreground rows. The compact plugin-manager header scenario checks that the marked header box does not extend over list controls. Per-row probes, detail-page body exclusion, overlays, and the right-panel recall pulse remain covered.
|
||||
- `apps/web/tests/window-drag-inheritance.e2e.ts` — the installed Chrome reports its version and exercises inherited exclusions, the production reset, authored `none` versus `initial`, and a removed-reset negative control. A missing Chrome installation or runtime without inheritance support explicitly skips this scenario; assertions and other startup failures fail. The regular Chromium 149 lane does not implement inheritance, so it cannot establish reset behavior by itself.
|
||||
- Runtime diagnostic (2026-11-02, Electron 44.7.0 / Chromium 152.0.7977.130): an isolated hidden BrowserWindow confirmed a child computes to `no-drag` before the element reset and `none` after it; authored `none` computes to `no-drag`, while `initial` computes to `none`. This checks CSS property behavior in the shipped runtime, without DSH product code, and is not native state-matrix verification.
|
||||
- Real-machine verification (2026-09-20, both platforms, HID-level pointer input plus window-frame readback): the macOS matrix above ran on a real `hiddenInset` window and the Windows cells ran on a Windows 11 ARM64 VM — chrome rows drag, content and the covering overlay do not, controls keep their clicks, and the right panel's strip row drags after its slide.
|
||||
|
||||
## Manual state matrix
|
||||
@@ -58,7 +62,7 @@ Run on the packaged macOS app for any change to this layer. Each cell means both
|
||||
| Sidebar expanded, Conversation selected, single view (no tabs) | same rows, plus the transcript's first rows below the header |
|
||||
| Sidebar collapsed | Conversation header over the centre, `shell.leading` seat controls, reopen and New session |
|
||||
| Right sidebar pushed | panel strip row and its controls, the pane body run below the strip, the pane divider's top run |
|
||||
| Right sidebar fullscreen | panel strip row at the traffic lights, first pane's strip inset, its controls |
|
||||
| Right sidebar fullscreen | panel strip row at the traffic lights, first pane's strip inset, its controls, with and without floating panes |
|
||||
| Entry page (guide, plugin manager, settings) | page head with its 48px clearance, its first control row, and a detail page's head row over its body |
|
||||
| Window fullscreen on/off | every row above with the traffic lights hidden, in both sidebar states |
|
||||
| Double click in a chrome row's blank run | on Windows the caption row takes the system title-bar action (maximize/restore); on macOS the darwin rows are app-region areas rather than a title bar, so no system action is expected there — only the controls keep their own double click |
|
||||
+13
-9
@@ -12,9 +12,9 @@ macOS 桌面窗口的可拖区域被写了两遍。布局决定 chrome 行在哪
|
||||
|
||||
覆盖变成由四部分组成的、可执行的契约:
|
||||
|
||||
1. ui-web 的 `window-drag/regions.ts` 把合成规则发布成可执行模型——当包含某点的最后一个 app-region 盒声明 `drag` 时该点可拖,这正是原生窗口施加的"按 DOM 顺序的几何合成"——并发布 `INTERACTIVE_SELECTOR`,作为 ui-web base.css 交互减除的唯一来源。base-styles spec 断言样式表声明的正是该列表,两者无法漂移。
|
||||
1. ui-web 的 `window-drag/regions.ts` 把合成规则发布成可执行模型:当包含某点的最后一个 app-region 盒声明 `drag` 时该点可拖,这正是原生窗口施加的按 DOM 顺序的几何合成。它还发布 `INTERACTIVE_SELECTOR`,作为 `data-window-drag` 行和 `data-window-drag-controls` 范围内控件排除的来源。Shell 为每个元素重置继承的区域。不含嵌套拖动行的 `data-window-drag-exclude` 视口仅排除自身矩形,覆盖浮层和含嵌套拖动行的容器单独声明区域。base-styles spec 一并检查选择器、作用范围与重置声明。
|
||||
2. 外壳只声明一次 darwin 拖拽面:ui-web base.css 里的 `html[data-platform='darwin'] [data-window-drag]`。每个 chrome 行在 markup 里给自己的元素打标,于是该行自己的盒子就是窗口的可拖几何,不需要任何固定 band 去匹配行高。ui-theme 的 app-region 门禁持有一份清单,把每行的 markup 标记与其样式表、类选择器和写死的高度配成一条,并拒绝清单未点名的元素上的标记——行不再打标、新增未打标的行、把标记打在内容容器上,都会在同一条门禁上失败。
|
||||
3. 浏览器车道增加 `window-drag-coverage`:以 `data-platform='darwin'` 在 Chromium 里启动真实组合,用模型断言没有任何交互盒落在拖拽面内,并对各 chrome 行逐一探测,另加一个"页头行打标、其可滚动正文保持内容"的页面。
|
||||
3. 浏览器车道增加 `window-drag-coverage`:以 `data-platform='darwin'` 在 Chromium 里启动真实组合,将可点击控件的探测与贡献原生区域的矩形分开。它断言控件不落在拖拽面内,检查每个 chrome 行,并覆盖页头行打标、可滚动正文保持内容的页面。
|
||||
4. 外壳持有唯一那个重收集 watcher——ui-web 的 `window-drag/recall.ts`,由引导内核安装。在表面可能移动期间,它每帧测量每个被打标的行,凡该帧几何发生变化就在 body 上打 `data-window-drag-recall`;几何在一段很短的宽限窗口内保持不动之后才清掉,这样在 CSS transition 真正移动任何东西之前到达的一次报告,不会在它第一帧未测到变化时就终止循环。它会报告表面可能移动的每一种原因:触及被打标行或承载它的容器的 DOM 变化、被打标行的盒子尺寸变化,以及持有被打标行的元素上开始的 transition 或 animation。任何 chrome 行都不再自带脉冲。
|
||||
|
||||
原生那一半——一次按下到底是拖窗口还是到达页面——仍是清单,因为没有无密钥 CI 车道能到达窗口服务器:对本层的任何改动都要走下面的状态矩阵。
|
||||
@@ -25,15 +25,17 @@ macOS 桌面窗口的可拖区域被写了两遍。布局决定 chrome 行在哪
|
||||
|
||||
**全局背景拖拽(`movableByWindowBackground` 的读法)。** 只要不是控件就能拖,会破坏文本选择与空白处点击。
|
||||
|
||||
**全局交互元素 `no-drag` 排除。** 原生区域收集使用元素完整矩形,不考虑 `overflow` 或 `clip-path` 裁剪。因此,可聚焦表格或代码块滚出视口后,仍会减除它已不再可见地覆盖的页头。在继承该属性的内核中,子元素还可能将所属容器的排除扩展到其可见矩形之外。显式区域所有者、逐元素重置和控件作用范围使被裁剪内容不参与区域计算。
|
||||
|
||||
**每个行在自己的样式表里各声明一条 drag。** 这是本项工作最初的样子,并配一份清单钉住每行高度与覆盖它的 band。那样每个样式表都要复述窗口几何,而且只在布局里移动的盒子仍得在 CSS 里重新声明;改成"markup 打标 + 外壳一条规则"后声明只有一处,清单则继续把标记与门禁能查的几何配起来。
|
||||
|
||||
**把当前覆盖钉成一张金标准快照。** 那会把两类故障和正确行为一起冻住;清单把不变量(不得吞掉控件)与缺口(只许缩小的预算)分开。
|
||||
|
||||
## Consequences
|
||||
|
||||
- 新控件不需要任何 drag 声明:交互选择器会减除它。新 chrome 行只需给元素打上 `data-window-drag` 并在清单里加一条钉住高度,无需 band 算术,也不牵动其它样式表。
|
||||
- 行自己的盒子就是完整的可拖几何,内容容器永远不是。插件管理的详情视图给它们共用的页头行打标,而不是给它所在的详情容器打标:标记容器会让窗口在它的正文与表单 label 上拖动;车道会采样该页头下方的正文来守住这一点。
|
||||
- 不再有任何缺口:每个 chrome 行自持其拖拽段,门禁断言整个包树只在两处声明拖拽面——外壳的标记规则与 Windows 标题栏行。插件管理页头与其详情视图共用的页头行都把 48px 避让量包含在行内,入口页那条缺口也随之关闭。
|
||||
- 标记拖动行或显式控件范围内的控件无需本地声明,交互选择器会减除它们。范围外的控件不会获得自动排除;覆盖拖动行的浮层必须排除自身矩形。新 chrome 行只需给元素打上 `data-window-drag` 并在清单里加一条钉住高度,无需 band 算术,也不牵动其它样式表。
|
||||
- 行自己的盒子就是完整的可拖几何,内容容器永远不是。插件管理的详情视图给它们共用的页头行打标,而不是给它所在的详情容器打标:标记容器会让窗口在它的正文与表单 label 上拖动;车道会采样该页头下方的正文来守住这一点。收缩的 sticky 页头通过未标记的外层容器保留静止态文档流高度,标记行采用绘制高度。裁剪更大的标记矩形不会限制其原生拖动区域。
|
||||
- 每个已声明的 chrome 行自持其拖拽段,门禁断言整个包树只在两处声明拖拽面——外壳的标记规则与 Windows 标题栏行。插件管理页头与其详情视图共用的页头行都把 48px 避让量包含在行内,入口页那条缺口也随之关闭。
|
||||
- settings 浮层现在与 Modal primitive 一样 portal 到 `#root` 之外,并删掉了它自己那条 no-drag:挂在 root 内的覆盖层先于各列 chrome,后声明的 drag 行会盖掉它的减除;portal 之后由 ui-web base.css 的 `body > :not(#root)` 规则在它与 drag 行重叠处减除。车道在浮层打开时会断言这个归属。由于 onboarding 步骤的浮层只把 `#root` 置为 inert,onboarding 步骤挂载时面板会自行关闭,而不是留在它背后仍可聚焦。
|
||||
- Platform overlay 的返回栏只是 darwin 的 drag 行。它原先那条按表声明的 `app-region: drag` 没有任何平台限定,因此 Windows 上也能从这条 48px 栏拖动窗口;改到唯一那条带作用域的规则之后,该栏只在 macOS 上声明 drag,在那里它就是窗口顶条。Windows 保留原生标题栏行作为拖拽面,且外壳没有 Linux 桌面目标。人工状态矩阵会为这条栏增加一格。
|
||||
- 跨越隐藏/显示边界滑动的行不能只靠布局变化:只有当计算出的 app-region 值变化时,Electron 才会重新收集窗口的拖拽矩形(electron#32341),而 Blink 收集时会跳过隐藏的盒子。watcher 一次性把这个缺口对所有行补上——它在"触及被打标行的 DOM 变化"或"被打标行的盒子尺寸变化"时重新启动,随后在盒子仍在移动的每一帧发脉冲——因此右栏不再自带任何脉冲。
|
||||
@@ -41,11 +43,13 @@ macOS 桌面窗口的可拖区域被写了两遍。布局决定 chrome 行在哪
|
||||
## Testing
|
||||
|
||||
- `packages/client/web/tests/window-drag-regions.client.spec.ts` —— 合成规则,含顺序敏感与盒子边界。
|
||||
- `packages/client/web/tests/base-styles.client.spec.ts` —— base.css 声明的正是模型的交互选择器、唯一那条 darwin drag 规则,以及 recall 标记的减除。
|
||||
- `packages/client/web/tests/base-styles.client.spec.ts` —— base.css 声明模型的交互选择器和控件作用范围、逐元素重置、视口排除、唯一那条 darwin drag 规则,以及 recall 标记的减除。
|
||||
- `packages/client/ui-theme/tests/app-region-styles.client.spec.ts` —— 拖拽所有权、行清单与每行的标记;清单之外的标记或行高变化即失败。
|
||||
- `packages/client/ui-dockkit/tests/app-region-styles.client.spec.ts` —— strip 行自己的减除;它的 drag 属于 markup 标记,因此样式表一条都不声明。
|
||||
- `packages/client/ui-dockkit/tests/app-region-styles.client.spec.ts` —— 面板、strip 末端 chrome、浮动面板和分隔条排除自身矩形;面板的排除矩形覆盖其正文。strip 行的 drag 属于 markup 标记,因此样式表不声明 drag。
|
||||
- `packages/client/web/tests/window-drag-recall.client.spec.ts` —— 通过两个 seam 驱动 watcher:什么会重新启动它、什么被它忽略、收敛循环、外壳默认值,以及销毁。
|
||||
- `apps/web/tests/window-drag-coverage.e2e.ts` —— 真 Chromium:无控件被吞、逐行探测、一个页头行可拖而正文不可拖的详情页,以及右栏滑动期间外壳发出的 recall 脉冲。
|
||||
- `apps/web/tests/window-drag-coverage.e2e.ts` —— 真 Chromium:控件与区域所有者分开探测,被裁剪的转录区与标签内容不能减除页头,含浮动面板的全屏右栏保留预期排除,inert 的保留面板不影响前景行。插件管理页头的紧凑态场景检查标记页头的矩形不会延伸到列表控件上方。逐行探测、详情页正文排除、浮层和右栏 recall 脉冲仍有覆盖。
|
||||
- `apps/web/tests/window-drag-inheritance.e2e.ts` —— 已安装的 Chrome 报告版本,并验证继承的排除、生产重置、显式 `none` 与 `initial` 的区别,以及移除重置的负对照。未安装 Chrome 或运行时不支持属性继承时,显式跳过本场景;断言和其他启动错误正常失败。常规 Chromium 149 车道不实现继承,因此单靠该车道不能验证重置行为。
|
||||
- 运行时诊断(2026-11-02,Electron 44.7.0 / Chromium 152.0.7977.130):独立隐藏 BrowserWindow 确认,元素重置前子元素计算为 `no-drag`,重置后为 `none`;显式 `none` 计算为 `no-drag`,`initial` 计算为 `none`。该诊断不加载 DSH 产品代码,仅检查交付内核的 CSS 属性行为,不属于原生状态矩阵验收。
|
||||
- 真机验收(2026-09-20,双平台,HID 级指针输入 + 窗口矩形读数):上面的 macOS 矩阵在真实 `hiddenInset` 窗口上跑过,Windows 各格在 Windows 11 ARM64 虚拟机上跑过——chrome 行可拖、正文与覆盖浮层不可拖、控件保留点击、右栏 strip 行在滑入后可拖。
|
||||
|
||||
## Manual state matrix
|
||||
@@ -58,7 +62,7 @@ macOS 桌面窗口的可拖区域被写了两遍。布局决定 chrome 行在哪
|
||||
| 侧栏展开、选中会话、单视图(无 tab) | 同上,外加 header 之下的转录区首行 |
|
||||
| 侧栏收起 | 覆盖中列的 会话 header、`shell.leading` 座的控件、重新打开与新会话 |
|
||||
| 右栏 push | 面板 strip 行及其控件、strip 下方的 pane body 段、分栏分隔条的顶部一段 |
|
||||
| 右栏 fullscreen | 红绿灯处的面板 strip 行、首格 strip 内边距、其控件 |
|
||||
| 右栏 fullscreen | 红绿灯处的面板 strip 行、首格 strip 内边距、其控件,分别检查有无浮动面板 |
|
||||
| 入口页(开始页、插件管理、设置) | 含 48px 避让量的页头、它的首个控件行,以及详情页页头行与其正文 |
|
||||
| 窗口全屏开关 | 以上各行在红绿灯隐藏时、两种侧栏状态各一遍 |
|
||||
| 在 chrome 行空白段双击 | Windows 上 caption 行走系统标题栏行为(最大化/还原);macOS 上 darwin 行只是 app-region 区域而非标题栏,因此那里没有系统动作——只有控件保留自己的双击 |
|
||||
@@ -9,11 +9,11 @@
|
||||
en: d16c3bfef49e16be
|
||||
zh: 6714da1c9c984fe9
|
||||
/agent-note-add-the-hoc-slot-kind-so-plugins-can-wrap-owner-content-in-layers/decision:
|
||||
en: 3a4380324cfc1679
|
||||
zh: 44db368e091cbd46
|
||||
en: d23bc3c8c3b20ac9
|
||||
zh: 874c7a4dda283e89
|
||||
/agent-note-add-the-hoc-slot-kind-so-plugins-can-wrap-owner-content-in-layers/decision/implementation:
|
||||
en: 01189e457c91deeb
|
||||
zh: 94f7e8865b332a07
|
||||
en: 02ec3c2bfad692af
|
||||
zh: 229a3ae12eac49a6
|
||||
/agent-note-add-the-hoc-slot-kind-so-plugins-can-wrap-owner-content-in-layers/decision/first-consumer:
|
||||
en: 1afdf4ceeef7a3b3
|
||||
zh: be01cf39850db0ca
|
||||
@@ -21,5 +21,5 @@
|
||||
en: b4824619c502b422
|
||||
zh: 610364f2dc8cb2b7
|
||||
/agent-note-add-the-hoc-slot-kind-so-plugins-can-wrap-owner-content-in-layers/consequences:
|
||||
en: 7542ee2bc2c70258
|
||||
zh: bc0dd1dc6e50de66
|
||||
en: 6d350e8f9df1d33a
|
||||
zh: 5411b39fc4cc14ce
|
||||
@@ -20,27 +20,29 @@ The previous approach had the Conversation classify blank clicks on its body and
|
||||
|
||||
## Decision
|
||||
|
||||
[The Markdown fence keyed hoc slot note](2026-11-05-markdown-fence-hoc-slot.md) records why hoc layers take an optional dispatch `key`, may omit `children`, share compiled chains per key, and stay registered after a crash.
|
||||
|
||||
Add a fifth kind: `hoc`.
|
||||
|
||||
| Item | Contract |
|
||||
|---|---|
|
||||
| Declaration | The parent entry's `children` lists `{ kind: 'hoc', scope }`; `SlotMap` declares `kind: 'hoc'` |
|
||||
| Rendering | `renderSlot(key, owner, { children })`; `entryKey` / `only` / `fallback` / `hookContext` are unavailable; omitted `children` renders the layers around nothing |
|
||||
| Registration | Same as `list`: required `id`, optional `order` and `priority`; the same `id` at the same `priority` throws, a different `priority` shadows |
|
||||
| Rendering | `renderSlot(key, owner, { children, entryKey })`; `only` / `fallback` / `hookContext` are unavailable; omitted `children` renders the layers around nothing; omitted `entryKey` applies only wildcard layers |
|
||||
| Registration | Required `id`, optional `key`, `order`, and `priority`; a layer without `key` is a wildcard serving every dispatch, a layer with `key` serves only that `entryKey`; the cell is `(key or wildcard, id)`, and within one `id` an exact-key layer replaces the wildcard layer for that key; the same cell at the same `priority` throws, a different `priority` shadows |
|
||||
| Nesting order | Ascending `order`, then `priority`, then registration order; lowest outermost |
|
||||
| Layer props | Every layer receives the same owner props plus the framework-supplied `children` (the next inner layer; the innermost is the owner content); `children` is not an owner prop, `HocShare` types it, and owner props must not use that name |
|
||||
| Layer obligation | Render `children` exactly once without creating a box of its own; do not change the props passed inward |
|
||||
| Layer obligation | Render `children` at most once; a layer that omits it replaces everything inside it; a layer that wraps `children` creates no box of its own unless its slot documents otherwise; do not change the props passed inward |
|
||||
| No entry | `children` renders unchanged |
|
||||
| Layer crash | Each layer has its own error boundary; a crashed layer renders its `children` and abdicates, so the next render omits it |
|
||||
| Layer crash | Each layer has its own error boundary; a crashed layer reports its error and renders its `children` at the occurrence that failed for the rest of that occurrence's mount, even when its owner props change; it does not abdicate, so other occurrences, including those dispatching the same key, keep it, and a remounted occurrence renders it again |
|
||||
| Content error | An error thrown by the wrapped content passes every layer as its original error and is reported against no layer |
|
||||
| Remounting | Adding or removing a layer remounts the content inside it; a `session` or `session-maybe` layer also remounts it when its Session binding changes |
|
||||
| Remounting | Adding or removing a layer that applies to a dispatch remounts the content inside it, while a registration for another key leaves it mounted; a `session` or `session-maybe` layer also remounts it when its Session binding changes |
|
||||
|
||||
### Implementation
|
||||
|
||||
| Package | Realization |
|
||||
|---|---|
|
||||
| `ui-slots` | `SlotKind` includes `'hoc'`; `HocRenderOpts`, `HocKeysOf`, `HocShare`; `RenderSlotFn` has a hoc-key overload taking optional `{ children }`; `KindOptions` has a hoc branch; `SlotCore.register` / `entriesOfSlot` key hoc cells by `id` |
|
||||
| `ui-renderer` | `HocOutlet` calls `composeHocLayers` when the slot's ledger version or declaration changes (`useMemo` on the outlet instance) and nests the layers into one render function. Each layer is one Component that fixes its scope-specific Entry (`StrictSessionEntry` / `SessionMaybeEntry` / `RootEntry`), entry, slot inject, and crash reporting at composition, and receives only `owner` and the next inner layer when rendering. With no layer the outlet returns the owner content directly. `HocContentBoundary` wraps each layer's inner content and rethrows its errors as `HocContentError`, which layer boundaries pass through and `HocOutletBoundary` unwraps to the original error. The Entry chain passes `children` to the registered Component as JSX children; non-hoc entries receive `undefined`. `SlotErrorBoundary` takes a `crashed` replacement node |
|
||||
| `ui-slots` | `SlotKind` includes `'hoc'`; `HocRenderOpts` (optional `children` and `entryKey`), `HocKeysOf`, `HocShare`; `RenderSlotFn` has a hoc-key overload taking optional `{ children, entryKey }`; `KindOptions` has a hoc branch with optional `key`; `SlotCore.register` / `entriesOfSlot` key hoc cells by `(key or wildcard, id)` |
|
||||
| `ui-renderer` | `HocOutlet` takes its render function from `hocChainFor`, a cache per host, live declaration, and dispatch key that every outlet dispatching the same key shares. Once per ledger version it reselects the key's layers with `hocLayersFor` (wildcard winners plus exact-key winners, an exact-key layer replacing the wildcard layer with its id, sorted by `order`) and calls `composeHocLayers` to nest them into one render function only when that ordered list changes by entry identity. Each layer is one Component that fixes its scope-specific Entry (`StrictSessionEntry` / `SessionMaybeEntry` / `RootEntry`), entry, slot inject, and crash reporting at composition, and receives only `owner` and the next inner layer when rendering. With no layer the outlet returns the owner content directly. `HocContentBoundary` wraps each layer's inner content and rethrows its errors as `HocContentError`, which layer boundaries pass through and `HocOutletBoundary` unwraps to the original error. The Entry chain passes `children` to the registered Component as JSX children; non-hoc entries receive `undefined`. `SlotErrorBoundary` takes a `crashed` replacement node, which a layer's boundary keeps rendering for the rest of its mount; layer crash reports pass `abdicate: false` |
|
||||
| `scripts/gen-client-catalog.ts` | Register options, a `children`-rendering example, and the `wraps-owner-content` replace risk for hoc slots |
|
||||
|
||||
### First consumer
|
||||
@@ -68,5 +70,5 @@ Add a fifth kind: `hoc`.
|
||||
- React 18 recollects the component stack where an error is rethrown, so a content error reaching boundaries above a hoc slot with layers carries a component stack that starts at `HocOutletBoundary`.
|
||||
- A layer that wraps its `children` in its own error boundary catches the internal `HocContentError` rather than the original content error.
|
||||
- The frame contract holds only on the home: other phases leave the Conversation root out of the stacking order, so unportaled Conversation popovers still rise above AppFrame chrome that floats over the column, while on the home they stack only within the root.
|
||||
- `HocOutlet` keeps its layer Components through `useMemo`, as Factory local Components keep theirs; if React ever discards memoized values, both need an explicit cache, because these Components are the remount boundary of the content inside them.
|
||||
- `HocOutlet` keeps its layer Components in a compile cache shared per host, live declaration, and dispatch key, not in outlet-instance state, so they do not depend on React retaining memoized values; these Components are the remount boundary of the content inside them, and a redeclared slot starts with an empty cache.
|
||||
- [The slot system standard](2026-07-22-slot-type-chain-implementation.md) remains authoritative for the other kinds; this note adds the fifth.
|
||||
@@ -20,27 +20,29 @@ Status: implemented
|
||||
|
||||
## Decision
|
||||
|
||||
[Markdown fence keyed hoc slot 的 Note](2026-11-05-markdown-fence-hoc-slot.zh.md) 记录了 hoc 层为何接受可选的分派 `key`、可以不渲染 `children`、为何按 key 共享编译好的层链,以及为何崩溃后仍保持注册。
|
||||
|
||||
新增第五种 kind:`hoc`。
|
||||
|
||||
| 项 | 约定 |
|
||||
|---|---|
|
||||
| 声明 | 父 entry 的 `children` 写 `{ kind: 'hoc', scope }`;`SlotMap` 中 `kind: 'hoc'` |
|
||||
| 渲染 | `renderSlot(key, owner, { children })`;`entryKey` / `only` / `fallback` / `hookContext` 不可用;省略 `children` 时各层包裹空内容 |
|
||||
| 注册 | 与 `list` 相同:必填 `id`,可选 `order`、`priority`;同 `id` 同 `priority` 抛错,不同 `priority` 遮蔽 |
|
||||
| 渲染 | `renderSlot(key, owner, { children, entryKey })`;`only` / `fallback` / `hookContext` 不可用;省略 `children` 时各层包裹空内容;省略 `entryKey` 时只叠加通配层 |
|
||||
| 注册 | 必填 `id`,可选 `key`、`order`、`priority`;不带 `key` 的层是通配层,服务所有分派,带 `key` 的层只服务该 `entryKey`;cell 为 `(key 或通配, id)`,同一 `id` 下精确 key 的层替换该 key 的通配层;同 cell 同 `priority` 抛错,不同 `priority` 遮蔽 |
|
||||
| 嵌套顺序 | 依次按 `order`、`priority`、注册顺序升序,最小者在最外层 |
|
||||
| 层的 props | 每层收到同一份 owner props,加上框架注入的 `children`(内一层,最内层是宿主内容);`children` 不属于 owner props,类型由 `HocShare` 提供,owner props 不得使用这个名字 |
|
||||
| 层的义务 | 恰好渲染一次 `children`,自身不产生盒子;不能修改传给内层的 props |
|
||||
| 层的义务 | 至多渲染一次 `children`;不渲染 `children` 的层替换其内部全部内容;包裹 `children` 的层自身不产生盒子(其 slot 另有说明的除外);不能修改传给内层的 props |
|
||||
| 无 entry | 原样渲染 `children` |
|
||||
| 层崩溃 | 每层有独立 error boundary;崩溃时该层渲染自己的 `children` 并 abdicate,下一次渲染不再包含该层 |
|
||||
| 层崩溃 | 每层有独立 error boundary;崩溃时错误照常上报,该层在出错的那一处渲染自己的 `children`,直到该处本次挂载结束,owner props 变化也不恢复;该层不 abdicate,因此其他出现处(包括分派同一 key 的出现处)仍保留它,重新挂载的出现处会再次渲染它 |
|
||||
| 内容出错 | 被包裹内容抛出的错误以原始错误穿过所有层,不记到任何层名下 |
|
||||
| 重新挂载 | 增减一层会让其内部内容重新挂载;`session` 或 `session-maybe` 作用域的层在 Session binding 变化时也会让内部内容重新挂载 |
|
||||
| 重新挂载 | 增减一个对本次分派生效的层会让其内部内容重新挂载,为其他 key 注册的层不会;`session` 或 `session-maybe` 作用域的层在 Session binding 变化时也会让内部内容重新挂载 |
|
||||
|
||||
### 实现
|
||||
|
||||
| 包 | 实现 |
|
||||
|---|---|
|
||||
| `ui-slots` | `SlotKind` 包含 `'hoc'`;`HocRenderOpts`、`HocKeysOf`、`HocShare`;`RenderSlotFn` 为 hoc key 提供接受可选 `{ children }` 的重载;`KindOptions` 有 hoc 分支;`SlotCore.register` / `entriesOfSlot` 按 `id` 划分 hoc cell |
|
||||
| `ui-renderer` | `HocOutlet` 在插槽 ledger 版本或声明变化时调用 `composeHocLayers`(`useMemo` 缓存在 outlet 实例上),把各层组合成一个嵌套渲染函数。每层是一个组件,在组合时固定按 scope 选定的 Entry(`StrictSessionEntry` / `SessionMaybeEntry` / `RootEntry`)、entry、slot inject 与崩溃上报,渲染时只接收 `owner` 与内一层。没有层时 outlet 直接返回宿主内容。`HocContentBoundary` 包住每层的内层内容,把其中的错误包装为 `HocContentError` 重新抛出;层的边界放行这种错误,`HocOutletBoundary` 再解开为原始错误。Entry 链以 JSX children 把 `children` 交给注册组件;非 hoc entry 收到 `undefined`。`SlotErrorBoundary` 接受 `crashed` 替换节点 |
|
||||
| `ui-slots` | `SlotKind` 包含 `'hoc'`;`HocRenderOpts`(可选 `children` 与 `entryKey`)、`HocKeysOf`、`HocShare`;`RenderSlotFn` 为 hoc key 提供接受可选 `{ children, entryKey }` 的重载;`KindOptions` 的 hoc 分支含可选 `key`;`SlotCore.register` / `entriesOfSlot` 按 `(key 或通配, id)` 划分 hoc cell |
|
||||
| `ui-renderer` | `HocOutlet` 从 `hocChainFor` 取渲染函数,该缓存按 host、存活声明与分派 key 划分,分派同一 key 的所有 outlet 共享。每个 ledger 版本它只用 `hocLayersFor` 重新选出一次该 key 的层(通配胜者加精确 key 胜者,精确 key 的层替换同 id 的通配层,按 `order` 排序),仅当这一有序列表按 entry 身份比较发生变化时才调用 `composeHocLayers` 把各层组合成一个嵌套渲染函数。每层是一个组件,在组合时固定按 scope 选定的 Entry(`StrictSessionEntry` / `SessionMaybeEntry` / `RootEntry`)、entry、slot inject 与崩溃上报,渲染时只接收 `owner` 与内一层。没有层时 outlet 直接返回宿主内容。`HocContentBoundary` 包住每层的内层内容,把其中的错误包装为 `HocContentError` 重新抛出;层的边界放行这种错误,`HocOutletBoundary` 再解开为原始错误。Entry 链以 JSX children 把 `children` 交给注册组件;非 hoc entry 收到 `undefined`。`SlotErrorBoundary` 接受 `crashed` 替换节点,层的边界在本次挂载内一直渲染该节点;层的崩溃上报传 `abdicate: false` |
|
||||
| `scripts/gen-client-catalog.ts` | hoc 插槽的注册选项、渲染 `children` 的示例,以及 `wraps-owner-content` 替换风险 |
|
||||
|
||||
### 首个使用者
|
||||
@@ -68,5 +70,5 @@ Status: implemented
|
||||
- React 18 会在错误被重新抛出的位置重新采集组件栈,因此有层的 hoc 插槽之上的边界收到的内容错误,组件栈从 `HocOutletBoundary` 开始。
|
||||
- 层如果自己在 `children` 外包错误边界,捕获到的是内部的 `HocContentError`,而不是原始的内容错误。
|
||||
- frame 契约只在首页阶段成立:其他阶段根元素不参与层叠隔离,未经 portal 的 Conversation 浮层仍能盖住悬浮在中栏上方的 AppFrame 元素;首页阶段它们只在根元素内部比较层级。
|
||||
- `HocOutlet` 与 Factory 局部组件一样用 `useMemo` 保持层组件身份;如果 React 将来会丢弃 memo 缓存,两处都需要改成显式缓存,因为这些组件是其内部内容的重新挂载边界。
|
||||
- `HocOutlet` 把层组件保存在按 host、存活声明与分派 key 共享的编译缓存中,而不是 outlet 实例状态里,因此不依赖 React 保留 memo 缓存;这些组件是其内部内容的重新挂载边界,重新声明的插槽从空缓存开始。
|
||||
- 其他 kind 仍以 [slot 体系标准](2026-07-22-slot-type-chain-implementation.zh.md) 为准;本 Note 新增第五种。
|
||||
@@ -0,0 +1,31 @@
|
||||
# Bilingual-pair consistency record for 2026-11-05-markdown-fence-hoc-slot.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-11-05-markdown-fence-hoc-slot.md
|
||||
/agent-note-dispatch-markdown-fences-by-language-through-a-keyed-hoc-slot:
|
||||
en: 08e828c3c043e2c1
|
||||
zh: badc07274d22c3d3
|
||||
/agent-note-dispatch-markdown-fences-by-language-through-a-keyed-hoc-slot/problem:
|
||||
en: 47f1840013d9a25a
|
||||
zh: daa30c8a43adb61d
|
||||
/agent-note-dispatch-markdown-fences-by-language-through-a-keyed-hoc-slot/decision:
|
||||
en: a15c7c13f167e5ac
|
||||
zh: ff261e3705c47ff0
|
||||
/agent-note-dispatch-markdown-fences-by-language-through-a-keyed-hoc-slot/decision/developer-facing-api:
|
||||
en: 03db9adf39724644
|
||||
zh: 59350008e49737c3
|
||||
/agent-note-dispatch-markdown-fences-by-language-through-a-keyed-hoc-slot/decision/hoc-slots-with-dispatch-keys:
|
||||
en: efd632de6cd9612b
|
||||
zh: 996ebdce579fa62a
|
||||
/agent-note-dispatch-markdown-fences-by-language-through-a-keyed-hoc-slot/decision/code-blocks:
|
||||
en: 8ca893405e949ced
|
||||
zh: d08790568180ec36
|
||||
/agent-note-dispatch-markdown-fences-by-language-through-a-keyed-hoc-slot/decision/spacing:
|
||||
en: 5a1f4e68a5eb8492
|
||||
zh: cc50dc7d70fe6e3b
|
||||
/agent-note-dispatch-markdown-fences-by-language-through-a-keyed-hoc-slot/alternatives-considered:
|
||||
en: 7cc525b5fc20a009
|
||||
zh: 00698b2f024a29e8
|
||||
/agent-note-dispatch-markdown-fences-by-language-through-a-keyed-hoc-slot/consequences:
|
||||
en: fd3f897b3fd9fc1b
|
||||
zh: 75ab3c49866662e0
|
||||
@@ -0,0 +1,96 @@
|
||||
# Agent Note: Dispatch Markdown fences by language through a keyed hoc slot
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-11-05-markdown-fence-hoc-slot.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
A Markdown fence is a fenced code block: an opening line of three or more backticks or tildes followed by an info string, such as `ts title=demo`, then the body and a closing fence. The info string's first word names the fence's language.
|
||||
|
||||
The Chat flow renders fences by language, and plugins need to register, replace, or decorate the rendering of one language. Primitives choose fence rendering in fixed branches — `renderCode` in [render.tsx](../../../../packages/client/ui-primitives/src/markdown/render.tsx) sends `math` to KaTeX, [CodeBlock.tsx](../../../../packages/client/ui-primitives/src/markdown/CodeBlock.tsx) colors `diff` / `patch` rows, and other languages go to shiki. [ui-primitives](../../../../packages/client/ui-primitives/README.md) has no Cordis dependency, so its components cannot reach slots, and the other `MarkdownText` callers must keep their rendering unchanged.
|
||||
|
||||
## Decision
|
||||
|
||||
### Developer-facing API
|
||||
|
||||
The API has two parts: a delegate that hands each fence to its owner, and a keyed hoc slot through which plugins register language renderers.
|
||||
|
||||
| Part | Contract |
|
||||
|---|---|
|
||||
| Delegate | `MarkdownDelegate.renderCode?(fence: MarkdownDelegateFenceInfo): ReactNode`. An owner that supplies it on `MarkdownDelegateProvider` renders every fence in that scope that has a language and content |
|
||||
| Fence info | `lang` (the leading letters, digits, `_`, and `-` of the info string's language word, lowercased, so `c++` yields `c`), `code` (the body), `streaming` (whether the document is still streaming), and optional `meta` (the info string after the language word, as written) |
|
||||
| Keyed hoc slot | The owner dispatches each fence to its hoc slot with `entryKey: fence.lang` and passes the plain code block as `children`; plugins register layers by language `key` |
|
||||
| Chat flow | ChatView supplies the delegate and declares `conversation.chat.markdown.fence` (hoc, session scope) with `MarkdownDelegateFenceInfo` as owner props; its scope is every `MarkdownText` inside `conversation.chat.flow` |
|
||||
|
||||
A plugin registers a layer for one language:
|
||||
|
||||
```text
|
||||
ctx.slots.inject('conversation.chat.markdown.fence', () => ctx.slots.register(
|
||||
{ name: 'conversation.chat.markdown.fence', key: 'mermaid', id: '@acme/plugin/mermaid' },
|
||||
({ code, streaming, children }) => (streaming ? children : <MermaidDiagram source={code} />),
|
||||
))
|
||||
```
|
||||
|
||||
The API guarantees:
|
||||
|
||||
- `lang` and `meta` are final when `renderCode` first runs: the info-string line ends before content appears, and a fence without content takes the empty-fence branch.
|
||||
- The plain code block is the innermost `children` of every dispatch. A language without a layer, a layer that renders `children`, and a crashed layer all show it.
|
||||
- Each streamed chunk reaches the same layer instance as new owner props; the layer owns streaming. A layer that cannot render incomplete content renders `children` while `streaming`.
|
||||
- A layer that omits `children` replaces the code block; a layer that renders `children` decorates it. A layer may parse `meta` itself and keeps its root free of outer margins.
|
||||
- A document without `renderCode` keeps the built-in rendering: diff rows follow `lang`, and `math` renders as KaTeX once settled.
|
||||
- Chat registers `math` (source while streaming, display TeX once settled) and `diff` / `patch` (unified-diff rows, `children` omitted) under literal package-path ids.
|
||||
|
||||
### Hoc slots with dispatch keys
|
||||
|
||||
| Item | Contract |
|
||||
|---|---|
|
||||
| Registration | Optional `key`; a layer without `key` is a wildcard that serves every dispatch |
|
||||
| Cell | `(key or wildcard, id)`; the same cell at the same `priority` throws, and a lower `priority` shadows |
|
||||
| Dispatch | `renderSlot(name, owner, { entryKey, children })` applies wildcard layers and layers whose `key === entryKey`; without `entryKey`, only wildcard layers apply |
|
||||
| Exact key first | Within one `id`, an exact-key layer replaces the wildcard layer |
|
||||
| Nesting | Ascending `order`, then `priority`, then registration order; lowest `order` outermost |
|
||||
| children | Rendered at most once; omitting it replaces everything inside the layer |
|
||||
| Crash | The failed occurrence renders `children` for the rest of its mount and the error is reported; the layer stays registered (`abdicate: false`) and other occurrences keep it; the occurrence retries when it remounts |
|
||||
| Compilation | One chain per `(host, slot declaration, entryKey)`, shared by every outlet; each ledger version reselects that key's layers and recompiles only when they change, so a registration for another key leaves mounted content in place, while switching an outlet's `entryKey` switches chains |
|
||||
|
||||
These hoc semantics apply to every hoc slot, including `conversation.hero.frame`, whose layers are all wildcards. This note partially supersedes four decisions of [hoc slot kind](2026-11-03-hoc-slot-kind.md): rendering `children` exactly once, rejecting `entryKey`, per-outlet `useMemo` composition, and abdicating a crashed layer.
|
||||
|
||||
### Code blocks
|
||||
|
||||
| Item | Contract |
|
||||
|---|---|
|
||||
| `CodeBlock.diff` | Omitted follows `lang` (`diff` / `patch`); `true` or `false` forces unified-diff rows on or off for any `lang` |
|
||||
| `FenceCodeBlock` | Renders a fence body through `CodeBlock`, appending the trailing newline that `CodeBlock` trims, and forwards `diff`; ChatView's dispatch content passes `diff={false}` |
|
||||
| `FenceMathBlock` | Renders a `math` fence body as display KaTeX |
|
||||
| `MarkdownFenceSeat` | The Component in `render.tsx` that reads the delegate, so cached streaming elements follow a Provider change; without `renderCode` it renders the two presets |
|
||||
|
||||
### Spacing
|
||||
|
||||
Primitives wrap each delegated fence in a block box, `fenceSeat`, the direct child of `.markdown` or `li`. The box takes a code block's spacing among its siblings, so the slot's `display: contents` anchor inside it does not affect the document rhythm:
|
||||
|
||||
| Rule | Box value |
|
||||
|---|---|
|
||||
| Default | `margin: 16px 0` |
|
||||
| Followed by a sibling | `margin-bottom: 11px` |
|
||||
| compact variant | `margin: 4px 0` |
|
||||
| Last child of a list item | Keeps its margins, like a code block |
|
||||
| `.md-code-block` and `.katex-display` inside the box | `margin-block: 0` |
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Keep `diff` and `math` on fixed primitives branches and open a slot only for new languages**: two mechanisms, and built-in renderings cannot be replaced or decorated.
|
||||
- **Register the default code block as a wildcard layer**: other layers would need its `order`, and a layer ordered inside it would never mount because the default omits `children`; as hoc content it needs no id or `order`.
|
||||
- **Restore spacing with selectors that skip one wrapper level instead of a box**: they assume exactly one wrapper and one root element, and cannot restore the 11px gap.
|
||||
- **Abdicate a crashed layer, as single, keyed, and list do**: one content-triggered crash would remove that language's rendering from every fence until re-registration.
|
||||
- **Retry a crashed occurrence when owner props change**: needs state and shallow comparison in the error boundary every kind shares, and a faulty layer would report once per streamed chunk.
|
||||
- **Compose layers per outlet**: every same-language fence would compose the same chain, and any registration would remount every fence.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Plugins add, replace, or decorate Chat flow fence rendering by language without changing primitives; `math`, `diff` / `patch`, and third-party languages share one mechanism.
|
||||
- `MarkdownText` callers without `renderCode` render the built-in blocks, with no box and no slot.
|
||||
- A layer crash affects only the fence that failed; a faulty layer crashes and reports again at each mount while that fence shows the code block.
|
||||
- Layers of every hoc slot may omit `children`, so a `conversation.hero.frame` layer can hide the hero content.
|
||||
- Each delegated fence adds one block box and one slot outlet (one registry subscription, one locale subscription, and one `display: contents` element); compiled chains are shared per key.
|
||||
- In the Chat flow, display formula spacing comes from the box rather than KaTeX's default 1em, fence banners show the lowercase `lang`, and the `labels` of `conversation.chat.reasoning.content` apply only to footnotes and fences without a language.
|
||||
@@ -0,0 +1,96 @@
|
||||
# Agent Note: Markdown fence 按语言分派到 keyed hoc slot
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-11-05-markdown-fence-hoc-slot.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
Markdown fence 指围栏代码块:开头一行是三个或更多反引号(或波浪号),后接 info string,例如 `ts title=demo`;随后是正文和闭合围栏。info string 的第一个词表示 fence 的语言。
|
||||
|
||||
会话流要按语言渲染 fence,插件需要能注册、替换或装饰某一种语言的渲染。primitives 在固定分支里选择 fence 的渲染方式:[render.tsx](../../../../packages/client/ui-primitives/src/markdown/render.tsx) 的 `renderCode` 把 `math` 交给 KaTeX,[CodeBlock.tsx](../../../../packages/client/ui-primitives/src/markdown/CodeBlock.tsx) 给 `diff` / `patch` 画行着色,其余语言走 shiki。[ui-primitives](../../../../packages/client/ui-primitives/README.zh.md) 不依赖 Cordis,组件拿不到 slot;其他 `MarkdownText` 调用方的渲染必须保持不变。
|
||||
|
||||
## 决定
|
||||
|
||||
### 面向开发者的 API
|
||||
|
||||
API 分两部分:把每个 fence 交给 owner 的 delegate,以及插件按语言注册渲染器的 keyed hoc slot。
|
||||
|
||||
| 部分 | 约定 |
|
||||
|---|---|
|
||||
| Delegate | `MarkdownDelegate.renderCode?(fence: MarkdownDelegateFenceInfo): ReactNode`。在 `MarkdownDelegateProvider` 上提供它的 owner,负责渲染该范围内每个带语言且有内容的 fence |
|
||||
| Fence 信息 | `lang`(info string 语言词开头的字母、数字、`_`、`-` 部分转小写,`c++` 得 `c`)、`code`(正文)、`streaming`(文档是否仍在流式输出)、可选的 `meta`(语言词之后的原文) |
|
||||
| Keyed hoc slot | owner 用 `entryKey: fence.lang` 把每个 fence 分派到自己的 hoc slot,并把普通代码块作为 `children` 传入;插件按语言 `key` 注册层 |
|
||||
| 会话流 | ChatView 提供 delegate 并声明 `conversation.chat.markdown.fence`(hoc,session scope),owner props 为 `MarkdownDelegateFenceInfo`;覆盖 `conversation.chat.flow` 内的全部 `MarkdownText` |
|
||||
|
||||
插件为一种语言注册一层:
|
||||
|
||||
```text
|
||||
ctx.slots.inject('conversation.chat.markdown.fence', () => ctx.slots.register(
|
||||
{ name: 'conversation.chat.markdown.fence', key: 'mermaid', id: '@acme/plugin/mermaid' },
|
||||
({ code, streaming, children }) => (streaming ? children : <MermaidDiagram source={code} />),
|
||||
))
|
||||
```
|
||||
|
||||
API 保证:
|
||||
|
||||
- `renderCode` 第一次运行时 `lang` 与 `meta` 已经定稿:内容出现之前 info string 那一行已经结束,没有内容的 fence 走空 fence 分支。
|
||||
- 普通代码块是每次分派最内层的 `children`。没有对应层的语言、渲染 `children` 的层、崩溃的层,显示的都是它。
|
||||
- 每个流式 chunk 以新的 owner props 送到同一个层实例,流式由层自己处理。处理不了不完整内容的层,在 `streaming` 期间渲染 `children`。
|
||||
- 不渲染 `children` 的层替换代码块,渲染 `children` 的层装饰代码块。层可以自行解析 `meta`,根元素不设外边距。
|
||||
- 没有 `renderCode` 的文档保持内建渲染:diff 行按 `lang` 自动选择,`math` 落定后渲染为 KaTeX。
|
||||
- Chat 以字面量包路径 id 注册 `math`(流式显示源码,落定后显示 display TeX)与 `diff` / `patch`(统一 diff 行,不渲染 `children`)。
|
||||
|
||||
### 带分派 key 的 hoc slot
|
||||
|
||||
| 项 | 约定 |
|
||||
|---|---|
|
||||
| 注册 | 可选 `key`;不带 `key` 的层是通配层,作用于每次分派 |
|
||||
| 格子 | `(key 或通配, id)`;同一格子同一 `priority` 重复注册报错,更低的 `priority` 遮蔽 |
|
||||
| 分派 | `renderSlot(name, owner, { entryKey, children })` 叠加通配层与 `key === entryKey` 的层;不传 `entryKey` 时只叠加通配层 |
|
||||
| 精确 key 优先 | 同一 `id` 下,精确 key 的层替换通配层 |
|
||||
| 嵌套 | 依次按 `order`、`priority`、注册顺序升序,`order` 最小的在最外层 |
|
||||
| children | 至多渲染一次;不渲染即替换该层内部的全部内容 |
|
||||
| 崩溃 | 出错的那一处在本次挂载内渲染 `children`,错误照常上报;层保持注册(`abdicate: false`),其他出现处保留该层;该处重新挂载时重试 |
|
||||
| 编译 | 每个 `(host, slot 声明, entryKey)` 一条层链,所有 outlet 共享;每个 ledger 版本重新选取该 key 的层,只有层变化时才重编,因此为其他 key 注册的层不会让已挂载内容重新挂载,outlet 切换 `entryKey` 则换到另一条层链 |
|
||||
|
||||
这些 hoc 语义适用于所有 hoc slot,包括层全部为通配层的 `conversation.hero.frame`。本 note 部分取代 [hoc slot kind](2026-11-03-hoc-slot-kind.zh.md) 的四处决定:恰好渲染一次 `children`、不接受 `entryKey`、按 outlet `useMemo` 组合层、崩溃层 abdicate。
|
||||
|
||||
### 代码块
|
||||
|
||||
| 项 | 约定 |
|
||||
|---|---|
|
||||
| `CodeBlock.diff` | 不传时按 `lang`(`diff` / `patch`)自动选择;`true` 或 `false` 对任意 `lang` 强制开关统一 diff 行 |
|
||||
| `FenceCodeBlock` | 经 `CodeBlock` 渲染 fence 正文,补上 `CodeBlock` 会去掉的结尾换行,并透传 `diff`;ChatView 分派的 content 传 `diff={false}` |
|
||||
| `FenceMathBlock` | 把 `math` fence 正文渲染为 display KaTeX |
|
||||
| `MarkdownFenceSeat` | `render.tsx` 中读取 delegate 的组件,流式缓存的元素因此跟随 Provider 变化;没有 `renderCode` 时渲染上面两个预设 |
|
||||
|
||||
### 间距
|
||||
|
||||
primitives 把每个经 delegate 渲染的 fence 包进块级外框 `fenceSeat`,外框是 `.markdown` 或 `li` 的直接子元素。外框承担代码块与相邻块之间的间距,所以外框内 slot 的 `display: contents` 锚点不影响文档节奏:
|
||||
|
||||
| 规则 | 外框取值 |
|
||||
|---|---|
|
||||
| 默认 | `margin: 16px 0` |
|
||||
| 后面还有兄弟元素 | `margin-bottom: 11px` |
|
||||
| compact 变体 | `margin: 4px 0` |
|
||||
| 列表项最后一个子元素 | 与代码块一样保留外边距 |
|
||||
| 外框内的 `.md-code-block` 与 `.katex-display` | `margin-block: 0` |
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
- **`diff` 与 `math` 留在 primitives 的固定分支里,只为新语言开 slot**:两套机制,内建渲染无法被替换或装饰。
|
||||
- **默认代码块注册成通配层**:其他层需要知道它的 `order`,排在它里面的层永远不会挂载,因为默认层不渲染 `children`;作为 hoc content 则不需要任何 id 或 `order`。
|
||||
- **不加外框,用跳过一层包装的选择器恢复间距**:这些规则默认恰好一层包装、一个根元素,也补不回 11px 间距。
|
||||
- **像 single、keyed、list 那样让崩溃的层 abdicate**:一次由内容触发的崩溃会让该语言的所有 fence 失去渲染,直到重新注册。
|
||||
- **owner props 变化时重试崩溃的出现处**:需要在所有 kind 共用的错误边界里加状态与浅比较,有问题的层每个流式 chunk 都会报一次错。
|
||||
- **按 outlet 组合层**:每个同语言 fence 都要组合同一条层链,任何注册都会让所有 fence 重新挂载。
|
||||
|
||||
## 后果
|
||||
|
||||
- 插件无需改动 primitives,就能按语言新增、替换或装饰会话流 fence 的渲染;`math`、`diff` / `patch` 与第三方语言共用同一机制。
|
||||
- 没有 `renderCode` 的 `MarkdownText` 调用方渲染内建块,没有外框也没有 slot。
|
||||
- 层崩溃只影响出错的那个 fence;有问题的层每次挂载都会再崩一次、再报一次错,该 fence 显示代码块。
|
||||
- 所有 hoc slot 的层都可以不渲染 `children`,因此 `conversation.hero.frame` 的层可以隐藏 hero 内容。
|
||||
- 每个经 delegate 渲染的 fence 多一个块级外框和一个 slot outlet(一次注册表订阅、一次 locale 订阅、一个 `display: contents` 元素);编译结果按 key 共享。
|
||||
- 在会话流中,display 公式的间距来自外框而不是 KaTeX 默认的 1em,fence 横幅显示小写 `lang`,`conversation.chat.reasoning.content` 的 `labels` 只作用于脚注与无语言 fence。
|
||||
@@ -18,8 +18,8 @@
|
||||
en: c2a38b8c0403e1e4
|
||||
zh: 6063cf5e41a6582e
|
||||
/agent-note-persistent-pty-sessions/decision/security-and-process-boundary:
|
||||
en: 882bbfd3f0dc6e09
|
||||
zh: a547d6ff5e6ef7a1
|
||||
en: cf7666742795b0fe
|
||||
zh: 4fccb74f8271dca6
|
||||
/agent-note-persistent-pty-sessions/decision/six-model-facing-tools:
|
||||
en: b92715f36550c728
|
||||
zh: e18d7e0e2363347a
|
||||
|
||||
@@ -43,7 +43,7 @@ The [cleanup dependency proposal](../../proposed/architecture/2026-10-09-cordis-
|
||||
A registered `shell` backend constrains how a terminal starts; it does not constrain commands typed after startup. `dsh-terminal-bash` therefore applies two protections before spawning:
|
||||
|
||||
- It supplies only terminal-specific environment overrides; the mounted subprocess provider applies the shared credential-shaped-name scrub before merging them.
|
||||
- It requires the shared `ctx.sandboxPolicy`. At spawn, the backend resolves the owner's effective session mode over the deployment default; `danger-full-access` starts the shell directly, while confined modes require a same-world `ctx.sandbox` provider and wrap the shell argv once. That mode and workspace root remain the process boundary for the PTY lifetime. A write that would change the effective `sandbox/mode` is rejected before commit while the owner has any open PTY or unpublished spawn, with an instruction to wait for creation to settle and close those sessions first; same-effective-mode writes remain valid. The pending reservation spans backend setup through publication, so there is no race in which a wider terminal appears after a downgrade. `danger-full-access` is the existing explicit unconfined choice rather than a PTY-specific bypass.
|
||||
- It requires the shared `ctx.sandboxPolicy`. At spawn, the backend resolves the owner's effective session mode over the deployment default; `danger-full-access` starts the shell directly, while confined modes require a same-world `ctx.sandbox` provider and wrap the shell argv once. That mode and workspace root remain the process boundary for the PTY lifetime. A write that would change the effective `sandbox/mode` is rejected before commit while the owner has any open PTY or unpublished spawn, with an instruction to wait for creation to settle and close those sessions first; same-effective-mode writes remain valid. The pending reservation spans backend setup through publication, so there is no race in which a wider terminal appears after a downgrade. `danger-full-access` is the existing explicit unconfined choice rather than a PTY-specific bypass. The sidebar backend of the [Agent terminal panel decision](2026-10-12-agent-terminal-panel.md) applies the same launch policy without this mode-change fence.
|
||||
|
||||
Sandboxing confines local process effects but does not make arbitrary shell input safe: network calls and other external side effects remain governed by deployment policy. Tool descriptions state that PTY sessions are less auditable than one-shot tools and should be used only when persistence or interactive stdin is necessary.
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ agent scope dispose(资源释放)时先撤销注册,再等待全部所属
|
||||
注册的 `shell` 后端只约束终端如何启动,不约束启动后输入的命令。因此 `dsh-terminal-bash` 在 spawn 前应用两层保护:
|
||||
|
||||
- 它只提供终端专用的环境覆盖;挂载的子进程提供方先清除名称形似凭据的环境变量,再合并这些覆盖。
|
||||
- 它要求共享的 `ctx.sandboxPolicy`。后端在 spawn 时,以部署默认值为底折叠 owner 的有效会话模式;`danger-full-access` 会直接启动 shell,受限模式则要求同一执行世界中存在 `ctx.sandbox` 提供方,并只包装一次 shell argv。该模式与 workspace root 在 PTY 的整个生命周期中充当进程边界。只要 owner 有任何已打开的 PTY 或尚未发布的 spawn,任何会改变生效 `sandbox/mode` 的写入都会在提交前被拒绝,并提示先等待创建操作结算,再关闭这些会话;不会改变生效模式的写入仍然有效。这项进行中的预留从后端 setup 持续到发布完成,因此不存在降级后又出现权限更宽的终端这一竞态。`danger-full-access` 是现有的显式无约束选择,不另设 PTY 私有 bypass。
|
||||
- 它要求共享的 `ctx.sandboxPolicy`。后端在 spawn 时,以部署默认值为底折叠 owner 的有效会话模式;`danger-full-access` 会直接启动 shell,受限模式则要求同一执行世界中存在 `ctx.sandbox` 提供方,并只包装一次 shell argv。该模式与 workspace root 在 PTY 的整个生命周期中充当进程边界。只要 owner 有任何已打开的 PTY 或尚未发布的 spawn,任何会改变生效 `sandbox/mode` 的写入都会在提交前被拒绝,并提示先等待创建操作结算,再关闭这些会话;不会改变生效模式的写入仍然有效。这项进行中的预留从后端 setup 持续到发布完成,因此不存在降级后又出现权限更宽的终端这一竞态。`danger-full-access` 是现有的显式无约束选择,不另设 PTY 私有 bypass。[Agent 终端面板决策](2026-10-12-agent-terminal-panel.zh.md)中的侧栏后端采用相同的启动策略,但不设这道切换模式的栅栏。
|
||||
|
||||
沙箱限制本地进程副作用,但不会让任意 shell 输入自动安全:网络调用和其他外部副作用仍由部署策略治理。工具描述会说明 PTY 会话比一次性工具更难审计,只应在确实需要持久状态或交互式 stdin 时使用。
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
en: 74c06aa0b3e5337e
|
||||
zh: c13364dbaebabba1
|
||||
/agent-note-producer-declared-context-forms/decision:
|
||||
en: e97830627fd8245f
|
||||
zh: 0b035c15ed09902d
|
||||
en: ed244fc93ee1a5bf
|
||||
zh: f43dfcdd5bae39f2
|
||||
/agent-note-producer-declared-context-forms/why-not-a-presenter-registry:
|
||||
en: feb1b16eeb312e0c
|
||||
zh: 9eb3c5af2d510235
|
||||
|
||||
@@ -29,7 +29,7 @@ Entries record the published fact **unescaped**. The pseudo-XML escaping belongs
|
||||
|
||||
That move also relocates catalog **identity**: the republish digest now covers the durable entries rather than the rendered text, so the model-facing framing can no longer decide whether a republish is needed, and the text-slicing that recovered entries from a logged message is gone. The v0-to-v1 edge preserves an accepted older catalog payload, so a resumed session whose newest catalog predates this change republishes once. One case does not self-heal: if that old-form catalog is the only one and the current view has no skills, the plugin sees no published catalog and emits no tombstone, so the model keeps a stale catalog nothing replaces. This is the context feature's explicit degradation, not a Session-format refusal.
|
||||
|
||||
**`snapshot`** — current state that a later snapshot from the same producer supersedes. The runtime-context snapshot, `time-context`, and `tmux-context` declare it. `renderContextSections()` exposes the assembly's named contributions, which `renderContextSnapshot()` already joined for the model, so the body attributes each part to the subsystem that produced it without re-splitting joined prose. The two single-contribution producers record one section each. The cleared runtime-context marker has no contributions left and declares no form.
|
||||
**`snapshot`** — current state that a later snapshot from the same producer supersedes. The runtime-context snapshot, `time-context`, and `tmux-context` declare it. `renderContextSections()` exposes the assembly's named contributions; agent-loop frames them for the model and records their original text, so the body attributes each part without re-splitting joined prose. The public `renderContextSnapshot()` helper retains its standalone rendering. The two single-contribution producers record one section each. The cleared runtime-context marker declares `form: 'snapshot'` with empty `sections`; the body falls back to its model-facing text. The form does not assert that an earlier snapshot exists.
|
||||
|
||||
**`notice`** — a one-off account of something that just happened. `tool-jobs`, `tool-goal` wrap-up, `plan-mode` switches, and `repeat-tool-reminder` reminders declare it with a `summary`, which rides the **collapsed** row: a notice is meant to be read without expanding at all. The summary is bounded where its inputs are caller text (a task's label and status detail have no length of their own). Goal state changes remain domain-owned `goal/change` events rather than model context, so they declare no form.
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ Status: implemented
|
||||
|
||||
这次迁移同时挪动了目录的**身份**:重新发布用的 digest 现在覆盖持久条目而非渲染文本,于是面向模型的包装再也无法左右是否需要重新发布,那段从已记录消息里切出条目的文本切分逻辑也随之删除。v0-to-v1 迁移边会保留可接纳的旧目录 payload,因此恢复的 Session 若以本变更前的目录为最新目录,就会重新发布一次。有一种情形不会自愈:当那份旧结构目录是唯一的一份、且当前视图没有任何 skill 时,插件看不到已发布目录,也就不会发出 tombstone,模型手里会留着一份无人替换的陈旧目录。这是 context 功能的显式降级,不是 Session 格式拒绝。
|
||||
|
||||
**`snapshot`**——会被同一生产方后续快照取代的当前状态。运行时快照、`time-context`、`tmux-context` 声明它。`renderContextSections()` 暴露出装配时的具名贡献——`renderContextSnapshot()` 本来就是把它们拼给模型的——因此内容区能把每一段归属到产生它的子系统,而不必去切分已经拼好的散文。两个单贡献生产方各记录一段。运行时快照的「已清空」标记没有任何贡献可归属,因此不声明形态。
|
||||
**`snapshot`**——会被同一生产方后续快照取代的当前状态。运行时快照、`time-context`、`tmux-context` 声明它。`renderContextSections()` 暴露出装配时的具名贡献;agent-loop 将它们包装给模型,并记录原始文本,因此内容区能直接归属每一段,而不必切分已经拼好的散文。公开的 `renderContextSnapshot()` 辅助方法保留独立渲染行为。两个单贡献生产方各记录一段。运行时快照的「已清空」标记声明 `form: 'snapshot'` 并携带空 `sections`,内容区回退为面向模型的文本。这一形态本身不表示已有更早的快照。
|
||||
|
||||
**`notice`**——刚刚发生了什么的一次性说明。`tool-jobs`、`tool-goal` 收尾、`plan-mode` 切换与 `repeat-tool-reminder` 提醒都带 `summary` 声明它,而该摘要出现在**折叠态**行上:notice 的全部意义就是不展开也能读完。摘要在其输入是调用方文本时自行封顶(任务的 label 与状态 detail 本身没有长度约束)。Goal 状态变更仍是由领域层持有的 `goal/change` 事件,而非模型上下文,因此不声明 form。
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
en: 71813b1cb1f750ba
|
||||
zh: 64d2b999933ccd98
|
||||
/agent-note-web-sidebar-terminals/decision:
|
||||
en: ef54b8ca31bb71de
|
||||
zh: 0c05685c25010e8c
|
||||
en: d3acc67b30f2ccf5
|
||||
zh: 90f4b6db49b521ed
|
||||
/agent-note-web-sidebar-terminals/alternatives-considered:
|
||||
en: b0ffb94cd5033e9c
|
||||
zh: 9294bb76503c858a
|
||||
|
||||
@@ -22,7 +22,7 @@ Close and replacement remove the tab synchronously and run process cleanup in th
|
||||
|
||||
The caller retains a terminal ID before creating it. Repeating create with the same Session and open ID does not allocate a second process. Closing an uncertain create uses that ID even when no creation response arrived. The Host records closed IDs before awaiting allocation, preventing a delayed create from reviving a closed terminal. Each attachment begins with a consistent, bounded serialized xterm screen; ordered output follows through the Gateway's existing multiplexed Remote stream. Output and screen snapshots share one operation queue. Followers retain final output on normal closure and fail explicitly on buffer overflow. Browser render acknowledgement prevents React batching from dropping increments.
|
||||
|
||||
The latest attachment controls input and dimensions; other attachments remain read-only. Each physical `attach` stream owns its input and resize commands, including after automatic transport recovery. The Host orders commands and acknowledges provider acceptance; the Client bounds raw input and complete encoded frames, queues commands when the advertised carrier window is full, and resumes on acknowledgements. Superseded streams cannot write through their replacements, and unacknowledged input is never replayed. Terminal output creates no model input, Agent tool result or Session event. The existing [persistent Agent terminal decision](2026-07-16-persistent-pty-sessions.md) continues to govern model-owned sessions; this feature extends the [portable subprocess provider](../architecture/2026-07-28-portable-execution-world-consumers.md) with terminal environment facts and resize. Control-transfer and process-exit refusals only disable input, preserving the healthy output view. Client-owned error identifiers are translated by the terminal UI, including guidance to close retained exited terminals when the quota is full.
|
||||
The latest attachment controls input and dimensions; other attachments remain read-only. Each physical `attach` stream owns its input and resize commands, including after automatic transport recovery. The Host orders commands and acknowledges provider acceptance; the Client bounds raw input and complete encoded frames, queues commands when the advertised carrier window is full, and resumes on acknowledgements. Superseded streams cannot write through their replacements, and unacknowledged input is never replayed. Terminal output creates no model input, Agent tool result or Session event, except through the experimental tools of the [Agent terminal panel decision](2026-10-12-agent-terminal-panel.md), which partially supersedes this rule. The existing [persistent Agent terminal decision](2026-07-16-persistent-pty-sessions.md) continues to govern model-owned sessions; this feature extends the [portable subprocess provider](../architecture/2026-07-28-portable-execution-world-consumers.md) with terminal environment facts and resize. Control-transfer and process-exit refusals only disable input, preserving the healthy output view. Client-owned error identifiers are translated by the terminal UI, including guidance to close retained exited terminals when the quota is full.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ Web 用户需要在 Session 旁使用交互式 shell 检查工作区和运行命
|
||||
|
||||
调用者在创建之前保留终端 ID。相同 Session 和未关闭 ID 的重复 create 不再分配进程。创建结果不确定时,关闭仍使用该 ID,即使没有收到创建响应。Host 在等待分配完成前记录已关闭 ID,防止迟到的 create 复活已关闭终端。每次连接先接收一致、有界的 xterm 序列化屏幕,后续有序输出使用 Gateway 已有的复用 Remote stream。输出和屏幕快照共享操作队列。订阅者正常关闭时保留末尾输出,缓存超限时明确失败。浏览器在完成渲染后确认帧,避免 React 批处理丢失增量。
|
||||
|
||||
最新连接控制输入和尺寸,其他连接保持只读。每条物理 `attach` 流拥有自己的输入和尺寸命令,传输自动恢复后亦如此。Host 按序执行命令并确认提供者已接受操作;Client 限制原始输入和完整编码帧,在公布的载体窗口满时排队,并在收到确认后继续发送。旧流不能借用新流写入,未确认输入绝不重放。终端输出不产生模型输入、Agent 工具结果或 Session 事件。[Agent 持久终端决策](2026-07-16-persistent-pty-sessions.zh.md)仍约束模型拥有的终端;此功能为[可移植 subprocess provider](../architecture/2026-07-28-portable-execution-world-consumers.zh.md)增加执行环境事实和 resize。 控制权转移和进程退出导致的拒绝只禁用输入,保留健康的输出视图。Client 自产错误标识由终端 UI 翻译,包括名额用满时关闭已保留的退出终端的提示。
|
||||
最新连接控制输入和尺寸,其他连接保持只读。每条物理 `attach` 流拥有自己的输入和尺寸命令,传输自动恢复后亦如此。Host 按序执行命令并确认提供者已接受操作;Client 限制原始输入和完整编码帧,在公布的载体窗口满时排队,并在收到确认后继续发送。旧流不能借用新流写入,未确认输入绝不重放。终端输出不产生模型输入、Agent 工具结果或 Session 事件;[Agent 终端面板决策](2026-10-12-agent-terminal-panel.zh.md)的实验工具是例外,它部分取代了这条规则。[Agent 持久终端决策](2026-07-16-persistent-pty-sessions.zh.md)仍约束模型拥有的终端;此功能为[可移植 subprocess provider](../architecture/2026-07-28-portable-execution-world-consumers.zh.md)增加执行环境事实和 resize。 控制权转移和进程退出导致的拒绝只禁用输入,保留健康的输出视图。Client 自产错误标识由终端 UI 翻译,包括名额用满时关闭已保留的退出终端的提示。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
en: 12ab239ab0c58e31
|
||||
zh: c32874442ce66041
|
||||
/agent-note-explicit-worktree-creation/decision:
|
||||
en: e3be190201a36710
|
||||
zh: 9ef0036b9b95377e
|
||||
en: 92782d5964d1a021
|
||||
zh: 37b385836e9ae2b9
|
||||
/agent-note-explicit-worktree-creation/alternatives-considered:
|
||||
en: 72d42e892a37b94c
|
||||
zh: 92a14a51c6a89c53
|
||||
/agent-note-explicit-worktree-creation/consequences:
|
||||
en: 083851db27cc4846
|
||||
zh: b3db48d41356c9cf
|
||||
en: 2643edaca2e8b283
|
||||
zh: 6e2b66ce43638a39
|
||||
@@ -16,7 +16,9 @@ The source repository comes from the calling Session's current directory. Creati
|
||||
|
||||
Git and directory creation use the mounted subprocess and sandbox providers without widening the calling Session's policy. Both destination and shared Git metadata writes must already be permitted. Successful creation changes only the current working directory; the Session identity and original sandbox root remain owned by their existing services.
|
||||
|
||||
Leaving uses the shared working-directory operation and retains the checkout and branch. Failed setup can retain partial artifacts and reports their location. The service does not own a checkout registry, cleanup schedule, branch merge policy, or task association database.
|
||||
Leaving uses the shared working-directory operation and retains the checkout and branch. Failed setup can retain partial artifacts and reports their location. The service does not own a branch merge policy or a task association database.
|
||||
|
||||
The service records each creation as a `plugin:worktree/created` record and owns archive-triggered cleanup of those checkouts, as the job and Agent registries own their archive-admission families. A Session's inherited prefix decides whether it created a checkout or merely refers to one; cwd alone never grants cleanup ownership. Admission registers the attempt inside the Workspace write queue and returns; inspection reads cold Sessions through the projection cache or their log, without starting an Agent, and runs afterwards. Only a clean, unreferenced checkout is removed, without force and without deleting its branch; tracked changes and untracked files retain it, ignored files do not, matching Git's own non-force refusal. Processes and terminals are not consulted. A volatile `retainOnArchive` setting, read at admission, turns the cleanup off without unmounting the service; the Forge client edits it on the bundle's worktree row of the Plugins page because the service package has no Client half. Unarchiving cancels an unadmitted removal and waits for an admitted one, and directory entry into a checkout being removed waits for that removal. No startup sweep deletes previously archived worktrees.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -30,4 +32,4 @@ Leaving uses the shared working-directory operation and retains the checkout and
|
||||
|
||||
## Consequences
|
||||
|
||||
The caller can continue one Session in a new checkout with a reconstructable directory change. Retention avoids deleting work on exit or a failed final publication, but callers must manage unused checkouts and branches themselves. Git and Node must be available in the filesystem provider's execution world. Submodule setup and dependency installation remain separate operations.
|
||||
The caller can continue one Session in a new checkout with a reconstructable directory change. Retention avoids deleting work on exit or a failed final publication, while archiving reclaims a clean checkout without touching branch lifetime. Git and Node must be available in the filesystem provider's execution world. Submodule setup and dependency installation remain separate operations.
|
||||
@@ -16,7 +16,9 @@ Status: implemented
|
||||
|
||||
Git 与目录创建使用挂载的子进程及沙箱提供方,不扩大调用 Session 的策略。目标目录与共享 Git 元数据必须均已允许写入。成功创建只改变当前工作目录;Session 身份与原始沙箱根目录仍由已有服务拥有。
|
||||
|
||||
离开使用共享工作目录操作,保留检出目录与分支。初始化失败可能留下部分产物,并报告其位置。该服务不拥有工作树注册表、清理计划、分支合并策略或任务关联数据库。
|
||||
离开使用共享工作目录操作,保留检出目录与分支。初始化失败可能留下部分产物,并报告其位置。该服务不拥有分支合并策略或任务关联数据库。
|
||||
|
||||
服务把每次创建记录为 `plugin:worktree/created` Session 事件,并像 job 与 Agent 注册表各自拥有归档准入家族那样,拥有这些检出目录的归档触发清理。Session 的继承前缀决定它创建了某个检出目录还是仅引用它;当前工作目录本身不授予清理所有权。准入在 Workspace 写队列内登记尝试并返回;检查通过投影缓存或日志读取冷 Session,不启动 Agent,并在之后执行。只删除干净且无人引用的检出目录,不带 force 也不删除分支;跟踪文件改动与未跟踪文件保留目录,被忽略的文件不保留,与 Git 自身不带 force 时的拒绝条件一致。不咨询进程与终端。volatile 的 `retainOnArchive` 设置在准入时读取,无需卸载服务即可关闭清理;由于服务包没有 Client 半部,Forge 客户端在插件页该套件的 worktree 行上编辑它。取消归档会取消尚未获准的删除并等待已获准的删除,进入正在删除的检出目录会等待该删除结束。不在启动时扫描删除此前已归档的工作树。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -30,4 +32,4 @@ Git 与目录创建使用挂载的子进程及沙箱提供方,不扩大调用
|
||||
|
||||
## Consequences
|
||||
|
||||
调用方可以在新的检出目录继续同一个 Session,且目录切换可重建。保留策略避免在离开或最后发布失败时删除工作,但调用方必须自行管理闲置检出目录与分支。Git 与 Node 必须存在于文件系统提供方的执行环境中。子模块配置与依赖安装仍是独立操作。
|
||||
调用方可以在新的检出目录继续同一个 Session,且目录切换可重建。保留策略避免在离开或最后发布失败时删除工作,而归档会回收干净的检出目录,不触及分支生命周期。Git 与 Node 必须存在于文件系统提供方的执行环境中。子模块配置与依赖安装仍是独立操作。
|
||||
@@ -15,11 +15,11 @@
|
||||
en: 1b1c960c966cf912
|
||||
zh: 97be87cb14893c26
|
||||
/agent-note-plugin-market-index-format/decision/root-fields:
|
||||
en: 4fac5a9b8261342d
|
||||
zh: f9168a8a4209feab
|
||||
en: 1723f6d4f6ff1b67
|
||||
zh: 02021e246a8f7d9f
|
||||
/agent-note-plugin-market-index-format/decision/plugin-fields:
|
||||
en: b79abd05a957948b
|
||||
zh: b6f0c203ea8e4838
|
||||
en: d083215d5f5f94b8
|
||||
zh: e0175c8869be6ff4
|
||||
/agent-note-plugin-market-index-format/decision/version-fields:
|
||||
en: d6c0fa99e1cbb068
|
||||
zh: 3d2a7b85299c679a
|
||||
@@ -33,8 +33,8 @@
|
||||
en: 923849223cb1e70e
|
||||
zh: f3daf398bc06c81a
|
||||
/agent-note-plugin-market-index-format/decision/reader-rules:
|
||||
en: 92903cbe87d9c0ca
|
||||
zh: 54af6cb33e9da252
|
||||
en: d41a2bd38b0e3830
|
||||
zh: 5cdf73d5bda7d596
|
||||
/agent-note-plugin-market-index-format/alternatives-considered:
|
||||
en: 908407ed974101d2
|
||||
zh: b4b7391c513bda60
|
||||
|
||||
@@ -104,10 +104,12 @@ The market reuses npm's package names, versions, ranges, distribution tags, and
|
||||
| `metadata.categories` | no | Object mapping category id → `{ title: LocalizedText }`. |
|
||||
| `metadata.labels` | no | Object mapping label id → `{ title: LocalizedText }`. |
|
||||
| `metadata.sortKeys` | no | Object mapping sort-key id → `{ title: LocalizedText, order: "asc" \| "desc" }`; `order` is the initial sort direction. |
|
||||
| `plugins` | yes | Array of plugins, possibly empty; its order is the index's default ordering. |
|
||||
| `plugins` | yes | Array of plugins, possibly empty; clients must use this array order for the initial unfiltered view. |
|
||||
|
||||
Category, label, and sort-key ids are non-empty strings. Readers look ids up as own properties, so ids such as `constructor` have no special meaning.
|
||||
|
||||
Clients must preserve the publisher's `plugins` array order unless the user selects another sort or text search uses relevance ranking. Filtering alone preserves the relative array order of matching plugins. Declaring sort keys or publishing metric values does not authorize clients to choose a default order by popularity, downloads, or publish time.
|
||||
|
||||
### Plugin fields
|
||||
|
||||
| Field | Required | Rule |
|
||||
@@ -123,12 +125,16 @@ Category, label, and sort-key ids are non-empty strings. Readers look ids up as
|
||||
| `metadata.category` | no | One declared category id. |
|
||||
| `metadata.labels` | no | Array of distinct declared label ids. |
|
||||
| `metadata.keywords` | no | Array of free-form strings; no root declaration required. |
|
||||
| `metadata.sortKeys` | no | Object mapping a declared sort-key id → finite number. |
|
||||
| `metadata.sortKeys` | no | Object mapping a declared sort-key id → finite number or `null` for unavailable data. |
|
||||
| `dist-tags` | no | Object mapping npm distribution-tag names to exact listed version strings; absent means no recorded channels. |
|
||||
| `versions` | yes | Non-empty array of versions; see below. Array order does not select the installation version. |
|
||||
|
||||
`metadata.keywords` conventionally comes from npm `package.json` `keywords`, but generators may use another source, add strings, or adjust values. Neither presence nor equality with the package manifest is required. Readers preserve the strings and their order; an empty array is valid.
|
||||
|
||||
A plugin's `metadata`, its `category`, `keywords`, `labels`, and `sortKeys` members, and individual declared sort-key values may be absent or `null` when unavailable. Readers normalize these to absent category, empty arrays/maps, and omitted numeric values. They never substitute zero; a measured zero is valid. String placeholders such as `"n/a"` are invalid.
|
||||
|
||||
Recommended standardized definitions for optional metadata keys are `github_stars` for a repository's GitHub star count and `npm_weekly_downloads` for the public npm package's download count over the window returned by npm's `last-week` endpoint. Both values are non-negative integer counts. Publishers using these keys declare them under root `metadata.sortKeys` with a localized title and their chosen `order`, and publish available values under each plugin's `metadata.sortKeys`. These definitions standardize names, meanings, value types, and measurement periods. Other declared finite numeric keys remain valid. Generators omit unavailable counts, including fetch failures, and never relabel an upstream monthly count as weekly downloads. The reference generator uses an npm package's author-declared repository for `github_stars` without verifying ownership or source correspondence; that count is not a trust or quality signal.
|
||||
|
||||
### Version fields
|
||||
|
||||
| Field | Required | Rule |
|
||||
@@ -175,7 +181,7 @@ A reader cannot verify these rules; a generator that violates them publishes a m
|
||||
|
||||
A reader rejects the whole index when the document violates any rule in Root fields, Plugin fields, Version fields, or the `source` forms, including an undeclared category, label, or sort-key reference, an unlisted distribution-tag target, a duplicate `name` or exact `version` string, or a `format` other than `1`. Each such defect is a generator bug, and dropping entries silently would hide it. Readers may bound the document size.
|
||||
|
||||
A reader ignores members that this note does not define, at every level; an unknown `source.type` value is rejected. Format 1 can then gain optional members without breaking existing readers; a change that existing readers would misinterpret uses a new `format` value.
|
||||
A reader ignores members that this note does not define, at every level; an unknown `source.type` value is rejected. Keys inside declaration maps, localized-text maps, and plugin sort-value maps are data and still follow their respective validation rules.
|
||||
|
||||
Tag names must parse as registry tag specs under npm's maintained `npm-package-arg`; targets are exact listed version strings. npm sources use canonical published identifiers (`node-semver.valid(version) === version`); Git sources retain build metadata and pin their exact source by commit.
|
||||
|
||||
|
||||
@@ -104,10 +104,12 @@ DSH 只能从用户已知的 spec 安装第三方组合包(bundle):npm 包
|
||||
| `metadata.categories` | 否 | 对象,将分类 id 映射为 `{ title: LocalizedText }`。 |
|
||||
| `metadata.labels` | 否 | 对象,将标签 id 映射为 `{ title: LocalizedText }`。 |
|
||||
| `metadata.sortKeys` | 否 | 对象,将排序键 id 映射为 `{ title: LocalizedText, order: "asc" \| "desc" }`;`order` 是初始排序方向。 |
|
||||
| `plugins` | 是 | 插件数组,可以为空;数组顺序即索引的默认排序。 |
|
||||
| `plugins` | 是 | 插件数组,可以为空;客户端必须在初始的未筛选视图中使用此数组顺序。 |
|
||||
|
||||
分类、标签和排序键的 id 均为非空字符串。读取器按自有属性查找 id,因此 `constructor` 等 id 没有特殊含义。
|
||||
|
||||
客户端必须保留发布者的 `plugins` 数组顺序,除非用户选择其他排序方式,或文本搜索按相关性排序。仅进行筛选时,匹配的插件仍保持它们在数组中的相对顺序。声明排序键或发布指标值并不允许客户端自行选择按热度、下载量或发布时间进行默认排序。
|
||||
|
||||
### 插件字段
|
||||
|
||||
| 字段 | 必填 | 规则 |
|
||||
@@ -123,12 +125,16 @@ DSH 只能从用户已知的 spec 安装第三方组合包(bundle):npm 包
|
||||
| `metadata.category` | 否 | 一个已声明的分类 id。 |
|
||||
| `metadata.labels` | 否 | 由互不相同的已声明标签 id 组成的数组。 |
|
||||
| `metadata.keywords` | 否 | 自由字符串数组;无需在根部声明。 |
|
||||
| `metadata.sortKeys` | 否 | 对象,将已声明的排序键 id 映射为有限数值。 |
|
||||
| `metadata.sortKeys` | 否 | 对象,将已声明的排序键 id 映射为有限数值,或以 `null` 表示数据不可用。 |
|
||||
| `dist-tags` | 否 | 将 npm 发布标签名称映射为已列出的精确版本字符串的对象;缺省表示未记录发布渠道。 |
|
||||
| `versions` | 是 | 非空的版本数组;见下文。数组顺序不选择安装版本。 |
|
||||
|
||||
`metadata.keywords` 按约定来自 npm `package.json` 的 `keywords`,但生成器可以使用其他来源、添加字符串或调整值。不要求提供此字段,也不要求它与包 manifest 一致。读取器保留字符串及其顺序;允许空数组。
|
||||
|
||||
插件的 `metadata`、其中的 `category`、`keywords`、`labels` 和 `sortKeys` 成员,以及单个已声明排序键的值,在不可用时均可缺省或为 `null`。读取器将它们归一化为缺省的分类、空数组或空映射,并省略对应数值。读取器绝不以零代替缺失值;实际测得的零是有效值。`"n/a"` 等字符串占位值无效。
|
||||
|
||||
建议对可选元数据键采用以下统一定义:`github_stars` 表示仓库的 GitHub star 数,`npm_weekly_downloads` 表示公共 npm 包在 npm `last-week` 端点返回的时间窗口内的下载量。两者均为非负整数计数。发布者使用这些键时,在根级 `metadata.sortKeys` 中声明本地化标题和自行选择的 `order`,并在各插件的 `metadata.sortKeys` 中发布可用值。这些定义统一名称、含义、值类型和统计周期。其他已声明的有限数值键仍然有效。生成器省略不可用的计数,包括获取失败的计数,绝不把上游月下载量标为周下载量。参考生成器使用 npm 包作者声明的仓库获取 `github_stars`,不验证仓库归属或与包源码的对应关系;该计数不代表可信度或质量。
|
||||
|
||||
### 版本字段
|
||||
|
||||
| 字段 | 必填 | 规则 |
|
||||
@@ -175,7 +181,7 @@ DSH 只能从用户已知的 spec 安装第三方组合包(bundle):npm 包
|
||||
|
||||
文档违反根字段、插件字段、版本字段或 `source` 形式中的任何规则时,读取器拒绝整个索引,包括引用了未声明的分类、标签或排序键,发布标签指向未列出的目标,`name` 或精确的 `version` 字符串重复,以及 `format` 不是 `1`。每个此类缺陷都是生成器 bug,静默丢弃条目会掩盖它。读取器可以限制文档大小。
|
||||
|
||||
读取器在每一层都忽略本 Agent Note 未定义的成员;未知的 `source.type` 值会被拒绝。这样格式 1 可以增加可选成员而不破坏现有读取器;现有读取器会误解的变更使用新的 `format` 值。
|
||||
读取器在每一层都忽略本 Agent Note 未定义的成员;未知的 `source.type` 值会被拒绝。声明映射、本地化文本映射和插件排序值映射中的键是数据,仍遵循各自的校验规则。
|
||||
|
||||
标签名称必须能被 npm 维护的 `npm-package-arg` 解析为 registry 标签 spec;目标是已列出的精确版本字符串。npm 来源使用规范发布标识(`node-semver.valid(version) === version`);Git 来源保留版本编译信息并用提交固定精确来源。
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Bilingual-pair consistency record for 2026-10-12-agent-terminal-panel.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-10-12-agent-terminal-panel.md
|
||||
/agent-note-agent-terminals-in-the-web-sidebar:
|
||||
en: 8848102fb82b68ca
|
||||
zh: 833c922944a24e26
|
||||
/agent-note-agent-terminals-in-the-web-sidebar/problem:
|
||||
en: 82f0228676e804f1
|
||||
zh: b4f2e309f0570cfa
|
||||
/agent-note-agent-terminals-in-the-web-sidebar/decision:
|
||||
en: 9b82bdaf681bad16
|
||||
zh: f245c74d84f26d14
|
||||
/agent-note-agent-terminals-in-the-web-sidebar/decision/ownership-by-creator:
|
||||
en: 06adc75d5ec6b509
|
||||
zh: fb3272ef1d8a3985
|
||||
/agent-note-agent-terminals-in-the-web-sidebar/decision/permissions:
|
||||
en: cabac18c98fe9931
|
||||
zh: 4edc9e88c27689c3
|
||||
/agent-note-agent-terminals-in-the-web-sidebar/decision/readiness-and-reads:
|
||||
en: 326e3fead9936f32
|
||||
zh: 8e45a00cf61307f1
|
||||
/agent-note-agent-terminals-in-the-web-sidebar/decision/presentation:
|
||||
en: da4a53abe0752ec7
|
||||
zh: 5937e83bca8c0951
|
||||
/agent-note-agent-terminals-in-the-web-sidebar/alternatives-considered:
|
||||
en: 05d662a9e9862ae0
|
||||
zh: 220c65b5fb4b6baa
|
||||
/agent-note-agent-terminals-in-the-web-sidebar/consequences:
|
||||
en: d0657d7ac9f6eeca
|
||||
zh: 173d0e9df6afaebb
|
||||
@@ -0,0 +1,47 @@
|
||||
# Agent Note: Agent terminals in the Web sidebar
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-10-12-agent-terminal-panel.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
DSH had two terminal systems that could not see each other. The Agent's [persistent terminals](2026-07-16-persistent-pty-sessions.md) ran in private PTYs that the person could neither watch nor take over, and the [Web sidebar terminals](2026-09-09-web-sidebar-terminal.md) belonged to the person and gave the Agent no access. Comparable products let the Agent run commands in terminals the person can see, and let the Agent read what the person saw. A long-running process the Agent starts, such as a dev server, should be visible where the person works, without new model tools or a second permission model.
|
||||
|
||||
## Decision
|
||||
|
||||
The experimental [`terminal-panel`](../../../../packages/experimental/terminal-panel/README.md) package registers a `ctx.terminals` backend whose sessions are sidebar terminals opened through the [terminal controller](../../../../packages/api/terminal-controller/README.md). The six `terminal_*` tools keep their names, schemas and results; the [experimental terminal bundle](../../../../packages/experimental/terminal-bundle/README.md) mounts this backend in place of `terminal-bash`.
|
||||
|
||||
### Ownership by creator
|
||||
|
||||
Terminals are partitioned by who opened them. An Agent terminal records `origin: 'agent'` and starts with `control: 'agent'`; the person's first typed browser input passes control to the person. The browser marks input as typed only shortly after a keyboard, paste, input-method, drop, pointer or touch gesture, because xterm.js reports terminal-protocol replies through the same data event; focus reports and wheel-produced arrow keys never count, so focusing or scrolling an Agent terminal does not take it over. After takeover the controller refuses Agent writes, signals and closing, reads continue, and `terminal_close` releases the registry session without ending the shell. Terminals the person opened are never writable by the Agent; `user_terminal_list` and `user_terminal_read` read them, and `readUserTerminals: false` removes both tools.
|
||||
|
||||
### Permissions
|
||||
|
||||
An Agent terminal starts under the Session sandbox policy that the bash tool resolves, and the controller wraps confined shells through `ctx.sandbox` exactly as `terminal-bash` does. Commands in the terminal cannot request escalation; the guidance sends denied commands back to bash. Changing the Session's sandbox mode neither blocks the change nor stops open Agent terminals, whose processes keep their launch policy like bash background jobs. Unlike a background job, a terminal keeps accepting commands, so once the Session resolves a different policy `terminal_send` refuses input and points the model to a new terminal; reads, signals and closing continue. User terminals keep the [system-user permissions](../architecture/2026-09-16-user-terminal-permissions.md) of their own decision. Subagents are refused, because their terminals would not appear in the Session the person is viewing.
|
||||
|
||||
### Readiness and reads
|
||||
|
||||
The backend adds nothing to shell startup, because remote execution environments such as SSH lack the Host's files. A send returns `stdin_read` once output is silent and the shell's process group, or a program the provider reports as reading terminal input, owns the foreground; quiet commands become `inferred_idle` and busy ones `timeout`. Foreground inspection may start `ps` twice on macOS, so it runs only after silence and at a bounded rate. Results render the controller's headless screen from a mark at the start of the send's logical line; background reads move that mark past consumed complete lines.
|
||||
|
||||
### Presentation
|
||||
|
||||
The controller streams each Session's terminal list. A displayed Session opens a sidebar tab for each running Agent terminal it does not show yet, expanding a collapsed sidebar, and badges terminals the Agent controls, or after takeover a confined sandbox, naming the sandbox mode on hover. A terminal that leaves the list while the Agent controls it closes its tabs.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Share one terminal between the person and the Agent.** It covers re-running a failed command in place, but interleaved input, changing foreground programs such as ssh or sudo prompts, and running Agent commands in an unconfined terminal holding the person's login state would need approval for every write. Reading the person's terminal and re-running in an Agent terminal covers the same case.
|
||||
|
||||
**Add a separate tool family for visible terminals.** The model would see two overlapping terminal tool sets. Ownership by creator matches the registry's rule that a session belongs to the Agent that opened it, so a backend suffices.
|
||||
|
||||
**Mirror `terminal-bash` sessions into the sidebar read-only.** Those shells run with controlled prompts, `TERM=dumb` and sanitized line output, so there is no screen for xterm.js and the person could neither type nor be read.
|
||||
|
||||
**Inject shell integration for exact command completion.** Prompt markers from injected startup files give exact readiness and exit codes, but the files would have to exist inside every execution environment and sandbox; silence plus foreground inspection works wherever the provider can inspect processes.
|
||||
|
||||
**Reuse the Agent's sandbox fence on mode changes.** `terminal-bash` rejects mode changes while terminals are open; visible terminals the person may have taken over would then block permission changes indefinitely, and counting only Agent-controlled terminals would still make the person close the Agent's dev server before tightening permissions. Closing Agent terminals on a stricter mode would end processes the person may be watching.
|
||||
|
||||
## Consequences
|
||||
|
||||
The Agent's long-running work is visible and interruptible by the person, and the person can point the Agent at their own terminal without copying text. Screen text the Agent reads from the person's terminals enters the Session log and model requests, which the `readUserTerminals` switch controls. Readiness is inferred: a silent shell builtin reads as `stdin_read`, and on macOS a program waiting for input returns only after `idleSilenceMs`. A terminal taken over cannot be handed back, and the backend needs the Web or Desktop composition that mounts the terminal controller.
|
||||
|
||||
Controller and sidebar tests cover takeover, typed-input recognition, refusals, screen reads, marks and roster streams; backend tests cover every wait reason, cancellation, release and subagent refusal on a fake clock, and an integration test drives a real shell through the registry. The `terminal-panel-tools` snapshot records the guidance, schemas and user-terminal results.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Agent Note: Agent terminals in the Web sidebar
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-10-12-agent-terminal-panel.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
DSH 原有两套互相看不见的终端。Agent 的[持久终端](2026-07-16-persistent-pty-sessions.zh.md)运行在私有 PTY 中,人既看不到也无法接管;[Web 侧栏终端](2026-09-09-web-sidebar-terminal.zh.md)属于人,Agent 无法访问。同类产品让 Agent 在人能看到的终端里运行命令,也能读取人看到的内容。Agent 启动的长时间进程(例如开发服务器)应当出现在人工作的地方,而且不需要新的模型工具或第二套权限模型。
|
||||
|
||||
## 决定
|
||||
|
||||
实验包 [`terminal-panel`](../../../../packages/experimental/terminal-panel/README.zh.md) 注册一个 `ctx.terminals` 后端,其会话就是通过[终端控制器](../../../../packages/api/terminal-controller/README.zh.md)打开的侧栏终端。6 个 `terminal_*` 工具的名称、schema 和结果都不变;[实验终端 bundle](../../../../packages/experimental/terminal-bundle/README.zh.md) 用这个后端替换 `terminal-bash`。
|
||||
|
||||
### 按创建者划分所有权
|
||||
|
||||
终端按打开者划分。Agent 终端记录 `origin: 'agent'`,初始为 `control: 'agent'`;人在浏览器中第一次键入的输入会把控制权交给人。浏览器只在键盘、粘贴、输入法、拖放、指针或触摸操作之后的短时间内把输入标记为键入,因为 xterm.js 通过同一个数据事件报告终端协议应答;焦点上报和滚轮产生的方向键从不计入,因此聚焦或滚动 Agent 终端不会接管它。接管之后,控制器拒绝 Agent 的写入、信号和关闭,读取照常可用,`terminal_close` 只释放注册表会话而不结束 shell。人自己打开的终端永远不允许 Agent 写入;`user_terminal_list` 和 `user_terminal_read` 读取它们,`readUserTerminals: false` 会移除这两个工具。
|
||||
|
||||
### 权限
|
||||
|
||||
Agent 终端按 bash 工具解析出的 Session 沙箱策略启动,控制器像 `terminal-bash` 一样通过 `ctx.sandbox` 包裹受限 shell。终端里的命令不能申请提权;指引让模型把被拒绝的命令改用 bash 执行。切换 Session 的沙箱模式既不会被阻止,也不会停止已打开的 Agent 终端,终端中的进程像 bash 后台任务一样保留启动时的策略。与后台任务不同,终端会持续接收命令,因此 Session 解析出的策略一旦不同,`terminal_send` 就拒绝输入并提示模型另开终端;读取、发送信号和关闭照常可用。用户终端沿用其自身决策中的[系统用户权限](../architecture/2026-09-16-user-terminal-permissions.zh.md)。子 agent 会被拒绝,因为它们的终端不会出现在人正在查看的 Session 中。
|
||||
|
||||
### 就绪判定与读取
|
||||
|
||||
后端不向 shell 启动过程添加任何内容,因为 SSH 等远程执行环境没有 Host 上的文件。输出静默、且 shell 的进程组或 provider 报告正在读取终端输入的程序占据前台时,一次发送返回 `stdin_read`;安静的命令变为 `inferred_idle`,忙碌的命令变为 `timeout`。在 macOS 上前台检查可能启动两次 `ps`,所以只在静默之后、以有界的频率执行。结果从一次发送所在逻辑行起点的标记开始,渲染控制器的 headless 屏幕;后台读取会把标记移过已消费的完整行。
|
||||
|
||||
### 呈现
|
||||
|
||||
控制器为每个 Session 推送终端列表。正在显示的 Session 会为每个尚未展示的运行中 Agent 终端打开侧栏标签页(侧栏折叠时会展开),并为 Agent 控制的终端标出徽标,接管后的受限终端则标出沙箱徽标,悬停可看到沙箱模式。仍由 Agent 控制的终端离开列表时,它的标签页随之关闭。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**人与 Agent 共用同一个终端。** 它能覆盖在原地重跑失败命令的场景,但输入交错、前台程序变化(例如 ssh 或 sudo 提示),以及在持有人登录状态、未受限的终端里运行 Agent 命令,都意味着每次写入都需要审批。读取人的终端、再在 Agent 终端里重跑,可以覆盖同样的场景。
|
||||
|
||||
**为可见终端新增一套工具。** 模型会看到两套重叠的终端工具。按创建者划分所有权,正好符合注册表“会话属于打开它的 Agent”的规则,因此一个后端就够了。
|
||||
|
||||
**把 `terminal-bash` 会话只读镜像到侧栏。** 这些 shell 使用受控提示符、`TERM=dumb` 和清洗后的行输出,没有可供 xterm.js 显示的屏幕,人既不能输入,Agent 也读不到人的终端。
|
||||
|
||||
**注入 shell 集成以精确判断命令完成。** 注入启动文件输出的提示符标记能给出精确的就绪状态和退出码,但这些文件必须存在于每个执行环境和沙箱内;静默加前台检查在 provider 能检查进程的任何地方都可用。
|
||||
|
||||
**沿用 Agent 在切换模式时的沙箱栅栏。** `terminal-bash` 在有终端打开时拒绝切换模式;对于人可能已接管的可见终端,这会无限期阻止权限变更;只统计 Agent 控制的终端,仍会迫使人在收紧权限前先关掉 Agent 的开发服务器。模式收紧时关闭 Agent 终端,则会结束人可能正在观察的进程。
|
||||
|
||||
## 影响
|
||||
|
||||
Agent 的长时间工作对人可见、可被人中断,人也可以直接让 Agent 查看自己的终端而不必复制文本。Agent 从人终端中读取的屏幕文本会进入 Session 日志和模型请求,由 `readUserTerminals` 开关控制。就绪判定是推断的:静默的 shell 内建命令会被当作 `stdin_read`,在 macOS 上等待输入的程序要到 `idleSilenceMs` 后才返回。被接管的终端无法交还,后端需要挂载终端控制器的 Web 或 Desktop 组合。
|
||||
|
||||
控制器与侧栏测试覆盖接管、键入识别、拒绝、读屏、标记和终端列表流;后端测试在假时钟上覆盖每种等待原因、取消、释放和子 agent 拒绝,集成测试通过注册表驱动真实 shell。`terminal-panel-tools` 快照记录了指引、schema 和用户终端结果。
|
||||
@@ -0,0 +1,22 @@
|
||||
# Bilingual-pair consistency record for 2026-11-04-plugin-entry-value-exports.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-11-04-plugin-entry-value-exports.md
|
||||
/agent-note-plugin-entries-must-not-add-value-exports:
|
||||
en: 7fa97c44ef2ea43c
|
||||
zh: 40b94e6a814ff3be
|
||||
/agent-note-plugin-entries-must-not-add-value-exports/problem:
|
||||
en: 430b0f35a1358a2f
|
||||
zh: 83b10ec03fab2477
|
||||
/agent-note-plugin-entries-must-not-add-value-exports/decision:
|
||||
en: af431ecc09ef6646
|
||||
zh: 3231ab74de70dffa
|
||||
/agent-note-plugin-entries-must-not-add-value-exports/alternatives-considered:
|
||||
en: eba451c0ac372a95
|
||||
zh: 906cfc30c66680de
|
||||
/agent-note-plugin-entries-must-not-add-value-exports/consequences:
|
||||
en: aef0c3a9efa2e76a
|
||||
zh: d490227953a5f397
|
||||
/agent-note-plugin-entries-must-not-add-value-exports/testing:
|
||||
en: 50085a461a070e5c
|
||||
zh: 4d9935def73c5b6a
|
||||
@@ -0,0 +1,48 @@
|
||||
# Agent Note: Plugin entries must not add value exports
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-11-04-plugin-entry-value-exports.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
[The package surface rules](../../../../docs/defensive-patterns/package-surface.md) limit a plugin entry to what the Cordis Loader loads: a function plugin's entry exports `name`, `inject`, `Config`, and `apply`, a service plugin's entry default-exports its plugin class, and beyond those a Host entry may export only the Service Definition classes and error types that other packages extend or throw. No gate checked the rule, and value exports beyond it accumulated on many plugin entries; `dsh-mcp-client`'s entry, for example, gained `probeServer`, `ReconnectSchema`, and four other values that `dsh-mcp-manager` imports.
|
||||
|
||||
The Cordis Loader imports a plugin entry when its row activates and reads only what the plugin form provides: the Loader fields from a function plugin's module namespace, or a service plugin's default-exported class. Any other value on the entry is API that another package can import statically, and that import evaluates the entry module and its dependencies when the importer loads, without the row's activation or `inject` wait.
|
||||
|
||||
## Decision
|
||||
|
||||
The gate [verify-plugin-entry-exports](../../../../scripts/verify-plugin-entry-exports.ts) enforces the rule for both plugin forms:
|
||||
|
||||
- **Scope**: every `packages/<group>/<package>/src/index.ts` that is a plugin entry. An entry that exports a value named `apply` is a function plugin. An entry without `apply` whose default export is a class the Loader can construct is a service plugin. Libraries, a default export that is not a class, and an abstract default class are not checked; an abstract class defines a capability seam that providers extend, and the Loader cannot construct it.
|
||||
- **Compiler faces**: the gate reads each entry through the Host compiler face and then the Client face, and the first face that compiles an entry classifies it. An entry that neither face compiles fails the gate, and so does a corpus without a function plugin entry or without a service plugin entry.
|
||||
- **Rule**: a function plugin entry may export `name`, `inject`, `Config`, and `apply`, and a service plugin entry may export its default class under any name. Either may also export types and values whose every construct signature builds an instance whose base types reach Cordis `Service` or the global `Error`. Class declarations, class expressions, and mixin results pass on the same terms. Every other value fails, including a value that arrives through `export *`, an export whose target does not resolve, and an `export *` whose module does not resolve, which is reported as `*`. A service plugin entry's `name`, `inject`, and `Config` fail like other values, because the Loader reads them from the class. A default export beside `apply` fails whatever it holds, even an allowed class, because the Loader then loads the default export instead of the module namespace.
|
||||
- **Value or type**: decided by the names the entry exports at run time, with module targets and local bindings resolved against the bound Program because `verbatimModuleSyntax` is off, as in [verify-optional-dependency-imports](../../../../scripts/verify-optional-dependency-imports.ts). Type-only exports and declarations emit nothing, a re-export emits only a run-time export of its target, and `export *` brings in every run-time name of its target except `default` and the names the module exports itself. An `export type` therefore does not hide a value that `export *` provides under the same name, and a name that only `export type *` reaches, at any depth, is a type.
|
||||
- **Existing exports**: [plugin-entry-existing-exports.ts](../../../../scripts/plugin-entry-existing-exports.ts) lists, by package, the value exports that entries had when the gate arrived. A line records debt, not an approved exception. A listed export passes. A line fails when its package has no entry or its entry is not a plugin entry, when its export list is empty or repeats a name, and when it names an export the entry no longer reports, so removing an export removes its line in the same change. No document or test records the number of listed exports; the gate prints the number it matched.
|
||||
- **Registration**: a shared hygiene gate in [run-gates](../../../../scripts/run-gates.ts), so it runs in the CI static lane, in master CI, in the local `hygiene` and `check-all` aggregates, and in the Windows observational lane.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Compare the list with the pull request base in CI.** CI already passes the base commit to the [frozen-archive gate](2026-07-26-frozen-agent-note-archive.md), so a gate could reject every line a change adds. The comparison also rejects legitimate changes: renaming a package moves its lines to a new key, and reverting an export removal restores a line. Additions stay a review decision, as they are for the other allowance lists in this repository.
|
||||
|
||||
**Generate a baseline and prune it with a command.** The [unknown-cast gate](2026-09-19-no-unknown-casts.md) keeps a generated JSON baseline with a `--prune` command. A hand-edited TypeScript list keeps one readable entry per package in the diff, and the gate's validation names the line to delete.
|
||||
|
||||
**Check only function plugin entries.** The rule's judged condition names a function plugin's `src/index.ts`, but its first paragraph describes both entry forms and allows a Host entry only the Service Definition classes and error types beyond what the Loader loads. A value that another package imports from a service plugin entry bypasses the row in the same way, and a gate limited to function plugins would let an entry escape it by default-exporting a class.
|
||||
|
||||
**Check Loader rows that name a package subpath.** A Loader row can name a module other than the package root. Finding those modules requires reading every profile, bundle, and preset composition, which this gate does not do, so review still owns them.
|
||||
|
||||
**Remove every existing export in the same change.** Removing the listed exports needs test and consumer changes in dozens of packages. The list lets the gate block new exports immediately while each package removes its own.
|
||||
|
||||
**Judge classes by declaration syntax.** Allowing only `class` declarations that extend `Service` or `Error` rejects class expressions and mixin results that construct the same instances. The gate judges what an export constructs.
|
||||
|
||||
## Consequences
|
||||
|
||||
A new value export on a plugin entry fails CI with the file, line, package, and export name, and the message names the rule and the way out. Existing exports keep working until their packages remove them, and each removal shortens the list.
|
||||
|
||||
The gate builds the Host Program and then the Client Program, and releases the first before building the second, so its peak memory stays near that of the Host Program, like verify-optional-dependency-imports.
|
||||
|
||||
Loader rows that name a subpath, default exports that are not classes, deep imports through `./src/*`, and consumers that import a listed export stay outside the gate, and review still owns them. `/client` entries follow [the Client package rules](../../../../packages/client/AGENTS.md), and the Client build already rejects cross-plugin value imports. The list accepts an added line like any other file, so an addition depends on review.
|
||||
|
||||
## Testing
|
||||
|
||||
[verify-plugin-entry-exports.spec.ts](../../../../scripts/verify-plugin-entry-exports.spec.ts) compiles a fixture with its own Host and Client configs and a stand-in Cordis `Service`. It pins each reported and passing export form, including the run-time paths through `export type` and nested `export type *` and base classes that only share a root's name, the three ways an entry exports `apply`, an `apply` that only a type-only path reaches, service plugin entries whose default class extends `Service` or no class, the entries that are not plugins (a library, an abstract default class, and a default-exported function), an entry only the Client face compiles, an entry neither face compiles, every list validation, a corpus without either plugin form, and a manifest without a name. [run-gates.spec.ts](../../../../scripts/run-gates.spec.ts) pins the gate's registration. Adding a value export to a real function plugin or service plugin entry and removing a listed export makes the gate report both.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Agent Note: 插件入口不得新增值导出
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-11-04-plugin-entry-value-exports.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
[包表面规则](../../../../docs/defensive-patterns/package-surface.zh.md)把插件入口限定为 Cordis Loader 加载的内容:函数插件的入口导出 `name`、`inject`、`Config`、`apply`,服务插件的入口默认导出它的插件类;除此之外,Host 入口只可再导出供其他包 `extends` 或 `throw` 的 Service Definition 类与错误类型。此前没有门禁检查这条规则,超出规则的值导出在许多插件入口上累积;例如 `dsh-mcp-client` 的入口新增了 `probeServer`、`ReconnectSchema` 以及另外四个由 `dsh-mcp-manager` 导入的值。
|
||||
|
||||
Cordis Loader 在插件条目激活时才导入它的入口,并且只读取插件形态提供的内容:函数插件模块命名空间里的 Loader 字段,或服务插件默认导出的类。入口上的其他值都是其他包可以静态导入的 API;这种导入会在导入方加载时执行该入口模块及其依赖,不经过该条目的激活,也不等待 `inject`。
|
||||
|
||||
## 决策
|
||||
|
||||
门禁 [verify-plugin-entry-exports](../../../../scripts/verify-plugin-entry-exports.ts) 对两种插件形态执行这条规则:
|
||||
|
||||
- **范围**:每个作为插件入口的 `packages/<group>/<package>/src/index.ts`。导出了名为 `apply` 的值的入口是函数插件;没有 `apply`、而默认导出 Loader 能构造的类的入口是服务插件。库、默认导出不是类的入口,以及默认导出抽象类的入口都不检查;抽象类定义的是供提供方继承的能力接缝,Loader 无法构造它。
|
||||
- **compiler face**:门禁先用 Host compiler face、再用 Client face 读取每个入口,由第一个编译它的 face 判定。两个 face 都不编译的入口会让门禁失败;找不到任何函数插件入口或任何服务插件入口时也失败。
|
||||
- **判定**:函数插件入口可以导出 `name`、`inject`、`Config`、`apply`,服务插件入口可以用任意名字导出它的默认类。两者都还可以导出类型,以及每个构造签名构造出的实例其基类型都能追溯到 Cordis `Service` 或全局 `Error` 的值。类声明、类表达式和 mixin 结果按同样的条件放行。其他值都会失败,包括经 `export *` 带出的值、目标无法解析的导出,以及模块无法解析的 `export *`(以 `*` 报告)。服务插件入口上的 `name`、`inject`、`Config` 和其他值一样会失败,因为 Loader 从类上读取它们。`apply` 旁边的默认导出无论是什么都会失败,即使是放行的类,因为 Loader 此时会加载默认导出,而不是模块命名空间。
|
||||
- **值还是类型**:按入口在运行时导出的名字判定;因为 `verbatimModuleSyntax` 是关闭的,模块目标和本地绑定和 [verify-optional-dependency-imports](../../../../scripts/verify-optional-dependency-imports.ts) 一样基于绑定后的 Program 解析。仅类型的导出和声明不产生任何输出,再导出只在目标是运行时导出时才产生输出,`export *` 带出目标的全部运行时名字,但不包括 `default` 和本模块自己导出的名字。因此 `export type` 不会遮住 `export *` 以同名带出的值,而只能经 `export type *` 到达的名字,无论经过几层,都算类型。
|
||||
- **存量导出**:[plugin-entry-existing-exports.ts](../../../../scripts/plugin-entry-existing-exports.ts) 按包列出门禁引入时各入口已有的值导出。每一行记录的是存量欠账,不是批准的例外。列出的导出可以通过。以下情况该行失败:包没有入口或入口不是插件入口;导出列表为空或有重复的名字;列出的导出已不再被入口报告。因此删除一个导出时要在同一改动中删掉它在清单里的行。任何文档或测试都不记录清单条目的数量;门禁只打印它匹配到的数量。
|
||||
- **注册**:[run-gates](../../../../scripts/run-gates.ts) 的共享卫生门禁,因此在 CI static lane、master CI、本地 `hygiene` 和 `check-all` 聚合以及 Windows observational lane 中运行。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**在 CI 中把清单与 PR 的 base 比较。** CI 已经把 base 提交传给[冻结归档门禁](2026-07-26-frozen-agent-note-archive.zh.md),因此门禁可以拒绝改动新增的每一行。但这种比较也会拒绝正当的改动:包改名会把它的行移到新的键下,回滚一次导出删除会恢复一行。新增条目仍由 review 决定,和仓库里其他放行清单一样。
|
||||
|
||||
**生成基线并用命令裁剪。** [unknown-cast 门禁](2026-09-19-no-unknown-casts.zh.md)维护一份生成的 JSON 基线和 `--prune` 命令。手工编辑的 TypeScript 清单在 diff 中每个包一条,便于阅读,门禁的校验会指出要删的行。
|
||||
|
||||
**只检查函数插件入口。** 规则的判定条件写的是函数插件的 `src/index.ts`,但规则第一段同时描述了两种入口形态,并且规定 Host 入口在 Loader 加载的内容之外只能导出 Service Definition 类与错误类型。其他包从服务插件入口导入的值同样绕过了插件条目;只检查函数插件的门禁,会让入口改成默认导出类就避开检查。
|
||||
|
||||
**检查指向包子路径的 Loader 条目。** Loader 条目可以指向包根以外的模块。找出这些模块需要读取所有 profile、bundle 和 preset 组合,本门禁不做这件事,因此仍由 review 负责。
|
||||
|
||||
**在同一改动中删除所有存量导出。** 删除清单里的导出需要改动几十个包的测试和使用方。有了清单,门禁可以立即拦住新增导出,各包再自行删除存量。
|
||||
|
||||
**按声明语法判断类。** 只放行继承 `Service` 或 `Error` 的 `class` 声明,会拒绝构造出同样实例的类表达式和 mixin 结果。门禁按导出构造出的实例判断。
|
||||
|
||||
## 后果
|
||||
|
||||
插件入口新增值导出时,CI 失败并给出文件、行号、包名和导出名,消息中写明规则和改法。存量导出在所属包删除前继续可用,每次删除都会让清单变短。
|
||||
|
||||
门禁先构建 Host Program,再构建 Client Program,并在构建第二个之前释放第一个,因此峰值内存接近 Host Program 本身,与 verify-optional-dependency-imports 相当。
|
||||
|
||||
指向子路径的 Loader 条目、默认导出不是类的入口、经 `./src/*` 的深导入,以及导入清单中导出的使用方都不在门禁范围内,仍由 review 负责。`/client` 入口遵循 [Client 包规则](../../../../packages/client/AGENTS.md),Client 构建已经拒绝跨插件的值导入。清单和其他文件一样可以新增行,因此新增条目依赖 review。
|
||||
|
||||
## 测试
|
||||
|
||||
[verify-plugin-entry-exports.spec.ts](../../../../scripts/verify-plugin-entry-exports.spec.ts) 编译一个 fixture,它有自己的 Host、Client 配置和一个替身 Cordis `Service`。它锁定每种被报告和被放行的导出写法(包括经 `export type` 与多层 `export type *` 的运行时路径,以及只是与根类同名的基类)、入口导出 `apply` 的三种方式、只经仅类型路径到达的 `apply`、默认类继承 `Service` 或不继承任何类的服务插件入口、不是插件的入口(库、默认导出抽象类、默认导出函数)、只有 Client face 编译的入口、两个 face 都不编译的入口、每项清单校验、缺少任一插件形态的语料,以及没有名字的 manifest。[run-gates.spec.ts](../../../../scripts/run-gates.spec.ts) 锁定门禁的注册。在真实的函数插件或服务插件入口上新增一个值导出并删除一个已列出的导出,门禁会同时报告这两处。
|
||||
+6
-6
@@ -27,8 +27,8 @@
|
||||
en: 1f28c04d5002af94
|
||||
zh: 4d4dfea99fc6aee2
|
||||
/agent-note-audience-first-documentation-quality-criteria/proposal/prototype-rules:
|
||||
en: 6ee5714bc0b33cda
|
||||
zh: 587bd85f6f712868
|
||||
en: badae41417e3e3c0
|
||||
zh: 52336cb9ce2790f9
|
||||
/agent-note-audience-first-documentation-quality-criteria/proposal/criteria-by-document-job:
|
||||
en: 9abbe4f2d5038284
|
||||
zh: 45d30352d9cce4b5
|
||||
@@ -36,8 +36,8 @@
|
||||
en: 2641ca1307c3308f
|
||||
zh: 15fd0415d40a9190
|
||||
/agent-note-audience-first-documentation-quality-criteria/proposal/enforcement-slices:
|
||||
en: c2f6617cf8b3bab4
|
||||
zh: b9ad3dae2fc50ddf
|
||||
en: a8bfbf127516e617
|
||||
zh: 284843d0f0a59f58
|
||||
/agent-note-audience-first-documentation-quality-criteria/proposal/non-goals:
|
||||
en: e8633d73b0273bfd
|
||||
zh: 516e6d805158d303
|
||||
@@ -45,8 +45,8 @@
|
||||
en: 5f09627b84bf112b
|
||||
zh: b0f1845df90e5f1a
|
||||
/agent-note-audience-first-documentation-quality-criteria/acceptance-criteria:
|
||||
en: 696ff7205b3362d0
|
||||
zh: a62a50cf3ceadac6
|
||||
en: 1d3ca0cd3ed33607
|
||||
zh: 68ed7b6de42ba3aa
|
||||
/agent-note-audience-first-documentation-quality-criteria/risks:
|
||||
en: b920900d3ff05073
|
||||
zh: 05b2a0271b825260
|
||||
@@ -48,7 +48,7 @@ Adopt one audience-first quality contract with five definitions:
|
||||
|
||||
### Prototype rules
|
||||
|
||||
The [dsh-doc skill](../../../skills/dsh-doc/SKILL.md) owns the first executable version of these rules. The `session-persistence-jsonl` README pair uses the shipped append, recovery, and encoding behavior as evidence rather than treating its prior prose as authority.
|
||||
The [dsh-doc-writing skill](../../../skills/dsh-doc-writing/SKILL.md) owns the first executable version of these rules. The `session-persistence-jsonl` README pair uses the shipped append, recovery, and encoding behavior as evidence rather than treating its prior prose as authority.
|
||||
|
||||
- Every authored package README starts with searchable YAML. A Skill-style `description` and mechanically derived `kind` are required. Four kinds map one-to-one to four skill templates: `package-group` (group map), `package-reference` (plugin or service package), `package-library` (plain module entry), and `package-bundle` (`dsh.bundle.patch`). The counterpart path, hashes, and physical line alignment belong to the merge-safe sidecar and its gate, so README frontmatter contains no `i18n` block. The title or package manifest already owns the name, the document job expresses its audience, and tags remain absent until a governed taxonomy and search consumer proves value beyond full-text search.
|
||||
- Authored pages start with a three-to-five-sentence `Summary`, then a linked `Table of Contents`. An English package README Summary stays within 100 `wc -w`-style words. It describes reader-visible capability instead of Cordis roles, registrations, or internal components, and omits source identifiers unless readers use them directly in configuration, commands, or a public API. Format-owned Agent Notes, postmortems, generated fragments, and machine files keep their required skeletons.
|
||||
@@ -85,7 +85,7 @@ The first prototype should use one large catalog and one mixed subsystem page. I
|
||||
|
||||
### Enforcement slices
|
||||
|
||||
1. Create and validate `dsh-doc`, then rewrite one package README pair as a line-aligned, metadata-bearing prototype without changing runtime claims.
|
||||
1. Create and validate `dsh-doc-writing`, then rewrite one package README pair as a line-aligned, metadata-bearing prototype without changing runtime claims.
|
||||
2. Review the rendered prototype with newcomer, user, developer, and agent tasks; revise the skill before enforcing the format elsewhere.
|
||||
3. Add narrow metadata, Summary-length, section-order, line-alignment, link-resolution, and pairing fixtures. Migrate every existing package Summary that violates the accepted entry limit, and keep sidecars until every merge and recovery consumer has replacement support.
|
||||
4. Extract accepted standing rules into one canonical quality reference, condense `docs/AGENTS.md` below its target, and organize one coherent `docs/` topic at a time with atomic link/navigation repair.
|
||||
@@ -93,7 +93,7 @@ The first prototype should use one large catalog and one mixed subsystem page. I
|
||||
|
||||
This sequence keeps each change independently reviewable. The first three slices improve criteria and package entry points without changing the broader information architecture; the generated-doc prototype supplies evidence before a broader structural change.
|
||||
|
||||
Slices 1–3 have shipped in this form: `dsh-doc` is the consolidated standard (`dsh-doc-standards` and `dsh-doc-site-sync` are folded into it, and the site workflow carries the corrected sidebar values), the `session-persistence-jsonl` README pair is the reference example, and `pnpm run test:docs` enforces the metadata, pairing, and quick documentation checks. Slices 4–5 remain open.
|
||||
Slices 1–3 have shipped in this form: `dsh-doc-writing` is the consolidated standard (`dsh-doc-standards` and `dsh-doc-site-sync` are folded into it, and the site workflow carries the corrected sidebar values), the `session-persistence-jsonl` README pair is the reference example, and `pnpm run test:docs` enforces the metadata, pairing, and quick documentation checks. Slices 4–5 remain open.
|
||||
|
||||
### Non-goals
|
||||
|
||||
@@ -114,9 +114,9 @@ This proposal does not shorten exhaustive facts, merge audience tiers, publish i
|
||||
## Acceptance criteria
|
||||
|
||||
- One canonical quality reference defines brief, intuitive, friendly, accurate, and agent-readable documentation by document job.
|
||||
- `.agents/skills/dsh-doc` validates and directly links its metadata, structure/hierarchy, and review/prototype references without duplicating their detailed rules in `SKILL.md`.
|
||||
- `.agents/skills/dsh-doc-writing` validates and directly links its metadata, structure/hierarchy, and review/prototype references without duplicating their detailed rules in `SKILL.md`.
|
||||
- The `session-persistence-jsonl` README pair demonstrates searchable YAML, Summary, Table of Contents, user-to-developer progression, Further Exploration, final Dev Note, structural parity, and exact line-count equality while preserving verified package contracts.
|
||||
- Every English package README Summary stays within 100 `wc -w`-style words; the focused gate reports the measured count and directs failures to `dsh-doc` and the selected kind template.
|
||||
- Every English package README Summary stays within 100 `wc -w`-style words; the focused gate reports the measured count and directs failures to `dsh-doc-writing` and the selected kind template.
|
||||
- `docs/AGENTS.md` links that reference, remains sufficient as standing instruction, and is below its target with at least 5% headroom.
|
||||
- The root user path, Web quick start, first-plugin tutorial, contributor setup, and architecture overview each name an observable outcome and a verification owner without duplicating implementation detail.
|
||||
- The budget manifest records both target and temporary ceiling, and its check reports or rejects a violated headroom/ratchet state.
|
||||
|
||||
+5
-5
@@ -48,7 +48,7 @@ Status: proposed
|
||||
|
||||
### 原型规则
|
||||
|
||||
[dsh-doc skill](../../../skills/dsh-doc/SKILL.md) 负责这些规则的首个可执行版本。`session-persistence-jsonl` README 对以已交付的追加、恢复与编码行为为证据,而不把其旧版正文当作权威。
|
||||
[dsh-doc-writing skill](../../../skills/dsh-doc-writing/SKILL.md) 负责这些规则的首个可执行版本。`session-persistence-jsonl` README 对以已交付的追加、恢复与编码行为为证据,而不把其旧版正文当作权威。
|
||||
|
||||
- 每个撰写型包 README 都以可搜索 YAML 开头。Skill 风格的 `description` 与按机制推导的 `kind` 为必填字段。四种 kind 与四个技能模板一一对应:`package-group`(组地图)、`package-reference`(插件或服务包)、`package-library`(纯模块入口)与 `package-bundle`(`dsh.bundle.patch`)。对照文件路径、哈希与物理行对齐由支持自动合并的 sidecar 及其门禁负责,因此 README frontmatter 不包含 `i18n` 块。名称已由标题或包 manifest 归属,受众已由文档职责表达;在受治理的标签分类与搜索消费方证明其价值超过全文检索之前,不加入标签。
|
||||
- 撰写型页面先写三至五句的 `Summary`,再写带链接的 `Table of Contents`。英文包 README 的 Summary 不超过 100 个按 `wc -w` 语义统计的词。它描述读者可见能力,而不是 Cordis 角色、注册项或内部组件;除非读者会在配置、命令或公开 API 中直接使用某个源码标识符,否则不得写入该标识符。由格式约束的 Agent Note、事故复盘、生成片段和机器文件保留其必需骨架。
|
||||
@@ -85,7 +85,7 @@ Status: proposed
|
||||
|
||||
### 执行切片
|
||||
|
||||
1. 创建并验证 `dsh-doc`,再把一组 package README 对改写为行对齐、带元数据的原型,同时不改变运行时事实。
|
||||
1. 创建并验证 `dsh-doc-writing`,再把一组 package README 对改写为行对齐、带元数据的原型,同时不改变运行时事实。
|
||||
2. 用新人、用户、开发者和 agent 任务评审渲染后的原型;先修订 skill,再在其他位置强制执行该格式。
|
||||
3. 添加聚焦的元数据、Summary 长度、章节顺序、行对齐、链接解析和配对 fixture。迁移所有违反已接受入口上限的既有包 Summary;在每个合并与恢复消费方都有替代支持前,保留伴随文件。
|
||||
4. 把已接受的常驻规则提取到一份规范质量参考,将 `docs/AGENTS.md` 精简到目标以下,并且一次只组织一个内聚的 `docs/` 主题,同时原子地修复链接与导航。
|
||||
@@ -93,7 +93,7 @@ Status: proposed
|
||||
|
||||
该顺序使每项变更都能独立评审。前三个切片改进标准与包入口,而不改变更广的信息架构;生成文档原型则在更广的结构变更前提供证据。
|
||||
|
||||
切片 1–3 已按此形式交付:`dsh-doc` 成为合并后的标准(`dsh-doc-standards` 与 `dsh-doc-site-sync` 已并入其中,站点工作流携带修正后的侧边栏值),`session-persistence-jsonl` README 对是参考示例,`pnpm run test:docs` 强制执行元数据、配对与快速文档检查。切片 4–5 仍待完成。
|
||||
切片 1–3 已按此形式交付:`dsh-doc-writing` 成为合并后的标准(`dsh-doc-standards` 与 `dsh-doc-site-sync` 已并入其中,站点工作流携带修正后的侧边栏值),`session-persistence-jsonl` README 对是参考示例,`pnpm run test:docs` 强制执行元数据、配对与快速文档检查。切片 4–5 仍待完成。
|
||||
|
||||
### 非目标
|
||||
|
||||
@@ -114,9 +114,9 @@ Status: proposed
|
||||
## 验收标准
|
||||
|
||||
- 一份规范质量参考按文档职责定义简短、直观、友好、准确和便于 agent 阅读的文档。
|
||||
- `.agents/skills/dsh-doc` 通过验证,并直接链接其元数据、结构或层级及评审或原型参考,而不在 `SKILL.md` 中复制这些参考的详细规则。
|
||||
- `.agents/skills/dsh-doc-writing` 通过验证,并直接链接其元数据、结构或层级及评审或原型参考,而不在 `SKILL.md` 中复制这些参考的详细规则。
|
||||
- `session-persistence-jsonl` README 对展示可搜索 YAML、Summary、Table of Contents、从用户到开发者的渐进结构、Further Exploration、结尾 Dev Note、结构一致性和精确行数相等,同时保留已验证的包约定。
|
||||
- 每个英文包 README Summary 都不超过 100 个按 `wc -w` 语义统计的词;聚焦门禁报告实测词数,并引导失败项阅读 `dsh-doc` 与所选 kind 模板。
|
||||
- 每个英文包 README Summary 都不超过 100 个按 `wc -w` 语义统计的词;聚焦门禁报告实测词数,并引导失败项阅读 `dsh-doc-writing` 与所选 kind 模板。
|
||||
- `docs/AGENTS.md` 链接该参考,仍足以充当常驻指令,并低于其目标且至少保留 5% 余量。
|
||||
- 根级用户路径、Web 快速开始、第一个插件教程、贡献者设置和架构概览各自给出一个可观察结果与验证归属者,同时不复制实现细节。
|
||||
- 预算 manifest 同时记录目标与临时上限,其检查会报告或拒绝违反余量或棘轮规则的状态。
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../../../packages/preset/agent-preset/skills/agent-experience/SKILL.md
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
name: agent-experience
|
||||
description: Use when writing or changing a model-facing tool definition (name, description, parameter schema, or its system-prompt section), and when designing skills, context loading, or multi-step workflows, to make information discoverable and use context efficiently.
|
||||
---
|
||||
|
||||
Read and follow [tool design](../../../packages/preset/agent-preset/skills/dsh-doc/references/tool-design.md). The `dsh-doc` skill ships that file to installed DSH, so edit the rules there.
|
||||
@@ -33,7 +33,7 @@ While the root version is unchanged, the current version directory describes the
|
||||
|
||||
## Format
|
||||
|
||||
Start from the [upgrade-guide template](../dsh-doc/templates/upgrade-guide.md). `verify-upgrade-guides` enforces:
|
||||
Start from the [upgrade-guide template](../dsh-doc-writing/templates/upgrade-guide.md). `verify-upgrade-guides` enforces:
|
||||
|
||||
- frontmatter with exactly `kind: upgrade-guide` and a one-sentence `description` naming what breaks; add a key only together with its gate check, then backfill every guide;
|
||||
- one `#` title, then exactly the `## Change` and `## Migration` sections in `guide.md`, and `## 变更` and `## 迁移` in `guide.zh.md`, in that order;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
name: dsh-doc
|
||||
name: dsh-doc-writing
|
||||
description: Create, restructure, review, audit, or migrate DeepSeek Harness Markdown documentation, package READMEs, and the documentation website using audience-first hierarchy, kind-mapped YAML metadata, bilingual line alignment, summary/contents navigation, progressive user-to-developer detail, executed-operation fact-checking, and repository validation. Use for new or revised DSH docs, docs-tree organization, documentation-quality audits and budgets, website page publishing, and bilingual documentation structure changes.
|
||||
---
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,43 +1,30 @@
|
||||
---
|
||||
name: dsh-official-plugin-artwork
|
||||
description: Draw and review official DeepSeek Harness plugin icons and optional feature previews, including experimental plugins. Use for package artwork and PluginArtwork glyphs; covers semantic references, proportions, lettering, transparency, localization, package metadata, and designer review. Not for toolbar or action icons.
|
||||
description: Draw and review official DeepSeek Harness plugin icons and optional feature previews, including experimental plugins. Use for package artwork and PluginArtwork glyphs; covers official references and palettes, feature previews, package metadata gates, and designer review, and links the shared drawing rules that the `dsh-doc` skill ships. Not for toolbar or action icons.
|
||||
---
|
||||
|
||||
# Official plugin artwork
|
||||
|
||||
Draw recognizable artwork that belongs beside the existing official icons. Official and official experimental plugins share this visual language; the UI label communicates experimental status. This guidance does not replace designer approval.
|
||||
Read and follow [plugin artwork](../../../packages/preset/agent-preset/skills/dsh-doc/references/artwork.md) first; installed DSH ships it in the `dsh-doc` skill, and it owns the subject, drawing, and export rules. Its Review section describes Creator bundles installed through Plugin Manager; for official plugins, follow its comparison step and replace its other steps with [Review and verify](#review-and-verify) below. Official and official experimental plugins share one visual language; the UI label communicates experimental status. This guidance does not replace designer approval.
|
||||
|
||||
When reviewing the containing page, use the [UI review baseline](../../../docs/ui-design-review-cases.md#review-baseline) to combine artwork balance with its tile, label, neighboring cards and theme material. Keep package artwork distinct from action icons: coherent optical weight does not require equal drawing bounds, monochrome fills or identical geometry across those roles.
|
||||
|
||||
## Optional feature previews
|
||||
|
||||
Before drawing a larger image for the top of an official bundle's detail document, read and follow [adding a plugin feature preview](../../../docs/cookbook/adding-a-plugin-preview.md). It owns optionality, image count, design canvas, position, language variants, file location, and acceptance. The guide also owns the embedded-raster exception for SVG previews; small icons remain geometry-only. The icon dimensions below do not apply to previews. Inspect the real feature and preserve its produced assets.
|
||||
Before drawing a larger image for the top of an official bundle's detail document, read and follow [adding a plugin feature preview](../../../docs/cookbook/adding-a-plugin-preview.md). It owns optionality, image count, design canvas, position, language variants, file location, and acceptance. The guide also owns the embedded-raster exception for SVG previews; small icons remain geometry-only. The icon dimensions in plugin artwork do not apply to previews. Inspect the real feature and preserve its produced assets.
|
||||
|
||||
## Establish the subject
|
||||
## Official references and owners
|
||||
|
||||
- Inspect the actual feature, its output, and its owning manifest before drawing. A badge-producing skill may need a tag, not a medal. Reuse designer-supplied assets; do not invent a brand mark or change product names, identifiers, or unrelated icons without approval.
|
||||
- Read the [existing artwork](../../../packages/client/ui-primitives/src/plugin-artwork.tsx) and the nearest relevant package SVGs. Borrow one coherent treatment, not every reference's effects. Action icons remain with [the UI design entry](../dsh-client-ui-ux/SKILL.md#select-the-rule-owners).
|
||||
- Check every affected entry, list, detail, and component row. Bundle artwork and a component's name and icon can have separate metadata owners; changing one does not update the other.
|
||||
|
||||
## Draw and refine
|
||||
|
||||
1. **Start simple.** Use one dominant subject and only the detail needed to explain it. Related capabilities can share a metaphor; distinguish them through proportions, silhouette, or palette rather than extra panels, symbols, or badges.
|
||||
2. **Judge optical size.** Use `width="36" height="36" viewBox="0 0 36 36"`. Center the artwork with deliberate margins; 18–22 units is a useful starting extent, not a requirement for every silhouette or wide text tag. The [page renderer](../../../packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx) owns display sizes and outer tile framing. Do not bake that tile into the SVG or enlarge a toolbar glyph to fill it.
|
||||
3. **Keep geometry consistent.** Balance the main subject and secondary element. Align repeated marks and corners; keep tails and handles intentional. Refine one requested dimension at a time without changing approved parts.
|
||||
4. **Use restrained color by default.** Balance saturation and brightness with neighboring icons; prefer existing palettes such as [Auto Authorization Review](../../../packages/experimental/auto-review/icon.svg) and [Codex subagent](../../../packages/subagent/subagent-codex/icon.svg). Reserve unusually vivid, bright, or neon colors for approved brand requirements, essential contrast, or explicit designer direction. Tune saturation separately from hue and lightness; do not make artwork gray or muddy merely to reduce saturation. Keep flat fills or short gradients and limited layering; avoid glow, drop shadows, glossy highlights, and emoji. Page CSS still uses semantic theme tokens.
|
||||
5. **Align lettering by visible ink.** Use consistent letterforms, cap height, baseline, weight, and spacing. Compare rendered stroke edges, not just path coordinates: stroke caps and joins can make equal coordinates appear unequal. Export lettering as paths without a font dependency, and keep it readable at actual size.
|
||||
6. **Make empty areas genuinely transparent.** A hollow lens must reveal the background, including where it overlaps another shape. Remove the underlying fill with a local mask or cutout; do not substitute a white or dark disk. Check both themes.
|
||||
|
||||
## Export at the owner
|
||||
|
||||
- Use the manifest's `icon` path and publish its resources. Update intentional bundle/component copies together; verify localized display names and descriptions when those are in scope. Do not change the feature's produced assets merely to change its plugin icon.
|
||||
- Package images render through `<img>`: export self-contained SVG geometry with local, uniquely named gradient, mask, and clip references. No scripts, event handlers, animation, external images/styles, fonts, or `foreignObject`. Inline React artwork needs per-instance IDs; separate image documents do not share IDs.
|
||||
- Reuse designer-supplied assets. Do not invent a brand mark or change product names, identifiers, or unrelated icons without approval.
|
||||
- Read the [existing artwork](../../../packages/client/ui-primitives/src/plugin-artwork.tsx) and the nearest relevant package SVGs. Prefer existing palettes such as [Auto Authorization Review](../../../packages/experimental/auto-review/icon.svg) and [Codex subagent](../../../packages/subagent/subagent-codex/icon.svg). The designer supplies explicit permission for vivid colors; brand requirements need approval. Use these references instead of installed-plugin discovery. Action icons remain with [the UI design entry](../dsh-client-ui-ux/SKILL.md#select-the-rule-owners).
|
||||
- The [page renderer](../../../packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx) owns display sizes and outer tile framing. SVG artwork may use literal colors; page CSS uses semantic theme tokens.
|
||||
- Check every affected entry, list, detail, and component row. Bundle artwork and component metadata have separate owners; update intentional copies together and verify localized display names and descriptions when those are in scope.
|
||||
- Inline React artwork needs per-instance IDs; separate image documents do not share IDs.
|
||||
- Keep the asset change local. Do not add runtime dependencies, primitive exports, or renderer special cases just to display it.
|
||||
|
||||
## Review and verify
|
||||
|
||||
1. Show a few numbered alternatives before polishing uncertain geometry. Preserve their labels through review and apply the chosen version; an existing file or generated draft does not establish designer approval. Keep comparison artifacts in a gitignored directory, not the product package.
|
||||
2. Compare old and new artwork beside unchanged references in light and dark themes, at actual row/card sizes as well as enlarged. Check balance, lettering, empty space, clipping, and image decoding.
|
||||
3. Run `pnpm run verify-package-meta` for [package metadata and SVG artwork](../../../scripts/verify-package-meta.ts), `pnpm run constraints` for the canonical package payload, and `pnpm run verify-official-bundle-catalog`. Regenerate a stale catalog with `pnpm run gen-official-bundle-catalog`, never by hand. Structural checks do not replace visual review.
|
||||
4. Build affected artifacts and inspect the real page through `dsh web`. Batch approved refinements before restarting a metadata-caching Host, then verify the entry and nested component rows in both themes.
|
||||
5. Follow the repository's UI testing policy and [record-browser-gif](../record-browser-gif/SKILL.md) for a GUI PR. Obtain designer approval before landing new glyphs. Record reusable drawing principles here, not the history of rejected drafts.
|
||||
1. Before polishing uncertain geometry, show a few numbered alternatives and keep their numbers through designer review. Apply the chosen one; existing files and generated drafts do not establish approval. Keep comparison artifacts in a gitignored directory, not the product package.
|
||||
2. Run `pnpm run verify-package-meta` for [package metadata and SVG artwork](../../../scripts/verify-package-meta.ts), `pnpm run constraints` for the canonical package payload, and `pnpm run verify-official-bundle-catalog`. Regenerate a stale catalog with `pnpm run gen-official-bundle-catalog`, never by hand. Structural checks do not replace visual review.
|
||||
3. Build affected artifacts and inspect the real page through `dsh web`. Batch approved refinements before restarting a metadata-caching Host, then verify the entry and nested component rows in both themes.
|
||||
4. Follow the repository's UI testing policy and [record-browser-gif](../record-browser-gif/SKILL.md) for a GUI PR. Obtain designer approval before landing new glyphs. Record reusable drawing principles in [plugin artwork](../../../packages/preset/agent-preset/skills/dsh-doc/references/artwork.md), not the history of rejected drafts.
|
||||
@@ -5,7 +5,7 @@ description: Use when writing, reviewing, restoring, trimming, or auditing prose
|
||||
|
||||
# DeepSeek Harness Prose Standard
|
||||
|
||||
Write enough to preserve the contract, then remove reasoning transcripts, repetition, and decoration. A contract is an obligation, invariant, precondition, postcondition, or compatibility promise that a caller, callee, implementer, producer, or consumer relies on. This skill owns editorial judgment and required prose coverage; use [dsh-doc](../dsh-doc/SKILL.md) for placement, budgets, bilingual pairs, and documentation gates, and [dsh-trim-cot-leakage](../dsh-trim-cot-leakage/SKILL.md) for hunting and fixing reasoning-transcript leakage. It is guidance, not a script.
|
||||
Write enough to preserve the contract, then remove reasoning transcripts, repetition, and decoration. A contract is an obligation, invariant, precondition, postcondition, or compatibility promise that a caller, callee, implementer, producer, or consumer relies on. This skill owns editorial judgment and required prose coverage; use [dsh-doc-writing](../dsh-doc-writing/SKILL.md) for placement, budgets, bilingual pairs, and documentation gates, and [dsh-trim-cot-leakage](../dsh-trim-cot-leakage/SKILL.md) for hunting and fixing reasoning-transcript leakage. It is guidance, not a script.
|
||||
|
||||
Treat `contract`, `boundary`, `shape`, `surface`, `seam`, `gate`, and `vocabulary` as terms to check before use, not banned words. First ask whether the exact rule, API, field set, type, validation, timing point, component split, or failure states the fact better. Keep a term when it names the exact technical subject, including caller/callee contracts and security/process boundaries.
|
||||
|
||||
|
||||
@@ -40,6 +40,6 @@ Unaided citation passes fail in both directions by deleting durable references a
|
||||
|
||||
1. Scope and exclusions per [dsh-prose-standard](../dsh-prose-standard/SKILL.md): require an explicit scope; never touch `vendor/` or `.agents/notes/archived/`. Recorded fixtures and snapshots are derivatives, not prose targets: change the owning source or scenario and regenerate them only when an authorized behavior change requires new evidence.
|
||||
2. Audit read-only first: run the [recall batteries](references/recall-batteries.md) (with `--hidden` so `.agents/` is searched), calibrating each probe against a known positive and a near-miss negative before trusting its output, then judge every hit semantically. The batteries are probes, not the definition — each review round of the original purge found cases the batteries missed, so also read the densest prose in scope (module JSDoc, READMEs, Agent Notes) without a pattern in hand.
|
||||
3. Fix owner-first per surface: generated catalogs → trace every consumer, fix the source JSDoc or generator template, then regenerate all derivatives; type-equivalence fences → fix the source JSDoc, then re-paste both bilingual pages (`verify-type-equiv` pins them); bilingual prose → update the counterpart minimally and re-record it through the [lightweight routine](../../../docs/AGENTS.md#writing-rules); bilingual fences → copy the corrected verbatim block byte-for-byte into both sides per [dsh-doc](../dsh-doc/SKILL.md), then re-record the pair; model- or user-visible strings → route through [dsh-prose-standard](../dsh-prose-standard/SKILL.md) and change only with owning behavior evidence, otherwise leave unchanged and report the deferral.
|
||||
3. Fix owner-first per surface: generated catalogs → trace every consumer, fix the source JSDoc or generator template, then regenerate all derivatives; type-equivalence fences → fix the source JSDoc, then re-paste both bilingual pages (`verify-type-equiv` pins them); bilingual prose → update the counterpart minimally and re-record it through the [lightweight routine](../../../docs/AGENTS.md#writing-rules); bilingual fences → copy the corrected verbatim block byte-for-byte into both sides per [dsh-doc-writing](../dsh-doc-writing/SKILL.md), then re-record the pair; model- or user-visible strings → route through [dsh-prose-standard](../dsh-prose-standard/SKILL.md) and change only with owning behavior evidence, otherwise leave unchanged and report the deferral.
|
||||
4. Before deleting anything, enumerate the passage's propositions (prose-standard) and check the [overcorrection traps](references/examples.md#overcorrection-traps): trims that flip an obligation into an endorsement, promote a hypothetical to a shipped feature, delete a true fact, or drop the evidence source.
|
||||
5. Verify: re-run the batteries expecting only sanctioned keeps, this skill's own directory, and the owning note's quoted evidence; confirm every remaining citation resolves at HEAD; run the gates for touched surfaces (`doc-sync` for docs, `verify-type-equiv`, `verify-translation-pairing`).
|
||||
@@ -173,7 +173,7 @@ describe.skipIf(!existsSync(join(root, 'apps/cli/lib/bin.js')))('Official option
|
||||
const standard = await agent()
|
||||
expect(names()).toEqual(['team_task_create', 'team_task_get', 'team_task_list', 'team_task_update', 'working_directory'])
|
||||
expect(names(minimal.agent)).toEqual([process.platform === 'win32' ? 'pwsh' : 'bash', 'read_image', 'team_task_create', 'team_task_get', 'team_task_list', 'team_task_update', 'working_directory'])
|
||||
for (const name of ['str_replace_editor', 'session_search', 'terminal_open', 'ralph']) expect(names(standard.agent)).not.toContain(name)
|
||||
for (const name of ['str_replace_editor', 'session_search', 'terminal_open', 'user_terminal_list', 'ralph']) expect(names(standard.agent)).not.toContain(name)
|
||||
expect((await ctx.skills.list({ cwd: workspace, scope: standard.agent })).some(skill => skill.name === 'dsh-badge')).toBe(false)
|
||||
})
|
||||
|
||||
@@ -254,6 +254,7 @@ describe.skipIf(!existsSync(join(root, 'apps/cli/lib/bin.js')))('Official option
|
||||
it('runs and closes a real persistent terminal delivered by its bundle', async () => {
|
||||
await select(bundles.terminal, true)
|
||||
const owner = await agent()
|
||||
expect(names(owner.agent)).toEqual(expect.arrayContaining(['terminal_open', 'user_terminal_list', 'user_terminal_read']))
|
||||
const target = join(workspace, 'terminal-output.txt')
|
||||
let terminalId: string | undefined
|
||||
const id = () => {
|
||||
@@ -280,6 +281,7 @@ describe.skipIf(!existsSync(join(root, 'apps/cli/lib/bin.js')))('Official option
|
||||
expect(toolResult(owner.agent, 'terminal-close')).toContain('closed terminal session')
|
||||
await select(bundles.terminal, false)
|
||||
expect(names((await agent()).agent)).not.toContain('terminal_open')
|
||||
expect(names((await agent()).agent)).not.toContain('user_terminal_list')
|
||||
})
|
||||
|
||||
it('runs a real fresh-Agent Ralph workflow through the selected bundle', async () => {
|
||||
@@ -332,7 +334,7 @@ describe.skipIf(!existsSync(join(root, 'apps/cli/lib/bin.js')))('Official option
|
||||
let coded: Agent | undefined
|
||||
for (const preset of ['standard', 'cordis', 'ptc', 'minimal']) {
|
||||
const owner = await agent(preset)
|
||||
expect(names(owner.agent)).toEqual(expect.arrayContaining(['session_search', 'terminal_open', 'ralph']))
|
||||
expect(names(owner.agent)).toEqual(expect.arrayContaining(['session_search', 'terminal_open', 'user_terminal_read', 'ralph']))
|
||||
if (preset === 'ptc') coded = owner.agent
|
||||
}
|
||||
if (coded === undefined) throw new Error('the PTC Agent was not created')
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
{"type":"step/start","data":{"turn":1,"step":1}}
|
||||
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Create a durable two-round goal for the ACP snapshot, inspect it, then report readiness."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"<system-reminder>\nThe following information describes your current session:\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names.\n</system-reminder>"}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"session/title","data":{"title":"Create a durable two-round goal","messageSeqs":[8],"source":{"kind":"fallback"}}}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
{"type":"step/start","data":{"turn":1,"step":1}}
|
||||
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Create a durable goal for the wrap-up snapshot, then report readiness."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"<system-reminder>\nThe following information describes your current session:\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names.\n</system-reminder>"}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"session/title","data":{"title":"Create a durable goal for","messageSeqs":[8],"source":{"kind":"fallback"}}}
|
||||
|
||||
@@ -140,13 +140,13 @@ it('pins Messages Files offload and inline fallback in assembled requests', asyn
|
||||
const fileImage = { type: 'image', source: { type: 'file', file_id: 'file-api-snapshot-1' } }
|
||||
const offloadedImage = `[image omitted to fit request image limits; ${attachmentId}.${normalizedAccess}]`
|
||||
const imageHandle = `Image ${attachmentId}; request preview 1x1px.${normalizedAccess}`
|
||||
const runtimeContext = (cwd: string): string => 'Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\n'
|
||||
const runtimeContext = (cwd: string): string => '<system-reminder>\nThe following information describes your current session:\n\n'
|
||||
+ `Current working directory: ${JSON.stringify(cwd)}.\n\n`
|
||||
+ 'Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\n'
|
||||
+ 'Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\n'
|
||||
+ 'You are the root agent. Your name is "lead".\n\n'
|
||||
+ 'To message other agents, use send_message({ agent: "<agent-name>", message: "..." }).\n'
|
||||
+ 'Use list_agents({}) to find your collaborator agents and their names.'
|
||||
+ 'Use list_agents({}) to find your collaborator agents and their names.\n</system-reminder>'
|
||||
const messages = requests[0]?.messages as { content?: unknown }[] | undefined
|
||||
const offloaded = messages?.find(message => JSON.stringify(message.content).includes('[image omitted'))
|
||||
expect(offloaded?.content).toEqual([
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"Probe strict-schema fillers against missing-goal revision 1, then create a durable goal to finish the snapshot proof and inspect it."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"<system-reminder>\nThe following information describes your current session:\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names.\n</system-reminder>"}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"Probe strict-schema fillers against miss","messageSeqs":[8],"source":{"kind":"fallback"}}}}
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
{"type":"step/start","data":{"turn":1,"step":1}}
|
||||
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Prove the product headless profile path with one real tool round trip."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"<system-reminder>\nThe following information describes your current session:\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names.\n</system-reminder>"}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"cli-mock","model":"cli-mock","reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true},"tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"request/context","data":{"provider":"cli-mock","model":"cli-mock"}}
|
||||
{"type":"session/title","data":{"title":"Prove the product headless profile","messageSeqs":[8],"source":{"kind":"fallback"}}}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"say pong"}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"<system-reminder>\nThe following information describes your current session:\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names.\n</system-reminder>"}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":"{{tools}}"},"reason":"initial"}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":1000000}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"say pong","messageSeqs":[8],"source":{"kind":"fallback"}}}}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"say pong"}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"<system-reminder>\nThe following information describes your current session:\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names.\n</system-reminder>"}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":"{{tools}}"},"reason":"initial"}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":1000000}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"say pong","messageSeqs":[8],"source":{"kind":"fallback"}}}}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"retry the transient provider failure"}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"<system-reminder>\nThe following information describes your current session:\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names.\n</system-reminder>"}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"retry the transient provider failure","messageSeqs":[8],"source":{"kind":"fallback"}}}}
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
{"type":"step/start","data":{"turn":2,"step":1}}
|
||||
{"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Continue safely from the interrupted operation."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"<system-reminder>\nThe following information describes your current session:\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names.\n</system-reminder>"}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial","startsSeries":true}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"session/title","data":{"title":"Perform one side-effecting remote mutati","messageSeqs":[3],"source":{"kind":"fallback"}}}
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
{"type":"step/start","data":{"turn":2,"step":1}}
|
||||
{"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Call list_agents once and report what it shows."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"<system-reminder>\nThe following information describes your current session:\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names.\n</system-reminder>"}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial","startsSeries":true}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"session/title","data":{"title":"Start a background job.","messageSeqs":[3],"source":{"kind":"fallback"}}}
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
{"type":"step/start","data":{"turn":1,"step":1}}
|
||||
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE."},{"type":"text","text":"Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. If your last action is a message to your parent, the completion notice omits your final answer; include the result and relevant evidence in that message."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a subagent delegated by agent \"lead\".\nYour name is \"agent-1\".\nThe root agent of your collaboration tree is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names.\n\nDelegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly.\n\nDelegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:identity","text":"You are a subagent delegated by agent \"lead\".\nYour name is \"agent-1\".\nThe root agent of your collaboration tree is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."},{"name":"tool:subagent:depth","text":"Delegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly."},{"name":"tool:subagent_fork:depth","text":"Delegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"<system-reminder>\nThe following information describes your current session:\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a subagent delegated by agent \"lead\".\nYour name is \"agent-1\".\nThe root agent of your collaboration tree is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names.\n\nDelegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly.\n\nDelegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it.\n</system-reminder>"}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:identity","text":"You are a subagent delegated by agent \"lead\".\nYour name is \"agent-1\".\nThe root agent of your collaboration tree is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."},{"name":"tool:subagent:depth","text":"Delegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly."},{"name":"tool:subagent_fork:depth","text":"Delegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","reasoningEffort":"low"},"tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"session/title","data":{"title":"Use the write tool exactly","messageSeqs":[8],"source":{"kind":"fallback"}}}
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
{"type":"step/start","data":{"turn":2,"step":1}}
|
||||
{"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Delegate the write probe to a subagent."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"<system-reminder>\nThe following information describes your current session:\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names.\n</system-reminder>"}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","reasoningEffort":"low"},"tools":["apply_patch","bash","create_goal","edit","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","wait_agent","web_fetch","web_search","workflow","working_directory","write"]},"reason":"resume","startsSeries":true}}
|
||||
{"type":"developer/message","data":{"turn":2,"step":1,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"apply_patch"},{"type":"tool-addition","toolName":"bash"},{"type":"tool-addition","toolName":"create_goal"},{"type":"tool-addition","toolName":"edit"},{"type":"tool-addition","toolName":"get_goal"},{"type":"tool-addition","toolName":"glob"},{"type":"tool-addition","toolName":"grep"},{"type":"tool-addition","toolName":"interrupt_agent"},{"type":"tool-addition","toolName":"job_kill"},{"type":"tool-addition","toolName":"job_list"},{"type":"tool-addition","toolName":"job_output"},{"type":"tool-addition","toolName":"list_agents"},{"type":"tool-addition","toolName":"read"},{"type":"tool-addition","toolName":"read_image"},{"type":"tool-addition","toolName":"send_message"},{"type":"tool-addition","toolName":"skill"},{"type":"tool-addition","toolName":"subagent"},{"type":"tool-addition","toolName":"subagent_fork"},{"type":"tool-addition","toolName":"todo_write"},{"type":"tool-addition","toolName":"update_goal"},{"type":"tool-addition","toolName":"wait_agent"},{"type":"tool-addition","toolName":"web_fetch"},{"type":"tool-addition","toolName":"web_search"},{"type":"tool-addition","toolName":"workflow"},{"type":"tool-addition","toolName":"working_directory"},{"type":"tool-addition","toolName":"write"}],"role":"developer","id":"{{sessionId}}"},"headerSeq":18},"surfaceOp":"append"}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
{"type":"step/start","data":{"turn":1,"step":1}}
|
||||
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message."},{"type":"text","text":"Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. If your last action is a message to your parent, the completion notice omits your final answer; include the result and relevant evidence in that message."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a subagent delegated by agent \"lead\".\nYour name is \"agent-1\".\nThe root agent of your collaboration tree is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names.\n\nDelegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly.\n\nDelegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:identity","text":"You are a subagent delegated by agent \"lead\".\nYour name is \"agent-1\".\nThe root agent of your collaboration tree is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."},{"name":"tool:subagent:depth","text":"Delegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly."},{"name":"tool:subagent_fork:depth","text":"Delegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"<system-reminder>\nThe following information describes your current session:\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a subagent delegated by agent \"lead\".\nYour name is \"agent-1\".\nThe root agent of your collaboration tree is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names.\n\nDelegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly.\n\nDelegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it.\n</system-reminder>"}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:identity","text":"You are a subagent delegated by agent \"lead\".\nYour name is \"agent-1\".\nThe root agent of your collaboration tree is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."},{"name":"tool:subagent:depth","text":"Delegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly."},{"name":"tool:subagent_fork:depth","text":"Delegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"session/title","data":{"title":"Reply with exactly CHILD_RESULT and","messageSeqs":[8],"source":{"kind":"fallback"}}}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"Start one continuable subagent and acknowledge its completion notice. Do not call list_agents or send_message."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"<system-reminder>\nThe following information describes your current session:\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names.\n</system-reminder>"}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"Start one continuable subagent and","messageSeqs":[8],"source":{"kind":"fallback"}}}}
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
{"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Acknowledge the current workspace instruction."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"<system-reminder>\nUpdated instructions from: AGENTS.md\n\nThis file changed after it was loaded. Use the following content instead of the previously loaded instructions from this file.\n\nNew workspace instruction after offline edit.\n\n</system-reminder>"}],"source":{"kind":"agent-instructions","form":"instructions","changes":[{"action":"replace","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"d8375b516f158718bd3463bc8eb7ed42c011b29f"}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"<system-reminder>\nThe following information describes your current session:\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names.\n</system-reminder>"}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial","startsSeries":true}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"session/title","data":{"title":"Remember the workspace instruction.","messageSeqs":[3],"source":{"kind":"fallback"}}}
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
{"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Acknowledge the current workspace instruction."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"<system-reminder>\nThis complete workspace instruction baseline replaces all earlier workspace instruction baselines. The following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nCurrent AGENTS rule.\n\n\nInstructions from: CLAUDE.md\n\nCurrent CLAUDE rule.\n\n</system-reminder>"}],"source":{"kind":"agent-instructions","form":"instructions","baseline":true,"baselineIdentity":"{\"projectRoot\":\"\",\"projectRootMarkers\":[\".git\"],\"maxBytes\":65536,\"maxSourceBytes\":1048576,\"instructionFileCandidates\":[\"AGENTS.md\",\"CLAUDE.md\"],\"localInstructionFileCandidates\":[\"AGENTS.local.md\",\"CLAUDE.local.md\"]}","changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"7f53d2327837129750aef117f9754a001c46cf68"},{"action":"set","scope":".\u0000CLAUDE.md","path":"CLAUDE.md","digest":"5b1e9e3fd759eee6b43ceff899e47fb10c64701a"}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"<system-reminder>\nThe following information describes your current session:\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names.\n</system-reminder>"}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial","startsSeries":true}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"session/title","data":{"title":"Remember the workspace instruction.","messageSeqs":[3],"source":{"kind":"fallback"}}}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { createRequire } from 'node:module'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { mkdir, mkdtemp, readFile, realpath, rm, symlink, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { readdirSync, readFileSync, realpathSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
@@ -31,6 +31,7 @@ import { applyChildComposition, childSessionMeta } from '@deepseek-ai/dsh-subage
|
||||
import { ToolCallId, LlmAdapter, createUserMessage } from '@deepseek-ai/dsh-llm'
|
||||
import type { GenerateOptions, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import type {} from '@deepseek-ai/dsh-compaction-basic'
|
||||
import { codePointLength } from '@deepseek-ai/dsh-compaction-tool-result-pruner'
|
||||
import type {} from '@deepseek-ai/dsh-skill'
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
// Type-only: resolves `ctx.get('sessionProjections')` and `ctx.get('tokenMeter')`.
|
||||
@@ -561,11 +562,42 @@ describe('the shipped Web composition', () => {
|
||||
expect(tools).not.toContain('str_replace_editor')
|
||||
expect(ctx.commands.find(handle.agent, 'goal')).toBeDefined()
|
||||
|
||||
// The preset's own authoring skill registers into ITS layer of the host
|
||||
// The preset's own skill registers into ITS layer of the host
|
||||
// registry: the cordis agent's view carries it, the global view does not.
|
||||
const scoped = (await ctx.skills.list({ scope: handle.agent })).map(skill => skill.name)
|
||||
expect(scoped).toContain('editing-cordis-compositions')
|
||||
expect((await ctx.skills.list()).map(skill => skill.name)).not.toContain('editing-cordis-compositions')
|
||||
const packageSkills = realpathSync(join(REPO_ROOT, 'packages/preset/agent-preset/skills'))
|
||||
const shipped = (await ctx.skills.list({ scope: handle.agent }))
|
||||
.filter(skill => skill.path?.startsWith(packageSkills) === true).map(skill => skill.name)
|
||||
expect(shipped).toEqual(['dsh-doc'])
|
||||
expect((await ctx.skills.list()).map(skill => skill.name)).not.toContain('dsh-doc')
|
||||
const loaded = await executeToolFixture(ctx, {
|
||||
callId: ToolCallId('preset-cordis-dsh-doc'),
|
||||
name: 'skill',
|
||||
arguments: { name: 'dsh-doc' },
|
||||
signal: new AbortController().signal,
|
||||
agent: handle.agent,
|
||||
})
|
||||
expect(loaded.isError).toBe(false)
|
||||
const output = loaded.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('\n')
|
||||
expect(output).toContain('<skill_content name="dsh-doc">')
|
||||
expect(output).toContain(`Base directory for this skill: ${join(packageSkills, 'dsh-doc')}`)
|
||||
expect(output).toContain('Loading this skill does not authorize changes.')
|
||||
// The composed `read` tool, behind the fs sandbox, returns each whole reference below the
|
||||
// pruner threshold, leaving room for an installation path of up to 256 characters.
|
||||
const references = join(packageSkills, 'dsh-doc', 'references')
|
||||
for (const file of readdirSync(references).filter(entry => entry !== 'packages.md')) {
|
||||
const path = join(references, file)
|
||||
const result = await executeToolFixture(ctx, {
|
||||
callId: ToolCallId(`preset-cordis-dsh-doc-${file}`),
|
||||
name: 'read',
|
||||
arguments: { file_path: path },
|
||||
signal: new AbortController().signal,
|
||||
agent: handle.agent,
|
||||
})
|
||||
expect(result.isError, file).toBe(false)
|
||||
const text = result.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('\n')
|
||||
expect(text, file).toContain('(End of file - total ')
|
||||
expect(codePointLength(text) - codePointLength(path) + 256, file).toBeLessThan(8192)
|
||||
}
|
||||
|
||||
// The persona is `standard`'s, pinned verbatim so the two declarations
|
||||
// cannot drift apart: tool descriptions and the skill catalog carry
|
||||
@@ -634,12 +666,10 @@ describe('the shipped Web composition', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('ships the composition-authoring skill in the declaration package', async () => {
|
||||
const skill = join(
|
||||
REPO_ROOT, 'packages/preset/agent-preset', 'skills', 'editing-cordis-compositions', 'SKILL.md',
|
||||
)
|
||||
it('ships the dsh-doc skill in the declaration package', async () => {
|
||||
const skill = join(REPO_ROOT, 'packages/preset/agent-preset', 'skills', 'dsh-doc', 'SKILL.md')
|
||||
|
||||
expect((await readFile(skill, 'utf8')).startsWith('---\nname: editing-cordis-compositions')).toBe(true)
|
||||
expect((await readFile(skill, 'utf8')).startsWith('---\nname: dsh-doc\n')).toBe(true)
|
||||
})
|
||||
|
||||
it('merges the global skill layer into a preset agent\'s catalog, keeping local discovery preset-side', async () => {
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
en: 127ea90553cb6af7
|
||||
zh: 7b1c5aec8558c684
|
||||
/deepseek-harness-desktop/application-overview:
|
||||
en: d4afa46403997884
|
||||
zh: 9b9fe126afda70f0
|
||||
en: 1f59637852e162b4
|
||||
zh: b1530a1af195cb92
|
||||
/deepseek-harness-desktop/terminal-command:
|
||||
en: 5f2d25a1d8472647
|
||||
zh: 2acb5c5d7d1917e2
|
||||
|
||||
@@ -31,6 +31,8 @@ Desktop runs the Harness workspace in a native application. This reference cover
|
||||
|
||||
## Application overview
|
||||
|
||||
Desktop reads the effective application theme from the Host startup settings response before showing the welcome or workspace window. Missing or invalid optional theme settings use system appearance; invalid values are logged. Windows workspace backgrounds remain opaque and follow the effective light/dark theme, including runtime changes; custom palettes use the built-in light/dark background approximation. macOS windows use a transparent background with sidebar vibrancy.
|
||||
|
||||
Electron caches the last confirmed UI language in `userData/desktop/locale-cache` so startup recovery dialogs can use it before Host settings are available. Missing, invalid, or unreadable caches fall back to the system language; cache write failures do not block startup. A successful Host startup or a UI language change refreshes the cache. Until then, a changed system language may differ from the cached language.
|
||||
|
||||
Desktop analytics follows the [product collection policy](../../packages/client/product-analytics/README.md), including its live application setting. Web usage is excluded. Update installation waits for its local analytics intake before locking API admission and stopping the Host. Intake has a one-second deadline; failure does not prevent installation, and collector delivery is not awaited.
|
||||
|
||||
@@ -31,6 +31,8 @@ Desktop 在原生应用中运行 Harness 工作区。本参考文档说明应用
|
||||
|
||||
## 应用概览
|
||||
|
||||
Desktop 在显示欢迎页或工作区窗口前,从 Host 启动设置响应读取有效的应用主题。可选主题设置缺失或无效时使用系统外观;无效值会记录日志。Windows 工作区底色保持不透明,并跟随有效的浅色/深色主题及运行中的变化;自定义配色使用内置浅色/深色底色近似。macOS 窗口使用透明背景和侧边栏毛玻璃效果。
|
||||
|
||||
Electron 在 `userData/desktop/locale-cache` 缓存最近确认的界面语言,使启动恢复弹窗在 Host 设置尚不可用时也能使用该语言。缓存缺失、无效或不可读时回退到系统语言;缓存写入失败不阻断启动。Host 成功启动或界面语言变更时会刷新缓存;在此之前,变更后的系统语言可能与缓存语言不同。
|
||||
|
||||
桌面埋点遵循[产品采集策略](../../packages/client/product-analytics/README.zh.md)及其动态应用配置,不包含 Web 使用情况。安装更新会等待该操作的本地埋点接收请求结束,再锁定 API 准入并停止 Host。接收请求的时限为一秒,失败不会阻止安装,也不等待收集端完成发送。
|
||||
|
||||
@@ -238,9 +238,10 @@ function developmentHostInspectPort(enabled: boolean): number | undefined {
|
||||
/**
|
||||
* Opaque chrome fallback matching the built-in sidebar palette (the resolved
|
||||
* `--dsw-static-neutral-bluish-900` / `-50` tokens). An approximation for
|
||||
* custom themes: Windows swaps in the renderer's measured palette over the
|
||||
* windowsAppearance IPC, and macOS shows it only while minimized or hidden.
|
||||
* @returns the sidebar fill hex for the active system color scheme.
|
||||
* custom themes: Windows captions use the renderer's measured palette over
|
||||
* windowsAppearance IPC. The Windows workspace base stays opaque and follows
|
||||
* the effective light/dark theme; macOS uses it only while minimized or hidden.
|
||||
* @returns the sidebar fill hex for the effective application color scheme.
|
||||
*/
|
||||
function chromeFallbackFill(): string {
|
||||
return nativeTheme.shouldUseDarkColors ? '#1b1b1c' : '#f9fafb'
|
||||
@@ -268,6 +269,7 @@ function createWindow(preload: string, show = false, primary = false): BrowserWi
|
||||
minHeight: 600,
|
||||
show,
|
||||
...(process.platform === 'win32' && primary ? {
|
||||
backgroundColor: chromeFallbackFill(),
|
||||
titleBarStyle: 'hidden' as const,
|
||||
titleBarOverlay: { height: WINDOWS_TITLEBAR_HEIGHT, color: chromeFallbackFill(),
|
||||
symbolColor: nativeTheme.shouldUseDarkColors ? '#f9fafb' : '#0f1115' },
|
||||
@@ -308,6 +310,13 @@ function createWindow(preload: string, show = false, primary = false): BrowserWi
|
||||
// current state so a fullscreen reload does not fall back to windowed CSS.
|
||||
window.webContents.on('did-finish-load', sendFullscreen)
|
||||
}
|
||||
if (process.platform === 'win32' && primary) {
|
||||
const updateBackground = (): void => {
|
||||
if (!window.isDestroyed()) window.setBackgroundColor(chromeFallbackFill())
|
||||
}
|
||||
nativeTheme.on('updated', updateBackground)
|
||||
window.once('closed', () => { nativeTheme.removeListener('updated', updateBackground) })
|
||||
}
|
||||
if (process.platform === 'darwin') {
|
||||
// Deminiaturize reattaches the NSVisualEffectView material late
|
||||
// (electron/electron#25368), so a transparent window shows the desktop
|
||||
@@ -870,8 +879,11 @@ async function main(): Promise<void> {
|
||||
ipcMain.handle(PLATFORM_IPC.close, (event) => { assertMainApplication(event); platformView.close() })
|
||||
// Only the main window may synchronize its palette with the native material.
|
||||
ipcMain.on(DESKTOP_IPC.nativeThemeSet, (event, source: unknown) => {
|
||||
if (mainWindow === undefined || event.sender !== mainWindow.webContents) return
|
||||
if (source === 'light' || source === 'dark' || source === 'system') nativeTheme.themeSource = source
|
||||
if (mainWindow === undefined || event.sender !== mainWindow.webContents
|
||||
|| event.senderFrame !== mainWindow.webContents.mainFrame
|
||||
|| !event.senderFrame.url.startsWith(`${SCHEME}://app/`)) return
|
||||
if (source !== 'light' && source !== 'dark' && source !== 'system') return
|
||||
nativeTheme.themeSource = source
|
||||
})
|
||||
ipcMain.handle(DESKTOP_IPC.localeBootstrap, async (event) => {
|
||||
if (event.sender !== mainWindow?.webContents || event.senderFrame !== mainWindow.webContents.mainFrame
|
||||
@@ -1345,6 +1357,7 @@ async function main(): Promise<void> {
|
||||
const window = mainWindow ?? createMainWindow()
|
||||
await navigateMain(applicationUrl)
|
||||
if (isQuitting() || recovery.active || window.isDestroyed()) return
|
||||
if (process.platform === 'win32') window.setBackgroundColor(chromeFallbackFill())
|
||||
if (activate) window.show()
|
||||
else window.showInactive()
|
||||
enteredWorkspace = true
|
||||
@@ -1428,6 +1441,7 @@ async function main(): Promise<void> {
|
||||
if (quitting || recovery.active) return
|
||||
const state = await readWelcomeState()
|
||||
if (isQuitting() || backend.state.phase !== 'ready') return
|
||||
nativeTheme.themeSource = state.themePreference
|
||||
locale = resolveDesktopStartupLocale(state.localePreference, systemLanguages)
|
||||
writeDesktopLocaleCache(app.getPath('userData'), locale.id)
|
||||
windowsLanguage = locale.id
|
||||
|
||||
@@ -10,6 +10,8 @@ export interface WelcomeState {
|
||||
readonly hasApiKey: boolean
|
||||
readonly writable: boolean
|
||||
readonly localePreference: string | null
|
||||
/** Effective Host theme preference; absent or invalid optional theme settings use system appearance. */
|
||||
readonly themePreference: 'light' | 'dark' | 'system'
|
||||
}
|
||||
|
||||
/** Narrow operations available to the native welcome flow. */
|
||||
@@ -19,7 +21,7 @@ export interface DesktopWelcomeBackend {
|
||||
readonly account: DesktopAccountBackend
|
||||
/** @param event - desktop-owned fields. @returns after local Host intake. */
|
||||
report(event: ProductEvent): Promise<void>
|
||||
/** @returns Configured-key presence and the shared language preference, without credential values. */
|
||||
/** @returns Configured-key presence and the shared language and theme preferences, without credential values. */
|
||||
read(): Promise<WelcomeState>
|
||||
/** @returns The saved UI language without account or provider requests. */
|
||||
readLocalePreference(): Promise<string | null>
|
||||
@@ -85,6 +87,14 @@ export function connectDesktopWelcome(
|
||||
}
|
||||
return locale.value.preference ?? null
|
||||
}
|
||||
const themePreference = (namespaces: unknown[]): WelcomeState['themePreference'] => {
|
||||
const theme = namespaces.find(item => record(item) && item.ns === 'ui-theme')
|
||||
if (theme === undefined) return 'system'
|
||||
const preference = record(theme) && record(theme.value) ? theme.value.preference : undefined
|
||||
if (preference === 'light' || preference === 'dark' || preference === 'system') return preference
|
||||
console.warn('desktop appearance: invalid Host theme preference; using system appearance')
|
||||
return 'system'
|
||||
}
|
||||
const read = async (): Promise<WelcomeState> => {
|
||||
const { settings, ref } = await settingsAndReference()
|
||||
const providers = await invoke({ namespace: 'llm', method: 'listConfigurableProviders', args: {} })
|
||||
@@ -116,6 +126,7 @@ export function connectDesktopWelcome(
|
||||
hasApiKey: Object.values(states).some(value => record(value) && value.configured === true),
|
||||
writable: ref !== undefined && record(states[ref]) && states[ref].writable === true,
|
||||
localePreference: localePreference(namespaces),
|
||||
themePreference: themePreference(namespaces),
|
||||
}
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -156,7 +156,7 @@ const harness = await vi.hoisted(async () => {
|
||||
readonly updateTasks = vi.fn(async (_action: 'inspect' | 'lock' | 'unlock') => false)
|
||||
readonly inspectQuit = vi.fn(async () => ({ activeTasks: false, scheduledTasks: false }))
|
||||
url = 'http://127.0.0.1:3080/?token=test'
|
||||
fetch = vi.fn(async () => Response.json({ hasApiKey: true, writable: true, localePreference: null }))
|
||||
fetch = vi.fn(async () => Response.json({ hasApiKey: true, writable: true, localePreference: null, themePreference: 'system' }))
|
||||
readonly ready = deferred()
|
||||
readonly exited = deferred()
|
||||
readonly stopping = deferred()
|
||||
@@ -204,7 +204,12 @@ const harness = await vi.hoisted(async () => {
|
||||
let analyticsEnabled = true
|
||||
let analyticsEnabledListener: ((enabled: boolean) => void) | undefined
|
||||
const analytics = vi.fn(async (_event: unknown) => {})
|
||||
const nativeTheme = { themeSource: 'system', shouldUseDarkColors: false }
|
||||
const nativeTheme = new class extends EventEmitter {
|
||||
themeSource = 'system'
|
||||
systemDark = false
|
||||
get shouldUseDarkColors() { return this.themeSource === 'system' ? this.systemDark : this.themeSource === 'dark' }
|
||||
set shouldUseDarkColors(value: boolean) { this.systemDark = value }
|
||||
}()
|
||||
const trays: FakeTray[] = []
|
||||
class FakeTray extends EventEmitter {
|
||||
readonly setToolTip = vi.fn()
|
||||
@@ -282,6 +287,7 @@ const harness = await vi.hoisted(async () => {
|
||||
loginShell.mockReset().mockImplementation(base => readLoginShell(base))
|
||||
updateDownload.mockReset().mockImplementation(async () => updateState)
|
||||
updateInstall.mockReset().mockImplementation(async () => updateState)
|
||||
nativeTheme.removeAllListeners()
|
||||
nativeTheme.themeSource = 'system'; nativeTheme.shouldUseDarkColors = false
|
||||
preparing = deferred(); prepared = deferred(); hostStarted = deferred()
|
||||
navigated = deferred(); dialogShown = deferred(); quitCompleted = deferred()
|
||||
@@ -1158,12 +1164,77 @@ describe('desktop main startup', () => {
|
||||
{ role: 'toggleDevTools', visible: false, accelerator: 'F12' },
|
||||
])
|
||||
} else {
|
||||
expect(window.options).not.toHaveProperty('backgroundColor')
|
||||
expect(window.options).not.toHaveProperty('titleBarStyle')
|
||||
expect(window.options).not.toHaveProperty('vibrancy')
|
||||
}
|
||||
expect(harness.hosts).toHaveLength(0)
|
||||
})
|
||||
|
||||
it.each([[false, '#f9fafb'], [true, '#1b1b1c']] as const)('paints the Windows workspace in its startup palette (dark=%s)', async (dark, color) => {
|
||||
vi.spyOn(process, 'platform', 'get').mockReturnValue('win32')
|
||||
harness.nativeTheme.shouldUseDarkColors = dark
|
||||
await import('../src/main.ts')
|
||||
await harness.preparing.promise
|
||||
expect(harness.windows[0]!.options).toMatchObject({ backgroundColor: color })
|
||||
})
|
||||
|
||||
describe.each(['win32', 'darwin'] as const)('Host theme on %s', (platform) => {
|
||||
it.each([
|
||||
['light', true, '#f9fafb'], ['dark', false, '#1b1b1c'],
|
||||
['system', true, '#1b1b1c'], ['system', false, '#f9fafb'],
|
||||
] as const)('applies %s with system dark=%s before revealing the workspace', async (source, systemDark, color) => {
|
||||
vi.spyOn(process, 'platform', 'get').mockReturnValue(platform)
|
||||
harness.nativeTheme.systemDark = systemDark
|
||||
await import('../src/main.ts')
|
||||
await harness.preparing.promise
|
||||
const window = harness.windows[0]!
|
||||
expect(window.show).not.toHaveBeenCalled()
|
||||
harness.prepared.resolve()
|
||||
await harness.hostStarted.promise
|
||||
const host = harness.hosts[0]!
|
||||
host.fetch.mockResolvedValue(Response.json({ hasApiKey: true, localePreference: null, themePreference: source }))
|
||||
window.show.mockImplementation(() => {
|
||||
expect(harness.nativeTheme.themeSource).toBe(source)
|
||||
if (platform === 'win32') expect(window.setBackgroundColor).toHaveBeenLastCalledWith(color)
|
||||
else expect(window.options).toMatchObject({ backgroundColor: '#00000000', vibrancy: 'sidebar' })
|
||||
})
|
||||
host.ready.resolve()
|
||||
await Promise.resolve(invoke(DESKTOP_IPC.boot))
|
||||
await vi.waitFor(() => { expect(window.show).toHaveBeenCalledOnce() })
|
||||
expect(host.fetch).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
|
||||
it('tracks effective appearance changes and removes the listener when closed', async () => {
|
||||
await import('../src/main.ts')
|
||||
await harness.preparing.promise
|
||||
const window = harness.windows[0]!
|
||||
for (const dark of [true, false]) {
|
||||
harness.nativeTheme.systemDark = dark
|
||||
harness.nativeTheme.emit('updated')
|
||||
expect(window.setBackgroundColor).toHaveBeenLastCalledWith(dark ? '#1b1b1c' : '#f9fafb')
|
||||
}
|
||||
window.emit('closed')
|
||||
expect(harness.nativeTheme.listenerCount('updated')).toBe(0)
|
||||
})
|
||||
|
||||
it('accepts only theme sources from the owned application main frame', async () => {
|
||||
await import('../src/main.ts')
|
||||
await harness.preparing.promise
|
||||
const window = harness.windows[0]!
|
||||
const listener = harness.ipcOn.mock.calls.find(([channel]) => channel === DESKTOP_IPC.nativeThemeSet)![1]
|
||||
const event = { sender: window.webContents, senderFrame: window.webContents.mainFrame }
|
||||
listener({ ...event, senderFrame: { url: 'dsh-app://app/' } }, 'dark')
|
||||
listener(event, 'invalid')
|
||||
expect(harness.nativeTheme.themeSource).toBe('system')
|
||||
listener(event, 'dark')
|
||||
expect(harness.nativeTheme.themeSource).toBe('dark')
|
||||
window.webContents.mainFrame.url = 'dsh-app://unowned/'
|
||||
listener(event, 'light')
|
||||
expect(harness.nativeTheme.themeSource).toBe('dark')
|
||||
})
|
||||
|
||||
it('serves packaged shell documents instead of rejecting the shell origin', async () => {
|
||||
const { serveWebDocument } = await import('../src/web-document.ts')
|
||||
vi.mocked(serveWebDocument).mockResolvedValue(new Response('shell document'))
|
||||
|
||||
@@ -32,13 +32,33 @@ const url = 'http://127.0.0.1:19387/'
|
||||
const cookie = 'dsh-auth-fixture=signed'
|
||||
|
||||
describe('desktop welcome Web operations', () => {
|
||||
it.each(['light', 'dark', 'system'] as const)('reads the live %s theme in the existing settings request', async (preference) => {
|
||||
const host = transport()
|
||||
host.namespaces.push({ ns: 'ui-theme', value: { preference } })
|
||||
const backend = connectDesktopWelcome(url, cookie, host.send)
|
||||
expect(await backend.read()).toMatchObject({ themePreference: preference })
|
||||
expect(host.send.mock.calls.filter(([input]) => input.endsWith('/api/settings/describe'))).toHaveLength(1)
|
||||
host.namespaces.find(item => item.ns === 'ui-theme')!.value.preference = 'system'
|
||||
expect(await backend.read()).toMatchObject({ themePreference: 'system' })
|
||||
})
|
||||
|
||||
it('falls back for an invalid optional theme without blocking startup metadata', async () => {
|
||||
const host = transport()
|
||||
host.namespaces.push({ ns: 'ui-theme', value: { preference: 'invalid' } })
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
try {
|
||||
expect(await connectDesktopWelcome(url, cookie, host.send).read()).toMatchObject({ themePreference: 'system' })
|
||||
expect(warn).toHaveBeenCalledOnce()
|
||||
} finally { warn.mockRestore() }
|
||||
})
|
||||
|
||||
it('authenticates with the explicit Host cookie and stores only through the configured credential reference', async () => {
|
||||
const host = transport()
|
||||
const backend = connectDesktopWelcome(url, cookie, host.send)
|
||||
expect(await backend.analyticsEnabled()).toBe(true)
|
||||
expect(await backend.save('sk-example')).toEqual({ ok: true })
|
||||
expect(host.keys.get('CUSTOM_DEEPSEEK_KEY')).toBe('sk-example')
|
||||
expect(await backend.read()).toEqual({ loggedIn: false, hasApiKey: true, writable: true, localePreference: null })
|
||||
expect(await backend.read()).toEqual({ loggedIn: false, hasApiKey: true, writable: true, localePreference: null, themePreference: 'system' })
|
||||
for (const [input, init] of host.send.mock.calls) {
|
||||
expect(input).toMatch(/^http:\/\/127\.0\.0\.1:19387\/api\//u)
|
||||
expect(init).toMatchObject({ redirect: 'error', headers: { cookie } })
|
||||
|
||||
@@ -33,6 +33,7 @@ const state = vi.hoisted(() => ({
|
||||
closeWelcome: vi.fn(),
|
||||
welcomeLocale: undefined as DesktopLocale | undefined,
|
||||
preference: 'zh',
|
||||
themePreference: 'system',
|
||||
hasApiKey: false,
|
||||
handlers: new Map<string, (...args: unknown[]) => unknown>(),
|
||||
listeners: new Map<string, (...args: unknown[]) => void>(),
|
||||
@@ -40,7 +41,7 @@ const state = vi.hoisted(() => ({
|
||||
windowOptions: undefined as BrowserWindowConstructorOptions | undefined,
|
||||
menu: vi.fn(),
|
||||
operations: undefined as WelcomeOperations | undefined,
|
||||
nativeTheme: { themeSource: 'system', shouldUseDarkColors: false },
|
||||
nativeTheme: { themeSource: 'system', shouldUseDarkColors: false, on: vi.fn(), removeListener: vi.fn() },
|
||||
}))
|
||||
|
||||
vi.mock('../src/crash-report.ts', async importOriginal => ({
|
||||
@@ -82,6 +83,7 @@ vi.mock('electron', () => ({
|
||||
focus = state.focusWorkspace
|
||||
moveTop = state.moveTopWorkspace
|
||||
hide = vi.fn()
|
||||
setBackgroundColor = vi.fn()
|
||||
show = state.showWorkspace
|
||||
showInactive = state.showInactiveWorkspace
|
||||
async loadURL(url: string) { state.contents = this.webContents; await state.loadWorkspace(url); this.ready?.() }
|
||||
@@ -120,6 +122,7 @@ vi.mock('../src/host-process.ts', () => ({
|
||||
fetch() {
|
||||
return Promise.resolve(Response.json({
|
||||
loggedIn: false, hasApiKey: state.hasApiKey, writable: true, localePreference: state.preference,
|
||||
themePreference: state.themePreference,
|
||||
}))
|
||||
}
|
||||
},
|
||||
@@ -130,7 +133,8 @@ vi.mock('../src/welcome-backend.ts', () => ({
|
||||
readLocalePreference: async () => state.preference,
|
||||
read: async () => {
|
||||
await state.beforeRead()
|
||||
return { loggedIn: false, hasApiKey: state.hasApiKey, writable: true, localePreference: state.preference }
|
||||
return { loggedIn: false, hasApiKey: state.hasApiKey, writable: true,
|
||||
localePreference: state.preference, themePreference: state.themePreference }
|
||||
},
|
||||
save: async () => ({ ok: true }),
|
||||
account: {
|
||||
@@ -178,6 +182,7 @@ it.each([false, true])('starts welcome onboarding without carrying update focus
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
state.preference = 'zh'
|
||||
state.themePreference = updated ? 'dark' : 'light'
|
||||
state.hasApiKey = false
|
||||
state.operations = undefined
|
||||
state.accountState.mockResolvedValue({ status: 'signed-out', attempt: null, links: { usageUrl: '', topUpUrl: '' } })
|
||||
@@ -196,7 +201,10 @@ it.each([false, true])('starts welcome onboarding without carrying update focus
|
||||
const reading = Promise.withResolvers<undefined>()
|
||||
const loading = Promise.withResolvers<undefined>()
|
||||
state.beforeRead.mockReturnValueOnce(reading.promise)
|
||||
state.beforeWelcome.mockReturnValueOnce(loading.promise)
|
||||
state.beforeWelcome.mockImplementationOnce(async () => {
|
||||
expect(state.nativeTheme.themeSource).toBe(state.themePreference)
|
||||
await loading.promise
|
||||
})
|
||||
const activate = () => {
|
||||
state.appListeners.get('second-instance')!()
|
||||
state.appListeners.get('open-url')!({ preventDefault: vi.fn() }, 'dsh://open')
|
||||
|
||||
@@ -6,17 +6,17 @@
|
||||
en: 531349889ccc3124
|
||||
zh: 08769a2d9fc586fd
|
||||
/plugin-market-index-generator/usage:
|
||||
en: ed2a83d06f6789ef
|
||||
zh: e5c1e26486dfefb0
|
||||
en: b354d10e2463e99e
|
||||
zh: 1f83a96f164864fc
|
||||
/plugin-market-index-generator/what-gets-listed:
|
||||
en: 4a01101246724006
|
||||
zh: 8455f6bc4552b787
|
||||
en: bcf7ee834276a804
|
||||
zh: d6723321c0baeef7
|
||||
/plugin-market-index-generator/caching-and-repeated-runs:
|
||||
en: a62c9bf12310c33c
|
||||
zh: 60aa01288f46133c
|
||||
en: 02a87e53b76a017c
|
||||
zh: 951a71238a073c44
|
||||
/plugin-market-index-generator/security:
|
||||
en: de5821128b676a61
|
||||
zh: aad295eeb80cad7e
|
||||
/plugin-market-index-generator/limits:
|
||||
en: b5e02017e961e818
|
||||
zh: 0f0aaa26d519acb1
|
||||
en: 08a91f2f2fdd71a6
|
||||
zh: 0641a53608eb342a
|
||||
@@ -39,7 +39,9 @@ pnpm --filter @deepseek-ai/dsh-plugin-market-generator start \
|
||||
| `--timeout-ms <n>` | `60000` | Deadline for one request or Git command. |
|
||||
| `-h`, `--help` | | Print these options and the exit statuses. |
|
||||
|
||||
The exit status is 0 when every source was read, 1 when a source could not be read, and 2 for invalid arguments or an invalid list. A list line that the index format would reject, such as an `http:` URL, a URL with credentials, a query, or a fragment, a URL with fewer than two path segments, or a tarball URL, is an invalid list. A source that cannot be read keeps its last successful result, so a temporary outage does not remove plugins from the index; the failure is still reported.
|
||||
Set `GITHUB_TOKEN` to collect `github_stars`. Surrounding whitespace is trimmed; an unset or blank token skips GitHub metric requests and omits that metric. The token is used only in the GitHub API Authorization header and is not written to the index, cache, or diagnostic messages. npm weekly downloads do not require it.
|
||||
|
||||
The exit status is 0 when every source was read, 1 when a source could not be read, and 2 for invalid arguments or an invalid list. A list line that the index format would reject, such as an `http:` URL, a URL with credentials, a query, or a fragment, a URL with fewer than two path segments, or a tarball URL, is an invalid list. A source that cannot be read keeps its last successful result, so a temporary outage does not remove plugins from the index; the failure is still reported. Metric failures warn and omit the unavailable values, including counts present in an earlier output. They leave exit status 0 only when every source was read successfully; offline source fallback still returns 1.
|
||||
|
||||
## What gets listed
|
||||
|
||||
@@ -59,11 +61,13 @@ Display fields come from the highest listed version and follow the rules DSH use
|
||||
|
||||
`metadata.keywords` is an optional array of strings for discovery. npm `package.json` `keywords` is the convention; index generators may supplement or adjust it. This reference generator copies the highest listed version's manifest `keywords` unchanged, including empty strings, duplicates, and an empty array. An absent field is omitted; a non-array value or an array containing any non-string produces a warning and omits the whole field without dropping the package.
|
||||
|
||||
The generator reads optional `github_stars` from the GitHub repository API and `npm_weekly_downloads` from npm's `downloads/point/last-week` endpoint. GitHub counts use the actual Git source repository for Git packages and the author-declared manifest repository for npm packages. npm repository ownership and source correspondence are not verified, so stars are not a trust or quality signal. npm counts are requested only for public npm sources, never custom registries or Git-only packages. Weekly means the window returned by npm's `last-week`, not an upstream 30-day total. Available counts, including zero, appear in plugin `metadata.sortKeys`; root `metadata.sortKeys` declares only metrics available for at least one plugin, with descending initial order, and root `metadata` is omitted when no metric is available. Fetch failures and invalid counts produce warnings and omit the metric without dropping the plugin or failing the run.
|
||||
|
||||
When several sources produce the same package name, the first one in the list wins.
|
||||
|
||||
## Caching and repeated runs
|
||||
|
||||
Each run reads the registry document of every npm source and the refs of every Git repository. Results are cached per immutable input: an npm plugin per registry, icon threshold, package, and set of listed versions; a Git repository per default branch commit and release tags; and each Git package per commit and directory. A Git run therefore fetches only the commits whose packages are not cached. Cache files are written atomically, and a cache file that is damaged or that the index format rejects is rebuilt. Output is deterministic: entries follow the list order, and the index and icon files are rewritten only when their contents change, so running the generator again without upstream changes leaves the output untouched.
|
||||
Each run reads the registry document of every npm source and the refs of every Git repository. Results are cached per immutable input: an npm plugin per registry, icon threshold, package, and set of listed versions; a Git repository per default branch commit and release tags; and each Git package per commit and directory. A Git run therefore fetches only the commits whose packages are not cached. Cache files are written atomically, and a cache file that is damaged or that the index format rejects is rebuilt. Enabled metrics are refreshed on every run outside these caches, with shared requests per repository or package and the configured concurrency and timeout limits. Metric failures never reuse stale counts. For unchanged package data and metric responses, output is deterministic: entries follow the list order, and the index and icon files are rewritten only when their contents change, so running the generator again without upstream changes leaves the output untouched.
|
||||
|
||||
## Security
|
||||
|
||||
@@ -73,6 +77,8 @@ Index content is what the packages claim about themselves. Review the list you p
|
||||
|
||||
## Limits
|
||||
|
||||
- GitHub token permissions and API rate limits apply; refused or rate-limited requests warn and omit the affected counts.
|
||||
- Cached package data does not make a run offline: source documents/refs and enabled metrics are requested on each run. Unreachable metric APIs remove those counts from the output, without reusing stale values; source failures follow the exit-status rules above.
|
||||
- A Git server must allow fetching a commit by its SHA, as GitHub, GitLab, and Gitea do. When a server refuses, the generator fetches the tags and the default branch by name, which fails if a ref moved after it was listed.
|
||||
- Registry tokens from `.npmrc` are not applied; only public or unauthenticated registries work.
|
||||
- Git never prompts for credentials. A repository that needs them must be readable through a credential helper or an ssh agent.
|
||||
@@ -39,7 +39,9 @@ pnpm --filter @deepseek-ai/dsh-plugin-market-generator start \
|
||||
| `--timeout-ms <n>` | `60000` | 单个请求或 Git 命令的时限。 |
|
||||
| `-h`、`--help` | | 打印这些选项和退出状态。 |
|
||||
|
||||
所有来源都读取成功时退出状态为 0,有来源无法读取时为 1,参数无效或列表无效时为 2。索引格式会拒绝的列表行属于无效列表,例如 `http:` URL、带凭据、查询或片段的 URL、路径段少于两个的 URL,以及 tarball URL。无法读取的来源保留其上一次成功的结果,因此临时故障不会把插件从索引中移除;生成器仍会报告该失败。
|
||||
设置 `GITHUB_TOKEN` 以采集 `github_stars`。生成器会去掉令牌首尾的空白;令牌未设置或为空白时,会跳过 GitHub 指标请求并省略该指标。令牌仅用于 GitHub API 的 Authorization 请求头,不会写入索引、缓存或诊断消息。npm 周下载量不需要该令牌。
|
||||
|
||||
所有来源都读取成功时退出状态为 0,有来源无法读取时为 1,参数无效或列表无效时为 2。索引格式会拒绝的列表行属于无效列表,例如 `http:` URL、带凭据、查询或片段的 URL、路径段少于两个的 URL,以及 tarball URL。无法读取的来源保留其上一次成功的结果,因此临时故障不会把插件从索引中移除;生成器仍会报告该失败。指标获取失败时会发出警告并省略不可用的值,包括此前输出中已有的计数。只有所有来源都读取成功时,指标获取失败才仍返回退出状态 0;离线时回退到缓存的来源结果仍返回 1。
|
||||
|
||||
## 列出的内容
|
||||
|
||||
@@ -59,11 +61,13 @@ pnpm --filter @deepseek-ai/dsh-plugin-market-generator start \
|
||||
|
||||
`metadata.keywords` 是用于发现插件的可选字符串数组。按约定使用 npm `package.json` 的 `keywords`;索引生成器可以补充或调整它。本参考生成器原样复制最高列出版本 manifest 的 `keywords`,包括空字符串、重复值和空数组。字段缺省时省略;如果值不是数组,或数组包含任何非字符串成员,则发出警告并省略整个字段,但保留该包。
|
||||
|
||||
生成器从 GitHub 仓库 API 读取可选的 `github_stars`,从 npm 的 `downloads/point/last-week` 端点读取可选的 `npm_weekly_downloads`。Git 包的 GitHub 计数取自实际 Git 来源仓库,npm 包则取自作者在 manifest 中声明的仓库。生成器不验证 npm 包声明的仓库归属或与包源码的对应关系,因此 star 数不代表可信度或质量。npm 计数仅对公共 npm 来源发起请求,不对自定义注册表或仅有 Git 来源的包发起请求。周下载量指 npm 的 `last-week` 返回的时间窗口,不是上游的 30 天总量。可用的计数(包括零)写入插件的 `metadata.sortKeys`;根级 `metadata.sortKeys` 仅声明至少有一个插件提供值的指标,初始顺序为降序;没有可用指标时,省略根级 `metadata`。获取失败和无效计数会产生警告并省略该指标,不会丢弃插件或使运行失败。
|
||||
|
||||
多个来源产生相同包名时,列表中靠前的来源生效。
|
||||
|
||||
## 缓存与重复运行
|
||||
|
||||
每次运行时,生成器读取每个 npm 来源的注册表文档和每个 Git 仓库的引用(ref)。结果按不可变的输入缓存:npm 插件按注册表、图标阈值、包和列出版本的集合缓存;Git 仓库按默认分支提交和发布标签缓存;每个 Git 包按提交和目录缓存。因此 Git 运行只拉取其包尚未缓存的提交。生成器以原子方式写入缓存文件,并重建损坏的或索引格式会拒绝的缓存文件。输出是确定性的:条目按列表顺序排列,索引文件和图标文件只在内容变化时才重写,因此上游没有变化时再次运行生成器不会改动输出。
|
||||
每次运行时,生成器读取每个 npm 来源的注册表文档和每个 Git 仓库的引用(ref)。结果按不可变的输入缓存:npm 插件按注册表、图标阈值、包和列出版本的集合缓存;Git 仓库按默认分支提交和发布标签缓存;每个 Git 包按提交和目录缓存。因此 Git 运行只拉取其包尚未缓存的提交。生成器以原子方式写入缓存文件,并重建损坏的或索引格式会拒绝的缓存文件。已启用的指标在这些缓存之外于每次运行时刷新,同一仓库或包共享请求,并遵循配置的并发和超时限制。指标获取失败时绝不复用陈旧计数。包数据和指标响应不变时,输出是确定性的:条目按列表顺序排列,索引文件和图标文件只在内容变化时才重写,因此上游没有变化时再次运行生成器不会改动输出。
|
||||
|
||||
## 安全
|
||||
|
||||
@@ -73,6 +77,8 @@ pnpm --filter @deepseek-ai/dsh-plugin-market-generator start \
|
||||
|
||||
## 限制
|
||||
|
||||
- 请求受 GitHub 令牌权限和 API 速率限制约束;请求被拒绝或受到速率限制时,会发出警告并省略受影响的计数。
|
||||
- 已缓存包数据不表示运行可以离线完成:每次运行仍会请求来源文档或引用以及已启用的指标。指标 API 无法访问时,输出会移除这些计数,不复用陈旧值;来源获取失败遵循上述退出状态规则。
|
||||
- Git 服务器必须允许按 SHA 拉取提交,GitHub、GitLab 和 Gitea 都允许。服务器拒绝时,生成器按名称拉取标签和默认分支;如果某个引用在列出之后移动了,这一步会失败。
|
||||
- 生成器不使用 `.npmrc` 中的注册表令牌;只支持公开或无需认证的注册表。
|
||||
- Git 从不提示输入凭据。需要凭据的仓库必须能通过凭据助手或 ssh agent 读取。
|
||||
@@ -15,10 +15,9 @@ import { MAX_INLINE_ICON_BYTES } from '@deepseek-ai/dsh-plugin-market-index'
|
||||
import { writeIfChanged } from './files.ts'
|
||||
import { generate } from './generate.ts'
|
||||
import { GENERATED_ICON } from './metadata.ts'
|
||||
import { PUBLIC_NPM_REGISTRY } from './fetchers.ts'
|
||||
import { parseSources } from './sources.ts'
|
||||
|
||||
const PUBLIC_REGISTRY = 'https://registry.npmjs.org/'
|
||||
|
||||
/** Parsed command-line options. */
|
||||
interface Options {
|
||||
readonly input: string
|
||||
@@ -51,7 +50,7 @@ function program(): Command {
|
||||
.requiredOption('--title <text>', 'market title')
|
||||
.option('--description <text>', 'market description')
|
||||
.option('--cache <dir>', 'cache directory (default: .plugin-market-cache beside the input list)')
|
||||
.option('--registry <url>', 'npm registry', PUBLIC_REGISTRY)
|
||||
.option('--registry <url>', 'npm registry', PUBLIC_NPM_REGISTRY)
|
||||
.addOption(new Option('--inline-icon-bytes <n>', `largest icon embedded as a data URL, at most ${String(MAX_INLINE_ICON_BYTES)}`)
|
||||
.argParser(integer(MAX_INLINE_ICON_BYTES)).default(16384))
|
||||
.addOption(new Option('--concurrency <n>', 'sources read at once').argParser(integer()).default(4))
|
||||
@@ -68,11 +67,13 @@ async function run(options: Options): Promise<number> {
|
||||
const input = resolve(base, options.input)
|
||||
const output = resolve(base, options.output)
|
||||
const registry = new URL(options.registry).href.replace(/\/?$/u, '/')
|
||||
const githubToken = process.env.GITHUB_TOKEN?.trim() || undefined
|
||||
const result = await generate({
|
||||
sources: parseSources(readFileSync(input, 'utf8')),
|
||||
cacheDir: options.cache === undefined ? join(dirname(input), '.plugin-market-cache') : resolve(base, options.cache),
|
||||
registry,
|
||||
recordRegistry: registry !== PUBLIC_REGISTRY,
|
||||
recordRegistry: registry !== PUBLIC_NPM_REGISTRY,
|
||||
...githubToken === undefined ? {} : { githubToken },
|
||||
title: options.title,
|
||||
...options.description === undefined ? {} : { description: options.description },
|
||||
inlineIconBytes: options.inlineIconBytes,
|
||||
|
||||
@@ -16,6 +16,9 @@ import { readManifest, type Manifest } from './packages.ts'
|
||||
|
||||
const run = promisify(execFile)
|
||||
|
||||
/** Public npm registry whose package identity permits npm download statistics. */
|
||||
export const PUBLIC_NPM_REGISTRY = 'https://registry.npmjs.org/'
|
||||
|
||||
/** Network limits for one source. */
|
||||
export interface FetchLimits {
|
||||
readonly timeoutMs: number
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
/** Turn a source list into a validated format-1 index, reusing cached results for unchanged releases and commits. */
|
||||
import { existsSync } from 'node:fs'
|
||||
import { join, posix } from 'node:path'
|
||||
import { parseMarketIndex } from '@deepseek-ai/dsh-plugin-market-index'
|
||||
import { parseMarketIndex, type MarketSortKeyDeclaration } from '@deepseek-ai/dsh-plugin-market-index'
|
||||
import semver from 'semver'
|
||||
import { cacheKey, gitRefs, npmPackument, npmTarball, withGitObjects, type FetchLimits, type GitObjects, type GitTransports } from './fetchers.ts'
|
||||
import { readJsonFile, writeIfChanged, type PackageFiles } from './files.ts'
|
||||
import { GENERATED_ICON, readDisplayMetadata, type Display, type IconFile } from './metadata.ts'
|
||||
import { dshRange, findBundlePackages, isBundle, isPrivate, readManifest, type Manifest } from './packages.ts'
|
||||
import { METRIC_DECLARATIONS, metricReader } from './metrics.ts'
|
||||
import type { Source } from './sources.ts'
|
||||
|
||||
/** One generated version, in the field order of the format. */
|
||||
@@ -18,8 +19,10 @@ export interface GeneratedVersion {
|
||||
}
|
||||
|
||||
/** One generated plugin, in the field order of the format. */
|
||||
export interface GeneratedEntry extends Display {
|
||||
export interface GeneratedEntry extends Omit<Display, 'metadata'> {
|
||||
readonly name: string
|
||||
/** Manifest keywords and optional statistics added after reading source caches. */
|
||||
readonly metadata?: { readonly keywords?: readonly string[]; readonly sortKeys?: Readonly<Record<string, number>> }
|
||||
readonly versions: readonly GeneratedVersion[]
|
||||
}
|
||||
|
||||
@@ -31,6 +34,8 @@ export interface GenerateOptions {
|
||||
readonly registry: string
|
||||
/** Whether versions name `registry`; false for the public registry, which clients use by default. */
|
||||
readonly recordRegistry: boolean
|
||||
/** Nonblank token used only for GitHub API metrics; absent disables GitHub star collection. */
|
||||
readonly githubToken?: string
|
||||
readonly title: string
|
||||
readonly description?: string
|
||||
readonly inlineIconBytes: number
|
||||
@@ -43,7 +48,13 @@ export interface GenerateOptions {
|
||||
|
||||
/** A finished run: the index document, the icon files it refers to, and the sources that could not be read. */
|
||||
export interface GenerateResult {
|
||||
readonly index: { readonly format: 1; readonly title: string; readonly description?: string; readonly plugins: readonly GeneratedEntry[] }
|
||||
readonly index: {
|
||||
readonly format: 1
|
||||
readonly title: string
|
||||
readonly description?: string
|
||||
readonly metadata?: { readonly sortKeys: Readonly<Record<string, MarketSortKeyDeclaration>> }
|
||||
readonly plugins: readonly GeneratedEntry[]
|
||||
}
|
||||
/** Icon files by index-relative path; contents live under the cache. */
|
||||
readonly icons: ReadonlyMap<string, string>
|
||||
readonly failures: readonly string[]
|
||||
@@ -350,11 +361,16 @@ export async function generate(options: GenerateOptions): Promise<GenerateResult
|
||||
plugins.push(entry)
|
||||
if (entry.icon?.startsWith('icons/') === true) icons.set(entry.icon, join(options.cacheDir, entry.icon))
|
||||
}
|
||||
const measured = await mapLimited(plugins, options.concurrency,
|
||||
metricReader(options.limits, options.registry, options.warn, options.githubToken))
|
||||
const sortKeys = Object.fromEntries(Object.entries(METRIC_DECLARATIONS)
|
||||
.filter(([id]) => measured.some(entry => Object.hasOwn(entry.metadata?.sortKeys ?? {}, id))))
|
||||
const index = {
|
||||
format: 1 as const,
|
||||
title: options.title,
|
||||
...options.description === undefined ? {} : { description: options.description },
|
||||
plugins,
|
||||
...Object.keys(sortKeys).length === 0 ? {} : { metadata: { sortKeys } },
|
||||
plugins: measured,
|
||||
}
|
||||
parseMarketIndex(index, VALIDATION_URL)
|
||||
return { index, icons, failures: failed.filter(message => message !== undefined) }
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/** Fetch optional popularity counts separately from immutable package metadata. */
|
||||
import type { MarketSortKeyDeclaration } from '@deepseek-ai/dsh-plugin-market-index'
|
||||
import { PUBLIC_NPM_REGISTRY, type FetchLimits } from './fetchers.ts'
|
||||
import type { GeneratedEntry } from './generate.ts'
|
||||
|
||||
/** Declarations for the standardized metric definitions. */
|
||||
export const METRIC_DECLARATIONS = {
|
||||
github_stars: { title: { en: 'GitHub stars', zh: 'GitHub 星标数' }, order: 'desc' },
|
||||
npm_weekly_downloads: { title: { en: 'npm weekly downloads', zh: 'npm 周下载量' }, order: 'desc' },
|
||||
} satisfies Record<string, MarketSortKeyDeclaration>
|
||||
|
||||
/**
|
||||
* Create a per-run reader that shares requests for the same repository or package.
|
||||
* @param limits - deadline for each metric request.
|
||||
* @param registry - the registry used to discover npm packages; only public npm supplies downloads.
|
||||
* @param warn - reports unavailable metrics without failing package discovery.
|
||||
* @param githubToken - nonblank GitHub API token; absent skips GitHub star requests.
|
||||
* @returns an entry reader; failed or invalid counts are omitted, including on repeated runs.
|
||||
*/
|
||||
export function metricReader(
|
||||
limits: FetchLimits, registry: string, warn: (message: string) => void, githubToken?: string,
|
||||
): (entry: GeneratedEntry) => Promise<GeneratedEntry> {
|
||||
const requests = new Map<string, Promise<number | undefined>>()
|
||||
async function count(url: string, field: string): Promise<number | undefined> {
|
||||
const authenticated = githubToken !== undefined && new URL(url).origin === 'https://api.github.com'
|
||||
let response: Response | undefined
|
||||
let value: unknown
|
||||
try {
|
||||
response = await fetch(url, {
|
||||
signal: AbortSignal.timeout(limits.timeoutMs),
|
||||
...authenticated ? { headers: { Authorization: `Bearer ${githubToken}` } } : {},
|
||||
})
|
||||
if (!response.ok) {
|
||||
await response.body?.cancel()
|
||||
warn(`${url}: HTTP ${response.status}; metric omitted`)
|
||||
return undefined
|
||||
}
|
||||
value = await response.json()
|
||||
} catch (error) {
|
||||
const reason = authenticated ? 'GitHub metric request failed' : String(error)
|
||||
warn(`${url}: ${response === undefined ? '' : `HTTP ${response.status}; `}${reason}; metric omitted`)
|
||||
return undefined
|
||||
}
|
||||
const result = typeof value === 'object' && value !== null && field in value ? (value as Record<string, unknown>)[field] : undefined
|
||||
if (typeof result === 'number' && Number.isSafeInteger(result) && result >= 0) return result
|
||||
warn(`${url}: ${field} must be a non-negative safe integer; metric omitted`)
|
||||
return undefined
|
||||
}
|
||||
function shared(url: string, field: string): Promise<number | undefined> {
|
||||
let request = requests.get(url)
|
||||
if (request === undefined) {
|
||||
request = count(url, field)
|
||||
requests.set(url, request)
|
||||
}
|
||||
return request
|
||||
}
|
||||
return async (entry) => {
|
||||
const sortKeys: Record<string, number> = {}
|
||||
const source = entry.versions[0]?.source
|
||||
const repositoryUrl = source?.type === 'git' ? source.url : entry.repository?.url
|
||||
const repository = repositoryUrl === undefined ? undefined : new URL(repositoryUrl)
|
||||
const match = repository?.hostname === 'github.com' ? /^\/([^/]+)\/([^/]+?)\/?$/u.exec(repository.pathname) : null
|
||||
if (githubToken !== undefined && match !== null) {
|
||||
const stars = await shared(`https://api.github.com/repos/${match[1]}/${match[2]?.replace(/\.git$/u, '')}`, 'stargazers_count')
|
||||
if (stars !== undefined) sortKeys.github_stars = stars
|
||||
}
|
||||
// Private registries can publish unrelated packages under a public npm name.
|
||||
if (registry === PUBLIC_NPM_REGISTRY && entry.versions.some(({ source }) => source.type === 'npm')) {
|
||||
const downloads = await shared(`https://api.npmjs.org/downloads/point/last-week/${encodeURIComponent(entry.name)}`, 'downloads')
|
||||
if (downloads !== undefined) sortKeys.npm_weekly_downloads = downloads
|
||||
}
|
||||
return Object.keys(sortKeys).length === 0 ? entry : { ...entry, metadata: { ...entry.metadata, sortKeys } }
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,16 @@
|
||||
/** Command-line parsing and help of the generator. */
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
import { main } from '../src/cli.ts'
|
||||
import { generate } from '../src/generate.ts'
|
||||
import { scratch } from './support.ts'
|
||||
|
||||
afterEach(() => { vi.restoreAllMocks() })
|
||||
vi.mock('../src/generate.ts', () => ({
|
||||
generate: vi.fn(async () => ({ index: { format: 1, title: 'T', plugins: [] }, icons: new Map(), failures: [] })),
|
||||
}))
|
||||
|
||||
afterEach(() => { vi.restoreAllMocks(); vi.unstubAllEnvs() })
|
||||
|
||||
/** Run the CLI and collect what it writes to stdout and stderr. */
|
||||
async function cli(...argv: string[]) {
|
||||
@@ -16,6 +24,24 @@ async function cli(...argv: string[]) {
|
||||
|
||||
const REQUIRED = ['--input', '/nonexistent/list.txt', '--output', '/nonexistent/index.json', '--title', 'T']
|
||||
|
||||
it.each([
|
||||
[undefined, undefined], ['', undefined], [' \t ', undefined], [' test-only-cli-token ', 'test-only-cli-token'],
|
||||
])('resolves the GitHub token from the environment (%#)', async (authored, expected) => {
|
||||
vi.stubEnv('GITHUB_TOKEN', authored)
|
||||
vi.stubEnv('GH_TOKEN', 'ignored-test-token')
|
||||
vi.mocked(generate).mockClear()
|
||||
const dir = scratch()
|
||||
const input = join(dir, 'plugins.txt')
|
||||
const output = join(dir, 'index.json')
|
||||
writeFileSync(input, 'dsh-example\n')
|
||||
expect((await cli('--input', input, '--output', output, '--title', 'T')).status).toBe(0)
|
||||
expect(generate).toHaveBeenCalledOnce()
|
||||
const options = vi.mocked(generate).mock.calls[0]?.[0]
|
||||
expect(options?.githubToken).toBe(expected)
|
||||
if (expected === undefined) expect(options).not.toHaveProperty('githubToken')
|
||||
expect(readFileSync(output, 'utf8')).not.toContain('test-only-cli-token')
|
||||
})
|
||||
|
||||
it('prints help with every option, its default, and the exit statuses', async () => {
|
||||
const { status, out } = await cli('--help')
|
||||
expect(status).toBe(0)
|
||||
|
||||
@@ -3,11 +3,17 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from
|
||||
import { createServer } from 'node:http'
|
||||
import { join } from 'node:path'
|
||||
import { c as createTar } from 'tar'
|
||||
import { expect, it } from 'vitest'
|
||||
import { expect, it, vi } from 'vitest'
|
||||
import { main } from '../src/cli.ts'
|
||||
import { generate, type GenerateOptions } from '../src/generate.ts'
|
||||
import { metricReader } from '../src/metrics.ts'
|
||||
import { onCleanup, scratch } from './support.ts'
|
||||
|
||||
vi.mock('../src/metrics.ts', async original => ({
|
||||
...await original<typeof import('../src/metrics.ts')>(),
|
||||
metricReader: vi.fn(() => async (entry: import('../src/generate.ts').GeneratedEntry) => entry),
|
||||
}))
|
||||
|
||||
/** Pack a package directory the way npm does: every file under `package/`. */
|
||||
async function pack(files: Record<string, string | Uint8Array>): Promise<Buffer> {
|
||||
const dir = scratch()
|
||||
@@ -259,3 +265,57 @@ it('writes the index and icons once, keeps the cache beside the input, and remov
|
||||
writeFileSync(list, 'http://example.com/o/r\n')
|
||||
expect(await main(['--input', list, '--output', other, '--title', 'T'])).toBe(2)
|
||||
})
|
||||
|
||||
it('refreshes optional metrics outside source caches and keeps plugins on metric outages', async () => {
|
||||
const server = await registry({ '@scope/dsh-a': { '1.0.0': {
|
||||
time: '2026-06-01T00:00:00.000Z', manifest: BUNDLE,
|
||||
tarball: await pack({ 'package.json': bundleManifest('@scope/dsh-a', '1.0.0', { repository: 'github:team/plugins', keywords: ['tools'] }) }),
|
||||
} } })
|
||||
const actual = await vi.importActual<typeof import('../src/metrics.ts')>('../src/metrics.ts')
|
||||
const network = globalThis.fetch
|
||||
let unavailable = false
|
||||
const metricUrls: string[] = []
|
||||
const authorization: [string, string | null][] = []
|
||||
const fetch = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
|
||||
const url = input instanceof Request ? input.url : input.toString()
|
||||
authorization.push([url, new Headers(init?.headers).get('authorization')])
|
||||
if (url.startsWith('https://registry.npmjs.org/')) return network(url.replace('https://registry.npmjs.org/', server.url), init)
|
||||
if (!url.startsWith('https://api.')) return network(input, init)
|
||||
metricUrls.push(url)
|
||||
if (unavailable) return new Response('', { status: 503 })
|
||||
return Response.json(url.includes('github.com') ? { stargazers_count: 0 } : { downloads: 42 })
|
||||
})
|
||||
onCleanup(() => { fetch.mockRestore() })
|
||||
const warnings: string[] = []
|
||||
const token = 'test-only-integration-token'
|
||||
const opts = { ...options(server.url, scratch(), ['@scope/dsh-a'], warnings), registry: 'https://registry.npmjs.org/', recordRegistry: false, githubToken: token }
|
||||
vi.mocked(metricReader).mockImplementationOnce(actual.metricReader)
|
||||
const first = await generate(opts)
|
||||
expect(first.failures).toEqual([])
|
||||
expect(first.index.metadata?.sortKeys).toEqual(actual.METRIC_DECLARATIONS)
|
||||
expect(first.index.plugins[0]?.metadata).toEqual({ keywords: ['tools'], sortKeys: { github_stars: 0, npm_weekly_downloads: 42 } })
|
||||
expect(metricUrls).toContain('https://api.npmjs.org/downloads/point/last-week/%40scope%2Fdsh-a')
|
||||
vi.mocked(metricReader).mockImplementationOnce(actual.metricReader)
|
||||
const { githubToken: _token, ...withoutToken } = opts
|
||||
const disabled = await generate(withoutToken)
|
||||
expect(disabled.index.plugins[0]?.metadata).toEqual({ keywords: ['tools'], sortKeys: { npm_weekly_downloads: 42 } })
|
||||
expect(disabled.index.metadata?.sortKeys).toEqual({ npm_weekly_downloads: actual.METRIC_DECLARATIONS.npm_weekly_downloads })
|
||||
expect(metricUrls.filter(url => url.startsWith('https://api.github.com/'))).toHaveLength(1)
|
||||
unavailable = true
|
||||
vi.mocked(metricReader).mockImplementationOnce(actual.metricReader)
|
||||
const second = await generate(opts)
|
||||
expect(second.failures).toEqual([])
|
||||
expect(second.index.plugins[0]?.metadata).toEqual({ keywords: ['tools'] })
|
||||
expect(second.index).not.toHaveProperty('metadata')
|
||||
expect(server.tarballs).toHaveLength(1)
|
||||
expect(warnings.filter(message => message.includes('metric omitted'))).toHaveLength(2)
|
||||
for (const [url, header] of authorization) {
|
||||
expect(header).toBe(url.startsWith('https://api.github.com/') ? `Bearer ${token}` : null)
|
||||
}
|
||||
expect(JSON.stringify([first, disabled, second, warnings])).not.toContain(token)
|
||||
for (const directory of ['last', 'results']) {
|
||||
for (const name of readdirSync(join(opts.cacheDir, directory))) {
|
||||
expect(readFileSync(join(opts.cacheDir, directory, name), 'utf8')).not.toContain(token)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -2,10 +2,15 @@ import { execFileSync } from 'node:child_process'
|
||||
import { existsSync, mkdirSync, readdirSync, symlinkSync, writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { expect, it } from 'vitest'
|
||||
import { expect, it, vi } from 'vitest'
|
||||
import { generate, type GenerateOptions } from '../src/generate.ts'
|
||||
import { scratch, setEnv } from './support.ts'
|
||||
|
||||
vi.mock('../src/metrics.ts', async original => ({
|
||||
...await original<typeof import('../src/metrics.ts')>(),
|
||||
metricReader: () => async (entry: import('../src/generate.ts').GeneratedEntry) => entry,
|
||||
}))
|
||||
|
||||
const REPOSITORY = 'https://git.example/team/plugins'
|
||||
|
||||
/** Git subprocesses run under CI load, so each test gets more than the default deadline. */
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
import type { GeneratedEntry } from '../src/generate.ts'
|
||||
import { metricReader } from '../src/metrics.ts'
|
||||
|
||||
const limits = { timeoutMs: 10, maxTarballBytes: 1024 }
|
||||
const TOKEN = 'test-only-github-token'
|
||||
const plugin = (name = '@team/dsh-test', source: Record<string, string> = { type: 'npm' }): GeneratedEntry => ({
|
||||
name, repository: { url: 'https://github.com/team/plugins' },
|
||||
versions: [{ version: '1.0.0', publishTimestamp: 0, source }],
|
||||
})
|
||||
|
||||
afterEach(() => { vi.unstubAllGlobals() })
|
||||
|
||||
it('shares repository requests across packages and retains true zero counts', async () => {
|
||||
const fetch = vi.fn().mockResolvedValue(Response.json({ stargazers_count: 0 }))
|
||||
vi.stubGlobal('fetch', fetch)
|
||||
const read = metricReader(limits, 'https://registry.npmjs.org/', vi.fn(), TOKEN)
|
||||
const entries = await Promise.all(['a', 'b'].map(name => read(plugin(name, { type: 'git', url: 'https://github.com/team/plugins' }))))
|
||||
expect(entries.map(entry => entry.metadata?.sortKeys)).toEqual([{ github_stars: 0 }, { github_stars: 0 }])
|
||||
expect(fetch).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it.each([null, {}, { downloads: null }, { downloads: -1 }, { downloads: 1.2 }, { downloads: '3' }, { downloads: 1e20 }])(
|
||||
'omits invalid count response %j while keeping an independently available metric', async (body) => {
|
||||
vi.stubGlobal('fetch', vi.fn(async (url: string) => Response.json(url.includes('github.com') ? { stargazers_count: 7 } : body)))
|
||||
const warn = vi.fn()
|
||||
expect((await metricReader(limits, 'https://registry.npmjs.org/', warn, TOKEN)(plugin())).metadata?.sortKeys).toEqual({ github_stars: 7 })
|
||||
expect(warn).toHaveBeenCalledOnce()
|
||||
},
|
||||
)
|
||||
|
||||
it('omits rate-limited GitHub counts while retaining npm downloads and sharing the failed request', async () => {
|
||||
const fetch = vi.fn(async (url: string) => url.includes('github.com')
|
||||
? Response.json({ message: 'API rate limit exceeded' }, { status: 403, headers: { 'x-ratelimit-remaining': '0' } })
|
||||
: Response.json({ downloads: 42 }))
|
||||
vi.stubGlobal('fetch', fetch)
|
||||
const warn = vi.fn()
|
||||
const read = metricReader(limits, 'https://registry.npmjs.org/', warn, TOKEN)
|
||||
for (const result of await Promise.all([read(plugin()), read(plugin())])) {
|
||||
expect(result.metadata?.sortKeys).toEqual({ npm_weekly_downloads: 42 })
|
||||
}
|
||||
expect(fetch).toHaveBeenCalledTimes(2)
|
||||
expect(warn).toHaveBeenCalledExactlyOnceWith('https://api.github.com/repos/team/plugins: HTTP 403; metric omitted')
|
||||
})
|
||||
|
||||
it.each(['http', 'json', 'timeout', 'network'])('omits %s failures without dropping the entry', async (failure) => {
|
||||
vi.stubGlobal('fetch', vi.fn(async () => {
|
||||
if (failure === 'http') return new Response('', { status: 429 })
|
||||
if (failure === 'json') return new Response('not json')
|
||||
throw failure === 'timeout' ? new DOMException('deadline', 'TimeoutError') : new TypeError('offline')
|
||||
}))
|
||||
const warn = vi.fn()
|
||||
const entry = plugin()
|
||||
expect(await metricReader(limits, 'https://registry.npmjs.org/', warn, TOKEN)(entry)).toEqual(entry)
|
||||
expect(warn).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it.each([false, true])('settles streaming HTTP error responses before returning (cancel rejects: %s)', async (rejectCancel) => {
|
||||
const started = Promise.withResolvers<undefined>()
|
||||
const cancellation = Promise.withResolvers<undefined>()
|
||||
const cancel = vi.fn(() => {
|
||||
started.resolve(undefined)
|
||||
return cancellation.promise
|
||||
})
|
||||
vi.stubGlobal('fetch', vi.fn(async () => new Response(new ReadableStream({
|
||||
start(controller) { controller.enqueue(new TextEncoder().encode('unavailable')) },
|
||||
cancel,
|
||||
}), { status: 503 })))
|
||||
const entry = plugin('git-only', { type: 'git', url: 'https://github.com/team/plugins' })
|
||||
const warn = vi.fn()
|
||||
let finished = false
|
||||
const reading = metricReader(limits, 'https://registry.npmjs.org/', warn, TOKEN)(entry).then((result) => {
|
||||
finished = true
|
||||
return result
|
||||
})
|
||||
await started.promise
|
||||
expect(finished).toBe(false)
|
||||
if (rejectCancel) cancellation.reject(new Error('cancel failed'))
|
||||
else cancellation.resolve(undefined)
|
||||
expect(await reading).toEqual(entry)
|
||||
expect(cancel).toHaveBeenCalledOnce()
|
||||
expect(warn).toHaveBeenCalledExactlyOnceWith(
|
||||
`https://api.github.com/repos/team/plugins: HTTP 503; ${rejectCancel ? 'GitHub metric request failed; ' : ''}metric omitted`,
|
||||
)
|
||||
})
|
||||
|
||||
it('does not look up public counts for private-registry names or non-GitHub repositories', async () => {
|
||||
const fetch = vi.fn()
|
||||
vi.stubGlobal('fetch', fetch)
|
||||
const entry = { ...plugin('private-name', { type: 'npm', registry: 'https://registry.example/' }), repository: { url: 'https://gitlab.example/team/plugins' } }
|
||||
expect(await metricReader(limits, 'https://registry.example/', vi.fn(), TOKEN)(entry)).toEqual(entry)
|
||||
expect(fetch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('attributes Git stars to the installed repository instead of a conflicting manifest repository', async () => {
|
||||
const fetch = vi.fn().mockResolvedValue(Response.json({ stargazers_count: 3 }))
|
||||
vi.stubGlobal('fetch', fetch)
|
||||
await metricReader(limits, 'https://registry.npmjs.org/', vi.fn(), TOKEN)(plugin('git-only', { type: 'git', url: 'https://github.com/actual/source.git' }))
|
||||
expect(fetch).toHaveBeenCalledExactlyOnceWith('https://api.github.com/repos/actual/source', expect.any(Object))
|
||||
})
|
||||
|
||||
it('skips GitHub without a token while keeping npm downloads', async () => {
|
||||
const fetch = vi.fn().mockResolvedValue(Response.json({ downloads: 42 }))
|
||||
vi.stubGlobal('fetch', fetch)
|
||||
const warn = vi.fn()
|
||||
const read = metricReader(limits, 'https://registry.npmjs.org/', warn)
|
||||
expect((await read(plugin())).metadata?.sortKeys).toEqual({ npm_weekly_downloads: 42 })
|
||||
const git = plugin('git-only', { type: 'git', url: 'https://github.com/team/plugins' })
|
||||
expect(await read(git)).toEqual(git)
|
||||
expect(fetch).toHaveBeenCalledExactlyOnceWith('https://api.npmjs.org/downloads/point/last-week/%40team%2Fdsh-test', expect.any(Object))
|
||||
expect(warn).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('authenticates only the GitHub API request and leaves npm unauthenticated', async () => {
|
||||
const fetch = vi.fn(async (url: string, _init?: RequestInit) => Response.json(url.includes('github.com')
|
||||
? { stargazers_count: 7 } : { downloads: 42 }))
|
||||
vi.stubGlobal('fetch', fetch)
|
||||
const entry = await metricReader(limits, 'https://registry.npmjs.org/', vi.fn(), TOKEN)(plugin())
|
||||
expect(entry.metadata?.sortKeys).toEqual({ github_stars: 7, npm_weekly_downloads: 42 })
|
||||
expect(fetch).toHaveBeenCalledTimes(2)
|
||||
for (const [url, init] of fetch.mock.calls) {
|
||||
expect(new Headers(init?.headers).get('authorization')).toBe(url.startsWith('https://api.github.com/') ? `Bearer ${TOKEN}` : null)
|
||||
}
|
||||
expect(JSON.stringify(entry)).not.toContain(TOKEN)
|
||||
})
|
||||
|
||||
it.each(['fetch', 'json', 'cancel'])('does not expose a token in %s errors from GitHub', async (failure) => {
|
||||
vi.stubGlobal('fetch', vi.fn(async () => {
|
||||
if (failure === 'fetch') throw new Error(`rejected header Bearer ${TOKEN}`)
|
||||
if (failure === 'json') return new Response(`invalid ${TOKEN}`)
|
||||
return new Response(new ReadableStream({ cancel: () => Promise.reject(new Error(TOKEN)) }), { status: 503 })
|
||||
}))
|
||||
const warn = vi.fn()
|
||||
const entry = plugin('git-only', { type: 'git', url: 'https://github.com/team/plugins' })
|
||||
expect(await metricReader(limits, 'https://registry.npmjs.org/', warn, TOKEN)(entry)).toEqual(entry)
|
||||
expect(warn).toHaveBeenCalledExactlyOnceWith(
|
||||
`https://api.github.com/repos/team/plugins: ${failure === 'fetch' ? '' : `HTTP ${failure === 'json' ? 200 : 503}; `}GitHub metric request failed; metric omitted`,
|
||||
)
|
||||
expect(JSON.stringify(warn.mock.calls)).not.toContain(TOKEN)
|
||||
})
|
||||
@@ -10,11 +10,12 @@ import { chromium, type Browser, type Page } from 'playwright'
|
||||
import { afterEach, beforeEach, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import { credentialKey, credentialRef } from '@deepseek-ai/dsh-credentials'
|
||||
import type {} from '@deepseek-ai/dsh-settings'
|
||||
import { isDraggableAt } from '@deepseek-ai/dsh-client-web/src/window-drag/regions.ts'
|
||||
import {
|
||||
acknowledgeReloadConnectionLoss, captureStableAria, compareOrRefreshGolden,
|
||||
launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { REPO_ROOT, saveFailureShot, ZH_BROWSER_LOCALE } from './support.ts'
|
||||
import { collectWindowSurface, REPO_ROOT, saveFailureShot, ZH_BROWSER_LOCALE } from './support.ts'
|
||||
|
||||
const MODE = webSnapshotMode()
|
||||
const EXPECTED = fileURLToPath(new URL('./expected/desktop-onboarding', import.meta.url))
|
||||
@@ -252,6 +253,16 @@ describe.skipIf(MODE === 'record')('web e2e: App-only desktop onboarding', () =>
|
||||
await page.evaluate((value) => { document.documentElement.dataset.platform = value }, platform)
|
||||
expect(await page.locator('[data-desktop-onboarding]').evaluate(element => getComputedStyle(element).getPropertyValue('-webkit-app-region'))).toBe('none')
|
||||
expect(await page.getByRole('button', { name: '开始设置', exact: true }).evaluate(element => getComputedStyle(element).getPropertyValue('-webkit-app-region'))).toBe(platform === 'darwin' ? 'no-drag' : 'none')
|
||||
if (platform === 'darwin') {
|
||||
expect(await page.locator('[data-window-drag-controls] [data-desktop-onboarding]').count()).toBe(1)
|
||||
const { regions } = await collectWindowSurface(page)
|
||||
const band = await page.locator('[data-window-drag-controls]').locator('..').locator('[data-window-drag]').boundingBox()
|
||||
const button = await page.getByRole('button', { name: '开始设置', exact: true }).boundingBox()
|
||||
if (band === null || button === null) throw new Error('The onboarding drag fixture has no layout box')
|
||||
expect(isDraggableAt(regions, band.x + 4, band.y + 4), 'the introduction keeps its blank drag band').toBe(true)
|
||||
expect(isDraggableAt(regions, button.x + button.width / 2, button.y + button.height / 2),
|
||||
'the introduction action receives clicks over the full-window drag band').toBe(false)
|
||||
}
|
||||
}
|
||||
await page.evaluate(() => { delete document.documentElement.dataset.platform })
|
||||
await page.getByRole('button', { name: '开始设置', exact: true }).click()
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
- region "工作树不可用":
|
||||
- text: 工作树不可用 dsh-{{worktree}}
|
||||
- paragraph: 可以在原位置恢复分支 dsh-{{worktree}} 的工作树。删除前未提交的修改无法恢复
|
||||
- button "恢复工作树"
|
||||
- button "在项目目录继续"
|
||||
@@ -10,7 +10,7 @@
|
||||
- paragraph:
|
||||
- img "会话关联 GitHub PR,跟踪检查、评审与代码差异,并用工作树隔离任务改动"
|
||||
- heading "包含的组件" [level=4]
|
||||
- text: 共 7 个 · 7 已停用
|
||||
- text: 共 6 个 · 6 已停用
|
||||
- list:
|
||||
- listitem:
|
||||
- text: 工作树服务 创建命名的 Git 工作树,并切换当前会话的工作目录。
|
||||
@@ -37,11 +37,6 @@
|
||||
- code: forge-session
|
||||
- code: "@deepseek-ai/dsh-experimental-forge-session"
|
||||
- text: 已关闭
|
||||
- listitem:
|
||||
- text: 工作树服务 创建命名的 Git 工作树,并切换当前会话的工作目录。
|
||||
- code: forge-worktree
|
||||
- code: "@deepseek-ai/dsh-experimental-worktree"
|
||||
- text: 已关闭
|
||||
- listitem:
|
||||
- text: 变更请求面板 在侧栏标签页中显示会话的变更请求,并让新会话可以在工作树中开始。
|
||||
- code: forge-panel
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
- row "USER, Use the ask_user_question tool to ask me exactly one question with id \"checkpoint\", question \"Ready to continue?\", header \"Checkpoint\", and options labeled \"Yes\" and \"No\". After I answer, reply with one short sentence acknowledging my answer and stop.":
|
||||
- cell "Turn 1 USER": USER
|
||||
- cell "Use the ask_user_question tool to ask me exactly one question with id \"checkpoint\", question \"Ready to continue?\", header \"Checkpoint\", and options labeled \"Yes\" and \"No\". After I answer, reply with one short sentence acknowledging my answer and stop."
|
||||
- 'row "CONTEXT, Current runtime context. This snapshot supersedes earlier runtime-context snapshots. Current working directory: \"{{cwd}}/workspace\". Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}/workspace\". Some pl…"':
|
||||
- 'row "CONTEXT, <system-reminder> The following information describes your current session: Current working directory: \"{{cwd}}/workspace\". Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}/workspace\". Some platform te…"':
|
||||
- cell "CONTEXT"
|
||||
- 'cell "Current runtime context. This snapshot supersedes earlier runtime-context snapshots. Current working directory: \"{{cwd}}/workspace\". Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}/workspace\". Some pl…"'
|
||||
- 'cell "<system-reminder> The following information describes your current session: Current working directory: \"{{cwd}}/workspace\". Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}/workspace\". Some platform te…"'
|
||||
- 'row "CONTEXT, <system-reminder> A skill is a reusable set of task-specific instructions. The following skills are available in this session: <available_skills> office: Create or edit Office files; inspect Office files and PDFs. Load before rendering, conversion, or recalculation to enable office_render, office_convert, and office_recalculate next step. office-docx: Create, read, edit, and validate Word documents (.docx). office-pptx: Create, read, edit, and validate PowerPoint presentations (.pptx). office-xlsx: Read, cr…"':
|
||||
- cell "CONTEXT"
|
||||
- 'cell "<system-reminder> A skill is a reusable set of task-specific instructions. The following skills are available in this session: <available_skills> office: Create or edit Office files; inspect Office files and PDFs. Load before rendering, conversion, or recalculation to enable office_render, office_convert, and office_recalculate next step. office-docx: Create, read, edit, and validate Word documents (.docx). office-pptx: Create, read, edit, and validate PowerPoint presentations (.pptx). office-xlsx: Read, cr…"'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** Worktree Sessions and the change request panel through the real optional bundle, a Git checkout, and a scripted GitHub CLI. */
|
||||
import { execFile } from 'node:child_process'
|
||||
import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { access, chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { homedir, tmpdir } from 'node:os'
|
||||
import { basename, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
@@ -376,7 +376,9 @@ async function sendPrompt(page: Page, scaffold: WebScaffold): Promise<{ readonly
|
||||
function worktreeRecords(events: readonly SessionEvent[]): unknown[] {
|
||||
return events.flatMap((event) => {
|
||||
const record = pluginRecordOf(event)
|
||||
return record?.type === 'plugin:client-ui-forge/worktree' ? [record.data] : []
|
||||
if (record?.type === 'plugin:client-ui-forge/worktree') return [record.data]
|
||||
if (record?.type === 'plugin:worktree/created') return [{ created: record.data }]
|
||||
return []
|
||||
})
|
||||
}
|
||||
|
||||
@@ -491,13 +493,12 @@ it.skipIf(MODE === 'record')('starts a Session in a worktree before its first me
|
||||
expect(worktreeRecords(events)).toEqual([
|
||||
{ action: 'request', from: 'main' },
|
||||
{ action: 'request', from: 'release' },
|
||||
{ action: 'created', worktree: { path: directory, branch, from: 'release', baseCommit, repositoryRoot: project } },
|
||||
{ created: { path: directory, branch, baseCommit, repositoryRoot: project } },
|
||||
])
|
||||
// The worktree existed before the turn, so the first model request carried its directory notice.
|
||||
const created = events.findIndex((event) => {
|
||||
const record = pluginRecordOf(event)
|
||||
const data = record?.type === 'plugin:client-ui-forge/worktree' ? record.data : null
|
||||
return typeof data === 'object' && data !== null && 'action' in data && data.action === 'created'
|
||||
return record?.type === 'plugin:worktree/created'
|
||||
})
|
||||
const turn = events.findIndex(event => event.type === 'turn/start')
|
||||
const notice = events.findIndex(event => event.type === 'user/message' && event.data.source.kind === 'working-directory')
|
||||
@@ -1406,6 +1407,42 @@ it.skipIf(MODE === 'record').each(['darwin', 'win32'] as const)('closes diff men
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
})
|
||||
|
||||
|
||||
it.skipIf(MODE === 'record')('archives a clean UI worktree and restores its retained branch', async () => {
|
||||
const { scaffold, page, project, tripwire } = await requestReleaseWorktree('web-e2e-forge-archive')
|
||||
const { settled } = await sendPrompt(page, scaffold)
|
||||
const sessionId = await settled
|
||||
const session = scaffold.ctx.sessions.get(sessionId)
|
||||
if (session === undefined) throw new Error('The completed Session was not retained')
|
||||
const directory = scaffold.ctx.workingDirectory.get(session)
|
||||
const branch = await git(directory, 'branch', '--show-current')
|
||||
const head = await git(directory, 'rev-parse', 'HEAD')
|
||||
const title = '工作树归档恢复'
|
||||
await scaffold.ctx.sessionController.rename({ sessionId, title })
|
||||
const row = page.getByRole('treeitem').filter({ has: page.getByText(title, { exact: true }) })
|
||||
await row.hover()
|
||||
await row.getByRole('button', { name: '归档会话', exact: true }).click()
|
||||
await expect.poll(async () => {
|
||||
try { await access(directory); return true } catch { return false }
|
||||
}, { timeout: 20_000 }).toBe(false)
|
||||
expect(scaffold.ctx.workspaceRegistry.archivedSessionIds).toContain(sessionId)
|
||||
expect(await git(project, 'rev-parse', branch)).toBe(head)
|
||||
await page.getByRole('button', { name: '视图选项', exact: true }).click()
|
||||
await page.getByRole('menuitem', { name: '全部对话(显示已归档)', exact: true }).click()
|
||||
await row.hover()
|
||||
await row.getByRole('button', { name: '取消归档', exact: true }).click()
|
||||
await row.click()
|
||||
const missing = page.locator('[data-forge-missing]')
|
||||
await missing.getByRole('button', { name: '恢复工作树', exact: true }).waitFor({ timeout: 20_000 })
|
||||
await compareOrRefreshGolden(join(EXPECTED, 'archive-restore.expected.md'),
|
||||
await captureStableAria(page, '[data-forge-missing]', scaffold.workspaceCwd, { replacements: [[branch, 'dsh-{{worktree}}']] }), MODE)
|
||||
expect(scaffold.ctx.workingDirectory.get(session)).toBe(directory)
|
||||
await missing.getByRole('button', { name: '恢复工作树', exact: true }).click()
|
||||
await missing.waitFor({ state: 'detached', timeout: 20_000 })
|
||||
expect(await git(directory, 'rev-parse', 'HEAD')).toBe(head)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
})
|
||||
|
||||
it.skipIf(MODE === 'record').each([120, 4000])('types a review comment beside %i changed lines', async (count) => {
|
||||
const { scaffold, page, root, tripwire } = await launchPanel('web-e2e-forge-review-typing')
|
||||
const patch = ['diff --git a/src/typing.ts b/src/typing.ts', 'index 1111111..2222222 100644',
|
||||
|
||||
@@ -94,7 +94,7 @@ describe('minimal agent preset', () => {
|
||||
? [event.data.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('')]
|
||||
: [])
|
||||
expect(runtimeContexts).toEqual([
|
||||
`Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: ${JSON.stringify(agentHandle.agent.session.header.cwd)}.`,
|
||||
`<system-reminder>\nThe following information describes your current session:\n\nCurrent working directory: ${JSON.stringify(agentHandle.agent.session.header.cwd)}.\n</system-reminder>`,
|
||||
])
|
||||
expect(scaffold.ctx.agentPresets.serviceFor(agentHandle.agent, 'fs')).toBeUndefined()
|
||||
expect(scaffold.ctx.agentPresets.serviceFor(agentHandle.agent, 'compaction')).toBeDefined()
|
||||
|
||||
@@ -6,12 +6,74 @@ import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import type { Browser, Locator, Page } from 'playwright'
|
||||
import { expect, vi, type MockInstance } from 'vitest'
|
||||
import type { RegionRect } from '@deepseek-ai/dsh-client-web/src/window-drag/regions.ts'
|
||||
|
||||
/** The built page under test; `pnpm run test:web` rebuilds it before running. */
|
||||
export const DIST_INDEX = fileURLToPath(new URL('../dist/index.html', import.meta.url))
|
||||
|
||||
export const REPO_ROOT = fileURLToPath(new URL('../../..', import.meta.url))
|
||||
|
||||
/** A visible control, including controls that declare no native window region. */
|
||||
interface WindowControlBox extends Omit<RegionRect, 'draggable'> {
|
||||
/** Sample points where the browser hit test reaches this control or its content. */
|
||||
readonly hits: readonly (readonly [number, number])[]
|
||||
}
|
||||
|
||||
/** Native region owners and optional control probes from one browser evaluation. */
|
||||
export interface CollectedWindowSurface {
|
||||
readonly regions: readonly RegionRect[]
|
||||
/** Empty when no control probe was requested. */
|
||||
readonly controls: readonly WindowControlBox[]
|
||||
}
|
||||
|
||||
/** Controls to probe alongside the native region owners. */
|
||||
interface WindowControlProbe {
|
||||
readonly selector: string
|
||||
/** Containers whose nested drag rows own their pixels independently of the container. */
|
||||
readonly containersWithDragRows: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect visible native window regions in DOM order and optionally probe controls in the same frame.
|
||||
* Controls with computed `none` contribute no native rectangle but still receive hit-test probes.
|
||||
* @param page - Page whose current layout is sampled.
|
||||
* @param controlProbe - Control selectors; omit when only native regions are needed.
|
||||
* @returns Native regions with unclipped bounds and independently sampled controls.
|
||||
*/
|
||||
export async function collectWindowSurface(page: Page, controlProbe?: WindowControlProbe): Promise<CollectedWindowSurface> {
|
||||
return page.evaluate((probe) => {
|
||||
const regions: RegionRect[] = []
|
||||
const controls: WindowControlBox[] = []
|
||||
for (const element of document.querySelectorAll('*')) {
|
||||
const style = getComputedStyle(element)
|
||||
const region = style.getPropertyValue('-webkit-app-region')
|
||||
if (style.visibility === 'hidden' || style.display === 'none') continue
|
||||
const rect = element.getBoundingClientRect()
|
||||
if (rect.width === 0 || rect.height === 0) continue
|
||||
const box = { x: rect.x, y: rect.y, width: rect.width, height: rect.height }
|
||||
if (region === 'drag' || region === 'no-drag') regions.push({ ...box, draggable: region === 'drag' })
|
||||
if (probe === undefined || !element.matches(probe.selector)) continue
|
||||
const insetX = Math.min(2, rect.width / 4)
|
||||
const insetY = Math.min(2, rect.height / 4)
|
||||
const samples: [number, number][] = [
|
||||
[rect.x + rect.width / 2, rect.y + rect.height / 2],
|
||||
[rect.x + insetX, rect.y + insetY],
|
||||
[rect.x + rect.width - insetX, rect.y + insetY],
|
||||
[rect.x + insetX, rect.y + rect.height - insetY],
|
||||
[rect.x + rect.width - insetX, rect.y + rect.height - insetY],
|
||||
]
|
||||
const hits = samples.filter(([x, y]) => {
|
||||
const top = document.elementFromPoint(x, y)
|
||||
const strip = top?.closest('[data-window-drag]')
|
||||
if (element.matches(probe.containersWithDragRows) && strip != null && element.contains(strip)) return false
|
||||
return top !== null && (top === element || element.contains(top))
|
||||
})
|
||||
controls.push({ ...box, hits })
|
||||
}
|
||||
return { regions, controls }
|
||||
}, controlProbe)
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the `meta.description` an experimental bundle's plugin page shows for one locale.
|
||||
* @param bundle - Package directory under `packages/experimental`.
|
||||
|
||||
Loaded 100 of 817 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user