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 +21 -0
- package/apps/desktop/package.json +1 -1
- package/apps/desktop/resources/component.js +1 -1
- package/bin/tosijs-dev.mjs +1 -0
- package/bin/version.mjs +1 -1
- package/dist/api-handlers.d.ts +12 -0
- package/dist/component.d.ts +1 -1
- package/dist/component.esm.js +1 -1
- package/dist/component.js +1 -1
- package/dist/hj.js +2 -2
- package/dist/index.js +72 -22
- package/dist/server.js +72 -22
- package/dist/version.d.ts +1 -1
- package/dist/warning-dedupe.d.ts +19 -0
- package/package.json +1 -1
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"
|
package/bin/tosijs-dev.mjs
CHANGED
|
@@ -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
package/dist/api-handlers.d.ts
CHANGED
|
@@ -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;
|
package/dist/component.d.ts
CHANGED
|
@@ -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.
|
|
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;
|
package/dist/component.esm.js
CHANGED
package/dist/component.js
CHANGED
package/dist/hj.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
// haltija-cli:do-not-edit v1.5.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
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**:
|
|
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.
|
|
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: `
|
|
12862
|
-
|
|
12863
|
-
|
|
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: "
|
|
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
|
|
14258
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18304
|
-
if (!resp.success) {
|
|
18355
|
+
if (!step.window || !windows2.has(step.window)) {
|
|
18305
18356
|
stepPassed = false;
|
|
18306
|
-
error =
|
|
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.
|
|
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
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
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**:
|
|
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.
|
|
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: `
|
|
12862
|
-
|
|
12863
|
-
|
|
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: "
|
|
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
|
|
14258
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18304
|
-
if (!resp.success) {
|
|
18355
|
+
if (!step.window || !windows2.has(step.window)) {
|
|
18305
18356
|
stepPassed = false;
|
|
18306
|
-
error =
|
|
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
|
@@ -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;
|