viberag 0.1.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.
Files changed (151) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +219 -0
  3. package/dist/cli/__tests__/mcp-setup.test.d.ts +6 -0
  4. package/dist/cli/__tests__/mcp-setup.test.js +597 -0
  5. package/dist/cli/app.d.ts +2 -0
  6. package/dist/cli/app.js +238 -0
  7. package/dist/cli/commands/handlers.d.ts +57 -0
  8. package/dist/cli/commands/handlers.js +231 -0
  9. package/dist/cli/commands/index.d.ts +2 -0
  10. package/dist/cli/commands/index.js +2 -0
  11. package/dist/cli/commands/mcp-setup.d.ts +107 -0
  12. package/dist/cli/commands/mcp-setup.js +509 -0
  13. package/dist/cli/commands/useRagCommands.d.ts +23 -0
  14. package/dist/cli/commands/useRagCommands.js +180 -0
  15. package/dist/cli/components/CleanWizard.d.ts +17 -0
  16. package/dist/cli/components/CleanWizard.js +169 -0
  17. package/dist/cli/components/InitWizard.d.ts +20 -0
  18. package/dist/cli/components/InitWizard.js +370 -0
  19. package/dist/cli/components/McpSetupWizard.d.ts +37 -0
  20. package/dist/cli/components/McpSetupWizard.js +387 -0
  21. package/dist/cli/components/SearchResultsDisplay.d.ts +13 -0
  22. package/dist/cli/components/SearchResultsDisplay.js +130 -0
  23. package/dist/cli/components/WelcomeBanner.d.ts +10 -0
  24. package/dist/cli/components/WelcomeBanner.js +26 -0
  25. package/dist/cli/components/index.d.ts +1 -0
  26. package/dist/cli/components/index.js +1 -0
  27. package/dist/cli/data/mcp-editors.d.ts +80 -0
  28. package/dist/cli/data/mcp-editors.js +270 -0
  29. package/dist/cli/index.d.ts +2 -0
  30. package/dist/cli/index.js +26 -0
  31. package/dist/cli-bundle.cjs +5269 -0
  32. package/dist/common/commands/terminalSetup.d.ts +2 -0
  33. package/dist/common/commands/terminalSetup.js +144 -0
  34. package/dist/common/components/CommandSuggestions.d.ts +9 -0
  35. package/dist/common/components/CommandSuggestions.js +20 -0
  36. package/dist/common/components/StaticWithResize.d.ts +23 -0
  37. package/dist/common/components/StaticWithResize.js +62 -0
  38. package/dist/common/components/StatusBar.d.ts +8 -0
  39. package/dist/common/components/StatusBar.js +64 -0
  40. package/dist/common/components/TextInput.d.ts +12 -0
  41. package/dist/common/components/TextInput.js +239 -0
  42. package/dist/common/components/index.d.ts +3 -0
  43. package/dist/common/components/index.js +3 -0
  44. package/dist/common/hooks/index.d.ts +4 -0
  45. package/dist/common/hooks/index.js +4 -0
  46. package/dist/common/hooks/useCommandHistory.d.ts +7 -0
  47. package/dist/common/hooks/useCommandHistory.js +51 -0
  48. package/dist/common/hooks/useCtrlC.d.ts +9 -0
  49. package/dist/common/hooks/useCtrlC.js +40 -0
  50. package/dist/common/hooks/useKittyKeyboard.d.ts +10 -0
  51. package/dist/common/hooks/useKittyKeyboard.js +26 -0
  52. package/dist/common/hooks/useStaticOutputBuffer.d.ts +31 -0
  53. package/dist/common/hooks/useStaticOutputBuffer.js +58 -0
  54. package/dist/common/hooks/useTerminalResize.d.ts +28 -0
  55. package/dist/common/hooks/useTerminalResize.js +51 -0
  56. package/dist/common/hooks/useTextBuffer.d.ts +13 -0
  57. package/dist/common/hooks/useTextBuffer.js +165 -0
  58. package/dist/common/index.d.ts +13 -0
  59. package/dist/common/index.js +17 -0
  60. package/dist/common/types.d.ts +162 -0
  61. package/dist/common/types.js +1 -0
  62. package/dist/mcp/index.d.ts +12 -0
  63. package/dist/mcp/index.js +66 -0
  64. package/dist/mcp/server.d.ts +25 -0
  65. package/dist/mcp/server.js +837 -0
  66. package/dist/mcp/watcher.d.ts +86 -0
  67. package/dist/mcp/watcher.js +334 -0
  68. package/dist/rag/__tests__/grammar-smoke.test.d.ts +9 -0
  69. package/dist/rag/__tests__/grammar-smoke.test.js +161 -0
  70. package/dist/rag/__tests__/helpers.d.ts +30 -0
  71. package/dist/rag/__tests__/helpers.js +67 -0
  72. package/dist/rag/__tests__/merkle.test.d.ts +5 -0
  73. package/dist/rag/__tests__/merkle.test.js +161 -0
  74. package/dist/rag/__tests__/metadata-extraction.test.d.ts +10 -0
  75. package/dist/rag/__tests__/metadata-extraction.test.js +202 -0
  76. package/dist/rag/__tests__/multi-language.test.d.ts +13 -0
  77. package/dist/rag/__tests__/multi-language.test.js +535 -0
  78. package/dist/rag/__tests__/rag.test.d.ts +10 -0
  79. package/dist/rag/__tests__/rag.test.js +311 -0
  80. package/dist/rag/__tests__/search-exhaustive.test.d.ts +9 -0
  81. package/dist/rag/__tests__/search-exhaustive.test.js +87 -0
  82. package/dist/rag/__tests__/search-filters.test.d.ts +10 -0
  83. package/dist/rag/__tests__/search-filters.test.js +250 -0
  84. package/dist/rag/__tests__/search-modes.test.d.ts +8 -0
  85. package/dist/rag/__tests__/search-modes.test.js +133 -0
  86. package/dist/rag/config/index.d.ts +61 -0
  87. package/dist/rag/config/index.js +111 -0
  88. package/dist/rag/constants.d.ts +41 -0
  89. package/dist/rag/constants.js +57 -0
  90. package/dist/rag/embeddings/fastembed.d.ts +62 -0
  91. package/dist/rag/embeddings/fastembed.js +124 -0
  92. package/dist/rag/embeddings/gemini.d.ts +26 -0
  93. package/dist/rag/embeddings/gemini.js +116 -0
  94. package/dist/rag/embeddings/index.d.ts +10 -0
  95. package/dist/rag/embeddings/index.js +9 -0
  96. package/dist/rag/embeddings/local-4b.d.ts +28 -0
  97. package/dist/rag/embeddings/local-4b.js +51 -0
  98. package/dist/rag/embeddings/local.d.ts +29 -0
  99. package/dist/rag/embeddings/local.js +119 -0
  100. package/dist/rag/embeddings/mistral.d.ts +22 -0
  101. package/dist/rag/embeddings/mistral.js +85 -0
  102. package/dist/rag/embeddings/openai.d.ts +22 -0
  103. package/dist/rag/embeddings/openai.js +85 -0
  104. package/dist/rag/embeddings/types.d.ts +37 -0
  105. package/dist/rag/embeddings/types.js +1 -0
  106. package/dist/rag/gitignore/index.d.ts +57 -0
  107. package/dist/rag/gitignore/index.js +178 -0
  108. package/dist/rag/index.d.ts +15 -0
  109. package/dist/rag/index.js +25 -0
  110. package/dist/rag/indexer/chunker.d.ts +129 -0
  111. package/dist/rag/indexer/chunker.js +1352 -0
  112. package/dist/rag/indexer/index.d.ts +6 -0
  113. package/dist/rag/indexer/index.js +6 -0
  114. package/dist/rag/indexer/indexer.d.ts +73 -0
  115. package/dist/rag/indexer/indexer.js +356 -0
  116. package/dist/rag/indexer/types.d.ts +68 -0
  117. package/dist/rag/indexer/types.js +47 -0
  118. package/dist/rag/logger/index.d.ts +20 -0
  119. package/dist/rag/logger/index.js +75 -0
  120. package/dist/rag/manifest/index.d.ts +50 -0
  121. package/dist/rag/manifest/index.js +97 -0
  122. package/dist/rag/merkle/diff.d.ts +26 -0
  123. package/dist/rag/merkle/diff.js +95 -0
  124. package/dist/rag/merkle/hash.d.ts +34 -0
  125. package/dist/rag/merkle/hash.js +165 -0
  126. package/dist/rag/merkle/index.d.ts +68 -0
  127. package/dist/rag/merkle/index.js +298 -0
  128. package/dist/rag/merkle/node.d.ts +51 -0
  129. package/dist/rag/merkle/node.js +69 -0
  130. package/dist/rag/search/filters.d.ts +21 -0
  131. package/dist/rag/search/filters.js +100 -0
  132. package/dist/rag/search/fts.d.ts +32 -0
  133. package/dist/rag/search/fts.js +61 -0
  134. package/dist/rag/search/hybrid.d.ts +17 -0
  135. package/dist/rag/search/hybrid.js +58 -0
  136. package/dist/rag/search/index.d.ts +89 -0
  137. package/dist/rag/search/index.js +367 -0
  138. package/dist/rag/search/types.d.ts +130 -0
  139. package/dist/rag/search/types.js +4 -0
  140. package/dist/rag/search/vector.d.ts +25 -0
  141. package/dist/rag/search/vector.js +44 -0
  142. package/dist/rag/storage/index.d.ts +92 -0
  143. package/dist/rag/storage/index.js +287 -0
  144. package/dist/rag/storage/lancedb-native.d.ts +7 -0
  145. package/dist/rag/storage/lancedb-native.js +10 -0
  146. package/dist/rag/storage/schema.d.ts +23 -0
  147. package/dist/rag/storage/schema.js +50 -0
  148. package/dist/rag/storage/types.d.ts +100 -0
  149. package/dist/rag/storage/types.js +68 -0
  150. package/package.json +67 -0
  151. package/scripts/check-node-version.js +37 -0
