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.
- package/dist/cli.js +20 -1
- package/dist/components/App.js +7 -0
- package/dist/components/BtwDisplay.js +13 -3
- package/dist/components/ChatInterface.js +21 -6
- package/dist/components/InputBox.d.ts +0 -3
- package/dist/components/InputBox.js +11 -9
- package/dist/components/LoadingIndicator.d.ts +1 -2
- package/dist/components/LoadingIndicator.js +2 -2
- package/dist/components/Markdown.js +13 -16
- package/dist/components/MessageList.d.ts +2 -1
- package/dist/components/MessageList.js +2 -2
- package/dist/components/StatusLine.d.ts +0 -2
- package/dist/components/StatusLine.js +6 -6
- package/dist/components/TaskList.js +2 -1
- package/dist/components/ToolDisplay.d.ts +1 -0
- package/dist/components/ToolDisplay.js +17 -9
- package/dist/constants/commands.js +0 -6
- package/dist/contexts/useChat.d.ts +3 -5
- package/dist/contexts/useChat.js +242 -82
- package/dist/daemon-cli.d.ts +10 -0
- package/dist/daemon-cli.js +15 -0
- package/dist/hooks/useInputManager.js +99 -22
- package/dist/index.js +10 -0
- package/dist/managers/inputHandlers.js +50 -22
- package/dist/managers/inputReducer.d.ts +12 -2
- package/dist/managers/inputReducer.js +57 -9
- package/dist/stdio/agentBridge.d.ts +23 -0
- package/dist/stdio/agentBridge.js +126 -16
- package/dist/stdio/daemonServer.d.ts +67 -0
- package/dist/stdio/daemonServer.js +191 -0
- package/dist/stdio/index.d.ts +2 -0
- package/dist/stdio/index.js +2 -0
- package/dist/stdio/jsonRpcConnection.d.ts +30 -0
- package/dist/stdio/jsonRpcConnection.js +127 -0
- package/dist/stdio/protocol.d.ts +2 -2
- package/dist/stdio/stdioServer.d.ts +2 -7
- package/dist/stdio/stdioServer.js +9 -100
- package/dist/utils/bracketedPaste.d.ts +39 -0
- package/dist/utils/bracketedPaste.js +122 -0
- package/dist/utils/markdownTable.d.ts +34 -0
- package/dist/utils/markdownTable.js +302 -0
- package/dist/utils/throttle.d.ts +3 -3
- package/package.json +4 -2
- package/src/cli.tsx +20 -1
- package/src/components/App.tsx +5 -0
- package/src/components/BtwDisplay.tsx +36 -12
- package/src/components/ChatInterface.tsx +25 -12
- package/src/components/InputBox.tsx +10 -18
- package/src/components/LoadingIndicator.tsx +1 -4
- package/src/components/Markdown.tsx +15 -18
- package/src/components/MessageList.tsx +6 -0
- package/src/components/StatusLine.tsx +0 -10
- package/src/components/TaskList.tsx +2 -1
- package/src/components/ToolDisplay.tsx +17 -6
- package/src/constants/commands.ts +0 -6
- package/src/contexts/useChat.tsx +310 -95
- package/src/daemon-cli.ts +17 -0
- package/src/hooks/useInputManager.ts +108 -22
- package/src/index.ts +12 -0
- package/src/managers/inputHandlers.ts +49 -22
- package/src/managers/inputReducer.ts +66 -11
- package/src/stdio/agentBridge.ts +188 -17
- package/src/stdio/daemonServer.ts +212 -0
- package/src/stdio/index.ts +2 -0
- package/src/stdio/jsonRpcConnection.ts +160 -0
- package/src/stdio/protocol.ts +5 -2
- package/src/stdio/stdioServer.ts +14 -120
- package/src/utils/bracketedPaste.ts +170 -0
- package/src/utils/markdownTable.ts +359 -0
- 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?.(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
//
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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?: (
|
|
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
|
|
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
|
-
//
|
|
270
|
-
return
|
|
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 —
|
|
291
|
-
return
|
|
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.
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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,
|