webhelm 1.0.19 → 1.0.21
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/package.json +1 -1
- package/runtime/chatgpt/chatgpt-queue.mjs +9 -2
- package/runtime/cli-help.mjs +0 -11
- package/runtime/cli.mjs +0 -9
- package/runtime/durable-queue-core.mjs +10 -1
- package/runtime/extension/control-channel.mjs +1 -1
- package/runtime/extension/control-rpc.mjs +38 -2
- package/runtime/extension/popup.html +8 -2
- package/runtime/extension/popup.mjs +22 -11
- package/runtime/extension/service-worker.mjs +2 -1
- package/runtime/node-daemon.mjs +1 -0
package/package.json
CHANGED
|
@@ -60,11 +60,18 @@ const chatgptQueuePolicy = {
|
|
|
60
60
|
db.prepare("UPDATE turns SET state='CHECKING',lease_owner=?,lease_until=?,attempts=attempts+1 WHERE id=?").run(owner,now+leaseSeconds,row.id);
|
|
61
61
|
return this.one(db, "SELECT * FROM turns WHERE id=?", row.id);
|
|
62
62
|
},
|
|
63
|
+
// A queue adapter registers a display description; Actions stay unaware of queues.
|
|
64
|
+
describe(row) {
|
|
65
|
+
const firstLine = row.message.split(/\r?\n/, 1)[0].replace(/\s+/g, ' ').trim();
|
|
66
|
+
const title = firstLine ? (firstLine.length > 52 ? `${firstLine.slice(0, 51)}…` : firstLine) : 'ChatGPT message';
|
|
67
|
+
const prefix = row.project_id ? `/g/${encodeURIComponent(row.project_id)}` : '';
|
|
68
|
+
return {title, url:`https://chatgpt.com${prefix}/c/${encodeURIComponent(row.thread_id)}`};
|
|
69
|
+
},
|
|
63
70
|
cancel(db, id) {
|
|
64
71
|
const row=this.one(db, "SELECT * FROM turns WHERE id=?", id);
|
|
65
72
|
if (!row) return {ok:false,queue_id:id,error:"queue item not found"};
|
|
66
|
-
if (row.state
|
|
67
|
-
db.prepare("UPDATE turns SET state='CANCELLED',lease_owner=NULL,lease_until=NULL WHERE id=? AND state
|
|
73
|
+
if (!["QUEUED","WAITING","BLOCKED"].includes(row.state)) return {ok:false,queue_id:id,state:row.state,error:"cannot remove a running or uncertain item"};
|
|
74
|
+
db.prepare("UPDATE turns SET state='CANCELLED',lease_owner=NULL,lease_until=NULL WHERE id=? AND state IN ('QUEUED','WAITING','BLOCKED')").run(id);
|
|
68
75
|
return {ok:true,queue_id:id,state:"CANCELLED"};
|
|
69
76
|
},
|
|
70
77
|
list(db) {
|
package/runtime/cli-help.mjs
CHANGED
|
@@ -14,8 +14,6 @@ Commands:
|
|
|
14
14
|
webhelm status Report browser and daemon status (JSON)
|
|
15
15
|
webhelm ensure Check and recover managed Chrome health
|
|
16
16
|
webhelm capacity Report available browser capacity (JSON)
|
|
17
|
-
webhelm queue list List active ChatGPT new-turn queue items
|
|
18
|
-
webhelm queue cancel QUEUE_ID Cancel an item that is still queued
|
|
19
17
|
webhelm session start [--url URL] Start a persistent browser Session
|
|
20
18
|
webhelm session list List CLI-owned Sessions
|
|
21
19
|
webhelm session exec ID [--code-file F] Run JavaScript in a Session
|
|
@@ -89,15 +87,6 @@ Output: JSON capacity information.
|
|
|
89
87
|
|
|
90
88
|
Example:
|
|
91
89
|
webhelm capacity`,
|
|
92
|
-
queue: `webhelm queue — inspect or cancel ChatGPT new-turn queue items
|
|
93
|
-
|
|
94
|
-
Usage:
|
|
95
|
-
webhelm queue list
|
|
96
|
-
webhelm queue cancel QUEUE_ID
|
|
97
|
-
|
|
98
|
-
Cancellation succeeds only while an item remains QUEUED. Items already being
|
|
99
|
-
checked, sent, or awaiting reconciliation cannot be cancelled.
|
|
100
|
-
Output: JSON queue state or cancellation receipt.`,
|
|
101
90
|
migrate: `webhelm migrate — migrate supported legacy local browser data
|
|
102
91
|
|
|
103
92
|
Usage:
|
package/runtime/cli.mjs
CHANGED
|
@@ -149,15 +149,6 @@ async function main(args = process.argv.slice(2)) {
|
|
|
149
149
|
// reload an older daemon's Extension while its Sessions are still active.
|
|
150
150
|
return emit(await request(payload));
|
|
151
151
|
}
|
|
152
|
-
if (group === 'queue') {
|
|
153
|
-
if (command === 'list') return emit(await request({op:'queue-list'}));
|
|
154
|
-
if (command === 'cancel') {
|
|
155
|
-
const queueId=args[2];
|
|
156
|
-
if (!queueId) throw Error('queue cancel requires QUEUE_ID');
|
|
157
|
-
return emit(await request({op:'queue-cancel',queue_id:queueId}));
|
|
158
|
-
}
|
|
159
|
-
throw Error('queue requires list or cancel QUEUE_ID');
|
|
160
|
-
}
|
|
161
152
|
if (group === 'session') {
|
|
162
153
|
if (command === 'start') return emit(await request({op:'start', workspace:CLI_WORKSPACE, url:arg(args,'--url')}));
|
|
163
154
|
if (command === 'list') {
|
|
@@ -42,7 +42,16 @@ export class DurableQueueCore {
|
|
|
42
42
|
cancel(id) {
|
|
43
43
|
return this._transaction(() => this.policy.cancel(this.db, id));
|
|
44
44
|
}
|
|
45
|
-
list() {
|
|
45
|
+
list() {
|
|
46
|
+
const result = this.policy.list(this.db);
|
|
47
|
+
if (typeof this.policy.describe !== 'function') return result;
|
|
48
|
+
const rows = new Map(this.rows().map(row => [row.id, row]));
|
|
49
|
+
return {...result, items: result.items.map(item => {
|
|
50
|
+
const row = rows.get(item.id);
|
|
51
|
+
const description = row && this.policy.describe(row);
|
|
52
|
+
return description ? {...item, title: description.title, url: description.url} : item;
|
|
53
|
+
})};
|
|
54
|
+
}
|
|
46
55
|
start() {
|
|
47
56
|
if (this.started) return this;
|
|
48
57
|
this.started = true;
|
|
@@ -85,7 +85,7 @@ export class ControlChannel {
|
|
|
85
85
|
if (state.request) throw new Error('Another WebHelm operation is already running');
|
|
86
86
|
if (!Number.isFinite(state.daemon?.seenAt) || this.now() - state.daemon.seenAt >= FRESH_MS)
|
|
87
87
|
throw new Error('WebHelm daemon is not responding');
|
|
88
|
-
if (!(state.queue || []).some(item => item.id === queueId && item.state
|
|
88
|
+
if (!(state.queue || []).some(item => item.id === queueId && ['QUEUED','WAITING','BLOCKED'].includes(item.state)))
|
|
89
89
|
throw new Error('Queue item is no longer cancellable');
|
|
90
90
|
const request = {id:this.makeId(), action:'queue-cancel', queueId, state:'pending', createdAt:this.now()};
|
|
91
91
|
await this.save({...state, request, lastResult:null});
|
|
@@ -1,10 +1,46 @@
|
|
|
1
|
-
|
|
1
|
+
function validTabId(tabId) { return Number.isSafeInteger(tabId) && tabId > 0; }
|
|
2
|
+
function validateQueueUrl(url) {
|
|
3
|
+
const parsed = new URL(url);
|
|
4
|
+
if (parsed.protocol !== 'https:' || !parsed.hostname) throw Error('Queue target must be an HTTPS URL');
|
|
5
|
+
return parsed.href;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function createControlRpc({control, manager, summarizeWorkspaces, scheduleBadge, chromeApi=globalThis.chrome ?? control.chrome}) {
|
|
2
9
|
return async function controlRpc(method, args = {}) {
|
|
3
10
|
switch (method) {
|
|
4
11
|
case "workspace.control.status": {
|
|
5
12
|
const state = await control.status();
|
|
6
13
|
const summary = summarizeWorkspaces((await manager.list()).workspaces, state.sessions);
|
|
7
|
-
|
|
14
|
+
const workspaces = await Promise.all(summary.workspaces.map(async workspace => ({
|
|
15
|
+
...workspace,
|
|
16
|
+
sessions: await Promise.all(workspace.sessions.map(async session => {
|
|
17
|
+
if (!validTabId(session.tabId)) return session;
|
|
18
|
+
try {
|
|
19
|
+
const tab = await chromeApi.tabs.get(session.tabId);
|
|
20
|
+
return {...session, tab_title: tab.title || 'Untitled tab'};
|
|
21
|
+
} catch { return {...session, tabId:null}; }
|
|
22
|
+
})),
|
|
23
|
+
})));
|
|
24
|
+
return {...state,...summary,workspaces};
|
|
25
|
+
}
|
|
26
|
+
case "workspace.control.activateTab": {
|
|
27
|
+
const state = await control.status();
|
|
28
|
+
if (!state.connected) throw Error('WebHelm daemon is not responding');
|
|
29
|
+
const session = state.sessions.find(s => s.session_id === args.sessionId);
|
|
30
|
+
if (!session || !validTabId(session.tabId)) throw Error('Session tab is no longer active');
|
|
31
|
+
const tab = await chromeApi.tabs.get(session.tabId);
|
|
32
|
+
await chromeApi.tabs.update(tab.id,{active:true});
|
|
33
|
+
await chromeApi.windows.update(tab.windowId,{focused:true});
|
|
34
|
+
return {ok:true};
|
|
35
|
+
}
|
|
36
|
+
case "workspace.control.openQueue": {
|
|
37
|
+
const state = await control.status();
|
|
38
|
+
if (!state.connected) throw Error('WebHelm daemon is not responding');
|
|
39
|
+
const item = state.queue.find(item => item.id === args.queueId);
|
|
40
|
+
if (!item || typeof item.url !== 'string') throw Error('Queue entry no longer available');
|
|
41
|
+
const url = validateQueueUrl(item.url);
|
|
42
|
+
await chromeApi.tabs.create({url, active:true});
|
|
43
|
+
return {ok:true};
|
|
8
44
|
}
|
|
9
45
|
case "workspace.control.requestStop":
|
|
10
46
|
return control.requestStop(args.action, args.sessionId, args.workspace);
|
|
@@ -37,6 +37,12 @@
|
|
|
37
37
|
.queue-info { min-width:0; flex:1; overflow-wrap:anywhere; }
|
|
38
38
|
.queue-info span { display:block; color:light-dark(var(--muted),#acb8c9); font-size:11px; }
|
|
39
39
|
.queue-row button { width:auto; padding:5px 9px; font-size:11px; }
|
|
40
|
+
button.tab-title, button.queue-title { flex:1; min-width:0; width:100%; padding:0; border:0; border-radius:2px; background:transparent; color:inherit; text-align:left; font-size:12px; font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; cursor:pointer; }
|
|
41
|
+
button.tab-title:hover, button.queue-title:hover { text-decoration:underline; }
|
|
42
|
+
button.tab-title:disabled, button.queue-title:disabled { cursor:default; text-decoration:none; }
|
|
43
|
+
.session-info, .queue-info { overflow:hidden; }
|
|
44
|
+
.queue-info .queue-title { margin:0; }
|
|
45
|
+
|
|
40
46
|
#message { margin-top:8px; font-size:11px; overflow-wrap:anywhere; }
|
|
41
47
|
#message:empty { display:none; }
|
|
42
48
|
[hidden] { display:none !important; }
|
|
@@ -48,8 +54,8 @@
|
|
|
48
54
|
<span id="daemon-status" class="daemon-state" role="status">Connecting…</span>
|
|
49
55
|
</header>
|
|
50
56
|
<div id="workspace-list" class="session-list" aria-label="Workspaces" aria-live="polite">Connecting…</div>
|
|
51
|
-
<strong>
|
|
52
|
-
<div id="queue-list" class="session-list" aria-label="
|
|
57
|
+
<strong>Queue</strong>
|
|
58
|
+
<div id="queue-list" class="session-list" aria-label="Queue" aria-live="polite">No queue entries</div>
|
|
53
59
|
<section id="confirmation" hidden aria-label="Confirm clear sessions">
|
|
54
60
|
<p id="confirm-text">Stop the selected WebHelm session(s)? Tasks in progress may be interrupted. Queued jobs remain and may create new sessions.</p>
|
|
55
61
|
<div class="buttons">
|
|
@@ -23,9 +23,10 @@ function showConfirmation(action, params = {}) {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
function actionButton(label, disabled, onClick) {
|
|
26
|
+
function actionButton(label, disabled, onClick, className = '') {
|
|
27
27
|
const button = document.createElement('button');
|
|
28
28
|
button.textContent = label;
|
|
29
|
+
if (className) button.className = className;
|
|
29
30
|
button.disabled = disabled;
|
|
30
31
|
button.addEventListener('click', onClick);
|
|
31
32
|
return button;
|
|
@@ -33,7 +34,7 @@ function actionButton(label, disabled, onClick) {
|
|
|
33
34
|
|
|
34
35
|
function renderWorkspaces(workspaces, busy, connected) {
|
|
35
36
|
const signature = JSON.stringify(workspaces.map(w => [
|
|
36
|
-
w.name,w.initialized,w.available,w.poolSize,w.idleTabs,w.leasedTabs,w.sessions.map(s => s.session_id),
|
|
37
|
+
w.name,w.initialized,w.available,w.poolSize,w.idleTabs,w.leasedTabs,w.sessions.map(s => [s.session_id,s.tab_title,s.tabId]),
|
|
37
38
|
])) + ':' + busy + ':' + connected;
|
|
38
39
|
if (signature === lastSignature) return;
|
|
39
40
|
lastSignature = signature;
|
|
@@ -63,9 +64,12 @@ function renderWorkspaces(workspaces, busy, connected) {
|
|
|
63
64
|
sessionRow.className = 'session-row';
|
|
64
65
|
const details = document.createElement('div');
|
|
65
66
|
details.className = 'session-info';
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
const title = actionButton(session.tab_title || 'Untitled tab', busy || !connected || !session.tabId, async () => {
|
|
68
|
+
try { await rpc('workspace.control.activateTab', {sessionId:session.session_id}); }
|
|
69
|
+
catch(error) { el('message').textContent=error.message; }
|
|
70
|
+
}, 'tab-title');
|
|
71
|
+
title.title = session.tab_title || 'Untitled tab';
|
|
72
|
+
details.append(title);
|
|
69
73
|
sessionRow.append(details, actionButton('Stop', busy || !connected,
|
|
70
74
|
() => showConfirmation('stop', {sessionId:session.session_id})));
|
|
71
75
|
row.append(sessionRow);
|
|
@@ -77,18 +81,25 @@ function renderWorkspaces(workspaces, busy, connected) {
|
|
|
77
81
|
function renderQueue(items, busy, connected) {
|
|
78
82
|
const target=el('queue-list');
|
|
79
83
|
target.replaceChildren();
|
|
80
|
-
if (!items.length) { target.textContent='No
|
|
84
|
+
if (!items.length) { target.textContent='No queue entries'; return; }
|
|
81
85
|
for (const item of items) {
|
|
82
86
|
const row=document.createElement('div'); row.className='queue-row';
|
|
83
87
|
const info=document.createElement('div'); info.className='queue-info';
|
|
84
|
-
const title=
|
|
88
|
+
const title=actionButton(item.title || 'ChatGPT message', !connected || !item.url, async()=>{
|
|
89
|
+
try { await rpc('workspace.control.openQueue',{queueId:item.id}); }
|
|
90
|
+
catch(error) { el('message').textContent=error.message; }
|
|
91
|
+
}, 'queue-title');
|
|
92
|
+
title.title=item.title || 'ChatGPT message';
|
|
85
93
|
const detail=document.createElement('span'); detail.textContent=`${item.state.toLowerCase()} · ${item.attempts} attempt${item.attempts===1?'':'s'}`;
|
|
86
94
|
info.append(title,detail); row.append(info);
|
|
87
|
-
|
|
95
|
+
const safe=['QUEUED','WAITING','BLOCKED'].includes(item.state);
|
|
96
|
+
const remove=actionButton('Remove',!safe || busy || !connected,async()=>{
|
|
88
97
|
try { const result=await rpc('workspace.control.queueCancel',{queueId:item.id}); if(!result.id) throw new Error('Missing queue request ID'); el('message').textContent='Waiting for WebHelm daemon…'; }
|
|
89
98
|
catch(error) { el('message').textContent=error.message; }
|
|
90
99
|
refresh();
|
|
91
|
-
})
|
|
100
|
+
});
|
|
101
|
+
if (!safe) remove.title='Cannot remove a running or unverified submission';
|
|
102
|
+
row.append(remove);
|
|
92
103
|
target.append(row);
|
|
93
104
|
}
|
|
94
105
|
}
|
|
@@ -105,10 +116,10 @@ async function refresh() {
|
|
|
105
116
|
renderQueue(state.queue || [], busy, state.connected);
|
|
106
117
|
el('confirm').disabled = !state.connected || busy;
|
|
107
118
|
if (busy) {
|
|
108
|
-
el('message').textContent = '
|
|
119
|
+
el('message').textContent = 'Applying WebHelm action…';
|
|
109
120
|
} else if (state.lastResult) {
|
|
110
121
|
el('message').textContent = state.lastResult.ok
|
|
111
|
-
? `Last action: stopped ${state.lastResult.stoppedCount} session(s).`
|
|
122
|
+
? (state.lastResult.queue_id ? 'Queue entry removed.' : `Last action: stopped ${state.lastResult.stoppedCount} session(s).`)
|
|
112
123
|
: `Last action failed: ${state.lastResult.error}`;
|
|
113
124
|
}
|
|
114
125
|
} catch (error) {
|
|
@@ -92,7 +92,8 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|
|
92
92
|
}
|
|
93
93
|
if (request?.method?.startsWith("workspace.control.") &&
|
|
94
94
|
(sender?.url !== chrome.runtime.getURL("popup.html") ||
|
|
95
|
-
!["workspace.control.requestStop", "workspace.control.queueCancel", "workspace.control.status"
|
|
95
|
+
!["workspace.control.requestStop", "workspace.control.queueCancel", "workspace.control.status",
|
|
96
|
+
"workspace.control.activateTab", "workspace.control.openQueue"].includes(request.method))) {
|
|
96
97
|
sendResponse({ok:false,error:{message:"WebHelm control actions require the toolbar popup"}});
|
|
97
98
|
return false;
|
|
98
99
|
}
|
package/runtime/node-daemon.mjs
CHANGED
|
@@ -615,6 +615,7 @@ let controlPollActive = false;
|
|
|
615
615
|
function visibleSessions() {
|
|
616
616
|
return [...sessions.values()].map(s => ({
|
|
617
617
|
session_id:s.session_id,workspace:s.workspace,last_used_at:s.last_used_at,
|
|
618
|
+
tabId:s.leasedTabs.get(s.targetId) ?? s.leasedTabs.get(s.rootTargetId) ?? null,
|
|
618
619
|
}));
|
|
619
620
|
}
|
|
620
621
|
async function pollExtensionControl() {
|