@pheem49/mint 1.6.3 → 1.7.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 (63) hide show
  1. package/Cargo.lock +3 -3
  2. package/Cargo.toml +1 -1
  3. package/README.md +82 -21
  4. package/assets/Mint_CLI.png +0 -0
  5. package/assets/Mint_Desktop.png +0 -0
  6. package/assets/Mint_Web.png +0 -0
  7. package/assets/Mint_WorkSpace.png +0 -0
  8. package/assets/Setting.png +0 -0
  9. package/assets/bridges.svg +4 -0
  10. package/assets/chat.svg +3 -0
  11. package/assets/code.svg +4 -0
  12. package/assets/features.svg +3 -0
  13. package/assets/imagegen.svg +5 -0
  14. package/assets/live2d.svg +6 -0
  15. package/assets/memory.svg +5 -0
  16. package/assets/screencapture.svg +4 -0
  17. package/assets/setup.svg +3 -0
  18. package/assets/tools.svg +4 -0
  19. package/bin/mint +0 -0
  20. package/crates/mint-cli/src/agent.rs +47 -15
  21. package/crates/mint-cli/src/main.rs +534 -7
  22. package/crates/mint-cli/src/onboard.rs +134 -1
  23. package/crates/mint-cli/src/setup.rs +173 -32
  24. package/crates/mint-core/src/api_server.rs +147 -3
  25. package/crates/mint-core/src/config.rs +22 -3
  26. package/crates/mint-core/src/image_gen.rs +895 -0
  27. package/crates/mint-core/src/lib.rs +4 -0
  28. package/crates/mint-core/src/memory.rs +27 -1
  29. package/crates/mint-core/src/orchestration.rs +1 -1
  30. package/crates/mint-core/src/plugins.rs +32 -0
  31. package/crates/mint-core/tests/memory_persistence.rs +28 -0
  32. package/package.json +1 -1
  33. package/src/bin/index.js +0 -0
  34. package/src/renderer/src/agentProgress.ts +39 -0
  35. package/src/renderer/src/components/ChatPanel.tsx +547 -47
  36. package/src/renderer/src/components/DashboardSidebar.tsx +12 -4
  37. package/src/renderer/src/components/ImageStudioPanel.tsx +638 -0
  38. package/src/renderer/src/components/MintDashboard.tsx +84 -31
  39. package/src/renderer/src/components/ModelPanel.tsx +2 -2
  40. package/src/renderer/src/components/PicturesLibrary.tsx +5 -1
  41. package/src/renderer/src/components/SettingsWindow.tsx +29 -20
  42. package/src/renderer/src/components/SpotlightWindow.tsx +8 -8
  43. package/src/renderer/src/components/WorkspacePanel.tsx +1 -1
  44. package/src/renderer/src/css/image-studio.css +722 -0
  45. package/src/renderer/src/css/settings.css +13 -3
  46. package/src/renderer/src/css/styles.css +245 -43
  47. package/src/renderer/src/index.css +152 -0
  48. package/src/renderer/src/tauri.ts +148 -0
  49. package/src/renderer/src-web/App.tsx +16 -1
  50. package/src/renderer/src-web/agentProgress.ts +39 -0
  51. package/src/renderer/src-web/components/ChatPanel.tsx +568 -46
  52. package/src/renderer/src-web/components/DashboardSidebar.tsx +10 -2
  53. package/src/renderer/src-web/components/ImageStudioPanel.tsx +638 -0
  54. package/src/renderer/src-web/components/MintDashboard.tsx +65 -19
  55. package/src/renderer/src-web/components/PicturesLibrary.tsx +5 -1
  56. package/src/renderer/src-web/components/SettingsWindow.tsx +29 -20
  57. package/src/renderer/src-web/css/image-studio.css +722 -0
  58. package/src/renderer/src-web/css/settings.css +14 -4
  59. package/src/renderer/src-web/css/styles.css +292 -44
  60. package/src/renderer/src-web/index.css +152 -0
  61. package/src/renderer/src-web/tauri.ts +149 -0
  62. package/src-tauri/src/lib.rs +65 -5
  63. package/src-tauri/tauri.conf.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import { useEffect, useRef, useState, Fragment, type ChangeEvent, type ClipboardEvent, type FormEvent, type KeyboardEvent, type RefObject, type DragEvent } from 'react'
