pinokiod 3.271.0 → 3.273.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/kernel/ansi_stream_tracker.js +115 -0
  2. package/kernel/api/app/index.js +422 -0
  3. package/kernel/api/htmlmodal/index.js +94 -0
  4. package/kernel/app_launcher/index.js +115 -0
  5. package/kernel/app_launcher/platform/base.js +276 -0
  6. package/kernel/app_launcher/platform/linux.js +229 -0
  7. package/kernel/app_launcher/platform/macos.js +163 -0
  8. package/kernel/app_launcher/platform/unsupported.js +34 -0
  9. package/kernel/app_launcher/platform/windows.js +247 -0
  10. package/kernel/bin/conda-meta.js +93 -0
  11. package/kernel/bin/conda.js +2 -4
  12. package/kernel/bin/index.js +2 -4
  13. package/kernel/index.js +7 -0
  14. package/kernel/shell.js +212 -1
  15. package/package.json +1 -1
  16. package/server/index.js +491 -6
  17. package/server/public/common.js +224 -741
  18. package/server/public/create-launcher.js +754 -0
  19. package/server/public/htmlmodal.js +292 -0
  20. package/server/public/logs.js +715 -0
  21. package/server/public/resizeSync.js +117 -0
  22. package/server/public/style.css +653 -8
  23. package/server/public/tab-idle-notifier.js +34 -59
  24. package/server/public/tab-link-popover.js +7 -10
  25. package/server/public/terminal-settings.js +723 -9
  26. package/server/public/terminal_input_utils.js +72 -0
  27. package/server/public/terminal_key_caption.js +187 -0
  28. package/server/public/urldropdown.css +120 -3
  29. package/server/public/xterm-inline-bridge.js +116 -0
  30. package/server/socket.js +29 -0
  31. package/server/views/agents.ejs +1 -2
  32. package/server/views/app.ejs +55 -28
  33. package/server/views/bookmarklet.ejs +1 -1
  34. package/server/views/bootstrap.ejs +1 -0
  35. package/server/views/connect.ejs +1 -2
  36. package/server/views/create.ejs +63 -0
  37. package/server/views/editor.ejs +36 -4
  38. package/server/views/index.ejs +1 -2
  39. package/server/views/index2.ejs +1 -2
  40. package/server/views/init/index.ejs +36 -28
  41. package/server/views/install.ejs +20 -22
  42. package/server/views/layout.ejs +2 -8
  43. package/server/views/logs.ejs +155 -0
  44. package/server/views/mini.ejs +0 -18
  45. package/server/views/net.ejs +2 -2
  46. package/server/views/network.ejs +1 -2
  47. package/server/views/network2.ejs +1 -2
  48. package/server/views/old_network.ejs +1 -2
  49. package/server/views/pro.ejs +26 -23
  50. package/server/views/prototype/index.ejs +30 -34
  51. package/server/views/screenshots.ejs +1 -2
  52. package/server/views/settings.ejs +1 -20
  53. package/server/views/shell.ejs +59 -66
  54. package/server/views/terminal.ejs +118 -73
  55. package/server/views/tools.ejs +1 -2
