@ebowwa/coder 0.7.64 → 0.7.66
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/index.js +36233 -32
- package/dist/interfaces/ui/terminal/cli/index.js +34318 -158
- package/dist/interfaces/ui/terminal/native/README.md +53 -0
- package/dist/interfaces/ui/terminal/native/claude_code_native.darwin-x64.node +0 -0
- package/dist/interfaces/ui/terminal/native/claude_code_native.dylib +0 -0
- package/dist/interfaces/ui/terminal/native/index.d.ts +0 -0
- package/dist/interfaces/ui/terminal/native/index.darwin-arm64.node +0 -0
- package/dist/interfaces/ui/terminal/native/index.js +43 -0
- package/dist/interfaces/ui/terminal/native/index.node +0 -0
- package/dist/interfaces/ui/terminal/native/package.json +34 -0
- package/dist/native/README.md +53 -0
- package/dist/native/claude_code_native.darwin-x64.node +0 -0
- package/dist/native/claude_code_native.dylib +0 -0
- package/dist/native/index.d.ts +0 -480
- package/dist/native/index.darwin-arm64.node +0 -0
- package/dist/native/index.js +43 -1625
- package/dist/native/index.node +0 -0
- package/dist/native/package.json +34 -0
- package/native/index.darwin-arm64.node +0 -0
- package/native/index.js +33 -19
- package/package.json +3 -2
- package/packages/src/core/agent-loop/__tests__/compaction.test.ts +17 -14
- package/packages/src/core/agent-loop/compaction.ts +6 -2
- package/packages/src/core/agent-loop/index.ts +2 -0
- package/packages/src/core/agent-loop/loop-state.ts +1 -1
- package/packages/src/core/agent-loop/turn-executor.ts +4 -0
- package/packages/src/core/agent-loop/types.ts +4 -0
- package/packages/src/core/api-client-impl.ts +377 -176
- package/packages/src/core/cognitive-security/hooks.ts +2 -1
- package/packages/src/core/config/todo +7 -0
- package/packages/src/core/context/__tests__/integration.test.ts +334 -0
- package/packages/src/core/context/compaction.ts +170 -0
- package/packages/src/core/context/constants.ts +58 -0
- package/packages/src/core/context/extraction.ts +85 -0
- package/packages/src/core/context/index.ts +66 -0
- package/packages/src/core/context/summarization.ts +251 -0
- package/packages/src/core/context/token-estimation.ts +98 -0
- package/packages/src/core/context/types.ts +59 -0
- package/packages/src/core/models.ts +81 -4
- package/packages/src/core/normalizers/todo +5 -1
- package/packages/src/core/providers/README.md +230 -0
- package/packages/src/core/providers/__tests__/providers.test.ts +135 -0
- package/packages/src/core/providers/index.ts +419 -0
- package/packages/src/core/providers/types.ts +132 -0
- package/packages/src/core/retry.ts +10 -0
- package/packages/src/ecosystem/tools/index.ts +174 -0
- package/packages/src/index.ts +23 -2
- package/packages/src/interfaces/ui/index.ts +17 -20
- package/packages/src/interfaces/ui/spinner.ts +2 -2
- package/packages/src/interfaces/ui/terminal/bridge/index.ts +370 -0
- package/packages/src/interfaces/ui/terminal/bridge/ipc.ts +829 -0
- package/packages/src/interfaces/ui/terminal/bridge/screen-export.ts +968 -0
- package/packages/src/interfaces/ui/terminal/bridge/types.ts +226 -0
- package/packages/src/interfaces/ui/terminal/bridge/useBridge.ts +210 -0
- package/packages/src/interfaces/ui/terminal/cli/bootstrap.ts +132 -0
- package/packages/src/interfaces/ui/terminal/cli/index.ts +200 -13
- package/packages/src/interfaces/ui/terminal/cli/interactive/index.ts +110 -0
- package/packages/src/interfaces/ui/terminal/cli/interactive/input-handler.ts +402 -0
- package/packages/src/interfaces/ui/terminal/cli/interactive/interactive-runner.ts +820 -0
- package/packages/src/interfaces/ui/terminal/cli/interactive/message-store.ts +299 -0
- package/packages/src/interfaces/ui/terminal/cli/interactive/types.ts +274 -0
- package/packages/src/interfaces/ui/terminal/shared/index.ts +13 -0
- package/packages/src/interfaces/ui/terminal/shared/query.ts +9 -3
- package/packages/src/interfaces/ui/terminal/shared/setup.ts +5 -1
- package/packages/src/interfaces/ui/terminal/shared/spinner-frames.ts +73 -0
- package/packages/src/interfaces/ui/terminal/shared/status-line.ts +10 -2
- package/packages/src/native/index.ts +404 -27
- package/packages/src/native/tui_v2_types.ts +39 -0
- package/packages/src/teammates/coordination.test.ts +279 -0
- package/packages/src/teammates/coordination.ts +646 -0
- package/packages/src/teammates/index.ts +95 -25
- package/packages/src/teammates/integration.test.ts +272 -0
- package/packages/src/teammates/runner.test.ts +235 -0
- package/packages/src/teammates/runner.ts +750 -0
- package/packages/src/teammates/schemas.ts +673 -0
- package/packages/src/types/index.ts +1 -0
- package/packages/src/core/context-compaction.ts +0 -578
- package/packages/src/interfaces/ui/Screenshot 2026-03-02 at 9.23.10/342/200/257PM.png +0 -0
- package/packages/src/interfaces/ui/Screenshot 2026-03-03 at 10.55.11/342/200/257AM.png +0 -0
- package/packages/src/interfaces/ui/terminal/tui/HelpPanel.tsx +0 -262
- package/packages/src/interfaces/ui/terminal/tui/InputContext.tsx +0 -232
- package/packages/src/interfaces/ui/terminal/tui/InputField.tsx +0 -62
- package/packages/src/interfaces/ui/terminal/tui/InteractiveTUI.tsx +0 -537
- package/packages/src/interfaces/ui/terminal/tui/MessageArea.tsx +0 -107
- package/packages/src/interfaces/ui/terminal/tui/MessageStore.tsx +0 -240
- package/packages/src/interfaces/ui/terminal/tui/StatusBar.tsx +0 -54
- package/packages/src/interfaces/ui/terminal/tui/commands.ts +0 -438
- package/packages/src/interfaces/ui/terminal/tui/components/InteractiveElements.tsx +0 -584
- package/packages/src/interfaces/ui/terminal/tui/components/MultilineInput.tsx +0 -614
- package/packages/src/interfaces/ui/terminal/tui/components/PaneManager.tsx +0 -333
- package/packages/src/interfaces/ui/terminal/tui/components/Sidebar.tsx +0 -604
- package/packages/src/interfaces/ui/terminal/tui/components/index.ts +0 -118
- package/packages/src/interfaces/ui/terminal/tui/console.ts +0 -49
- package/packages/src/interfaces/ui/terminal/tui/index.ts +0 -90
- package/packages/src/interfaces/ui/terminal/tui/run.tsx +0 -42
- package/packages/src/interfaces/ui/terminal/tui/spinner.ts +0 -69
- package/packages/src/interfaces/ui/terminal/tui/tui-app.tsx +0 -390
- package/packages/src/interfaces/ui/terminal/tui/tui-footer.ts +0 -422
- package/packages/src/interfaces/ui/terminal/tui/types.ts +0 -186
- package/packages/src/interfaces/ui/terminal/tui/useInputHandler.ts +0 -104
- package/packages/src/interfaces/ui/terminal/tui/useNativeInput.ts +0 -239
|
@@ -1,614 +0,0 @@
|
|
|
1
|
-
/** @jsx React.createElement */
|
|
2
|
-
/**
|
|
3
|
-
* Multiline Input Component
|
|
4
|
-
* Enhanced text input with:
|
|
5
|
-
* - Multi-line support (Ctrl+Enter for newline)
|
|
6
|
-
* - Command autocomplete
|
|
7
|
-
* - Input history navigation
|
|
8
|
-
* - Syntax highlighting for input
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import React, { useState, useCallback, useRef, useEffect } from "react";
|
|
12
|
-
import { Box, Text, useStdout } from "ink";
|
|
13
|
-
|
|
14
|
-
// ============================================
|
|
15
|
-
// TYPES
|
|
16
|
-
// ============================================
|
|
17
|
-
|
|
18
|
-
export interface AutocompleteSuggestion {
|
|
19
|
-
id: string;
|
|
20
|
-
label: string;
|
|
21
|
-
description?: string;
|
|
22
|
-
category?: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface MultilineInputProps {
|
|
26
|
-
/** Current input value */
|
|
27
|
-
value: string;
|
|
28
|
-
/** Cursor position (0 = start) */
|
|
29
|
-
cursorPos: number;
|
|
30
|
-
/** Current line for multiline (0-based) */
|
|
31
|
-
currentLine?: number;
|
|
32
|
-
/** Placeholder text */
|
|
33
|
-
placeholder?: string;
|
|
34
|
-
/** Whether input is active */
|
|
35
|
-
isActive?: boolean;
|
|
36
|
-
/** Whether to show autocomplete */
|
|
37
|
-
showAutocomplete?: boolean;
|
|
38
|
-
/** Autocomplete suggestions */
|
|
39
|
-
autocompleteSuggestions?: AutocompleteSuggestion[];
|
|
40
|
-
/** Selected autocomplete index */
|
|
41
|
-
autocompleteIndex?: number;
|
|
42
|
-
/** Input history for navigation */
|
|
43
|
-
inputHistory?: string[];
|
|
44
|
-
/** History index (-1 = none) */
|
|
45
|
-
historyIndex?: number;
|
|
46
|
-
/** Called when input changes */
|
|
47
|
-
onChange?: (value: string, cursorPos: number, currentLine: number) => void;
|
|
48
|
-
/** Called when submit (Enter on single line, Ctrl+Enter on multiline) */
|
|
49
|
-
onSubmit?: (value: string) => void;
|
|
50
|
-
/** Called when requesting autocomplete */
|
|
51
|
-
onRequestAutocomplete?: (prefix: string) => AutocompleteSuggestion[];
|
|
52
|
-
/** Max lines before scrolling */
|
|
53
|
-
maxLines?: number;
|
|
54
|
-
/** Show line numbers */
|
|
55
|
-
showLineNumbers?: boolean;
|
|
56
|
-
/** Width override */
|
|
57
|
-
width?: number;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export interface MultilineInputState {
|
|
61
|
-
value: string;
|
|
62
|
-
cursorPos: number;
|
|
63
|
-
currentLine: number;
|
|
64
|
-
historyIndex: number;
|
|
65
|
-
savedInput: string;
|
|
66
|
-
showAutocomplete: boolean;
|
|
67
|
-
autocompleteSuggestions: AutocompleteSuggestion[];
|
|
68
|
-
autocompleteIndex: number;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export interface UseMultilineInputOptions {
|
|
72
|
-
isActive?: boolean;
|
|
73
|
-
onSubmit?: (value: string) => void;
|
|
74
|
-
onRequestAutocomplete?: (prefix: string) => AutocompleteSuggestion[];
|
|
75
|
-
inputHistory?: string[];
|
|
76
|
-
initialHistory?: string[];
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// ============================================
|
|
80
|
-
// COMMAND AUTOCOMPLETE DEFINITIONS
|
|
81
|
-
// ============================================
|
|
82
|
-
|
|
83
|
-
const COMMAND_SUGGESTIONS: AutocompleteSuggestion[] = [
|
|
84
|
-
// Session commands
|
|
85
|
-
{ id: "/help", label: "/help", description: "Show help", category: "session" },
|
|
86
|
-
{ id: "/exit", label: "/exit", description: "Exit session", category: "session" },
|
|
87
|
-
{ id: "/new", label: "/new", description: "Start new session", category: "session" },
|
|
88
|
-
{ id: "/clear", label: "/clear", description: "Clear conversation", category: "session" },
|
|
89
|
-
{ id: "/status", label: "/status", description: "Show session status", category: "session" },
|
|
90
|
-
{ id: "/cost", label: "/cost", description: "Show total cost", category: "session" },
|
|
91
|
-
|
|
92
|
-
// Model commands
|
|
93
|
-
{ id: "/model", label: "/model", description: "Switch model", category: "model" },
|
|
94
|
-
{ id: "/models", label: "/models", description: "List available models", category: "model" },
|
|
95
|
-
{ id: "/tools", label: "/tools", description: "List available tools", category: "model" },
|
|
96
|
-
|
|
97
|
-
// Context commands
|
|
98
|
-
{ id: "/compact", label: "/compact", description: "Force context compaction", category: "context" },
|
|
99
|
-
{ id: "/export", label: "/export", description: "Export session", category: "context" },
|
|
100
|
-
{ id: "/checkpoint", label: "/checkpoint", description: "Save checkpoint", category: "context" },
|
|
101
|
-
{ id: "/checkpoints", label: "/checkpoints", description: "List checkpoints", category: "context" },
|
|
102
|
-
{ id: "/restore", label: "/restore", description: "Restore checkpoint", category: "context" },
|
|
103
|
-
{ id: "/undo", label: "/undo", description: "Undo last action", category: "context" },
|
|
104
|
-
{ id: "/redo", label: "/redo", description: "Redo action", category: "context" },
|
|
105
|
-
|
|
106
|
-
// Session management
|
|
107
|
-
{ id: "/resume", label: "/resume", description: "Resume session", category: "sessions" },
|
|
108
|
-
{ id: "/sessions", label: "/sessions", description: "List sessions", category: "sessions" },
|
|
109
|
-
];
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Filter autocomplete suggestions based on prefix
|
|
113
|
-
*/
|
|
114
|
-
export function filterSuggestions(prefix: string): AutocompleteSuggestion[] {
|
|
115
|
-
if (!prefix || !prefix.startsWith("/")) {
|
|
116
|
-
return [];
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const query = prefix.toLowerCase();
|
|
120
|
-
return COMMAND_SUGGESTIONS.filter((cmd) => {
|
|
121
|
-
return (
|
|
122
|
-
cmd.id.toLowerCase().startsWith(query) ||
|
|
123
|
-
cmd.label.toLowerCase().includes(query) ||
|
|
124
|
-
(cmd.description?.toLowerCase().includes(query) ?? false)
|
|
125
|
-
);
|
|
126
|
-
}).slice(0, 5);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Syntax highlight code block
|
|
131
|
-
* Returns highlighted segments for display
|
|
132
|
-
*/
|
|
133
|
-
export function highlightSyntax(
|
|
134
|
-
text: string
|
|
135
|
-
): Array<{ text: string; color: string }> {
|
|
136
|
-
const segments: Array<{ text: string; color: string }> = [];
|
|
137
|
-
|
|
138
|
-
// Code block detection (``` ... ```)
|
|
139
|
-
const codeBlockRegex = /```[\s\S]*?```/g;
|
|
140
|
-
|
|
141
|
-
// Keywords
|
|
142
|
-
const keywords =
|
|
143
|
-
/\b(const|let|var|function|return|if|else|for|while|class|interface|type|import|export|from|async|await)\b/g;
|
|
144
|
-
|
|
145
|
-
// Strings
|
|
146
|
-
const strings = /(["'`])(?:(?!\1)[\s\S])*?\1/g;
|
|
147
|
-
|
|
148
|
-
// Numbers
|
|
149
|
-
const numbers = /\b(\d+\.?\d*)\b/g;
|
|
150
|
-
|
|
151
|
-
// Comments
|
|
152
|
-
const comments = /(\/\/.*$|\/\*[\s\S]*?\*\/|#.*$)/gm;
|
|
153
|
-
|
|
154
|
-
// Check for code blocks first
|
|
155
|
-
if (codeBlockRegex.test(text)) {
|
|
156
|
-
segments.push({ text, color: "green" });
|
|
157
|
-
return segments;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Otherwise return as-is
|
|
161
|
-
segments.push({ text, color: "white" });
|
|
162
|
-
return segments;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// ============================================
|
|
166
|
-
// MULTILINE INPUT COMPONENT
|
|
167
|
-
// ============================================
|
|
168
|
-
|
|
169
|
-
export function MultilineInput({
|
|
170
|
-
value,
|
|
171
|
-
cursorPos,
|
|
172
|
-
currentLine = 0,
|
|
173
|
-
placeholder = "Type your message... (/help for commands)",
|
|
174
|
-
isActive = true,
|
|
175
|
-
showAutocomplete = false,
|
|
176
|
-
autocompleteSuggestions = [],
|
|
177
|
-
autocompleteIndex = 0,
|
|
178
|
-
inputHistory = [],
|
|
179
|
-
historyIndex = -1,
|
|
180
|
-
onChange,
|
|
181
|
-
onSubmit,
|
|
182
|
-
maxLines = 5,
|
|
183
|
-
showLineNumbers = false,
|
|
184
|
-
width: propWidth,
|
|
185
|
-
}: MultilineInputProps) {
|
|
186
|
-
const { stdout } = useStdout();
|
|
187
|
-
const width = propWidth ?? stdout.columns ?? 80;
|
|
188
|
-
|
|
189
|
-
// Split value into lines
|
|
190
|
-
const lines = value.split("\n");
|
|
191
|
-
const displayLines = lines.slice(-maxLines);
|
|
192
|
-
|
|
193
|
-
// Calculate cursor display position
|
|
194
|
-
const cursorLineIndex = Math.min(currentLine, displayLines.length - 1);
|
|
195
|
-
const currentLineText = displayLines[cursorLineIndex] ?? "";
|
|
196
|
-
const cursorColInLine = cursorPos;
|
|
197
|
-
|
|
198
|
-
// Build line display
|
|
199
|
-
const renderLines = displayLines.map((line, i) => {
|
|
200
|
-
const lineNum = showLineNumbers
|
|
201
|
-
? `${String(lines.length - displayLines.length + i + 1).padStart(3, " ")} `
|
|
202
|
-
: "";
|
|
203
|
-
const isCurrentLine = i === cursorLineIndex;
|
|
204
|
-
|
|
205
|
-
if (isCurrentLine) {
|
|
206
|
-
const beforeCursor = line.slice(0, cursorColInLine);
|
|
207
|
-
const cursorChar = line[cursorColInLine] ?? " ";
|
|
208
|
-
const afterCursor = line.slice(cursorColInLine + 1);
|
|
209
|
-
|
|
210
|
-
return (
|
|
211
|
-
<Box key={i}>
|
|
212
|
-
<Text dimColor>{lineNum}</Text>
|
|
213
|
-
<Text>{beforeCursor}</Text>
|
|
214
|
-
<Text backgroundColor="cyan" color="black">
|
|
215
|
-
{cursorChar}
|
|
216
|
-
</Text>
|
|
217
|
-
<Text>{afterCursor}</Text>
|
|
218
|
-
</Box>
|
|
219
|
-
);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return (
|
|
223
|
-
<Box key={i}>
|
|
224
|
-
<Text dimColor>{lineNum}</Text>
|
|
225
|
-
<Text dimColor={line.length === 0}>{line || " "}</Text>
|
|
226
|
-
</Box>
|
|
227
|
-
);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
// Render autocomplete dropdown
|
|
231
|
-
const renderAutocomplete = () => {
|
|
232
|
-
if (!showAutocomplete || autocompleteSuggestions.length === 0) {
|
|
233
|
-
return null;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return (
|
|
237
|
-
<Box
|
|
238
|
-
flexDirection="column"
|
|
239
|
-
borderStyle="round"
|
|
240
|
-
borderColor="yellow"
|
|
241
|
-
paddingX={1}
|
|
242
|
-
marginTop={1}
|
|
243
|
-
>
|
|
244
|
-
<Text dimColor>Suggestions (Tab to select):</Text>
|
|
245
|
-
{autocompleteSuggestions.map((suggestion, i) => (
|
|
246
|
-
<Box key={suggestion.id}>
|
|
247
|
-
<Text
|
|
248
|
-
color={i === autocompleteIndex ? "yellow" : "white"}
|
|
249
|
-
bold={i === autocompleteIndex}
|
|
250
|
-
>
|
|
251
|
-
{suggestion.label}
|
|
252
|
-
</Text>
|
|
253
|
-
{suggestion.description && (
|
|
254
|
-
<Text dimColor> - {suggestion.description}</Text>
|
|
255
|
-
)}
|
|
256
|
-
</Box>
|
|
257
|
-
))}
|
|
258
|
-
</Box>
|
|
259
|
-
);
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
// Line indicator
|
|
263
|
-
const lineIndicator =
|
|
264
|
-
lines.length > 1 ? (
|
|
265
|
-
<Text dimColor> [Line {currentLine + 1}/{lines.length}]</Text>
|
|
266
|
-
) : null;
|
|
267
|
-
|
|
268
|
-
return (
|
|
269
|
-
<Box
|
|
270
|
-
flexDirection="column"
|
|
271
|
-
width="100%"
|
|
272
|
-
borderStyle="round"
|
|
273
|
-
borderColor={isActive ? "cyan" : "gray"}
|
|
274
|
-
paddingX={1}
|
|
275
|
-
>
|
|
276
|
-
{/* Input prompt */}
|
|
277
|
-
<Box>
|
|
278
|
-
<Text bold color="cyan">
|
|
279
|
-
You:
|
|
280
|
-
</Text>
|
|
281
|
-
{lineIndicator}
|
|
282
|
-
</Box>
|
|
283
|
-
|
|
284
|
-
{/* Multiline content */}
|
|
285
|
-
<Box flexDirection="column">
|
|
286
|
-
{lines.length === 0 || (lines.length === 1 && lines[0] === "") ? (
|
|
287
|
-
<Text dimColor>{placeholder}</Text>
|
|
288
|
-
) : (
|
|
289
|
-
renderLines
|
|
290
|
-
)}
|
|
291
|
-
</Box>
|
|
292
|
-
|
|
293
|
-
{/* Autocomplete dropdown */}
|
|
294
|
-
{renderAutocomplete()}
|
|
295
|
-
|
|
296
|
-
{/* Footer hints */}
|
|
297
|
-
<Box marginTop={1}>
|
|
298
|
-
<Text dimColor>
|
|
299
|
-
Ctrl+Enter: newline | up/down: history | Tab: autocomplete
|
|
300
|
-
</Text>
|
|
301
|
-
</Box>
|
|
302
|
-
</Box>
|
|
303
|
-
);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// ============================================
|
|
307
|
-
// INPUT HANDLER HOOK
|
|
308
|
-
// ============================================
|
|
309
|
-
|
|
310
|
-
export function useMultilineInputHandler({
|
|
311
|
-
isActive = true,
|
|
312
|
-
onSubmit,
|
|
313
|
-
onRequestAutocomplete,
|
|
314
|
-
inputHistory = [],
|
|
315
|
-
}: UseMultilineInputOptions) {
|
|
316
|
-
const [state, setState] = useState<MultilineInputState>({
|
|
317
|
-
value: "",
|
|
318
|
-
cursorPos: 0,
|
|
319
|
-
currentLine: 0,
|
|
320
|
-
historyIndex: -1,
|
|
321
|
-
savedInput: "",
|
|
322
|
-
showAutocomplete: false,
|
|
323
|
-
autocompleteSuggestions: [],
|
|
324
|
-
autocompleteIndex: 0,
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
const handleKeyEvent = useCallback(
|
|
328
|
-
(event: { code: string; ctrl?: boolean; shift?: boolean }) => {
|
|
329
|
-
if (!isActive) return "unhandled";
|
|
330
|
-
|
|
331
|
-
const { code, ctrl, shift } = event;
|
|
332
|
-
|
|
333
|
-
// Update helper
|
|
334
|
-
const update = (updates: Partial<MultilineInputState>) => {
|
|
335
|
-
setState((prev) => ({ ...prev, ...updates }));
|
|
336
|
-
};
|
|
337
|
-
|
|
338
|
-
// Get current state
|
|
339
|
-
const { value, cursorPos, currentLine, historyIndex, showAutocomplete } =
|
|
340
|
-
state;
|
|
341
|
-
|
|
342
|
-
// Split into lines for calculations
|
|
343
|
-
const lines = value.split("\n");
|
|
344
|
-
|
|
345
|
-
// Autocomplete navigation
|
|
346
|
-
if (showAutocomplete) {
|
|
347
|
-
if (code === "tab" || code === "down") {
|
|
348
|
-
const suggestions = state.autocompleteSuggestions;
|
|
349
|
-
const newIndex =
|
|
350
|
-
(state.autocompleteIndex + 1) % suggestions.length;
|
|
351
|
-
update({ autocompleteIndex: newIndex });
|
|
352
|
-
return "handled";
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
if (code === "up") {
|
|
356
|
-
const suggestions = state.autocompleteSuggestions;
|
|
357
|
-
const newIndex =
|
|
358
|
-
(state.autocompleteIndex - 1 + suggestions.length) %
|
|
359
|
-
suggestions.length;
|
|
360
|
-
update({ autocompleteIndex: newIndex });
|
|
361
|
-
return "handled";
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
if (code === "enter" || code === "return") {
|
|
365
|
-
const suggestion =
|
|
366
|
-
state.autocompleteSuggestions[state.autocompleteIndex];
|
|
367
|
-
if (suggestion) {
|
|
368
|
-
// Replace current word with suggestion
|
|
369
|
-
const words = value.split(" ");
|
|
370
|
-
words[words.length - 1] = suggestion.id;
|
|
371
|
-
const newValue = words.join(" ");
|
|
372
|
-
update({
|
|
373
|
-
value: newValue,
|
|
374
|
-
cursorPos: newValue.length,
|
|
375
|
-
showAutocomplete: false,
|
|
376
|
-
autocompleteSuggestions: [],
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
return "handled";
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
if (code === "escape") {
|
|
383
|
-
update({ showAutocomplete: false, autocompleteSuggestions: [] });
|
|
384
|
-
return "handled";
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// Submit (Enter without Ctrl = submit if single line, with Ctrl = always submit)
|
|
389
|
-
if (code === "enter" || code === "return") {
|
|
390
|
-
if (ctrl) {
|
|
391
|
-
// Ctrl+Enter = submit even in multiline
|
|
392
|
-
if (value.trim() && onSubmit) {
|
|
393
|
-
onSubmit(value);
|
|
394
|
-
update({
|
|
395
|
-
value: "",
|
|
396
|
-
cursorPos: 0,
|
|
397
|
-
currentLine: 0,
|
|
398
|
-
historyIndex: -1,
|
|
399
|
-
savedInput: "",
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
return "handled";
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// Regular Enter
|
|
406
|
-
if (lines.length === 1) {
|
|
407
|
-
// Single line mode - submit
|
|
408
|
-
if (value.trim() && onSubmit) {
|
|
409
|
-
onSubmit(value);
|
|
410
|
-
update({
|
|
411
|
-
value: "",
|
|
412
|
-
cursorPos: 0,
|
|
413
|
-
currentLine: 0,
|
|
414
|
-
historyIndex: -1,
|
|
415
|
-
savedInput: "",
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
} else {
|
|
419
|
-
// Multiline mode - insert newline
|
|
420
|
-
const newValue =
|
|
421
|
-
value.slice(0, cursorPos) + "\n" + value.slice(cursorPos);
|
|
422
|
-
const newCursorPos = cursorPos + 1;
|
|
423
|
-
update({
|
|
424
|
-
value: newValue,
|
|
425
|
-
cursorPos: newCursorPos,
|
|
426
|
-
currentLine: currentLine + 1,
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
|
-
return "handled";
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// Backspace
|
|
433
|
-
if (code === "backspace") {
|
|
434
|
-
if (cursorPos > 0) {
|
|
435
|
-
const newValue =
|
|
436
|
-
value.slice(0, cursorPos - 1) + value.slice(cursorPos);
|
|
437
|
-
const newCursorPos = cursorPos - 1;
|
|
438
|
-
const newCurrentLine =
|
|
439
|
-
newValue.slice(0, newCursorPos).split("\n").length - 1;
|
|
440
|
-
update({
|
|
441
|
-
value: newValue,
|
|
442
|
-
cursorPos: newCursorPos,
|
|
443
|
-
currentLine: newCurrentLine,
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
// Check for autocomplete
|
|
447
|
-
if (newValue.startsWith("/") && onRequestAutocomplete) {
|
|
448
|
-
const suggestions = onRequestAutocomplete(newValue);
|
|
449
|
-
update({
|
|
450
|
-
showAutocomplete: suggestions.length > 0,
|
|
451
|
-
autocompleteSuggestions: suggestions,
|
|
452
|
-
autocompleteIndex: 0,
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
return "handled";
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// Delete
|
|
460
|
-
if (code === "delete") {
|
|
461
|
-
if (cursorPos < value.length) {
|
|
462
|
-
const newValue =
|
|
463
|
-
value.slice(0, cursorPos) + value.slice(cursorPos + 1);
|
|
464
|
-
update({ value: newValue });
|
|
465
|
-
}
|
|
466
|
-
return "handled";
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
// Arrow keys
|
|
470
|
-
if (code === "left") {
|
|
471
|
-
update({ cursorPos: Math.max(0, cursorPos - 1) });
|
|
472
|
-
return "handled";
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
if (code === "right") {
|
|
476
|
-
update({ cursorPos: Math.min(value.length, cursorPos + 1) });
|
|
477
|
-
return "handled";
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
if (code === "up") {
|
|
481
|
-
// History navigation
|
|
482
|
-
if (inputHistory.length > 0) {
|
|
483
|
-
const newIndex =
|
|
484
|
-
historyIndex === -1 ? 0 : Math.min(historyIndex + 1, inputHistory.length - 1);
|
|
485
|
-
const historyValue = inputHistory[newIndex] ?? "";
|
|
486
|
-
update({
|
|
487
|
-
historyIndex: newIndex,
|
|
488
|
-
value: historyValue,
|
|
489
|
-
cursorPos: historyValue.length,
|
|
490
|
-
currentLine: historyValue.split("\n").length - 1,
|
|
491
|
-
});
|
|
492
|
-
}
|
|
493
|
-
return "handled";
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
if (code === "down") {
|
|
497
|
-
if (historyIndex > 0) {
|
|
498
|
-
const newIndex = historyIndex - 1;
|
|
499
|
-
const historyValue = inputHistory[newIndex] ?? "";
|
|
500
|
-
update({
|
|
501
|
-
historyIndex: newIndex,
|
|
502
|
-
value: historyValue,
|
|
503
|
-
cursorPos: historyValue.length,
|
|
504
|
-
currentLine: historyValue.split("\n").length - 1,
|
|
505
|
-
});
|
|
506
|
-
} else if (historyIndex === 0) {
|
|
507
|
-
// Restore saved input
|
|
508
|
-
update({
|
|
509
|
-
historyIndex: -1,
|
|
510
|
-
value: state.savedInput,
|
|
511
|
-
cursorPos: state.savedInput.length,
|
|
512
|
-
currentLine: state.savedInput.split("\n").length - 1,
|
|
513
|
-
});
|
|
514
|
-
}
|
|
515
|
-
return "handled";
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// Home/End
|
|
519
|
-
if (code === "home" || (code === "a" && ctrl)) {
|
|
520
|
-
// Go to start of current line
|
|
521
|
-
const lineStart =
|
|
522
|
-
value.slice(0, cursorPos).lastIndexOf("\n") + 1;
|
|
523
|
-
update({ cursorPos: lineStart });
|
|
524
|
-
return "handled";
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
if (code === "end" || (code === "e" && ctrl)) {
|
|
528
|
-
// Go to end of current line
|
|
529
|
-
const lineEnd = value.indexOf("\n", cursorPos);
|
|
530
|
-
update({ cursorPos: lineEnd === -1 ? value.length : lineEnd });
|
|
531
|
-
return "handled";
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// Ctrl+U - clear line
|
|
535
|
-
if (code === "u" && ctrl) {
|
|
536
|
-
const lineStart =
|
|
537
|
-
value.slice(0, cursorPos).lastIndexOf("\n") + 1;
|
|
538
|
-
const lineEnd = value.indexOf("\n", cursorPos);
|
|
539
|
-
const newValue =
|
|
540
|
-
value.slice(0, lineStart) +
|
|
541
|
-
(lineEnd === -1 ? "" : value.slice(lineEnd + 1));
|
|
542
|
-
update({
|
|
543
|
-
value: newValue,
|
|
544
|
-
cursorPos: lineStart,
|
|
545
|
-
});
|
|
546
|
-
return "handled";
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
// Ctrl+W - delete word
|
|
550
|
-
if (code === "w" && ctrl) {
|
|
551
|
-
const beforeCursor = value
|
|
552
|
-
.slice(0, cursorPos)
|
|
553
|
-
.replace(/\s+\S*$/, "");
|
|
554
|
-
const newValue = beforeCursor + value.slice(cursorPos);
|
|
555
|
-
update({
|
|
556
|
-
value: newValue,
|
|
557
|
-
cursorPos: beforeCursor.length,
|
|
558
|
-
});
|
|
559
|
-
return "handled";
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
// Regular printable character
|
|
563
|
-
if (code.length === 1 && !ctrl) {
|
|
564
|
-
const newValue =
|
|
565
|
-
value.slice(0, cursorPos) + code + value.slice(cursorPos);
|
|
566
|
-
const newCursorPos = cursorPos + 1;
|
|
567
|
-
|
|
568
|
-
update({
|
|
569
|
-
value: newValue,
|
|
570
|
-
cursorPos: newCursorPos,
|
|
571
|
-
historyIndex: -1, // Reset history navigation on typing
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
// Check for autocomplete trigger
|
|
575
|
-
if (newValue.startsWith("/") && onRequestAutocomplete) {
|
|
576
|
-
const suggestions = onRequestAutocomplete(newValue);
|
|
577
|
-
update({
|
|
578
|
-
showAutocomplete: suggestions.length > 0,
|
|
579
|
-
autocompleteSuggestions: suggestions,
|
|
580
|
-
autocompleteIndex: 0,
|
|
581
|
-
});
|
|
582
|
-
} else {
|
|
583
|
-
update({ showAutocomplete: false });
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
return "handled";
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
return "unhandled";
|
|
590
|
-
},
|
|
591
|
-
[isActive, onSubmit, onRequestAutocomplete, inputHistory, state]
|
|
592
|
-
);
|
|
593
|
-
|
|
594
|
-
return {
|
|
595
|
-
state,
|
|
596
|
-
handleKeyEvent,
|
|
597
|
-
// Convenience helpers
|
|
598
|
-
setValue: (value: string) =>
|
|
599
|
-
setState((prev) => ({ ...prev, value })),
|
|
600
|
-
reset: () =>
|
|
601
|
-
setState({
|
|
602
|
-
value: "",
|
|
603
|
-
cursorPos: 0,
|
|
604
|
-
currentLine: 0,
|
|
605
|
-
historyIndex: -1,
|
|
606
|
-
savedInput: "",
|
|
607
|
-
showAutocomplete: false,
|
|
608
|
-
autocompleteSuggestions: [],
|
|
609
|
-
autocompleteIndex: 0,
|
|
610
|
-
}),
|
|
611
|
-
};
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
export default MultilineInput;
|