@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.
- package/Cargo.lock +3 -3
- package/Cargo.toml +1 -1
- package/README.md +82 -21
- package/assets/Mint_CLI.png +0 -0
- package/assets/Mint_Desktop.png +0 -0
- package/assets/Mint_Web.png +0 -0
- package/assets/Mint_WorkSpace.png +0 -0
- package/assets/Setting.png +0 -0
- package/assets/bridges.svg +4 -0
- package/assets/chat.svg +3 -0
- package/assets/code.svg +4 -0
- package/assets/features.svg +3 -0
- package/assets/imagegen.svg +5 -0
- package/assets/live2d.svg +6 -0
- package/assets/memory.svg +5 -0
- package/assets/screencapture.svg +4 -0
- package/assets/setup.svg +3 -0
- package/assets/tools.svg +4 -0
- package/bin/mint +0 -0
- package/crates/mint-cli/src/agent.rs +47 -15
- package/crates/mint-cli/src/main.rs +534 -7
- package/crates/mint-cli/src/onboard.rs +134 -1
- package/crates/mint-cli/src/setup.rs +173 -32
- package/crates/mint-core/src/api_server.rs +147 -3
- package/crates/mint-core/src/config.rs +22 -3
- package/crates/mint-core/src/image_gen.rs +895 -0
- package/crates/mint-core/src/lib.rs +4 -0
- 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 +32 -0
- package/crates/mint-core/tests/memory_persistence.rs +28 -0
- package/package.json +1 -1
- package/src/bin/index.js +0 -0
- package/src/renderer/src/agentProgress.ts +39 -0
- package/src/renderer/src/components/ChatPanel.tsx +547 -47
- package/src/renderer/src/components/DashboardSidebar.tsx +12 -4
- package/src/renderer/src/components/ImageStudioPanel.tsx +638 -0
- package/src/renderer/src/components/MintDashboard.tsx +84 -31
- package/src/renderer/src/components/ModelPanel.tsx +2 -2
- package/src/renderer/src/components/PicturesLibrary.tsx +5 -1
- package/src/renderer/src/components/SettingsWindow.tsx +29 -20
- package/src/renderer/src/components/SpotlightWindow.tsx +8 -8
- package/src/renderer/src/components/WorkspacePanel.tsx +1 -1
- package/src/renderer/src/css/image-studio.css +722 -0
- package/src/renderer/src/css/settings.css +13 -3
- package/src/renderer/src/css/styles.css +245 -43
- package/src/renderer/src/index.css +152 -0
- package/src/renderer/src/tauri.ts +148 -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 +568 -46
- package/src/renderer/src-web/components/DashboardSidebar.tsx +10 -2
- package/src/renderer/src-web/components/ImageStudioPanel.tsx +638 -0
- package/src/renderer/src-web/components/MintDashboard.tsx +65 -19
- package/src/renderer/src-web/components/PicturesLibrary.tsx +5 -1
- package/src/renderer/src-web/components/SettingsWindow.tsx +29 -20
- package/src/renderer/src-web/css/image-studio.css +722 -0
- package/src/renderer/src-web/css/settings.css +14 -4
- package/src/renderer/src-web/css/styles.css +292 -44
- package/src/renderer/src-web/index.css +152 -0
- package/src/renderer/src-web/tauri.ts +149 -0
- package/src-tauri/src/lib.rs +65 -5
- 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,
|
|
@@ -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
|
|
@@ -299,55 +360,414 @@ interface ChatPanelProps {
|
|
|
299
360
|
onSetModel: (model: string) => void
|
|
300
361
|
}
|
|
301
362
|
|
|
363
|
+
function ChatCodeBlock({ code, language }: { code: string; language: string; key?: any }) {
|
|
364
|
+
const [copied, setCopied] = useState(false)
|
|
365
|
+
|
|
366
|
+
const handleCopy = async () => {
|
|
367
|
+
try {
|
|
368
|
+
await navigator.clipboard.writeText(code)
|
|
369
|
+
setCopied(true)
|
|
370
|
+
setTimeout(() => setCopied(false), 2000)
|
|
371
|
+
} catch (err) {
|
|
372
|
+
console.error('Failed to copy code: ', err)
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const handleDownload = () => {
|
|
377
|
+
try {
|
|
378
|
+
const blob = new Blob([code], { type: 'text/plain;charset=utf-8' })
|
|
379
|
+
const url = URL.createObjectURL(blob)
|
|
380
|
+
const link = document.createElement('a')
|
|
381
|
+
link.href = url
|
|
382
|
+
link.download = `code.${language === 'plaintext' ? 'txt' : language}`
|
|
383
|
+
link.click()
|
|
384
|
+
URL.revokeObjectURL(url)
|
|
385
|
+
} catch (err) {
|
|
386
|
+
console.error('Failed to download code: ', err)
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const displayLang = language.charAt(0).toUpperCase() + language.slice(1).toLowerCase()
|
|
391
|
+
|
|
392
|
+
return (
|
|
393
|
+
<div className="chat-code-block-container" style={{ whiteSpace: 'normal' }}>
|
|
394
|
+
<div className="chat-code-block-header">
|
|
395
|
+
<span className="chat-code-block-lang">{displayLang}</span>
|
|
396
|
+
<div className="chat-code-block-actions">
|
|
397
|
+
<button type="button" onClick={handleDownload} title="Download code" className="chat-code-action-btn">
|
|
398
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
399
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
400
|
+
<polyline points="7 10 12 15 17 10" />
|
|
401
|
+
<line x1="12" y1="15" x2="12" y2="3" />
|
|
402
|
+
</svg>
|
|
403
|
+
</button>
|
|
404
|
+
<button type="button" onClick={handleCopy} title="Copy code" className="chat-code-action-btn">
|
|
405
|
+
{copied ? (
|
|
406
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
|
|
407
|
+
<polyline points="20 6 9 17 4 12" />
|
|
408
|
+
</svg>
|
|
409
|
+
) : (
|
|
410
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
411
|
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
|
412
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
413
|
+
</svg>
|
|
414
|
+
)}
|
|
415
|
+
</button>
|
|
416
|
+
</div>
|
|
417
|
+
</div>
|
|
418
|
+
<pre className="chat-code-block-body">
|
|
419
|
+
<code>{code}</code>
|
|
420
|
+
</pre>
|
|
421
|
+
</div>
|
|
422
|
+
)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const isTableLine = (line: string): boolean => {
|
|
426
|
+
const trimmed = line.trim()
|
|
427
|
+
return trimmed.startsWith('|') && trimmed.endsWith('|') && trimmed.length > 1
|
|
428
|
+
}
|
|
429
|
+
|
|
302
430
|
function renderFormattedMessage(text: string) {
|
|
303
431
|
const displayText = readableAssistantText(text)
|
|
304
432
|
if (!displayText) return null
|
|
305
|
-
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
433
|
+
|
|
434
|
+
const formatInline = (str: string) => {
|
|
435
|
+
const codeParts = str.split(/`([\s\S]*?)`/g)
|
|
436
|
+
return codeParts.map((codePart, codeIndex) => {
|
|
437
|
+
if (codeIndex % 2 === 1) {
|
|
438
|
+
return (
|
|
439
|
+
<code key={`code-${codeIndex}`} className="chat-inline-code">
|
|
440
|
+
{codePart}
|
|
441
|
+
</code>
|
|
442
|
+
)
|
|
443
|
+
}
|
|
444
|
+
const boldParts = codePart.split(/\*\*([\s\S]*?)\*\*/g)
|
|
445
|
+
return boldParts.map((boldPart, boldIndex) => {
|
|
446
|
+
if (boldIndex % 2 === 1) {
|
|
314
447
|
return (
|
|
315
|
-
<strong key={
|
|
316
|
-
{
|
|
448
|
+
<strong key={`bold-${boldIndex}`} className="chat-bold-highlight">
|
|
449
|
+
{boldPart}
|
|
317
450
|
</strong>
|
|
318
451
|
)
|
|
319
452
|
}
|
|
320
|
-
return
|
|
453
|
+
return boldPart
|
|
321
454
|
})
|
|
455
|
+
})
|
|
456
|
+
}
|
|
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)
|
|
322
467
|
}
|
|
323
468
|
|
|
324
|
-
if (
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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])
|
|
335
504
|
}
|
|
336
|
-
|
|
337
|
-
return (
|
|
338
|
-
<span key={lineIndex} style={style}>
|
|
339
|
-
{formatInline(content)}
|
|
340
|
-
</span>
|
|
341
|
-
)
|
|
342
505
|
}
|
|
343
506
|
|
|
344
507
|
return (
|
|
345
|
-
<
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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>
|
|
349
562
|
)
|
|
350
|
-
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const lines = displayText.split('\n')
|
|
566
|
+
const items: any[] = []
|
|
567
|
+
|
|
568
|
+
let inCodeBlock = false
|
|
569
|
+
let codeBlockLang = ''
|
|
570
|
+
let codeBlockLines: string[] = []
|
|
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
|
+
|
|
583
|
+
for (let i = 0; i < lines.length; i++) {
|
|
584
|
+
const line = lines[i]
|
|
585
|
+
|
|
586
|
+
if (line.trim().startsWith('```')) {
|
|
587
|
+
if (inTable) {
|
|
588
|
+
flushTable(i)
|
|
589
|
+
}
|
|
590
|
+
if (inCodeBlock) {
|
|
591
|
+
const codeText = codeBlockLines.join('\n')
|
|
592
|
+
items.push(
|
|
593
|
+
<ChatCodeBlock
|
|
594
|
+
key={`code-block-${i}`}
|
|
595
|
+
code={codeText}
|
|
596
|
+
language={codeBlockLang}
|
|
597
|
+
/>
|
|
598
|
+
)
|
|
599
|
+
inCodeBlock = false
|
|
600
|
+
codeBlockLines = []
|
|
601
|
+
} else {
|
|
602
|
+
inCodeBlock = true
|
|
603
|
+
codeBlockLang = line.trim().slice(3).trim() || 'plaintext'
|
|
604
|
+
}
|
|
605
|
+
continue
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (inCodeBlock) {
|
|
609
|
+
codeBlockLines.push(line)
|
|
610
|
+
continue
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (isTableLine(line)) {
|
|
614
|
+
inTable = true
|
|
615
|
+
tableLines.push(line)
|
|
616
|
+
} else {
|
|
617
|
+
if (inTable) {
|
|
618
|
+
flushTable(i)
|
|
619
|
+
}
|
|
620
|
+
|
|
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
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (inTable) {
|
|
664
|
+
flushTable(lines.length)
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (inCodeBlock && codeBlockLines.length > 0) {
|
|
668
|
+
const codeText = codeBlockLines.join('\n')
|
|
669
|
+
items.push(
|
|
670
|
+
<ChatCodeBlock
|
|
671
|
+
key={`code-block-end`}
|
|
672
|
+
code={codeText}
|
|
673
|
+
language={codeBlockLang}
|
|
674
|
+
/>
|
|
675
|
+
)
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return items
|
|
679
|
+
}
|
|
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
|
+
)
|
|
351
771
|
}
|
|
352
772
|
|
|
353
773
|
function readableAssistantText(text: string) {
|
|
@@ -519,6 +939,8 @@ export default function ChatPanel({
|
|
|
519
939
|
streamedResponse,
|
|
520
940
|
agentProgress,
|
|
521
941
|
agentActivitySnapshots,
|
|
942
|
+
thinkingExpanded,
|
|
943
|
+
onThinkingExpandedChange,
|
|
522
944
|
message,
|
|
523
945
|
imageAttachments,
|
|
524
946
|
documentName,
|
|
@@ -554,6 +976,24 @@ export default function ChatPanel({
|
|
|
554
976
|
const [openReviewIds, setOpenReviewIds] = useState<Record<string, boolean>>({})
|
|
555
977
|
const [openFileDiffs, setOpenFileDiffs] = useState<Record<string, boolean>>({})
|
|
556
978
|
const [toolMenuOpen, setToolMenuOpen] = useState(false)
|
|
979
|
+
const [elapsedSeconds, setElapsedSeconds] = useState(0)
|
|
980
|
+
|
|
981
|
+
useEffect(() => {
|
|
982
|
+
if (!sending) {
|
|
983
|
+
setElapsedSeconds(0)
|
|
984
|
+
return
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const startTime = Date.now()
|
|
988
|
+
setElapsedSeconds(0)
|
|
989
|
+
|
|
990
|
+
const timer = setInterval(() => {
|
|
991
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000)
|
|
992
|
+
setElapsedSeconds(elapsed)
|
|
993
|
+
}, 1000)
|
|
994
|
+
|
|
995
|
+
return () => clearInterval(timer)
|
|
996
|
+
}, [sending])
|
|
557
997
|
const [isRecording, setIsRecording] = useState(false)
|
|
558
998
|
const [voiceMode, setVoiceMode] = useState(false)
|
|
559
999
|
const [voiceTranscript, setVoiceTranscript] = useState('')
|
|
@@ -578,7 +1018,7 @@ export default function ChatPanel({
|
|
|
578
1018
|
const canSubmit = Boolean(message.trim() || imageAttachments.length > 0 || documentName)
|
|
579
1019
|
const sendingImageMarkers = Array.from({ length: sendingImageCount }, (_, index) => `[Image #${index + 1}]`).join(' ')
|
|
580
1020
|
const voiceStatus = speakingText ? 'speaking' : (sending || voiceAwaitingResponse) ? 'thinking' : isRecording ? 'listening' : voiceMode ? 'ready' : 'off'
|
|
581
|
-
const voiceStatusLabel = voiceStatus === 'speaking' ? '
|
|
1021
|
+
const voiceStatusLabel = voiceStatus === 'speaking' ? 'Speaking' : voiceStatus === 'thinking' ? 'Thinking' : voiceStatus === 'listening' ? 'Listening' : 'Ready'
|
|
582
1022
|
|
|
583
1023
|
const getAvailableModels = (provider: string) => {
|
|
584
1024
|
switch (provider) {
|
|
@@ -762,7 +1202,7 @@ export default function ChatPanel({
|
|
|
762
1202
|
}
|
|
763
1203
|
const startAudioRecording = async (autoSend: boolean) => {
|
|
764
1204
|
if (!navigator.mediaDevices?.getUserMedia || typeof MediaRecorder === 'undefined') {
|
|
765
|
-
alert('
|
|
1205
|
+
alert('Sorry, this system cannot access the microphone or record audio in this WebView')
|
|
766
1206
|
voiceModeRef.current = false
|
|
767
1207
|
setVoiceMode(false)
|
|
768
1208
|
return
|
|
@@ -785,7 +1225,7 @@ export default function ChatPanel({
|
|
|
785
1225
|
mediaStreamRef.current = stream
|
|
786
1226
|
mediaRecorderRef.current = recorder
|
|
787
1227
|
audioContextRef.current = audioContext
|
|
788
|
-
setVoiceTranscript('
|
|
1228
|
+
setVoiceTranscript('Listening to microphone...')
|
|
789
1229
|
|
|
790
1230
|
recorder.ondataavailable = (event) => {
|
|
791
1231
|
if (event.data.size > 0) chunks.push(event.data)
|
|
@@ -800,13 +1240,13 @@ export default function ChatPanel({
|
|
|
800
1240
|
setIsRecording(false)
|
|
801
1241
|
if (!autoSend || !voiceModeRef.current || chunks.length === 0) return
|
|
802
1242
|
if (!heardVoice) {
|
|
803
|
-
setVoiceTranscript('
|
|
1243
|
+
setVoiceTranscript('No speech detected')
|
|
804
1244
|
scheduleVoiceListen(700)
|
|
805
1245
|
return
|
|
806
1246
|
}
|
|
807
1247
|
const blob = new Blob(chunks, { type: recorder.mimeType || 'audio/webm' })
|
|
808
1248
|
const audioDataUri = await blobToDataUri(blob)
|
|
809
|
-
setVoiceTranscript('
|
|
1249
|
+
setVoiceTranscript('Sent audio to AI')
|
|
810
1250
|
voiceAwaitingResponseRef.current = true
|
|
811
1251
|
setVoiceAwaitingResponse(true)
|
|
812
1252
|
onSendVoiceMessage('', audioDataUri)
|
|
@@ -830,7 +1270,7 @@ export default function ChatPanel({
|
|
|
830
1270
|
if (peak > 12) {
|
|
831
1271
|
heardVoice = true
|
|
832
1272
|
quietSince = 0
|
|
833
|
-
setVoiceTranscript('
|
|
1273
|
+
setVoiceTranscript('Listening...')
|
|
834
1274
|
} else if (heardVoice) {
|
|
835
1275
|
quietSince = quietSince || now
|
|
836
1276
|
}
|
|
@@ -845,7 +1285,7 @@ export default function ChatPanel({
|
|
|
845
1285
|
setIsRecording(false)
|
|
846
1286
|
voiceModeRef.current = false
|
|
847
1287
|
setVoiceMode(false)
|
|
848
|
-
alert('
|
|
1288
|
+
alert('Failed to open microphone. Please check microphone permissions for this app')
|
|
849
1289
|
}
|
|
850
1290
|
}
|
|
851
1291
|
const startRecognition = (autoSend: boolean) => {
|
|
@@ -1280,6 +1720,19 @@ export default function ChatPanel({
|
|
|
1280
1720
|
<div className="bubble-wrapper">
|
|
1281
1721
|
{renderCompletedActivity(interaction)}
|
|
1282
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
|
+
})()}
|
|
1283
1736
|
<div className="message-bubble" style={{ whiteSpace: 'pre-wrap' }}>{renderFormattedMessage(interaction.aiText)}</div>
|
|
1284
1737
|
<div className="message-time" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
1285
1738
|
<button className="provider-badge">{interaction.provider} • {interaction.model}</button>
|
|
@@ -1289,7 +1742,7 @@ export default function ChatPanel({
|
|
|
1289
1742
|
type="button"
|
|
1290
1743
|
className={`tts-btn ${speakingText === interaction.aiText ? 'is-speaking' : ''}`}
|
|
1291
1744
|
onClick={() => speak(interaction.aiText)}
|
|
1292
|
-
title={speakingText === interaction.aiText ? '
|
|
1745
|
+
title={speakingText === interaction.aiText ? 'Stop reading' : 'Read aloud'}
|
|
1293
1746
|
>
|
|
1294
1747
|
{renderSpeakerIcon(speakingText === interaction.aiText)}
|
|
1295
1748
|
</button>
|
|
@@ -1318,7 +1771,43 @@ export default function ChatPanel({
|
|
|
1318
1771
|
{renderActiveFileChanges()}
|
|
1319
1772
|
<div className="message ai-message thinking-message">
|
|
1320
1773
|
<div className="bubble-wrapper">
|
|
1321
|
-
<
|
|
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
|
+
/>
|
|
1787
|
+
<div className="message-bubble">
|
|
1788
|
+
<span>
|
|
1789
|
+
{streamedReply ? (
|
|
1790
|
+
renderFormattedMessage(streamedReply)
|
|
1791
|
+
) : (
|
|
1792
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: 'var(--text-soft, #94a3b8)' }}>
|
|
1793
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" style={{ display: 'inline-block', flexShrink: 0 }}>
|
|
1794
|
+
<circle cx="12" cy="12" r="10" stroke="rgba(255, 255, 255, 0.12)" />
|
|
1795
|
+
<path d="M12 2a10 10 0 0 1 10 10" stroke="var(--accent)" strokeLinecap="round">
|
|
1796
|
+
<animateTransform
|
|
1797
|
+
attributeName="transform"
|
|
1798
|
+
type="rotate"
|
|
1799
|
+
from="0 12 12"
|
|
1800
|
+
to="360 12 12"
|
|
1801
|
+
dur="0.9s"
|
|
1802
|
+
repeatCount="indefinite"
|
|
1803
|
+
/>
|
|
1804
|
+
</path>
|
|
1805
|
+
</svg>
|
|
1806
|
+
<span>Thinking for {elapsedSeconds}s</span>
|
|
1807
|
+
</div>
|
|
1808
|
+
)}
|
|
1809
|
+
</span>
|
|
1810
|
+
</div>
|
|
1322
1811
|
{streamedResponse && (
|
|
1323
1812
|
<div className="message-time" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
1324
1813
|
<button className="provider-badge">{badge(streamedResponse.provider, streamedResponse.model)}</button>
|
|
@@ -1328,7 +1817,7 @@ export default function ChatPanel({
|
|
|
1328
1817
|
type="button"
|
|
1329
1818
|
className={`tts-btn ${speakingText === streamedReply ? 'is-speaking' : ''}`}
|
|
1330
1819
|
onClick={() => speak(streamedReply)}
|
|
1331
|
-
title={speakingText === streamedReply ? '
|
|
1820
|
+
title={speakingText === streamedReply ? 'Stop reading' : 'Read aloud'}
|
|
1332
1821
|
>
|
|
1333
1822
|
{renderSpeakerIcon(speakingText === streamedReply)}
|
|
1334
1823
|
</button>
|
|
@@ -1342,14 +1831,25 @@ export default function ChatPanel({
|
|
|
1342
1831
|
|
|
1343
1832
|
{pendingApproval && (() => {
|
|
1344
1833
|
const details = renderApprovalDetails(pendingApproval.approval)
|
|
1834
|
+
const writeFile = pendingApproval.approval?.WriteFile
|
|
1835
|
+
const applyPatch = pendingApproval.approval?.ApplyPatch
|
|
1836
|
+
const diffText = writeFile?.diff || applyPatch?.diff
|
|
1837
|
+
|
|
1345
1838
|
return (
|
|
1346
1839
|
<div className="message ai-message" style={{ width: '100%' }}>
|
|
1347
1840
|
<div className="bubble-wrapper" style={{ width: '100%' }}>
|
|
1348
1841
|
<div className="action-card approval-card" data-tier={details.isDangerous ? 'dangerous' : undefined} style={{ width: '100%' }}>
|
|
1349
|
-
<div className="approval-card-content">
|
|
1842
|
+
<div className="approval-card-content" style={{ width: '100%' }}>
|
|
1350
1843
|
<div className="approval-card-title">{details.title}</div>
|
|
1351
1844
|
<div className="approval-card-body" style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>{details.body}</div>
|
|
1352
|
-
{
|
|
1845
|
+
{diffText ? (
|
|
1846
|
+
<div className="approval-card-diff-container" style={{ marginTop: '8px', width: '100%' }}>
|
|
1847
|
+
<div style={{ fontSize: '0.8rem', fontWeight: 600, color: 'var(--text-soft, #94a3b8)', marginBottom: '4px' }}>Diff:</div>
|
|
1848
|
+
{renderDiff(diffText)}
|
|
1849
|
+
</div>
|
|
1850
|
+
) : (
|
|
1851
|
+
details.reason && <div className="approval-card-reason" style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>{details.reason}</div>
|
|
1852
|
+
)}
|
|
1353
1853
|
</div>
|
|
1354
1854
|
<div className="approval-card-actions">
|
|
1355
1855
|
<button type="button" className="approval-btn approval-btn-approve" onClick={() => onApproval(true)}>Approve</button>
|
|
@@ -1569,7 +2069,7 @@ export default function ChatPanel({
|
|
|
1569
2069
|
className={`${isRecording ? 'is-recording' : ''} ${voiceMode ? 'voice-mode-active' : ''}`}
|
|
1570
2070
|
type="button"
|
|
1571
2071
|
onClick={toggleRecording}
|
|
1572
|
-
title={voiceMode ? '
|
|
2072
|
+
title={voiceMode ? 'Disable voice conversation' : 'Enable voice conversation'}
|
|
1573
2073
|
style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}
|
|
1574
2074
|
>
|
|
1575
2075
|
{isRecording ? (
|