@@ -0,0 +1,715 @@
1
+ (function() {
2
+ const MAX_VIEWER_CHARS = 2 * 1024 * 1024
3
+ const LOGS_SIDEBAR_STORAGE_KEY = 'pinokio.logs.sidebar-collapsed'
4
+ const LOGS_SIDEBAR_WIDTH_KEY = 'pinokio.logs.sidebar-width'
5
+ const LOGS_SIDEBAR_MIN_WIDTH = 220
6
+ const LOGS_SIDEBAR_MAX_WIDTH = 560
7
+
8
+ const safeJsonParse = (value) => {
9
+ try {
10
+ return JSON.parse(value)
11
+ } catch (_) {
12
+ return null
13
+ }
14
+ }
15
+
16
+ class LogsZipControls {
17
+ constructor(options) {
18
+ this.button = options.button
19
+ this.downloadLink = options.downloadLink
20
+ this.statusEl = options.status
21
+ this.defaultLabel = this.button ? this.button.innerHTML : ''
22
+ this.endpoint = options.endpoint || '/pinokio/log'
23
+ this.defaultDownloadHref = options.defaultDownloadHref || (this.downloadLink ? this.downloadLink.getAttribute('href') || '/pinokio/logs.zip' : '/pinokio/logs.zip')
24
+ if (this.button) {
25
+ this.button.addEventListener('click', () => this.generate())
26
+ }
27
+ }
28
+ updateDownloadLink(href) {
29
+ const targetHref = typeof href === 'string' && href.length > 0 ? href : this.defaultDownloadHref
30
+ this.defaultDownloadHref = targetHref
31
+ if (this.downloadLink) {
32
+ this.downloadLink.href = targetHref
33
+ this.downloadLink.classList.remove('hidden')
34
+ }
35
+ }
36
+ setStatus(message, isError) {
37
+ if (!this.statusEl) return
38
+ this.statusEl.textContent = message || ''
39
+ this.statusEl.classList.toggle('is-error', Boolean(isError))
40
+ }
41
+ setBusy(isBusy) {
42
+ if (!this.button) return
43
+ if (isBusy) {
44
+ this.button.disabled = true
45
+ this.button.innerHTML = '<i class="fa-solid fa-circle-notch fa-spin"></i><span>Generating…</span>'
46
+ } else {
47
+ this.button.disabled = false
48
+ this.button.innerHTML = this.defaultLabel
49
+ }
50
+ }
51
+ async generate() {
52
+ this.setBusy(true)
53
+ this.setStatus('Generating archive…')
54
+ try {
55
+ const response = await fetch(this.endpoint, { method: 'POST' })
56
+ if (!response.ok) {
57
+ throw new Error(`Failed (${response.status})`)
58
+ }
59
+ const payload = await response.json().catch(() => ({}))
60
+ const downloadHref = payload && payload.download ? payload.download : this.defaultDownloadHref
61
+ this.updateDownloadLink(downloadHref)
62
+ this.setStatus('Archive ready. Click download.')
63
+ } catch (error) {
64
+ this.setStatus(error.message || 'Failed to generate archive.', true)
65
+ } finally {
66
+ this.setBusy(false)
67
+ }
68
+ }
69
+ }
70
+
71
+ class LogsViewer {
72
+ constructor(options) {
73
+ this.outputEl = options.outputEl
74
+ this.statusEl = options.statusEl
75
+ this.pathEl = options.pathEl
76
+ this.clearButton = options.clearButton
77
+ this.autoScrollInput = options.autoScrollInput
78
+ this.rootDisplay = options.rootDisplay || ''
79
+ this.workspace = options.workspace || ''
80
+ this.currentSource = null
81
+ this.currentPath = ''
82
+ if (this.clearButton) {
83
+ this.clearButton.addEventListener('click', () => {
84
+ this.outputEl.textContent = ''
85
+ this.setStatus('Cleared. Waiting for new data…')
86
+ })
87
+ }
88
+ window.addEventListener('beforeunload', () => this.stop())
89
+ }
90
+ buildDisplayPath(relativePath) {
91
+ if (!this.rootDisplay) {
92
+ return relativePath || ''
93
+ }
94
+ if (!relativePath) {
95
+ return this.rootDisplay
96
+ }
97
+ const base = this.rootDisplay.endsWith('/') ? this.rootDisplay.slice(0, -1) : this.rootDisplay
98
+ return `${base}/${relativePath}`
99
+ }
100
+ setStatus(message) {
101
+ if (this.statusEl) {
102
+ this.statusEl.textContent = message || ''
103
+ }
104
+ }
105
+ updatePath(relativePath) {
106
+ if (this.pathEl) {
107
+ this.pathEl.textContent = relativePath ? this.buildDisplayPath(relativePath) : this.rootDisplay || ''
108
+ }
109
+ }
110
+ shouldAutoScroll() {
111
+ if (!this.autoScrollInput || this.autoScrollInput.checked) {
112
+ if (!this.outputEl) return false
113
+ const threshold = 40
114
+ return (this.outputEl.scrollHeight - this.outputEl.clientHeight - this.outputEl.scrollTop) < threshold
115
+ }
116
+ return false
117
+ }
118
+ scrollToBottom() {
119
+ if (this.outputEl) {
120
+ this.outputEl.scrollTop = this.outputEl.scrollHeight
121
+ }
122
+ }
123
+ trimBuffer() {
124
+ if (!this.outputEl) return
125
+ const text = this.outputEl.textContent || ''
126
+ if (text.length > MAX_VIEWER_CHARS) {
127
+ this.outputEl.textContent = text.slice(text.length - MAX_VIEWER_CHARS)
128
+ }
129
+ }
130
+ appendChunk(chunk) {
131
+ if (!this.outputEl) return
132
+ const stick = this.shouldAutoScroll()
133
+ this.outputEl.append(document.createTextNode(chunk))
134
+ this.trimBuffer()
135
+ if (stick) {
136
+ this.scrollToBottom()
137
+ }
138
+ }
139
+ stop() {
140
+ if (this.currentSource) {
141
+ this.currentSource.close()
142
+ this.currentSource = null
143
+ }
144
+ }
145
+ open(entry) {
146
+ if (!entry || !entry.path) {
147
+ return
148
+ }
149
+ if (entry.path === this.currentPath) {
150
+ return
151
+ }
152
+ this.stop()
153
+ this.currentPath = entry.path
154
+ if (this.outputEl) {
155
+ this.outputEl.textContent = ''
156
+ }
157
+ if (this.clearButton) {
158
+ this.clearButton.disabled = false
159
+ }
160
+ this.updatePath(entry.path)
161
+ this.setStatus('Connecting…')
162
+ if (typeof EventSource === 'undefined') {
163
+ this.setStatus('EventSource is not supported in this browser.')
164
+ return
165
+ }
166
+ const url = new URL('/api/logs/stream', window.location.origin)
167
+ url.searchParams.set('path', entry.path)
168
+ if (this.workspace) {
169
+ url.searchParams.set('workspace', this.workspace)
170
+ }
171
+ const source = new EventSource(url)
172
+ this.currentSource = source
173
+
174
+ source.addEventListener('snapshot', (event) => {
175
+ const payload = safeJsonParse(event.data)
176
+ if (payload && payload.truncated) {
177
+ this.setStatus('Showing the latest part of this file…')
178
+ }
179
+ })
180
+ source.addEventListener('ready', () => {
181
+ this.setStatus('Streaming live output.')
182
+ })
183
+ source.addEventListener('chunk', (event) => {
184
+ const payload = safeJsonParse(event.data)
185
+ if (payload && typeof payload.data === 'string') {
186
+ this.appendChunk(payload.data)
187
+ }
188
+ })
189
+ source.addEventListener('reset', () => {
190
+ if (this.outputEl) {
191
+ this.outputEl.textContent = ''
192
+ }
193
+ this.setStatus('File truncated. Restarting stream…')
194
+ })
195
+ source.addEventListener('rotate', (event) => {
196
+ const payload = safeJsonParse(event.data)
197
+ this.setStatus((payload && payload.message) || 'File rotated. Stream closed.')
198
+ this.stop()
199
+ })
200
+ source.addEventListener('server-error', (event) => {
201
+ const payload = safeJsonParse(event.data)
202
+ this.setStatus((payload && payload.message) || 'Streaming error.')
203
+ })
204
+ source.onerror = () => {
205
+ if (!this.currentSource) {
206
+ return
207
+ }
208
+ if (this.currentSource.readyState === EventSource.CLOSED) {
209
+ this.setStatus('Stream closed.')
210
+ } else {
211
+ this.setStatus('Connection lost. Reconnecting…')
212
+ }
213
+ }
214
+ }
215
+ }
216
+
217
+ class LogsTree {
218
+ constructor(options) {
219
+ this.container = options.container
220
+ this.rootLabel = options.rootLabel || 'logs'
221
+ this.onFileSelected = options.onFileSelected
222
+ this.fileButtons = new Map()
223
+ this.nodes = new Map()
224
+ this.workspace = options.workspace || ''
225
+ if (this.container) {
226
+ this.renderRoot()
227
+ }
228
+ }
229
+ async renderRoot() {
230
+ if (!this.container) return
231
+ this.container.innerHTML = ''
232
+ const rootNode = this.createBranch({ name: this.rootLabel, path: '' }, 0)
233
+ rootNode.details.setAttribute('open', 'open')
234
+ this.container.appendChild(rootNode.details)
235
+ await this.populateChildren(rootNode)
236
+ }
237
+ async refresh() {
238
+ this.fileButtons.clear()
239
+ this.nodes.clear()
240
+ await this.renderRoot()
241
+ }
242
+ createBranch(entry, depth) {
243
+ const details = document.createElement('details')
244
+ details.className = 'logs-branch'
245
+ if (depth === 0) {
246
+ details.open = true
247
+ }
248
+ const summary = document.createElement('summary')
249
+ summary.className = 'logs-branch-summary'
250
+ summary.innerHTML = `
251
+ <span class="logs-branch-chevron"><i class="fa-solid fa-chevron-right"></i></span>
252
+ <span class="logs-branch-icon"><i class="fa-solid fa-folder"></i></span>
253
+ <span class="logs-branch-label">${entry.name || this.rootLabel}</span>
254
+ `
255
+ details.appendChild(summary)
256
+ const children = document.createElement('div')
257
+ children.className = 'logs-children'
258
+ details.appendChild(children)
259
+ const node = { entry, details, children, loaded: false, depth }
260
+ details.addEventListener('toggle', () => {
261
+ details.classList.toggle('is-open', details.open)
262
+ if (details.open) {
263
+ this.populateChildren(node)
264
+ }
265
+ })
266
+ this.nodes.set(entry.path || '__root__', node)
267
+ return node
268
+ }
269
+ buildMessage(text, variant) {
270
+ const div = document.createElement('div')
271
+ div.className = 'logs-tree-message'
272
+ if (variant === 'error') {
273
+ div.classList.add('is-error')
274
+ }
275
+ div.textContent = text
276
+ return div
277
+ }
278
+ formatSize(bytes) {
279
+ if (typeof bytes !== 'number' || Number.isNaN(bytes)) {
280
+ return ''
281
+ }
282
+ const units = ['B', 'KB', 'MB', 'GB', 'TB']
283
+ let size = bytes
284
+ let unit = 0
285
+ while (size >= 1024 && unit < units.length - 1) {
286
+ size /= 1024
287
+ unit += 1
288
+ }
289
+ return `${size.toFixed(unit === 0 ? 0 : 1)} ${units[unit]}`
290
+ }
291
+ createFile(entry) {
292
+ const button = document.createElement('button')
293
+ button.type = 'button'
294
+ button.className = 'logs-file'
295
+ button.dataset.path = entry.path || ''
296
+ const sizeLabel = entry.size != null ? `<span class="logs-file-meta">${this.formatSize(entry.size)}</span>` : ''
297
+ button.innerHTML = `
298
+ <span class="logs-file-icon"><i class="fa-regular fa-file-lines"></i></span>
299
+ <span class="logs-file-label">${entry.name}</span>
300
+ ${sizeLabel}
301
+ `
302
+ button.addEventListener('click', () => {
303
+ this.setActiveFile(entry.path || '')
304
+ if (typeof this.onFileSelected === 'function') {
305
+ this.onFileSelected(entry)
306
+ }
307
+ })
308
+ this.fileButtons.set(entry.path || '', button)
309
+ return button
310
+ }
311
+ setActiveFile(path) {
312
+ if (!path) {
313
+ return
314
+ }
315
+ this.fileButtons.forEach((btn, btnPath) => {
316
+ if (btnPath === path) {
317
+ btn.classList.add('is-active')
318
+ btn.scrollIntoView({ block: 'nearest' })
319
+ } else {
320
+ btn.classList.remove('is-active')
321
+ }
322
+ })
323
+ }
324
+ async populateChildren(node) {
325
+ if (node.loaded || node.loading) {
326
+ return
327
+ }
328
+ node.loading = true
329
+ node.children.innerHTML = ''
330
+ node.children.appendChild(this.buildMessage('Loading…'))
331
+ try {
332
+ const payload = await this.fetchChildren(node.entry.path || '')
333
+ node.children.innerHTML = ''
334
+ if (!payload.entries || payload.entries.length === 0) {
335
+ node.children.appendChild(this.buildMessage('Empty folder'))
336
+ } else {
337
+ const directories = payload.entries.filter((entry) => entry.type === 'directory')
338
+ const files = payload.entries.filter((entry) => entry.type !== 'directory')
339
+ directories.forEach((entry) => {
340
+ const child = this.createBranch(entry, (node.depth || 0) + 1)
341
+ node.children.appendChild(child.details)
342
+ })
343
+ files.forEach((entry) => {
344
+ node.children.appendChild(this.createFile(entry))
345
+ })
346
+ }
347
+ node.loaded = true
348
+ } catch (error) {
349
+ node.children.innerHTML = ''
350
+ node.children.appendChild(this.buildMessage(error.message || 'Failed to load folder', 'error'))
351
+ } finally {
352
+ node.loading = false
353
+ }
354
+ }
355
+ async fetchChildren(pathValue) {
356
+ const url = new URL('/api/logs/tree', window.location.origin)
357
+ if (pathValue) {
358
+ url.searchParams.set('path', pathValue)
359
+ }
360
+ if (this.workspace) {
361
+ url.searchParams.set('workspace', this.workspace)
362
+ }
363
+ const response = await fetch(url, { headers: { 'Accept': 'application/json' } })
364
+ if (!response.ok) {
365
+ const message = await response.text()
366
+ throw new Error(message || `HTTP ${response.status}`)
367
+ }
368
+ return response.json()
369
+ }
370
+ }
371
+
372
+ class LogsPage {
373
+ constructor(config) {
374
+ this.rootElement = config.rootElement || document.getElementById('logs-root')
375
+ this.rootDisplay = config.rootDisplay || ''
376
+ this.workspace = typeof config.workspace === 'string' ? config.workspace.trim() : ''
377
+ this.workspaceTitle = config.workspaceTitle || ''
378
+ this.boundApplyHeight = null
379
+ this.boundBeforeUnload = null
380
+ this.headerObserver = null
381
+ this.storageListener = null
382
+ this.sidebarCollapsed = false
383
+ this.sidebarElement = this.rootElement ? this.rootElement.querySelector('.logs-sidebar') : null
384
+ this.resizer = document.getElementById('logs-resizer')
385
+ this.sidebarToggle = this.resizer ? this.resizer.querySelector('.logs-resizer-toggle') : null
386
+ this.sidebarWidth = null
387
+ this.isResizing = false
388
+ this.pointerId = null
389
+ this.resizeState = null
390
+ this.sidebarPreferenceKey = this.workspace ? `${LOGS_SIDEBAR_STORAGE_KEY}:${this.workspace}` : LOGS_SIDEBAR_STORAGE_KEY
391
+ this.sidebarWidthKey = this.workspace ? `${LOGS_SIDEBAR_WIDTH_KEY}:${this.workspace}` : LOGS_SIDEBAR_WIDTH_KEY
392
+ const downloadHref = config.downloadUrl || (this.workspace ? `/pinokio/logs.zip?workspace=${encodeURIComponent(this.workspace)}` : '/pinokio/logs.zip')
393
+ const zipEndpoint = this.workspace ? `/pinokio/log?workspace=${encodeURIComponent(this.workspace)}` : '/pinokio/log'
394
+ const zipControls = new LogsZipControls({
395
+ button: document.getElementById('logs-generate-archive'),
396
+ downloadLink: document.getElementById('logs-download-archive'),
397
+ status: document.getElementById('logs-zip-status'),
398
+ endpoint: zipEndpoint,
399
+ defaultDownloadHref: downloadHref
400
+ })
401
+ this.zipControls = zipControls
402
+ this.viewer = new LogsViewer({
403
+ outputEl: document.getElementById('logs-viewer-output'),
404
+ statusEl: document.getElementById('logs-viewer-status'),
405
+ pathEl: document.getElementById('logs-viewer-path'),
406
+ clearButton: document.getElementById('logs-clear-viewer'),
407
+ autoScrollInput: document.getElementById('logs-autoscroll'),
408
+ rootDisplay: this.rootDisplay,
409
+ workspace: this.workspace
410
+ })
411
+ this.tree = new LogsTree({
412
+ container: document.getElementById('logs-tree'),
413
+ rootLabel: this.rootDisplay || 'logs',
414
+ onFileSelected: (entry) => this.viewer.open(entry),
415
+ workspace: this.workspace
416
+ })
417
+ const refreshBtn = document.getElementById('logs-refresh-tree')
418
+ if (refreshBtn) {
419
+ refreshBtn.addEventListener('click', async () => {
420
+ refreshBtn.disabled = true
421
+ refreshBtn.classList.add('is-busy')
422
+ try {
423
+ await this.tree.refresh()
424
+ this.viewer.setStatus('Tree refreshed.')
425
+ } catch (error) {
426
+ this.viewer.setStatus(error.message || 'Failed to refresh tree.')
427
+ } finally {
428
+ refreshBtn.disabled = false
429
+ refreshBtn.classList.remove('is-busy')
430
+ }
431
+ })
432
+ }
433
+ this.initSidebarWidth()
434
+ this.initSidebarToggle()
435
+ this.initSidebarResizer()
436
+ this.setupPaneHeightManagement()
437
+ }
438
+
439
+ initSidebarWidth() {
440
+ if (!this.rootElement) {
441
+ return
442
+ }
443
+ const stored = this.readSidebarWidth()
444
+ const baseValue = typeof stored === 'number' ? stored : parseInt(getComputedStyle(this.rootElement).getPropertyValue('--logs-sidebar-width'), 10) || 320
445
+ this.applySidebarWidth(baseValue, false)
446
+ }
447
+
448
+ initSidebarToggle() {
449
+ if (!this.rootElement) {
450
+ return
451
+ }
452
+ const stored = this.readSidebarPreference()
453
+ if (typeof stored === 'boolean') {
454
+ this.applySidebarCollapsed(stored)
455
+ } else {
456
+ this.applySidebarCollapsed(false)
457
+ }
458
+ if (this.sidebarToggle) {
459
+ this.sidebarToggle.addEventListener('click', (event) => {
460
+ event.preventDefault()
461
+ event.stopPropagation()
462
+ const nextState = !this.sidebarCollapsed
463
+ this.applySidebarCollapsed(nextState)
464
+ this.persistSidebarPreference(nextState)
465
+ })
466
+ }
467
+ this.storageListener = (event) => {
468
+ if (event.key === this.sidebarPreferenceKey) {
469
+ const next = event.newValue === '1'
470
+ if (next !== this.sidebarCollapsed) {
471
+ this.applySidebarCollapsed(next)
472
+ }
473
+ }
474
+ }
475
+ window.addEventListener('storage', this.storageListener)
476
+ }
477
+
478
+ initSidebarResizer() {
479
+ if (!this.resizer || !this.sidebarElement || !this.rootElement) {
480
+ return
481
+ }
482
+ this.resizer.addEventListener('pointerdown', (event) => {
483
+ if (event.target && event.target.closest('.logs-resizer-toggle')) {
484
+ return
485
+ }
486
+ if (event.button !== 0 && event.pointerType !== 'touch') {
487
+ return
488
+ }
489
+ event.preventDefault()
490
+ this.isResizing = true
491
+ this.pointerId = event.pointerId
492
+ try {
493
+ this.resizer.setPointerCapture(event.pointerId)
494
+ } catch (_) {}
495
+ const sidebarRect = this.sidebarElement.getBoundingClientRect()
496
+ this.resizeState = {
497
+ left: sidebarRect.left,
498
+ offset: event.clientX - (sidebarRect.left + sidebarRect.width)
499
+ }
500
+ this.bindResizeListeners()
501
+ })
502
+
503
+ this.resizer.addEventListener('dblclick', (event) => {
504
+ event.preventDefault()
505
+ this.applySidebarWidth(320, true)
506
+ })
507
+ }
508
+
509
+ bindResizeListeners() {
510
+ if (!this.handlePointerMove) {
511
+ this.handlePointerMove = (event) => {
512
+ if (!this.isResizing) {
513
+ return
514
+ }
515
+ if (this.pointerId != null && event.pointerId !== this.pointerId) {
516
+ return
517
+ }
518
+ event.preventDefault()
519
+ const state = this.resizeState || {}
520
+ const baseLeft = typeof state.left === 'number' ? state.left : this.rootElement.getBoundingClientRect().left
521
+ const offset = typeof state.offset === 'number' ? state.offset : 0
522
+ const nextWidth = event.clientX - baseLeft - offset
523
+ this.applySidebarWidth(nextWidth, false)
524
+ }
525
+ }
526
+ if (!this.handlePointerUp) {
527
+ this.handlePointerUp = (event) => {
528
+ if (this.pointerId != null && event.pointerId !== this.pointerId) {
529
+ return
530
+ }
531
+ this.finishResizing()
532
+ }
533
+ }
534
+ window.addEventListener('pointermove', this.handlePointerMove)
535
+ window.addEventListener('pointerup', this.handlePointerUp, { once: true })
536
+ }
537
+
538
+ finishResizing() {
539
+ if (!this.isResizing) {
540
+ return
541
+ }
542
+ this.isResizing = false
543
+ if (this.pointerId != null && this.resizer) {
544
+ try {
545
+ this.resizer.releasePointerCapture(this.pointerId)
546
+ } catch (_) {}
547
+ }
548
+ this.pointerId = null
549
+ this.resizeState = null
550
+ window.removeEventListener('pointermove', this.handlePointerMove)
551
+ window.removeEventListener('pointerup', this.handlePointerUp)
552
+ if (this.sidebarWidth != null) {
553
+ this.persistSidebarWidth(this.sidebarWidth)
554
+ }
555
+ }
556
+
557
+ readSidebarPreference() {
558
+ try {
559
+ const storedValue = window.localStorage.getItem(this.sidebarPreferenceKey)
560
+ if (storedValue === null) {
561
+ return null
562
+ }
563
+ return storedValue === '1'
564
+ } catch (error) {
565
+ return null
566
+ }
567
+ }
568
+
569
+ readSidebarWidth() {
570
+ try {
571
+ const storedValue = window.localStorage.getItem(this.sidebarWidthKey)
572
+ if (!storedValue) {
573
+ return null
574
+ }
575
+ const width = parseInt(storedValue, 10)
576
+ if (Number.isNaN(width)) {
577
+ return null
578
+ }
579
+ return width
580
+ } catch (error) {
581
+ return null
582
+ }
583
+ }
584
+
585
+ persistSidebarWidth(width) {
586
+ try {
587
+ window.localStorage.setItem(this.sidebarWidthKey, String(width))
588
+ } catch (error) {
589
+ /* ignore */
590
+ }
591
+ }
592
+
593
+ persistSidebarPreference(collapsed) {
594
+ try {
595
+ window.localStorage.setItem(this.sidebarPreferenceKey, collapsed ? '1' : '0')
596
+ } catch (error) {
597
+ /* ignore */
598
+ }
599
+ }
600
+
601
+ clampSidebarWidth(value) {
602
+ const numeric = typeof value === 'number' ? value : parseInt(value, 10)
603
+ if (Number.isNaN(numeric)) {
604
+ return 320
605
+ }
606
+ return Math.min(Math.max(numeric, LOGS_SIDEBAR_MIN_WIDTH), LOGS_SIDEBAR_MAX_WIDTH)
607
+ }
608
+
609
+ applySidebarWidth(value, persist = false) {
610
+ if (!this.rootElement) {
611
+ return
612
+ }
613
+ const width = this.clampSidebarWidth(value)
614
+ this.sidebarWidth = width
615
+ this.rootElement.style.setProperty('--logs-sidebar-width', `${width}px`)
616
+ if (this.resizer) {
617
+ this.resizer.setAttribute('aria-valuenow', String(width))
618
+ }
619
+ if (persist) {
620
+ this.persistSidebarWidth(width)
621
+ }
622
+ }
623
+
624
+ applySidebarCollapsed(collapsed) {
625
+ this.sidebarCollapsed = collapsed
626
+ if (this.rootElement) {
627
+ this.rootElement.classList.toggle('logs-sidebar-collapsed', collapsed)
628
+ }
629
+ if (this.sidebarElement) {
630
+ if (collapsed) {
631
+ this.sidebarElement.setAttribute('aria-hidden', 'true')
632
+ } else {
633
+ this.sidebarElement.removeAttribute('aria-hidden')
634
+ }
635
+ }
636
+ if (this.resizer) {
637
+ if (collapsed) {
638
+ this.finishResizing()
639
+ }
640
+ this.resizer.removeAttribute('aria-hidden')
641
+ this.resizer.tabIndex = 0
642
+ }
643
+ if (!this.sidebarToggle) {
644
+ return
645
+ }
646
+ const label = collapsed ? 'Expand log navigation' : 'Collapse log navigation'
647
+ this.sidebarToggle.setAttribute('aria-label', label)
648
+ this.sidebarToggle.setAttribute('aria-expanded', String(!collapsed))
649
+ this.sidebarToggle.title = label
650
+ }
651
+
652
+ setupPaneHeightManagement() {
653
+ if (!this.rootElement) {
654
+ return
655
+ }
656
+ const apply = () => this.applyPaneHeight()
657
+ this.boundApplyHeight = apply
658
+ window.addEventListener('resize', apply)
659
+ window.addEventListener('orientationchange', apply)
660
+ if (window.ResizeObserver) {
661
+ const header = document.querySelector('header.navheader')
662
+ if (header) {
663
+ this.headerObserver = new ResizeObserver(apply)
664
+ this.headerObserver.observe(header)
665
+ }
666
+ }
667
+ this.boundBeforeUnload = () => this.dispose()
668
+ window.addEventListener('beforeunload', this.boundBeforeUnload)
669
+ this.applyPaneHeight()
670
+ requestAnimationFrame(apply)
671
+ }
672
+
673
+ applyPaneHeight() {
674
+ if (!this.rootElement) {
675
+ return
676
+ }
677
+ const rect = this.rootElement.getBoundingClientRect()
678
+ const viewportHeight = window.innerHeight || document.documentElement.clientHeight || 0
679
+ const padding = 16
680
+ const available = Math.max(0, viewportHeight - rect.top - padding)
681
+ const fallback = Math.max(320, Math.round(viewportHeight * 0.6))
682
+ const target = available > 0 ? available : fallback
683
+ this.rootElement.style.setProperty('--logs-pane-height', `${target}px`)
684
+ }
685
+
686
+ dispose() {
687
+ if (this.headerObserver) {
688
+ this.headerObserver.disconnect()
689
+ this.headerObserver = null
690
+ }
691
+ if (this.boundApplyHeight) {
692
+ window.removeEventListener('resize', this.boundApplyHeight)
693
+ window.removeEventListener('orientationchange', this.boundApplyHeight)
694
+ this.boundApplyHeight = null
695
+ }
696
+ if (this.boundBeforeUnload) {
697
+ window.removeEventListener('beforeunload', this.boundBeforeUnload)
698
+ this.boundBeforeUnload = null
699
+ }
700
+ if (this.storageListener) {
701
+ window.removeEventListener('storage', this.storageListener)
702
+ this.storageListener = null
703
+ }
704
+ this.finishResizing()
705
+ }
706
+ }
707
+
708
+ document.addEventListener('DOMContentLoaded', () => {
709
+ const root = document.getElementById('logs-root')
710
+ if (!root) return
711
+ const config = Object.assign({}, window.LOGS_PAGE_DATA || {}, { rootElement: root })
712
+ root.dataset.initialized = 'true'
713
+ new LogsPage(config)
714
+ })
715
+ })()