haltija 1.2.7 → 1.3.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,379 @@
1
+ /**
2
+ * CDP Network Monitor — Captures network traffic via Chrome DevTools Protocol.
3
+ *
4
+ * Uses Electron's webContents.debugger API to attach to webviews and
5
+ * capture request/response data. Output is token-optimized for AI agents.
6
+ *
7
+ * Usage from main process:
8
+ * const { attachNetwork, detachNetwork, getNetworkLog, getNetworkStats, clearNetwork } = require('./cdp-network.js')
9
+ * attachNetwork(webContents) // Start monitoring
10
+ * getNetworkLog(webContents.id) // Get buffered entries
11
+ */
12
+
13
+ // Per-webContents state
14
+ const monitors = new Map()
15
+
16
+ // Default noise patterns — URLs matching these are filtered in standard/minimal presets
17
+ const NOISE_PATTERNS = [
18
+ /google-analytics\.com/,
19
+ /googletagmanager\.com/,
20
+ /facebook\.net\/tr/,
21
+ /doubleclick\.net/,
22
+ /hotjar\.com/,
23
+ /sentry\.io\/api/,
24
+ /clarity\.ms/,
25
+ /segment\.io/,
26
+ /mixpanel\.com/,
27
+ /amplitude\.com/,
28
+ /intercom\.io/,
29
+ /fullstory\.com/,
30
+ /newrelic\.com/,
31
+ /datadoghq\.com/,
32
+ ]
33
+
34
+ // Resource types considered "noise" for minimal preset
35
+ const ASSET_TYPES = new Set(['Image', 'Font', 'Stylesheet', 'Media'])
36
+
37
+ const PRESETS = {
38
+ errors: { showAssets: false, showNoise: false, errorsOnly: true },
39
+ minimal: { showAssets: false, showNoise: false, errorsOnly: false },
40
+ standard: { showAssets: true, showNoise: false, errorsOnly: false },
41
+ verbose: { showAssets: true, showNoise: true, errorsOnly: false },
42
+ }
43
+
44
+ /**
45
+ * Attach CDP Network monitoring to a webContents.
46
+ * @param {Electron.WebContents} wc
47
+ * @param {object} opts
48
+ * @param {string} opts.preset - 'errors' | 'minimal' | 'standard' | 'verbose'
49
+ * @param {number} opts.maxBuffer - Max entries to keep (default 200)
50
+ * @param {string[]} opts.includePatterns - URL patterns to include (overrides noise filter)
51
+ * @param {string[]} opts.excludePatterns - Additional URL patterns to exclude
52
+ */
53
+ function attachNetwork(wc, opts = {}) {
54
+ const wcId = wc.id
55
+ if (monitors.has(wcId)) {
56
+ // Already attached — update options
57
+ const mon = monitors.get(wcId)
58
+ mon.preset = opts.preset || mon.preset
59
+ mon.maxBuffer = opts.maxBuffer || mon.maxBuffer
60
+ return { success: true, alreadyAttached: true }
61
+ }
62
+
63
+ const monitor = {
64
+ wc,
65
+ preset: opts.preset || 'standard',
66
+ maxBuffer: opts.maxBuffer || 200,
67
+ includePatterns: (opts.includePatterns || []).map(p => new RegExp(p)),
68
+ excludePatterns: (opts.excludePatterns || []).map(p => new RegExp(p)),
69
+ entries: [], // circular buffer of NetworkEntry
70
+ pending: new Map(), // requestId → partial entry (waiting for response)
71
+ attached: false,
72
+ }
73
+
74
+ try {
75
+ wc.debugger.attach('1.3')
76
+ monitor.attached = true
77
+ } catch (err) {
78
+ // Debugger may already be attached by DevTools
79
+ if (err.message?.includes('Already attached')) {
80
+ monitor.attached = true
81
+ } else {
82
+ return { success: false, error: `CDP attach failed: ${err.message}` }
83
+ }
84
+ }
85
+
86
+ // Enable Network domain
87
+ wc.debugger.sendCommand('Network.enable', {}).catch(err => {
88
+ console.error(`[CDP Network] Network.enable failed for wc ${wcId}:`, err.message)
89
+ })
90
+
91
+ // Listen for CDP events
92
+ const handler = (event, method, params) => {
93
+ handleCdpEvent(monitor, method, params)
94
+ }
95
+ wc.debugger.on('message', handler)
96
+ monitor._handler = handler
97
+
98
+ // Clean up when webContents is destroyed
99
+ const destroyHandler = () => {
100
+ detachNetwork(wc)
101
+ }
102
+ wc.once('destroyed', destroyHandler)
103
+ monitor._destroyHandler = destroyHandler
104
+
105
+ monitors.set(wcId, monitor)
106
+ return { success: true }
107
+ }
108
+
109
+ /**
110
+ * Detach CDP Network monitoring from a webContents.
111
+ */
112
+ function detachNetwork(wc) {
113
+ const wcId = wc.id
114
+ const monitor = monitors.get(wcId)
115
+ if (!monitor) return
116
+
117
+ try {
118
+ if (monitor._handler) {
119
+ wc.debugger.removeListener('message', monitor._handler)
120
+ }
121
+ if (monitor.attached) {
122
+ wc.debugger.sendCommand('Network.disable', {}).catch(() => {})
123
+ wc.debugger.detach()
124
+ }
125
+ } catch {
126
+ // Already detached or destroyed
127
+ }
128
+
129
+ monitors.delete(wcId)
130
+ }
131
+
132
+ /**
133
+ * Get buffered network entries for a webContents, filtered by preset.
134
+ * @param {number} wcId - webContents.id
135
+ * @param {object} opts
136
+ * @param {string} opts.preset - Override the monitor's preset
137
+ * @param {number} opts.since - Only entries after this timestamp
138
+ * @param {number} opts.limit - Max entries to return
139
+ * @returns {{ entries: NetworkEntry[], summary: string }}
140
+ */
141
+ function getNetworkLog(wcId, opts = {}) {
142
+ const monitor = monitors.get(wcId)
143
+ if (!monitor) return { entries: [], summary: 'not watching' }
144
+
145
+ const preset = PRESETS[opts.preset || monitor.preset] || PRESETS.standard
146
+ const since = opts.since || 0
147
+ const limit = opts.limit || 100
148
+
149
+ let filtered = monitor.entries.filter(e => e.ts >= since)
150
+
151
+ // Apply preset filtering
152
+ if (preset.errorsOnly) {
153
+ filtered = filtered.filter(e => e.s >= 400 || e.s === -1 || e.err)
154
+ }
155
+ if (!preset.showAssets) {
156
+ filtered = filtered.filter(e => !ASSET_TYPES.has(e.type))
157
+ }
158
+ if (!preset.showNoise) {
159
+ filtered = filtered.filter(e => !isNoise(e.url, monitor))
160
+ }
161
+
162
+ // Apply custom include/exclude
163
+ if (monitor.includePatterns.length > 0) {
164
+ // Include patterns override noise filter
165
+ filtered = monitor.entries.filter(e =>
166
+ e.ts >= since && monitor.includePatterns.some(p => p.test(e.url))
167
+ )
168
+ }
169
+ if (monitor.excludePatterns.length > 0) {
170
+ filtered = filtered.filter(e => !monitor.excludePatterns.some(p => p.test(e.url)))
171
+ }
172
+
173
+ // Most recent first, limited
174
+ filtered = filtered.slice(-limit)
175
+
176
+ const summary = buildSummary(monitor.entries.filter(e => e.ts >= since))
177
+
178
+ return { entries: filtered, summary }
179
+ }
180
+
181
+ /**
182
+ * Get summary statistics.
183
+ */
184
+ function getNetworkStats(wcId) {
185
+ const monitor = monitors.get(wcId)
186
+ if (!monitor) return { watching: false }
187
+
188
+ const entries = monitor.entries
189
+ const total = entries.length
190
+ const failed = entries.filter(e => e.s >= 400 || e.s === -1 || e.err).length
191
+ const pending = monitor.pending.size
192
+ const totalBytes = entries.reduce((sum, e) => sum + (e.bytes || 0), 0)
193
+ const avgTime = total > 0 ? Math.round(entries.reduce((sum, e) => sum + (e.t || 0), 0) / total) : 0
194
+
195
+ return {
196
+ watching: true,
197
+ preset: monitor.preset,
198
+ total,
199
+ failed,
200
+ pending,
201
+ totalBytes,
202
+ avgTime,
203
+ summary: buildSummary(entries),
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Clear the network buffer.
209
+ */
210
+ function clearNetwork(wcId) {
211
+ const monitor = monitors.get(wcId)
212
+ if (!monitor) return
213
+ monitor.entries = []
214
+ monitor.pending.clear()
215
+ }
216
+
217
+ /**
218
+ * Check if a webContents is being monitored.
219
+ */
220
+ function isMonitoring(wcId) {
221
+ return monitors.has(wcId)
222
+ }
223
+
224
+ // ============================================
225
+ // Internal: CDP event handling
226
+ // ============================================
227
+
228
+ function handleCdpEvent(monitor, method, params) {
229
+ switch (method) {
230
+ case 'Network.requestWillBeSent':
231
+ handleRequestStart(monitor, params)
232
+ break
233
+ case 'Network.responseReceived':
234
+ handleResponse(monitor, params)
235
+ break
236
+ case 'Network.loadingFinished':
237
+ handleLoadingFinished(monitor, params)
238
+ break
239
+ case 'Network.loadingFailed':
240
+ handleLoadingFailed(monitor, params)
241
+ break
242
+ }
243
+ }
244
+
245
+ function handleRequestStart(monitor, params) {
246
+ const { requestId, request, redirectResponse, type, timestamp } = params
247
+
248
+ // If this is a redirect, update the existing entry
249
+ if (redirectResponse && monitor.pending.has(requestId)) {
250
+ const existing = monitor.pending.get(requestId)
251
+ existing.redirects = (existing.redirects || 0) + 1
252
+ existing.url = trimUrl(request.url)
253
+ existing.m = request.method
254
+ return
255
+ }
256
+
257
+ const entry = {
258
+ id: requestId.slice(0, 8),
259
+ m: request.method,
260
+ url: trimUrl(request.url),
261
+ fullUrl: request.url,
262
+ s: 0, // pending
263
+ t: 0,
264
+ sz: '',
265
+ bytes: 0,
266
+ type: type || 'Other',
267
+ ts: Math.round(timestamp * 1000),
268
+ _startTime: timestamp,
269
+ }
270
+
271
+ monitor.pending.set(requestId, entry)
272
+ }
273
+
274
+ function handleResponse(monitor, params) {
275
+ const { requestId, response } = params
276
+ const entry = monitor.pending.get(requestId)
277
+ if (!entry) return
278
+
279
+ entry.s = response.status
280
+ entry.mimeType = response.mimeType
281
+
282
+ // Detect CORS errors
283
+ if (response.status === 0 && response.headers?.['access-control-allow-origin'] === undefined) {
284
+ entry.cors = true
285
+ entry.err = 'CORS'
286
+ }
287
+ }
288
+
289
+ function handleLoadingFinished(monitor, params) {
290
+ const { requestId, encodedDataLength, timestamp } = params
291
+ const entry = monitor.pending.get(requestId)
292
+ if (!entry) return
293
+
294
+ entry.bytes = encodedDataLength || 0
295
+ entry.sz = humanSize(entry.bytes)
296
+ entry.t = Math.round((timestamp - entry._startTime) * 1000)
297
+ delete entry._startTime
298
+ delete entry.fullUrl
299
+
300
+ finishEntry(monitor, requestId, entry)
301
+ }
302
+
303
+ function handleLoadingFailed(monitor, params) {
304
+ const { requestId, errorText, canceled, timestamp } = params
305
+ const entry = monitor.pending.get(requestId)
306
+ if (!entry) return
307
+
308
+ entry.s = -1
309
+ entry.err = canceled ? 'canceled' : (errorText || 'failed')
310
+ entry.t = Math.round((timestamp - entry._startTime) * 1000)
311
+ delete entry._startTime
312
+ delete entry.fullUrl
313
+
314
+ finishEntry(monitor, requestId, entry)
315
+ }
316
+
317
+ function finishEntry(monitor, requestId, entry) {
318
+ monitor.pending.delete(requestId)
319
+ monitor.entries.push(entry)
320
+
321
+ // Trim buffer
322
+ while (monitor.entries.length > monitor.maxBuffer) {
323
+ monitor.entries.shift()
324
+ }
325
+ }
326
+
327
+ // ============================================
328
+ // Internal: formatting helpers
329
+ // ============================================
330
+
331
+ function trimUrl(url) {
332
+ try {
333
+ const u = new URL(url)
334
+ let path = u.pathname
335
+ // Collapse long paths
336
+ if (path.length > 60) {
337
+ const parts = path.split('/')
338
+ if (parts.length > 4) {
339
+ path = '/' + parts[1] + '/.../' + parts[parts.length - 1]
340
+ }
341
+ }
342
+ // Trim query
343
+ let query = u.search
344
+ if (query.length > 30) {
345
+ query = query.slice(0, 27) + '...'
346
+ }
347
+ // Omit localhost origin
348
+ const origin = (u.hostname === 'localhost' || u.hostname === '127.0.0.1')
349
+ ? '' : u.host
350
+ return (origin ? origin : '') + path + query
351
+ } catch {
352
+ return url.length > 80 ? url.slice(0, 77) + '...' : url
353
+ }
354
+ }
355
+
356
+ function humanSize(bytes) {
357
+ if (bytes === 0) return '0B'
358
+ if (bytes < 1024) return bytes + 'B'
359
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1).replace(/\.0$/, '') + 'K'
360
+ return (bytes / (1024 * 1024)).toFixed(1).replace(/\.0$/, '') + 'M'
361
+ }
362
+
363
+ function isNoise(url, monitor) {
364
+ return NOISE_PATTERNS.some(p => p.test(url))
365
+ }
366
+
367
+ function buildSummary(entries) {
368
+ const total = entries.length
369
+ const failed = entries.filter(e => e.s >= 400 || e.s === -1 || e.err).length
370
+ const totalBytes = entries.reduce((sum, e) => sum + (e.bytes || 0), 0)
371
+ const avgTime = total > 0 ? Math.round(entries.reduce((sum, e) => sum + (e.t || 0), 0) / total) : 0
372
+ const parts = [`${total} req`]
373
+ if (failed > 0) parts.push(`${failed} failed`)
374
+ parts.push(`${avgTime}ms avg`)
375
+ parts.push(humanSize(totalBytes))
376
+ return parts.join(', ')
377
+ }
378
+
379
+ module.exports = { attachNetwork, detachNetwork, getNetworkLog, getNetworkStats, clearNetwork, isMonitoring, humanSize }
@@ -23,6 +23,7 @@ const fs = require('fs')
23
23
  const os = require('os')
24
24
  const { spawn } = require('child_process')
25
25
  const http = require('http')
26
+ const { attachNetwork, detachNetwork, getNetworkLog, getNetworkStats, clearNetwork, isMonitoring } = require('./cdp-network.js')
26
27
 
27
28
  // Suppress EIO errors when stdout/stderr pipes break during shutdown
28
29
  process.stdout.on('error', () => {})
@@ -700,6 +701,19 @@ function setupWebContentsInjection(wc) {
700
701
  })
