@pheem49/mint 1.6.4 → 1.7.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.
- package/Cargo.toml +1 -1
- package/bin/mint +0 -0
- package/crates/mint-cli/src/agent.rs +42 -14
- package/crates/mint-cli/src/main.rs +412 -2
- package/crates/mint-cli/src/setup.rs +173 -32
- package/crates/mint-core/src/api_server.rs +35 -0
- package/crates/mint-core/src/config.rs +2 -2
- package/crates/mint-core/src/memory.rs +27 -1
- package/crates/mint-core/src/orchestration.rs +1 -1
- package/crates/mint-core/src/plugins.rs +35 -0
- package/crates/mint-core/tests/memory_persistence.rs +28 -0
- package/package.json +12 -2
- package/src/renderer/src/agentProgress.ts +39 -0
- package/src/renderer/src/components/ChatPanel.tsx +308 -34
- package/src/renderer/src/components/DashboardSidebar.tsx +3 -3
- package/src/renderer/src/components/MintDashboard.tsx +71 -27
- package/src/renderer/src/components/ModelPanel.tsx +2 -2
- package/src/renderer/src/components/PicturesLibrary.tsx +5 -1
- package/src/renderer/src/components/SpotlightWindow.tsx +8 -8
- package/src/renderer/src/components/WorkspacePanel.tsx +1 -1
- package/src/renderer/src/css/styles.css +195 -17
- package/src/renderer/src/index.css +46 -0
- package/src/renderer/src/tauri.ts +25 -0
- package/src/renderer/src-web/App.tsx +16 -1
- package/src/renderer/src-web/agentProgress.ts +39 -0
- package/src/renderer/src-web/components/ChatPanel.tsx +309 -35
- package/src/renderer/src-web/components/DashboardSidebar.tsx +1 -1
- package/src/renderer/src-web/components/MintDashboard.tsx +53 -16
- package/src/renderer/src-web/components/PicturesLibrary.tsx +5 -1
- package/src/renderer/src-web/css/styles.css +242 -18
- package/src/renderer/src-web/index.css +46 -0
- package/src/renderer/src-web/tauri.ts +25 -0
- package/src-tauri/src/lib.rs +16 -0
- package/src-tauri/tauri.conf.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect, useRef, useState, Fragment, type ChangeEvent, type ClipboardEvent, type DragEvent, type FormEvent, type KeyboardEvent, type RefObject } from 'react'
|
|
2
|
+
import { hasAgentToolActivity, thoughtsFrom } from '../agentProgress'
|
|
2
3
|
import {
|
|
3
4
|
type AgentProgress,
|
|
4
5
|
type ChatResponse,
|
|
@@ -327,6 +328,8 @@ interface ChatPanelProps {
|
|
|
327
328
|
streamedResponse: ChatResponse | null
|
|
328
329
|
agentProgress: AgentProgress[]
|
|
329
330
|
agentActivitySnapshots: Record<string, AgentProgress[]>
|
|
331
|
+
thinkingExpanded: Record<string, boolean>
|
|
332
|
+
onThinkingExpandedChange: (key: string, open: boolean) => void
|
|
330
333
|
message: string
|
|
331
334
|
imageAttachments: Array<{ dataUri: string; name: string; previewDataUri?: string }>
|
|
332
335
|
documentName: string
|
|
@@ -419,6 +422,11 @@ function ChatCodeBlock({ code, language }: { code: string; language: string; key
|
|
|
419
422
|
)
|
|
420
423
|
}
|
|
421
424
|
|
|
425
|
+
const isTableLine = (line: string): boolean => {
|
|
426
|
+
const trimmed = line.trim()
|
|
427
|
+
return trimmed.startsWith('|') && trimmed.endsWith('|') && trimmed.length > 1
|
|
428
|
+
}
|
|
429
|
+
|
|
422
430
|
function renderFormattedMessage(text: string) {
|
|
423
431
|
const displayText = readableAssistantText(text)
|
|
424
432
|
if (!displayText) return null
|
|
@@ -447,6 +455,113 @@ function renderFormattedMessage(text: string) {
|
|
|
447
455
|
})
|
|
448
456
|
}
|
|
449
457
|
|
|
458
|
+
const renderHtmlTable = (tableLines: string[], key: string) => {
|
|
459
|
+
const rows: string[][] = []
|
|
460
|
+
for (const line of tableLines) {
|
|
461
|
+
const trimmed = line.trim()
|
|
462
|
+
if (!trimmed) continue
|
|
463
|
+
const content = trimmed.slice(1, -1)
|
|
464
|
+
const contentEscaped = content.replace(/\\\|/g, '\u0000')
|
|
465
|
+
const cols = contentEscaped.split('|').map(s => s.replace(/\u0000/g, '|').trim())
|
|
466
|
+
rows.push(cols)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (rows.length === 0) return null
|
|
470
|
+
|
|
471
|
+
let hasSeparator = false
|
|
472
|
+
let separatorIdx = -1
|
|
473
|
+
for (let i = 0; i < rows.length; i++) {
|
|
474
|
+
const row = rows[i]
|
|
475
|
+
if (row.length > 0 && row.every(col => /^[-\s:]+$/.test(col))) {
|
|
476
|
+
hasSeparator = true
|
|
477
|
+
separatorIdx = i
|
|
478
|
+
break
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
let headerRow: string[] | null = null
|
|
483
|
+
const dataRows: string[][] = []
|
|
484
|
+
|
|
485
|
+
if (hasSeparator) {
|
|
486
|
+
if (separatorIdx > 0) {
|
|
487
|
+
headerRow = rows[separatorIdx - 1]
|
|
488
|
+
for (let i = 0; i < rows.length; i++) {
|
|
489
|
+
if (i !== separatorIdx && i !== separatorIdx - 1) {
|
|
490
|
+
dataRows.push(rows[i])
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
} else {
|
|
494
|
+
for (let i = 0; i < rows.length; i++) {
|
|
495
|
+
if (i !== separatorIdx) {
|
|
496
|
+
dataRows.push(rows[i])
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
} else {
|
|
501
|
+
headerRow = rows[0]
|
|
502
|
+
for (let i = 1; i < rows.length; i++) {
|
|
503
|
+
dataRows.push(rows[i])
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return (
|
|
508
|
+
<div key={key} className="chat-table-container" style={{
|
|
509
|
+
overflowX: 'auto',
|
|
510
|
+
margin: '14px 0',
|
|
511
|
+
width: '100%',
|
|
512
|
+
borderRadius: '8px',
|
|
513
|
+
border: '1px solid rgba(255, 255, 255, 0.12)',
|
|
514
|
+
background: 'rgba(30, 41, 59, 0.35)',
|
|
515
|
+
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
|
516
|
+
}}>
|
|
517
|
+
<table className="chat-table" style={{
|
|
518
|
+
width: '100%',
|
|
519
|
+
borderCollapse: 'collapse',
|
|
520
|
+
fontSize: '0.86rem',
|
|
521
|
+
textAlign: 'left',
|
|
522
|
+
lineHeight: '1.5',
|
|
523
|
+
}}>
|
|
524
|
+
{headerRow && (
|
|
525
|
+
<thead>
|
|
526
|
+
<tr style={{
|
|
527
|
+
background: 'rgba(255, 255, 255, 0.04)',
|
|
528
|
+
borderBottom: '2px solid rgba(255, 255, 255, 0.15)',
|
|
529
|
+
}}>
|
|
530
|
+
{headerRow.map((col, idx) => (
|
|
531
|
+
<th key={`th-${idx}`} style={{
|
|
532
|
+
padding: '12px 16px',
|
|
533
|
+
fontWeight: 700,
|
|
534
|
+
color: 'var(--accent, #38bdf8)',
|
|
535
|
+
fontFamily: 'ui-sans-serif, system-ui, sans-serif',
|
|
536
|
+
}}>
|
|
537
|
+
{formatInline(col)}
|
|
538
|
+
</th>
|
|
539
|
+
))}
|
|
540
|
+
</tr>
|
|
541
|
+
</thead>
|
|
542
|
+
)}
|
|
543
|
+
<tbody>
|
|
544
|
+
{dataRows.map((row, rIdx) => (
|
|
545
|
+
<tr key={`tr-${rIdx}`} style={{
|
|
546
|
+
background: rIdx % 2 === 1 ? 'rgba(255, 255, 255, 0.015)' : 'transparent',
|
|
547
|
+
borderBottom: rIdx < dataRows.length - 1 ? '1px solid rgba(255, 255, 255, 0.08)' : 'none',
|
|
548
|
+
}}>
|
|
549
|
+
{row.map((col, cIdx) => (
|
|
550
|
+
<td key={`td-${cIdx}`} style={{
|
|
551
|
+
padding: '12px 16px',
|
|
552
|
+
color: 'var(--text-main, #e2e8f0)',
|
|
553
|
+
}}>
|
|
554
|
+
{formatInline(col)}
|
|
555
|
+
</td>
|
|
556
|
+
))}
|
|
557
|
+
</tr>
|
|
558
|
+
))}
|
|
559
|
+
</tbody>
|
|
560
|
+
</table>
|
|
561
|
+
</div>
|
|
562
|
+
)
|
|
563
|
+
}
|
|
564
|
+
|
|
450
565
|
const lines = displayText.split('\n')
|
|
451
566
|
const items: any[] = []
|
|
452
567
|
|
|
@@ -454,10 +569,24 @@ function renderFormattedMessage(text: string) {
|
|
|
454
569
|
let codeBlockLang = ''
|
|
455
570
|
let codeBlockLines: string[] = []
|
|
456
571
|
|
|
572
|
+
let inTable = false
|
|
573
|
+
let tableLines: string[] = []
|
|
574
|
+
|
|
575
|
+
const flushTable = (index: number) => {
|
|
576
|
+
if (tableLines.length > 0) {
|
|
577
|
+
items.push(renderHtmlTable(tableLines, `table-${index}`))
|
|
578
|
+
tableLines = []
|
|
579
|
+
inTable = false
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
457
583
|
for (let i = 0; i < lines.length; i++) {
|
|
458
584
|
const line = lines[i]
|
|
459
585
|
|
|
460
586
|
if (line.trim().startsWith('```')) {
|
|
587
|
+
if (inTable) {
|
|
588
|
+
flushTable(i)
|
|
589
|
+
}
|
|
461
590
|
if (inCodeBlock) {
|
|
462
591
|
const codeText = codeBlockLines.join('\n')
|
|
463
592
|
items.push(
|
|
@@ -481,35 +610,60 @@ function renderFormattedMessage(text: string) {
|
|
|
481
610
|
continue
|
|
482
611
|
}
|
|
483
612
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
fontWeight: 'bold',
|
|
491
|
-
display: 'block',
|
|
492
|
-
marginTop: level === 1 ? '16px' : level === 2 ? '14px' : '10px',
|
|
493
|
-
marginBottom: '6px',
|
|
494
|
-
fontSize: level === 1 ? '1.25em' : level === 2 ? '1.15em' : '1.05em',
|
|
495
|
-
color: 'var(--text-main)',
|
|
613
|
+
if (isTableLine(line)) {
|
|
614
|
+
inTable = true
|
|
615
|
+
tableLines.push(line)
|
|
616
|
+
} else {
|
|
617
|
+
if (inTable) {
|
|
618
|
+
flushTable(i)
|
|
496
619
|
}
|
|
497
620
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
621
|
+
const headerMatch = line.match(/^(#{1,6})\s+(.*)$/)
|
|
622
|
+
if (headerMatch) {
|
|
623
|
+
const level = headerMatch[1].length
|
|
624
|
+
const content = headerMatch[2]
|
|
625
|
+
|
|
626
|
+
const style = {
|
|
627
|
+
fontWeight: 'bold',
|
|
628
|
+
display: 'block',
|
|
629
|
+
marginTop: level === 1 ? '16px' : level === 2 ? '14px' : '10px',
|
|
630
|
+
marginBottom: '6px',
|
|
631
|
+
fontSize: level === 1 ? '1.25em' : level === 2 ? '1.15em' : '1.05em',
|
|
632
|
+
color: 'var(--text-main)',
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
items.push(
|
|
636
|
+
<span key={`line-${i}`} style={style}>
|
|
637
|
+
{formatInline(content)}
|
|
638
|
+
</span>
|
|
639
|
+
)
|
|
640
|
+
} else {
|
|
641
|
+
const listMatch = line.match(/^(\s*)([-*+])\s+(.*)$/)
|
|
642
|
+
if (listMatch) {
|
|
643
|
+
const indent = listMatch[1]
|
|
644
|
+
const content = listMatch[3]
|
|
645
|
+
items.push(
|
|
646
|
+
<Fragment key={`line-${i}`}>
|
|
647
|
+
{indent}• {formatInline(content)}
|
|
648
|
+
{i < lines.length - 1 && '\n'}
|
|
649
|
+
</Fragment>
|
|
650
|
+
)
|
|
651
|
+
} else {
|
|
652
|
+
items.push(
|
|
653
|
+
<Fragment key={`line-${i}`}>
|
|
654
|
+
{formatInline(line)}
|
|
655
|
+
{i < lines.length - 1 && '\n'}
|
|
656
|
+
</Fragment>
|
|
657
|
+
)
|
|
658
|
+
}
|
|
659
|
+
}
|
|
510
660
|
}
|
|
511
661
|
}
|
|
512
662
|
|
|
663
|
+
if (inTable) {
|
|
664
|
+
flushTable(lines.length)
|
|
665
|
+
}
|
|
666
|
+
|
|
513
667
|
if (inCodeBlock && codeBlockLines.length > 0) {
|
|
514
668
|
const codeText = codeBlockLines.join('\n')
|
|
515
669
|
items.push(
|
|
@@ -524,6 +678,98 @@ function renderFormattedMessage(text: string) {
|
|
|
524
678
|
return items
|
|
525
679
|
}
|
|
526
680
|
|
|
681
|
+
const THINKING_LABELS = {
|
|
682
|
+
live: 'Thinking…',
|
|
683
|
+
completed: (count: number) => `Thinking process (${count} steps)`,
|
|
684
|
+
step: (index: number) => `Step ${index}`,
|
|
685
|
+
emptyHint: 'Model didn\'t send thinking steps this time',
|
|
686
|
+
} as const
|
|
687
|
+
|
|
688
|
+
function ThinkingBlock({
|
|
689
|
+
thoughts,
|
|
690
|
+
isLive = false,
|
|
691
|
+
blockKey,
|
|
692
|
+
expanded,
|
|
693
|
+
onExpandedChange,
|
|
694
|
+
showEmptyHint = false,
|
|
695
|
+
}: {
|
|
696
|
+
thoughts: string[]
|
|
697
|
+
isLive?: boolean
|
|
698
|
+
blockKey: string
|
|
699
|
+
expanded?: boolean
|
|
700
|
+
onExpandedChange?: (key: string, open: boolean) => void
|
|
701
|
+
showEmptyHint?: boolean
|
|
702
|
+
}) {
|
|
703
|
+
const contentRef = useRef<HTMLDivElement>(null)
|
|
704
|
+
const isControlled = expanded !== undefined && Boolean(onExpandedChange)
|
|
705
|
+
const [localOpen, setLocalOpen] = useState(isLive)
|
|
706
|
+
const isOpen = isControlled ? Boolean(expanded) : localOpen
|
|
707
|
+
|
|
708
|
+
const setOpen = (open: boolean) => {
|
|
709
|
+
if (isControlled && onExpandedChange) onExpandedChange(blockKey, open)
|
|
710
|
+
else setLocalOpen(open)
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
useEffect(() => {
|
|
714
|
+
if (isLive && !isControlled) setLocalOpen(true)
|
|
715
|
+
}, [isLive, isControlled])
|
|
716
|
+
|
|
717
|
+
useEffect(() => {
|
|
718
|
+
if (!isLive || !isOpen || !contentRef.current) return
|
|
719
|
+
contentRef.current.scrollTop = contentRef.current.scrollHeight
|
|
720
|
+
}, [thoughts, isLive, isOpen])
|
|
721
|
+
|
|
722
|
+
if (thoughts.length === 0 && !showEmptyHint) return null
|
|
723
|
+
|
|
724
|
+
if (thoughts.length === 0) {
|
|
725
|
+
return (
|
|
726
|
+
<div className="thinking-block thinking-block-empty-state" data-live={isLive ? 'true' : 'false'}>
|
|
727
|
+
<span className="thinking-block-empty">{THINKING_LABELS.emptyHint}</span>
|
|
728
|
+
</div>
|
|
729
|
+
)
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
return (
|
|
733
|
+
<div className={`thinking-block${isLive ? ' is-live' : ''}`}>
|
|
734
|
+
<button
|
|
735
|
+
type="button"
|
|
736
|
+
className="thinking-block-header"
|
|
737
|
+
onClick={() => setOpen(!isOpen)}
|
|
738
|
+
aria-expanded={isOpen}
|
|
739
|
+
>
|
|
740
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ flexShrink: 0 }}>
|
|
741
|
+
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
|
|
742
|
+
<line x1="12" y1="17" x2="12.01" y2="17" />
|
|
743
|
+
<circle cx="12" cy="12" r="10" />
|
|
744
|
+
</svg>
|
|
745
|
+
<span className="thinking-block-label">
|
|
746
|
+
{isLive ? THINKING_LABELS.live : THINKING_LABELS.completed(thoughts.length)}
|
|
747
|
+
</span>
|
|
748
|
+
{isLive && <span className="thinking-block-live-dot" aria-hidden="true" />}
|
|
749
|
+
<span className={`thinking-block-chevron${isOpen ? ' is-open' : ''}`} aria-hidden="true">
|
|
750
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
751
|
+
<polyline points="9 18 15 12 9 6" />
|
|
752
|
+
</svg>
|
|
753
|
+
</span>
|
|
754
|
+
</button>
|
|
755
|
+
{isOpen && (
|
|
756
|
+
<div className="thinking-block-body">
|
|
757
|
+
<div className="thinking-block-content" ref={contentRef}>
|
|
758
|
+
{thoughts.map((thought, index) => (
|
|
759
|
+
<div className="thinking-block-step" key={`${blockKey}-${index}`}>
|
|
760
|
+
{thoughts.length > 1 && (
|
|
761
|
+
<div className="thinking-block-step-label">{THINKING_LABELS.step(index + 1)}</div>
|
|
762
|
+
)}
|
|
763
|
+
<div className="thinking-block-step-body">{renderFormattedMessage(thought)}</div>
|
|
764
|
+
</div>
|
|
765
|
+
))}
|
|
766
|
+
</div>
|
|
767
|
+
</div>
|
|
768
|
+
)}
|
|
769
|
+
</div>
|
|
770
|
+
)
|
|
771
|
+
}
|
|
772
|
+
|
|
527
773
|
function readableAssistantText(text: string) {
|
|
528
774
|
if (typeof text !== 'string') return ''
|
|
529
775
|
const trimmed = text.trim()
|
|
@@ -693,6 +939,8 @@ export default function ChatPanel({
|
|
|
693
939
|
streamedResponse,
|
|
694
940
|
agentProgress,
|
|
695
941
|
agentActivitySnapshots,
|
|
942
|
+
thinkingExpanded,
|
|
943
|
+
onThinkingExpandedChange,
|
|
696
944
|
message,
|
|
697
945
|
imageAttachments,
|
|
698
946
|
documentName,
|
|
@@ -770,7 +1018,7 @@ export default function ChatPanel({
|
|
|
770
1018
|
const canSubmit = Boolean(message.trim() || imageAttachments.length > 0 || documentName)
|
|
771
1019
|
const sendingImageMarkers = Array.from({ length: sendingImageCount }, (_, index) => `[Image #${index + 1}]`).join(' ')
|
|
772
1020
|
const voiceStatus = speakingText ? 'speaking' : (sending || voiceAwaitingResponse) ? 'thinking' : isRecording ? 'listening' : voiceMode ? 'ready' : 'off'
|
|
773
|
-
const voiceStatusLabel = voiceStatus === 'speaking' ? '
|
|
1021
|
+
const voiceStatusLabel = voiceStatus === 'speaking' ? 'Speaking' : voiceStatus === 'thinking' ? 'Thinking' : voiceStatus === 'listening' ? 'Listening' : 'Ready'
|
|
774
1022
|
|
|
775
1023
|
const getAvailableModels = (provider: string) => {
|
|
776
1024
|
switch (provider) {
|
|
@@ -954,7 +1202,7 @@ export default function ChatPanel({
|
|
|
954
1202
|
}
|
|
955
1203
|
const startAudioRecording = async (autoSend: boolean) => {
|
|
956
1204
|
if (!navigator.mediaDevices?.getUserMedia || typeof MediaRecorder === 'undefined') {
|
|
957
|
-
alert('
|
|
1205
|
+
alert('Sorry, this system cannot access the microphone or record audio in this WebView')
|
|
958
1206
|
voiceModeRef.current = false
|
|
959
1207
|
setVoiceMode(false)
|
|
960
1208
|
return
|
|
@@ -977,7 +1225,7 @@ export default function ChatPanel({
|
|
|
977
1225
|
mediaStreamRef.current = stream
|
|
978
1226
|
mediaRecorderRef.current = recorder
|
|
979
1227
|
audioContextRef.current = audioContext
|
|
980
|
-
setVoiceTranscript('
|
|
1228
|
+
setVoiceTranscript('Listening to microphone...')
|
|
981
1229
|
|
|
982
1230
|
recorder.ondataavailable = (event) => {
|
|
983
1231
|
if (event.data.size > 0) chunks.push(event.data)
|
|
@@ -992,13 +1240,13 @@ export default function ChatPanel({
|
|
|
992
1240
|
setIsRecording(false)
|
|
993
1241
|
if (!autoSend || !voiceModeRef.current || chunks.length === 0) return
|
|
994
1242
|
if (!heardVoice) {
|
|
995
|
-
setVoiceTranscript('
|
|
1243
|
+
setVoiceTranscript('No speech detected')
|
|
996
1244
|
scheduleVoiceListen(700)
|
|
997
1245
|
return
|
|
998
1246
|
}
|
|
999
1247
|
const blob = new Blob(chunks, { type: recorder.mimeType || 'audio/webm' })
|
|
1000
1248
|
const audioDataUri = await blobToDataUri(blob)
|
|
1001
|
-
setVoiceTranscript('
|
|
1249
|
+
setVoiceTranscript('Sent audio to AI')
|
|
1002
1250
|
voiceAwaitingResponseRef.current = true
|
|
1003
1251
|
setVoiceAwaitingResponse(true)
|
|
1004
1252
|
onSendVoiceMessage('', audioDataUri)
|
|
@@ -1022,7 +1270,7 @@ export default function ChatPanel({
|
|
|
1022
1270
|
if (peak > 12) {
|
|
1023
1271
|
heardVoice = true
|
|
1024
1272
|
quietSince = 0
|
|
1025
|
-
setVoiceTranscript('
|
|
1273
|
+
setVoiceTranscript('Listening...')
|
|
1026
1274
|
} else if (heardVoice) {
|
|
1027
1275
|
quietSince = quietSince || now
|
|
1028
1276
|
}
|
|
@@ -1037,7 +1285,7 @@ export default function ChatPanel({
|
|
|
1037
1285
|
setIsRecording(false)
|
|
1038
1286
|
voiceModeRef.current = false
|
|
1039
1287
|
setVoiceMode(false)
|
|
1040
|
-
alert('
|
|
1288
|
+
alert('Failed to open microphone. Please check microphone permissions for this app')
|
|
1041
1289
|
}
|
|
1042
1290
|
}
|
|
1043
1291
|
const startRecognition = (autoSend: boolean) => {
|
|
@@ -1472,6 +1720,19 @@ export default function ChatPanel({
|
|
|
1472
1720
|
<div className="bubble-wrapper">
|
|
1473
1721
|
{renderCompletedActivity(interaction)}
|
|
1474
1722
|
{renderFileChanges(interaction)}
|
|
1723
|
+
{(() => {
|
|
1724
|
+
const progress = agentActivitySnapshots[String(interaction.id)] ?? interaction.agentActivity ?? []
|
|
1725
|
+
const thoughts = thoughtsFrom(progress)
|
|
1726
|
+
return (
|
|
1727
|
+
<ThinkingBlock
|
|
1728
|
+
blockKey={String(interaction.id)}
|
|
1729
|
+
thoughts={thoughts}
|
|
1730
|
+
expanded={thinkingExpanded[String(interaction.id)] ?? false}
|
|
1731
|
+
onExpandedChange={onThinkingExpandedChange}
|
|
1732
|
+
showEmptyHint={hasAgentToolActivity(progress) && thoughts.length === 0}
|
|
1733
|
+
/>
|
|
1734
|
+
)
|
|
1735
|
+
})()}
|
|
1475
1736
|
<div className="message-bubble" style={{ whiteSpace: 'pre-wrap' }}>{renderFormattedMessage(interaction.aiText)}</div>
|
|
1476
1737
|
<div className="message-time" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
1477
1738
|
<button className="provider-badge">{interaction.provider} • {interaction.model}</button>
|
|
@@ -1481,7 +1742,7 @@ export default function ChatPanel({
|
|
|
1481
1742
|
type="button"
|
|
1482
1743
|
className={`tts-btn ${speakingText === interaction.aiText ? 'is-speaking' : ''}`}
|
|
1483
1744
|
onClick={() => speak(interaction.aiText)}
|
|
1484
|
-
title={speakingText === interaction.aiText ? '
|
|
1745
|
+
title={speakingText === interaction.aiText ? 'Stop reading' : 'Read aloud'}
|
|
1485
1746
|
>
|
|
1486
1747
|
{renderSpeakerIcon(speakingText === interaction.aiText)}
|
|
1487
1748
|
</button>
|
|
@@ -1510,6 +1771,19 @@ export default function ChatPanel({
|
|
|
1510
1771
|
{renderActiveFileChanges()}
|
|
1511
1772
|
<div className="message ai-message thinking-message">
|
|
1512
1773
|
<div className="bubble-wrapper">
|
|
1774
|
+
<ThinkingBlock
|
|
1775
|
+
blockKey="live"
|
|
1776
|
+
thoughts={thoughtsFrom(agentProgress)}
|
|
1777
|
+
isLive={true}
|
|
1778
|
+
expanded={thinkingExpanded.live ?? true}
|
|
1779
|
+
onExpandedChange={onThinkingExpandedChange}
|
|
1780
|
+
showEmptyHint={
|
|
1781
|
+
agentMode
|
|
1782
|
+
&& hasAgentToolActivity(agentProgress)
|
|
1783
|
+
&& thoughtsFrom(agentProgress).length === 0
|
|
1784
|
+
&& !streamedReply
|
|
1785
|
+
}
|
|
1786
|
+
/>
|
|
1513
1787
|
<div className="message-bubble">
|
|
1514
1788
|
<span>
|
|
1515
1789
|
{streamedReply ? (
|
|
@@ -1543,7 +1817,7 @@ export default function ChatPanel({
|
|
|
1543
1817
|
type="button"
|
|
1544
1818
|
className={`tts-btn ${speakingText === streamedReply ? 'is-speaking' : ''}`}
|
|
1545
1819
|
onClick={() => speak(streamedReply)}
|
|
1546
|
-
title={speakingText === streamedReply ? '
|
|
1820
|
+
title={speakingText === streamedReply ? 'Stop reading' : 'Read aloud'}
|
|
1547
1821
|
>
|
|
1548
1822
|
{renderSpeakerIcon(speakingText === streamedReply)}
|
|
1549
1823
|
</button>
|
|
@@ -1795,7 +2069,7 @@ export default function ChatPanel({
|
|
|
1795
2069
|
className={`${isRecording ? 'is-recording' : ''} ${voiceMode ? 'voice-mode-active' : ''}`}
|
|
1796
2070
|
type="button"
|
|
1797
2071
|
onClick={toggleRecording}
|
|
1798
|
-
title={voiceMode ? '
|
|
2072
|
+
title={voiceMode ? 'Disable voice conversation' : 'Enable voice conversation'}
|
|
1799
2073
|
style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}
|
|
1800
2074
|
>
|
|
1801
2075
|
{isRecording ? (
|
|
@@ -73,13 +73,13 @@ export default function DashboardSidebar({
|
|
|
73
73
|
const toggleInteractionGuide = () => {
|
|
74
74
|
const next = !showInteractionGuide
|
|
75
75
|
onSetShowInteractionGuide(next)
|
|
76
|
-
onShowToast(next ? '
|
|
76
|
+
onShowToast(next ? 'Show interaction zones ⊹' : 'Hide interaction zones ⊹')
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
const toggleInteraction = () => {
|
|
80
80
|
const next = !interactionEnabled
|
|
81
81
|
onSetInteractionEnabled(next)
|
|
82
|
-
onShowToast(next ? '
|
|
82
|
+
onShowToast(next ? 'Enable model interaction ⦸' : 'Disable model interaction ⦸')
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
const conversationSessions = chatSessions.filter((session) => session.kind !== 'cli' && session.id !== 'conversation-default')
|
|
@@ -94,7 +94,7 @@ export default function DashboardSidebar({
|
|
|
94
94
|
<div
|
|
95
95
|
className="sidebar-brand clickable"
|
|
96
96
|
onClick={onToggleSidebar}
|
|
97
|
-
title={sidebarCollapsed ? "
|
|
97
|
+
title={sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
|
|
98
98
|
role="button"
|
|
99
99
|
tabIndex={0}
|
|
100
100
|
onKeyDown={(event) => {
|