wave-code 1.0.0 → 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 (70) 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/InputBox.d.ts +0 -3
  6. package/dist/components/InputBox.js +11 -9
  7. package/dist/components/LoadingIndicator.d.ts +1 -2
  8. package/dist/components/LoadingIndicator.js +2 -2
  9. package/dist/components/Markdown.js +13 -16
  10. package/dist/components/MessageList.d.ts +2 -1
  11. package/dist/components/MessageList.js +2 -2
  12. package/dist/components/StatusLine.d.ts +0 -2
  13. package/dist/components/StatusLine.js +6 -6
  14. package/dist/components/TaskList.js +2 -1
  15. package/dist/components/ToolDisplay.d.ts +1 -0
  16. package/dist/components/ToolDisplay.js +17 -9
  17. package/dist/constants/commands.js +0 -6
  18. package/dist/contexts/useChat.d.ts +3 -5
  19. package/dist/contexts/useChat.js +242 -82
  20. package/dist/daemon-cli.d.ts +10 -0
  21. package/dist/daemon-cli.js +15 -0
  22. package/dist/hooks/useInputManager.js +99 -22
  23. package/dist/index.js +10 -0
  24. package/dist/managers/inputHandlers.js +50 -22
  25. package/dist/managers/inputReducer.d.ts +12 -2
  26. package/dist/managers/inputReducer.js +57 -9
  27. package/dist/stdio/agentBridge.d.ts +23 -0
  28. package/dist/stdio/agentBridge.js +126 -16
  29. package/dist/stdio/daemonServer.d.ts +67 -0
  30. package/dist/stdio/daemonServer.js +191 -0
  31. package/dist/stdio/index.d.ts +2 -0
  32. package/dist/stdio/index.js +2 -0
  33. package/dist/stdio/jsonRpcConnection.d.ts +30 -0
  34. package/dist/stdio/jsonRpcConnection.js +127 -0
  35. package/dist/stdio/protocol.d.ts +2 -2
  36. package/dist/stdio/stdioServer.d.ts +2 -7
  37. package/dist/stdio/stdioServer.js +9 -100
  38. package/dist/utils/bracketedPaste.d.ts +39 -0
  39. package/dist/utils/bracketedPaste.js +122 -0
  40. package/dist/utils/markdownTable.d.ts +34 -0
  41. package/dist/utils/markdownTable.js +302 -0
  42. package/dist/utils/throttle.d.ts +3 -3
  43. package/package.json +4 -2
  44. package/src/cli.tsx +20 -1
  45. package/src/components/App.tsx +5 -0
  46. package/src/components/BtwDisplay.tsx +36 -12
  47. package/src/components/ChatInterface.tsx +25 -12
  48. package/src/components/InputBox.tsx +10 -18
  49. package/src/components/LoadingIndicator.tsx +1 -4
  50. package/src/components/Markdown.tsx +15 -18
  51. package/src/components/MessageList.tsx +6 -0
  52. package/src/components/StatusLine.tsx +0 -10
  53. package/src/components/TaskList.tsx +2 -1
  54. package/src/components/ToolDisplay.tsx +17 -6
  55. package/src/constants/commands.ts +0 -6
  56. package/src/contexts/useChat.tsx +310 -95
  57. package/src/daemon-cli.ts +17 -0
  58. package/src/hooks/useInputManager.ts +108 -22
  59. package/src/index.ts +12 -0
  60. package/src/managers/inputHandlers.ts +49 -22
  61. package/src/managers/inputReducer.ts +66 -11
  62. package/src/stdio/agentBridge.ts +188 -17
  63. package/src/stdio/daemonServer.ts +212 -0
  64. package/src/stdio/index.ts +2 -0
  65. package/src/stdio/jsonRpcConnection.ts +160 -0
  66. package/src/stdio/protocol.ts +5 -2
  67. package/src/stdio/stdioServer.ts +14 -120
  68. package/src/utils/bracketedPaste.ts +170 -0
  69. package/src/utils/markdownTable.ts +359 -0
  70. package/src/utils/throttle.ts +8 -8
@@ -1,11 +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
7
  ESC_DOUBLE_PRESS_TIMEOUT_MS,
8
+ btwOverlayActiveRef,
8
9
  } from "../managers/inputReducer.js";
