webhelm 1.0.7 → 1.0.8

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 CHANGED
@@ -35,7 +35,7 @@ The daemon-managed Chrome Extension is installed from the npm Runtime and does n
35
35
 
36
36
  The popup has a confirmation step to stop one Session, clear Sessions in one workspace, or clear Sessions across all workspaces. These actions are executed by the same Node daemon as the CLI, over the existing CDP-backed Extension RPC. No shell access or HTTP listener is granted to the Extension. The persistent ChatGPT queue is not deleted, and queued work may create new Sessions afterward.
37
37
 
38
- New installations use the default Chrome tab group `WebHelm` and the idle page `helm.html`. Legacy `Harness` default groups migrate only when their tabs are idle. Custom workspace names are unchanged; the old `workspace.html` path and lease titles remain readable until their Sessions finish. Closing an allocated Session returns its managed tabs to the idle page, retaining physical Chrome targets, while unleased popup children are closed. Do not manually remove active managed tabs.
38
+ New installations use the default Chrome tab group `WebHelm` and the idle page `helm.html`. Idle URLs use only `workspace` and `role=idle`; Lease URLs use Chrome's actual `tabId` for initial CDP target discovery. There is no separate slot identity. Legacy `Harness` default groups migrate only when their tabs are idle. Custom workspace names are unchanged; the old `workspace.html` path and lease titles remain readable until their Sessions finish. Closing an allocated Session returns its managed tabs to the idle page, retaining physical Chrome targets, while unleased popup children are closed. Do not manually remove active managed tabs.
39
39
 
40
40
  For a real browser integration check on macOS, run `node test/e2e/extension-control.e2e.mjs`; it creates a temporary isolated Chrome profile and daemon and does not touch the current production Sessions.
41
41
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webhelm",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "Agent browser automation in a visible managed Chrome with persistent JavaScript sessions",
5
5
  "type": "module",
