mia-code 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +9 -0
- package/.coaia/pde/d77620fc-1cd9-47e2-ba00-c03e114e42e9.jsonl +16 -0
- package/.coaia/pde/de44d838-b58b-4e91-b791-dd3b0f940ed1.jsonl +60 -0
- package/.gemini/settings.json +8 -0
- package/.hch/issue_.env +4 -0
- package/.hch/issue_add__2601211715.json +77 -0
- package/.hch/issue_add__2601211715.md +4 -0
- package/.hch/issue_add__2602242020.json +78 -0
- package/.hch/issue_add__2602242020.md +7 -0
- package/.hch/issues.json +2312 -0
- package/.hch/issues.md +30 -0
- package/260123084839.coaia-narrative.autoRevisionOfInitial_NewStructuralTensionChart-to-initiate-HierarchicalThinking.txt +5 -0
- package/2602010101.issue.txt +31 -0
- package/BUGS.md +242 -0
- package/CLAUDE.md +2 -0
- package/ENHANCEMENTS.md +129 -0
- package/FEATURES_ENDING_SESSIONS.md +21 -0
- package/FIXES.md +114 -0
- package/GUILLAUME.md +77 -0
- package/KINSHIP.md +50 -0
- package/LAUNCH__session_id__MiaCodeNextWorkReviewAndCommits_2601312020.sh +7 -0
- package/PHASE_2.md +153 -0
- package/PHASE_2_IMPLEMENTATION.md +134 -0
- package/README.md +203 -0
- package/RESUME__issueMaker__540244c2-b096-40d8-8c3f-398408d3e0eb.2602041757.sh +1 -0
- package/RUN_COPILOT_with_related_folders__260130.sh +2 -0
- package/WS__mia-code__260214__IAIP_PDE.code-workspace +29 -0
- package/WS__mia-code__src332__260122.code-workspace +23 -0
- package/_env.sh +12 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +679 -0
- package/dist/commands.d.ts +43 -0
- package/dist/commands.js +108 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.js +57 -0
- package/dist/formatting.d.ts +12 -0
- package/dist/formatting.js +133 -0
- package/dist/geminiHeadless.d.ts +25 -0
- package/dist/geminiHeadless.js +246 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +186 -0
- package/dist/mcp/config-generator.d.ts +23 -0
- package/dist/mcp/config-generator.js +116 -0
- package/dist/mcp/index.d.ts +18 -0
- package/dist/mcp/index.js +43 -0
- package/dist/mcp/miaco-server.d.ts +15 -0
- package/dist/mcp/miaco-server.js +161 -0
- package/dist/mcp/miatel-server.d.ts +15 -0
- package/dist/mcp/miatel-server.js +123 -0
- package/dist/mcp/miawa-server.d.ts +15 -0
- package/dist/mcp/miawa-server.js +125 -0
- package/dist/mcp/utils.d.ts +51 -0
- package/dist/mcp/utils.js +76 -0
- package/dist/multiline-input.d.ts +98 -0
- package/dist/multiline-input.js +630 -0
- package/dist/narrative/index.d.ts +9 -0
- package/dist/narrative/index.js +11 -0
- package/dist/narrative/router.d.ts +89 -0
- package/dist/narrative/router.js +186 -0
- package/dist/narrative/tracer.d.ts +75 -0
- package/dist/narrative/tracer.js +180 -0
- package/dist/sessionStore.d.ts +10 -0
- package/dist/sessionStore.js +93 -0
- package/dist/types.d.ts +44 -0
- package/dist/types.js +1 -0
- package/dist/unifier.d.ts +6 -0
- package/dist/unifier.js +147 -0
- package/issue-358--architecture/ARCHITECTURE_OVERVIEW.md +60 -0
- package/issue-358--architecture/CLI_INTEGRATION.md +61 -0
- package/issue-358--architecture/COVER_ART_BRIEF.md +68 -0
- package/issue-358--architecture/MEMORY_SYSTEM.md +89 -0
- package/issue-358--architecture/PERSONA_REGISTRY.md +97 -0
- package/issue-358--architecture/PODCAST_PRODUCTION_PLAN.md +61 -0
- package/issue-358--architecture/PODCAST_SCRIPT_FINAL.md +109 -0
- package/issue-358--architecture/PROTOTYPE_CHARACTER_SPEC.md +59 -0
- package/issue-358--architecture/RESOURCES.md +41 -0
- package/issue-358--architecture/TEAM_LISTENING_GUIDE.md +53 -0
- package/llms-gemini-cli.txt +145 -0
- package/package.json +39 -0
- package/samples/copilot/session-state/be76abaa-a27f-4725-b2a9-22fb45f7e0f7/checkpoints/index.md +6 -0
- package/samples/copilot/session-state/be76abaa-a27f-4725-b2a9-22fb45f7e0f7/events.jsonl +213 -0
- package/samples/copilot/session-state/be76abaa-a27f-4725-b2a9-22fb45f7e0f7/plan.md +243 -0
- package/samples/copilot/session-state/be76abaa-a27f-4725-b2a9-22fb45f7e0f7/workspace.yaml +5 -0
- package/src/cli.ts +742 -0
- package/src/commands.ts +127 -0
- package/src/config.ts +67 -0
- package/src/formatting.ts +157 -0
- package/src/geminiHeadless.ts +300 -0
- package/src/index.ts +194 -0
- package/src/mcp/config-generator.ts +141 -0
- package/src/mcp/index.ts +55 -0
- package/src/mcp/miaco-server.ts +199 -0
- package/src/mcp/miatel-server.ts +138 -0
- package/src/mcp/miawa-server.ts +158 -0
- package/src/mcp/utils.ts +121 -0
- package/src/multiline-input.ts +739 -0
- package/src/narrative/index.ts +33 -0
- package/src/narrative/router.ts +260 -0
- package/src/narrative/tracer.ts +249 -0
- package/src/sessionStore.ts +111 -0
- package/src/types.ts +49 -0
- package/src/unifier.ts +171 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,739 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MultilineInput - Multiline input handler for terminal CLIs
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - Ctrl+J to add new lines without submitting
|
|
6
|
+
* - Enter to submit the complete input
|
|
7
|
+
* - Pasting multiline content
|
|
8
|
+
* - Arrow key navigation
|
|
9
|
+
* - Interactive dropdown completion for / commands and @ file paths
|
|
10
|
+
* - TTY detection with readline fallback
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { EventEmitter } from "events";
|
|
14
|
+
|
|
15
|
+
export interface TabCompletion {
|
|
16
|
+
matches: string[];
|
|
17
|
+
/** Display labels (shown in dropdown). Falls back to matches if not provided. */
|
|
18
|
+
labels?: string[];
|
|
19
|
+
prefix: string;
|
|
20
|
+
/** Position in the input line where the completable token starts */
|
|
21
|
+
tokenStart: number;
|
|
22
|
+
/** Character to prepend to each match for display (e.g. "/" or "@") */
|
|
23
|
+
triggerChar: string;
|
|
24
|
+
/** If true, append "/" to directories instead of " " */
|
|
25
|
+
isFilePath?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface MultilineInputOptions {
|
|
29
|
+
prompt: string;
|
|
30
|
+
continuationPrompt?: string;
|
|
31
|
+
maxLines?: number;
|
|
32
|
+
onSubmit: (input: string) => Promise<void>;
|
|
33
|
+
onClose?: () => void;
|
|
34
|
+
onTabComplete?: (input: string) => TabCompletion | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Check if environment supports raw mode multiline input
|
|
39
|
+
*/
|
|
40
|
+
export function supportsMultilineInput(): boolean {
|
|
41
|
+
return process.stdin.isTTY === true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── Dropdown state ──────────────────────────────────────────────────
|
|
45
|
+
interface DropdownState {
|
|
46
|
+
visible: boolean;
|
|
47
|
+
items: string[]; // completion values
|
|
48
|
+
labels: string[]; // display labels
|
|
49
|
+
selectedIndex: number;
|
|
50
|
+
completion: TabCompletion;
|
|
51
|
+
renderedRows: number; // how many rows the dropdown currently occupies on screen
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class MultilineInput extends EventEmitter {
|
|
55
|
+
private prompt: string;
|
|
56
|
+
private continuationPrompt: string;
|
|
57
|
+
private maxLines: number;
|
|
58
|
+
private onSubmit: (input: string) => Promise<void>;
|
|
59
|
+
private onClose?: () => void;
|
|
60
|
+
private onTabComplete?: (input: string) => TabCompletion | null;
|
|
61
|
+
|
|
62
|
+
private lines: string[] = [];
|
|
63
|
+
private currentLine: string = "";
|
|
64
|
+
private cursorPos: number = 0;
|
|
65
|
+
private isRunning: boolean = false;
|
|
66
|
+
private isPaused: boolean = false;
|
|
67
|
+
private isProcessing: boolean = false;
|
|
68
|
+
|
|
69
|
+
// Dropdown
|
|
70
|
+
private dropdown: DropdownState = {
|
|
71
|
+
visible: false,
|
|
72
|
+
items: [],
|
|
73
|
+
labels: [],
|
|
74
|
+
selectedIndex: 0,
|
|
75
|
+
completion: { matches: [], prefix: "", tokenStart: 0, triggerChar: "" },
|
|
76
|
+
renderedRows: 0,
|
|
77
|
+
};
|
|
78
|
+
private static readonly MAX_DROPDOWN_ITEMS = 10;
|
|
79
|
+
|
|
80
|
+
// ── Helpers ─────────────────────────────────────────────────────
|
|
81
|
+
private static stripAnsi(str: string): string {
|
|
82
|
+
// eslint-disable-next-line no-control-regex
|
|
83
|
+
return str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private visibleLength(str: string): number {
|
|
87
|
+
return MultilineInput.stripAnsi(str).length;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private getColumns(): number {
|
|
91
|
+
return process.stdout.columns || 80;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private getRows(): number {
|
|
95
|
+
return process.stdout.rows || 24;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private wrappedRows(promptStr: string, text: string): number {
|
|
99
|
+
const totalVisible = this.visibleLength(promptStr) + text.length;
|
|
100
|
+
const cols = this.getColumns();
|
|
101
|
+
if (cols <= 0) return 1;
|
|
102
|
+
return Math.max(1, Math.ceil(totalVisible / cols));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
constructor(options: MultilineInputOptions) {
|
|
106
|
+
super();
|
|
107
|
+
this.prompt = options.prompt;
|
|
108
|
+
this.continuationPrompt = options.continuationPrompt || "... ";
|
|
109
|
+
this.maxLines = options.maxLines || 100;
|
|
110
|
+
this.onSubmit = options.onSubmit;
|
|
111
|
+
this.onClose = options.onClose;
|
|
112
|
+
this.onTabComplete = options.onTabComplete;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
start(): void {
|
|
116
|
+
if (!supportsMultilineInput()) {
|
|
117
|
+
throw new Error("MultilineInput requires TTY mode");
|
|
118
|
+
}
|
|
119
|
+
this.isRunning = true;
|
|
120
|
+
process.stdin.setRawMode(true);
|
|
121
|
+
process.stdin.resume();
|
|
122
|
+
process.stdin.on("data", this.handleData);
|
|
123
|
+
this.showPrompt();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
stop(): void {
|
|
127
|
+
this.isRunning = false;
|
|
128
|
+
this.clearDropdown();
|
|
129
|
+
process.stdin.setRawMode(false);
|
|
130
|
+
process.stdin.removeListener("data", this.handleData);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
pause(): void {
|
|
134
|
+
if (!this.isRunning) return;
|
|
135
|
+
this.isPaused = true;
|
|
136
|
+
this.clearDropdown();
|
|
137
|
+
process.stdin.setRawMode(false);
|
|
138
|
+
process.stdin.removeListener("data", this.handleData);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
resume(): void {
|
|
142
|
+
if (!this.isRunning || !this.isPaused) return;
|
|
143
|
+
this.isPaused = false;
|
|
144
|
+
process.stdin.setRawMode(true);
|
|
145
|
+
process.stdin.on("data", this.handleData);
|
|
146
|
+
this.redraw();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
reset(): void {
|
|
150
|
+
this.lines = [];
|
|
151
|
+
this.currentLine = "";
|
|
152
|
+
this.cursorPos = 0;
|
|
153
|
+
this.isProcessing = false;
|
|
154
|
+
this.clearDropdown();
|
|
155
|
+
this.showPrompt();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── Prompt / Redraw ─────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
private showPrompt(): void {
|
|
161
|
+
const promptText = this.lines.length === 0 ? this.prompt : this.continuationPrompt;
|
|
162
|
+
process.stdout.write(promptText);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private redraw(): void {
|
|
166
|
+
// First erase any visible dropdown so it doesn't leave ghosts
|
|
167
|
+
this.eraseDropdownRows();
|
|
168
|
+
|
|
169
|
+
const promptText = this.lines.length === 0 ? this.prompt : this.continuationPrompt;
|
|
170
|
+
const cols = this.getColumns();
|
|
171
|
+
const promptVisible = this.visibleLength(promptText);
|
|
172
|
+
|
|
173
|
+
const totalVisible = promptVisible + this.currentLine.length;
|
|
174
|
+
const totalRows = Math.max(1, Math.ceil(totalVisible / cols));
|
|
175
|
+
|
|
176
|
+
const cursorAbsPos = promptVisible + this.cursorPos;
|
|
177
|
+
const cursorRow = Math.floor(cursorAbsPos / cols);
|
|
178
|
+
|
|
179
|
+
if (cursorRow > 0) {
|
|
180
|
+
process.stdout.write(`\x1B[${cursorRow}A`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for (let i = 0; i < totalRows; i++) {
|
|
184
|
+
process.stdout.write("\r\x1B[K");
|
|
185
|
+
if (i < totalRows - 1) {
|
|
186
|
+
process.stdout.write("\x1B[1B");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (totalRows > 1) {
|
|
191
|
+
process.stdout.write(`\x1B[${totalRows - 1}A`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
process.stdout.write("\r");
|
|
195
|
+
process.stdout.write(promptText);
|
|
196
|
+
process.stdout.write(this.currentLine);
|
|
197
|
+
|
|
198
|
+
if (this.cursorPos < this.currentLine.length) {
|
|
199
|
+
const newTotalRows = Math.max(1, Math.ceil((promptVisible + this.currentLine.length) / cols));
|
|
200
|
+
const endRow = newTotalRows - 1;
|
|
201
|
+
const targetAbsPos = promptVisible + this.cursorPos;
|
|
202
|
+
const targetRow = Math.floor(targetAbsPos / cols);
|
|
203
|
+
const targetCol = targetAbsPos % cols;
|
|
204
|
+
|
|
205
|
+
if (endRow > targetRow) {
|
|
206
|
+
process.stdout.write(`\x1B[${endRow - targetRow}A`);
|
|
207
|
+
}
|
|
208
|
+
process.stdout.write(`\r\x1B[${targetCol}C`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Re-render dropdown if still active
|
|
212
|
+
if (this.dropdown.visible) {
|
|
213
|
+
this.renderDropdown();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── Dropdown rendering ──────────────────────────────────────────
|
|
218
|
+
//
|
|
219
|
+
// IMPORTANT: We avoid \x1B[s / \x1B[u (save/restore cursor) because
|
|
220
|
+
// terminal scrolling invalidates the saved position, causing ghost rows.
|
|
221
|
+
// Instead we use explicit relative movement and always return the cursor
|
|
222
|
+
// to the input position by counting rows moved.
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Calculate how many rows down from the cursor to the start of the dropdown
|
|
226
|
+
*/
|
|
227
|
+
private rowsFromCursorToDropdown(): number {
|
|
228
|
+
const promptText = this.lines.length === 0 ? this.prompt : this.continuationPrompt;
|
|
229
|
+
const promptVisible = this.visibleLength(promptText);
|
|
230
|
+
const cols = this.getColumns();
|
|
231
|
+
const inputRows = Math.max(1, Math.ceil((promptVisible + this.currentLine.length) / cols));
|
|
232
|
+
const cursorAbsPos = promptVisible + this.cursorPos;
|
|
233
|
+
const cursorRow = Math.floor(cursorAbsPos / cols);
|
|
234
|
+
return (inputRows - 1 - cursorRow) + 1;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Reposition cursor back to the input editing position (absolute)
|
|
239
|
+
*/
|
|
240
|
+
private repositionCursor(): void {
|
|
241
|
+
const promptText = this.lines.length === 0 ? this.prompt : this.continuationPrompt;
|
|
242
|
+
const promptVisible = this.visibleLength(promptText);
|
|
243
|
+
const cols = this.getColumns();
|
|
244
|
+
const cursorAbsPos = promptVisible + this.cursorPos;
|
|
245
|
+
const cursorCol = cursorAbsPos % cols;
|
|
246
|
+
process.stdout.write("\r");
|
|
247
|
+
if (cursorCol > 0) {
|
|
248
|
+
process.stdout.write(`\x1B[${cursorCol}C`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private eraseDropdownRows(): void {
|
|
253
|
+
if (this.dropdown.renderedRows <= 0) return;
|
|
254
|
+
|
|
255
|
+
const down = this.rowsFromCursorToDropdown();
|
|
256
|
+
|
|
257
|
+
// Move down from cursor to dropdown start
|
|
258
|
+
if (down > 0) {
|
|
259
|
+
process.stdout.write(`\x1B[${down}B`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Clear from here to end of screen (wipes all dropdown rows at once)
|
|
263
|
+
process.stdout.write("\r\x1B[J");
|
|
264
|
+
|
|
265
|
+
// Move back up to cursor row
|
|
266
|
+
if (down > 0) {
|
|
267
|
+
process.stdout.write(`\x1B[${down}A`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Restore column position
|
|
271
|
+
this.repositionCursor();
|
|
272
|
+
|
|
273
|
+
this.dropdown.renderedRows = 0;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private renderDropdown(): void {
|
|
277
|
+
if (!this.dropdown.visible || this.dropdown.items.length === 0) return;
|
|
278
|
+
|
|
279
|
+
const cols = this.getColumns();
|
|
280
|
+
const promptText = this.lines.length === 0 ? this.prompt : this.continuationPrompt;
|
|
281
|
+
const promptVisible = this.visibleLength(promptText);
|
|
282
|
+
const inputRows = Math.max(1, Math.ceil((promptVisible + this.currentLine.length) / cols));
|
|
283
|
+
|
|
284
|
+
const down = this.rowsFromCursorToDropdown();
|
|
285
|
+
|
|
286
|
+
// Move down from cursor to dropdown area
|
|
287
|
+
if (down > 0) {
|
|
288
|
+
process.stdout.write(`\x1B[${down}B`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Clear everything below (any stale dropdown content)
|
|
292
|
+
process.stdout.write("\r\x1B[J");
|
|
293
|
+
|
|
294
|
+
// Determine how many items to show
|
|
295
|
+
const maxVisible = Math.min(
|
|
296
|
+
this.dropdown.items.length,
|
|
297
|
+
MultilineInput.MAX_DROPDOWN_ITEMS,
|
|
298
|
+
Math.max(1, this.getRows() - inputRows - 1)
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
// Scroll window around selected index
|
|
302
|
+
let startIdx = 0;
|
|
303
|
+
if (this.dropdown.selectedIndex >= maxVisible) {
|
|
304
|
+
startIdx = this.dropdown.selectedIndex - maxVisible + 1;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const { triggerChar } = this.dropdown.completion;
|
|
308
|
+
let rowsDrawn = 0;
|
|
309
|
+
|
|
310
|
+
for (let i = 0; i < maxVisible; i++) {
|
|
311
|
+
const idx = startIdx + i;
|
|
312
|
+
if (idx >= this.dropdown.items.length) break;
|
|
313
|
+
|
|
314
|
+
const label = this.dropdown.labels[idx] || (triggerChar + this.dropdown.items[idx]);
|
|
315
|
+
const isSelected = idx === this.dropdown.selectedIndex;
|
|
316
|
+
const truncated = label.length > cols - 4
|
|
317
|
+
? label.slice(0, cols - 7) + "..."
|
|
318
|
+
: label;
|
|
319
|
+
|
|
320
|
+
if (isSelected) {
|
|
321
|
+
process.stdout.write(`\x1B[7m ${truncated} \x1B[0m`);
|
|
322
|
+
} else {
|
|
323
|
+
process.stdout.write(`\x1B[90m ${truncated}\x1B[0m`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
rowsDrawn++;
|
|
327
|
+
if (i < maxVisible - 1) {
|
|
328
|
+
process.stdout.write("\n\r");
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Scroll indicator
|
|
333
|
+
if (this.dropdown.items.length > maxVisible) {
|
|
334
|
+
const showing = `${startIdx + 1}-${Math.min(startIdx + maxVisible, this.dropdown.items.length)}`;
|
|
335
|
+
process.stdout.write(`\n\r\x1B[90m ↕ ${showing} of ${this.dropdown.items.length}\x1B[0m`);
|
|
336
|
+
rowsDrawn++;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
this.dropdown.renderedRows = rowsDrawn;
|
|
340
|
+
|
|
341
|
+
// Move back up to cursor row: rowsDrawn lines back to dropdown start, then `down` back to cursor
|
|
342
|
+
const totalUp = rowsDrawn + down - 1;
|
|
343
|
+
if (totalUp > 0) {
|
|
344
|
+
process.stdout.write(`\x1B[${totalUp}A`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Restore column position
|
|
348
|
+
this.repositionCursor();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private clearDropdown(): void {
|
|
352
|
+
this.eraseDropdownRows();
|
|
353
|
+
this.dropdown.visible = false;
|
|
354
|
+
this.dropdown.items = [];
|
|
355
|
+
this.dropdown.labels = [];
|
|
356
|
+
this.dropdown.selectedIndex = 0;
|
|
357
|
+
this.dropdown.renderedRows = 0;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Query the completion handler and update dropdown state
|
|
362
|
+
*/
|
|
363
|
+
private updateDropdown(): void {
|
|
364
|
+
if (!this.onTabComplete) {
|
|
365
|
+
this.clearDropdown();
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const result = this.onTabComplete(this.currentLine);
|
|
370
|
+
if (!result || result.matches.length === 0) {
|
|
371
|
+
this.clearDropdown();
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
this.dropdown.visible = true;
|
|
376
|
+
this.dropdown.items = result.matches;
|
|
377
|
+
this.dropdown.labels = result.labels || result.matches.map(m => result.triggerChar + m);
|
|
378
|
+
this.dropdown.selectedIndex = 0;
|
|
379
|
+
this.dropdown.completion = result;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Accept the currently selected dropdown item
|
|
384
|
+
*/
|
|
385
|
+
private acceptDropdownSelection(): void {
|
|
386
|
+
if (!this.dropdown.visible || this.dropdown.items.length === 0) return;
|
|
387
|
+
|
|
388
|
+
const { tokenStart, triggerChar, isFilePath } = this.dropdown.completion;
|
|
389
|
+
const match = this.dropdown.items[this.dropdown.selectedIndex];
|
|
390
|
+
const before = this.currentLine.slice(0, tokenStart);
|
|
391
|
+
const suffix = (isFilePath && match.endsWith("/")) ? "" : " ";
|
|
392
|
+
const completed = before + triggerChar + match + suffix;
|
|
393
|
+
|
|
394
|
+
this.currentLine = completed;
|
|
395
|
+
this.cursorPos = completed.length;
|
|
396
|
+
|
|
397
|
+
// If it's a directory, keep dropdown open for deeper navigation
|
|
398
|
+
if (isFilePath && match.endsWith("/")) {
|
|
399
|
+
this.redraw();
|
|
400
|
+
this.updateDropdown();
|
|
401
|
+
this.renderDropdown();
|
|
402
|
+
} else {
|
|
403
|
+
this.clearDropdown();
|
|
404
|
+
this.redraw();
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ── Input handling ──────────────────────────────────────────────
|
|
409
|
+
|
|
410
|
+
private handleData = async (data: Buffer): Promise<void> => {
|
|
411
|
+
if (this.isProcessing) return;
|
|
412
|
+
|
|
413
|
+
const str = data.toString("utf8");
|
|
414
|
+
|
|
415
|
+
// Paste detection: multiple chars without escape sequence
|
|
416
|
+
if (str.length > 1 && !str.startsWith("\x1B")) {
|
|
417
|
+
this.clearDropdown();
|
|
418
|
+
this.handlePaste(str);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const char = str;
|
|
423
|
+
const code = char.charCodeAt(0);
|
|
424
|
+
|
|
425
|
+
// ── Dropdown-aware key handling ──
|
|
426
|
+
|
|
427
|
+
// Escape sequences while dropdown is open
|
|
428
|
+
if (char.startsWith("\x1B") && this.dropdown.visible) {
|
|
429
|
+
if (char === "\x1B[A") {
|
|
430
|
+
// Arrow Up — move selection up
|
|
431
|
+
if (this.dropdown.selectedIndex > 0) {
|
|
432
|
+
this.dropdown.selectedIndex--;
|
|
433
|
+
this.eraseDropdownRows();
|
|
434
|
+
this.renderDropdown();
|
|
435
|
+
}
|
|
436
|
+
return;
|
|
437
|
+
} else if (char === "\x1B[B") {
|
|
438
|
+
// Arrow Down — move selection down
|
|
439
|
+
if (this.dropdown.selectedIndex < this.dropdown.items.length - 1) {
|
|
440
|
+
this.dropdown.selectedIndex++;
|
|
441
|
+
this.eraseDropdownRows();
|
|
442
|
+
this.renderDropdown();
|
|
443
|
+
}
|
|
444
|
+
return;
|
|
445
|
+
} else if (char === "\x1B" || char === "\x1B[D" || char === "\x1B[C") {
|
|
446
|
+
// Esc or horizontal arrows — close dropdown
|
|
447
|
+
this.clearDropdown();
|
|
448
|
+
if (char !== "\x1B") {
|
|
449
|
+
this.handleEscapeSequence(char);
|
|
450
|
+
}
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Escape (standalone) to close dropdown
|
|
456
|
+
if (code === 27 && char.length === 1 && this.dropdown.visible) {
|
|
457
|
+
this.clearDropdown();
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
switch (code) {
|
|
462
|
+
case 3: // Ctrl+C
|
|
463
|
+
this.clearDropdown();
|
|
464
|
+
if (this.currentLine || this.lines.length > 0) {
|
|
465
|
+
process.stdout.write("\n");
|
|
466
|
+
this.lines = [];
|
|
467
|
+
this.currentLine = "";
|
|
468
|
+
this.cursorPos = 0;
|
|
469
|
+
this.showPrompt();
|
|
470
|
+
} else {
|
|
471
|
+
process.stdout.write("\n");
|
|
472
|
+
if (this.onClose) this.onClose();
|
|
473
|
+
}
|
|
474
|
+
break;
|
|
475
|
+
|
|
476
|
+
case 4: // Ctrl+D
|
|
477
|
+
if (!this.currentLine && this.lines.length === 0) {
|
|
478
|
+
this.clearDropdown();
|
|
479
|
+
process.stdout.write("\n");
|
|
480
|
+
if (this.onClose) this.onClose();
|
|
481
|
+
}
|
|
482
|
+
break;
|
|
483
|
+
|
|
484
|
+
case 10: // Ctrl+J (newline without submit)
|
|
485
|
+
this.clearDropdown();
|
|
486
|
+
if (this.lines.length < this.maxLines) {
|
|
487
|
+
this.lines.push(this.currentLine);
|
|
488
|
+
this.currentLine = "";
|
|
489
|
+
this.cursorPos = 0;
|
|
490
|
+
process.stdout.write("\n");
|
|
491
|
+
this.showPrompt();
|
|
492
|
+
}
|
|
493
|
+
break;
|
|
494
|
+
|
|
495
|
+
case 9: // Tab — accept dropdown selection or trigger completion
|
|
496
|
+
if (this.dropdown.visible) {
|
|
497
|
+
this.acceptDropdownSelection();
|
|
498
|
+
} else if (this.onTabComplete) {
|
|
499
|
+
// Open dropdown on Tab if there's a completable context
|
|
500
|
+
this.updateDropdown();
|
|
501
|
+
if (this.dropdown.visible) {
|
|
502
|
+
this.redraw();
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
break;
|
|
506
|
+
|
|
507
|
+
case 13: // Enter
|
|
508
|
+
if (this.dropdown.visible) {
|
|
509
|
+
// Accept selection instead of submitting
|
|
510
|
+
this.acceptDropdownSelection();
|
|
511
|
+
} else {
|
|
512
|
+
this.clearDropdown();
|
|
513
|
+
this.lines.push(this.currentLine);
|
|
514
|
+
const fullInput = this.lines.join("\n");
|
|
515
|
+
this.lines = [];
|
|
516
|
+
this.currentLine = "";
|
|
517
|
+
this.cursorPos = 0;
|
|
518
|
+
process.stdout.write("\n");
|
|
519
|
+
|
|
520
|
+
this.isProcessing = true;
|
|
521
|
+
try {
|
|
522
|
+
await this.onSubmit(fullInput);
|
|
523
|
+
} finally {
|
|
524
|
+
this.isProcessing = false;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
break;
|
|
528
|
+
|
|
529
|
+
case 127: // Backspace (macOS/Linux)
|
|
530
|
+
case 8: // Backspace (Windows)
|
|
531
|
+
if (this.cursorPos > 0) {
|
|
532
|
+
this.currentLine =
|
|
533
|
+
this.currentLine.slice(0, this.cursorPos - 1) +
|
|
534
|
+
this.currentLine.slice(this.cursorPos);
|
|
535
|
+
this.cursorPos--;
|
|
536
|
+
this.redraw();
|
|
537
|
+
this.maybeShowDropdown();
|
|
538
|
+
} else if (this.lines.length > 0) {
|
|
539
|
+
const prevLine = this.lines.pop()!;
|
|
540
|
+
this.cursorPos = prevLine.length;
|
|
541
|
+
this.currentLine = prevLine + this.currentLine;
|
|
542
|
+
this.clearDropdown();
|
|
543
|
+
process.stdout.write("\x1B[A");
|
|
544
|
+
this.redraw();
|
|
545
|
+
}
|
|
546
|
+
break;
|
|
547
|
+
|
|
548
|
+
case 21: // Ctrl+U (clear line)
|
|
549
|
+
this.currentLine = this.currentLine.slice(this.cursorPos);
|
|
550
|
+
this.cursorPos = 0;
|
|
551
|
+
this.clearDropdown();
|
|
552
|
+
this.redraw();
|
|
553
|
+
break;
|
|
554
|
+
|
|
555
|
+
case 23: // Ctrl+W (delete word)
|
|
556
|
+
if (this.cursorPos > 0) {
|
|
557
|
+
const before = this.currentLine.slice(0, this.cursorPos);
|
|
558
|
+
const after = this.currentLine.slice(this.cursorPos);
|
|
559
|
+
const lastSpace = before.trimEnd().lastIndexOf(" ");
|
|
560
|
+
const newBefore = lastSpace >= 0 ? before.slice(0, lastSpace + 1) : "";
|
|
561
|
+
this.currentLine = newBefore + after;
|
|
562
|
+
this.cursorPos = newBefore.length;
|
|
563
|
+
this.redraw();
|
|
564
|
+
this.maybeShowDropdown();
|
|
565
|
+
}
|
|
566
|
+
break;
|
|
567
|
+
|
|
568
|
+
case 12: // Ctrl+L (clear screen)
|
|
569
|
+
this.clearDropdown();
|
|
570
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
571
|
+
this.redrawAll();
|
|
572
|
+
break;
|
|
573
|
+
|
|
574
|
+
default:
|
|
575
|
+
if (char.startsWith("\x1B")) {
|
|
576
|
+
this.clearDropdown();
|
|
577
|
+
this.handleEscapeSequence(char);
|
|
578
|
+
} else if (code >= 32) {
|
|
579
|
+
// Regular character
|
|
580
|
+
this.currentLine =
|
|
581
|
+
this.currentLine.slice(0, this.cursorPos) +
|
|
582
|
+
char +
|
|
583
|
+
this.currentLine.slice(this.cursorPos);
|
|
584
|
+
this.cursorPos++;
|
|
585
|
+
this.redraw();
|
|
586
|
+
this.maybeShowDropdown();
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Check if the current input context should trigger a live dropdown.
|
|
593
|
+
* "/" commands: always live (few items, instant feedback).
|
|
594
|
+
* "@" files: only update if dropdown is already open (Tab to initiate).
|
|
595
|
+
*/
|
|
596
|
+
private maybeShowDropdown(): void {
|
|
597
|
+
if (!this.onTabComplete) return;
|
|
598
|
+
|
|
599
|
+
const textUpToCursor = this.currentLine.slice(0, this.cursorPos);
|
|
600
|
+
|
|
601
|
+
// "/" commands: live dropdown as you type
|
|
602
|
+
if (textUpToCursor.startsWith("/") && !textUpToCursor.includes(" ")) {
|
|
603
|
+
this.updateDropdown();
|
|
604
|
+
if (this.dropdown.visible) {
|
|
605
|
+
this.renderDropdown();
|
|
606
|
+
}
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// "@" files: if dropdown already open, update it as you type deeper
|
|
611
|
+
if (this.dropdown.visible && this.isInAtContext(textUpToCursor)) {
|
|
612
|
+
this.updateDropdown();
|
|
613
|
+
if (this.dropdown.visible) {
|
|
614
|
+
this.renderDropdown();
|
|
615
|
+
}
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// No trigger context — close dropdown
|
|
620
|
+
if (this.dropdown.visible) {
|
|
621
|
+
this.clearDropdown();
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Check if text ends with an active @path context (no space after @)
|
|
627
|
+
*/
|
|
628
|
+
private isInAtContext(text: string): boolean {
|
|
629
|
+
const atIdx = text.lastIndexOf("@");
|
|
630
|
+
if (atIdx < 0) return false;
|
|
631
|
+
const afterAt = text.slice(atIdx + 1);
|
|
632
|
+
return !afterAt.includes(" ");
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
private handlePaste(str: string): void {
|
|
636
|
+
const pastedLines = str.split(/\r?\n/);
|
|
637
|
+
|
|
638
|
+
this.currentLine =
|
|
639
|
+
this.currentLine.slice(0, this.cursorPos) +
|
|
640
|
+
pastedLines[0] +
|
|
641
|
+
this.currentLine.slice(this.cursorPos);
|
|
642
|
+
this.cursorPos += pastedLines[0].length;
|
|
643
|
+
|
|
644
|
+
for (let i = 1; i < pastedLines.length; i++) {
|
|
645
|
+
if (this.lines.length >= this.maxLines) break;
|
|
646
|
+
this.lines.push(this.currentLine);
|
|
647
|
+
this.currentLine = pastedLines[i];
|
|
648
|
+
this.cursorPos = this.currentLine.length;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
this.redrawAll();
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
private handleEscapeSequence(seq: string): void {
|
|
655
|
+
if (seq === "\x1B[D") {
|
|
656
|
+
if (this.cursorPos > 0) {
|
|
657
|
+
this.cursorPos--;
|
|
658
|
+
this.redraw();
|
|
659
|
+
}
|
|
660
|
+
} else if (seq === "\x1B[C") {
|
|
661
|
+
if (this.cursorPos < this.currentLine.length) {
|
|
662
|
+
this.cursorPos++;
|
|
663
|
+
this.redraw();
|
|
664
|
+
}
|
|
665
|
+
} else if (seq === "\x1B[A") {
|
|
666
|
+
// Arrow Up — could implement history
|
|
667
|
+
} else if (seq === "\x1B[B") {
|
|
668
|
+
// Arrow Down — could implement history
|
|
669
|
+
} else if (seq === "\x1B[H" || seq === "\x1B[1~") {
|
|
670
|
+
this.cursorPos = 0;
|
|
671
|
+
this.redraw();
|
|
672
|
+
} else if (seq === "\x1B[F" || seq === "\x1B[4~") {
|
|
673
|
+
this.cursorPos = this.currentLine.length;
|
|
674
|
+
this.redraw();
|
|
675
|
+
} else if (seq === "\x1B[3~") {
|
|
676
|
+
if (this.cursorPos < this.currentLine.length) {
|
|
677
|
+
this.currentLine =
|
|
678
|
+
this.currentLine.slice(0, this.cursorPos) +
|
|
679
|
+
this.currentLine.slice(this.cursorPos + 1);
|
|
680
|
+
this.redraw();
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
private redrawAll(): void {
|
|
686
|
+
const cols = this.getColumns();
|
|
687
|
+
|
|
688
|
+
let totalTermRows = 0;
|
|
689
|
+
for (let i = 0; i < this.lines.length; i++) {
|
|
690
|
+
const p = i === 0 ? this.prompt : this.continuationPrompt;
|
|
691
|
+
totalTermRows += this.wrappedRows(p, this.lines[i]);
|
|
692
|
+
}
|
|
693
|
+
const currentPrompt = this.lines.length === 0 ? this.prompt : this.continuationPrompt;
|
|
694
|
+
totalTermRows += this.wrappedRows(currentPrompt, this.currentLine);
|
|
695
|
+
|
|
696
|
+
const currentLineRows = this.wrappedRows(currentPrompt, this.currentLine);
|
|
697
|
+
const rowsAboveCurrent = totalTermRows - currentLineRows;
|
|
698
|
+
const cursorRowInCurrent = currentLineRows - 1;
|
|
699
|
+
const moveUp = rowsAboveCurrent + cursorRowInCurrent;
|
|
700
|
+
if (moveUp > 0) {
|
|
701
|
+
process.stdout.write(`\x1B[${moveUp}A`);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
for (let i = 0; i < totalTermRows; i++) {
|
|
705
|
+
process.stdout.write("\r\x1B[K");
|
|
706
|
+
if (i < totalTermRows - 1) {
|
|
707
|
+
process.stdout.write("\x1B[1B");
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (totalTermRows > 1) {
|
|
712
|
+
process.stdout.write(`\x1B[${totalTermRows - 1}A`);
|
|
713
|
+
}
|
|
714
|
+
process.stdout.write("\r");
|
|
715
|
+
|
|
716
|
+
for (let i = 0; i < this.lines.length; i++) {
|
|
717
|
+
const p = i === 0 ? this.prompt : this.continuationPrompt;
|
|
718
|
+
process.stdout.write(p + this.lines[i] + "\n");
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const promptText = this.lines.length === 0 ? this.prompt : this.continuationPrompt;
|
|
722
|
+
const promptVisible = this.visibleLength(promptText);
|
|
723
|
+
process.stdout.write(promptText);
|
|
724
|
+
process.stdout.write(this.currentLine);
|
|
725
|
+
|
|
726
|
+
if (this.cursorPos < this.currentLine.length) {
|
|
727
|
+
const newRows = this.wrappedRows(promptText, this.currentLine);
|
|
728
|
+
const endRow = newRows - 1;
|
|
729
|
+
const targetAbsPos = promptVisible + this.cursorPos;
|
|
730
|
+
const targetRow = Math.floor(targetAbsPos / cols);
|
|
731
|
+
const targetCol = targetAbsPos % cols;
|
|
732
|
+
|
|
733
|
+
if (endRow > targetRow) {
|
|
734
|
+
process.stdout.write(`\x1B[${endRow - targetRow}A`);
|
|
735
|
+
}
|
|
736
|
+
process.stdout.write(`\r\x1B[${targetCol}C`);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|