haltija 1.5.2 → 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 CHANGED
@@ -1,5 +1,37 @@
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
+
3
35
  ## 1.5.2
4
36
 
5
37
  Two follow-ups from the 1.5.0 review, both about the multi-tab experience on a shared server.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "haltija-desktop",
3
- "version": "1.5.2",
3
+ "version": "1.5.4",
4
4
  "private": true,
5
5
  "description": "Haltija Desktop - God Mode Browser for AI Agents",
6
6
  "homepage": "https://github.com/tonioloewald/haltija",
@@ -46,7 +46,7 @@
46
46
  });
47
47
 
48
48
  // src/version.ts
49
- var VERSION = "1.5.2";
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, { opened: true, fallback: 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) {
@@ -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 (for CI, headless, or bookmarklet usage)
44
- --headless Start with headless Chromium browser (for CI)
45
- --ci CI mode: Electron app + wait for ready + sandbox disabled
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. Pair with --headless.
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
@@ -106,13 +108,23 @@ Subcommands:
106
108
 
107
109
  Use 'hj' as a short alias: hj tree, hj click @42, etc.
108
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
+
109
121
  Examples:
110
122
  haltija # Desktop app (or server fallback)
111
123
  haltija --app # Desktop app explicitly
112
- haltija --server # Server only
124
+ haltija --server # Server only (bring your own browser)
113
125
  haltija --server --https # HTTPS server only
114
- haltija --headless # Headless browser for CI
115
- haltija --ci # CI mode (headless + wait + sandbox disabled)
126
+ haltija --ci # CI: Electron (no Playwright) + wait + sandbox off
127
+ haltija --headless # CI: Playwright Chromium (needs playwright)
116
128
  haltija --setup-mcp # Configure Claude Desktop integration
117
129
  `)
118
130
  process.exit(0)
@@ -729,8 +741,12 @@ const startHeadlessBrowser = async (port) => {
729
741
 
730
742
  } catch (err) {
731
743
  if (err.code === 'ERR_MODULE_NOT_FOUND') {
732
- console.error('[tosijs-dev] Playwright not installed. Run: npm install playwright')
733
- console.error('[tosijs-dev] Then: npx playwright install chromium')
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.')
734
750
  } else {
735
751
  console.error('[tosijs-dev] Failed to start headless browser:', err.message)
736
752
  }
package/bin/version.mjs CHANGED
@@ -3,4 +3,4 @@
3
3
  * ⚠️ To change the version, update package.json and run: bun run build
4
4
  */
5
5
 
6
- export const HJ_VERSION = '1.5.2'
6
+ export const HJ_VERSION = '1.5.4'
@@ -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.2";
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;
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var VERSION = "1.5.2";
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, { opened: true, fallback: 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.2";
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, { opened: true, fallback: 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
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.2";
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.2";
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
- Desktop app only. Opens a new tab with optional URL.
2179
+ Opens a new tab with optional URL. If url is omitted, opens a blank tab.
2180
2180
 
2181
- If url is omitted, opens a blank tab. The new tab gets the widget auto-injected.
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
 
@@ -3024,8 +3030,17 @@ Two first-party ways to run (plus embedding, below):
3024
3030
  \`hj tree\` inside a project reaches that project's server with no flags. Falling
3025
3031
  back to the shared default port 8700 warns on stderr. Use \`hj where\` to see which
3026
3032
  port a shell targets and WHY; override with \`--port\` or \`--name\`.
3027
- - **CI:** \`haltija --headless\` (or \`--ci\`) runs a headless Chromium with the widget
3028
- auto-injected \u2014 deterministic, no desktop app required. See the CI guide below.
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.
3029
3044
 
3030
3045
  ## Embed in your app
3031
3046
 
@@ -3101,7 +3116,7 @@ var COMPONENT_JS = `(() => {
3101
3116
  });
3102
3117
 
3103
3118
  // src/version.ts
3104
- var VERSION = "1.5.2";
3119
+ var VERSION = "1.5.4";
3105
3120
 
3106
3121
  // src/text-selector.ts
3107
3122
  var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\\(/;
@@ -8692,7 +8707,11 @@ var COMPONENT_JS = `(() => {
8692
8707
  });
8693
8708
  } else {
8694
8709
  window.open(payload2.url, "_blank");
8695
- this.respond(msg2.id, true, { opened: true, fallback: 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
+ });
8696
8715
  }
8697
8716
  } else if (action2 === "close") {
8698
8717
  if (haltija?.closeTab) {
@@ -12818,9 +12837,15 @@ var tabsOpen = endpoint({
12818
12837
  path: "/tabs/open",
12819
12838
  method: "POST",
12820
12839
  summary: "Open a new tab",
12821
- description: `Desktop app only. Opens a new tab with optional URL.
12822
-
12823
- If url is omitted, opens a blank tab. The new tab gets the widget auto-injected.`,
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.`,
12824
12849
  category: "windows",
12825
12850
  input: L.object({
12826
12851
  url: L.string.describe("URL to open").optional
@@ -14247,6 +14272,9 @@ registerHandler(screenshot, async (body, ctx) => {
14247
14272
  });
14248
14273
  registerHandler(tabsOpen, async (body, ctx) => {
14249
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
+ }
14250
14278
  return Response.json(response, { headers: ctx.headers });
14251
14279
  });
14252
14280
  registerHandler(tabsClose, async (body, ctx) => {
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.2";
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
- Desktop app only. Opens a new tab with optional URL.
2179
+ Opens a new tab with optional URL. If url is omitted, opens a blank tab.
2180
2180
 
2181
- If url is omitted, opens a blank tab. The new tab gets the widget auto-injected.
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
 
@@ -3024,8 +3030,17 @@ Two first-party ways to run (plus embedding, below):
3024
3030
  \`hj tree\` inside a project reaches that project's server with no flags. Falling
3025
3031
  back to the shared default port 8700 warns on stderr. Use \`hj where\` to see which
3026
3032
  port a shell targets and WHY; override with \`--port\` or \`--name\`.
3027
- - **CI:** \`haltija --headless\` (or \`--ci\`) runs a headless Chromium with the widget
3028
- auto-injected \u2014 deterministic, no desktop app required. See the CI guide below.
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.
3029
3044
 
3030
3045
  ## Embed in your app
3031
3046
 
@@ -3101,7 +3116,7 @@ var COMPONENT_JS = `(() => {
3101
3116
  });
3102
3117
 
3103
3118
  // src/version.ts
3104
- var VERSION = "1.5.2";
3119
+ var VERSION = "1.5.4";
3105
3120
 
3106
3121
  // src/text-selector.ts
3107
3122
  var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\\(/;
@@ -8692,7 +8707,11 @@ var COMPONENT_JS = `(() => {
8692
8707
  });
8693
8708
  } else {
8694
8709
  window.open(payload2.url, "_blank");
8695
- this.respond(msg2.id, true, { opened: true, fallback: 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
+ });
8696
8715
  }
8697
8716
  } else if (action2 === "close") {
8698
8717
  if (haltija?.closeTab) {
@@ -12818,9 +12837,15 @@ var tabsOpen = endpoint({
12818
12837
  path: "/tabs/open",
12819
12838
  method: "POST",
12820
12839
  summary: "Open a new tab",
12821
- description: `Desktop app only. Opens a new tab with optional URL.
12822
-
12823
- If url is omitted, opens a blank tab. The new tab gets the widget auto-injected.`,
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.`,
12824
12849
  category: "windows",
12825
12850
  input: L.object({
12826
12851
  url: L.string.describe("URL to open").optional
@@ -14247,6 +14272,9 @@ registerHandler(screenshot, async (body, ctx) => {
14247
14272
  });
14248
14273
  registerHandler(tabsOpen, async (body, ctx) => {
14249
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
+ }
14250
14278
  return Response.json(response, { headers: ctx.headers });
14251
14279
  });
14252
14280
  registerHandler(tabsClose, async (body, ctx) => {
package/dist/version.d.ts CHANGED
@@ -8,4 +8,4 @@
8
8
  * ⚠️ AUTO-GENERATED FROM package.json - DO NOT EDIT THIS FILE
9
9
  * ⚠️ To change the version, update package.json and run: bun run build
10
10
  */
11
- export declare const VERSION = "1.5.2";
11
+ export declare const VERSION = "1.5.4";
@@ -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 (lighter weight)
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. Lighter weight but slightly different browser engine.
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:** `haltija --headless` (or `--ci`) runs a headless Chromium with the widget
44
- auto-injected deterministic, no desktop app required. See the CI guide below.
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "haltija",
3
- "version": "1.5.2",
3
+ "version": "1.5.4",
4
4
  "description": "Browser control for AI agents - query DOM, click, type, run JS, watch mutations",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",