haltija 1.2.3 → 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/tabs.js +12 -1
- package/apps/desktop/renderer.js +1 -1
- package/apps/desktop/resources/component.js +66 -6
- package/apps/desktop/styles.css +7 -1
- package/apps/desktop/terminal.html +38 -4
- package/apps/desktop/webview-preload.js +1 -1
- package/bin/cli-subcommand.mjs +121 -9
- 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 +140 -11
- package/dist/index.js +212 -45
- package/dist/server.js +212 -45
- package/package.json +5 -3
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
|
|
|
@@ -150,13 +150,24 @@ export function renameTerminalTab(tab, name) {
|
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
// Track the last active content (non-terminal) tab so it stays visible
|
|
154
|
+
// behind terminal/agent overlays for screenshot/hj command support
|
|
155
|
+
let lastContentTabId = null
|
|
156
|
+
|
|
153
157
|
export function activateTab(tabId) {
|
|
154
158
|
const tab = tabs.find((t) => t.id === tabId)
|
|
155
159
|
if (!tab) return
|
|
156
160
|
|
|
161
|
+
const isTerminalTab = tab.isTerminal || tab.url === 'terminal'
|
|
162
|
+
if (!isTerminalTab) lastContentTabId = tabId
|
|
163
|
+
|
|
157
164
|
tabs.forEach((t) => {
|
|
158
165
|
t.element.classList.remove('active')
|
|
159
|
-
t.
|
|
166
|
+
if (isTerminalTab && t.id === lastContentTabId) {
|
|
167
|
+
// Keep the last content webview visible behind the terminal iframe
|
|
168
|
+
} else {
|
|
169
|
+
t.webview.classList.remove('active')
|
|
170
|
+
}
|
|
160
171
|
})
|
|
161
172
|
|
|
162
173
|
tab.element.classList.add('active')
|
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
|
}
|
package/apps/desktop/styles.css
CHANGED
|
@@ -640,9 +640,15 @@ body {
|
|
|
640
640
|
display: none;
|
|
641
641
|
}
|
|
642
642
|
|
|
643
|
-
#webview-container webview.active
|
|
643
|
+
#webview-container webview.active {
|
|
644
|
+
display: flex;
|
|
645
|
+
z-index: 0;
|
|
646
|
+
}
|
|
647
|
+
|
|
644
648
|
#webview-container .terminal-frame.active {
|
|
645
649
|
display: flex;
|
|
650
|
+
z-index: 1;
|
|
651
|
+
opacity: 0.88;
|
|
646
652
|
}
|
|
647
653
|
|
|
648
654
|
#webview-container .terminal-frame {
|
|
@@ -1192,7 +1192,12 @@
|
|
|
1192
1192
|
outputPreview.className = 'tool-output-preview'
|
|
1193
1193
|
const lines = (output || '').split('\n')
|
|
1194
1194
|
const previewText = lines.slice(0, 3).join('\n')
|
|
1195
|
-
|
|
1195
|
+
const previewFull = previewText + (lines.length > 3 ? `\n... (${lines.length} lines)` : '')
|
|
1196
|
+
if (/\x1b\[/.test(previewFull)) {
|
|
1197
|
+
outputPreview.innerHTML = ansiToHtml(previewFull)
|
|
1198
|
+
} else {
|
|
1199
|
+
outputPreview.textContent = previewFull
|
|
1200
|
+
}
|
|
1196
1201
|
card.appendChild(outputPreview)
|
|
1197
1202
|
|
|
1198
1203
|
// Add full output to expandable body
|
|
@@ -1205,10 +1210,13 @@
|
|
|
1205
1210
|
outputDiv.appendChild(outputLabel)
|
|
1206
1211
|
const outputPre = document.createElement('pre')
|
|
1207
1212
|
outputPre.style.cssText = 'margin: 0; white-space: pre-wrap;'
|
|
1208
|
-
|
|
1209
|
-
|
|
1213
|
+
const fullText = lines.length > 20
|
|
1214
|
+
? lines.slice(0, 20).join('\n') + `\n... (${lines.length} lines total)`
|
|
1215
|
+
: (output || '')
|
|
1216
|
+
if (/\x1b\[/.test(fullText)) {
|
|
1217
|
+
outputPre.innerHTML = ansiToHtml(fullText)
|
|
1210
1218
|
} else {
|
|
1211
|
-
outputPre.textContent =
|
|
1219
|
+
outputPre.textContent = fullText
|
|
1212
1220
|
}
|
|
1213
1221
|
outputDiv.appendChild(outputPre)
|
|
1214
1222
|
body.appendChild(outputDiv)
|
|
@@ -1277,6 +1285,8 @@
|
|
|
1277
1285
|
div.textContent = text
|
|
1278
1286
|
} else if (type === 'summary') {
|
|
1279
1287
|
div.textContent = text
|
|
1288
|
+
} else if (/\x1b\[/.test(text)) {
|
|
1289
|
+
div.innerHTML = ansiToHtml(text)
|
|
1280
1290
|
} else {
|
|
1281
1291
|
div.textContent = text
|
|
1282
1292
|
}
|
|
@@ -1296,6 +1306,30 @@
|
|
|
1296
1306
|
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
1297
1307
|
}
|
|
1298
1308
|
|
|
1309
|
+
/** Convert ANSI escape codes to HTML spans */
|
|
1310
|
+
function ansiToHtml(str) {
|
|
1311
|
+
if (str == null) return ''
|
|
1312
|
+
const escaped = escapeHtml(str)
|
|
1313
|
+
// Map SGR codes to CSS styles
|
|
1314
|
+
const styles = {
|
|
1315
|
+
'1': 'font-weight:bold',
|
|
1316
|
+
'2': 'opacity:0.6',
|
|
1317
|
+
'3': 'font-style:italic',
|
|
1318
|
+
'4': 'text-decoration:underline',
|
|
1319
|
+
'31': 'color:#f87171', '32': 'color:#4ade80', '33': 'color:#fbbf24',
|
|
1320
|
+
'34': 'color:#60a5fa', '35': 'color:#c084fc', '36': 'color:#22d3ee',
|
|
1321
|
+
'90': 'color:#6b7280', '91': 'color:#f87171', '92': 'color:#4ade80',
|
|
1322
|
+
'93': 'color:#fbbf24', '94': 'color:#60a5fa', '95': 'color:#c084fc',
|
|
1323
|
+
'96': 'color:#22d3ee',
|
|
1324
|
+
}
|
|
1325
|
+
// Replace \e[...m sequences with styled spans
|
|
1326
|
+
return escaped.replace(/\x1b\[([0-9;]*)m/g, (_, codes) => {
|
|
1327
|
+
if (!codes || codes === '0') return '</span>'
|
|
1328
|
+
const css = codes.split(';').map(c => styles[c] || '').filter(Boolean).join(';')
|
|
1329
|
+
return css ? `<span style="${css}">` : ''
|
|
1330
|
+
})
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1299
1333
|
// ==========================================
|
|
1300
1334
|
// Init
|
|
1301
1335
|
// ==========================================
|
|
@@ -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
|
@@ -95,9 +95,19 @@ export const ARG_MAPS = {
|
|
|
95
95
|
call: (args) => ({ ...parseTargetArgs(args.slice(0, 1)), method: args[1], args: args.slice(2).map(tryParseJSON) }),
|
|
96
96
|
fetch: (args) => ({ url: args[0], prompt: args.slice(1).join(' ') || undefined }),
|
|
97
97
|
screenshot: (args) => {
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
|
|
98
|
+
const body = { file: true }
|
|
99
|
+
const positional = []
|
|
100
|
+
for (let i = 0; i < args.length; i++) {
|
|
101
|
+
const a = args[i]
|
|
102
|
+
if (a === '--data-url') { body.file = false; continue }
|
|
103
|
+
if (a === '--scale') { body.scale = num(args[++i]); continue }
|
|
104
|
+
if (a === '--maxWidth' || a === '--max-width') { body.maxWidth = num(args[++i]); continue }
|
|
105
|
+
if (a === '--maxHeight' || a === '--max-height') { body.maxHeight = num(args[++i]); continue }
|
|
106
|
+
if (a === '--delay') { body.delay = num(args[++i]); continue }
|
|
107
|
+
if (a === '--no-chyron') { body.chyron = false; continue }
|
|
108
|
+
if (!a.startsWith('-')) { positional.push(a) }
|
|
109
|
+
}
|
|
110
|
+
return { ...body, ...parseTargetArgs(positional) }
|
|
101
111
|
},
|
|
102
112
|
snapshot: (args) => ({ context: args.join(' ') || undefined }),
|
|
103
113
|
select: (args) => ({ action: args[0] }),
|
|
@@ -192,6 +202,7 @@ export function parseTreeArgs(args) {
|
|
|
192
202
|
for (let i = 0; i < args.length; i++) {
|
|
193
203
|
const a = args[i]
|
|
194
204
|
if (a === '--depth' || a === '-d') { body.depth = num(args[++i]); continue }
|
|
205
|
+
if (a === '--all' || a === '-a') { body.depth = -1; continue }
|
|
195
206
|
if (a === '--selector' || a === '-s') { body.selector = args[++i]; continue }
|
|
196
207
|
if (a === '--compact' || a === '-c') { body.compact = true; continue }
|
|
197
208
|
if (a === '--visible') { body.visibleOnly = true; continue }
|
|
@@ -422,6 +433,18 @@ export function clean(obj) {
|
|
|
422
433
|
return Object.keys(result).length ? result : undefined
|
|
423
434
|
}
|
|
424
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
|
+
|
|
425
448
|
// ============================================
|
|
426
449
|
// Server auto-start
|
|
427
450
|
// ============================================
|
|
@@ -499,14 +522,92 @@ async function startServerInBackground(port) {
|
|
|
499
522
|
return false
|
|
500
523
|
}
|
|
501
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
|
+
|
|
502
602
|
// ============================================
|
|
503
603
|
// Main subcommand execution
|
|
504
604
|
// ============================================
|
|
505
605
|
|
|
506
|
-
export async function runSubcommand(subcommand, subArgs, port = '8700') {
|
|
606
|
+
export async function runSubcommand(subcommand, subArgs, port = '8700', options = {}) {
|
|
507
607
|
const baseUrl = `http://localhost:${port}`
|
|
508
608
|
const jsonOutput = subArgs.includes('--json')
|
|
509
|
-
|
|
609
|
+
const noLaunch = options.noLaunch || false
|
|
610
|
+
// Remove --json and extract --window/--session before processing
|
|
510
611
|
let filteredArgs = subArgs.filter(a => a !== '--json')
|
|
511
612
|
let targetWindowId = undefined
|
|
512
613
|
const windowIdx = filteredArgs.indexOf('--window')
|
|
@@ -514,6 +615,11 @@ export async function runSubcommand(subcommand, subArgs, port = '8700') {
|
|
|
514
615
|
targetWindowId = filteredArgs[windowIdx + 1]
|
|
515
616
|
filteredArgs = [...filteredArgs.slice(0, windowIdx), ...filteredArgs.slice(windowIdx + 2)]
|
|
516
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
|
+
}
|
|
517
623
|
|
|
518
624
|
// Check if server is running, auto-start if not
|
|
519
625
|
if (!(await isServerRunning(port))) {
|
|
@@ -528,6 +634,11 @@ export async function runSubcommand(subcommand, subArgs, port = '8700') {
|
|
|
528
634
|
}
|
|
529
635
|
}
|
|
530
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
|
+
|
|
531
642
|
// Special handling for 'send' command - route to appropriate endpoint
|
|
532
643
|
// hj send selection [agent] → /send/selection
|
|
533
644
|
// hj send recording [agent] → /send/recording
|
|
@@ -586,9 +697,10 @@ export async function runSubcommand(subcommand, subArgs, port = '8700') {
|
|
|
586
697
|
async function doRequest(url, method, body, context = {}) {
|
|
587
698
|
const { subcommand, jsonOutput } = context
|
|
588
699
|
try {
|
|
589
|
-
const
|
|
700
|
+
const sessionId = getSessionId()
|
|
701
|
+
const opts = { method, headers: { 'X-Haltija-Session': sessionId } }
|
|
590
702
|
if (body) {
|
|
591
|
-
opts.headers
|
|
703
|
+
opts.headers['Content-Type'] = 'application/json'
|
|
592
704
|
opts.body = JSON.stringify(body)
|
|
593
705
|
}
|
|
594
706
|
|
|
@@ -600,7 +712,7 @@ async function doRequest(url, method, body, context = {}) {
|
|
|
600
712
|
|
|
601
713
|
// Text format for supported subcommands (unless --json)
|
|
602
714
|
if (!jsonOutput && subcommand === 'tree' && json.success && json.data) {
|
|
603
|
-
console.log(formatTree(json.data))
|
|
715
|
+
console.log(formatTree(json.data, 0, { depth: body?.depth }))
|
|
604
716
|
} else if (!jsonOutput && subcommand === 'events' && (json.events || Array.isArray(json))) {
|
|
605
717
|
console.log(formatEvents(json))
|
|
606
718
|
} else if (!jsonOutput && subcommand === 'test-run' && json.test) {
|
|
@@ -720,7 +832,7 @@ export function listSubcommands() {
|
|
|
720
832
|
return `
|
|
721
833
|
Subcommands (replace curl with simple commands):
|
|
722
834
|
${bold('Inspect')}
|
|
723
|
-
tree [selector] [-d
|
|
835
|
+
tree [selector] [-d N] [-a] DOM tree with ref IDs (default depth 5, -a = all)
|
|
724
836
|
query <selector> Find elements matching selector
|
|
725
837
|
inspect <@ref|selector> Detailed element info
|
|
726
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
|