10
+ import { createBracketedPasteDetector } from "../utils/bracketedPaste.js";
9
11
  import {
10
12
  searchFiles as searchFilesUtil,
11
13
  PermissionMode,
@@ -19,6 +21,17 @@ export const useInputManager = (
19
21
  ) => {
20
22
  const [state, dispatch] = useReducer(inputReducer, initialState);
21
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
+
22
35
  const {
23
36
  onInputTextChange,
24
37
  onCursorPositionChange,
@@ -48,7 +61,6 @@ export const useInputManager = (
48
61
  onRecallQueuedMessage,
49
62
  onClearMessages,
50
63
  onCompact,
51
- onGoalCommand,
52
64
  isIdle: isIdleProp,
53
65
  } = callbacks;
54
66
 
@@ -132,25 +144,53 @@ export const useInputManager = (
132
144
  case "BACKGROUND_CURRENT_TASK":
133
145
  onBackgroundCurrentTask?.();
134
146
  break;
135
- case "ASK_BTW":
147
+ case "ASK_BTW": {
148
+ const controller = new AbortController();
149
+ btwAbortRef.current = controller;
150
+ btwDismissedRef.current = false;
136
151
  try {
137
- const answer = await onAskBtw?.(effect.question);
138
- dispatch({
139
- type: "SET_BTW_STATE",
140
- payload: { answer, isLoading: false },
141
- });
142
- } catch (error) {
143
- console.error("Failed to ask side question:", error);
144
- dispatch({
145
- type: "SET_BTW_STATE",
146
- payload: {
147
- answer:
148
- "Error: Failed to get an answer for your side question.",
149
- 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
+ }
150
166
  },
151
- });
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
+ }
152
186
  }
153
187
  break;
188
+ }
189
+ case "ABORT_BTW":
190
+ btwDismissedRef.current = true;
191
+ btwAbortRef.current?.abort();
192
+ btwAbortRef.current = null;
193
+ break;
154
194
  case "PERMISSION_MODE_CHANGE":
155
195
  onPermissionModeChange?.(effect.mode);
156
196
  break;
@@ -211,6 +251,17 @@ export const useInputManager = (
211
251
  dispatch({ type: "SET_SHOW_LOGIN_COMMAND", payload: true });
212
252
  } else if (command === "logout") {
213
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
+ });
214
265
  } else if (command === "plugin") {
215
266
  dispatch({ type: "SET_SHOW_PLUGIN_MANAGER", payload: true });
216
267
  } else if (command === "model") {
@@ -221,8 +272,6 @@ export const useInputManager = (
221
272
  await onClearMessages?.();
222
273
  } else if (command === "compact") {
223
274
  await onCompact?.(effect.args);
224
- } else if (command === "goal") {
225
- await onGoalCommand?.(effect.args);
226
275
  }
227
276
  }
228
277
  break;
@@ -251,7 +300,6 @@ export const useInputManager = (
251
300
  onRecallQueuedMessage,
252
301
  onClearMessages,
253
302
  onCompact,
254
- onGoalCommand,
255
303
  ]);
256
304
 
