haltija 1.2.5 → 1.2.7

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "haltija-desktop",
3
- "version": "1.2.5",
3
+ "version": "1.2.7",
4
4
  "description": "Haltija Desktop - God Mode Browser for AI Agents",
5
5
  "homepage": "https://github.com/tonioloewald/haltija",
6
6
  "author": {
@@ -46,7 +46,7 @@
46
46
  });
47
47
 
48
48
  // src/version.ts
49
- var VERSION = "1.2.5";
49
+ var VERSION = "1.2.7";
50
50
 
51
51
  // src/text-selector.ts
52
52
  var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\(/;
@@ -1310,9 +1310,22 @@
1310
1310
  }
1311
1311
  return summary;
1312
1312
  }
1313
+ function pruneNonInteractive(node) {
1314
+ const isInteractive = node.flags?.interactive || node.flags?.hasEvents;
1315
+ const children = node.children?.map((c) => pruneNonInteractive(c)).filter((c) => c !== null);
1316
+ const shadowChildren = node.shadowChildren?.map((c) => pruneNonInteractive(c)).filter((c) => c !== null);
1317
+ const hasInteractiveDescendant = children && children.length > 0 || shadowChildren && shadowChildren.length > 0;
1318
+ if (!isInteractive && !hasInteractiveDescendant)
1319
+ return null;
1320
+ return {
1321
+ ...node,
1322
+ children: children?.length ? children : undefined,
1323
+ shadowChildren: shadowChildren?.length ? shadowChildren : undefined
1324
+ };
1325
+ }
1313
1326
  function buildDomTree(el, options, currentDepth = 0) {
1314
1327
  const {
1315
- depth = 5,
1328
+ depth = -1,
1316
1329
  includeText = true,
1317
1330
  allAttributes = false,
1318
1331
  includeStyles = false,
@@ -5529,7 +5542,10 @@ ${elementSummary}${moreText}`;
5529
5542
  const summary = buildActionableSummary(el);
5530
5543
  this.respond(msg2.id, true, summary);
5531
5544
  } else {
5532
- const tree = buildDomTree(el, request);
5545
+ let tree = buildDomTree(el, request);
5546
+ if (request.interactiveOnly && tree) {
5547
+ tree = pruneNonInteractive(tree);
5548
+ }
5533
5549
  if (request.ancestors && tree) {
5534
5550
  const ancestors = [];
5535
5551
  let parent = el.parentElement;
@@ -202,10 +202,10 @@ export function parseTreeArgs(args) {
202
202
  for (let i = 0; i < args.length; i++) {
203
203
  const a = args[i]
204
204
  if (a === '--depth' || a === '-d') { body.depth = num(args[++i]); continue }
205
- if (a === '--all' || a === '-a') { body.depth = -1; continue }
206
205
  if (a === '--selector' || a === '-s') { body.selector = args[++i]; continue }
207
206
  if (a === '--compact' || a === '-c') { body.compact = true; continue }
208
- if (a === '--visible') { body.visibleOnly = true; continue }
207
+ if (a === '--interactive' || a === '-i') { body.interactiveOnly = true; continue }
208
+ if (a === '--visible' || a === '-v') { body.visibleOnly = true; continue }
209
209
  if (a === '--text') { body.includeText = true; continue }
210
210
  if (a === '--no-text') { body.includeText = false; continue }
211
211
  if (a === '--shadow') { body.pierceShadow = true; continue }
@@ -832,7 +832,7 @@ export function listSubcommands() {
832
832
  return `
833
833
  Subcommands (replace curl with simple commands):
834
834
  ${bold('Inspect')}
835
- tree [selector] [-d N] [-a] DOM tree with ref IDs (default depth 5, -a = all)
835
+ tree [selector] [-d N] [-i] [-v] DOM tree (full depth, -i=interactive, -v=visible)
836
836
  query <selector> Find elements matching selector
837
837
  inspect <@ref|selector> Detailed element info
838
838
  inspectAll <selector> Deep inspect all matches
@@ -33,11 +33,11 @@ export function formatTree(node, indent = 0, { depth } = {}) {
33
33
  const lines = []
34
34
  formatNode(node, indent, lines)
35
35
 
36
- // Footer: depth and options hint
37
- const d = depth ?? 5
38
- const isDefault = depth === undefined || depth === null
36
+ // Footer: options hint
37
+ const d = depth ?? -1
38
+ const depthLabel = d === -1 ? 'unlimited' : String(d)
39
39
  lines.push('---')
40
- lines.push(`depth=${d}${isDefault ? ' (default)' : d === -1 ? ' (unlimited)' : ''} | -d N | --all | --json`)
40
+ lines.push(`depth=${depthLabel} | -d N | -i (interactive) | --visible | --json`)
41
41
 
42
42
  return lines.join('\n')
43
43
  }
package/bin/hints.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "tree": "-d 5 (deeper), --compact, \"#selector\" | see: inspect, query, click",
2
+ "tree": "-d 3 (shallow), -i (interactive only), --visible, --compact | see: inspect, query, click",
3
3
  "query": "@ref or \"selector\", --all | see: tree, inspect",
4
4
  "inspect": "@ref or \"selector\", --styles, --rules, --ancestors | see: tree, query",
5
5
  "click": "@ref or \"selector\", :text(Button), --diff | see: tree, wait, type",
package/dist/component.js CHANGED
@@ -46,7 +46,7 @@
46
46
  });
