haltija 1.5.0 → 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 +43 -0
- package/apps/desktop/main.js +6 -1
- package/apps/desktop/package.json +1 -1
- package/apps/desktop/resources/component.js +1 -1
- package/bin/arg-utils.mjs +21 -0
- package/bin/hj.mjs +6 -6
- package/bin/tosijs-dev.mjs +1 -0
- package/bin/version.mjs +1 -1
- package/dist/api-handlers.d.ts +14 -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 +16 -8
- package/dist/index.js +83 -29
- package/dist/server.js +83 -29
- 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,48 @@
|
|
|
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
|
+
|
|
24
|
+
## 1.5.1
|
|
25
|
+
|
|
26
|
+
Low-risk follow-ups from the 1.5.0 pre-release review — the two new "instrument must not lie"
|
|
27
|
+
warnings now reach more of the surfaces where they matter.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- **The hidden-tab / focus-ambiguity warning is no longer dropped on the paths where it's most
|
|
32
|
+
useful.** It's now attached on the **timeout** path — a hidden tab whose rAF-driven `eval` never
|
|
33
|
+
resolves now returns a `Timeout` that *explains* it may be asleep, instead of a bare timeout —
|
|
34
|
+
and preserved by the `hj find` / `hj form` handlers, which previously reshaped the response and
|
|
35
|
+
lost it. (`hj screenshot` already carried it; `hj call` intentionally still returns the raw value
|
|
36
|
+
with no envelope.)
|
|
37
|
+
|
|
38
|
+
### Internal
|
|
39
|
+
|
|
40
|
+
- Extracted the `hj --window <id>` argument handling into a pure, unit-tested helper
|
|
41
|
+
(`bin/arg-utils.mjs`), covering both the leading and trailing positions — the leading form was
|
|
42
|
+
the escape hatch that broke in 1.4.0, and it now has a regression test.
|
|
43
|
+
- A private-app startup that fails to learn its ephemeral port no longer leaves its temp port-files
|
|
44
|
+
behind.
|
|
45
|
+
|
|
3
46
|
## 1.5.0
|
|
4
47
|
|
|
5
48
|
Completes the **private-automation** feature (`--private`) begun in 1.4.1 — now for the Electron
|
package/apps/desktop/main.js
CHANGED
|
@@ -1350,7 +1350,12 @@ async function startEmbeddedServer() {
|
|
|
1350
1350
|
}
|
|
1351
1351
|
const pubPort = await readPort(pubFile)
|
|
1352
1352
|
const intPort = await readPort(intFile)
|
|
1353
|
-
if (!pubPort) {
|
|
1353
|
+
if (!pubPort) {
|
|
1354
|
+
// Don't leak the pid-scoped tmp port-files on the failure path (the success path cleans below).
|
|
1355
|
+
try { fs.rmSync(pubFile, { force: true }); fs.rmSync(intFile, { force: true }) } catch {}
|
|
1356
|
+
console.error('[Haltija Desktop] Private public server did not report its port')
|
|
1357
|
+
return false
|
|
1358
|
+
}
|
|
1354
1359
|
|
|
1355
1360
|
HALTIJA_PORT = pubPort
|
|
1356
1361
|
HALTIJA_SERVER = `http://localhost:${pubPort}`
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure arg helpers for the `hj` CLI, extracted so they can be unit-tested (see
|
|
3
|
+
* src/hj-args.test.ts) — the leading-`--window` form was fully broken and shipped without a test.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Pull a `--window <id>` flag out of `args` from ANY position, returning the id and the remaining
|
|
8
|
+
* args (input is not mutated). Mirrors how `--port`/`--name`/`--token` are pre-parsed, so both
|
|
9
|
+
* `hj --window <id> <cmd>` (leading) and `hj <cmd> --window <id>` (trailing) resolve the same tab.
|
|
10
|
+
* A `--window` with no following value is left in place (treated as not-a-target) so it surfaces as
|
|
11
|
+
* a normal unknown-flag rather than silently swallowing the next real arg.
|
|
12
|
+
*/
|
|
13
|
+
export function extractWindowTarget(args) {
|
|
14
|
+
const i = args.indexOf('--window')
|
|
15
|
+
if (i === -1 || args[i + 1] === undefined) {
|
|
16
|
+
return { windowTarget: null, args: [...args] }
|
|
17
|
+
}
|
|
18
|
+
const rest = [...args]
|
|
19
|
+
rest.splice(i, 2)
|
|
20
|
+
return { windowTarget: args[i + 1], args: rest }
|
|
21
|
+
}
|
package/bin/hj.mjs
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { runSubcommand, isSubcommand, getSuggestion, listSubcommands, COMMAND_HINTS } from './cli-subcommand.mjs'
|
|
14
|
+
import { extractWindowTarget } from './arg-utils.mjs'
|
|
14
15
|
import { HJ_VERSION } from './version.mjs'
|
|
15
16
|
import { differsBeyondPatch } from './semver.mjs'
|
|
16
17
|
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
@@ -313,12 +314,11 @@ if (noLaunchIdx !== -1) {
|
|
|
313
314
|
// died with "Unknown command: '--window'" — i.e. the escape hatch we tell people to use for a
|
|
314
315
|
// hidden/wrong tab didn't work in the shape the docs gave. Pulled out here and re-appended to
|
|
315
316
|
// the subcommand args below, so BOTH positions work.
|
|
316
|
-
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}
|
|
317
|
+
const { windowTarget, args: argsWithoutWindow } = extractWindowTarget(args)
|
|
318
|
+
// Preserve the `const args` reference (downstream code mutates it in place) while dropping the
|
|
319
|
+
// consumed --window <id> pair.
|
|
320
|
+
args.length = 0
|
|
321
|
+
args.push(...argsWithoutWindow)
|
|
322
322
|
|
|
323
323
|
// Did the shell explicitly target a private instance (--port / --name /
|
|
324
324
|
// HALTIJA_PORT / HALTIJA_NAME / DEV_CHANNEL_PORT)? If so, this is a
|
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
|
@@ -16,6 +16,8 @@ export interface DevResponse {
|
|
|
16
16
|
data?: any;
|
|
17
17
|
error?: string;
|
|
18
18
|
timestamp: number;
|
|
19
|
+
/** Hidden-tab / focus-ambiguity caveat attached by the server (see requestFromBrowser). */
|
|
20
|
+
warning?: string;
|
|
19
21
|
}
|
|
20
22
|
/** Function to send request to browser widget */
|
|
21
23
|
export type RequestFromBrowserFn = (channel: string, action: string, payload: any, timeoutMs?: number, windowId?: string) => Promise<DevResponse>;
|
|
@@ -50,6 +52,18 @@ export interface HandlerContext {
|
|
|
50
52
|
headers: Record<string, string>;
|
|
51
53
|
url: URL;
|
|
52
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
|
+
};
|
|
53
67
|
startRecordingSession: (windowId: string, url: string, name?: string) => void;
|
|
54
68
|
stopRecordingSession: (windowId: string) => RecordingSessionInfo | undefined;
|
|
55
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) {
|
|
@@ -1906,6 +1906,17 @@ function dim2(s) {
|
|
|
1906
1906
|
return `\x1B[2m${s}\x1B[0m`;
|
|
1907
1907
|
}
|
|
1908
1908
|
|
|
1909
|
+
// bin/arg-utils.mjs
|
|
1910
|
+
function extractWindowTarget(args) {
|
|
1911
|
+
const i = args.indexOf("--window");
|
|
1912
|
+
if (i === -1 || args[i + 1] === undefined) {
|
|
1913
|
+
return { windowTarget: null, args: [...args] };
|
|
1914
|
+
}
|
|
1915
|
+
const rest = [...args];
|
|
1916
|
+
rest.splice(i, 2);
|
|
1917
|
+
return { windowTarget: args[i + 1], args: rest };
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1909
1920
|
// bin/hj.mjs
|
|
1910
1921
|
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "node:fs";
|
|
1911
1922
|
import { homedir as homedir2 } from "node:os";
|
|
@@ -2137,12 +2148,9 @@ if (noLaunchIdx !== -1) {
|
|
|
2137
2148
|
noLaunch = true;
|
|
2138
2149
|
args.splice(noLaunchIdx, 1);
|
|
2139
2150
|
}
|
|
2140
|
-
var windowTarget =
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
windowTarget = args[windowIdx + 1];
|
|
2144
|
-
args.splice(windowIdx, 2);
|
|
2145
|
-
}
|
|
2151
|
+
var { windowTarget, args: argsWithoutWindow } = extractWindowTarget(args);
|
|
2152
|
+
args.length = 0;
|
|
2153
|
+
args.push(...argsWithoutWindow);
|
|
2146
2154
|
var explicitTarget = portSource !== "8700 (default)";
|
|
2147
2155
|
if (args.length >= 2 && isSubcommand(`${args[0]}-${args[1]}`)) {
|
|
2148
2156
|
args.splice(0, 2, `${args[0]}-${args[1]}`);
|
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"
|
|
@@ -13727,6 +13735,9 @@ function inferSuggestion(step, pageContext) {
|
|
|
13727
13735
|
}
|
|
13728
13736
|
|
|
13729
13737
|
// src/api-handlers.ts
|
|
13738
|
+
function withWarning(body, response) {
|
|
13739
|
+
return response.warning ? { ...body, warning: response.warning } : body;
|
|
13740
|
+
}
|
|
13730
13741
|
var handlers = new Map;
|
|
13731
13742
|
function registerHandler(endpoint2, handler) {
|
|
13732
13743
|
handlers.set(endpoint2.path, handler);
|
|
@@ -14249,10 +14260,19 @@ registerHandler(tabsClose, async (body, ctx) => {
|
|
|
14249
14260
|
registerHandler(tabsFocus, async (body, ctx) => {
|
|
14250
14261
|
const windowId = body.window || ctx.targetWindowId;
|
|
14251
14262
|
if (!windowId) {
|
|
14252
|
-
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 });
|
|
14253
14264
|
}
|
|
14254
|
-
const
|
|
14255
|
-
|
|
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 });
|
|
14256
14276
|
});
|
|
14257
14277
|
registerHandler(mutationsWatch, async (body, ctx) => {
|
|
14258
14278
|
const response = await ctx.requestFromBrowser("mutations", "watch", {
|
|
@@ -14557,7 +14577,7 @@ registerHandler(find, async (body, ctx) => {
|
|
|
14557
14577
|
})()`;
|
|
14558
14578
|
const response = await ctx.requestFromBrowser("eval", "exec", { code: findCode }, 5000, windowId);
|
|
14559
14579
|
if (response.success && response.data) {
|
|
14560
|
-
return Response.json({ success: true, ...response.data }, { headers: ctx.headers });
|
|
14580
|
+
return Response.json(withWarning({ success: true, ...response.data }, response), { headers: ctx.headers });
|
|
14561
14581
|
}
|
|
14562
14582
|
return Response.json(response, { headers: ctx.headers });
|
|
14563
14583
|
});
|
|
@@ -14663,7 +14683,7 @@ registerHandler(formData, async (body, ctx) => {
|
|
|
14663
14683
|
})()`;
|
|
14664
14684
|
const response = await ctx.requestFromBrowser("eval", "exec", { code: formCode }, 5000, windowId);
|
|
14665
14685
|
if (response.success && response.data) {
|
|
14666
|
-
return Response.json(response.data, { headers: ctx.headers });
|
|
14686
|
+
return Response.json(withWarning(response.data, response), { headers: ctx.headers });
|
|
14667
14687
|
}
|
|
14668
14688
|
return Response.json(response, { headers: ctx.headers });
|
|
14669
14689
|
});
|
|
@@ -15272,6 +15292,22 @@ function ambiguousFocusWarning(opts) {
|
|
|
15272
15292
|
${pins}${more}`;
|
|
15273
15293
|
}
|
|
15274
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
|
+
|
|
15275
15311
|
// src/server.ts
|
|
15276
15312
|
init_terminal();
|
|
15277
15313
|
|
|
@@ -16126,6 +16162,9 @@ function getComponentJs() {
|
|
|
16126
16162
|
}
|
|
16127
16163
|
var browsers = new Map;
|
|
16128
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";
|
|
16129
16168
|
var agents = new Set;
|
|
16130
16169
|
var focusedWindowId = null;
|
|
16131
16170
|
var isDesktopApp = process.env.HALTIJA_DESKTOP === "1";
|
|
@@ -16438,12 +16477,10 @@ async function requestFromBrowser(channel, action, payload, timeoutMs = 5000, wi
|
|
|
16438
16477
|
source: "agent"
|
|
16439
16478
|
};
|
|
16440
16479
|
return new Promise((resolve) => {
|
|
16441
|
-
const timeout = setTimeout(() => {
|
|
16442
|
-
pendingResponses.delete(id);
|
|
16443
|
-
resolve({ id, success: false, error: "Timeout", timestamp: Date.now() });
|
|
16444
|
-
}, timeoutMs);
|
|
16445
16480
|
let sentTo = null;
|
|
16446
|
-
const
|
|
16481
|
+
const attachWarning = (res) => {
|
|
16482
|
+
if (TAB_WARN_DISABLED)
|
|
16483
|
+
return res;
|
|
16447
16484
|
const hidden = hiddenTabWarning(sentTo);
|
|
16448
16485
|
const ambiguous = ambiguousFocusWarning({
|
|
16449
16486
|
windows: Array.from(windows2.values()),
|
|
@@ -16453,8 +16490,18 @@ async function requestFromBrowser(channel, action, payload, timeoutMs = 5000, wi
|
|
|
16453
16490
|
const warning = [hidden, ambiguous].filter(Boolean).join(`
|
|
16454
16491
|
|
|
16455
16492
|
`);
|
|
16456
|
-
|
|
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 };
|
|
16457
16499
|
};
|
|
16500
|
+
const timeout = setTimeout(() => {
|
|
16501
|
+
pendingResponses.delete(id);
|
|
16502
|
+
resolve(attachWarning({ id, success: false, error: "Timeout", timestamp: Date.now() }));
|
|
16503
|
+
}, timeoutMs);
|
|
16504
|
+
const resolveWithLiveness = (res) => resolve(attachWarning(res));
|
|
16458
16505
|
pendingResponses.set(id, { resolve: resolveWithLiveness, timeout });
|
|
16459
16506
|
if (windowId) {
|
|
16460
16507
|
const win = windows2.get(windowId);
|
|
@@ -16619,6 +16666,15 @@ var createHandlerContext = (req, url) => {
|
|
|
16619
16666
|
headers,
|
|
16620
16667
|
url,
|
|
16621
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
|
+
},
|
|
16622
16678
|
startRecordingSession,
|
|
16623
16679
|
stopRecordingSession,
|
|
16624
16680
|
getRecordingSession,
|
|
@@ -18296,12 +18352,10 @@ Type 'help <topic>' for details.`);
|
|
|
18296
18352
|
break;
|
|
18297
18353
|
}
|
|
18298
18354
|
case "tabs-focus": {
|
|
18299
|
-
|
|
18300
|
-
if (!resp.success) {
|
|
18355
|
+
if (!step.window || !windows2.has(step.window)) {
|
|
18301
18356
|
stepPassed = false;
|
|
18302
|
-
error =
|
|
18303
|
-
}
|
|
18304
|
-
if (step.window && windows2.has(step.window)) {
|
|
18357
|
+
error = `Tab ${step.window ?? "(none)"} not found`;
|
|
18358
|
+
} else {
|
|
18305
18359
|
focusedWindowId = step.window;
|
|
18306
18360
|
}
|
|
18307
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"
|
|
@@ -13727,6 +13735,9 @@ function inferSuggestion(step, pageContext) {
|
|
|
13727
13735
|
}
|
|
13728
13736
|
|
|
13729
13737
|
// src/api-handlers.ts
|
|
13738
|
+
function withWarning(body, response) {
|
|
13739
|
+
return response.warning ? { ...body, warning: response.warning } : body;
|
|
13740
|
+
}
|
|
13730
13741
|
var handlers = new Map;
|
|
13731
13742
|
function registerHandler(endpoint2, handler) {
|
|
13732
13743
|
handlers.set(endpoint2.path, handler);
|
|
@@ -14249,10 +14260,19 @@ registerHandler(tabsClose, async (body, ctx) => {
|
|
|
14249
14260
|
registerHandler(tabsFocus, async (body, ctx) => {
|
|
14250
14261
|
const windowId = body.window || ctx.targetWindowId;
|
|
14251
14262
|
if (!windowId) {
|
|
14252
|
-
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 });
|
|
14253
14264
|
}
|
|
14254
|
-
const
|
|
14255
|
-
|
|
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 });
|
|
14256
14276
|
});
|
|
14257
14277
|
registerHandler(mutationsWatch, async (body, ctx) => {
|
|
14258
14278
|
const response = await ctx.requestFromBrowser("mutations", "watch", {
|
|
@@ -14557,7 +14577,7 @@ registerHandler(find, async (body, ctx) => {
|
|
|
14557
14577
|
})()`;
|
|
14558
14578
|
const response = await ctx.requestFromBrowser("eval", "exec", { code: findCode }, 5000, windowId);
|
|
14559
14579
|
if (response.success && response.data) {
|
|
14560
|
-
return Response.json({ success: true, ...response.data }, { headers: ctx.headers });
|
|
14580
|
+
return Response.json(withWarning({ success: true, ...response.data }, response), { headers: ctx.headers });
|
|
14561
14581
|
}
|
|
14562
14582
|
return Response.json(response, { headers: ctx.headers });
|
|
14563
14583
|
});
|
|
@@ -14663,7 +14683,7 @@ registerHandler(formData, async (body, ctx) => {
|
|
|
14663
14683
|
})()`;
|
|
14664
14684
|
const response = await ctx.requestFromBrowser("eval", "exec", { code: formCode }, 5000, windowId);
|
|
14665
14685
|
if (response.success && response.data) {
|
|
14666
|
-
return Response.json(response.data, { headers: ctx.headers });
|
|
14686
|
+
return Response.json(withWarning(response.data, response), { headers: ctx.headers });
|
|
14667
14687
|
}
|
|
14668
14688
|
return Response.json(response, { headers: ctx.headers });
|
|
14669
14689
|
});
|
|
@@ -15272,6 +15292,22 @@ function ambiguousFocusWarning(opts) {
|
|
|
15272
15292
|
${pins}${more}`;
|
|
15273
15293
|
}
|
|
15274
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
|
+
|
|
15275
15311
|
// src/server.ts
|
|
15276
15312
|
init_terminal();
|
|
15277
15313
|
|
|
@@ -16126,6 +16162,9 @@ function getComponentJs() {
|
|
|
16126
16162
|
}
|
|
16127
16163
|
var browsers = new Map;
|
|
16128
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";
|
|
16129
16168
|
var agents = new Set;
|
|
16130
16169
|
var focusedWindowId = null;
|
|
16131
16170
|
var isDesktopApp = process.env.HALTIJA_DESKTOP === "1";
|
|
@@ -16438,12 +16477,10 @@ async function requestFromBrowser(channel, action, payload, timeoutMs = 5000, wi
|
|
|
16438
16477
|
source: "agent"
|
|
16439
16478
|
};
|
|
16440
16479
|
return new Promise((resolve) => {
|
|
16441
|
-
const timeout = setTimeout(() => {
|
|
16442
|
-
pendingResponses.delete(id);
|
|
16443
|
-
resolve({ id, success: false, error: "Timeout", timestamp: Date.now() });
|
|
16444
|
-
}, timeoutMs);
|
|
16445
16480
|
let sentTo = null;
|
|
16446
|
-
const
|
|
16481
|
+
const attachWarning = (res) => {
|
|
16482
|
+
if (TAB_WARN_DISABLED)
|
|
16483
|
+
return res;
|
|
16447
16484
|
const hidden = hiddenTabWarning(sentTo);
|
|
16448
16485
|
const ambiguous = ambiguousFocusWarning({
|
|
16449
16486
|
windows: Array.from(windows2.values()),
|
|
@@ -16453,8 +16490,18 @@ async function requestFromBrowser(channel, action, payload, timeoutMs = 5000, wi
|
|
|
16453
16490
|
const warning = [hidden, ambiguous].filter(Boolean).join(`
|
|
16454
16491
|
|
|
16455
16492
|
`);
|
|
16456
|
-
|
|
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 };
|
|
16457
16499
|
};
|
|
16500
|
+
const timeout = setTimeout(() => {
|
|
16501
|
+
pendingResponses.delete(id);
|
|
16502
|
+
resolve(attachWarning({ id, success: false, error: "Timeout", timestamp: Date.now() }));
|
|
16503
|
+
}, timeoutMs);
|
|
16504
|
+
const resolveWithLiveness = (res) => resolve(attachWarning(res));
|
|
16458
16505
|
pendingResponses.set(id, { resolve: resolveWithLiveness, timeout });
|
|
16459
16506
|
if (windowId) {
|
|
16460
16507
|
const win = windows2.get(windowId);
|
|
@@ -16619,6 +16666,15 @@ var createHandlerContext = (req, url) => {
|
|
|
16619
16666
|
headers,
|
|
16620
16667
|
url,
|
|
16621
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
|
+
},
|
|
16622
16678
|
startRecordingSession,
|
|
16623
16679
|
stopRecordingSession,
|
|
16624
16680
|
getRecordingSession,
|
|
@@ -18296,12 +18352,10 @@ Type 'help <topic>' for details.`);
|
|
|
18296
18352
|
break;
|
|
18297
18353
|
}
|
|
18298
18354
|
case "tabs-focus": {
|
|
18299
|
-
|
|
18300
|
-
if (!resp.success) {
|
|
18355
|
+
if (!step.window || !windows2.has(step.window)) {
|
|
18301
18356
|
stepPassed = false;
|
|
18302
|
-
error =
|
|
18303
|
-
}
|
|
18304
|
-
if (step.window && windows2.has(step.window)) {
|
|
18357
|
+
error = `Tab ${step.window ?? "(none)"} not found`;
|
|
18358
|
+
} else {
|
|
18305
18359
|
focusedWindowId = step.window;
|
|
18306
18360
|
}
|
|
18307
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;
|