257
305
  useEffect(() => {
@@ -329,7 +377,13 @@ export const useInputManager = (
329
377
  onImagesStateChange?.(state.attachedImages);
330
378
  }, [state.attachedImages, onImagesStateChange]);
331
379
 
332
- // /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]);
333
387
 
334
388
  // Methods
335
389
  const insertTextAtCursor = useCallback((text: string) => {
@@ -536,10 +590,42 @@ export const useInputManager = (
536
590
 
537
591
  const handleInput = useCallback(
538
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
+
539
625
  dispatch({
540
626
  type: "HANDLE_KEY",
541
627
  payload: {
542
- input,
628
+ input: result.input,
543
629
  key,
544
630
  hasSlashCommand: (cmd) => !!onHasSlashCommand?.(cmd),
545
631
  hasQueuedMessages: hasQueuedMessagesProp ?? false,
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" });
@@ -363,6 +373,17 @@ export const handleCommandSelect = (
363
373
  dispatch({ type: "SET_SHOW_HELP", payload: true });
364
374
  } else if (command === "status") {
365
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
+ });
366
387
  } else if (command === "plugin") {
367
388
  dispatch({ type: "SET_SHOW_PLUGIN_MANAGER", payload: true });
368
389
  } else if (command === "model") {
@@ -373,8 +394,6 @@ export const handleCommandSelect = (
373
394
  await callbacks.onClearMessages?.();
374
395
  } else if (command === "compact") {
375
396
  await callbacks.onCompact?.();
376
- } else if (command === "goal") {
377
- await callbacks.onGoalCommand?.();
378
397
  }
379
398
  }
380
399
  })();
@@ -698,16 +717,24 @@ export const handleInput = async (
698
717
  key: Key,
699
718
  clearImages?: () => void,
700
719
  ): Promise<boolean> => {
701
- // Handle ESC to dismiss btw answer
702
- if (key.escape && state.btwState.question && !state.btwState.isLoading) {
703
- dispatch({
704
- type: "SET_BTW_STATE",
705
- payload: {
706
- question: "",
707
- answer: undefined,
708
- isLoading: false,
709
- },
710
- });
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
711
738
  return true;
712
739
  }
713
740
 
@@ -27,6 +27,14 @@ export interface BtwState {
27
27
  isLoading: boolean;
28
28
  }
29
29
 
30
+ /**
31
+ * True while the /btw overlay is up (a question is on display, loading or
32
+ * answered). App's Ctrl+C exit handler checks this so Ctrl+C does not quit
33
+ * the app while the overlay owns the keys. Synced from useInputManager via
34
+ * an effect.
35
+ */
36
+ export const btwOverlayActiveRef: { current: boolean } = { current: false };
37
+
30
38
  export const ESC_DOUBLE_PRESS_TIMEOUT_MS = 1000;
31
39
 
32
40
  export type PendingEffect =
@@ -44,6 +52,7 @@ export type PendingEffect =
44
52
  | { type: "ABORT_MESSAGE" }
45
53
  | { type: "BACKGROUND_CURRENT_TASK" }
46
54
  | { type: "ASK_BTW"; question: string }
55
+ | { type: "ABORT_BTW" }
47
56
  | { type: "PERMISSION_MODE_CHANGE"; mode: PermissionMode }
48
57
  | { type: "FETCH_HISTORY" }
49
58
  | { type: "PASTE_IMAGE" }
@@ -83,10 +92,13 @@ export interface InputManagerCallbacks {
83
92
  onAbortMessage?: () => void;
84
93
  onBackgroundCurrentTask?: () => void;
85
94
  onPermissionModeChange?: (mode: PermissionMode) => void;
86
- onAskBtw?: (question: string) => Promise<string>;
95
+ onAskBtw?: (
96
+ question: string,
97
+ abortSignal?: AbortSignal,
98
+ onContent?: (content: string) => void,
99
+ ) => Promise<string>;
87
100
  onClearMessages?: () => Promise<void>;
88
101
  onCompact?: (instructions?: string) => Promise<void>;
89
- onGoalCommand?: (args?: string) => Promise<void>;
90
102
  sessionId?: string;
91
103
  workdir?: string;
92
104
  getFullMessageThread?: () => Promise<{
@@ -243,7 +255,7 @@ function insertTextWithPlaceholder(
243
255
  /**
244
256
  * Submit the current input text: extract [Image #N] references, route /btw
245
257
  * and CLI-internal slash commands, otherwise send as a message. Returns null
246
- * when there is nothing to submit (empty text, bare /btw).
258
+ * when there is nothing to submit (empty text).
247
259
  */
248
260
  function submitInput(state: InputState): InputState | null {
249
261
  if (!state.inputText.trim()) {
@@ -266,8 +278,20 @@ function submitInput(state: InputState): InputState | null {
266
278
  if (contentWithPlaceholders.startsWith("/btw ")) {
267
279
  const question = contentWithPlaceholders.substring(5).trim();
268
280
  if (!question) {
269
- // Bare /btw with no question text — ignore
270
- return null;
281
+ // "/btw " with no question text — show usage (aligned with Claude Code)
282
+ return {
283
+ ...state,
284
+ inputText: "",
285
+ cursorPosition: 0,
286
+ historyIndex: -1,
287
+ longTextMap: {},
288
+ attachedImages: [],
289
+ btwState: {
290
+ question: "",
291
+ isLoading: false,
292
+ answer: "Usage: /btw <your question>",
293
+ },
294
+ };
271
295
  }
272
296
 
273
297
  return {
@@ -287,8 +311,20 @@ function submitInput(state: InputState): InputState | null {
287
311
  }
288
312
 
289
313
  if (contentWithPlaceholders === "/btw") {
290
- // Bare /btw — ignore
291
- return null;
314
+ // Bare /btw — show usage (aligned with Claude Code)
315
+ return {
316
+ ...state,
317
+ inputText: "",
318
+ cursorPosition: 0,
319
+ historyIndex: -1,
320
+ longTextMap: {},
321
+ attachedImages: [],
322
+ btwState: {
323
+ question: "",
324
+ isLoading: false,
325
+ answer: "Usage: /btw <your question>",
326
+ },
327
+ };
292
328
  }
293
329
 
294
330
  // Check if the content is a CLI-internal slash command (help, tasks,
@@ -858,10 +894,22 @@ export function inputReducer(
858
894
  return state;
859
895
  }
860
896
 
861
- // 1. Escape Handling
862
- if (key.escape) {
863
- // Dismiss btw answer
864
- if (state.btwState.question && !state.btwState.isLoading) {
897
+ // 1. /btw overlay handling (active while a question is displayed, or
898
+ // the bare-/btw usage message). Only Escape dismisses (or aborts the
899
+ // in-flight side question while loading); every other key is ignored.
900
+ if (state.btwState.question || state.btwState.answer) {
901
+ if (key.escape) {
902
+ if (state.btwState.isLoading) {
903
+ return {
904
+ ...state,
905
+ btwState: {
906
+ question: "",
907
+ answer: undefined,
908
+ isLoading: false,
909
+ },
910
+ pendingEffect: { type: "ABORT_BTW" },
911
+ };
912
+ }
865
913
  return {
866
914
  ...state,
867
915
  btwState: {
@@ -871,6 +919,13 @@ export function inputReducer(
871
919
  },
872
920
  };
873
921
  }
922
+
923
+ // Any other key while the overlay is up is ignored
924
+ return state;
925
+ }
926
+
927
+ // 1. Escape Handling
928
+ if (key.escape) {
874
929
  if (state.showFileSelector) {
875
930
  return {
876
931
  ...state,