@glrs-dev/cli 2.2.0 → 2.4.0
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/CHANGELOG.md +4 -0
- package/dist/{chunk-EM4MJBOD.js → chunk-2AZKRWC6.js} +4 -4
- package/dist/{chunk-UXBOTMDY.js → chunk-2P3ETOT2.js} +2 -2
- package/dist/chunk-2VMFXAJH.js +795 -0
- package/dist/chunk-5ZVUFNCP.js +140 -0
- package/dist/{chunk-W37UX3U2.js → chunk-6Y27RQQL.js} +2 -2
- package/dist/{chunk-RZWOWTKF.js → chunk-EKNRKZWR.js} +4 -4
- package/dist/{chunk-YGNDPKIW.js → chunk-HQUCVJ4G.js} +3 -1
- package/dist/{chunk-OABVEBWW.js → chunk-MBEVC327.js} +1 -1
- package/dist/{chunk-SB3MLROC.js → chunk-MCM47HH4.js} +8 -3
- package/dist/{chunk-F3AFRUT2.js → chunk-PTIO556V.js} +2 -2
- package/dist/{chunk-E2UNZIZT.js → chunk-R2WXQ54P.js} +1 -1
- package/dist/{chunk-I2KUXY3I.js → chunk-SMDIOB5B.js} +2 -2
- package/dist/{chunk-SPULDN7P.js → chunk-YY7EWHMA.js} +5 -3
- package/dist/cli.js +31 -20
- package/dist/commands/autopilot-interactive.d.ts +89 -0
- package/dist/commands/autopilot-interactive.js +248 -0
- package/dist/commands/autopilot-raw.d.ts +1 -0
- package/dist/commands/autopilot-raw.js +368 -0
- package/dist/commands/autopilot-tui.d.ts +7 -0
- package/dist/commands/autopilot-tui.js +7 -0
- package/dist/commands/autopilot.d.ts +39 -0
- package/dist/commands/autopilot.js +395 -0
- package/dist/commands/cleanup.js +3 -3
- package/dist/commands/create.js +4 -4
- package/dist/commands/dashboard.d.ts +3 -0
- package/dist/commands/dashboard.js +1549 -0
- package/dist/commands/debrief.d.ts +57 -0
- package/dist/commands/debrief.js +9 -0
- package/dist/commands/delete.js +3 -3
- package/dist/commands/go.js +2 -2
- package/dist/commands/list.js +3 -3
- package/dist/commands/loop.d.ts +42 -0
- package/dist/commands/loop.js +133 -0
- package/dist/commands/plan-picker.d.ts +15 -0
- package/dist/commands/plan-picker.js +76 -0
- package/dist/commands/scoper.d.ts +54 -0
- package/dist/commands/scoper.js +341 -0
- package/dist/commands/switch.js +3 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/lib/auto-update.js +1 -1
- package/dist/lib/config.d.ts +3 -2
- package/dist/lib/config.js +1 -1
- package/dist/lib/registry.d.ts +2 -0
- package/dist/lib/registry.js +1 -1
- package/dist/lib/worktree.js +3 -3
- package/dist/vendor/harness-opencode/dist/agents/prompts/build.md +16 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/code-reviewer-thorough.md +6 -7
- package/dist/vendor/harness-opencode/dist/agents/prompts/debriefer.md +55 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/plan-reviewer.md +2 -1
- package/dist/vendor/harness-opencode/dist/agents/prompts/plan.md +104 -7
- package/dist/vendor/harness-opencode/dist/agents/prompts/prime.md +4 -2
- package/dist/vendor/harness-opencode/dist/agents/prompts/scoper.md +129 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/spec-reviewer.md +0 -1
- package/dist/vendor/harness-opencode/dist/agents/prompts/spec-reviewer.open.md +0 -1
- package/dist/vendor/harness-opencode/dist/chunk-GILWWWMB.js +66 -0
- package/dist/vendor/harness-opencode/dist/cli.js +328 -687
- package/dist/vendor/harness-opencode/dist/index.js +123 -20
- package/dist/vendor/harness-opencode/dist/plugin-check-GJRD2OK6.js +14 -0
- package/dist/vendor/harness-opencode/dist/skills/spear-protocol/SKILL.md +2 -1
- package/dist/vendor/harness-opencode/package.json +1 -1
- package/package.json +10 -2
- package/dist/vendor/harness-opencode/dist/autopilot/prompt-template.md +0 -80
- package/dist/vendor/harness-opencode/dist/bin/plan-check.sh +0 -255
|
@@ -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
|
+
};
|