701
702
  }
702
703
 
704
+ /**
705
+ * Find the webview webContents for a given IPC sender.
706
+ * If sender IS a webview, return it directly.
707
+ * If sender is the renderer (main window), return null (caller should use explicit wcId).
708
+ */
709
+ function findWebContentsForSender(sender) {
710
+ if (!sender) return null
711
+ // webview-preload.js IPC: sender is the webview itself
712
+ if (sender.getType() === 'webview') return sender
713
+ // renderer preload.js IPC: sender is the main window, not useful for targeting a specific webview
714
+ return null
715
+ }
716
+
703
717
  async function injectWidget(webContents) {
704
718
  const url = webContents.getURL()
705
719
  console.log('[Haltija Desktop] Injecting widget into:', url)
@@ -881,12 +895,13 @@ function setupScreenCapture() {
881
895
 
882
896
  // Navigate URL with smart fallback (called from widget in webview)
883
897
  // Routes through renderer's navigate() which has https->http fallback
898
+ // Passes the sender's webContentsId so the renderer navigates the correct tab
884
899
  ipcMain.handle('navigate-url', async (event, url) => {
885
900
  if (!mainWindow) return { success: false, error: 'No window' }
886
901
 
887
902
  try {
888
- // Send to renderer which has the smart navigate function
889
- mainWindow.webContents.send('navigate-url', { url })
903
+ const senderWcId = event.sender.id
904
+ mainWindow.webContents.send('navigate-url', { url, webContentsId: senderWcId })
890
905
  return { success: true }
891
906
  } catch (err) {
892
907
  return { success: false, error: err.message }
@@ -913,6 +928,48 @@ function setupScreenCapture() {
913
928
  return true
914
929
  })
915
930
 
931
+ // CDP Network monitoring — manages per-webContents debugger sessions
932
+ // Accepts explicit wcId for renderer-routed calls, falls back to sender for direct webview calls
933
+ const resolveNetworkWc = (event, wcId) => {
934
+ if (wcId) {
935
+ const { webContents: wcModule } = require('electron')
936
+ return wcModule.fromId(wcId)
937
+ }
938
+ return findWebContentsForSender(event.sender)
939
+ }
940
+
941
+ ipcMain.handle('network-watch', async (event, opts = {}) => {
942
+ const wc = resolveNetworkWc(event, opts.wcId)
943
+ if (!wc) return { success: false, error: 'No webview found' }
944
+ return attachNetwork(wc, opts)
945
+ })
946
+
947
+ ipcMain.handle('network-unwatch', async (event, wcId) => {
948
+ const wc = resolveNetworkWc(event, wcId)
949
+ if (!wc) return { success: false, error: 'No webview found' }
950
+ detachNetwork(wc)
951
+ return { success: true }
952
+ })
953
+
954
+ ipcMain.handle('network-log', async (event, opts = {}) => {
955
+ const wc = resolveNetworkWc(event, opts.wcId)
956
+ if (!wc) return { entries: [], summary: 'no webview' }
957
+ return getNetworkLog(wc.id, opts)
958
+ })
959
+
960
+ ipcMain.handle('network-stats', async (event, wcId) => {
961
+ const wc = resolveNetworkWc(event, wcId)
962
+ if (!wc) return { watching: false }
963
+ return getNetworkStats(wc.id)
964
+ })
965
+
966
+ ipcMain.handle('network-clear', async (event, wcId) => {
967
+ const wc = resolveNetworkWc(event, wcId)
968
+ if (!wc) return { success: false }
969
+ clearNetwork(wc.id)
970
+ return { success: true }
971
+ })
972
+
916
973
  // Video capture — forwarded to renderer where MediaRecorder runs
917
974
  // Widget (webview) → main → renderer → main → widget
918
975
  // Main also provides media source ID from webContents (not available on webview DOM element)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "haltija-desktop",
3
- "version": "1.2.7",
3
+ "version": "1.3.0-beta.1",
4
4
  "description": "Haltija Desktop - God Mode Browser for AI Agents",
5
5
  "homepage": "https://github.com/tonioloewald/haltija",
6
6
  "author": {
@@ -52,7 +52,9 @@ function handleAgentStatusMessage(msg) {
52
52
  break
53
53
  case 'shell-renamed':
54
54
  if (connectedShells.has(msg.shellId)) {
55
- connectedShells.get(msg.shellId).name = msg.name
55
+ const info = connectedShells.get(msg.shellId)
56
+ info.name = msg.name
57
+ info.isAgent = msg.name?.includes('agent')
56
58
  updateAgentSelector()
57
59
  }
58
60
  break
@@ -186,6 +188,12 @@ function updateAgentSelector() {
186
188
  const agents = Array.from(connectedShells.entries())
187
189
  .filter(([_, info]) => info.isAgent)
188
190
 
191
+ // Hide the selector unless there are multiple agents to choose between
192
+ const selectorEl = el.agentSelect?.parentElement
193
+ if (selectorEl) {
194
+ selectorEl.style.display = agents.length > 1 ? '' : 'none'
195
+ }
196
+
189
197
  if (agents.length === 0) {
190
198
  el.agentSelect.innerHTML = '<option value="">No agents</option>'
191
199
  } else {
@@ -196,6 +204,10 @@ function updateAgentSelector() {
196
204
  }
197
205
 
198
206
  export async function initAgentStatusBar() {
207
+ // Hide agent selector by default (only shown when multiple agents connected)
208
+ const selectorEl = el.agentSelect?.parentElement
209
+ if (selectorEl) selectorEl.style.display = 'none'
210
+
199
211
  try {
200
212
  const response = await fetch(`${getServerUrl()}/terminal/status`)
201
213
  if (response.ok) {
@@ -206,5 +218,17 @@ export async function initAgentStatusBar() {
206
218
  console.log('[Agent Status] Could not fetch initial status:', err.message)
207
219
  }
208
220
 
221
+ // Seed agent list from existing sessions (may have connected before this WS)
222
+ try {
223
+ const res = await fetch(`${getServerUrl()}/terminal/agents`)
224
+ if (res.ok) {
225
+ const data = await res.json()
226
+ for (const agent of data.agents || []) {
227
+ connectedShells.set(agent.id, { name: agent.name, isAgent: true })
228
+ }
229
+ updateAgentSelector()
230
+ }
231
+ } catch {}
232
+
209
233
  connectAgentStatusWs()
210
234
  }
@@ -258,6 +258,14 @@ export function navigate(url, tabId = activeTabId) {
258
258
  const tab = tabs.find((t) => t.id === tabId)
259
259
  if (!tab) return
260
260
 
261
+ // Never navigate terminal/agent iframes — they aren't browser tabs
262
+ if (tab.isTerminal) {
263
+ console.warn('[Haltija] Refusing to navigate terminal tab, finding content tab instead')
264
+ const contentTab = tabs.find(t => !t.isTerminal)
265
+ if (contentTab) return navigate(url, contentTab.id)
266
+ return
267
+ }
268
+
261
269
  let addedHttps = false
262
270
 
263
271
  if (url && !url.match(/^(https?|blob|data|file|about|javascript):\/?\/?/i)) {
@@ -202,8 +202,24 @@ if (window.haltija) {
202
202
  })
203
203
 
204
204
  window.haltija.onNavigateUrl?.((data) => {
205
- console.log('[Haltija Desktop] Navigate request from widget:', data.url)
206
- navigate(data.url)
205
+ console.log('[Haltija Desktop] Navigate request from widget:', data.url, 'wcId:', data.webContentsId)
206
+ // Find the tab that owns the webview that sent the navigate request
207
+ // so we navigate the correct tab, not the active (possibly terminal) tab
208
+ let targetTabId = undefined
209
+ if (data.webContentsId) {
210
+ const match = tabs.find(t => {
211
+ try {
212
+ return t.webview?.getWebContentsId && t.webview.getWebContentsId() === data.webContentsId
213
+ } catch { return false }
214
+ })
215
+ if (match) targetTabId = match.id
216
+ }
217
+ // Fallback: navigate the first non-terminal tab (never navigate a terminal/agent iframe)
218
+ if (!targetTabId) {
219
+ const contentTab = tabs.find(t => !t.isTerminal)
220
+ if (contentTab) targetTabId = contentTab.id
221
+ }
222
+ navigate(data.url, targetTabId)
207
223
  })
208
224
  }
209
225
 
@@ -46,7 +46,7 @@
46
46
  });
47
47
 
48
48
  // src/version.ts
49
- var VERSION = "1.2.7";
49
+ var VERSION = "1.3.0-beta.1";
50
50
 
51
51
  // src/text-selector.ts
52
52
  var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\(/;
@@ -5164,6 +5164,9 @@ ${elementSummary}${moreText}`;
5164
5164
  case "video":
5165
5165
  this.handleVideoMessage(msg2);
5166
5166
  break;
5167
+ case "network":
5168
+ this.handleNetworkMessage(msg2);
5169
+ break;
5167
5170
  }
5168
5171
  this.render();
5169
5172
  }
@@ -5232,6 +5235,64 @@ ${elementSummary}${moreText}`;
5232
5235
  this.respond(msg2.id, false, undefined, `Unknown video action: ${action2}`);
5233
5236
  }
5234
5237
  }
5238
+ handleNetworkMessage(msg2) {
5239
+ const { action: action2, payload: payload2 } = msg2;
5240
+ const haltija = window.haltija;
5241
+ const notAvailable = "Network monitoring requires the Haltija Desktop app (CDP access)";
5242
+ if (action2 === "watch") {
5243
+ if (!haltija?.networkWatch) {
5244
+ this.respond(msg2.id, false, undefined, notAvailable);
5245
+ return;
5246
+ }
5247
+ haltija.networkWatch(payload2 || {}).then((result2) => {
5248
+ this.respond(msg2.id, result2.success !== false, result2);
5249
+ }).catch((err) => {
5250
+ this.respond(msg2.id, false, undefined, err.message);
5251
+ });
5252
+ } else if (action2 === "unwatch") {
5253
+ if (!haltija?.networkUnwatch) {
5254
+ this.respond(msg2.id, false, undefined, notAvailable);
5255
+ return;
5256
+ }
5257
+ haltija.networkUnwatch().then((result2) => {
5258
+ this.respond(msg2.id, true, result2);
5259
+ }).catch((err) => {
5260
+ this.respond(msg2.id, false, undefined, err.message);
5261
+ });
5262
+ } else if (action2 === "get") {
5263
+ if (!haltija?.networkLog) {
5264
+ this.respond(msg2.id, false, undefined, notAvailable);
5265
+ return;
5266
+ }
5267
+ haltija.networkLog(payload2 || {}).then((result2) => {
5268
+ this.respond(msg2.id, true, result2);
5269
+ }).catch((err) => {
5270
+ this.respond(msg2.id, false, undefined, err.message);
5271
+ });
5272
+ } else if (action2 === "stats") {
5273
+ if (!haltija?.networkStats) {
5274
+ this.respond(msg2.id, false, undefined, notAvailable);
5275
+ return;
5276
+ }
5277
+ haltija.networkStats().then((result2) => {
5278
+ this.respond(msg2.id, true, result2);
5279
+ }).catch((err) => {
5280
+ this.respond(msg2.id, false, undefined, err.message);
5281
+ });
5282
+ } else if (action2 === "clear") {
5283
+ if (!haltija?.networkClear) {
5284
+ this.respond(msg2.id, false, undefined, notAvailable);
5285
+ return;
5286
+ }
5287
+ haltija.networkClear().then((result2) => {
5288
+ this.respond(msg2.id, true, result2);
5289
+ }).catch((err) => {
5290
+ this.respond(msg2.id, false, undefined, err.message);
5291
+ });
5292
+ } else {
5293
+ this.respond(msg2.id, false, undefined, `Unknown network action: ${action2}`);
5294
+ }
5295
+ }
5235
5296
  handleSemanticMessage(msg2) {
5236
5297
  const { action: action2, payload: payload2 } = msg2;
5237
5298
  if (action2 === "start" || action2 === "watch") {