@glrs-dev/cli 2.3.0 → 2.4.1

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.
Files changed (79) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/{chunk-EM4MJBOD.js → chunk-2AZKRWC6.js} +4 -4
  3. package/dist/{chunk-UXBOTMDY.js → chunk-2P3ETOT2.js} +2 -2
  4. package/dist/chunk-2VMFXAJH.js +795 -0
  5. package/dist/chunk-5ZVUFNCP.js +140 -0
  6. package/dist/{chunk-W37UX3U2.js → chunk-6Y27RQQL.js} +2 -2
  7. package/dist/{chunk-RZWOWTKF.js → chunk-EKNRKZWR.js} +4 -4
  8. package/dist/{chunk-YGNDPKIW.js → chunk-HQUCVJ4G.js} +3 -1
  9. package/dist/{chunk-OABVEBWW.js → chunk-MBEVC327.js} +1 -1
  10. package/dist/{chunk-MIWZLETC.js → chunk-MCM47HH4.js} +1 -1
  11. package/dist/{chunk-F3AFRUT2.js → chunk-PTIO556V.js} +2 -2
  12. package/dist/{chunk-E2UNZIZT.js → chunk-R2WXQ54P.js} +1 -1
  13. package/dist/{chunk-I2KUXY3I.js → chunk-SMDIOB5B.js} +2 -2
  14. package/dist/{chunk-SPULDN7P.js → chunk-YY7EWHMA.js} +5 -3
  15. package/dist/cli.js +31 -20
  16. package/dist/commands/autopilot-interactive.d.ts +89 -0
  17. package/dist/commands/autopilot-interactive.js +248 -0
  18. package/dist/commands/autopilot-raw.d.ts +1 -0
  19. package/dist/commands/autopilot-raw.js +368 -0
  20. package/dist/commands/autopilot-tui.d.ts +7 -0
  21. package/dist/commands/autopilot-tui.js +7 -0
  22. package/dist/commands/autopilot.d.ts +39 -0
  23. package/dist/commands/autopilot.js +395 -0
  24. package/dist/commands/cleanup.js +3 -3
  25. package/dist/commands/create.js +4 -4
  26. package/dist/commands/dashboard.d.ts +3 -0
  27. package/dist/commands/dashboard.js +1549 -0
  28. package/dist/commands/debrief.d.ts +57 -0
  29. package/dist/commands/debrief.js +9 -0
  30. package/dist/commands/delete.js +3 -3
  31. package/dist/commands/go.js +2 -2
  32. package/dist/commands/list.js +3 -3
  33. package/dist/commands/loop.d.ts +42 -0
  34. package/dist/commands/loop.js +133 -0
  35. package/dist/commands/plan-picker.d.ts +15 -0
  36. package/dist/commands/plan-picker.js +76 -0
  37. package/dist/commands/scoper.d.ts +54 -0
  38. package/dist/{vendor/harness-opencode/dist/scoper-S77SOK7X.js → commands/scoper.js} +30 -15
  39. package/dist/commands/switch.js +3 -3
  40. package/dist/index.d.ts +2 -2
  41. package/dist/index.js +1 -1
  42. package/dist/lib/auto-update.js +1 -1
  43. package/dist/lib/config.d.ts +3 -2
  44. package/dist/lib/config.js +1 -1
  45. package/dist/lib/registry.d.ts +2 -0
  46. package/dist/lib/registry.js +1 -1
  47. package/dist/lib/worktree.js +3 -3
  48. package/dist/node_modules/@glrs-dev/adapter-opencode/dist/index.d.ts +261 -0
  49. package/dist/node_modules/@glrs-dev/adapter-opencode/dist/index.js +488 -0
  50. package/dist/node_modules/@glrs-dev/adapter-opencode/package.json +8 -0
  51. package/dist/node_modules/@glrs-dev/autopilot/dist/auto-ship-LCT6LIH7.js +7 -0
  52. package/dist/node_modules/@glrs-dev/autopilot/dist/changeset-generator-DG3MVWVV.js +15 -0
  53. package/dist/node_modules/@glrs-dev/autopilot/dist/chunk-7OSEI5TF.js +249 -0
  54. package/dist/node_modules/@glrs-dev/autopilot/dist/chunk-E7PWTRFO.js +91 -0
  55. package/dist/node_modules/@glrs-dev/autopilot/dist/chunk-M2ZVBPWL.js +101 -0
  56. package/dist/node_modules/@glrs-dev/autopilot/dist/chunk-Q4ULU6ER.js +68 -0
  57. package/dist/node_modules/@glrs-dev/autopilot/dist/chunk-VITL2Z45.js +2772 -0
  58. package/dist/node_modules/@glrs-dev/autopilot/dist/chunk-ZNJWARTM.js +449 -0
  59. package/dist/node_modules/@glrs-dev/autopilot/dist/index.d.ts +1765 -0
  60. package/dist/node_modules/@glrs-dev/autopilot/dist/index.js +688 -0
  61. package/dist/node_modules/@glrs-dev/autopilot/dist/logger-UITJGIZE.js +8 -0
  62. package/dist/node_modules/@glrs-dev/autopilot/dist/loop-session-XKL3NHUA.js +8 -0
  63. package/dist/node_modules/@glrs-dev/autopilot/dist/plan-enrichment-D3RPJR2J.js +14 -0
  64. package/dist/node_modules/@glrs-dev/autopilot/package.json +8 -0
  65. package/dist/vendor/harness-opencode/dist/agents/prompts/plan.md +7 -0
  66. package/dist/vendor/harness-opencode/dist/chunk-GILWWWMB.js +66 -0
  67. package/dist/vendor/harness-opencode/dist/cli.js +335 -639
  68. package/dist/vendor/harness-opencode/dist/index.js +35 -8
  69. package/dist/vendor/harness-opencode/dist/plugin-check-GJRD2OK6.js +14 -0
  70. package/dist/vendor/harness-opencode/package.json +1 -1
  71. package/package.json +14 -6
  72. package/dist/vendor/harness-opencode/dist/autopilot/prompt-template.md +0 -104
  73. package/dist/vendor/harness-opencode/dist/chunk-GCWHRUOK.js +0 -259
  74. package/dist/vendor/harness-opencode/dist/chunk-MJSMBY2Y.js +0 -87
  75. package/dist/vendor/harness-opencode/dist/chunk-NIFAVPNN.js +0 -544
  76. package/dist/vendor/harness-opencode/dist/loop-session-J35NILUZ.js +0 -30
  77. package/dist/vendor/harness-opencode/dist/opencode-server-KPCDFYAX.js +0 -22
  78. package/dist/vendor/harness-opencode/dist/plan-parser-TMHEKT22.js +0 -6
  79. package/dist/vendor/harness-opencode/dist/plan-session-7VS32P52.js +0 -117
