better-dsh 0.2.4-c → 0.2.5-a
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cordis.patch.yml +11 -0
- package/docs/50_test-reports/2026-09-28-ctx-scheme/351/225/277/344/274/232/350/257/235utility/345/256/236/346/265/213/346/212/245/345/221/212.md +155 -0
- package/docs/50_test-reports/2026-10-01-v0.2.4d-web-password-gate/345/256/236/346/265/213/346/212/245/345/221/212.md +105 -0
- package/docs/50_test-reports/2026-10-02-ctx-navigation/345/256/236/346/265/213/346/212/245/345/221/212.md +137 -0
- package/docs/50_test-reports/2026-10-02-v0.2.0-rc.2/345/257/271/351/275/220/350/275/256/345/256/236/346/265/213/346/212/245/345/221/212.md +38 -0
- package/docs/specs/ctx/spec.md +78 -6
- package/docs/specs/model-picker-ios-blur/spec.md +168 -0
- package/docs/superpowers/plans/2026-10-02-ctx-navigation.md +319 -0
- package/docs/superpowers/specs/2026-10-02-ctx-navigation-design.md +209 -0
- package/lib/fs-aware/sandbox-plugin.js +2 -2
- package/lib/url-schemes/index.js +194 -37
- package/lib/web-password.d.ts +156 -0
- package/lib/web-password.js +562 -0
- package/lib/{wrap-Dr8XtIak.js → wrap-Bzdimp4p.js} +1 -1
- package/package.json +18 -3
package/docs/specs/ctx/spec.md
CHANGED
|
@@ -2,12 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## Purpose
|
|
4
4
|
|
|
5
|
-
Let the model read a curated, read-only snapshot of its calling environment via `ctx://` URLs — small, static, agent-derived facts (who am I, what model, what cwd) addressable like any other resource. This replaces the v0.1.8c design that mapped `ctx://` onto persistent-kernel variables; see design.md D4 for why that semantics was wrong (the kernel namespace is the model's own REPL scratchpad, not its environment).
|
|
5
|
+
Let the model read a curated, read-only snapshot of its calling environment via `ctx://` URLs — small, static, agent-derived facts (who am I, what model, what cwd) addressable like any other resource, plus a recallable-context navigation surface over the live session's event log. This replaces the v0.1.8c design that mapped `ctx://` onto persistent-kernel variables; see design.md D4 for why that semantics was wrong (the kernel namespace is the model's own REPL scratchpad, not its environment).
|
|
6
6
|
|
|
7
7
|
## Requirements
|
|
8
8
|
|
|
9
|
+
### Requirement: Coordinate invariant
|
|
10
|
+
Every number the system shows the model SHALL be either directly usable as a selector in the same URL family, or explicitly labeled metadata with its unit. The two coordinates are `seq` (event sequence, for `[<label|n>]` addressing) and `line` (transcript line, for `:N-M` windows and `grep` on `ctx://session/transcript`); both SHALL be labeled wherever shown. An unlabeled number that is not an addressable coordinate SHALL NOT appear.
|
|
11
|
+
|
|
12
|
+
#### Scenario: Labeled coordinates in the manifest
|
|
13
|
+
- **WHEN** the model reads `ctx://session/compactions`
|
|
14
|
+
- **THEN** every episode row carries `seq=<start>..<end>` and `lines=<start>..<end>` (or `lines=<none>` for an empty span), never a bare range that could be misread as the other unit
|
|
15
|
+
|
|
9
16
|
### Requirement: Curated snapshot keys
|
|
10
|
-
The system SHALL resolve `ctx://session` as the recallable-context statistics snapshot: the prepared default face SHALL carry the session header (absorbing the former identity fields `id`/`status`/`origin`/`delegationDepth`), storage facts, totals using native DSH field names, per-compaction segments plus a `live` tail segment, the inline compactions manifest (label, checkpoint_seq, compactionId,
|
|
17
|
+
The system SHALL resolve `ctx://session` as the recallable-context statistics snapshot: the prepared default face SHALL carry the session header (absorbing the former identity fields `id`/`status`/`origin`/`delegationDepth`), storage facts, totals using native DSH field names, per-compaction segments plus a `live` tail segment (each with `seq_start`/`seq_end` and `lines`), the inline compactions manifest (label, checkpoint_seq, compactionId, `seq_start`/`seq_end`, `lines`, shadowedItems, shadowedTokenCount, `fidelity`, `replaces_checkpoint`, and an 8-section × ≤100-char summary preview per episode), an `asOf` card (max seq and transcript line at read time, with a note that totals are live values), and a `system_prompt` info card. The canonical face (see `:raw`) SHALL be the full session transcript. The first-level keys `model` and `cwd` SHALL be removed — their information SHALL appear only as info-card fields inside the snapshot. Any other first-level key SHALL return the structured `CTX_UNKNOWN_KEY` error listing the known keys and sub-paths.
|
|
11
18
|
|
|
12
19
|
#### Scenario: Reading session identity
|
|
13
20
|
- **WHEN** the model reads `ctx://session` from a delegated subagent session
|
|
@@ -21,6 +28,10 @@ The system SHALL resolve `ctx://session` as the recallable-context statistics sn
|
|
|
21
28
|
- **WHEN** the model reads `ctx://session`
|
|
22
29
|
- **THEN** the session's creation working directory appears as an info-card field, not as a separate key
|
|
23
30
|
|
|
31
|
+
#### Scenario: Staleness is self-evident
|
|
32
|
+
- **WHEN** the model reads `ctx://session` and caches the snapshot
|
|
33
|
+
- **THEN** the `asOf` card tells it the seq and line the snapshot was taken at, so it cannot mistake a cached snapshot for live totals
|
|
34
|
+
|
|
24
35
|
#### Scenario: Unknown key
|
|
25
36
|
- **WHEN** the model reads `ctx://<other key>`
|
|
26
37
|
- **THEN** the system returns the structured `CTX_UNKNOWN_KEY` error naming the known keys and sub-paths
|
|
@@ -47,16 +58,20 @@ The system SHALL reject every write to `ctx://` with the structured `URL_READ_ON
|
|
|
47
58
|
- **THEN** the system returns the structured `URL_READ_ONLY` error and changes nothing
|
|
48
59
|
|
|
49
60
|
### Requirement: Session sub-path grammar
|
|
50
|
-
The system SHALL resolve `ctx://session/…` sub-paths: `transcript` (full transcript), `compactions` (manifest), `compactions[<label|ordinal>]` (the episode summary
|
|
61
|
+
The system SHALL resolve `ctx://session/…` sub-paths: `transcript` (full transcript), `compactions` (manifest), `compactions[<label|ordinal>]` (the episode summary), `user_prompts[<n|seq>]`, `tool_calls[<n|seq>]`, `agent_responses[<n|seq>]`, `thinking[<n|seq>]` (reasoning blocks), and `system[<n|seq>]` (system messages). The former `compactions[<label|n>]/original` sub-path SHALL be removed — it was identical to `:raw` and is superseded by composing the `:raw` / `:N-M` selectors directly on the episode; a path using it SHALL be rejected with the structured `CTX_BAD_PATH` error echoing the URL. Bracket resolution SHALL match the label (the element's immutable seq coordinate) exactly first, and SHALL fall back to the 0-based ordinal on miss. The system SHALL support `:raw`, line windows (`:N`, `:N-M`, `:N+K`, `:N-`, comma-separated ranges), `:path/<dot-path>`, and `?q=<query>` on every resolved resource; the composite `:raw:<lines>` form SHALL be valid everywhere and SHALL equal `:<lines>` (the `:raw` prefix is redundant in a line-window context but MUST parse). Line windows on a compaction episode SHALL interpret numbers as transcript line coordinates (identical to the same window on `transcript`), and `:path/`/`?q=` SHALL apply to the bare (prepared) face.
|
|
51
62
|
|
|
52
63
|
#### Scenario: Drilling into a compaction episode by label
|
|
53
64
|
- **WHEN** the model reads `ctx://session/compactions[221217]`
|
|
54
|
-
- **THEN** the system returns that episode's structured summary (all 8 sections verbatim)
|
|
65
|
+
- **THEN** the system returns that episode's structured summary (all 8 sections verbatim) or, for the latest episode, the navigation block (see digest residence)
|
|
55
66
|
|
|
56
67
|
#### Scenario: Ordinal fallback
|
|
57
68
|
- **WHEN** the model reads `ctx://session/compactions[0]` and no episode carries the label `0`
|
|
58
69
|
- **THEN** the system returns the first-recorded episode (0-based)
|
|
59
70
|
|
|
71
|
+
#### Scenario: Transcript-relative episode window
|
|
72
|
+
- **WHEN** the model reads `ctx://session/compactions[<label>]:5300-5310`
|
|
73
|
+
- **THEN** the system returns transcript lines 5300–5310 — the same bytes as `ctx://session/transcript:5300-5310`
|
|
74
|
+
|
|
60
75
|
#### Scenario: Composite raw selector
|
|
61
76
|
- **WHEN** the model reads `ctx://session/compactions[221217]:raw:500-560`
|
|
62
77
|
- **THEN** the system returns the same lines as `ctx://session/compactions[221217]:500-560`
|
|
@@ -66,11 +81,56 @@ The system SHALL resolve `ctx://session/…` sub-paths: `transcript` (full trans
|
|
|
66
81
|
- **THEN** the system returns the structured `CTX_BAD_PATH` error echoing the URL and naming the `:raw` / `:N-M` selectors as the replacement
|
|
67
82
|
|
|
68
83
|
### Requirement: Canonical and prepared content faces
|
|
69
|
-
The system SHALL treat every resource as having one canonical content: `:raw` SHALL return the canonical full content, line windows SHALL always apply to the canonical content, and the bare URL SHALL return the prepared default face when one is prepared (session → statistics snapshot; compaction episodes →
|
|
84
|
+
The system SHALL treat every resource as having one canonical content: `:raw` SHALL return the canonical full content, line windows SHALL always apply to the canonical content, and the bare URL SHALL return the prepared default face when one is prepared (session → statistics snapshot; compaction episodes → digest/navigation block; `thinking`/`system`/`injections` and the element collections → index lists) or the canonical content when none is. `:raw:<lines>` SHALL equal `:<lines>` — the composite form is accepted on every resource. A line window that reaches past a resource's canonical extent SHALL return an explicit boundary note (e.g. `[ctx:// note: … span is transcript lines <s>-<e>]`) rather than silently returning a truncated or empty view.
|
|
70
85
|
|
|
71
86
|
#### Scenario: Line windows ignore the prepared face
|
|
72
87
|
- **WHEN** the model reads `ctx://session/compactions[221217]:500-560`
|
|
73
|
-
- **THEN** the system returns lines 500–560 of the episode's original shadowed span, not of the summary
|
|
88
|
+
- **THEN** the system returns transcript lines 500–560 of the episode's original shadowed span, not of the summary
|
|
89
|
+
|
|
90
|
+
#### Scenario: Out-of-span window is explicit
|
|
91
|
+
- **WHEN** the model reads `ctx://session/compactions[221217]:5500` and the episode span ends before transcript line 5500
|
|
92
|
+
- **THEN** the system returns the boundary note naming the episode's span instead of an empty string
|
|
93
|
+
|
|
94
|
+
#### Scenario: Over-read notes, open tail does not
|
|
95
|
+
- **WHEN** the model reads a window whose upper bound exceeds the canonical extent (e.g. `transcript:1940-2000` on a 1947-line transcript)
|
|
96
|
+
- **THEN** the system returns the available lines plus the boundary note naming the canonical end — a partial over-read is still an over-read, because otherwise the model cannot tell "empty" from "does not exist"
|
|
97
|
+
- **WHEN** the model reads the open-tailed `transcript:1940-`
|
|
98
|
+
- **THEN** the system returns the remaining content to the end with no boundary note, provided the window's start is within the extent
|
|
99
|
+
- **WHEN** the model reads an open tail whose start is already past the extent (e.g. `compactions:2-` when the canonical content is one line)
|
|
100
|
+
- **THEN** the system returns the boundary note alone — there is no content to return, so the over-read must be spoken for
|
|
101
|
+
|
|
102
|
+
#### Scenario: Upper bound exactly at the extent is not an over-read
|
|
103
|
+
- **WHEN** the model reads `compactions:1-1` where the canonical content is exactly one line
|
|
104
|
+
- **THEN** the system returns that line with no boundary note
|
|
105
|
+
|
|
106
|
+
### Requirement: Episode digest residence
|
|
107
|
+
The system SHALL render the prepared face of the **latest** compaction episode as a navigation block (NOT the digest text, which is already resident in the model's live context as the compact-checkpoint message): it SHALL name the episode's `lines`/`seq`/items/tokens, its `fidelity`, the transcript lines where the resident digest lives (`digest resident at transcript:<start>-<end> (seq=<checkpointSeq>)`), and the landmark roster. The prepared face of every **older** episode SHALL return that episode's full digest (`summaryText`) followed by the same pointer block. `:raw` on any episode SHALL always return the full shadowed span, so a digest is never lost.
|
|
108
|
+
|
|
109
|
+
#### Scenario: Latest episode is a pointer, not a dump
|
|
110
|
+
- **WHEN** the model reads the bare URL of the most recent compaction episode
|
|
111
|
+
- **THEN** the system returns the navigation block with the digest's transcript location and landmarks, and does NOT repeat the digest text
|
|
112
|
+
|
|
113
|
+
#### Scenario: Older episode returns its digest
|
|
114
|
+
- **WHEN** the model reads the bare URL of a non-latest compaction episode
|
|
115
|
+
- **THEN** the system returns that episode's full digest followed by its pointer block
|
|
116
|
+
|
|
117
|
+
### Requirement: Landmark roster
|
|
118
|
+
The system SHALL include, in each compaction episode's navigation/pointer block, a direction-marker roster computed over that episode's shadowed events, every marker expressed as transcript line coordinates directly usable with `:N-M`: user-turn lines, failure lines (tool results with `message.isError` true or a present error), touched file paths with occurrence counts (best-effort, from tool-call arguments), and the count of prior checkpoints absorbed in the span. Rosters SHALL be capped to a screen and SHALL tail with `… +N more` when capped.
|
|
119
|
+
|
|
120
|
+
#### Scenario: Locating user turns and failures
|
|
121
|
+
- **WHEN** the model reads a compaction episode's prepared face
|
|
122
|
+
- **THEN** the landmark block lists user-turn lines and failure lines as transcript line numbers
|
|
123
|
+
|
|
124
|
+
#### Scenario: Locating touched files
|
|
125
|
+
- **WHEN** the model reads a compaction episode whose span contains file tool calls
|
|
126
|
+
- **THEN** the landmark block lists each touched path with its occurrence count
|
|
127
|
+
|
|
128
|
+
### Requirement: Fidelity signal
|
|
129
|
+
The system SHALL include, per compaction episode, a `fidelity=<user_turns>:<tool_calls>` figure (in the manifest, the snapshot, and the episode pointer block) so the model can judge whether the digest is trustworthy: a tool-heavy span's digest is trustworthy and detail is regenerable, while a user-intent-heavy span must be drilled into.
|
|
130
|
+
|
|
131
|
+
#### Scenario: Reading fidelity
|
|
132
|
+
- **WHEN** the model reads `ctx://session/compactions` or a compaction episode's prepared face
|
|
133
|
+
- **THEN** each episode carries its `fidelity` ratio
|
|
74
134
|
|
|
75
135
|
### Requirement: Thinking, system, and injections collections
|
|
76
136
|
The system SHALL expose three index-faced collections under `ctx://session/`, all following the canonical/prepared face model (bare URL = prepared index; `:raw` / `:N-M` = canonical full text):
|
|
@@ -99,9 +159,21 @@ The system SHALL expose three index-faced collections under `ctx://session/`, al
|
|
|
99
159
|
- **WHEN** the model reads `ctx://session/injections[0]` or `ctx://session/injections[<seq>]`
|
|
100
160
|
- **THEN** the system returns that injected message's full text with its `INJECTED <kind>` header line
|
|
101
161
|
|
|
162
|
+
### Requirement: Element collections carry index faces
|
|
163
|
+
The system SHALL give `user_prompts`, `tool_calls`, and `agent_responses` the same prepared-index/canonical model: the bare URL SHALL return an index list (one line per element: event seq and a ≤100-char preview; the long `tool_calls`/`agent_responses` collections SHALL cap the bare index at a screen and tail with `… +N more — use :raw for all, ?q= to filter, [n|seq] for one`); `[<n|seq>]` SHALL return that element's full text; `:raw` SHALL return all elements' full text joined; line windows SHALL index that canonical text. The error for an unaddressed or out-of-range element SHALL name the collection and its count and SHALL NOT echo an empty-bracket URL.
|
|
164
|
+
|
|
165
|
+
#### Scenario: Browsing user prompts
|
|
166
|
+
- **WHEN** the model reads `ctx://session/user_prompts`
|
|
167
|
+
- **THEN** the system returns an index list of the real user prompts (seq + preview)
|
|
168
|
+
|
|
169
|
+
#### Scenario: Addressing a tool call
|
|
170
|
+
- **WHEN** the model reads `ctx://session/tool_calls[<seq>]`
|
|
171
|
+
- **THEN** the system returns that call's name, arguments, and result — where a present result's text is rendered from either the flat or the nested content-block shape, never silently empty
|
|
172
|
+
|
|
102
173
|
### Requirement: Unknown key echoes known keys
|
|
103
174
|
The `CTX_UNKNOWN_KEY` error SHALL list the currently known first-level keys and the sub-path pointer, so the model can self-correct without leaving the read tool.
|
|
104
175
|
|
|
105
176
|
#### Scenario: Unknown key with roster echo
|
|
106
177
|
- **WHEN** the model reads `ctx://bogus`
|
|
107
178
|
- **THEN** the system returns `CTX_UNKNOWN_KEY` naming `session` and the sub-path roster
|
|
179
|
+
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# model-picker-ios-blur Specification
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
**OPEN — 不修,仅记录。** 2026-10-01 user 裁决:按奥卡姆剃刀,production 没问题就不引入非必要的兜底;当时写好的页面级兜底**已 revert**(源码、单测、mobile leg 接线全部移除)。本文是该问题的**调查记录**,不含任何生效的代码变更。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
记录一个**未坐实的 iOS 触摸缺陷**的完整证据链,使得:
|
|
12
|
+
|
|
13
|
+
1. 日后任何人不必从零重查——已知的事实、已证伪的假设、以及仍未知的部分都在这里;
|
|
14
|
+
2. 当时那段**已删除**的兜底实现(要点 + 判据)可被重建;
|
|
15
|
+
3. 「什么时候该重新研究」有明确触发条件。
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 现象(user 报告,2026-10-01)
|
|
20
|
+
|
|
21
|
+
iPhone Safari / 加到主屏的 Web App,composer 的 model seat:
|
|
22
|
+
|
|
23
|
+
- 第一次点触发按钮 → 弹出**一级菜单**(两行:`Model │ <当前模型>`、`Effort │ <当前等级>`)—— 正常。
|
|
24
|
+
- 再点一级菜单里的 **`Model` 那一行**(期望进入二级的 provider 分组模型清单)→ **整个 menu 直接关掉**,清单不出现。
|
|
25
|
+
- 换模型、resume 会话等后续流程在能进入二级菜单时一切正常。
|
|
26
|
+
|
|
27
|
+
发生实例:**4999 rig**(DSH 0.1.7-rc.1 checkout 构建 + better-dsh 0.2.4-d)。
|
|
28
|
+
未发生实例:**3080 production**(DSH 0.1.7-rc.2 全局安装 + better-dsh 0.2.4-c),同一台 iPhone。
|
|
29
|
+
|
|
30
|
+
> user 补充的历史:更早 **test.pc 的 PC 端**也出现过同样行为,后来某次更新/调整后**自愈**了。这条是「上游版本差异」这一方向的重要旁证。
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Why not fixed (Occam 裁决)
|
|
35
|
+
|
|
36
|
+
| 事实 | 含义 |
|
|
37
|
+
|---|---|
|
|
38
|
+
| prod(rc.2)在同机型同模式下**没有**这个问题 | 现役稳定版本不需要兜底 |
|
|
39
|
+
| user 推测更早的 beta 也没有 | 不是「新版本引入的回归」 |
|
|
40
|
+
| 曾经的 test.pc PC 端**自愈** | 更像上游某次改动带进来的差异,而非我们的插件 |
|
|
41
|
+
| 两端**都是**加到主屏的 Web App(PWA) | PWA/浏览器模式差异这条最省事的解释被**排除** |
|
|
42
|
+
|
|
43
|
+
结论:**不引入页面级兜底**;把证据留下,等有更强的复现条件(或上游版本对齐)再研究。
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 已确证的事实(证据)
|
|
48
|
+
|
|
49
|
+
**E1 — 不是数据/凭据问题。** 4999 活体驱动该组件:根菜单正常;二级清单完整加载 `DeepSeek`(V4.1-Flash、V4-Pro)+ `zai`(GLM-4.7、GLM-5-Turbo、GLM-5.2、GLM-5.2 Highspeed、GLM-5.3、GLM-5.3-Flash、GLM-5.3 Highspeed),**无 error strip / warning 行**。host 侧 `agent-default-model: zai/glm-5.3-flash`,`ZAI_API_KEY` 在凭据库。
|
|
50
|
+
|
|
51
|
+
**E2 — 上游的关闭逻辑在两版之间字节一致。** `@deepseek-ai/dsh-client-ui-model-selection` 的 `ModelSelect.onBlur`:
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
const onBlur = (event) => {
|
|
55
|
+
if (event.relatedTarget instanceof Node && (rootRef.current?.contains(event.relatedTarget) === true
|
|
56
|
+
|| menuRef.current?.contains(event.relatedTarget) === true)) return;
|
|
57
|
+
close(); // 任何「没落在卡片里」的 blur 都关菜单
|
|
58
|
+
};
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
prod 的 rc.2 与 rig 的 rc.1 逐字符相同 ⇒ 组件本身在两个实例上**同样脆弱**。
|
|
62
|
+
|
|
63
|
+
**E3 — 机制可达(本地复现,无需 iOS)。** 菜单开着、触发器持焦时,制造一个 `relatedTarget === null` 的 `focusout`:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
before: menuOpen=1, active=Select model, current …
|
|
67
|
+
after: menuOpen=0 ← 菜单被 onBlur 关掉
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
即:**只要 iOS 在 tap 的触摸相位把焦点抹到"没有地方",菜单就会在 click 之前被 unmount,tap 被吃掉。** 这也解释了 user 观察到的「按完直接关回去」。
|
|
71
|
+
|
|
72
|
+
**E4 — prod 并不免疫(对照实验,决定性)。** 用 prod 自己的 cookie 在真实浏览器里对 **3080 活体页面**施加同一个事件:
|
|
73
|
+
|
|
74
|
+
| | 菜单开 | 施加 `focusout(relatedTarget=null)` | 点 Model 行 |
|
|
75
|
+
|---|---|---|---|
|
|
76
|
+
| **PROD 3080** | ✓ | **菜单被关掉** ❌ | — |
|
|
77
|
+
| **RIG 4999**(当时带兜底) | ✓ | 存活 ✓ | 9 行清单 ✓ |
|
|
78
|
+
|
|
79
|
+
⇒ 「prod 的代码里没有这个缺陷」被**证伪**。差异不在组件逻辑。
|
|
80
|
+
|
|
81
|
+
**E5 — 两个实例服务的组件实现并不"几乎一样"。** 抓取**实际被浏览器加载**的 client bundle 比对(rc.1 vs rc.2),差异是实质性的(非仅 CSS):
|
|
82
|
+
|
|
83
|
+
- 菜单容器:裸 `<div>` → primitives 的 **`MenuSurface`**(多一层 `material` + 一个 `position:fixed` 的 backing portal);
|
|
84
|
+
- `show()`:rc.2 在「没有当前选择」时**直接开二级清单**(`setPane(state.current === null ? 'model' : 'root')`),rc.1 永远先开一级;
|
|
85
|
+
- 新增 `pending` / `StateDot` 转圈、`retainedEffort`、`deepseek-account` provider 分组与文案;
|
|
86
|
+
- 删除 composer-block(`blocked.composer`);
|
|
87
|
+
- `busy` 语义:`status === 'selecting'` → `pending !== null`;
|
|
88
|
+
- 目录同步整段重写:catalog 未就绪时 **rc.1 把 `current` 清成 null / `groups` 清空**(除非 `resolved`),**rc.2 保留**。
|
|
89
|
+
|
|
90
|
+
**E6 — 我们的插件不碰焦点/指针默认。** `better-dsh/src/mobile/client/index.ts` 的滑动手势只在 `document` 上**记录** pointerdown/move 的起点与位移,**既不 `preventDefault()` 也不 `stopPropagation()`**;zoom guard 只在 iOS 窄视口下改 viewport meta / 注入字号地板,不触碰焦点。⇒ 没有「我们的插件把 tap 吃掉」的通路。
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 已证伪的假设
|
|
95
|
+
|
|
96
|
+
| # | 假设 | 证伪方式 |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| H1 | 没有 GLM-5.3 的 key / catalog 两边对不上 → listing 报错后静默弹回 | **E1**(清单完整、无报错) |
|
|
99
|
+
| H2 | 我们某个插件开发不到位(手势/zoom guard 吞掉了 tap) | **E6** |
|
|
100
|
+
| H3 | prod 是 PWA、rig 是浏览器标签页,模式差异 | user 确认**两端都是加到主屏的 Web App** |
|
|
101
|
+
| H4 | prod 的组件代码免疫,所以只有 rig 坏 | **E4**(prod 活体同样被关掉) |
|
|
102
|
+
| H5 | 「两个实例几乎一样,只是版本落后一点点」 | **E5**(该组件实现有实质差异) |
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## 仍然未知(open question)
|
|
107
|
+
|
|
108
|
+
> 同一台 iPhone、同为 PWA:为什么 **rc.2 的页面不发**那个杀死菜单的 blur,而 **rc.1 的页面发**?
|
|
109
|
+
|
|
110
|
+
按可疑度排序的候选方向(**均未验证**):
|
|
111
|
+
|
|
112
|
+
- **D1(最可疑)** rc.1 → rc.2 之间**该组件自身**的行为差异(E5 那一串),特别是菜单容器从裸 `div` 换成 `MenuSurface`、以及 `show()` 开窗策略/目录同步重写。这条与 user 的「更新后自愈」时间线吻合。
|
|
113
|
+
- **D2** 壳层(shell / kernel / composer / `ui-conversation`)在 rc.1→rc.2 的焦点处理差异。
|
|
114
|
+
- **D3(低)** better-dsh 0.2.4-c(prod)vs 0.2.4-d(rig)的差异 —— 但 c→d 只多了 web-password gate 与本次已 revert 的 fix,两者都不参与焦点。
|
|
115
|
+
- **D4** 其它环境项:iOS 版本、键盘是否弹起、PWA 安装时间与缓存态、tap 时 `document.activeElement` 究竟是谁。
|
|
116
|
+
|
|
117
|
+
**重新研究的触发条件**:把 rig 对齐到 rc.2 后现象**仍在** ⇒ 推翻 D1/D2,转 D4,此时才值得上真机探针。
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## 复现与取证
|
|
122
|
+
|
|
123
|
+
### R1 本地机制复现(Chromium,无需 iOS)
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
// 菜单开着、触发器持焦时执行
|
|
127
|
+
document.activeElement.blur() // → menuOpen 1 → 0,菜单消失
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
或者用完整对照脚本:用 cookie 打开 3080 / 4999,`click` 触发器 → `document.activeElement.blur()` → 看 `[role="menu"]` 是否还在 → 若在,点 `[role="menuitem"]` 里含 `Model` 的那行,数 `[role="menuitemradio"]`。当时的脚本(已随 revert 删除)用的是 `puppeteer-core`(`better-dsh/node_modules`)+ playwright 缓存的 Chromium(`~/.cache/ms-playwright/chromium-1228/chrome-linux64/chrome`),cookie 从 curl 的 Netscape jar 解析(**注意 `#HttpOnly_` 前缀不是注释**)。
|
|
131
|
+
|
|
132
|
+
### R2 真机取证(建议的下一步,尚未做)
|
|
133
|
+
|
|
134
|
+
在页面注入探针,记录并**悬浮显示**最近 N 条 `touchstart / touchend / mousedown / focusin / focusout(relatedTarget) / click` + 每步的 `document.activeElement`,在 iPhone 上点一次、截图。这是唯一能看到「iOS 到底发了什么事件」的手段——本机没有可用真 WebKit(Playwright 不支持 Ubuntu 26.04,系统只有 snap epiphany)。
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 当时的兜底实现(已 revert,存档备查)
|
|
139
|
+
|
|
140
|
+
若将来 R2 证明「iOS 确实发了那个 blur」,可按此重建(`better-dsh` 的 `dashr-mobile` boot-script 腿,纯函数 + `Function.prototype.toString` 内联,与 `zoom-guard.ts` 同一约束:ES5、自包含):
|
|
141
|
+
|
|
142
|
+
```js
|
|
143
|
+
export function isSpuriousMenuBlur(target, relatedTarget) {
|
|
144
|
+
if (relatedTarget !== null && relatedTarget !== undefined) return false
|
|
145
|
+
if (target === null || typeof target !== 'object') return false
|
|
146
|
+
var el = target
|
|
147
|
+
if (typeof el.closest === 'function') {
|
|
148
|
+
var owner = el.closest('[role="menu"]')
|
|
149
|
+
if (owner !== null && owner !== undefined) return true
|
|
150
|
+
}
|
|
151
|
+
if (typeof el.getAttribute !== 'function') return false
|
|
152
|
+
return el.getAttribute('aria-haspopup') === 'menu'
|
|
153
|
+
&& el.getAttribute('aria-expanded') === 'true'
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
接线:`document.addEventListener('focusout', e => { if (isSpuriousMenuBlur(e.target, e.relatedTarget)) e.stopPropagation() }, true)` —— **capture 相位在 React 的 root container 监听器之前跑**,所以 React 的 `onBlur` 收不到该事件。
|
|
158
|
+
|
|
159
|
+
验证过的安全性:菜单外 `mousedown`(不依赖焦点)、`Escape`、`Tab`、以及任何 `relatedTarget` 是真实元素的 blur,全部照旧关闭;只有「从打开的菜单里 blur 到没有地方」这一种被压掉。段落需自带分号定界(相邻的 zoom 段以 `})()` 结尾、靠 ASI,裸语句会拼成 `})()var …` = SyntaxError)。
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## 参考锚点
|
|
164
|
+
|
|
165
|
+
- 上游组件:`packages/client/ui-model-selection/src/client/ModelSelect.tsx`(`onBlur` / `show` / `drill` / `close`)
|
|
166
|
+
- 上游 WebKit 相关的两笔提交(**只修了鼠标路径**,rig 与 prod 都已包含):`6613660223`(keep model picker focus during mouse selection)、`06ef26a80f`(preserve focus when toggling the model picker),分支 `fix/webkit-model-picker`
|
|
167
|
+
- 上游 e2e:`apps/web/tests/declared-reasoning.e2e.ts`(Chromium + WebKit 双引擎,含 `pointer-menu` 快照);其 WebKit 用例走的是 `page.mouse.down()`(**鼠标**),因此覆盖不到触摸相位
|
|
168
|
+
- 相关实现:`packages/client/ui-primitives/src/client/MenuSurface.tsx`(rc.2 新增的菜单容器)
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# ctx:// Long-Session Navigability Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Make `ctx://` a navigable long-session surface: every coordinate it shows is addressable, digests are pointers not re-dumps, and landmarks/fidelity direct the model to what matters.
|
|
6
|
+
|
|
7
|
+
**Architecture:** One handler file (`src/url-schemes/handlers/ctx.ts`) gains a transcript line index (single source of truth for line coordinates), transcript-relative episode windows, a digest-residence rule, a landmark/fidelity block, and `:path/`/`?q=` selector support via the existing `applySelector` in `selector.ts`.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript (ESM), Vitest, the `@deepseek-ai/dsh-*` harness peer types.
|
|
10
|
+
|
|
11
|
+
**Spec:** `docs/superpowers/specs/2026-10-02-ctx-navigation-design.md` (algorithms) and `docs/specs/ctx/spec.md` (requirements). Executors read both.
|
|
12
|
+
|
|
13
|
+
## Global Constraints
|
|
14
|
+
|
|
15
|
+
- Coordinate invariant: every number shown is labeled (`seq=` or `lines=`) and directly addressable; transcript is the canonical line space.
|
|
16
|
+
- Episode line windows are transcript-relative (`compactions[label]:N-M` ≡ `transcript:N-M`).
|
|
17
|
+
- Out-of-span windows return an explicit boundary note, never a silent empty/truncated view.
|
|
18
|
+
- Digest residence: latest episode's prepared face never re-dumps the digest; older episodes return their full digest; `:raw` always returns the span.
|
|
19
|
+
- Landmarks and fidelity are capped to one screen and tail `… +N more`.
|
|
20
|
+
- No semantic/vector retrieval. Phone book + index + pointer only.
|
|
21
|
+
- `tsc --noEmit` 0 errors; `npx vitest run` green (baseline 641 passed / 1 skipped).
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Task 1: Transcript line index + episode `lines`
|
|
26
|
+
|
|
27
|
+
**Files:**
|
|
28
|
+
- Modify: `better-dsh/src/url-schemes/handlers/ctx.ts`
|
|
29
|
+
- Test: `better-dsh/test/url-schemes/ctx.spec.ts`
|
|
30
|
+
|
|
31
|
+
**Interfaces:**
|
|
32
|
+
- Produces: `TranscriptIndex { text; totalLines; lineOfSeq: Map<number, {start,end}> }` (see design §3.1); each episode gains `lines: { start: number; end: number } | null` and `originalText` is derived from the transcript slice.
|
|
33
|
+
|
|
34
|
+
- [ ] **Step 1: Add the failing test for episode line coordinates**
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
it('reports transcript line coordinates per episode and keeps the snapshot labeled', async () => {
|
|
38
|
+
const closed = { value: false }
|
|
39
|
+
const r = ctxResolver(closed)
|
|
40
|
+
const snap = JSON.parse(await r.resolve({} as ResolverEnv, 'ctx://session', null))
|
|
41
|
+
const ep0 = snap.compacted[0]
|
|
42
|
+
expect(ep0.lines).toBeTypeOf('object')
|
|
43
|
+
expect(ep0.lines.start).toBeGreaterThanOrEqual(1)
|
|
44
|
+
expect(ep0.lines.end).toBeGreaterThanOrEqual(ep0.lines.start)
|
|
45
|
+
// the manifest prints labeled lines= too
|
|
46
|
+
const manifest = await r.resolve({} as ResolverEnv, 'ctx://session/compactions', null)
|
|
47
|
+
expect(manifest).toMatch(/lines=\d+\.\.\d+/)
|
|
48
|
+
expect(manifest).toMatch(/seq=\d+\.\.\d+/)
|
|
49
|
+
})
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- [ ] **Step 2: Run it, confirm it fails** (`npx vitest run test/url-schemes/ctx.spec.ts` — `lines` is `undefined`)
|
|
53
|
+
|
|
54
|
+
- [ ] **Step 3: Implement `buildTranscript` and derive episode `lines`/`originalText`**
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
interface TranscriptIndex {
|
|
58
|
+
text: string
|
|
59
|
+
totalLines: number
|
|
60
|
+
lineOfSeq: Map<number, { start: number; end: number }>
|
|
61
|
+
}
|
|
62
|
+
function buildTranscript(events: readonly PersistenceEvent[], toolNameOf: (s?: number) => string | undefined): TranscriptIndex {
|
|
63
|
+
const entries = events.map(e => ({ seq: e.seq as number, text: renderEntry(e, toolNameOf) })).filter(t => t.text !== '')
|
|
64
|
+
const lineOfSeq = new Map<number, { start: number; end: number }>()
|
|
65
|
+
let line = 1
|
|
66
|
+
for (let i = 0; i < entries.length; i++) {
|
|
67
|
+
const n = entries[i].text.split('\n').length
|
|
68
|
+
lineOfSeq.set(entries[i].seq, { start: line, end: line + n - 1 })
|
|
69
|
+
line += n + (i < entries.length - 1 ? 1 : 0)
|
|
70
|
+
}
|
|
71
|
+
return { text: entries.map(e => e.text).join('\n\n'), totalLines: line - 1, lineOfSeq }
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
In `resolve`, replace the standalone `transcript()` closure with a single `const tIdx = buildTranscript(events, toolNameOf)` computed once (after `toolNameOf` is defined) and reuse `tIdx.text` everywhere `transcript()` was called. In the episode build loop, after `originalText` is assembled, set:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
const shadowLines = d.shadowedSeqs
|
|
79
|
+
.map(s => tIdx.lineOfSeq.get(s)).filter((x): x is {start:number;end:number} => x !== undefined)
|
|
80
|
+
const lines = shadowLines.length === 0 ? null
|
|
81
|
+
: { start: Math.min(...shadowLines.map(x => x.start)), end: Math.max(...shadowLines.map(x => x.end)) }
|
|
82
|
+
const originalText = lines === null ? ''
|
|
83
|
+
: tIdx.text.split('\n').slice(lines.start - 1, lines.end).join('\n')
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- [ ] **Step 4: Run the test, confirm it passes**
|
|
87
|
+
|
|
88
|
+
- [ ] **Step 5: Commit** `git add better-dsh/src/url-schemes/handlers/ctx.ts better-dsh/test/url-schemes/ctx.spec.ts && git commit -m "feat(ctx): transcript line index + per-episode line coordinates (N1)"`
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Task 2: Transcript-relative episode windows + boundary note
|
|
93
|
+
|
|
94
|
+
**Files:** `ctx.ts`, `ctx.spec.ts`
|
|
95
|
+
|
|
96
|
+
**Interfaces:** Produces `applyEpisodeLines(slice: string, lineStart: number, lineEnd: number, ranges: Array<[number,number]>): string`.
|
|
97
|
+
|
|
98
|
+
- [ ] **Step 1: Add failing tests**
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
it('episode line window is transcript-relative (≡ transcript window)', async () => {
|
|
102
|
+
const closed = { value: false }
|
|
103
|
+
const r = ctxResolver(closed)
|
|
104
|
+
const ep0 = JSON.parse(await r.resolve({} as ResolverEnv, 'ctx://session', null)).compacted[0]
|
|
105
|
+
const viaEpisode = await r.resolve({} as ResolverEnv, `ctx://session/compactions[${ep0.label}]:${ep0.lines.start}-${ep0.lines.end}`, null)
|
|
106
|
+
const viaTranscript = await r.resolve({} as ResolverEnv, `ctx://session/transcript:${ep0.lines.start}-${ep0.lines.end}`, null)
|
|
107
|
+
expect(viaEpisode).toBe(viaTranscript)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('out-of-span episode window returns an explicit boundary note', async () => {
|
|
111
|
+
const closed = { value: false }
|
|
112
|
+
const r = ctxResolver(closed)
|
|
113
|
+
const ep0 = JSON.parse(await r.resolve({} as ResolverEnv, 'ctx://session', null)).compacted[0]
|
|
114
|
+
const out = await r.resolve({} as ResolverEnv, `ctx://session/compactions[${ep0.label}]:${ep0.lines.end + 1000}`, null)
|
|
115
|
+
expect(out).toMatch(/end of episode span|span is transcript lines/)
|
|
116
|
+
})
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
- [ ] **Step 2: Run, confirm both fail**
|
|
120
|
+
|
|
121
|
+
- [ ] **Step 3: Implement transcript-relative episode line application**
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
function applyEpisodeLines(slice: string, lineStart: number, lineEnd: number, ranges: Array<[number, number]>): string {
|
|
125
|
+
const lines = slice.split('\n')
|
|
126
|
+
const out: string[] = []
|
|
127
|
+
let noted = false
|
|
128
|
+
for (const [aRaw, bRaw] of ranges) {
|
|
129
|
+
const a = Math.max(1, aRaw); const b = bRaw === Infinity ? lineEnd : bRaw
|
|
130
|
+
if (b < lineStart || a > lineEnd) { noted = true; continue }
|
|
131
|
+
const la = Math.max(a, lineStart) - lineStart + 1
|
|
132
|
+
const lb = Math.min(b, lineEnd) - lineStart + 1
|
|
133
|
+
out.push(...lines.slice(la - 1, lb))
|
|
134
|
+
if (bRaw === Infinity ? false : b > lineEnd || a < lineStart) noted = true
|
|
135
|
+
}
|
|
136
|
+
const body = out.join('\n')
|
|
137
|
+
const note = noted ? `\n\n[ctx:// note: episode span is transcript lines ${lineStart}-${lineEnd}]` : ''
|
|
138
|
+
return body + note
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
In the episode branch, when the episode has `lines !== null`, route `sel.kind === 'lines'` through `applyEpisodeLines(episode.originalText, episode.lines.start, episode.lines.end, sel.ranges)` instead of the shared `applyLines`.
|
|
143
|
+
|
|
144
|
+
- [ ] **Step 4: Run, confirm both pass (the first test also proves the episode-span ≡ transcript-slice invariant)**
|
|
145
|
+
|
|
146
|
+
- [ ] **Step 5: Commit** `git commit -am "feat(ctx): transcript-relative episode windows + explicit span bounds (N1/F10)"`
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Task 3: Digest residence rule
|
|
151
|
+
|
|
152
|
+
**Files:** `ctx.ts`, `ctx.spec.ts`
|
|
153
|
+
|
|
154
|
+
**Interfaces:** Produces `episodeNavigation(episode, isLatest, checkpointLines) => string` (latest → navigation block, older → digest + pointer block).
|
|
155
|
+
|
|
156
|
+
- [ ] **Step 1: Add failing tests**
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
it('latest episode prepared face is a navigation block without the digest text', async () => {
|
|
160
|
+
const closed = { value: false }
|
|
161
|
+
const r = ctxResolver(closed)
|
|
162
|
+
const latest = await r.resolve({} as ResolverEnv, 'ctx://session/compactions[40]', null) // label 40 is last
|
|
163
|
+
expect(latest).toMatch(/digest is already in your live context/)
|
|
164
|
+
expect(latest).toMatch(/digest resident .*seq=41/)
|
|
165
|
+
expect(latest).not.toMatch(/second round/) // the digest body must NOT be dumped
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('older episode prepared face returns its full digest', async () => {
|
|
169
|
+
const closed = { value: false }
|
|
170
|
+
const r = ctxResolver(closed)
|
|
171
|
+
const older = await r.resolve({} as ResolverEnv, 'ctx://session/compactions[20]', null)
|
|
172
|
+
expect(older).toMatch(/do the thing/) // digest body present
|
|
173
|
+
expect(older).toMatch(/lines=\d+\.\.\d+/)
|
|
174
|
+
})
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
- [ ] **Step 2: Run, confirm fail**
|
|
178
|
+
|
|
179
|
+
- [ ] **Step 3: Implement** the checkpoint-line lookup (the `user/message` whose `source.plugin === 'compact'` at `checkpointSeq`, via `tIdx.lineOfSeq`, fallback `undefined`) and the two branches. Latest block shape (no digest): see design §4 (N2). Older: `episode.summaryText` + the pointer block. `:raw`/`:N-M` unchanged (span).
|
|
180
|
+
|
|
181
|
+
- [ ] **Step 4: Run, confirm pass**
|
|
182
|
+
|
|
183
|
+
- [ ] **Step 5: Commit** `git commit -am "feat(ctx): digest residence rule — latest is a pointer, older dumps digest (N2)"`
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Task 4: Landmark roster + fidelity
|
|
188
|
+
|
|
189
|
+
**Files:** `ctx.ts`, `ctx.spec.ts`
|
|
190
|
+
|
|
191
|
+
**Interfaces:** Produces `landmarksOf(episodeEvents, tIdx): string` and `fidelityOf(episodeEvents): { userTurns; toolCalls }`.
|
|
192
|
+
|
|
193
|
+
- [ ] **Step 1: Add failing tests**
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
it('landmark block lists user-turn and failure lines and touched paths', async () => {
|
|
197
|
+
const closed = { value: false }
|
|
198
|
+
const r = ctxResolver(closed)
|
|
199
|
+
const out = await r.resolve({} as ResolverEnv, 'ctx://session/compactions[20]', null)
|
|
200
|
+
expect(out).toMatch(/user turns \(\d+\): \d+/)
|
|
201
|
+
expect(out).toMatch(/touched paths:/)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('manifest and snapshot carry fidelity', async () => {
|
|
205
|
+
const closed = { value: false }
|
|
206
|
+
const r = ctxResolver(closed)
|
|
207
|
+
expect(await r.resolve({} as ResolverEnv, 'ctx://session/compactions', null)).toMatch(/fidelity=\d+:\d+/)
|
|
208
|
+
const snap = JSON.parse(await r.resolve({} as ResolverEnv, 'ctx://session', null))
|
|
209
|
+
expect(snap.compacted[0].fidelity).toEqual(expect.objectContaining({ userTurns: expect.any(Number), toolCalls: expect.any(Number) }))
|
|
210
|
+
})
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
- [ ] **Step 2: Run, confirm fail**
|
|
214
|
+
|
|
215
|
+
- [ ] **Step 3: Implement** `landmarksOf` (user turns = `user/message` with `source.kind==='user'` → `tIdx.lineOfSeq` line; failures = `tool/result` with `data.message.isError===true` or `data.error` present → line + tool name; touched paths = path-valued strings from `tool/call` args keys `path`/`file_path`/`filePath`/`file` + `edits[].path` + `files[].path`, args > 64 000 chars skipped; prior checkpoints = count of `user/message` with `source.plugin==='compact'`). Caps: 30 / 20 / 12. `fidelityOf` = counts of user turns and `tool/call` events. Wire into the episode block (Task 3), the manifest line, and the snapshot `compacted[]`.
|
|
216
|
+
|
|
217
|
+
- [ ] **Step 4: Run, confirm pass**
|
|
218
|
+
|
|
219
|
+
- [ ] **Step 5: Commit** `git commit -am "feat(ctx): landmark roster + fidelity signal (N3/N4)"`
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Task 5: `:path/` + `?q=` selectors + error echo
|
|
224
|
+
|
|
225
|
+
**Files:** `ctx.ts`, `ctx.spec.ts`
|
|
226
|
+
|
|
227
|
+
**Interfaces:** Consumes `applySelector(text, sel)` from `../../src/url-schemes/selector.ts` (already exported).
|
|
228
|
+
|
|
229
|
+
- [ ] **Step 1: Add failing tests**
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
it('supports :path/ on the snapshot JSON and ?q= on an index face', async () => {
|
|
233
|
+
const closed = { value: false }
|
|
234
|
+
const r = ctxResolver(closed)
|
|
235
|
+
const n = await r.resolve({} as ResolverEnv, 'ctx://session:path/totals.tool_calls', null)
|
|
236
|
+
expect(n).toMatch(/^\d+$/)
|
|
237
|
+
const q = await r.resolve({} as ResolverEnv, 'ctx://session/injections?q=agent-instructions', null)
|
|
238
|
+
expect(q).toMatch(/AGENTS\.md/)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
- [ ] **Step 2: Run, confirm fail**
|
|
244
|
+
|
|
245
|
+
- [ ] **Step 3: Implement** — in `applyFace`, before the `lines` branch:
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
if (selector.kind === 'path' || selector.kind === 'query') {
|
|
249
|
+
return applySelector(face === 'canonical' ? canonical : prepared, selector)
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Import `applySelector` from `../selector.ts` (add to the existing `import { UrlSchemesError }` line). Replace the manifest special-case rejection with `return applySelector(body, sel)` so `?q=`/`:N-M`/`:path/` all work there too. Keep the now-unreachable `CTX_BAD_SELECTOR` throw but give it a support-set message (`:raw`, `:N-M`, `:path/`, `?q=`) for future kinds.
|
|
254
|
+
|
|
255
|
+
- [ ] **Step 4: Run, confirm pass**
|
|
256
|
+
|
|
257
|
+
- [ ] **Step 5: Commit** `git commit -am "feat(ctx): :path/ and ?q= selectors (N5/F5/F8)"`
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## Task 6: `asOf`, F1 index faces, F2 result shape
|
|
262
|
+
|
|
263
|
+
**Files:** `ctx.ts`, `ctx.spec.ts`
|
|
264
|
+
|
|
265
|
+
- [ ] **Step 1: Add failing tests**
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
it('snapshot carries asOf', async () => {
|
|
269
|
+
const closed = { value: false }
|
|
270
|
+
const r = ctxResolver(closed)
|
|
271
|
+
const snap = JSON.parse(await r.resolve({} as ResolverEnv, 'ctx://session', null))
|
|
272
|
+
expect(snap.asOf).toEqual(expect.objectContaining({ seq: expect.any(Number), line: expect.any(Number) }))
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it('bare element collections return index faces, not an empty-bracket error', async () => {
|
|
276
|
+
const closed = { value: false }
|
|
277
|
+
const r = ctxResolver(closed)
|
|
278
|
+
const idx = await r.resolve({} as ResolverEnv, 'ctx://session/user_prompts', null)
|
|
279
|
+
expect(idx).toMatch(/seq=\d+/)
|
|
280
|
+
expect(idx).toMatch(/hello world/)
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it('flat tool-result content renders (F2)', async () => {
|
|
284
|
+
const closed = { value: false }
|
|
285
|
+
const flat: PersistenceEvent[] = [
|
|
286
|
+
{ seq: 5, type: 'tool/call', time: T, data: { callId: 'c1', name: 'bash', arguments: '{"command":"echo hi"}' } },
|
|
287
|
+
{ seq: 6, type: 'tool/result', time: T, sourceEventSeqs: [5], data: { message: { role: 'tool', isError: false, content: [{ type: 'text', text: 'hi' }] } } },
|
|
288
|
+
]
|
|
289
|
+
const r = ctxResolver(closed, flat)
|
|
290
|
+
expect(await r.resolve({} as ResolverEnv, 'ctx://session/tool_calls[5]', null)).toMatch(/result: hi/)
|
|
291
|
+
})
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
- [ ] **Step 2: Run, confirm fail**
|
|
295
|
+
|
|
296
|
+
- [ ] **Step 3: Implement**
|
|
297
|
+
- `asOf`: add `asOf: { seq: <max seq>, line: tIdx.totalLines }` to `buildSnapshot`.
|
|
298
|
+
- F1: give `user_prompts`/`tool_calls`/`agent_responses` a bare index face (per-element `seq=<s> <preview>`), capping `tool_calls`/`agent_responses` at 20 lines with a `… +N more` tail; `[n|seq]` and `:raw` unchanged. Update the element error to name the collection + count (no `ctx://…[]`).
|
|
299
|
+
- F2: `resultTextOf` returns `nested !== '' ? nested : textOf(blocks)` (flat fallback; see design §3.3).
|
|
300
|
+
|
|
301
|
+
- [ ] **Step 4: Run full suite** `npx vitest run` and `npx tsc --noEmit` — all green.
|
|
302
|
+
|
|
303
|
+
- [ ] **Step 5: Commit** `git commit -am "feat(ctx): asOf, element index faces, flat tool-result rendering (N6/F1/F2)"`
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Task 7: Docs, rig verification, report
|
|
308
|
+
|
|
309
|
+
**Files:** `docs/specs/ctx/spec.md` (already updated), `better-dsh/dashr/url-schemes-instruction.md` (model-facing instruction, if it lists selectors — verify and add `:path/`/`?q=`/transcript-relative note), `docs/50_test-reports/2026-10-02-ctx-navigation实测报告.md`.
|
|
310
|
+
|
|
311
|
+
- [ ] **Step 1:** Grep `better-dsh/` for the model-facing `url-schemes` instruction doc and update the ctx selector list if present.
|
|
312
|
+
|
|
313
|
+
- [ ] **Step 2:** Rebuild + sync: `cd better-dsh && npm run build && cd .. && rsync -a --delete better-dsh/lib/ .test/home/compat/profiles/web/node_modules/better-dsh/lib/` and restart `bash .test/seed/test123/start.sh`.
|
|
314
|
+
|
|
315
|
+
- [ ] **Step 3:** First-person verify on 4999 via `dvc://browser` and a `headless` rigcheck session: snapshot has `asOf` + labeled `lines`; a deep episode windows by transcript line; `?q=` filters an index; `:path/` narrow-reads; a tool result is non-empty.
|
|
316
|
+
|
|
317
|
+
- [ ] **Step 4:** Write the test report to `docs/50_test-reports/2026-10-02-ctx-navigation实测报告.md` (findings, probes, verdict).
|
|
318
|
+
|
|
319
|
+
- [ ] **Step 5: Commit** `git commit -am "docs(ctx): navigation change — instruction + first-person test report"`
|