wave-code 0.19.9 → 1.0.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 (83) hide show
  1. package/dist/cli.js +20 -1
  2. package/dist/components/App.js +7 -0
  3. package/dist/components/BtwDisplay.js +13 -3
  4. package/dist/components/ChatInterface.js +21 -6
  5. package/dist/components/HelpView.js +6 -0
  6. package/dist/components/InputBox.d.ts +1 -2
  7. package/dist/components/InputBox.js +16 -5
  8. package/dist/components/LoadingIndicator.d.ts +1 -2
  9. package/dist/components/LoadingIndicator.js +2 -2
  10. package/dist/components/Markdown.js +13 -16
  11. package/dist/components/MessageList.d.ts +2 -1
  12. package/dist/components/MessageList.js +2 -2
  13. package/dist/components/RewindCommand.js +4 -2
  14. package/dist/components/StatusLine.d.ts +0 -2
  15. package/dist/components/StatusLine.js +6 -6
  16. package/dist/components/TaskList.js +2 -1
  17. package/dist/components/ToolDisplay.d.ts +1 -0
  18. package/dist/components/ToolDisplay.js +17 -9
  19. package/dist/constants/commands.js +0 -6
  20. package/dist/contexts/useChat.d.ts +3 -5
  21. package/dist/contexts/useChat.js +242 -82
  22. package/dist/daemon-cli.d.ts +10 -0
  23. package/dist/daemon-cli.js +15 -0
  24. package/dist/hooks/useInputManager.d.ts +1 -0
  25. package/dist/hooks/useInputManager.js +120 -40
  26. package/dist/index.js +10 -0
  27. package/dist/managers/inputHandlers.js +55 -30
  28. package/dist/managers/inputReducer.d.ts +22 -22
  29. package/dist/managers/inputReducer.js +361 -177
  30. package/dist/print-cli.js +36 -10
  31. package/dist/stdio/agentBridge.d.ts +23 -0
  32. package/dist/stdio/agentBridge.js +151 -18
  33. package/dist/stdio/daemonServer.d.ts +67 -0
  34. package/dist/stdio/daemonServer.js +191 -0
  35. package/dist/stdio/index.d.ts +2 -0
  36. package/dist/stdio/index.js +2 -0
  37. package/dist/stdio/jsonRpcConnection.d.ts +30 -0
  38. package/dist/stdio/jsonRpcConnection.js +127 -0
  39. package/dist/stdio/protocol.d.ts +2 -2
  40. package/dist/stdio/stdioServer.d.ts +2 -7
  41. package/dist/stdio/stdioServer.js +9 -100
  42. package/dist/utils/bracketedPaste.d.ts +39 -0
  43. package/dist/utils/bracketedPaste.js +122 -0
  44. package/dist/utils/markdownTable.d.ts +34 -0
  45. package/dist/utils/markdownTable.js +302 -0
  46. package/dist/utils/rewindCheckpoints.d.ts +8 -0
  47. package/dist/utils/rewindCheckpoints.js +15 -0
  48. package/dist/utils/throttle.d.ts +3 -3
  49. package/dist/utils/worktree.d.ts +8 -0
  50. package/dist/utils/worktree.js +32 -1
  51. package/package.json +4 -2
  52. package/src/cli.tsx +20 -1
  53. package/src/components/App.tsx +5 -0
  54. package/src/components/BtwDisplay.tsx +36 -12
  55. package/src/components/ChatInterface.tsx +26 -11
  56. package/src/components/HelpView.tsx +6 -0
  57. package/src/components/InputBox.tsx +21 -10
  58. package/src/components/LoadingIndicator.tsx +1 -4
  59. package/src/components/Markdown.tsx +15 -18
  60. package/src/components/MessageList.tsx +6 -0
  61. package/src/components/RewindCommand.tsx +4 -2
  62. package/src/components/StatusLine.tsx +0 -10
  63. package/src/components/TaskList.tsx +2 -1
  64. package/src/components/ToolDisplay.tsx +17 -6
  65. package/src/constants/commands.ts +0 -6
  66. package/src/contexts/useChat.tsx +310 -95
  67. package/src/daemon-cli.ts +17 -0
  68. package/src/hooks/useInputManager.ts +135 -43
  69. package/src/index.ts +12 -0
  70. package/src/managers/inputHandlers.ts +55 -32
  71. package/src/managers/inputReducer.ts +442 -214
  72. package/src/print-cli.ts +48 -11
  73. package/src/stdio/agentBridge.ts +213 -18
  74. package/src/stdio/daemonServer.ts +212 -0
  75. package/src/stdio/index.ts +2 -0
  76. package/src/stdio/jsonRpcConnection.ts +160 -0
  77. package/src/stdio/protocol.ts +5 -2
  78. package/src/stdio/stdioServer.ts +14 -120
  79. package/src/utils/bracketedPaste.ts +170 -0
  80. package/src/utils/markdownTable.ts +359 -0
  81. package/src/utils/rewindCheckpoints.ts +15 -0
  82. package/src/utils/throttle.ts +8 -8
  83. package/src/utils/worktree.ts +50 -1