47
47
 
48
48
  // src/version.ts
49
- var VERSION = "1.2.5";
49
+ var VERSION = "1.2.7";
50
50
 
51
51
  // src/text-selector.ts
52
52
  var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\(/;
@@ -1310,9 +1310,22 @@
1310
1310
  }
1311
1311
  return summary;
1312
1312
  }
1313
+ function pruneNonInteractive(node) {
1314
+ const isInteractive = node.flags?.interactive || node.flags?.hasEvents;
1315
+ const children = node.children?.map((c) => pruneNonInteractive(c)).filter((c) => c !== null);
1316
+ const shadowChildren = node.shadowChildren?.map((c) => pruneNonInteractive(c)).filter((c) => c !== null);
1317
+ const hasInteractiveDescendant = children && children.length > 0 || shadowChildren && shadowChildren.length > 0;
1318
+ if (!isInteractive && !hasInteractiveDescendant)
1319
+ return null;
1320
+ return {
1321
+ ...node,
1322
+ children: children?.length ? children : undefined,
1323
+ shadowChildren: shadowChildren?.length ? shadowChildren : undefined
1324
+ };
1325
+ }
1313
1326
  function buildDomTree(el, options, currentDepth = 0) {
1314
1327
  const {
1315
- depth = 5,
1328
+ depth = -1,
1316
1329
  includeText = true,
1317
1330
  allAttributes = false,
1318
1331
  includeStyles = false,
@@ -5529,7 +5542,10 @@ ${elementSummary}${moreText}`;
5529
5542
  const summary = buildActionableSummary(el);
5530
5543
  this.respond(msg2.id, true, summary);
5531
5544
  } else {
5532
- const tree = buildDomTree(el, request);
5545
+ let tree = buildDomTree(el, request);
5546
+ if (request.interactiveOnly && tree) {
5547
+ tree = pruneNonInteractive(tree);
5548
+ }
5533
5549
  if (request.ancestors && tree) {
5534
5550
  const ancestors = [];
5535
5551
  let parent = el.parentElement;
package/dist/hj.js CHANGED
@@ -15,10 +15,10 @@ function formatTree(node, indent = 0, { depth } = {}) {
15
15
  return "";
16
16
  const lines = [];
17
17
  formatNode(node, indent, lines);
18
- const d = depth ?? 5;
19
- const isDefault = depth === undefined || depth === null;
18
+ const d = depth ?? -1;
19
+ const depthLabel = d === -1 ? "unlimited" : String(d);
20
20
  lines.push("---");
21
- lines.push(`depth=${d}${isDefault ? " (default)" : d === -1 ? " (unlimited)" : ""} | -d N | --all | --json`);
21
+ lines.push(`depth=${depthLabel} | -d N | -i (interactive) | --visible | --json`);
22
22
  return lines.join(`
23
23
  `);
24
24
  }
@@ -892,10 +892,6 @@ function parseTreeArgs(args) {
892
892
  body.depth = num(args[++i]);
893
893
  continue;
894
894
  }
895
- if (a === "--all" || a === "-a") {
896
- body.depth = -1;
897
- continue;
898
- }
899
895
  if (a === "--selector" || a === "-s") {
900
896
  body.selector = args[++i];
901
897
  continue;
@@ -904,7 +900,11 @@ function parseTreeArgs(args) {
904
900
  body.compact = true;
905
901
  continue;
906
902
  }
907
- if (a === "--visible") {
903
+ if (a === "--interactive" || a === "-i") {
904
+ body.interactiveOnly = true;
905
+ continue;
906
+ }
907
+ if (a === "--visible" || a === "-v") {
908
908
  body.visibleOnly = true;
909
909
  continue;
910
910
  }
@@ -1516,7 +1516,7 @@ function listSubcommands() {
1516
1516
  return `
1517
1517
  Subcommands (replace curl with simple commands):
1518
1518
  ${bold("Inspect")}
1519
- tree [selector] [-d N] [-a] DOM tree with ref IDs (default depth 5, -a = all)
1519
+ tree [selector] [-d N] [-i] [-v] DOM tree (full depth, -i=interactive, -v=visible)
1520
1520
  query <selector> Find elements matching selector
1521
1521
  inspect <@ref|selector> Detailed element info
1522
1522
  inspectAll <selector> Deep inspect all matches
package/dist/index.js CHANGED
@@ -674,7 +674,7 @@ var injectorCode = `
674
674
  `;
675
675
 
676
676
  // src/version.ts
677
- var VERSION = "1.2.5";
677
+ var VERSION = "1.2.7";
678
678
 
679
679
  // src/embedded-assets.ts
680
680
  var APP_MD = `# Haltija App
@@ -1078,9 +1078,10 @@ Use ancestors:true to see parent elements when inspecting deep elements.
1078
1078
  | Name | Type | Description |
1079
1079
  |------|------|-------------|
1080
1080
  | \`selector\` | string,null | Root element selector |
1081
- | \`depth\` | number,null | Max depth (-1 = unlimited, default 5) |
1081
+ | \`depth\` | number,null | Max depth (-1 = unlimited). Default: unlimited |
1082
1082
  | \`includeText\` | boolean,null | Include text content (default true) |
1083
1083
  | \`visibleOnly\` | boolean,null | Only visible elements (default false) |
1084
+ | \`interactiveOnly\` | boolean,null | Only interactive elements and their ancestors (default false) |
1084
1085
  | \`pierceShadow\` | boolean,null | Pierce shadow DOM (default true) |
1085
1086
  | \`pierceFrames\` | boolean,null | Pierce same-origin iframes (default true) |
1086
1087
  | \`compact\` | boolean,null | Minimal output (default false) |
@@ -1089,17 +1090,17 @@ Use ancestors:true to see parent elements when inspecting deep elements.
1089
1090
 
1090
1091
  **Examples:**
1091
1092
 
1092
- - **overview**: Quick page overview
1093
+ - **overview**: Quick page overview (shallow)
1093
1094
  \`\`\`json
1094
- {"depth":2}
1095
+ {"depth":3}
1095
1096
  \`\`\`
1096
1097
  - **form-only**: Full form structure
1097
1098
  \`\`\`json
1098
- {"selector":"form","depth":-1}
1099
+ {"selector":"form"}
1099
1100
  \`\`\`
1100
- - **visible-buttons**: Find visible interactive elements
1101
+ - **interactive**: Only buttons, inputs, links, and their containers
1101
1102
  \`\`\`json
1102
- {"selector":"body","visibleOnly":true,"depth":4}
1103
+ {"interactiveOnly":true}
1103
1104
  \`\`\`
1104
1105
  - **with-context**: See element with parent context
1105
1106
  \`\`\`json
@@ -3702,9 +3703,10 @@ Use ancestors:true to see parent elements when inspecting deep elements.`,
3702
3703
  category: "dom",
3703
3704
  input: L.object({
3704
3705
  selector: L.string.describe("Root element selector").optional,
3705
- depth: L.number.describe("Max depth (-1 = unlimited, default 5)").optional,
3706
+ depth: L.number.describe("Max depth (-1 = unlimited). Default: unlimited").optional,
3706
3707
  includeText: L.boolean.describe("Include text content (default true)").optional,
3707
3708
  visibleOnly: L.boolean.describe("Only visible elements (default false)").optional,
3709
+ interactiveOnly: L.boolean.describe("Only interactive elements and their ancestors (default false)").optional,
3708
3710
  pierceShadow: L.boolean.describe("Pierce shadow DOM (default true)").optional,
3709
3711
  pierceFrames: L.boolean.describe("Pierce same-origin iframes (default true)").optional,
3710
3712
  compact: L.boolean.describe("Minimal output (default false)").optional,
@@ -3714,18 +3716,18 @@ Use ancestors:true to see parent elements when inspecting deep elements.`,
3714
3716
  examples: [
3715
3717
  {
3716
3718
  name: "overview",
3717
- input: { depth: 2 },
3718
- description: "Quick page overview"
3719
+ input: { depth: 3 },
3720
+ description: "Quick page overview (shallow)"
3719
3721
  },
3720
3722
  {
3721
3723
  name: "form-only",
3722
- input: { selector: "form", depth: -1 },
3724
+ input: { selector: "form" },
3723
3725
  description: "Full form structure"
3724
3726
  },
3725
3727
  {
3726
- name: "visible-buttons",
3727
- input: { selector: "body", visibleOnly: true, depth: 4 },
3728
- description: "Find visible interactive elements"
3728
+ name: "interactive",
3729
+ input: { interactiveOnly: true },
3730
+ description: "Only buttons, inputs, links, and their containers"
3729
3731
  },
3730
3732
  {
3731
3733
  name: "with-context",
@@ -3733,7 +3735,7 @@ Use ancestors:true to see parent elements when inspecting deep elements.`,
3733
3735
  description: "See element with parent context"
3734
3736
  }
3735
3737
  ],
3736
- hints: '-d 5 (deeper), --compact, "#selector" | see: inspect, query, click'
3738
+ hints: "-d 3 (shallow), -i (interactive only), --visible, --compact | see: inspect, query, click"
3737
3739
  });
