pinokiod 7.5.7 → 7.5.8

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.
@@ -20,6 +20,8 @@
20
20
  const DRAFT_TITLE_STRONG_PATTERN = /\b(?:[A-Za-z_][A-Za-z0-9_.]*(?:Error|Exception)|ERROR|FATAL|ERR!|exit code\s+\d+)\b/i
21
21
  const DRAFT_TITLE_STACK_PATTERN = /^\s*(?:File\s+"[^"]+",\s+line\s+\d+|at\s+\S+|from\s+\S+\s+import\s+|return\s+|await\s+|sys\.exit\b)/i
22
22
  const DRAFT_TITLE_NOISE_PATTERN = /^\s*(?:<<PINOKIO_SHELL>>|={6,}|-{6,}|\[api\s+local\.set\]|The default interactive shell is now|To update your account|For more details, please visit)/i
23
+ const ASK_AI_DEFAULT_PROMPT = 'Investigate what went wrong. Inspect the app logs and explain the likely root cause and next fix.'
24
+ const TOOL_PREFERENCE_KEY = 'pinokio.universalLauncher.tool'
23
25
  const textEncoder = typeof TextEncoder === 'function' ? new TextEncoder() : null
24
26
 
25
27
  const safeJsonParse = (value) => {
@@ -59,6 +61,115 @@
59
61
  }
60
62
  }
61
63
 