@@ -1,10 +1,13 @@
1
- import { useEffect, useReducer, useCallback } from "react";
1
+ import { useEffect, useReducer, useCallback, useRef } from "react";
2
2
  import { Key } from "ink";
3
3
  import {
4
4
  inputReducer,
5
5
  initialState,
6
6
  InputManagerCallbacks,
7
+ ESC_DOUBLE_PRESS_TIMEOUT_MS,
8
+ btwOverlayActiveRef,
7
9
  } from "../managers/inputReducer.js";
10
+ import { createBracketedPasteDetector } from "../utils/bracketedPaste.js";
8
11
  import {
9
12
  searchFiles as searchFilesUtil,
10
13
  PermissionMode,
@@ -18,6 +21,17 @@ export const useInputManager = (
18
21
  ) => {
19
22
  const [state, dispatch] = useReducer(inputReducer, initialState);
20
23
 
24
+ // Detects bracketed paste (DECSET 2004) markers so pasted text is inserted
25
+ // without triggering submit — a pasted trailing \r must not be treated as
26
+ // Enter (see utils/bracketedPaste.ts).
27
+ const pasteDetectorRef = useRef(createBracketedPasteDetector());
28
+
29
+ // Abort plumbing for the /btw side question: ABORT_BTW (set when the overlay
30
+ // is dismissed while loading) aborts the in-flight onAskBtw call; the
31
+ // dismissed ref suppresses the late SET_BTW_STATE dispatch / error logging.
32
+ const btwAbortRef = useRef<AbortController | null>(null);
33
+ const btwDismissedRef = useRef(false);
34
+
21
35
  const {
22
36
  onInputTextChange,
23
37
  onCursorPositionChange,
@@ -47,7 +61,7 @@ export const useInputManager = (
47
61
  onRecallQueuedMessage,
48
62
  onClearMessages,
49
63
  onCompact,
50
- onGoalCommand,
64
+ isIdle: isIdleProp,
51
65
  } = callbacks;
52
66
 
53
67
  // Handle debounced file search
@@ -70,25 +84,15 @@ export const useInputManager = (
70
84
  }
71
85
  }, [state.showFileSelector, state.fileSearchQuery]);
72
86
 
73
- // Handle paste debouncing
87
+ // Auto-expire the double-Esc clear pending flag after the timeout window
88
+ // (aligned with Claude Code's useDoublePress timeout-based expiry).
74
89
  useEffect(() => {
75
- if (state.isPasting) {
76
- const pasteDebounceDelay = parseInt(
77
- process.env.PASTE_DEBOUNCE_MS || "30",
78
- 10,
79
- );
80
- const timer = setTimeout(() => {
81
- const processedInput = state.pasteBuffer.replace(/\r/g, "\n");
82
- dispatch({
83
- type: "INSERT_TEXT_WITH_PLACEHOLDER",
84
- payload: processedInput,
85
- });
86
- dispatch({ type: "END_PASTE" });
87
- dispatch({ type: "RESET_HISTORY_NAVIGATION" });
88
- }, pasteDebounceDelay);
89
- return () => clearTimeout(timer);
90
- }
91
- }, [state.isPasting, state.pasteBuffer]);
90
+ if (!state.escClearPending) return;
91
+ const timer = setTimeout(() => {
92
+ dispatch({ type: "RESET_ESC_CLEAR_PENDING" });
93
+ }, ESC_DOUBLE_PRESS_TIMEOUT_MS);
94
+ return () => clearTimeout(timer);
95
+ }, [state.escClearPending]);
92
96
 
