wave-code 0.19.9 → 1.0.0
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/components/ChatInterface.js +1 -1
- package/dist/components/HelpView.js +6 -0
- package/dist/components/InputBox.d.ts +2 -0
- package/dist/components/InputBox.js +11 -2
- package/dist/components/RewindCommand.js +4 -2
- package/dist/hooks/useInputManager.d.ts +1 -0
- package/dist/hooks/useInputManager.js +23 -20
- package/dist/managers/inputHandlers.js +5 -8
- package/dist/managers/inputReducer.d.ts +10 -20
- package/dist/managers/inputReducer.js +309 -173
- package/dist/print-cli.js +36 -10
- package/dist/stdio/agentBridge.js +25 -2
- package/dist/utils/rewindCheckpoints.d.ts +8 -0
- package/dist/utils/rewindCheckpoints.js +15 -0
- package/dist/utils/worktree.d.ts +8 -0
- package/dist/utils/worktree.js +32 -1
- package/package.json +2 -2
- package/src/components/ChatInterface.tsx +2 -0
- package/src/components/HelpView.tsx +6 -0
- package/src/components/InputBox.tsx +19 -0
- package/src/components/RewindCommand.tsx +4 -2
- package/src/hooks/useInputManager.ts +27 -21
- package/src/managers/inputHandlers.ts +6 -10
- package/src/managers/inputReducer.ts +381 -208
- package/src/print-cli.ts +48 -11
- package/src/stdio/agentBridge.ts +25 -1
- package/src/utils/rewindCheckpoints.ts +15 -0
- package/src/utils/worktree.ts +50 -1
|
@@ -14,11 +14,12 @@
|
|
|
14
14
|
* - Implement the canUseTool permission flow over the stdio protocol
|
|
15
15
|
* - Handle config updates by destroying and recreating the Agent
|
|
16
16
|
*/
|
|
17
|
-
import { Agent, listSessions, searchFiles, generateRandomName, getDefaultRemoteBranch, getMessageContent, PromptHistoryManager, AuthService, PluginCore, } from "wave-agent-sdk";
|
|
17
|
+
import { Agent, listSessions, searchFiles, generateRandomName, getDefaultRemoteBranch, getMessageContent, PromptHistoryManager, AuthService, PluginCore, validateWorktreeRemovalPath, } from "wave-agent-sdk";
|
|
18
18
|
import { INTERNAL_ERROR as PROTOCOL_INTERNAL_ERROR, METHOD_NOT_FOUND as PROTOCOL_METHOD_NOT_FOUND, } from "./protocol.js";
|
|
19
19
|
import { execFileSync } from "node:child_process";
|
|
20
20
|
import { createWorktree, removeWorktree } from "../utils/worktree.js";
|
|
21
21
|
import { logger } from "../utils/logger.js";
|
|
22
|
+
import { isUserCheckpointMessage } from "../utils/rewindCheckpoints.js";
|
|
22
23
|
export class AgentBridge {
|
|
23
24
|
constructor(options) {
|
|
24
25
|
this.sessions = new Map();
|
|
@@ -268,6 +269,28 @@ export class AgentBridge {
|
|
|
268
269
|
}
|
|
269
270
|
}
|
|
270
271
|
async removeWorktreeSession(params) {
|
|
272
|
+
// Align with Claude Code v2.1.216+: refuse to remove a worktree whose path
|
|
273
|
+
// is a symlink or resolves outside the repo root. Already-removed (missing)
|
|
274
|
+
// paths pass validation so removal stays idempotent.
|
|
275
|
+
try {
|
|
276
|
+
validateWorktreeRemovalPath(params.path, params.repoRoot);
|
|
277
|
+
}
|
|
278
|
+
catch (e) {
|
|
279
|
+
throw new RpcError(PROTOCOL_INTERNAL_ERROR, e.message);
|
|
280
|
+
}
|
|
281
|
+
// Trigger the WorktreeRemove hook (before git removal, non-blocking) using
|
|
282
|
+
// the session that runs in this worktree, if it is still registered.
|
|
283
|
+
for (const entry of this.sessions.values()) {
|
|
284
|
+
if (entry.agent.workingDirectory === params.path) {
|
|
285
|
+
try {
|
|
286
|
+
await entry.agent.triggerWorktreeRemoveHook(params.path);
|
|
287
|
+
}
|
|
288
|
+
catch (e) {
|
|
289
|
+
logger.warn("WorktreeRemove hooks execution failed:", e);
|
|
290
|
+
}
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
271
294
|
// removeWorktree is best-effort/idempotent: already-removed worktrees or
|
|
272
295
|
// branches only log, never throw.
|
|
273
296
|
await removeWorktree({
|
|
@@ -381,7 +404,7 @@ export class AgentBridge {
|
|
|
381
404
|
const entry = this.requireSession(sessionId);
|
|
382
405
|
const { messages } = await entry.agent.getFullMessageThread();
|
|
383
406
|
const checkpoints = messages
|
|
384
|
-
.filter((m) => m
|
|
407
|
+
.filter((m) => isUserCheckpointMessage(m) && m.id)
|
|
385
408
|
.map((m) => ({
|
|
386
409
|
id: m.id,
|
|
387
410
|
content: getMessageContent(m).replace(/\s+/g, " ").trim(),
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Message } from "wave-agent-sdk";
|
|
2
|
+
/**
|
|
3
|
+
* 判断一条 user 消息能否作为 /rewind 检查点。
|
|
4
|
+
* 后台任务通知(task_notification)与 hook 注入的消息(source: "hook")
|
|
5
|
+
* 都是系统生成、用户不可见的,不能作为回滚点。
|
|
6
|
+
* CLI 交互式选择器与 stdio listRewindCheckpoints 共用此判定,避免两处漂移。
|
|
7
|
+
*/
|
|
8
|
+
export declare function isUserCheckpointMessage(m: Message): boolean;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 判断一条 user 消息能否作为 /rewind 检查点。
|
|
3
|
+
* 后台任务通知(task_notification)与 hook 注入的消息(source: "hook")
|
|
4
|
+
* 都是系统生成、用户不可见的,不能作为回滚点。
|
|
5
|
+
* CLI 交互式选择器与 stdio listRewindCheckpoints 共用此判定,避免两处漂移。
|
|
6
|
+
*/
|
|
7
|
+
export function isUserCheckpointMessage(m) {
|
|
8
|
+
if (m.role !== "user" || m.isMeta)
|
|
9
|
+
return false;
|
|
10
|
+
if (m.blocks.some((b) => b.type === "task_notification"))
|
|
11
|
+
return false;
|
|
12
|
+
if (m.blocks.some((b) => b.type === "text" && b.source === "hook"))
|
|
13
|
+
return false;
|
|
14
|
+
return true;
|
|
15
|
+
}
|
package/dist/utils/worktree.d.ts
CHANGED
|
@@ -7,6 +7,14 @@ export interface WorktreeSession {
|
|
|
7
7
|
hasNewCommits: boolean;
|
|
8
8
|
isNew: boolean;
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Validate a worktree name before any side effects. Names are slash-separated
|
|
12
|
+
* slugs: every segment must be non-empty and contain only letters, digits,
|
|
13
|
+
* dots, underscores, and dashes. The total length is capped at 64 characters
|
|
14
|
+
* and "." / ".." segments are rejected (path traversal protection).
|
|
15
|
+
* @throws {Error} When the name is not a valid slug
|
|
16
|
+
*/
|
|
17
|
+
export declare function validateWorktreeSlug(name: string): void;
|
|
10
18
|
/**
|
|
11
19
|
* Create a new git worktree
|
|
12
20
|
* @param name Worktree name
|
package/dist/utils/worktree.js
CHANGED
|
@@ -2,11 +2,34 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import * as fs from "node:fs";
|
|
5
|
-
import { getDefaultRemoteBranch, getGitMainRepoRoot } from "wave-agent-sdk";
|
|
5
|
+
import { getDefaultRemoteBranch, getGitMainRepoRoot, performPostCreationSetup, } from "wave-agent-sdk";
|
|
6
6
|
// Never use execFileSync here: the shared `wave --stdio` process handles all
|
|
7
7
|
// desktop sessions, so a synchronous git call (especially a multi-second
|
|
8
8
|
// recursive worktree delete or a network fetch) freezes every session.
|
|
9
9
|
const execFileAsync = promisify(execFile);
|
|
10
|
+
// --- Worktree name validation ------------------------------------------------
|
|
11
|
+
const VALID_WORKTREE_SLUG_SEGMENT = /^[a-zA-Z0-9._-]+$/;
|
|
12
|
+
const MAX_WORKTREE_SLUG_LENGTH = 64;
|
|
13
|
+
/**
|
|
14
|
+
* Validate a worktree name before any side effects. Names are slash-separated
|
|
15
|
+
* slugs: every segment must be non-empty and contain only letters, digits,
|
|
16
|
+
* dots, underscores, and dashes. The total length is capped at 64 characters
|
|
17
|
+
* and "." / ".." segments are rejected (path traversal protection).
|
|
18
|
+
* @throws {Error} When the name is not a valid slug
|
|
19
|
+
*/
|
|
20
|
+
export function validateWorktreeSlug(name) {
|
|
21
|
+
if (name.length > MAX_WORKTREE_SLUG_LENGTH) {
|
|
22
|
+
throw new Error(`Invalid worktree name: "${name}" must be ${MAX_WORKTREE_SLUG_LENGTH} characters or fewer (got ${name.length})`);
|
|
23
|
+
}
|
|
24
|
+
for (const segment of name.split("/")) {
|
|
25
|
+
if (segment === "." || segment === "..") {
|
|
26
|
+
throw new Error(`Invalid worktree name: "${name}" must not contain "." or ".." path segments`);
|
|
27
|
+
}
|
|
28
|
+
if (segment.length === 0 || !VALID_WORKTREE_SLUG_SEGMENT.test(segment)) {
|
|
29
|
+
throw new Error(`Invalid worktree name: "${name}" each "/"-separated segment must be non-empty and contain only letters, digits, dots, underscores, and dashes`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
10
33
|
/**
|
|
11
34
|
* Create a new git worktree
|
|
12
35
|
* @param name Worktree name
|
|
@@ -17,6 +40,14 @@ const execFileAsync = promisify(execFile);
|
|
|
17
40
|
* @returns Worktree session details
|
|
18
41
|
*/
|
|
19
42
|
export async function createWorktree(name, cwd, options) {
|
|
43
|
+
validateWorktreeSlug(name);
|
|
44
|
+
const session = await createWorktreeInternal(name, cwd, options);
|
|
45
|
+
if (session.isNew) {
|
|
46
|
+
await performPostCreationSetup(session.path, session.repoRoot);
|
|
47
|
+
}
|
|
48
|
+
return session;
|
|
49
|
+
}
|
|
50
|
+
async function createWorktreeInternal(name, cwd, options) {
|
|
20
51
|
const repoRoot = getGitMainRepoRoot(cwd);
|
|
21
52
|
const worktreePath = path.join(repoRoot, ".wave", "worktrees", name);
|
|
22
53
|
const branchName = `worktree-${name}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wave-code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "CLI-based code assistant powered by AI, built with React and Ink",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"semver": "^7.7.4",
|
|
42
42
|
"yargs": "^17.7.2",
|
|
43
43
|
"zod": "^3.23.8",
|
|
44
|
-
"wave-agent-sdk": "0.
|
|
44
|
+
"wave-agent-sdk": "1.0.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/react": "^19.1.8",
|
|
@@ -100,6 +100,8 @@ export const ChatInterface: React.FC = () => {
|
|
|
100
100
|
<InputBox
|
|
101
101
|
isLoading={isLoading}
|
|
102
102
|
isCommandRunning={isCommandRunning}
|
|
103
|
+
isCompacting={isCompacting}
|
|
104
|
+
isGoalEvaluating={isGoalEvaluating}
|
|
103
105
|
sendMessage={sendMessage}
|
|
104
106
|
abortMessage={abortMessage}
|
|
105
107
|
mcpServers={mcpServers}
|
|
@@ -74,11 +74,17 @@ export const HelpView: React.FC<HelpViewProps> = ({
|
|
|
74
74
|
{ key: "Ctrl+B", description: "Background current task" },
|
|
75
75
|
{ key: "Ctrl+V", description: "Paste image" },
|
|
76
76
|
{ key: "Ctrl+J", description: "Newline" },
|
|
77
|
+
{ key: "Ctrl+A", description: "Cursor to line start" },
|
|
78
|
+
{ key: "Ctrl+E", description: "Cursor to line end" },
|
|
79
|
+
{ key: "Ctrl+U", description: "Delete to line start" },
|
|
80
|
+
{ key: "Ctrl+K", description: "Delete to line end" },
|
|
81
|
+
{ key: "Ctrl+W", description: "Delete word before cursor" },
|
|
77
82
|
{ key: "Shift+Tab", description: "Cycle permission mode" },
|
|
78
83
|
{
|
|
79
84
|
key: "Esc",
|
|
80
85
|
description: "Interrupt AI or command / Cancel selector / Close help",
|
|
81
86
|
},
|
|
87
|
+
{ key: "Esc ×2", description: "Clear input (idle)" },
|
|
82
88
|
];
|
|
83
89
|
|
|
84
90
|
const footerText = [
|
|
@@ -31,6 +31,8 @@ export const INPUT_PLACEHOLDER_TEXT_PREFIX = INPUT_PLACEHOLDER_TEXT.substring(
|
|
|
31
31
|
export interface InputBoxProps {
|
|
32
32
|
isLoading?: boolean;
|
|
33
33
|
isCommandRunning?: boolean;
|
|
34
|
+
isCompacting?: boolean;
|
|
35
|
+
isGoalEvaluating?: boolean;
|
|
34
36
|
workdir?: string;
|
|
35
37
|
sendMessage?: (
|
|
36
38
|
message: string,
|
|
@@ -54,6 +56,10 @@ export interface InputBoxProps {
|
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
export const InputBox: React.FC<InputBoxProps> = ({
|
|
59
|
+
isLoading,
|
|
60
|
+
isCommandRunning,
|
|
61
|
+
isCompacting,
|
|
62
|
+
isGoalEvaluating,
|
|
57
63
|
sendMessage = () => {},
|
|
58
64
|
abortMessage = () => {},
|
|
59
65
|
mcpServers = [],
|
|
@@ -92,6 +98,15 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
92
98
|
|
|
93
99
|
const hasQueuedMessages = (queuedMessages?.length ?? 0) > 0;
|
|
94
100
|
|
|
101
|
+
// Idle means no AI work in flight. Esc double-press clear only applies when
|
|
102
|
+
// idle; while busy, Esc keeps its abort semantics.
|
|
103
|
+
const isIdle = !(
|
|
104
|
+
isLoading ||
|
|
105
|
+
isCommandRunning ||
|
|
106
|
+
isCompacting ||
|
|
107
|
+
isGoalEvaluating
|
|
108
|
+
);
|
|
109
|
+
|
|
95
110
|
const onRecallQueuedMessage = useCallback(() => {
|
|
96
111
|
const msg = recallQueuedMessage();
|
|
97
112
|
if (msg) {
|
|
@@ -146,6 +161,8 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
146
161
|
setPermissionMode,
|
|
147
162
|
// BTW state
|
|
148
163
|
btwState,
|
|
164
|
+
// Esc double-press clear pending
|
|
165
|
+
escClearPending,
|
|
149
166
|
// Main handler
|
|
150
167
|
handleInput,
|
|
151
168
|
// Manager ready state
|
|
@@ -166,6 +183,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
166
183
|
workdir: workingDirectory,
|
|
167
184
|
getFullMessageThread,
|
|
168
185
|
hasQueuedMessages,
|
|
186
|
+
isIdle,
|
|
169
187
|
onRecallQueuedMessage,
|
|
170
188
|
});
|
|
171
189
|
|
|
@@ -342,6 +360,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
342
360
|
showPluginManager ||
|
|
343
361
|
showWorkflowManager || (
|
|
344
362
|
<Box flexDirection="column">
|
|
363
|
+
{escClearPending && <Text color="gray">再次按 Esc 清空输入</Text>}
|
|
345
364
|
<Box
|
|
346
365
|
borderStyle="single"
|
|
347
366
|
borderColor="gray"
|
|
@@ -3,6 +3,7 @@ import { Box, Text, useInput } from "ink";
|
|
|
3
3
|
import type { Message } from "wave-agent-sdk";
|
|
4
4
|
import { getMessageContent } from "wave-agent-sdk";
|
|
5
5
|
import { rewindSelectorReducer } from "../reducers/rewindSelectorReducer.js";
|
|
6
|
+
import { isUserCheckpointMessage } from "../utils/rewindCheckpoints.js";
|
|
6
7
|
|
|
7
8
|
export interface RewindCommandProps {
|
|
8
9
|
messages: Message[];
|
|
@@ -32,10 +33,11 @@ export const RewindCommand: React.FC<RewindCommandProps> = ({
|
|
|
32
33
|
}
|
|
33
34
|
}, [getFullMessageThread]);
|
|
34
35
|
|
|
35
|
-
// Filter user messages as checkpoints, excluding meta messages
|
|
36
|
+
// Filter user messages as checkpoints, excluding meta messages and
|
|
37
|
+
// system-generated user-role messages (task notifications, hook injections)
|
|
36
38
|
const checkpoints = messages
|
|
37
39
|
.map((msg, index) => ({ msg, index }))
|
|
38
|
-
.filter(({ msg }) => msg
|
|
40
|
+
.filter(({ msg }) => isUserCheckpointMessage(msg));
|
|
39
41
|
|
|
40
42
|
const MAX_VISIBLE_ITEMS = 3;
|
|
41
43
|
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
inputReducer,
|
|
5
5
|
initialState,
|
|
6
6
|
InputManagerCallbacks,
|
|
7
|
+
ESC_DOUBLE_PRESS_TIMEOUT_MS,
|
|
7
8
|
} from "../managers/inputReducer.js";
|
|
8
9
|
import {
|
|
9
10
|
searchFiles as searchFilesUtil,
|
|
@@ -48,6 +49,7 @@ export const useInputManager = (
|
|
|
48
49
|
onClearMessages,
|
|
49
50
|
onCompact,
|
|
50
51
|
onGoalCommand,
|
|
52
|
+
isIdle: isIdleProp,
|
|
51
53
|
} = callbacks;
|
|
52
54
|
|
|
53
55
|
// Handle debounced file search
|
|
@@ -70,25 +72,15 @@ export const useInputManager = (
|
|
|
70
72
|
}
|
|
71
73
|
}, [state.showFileSelector, state.fileSearchQuery]);
|
|
72
74
|
|
|
73
|
-
//
|
|
75
|
+
// Auto-expire the double-Esc clear pending flag after the timeout window
|
|
76
|
+
// (aligned with Claude Code's useDoublePress timeout-based expiry).
|
|
74
77
|
useEffect(() => {
|
|
75
|
-
if (state.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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]);
|
|
78
|
+
if (!state.escClearPending) return;
|
|
79
|
+
const timer = setTimeout(() => {
|
|
80
|
+
dispatch({ type: "RESET_ESC_CLEAR_PENDING" });
|
|
81
|
+
}, ESC_DOUBLE_PRESS_TIMEOUT_MS);
|
|
82
|
+
return () => clearTimeout(timer);
|
|
83
|
+
}, [state.escClearPending]);
|
|
92
84
|
|
|
93
85
|
// Sync state changes with callbacks
|
|
94
86
|
useEffect(() => {
|
|
@@ -127,6 +119,16 @@ export const useInputManager = (
|
|
|
127
119
|
case "ABORT_MESSAGE":
|
|
128
120
|
onAbortMessage?.();
|
|
129
121
|
break;
|
|
122
|
+
case "SAVE_PROMPT_HISTORY":
|
|
123
|
+
PromptHistoryManager.addEntry(
|
|
124
|
+
effect.content,
|
|
125
|
+
sessionId,
|
|
126
|
+
effect.longTextMap,
|
|
127
|
+
workdir,
|
|
128
|
+
).catch((err: unknown) => {
|
|
129
|
+
logger?.error("Failed to save prompt history", err);
|
|
130
|
+
});
|
|
131
|
+
break;
|
|
130
132
|
case "BACKGROUND_CURRENT_TASK":
|
|
131
133
|
onBackgroundCurrentTask?.();
|
|
132
134
|
break;
|
|
@@ -501,10 +503,11 @@ export const useInputManager = (
|
|
|
501
503
|
key: {} as Key,
|
|
502
504
|
hasSlashCommand: (cmd) => !!onHasSlashCommand?.(cmd),
|
|
503
505
|
hasQueuedMessages: hasQueuedMessagesProp ?? false,
|
|
506
|
+
isIdle: isIdleProp ?? false,
|
|
504
507
|
},
|
|
505
508
|
});
|
|
506
509
|
},
|
|
507
|
-
[onHasSlashCommand, hasQueuedMessagesProp],
|
|
510
|
+
[onHasSlashCommand, hasQueuedMessagesProp, isIdleProp],
|
|
508
511
|
);
|
|
509
512
|
|
|
510
513
|
const handleSubmit = useCallback(async () => {
|
|
@@ -515,9 +518,10 @@ export const useInputManager = (
|
|
|
515
518
|
key: { return: true } as Key,
|
|
516
519
|
hasSlashCommand: (cmd) => !!onHasSlashCommand?.(cmd),
|
|
517
520
|
hasQueuedMessages: hasQueuedMessagesProp ?? false,
|
|
521
|
+
isIdle: isIdleProp ?? false,
|
|
518
522
|
},
|
|
519
523
|
});
|
|
520
|
-
}, [onHasSlashCommand, hasQueuedMessagesProp]);
|
|
524
|
+
}, [onHasSlashCommand, hasQueuedMessagesProp, isIdleProp]);
|
|
521
525
|
|
|
522
526
|
const expandLongTextPlaceholders = useCallback(
|
|
523
527
|
(text: string) => {
|
|
@@ -539,11 +543,12 @@ export const useInputManager = (
|
|
|
539
543
|
key,
|
|
540
544
|
hasSlashCommand: (cmd) => !!onHasSlashCommand?.(cmd),
|
|
541
545
|
hasQueuedMessages: hasQueuedMessagesProp ?? false,
|
|
546
|
+
isIdle: isIdleProp ?? false,
|
|
542
547
|
},
|
|
543
548
|
});
|
|
544
549
|
return true;
|
|
545
550
|
},
|
|
546
|
-
[onHasSlashCommand, hasQueuedMessagesProp],
|
|
551
|
+
[onHasSlashCommand, hasQueuedMessagesProp, isIdleProp],
|
|
547
552
|
);
|
|
548
553
|
|
|
549
554
|
return {
|
|
@@ -572,6 +577,7 @@ export const useInputManager = (
|
|
|
572
577
|
permissionMode: state.permissionMode,
|
|
573
578
|
attachedImages: state.attachedImages,
|
|
574
579
|
btwState: state.btwState,
|
|
580
|
+
escClearPending: state.escClearPending,
|
|
575
581
|
isManagerReady: true,
|
|
576
582
|
|
|
577
583
|
// Methods
|
|
@@ -292,17 +292,13 @@ export const handlePasteInput = (
|
|
|
292
292
|
input: string,
|
|
293
293
|
): void => {
|
|
294
294
|
const inputString = input;
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
if (isPasteOperation) {
|
|
301
|
-
// Dispatch a single action type; the reducer determines start vs append
|
|
302
|
-
// by checking pasteBuffer, avoiding stale state issues
|
|
295
|
+
|
|
296
|
+
if (inputString.length > 1) {
|
|
297
|
+
// Multi-char chunk: insert immediately (\r → \n normalizes CRLF
|
|
298
|
+
// terminals), matching the reducer's HANDLE_KEY path.
|
|
303
299
|
dispatch({
|
|
304
|
-
type: "
|
|
305
|
-
payload:
|
|
300
|
+
type: "INSERT_TEXT_WITH_PLACEHOLDER",
|
|
301
|
+
payload: inputString.replace(/\r/g, "\n"),
|
|
306
302
|
});
|
|
307
303
|
} else {
|
|
308
304
|
let char = inputString;
|