pi-ele-picker 0.6.0 → 0.6.1
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 -0
- package/extensions/pi-picker.ts +61 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -153,6 +153,7 @@ The same payload is available from the bridge directly:
|
|
|
153
153
|
- Discovery is per-project: the bridge whose `cwd` matches `root` (and whose process is still alive) is selected, so several projects/sessions can run side by side without cross-talk.
|
|
154
154
|
- If pi restarts while a page is open (new random port + token), the next send re-discovers the bridge and retries once automatically — no page refresh needed. The status poll does the same: a stale port flips to `offline` for one cycle, then rediscovers.
|
|
155
155
|
- The bridge never auto-submits on its own: **写入pi输入框** only fills the editor; **直接发送** is an explicit user action in the page.
|
|
156
|
+
- Attached screenshots are decoded to temp files under `<tmpdir>/pi-picker` and referenced by path in the composed prompt (that is what the agent reads). Files older than 24h are swept on the next session start or pick, so the directory cannot grow without bound — nothing is meant to be kept there, and nothing needs cleaning up by hand.
|
|
156
157
|
|
|
157
158
|
## Development
|
|
158
159
|
|
package/extensions/pi-picker.ts
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
*/
|
|
37
37
|
import { createServer, type Server } from 'node:http';
|
|
38
38
|
import { randomBytes } from 'node:crypto';
|
|
39
|
-
import { mkdirSync, writeFileSync, rmSync, existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
39
|
+
import { mkdirSync, writeFileSync, rmSync, existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
40
40
|
import { join } from 'node:path';
|
|
41
41
|
import { homedir, tmpdir } from 'node:os';
|
|
42
42
|
import type { ExtensionAPI, ExtensionUIContext } from '@earendil-works/pi-coding-agent';
|
|
@@ -45,6 +45,24 @@ const REG_DIR = join(homedir(), '.pi', 'agent', 'pi-picker');
|
|
|
45
45
|
const IMG_DIR = join(tmpdir(), 'pi-picker');
|
|
46
46
|
const MAX_BODY = 40 * 1024 * 1024;
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* How long a staged screenshot is kept.
|
|
50
|
+
*
|
|
51
|
+
* The file only has to outlive the moment the agent reads it, and the agent reads
|
|
52
|
+
* it after the developer sends the prompt — which is not always right away: the
|
|
53
|
+
* "写入pi输入框" flow can leave a prompt sitting in the editor until the next
|
|
54
|
+
* morning. A day covers that, and it matches the browser draft's own TTL: once the
|
|
55
|
+
* draft has expired those paths can never be referenced again.
|
|
56
|
+
*
|
|
57
|
+
* Nothing else ever deletes these files, so without the sweep below the directory
|
|
58
|
+
* grows forever — one screenshot is a few MB, and every pick leaves its own here.
|
|
59
|
+
*/
|
|
60
|
+
const IMG_TTL_MS = 24 * 60 * 60 * 1000;
|
|
61
|
+
|
|
62
|
+
/** The sweep is cheap but pointless per pick, so it runs at most this often. */
|
|
63
|
+
const IMG_SWEEP_INTERVAL_MS = 60 * 60 * 1000;
|
|
64
|
+
let lastImageSweepAt = 0;
|
|
65
|
+
|
|
48
66
|
function log(msg: string): void {
|
|
49
67
|
try {
|
|
50
68
|
process.stderr.write(`[pi-picker] ${msg}\n`);
|
|
@@ -203,8 +221,49 @@ interface PickPayload {
|
|
|
203
221
|
submit?: boolean;
|
|
204
222
|
}
|
|
205
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Delete staged screenshots older than IMG_TTL_MS.
|
|
226
|
+
*
|
|
227
|
+
* Age comes from the file's mtime rather than from any index we keep, so images
|
|
228
|
+
* orphaned by a pi process that died mid-pick are collected too.
|
|
229
|
+
*
|
|
230
|
+
* Best-effort throughout, and `force: true` on removal: a second pi process
|
|
231
|
+
* sweeping the same shared tmpdir at the same moment must not be able to fail a
|
|
232
|
+
* pick, and a file that is already gone is the outcome we wanted anyway.
|
|
233
|
+
*
|
|
234
|
+
* `force` bypasses the throttle — used on session start, where one sweep per
|
|
235
|
+
* session is exactly the guarantee we want.
|
|
236
|
+
*/
|
|
237
|
+
function sweepStaleImages(force = false): void {
|
|
238
|
+
const now = Date.now();
|
|
239
|
+
if (!force && now - lastImageSweepAt < IMG_SWEEP_INTERVAL_MS) return;
|
|
240
|
+
lastImageSweepAt = now;
|
|
241
|
+
|
|
242
|
+
let removed = 0;
|
|
243
|
+
try {
|
|
244
|
+
for (const entry of readdirSync(IMG_DIR, { withFileTypes: true })) {
|
|
245
|
+
// Only ever touch regular files we could have written ourselves.
|
|
246
|
+
if (!entry.isFile()) continue;
|
|
247
|
+
const full = join(IMG_DIR, entry.name);
|
|
248
|
+
try {
|
|
249
|
+
if (now - statSync(full).mtimeMs <= IMG_TTL_MS) continue;
|
|
250
|
+
rmSync(full, { force: true });
|
|
251
|
+
removed++;
|
|
252
|
+
} catch {
|
|
253
|
+
/* vanished under us, or unreadable — nothing left to do for this one */
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
} catch {
|
|
257
|
+
return; // IMG_DIR does not exist yet: nothing has ever been staged
|
|
258
|
+
}
|
|
259
|
+
if (removed) log(`swept ${removed} expired screenshot(s) from ${IMG_DIR}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
206
262
|
function saveImages(images: PickImage[] | undefined): string[] {
|
|
207
263
|
const paths: string[] = [];
|
|
264
|
+
// Throttled, so a long-running session does not accumulate a week of screenshots
|
|
265
|
+
// behind it; session start forces a sweep, so a restart always collects.
|
|
266
|
+
sweepStaleImages();
|
|
208
267
|
(images ?? []).forEach((img, i) => {
|
|
209
268
|
const m = /^data:(image\/[a-z0-9.+-]+);base64,(.+)$/i.exec(img?.dataUrl ?? '');
|
|
210
269
|
if (!m) return;
|
|
@@ -467,6 +526,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
467
526
|
const port = typeof addr === 'object' && addr ? addr.port : 0;
|
|
468
527
|
mkdirSync(REG_DIR, { recursive: true });
|
|
469
528
|
sweepStaleRegistry();
|
|
529
|
+
sweepStaleImages(true);
|
|
470
530
|
regFile = join(REG_DIR, `${process.pid}.json`);
|
|
471
531
|
writeFileSync(
|
|
472
532
|
regFile,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-ele-picker",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Pick an element in your running page, describe the change, and send it to a pi session. Ships a Vite dev plugin (browser picker + discovery endpoint) and the matching pi bridge extension.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|