haltija 1.3.3 → 1.4.0
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 +64 -0
- package/README.md +49 -1
- package/apps/desktop/main.js +75 -24
- package/apps/desktop/package.json +5 -5
- package/apps/desktop/resources/component.js +81 -66
- package/bin/cli-subcommand.mjs +71 -2
- package/bin/hj.mjs +130 -18
- package/bin/semver.mjs +71 -0
- package/bin/tosijs-dev.mjs +58 -15
- package/bin/version.mjs +6 -0
- package/dist/component.d.ts +12 -1
- package/dist/component.esm.js +81 -66
- package/dist/component.js +81 -66
- package/dist/hj-install.d.ts +112 -0
- package/dist/hj.js +137 -27
- package/dist/index.js +780 -259
- package/dist/legacy-servers.d.ts +88 -0
- package/dist/machine-log.d.ts +46 -0
- package/dist/port-pid.d.ts +33 -0
- package/dist/semver.d.ts +43 -0
- package/dist/server.js +780 -259
- package/dist/sessions.d.ts +60 -6
- package/dist/version.d.ts +1 -1
- package/llms.txt +11 -2
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.4.0
|
|
4
|
+
|
|
5
|
+
**`hj` now routes to the server that owns your current directory.** If you run more than one
|
|
6
|
+
project, this changes where your commands go — for the better, but read the first section.
|
|
7
|
+
|
|
8
|
+
### Fixed: `hj` drove the wrong browser across projects
|
|
9
|
+
|
|
10
|
+
`hj` never looked at your working directory. Every invocation, in every project, fell back to
|
|
11
|
+
port 8700 and drove whatever browser was focused there — silently, with no error. The only way
|
|
12
|
+
to target a project's own server was per-shell environment variables that agents spawning fresh
|
|
13
|
+
shells routinely lose.
|
|
14
|
+
|
|
15
|
+
Servers now record the directory they were started in, and `hj` picks the live server whose
|
|
16
|
+
directory is the nearest ancestor of your cwd. Inside a project with its own server, plain
|
|
17
|
+
`hj tree` just works — no flags, no env vars.
|
|
18
|
+
|
|
19
|
+
- Falling back to the shared default port while other servers are running now **warns** on
|
|
20
|
+
stderr instead of quietly misrouting.
|
|
21
|
+
- **`hj where`** tells you which port you're targeting, *why* it was chosen, and what's alive
|
|
22
|
+
there. Reach for it first when a command seems to hit the wrong page.
|
|
23
|
+
- Precedence is unchanged and still wins: `--port` > `--name`/`HALTIJA_NAME` > `HALTIJA_PORT` >
|
|
24
|
+
`DEV_CHANNEL_PORT` > cwd match > port 8700.
|
|
25
|
+
|
|
26
|
+
### Fixed: a stale server could hand every project an old `hj`
|
|
27
|
+
|
|
28
|
+
`hj` is a single binary on your `PATH`, and every haltija server used to overwrite it on
|
|
29
|
+
startup — so the last server to boot decided which `hj` *every project on the machine* ran. One
|
|
30
|
+
forgotten `bunx haltija@beta` could silently downgrade the CLI for an unrelated, up-to-date
|
|
31
|
+
project.
|
|
32
|
+
|
|
33
|
+
- A **symlinked `hj` is never touched.** Point it at your own build and it stays put.
|
|
34
|
+
- Servers **only bootstrap or repair** `hj` — they write it when nothing is there, or when
|
|
35
|
+
what's there is strictly older. They never downgrade it, and never rewrite it just because
|
|
36
|
+
the bytes differ. To find out what's installed they ask it (`hj --version`).
|
|
37
|
+
- **`hj --version`**, and `hj` now warns when its version differs from the server it's driving.
|
|
38
|
+
- `HALTIJA_NO_INSTALL=1` opts out of the install entirely.
|
|
39
|
+
|
|
40
|
+
### New: pre-1.4.0 servers are retired on startup
|
|
41
|
+
|
|
42
|
+
Older servers have none of the guards above and cannot be fixed in code that already shipped, so
|
|
43
|
+
a 1.4.0+ server **asks** any haltija server **below 1.4.0** to stop when it starts, and says what
|
|
44
|
+
it did. Retirement is `POST /shutdown` — an endpoint every haltija has understood since 0.1.7 —
|
|
45
|
+
so it needs no process IDs and does no killing.
|
|
46
|
+
|
|
47
|
+
This is deliberately narrow: it never stops a peer (1.4.0 and 1.4.1 coexist, and once 1.3.x is
|
|
48
|
+
gone it never fires again), never touches a running desktop app, and never touches anything it
|
|
49
|
+
cannot identify as haltija. When it can't stop a server, it complains rather than failing
|
|
50
|
+
silently.
|
|
51
|
+
|
|
52
|
+
`HALTIJA_NO_RETIRE=1` opts out. See "Housekeeping" in the README.
|
|
53
|
+
|
|
54
|
+
### Also
|
|
55
|
+
|
|
56
|
+
- HTTPS-only servers no longer advertise an HTTP port they aren't listening on.
|
|
57
|
+
- Every REST response carries `X-Haltija-Version`.
|
|
58
|
+
- **`hj` exits non-zero when an operation fails** (`success:false`) — not just with `--json`, but on action commands too (`hj click`, `navigate`, `key`, …). A click that didn't land, or any command with no browser connected, now exits 1 instead of 0, so an agent checking the exit code can't read a failed step as success. (Commands with their own human formatting were already this way; this closes the gap for the rest.)
|
|
59
|
+
- `HALTIJA_REGISTRY_DIR` overrides the instance-registry location.
|
|
60
|
+
- `hj` no longer auto-spawns a server against an **explicitly targeted** port (`--port`/`--name`/`HALTIJA_PORT`) or under `--no-launch` — a read-only command against a server you manage will not start a colliding one; it errors instead. Auto-spawn remains only for the bare default port.
|
|
61
|
+
|
|
62
|
+
### Platform
|
|
63
|
+
|
|
64
|
+
macOS and Linux. Native Windows is not supported — use WSL, where all of this works unmodified.
|
package/README.md
CHANGED
|
@@ -249,7 +249,11 @@ export HALTIJA_PORT=9123
|
|
|
249
249
|
hj tree
|
|
250
250
|
```
|
|
251
251
|
|
|
252
|
-
If you don't pass `--port`, haltija tries 8700 first and falls back to a kernel-assigned ephemeral port
|
|
252
|
+
If you don't pass `--port`, haltija tries 8700 first and falls back to a kernel-assigned ephemeral port.
|
|
253
|
+
|
|
254
|
+
**You usually don't need `--name` at all.** A server records the directory it was started in, so **plain `hj` inside a project reaches that project's server** — no flags, no environment variables. `--name` and `--port` are overrides for when you want to address a server from *outside* its directory. `hj where` shows which server a shell is targeting and why.
|
|
255
|
+
|
|
256
|
+
Haltija does keep a little state outside your project — a shared `hj` on your PATH and a registry of running servers. That's deliberate, and it's all logged: see [Housekeeping](#housekeeping--what-haltija-does-to-your-machine) below.
|
|
253
257
|
|
|
254
258
|
**Production embedding.** When haltija is reachable beyond loopback, gate it with a shared-secret token:
|
|
255
259
|
|
|
@@ -302,6 +306,50 @@ app also exists but isn't required for either.
|
|
|
302
306
|
|
|
303
307
|
---
|
|
304
308
|
|
|
309
|
+
## Housekeeping — what Haltija does to your machine
|
|
310
|
+
|
|
311
|
+
Haltija acts at **machine** scope on purpose. "Which `hj` does every shell on this box
|
|
312
|
+
run?" is not a question a per-project fix can answer — so a few things live outside your
|
|
313
|
+
project directory. All of it is opt-out, and **all of it leaves a receipt.**
|
|
314
|
+
|
|
315
|
+
**Everything Haltija does outside its own project is logged to
|
|
316
|
+
`~/.haltija/machine-actions.log`** (timestamped, with the version and the *directory of the
|
|
317
|
+
project that triggered it*) and announced on **stderr**. That matters because Haltija is
|
|
318
|
+
often a transitive dependency: if some other project's `test-browser` script spawned it,
|
|
319
|
+
you may not know what Haltija even is. The receipt tells you what happened, when, and which
|
|
320
|
+
project caused it.
|
|
321
|
+
|
|
322
|
+
**It installs `hj` into `~/.local/bin`.** One CLI, shared by every project. It will
|
|
323
|
+
**never overwrite a symlink** (if you point `hj` at your own build, that's yours) and
|
|
324
|
+
**never downgrade** a newer `hj` than the one it carries. `HALTIJA_NO_INSTALL=1` disables it.
|
|
325
|
+
|
|
326
|
+
**It registers itself in `~/.haltija/servers/`,** recording the directory it was started
|
|
327
|
+
in. That's what lets plain `hj` inside a project reach *that project's* server instead of
|
|
328
|
+
whichever browser happens to be focused somewhere else. The entry is removed on shutdown.
|
|
329
|
+
`hj where` shows what your shell is targeting and why.
|
|
330
|
+
|
|
331
|
+
**It stops haltija servers older than 1.4.0.** Those versions overwrite the shared
|
|
332
|
+
`~/.local/bin/hj` on every boot, so one stale server left running quietly hands every
|
|
333
|
+
project on your machine an old CLI. A 1.4.0+ server **asks** them to stop on startup
|
|
334
|
+
(`POST /shutdown`, which every haltija has understood since 0.1.7) and says so. It does
|
|
335
|
+
not go hunting for processes to kill. It is deliberately narrow:
|
|
336
|
+
|
|
337
|
+
- It only stops servers **below 1.4.0** — never a peer. Two projects on 1.4.0 and 1.4.1
|
|
338
|
+
coexist; nothing kills anything once 1.3.x is gone.
|
|
339
|
+
- It **will not touch a running desktop app** (that would orphan a window you can see) —
|
|
340
|
+
it tells you to quit and update it instead.
|
|
341
|
+
- It **asks rather than kills.** Retirement is an HTTP request to a server that already
|
|
342
|
+
told us what it is — no process IDs, no `kill`. (An earlier draft resolved a port to a
|
|
343
|
+
pid with `lsof` and signalled it; `lsof -i :PORT` also matches connected *clients*, so
|
|
344
|
+
that would have killed your **browser**. Asking makes that whole class of mistake
|
|
345
|
+
impossible.)
|
|
346
|
+
- On the rare fallback paths that must free a port without waiting, it signals only
|
|
347
|
+
**listeners** that `ps` confirms are haltija — never something it cannot identify.
|
|
348
|
+
|
|
349
|
+
`HALTIJA_NO_RETIRE=1` disables it.
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
305
353
|
## Use Cases
|
|
306
354
|
|
|
307
355
|
- **AI pair programming** — Agent sees your actual app, not a description of it
|
package/apps/desktop/main.js
CHANGED
|
@@ -1292,39 +1292,90 @@ async function startEmbeddedServer() {
|
|
|
1292
1292
|
/**
|
|
1293
1293
|
* Ensure Haltija server is available
|
|
1294
1294
|
*/
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1295
|
+
/** Is anything listening on this port? Listeners only — never connected clients. */
|
|
1296
|
+
function listenerPidsOnPort(port) {
|
|
1298
1297
|
const { execSync } = require('child_process')
|
|
1298
|
+
try {
|
|
1299
|
+
// `-sTCP:LISTEN` is load-bearing. `lsof -i :PORT` matches sockets whose local OR
|
|
1300
|
+
// REMOTE port is PORT, so without it this also returns every connected CLIENT —
|
|
1301
|
+
// i.e. the user's browser, holding a WebSocket to this very server. Browsers open
|
|
1302
|
+
// since login have the lower pid and sort first, so killing what this returns used
|
|
1303
|
+
// to kill the browser and leave the server running.
|
|
1304
|
+
const out = execSync(`lsof -ti:${port} -sTCP:LISTEN 2>/dev/null`, { encoding: 'utf-8' }).trim()
|
|
1305
|
+
if (!out) return []
|
|
1306
|
+
return out.split('\n').filter(Boolean).map(Number)
|
|
1307
|
+
.filter(pid => Number.isFinite(pid) && pid !== process.pid)
|
|
1308
|
+
} catch {
|
|
1309
|
+
return []
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1299
1312
|
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1313
|
+
/** Only signal something we can positively identify as haltija. */
|
|
1314
|
+
function isHaltijaProcess(pid) {
|
|
1315
|
+
const { execSync } = require('child_process')
|
|
1316
|
+
try {
|
|
1317
|
+
const cmd = execSync(`ps -p ${pid} -o command= 2>/dev/null`, { encoding: 'utf-8' }).trim()
|
|
1318
|
+
return !!cmd && /haltija|tosijs-dev/i.test(cmd)
|
|
1319
|
+
} catch {
|
|
1320
|
+
return false
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1309
1323
|
|
|
1310
|
-
|
|
1311
|
-
|
|
1324
|
+
/**
|
|
1325
|
+
* Free a port held by an old haltija server.
|
|
1326
|
+
*
|
|
1327
|
+
* ASK FIRST. `POST /shutdown` has shipped since 0.1.7, so any haltija server on this
|
|
1328
|
+
* port can stop itself — cleanly, and with no pid involved. That matters: this used to
|
|
1329
|
+
* run `lsof -ti:PORT | xargs kill -9`, which kills every pid matching the port
|
|
1330
|
+
* INCLUDING connected clients, i.e. the user's browser. It ran on the default launch
|
|
1331
|
+
* path, whenever a server was already up — exactly when browsers are attached.
|
|
1332
|
+
*
|
|
1333
|
+
* Signalling is now the fallback only, restricted to LISTENERS that `ps` confirms are
|
|
1334
|
+
* haltija, and it never escalates to -9 against something it could not identify.
|
|
1335
|
+
*/
|
|
1336
|
+
async function killZombieOnPort(port) {
|
|
1337
|
+
if (os.platform() === 'win32') return true
|
|
1312
1338
|
|
|
1313
|
-
|
|
1339
|
+
if (listenerPidsOnPort(port).length === 0) {
|
|
1340
|
+
console.log(`[Haltija Desktop] Port ${port} is free`)
|
|
1341
|
+
return true
|
|
1342
|
+
}
|
|
1314
1343
|
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1344
|
+
// 1. Ask.
|
|
1345
|
+
try {
|
|
1346
|
+
await fetch(`http://localhost:${port}/shutdown`, {
|
|
1347
|
+
method: 'POST',
|
|
1348
|
+
signal: AbortSignal.timeout(1500),
|
|
1349
|
+
})
|
|
1350
|
+
} catch {
|
|
1351
|
+
// Dying mid-response is a success; the liveness check below decides.
|
|
1352
|
+
}
|
|
1353
|
+
for (let i = 0; i < 20; i++) {
|
|
1354
|
+
if (listenerPidsOnPort(port).length === 0) {
|
|
1355
|
+
console.log(`[Haltija Desktop] Server on port ${port} shut down cleanly`)
|
|
1323
1356
|
return true
|
|
1324
1357
|
}
|
|
1358
|
+
await new Promise(r => setTimeout(r, 100))
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// 2. It didn't answer. Signal only what we can identify.
|
|
1362
|
+
for (const pid of listenerPidsOnPort(port)) {
|
|
1363
|
+
if (!isHaltijaProcess(pid)) {
|
|
1364
|
+
console.error(`[Haltija Desktop] Port ${port} is held by pid ${pid}, which is not a haltija server — leaving it alone`)
|
|
1365
|
+
continue
|
|
1366
|
+
}
|
|
1367
|
+
try {
|
|
1368
|
+
process.kill(pid, 'SIGTERM')
|
|
1369
|
+
console.log(`[Haltija Desktop] Stopped unresponsive haltija (pid ${pid}) on port ${port}`)
|
|
1370
|
+
} catch {}
|
|
1325
1371
|
}
|
|
1372
|
+
await new Promise(r => setTimeout(r, 500))
|
|
1326
1373
|
|
|
1327
|
-
|
|
1374
|
+
if (listenerPidsOnPort(port).length === 0) {
|
|
1375
|
+
console.log(`[Haltija Desktop] Port ${port} freed`)
|
|
1376
|
+
return true
|
|
1377
|
+
}
|
|
1378
|
+
console.error(`[Haltija Desktop] Warning: could not free port ${port}`)
|
|
1328
1379
|
return false
|
|
1329
1380
|
}
|
|
1330
1381
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "haltija-desktop",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Haltija Desktop - God Mode Browser for AI Agents",
|
|
6
6
|
"homepage": "https://github.com/tonioloewald/haltija",
|
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
},
|
|
11
11
|
"main": "main.js",
|
|
12
12
|
"scripts": {
|
|
13
|
-
"start": "cd ../.. && bun run build && cd apps/desktop && electron .",
|
|
13
|
+
"start": "cd ../.. && bun run build && rm -f apps/desktop/resources/haltija-server-* && cd apps/desktop && electron .",
|
|
14
14
|
"start:quick": "electron .",
|
|
15
15
|
"icons": "electron scripts/generate-icons.js",
|
|
16
16
|
"sync-version": "node -e \"const root = require('../../package.json'); const pkg = require('./package.json'); pkg.version = root.version; require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2) + '\\n')\"",
|
|
17
17
|
"prebuild": "npm run sync-version && npm run icons && npm run compile:server",
|
|
18
|
-
"compile:server": "cd ../.. && bun run build && bun build --compile --target=bun-darwin-arm64 src/server.ts --outfile apps/desktop/resources/haltija-server-arm64 && bun build --compile --target=bun-darwin-x64 src/server.ts --outfile apps/desktop/resources/haltija-server-x64 &&
|
|
18
|
+
"compile:server": "cd ../.. && bun run build && bun build --compile --target=bun-darwin-arm64 src/server.ts --outfile apps/desktop/resources/haltija-server-arm64 && bun build --compile --target=bun-darwin-x64 src/server.ts --outfile apps/desktop/resources/haltija-server-x64 && cp dist/component.js apps/desktop/resources/component.js && cp haltija-icon.svg apps/desktop/resources/icon.svg",
|
|
19
19
|
"compile:server:win": "cd ../.. && bun build --compile --target=bun-windows-x64 src/server.ts --outfile apps/desktop/resources/haltija-server-x64.exe",
|
|
20
20
|
"compile:server:linux": "cd ../.. && bun build --compile --target=bun-linux-x64 src/server.ts --outfile apps/desktop/resources/haltija-server-x64 && bun build --compile --target=bun-linux-arm64 src/server.ts --outfile apps/desktop/resources/haltija-server-arm64",
|
|
21
21
|
"build": "electron-builder",
|
|
@@ -94,8 +94,8 @@
|
|
|
94
94
|
"to": "haltija-server-${arch}"
|
|
95
95
|
},
|
|
96
96
|
{
|
|
97
|
-
"from": "resources/hj
|
|
98
|
-
"to": "hj
|
|
97
|
+
"from": "resources/hj.mjs",
|
|
98
|
+
"to": "hj.mjs"
|
|
99
99
|
},
|
|
100
100
|
{
|
|
101
101
|
"from": "resources/component.js",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
// src/version.ts
|
|
49
|
-
var VERSION = "1.
|
|
49
|
+
var VERSION = "1.4.0";
|
|
50
50
|
|
|
51
51
|
// src/text-selector.ts
|
|
52
52
|
var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\(/;
|
|
@@ -598,13 +598,13 @@
|
|
|
598
598
|
}
|
|
599
599
|
function resolveRefOrSelector(ref, selector) {
|
|
600
600
|
if (ref) {
|
|
601
|
-
const
|
|
602
|
-
if (
|
|
601
|
+
const result = refRegistry.resolveWithStatus(ref);
|
|
602
|
+
if (result.element) {
|
|
603
603
|
stats.refsResolved++;
|
|
604
|
-
return { element:
|
|
604
|
+
return { element: result.element, targetDesc: `@${ref}` };
|
|
605
605
|
}
|
|
606
606
|
stats.refsStale++;
|
|
607
|
-
const errorMsg =
|
|
607
|
+
const errorMsg = result.status === "never_assigned" ? `Ref @${ref} was never assigned (highest ref is @${refRegistry.getStats().highWaterMark})` : result.status === "removed_from_dom" ? `Ref @${ref} points to an element that was removed from the DOM (try refreshing /tree)` : `Ref @${ref} is stale - element was garbage collected (try refreshing /tree)`;
|
|
608
608
|
return { element: null, targetDesc: `@${ref}`, error: errorMsg };
|
|
609
609
|
}
|
|
610
610
|
if (selector) {
|
|
@@ -615,13 +615,13 @@
|
|
|
615
615
|
}
|
|
616
616
|
function resolveRefOrSelectorAll(ref, selector) {
|
|
617
617
|
if (ref) {
|
|
618
|
-
const
|
|
619
|
-
if (
|
|
618
|
+
const result = refRegistry.resolveWithStatus(ref);
|
|
619
|
+
if (result.element) {
|
|
620
620
|
stats.refsResolved++;
|
|
621
|
-
return { elements: [
|
|
621
|
+
return { elements: [result.element], targetDesc: `@${ref}` };
|
|
622
622
|
}
|
|
623
623
|
stats.refsStale++;
|
|
624
|
-
const errorMsg =
|
|
624
|
+
const errorMsg = result.status === "never_assigned" ? `Ref @${ref} was never assigned (highest ref is @${refRegistry.getStats().highWaterMark})` : result.status === "removed_from_dom" ? `Ref @${ref} points to an element that was removed from the DOM (try refreshing /tree)` : `Ref @${ref} is stale - element was garbage collected (try refreshing /tree)`;
|
|
625
625
|
return { elements: [], targetDesc: `@${ref}`, error: errorMsg };
|
|
626
626
|
}
|
|
627
627
|
if (selector) {
|
|
@@ -2870,8 +2870,8 @@
|
|
|
2870
2870
|
headers: { "Content-Type": "application/json" },
|
|
2871
2871
|
body: JSON.stringify({ action: "start", window: this.windowId })
|
|
2872
2872
|
});
|
|
2873
|
-
const
|
|
2874
|
-
if (
|
|
2873
|
+
const result = await response.json();
|
|
2874
|
+
if (result.success) {
|
|
2875
2875
|
this.isRecording = true;
|
|
2876
2876
|
this.serverManagedRecording = true;
|
|
2877
2877
|
this.recordingStartTime = Date.now();
|
|
@@ -2883,7 +2883,7 @@
|
|
|
2883
2883
|
this.updateRecordingUI(true);
|
|
2884
2884
|
console.log("[Haltija] Recording started (server-managed)");
|
|
2885
2885
|
} else {
|
|
2886
|
-
console.error("[Haltija] Failed to start recording:",
|
|
2886
|
+
console.error("[Haltija] Failed to start recording:", result.error);
|
|
2887
2887
|
}
|
|
2888
2888
|
} catch (err) {
|
|
2889
2889
|
console.error("[Haltija] Failed to start recording:", err);
|
|
@@ -2898,10 +2898,10 @@
|
|
|
2898
2898
|
headers: { "Content-Type": "application/json" },
|
|
2899
2899
|
body: JSON.stringify({ action: "stop", window: this.windowId })
|
|
2900
2900
|
});
|
|
2901
|
-
const
|
|
2902
|
-
if (
|
|
2903
|
-
this.recordingEvents =
|
|
2904
|
-
this.recordingStartTime =
|
|
2901
|
+
const result = await response.json();
|
|
2902
|
+
if (result.success && result.data) {
|
|
2903
|
+
this.recordingEvents = result.data.events || [];
|
|
2904
|
+
this.recordingStartTime = result.data.startTime || this.recordingStartTime;
|
|
2905
2905
|
const recordingId = `rec_${this.recordingStartTime}_${Math.random().toString(36).slice(2, 8)}`;
|
|
2906
2906
|
this.lastRecordingId = recordingId;
|
|
2907
2907
|
console.log(`[Haltija] Recording stopped (${this.recordingEvents.length} events)`);
|
|
@@ -3214,15 +3214,15 @@
|
|
|
3214
3214
|
headers: { "Content-Type": "application/json" },
|
|
3215
3215
|
body: JSON.stringify({ description, agentId })
|
|
3216
3216
|
});
|
|
3217
|
-
const
|
|
3218
|
-
if (
|
|
3217
|
+
const result = await response.json();
|
|
3218
|
+
if (result.error) {
|
|
3219
3219
|
if (successMsg) {
|
|
3220
|
-
successMsg.textContent = `⚠️ ${
|
|
3220
|
+
successMsg.textContent = `⚠️ ${result.error}`;
|
|
3221
3221
|
successMsg.style.color = "#f59e0b";
|
|
3222
3222
|
}
|
|
3223
3223
|
} else {
|
|
3224
3224
|
if (successMsg) {
|
|
3225
|
-
successMsg.textContent = `✓ Sent to ${agentName}${
|
|
3225
|
+
successMsg.textContent = `✓ Sent to ${agentName}${result.interrupted ? " (interrupted)" : ""}`;
|
|
3226
3226
|
successMsg.style.color = "#22c55e";
|
|
3227
3227
|
}
|
|
3228
3228
|
setTimeout(() => this.closeTestModal(), 1500);
|
|
@@ -3753,13 +3753,13 @@
|
|
|
3753
3753
|
const haltija = window.haltija;
|
|
3754
3754
|
if (haltija?.openAgentTab) {
|
|
3755
3755
|
newAgentText.textContent = "Creating...";
|
|
3756
|
-
const
|
|
3757
|
-
if (
|
|
3756
|
+
const result = await haltija.openAgentTab();
|
|
3757
|
+
if (result?.shellId) {
|
|
3758
3758
|
await new Promise((r) => setTimeout(r, 300));
|
|
3759
3759
|
if (type === "selection") {
|
|
3760
|
-
this.sendSelectionToAgent(
|
|
3760
|
+
this.sendSelectionToAgent(result.shellId, result.name || "agent");
|
|
3761
3761
|
} else if (type === "recording") {
|
|
3762
|
-
this.sendRecordingToAgent(
|
|
3762
|
+
this.sendRecordingToAgent(result.shellId, result.name || "agent");
|
|
3763
3763
|
}
|
|
3764
3764
|
} else {
|
|
3765
3765
|
console.error(`${LOG_PREFIX} Failed to create agent tab`);
|
|
@@ -3828,8 +3828,8 @@ ${elementSummary}${moreText}`;
|
|
|
3828
3828
|
context: `selection from ${window.location.host}`
|
|
3829
3829
|
})
|
|
3830
3830
|
});
|
|
3831
|
-
const
|
|
3832
|
-
if (
|
|
3831
|
+
const result = await response.json();
|
|
3832
|
+
if (result.sent) {
|
|
3833
3833
|
this.clearSelection();
|
|
3834
3834
|
console.log(`${LOG_PREFIX} Selection sent to ${agentName}`);
|
|
3835
3835
|
}
|
|
@@ -5344,11 +5344,11 @@ ${elementSummary}${moreText}`;
|
|
|
5344
5344
|
this.respond(msg2.id, false, undefined, "Video capture requires the Haltija Desktop app. Run: npx haltija@latest -f");
|
|
5345
5345
|
return;
|
|
5346
5346
|
}
|
|
5347
|
-
haltija.startVideoCapture({ maxDuration: payload2.maxDuration }).then((
|
|
5348
|
-
if (
|
|
5349
|
-
this.respond(msg2.id, true, { recordingId:
|
|
5347
|
+
haltija.startVideoCapture({ maxDuration: payload2.maxDuration }).then((result) => {
|
|
5348
|
+
if (result.success) {
|
|
5349
|
+
this.respond(msg2.id, true, { recordingId: result.recordingId });
|
|
5350
5350
|
} else {
|
|
5351
|
-
this.respond(msg2.id, false, undefined,
|
|
5351
|
+
this.respond(msg2.id, false, undefined, result.error || "Failed to start video");
|
|
5352
5352
|
}
|
|
5353
5353
|
}).catch((err) => {
|
|
5354
5354
|
this.respond(msg2.id, false, undefined, `Failed to start video: ${err.message}`);
|
|
@@ -5358,11 +5358,11 @@ ${elementSummary}${moreText}`;
|
|
|
5358
5358
|
this.respond(msg2.id, false, undefined, "Video capture requires the Haltija Desktop app. Run: npx haltija@latest -f");
|
|
5359
5359
|
return;
|
|
5360
5360
|
}
|
|
5361
|
-
haltija.stopVideoCapture().then((
|
|
5362
|
-
if (
|
|
5363
|
-
this.respond(msg2.id, true, { path:
|
|
5361
|
+
haltija.stopVideoCapture().then((result) => {
|
|
5362
|
+
if (result.success) {
|
|
5363
|
+
this.respond(msg2.id, true, { path: result.path, duration: result.duration, size: result.size, format: result.format });
|
|
5364
5364
|
} else {
|
|
5365
|
-
this.respond(msg2.id, false, undefined,
|
|
5365
|
+
this.respond(msg2.id, false, undefined, result.error || "Failed to stop video");
|
|
5366
5366
|
}
|
|
5367
5367
|
}).catch((err) => {
|
|
5368
5368
|
this.respond(msg2.id, false, undefined, `Failed to stop video: ${err.message}`);
|
|
@@ -5372,8 +5372,8 @@ ${elementSummary}${moreText}`;
|
|
|
5372
5372
|
this.respond(msg2.id, true, { recording: false });
|
|
5373
5373
|
return;
|
|
5374
5374
|
}
|
|
5375
|
-
haltija.videoStatus().then((
|
|
5376
|
-
this.respond(msg2.id, true,
|
|
5375
|
+
haltija.videoStatus().then((result) => {
|
|
5376
|
+
this.respond(msg2.id, true, result);
|
|
5377
5377
|
}).catch(() => {
|
|
5378
5378
|
this.respond(msg2.id, true, { recording: false });
|
|
5379
5379
|
});
|
|
@@ -5390,8 +5390,8 @@ ${elementSummary}${moreText}`;
|
|
|
5390
5390
|
this.respond(msg2.id, false, undefined, notAvailable);
|
|
5391
5391
|
return;
|
|
5392
5392
|
}
|
|
5393
|
-
haltija.networkWatch(payload2 || {}).then((
|
|
5394
|
-
this.respond(msg2.id,
|
|
5393
|
+
haltija.networkWatch(payload2 || {}).then((result) => {
|
|
5394
|
+
this.respond(msg2.id, result.success !== false, result);
|
|
5395
5395
|
}).catch((err) => {
|
|
5396
5396
|
this.respond(msg2.id, false, undefined, err.message);
|
|
5397
5397
|
});
|
|
@@ -5400,8 +5400,8 @@ ${elementSummary}${moreText}`;
|
|
|
5400
5400
|
this.respond(msg2.id, false, undefined, notAvailable);
|
|
5401
5401
|
return;
|
|
5402
5402
|
}
|
|
5403
|
-
haltija.networkUnwatch().then((
|
|
5404
|
-
this.respond(msg2.id, true,
|
|
5403
|
+
haltija.networkUnwatch().then((result) => {
|
|
5404
|
+
this.respond(msg2.id, true, result);
|
|
5405
5405
|
}).catch((err) => {
|
|
5406
5406
|
this.respond(msg2.id, false, undefined, err.message);
|
|
5407
5407
|
});
|
|
@@ -5410,8 +5410,8 @@ ${elementSummary}${moreText}`;
|
|
|
5410
5410
|
this.respond(msg2.id, false, undefined, notAvailable);
|
|
5411
5411
|
return;
|
|
5412
5412
|
}
|
|
5413
|
-
haltija.networkLog(payload2 || {}).then((
|
|
5414
|
-
this.respond(msg2.id, true,
|
|
5413
|
+
haltija.networkLog(payload2 || {}).then((result) => {
|
|
5414
|
+
this.respond(msg2.id, true, result);
|
|
5415
5415
|
}).catch((err) => {
|
|
5416
5416
|
this.respond(msg2.id, false, undefined, err.message);
|
|
5417
5417
|
});
|
|
@@ -5420,8 +5420,8 @@ ${elementSummary}${moreText}`;
|
|
|
5420
5420
|
this.respond(msg2.id, false, undefined, notAvailable);
|
|
5421
5421
|
return;
|
|
5422
5422
|
}
|
|
5423
|
-
haltija.networkStats().then((
|
|
5424
|
-
this.respond(msg2.id, true,
|
|
5423
|
+
haltija.networkStats().then((result) => {
|
|
5424
|
+
this.respond(msg2.id, true, result);
|
|
5425
5425
|
}).catch((err) => {
|
|
5426
5426
|
this.respond(msg2.id, false, undefined, err.message);
|
|
5427
5427
|
});
|
|
@@ -5430,8 +5430,8 @@ ${elementSummary}${moreText}`;
|
|
|
5430
5430
|
this.respond(msg2.id, false, undefined, notAvailable);
|
|
5431
5431
|
return;
|
|
5432
5432
|
}
|
|
5433
|
-
haltija.networkClear().then((
|
|
5434
|
-
this.respond(msg2.id, true,
|
|
5433
|
+
haltija.networkClear().then((result) => {
|
|
5434
|
+
this.respond(msg2.id, true, result);
|
|
5435
5435
|
}).catch((err) => {
|
|
5436
5436
|
this.respond(msg2.id, false, undefined, err.message);
|
|
5437
5437
|
});
|
|
@@ -5877,9 +5877,9 @@ ${elementSummary}${moreText}`;
|
|
|
5877
5877
|
try {
|
|
5878
5878
|
const capturePromise = targetSelector ? haltija.captureElement(targetSelector) : haltija.capturePage();
|
|
5879
5879
|
const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve({ success: false, error: "Screenshot capture timed out after 10s" }), 1e4));
|
|
5880
|
-
const
|
|
5881
|
-
if (
|
|
5882
|
-
const converted = await convertFormat(
|
|
5880
|
+
const result = await Promise.race([capturePromise, timeoutPromise]);
|
|
5881
|
+
if (result?.success && result.data) {
|
|
5882
|
+
const converted = await convertFormat(result.data);
|
|
5883
5883
|
this.respond(msg2.id, true, {
|
|
5884
5884
|
image: converted.image,
|
|
5885
5885
|
viewport,
|
|
@@ -5889,8 +5889,8 @@ ${elementSummary}${moreText}`;
|
|
|
5889
5889
|
source: "electron"
|
|
5890
5890
|
});
|
|
5891
5891
|
return;
|
|
5892
|
-
} else if (
|
|
5893
|
-
this.respond(msg2.id, false, null,
|
|
5892
|
+
} else if (result && !result.success) {
|
|
5893
|
+
this.respond(msg2.id, false, null, result.error || "Electron capture failed");
|
|
5894
5894
|
return;
|
|
5895
5895
|
}
|
|
5896
5896
|
} catch (err) {
|
|
@@ -5961,14 +5961,29 @@ ${elementSummary}${moreText}`;
|
|
|
5961
5961
|
this.respond(msg2.id, true);
|
|
5962
5962
|
}
|
|
5963
5963
|
}
|
|
5964
|
-
async handleEvalMessage(
|
|
5964
|
+
async handleEvalMessage(msg2) {
|
|
5965
5965
|
try {
|
|
5966
|
-
const result =
|
|
5966
|
+
const result = this.evalCode(msg2.payload.code);
|
|
5967
5967
|
const resolved = result instanceof Promise ? await result : result;
|
|
5968
|
-
this.respond(
|
|
5968
|
+
this.respond(msg2.id, true, resolved);
|
|
5969
5969
|
} catch (err) {
|
|
5970
|
-
this.respond(
|
|
5970
|
+
this.respond(msg2.id, false, null, err.message);
|
|
5971
|
+
}
|
|
5972
|
+
}
|
|
5973
|
+
evalCode(code) {
|
|
5974
|
+
try {
|
|
5975
|
+
return eval(code);
|
|
5976
|
+
} catch (err) {
|
|
5977
|
+
if (!(err instanceof SyntaxError) || !/\b(await|return)\b/.test(code))
|
|
5978
|
+
throw err;
|
|
5979
|
+
}
|
|
5980
|
+
try {
|
|
5981
|
+
return eval(`(async () => (${code}))()`);
|
|
5982
|
+
} catch (err) {
|
|
5983
|
+
if (!(err instanceof SyntaxError))
|
|
5984
|
+
throw err;
|
|
5971
5985
|
}
|
|
5986
|
+
return eval(`(async () => { ${code} })()`);
|
|
5972
5987
|
}
|
|
5973
5988
|
async handleFetchMessage(msg2) {
|
|
5974
5989
|
const { url } = msg2.payload;
|
|
@@ -6046,14 +6061,14 @@ ${elementSummary}${moreText}`;
|
|
|
6046
6061
|
let el = null;
|
|
6047
6062
|
let targetDesc = "";
|
|
6048
6063
|
if (ref) {
|
|
6049
|
-
const
|
|
6050
|
-
el =
|
|
6064
|
+
const result = refRegistry.resolveWithStatus(ref);
|
|
6065
|
+
el = result.element;
|
|
6051
6066
|
targetDesc = `@${ref}`;
|
|
6052
6067
|
if (el) {
|
|
6053
6068
|
stats.refsResolved++;
|
|
6054
6069
|
} else {
|
|
6055
6070
|
stats.refsStale++;
|
|
6056
|
-
const errorMsg =
|
|
6071
|
+
const errorMsg = result.status === "never_assigned" ? `Ref @${ref} was never assigned (highest ref is @${refRegistry.getStats().highWaterMark})` : result.status === "removed_from_dom" ? `Ref @${ref} points to an element that was removed from the DOM (try refreshing /tree)` : `Ref @${ref} is stale - element was garbage collected (try refreshing /tree)`;
|
|
6057
6072
|
this.respond(responseId, false, null, errorMsg);
|
|
6058
6073
|
return;
|
|
6059
6074
|
}
|
|
@@ -6398,14 +6413,14 @@ ${elementSummary}${moreText}`;
|
|
|
6398
6413
|
let el = null;
|
|
6399
6414
|
let targetDesc = "";
|
|
6400
6415
|
if (payload2.ref) {
|
|
6401
|
-
const
|
|
6402
|
-
el =
|
|
6416
|
+
const result = refRegistry.resolveWithStatus(payload2.ref);
|
|
6417
|
+
el = result.element;
|
|
6403
6418
|
targetDesc = `@${payload2.ref}`;
|
|
6404
6419
|
if (el) {
|
|
6405
6420
|
stats.refsResolved++;
|
|
6406
6421
|
} else {
|
|
6407
6422
|
stats.refsStale++;
|
|
6408
|
-
const errorMsg =
|
|
6423
|
+
const errorMsg = result.status === "never_assigned" ? `Ref @${payload2.ref} was never assigned (highest ref is @${refRegistry.getStats().highWaterMark})` : result.status === "removed_from_dom" ? `Ref @${payload2.ref} points to an element that was removed from the DOM (try refreshing /tree)` : `Ref @${payload2.ref} is stale - element was garbage collected (try refreshing /tree)`;
|
|
6409
6424
|
this.respond(responseId, false, null, errorMsg);
|
|
6410
6425
|
return;
|
|
6411
6426
|
}
|
|
@@ -6474,15 +6489,15 @@ ${elementSummary}${moreText}`;
|
|
|
6474
6489
|
let target = null;
|
|
6475
6490
|
let targetDesc = "";
|
|
6476
6491
|
if (ref) {
|
|
6477
|
-
const
|
|
6478
|
-
target =
|
|
6492
|
+
const result = refRegistry.resolveWithStatus(ref);
|
|
6493
|
+
target = result.element;
|
|
6479
6494
|
targetDesc = `@${ref}`;
|
|
6480
6495
|
if (target) {
|
|
6481
6496
|
stats.refsResolved++;
|
|
6482
6497
|
target.focus();
|
|
6483
6498
|
} else {
|
|
6484
6499
|
stats.refsStale++;
|
|
6485
|
-
const errorMsg =
|
|
6500
|
+
const errorMsg = result.status === "never_assigned" ? `Ref @${ref} was never assigned (highest ref is @${refRegistry.getStats().highWaterMark})` : result.status === "removed_from_dom" ? `Ref @${ref} points to an element that was removed from the DOM (try refreshing /tree)` : `Ref @${ref} is stale - element was garbage collected (try refreshing /tree)`;
|
|
6486
6501
|
this.respond(responseId, false, null, errorMsg);
|
|
6487
6502
|
return;
|
|
6488
6503
|
}
|
|
@@ -6874,9 +6889,9 @@ ${elementSummary}${moreText}`;
|
|
|
6874
6889
|
});
|
|
6875
6890
|
} else if (action2 === "result") {
|
|
6876
6891
|
if (this.selectionResult) {
|
|
6877
|
-
const
|
|
6892
|
+
const result = this.selectionResult;
|
|
6878
6893
|
this.clearSelection();
|
|
6879
|
-
this.respond(msg2.id, true,
|
|
6894
|
+
this.respond(msg2.id, true, result);
|
|
6880
6895
|
} else {
|
|
6881
6896
|
this.respond(msg2.id, false, null, "No selection available");
|
|
6882
6897
|
}
|