@pheem49/mint 1.3.0 → 1.4.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.
Files changed (38) hide show
  1. package/.codex +0 -0
  2. package/README.md +174 -126
  3. package/main.js +21 -1
  4. package/mint-cli-logic.js +21 -1
  5. package/mint-cli.js +287 -45
  6. package/package.json +13 -2
  7. package/src/AI_Brain/Gemini_API.js +331 -64
  8. package/src/AI_Brain/agent_orchestrator.js +73 -0
  9. package/src/AI_Brain/autonomous_brain.js +2 -0
  10. package/src/AI_Brain/memory_store.js +318 -0
  11. package/src/AI_Brain/proactive_engine.js +2 -8
  12. package/src/Automation_Layer/file_operations.js +123 -4
  13. package/src/Automation_Layer/open_app.js +72 -43
  14. package/src/Automation_Layer/open_website.js +3 -3
  15. package/src/CLI/chat_router.js +57 -9
  16. package/src/CLI/chat_ui.js +117 -11
  17. package/src/CLI/code_agent.js +249 -36
  18. package/src/CLI/onboarding.js +53 -6
  19. package/src/CLI/workspace_manager.js +90 -0
  20. package/src/Plugins/docker.js +12 -10
  21. package/src/Plugins/spotify.js +168 -40
  22. package/src/Plugins/system_monitor.js +72 -0
  23. package/src/System/config_manager.js +35 -2
  24. package/src/System/custom_workflows.js +9 -2
  25. package/src/System/notifications.js +23 -0
  26. package/src/UI/settings.html +143 -65
  27. package/src/UI/settings.js +155 -41
  28. package/tests/agent_orchestrator.test.js +41 -0
  29. package/tests/chat_router.test.js +42 -0
  30. package/tests/code_agent.test.js +69 -0
  31. package/tests/config_manager.test.js +141 -0
  32. package/tests/docker.test.js +46 -0
  33. package/tests/file_operations.test.js +57 -0
  34. package/tests/memory_store.test.js +185 -0
  35. package/tests/provider_routing.test.js +67 -0
  36. package/tests/spotify.test.js +201 -0
  37. package/tests/system_monitor.test.js +37 -0
  38. package/tests/workspace_manager.test.js +56 -0
@@ -1,8 +1,8 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { GoogleGenAI } = require('@google/genai');
4
- const { executeCodeTask } = require('./code_agent');
5
- const { readConfig } = require('../System/config_manager');
4
+ const { executeCodeTask, _helpers: codeAgentHelpers } = require('./code_agent');
5
+ const { readConfig, getAvailableProviders } = require('../System/config_manager');
6
6
 
7
7
  const DEFAULT_GEMINI_MODEL = 'gemini-2.5-flash';
8
8
 
@@ -26,7 +26,19 @@ Return JSON only:
26
26
  }
27
27
 
28
28
  Choose "code" when the user is asking to inspect, edit, review, debug, explain, refactor, verify, or otherwise operate on the current project/workspace/codebase/files.
29
- Choose "chat" for general conversation, factual Q&A, or non-code assistant tasks.`;
29
+ Choose "chat" for general conversation, factual Q&A, small/simple requests, non-code assistant tasks, or direct file-system actions like finding/opening a folder or file by name.
30
+ Only choose "code" for substantial coding work that likely needs multiple steps, workspace inspection, edits, verification, or project-wide reasoning.`;
31
+
32
+ function isDirectFilesystemActionRequest(text) {
33
+ const input = (text || '').trim().toLowerCase();
34
+ if (!input) return false;
35
+
36
+ const filesystemActionPattern = /(open|find|locate|search for|look for|หา|ค้นหา|เปิด)/;
37
+ const filesystemTargetPattern = /(folder|directory|dir|file|โฟลเดอร์|ไฟล์|ไดเรกทอรี)/;
38
+ const codeOperationPattern = /(inspect|review|refactor|debug|implement|edit|change|fix|explain|analyze|สำรวจ|รีวิว|รีแฟกเตอร์|แก้|อธิบาย|วิเคราะห์)/;
39
+
40
+ return filesystemActionPattern.test(input) && filesystemTargetPattern.test(input) && !codeOperationPattern.test(input);
41
+ }
30
42
 
