pi-ui-extend 1.0.39 → 1.0.41
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/README.md +1 -1
- package/dist/app/commands/command-registry.js +2 -2
- package/dist/app/commands/command-session-actions.d.ts +0 -1
- package/dist/app/commands/command-session-actions.js +22 -13
- package/dist/app/icons.d.ts +14 -0
- package/dist/app/icons.js +33 -0
- package/dist/app/rendering/conversation-tool-renderer.js +2 -2
- package/dist/app/rendering/dcp-stats.d.ts +6 -1
- package/dist/app/rendering/dcp-stats.js +214 -46
- package/dist/app/rendering/editor-panels.js +8 -5
- package/dist/app/session/lazy-session-manager.js +12 -1
- package/dist/app/session/tabs-controller.d.ts +2 -5
- package/dist/app/session/tabs-controller.js +12 -21
- package/dist/app/subagents/subagents-model.d.ts +14 -1
- package/dist/app/subagents/subagents-model.js +34 -15
- package/dist/app/types.d.ts +2 -0
- package/dist/bundled-extensions/session-title/config.js +1 -1
- package/dist/markdown-format.js +27 -9
- package/dist/schemas/pi-tools-suite-schema.d.ts +29 -16
- package/dist/schemas/pi-tools-suite-schema.js +46 -31
- package/external/pi-tools-suite/README.md +392 -52
- package/external/pi-tools-suite/docs/browser-qa-subagent.md +31 -21
- package/external/pi-tools-suite/docs/context-gateway-p00-adr.md +216 -0
- package/external/pi-tools-suite/docs/context-gateway-p01n-gate-review.md +122 -0
- package/external/pi-tools-suite/docs/context-gateway-p01n-measurement.md +133 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-ra-evidence.md +111 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-rb-evidence.md +100 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-rc-evidence.md +69 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-rd-evidence.md +100 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-re-evidence.md +74 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-rf-evidence.md +153 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-rg-evidence.md +235 -0
- package/external/pi-tools-suite/docs/evals.md +684 -0
- package/external/pi-tools-suite/docs/subagent-model-pools.md +109 -0
- package/external/pi-tools-suite/package.json +10 -3
- package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/scripts/browser-qa-runner.mjs +82 -1
- package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/SKILL.md → agents/browser-qa.md} +261 -12
- package/external/pi-tools-suite/src/async-subagents/agents/implement.md +20 -0
- package/external/pi-tools-suite/src/async-subagents/agents/oracle.md +16 -0
- package/external/pi-tools-suite/src/async-subagents/agents/research.md +18 -0
- package/external/pi-tools-suite/src/async-subagents/agents/verify.md +18 -0
- package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +27 -243
- package/external/pi-tools-suite/src/async-subagents/commands.ts +6 -2
- package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +41 -0
- package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +13 -93
- package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +494 -0
- package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +9 -0
- package/external/pi-tools-suite/src/async-subagents/core/config.ts +200 -143
- package/external/pi-tools-suite/src/async-subagents/core/model-fallback.ts +1 -1
- package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +54 -0
- package/external/pi-tools-suite/src/async-subagents/core/prompt.ts +7 -6
- package/external/pi-tools-suite/src/async-subagents/core/routing.ts +52 -45
- package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +12 -4
- package/external/pi-tools-suite/src/async-subagents/index.ts +11 -1
- package/external/pi-tools-suite/src/async-subagents/lib.ts +6 -2
- package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +46 -18
- package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +3 -2
- package/external/pi-tools-suite/src/async-subagents/types.ts +2 -0
- package/external/pi-tools-suite/src/coding-discipline/index.ts +41 -142
- package/external/pi-tools-suite/src/config.ts +1 -22
- package/external/pi-tools-suite/src/context-gateway/accounting.ts +151 -0
- package/external/pi-tools-suite/src/context-gateway/config.ts +111 -0
- package/external/pi-tools-suite/src/context-gateway/index.ts +160 -0
- package/external/pi-tools-suite/src/context-gateway/metadata-normalization.ts +88 -0
- package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +89 -0
- package/external/pi-tools-suite/src/context-gateway/telemetry.ts +429 -0
- package/external/pi-tools-suite/src/context-gateway/test-output-parser.ts +326 -0
- package/external/pi-tools-suite/src/context-gateway/types.ts +152 -0
- package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +106 -0
- package/external/pi-tools-suite/src/dcp/auto-compress.ts +810 -106
- package/external/pi-tools-suite/src/dcp/commands.ts +64 -139
- package/external/pi-tools-suite/src/dcp/compress-tool.ts +369 -35
- package/external/pi-tools-suite/src/dcp/compression-blocks.ts +510 -64
- package/external/pi-tools-suite/src/dcp/compression-preview.ts +113 -0
- package/external/pi-tools-suite/src/dcp/compression-progress.ts +70 -0
- package/external/pi-tools-suite/src/dcp/config.ts +36 -61
- package/external/pi-tools-suite/src/dcp/conversation-index.ts +421 -0
- package/external/pi-tools-suite/src/dcp/debug-log.ts +7 -5
- package/external/pi-tools-suite/src/dcp/index.ts +617 -203
- package/external/pi-tools-suite/src/dcp/journal.ts +566 -0
- package/external/pi-tools-suite/src/dcp/progress-controller.ts +244 -0
- package/external/pi-tools-suite/src/dcp/prompts.ts +10 -7
- package/external/pi-tools-suite/src/dcp/provider-tool-results.ts +189 -0
- package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +298 -78
- package/external/pi-tools-suite/src/dcp/pruner-compression-blocks.ts +173 -281
- package/external/pi-tools-suite/src/dcp/pruner-emergency.ts +2 -4
- package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +17 -5
- package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +11 -1
- package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +30 -82
- package/external/pi-tools-suite/src/dcp/pruner-tools.ts +22 -133
- package/external/pi-tools-suite/src/dcp/pruner.ts +18 -33
- package/external/pi-tools-suite/src/dcp/recovery.ts +129 -0
- package/external/pi-tools-suite/src/dcp/shadow-plan.ts +127 -0
- package/external/pi-tools-suite/src/dcp/state-transaction.ts +102 -0
- package/external/pi-tools-suite/src/dcp/state.ts +158 -580
- package/external/pi-tools-suite/src/dcp/ui.ts +1 -0
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +55 -220
- package/external/pi-tools-suite/src/index.ts +9 -0
- package/external/pi-tools-suite/src/model-tools/index.ts +76 -42
- package/external/pi-tools-suite/src/repo-discovery/index.ts +84 -18
- package/external/pi-tools-suite/src/repo-discovery/native-compact.ts +458 -0
- package/external/pi-tools-suite/src/session-recovery/index.ts +189 -43
- package/external/pi-tools-suite/src/tool-descriptions.ts +43 -38
- package/external/pi-tools-suite/src/truncation-metadata-normalizer/index.ts +17 -0
- package/package.json +6 -6
- package/schemas/pi-tools-suite.json +159 -78
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/auth-scaffold-spec.md +0 -78
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/qa-design.md +0 -223
- package/external/pi-tools-suite/src/dcp/state-persistence.ts +0 -195
- /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-auth.example.jsonc +0 -0
- /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-flow.example.jsonc +0 -0
- /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.LICENSE +0 -0
- /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.mjs +0 -0
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
# Designing deterministic browser QA flows
|
|
2
|
-
|
|
3
|
-
Use this reference with the bundled declarative runner. It intentionally does
|
|
4
|
-
not describe a separate browser CLI or executable Playwright scripts.
|
|
5
|
-
|
|
6
|
-
## Build the proof before the steps
|
|
7
|
-
|
|
8
|
-
Write down three things first:
|
|
9
|
-
|
|
10
|
-
1. **Setup:** the page and state needed to expose the behavior.
|
|
11
|
-
2. **Action:** the smallest user interaction that exercises it.
|
|
12
|
-
3. **Oracle:** the observable state that proves success or reproduces failure.
|
|
13
|
-
|
|
14
|
-
Good oracles are product-visible and specific: an exact URL, a stable status
|
|
15
|
-
message, a field value, item count, enabled/disabled state, or checked state.
|
|
16
|
-
Avoid treating “the click did not throw” or “the screenshot looks plausible” as
|
|
17
|
-
proof.
|
|
18
|
-
|
|
19
|
-
When verifying a fix, prefer a focused regression flow over a broad tour of the
|
|
20
|
-
application. If multiple independent states matter, assert each one explicitly.
|
|
21
|
-
|
|
22
|
-
For explicit exploratory/manual QA, keep exploration bounded rather than turning
|
|
23
|
-
it into an open-ended crawl. Run at most three minimal rounds. Each round should
|
|
24
|
-
start from one concrete hypothesis, produce a deterministic observation plus a
|
|
25
|
-
meaningful screenshot, and use that evidence to decide whether another round is
|
|
26
|
-
justified. Verification tasks remain one browser run per profile.
|
|
27
|
-
|
|
28
|
-
## Choose resilient locators
|
|
29
|
-
|
|
30
|
-
Prefer locators that match how users and accessibility APIs identify controls:
|
|
31
|
-
|
|
32
|
-
1. `testId` when the product exposes a stable test contract.
|
|
33
|
-
2. `role` with accessible `name` for buttons, links, headings, dialogs, and
|
|
34
|
-
similar semantic elements.
|
|
35
|
-
3. `label` for form controls.
|
|
36
|
-
4. `placeholder` or visible `text` when they are stable product copy.
|
|
37
|
-
5. `css` only when no semantic contract exists.
|
|
38
|
-
|
|
39
|
-
Use `exact: true` when duplicate or substring matches are possible. Avoid CSS
|
|
40
|
-
that encodes DOM depth, generated classes, styling details, or element order.
|
|
41
|
-
If a locator is ambiguous, inspect nearby source or rendered copy and choose a
|
|
42
|
-
more specific product contract rather than adding arbitrary delays.
|
|
43
|
-
|
|
44
|
-
## Wait for state, not time
|
|
45
|
-
|
|
46
|
-
Runner interactions inherit Playwright auto-waiting. Usually an action followed
|
|
47
|
-
by an assertion is enough. Use `waitFor` only when the next operation depends on
|
|
48
|
-
a distinct attached/detached/visible/hidden transition.
|
|
49
|
-
|
|
50
|
-
After navigation and visible interactions, the runner also waits for DOM
|
|
51
|
-
readiness, tracks requests causally started by that action through a bounded
|
|
52
|
-
readiness window, waits for common visible
|
|
53
|
-
`aria-busy`/progress/loading/spinner/skeleton markers, and keeps a 500 ms stable
|
|
54
|
-
interval. EventSource/WebSocket traffic is ignored, and a long poll is not
|
|
55
|
-
allowed to pin the entire flow timeout. A visible busy indicator may still hold
|
|
56
|
-
readiness until `timeoutMs`. This is a safe baseline, not an application-specific
|
|
57
|
-
oracle: explicitly wait for a custom loader to become hidden and assert the
|
|
58
|
-
loaded content when the application uses different readiness semantics.
|
|
59
|
-
|
|
60
|
-
`waitForTimeout` is bounded to five seconds and should be exceptional—for a
|
|
61
|
-
known animation, debounce, or externally scheduled transition with no
|
|
62
|
-
observable intermediate state. Sleeping longer hides races instead of proving
|
|
63
|
-
behavior. If a normal operation legitimately needs more time, adjust the flow's
|
|
64
|
-
`timeoutMs` rather than inserting repeated sleeps.
|
|
65
|
-
|
|
66
|
-
All assertion actions retry until that timeout. This makes an action followed
|
|
67
|
-
directly by `assertText`, `assertVisible`, `assertURL`, or another assertion
|
|
68
|
-
safe for asynchronously rendered outcomes. `assertText` requires the locator to
|
|
69
|
-
be visible and matches rendered `innerText`; use `assertTextContent` only when
|
|
70
|
-
hidden/raw DOM text is deliberately part of the oracle. Use `assertAttribute` for
|
|
71
|
-
observable state such as `aria-expanded`, `aria-invalid`, or `data-state`
|
|
72
|
-
instead of reading DOM state through executable JavaScript.
|
|
73
|
-
|
|
74
|
-
## Responsive and scrolling scenarios
|
|
75
|
-
|
|
76
|
-
Set the flow's top-level `viewport` whenever the bug depends on a breakpoint or
|
|
77
|
-
available height. Assert `viewportWidth` or `viewportHeight` with
|
|
78
|
-
`assertDOMMetric` when the dimensions themselves are part of the proof; the
|
|
79
|
-
runner also includes the applied viewport in its result.
|
|
80
|
-
|
|
81
|
-
Use `wheel` to reproduce real pointer-wheel input. Add a locator when the wheel
|
|
82
|
-
must target a nested scrolling container—the runner hovers it before sending
|
|
83
|
-
the input. Because browser scrolling may be scheduled after the wheel event,
|
|
84
|
-
wait only for a short known settling interval when a direct metric assertion is
|
|
85
|
-
otherwise racy.
|
|
86
|
-
|
|
87
|
-
Use safe `evaluate` `scrollTo`/`scrollBy` operations for deterministic setup or
|
|
88
|
-
to distinguish input handling from layout behavior. Use the `metrics` operation
|
|
89
|
-
to retain a named page/element snapshot in result `observations`, and use
|
|
90
|
-
`assertDOMMetric` for pass/fail. Raw JavaScript expressions are intentionally
|
|
91
|
-
excluded: flows remain declarative and cannot inspect authentication storage or
|
|
92
|
-
execute arbitrary same-origin requests.
|
|
93
|
-
|
|
94
|
-
## Deterministic browser environment
|
|
95
|
-
|
|
96
|
-
Set top-level `environment` when locale, timezone, color scheme, or motion
|
|
97
|
-
preferences can change the behavior. The runner otherwise uses stable defaults
|
|
98
|
-
(`en-US`, `UTC`, `light`, `reduce`) instead of inheriting host settings. Prefer
|
|
99
|
-
asserting product-visible copy or state derived from those settings; do not use
|
|
100
|
-
screenshot pixels as the only oracle.
|
|
101
|
-
|
|
102
|
-
## Causal network and dialog expectations
|
|
103
|
-
|
|
104
|
-
Put `expectResponse` or `expectDialog` on the interaction that causes the event.
|
|
105
|
-
The runner arms both listeners before the interaction, avoiding the race in a
|
|
106
|
-
separate “click, then wait” sequence. Response expectations deliberately match
|
|
107
|
-
only an exact allowlisted-origin pathname, HTTP method, and status. This proves
|
|
108
|
-
that a matching request started and received a response within the action
|
|
109
|
-
window without retaining its URL query, headers, or body.
|
|
110
|
-
|
|
111
|
-
Dialog expectations match a fixed dialog type and exact/included message, then
|
|
112
|
-
accept or dismiss it declaratively. A mismatch is dismissed before the step
|
|
113
|
-
fails so the page cannot freeze. Actual event metadata is never included in
|
|
114
|
-
failure diagnostics. Do not place secrets in expected messages or response
|
|
115
|
-
paths even though the runner keeps diagnostics generic.
|
|
116
|
-
|
|
117
|
-
## Drag, upload, and download scenarios
|
|
118
|
-
|
|
119
|
-
Use `dragTo` for native DOM drag/drop and assert the resulting product state.
|
|
120
|
-
Optional source/drop positions are relative bounded coordinates. Canvas-only,
|
|
121
|
-
OS-native, or custom synthetic-event drag protocols remain unsupported; do not
|
|
122
|
-
work around that with executable JavaScript.
|
|
123
|
-
|
|
124
|
-
Uploads are memory-only base64 payloads declared in the flow. This intentionally
|
|
125
|
-
prevents a flow from selecting arbitrary project files, credential config, or
|
|
126
|
-
directories. Keep fixtures minimal and non-secret. An empty file list clears a
|
|
127
|
-
file input.
|
|
128
|
-
|
|
129
|
-
Use the atomic `download` action rather than clicking a download link directly.
|
|
130
|
-
Always match the suggested filename and choose a tight `maxBytes`. Retain a
|
|
131
|
-
download only when its contents are needed as evidence; otherwise the runner
|
|
132
|
-
deletes it after validation. Retained downloads use generated private names,
|
|
133
|
-
not server-provided paths, and are scanned for configured authentication before
|
|
134
|
-
publication. `maxBytes` bounds the runner's private evidence copy and triggers
|
|
135
|
-
cancellation while it grows, but it is not a network-bandwidth guarantee: the
|
|
136
|
-
browser can receive temporary bytes before cancellation.
|
|
137
|
-
|
|
138
|
-
## Same-origin frames and popups
|
|
139
|
-
|
|
140
|
-
Use a scoped `target` for iframe or named popup interactions. The runner checks
|
|
141
|
-
the live frame origin before every scoped step and checks a popup after it loads;
|
|
142
|
-
both must remain in `allowedOrigins`. This supports embedded application UI and
|
|
143
|
-
same-origin auxiliary windows without opening a route around the network guard.
|
|
144
|
-
Cross-origin login, payment, and third-party widgets remain intentionally out of
|
|
145
|
-
scope. Each popup adds a separate private video artifact, so open only the
|
|
146
|
-
windows needed for the proof.
|
|
147
|
-
|
|
148
|
-
## Authentication transitions
|
|
149
|
-
|
|
150
|
-
Add `authRejectedIf` directly after initial navigation and after transitions
|
|
151
|
-
that can redirect to login or display an expired-session marker. This converts
|
|
152
|
-
stale credentials into an explicit update request instead of misreporting a
|
|
153
|
-
product regression.
|
|
154
|
-
|
|
155
|
-
Do not encode credentials, tokens, storage values, or login form secrets in the
|
|
156
|
-
flow. The trusted runner applies the selected profile internally. For form auth,
|
|
157
|
-
video starts on the login page and includes field filling and submission; password
|
|
158
|
-
inputs remain masked, but visible identifiers can appear, so treat the video as
|
|
159
|
-
sensitive private evidence. Tracing starts only after login succeeds and is
|
|
160
|
-
sanitized before retention.
|
|
161
|
-
|
|
162
|
-
## Evidence strategy
|
|
163
|
-
|
|
164
|
-
The runner always attempts a final or failure screenshot, records video from the
|
|
165
|
-
first page, and creates a sanitized post-auth trace. Add named `screenshot`
|
|
166
|
-
steps only at states that materially help explain the result—for example before
|
|
167
|
-
and after a destructive interaction, or when a transient success message is
|
|
168
|
-
the oracle.
|
|
169
|
-
|
|
170
|
-
Use evidence by purpose:
|
|
171
|
-
|
|
172
|
-
- **Screenshot:** quick review of one meaningful visual state.
|
|
173
|
-
- **Video:** chronological confirmation of the complete user flow.
|
|
174
|
-
- **Trace:** action/DOM timing diagnosis for a failed or flaky interaction.
|
|
175
|
-
|
|
176
|
-
Videos automatically show a transient cursor and yellow pulse for clicks and
|
|
177
|
-
double-clicks. After a native `dragTo` gesture completes, its resolved
|
|
178
|
-
source-to-target route is replayed over 450 ms with a large orange cursor and
|
|
179
|
-
progressively drawn high-contrast trail, followed by a green drop marker. These
|
|
180
|
-
annotations are
|
|
181
|
-
runner-owned, pointer-transparent, and accessibility-hidden; they cover the main
|
|
182
|
-
page, same-origin frames, declared popups, and form-auth submission. Their
|
|
183
|
-
bounded animations finish within the normal post-action stable interval, so
|
|
184
|
-
they explain the chronology without becoming screenshot or assertion oracles.
|
|
185
|
-
|
|
186
|
-
Assertions determine pass/fail; evidence explains it. Preserve and link every
|
|
187
|
-
artifact group returned on both passed and failed runs.
|
|
188
|
-
|
|
189
|
-
For visual QA, do not stop at artifact generation. Open at least one meaningful
|
|
190
|
-
PNG with the model's image-capable `read` path and inspect layout, clipping,
|
|
191
|
-
overlap, state styling, and other visual defects relevant to the scenario. If
|
|
192
|
-
the active model cannot read images, explicitly report visual inspection as
|
|
193
|
-
unavailable rather than treating deterministic assertions as a visual pass.
|
|
194
|
-
|
|
195
|
-
## Diagnose failures without weakening the test
|
|
196
|
-
|
|
197
|
-
Classify the first failing step:
|
|
198
|
-
|
|
199
|
-
- wrong target/setup or service unavailable;
|
|
200
|
-
- authentication rejected or expired;
|
|
201
|
-
- locator no longer matches the product contract;
|
|
202
|
-
- expected state never appeared;
|
|
203
|
-
- actual product behavior contradicts the expectation.
|
|
204
|
-
|
|
205
|
-
Fix the flow only when its setup or locator is wrong. Do not replace a precise
|
|
206
|
-
assertion with a vague one, increase timeouts reflexively, or remove the failing
|
|
207
|
-
step to manufacture a pass. Keep the failure artifacts and state the expected
|
|
208
|
-
versus observed behavior.
|
|
209
|
-
|
|
210
|
-
## Cleanup and isolation
|
|
211
|
-
|
|
212
|
-
Each runner invocation owns one isolated context and evidence directory and
|
|
213
|
-
closes its browser resources in a `finally` path. Do not create parallel shared
|
|
214
|
-
or default sessions outside the runner. Test multiple auth profiles with
|
|
215
|
-
separate invocations so cookies, storage, traces, and evidence cannot mix.
|
|
216
|
-
|
|
217
|
-
Keep the declarative flow and every generated screenshot, video, trace, and
|
|
218
|
-
result manifest inside `$PI_SUBAGENT_AGENT_DIR/browser-qa/`. The launcher owns
|
|
219
|
-
that path and the runner validates it before opening a browser. Do not override
|
|
220
|
-
the environment path or copy evidence into shared `.pi/qa-runs`/`.pi/qa-flows`
|
|
221
|
-
directories: the agent-local workspace is intentionally removed by the normal
|
|
222
|
-
sub-agent shutdown and cleanup lifecycle. Authentication config remains a
|
|
223
|
-
separate persistent input under project `.pi/`.
|
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
import { mkdir, open, readFile, readdir, stat, unlink, writeFile } from "node:fs/promises"
|
|
2
|
-
import type { Dirent } from "node:fs"
|
|
3
|
-
import { dirname, join } from "node:path"
|
|
4
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent"
|
|
5
|
-
import { hashSerializedState, serializeState, type DcpState, type SerializedDcpState } from "./state.js"
|
|
6
|
-
|
|
7
|
-
const DCP_STATE_DIR = "dcp-state"
|
|
8
|
-
const DCP_STATE_EXT = ".json"
|
|
9
|
-
const DCP_STATE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000
|
|
10
|
-
const MAX_SESSION_HEADER_BYTES = 64 * 1024
|
|
11
|
-
|
|
12
|
-
let lastPersistedStateHash: string | undefined
|
|
13
|
-
let saveQueue: Promise<void> = Promise.resolve()
|
|
14
|
-
|
|
15
|
-
function safeSessionFileName(sessionId: string): string {
|
|
16
|
-
return sessionId.replace(/[^a-zA-Z0-9._-]/g, "_") + DCP_STATE_EXT
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export async function readSessionIdFromFile(sessionPath: string): Promise<string | undefined> {
|
|
20
|
-
const file = await open(sessionPath, "r")
|
|
21
|
-
try {
|
|
22
|
-
const buffer = Buffer.alloc(MAX_SESSION_HEADER_BYTES)
|
|
23
|
-
const { bytesRead } = await file.read(buffer, 0, buffer.length, 0)
|
|
24
|
-
if (bytesRead <= 0) return undefined
|
|
25
|
-
|
|
26
|
-
const content = buffer.subarray(0, bytesRead)
|
|
27
|
-
const newlineIndex = content.indexOf(0x0a)
|
|
28
|
-
if (newlineIndex < 0 && bytesRead === buffer.length) return undefined
|
|
29
|
-
|
|
30
|
-
const firstLine = content.subarray(0, newlineIndex >= 0 ? newlineIndex : bytesRead).toString("utf8").trim()
|
|
31
|
-
if (!firstLine) return undefined
|
|
32
|
-
const parsed = JSON.parse(firstLine) as { type?: string; id?: unknown }
|
|
33
|
-
return parsed.type === "session" && typeof parsed.id === "string" && parsed.id.length > 0
|
|
34
|
-
? parsed.id
|
|
35
|
-
: undefined
|
|
36
|
-
} finally {
|
|
37
|
-
await file.close()
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async function listSessionIds(sessionDir: string): Promise<string[]> {
|
|
42
|
-
let entries: Dirent[]
|
|
43
|
-
try {
|
|
44
|
-
entries = await readdir(sessionDir, { withFileTypes: true })
|
|
45
|
-
} catch (error) {
|
|
46
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return []
|
|
47
|
-
throw error
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const sessionIds = new Set<string>()
|
|
51
|
-
for (const entry of entries) {
|
|
52
|
-
if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue
|
|
53
|
-
|
|
54
|
-
const sessionPath = join(sessionDir, entry.name)
|
|
55
|
-
try {
|
|
56
|
-
const sessionId = await readSessionIdFromFile(sessionPath)
|
|
57
|
-
if (sessionId) sessionIds.add(sessionId)
|
|
58
|
-
} catch {
|
|
59
|
-
// Ignore malformed or transient session files during cleanup.
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return [...sessionIds]
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function resolveDcpStateDir(ctx: ExtensionContext): string | undefined {
|
|
67
|
-
const sessionDir = ctx.sessionManager?.getSessionDir?.()
|
|
68
|
-
if (!sessionDir) return undefined
|
|
69
|
-
return join(sessionDir, DCP_STATE_DIR)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function resolveDcpStatePath(ctx: ExtensionContext): string | undefined {
|
|
73
|
-
const sessionId = ctx.sessionManager?.getSessionId?.()
|
|
74
|
-
const stateDir = resolveDcpStateDir(ctx)
|
|
75
|
-
if (!sessionId || !stateDir) return undefined
|
|
76
|
-
return join(stateDir, safeSessionFileName(sessionId))
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function resetDcpPersistenceDedup(): void {
|
|
80
|
-
lastPersistedStateHash = undefined
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export async function loadDcpState(ctx: ExtensionContext): Promise<SerializedDcpState | undefined> {
|
|
84
|
-
const statePath = resolveDcpStatePath(ctx)
|
|
85
|
-
if (!statePath) return undefined
|
|
86
|
-
|
|
87
|
-
try {
|
|
88
|
-
const text = await readFile(statePath, "utf8")
|
|
89
|
-
const data = JSON.parse(text) as SerializedDcpState
|
|
90
|
-
lastPersistedStateHash = hashSerializedState(data)
|
|
91
|
-
return data
|
|
92
|
-
} catch (error) {
|
|
93
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined
|
|
94
|
-
throw error
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Load the DCP sidecar for an arbitrary session file path, e.g. the previous
|
|
100
|
-
* session during fork/resume/new. Resolves the sidecar via the session file's
|
|
101
|
-
* first-line session id rather than the live session manager, so it works
|
|
102
|
-
* independent of the current ctx.sessionManager state.
|
|
103
|
-
*/
|
|
104
|
-
export async function loadDcpStateFromSessionFile(
|
|
105
|
-
sessionFile: string,
|
|
106
|
-
): Promise<SerializedDcpState | undefined> {
|
|
107
|
-
if (!sessionFile) return undefined
|
|
108
|
-
|
|
109
|
-
try {
|
|
110
|
-
const sessionId = await readSessionIdFromFile(sessionFile)
|
|
111
|
-
if (!sessionId) return undefined
|
|
112
|
-
const stateDir = join(dirname(sessionFile), DCP_STATE_DIR)
|
|
113
|
-
const statePath = join(stateDir, safeSessionFileName(sessionId))
|
|
114
|
-
const text = await readFile(statePath, "utf8")
|
|
115
|
-
return JSON.parse(text) as SerializedDcpState
|
|
116
|
-
} catch {
|
|
117
|
-
// A missing/unreadable sidecar for the previous session (e.g. a fresh
|
|
118
|
-
// fork with no prior compression) means there is simply nothing to inherit.
|
|
119
|
-
return undefined
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export async function cleanupStaleDcpStateFiles(ctx: ExtensionContext): Promise<number> {
|
|
124
|
-
const stateDir = resolveDcpStateDir(ctx)
|
|
125
|
-
const sessionDir = ctx.sessionManager.getSessionDir()
|
|
126
|
-
if (!stateDir || !sessionDir) return 0
|
|
127
|
-
|
|
128
|
-
const currentSessionId = ctx.sessionManager.getSessionId()
|
|
129
|
-
const liveStateFiles = new Set<string>()
|
|
130
|
-
if (currentSessionId) liveStateFiles.add(safeSessionFileName(currentSessionId))
|
|
131
|
-
|
|
132
|
-
const sessionIds = await listSessionIds(sessionDir)
|
|
133
|
-
for (const sessionId of sessionIds) {
|
|
134
|
-
liveStateFiles.add(safeSessionFileName(sessionId))
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
let entries: Dirent[]
|
|
138
|
-
try {
|
|
139
|
-
entries = await readdir(stateDir, { withFileTypes: true })
|
|
140
|
-
} catch (error) {
|
|
141
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return 0
|
|
142
|
-
throw error
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const now = Date.now()
|
|
146
|
-
let deleted = 0
|
|
147
|
-
for (const entry of entries) {
|
|
148
|
-
if (!entry.isFile() || !entry.name.endsWith(DCP_STATE_EXT)) continue
|
|
149
|
-
if (currentSessionId && entry.name === safeSessionFileName(currentSessionId)) continue
|
|
150
|
-
|
|
151
|
-
const statePath = join(stateDir, entry.name)
|
|
152
|
-
const isLiveSession = liveStateFiles.has(entry.name)
|
|
153
|
-
let isTooOld = false
|
|
154
|
-
try {
|
|
155
|
-
const info = await stat(statePath)
|
|
156
|
-
isTooOld = now - info.mtimeMs > DCP_STATE_MAX_AGE_MS
|
|
157
|
-
} catch (error) {
|
|
158
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") continue
|
|
159
|
-
throw error
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (isLiveSession && !isTooOld) continue
|
|
163
|
-
|
|
164
|
-
await unlink(statePath)
|
|
165
|
-
deleted++
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
return deleted
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export async function saveDcpState(ctx: ExtensionContext, state: DcpState): Promise<void> {
|
|
172
|
-
const statePath = resolveDcpStatePath(ctx)
|
|
173
|
-
if (!statePath) return
|
|
174
|
-
|
|
175
|
-
const serialized = serializeState(state)
|
|
176
|
-
const hash = hashSerializedState(serialized)
|
|
177
|
-
if (hash === lastPersistedStateHash) return
|
|
178
|
-
lastPersistedStateHash = hash
|
|
179
|
-
|
|
180
|
-
saveQueue = saveQueue
|
|
181
|
-
.catch(() => {
|
|
182
|
-
// Keep later saves moving even if an earlier write failed.
|
|
183
|
-
})
|
|
184
|
-
.then(async () => {
|
|
185
|
-
await mkdir(dirname(statePath), { recursive: true })
|
|
186
|
-
await writeFile(statePath, JSON.stringify(serialized), "utf8")
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
try {
|
|
190
|
-
await saveQueue
|
|
191
|
-
} catch (error) {
|
|
192
|
-
if (lastPersistedStateHash === hash) lastPersistedStateHash = undefined
|
|
193
|
-
throw error
|
|
194
|
-
}
|
|
195
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|