@pheem49/mint 1.6.3 → 1.6.4

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 (50) 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 +6 -2
  21. package/crates/mint-cli/src/main.rs +145 -5
  22. package/crates/mint-cli/src/onboard.rs +134 -1
  23. package/crates/mint-core/src/api_server.rs +108 -3
  24. package/crates/mint-core/src/config.rs +20 -1
  25. package/crates/mint-core/src/image_gen.rs +895 -0
  26. package/crates/mint-core/src/lib.rs +4 -0
  27. package/package.json +1 -1
  28. package/src/bin/index.js +0 -0
  29. package/src/renderer/src/components/ChatPanel.tsx +252 -26
  30. package/src/renderer/src/components/DashboardSidebar.tsx +9 -1
  31. package/src/renderer/src/components/ImageStudioPanel.tsx +638 -0
  32. package/src/renderer/src/components/MintDashboard.tsx +13 -4
  33. package/src/renderer/src/components/SettingsWindow.tsx +29 -20
  34. package/src/renderer/src/css/image-studio.css +722 -0
  35. package/src/renderer/src/css/settings.css +13 -3
  36. package/src/renderer/src/css/styles.css +50 -26
  37. package/src/renderer/src/index.css +106 -0
  38. package/src/renderer/src/tauri.ts +123 -0
  39. package/src/renderer/src-web/components/ChatPanel.tsx +272 -24
  40. package/src/renderer/src-web/components/DashboardSidebar.tsx +9 -1
  41. package/src/renderer/src-web/components/ImageStudioPanel.tsx +638 -0
  42. package/src/renderer/src-web/components/MintDashboard.tsx +12 -3
  43. package/src/renderer/src-web/components/SettingsWindow.tsx +29 -20
  44. package/src/renderer/src-web/css/image-studio.css +722 -0
  45. package/src/renderer/src-web/css/settings.css +14 -4
  46. package/src/renderer/src-web/css/styles.css +50 -26
  47. package/src/renderer/src-web/index.css +106 -0
  48. package/src/renderer/src-web/tauri.ts +124 -0
  49. package/src-tauri/src/lib.rs +48 -5
  50. package/src-tauri/tauri.conf.json +1 -1
@@ -5,6 +5,7 @@ pub mod chat;
5
5
  pub mod code_tools;
6
6
  pub mod config;
7
7
  pub mod files;
8
+ pub mod image_gen;
8
9
  pub mod knowledge;
9
10
  pub mod mcp;
10
11
  pub mod memory;
@@ -39,6 +40,9 @@ pub use config::{
39
40
  set_config_value,
40
41
  };
41
42
  pub use files::{FileOperationError, PathKind, PathMatch, create_folder, find_paths};
