haltija 1.5.1 → 1.5.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.2
4
+
5
+ Two follow-ups from the 1.5.0 review, both about the multi-tab experience on a shared server.
6
+
7
+ ### Fixed
8
+
9
+ - **`hj tabs focus <id>` no longer times out** ([#4](https://github.com/tonioloewald/haltija/issues/4)).
10
+ It was dispatching a `focus` command to the browser, routed to the *focused* tab rather than the
11
+ target, so nobody answered — and even routed correctly, a backgrounded tab can't raise itself.
12
+ Focus is now a **server-side** routing change: it validates the tab and points untargeted commands
13
+ at it, returning instantly (unknown tab → a clean error, never a timeout). It does not physically
14
+ raise the tab; to pin a single command use `--window <id>`. "Focus follows the visible tab" still
15
+ applies when you physically switch tabs — that's genuine intent that should win over a stale pin.
16
+
17
+ ### Changed
18
+
19
+ - **The hidden-tab / focus-ambiguity warnings are de-duplicated within a short (15s) cooldown**, so
20
+ a burst of commands from one agent doesn't repeat the same block every time. A *changed* condition
21
+ (different tab, newly-hidden tab, a new origin on the server) always re-warns; the cooldown
22
+ re-arms rather than suppressing forever. Set `HALTIJA_NO_TAB_WARN=1` to silence them entirely.
23
+
3
24
  ## 1.5.1
4
25
 
5
26
  Low-risk follow-ups from the 1.5.0 pre-release review — the two new "instrument must not lie"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "haltija-desktop",
3
- "version": "1.5.1",
3
+ "version": "1.5.2",
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.5.1";
49
+ var VERSION = "1.5.2";
50
50
 
51
51
  // src/text-selector.ts
52
52
  var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\(/;
@@ -88,6 +88,7 @@ Environment Variables:
88
88
  HALTIJA_NO_RETIRE Set to 1: do not stop pre-1.4.0 haltija servers
89
89
  HALTIJA_NO_INSTALL Set to 1: do not install hj into ~/.local/bin
90
90
  HALTIJA_NO_SKEW_WARN Set to 1: silence hj's version-skew warning
91
+ HALTIJA_NO_TAB_WARN Set to 1: silence hidden-tab / focus-ambiguity result warnings
91
92
  HALTIJA_REGISTRY_DIR Instance registry location (default: ~/.haltija/servers)
92
93
  DEV_CHANNEL_PORT Legacy alias for HALTIJA_PORT
93
94
  DEV_CHANNEL_HTTPS_PORT HTTPS port (default: 8701)
package/bin/version.mjs CHANGED
@@ -3,4 +3,4 @@
3
3
  * ⚠️ To change the version, update package.json and run: bun run build
4
4
  */
5
5
 
6
- export const HJ_VERSION = '1.5.1'
6
+ export const HJ_VERSION = '1.5.2'
@@ -52,6 +52,18 @@ export interface HandlerContext {
52
52
  headers: Record<string, string>;
53
53
  url: URL;
54
54
  getWindowInfo: (windowId?: string) => WindowInfo | undefined;
55
+ /**
56
+ * Set the server-side focused window — the tab that receives untargeted commands. This is pure
57
+ * server state, NOT a browser roundtrip: it validates the tab exists and returns immediately, so
58
+ * it can never time out the way dispatching a command *to* the (possibly hidden) tab does (#4).
59
+ */
60
+ focusWindow: (windowId: string) => {
61
+ ok: boolean;
62
+ error?: string;
63
+ active?: boolean;
64
+ windowType?: string;
65
+ title?: string;
66
+ };
55
67
  startRecordingSession: (windowId: string, url: string, name?: string) => void;
56
68
  stopRecordingSession: (windowId: string) => RecordingSessionInfo | undefined;
57
69
  getRecordingSession: (windowId: string) => RecordingSessionInfo | 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.5.1";
23
+ export declare const VERSION = "1.5.2";
24
24
  export declare class DevChannel extends HTMLElement {
25
25
  static get tagName(): string;
26
26
  static elementCreator(): () => DevChannel;
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var VERSION = "1.5.1";
2
+ var VERSION = "1.5.2";
3
3
 
4
4
  // src/text-selector.ts
5
5
  var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\(/;
package/dist/component.js CHANGED
@@ -46,7 +46,7 @@
46
46
  });
47
47
 
48
48
  // src/version.ts
49
- var VERSION = "1.5.1";
49
+ var VERSION = "1.5.2";
50
50
 
51
51
  // src/text-selector.ts
52
52
  var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\(/;
package/dist/hj.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bun
2
- // haltija-cli:do-not-edit v1.5.1
2
+ // haltija-cli:do-not-edit v1.5.2
3
3
  import { createRequire } from "node:module";
4
4
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
5
5
 
@@ -756,7 +756,7 @@ function substituteGeneratedVars(text, seed) {
756
756
  }
757
757
 
758
758
  // bin/version.mjs
759
- var HJ_VERSION = "1.5.1";
759
+ var HJ_VERSION = "1.5.2";
760
760
 
761
761
  // bin/semver.mjs
762
762
  function parseVersion(v) {
package/dist/index.js CHANGED
@@ -674,7 +674,7 @@ var injectorCode = `
674
674
  `;
675
675
 
676
676
  // src/version.ts
677
- var VERSION = "1.5.1";
677
+ var VERSION = "1.5.2";
678
678
 
679
679
  // src/embedded-assets.ts
680
680
  var APP_MD = `# Haltija App
@@ -2224,11 +2224,15 @@ Get window IDs from /windows endpoint.
2224
2224
 
2225
2225
  ### \`POST /tabs/focus\`
2226
2226
 
2227
- **Focus a tab**
2227
+ **Focus a tab (route untargeted commands to it)**
2228
2228
 
2229
- Desktop app only. Brings the specified tab to front.
2230
-
2231
- Useful when working with multiple tabs to ensure the right one is visible.
2229
+ Make the given tab the target of untargeted commands. This is a server-side
2230
+ routing change, NOT a browser action: it does not physically raise the tab (a backgrounded browser
2231
+ tab cannot be raised remotely), and because it never dispatches to the tab it can't time out \u2014 even
2232
+ if the tab is hidden. After this, commands without a \`window\`/\`--window\` go to this tab until you
2233
+ focus another or physically switch tabs in the browser. To pin a single command instead, use
2234
+ \`--window <id>\`. If the tab is hidden the response includes a warning (a backgrounded tab's results
2235
+ can be stale). Returns \`{ success, focused, active, title }\`.
2232
2236
 
2233
2237
  **Parameters:**
2234
2238
 
@@ -2238,7 +2242,7 @@ Useful when working with multiple tabs to ensure the right one is visible.
2238
2242
 
2239
2243
  **Examples:**
2240
2244
 
2241
- - **focus**: Bring tab to front
2245
+ - **focus**: Route untargeted commands to this tab
2242
2246
  \`\`\`json
2243
2247
  {"window":"window-abc123"}
2244
2248
  \`\`\`
@@ -2802,7 +2806,7 @@ hj --help # All commands
2802
2806
  - \`hj windows\` - List connected windows
2803
2807
  - \`hj tabs-open [url]\` - Open a new tab
2804
2808
  - \`hj tabs-close [window]\` - Close a tab
2805
- - \`hj tabs-focus [window]\` - Focus a tab
2809
+ - \`hj tabs-focus [window]\` - Focus a tab (route untargeted commands to it)
2806
2810
 
2807
2811
  ### Record & Replay
2808
2812
 
@@ -3097,7 +3101,7 @@ var COMPONENT_JS = `(() => {
3097
3101
  });
3098
3102
 
3099
3103
  // src/version.ts
3100
- var VERSION = "1.5.1";
3104
+ var VERSION = "1.5.2";
3101
3105
 
3102
3106
  // src/text-selector.ts
3103
3107
  var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\\(/;
@@ -12857,10 +12861,14 @@ Get window IDs from /windows endpoint.`,
12857
12861
  var tabsFocus = endpoint({
12858
12862
  path: "/tabs/focus",
12859
12863
  method: "POST",
12860
- summary: "Focus a tab",
12861
- description: `Desktop app only. Brings the specified tab to front.
12862
-
12863
- Useful when working with multiple tabs to ensure the right one is visible.`,
12864
+ summary: "Focus a tab (route untargeted commands to it)",
12865
+ description: `Make the given tab the target of untargeted commands. This is a server-side
12866
+ routing change, NOT a browser action: it does not physically raise the tab (a backgrounded browser
12867
+ tab cannot be raised remotely), and because it never dispatches to the tab it can't time out \u2014 even
12868
+ if the tab is hidden. After this, commands without a \`window\`/\`--window\` go to this tab until you
12869
+ focus another or physically switch tabs in the browser. To pin a single command instead, use
12870
+ \`--window <id>\`. If the tab is hidden the response includes a warning (a backgrounded tab's results
12871
+ can be stale). Returns \`{ success, focused, active, title }\`.`,
12864
12872
  category: "windows",
12865
12873
  input: L.object({
12866
12874
  window: L.string.describe("Window ID to focus")
@@ -12869,7 +12877,7 @@ Useful when working with multiple tabs to ensure the right one is visible.`,
12869
12877
  {
12870
12878
  name: "focus",
12871
12879
  input: { window: "window-abc123" },
12872
- description: "Bring tab to front"
12880
+ description: "Route untargeted commands to this tab"
12873
12881
  }
12874
12882
  ],
12875
12883
  hints: "<window-id> | see: windows, tabs-close, tabs-open"
@@ -14252,10 +14260,19 @@ registerHandler(tabsClose, async (body, ctx) => {
14252
14260
  registerHandler(tabsFocus, async (body, ctx) => {
14253
14261
  const windowId = body.window || ctx.targetWindowId;
14254
14262
  if (!windowId) {
14255
- return Response.json({ success: false, error: "window id is required" }, { status: 400, headers: ctx.headers });
14263
+ return Response.json({ success: false, error: "window id is required (run `hj tabs` to list connected tabs)" }, { status: 400, headers: ctx.headers });
14256
14264
  }
14257
- const response = await ctx.requestFromBrowser("tabs", "focus", { windowId });
14258
- return Response.json(response, { headers: ctx.headers });
14265
+ const r = ctx.focusWindow(windowId);
14266
+ if (!r.ok) {
14267
+ return Response.json({ success: false, error: r.error }, { status: 404, headers: ctx.headers });
14268
+ }
14269
+ const result = { success: true, focused: windowId, active: r.active !== false };
14270
+ if (r.title)
14271
+ result.title = r.title;
14272
+ if (r.active === false) {
14273
+ result.warning = "Untargeted commands now route to this tab, but it reports HIDDEN \u2014 browsers freeze " + "requestAnimationFrame and throttle timers in a backgrounded tab, so results can be stale " + '("not mounted yet", not "broken"). Bring it to the front in your browser to wake it.';
14274
+ }
14275
+ return Response.json(result, { headers: ctx.headers });
14259
14276
  });
14260
14277
  registerHandler(mutationsWatch, async (body, ctx) => {
14261
14278
  const response = await ctx.requestFromBrowser("mutations", "watch", {
@@ -15275,6 +15292,22 @@ function ambiguousFocusWarning(opts) {
15275
15292
  ${pins}${more}`;
15276
15293
  }
15277
15294
 
15295
+ // src/warning-dedupe.ts
15296
+ function shouldEmitWarning(warning, cache, now, cooldownMs) {
15297
+ const last = cache.get(warning);
15298
+ if (last !== undefined && now - last < cooldownMs) {
15299
+ return false;
15300
+ }
15301
+ cache.set(warning, now);
15302
+ if (cache.size > 64) {
15303
+ for (const [key2, ts] of cache) {
15304
+ if (now - ts >= cooldownMs)
15305
+ cache.delete(key2);
15306
+ }
15307
+ }
15308
+ return true;
15309
+ }
15310
+
15278
15311
  // src/server.ts
15279
15312
  init_terminal();
15280
15313
 
@@ -16129,6 +16162,9 @@ function getComponentJs() {
16129
16162
  }
16130
16163
  var browsers = new Map;
16131
16164
  var windows2 = new Map;
16165
+ var recentTabWarnings = new Map;
16166
+ var TAB_WARN_COOLDOWN_MS = 15000;
16167
+ var TAB_WARN_DISABLED = process.env.HALTIJA_NO_TAB_WARN === "1";
16132
16168
  var agents = new Set;
16133
16169
  var focusedWindowId = null;
16134
16170
  var isDesktopApp = process.env.HALTIJA_DESKTOP === "1";
@@ -16443,6 +16479,8 @@ async function requestFromBrowser(channel, action, payload, timeoutMs = 5000, wi
16443
16479
  return new Promise((resolve) => {
16444
16480
  let sentTo = null;
16445
16481
  const attachWarning = (res) => {
16482
+ if (TAB_WARN_DISABLED)
16483
+ return res;
16446
16484
  const hidden = hiddenTabWarning(sentTo);
16447
16485
  const ambiguous = ambiguousFocusWarning({
16448
16486
  windows: Array.from(windows2.values()),
@@ -16452,7 +16490,12 @@ async function requestFromBrowser(channel, action, payload, timeoutMs = 5000, wi
16452
16490
  const warning = [hidden, ambiguous].filter(Boolean).join(`
16453
16491
 
16454
16492
  `);
16455
- return warning ? { ...res, warning } : res;
16493
+ if (!warning)
16494
+ return res;
16495
+ if (!shouldEmitWarning(warning, recentTabWarnings, Date.now(), TAB_WARN_COOLDOWN_MS)) {
16496
+ return res;
16497
+ }
16498
+ return { ...res, warning };
16456
16499
  };
16457
16500
  const timeout = setTimeout(() => {
16458
16501
  pendingResponses.delete(id);
@@ -16623,6 +16666,15 @@ var createHandlerContext = (req, url) => {
16623
16666
  headers,
16624
16667
  url,
16625
16668
  getWindowInfo,
16669
+ focusWindow: (windowId) => {
16670
+ const win = windows2.get(windowId);
16671
+ if (!win) {
16672
+ return { ok: false, error: `Tab ${windowId} not found (run \`hj tabs\` to list connected tabs)` };
16673
+ }
16674
+ focusedWindowId = windowId;
16675
+ updateHjStatus();
16676
+ return { ok: true, active: win.active, windowType: win.windowType, title: win.title };
16677
+ },
16626
16678
  startRecordingSession,
16627
16679
  stopRecordingSession,
16628
16680
  getRecordingSession,
@@ -18300,12 +18352,10 @@ Type 'help <topic>' for details.`);
18300
18352
  break;
18301
18353
  }
18302
18354
  case "tabs-focus": {
18303
- const resp = await requestFromBrowser("tabs", "focus", { windowId: step.window }, stepTimeout);
18304
- if (!resp.success) {
18355
+ if (!step.window || !windows2.has(step.window)) {
18305
18356
  stepPassed = false;
18306
- error = resp.error || "Failed to focus tab";
18307
- }
18308
- if (step.window && windows2.has(step.window)) {
18357
+ error = `Tab ${step.window ?? "(none)"} not found`;
18358
+ } else {
18309
18359
  focusedWindowId = step.window;
18310
18360
  }
18311
18361
  break;
package/dist/server.js CHANGED
@@ -674,7 +674,7 @@ var injectorCode = `
674
674
  `;
675
675
 
676
676
  // src/version.ts
677
- var VERSION = "1.5.1";
677
+ var VERSION = "1.5.2";
678
678
 
679
679
  // src/embedded-assets.ts
680
680
  var APP_MD = `# Haltija App
@@ -2224,11 +2224,15 @@ Get window IDs from /windows endpoint.
2224
2224
 
2225
2225
  ### \`POST /tabs/focus\`
2226
2226
 
2227
- **Focus a tab**
2227
+ **Focus a tab (route untargeted commands to it)**
2228
2228
 
2229
- Desktop app only. Brings the specified tab to front.
2230
-
2231
- Useful when working with multiple tabs to ensure the right one is visible.
2229
+ Make the given tab the target of untargeted commands. This is a server-side
2230
+ routing change, NOT a browser action: it does not physically raise the tab (a backgrounded browser
2231
+ tab cannot be raised remotely), and because it never dispatches to the tab it can't time out \u2014 even
2232
+ if the tab is hidden. After this, commands without a \`window\`/\`--window\` go to this tab until you
2233
+ focus another or physically switch tabs in the browser. To pin a single command instead, use
2234
+ \`--window <id>\`. If the tab is hidden the response includes a warning (a backgrounded tab's results
2235
+ can be stale). Returns \`{ success, focused, active, title }\`.
2232
2236
 
2233
2237
  **Parameters:**
2234
2238
 
@@ -2238,7 +2242,7 @@ Useful when working with multiple tabs to ensure the right one is visible.
2238
2242
 
2239
2243
  **Examples:**
2240
2244
 
2241
- - **focus**: Bring tab to front
2245
+ - **focus**: Route untargeted commands to this tab
2242
2246
  \`\`\`json
2243
2247
  {"window":"window-abc123"}
2244
2248
  \`\`\`
@@ -2802,7 +2806,7 @@ hj --help # All commands
2802
2806
  - \`hj windows\` - List connected windows
2803
2807
  - \`hj tabs-open [url]\` - Open a new tab
2804
2808
  - \`hj tabs-close [window]\` - Close a tab
2805
- - \`hj tabs-focus [window]\` - Focus a tab
2809
+ - \`hj tabs-focus [window]\` - Focus a tab (route untargeted commands to it)
2806
2810
 
2807
2811
  ### Record & Replay
2808
2812
 
@@ -3097,7 +3101,7 @@ var COMPONENT_JS = `(() => {
3097
3101
  });
3098
3102
 
3099
3103
  // src/version.ts
3100
- var VERSION = "1.5.1";
3104
+ var VERSION = "1.5.2";
3101
3105
 
3102
3106
  // src/text-selector.ts
3103
3107
  var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\\(/;
@@ -12857,10 +12861,14 @@ Get window IDs from /windows endpoint.`,
12857
12861
  var tabsFocus = endpoint({
12858
12862
  path: "/tabs/focus",
12859
12863
  method: "POST",
12860
- summary: "Focus a tab",
12861
- description: `Desktop app only. Brings the specified tab to front.
12862
-
12863
- Useful when working with multiple tabs to ensure the right one is visible.`,
12864
+ summary: "Focus a tab (route untargeted commands to it)",
12865
+ description: `Make the given tab the target of untargeted commands. This is a server-side
12866
+ routing change, NOT a browser action: it does not physically raise the tab (a backgrounded browser
12867
+ tab cannot be raised remotely), and because it never dispatches to the tab it can't time out \u2014 even
12868
+ if the tab is hidden. After this, commands without a \`window\`/\`--window\` go to this tab until you
12869
+ focus another or physically switch tabs in the browser. To pin a single command instead, use
12870
+ \`--window <id>\`. If the tab is hidden the response includes a warning (a backgrounded tab's results
12871
+ can be stale). Returns \`{ success, focused, active, title }\`.`,
12864
12872
  category: "windows",
12865
12873
  input: L.object({
12866
12874
  window: L.string.describe("Window ID to focus")
@@ -12869,7 +12877,7 @@ Useful when working with multiple tabs to ensure the right one is visible.`,
12869
12877
  {
12870
12878
  name: "focus",
12871
12879
  input: { window: "window-abc123" },
12872
- description: "Bring tab to front"
12880
+ description: "Route untargeted commands to this tab"
12873
12881
  }
12874
12882
  ],
12875
12883
  hints: "<window-id> | see: windows, tabs-close, tabs-open"
@@ -14252,10 +14260,19 @@ registerHandler(tabsClose, async (body, ctx) => {
14252
14260
  registerHandler(tabsFocus, async (body, ctx) => {
14253
14261
  const windowId = body.window || ctx.targetWindowId;
14254
14262
  if (!windowId) {
14255
- return Response.json({ success: false, error: "window id is required" }, { status: 400, headers: ctx.headers });
14263
+ return Response.json({ success: false, error: "window id is required (run `hj tabs` to list connected tabs)" }, { status: 400, headers: ctx.headers });
14256
14264
  }
14257
- const response = await ctx.requestFromBrowser("tabs", "focus", { windowId });
14258
- return Response.json(response, { headers: ctx.headers });
14265
+ const r = ctx.focusWindow(windowId);
14266
+ if (!r.ok) {
14267
+ return Response.json({ success: false, error: r.error }, { status: 404, headers: ctx.headers });
14268
+ }
14269
+ const result = { success: true, focused: windowId, active: r.active !== false };
14270
+ if (r.title)
14271
+ result.title = r.title;
14272
+ if (r.active === false) {
14273
+ result.warning = "Untargeted commands now route to this tab, but it reports HIDDEN \u2014 browsers freeze " + "requestAnimationFrame and throttle timers in a backgrounded tab, so results can be stale " + '("not mounted yet", not "broken"). Bring it to the front in your browser to wake it.';
14274
+ }
14275
+ return Response.json(result, { headers: ctx.headers });
14259
14276
  });
14260
14277
  registerHandler(mutationsWatch, async (body, ctx) => {
14261
14278
  const response = await ctx.requestFromBrowser("mutations", "watch", {
@@ -15275,6 +15292,22 @@ function ambiguousFocusWarning(opts) {
15275
15292
  ${pins}${more}`;
15276
15293
  }
15277
15294
 
15295
+ // src/warning-dedupe.ts
15296
+ function shouldEmitWarning(warning, cache, now, cooldownMs) {
15297
+ const last = cache.get(warning);
15298
+ if (last !== undefined && now - last < cooldownMs) {
15299
+ return false;
15300
+ }
15301
+ cache.set(warning, now);
15302
+ if (cache.size > 64) {
15303
+ for (const [key2, ts] of cache) {
15304
+ if (now - ts >= cooldownMs)
15305
+ cache.delete(key2);
15306
+ }
15307
+ }
15308
+ return true;
15309
+ }
15310
+
15278
15311
  // src/server.ts
15279
15312
  init_terminal();
15280
15313
 
@@ -16129,6 +16162,9 @@ function getComponentJs() {
16129
16162
  }
16130
16163
  var browsers = new Map;
16131
16164
  var windows2 = new Map;
16165
+ var recentTabWarnings = new Map;
16166
+ var TAB_WARN_COOLDOWN_MS = 15000;
16167
+ var TAB_WARN_DISABLED = process.env.HALTIJA_NO_TAB_WARN === "1";
16132
16168
  var agents = new Set;
16133
16169
  var focusedWindowId = null;
16134
16170
  var isDesktopApp = process.env.HALTIJA_DESKTOP === "1";
@@ -16443,6 +16479,8 @@ async function requestFromBrowser(channel, action, payload, timeoutMs = 5000, wi
16443
16479
  return new Promise((resolve) => {
16444
16480
  let sentTo = null;
16445
16481
  const attachWarning = (res) => {
16482
+ if (TAB_WARN_DISABLED)
16483
+ return res;
16446
16484
  const hidden = hiddenTabWarning(sentTo);
16447
16485
  const ambiguous = ambiguousFocusWarning({
16448
16486
  windows: Array.from(windows2.values()),
@@ -16452,7 +16490,12 @@ async function requestFromBrowser(channel, action, payload, timeoutMs = 5000, wi
16452
16490
  const warning = [hidden, ambiguous].filter(Boolean).join(`
16453
16491
 
16454
16492
  `);
16455
- return warning ? { ...res, warning } : res;
16493
+ if (!warning)
16494
+ return res;
16495
+ if (!shouldEmitWarning(warning, recentTabWarnings, Date.now(), TAB_WARN_COOLDOWN_MS)) {
16496
+ return res;
16497
+ }
16498
+ return { ...res, warning };
16456
16499
  };
16457
16500
  const timeout = setTimeout(() => {
16458
16501
  pendingResponses.delete(id);
@@ -16623,6 +16666,15 @@ var createHandlerContext = (req, url) => {
16623
16666
  headers,
16624
16667
  url,
16625
16668
  getWindowInfo,
16669
+ focusWindow: (windowId) => {
16670
+ const win = windows2.get(windowId);
16671
+ if (!win) {
16672
+ return { ok: false, error: `Tab ${windowId} not found (run \`hj tabs\` to list connected tabs)` };
16673
+ }
16674
+ focusedWindowId = windowId;
16675
+ updateHjStatus();
16676
+ return { ok: true, active: win.active, windowType: win.windowType, title: win.title };
16677
+ },
16626
16678
  startRecordingSession,
16627
16679
  stopRecordingSession,
16628
16680
  getRecordingSession,
@@ -18300,12 +18352,10 @@ Type 'help <topic>' for details.`);
18300
18352
  break;
18301
18353
  }
18302
18354
  case "tabs-focus": {
18303
- const resp = await requestFromBrowser("tabs", "focus", { windowId: step.window }, stepTimeout);
18304
- if (!resp.success) {
18355
+ if (!step.window || !windows2.has(step.window)) {
18305
18356
  stepPassed = false;
18306
- error = resp.error || "Failed to focus tab";
18307
- }
18308
- if (step.window && windows2.has(step.window)) {
18357
+ error = `Tab ${step.window ?? "(none)"} not found`;
18358
+ } else {
18309
18359
  focusedWindowId = step.window;
18310
18360
  }
18311
18361
  break;
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.5.1";
11
+ export declare const VERSION = "1.5.2";
@@ -0,0 +1,19 @@
1
+ /**
2
+ * De-duplicate the tab warnings (hidden-tab #3, focus-ambiguity #2) so a burst of untargeted
3
+ * commands from one agent doesn't repeat the same multi-line block on every single one. That
4
+ * repetition is the alarm-fatigue the repo's own skew-warning rationale warns about: a caveat that
5
+ * fires on every command trains agents to ignore it, including the time it matters.
6
+ *
7
+ * **Why a short cooldown and not "once, forever":** the server has many clients and can't tell them
8
+ * apart, so a permanent global suppression would hide the warning from a *second* agent that never
9
+ * saw it — the exact "plausible-but-wrong answer with no caveat" failure these warnings exist to
10
+ * prevent. A short cooldown collapses the one-agent burst (the real fatigue source) while keeping
11
+ * the window in which another client could miss it tiny; the condition persists, so their next
12
+ * command past the cooldown re-warns.
13
+ *
14
+ * **Why key on the whole warning string:** the text already encodes the full condition — which tab
15
+ * answered, whether it's hidden, the exact set of other origins. So a *changed* situation (a
16
+ * different tab, a newly-hidden tab, a new origin on the server) produces different text and is
17
+ * never suppressed. Identical text means identical situation.
18
+ */
19
+ export declare function shouldEmitWarning(warning: string, cache: Map<string, number>, now: number, cooldownMs: number): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "haltija",
3
- "version": "1.5.1",
3
+ "version": "1.5.2",
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",