haltija 1.3.1 → 1.3.3

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.3.1",
3
+ "version": "1.3.3",
4
4
  "private": true,
5
5
  "description": "Haltija Desktop - God Mode Browser for AI Agents",
6
6
  "homepage": "https://github.com/tonioloewald/haltija",
@@ -46,7 +46,7 @@
46
46
  });
47
47
 
48
48
  // src/version.ts
49
- var VERSION = "1.3.1";
49
+ var VERSION = "1.3.3";
50
50
 
51
51
  // src/text-selector.ts
52
52
  var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\(/;
@@ -1761,6 +1761,7 @@
1761
1761
  displayStream = null;
1762
1762
  displayVideo = null;
1763
1763
  isActive = true;
1764
+ visibilityHandler = null;
1764
1765
  homeLeft = 0;
1765
1766
  homeBottom = 16;
1766
1767
  headless = false;
@@ -1952,6 +1953,16 @@
1952
1953
  }
1953
1954
  this.interceptConsole();
1954
1955
  this.interceptDialogs();
1956
+ if (typeof document !== "undefined") {
1957
+ this.visibilityHandler = () => {
1958
+ if (document.visibilityState === "hidden") {
1959
+ this.deactivate();
1960
+ } else {
1961
+ this.activate();
1962
+ }
1963
+ };
1964
+ document.addEventListener("visibilitychange", this.visibilityHandler);
1965
+ }
1955
1966
  this.connect();
1956
1967
  }
1957
1968
  disconnectedCallback() {
@@ -1962,6 +1973,10 @@
1962
1973
  this.clearEventWatchers();
1963
1974
  this.stopMutationWatch();
1964
1975
  this.stopScreenCapture();
1976
+ if (this.visibilityHandler && typeof document !== "undefined") {
1977
+ document.removeEventListener("visibilitychange", this.visibilityHandler);
1978
+ this.visibilityHandler = null;
1979
+ }
1965
1980
  }
