pi-cursor-sdk 0.1.17 → 0.1.18
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/CHANGELOG.md +24 -0
- package/README.md +1 -1
- package/docs/cursor-live-smoke-checklist.md +19 -2
- package/docs/cursor-model-ux-spec.md +1 -1
- package/docs/cursor-testing-lessons.md +199 -0
- package/package.json +4 -1
- package/scripts/isolated-cursor-smoke.sh +226 -0
- package/scripts/validate-smoke-jsonl.mjs +62 -7
- package/src/cursor-context-tools.ts +6 -0
- package/src/cursor-display-text.ts +10 -0
- package/src/cursor-native-replay-routing.ts +48 -0
- package/src/cursor-native-replay-trace.ts +29 -0
- package/src/cursor-native-tool-display-registration.ts +13 -3
- package/src/cursor-provider-live-run-drain.ts +36 -10
- package/src/cursor-provider-turn-coordinator.ts +21 -17
- package/src/cursor-provider.ts +5 -0
- package/src/cursor-question-tool.ts +9 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.18 - 2026-05-23
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Add `scripts/isolated-cursor-smoke.sh` and `npm run smoke:isolated` for packed `/tmp` install smoke with seeded `auth.json`, plan-strip shim, and JSONL replay-error scans.
|
|
8
|
+
- Add `scripts/fixtures/plan-strip-shim/` to simulate plan-mode execute stripping active tools to `read`, `bash`, `edit`, and `write`.
|
|
9
|
+
- Extend `scripts/validate-smoke-jsonl.mjs` with `--replay-errors` and `--replay-errors-only` to fail on persisted `Tool grep/cursor/find/ls not found` entries.
|
|
10
|
+
- Add [Cursor testing lessons](docs/cursor-testing-lessons.md) documenting auth.json seeding, isolated harness layout, JSONL replay scans, and the plan-mode replay regression chain.
|
|
11
|
+
- Add regression coverage in `test/cursor-native-replay-stress.test.ts`, `test/cursor-native-replay-trace.test.ts`, `test/cursor-native-replay-routing.test.ts`, and expanded live-run / extension lifecycle tests.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Centralize native replay routing in `src/cursor-native-replay-routing.ts` (`resolveNativeReplayDisposition`, shared context-tool partitioning) for turn coordinator and live-run drain.
|
|
16
|
+
- Unify 240-character display truncation in `src/cursor-display-text.ts` and share `getActiveContextToolNames()` via `src/cursor-context-tools.ts`.
|
|
17
|
+
- Unify inactive native replay trace formatting through `src/cursor-native-replay-trace.ts` (`title: summary`) for both live-run drain and turn-coordinator paths.
|
|
18
|
+
- On non-Cursor model switch, strip all registered native replay wrappers except core pi tools (`read`, `bash`, `edit`, `write`), not only `cursor`.
|
|
19
|
+
- Document `auth.json` as the primary live-smoke auth source in the live smoke checklist, README maintainer gate, and UX spec.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Fix `Tool grep not found` and related native replay failures after plan-mode execute resets active tools by re-syncing registered Cursor replay wrappers on `before_agent_start` and `turn_start`.
|
|
24
|
+
- Skip native replay `toolUse` when a replay tool is inactive in `context.tools`; emit scrubbed thinking trace instead of a broken pi tool call.
|
|
25
|
+
- Partition live-run drain replay emission so inactive queued native tools fall back to trace output instead of invalid `toolUse` turns.
|
|
26
|
+
|
|
3
27
|
## 0.1.17 - 2026-05-23
|
|
4
28
|
|
|
5
29
|
### Added
|
package/README.md
CHANGED
|
@@ -232,7 +232,7 @@ PI_CURSOR_PI_TOOL_BRIDGE_DEBUG=1 pi --model cursor/composer-2.5
|
|
|
232
232
|
|
|
233
233
|
### Maintainer live smoke release gate
|
|
234
234
|
|
|
235
|
-
For Cursor provider/runtime changes, follow the manual [Cursor live smoke checklist](docs/cursor-live-smoke-checklist.md) before release. Assume every runtime surface is in scope. The checklist uses real `pi -e . --cursor-no-fast --model cursor/composer-2.5` runs with temporary session dirs and requires the visible TUI/output, scrubbed diagnostics, and persisted JSONL to agree. Do not mark a release ready with optional, deferred, mostly-passing, or unobserved smoke checks outstanding.
|
|
235
|
+
For Cursor provider/runtime changes, follow the manual [Cursor live smoke checklist](docs/cursor-live-smoke-checklist.md) before release. See [Cursor testing lessons](docs/cursor-testing-lessons.md) for auth.json seeding, isolated `/tmp` harness layout, JSONL replay-error scans, and other regression traps. Assume every runtime surface is in scope. The checklist uses real `pi -e . --cursor-no-fast --model cursor/composer-2.5` runs with temporary session dirs and requires the visible TUI/output, scrubbed diagnostics, and persisted JSONL to agree. Do not mark a release ready with optional, deferred, mostly-passing, or unobserved smoke checks outstanding.
|
|
236
236
|
|
|
237
237
|
## Fallback models
|
|
238
238
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Purpose
|
|
4
4
|
|
|
5
|
-
Use this manual checklist before releasing Cursor provider/runtime changes. Unit tests and mocks are necessary, but they are not enough for this extension. Always assume every runtime surface is in scope. A release is not ready until every live check below has been observed with `cursor/composer-2.5` through the local working tree.
|
|
5
|
+
Use this manual checklist before releasing Cursor provider/runtime changes. Unit tests and mocks are necessary, but they are not enough for this extension. See [Cursor testing lessons](./cursor-testing-lessons.md) for auth/isolated-harness pitfalls and the plan-mode replay regression that motivated recent hardening. Always assume every runtime surface is in scope. A release is not ready until every live check below has been observed with `cursor/composer-2.5` through the local working tree.
|
|
6
6
|
|
|
7
7
|
## Release rule
|
|
8
8
|
|
|
@@ -22,19 +22,36 @@ mkdir -p "$SMOKE_DIR"
|
|
|
22
22
|
pi -e . --list-models cursor
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
Live pi runs resolve provider auth from **`~/.pi/agent/auth.json`**, not only shell env. Isolated smoke copies that file into a clean temporary `HOME`. Ensure `auth.json` includes a `cursor` provider entry, or export `CURSOR_API_KEY` as a fallback.
|
|
26
|
+
|
|
25
27
|
The repo also ships partial automation for the prerequisite/basic/default-settings/non-interactive math/TUI output polling/steering/diagnostic/JSONL subset:
|
|
26
28
|
|
|
27
29
|
```bash
|
|
28
30
|
npm run smoke:live
|
|
29
31
|
```
|
|
30
32
|
|
|
33
|
+
For native replay regression checks (packed install, plan-strip resync, JSONL replay-error scan), use the isolated helper:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm run smoke:isolated
|
|
37
|
+
# unit tests + pack only (no live Cursor):
|
|
38
|
+
SKIP_LIVE=1 npm run smoke:isolated
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Scan persisted sessions for native replay tool failures:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
node scripts/validate-smoke-jsonl.mjs --replay-errors "$SMOKE_DIR"
|
|
45
|
+
node scripts/validate-smoke-jsonl.mjs --replay-errors-only "$SMOKE_DIR/session-subdir"
|
|
46
|
+
```
|
|
47
|
+
|
|
31
48
|
The script is a helper only; it polls the section 3 TUI for answer/footer evidence and then cleans up the tmux session, but it does not replace manual visual review of the full TUI checklist. Release readiness still requires the manual checks below for detailed TUI behavior, bridge, standalone native replay, abort/cancel, packaging, cleanup, and any touched runtime surface not covered by the helper.
|
|
32
49
|
|
|
33
50
|
Pass criteria:
|
|
34
51
|
|
|
35
52
|
- `cursor/composer-2.5` appears in the model list.
|
|
36
53
|
- No Cursor key or auth token is printed.
|
|
37
|
-
- If `
|
|
54
|
+
- If neither `~/.pi/agent/auth.json` cursor auth nor `CURSOR_API_KEY` is available, stop and report the live smoke as blocked.
|
|
38
55
|
|
|
39
56
|
## 1. Basic provider reality check
|
|
40
57
|
|
|
@@ -32,7 +32,7 @@ Current implementation notes:
|
|
|
32
32
|
- Cursor SDK usage events report cumulative internal agent/tool/cache work, not the replayable pi prompt context. The extension does not copy raw Cursor SDK usage into pi usage or compaction. For Cursor assistant messages, `usage.input`/`usage.output` are approximate pi session activity components: initial Cursor prompt input is counted once, consumed split-run tool results are counted as deduped input on the following assistant turn, and assistant output includes visible text/thinking/tool-call content. `usage.totalTokens` is the replayable Cursor prompt/context estimate derived from the same `buildCursorPrompt()` path used for `Agent.send`; it may differ from `input + output` and is the context-safe value for display/compaction. `src/cursor-usage-accounting.ts` owns this usage policy, and `src/cursor-live-run-accounting.ts` owns prompt-once and consumed-tool-result accounting so provider usage and bridge result resolution share the same matched tool-result boundary.
|
|
33
33
|
- Audit observation, 2026-05-19, superseded by the 2026-05-21 replay pass: a missing-file read with Composer 2.5 emitted `tool-call-started` for Cursor `read`, then streamed final text `Error: File not found`, but did not emit `tool-call-completed` or an `onStep` `toolCall` error result. Leftover started calls are now discarded at run completion instead of becoming synthetic replay errors. Cursor-reported completed/step errors remain visible.
|
|
34
34
|
- Maintainer visual verification for replay-card changes should follow [Cursor Native Tool Visual Audit Workflow](./cursor-native-tool-visual-audit.md): offscreen PTY-driven pi run, xterm.js/Playwright screenshot rendering, and JSONL inspection before accepting commits or PRs.
|
|
35
|
-
- Cursor provider/runtime releases should follow [Cursor Live Smoke Checklist](./cursor-live-smoke-checklist.md) with real `pi -e . --cursor-no-fast --model cursor/composer-2.5` invocations, manual observation, temporary session dirs, diagnostics scans, and persisted JSONL inspection. Assume every runtime surface is in scope. A release is not ready when any live check is optional, deferred, mostly passing, or unobserved.
|
|
35
|
+
- Cursor provider/runtime releases should follow [Cursor Live Smoke Checklist](./cursor-live-smoke-checklist.md) with real `pi -e . --cursor-no-fast --model cursor/composer-2.5` invocations, manual observation, temporary session dirs, diagnostics scans, and persisted JSONL inspection. See [Cursor testing lessons](./cursor-testing-lessons.md) for auth.json seeding, isolated smoke harnesses, and replay JSONL scans. Assume every runtime surface is in scope. A release is not ready when any live check is optional, deferred, mostly passing, or unobserved.
|
|
36
36
|
- For models without a catalog `context` parameter, context windows are not hardcoded. The extension ships a bundled SDK-derived default/non-Max cache generated from `createAgentPlatform().checkpointStore.loadLatest(agentId).tokenDetails.maxTokens`. Successful runs can update a local override cache, but model discovery does not probe models at startup.
|
|
37
37
|
- Max Mode context windows are distinct from default/non-Max context windows. `@cursor/sdk` 1.0.13 documentation says the SDK may enable Max Mode automatically when a selected model requires it, but the public local-agent `ModelSelection` path still does not expose a manual Max Mode selector. Do not advertise Max Mode context windows unless the SDK catalog exposes an exact parameter/variant or the SDK public API adds a Max Mode selector that the extension actually sends.
|
|
38
38
|
- `@cursor/sdk` 1.0.13 adds latest-style `ModelListItem.aliases`. The extension registers only unambiguous aliases as pi model IDs (with the same context suffixes when applicable) and sends the alias back in `ModelSelection.id`, while sharing Cursor-only state such as fast defaults with the underlying catalog `id`. Aliases shared by multiple base models, such as generic family aliases, are skipped because the pi row metadata would otherwise imply one base model while Cursor may resolve the alias to another.
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Cursor Testing Lessons
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
This document records maintainer testing lessons for `pi-cursor-sdk`. It complements unit tests and the [Cursor live smoke checklist](./cursor-live-smoke-checklist.md). Use it when adding regression coverage, debugging false-green releases, or building isolated smoke harnesses.
|
|
6
|
+
|
|
7
|
+
## Core lesson: integration-shaped bugs beat unit mocks
|
|
8
|
+
|
|
9
|
+
The native replay `Tool grep not found` failure was integration-shaped, not unit-shaped:
|
|
10
|
+
|
|
11
|
+
1. **Plan mode** calls `setActiveTools(["read", "bash", "edit", "write"])` when execution starts.
|
|
12
|
+
2. **pi-cursor-sdk** only re-synced native replay wrappers on `session_start` / `model_select`, not every turn.
|
|
13
|
+
3. **The provider** still emitted native replay `toolUse` for `grep` / `cursor`.
|
|
14
|
+
4. **pi's agent loop** looked up tools in `context.tools` and failed with `Tool grep not found`.
|
|
15
|
+
|
|
16
|
+
Passing hundreds of unit tests did not prove that chain was safe. Regression coverage now includes:
|
|
17
|
+
|
|
18
|
+
- `test/index.test.ts` — `before_agent_start` and `turn_start` resync after plan-style strip
|
|
19
|
+
- `test/cursor-native-replay-stress.test.ts` — plan strip → resync → grep replay; inactive-tool trace fallbacks
|
|
20
|
+
- `test/cursor-provider-replay-live-run.test.ts` — inactive replay tools emit trace instead of broken `toolUse`
|
|
21
|
+
- `test/cursor-native-replay-trace.test.ts` — shared inactive replay trace formatting
|
|
22
|
+
- `test/cursor-native-replay-routing.test.ts` — `resolveNativeReplayDisposition` and `partitionNativeToolsByActiveContext`
|
|
23
|
+
|
|
24
|
+
When changing provider/runtime behavior, ask whether the bug spans **pi extension lifecycle**, **active tool state**, **provider streaming**, and **persisted JSONL**. If yes, add an integration-style unit test or live smoke coverage for that chain.
|
|
25
|
+
|
|
26
|
+
## Dual-check invariant: `context.tools` vs pi active tools
|
|
27
|
+
|
|
28
|
+
Native replay routing intentionally uses two layers:
|
|
29
|
+
|
|
30
|
+
1. **Extension resync** (`before_agent_start`, `turn_start`) updates pi's active tool set via `syncRegisteredNativeCursorToolsForModel`. This fixes the common case where plan-mode execute strips `grep`/`find`/`cursor` before the next turn.
|
|
31
|
+
2. **Provider routing** uses the **`context.tools` snapshot** captured when `streamCursor()` starts (`getActiveContextToolNames` in `src/cursor-context-tools.ts`). It does not read live `pi.getActiveTools()` mid-stream.
|
|
32
|
+
|
|
33
|
+
`src/cursor-native-replay-routing.ts` centralizes provider-side routing against the same `context.tools` snapshot:
|
|
34
|
+
|
|
35
|
+
- **Turn coordinator** calls `resolveNativeReplayDisposition()` per completed SDK tool → `queue_replay` (queue native `toolUse`), `inactive_trace` (`formatInactiveCursorReplayTrace()`), or `transcript_trace`.
|
|
36
|
+
- **Live-run drain** calls `partitionNativeToolsByActiveContext()` on already-queued native tool batches → active tools become `toolUse`; inactive tools get trace only and the batch returns `"handled"` without `toolUse`.
|
|
37
|
+
|
|
38
|
+
Disposition outcomes:
|
|
39
|
+
|
|
40
|
+
- `queue_replay` — tool is in `context.tools` and a live run exists
|
|
41
|
+
- `inactive_trace` — native replay tool missing from `context.tools`
|
|
42
|
+
- `transcript_trace` — native replay off or non-native tool
|
|
43
|
+
|
|
44
|
+
If resync runs but `context.tools` is still stale (e.g. only `read` listed), the provider must **not** emit `toolUse` for inactive tools. `test/cursor-native-replay-stress.test.ts` covers that stale-snapshot path.
|
|
45
|
+
|
|
46
|
+
## Auth: use `auth.json`, not only env
|
|
47
|
+
|
|
48
|
+
pi resolves Cursor auth in this order:
|
|
49
|
+
|
|
50
|
+
1. pi `--api-key`
|
|
51
|
+
2. stored `cursor` key in `~/.pi/agent/auth.json` from `/login`
|
|
52
|
+
3. `CURSOR_API_KEY`
|
|
53
|
+
|
|
54
|
+
For live smoke and isolated harnesses:
|
|
55
|
+
|
|
56
|
+
- **Do not assume** `CURSOR_API_KEY` or `~/.secrets` alone is enough.
|
|
57
|
+
- **Do assume** pi reads auth from the active `HOME`, usually `~/.pi/agent/auth.json`.
|
|
58
|
+
- Isolated runs with `env -i HOME=/tmp/...` must **copy** `auth.json` into that temporary home before calling `pi`.
|
|
59
|
+
|
|
60
|
+
Example seed step used by `scripts/isolated-cursor-smoke.sh`:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
mkdir -p "$HOME/.pi/agent"
|
|
64
|
+
cp "$REAL_HOME/.pi/agent/auth.json" "$HOME/.pi/agent/auth.json"
|
|
65
|
+
chmod 600 "$HOME/.pi/agent/auth.json"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Fallback when `auth.json` lacks a `cursor` provider entry:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
export CURSOR_API_KEY="your-key"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Never commit, log, or paste `auth.json` contents, API keys, or session JSONL with secrets.
|
|
75
|
+
|
|
76
|
+
## Isolated directories: why and how
|
|
77
|
+
|
|
78
|
+
Use isolated `/tmp` trees when validating:
|
|
79
|
+
|
|
80
|
+
- packed tarball install (`npm pack` → extract → `pi install -l`)
|
|
81
|
+
- clean `HOME` with no inherited shell profile state
|
|
82
|
+
- plan-mode-style tool stripping via a shim extension
|
|
83
|
+
- JSONL replay-error scans independent of stdout
|
|
84
|
+
|
|
85
|
+
Recommended layout:
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
/tmp/pi-cursor-sdk-isolated-<timestamp>/
|
|
89
|
+
home/ # seeded ~/.pi/agent/auth.json
|
|
90
|
+
pack/ # npm pack output (*.tgz)
|
|
91
|
+
extract/package/ # unpacked extension
|
|
92
|
+
project/ # empty pi project for install -l
|
|
93
|
+
sessions/
|
|
94
|
+
basic/
|
|
95
|
+
native-replay/
|
|
96
|
+
plan-strip/
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Commands:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# full isolated smoke (unit preflight + pack + live pi)
|
|
103
|
+
npm run smoke:isolated
|
|
104
|
+
|
|
105
|
+
# pack/unit only, no live Cursor calls
|
|
106
|
+
SKIP_LIVE=1 npm run smoke:isolated
|
|
107
|
+
|
|
108
|
+
# custom artifact root
|
|
109
|
+
ISOLATED=/tmp/pi-cursor-sdk-isolated-manual npm run smoke:isolated
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Every live check should use its own `--session-dir` under the isolated tree. Do not reuse session dirs across scenarios.
|
|
113
|
+
|
|
114
|
+
## Harness traps we hit repeatedly
|
|
115
|
+
|
|
116
|
+
| Trap | What went wrong | Fix |
|
|
117
|
+
| --- | --- | --- |
|
|
118
|
+
| Clean `HOME` without auth | `pi` could not authenticate Cursor in isolated runs | Copy `~/.pi/agent/auth.json` into isolated `HOME` |
|
|
119
|
+
| `npm pack \| tail -1` | Captured npm notice text, not tarball path | Use `ls -t "$PACK_DIR"/*.tgz \| head -1` |
|
|
120
|
+
| Packed extension, no install | Provider never loaded in isolated project | Run `npm install --omit=dev` inside extracted package |
|
|
121
|
+
| Inherited shell env | mise/profile hooks hung or polluted runs | Use `env -i ... MISE_DISABLE=1` for isolated pi calls |
|
|
122
|
+
| No per-check timeout | One stuck prompt blocked entire harness | Wrap each live check with timeout/watchdog |
|
|
123
|
+
| stdout-only assertions | Missed replay failures persisted only in JSONL | Scan JSONL for `Tool grep/cursor/find/ls not found` |
|
|
124
|
+
| Plan strip only on first turn | Under-tested multi-turn resync | Shim strips on every `turn_start`; stress multi-turn separately |
|
|
125
|
+
| Assuming env auth equals pi auth | False "blocked" or false "pass" in CI-like shells | Check `auth.json` provider keys explicitly when needed |
|
|
126
|
+
|
|
127
|
+
## JSONL is the source of truth for replay regressions
|
|
128
|
+
|
|
129
|
+
Stdout can look fine while persisted tool results contain errors. Prefer structural JSONL scans over grepping terminal output.
|
|
130
|
+
|
|
131
|
+
Replay failure scan:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
node scripts/validate-smoke-jsonl.mjs --replay-errors-only "$SESSION_DIR"
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Combined usage + replay scan after broader smoke:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
node scripts/validate-smoke-jsonl.mjs --replay-errors "$SMOKE_DIR"
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The replay scan fails on records containing:
|
|
144
|
+
|
|
145
|
+
- `Tool grep not found`
|
|
146
|
+
- `Tool cursor not found`
|
|
147
|
+
- `Tool find not found`
|
|
148
|
+
- `Tool ls not found`
|
|
149
|
+
|
|
150
|
+
## Plan-mode regression scenario
|
|
151
|
+
|
|
152
|
+
Simulate plan-mode execute stripping with the repo fixture:
|
|
153
|
+
|
|
154
|
+
- `scripts/fixtures/plan-strip-shim/index.ts`
|
|
155
|
+
|
|
156
|
+
It sets active tools to `read`, `bash`, `edit`, `write` on each `turn_start`. Run pi with:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
pi -e scripts/fixtures/plan-strip-shim --cursor-no-fast --model cursor/composer-2.5 \
|
|
160
|
+
--session-dir "$SMOKE_DIR/plan-strip" \
|
|
161
|
+
-p 'After reset, read README.md and answer PLAN_STRIP_OK=yes.'
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Pass criteria:
|
|
165
|
+
|
|
166
|
+
- No replay `Tool * not found` entries in JSONL
|
|
167
|
+
- Native replay tools (`grep`, `find`, `read`, etc.) succeed after `turn_start` resync
|
|
168
|
+
- On non-Cursor model switch, native replay wrappers are removed except core pi tools
|
|
169
|
+
|
|
170
|
+
## Local validation ladder
|
|
171
|
+
|
|
172
|
+
Run in order before claiming release-ready for provider/runtime changes:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
npm test
|
|
176
|
+
npm run typecheck
|
|
177
|
+
npm pack --dry-run
|
|
178
|
+
SKIP_LIVE=1 npm run smoke:isolated
|
|
179
|
+
npm run smoke:isolated # requires auth.json or CURSOR_API_KEY
|
|
180
|
+
npm run smoke:live # partial tmux checklist subset
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Then follow the full manual [Cursor live smoke checklist](./cursor-live-smoke-checklist.md) for surfaces the scripts do not cover (bridge MCP, abort/cancel, full TUI observation, packaging review, cleanup).
|
|
184
|
+
|
|
185
|
+
## What belongs in CI vs manual smoke
|
|
186
|
+
|
|
187
|
+
- **CI / default `npm test`:** mocked provider tests, extension lifecycle tests, JSONL validator tests, script syntax/help checks. No live Cursor calls.
|
|
188
|
+
- **Manual / pre-release:** `npm run smoke:isolated`, `npm run smoke:live`, and the full checklist. Requires real Cursor auth and observes TUI/runtime behavior mocks cannot reproduce.
|
|
189
|
+
|
|
190
|
+
If live smoke auth is unavailable, report the release as **blocked**, not skipped-ready.
|
|
191
|
+
|
|
192
|
+
## Related docs and scripts
|
|
193
|
+
|
|
194
|
+
- [Cursor live smoke checklist](./cursor-live-smoke-checklist.md)
|
|
195
|
+
- [Cursor native tool replay](./cursor-native-tool-replay.md)
|
|
196
|
+
- `scripts/isolated-cursor-smoke.sh`
|
|
197
|
+
- `scripts/tmux-live-smoke.sh`
|
|
198
|
+
- `scripts/validate-smoke-jsonl.mjs`
|
|
199
|
+
- `test/helpers/cursor-provider-harness.ts` — controllable native replay pi mock (`createNativeToolDisplayPiForTest`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cursor-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"description": "pi provider extension backed by @cursor/sdk local agents",
|
|
5
5
|
"author": "Mitch Fultz (https://github.com/fitchmultz)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -26,10 +26,12 @@
|
|
|
26
26
|
"scripts/refresh-cursor-model-snapshots.mjs",
|
|
27
27
|
"scripts/steering-rpc-smoke.mjs",
|
|
28
28
|
"scripts/tmux-live-smoke.sh",
|
|
29
|
+
"scripts/isolated-cursor-smoke.sh",
|
|
29
30
|
"scripts/validate-smoke-jsonl.mjs",
|
|
30
31
|
"README.md",
|
|
31
32
|
"docs/cursor-model-ux-spec.md",
|
|
32
33
|
"docs/cursor-live-smoke-checklist.md",
|
|
34
|
+
"docs/cursor-testing-lessons.md",
|
|
33
35
|
"docs/cursor-native-tool-replay.md",
|
|
34
36
|
"docs/cursor-native-tool-visual-audit.md",
|
|
35
37
|
"LICENSE",
|
|
@@ -45,6 +47,7 @@
|
|
|
45
47
|
"test:watch": "vitest",
|
|
46
48
|
"refresh:cursor-snapshots": "node scripts/refresh-cursor-model-snapshots.mjs",
|
|
47
49
|
"smoke:live": "scripts/tmux-live-smoke.sh",
|
|
50
|
+
"smoke:isolated": "scripts/isolated-cursor-smoke.sh",
|
|
48
51
|
"smoke:steering": "node scripts/steering-rpc-smoke.mjs",
|
|
49
52
|
"smoke:jsonl": "node scripts/validate-smoke-jsonl.mjs"
|
|
50
53
|
},
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Isolated /tmp install + fail-fast live smoke for pi-cursor-sdk native replay.
|
|
3
|
+
#
|
|
4
|
+
# Validates packed extension load, plan-strip resync, and absence of "Tool * not found".
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
8
|
+
REAL_HOME="${REAL_HOME:-$HOME}"
|
|
9
|
+
PI_AGENT_DIR="${PI_AGENT_DIR:-$REAL_HOME/.pi/agent}"
|
|
10
|
+
AUTH_JSON="${AUTH_JSON:-$PI_AGENT_DIR/auth.json}"
|
|
11
|
+
REPO="${REPO:-$ROOT}"
|
|
12
|
+
ISOLATED="${ISOLATED:-/tmp/pi-cursor-sdk-isolated-$(date +%Y%m%dT%H%M%S)}"
|
|
13
|
+
PI_LIVE_TIMEOUT="${PI_LIVE_TIMEOUT:-45}"
|
|
14
|
+
SKIP_LIVE="${SKIP_LIVE:-0}"
|
|
15
|
+
SKIP_UNIT="${SKIP_UNIT:-0}"
|
|
16
|
+
PI_BIN="${PI_BIN:-pi}"
|
|
17
|
+
PI_PATH="${PI_PATH:-/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin}"
|
|
18
|
+
|
|
19
|
+
PACK_DIR="$ISOLATED/pack"
|
|
20
|
+
EXTRACT_DIR="$ISOLATED/extract"
|
|
21
|
+
PROJECT_DIR="$ISOLATED/project"
|
|
22
|
+
SESSION_ROOT="$ISOLATED/sessions"
|
|
23
|
+
SHIM_DIR="$ROOT/scripts/fixtures/plan-strip-shim"
|
|
24
|
+
HOME_DIR="$ISOLATED/home"
|
|
25
|
+
|
|
26
|
+
print_help() {
|
|
27
|
+
cat <<EOF
|
|
28
|
+
Isolated /tmp install smoke for pi-cursor-sdk (native replay + plan-strip resync).
|
|
29
|
+
|
|
30
|
+
Usage:
|
|
31
|
+
./scripts/isolated-cursor-smoke.sh
|
|
32
|
+
SKIP_LIVE=1 ./scripts/isolated-cursor-smoke.sh
|
|
33
|
+
PI_LIVE_TIMEOUT=90 ./scripts/isolated-cursor-smoke.sh
|
|
34
|
+
|
|
35
|
+
Environment:
|
|
36
|
+
REPO Repo under test (default: script parent directory).
|
|
37
|
+
ISOLATED Artifact root (default: /tmp/pi-cursor-sdk-isolated-<timestamp>).
|
|
38
|
+
REAL_HOME Source for auth.json (default: \$HOME).
|
|
39
|
+
AUTH_JSON Path to pi auth.json to seed isolated HOME (default: ~/.pi/agent/auth.json).
|
|
40
|
+
PI_LIVE_TIMEOUT Per live pi check timeout in seconds (default: 45).
|
|
41
|
+
PI_BIN pi executable (default: pi on PATH).
|
|
42
|
+
PI_PATH PATH for isolated pi runs.
|
|
43
|
+
SKIP_LIVE=1 Run unit tests + pack only; skip live Cursor calls.
|
|
44
|
+
SKIP_UNIT=1 Skip repo unit tests (live checks only).
|
|
45
|
+
CURSOR_API_KEY Optional fallback when auth.json lacks cursor provider.
|
|
46
|
+
|
|
47
|
+
Prerequisites:
|
|
48
|
+
node, npm, pi, rg, python3 on PATH
|
|
49
|
+
~/.pi/agent/auth.json with cursor provider OR CURSOR_API_KEY
|
|
50
|
+
|
|
51
|
+
Exit codes:
|
|
52
|
+
0 all requested checks passed
|
|
53
|
+
1 prerequisite, unit, pack, live smoke, or JSONL replay validation failure
|
|
54
|
+
EOF
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
log() {
|
|
58
|
+
printf '[isolated-smoke] %s\n' "$*"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fail() {
|
|
62
|
+
printf '[isolated-smoke] FAIL: %s\n' "$*" >&2
|
|
63
|
+
exit 1
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
seed_pi_agent_home() {
|
|
67
|
+
local home="$1"
|
|
68
|
+
mkdir -p "$home/.pi/agent"
|
|
69
|
+
if [[ -f "$AUTH_JSON" ]]; then
|
|
70
|
+
cp "$AUTH_JSON" "$home/.pi/agent/auth.json"
|
|
71
|
+
chmod 600 "$home/.pi/agent/auth.json"
|
|
72
|
+
log "seeded $home/.pi/agent/auth.json"
|
|
73
|
+
else
|
|
74
|
+
log "WARN: no auth.json at $AUTH_JSON"
|
|
75
|
+
fi
|
|
76
|
+
if [[ -f "$PI_AGENT_DIR/models.json" ]]; then
|
|
77
|
+
cp "$PI_AGENT_DIR/models.json" "$home/.pi/agent/models.json"
|
|
78
|
+
fi
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
has_auth_provider() {
|
|
82
|
+
local provider="$1"
|
|
83
|
+
python3 - "$provider" "$HOME_DIR/.pi/agent/auth.json" <<'PY'
|
|
84
|
+
import json, sys
|
|
85
|
+
provider, path = sys.argv[1], sys.argv[2]
|
|
86
|
+
try:
|
|
87
|
+
data = json.load(open(path))
|
|
88
|
+
except FileNotFoundError:
|
|
89
|
+
sys.exit(1)
|
|
90
|
+
sys.exit(0 if provider in data and data[provider] else 1)
|
|
91
|
+
PY
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
run_with_timeout() {
|
|
95
|
+
local label="$1"
|
|
96
|
+
local seconds="$2"
|
|
97
|
+
shift 2
|
|
98
|
+
log "$label (timeout ${seconds}s)"
|
|
99
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
100
|
+
timeout --foreground "${seconds}s" "$@" || {
|
|
101
|
+
local rc=$?
|
|
102
|
+
[[ $rc -eq 124 ]] && fail "$label timed out after ${seconds}s"
|
|
103
|
+
fail "$label exited $rc"
|
|
104
|
+
}
|
|
105
|
+
return
|
|
106
|
+
fi
|
|
107
|
+
if command -v gtimeout >/dev/null 2>&1; then
|
|
108
|
+
gtimeout "${seconds}s" "$@" || {
|
|
109
|
+
local rc=$?
|
|
110
|
+
[[ $rc -eq 124 ]] && fail "$label timed out after ${seconds}s"
|
|
111
|
+
fail "$label exited $rc"
|
|
112
|
+
}
|
|
113
|
+
return
|
|
114
|
+
fi
|
|
115
|
+
"$@" &
|
|
116
|
+
local pid=$!
|
|
117
|
+
local waited=0
|
|
118
|
+
while kill -0 "$pid" 2>/dev/null; do
|
|
119
|
+
if (( waited >= seconds )); then
|
|
120
|
+
kill -TERM "$pid" 2>/dev/null || true
|
|
121
|
+
sleep 1
|
|
122
|
+
kill -KILL "$pid" 2>/dev/null || true
|
|
123
|
+
fail "$label timed out after ${seconds}s"
|
|
124
|
+
fi
|
|
125
|
+
sleep 1
|
|
126
|
+
waited=$((waited + 1))
|
|
127
|
+
done
|
|
128
|
+
wait "$pid" || fail "$label exited $?"
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
validate_replay_jsonl() {
|
|
132
|
+
local dir="$1"
|
|
133
|
+
node "$ROOT/scripts/validate-smoke-jsonl.mjs" --replay-errors-only "$dir"
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
|
137
|
+
print_help
|
|
138
|
+
exit 0
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
if [[ -f "${SECRETS_FILE:-$REAL_HOME/.secrets}" ]]; then
|
|
142
|
+
set +u
|
|
143
|
+
# shellcheck disable=SC1090
|
|
144
|
+
source "${SECRETS_FILE:-$REAL_HOME/.secrets}"
|
|
145
|
+
set -u
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
command -v node >/dev/null || fail "missing node"
|
|
149
|
+
command -v npm >/dev/null || fail "missing npm"
|
|
150
|
+
command -v rg >/dev/null || fail "missing rg"
|
|
151
|
+
command -v python3 >/dev/null || fail "missing python3"
|
|
152
|
+
|
|
153
|
+
mkdir -p "$PACK_DIR" "$EXTRACT_DIR" "$PROJECT_DIR" "$SESSION_ROOT" "$HOME_DIR"
|
|
154
|
+
seed_pi_agent_home "$HOME_DIR"
|
|
155
|
+
|
|
156
|
+
log "isolated root: $ISOLATED"
|
|
157
|
+
log "HOME=$HOME_DIR"
|
|
158
|
+
|
|
159
|
+
if [[ "$SKIP_UNIT" != "1" ]]; then
|
|
160
|
+
log "preflight: repo unit tests"
|
|
161
|
+
run_with_timeout "npm test" 120 bash -lc "cd '$REPO' && npm test"
|
|
162
|
+
fi
|
|
163
|
+
|
|
164
|
+
if [[ "$SKIP_LIVE" == "1" ]]; then
|
|
165
|
+
log "SKIP_LIVE=1 — skipping live pi checks"
|
|
166
|
+
exit 0
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
if ! has_auth_provider cursor && [[ -z "${CURSOR_API_KEY:-}" ]]; then
|
|
170
|
+
fail "no cursor auth in $HOME_DIR/.pi/agent/auth.json and CURSOR_API_KEY unset"
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
command -v "$PI_BIN" >/dev/null || fail "PI_BIN not found: $PI_BIN"
|
|
174
|
+
|
|
175
|
+
log "npm pack from $REPO"
|
|
176
|
+
(cd "$REPO" && npm pack --pack-destination "$PACK_DIR" >/dev/null 2>&1)
|
|
177
|
+
PACK_TGZ="$(ls -t "$PACK_DIR"/*.tgz | head -1)"
|
|
178
|
+
[[ -f "$PACK_TGZ" ]] || fail "missing pack tarball"
|
|
179
|
+
tar -xzf "$PACK_TGZ" -C "$EXTRACT_DIR"
|
|
180
|
+
[[ -d "$EXTRACT_DIR/package" ]] || fail "extract missing package/ dir"
|
|
181
|
+
|
|
182
|
+
log "npm install packed extension deps"
|
|
183
|
+
run_with_timeout "npm install --omit=dev" 120 bash -lc "cd '$EXTRACT_DIR/package' && npm install --omit=dev >/dev/null 2>&1"
|
|
184
|
+
|
|
185
|
+
log "pi install -l (clean HOME)"
|
|
186
|
+
cp "$REPO/README.md" "$PROJECT_DIR/README.md"
|
|
187
|
+
run_with_timeout "pi install" 30 env -i HOME="$HOME_DIR" PATH="$PI_PATH" MISE_DISABLE=1 \
|
|
188
|
+
bash -c "cd '$PROJECT_DIR' && '$PI_BIN' install -l '$EXTRACT_DIR/package' >/dev/null"
|
|
189
|
+
|
|
190
|
+
run_with_timeout "pi list" 15 env -i HOME="$HOME_DIR" PATH="$PI_PATH" MISE_DISABLE=1 \
|
|
191
|
+
bash -c "cd '$PROJECT_DIR' && '$PI_BIN' list" | rg -q "extract/package" || fail "packed extension not installed"
|
|
192
|
+
|
|
193
|
+
PI_ENV=(HOME="$HOME_DIR" PATH="$PI_PATH" MISE_DISABLE=1 PI_CURSOR_SETTING_SOURCES=none)
|
|
194
|
+
if [[ -n "${CURSOR_API_KEY:-}" ]]; then
|
|
195
|
+
PI_ENV+=(CURSOR_API_KEY="$CURSOR_API_KEY")
|
|
196
|
+
fi
|
|
197
|
+
|
|
198
|
+
log "check: list-models"
|
|
199
|
+
LIST_OUT="$ISOLATED/list-models.txt"
|
|
200
|
+
run_with_timeout "list-models" 30 env -i "${PI_ENV[@]}" \
|
|
201
|
+
bash -c "cd '$PROJECT_DIR' && '$PI_BIN' --cursor-no-fast --list-models cursor > '$LIST_OUT' 2>&1"
|
|
202
|
+
rg -q "composer-2\\.5|composer-2-5" "$LIST_OUT" || fail "composer-2.5 not listed (see $LIST_OUT)"
|
|
203
|
+
|
|
204
|
+
log "check: basic provider prompt"
|
|
205
|
+
BASIC_DIR="$SESSION_ROOT/basic"
|
|
206
|
+
mkdir -p "$BASIC_DIR"
|
|
207
|
+
run_with_timeout "basic prompt" "$PI_LIVE_TIMEOUT" env -i "${PI_ENV[@]}" \
|
|
208
|
+
bash -c "cd '$PROJECT_DIR' && '$PI_BIN' --cursor-no-fast --model cursor/composer-2.5 --session-dir '$BASIC_DIR' --no-tools -p 'Reply exactly: PI_CURSOR_ISOLATED_OK' > '$ISOLATED/basic.stdout.txt' 2> '$ISOLATED/basic.stderr.txt'"
|
|
209
|
+
rg -q "PI_CURSOR_ISOLATED_OK" "$ISOLATED/basic.stdout.txt" || fail "basic prompt missing PI_CURSOR_ISOLATED_OK"
|
|
210
|
+
validate_replay_jsonl "$BASIC_DIR"
|
|
211
|
+
|
|
212
|
+
log "check: native replay"
|
|
213
|
+
REPLAY_DIR="$SESSION_ROOT/native-replay"
|
|
214
|
+
mkdir -p "$REPLAY_DIR"
|
|
215
|
+
run_with_timeout "native replay" "$PI_LIVE_TIMEOUT" env -i "${PI_ENV[@]}" PI_CURSOR_NATIVE_TOOL_DISPLAY=1 \
|
|
216
|
+
bash -c "cd '$PROJECT_DIR' && '$PI_BIN' --cursor-no-fast --model cursor/composer-2.5 --session-dir '$REPLAY_DIR' -p 'Read ./README.md briefly, then answer README_SEEN=yes if it mentions pi-cursor-sdk.' > '$ISOLATED/replay.stdout.txt' 2> '$ISOLATED/replay.stderr.txt'"
|
|
217
|
+
validate_replay_jsonl "$REPLAY_DIR"
|
|
218
|
+
|
|
219
|
+
log "check: plan-strip shim (plan-mode execute reset)"
|
|
220
|
+
PLAN_DIR="$SESSION_ROOT/plan-strip"
|
|
221
|
+
mkdir -p "$PLAN_DIR"
|
|
222
|
+
run_with_timeout "plan-strip replay" "$PI_LIVE_TIMEOUT" env -i "${PI_ENV[@]}" PI_CURSOR_NATIVE_TOOL_DISPLAY=1 \
|
|
223
|
+
bash -c "cd '$PROJECT_DIR' && '$PI_BIN' -e '$SHIM_DIR' --cursor-no-fast --model cursor/composer-2.5 --session-dir '$PLAN_DIR' -p 'After reset, read README.md and answer PLAN_STRIP_OK=yes.' > '$ISOLATED/plan.stdout.txt' 2> '$ISOLATED/plan.stderr.txt'"
|
|
224
|
+
validate_replay_jsonl "$PLAN_DIR"
|
|
225
|
+
|
|
226
|
+
log "PASS isolated install smoke: $ISOLATED"
|
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
6
6
|
import { join, relative } from "node:path";
|
|
7
7
|
|
|
8
|
+
const REPLAY_TOOL_NOT_FOUND = [
|
|
9
|
+
"Tool grep not found",
|
|
10
|
+
"Tool cursor not found",
|
|
11
|
+
"Tool find not found",
|
|
12
|
+
"Tool ls not found",
|
|
13
|
+
];
|
|
14
|
+
|
|
8
15
|
function printHelp() {
|
|
9
16
|
console.log(`Validate assistant presence and usage metadata in pi smoke session JSONL files.
|
|
10
17
|
|
|
@@ -18,21 +25,26 @@ Arguments:
|
|
|
18
25
|
|
|
19
26
|
Options:
|
|
20
27
|
-h, --help Show this help.
|
|
28
|
+
--replay-errors Also fail when JSONL contains native replay "Tool * not found" errors.
|
|
29
|
+
--replay-errors-only Scan only for native replay "Tool * not found" errors (skip usage checks).
|
|
21
30
|
|
|
22
31
|
Exit codes:
|
|
23
|
-
0 every
|
|
24
|
-
1 invalid arguments, unreadable directory, invalid JSONL, empty/no-assistant files,
|
|
32
|
+
0 every enforced invariant passed for the selected mode(s)
|
|
33
|
+
1 invalid arguments, unreadable directory, invalid JSONL, empty/no-assistant files, usage validation failures, or replay tool errors
|
|
25
34
|
2 no JSONL files found under the smoke directory
|
|
26
35
|
|
|
27
|
-
Enforced invariants:
|
|
36
|
+
Enforced invariants (default mode):
|
|
28
37
|
- each scanned JSONL file contains parseable JSONL records
|
|
29
38
|
- each scanned JSONL file contains at least one persisted assistant message
|
|
30
39
|
- every persisted assistant message has usage metadata
|
|
31
40
|
- assistant usage input/output/totalTokens are non-negative numbers
|
|
32
41
|
- assistant usage cacheRead/cacheWrite are exactly 0
|
|
33
42
|
|
|
43
|
+
Replay error scan (--replay-errors / --replay-errors-only):
|
|
44
|
+
- no JSONL record contains "Tool grep/cursor/find/ls not found"
|
|
45
|
+
|
|
34
46
|
Notes:
|
|
35
|
-
- Prints one JSON summary line per scanned session file.
|
|
47
|
+
- Prints one JSON summary line per scanned session file (usage mode) or one replay summary line (replay-only mode).
|
|
36
48
|
- Does not print session message contents or secrets.`);
|
|
37
49
|
}
|
|
38
50
|
|
|
@@ -88,6 +100,19 @@ function parseJsonlFile(file) {
|
|
|
88
100
|
return { lineCount: lines.length, records, parseErrorCount };
|
|
89
101
|
}
|
|
90
102
|
|
|
103
|
+
function scanReplayErrors(file, records) {
|
|
104
|
+
const hits = [];
|
|
105
|
+
for (const [index, record] of records.entries()) {
|
|
106
|
+
const blob = JSON.stringify(record);
|
|
107
|
+
for (const needle of REPLAY_TOOL_NOT_FOUND) {
|
|
108
|
+
if (blob.includes(needle)) {
|
|
109
|
+
hits.push({ line: index + 1, needle });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return hits;
|
|
114
|
+
}
|
|
115
|
+
|
|
91
116
|
function main() {
|
|
92
117
|
const args = process.argv.slice(2);
|
|
93
118
|
if (args.includes("-h") || args.includes("--help")) {
|
|
@@ -95,11 +120,15 @@ function main() {
|
|
|
95
120
|
return;
|
|
96
121
|
}
|
|
97
122
|
|
|
98
|
-
|
|
123
|
+
const replayErrorsOnly = args.includes("--replay-errors-only");
|
|
124
|
+
const replayErrors = replayErrorsOnly || args.includes("--replay-errors");
|
|
125
|
+
const positional = args.filter((arg) => !arg.startsWith("-"));
|
|
126
|
+
|
|
127
|
+
if (positional.length > 1) {
|
|
99
128
|
fail("too many arguments; pass only the smoke directory");
|
|
100
129
|
}
|
|
101
130
|
|
|
102
|
-
const smokeDir =
|
|
131
|
+
const smokeDir = positional[0] ?? process.env.SMOKE_DIR;
|
|
103
132
|
if (!smokeDir) {
|
|
104
133
|
fail("missing smoke directory; pass a path or set SMOKE_DIR");
|
|
105
134
|
}
|
|
@@ -117,6 +146,24 @@ function main() {
|
|
|
117
146
|
}
|
|
118
147
|
|
|
119
148
|
let failures = 0;
|
|
149
|
+
if (replayErrorsOnly) {
|
|
150
|
+
let replayHitCount = 0;
|
|
151
|
+
for (const file of files) {
|
|
152
|
+
const { records } = parseJsonlFile(file);
|
|
153
|
+
const hits = scanReplayErrors(file, records);
|
|
154
|
+
replayHitCount += hits.length;
|
|
155
|
+
if (hits.length > 0) failures += 1;
|
|
156
|
+
console.log(
|
|
157
|
+
JSON.stringify({
|
|
158
|
+
file: relative(smokeDir, file),
|
|
159
|
+
replayErrorCount: hits.length,
|
|
160
|
+
replayErrors: hits.slice(0, 5),
|
|
161
|
+
}),
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
process.exit(failures === 0 ? 0 : 1);
|
|
165
|
+
}
|
|
166
|
+
|
|
120
167
|
for (const file of files) {
|
|
121
168
|
let summary;
|
|
122
169
|
try {
|
|
@@ -125,7 +172,14 @@ function main() {
|
|
|
125
172
|
const assistants = messages.filter((message) => message?.role === "assistant");
|
|
126
173
|
const usage = assistants.map((message) => message.usage).filter(Boolean);
|
|
127
174
|
const badUsage = assistants.map((message) => message.usage).filter(isBadUsage);
|
|
128
|
-
const
|
|
175
|
+
const replayHits = replayErrors ? scanReplayErrors(file, records) : [];
|
|
176
|
+
const fileFailure =
|
|
177
|
+
lineCount === 0 ||
|
|
178
|
+
parseErrorCount > 0 ||
|
|
179
|
+
assistants.length === 0 ||
|
|
180
|
+
usage.length !== assistants.length ||
|
|
181
|
+
badUsage.length > 0 ||
|
|
182
|
+
replayHits.length > 0;
|
|
129
183
|
if (fileFailure) failures += 1;
|
|
130
184
|
summary = {
|
|
131
185
|
file: relative(smokeDir, file),
|
|
@@ -135,6 +189,7 @@ function main() {
|
|
|
135
189
|
assistantCount: assistants.length,
|
|
136
190
|
usageCount: usage.length,
|
|
137
191
|
badUsageCount: badUsage.length,
|
|
192
|
+
replayErrorCount: replayHits.length,
|
|
138
193
|
};
|
|
139
194
|
} catch (error) {
|
|
140
195
|
failures += 1;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Context } from "@earendil-works/pi-ai";
|
|
2
|
+
|
|
3
|
+
/** Tool names from the provider context snapshot at stream start (not live pi.getActiveTools()). */
|
|
4
|
+
export function getActiveContextToolNames(context: Context): ReadonlySet<string> | undefined {
|
|
5
|
+
return context.tools ? new Set(context.tools.map((tool) => tool.name)) : undefined;
|
|
6
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Canonical single-line sanitization and truncation for Cursor replay/trace display. */
|
|
2
|
+
export function sanitizeCursorDisplayLine(value: string): string {
|
|
3
|
+
return value.replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim();
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function truncateCursorDisplayLine(value: string, maxLength = 240): string {
|
|
7
|
+
const sanitized = sanitizeCursorDisplayLine(value);
|
|
8
|
+
if (sanitized.length <= maxLength) return sanitized;
|
|
9
|
+
return `${sanitized.slice(0, maxLength - 1)}…`;
|
|
10
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { canRenderCursorToolNatively } from "./cursor-native-tool-display.js";
|
|
2
|
+
import { getActiveContextToolNames } from "./cursor-context-tools.js";
|
|
3
|
+
import type { Context } from "@earendil-works/pi-ai";
|
|
4
|
+
|
|
5
|
+
export type NativeReplayDisposition = "queue_replay" | "inactive_trace" | "transcript_trace";
|
|
6
|
+
|
|
7
|
+
export interface NativeReplayRoutingInput {
|
|
8
|
+
toolName: string;
|
|
9
|
+
useNativeToolReplay: boolean;
|
|
10
|
+
activeToolNames?: ReadonlySet<string>;
|
|
11
|
+
hasLiveRun: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isNativeToolActiveInContext(toolName: string, activeToolNames?: ReadonlySet<string>): boolean {
|
|
15
|
+
return activeToolNames === undefined || activeToolNames.has(toolName);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Canonical native replay routing for coordinator and live-run drain.
|
|
20
|
+
* Extension resync (pi active tools) is separate; this uses context.tools snapshot only.
|
|
21
|
+
*/
|
|
22
|
+
export function resolveNativeReplayDisposition(input: NativeReplayRoutingInput): NativeReplayDisposition {
|
|
23
|
+
if (!input.useNativeToolReplay || !canRenderCursorToolNatively(input.toolName)) {
|
|
24
|
+
return "transcript_trace";
|
|
25
|
+
}
|
|
26
|
+
if (isNativeToolActiveInContext(input.toolName, input.activeToolNames) && input.hasLiveRun) {
|
|
27
|
+
return "queue_replay";
|
|
28
|
+
}
|
|
29
|
+
if (!isNativeToolActiveInContext(input.toolName, input.activeToolNames)) {
|
|
30
|
+
return "inactive_trace";
|
|
31
|
+
}
|
|
32
|
+
return "transcript_trace";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function partitionNativeToolsByActiveContext<T extends { toolName: string }>(
|
|
36
|
+
context: Context,
|
|
37
|
+
tools: readonly T[],
|
|
38
|
+
): { active: T[]; inactive: T[] } {
|
|
39
|
+
const activeToolNames = getActiveContextToolNames(context);
|
|
40
|
+
if (!activeToolNames) return { active: [...tools], inactive: [] };
|
|
41
|
+
const active: T[] = [];
|
|
42
|
+
const inactive: T[] = [];
|
|
43
|
+
for (const tool of tools) {
|
|
44
|
+
if (activeToolNames.has(tool.toolName)) active.push(tool);
|
|
45
|
+
else inactive.push(tool);
|
|
46
|
+
}
|
|
47
|
+
return { active, inactive };
|
|
48
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { CursorPiToolDisplay } from "./cursor-tool-transcript.js";
|
|
2
|
+
import { asRecord } from "./cursor-record-utils.js";
|
|
3
|
+
import { truncateCursorDisplayLine } from "./cursor-display-text.js";
|
|
4
|
+
|
|
5
|
+
function getCursorReplayResultText(display: CursorPiToolDisplay): string | undefined {
|
|
6
|
+
for (const content of display.result.content) {
|
|
7
|
+
if (content.type !== "text") continue;
|
|
8
|
+
const text = truncateCursorDisplayLine(content.text);
|
|
9
|
+
if (text) return text;
|
|
10
|
+
}
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Unified inactive native-replay fallback: `title: summary` in thinking trace. */
|
|
15
|
+
export function formatInactiveCursorReplayTrace(display: CursorPiToolDisplay): string {
|
|
16
|
+
const details = asRecord(display.result.details);
|
|
17
|
+
const args = asRecord(display.args);
|
|
18
|
+
const title = typeof details?.title === "string" && details.title.trim()
|
|
19
|
+
? details.title.trim()
|
|
20
|
+
: typeof args?.activityTitle === "string" && args.activityTitle.trim()
|
|
21
|
+
? args.activityTitle.trim()
|
|
22
|
+
: `Cursor ${display.toolName}`;
|
|
23
|
+
const summary = typeof details?.summary === "string" && details.summary.trim()
|
|
24
|
+
? details.summary.trim()
|
|
25
|
+
: typeof args?.activitySummary === "string" && args.activitySummary.trim()
|
|
26
|
+
? args.activitySummary.trim()
|
|
27
|
+
: getCursorReplayResultText(display) ?? "completed";
|
|
28
|
+
return `${truncateCursorDisplayLine(title)}: ${truncateCursorDisplayLine(summary)}\n`;
|
|
29
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionContext, ExtensionHandler, SessionStartEvent } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { BeforeAgentStartEvent, ExtensionAPI, ExtensionContext, ExtensionHandler, SessionStartEvent, TurnStartEvent } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import {
|
|
3
3
|
CURSOR_MODEL_ACTIVE_REPLAY_TOOL_NAMES,
|
|
4
|
-
CURSOR_REPLAY_TOOL_NAMES,
|
|
5
4
|
isNativeCursorToolName,
|
|
6
5
|
NATIVE_CURSOR_TOOL_NAMES,
|
|
7
6
|
registerNativeCursorTool,
|
|
@@ -16,10 +15,14 @@ import {
|
|
|
16
15
|
} from "./cursor-native-tool-display-state.js";
|
|
17
16
|
import { isCursorReplayToolName } from "./cursor-tool-names.js";
|
|
18
17
|
|
|
18
|
+
const CORE_PI_TOOL_NAMES = new Set(["read", "bash", "edit", "write"]);
|
|
19
|
+
|
|
19
20
|
type CursorNativeToolRegistryApi = Pick<ExtensionAPI, "getActiveTools" | "getAllTools" | "registerTool" | "setActiveTools">;
|
|
20
21
|
|
|
21
22
|
export interface CursorNativeToolDisplayExtensionApi extends CursorNativeToolRegistryApi {
|
|
22
23
|
on(event: "session_start", handler: ExtensionHandler<SessionStartEvent>): void;
|
|
24
|
+
on(event: "before_agent_start", handler: ExtensionHandler<BeforeAgentStartEvent>): void;
|
|
25
|
+
on(event: "turn_start", handler: ExtensionHandler<TurnStartEvent>): void;
|
|
23
26
|
on(event: "model_select", handler: (event: { model: ExtensionContext["model"] }, ctx: ExtensionContext) => Promise<void> | void): void;
|
|
24
27
|
}
|
|
25
28
|
|
|
@@ -46,7 +49,8 @@ export function syncRegisteredNativeCursorToolsForModel(pi: Pick<ExtensionAPI, "
|
|
|
46
49
|
changed = true;
|
|
47
50
|
}
|
|
48
51
|
} else {
|
|
49
|
-
for (const toolName of
|
|
52
|
+
for (const toolName of registeredNativeToolNames) {
|
|
53
|
+
if (CORE_PI_TOOL_NAMES.has(toolName)) continue;
|
|
50
54
|
if (!activeToolNames.delete(toolName)) continue;
|
|
51
55
|
changed = true;
|
|
52
56
|
}
|
|
@@ -85,6 +89,12 @@ export function registerCursorNativeToolDisplay(pi: CursorNativeToolDisplayExten
|
|
|
85
89
|
pi.on("session_start", (_event, ctx) => {
|
|
86
90
|
registerAvailableNativeCursorTools(pi, ctx);
|
|
87
91
|
});
|
|
92
|
+
pi.on("before_agent_start", (_event, ctx) => {
|
|
93
|
+
syncRegisteredNativeCursorToolsForModel(pi, ctx.model);
|
|
94
|
+
});
|
|
95
|
+
pi.on("turn_start", (_event, ctx) => {
|
|
96
|
+
syncRegisteredNativeCursorToolsForModel(pi, ctx.model);
|
|
97
|
+
});
|
|
88
98
|
pi.on("model_select", (event) => {
|
|
89
99
|
syncRegisteredNativeCursorToolsForModel(pi, event.model);
|
|
90
100
|
});
|
|
@@ -23,6 +23,8 @@ import { resetSessionCursorAgent } from "./cursor-session-agent.js";
|
|
|
23
23
|
import { applyCursorApproximateUsage } from "./cursor-usage-accounting.js";
|
|
24
24
|
import { CursorPartialContentEmitter } from "./cursor-partial-content-emitter.js";
|
|
25
25
|
import { hasUsableText } from "./cursor-record-utils.js";
|
|
26
|
+
import { formatInactiveCursorReplayTrace } from "./cursor-native-replay-trace.js";
|
|
27
|
+
import { partitionNativeToolsByActiveContext } from "./cursor-native-replay-routing.js";
|
|
26
28
|
|
|
27
29
|
export const DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS = 5 * 60 * 1000;
|
|
28
30
|
const CURSOR_NATIVE_REPLAY_TOOL_ID_PATTERN = /^(cursor-replay-\d+-\d+)-tool-\d+$/;
|
|
@@ -200,6 +202,13 @@ function emitCursorNativeToolUseTurn(
|
|
|
200
202
|
cursorLiveRuns.requestIdleDispose(run);
|
|
201
203
|
}
|
|
202
204
|
|
|
205
|
+
function emitInactiveCursorReplayTrace(turn: CursorLiveTurnState, tools: CursorNativeToolDisplayItem[]): void {
|
|
206
|
+
if (tools.length === 0) return;
|
|
207
|
+
for (const tool of tools) {
|
|
208
|
+
turn.emitter.appendThinkingBlock(formatInactiveCursorReplayTrace(tool));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
203
212
|
function emitCursorBridgeToolUseTurn(
|
|
204
213
|
stream: AssistantMessageEventStream,
|
|
205
214
|
partial: AssistantMessage,
|
|
@@ -229,24 +238,30 @@ function emitCursorBridgeToolUseTurn(
|
|
|
229
238
|
}
|
|
230
239
|
|
|
231
240
|
async function emitCursorLiveRunPendingToolUseTurn(
|
|
241
|
+
turn: CursorLiveTurnState,
|
|
232
242
|
stream: AssistantMessageEventStream,
|
|
233
243
|
partial: AssistantMessage,
|
|
234
244
|
model: Model<Api>,
|
|
235
245
|
context: Context,
|
|
236
246
|
run: CursorLiveRun,
|
|
237
247
|
toolResultInputTokens: number,
|
|
238
|
-
signal?: AbortSignal,
|
|
239
|
-
|
|
240
|
-
): Promise<"tool_use" | undefined> {
|
|
248
|
+
options: { mode: CursorLiveRunDrainMode; signal?: AbortSignal },
|
|
249
|
+
): Promise<"tool_use" | "handled" | undefined> {
|
|
241
250
|
const eventType = cursorLiveRuns.peekEvent(run)?.type;
|
|
242
251
|
if (eventType !== "tool" && eventType !== "bridge-tool") return undefined;
|
|
243
252
|
await settleCursorLiveToolBatch(run);
|
|
244
|
-
if (signal?.aborted) throw new CursorLiveRunAbortError();
|
|
245
|
-
beforeEmit?.();
|
|
253
|
+
if (options.signal?.aborted) throw new CursorLiveRunAbortError();
|
|
246
254
|
if (eventType === "tool") {
|
|
247
|
-
const
|
|
248
|
-
|
|
255
|
+
const { active, inactive } = partitionNativeToolsByActiveContext(context, cursorLiveRuns.collectNativeToolBatch(run));
|
|
256
|
+
if (options.mode === "emit") emitInactiveCursorReplayTrace(turn, inactive);
|
|
257
|
+
if (active.length === 0) {
|
|
258
|
+
// Inactive-only batch: trace was emitted above; do not emit toolUse.
|
|
259
|
+
return "handled";
|
|
260
|
+
}
|
|
261
|
+
if (options.mode === "emit") turn.emitter.closeAll();
|
|
262
|
+
emitCursorNativeToolUseTurn(stream, partial, model, context, run, toolResultInputTokens, active);
|
|
249
263
|
} else {
|
|
264
|
+
if (options.mode === "emit") turn.emitter.closeAll();
|
|
250
265
|
const requests = cursorLiveRuns.collectBridgeToolBatch(run);
|
|
251
266
|
emitCursorBridgeToolUseTurn(stream, partial, model, context, run, toolResultInputTokens, requests);
|
|
252
267
|
}
|
|
@@ -275,16 +290,17 @@ export async function drainCursorLiveRunTurn(
|
|
|
275
290
|
|
|
276
291
|
while (cursorLiveRuns.peekEvent(run)) {
|
|
277
292
|
const toolUse = await emitCursorLiveRunPendingToolUseTurn(
|
|
293
|
+
turn,
|
|
278
294
|
stream,
|
|
279
295
|
partial,
|
|
280
296
|
model,
|
|
281
297
|
context,
|
|
282
298
|
run,
|
|
283
299
|
toolResultInputTokens,
|
|
284
|
-
options
|
|
285
|
-
options.mode === "emit" ? () => turn.emitter.closeAll() : undefined,
|
|
300
|
+
options,
|
|
286
301
|
);
|
|
287
|
-
if (toolUse) return toolUse;
|
|
302
|
+
if (toolUse === "tool_use") return toolUse;
|
|
303
|
+
if (toolUse === "handled") continue;
|
|
288
304
|
const event = cursorLiveRuns.shiftEvent(run);
|
|
289
305
|
if (!event || event.type === "tool" || event.type === "bridge-tool") continue;
|
|
290
306
|
if (options.mode === "emit") emitCursorLiveQueuedEvent(turn, event, run);
|
|
@@ -376,4 +392,14 @@ export function resetCursorNativeReplayIdleDisposeMs(): void {
|
|
|
376
392
|
cursorNativeReplayIdleDisposeMs = DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS;
|
|
377
393
|
}
|
|
378
394
|
|
|
395
|
+
export async function releaseAllPendingCursorLiveRunsForTests(): Promise<void> {
|
|
396
|
+
while (cursorLiveRuns.count() > 0) {
|
|
397
|
+
const run = cursorLiveRuns.getActiveForScope();
|
|
398
|
+
if (!run) break;
|
|
399
|
+
const before = cursorLiveRuns.count();
|
|
400
|
+
await cursorLiveRuns.release(run);
|
|
401
|
+
if (cursorLiveRuns.count() >= before) break;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
379
405
|
export { hasTrailingUserMessagesAfterToolResults };
|
|
@@ -2,24 +2,17 @@ import type { AssistantMessage, AssistantMessageEventStream } from "@earendil-wo
|
|
|
2
2
|
import type { InteractionUpdate } from "@cursor/sdk";
|
|
3
3
|
import type { CursorLiveRun } from "./cursor-live-run-coordinator.js";
|
|
4
4
|
import { cursorLiveRuns } from "./cursor-provider-live-run-drain.js";
|
|
5
|
-
import {
|
|
5
|
+
import { formatInactiveCursorReplayTrace } from "./cursor-native-replay-trace.js";
|
|
6
|
+
import { resolveNativeReplayDisposition } from "./cursor-native-replay-routing.js";
|
|
7
|
+
import { truncateCursorDisplayLine } from "./cursor-display-text.js";
|
|
6
8
|
import { CursorPartialContentEmitter } from "./cursor-partial-content-emitter.js";
|
|
7
9
|
import { asRecord, getField, hasUsableText } from "./cursor-record-utils.js";
|
|
8
10
|
import { scrubPiToolDisplay, scrubSensitiveText } from "./cursor-sensitive-text.js";
|
|
9
11
|
import { buildCursorPiToolDisplay, formatCursorToolTranscript, getCursorCreatePlanText, mergeCursorToolCalls } from "./cursor-tool-transcript.js";
|
|
10
12
|
import { getString, getToolArgs, getToolName } from "./cursor-transcript-utils.js";
|
|
11
13
|
|
|
12
|
-
function sanitizeSingleLine(value: string): string {
|
|
13
|
-
return value.replace(/\s+/g, " ").trim();
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function truncateSingleLine(value: string, maxLength = 240): string {
|
|
17
|
-
const sanitized = sanitizeSingleLine(value);
|
|
18
|
-
return sanitized.length > maxLength ? `${sanitized.slice(0, maxLength - 1)}…` : sanitized;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
14
|
function formatCursorToolName(toolCall: unknown): string {
|
|
22
|
-
return
|
|
15
|
+
return truncateCursorDisplayLine(getToolName(toolCall), 80) || "unknown";
|
|
23
16
|
}
|
|
24
17
|
|
|
25
18
|
interface CursorShellOutputDelta {
|
|
@@ -45,7 +38,7 @@ function extractCursorTaskProgressLabel(toolCall: unknown, apiKey?: string): str
|
|
|
45
38
|
if (!isCursorTaskToolCall(toolCall)) return undefined;
|
|
46
39
|
const description = getString(getToolArgs(toolCall), "description");
|
|
47
40
|
if (!description?.trim()) return undefined;
|
|
48
|
-
return
|
|
41
|
+
return truncateCursorDisplayLine(scrubSensitiveText(description, apiKey));
|
|
49
42
|
}
|
|
50
43
|
|
|
51
44
|
function getCursorShellOutputDelta(update: InteractionUpdate): CursorShellOutputDelta | undefined {
|
|
@@ -107,6 +100,7 @@ export interface CursorSdkTurnCoordinatorOptions {
|
|
|
107
100
|
resolvedApiKey?: string;
|
|
108
101
|
liveRun?: CursorLiveRun;
|
|
109
102
|
useNativeToolReplay: boolean;
|
|
103
|
+
activeToolNames?: ReadonlySet<string>;
|
|
110
104
|
nativeReplayId: string;
|
|
111
105
|
textDeltas: string[];
|
|
112
106
|
}
|
|
@@ -118,6 +112,7 @@ export class CursorSdkTurnCoordinator {
|
|
|
118
112
|
readonly resolvedApiKey?: string;
|
|
119
113
|
readonly liveRun?: CursorLiveRun;
|
|
120
114
|
readonly useNativeToolReplay: boolean;
|
|
115
|
+
readonly activeToolNames?: ReadonlySet<string>;
|
|
121
116
|
readonly nativeReplayId: string;
|
|
122
117
|
readonly textDeltas: string[];
|
|
123
118
|
|
|
@@ -142,6 +137,7 @@ export class CursorSdkTurnCoordinator {
|
|
|
142
137
|
this.resolvedApiKey = options.resolvedApiKey;
|
|
143
138
|
this.liveRun = options.liveRun;
|
|
144
139
|
this.useNativeToolReplay = options.useNativeToolReplay;
|
|
140
|
+
this.activeToolNames = options.activeToolNames;
|
|
145
141
|
this.nativeReplayId = options.nativeReplayId;
|
|
146
142
|
this.textDeltas = options.textDeltas;
|
|
147
143
|
this.contentEmitter = new CursorPartialContentEmitter(options.stream, options.partial, undefined, false);
|
|
@@ -232,7 +228,7 @@ export class CursorSdkTurnCoordinator {
|
|
|
232
228
|
return;
|
|
233
229
|
}
|
|
234
230
|
if (update.type === "summary") {
|
|
235
|
-
const summary = `Cursor summary: ${
|
|
231
|
+
const summary = `Cursor summary: ${truncateCursorDisplayLine(update.summary)}\n`;
|
|
236
232
|
if (this.liveRun) {
|
|
237
233
|
cursorLiveRuns.queueEvent(this.liveRun, { type: "thinking-delta", text: summary });
|
|
238
234
|
} else {
|
|
@@ -346,10 +342,14 @@ export class CursorSdkTurnCoordinator {
|
|
|
346
342
|
this.completedFallbackToolFingerprints.add(fingerprint);
|
|
347
343
|
}
|
|
348
344
|
|
|
349
|
-
const
|
|
350
|
-
|
|
345
|
+
const disposition = resolveNativeReplayDisposition({
|
|
346
|
+
toolName: display.toolName,
|
|
347
|
+
useNativeToolReplay: this.useNativeToolReplay,
|
|
348
|
+
activeToolNames: this.activeToolNames,
|
|
349
|
+
hasLiveRun: this.liveRun !== undefined,
|
|
350
|
+
});
|
|
351
351
|
|
|
352
|
-
if (
|
|
352
|
+
if (disposition === "queue_replay" && this.liveRun) {
|
|
353
353
|
this.nativeToolReplayStarted = true;
|
|
354
354
|
const id = `${this.nativeReplayId}-tool-${++this.nativeToolDisplayCounter}`;
|
|
355
355
|
const scrubbedDisplay = scrubPiToolDisplay(display, this.resolvedApiKey);
|
|
@@ -360,7 +360,11 @@ export class CursorSdkTurnCoordinator {
|
|
|
360
360
|
return;
|
|
361
361
|
}
|
|
362
362
|
|
|
363
|
-
|
|
363
|
+
const traceText =
|
|
364
|
+
disposition === "inactive_trace"
|
|
365
|
+
? formatInactiveCursorReplayTrace(display)
|
|
366
|
+
: transcript || `Cursor tool: ${formatCursorToolName(toolCall)} completed`;
|
|
367
|
+
this.emitCursorToolTrace(traceText);
|
|
364
368
|
}
|
|
365
369
|
|
|
366
370
|
private emitCursorToolTrace(text: string): void {
|
package/src/cursor-provider.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
getCursorPromptOptions,
|
|
29
29
|
} from "./cursor-usage-accounting.js";
|
|
30
30
|
import { getCursorSessionCwd } from "./cursor-session-cwd.js";
|
|
31
|
+
import { getActiveContextToolNames } from "./cursor-context-tools.js";
|
|
31
32
|
import { CursorLiveRunAbortError, type CursorLiveRun } from "./cursor-live-run-coordinator.js";
|
|
32
33
|
import {
|
|
33
34
|
abandonSessionCursorAgent,
|
|
@@ -38,6 +39,7 @@ import {
|
|
|
38
39
|
DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS,
|
|
39
40
|
getPendingCursorLiveRun,
|
|
40
41
|
hasTrailingUserMessagesAfterToolResults,
|
|
42
|
+
releaseAllPendingCursorLiveRunsForTests,
|
|
41
43
|
resetCursorNativeReplayIdleDisposeMs,
|
|
42
44
|
selectCursorFinalText,
|
|
43
45
|
setCursorNativeReplayIdleDisposeMs,
|
|
@@ -209,6 +211,7 @@ export function streamCursor(
|
|
|
209
211
|
const sessionBridgeRun = sessionAgentLease.bridgeRun;
|
|
210
212
|
const promptInputTokens = estimateCursorPromptInputTokens(prompt, promptOptions);
|
|
211
213
|
const useNativeToolReplay = isCursorNativeToolDisplayRuntimeEnabled();
|
|
214
|
+
const activeToolNames = getActiveContextToolNames(context);
|
|
212
215
|
const nativeReplayId = createCursorNativeReplayId();
|
|
213
216
|
const textDeltas: string[] = [];
|
|
214
217
|
const useLiveRun = useNativeToolReplay || bridgeRun !== undefined;
|
|
@@ -237,6 +240,7 @@ export function streamCursor(
|
|
|
237
240
|
resolvedApiKey,
|
|
238
241
|
liveRun,
|
|
239
242
|
useNativeToolReplay,
|
|
243
|
+
activeToolNames,
|
|
240
244
|
nativeReplayId,
|
|
241
245
|
textDeltas,
|
|
242
246
|
});
|
|
@@ -365,5 +369,6 @@ export const __testUtils = {
|
|
|
365
369
|
hasTrailingUserMessagesAfterToolResults,
|
|
366
370
|
setCursorNativeReplayIdleDisposeMs,
|
|
367
371
|
resetCursorNativeReplayIdleDisposeMs,
|
|
372
|
+
releaseAllPendingCursorLiveRunsForTests,
|
|
368
373
|
resetSessionCursorAgents: () => disposeAllSessionCursorAgents(),
|
|
369
374
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionContext, ExtensionHandler, SessionStartEvent } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { BeforeAgentStartEvent, ExtensionAPI, ExtensionContext, ExtensionHandler, SessionStartEvent, TurnStartEvent } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Text } from "@earendil-works/pi-tui";
|
|
3
3
|
import { Type } from "typebox";
|
|
4
4
|
import { resolveCursorPiToolBridgeEnabled } from "./cursor-pi-tool-bridge.js";
|
|
@@ -36,6 +36,8 @@ interface CursorQuestionDetails {
|
|
|
36
36
|
|
|
37
37
|
interface CursorQuestionToolExtensionApi extends Pick<ExtensionAPI, "getActiveTools" | "registerTool" | "setActiveTools"> {
|
|
38
38
|
on(event: "session_start", handler: ExtensionHandler<SessionStartEvent>): void;
|
|
39
|
+
on(event: "before_agent_start", handler: ExtensionHandler<BeforeAgentStartEvent>): void;
|
|
40
|
+
on(event: "turn_start", handler: ExtensionHandler<TurnStartEvent>): void;
|
|
39
41
|
on(event: "model_select", handler: (event: { model: ExtensionContext["model"] }, ctx: ExtensionContext) => Promise<void> | void): void;
|
|
40
42
|
}
|
|
41
43
|
|
|
@@ -246,6 +248,12 @@ export function registerCursorQuestionTool(pi: CursorQuestionToolExtensionApi):
|
|
|
246
248
|
pi.on("session_start", (_event, ctx) => {
|
|
247
249
|
syncCursorQuestionToolForModel(pi, ctx.model);
|
|
248
250
|
});
|
|
251
|
+
pi.on("before_agent_start", (_event, ctx) => {
|
|
252
|
+
syncCursorQuestionToolForModel(pi, ctx.model);
|
|
253
|
+
});
|
|
254
|
+
pi.on("turn_start", (_event, ctx) => {
|
|
255
|
+
syncCursorQuestionToolForModel(pi, ctx.model);
|
|
256
|
+
});
|
|
249
257
|
pi.on("model_select", (event) => {
|
|
250
258
|
syncCursorQuestionToolForModel(pi, event.model);
|
|
251
259
|
});
|