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.
@@ -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__ = { serverUrl: '${wsUrl}', windowId: '${windowId}' };`,
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
- mainWindow.webContents.send('open-tab', { url })
899
+ if (session) pendingTabSession = session
900
+ mainWindow.webContents.send('open-tab', { url, session })
888
901
  return true
889
902
  })
890
903
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "haltija-desktop",
3
- "version": "1.2.4",
3
+ "version": "1.2.5",
4
4
  "description": "Haltija Desktop - God Mode Browser for AI Agents",
5
5
  "homepage": "https://github.com/tonioloewald/haltija",
6
6
  "author": {
@@ -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.4";
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 = 3,
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: () => {
@@ -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
- // Remove --json and extract --window before processing
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 opts = { method }
700
+ const sessionId = getSessionId()
701
+ const opts = { method, headers: { 'X-Haltija-Session': sessionId } }
600
702
  if (body) {
601
- opts.headers = { 'Content-Type': 'application/json' }
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 depth] DOM tree with ref IDs
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
@@ -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: JSON escape hatch
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('hj tree --json')
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) {
@@ -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.4";
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 = 3,
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
  }