1966
1981
  attributeChangedCallback(name, _old, value) {
1967
1982
  if (name === "server") {
@@ -5100,6 +5115,7 @@ ${elementSummary}${moreText}`;
5100
5115
  this.state = "connected";
5101
5116
  this.show();
5102
5117
  const windowType = window.opener ? "popup" : window.parent !== window ? "iframe" : "tab";
5118
+ this.isActive = typeof document === "undefined" || document.visibilityState !== "hidden";
5103
5119
  this.send("system", "connected", {
5104
5120
  windowId: this.windowId,
5105
5121
  browserId: this.browserId,
@@ -584,7 +584,7 @@ async function launchElectronApp() {
584
584
  return false
585
585
  }
586
586
 
587
- async function ensureBrowserConnected(port) {
587
+ async function ensureBrowserConnected(port, { explicitTarget = false } = {}) {
588
588
  let status
589
589
  try {
590
590
  const resp = await fetch(`http://localhost:${port}/status`, {
@@ -600,6 +600,19 @@ async function ensureBrowserConnected(port) {
600
600
  // produce two app instances side by side — skip the launch.
601
601
  if (status?.desktopApp) return true
602
602
 
603
+ // Private / project-owned server (explicitly targeted via --port / --name /
604
+ // HALTIJA_PORT). Launching the standalone Haltija.app here is wrong — it runs
605
+ // its own server on 8700 and would never connect to this port — so guide the
606
+ // user to attach a browser to *this* server instead of spawning Electron.
607
+ if (explicitTarget) {
608
+ process.stderr.write(
609
+ `\x1b[2mNo browser connected to the haltija server on port ${port}. ` +
610
+ `Open your app/page with the widget injected (script tag or bookmarklet), ` +
611
+ `or run \`hj --no-launch\` to skip this check.\x1b[0m\n`
612
+ )
613
+ return false
614
+ }
615
+
603
616
  // Respect "user explicitly quit" — don't auto-relaunch on every agent
604
617
  // call. Cleared when the user starts Haltija manually.
605
618
  try {
@@ -748,6 +761,7 @@ export async function runSubcommand(subcommand, subArgs, port = '8700', options
748
761
  const baseUrl = `http://localhost:${port}`
749
762
  const jsonOutput = subArgs.includes('--json')
750
763
  const noLaunch = options.noLaunch || false
764
+ const explicitTarget = options.explicitTarget || false
751
765
  // Remove --json and extract --window before processing
752
766
  let filteredArgs = subArgs.filter(a => a !== '--json')
753
767
  let targetWindowId = undefined
@@ -791,7 +805,7 @@ export async function runSubcommand(subcommand, subArgs, port = '8700', options
791
805
 
792
806
  // Auto-launch browser if no windows connected (skip for info commands and --no-launch)
793
807
  if (!noLaunch && !INFO_COMMANDS.has(subcommand)) {
794
- await ensureBrowserConnected(port)
808
+ await ensureBrowserConnected(port, { explicitTarget })
795
809
  }
796
810
 
797
811
  // Special handling for 'send' command - route to appropriate endpoint
package/bin/hj.mjs CHANGED
@@ -196,6 +196,15 @@ if (noLaunchIdx !== -1) {
196
196
  args.splice(noLaunchIdx, 1)
197
197
  }
198
198
 
199
+ // Did the shell explicitly target a private instance (--port / --name /
200
+ // HALTIJA_PORT / HALTIJA_NAME / DEV_CHANNEL_PORT)? If so, this is a
201
+ // project-owned server with a bring-your-own browser — auto-launching the
202
+ // standalone Haltija.app is never right (it runs its own server on 8700 and
203
+ // can't connect to this port), so we suppress the Electron launch and print
204
+ // an actionable hint instead. Only the bare, unconfigured 8700 default keeps
205
+ // the zero-config desktop auto-launch.
206
+ const explicitTarget = portSource !== '8700 (default)'
207
+
199
208
  // --- Space-to-hyphen sub-command resolution ---
200
209
  // "hj test run foo.json" → "hj test-run foo.json"
201
210
  // "hj events watch" → "hj events-watch"
@@ -247,7 +256,7 @@ if (!isSubcommand(subcommand)) {
247
256
 
248
257
  // Auto-execute if there's exactly one fuzzy match
249
258
  if (suggestion) {
250
- runSubcommand(suggestion, subArgs, port, { noLaunch })
259
+ runSubcommand(suggestion, subArgs, port, { noLaunch, explicitTarget })
251
260
  } else {
252
261
  console.error(`Unknown command: '${subcommand}'`)
253
262
  console.error(`\nExamples: hj tree, hj navigate <url>, hj click @42`)
@@ -255,7 +264,7 @@ if (!isSubcommand(subcommand)) {
255
264
  process.exit(1)
256
265
  }
257
266
  } else {
258
- runSubcommand(subcommand, subArgs, port, { noLaunch })
267
+ runSubcommand(subcommand, subArgs, port, { noLaunch, explicitTarget })
259
268
  }
260
269
 
261
270
  function filterHelp(topic) {
@@ -63,6 +63,8 @@ export declare const tree: EndpointDef<{
63
63
  pierceFrames?: boolean | undefined;
64
64
  compact?: boolean | undefined;
65
65
  ancestors?: boolean | undefined;
66
+ includeBox?: boolean | undefined;
67
+ mode?: string | undefined;
66
68
  window?: string | undefined;
67
69
  }>;
68
70
  export declare const query: EndpointDef<{
@@ -354,6 +356,8 @@ export declare const endpoints: {
354
356
  pierceFrames?: boolean | undefined;
355
357
  compact?: boolean | undefined;
356
358
  ancestors?: boolean | undefined;
359
+ includeBox?: boolean | undefined;
360
+ mode?: string | undefined;
357
361
  window?: string | undefined;
358
362
  }>;
359
363
  readonly query: EndpointDef<{
@@ -645,6 +649,8 @@ export declare const ALL_ENDPOINTS: (EndpointDef<{
645
649
  pierceFrames?: boolean | undefined;
646
650
  compact?: boolean | undefined;
647
651
  ancestors?: boolean | undefined;
652
+ includeBox?: boolean | undefined;
653
+ mode?: string | undefined;
648
654
  window?: string | undefined;
649
655
  }> | EndpointDef<{
650
656
  selector?: string | undefined;
@@ -20,7 +20,7 @@
20
20
  * - Option+Tab toggles visibility (but active state always shows briefly)
21
21
  * - Localhost only by default
22
22
  */
23
- export declare const VERSION = "1.3.1";
23
+ export declare const VERSION = "1.3.3";
24
24
  export declare class DevChannel extends HTMLElement {
25
25
  static get tagName(): string;
26
26
  static elementCreator(): () => DevChannel;
@@ -50,6 +50,7 @@ export declare class DevChannel extends HTMLElement {
50
50
  private displayStream;
51
51
  private displayVideo;
52
52
  private isActive;
53
+ private visibilityHandler;
53
54
  private homeLeft;
54
55
  private homeBottom;
55
56
  private headless;
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var VERSION = "1.3.1";
2
+ var VERSION = "1.3.3";
3
3
 
4
4
  // src/text-selector.ts
5
5
  var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\(/;
@@ -1714,6 +1714,7 @@ class DevChannel extends HTMLElement {
1714
1714
  displayStream = null;
1715
1715
  displayVideo = null;
1716
1716
  isActive = true;
1717
+ visibilityHandler = null;
1717
1718
  homeLeft = 0;
1718
1719
  homeBottom = 16;
1719
1720
  headless = false;
@@ -1905,6 +1906,16 @@ class DevChannel extends HTMLElement {
1905
1906
  }
1906
1907
  this.interceptConsole();
1907
1908
  this.interceptDialogs();
1909
+ if (typeof document !== "undefined") {
1910
+ this.visibilityHandler = () => {
1911
+ if (document.visibilityState === "hidden") {
1912
+ this.deactivate();
1913
+ } else {
1914
+ this.activate();
1915
+ }
1916
+ };
1917
+ document.addEventListener("visibilitychange", this.visibilityHandler);
1918
+ }
1908
1919
  this.connect();
1909
1920
  }
1910
1921
  disconnectedCallback() {
@@ -1915,6 +1926,10 @@ class DevChannel extends HTMLElement {
1915
1926
  this.clearEventWatchers();
1916
1927
  this.stopMutationWatch();
1917
1928
  this.stopScreenCapture();
1929
+ if (this.visibilityHandler && typeof document !== "undefined") {
1930
+ document.removeEventListener("visibilitychange", this.visibilityHandler);
1931
+ this.visibilityHandler = null;
1932
+ }
1918
1933
  }
1919
1934
  attributeChangedCallback(name, _old, value) {
1920
1935
  if (name === "server") {
@@ -5053,6 +5068,7 @@ ${elementSummary}${moreText}`;
5053
5068
  this.state = "connected";
5054
5069
  this.show();
5055
5070
  const windowType = window.opener ? "popup" : window.parent !== window ? "iframe" : "tab";
5071
+ this.isActive = typeof document === "undefined" || document.visibilityState !== "hidden";
5056
5072
  this.send("system", "connected", {
5057
5073
  windowId: this.windowId,
5058
5074
  browserId: this.browserId,
package/dist/component.js CHANGED
@@ -46,7 +46,7 @@
46
46
  });
47
47
 
48
48
  // src/version.ts
49
- var VERSION = "1.3.1";
49
+ var VERSION = "1.3.3";
50
50
 
51
51
  // src/text-selector.ts
52
52
  var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\(/;
@@ -1761,6 +1761,7 @@
1761
1761
  displayStream = null;
1762
1762
  displayVideo = null;
1763
1763
  isActive = true;
1764
+ visibilityHandler = null;
1764
1765
  homeLeft = 0;
1765
1766
  homeBottom = 16;
1766
1767
  headless = false;
@@ -1952,6 +1953,16 @@
1952
1953
  }
1953
1954
  this.interceptConsole();
1954
1955
  this.interceptDialogs();
1956
+ if (typeof document !== "undefined") {
1957
+ this.visibilityHandler = () => {
1958
+ if (document.visibilityState === "hidden") {
1959
+ this.deactivate();
1960
+ } else {
1961
+ this.activate();
1962
+ }
1963
+ };
1964
+ document.addEventListener("visibilitychange", this.visibilityHandler);
1965
+ }
1955
1966
  this.connect();
1956
1967
  }
1957
1968
  disconnectedCallback() {
@@ -1962,6 +1973,10 @@
1962
1973
  this.clearEventWatchers();
1963
1974
  this.stopMutationWatch();
1964
1975
  this.stopScreenCapture();
1976
+ if (this.visibilityHandler && typeof document !== "undefined") {
1977
+ document.removeEventListener("visibilitychange", this.visibilityHandler);
1978
+ this.visibilityHandler = null;
1979
+ }
1965
1980
  }
1966
1981
  attributeChangedCallback(name, _old, value) {
1967
1982
  if (name === "server") {
@@ -5100,6 +5115,7 @@ ${elementSummary}${moreText}`;
5100
5115
  this.state = "connected";
5101
5116
  this.show();
5102
5117
  const windowType = window.opener ? "popup" : window.parent !== window ? "iframe" : "tab";
5118
+ this.isActive = typeof document === "undefined" || document.visibilityState !== "hidden";
5103
5119
  this.send("system", "connected", {
5104
5120
  windowId: this.windowId,
5105
5121
  browserId: this.browserId,
package/dist/hj.js CHANGED
@@ -1345,7 +1345,7 @@ async function launchElectronApp() {
1345
1345
  }
1346
1346
  return false;
1347
1347
  }
1348
- async function ensureBrowserConnected(port) {
1348
+ async function ensureBrowserConnected(port, { explicitTarget = false } = {}) {
1349
1349
  let status;
1350
1350
  try {
1351
1351
  const resp = await fetch(`http://localhost:${port}/status`, {
@@ -1359,6 +1359,11 @@ async function ensureBrowserConnected(port) {
1359
1359
  }
1360
1360
  if (status?.desktopApp)
1361
1361
  return true;
1362
+ if (explicitTarget) {
1363
+ process.stderr.write(`\x1B[2mNo browser connected to the haltija server on port ${port}. Open your app/page with the widget injected (script tag or bookmarklet), or run \`hj --no-launch\` to skip this check.\x1B[0m
1364
+ `);
1365
+ return false;
1366
+ }
1362
1367
  try {
1363
1368
  const quitMarker = join(homedir(), ".haltija", "last-quit");
1364
1369
  if (existsSync(quitMarker)) {
@@ -1483,6 +1488,7 @@ async function runSubcommand(subcommand, subArgs, port = "8700", options = {}) {
1483
1488
  const baseUrl = `http://localhost:${port}`;
1484
1489
  const jsonOutput = subArgs.includes("--json");
1485
1490
  const noLaunch = options.noLaunch || false;
1491
+ const explicitTarget = options.explicitTarget || false;
1486
1492
  let filteredArgs = subArgs.filter((a) => a !== "--json");
1487
1493
  let targetWindowId = undefined;
1488
1494
  const windowIdx = filteredArgs.indexOf("--window");
@@ -1516,7 +1522,7 @@ async function runSubcommand(subcommand, subArgs, port = "8700", options = {}) {
1516
1522
  }
1517
1523
  }
1518
1524
  if (!noLaunch && !INFO_COMMANDS.has(subcommand)) {
1519
- await ensureBrowserConnected(port);
1525
+ await ensureBrowserConnected(port, { explicitTarget });
1520
1526
  }
1521
1527
  if (subcommand === "send") {
1522
1528
  const firstArg = filteredArgs[0]?.toLocaleLowerCase();
@@ -2018,6 +2024,7 @@ if (noLaunchIdx !== -1) {
2018
2024
  noLaunch = true;
2019
2025
  args.splice(noLaunchIdx, 1);
2020
2026
  }
2027
+ var explicitTarget = portSource !== "8700 (default)";
2021
2028
  if (args.length >= 2 && isSubcommand(`${args[0]}-${args[1]}`)) {
2022
2029
  args.splice(0, 2, `${args[0]}-${args[1]}`);
2023
2030
  }
@@ -2052,7 +2059,7 @@ if (!isSubcommand(subcommand)) {
2052
2059
  process.exit(0);
2053
2060
  }
2054
2061
  if (suggestion) {
2055
- runSubcommand(suggestion, subArgs, port, { noLaunch });
2062
+ runSubcommand(suggestion, subArgs, port, { noLaunch, explicitTarget });
2056
2063
  } else {
2057
2064
  console.error(`Unknown command: '${subcommand}'`);
2058
2065
  console.error(`
@@ -2061,7 +2068,7 @@ Examples: hj tree, hj navigate <url>, hj click @42`);
2061
2068
  process.exit(1);
2062
2069
  }
2063
2070
  } else {
2064
- runSubcommand(subcommand, subArgs, port, { noLaunch });
2071
+ runSubcommand(subcommand, subArgs, port, { noLaunch, explicitTarget });
2065
2072
  }
2066
2073
  function filterHelp(topic) {
2067
2074
  const bold2 = (s) => `\x1B[1m${s}\x1B[0m`;
package/dist/index.js CHANGED
@@ -674,7 +674,7 @@ var injectorCode = `
674
674
  `;
675
675
 
676
676
  // src/version.ts
677
- var VERSION = "1.3.1";
677
+ var VERSION = "1.3.3";
678
678
 
679
679
  // src/embedded-assets.ts
680
680
  var APP_MD = `# Haltija App
@@ -1086,6 +1086,8 @@ Use ancestors:true to see parent elements when inspecting deep elements.
1086
1086
  | \`pierceFrames\` | boolean,null | Pierce same-origin iframes (default true) |
1087
1087
  | \`compact\` | boolean,null | Minimal output (default false) |
1088
1088
  | \`ancestors\` | boolean,null | Include ancestor path from root (default false) |
1089
+ | \`includeBox\` | boolean,null | Include each node's bounding box { x, y, w, h, visible } (default false) |
1090
+ | \`mode\` | string,null | 'actionable' returns a page action summary (url, title, headings, buttons, links, inputs, selects) instead of the DOM tree |
1089
1091
  | \`window\` | string,null | Target window ID |
1090
1092
 
1091
1093
  **Examples:**
@@ -3019,7 +3021,7 @@ var COMPONENT_JS = `(() => {
3019
3021
  });
3020
3022
 
3021
3023
  // src/version.ts
3022
- var VERSION = "1.3.1";
3024
+ var VERSION = "1.3.3";
3023
3025
 
3024
3026
  // src/text-selector.ts
3025
3027
  var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\\(/;
@@ -4734,6 +4736,7 @@ var COMPONENT_JS = `(() => {
4734
4736
  displayStream = null;
4735
4737
  displayVideo = null;
4736
4738
  isActive = true;
4739
+ visibilityHandler = null;
4737
4740
  homeLeft = 0;
4738
4741
  homeBottom = 16;
4739
4742
  headless = false;
@@ -4925,6 +4928,16 @@ var COMPONENT_JS = `(() => {
4925
4928
  }
4926
4929
  this.interceptConsole();
4927
4930
  this.interceptDialogs();
4931
+ if (typeof document !== "undefined") {
4932
+ this.visibilityHandler = () => {
4933
+ if (document.visibilityState === "hidden") {
4934
+ this.deactivate();
4935
+ } else {
4936
+ this.activate();
4937
+ }
4938
+ };
4939
+ document.addEventListener("visibilitychange", this.visibilityHandler);
4940
+ }
4928
4941
  this.connect();
4929
4942
  }
4930
4943
  disconnectedCallback() {
@@ -4935,6 +4948,10 @@ var COMPONENT_JS = `(() => {
4935
4948
  this.clearEventWatchers();
4936
4949
  this.stopMutationWatch();
4937
4950
  this.stopScreenCapture();
4951
+ if (this.visibilityHandler && typeof document !== "undefined") {
4952
+ document.removeEventListener("visibilitychange", this.visibilityHandler);
4953
+ this.visibilityHandler = null;
4954
+ }
4938
4955
  }
4939
4956
  attributeChangedCallback(name, _old, value) {
4940
4957
  if (name === "server") {
@@ -8073,6 +8090,7 @@ var COMPONENT_JS = `(() => {
8073
8090
  this.state = "connected";
8074
8091
  this.show();
8075
8092
  const windowType = window.opener ? "popup" : window.parent !== window ? "iframe" : "tab";
8093
+ this.isActive = typeof document === "undefined" || document.visibilityState !== "hidden";
8076
8094
  this.send("system", "connected", {
8077
8095
  windowId: this.windowId,
8078
8096
  browserId: this.browserId,
@@ -11373,6 +11391,8 @@ Use ancestors:true to see parent elements when inspecting deep elements.`,
11373
11391
  pierceFrames: L.boolean.describe("Pierce same-origin iframes (default true)").optional,
11374
11392
  compact: L.boolean.describe("Minimal output (default false)").optional,
11375
11393
  ancestors: L.boolean.describe("Include ancestor path from root (default false)").optional,
11394
+ includeBox: L.boolean.describe("Include each node's bounding box { x, y, w, h, visible } (default false)").optional,
11395
+ mode: L.string.describe("'actionable' returns a page action summary (url, title, headings, buttons, links, inputs, selects) instead of the DOM tree").optional,
11376
11396
  window: L.string.describe("Target window ID").optional
11377
11397
  }),
11378
11398
  examples: [
@@ -14063,7 +14083,10 @@ registerHandler(tree, async (body, ctx) => {
14063
14083
  pierceShadow: body.pierceShadow,
14064
14084
  pierceFrames: body.pierceFrames,
14065
14085
  visibleOnly: body.visibleOnly,
14066
- ancestors: body.ancestors
14086
+ interactiveOnly: body.interactiveOnly,
14087
+ ancestors: body.ancestors,
14088
+ includeBox: body.includeBox,
14089
+ mode: body.mode
14067
14090
  }, 5000, windowId);
14068
14091
  return Response.json(response, { headers: ctx.headers });
14069
14092
  });
@@ -18525,7 +18548,8 @@ var serverConfig = {
18525
18548
  label: existingWindow?.label,
18526
18549
  windowType: windowType || "tab"
18527
18550
  });
18528
- if (!focusedWindowId || focusedWindowId === windowId) {
18551
+ const isVisibleTab = (windowType || "tab") === "tab" && active !== false;
18552
+ if (!focusedWindowId || focusedWindowId === windowId || isVisibleTab) {
18529
18553
  focusedWindowId = windowId;
18530
18554
  }
18531
18555
  console.log(`${LOG_PREFIX} Window connected: ${windowId} (${url})`);
@@ -18569,6 +18593,9 @@ var serverConfig = {
18569
18593
  if (win) {
18570
18594
  win.active = active;
18571
18595
  win.lastSeen = Date.now();
18596
+ if (active && win.windowType === "tab") {
18597
+ focusedWindowId = windowId;
18598
+ }
18572
18599
  }
18573
18600
  }
18574
18601
  } catch {}
package/dist/server.js CHANGED
@@ -674,7 +674,7 @@ var injectorCode = `
674
674
  `;
675
675
 
676
676
  // src/version.ts
677
- var VERSION = "1.3.1";
677
+ var VERSION = "1.3.3";
678
678
 
679
679
  // src/embedded-assets.ts
680
680
  var APP_MD = `# Haltija App
@@ -1086,6 +1086,8 @@ Use ancestors:true to see parent elements when inspecting deep elements.
1086
1086
  | \`pierceFrames\` | boolean,null | Pierce same-origin iframes (default true) |
1087
1087
  | \`compact\` | boolean,null | Minimal output (default false) |
1088
1088
  | \`ancestors\` | boolean,null | Include ancestor path from root (default false) |
1089
+ | \`includeBox\` | boolean,null | Include each node's bounding box { x, y, w, h, visible } (default false) |
1090
+ | \`mode\` | string,null | 'actionable' returns a page action summary (url, title, headings, buttons, links, inputs, selects) instead of the DOM tree |
1089
1091
  | \`window\` | string,null | Target window ID |
1090
1092
 
1091
1093
  **Examples:**
@@ -3019,7 +3021,7 @@ var COMPONENT_JS = `(() => {
3019
3021
  });
3020
3022
 
3021
3023
  // src/version.ts
3022
- var VERSION = "1.3.1";
3024
+ var VERSION = "1.3.3";
3023
3025
 
3024
3026
  // src/text-selector.ts
3025
3027
  var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\\(/;
@@ -4734,6 +4736,7 @@ var COMPONENT_JS = `(() => {
4734
4736
  displayStream = null;
4735
4737
  displayVideo = null;
4736
4738
  isActive = true;
4739
+ visibilityHandler = null;
4737
4740
  homeLeft = 0;
4738
4741
  homeBottom = 16;
4739
4742
  headless = false;
@@ -4925,6 +4928,16 @@ var COMPONENT_JS = `(() => {
4925
4928
  }
4926
4929
  this.interceptConsole();
4927
4930
  this.interceptDialogs();
4931
+ if (typeof document !== "undefined") {
4932
+ this.visibilityHandler = () => {
4933
+ if (document.visibilityState === "hidden") {
4934
+ this.deactivate();
4935
+ } else {
4936
+ this.activate();
4937
+ }
4938
+ };
4939
+ document.addEventListener("visibilitychange", this.visibilityHandler);
4940
+ }
4928
4941
  this.connect();
4929
4942
  }
4930
4943
  disconnectedCallback() {
@@ -4935,6 +4948,10 @@ var COMPONENT_JS = `(() => {
4935
4948
  this.clearEventWatchers();
4936
4949
  this.stopMutationWatch();
4937
4950
  this.stopScreenCapture();
4951
+ if (this.visibilityHandler && typeof document !== "undefined") {
4952
+ document.removeEventListener("visibilitychange", this.visibilityHandler);
4953
+ this.visibilityHandler = null;
4954
+ }
4938
4955
  }
4939
4956
  attributeChangedCallback(name, _old, value) {
4940
4957
  if (name === "server") {
@@ -8073,6 +8090,7 @@ var COMPONENT_JS = `(() => {
8073
8090
  this.state = "connected";
8074
8091
  this.show();
8075
8092
  const windowType = window.opener ? "popup" : window.parent !== window ? "iframe" : "tab";
8093
+ this.isActive = typeof document === "undefined" || document.visibilityState !== "hidden";
8076
8094
  this.send("system", "connected", {
8077
8095
  windowId: this.windowId,
8078
8096
  browserId: this.browserId,
@@ -11373,6 +11391,8 @@ Use ancestors:true to see parent elements when inspecting deep elements.`,
11373
11391
  pierceFrames: L.boolean.describe("Pierce same-origin iframes (default true)").optional,
11374
11392
  compact: L.boolean.describe("Minimal output (default false)").optional,
11375
11393
  ancestors: L.boolean.describe("Include ancestor path from root (default false)").optional,
11394
+ includeBox: L.boolean.describe("Include each node's bounding box { x, y, w, h, visible } (default false)").optional,
11395
+ mode: L.string.describe("'actionable' returns a page action summary (url, title, headings, buttons, links, inputs, selects) instead of the DOM tree").optional,
11376
11396
  window: L.string.describe("Target window ID").optional
11377
11397
  }),
11378
11398
  examples: [
@@ -14063,7 +14083,10 @@ registerHandler(tree, async (body, ctx) => {
14063
14083
  pierceShadow: body.pierceShadow,
14064
14084
  pierceFrames: body.pierceFrames,
14065
14085
  visibleOnly: body.visibleOnly,
14066
- ancestors: body.ancestors
14086
+ interactiveOnly: body.interactiveOnly,
14087
+ ancestors: body.ancestors,
14088
+ includeBox: body.includeBox,
14089
+ mode: body.mode
14067
14090
  }, 5000, windowId);
14068
14091
  return Response.json(response, { headers: ctx.headers });
14069
14092
  });
@@ -18525,7 +18548,8 @@ var serverConfig = {
18525
18548
  label: existingWindow?.label,
18526
18549
  windowType: windowType || "tab"
18527
18550
  });
18528
- if (!focusedWindowId || focusedWindowId === windowId) {
18551
+ const isVisibleTab = (windowType || "tab") === "tab" && active !== false;
18552
+ if (!focusedWindowId || focusedWindowId === windowId || isVisibleTab) {
18529
18553
  focusedWindowId = windowId;
18530
18554
  }
18531
18555
  console.log(`${LOG_PREFIX} Window connected: ${windowId} (${url})`);
@@ -18569,6 +18593,9 @@ var serverConfig = {
18569
18593
  if (win) {
18570
18594
  win.active = active;
18571
18595
  win.lastSeen = Date.now();
18596
+ if (active && win.windowType === "tab") {
18597
+ focusedWindowId = windowId;
18598
+ }
18572
18599
  }
18573
18600
  }
18574
18601
  } catch {}
package/dist/version.d.ts CHANGED
@@ -8,4 +8,4 @@
8
8
  * ⚠️ AUTO-GENERATED FROM package.json - DO NOT EDIT THIS FILE
9
9
  * ⚠️ To change the version, update package.json and run: bun run build
10
10
  */
11
- export declare const VERSION = "1.3.1";
11
+ export declare const VERSION = "1.3.3";
package/docs/README.md CHANGED
@@ -34,7 +34,7 @@
34
34
  ## Planning
35
35
 
36
36
  - **[Executive Summary](EXECUTIVE-SUMMARY.md)** - What Haltija is, who it's for
37
- - **[Roadmap to 10/10](ROADMAP-TO-10.md)** - Where we're going
37
+ - **[Roadmap](ROADMAP.md)** - Where we're going
38
38
  - **[Development Roadmap](../ROADMAP.md)** - Completed and planned phases
39
39
  - **[TODO](../TODO.md)** - Outstanding issues and ideas
40
40
 
@@ -81,7 +81,7 @@ To fill this form:
81
81
  hj tree -d 5 Deeper tree
82
82
  hj tree form Subtree rooted at selector
83
83
  hj console Recent console logs/errors
84
- hj screenshot Capture page as image
84
+ hj screenshot Capture page as image (--format webp for smaller)
85
85
  hj location Current URL and title
86
86
  hj events Recent semantic events
87
87
 
@@ -113,6 +113,10 @@ To fill this form:
113
113
  hj windows List connected tabs
114
114
  hj click 5 --window abc Target specific tab
115
115
 
116
+ ### Server
117
+ hj status Is the server up? which tab is focused?
118
+ hj where Which server this shell targets (add --json)
119
+
116
120
  ## Tips
117
121
 
118
122
  - Start with `hj tree` — look for [interactive] to find actionable elements
package/docs/recipes.md CHANGED
@@ -128,8 +128,8 @@ curl localhost:8700/events
128
128
  # Highlight what might be wrong
129
129
  curl -X POST localhost:8700/highlight -d '{"selector":".error-message","label":"This error"}'
130
130
 
131
- # Screenshot for the ticket
132
- curl -X POST localhost:8700/screenshot -d '{"maxWidth":1200}'
131
+ # Screenshot for the ticket (webp keeps it small)
132
+ curl -X POST localhost:8700/screenshot -d '{"maxWidth":1200,"format":"webp"}'
133
133
  ```
134
134
 
135
135
  **Or just tell the agent**: "Customer says Settings page is broken. Check it out."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "haltija",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
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",