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.
@@ -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 { readProcessOutputWithBuffer } from "../../utils/stream";
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
- function getStatusIcon(status: "pending" | "running" | "done" | "error"): {
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: "pending" | "running" | "done" | "error" }>;
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: "pending" | "running" | "done" | "error" }>,
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 { tasks, showOutput = true, outputLines = 5 } = options;
158
-
159
- const { taskStatuses } = showCommandRunner(renderer, options);
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
- for (let i = 0; i < tasks.length; i++) {
163
- const task = tasks[i];
164
- const taskState = taskStatuses[i];
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
- if (!task || !taskState) continue;
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
- // Update status to running
169
- taskState.status = "running";
170
- updateCommandRunner(renderer, taskStatuses, i, []);
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
- try {
173
- const proc = Bun.spawn(task.command, {
174
- cwd: task.cwd,
175
- stdout: "pipe",
176
- stderr: "pipe",
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
- let outputBuffer: string[] = [];
251
+ // Subscribe to keyboard events via renderer's keyInput
252
+ if (footerHints.length > 0) {
253
+ renderer.keyInput.on("keypress", keyHandler);
254
+ }
180
255
 
181
- const { success, output, fullOutput } = await readProcessOutputWithBuffer(proc, {
182
- maxBufferLines: outputLines,
183
- onBufferUpdate: (buffer) => {
184
- outputBuffer = buffer;
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
- const exitCode = await proc.exited;
261
+ if (!task || !taskState) continue;
192
262
 
193
- if (success && exitCode === 0) {
194
- taskState.status = "done";
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: true,
198
- exitCode: 0,
199
- output: outputBuffer.length > 0 ? outputBuffer : 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
- } else {
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 = fullOutput.trim() || `Command exited with code ${exitCode}`;
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: outputBuffer.length > 0 ? outputBuffer : output,
421
+ exitCode: 1,
422
+ output: [],
209
423
  error: errorMsg,
424
+ outcome: "error",
210
425
  });
211
- }
212
426
 
213
- // Final update with last output
214
- if (showOutput) {
215
- updateCommandRunner(
216
- renderer,
217
- taskStatuses,
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 roMarker = repo.readOnly ? " [RO]" : "";
20
- const latestMarker = repo.latest ? " [Latest]" : "";
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.readOnly) {
37
- parts.push("[RO]");
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(" ");