haltija 1.2.4 → 1.2.5
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/apps/desktop/main.js +16 -3
- package/apps/desktop/package.json +1 -1
- package/apps/desktop/renderer.js +1 -1
- package/apps/desktop/resources/component.js +66 -6
- package/apps/desktop/webview-preload.js +1 -1
- package/bin/cli-subcommand.mjs +108 -6
- package/bin/format-tree.mjs +5 -3
- package/bin/hj.mjs +16 -1
- package/bin/tosijs-dev.mjs +2 -0
- package/dist/component.js +66 -6
- package/dist/hj.js +107 -8
- package/dist/index.js +195 -16
- package/dist/server.js +195 -16
- package/package.json +1 -1
package/apps/desktop/main.js
CHANGED
|
@@ -36,6 +36,10 @@ const HALTIJA_SERVER = `http://localhost:${HALTIJA_PORT}`
|
|
|
36
36
|
// Combined with webContents.id to create globally unique tab identifiers
|
|
37
37
|
const APP_INSTANCE_ID = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`
|
|
38
38
|
|
|
39
|
+
// Pending session: when an agent opens a tab with a session, store it here
|
|
40
|
+
// so injectWidget can include it in the widget config for the next new tab
|
|
41
|
+
let pendingTabSession = null
|
|
42
|
+
|
|
39
43
|
// ============================================
|
|
40
44
|
// Preferences
|
|
41
45
|
// ============================================
|
|
@@ -741,8 +745,16 @@ async function injectWidget(webContents) {
|
|
|
741
745
|
// This is stable across navigations (even cross-origin) enabling cross-page recording
|
|
742
746
|
const wsUrl = HALTIJA_SERVER.replace('http:', 'ws:') + '/ws/browser'
|
|
743
747
|
const windowId = `hj-${APP_INSTANCE_ID}-${webContents.id}`
|
|
748
|
+
const configObj = { serverUrl: wsUrl, windowId }
|
|
749
|
+
// Session priority: pending session from agent's tabs/open > env var
|
|
750
|
+
if (pendingTabSession) {
|
|
751
|
+
configObj.session = pendingTabSession
|
|
752
|
+
pendingTabSession = null // Consume — only applies to the next tab
|
|
753
|
+
} else if (process.env.HALTIJA_SESSION) {
|
|
754
|
+
configObj.session = process.env.HALTIJA_SESSION
|
|
755
|
+
}
|
|
744
756
|
await webContents.executeJavaScript(
|
|
745
|
-
`window.__haltija_config__ =
|
|
757
|
+
`window.__haltija_config__ = ${JSON.stringify(configObj)};`,
|
|
746
758
|
)
|
|
747
759
|
await webContents.executeJavaScript(componentCode)
|
|
748
760
|
|
|
@@ -882,9 +894,10 @@ function setupScreenCapture() {
|
|
|
882
894
|
})
|
|
883
895
|
|
|
884
896
|
// Tab management — forwarded to renderer
|
|
885
|
-
ipcMain.handle('open-tab', async (event, url) => {
|
|
897
|
+
ipcMain.handle('open-tab', async (event, url, session) => {
|
|
886
898
|
if (!mainWindow) return false
|
|
887
|
-
|
|
899
|
+
if (session) pendingTabSession = session
|
|
900
|
+
mainWindow.webContents.send('open-tab', { url, session })
|
|
888
901
|
return true
|
|
889
902
|
})
|
|
890
903
|
|
package/apps/desktop/renderer.js
CHANGED
|
@@ -130,7 +130,7 @@ document.addEventListener('keydown', (e) => {
|
|
|
130
130
|
// ============================================
|
|
131
131
|
|
|
132
132
|
// Tab management from widget (via main process IPC)
|
|
133
|
-
window.haltija?.onOpenTab?.((data) => createTab(data.url))
|
|
133
|
+
window.haltija?.onOpenTab?.((data) => createTab(data.url, { session: data.session }))
|
|
134
134
|
window.haltija?.onCloseTab?.((data) => {
|
|
135
135
|
const tab = findTabByWindowId(data.windowId)
|
|
136
136
|
if (tab) closeTab(tab.id)
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
// src/version.ts
|
|
49
|
-
var VERSION = "1.2.
|
|
49
|
+
var VERSION = "1.2.5";
|
|
50
50
|
|
|
51
51
|
// src/text-selector.ts
|
|
52
52
|
var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\(/;
|
|
@@ -1312,7 +1312,7 @@
|
|
|
1312
1312
|
}
|
|
1313
1313
|
function buildDomTree(el, options, currentDepth = 0) {
|
|
1314
1314
|
const {
|
|
1315
|
-
depth =
|
|
1315
|
+
depth = 5,
|
|
1316
1316
|
includeText = true,
|
|
1317
1317
|
allAttributes = false,
|
|
1318
1318
|
includeStyles = false,
|
|
@@ -1703,6 +1703,7 @@
|
|
|
1703
1703
|
windowId;
|
|
1704
1704
|
isElectron = false;
|
|
1705
1705
|
browserId = uid();
|
|
1706
|
+
sessionToken = "";
|
|
1706
1707
|
killed = false;
|
|
1707
1708
|
isActive = true;
|
|
1708
1709
|
homeLeft = 0;
|
|
@@ -1775,7 +1776,7 @@
|
|
|
1775
1776
|
selectionBox = null;
|
|
1776
1777
|
highlightedElements = [];
|
|
1777
1778
|
static get observedAttributes() {
|
|
1778
|
-
return ["server", "hidden"];
|
|
1779
|
+
return ["server", "hidden", "session"];
|
|
1779
1780
|
}
|
|
1780
1781
|
static async runTests() {
|
|
1781
1782
|
const el = document.querySelector(TAG_NAME);
|
|
@@ -1871,10 +1872,16 @@
|
|
|
1871
1872
|
}
|
|
1872
1873
|
this.windowId = storedWindowId;
|
|
1873
1874
|
}
|
|
1875
|
+
if (config?.session) {
|
|
1876
|
+
this.sessionToken = config.session;
|
|
1877
|
+
} else {
|
|
1878
|
+
this.sessionToken = uid();
|
|
1879
|
+
}
|
|
1874
1880
|
}
|
|
1875
1881
|
connectedCallback() {
|
|
1876
1882
|
this.killed = false;
|
|
1877
1883
|
this.serverUrl = this.getAttribute("server") || this.serverUrl;
|
|
1884
|
+
this.sessionToken = this.getAttribute("session") || this.sessionToken;
|
|
1878
1885
|
this.render();
|
|
1879
1886
|
const rect = this.getBoundingClientRect();
|
|
1880
1887
|
this.homeLeft = window.innerWidth - rect.width - 16;
|
|
@@ -1902,6 +1909,11 @@
|
|
|
1902
1909
|
this.connect();
|
|
1903
1910
|
}
|
|
1904
1911
|
}
|
|
1912
|
+
if (name === "session") {
|
|
1913
|
+
this.sessionToken = value;
|
|
1914
|
+
this.disconnect();
|
|
1915
|
+
this.connect();
|
|
1916
|
+
}
|
|
1905
1917
|
}
|
|
1906
1918
|
render() {
|
|
1907
1919
|
if (this.shadowRoot.querySelector(".widget")) {
|
|
@@ -2087,6 +2099,25 @@
|
|
|
2087
2099
|
.btn.info-btn:hover {
|
|
2088
2100
|
background: #2563eb;
|
|
2089
2101
|
}
|
|
2102
|
+
.session-badge {
|
|
2103
|
+
display: flex;
|
|
2104
|
+
align-items: center;
|
|
2105
|
+
gap: 2px;
|
|
2106
|
+
font-size: 9px;
|
|
2107
|
+
color: #666;
|
|
2108
|
+
background: rgba(255,255,255,0.05);
|
|
2109
|
+
padding: 2px 4px;
|
|
2110
|
+
border-radius: 3px;
|
|
2111
|
+
cursor: pointer;
|
|
2112
|
+
user-select: none;
|
|
2113
|
+
}
|
|
2114
|
+
.session-badge:hover {
|
|
2115
|
+
background: rgba(255,255,255,0.1);
|
|
2116
|
+
color: #aaa;
|
|
2117
|
+
}
|
|
2118
|
+
.session-badge.copied {
|
|
2119
|
+
color: #22c55e;
|
|
2120
|
+
}
|
|
2090
2121
|
@keyframes pulse {
|
|
2091
2122
|
0%, 100% { opacity: 1; }
|
|
2092
2123
|
50% { opacity: 0.5; }
|
|
@@ -2449,6 +2480,8 @@
|
|
|
2449
2480
|
<span class="logo">\uD83E\uDDDD</span>
|
|
2450
2481
|
</div>
|
|
2451
2482
|
<div class="title">${PRODUCT_NAME}</div>
|
|
2483
|
+
<span class="session-badge" data-action="copy-session" title="Session: ${this.sessionToken}
|
|
2484
|
+
Click to copy session command">${this.sessionToken.slice(0, 8)}</span>
|
|
2452
2485
|
<div class="controls">
|
|
2453
2486
|
<button class="btn" data-action="select" title="Select elements (drag to select area)" aria-label="Select elements">\uD83D\uDC46</button>
|
|
2454
2487
|
<button class="btn" data-action="record" title="Record test (click to start/stop)" aria-label="Record test">REC</button>
|
|
@@ -2535,6 +2568,8 @@
|
|
|
2535
2568
|
this.startSelection();
|
|
2536
2569
|
if (action2 === "stats")
|
|
2537
2570
|
this.copyStatsToClipboard();
|
|
2571
|
+
if (action2 === "copy-session")
|
|
2572
|
+
this.copySessionToken(e.currentTarget);
|
|
2538
2573
|
if (action2 === "close-modal")
|
|
2539
2574
|
this.closeTestModal();
|
|
2540
2575
|
if (action2 === "copy-test")
|
|
@@ -3160,6 +3195,28 @@
|
|
|
3160
3195
|
});
|
|
3161
3196
|
}
|
|
3162
3197
|
}
|
|
3198
|
+
async copySessionToken(badge) {
|
|
3199
|
+
const cmd = `export HALTIJA_SESSION=${this.sessionToken}`;
|
|
3200
|
+
try {
|
|
3201
|
+
await navigator.clipboard.writeText(cmd);
|
|
3202
|
+
badge.textContent = "copied!";
|
|
3203
|
+
badge.classList.add("copied");
|
|
3204
|
+
setTimeout(() => {
|
|
3205
|
+
badge.textContent = this.sessionToken.slice(0, 8);
|
|
3206
|
+
badge.classList.remove("copied");
|
|
3207
|
+
}, 1500);
|
|
3208
|
+
} catch {
|
|
3209
|
+
try {
|
|
3210
|
+
await navigator.clipboard.writeText(this.sessionToken);
|
|
3211
|
+
badge.textContent = "copied!";
|
|
3212
|
+
badge.classList.add("copied");
|
|
3213
|
+
setTimeout(() => {
|
|
3214
|
+
badge.textContent = this.sessionToken.slice(0, 8);
|
|
3215
|
+
badge.classList.remove("copied");
|
|
3216
|
+
}, 1500);
|
|
3217
|
+
} catch {}
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3163
3220
|
async copyStatsToClipboard() {
|
|
3164
3221
|
const refStats = refRegistry.getStats();
|
|
3165
3222
|
stats.refsStale = refStats.stale;
|
|
@@ -4926,6 +4983,7 @@ ${elementSummary}${moreText}`;
|
|
|
4926
4983
|
this.send("system", "connected", {
|
|
4927
4984
|
windowId: this.windowId,
|
|
4928
4985
|
browserId: this.browserId,
|
|
4986
|
+
session: this.sessionToken,
|
|
4929
4987
|
version: VERSION2,
|
|
4930
4988
|
serverSessionId: SERVER_SESSION_ID,
|
|
4931
4989
|
url: location.href,
|
|
@@ -5351,7 +5409,7 @@ ${elementSummary}${moreText}`;
|
|
|
5351
5409
|
const haltija = window.haltija;
|
|
5352
5410
|
if (action2 === "open") {
|
|
5353
5411
|
if (haltija?.openTab) {
|
|
5354
|
-
haltija.openTab(payload2.url).then((opened) => {
|
|
5412
|
+
haltija.openTab(payload2.url, payload2.session).then((opened) => {
|
|
5355
5413
|
this.respond(msg2.id, true, { opened });
|
|
5356
5414
|
}).catch((err) => {
|
|
5357
5415
|
this.respond(msg2.id, false, null, err.message);
|
|
@@ -7207,7 +7265,7 @@ ${elementSummary}${moreText}`;
|
|
|
7207
7265
|
}
|
|
7208
7266
|
registerDevChannel();
|
|
7209
7267
|
var WIDGET_ID = "haltija-widget";
|
|
7210
|
-
function inject(serverUrl2 = "wss://localhost:8700/ws/browser") {
|
|
7268
|
+
function inject(serverUrl2 = "wss://localhost:8700/ws/browser", options) {
|
|
7211
7269
|
const existing = document.getElementById(WIDGET_ID);
|
|
7212
7270
|
if (existing) {
|
|
7213
7271
|
console.log(`${LOG_PREFIX} Already injected`);
|
|
@@ -7222,6 +7280,8 @@ ${elementSummary}${moreText}`;
|
|
|
7222
7280
|
const el = DevChannel.elementCreator()();
|
|
7223
7281
|
el.id = WIDGET_ID;
|
|
7224
7282
|
el.setAttribute("server", serverUrl2);
|
|
7283
|
+
if (options?.session)
|
|
7284
|
+
el.setAttribute("session", options.session);
|
|
7225
7285
|
el.setAttribute("data-version", VERSION2);
|
|
7226
7286
|
document.body.appendChild(el);
|
|
7227
7287
|
console.log(`${LOG_PREFIX} Injected`);
|
|
@@ -7233,7 +7293,7 @@ ${elementSummary}${moreText}`;
|
|
|
7233
7293
|
const config = window.__haltija_config__;
|
|
7234
7294
|
if (config?.autoInject !== false) {
|
|
7235
7295
|
if (config) {
|
|
7236
|
-
inject(config.serverUrl || config.wsUrl);
|
|
7296
|
+
inject(config.serverUrl || config.wsUrl, { session: config.session });
|
|
7237
7297
|
return;
|
|
7238
7298
|
}
|
|
7239
7299
|
}
|
|
@@ -25,7 +25,7 @@ contextBridge.exposeInMainWorld('haltija', {
|
|
|
25
25
|
return ipcRenderer.invoke('navigate-url', url)
|
|
26
26
|
},
|
|
27
27
|
// Tab management — routed through main process to renderer
|
|
28
|
-
openTab: (url) => ipcRenderer.invoke('open-tab', url),
|
|
28
|
+
openTab: (url, session) => ipcRenderer.invoke('open-tab', url, session),
|
|
29
29
|
closeTab: (windowId) => ipcRenderer.invoke('close-tab', windowId),
|
|
30
30
|
focusTab: (windowId) => ipcRenderer.invoke('focus-tab', windowId),
|
|
31
31
|
openAgentTab: () => {
|
package/bin/cli-subcommand.mjs
CHANGED
|
@@ -202,6 +202,7 @@ export function parseTreeArgs(args) {
|
|
|
202
202
|
for (let i = 0; i < args.length; i++) {
|
|
203
203
|
const a = args[i]
|
|
204
204
|
if (a === '--depth' || a === '-d') { body.depth = num(args[++i]); continue }
|
|
205
|
+
if (a === '--all' || a === '-a') { body.depth = -1; continue }
|
|
205
206
|
if (a === '--selector' || a === '-s') { body.selector = args[++i]; continue }
|
|
206
207
|
if (a === '--compact' || a === '-c') { body.compact = true; continue }
|
|
207
208
|
if (a === '--visible') { body.visibleOnly = true; continue }
|
|
@@ -432,6 +433,18 @@ export function clean(obj) {
|
|
|
432
433
|
return Object.keys(result).length ? result : undefined
|
|
433
434
|
}
|
|
434
435
|
|
|
436
|
+
// ============================================
|
|
437
|
+
// Session ID for multi-agent isolation
|
|
438
|
+
// ============================================
|
|
439
|
+
|
|
440
|
+
export function getSessionId() {
|
|
441
|
+
if (process.env.HALTIJA_SESSION) return process.env.HALTIJA_SESSION
|
|
442
|
+
// Auto-generate a session ID for this shell — persisted in env for subsequent calls
|
|
443
|
+
const id = `hj_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`
|
|
444
|
+
process.env.HALTIJA_SESSION = id
|
|
445
|
+
return id
|
|
446
|
+
}
|
|
447
|
+
|
|
435
448
|
// ============================================
|
|
436
449
|
// Server auto-start
|
|
437
450
|
// ============================================
|
|
@@ -509,14 +522,92 @@ async function startServerInBackground(port) {
|
|
|
509
522
|
return false
|
|
510
523
|
}
|
|
511
524
|
|
|
525
|
+
// ============================================
|
|
526
|
+
// Auto-launch Electron app when no browser windows connected
|
|
527
|
+
// ============================================
|
|
528
|
+
|
|
529
|
+
async function launchElectronApp() {
|
|
530
|
+
const { execSync, spawn: spawnChild } = await import('child_process')
|
|
531
|
+
|
|
532
|
+
if (process.platform === 'darwin') {
|
|
533
|
+
// Check common locations for Haltija.app
|
|
534
|
+
const appPaths = [
|
|
535
|
+
'/Applications/Haltija.app',
|
|
536
|
+
`${process.env.HOME}/Applications/Haltija.app`,
|
|
537
|
+
]
|
|
538
|
+
for (const p of appPaths) {
|
|
539
|
+
if (existsSync(p)) {
|
|
540
|
+
spawnChild('open', ['-a', p], { stdio: 'ignore', detached: true }).unref()
|
|
541
|
+
return true
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
// Try spotlight search as fallback
|
|
545
|
+
try {
|
|
546
|
+
const result = execSync('mdfind "kMDItemCFBundleIdentifier == com.electron.haltija" | head -1', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim()
|
|
547
|
+
if (result) {
|
|
548
|
+
spawnChild('open', ['-a', result], { stdio: 'ignore', detached: true }).unref()
|
|
549
|
+
return true
|
|
550
|
+
}
|
|
551
|
+
} catch {}
|
|
552
|
+
return false
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Linux/Windows: not yet supported
|
|
556
|
+
return false
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async function ensureBrowserConnected(port) {
|
|
560
|
+
try {
|
|
561
|
+
const resp = await fetch(`http://localhost:${port}/status`, {
|
|
562
|
+
signal: AbortSignal.timeout(2000)
|
|
563
|
+
})
|
|
564
|
+
const status = await resp.json()
|
|
565
|
+
// Use status.ok (global, not session-filtered) to check if any browser is connected
|
|
566
|
+
if (status.ok) return true
|
|
567
|
+
} catch { return false }
|
|
568
|
+
|
|
569
|
+
// No windows connected — try to launch Electron app (macOS only)
|
|
570
|
+
if (process.platform !== 'darwin') return false
|
|
571
|
+
|
|
572
|
+
process.stderr.write('\x1b[2mLaunching Haltija browser...\x1b[0m')
|
|
573
|
+
const launched = await launchElectronApp()
|
|
574
|
+
if (!launched) {
|
|
575
|
+
process.stderr.write('\x1b[2m not found\x1b[0m\n')
|
|
576
|
+
return false
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Wait for a window to connect (up to 10s)
|
|
580
|
+
const maxWait = 10000
|
|
581
|
+
const start = Date.now()
|
|
582
|
+
while (Date.now() - start < maxWait) {
|
|
583
|
+
try {
|
|
584
|
+
const resp = await fetch(`http://localhost:${port}/status`, {
|
|
585
|
+
signal: AbortSignal.timeout(1000)
|
|
586
|
+
})
|
|
587
|
+
const status = await resp.json()
|
|
588
|
+
if (status.ok) {
|
|
589
|
+
process.stderr.write('\x1b[2m ready\x1b[0m\n')
|
|
590
|
+
return true
|
|
591
|
+
}
|
|
592
|
+
} catch {}
|
|
593
|
+
await new Promise(r => setTimeout(r, 500))
|
|
594
|
+
}
|
|
595
|
+
process.stderr.write('\x1b[2m timeout\x1b[0m\n')
|
|
596
|
+
return false
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Commands that don't need a browser window to be connected
|
|
600
|
+
const INFO_COMMANDS = new Set(['status', 'windows', 'version', 'help'])
|
|
601
|
+
|
|
512
602
|
// ============================================
|
|
513
603
|
// Main subcommand execution
|
|
514
604
|
// ============================================
|
|
515
605
|
|
|
516
|
-
export async function runSubcommand(subcommand, subArgs, port = '8700') {
|
|
606
|
+
export async function runSubcommand(subcommand, subArgs, port = '8700', options = {}) {
|
|
517
607
|
const baseUrl = `http://localhost:${port}`
|
|
518
608
|
const jsonOutput = subArgs.includes('--json')
|
|
519
|
-
|
|
609
|
+
const noLaunch = options.noLaunch || false
|
|
610
|
+
// Remove --json and extract --window/--session before processing
|
|
520
611
|
let filteredArgs = subArgs.filter(a => a !== '--json')
|
|
521
612
|
let targetWindowId = undefined
|
|
522
613
|
const windowIdx = filteredArgs.indexOf('--window')
|
|
@@ -524,6 +615,11 @@ export async function runSubcommand(subcommand, subArgs, port = '8700') {
|
|
|
524
615
|
targetWindowId = filteredArgs[windowIdx + 1]
|
|
525
616
|
filteredArgs = [...filteredArgs.slice(0, windowIdx), ...filteredArgs.slice(windowIdx + 2)]
|
|
526
617
|
}
|
|
618
|
+
const sessionIdx = filteredArgs.indexOf('--session')
|
|
619
|
+
if (sessionIdx !== -1) {
|
|
620
|
+
process.env.HALTIJA_SESSION = filteredArgs[sessionIdx + 1]
|
|
621
|
+
filteredArgs = [...filteredArgs.slice(0, sessionIdx), ...filteredArgs.slice(sessionIdx + 2)]
|
|
622
|
+
}
|
|
527
623
|
|
|
528
624
|
// Check if server is running, auto-start if not
|
|
529
625
|
if (!(await isServerRunning(port))) {
|
|
@@ -538,6 +634,11 @@ export async function runSubcommand(subcommand, subArgs, port = '8700') {
|
|
|
538
634
|
}
|
|
539
635
|
}
|
|
540
636
|
|
|
637
|
+
// Auto-launch browser if no windows connected (skip for info commands and --no-launch)
|
|
638
|
+
if (!noLaunch && !INFO_COMMANDS.has(subcommand)) {
|
|
639
|
+
await ensureBrowserConnected(port)
|
|
640
|
+
}
|
|
641
|
+
|
|
541
642
|
// Special handling for 'send' command - route to appropriate endpoint
|
|
542
643
|
// hj send selection [agent] → /send/selection
|
|
543
644
|
// hj send recording [agent] → /send/recording
|
|
@@ -596,9 +697,10 @@ export async function runSubcommand(subcommand, subArgs, port = '8700') {
|
|
|
596
697
|
async function doRequest(url, method, body, context = {}) {
|
|
597
698
|
const { subcommand, jsonOutput } = context
|
|
598
699
|
try {
|
|
599
|
-
const
|
|
700
|
+
const sessionId = getSessionId()
|
|
701
|
+
const opts = { method, headers: { 'X-Haltija-Session': sessionId } }
|
|
600
702
|
if (body) {
|
|
601
|
-
opts.headers
|
|
703
|
+
opts.headers['Content-Type'] = 'application/json'
|
|
602
704
|
opts.body = JSON.stringify(body)
|
|
603
705
|
}
|
|
604
706
|
|
|
@@ -610,7 +712,7 @@ async function doRequest(url, method, body, context = {}) {
|
|
|
610
712
|
|
|
611
713
|
// Text format for supported subcommands (unless --json)
|
|
612
714
|
if (!jsonOutput && subcommand === 'tree' && json.success && json.data) {
|
|
613
|
-
console.log(formatTree(json.data))
|
|
715
|
+
console.log(formatTree(json.data, 0, { depth: body?.depth }))
|
|
614
716
|
} else if (!jsonOutput && subcommand === 'events' && (json.events || Array.isArray(json))) {
|
|
615
717
|
console.log(formatEvents(json))
|
|
616
718
|
} else if (!jsonOutput && subcommand === 'test-run' && json.test) {
|
|
@@ -730,7 +832,7 @@ export function listSubcommands() {
|
|
|
730
832
|
return `
|
|
731
833
|
Subcommands (replace curl with simple commands):
|
|
732
834
|
${bold('Inspect')}
|
|
733
|
-
tree [selector] [-d
|
|
835
|
+
tree [selector] [-d N] [-a] DOM tree with ref IDs (default depth 5, -a = all)
|
|
734
836
|
query <selector> Find elements matching selector
|
|
735
837
|
inspect <@ref|selector> Detailed element info
|
|
736
838
|
inspectAll <selector> Deep inspect all matches
|
package/bin/format-tree.mjs
CHANGED
|
@@ -27,15 +27,17 @@ const MAX_TEXT_LEN = 80
|
|
|
27
27
|
* @param {number} indent - current indentation level (internal)
|
|
28
28
|
* @returns {string} formatted text output with footer
|
|
29
29
|
*/
|
|
30
|
-
export function formatTree(node, indent = 0) {
|
|
30
|
+
export function formatTree(node, indent = 0, { depth } = {}) {
|
|
31
31
|
if (!node) return ''
|
|
32
32
|
|
|
33
33
|
const lines = []
|
|
34
34
|
formatNode(node, indent, lines)
|
|
35
35
|
|
|
36
|
-
// Footer:
|
|
36
|
+
// Footer: depth and options hint
|
|
37
|
+
const d = depth ?? 5
|
|
38
|
+
const isDefault = depth === undefined || depth === null
|
|
37
39
|
lines.push('---')
|
|
38
|
-
lines.push('
|
|
40
|
+
lines.push(`depth=${d}${isDefault ? ' (default)' : d === -1 ? ' (unlimited)' : ''} | -d N | --all | --json`)
|
|
39
41
|
|
|
40
42
|
return lines.join('\n')
|
|
41
43
|
}
|
package/bin/hj.mjs
CHANGED
|
@@ -36,6 +36,21 @@ if (portIdx !== -1 && args[portIdx + 1]) {
|
|
|
36
36
|
args.splice(portIdx, 2)
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// Parse --session option (set session token)
|
|
40
|
+
const sessionIdx = args.indexOf('--session')
|
|
41
|
+
if (sessionIdx !== -1 && args[sessionIdx + 1]) {
|
|
42
|
+
process.env.HALTIJA_SESSION = args[sessionIdx + 1]
|
|
43
|
+
args.splice(sessionIdx, 2)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Parse --no-launch option (skip auto-launching Electron app)
|
|
47
|
+
let noLaunch = false
|
|
48
|
+
const noLaunchIdx = args.indexOf('--no-launch')
|
|
49
|
+
if (noLaunchIdx !== -1) {
|
|
50
|
+
noLaunch = true
|
|
51
|
+
args.splice(noLaunchIdx, 1)
|
|
52
|
+
}
|
|
53
|
+
|
|
39
54
|
const subcommand = args[0]
|
|
40
55
|
const subArgs = args.slice(1).filter(a => a !== '--window' || true) // keep all args
|
|
41
56
|
|
|
@@ -61,7 +76,7 @@ if (!isSubcommand(subcommand)) {
|
|
|
61
76
|
console.error(`Run 'hj' for docs.`)
|
|
62
77
|
process.exit(1)
|
|
63
78
|
} else {
|
|
64
|
-
runSubcommand(subcommand, subArgs, port)
|
|
79
|
+
runSubcommand(subcommand, subArgs, port, { noLaunch })
|
|
65
80
|
}
|
|
66
81
|
|
|
67
82
|
function filterHelp(topic) {
|
package/bin/tosijs-dev.mjs
CHANGED
|
@@ -362,6 +362,8 @@ if (docsDirIdx !== -1 && args[docsDirIdx + 1]) {
|
|
|
362
362
|
// ============================================
|
|
363
363
|
|
|
364
364
|
const ciMode = args.includes('--ci')
|
|
365
|
+
const secureMode = args.includes('--secure')
|
|
366
|
+
if (secureMode) process.env.HALTIJA_SECURE = '1'
|
|
365
367
|
const headlessMode = args.includes('--headless') // Playwright headless (separate from CI)
|
|
366
368
|
const headlessUrlIdx = args.indexOf('--headless-url')
|
|
367
369
|
const headlessUrl = headlessUrlIdx !== -1 ? args[headlessUrlIdx + 1] : null
|
package/dist/component.js
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
// src/version.ts
|
|
49
|
-
var VERSION = "1.2.
|
|
49
|
+
var VERSION = "1.2.5";
|
|
50
50
|
|
|
51
51
|
// src/text-selector.ts
|
|
52
52
|
var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\(/;
|
|
@@ -1312,7 +1312,7 @@
|
|
|
1312
1312
|
}
|
|
1313
1313
|
function buildDomTree(el, options, currentDepth = 0) {
|
|
1314
1314
|
const {
|
|
1315
|
-
depth =
|
|
1315
|
+
depth = 5,
|
|
1316
1316
|
includeText = true,
|
|
1317
1317
|
allAttributes = false,
|
|
1318
1318
|
includeStyles = false,
|
|
@@ -1703,6 +1703,7 @@
|
|
|
1703
1703
|
windowId;
|
|
1704
1704
|
isElectron = false;
|
|
1705
1705
|
browserId = uid();
|
|
1706
|
+
sessionToken = "";
|
|
1706
1707
|
killed = false;
|
|
1707
1708
|
isActive = true;
|
|
1708
1709
|
homeLeft = 0;
|
|
@@ -1775,7 +1776,7 @@
|
|
|
1775
1776
|
selectionBox = null;
|
|
1776
1777
|
highlightedElements = [];
|
|
1777
1778
|
static get observedAttributes() {
|
|
1778
|
-
return ["server", "hidden"];
|
|
1779
|
+
return ["server", "hidden", "session"];
|
|
1779
1780
|
}
|
|
1780
1781
|
static async runTests() {
|
|
1781
1782
|
const el = document.querySelector(TAG_NAME);
|
|
@@ -1871,10 +1872,16 @@
|
|
|
1871
1872
|
}
|
|
1872
1873
|
this.windowId = storedWindowId;
|
|
1873
1874
|
}
|
|
1875
|
+
if (config?.session) {
|
|
1876
|
+
this.sessionToken = config.session;
|
|
1877
|
+
} else {
|
|
1878
|
+
this.sessionToken = uid();
|
|
1879
|
+
}
|
|
1874
1880
|
}
|
|
1875
1881
|
connectedCallback() {
|
|
1876
1882
|
this.killed = false;
|
|
1877
1883
|
this.serverUrl = this.getAttribute("server") || this.serverUrl;
|
|
1884
|
+
this.sessionToken = this.getAttribute("session") || this.sessionToken;
|
|
1878
1885
|
this.render();
|
|
1879
1886
|
const rect = this.getBoundingClientRect();
|
|
1880
1887
|
this.homeLeft = window.innerWidth - rect.width - 16;
|
|
@@ -1902,6 +1909,11 @@
|
|
|
1902
1909
|
this.connect();
|
|
1903
1910
|
}
|
|
1904
1911
|
}
|
|
1912
|
+
if (name === "session") {
|
|
1913
|
+
this.sessionToken = value;
|
|
1914
|
+
this.disconnect();
|
|
1915
|
+
this.connect();
|
|
1916
|
+
}
|
|
1905
1917
|
}
|
|
1906
1918
|
render() {
|
|
1907
1919
|
if (this.shadowRoot.querySelector(".widget")) {
|
|
@@ -2087,6 +2099,25 @@
|
|
|
2087
2099
|
.btn.info-btn:hover {
|
|
2088
2100
|
background: #2563eb;
|
|
2089
2101
|
}
|
|
2102
|
+
.session-badge {
|
|
2103
|
+
display: flex;
|
|
2104
|
+
align-items: center;
|
|
2105
|
+
gap: 2px;
|
|
2106
|
+
font-size: 9px;
|
|
2107
|
+
color: #666;
|
|
2108
|
+
background: rgba(255,255,255,0.05);
|
|
2109
|
+
padding: 2px 4px;
|
|
2110
|
+
border-radius: 3px;
|
|
2111
|
+
cursor: pointer;
|
|
2112
|
+
user-select: none;
|
|
2113
|
+
}
|
|
2114
|
+
.session-badge:hover {
|
|
2115
|
+
background: rgba(255,255,255,0.1);
|
|
2116
|
+
color: #aaa;
|
|
2117
|
+
}
|
|
2118
|
+
.session-badge.copied {
|
|
2119
|
+
color: #22c55e;
|
|
2120
|
+
}
|
|
2090
2121
|
@keyframes pulse {
|
|
2091
2122
|
0%, 100% { opacity: 1; }
|
|
2092
2123
|
50% { opacity: 0.5; }
|
|
@@ -2449,6 +2480,8 @@
|
|
|
2449
2480
|
<span class="logo">\uD83E\uDDDD</span>
|
|
2450
2481
|
</div>
|
|
2451
2482
|
<div class="title">${PRODUCT_NAME}</div>
|
|
2483
|
+
<span class="session-badge" data-action="copy-session" title="Session: ${this.sessionToken}
|
|
2484
|
+
Click to copy session command">${this.sessionToken.slice(0, 8)}</span>
|
|
2452
2485
|
<div class="controls">
|
|
2453
2486
|
<button class="btn" data-action="select" title="Select elements (drag to select area)" aria-label="Select elements">\uD83D\uDC46</button>
|
|
2454
2487
|
<button class="btn" data-action="record" title="Record test (click to start/stop)" aria-label="Record test">REC</button>
|
|
@@ -2535,6 +2568,8 @@
|
|
|
2535
2568
|
this.startSelection();
|
|
2536
2569
|
if (action2 === "stats")
|
|
2537
2570
|
this.copyStatsToClipboard();
|
|
2571
|
+
if (action2 === "copy-session")
|
|
2572
|
+
this.copySessionToken(e.currentTarget);
|
|
2538
2573
|
if (action2 === "close-modal")
|
|
2539
2574
|
this.closeTestModal();
|
|
2540
2575
|
if (action2 === "copy-test")
|
|
@@ -3160,6 +3195,28 @@
|
|
|
3160
3195
|
});
|
|
3161
3196
|
}
|
|
3162
3197
|
}
|
|
3198
|
+
async copySessionToken(badge) {
|
|
3199
|
+
const cmd = `export HALTIJA_SESSION=${this.sessionToken}`;
|
|
3200
|
+
try {
|
|
3201
|
+
await navigator.clipboard.writeText(cmd);
|
|
3202
|
+
badge.textContent = "copied!";
|
|
3203
|
+
badge.classList.add("copied");
|
|
3204
|
+
setTimeout(() => {
|
|
3205
|
+
badge.textContent = this.sessionToken.slice(0, 8);
|
|
3206
|
+
badge.classList.remove("copied");
|
|
3207
|
+
}, 1500);
|
|
3208
|
+
} catch {
|
|
3209
|
+
try {
|
|
3210
|
+
await navigator.clipboard.writeText(this.sessionToken);
|
|
3211
|
+
badge.textContent = "copied!";
|
|
3212
|
+
badge.classList.add("copied");
|
|
3213
|
+
setTimeout(() => {
|
|
3214
|
+
badge.textContent = this.sessionToken.slice(0, 8);
|
|
3215
|
+
badge.classList.remove("copied");
|
|
3216
|
+
}, 1500);
|
|
3217
|
+
} catch {}
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3163
3220
|
async copyStatsToClipboard() {
|
|
3164
3221
|
const refStats = refRegistry.getStats();
|
|
3165
3222
|
stats.refsStale = refStats.stale;
|
|
@@ -4926,6 +4983,7 @@ ${elementSummary}${moreText}`;
|
|
|
4926
4983
|
this.send("system", "connected", {
|
|
4927
4984
|
windowId: this.windowId,
|
|
4928
4985
|
browserId: this.browserId,
|
|
4986
|
+
session: this.sessionToken,
|
|
4929
4987
|
version: VERSION2,
|
|
4930
4988
|
serverSessionId: SERVER_SESSION_ID,
|
|
4931
4989
|
url: location.href,
|
|
@@ -5351,7 +5409,7 @@ ${elementSummary}${moreText}`;
|
|
|
5351
5409
|
const haltija = window.haltija;
|
|
5352
5410
|
if (action2 === "open") {
|
|
5353
5411
|
if (haltija?.openTab) {
|
|
5354
|
-
haltija.openTab(payload2.url).then((opened) => {
|
|
5412
|
+
haltija.openTab(payload2.url, payload2.session).then((opened) => {
|
|
5355
5413
|
this.respond(msg2.id, true, { opened });
|
|
5356
5414
|
}).catch((err) => {
|
|
5357
5415
|
this.respond(msg2.id, false, null, err.message);
|
|
@@ -7207,7 +7265,7 @@ ${elementSummary}${moreText}`;
|
|
|
7207
7265
|
}
|
|
7208
7266
|
registerDevChannel();
|
|
7209
7267
|
var WIDGET_ID = "haltija-widget";
|
|
7210
|
-
function inject(serverUrl2 = "wss://localhost:8700/ws/browser") {
|
|
7268
|
+
function inject(serverUrl2 = "wss://localhost:8700/ws/browser", options) {
|
|
7211
7269
|
const existing = document.getElementById(WIDGET_ID);
|
|
7212
7270
|
if (existing) {
|
|
7213
7271
|
console.log(`${LOG_PREFIX} Already injected`);
|
|
@@ -7222,6 +7280,8 @@ ${elementSummary}${moreText}`;
|
|
|
7222
7280
|
const el = DevChannel.elementCreator()();
|
|
7223
7281
|
el.id = WIDGET_ID;
|
|
7224
7282
|
el.setAttribute("server", serverUrl2);
|
|
7283
|
+
if (options?.session)
|
|
7284
|
+
el.setAttribute("session", options.session);
|
|
7225
7285
|
el.setAttribute("data-version", VERSION2);
|
|
7226
7286
|
document.body.appendChild(el);
|
|
7227
7287
|
console.log(`${LOG_PREFIX} Injected`);
|
|
@@ -7233,7 +7293,7 @@ ${elementSummary}${moreText}`;
|
|
|
7233
7293
|
const config = window.__haltija_config__;
|
|
7234
7294
|
if (config?.autoInject !== false) {
|
|
7235
7295
|
if (config) {
|
|
7236
|
-
inject(config.serverUrl || config.wsUrl);
|
|
7296
|
+
inject(config.serverUrl || config.wsUrl, { session: config.session });
|
|
7237
7297
|
return;
|
|
7238
7298
|
}
|
|
7239
7299
|
}
|