43
+ pub use image_gen::{
44
+ GeneratedImage, ImageGenError, ImageGenRequest, ImageGenResponse, generate_images,
45
+ };
42
46
  pub use knowledge::{
43
47
  KnowledgeError, KnowledgeHit, KnowledgeSource, KnowledgeStore, extract_document_text,
44
48
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pheem49/mint",
3
- "version": "1.6.3",
3
+ "version": "1.6.4",
4
4
  "description": "A native Tauri desktop AI assistant with a Rust backend and React UI.",
5
5
  "scripts": {
6
6
  "dev:web": "vite --config vite.config.web.ts",
package/src/bin/index.js CHANGED
File without changes
@@ -260,6 +260,64 @@ function renderApprovalDetails(approval: any): ApprovalDetails {
260
260
  return { title: 'Unknown Action', body: JSON.stringify(approval, null, 2), reason: 'Requires approval to proceed.', isDangerous: false }
261
261
  }
262
262
 
263
+ function renderDiff(diffText: string) {
264
+ if (!diffText) return null
265
+ const lines = diffText.split('\n')
266
+ return (
267
+ <div style={{
268
+ background: '#0b0f19',
269
+ borderRadius: '6px',
270
+ padding: '8px',
271
+ border: '1px solid rgba(255, 255, 255, 0.08)',
272
+ overflowX: 'auto',
273
+ maxHeight: '400px',
274
+ fontFamily: 'monospace',
275
+ fontSize: '0.74rem',
276
+ lineHeight: '1.4',
277
+ }}>
278
+ {lines.map((line, idx) => {
279
+ let style: any = {
280
+ whiteSpace: 'pre-wrap',
281
+ padding: '2px 6px',
282
+ }
283
+
284
+ if (line.startsWith('@@') || line.startsWith('---') || line.startsWith('+++')) {
285
+ style = {
286
+ ...style,
287
+ color: '#64748b',
288
+ fontWeight: 'bold',
289
+ }
290
+ } else if (line.startsWith('+')) {
291
+ style = {
292
+ ...style,
293
+ background: 'rgba(16, 185, 129, 0.12)',
294
+ borderLeft: '3px solid #10b981',
295
+ color: '#a7f3d0'
296
+ }
297
+ } else if (line.startsWith('-')) {
298
+ style = {
299
+ ...style,
300
+ background: 'rgba(239, 68, 68, 0.12)',
301
+ borderLeft: '3px solid #ef4444',
302
+ color: '#fca5a5'
303
+ }
304
+ } else {
305
+ style = {
306
+ ...style,
307
+ color: '#e2e8f0',
308
+ }
309
+ }
310
+
311
+ return (
312
+ <div key={idx} style={style}>
313
+ {line}
314
+ </div>
315
+ )
316
+ })}
317
+ </div>
318
+ )
319
+ }
320
+
263
321
  interface ChatPanelProps {
264
322
  interactions: any[]
265
323
  sending: boolean
@@ -299,55 +357,171 @@ interface ChatPanelProps {
299
357
  onSetModel: (model: string) => void
300
358
  }
301
359
 
360
+ function ChatCodeBlock({ code, language }: { code: string; language: string; key?: any }) {
361
+ const [copied, setCopied] = useState(false)
362
+
363
+ const handleCopy = async () => {
364
+ try {
365
+ await navigator.clipboard.writeText(code)
366
+ setCopied(true)
367
+ setTimeout(() => setCopied(false), 2000)
368
+ } catch (err) {
369
+ console.error('Failed to copy code: ', err)
370
+ }
371
+ }
372
+
373
+ const handleDownload = () => {
374
+ try {
375
+ const blob = new Blob([code], { type: 'text/plain;charset=utf-8' })
376
+ const url = URL.createObjectURL(blob)
377
+ const link = document.createElement('a')
378
+ link.href = url
379
+ link.download = `code.${language === 'plaintext' ? 'txt' : language}`
380
+ link.click()
381
+ URL.revokeObjectURL(url)
382
+ } catch (err) {
383
+ console.error('Failed to download code: ', err)
384
+ }
385
+ }
386
+
387
+ const displayLang = language.charAt(0).toUpperCase() + language.slice(1).toLowerCase()
388
+
389
+ return (
390
+ <div className="chat-code-block-container" style={{ whiteSpace: 'normal' }}>
391
+ <div className="chat-code-block-header">
392
+ <span className="chat-code-block-lang">{displayLang}</span>
393
+ <div className="chat-code-block-actions">
394
+ <button type="button" onClick={handleDownload} title="Download code" className="chat-code-action-btn">
395
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
396
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
397
+ <polyline points="7 10 12 15 17 10" />
398
+ <line x1="12" y1="15" x2="12" y2="3" />
399
+ </svg>
400
+ </button>
401
+ <button type="button" onClick={handleCopy} title="Copy code" className="chat-code-action-btn">
402
+ {copied ? (
403
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
404
+ <polyline points="20 6 9 17 4 12" />
405
+ </svg>
406
+ ) : (
407
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
408
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
409
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
410
+ </svg>
411
+ )}
412
+ </button>
413
+ </div>
414
+ </div>
415
+ <pre className="chat-code-block-body">
416
+ <code>{code}</code>
417
+ </pre>
418
+ </div>
419
+ )
420
+ }
421
+
302
422
  function renderFormattedMessage(text: string) {
303
423
  const displayText = readableAssistantText(text)
304
424
  if (!displayText) return null
305
-
306
- const lines = displayText.split('\n')
307
- return lines.map((line, lineIndex) => {
308
- const headerMatch = line.match(/^(#{1,6})\s+(.*)$/)
309
-
310
- const formatInline = (str: string) => {
311
- const parts = str.split(/\*\*([\s\S]*?)\*\*/g)
312
- return parts.map((part, partIndex) => {
313
- if (partIndex % 2 === 1) {
425
+
426
+ const formatInline = (str: string) => {
427
+ const codeParts = str.split(/`([\s\S]*?)`/g)
428
+ return codeParts.map((codePart, codeIndex) => {
429
+ if (codeIndex % 2 === 1) {
430
+ return (
431
+ <code key={`code-${codeIndex}`} className="chat-inline-code">
432
+ {codePart}
433
+ </code>
434
+ )
435
+ }
436
+ const boldParts = codePart.split(/\*\*([\s\S]*?)\*\*/g)
437
+ return boldParts.map((boldPart, boldIndex) => {
438
+ if (boldIndex % 2 === 1) {
314
439
  return (
315
- <strong key={partIndex} className="chat-bold-highlight">
316
- {part}
440
+ <strong key={`bold-${boldIndex}`} className="chat-bold-highlight">
441
+ {boldPart}
317
442
  </strong>
318
443
  )
319
444
  }
320
- return part
445
+ return boldPart
321
446
  })
447
+ })
448
+ }
449
+
450
+ const lines = displayText.split('\n')
451
+ const items: any[] = []
452
+
453
+ let inCodeBlock = false
454
+ let codeBlockLang = ''
455
+ let codeBlockLines: string[] = []
456
+
457
+ for (let i = 0; i < lines.length; i++) {
458
+ const line = lines[i]
459
+
460
+ if (line.trim().startsWith('```')) {
461
+ if (inCodeBlock) {
462
+ const codeText = codeBlockLines.join('\n')
463
+ items.push(
464
+ <ChatCodeBlock
465
+ key={`code-block-${i}`}
466
+ code={codeText}
467
+ language={codeBlockLang}
468
+ />
469
+ )
470
+ inCodeBlock = false
471
+ codeBlockLines = []
472
+ } else {
473
+ inCodeBlock = true
474
+ codeBlockLang = line.trim().slice(3).trim() || 'plaintext'
475
+ }
476
+ continue
322
477
  }
323
478
 
479
+ if (inCodeBlock) {
480
+ codeBlockLines.push(line)
481
+ continue
482
+ }
483
+
484
+ const headerMatch = line.match(/^(#{1,6})\s+(.*)$/)
324
485
  if (headerMatch) {
325
486
  const level = headerMatch[1].length
326
487
  const content = headerMatch[2]
327
-
488
+
328
489
  const style = {
329
490
  fontWeight: 'bold',
330
491
  display: 'block',
331
492
  marginTop: level === 1 ? '16px' : level === 2 ? '14px' : '10px',
332
493
  marginBottom: '6px',
333
494
  fontSize: level === 1 ? '1.25em' : level === 2 ? '1.15em' : '1.05em',
334
- color: 'var(--text-main)'
495
+ color: 'var(--text-main)',
335
496
  }
336
-
337
- return (
338
- <span key={lineIndex} style={style}>
497
+
498
+ items.push(
499
+ <span key={`line-${i}`} style={style}>
339
500
  {formatInline(content)}
340
501
  </span>
341
502
  )
503
+ } else {
504
+ items.push(
505
+ <Fragment key={`line-${i}`}>
506
+ {formatInline(line)}
507
+ {i < lines.length - 1 && '\n'}
508
+ </Fragment>
509
+ )
342
510
  }
511
+ }
343
512
 
344
- return (
345
- <Fragment key={lineIndex}>
346
- {formatInline(line)}
347
- {lineIndex < lines.length - 1 && '\n'}
348
- </Fragment>
513
+ if (inCodeBlock && codeBlockLines.length > 0) {
514
+ const codeText = codeBlockLines.join('\n')
515
+ items.push(
516
+ <ChatCodeBlock
517
+ key={`code-block-end`}
518
+ code={codeText}
519
+ language={codeBlockLang}
520
+ />
349
521
  )
350
- })
522
+ }
523
+
524
+ return items
351
525
  }
352
526
 
353
527
  function readableAssistantText(text: string) {
@@ -554,6 +728,24 @@ export default function ChatPanel({
554
728
  const [openReviewIds, setOpenReviewIds] = useState<Record<string, boolean>>({})
555
729
  const [openFileDiffs, setOpenFileDiffs] = useState<Record<string, boolean>>({})
556
730
  const [toolMenuOpen, setToolMenuOpen] = useState(false)
731
+ const [elapsedSeconds, setElapsedSeconds] = useState(0)
732
+
733
+ useEffect(() => {
734
+ if (!sending) {
735
+ setElapsedSeconds(0)
736
+ return
737
+ }
738
+
739
+ const startTime = Date.now()
740
+ setElapsedSeconds(0)
741
+
742
+ const timer = setInterval(() => {
743
+ const elapsed = Math.round((Date.now() - startTime) / 1000)
744
+ setElapsedSeconds(elapsed)
745
+ }, 1000)
746
+
747
+ return () => clearInterval(timer)
748
+ }, [sending])
557
749
  const [isRecording, setIsRecording] = useState(false)
558
750
  const [voiceMode, setVoiceMode] = useState(false)
559
751
  const [voiceTranscript, setVoiceTranscript] = useState('')
@@ -1318,7 +1510,30 @@ export default function ChatPanel({
1318
1510
  {renderActiveFileChanges()}
1319
1511
  <div className="message ai-message thinking-message">
1320
1512
  <div className="bubble-wrapper">
1321
- <div className="message-bubble"><span>{streamedReply ? renderFormattedMessage(streamedReply) : 'Thinking...'}</span></div>
1513
+ <div className="message-bubble">
1514
+ <span>
1515
+ {streamedReply ? (
1516
+ renderFormattedMessage(streamedReply)
1517
+ ) : (
1518
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: 'var(--text-soft, #94a3b8)' }}>
1519
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" style={{ display: 'inline-block', flexShrink: 0 }}>
1520
+ <circle cx="12" cy="12" r="10" stroke="rgba(255, 255, 255, 0.12)" />
1521
+ <path d="M12 2a10 10 0 0 1 10 10" stroke="var(--accent)" strokeLinecap="round">
1522
+ <animateTransform
1523
+ attributeName="transform"
1524
+ type="rotate"
1525
+ from="0 12 12"
1526
+ to="360 12 12"
1527
+ dur="0.9s"
1528
+ repeatCount="indefinite"
1529
+ />
1530
+ </path>
1531
+ </svg>
1532
+ <span>Thinking for {elapsedSeconds}s</span>
1533
+ </div>
1534
+ )}
1535
+ </span>
1536
+ </div>
1322
1537
  {streamedResponse && (
1323
1538
  <div className="message-time" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
1324
1539
  <button className="provider-badge">{badge(streamedResponse.provider, streamedResponse.model)}</button>
@@ -1342,14 +1557,25 @@ export default function ChatPanel({
1342
1557
 
1343
1558
  {pendingApproval && (() => {
1344
1559
  const details = renderApprovalDetails(pendingApproval.approval)
1560
+ const writeFile = pendingApproval.approval?.WriteFile
1561
+ const applyPatch = pendingApproval.approval?.ApplyPatch
1562
+ const diffText = writeFile?.diff || applyPatch?.diff
1563
+
1345
1564
  return (
1346
1565
  <div className="message ai-message" style={{ width: '100%' }}>
1347
1566
  <div className="bubble-wrapper" style={{ width: '100%' }}>
1348
1567
  <div className="action-card approval-card" data-tier={details.isDangerous ? 'dangerous' : undefined} style={{ width: '100%' }}>
1349
- <div className="approval-card-content">
1568
+ <div className="approval-card-content" style={{ width: '100%' }}>
1350
1569
  <div className="approval-card-title">{details.title}</div>
1351
1570
  <div className="approval-card-body" style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>{details.body}</div>
1352
- {details.reason && <div className="approval-card-reason" style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>{details.reason}</div>}
1571
+ {diffText ? (
1572
+ <div className="approval-card-diff-container" style={{ marginTop: '8px', width: '100%' }}>
1573
+ <div style={{ fontSize: '0.8rem', fontWeight: 600, color: 'var(--text-soft, #94a3b8)', marginBottom: '4px' }}>Diff:</div>
1574
+ {renderDiff(diffText)}
1575
+ </div>
1576
+ ) : (
1577
+ details.reason && <div className="approval-card-reason" style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>{details.reason}</div>
1578
+ )}
1353
1579
  </div>
1354
1580
  <div className="approval-card-actions">
1355
1581
  <button type="button" className="approval-btn approval-btn-approve" onClick={() => onApproval(true)}>Approve</button>
@@ -1,6 +1,6 @@
1
1
  import { useState } from 'react'
2
2
 
3
- export type DashboardView = 'chat' | 'pictures' | 'model' | 'workspeac'
3
+ export type DashboardView = 'chat' | 'pictures' | 'model' | 'workspeac' | 'imagine'
4
4
 
5
5
  interface ChatSessionItem {
6
6
  id: string
@@ -133,6 +133,14 @@ export default function DashboardSidebar({
133
133
  </span>
134
134
  <span>Pictures</span>
135
135
  </button>
136
+ <button className={`sidebar-top-action ${view === 'imagine' ? 'is-active' : ''}`} onClick={() => onSetView('imagine')}>
137
+ <span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
138
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
139
+ <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
140
+ </svg>
141
+ </span>
142
+ <span>Image Studio</span>
143
+ </button>
136
144
  <button className={`sidebar-top-action ${view === 'workspeac' ? 'is-active' : ''}`} onClick={() => onSetView('workspeac')}>
137
145
  <span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
138
146
  <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">