haltija 1.5.1 → 1.5.4
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 +53 -0
- package/apps/desktop/package.json +1 -1
- package/apps/desktop/resources/component.js +6 -2
- package/bin/tosijs-dev.mjs +28 -11
- 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 +6 -2
- package/dist/component.js +6 -2
- package/dist/hj.js +2 -2
- package/dist/index.js +108 -30
- package/dist/server.js +108 -30
- package/dist/version.d.ts +1 -1
- package/dist/warning-dedupe.d.ts +19 -0
- package/docs/CI-INTEGRATION.md +25 -2
- package/llms.txt +11 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,58 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.4
|
|
4
|
+
|
|
5
|
+
The `hj tabs open` client-less-tab trap now explains itself ([#5](https://github.com/tonioloewald/haltija/issues/5)).
|
|
6
|
+
|
|
7
|
+
Outside the desktop app, `hj tabs open <url>` has no tab API, so it falls back to `window.open()` —
|
|
8
|
+
and that new tab has **no haltija widget** unless its page injects one. So the server never hears
|
|
9
|
+
from it, it doesn't appear in `hj tabs`, and commands can't reach it (they go to the focused widget
|
|
10
|
+
tab). It presents as "the tab opened fine but every command goes somewhere else" — indistinguishable
|
|
11
|
+
from a routing bug.
|
|
12
|
+
|
|
13
|
+
- The fallback response now carries a `reason`, promoted to a top-level `warning` that `hj` prints
|
|
14
|
+
on stderr — at the one moment the client-less tab is created.
|
|
15
|
+
- The `/tabs/open` schema and `SKILL.md` now state plainly that only widget-injected tabs are
|
|
16
|
+
controllable and appear in `hj tabs`, so a non-responding tab is the first thing to check.
|
|
17
|
+
|
|
18
|
+
## 1.5.3
|
|
19
|
+
|
|
20
|
+
Discoverability fix for the two CI browser engines ([#6](https://github.com/tonioloewald/haltija/issues/6)).
|
|
21
|
+
|
|
22
|
+
Both `--headless` and `--ci` said "for CI" with no hint that they drive **different** engines:
|
|
23
|
+
`--headless` is Playwright Chromium (and needs the `playwright` package), while `--ci` / `--app` /
|
|
24
|
+
`--private --app` drive Electron and need no Playwright. An agent picked `--headless`, hit
|
|
25
|
+
"Playwright not installed", and wrongly concluded haltija's CI mode is just a Playwright wrapper.
|
|
26
|
+
|
|
27
|
+
- `hj --help` now names the engine per mode and adds a "Choosing a CI engine" block; `--private` is
|
|
28
|
+
clarified as an *isolation* modifier that pairs with either engine (not "pair with `--headless`").
|
|
29
|
+
- The "Playwright not installed" error points at the Electron path (`--ci` / `--private --app`) as
|
|
30
|
+
the no-Playwright alternative.
|
|
31
|
+
- `llms.txt`, `docs/CI-INTEGRATION.md`, and `SKILL.md` get an honest "which engine?" framing:
|
|
32
|
+
it's Electron vs Playwright (neither is bundled), and the real reason to choose Playwright is
|
|
33
|
+
multi-engine coverage (Firefox/WebKit), not the words "for CI".
|
|
34
|
+
|
|
35
|
+
## 1.5.2
|
|
36
|
+
|
|
37
|
+
Two follow-ups from the 1.5.0 review, both about the multi-tab experience on a shared server.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- **`hj tabs focus <id>` no longer times out** ([#4](https://github.com/tonioloewald/haltija/issues/4)).
|
|
42
|
+
It was dispatching a `focus` command to the browser, routed to the *focused* tab rather than the
|
|
43
|
+
target, so nobody answered — and even routed correctly, a backgrounded tab can't raise itself.
|
|
44
|
+
Focus is now a **server-side** routing change: it validates the tab and points untargeted commands
|
|
45
|
+
at it, returning instantly (unknown tab → a clean error, never a timeout). It does not physically
|
|
46
|
+
raise the tab; to pin a single command use `--window <id>`. "Focus follows the visible tab" still
|
|
47
|
+
applies when you physically switch tabs — that's genuine intent that should win over a stale pin.
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
|
|
51
|
+
- **The hidden-tab / focus-ambiguity warnings are de-duplicated within a short (15s) cooldown**, so
|
|
52
|
+
a burst of commands from one agent doesn't repeat the same block every time. A *changed* condition
|
|
53
|
+
(different tab, newly-hidden tab, a new origin on the server) always re-warns; the cooldown
|
|
54
|
+
re-arms rather than suppressing forever. Set `HALTIJA_NO_TAB_WARN=1` to silence them entirely.
|
|
55
|
+
|
|
3
56
|
## 1.5.1
|
|
4
57
|
|
|
5
58
|
Low-risk follow-ups from the 1.5.0 pre-release review — the two new "instrument must not lie"
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
// src/version.ts
|
|
49
|
-
var VERSION = "1.5.
|
|
49
|
+
var VERSION = "1.5.4";
|
|
50
50
|
|
|
51
51
|
// src/text-selector.ts
|
|
52
52
|
var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\(/;
|
|
@@ -5637,7 +5637,11 @@ ${elementSummary}${moreText}`;
|
|
|
5637
5637
|
});
|
|
5638
5638
|
} else {
|
|
5639
5639
|
window.open(payload2.url, "_blank");
|
|
5640
|
-
this.respond(msg2.id, true, {
|
|
5640
|
+
this.respond(msg2.id, true, {
|
|
5641
|
+
opened: true,
|
|
5642
|
+
fallback: true,
|
|
5643
|
+
reason: "Opened a plain browser tab via window.open() (this is not the Haltija desktop app). " + "That tab has NO Haltija widget unless its page injects one, so it will NOT appear in " + "`hj tabs` and hj commands cannot reach it — untargeted commands go to the focused " + "widget tab instead. To control the new tab: inject the widget on that page (e.g. " + "HALTIJA_DEV=1 / haltijaDev:true in that project), or use the Haltija desktop app, " + "which auto-injects into every tab."
|
|
5644
|
+
});
|
|
5641
5645
|
}
|
|
5642
5646
|
} else if (action2 === "close") {
|
|
5643
5647
|
if (haltija?.closeTab) {
|
package/bin/tosijs-dev.mjs
CHANGED
|
@@ -37,12 +37,12 @@ ${bold('haltija')} - Browser control for AI agents
|
|
|
37
37
|
Usage:
|
|
38
38
|
haltija [options]
|
|
39
39
|
|
|
40
|
-
Modes:
|
|
40
|
+
Modes: ${dim('(the browser ENGINE differs — see "Choosing a CI engine" below)')}
|
|
41
41
|
${dim('(default)')} Launch desktop app if electron available, otherwise server
|
|
42
|
-
--app Explicitly launch desktop app (Electron)
|
|
43
|
-
--server Server only
|
|
44
|
-
--headless
|
|
45
|
-
--ci CI mode: Electron app + wait
|
|
42
|
+
--app Explicitly launch desktop app (Electron — Chromium)
|
|
43
|
+
--server Server only; bring your own browser (inject the widget). For bookmarklets/CI-lite
|
|
44
|
+
--headless Headless Chromium via ${bold('Playwright')} (needs the 'playwright' package) — for CI
|
|
45
|
+
--ci CI mode: ${bold('Electron')} app + wait-ready + sandbox off. No Playwright. Recommended for CI
|
|
46
46
|
|
|
47
47
|
Options:
|
|
48
48
|
--http HTTP only on port 8700 (default protocol)
|
|
@@ -62,7 +62,9 @@ Options:
|
|
|
62
62
|
--private Isolated automation instance: own server on an EPHEMERAL port (never
|
|
63
63
|
8700), own browser, torn down with the run. Never sees/adopts/navigates
|
|
64
64
|
the shared interactive browser. Reports its address on stdout
|
|
65
|
-
(HALTIJA_PRIVATE_READY {json}) and to --port-file.
|
|
65
|
+
(HALTIJA_PRIVATE_READY {json}) and to --port-file. This is an ISOLATION
|
|
66
|
+
modifier, not an engine: pair with --app (Electron, no Playwright) or
|
|
67
|
+
--headless (Playwright Chromium).
|
|
66
68
|
--port-file <p> Write the (private) server's bound address as JSON to <p> when ready.
|
|
67
69
|
--setup-mcp Configure Claude Desktop MCP integration
|
|
68
70
|
--setup-mcp-check Check MCP configuration status
|
|
@@ -88,6 +90,7 @@ Environment Variables:
|
|
|
88
90
|
HALTIJA_NO_RETIRE Set to 1: do not stop pre-1.4.0 haltija servers
|
|
89
91
|
HALTIJA_NO_INSTALL Set to 1: do not install hj into ~/.local/bin
|
|
90
92
|
HALTIJA_NO_SKEW_WARN Set to 1: silence hj's version-skew warning
|
|
93
|
+
HALTIJA_NO_TAB_WARN Set to 1: silence hidden-tab / focus-ambiguity result warnings
|
|
91
94
|
HALTIJA_REGISTRY_DIR Instance registry location (default: ~/.haltija/servers)
|
|
92
95
|
DEV_CHANNEL_PORT Legacy alias for HALTIJA_PORT
|
|
93
96
|
DEV_CHANNEL_HTTPS_PORT HTTPS port (default: 8701)
|
|
@@ -105,13 +108,23 @@ Subcommands:
|
|
|
105
108
|
|
|
106
109
|
Use 'hj' as a short alias: hj tree, hj click @42, etc.
|
|
107
110
|
|
|
111
|
+
Choosing a CI engine ${dim('(both need one external browser, but a DIFFERENT one):')}
|
|
112
|
+
${bold('Electron')} (Chromium, same engine the desktop app uses) — no Playwright:
|
|
113
|
+
haltija --ci # recommended: Electron + wait-ready + sandbox off
|
|
114
|
+
haltija --private --app # + isolation (own ephemeral port, adopts nothing)
|
|
115
|
+
${dim('Electron is fetched via npx if not already installed.')}
|
|
116
|
+
${bold('Playwright')} Chromium — needs the 'playwright' package:
|
|
117
|
+
npm i playwright && npx playwright install chromium
|
|
118
|
+
haltija --headless # single-engine; or --private --headless for isolation
|
|
119
|
+
${dim("Pick this for MULTI-ENGINE coverage (Firefox/WebKit) — not just because it says \"CI\".")}
|
|
120
|
+
|
|
108
121
|
Examples:
|
|
109
122
|
haltija # Desktop app (or server fallback)
|
|
110
123
|
haltija --app # Desktop app explicitly
|
|
111
|
-
haltija --server # Server only
|
|
124
|
+
haltija --server # Server only (bring your own browser)
|
|
112
125
|
haltija --server --https # HTTPS server only
|
|
113
|
-
haltija --
|
|
114
|
-
haltija --
|
|
126
|
+
haltija --ci # CI: Electron (no Playwright) + wait + sandbox off
|
|
127
|
+
haltija --headless # CI: Playwright Chromium (needs playwright)
|
|
115
128
|
haltija --setup-mcp # Configure Claude Desktop integration
|
|
116
129
|
`)
|
|
117
130
|
process.exit(0)
|
|
@@ -728,8 +741,12 @@ const startHeadlessBrowser = async (port) => {
|
|
|
728
741
|
|
|
729
742
|
} catch (err) {
|
|
730
743
|
if (err.code === 'ERR_MODULE_NOT_FOUND') {
|
|
731
|
-
console.error('[tosijs-dev] Playwright not installed.
|
|
732
|
-
console.error('[tosijs-dev]
|
|
744
|
+
console.error('[tosijs-dev] --headless uses Playwright Chromium, which is not installed.')
|
|
745
|
+
console.error('[tosijs-dev] To use Playwright: npm install playwright && npx playwright install chromium')
|
|
746
|
+
console.error('[tosijs-dev] Or skip Playwright entirely and use the ELECTRON engine instead:')
|
|
747
|
+
console.error('[tosijs-dev] haltija --ci (Electron, waits for ready, sandbox off)')
|
|
748
|
+
console.error('[tosijs-dev] haltija --private --app (Electron + isolated ephemeral instance)')
|
|
749
|
+
console.error('[tosijs-dev] Reach for --headless (Playwright) when you need Firefox/WebKit coverage.')
|
|
733
750
|
} else {
|
|
734
751
|
console.error('[tosijs-dev] Failed to start headless browser:', err.message)
|
|
735
752
|
}
|
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.4";
|
|
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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/version.ts
|
|
2
|
-
var VERSION = "1.5.
|
|
2
|
+
var VERSION = "1.5.4";
|
|
3
3
|
|
|
4
4
|
// src/text-selector.ts
|
|
5
5
|
var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\(/;
|
|
@@ -5590,7 +5590,11 @@ ${elementSummary}${moreText}`;
|
|
|
5590
5590
|
});
|
|
5591
5591
|
} else {
|
|
5592
5592
|
window.open(payload2.url, "_blank");
|
|
5593
|
-
this.respond(msg2.id, true, {
|
|
5593
|
+
this.respond(msg2.id, true, {
|
|
5594
|
+
opened: true,
|
|
5595
|
+
fallback: true,
|
|
5596
|
+
reason: "Opened a plain browser tab via window.open() (this is not the Haltija desktop app). " + "That tab has NO Haltija widget unless its page injects one, so it will NOT appear in " + "`hj tabs` and hj commands cannot reach it — untargeted commands go to the focused " + "widget tab instead. To control the new tab: inject the widget on that page (e.g. " + "HALTIJA_DEV=1 / haltijaDev:true in that project), or use the Haltija desktop app, " + "which auto-injects into every tab."
|
|
5597
|
+
});
|
|
5594
5598
|
}
|
|
5595
5599
|
} else if (action2 === "close") {
|
|
5596
5600
|
if (haltija?.closeTab) {
|
package/dist/component.js
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
// src/version.ts
|
|
49
|
-
var VERSION = "1.5.
|
|
49
|
+
var VERSION = "1.5.4";
|
|
50
50
|
|
|
51
51
|
// src/text-selector.ts
|
|
52
52
|
var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\(/;
|
|
@@ -5637,7 +5637,11 @@ ${elementSummary}${moreText}`;
|
|
|
5637
5637
|
});
|
|
5638
5638
|
} else {
|
|
5639
5639
|
window.open(payload2.url, "_blank");
|
|
5640
|
-
this.respond(msg2.id, true, {
|
|
5640
|
+
this.respond(msg2.id, true, {
|
|
5641
|
+
opened: true,
|
|
5642
|
+
fallback: true,
|
|
5643
|
+
reason: "Opened a plain browser tab via window.open() (this is not the Haltija desktop app). " + "That tab has NO Haltija widget unless its page injects one, so it will NOT appear in " + "`hj tabs` and hj commands cannot reach it — untargeted commands go to the focused " + "widget tab instead. To control the new tab: inject the widget on that page (e.g. " + "HALTIJA_DEV=1 / haltijaDev:true in that project), or use the Haltija desktop app, " + "which auto-injects into every tab."
|
|
5644
|
+
});
|
|
5641
5645
|
}
|
|
5642
5646
|
} else if (action2 === "close") {
|
|
5643
5647
|
if (haltija?.closeTab) {
|
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.4
|
|
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.4";
|
|
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.4";
|
|
678
678
|
|
|
679
679
|
// src/embedded-assets.ts
|
|
680
680
|
var APP_MD = `# Haltija App
|
|
@@ -2176,9 +2176,15 @@ Use window IDs in other endpoints (e.g., /click, /tree) to target specific tabs.
|
|
|
2176
2176
|
|
|
2177
2177
|
**Open a new tab**
|
|
2178
2178
|
|
|
2179
|
-
|
|
2179
|
+
Opens a new tab with optional URL. If url is omitted, opens a blank tab.
|
|
2180
2180
|
|
|
2181
|
-
|
|
2181
|
+
**In the Haltija desktop app** the new tab gets the widget auto-injected, so it's immediately
|
|
2182
|
+
controllable. **Anywhere else** (widget injected into a normal browser via bookmarklet/dev-server)
|
|
2183
|
+
there is no tab API, so this falls back to \`window.open()\` \u2014 the new tab is a plain browser tab
|
|
2184
|
+
with NO widget unless its own page injects one. In that case the response has \`fallback: true\`
|
|
2185
|
+
plus a \`reason\`, and a top-level \`warning\`: the tab will NOT appear in \`hj tabs\` and hj commands
|
|
2186
|
+
cannot reach it (they go to the focused widget tab). To control it, inject the widget on that page
|
|
2187
|
+
(e.g. HALTIJA_DEV / haltijaDev) or use the desktop app.
|
|
2182
2188
|
|
|
2183
2189
|
**Parameters:**
|
|
2184
2190
|
|
|
@@ -2224,11 +2230,15 @@ Get window IDs from /windows endpoint.
|
|
|
2224
2230
|
|
|
2225
2231
|
### \`POST /tabs/focus\`
|
|
2226
2232
|
|
|
2227
|
-
**Focus a tab**
|
|
2233
|
+
**Focus a tab (route untargeted commands to it)**
|
|
2228
2234
|
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2235
|
+
Make the given tab the target of untargeted commands. This is a server-side
|
|
2236
|
+
routing change, NOT a browser action: it does not physically raise the tab (a backgrounded browser
|
|
2237
|
+
tab cannot be raised remotely), and because it never dispatches to the tab it can't time out \u2014 even
|
|
2238
|
+
if the tab is hidden. After this, commands without a \`window\`/\`--window\` go to this tab until you
|
|
2239
|
+
focus another or physically switch tabs in the browser. To pin a single command instead, use
|
|
2240
|
+
\`--window <id>\`. If the tab is hidden the response includes a warning (a backgrounded tab's results
|
|
2241
|
+
can be stale). Returns \`{ success, focused, active, title }\`.
|
|
2232
2242
|
|
|
2233
2243
|
**Parameters:**
|
|
2234
2244
|
|
|
@@ -2238,7 +2248,7 @@ Useful when working with multiple tabs to ensure the right one is visible.
|
|
|
2238
2248
|
|
|
2239
2249
|
**Examples:**
|
|
2240
2250
|
|
|
2241
|
-
- **focus**:
|
|
2251
|
+
- **focus**: Route untargeted commands to this tab
|
|
2242
2252
|
\`\`\`json
|
|
2243
2253
|
{"window":"window-abc123"}
|
|
2244
2254
|
\`\`\`
|
|
@@ -2802,7 +2812,7 @@ hj --help # All commands
|
|
|
2802
2812
|
- \`hj windows\` - List connected windows
|
|
2803
2813
|
- \`hj tabs-open [url]\` - Open a new tab
|
|
2804
2814
|
- \`hj tabs-close [window]\` - Close a tab
|
|
2805
|
-
- \`hj tabs-focus [window]\` - Focus a tab
|
|
2815
|
+
- \`hj tabs-focus [window]\` - Focus a tab (route untargeted commands to it)
|
|
2806
2816
|
|
|
2807
2817
|
### Record & Replay
|
|
2808
2818
|
|
|
@@ -3020,8 +3030,17 @@ Two first-party ways to run (plus embedding, below):
|
|
|
3020
3030
|
\`hj tree\` inside a project reaches that project's server with no flags. Falling
|
|
3021
3031
|
back to the shared default port 8700 warns on stderr. Use \`hj where\` to see which
|
|
3022
3032
|
port a shell targets and WHY; override with \`--port\` or \`--name\`.
|
|
3023
|
-
- **CI:**
|
|
3024
|
-
|
|
3033
|
+
- **CI:** two engines, and the choice matters \u2014 both need one external browser, but
|
|
3034
|
+
a *different* one:
|
|
3035
|
+
- \`haltija --ci\` (or \`--private --app\` for an isolated instance) drives **Electron**
|
|
3036
|
+
(Chromium \u2014 the same engine the Haltija desktop app uses). Electron is fetched via
|
|
3037
|
+
\`npx\` if not already present; **no \`playwright\` needed.** This is the default CI path.
|
|
3038
|
+
- \`haltija --headless\` drives **Playwright Chromium**, which requires the \`playwright\`
|
|
3039
|
+
package (\`npm i playwright && npx playwright install chromium\`). Reach for it when you
|
|
3040
|
+
want multi-engine coverage (Firefox/WebKit) or a lighter single-engine run \u2014 not just
|
|
3041
|
+
because it says "for CI". \`--private\` is an *isolation* modifier (ephemeral port), and
|
|
3042
|
+
pairs with either engine: \`--private --app\` (Electron) or \`--private --headless\`
|
|
3043
|
+
(Playwright). See the CI guide below.
|
|
3025
3044
|
|
|
3026
3045
|
## Embed in your app
|
|
3027
3046
|
|
|
@@ -3097,7 +3116,7 @@ var COMPONENT_JS = `(() => {
|
|
|
3097
3116
|
});
|
|
3098
3117
|
|
|
3099
3118
|
// src/version.ts
|
|
3100
|
-
var VERSION = "1.5.
|
|
3119
|
+
var VERSION = "1.5.4";
|
|
3101
3120
|
|
|
3102
3121
|
// src/text-selector.ts
|
|
3103
3122
|
var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\\(/;
|
|
@@ -8688,7 +8707,11 @@ var COMPONENT_JS = `(() => {
|
|
|
8688
8707
|
});
|
|
8689
8708
|
} else {
|
|
8690
8709
|
window.open(payload2.url, "_blank");
|
|
8691
|
-
this.respond(msg2.id, true, {
|
|
8710
|
+
this.respond(msg2.id, true, {
|
|
8711
|
+
opened: true,
|
|
8712
|
+
fallback: true,
|
|
8713
|
+
reason: "Opened a plain browser tab via window.open() (this is not the Haltija desktop app). " + "That tab has NO Haltija widget unless its page injects one, so it will NOT appear in " + "\`hj tabs\` and hj commands cannot reach it \u2014 untargeted commands go to the focused " + "widget tab instead. To control the new tab: inject the widget on that page (e.g. " + "HALTIJA_DEV=1 / haltijaDev:true in that project), or use the Haltija desktop app, " + "which auto-injects into every tab."
|
|
8714
|
+
});
|
|
8692
8715
|
}
|
|
8693
8716
|
} else if (action2 === "close") {
|
|
8694
8717
|
if (haltija?.closeTab) {
|
|
@@ -12814,9 +12837,15 @@ var tabsOpen = endpoint({
|
|
|
12814
12837
|
path: "/tabs/open",
|
|
12815
12838
|
method: "POST",
|
|
12816
12839
|
summary: "Open a new tab",
|
|
12817
|
-
description: `
|
|
12818
|
-
|
|
12819
|
-
|
|
12840
|
+
description: `Opens a new tab with optional URL. If url is omitted, opens a blank tab.
|
|
12841
|
+
|
|
12842
|
+
**In the Haltija desktop app** the new tab gets the widget auto-injected, so it's immediately
|
|
12843
|
+
controllable. **Anywhere else** (widget injected into a normal browser via bookmarklet/dev-server)
|
|
12844
|
+
there is no tab API, so this falls back to \`window.open()\` \u2014 the new tab is a plain browser tab
|
|
12845
|
+
with NO widget unless its own page injects one. In that case the response has \`fallback: true\`
|
|
12846
|
+
plus a \`reason\`, and a top-level \`warning\`: the tab will NOT appear in \`hj tabs\` and hj commands
|
|
12847
|
+
cannot reach it (they go to the focused widget tab). To control it, inject the widget on that page
|
|
12848
|
+
(e.g. HALTIJA_DEV / haltijaDev) or use the desktop app.`,
|
|
12820
12849
|
category: "windows",
|
|
12821
12850
|
input: L.object({
|
|
12822
12851
|
url: L.string.describe("URL to open").optional
|
|
@@ -12857,10 +12886,14 @@ Get window IDs from /windows endpoint.`,
|
|
|
12857
12886
|
var tabsFocus = endpoint({
|
|
12858
12887
|
path: "/tabs/focus",
|
|
12859
12888
|
method: "POST",
|
|
12860
|
-
summary: "Focus a tab",
|
|
12861
|
-
description: `
|
|
12862
|
-
|
|
12863
|
-
|
|
12889
|
+
summary: "Focus a tab (route untargeted commands to it)",
|
|
12890
|
+
description: `Make the given tab the target of untargeted commands. This is a server-side
|
|
12891
|
+
routing change, NOT a browser action: it does not physically raise the tab (a backgrounded browser
|
|
12892
|
+
tab cannot be raised remotely), and because it never dispatches to the tab it can't time out \u2014 even
|
|
12893
|
+
if the tab is hidden. After this, commands without a \`window\`/\`--window\` go to this tab until you
|
|
12894
|
+
focus another or physically switch tabs in the browser. To pin a single command instead, use
|
|
12895
|
+
\`--window <id>\`. If the tab is hidden the response includes a warning (a backgrounded tab's results
|
|
12896
|
+
can be stale). Returns \`{ success, focused, active, title }\`.`,
|
|
12864
12897
|
category: "windows",
|
|
12865
12898
|
input: L.object({
|
|
12866
12899
|
window: L.string.describe("Window ID to focus")
|
|
@@ -12869,7 +12902,7 @@ Useful when working with multiple tabs to ensure the right one is visible.`,
|
|
|
12869
12902
|
{
|
|
12870
12903
|
name: "focus",
|
|
12871
12904
|
input: { window: "window-abc123" },
|
|
12872
|
-
description: "
|
|
12905
|
+
description: "Route untargeted commands to this tab"
|
|
12873
12906
|
}
|
|
12874
12907
|
],
|
|
12875
12908
|
hints: "<window-id> | see: windows, tabs-close, tabs-open"
|
|
@@ -14239,6 +14272,9 @@ registerHandler(screenshot, async (body, ctx) => {
|
|
|
14239
14272
|
});
|
|
14240
14273
|
registerHandler(tabsOpen, async (body, ctx) => {
|
|
14241
14274
|
const response = await ctx.requestFromBrowser("tabs", "open", { url: body.url });
|
|
14275
|
+
if (response?.data?.fallback && response.data.reason && !response.warning) {
|
|
14276
|
+
response.warning = response.data.reason;
|
|
14277
|
+
}
|
|
14242
14278
|
return Response.json(response, { headers: ctx.headers });
|
|
14243
14279
|
});
|
|
14244
14280
|
registerHandler(tabsClose, async (body, ctx) => {
|
|
@@ -14252,10 +14288,19 @@ registerHandler(tabsClose, async (body, ctx) => {
|
|
|
14252
14288
|
registerHandler(tabsFocus, async (body, ctx) => {
|
|
14253
14289
|
const windowId = body.window || ctx.targetWindowId;
|
|
14254
14290
|
if (!windowId) {
|
|
14255
|
-
return Response.json({ success: false, error: "window id is required" }, { status: 400, headers: ctx.headers });
|
|
14291
|
+
return Response.json({ success: false, error: "window id is required (run `hj tabs` to list connected tabs)" }, { status: 400, headers: ctx.headers });
|
|
14256
14292
|
}
|
|
14257
|
-
const
|
|
14258
|
-
|
|
14293
|
+
const r = ctx.focusWindow(windowId);
|
|
14294
|
+
if (!r.ok) {
|
|
14295
|
+
return Response.json({ success: false, error: r.error }, { status: 404, headers: ctx.headers });
|
|
14296
|
+
}
|
|
14297
|
+
const result = { success: true, focused: windowId, active: r.active !== false };
|
|
14298
|
+
if (r.title)
|
|
14299
|
+
result.title = r.title;
|
|
14300
|
+
if (r.active === false) {
|
|
14301
|
+
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.';
|
|
14302
|
+
}
|
|
14303
|
+
return Response.json(result, { headers: ctx.headers });
|
|
14259
14304
|
});
|
|
14260
14305
|
registerHandler(mutationsWatch, async (body, ctx) => {
|
|
14261
14306
|
const response = await ctx.requestFromBrowser("mutations", "watch", {
|
|
@@ -15275,6 +15320,22 @@ function ambiguousFocusWarning(opts) {
|
|
|
15275
15320
|
${pins}${more}`;
|
|
15276
15321
|
}
|
|
15277
15322
|
|
|
15323
|
+
// src/warning-dedupe.ts
|
|
15324
|
+
function shouldEmitWarning(warning, cache, now, cooldownMs) {
|
|
15325
|
+
const last = cache.get(warning);
|
|
15326
|
+
if (last !== undefined && now - last < cooldownMs) {
|
|
15327
|
+
return false;
|
|
15328
|
+
}
|
|
15329
|
+
cache.set(warning, now);
|
|
15330
|
+
if (cache.size > 64) {
|
|
15331
|
+
for (const [key2, ts] of cache) {
|
|
15332
|
+
if (now - ts >= cooldownMs)
|
|
15333
|
+
cache.delete(key2);
|
|
15334
|
+
}
|
|
15335
|
+
}
|
|
15336
|
+
return true;
|
|
15337
|
+
}
|
|
15338
|
+
|
|
15278
15339
|
// src/server.ts
|
|
15279
15340
|
init_terminal();
|
|
15280
15341
|
|
|
@@ -16129,6 +16190,9 @@ function getComponentJs() {
|
|
|
16129
16190
|
}
|
|
16130
16191
|
var browsers = new Map;
|
|
16131
16192
|
var windows2 = new Map;
|
|
16193
|
+
var recentTabWarnings = new Map;
|
|
16194
|
+
var TAB_WARN_COOLDOWN_MS = 15000;
|
|
16195
|
+
var TAB_WARN_DISABLED = process.env.HALTIJA_NO_TAB_WARN === "1";
|
|
16132
16196
|
var agents = new Set;
|
|
16133
16197
|
var focusedWindowId = null;
|
|
16134
16198
|
var isDesktopApp = process.env.HALTIJA_DESKTOP === "1";
|
|
@@ -16443,6 +16507,8 @@ async function requestFromBrowser(channel, action, payload, timeoutMs = 5000, wi
|
|
|
16443
16507
|
return new Promise((resolve) => {
|
|
16444
16508
|
let sentTo = null;
|
|
16445
16509
|
const attachWarning = (res) => {
|
|
16510
|
+
if (TAB_WARN_DISABLED)
|
|
16511
|
+
return res;
|
|
16446
16512
|
const hidden = hiddenTabWarning(sentTo);
|
|
16447
16513
|
const ambiguous = ambiguousFocusWarning({
|
|
16448
16514
|
windows: Array.from(windows2.values()),
|
|
@@ -16452,7 +16518,12 @@ async function requestFromBrowser(channel, action, payload, timeoutMs = 5000, wi
|
|
|
16452
16518
|
const warning = [hidden, ambiguous].filter(Boolean).join(`
|
|
16453
16519
|
|
|
16454
16520
|
`);
|
|
16455
|
-
|
|
16521
|
+
if (!warning)
|
|
16522
|
+
return res;
|
|
16523
|
+
if (!shouldEmitWarning(warning, recentTabWarnings, Date.now(), TAB_WARN_COOLDOWN_MS)) {
|
|
16524
|
+
return res;
|
|
16525
|
+
}
|
|
16526
|
+
return { ...res, warning };
|
|
16456
16527
|
};
|
|
16457
16528
|
const timeout = setTimeout(() => {
|
|
16458
16529
|
pendingResponses.delete(id);
|
|
@@ -16623,6 +16694,15 @@ var createHandlerContext = (req, url) => {
|
|
|
16623
16694
|
headers,
|
|
16624
16695
|
url,
|
|
16625
16696
|
getWindowInfo,
|
|
16697
|
+
focusWindow: (windowId) => {
|
|
16698
|
+
const win = windows2.get(windowId);
|
|
16699
|
+
if (!win) {
|
|
16700
|
+
return { ok: false, error: `Tab ${windowId} not found (run \`hj tabs\` to list connected tabs)` };
|
|
16701
|
+
}
|
|
16702
|
+
focusedWindowId = windowId;
|
|
16703
|
+
updateHjStatus();
|
|
16704
|
+
return { ok: true, active: win.active, windowType: win.windowType, title: win.title };
|
|
16705
|
+
},
|
|
16626
16706
|
startRecordingSession,
|
|
16627
16707
|
stopRecordingSession,
|
|
16628
16708
|
getRecordingSession,
|
|
@@ -18300,12 +18380,10 @@ Type 'help <topic>' for details.`);
|
|
|
18300
18380
|
break;
|
|
18301
18381
|
}
|
|
18302
18382
|
case "tabs-focus": {
|
|
18303
|
-
|
|
18304
|
-
if (!resp.success) {
|
|
18383
|
+
if (!step.window || !windows2.has(step.window)) {
|
|
18305
18384
|
stepPassed = false;
|
|
18306
|
-
error =
|
|
18307
|
-
}
|
|
18308
|
-
if (step.window && windows2.has(step.window)) {
|
|
18385
|
+
error = `Tab ${step.window ?? "(none)"} not found`;
|
|
18386
|
+
} else {
|
|
18309
18387
|
focusedWindowId = step.window;
|
|
18310
18388
|
}
|
|
18311
18389
|
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.4";
|
|
678
678
|
|
|
679
679
|
// src/embedded-assets.ts
|
|
680
680
|
var APP_MD = `# Haltija App
|
|
@@ -2176,9 +2176,15 @@ Use window IDs in other endpoints (e.g., /click, /tree) to target specific tabs.
|
|
|
2176
2176
|
|
|
2177
2177
|
**Open a new tab**
|
|
2178
2178
|
|
|
2179
|
-
|
|
2179
|
+
Opens a new tab with optional URL. If url is omitted, opens a blank tab.
|
|
2180
2180
|
|
|
2181
|
-
|
|
2181
|
+
**In the Haltija desktop app** the new tab gets the widget auto-injected, so it's immediately
|
|
2182
|
+
controllable. **Anywhere else** (widget injected into a normal browser via bookmarklet/dev-server)
|
|
2183
|
+
there is no tab API, so this falls back to \`window.open()\` \u2014 the new tab is a plain browser tab
|
|
2184
|
+
with NO widget unless its own page injects one. In that case the response has \`fallback: true\`
|
|
2185
|
+
plus a \`reason\`, and a top-level \`warning\`: the tab will NOT appear in \`hj tabs\` and hj commands
|
|
2186
|
+
cannot reach it (they go to the focused widget tab). To control it, inject the widget on that page
|
|
2187
|
+
(e.g. HALTIJA_DEV / haltijaDev) or use the desktop app.
|
|
2182
2188
|
|
|
2183
2189
|
**Parameters:**
|
|
2184
2190
|
|
|
@@ -2224,11 +2230,15 @@ Get window IDs from /windows endpoint.
|
|
|
2224
2230
|
|
|
2225
2231
|
### \`POST /tabs/focus\`
|
|
2226
2232
|
|
|
2227
|
-
**Focus a tab**
|
|
2233
|
+
**Focus a tab (route untargeted commands to it)**
|
|
2228
2234
|
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2235
|
+
Make the given tab the target of untargeted commands. This is a server-side
|
|
2236
|
+
routing change, NOT a browser action: it does not physically raise the tab (a backgrounded browser
|
|
2237
|
+
tab cannot be raised remotely), and because it never dispatches to the tab it can't time out \u2014 even
|
|
2238
|
+
if the tab is hidden. After this, commands without a \`window\`/\`--window\` go to this tab until you
|
|
2239
|
+
focus another or physically switch tabs in the browser. To pin a single command instead, use
|
|
2240
|
+
\`--window <id>\`. If the tab is hidden the response includes a warning (a backgrounded tab's results
|
|
2241
|
+
can be stale). Returns \`{ success, focused, active, title }\`.
|
|
2232
2242
|
|
|
2233
2243
|
**Parameters:**
|
|
2234
2244
|
|
|
@@ -2238,7 +2248,7 @@ Useful when working with multiple tabs to ensure the right one is visible.
|
|
|
2238
2248
|
|
|
2239
2249
|
**Examples:**
|
|
2240
2250
|
|
|
2241
|
-
- **focus**:
|
|
2251
|
+
- **focus**: Route untargeted commands to this tab
|
|
2242
2252
|
\`\`\`json
|
|
2243
2253
|
{"window":"window-abc123"}
|
|
2244
2254
|
\`\`\`
|
|
@@ -2802,7 +2812,7 @@ hj --help # All commands
|
|
|
2802
2812
|
- \`hj windows\` - List connected windows
|
|
2803
2813
|
- \`hj tabs-open [url]\` - Open a new tab
|
|
2804
2814
|
- \`hj tabs-close [window]\` - Close a tab
|
|
2805
|
-
- \`hj tabs-focus [window]\` - Focus a tab
|
|
2815
|
+
- \`hj tabs-focus [window]\` - Focus a tab (route untargeted commands to it)
|
|
2806
2816
|
|
|
2807
2817
|
### Record & Replay
|
|
2808
2818
|
|
|
@@ -3020,8 +3030,17 @@ Two first-party ways to run (plus embedding, below):
|
|
|
3020
3030
|
\`hj tree\` inside a project reaches that project's server with no flags. Falling
|
|
3021
3031
|
back to the shared default port 8700 warns on stderr. Use \`hj where\` to see which
|
|
3022
3032
|
port a shell targets and WHY; override with \`--port\` or \`--name\`.
|
|
3023
|
-
- **CI:**
|
|
3024
|
-
|
|
3033
|
+
- **CI:** two engines, and the choice matters \u2014 both need one external browser, but
|
|
3034
|
+
a *different* one:
|
|
3035
|
+
- \`haltija --ci\` (or \`--private --app\` for an isolated instance) drives **Electron**
|
|
3036
|
+
(Chromium \u2014 the same engine the Haltija desktop app uses). Electron is fetched via
|
|
3037
|
+
\`npx\` if not already present; **no \`playwright\` needed.** This is the default CI path.
|
|
3038
|
+
- \`haltija --headless\` drives **Playwright Chromium**, which requires the \`playwright\`
|
|
3039
|
+
package (\`npm i playwright && npx playwright install chromium\`). Reach for it when you
|
|
3040
|
+
want multi-engine coverage (Firefox/WebKit) or a lighter single-engine run \u2014 not just
|
|
3041
|
+
because it says "for CI". \`--private\` is an *isolation* modifier (ephemeral port), and
|
|
3042
|
+
pairs with either engine: \`--private --app\` (Electron) or \`--private --headless\`
|
|
3043
|
+
(Playwright). See the CI guide below.
|
|
3025
3044
|
|
|
3026
3045
|
## Embed in your app
|
|
3027
3046
|
|
|
@@ -3097,7 +3116,7 @@ var COMPONENT_JS = `(() => {
|
|
|
3097
3116
|
});
|
|
3098
3117
|
|
|
3099
3118
|
// src/version.ts
|
|
3100
|
-
var VERSION = "1.5.
|
|
3119
|
+
var VERSION = "1.5.4";
|
|
3101
3120
|
|
|
3102
3121
|
// src/text-selector.ts
|
|
3103
3122
|
var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\\(/;
|
|
@@ -8688,7 +8707,11 @@ var COMPONENT_JS = `(() => {
|
|
|
8688
8707
|
});
|
|
8689
8708
|
} else {
|
|
8690
8709
|
window.open(payload2.url, "_blank");
|
|
8691
|
-
this.respond(msg2.id, true, {
|
|
8710
|
+
this.respond(msg2.id, true, {
|
|
8711
|
+
opened: true,
|
|
8712
|
+
fallback: true,
|
|
8713
|
+
reason: "Opened a plain browser tab via window.open() (this is not the Haltija desktop app). " + "That tab has NO Haltija widget unless its page injects one, so it will NOT appear in " + "\`hj tabs\` and hj commands cannot reach it \u2014 untargeted commands go to the focused " + "widget tab instead. To control the new tab: inject the widget on that page (e.g. " + "HALTIJA_DEV=1 / haltijaDev:true in that project), or use the Haltija desktop app, " + "which auto-injects into every tab."
|
|
8714
|
+
});
|
|
8692
8715
|
}
|
|
8693
8716
|
} else if (action2 === "close") {
|
|
8694
8717
|
if (haltija?.closeTab) {
|
|
@@ -12814,9 +12837,15 @@ var tabsOpen = endpoint({
|
|
|
12814
12837
|
path: "/tabs/open",
|
|
12815
12838
|
method: "POST",
|
|
12816
12839
|
summary: "Open a new tab",
|
|
12817
|
-
description: `
|
|
12818
|
-
|
|
12819
|
-
|
|
12840
|
+
description: `Opens a new tab with optional URL. If url is omitted, opens a blank tab.
|
|
12841
|
+
|
|
12842
|
+
**In the Haltija desktop app** the new tab gets the widget auto-injected, so it's immediately
|
|
12843
|
+
controllable. **Anywhere else** (widget injected into a normal browser via bookmarklet/dev-server)
|
|
12844
|
+
there is no tab API, so this falls back to \`window.open()\` \u2014 the new tab is a plain browser tab
|
|
12845
|
+
with NO widget unless its own page injects one. In that case the response has \`fallback: true\`
|
|
12846
|
+
plus a \`reason\`, and a top-level \`warning\`: the tab will NOT appear in \`hj tabs\` and hj commands
|
|
12847
|
+
cannot reach it (they go to the focused widget tab). To control it, inject the widget on that page
|
|
12848
|
+
(e.g. HALTIJA_DEV / haltijaDev) or use the desktop app.`,
|
|
12820
12849
|
category: "windows",
|
|
12821
12850
|
input: L.object({
|
|
12822
12851
|
url: L.string.describe("URL to open").optional
|
|
@@ -12857,10 +12886,14 @@ Get window IDs from /windows endpoint.`,
|
|
|
12857
12886
|
var tabsFocus = endpoint({
|
|
12858
12887
|
path: "/tabs/focus",
|
|
12859
12888
|
method: "POST",
|
|
12860
|
-
summary: "Focus a tab",
|
|
12861
|
-
description: `
|
|
12862
|
-
|
|
12863
|
-
|
|
12889
|
+
summary: "Focus a tab (route untargeted commands to it)",
|
|
12890
|
+
description: `Make the given tab the target of untargeted commands. This is a server-side
|
|
12891
|
+
routing change, NOT a browser action: it does not physically raise the tab (a backgrounded browser
|
|
12892
|
+
tab cannot be raised remotely), and because it never dispatches to the tab it can't time out \u2014 even
|
|
12893
|
+
if the tab is hidden. After this, commands without a \`window\`/\`--window\` go to this tab until you
|
|
12894
|
+
focus another or physically switch tabs in the browser. To pin a single command instead, use
|
|
12895
|
+
\`--window <id>\`. If the tab is hidden the response includes a warning (a backgrounded tab's results
|
|
12896
|
+
can be stale). Returns \`{ success, focused, active, title }\`.`,
|
|
12864
12897
|
category: "windows",
|
|
12865
12898
|
input: L.object({
|
|
12866
12899
|
window: L.string.describe("Window ID to focus")
|
|
@@ -12869,7 +12902,7 @@ Useful when working with multiple tabs to ensure the right one is visible.`,
|
|
|
12869
12902
|
{
|
|
12870
12903
|
name: "focus",
|
|
12871
12904
|
input: { window: "window-abc123" },
|
|
12872
|
-
description: "
|
|
12905
|
+
description: "Route untargeted commands to this tab"
|
|
12873
12906
|
}
|
|
12874
12907
|
],
|
|
12875
12908
|
hints: "<window-id> | see: windows, tabs-close, tabs-open"
|
|
@@ -14239,6 +14272,9 @@ registerHandler(screenshot, async (body, ctx) => {
|
|
|
14239
14272
|
});
|
|
14240
14273
|
registerHandler(tabsOpen, async (body, ctx) => {
|
|
14241
14274
|
const response = await ctx.requestFromBrowser("tabs", "open", { url: body.url });
|
|
14275
|
+
if (response?.data?.fallback && response.data.reason && !response.warning) {
|
|
14276
|
+
response.warning = response.data.reason;
|
|
14277
|
+
}
|
|
14242
14278
|
return Response.json(response, { headers: ctx.headers });
|
|
14243
14279
|
});
|
|
14244
14280
|
registerHandler(tabsClose, async (body, ctx) => {
|
|
@@ -14252,10 +14288,19 @@ registerHandler(tabsClose, async (body, ctx) => {
|
|
|
14252
14288
|
registerHandler(tabsFocus, async (body, ctx) => {
|
|
14253
14289
|
const windowId = body.window || ctx.targetWindowId;
|
|
14254
14290
|
if (!windowId) {
|
|
14255
|
-
return Response.json({ success: false, error: "window id is required" }, { status: 400, headers: ctx.headers });
|
|
14291
|
+
return Response.json({ success: false, error: "window id is required (run `hj tabs` to list connected tabs)" }, { status: 400, headers: ctx.headers });
|
|
14256
14292
|
}
|
|
14257
|
-
const
|
|
14258
|
-
|
|
14293
|
+
const r = ctx.focusWindow(windowId);
|
|
14294
|
+
if (!r.ok) {
|
|
14295
|
+
return Response.json({ success: false, error: r.error }, { status: 404, headers: ctx.headers });
|
|
14296
|
+
}
|
|
14297
|
+
const result = { success: true, focused: windowId, active: r.active !== false };
|
|
14298
|
+
if (r.title)
|
|
14299
|
+
result.title = r.title;
|
|
14300
|
+
if (r.active === false) {
|
|
14301
|
+
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.';
|
|
14302
|
+
}
|
|
14303
|
+
return Response.json(result, { headers: ctx.headers });
|
|
14259
14304
|
});
|
|
14260
14305
|
registerHandler(mutationsWatch, async (body, ctx) => {
|
|
14261
14306
|
const response = await ctx.requestFromBrowser("mutations", "watch", {
|
|
@@ -15275,6 +15320,22 @@ function ambiguousFocusWarning(opts) {
|
|
|
15275
15320
|
${pins}${more}`;
|
|
15276
15321
|
}
|
|
15277
15322
|
|
|
15323
|
+
// src/warning-dedupe.ts
|
|
15324
|
+
function shouldEmitWarning(warning, cache, now, cooldownMs) {
|
|
15325
|
+
const last = cache.get(warning);
|
|
15326
|
+
if (last !== undefined && now - last < cooldownMs) {
|
|
15327
|
+
return false;
|
|
15328
|
+
}
|
|
15329
|
+
cache.set(warning, now);
|
|
15330
|
+
if (cache.size > 64) {
|
|
15331
|
+
for (const [key2, ts] of cache) {
|
|
15332
|
+
if (now - ts >= cooldownMs)
|
|
15333
|
+
cache.delete(key2);
|
|
15334
|
+
}
|
|
15335
|
+
}
|
|
15336
|
+
return true;
|
|
15337
|
+
}
|
|
15338
|
+
|
|
15278
15339
|
// src/server.ts
|
|
15279
15340
|
init_terminal();
|
|
15280
15341
|
|
|
@@ -16129,6 +16190,9 @@ function getComponentJs() {
|
|
|
16129
16190
|
}
|
|
16130
16191
|
var browsers = new Map;
|
|
16131
16192
|
var windows2 = new Map;
|
|
16193
|
+
var recentTabWarnings = new Map;
|
|
16194
|
+
var TAB_WARN_COOLDOWN_MS = 15000;
|
|
16195
|
+
var TAB_WARN_DISABLED = process.env.HALTIJA_NO_TAB_WARN === "1";
|
|
16132
16196
|
var agents = new Set;
|
|
16133
16197
|
var focusedWindowId = null;
|
|
16134
16198
|
var isDesktopApp = process.env.HALTIJA_DESKTOP === "1";
|
|
@@ -16443,6 +16507,8 @@ async function requestFromBrowser(channel, action, payload, timeoutMs = 5000, wi
|
|
|
16443
16507
|
return new Promise((resolve) => {
|
|
16444
16508
|
let sentTo = null;
|
|
16445
16509
|
const attachWarning = (res) => {
|
|
16510
|
+
if (TAB_WARN_DISABLED)
|
|
16511
|
+
return res;
|
|
16446
16512
|
const hidden = hiddenTabWarning(sentTo);
|
|
16447
16513
|
const ambiguous = ambiguousFocusWarning({
|
|
16448
16514
|
windows: Array.from(windows2.values()),
|
|
@@ -16452,7 +16518,12 @@ async function requestFromBrowser(channel, action, payload, timeoutMs = 5000, wi
|
|
|
16452
16518
|
const warning = [hidden, ambiguous].filter(Boolean).join(`
|
|
16453
16519
|
|
|
16454
16520
|
`);
|
|
16455
|
-
|
|
16521
|
+
if (!warning)
|
|
16522
|
+
return res;
|
|
16523
|
+
if (!shouldEmitWarning(warning, recentTabWarnings, Date.now(), TAB_WARN_COOLDOWN_MS)) {
|
|
16524
|
+
return res;
|
|
16525
|
+
}
|
|
16526
|
+
return { ...res, warning };
|
|
16456
16527
|
};
|
|
16457
16528
|
const timeout = setTimeout(() => {
|
|
16458
16529
|
pendingResponses.delete(id);
|
|
@@ -16623,6 +16694,15 @@ var createHandlerContext = (req, url) => {
|
|
|
16623
16694
|
headers,
|
|
16624
16695
|
url,
|
|
16625
16696
|
getWindowInfo,
|
|
16697
|
+
focusWindow: (windowId) => {
|
|
16698
|
+
const win = windows2.get(windowId);
|
|
16699
|
+
if (!win) {
|
|
16700
|
+
return { ok: false, error: `Tab ${windowId} not found (run \`hj tabs\` to list connected tabs)` };
|
|
16701
|
+
}
|
|
16702
|
+
focusedWindowId = windowId;
|
|
16703
|
+
updateHjStatus();
|
|
16704
|
+
return { ok: true, active: win.active, windowType: win.windowType, title: win.title };
|
|
16705
|
+
},
|
|
16626
16706
|
startRecordingSession,
|
|
16627
16707
|
stopRecordingSession,
|
|
16628
16708
|
getRecordingSession,
|
|
@@ -18300,12 +18380,10 @@ Type 'help <topic>' for details.`);
|
|
|
18300
18380
|
break;
|
|
18301
18381
|
}
|
|
18302
18382
|
case "tabs-focus": {
|
|
18303
|
-
|
|
18304
|
-
if (!resp.success) {
|
|
18383
|
+
if (!step.window || !windows2.has(step.window)) {
|
|
18305
18384
|
stepPassed = false;
|
|
18306
|
-
error =
|
|
18307
|
-
}
|
|
18308
|
-
if (step.window && windows2.has(step.window)) {
|
|
18385
|
+
error = `Tab ${step.window ?? "(none)"} not found`;
|
|
18386
|
+
} else {
|
|
18309
18387
|
focusedWindowId = step.window;
|
|
18310
18388
|
}
|
|
18311
18389
|
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;
|
package/docs/CI-INTEGRATION.md
CHANGED
|
@@ -95,6 +95,24 @@ run: |
|
|
|
95
95
|
|
|
96
96
|
## Launch Options
|
|
97
97
|
|
|
98
|
+
### Which engine? (read this first)
|
|
99
|
+
|
|
100
|
+
Haltija can drive one of two browser engines in CI, and they need **different** external browsers —
|
|
101
|
+
picking by the words "for CI" alone leads people to the wrong one:
|
|
102
|
+
|
|
103
|
+
| You want | Use | Engine | External dependency |
|
|
104
|
+
| --- | --- | --- | --- |
|
|
105
|
+
| The default CI path | `--ci` | **Electron** (Chromium) | Electron (auto-fetched via `npx`) — **no Playwright** |
|
|
106
|
+
| Isolated CI instance, own ephemeral port | `--private --app` | **Electron** (Chromium) | Electron (as above) |
|
|
107
|
+
| Multi-engine coverage (Firefox/WebKit) or a lighter single-engine run | `--headless` | **Playwright** Chromium | the `playwright` package (`npm i playwright && npx playwright install`) |
|
|
108
|
+
|
|
109
|
+
Neither engine is bundled in the npm package, so *something* is downloaded either way — the choice
|
|
110
|
+
is **Electron vs. Playwright**, not "deps vs. no deps". The one genuine reason to reach for
|
|
111
|
+
`--headless`/Playwright is engines Electron can't give you (Firefox, WebKit). For a straight
|
|
112
|
+
Chromium CI lane, `--ci` (Electron) needs no separate `playwright` install. `--private` is an
|
|
113
|
+
*isolation* modifier — it pairs with either engine (`--private --app` = Electron, `--private
|
|
114
|
+
--headless` = Playwright).
|
|
115
|
+
|
|
98
116
|
### Option 1: CI mode (recommended)
|
|
99
117
|
|
|
100
118
|
```bash
|
|
@@ -115,13 +133,18 @@ xvfb-run --auto-servernum bunx haltija@latest --app &
|
|
|
115
133
|
|
|
116
134
|
Same as `--ci` but doesn't wait for ready state. You'll need to poll `/status` yourself.
|
|
117
135
|
|
|
118
|
-
### Option 3: Playwright headless (
|
|
136
|
+
### Option 3: Playwright headless (multi-engine)
|
|
119
137
|
|
|
120
138
|
```bash
|
|
139
|
+
npm install playwright && npx playwright install chromium # required — not bundled
|
|
121
140
|
bunx haltija@latest --headless &
|
|
122
141
|
```
|
|
123
142
|
|
|
124
|
-
Uses Playwright's Chromium instead of Electron
|
|
143
|
+
Uses **Playwright's** Chromium instead of Electron, so it needs the `playwright` package installed
|
|
144
|
+
(the command above; without it, `--headless` exits with a "Playwright not installed" error that
|
|
145
|
+
points you back to `--ci`). Choose this when you want a lighter single-engine run or, the real
|
|
146
|
+
reason, **multi-engine coverage** — Playwright can also drive Firefox and WebKit, which Electron
|
|
147
|
+
cannot. For a plain Chromium lane with no extra install, prefer Option 1 (`--ci`).
|
|
125
148
|
|
|
126
149
|
### Option 4: Server-only mode (lighter weight)
|
|
127
150
|
|
package/llms.txt
CHANGED
|
@@ -40,8 +40,17 @@ Two first-party ways to run (plus embedding, below):
|
|
|
40
40
|
`hj tree` inside a project reaches that project's server with no flags. Falling
|
|
41
41
|
back to the shared default port 8700 warns on stderr. Use `hj where` to see which
|
|
42
42
|
port a shell targets and WHY; override with `--port` or `--name`.
|
|
43
|
-
- **CI:**
|
|
44
|
-
|
|
43
|
+
- **CI:** two engines, and the choice matters — both need one external browser, but
|
|
44
|
+
a *different* one:
|
|
45
|
+
- `haltija --ci` (or `--private --app` for an isolated instance) drives **Electron**
|
|
46
|
+
(Chromium — the same engine the Haltija desktop app uses). Electron is fetched via
|
|
47
|
+
`npx` if not already present; **no `playwright` needed.** This is the default CI path.
|
|
48
|
+
- `haltija --headless` drives **Playwright Chromium**, which requires the `playwright`
|
|
49
|
+
package (`npm i playwright && npx playwright install chromium`). Reach for it when you
|
|
50
|
+
want multi-engine coverage (Firefox/WebKit) or a lighter single-engine run — not just
|
|
51
|
+
because it says "for CI". `--private` is an *isolation* modifier (ephemeral port), and
|
|
52
|
+
pairs with either engine: `--private --app` (Electron) or `--private --headless`
|
|
53
|
+
(Playwright). See the CI guide below.
|
|
45
54
|
|
|
46
55
|
## Embed in your app
|
|
47
56
|
|