31
43
  function workspaceLooksLikeCodebase(workspaceRoot) {
32
44
  const markers = [
@@ -45,13 +57,27 @@ function detectCodeIntentHeuristic(text, workspaceRoot = process.cwd()) {
45
57
  const input = (text || '').trim().toLowerCase();
46
58
  if (!input) return false;
47
59
  if (input.startsWith('/code ')) return true;
60
+ if (isDirectFilesystemActionRequest(input)) return false;
61
+
62
+ return isLargeCodeTaskRequest(input, workspaceRoot);
63
+ }
64
+
65
+ function isLargeCodeTaskRequest(text, workspaceRoot = process.cwd()) {
66
+ const input = (text || '').trim().toLowerCase();
67
+ if (!input) return false;
68
+ if (!workspaceLooksLikeCodebase(workspaceRoot)) return false;
48
69
 
49
70
  const hasCodeKeyword = CODE_KEYWORDS.some(keyword => input.includes(keyword));
50
71
  const hasThaiCodeKeyword = THAI_CODE_KEYWORDS.some(keyword => input.includes(keyword));
51
72
  const referencesProject = /โปรเจคนี้|โปรเจ็กต์นี้|this project|this repo|this repository|codebase|workspace/.test(input);
52
73
  const asksForAction = /สำรวจ|ดู|แก้|เพิ่ม|ลบ|ปรับ|ตรวจ|วิเคราะห์|implement|inspect|explore|fix|update|change|refactor|review|explain|debug/.test(input);
74
+ const strongTaskSignal = /failing tests?|run tests?|verify|verification|bug|issue|error|refactor|implement|feature|patch|edit|modify|analyze the project|แก้บั๊ก|รันเทสต์|ทดสอบ|ตรวจสอบ|ยืนยันผล|รีแฟกเตอร์|เพิ่มฟีเจอร์|แก้โค้ด|วิเคราะห์โปรเจค/.test(input);
75
+ const multiStepSignal = /and|then|พร้อม|แล้ว|จากนั้น|ทั้ง|ทั่วทั้ง|ทั้งโปรเจค|project-wide|entire project|whole project/.test(input);
53
76
 
54
- return workspaceLooksLikeCodebase(workspaceRoot) && (referencesProject || ((hasCodeKeyword || hasThaiCodeKeyword) && asksForAction));
77
+ if (referencesProject && strongTaskSignal) return true;
78
+ if ((hasCodeKeyword || hasThaiCodeKeyword) && asksForAction && strongTaskSignal) return true;
79
+ if ((hasCodeKeyword || hasThaiCodeKeyword) && multiStepSignal && asksForAction) return true;
80
+ return false;
55
81
  }
56
82
 
57
83
  function getRouterClient() {
@@ -71,7 +97,7 @@ function summarizeWorkspace(workspaceRoot) {
71
97
  .join(', ') || '(no obvious code markers)';
72
98
  }
73
99
 
74
- async function detectCodeIntent(text, workspaceRoot = process.cwd()) {
100
+ async function detectCodeIntent(text, workspaceRoot = process.cwd(), history = []) {
75
101
  const input = (text || '').trim();
76
102
  if (!input) {
77
103
  return { route: 'chat', reason: 'Empty input.' };
@@ -81,6 +107,10 @@ async function detectCodeIntent(text, workspaceRoot = process.cwd()) {
81
107
  return { route: 'code', reason: 'Explicit /code command.' };
82
108
  }
83
109
 
110
+ if (isDirectFilesystemActionRequest(input)) {
111
+ return { route: 'chat', reason: 'Direct file-system action request.' };
112
+ }
113
+
84
114
  const heuristicRoute = detectCodeIntentHeuristic(input, workspaceRoot);
85
115
  const routerClient = getRouterClient();
86
116
  if (!routerClient) {
@@ -103,7 +133,8 @@ async function detectCodeIntent(text, workspaceRoot = process.cwd()) {
103
133
  text: [
104
134
  `Workspace: ${workspaceRoot}`,
105
135
  `Workspace markers: ${summarizeWorkspace(workspaceRoot)}`,
106
- `Message: ${input}`
136
+ `Context (Last 5 turns): ${history.slice(-10).map(m => `${m.sender}: ${m.text}`).join('\n')}`,
137
+ `Current Message: ${input}`
107
138
  ].join('\n')
108
139
  }]
109
140
  }]
@@ -112,6 +143,12 @@ async function detectCodeIntent(text, workspaceRoot = process.cwd()) {
112
143
  const textOutput = typeof response.text === 'function' ? response.text() : response.text;
113
144
  const parsed = JSON.parse(textOutput);
114
145
  const route = parsed.route === 'code' ? 'code' : 'chat';
146
+ if (route === 'code' && !isLargeCodeTaskRequest(input, workspaceRoot)) {
147
+ return {
148
+ route: 'chat',
149
+ reason: 'Request looks small enough for normal chat.'
150
+ };
151
+ }
115
152
  return {
116
153
  route,
117
154
  reason: parsed.reason || (route === 'code' ? 'Model classified as code.' : 'Model classified as chat.')
@@ -126,9 +163,13 @@ async function detectCodeIntent(text, workspaceRoot = process.cwd()) {
126
163
 
127
164
  async function runChatRoutedTask(input, context) {
128
165
  const text = input.startsWith('/code ') ? input.slice('/code '.length).trim() : input;
129
- const { appendMessage, setThinking, requestApproval, setMode } = context;
166
+ const { appendMessage, setThinking, requestApproval, setMode, history } = context;
167
+
168
+ const config = readConfig();
169
+ const availableProviders = getAvailableProviders(config);
170
+ const preferredProvider = codeAgentHelpers.selectSupportedCodeProvider(config, availableProviders);
130
171
 
131
- appendMessage('system', `Routing this request to Code Mode for workspace: ${process.cwd()}`);
172
+ appendMessage('system', `Routing this request to Code Mode for workspace: ${process.cwd()} using [${preferredProvider}]`);
132
173
  if (setMode) setMode('Code');
133
174
 
134
175
  let seconds = 0;
@@ -142,6 +183,8 @@ async function runChatRoutedTask(input, context) {
142
183
  const result = await executeCodeTask(text, {
143
184
  cwd: process.cwd(),
144
185
  requestApproval,
186
+ provider: preferredProvider,
187
+ history: history,
145
188
  onProgress: (message) => appendMessage('system', `[Code] ${message}`)
146
189
  });
147
190
  clearInterval(timer);
@@ -162,5 +205,10 @@ async function runChatRoutedTask(input, context) {
162
205
 
163
206
  module.exports = {
164
207
  detectCodeIntent,
165
- runChatRoutedTask
208
+ runChatRoutedTask,
209
+ _helpers: {
210
+ detectCodeIntentHeuristic,
211
+ isDirectFilesystemActionRequest,
212
+ isLargeCodeTaskRequest
213
+ }
166
214
  };
@@ -14,6 +14,10 @@ const SLASH_COMMANDS = [
14
14
  { name: '/copy', desc: 'Copy last response to clipboard' },
15
15
  { name: '/clear', desc: 'Clear conversation history' },
16
16
  { name: '/reset', desc: 'Reset conversation history' },
17
+ { name: '/agent', desc: 'Switch AI personas (coder, researcher, etc)' },
18
+ { name: '/workspace', desc: 'Manage project-specific contexts' },
19
+ { name: '/review', desc: 'Request a second-pass review of the last response' },
20
+ { name: '/stats', desc: 'Show system health stats (CPU/RAM/Disk)' },
17
21
  { name: '/help', desc: 'Show help information' },
18
22
  { name: '/exit', desc: 'Exit Mint' }
19
23
  ];
@@ -86,11 +90,10 @@ function createChatUI({ onSubmit, onExit }) {
86
90
  style: { bg: 'default' }
87
91
  });
88
92
 
89
- // ─── Input area ───────────────────────────────────────────────────────────
90
93
  const inputBox = blessed.textbox({
91
94
  bottom: 3, left: 1, width: '100%-2', height: 3,
92
95
  tags: false,
93
- inputOnFocus: true,
96
+ inputOnFocus: false, // We'll manage this manually for stability
94
97
  keys: true,
95
98
  style: {
96
99
  bg: INPUT_BG,
@@ -107,6 +110,16 @@ function createChatUI({ onSubmit, onExit }) {
107
110
  label: ' Message '
108
111
  });
109
112
 
113
+ // --- SAFETY PATCH ---
114
+ // Prevent "TypeError: done is not a function" if a listener survives a blur/focus cycle.
115
+ const originalListener = inputBox._listener;
116
+ inputBox._listener = function(ch, key) {
117
+ if (typeof this._done !== 'function') return;
118
+ return originalListener.call(this, ch, key);
119
+ };
120
+
121
+
122
+
110
123
  // ─── Placeholder (SIBLING widget floating over input content area) ─────────
111
124
  // inputBox: bottom=3, height=3, border=1 → content row at bottom=4, left=2
112
125
  const placeholderWidget = blessed.text({
@@ -232,6 +245,7 @@ function createChatUI({ onSubmit, onExit }) {
232
245
 
233
246
  /** Update model name in status bar (called after /models switch) */
234
247
  function updateStatusModel(newModel) {
248
+ if (!newModel) return;
235
249
  statusRight.setContent(`{#88e0b0-fg}${newModel}{/}`);
236
250
  screen.render();
237
251
  }
@@ -280,7 +294,7 @@ function createChatUI({ onSubmit, onExit }) {
280
294
  border: { fg: '#88e0b0' }
281
295
  },
282
296
  width: '80%',
283
- height: 'shrink',
297
+ height: 12, // Fixed height to avoid 'shrink' miscalculation with buttons
284
298
  top: 'center',
285
299
  left: 'center',
286
300
  label: ' Approval ',
@@ -377,26 +391,28 @@ function createChatUI({ onSubmit, onExit }) {
377
391
 
378
392
 
379
393
  // Submit or Select Suggestion on Enter
380
- inputBox.key(['enter'], () => {
394
+ inputBox.on('submit', (value) => {
381
395
  if (!commandList.hidden) {
382
396
  const selected = activeSuggestions[commandList.selected];
383
397
  if (selected) {
384
398
  inputBox.setValue(selected.name + ' ');
385
399
  commandList.hide();
386
400
  hidePlaceholder();
387
- inputBox.focus();
401
+ inputBox.focus();
402
+ inputBox.readInput(); // Re-focus to continue typing
388
403
  refreshInputStyles();
389
404
  screen.render();
390
405
  return; // Don't submit yet, let user add args or press enter again
391
406
  }
392
407
  }
393
408
 
394
- const raw = (inputBox.getValue ? inputBox.getValue() : inputBox.value) || '';
409
+ const raw = value || '';
395
410
  const text = raw.trim();
396
411
  if (!text) {
397
412
  inputBox.clearValue();
398
413
  showPlaceholder();
399
- inputBox.focus();
414
+ inputBox.focus();
415
+ inputBox.readInput(); // Re-focus to continue typing
400
416
  refreshInputStyles();
401
417
  screen.render();
402
418
  return;
@@ -405,7 +421,8 @@ function createChatUI({ onSubmit, onExit }) {
405
421
  // Clear input and restore placeholder
406
422
  inputBox.clearValue();
407
423
  showPlaceholder();
408
- inputBox.focus();
424
+ inputBox.focus();
425
+ inputBox.readInput(); // Explicitly restart reading
409
426
  refreshInputStyles();
410
427
  screen.render();
411
428
 
@@ -482,6 +499,7 @@ function createChatUI({ onSubmit, onExit }) {
482
499
 
483
500
  // ─── Initial render ───────────────────────────────────────────────────────
484
501
  inputBox.focus();
502
+ inputBox.readInput(); // Initial start
485
503
  refreshInputStyles();
486
504
  screen.render();
487
505
 
@@ -588,6 +606,84 @@ function createChatUI({ onSubmit, onExit }) {
588
606
  screen.render();
589
607
  }
590
608
 
609
+ /**
610
+ * Opens a streaming message bubble for the assistant.
611
+ * Returns { appendChunk(text), finalize(timestamp) } for typewriter rendering.
612
+ * Usage:
613
+ * const stream = streamMessage('assistant');
614
+ * stream.appendChunk('Hello'); stream.appendChunk(' World');
615
+ * stream.finalize(timestamp);
616
+ */
617
+ function streamMessage(role = 'assistant') {
618
+ const now = new Date();
619
+ const timeStr = now.toLocaleTimeString('th-TH', { hour: '2-digit', minute: '2-digit', hour12: false });
620
+ const maxLineWidth = Math.max(screen.width - 20, 36);
621
+
622
+ // Print the header bubble once
623
+ chatBox.log('');
624
+ if (role === 'assistant') {
625
+ chatBox.log(` {bold}{#d4a8ff-fg}Mint{/} {gray-fg}${timeStr}{/}`);
626
+ }
627
+
628
+ let buffer = ''; // accumulates the full response text
629
+ let lineBuffer = ''; // current partial line being built
630
+ let lineRendered = false; // whether we already pushed the first line prefix
631
+
632
+ function flushLine(force = false) {
633
+ // Flush content that fits on one line-width or when forced
634
+ if (!lineBuffer && !force) return;
635
+ if (!lineRendered) {
636
+ chatBox.log(` {#5a456d-fg}▏{/} {#ffffff-fg}${lineBuffer}{/}`);
637
+ lineRendered = true;
638
+ } else {
639
+ // Overwrite the last line by popping + re-pushing (blessed.log limitation)
640
+ // We can't truly overwrite, so we just keep appending new lines for each chunk.
641
+ // For large chunks, split on newline and emit per-line.
642
+ chatBox.log(` {#5a456d-fg}▏{/} {#ffffff-fg}${lineBuffer}{/}`);
643
+ }
644
+ screen.render();
645
+ }
646
+
647
+ function appendChunk(text) {
648
+ if (!text) return;
649
+ buffer += text;
650
+ const segments = text.split('\n');
651
+ for (let i = 0; i < segments.length; i++) {
652
+ lineBuffer += segments[i];
653
+ if (i < segments.length - 1) {
654
+ // Newline boundary — emit current line
655
+ const lines = wrapLineSmart(lineBuffer, maxLineWidth);
656
+ lines.forEach(l => chatBox.log(` {#5a456d-fg}▏{/} {#ffffff-fg}${l}{/}`));
657
+ lineBuffer = '';
658
+ lineRendered = true;
659
+ screen.render();
660
+ } else if (lineBuffer.length >= maxLineWidth) {
661
+ // Line overflow — auto-wrap
662
+ const lines = wrapLineSmart(lineBuffer, maxLineWidth);
663
+ lines.slice(0, -1).forEach(l => chatBox.log(` {#5a456d-fg}▏{/} {#ffffff-fg}${l}{/}`));
664
+ lineBuffer = lines[lines.length - 1] || '';
665
+ lineRendered = true;
666
+ screen.render();
667
+ }
668
+ // Otherwise keep buffering the partial line
669
+ }
670
+ }
671
+
672
+ function finalize(timestamp = null) {
673
+ // Flush remaining buffer
674
+ if (lineBuffer) {
675
+ const lines = wrapLineSmart(lineBuffer, maxLineWidth);
676
+ lines.forEach(l => chatBox.log(` {#5a456d-fg}▏{/} {#ffffff-fg}${l}{/}`));
677
+ lineBuffer = '';
678
+ }
679
+ // Track last response for clipboard
680
+ lastAssistantResponse = buffer;
681
+ screen.render();
682
+ }
683
+
684
+ return { appendChunk, finalize };
685
+ }
686
+
591
687
  /** Show/hide thinking indicator in status bar */
592
688
  function setThinking(active, secondsElapsed = 0) {
593
689
  if (active) {
@@ -609,18 +705,28 @@ function createChatUI({ onSubmit, onExit }) {
609
705
  ? 'Shell Command'
610
706
  : request.type === 'patch'
611
707
  ? 'Patch Edit'
612
- : 'File Write';
708
+ : request.type === 'code_mode'
709
+ ? 'Enter Code Mode'
710
+ : 'File Write';
613
711
  const preview = request.preview || request.label || '';
614
712
  const message = [
615
713
  `{bold}${typeLabel}{/bold}`,
616
714
  '',
617
715
  preview,
618
716
  '',
619
- 'Approve this action?'
717
+ 'Approve this action?',
718
+ '', // Extra lines to push buttons down and avoid overlapping
719
+ ''
620
720
  ].join('\n');
621
721
 
722
+ // Temporarily stop reading input so the dialog can receive keys
723
+ if (inputBox._reading) {
724
+ inputBox.cancel();
725
+ }
726
+
622
727
  approvalDialog.ask(message, (approved) => {
623
728
  inputBox.focus();
729
+ inputBox.readInput(); // Ensure we resume reading after dialog
624
730
  refreshInputStyles();
625
731
  screen.render();
626
732
  resolve(Boolean(approved));
@@ -628,7 +734,7 @@ function createChatUI({ onSubmit, onExit }) {
628
734
  });
629
735
  }
630
736
 
631
- return { screen, appendMessage, setThinking, updateStatusModel, copyLastResponse, requestApproval, setMode };
737
+ return { screen, appendMessage, streamMessage, setThinking, updateStatusModel, copyLastResponse, requestApproval, setMode };
632
738
  }
633
739
 
634
740
  module.exports = { createChatUI };