@ebowwa/coder 0.7.64 → 0.7.65

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 (101) hide show
  1. package/dist/index.js +36168 -32
  2. package/dist/interfaces/ui/terminal/cli/index.js +34253 -158
  3. package/dist/interfaces/ui/terminal/native/README.md +53 -0
  4. package/dist/interfaces/ui/terminal/native/claude_code_native.darwin-x64.node +0 -0
  5. package/dist/interfaces/ui/terminal/native/claude_code_native.dylib +0 -0
  6. package/dist/interfaces/ui/terminal/native/index.d.ts +0 -0
  7. package/dist/interfaces/ui/terminal/native/index.darwin-arm64.node +0 -0
  8. package/dist/interfaces/ui/terminal/native/index.js +43 -0
  9. package/dist/interfaces/ui/terminal/native/index.node +0 -0
  10. package/dist/interfaces/ui/terminal/native/package.json +34 -0
  11. package/dist/native/README.md +53 -0
  12. package/dist/native/claude_code_native.darwin-x64.node +0 -0
  13. package/dist/native/claude_code_native.dylib +0 -0
  14. package/dist/native/index.d.ts +0 -480
  15. package/dist/native/index.darwin-arm64.node +0 -0
  16. package/dist/native/index.js +43 -1625
  17. package/dist/native/index.node +0 -0
  18. package/dist/native/package.json +34 -0
  19. package/native/index.darwin-arm64.node +0 -0
  20. package/native/index.js +33 -19
  21. package/package.json +3 -2
  22. package/packages/src/core/agent-loop/__tests__/compaction.test.ts +17 -14
  23. package/packages/src/core/agent-loop/compaction.ts +6 -2
  24. package/packages/src/core/agent-loop/index.ts +2 -0
  25. package/packages/src/core/agent-loop/loop-state.ts +1 -1
  26. package/packages/src/core/agent-loop/turn-executor.ts +4 -0
  27. package/packages/src/core/agent-loop/types.ts +4 -0
  28. package/packages/src/core/api-client-impl.ts +283 -173
  29. package/packages/src/core/cognitive-security/hooks.ts +2 -1
  30. package/packages/src/core/config/todo +7 -0
  31. package/packages/src/core/context/__tests__/integration.test.ts +334 -0
  32. package/packages/src/core/context/compaction.ts +170 -0
  33. package/packages/src/core/context/constants.ts +58 -0
  34. package/packages/src/core/context/extraction.ts +85 -0
  35. package/packages/src/core/context/index.ts +66 -0
  36. package/packages/src/core/context/summarization.ts +251 -0
  37. package/packages/src/core/context/token-estimation.ts +98 -0
  38. package/packages/src/core/context/types.ts +59 -0
  39. package/packages/src/core/models.ts +81 -4
  40. package/packages/src/core/normalizers/todo +5 -1
  41. package/packages/src/core/providers/README.md +230 -0
  42. package/packages/src/core/providers/__tests__/providers.test.ts +135 -0
  43. package/packages/src/core/providers/index.ts +419 -0
  44. package/packages/src/core/providers/types.ts +132 -0
  45. package/packages/src/core/retry.ts +10 -0
  46. package/packages/src/ecosystem/tools/index.ts +174 -0
  47. package/packages/src/index.ts +23 -2
  48. package/packages/src/interfaces/ui/index.ts +17 -20
  49. package/packages/src/interfaces/ui/spinner.ts +2 -2
  50. package/packages/src/interfaces/ui/terminal/bridge/index.ts +370 -0
  51. package/packages/src/interfaces/ui/terminal/bridge/ipc.ts +829 -0
  52. package/packages/src/interfaces/ui/terminal/bridge/screen-export.ts +968 -0
  53. package/packages/src/interfaces/ui/terminal/bridge/types.ts +226 -0
  54. package/packages/src/interfaces/ui/terminal/bridge/useBridge.ts +210 -0
  55. package/packages/src/interfaces/ui/terminal/cli/bootstrap.ts +132 -0
  56. package/packages/src/interfaces/ui/terminal/cli/index.ts +200 -13
  57. package/packages/src/interfaces/ui/terminal/cli/interactive/index.ts +110 -0
  58. package/packages/src/interfaces/ui/terminal/cli/interactive/input-handler.ts +393 -0
  59. package/packages/src/interfaces/ui/terminal/cli/interactive/interactive-runner.ts +820 -0
  60. package/packages/src/interfaces/ui/terminal/cli/interactive/message-store.ts +299 -0
  61. package/packages/src/interfaces/ui/terminal/cli/interactive/types.ts +274 -0
  62. package/packages/src/interfaces/ui/terminal/shared/index.ts +13 -0
  63. package/packages/src/interfaces/ui/terminal/shared/query.ts +9 -3
  64. package/packages/src/interfaces/ui/terminal/shared/setup.ts +5 -1
  65. package/packages/src/interfaces/ui/terminal/shared/spinner-frames.ts +73 -0
  66. package/packages/src/interfaces/ui/terminal/shared/status-line.ts +10 -2
  67. package/packages/src/native/index.ts +404 -27
  68. package/packages/src/native/tui_v2_types.ts +39 -0
  69. package/packages/src/teammates/coordination.test.ts +279 -0
  70. package/packages/src/teammates/coordination.ts +646 -0
  71. package/packages/src/teammates/index.ts +95 -25
  72. package/packages/src/teammates/integration.test.ts +272 -0
  73. package/packages/src/teammates/runner.test.ts +235 -0
  74. package/packages/src/teammates/runner.ts +750 -0
  75. package/packages/src/teammates/schemas.ts +673 -0
  76. package/packages/src/types/index.ts +1 -0
  77. package/packages/src/core/context-compaction.ts +0 -578
  78. package/packages/src/interfaces/ui/Screenshot 2026-03-02 at 9.23.10/342/200/257PM.png +0 -0
  79. package/packages/src/interfaces/ui/Screenshot 2026-03-03 at 10.55.11/342/200/257AM.png +0 -0
  80. package/packages/src/interfaces/ui/terminal/tui/HelpPanel.tsx +0 -262
  81. package/packages/src/interfaces/ui/terminal/tui/InputContext.tsx +0 -232
  82. package/packages/src/interfaces/ui/terminal/tui/InputField.tsx +0 -62
  83. package/packages/src/interfaces/ui/terminal/tui/InteractiveTUI.tsx +0 -537
  84. package/packages/src/interfaces/ui/terminal/tui/MessageArea.tsx +0 -107
  85. package/packages/src/interfaces/ui/terminal/tui/MessageStore.tsx +0 -240
  86. package/packages/src/interfaces/ui/terminal/tui/StatusBar.tsx +0 -54
  87. package/packages/src/interfaces/ui/terminal/tui/commands.ts +0 -438
  88. package/packages/src/interfaces/ui/terminal/tui/components/InteractiveElements.tsx +0 -584
  89. package/packages/src/interfaces/ui/terminal/tui/components/MultilineInput.tsx +0 -614
  90. package/packages/src/interfaces/ui/terminal/tui/components/PaneManager.tsx +0 -333
  91. package/packages/src/interfaces/ui/terminal/tui/components/Sidebar.tsx +0 -604
  92. package/packages/src/interfaces/ui/terminal/tui/components/index.ts +0 -118
  93. package/packages/src/interfaces/ui/terminal/tui/console.ts +0 -49
  94. package/packages/src/interfaces/ui/terminal/tui/index.ts +0 -90
  95. package/packages/src/interfaces/ui/terminal/tui/run.tsx +0 -42
  96. package/packages/src/interfaces/ui/terminal/tui/spinner.ts +0 -69
  97. package/packages/src/interfaces/ui/terminal/tui/tui-app.tsx +0 -390
  98. package/packages/src/interfaces/ui/terminal/tui/tui-footer.ts +0 -422
  99. package/packages/src/interfaces/ui/terminal/tui/types.ts +0 -186
  100. package/packages/src/interfaces/ui/terminal/tui/useInputHandler.ts +0 -104
  101. 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;