2
+ import { hasAgentToolActivity, thoughtsFrom } from '../agentProgress'
2
3
  import {
3
4
  type AgentProgress,
4
5
  type ChatResponse,
@@ -260,6 +261,64 @@ function renderApprovalDetails(approval: any): ApprovalDetails {
260
261
  return { title: 'Unknown Action', body: JSON.stringify(approval, null, 2), reason: 'Requires approval to proceed.', isDangerous: false }
261
262
  }
262
263
 
264
+ function renderDiff(diffText: string) {
265
+ if (!diffText) return null
266
+ const lines = diffText.split('\n')
267
+ return (
268
+ <div style={{
269
+ background: '#0b0f19',
270
+ borderRadius: '6px',
271
+ padding: '8px',
272
+ border: '1px solid rgba(255, 255, 255, 0.08)',
273
+ overflowX: 'auto',
274
+ maxHeight: '400px',
275
+ fontFamily: 'monospace',
276
+ fontSize: '0.74rem',
277
+ lineHeight: '1.4',
278
+ }}>
279
+ {lines.map((line, idx) => {
280
+ let style: any = {
281
+ whiteSpace: 'pre-wrap',
282
+ padding: '2px 6px',
283
+ }
284
+
285
+ if (line.startsWith('@@') || line.startsWith('---') || line.startsWith('+++')) {
286
+ style = {
287
+ ...style,
288
+ color: '#64748b',
289
+ fontWeight: 'bold',
290
+ }
291
+ } else if (line.startsWith('+')) {
292
+ style = {
293
+ ...style,
294
+ background: 'rgba(16, 185, 129, 0.12)',
295
+ borderLeft: '3px solid #10b981',
296
+ color: '#a7f3d0'
297
+ }
298
+ } else if (line.startsWith('-')) {
299
+ style = {
300
+ ...style,
301
+ background: 'rgba(239, 68, 68, 0.12)',
302
+ borderLeft: '3px solid #ef4444',
303
+ color: '#fca5a5'
304
+ }
305
+ } else {
306
+ style = {
307
+ ...style,
308
+ color: '#e2e8f0',
309
+ }
310
+ }
311
+
312
+ return (
313
+ <div key={idx} style={style}>
314
+ {line}
315
+ </div>
316
+ )
317
+ })}
318
+ </div>
319
+ )
320
+ }
321
+
263
322
  interface ChatPanelProps {
264
323
  interactions: any[]
265
324
  sending: boolean
@@ -269,6 +328,8 @@ interface ChatPanelProps {
269
328
  streamedResponse: ChatResponse | null
270
329
  agentProgress: AgentProgress[]
271
330
  agentActivitySnapshots: Record<string, AgentProgress[]>
331
+ thinkingExpanded: Record<string, boolean>
332
+ onThinkingExpandedChange: (key: string, open: boolean) => void
272
333
  message: string
273
334
  imageAttachments: Array<{ dataUri: string; name: string; previewDataUri?: string }>
274
335
  documentName: string
@@ -298,55 +359,414 @@ interface ChatPanelProps {
298
359
  onSetModel: (model: string) => void
299
360
  }
300
361
 
362
+ function ChatCodeBlock({ code, language }: { code: string; language: string; key?: any }) {
363
+ const [copied, setCopied] = useState(false)
364
+
365
+ const handleCopy = async () => {
366
+ try {
367
+ await navigator.clipboard.writeText(code)
368
+ setCopied(true)
369
+ setTimeout(() => setCopied(false), 2000)
370
+ } catch (err) {
371
+ console.error('Failed to copy code: ', err)
372
+ }
373
+ }
374
+
375
+ const handleDownload = () => {
376
+ try {
377
+ const blob = new Blob([code], { type: 'text/plain;charset=utf-8' })
378
+ const url = URL.createObjectURL(blob)
379
+ const link = document.createElement('a')
380
+ link.href = url
381
+ link.download = `code.${language === 'plaintext' ? 'txt' : language}`
382
+ link.click()
383
+ URL.revokeObjectURL(url)
384
+ } catch (err) {
385
+ console.error('Failed to download code: ', err)
386
+ }
387
+ }
388
+
389
+ const displayLang = language.charAt(0).toUpperCase() + language.slice(1).toLowerCase()
390
+
391
+ return (
392
+ <div className="chat-code-block-container" style={{ whiteSpace: 'normal' }}>
393
+ <div className="chat-code-block-header">
394
+ <span className="chat-code-block-lang">{displayLang}</span>
395
+ <div className="chat-code-block-actions">
396
+ <button type="button" onClick={handleDownload} title="Download code" className="chat-code-action-btn">
397
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
398
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
399
+ <polyline points="7 10 12 15 17 10" />
400
+ <line x1="12" y1="15" x2="12" y2="3" />
401
+ </svg>
402
+ </button>
403
+ <button type="button" onClick={handleCopy} title="Copy code" className="chat-code-action-btn">
404
+ {copied ? (
405
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
406
+ <polyline points="20 6 9 17 4 12" />
407
+ </svg>
408
+ ) : (
409
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
410
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
411
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
412
+ </svg>
413
+ )}
414
+ </button>
415
+ </div>
416
+ </div>
417
+ <pre className="chat-code-block-body">
418
+ <code>{code}</code>
419
+ </pre>
420
+ </div>
421
+ )
422
+ }
423
+
424
+ const isTableLine = (line: string): boolean => {
425
+ const trimmed = line.trim()
426
+ return trimmed.startsWith('|') && trimmed.endsWith('|') && trimmed.length > 1
427
+ }
428
+
301
429
  function renderFormattedMessage(text: string) {
302
430
  const displayText = readableAssistantText(text)
303
431
  if (!displayText) return null
304
-
305
- const lines = displayText.split('\n')
306
- return lines.map((line, lineIndex) => {
307
- const headerMatch = line.match(/^(#{1,6})\s+(.*)$/)
308
-
309
- const formatInline = (str: string) => {
310
- const parts = str.split(/\*\*([\s\S]*?)\*\*/g)
311
- return parts.map((part, partIndex) => {
312
- if (partIndex % 2 === 1) {
432
+
433
+ const formatInline = (str: string) => {
434
+ const codeParts = str.split(/`([\s\S]*?)`/g)
435
+ return codeParts.map((codePart, codeIndex) => {
436
+ if (codeIndex % 2 === 1) {
437
+ return (
438
+ <code key={`code-${codeIndex}`} className="chat-inline-code">
439
+ {codePart}
440
+ </code>
441
+ )
442
+ }
443
+ const boldParts = codePart.split(/\*\*([\s\S]*?)\*\*/g)
444
+ return boldParts.map((boldPart, boldIndex) => {
445
+ if (boldIndex % 2 === 1) {
313
446
  return (
314
- <strong key={partIndex} className="chat-bold-highlight">
315
- {part}
447
+ <strong key={`bold-${boldIndex}`} className="chat-bold-highlight">
448
+ {boldPart}
316
449
  </strong>
317
450
  )
318
451
  }
319
- return part
452
+ return boldPart
320
453
  })
454
+ })
455
+ }
456
+
457
+ const renderHtmlTable = (tableLines: string[], key: string) => {
458
+ const rows: string[][] = []
459
+ for (const line of tableLines) {
460
+ const trimmed = line.trim()
461
+ if (!trimmed) continue
462
+ const content = trimmed.slice(1, -1)
463
+ const contentEscaped = content.replace(/\\\|/g, '\u0000')
464
+ const cols = contentEscaped.split('|').map(s => s.replace(/\u0000/g, '|').trim())
465
+ rows.push(cols)
321
466
  }
322
467
 
323
- if (headerMatch) {
324
- const level = headerMatch[1].length
325
- const content = headerMatch[2]
326
-
327
- const style = {
328
- fontWeight: 'bold',
329
- display: 'block',
330
- marginTop: level === 1 ? '16px' : level === 2 ? '14px' : '10px',
331
- marginBottom: '6px',
332
- fontSize: level === 1 ? '1.25em' : level === 2 ? '1.15em' : '1.05em',
333
- color: 'var(--text-main)'
468
+ if (rows.length === 0) return null
469
+
470
+ let hasSeparator = false
471
+ let separatorIdx = -1
472
+ for (let i = 0; i < rows.length; i++) {
473
+ const row = rows[i]
474
+ if (row.length > 0 && row.every(col => /^[-\s:]+$/.test(col))) {
475
+ hasSeparator = true
476
+ separatorIdx = i
477
+ break
478
+ }
479
+ }
480
+
481
+ let headerRow: string[] | null = null
482
+ const dataRows: string[][] = []
483
+
484
+ if (hasSeparator) {
485
+ if (separatorIdx > 0) {
486
+ headerRow = rows[separatorIdx - 1]
487
+ for (let i = 0; i < rows.length; i++) {
488
+ if (i !== separatorIdx && i !== separatorIdx - 1) {
489
+ dataRows.push(rows[i])
490
+ }
491
+ }
492
+ } else {
493
+ for (let i = 0; i < rows.length; i++) {
494
+ if (i !== separatorIdx) {
495
+ dataRows.push(rows[i])
496
+ }
497
+ }
498
+ }
499
+ } else {
500
+ headerRow = rows[0]
501
+ for (let i = 1; i < rows.length; i++) {
502
+ dataRows.push(rows[i])
334
503
  }
335
-
336
- return (
337
- <span key={lineIndex} style={style}>
338
- {formatInline(content)}
339
- </span>
340
- )
341
504
  }
342
505
 
343
506
  return (
344
- <Fragment key={lineIndex}>
345
- {formatInline(line)}
346
- {lineIndex < lines.length - 1 && '\n'}
347
- </Fragment>
507
+ <div key={key} className="chat-table-container" style={{
508
+ overflowX: 'auto',
509
+ margin: '14px 0',
510
+ width: '100%',
511
+ borderRadius: '8px',
512
+ border: '1px solid rgba(255, 255, 255, 0.12)',
513
+ background: 'rgba(30, 41, 59, 0.35)',
514
+ boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
515
+ }}>
516
+ <table className="chat-table" style={{
517
+ width: '100%',
518
+ borderCollapse: 'collapse',
519
+ fontSize: '0.86rem',
520
+ textAlign: 'left',
521
+ lineHeight: '1.5',
522
+ }}>
523
+ {headerRow && (
524
+ <thead>
525
+ <tr style={{
526
+ background: 'rgba(255, 255, 255, 0.04)',
527
+ borderBottom: '2px solid rgba(255, 255, 255, 0.15)',
528
+ }}>
529
+ {headerRow.map((col, idx) => (
530
+ <th key={`th-${idx}`} style={{
531
+ padding: '12px 16px',
532
+ fontWeight: 700,
533
+ color: 'var(--accent, #38bdf8)',
534
+ fontFamily: 'ui-sans-serif, system-ui, sans-serif',
535
+ }}>
536
+ {formatInline(col)}
537
+ </th>
538
+ ))}
539
+ </tr>
540
+ </thead>
541
+ )}
542
+ <tbody>
543
+ {dataRows.map((row, rIdx) => (
544
+ <tr key={`tr-${rIdx}`} style={{
545
+ background: rIdx % 2 === 1 ? 'rgba(255, 255, 255, 0.015)' : 'transparent',
546
+ borderBottom: rIdx < dataRows.length - 1 ? '1px solid rgba(255, 255, 255, 0.08)' : 'none',
547
+ }}>
548
+ {row.map((col, cIdx) => (
549
+ <td key={`td-${cIdx}`} style={{
550
+ padding: '12px 16px',
551
+ color: 'var(--text-main, #e2e8f0)',
552
+ }}>
553
+ {formatInline(col)}
554
+ </td>
555
+ ))}
556
+ </tr>
557
+ ))}
558
+ </tbody>
559
+ </table>
560
+ </div>
348
561
  )
349
- })
562
+ }
563
+
564
+ const lines = displayText.split('\n')
565
+ const items: any[] = []
566
+
567
+ let inCodeBlock = false
568
+ let codeBlockLang = ''
569
+ let codeBlockLines: string[] = []
570
+
571
+ let inTable = false
572
+ let tableLines: string[] = []
573
+
574
+ const flushTable = (index: number) => {
575
+ if (tableLines.length > 0) {
576
+ items.push(renderHtmlTable(tableLines, `table-${index}`))
577
+ tableLines = []
578
+ inTable = false
579
+ }
580
+ }
581
+
582
+ for (let i = 0; i < lines.length; i++) {
583
+ const line = lines[i]
584
+
585
+ if (line.trim().startsWith('```')) {
586
+ if (inTable) {
587
+ flushTable(i)
588
+ }
589
+ if (inCodeBlock) {
590
+ const codeText = codeBlockLines.join('\n')
591
+ items.push(
592
+ <ChatCodeBlock
593
+ key={`code-block-${i}`}
594
+ code={codeText}
595
+ language={codeBlockLang}
596
+ />
597
+ )
598
+ inCodeBlock = false
599
+ codeBlockLines = []
600
+ } else {
601
+ inCodeBlock = true
602
+ codeBlockLang = line.trim().slice(3).trim() || 'plaintext'
603
+ }
604
+ continue
605
+ }
606
+
607
+ if (inCodeBlock) {
608
+ codeBlockLines.push(line)
609
+ continue
610
+ }
611
+
612
+ if (isTableLine(line)) {
613
+ inTable = true
614
+ tableLines.push(line)
615
+ } else {
616
+ if (inTable) {
617
+ flushTable(i)
618
+ }
619
+
620
+ const headerMatch = line.match(/^(#{1,6})\s+(.*)$/)
621
+ if (headerMatch) {
622
+ const level = headerMatch[1].length
623
+ const content = headerMatch[2]
624
+
625
+ const style = {
626
+ fontWeight: 'bold',
627
+ display: 'block',
628
+ marginTop: level === 1 ? '16px' : level === 2 ? '14px' : '10px',
629
+ marginBottom: '6px',
630
+ fontSize: level === 1 ? '1.25em' : level === 2 ? '1.15em' : '1.05em',
631
+ color: 'var(--text-main)',
632
+ }
633
+
634
+ items.push(
635
+ <span key={`line-${i}`} style={style}>
636
+ {formatInline(content)}
637
+ </span>
638
+ )
639
+ } else {
640
+ const listMatch = line.match(/^(\s*)([-*+])\s+(.*)$/)
641
+ if (listMatch) {
642
+ const indent = listMatch[1]
643
+ const content = listMatch[3]
644
+ items.push(
645
+ <Fragment key={`line-${i}`}>
646
+ {indent}• {formatInline(content)}
647
+ {i < lines.length - 1 && '\n'}
648
+ </Fragment>
649
+ )
650
+ } else {
651
+ items.push(
652
+ <Fragment key={`line-${i}`}>
653
+ {formatInline(line)}
654
+ {i < lines.length - 1 && '\n'}
655
+ </Fragment>
656
+ )
657
+ }
658
+ }
659
+ }
660
+ }
661
+
662
+ if (inTable) {
663
+ flushTable(lines.length)
664
+ }
665
+
666
+ if (inCodeBlock && codeBlockLines.length > 0) {
667
+ const codeText = codeBlockLines.join('\n')
668
+ items.push(
669
+ <ChatCodeBlock
670
+ key={`code-block-end`}
671
+ code={codeText}
672
+ language={codeBlockLang}
673
+ />
674
+ )
675
+ }
676
+
677
+ return items
678
+ }
679
+
680
+ const THINKING_LABELS = {
681
+ live: 'Thinking…',
682
+ completed: (count: number) => `Thinking process (${count} steps)`,
683
+ step: (index: number) => `Step ${index}`,
684
+ emptyHint: 'Model didn\'t send thinking steps this time',
685
+ } as const
686
+
687
+ function ThinkingBlock({
688
+ thoughts,
689
+ isLive = false,
690
+ blockKey,
691
+ expanded,
692
+ onExpandedChange,
693
+ showEmptyHint = false,
694
+ }: {
695
+ thoughts: string[]
696
+ isLive?: boolean
697
+ blockKey: string
698
+ expanded?: boolean
699
+ onExpandedChange?: (key: string, open: boolean) => void
700
+ showEmptyHint?: boolean
701
+ }) {
702
+ const contentRef = useRef<HTMLDivElement>(null)
703
+ const isControlled = expanded !== undefined && Boolean(onExpandedChange)
704
+ const [localOpen, setLocalOpen] = useState(isLive)
705
+ const isOpen = isControlled ? Boolean(expanded) : localOpen
706
+
707
+ const setOpen = (open: boolean) => {
708
+ if (isControlled && onExpandedChange) onExpandedChange(blockKey, open)
709
+ else setLocalOpen(open)
710
+ }
711
+
712
+ useEffect(() => {
713
+ if (isLive && !isControlled) setLocalOpen(true)
714
+ }, [isLive, isControlled])
715
+
716
+ useEffect(() => {
717
+ if (!isLive || !isOpen || !contentRef.current) return
718
+ contentRef.current.scrollTop = contentRef.current.scrollHeight
719
+ }, [thoughts, isLive, isOpen])
720
+
721
+ if (thoughts.length === 0 && !showEmptyHint) return null
722
+
723
+ if (thoughts.length === 0) {
724
+ return (
725
+ <div className="thinking-block thinking-block-empty-state" data-live={isLive ? 'true' : 'false'}>
726
+ <span className="thinking-block-empty">{THINKING_LABELS.emptyHint}</span>
727
+ </div>
728
+ )
729
+ }
730
+
731
+ return (
732
+ <div className={`thinking-block${isLive ? ' is-live' : ''}`}>
733
+ <button
734
+ type="button"
735
+ className="thinking-block-header"
736
+ onClick={() => setOpen(!isOpen)}
737
+ aria-expanded={isOpen}
738
+ >
739
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ flexShrink: 0 }}>
740
+ <path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
741
+ <line x1="12" y1="17" x2="12.01" y2="17" />
742
+ <circle cx="12" cy="12" r="10" />
743
+ </svg>
744
+ <span className="thinking-block-label">
745
+ {isLive ? THINKING_LABELS.live : THINKING_LABELS.completed(thoughts.length)}
746
+ </span>
747
+ {isLive && <span className="thinking-block-live-dot" aria-hidden="true" />}
748
+ <span className={`thinking-block-chevron${isOpen ? ' is-open' : ''}`} aria-hidden="true">
749
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
750
+ <polyline points="9 18 15 12 9 6" />
751
+ </svg>
752
+ </span>
753
+ </button>
754
+ {isOpen && (
755
+ <div className="thinking-block-body">
756
+ <div className="thinking-block-content" ref={contentRef}>
757
+ {thoughts.map((thought, index) => (
758
+ <div className="thinking-block-step" key={`${blockKey}-${index}`}>
759
+ {thoughts.length > 1 && (
760
+ <div className="thinking-block-step-label">{THINKING_LABELS.step(index + 1)}</div>
761
+ )}
762
+ <div className="thinking-block-step-body">{renderFormattedMessage(thought)}</div>
763
+ </div>
764
+ ))}
765
+ </div>
766
+ </div>
767
+ )}
768
+ </div>
769
+ )
350
770
  }
351
771
 
352
772
  function readableAssistantText(text: string) {
@@ -518,6 +938,8 @@ export default function ChatPanel({
518
938
  streamedResponse,
519
939
  agentProgress,
520
940
  agentActivitySnapshots,
941
+ thinkingExpanded,
942
+ onThinkingExpandedChange,
521
943
  message,
522
944
  imageAttachments,
523
945
  documentName,
@@ -607,6 +1029,25 @@ export default function ChatPanel({
607
1029
  }
608
1030
  const activeModel = getActiveModel(activeProvider)
609
1031
 
1032
+ const [elapsedSeconds, setElapsedSeconds] = useState(0)
1033
+
1034
+ useEffect(() => {
1035
+ if (!sending) {
1036
+ setElapsedSeconds(0)
1037
+ return
1038
+ }
1039
+
1040
+ const startTime = Date.now()
1041
+ setElapsedSeconds(0)
1042
+
1043
+ const timer = setInterval(() => {
1044
+ const elapsed = Math.round((Date.now() - startTime) / 1000)
1045
+ setElapsedSeconds(elapsed)
1046
+ }, 1000)
1047
+
1048
+ return () => clearInterval(timer)
1049
+ }, [sending])
1050
+
610
1051
  const [isRecording, setIsRecording] = useState(false)
611
1052
  const [voiceMode, setVoiceMode] = useState(false)
612
1053
  const [voiceTranscript, setVoiceTranscript] = useState('')
@@ -628,7 +1069,7 @@ export default function ChatPanel({
628
1069
  const speakingRef = useRef<string | null>(null)
629
1070
  const restartTimerRef = useRef<number | null>(null)
630
1071
  const voiceStatus = speakingText ? 'speaking' : (sending || voiceAwaitingResponse) ? 'thinking' : isRecording ? 'listening' : voiceMode ? 'ready' : 'off'
631
- const voiceStatusLabel = voiceStatus === 'speaking' ? 'กำลังตอบ' : voiceStatus === 'thinking' ? 'กำลังคิด' : voiceStatus === 'listening' ? 'กำลังฟัง' : 'พร้อมฟัง'
1072
+ const voiceStatusLabel = voiceStatus === 'speaking' ? 'Speaking' : voiceStatus === 'thinking' ? 'Thinking' : voiceStatus === 'listening' ? 'Listening' : 'Ready'
632
1073
 
633
1074
  const clearRestartTimer = () => {
634
1075
  if (restartTimerRef.current === null) return
@@ -750,7 +1191,7 @@ export default function ChatPanel({
750
1191
  }
751
1192
  const startAudioRecording = async (autoSend: boolean) => {
752
1193
  if (!navigator.mediaDevices?.getUserMedia || typeof MediaRecorder === 'undefined') {
753
- alert('ขออภัย ระบบนี้ไม่สามารถเข้าถึงไมค์หรืออัดเสียงในเบราว์เซอร์นี้ได้')
1194
+ alert("Sorry, this system cannot access the microphone or record audio in this browser")
754
1195
  voiceModeRef.current = false
755
1196
  setVoiceMode(false)
756
1197
  return
@@ -773,7 +1214,7 @@ export default function ChatPanel({
773
1214
  mediaStreamRef.current = stream
774
1215
  mediaRecorderRef.current = recorder
775
1216
  audioContextRef.current = audioContext
776
- setVoiceTranscript('กำลังฟังเสียงจากไมค์')
1217
+ setVoiceTranscript('Listening to microphone...')
777
1218
 
778
1219
  recorder.ondataavailable = (event) => {
779
1220
  if (event.data.size > 0) chunks.push(event.data)
@@ -788,13 +1229,13 @@ export default function ChatPanel({
788
1229
  setIsRecording(false)
789
1230
  if (!autoSend || !voiceModeRef.current || chunks.length === 0) return
790
1231
  if (!heardVoice) {
791
- setVoiceTranscript('ยังไม่ได้ยินเสียงพูด')
1232
+ setVoiceTranscript('No speech detected')
792
1233
  scheduleVoiceListen(700)
793
1234
  return
794
1235
  }
795
1236
  const blob = new Blob(chunks, { type: recorder.mimeType || 'audio/webm' })
796
1237
  const audioDataUri = await blobToDataUri(blob)
797
- setVoiceTranscript('ส่งเสียงให้ AI แล้ว')
1238
+ setVoiceTranscript('Sent audio to AI')
798
1239
  voiceAwaitingResponseRef.current = true
799
1240
  setVoiceAwaitingResponse(true)
800
1241
  onSendVoiceMessage('', audioDataUri)
@@ -818,7 +1259,7 @@ export default function ChatPanel({
818
1259
  if (peak > 12) {
819
1260
  heardVoice = true
820
1261
  quietSince = 0
821
- setVoiceTranscript('กำลังฟัง...')
1262
+ setVoiceTranscript('Listening...')
822
1263
  } else if (heardVoice) {
823
1264
  quietSince = quietSince || now
824
1265
  }
@@ -833,7 +1274,7 @@ export default function ChatPanel({
833
1274
  setIsRecording(false)
834
1275
  voiceModeRef.current = false
835
1276
  setVoiceMode(false)
836
- alert('เปิดไมค์ไม่สำเร็จ กรุณาตรวจสิทธิ์ microphone ของเบราว์เซอร์')
1277
+ alert('Failed to open microphone. Please check microphone permissions in your browser')
837
1278
  }
838
1279
  }
839
1280
  const startRecognition = (autoSend: boolean) => {
@@ -1351,8 +1792,8 @@ export default function ChatPanel({
1351
1792
  }}
1352
1793
  >
1353
1794
  <div style={{ fontSize: '3.5rem', marginBottom: '16px' }}>🖼️</div>
1354
- <div style={{ fontSize: '1.25rem', fontWeight: 'bold', letterSpacing: '0.5px' }}>วางไฟล์เพื่อแนบข้อมูล</div>
1355
- <div style={{ fontSize: '0.85rem', color: '#94a3b8', marginTop: '8px' }}>รองรับรูปภาพ (PNG, JPEG, WebP, GIF) และไฟล์ PDF</div>
1795
+ <div style={{ fontSize: '1.25rem', fontWeight: 'bold', letterSpacing: '0.5px' }}>Drag files to attach data</div>
1796
+ <div style={{ fontSize: '0.85rem', color: '#94a3b8', marginTop: '8px' }}>Supports images (PNG, JPEG, WebP, GIF) and PDF files</div>
1356
1797
  </div>
1357
1798
  )}
1358
1799
 
@@ -1375,6 +1816,19 @@ export default function ChatPanel({
1375
1816
  <div className="bubble-wrapper">
1376
1817
  {renderCompletedActivity(interaction)}
1377
1818
  {renderFileChanges(interaction)}
1819
+ {(() => {
1820
+ const progress = agentActivitySnapshots[String(interaction.id)] ?? interaction.agentActivity ?? []
1821
+ const thoughts = thoughtsFrom(progress)
1822
+ return (
1823
+ <ThinkingBlock
1824
+ blockKey={String(interaction.id)}
1825
+ thoughts={thoughts}
1826
+ expanded={thinkingExpanded[String(interaction.id)] ?? false}
1827
+ onExpandedChange={onThinkingExpandedChange}
1828
+ showEmptyHint={hasAgentToolActivity(progress) && thoughts.length === 0}
1829
+ />
1830
+ )
1831
+ })()}
1378
1832
  <div className="message-bubble" style={{ whiteSpace: 'pre-wrap' }}>{renderFormattedMessage(interaction.aiText)}</div>
1379
1833
  <div className="message-time" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
1380
1834
  <button className="provider-badge">{interaction.provider} • {interaction.model}</button>
@@ -1396,7 +1850,7 @@ export default function ChatPanel({
1396
1850
  opacity: 0.7,
1397
1851
  transition: 'all 0.2s',
1398
1852
  }}
1399
- title={speakingText === interaction.aiText ? "หยุดอ่านออกเสียง" : "อ่านออกเสียง"}
1853
+ title={speakingText === interaction.aiText ? "Stop reading" : "Read aloud"}
1400
1854
  >
1401
1855
  {renderSpeakerIcon(speakingText === interaction.aiText)}
1402
1856
  </button>
@@ -1425,7 +1879,43 @@ export default function ChatPanel({
1425
1879
  {renderActiveFileChanges()}
1426
1880
  <div className="message ai-message thinking-message">
1427
1881
  <div className="bubble-wrapper">
1428
- <div className="message-bubble"><span>{streamedReply ? renderFormattedMessage(streamedReply) : 'Thinking...'}</span></div>
1882
+ <ThinkingBlock
1883
+ blockKey="live"
1884
+ thoughts={thoughtsFrom(agentProgress)}
1885
+ isLive={true}
1886
+ expanded={thinkingExpanded.live ?? true}
1887
+ onExpandedChange={onThinkingExpandedChange}
1888
+ showEmptyHint={
1889
+ agentMode
1890
+ && hasAgentToolActivity(agentProgress)
1891
+ && thoughtsFrom(agentProgress).length === 0
1892
+ && !streamedReply
1893
+ }
1894
+ />
1895
+ <div className="message-bubble">
1896
+ <span>
1897
+ {streamedReply ? (
1898
+ renderFormattedMessage(streamedReply)
1899
+ ) : (
1900
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: 'var(--text-soft, #94a3b8)' }}>
1901
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" style={{ display: 'inline-block', flexShrink: 0 }}>
1902
+ <circle cx="12" cy="12" r="10" stroke="rgba(255, 255, 255, 0.12)" />
1903
+ <path d="M12 2a10 10 0 0 1 10 10" stroke="var(--accent)" strokeLinecap="round">
1904
+ <animateTransform
1905
+ attributeName="transform"
1906
+ type="rotate"
1907
+ from="0 12 12"
1908
+ to="360 12 12"
1909
+ dur="0.9s"
1910
+ repeatCount="indefinite"
1911
+ />
1912
+ </path>
1913
+ </svg>
1914
+ <span>Thinking for {elapsedSeconds}s</span>
1915
+ </div>
1916
+ )}
1917
+ </span>
1918
+ </div>
1429
1919
  {streamedResponse && (
1430
1920
  <div className="message-time" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
1431
1921
  <button className="provider-badge">{badge(streamedResponse.provider, streamedResponse.model)}</button>
@@ -1447,7 +1937,7 @@ export default function ChatPanel({
1447
1937
  opacity: 0.7,
1448
1938
  transition: 'all 0.2s',
1449
1939
  }}
1450
- title={speakingText === streamedReply ? "หยุดอ่านออกเสียง" : "อ่านออกเสียง"}
1940
+ title={speakingText === streamedReply ? "Stop reading" : "Read aloud"}
1451
1941
  >
1452
1942
  {renderSpeakerIcon(speakingText === streamedReply)}
1453
1943
  </button>
@@ -1459,6 +1949,38 @@ export default function ChatPanel({
1459
1949
  </div>
1460
1950
  )}
1461
1951
 
1952
+ {pendingApproval && (() => {
1953
+ const details = renderApprovalDetails(pendingApproval.approval)
1954
+ const writeFile = pendingApproval.approval?.WriteFile
1955
+ const applyPatch = pendingApproval.approval?.ApplyPatch
1956
+ const diffText = writeFile?.diff || applyPatch?.diff
1957
+
1958
+ return (
1959
+ <div className="message ai-message" style={{ width: '100%' }}>
1960
+ <div className="bubble-wrapper" style={{ width: '100%' }}>
1961
+ <div className="action-card approval-card" data-tier={details.isDangerous ? 'dangerous' : undefined} style={{ width: '100%' }}>
1962
+ <div className="approval-card-content" style={{ width: '100%' }}>
1963
+ <div className="approval-card-title">{details.title}</div>
1964
+ <div className="approval-card-body" style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>{details.body}</div>
1965
+ {diffText ? (
1966
+ <div className="approval-card-diff-container" style={{ marginTop: '8px', width: '100%' }}>
1967
+ <div style={{ fontSize: '0.8rem', fontWeight: 600, color: 'var(--text-soft, #94a3b8)', marginBottom: '4px' }}>Diff:</div>
1968
+ {renderDiff(diffText)}
1969
+ </div>
1970
+ ) : (
1971
+ details.reason && <div className="approval-card-reason" style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>{details.reason}</div>
1972
+ )}
1973
+ </div>
1974
+ <div className="approval-card-actions">
1975
+ <button type="button" className="approval-btn approval-btn-approve" onClick={() => onApproval(true)}>Approve</button>
1976
+ <button type="button" className="approval-btn" style={{ backgroundColor: 'rgba(16, 185, 129, 0.22)', borderColor: 'rgba(16, 185, 129, 0.4)', color: '#a7f3d0' }} onClick={() => onApproval(true, true)}>Approve this session</button>
1977
+ <button type="button" className="approval-btn approval-btn-cancel" onClick={() => onApproval(false)}>Cancel</button>
1978
+ </div>
1979
+ </div>
1980
+ </div>
1981
+ </div>
1982
+ )
1983
+ })()}
1462
1984
  <div ref={chatEnd} />
1463
1985
  </div>
1464
1986