93
97
  // Sync state changes with callbacks
94
98
  useEffect(() => {
@@ -127,28 +131,66 @@ export const useInputManager = (
127
131
  case "ABORT_MESSAGE":
128
132
  onAbortMessage?.();
129
133
  break;
134
+ case "SAVE_PROMPT_HISTORY":
135
+ PromptHistoryManager.addEntry(
136
+ effect.content,
137
+ sessionId,
138
+ effect.longTextMap,
139
+ workdir,
140
+ ).catch((err: unknown) => {
141
+ logger?.error("Failed to save prompt history", err);
142
+ });
143
+ break;
130
144
  case "BACKGROUND_CURRENT_TASK":
131
145
  onBackgroundCurrentTask?.();
132
146
  break;
133
- case "ASK_BTW":
147
+ case "ASK_BTW": {
148
+ const controller = new AbortController();
149
+ btwAbortRef.current = controller;
150
+ btwDismissedRef.current = false;
134
151
  try {
135
- const answer = await onAskBtw?.(effect.question);
136
- dispatch({
137
- type: "SET_BTW_STATE",
138
- payload: { answer, isLoading: false },
139
- });
140
- } catch (error) {
141
- console.error("Failed to ask side question:", error);
142
- dispatch({
143
- type: "SET_BTW_STATE",
144
- payload: {
145
- answer:
146
- "Error: Failed to get an answer for your side question.",
147
- isLoading: false,
152
+ const answer = await onAskBtw?.(
153
+ effect.question,
154
+ controller.signal,
155
+ (content: string) => {
156
+ // Stream partial answers into the overlay so the user sees
157
+ // the response grow in real time (same visual language as
158
+ // assistant text / thinking blocks) instead of a static
159
+ // "Answering..." indicator.
160
+ if (!btwDismissedRef.current) {
161
+ dispatch({
162
+ type: "SET_BTW_STATE",
163
+ payload: { answer: content, isLoading: true },
164
+ });
165
+ }
148
166
  },
149
- });
167
+ );
168
+ if (!btwDismissedRef.current) {
169
+ dispatch({
170
+ type: "SET_BTW_STATE",
171
+ payload: { answer, isLoading: false },
172
+ });
173
+ }
174
+ } catch (error) {
175
+ if (!btwDismissedRef.current) {
176
+ console.error("Failed to ask side question:", error);
177
+ dispatch({
178
+ type: "SET_BTW_STATE",
179
+ payload: {
180
+ answer:
181
+ "Error: Failed to get an answer for your side question.",
182
+ isLoading: false,
183
+ },
184
+ });
185
+ }
150
186
  }
151
187
  break;
188
+ }
189
+ case "ABORT_BTW":
190
+ btwDismissedRef.current = true;
191
+ btwAbortRef.current?.abort();
192
+ btwAbortRef.current = null;
193
+ break;
152
194
  case "PERMISSION_MODE_CHANGE":
153
195
  onPermissionModeChange?.(effect.mode);
154
196
  break;
@@ -209,6 +251,17 @@ export const useInputManager = (
209
251
  dispatch({ type: "SET_SHOW_LOGIN_COMMAND", payload: true });
210
252
  } else if (command === "logout") {
211
253
  dispatch({ type: "SET_SHOW_LOGIN_COMMAND", payload: true });
254
+ } else if (command === "btw") {
255
+ // Bare /btw executed via the command selector — show usage
256
+ // (aligned with Claude Code's empty-args message).
257
+ dispatch({
258
+ type: "SET_BTW_STATE",
259
+ payload: {
260
+ question: "",
261
+ isLoading: false,
262
+ answer: "Usage: /btw <your question>",
263
+ },
264
+ });
212
265
  } else if (command === "plugin") {
213
266
  dispatch({ type: "SET_SHOW_PLUGIN_MANAGER", payload: true });
214
267
  } else if (command === "model") {
@@ -219,8 +272,6 @@ export const useInputManager = (
219
272
  await onClearMessages?.();
220
273
  } else if (command === "compact") {
221
274
  await onCompact?.(effect.args);
222
- } else if (command === "goal") {
223
- await onGoalCommand?.(effect.args);
224
275
  }
225
276
  }
226
277
  break;
@@ -249,7 +300,6 @@ export const useInputManager = (
249
300
  onRecallQueuedMessage,
250
301
  onClearMessages,
251
302
  onCompact,
252
- onGoalCommand,
253
303
  ]);
254
304
 
255
305
  useEffect(() => {
@@ -327,7 +377,13 @@ export const useInputManager = (
327
377
  onImagesStateChange?.(state.attachedImages);
328
378
  }, [state.attachedImages, onImagesStateChange]);
329
379
 
330
- // /btw side question is handled via pendingEffect "ASK_BTW" above
380
+ // Keep the shared overlay-active flag in sync so App's Ctrl+C exit handler
381
+ // can defer to the /btw overlay (ink runs ALL useInput handlers per keypress
382
+ // with no propagation control).
383
+ useEffect(() => {
384
+ btwOverlayActiveRef.current =
385
+ state.btwState.question !== "" || state.btwState.answer !== undefined;
386
+ }, [state.btwState.question, state.btwState.answer]);
331
387
 
332
388
  // Methods
333
389
  const insertTextAtCursor = useCallback((text: string) => {
@@ -501,10 +557,11 @@ export const useInputManager = (
501
557
  key: {} as Key,
502
558
  hasSlashCommand: (cmd) => !!onHasSlashCommand?.(cmd),
503
559
  hasQueuedMessages: hasQueuedMessagesProp ?? false,
560
+ isIdle: isIdleProp ?? false,
504
561
  },
505
562
  });
506
563
  },
507
- [onHasSlashCommand, hasQueuedMessagesProp],
564
+ [onHasSlashCommand, hasQueuedMessagesProp, isIdleProp],
508
565
  );
509
566
 
510
567
  const handleSubmit = useCallback(async () => {
@@ -515,9 +572,10 @@ export const useInputManager = (
515
572
  key: { return: true } as Key,
516
573
  hasSlashCommand: (cmd) => !!onHasSlashCommand?.(cmd),
517
574
  hasQueuedMessages: hasQueuedMessagesProp ?? false,
575
+ isIdle: isIdleProp ?? false,
518
576
  },
519
577
  });
520
- }, [onHasSlashCommand, hasQueuedMessagesProp]);
578
+ }, [onHasSlashCommand, hasQueuedMessagesProp, isIdleProp]);
521
579
 
