@staticpayload/zai-code 1.4.2 → 1.4.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.
package/dist/tui.js CHANGED
@@ -136,6 +136,15 @@ function getPlaceholder() {
136
136
  // Recent commands history
137
137
  const commandHistory = [];
138
138
  let historyIndex = -1;
139
+ // Big ASCII Logo
140
+ const ASCII_LOGO = `{bold}{cyan-fg}
141
+ ███████╗ █████╗ ██╗ ██████╗ ██████╗ ██████╗ ███████╗
142
+ ╚══███╔╝██╔══██╗██║ ██╔════╝██╔═══██╗██╔══██╗██╔════╝
143
+ ███╔╝ ███████║██║ ██║ ██║ ██║██║ ██║█████╗
144
+ ███╔╝ ██╔══██║██║ ██║ ██║ ██║██║ ██║██╔══╝
145
+ ███████╗██║ ██║██║ ╚██████╗╚██████╔╝██████╔╝███████╗
146
+ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝{/cyan-fg}{/bold}
147
+ {gray-fg}AI-native code editor v1.4.3{/gray-fg}`;
139
148
  // Animated Robot Mascot Frames
140
149
  const MASCOT_FRAMES = [
141
150
  // Frame 1 - looking right
@@ -168,7 +177,7 @@ const MASCOT_FRAMES = [
168
177
  {cyan-fg} ╰┬───┬╯ {/cyan-fg}`,
169
178
  ];
170
179
  // Compact header with mascot
171
- const HEADER_WITH_MASCOT = `{bold}{cyan-fg}zai{/cyan-fg}{blue-fg}·{/blue-fg}{cyan-fg}code{/cyan-fg}{/bold} {gray-fg}v1.4.2{/gray-fg}`;
180
+ const HEADER_WITH_MASCOT = `{bold}{cyan-fg}zai{/cyan-fg}{blue-fg}·{/blue-fg}{cyan-fg}code{/cyan-fg}{/bold} {gray-fg}v1.4.3{/gray-fg}`;
172
181
  const MINIMAL_LOGO = '{bold}{cyan-fg}⚡ zai·code{/cyan-fg}{/bold} {gray-fg}AI-native editor{/gray-fg}';
173
182
  // Welcome tips - rotate through these
174
183
  const WELCOME_TIPS = [
@@ -216,71 +225,39 @@ async function startTUI(options) {
216
225
  let spinnerFrame = 0;
217
226
  let isProcessing = false;
218
227
  let currentTip = Math.floor(Math.random() * WELCOME_TIPS.length);
219
- let mascotFrame = 0;
220
- let mascotInterval = null;
221
- // Header with animated mascot
228
+ // Header with ASCII logo
222
229
  const header = blessed.box({
223
230
  top: 0,
224
231
  left: 0,
225
232
  width: '100%',
226
- height: (0, settings_1.shouldShowLogo)() ? 8 : 2,
233
+ height: (0, settings_1.shouldShowLogo)() ? 9 : 2,
227
234
  tags: true,
228
235
  style: {
229
236
  fg: theme.fg,
230
237
  bg: theme.bg,
231
238
  },
232
239
  });
233
- // Update header with mascot animation
240
+ // Update header with logo
234
241
  function updateHeader() {
235
242
  if ((0, settings_1.shouldShowLogo)()) {
236
- const mascot = MASCOT_FRAMES[mascotFrame];
237
- const title = `{bold}{cyan-fg}zai{/cyan-fg}{blue-fg}·{/blue-fg}{cyan-fg}code{/cyan-fg}{/bold} {gray-fg}v1.4.2{/gray-fg}`;
238
- const subtitle = '{gray-fg}AI-native code editor{/gray-fg}';
239
- // Combine mascot with title
240
- const mascotLines = mascot.split('\n');
241
- const content = mascotLines.map((line, i) => {
242
- if (i === 1)
243
- return line + ' ' + title;
244
- if (i === 2)
245
- return line + ' ' + subtitle;
246
- return line;
247
- }).join('\n');
248
- header.setContent(content);
243
+ header.setContent(ASCII_LOGO);
244
+ header.height = 9;
249
245
  }
250
246
  else {
251
247
  header.setContent(MINIMAL_LOGO);
252
- }
253
- }
254
- // Start mascot animation
255
- function startMascotAnimation() {
256
- if (mascotInterval)
257
- return;
258
- mascotInterval = setInterval(() => {
259
- mascotFrame = (mascotFrame + 1) % MASCOT_FRAMES.length;
260
- updateHeader();
261
- screen.render();
262
- }, 2000); // Change every 2 seconds
263
- }
264
- // Stop mascot animation
265
- function stopMascotAnimation() {
266
- if (mascotInterval) {
267
- clearInterval(mascotInterval);
268
- mascotInterval = null;
248
+ header.height = 2;
269
249
  }
270
250
  }
271
251
  // Initialize header
272
252
  updateHeader();
273
- if ((0, settings_1.shouldShowLogo)()) {
274
- startMascotAnimation();
275
- }
276
253
  // Quick actions bar - keyboard shortcuts
277
254
  const quickActions = blessed.box({
278
- top: (0, settings_1.shouldShowLogo)() ? 8 : 2,
255
+ top: (0, settings_1.shouldShowLogo)() ? 9 : 2,
279
256
  left: 0,
280
257
  width: '100%',
281
258
  height: 1,
282
259
  tags: true,
283
- content: '{gray-fg}↑↓{/gray-fg} navigate {gray-fg}Tab{/gray-fg} complete {gray-fg}Enter{/gray-fg} select {gray-fg}Esc{/gray-fg} close',
260
+ content: '{gray-fg}↑↓{/gray-fg} navigate {gray-fg}Tab{/gray-fg} complete {gray-fg}Enter{/gray-fg} select {gray-fg}Esc{/gray-fg} close {gray-fg}Shift+Tab{/gray-fg} mode',
284
261
  style: {
285
262
  fg: theme.fg,
286
263
  bg: theme.bg,
@@ -288,7 +265,7 @@ async function startTUI(options) {
288
265
  padding: { left: 1 },
289
266
  });
290
267
  // Context/status line
291
- const contextTop = (0, settings_1.shouldShowLogo)() ? 9 : 3;
268
+ const contextTop = (0, settings_1.shouldShowLogo)() ? 10 : 3;
292
269
  const contextLine = blessed.box({
293
270
  top: contextTop,
294
271
  left: 0,
@@ -484,7 +461,7 @@ async function startTUI(options) {
484
461
  const icon = icons[mode] || '❯';
485
462
  modeIndicator.setContent(`{bold}{${color}-fg}${icon}{/${color}-fg}{/bold}`);
486
463
  }
487
- // Input textbox
464
+ // Input textbox - keys: false to prevent double input
488
465
  const input = blessed.textbox({
489
466
  parent: inputContainer,
490
467
  left: 3,
@@ -492,7 +469,7 @@ async function startTUI(options) {
492
469
  width: '100%-5',
493
470
  height: 1,
494
471
  inputOnFocus: true,
495
- keys: true,
472
+ keys: false, // IMPORTANT: false to prevent double key handling
496
473
  mouse: true,
497
474
  style: {
498
475
  fg: theme.fg,
@@ -511,14 +488,16 @@ async function startTUI(options) {
511
488
  },
512
489
  });
513
490
  function updatePlaceholder() {
514
- const val = input.getValue();
515
- if (!val || val.length === 0) {
491
+ const val = input.getValue() || '';
492
+ if (val.length === 0) {
516
493
  placeholder.setContent(`{gray-fg}${getPlaceholder()}{/gray-fg}`);
517
494
  placeholder.show();
518
495
  }
519
496
  else {
497
+ placeholder.setContent('');
520
498
  placeholder.hide();
521
499
  }
500
+ screen.render();
522
501
  }
523
502
  // Status bar at bottom - more informative
524
503
  const statusBar = blessed.box({
@@ -656,14 +635,15 @@ async function startTUI(options) {
656
635
  let autocompleteFiles = [];
657
636
  function updatePalette(filter) {
658
637
  const query = filter.replace(/^\//, '').toLowerCase();
659
- const filtered = COMMANDS.filter(c => c.name.startsWith(query) || c.description.toLowerCase().includes(query));
660
- const items = filtered.slice(0, 10).map(c => {
638
+ filteredCommandsCache = COMMANDS.filter(c => c.name.startsWith(query) || c.description.toLowerCase().includes(query)).slice(0, 10);
639
+ const items = filteredCommandsCache.map(c => {
661
640
  const shortcut = c.shortcut ? ` {gray-fg}${c.shortcut}{/gray-fg}` : '';
662
641
  return `{bold}{cyan-fg}/${c.name}{/cyan-fg}{/bold} ${c.description}${shortcut}`;
663
642
  });
664
643
  palette.setItems(items);
665
- if (filtered.length > 0) {
644
+ if (filteredCommandsCache.length > 0) {
666
645
  palette.select(0);
646
+ paletteSelectedIndex = 0;
667
647
  }
668
648
  }
669
649
  function updateFileAutocomplete(query) {
@@ -798,15 +778,16 @@ async function startTUI(options) {
798
778
  // Track palette selection index manually
799
779
  let paletteSelectedIndex = 0;
800
780
  let fileSelectedIndex = 0;
781
+ let filteredCommandsCache = [];
801
782
  // Screen-level key handling for arrow keys (works better than input keypress)
802
783
  screen.key(['up'], () => {
803
- if (showPalette) {
784
+ if (showPalette && filteredCommandsCache.length > 0) {
804
785
  paletteSelectedIndex = Math.max(0, paletteSelectedIndex - 1);
805
786
  palette.select(paletteSelectedIndex);
806
787
  screen.render();
807
788
  return;
808
789
  }
809
- if (showFileAutocomplete) {
790
+ if (showFileAutocomplete && autocompleteFiles.length > 0) {
810
791
  fileSelectedIndex = Math.max(0, fileSelectedIndex - 1);
811
792
  fileAutocomplete.select(fileSelectedIndex);
812
793
  screen.render();
@@ -821,15 +802,15 @@ async function startTUI(options) {
821
802
  }
822
803
  });
823
804
  screen.key(['down'], () => {
824
- if (showPalette) {
825
- const maxIndex = Math.min(COMMANDS.length - 1, 9);
805
+ if (showPalette && filteredCommandsCache.length > 0) {
806
+ const maxIndex = filteredCommandsCache.length - 1;
826
807
  paletteSelectedIndex = Math.min(maxIndex, paletteSelectedIndex + 1);
827
808
  palette.select(paletteSelectedIndex);
828
809
  screen.render();
829
810
  return;
830
811
  }
831
- if (showFileAutocomplete) {
832
- const maxIndex = Math.min(autocompleteFiles.length - 1, 9);
812
+ if (showFileAutocomplete && autocompleteFiles.length > 0) {
813
+ const maxIndex = autocompleteFiles.length - 1;
833
814
  fileSelectedIndex = Math.min(maxIndex, fileSelectedIndex + 1);
834
815
  fileAutocomplete.select(fileSelectedIndex);
835
816
  screen.render();
@@ -851,11 +832,9 @@ async function startTUI(options) {
851
832
  });
852
833
  // Tab completion
853
834
  screen.key(['tab'], () => {
854
- if (showPalette) {
855
- const filter = (input.getValue() || '').replace(/^\//, '').toLowerCase();
856
- const filtered = COMMANDS.filter(c => c.name.startsWith(filter) || c.description.toLowerCase().includes(filter));
857
- if (filtered[paletteSelectedIndex]) {
858
- const cmd = filtered[paletteSelectedIndex];
835
+ if (showPalette && filteredCommandsCache.length > 0) {
836
+ if (filteredCommandsCache[paletteSelectedIndex]) {
837
+ const cmd = filteredCommandsCache[paletteSelectedIndex];
859
838
  input.setValue('/' + cmd.name + ' ');
860
839
  togglePalette(false);
861
840
  paletteSelectedIndex = 0;
@@ -882,9 +861,14 @@ async function startTUI(options) {
882
861
  });
883
862
  // Manual keypress handling for other keys
884
863
  input.on('keypress', (ch, key) => {
864
+ // Hide placeholder immediately on any keypress
865
+ placeholder.hide();
866
+ screen.render();
885
867
  if (!key) {
886
- updatePlaceholder();
887
- screen.render();
868
+ setImmediate(() => {
869
+ updatePlaceholder();
870
+ screen.render();
871
+ });
888
872
  return;
889
873
  }
890
874
  if (key.name === 'escape') {
@@ -901,9 +885,9 @@ async function startTUI(options) {
901
885
  }
902
886
  // Check input content on next tick to see if we should show palette
903
887
  setImmediate(() => {
904
- const val = input.getValue();
888
+ const val = input.getValue() || '';
905
889
  updatePlaceholder();
906
- if (val && val.startsWith('/')) {
890
+ if (val.startsWith('/')) {
907
891
  toggleFileAutocomplete(false);
908
892
  fileSelectedIndex = 0;
909
893
  // Reset palette selection when filter changes
@@ -939,11 +923,9 @@ async function startTUI(options) {
939
923
  // Submit handler
940
924
  input.on('submit', async (value) => {
941
925
  const inputValue = value || input.getValue() || '';
942
- if (showPalette && inputValue.startsWith('/')) {
943
- const filter = inputValue.replace(/^\//, '').toLowerCase();
944
- const filteredCommands = COMMANDS.filter(c => c.name.startsWith(filter) || c.description.toLowerCase().includes(filter));
945
- if (filteredCommands[paletteSelectedIndex]) {
946
- const cmd = filteredCommands[paletteSelectedIndex];
926
+ if (showPalette && inputValue.startsWith('/') && filteredCommandsCache.length > 0) {
927
+ if (filteredCommandsCache[paletteSelectedIndex]) {
928
+ const cmd = filteredCommandsCache[paletteSelectedIndex];
947
929
  const needsArgs = ['mode', 'model', 'open', 'read', 'cat', 'close', 'do', 'run', 'ask', 'fix', 'exec', 'search'].includes(cmd.name);
948
930
  if (needsArgs && !inputValue.includes(' ')) {
949
931
  input.setValue('/' + cmd.name + ' ');
@@ -997,34 +979,37 @@ async function startTUI(options) {
997
979
  updatePlaceholder();
998
980
  screen.render();
999
981
  });
1000
- // SETTINGS MODAL
982
+ // SETTINGS MODAL - Completely rewritten for proper keyboard handling
1001
983
  function showSettingsMenu() {
1002
- let settingsActive = true;
984
+ // Disable main input while settings is open
985
+ input.hide();
1003
986
  let currentSettings = (0, settings_1.loadSettings)();
1004
987
  const models = settings_1.AVAILABLE_MODELS;
988
+ const modes = ['edit', 'auto', 'ask', 'debug', 'review', 'explain'];
1005
989
  let selectedIndex = 0;
1006
990
  const getListItems = () => [
1007
- `Model: ${currentSettings.model.current}`,
1008
- `Color: ${currentSettings.ui.color}`,
1009
- `Prompt: ${currentSettings.ui.promptStyle}`,
1010
- `Confirm: ${currentSettings.execution.confirmationMode}`,
1011
- `Shell Exec: ${currentSettings.execution.allowShellExec}`,
1012
- `Debug Log: ${currentSettings.debug.logging}`
991
+ `{cyan-fg}Model:{/cyan-fg} ${currentSettings.model.current}`,
992
+ `{cyan-fg}Default Mode:{/cyan-fg} ${currentSettings.execution.defaultMode || 'edit'}`,
993
+ `{cyan-fg}ASCII Logo:{/cyan-fg} ${currentSettings.ui.asciiLogo}`,
994
+ `{cyan-fg}Color:{/cyan-fg} ${currentSettings.ui.color}`,
995
+ `{cyan-fg}Prompt Style:{/cyan-fg} ${currentSettings.ui.promptStyle}`,
996
+ `{cyan-fg}Confirm Mode:{/cyan-fg} ${currentSettings.execution.confirmationMode}`,
997
+ `{cyan-fg}Shell Exec:{/cyan-fg} ${currentSettings.execution.allowShellExec}`,
998
+ `{cyan-fg}Debug Log:{/cyan-fg} ${currentSettings.debug.logging}`
1013
999
  ];
1014
1000
  const modal = blessed.box({
1015
1001
  parent: screen,
1016
1002
  top: 'center',
1017
1003
  left: 'center',
1018
- width: '50%',
1019
- height: '60%',
1004
+ width: 50,
1005
+ height: 16,
1020
1006
  border: { type: 'line' },
1021
- label: ' Settings ',
1007
+ label: ' Settings ',
1022
1008
  tags: true,
1023
1009
  style: {
1024
1010
  bg: 'black',
1025
1011
  fg: 'white',
1026
1012
  border: { fg: 'cyan' },
1027
- label: { fg: 'cyan', bold: true }
1028
1013
  },
1029
1014
  });
1030
1015
  const list = blessed.list({
@@ -1033,8 +1018,10 @@ async function startTUI(options) {
1033
1018
  left: 1,
1034
1019
  right: 1,
1035
1020
  bottom: 3,
1036
- keys: false, // We handle keys manually
1037
- mouse: false,
1021
+ tags: true,
1022
+ mouse: true,
1023
+ keys: true,
1024
+ vi: true,
1038
1025
  style: {
1039
1026
  selected: { bg: 'blue', fg: 'white', bold: true },
1040
1027
  item: { fg: 'white', bg: 'black' }
@@ -1045,10 +1032,12 @@ async function startTUI(options) {
1045
1032
  parent: modal,
1046
1033
  bottom: 1,
1047
1034
  left: 1,
1048
- content: '↑↓:navigate Enter:toggle Esc:save & exit',
1035
+ tags: true,
1036
+ content: '{gray-fg}↑↓{/gray-fg} navigate {gray-fg}Enter{/gray-fg} toggle {gray-fg}Esc{/gray-fg} save & exit',
1049
1037
  style: { fg: 'gray', bg: 'black' }
1050
1038
  });
1051
1039
  list.select(0);
1040
+ list.focus();
1052
1041
  screen.render();
1053
1042
  function cycleValue() {
1054
1043
  if (selectedIndex === 0) { // Model
@@ -1056,21 +1045,30 @@ async function startTUI(options) {
1056
1045
  const nextIdx = (currIdx + 1) % models.length;
1057
1046
  currentSettings.model.current = models[nextIdx];
1058
1047
  }
1059
- else if (selectedIndex === 1) { // Color
1048
+ else if (selectedIndex === 1) { // Default Mode
1049
+ const currMode = currentSettings.execution.defaultMode || 'edit';
1050
+ const currIdx = modes.indexOf(currMode);
1051
+ const nextIdx = (currIdx + 1) % modes.length;
1052
+ currentSettings.execution.defaultMode = modes[nextIdx];
1053
+ }
1054
+ else if (selectedIndex === 2) { // ASCII Logo
1055
+ currentSettings.ui.asciiLogo = currentSettings.ui.asciiLogo === 'on' ? 'off' : 'on';
1056
+ }
1057
+ else if (selectedIndex === 3) { // Color
1060
1058
  const vals = ['auto', 'on', 'off'];
1061
1059
  const n = (vals.indexOf(currentSettings.ui.color) + 1) % 3;
1062
1060
  currentSettings.ui.color = vals[n];
1063
1061
  }
1064
- else if (selectedIndex === 2) { // Prompt
1062
+ else if (selectedIndex === 4) { // Prompt Style
1065
1063
  currentSettings.ui.promptStyle = currentSettings.ui.promptStyle === 'compact' ? 'verbose' : 'compact';
1066
1064
  }
1067
- else if (selectedIndex === 3) { // Confirm
1065
+ else if (selectedIndex === 5) { // Confirm Mode
1068
1066
  currentSettings.execution.confirmationMode = currentSettings.execution.confirmationMode === 'strict' ? 'normal' : 'strict';
1069
1067
  }
1070
- else if (selectedIndex === 4) { // Shell
1068
+ else if (selectedIndex === 6) { // Shell Exec
1071
1069
  currentSettings.execution.allowShellExec = !currentSettings.execution.allowShellExec;
1072
1070
  }
1073
- else if (selectedIndex === 5) { // Debug
1071
+ else if (selectedIndex === 7) { // Debug Log
1074
1072
  currentSettings.debug.logging = !currentSettings.debug.logging;
1075
1073
  }
1076
1074
  list.setItems(getListItems());
@@ -1078,41 +1076,38 @@ async function startTUI(options) {
1078
1076
  screen.render();
1079
1077
  }
1080
1078
  function closeSettings() {
1081
- if (!settingsActive)
1082
- return;
1083
- settingsActive = false;
1084
1079
  (0, settings_1.saveSettings)(currentSettings);
1080
+ // Update header if logo setting changed
1081
+ updateHeader();
1085
1082
  updateStatusBar();
1083
+ updateModeIndicator();
1086
1084
  screen.remove(modal);
1085
+ input.show();
1087
1086
  input.focus();
1088
1087
  screen.render();
1089
- output.log('{green-fg}Settings saved.{/green-fg}');
1088
+ output.log('{green-fg}Settings saved{/green-fg}');
1090
1089
  }
1091
- // Handle keys on screen level while settings is open
1092
- const settingsKeyHandler = (ch, key) => {
1093
- if (!settingsActive)
1094
- return;
1095
- if (key.name === 'escape') {
1096
- closeSettings();
1097
- }
1098
- else if (key.name === 'up') {
1099
- selectedIndex = Math.max(0, selectedIndex - 1);
1100
- list.select(selectedIndex);
1101
- screen.render();
1102
- }
1103
- else if (key.name === 'down') {
1104
- selectedIndex = Math.min(getListItems().length - 1, selectedIndex + 1);
1105
- list.select(selectedIndex);
1106
- screen.render();
1107
- }
1108
- else if (key.name === 'enter' || key.name === 'return') {
1109
- cycleValue();
1110
- }
1111
- };
1112
- screen.on('keypress', settingsKeyHandler);
1113
- // Cleanup when modal is destroyed
1114
- modal.on('destroy', () => {
1115
- screen.removeListener('keypress', settingsKeyHandler);
1090
+ // List key handlers
1091
+ list.key(['up', 'k'], () => {
1092
+ selectedIndex = Math.max(0, selectedIndex - 1);
1093
+ list.select(selectedIndex);
1094
+ screen.render();
1095
+ });
1096
+ list.key(['down', 'j'], () => {
1097
+ selectedIndex = Math.min(getListItems().length - 1, selectedIndex + 1);
1098
+ list.select(selectedIndex);
1099
+ screen.render();
1100
+ });
1101
+ list.key(['enter', 'space'], () => {
1102
+ cycleValue();
1103
+ });
1104
+ list.key(['escape', 'q'], () => {
1105
+ closeSettings();
1106
+ });
1107
+ // Mouse selection
1108
+ list.on('select', (item, index) => {
1109
+ selectedIndex = index;
1110
+ cycleValue();
1116
1111
  });
1117
1112
  }
1118
1113
  // Mode cycling with Shift+Tab
@@ -1132,13 +1127,16 @@ async function startTUI(options) {
1132
1127
  updatePlaceholder();
1133
1128
  screen.render();
1134
1129
  }
1135
- // Shift+Tab to cycle modes
1136
- screen.key(['S-tab'], () => {
1130
+ // Shift+Tab to cycle modes - try multiple key names for compatibility
1131
+ screen.key(['S-tab', 'shift-tab'], () => {
1132
+ cycleMode(1);
1133
+ });
1134
+ // Also bind to backtick as alternative mode switcher
1135
+ screen.key(['`'], () => {
1137
1136
  cycleMode(1);
1138
1137
  });
1139
1138
  // Global Key Bindings
1140
1139
  screen.key(['C-c'], () => {
1141
- stopMascotAnimation();
1142
1140
  onExit?.();
1143
1141
  return process.exit(0);
1144
1142
  });
@@ -1213,7 +1211,6 @@ async function startTUI(options) {
1213
1211
  screen.key(['q'], () => {
1214
1212
  // Only quit if not focused on input
1215
1213
  if (screen.focused !== input) {
1216
- stopMascotAnimation();
1217
1214
  onExit?.();
1218
1215
  return process.exit(0);
1219
1216
  }
@@ -1228,7 +1225,7 @@ async function startTUI(options) {
1228
1225
  });
1229
1226
  // Cleanup on exit
1230
1227
  process.on('exit', () => {
1231
- stopMascotAnimation();
1228
+ // Cleanup if needed
1232
1229
  });
1233
1230
  screen.render();
1234
1231
  }