64
+ const isPluginLauncherPath = (pathname) => {
65
+ return typeof pathname === 'string'
66
+ && (
67
+ pathname.startsWith('/run/plugin/')
68
+ || pathname.startsWith('/pinokio/run/plugin/')
69
+ || (pathname.startsWith('/run/api/') && /\/pinokio\.js$/i.test(pathname))
70
+ )
71
+ }
72
+
73
+ const pluginToolCategory = (plugin) => {
74
+ const explicitCategory = typeof plugin?.category === 'string' ? plugin.category.trim().toLowerCase() : ''
75
+ if (explicitCategory === 'ide') return 'Desktop'
76
+ if (explicitCategory === 'cli') return 'Terminal'
77
+ const launchType = typeof plugin?.launch_type === 'string' ? plugin.launch_type.trim().toLowerCase() : ''
78
+ if (launchType === 'desktop') return 'Desktop'
79
+ if (launchType === 'terminal') return 'Terminal'
80
+ return plugin && plugin.categoryTitle ? String(plugin.categoryTitle) : 'Plugin'
81
+ }
82
+
83
+ const pluginToolValue = (href) => {
84
+ const normalized = String(href || '').replace(/^\/run/, '').replace(/^\/+/, '')
85
+ const parts = normalized.split('/').filter(Boolean)
86
+ let value = ''
87
+ if (parts[0] === 'plugin' && parts.length >= 3) {
88
+ value = parts.slice(1, -1).join('/')
89
+ } else {
90
+ value = normalized
91
+ }
92
+ if (value.endsWith('/pinokio.js')) {
93
+ value = value.replace(/\/pinokio\.js$/i, '')
94
+ }
95
+ return value
96
+ }
97
+
98
+ const getStoredToolPreference = () => {
99
+ try {
100
+ const value = window.localStorage.getItem(TOOL_PREFERENCE_KEY)
101
+ return typeof value === 'string' ? value.trim() : ''
102
+ } catch (_) {
103
+ return ''
104
+ }
105
+ }
106
+
107
+ const setStoredToolPreference = (value) => {
108
+ try {
109
+ const normalized = typeof value === 'string' ? value.trim() : ''
110
+ if (normalized) {
111
+ window.localStorage.setItem(TOOL_PREFERENCE_KEY, normalized)
112
+ } else {
113
+ window.localStorage.removeItem(TOOL_PREFERENCE_KEY)
114
+ }
115
+ } catch (_) {}
116
+ }
117
+
118
+ const mapPluginMenuToAskAiTools = (menu) => {
119
+ if (!Array.isArray(menu)) {
120
+ return []
121
+ }
122
+ return menu.map((plugin) => {
123
+ if (!plugin || typeof plugin !== 'object') {
124
+ return null
125
+ }
126
+ const href = typeof plugin.href === 'string' ? plugin.href.trim() : ''
127
+ if (!href) {
128
+ return null
129
+ }
130
+ let parsed
131
+ try {
132
+ parsed = new URL(href, window.location.origin)
133
+ } catch (_) {
134
+ return null
135
+ }
136
+ if (parsed.origin !== window.location.origin || !isPluginLauncherPath(parsed.pathname)) {
137
+ return null
138
+ }
139
+ const label = typeof plugin.title === 'string' && plugin.title.trim()
140
+ ? plugin.title.trim()
141
+ : (typeof plugin.text === 'string' && plugin.text.trim() ? plugin.text.trim() : href)
142
+ return {
143
+ href,
144
+ value: pluginToolValue(href),
145
+ label,
146
+ category: pluginToolCategory(plugin),
147
+ iconSrc: typeof plugin.image === 'string' ? plugin.image : (typeof plugin.icon === 'string' ? plugin.icon : ''),
148
+ pluginPath: typeof plugin.pluginPath === 'string' ? plugin.pluginPath : '',
149
+ detailUrl: typeof plugin.detailUrl === 'string' ? plugin.detailUrl : '',
150
+ hasInstall: plugin.hasInstall === true,
151
+ hasInstalledCheck: plugin.hasInstalledCheck === true,
152
+ installed: typeof plugin.installed === 'boolean' ? plugin.installed : null
153
+ }
154
+ }).filter(Boolean).sort((a, b) => {
155
+ const categoryDelta = String(a.category || '').localeCompare(String(b.category || ''), undefined, { sensitivity: 'base' })
156
+ if (categoryDelta !== 0) return categoryDelta
157
+ return String(a.label || '').localeCompare(String(b.label || ''), undefined, { sensitivity: 'base' })
158
+ })
159
+ }
160
+
161
+ const askAiToolOptionLabel = (tool) => {
162
+ const label = String(tool?.label || '').trim()
163
+ const category = String(tool?.category || '').trim()
164
+ if (!category) {
165
+ return label
166
+ }
167
+ if (label.toLowerCase().includes(category.toLowerCase())) {
168
+ return label
169
+ }
170
+ return `${label} (${category})`
171
+ }
172
+
62
173
  class PrivacyFilterClient {
63
174
  constructor() {
64
175
  this.worker = null
@@ -218,6 +329,7 @@
218
329
  this.statusEl = options.statusEl
219
330
  this.outputEl = options.outputEl
220
331
  this.copyButton = options.copyButton
332
+ this.askAiButton = options.askAiButton
221
333
  this.createDraftButton = options.createDraftButton
222
334
  this.draftTitleInput = options.draftTitleInput
223
335
  this.draftTitleNoteEl = options.draftTitleNoteEl
@@ -232,7 +344,12 @@
232
344
  this.reviewListEl = options.reviewListEl
233
345
  this.reviewFiltersEl = options.reviewFiltersEl
234
346
  this.reviewCountEl = options.reviewCountEl
347
+ this.workspace = typeof options.workspace === 'string' ? options.workspace.trim() : ''
348
+ this.workspaceCwd = typeof options.workspaceCwd === 'string' ? options.workspaceCwd.trim() : ''
235
349
  this.privacyFilter = options.privacyFilter || new PrivacyFilterClient()
350
+ this.askAiTools = null
351
+ this.askAiToolsPromise = null
352
+ this.askAiLauncher = null
236
353
  this.report = null
237
354
  this.rawMarkdown = ''
238
355
  this.reviewMarkdown = ''
@@ -257,6 +374,9 @@
257
374
  if (this.copyButton) {
258
375
  this.copyButton.addEventListener('click', () => this.copy())
259
376
  }
377
+ if (this.askAiButton) {
378
+ this.askAiButton.addEventListener('click', () => this.openAskAiModal())
379
+ }
260
380
  if (this.createDraftButton) {
261
381
  this.createDraftButton.addEventListener('click', () => this.createDraft())
262
382
  }
@@ -284,6 +404,471 @@
284
404
  this.refreshButton.disabled = Boolean(isBusy)
285
405
  this.refreshButton.classList.toggle('is-busy', Boolean(isBusy))
286
406
  }
407
+ setAskAiBusy(isBusy) {
408
+ if (!this.askAiButton) return
409
+ if (isBusy) {
410
+ this.askAiButton.disabled = true
411
+ this.askAiButton.innerHTML = '<i class="fa-solid fa-circle-notch fa-spin"></i><span>Loading…</span>'
412
+ } else {
413
+ this.askAiButton.innerHTML = '<i class="fa-solid fa-robot"></i><span>Ask AI</span>'
414
+ this.updateDraftSizeReview()
415
+ }
416
+ }
417
+ buildAskAiMenuUrl() {
418
+ const url = new URL('/api/plugin/menu', window.location.origin)
419
+ if (this.workspaceCwd) {
420
+ url.searchParams.set('workspace', this.workspaceCwd)
421
+ }
422
+ return `${url.pathname}${url.search}`
423
+ }
424
+ async loadAskAiTools() {
425
+ if (Array.isArray(this.askAiTools) && this.askAiTools.length > 0) {
426
+ return this.askAiTools
427
+ }
428
+ if (this.askAiToolsPromise) {
429
+ return this.askAiToolsPromise
430
+ }
431
+ this.askAiToolsPromise = fetch(this.buildAskAiMenuUrl(), {
432
+ cache: 'no-store',
433
+ headers: {
434
+ Accept: 'application/json'
435
+ }
436
+ })
437
+ .then((response) => {
438
+ if (!response.ok) {
439
+ throw new Error(`Failed to load plugins (${response.status})`)
440
+ }
441
+ return response.json()
442
+ })
443
+ .then((payload) => mapPluginMenuToAskAiTools(payload && Array.isArray(payload.menu) ? payload.menu : []))
444
+ .finally(() => {
445
+ this.askAiToolsPromise = null
446
+ })
447
+ const tools = await this.askAiToolsPromise
448
+ this.askAiTools = tools
449
+ return tools
450
+ }
451
+ buildAskAiLaunchHref(tool, prompt) {
452
+ const href = tool && typeof tool.href === 'string' ? tool.href : ''
453
+ if (!href) {
454
+ return ''
455
+ }
456
+ try {
457
+ const parsed = new URL(href, window.location.origin)
458
+ if (parsed.origin !== window.location.origin || !isPluginLauncherPath(parsed.pathname)) {
459
+ return ''
460
+ }
461
+ if (this.workspaceCwd && !parsed.searchParams.has('cwd')) {
462
+ parsed.searchParams.set('cwd', this.workspaceCwd)
463
+ }
464
+ parsed.searchParams.set('ask_ai', '1')
465
+ const question = String(prompt || '').trim()
466
+ if (question) {
467
+ parsed.searchParams.set('prompt', question)
468
+ } else {
469
+ parsed.searchParams.delete('prompt')
470
+ }
471
+ return `${parsed.pathname}${parsed.search}${parsed.hash}`
472
+ } catch (_) {
473
+ return ''
474
+ }
475
+ }
476
+ pluginInstallHref(tool) {
477
+ if (!tool || tool.hasInstall !== true || tool.hasInstalledCheck !== true || tool.installed !== false) {
478
+ return ''
479
+ }
480
+ const fallbackPath = tool.pluginPath || ''
481
+ const detailUrl = tool.detailUrl || (fallbackPath ? `/plugin?path=${encodeURIComponent(fallbackPath)}` : '')
482
+ if (!detailUrl) {
483
+ return ''
484
+ }
485
+ try {
486
+ const parsed = new URL(detailUrl, window.location.origin)
487
+ parsed.searchParams.set('next', 'install')
488
+ return `${parsed.pathname}${parsed.search}${parsed.hash}`
489
+ } catch (_) {
490
+ const separator = detailUrl.includes('?') ? '&' : '?'
491
+ return `${detailUrl}${separator}next=install`
492
+ }
493
+ }
494
+ redirectToPluginInstallIfNeeded(tool) {
495
+ const href = this.pluginInstallHref(tool)
496
+ if (!href) {
497
+ return false
498
+ }
499
+ try {
500
+ if (window.parent && window.parent !== window) {
501
+ window.parent.location.href = href
502
+ return true
503
+ }
504
+ } catch (_) {}
505
+ window.location.href = href
506
+ return true
507
+ }
508
+ dispatchAskAiLaunch(tool, prompt) {
509
+ const launchHref = this.buildAskAiLaunchHref(tool, prompt)
510
+ if (!launchHref) {
511
+ this.setStatus('Could not launch this plugin.', true)
512
+ return false
513
+ }
514
+ const payload = {
515
+ e: 'pinokio:ask-ai-launch',
516
+ workspace: this.workspace,
517
+ workspaceCwd: this.workspaceCwd,
518
+ agentHref: launchHref,
519
+ agentLabel: tool && tool.label ? tool.label : '',
520
+ prompt: String(prompt || '').trim()
521
+ }
522
+ if (window.PinokioAskAiDrawer && typeof window.PinokioAskAiDrawer.openWithUrl === 'function') {
523
+ try {
524
+ const opened = window.PinokioAskAiDrawer.openWithUrl(launchHref, {
525
+ workspaceCwd: this.workspaceCwd,
526
+ prompt: payload.prompt
527
+ })
528
+ if (opened !== false) {
529
+ this.setStatus('Launching Ask AI…')
530
+ return true
531
+ }
532
+ } catch (_) {}
533
+ }
534
+ try {
535
+ if (window.parent && window.parent !== window && typeof window.parent.postMessage === 'function') {
536
+ window.parent.postMessage(payload, '*')
537
+ this.setStatus('Launching Ask AI…')
538
+ return true
539
+ }
540
+ } catch (_) {}
541
+ window.location.href = launchHref
542
+ return true
543
+ }
544
+ createAskAiLauncher() {
545
+ if (this.askAiLauncher) {
546
+ return this.askAiLauncher
547
+ }
548
+ const overlay = document.createElement('div')
549
+ overlay.className = 'universal-launcher-overlay logs-ask-ai-launcher'
550
+ overlay.hidden = true
551
+
552
+ const panel = document.createElement('section')
553
+ panel.className = 'universal-launcher-panel logs-ask-ai-launcher-panel'
554
+ panel.setAttribute('role', 'dialog')
555
+ panel.setAttribute('aria-modal', 'true')
556
+ panel.setAttribute('aria-labelledby', 'logs-ask-ai-launcher-title')
557
+ panel.setAttribute('aria-describedby', 'logs-ask-ai-launcher-description')
558
+ overlay.appendChild(panel)
559
+
560
+ const header = document.createElement('header')
561
+ header.className = 'universal-launcher-header'
562
+ panel.appendChild(header)
563
+
564
+ const heading = document.createElement('div')
565
+ heading.className = 'universal-launcher-heading'
566
+ header.appendChild(heading)
567
+
568
+ const titleRow = document.createElement('div')
569
+ titleRow.className = 'universal-launcher-title-row'
570
+ heading.appendChild(titleRow)
571
+
572
+ const brandMark = document.createElement('span')
573
+ brandMark.className = 'universal-launcher-brand-mark'
574
+ brandMark.setAttribute('aria-hidden', 'true')
575
+ titleRow.appendChild(brandMark)
576
+
577
+ const title = document.createElement('h3')
578
+ title.className = 'universal-launcher-title'
579
+ title.id = 'logs-ask-ai-launcher-title'
580
+ title.textContent = 'Ask Pinokio'
581
+ titleRow.appendChild(title)
582
+
583
+ const description = document.createElement('p')
584
+ description.className = 'universal-launcher-description'
585
+ description.id = 'logs-ask-ai-launcher-description'
586
+ description.textContent = 'Launch a local agent with this log report open.'
587
+ heading.appendChild(description)
588
+
589
+ const closeButton = document.createElement('button')
590
+ closeButton.type = 'button'
591
+ closeButton.className = 'universal-launcher-close'
592
+ closeButton.setAttribute('aria-label', 'Close Ask Pinokio')
593
+ closeButton.innerHTML = '<i class="fa-solid fa-xmark" aria-hidden="true"></i>'
594
+ header.appendChild(closeButton)
595
+
596
+ const body = document.createElement('div')
597
+ body.className = 'universal-launcher-body logs-ask-ai-launcher-body'
598
+ panel.appendChild(body)
599
+
600
+ const promptSection = document.createElement('section')
601
+ promptSection.className = 'universal-launcher-section universal-launcher-section-prompt'
602
+ body.appendChild(promptSection)
603
+
604
+ const promptHeading = document.createElement('div')
605
+ promptHeading.className = 'universal-launcher-section-heading'
606
+ promptSection.appendChild(promptHeading)
607
+
608
+ const promptTitle = document.createElement('div')
609
+ promptTitle.className = 'universal-launcher-section-title'
610
+ promptTitle.textContent = 'What should Pinokio do?'
611
+ promptHeading.appendChild(promptTitle)
612
+
613
+ const composer = document.createElement('div')
614
+ composer.className = 'universal-launcher-ask-composer is-ask-intent'
615
+ promptSection.appendChild(composer)
616
+
617
+ const promptTextarea = document.createElement('textarea')
618
+ promptTextarea.className = 'universal-launcher-textarea logs-ask-ai-launcher-textarea'
619
+ promptTextarea.rows = 3
620
+ promptTextarea.setAttribute('aria-label', 'Question')
621
+ composer.appendChild(promptTextarea)
622
+
623
+ const error = document.createElement('div')
624
+ error.className = 'universal-launcher-error'
625
+ body.appendChild(error)
626
+
627
+ const footer = document.createElement('footer')
628
+ footer.className = 'universal-launcher-footer logs-ask-ai-launcher-footer'
629
+ panel.appendChild(footer)
630
+
631
+ const toolSection = document.createElement('section')
632
+ toolSection.className = 'universal-launcher-section universal-launcher-section-tools footer-mounted logs-ask-ai-launcher-tool-section'
633
+ footer.appendChild(toolSection)
634
+
635
+ const toolPicker = document.createElement('div')
636
+ toolPicker.className = 'universal-launcher-tool-picker logs-ask-ai-tool-picker'
637
+ toolSection.appendChild(toolPicker)
638
+
639
+ const toolTrigger = document.createElement('div')
640
+ toolTrigger.className = 'universal-launcher-tool-trigger has-value logs-ask-ai-tool-trigger'
641
+ toolTrigger.setAttribute('aria-hidden', 'true')
642
+ toolPicker.appendChild(toolTrigger)
643
+
644
+ const toolIcon = document.createElement('span')
645
+ toolIcon.className = 'universal-launcher-tool-trigger-icon logs-ask-ai-tool-trigger-icon'
646
+ toolTrigger.appendChild(toolIcon)
647
+
648
+ const toolContent = document.createElement('div')
649
+ toolContent.className = 'universal-launcher-tool-trigger-content'
650
+ toolTrigger.appendChild(toolContent)
651
+
652
+ const toolLabel = document.createElement('div')
653
+ toolLabel.className = 'universal-launcher-tool-trigger-label'
654
+ toolContent.appendChild(toolLabel)
655
+
656
+ const toolMeta = document.createElement('div')
657
+ toolMeta.className = 'universal-launcher-tool-trigger-meta'
658
+ toolContent.appendChild(toolMeta)
659
+
660
+ const toolCaret = document.createElement('i')
661
+ toolCaret.className = 'fa-solid fa-chevron-down universal-launcher-tool-trigger-caret'
662
+ toolCaret.setAttribute('aria-hidden', 'true')
663
+ toolTrigger.appendChild(toolCaret)
664
+
665
+ const toolSelect = document.createElement('select')
666
+ toolSelect.className = 'logs-ask-ai-tool-select'
667
+ toolSelect.setAttribute('aria-label', 'Agent')
668
+ toolPicker.appendChild(toolSelect)
669
+
670
+ const footerActions = document.createElement('div')
671
+ footerActions.className = 'universal-launcher-footer-actions'
672
+ footer.appendChild(footerActions)
673
+
674
+ const runButton = document.createElement('button')
675
+ runButton.type = 'button'
676
+ runButton.className = 'universal-launcher-button universal-launcher-button-primary'
677
+ runButton.textContent = 'Run'
678
+ footerActions.appendChild(runButton)
679
+
680
+ let returnFocusEl = null
681
+ const syncRunState = () => {
682
+ runButton.disabled = !String(promptTextarea.value || '').trim() || !this.selectedAskAiTool()
683
+ }
684
+ const getFocusable = () => {
685
+ return Array.from(panel.querySelectorAll('a[href], button:not([disabled]), textarea:not([disabled]), select:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])'))
686
+ .filter((node) => node && !node.hidden && node.offsetParent !== null)
687
+ }
688
+ const setOpen = (isOpen) => {
689
+ overlay.hidden = !isOpen
690
+ document.documentElement.classList.toggle('universal-launcher-open', isOpen)
691
+ document.body.classList.toggle('universal-launcher-open', isOpen)
692
+ if (isOpen) {
693
+ returnFocusEl = document.activeElement && typeof document.activeElement.focus === 'function'
694
+ ? document.activeElement
695
+ : this.askAiButton
696
+ window.requestAnimationFrame(() => {
697
+ promptTextarea.focus()
698
+ const cursorIndex = promptTextarea.value.length
699
+ try {
700
+ promptTextarea.setSelectionRange(cursorIndex, cursorIndex)
701
+ } catch (_) {}
702
+ })
703
+ } else if (returnFocusEl && typeof returnFocusEl.focus === 'function' && document.contains(returnFocusEl)) {
704
+ try {
705
+ returnFocusEl.focus()
706
+ } catch (_) {}
707
+ returnFocusEl = null
708
+ }
709
+ }
710
+ const close = () => setOpen(false)
711
+ const syncTool = () => {
712
+ const tool = this.selectedAskAiTool()
713
+ toolLabel.textContent = tool ? tool.label : 'Choose agent'
714
+ toolMeta.textContent = tool ? (tool.category || 'Plugin') : ''
715
+ toolIcon.textContent = ''
716
+ while (toolIcon.firstChild) {
717
+ toolIcon.removeChild(toolIcon.firstChild)
718
+ }
719
+ if (tool && tool.iconSrc) {
720
+ const icon = document.createElement('img')
721
+ icon.src = tool.iconSrc
722
+ icon.alt = ''
723
+ icon.className = 'logs-ask-ai-tool-trigger-image'
724
+ toolIcon.appendChild(icon)
725
+ } else {
726
+ const icon = document.createElement('i')
727
+ icon.className = 'fa-solid fa-robot'
728
+ icon.setAttribute('aria-hidden', 'true')
729
+ toolIcon.appendChild(icon)
730
+ }
731
+ syncRunState()
732
+ }
733
+ const run = () => {
734
+ const prompt = String(promptTextarea.value || '').trim()
735
+ const tool = this.selectedAskAiTool()
736
+ if (!prompt) {
737
+ error.textContent = 'Enter a question for the agent.'
738
+ promptTextarea.focus()
739
+ return
740
+ }
741
+ if (!tool) {
742
+ error.textContent = 'Choose an agent.'
743
+ toolSelect.focus()
744
+ return
745
+ }
746
+ error.textContent = ''
747
+ if (this.redirectToPluginInstallIfNeeded(tool)) {
748
+ close()
749
+ return
750
+ }
751
+ if (this.dispatchAskAiLaunch(tool, prompt)) {
752
+ close()
753
+ }
754
+ }
755
+
756
+ closeButton.addEventListener('click', close)
757
+ overlay.addEventListener('click', (event) => {
758
+ if (event.target === overlay) {
759
+ close()
760
+ }
761
+ })
762
+ toolSelect.addEventListener('change', syncTool)
763
+ toolSelect.addEventListener('change', () => {
764
+ const tool = this.selectedAskAiTool()
765
+ setStoredToolPreference(tool && tool.value ? tool.value : '')
766
+ })
767
+ promptTextarea.addEventListener('input', syncRunState)
768
+ runButton.addEventListener('click', run)
769
+ overlay.addEventListener('keydown', (event) => {
770
+ event.stopPropagation()
771
+ if (event.key === 'Escape') {
772
+ event.preventDefault()
773
+ close()
774
+ } else if (event.key === 'Tab') {
775
+ const focusable = getFocusable()
776
+ if (focusable.length === 0) {
777
+ event.preventDefault()
778
+ return
779
+ }
780
+ const first = focusable[0]
781
+ const last = focusable[focusable.length - 1]
782
+ if (event.shiftKey && document.activeElement === first) {
783
+ event.preventDefault()
784
+ last.focus()
785
+ } else if (!event.shiftKey && document.activeElement === last) {
786
+ event.preventDefault()
787
+ first.focus()
788
+ }
789
+ } else if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {
790
+ event.preventDefault()
791
+ if (!runButton.disabled) {
792
+ run()
793
+ }
794
+ }
795
+ })
796
+
797
+ document.body.appendChild(overlay)
798
+ this.askAiLauncher = {
799
+ overlay,
800
+ panel,
801
+ promptTextarea,
802
+ toolSelect,
803
+ toolLabel,
804
+ toolMeta,
805
+ toolIcon,
806
+ error,
807
+ runButton,
808
+ setOpen,
809
+ syncTool,
810
+ syncRunState
811
+ }
812
+ return this.askAiLauncher
813
+ }
814
+ populateAskAiLauncherTools(tools) {
815
+ const launcher = this.createAskAiLauncher()
816
+ while (launcher.toolSelect.firstChild) {
817
+ launcher.toolSelect.removeChild(launcher.toolSelect.firstChild)
818
+ }
819
+ const placeholder = document.createElement('option')
820
+ placeholder.value = ''
821
+ placeholder.textContent = 'Choose agent'
822
+ launcher.toolSelect.appendChild(placeholder)
823
+ tools.forEach((tool, index) => {
824
+ const option = document.createElement('option')
825
+ option.value = String(index)
826
+ option.textContent = askAiToolOptionLabel(tool)
827
+ launcher.toolSelect.appendChild(option)
828
+ })
829
+ const preferredTool = getStoredToolPreference()
830
+ const preferredIndex = preferredTool
831
+ ? tools.findIndex((tool) => tool && tool.value === preferredTool)
832
+ : -1
833
+ launcher.toolSelect.value = preferredIndex >= 0 ? String(preferredIndex) : ''
834
+ if (preferredTool && preferredIndex < 0) {
835
+ setStoredToolPreference('')
836
+ }
837
+ launcher.syncTool()
838
+ }
839
+ selectedAskAiTool() {
840
+ const launcher = this.askAiLauncher
841
+ if (!launcher || !Array.isArray(this.askAiTools)) {
842
+ return null
843
+ }
844
+ const index = Number.parseInt(launcher.toolSelect.value, 10)
845
+ return Number.isFinite(index) ? this.askAiTools[index] || null : null
846
+ }
847
+ async openAskAiModal() {
848
+ if (!this.currentMarkdown && !this.reviewMarkdown) {
849
+ this.updateDraftSizeReview()
850
+ return
851
+ }
852
+ this.setAskAiBusy(true)
853
+ let tools = []
854
+ try {
855
+ tools = await this.loadAskAiTools()
856
+ } catch (error) {
857
+ this.setStatus(error && error.message ? error.message : 'Failed to load plugins.', true)
858
+ this.setAskAiBusy(false)
859
+ return
860
+ }
861
+ this.setAskAiBusy(false)
862
+ if (!Array.isArray(tools) || tools.length === 0) {
863
+ this.setStatus('No AI plugins are available.', true)
864
+ return
865
+ }
866
+ const launcher = this.createAskAiLauncher()
867
+ this.populateAskAiLauncherTools(tools)
868
+ launcher.error.textContent = ''
869
+ launcher.promptTextarea.value = ASK_AI_DEFAULT_PROMPT
870
+ launcher.setOpen(true)
871
+ }
287
872
  formatGenerated(value) {
288
873
  if (!value) return '--'
289
874
  const date = new Date(value)
@@ -393,7 +978,7 @@
393
978
  updateDraftTitleNote() {
394
979
  if (!this.draftTitleNoteEl) return
395
980
  let note = ''
396
- if (this.draftTitleInput && !this.draftTitleInput.disabled && !this.draftTitleInput.value.trim()) {
981
+ if (this.draftTitleInput && this.draftTitleInput.type !== 'hidden' && !this.draftTitleInput.disabled && !this.draftTitleInput.value.trim()) {
397
982
  note = 'Title required'
398
983
  }
399
984
  this.draftTitleNoteEl.textContent = note
@@ -402,7 +987,7 @@
402
987
  this.draftTitleSuggestion = this.suggestDraftTitle()
403
988
  if (this.draftTitleInput) {
404
989
  if (force || !this.draftTitleEdited) {
405
- this.draftTitleInput.value = this.draftTitleSuggestion.title
990
+ this.draftTitleInput.value = this.draftTitleSuggestion.title || this.defaultDraftTitle()
406
991
  this.draftTitleEdited = false
407
992
  }
408
993
  this.draftTitleInput.disabled = !this.reviewMarkdown
@@ -882,7 +1467,7 @@
882
1467
  isError = true
883
1468
  message = 'Add a title before posting to Community.'
884
1469
  } else if (hasText) {
885
- message = 'Ready. Community will use this preview exactly.'
1470
+ message = 'Ready for Ask AI or Community.'
886
1471
  }
887
1472
  this.draftStatusEl.textContent = message
888
1473
  this.draftStatusEl.classList.toggle('is-error', isError)
@@ -891,6 +1476,11 @@
891
1476
  this.draftTitleInput.disabled = !hasText
892
1477
  }
893
1478
  this.updateDraftTitleNote()
1479
+ if (this.askAiButton) {
1480
+ const canAsk = hasText && !this.filtering
1481
+ this.askAiButton.disabled = !canAsk
1482
+ this.askAiButton.title = canAsk ? 'Launch a local AI agent with this report context' : 'Latest report is not ready yet'
1483
+ }
894
1484
  if (this.createDraftButton && !this.importingDraft) {
895
1485
  const canCreate = hasText && hasTitle && !this.draftOversized && !this.filtering && Boolean(this.draftUrl)
896
1486
  this.createDraftButton.disabled = !canCreate
@@ -1766,6 +2356,7 @@
1766
2356
  statusEl: document.getElementById('logs-report-status'),
1767
2357
  outputEl: document.getElementById('logs-report-output'),
1768
2358
  copyButton: document.getElementById('logs-copy-report'),
2359
+ askAiButton: document.getElementById('logs-ask-ai'),
1769
2360
  createDraftButton: document.getElementById('logs-create-draft'),
1770
2361
  draftTitleInput: document.getElementById('logs-draft-title'),
1771
2362
  draftTitleNoteEl: document.getElementById('logs-draft-title-note'),
@@ -1779,7 +2370,9 @@
1779
2370
  draftStatusEl: document.getElementById('logs-draft-status'),
1780
2371
  reviewListEl: document.getElementById('logs-redaction-list'),
1781
2372
  reviewFiltersEl: document.getElementById('logs-redaction-filters'),
1782
- reviewCountEl: document.getElementById('logs-redaction-count')
2373
+ reviewCountEl: document.getElementById('logs-redaction-count'),
2374
+ workspace: this.workspace,
2375
+ workspaceCwd: config.workspaceCwd || ''
1783
2376
  })
1784
2377
  this.viewer = new LogsViewer({
1785
2378
  outputEl: document.getElementById('logs-viewer-output'),