522
580
  const expandLongTextPlaceholders = useCallback(
523
581
  (text: string) => {
@@ -532,18 +590,51 @@ export const useInputManager = (
532
590
 
533
591
  const handleInput = useCallback(
534
592
  async (input: string, key: Key) => {
593
+ const result = pasteDetectorRef.current.process(input);
594
+
595
+ if (result.kind === "consume") {
596
+ // Content of an in-flight bracketed paste (or an empty paste):
597
+ // hold it, never submit or insert prematurely.
598
+ return true;
599
+ }
600
+
601
+ if (result.kind === "paste") {
602
+ if (result.leadingInput) {
603
+ dispatch({
604
+ type: "HANDLE_KEY",
605
+ payload: {
606
+ input: result.leadingInput,
607
+ key,
608
+ hasSlashCommand: (cmd) => !!onHasSlashCommand?.(cmd),
609
+ hasQueuedMessages: hasQueuedMessagesProp ?? false,
610
+ isIdle: isIdleProp ?? false,
611
+ },
612
+ });
613
+ }
614
+ if (result.text !== "") {
615
+ // Insert-only: \r → \n normalizes CRLF terminals, matching the
616
+ // canonical paste path (inputHandlers.handlePasteInput).
617
+ dispatch({
618
+ type: "INSERT_TEXT_WITH_PLACEHOLDER",
619
+ payload: result.text.replace(/\r/g, "\n"),
620
+ });
621
+ }
622
+ return true;
623
+ }
624
+
535
625
  dispatch({
536
626
  type: "HANDLE_KEY",
537
627
  payload: {
538
- input,
628
+ input: result.input,
539
629
  key,
540
630
  hasSlashCommand: (cmd) => !!onHasSlashCommand?.(cmd),
541
631
  hasQueuedMessages: hasQueuedMessagesProp ?? false,
632
+ isIdle: isIdleProp ?? false,
542
633
  },
543
634
  });
544
635
  return true;
545
636
  },
546
- [onHasSlashCommand, hasQueuedMessagesProp],
637
+ [onHasSlashCommand, hasQueuedMessagesProp, isIdleProp],
547
638
  );
548
639
 
549
640
  return {
@@ -572,6 +663,7 @@ export const useInputManager = (
572
663
  permissionMode: state.permissionMode,
573
664
  attachedImages: state.attachedImages,
574
665
  btwState: state.btwState,
666
+ escClearPending: state.escClearPending,
575
667
  isManagerReady: true,
576
668
 
577
669
  // Methods
package/src/index.ts CHANGED
@@ -53,6 +53,12 @@ export async function main() {
53
53
  default: false,
54
54
  global: false,
55
55
  })
56
+ .option("daemon", {
57
+ description:
58
+ "Start as a background daemon (JSON-RPC over a unix socket at PATH)",
59
+ type: "string",
60
+ global: false,
61
+ })
56
62
  .option("show-stats", {
57
63
  description: "Show timing and usage statistics in print mode",
58
64
  type: "boolean",
@@ -402,6 +408,12 @@ export async function main() {
402
408
  return startStdioCli();
403
409
  }
404
410
 
411
+ // Handle daemon mode (remote background sessions)
412
+ if (typeof argv.daemon === "string") {
413
+ const { startDaemonCli } = await import("./daemon-cli.js");
414
+ return startDaemonCli(argv.daemon);
415
+ }
416
+
405
417
  await startCli({
406
418
  restoreSessionId: argv.restore as string | undefined,
407
419
  continueLastSession: argv.continue as boolean | undefined,
@@ -63,16 +63,26 @@ export const handleSubmit = async (
63
63
  ? contentWithPlaceholders.substring(5).trim()
64
64
  : "";
65
65
 
66
- const payload = {
67
- isActive: true,
68
- question,
69
- isLoading: question !== "",
70
- };
71
-
72
- dispatch({
73
- type: "SET_BTW_STATE",
74
- payload,
75
- });
66
+ if (question) {
67
+ dispatch({
68
+ type: "SET_BTW_STATE",
69
+ payload: {
70
+ question,
71
+ isLoading: true,
72
+ answer: undefined,
73
+ },
74
+ });
75
+ } else {
76
+ // Bare /btw — show usage (aligned with Claude Code)
77
+ dispatch({
78
+ type: "SET_BTW_STATE",
79
+ payload: {
80
+ question: "",
81
+ isLoading: false,
82
+ answer: "Usage: /btw <your question>",
83
+ },
84
+ });
85
+ }
76
86
  dispatch({ type: "CLEAR_INPUT" });
77
87
  dispatch({ type: "RESET_HISTORY_NAVIGATION" });
78
88
  dispatch({ type: "CLEAR_LONG_TEXT_MAP" });
@@ -292,17 +302,13 @@ export const handlePasteInput = (
292
302
  input: string,
293
303
  ): void => {
294
304
  const inputString = input;
295
- const isPasteOperation =
296
- inputString.length > 1 ||
297
- inputString.includes("\n") ||
298
- inputString.includes("\r");
299
-
300
- if (isPasteOperation) {
301
- // Dispatch a single action type; the reducer determines start vs append
302
- // by checking pasteBuffer, avoiding stale state issues
305
+
306
+ if (inputString.length > 1) {
307
+ // Multi-char chunk: insert immediately (\r → \n normalizes CRLF
308
+ // terminals), matching the reducer's HANDLE_KEY path.
303
309
  dispatch({
304
- type: "APPEND_PASTE_CHUNK",
305
- payload: { chunk: inputString, cursorPosition: state.cursorPosition },
310
+ type: "INSERT_TEXT_WITH_PLACEHOLDER",
311
+ payload: inputString.replace(/\r/g, "\n"),
306
312
  });
307
313
  } else {
308
314
  let char = inputString;
@@ -367,6 +373,17 @@ export const handleCommandSelect = (
367
373
  dispatch({ type: "SET_SHOW_HELP", payload: true });
368
374
  } else if (command === "status") {
369
375
  dispatch({ type: "SET_SHOW_STATUS_COMMAND", payload: true });
376
+ } else if (command === "btw") {
377
+ // Bare /btw executed via the command selector — show usage
378
+ // (aligned with Claude Code's empty-args message).
379
+ dispatch({
380
+ type: "SET_BTW_STATE",
381
+ payload: {
382
+ question: "",
383
+ isLoading: false,
384
+ answer: "Usage: /btw <your question>",
385
+ },
386
+ });
370
387
  } else if (command === "plugin") {
371
388
  dispatch({ type: "SET_SHOW_PLUGIN_MANAGER", payload: true });
372
389
  } else if (command === "model") {
@@ -377,8 +394,6 @@ export const handleCommandSelect = (
377
394
  await callbacks.onClearMessages?.();
378
395
  } else if (command === "compact") {
379
396
  await callbacks.onCompact?.();
380
- } else if (command === "goal") {
381
- await callbacks.onGoalCommand?.();
382
397
  }
383
398
  }
384
399
  })();
@@ -702,16 +717,24 @@ export const handleInput = async (
702
717
  key: Key,
703
718
  clearImages?: () => void,
704
719
  ): Promise<boolean> => {
705
- // Handle ESC to dismiss btw answer
706
- if (key.escape && state.btwState.question && !state.btwState.isLoading) {
707
- dispatch({
708
- type: "SET_BTW_STATE",
709
- payload: {
710
- question: "",
711
- answer: undefined,
712
- isLoading: false,
713
- },
714
- });
720
+ // /btw overlay handling (mirrors inputReducer's HANDLE_KEY block; this
721
+ // handler is not wired, kept in sync for consistency). Active while a
722
+ // question is displayed, or the bare-/btw usage message shows. Only
723
+ // Escape dismisses; every other key is ignored.
724
+ if (state.btwState.question || state.btwState.answer) {
725
+ if (key.escape) {
726
+ dispatch({
727
+ type: "SET_BTW_STATE",
728
+ payload: {
729
+ question: "",
730
+ answer: undefined,
731
+ isLoading: false,
732
+ },
733
+ });
734
+ return true;
735
+ }
736
+
737
+ // Any other key while the overlay is up is ignored
715
738
  return true;
716
739
  }
717
740