3738
3740
  var query = endpoint({
3739
3741
  path: "/query",
@@ -6326,34 +6328,12 @@ registerHandler(unhighlight, async (_body, ctx) => {
6326
6328
  registerHandler(navigate, async (body, ctx) => {
6327
6329
  const windowId = body.window || ctx.targetWindowId;
6328
6330
  const response = await ctx.requestFromBrowser("navigation", "goto", { url: body.url }, 5000, windowId);
6329
- if (!response.success) {
6330
- return Response.json(response, { headers: ctx.headers });
6331
- }
6332
- const reconnected = await ctx.waitForReconnect(windowId, 1e4);
6333
- if (!reconnected) {
6334
- return Response.json({
6335
- ...response,
6336
- success: false,
6337
- error: `Navigation sent but page did not reconnect within 10s. The page may have loaded without the Haltija widget.`
6338
- }, { headers: ctx.headers });
6339
- }
6340
6331
  return Response.json(response, { headers: ctx.headers });
6341
6332
  });
6342
6333
  registerHandler(refresh, async (body, ctx) => {
6343
6334
  const soft = body.soft ?? false;
6344
6335
  const windowId = body.window || ctx.targetWindowId;
6345
6336
  const response = await ctx.requestFromBrowser("navigation", "refresh", { soft }, 5000, windowId);
6346
- if (!response.success) {
6347
- return Response.json(response, { headers: ctx.headers });
6348
- }
6349
- const reconnected = await ctx.waitForReconnect(windowId, 8000);
6350
- if (!reconnected) {
6351
- return Response.json({
6352
- ...response,
6353
- success: false,
6354
- error: `Refresh sent but page did not reconnect within 8s. The page may have loaded without the Haltija widget.`
6355
- }, { headers: ctx.headers });
6356
- }
6357
6337
  return Response.json(response, { headers: ctx.headers });
6358
6338
  });
6359
6339
  registerHandler(tree, async (body, ctx) => {
@@ -6976,7 +6956,7 @@ async function wrapWithDeprecation(response, endpoint2) {
6976
6956
  }
6977
6957
  }
6978
6958
  var GET_DEFAULTS = {
6979
- "/tree": { selector: "body", depth: 5 },
6959
+ "/tree": { selector: "body", depth: -1 },
6980
6960
  "/screenshot": {},
6981
6961
  "/click": {},
6982
6962
  "/type": {},
@@ -8310,12 +8290,12 @@ var createHandlerContext = (req, url) => {
8310
8290
  return recordings2.get(id);
8311
8291
  };
8312
8292
  const sessionFilteredRequest = async (channel, action, payload, timeoutMs, windowId) => {
8313
- if (sessionId && browsers.size === 0) {
8293
+ if (sessionId && (browsers.size === 0 || windows2.size === 0)) {
8314
8294
  const waitStart = Date.now();
8315
8295
  while (Date.now() - waitStart < 5000) {
8316
- await new Promise((r) => setTimeout(r, 250));
8317
- if (browsers.size > 0)
8296
+ if (browsers.size > 0 && windows2.size > 0)
8318
8297
  break;
8298
+ await new Promise((r) => setTimeout(r, 250));
8319
8299
  }
8320
8300
  }
8321
8301
  const effectiveWindowId = windowId || targetWindowId;
@@ -8325,7 +8305,8 @@ var createHandlerContext = (req, url) => {
8325
8305
  return requestFromBrowser(channel, action, payload, timeoutMs, effectiveWindowId);
8326
8306
  }
8327
8307
  if (sessionId) {
8328
- const matching = Array.from(windows2.values()).filter((w) => w.session === sessionId).sort((a, b) => {
8308
+ const allWins = Array.from(windows2.values());
8309
+ const matching = allWins.filter((w) => w.session === sessionId).sort((a, b) => {
8329
8310
  if (a.id === focusedWindowId)
8330
8311
  return -1;
8331
8312
  if (b.id === focusedWindowId)
@@ -8336,8 +8317,22 @@ var createHandlerContext = (req, url) => {
8336
8317
  agentWindowAffinity.set(sessionId, matching[0].id);
8337
8318
  return requestFromBrowser(channel, action, payload, timeoutMs, matching[0].id);
8338
8319
  }
8320
+ const distinctSessions = new Set(allWins.map((w) => w.session).filter(Boolean));
8321
+ if (distinctSessions.size <= 1 && !isSecureMode) {
8322
+ const best = allWins.sort((a, b) => {
8323
+ if (a.id === focusedWindowId)
8324
+ return -1;
8325
+ if (b.id === focusedWindowId)
8326
+ return 1;
8327
+ return b.lastSeen - a.lastSeen;
8328
+ })[0];
8329
+ if (best) {
8330
+ agentWindowAffinity.set(sessionId, best.id);
8331
+ return requestFromBrowser(channel, action, payload, timeoutMs, best.id);
8332
+ }
8333
+ }
8339
8334
  if (windows2.size > 0) {
8340
- const anyWindow = Array.from(windows2.values())[0];
8335
+ const anyWindow = allWins[0];
8341
8336
  try {
8342
8337
  const openResult = await requestFromBrowser("tabs", "open", { url: undefined, session: sessionId }, 5000, anyWindow.id);
8343
8338
  if (openResult.success) {
@@ -8353,8 +8348,7 @@ var createHandlerContext = (req, url) => {
8353
8348
  }
8354
8349
  } catch {}
8355
8350
  }
8356
- const allSessions = new Set(Array.from(windows2.values()).map((w) => w.session).filter(Boolean));
8357
- const hint = allSessions.size > 0 ? `Available sessions: ${[...allSessions].map((s) => s.slice(0, 8) + "\u2026").join(", ")}. Check the widget UI for the full token.` : "No widgets have session tokens. Connect a browser tab first.";
8351
+ const hint = distinctSessions.size > 0 ? `Available sessions: ${[...distinctSessions].map((s) => s.slice(0, 8) + "\u2026").join(", ")}. Check the widget UI for the full token.` : "No widgets have session tokens. Connect a browser tab first.";
8358
8352
  return { id: "", success: false, error: `No windows in session ${sessionId.slice(0, 8)}\u2026. ${hint}`, timestamp: Date.now() };
8359
8353
  }
8360
8354
  if (isSecureMode) {
@@ -8362,26 +8356,6 @@ var createHandlerContext = (req, url) => {
8362
8356
  }
8363
8357
  return requestFromBrowser(channel, action, payload, timeoutMs);
8364
8358
  };
8365
- const waitForReconnect = async (windowId, timeoutMs = 8000) => {
8366
- const id = windowId || targetWindowId || focusedWindowId;
8367
- const prevBrowserId = id ? windows2.get(id)?.browserId : undefined;
8368
- const start = Date.now();
8369
- await new Promise((r) => setTimeout(r, 100));
8370
- while (Date.now() - start < timeoutMs) {
8371
- if (id) {
8372
- const w = windows2.get(id);
8373
- if (w && w.browserId !== prevBrowserId) {
8374
- await new Promise((r) => setTimeout(r, 300));
8375
- return true;
8376
- }
8377
- } else if (browsers.size > 0) {
8378
- await new Promise((r) => setTimeout(r, 300));
8379
- return true;
8380
- }
8381
- await new Promise((r) => setTimeout(r, 100));
8382
- }
8383
- return false;
8384
- };
8385
8359
  return {
8386
8360
  requestFromBrowser: sessionFilteredRequest,
8387
8361
  targetWindowId,
@@ -8390,7 +8364,6 @@ var createHandlerContext = (req, url) => {
8390
8364
  sessionId,
8391
8365
  getWindowInfo,
8392
8366
  updateSessionAffinity,
8393
- waitForReconnect,
8394
8367
  startRecordingSession,
8395
8368
  stopRecordingSession,
8396
8369
  getRecordingSession,
@@ -8623,7 +8596,10 @@ async function handleRest(req) {
8623
8596
  const reqSession = req.headers.get("X-Haltija-Session") || undefined;
8624
8597
  let allWindows = Array.from(windows2.values());
8625
8598
  if (reqSession) {
8626
- allWindows = allWindows.filter((w) => w.session === reqSession);
8599
+ const distinctSessions = new Set(allWindows.map((w) => w.session).filter(Boolean));
8600
+ if (distinctSessions.size > 1) {
8601
+ allWindows = allWindows.filter((w) => w.session === reqSession);
8602
+ }
8627
8603
  } else if (isSecureMode) {
8628
8604
  return Response.json({
8629
8605
  ok: windows2.size > 0,
@@ -10326,7 +10302,10 @@ Type 'help <topic>' for details.`);
10326
10302
  const reqSession = req.headers.get("X-Haltija-Session") || undefined;
10327
10303
  let allWindows = Array.from(windows2.values());
10328
10304
  if (reqSession) {
10329
- allWindows = allWindows.filter((w) => w.session === reqSession);
10305
+ const distinctSessions = new Set(allWindows.map((w) => w.session).filter(Boolean));
10306
+ if (distinctSessions.size > 1) {
10307
+ allWindows = allWindows.filter((w) => w.session === reqSession);
10308
+ }
10330
10309
  } else if (isSecureMode) {
10331
10310
  const totalWindows = windows2.size;
10332
10311
  return Response.json({
package/dist/server.js CHANGED
@@ -674,7 +674,7 @@ var injectorCode = `
674
674
  `;
675
675
 
676
676
  // src/version.ts
677
- var VERSION = "1.2.5";
677
+ var VERSION = "1.2.7";
678
678
 
679
679
  // src/embedded-assets.ts
680
680
  var APP_MD = `# Haltija App
@@ -1078,9 +1078,10 @@ Use ancestors:true to see parent elements when inspecting deep elements.
1078
1078
  | Name | Type | Description |
1079
1079
  |------|------|-------------|
1080
1080
  | \`selector\` | string,null | Root element selector |
1081
- | \`depth\` | number,null | Max depth (-1 = unlimited, default 5) |
1081
+ | \`depth\` | number,null | Max depth (-1 = unlimited). Default: unlimited |
1082
1082
  | \`includeText\` | boolean,null | Include text content (default true) |
1083
1083
  | \`visibleOnly\` | boolean,null | Only visible elements (default false) |
1084
+ | \`interactiveOnly\` | boolean,null | Only interactive elements and their ancestors (default false) |
1084
1085
  | \`pierceShadow\` | boolean,null | Pierce shadow DOM (default true) |
1085
1086
  | \`pierceFrames\` | boolean,null | Pierce same-origin iframes (default true) |
1086
1087
  | \`compact\` | boolean,null | Minimal output (default false) |
@@ -1089,17 +1090,17 @@ Use ancestors:true to see parent elements when inspecting deep elements.
1089
1090
 
1090
1091
  **Examples:**
1091
1092
 
1092
- - **overview**: Quick page overview
1093
+ - **overview**: Quick page overview (shallow)
1093
1094
  \`\`\`json
1094
- {"depth":2}
1095
+ {"depth":3}
1095
1096
  \`\`\`
1096
1097
  - **form-only**: Full form structure
1097
1098
  \`\`\`json
1098
- {"selector":"form","depth":-1}
1099
+ {"selector":"form"}
1099
1100
  \`\`\`
1100
- - **visible-buttons**: Find visible interactive elements
1101
+ - **interactive**: Only buttons, inputs, links, and their containers
1101
1102
  \`\`\`json
1102
- {"selector":"body","visibleOnly":true,"depth":4}
1103
+ {"interactiveOnly":true}
1103
1104
  \`\`\`
1104
1105
  - **with-context**: See element with parent context
1105
1106
  \`\`\`json
@@ -3702,9 +3703,10 @@ Use ancestors:true to see parent elements when inspecting deep elements.`,
3702
3703
  category: "dom",
3703
3704
  input: L.object({
3704
3705
  selector: L.string.describe("Root element selector").optional,
3705
- depth: L.number.describe("Max depth (-1 = unlimited, default 5)").optional,
3706
+ depth: L.number.describe("Max depth (-1 = unlimited). Default: unlimited").optional,
3706
3707
  includeText: L.boolean.describe("Include text content (default true)").optional,
3707
3708
  visibleOnly: L.boolean.describe("Only visible elements (default false)").optional,
3709
+ interactiveOnly: L.boolean.describe("Only interactive elements and their ancestors (default false)").optional,
3708
3710
  pierceShadow: L.boolean.describe("Pierce shadow DOM (default true)").optional,
3709
3711
  pierceFrames: L.boolean.describe("Pierce same-origin iframes (default true)").optional,
3710
3712
  compact: L.boolean.describe("Minimal output (default false)").optional,
@@ -3714,18 +3716,18 @@ Use ancestors:true to see parent elements when inspecting deep elements.`,
3714
3716
  examples: [
3715
3717
  {
3716
3718
  name: "overview",
3717
- input: { depth: 2 },
3718
- description: "Quick page overview"
3719
+ input: { depth: 3 },
3720
+ description: "Quick page overview (shallow)"
3719
3721
  },
3720
3722
  {
3721
3723
  name: "form-only",
3722
- input: { selector: "form", depth: -1 },
3724
+ input: { selector: "form" },
3723
3725
  description: "Full form structure"
3724
3726
  },
3725
3727
  {
3726
- name: "visible-buttons",
3727
- input: { selector: "body", visibleOnly: true, depth: 4 },
3728
- description: "Find visible interactive elements"
3728
+ name: "interactive",
3729
+ input: { interactiveOnly: true },
3730
+ description: "Only buttons, inputs, links, and their containers"
3729
3731
  },
3730
3732
  {
3731
3733
  name: "with-context",
@@ -3733,7 +3735,7 @@ Use ancestors:true to see parent elements when inspecting deep elements.`,
3733
3735
  description: "See element with parent context"
3734
3736
  }
3735
3737
  ],
3736
- hints: '-d 5 (deeper), --compact, "#selector" | see: inspect, query, click'
3738
+ hints: "-d 3 (shallow), -i (interactive only), --visible, --compact | see: inspect, query, click"
3737
3739
  });
3738
3740
  var query = endpoint({
3739
3741
  path: "/query",
@@ -6326,34 +6328,12 @@ registerHandler(unhighlight, async (_body, ctx) => {
6326
6328
  registerHandler(navigate, async (body, ctx) => {
6327
6329
  const windowId = body.window || ctx.targetWindowId;
6328
6330
  const response = await ctx.requestFromBrowser("navigation", "goto", { url: body.url }, 5000, windowId);
6329
- if (!response.success) {
6330
- return Response.json(response, { headers: ctx.headers });
6331
- }
6332
- const reconnected = await ctx.waitForReconnect(windowId, 1e4);
6333
- if (!reconnected) {
6334
- return Response.json({
6335
- ...response,
6336
- success: false,
6337
- error: `Navigation sent but page did not reconnect within 10s. The page may have loaded without the Haltija widget.`
6338
- }, { headers: ctx.headers });
6339
- }
6340
6331
  return Response.json(response, { headers: ctx.headers });
6341
6332
  });
6342
6333
  registerHandler(refresh, async (body, ctx) => {
6343
6334
  const soft = body.soft ?? false;
6344
6335
  const windowId = body.window || ctx.targetWindowId;
6345
6336
  const response = await ctx.requestFromBrowser("navigation", "refresh", { soft }, 5000, windowId);
6346
- if (!response.success) {
6347
- return Response.json(response, { headers: ctx.headers });
6348
- }
6349
- const reconnected = await ctx.waitForReconnect(windowId, 8000);
6350
- if (!reconnected) {
6351
- return Response.json({
6352
- ...response,
6353
- success: false,
6354
- error: `Refresh sent but page did not reconnect within 8s. The page may have loaded without the Haltija widget.`
6355
- }, { headers: ctx.headers });
6356
- }
6357
6337
  return Response.json(response, { headers: ctx.headers });
6358
6338
  });
6359
6339
  registerHandler(tree, async (body, ctx) => {
@@ -6976,7 +6956,7 @@ async function wrapWithDeprecation(response, endpoint2) {
6976
6956
  }
6977
6957
  }
6978
6958
  var GET_DEFAULTS = {
6979
- "/tree": { selector: "body", depth: 5 },
6959
+ "/tree": { selector: "body", depth: -1 },
6980
6960
  "/screenshot": {},
6981
6961
  "/click": {},
6982
6962
  "/type": {},
@@ -8310,12 +8290,12 @@ var createHandlerContext = (req, url) => {
8310
8290
  return recordings2.get(id);
8311
8291
  };
8312
8292
  const sessionFilteredRequest = async (channel, action, payload, timeoutMs, windowId) => {
8313
- if (sessionId && browsers.size === 0) {
8293
+ if (sessionId && (browsers.size === 0 || windows2.size === 0)) {
8314
8294
  const waitStart = Date.now();
8315
8295
  while (Date.now() - waitStart < 5000) {
8316
- await new Promise((r) => setTimeout(r, 250));
8317
- if (browsers.size > 0)
8296
+ if (browsers.size > 0 && windows2.size > 0)
8318
8297
  break;
8298
+ await new Promise((r) => setTimeout(r, 250));
8319
8299
  }
8320
8300
  }
8321
8301
  const effectiveWindowId = windowId || targetWindowId;
@@ -8325,7 +8305,8 @@ var createHandlerContext = (req, url) => {
8325
8305
  return requestFromBrowser(channel, action, payload, timeoutMs, effectiveWindowId);
8326
8306
  }
8327
8307
  if (sessionId) {
8328
- const matching = Array.from(windows2.values()).filter((w) => w.session === sessionId).sort((a, b) => {
8308
+ const allWins = Array.from(windows2.values());
8309
+ const matching = allWins.filter((w) => w.session === sessionId).sort((a, b) => {
8329
8310
  if (a.id === focusedWindowId)
8330
8311
  return -1;
8331
8312
  if (b.id === focusedWindowId)
@@ -8336,8 +8317,22 @@ var createHandlerContext = (req, url) => {
8336
8317
  agentWindowAffinity.set(sessionId, matching[0].id);
8337
8318
  return requestFromBrowser(channel, action, payload, timeoutMs, matching[0].id);
8338
8319
  }
8320
+ const distinctSessions = new Set(allWins.map((w) => w.session).filter(Boolean));
8321
+ if (distinctSessions.size <= 1 && !isSecureMode) {
8322
+ const best = allWins.sort((a, b) => {
8323
+ if (a.id === focusedWindowId)
8324
+ return -1;
8325
+ if (b.id === focusedWindowId)
8326
+ return 1;
8327
+ return b.lastSeen - a.lastSeen;
8328
+ })[0];
8329
+ if (best) {
8330
+ agentWindowAffinity.set(sessionId, best.id);
8331
+ return requestFromBrowser(channel, action, payload, timeoutMs, best.id);
8332
+ }
8333
+ }
8339
8334
  if (windows2.size > 0) {
8340
- const anyWindow = Array.from(windows2.values())[0];
8335
+ const anyWindow = allWins[0];
8341
8336
  try {
8342
8337
  const openResult = await requestFromBrowser("tabs", "open", { url: undefined, session: sessionId }, 5000, anyWindow.id);
8343
8338
  if (openResult.success) {
@@ -8353,8 +8348,7 @@ var createHandlerContext = (req, url) => {
8353
8348
  }
8354
8349
  } catch {}
8355
8350
  }
8356
- const allSessions = new Set(Array.from(windows2.values()).map((w) => w.session).filter(Boolean));
8357
- const hint = allSessions.size > 0 ? `Available sessions: ${[...allSessions].map((s) => s.slice(0, 8) + "\u2026").join(", ")}. Check the widget UI for the full token.` : "No widgets have session tokens. Connect a browser tab first.";
8351
+ const hint = distinctSessions.size > 0 ? `Available sessions: ${[...distinctSessions].map((s) => s.slice(0, 8) + "\u2026").join(", ")}. Check the widget UI for the full token.` : "No widgets have session tokens. Connect a browser tab first.";
8358
8352
  return { id: "", success: false, error: `No windows in session ${sessionId.slice(0, 8)}\u2026. ${hint}`, timestamp: Date.now() };
8359
8353
  }
8360
8354
  if (isSecureMode) {
@@ -8362,26 +8356,6 @@ var createHandlerContext = (req, url) => {
8362
8356
  }
8363
8357
  return requestFromBrowser(channel, action, payload, timeoutMs);
8364
8358
  };
8365
- const waitForReconnect = async (windowId, timeoutMs = 8000) => {
8366
- const id = windowId || targetWindowId || focusedWindowId;
8367
- const prevBrowserId = id ? windows2.get(id)?.browserId : undefined;
8368
- const start = Date.now();
8369
- await new Promise((r) => setTimeout(r, 100));
8370
- while (Date.now() - start < timeoutMs) {
8371
- if (id) {
8372
- const w = windows2.get(id);
8373
- if (w && w.browserId !== prevBrowserId) {
8374
- await new Promise((r) => setTimeout(r, 300));
8375
- return true;
8376
- }
8377
- } else if (browsers.size > 0) {
8378
- await new Promise((r) => setTimeout(r, 300));
8379
- return true;
8380
- }
8381
- await new Promise((r) => setTimeout(r, 100));
8382
- }
8383
- return false;
8384
- };
8385
8359
  return {
8386
8360
  requestFromBrowser: sessionFilteredRequest,
8387
8361
  targetWindowId,
@@ -8390,7 +8364,6 @@ var createHandlerContext = (req, url) => {
8390
8364
  sessionId,
8391
8365
  getWindowInfo,
8392
8366
  updateSessionAffinity,
8393
- waitForReconnect,
8394
8367
  startRecordingSession,
8395
8368
  stopRecordingSession,
8396
8369
  getRecordingSession,
@@ -8623,7 +8596,10 @@ async function handleRest(req) {
8623
8596
  const reqSession = req.headers.get("X-Haltija-Session") || undefined;
8624
8597
  let allWindows = Array.from(windows2.values());
8625
8598
  if (reqSession) {
8626
- allWindows = allWindows.filter((w) => w.session === reqSession);
8599
+ const distinctSessions = new Set(allWindows.map((w) => w.session).filter(Boolean));
8600
+ if (distinctSessions.size > 1) {
8601
+ allWindows = allWindows.filter((w) => w.session === reqSession);
8602
+ }
8627
8603
  } else if (isSecureMode) {
8628
8604
  return Response.json({
8629
8605
  ok: windows2.size > 0,
@@ -10326,7 +10302,10 @@ Type 'help <topic>' for details.`);
10326
10302
  const reqSession = req.headers.get("X-Haltija-Session") || undefined;
10327
10303
  let allWindows = Array.from(windows2.values());
10328
10304
  if (reqSession) {
10329
- allWindows = allWindows.filter((w) => w.session === reqSession);
10305
+ const distinctSessions = new Set(allWindows.map((w) => w.session).filter(Boolean));
10306
+ if (distinctSessions.size > 1) {
10307
+ allWindows = allWindows.filter((w) => w.session === reqSession);
10308
+ }
10330
10309
  } else if (isSecureMode) {
10331
10310
  const totalWindows = windows2.size;
10332
10311
  return Response.json({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "haltija",
3
- "version": "1.2.5",
3
+ "version": "1.2.7",
4
4
  "description": "Browser control for AI agents - query DOM, click, type, run JS, watch mutations",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",