@pheem49/mint 1.4.0 → 1.4.2

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.
@@ -32,7 +32,7 @@ const SLASH_COMMANDS = [
32
32
  function createChatUI({ onSubmit, onExit }) {
33
33
  const config = readConfig();
34
34
  const modelName = config.geminiModel || 'gemini';
35
- const workspaceName = path.basename(process.cwd());
35
+ const workspacePath = process.cwd();
36
36
  const HINT_DEFAULT = `{gray-fg} Enter send · Ctrl+Y copy · /help commands{/}`;
37
37
  const INPUT_FG = '#f8fafc';
38
38
  const INPUT_BG = '#10141c';
@@ -90,11 +90,10 @@ function createChatUI({ onSubmit, onExit }) {
90
90
  style: { bg: 'default' }
91
91
  });
92
92
 
93
- // ─── Input area ───────────────────────────────────────────────────────────
94
93
  const inputBox = blessed.textbox({
95
94
  bottom: 3, left: 1, width: '100%-2', height: 3,
96
95
  tags: false,
97
- inputOnFocus: true,
96
+ inputOnFocus: false, // We'll manage this manually for stability
98
97
  keys: true,
99
98
  style: {
100
99
  bg: INPUT_BG,
@@ -111,6 +110,16 @@ function createChatUI({ onSubmit, onExit }) {
111
110
  label: ' Message '
112
111
  });
113
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
+
114
123
  // ─── Placeholder (SIBLING widget floating over input content area) ─────────
115
124
  // inputBox: bottom=3, height=3, border=1 → content row at bottom=4, left=2
116
125
  const placeholderWidget = blessed.text({
@@ -169,7 +178,7 @@ function createChatUI({ onSubmit, onExit }) {
169
178
  } catch (_) {}
170
179
  }
171
180
 
172
- // ─── Status bar (3 columns: left / center / right) ──────────────────────
181
+ // ─── Status bar (2 lines as per screenshot) ──────────────────────
173
182
  const statusBar = blessed.box({
174
183
  bottom: 0, left: 1, width: '100%-2', height: 3,
175
184
  tags: true,
@@ -177,54 +186,62 @@ function createChatUI({ onSubmit, onExit }) {
177
186
  border: { type: 'line', fg: '#222c38' }
178
187
  });
179
188
 
180
- // Left: workspace info
181
- const statusLeft = blessed.text({
189
+ // Line 1: Thinking / Status (Left) and Shortcut (Right)
190
+ const statusLine1 = blessed.text({
182
191
  parent: statusBar,
183
- top: 0, left: 1,
184
- width: '33%',
192
+ top: 0, left: 1, right: 1,
185
193
  height: 1,
186
194
  tags: true,
187
- content: ` workspace {bold}(${workspaceName}){/bold}`,
188
- style: { bg: '#10141c', fg: '#93a0b7' }
195
+ content: `{#88aaff-fg}[Chat]{/} {#cc4444-fg}no sandbox{/}`,
196
+ style: { bg: '#10141c' }
189
197
  });
190
198
 
191
- // Center: mode + status
192
- const statusCenter = blessed.text({
199
+ const shortcutHint = blessed.text({
193
200
  parent: statusBar,
194
- top: 0,
195
- left: 'center',
196
- width: '44%',
201
+ top: 0, right: 1,
197
202
  height: 1,
198
- align: 'center',
199
203
  tags: true,
200
- content: `{#88aaff-fg}[Chat]{/} {#cc4444-fg}no sandbox{/}`,
201
- style: { bg: '#10141c', fg: '#888888' }
204
+ content: `{gray-fg}? for shortcuts{/}`,
205
+ style: { bg: '#10141c' }
202
206
  });
203
207
 
204
- // Right: current model
205
- const statusRight = blessed.text({
208
+ // Line 2: Action Hint (Left) and File Info (Right)
209
+ const statusLine2 = blessed.text({
206
210
  parent: statusBar,
207
- top: 0, right: 1,
208
- width: '33%',
211
+ top: 1, left: 1, right: 1,
209
212
  height: 1,
210
- align: 'right',
211
213
  tags: true,
212
- content: `{#88e0b0-fg}${modelName}{/}`,
213
- style: { bg: '#10141c', fg: '#88e0b0' }
214
+ content: `{gray-fg}Shift+Tab to accept edits{/}`,
215
+ style: { bg: '#10141c' }
214
216
  });
215
217
 
216
- let activeMode = 'Chat';
218
+ const fileInfo = blessed.text({
219
+ parent: statusBar,
220
+ top: 1, right: 1,
221
+ height: 1,
222
+ tags: true,
223
+ content: `{gray-fg}path: ${workspacePath}{/}`,
224
+ style: { bg: '#10141c' }
225
+ });
217
226
 
218
- function formatModeTag(mode) {
219
- if (mode === 'Code') return `{#ffd166-fg}[Code]{/}`;
220
- return `{#88aaff-fg}[Chat]{/}`;
227
+ let activeMode = 'Chat';
228
+ let spinnerIdx = 0;
229
+ const spinnerChars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
230
+
231
+ function formatTime(seconds) {
232
+ if (seconds < 60) return `${seconds}s`;
233
+ const mins = Math.floor(seconds / 60);
234
+ const secs = seconds % 60;
235
+ return `${mins}m ${secs}s`;
221
236
  }
222
237
 
223
- function updateStatusBar(thinkingText = null) {
238
+ function updateStatusBar(thinkingText = null, secondsElapsed = 0) {
224
239
  if (thinkingText) {
225
- statusCenter.setContent(`${formatModeTag(activeMode)} {#88e0b0-fg}${thinkingText}{/}`);
240
+ const char = spinnerChars[spinnerIdx % spinnerChars.length];
241
+ spinnerIdx++;
242
+ statusLine1.setContent(`{#88e0b0-fg}${char}{/} Thinking... {gray-fg}(esc to cancel, ${formatTime(secondsElapsed)}){/}`);
226
243
  } else {
227
- statusCenter.setContent(`${formatModeTag(activeMode)} {#cc4444-fg}no sandbox{/}`);
244
+ statusLine1.setContent(`${activeMode === 'Code' ? '{#ffd166-fg}[Code]{/}' : '{#88aaff-fg}[Chat]{/}'} {#cc4444-fg}no sandbox{/}`);
228
245
  }
229
246
  screen.render();
230
247
  }
@@ -234,9 +251,16 @@ function createChatUI({ onSubmit, onExit }) {
234
251
  updateStatusBar(null);
235
252
  }
236
253
 
237
- /** Update model name in status bar (called after /models switch) */
238
254
  function updateStatusModel(newModel) {
239
- statusRight.setContent(`{#88e0b0-fg}${newModel}{/}`);
255
+ if (!newModel) return;
256
+ shortcutHint.setContent(`{gray-fg}${newModel} · ? for shortcuts{/}`);
257
+ screen.render();
258
+ }
259
+
260
+ /** Update workspace name in status bar */
261
+ function updateWorkspace(newPath) {
262
+ if (!newPath) return;
263
+ fileInfo.setContent(`{gray-fg}path: ${newPath}{/}`);
240
264
  screen.render();
241
265
  }
242
266
  updateStatusBar();
@@ -250,6 +274,8 @@ function createChatUI({ onSubmit, onExit }) {
250
274
  screen.append(statusBar);
251
275
  screen.append(placeholderWidget); // sibling on top of inputBox
252
276
 
277
+ // Suggestion List and Approval Dialog remain same ...
278
+
253
279
  // ─── Suggestion List ──────────────────────────────────────────────────────
254
280
  const commandList = blessed.list({
255
281
  parent: screen,
@@ -284,7 +310,7 @@ function createChatUI({ onSubmit, onExit }) {
284
310
  border: { fg: '#88e0b0' }
285
311
  },
286
312
  width: '80%',
287
- height: 'shrink',
313
+ height: 12, // Fixed height to avoid 'shrink' miscalculation with buttons
288
314
  top: 'center',
289
315
  left: 'center',
290
316
  label: ' Approval ',
@@ -379,28 +405,43 @@ function createChatUI({ onSubmit, onExit }) {
379
405
  applyTerminalInputAttrs();
380
406
  });
381
407
 
408
+ // Restore focus to inputBox when clicked or when screen is clicked
409
+ screen.on('click', () => {
410
+ if (!approvalDialog.visible) {
411
+ inputBox.focus();
412
+ screen.render();
413
+ }
414
+ });
415
+
416
+ inputBox.on('click', () => {
417
+ inputBox.focus();
418
+ screen.render();
419
+ });
420
+
382
421
 
383
422
  // Submit or Select Suggestion on Enter
384
- inputBox.key(['enter'], () => {
423
+ inputBox.on('submit', (value) => {
385
424
  if (!commandList.hidden) {
386
425
  const selected = activeSuggestions[commandList.selected];
387
426
  if (selected) {
388
427
  inputBox.setValue(selected.name + ' ');
389
428
  commandList.hide();
390
429
  hidePlaceholder();
391
- inputBox.focus();
430
+ inputBox.focus();
431
+ inputBox.readInput(); // Re-focus to continue typing
392
432
  refreshInputStyles();
393
433
  screen.render();
394
434
  return; // Don't submit yet, let user add args or press enter again
395
435
  }
396
436
  }
397
437
 
398
- const raw = (inputBox.getValue ? inputBox.getValue() : inputBox.value) || '';
438
+ const raw = value || '';
399
439
  const text = raw.trim();
400
440
  if (!text) {
401
441
  inputBox.clearValue();
402
442
  showPlaceholder();
403
- inputBox.focus();
443
+ inputBox.focus();
444
+ inputBox.readInput(); // Re-focus to continue typing
404
445
  refreshInputStyles();
405
446
  screen.render();
406
447
  return;
@@ -409,7 +450,8 @@ function createChatUI({ onSubmit, onExit }) {
409
450
  // Clear input and restore placeholder
410
451
  inputBox.clearValue();
411
452
  showPlaceholder();
412
- inputBox.focus();
453
+ inputBox.focus();
454
+ inputBox.readInput(); // Explicitly restart reading
413
455
  refreshInputStyles();
414
456
  screen.render();
415
457
 
@@ -486,6 +528,7 @@ function createChatUI({ onSubmit, onExit }) {
486
528
 
487
529
  // ─── Initial render ───────────────────────────────────────────────────────
488
530
  inputBox.focus();
531
+ inputBox.readInput(); // Initial start
489
532
  refreshInputStyles();
490
533
  screen.render();
491
534
 
@@ -670,10 +713,72 @@ function createChatUI({ onSubmit, onExit }) {
670
713
  return { appendChunk, finalize };
671
714
  }
672
715
 
716
+ function appendCodeStep(info) {
717
+ if (typeof info === 'string') {
718
+ appendMessage('system', `[Code] ${info}`);
719
+ return;
720
+ }
721
+
722
+ const { step, phase, action, target, message, thought } = info;
723
+ const maxLineWidth = Math.max(screen.width - 20, 36);
724
+
725
+ // Special handling for ask_user which needs a box style
726
+ if (action === 'ask_user') {
727
+ chatBox.log('');
728
+ chatBox.log(` {#88e0b0-fg}✓{/} {bold}Ask User{/}`);
729
+ const questionLines = wrapText(target || message || '', maxLineWidth - 6);
730
+ questionLines.forEach(l => chatBox.log(` {#95a2b8-fg}${l}{/}`));
731
+ screen.render();
732
+ return;
733
+ }
734
+
735
+ let icon = '{#88e0b0-fg}✓{/}';
736
+ let label = action || phase;
737
+ let color = '{#ffffff-fg}';
738
+
739
+ // Map internal action names to display names seen in the screenshot
740
+ switch (action) {
741
+ case 'thinking':
742
+ if (phase === 'thinking' && !thought) {
743
+ // Initial "Thinking..." without a bubble
744
+ chatBox.log('');
745
+ chatBox.log(` {#ffd166-fg}* {bold}Thinking{/}`);
746
+ } else if (thought) {
747
+ // Show reasoning bubble
748
+ const thoughtLines = wrapText(thought, maxLineWidth - 6);
749
+ thoughtLines.forEach(l => chatBox.log(` {gray-fg}> ${l}{/}`));
750
+ }
751
+ screen.render();
752
+ return;
753
+ case 'ask_user': label = 'AskUser'; break;
754
+ case 'open_url': label = 'OpenURL'; break;
755
+ case 'open_app': label = 'OpenApp'; break;
756
+ case 'open_file': label = 'OpenFile'; break;
757
+ case 'open_folder': label = 'OpenFolder'; break;
758
+ case 'create_folder': label = 'CreateFolder'; break;
759
+ case 'system_info': label = 'SystemInfo'; break;
760
+ case 'system_automation': label = 'SystemAction'; break;
761
+ case 'web_search': label = 'WebSearch'; break;
762
+ case 'list_files':
763
+ case 'find_path': label = 'Explored'; break;
764
+ case 'read_file': label = 'ReadFile'; break;
765
+ case 'search_code': label = 'SearchText'; break;
766
+ case 'apply_patch':
767
+ case 'write_file': label = 'Edited'; break;
768
+ case 'run_shell': label = 'Ran command'; break;
769
+ case 'json_repair': icon = '*'; label = 'Repairing JSON'; color = '{#ffd166-fg}'; break;
770
+ case 'reviewer_start': label = 'Reviewing'; break;
771
+ }
772
+
773
+ const content = target || message || '';
774
+ chatBox.log(` ${icon} {bold}${label}{/} ${color}${content}{/}`);
775
+ screen.render();
776
+ }
777
+
673
778
  /** Show/hide thinking indicator in status bar */
674
779
  function setThinking(active, secondsElapsed = 0) {
675
780
  if (active) {
676
- updateStatusBar(`Thinking... {gray-fg}(esc to cancel, ${secondsElapsed}s){/}`);
781
+ updateStatusBar('Thinking...', secondsElapsed);
677
782
  } else {
678
783
  updateStatusBar(null);
679
784
  }
@@ -691,18 +796,28 @@ function createChatUI({ onSubmit, onExit }) {
691
796
  ? 'Shell Command'
692
797
  : request.type === 'patch'
693
798
  ? 'Patch Edit'
694
- : 'File Write';
799
+ : request.type === 'code_mode'
800
+ ? 'Enter Code Mode'
801
+ : 'File Write';
695
802
  const preview = request.preview || request.label || '';
696
803
  const message = [
697
804
  `{bold}${typeLabel}{/bold}`,
698
805
  '',
699
806
  preview,
700
807
  '',
701
- 'Approve this action?'
808
+ 'Approve this action?',
809
+ '', // Extra lines to push buttons down and avoid overlapping
810
+ ''
702
811
  ].join('\n');
703
812
 
813
+ // Temporarily stop reading input so the dialog can receive keys
814
+ if (inputBox._reading) {
815
+ inputBox.cancel();
816
+ }
817
+
704
818
  approvalDialog.ask(message, (approved) => {
705
819
  inputBox.focus();
820
+ inputBox.readInput(); // Ensure we resume reading after dialog
706
821
  refreshInputStyles();
707
822
  screen.render();
708
823
  resolve(Boolean(approved));
@@ -710,7 +825,45 @@ function createChatUI({ onSubmit, onExit }) {
710
825
  });
711
826
  }
712
827
 
713
- return { screen, appendMessage, streamMessage, setThinking, updateStatusModel, copyLastResponse, requestApproval, setMode };
828
+ function askUser(question) {
829
+ return new Promise((resolve) => {
830
+ // Temporarily stop reading input so we can capture the answer
831
+ if (inputBox._reading) {
832
+ inputBox.cancel();
833
+ }
834
+
835
+ // We use a simple textbox floating over the chat or reuse the main input?
836
+ // Reusing the main input is cleaner for CLI.
837
+ // But we need to change the label to ' Answer '
838
+ const oldLabel = inputBox._label.content;
839
+ inputBox._label.setContent(' Answer ');
840
+
841
+ // Clear input for the answer
842
+ inputBox.clearValue();
843
+ hidePlaceholder();
844
+ inputBox.focus();
845
+ inputBox.readInput();
846
+ screen.render();
847
+
848
+ const submitHandler = (value) => {
849
+ inputBox.removeListener('submit', submitHandler);
850
+ inputBox._label.setContent(oldLabel);
851
+
852
+ const answer = value || '';
853
+ chatBox.log('');
854
+ chatBox.log(` {bold}User answered:{/}`);
855
+ const lines = wrapText(answer, screen.width - 20);
856
+ lines.forEach(l => chatBox.log(` {#95a2b8-fg}${l}{/}`));
857
+ screen.render();
858
+
859
+ resolve(answer);
860
+ };
861
+
862
+ inputBox.on('submit', submitHandler);
863
+ });
864
+ }
865
+
866
+ return { screen, appendMessage, streamMessage, setThinking, updateStatusModel, copyLastResponse, requestApproval, setMode, appendCodeStep, updateWorkspace, askUser };
714
867
  }
715
868
 
716
869
  module.exports = { createChatUI };