letmecook 0.0.15 → 0.0.17
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 +26 -70
- package/src/ui/background-warning.ts +196 -0
- 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
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CliRenderer,
|
|
3
|
+
TextRenderable,
|
|
4
|
+
SelectRenderable,
|
|
5
|
+
SelectRenderableEvents,
|
|
6
|
+
type KeyEvent,
|
|
7
|
+
} from "@opentui/core";
|
|
8
|
+
import { createBaseLayout, clearLayout } from "./renderer";
|
|
9
|
+
import { showFooter, hideFooter } from "./common/footer";
|
|
10
|
+
import { isEscape } from "./common/keyboard";
|
|
11
|
+
import type { BackgroundProcess } from "../process-registry";
|
|
12
|
+
|
|
13
|
+
export type QuitWarningChoice = "continue" | "kill" | "cancel";
|
|
14
|
+
|
|
15
|
+
export function showQuitWarning(
|
|
16
|
+
renderer: CliRenderer,
|
|
17
|
+
processes: BackgroundProcess[],
|
|
18
|
+
): Promise<QuitWarningChoice> {
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
clearLayout(renderer);
|
|
21
|
+
|
|
22
|
+
const { content } = createBaseLayout(renderer, "Background processes running");
|
|
23
|
+
|
|
24
|
+
const warning = new TextRenderable(renderer, {
|
|
25
|
+
id: "warning",
|
|
26
|
+
content: `${processes.length} background process${processes.length > 1 ? "es" : ""} still running:`,
|
|
27
|
+
fg: "#f59e0b",
|
|
28
|
+
marginBottom: 1,
|
|
29
|
+
});
|
|
30
|
+
content.add(warning);
|
|
31
|
+
|
|
32
|
+
// List the running processes
|
|
33
|
+
processes.forEach((proc, i) => {
|
|
34
|
+
const processInfo = new TextRenderable(renderer, {
|
|
35
|
+
id: `process-${i}`,
|
|
36
|
+
content: ` • ${proc.description}`,
|
|
37
|
+
fg: "#94a3b8",
|
|
38
|
+
});
|
|
39
|
+
content.add(processInfo);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const question = new TextRenderable(renderer, {
|
|
43
|
+
id: "question",
|
|
44
|
+
content: "What would you like to do?",
|
|
45
|
+
fg: "#e2e8f0",
|
|
46
|
+
marginTop: 1,
|
|
47
|
+
});
|
|
48
|
+
content.add(question);
|
|
49
|
+
|
|
50
|
+
const select = new SelectRenderable(renderer, {
|
|
51
|
+
id: "quit-warning-select",
|
|
52
|
+
width: 38,
|
|
53
|
+
height: 3,
|
|
54
|
+
options: [
|
|
55
|
+
{ name: "Keep running & quit", description: "", value: "continue" },
|
|
56
|
+
{ name: "Kill all & quit", description: "", value: "kill" },
|
|
57
|
+
{ name: "Cancel", description: "", value: "cancel" },
|
|
58
|
+
],
|
|
59
|
+
showDescription: false,
|
|
60
|
+
backgroundColor: "transparent",
|
|
61
|
+
focusedBackgroundColor: "transparent",
|
|
62
|
+
selectedBackgroundColor: "#334155",
|
|
63
|
+
textColor: "#e2e8f0",
|
|
64
|
+
selectedTextColor: "#38bdf8",
|
|
65
|
+
marginTop: 1,
|
|
66
|
+
});
|
|
67
|
+
content.add(select);
|
|
68
|
+
|
|
69
|
+
select.focus();
|
|
70
|
+
|
|
71
|
+
const handleSelect = (_index: number, option: { value: string }) => {
|
|
72
|
+
cleanup();
|
|
73
|
+
resolve(option.value as QuitWarningChoice);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const handleKeypress = (key: KeyEvent) => {
|
|
77
|
+
if (isEscape(key)) {
|
|
78
|
+
cleanup();
|
|
79
|
+
resolve("cancel");
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const cleanup = () => {
|
|
84
|
+
select.off(SelectRenderableEvents.ITEM_SELECTED, handleSelect);
|
|
85
|
+
renderer.keyInput.off("keypress", handleKeypress);
|
|
86
|
+
select.blur();
|
|
87
|
+
hideFooter(renderer);
|
|
88
|
+
clearLayout(renderer);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
showFooter(renderer, content, {
|
|
92
|
+
navigate: true,
|
|
93
|
+
select: true,
|
|
94
|
+
back: true,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
select.on(SelectRenderableEvents.ITEM_SELECTED, handleSelect);
|
|
98
|
+
renderer.keyInput.on("keypress", handleKeypress);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export type SessionStartWarningChoice = "continue" | "cancel";
|
|
103
|
+
|
|
104
|
+
export function showSessionStartWarning(
|
|
105
|
+
renderer: CliRenderer,
|
|
106
|
+
processes: BackgroundProcess[],
|
|
107
|
+
): Promise<SessionStartWarningChoice> {
|
|
108
|
+
return new Promise((resolve) => {
|
|
109
|
+
clearLayout(renderer);
|
|
110
|
+
|
|
111
|
+
const { content } = createBaseLayout(renderer, "Background processes detected");
|
|
112
|
+
|
|
113
|
+
const warning = new TextRenderable(renderer, {
|
|
114
|
+
id: "warning",
|
|
115
|
+
content: `${processes.length} background process${processes.length > 1 ? "es" : ""} still running for this session:`,
|
|
116
|
+
fg: "#f59e0b",
|
|
117
|
+
marginBottom: 1,
|
|
118
|
+
});
|
|
119
|
+
content.add(warning);
|
|
120
|
+
|
|
121
|
+
// List the running processes
|
|
122
|
+
processes.forEach((proc, i) => {
|
|
123
|
+
const processInfo = new TextRenderable(renderer, {
|
|
124
|
+
id: `process-${i}`,
|
|
125
|
+
content: ` • ${proc.description}`,
|
|
126
|
+
fg: "#94a3b8",
|
|
127
|
+
});
|
|
128
|
+
content.add(processInfo);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const note = new TextRenderable(renderer, {
|
|
132
|
+
id: "note",
|
|
133
|
+
content: "Some repositories may not be fully cloned yet.",
|
|
134
|
+
fg: "#94a3b8",
|
|
135
|
+
marginTop: 1,
|
|
136
|
+
});
|
|
137
|
+
content.add(note);
|
|
138
|
+
|
|
139
|
+
const question = new TextRenderable(renderer, {
|
|
140
|
+
id: "question",
|
|
141
|
+
content: "Continue with session?",
|
|
142
|
+
fg: "#e2e8f0",
|
|
143
|
+
marginTop: 1,
|
|
144
|
+
});
|
|
145
|
+
content.add(question);
|
|
146
|
+
|
|
147
|
+
const select = new SelectRenderable(renderer, {
|
|
148
|
+
id: "session-warning-select",
|
|
149
|
+
width: 38,
|
|
150
|
+
height: 2,
|
|
151
|
+
options: [
|
|
152
|
+
{ name: "Continue anyway", description: "", value: "continue" },
|
|
153
|
+
{ name: "Cancel", description: "", value: "cancel" },
|
|
154
|
+
],
|
|
155
|
+
showDescription: false,
|
|
156
|
+
backgroundColor: "transparent",
|
|
157
|
+
focusedBackgroundColor: "transparent",
|
|
158
|
+
selectedBackgroundColor: "#334155",
|
|
159
|
+
textColor: "#e2e8f0",
|
|
160
|
+
selectedTextColor: "#38bdf8",
|
|
161
|
+
marginTop: 1,
|
|
162
|
+
});
|
|
163
|
+
content.add(select);
|
|
164
|
+
|
|
165
|
+
select.focus();
|
|
166
|
+
|
|
167
|
+
const handleSelect = (_index: number, option: { value: string }) => {
|
|
168
|
+
cleanup();
|
|
169
|
+
resolve(option.value as SessionStartWarningChoice);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const handleKeypress = (key: KeyEvent) => {
|
|
173
|
+
if (isEscape(key)) {
|
|
174
|
+
cleanup();
|
|
175
|
+
resolve("cancel");
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const cleanup = () => {
|
|
180
|
+
select.off(SelectRenderableEvents.ITEM_SELECTED, handleSelect);
|
|
181
|
+
renderer.keyInput.off("keypress", handleKeypress);
|
|
182
|
+
select.blur();
|
|
183
|
+
hideFooter(renderer);
|
|
184
|
+
clearLayout(renderer);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
showFooter(renderer, content, {
|
|
188
|
+
navigate: true,
|
|
189
|
+
select: true,
|
|
190
|
+
back: true,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
select.on(SelectRenderableEvents.ITEM_SELECTED, handleSelect);
|
|
194
|
+
renderer.keyInput.on("keypress", handleKeypress);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
@@ -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(" ");
|