wave-code 0.8.4 → 0.9.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 (54) hide show
  1. package/dist/components/ChatInterface.d.ts.map +1 -1
  2. package/dist/components/ChatInterface.js +26 -7
  3. package/dist/components/FileSelector.d.ts +1 -0
  4. package/dist/components/FileSelector.d.ts.map +1 -1
  5. package/dist/components/FileSelector.js +3 -3
  6. package/dist/components/HelpView.d.ts.map +1 -1
  7. package/dist/components/HelpView.js +1 -0
  8. package/dist/components/HistorySearch.d.ts +2 -1
  9. package/dist/components/HistorySearch.d.ts.map +1 -1
  10. package/dist/components/HistorySearch.js +1 -1
  11. package/dist/components/InputBox.d.ts.map +1 -1
  12. package/dist/components/InputBox.js +8 -6
  13. package/dist/components/MessageBlockItem.d.ts.map +1 -1
  14. package/dist/components/MessageBlockItem.js +1 -1
  15. package/dist/components/MessageList.d.ts +2 -1
  16. package/dist/components/MessageList.d.ts.map +1 -1
  17. package/dist/components/MessageList.js +14 -4
  18. package/dist/components/QueuedMessageList.d.ts +3 -0
  19. package/dist/components/QueuedMessageList.d.ts.map +1 -0
  20. package/dist/components/QueuedMessageList.js +17 -0
  21. package/dist/components/ReasoningDisplay.d.ts +1 -0
  22. package/dist/components/ReasoningDisplay.d.ts.map +1 -1
  23. package/dist/components/ReasoningDisplay.js +3 -3
  24. package/dist/components/ToolDisplay.js +1 -1
  25. package/dist/contexts/useChat.d.ts +7 -0
  26. package/dist/contexts/useChat.d.ts.map +1 -1
  27. package/dist/contexts/useChat.js +17 -1
  28. package/dist/hooks/useInputManager.d.ts +6 -5
  29. package/dist/hooks/useInputManager.d.ts.map +1 -1
  30. package/dist/hooks/useInputManager.js +18 -16
  31. package/dist/managers/inputHandlers.d.ts +7 -4
  32. package/dist/managers/inputHandlers.d.ts.map +1 -1
  33. package/dist/managers/inputHandlers.js +184 -46
  34. package/dist/managers/inputReducer.d.ts +18 -2
  35. package/dist/managers/inputReducer.d.ts.map +1 -1
  36. package/dist/managers/inputReducer.js +92 -3
  37. package/dist/utils/logger.d.ts.map +1 -1
  38. package/dist/utils/logger.js +13 -1
  39. package/package.json +2 -2
  40. package/src/components/ChatInterface.tsx +42 -15
  41. package/src/components/FileSelector.tsx +13 -3
  42. package/src/components/HelpView.tsx +1 -0
  43. package/src/components/HistorySearch.tsx +2 -2
  44. package/src/components/InputBox.tsx +21 -17
  45. package/src/components/MessageBlockItem.tsx +8 -3
  46. package/src/components/MessageList.tsx +16 -3
  47. package/src/components/QueuedMessageList.tsx +31 -0
  48. package/src/components/ReasoningDisplay.tsx +8 -2
  49. package/src/components/ToolDisplay.tsx +2 -2
  50. package/src/contexts/useChat.tsx +29 -1
  51. package/src/hooks/useInputManager.ts +22 -31
  52. package/src/managers/inputHandlers.ts +223 -60
  53. package/src/managers/inputReducer.ts +104 -6
  54. package/src/utils/logger.ts +15 -1
