pi-ele-picker 0.1.2 → 0.2.0
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 +38 -1
- package/client/picker.js +155 -2
- package/dist/index.cjs +21 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +22 -5
- package/dist/index.js.map +1 -1
- package/extensions/pi-picker.ts +74 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,6 +9,8 @@ The picker lives entirely inside a Shadow DOM, so it never leaks styles into —
|
|
|
9
9
|
```
|
|
10
10
|
browser page ──(same-origin /__pi_picker__/config)──▶ Vite plugin ──▶ reads ~/.pi/agent/pi-picker/*.json
|
|
11
11
|
│ (discovers the live bridge)
|
|
12
|
+
├──(GET http://127.0.0.1:<port>/status, every 3s)───────────────────────▶ pi bridge extension
|
|
13
|
+
│ (session status → launcher dot)
|
|
12
14
|
└──(POST http://127.0.0.1:<port>/pick)────────────────────────────────▶ pi bridge extension
|
|
13
15
|
│
|
|
14
16
|
writes to the pi input box
|
|
@@ -72,6 +74,10 @@ piElePicker({
|
|
|
72
74
|
// Directory whose pi bridge to discover; must match the cwd the pi session
|
|
73
75
|
// was started in. Defaults to process.cwd().
|
|
74
76
|
root: process.cwd(),
|
|
77
|
+
// Project-level default for the in-page session-status switch. Off by default:
|
|
78
|
+
// the status poll costs one loopback request every 3s, so users opt in from the
|
|
79
|
+
// picker panel unless you flip this on for them.
|
|
80
|
+
status: false,
|
|
75
81
|
});
|
|
76
82
|
```
|
|
77
83
|
|
|
@@ -87,6 +93,7 @@ The launcher starts in the **bottom-right** corner and stays out of the way:
|
|
|
87
93
|
|
|
88
94
|
- **Draggable** — grab it and drop it anywhere; the position is remembered per browser (`localStorage`).
|
|
89
95
|
- **Fades when idle** (opacity `0.5`) and returns to full opacity on hover.
|
|
96
|
+
- **Shows the live pi session status** as a coloured dot **when enabled** — green while pi is running, amber while follow-ups are queued, grey while awaiting input, red when the bridge is gone (see [Session status](#session-status)).
|
|
90
97
|
- The panel always opens **next to the launcher**, flipping sides and clamping to stay on screen.
|
|
91
98
|
|
|
92
99
|
1. Click **🎯 pi 选择器** and pick an element on the page.
|
|
@@ -95,11 +102,41 @@ The launcher starts in the **bottom-right** corner and stays out of the way:
|
|
|
95
102
|
- **写入pi输入框** — pre-fills the current pi input box with the composed prompt; you edit and press Enter.
|
|
96
103
|
- **直接发送** — submits the composed prompt as a real user turn immediately (queued as a follow-up if pi is mid-turn).
|
|
97
104
|
|
|
105
|
+
### Session status
|
|
106
|
+
|
|
107
|
+
Status polling is **opt-in and off by default**: each cycle opens a loopback request, so it is disabled unless you want it. There are two levels:
|
|
108
|
+
|
|
109
|
+
- **Per browser (runtime):** the switch in the picker panel (labelled **状态**) turns polling on/off; the choice is remembered in `localStorage`.
|
|
110
|
+
- **Per project (build time):** the `status` option above sets the default for anyone who has not chosen yet.
|
|
111
|
+
|
|
112
|
+
Effective state = this browser's saved choice, else the project default. When it is off, the picker issues **no** status requests at all.
|
|
113
|
+
|
|
114
|
+
When enabled, the launcher carries a small status dot and the panel repeats it as text (status · model · session id). The picker polls the bridge's `GET /status` every 3 seconds (pausing while the tab is hidden and re-discovering the bridge after a pi restart):
|
|
115
|
+
|
|
116
|
+
| Dot | State | Meaning |
|
|
117
|
+
| --- | --- | --- |
|
|
118
|
+
| 🟢 pulsing | `running` | pi is streaming / executing a turn |
|
|
119
|
+
| 🟡 | `queued` | pi is idle but follow-up messages are queued |
|
|
120
|
+
| ⚪ | `idle` | pi is idle, waiting for your input |
|
|
121
|
+
| 🔴 | `offline` | no live bridge for this project — pi is stopped / not running here |
|
|
122
|
+
| ◯ hollow | — | status display is switched off |
|
|
123
|
+
|
|
124
|
+
The same payload is available from the bridge directly:
|
|
125
|
+
|
|
126
|
+
```jsonc
|
|
127
|
+
// GET http://127.0.0.1:<bridge port>/status
|
|
128
|
+
{ "ok": true, "status": "running", "idle": false, "pending": false,
|
|
129
|
+
"mode": "tui", "sessionId": "…", "model": "Claude Sonnet 4",
|
|
130
|
+
"provider": "anthropic", "pid": 12345, "cwd": "…", "updatedAt": 1700000000000 }
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`/status` needs no token, but non-loopback browser origins get `403`, so an arbitrary website cannot probe session metadata.
|
|
134
|
+
|
|
98
135
|
## Notes
|
|
99
136
|
|
|
100
137
|
- The Vite plugin only runs during `vite dev` (`apply: 'serve'`); it has no effect on builds.
|
|
101
138
|
- 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.
|
|
102
|
-
- 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.
|
|
139
|
+
- 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.
|
|
103
140
|
- The bridge never auto-submits on its own: **写入pi输入框** only fills the editor; **直接发送** is an explicit user action in the page.
|
|
104
141
|
|
|
105
142
|
## License
|
package/client/picker.js
CHANGED
|
@@ -46,6 +46,23 @@
|
|
|
46
46
|
'.trigger:hover { background: #4338ca; }',
|
|
47
47
|
'.trigger.dragging { cursor: grabbing; }',
|
|
48
48
|
'.trigger.active { background: #059669; }',
|
|
49
|
+
'.dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; background: #6b7280; transition: background-color .2s ease; }',
|
|
50
|
+
'.dot.st-running { background: #34d399; animation: pipulse 1.4s ease-out infinite; }',
|
|
51
|
+
'.dot.st-queued { background: #fbbf24; }',
|
|
52
|
+
'.dot.st-idle { background: #9a9aa5; }',
|
|
53
|
+
'.dot.st-offline { background: #f87171; }',
|
|
54
|
+
'.dot.st-unknown { background: #6b7280; }',
|
|
55
|
+
'.dot.st-off { background: transparent; box-shadow: inset 0 0 0 1.5px #6b7280; }',
|
|
56
|
+
'@keyframes pipulse { 0% { box-shadow: 0 0 0 0 rgba(52,211,153,.6); } 70% { box-shadow: 0 0 0 6px rgba(52,211,153,0); } 100% { box-shadow: 0 0 0 0 rgba(52,211,153,0); } }',
|
|
57
|
+
'.session { display: flex; align-items: center; gap: 6px; padding: 8px 12px 0; color: #b9b9c2; font-size: 11px; }',
|
|
58
|
+
'.session .st { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }',
|
|
59
|
+
'.sw { display: inline-flex; align-items: center; gap: 5px; flex: 0 0 auto; cursor: pointer; color: #9a9aa5; }',
|
|
60
|
+
'.sw input { appearance: none; -webkit-appearance: none; width: 28px; height: 16px; margin: 0; border-radius: 999px; background: #34353d; position: relative; cursor: pointer; transition: background-color .15s ease; }',
|
|
61
|
+
'.sw input::after { content: ""; position: absolute; top: 2px; left: 2px; width: 12px; height: 12px; border-radius: 50%; background: #cfcfd6; transition: transform .15s ease; }',
|
|
62
|
+
'.sw input:checked { background: #4f46e5; }',
|
|
63
|
+
'.sw input:checked::after { transform: translateX(12px); background: #fff; }',
|
|
64
|
+
'.sw input:focus-visible { outline: 2px solid #4f46e5; outline-offset: 2px; }',
|
|
65
|
+
'.sw .sw-label { font-size: 11px; }',
|
|
49
66
|
'.panel { position: fixed; width: 380px; max-width: calc(100vw - 36px); z-index: 2147483647; pointer-events: auto; background: #1b1c22; color: #e7e7ea; border: 1px solid #34353d; border-radius: 12px; box-shadow: 0 18px 48px rgba(0,0,0,.5); font-size: 13px; display: none; }',
|
|
50
67
|
'.panel.open { display: block; }',
|
|
51
68
|
'.hd { display: flex; align-items: center; gap: 8px; padding: 10px 12px; border-bottom: 1px solid #2b2c33; }',
|
|
@@ -96,7 +113,10 @@
|
|
|
96
113
|
var trigger = document.createElement('button');
|
|
97
114
|
trigger.className = 'trigger';
|
|
98
115
|
trigger.type = 'button';
|
|
99
|
-
|
|
116
|
+
var triggerDot = document.createElement('span');
|
|
117
|
+
triggerDot.className = 'dot st-off';
|
|
118
|
+
trigger.appendChild(triggerDot);
|
|
119
|
+
trigger.appendChild(document.createTextNode('pi picker'));
|
|
100
120
|
trigger.title = '拖拽可移动 · 点击开始选择元素';
|
|
101
121
|
root.appendChild(trigger);
|
|
102
122
|
|
|
@@ -104,6 +124,7 @@
|
|
|
104
124
|
panel.className = 'panel';
|
|
105
125
|
panel.innerHTML =
|
|
106
126
|
'<div class="hd"><div class="title">元素修改</div><button class="close" type="button" title="关闭">✕</button></div>' +
|
|
127
|
+
'<div class="session"><span class="dot st-off"></span><span class="st">状态显示已关闭</span><label class="sw" title="每 3 秒同步一次 pi 会话状态;关闭后不再发起任何轮询请求"><input type="checkbox" class="status-toggle" /><span class="sw-label">状态</span></label></div>' +
|
|
107
128
|
'<div class="meta"></div>' +
|
|
108
129
|
'<textarea placeholder="描述你想要的修改,例如:把这个按钮改成圆角主色、增加 hover 效果…(可直接粘贴截图)"></textarea>' +
|
|
109
130
|
'<div class="shots"></div>' +
|
|
@@ -113,6 +134,9 @@
|
|
|
113
134
|
root.appendChild(panel);
|
|
114
135
|
|
|
115
136
|
var metaEl = panel.querySelector('.meta');
|
|
137
|
+
var sessionDot = panel.querySelector('.session .dot');
|
|
138
|
+
var sessionText = panel.querySelector('.session .st');
|
|
139
|
+
var statusToggle = panel.querySelector('.status-toggle');
|
|
116
140
|
var taEl = panel.querySelector('textarea');
|
|
117
141
|
var shotsEl = panel.querySelector('.shots');
|
|
118
142
|
var statusEl = panel.querySelector('.status');
|
|
@@ -638,5 +662,134 @@
|
|
|
638
662
|
readFiles(e.dataTransfer && e.dataTransfer.files);
|
|
639
663
|
});
|
|
640
664
|
|
|
641
|
-
|
|
665
|
+
// --- live session status (opt-in) ----------------------------------------
|
|
666
|
+
// Polling is OFF by default because each cycle opens a loopback connection.
|
|
667
|
+
// Users opt in per browser with the panel switch, or a project flips the
|
|
668
|
+
// default with the Vite option (`piElePicker({ status: true })`). While enabled
|
|
669
|
+
// the launcher dot + panel line show whether pi is running, waiting for input,
|
|
670
|
+
// holding queued follow-ups, or gone. Polling pauses on a hidden tab and drops
|
|
671
|
+
// a stale port so the next cycle re-discovers a restarted bridge.
|
|
672
|
+
var STATUS_INTERVAL = 3000;
|
|
673
|
+
var STATUS_KEY = 'pi-ele-picker:status:v1';
|
|
674
|
+
var statusTimer = null;
|
|
675
|
+
var statusTitle = '';
|
|
676
|
+
var statusEnabled = false;
|
|
677
|
+
var STATUS_LABEL = {
|
|
678
|
+
running: 'pi 执行中',
|
|
679
|
+
queued: 'pi 排队中',
|
|
680
|
+
idle: 'pi 等待输入',
|
|
681
|
+
offline: 'pi 已停止 / 未连接',
|
|
682
|
+
unknown: 'pi 状态检测中…',
|
|
683
|
+
off: '状态显示已关闭',
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
function renderStatus(info) {
|
|
687
|
+
var st = info && info.status;
|
|
688
|
+
if (!STATUS_LABEL[st]) st = 'unknown';
|
|
689
|
+
var cls = 'dot st-' + st;
|
|
690
|
+
triggerDot.className = cls;
|
|
691
|
+
sessionDot.className = cls;
|
|
692
|
+
var detail = [];
|
|
693
|
+
if (info && info.model) detail.push(info.model);
|
|
694
|
+
if (info && info.sessionId) detail.push('会话 ' + String(info.sessionId).slice(0, 8));
|
|
695
|
+
var line = STATUS_LABEL[st] + (detail.length ? ' · ' + detail.join(' · ') : '');
|
|
696
|
+
sessionText.textContent = line;
|
|
697
|
+
var title = line + ' · 拖拽可移动 · 点击开始选择元素';
|
|
698
|
+
if (title !== statusTitle) {
|
|
699
|
+
trigger.title = title;
|
|
700
|
+
statusTitle = title;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function readStatusOverride() {
|
|
705
|
+
try {
|
|
706
|
+
var v = localStorage.getItem(STATUS_KEY);
|
|
707
|
+
if (v === 'on') return true;
|
|
708
|
+
if (v === 'off') return false;
|
|
709
|
+
} catch (e) {
|
|
710
|
+
/* storage unavailable — fall back to the project default */
|
|
711
|
+
}
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function writeStatusOverride(enabled) {
|
|
716
|
+
try {
|
|
717
|
+
localStorage.setItem(STATUS_KEY, enabled ? 'on' : 'off');
|
|
718
|
+
} catch (e) {
|
|
719
|
+
/* ignore */
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function stopStatusPoll() {
|
|
724
|
+
if (statusTimer) {
|
|
725
|
+
clearTimeout(statusTimer);
|
|
726
|
+
statusTimer = null;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/** Turn polling on/off; `persist` records the choice as this browser's override. */
|
|
731
|
+
function setStatusEnabled(enabled, persist) {
|
|
732
|
+
statusEnabled = !!enabled;
|
|
733
|
+
if (persist) writeStatusOverride(statusEnabled);
|
|
734
|
+
if (statusToggle) statusToggle.checked = statusEnabled;
|
|
735
|
+
if (statusEnabled) {
|
|
736
|
+
renderStatus({ status: 'unknown' });
|
|
737
|
+
pollStatus();
|
|
738
|
+
} else {
|
|
739
|
+
stopStatusPoll();
|
|
740
|
+
renderStatus({ status: 'off' });
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/** Never rejects: any failure becomes the offline state. */
|
|
745
|
+
function fetchStatus() {
|
|
746
|
+
return loadConfig(false).then(function (c) {
|
|
747
|
+
if (!c || !c.port) return { status: 'offline' };
|
|
748
|
+
return fetch('http://127.0.0.1:' + c.port + '/status', { cache: 'no-store' })
|
|
749
|
+
.then(function (r) {
|
|
750
|
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
751
|
+
return r.json();
|
|
752
|
+
})
|
|
753
|
+
.catch(function () {
|
|
754
|
+
// The cached port may be stale after a pi restart: clear it so the next
|
|
755
|
+
// poll re-reads the config and finds the new bridge.
|
|
756
|
+
cfg = { port: null, token: null };
|
|
757
|
+
return { status: 'offline' };
|
|
758
|
+
});
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function scheduleStatusPoll() {
|
|
763
|
+
if (statusTimer) clearTimeout(statusTimer);
|
|
764
|
+
statusTimer = setTimeout(pollStatus, STATUS_INTERVAL);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function pollStatus() {
|
|
768
|
+
if (!statusEnabled) return;
|
|
769
|
+
if (document.hidden) {
|
|
770
|
+
scheduleStatusPoll();
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
fetchStatus()
|
|
774
|
+
.then(renderStatus)
|
|
775
|
+
.catch(function () {
|
|
776
|
+
renderStatus({ status: 'offline' });
|
|
777
|
+
})
|
|
778
|
+
.then(scheduleStatusPoll);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
document.addEventListener('visibilitychange', function () {
|
|
782
|
+
if (!document.hidden && statusEnabled) pollStatus();
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
statusToggle.addEventListener('change', function () {
|
|
786
|
+
setStatusEnabled(statusToggle.checked, true);
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
// Resolve the project default first (statusDefault from the Vite plugin), then
|
|
790
|
+
// apply this browser's override if the user has one.
|
|
791
|
+
loadConfig().then(function () {
|
|
792
|
+
var override = readStatusOverride();
|
|
793
|
+
setStatusEnabled(override !== null ? override : cfg && cfg.statusDefault === true, false);
|
|
794
|
+
});
|
|
642
795
|
})();
|
package/dist/index.cjs
CHANGED
|
@@ -35,7 +35,16 @@ var import_node_os = require("os");
|
|
|
35
35
|
var import_node_url = require("url");
|
|
36
36
|
var REG_DIR = (0, import_node_path.join)((0, import_node_os.homedir)(), ".pi", "agent", "pi-picker");
|
|
37
37
|
var ENDPOINT = "/__pi_picker__";
|
|
38
|
-
var
|
|
38
|
+
var OWN_CLIENT_FILE = (0, import_node_url.fileURLToPath)(new URL("../client/picker.js", importMetaUrl));
|
|
39
|
+
function resolveClientFile(projectRoot) {
|
|
40
|
+
for (let dir = projectRoot; dir; ) {
|
|
41
|
+
const candidate = (0, import_node_path.join)(dir, "node_modules", "pi-ele-picker", "client", "picker.js");
|
|
42
|
+
if ((0, import_node_fs.existsSync)(candidate)) return candidate;
|
|
43
|
+
const parent = (0, import_node_path.dirname)(dir);
|
|
44
|
+
dir = parent === dir ? "" : parent;
|
|
45
|
+
}
|
|
46
|
+
return (0, import_node_fs.existsSync)(OWN_CLIENT_FILE) ? OWN_CLIENT_FILE : null;
|
|
47
|
+
}
|
|
39
48
|
function isPidAlive(pid) {
|
|
40
49
|
try {
|
|
41
50
|
process.kill(pid, 0);
|
|
@@ -65,6 +74,7 @@ function discoverBridge(root) {
|
|
|
65
74
|
}
|
|
66
75
|
function piElePicker(options = {}) {
|
|
67
76
|
const root = options.root ?? process.cwd();
|
|
77
|
+
const statusDefault = options.status ?? false;
|
|
68
78
|
return {
|
|
69
79
|
name: "pi-ele-picker",
|
|
70
80
|
apply: "serve",
|
|
@@ -84,18 +94,25 @@ function piElePicker(options = {}) {
|
|
|
84
94
|
const info = discoverBridge(root);
|
|
85
95
|
res.setHeader("content-type", "application/json");
|
|
86
96
|
res.setHeader("cache-control", "no-store");
|
|
87
|
-
res.end(
|
|
97
|
+
res.end(
|
|
98
|
+
JSON.stringify({
|
|
99
|
+
port: info ? info.port : null,
|
|
100
|
+
token: info ? info.token : null,
|
|
101
|
+
statusDefault
|
|
102
|
+
})
|
|
103
|
+
);
|
|
88
104
|
return;
|
|
89
105
|
}
|
|
90
106
|
if (url === "/picker.js") {
|
|
91
|
-
|
|
107
|
+
const clientFile = resolveClientFile(server.config.root);
|
|
108
|
+
if (!clientFile) {
|
|
92
109
|
res.statusCode = 404;
|
|
93
110
|
res.end("pi-ele-picker client missing");
|
|
94
111
|
return;
|
|
95
112
|
}
|
|
96
113
|
res.setHeader("content-type", "text/javascript");
|
|
97
114
|
res.setHeader("cache-control", "no-store");
|
|
98
|
-
res.end((0, import_node_fs.readFileSync)(
|
|
115
|
+
res.end((0, import_node_fs.readFileSync)(clientFile));
|
|
99
116
|
return;
|
|
100
117
|
}
|
|
101
118
|
next();
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../node_modules/tsup/assets/cjs_shims.js"],"sourcesContent":["import type { Plugin } from 'vite';\nimport { existsSync, readFileSync, readdirSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { homedir } from 'node:os';\nimport { fileURLToPath } from 'node:url';\n\n/**\n * pi-ele-picker\n *\n * A Vite dev-only plugin that injects the pi element picker into the page and\n * exposes a same-origin endpoint the picker uses to discover the running pi\n * bridge.\n *\n * The bridge registry is written by the pi extension at\n * `~/.pi/agent/extensions/pi-picker.ts` (or any extension implementing the same\n * `~/.pi/agent/pi-picker/<pid>.json` registry + `POST /pick` protocol).\n */\n\n/** Directory where pi bridge extensions register themselves. */\nconst REG_DIR = join(homedir(), '.pi', 'agent', 'pi-picker');\n\n/** URL prefix under which the picker client and discovery endpoint are served. */\nconst ENDPOINT = '/__pi_picker__';\n\n/**
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../node_modules/tsup/assets/cjs_shims.js"],"sourcesContent":["import type { Plugin } from 'vite';\nimport { existsSync, readFileSync, readdirSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { homedir } from 'node:os';\nimport { fileURLToPath } from 'node:url';\n\n/**\n * pi-ele-picker\n *\n * A Vite dev-only plugin that injects the pi element picker into the page and\n * exposes a same-origin endpoint the picker uses to discover the running pi\n * bridge.\n *\n * The bridge registry is written by the pi extension at\n * `~/.pi/agent/extensions/pi-picker.ts` (or any extension implementing the same\n * `~/.pi/agent/pi-picker/<pid>.json` registry + `POST /pick` protocol).\n */\n\n/** Directory where pi bridge extensions register themselves. */\nconst REG_DIR = join(homedir(), '.pi', 'agent', 'pi-picker');\n\n/** URL prefix under which the picker client and discovery endpoint are served. */\nconst ENDPOINT = '/__pi_picker__';\n\n/** Path to this package's own browser client, anchored at the plugin's install location. */\nconst OWN_CLIENT_FILE = fileURLToPath(new URL('../client/picker.js', import.meta.url));\n\n/**\n * Resolve the browser client to serve. Called per request, never cached.\n *\n * The plugin module is evaluated once when the dev server boots, so anything\n * captured at module scope is frozen for the life of that process. Upgrading\n * pi-ele-picker while a dev server is running therefore used to keep serving the\n * old client forever: the frozen path still pointed at\n * `node_modules/.pnpm/pi-ele-picker@<old>/...`, pnpm leaves that directory on\n * disk, and the stale client was handed out silently — refreshing never helped.\n *\n * Resolving from the project root instead means the currently installed copy\n * wins, with no server restart.\n *\n * Two traps this deliberately avoids:\n *\n * 1. Anchoring at `import.meta.url`. From inside `.../pi-ele-picker@<old>/dist/`\n * Node resolves `pi-ele-picker` straight back to that same old copy.\n * 2. `require.resolve()`. Node memoises resolutions in `Module._pathCache` and\n * never invalidates them when `node_modules` changes underneath a running\n * process, which pins the first lookup forever — the same staleness, moved.\n *\n * So the lookup is a plain filesystem walk, mirroring Node's own node_modules\n * search (upward from the project root, for workspace/monorepo layouts), and it\n * falls back to this package's own copy when nothing matches.\n */\nfunction resolveClientFile(projectRoot: string): string | null {\n for (let dir = projectRoot; dir; ) {\n const candidate = join(dir, 'node_modules', 'pi-ele-picker', 'client', 'picker.js');\n if (existsSync(candidate)) return candidate;\n const parent = dirname(dir);\n dir = parent === dir ? '' : parent; // stop at the filesystem root\n }\n return existsSync(OWN_CLIENT_FILE) ? OWN_CLIENT_FILE : null;\n}\n\nexport interface PiElePickerOptions {\n /**\n * Directory whose pi bridge to discover. Must match the `cwd` the pi session\n * was started in. Defaults to `process.cwd()`.\n */\n root?: string;\n /**\n * Project-level default for the in-page \"session status\" toggle. The picker\n * polls the bridge every 3s to show whether pi is running/idle/stopped, which\n * costs a loopback request per cycle; it is therefore **off by default**.\n *\n * This is only a default: each browser can override it with the switch in the\n * picker panel (remembered in `localStorage`). Defaults to `false`.\n */\n status?: boolean;\n}\n\ninterface BridgeInfo {\n port: number;\n pid: number;\n cwd: string;\n token: string;\n updatedAt: number;\n}\n\nfunction isPidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n}\n\n/** Find the freshest live bridge registered for the given project root. */\nfunction discoverBridge(root: string): BridgeInfo | null {\n let files: string[];\n try {\n files = readdirSync(REG_DIR).filter((f) => f.endsWith('.json'));\n } catch {\n return null;\n }\n let best: BridgeInfo | null = null;\n for (const f of files) {\n try {\n const info = JSON.parse(readFileSync(join(REG_DIR, f), 'utf8')) as BridgeInfo;\n if (info.cwd !== root) continue;\n if (!isPidAlive(info.pid)) continue;\n if (!best || info.updatedAt > best.updatedAt) best = info;\n } catch {\n /* skip malformed */\n }\n }\n return best;\n}\n\nexport default function piElePicker(options: PiElePickerOptions = {}): Plugin {\n const root = options.root ?? process.cwd();\n const statusDefault = options.status ?? false;\n\n return {\n name: 'pi-ele-picker',\n apply: 'serve',\n transformIndexHtml() {\n return [\n {\n tag: 'script',\n attrs: { type: 'module', src: `${ENDPOINT}/picker.js` },\n injectTo: 'body',\n },\n ];\n },\n configureServer(server) {\n server.middlewares.use(ENDPOINT, (req, res, next) => {\n const url = (req.url ?? '').split('?')[0];\n\n if (url === '/config') {\n const info = discoverBridge(root);\n res.setHeader('content-type', 'application/json');\n res.setHeader('cache-control', 'no-store');\n res.end(\n JSON.stringify({\n port: info ? info.port : null,\n token: info ? info.token : null,\n statusDefault,\n })\n );\n return;\n }\n\n if (url === '/picker.js') {\n const clientFile = resolveClientFile(server.config.root);\n if (!clientFile) {\n res.statusCode = 404;\n res.end('pi-ele-picker client missing');\n return;\n }\n res.setHeader('content-type', 'text/javascript');\n res.setHeader('cache-control', 'no-store');\n res.end(readFileSync(clientFile));\n return;\n }\n\n next();\n });\n },\n };\n}\n","// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () => \n typeof document === \"undefined\" \n ? new URL(`file:${__filename}`).href \n : (document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT') \n ? document.currentScript.src \n : new URL(\"main.js\", document.baseURI).href;\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -6,6 +6,15 @@ interface PiElePickerOptions {
|
|
|
6
6
|
* was started in. Defaults to `process.cwd()`.
|
|
7
7
|
*/
|
|
8
8
|
root?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Project-level default for the in-page "session status" toggle. The picker
|
|
11
|
+
* polls the bridge every 3s to show whether pi is running/idle/stopped, which
|
|
12
|
+
* costs a loopback request per cycle; it is therefore **off by default**.
|
|
13
|
+
*
|
|
14
|
+
* This is only a default: each browser can override it with the switch in the
|
|
15
|
+
* picker panel (remembered in `localStorage`). Defaults to `false`.
|
|
16
|
+
*/
|
|
17
|
+
status?: boolean;
|
|
9
18
|
}
|
|
10
19
|
declare function piElePicker(options?: PiElePickerOptions): Plugin;
|
|
11
20
|
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,15 @@ interface PiElePickerOptions {
|
|
|
6
6
|
* was started in. Defaults to `process.cwd()`.
|
|
7
7
|
*/
|
|
8
8
|
root?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Project-level default for the in-page "session status" toggle. The picker
|
|
11
|
+
* polls the bridge every 3s to show whether pi is running/idle/stopped, which
|
|
12
|
+
* costs a loopback request per cycle; it is therefore **off by default**.
|
|
13
|
+
*
|
|
14
|
+
* This is only a default: each browser can override it with the switch in the
|
|
15
|
+
* picker panel (remembered in `localStorage`). Defaults to `false`.
|
|
16
|
+
*/
|
|
17
|
+
status?: boolean;
|
|
9
18
|
}
|
|
10
19
|
declare function piElePicker(options?: PiElePickerOptions): Plugin;
|
|
11
20
|
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
3
|
-
import { join } from "path";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
4
|
import { homedir } from "os";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
var REG_DIR = join(homedir(), ".pi", "agent", "pi-picker");
|
|
7
7
|
var ENDPOINT = "/__pi_picker__";
|
|
8
|
-
var
|
|
8
|
+
var OWN_CLIENT_FILE = fileURLToPath(new URL("../client/picker.js", import.meta.url));
|
|
9
|
+
function resolveClientFile(projectRoot) {
|
|
10
|
+
for (let dir = projectRoot; dir; ) {
|
|
11
|
+
const candidate = join(dir, "node_modules", "pi-ele-picker", "client", "picker.js");
|
|
12
|
+
if (existsSync(candidate)) return candidate;
|
|
13
|
+
const parent = dirname(dir);
|
|
14
|
+
dir = parent === dir ? "" : parent;
|
|
15
|
+
}
|
|
16
|
+
return existsSync(OWN_CLIENT_FILE) ? OWN_CLIENT_FILE : null;
|
|
17
|
+
}
|
|
9
18
|
function isPidAlive(pid) {
|
|
10
19
|
try {
|
|
11
20
|
process.kill(pid, 0);
|
|
@@ -35,6 +44,7 @@ function discoverBridge(root) {
|
|
|
35
44
|
}
|
|
36
45
|
function piElePicker(options = {}) {
|
|
37
46
|
const root = options.root ?? process.cwd();
|
|
47
|
+
const statusDefault = options.status ?? false;
|
|
38
48
|
return {
|
|
39
49
|
name: "pi-ele-picker",
|
|
40
50
|
apply: "serve",
|
|
@@ -54,18 +64,25 @@ function piElePicker(options = {}) {
|
|
|
54
64
|
const info = discoverBridge(root);
|
|
55
65
|
res.setHeader("content-type", "application/json");
|
|
56
66
|
res.setHeader("cache-control", "no-store");
|
|
57
|
-
res.end(
|
|
67
|
+
res.end(
|
|
68
|
+
JSON.stringify({
|
|
69
|
+
port: info ? info.port : null,
|
|
70
|
+
token: info ? info.token : null,
|
|
71
|
+
statusDefault
|
|
72
|
+
})
|
|
73
|
+
);
|
|
58
74
|
return;
|
|
59
75
|
}
|
|
60
76
|
if (url === "/picker.js") {
|
|
61
|
-
|
|
77
|
+
const clientFile = resolveClientFile(server.config.root);
|
|
78
|
+
if (!clientFile) {
|
|
62
79
|
res.statusCode = 404;
|
|
63
80
|
res.end("pi-ele-picker client missing");
|
|
64
81
|
return;
|
|
65
82
|
}
|
|
66
83
|
res.setHeader("content-type", "text/javascript");
|
|
67
84
|
res.setHeader("cache-control", "no-store");
|
|
68
|
-
res.end(readFileSync(
|
|
85
|
+
res.end(readFileSync(clientFile));
|
|
69
86
|
return;
|
|
70
87
|
}
|
|
71
88
|
next();
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Plugin } from 'vite';\nimport { existsSync, readFileSync, readdirSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { homedir } from 'node:os';\nimport { fileURLToPath } from 'node:url';\n\n/**\n * pi-ele-picker\n *\n * A Vite dev-only plugin that injects the pi element picker into the page and\n * exposes a same-origin endpoint the picker uses to discover the running pi\n * bridge.\n *\n * The bridge registry is written by the pi extension at\n * `~/.pi/agent/extensions/pi-picker.ts` (or any extension implementing the same\n * `~/.pi/agent/pi-picker/<pid>.json` registry + `POST /pick` protocol).\n */\n\n/** Directory where pi bridge extensions register themselves. */\nconst REG_DIR = join(homedir(), '.pi', 'agent', 'pi-picker');\n\n/** URL prefix under which the picker client and discovery endpoint are served. */\nconst ENDPOINT = '/__pi_picker__';\n\n/**
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Plugin } from 'vite';\nimport { existsSync, readFileSync, readdirSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { homedir } from 'node:os';\nimport { fileURLToPath } from 'node:url';\n\n/**\n * pi-ele-picker\n *\n * A Vite dev-only plugin that injects the pi element picker into the page and\n * exposes a same-origin endpoint the picker uses to discover the running pi\n * bridge.\n *\n * The bridge registry is written by the pi extension at\n * `~/.pi/agent/extensions/pi-picker.ts` (or any extension implementing the same\n * `~/.pi/agent/pi-picker/<pid>.json` registry + `POST /pick` protocol).\n */\n\n/** Directory where pi bridge extensions register themselves. */\nconst REG_DIR = join(homedir(), '.pi', 'agent', 'pi-picker');\n\n/** URL prefix under which the picker client and discovery endpoint are served. */\nconst ENDPOINT = '/__pi_picker__';\n\n/** Path to this package's own browser client, anchored at the plugin's install location. */\nconst OWN_CLIENT_FILE = fileURLToPath(new URL('../client/picker.js', import.meta.url));\n\n/**\n * Resolve the browser client to serve. Called per request, never cached.\n *\n * The plugin module is evaluated once when the dev server boots, so anything\n * captured at module scope is frozen for the life of that process. Upgrading\n * pi-ele-picker while a dev server is running therefore used to keep serving the\n * old client forever: the frozen path still pointed at\n * `node_modules/.pnpm/pi-ele-picker@<old>/...`, pnpm leaves that directory on\n * disk, and the stale client was handed out silently — refreshing never helped.\n *\n * Resolving from the project root instead means the currently installed copy\n * wins, with no server restart.\n *\n * Two traps this deliberately avoids:\n *\n * 1. Anchoring at `import.meta.url`. From inside `.../pi-ele-picker@<old>/dist/`\n * Node resolves `pi-ele-picker` straight back to that same old copy.\n * 2. `require.resolve()`. Node memoises resolutions in `Module._pathCache` and\n * never invalidates them when `node_modules` changes underneath a running\n * process, which pins the first lookup forever — the same staleness, moved.\n *\n * So the lookup is a plain filesystem walk, mirroring Node's own node_modules\n * search (upward from the project root, for workspace/monorepo layouts), and it\n * falls back to this package's own copy when nothing matches.\n */\nfunction resolveClientFile(projectRoot: string): string | null {\n for (let dir = projectRoot; dir; ) {\n const candidate = join(dir, 'node_modules', 'pi-ele-picker', 'client', 'picker.js');\n if (existsSync(candidate)) return candidate;\n const parent = dirname(dir);\n dir = parent === dir ? '' : parent; // stop at the filesystem root\n }\n return existsSync(OWN_CLIENT_FILE) ? OWN_CLIENT_FILE : null;\n}\n\nexport interface PiElePickerOptions {\n /**\n * Directory whose pi bridge to discover. Must match the `cwd` the pi session\n * was started in. Defaults to `process.cwd()`.\n */\n root?: string;\n /**\n * Project-level default for the in-page \"session status\" toggle. The picker\n * polls the bridge every 3s to show whether pi is running/idle/stopped, which\n * costs a loopback request per cycle; it is therefore **off by default**.\n *\n * This is only a default: each browser can override it with the switch in the\n * picker panel (remembered in `localStorage`). Defaults to `false`.\n */\n status?: boolean;\n}\n\ninterface BridgeInfo {\n port: number;\n pid: number;\n cwd: string;\n token: string;\n updatedAt: number;\n}\n\nfunction isPidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n}\n\n/** Find the freshest live bridge registered for the given project root. */\nfunction discoverBridge(root: string): BridgeInfo | null {\n let files: string[];\n try {\n files = readdirSync(REG_DIR).filter((f) => f.endsWith('.json'));\n } catch {\n return null;\n }\n let best: BridgeInfo | null = null;\n for (const f of files) {\n try {\n const info = JSON.parse(readFileSync(join(REG_DIR, f), 'utf8')) as BridgeInfo;\n if (info.cwd !== root) continue;\n if (!isPidAlive(info.pid)) continue;\n if (!best || info.updatedAt > best.updatedAt) best = info;\n } catch {\n /* skip malformed */\n }\n }\n return best;\n}\n\nexport default function piElePicker(options: PiElePickerOptions = {}): Plugin {\n const root = options.root ?? process.cwd();\n const statusDefault = options.status ?? false;\n\n return {\n name: 'pi-ele-picker',\n apply: 'serve',\n transformIndexHtml() {\n return [\n {\n tag: 'script',\n attrs: { type: 'module', src: `${ENDPOINT}/picker.js` },\n injectTo: 'body',\n },\n ];\n },\n configureServer(server) {\n server.middlewares.use(ENDPOINT, (req, res, next) => {\n const url = (req.url ?? '').split('?')[0];\n\n if (url === '/config') {\n const info = discoverBridge(root);\n res.setHeader('content-type', 'application/json');\n res.setHeader('cache-control', 'no-store');\n res.end(\n JSON.stringify({\n port: info ? info.port : null,\n token: info ? info.token : null,\n statusDefault,\n })\n );\n return;\n }\n\n if (url === '/picker.js') {\n const clientFile = resolveClientFile(server.config.root);\n if (!clientFile) {\n res.statusCode = 404;\n res.end('pi-ele-picker client missing');\n return;\n }\n res.setHeader('content-type', 'text/javascript');\n res.setHeader('cache-control', 'no-store');\n res.end(readFileSync(clientFile));\n return;\n }\n\n next();\n });\n },\n };\n}\n"],"mappings":";AACA,SAAS,YAAY,cAAc,mBAAmB;AACtD,SAAS,SAAS,YAAY;AAC9B,SAAS,eAAe;AACxB,SAAS,qBAAqB;AAe9B,IAAM,UAAU,KAAK,QAAQ,GAAG,OAAO,SAAS,WAAW;AAG3D,IAAM,WAAW;AAGjB,IAAM,kBAAkB,cAAc,IAAI,IAAI,uBAAuB,YAAY,GAAG,CAAC;AA2BrF,SAAS,kBAAkB,aAAoC;AAC7D,WAAS,MAAM,aAAa,OAAO;AACjC,UAAM,YAAY,KAAK,KAAK,gBAAgB,iBAAiB,UAAU,WAAW;AAClF,QAAI,WAAW,SAAS,EAAG,QAAO;AAClC,UAAM,SAAS,QAAQ,GAAG;AAC1B,UAAM,WAAW,MAAM,KAAK;AAAA,EAC9B;AACA,SAAO,WAAW,eAAe,IAAI,kBAAkB;AACzD;AA2BA,SAAS,WAAW,KAAsB;AACxC,MAAI;AACF,YAAQ,KAAK,KAAK,CAAC;AACnB,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,SAAS,eAAe,MAAiC;AACvD,MAAI;AACJ,MAAI;AACF,YAAQ,YAAY,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE,SAAS,OAAO,CAAC;AAAA,EAChE,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,OAA0B;AAC9B,aAAW,KAAK,OAAO;AACrB,QAAI;AACF,YAAM,OAAO,KAAK,MAAM,aAAa,KAAK,SAAS,CAAC,GAAG,MAAM,CAAC;AAC9D,UAAI,KAAK,QAAQ,KAAM;AACvB,UAAI,CAAC,WAAW,KAAK,GAAG,EAAG;AAC3B,UAAI,CAAC,QAAQ,KAAK,YAAY,KAAK,UAAW,QAAO;AAAA,IACvD,QAAQ;AAAA,IAER;AAAA,EACF;AACA,SAAO;AACT;AAEe,SAAR,YAA6B,UAA8B,CAAC,GAAW;AAC5E,QAAM,OAAO,QAAQ,QAAQ,QAAQ,IAAI;AACzC,QAAM,gBAAgB,QAAQ,UAAU;AAExC,SAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO;AAAA,IACP,qBAAqB;AACnB,aAAO;AAAA,QACL;AAAA,UACE,KAAK;AAAA,UACL,OAAO,EAAE,MAAM,UAAU,KAAK,GAAG,QAAQ,aAAa;AAAA,UACtD,UAAU;AAAA,QACZ;AAAA,MACF;AAAA,IACF;AAAA,IACA,gBAAgB,QAAQ;AACtB,aAAO,YAAY,IAAI,UAAU,CAAC,KAAK,KAAK,SAAS;AACnD,cAAM,OAAO,IAAI,OAAO,IAAI,MAAM,GAAG,EAAE,CAAC;AAExC,YAAI,QAAQ,WAAW;AACrB,gBAAM,OAAO,eAAe,IAAI;AAChC,cAAI,UAAU,gBAAgB,kBAAkB;AAChD,cAAI,UAAU,iBAAiB,UAAU;AACzC,cAAI;AAAA,YACF,KAAK,UAAU;AAAA,cACb,MAAM,OAAO,KAAK,OAAO;AAAA,cACzB,OAAO,OAAO,KAAK,QAAQ;AAAA,cAC3B;AAAA,YACF,CAAC;AAAA,UACH;AACA;AAAA,QACF;AAEA,YAAI,QAAQ,cAAc;AACxB,gBAAM,aAAa,kBAAkB,OAAO,OAAO,IAAI;AACvD,cAAI,CAAC,YAAY;AACf,gBAAI,aAAa;AACjB,gBAAI,IAAI,8BAA8B;AACtC;AAAA,UACF;AACA,cAAI,UAAU,gBAAgB,iBAAiB;AAC/C,cAAI,UAAU,iBAAiB,UAAU;AACzC,cAAI,IAAI,aAAa,UAAU,CAAC;AAChC;AAAA,QACF;AAEA,aAAK;AAAA,MACP,CAAC;AAAA,IACH;AAAA,EACF;AACF;","names":[]}
|
package/extensions/pi-picker.ts
CHANGED
|
@@ -17,6 +17,15 @@
|
|
|
17
17
|
* reply: { ok: true, mode, inserted } | { ok: true, mode, submitted, queued? }
|
|
18
18
|
* | { ok: false, error }
|
|
19
19
|
*
|
|
20
|
+
* Wire protocol (GET /status):
|
|
21
|
+
* reply: { ok: true, status: "running" | "queued" | "idle", idle, pending,
|
|
22
|
+
* mode, sessionId, model, provider, pid, cwd, updatedAt }
|
|
23
|
+
*
|
|
24
|
+
* `/status` is polled by the browser picker to render a live session badge (the
|
|
25
|
+
* launcher dot + the panel status line). It has no token: any loopback-origin
|
|
26
|
+
* page may read it, but non-loopback browser origins are rejected so an arbitrary
|
|
27
|
+
* website cannot probe session metadata.
|
|
28
|
+
*
|
|
20
29
|
* Two delivery modes:
|
|
21
30
|
* - default (submit falsy): pre-fill the current pi input editor for review.
|
|
22
31
|
* - submit: true: deliver a real user turn immediately (queued as a follow-up
|
|
@@ -169,6 +178,58 @@ function compose(payload: PickPayload, imagePaths: string[]): string {
|
|
|
169
178
|
return lines.join('\n');
|
|
170
179
|
}
|
|
171
180
|
|
|
181
|
+
interface SessionStatus {
|
|
182
|
+
ok: true;
|
|
183
|
+
/** Coarse state for the picker UI. */
|
|
184
|
+
status: 'running' | 'queued' | 'idle';
|
|
185
|
+
/** Whether the agent loop is idle (not streaming). */
|
|
186
|
+
idle: boolean;
|
|
187
|
+
/** Whether follow-up messages are queued behind the current run. */
|
|
188
|
+
pending: boolean;
|
|
189
|
+
mode: string | null;
|
|
190
|
+
sessionId: string | null;
|
|
191
|
+
model: string | null;
|
|
192
|
+
provider: string | null;
|
|
193
|
+
pid: number;
|
|
194
|
+
cwd: string;
|
|
195
|
+
updatedAt: number;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Snapshot the active session's live state for GET /status.
|
|
200
|
+
*
|
|
201
|
+
* Derivation is ordered so "busy" always wins: a session that is mid-run reads
|
|
202
|
+
* `running` even while follow-ups are queued; only an idle session with queued
|
|
203
|
+
* messages reports `queued`; everything else is `idle`.
|
|
204
|
+
*/
|
|
205
|
+
function sessionStatus(ctx: unknown): SessionStatus {
|
|
206
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
207
|
+
const c = ctx as any;
|
|
208
|
+
let idle = true;
|
|
209
|
+
let pending = false;
|
|
210
|
+
try {
|
|
211
|
+
if (c && typeof c.isIdle === 'function') idle = !!c.isIdle();
|
|
212
|
+
if (c && typeof c.hasPendingMessages === 'function') pending = !!c.hasPendingMessages();
|
|
213
|
+
} catch {
|
|
214
|
+
/* fall back to the idle/no-queue defaults */
|
|
215
|
+
}
|
|
216
|
+
const model = c && c.model ? c.model.name || c.model.id || null : null;
|
|
217
|
+
const provider = c && c.model ? c.model.provider ?? null : null;
|
|
218
|
+
return {
|
|
219
|
+
ok: true,
|
|
220
|
+
status: !idle ? 'running' : pending ? 'queued' : 'idle',
|
|
221
|
+
idle,
|
|
222
|
+
pending,
|
|
223
|
+
mode: c ? c.mode ?? null : null,
|
|
224
|
+
sessionId: c ? c.sessionId ?? null : null,
|
|
225
|
+
model,
|
|
226
|
+
provider,
|
|
227
|
+
pid: process.pid,
|
|
228
|
+
cwd: process.cwd(),
|
|
229
|
+
updatedAt: Date.now(),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
172
233
|
export default function (pi: ExtensionAPI): void {
|
|
173
234
|
const token = randomBytes(16).toString('hex');
|
|
174
235
|
let server: Server | undefined;
|
|
@@ -249,6 +310,19 @@ export default function (pi: ExtensionAPI): void {
|
|
|
249
310
|
return;
|
|
250
311
|
}
|
|
251
312
|
|
|
313
|
+
if (req.method === 'GET' && url === '/status') {
|
|
314
|
+
// Read-only and tokenless, so gate it on a loopback browser origin: reject a
|
|
315
|
+
// foreign page while still allowing no-Origin callers (curl, local scripts).
|
|
316
|
+
if (origin && !isLoopbackOrigin(origin)) {
|
|
317
|
+
res.writeHead(403, { 'content-type': 'application/json' });
|
|
318
|
+
res.end(JSON.stringify({ ok: false, error: 'forbidden origin' }));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
322
|
+
res.end(JSON.stringify(sessionStatus(activeCtx)));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
252
326
|
if (req.method !== 'POST' || url !== '/pick') {
|
|
253
327
|
res.writeHead(404, { 'content-type': 'application/json' });
|
|
254
328
|
res.end(JSON.stringify({ ok: false, error: 'not found' }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-ele-picker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|