@plusonelabs/cue 0.0.38 → 0.0.39

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.
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Handles multi-line text navigation and editing
3
+ */
4
+ export class Cursor {
5
+ readonly offset: number;
6
+ readonly text: string;
7
+
8
+ constructor(text: string, offset: number = 0) {
9
+ this.text = text;
10
+ this.offset = Math.max(0, Math.min(text.length, offset));
11
+ }
12
+
13
+ static fromText(text: string, _columns: number, offset: number = 0): Cursor {
14
+ return new Cursor(text, offset);
15
+ }
16
+
17
+ // Navigation methods
18
+ left(): Cursor {
19
+ return new Cursor(this.text, this.offset - 1);
20
+ }
21
+
22
+ right(): Cursor {
23
+ return new Cursor(this.text, this.offset + 1);
24
+ }
25
+
26
+ up(): Cursor {
27
+ const lines = this.text.split('\n');
28
+ let currentLine = 0;
29
+ let currentCol = 0;
30
+ let charCount = 0;
31
+
32
+ // Find current line and column
33
+ for (let i = 0; i < lines.length; i++) {
34
+ const lineLength = lines[i].length;
35
+ if (charCount + lineLength >= this.offset) {
36
+ currentLine = i;
37
+ currentCol = this.offset - charCount;
38
+ break;
39
+ }
40
+ charCount += lineLength + 1; // +1 for newline
41
+ }
42
+
43
+ if (currentLine === 0) {
44
+ return new Cursor(this.text, 0);
45
+ }
46
+
47
+ // Move to previous line, maintaining column position if possible
48
+ const prevLine = lines[currentLine - 1];
49
+ const newCol = Math.min(currentCol, prevLine.length);
50
+
51
+ let newOffset = 0;
52
+ for (let i = 0; i < currentLine - 1; i++) {
53
+ newOffset += lines[i].length + 1;
54
+ }
55
+ newOffset += newCol;
56
+
57
+ return new Cursor(this.text, newOffset);
58
+ }
59
+
60
+ down(): Cursor {
61
+ const lines = this.text.split('\n');
62
+ let currentLine = 0;
63
+ let currentCol = 0;
64
+ let charCount = 0;
65
+
66
+ // Find current line and column
67
+ for (let i = 0; i < lines.length; i++) {
68
+ const lineLength = lines[i].length;
69
+ if (charCount + lineLength >= this.offset) {
70
+ currentLine = i;
71
+ currentCol = this.offset - charCount;
72
+ break;
73
+ }
74
+ charCount += lineLength + 1;
75
+ }
76
+
77
+ if (currentLine >= lines.length - 1) {
78
+ return new Cursor(this.text, this.text.length);
79
+ }
80
+
81
+ // Move to next line, maintaining column position if possible
82
+ const nextLine = lines[currentLine + 1];
83
+ const newCol = Math.min(currentCol, nextLine.length);
84
+
85
+ let newOffset = 0;
86
+ for (let i = 0; i <= currentLine; i++) {
87
+ newOffset += lines[i].length + 1;
88
+ }
89
+ newOffset += newCol;
90
+
91
+ return new Cursor(this.text, Math.min(newOffset, this.text.length));
92
+ }
93
+
94
+ startOfLine(): Cursor {
95
+ const lines = this.text.split('\n');
96
+ let charCount = 0;
97
+
98
+ for (let i = 0; i < lines.length; i++) {
99
+ const lineLength = lines[i].length;
100
+ if (charCount + lineLength >= this.offset) {
101
+ return new Cursor(this.text, charCount);
102
+ }
103
+ charCount += lineLength + 1;
104
+ }
105
+
106
+ return new Cursor(this.text, 0);
107
+ }
108
+
109
+ endOfLine(): Cursor {
110
+ const lines = this.text.split('\n');
111
+ let charCount = 0;
112
+
113
+ for (let i = 0; i < lines.length; i++) {
114
+ const lineLength = lines[i].length;
115
+ if (charCount + lineLength >= this.offset) {
116
+ return new Cursor(this.text, charCount + lineLength);
117
+ }
118
+ charCount += lineLength + 1;
119
+ }
120
+
121
+ return new Cursor(this.text, this.text.length);
122
+ }
123
+
124
+ prevWord(): Cursor {
125
+ let pos = this.offset;
126
+
127
+ // Skip current word if we're in the middle of one
128
+ while (pos > 0 && /\w/.test(this.text[pos - 1])) {
129
+ pos--;
130
+ }
131
+
132
+ // Skip whitespace
133
+ while (pos > 0 && /\s/.test(this.text[pos - 1])) {
134
+ pos--;
135
+ }
136
+
137
+ // Skip to beginning of previous word
138
+ while (pos > 0 && /\w/.test(this.text[pos - 1])) {
139
+ pos--;
140
+ }
141
+
142
+ return new Cursor(this.text, pos);
143
+ }
144
+
145
+ nextWord(): Cursor {
146
+ let pos = this.offset;
147
+
148
+ // Skip current word
149
+ while (pos < this.text.length && /\w/.test(this.text[pos])) {
150
+ pos++;
151
+ }
152
+
153
+ // Skip whitespace
154
+ while (pos < this.text.length && /\s/.test(this.text[pos])) {
155
+ pos++;
156
+ }
157
+
158
+ return new Cursor(this.text, pos);
159
+ }
160
+
161
+ // Editing methods
162
+ insert(str: string): Cursor {
163
+ // Convert \r to \n for consistent line handling
164
+ const normalizedStr = str.replace(/\r/g, '\n');
165
+ const newText =
166
+ this.text.slice(0, this.offset) +
167
+ normalizedStr +
168
+ this.text.slice(this.offset);
169
+ return new Cursor(newText, this.offset + normalizedStr.length);
170
+ }
171
+
172
+ backspace(): Cursor {
173
+ if (this.offset === 0) {
174
+ return this;
175
+ }
176
+ const newText =
177
+ this.text.slice(0, this.offset - 1) + this.text.slice(this.offset);
178
+ return new Cursor(newText, this.offset - 1);
179
+ }
180
+
181
+ del(): Cursor {
182
+ if (this.offset >= this.text.length) {
183
+ return this;
184
+ }
185
+ const newText =
186
+ this.text.slice(0, this.offset) + this.text.slice(this.offset + 1);
187
+ return new Cursor(newText, this.offset);
188
+ }
189
+
190
+ deleteToLineStart(): Cursor {
191
+ const lineStart = this.startOfLine();
192
+ const newText =
193
+ this.text.slice(0, lineStart.offset) + this.text.slice(this.offset);
194
+ return new Cursor(newText, lineStart.offset);
195
+ }
196
+
197
+ deleteToLineEnd(): Cursor {
198
+ const lineEnd = this.endOfLine();
199
+ const newText =
200
+ this.text.slice(0, this.offset) + this.text.slice(lineEnd.offset);
201
+ return new Cursor(newText, this.offset);
202
+ }
203
+
204
+ deleteWordBefore(): Cursor {
205
+ const prevWord = this.prevWord();
206
+ const newText =
207
+ this.text.slice(0, prevWord.offset) + this.text.slice(this.offset);
208
+ return new Cursor(newText, prevWord.offset);
209
+ }
210
+
211
+ deleteWordAfter(): Cursor {
212
+ const nextWord = this.nextWord();
213
+ const newText =
214
+ this.text.slice(0, this.offset) + this.text.slice(nextWord.offset);
215
+ return new Cursor(newText, this.offset);
216
+ }
217
+
218
+ // Utility methods
219
+ equals(other: Cursor): boolean {
220
+ return this.offset === other.offset && this.text === other.text;
221
+ }
222
+
223
+ render(
224
+ cursorChar: string,
225
+ mask?: string,
226
+ invert?: (text: string) => string
227
+ ): string {
228
+ if (!invert) {
229
+ invert = (text: string) => text;
230
+ }
231
+
232
+ const lines = this.text.split('\n');
233
+ let currentLine = 0;
234
+ let currentCol = 0;
235
+ let charCount = 0;
236
+
237
+ // Find cursor position
238
+ for (let i = 0; i < lines.length; i++) {
239
+ const lineLength = lines[i].length;
240
+ if (charCount + lineLength >= this.offset) {
241
+ currentLine = i;
242
+ currentCol = this.offset - charCount;
243
+ break;
244
+ }
245
+ charCount += lineLength + 1;
246
+ }
247
+
248
+ // Render lines with cursor
249
+ return lines
250
+ .map((line, i) => {
251
+ if (i === currentLine) {
252
+ const beforeCursor = line.slice(0, currentCol);
253
+ const atCursor = line[currentCol] || cursorChar;
254
+ const afterCursor = line.slice(currentCol + 1);
255
+ return beforeCursor + invert(atCursor) + afterCursor;
256
+ }
257
+ return line;
258
+ })
259
+ .join('\n');
260
+ }
261
+ }
@@ -0,0 +1,121 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+
4
+ interface InputComponentProps {
5
+ value: string;
6
+ suggestions: string[];
7
+ selectedSuggestion: number;
8
+ message: { show: boolean; text?: string };
9
+ commands: Array<{ name: string; description: string }>;
10
+ showWelcome: boolean;
11
+ showShortcuts: boolean;
12
+ cursor: number;
13
+ selection: { start: number; end: number } | null;
14
+ chatTarget?: string | null;
15
+ }
16
+
17
+ export function InputComponent({
18
+ value,
19
+ suggestions,
20
+ selectedSuggestion,
21
+ message,
22
+ commands,
23
+ showWelcome,
24
+ showShortcuts,
25
+ cursor,
26
+ selection,
27
+ chatTarget,
28
+ }: InputComponentProps) {
29
+ return (
30
+ <Box flexDirection="column">
31
+ <Box borderStyle="round" borderColor="gray" paddingX={1}>
32
+ <Text>
33
+ <Text color="gray">
34
+ {chatTarget
35
+ ? (() => {
36
+ // If chatTarget looks like a file path, don't truncate it
37
+ if (chatTarget.includes('/') || chatTarget.includes('\\')) {
38
+ // For file paths, show the filename or last part
39
+ const parts = chatTarget.split(/[/\\]/);
40
+ const filename = parts[parts.length - 1];
41
+ return `[chat ${filename}] > `;
42
+ }
43
+ // For regular client IDs, use the original truncation logic
44
+ return `[chat ${chatTarget.length <= 16 ? chatTarget : `${chatTarget.substring(0, 8)}...${chatTarget.substring(chatTarget.length - 4)}`}] > `;
45
+ })()
46
+ : '> '}
47
+ </Text>
48
+ {showWelcome && !value ? (
49
+ <Text backgroundColor="white" color="black">
50
+ _
51
+ </Text>
52
+ ) : selection ? (
53
+ <>
54
+ {(() => {
55
+ const start = Math.min(selection.start, selection.end);
56
+ const end = Math.max(selection.start, selection.end);
57
+ return (
58
+ <>
59
+ <Text color="white">{value.slice(0, start)}</Text>
60
+ <Text backgroundColor="blue" color="white">
61
+ {value.slice(start, end)}
62
+ </Text>
63
+ <Text color="white">{value.slice(end)}</Text>
64
+ </>
65
+ );
66
+ })()}
67
+ </>
68
+ ) : (
69
+ <>
70
+ <Text color="white">{value.slice(0, cursor)}</Text>
71
+ <Text backgroundColor="white" color="black">
72
+ {cursor < value.length ? value[cursor] : '_'}
73
+ </Text>
74
+ <Text color="white">{value.slice(cursor + 1)}</Text>
75
+ </>
76
+ )}
77
+ </Text>
78
+ </Box>
79
+
80
+ {suggestions.length > 0 && (
81
+ <Box flexDirection="column" paddingX={1}>
82
+ {suggestions.map((suggestion, index) => {
83
+ const command = commands.find((cmd) => cmd.name === suggestion);
84
+ return (
85
+ <Text
86
+ key={suggestion}
87
+ color={index === selectedSuggestion ? 'blue' : 'gray'}
88
+ >
89
+ {index === selectedSuggestion ? '> ' : ' '}/{suggestion} -{' '}
90
+ {command?.description || ''}
91
+ </Text>
92
+ );
93
+ })}
94
+ </Box>
95
+ )}
96
+
97
+ {showShortcuts && (
98
+ <Box flexDirection="column" paddingX={1}>
99
+ <Text color="gray"> ? - Show/hide this help</Text>
100
+ <Text color="gray"> / - Show commands</Text>
101
+ <Text color="gray"> Esc Esc - Clear input</Text>
102
+ <Text color="gray"> ↑/↓ - Navigate commands</Text>
103
+ <Text color="gray"> Tab/Enter - Select command</Text>
104
+ <Text color="gray"> Ctrl+C - Exit</Text>
105
+ </Box>
106
+ )}
107
+
108
+ {message.show && message.text && (
109
+ <Box paddingX={1}>
110
+ <Text color="yellow">{message.text}</Text>
111
+ </Box>
112
+ )}
113
+
114
+ {chatTarget && <Text color="blue">Chat mode - Esc to exit</Text>}
115
+
116
+ {showWelcome && !showShortcuts && suggestions.length === 0 && (
117
+ <Text color="gray">? for shortcuts</Text>
118
+ )}
119
+ </Box>
120
+ );
121
+ }
@@ -0,0 +1,80 @@
1
+ import { useState } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+
4
+ interface PromptInputProps {
5
+ onSubmit: (input: string) => void;
6
+ disabled?: boolean;
7
+ placeholder?: string;
8
+ }
9
+
10
+ export function PromptInput({
11
+ onSubmit,
12
+ disabled = false,
13
+ placeholder = '',
14
+ }: PromptInputProps) {
15
+ const [input, setInput] = useState('');
16
+
17
+ useInput((inputChar, key) => {
18
+ if (disabled) return;
19
+
20
+ // Handle Enter key - submit input
21
+ if (key.return) {
22
+ if (input.trim()) {
23
+ onSubmit(input.trim());
24
+ setInput('');
25
+ }
26
+ return;
27
+ }
28
+
29
+ // Handle backspace/delete - multiple detection methods like anon-kode-main
30
+ if (
31
+ key.backspace ||
32
+ key.delete ||
33
+ inputChar === '\b' ||
34
+ inputChar === '\x7f' ||
35
+ inputChar === '\x08'
36
+ ) {
37
+ setInput((prev) => prev.slice(0, -1));
38
+ return;
39
+ }
40
+
41
+ // Handle Ctrl+C - exit
42
+ if (key.ctrl && inputChar === 'c') {
43
+ process.exit(0);
44
+ return;
45
+ }
46
+
47
+ // Add regular characters - exclude special keys
48
+ if (
49
+ inputChar &&
50
+ inputChar.length === 1 &&
51
+ !key.ctrl &&
52
+ !key.escape &&
53
+ !key.tab &&
54
+ !key.return &&
55
+ !key.upArrow &&
56
+ !key.downArrow &&
57
+ !key.leftArrow &&
58
+ !key.rightArrow
59
+ ) {
60
+ setInput((prev) => prev + inputChar);
61
+ }
62
+ });
63
+
64
+ const displayInput = input || placeholder;
65
+ const isPlaceholder = !input && placeholder;
66
+
67
+ return (
68
+ <Box>
69
+ <Text color="gray">&gt; </Text>
70
+ <Text color={isPlaceholder ? 'gray' : 'white'}>
71
+ {displayInput}
72
+ {!disabled && !isPlaceholder && (
73
+ <Text backgroundColor="white" color="black">
74
+ _
75
+ </Text>
76
+ )}
77
+ </Text>
78
+ </Box>
79
+ );
80
+ }