@@ -30,18 +30,12 @@ export const handleSubmit = async (
30
30
  state: InputState,
31
31
  dispatch: React.Dispatch<InputAction>,
32
32
  callbacks: Partial<InputManagerCallbacks>,
33
- isLoading: boolean = false,
34
- isCommandRunning: boolean = false,
35
33
  attachedImagesOverride?: Array<{
36
34
  id: number;
37
35
  path: string;
38
36
  mimeType: string;
39
37
  }>,
40
38
  ): Promise<void> => {
41
- if (isLoading || isCommandRunning) {
42
- return;
43
- }
44
-
45
39
  if (state.inputText.trim()) {
46
40
  const imageRegex = /\[Image #(\d+)\]/g;
47
41
  const matches = [...state.inputText.matchAll(imageRegex)];
@@ -60,7 +54,11 @@ export const handleSubmit = async (
60
54
  let cleanContent = state.inputText.replace(imageRegex, "").trim();
61
55
  cleanContent = expandLongTextPlaceholders(cleanContent, state.longTextMap);
62
56
 
63
- PromptHistoryManager.addEntry(cleanContent).catch((err: unknown) => {
57
+ PromptHistoryManager.addEntry(
58
+ cleanContent,
59
+ callbacks.sessionId,
60
+ state.longTextMap,
61
+ ).catch((err: unknown) => {
64
62
  callbacks.logger?.error("Failed to save prompt history", err);
65
63
  });
66
64
 
@@ -69,7 +67,7 @@ export const handleSubmit = async (
69
67
  referencedImages.length > 0 ? referencedImages : undefined,
70
68
  );
71
69
  dispatch({ type: "CLEAR_INPUT" });
72
- callbacks.onResetHistoryNavigation?.();
70
+ dispatch({ type: "RESET_HISTORY_NAVIGATION" });
73
71
  dispatch({ type: "CLEAR_LONG_TEXT_MAP" });
74
72
  }
75
73
  };
@@ -114,6 +112,72 @@ export const cyclePermissionMode = (
114
112
  callbacks.onPermissionModeChange?.(nextMode);
115
113
  };
116
114
 
115
+ const SELECTOR_TRIGGERS = [
116
+ {
117
+ char: "@",
118
+ type: "ACTIVATE_FILE_SELECTOR",
119
+ shouldActivate: (char: string, pos: number, text: string) =>
120
+ char === "@" && (pos === 1 || /\s/.test(text[pos - 2])),
121
+ },
122
+ {
123
+ char: "/",
124
+ type: "ACTIVATE_COMMAND_SELECTOR",
125
+ shouldActivate: (
126
+ char: string,
127
+ pos: number,
128
+ text: string,
129
+ state: InputState,
130
+ ) =>
131
+ char === "/" &&
132
+ !state.showFileSelector &&
133
+ (pos === 1 || /\s/.test(text[pos - 2])),
134
+ },
135
+ ] as const;
136
+
137
+ const getProjectedState = (state: InputState, char: string) => {
138
+ const beforeCursor = state.inputText.substring(0, state.cursorPosition);
139
+ const afterCursor = state.inputText.substring(state.cursorPosition);
140
+ const newInputText = beforeCursor + char + afterCursor;
141
+ const newCursorPosition = state.cursorPosition + char.length;
142
+ return { newInputText, newCursorPosition };
143
+ };
144
+
145
+ export const getAtSelectorPosition = (
146
+ text: string,
147
+ cursorPosition: number,
148
+ ): number => {
149
+ let i = cursorPosition - 1;
150
+ while (i >= 0 && !/\s/.test(text[i])) {
151
+ if (text[i] === "@") {
152
+ // Check if this @ is at the start or preceded by whitespace
153
+ if (i === 0 || /\s/.test(text[i - 1])) {
154
+ return i;
155
+ }
156
+ break;
157
+ }
158
+ i--;
159
+ }
160
+ return -1;
161
+ };
162
+
163
+ export const getSlashSelectorPosition = (
164
+ text: string,
165
+ cursorPosition: number,
166
+ ): number => {
167
+ let i = cursorPosition - 1;
168
+ while (i >= 0 && !/\s/.test(text[i])) {
169
+ if (text[i] === "/") {
170
+ // Check if this / is at the start or preceded by whitespace
171
+ if (i === 0 || /\s/.test(text[i - 1])) {
172
+ return i;
173
+ }
174
+ break;
175
+ }
176
+ i--;
177
+ }
178
+ return -1;
179
+ };
180
+
117
181
  export const updateSearchQueriesForActiveSelectors = (
118
182
  state: InputState,
119
183
  dispatch: React.Dispatch<InputAction>,
@@ -133,26 +197,58 @@ export const updateSearchQueriesForActiveSelectors = (
133
197
  }
134
198
  };
135
199
 
136
- export const handleSpecialCharInput = (
200
+ export const processSelectorInput = (
137
201
  state: InputState,
138
202
  dispatch: React.Dispatch<InputAction>,
139
203
  char: string,
140
- cursorPosition: number,
141
- inputText: string,
142
204
  ): void => {
143
- if (char === "@") {
144
- dispatch({ type: "ACTIVATE_FILE_SELECTOR", payload: cursorPosition - 1 });
145
- } else if (char === "/" && !state.showFileSelector && cursorPosition === 1) {
205
+ const { newInputText, newCursorPosition } = getProjectedState(state, char);
206
+
207
+ const trigger = SELECTOR_TRIGGERS.find((t) =>
208
+ t.shouldActivate(char, newCursorPosition, newInputText, state),
209
+ );
210
+
211
+ if (trigger) {
146
212
  dispatch({
147
- type: "ACTIVATE_COMMAND_SELECTOR",
148
- payload: cursorPosition - 1,
149
- });
213
+ type: trigger.type,
214
+ payload: newCursorPosition - 1,
215
+ } as InputAction);
150
216
  } else {
217
+ const atPos = getAtSelectorPosition(newInputText, newCursorPosition);
218
+ let showFileSelector = state.showFileSelector;
219
+ let atPosition = state.atPosition;
220
+ if (atPos !== -1 && !state.showFileSelector) {
221
+ dispatch({
222
+ type: "ACTIVATE_FILE_SELECTOR",
223
+ payload: atPos,
224
+ });
225
+ showFileSelector = true;
226
+ atPosition = atPos;
227
+ }
228
+
229
+ const slashPos = getSlashSelectorPosition(newInputText, newCursorPosition);
230
+ let showCommandSelector = state.showCommandSelector;
231
+ let slashPosition = state.slashPosition;
232
+ if (slashPos !== -1 && !state.showCommandSelector) {
233
+ dispatch({
234
+ type: "ACTIVATE_COMMAND_SELECTOR",
235
+ payload: slashPos,
236
+ });
237
+ showCommandSelector = true;
238
+ slashPosition = slashPos;
239
+ }
240
+
151
241
  updateSearchQueriesForActiveSelectors(
152
- state,
242
+ {
243
+ ...state,
244
+ showFileSelector,
245
+ atPosition,
246
+ showCommandSelector,
247
+ slashPosition,
248
+ },
153
249
  dispatch,
154
- inputText,
155
- cursorPosition,
250
+ newInputText,
251
+ newCursorPosition,
156
252
  );
157
253
  }
158
254
  };
@@ -184,23 +280,19 @@ export const handlePasteInput = (
184
280
  char = "!";
185
281
  }
186
282
 
187
- callbacks.onResetHistoryNavigation?.();
283
+ dispatch({ type: "RESET_HISTORY_NAVIGATION" });
188
284
  dispatch({ type: "INSERT_TEXT", payload: char });
189
285
 
190
- // Calculate new state for special char handling
191
- const newCursorPosition = state.cursorPosition + char.length;
192
- const beforeCursor = state.inputText.substring(0, state.cursorPosition);
193
- const afterCursor = state.inputText.substring(state.cursorPosition);
194
- const newInputText = beforeCursor + char + afterCursor;
286
+ processSelectorInput(state, dispatch, char);
287
+ }
288
+ };
195
289
 
196
- handleSpecialCharInput(
197
- state,
198
- dispatch,
199
- char,
200
- newCursorPosition,
201
- newInputText,
202
- );
290
+ export const getWordEnd = (text: string, startPos: number): number => {
291
+ let i = startPos;
292
+ while (i < text.length && !/\s/.test(text[i])) {
293
+ i++;
203
294
  }
295
+ return i;
204
296
  };
205
297
 
206
298
  export const handleCommandSelect = (
@@ -210,9 +302,10 @@ export const handleCommandSelect = (
210
302
  command: string,
211
303
  ) => {
212
304
  if (state.slashPosition >= 0) {
305
+ const wordEnd = getWordEnd(state.inputText, state.slashPosition);
213
306
  const beforeSlash = state.inputText.substring(0, state.slashPosition);
214
- const afterQuery = state.inputText.substring(state.cursorPosition);
215
- const newInput = beforeSlash + afterQuery;
307
+ const afterWord = state.inputText.substring(wordEnd);
308
+ const newInput = beforeSlash + afterWord;
216
309
  const newCursorPosition = beforeSlash.length;
217
310
 
218
311
  dispatch({ type: "SET_INPUT_TEXT", payload: newInput });
@@ -264,10 +357,11 @@ export const handleFileSelect = (
264
357
  filePath: string,
265
358
  ) => {
266
359
  if (state.atPosition >= 0) {
360
+ const wordEnd = getWordEnd(state.inputText, state.atPosition);
267
361
  const beforeAt = state.inputText.substring(0, state.atPosition);
268
- const afterQuery = state.inputText.substring(state.cursorPosition);
269
- const newInput = beforeAt + `${filePath} ` + afterQuery;
270
- const newCursorPosition = beforeAt.length + filePath.length + 1;
362
+ const afterWord = state.inputText.substring(wordEnd);
363
+ const newInput = beforeAt + `@${filePath} ` + afterWord;
364
+ const newCursorPosition = beforeAt.length + filePath.length + 2;
271
365
 
272
366
  dispatch({ type: "SET_INPUT_TEXT", payload: newInput });
273
367
  dispatch({ type: "SET_CURSOR_POSITION", payload: newCursorPosition });
@@ -343,6 +437,26 @@ export const handleSelectorInput = (
343
437
  return true;
344
438
  }
345
439
 
440
+ if (key.leftArrow) {
441
+ const newCursorPosition = state.cursorPosition - 1;
442
+ dispatch({ type: "MOVE_CURSOR", payload: -1 });
443
+ checkForAtDeletion(state, dispatch, newCursorPosition);
444
+ checkForSlashDeletion(state, dispatch, newCursorPosition);
445
+ return true;
446
+ }
447
+
448
+ if (key.rightArrow) {
449
+ const newCursorPosition = state.cursorPosition + 1;
450
+ dispatch({ type: "MOVE_CURSOR", payload: 1 });
451
+ checkForAtDeletion(state, dispatch, newCursorPosition);
452
+ checkForSlashDeletion(state, dispatch, newCursorPosition);
453
+ return true;
454
+ }
455
+
456
+ if (input === " " && state.showFileSelector) {
457
+ dispatch({ type: "CANCEL_FILE_SELECTOR" });
458
+ }
459
+
346
460
  if (
347
461
  input &&
348
462
  !key.ctrl &&
@@ -358,19 +472,7 @@ export const handleSelectorInput = (
358
472
  ) {
359
473
  dispatch({ type: "INSERT_TEXT", payload: input });
360
474
 
361
- // Calculate new state for special char handling
362
- const newCursorPosition = state.cursorPosition + input.length;
363
- const beforeCursor = state.inputText.substring(0, state.cursorPosition);
364
- const afterCursor = state.inputText.substring(state.cursorPosition);
365
- const newInputText = beforeCursor + input + afterCursor;
366
-
367
- handleSpecialCharInput(
368
- state,
369
- dispatch,
370
- input,
371
- newCursorPosition,
372
- newInputText,
373
- );
475
+ processSelectorInput(state, dispatch, input);
374
476
  return true;
375
477
  }
376
478
 
@@ -383,12 +485,10 @@ export const handleNormalInput = async (
383
485
  callbacks: Partial<InputManagerCallbacks>,
384
486
  input: string,
385
487
  key: Key,
386
- isLoading: boolean = false,
387
- isCommandRunning: boolean = false,
388
488
  clearImages?: () => void,
389
489
  ): Promise<boolean> => {
390
490
  if (key.return) {
391
- await handleSubmit(state, dispatch, callbacks, isLoading, isCommandRunning);
491
+ await handleSubmit(state, dispatch, callbacks);
392
492
  clearImages?.();
393
493
  return true;
394
494
  }
@@ -398,18 +498,86 @@ export const handleNormalInput = async (
398
498
  dispatch({ type: "CANCEL_FILE_SELECTOR" });
399
499
  } else if (state.showCommandSelector) {
400
500
  dispatch({ type: "CANCEL_COMMAND_SELECTOR" });
501
+ } else if (state.historyIndex !== -1) {
502
+ dispatch({ type: "RESET_HISTORY_NAVIGATION" });
503
+ }
504
+ return true;
505
+ }
506
+
507
+ if (key.upArrow) {
508
+ if (state.history.length === 0) {
509
+ const history = await PromptHistoryManager.getHistory(
510
+ callbacks.sessionId,
511
+ );
512
+ dispatch({ type: "SET_HISTORY_ENTRIES", payload: history });
401
513
  }
514
+ dispatch({ type: "NAVIGATE_HISTORY", payload: "up" });
515
+ return true;
516
+ }
517
+
518
+ if (key.downArrow) {
519
+ dispatch({ type: "NAVIGATE_HISTORY", payload: "down" });
402
520
  return true;
403
521
  }
404
522
 
405
523
  if (key.backspace || key.delete) {
406
524
  if (state.cursorPosition > 0) {
407
525
  const newCursorPosition = state.cursorPosition - 1;
526
+ const beforeCursor = state.inputText.substring(
527
+ 0,
528
+ state.cursorPosition - 1,
529
+ );
530
+ const afterCursor = state.inputText.substring(state.cursorPosition);
531
+ const newInputText = beforeCursor + afterCursor;
532
+
408
533
  dispatch({ type: "DELETE_CHAR" });
409
- callbacks.onResetHistoryNavigation?.();
534
+ dispatch({ type: "RESET_HISTORY_NAVIGATION" });
410
535
 
411
536
  checkForAtDeletion(state, dispatch, newCursorPosition);
412
537
  checkForSlashDeletion(state, dispatch, newCursorPosition);
538
+
539
+ // Reactivate file selector if cursor is now within an @word
540
+ const atPos = getAtSelectorPosition(newInputText, newCursorPosition);
541
+ let showFileSelector = state.showFileSelector;
542
+ let atPosition = state.atPosition;
543
+ if (atPos !== -1 && !state.showFileSelector) {
544
+ dispatch({
545
+ type: "ACTIVATE_FILE_SELECTOR",
546
+ payload: atPos,
547
+ });
548
+ showFileSelector = true;
549
+ atPosition = atPos;
550
+ }
551
+
552
+ const slashPos = getSlashSelectorPosition(
553
+ newInputText,
554
+ newCursorPosition,
555
+ );
556
+ let showCommandSelector = state.showCommandSelector;
557
+ let slashPosition = state.slashPosition;
558
+ if (slashPos !== -1 && !state.showCommandSelector) {
559
+ dispatch({
560
+ type: "ACTIVATE_COMMAND_SELECTOR",
561
+ payload: slashPos,
562
+ });
563
+ showCommandSelector = true;
564
+ slashPosition = slashPos;
565
+ }
566
+
567
+ updateSearchQueriesForActiveSelectors(
568
+ {
569
+ ...state,
570
+ inputText: newInputText,
571
+ cursorPosition: newCursorPosition,
572
+ showFileSelector,
573
+ atPosition,
574
+ showCommandSelector,
575
+ slashPosition,
576
+ },
577
+ dispatch,
578
+ newInputText,
579
+ newCursorPosition,
580
+ );
413
581
  }
414
582
  return true;
415
583
  }
@@ -468,8 +636,6 @@ export const handleInput = async (
468
636
  callbacks: Partial<InputManagerCallbacks>,
469
637
  input: string,
470
638
  key: Key,
471
- isLoading: boolean = false,
472
- isCommandRunning: boolean = false,
473
639
  clearImages?: () => void,
474
640
  ): Promise<boolean> => {
475
641
  if (state.selectorJustUsed) {
@@ -478,7 +644,6 @@ export const handleInput = async (
478
644
 
479
645
  if (key.escape) {
480
646
  if (
481
- (isLoading || isCommandRunning) &&
482
647
  !(
483
648
  state.showFileSelector ||
484
649
  state.showCommandSelector ||
@@ -552,8 +717,6 @@ export const handleInput = async (
552
717
  callbacks,
553
718
  input,
554
719
  key,
555
- isLoading,
556
- isCommandRunning,
557
720
  clearImages,
558
721
  );
559
722
  }
@@ -1,4 +1,4 @@
1
- import { FileItem, PermissionMode, Logger } from "wave-agent-sdk";
1
+ import { FileItem, PermissionMode, Logger, PromptEntry } from "wave-agent-sdk";
2
2
 
3
3
  export interface AttachedImage {
4
4
  id: number;
@@ -35,8 +35,8 @@ export interface InputManagerCallbacks {
35
35
  onAbortMessage?: () => void;
36
36
  onClearMessages?: () => void;
37
37
  onBackgroundCurrentTask?: () => void;
38
- onResetHistoryNavigation?: () => void;
39
38
  onPermissionModeChange?: (mode: PermissionMode) => void;
39
+ sessionId?: string;
40
40
  logger?: Logger;
41
41
  }
42
42
 
@@ -66,6 +66,11 @@ export interface InputState {
66
66
  isPasting: boolean;
67
67
  pasteBuffer: string;
68
68
  initialPasteCursorPosition: number;
69
+ history: PromptEntry[];
70
+ historyIndex: number;
71
+ originalInputText: string;
72
+ originalLongTextMap: Record<string, string>;
73
+ isFileSearching: boolean;
69
74
  }
70
75
 
71
76
  export const initialState: InputState = {
@@ -94,6 +99,11 @@ export const initialState: InputState = {
94
99
  isPasting: false,
95
100
  pasteBuffer: "",
96
101
  initialPasteCursorPosition: 0,
102
+ history: [],
103
+ historyIndex: -1,
104
+ originalInputText: "",
105
+ originalLongTextMap: {},
106
+ isFileSearching: false,
97
107
  };
98
108
 
99
109
  export type InputAction =
@@ -131,7 +141,11 @@ export type InputAction =
131
141
  | {
132
142
  type: "ADD_IMAGE_AND_INSERT_PLACEHOLDER";
133
143
  payload: { path: string; mimeType: string };
134
- };
144
+ }
145
+ | { type: "SET_HISTORY_ENTRIES"; payload: PromptEntry[] }
146
+ | { type: "NAVIGATE_HISTORY"; payload: "up" | "down" }
147
+ | { type: "RESET_HISTORY_NAVIGATION" }
148
+ | { type: "SELECT_HISTORY_ENTRY"; payload: PromptEntry };
135
149
 
136
150
  export function inputReducer(
137
151
  state: InputState,
@@ -139,7 +153,11 @@ export function inputReducer(
139
153
  ): InputState {
140
154
  switch (action.type) {
141
155
  case "SET_INPUT_TEXT":
142
- return { ...state, inputText: action.payload };
156
+ return {
157
+ ...state,
158
+ inputText: action.payload,
159
+ historyIndex: -1,
160
+ };
143
161
  case "SET_CURSOR_POSITION":
144
162
  return {
145
163
  ...state,
@@ -157,6 +175,7 @@ export function inputReducer(
157
175
  ...state,
158
176
  inputText: newText,
159
177
  cursorPosition: newCursorPosition,
178
+ historyIndex: -1,
160
179
  };
161
180
  }
162
181
  case "DELETE_CHAR": {
@@ -172,6 +191,7 @@ export function inputReducer(
172
191
  ...state,
173
192
  inputText: newText,
174
193
  cursorPosition: newCursorPosition,
194
+ historyIndex: -1,
175
195
  };
176
196
  }
177
197
  return state;
@@ -190,11 +210,20 @@ export function inputReducer(
190
210
  atPosition: action.payload,
191
211
  fileSearchQuery: "",
192
212
  filteredFiles: [],
213
+ isFileSearching: true,
193
214
  };
194
215
  case "SET_FILE_SEARCH_QUERY":
195
- return { ...state, fileSearchQuery: action.payload };
216
+ return {
217
+ ...state,
218
+ fileSearchQuery: action.payload,
219
+ isFileSearching: true,
220
+ };
196
221
  case "SET_FILTERED_FILES":
197
- return { ...state, filteredFiles: action.payload };
222
+ return {
223
+ ...state,
224
+ filteredFiles: action.payload,
225
+ isFileSearching: false,
226
+ };
198
227
  case "CANCEL_FILE_SELECTOR":
199
228
  return {
200
229
  ...state,
@@ -203,6 +232,7 @@ export function inputReducer(
203
232
  fileSearchQuery: "",
204
233
  filteredFiles: [],
205
234
  selectorJustUsed: true,
235
+ isFileSearching: false,
206
236
  };
207
237
  case "ACTIVATE_COMMAND_SELECTOR":
208
238
  return {
@@ -314,6 +344,7 @@ export function inputReducer(
314
344
  cursorPosition: newCursorPosition,
315
345
  longTextCounter: newLongTextCounter,
316
346
  longTextMap: newLongTextMap,
347
+ historyIndex: -1,
317
348
  };
318
349
  }
319
350
  case "CLEAR_LONG_TEXT_MAP":
@@ -323,6 +354,7 @@ export function inputReducer(
323
354
  ...state,
324
355
  inputText: "",
325
356
  cursorPosition: 0,
357
+ historyIndex: -1,
326
358
  };
327
359
  case "START_PASTE":
328
360
  return {
@@ -359,8 +391,74 @@ export function inputReducer(
359
391
  imageIdCounter: state.imageIdCounter + 1,
360
392
  inputText: newText,
361
393
  cursorPosition: newCursorPosition,
394
+ historyIndex: -1,
362
395
  };
363
396
  }
397
+ case "SET_HISTORY_ENTRIES":
398
+ return { ...state, history: action.payload };
399
+ case "NAVIGATE_HISTORY": {
400
+ const direction = action.payload;
401
+ let newIndex = state.historyIndex;
402
+ let newOriginalInputText = state.originalInputText;
403
+ let newOriginalLongTextMap = state.originalLongTextMap;
404
+
405
+ if (direction === "up") {
406
+ if (newIndex === -1) {
407
+ newOriginalInputText = state.inputText;
408
+ newOriginalLongTextMap = state.longTextMap;
409
+ }
410
+ newIndex = Math.min(state.history.length - 1, newIndex + 1);
411
+ } else {
412
+ newIndex = Math.max(-1, newIndex - 1);
413
+ }
414
+
415
+ if (newIndex === -1) {
416
+ return {
417
+ ...state,
418
+ historyIndex: newIndex,
419
+ inputText: newOriginalInputText,
420
+ longTextMap: newOriginalLongTextMap,
421
+ cursorPosition: newOriginalInputText.length,
422
+ originalInputText: newOriginalInputText,
423
+ originalLongTextMap: newOriginalLongTextMap,
424
+ };
425
+ } else {
426
+ const entry = state.history[newIndex];
427
+ return {
428
+ ...state,
429
+ historyIndex: newIndex,
430
+ inputText: entry.prompt,
431
+ longTextMap: entry.longTextMap || {},
432
+ cursorPosition: entry.prompt.length,
433
+ originalInputText: newOriginalInputText,
434
+ originalLongTextMap: newOriginalLongTextMap,
435
+ };
436
+ }
437
+ }
438
+ case "SELECT_HISTORY_ENTRY": {
439
+ const entry = action.payload;
440
+ return {
441
+ ...state,
442
+ inputText: entry.prompt,
443
+ longTextMap: entry.longTextMap || {},
444
+ cursorPosition: entry.prompt.length,
445
+ historyIndex: -1,
446
+ history: [],
447
+ originalInputText: "",
448
+ originalLongTextMap: {},
449
+ showHistorySearch: false,
450
+ historySearchQuery: "",
451
+ selectorJustUsed: true,
452
+ };
453
+ }
454
+ case "RESET_HISTORY_NAVIGATION":
455
+ return {
456
+ ...state,
457
+ historyIndex: -1,
458
+ history: [],
459
+ originalInputText: "",
460
+ originalLongTextMap: {},
461
+ };
364
462
  default:
365
463
  return state;
366
464
  }
@@ -9,8 +9,11 @@
9
9
  */
10
10
 
11
11
  import * as fs from "fs";
12
+ import { Chalk } from "chalk";
12
13
  import { LOG_FILE, DATA_DIRECTORY } from "./constants.js";
13
14
 
15
+ const chalk = new Chalk({ level: 3 });
16
+
14
17
  const logFile = process.env.LOG_FILE || LOG_FILE;
15
18
 
16
19
  /**
@@ -33,6 +36,16 @@ const LOG_LEVEL_NAMES = {
33
36
  [LogLevel.ERROR]: "ERROR",
34
37
  };
35
38
 
39
+ /**
40
+ * Log level color mapping
41
+ */
42
+ const LEVEL_COLORS = {
43
+ [LogLevel.DEBUG]: chalk.gray,
44
+ [LogLevel.INFO]: chalk.blue,
45
+ [LogLevel.WARN]: chalk.yellow,
46
+ [LogLevel.ERROR]: chalk.red,
47
+ };
48
+
36
49
  /**
37
50
  * Log configuration interface
38
51
  */
@@ -146,7 +159,8 @@ const logMessage = (level: LogLevel, ...args: unknown[]): void => {
146
159
 
147
160
  const levelName = LOG_LEVEL_NAMES[level];
148
161
  const timestamp = new Date().toISOString();
149
- const formattedMessage = `[${timestamp}] [${levelName}] ${messageText}\n`;
162
+ const color = LEVEL_COLORS[level] || ((s: string) => s);
163
+ const formattedMessage = `[${chalk.gray(timestamp)}] [${color(levelName)}] ${messageText}\n`;
150
164
 
151
165
  try {
152
166
  // Ensure directory exists