haltija 1.4.0 → 1.4.1
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 +71 -0
- package/apps/desktop/main.js +32 -6
- package/apps/desktop/package.json +1 -1
- package/apps/desktop/resources/component.js +1 -1
- package/bin/tosijs-dev.mjs +60 -4
- package/bin/version.mjs +1 -1
- package/dist/component.d.ts +1 -1
- package/dist/component.esm.js +1 -1
- package/dist/component.js +1 -1
- package/dist/hj.js +2 -2
- package/dist/index.js +89 -18
- package/dist/server.js +89 -18
- package/dist/version.d.ts +1 -1
- package/llms.txt +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,76 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.4.1
|
|
4
|
+
|
|
5
|
+
Five cross-project bugs, all of the same shape: **haltija reaching out and disrupting a healthy
|
|
6
|
+
peer.** If you run more than one project on a machine, this is the release that stops your
|
|
7
|
+
browser channel vanishing.
|
|
8
|
+
|
|
9
|
+
### Behavior changes (no API breaks)
|
|
10
|
+
|
|
11
|
+
Nothing was removed or renamed — no endpoint, export, or flag — so nothing should fail to compile
|
|
12
|
+
or resolve. Two *runtime* behaviors changed, and in both the old behavior was the bug:
|
|
13
|
+
|
|
14
|
+
- **The desktop app attaches to an existing server instead of replacing it.** If you relied on a
|
|
15
|
+
launch always giving you a pristine embedded server, set `HALTIJA_SERVER_MODE=builtin`.
|
|
16
|
+
- **`--https` (https-only) now exits if it cannot bind its port**, instead of silently starting on
|
|
17
|
+
an ephemeral one. The old "success" produced a channel no widget could reach.
|
|
18
|
+
|
|
19
|
+
### New: `--private` — isolated automation instances ([#1](https://github.com/tonioloewald/haltija/issues/1))
|
|
20
|
+
|
|
21
|
+
haltija plays two roles that were conflated. A **shared interactive** browser on the default port
|
|
22
|
+
is a feature — whatever window is focused is what `hj` drives, across projects. But **ephemeral
|
|
23
|
+
automation** (a test lane that spawns a browser, drives fixed pages, and exits) was consulting
|
|
24
|
+
that shared server and, if any was reachable, *adopting and navigating it* — so one project's
|
|
25
|
+
doc-test lane yanked another project's live browser to different pages, and then failed on a
|
|
26
|
+
timeout. Intermittent and baffling, because it only bit when a foreign haltija happened to be up.
|
|
27
|
+
|
|
28
|
+
`haltija --private` (pair with `--headless`) is isolated by construction:
|
|
29
|
+
|
|
30
|
+
- binds an **ephemeral port, never 8700** — it can't collide with or be mistaken for the shared server;
|
|
31
|
+
- is **not registered** in the shared registry, so interactive `hj` / cwd-routing can't adopt it;
|
|
32
|
+
- **never reaches out** — it retires nothing and touches no other server;
|
|
33
|
+
- **reports its address** on stdout (`HALTIJA_PRIVATE_READY {json}`) and to `--port-file` — since
|
|
34
|
+
it's not in the registry, that's how you find it.
|
|
35
|
+
|
|
36
|
+
A consumer's test lane should request a private instance and drive *that* by the port it reports,
|
|
37
|
+
instead of an unscoped `hj windows` check that races whatever else is on the machine.
|
|
38
|
+
|
|
39
|
+
### Fixed: the desktop app killed other projects' channels
|
|
40
|
+
|
|
41
|
+
Its default was to stop any server on 8700/8701 and start fresh — so launching the app (`bunx
|
|
42
|
+
haltija`, an `hj` auto-launch, `--ci`, the integration test) silently took down a live channel
|
|
43
|
+
another project was using, and made its widget vanish. It now **attaches to a healthy existing
|
|
44
|
+
server and says so**. Force the old behavior with `HALTIJA_SERVER_MODE=builtin`.
|
|
45
|
+
|
|
46
|
+
### Fixed: a half-dead `--both` channel (HTTPS silently on the wrong port)
|
|
47
|
+
|
|
48
|
+
When the HTTPS port was busy (a fast restart racing the previous server), the HTTPS side quietly
|
|
49
|
+
fell back to an **ephemeral** port. But a widget on an https page connects to the *known* port —
|
|
50
|
+
so 8701 sat empty, the page couldn't connect, and the server looked healthy because HTTP was fine.
|
|
51
|
+
HTTPS now retries its intended port and, failing that, **fails loudly** rather than relocating;
|
|
52
|
+
the startup banner never advertises a port it didn't bind.
|
|
53
|
+
|
|
54
|
+
### Fixed: silent HTTP port relocation
|
|
55
|
+
|
|
56
|
+
When the wanted HTTP port was taken, the server bound an ephemeral one without a word — so a
|
|
57
|
+
caller probing a fixed port had no idea why nothing was there. It now says
|
|
58
|
+
`<port> was taken; bound HTTP on <n> instead. Find it with \`hj where\`.`
|
|
59
|
+
|
|
60
|
+
### Fixed: the test suite disrupted other servers
|
|
61
|
+
|
|
62
|
+
`bun test` bound fixed 87xx ports — the range real servers live in — and on a collision would
|
|
63
|
+
`POST /shutdown` whatever was there, including another project's channel. The suite now uses
|
|
64
|
+
high, per-process-unique ports and can never stop a server it didn't start.
|
|
65
|
+
|
|
66
|
+
### Docs
|
|
67
|
+
|
|
68
|
+
A "tab that reads as unreachable" troubleshooting section in `DOCS.md` and `llms.txt`: a hidden,
|
|
69
|
+
backgrounded, minimized, or occluded tab (and an active WebXR session) suspends
|
|
70
|
+
`requestAnimationFrame` and throttles timers, so the tab can stop answering even though the page
|
|
71
|
+
is fine. Bring it forward, or target it explicitly with `hj --window <id>`.
|
|
72
|
+
|
|
73
|
+
|
|
3
74
|
## 1.4.0
|
|
4
75
|
|
|
5
76
|
**`hj` now routes to the server that owns your current directory.** If you run more than one
|
package/apps/desktop/main.js
CHANGED
|
@@ -56,10 +56,17 @@ let navigateRequestId = 0
|
|
|
56
56
|
|
|
57
57
|
const DEFAULT_PREFS = {
|
|
58
58
|
// Server startup behavior:
|
|
59
|
-
// '
|
|
60
|
-
// '
|
|
61
|
-
// '
|
|
62
|
-
|
|
59
|
+
// 'auto' - Use an existing healthy server if one is on the port, else start embedded (DEFAULT)
|
|
60
|
+
// 'builtin' - Always stop any existing server and start fresh (the old default)
|
|
61
|
+
// 'external' - Never start a server, expect one running externally
|
|
62
|
+
//
|
|
63
|
+
// Default is 'auto', NOT 'builtin'. On a machine running more than one project, 8700/8701 are
|
|
64
|
+
// shared: another project may legitimately have a live channel there (e.g. `haltija --server
|
|
65
|
+
// --both`). 'builtin' treated that channel as a "zombie" and killed it to start fresh — so
|
|
66
|
+
// launching the desktop app (or `bunx haltija`, or an `hj` auto-launch) silently took down
|
|
67
|
+
// another project's channel and made its widget vanish. 'auto' reuses a healthy server instead.
|
|
68
|
+
// Force the old behavior with HALTIJA_SERVER_MODE=builtin when you specifically want your own.
|
|
69
|
+
serverMode: process.env.HALTIJA_SERVER_MODE || 'auto',
|
|
63
70
|
}
|
|
64
71
|
|
|
65
72
|
// Active prefs (will be overwritten by persisted prefs when IPC is wired up)
|
|
@@ -67,6 +74,10 @@ const prefs = { ...DEFAULT_PREFS }
|
|
|
67
74
|
|
|
68
75
|
let mainWindow = null
|
|
69
76
|
const embeddedServers = []
|
|
77
|
+
// True when this app attached to a server it did not start (auto mode found one already on the
|
|
78
|
+
// port). Surfaced to the window so the user knows they're driving a reused, possibly-foreign server.
|
|
79
|
+
let reusedExternalServer = false
|
|
80
|
+
let reusedServerBanner = ''
|
|
70
81
|
|
|
71
82
|
// ============================================
|
|
72
83
|
// MCP Setup for Claude Desktop
|
|
@@ -299,6 +310,15 @@ function createWindow() {
|
|
|
299
310
|
// Load the shell UI
|
|
300
311
|
mainWindow.loadFile('index.html')
|
|
301
312
|
|
|
313
|
+
// If we attached to a server we didn't start, tell the renderer so it can surface it in the
|
|
314
|
+
// UI. Best-effort: harmless if the renderer doesn't handle 'server-reused' yet (that UI is a
|
|
315
|
+
// filed follow-up); the reuse is also announced on the app's console output regardless.
|
|
316
|
+
if (reusedExternalServer) {
|
|
317
|
+
mainWindow.webContents.once('did-finish-load', () => {
|
|
318
|
+
try { mainWindow.webContents.send('server-reused', reusedServerBanner) } catch {}
|
|
319
|
+
})
|
|
320
|
+
}
|
|
321
|
+
|
|
302
322
|
// Open DevTools in development
|
|
303
323
|
if (process.env.NODE_ENV === 'development') {
|
|
304
324
|
mainWindow.webContents.openDevTools()
|
|
@@ -1399,9 +1419,15 @@ async function ensureServer() {
|
|
|
1399
1419
|
return false
|
|
1400
1420
|
|
|
1401
1421
|
case 'auto':
|
|
1402
|
-
// Use existing if found, else start embedded
|
|
1422
|
+
// Use existing if found, else start embedded. Reusing is the whole point: it means the
|
|
1423
|
+
// app does NOT kill a channel another project may be running on 8700/8701.
|
|
1403
1424
|
if (running) {
|
|
1404
|
-
|
|
1425
|
+
// Surface this prominently — the user should know the app attached to a server it did
|
|
1426
|
+
// NOT start (possibly a different version/config), rather than silently assuming its own.
|
|
1427
|
+
const banner = `Attached to an existing haltija server at ${HALTIJA_SERVER} (did not start my own; set HALTIJA_SERVER_MODE=builtin to force a fresh one)`
|
|
1428
|
+
console.log(`[Haltija Desktop] ${banner}`)
|
|
1429
|
+
reusedExternalServer = true
|
|
1430
|
+
reusedServerBanner = banner
|
|
1405
1431
|
return true
|
|
1406
1432
|
}
|
|
1407
1433
|
console.log('[Haltija Desktop] No server found, starting embedded')
|
package/bin/tosijs-dev.mjs
CHANGED
|
@@ -59,6 +59,11 @@ Options:
|
|
|
59
59
|
HALTIJA_NAME=foo hj <cmd> can resolve back to its port
|
|
60
60
|
--force, -f Restart even if server already running
|
|
61
61
|
--wait-ready Block until server + browser are ready (for scripts)
|
|
62
|
+
--private Isolated automation instance: own server on an EPHEMERAL port (never
|
|
63
|
+
8700), own browser, torn down with the run. Never sees/adopts/navigates
|
|
64
|
+
the shared interactive browser. Reports its address on stdout
|
|
65
|
+
(HALTIJA_PRIVATE_READY {json}) and to --port-file. Pair with --headless.
|
|
66
|
+
--port-file <p> Write the (private) server's bound address as JSON to <p> when ready.
|
|
62
67
|
--setup-mcp Configure Claude Desktop MCP integration
|
|
63
68
|
--setup-mcp-check Check MCP configuration status
|
|
64
69
|
--setup-mcp-remove Remove Haltija from Claude Desktop config
|
|
@@ -408,6 +413,43 @@ const explicitServer = args.includes('--server')
|
|
|
408
413
|
const explicitApp = args.includes('--app') || ciMode // CI mode uses Electron app
|
|
409
414
|
const waitReady = args.includes('--wait-ready') || ciMode
|
|
410
415
|
|
|
416
|
+
// --private: the "ephemeral automation" role (issue #1). An isolated instance — own server on an
|
|
417
|
+
// EPHEMERAL port (never 8700), its own browser, torn down with the run — that must never see,
|
|
418
|
+
// adopt, or navigate the shared interactive browser. It reports its address on stdout
|
|
419
|
+
// (HALTIJA_PRIVATE_READY {json}) and to --port-file, so the caller drives THAT, with no `hj
|
|
420
|
+
// windows` race against whatever else is on the machine. Sets the isolation env for the server.
|
|
421
|
+
const privateMode = args.includes('--private')
|
|
422
|
+
const portFileIdx = args.indexOf('--port-file')
|
|
423
|
+
let privatePortFile = null
|
|
424
|
+
if (privateMode) {
|
|
425
|
+
// Mutate `env` (the object passed to the spawned server), NOT process.env — `env` was already
|
|
426
|
+
// snapshotted above, so setting process.env here would never reach the server.
|
|
427
|
+
env.HALTIJA_PRIVATE = '1'
|
|
428
|
+
env.HALTIJA_NO_RETIRE = '1' // never touch another server
|
|
429
|
+
env.HALTIJA_NO_INSTALL = '1' // never rewrite the shared ~/.local/bin/hj
|
|
430
|
+
delete env.HALTIJA_PORT // force ephemeral — never bind the shared default
|
|
431
|
+
delete env.DEV_CHANNEL_PORT
|
|
432
|
+
// The server writes its actual (ephemeral) address here on ready; we read it back to point our
|
|
433
|
+
// own browser at it, and the caller can read it too. Default to a temp file if none was given.
|
|
434
|
+
privatePortFile = (portFileIdx !== -1 && args[portFileIdx + 1])
|
|
435
|
+
? args[portFileIdx + 1]
|
|
436
|
+
: join(tmpdir(), `haltija-private-${process.pid}.json`)
|
|
437
|
+
env.HALTIJA_PORT_FILE = privatePortFile
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** Poll the private instance's port-file until it reports its ephemeral port. */
|
|
441
|
+
async function discoverPrivatePort(portFile, timeout = 30000) {
|
|
442
|
+
const start = Date.now()
|
|
443
|
+
while (Date.now() - start < timeout) {
|
|
444
|
+
try {
|
|
445
|
+
const data = JSON.parse(readFileSync(portFile, 'utf-8'))
|
|
446
|
+
if (data && data.port) return data.port
|
|
447
|
+
} catch {}
|
|
448
|
+
await new Promise((r) => setTimeout(r, 200))
|
|
449
|
+
}
|
|
450
|
+
return null
|
|
451
|
+
}
|
|
452
|
+
|
|
411
453
|
// CI mode sets ELECTRON_DISABLE_SANDBOX automatically (needed for containerized CI)
|
|
412
454
|
if (ciMode && !process.env.ELECTRON_DISABLE_SANDBOX) {
|
|
413
455
|
process.env.ELECTRON_DISABLE_SANDBOX = '1'
|
|
@@ -717,7 +759,13 @@ const tryBun = () => {
|
|
|
717
759
|
// Start headless browser after server is ready
|
|
718
760
|
if (headlessMode) {
|
|
719
761
|
(async () => {
|
|
720
|
-
const serverPort =
|
|
762
|
+
const serverPort = privateMode
|
|
763
|
+
? await discoverPrivatePort(privatePortFile)
|
|
764
|
+
: (env.DEV_CHANNEL_PORT || 8700)
|
|
765
|
+
if (!serverPort) {
|
|
766
|
+
console.error('[tosijs-dev] Private server did not report its port within timeout')
|
|
767
|
+
process.exit(1)
|
|
768
|
+
}
|
|
721
769
|
console.log('[tosijs-dev] Waiting for server to be ready...')
|
|
722
770
|
const serverReady = await waitForServer(serverPort)
|
|
723
771
|
if (serverReady) {
|
|
@@ -750,7 +798,13 @@ const tryNode = () => {
|
|
|
750
798
|
// Start headless browser after server is ready
|
|
751
799
|
if (headlessMode) {
|
|
752
800
|
(async () => {
|
|
753
|
-
const serverPort =
|
|
801
|
+
const serverPort = privateMode
|
|
802
|
+
? await discoverPrivatePort(privatePortFile)
|
|
803
|
+
: (env.DEV_CHANNEL_PORT || 8700)
|
|
804
|
+
if (!serverPort) {
|
|
805
|
+
console.error('[tosijs-dev] Private server did not report its port within timeout')
|
|
806
|
+
process.exit(1)
|
|
807
|
+
}
|
|
754
808
|
console.log('[tosijs-dev] Waiting for server to be ready...')
|
|
755
809
|
const serverReady = await waitForServer(serverPort)
|
|
756
810
|
if (serverReady) {
|
|
@@ -867,8 +921,10 @@ function killOnPort(port) {
|
|
|
867
921
|
const port = env.DEV_CHANNEL_PORT || '8700'
|
|
868
922
|
const forceRestart = args.includes('--force') || args.includes('-f')
|
|
869
923
|
|
|
870
|
-
//
|
|
871
|
-
|
|
924
|
+
// A PRIVATE instance NEVER consults or adopts the shared server — that adoption is the exact
|
|
925
|
+
// bug issue #1 is about, and it lived right here in our own launcher (it saw the shared 8700
|
|
926
|
+
// server and exited, "already running"). Private always starts its own on an ephemeral port.
|
|
927
|
+
const existingServer = privateMode ? { running: false } : await checkExistingServer(port)
|
|
872
928
|
if (existingServer.running && !forceRestart) {
|
|
873
929
|
console.log('')
|
|
874
930
|
console.log(green('✓') + ` Haltija is already running on port ${port}` +
|
package/bin/version.mjs
CHANGED
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.4.
|
|
23
|
+
export declare const VERSION = "1.4.1";
|
|
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.4.
|
|
2
|
+
// haltija-cli:do-not-edit v1.4.1
|
|
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.4.
|
|
759
|
+
var HJ_VERSION = "1.4.1";
|
|
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.4.
|
|
677
|
+
var VERSION = "1.4.1";
|
|
678
678
|
|
|
679
679
|
// src/embedded-assets.ts
|
|
680
680
|
var APP_MD = `# Haltija App
|
|
@@ -2897,6 +2897,18 @@ breaking; do not chase a page bug that isn't there. Known causes:
|
|
|
2897
2897
|
e.g. write the values you care about somewhere your test/agent can read them (an
|
|
2898
2898
|
in-page debug source, an in-headset stats panel), rather than issuing \`hj eval\` that
|
|
2899
2899
|
round-trips through the suspended heartbeat.
|
|
2900
|
+
- **A tab that isn't visible on screen** \u2014 the common everyday case, and the one
|
|
2901
|
+
people hit without realizing. A backgrounded tab, a minimized window, or a window
|
|
2902
|
+
that is *maximized on another Space or fully occluded* (e.g. on macOS) all count as
|
|
2903
|
+
**hidden** (\`document.visibilityState === "hidden"\`). While hidden, browsers stop
|
|
2904
|
+
\`requestAnimationFrame\` entirely and throttle timers \u2014 and, on top of that, the
|
|
2905
|
+
Haltija widget deliberately **deactivates** a hidden tab so untargeted commands
|
|
2906
|
+
route to whichever tab is actually in front (focus-follows-visible-tab). So a page
|
|
2907
|
+
you can't see may not answer an untargeted \`hj eval\`/\`hj tree\`, and any
|
|
2908
|
+
\`rAF\`-driven state (scroll progress, animation frames) is frozen even if it does.
|
|
2909
|
+
Fixes: **bring the tab to the front**, or **target it explicitly** \u2014
|
|
2910
|
+
\`hj --window <id> eval \u2026\` (get the id from \`hj windows\`) reaches a specific tab
|
|
2911
|
+
regardless of visibility. Don't rely on rAF-driven values while a tab is hidden.
|
|
2900
2912
|
- **A heavily throttled background tab, or a long blocking task** on the main thread \u2014
|
|
2901
2913
|
same shape: the heartbeat can't run, so the tab looks dead until the thread frees up.
|
|
2902
2914
|
|
|
@@ -2925,6 +2937,13 @@ hj tree --no-launch # Skip auto-launch
|
|
|
2925
2937
|
hj status # Info commands never trigger auto-launch
|
|
2926
2938
|
\`\`\`
|
|
2927
2939
|
|
|
2940
|
+
**The desktop app REUSES an existing server rather than replacing it.** If a
|
|
2941
|
+
haltija server is already running on the port (e.g. another project started one
|
|
2942
|
+
with \`haltija --server --both\`), the app attaches to it and says so, instead of
|
|
2943
|
+
killing it to start its own \u2014 so launching the app (or an \`hj\` auto-launch) no
|
|
2944
|
+
longer takes down another project's channel. Set \`HALTIJA_SERVER_MODE=builtin\` to
|
|
2945
|
+
force the app to run its own fresh server instead.
|
|
2946
|
+
|
|
2928
2947
|
## CI / Headless
|
|
2929
2948
|
|
|
2930
2949
|
\`\`\`bash
|
|
@@ -3020,6 +3039,7 @@ Or via a script tag (auto-injecting IIFE bundle served by the running server):
|
|
|
3020
3039
|
|
|
3021
3040
|
## Gotcha: a tab can read as unreachable while its main thread is suspended
|
|
3022
3041
|
- The widget proves liveness with a heartbeat. If the page's main thread or its \`requestAnimationFrame\` loop is suspended, the heartbeat stalls and commands time out or report "no browser connected" \u2014 even though the PAGE is fine. It's the instrument going quiet, not a page bug.
|
|
3042
|
+
- **A tab that is not visible on screen** \u2014 backgrounded, minimized, or maximized on another Space / fully occluded (macOS) \u2014 counts as \`hidden\`. Browsers stop \`requestAnimationFrame\` and throttle timers while hidden, AND the widget deliberately deactivates a hidden tab so untargeted commands go to the front tab. So a page you can't see may not answer an untargeted \`hj eval\`/\`hj tree\`, and rAF-driven state (scroll/animation) is frozen. Fix: bring it to the front, or **target it explicitly** \u2014 \`hj --window <id> eval \u2026\` (id from \`hj windows\`) reaches a specific tab regardless of visibility.
|
|
3023
3043
|
- **WebXR / immersive sessions suspend \`window.requestAnimationFrame\`** (the XR compositor drives frames instead), so a tab is unreachable *while immersive*. Drive state out through your own channel \u2014 write the values you care about somewhere your agent can read (a debug source, an in-headset stats panel) \u2014 rather than \`hj eval\` round-trips through the suspended heartbeat. A heavily throttled background tab or a long blocking task has the same shape.
|
|
3024
3044
|
- A command hitting the WRONG page (not a dead one) is targeting, not liveness: run \`hj where\`.
|
|
3025
3045
|
|
|
@@ -3077,7 +3097,7 @@ var COMPONENT_JS = `(() => {
|
|
|
3077
3097
|
});
|
|
3078
3098
|
|
|
3079
3099
|
// src/version.ts
|
|
3080
|
-
var VERSION = "1.4.
|
|
3100
|
+
var VERSION = "1.4.1";
|
|
3081
3101
|
|
|
3082
3102
|
// src/text-selector.ts
|
|
3083
3103
|
var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\\(/;
|
|
@@ -16000,10 +16020,11 @@ function getMcpStatus() {
|
|
|
16000
16020
|
var PRODUCT_NAME2 = "Haltija";
|
|
16001
16021
|
var TAG_NAME = "haltija-dev";
|
|
16002
16022
|
var LOG_PREFIX = "[haltija]";
|
|
16023
|
+
var IS_PRIVATE = process.env.HALTIJA_PRIVATE === "1";
|
|
16003
16024
|
var PORT_PREFERENCE = process.env.HALTIJA_PORT || process.env.DEV_CHANNEL_PORT;
|
|
16004
|
-
var PORT_IS_STRICT = !!PORT_PREFERENCE;
|
|
16005
|
-
var PORT = parseInt(PORT_PREFERENCE || "8700");
|
|
16006
|
-
var HTTPS_PORT = parseInt(process.env.DEV_CHANNEL_HTTPS_PORT || "8701");
|
|
16025
|
+
var PORT_IS_STRICT = !!PORT_PREFERENCE && !IS_PRIVATE;
|
|
16026
|
+
var PORT = IS_PRIVATE ? 0 : parseInt(PORT_PREFERENCE || "8700");
|
|
16027
|
+
var HTTPS_PORT = IS_PRIVATE ? 0 : parseInt(process.env.DEV_CHANNEL_HTTPS_PORT || "8701");
|
|
16007
16028
|
var INSTANCE_NAME = process.env.HALTIJA_NAME || "";
|
|
16008
16029
|
var SNAPSHOTS_DIR = process.env.DEV_CHANNEL_SNAPSHOTS_DIR || null;
|
|
16009
16030
|
var DOCS_DIR = process.env.DEV_CHANNEL_DOCS_DIR || null;
|
|
@@ -18993,6 +19014,8 @@ var serverConfig = {
|
|
|
18993
19014
|
}
|
|
18994
19015
|
};
|
|
18995
19016
|
async function freePort(port) {
|
|
19017
|
+
if (process.env.HALTIJA_NO_RETIRE === "1")
|
|
19018
|
+
return false;
|
|
18996
19019
|
if (listenerPidsOnPort(port).length === 0)
|
|
18997
19020
|
return false;
|
|
18998
19021
|
const probe = await probePort(port);
|
|
@@ -19080,6 +19103,8 @@ async function probePort(port) {
|
|
|
19080
19103
|
async function retireLegacyServers() {
|
|
19081
19104
|
if (process.env.HALTIJA_NO_RETIRE === "1")
|
|
19082
19105
|
return;
|
|
19106
|
+
if (IS_PRIVATE)
|
|
19107
|
+
return;
|
|
19083
19108
|
const registryPorts = list().map((e) => e.port);
|
|
19084
19109
|
const ports = candidatePorts({ defaults: [8700, 8701, PORT], registryPorts });
|
|
19085
19110
|
let stopped = false;
|
|
@@ -19118,14 +19143,20 @@ if (USE_HTTP) {
|
|
|
19118
19143
|
});
|
|
19119
19144
|
} catch (err) {
|
|
19120
19145
|
if (err && typeof err === "object" && "code" in err && err.code === "EADDRINUSE") {
|
|
19146
|
+
const wantedHttpPort = PORT;
|
|
19121
19147
|
if (PORT_IS_STRICT) {
|
|
19122
19148
|
if (await freePort(PORT)) {
|
|
19123
19149
|
httpServer = Bun.serve({ port: PORT, ...serverConfig });
|
|
19124
19150
|
} else {
|
|
19151
|
+
console.error("");
|
|
19152
|
+
console.error(` \u26A0\uFE0F Could not bind the HTTP port ${wantedHttpPort} you asked for \u2014 it is in use.`);
|
|
19153
|
+
console.error(` Free it, or start on another port (--port N / HALTIJA_PORT).`);
|
|
19154
|
+
console.error("");
|
|
19125
19155
|
throw err;
|
|
19126
19156
|
}
|
|
19127
19157
|
} else {
|
|
19128
19158
|
httpServer = Bun.serve({ port: 0, ...serverConfig });
|
|
19159
|
+
console.warn(` [port] ${wantedHttpPort} was taken; bound HTTP on ${httpServer.port} instead. Find it with \`hj where\` (or read ~/.haltija/servers/).`);
|
|
19129
19160
|
}
|
|
19130
19161
|
} else {
|
|
19131
19162
|
throw err;
|
|
@@ -19138,29 +19169,63 @@ if (USE_HTTPS) {
|
|
|
19138
19169
|
cert: readFileSync5(certPath),
|
|
19139
19170
|
key: readFileSync5(keyPath)
|
|
19140
19171
|
};
|
|
19172
|
+
const wantedHttpsPort = HTTPS_PORT;
|
|
19173
|
+
const bindHttps = () => Bun.serve({ port: wantedHttpsPort, tls, ...serverConfig });
|
|
19141
19174
|
try {
|
|
19142
|
-
httpsServer =
|
|
19175
|
+
httpsServer = bindHttps();
|
|
19143
19176
|
} catch (err) {
|
|
19144
19177
|
if (err && typeof err === "object" && "code" in err && err.code === "EADDRINUSE") {
|
|
19145
|
-
if (PORT_IS_STRICT) {
|
|
19146
|
-
|
|
19147
|
-
httpsServer = Bun.serve({ port: HTTPS_PORT, tls, ...serverConfig });
|
|
19148
|
-
} else {
|
|
19149
|
-
throw err;
|
|
19150
|
-
}
|
|
19178
|
+
if (PORT_IS_STRICT && await freePort(wantedHttpsPort)) {
|
|
19179
|
+
httpsServer = bindHttps();
|
|
19151
19180
|
} else {
|
|
19152
|
-
|
|
19181
|
+
for (let attempt = 0;attempt < 5 && !httpsServer; attempt++) {
|
|
19182
|
+
Bun.sleepSync(400);
|
|
19183
|
+
try {
|
|
19184
|
+
httpsServer = bindHttps();
|
|
19185
|
+
} catch {}
|
|
19186
|
+
}
|
|
19187
|
+
if (!httpsServer) {
|
|
19188
|
+
console.error("");
|
|
19189
|
+
console.error(` \u26A0\uFE0F HTTPS could not bind its port ${wantedHttpsPort} \u2014 it is still in use`);
|
|
19190
|
+
console.error(` (a previous server in TIME_WAIT, or another process holding it).`);
|
|
19191
|
+
console.error(` A widget on an https page connects to :${wantedHttpsPort}, so it will`);
|
|
19192
|
+
console.error(` NOT connect until this is free. Haltija is NOT silently relocating it.`);
|
|
19193
|
+
console.error(` Fix: wait a few seconds and restart, or free port ${wantedHttpsPort}.`);
|
|
19194
|
+
console.error("");
|
|
19195
|
+
if (!USE_HTTP) {
|
|
19196
|
+
throw err;
|
|
19197
|
+
}
|
|
19198
|
+
}
|
|
19153
19199
|
}
|
|
19154
19200
|
} else {
|
|
19155
19201
|
throw err;
|
|
19156
19202
|
}
|
|
19157
19203
|
}
|
|
19158
|
-
|
|
19204
|
+
if (httpsServer)
|
|
19205
|
+
HTTPS_PORT = httpsServer.port ?? HTTPS_PORT;
|
|
19159
19206
|
}
|
|
19160
|
-
var httpUrl =
|
|
19161
|
-
var httpsUrl =
|
|
19207
|
+
var httpUrl = httpServer ? `http://localhost:${PORT}` : null;
|
|
19208
|
+
var httpsUrl = httpsServer ? `https://localhost:${HTTPS_PORT}` : null;
|
|
19162
19209
|
var primaryUrl = httpsUrl || httpUrl;
|
|
19163
|
-
|
|
19210
|
+
if (IS_PRIVATE) {
|
|
19211
|
+
const ready = {
|
|
19212
|
+
port: PORT,
|
|
19213
|
+
httpsPort: httpsServer ? HTTPS_PORT : null,
|
|
19214
|
+
url: httpUrl,
|
|
19215
|
+
httpsUrl,
|
|
19216
|
+
pid: process.pid
|
|
19217
|
+
};
|
|
19218
|
+
console.log(`HALTIJA_PRIVATE_READY ${JSON.stringify(ready)}`);
|
|
19219
|
+
const portFile = process.env.HALTIJA_PORT_FILE;
|
|
19220
|
+
if (portFile) {
|
|
19221
|
+
try {
|
|
19222
|
+
writeFileSync3(portFile, JSON.stringify(ready));
|
|
19223
|
+
} catch (err) {
|
|
19224
|
+
console.error(`${LOG_PREFIX} could not write HALTIJA_PORT_FILE ${portFile}: ${err instanceof Error ? err.message : err}`);
|
|
19225
|
+
}
|
|
19226
|
+
}
|
|
19227
|
+
}
|
|
19228
|
+
var CAN_BE_REGISTERED = USE_HTTP && !IS_PRIVATE;
|
|
19164
19229
|
var REGISTRY_NAME = !CAN_BE_REGISTERED ? "" : INSTANCE_NAME || (isDesktopApp ? "" : autoNameFor(PORT));
|
|
19165
19230
|
if (!CAN_BE_REGISTERED && (INSTANCE_NAME || !isDesktopApp)) {
|
|
19166
19231
|
console.log(`${LOG_PREFIX} HTTPS-only: not registering an instance (hj speaks HTTP; cwd routing and --name need an HTTP port)`);
|
|
@@ -19367,8 +19432,14 @@ if (USE_HTTPS && !httpUrl) {
|
|
|
19367
19432
|
`);
|
|
19368
19433
|
}
|
|
19369
19434
|
if (MODE === "both") {
|
|
19370
|
-
|
|
19435
|
+
if (httpsServer) {
|
|
19436
|
+
console.log(` Mode: both - Use HTTP (${PORT}) for HTTP sites, HTTPS (${HTTPS_PORT}) for HTTPS sites.
|
|
19371
19437
|
`);
|
|
19438
|
+
} else {
|
|
19439
|
+
console.log(` Mode: both - \u26A0\uFE0F HTTP (${PORT}) is up but HTTPS is DOWN (see the warning above).`);
|
|
19440
|
+
console.log(` Pages served over https will not connect until HTTPS binds.
|
|
19441
|
+
`);
|
|
19442
|
+
}
|
|
19372
19443
|
}
|
|
19373
19444
|
var server = httpsServer || httpServer;
|
|
19374
19445
|
// src/client.ts
|
package/dist/server.js
CHANGED
|
@@ -674,7 +674,7 @@ var injectorCode = `
|
|
|
674
674
|
`;
|
|
675
675
|
|
|
676
676
|
// src/version.ts
|
|
677
|
-
var VERSION = "1.4.
|
|
677
|
+
var VERSION = "1.4.1";
|
|
678
678
|
|
|
679
679
|
// src/embedded-assets.ts
|
|
680
680
|
var APP_MD = `# Haltija App
|
|
@@ -2897,6 +2897,18 @@ breaking; do not chase a page bug that isn't there. Known causes:
|
|
|
2897
2897
|
e.g. write the values you care about somewhere your test/agent can read them (an
|
|
2898
2898
|
in-page debug source, an in-headset stats panel), rather than issuing \`hj eval\` that
|
|
2899
2899
|
round-trips through the suspended heartbeat.
|
|
2900
|
+
- **A tab that isn't visible on screen** \u2014 the common everyday case, and the one
|
|
2901
|
+
people hit without realizing. A backgrounded tab, a minimized window, or a window
|
|
2902
|
+
that is *maximized on another Space or fully occluded* (e.g. on macOS) all count as
|
|
2903
|
+
**hidden** (\`document.visibilityState === "hidden"\`). While hidden, browsers stop
|
|
2904
|
+
\`requestAnimationFrame\` entirely and throttle timers \u2014 and, on top of that, the
|
|
2905
|
+
Haltija widget deliberately **deactivates** a hidden tab so untargeted commands
|
|
2906
|
+
route to whichever tab is actually in front (focus-follows-visible-tab). So a page
|
|
2907
|
+
you can't see may not answer an untargeted \`hj eval\`/\`hj tree\`, and any
|
|
2908
|
+
\`rAF\`-driven state (scroll progress, animation frames) is frozen even if it does.
|
|
2909
|
+
Fixes: **bring the tab to the front**, or **target it explicitly** \u2014
|
|
2910
|
+
\`hj --window <id> eval \u2026\` (get the id from \`hj windows\`) reaches a specific tab
|
|
2911
|
+
regardless of visibility. Don't rely on rAF-driven values while a tab is hidden.
|
|
2900
2912
|
- **A heavily throttled background tab, or a long blocking task** on the main thread \u2014
|
|
2901
2913
|
same shape: the heartbeat can't run, so the tab looks dead until the thread frees up.
|
|
2902
2914
|
|
|
@@ -2925,6 +2937,13 @@ hj tree --no-launch # Skip auto-launch
|
|
|
2925
2937
|
hj status # Info commands never trigger auto-launch
|
|
2926
2938
|
\`\`\`
|
|
2927
2939
|
|
|
2940
|
+
**The desktop app REUSES an existing server rather than replacing it.** If a
|
|
2941
|
+
haltija server is already running on the port (e.g. another project started one
|
|
2942
|
+
with \`haltija --server --both\`), the app attaches to it and says so, instead of
|
|
2943
|
+
killing it to start its own \u2014 so launching the app (or an \`hj\` auto-launch) no
|
|
2944
|
+
longer takes down another project's channel. Set \`HALTIJA_SERVER_MODE=builtin\` to
|
|
2945
|
+
force the app to run its own fresh server instead.
|
|
2946
|
+
|
|
2928
2947
|
## CI / Headless
|
|
2929
2948
|
|
|
2930
2949
|
\`\`\`bash
|
|
@@ -3020,6 +3039,7 @@ Or via a script tag (auto-injecting IIFE bundle served by the running server):
|
|
|
3020
3039
|
|
|
3021
3040
|
## Gotcha: a tab can read as unreachable while its main thread is suspended
|
|
3022
3041
|
- The widget proves liveness with a heartbeat. If the page's main thread or its \`requestAnimationFrame\` loop is suspended, the heartbeat stalls and commands time out or report "no browser connected" \u2014 even though the PAGE is fine. It's the instrument going quiet, not a page bug.
|
|
3042
|
+
- **A tab that is not visible on screen** \u2014 backgrounded, minimized, or maximized on another Space / fully occluded (macOS) \u2014 counts as \`hidden\`. Browsers stop \`requestAnimationFrame\` and throttle timers while hidden, AND the widget deliberately deactivates a hidden tab so untargeted commands go to the front tab. So a page you can't see may not answer an untargeted \`hj eval\`/\`hj tree\`, and rAF-driven state (scroll/animation) is frozen. Fix: bring it to the front, or **target it explicitly** \u2014 \`hj --window <id> eval \u2026\` (id from \`hj windows\`) reaches a specific tab regardless of visibility.
|
|
3023
3043
|
- **WebXR / immersive sessions suspend \`window.requestAnimationFrame\`** (the XR compositor drives frames instead), so a tab is unreachable *while immersive*. Drive state out through your own channel \u2014 write the values you care about somewhere your agent can read (a debug source, an in-headset stats panel) \u2014 rather than \`hj eval\` round-trips through the suspended heartbeat. A heavily throttled background tab or a long blocking task has the same shape.
|
|
3024
3044
|
- A command hitting the WRONG page (not a dead one) is targeting, not liveness: run \`hj where\`.
|
|
3025
3045
|
|
|
@@ -3077,7 +3097,7 @@ var COMPONENT_JS = `(() => {
|
|
|
3077
3097
|
});
|
|
3078
3098
|
|
|
3079
3099
|
// src/version.ts
|
|
3080
|
-
var VERSION = "1.4.
|
|
3100
|
+
var VERSION = "1.4.1";
|
|
3081
3101
|
|
|
3082
3102
|
// src/text-selector.ts
|
|
3083
3103
|
var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\\(/;
|
|
@@ -16000,10 +16020,11 @@ function getMcpStatus() {
|
|
|
16000
16020
|
var PRODUCT_NAME2 = "Haltija";
|
|
16001
16021
|
var TAG_NAME = "haltija-dev";
|
|
16002
16022
|
var LOG_PREFIX = "[haltija]";
|
|
16023
|
+
var IS_PRIVATE = process.env.HALTIJA_PRIVATE === "1";
|
|
16003
16024
|
var PORT_PREFERENCE = process.env.HALTIJA_PORT || process.env.DEV_CHANNEL_PORT;
|
|
16004
|
-
var PORT_IS_STRICT = !!PORT_PREFERENCE;
|
|
16005
|
-
var PORT = parseInt(PORT_PREFERENCE || "8700");
|
|
16006
|
-
var HTTPS_PORT = parseInt(process.env.DEV_CHANNEL_HTTPS_PORT || "8701");
|
|
16025
|
+
var PORT_IS_STRICT = !!PORT_PREFERENCE && !IS_PRIVATE;
|
|
16026
|
+
var PORT = IS_PRIVATE ? 0 : parseInt(PORT_PREFERENCE || "8700");
|
|
16027
|
+
var HTTPS_PORT = IS_PRIVATE ? 0 : parseInt(process.env.DEV_CHANNEL_HTTPS_PORT || "8701");
|
|
16007
16028
|
var INSTANCE_NAME = process.env.HALTIJA_NAME || "";
|
|
16008
16029
|
var SNAPSHOTS_DIR = process.env.DEV_CHANNEL_SNAPSHOTS_DIR || null;
|
|
16009
16030
|
var DOCS_DIR = process.env.DEV_CHANNEL_DOCS_DIR || null;
|
|
@@ -18993,6 +19014,8 @@ var serverConfig = {
|
|
|
18993
19014
|
}
|
|
18994
19015
|
};
|
|
18995
19016
|
async function freePort(port) {
|
|
19017
|
+
if (process.env.HALTIJA_NO_RETIRE === "1")
|
|
19018
|
+
return false;
|
|
18996
19019
|
if (listenerPidsOnPort(port).length === 0)
|
|
18997
19020
|
return false;
|
|
18998
19021
|
const probe = await probePort(port);
|
|
@@ -19080,6 +19103,8 @@ async function probePort(port) {
|
|
|
19080
19103
|
async function retireLegacyServers() {
|
|
19081
19104
|
if (process.env.HALTIJA_NO_RETIRE === "1")
|
|
19082
19105
|
return;
|
|
19106
|
+
if (IS_PRIVATE)
|
|
19107
|
+
return;
|
|
19083
19108
|
const registryPorts = list().map((e) => e.port);
|
|
19084
19109
|
const ports = candidatePorts({ defaults: [8700, 8701, PORT], registryPorts });
|
|
19085
19110
|
let stopped = false;
|
|
@@ -19118,14 +19143,20 @@ if (USE_HTTP) {
|
|
|
19118
19143
|
});
|
|
19119
19144
|
} catch (err) {
|
|
19120
19145
|
if (err && typeof err === "object" && "code" in err && err.code === "EADDRINUSE") {
|
|
19146
|
+
const wantedHttpPort = PORT;
|
|
19121
19147
|
if (PORT_IS_STRICT) {
|
|
19122
19148
|
if (await freePort(PORT)) {
|
|
19123
19149
|
httpServer = Bun.serve({ port: PORT, ...serverConfig });
|
|
19124
19150
|
} else {
|
|
19151
|
+
console.error("");
|
|
19152
|
+
console.error(` \u26A0\uFE0F Could not bind the HTTP port ${wantedHttpPort} you asked for \u2014 it is in use.`);
|
|
19153
|
+
console.error(` Free it, or start on another port (--port N / HALTIJA_PORT).`);
|
|
19154
|
+
console.error("");
|
|
19125
19155
|
throw err;
|
|
19126
19156
|
}
|
|
19127
19157
|
} else {
|
|
19128
19158
|
httpServer = Bun.serve({ port: 0, ...serverConfig });
|
|
19159
|
+
console.warn(` [port] ${wantedHttpPort} was taken; bound HTTP on ${httpServer.port} instead. Find it with \`hj where\` (or read ~/.haltija/servers/).`);
|
|
19129
19160
|
}
|
|
19130
19161
|
} else {
|
|
19131
19162
|
throw err;
|
|
@@ -19138,29 +19169,63 @@ if (USE_HTTPS) {
|
|
|
19138
19169
|
cert: readFileSync5(certPath),
|
|
19139
19170
|
key: readFileSync5(keyPath)
|
|
19140
19171
|
};
|
|
19172
|
+
const wantedHttpsPort = HTTPS_PORT;
|
|
19173
|
+
const bindHttps = () => Bun.serve({ port: wantedHttpsPort, tls, ...serverConfig });
|
|
19141
19174
|
try {
|
|
19142
|
-
httpsServer =
|
|
19175
|
+
httpsServer = bindHttps();
|
|
19143
19176
|
} catch (err) {
|
|
19144
19177
|
if (err && typeof err === "object" && "code" in err && err.code === "EADDRINUSE") {
|
|
19145
|
-
if (PORT_IS_STRICT) {
|
|
19146
|
-
|
|
19147
|
-
httpsServer = Bun.serve({ port: HTTPS_PORT, tls, ...serverConfig });
|
|
19148
|
-
} else {
|
|
19149
|
-
throw err;
|
|
19150
|
-
}
|
|
19178
|
+
if (PORT_IS_STRICT && await freePort(wantedHttpsPort)) {
|
|
19179
|
+
httpsServer = bindHttps();
|
|
19151
19180
|
} else {
|
|
19152
|
-
|
|
19181
|
+
for (let attempt = 0;attempt < 5 && !httpsServer; attempt++) {
|
|
19182
|
+
Bun.sleepSync(400);
|
|
19183
|
+
try {
|
|
19184
|
+
httpsServer = bindHttps();
|
|
19185
|
+
} catch {}
|
|
19186
|
+
}
|
|
19187
|
+
if (!httpsServer) {
|
|
19188
|
+
console.error("");
|
|
19189
|
+
console.error(` \u26A0\uFE0F HTTPS could not bind its port ${wantedHttpsPort} \u2014 it is still in use`);
|
|
19190
|
+
console.error(` (a previous server in TIME_WAIT, or another process holding it).`);
|
|
19191
|
+
console.error(` A widget on an https page connects to :${wantedHttpsPort}, so it will`);
|
|
19192
|
+
console.error(` NOT connect until this is free. Haltija is NOT silently relocating it.`);
|
|
19193
|
+
console.error(` Fix: wait a few seconds and restart, or free port ${wantedHttpsPort}.`);
|
|
19194
|
+
console.error("");
|
|
19195
|
+
if (!USE_HTTP) {
|
|
19196
|
+
throw err;
|
|
19197
|
+
}
|
|
19198
|
+
}
|
|
19153
19199
|
}
|
|
19154
19200
|
} else {
|
|
19155
19201
|
throw err;
|
|
19156
19202
|
}
|
|
19157
19203
|
}
|
|
19158
|
-
|
|
19204
|
+
if (httpsServer)
|
|
19205
|
+
HTTPS_PORT = httpsServer.port ?? HTTPS_PORT;
|
|
19159
19206
|
}
|
|
19160
|
-
var httpUrl =
|
|
19161
|
-
var httpsUrl =
|
|
19207
|
+
var httpUrl = httpServer ? `http://localhost:${PORT}` : null;
|
|
19208
|
+
var httpsUrl = httpsServer ? `https://localhost:${HTTPS_PORT}` : null;
|
|
19162
19209
|
var primaryUrl = httpsUrl || httpUrl;
|
|
19163
|
-
|
|
19210
|
+
if (IS_PRIVATE) {
|
|
19211
|
+
const ready = {
|
|
19212
|
+
port: PORT,
|
|
19213
|
+
httpsPort: httpsServer ? HTTPS_PORT : null,
|
|
19214
|
+
url: httpUrl,
|
|
19215
|
+
httpsUrl,
|
|
19216
|
+
pid: process.pid
|
|
19217
|
+
};
|
|
19218
|
+
console.log(`HALTIJA_PRIVATE_READY ${JSON.stringify(ready)}`);
|
|
19219
|
+
const portFile = process.env.HALTIJA_PORT_FILE;
|
|
19220
|
+
if (portFile) {
|
|
19221
|
+
try {
|
|
19222
|
+
writeFileSync3(portFile, JSON.stringify(ready));
|
|
19223
|
+
} catch (err) {
|
|
19224
|
+
console.error(`${LOG_PREFIX} could not write HALTIJA_PORT_FILE ${portFile}: ${err instanceof Error ? err.message : err}`);
|
|
19225
|
+
}
|
|
19226
|
+
}
|
|
19227
|
+
}
|
|
19228
|
+
var CAN_BE_REGISTERED = USE_HTTP && !IS_PRIVATE;
|
|
19164
19229
|
var REGISTRY_NAME = !CAN_BE_REGISTERED ? "" : INSTANCE_NAME || (isDesktopApp ? "" : autoNameFor(PORT));
|
|
19165
19230
|
if (!CAN_BE_REGISTERED && (INSTANCE_NAME || !isDesktopApp)) {
|
|
19166
19231
|
console.log(`${LOG_PREFIX} HTTPS-only: not registering an instance (hj speaks HTTP; cwd routing and --name need an HTTP port)`);
|
|
@@ -19367,8 +19432,14 @@ if (USE_HTTPS && !httpUrl) {
|
|
|
19367
19432
|
`);
|
|
19368
19433
|
}
|
|
19369
19434
|
if (MODE === "both") {
|
|
19370
|
-
|
|
19435
|
+
if (httpsServer) {
|
|
19436
|
+
console.log(` Mode: both - Use HTTP (${PORT}) for HTTP sites, HTTPS (${HTTPS_PORT}) for HTTPS sites.
|
|
19371
19437
|
`);
|
|
19438
|
+
} else {
|
|
19439
|
+
console.log(` Mode: both - \u26A0\uFE0F HTTP (${PORT}) is up but HTTPS is DOWN (see the warning above).`);
|
|
19440
|
+
console.log(` Pages served over https will not connect until HTTPS binds.
|
|
19441
|
+
`);
|
|
19442
|
+
}
|
|
19372
19443
|
}
|
|
19373
19444
|
var server = httpsServer || httpServer;
|
|
19374
19445
|
export {
|
package/dist/version.d.ts
CHANGED
package/llms.txt
CHANGED
|
@@ -59,6 +59,7 @@ Or via a script tag (auto-injecting IIFE bundle served by the running server):
|
|
|
59
59
|
|
|
60
60
|
## Gotcha: a tab can read as unreachable while its main thread is suspended
|
|
61
61
|
- The widget proves liveness with a heartbeat. If the page's main thread or its `requestAnimationFrame` loop is suspended, the heartbeat stalls and commands time out or report "no browser connected" — even though the PAGE is fine. It's the instrument going quiet, not a page bug.
|
|
62
|
+
- **A tab that is not visible on screen** — backgrounded, minimized, or maximized on another Space / fully occluded (macOS) — counts as `hidden`. Browsers stop `requestAnimationFrame` and throttle timers while hidden, AND the widget deliberately deactivates a hidden tab so untargeted commands go to the front tab. So a page you can't see may not answer an untargeted `hj eval`/`hj tree`, and rAF-driven state (scroll/animation) is frozen. Fix: bring it to the front, or **target it explicitly** — `hj --window <id> eval …` (id from `hj windows`) reaches a specific tab regardless of visibility.
|
|
62
63
|
- **WebXR / immersive sessions suspend `window.requestAnimationFrame`** (the XR compositor drives frames instead), so a tab is unreachable *while immersive*. Drive state out through your own channel — write the values you care about somewhere your agent can read (a debug source, an in-headset stats panel) — rather than `hj eval` round-trips through the suspended heartbeat. A heavily throttled background tab or a long blocking task has the same shape.
|
|
63
64
|
- A command hitting the WRONG page (not a dead one) is targeting, not liveness: run `hj where`.
|
|
64
65
|
|