6
6
  "bin": {
@@ -72,7 +72,7 @@
72
72
  .description { margin: 16px 0 24px; color: var(--subtle); }
73
73
  dl {
74
74
  display: grid;
75
- grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
75
+ grid-template-columns: minmax(0, 1fr);
76
76
  gap: 16px;
77
77
  margin: 0;
78
78
  padding: 18px 0;
@@ -117,7 +117,6 @@
117
117
  <p class="description" id="description">This page belongs to WebHelm's managed browser workspace.</p>
118
118
  <dl>
119
119
  <div><dt>Workspace</dt><dd id="workspace-name">—</dd></div>
120
- <div><dt>Tab slot</dt><dd id="slot-number">—</dd></div>
121
120
  </dl>
122
121
  <footer id="help-text">Tab status does not indicate whether the daemon is connected.</footer>
123
122
  </main>
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "WebHelm",
4
- "version": "0.4.0",
4
+ "version": "0.4.1",
5
5
  "description": "Own named Chrome tab-group workspaces and lazy reusable tab pools for browser agents.",
6
6
  "permissions": [
7
7
  "storage",
@@ -18,8 +18,8 @@ export function normalizePoolSize(value, fallback = 8) {
18
18
  return number;
19
19
  }
20
20
 
21
- function queryString(workspace, slot) {
22
- return new URLSearchParams({ workspace, role: "idle", slot: String(slot) }).toString();
21
+ function queryString(workspace) {
22
+ return new URLSearchParams({ workspace, role: "idle" }).toString();
23
23
  }
24
24
 
25
25
  export class WorkspaceManager {
@@ -41,8 +41,8 @@ export class WorkspaceManager {
41
41
  if (Number.isInteger(id)) this.ownedTabIds.add(id);
42
42
  }
43
43
 
44
- idleUrl(workspace, slot) {
45
- return this.chrome.runtime.getURL(`helm.html?${queryString(workspace, slot)}`);
44
+ idleUrl(workspace) {
45
+ return this.chrome.runtime.getURL(`helm.html?${queryString(workspace)}`);
46
46
  }
47
47
 
48
48
  identityTitle(tabId) {
@@ -53,7 +53,7 @@ export class WorkspaceManager {
53
53
  const query = new URLSearchParams({
54
54
  workspace,
55
55
  role: "lease",
56
- slot: String(tabId),
56
+ tabId: String(tabId),
57
57
  }).toString();
58
58
  return this.chrome.runtime.getURL(`helm.html?${query}`);
59
59
  }
@@ -67,7 +67,7 @@ export class WorkspaceManager {
67
67
  return {
68
68
  workspace: parsed.searchParams.get("workspace") || "",
69
69
  role: parsed.searchParams.get("role") || "",
70
- slot: parsed.searchParams.get("slot") || "",
70
+ tabId: parsed.searchParams.get("tabId") || "",
71
71
  };
72
72
  } catch {
73
73
  return null;
@@ -371,7 +371,7 @@ export class WorkspaceManager {
371
371
  // Chrome groups are created by grouping a tab. Keep one seed tab so the
372
372
  // workspace has an identity, then grow the physical pool on demand.
373
373
  const created = [await this.chrome.tabs.create({
374
- url: this.idleUrl(name, 0),
374
+ url: this.idleUrl(name),
375
375
  active: false,
376
376
  })];
377
377
  for (const tab of created) this.markOwned(tab.id);
@@ -489,7 +489,7 @@ export class WorkspaceManager {
489
489
  for (const tab of tabs) this.markOwned(tab.id);
490
490
  if (group) await this.chrome.tabGroups.update(group.id, {title: newName});
491
491
  for (const tab of tabs) {
492
- await this.chrome.tabs.update(tab.id, {url: this.idleUrl(newName, tab.id), active: false});
492
+ await this.chrome.tabs.update(tab.id, {url: this.idleUrl(newName), active: false});
493
493
  updated.push(tab);
494
494
  }
495
495
  config.workspaces[newName] = config.workspaces[oldName];
@@ -538,7 +538,7 @@ export class WorkspaceManager {
538
538
  let tab = idle[0];
539
539
  if (!tab) {
540
540
  tab = await this.chrome.tabs.create({
541
- url: this.idleUrl(normalizedName, tabs.length),
541
+ url: this.idleUrl(normalizedName),
542
542
  active: false,
543
543
  });
544
544
  this.markOwned(tab.id);
@@ -557,7 +557,7 @@ export class WorkspaceManager {
557
557
  tabId: updated.id,
558
558
  identityTitle,
559
559
  identityUrl,
560
- idleUrl: this.idleUrl(normalizedName, updated.id),
560
+ idleUrl: this.idleUrl(normalizedName),
561
561
  active: Boolean(updated.active),
562
562
  };
563
563
  }
@@ -579,7 +579,7 @@ export class WorkspaceManager {
579
579
  let tab = idle[0];
580
580
  if (!tab) {
581
581
  tab = await this.chrome.tabs.create({
582
- url: this.idleUrl(normalizedName, tabs.length),
582
+ url: this.idleUrl(normalizedName),
583
583
  active: false,
584
584
  });
585
585
  // Group reconciliation can fire before the extension idle URL is visible.
@@ -614,7 +614,7 @@ export class WorkspaceManager {
614
614
  } else {
615
615
  try {
616
616
  await this.chrome.tabs.update(wanted, {
617
- url: this.idleUrl(normalizedName, wanted),
617
+ url: this.idleUrl(normalizedName),
618
618
  active: false,
619
619
  });
620
620
  await this.waitForIdleTab(wanted, normalizedName);
@@ -623,7 +623,7 @@ export class WorkspaceManager {
623
623
  // the extension idle page. Preserve pool capacity by creating a clean
624
624
  // idle replacement in the same group before removing the stale tab.
625
625
  const replacement = await this.chrome.tabs.create({
626
- url: this.idleUrl(normalizedName, wanted),
626
+ url: this.idleUrl(normalizedName),
627
627
  active: false,
628
628
  });
629
629
  this.markOwned(replacement.id);
@@ -1,12 +1,13 @@
1
1
  const params = new URLSearchParams(location.search);
2
2
  const workspace = params.get("workspace") || "";
3
3
  const role = params.get("role") || "";
4
- const slot = params.get("slot") || "";
5
- const validSlot = /^\d+$/.test(slot);
4
+ // A lease carries the actual Chrome tabId for one-time CDP target discovery.
5
+ const tabId = params.get("tabId") || "";
6
+ const validTabId = /^\d+$/.test(tabId);
6
7
 
7
8
  // Keep this exact lease title: WebHelm's tab identity protocol depends on it.
8
- document.title = role === "lease" && validSlot
9
- ? `${location.pathname.endsWith("/workspace.html") ? "__BW_TAB_" : "__WEBHELM_TAB_"}${slot}__`
9
+ document.title = role === "lease" && validTabId
10
+ ? `${location.pathname.endsWith("/workspace.html") ? "__BW_TAB_" : "__WEBHELM_TAB_"}${tabId}__`
10
11
  : role === "idle" && workspace
11
12
  ? "WebHelm · Idle"
12
13
  : "WebHelm";
@@ -17,13 +18,12 @@ const setText = (id, value) => {
17
18
  };
18
19
 
19
20
  setText("workspace-name", workspace || "Not assigned");
20
- setText("slot-number", validSlot ? slot : "—");
21
21
 
22
22
  if (role === "idle" && workspace) {
23
23
  setText("status-label", "Idle · Awaiting next task");
24
24
  setText("description", "This tab is in the WebHelm pool and will be reused automatically when a task needs it.");
25
25
  setText("help-text", "No action required. Leave this tab open. This status describes the tab, not daemon connectivity.");
26
- } else if (role === "lease" && validSlot && workspace) {
26
+ } else if (role === "lease" && validTabId && workspace) {
27
27
  setText("status-label", "Reserved for a task");
28
28
  setText("description", "WebHelm has assigned this tab to a browser session.");
29
29
  }
@@ -65,7 +65,7 @@ async function acquireIdentityLease(session) {
65
65
  // Do not navigate/release a tab still owned by another Session.
66
66
  throw Error('Workspace extension returned a target already owned by another Session');
67
67
  }
68
- lease.idleUrl ||= idleUrlFromIdentity(lease.identityUrl, lease.tabId);
68
+ lease.idleUrl ||= idleUrlFromIdentity(lease.identityUrl);
69
69
  return {lease,targetId};
70
70
  });
71
71
  }
@@ -6,13 +6,16 @@ export function staleSession(session, now = Date.now(), ttl = SESSION_IDLE_MS) {
6
6
  now - session.last_used_at >= ttl;
7
7
  }
8
8
 
9
- export function idleUrlFromIdentity(identityUrl, tabId) {
9
+ export function idleUrlFromIdentity(identityUrl) {
10
10
  const url = new URL(identityUrl);
11
- if (url.protocol !== 'chrome-extension:' || !['/workspace.html','/helm.html'].includes(url.pathname)) {
11
+ if (url.protocol !== 'chrome-extension:' || !['/workspace.html', '/helm.html'].includes(url.pathname)) {
12
12
  throw new Error('invalid extension identity URL');
13
13
  }
14
- url.searchParams.set('role', 'idle');
15
- url.searchParams.set('slot', String(tabId));
14
+ const workspace = url.searchParams.get('workspace');
15
+ if (!workspace) throw new Error('missing workspace in extension identity URL');
16
+ // Construct the canonical idle URL from scratch. Never copy lease identity
17
+ // or unrelated query parameters into an idle tab.
18
+ url.search = new URLSearchParams({workspace, role:'idle'}).toString();
16
19
  return url.href;
17
20
  }
18
21