@@ -0,0 +1,2 @@
1
+ export declare function setupTerminal(): Promise<string>;
2
+ export { setupTerminal as setupVSCodeTerminal };
@@ -0,0 +1,144 @@
1
+ import { homedir, platform } from 'node:os';
2
+ import { join, dirname } from 'node:path';
3
+ import { readFile, writeFile, mkdir, access, constants } from 'node:fs/promises';
4
+ const EDITORS = [
5
+ { name: 'VS Code', folder: 'Code' },
6
+ { name: 'VS Code Insiders', folder: 'Code - Insiders' },
7
+ { name: 'Cursor', folder: 'Cursor' },
8
+ { name: 'Windsurf', folder: 'Windsurf' },
9
+ { name: 'VSCodium', folder: 'VSCodium' },
10
+ ];
11
+ // ESC + LF - actual control character bytes
12
+ // \u001B = ESC (0x1B), \u000A = LF (0x0A)
13
+ const KEYBINDING = {
14
+ key: 'shift+enter',
15
+ command: 'workbench.action.terminal.sendSequence',
16
+ args: { text: '\u001B\u000A' },
17
+ when: 'terminalFocus',
18
+ };
19
+ function detectTerminal() {
20
+ const termProgram = process.env['TERM_PROGRAM'];
21
+ const term = process.env['TERM'];
22
+ if (termProgram === 'vscode')
23
+ return 'vscode';
24
+ if (termProgram === 'iTerm.app')
25
+ return 'iterm';
26
+ if (termProgram === 'Apple_Terminal')
27
+ return 'apple_terminal';
28
+ if (termProgram === 'WezTerm')
29
+ return 'wezterm';
30
+ if (term?.includes('kitty'))
31
+ return 'kitty';
32
+ return 'unknown';
33
+ }
34
+ function getKeybindingsPath(folder) {
35
+ const home = homedir();
36
+ switch (platform()) {
37
+ case 'darwin':
38
+ return join(home, 'Library/Application Support', folder, 'User/keybindings.json');
39
+ case 'win32':
40
+ return join(process.env['APPDATA'] ?? home, folder, 'User/keybindings.json');
41
+ default:
42
+ return join(home, '.config', folder, 'User/keybindings.json');
43
+ }
44
+ }
45
+ async function setupVSCodeEditors() {
46
+ const configured = [];
47
+ for (const editor of EDITORS) {
48
+ const path = getKeybindingsPath(editor.folder);
49
+ const userDir = dirname(path);
50
+ try {
51
+ await access(userDir, constants.F_OK);
52
+ }
53
+ catch {
54
+ continue;
55
+ }
56
+ let keybindings = [];
57
+ try {
58
+ const content = await readFile(path, 'utf-8');
59
+ const parsed = JSON.parse(content);
60
+ if (Array.isArray(parsed)) {
61
+ keybindings = parsed;
62
+ }
63
+ }
64
+ catch {
65
+ await mkdir(dirname(path), { recursive: true });
66
+ }
67
+ keybindings = keybindings.filter((b) => {
68
+ if (typeof b !== 'object' || b === null)
69
+ return true;
70
+ const kb = b;
71
+ return !(kb['key'] === 'shift+enter' &&
72
+ kb['command'] === 'workbench.action.terminal.sendSequence');
73
+ });
74
+ keybindings.push(KEYBINDING);
75
+ await writeFile(path, JSON.stringify(keybindings, null, 2));
76
+ configured.push(editor.name);
77
+ }
78
+ if (configured.length === 0) {
79
+ return '';
80
+ }
81
+ return `Configured Shift+Enter for: ${configured.join(', ')}\n\nRestart your terminal to apply.`;
82
+ }
83
+ export async function setupTerminal() {
84
+ const terminal = detectTerminal();
85
+ switch (terminal) {
86
+ case 'vscode':
87
+ return setupVSCodeEditors();
88
+ case 'iterm':
89
+ return `iTerm2 detected.
90
+
91
+ To enable Shift+Enter for newlines:
92
+
93
+ 1. Open iTerm2 → Settings (⌘,) → Profiles
94
+ 2. Select your profile → Keys tab → General sub-tab
95
+ 3. Under "Report modifiers using CSI u", select "Yes"
96
+ 4. Restart iTerm2
97
+
98
+ Once enabled, Shift+Enter and Alt+Enter will insert newlines.
99
+
100
+ Alternative methods that work now:
101
+ - \\ + Enter (backslash then Enter)
102
+ - Ctrl+J`;
103
+ case 'kitty':
104
+ return `Kitty detected - Shift+Enter should work natively.
105
+
106
+ If not working, try:
107
+ - \\ + Enter (backslash then Enter)
108
+ - Ctrl+J`;
109
+ case 'wezterm':
110
+ return `WezTerm detected - Shift+Enter should work natively.
111
+
112
+ If not working, try:
113
+ - \\ + Enter (backslash then Enter)
114
+ - Ctrl+J`;
115
+ case 'apple_terminal':
116
+ return `macOS Terminal detected.
117
+
118
+ Shift+Enter is not supported in Terminal.app.
119
+
120
+ Use these methods instead:
121
+ - \\ + Enter (backslash then Enter)
122
+ - Ctrl+J
123
+ - Option+Enter
124
+
125
+ Consider using iTerm2 or running in VS Code for Shift+Enter support.`;
126
+ default: {
127
+ // Try VS Code editors anyway
128
+ const vsResult = await setupVSCodeEditors();
129
+ if (vsResult) {
130
+ return vsResult;
131
+ }
132
+ return `Unknown terminal (TERM_PROGRAM=${process.env['TERM_PROGRAM'] ?? 'unset'}).
133
+
134
+ Try these methods:
135
+ - \\ + Enter (backslash then Enter)
136
+ - Ctrl+J
137
+ - Option+Enter
138
+
139
+ Run /terminal-setup in VS Code to configure Shift+Enter.`;
140
+ }
141
+ }
142
+ }
143
+ // Keep old export for backward compatibility during transition
144
+ export { setupTerminal as setupVSCodeTerminal };
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import type { CommandInfo } from '../types.js';
3
+ type Props = {
4
+ suggestions: CommandInfo[];
5
+ selectedIndex: number;
6
+ visible: boolean;
7
+ };
8
+ export default function CommandSuggestions({ suggestions, selectedIndex, visible, }: Props): React.JSX.Element | null;
9
+ export {};
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ export default function CommandSuggestions({ suggestions, selectedIndex, visible, }) {
4
+ if (!visible || suggestions.length === 0) {
5
+ return null;
6
+ }
7
+ // Calculate max command width for alignment
8
+ const maxCmdWidth = Math.max(...suggestions.map(s => s.command.length));
9
+ return (React.createElement(Box, { flexDirection: "column", marginLeft: 2, marginTop: 0 }, suggestions.map((suggestion, index) => {
10
+ const isSelected = index === selectedIndex;
11
+ const paddedCmd = suggestion.command.padEnd(maxCmdWidth);
12
+ return (React.createElement(Box, { key: suggestion.command },
13
+ React.createElement(Text, { color: isSelected ? 'cyan' : undefined, inverse: isSelected },
14
+ ' ',
15
+ paddedCmd),
16
+ React.createElement(Text, { dimColor: !isSelected, color: isSelected ? 'gray' : undefined },
17
+ ' ',
18
+ suggestion.description)));
19
+ })));
20
+ }
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ export type StaticWithResizeProps<T extends {
3
+ id: string;
4
+ }> = {
5
+ /**
6
+ * Items to render statically.
7
+ */
8
+ items: T[];
9
+ /**
10
+ * Render function for each item.
11
+ */
12
+ children: (item: T, index: number) => React.ReactNode;
13
+ };
14
+ /**
15
+ * Drop-in replacement for Ink's <Static> that handles terminal resize.
16
+ *
17
+ * On resize, clears the terminal and forces Static to remount,
18
+ * which causes Ink to re-render all items with proper formatting.
19
+ */
20
+ export declare function StaticWithResize<T extends {
21
+ id: string;
22
+ }>({ items, children, }: StaticWithResizeProps<T>): React.ReactElement;
23
+ export default StaticWithResize;
@@ -0,0 +1,62 @@
1
+ import React, { useState, useRef, useEffect, useCallback } from 'react';
2
+ import { Static, useStdout } from 'ink';
3
+ import { useTerminalResize } from '../hooks/useTerminalResize.js';
4
+ /**
5
+ * ANSI escape sequences for terminal control.
6
+ */
7
+ const ANSI = {
8
+ /** Clear visible screen */
9
+ CLEAR_SCREEN: '\x1B[2J',
10
+ /** Clear scrollback buffer */
11
+ CLEAR_SCROLLBACK: '\x1B[3J',
12
+ /** Move cursor to top-left */
13
+ CURSOR_HOME: '\x1B[H',
14
+ };
15
+ /**
16
+ * Drop-in replacement for Ink's <Static> that handles terminal resize.
17
+ *
18
+ * On resize, clears the terminal and forces Static to remount,
19
+ * which causes Ink to re-render all items with proper formatting.
20
+ */
21
+ export function StaticWithResize({ items, children, }) {
22
+ const { stdout } = useStdout();
23
+ const [generation, setGeneration] = useState(0);
24
+ const resizeTimerRef = useRef(null);
25
+ // Debounced resize handler that clears terminal and forces remount
26
+ const handleResize = useCallback(() => {
27
+ if (resizeTimerRef.current) {
28
+ clearTimeout(resizeTimerRef.current);
29
+ }
30
+ resizeTimerRef.current = setTimeout(() => {
31
+ if (stdout && items.length > 0) {
32
+ // Clear terminal
33
+ stdout.write(ANSI.CLEAR_SCREEN + ANSI.CLEAR_SCROLLBACK + ANSI.CURSOR_HOME);
34
+ // Force Static to remount by changing key - this re-renders all items
35
+ setGeneration(g => g + 1);
36
+ }
37
+ resizeTimerRef.current = null;
38
+ }, 150); // Wait for resize to fully settle
39
+ }, [stdout, items.length]);
40
+ // Cleanup timer on unmount
41
+ useEffect(() => {
42
+ return () => {
43
+ if (resizeTimerRef.current) {
44
+ clearTimeout(resizeTimerRef.current);
45
+ }
46
+ };
47
+ }, []);
48
+ // Handle resize events
49
+ useTerminalResize({
50
+ onResize: (dimensions, previousDimensions) => {
51
+ if (previousDimensions &&
52
+ (dimensions.rows !== previousDimensions.rows ||
53
+ dimensions.columns !== previousDimensions.columns)) {
54
+ handleResize();
55
+ }
56
+ },
57
+ debounceMs: 100,
58
+ });
59
+ // Key forces remount of Static, causing all items to re-render
60
+ return (React.createElement(Static, { key: generation, items: items }, (item, index) => children(item, index)));
61
+ }
62
+ export default StaticWithResize;
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import type { AppStatus, IndexDisplayStats } from '../types.js';
3
+ type Props = {
4
+ status: AppStatus;
5
+ stats: IndexDisplayStats | null | undefined;
6
+ };
7
+ export default function StatusBar({ status, stats }: Props): React.JSX.Element;
8
+ export {};
@@ -0,0 +1,64 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ /** Braille dots spinner frames */
4
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
5
+ /**
6
+ * Simple spinner component using interval-based animation.
7
+ */
8
+ function Spinner({ color }) {
9
+ const [frame, setFrame] = useState(0);
10
+ useEffect(() => {
11
+ const timer = setInterval(() => {
12
+ setFrame(f => (f + 1) % SPINNER_FRAMES.length);
13
+ }, 80);
14
+ return () => clearInterval(timer);
15
+ }, []);
16
+ return React.createElement(Text, { color: color },
17
+ SPINNER_FRAMES[frame],
18
+ " ");
19
+ }
20
+ /**
21
+ * Format status message for display.
22
+ */
23
+ function formatStatus(status) {
24
+ switch (status.state) {
25
+ case 'ready':
26
+ return { text: 'Ready', color: 'green', showSpinner: false };
27
+ case 'indexing': {
28
+ if (status.total === 0) {
29
+ return { text: `${status.stage}`, color: 'cyan', showSpinner: true };
30
+ }
31
+ const percent = Math.round((status.current / status.total) * 100);
32
+ return {
33
+ text: `${status.stage} ${status.current}/${status.total} (${percent}%)`,
34
+ color: 'cyan',
35
+ showSpinner: true,
36
+ };
37
+ }
38
+ case 'searching':
39
+ return { text: 'Searching', color: 'cyan', showSpinner: true };
40
+ case 'warning':
41
+ return { text: status.message, color: 'yellow', showSpinner: false };
42
+ }
43
+ }
44
+ /**
45
+ * Format stats for display.
46
+ */
47
+ function formatStats(stats) {
48
+ if (stats === undefined) {
49
+ return 'Loading...';
50
+ }
51
+ if (stats === null) {
52
+ return 'Not indexed';
53
+ }
54
+ return `${stats.totalFiles} files · ${stats.totalChunks} chunks`;
55
+ }
56
+ export default function StatusBar({ status, stats }) {
57
+ const { text, color, showSpinner } = formatStatus(status);
58
+ const statsText = formatStats(stats);
59
+ return (React.createElement(Box, { paddingX: 1, justifyContent: "space-between" },
60
+ React.createElement(Box, null,
61
+ showSpinner && React.createElement(Spinner, { color: color }),
62
+ React.createElement(Text, { color: color }, text)),
63
+ React.createElement(Text, { dimColor: true }, statsText)));
64
+ }
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ import type { CommandInfo } from '../types.js';
3
+ type Props = {
4
+ onSubmit: (text: string) => void;
5
+ onCtrlC: () => void;
6
+ commands?: CommandInfo[];
7
+ navigateHistoryUp?: () => string | null;
8
+ navigateHistoryDown?: () => string | null;
9
+ resetHistoryIndex?: () => void;
10
+ };
11
+ export default function TextInput({ onSubmit, onCtrlC, commands, navigateHistoryUp, navigateHistoryDown, resetHistoryIndex, }: Props): React.JSX.Element;
12
+ export {};
@@ -0,0 +1,239 @@
1
+ import React, { useState, useMemo, useRef } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { useTextBuffer } from '../hooks/useTextBuffer.js';
4
+ import CommandSuggestions from './CommandSuggestions.js';
5
+ function filterCommands(input, commands) {
6
+ if (!input.startsWith('/'))
7
+ return [];
8
+ const lower = input.toLowerCase();
9
+ return commands.filter(cmd => cmd.command.toLowerCase().startsWith(lower));
10
+ }
11
+ export default function TextInput({ onSubmit, onCtrlC, commands = [], navigateHistoryUp, navigateHistoryDown, resetHistoryIndex, }) {
12
+ const { state, insertChar, insertNewline, deleteChar, deleteCharBefore, moveCursor, clear, setText, getText, isEmpty, } = useTextBuffer();
13
+ const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(0);
14
+ // Track ESC key timing for ESC+Enter newline detection
15
+ // (used by /terminal-setup which sends \u001b\r for Shift+Enter)
16
+ const escPressedTimeRef = useRef(0);
17
+ // Filter commands based on current input
18
+ const currentText = getText();
19
+ const suggestions = useMemo(() => filterCommands(currentText, commands), [currentText, commands]);
20
+ const suggestionsVisible = suggestions.length > 0;
21
+ useInput((input, key) => {
22
+ // Reset history index when user types
23
+ if (input && !key.ctrl && !key.meta) {
24
+ resetHistoryIndex?.();
25
+ }
26
+ // Ctrl+C handling
27
+ if (key.ctrl && input === 'c') {
28
+ if (!isEmpty()) {
29
+ clear();
30
+ setSelectedSuggestionIndex(0);
31
+ }
32
+ // Always call onCtrlC so the quit timer starts even when clearing text
33
+ onCtrlC();
34
+ return;
35
+ }
36
+ // === CSI u detection for iTerm2/Kitty with enhanced keyboard mode ===
37
+ // When "Report modifiers using CSI u" is enabled:
38
+ // Shift+Enter sends: \x1b[13;2u (keycode=13/Enter, modifier=2/Shift)
39
+ // Alt+Enter sends: \x1b[13;3u (modifier=3/Alt)
40
+ // Ink strips the ESC prefix, leaving: [13;2u or [13;3u
41
+ if (input === '[13;2u' || input === '[13;3u') {
42
+ insertNewline();
43
+ setSelectedSuggestionIndex(0);
44
+ escPressedTimeRef.current = 0;
45
+ return;
46
+ }
47
+ // === ESC+LF/CR detection for VS Code (via /terminal-setup) ===
48
+ // VS Code keybinding sends ESC (0x1B) + LF (0x0A) or ESC + CR (0x0D)
49
+ // Ink's parseKeypress doesn't recognize this 2-byte sequence, so:
50
+ // - keypress.name stays empty → key.return = false
51
+ // - ESC is stripped → input = '\n' or '\r'
52
+ // Detection: input is newline char BUT key.return is false (unrecognized sequence)
53
+ if ((input === '\n' || input === '\r') &&
54
+ !key.return &&
55
+ !key.ctrl &&
56
+ !key.shift) {
57
+ insertNewline();
58
+ setSelectedSuggestionIndex(0);
59
+ escPressedTimeRef.current = 0;
60
+ return;
61
+ }
62
+ // Also handle when escape/meta flag is set (fallback for terminals that do parse it)
63
+ if ((key.escape || key.meta) && (input === '\n' || input === '\r')) {
64
+ insertNewline();
65
+ setSelectedSuggestionIndex(0);
66
+ escPressedTimeRef.current = 0;
67
+ return;
68
+ }
69
+ // Ctrl+J - explicit handling for terminals that report it as ctrl+j
70
+ // (Most terminals send raw LF which is caught by the ESC+LF detection above)
71
+ if (key.ctrl && input === 'j') {
72
+ insertNewline();
73
+ setSelectedSuggestionIndex(0);
74
+ return;
75
+ }
76
+ // Tab - accept suggestion
77
+ if (key.tab && suggestionsVisible) {
78
+ const selected = suggestions[selectedSuggestionIndex];
79
+ if (selected) {
80
+ setText(selected.command);
81
+ setSelectedSuggestionIndex(0);
82
+ }
83
+ return;
84
+ }
85
+ // Submit on Enter (without shift) - but check for ESC+Enter and backslash first
86
+ if (key.return && !key.shift && !key.meta) {
87
+ // Timing-based ESC+Enter detection (for terminals that send separately)
88
+ if (Date.now() - escPressedTimeRef.current < 150) {
89
+ insertNewline();
90
+ setSelectedSuggestionIndex(0);
91
+ escPressedTimeRef.current = 0;
92
+ return;
93
+ }
94
+ // Method 1: Backslash + Enter (universal newline)
95
+ const currentLine = state.lines[state.cursorLine] ?? '';
96
+ const charBeforeCursor = currentLine[state.cursorCol - 1];
97
+ if (charBeforeCursor === '\\') {
98
+ // Remove backslash and insert newline
99
+ deleteCharBefore();
100
+ insertNewline();
101
+ setSelectedSuggestionIndex(0);
102
+ return;
103
+ }
104
+ let text = getText();
105
+ // If suggestions visible, complete AND submit in one action
106
+ if (suggestionsVisible) {
107
+ const selected = suggestions[selectedSuggestionIndex];
108
+ if (selected) {
109
+ text = selected.command;
110
+ }
111
+ }
112
+ // Submit
113
+ if (text.trim()) {
114
+ onSubmit(text);
115
+ clear();
116
+ setSelectedSuggestionIndex(0);
117
+ }
118
+ return;
119
+ }
120
+ // Method 2: Shift+Enter for newline (Kitty terminals)
121
+ if (key.return && key.shift) {
122
+ insertNewline();
123
+ setSelectedSuggestionIndex(0);
124
+ return;
125
+ }
126
+ // Method 4: Alt/Option+Enter for newline (most terminals)
127
+ if (key.return && key.meta) {
128
+ insertNewline();
129
+ setSelectedSuggestionIndex(0);
130
+ return;
131
+ }
132
+ // Backspace
133
+ if (key.backspace) {
134
+ deleteChar();
135
+ setSelectedSuggestionIndex(0);
136
+ return;
137
+ }
138
+ // Delete key
139
+ if (key.delete) {
140
+ deleteChar();
141
+ setSelectedSuggestionIndex(0);
142
+ return;
143
+ }
144
+ // Up arrow
145
+ if (key.upArrow) {
146
+ if (suggestionsVisible) {
147
+ // Navigate suggestions
148
+ setSelectedSuggestionIndex(i => Math.max(0, i - 1));
149
+ }
150
+ else if (isEmpty() && navigateHistoryUp) {
151
+ // Navigate history when input is empty
152
+ const prev = navigateHistoryUp();
153
+ if (prev !== null) {
154
+ setText(prev);
155
+ }
156
+ }
157
+ else {
158
+ // Move cursor in multi-line
159
+ moveCursor('up');
160
+ }
161
+ return;
162
+ }
163
+ // Down arrow
164
+ if (key.downArrow) {
165
+ if (suggestionsVisible) {
166
+ // Navigate suggestions
167
+ setSelectedSuggestionIndex(i => Math.min(suggestions.length - 1, i + 1));
168
+ }
169
+ else if (isEmpty() && navigateHistoryDown) {
170
+ // Navigate history when input is empty
171
+ const next = navigateHistoryDown();
172
+ if (next !== null) {
173
+ setText(next);
174
+ }
175
+ else {
176
+ clear();
177
+ }
178
+ }
179
+ else {
180
+ // Move cursor in multi-line
181
+ moveCursor('down');
182
+ }
183
+ return;
184
+ }
185
+ // Left/Right arrows
186
+ if (key.leftArrow) {
187
+ moveCursor('left');
188
+ return;
189
+ }
190
+ if (key.rightArrow) {
191
+ moveCursor('right');
192
+ return;
193
+ }
194
+ // Plain ESC key - clear input after a delay
195
+ // (ESC+LF for Shift+Enter is already handled above)
196
+ if (key.escape && !input) {
197
+ escPressedTimeRef.current = Date.now();
198
+ setTimeout(() => {
199
+ if (escPressedTimeRef.current !== 0) {
200
+ clear();
201
+ setSelectedSuggestionIndex(0);
202
+ escPressedTimeRef.current = 0;
203
+ }
204
+ }, 150);
205
+ return;
206
+ }
207
+ // Regular character input (ignore control sequences and newlines)
208
+ if (input &&
209
+ !key.ctrl &&
210
+ !key.meta &&
211
+ !key.escape &&
212
+ input !== '\n' &&
213
+ input !== '\r') {
214
+ insertChar(input);
215
+ setSelectedSuggestionIndex(0);
216
+ }
217
+ });
218
+ // Render lines with cursor
219
+ return (React.createElement(Box, { flexDirection: "column" },
220
+ React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "blue", paddingX: 1 }, state.lines.map((line, lineIdx) => {
221
+ const isCurrentLine = lineIdx === state.cursorLine;
222
+ const prefix = lineIdx === 0 ? '> ' : ' ';
223
+ if (!isCurrentLine) {
224
+ return (React.createElement(Box, { key: lineIdx },
225
+ React.createElement(Text, { color: "blue" }, prefix),
226
+ React.createElement(Text, null, line || ' ')));
227
+ }
228
+ // Current line with cursor - render as three parts on same line
229
+ const beforeCursor = line.slice(0, state.cursorCol);
230
+ const cursorChar = line[state.cursorCol] ?? ' ';
231
+ const afterCursor = line.slice(state.cursorCol + 1);
232
+ return (React.createElement(Box, { key: lineIdx },
233
+ React.createElement(Text, { color: "blue" }, prefix),
234
+ React.createElement(Text, null, beforeCursor),
235
+ React.createElement(Text, { inverse: true }, cursorChar),
236
+ React.createElement(Text, null, afterCursor)));
237
+ })),
238
+ React.createElement(CommandSuggestions, { suggestions: suggestions, selectedIndex: selectedSuggestionIndex, visible: suggestionsVisible })));
239
+ }
@@ -0,0 +1,3 @@
1
+ export { default as TextInput } from './TextInput.js';
2
+ export { default as StatusBar } from './StatusBar.js';
3
+ export { default as CommandSuggestions } from './CommandSuggestions.js';
@@ -0,0 +1,3 @@
1
+ export { default as TextInput } from './TextInput.js';
2
+ export { default as StatusBar } from './StatusBar.js';
3
+ export { default as CommandSuggestions } from './CommandSuggestions.js';
@@ -0,0 +1,4 @@
1
+ export { useCtrlC } from './useCtrlC.js';
2
+ export { useCommandHistory } from './useCommandHistory.js';
3
+ export { useTextBuffer } from './useTextBuffer.js';
4
+ export { useKittyKeyboard } from './useKittyKeyboard.js';
@@ -0,0 +1,4 @@
1
+ export { useCtrlC } from './useCtrlC.js';
2
+ export { useCommandHistory } from './useCommandHistory.js';
3
+ export { useTextBuffer } from './useTextBuffer.js';
4
+ export { useKittyKeyboard } from './useKittyKeyboard.js';
@@ -0,0 +1,7 @@
1
+ export declare function useCommandHistory(): {
2
+ history: string[];
3
+ addToHistory: (command: string) => void;
4
+ navigateUp: () => string | null;
5
+ navigateDown: () => string | null;
6
+ resetIndex: () => void;
7
+ };
@@ -0,0 +1,51 @@
1
+ import { useState, useCallback, useRef } from 'react';
2
+ export function useCommandHistory() {
3
+ const [history, setHistory] = useState([]);
4
+ const indexRef = useRef(-1);
5
+ const addToHistory = useCallback((command) => {
6
+ // Don't add empty or duplicate consecutive commands
7
+ setHistory(prev => {
8
+ if (!command.trim() || prev[prev.length - 1] === command) {
9
+ return prev;
10
+ }
11
+ return [...prev, command];
12
+ });
13
+ // Reset index when adding new command
14
+ indexRef.current = -1;
15
+ }, []);
16
+ const navigateUp = useCallback(() => {
17
+ if (history.length === 0)
18
+ return null;
19
+ // If not navigating yet, start from the end
20
+ if (indexRef.current === -1) {
21
+ indexRef.current = history.length - 1;
22
+ }
23
+ else if (indexRef.current > 0) {
24
+ indexRef.current -= 1;
25
+ }
26
+ return history[indexRef.current] ?? null;
27
+ }, [history]);
28
+ const navigateDown = useCallback(() => {
29
+ if (history.length === 0 || indexRef.current === -1)
30
+ return null;
31
+ if (indexRef.current < history.length - 1) {
32
+ indexRef.current += 1;
33
+ return history[indexRef.current] ?? null;
34
+ }
35
+ else {
36
+ // At the end, reset and return null to clear input
37
+ indexRef.current = -1;
38
+ return null;
39
+ }
40
+ }, [history]);
41
+ const resetIndex = useCallback(() => {
42
+ indexRef.current = -1;
43
+ }, []);
44
+ return {
45
+ history,
46
+ addToHistory,
47
+ navigateUp,
48
+ navigateDown,
49
+ resetIndex,
50
+ };
51
+ }