@@ -0,0 +1,1549 @@
1
+ import {
2
+ createWorktree
3
+ } from "../chunk-PTIO556V.js";
4
+ import "../chunk-6Y27RQQL.js";
5
+ import "../chunk-LMRDQ4GW.js";
6
+ import "../chunk-YY7EWHMA.js";
7
+ import "../chunk-YBCA3IP6.js";
8
+ import "../chunk-3RG5ZIWI.js";
9
+
10
+ // src/session-manager.ts
11
+ import * as fs2 from "fs";
12
+ import * as path2 from "path";
13
+ import { spawn } from "child_process";
14
+ import { EventStreamReader as EventStreamReader2 } from "@glrs-dev/autopilot";
15
+ import { deriveState as deriveState2 } from "@glrs-dev/autopilot";
16
+
17
+ // src/session-discovery.ts
18
+ import * as fs from "fs";
19
+ import * as path from "path";
20
+ import { EventStreamReader } from "@glrs-dev/autopilot";
21
+ import { deriveState } from "@glrs-dev/autopilot";
22
+ var EVENT_FILE_NAME = "autopilot-events.jsonl";
23
+ var STALE_THRESHOLD_MS = 5 * 60 * 1e3;
24
+ function discoverSessions(dirs) {
25
+ const results = [];
26
+ const now = Date.now();
27
+ for (const dir of dirs) {
28
+ const eventFilePath = path.join(dir, ".agent", EVENT_FILE_NAME);
29
+ if (!fs.existsSync(eventFilePath)) {
30
+ continue;
31
+ }
32
+ let handle;
33
+ try {
34
+ const reader = new EventStreamReader(eventFilePath);
35
+ const events = reader.readAll();
36
+ handle = deriveState(events);
37
+ } catch {
38
+ continue;
39
+ }
40
+ if (!handle) {
41
+ continue;
42
+ }
43
+ const lastEventMs = new Date(handle.lastEventAt).getTime();
44
+ const isStale = handle.status !== "complete" && now - lastEventMs > STALE_THRESHOLD_MS;
45
+ const finalHandle = isStale ? { ...handle, status: "stale" } : handle;
46
+ results.push({ eventFilePath, handle: finalHandle, isStale });
47
+ }
48
+ results.sort(
49
+ (a, b) => new Date(b.handle.lastEventAt).getTime() - new Date(a.handle.lastEventAt).getTime()
50
+ );
51
+ return results;
52
+ }
53
+
54
+ // src/session-manager.ts
55
+ var ACTIVE_POLL_MS = 1e3;
56
+ var IDLE_POLL_MS = 5e3;
57
+ var KILL_ESCALATION_MS = 5e3;
58
+ function resolveCliArgs() {
59
+ const script = process.argv[1];
60
+ if (script) {
61
+ return { bin: process.execPath, preArgs: [script] };
62
+ }
63
+ return { bin: "glrs", preArgs: [] };
64
+ }
65
+ var SessionManager = class {
66
+ constructor(dirs) {
67
+ this.dirs = dirs;
68
+ }
69
+ dirs;
70
+ sessions = /* @__PURE__ */ new Map();
71
+ pollInterval = null;
72
+ lastPollMs = 0;
73
+ // ---------------------------------------------------------------------------
74
+ // Lifecycle
75
+ // ---------------------------------------------------------------------------
76
+ /** Begin polling discovered sessions. */
77
+ start() {
78
+ if (this.pollInterval !== null) return;
79
+ this._discover();
80
+ this.pollInterval = setInterval(() => {
81
+ this._poll();
82
+ }, ACTIVE_POLL_MS);
83
+ }
84
+ /** Stop polling. */
85
+ stop() {
86
+ if (this.pollInterval !== null) {
87
+ clearInterval(this.pollInterval);
88
+ this.pollInterval = null;
89
+ }
90
+ }
91
+ // ---------------------------------------------------------------------------
92
+ // Queries
93
+ // ---------------------------------------------------------------------------
94
+ /** Returns a snapshot of all tracked session handles. */
95
+ getSessions() {
96
+ return Array.from(this.sessions.values()).map((t) => t.handle);
97
+ }
98
+ // ---------------------------------------------------------------------------
99
+ // Session operations
100
+ // ---------------------------------------------------------------------------
101
+ /**
102
+ * Launch a new autopilot session as a detached subprocess.
103
+ *
104
+ * Uses the same CLI binary that's currently running (so glrs-dev spawns
105
+ * glrs-dev, not the globally installed glrs).
106
+ *
107
+ * Returns a provisional SessionHandle (status: "running") immediately.
108
+ * The handle will be updated on the next poll once the event file appears.
109
+ */
110
+ launchSession(opts) {
111
+ const { planPath, cwd, fast = false } = opts;
112
+ const { bin, preArgs } = resolveCliArgs();
113
+ const args = [...preArgs, "oc", "autopilot", "--plan", planPath];
114
+ if (fast) args.push("--fast");
115
+ const child = spawn(bin, args, {
116
+ cwd,
117
+ detached: true,
118
+ stdio: "ignore"
119
+ });
120
+ child.unref();
121
+ const pid = child.pid;
122
+ const now = (/* @__PURE__ */ new Date()).toISOString();
123
+ const provisionalHandle = {
124
+ id: `provisional-${pid ?? Date.now()}`,
125
+ planPath,
126
+ cwd,
127
+ fast,
128
+ resume: false,
129
+ status: "running",
130
+ totalIterations: 0,
131
+ cost: 0,
132
+ startedAt: now,
133
+ lastEventAt: now
134
+ };
135
+ const eventFilePath = path2.join(cwd, ".agent", "autopilot-events.jsonl");
136
+ const reader = new EventStreamReader2(eventFilePath);
137
+ this.sessions.set(provisionalHandle.id, {
138
+ handle: provisionalHandle,
139
+ reader,
140
+ offset: 0,
141
+ pid,
142
+ eventFilePath
143
+ });
144
+ return provisionalHandle;
145
+ }
146
+ /**
147
+ * Create a fresh worktree and launch an autopilot session in it.
148
+ *
149
+ * Uses `createWorktree({ repo })` which resolves the repo via the
150
+ * worktree registry, repo index, and filesystem scan — the same
151
+ * resolution as `glrs wt new <repo>`.
152
+ *
153
+ * The plan path is resolved into the new worktree if it's inside the repo,
154
+ * or passed as an absolute path if it's external (e.g. ~/.glrs/).
155
+ */
156
+ launchSessionWithWorktree(opts) {
157
+ const { repoName: repoName3, planPath, fast = false } = opts;
158
+ const { wtPath } = createWorktree({ repo: repoName3 });
159
+ let resolvedPlanPath = planPath;
160
+ const repoRoot = this._findRepoRoot(wtPath);
161
+ if (repoRoot && planPath.startsWith(repoRoot)) {
162
+ const relativePlan = path2.relative(repoRoot, planPath);
163
+ resolvedPlanPath = path2.join(wtPath, relativePlan);
164
+ }
165
+ return this.launchSession({
166
+ planPath: resolvedPlanPath,
167
+ cwd: wtPath,
168
+ fast
169
+ });
170
+ }
171
+ /**
172
+ * Find the primary clone root from a worktree path by reading .git file.
173
+ */
174
+ _findRepoRoot(wtPath) {
175
+ try {
176
+ const gitFile = path2.join(wtPath, ".git");
177
+ const content = fs2.readFileSync(gitFile, "utf8").trim();
178
+ const match = content.match(/^gitdir:\s*(.+)$/);
179
+ if (match) {
180
+ const gitWorktreeDir = match[1];
181
+ const dotGitDir = path2.resolve(path2.dirname(path2.dirname(gitWorktreeDir)));
182
+ return path2.dirname(dotGitDir);
183
+ }
184
+ } catch {
185
+ }
186
+ return null;
187
+ }
188
+ /**
189
+ * Send SIGINT to the session's process (graceful kill).
190
+ * If called a second time within KILL_ESCALATION_MS, escalates to SIGKILL.
191
+ * No-op if the session was not launched by this manager or PID is unknown.
192
+ */
193
+ killSession(id) {
194
+ const tracked = this.sessions.get(id);
195
+ if (!tracked?.pid) return;
196
+ const now = Date.now();
197
+ const isEscalation = tracked.lastSigintAt !== void 0 && now - tracked.lastSigintAt < KILL_ESCALATION_MS;
198
+ const signal = isEscalation ? "SIGKILL" : "SIGINT";
199
+ try {
200
+ process.kill(tracked.pid, signal);
201
+ } catch {
202
+ }
203
+ tracked.lastSigintAt = now;
204
+ }
205
+ /**
206
+ * Unconditionally send SIGKILL to the session's process.
207
+ * No-op if the session was not launched by this manager or PID is unknown.
208
+ */
209
+ forceKillSession(id) {
210
+ const tracked = this.sessions.get(id);
211
+ if (!tracked?.pid) return;
212
+ try {
213
+ process.kill(tracked.pid, "SIGKILL");
214
+ } catch {
215
+ }
216
+ }
217
+ /**
218
+ * Re-launch the session with --resume.
219
+ * Finds the session's planPath and cwd, then spawns a new process.
220
+ */
221
+ retrySession(id) {
222
+ const tracked = this.sessions.get(id);
223
+ if (!tracked) return;
224
+ const { planPath, cwd, fast } = tracked.handle;
225
+ const { bin, preArgs } = resolveCliArgs();
226
+ const args = [...preArgs, "oc", "autopilot", "--plan", planPath, "--resume"];
227
+ if (fast) args.push("--fast");
228
+ const child = spawn(bin, args, {
229
+ cwd,
230
+ detached: true,
231
+ stdio: "ignore"
232
+ });
233
+ child.unref();
234
+ if (child.pid !== void 0) {
235
+ tracked.pid = child.pid;
236
+ }
237
+ tracked.offset = 0;
238
+ }
239
+ /**
240
+ * Delete the event file, checkpoint file, and debug log for a session.
241
+ * Removes the session from the tracked map.
242
+ */
243
+ cleanupSession(id) {
244
+ const tracked = this.sessions.get(id);
245
+ if (!tracked) return;
246
+ const agentDir = path2.dirname(tracked.eventFilePath);
247
+ const filesToDelete = [
248
+ path2.join(agentDir, "autopilot-events.jsonl"),
249
+ path2.join(agentDir, "autopilot-checkpoint.json"),
250
+ path2.join(agentDir, "autopilot-debug.log"),
251
+ path2.join(agentDir, "autopilot-status.json")
252
+ ];
253
+ for (const f of filesToDelete) {
254
+ try {
255
+ fs2.unlinkSync(f);
256
+ } catch {
257
+ }
258
+ }
259
+ this.sessions.delete(id);
260
+ }
261
+ // ---------------------------------------------------------------------------
262
+ // Internal polling
263
+ // ---------------------------------------------------------------------------
264
+ /** Discover new sessions from configured dirs. */
265
+ _discover() {
266
+ const discovered = discoverSessions(this.dirs);
267
+ for (const { eventFilePath, handle } of discovered) {
268
+ const alreadyTracked = Array.from(this.sessions.values()).some(
269
+ (t) => t.eventFilePath === eventFilePath
270
+ );
271
+ if (alreadyTracked) continue;
272
+ const reader = new EventStreamReader2(eventFilePath);
273
+ const { newOffset } = reader.readFrom(0);
274
+ this.sessions.set(handle.id, {
275
+ handle,
276
+ reader,
277
+ offset: newOffset,
278
+ eventFilePath
279
+ });
280
+ }
281
+ }
282
+ /** Poll all tracked sessions for new events. */
283
+ _poll() {
284
+ const now = Date.now();
285
+ if (now - this.lastPollMs >= IDLE_POLL_MS) {
286
+ this._discover();
287
+ this.lastPollMs = now;
288
+ }
289
+ for (const [id, tracked] of this.sessions) {
290
+ const isIdle = tracked.handle.status === "complete" || tracked.handle.status === "stale";
291
+ if (isIdle && now - this.lastPollMs < IDLE_POLL_MS) {
292
+ continue;
293
+ }
294
+ const { events, newOffset } = tracked.reader.readFrom(tracked.offset);
295
+ if (events.length === 0) continue;
296
+ tracked.offset = newOffset;
297
+ const allEvents = tracked.reader.readAll();
298
+ const newHandle = deriveState2(allEvents);
299
+ if (newHandle) {
300
+ this.sessions.set(id, { ...tracked, handle: newHandle, offset: newOffset });
301
+ }
302
+ }
303
+ }
304
+ };
305
+
306
+ // src/repo-config.ts
307
+ import * as fs3 from "fs";
308
+ import * as path3 from "path";
309
+ import * as os from "os";
310
+ import { parse as parseYaml } from "yaml";
311
+ var REPOS_CONFIG_PATH = path3.join(os.homedir(), ".config", "glrs", "repos.yaml");
312
+ var WORKTREES_BASE_DIR = path3.join(os.homedir(), ".glrs", "worktrees");
313
+ var OPENCODE_BASE_DIR = path3.join(os.homedir(), ".glrs", "opencode");
314
+ var LEGACY_WORKTREES_BASE_DIR = path3.join(os.homedir(), ".glorious", "worktrees");
315
+ var LEGACY_OPENCODE_BASE_DIR = path3.join(os.homedir(), ".glorious", "opencode");
316
+ var EVENT_FILE_RELATIVE = path3.join(".agent", "autopilot-events.jsonl");
317
+ function readGitBranch(repoPath) {
318
+ const headFile = path3.join(repoPath, ".git", "HEAD");
319
+ try {
320
+ const content = fs3.readFileSync(headFile, "utf8").trim();
321
+ const match = content.match(/^ref: refs\/heads\/(.+)$/);
322
+ return match ? match[1] : void 0;
323
+ } catch {
324
+ try {
325
+ const gitFile = path3.join(repoPath, ".git");
326
+ const content = fs3.readFileSync(gitFile, "utf8").trim();
327
+ const gitdirMatch = content.match(/^gitdir:\s*(.+)$/);
328
+ if (gitdirMatch) {
329
+ const worktreeGitDir = gitdirMatch[1];
330
+ const headPath = path3.join(worktreeGitDir, "HEAD");
331
+ const headContent = fs3.readFileSync(headPath, "utf8").trim();
332
+ const branchMatch = headContent.match(/^ref: refs\/heads\/(.+)$/);
333
+ return branchMatch ? branchMatch[1] : void 0;
334
+ }
335
+ } catch {
336
+ }
337
+ return void 0;
338
+ }
339
+ }
340
+ function hasActiveAutopilot(repoPath) {
341
+ return fs3.existsSync(path3.join(repoPath, EVENT_FILE_RELATIVE));
342
+ }
343
+ function buildRepoInfo(repoPath, nameOverride) {
344
+ const name = nameOverride ?? path3.basename(repoPath);
345
+ return {
346
+ path: repoPath,
347
+ name,
348
+ branch: readGitBranch(repoPath),
349
+ hasActiveAutopilot: hasActiveAutopilot(repoPath)
350
+ };
351
+ }
352
+ function parseReposYaml(raw) {
353
+ try {
354
+ const parsed = parseYaml(raw);
355
+ if (parsed && typeof parsed === "object") {
356
+ return parsed;
357
+ }
358
+ return {};
359
+ } catch {
360
+ return {};
361
+ }
362
+ }
363
+ function readRepos({ reposConfigPath, worktreesBaseDir, legacyWorktreesBaseDir }) {
364
+ const seen = /* @__PURE__ */ new Set();
365
+ const results = [];
366
+ function addRepo(repoPath, nameOverride) {
367
+ const resolved = path3.resolve(repoPath);
368
+ if (seen.has(resolved)) return;
369
+ if (!fs3.existsSync(resolved)) return;
370
+ seen.add(resolved);
371
+ results.push(buildRepoInfo(resolved, nameOverride));
372
+ }
373
+ try {
374
+ const raw = fs3.readFileSync(reposConfigPath, "utf8");
375
+ const config = parseReposYaml(raw);
376
+ if (Array.isArray(config.repos)) {
377
+ for (const entry of config.repos) {
378
+ if (typeof entry === "string") {
379
+ addRepo(entry);
380
+ } else if (entry && typeof entry === "object" && typeof entry.path === "string") {
381
+ addRepo(entry.path, entry.name);
382
+ }
383
+ }
384
+ }
385
+ } catch {
386
+ }
387
+ const worktreesDirs = [worktreesBaseDir];
388
+ if (legacyWorktreesBaseDir) worktreesDirs.push(legacyWorktreesBaseDir);
389
+ for (const baseDir of worktreesDirs) {
390
+ try {
391
+ if (!fs3.existsSync(baseDir)) continue;
392
+ const repoGroups = fs3.readdirSync(baseDir, { withFileTypes: true });
393
+ for (const repoGroup of repoGroups) {
394
+ if (!repoGroup.isDirectory()) continue;
395
+ const repoGroupPath = path3.join(baseDir, repoGroup.name);
396
+ let worktreeEntries;
397
+ try {
398
+ worktreeEntries = fs3.readdirSync(repoGroupPath, { withFileTypes: true });
399
+ } catch {
400
+ continue;
401
+ }
402
+ for (const wt of worktreeEntries) {
403
+ if (!wt.isDirectory()) continue;
404
+ const wtPath = path3.join(repoGroupPath, wt.name);
405
+ addRepo(wtPath, `${repoGroup.name}/${wt.name}`);
406
+ }
407
+ }
408
+ } catch {
409
+ }
410
+ }
411
+ return results;
412
+ }
413
+ function getConfiguredRepos() {
414
+ return readRepos({
415
+ reposConfigPath: REPOS_CONFIG_PATH,
416
+ worktreesBaseDir: WORKTREES_BASE_DIR,
417
+ legacyWorktreesBaseDir: LEGACY_WORKTREES_BASE_DIR
418
+ });
419
+ }
420
+ function getUniqueRepos() {
421
+ return readUniqueRepos({
422
+ reposConfigPath: REPOS_CONFIG_PATH,
423
+ worktreesBaseDir: WORKTREES_BASE_DIR,
424
+ legacyWorktreesBaseDir: LEGACY_WORKTREES_BASE_DIR
425
+ });
426
+ }
427
+ function readUniqueRepos({ reposConfigPath, worktreesBaseDir, legacyWorktreesBaseDir }) {
428
+ const byPath = /* @__PURE__ */ new Map();
429
+ function addOrUpgrade(name, primaryPath) {
430
+ const resolved = path3.resolve(primaryPath);
431
+ const existing = byPath.get(resolved);
432
+ if (existing) {
433
+ const existingIsSlug = /^wt-\d{6}-/.test(existing.name);
434
+ const newIsSlug = /^wt-\d{6}-/.test(name);
435
+ if (existingIsSlug && !newIsSlug) {
436
+ byPath.set(resolved, { name, primaryPath: resolved });
437
+ }
438
+ return;
439
+ }
440
+ byPath.set(resolved, { name, primaryPath: resolved });
441
+ }
442
+ try {
443
+ const raw = fs3.readFileSync(reposConfigPath, "utf8");
444
+ const config = parseReposYaml(raw);
445
+ if (Array.isArray(config.repos)) {
446
+ for (const entry of config.repos) {
447
+ const repoPath = typeof entry === "string" ? entry : entry?.path;
448
+ const name = typeof entry === "object" && entry?.name ? entry.name : void 0;
449
+ if (!repoPath) continue;
450
+ const resolved = path3.resolve(repoPath);
451
+ if (!fs3.existsSync(resolved)) continue;
452
+ addOrUpgrade(name ?? path3.basename(resolved), resolved);
453
+ }
454
+ }
455
+ } catch {
456
+ }
457
+ const worktreesDirs = [worktreesBaseDir];
458
+ if (legacyWorktreesBaseDir) worktreesDirs.push(legacyWorktreesBaseDir);
459
+ for (const baseDir of worktreesDirs) {
460
+ try {
461
+ if (!fs3.existsSync(baseDir)) continue;
462
+ const repoGroups = fs3.readdirSync(baseDir, { withFileTypes: true });
463
+ for (const repoGroup of repoGroups) {
464
+ if (!repoGroup.isDirectory()) continue;
465
+ const groupPath = path3.join(baseDir, repoGroup.name);
466
+ let primaryPath = groupPath;
467
+ try {
468
+ const entries = fs3.readdirSync(groupPath, { withFileTypes: true });
469
+ for (const entry of entries) {
470
+ if (!entry.isDirectory()) continue;
471
+ const wtPath = path3.join(groupPath, entry.name);
472
+ const gitFile = path3.join(wtPath, ".git");
473
+ try {
474
+ const content = fs3.readFileSync(gitFile, "utf8").trim();
475
+ const match = content.match(/^gitdir:\s*(.+)$/);
476
+ if (match) {
477
+ const gitWorktreeDir = match[1];
478
+ const dotGitDir = path3.resolve(path3.dirname(path3.dirname(gitWorktreeDir)));
479
+ const cloneRoot = path3.dirname(dotGitDir);
480
+ if (fs3.existsSync(cloneRoot)) {
481
+ primaryPath = cloneRoot;
482
+ break;
483
+ }
484
+ }
485
+ } catch {
486
+ }
487
+ }
488
+ } catch {
489
+ }
490
+ addOrUpgrade(repoGroup.name, primaryPath);
491
+ }
492
+ } catch {
493
+ }
494
+ }
495
+ const opencodeDirs = [OPENCODE_BASE_DIR, LEGACY_OPENCODE_BASE_DIR];
496
+ for (const opencodeDir of opencodeDirs) {
497
+ try {
498
+ if (fs3.existsSync(opencodeDir)) {
499
+ const entries = fs3.readdirSync(opencodeDir, { withFileTypes: true });
500
+ for (const entry of entries) {
501
+ if (!entry.isDirectory()) continue;
502
+ if (entry.name.startsWith("tmp") || entry.name.startsWith("costs")) continue;
503
+ if (entry.name === "default-base" || entry.name === "default-pilot" || entry.name === "pilot-smoke") continue;
504
+ const repoName3 = entry.name;
505
+ const alreadyFound = Array.from(byPath.values()).some((r) => r.name === repoName3);
506
+ if (alreadyFound) continue;
507
+ addOrUpgrade(repoName3, path3.join(opencodeDir, repoName3));
508
+ }
509
+ }
510
+ } catch {
511
+ }
512
+ }
513
+ return Array.from(byPath.values()).sort((a, b) => a.name.localeCompare(b.name));
514
+ }
515
+
516
+ // src/tui/index.ts
517
+ import React6 from "react";
518
+ import { render } from "ink";
519
+
520
+ // src/tui/components/Dashboard.tsx
521
+ import { useState as useState5, useEffect as useEffect2 } from "react";
522
+ import { Box as Box8, Text as Text8, useInput as useInput5, useApp } from "ink";
523
+
524
+ // src/tui/components/SessionCard.tsx
525
+ import { Box, Text } from "ink";
526
+ import { Spinner } from "@inkjs/ui";
527
+ import { jsx, jsxs } from "react/jsx-runtime";
528
+ var STATUS_COLORS = {
529
+ running: "blue",
530
+ enriching: "yellow",
531
+ verifying: "cyan",
532
+ complete: "green",
533
+ error: "red",
534
+ stale: "gray"
535
+ };
536
+ var STATUS_LABELS = {
537
+ running: "Running",
538
+ enriching: "Enriching",
539
+ verifying: "Verifying",
540
+ complete: "Complete",
541
+ error: "Error",
542
+ stale: "Stale"
543
+ };
544
+ function formatElapsed(startedAt) {
545
+ const ms = Date.now() - new Date(startedAt).getTime();
546
+ const seconds = Math.floor(ms / 1e3);
547
+ const minutes = Math.floor(seconds / 60);
548
+ const hours = Math.floor(minutes / 60);
549
+ if (hours > 0) return `${hours}h ${minutes % 60}m`;
550
+ if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
551
+ return `${seconds}s`;
552
+ }
553
+ function formatCost(cost) {
554
+ return `$${cost.toFixed(2)}`;
555
+ }
556
+ function repoName(cwd) {
557
+ return cwd.split("/").pop() || cwd;
558
+ }
559
+ function SessionCard({ handle, selected }) {
560
+ const color = STATUS_COLORS[handle.status];
561
+ const isActive = handle.status === "running" || handle.status === "enriching" || handle.status === "verifying";
562
+ return /* @__PURE__ */ jsxs(
563
+ Box,
564
+ {
565
+ flexDirection: "column",
566
+ borderStyle: selected ? "bold" : "single",
567
+ borderColor: selected ? color : void 0,
568
+ paddingX: 1,
569
+ marginBottom: 0,
570
+ children: [
571
+ /* @__PURE__ */ jsxs(Box, { children: [
572
+ /* @__PURE__ */ jsx(Text, { bold: true, children: repoName(handle.cwd) }),
573
+ /* @__PURE__ */ jsx(Text, { children: " " }),
574
+ isActive && /* @__PURE__ */ jsx(Spinner, {}),
575
+ /* @__PURE__ */ jsxs(Text, { color, children: [
576
+ " ",
577
+ STATUS_LABELS[handle.status]
578
+ ] })
579
+ ] }),
580
+ /* @__PURE__ */ jsxs(Box, { children: [
581
+ handle.currentPhase ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
582
+ "Phase ",
583
+ handle.currentPhase.current,
584
+ "/",
585
+ handle.currentPhase.total,
586
+ ":",
587
+ " ",
588
+ handle.currentPhase.phase
589
+ ] }) : /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2014" }),
590
+ handle.currentIteration && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
591
+ " ",
592
+ "\xB7 iter ",
593
+ handle.currentIteration.iteration,
594
+ "/",
595
+ handle.currentIteration.max
596
+ ] })
597
+ ] }),
598
+ /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
599
+ formatCost(handle.cost),
600
+ " \xB7 ",
601
+ formatElapsed(handle.startedAt),
602
+ handle.totalIterations > 0 && ` \xB7 ${handle.totalIterations} iterations`
603
+ ] }) }),
604
+ handle.error && /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { color: "red", children: [
605
+ "\u2717 ",
606
+ handle.error
607
+ ] }) })
608
+ ]
609
+ }
610
+ );
611
+ }
612
+
613
+ // src/tui/components/NewSessionFlow.tsx
614
+ import { useState as useState3, useMemo as useMemo2 } from "react";
615
+ import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
616
+ import { Spinner as Spinner2 } from "@inkjs/ui";
617
+
618
+ // src/tui/components/RepoSelector.tsx
619
+ import { useState } from "react";
620
+ import { Box as Box2, Text as Text2, useInput } from "ink";
621
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
622
+ var VIEWPORT_SIZE = 10;
623
+ function RepoSelector({ repos, onSelect, onCancel }) {
624
+ const [selectedIndex, setSelectedIndex] = useState(0);
625
+ useInput((_input, key) => {
626
+ if (key.escape) {
627
+ onCancel();
628
+ return;
629
+ }
630
+ if (key.return) {
631
+ if (repos[selectedIndex]) {
632
+ onSelect(repos[selectedIndex]);
633
+ }
634
+ return;
635
+ }
636
+ if (key.upArrow) {
637
+ setSelectedIndex((i) => Math.max(0, i - 1));
638
+ }
639
+ if (key.downArrow) {
640
+ setSelectedIndex((i) => Math.min(repos.length - 1, i + 1));
641
+ }
642
+ });
643
+ const halfWindow = Math.floor(VIEWPORT_SIZE / 2);
644
+ let windowStart = Math.max(0, selectedIndex - halfWindow);
645
+ const windowEnd = Math.min(repos.length, windowStart + VIEWPORT_SIZE);
646
+ if (windowEnd - windowStart < VIEWPORT_SIZE) {
647
+ windowStart = Math.max(0, windowEnd - VIEWPORT_SIZE);
648
+ }
649
+ const visibleRepos = repos.slice(windowStart, windowEnd);
650
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", padding: 1, children: [
651
+ /* @__PURE__ */ jsxs2(Box2, { marginBottom: 1, children: [
652
+ /* @__PURE__ */ jsx2(Text2, { bold: true, children: "Select Repository" }),
653
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
654
+ " \u2014 ",
655
+ repos.length,
656
+ " repos"
657
+ ] })
658
+ ] }),
659
+ repos.length === 0 ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "No repositories found. Add repos to ~/.config/glrs/repos.yaml" }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
660
+ windowStart > 0 && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
661
+ " \u2191 ",
662
+ windowStart,
663
+ " more"
664
+ ] }),
665
+ visibleRepos.map((repo, i) => {
666
+ const globalIndex = windowStart + i;
667
+ const isSelected = globalIndex === selectedIndex;
668
+ return /* @__PURE__ */ jsx2(Box2, { children: /* @__PURE__ */ jsxs2(Text2, { color: isSelected ? "blue" : void 0, children: [
669
+ isSelected ? "\u25B6 " : " ",
670
+ /* @__PURE__ */ jsx2(Text2, { bold: isSelected, children: repo.name })
671
+ ] }) }, `${repo.primaryPath}-${globalIndex}`);
672
+ }),
673
+ windowEnd < repos.length && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
674
+ " \u2193 ",
675
+ repos.length - windowEnd,
676
+ " more"
677
+ ] })
678
+ ] }),
679
+ /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\u2191\u2193 navigate \xB7 Enter select \xB7 Esc cancel" }) })
680
+ ] });
681
+ }
682
+
683
+ // src/tui/components/PlanSelector.tsx
684
+ import { useState as useState2, useMemo } from "react";
685
+ import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
686
+ import * as fs4 from "fs";
687
+ import * as path4 from "path";
688
+ import * as os2 from "os";
689
+ import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
690
+ function countCheckboxes(filePath) {
691
+ try {
692
+ const content = fs4.readFileSync(filePath, "utf8");
693
+ const matches = content.match(/^\s*-\s*\[[ x]\]/gm);
694
+ return matches?.length ?? 0;
695
+ } catch {
696
+ return 0;
697
+ }
698
+ }
699
+ function getMtimeMs(filePath) {
700
+ try {
701
+ return fs4.statSync(filePath).mtimeMs;
702
+ } catch {
703
+ return 0;
704
+ }
705
+ }
706
+ function countPlanEntries(dir) {
707
+ try {
708
+ if (!fs4.existsSync(dir)) return 0;
709
+ const entries = fs4.readdirSync(dir, { withFileTypes: true });
710
+ let count = 0;
711
+ for (const entry of entries) {
712
+ if (entry.isDirectory()) {
713
+ const mainMd = path4.join(dir, entry.name, "main.md");
714
+ const specYaml = path4.join(dir, entry.name, "spec", "main.yaml");
715
+ if (fs4.existsSync(mainMd) || fs4.existsSync(specYaml)) count++;
716
+ } else if (entry.isFile() && entry.name.endsWith(".md") && entry.name !== "README.md") {
717
+ count++;
718
+ }
719
+ }
720
+ return count;
721
+ } catch {
722
+ return 0;
723
+ }
724
+ }
725
+ function scanPlanDir(dir) {
726
+ const results = [];
727
+ try {
728
+ if (!fs4.existsSync(dir)) return results;
729
+ const entries = fs4.readdirSync(dir, { withFileTypes: true });
730
+ for (const entry of entries) {
731
+ const entryPath = path4.join(dir, entry.name);
732
+ if (entry.isDirectory()) {
733
+ const mainMd = path4.join(entryPath, "main.md");
734
+ if (fs4.existsSync(mainMd)) {
735
+ results.push({
736
+ name: entry.name,
737
+ planPath: mainMd,
738
+ itemCount: countCheckboxes(mainMd),
739
+ mtimeMs: getMtimeMs(mainMd)
740
+ });
741
+ continue;
742
+ }
743
+ const specYaml = path4.join(entryPath, "spec", "main.yaml");
744
+ if (fs4.existsSync(specYaml)) {
745
+ results.push({
746
+ name: `${entry.name} (yaml)`,
747
+ planPath: specYaml,
748
+ itemCount: 0,
749
+ mtimeMs: getMtimeMs(specYaml)
750
+ });
751
+ continue;
752
+ }
753
+ } else if (entry.isFile() && entry.name.endsWith(".md") && entry.name !== "README.md") {
754
+ const itemCount = countCheckboxes(entryPath);
755
+ if (itemCount > 0) {
756
+ results.push({
757
+ name: entry.name.replace(/\.md$/, ""),
758
+ planPath: entryPath,
759
+ itemCount,
760
+ mtimeMs: getMtimeMs(entryPath)
761
+ });
762
+ }
763
+ }
764
+ }
765
+ } catch {
766
+ }
767
+ results.sort((a, b) => b.mtimeMs - a.mtimeMs);
768
+ return results;
769
+ }
770
+ function shortenPath(p) {
771
+ const home = os2.homedir();
772
+ if (p.startsWith(home)) return "~" + p.slice(home.length);
773
+ return p;
774
+ }
775
+ function findClonePath(repoName3, candidatePath) {
776
+ const home = os2.homedir();
777
+ if (isGitRepo(candidatePath)) return candidatePath;
778
+ const searchRoots = [
779
+ path4.join(home, "repos"),
780
+ path4.join(home, "projects"),
781
+ path4.join(home, "src"),
782
+ path4.join(home, "code"),
783
+ path4.join(home, "dev")
784
+ ];
785
+ for (const root of searchRoots) {
786
+ const direct = path4.join(root, repoName3);
787
+ if (isGitRepo(direct)) return direct;
788
+ try {
789
+ if (!fs4.existsSync(root)) continue;
790
+ const orgs = fs4.readdirSync(root, { withFileTypes: true });
791
+ for (const org of orgs) {
792
+ if (!org.isDirectory()) continue;
793
+ const nested = path4.join(root, org.name, repoName3);
794
+ if (isGitRepo(nested)) return nested;
795
+ }
796
+ } catch {
797
+ }
798
+ }
799
+ return null;
800
+ }
801
+ function isGitRepo(dir) {
802
+ return fs4.existsSync(path4.join(dir, ".git"));
803
+ }
804
+ function buildPlanLocations(repoPath, repoName3) {
805
+ const home = os2.homedir();
806
+ const locations = [];
807
+ const clonePath = isGitRepo(repoPath) ? repoPath : findClonePath(repoName3, repoPath);
808
+ if (clonePath) {
809
+ const localDirs = [
810
+ { dir: path4.join(clonePath, ".glrs", "plans"), label: `${shortenPath(clonePath)}/.glrs/plans` },
811
+ { dir: path4.join(clonePath, ".opencode", "plans"), label: `${shortenPath(clonePath)}/.opencode/plans` },
812
+ { dir: path4.join(clonePath, "docs", "plans"), label: `${shortenPath(clonePath)}/docs/plans` },
813
+ { dir: path4.join(clonePath, "plans"), label: `${shortenPath(clonePath)}/plans` }
814
+ ];
815
+ for (const { dir, label } of localDirs) {
816
+ const fileCount = countPlanEntries(dir);
817
+ if (fileCount > 0 || dir.includes(".glrs")) {
818
+ locations.push({ dir, label, fileCount });
819
+ }
820
+ }
821
+ }
822
+ const globalDirs = [
823
+ { dir: path4.join(home, ".glrs", repoName3, "plans"), label: `~/.glrs/${repoName3}/plans` },
824
+ { dir: path4.join(home, ".glorious", "opencode", repoName3, "plans"), label: `~/.glorious/opencode/${repoName3}/plans`, deprecated: true }
825
+ ];
826
+ for (const { dir, label, deprecated } of globalDirs) {
827
+ const fileCount = countPlanEntries(dir);
828
+ if (fileCount > 0 || !deprecated) {
829
+ locations.push({ dir, label, fileCount, deprecated });
830
+ }
831
+ }
832
+ const exploreDir = clonePath ?? home;
833
+ locations.push({
834
+ dir: exploreDir,
835
+ label: `${shortenPath(exploreDir)} [EXPLORE]`,
836
+ fileCount: 0,
837
+ explore: true
838
+ });
839
+ return locations;
840
+ }
841
+ function FileExplorer({ startDir, onSelect, onCancel }) {
842
+ const [cwd, setCwd] = useState2(startDir);
843
+ const [selectedIndex, setSelectedIndex] = useState2(0);
844
+ const entries = useMemo(() => {
845
+ try {
846
+ const raw = fs4.readdirSync(cwd, { withFileTypes: true });
847
+ const dirs = [];
848
+ const files = [];
849
+ for (const entry of raw) {
850
+ if (entry.name.startsWith(".") && entry.name !== ".glrs" && entry.name !== ".opencode") continue;
851
+ if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".git") continue;
852
+ const fullPath = path4.join(cwd, entry.name);
853
+ if (entry.isDirectory()) {
854
+ dirs.push({ name: entry.name + "/", isDir: true, path: fullPath });
855
+ } else if (entry.name.endsWith(".md") || entry.name.endsWith(".yaml")) {
856
+ files.push({ name: entry.name, isDir: false, path: fullPath });
857
+ }
858
+ }
859
+ dirs.sort((a, b) => a.name.localeCompare(b.name));
860
+ files.sort((a, b) => a.name.localeCompare(b.name));
861
+ return [...dirs, ...files];
862
+ } catch {
863
+ return [];
864
+ }
865
+ }, [cwd]);
866
+ useInput2((_input, key) => {
867
+ if (key.escape) {
868
+ if (cwd === startDir) {
869
+ onCancel();
870
+ } else {
871
+ setCwd(path4.dirname(cwd));
872
+ setSelectedIndex(0);
873
+ }
874
+ return;
875
+ }
876
+ if (key.return && entries[selectedIndex]) {
877
+ const entry = entries[selectedIndex];
878
+ if (entry.isDir) {
879
+ const mainMd = path4.join(entry.path, "main.md");
880
+ const specYaml = path4.join(entry.path, "spec", "main.yaml");
881
+ if (fs4.existsSync(mainMd)) {
882
+ onSelect(mainMd);
883
+ } else if (fs4.existsSync(specYaml)) {
884
+ onSelect(specYaml);
885
+ } else {
886
+ setCwd(entry.path);
887
+ setSelectedIndex(0);
888
+ }
889
+ } else {
890
+ onSelect(entry.path);
891
+ }
892
+ return;
893
+ }
894
+ if (key.upArrow) {
895
+ setSelectedIndex((i) => Math.max(0, i - 1));
896
+ }
897
+ if (key.downArrow) {
898
+ setSelectedIndex((i) => Math.min(entries.length - 1, i + 1));
899
+ }
900
+ });
901
+ const VIEWPORT = 12;
902
+ const halfWindow = Math.floor(VIEWPORT / 2);
903
+ let windowStart = Math.max(0, selectedIndex - halfWindow);
904
+ const windowEnd = Math.min(entries.length, windowStart + VIEWPORT);
905
+ if (windowEnd - windowStart < VIEWPORT) {
906
+ windowStart = Math.max(0, windowEnd - VIEWPORT);
907
+ }
908
+ const visible = entries.slice(windowStart, windowEnd);
909
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", padding: 1, children: [
910
+ /* @__PURE__ */ jsxs3(Box3, { marginBottom: 1, children: [
911
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: "Explore: " }),
912
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: shortenPath(cwd) })
913
+ ] }),
914
+ windowStart > 0 && /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
915
+ " \u2191 ",
916
+ windowStart,
917
+ " more"
918
+ ] }),
919
+ visible.map((entry, i) => {
920
+ const globalIndex = windowStart + i;
921
+ const isSelected = globalIndex === selectedIndex;
922
+ return /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsxs3(Text3, { color: isSelected ? "blue" : void 0, children: [
923
+ isSelected ? "\u25B6 " : " ",
924
+ /* @__PURE__ */ jsx3(Text3, { bold: isSelected, color: entry.isDir ? "cyan" : void 0, children: entry.name })
925
+ ] }) }, `${entry.path}-${globalIndex}`);
926
+ }),
927
+ windowEnd < entries.length && /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
928
+ " \u2193 ",
929
+ entries.length - windowEnd,
930
+ " more"
931
+ ] }),
932
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2191\u2193 navigate \xB7 Enter open/select \xB7 Esc back" }) })
933
+ ] });
934
+ }
935
+ function PlanList({ location, onSelect, onBack }) {
936
+ const [selectedIndex, setSelectedIndex] = useState2(0);
937
+ const plans = useMemo(() => scanPlanDir(location.dir), [location.dir]);
938
+ useInput2((_input, key) => {
939
+ if (key.escape) {
940
+ onBack();
941
+ return;
942
+ }
943
+ if (key.return && plans[selectedIndex]) {
944
+ onSelect(plans[selectedIndex].planPath);
945
+ return;
946
+ }
947
+ if (key.upArrow) {
948
+ setSelectedIndex((i) => Math.max(0, i - 1));
949
+ }
950
+ if (key.downArrow) {
951
+ setSelectedIndex((i) => Math.min(plans.length - 1, i + 1));
952
+ }
953
+ });
954
+ const VIEWPORT = 10;
955
+ const halfWindow = Math.floor(VIEWPORT / 2);
956
+ let windowStart = Math.max(0, selectedIndex - halfWindow);
957
+ const windowEnd = Math.min(plans.length, windowStart + VIEWPORT);
958
+ if (windowEnd - windowStart < VIEWPORT) {
959
+ windowStart = Math.max(0, windowEnd - VIEWPORT);
960
+ }
961
+ const visible = plans.slice(windowStart, windowEnd);
962
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", padding: 1, children: [
963
+ /* @__PURE__ */ jsxs3(Box3, { marginBottom: 1, children: [
964
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: "Plans in " }),
965
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: location.label })
966
+ ] }),
967
+ plans.length === 0 ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No plans in this location." }) : /* @__PURE__ */ jsxs3(Fragment2, { children: [
968
+ windowStart > 0 && /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
969
+ " \u2191 ",
970
+ windowStart,
971
+ " more"
972
+ ] }),
973
+ visible.map((plan, i) => {
974
+ const globalIndex = windowStart + i;
975
+ const isSelected = globalIndex === selectedIndex;
976
+ return /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsxs3(Text3, { color: isSelected ? "blue" : void 0, children: [
977
+ isSelected ? "\u25B6 " : " ",
978
+ /* @__PURE__ */ jsx3(Text3, { bold: isSelected, children: plan.name }),
979
+ plan.itemCount > 0 && /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
980
+ " (",
981
+ plan.itemCount,
982
+ " items)"
983
+ ] })
984
+ ] }) }, `${plan.planPath}-${globalIndex}`);
985
+ }),
986
+ windowEnd < plans.length && /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
987
+ " \u2193 ",
988
+ plans.length - windowEnd,
989
+ " more"
990
+ ] })
991
+ ] }),
992
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2191\u2193 navigate \xB7 Enter select \xB7 Esc back" }) })
993
+ ] });
994
+ }
995
+ var VIEWPORT_SIZE2 = 10;
996
+ function PlanSelector({ repoPath, repoName: repoName3, onSelect, onCancel }) {
997
+ const [view, setView] = useState2({ kind: "locations" });
998
+ const [selectedIndex, setSelectedIndex] = useState2(0);
999
+ const locations = useMemo(() => buildPlanLocations(repoPath, repoName3), [repoPath, repoName3]);
1000
+ useInput2((_input, key) => {
1001
+ if (view.kind !== "locations") return;
1002
+ if (key.escape) {
1003
+ onCancel();
1004
+ return;
1005
+ }
1006
+ if (key.return && locations[selectedIndex]) {
1007
+ const loc = locations[selectedIndex];
1008
+ if (loc.explore) {
1009
+ setView({ kind: "explore", startDir: loc.dir });
1010
+ } else if (loc.fileCount > 0) {
1011
+ setView({ kind: "plans", location: loc });
1012
+ }
1013
+ return;
1014
+ }
1015
+ if (key.upArrow) {
1016
+ setSelectedIndex((i) => Math.max(0, i - 1));
1017
+ }
1018
+ if (key.downArrow) {
1019
+ setSelectedIndex((i) => Math.min(locations.length - 1, i + 1));
1020
+ }
1021
+ });
1022
+ if (view.kind === "plans") {
1023
+ return /* @__PURE__ */ jsx3(
1024
+ PlanList,
1025
+ {
1026
+ location: view.location,
1027
+ onSelect,
1028
+ onBack: () => setView({ kind: "locations" })
1029
+ }
1030
+ );
1031
+ }
1032
+ if (view.kind === "explore") {
1033
+ return /* @__PURE__ */ jsx3(
1034
+ FileExplorer,
1035
+ {
1036
+ startDir: view.startDir,
1037
+ onSelect,
1038
+ onCancel: () => setView({ kind: "locations" })
1039
+ }
1040
+ );
1041
+ }
1042
+ const halfWindow = Math.floor(VIEWPORT_SIZE2 / 2);
1043
+ let windowStart = Math.max(0, selectedIndex - halfWindow);
1044
+ const windowEnd = Math.min(locations.length, windowStart + VIEWPORT_SIZE2);
1045
+ if (windowEnd - windowStart < VIEWPORT_SIZE2) {
1046
+ windowStart = Math.max(0, windowEnd - VIEWPORT_SIZE2);
1047
+ }
1048
+ const visible = locations.slice(windowStart, windowEnd);
1049
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", padding: 1, children: [
1050
+ /* @__PURE__ */ jsxs3(Box3, { marginBottom: 1, children: [
1051
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: "Select Plan Location" }),
1052
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1053
+ " \u2014 ",
1054
+ repoName3
1055
+ ] })
1056
+ ] }),
1057
+ locations.length === 0 ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No plan locations found for this repo." }) : /* @__PURE__ */ jsxs3(Fragment2, { children: [
1058
+ windowStart > 0 && /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1059
+ " \u2191 ",
1060
+ windowStart,
1061
+ " more"
1062
+ ] }),
1063
+ visible.map((loc, i) => {
1064
+ const globalIndex = windowStart + i;
1065
+ const isSelected = globalIndex === selectedIndex;
1066
+ const canEnter = loc.fileCount > 0 || loc.explore;
1067
+ return /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsxs3(Text3, { color: isSelected ? "blue" : void 0, dimColor: !canEnter && !isSelected, children: [
1068
+ isSelected ? "\u25B6 " : " ",
1069
+ /* @__PURE__ */ jsx3(Text3, { bold: isSelected, children: loc.label }),
1070
+ loc.deprecated && /* @__PURE__ */ jsx3(Text3, { color: "yellow", children: " [DEPRECATED]" }),
1071
+ !loc.explore && /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1072
+ " (",
1073
+ loc.fileCount,
1074
+ " files)"
1075
+ ] })
1076
+ ] }) }, `${loc.dir}-${globalIndex}`);
1077
+ }),
1078
+ windowEnd < locations.length && /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1079
+ " \u2193 ",
1080
+ locations.length - windowEnd,
1081
+ " more"
1082
+ ] })
1083
+ ] }),
1084
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2191\u2193 navigate \xB7 Enter open \xB7 Esc back" }) })
1085
+ ] });
1086
+ }
1087
+
1088
+ // src/tui/components/NewSessionFlow.tsx
1089
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1090
+ function NewSessionFlow({ manager, onDone, onCancel }) {
1091
+ const [step, setStep] = useState3({ kind: "repo" });
1092
+ const repos = useMemo2(() => getUniqueRepos(), []);
1093
+ useInput3((_input, key) => {
1094
+ if (step.kind === "error" && (key.escape || key.return)) {
1095
+ onDone();
1096
+ }
1097
+ });
1098
+ if (step.kind === "repo") {
1099
+ return /* @__PURE__ */ jsx4(
1100
+ RepoSelector,
1101
+ {
1102
+ repos,
1103
+ onSelect: (repo) => setStep({ kind: "plan", repo }),
1104
+ onCancel
1105
+ }
1106
+ );
1107
+ }
1108
+ if (step.kind === "plan") {
1109
+ return /* @__PURE__ */ jsx4(
1110
+ PlanSelector,
1111
+ {
1112
+ repoPath: step.repo.primaryPath,
1113
+ repoName: step.repo.name,
1114
+ onSelect: (planPath) => setStep({ kind: "confirm", repo: step.repo, planPath }),
1115
+ onCancel: () => setStep({ kind: "repo" })
1116
+ }
1117
+ );
1118
+ }
1119
+ if (step.kind === "launching") {
1120
+ return /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", padding: 1, children: /* @__PURE__ */ jsxs4(Box4, { children: [
1121
+ /* @__PURE__ */ jsx4(Spinner2, {}),
1122
+ /* @__PURE__ */ jsx4(Text4, { children: " Creating worktree and launching session..." })
1123
+ ] }) });
1124
+ }
1125
+ if (step.kind === "error") {
1126
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", padding: 1, children: [
1127
+ /* @__PURE__ */ jsx4(Box4, { marginBottom: 1, children: /* @__PURE__ */ jsx4(Text4, { color: "red", bold: true, children: "Launch failed" }) }),
1128
+ /* @__PURE__ */ jsx4(Text4, { color: "red", children: step.message }),
1129
+ /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Press Enter or Esc to return to dashboard" }) })
1130
+ ] });
1131
+ }
1132
+ return /* @__PURE__ */ jsx4(
1133
+ ConfirmLaunch,
1134
+ {
1135
+ repo: step.repo,
1136
+ planPath: step.planPath,
1137
+ onConfirm: () => {
1138
+ setStep({ kind: "launching" });
1139
+ setTimeout(() => {
1140
+ try {
1141
+ manager.launchSessionWithWorktree({
1142
+ repoName: step.repo.name,
1143
+ planPath: step.planPath,
1144
+ fast: true
1145
+ });
1146
+ onDone();
1147
+ } catch (err) {
1148
+ const message = err instanceof Error ? err.message : String(err);
1149
+ setStep({ kind: "error", message });
1150
+ }
1151
+ }, 50);
1152
+ },
1153
+ onCancel: () => setStep({ kind: "plan", repo: step.repo })
1154
+ }
1155
+ );
1156
+ }
1157
+ function ConfirmLaunch({
1158
+ repo,
1159
+ planPath,
1160
+ onConfirm,
1161
+ onCancel
1162
+ }) {
1163
+ useInput3((_input, key) => {
1164
+ if (key.return) {
1165
+ onConfirm();
1166
+ return;
1167
+ }
1168
+ if (key.escape) {
1169
+ onCancel();
1170
+ return;
1171
+ }
1172
+ });
1173
+ const planName = planPath.split("/").pop() ?? planPath;
1174
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", padding: 1, children: [
1175
+ /* @__PURE__ */ jsx4(Box4, { marginBottom: 1, children: /* @__PURE__ */ jsx4(Text4, { bold: true, children: "Launch Session" }) }),
1176
+ /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginBottom: 1, children: [
1177
+ /* @__PURE__ */ jsxs4(Box4, { children: [
1178
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Repo: " }),
1179
+ /* @__PURE__ */ jsx4(Text4, { children: repo.name })
1180
+ ] }),
1181
+ /* @__PURE__ */ jsxs4(Box4, { children: [
1182
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Plan: " }),
1183
+ /* @__PURE__ */ jsx4(Text4, { children: planName })
1184
+ ] }),
1185
+ /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
1186
+ "A fresh worktree will be created via `glrs wt new ",
1187
+ repo.name,
1188
+ "`."
1189
+ ] }) })
1190
+ ] }),
1191
+ /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Enter to launch \xB7 Esc to cancel" }) })
1192
+ ] });
1193
+ }
1194
+
1195
+ // src/tui/components/SessionExpanded.tsx
1196
+ import { Box as Box7, Text as Text7, useInput as useInput4 } from "ink";
1197
+ import { Spinner as Spinner3 } from "@inkjs/ui";
1198
+
1199
+ // src/tui/components/PhaseTree.tsx
1200
+ import { Box as Box5, Text as Text5 } from "ink";
1201
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1202
+ function PhaseTree({ handle }) {
1203
+ const { currentPhase } = handle;
1204
+ if (!currentPhase) {
1205
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
1206
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Phase Progress" }),
1207
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " No phase information available" })
1208
+ ] });
1209
+ }
1210
+ const { phase, current, total } = currentPhase;
1211
+ const phases = [];
1212
+ for (let i = 1; i <= total; i++) {
1213
+ if (i < current) {
1214
+ phases.push(`phase_${i - 1}.md`);
1215
+ } else if (i === current) {
1216
+ phases.push(phase);
1217
+ } else {
1218
+ phases.push(`phase_${i - 1}.md`);
1219
+ }
1220
+ }
1221
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
1222
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Phase Progress" }),
1223
+ phases.map((p, i) => {
1224
+ const phaseNum = i + 1;
1225
+ let icon;
1226
+ let color;
1227
+ let extra = "";
1228
+ if (phaseNum < current) {
1229
+ icon = "\u2713";
1230
+ color = "green";
1231
+ } else if (phaseNum === current) {
1232
+ icon = "\u25CF";
1233
+ color = "blue";
1234
+ if (handle.currentIteration) {
1235
+ extra = ` (iter ${handle.currentIteration.iteration}/${handle.currentIteration.max})`;
1236
+ }
1237
+ } else {
1238
+ icon = "\u25CB";
1239
+ color = "gray";
1240
+ }
1241
+ return /* @__PURE__ */ jsxs5(Box5, { children: [
1242
+ /* @__PURE__ */ jsxs5(Text5, { color, children: [
1243
+ " ",
1244
+ icon,
1245
+ " "
1246
+ ] }),
1247
+ /* @__PURE__ */ jsxs5(Text5, { dimColor: phaseNum !== current, children: [
1248
+ p,
1249
+ extra
1250
+ ] })
1251
+ ] }, i);
1252
+ })
1253
+ ] });
1254
+ }
1255
+
1256
+ // src/tui/components/EventTail.tsx
1257
+ import { useState as useState4, useEffect, useRef } from "react";
1258
+ import { Box as Box6, Text as Text6 } from "ink";
1259
+ import { EventStreamReader as EventStreamReader3 } from "@glrs-dev/autopilot";
1260
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1261
+ function formatTime(timestamp) {
1262
+ try {
1263
+ const d = new Date(timestamp);
1264
+ const hh = String(d.getHours()).padStart(2, "0");
1265
+ const mm = String(d.getMinutes()).padStart(2, "0");
1266
+ const ss = String(d.getSeconds()).padStart(2, "0");
1267
+ return `${hh}:${mm}:${ss}`;
1268
+ } catch {
1269
+ return "??:??:??";
1270
+ }
1271
+ }
1272
+ function formatEventDetails(e) {
1273
+ switch (e.type) {
1274
+ case "session:start":
1275
+ return e.planPath ?? "";
1276
+ case "session:done":
1277
+ return e.exitReason ?? "";
1278
+ case "phase:start":
1279
+ return `${e.phase} (${e.current}/${e.total})`;
1280
+ case "phase:done":
1281
+ return `${e.phase}`;
1282
+ case "iteration:start":
1283
+ return `${e.iteration}/${e.maxIterations}`;
1284
+ case "iteration:done":
1285
+ return `${e.iteration} ${e.madeProgress ? "\u2713" : "\u2014"}`;
1286
+ case "tool:call":
1287
+ return `${e.toolName} ${e.firstArg ?? ""}`;
1288
+ case "cost:update":
1289
+ return `$${e.cumulativeCostUsd.toFixed(3)}`;
1290
+ case "error":
1291
+ return e.message.slice(0, 60);
1292
+ case "enrich:start":
1293
+ return e.planPath ?? "";
1294
+ case "enrich:done":
1295
+ return `${e.filesProcessed} files`;
1296
+ case "verify:start":
1297
+ return `${e.itemCount} items`;
1298
+ case "verify:done":
1299
+ return `${e.passed} passed, ${e.failed} failed`;
1300
+ default:
1301
+ return "";
1302
+ }
1303
+ }
1304
+ function EventTail({ eventFilePath, maxEvents = 20 }) {
1305
+ const [events, setEvents] = useState4([]);
1306
+ const offsetRef = useRef(0);
1307
+ useEffect(() => {
1308
+ const reader = new EventStreamReader3(eventFilePath);
1309
+ const interval = setInterval(() => {
1310
+ const { events: newEvents, newOffset } = reader.readFrom(offsetRef.current);
1311
+ offsetRef.current = newOffset;
1312
+ if (newEvents.length > 0) {
1313
+ setEvents((prev) => [...prev, ...newEvents].slice(-maxEvents));
1314
+ }
1315
+ }, 500);
1316
+ return () => clearInterval(interval);
1317
+ }, [eventFilePath, maxEvents]);
1318
+ if (events.length === 0) {
1319
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
1320
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: "Live Events" }),
1321
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " Waiting for events\u2026" })
1322
+ ] });
1323
+ }
1324
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
1325
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: "Live Events" }),
1326
+ events.map((e, i) => /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1327
+ " ",
1328
+ formatTime(e.timestamp),
1329
+ " ",
1330
+ e.type,
1331
+ " ",
1332
+ formatEventDetails(e)
1333
+ ] }, i))
1334
+ ] });
1335
+ }
1336
+
1337
+ // src/tui/components/SessionExpanded.tsx
1338
+ import * as path5 from "path";
1339
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1340
+ var STATUS_COLORS2 = {
1341
+ running: "blue",
1342
+ enriching: "yellow",
1343
+ verifying: "cyan",
1344
+ complete: "green",
1345
+ error: "red",
1346
+ stale: "gray"
1347
+ };
1348
+ function formatElapsed2(startedAt) {
1349
+ const ms = Date.now() - new Date(startedAt).getTime();
1350
+ const seconds = Math.floor(ms / 1e3);
1351
+ const minutes = Math.floor(seconds / 60);
1352
+ const hours = Math.floor(minutes / 60);
1353
+ if (hours > 0) return `${hours}h ${minutes % 60}m`;
1354
+ if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
1355
+ return `${seconds}s`;
1356
+ }
1357
+ function formatCost2(cost) {
1358
+ return `$${cost.toFixed(3)}`;
1359
+ }
1360
+ function repoName2(cwd) {
1361
+ return cwd.split("/").pop() || cwd;
1362
+ }
1363
+ function SessionExpanded({ handle, manager, onBack }) {
1364
+ const isActive = handle.status === "running" || handle.status === "enriching" || handle.status === "verifying";
1365
+ const isTerminal = handle.status === "complete" || handle.status === "error" || handle.status === "stale";
1366
+ const color = STATUS_COLORS2[handle.status];
1367
+ const eventFilePath = path5.join(handle.cwd, ".agent", "autopilot-events.jsonl");
1368
+ useInput4((input, key) => {
1369
+ if (key.escape) {
1370
+ onBack();
1371
+ return;
1372
+ }
1373
+ if (input === "k") {
1374
+ manager.killSession(handle.id);
1375
+ return;
1376
+ }
1377
+ if (input === "r" && isTerminal) {
1378
+ manager.retrySession(handle.id);
1379
+ onBack();
1380
+ return;
1381
+ }
1382
+ if (input === "c" && isTerminal) {
1383
+ manager.cleanupSession(handle.id);
1384
+ onBack();
1385
+ return;
1386
+ }
1387
+ });
1388
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", padding: 1, borderStyle: "single", children: [
1389
+ /* @__PURE__ */ jsxs7(Box7, { marginBottom: 1, children: [
1390
+ /* @__PURE__ */ jsxs7(Text7, { bold: true, children: [
1391
+ "Session: ",
1392
+ repoName2(handle.cwd)
1393
+ ] }),
1394
+ /* @__PURE__ */ jsx7(Text7, { children: " " }),
1395
+ isActive && /* @__PURE__ */ jsx7(Spinner3, {}),
1396
+ /* @__PURE__ */ jsxs7(Text7, { color, children: [
1397
+ " ",
1398
+ handle.status
1399
+ ] })
1400
+ ] }),
1401
+ /* @__PURE__ */ jsx7(Box7, { marginBottom: 1, children: /* @__PURE__ */ jsx7(PhaseTree, { handle }) }),
1402
+ /* @__PURE__ */ jsx7(Box7, { marginBottom: 1, children: /* @__PURE__ */ jsx7(Text7, { bold: true, children: "Summary" }) }),
1403
+ /* @__PURE__ */ jsx7(Box7, { marginBottom: 1, children: /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
1404
+ "Cost: ",
1405
+ formatCost2(handle.cost),
1406
+ " \xB7 Elapsed: ",
1407
+ formatElapsed2(handle.startedAt),
1408
+ handle.totalIterations > 0 && ` \xB7 ${handle.totalIterations} iterations`
1409
+ ] }) }),
1410
+ handle.error && /* @__PURE__ */ jsx7(Box7, { marginBottom: 1, children: /* @__PURE__ */ jsxs7(Text7, { color: "red", children: [
1411
+ "\u2717 Error: ",
1412
+ handle.error
1413
+ ] }) }),
1414
+ /* @__PURE__ */ jsx7(Box7, { marginBottom: 1, children: /* @__PURE__ */ jsx7(EventTail, { eventFilePath }) }),
1415
+ /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
1416
+ "esc back \xB7 k kill",
1417
+ isTerminal ? " \xB7 r retry \xB7 c cleanup" : ""
1418
+ ] }) })
1419
+ ] });
1420
+ }
1421
+
1422
+ // src/tui/components/Dashboard.tsx
1423
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1424
+ function Dashboard({ manager }) {
1425
+ const { exit } = useApp();
1426
+ const [sessions, setSessions] = useState5([]);
1427
+ const [selectedIndex, setSelectedIndex] = useState5(0);
1428
+ const [view, setView] = useState5({ kind: "dashboard" });
1429
+ useEffect2(() => {
1430
+ setSessions(manager.getSessions());
1431
+ const interval = setInterval(() => {
1432
+ setSessions(manager.getSessions());
1433
+ }, 1e3);
1434
+ return () => clearInterval(interval);
1435
+ }, [manager]);
1436
+ useInput5((input, key) => {
1437
+ if (view.kind !== "dashboard") return;
1438
+ if (input === "q") {
1439
+ exit();
1440
+ return;
1441
+ }
1442
+ if (input === "n") {
1443
+ setView({ kind: "new-session" });
1444
+ return;
1445
+ }
1446
+ if (key.return && sessions[selectedIndex]) {
1447
+ setView({ kind: "expanded", sessionId: sessions[selectedIndex].id });
1448
+ return;
1449
+ }
1450
+ if (input === "k" && sessions[selectedIndex]) {
1451
+ manager.killSession(sessions[selectedIndex].id);
1452
+ return;
1453
+ }
1454
+ if (key.upArrow) {
1455
+ setSelectedIndex((i) => Math.max(0, i - 1));
1456
+ }
1457
+ if (key.downArrow) {
1458
+ setSelectedIndex((i) => Math.min(sessions.length - 1, i + 1));
1459
+ }
1460
+ });
1461
+ if (view.kind === "new-session") {
1462
+ return /* @__PURE__ */ jsx8(
1463
+ NewSessionFlow,
1464
+ {
1465
+ manager,
1466
+ onDone: () => setView({ kind: "dashboard" }),
1467
+ onCancel: () => setView({ kind: "dashboard" })
1468
+ }
1469
+ );
1470
+ }
1471
+ if (view.kind === "expanded") {
1472
+ const session = sessions.find((s) => s.id === view.sessionId);
1473
+ if (session) {
1474
+ return /* @__PURE__ */ jsx8(
1475
+ SessionExpanded,
1476
+ {
1477
+ handle: session,
1478
+ manager,
1479
+ onBack: () => setView({ kind: "dashboard" })
1480
+ }
1481
+ );
1482
+ }
1483
+ setView({ kind: "dashboard" });
1484
+ }
1485
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", padding: 1, children: [
1486
+ /* @__PURE__ */ jsxs8(Box8, { marginBottom: 1, children: [
1487
+ /* @__PURE__ */ jsx8(Text8, { bold: true, children: "Autopilot Dashboard" }),
1488
+ /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
1489
+ " ",
1490
+ "\u2014 ",
1491
+ sessions.length,
1492
+ " session",
1493
+ sessions.length !== 1 ? "s" : ""
1494
+ ] })
1495
+ ] }),
1496
+ sessions.length === 0 ? /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "No active sessions. Press n to launch one, q to quit." }) : sessions.map((session, i) => /* @__PURE__ */ jsx8(
1497
+ SessionCard,
1498
+ {
1499
+ handle: session,
1500
+ selected: i === selectedIndex
1501
+ },
1502
+ session.id
1503
+ )),
1504
+ /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "\u2191\u2193 navigate \xB7 Enter expand \xB7 n new \xB7 k kill \xB7 q quit" }) })
1505
+ ] });
1506
+ }
1507
+
1508
+ // src/tui/index.ts
1509
+ async function startDashboard(manager) {
1510
+ manager.start();
1511
+ const app = render(
1512
+ React6.createElement(Dashboard, { manager }),
1513
+ { stdout: process.stderr, exitOnCtrlC: false }
1514
+ );
1515
+ await app.waitUntilExit();
1516
+ manager.stop();
1517
+ }
1518
+
1519
+ // src/commands/dashboard.ts
1520
+ async function runDashboard() {
1521
+ const repos = getConfiguredRepos();
1522
+ const dirs = repos.map((r) => r.path);
1523
+ const cwd = process.cwd();
1524
+ if (!dirs.includes(cwd)) {
1525
+ dirs.unshift(cwd);
1526
+ }
1527
+ const manager = new SessionManager(dirs);
1528
+ if (!process.stderr.isTTY) {
1529
+ manager.start();
1530
+ const sessions = manager.getSessions();
1531
+ manager.stop();
1532
+ if (sessions.length === 0) {
1533
+ process.stderr.write("No active autopilot sessions.\n");
1534
+ return;
1535
+ }
1536
+ for (const s of sessions) {
1537
+ const repo = s.cwd.split("/").pop() || s.cwd;
1538
+ process.stderr.write(
1539
+ `${s.status.toUpperCase().padEnd(10)} ${repo} \u2014 $${s.cost.toFixed(2)} \u2014 ${s.totalIterations} iterations
1540
+ `
1541
+ );
1542
+ }
1543
+ return;
1544
+ }
1545
+ await startDashboard(manager);
1546
+ }
1547
+ export {
1548
+ runDashboard
1549
+ };