letmecook 0.0.14 → 0.0.16
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/package.json +1 -1
- package/src/agents-md.ts +16 -27
- package/src/flows/add-repos.ts +132 -22
- package/src/flows/edit-session.ts +86 -15
- package/src/flows/new-session.ts +159 -34
- package/src/flows/resume-session.ts +53 -33
- package/src/git.ts +77 -39
- package/src/process-registry.ts +179 -0
- package/src/reference-repo.ts +288 -0
- package/src/tui-mode.ts +14 -1
- package/src/types.ts +2 -4
- package/src/ui/add-repos.ts +127 -56
- package/src/ui/common/command-runner.ts +270 -69
- package/src/ui/common/keyboard.ts +26 -0
- package/src/ui/common/repo-formatter.ts +4 -9
- package/src/ui/new-session.ts +2 -3
- package/src/ui/progress.ts +1 -1
- package/src/ui/session-settings.ts +2 -17
- package/src/utils/stream.ts +89 -0
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { type CliRenderer, TextRenderable } from "@opentui/core";
|
|
1
|
+
import { type CliRenderer, TextRenderable, type Renderable, type KeyEvent } from "@opentui/core";
|
|
2
2
|
import { createBaseLayout, clearLayout } from "../renderer";
|
|
3
|
-
import {
|
|
3
|
+
import { readProcessOutputWithControl } from "../../utils/stream";
|
|
4
|
+
import { showFooter, hideFooter } from "./footer";
|
|
5
|
+
import { isAbort, isSkip, isBackground } from "./keyboard";
|
|
6
|
+
import { registerBackgroundProcess } from "../../process-registry";
|
|
4
7
|
|
|
5
8
|
export interface CommandTask {
|
|
6
9
|
label: string; // "Cloning microsoft/playwright"
|
|
@@ -13,14 +16,21 @@ export interface CommandRunnerOptions {
|
|
|
13
16
|
tasks: CommandTask[]; // Array of commands to run
|
|
14
17
|
showOutput?: boolean; // Show last N lines (default: true)
|
|
15
18
|
outputLines?: number; // How many lines to show (default: 5)
|
|
19
|
+
allowAbort?: boolean; // Show 'a' Abort
|
|
20
|
+
allowSkip?: boolean; // Show 's' Skip (multi-task only)
|
|
21
|
+
allowBackground?: boolean; // Show 'b' Background
|
|
22
|
+
sessionName?: string; // Session name for tracking backgrounded processes
|
|
16
23
|
}
|
|
17
24
|
|
|
25
|
+
export type TaskOutcome = "completed" | "error" | "aborted" | "skipped" | "backgrounded";
|
|
26
|
+
|
|
18
27
|
export interface CommandResult {
|
|
19
28
|
task: CommandTask;
|
|
20
29
|
success: boolean;
|
|
21
30
|
exitCode: number;
|
|
22
31
|
output: string[];
|
|
23
32
|
error?: string;
|
|
33
|
+
outcome: TaskOutcome;
|
|
24
34
|
}
|
|
25
35
|
|
|
26
36
|
let taskTexts: TextRenderable[] = [];
|
|
@@ -28,7 +38,16 @@ let currentCommandText: TextRenderable | null = null;
|
|
|
28
38
|
let outputText: TextRenderable | null = null;
|
|
29
39
|
let tasksLabel: TextRenderable | null = null;
|
|
30
40
|
|
|
31
|
-
|
|
41
|
+
export type TaskStatus =
|
|
42
|
+
| "pending"
|
|
43
|
+
| "running"
|
|
44
|
+
| "done"
|
|
45
|
+
| "error"
|
|
46
|
+
| "aborted"
|
|
47
|
+
| "skipped"
|
|
48
|
+
| "backgrounded";
|
|
49
|
+
|
|
50
|
+
function getStatusIcon(status: TaskStatus): {
|
|
32
51
|
icon: string;
|
|
33
52
|
color: string;
|
|
34
53
|
} {
|
|
@@ -39,6 +58,12 @@ function getStatusIcon(status: "pending" | "running" | "done" | "error"): {
|
|
|
39
58
|
return { icon: "[~]", color: "#fbbf24" };
|
|
40
59
|
case "error":
|
|
41
60
|
return { icon: "[✗]", color: "#ef4444" };
|
|
61
|
+
case "aborted":
|
|
62
|
+
return { icon: "[X]", color: "#ef4444" };
|
|
63
|
+
case "skipped":
|
|
64
|
+
return { icon: "[>]", color: "#f59e0b" };
|
|
65
|
+
case "backgrounded":
|
|
66
|
+
return { icon: "[~]", color: "#38bdf8" };
|
|
42
67
|
case "pending":
|
|
43
68
|
default:
|
|
44
69
|
return { icon: "[ ]", color: "#94a3b8" };
|
|
@@ -49,7 +74,8 @@ export function showCommandRunner(
|
|
|
49
74
|
renderer: CliRenderer,
|
|
50
75
|
options: CommandRunnerOptions,
|
|
51
76
|
): {
|
|
52
|
-
taskStatuses: Array<{ task: CommandTask; status:
|
|
77
|
+
taskStatuses: Array<{ task: CommandTask; status: TaskStatus }>;
|
|
78
|
+
content: Renderable;
|
|
53
79
|
} {
|
|
54
80
|
clearLayout(renderer);
|
|
55
81
|
taskTexts = [];
|
|
@@ -106,12 +132,12 @@ export function showCommandRunner(
|
|
|
106
132
|
content.add(outputText);
|
|
107
133
|
}
|
|
108
134
|
|
|
109
|
-
return { taskStatuses };
|
|
135
|
+
return { taskStatuses, content };
|
|
110
136
|
}
|
|
111
137
|
|
|
112
138
|
export function updateCommandRunner(
|
|
113
139
|
renderer: CliRenderer,
|
|
114
|
-
taskStatuses: Array<{ task: CommandTask; status:
|
|
140
|
+
taskStatuses: Array<{ task: CommandTask; status: TaskStatus }>,
|
|
115
141
|
currentTaskIndex?: number,
|
|
116
142
|
outputLines?: string[],
|
|
117
143
|
): void {
|
|
@@ -143,6 +169,7 @@ export function updateCommandRunner(
|
|
|
143
169
|
}
|
|
144
170
|
|
|
145
171
|
export function hideCommandRunner(renderer: CliRenderer): void {
|
|
172
|
+
hideFooter(renderer);
|
|
146
173
|
clearLayout(renderer);
|
|
147
174
|
taskTexts = [];
|
|
148
175
|
currentCommandText = null;
|
|
@@ -150,94 +177,265 @@ export function hideCommandRunner(renderer: CliRenderer): void {
|
|
|
150
177
|
tasksLabel = null;
|
|
151
178
|
}
|
|
152
179
|
|
|
180
|
+
type ControlAction = "abort" | "skip" | "background" | null;
|
|
181
|
+
|
|
153
182
|
export async function runCommands(
|
|
154
183
|
renderer: CliRenderer,
|
|
155
184
|
options: CommandRunnerOptions,
|
|
156
185
|
): Promise<CommandResult[]> {
|
|
157
|
-
const {
|
|
158
|
-
|
|
159
|
-
|
|
186
|
+
const {
|
|
187
|
+
tasks,
|
|
188
|
+
showOutput = true,
|
|
189
|
+
outputLines = 5,
|
|
190
|
+
allowAbort = false,
|
|
191
|
+
allowSkip = false,
|
|
192
|
+
allowBackground = false,
|
|
193
|
+
sessionName,
|
|
194
|
+
} = options;
|
|
195
|
+
|
|
196
|
+
const { taskStatuses, content } = showCommandRunner(renderer, options);
|
|
160
197
|
const results: CommandResult[] = [];
|
|
161
198
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
199
|
+
// Track control state
|
|
200
|
+
let controlAction: ControlAction = null;
|
|
201
|
+
let abortAll = false;
|
|
202
|
+
let currentProc: ReturnType<typeof Bun.spawn> | null = null;
|
|
203
|
+
let backgroundResolve: (() => void) | null = null;
|
|
165
204
|
|
|
166
|
-
|
|
205
|
+
// Build footer hints
|
|
206
|
+
const footerHints: string[] = [];
|
|
207
|
+
if (allowAbort) {
|
|
208
|
+
footerHints.push(tasks.length > 1 ? "a Abort All" : "a Abort");
|
|
209
|
+
}
|
|
210
|
+
if (allowSkip && tasks.length > 1) {
|
|
211
|
+
footerHints.push("s Skip");
|
|
212
|
+
}
|
|
213
|
+
if (allowBackground) {
|
|
214
|
+
footerHints.push("b Background");
|
|
215
|
+
}
|
|
167
216
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
217
|
+
// Show footer with control options
|
|
218
|
+
if (footerHints.length > 0) {
|
|
219
|
+
showFooter(renderer, content, {
|
|
220
|
+
navigate: false,
|
|
221
|
+
select: false,
|
|
222
|
+
back: false,
|
|
223
|
+
custom: footerHints,
|
|
224
|
+
});
|
|
225
|
+
renderer.requestRender();
|
|
226
|
+
}
|
|
171
227
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
228
|
+
// Set up keyboard listener
|
|
229
|
+
const keyHandler = (key: KeyEvent) => {
|
|
230
|
+
if (allowAbort && isAbort(key)) {
|
|
231
|
+
controlAction = "abort";
|
|
232
|
+
abortAll = true;
|
|
233
|
+
if (currentProc) {
|
|
234
|
+
currentProc.kill("SIGTERM");
|
|
235
|
+
}
|
|
236
|
+
} else if (allowSkip && tasks.length > 1 && isSkip(key)) {
|
|
237
|
+
controlAction = "skip";
|
|
238
|
+
if (currentProc) {
|
|
239
|
+
currentProc.kill("SIGTERM");
|
|
240
|
+
}
|
|
241
|
+
} else if (allowBackground && isBackground(key)) {
|
|
242
|
+
controlAction = "background";
|
|
243
|
+
// Don't kill - just detach, but signal to move on
|
|
244
|
+
if (backgroundResolve) {
|
|
245
|
+
backgroundResolve();
|
|
246
|
+
backgroundResolve = null;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
};
|
|
178
250
|
|
|
179
|
-
|
|
251
|
+
// Subscribe to keyboard events via renderer's keyInput
|
|
252
|
+
if (footerHints.length > 0) {
|
|
253
|
+
renderer.keyInput.on("keypress", keyHandler);
|
|
254
|
+
}
|
|
180
255
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (showOutput) {
|
|
186
|
-
updateCommandRunner(renderer, taskStatuses, i, buffer);
|
|
187
|
-
}
|
|
188
|
-
},
|
|
189
|
-
});
|
|
256
|
+
try {
|
|
257
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
258
|
+
const task = tasks[i];
|
|
259
|
+
const taskState = taskStatuses[i];
|
|
190
260
|
|
|
191
|
-
|
|
261
|
+
if (!task || !taskState) continue;
|
|
192
262
|
|
|
193
|
-
|
|
194
|
-
|
|
263
|
+
// If abort all was triggered, mark remaining tasks as aborted
|
|
264
|
+
if (abortAll) {
|
|
265
|
+
taskState.status = "aborted";
|
|
195
266
|
results.push({
|
|
196
267
|
task,
|
|
197
|
-
success:
|
|
198
|
-
exitCode:
|
|
199
|
-
output:
|
|
268
|
+
success: false,
|
|
269
|
+
exitCode: -1,
|
|
270
|
+
output: [],
|
|
271
|
+
outcome: "aborted",
|
|
272
|
+
});
|
|
273
|
+
updateCommandRunner(renderer, taskStatuses, i, []);
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Reset control action for this task
|
|
278
|
+
controlAction = null;
|
|
279
|
+
|
|
280
|
+
// Update status to running
|
|
281
|
+
taskState.status = "running";
|
|
282
|
+
updateCommandRunner(renderer, taskStatuses, i, []);
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
const proc = Bun.spawn(task.command, {
|
|
286
|
+
cwd: task.cwd,
|
|
287
|
+
stdout: "pipe",
|
|
288
|
+
stderr: "pipe",
|
|
289
|
+
});
|
|
290
|
+
currentProc = proc;
|
|
291
|
+
|
|
292
|
+
let outputBuffer: string[] = [];
|
|
293
|
+
|
|
294
|
+
// Create a promise that resolves when background is requested
|
|
295
|
+
const backgroundPromise = new Promise<"background">((resolve) => {
|
|
296
|
+
backgroundResolve = () => resolve("background");
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// Race between normal completion and background request
|
|
300
|
+
const streamPromise = readProcessOutputWithControl(proc, {
|
|
301
|
+
maxBufferLines: outputLines,
|
|
302
|
+
onBufferUpdate: (buffer) => {
|
|
303
|
+
outputBuffer = buffer;
|
|
304
|
+
if (showOutput) {
|
|
305
|
+
updateCommandRunner(renderer, taskStatuses, i, buffer);
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
shouldStop: () => controlAction !== null && controlAction !== "background",
|
|
200
309
|
});
|
|
201
|
-
|
|
310
|
+
|
|
311
|
+
const raceResult = await Promise.race([
|
|
312
|
+
streamPromise.then((r) => ({ type: "stream" as const, ...r })),
|
|
313
|
+
backgroundPromise.then(() => ({ type: "background" as const })),
|
|
314
|
+
]);
|
|
315
|
+
|
|
316
|
+
backgroundResolve = null;
|
|
317
|
+
currentProc = null;
|
|
318
|
+
|
|
319
|
+
// Extract stream result or create default for background
|
|
320
|
+
const { success, output, fullOutput, wasInterrupted } =
|
|
321
|
+
raceResult.type === "stream"
|
|
322
|
+
? raceResult
|
|
323
|
+
: { success: true, output: outputBuffer, fullOutput: "", wasInterrupted: false };
|
|
324
|
+
|
|
325
|
+
// Handle control actions
|
|
326
|
+
if (controlAction === "abort" || abortAll) {
|
|
327
|
+
taskState.status = "aborted";
|
|
328
|
+
results.push({
|
|
329
|
+
task,
|
|
330
|
+
success: false,
|
|
331
|
+
exitCode: -1,
|
|
332
|
+
output: outputBuffer.length > 0 ? outputBuffer : output,
|
|
333
|
+
outcome: "aborted",
|
|
334
|
+
});
|
|
335
|
+
// abortAll is already set, remaining tasks will be marked as aborted
|
|
336
|
+
} else if (controlAction === "skip") {
|
|
337
|
+
taskState.status = "skipped";
|
|
338
|
+
results.push({
|
|
339
|
+
task,
|
|
340
|
+
success: false,
|
|
341
|
+
exitCode: -1,
|
|
342
|
+
output: outputBuffer.length > 0 ? outputBuffer : output,
|
|
343
|
+
outcome: "skipped",
|
|
344
|
+
});
|
|
345
|
+
} else if (controlAction === "background") {
|
|
346
|
+
taskState.status = "backgrounded";
|
|
347
|
+
|
|
348
|
+
// Register the background process for tracking
|
|
349
|
+
if (proc.pid && sessionName) {
|
|
350
|
+
await registerBackgroundProcess(
|
|
351
|
+
proc.pid,
|
|
352
|
+
task.command.join(" "),
|
|
353
|
+
task.label,
|
|
354
|
+
sessionName,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
results.push({
|
|
359
|
+
task,
|
|
360
|
+
success: true, // Consider backgrounded as success for flow purposes
|
|
361
|
+
exitCode: 0,
|
|
362
|
+
output: outputBuffer.length > 0 ? outputBuffer : output,
|
|
363
|
+
outcome: "backgrounded",
|
|
364
|
+
});
|
|
365
|
+
} else if (wasInterrupted) {
|
|
366
|
+
// Was interrupted but no specific action (shouldn't happen)
|
|
367
|
+
taskState.status = "error";
|
|
368
|
+
results.push({
|
|
369
|
+
task,
|
|
370
|
+
success: false,
|
|
371
|
+
exitCode: -1,
|
|
372
|
+
output: outputBuffer.length > 0 ? outputBuffer : output,
|
|
373
|
+
error: "Command was interrupted",
|
|
374
|
+
outcome: "error",
|
|
375
|
+
});
|
|
376
|
+
} else {
|
|
377
|
+
// Normal completion
|
|
378
|
+
const exitCode = await proc.exited;
|
|
379
|
+
|
|
380
|
+
if (success && exitCode === 0) {
|
|
381
|
+
taskState.status = "done";
|
|
382
|
+
results.push({
|
|
383
|
+
task,
|
|
384
|
+
success: true,
|
|
385
|
+
exitCode: 0,
|
|
386
|
+
output: outputBuffer.length > 0 ? outputBuffer : output,
|
|
387
|
+
outcome: "completed",
|
|
388
|
+
});
|
|
389
|
+
} else {
|
|
390
|
+
taskState.status = "error";
|
|
391
|
+
const errorMsg = fullOutput.trim() || `Command exited with code ${exitCode}`;
|
|
392
|
+
results.push({
|
|
393
|
+
task,
|
|
394
|
+
success: false,
|
|
395
|
+
exitCode,
|
|
396
|
+
output: outputBuffer.length > 0 ? outputBuffer : output,
|
|
397
|
+
error: errorMsg,
|
|
398
|
+
outcome: "error",
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Final update with last output
|
|
404
|
+
if (showOutput) {
|
|
405
|
+
updateCommandRunner(
|
|
406
|
+
renderer,
|
|
407
|
+
taskStatuses,
|
|
408
|
+
i,
|
|
409
|
+
outputBuffer.length > 0 ? outputBuffer : output,
|
|
410
|
+
);
|
|
411
|
+
} else {
|
|
412
|
+
updateCommandRunner(renderer, taskStatuses, i);
|
|
413
|
+
}
|
|
414
|
+
} catch (error) {
|
|
415
|
+
currentProc = null;
|
|
202
416
|
taskState.status = "error";
|
|
203
|
-
const errorMsg =
|
|
417
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
204
418
|
results.push({
|
|
205
419
|
task,
|
|
206
420
|
success: false,
|
|
207
|
-
exitCode,
|
|
208
|
-
output:
|
|
421
|
+
exitCode: 1,
|
|
422
|
+
output: [],
|
|
209
423
|
error: errorMsg,
|
|
424
|
+
outcome: "error",
|
|
210
425
|
});
|
|
211
|
-
}
|
|
212
426
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
renderer,
|
|
217
|
-
|
|
218
|
-
i,
|
|
219
|
-
outputBuffer.length > 0 ? outputBuffer : output,
|
|
220
|
-
);
|
|
221
|
-
} else {
|
|
222
|
-
updateCommandRunner(renderer, taskStatuses, i);
|
|
223
|
-
}
|
|
224
|
-
} catch (error) {
|
|
225
|
-
taskState.status = "error";
|
|
226
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
227
|
-
results.push({
|
|
228
|
-
task,
|
|
229
|
-
success: false,
|
|
230
|
-
exitCode: 1,
|
|
231
|
-
output: [],
|
|
232
|
-
error: errorMsg,
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
if (showOutput) {
|
|
236
|
-
updateCommandRunner(renderer, taskStatuses, i, [errorMsg]);
|
|
237
|
-
} else {
|
|
238
|
-
updateCommandRunner(renderer, taskStatuses, i);
|
|
427
|
+
if (showOutput) {
|
|
428
|
+
updateCommandRunner(renderer, taskStatuses, i, [errorMsg]);
|
|
429
|
+
} else {
|
|
430
|
+
updateCommandRunner(renderer, taskStatuses, i);
|
|
431
|
+
}
|
|
239
432
|
}
|
|
240
433
|
}
|
|
434
|
+
} finally {
|
|
435
|
+
// Clean up keyboard listener
|
|
436
|
+
if (footerHints.length > 0) {
|
|
437
|
+
renderer.keyInput.off("keypress", keyHandler);
|
|
438
|
+
}
|
|
241
439
|
}
|
|
242
440
|
|
|
243
441
|
// Clear current command indicator when done
|
|
@@ -245,5 +443,8 @@ export async function runCommands(
|
|
|
245
443
|
currentCommandText.content = "";
|
|
246
444
|
}
|
|
247
445
|
|
|
446
|
+
// Hide footer
|
|
447
|
+
hideFooter(renderer);
|
|
448
|
+
|
|
248
449
|
return results;
|
|
249
450
|
}
|
|
@@ -31,6 +31,11 @@ export const KEYBOARD = {
|
|
|
31
31
|
// Text input shortcuts
|
|
32
32
|
BACKSPACE: "backspace",
|
|
33
33
|
CTRL_D: "d", // Ctrl+D (handled via ctrl modifier)
|
|
34
|
+
|
|
35
|
+
// Command control shortcuts
|
|
36
|
+
ABORT: "a",
|
|
37
|
+
SKIP: "s",
|
|
38
|
+
BACKGROUND: "b",
|
|
34
39
|
} as const;
|
|
35
40
|
|
|
36
41
|
/**
|
|
@@ -93,3 +98,24 @@ export function isNavigation(key: KeyEvent): boolean {
|
|
|
93
98
|
key.name === KEYBOARD.RIGHT
|
|
94
99
|
);
|
|
95
100
|
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check if a key event is Abort (a)
|
|
104
|
+
*/
|
|
105
|
+
export function isAbort(key: Pick<KeyEvent, "name">): boolean {
|
|
106
|
+
return key.name === KEYBOARD.ABORT;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Check if a key event is Skip (s)
|
|
111
|
+
*/
|
|
112
|
+
export function isSkip(key: Pick<KeyEvent, "name">): boolean {
|
|
113
|
+
return key.name === KEYBOARD.SKIP;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Check if a key event is Background (b)
|
|
118
|
+
*/
|
|
119
|
+
export function isBackground(key: Pick<KeyEvent, "name">): boolean {
|
|
120
|
+
return key.name === KEYBOARD.BACKGROUND;
|
|
121
|
+
}
|
|
@@ -16,9 +16,8 @@ export function formatRepoList(repos: RepoSpec[], options: RepoFormatterOptions
|
|
|
16
16
|
|
|
17
17
|
if (showMarkers) {
|
|
18
18
|
const branchMarker = repo.branch ? ` (${repo.branch})` : "";
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
text += `${branchMarker}${roMarker}${latestMarker}`;
|
|
19
|
+
const refMarker = repo.reference ? " [Ref]" : "";
|
|
20
|
+
text += `${branchMarker}${refMarker}`;
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
return text;
|
|
@@ -33,12 +32,8 @@ export function formatRepoString(repo: RepoSpec): string {
|
|
|
33
32
|
parts.push(`(${repo.branch})`);
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
if (repo.
|
|
37
|
-
parts.push("[
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (repo.latest) {
|
|
41
|
-
parts.push("[Latest]");
|
|
35
|
+
if (repo.reference) {
|
|
36
|
+
parts.push("[Ref]");
|
|
42
37
|
}
|
|
43
38
|
|
|
44
39
|
return parts.join(" ");
|
package/src/ui/new-session.ts
CHANGED
|
@@ -27,11 +27,10 @@ export function showNewSessionPrompt(
|
|
|
27
27
|
|
|
28
28
|
repos.forEach((repo, i) => {
|
|
29
29
|
const branch = repo.branch ? ` (${repo.branch})` : " (default)";
|
|
30
|
-
const
|
|
31
|
-
const latestMarker = repo.latest ? " [Latest]" : "";
|
|
30
|
+
const refMarker = repo.reference ? " [Ref]" : "";
|
|
32
31
|
const repoText = new TextRenderable(renderer, {
|
|
33
32
|
id: `repo-${i}`,
|
|
34
|
-
content: ` - ${repo.owner}/${repo.name}${branch}${
|
|
33
|
+
content: ` - ${repo.owner}/${repo.name}${branch}${refMarker}`,
|
|
35
34
|
fg: "#94a3b8",
|
|
36
35
|
});
|
|
37
36
|
content.add(repoText);
|
package/src/ui/progress.ts
CHANGED
|
@@ -52,7 +52,7 @@ function getPhasePresentation(phase: ProgressPhase): { content: string; fg: stri
|
|
|
52
52
|
case "installing-skills":
|
|
53
53
|
return { content: "Installing skills...", fg: "#38bdf8" };
|
|
54
54
|
case "refreshing":
|
|
55
|
-
return { content: "Refreshing
|
|
55
|
+
return { content: "Refreshing reference repositories...", fg: "#38bdf8" };
|
|
56
56
|
case "done":
|
|
57
57
|
return { content: "Ready!", fg: "#22c55e" };
|
|
58
58
|
default:
|
|
@@ -208,7 +208,7 @@ export function showSessionSettings(
|
|
|
208
208
|
if (selectedTarget === "goal") {
|
|
209
209
|
customActions.push("Enter Edit");
|
|
210
210
|
} else if (selectedTarget === "repo") {
|
|
211
|
-
customActions.push("r Toggle
|
|
211
|
+
customActions.push("r Toggle Ref", "a Add repos");
|
|
212
212
|
} else if (selectedTarget === "skill") {
|
|
213
213
|
customActions.push("x Remove", "a Add repos");
|
|
214
214
|
}
|
|
@@ -342,22 +342,7 @@ export function showSessionSettings(
|
|
|
342
342
|
if (key.name === "r" && selectedTarget === "repo") {
|
|
343
343
|
const repo = updatedRepos[selectedRepoIndex];
|
|
344
344
|
if (repo) {
|
|
345
|
-
repo.
|
|
346
|
-
if (!repo.readOnly) {
|
|
347
|
-
repo.latest = false;
|
|
348
|
-
}
|
|
349
|
-
updateReposList();
|
|
350
|
-
}
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
if (key.name === "l" && selectedTarget === "repo") {
|
|
355
|
-
const repo = updatedRepos[selectedRepoIndex];
|
|
356
|
-
if (repo) {
|
|
357
|
-
repo.latest = !repo.latest;
|
|
358
|
-
if (repo.latest) {
|
|
359
|
-
repo.readOnly = true;
|
|
360
|
-
}
|
|
345
|
+
repo.reference = !repo.reference;
|
|
361
346
|
updateReposList();
|
|
362
347
|
}
|
|
363
348
|
return;
|
package/src/utils/stream.ts
CHANGED
|
@@ -51,6 +51,12 @@ interface ReadProcessOutputWithBufferOptions {
|
|
|
51
51
|
onBufferUpdate?: (buffer: string[]) => void;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
export interface ReadWithControlOptions {
|
|
55
|
+
maxBufferLines?: number;
|
|
56
|
+
onBufferUpdate?: (buffer: string[]) => void;
|
|
57
|
+
shouldStop?: () => boolean; // Check if we should stop early
|
|
58
|
+
}
|
|
59
|
+
|
|
54
60
|
export async function readProcessOutputWithBuffer(
|
|
55
61
|
proc: ReturnType<typeof Bun.spawn>,
|
|
56
62
|
options?: ReadProcessOutputWithBufferOptions,
|
|
@@ -106,3 +112,86 @@ export async function readProcessOutputWithBuffer(
|
|
|
106
112
|
fullOutput: fullOutputParts.join(""),
|
|
107
113
|
};
|
|
108
114
|
}
|
|
115
|
+
|
|
116
|
+
export async function readProcessOutputWithControl(
|
|
117
|
+
proc: ReturnType<typeof Bun.spawn>,
|
|
118
|
+
options?: ReadWithControlOptions,
|
|
119
|
+
): Promise<{ success: boolean; output: string[]; fullOutput: string; wasInterrupted: boolean }> {
|
|
120
|
+
const { maxBufferLines, onBufferUpdate, shouldStop } = options || {};
|
|
121
|
+
const outputBuffer: string[] = [];
|
|
122
|
+
let wasInterrupted = false;
|
|
123
|
+
|
|
124
|
+
const addLine = (line: string) => {
|
|
125
|
+
const trimmed = line.trim();
|
|
126
|
+
if (trimmed) {
|
|
127
|
+
outputBuffer.push(trimmed);
|
|
128
|
+
if (maxBufferLines && outputBuffer.length > maxBufferLines) {
|
|
129
|
+
outputBuffer.shift();
|
|
130
|
+
}
|
|
131
|
+
onBufferUpdate?.([...outputBuffer]);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const fullOutputParts: string[] = [];
|
|
136
|
+
|
|
137
|
+
const readStream = async (stream: ReadableStream<Uint8Array> | number | undefined) => {
|
|
138
|
+
if (!stream || typeof stream === "number") return;
|
|
139
|
+
|
|
140
|
+
const reader = stream.getReader();
|
|
141
|
+
const decoder = new TextDecoder();
|
|
142
|
+
let buffer = "";
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
while (true) {
|
|
146
|
+
// Check if we should stop before each read
|
|
147
|
+
if (shouldStop?.()) {
|
|
148
|
+
wasInterrupted = true;
|
|
149
|
+
reader.releaseLock();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const { done, value } = await reader.read();
|
|
154
|
+
if (done) break;
|
|
155
|
+
|
|
156
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
157
|
+
fullOutputParts.push(chunk);
|
|
158
|
+
buffer += chunk;
|
|
159
|
+
|
|
160
|
+
const lines = buffer.split(/[\r\n]+/);
|
|
161
|
+
buffer = lines.pop() || "";
|
|
162
|
+
|
|
163
|
+
for (const line of lines) {
|
|
164
|
+
addLine(line);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Check again after processing
|
|
168
|
+
if (shouldStop?.()) {
|
|
169
|
+
wasInterrupted = true;
|
|
170
|
+
reader.releaseLock();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (buffer.trim()) {
|
|
176
|
+
addLine(buffer);
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
// Stream may have been cancelled, that's ok
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
await Promise.all([readStream(proc.stdout), readStream(proc.stderr)]);
|
|
184
|
+
|
|
185
|
+
// Only wait for exit if not interrupted
|
|
186
|
+
let exitCode = 1;
|
|
187
|
+
if (!wasInterrupted) {
|
|
188
|
+
exitCode = await proc.exited;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
success: !wasInterrupted && exitCode === 0,
|
|
193
|
+
output: outputBuffer,
|
|
194
|
+
fullOutput: fullOutputParts.join(""),
|
|
195
|
+
wasInterrupted,
|
|
196
|
+
};
|
|
197
|
+
}
|