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
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { mkdir, symlink, readlink, lstat, readdir, rm } from "node:fs/promises";
|
|
4
|
+
import type { RepoSpec } from "./types";
|
|
5
|
+
import { readProcessOutputWithBuffer } from "./utils/stream";
|
|
6
|
+
|
|
7
|
+
const LETMECOOK_DIR = join(homedir(), ".letmecook");
|
|
8
|
+
export const REFERENCES_DIR = join(LETMECOOK_DIR, "references");
|
|
9
|
+
|
|
10
|
+
export interface RefreshResult {
|
|
11
|
+
repo: RepoSpec;
|
|
12
|
+
status: "updated" | "up-to-date" | "skipped" | "error";
|
|
13
|
+
reason?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type RefreshProgressStatus = "refreshing" | "updated" | "up-to-date" | "skipped" | "error";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get the path where a reference repo should be cached.
|
|
20
|
+
* Format: ~/.letmecook/references/{owner}/{name}/{branch|HEAD}
|
|
21
|
+
*/
|
|
22
|
+
export function getReferencePath(repo: RepoSpec): string {
|
|
23
|
+
const branchDir = repo.branch || "HEAD";
|
|
24
|
+
return join(REFERENCES_DIR, repo.owner, repo.name, branchDir);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Ensure the references directory exists.
|
|
29
|
+
*/
|
|
30
|
+
export async function ensureReferencesDir(): Promise<void> {
|
|
31
|
+
await mkdir(REFERENCES_DIR, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Check if a path is a symlink.
|
|
36
|
+
*/
|
|
37
|
+
export async function isSymlink(path: string): Promise<boolean> {
|
|
38
|
+
try {
|
|
39
|
+
const stats = await lstat(path);
|
|
40
|
+
return stats.isSymbolicLink();
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Clone a repo to the reference cache if not already present.
|
|
48
|
+
* Returns the path to the cached repo.
|
|
49
|
+
*/
|
|
50
|
+
export async function ensureReferenceRepo(
|
|
51
|
+
repo: RepoSpec,
|
|
52
|
+
onProgress?: (status: "cloning" | "done" | "error", outputLines?: string[]) => void,
|
|
53
|
+
): Promise<string> {
|
|
54
|
+
const refPath = getReferencePath(repo);
|
|
55
|
+
|
|
56
|
+
// Check if already cached
|
|
57
|
+
const gitDir = join(refPath, ".git");
|
|
58
|
+
const gitDirFile = Bun.file(gitDir);
|
|
59
|
+
if (await gitDirFile.exists()) {
|
|
60
|
+
onProgress?.("done", ["Already cached"]);
|
|
61
|
+
return refPath;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Ensure parent directories exist
|
|
65
|
+
await mkdir(join(REFERENCES_DIR, repo.owner, repo.name), { recursive: true });
|
|
66
|
+
|
|
67
|
+
onProgress?.("cloning");
|
|
68
|
+
|
|
69
|
+
const url = `https://github.com/${repo.owner}/${repo.name}.git`;
|
|
70
|
+
const args = repo.branch
|
|
71
|
+
? [
|
|
72
|
+
"git",
|
|
73
|
+
"clone",
|
|
74
|
+
"--depth",
|
|
75
|
+
"1",
|
|
76
|
+
"--single-branch",
|
|
77
|
+
"--branch",
|
|
78
|
+
repo.branch,
|
|
79
|
+
"--progress",
|
|
80
|
+
url,
|
|
81
|
+
refPath,
|
|
82
|
+
]
|
|
83
|
+
: ["git", "clone", "--depth", "1", "--single-branch", "--progress", url, refPath];
|
|
84
|
+
|
|
85
|
+
const proc = Bun.spawn(args, {
|
|
86
|
+
stdout: "pipe",
|
|
87
|
+
stderr: "pipe",
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const { success, output } = await readProcessOutputWithBuffer(proc, {
|
|
91
|
+
maxBufferLines: 5,
|
|
92
|
+
onBufferUpdate: (buffer) => onProgress?.("cloning", buffer),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (!success) {
|
|
96
|
+
onProgress?.("error", output);
|
|
97
|
+
throw new Error(`Failed to clone reference repo ${repo.owner}/${repo.name}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
onProgress?.("done", output);
|
|
101
|
+
return refPath;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Refresh a cached reference repo with git pull.
|
|
106
|
+
*/
|
|
107
|
+
export async function refreshReferenceRepo(
|
|
108
|
+
repo: RepoSpec,
|
|
109
|
+
onProgress?: (status: RefreshProgressStatus, outputLines?: string[]) => void,
|
|
110
|
+
): Promise<RefreshResult> {
|
|
111
|
+
const refPath = getReferencePath(repo);
|
|
112
|
+
|
|
113
|
+
// Check if cached repo exists
|
|
114
|
+
const gitDir = join(refPath, ".git");
|
|
115
|
+
const gitDirFile = Bun.file(gitDir);
|
|
116
|
+
if (!(await gitDirFile.exists())) {
|
|
117
|
+
// Try to clone it
|
|
118
|
+
try {
|
|
119
|
+
await ensureReferenceRepo(repo, (status, lines) => {
|
|
120
|
+
if (status === "cloning") onProgress?.("refreshing", lines);
|
|
121
|
+
else if (status === "done") onProgress?.("updated", lines);
|
|
122
|
+
else onProgress?.("error", lines);
|
|
123
|
+
});
|
|
124
|
+
return { repo, status: "updated", reason: "newly cloned" };
|
|
125
|
+
} catch (error) {
|
|
126
|
+
return {
|
|
127
|
+
repo,
|
|
128
|
+
status: "error",
|
|
129
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
onProgress?.("refreshing", [`Pulling ${repo.owner}/${repo.name}...`]);
|
|
135
|
+
|
|
136
|
+
const proc = Bun.spawn(["git", "-C", refPath, "pull", "--ff-only", "--depth", "1"], {
|
|
137
|
+
stdout: "pipe",
|
|
138
|
+
stderr: "pipe",
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const { success, output, fullOutput } = await readProcessOutputWithBuffer(proc, {
|
|
142
|
+
maxBufferLines: 5,
|
|
143
|
+
onBufferUpdate: (buffer) => onProgress?.("refreshing", buffer),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (!success) {
|
|
147
|
+
const reason = fullOutput.trim() || "git pull failed";
|
|
148
|
+
onProgress?.("error", output.length > 0 ? output : [reason]);
|
|
149
|
+
return { repo, status: "error", reason };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const normalized = fullOutput.toLowerCase();
|
|
153
|
+
const upToDate =
|
|
154
|
+
normalized.includes("already up to date") || normalized.includes("already up-to-date");
|
|
155
|
+
|
|
156
|
+
const status = upToDate ? "up-to-date" : "updated";
|
|
157
|
+
onProgress?.(status, output);
|
|
158
|
+
return { repo, status };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Create a symlink from session directory to the cached reference repo.
|
|
163
|
+
*/
|
|
164
|
+
export async function linkReferenceRepo(repo: RepoSpec, sessionPath: string): Promise<void> {
|
|
165
|
+
const refPath = getReferencePath(repo);
|
|
166
|
+
const linkPath = join(sessionPath, repo.dir);
|
|
167
|
+
|
|
168
|
+
// Check if link already exists and is valid
|
|
169
|
+
if (await isSymlink(linkPath)) {
|
|
170
|
+
try {
|
|
171
|
+
const target = await readlink(linkPath);
|
|
172
|
+
if (target === refPath) {
|
|
173
|
+
return; // Already correctly linked
|
|
174
|
+
}
|
|
175
|
+
// Remove incorrect symlink
|
|
176
|
+
await rm(linkPath);
|
|
177
|
+
} catch {
|
|
178
|
+
// Remove broken symlink
|
|
179
|
+
await rm(linkPath);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
await symlink(refPath, linkPath);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Verify that a symlink is valid and points to the correct reference.
|
|
188
|
+
* Returns true if valid, false if needs repair.
|
|
189
|
+
*/
|
|
190
|
+
export async function verifyReferenceLink(repo: RepoSpec, sessionPath: string): Promise<boolean> {
|
|
191
|
+
const refPath = getReferencePath(repo);
|
|
192
|
+
const linkPath = join(sessionPath, repo.dir);
|
|
193
|
+
|
|
194
|
+
if (!(await isSymlink(linkPath))) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const target = await readlink(linkPath);
|
|
200
|
+
// Check if target matches expected reference path
|
|
201
|
+
if (target !== refPath) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
// Check if target actually exists
|
|
205
|
+
const gitDir = Bun.file(join(refPath, ".git"));
|
|
206
|
+
return await gitDir.exists();
|
|
207
|
+
} catch {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Repair a broken reference link by re-ensuring the cache and re-linking.
|
|
214
|
+
*/
|
|
215
|
+
export async function repairReferenceLink(
|
|
216
|
+
repo: RepoSpec,
|
|
217
|
+
sessionPath: string,
|
|
218
|
+
onProgress?: (status: "cloning" | "done" | "error", outputLines?: string[]) => void,
|
|
219
|
+
): Promise<void> {
|
|
220
|
+
const linkPath = join(sessionPath, repo.dir);
|
|
221
|
+
|
|
222
|
+
// Remove existing link/directory
|
|
223
|
+
try {
|
|
224
|
+
await rm(linkPath, { recursive: true, force: true });
|
|
225
|
+
} catch {
|
|
226
|
+
// Ignore
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Ensure reference is cached
|
|
230
|
+
await ensureReferenceRepo(repo, onProgress);
|
|
231
|
+
|
|
232
|
+
// Create symlink
|
|
233
|
+
await linkReferenceRepo(repo, sessionPath);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Clean up orphaned references that aren't used by any session.
|
|
238
|
+
* Returns the number of references removed.
|
|
239
|
+
*/
|
|
240
|
+
export async function cleanOrphanedReferences(): Promise<number> {
|
|
241
|
+
// This is a placeholder for future implementation
|
|
242
|
+
// Would need to:
|
|
243
|
+
// 1. List all sessions
|
|
244
|
+
// 2. Collect all reference paths in use
|
|
245
|
+
// 3. Walk references dir and remove unused entries
|
|
246
|
+
return 0;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* List all cached references.
|
|
251
|
+
*/
|
|
252
|
+
export async function listCachedReferences(): Promise<
|
|
253
|
+
Array<{ owner: string; name: string; branch: string; path: string }>
|
|
254
|
+
> {
|
|
255
|
+
const refs: Array<{ owner: string; name: string; branch: string; path: string }> = [];
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
const owners = await readdir(REFERENCES_DIR);
|
|
259
|
+
|
|
260
|
+
for (const owner of owners) {
|
|
261
|
+
const ownerPath = join(REFERENCES_DIR, owner);
|
|
262
|
+
const ownerStat = await lstat(ownerPath);
|
|
263
|
+
if (!ownerStat.isDirectory()) continue;
|
|
264
|
+
|
|
265
|
+
const names = await readdir(ownerPath);
|
|
266
|
+
|
|
267
|
+
for (const name of names) {
|
|
268
|
+
const namePath = join(ownerPath, name);
|
|
269
|
+
const nameStat = await lstat(namePath);
|
|
270
|
+
if (!nameStat.isDirectory()) continue;
|
|
271
|
+
|
|
272
|
+
const branches = await readdir(namePath);
|
|
273
|
+
|
|
274
|
+
for (const branch of branches) {
|
|
275
|
+
const branchPath = join(namePath, branch);
|
|
276
|
+
const branchStat = await lstat(branchPath);
|
|
277
|
+
if (!branchStat.isDirectory()) continue;
|
|
278
|
+
|
|
279
|
+
refs.push({ owner, name, branch, path: branchPath });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
} catch {
|
|
284
|
+
// References dir doesn't exist yet
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return refs;
|
|
288
|
+
}
|
package/src/tui-mode.ts
CHANGED
|
@@ -9,9 +9,11 @@ import { showMainMenu } from "./ui/main-menu";
|
|
|
9
9
|
import { showSessionDetails } from "./ui/session-details";
|
|
10
10
|
import { showSessionSettings } from "./ui/session-settings";
|
|
11
11
|
import { showDeleteConfirm } from "./ui/confirm-delete";
|
|
12
|
+
import { showQuitWarning } from "./ui/background-warning";
|
|
12
13
|
import type { Session } from "./types";
|
|
13
14
|
import { createNewSession, resumeSession } from "./flows";
|
|
14
15
|
import { listSessions, deleteSession, updateLastAccessed, updateSessionSettings } from "./sessions";
|
|
16
|
+
import { getRunningProcesses, killAllProcesses } from "./process-registry";
|
|
15
17
|
|
|
16
18
|
export async function handleTUIMode(): Promise<void> {
|
|
17
19
|
let renderer = await createRenderer();
|
|
@@ -39,10 +41,21 @@ export async function handleTUIMode(): Promise<void> {
|
|
|
39
41
|
break;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
case "quit":
|
|
44
|
+
case "quit": {
|
|
45
|
+
const runningProcesses = await getRunningProcesses();
|
|
46
|
+
if (runningProcesses.length > 0) {
|
|
47
|
+
const choice = await showQuitWarning(renderer, runningProcesses);
|
|
48
|
+
if (choice === "cancel") {
|
|
49
|
+
break; // Stay in main loop
|
|
50
|
+
}
|
|
51
|
+
if (choice === "kill") {
|
|
52
|
+
await killAllProcesses();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
43
55
|
destroyRenderer();
|
|
44
56
|
console.log("\nGoodbye!");
|
|
45
57
|
return;
|
|
58
|
+
}
|
|
46
59
|
}
|
|
47
60
|
}
|
|
48
61
|
} catch (error) {
|
package/src/types.ts
CHANGED
|
@@ -9,10 +9,8 @@ export interface RepoSpec {
|
|
|
9
9
|
branch?: string;
|
|
10
10
|
/** Directory name in the session workspace */
|
|
11
11
|
dir: string;
|
|
12
|
-
/** Whether this
|
|
13
|
-
|
|
14
|
-
/** Whether this repo should be refreshed to latest before resuming */
|
|
15
|
-
latest?: boolean;
|
|
12
|
+
/** Whether this resource is a reference (symlinked from shared cache, read-only, auto-refreshed) */
|
|
13
|
+
reference?: boolean;
|
|
16
14
|
}
|
|
17
15
|
|
|
18
16
|
export interface SessionManifest {
|
package/src/ui/add-repos.ts
CHANGED
|
@@ -22,12 +22,13 @@ export async function showAddReposPrompt(renderer: CliRenderer): Promise<AddRepo
|
|
|
22
22
|
|
|
23
23
|
const repos: RepoSpec[] = [];
|
|
24
24
|
let currentInput = "";
|
|
25
|
-
let
|
|
26
|
-
let currentLatest = false;
|
|
25
|
+
let currentReference = false;
|
|
27
26
|
let currentValidRepo: RepoSpec | null = null;
|
|
28
27
|
let selectedMatchIndex = -1; // -1 means no match selected, user is typing freely
|
|
29
28
|
let lastQuery = ""; // Track the query that generated current matches
|
|
30
29
|
let isNavigating = false; // Flag to prevent input handler from resetting when navigating
|
|
30
|
+
let isConfirming = false; // Flag for confirmation mode (showing checkboxes)
|
|
31
|
+
let confirmOptionIndex = 0; // 0 = reference, 1 = confirm button
|
|
31
32
|
|
|
32
33
|
// Repository input
|
|
33
34
|
const repoLabel = new TextRenderable(renderer, {
|
|
@@ -77,21 +78,21 @@ export async function showAddReposPrompt(renderer: CliRenderer): Promise<AddRepo
|
|
|
77
78
|
});
|
|
78
79
|
content.add(detailsLabel);
|
|
79
80
|
|
|
80
|
-
const
|
|
81
|
-
id: "details-
|
|
81
|
+
const detailsReference = new TextRenderable(renderer, {
|
|
82
|
+
id: "details-reference",
|
|
82
83
|
content: "",
|
|
83
84
|
fg: "#94a3b8",
|
|
84
85
|
marginTop: 0,
|
|
85
86
|
});
|
|
86
|
-
content.add(
|
|
87
|
+
content.add(detailsReference);
|
|
87
88
|
|
|
88
|
-
const
|
|
89
|
-
id: "
|
|
89
|
+
const confirmButton = new TextRenderable(renderer, {
|
|
90
|
+
id: "confirm-button",
|
|
90
91
|
content: "",
|
|
91
|
-
fg: "#
|
|
92
|
-
marginTop:
|
|
92
|
+
fg: "#10b981",
|
|
93
|
+
marginTop: 1,
|
|
93
94
|
});
|
|
94
|
-
content.add(
|
|
95
|
+
content.add(confirmButton);
|
|
95
96
|
|
|
96
97
|
repoInput.onPaste = (event) => {
|
|
97
98
|
const text = event.text.replace(/[\r\n]+/g, "");
|
|
@@ -102,8 +103,7 @@ export async function showAddReposPrompt(renderer: CliRenderer): Promise<AddRepo
|
|
|
102
103
|
const repo = validateRepo(currentInput.trim());
|
|
103
104
|
currentValidRepo = repo;
|
|
104
105
|
if (repo) {
|
|
105
|
-
|
|
106
|
-
currentLatest = false;
|
|
106
|
+
currentReference = false;
|
|
107
107
|
}
|
|
108
108
|
} else {
|
|
109
109
|
statusText.content = "";
|
|
@@ -151,9 +151,8 @@ export async function showAddReposPrompt(renderer: CliRenderer): Promise<AddRepo
|
|
|
151
151
|
} else {
|
|
152
152
|
reposList.content = repos
|
|
153
153
|
.map((repo, i) => {
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
return ` ${i + 1}. ${repo.spec}${roMarker}${latestMarker}`;
|
|
154
|
+
const refMarker = repo.reference ? " [Ref]" : "";
|
|
155
|
+
return ` ${i + 1}. ${repo.spec}${refMarker}`;
|
|
157
156
|
})
|
|
158
157
|
.join("\n");
|
|
159
158
|
reposList.fg = "#94a3b8";
|
|
@@ -161,17 +160,28 @@ export async function showAddReposPrompt(renderer: CliRenderer): Promise<AddRepo
|
|
|
161
160
|
}
|
|
162
161
|
|
|
163
162
|
function updateDetails() {
|
|
164
|
-
if (currentValidRepo) {
|
|
165
|
-
|
|
166
|
-
detailsLabel.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
163
|
+
if (isConfirming && currentValidRepo) {
|
|
164
|
+
// Confirmation mode with interactive checkboxes
|
|
165
|
+
detailsLabel.content = `\nConfigure options for: ${currentInput.trim()}`;
|
|
166
|
+
detailsLabel.fg = "#38bdf8";
|
|
167
|
+
|
|
168
|
+
const refCheckbox = currentReference ? "[✓]" : "[ ]";
|
|
169
|
+
const refSelected = confirmOptionIndex === 0;
|
|
170
|
+
detailsReference.content = ` ${refSelected ? "▶" : " "} ${refCheckbox} Reference [r] (read-only, shared cache, auto-refresh)`;
|
|
171
|
+
detailsReference.fg = refSelected ? "#f8fafc" : currentReference ? "#22d3ee" : "#94a3b8";
|
|
172
|
+
|
|
173
|
+
const confirmSelected = confirmOptionIndex === 1;
|
|
174
|
+
confirmButton.content = ` ${confirmSelected ? "▶" : " "} [Add repository]`;
|
|
175
|
+
confirmButton.fg = confirmSelected ? "#10b981" : "#64748b";
|
|
176
|
+
} else if (currentValidRepo) {
|
|
177
|
+
detailsLabel.content = "\nPress Enter to configure options";
|
|
178
|
+
detailsLabel.fg = "#64748b";
|
|
179
|
+
detailsReference.content = "";
|
|
180
|
+
confirmButton.content = "";
|
|
171
181
|
} else {
|
|
172
182
|
detailsLabel.content = "";
|
|
173
|
-
|
|
174
|
-
|
|
183
|
+
detailsReference.content = "";
|
|
184
|
+
confirmButton.content = "";
|
|
175
185
|
}
|
|
176
186
|
}
|
|
177
187
|
|
|
@@ -240,8 +250,7 @@ export async function showAddReposPrompt(renderer: CliRenderer): Promise<AddRepo
|
|
|
240
250
|
const repo = validateRepo(selectedMatch.trim());
|
|
241
251
|
currentValidRepo = repo;
|
|
242
252
|
if (repo) {
|
|
243
|
-
|
|
244
|
-
currentLatest = false;
|
|
253
|
+
currentReference = false;
|
|
245
254
|
}
|
|
246
255
|
|
|
247
256
|
updateMatches(); // Refresh display with new selection (matches stay the same, just highlight changes)
|
|
@@ -262,15 +271,13 @@ export async function showAddReposPrompt(renderer: CliRenderer): Promise<AddRepo
|
|
|
262
271
|
}
|
|
263
272
|
|
|
264
273
|
const repoToAdd = { ...currentValidRepo };
|
|
265
|
-
repoToAdd.
|
|
266
|
-
repoToAdd.latest = currentLatest;
|
|
274
|
+
repoToAdd.reference = currentReference;
|
|
267
275
|
repos.push(repoToAdd);
|
|
268
276
|
|
|
269
277
|
currentInput = "";
|
|
270
278
|
repoInput.value = "";
|
|
271
279
|
currentValidRepo = null;
|
|
272
|
-
|
|
273
|
-
currentLatest = false;
|
|
280
|
+
currentReference = false;
|
|
274
281
|
updateReposList();
|
|
275
282
|
lastQuery = "";
|
|
276
283
|
selectedMatchIndex = -1;
|
|
@@ -280,37 +287,109 @@ export async function showAddReposPrompt(renderer: CliRenderer): Promise<AddRepo
|
|
|
280
287
|
statusText.content = "";
|
|
281
288
|
}
|
|
282
289
|
|
|
290
|
+
function enterConfirmMode() {
|
|
291
|
+
isConfirming = true;
|
|
292
|
+
confirmOptionIndex = 1; // Start on confirm button for quick add
|
|
293
|
+
repoInput.blur();
|
|
294
|
+
updateDetails();
|
|
295
|
+
updateFooter();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function exitConfirmMode() {
|
|
299
|
+
isConfirming = false;
|
|
300
|
+
confirmOptionIndex = 0;
|
|
301
|
+
repoInput.focus();
|
|
302
|
+
updateDetails();
|
|
303
|
+
updateFooter();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function toggleCurrentOption() {
|
|
307
|
+
if (confirmOptionIndex === 0) {
|
|
308
|
+
// Toggle reference
|
|
309
|
+
currentReference = !currentReference;
|
|
310
|
+
} else if (confirmOptionIndex === 1) {
|
|
311
|
+
// Confirm button - add the repo
|
|
312
|
+
addCurrentRepo();
|
|
313
|
+
exitConfirmMode();
|
|
314
|
+
}
|
|
315
|
+
updateDetails();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function updateFooter() {
|
|
319
|
+
if (isConfirming) {
|
|
320
|
+
showFooter(renderer, content, {
|
|
321
|
+
navigate: true,
|
|
322
|
+
select: false,
|
|
323
|
+
back: true,
|
|
324
|
+
custom: ["r Reference", "space Toggle", "enter Add"],
|
|
325
|
+
});
|
|
326
|
+
} else {
|
|
327
|
+
showFooter(renderer, content, {
|
|
328
|
+
navigate: true,
|
|
329
|
+
select: true,
|
|
330
|
+
back: true,
|
|
331
|
+
custom: repos.length > 0 ? ["enter (empty) Continue"] : [],
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
283
336
|
const handleKeypress = (key: KeyEvent) => {
|
|
337
|
+
// Escape behavior differs based on mode
|
|
284
338
|
if (isEscape(key)) {
|
|
339
|
+
if (isConfirming) {
|
|
340
|
+
// Exit confirmation mode, go back to input
|
|
341
|
+
exitConfirmMode();
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
285
344
|
cleanup();
|
|
286
345
|
resolve({ repos, cancelled: true });
|
|
287
346
|
return;
|
|
288
347
|
}
|
|
289
348
|
|
|
290
|
-
//
|
|
291
|
-
if (
|
|
292
|
-
|
|
293
|
-
if (
|
|
294
|
-
|
|
349
|
+
// Confirmation mode handling
|
|
350
|
+
if (isConfirming) {
|
|
351
|
+
// Toggle reference with 'r' hotkey
|
|
352
|
+
if (key.name === "r") {
|
|
353
|
+
currentReference = !currentReference;
|
|
354
|
+
updateDetails();
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Space to toggle current option
|
|
359
|
+
if (key.name === "space") {
|
|
360
|
+
toggleCurrentOption();
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Enter to confirm/add
|
|
365
|
+
if (isEnter(key)) {
|
|
366
|
+
addCurrentRepo();
|
|
367
|
+
exitConfirmMode();
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Arrow keys to navigate options
|
|
372
|
+
if (isArrowUp(key)) {
|
|
373
|
+
confirmOptionIndex = Math.max(0, confirmOptionIndex - 1);
|
|
374
|
+
updateDetails();
|
|
375
|
+
return;
|
|
295
376
|
}
|
|
296
|
-
updateDetails();
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
377
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
currentReadOnly = true;
|
|
378
|
+
if (isArrowDown(key)) {
|
|
379
|
+
confirmOptionIndex = Math.min(1, confirmOptionIndex + 1);
|
|
380
|
+
updateDetails();
|
|
381
|
+
return;
|
|
305
382
|
}
|
|
306
|
-
|
|
383
|
+
|
|
307
384
|
return;
|
|
308
385
|
}
|
|
309
386
|
|
|
310
|
-
//
|
|
387
|
+
// Normal input mode handling
|
|
388
|
+
|
|
389
|
+
// Enter to enter confirmation mode or continue
|
|
311
390
|
if (isEnter(key)) {
|
|
312
391
|
if (currentInput.trim() && currentValidRepo) {
|
|
313
|
-
|
|
392
|
+
enterConfirmMode();
|
|
314
393
|
} else if (!currentInput.trim() && repos.length > 0) {
|
|
315
394
|
// Empty input + repos added = continue
|
|
316
395
|
cleanup();
|
|
@@ -367,8 +446,7 @@ export async function showAddReposPrompt(renderer: CliRenderer): Promise<AddRepo
|
|
|
367
446
|
const repo = validateRepo(value.trim());
|
|
368
447
|
currentValidRepo = repo;
|
|
369
448
|
if (repo) {
|
|
370
|
-
|
|
371
|
-
currentLatest = false;
|
|
449
|
+
currentReference = false;
|
|
372
450
|
}
|
|
373
451
|
} else {
|
|
374
452
|
statusText.content = "";
|
|
@@ -385,16 +463,9 @@ export async function showAddReposPrompt(renderer: CliRenderer): Promise<AddRepo
|
|
|
385
463
|
clearLayout(renderer);
|
|
386
464
|
};
|
|
387
465
|
|
|
388
|
-
// Show footer with context-aware actions
|
|
389
|
-
showFooter(renderer, content, {
|
|
390
|
-
navigate: true,
|
|
391
|
-
select: true,
|
|
392
|
-
back: true,
|
|
393
|
-
custom: currentValidRepo ? ["r Toggle RO", "l Toggle Latest"] : ["↑↓ Navigate matches"],
|
|
394
|
-
});
|
|
395
|
-
|
|
396
466
|
renderer.keyInput.on("keypress", handleKeypress);
|
|
397
467
|
updateDetails();
|
|
398
468
|
updateReposList();
|
|
469
|
+
updateFooter();
|
|
399
470
|
});
|
|
400
471
|
}
|