copilot-agent 0.5.0 → 0.7.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/dist/index.js CHANGED
@@ -130,6 +130,102 @@ function findLatestIncomplete() {
130
130
  }
131
131
  return null;
132
132
  }
133
+ function getSessionReport(sid) {
134
+ if (!validateSession(sid)) return null;
135
+ const ws = readWorkspace(sid);
136
+ let lines;
137
+ try {
138
+ lines = readFileSync(join(SESSION_DIR, sid, "events.jsonl"), "utf-8").trimEnd().split("\n");
139
+ } catch {
140
+ return null;
141
+ }
142
+ const report = {
143
+ id: sid,
144
+ cwd: ws.cwd ?? "",
145
+ summary: ws.summary ?? "",
146
+ startTime: "",
147
+ endTime: "",
148
+ durationMs: 0,
149
+ complete: false,
150
+ userMessages: 0,
151
+ assistantTurns: 0,
152
+ outputTokens: 0,
153
+ premiumRequests: 0,
154
+ toolUsage: {},
155
+ gitCommits: [],
156
+ filesCreated: [],
157
+ filesEdited: [],
158
+ errors: [],
159
+ taskCompletions: []
160
+ };
161
+ for (const line of lines) {
162
+ let event;
163
+ try {
164
+ event = JSON.parse(line);
165
+ } catch {
166
+ continue;
167
+ }
168
+ const type = event.type;
169
+ const ts = event.timestamp;
170
+ const data = event.data ?? {};
171
+ if (ts && !report.startTime) report.startTime = ts;
172
+ if (ts) report.endTime = ts;
173
+ switch (type) {
174
+ case "user.message":
175
+ report.userMessages++;
176
+ break;
177
+ case "assistant.message":
178
+ report.assistantTurns++;
179
+ report.outputTokens += data.outputTokens ?? 0;
180
+ break;
181
+ case "tool.execution_start": {
182
+ const toolName = data.toolName;
183
+ if (toolName) {
184
+ report.toolUsage[toolName] = (report.toolUsage[toolName] ?? 0) + 1;
185
+ }
186
+ if (toolName === "bash") {
187
+ const args = data.arguments;
188
+ const cmd = args?.command ?? "";
189
+ if (cmd.includes("git") && cmd.includes("commit") && cmd.includes("-m")) {
190
+ const msgMatch = cmd.match(/-m\s+"([^"]{1,120})/);
191
+ if (msgMatch) report.gitCommits.push(msgMatch[1]);
192
+ }
193
+ }
194
+ if (toolName === "create") {
195
+ const args = data.arguments;
196
+ if (args?.path) report.filesCreated.push(args.path);
197
+ }
198
+ if (toolName === "edit") {
199
+ const args = data.arguments;
200
+ if (args?.path && !report.filesEdited.includes(args.path)) {
201
+ report.filesEdited.push(args.path);
202
+ }
203
+ }
204
+ break;
205
+ }
206
+ case "session.task_complete": {
207
+ const summary = data.summary;
208
+ report.taskCompletions.push(summary ?? "(task completed)");
209
+ report.complete = true;
210
+ break;
211
+ }
212
+ case "session.error": {
213
+ const msg = data.message;
214
+ if (msg) report.errors.push(msg);
215
+ break;
216
+ }
217
+ case "session.shutdown": {
218
+ const premium = data.totalPremiumRequests;
219
+ if (premium != null) report.premiumRequests = premium;
220
+ break;
221
+ }
222
+ }
223
+ }
224
+ if (report.startTime && report.endTime) {
225
+ report.durationMs = new Date(report.endTime).getTime() - new Date(report.startTime).getTime();
226
+ }
227
+ return report;
228
+ }
133
229
 
134
230
  // src/lib/process.ts
135
231
  import { execSync as execSync2, spawn } from "child_process";
@@ -315,7 +411,7 @@ async function runCopilot(args, options) {
315
411
  } else {
316
412
  await waitForCopilotInDir(dir);
317
413
  }
318
- return new Promise((resolve6) => {
414
+ return new Promise((resolve7) => {
319
415
  const child = spawn("copilot", args, {
320
416
  cwd: options?.cwd,
321
417
  stdio: "inherit",
@@ -325,14 +421,14 @@ async function runCopilot(args, options) {
325
421
  await sleep(3e3);
326
422
  const sid = getLatestSessionId();
327
423
  const premium = sid ? getSessionPremium(sid) : 0;
328
- resolve6({
424
+ resolve7({
329
425
  exitCode: code ?? 1,
330
426
  sessionId: sid,
331
427
  premium
332
428
  });
333
429
  });
334
430
  child.on("error", () => {
335
- resolve6({ exitCode: 1, sessionId: null, premium: 0 });
431
+ resolve7({ exitCode: 1, sessionId: null, premium: 0 });
336
432
  });
337
433
  });
338
434
  }
@@ -1079,13 +1175,708 @@ async function researchCommand(dir, opts) {
1079
1175
  notify("Research complete", projectName);
1080
1176
  }
1081
1177
 
1178
+ // src/commands/report.ts
1179
+ import { resolve as resolve6 } from "path";
1180
+ function registerReportCommand(program2) {
1181
+ program2.command("report [session-id]").description("Show what copilot did \u2014 timeline, tools, commits, files changed").option("-l, --limit <n>", "Number of recent sessions to report (when no ID given)", "1").option("--project <dir>", "Filter sessions by project directory").option("--json", "Output raw JSON").action((sessionId, opts) => {
1182
+ try {
1183
+ if (sessionId) {
1184
+ reportSingle(sessionId, opts.json ?? false);
1185
+ } else {
1186
+ reportRecent(parseInt(opts.limit, 10), opts.project, opts.json ?? false);
1187
+ }
1188
+ } catch (err) {
1189
+ fail(`Report error: ${err instanceof Error ? err.message : err}`);
1190
+ process.exit(1);
1191
+ }
1192
+ });
1193
+ }
1194
+ function reportRecent(limit, projectDir, json = false) {
1195
+ let sessions = listSessions(limit * 3);
1196
+ if (projectDir) {
1197
+ const target = resolve6(projectDir);
1198
+ sessions = sessions.filter((s) => s.cwd && resolve6(s.cwd) === target);
1199
+ }
1200
+ sessions = sessions.slice(0, limit);
1201
+ if (sessions.length === 0) {
1202
+ warn("No sessions found.");
1203
+ return;
1204
+ }
1205
+ for (const s of sessions) {
1206
+ reportSingle(s.id, json);
1207
+ }
1208
+ }
1209
+ function reportSingle(sid, json = false) {
1210
+ const report = getSessionReport(sid);
1211
+ if (!report) {
1212
+ warn(`Session ${sid} not found or invalid.`);
1213
+ return;
1214
+ }
1215
+ if (json) {
1216
+ log(JSON.stringify(report, null, 2));
1217
+ return;
1218
+ }
1219
+ renderReport(report);
1220
+ }
1221
+ function formatDuration(ms) {
1222
+ if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
1223
+ if (ms < 36e5) return `${Math.round(ms / 6e4)}m`;
1224
+ const h = Math.floor(ms / 36e5);
1225
+ const m = Math.round(ms % 36e5 / 6e4);
1226
+ return `${h}h ${m}m`;
1227
+ }
1228
+ function formatTime(iso) {
1229
+ if (!iso) return "\u2014";
1230
+ const d = new Date(iso);
1231
+ return d.toLocaleString("en-GB", {
1232
+ month: "short",
1233
+ day: "numeric",
1234
+ hour: "2-digit",
1235
+ minute: "2-digit"
1236
+ });
1237
+ }
1238
+ function bar(value, max, width = 20) {
1239
+ const filled = Math.round(value / Math.max(max, 1) * width);
1240
+ return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
1241
+ }
1242
+ function renderReport(r) {
1243
+ const status = r.complete ? `${GREEN}\u2714 Completed${RESET}` : `${YELLOW}\u23F8 Interrupted${RESET}`;
1244
+ const projectName = r.cwd.split("/").pop() ?? r.cwd;
1245
+ log("");
1246
+ log(`${BOLD}\u2554${"\u2550".repeat(62)}\u2557${RESET}`);
1247
+ log(`${BOLD}\u2551${RESET} \u{1F4CB} Session Report: ${CYAN}${r.id.slice(0, 8)}\u2026${RESET}${" ".repeat(62 - 28 - r.id.slice(0, 8).length)}${BOLD}\u2551${RESET}`);
1248
+ log(`${BOLD}\u255A${"\u2550".repeat(62)}\u255D${RESET}`);
1249
+ log("");
1250
+ log(`${BOLD} Overview${RESET}`);
1251
+ log(` ${"\u2500".repeat(58)}`);
1252
+ log(` Project: ${CYAN}${projectName}${RESET} ${DIM}${r.cwd}${RESET}`);
1253
+ log(` Status: ${status}`);
1254
+ log(` Duration: ${BOLD}${formatDuration(r.durationMs)}${RESET} ${DIM}(${formatTime(r.startTime)} \u2192 ${formatTime(r.endTime)})${RESET}`);
1255
+ log(` Summary: ${r.summary || DIM + "(none)" + RESET}`);
1256
+ log("");
1257
+ log(`${BOLD} Activity${RESET}`);
1258
+ log(` ${"\u2500".repeat(58)}`);
1259
+ log(` User messages: ${BOLD}${r.userMessages}${RESET}`);
1260
+ log(` Assistant turns: ${BOLD}${r.assistantTurns}${RESET}`);
1261
+ log(` Output tokens: ${BOLD}${r.outputTokens.toLocaleString()}${RESET}`);
1262
+ log(` Premium requests: ${BOLD}${r.premiumRequests}${RESET}`);
1263
+ log(` Tool calls: ${BOLD}${Object.values(r.toolUsage).reduce((a, b) => a + b, 0)}${RESET}`);
1264
+ const toolEntries = Object.entries(r.toolUsage).sort((a, b) => b[1] - a[1]);
1265
+ if (toolEntries.length > 0) {
1266
+ const maxCount = toolEntries[0][1];
1267
+ log("");
1268
+ log(`${BOLD} Tools Used${RESET}`);
1269
+ log(` ${"\u2500".repeat(58)}`);
1270
+ for (const [tool, count] of toolEntries.slice(0, 10)) {
1271
+ const barStr = bar(count, maxCount, 15);
1272
+ log(` ${DIM}${barStr}${RESET} ${String(count).padStart(5)} ${tool}`);
1273
+ }
1274
+ if (toolEntries.length > 10) {
1275
+ log(` ${DIM} \u2026 and ${toolEntries.length - 10} more tools${RESET}`);
1276
+ }
1277
+ }
1278
+ if (r.gitCommits.length > 0) {
1279
+ log("");
1280
+ log(`${BOLD} Git Commits (${r.gitCommits.length})${RESET}`);
1281
+ log(` ${"\u2500".repeat(58)}`);
1282
+ for (const msg of r.gitCommits) {
1283
+ log(` ${GREEN}\u25CF${RESET} ${msg.slice(0, 70)}${msg.length > 70 ? "\u2026" : ""}`);
1284
+ }
1285
+ }
1286
+ if (r.filesCreated.length > 0) {
1287
+ log("");
1288
+ log(`${BOLD} Files Created (${r.filesCreated.length})${RESET}`);
1289
+ log(` ${"\u2500".repeat(58)}`);
1290
+ for (const f of r.filesCreated.slice(0, 15)) {
1291
+ const short = f.includes(projectName) ? f.split(projectName + "/")[1] ?? f : f;
1292
+ log(` ${GREEN}+${RESET} ${short}`);
1293
+ }
1294
+ if (r.filesCreated.length > 15) {
1295
+ log(` ${DIM} \u2026 and ${r.filesCreated.length - 15} more${RESET}`);
1296
+ }
1297
+ }
1298
+ if (r.filesEdited.length > 0) {
1299
+ log("");
1300
+ log(`${BOLD} Files Edited (${r.filesEdited.length})${RESET}`);
1301
+ log(` ${"\u2500".repeat(58)}`);
1302
+ for (const f of r.filesEdited.slice(0, 15)) {
1303
+ const short = f.includes(projectName) ? f.split(projectName + "/")[1] ?? f : f;
1304
+ log(` ${YELLOW}~${RESET} ${short}`);
1305
+ }
1306
+ if (r.filesEdited.length > 15) {
1307
+ log(` ${DIM} \u2026 and ${r.filesEdited.length - 15} more${RESET}`);
1308
+ }
1309
+ }
1310
+ if (r.taskCompletions.length > 0) {
1311
+ log("");
1312
+ log(`${BOLD} Tasks Completed (${r.taskCompletions.length})${RESET}`);
1313
+ log(` ${"\u2500".repeat(58)}`);
1314
+ for (const t of r.taskCompletions) {
1315
+ const lines = t.split("\n");
1316
+ const first = lines[0].slice(0, 70);
1317
+ log(` ${GREEN}\u2714${RESET} ${first}${first.length < lines[0].length ? "\u2026" : ""}`);
1318
+ }
1319
+ }
1320
+ if (r.errors.length > 0) {
1321
+ log("");
1322
+ log(`${BOLD} Errors (${r.errors.length})${RESET}`);
1323
+ log(` ${"\u2500".repeat(58)}`);
1324
+ for (const e of r.errors.slice(0, 5)) {
1325
+ log(` ${RED}\u2716${RESET} ${e.slice(0, 70)}`);
1326
+ }
1327
+ if (r.errors.length > 5) {
1328
+ log(` ${DIM} \u2026 and ${r.errors.length - 5} more${RESET}`);
1329
+ }
1330
+ }
1331
+ log("");
1332
+ }
1333
+
1334
+ // src/commands/dashboard.ts
1335
+ var ESC = "\x1B";
1336
+ var CLEAR = `${ESC}[2J${ESC}[H`;
1337
+ var HIDE_CURSOR = `${ESC}[?25l`;
1338
+ var SHOW_CURSOR = `${ESC}[?25h`;
1339
+ var SAVE_CURSOR = `${ESC}7`;
1340
+ var RESTORE_CURSOR = `${ESC}8`;
1341
+ function registerDashboardCommand(program2) {
1342
+ program2.command("dashboard").alias("tui").description("Real-time terminal dashboard for copilot sessions").option("-r, --refresh <n>", "Refresh interval in seconds", "5").option("-l, --limit <n>", "Number of sessions to show", "8").action((opts) => {
1343
+ runDashboard(parseInt(opts.refresh, 10), parseInt(opts.limit, 10));
1344
+ });
1345
+ }
1346
+ function runDashboard(refreshSec, limit) {
1347
+ process.stdout.write(HIDE_CURSOR);
1348
+ const cleanup = () => {
1349
+ process.stdout.write(SHOW_CURSOR);
1350
+ process.stdout.write(CLEAR);
1351
+ process.exit(0);
1352
+ };
1353
+ process.on("SIGINT", cleanup);
1354
+ process.on("SIGTERM", cleanup);
1355
+ const render = () => {
1356
+ try {
1357
+ const output = buildScreen(limit);
1358
+ process.stdout.write(CLEAR + output);
1359
+ } catch {
1360
+ }
1361
+ };
1362
+ render();
1363
+ const timer = setInterval(render, refreshSec * 1e3);
1364
+ process.stdout.on("resize", render);
1365
+ process.stdin.setRawMode?.(true);
1366
+ process.stdin.resume();
1367
+ process.stdin.on("data", (data) => {
1368
+ const key = data.toString();
1369
+ if (key === "q" || key === "") {
1370
+ clearInterval(timer);
1371
+ cleanup();
1372
+ }
1373
+ if (key === "r") render();
1374
+ });
1375
+ }
1376
+ function buildScreen(limit) {
1377
+ const cols = process.stdout.columns || 80;
1378
+ const rows = process.stdout.rows || 40;
1379
+ const lines = [];
1380
+ const now = /* @__PURE__ */ new Date();
1381
+ const timeStr = now.toLocaleTimeString("en-GB");
1382
+ lines.push("");
1383
+ lines.push(` ${BOLD}${CYAN}\u250C${"\u2500".repeat(cols - 6)}\u2510${RESET}`);
1384
+ lines.push(` ${BOLD}${CYAN}\u2502${RESET} \u{1F916} ${BOLD}Copilot Agent Dashboard${RESET}${" ".repeat(Math.max(0, cols - 37 - timeStr.length))}${DIM}${timeStr}${RESET} ${BOLD}${CYAN}\u2502${RESET}`);
1385
+ lines.push(` ${BOLD}${CYAN}\u2514${"\u2500".repeat(cols - 6)}\u2518${RESET}`);
1386
+ lines.push("");
1387
+ const procs = findCopilotProcesses();
1388
+ lines.push(` ${BOLD}${GREEN}\u25CF Active Processes (${procs.length})${RESET}`);
1389
+ lines.push(` ${"\u2500".repeat(Math.min(cols - 4, 70))}`);
1390
+ if (procs.length === 0) {
1391
+ lines.push(` ${DIM}No copilot processes running${RESET}`);
1392
+ } else {
1393
+ for (const p of procs) {
1394
+ const sid = p.sessionId ? p.sessionId.slice(0, 8) + "\u2026" : "\u2014";
1395
+ const cwdShort = p.cwd ? "~/" + p.cwd.split("/").slice(-2).join("/") : "\u2014";
1396
+ lines.push(` ${GREEN}\u2B24${RESET} PID ${BOLD}${p.pid}${RESET} ${CYAN}${sid}${RESET} ${DIM}${cwdShort}${RESET}`);
1397
+ }
1398
+ }
1399
+ lines.push("");
1400
+ const sessions = listSessions(limit);
1401
+ lines.push(` ${BOLD}${CYAN}\u25CF Recent Sessions (${sessions.length})${RESET}`);
1402
+ lines.push(` ${"\u2500".repeat(Math.min(cols - 4, 70))}`);
1403
+ const headerCols = [
1404
+ pad("Status", 10),
1405
+ pad("Premium", 8),
1406
+ pad("Duration", 10),
1407
+ pad("Project", 18),
1408
+ pad("Last Activity", 20)
1409
+ ];
1410
+ lines.push(` ${DIM}${headerCols.join(" ")}${RESET}`);
1411
+ for (const s of sessions) {
1412
+ const report = getSessionReport(s.id);
1413
+ const statusIcon = s.complete ? `${GREEN}\u2714 done ${RESET}` : `${YELLOW}\u23F8 stop ${RESET}`;
1414
+ const premium = pad(String(s.premiumRequests), 8);
1415
+ const duration = report ? pad(formatDuration2(report.durationMs), 10) : pad("\u2014", 10);
1416
+ const project = pad(s.cwd.split("/").pop() ?? "\u2014", 18);
1417
+ const lastAct = pad(formatTimeAgo(s.mtime), 20);
1418
+ lines.push(` ${statusIcon}${premium}${duration}${project}${lastAct}`);
1419
+ }
1420
+ lines.push("");
1421
+ if (sessions.length > 0) {
1422
+ const latest = getSessionReport(sessions[0].id);
1423
+ if (latest) {
1424
+ lines.push(` ${BOLD}${CYAN}\u25CF Latest Session Detail${RESET} ${DIM}${latest.id.slice(0, 8)}\u2026${RESET}`);
1425
+ lines.push(` ${"\u2500".repeat(Math.min(cols - 4, 70))}`);
1426
+ lines.push(` Turns: ${BOLD}${latest.assistantTurns}${RESET} Tokens: ${BOLD}${latest.outputTokens.toLocaleString()}${RESET} Premium: ${BOLD}${latest.premiumRequests}${RESET} Commits: ${BOLD}${latest.gitCommits.length}${RESET} Files: ${BOLD}${latest.filesEdited.length}${RESET} edited, ${BOLD}${latest.filesCreated.length}${RESET} created`);
1427
+ lines.push("");
1428
+ const toolEntries = Object.entries(latest.toolUsage).sort((a, b) => b[1] - a[1]).slice(0, 5);
1429
+ if (toolEntries.length > 0) {
1430
+ const maxVal = toolEntries[0][1];
1431
+ const barW = Math.min(20, Math.floor((cols - 30) / 2));
1432
+ lines.push(` ${DIM}Tools:${RESET}`);
1433
+ for (const [tool, count] of toolEntries) {
1434
+ const filled = Math.round(count / maxVal * barW);
1435
+ const b = `${CYAN}${"\u2588".repeat(filled)}${DIM}${"\u2591".repeat(barW - filled)}${RESET}`;
1436
+ lines.push(` ${b} ${String(count).padStart(4)} ${tool}`);
1437
+ }
1438
+ lines.push("");
1439
+ }
1440
+ if (latest.gitCommits.length > 0) {
1441
+ const maxCommits = Math.min(3, latest.gitCommits.length);
1442
+ lines.push(` ${DIM}Recent commits:${RESET}`);
1443
+ for (let i = 0; i < maxCommits; i++) {
1444
+ const msg = latest.gitCommits[i].split("\n")[0].slice(0, cols - 10);
1445
+ lines.push(` ${GREEN}\u25CF${RESET} ${msg}`);
1446
+ }
1447
+ if (latest.gitCommits.length > maxCommits) {
1448
+ lines.push(` ${DIM} \u2026 +${latest.gitCommits.length - maxCommits} more${RESET}`);
1449
+ }
1450
+ lines.push("");
1451
+ }
1452
+ if (latest.taskCompletions.length > 0) {
1453
+ const maxTasks = Math.min(3, latest.taskCompletions.length);
1454
+ lines.push(` ${DIM}Completed tasks:${RESET}`);
1455
+ for (let i = 0; i < maxTasks; i++) {
1456
+ const msg = latest.taskCompletions[i].split("\n")[0].slice(0, cols - 10);
1457
+ lines.push(` ${GREEN}\u2714${RESET} ${msg}`);
1458
+ }
1459
+ if (latest.taskCompletions.length > maxTasks) {
1460
+ lines.push(` ${DIM} \u2026 +${latest.taskCompletions.length - maxTasks} more${RESET}`);
1461
+ }
1462
+ }
1463
+ }
1464
+ }
1465
+ lines.push("");
1466
+ lines.push(` ${DIM}Press ${BOLD}q${RESET}${DIM} to quit, ${BOLD}r${RESET}${DIM} to refresh${RESET}`);
1467
+ return lines.join("\n");
1468
+ }
1469
+ function pad(s, n) {
1470
+ if (s.length >= n) return s.slice(0, n);
1471
+ return s + " ".repeat(n - s.length);
1472
+ }
1473
+ function formatDuration2(ms) {
1474
+ if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
1475
+ if (ms < 36e5) return `${Math.round(ms / 6e4)}m`;
1476
+ const h = Math.floor(ms / 36e5);
1477
+ const m = Math.round(ms % 36e5 / 6e4);
1478
+ return `${h}h ${m}m`;
1479
+ }
1480
+ function formatTimeAgo(mtimeMs) {
1481
+ const diff = Date.now() - mtimeMs;
1482
+ if (diff < 6e4) return "just now";
1483
+ if (diff < 36e5) return `${Math.round(diff / 6e4)}m ago`;
1484
+ if (diff < 864e5) return `${Math.round(diff / 36e5)}h ago`;
1485
+ return `${Math.round(diff / 864e5)}d ago`;
1486
+ }
1487
+
1488
+ // src/commands/web.ts
1489
+ import { Hono } from "hono";
1490
+ import { streamSSE } from "hono/streaming";
1491
+ import { serve } from "@hono/node-server";
1492
+ import { spawn as spawn2 } from "child_process";
1493
+
1494
+ // src/web/layout.ts
1495
+ var cssStyles = `
1496
+ *{margin:0;padding:0;box-sizing:border-box}
1497
+ :root{
1498
+ --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--bg4:#292e36;
1499
+ --border:#30363d;--border2:#3d444d;
1500
+ --text:#e6edf3;--text2:#8b949e;--text3:#484f58;
1501
+ --cyan:#58a6ff;--green:#3fb950;--yellow:#d29922;--red:#f85149;--purple:#bc8cff;--orange:#f0883e;
1502
+ --font-sans:'Inter',system-ui,sans-serif;
1503
+ --font-mono:'JetBrains Mono','SF Mono',monospace;
1504
+ --radius:8px;
1505
+ }
1506
+ body{background:var(--bg);color:var(--text);font-family:var(--font-sans);font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased}
1507
+ .header{background:var(--bg2);border-bottom:1px solid var(--border);padding:12px 24px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:10;backdrop-filter:blur(12px)}
1508
+ .header-left{display:flex;align-items:center;gap:10px}
1509
+ .header-left h1{font-size:16px;font-weight:600}
1510
+ .header-left h1 span{color:var(--cyan)}
1511
+ .live-badge{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--green);background:rgba(63,185,80,.1);padding:3px 10px;border-radius:12px;font-weight:500}
1512
+ .live-dot{width:6px;height:6px;border-radius:50%;background:var(--green);animation:pulse 2s infinite}
1513
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
1514
+ .clock{font-family:var(--font-mono);font-size:13px;color:var(--text2)}
1515
+ .container{max-width:1280px;margin:0 auto;padding:16px 20px}
1516
+ .stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:10px;margin-bottom:16px}
1517
+ .stat{background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);padding:14px 16px;transition:border-color .2s}
1518
+ .stat:hover{border-color:var(--border2)}
1519
+ .stat-label{font-size:11px;font-weight:500;color:var(--text2);text-transform:uppercase;letter-spacing:.4px}
1520
+ .stat-value{font-size:22px;font-weight:700;font-family:var(--font-mono);margin-top:2px}
1521
+ .stat-value.green{color:var(--green)}.stat-value.cyan{color:var(--cyan)}
1522
+ .stat-value.yellow{color:var(--yellow)}.stat-value.purple{color:var(--purple)}
1523
+ .procs{background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);margin-bottom:16px;overflow:hidden}
1524
+ .procs-header{padding:10px 16px;font-size:13px;font-weight:600;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px}
1525
+ .procs-body{padding:4px 0}
1526
+ .proc{padding:6px 16px;display:flex;align-items:center;gap:10px;font-size:13px}
1527
+ .proc-dot{width:6px;height:6px;border-radius:50%;background:var(--green);flex-shrink:0}
1528
+ .proc-pid{font-family:var(--font-mono);color:var(--cyan);font-weight:500;min-width:70px}
1529
+ .proc-sid{color:var(--text2);font-family:var(--font-mono);font-size:12px}
1530
+ .proc-cwd{color:var(--text3);font-size:12px}
1531
+ .empty{padding:14px 16px;color:var(--text3);font-size:13px}
1532
+ .main{display:grid;grid-template-columns:360px 1fr;gap:0;background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;min-height:calc(100vh - 260px)}
1533
+ .sidebar{border-right:1px solid var(--border);overflow-y:auto}
1534
+ .sidebar-header{padding:10px 16px;font-size:13px;font-weight:600;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;position:sticky;top:0;background:var(--bg2);z-index:2}
1535
+ .count{background:var(--bg3);color:var(--text2);font-size:11px;padding:1px 7px;border-radius:10px;font-weight:500}
1536
+ .s-item{padding:10px 16px;border-bottom:1px solid var(--border);cursor:pointer;transition:background .15s}
1537
+ .s-item:last-child{border-bottom:none}
1538
+ .s-item:hover{background:var(--bg3)}
1539
+ .s-item.active{background:rgba(88,166,255,.06);border-left:3px solid var(--cyan);padding-left:13px}
1540
+ .s-row{display:flex;align-items:center;gap:10px}
1541
+ .s-icon{font-size:14px;flex-shrink:0}
1542
+ .s-info{flex:1;min-width:0}
1543
+ .s-title{font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
1544
+ .s-meta{font-size:11px;color:var(--text2);display:flex;gap:10px;margin-top:2px;flex-wrap:wrap}
1545
+ .badge{font-size:10px;padding:1px 6px;border-radius:4px;font-weight:600;font-family:var(--font-mono);text-transform:uppercase}
1546
+ .badge-done{background:rgba(63,185,80,.12);color:var(--green)}
1547
+ .badge-stop{background:rgba(210,153,34,.12);color:var(--yellow)}
1548
+ .detail{padding:20px;overflow-y:auto}
1549
+ .detail-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}
1550
+ .detail-title{font-size:18px;font-weight:700}
1551
+ .detail-id{font-family:var(--font-mono);font-size:11px;color:var(--text3);background:var(--bg);padding:3px 8px;border-radius:4px}
1552
+ .detail-time{font-size:12px;color:var(--text2);margin-bottom:16px}
1553
+ .detail-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:8px;margin-bottom:20px}
1554
+ .d-stat{background:var(--bg);border-radius:6px;padding:10px}
1555
+ .d-stat-label{font-size:10px;color:var(--text2);text-transform:uppercase;letter-spacing:.3px}
1556
+ .d-stat-val{font-size:16px;font-weight:700;font-family:var(--font-mono);margin-top:1px}
1557
+ .sub{margin-top:20px}
1558
+ .sub-title{font-size:12px;font-weight:600;color:var(--text2);text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px;display:flex;align-items:center;gap:6px}
1559
+ .tool-row{display:flex;align-items:center;gap:8px;padding:3px 0;font-size:12px}
1560
+ .tool-name{color:var(--text2);min-width:110px;text-align:right;font-family:var(--font-mono);font-size:11px}
1561
+ .tool-bar-bg{flex:1;background:var(--bg);border-radius:3px;height:14px;overflow:hidden}
1562
+ .tool-bar{height:100%;border-radius:3px;background:linear-gradient(90deg,var(--cyan),var(--purple));opacity:.7;transition:width .4s ease}
1563
+ .tool-count{font-family:var(--font-mono);color:var(--text2);min-width:40px;font-size:11px}
1564
+ .commit-list,.task-list,.file-list,.error-list{list-style:none}
1565
+ .commit-list li,.task-list li{padding:5px 0;font-size:13px;display:flex;align-items:flex-start;gap:6px;border-bottom:1px solid var(--border)}
1566
+ .commit-list li:last-child,.task-list li:last-child{border-bottom:none}
1567
+ .c-dot{color:var(--green);flex-shrink:0;margin-top:2px}
1568
+ .t-check{color:var(--green);flex-shrink:0;margin-top:2px}
1569
+ .file-list li{padding:2px 0;font-size:11px;font-family:var(--font-mono)}
1570
+ .file-created{color:var(--green)}
1571
+ .file-edited{color:var(--yellow)}
1572
+ .error-list li{padding:5px 0;font-size:12px;color:var(--red)}
1573
+ .more{font-size:11px;color:var(--text3);padding:4px 0}
1574
+ .empty-detail{display:flex;align-items:center;justify-content:center;height:100%;color:var(--text3)}
1575
+ .htmx-settling{opacity:0}
1576
+ .htmx-added{opacity:0;transition:opacity .3s ease}
1577
+ @media(max-width:768px){.main{grid-template-columns:1fr}.stats{grid-template-columns:repeat(2,1fr)}}
1578
+ `;
1579
+ var layoutHead = `<!DOCTYPE html>
1580
+ <html lang="en">
1581
+ <head>
1582
+ <meta charset="utf-8">
1583
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1584
+ <title>Copilot Agent Dashboard</title>
1585
+ <script src="https://unpkg.com/htmx.org@2.0.4" integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+" crossorigin="anonymous"></script>
1586
+ <script src="https://unpkg.com/htmx-ext-sse@2.2.2/sse.js"></script>
1587
+ <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
1588
+ <link rel="preconnect" href="https://fonts.googleapis.com">
1589
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
1590
+ <style>
1591
+ ${cssStyles}
1592
+ </style>
1593
+ </head>`;
1594
+ var layoutFoot = `</html>`;
1595
+
1596
+ // src/web/views.ts
1597
+ function esc(s) {
1598
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1599
+ }
1600
+ function fmt(n) {
1601
+ return n.toLocaleString();
1602
+ }
1603
+ function fmtDur(ms) {
1604
+ if (!ms) return "\u2014";
1605
+ if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
1606
+ if (ms < 36e5) return `${Math.round(ms / 6e4)}m`;
1607
+ const h = Math.floor(ms / 36e5);
1608
+ const m = Math.round(ms % 36e5 / 6e4);
1609
+ return `${h}h ${m}m`;
1610
+ }
1611
+ function fmtTime(iso) {
1612
+ if (!iso) return "\u2014";
1613
+ return new Date(iso).toLocaleString("en-GB", { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
1614
+ }
1615
+ function fmtAgo(iso) {
1616
+ if (!iso) return "\u2014";
1617
+ const diff = Date.now() - new Date(iso).getTime();
1618
+ if (diff < 6e4) return "just now";
1619
+ if (diff < 36e5) return `${Math.round(diff / 6e4)}m ago`;
1620
+ if (diff < 864e5) return `${Math.round(diff / 36e5)}h ago`;
1621
+ return `${Math.round(diff / 864e5)}d ago`;
1622
+ }
1623
+ function shortPath(p, proj) {
1624
+ if (proj && p.includes(proj + "/")) return p.split(proj + "/").pop() ?? p;
1625
+ return p.split("/").slice(-3).join("/");
1626
+ }
1627
+ function renderStats(sessions) {
1628
+ const totalPremium = sessions.reduce((a, s) => a + (s.premiumRequests ?? 0), 0);
1629
+ const totalTokens = sessions.reduce((a, s) => a + (s.outputTokens ?? 0), 0);
1630
+ const totalCommits = sessions.reduce((a, s) => a + (s.gitCommits?.length ?? 0), 0);
1631
+ const totalTasks = sessions.reduce((a, s) => a + (s.taskCompletions?.length ?? 0), 0);
1632
+ const completed = sessions.filter((s) => s.complete).length;
1633
+ const items = [
1634
+ { label: "Sessions", value: String(sessions.length), cls: "cyan" },
1635
+ { label: "Completed", value: `${completed}/${sessions.length}`, cls: "green" },
1636
+ { label: "Premium", value: fmt(totalPremium), cls: "yellow" },
1637
+ { label: "Tokens", value: fmt(totalTokens), cls: "purple" },
1638
+ { label: "Commits", value: String(totalCommits), cls: "green" },
1639
+ { label: "Tasks Done", value: String(totalTasks), cls: "cyan" }
1640
+ ];
1641
+ return items.map(
1642
+ (i) => `<div class="stat"><div class="stat-label">${i.label}</div><div class="stat-value ${i.cls}">${i.value}</div></div>`
1643
+ ).join("");
1644
+ }
1645
+ function renderProcesses(procs) {
1646
+ if (procs.length === 0) return '<div class="empty">No active copilot processes</div>';
1647
+ return procs.map((p) => {
1648
+ const sid = p.sessionId ? p.sessionId.slice(0, 8) + "\u2026" : "\u2014";
1649
+ return `<div class="proc">
1650
+ <div class="proc-dot"></div>
1651
+ <span class="proc-pid">PID ${p.pid}</span>
1652
+ <span class="proc-sid">${esc(sid)}</span>
1653
+ <span class="proc-cwd">${esc(p.cwd ?? "")}</span>
1654
+ </div>`;
1655
+ }).join("");
1656
+ }
1657
+ function renderSessionList(sessions, selectedId) {
1658
+ return sessions.map((s) => {
1659
+ const proj = (s.cwd ?? "").split("/").pop() ?? "\u2014";
1660
+ const isActive = s.id === selectedId;
1661
+ return `<div class="s-item${isActive ? " active" : ""}"
1662
+ hx-get="/partial/detail/${s.id}" hx-target="#detail" hx-swap="innerHTML"
1663
+ onclick="document.querySelectorAll('.s-item').forEach(e=>e.classList.remove('active'));this.classList.add('active')">
1664
+ <div class="s-row">
1665
+ <span class="s-icon">${s.complete ? "\u2705" : "\u23F8\uFE0F"}</span>
1666
+ <div class="s-info">
1667
+ <div class="s-title">${esc(proj)} \u2014 ${esc(s.summary || "(no summary)")}</div>
1668
+ <div class="s-meta">
1669
+ <span>${fmtDur(s.durationMs)}</span>
1670
+ <span>${fmt(s.premiumRequests)} premium</span>
1671
+ <span>${fmtAgo(s.endTime)}</span>
1672
+ <span class="badge ${s.complete ? "badge-done" : "badge-stop"}">${s.complete ? "done" : "stopped"}</span>
1673
+ </div>
1674
+ </div>
1675
+ </div>
1676
+ </div>`;
1677
+ }).join("");
1678
+ }
1679
+ function renderDetail(s) {
1680
+ const proj = (s.cwd ?? "").split("/").pop() ?? "\u2014";
1681
+ const totalTools = Object.values(s.toolUsage ?? {}).reduce((a, b) => a + b, 0);
1682
+ const toolEntries = Object.entries(s.toolUsage ?? {}).sort((a, b) => b[1] - a[1]);
1683
+ const maxTool = toolEntries[0]?.[1] ?? 1;
1684
+ let html = "";
1685
+ html += `<div class="detail-head">
1686
+ <div class="detail-title">${esc(proj)}</div>
1687
+ <div class="detail-id">${s.id}</div>
1688
+ </div>`;
1689
+ html += `<div class="detail-time">${fmtTime(s.startTime)} \u2192 ${fmtTime(s.endTime)}</div>`;
1690
+ html += `<div class="detail-stats">
1691
+ <div class="d-stat"><div class="d-stat-label">Duration</div><div class="d-stat-val">${fmtDur(s.durationMs)}</div></div>
1692
+ <div class="d-stat"><div class="d-stat-label">User Msgs</div><div class="d-stat-val">${s.userMessages}</div></div>
1693
+ <div class="d-stat"><div class="d-stat-label">Turns</div><div class="d-stat-val">${fmt(s.assistantTurns)}</div></div>
1694
+ <div class="d-stat"><div class="d-stat-label">Tokens</div><div class="d-stat-val">${fmt(s.outputTokens)}</div></div>
1695
+ <div class="d-stat"><div class="d-stat-label">Premium</div><div class="d-stat-val">${fmt(s.premiumRequests)}</div></div>
1696
+ <div class="d-stat"><div class="d-stat-label">Tool Calls</div><div class="d-stat-val">${fmt(totalTools)}</div></div>
1697
+ </div>`;
1698
+ if (toolEntries.length > 0) {
1699
+ html += `<div class="sub"><div class="sub-title">\u{1F527} Tools Used</div>`;
1700
+ for (const [tool, count] of toolEntries.slice(0, 12)) {
1701
+ const pct = Math.round(count / maxTool * 100);
1702
+ html += `<div class="tool-row">
1703
+ <span class="tool-name">${esc(tool)}</span>
1704
+ <div class="tool-bar-bg"><div class="tool-bar" style="width:${pct}%"></div></div>
1705
+ <span class="tool-count">${count}</span>
1706
+ </div>`;
1707
+ }
1708
+ if (toolEntries.length > 12) html += `<div class="more">\u2026 +${toolEntries.length - 12} more</div>`;
1709
+ html += `</div>`;
1710
+ }
1711
+ if (s.gitCommits.length > 0) {
1712
+ html += `<div class="sub"><div class="sub-title">\u{1F500} Git Commits <span class="count">${s.gitCommits.length}</span></div><ul class="commit-list">`;
1713
+ for (const msg of s.gitCommits.slice(0, 12)) {
1714
+ const first = msg.split("\n")[0].slice(0, 80);
1715
+ html += `<li><span class="c-dot">\u25CF</span><span>${esc(first)}</span></li>`;
1716
+ }
1717
+ if (s.gitCommits.length > 12) html += `<li class="more">\u2026 +${s.gitCommits.length - 12} more</li>`;
1718
+ html += `</ul></div>`;
1719
+ }
1720
+ const files = [
1721
+ ...s.filesCreated.map((f) => ({ path: f, type: "created" })),
1722
+ ...s.filesEdited.map((f) => ({ path: f, type: "edited" }))
1723
+ ];
1724
+ if (files.length > 0) {
1725
+ html += `<div class="sub"><div class="sub-title">\u{1F4C1} Files Changed <span class="count">${files.length}</span></div><ul class="file-list">`;
1726
+ for (const f of files.slice(0, 25)) {
1727
+ const cls = f.type === "created" ? "file-created" : "file-edited";
1728
+ const icon = f.type === "created" ? "+" : "~";
1729
+ html += `<li><span class="${cls}">${icon}</span> ${esc(shortPath(f.path, proj))}</li>`;
1730
+ }
1731
+ if (files.length > 25) html += `<li class="more">\u2026 +${files.length - 25} more</li>`;
1732
+ html += `</ul></div>`;
1733
+ }
1734
+ if (s.taskCompletions.length > 0) {
1735
+ html += `<div class="sub"><div class="sub-title">\u2705 Tasks Completed <span class="count">${s.taskCompletions.length}</span></div><ul class="task-list">`;
1736
+ for (const t of s.taskCompletions.slice(0, 10)) {
1737
+ const first = t.split("\n")[0].slice(0, 80);
1738
+ html += `<li><span class="t-check">\u2714</span><span>${esc(first)}</span></li>`;
1739
+ }
1740
+ if (s.taskCompletions.length > 10) html += `<li class="more">\u2026 +${s.taskCompletions.length - 10} more</li>`;
1741
+ html += `</ul></div>`;
1742
+ }
1743
+ if (s.errors.length > 0) {
1744
+ html += `<div class="sub"><div class="sub-title" style="color:var(--red)">\u26A0\uFE0F Errors <span class="count">${s.errors.length}</span></div><ul class="error-list">`;
1745
+ for (const e of s.errors.slice(0, 5)) {
1746
+ html += `<li>${esc(e.slice(0, 100))}</li>`;
1747
+ }
1748
+ if (s.errors.length > 5) html += `<li class="more">\u2026 +${s.errors.length - 5} more</li>`;
1749
+ html += `</ul></div>`;
1750
+ }
1751
+ return html;
1752
+ }
1753
+
1754
+ // src/commands/web.ts
1755
+ function registerWebCommand(program2) {
1756
+ program2.command("web").description("Launch web dashboard in browser").option("-p, --port <n>", "Port number", "3847").option("--no-open", "Do not auto-open browser").action((opts) => {
1757
+ startWebServer(parseInt(opts.port, 10), opts.open !== false);
1758
+ });
1759
+ }
1760
+ function getData() {
1761
+ const sessions = listSessions(20);
1762
+ const reports = sessions.map((s) => getSessionReport(s.id)).filter((r) => r !== null);
1763
+ const processes = findCopilotProcesses();
1764
+ return { sessions: reports, processes };
1765
+ }
1766
+ function startWebServer(port, autoOpen) {
1767
+ const app = new Hono();
1768
+ app.get("/api/sessions", (c) => c.json(getData()));
1769
+ app.get("/api/session/:id", (c) => {
1770
+ const report = getSessionReport(c.req.param("id"));
1771
+ if (!report) return c.json({ error: "Not found" }, 404);
1772
+ return c.json(report);
1773
+ });
1774
+ app.get("/events", (c) => {
1775
+ return streamSSE(c, async (stream) => {
1776
+ while (true) {
1777
+ const { sessions, processes } = getData();
1778
+ await stream.writeSSE({
1779
+ event: "stats",
1780
+ data: renderStats(sessions)
1781
+ });
1782
+ await stream.writeSSE({
1783
+ event: "procs",
1784
+ data: renderProcesses(processes)
1785
+ });
1786
+ await stream.writeSSE({
1787
+ event: "proc-count",
1788
+ data: String(processes.length)
1789
+ });
1790
+ await stream.sleep(5e3);
1791
+ }
1792
+ });
1793
+ });
1794
+ app.get("/partial/detail/:id", (c) => {
1795
+ const report = getSessionReport(c.req.param("id"));
1796
+ if (!report) return c.html('<div class="empty-detail">Session not found</div>');
1797
+ return c.html(renderDetail(report));
1798
+ });
1799
+ app.get("/", (c) => {
1800
+ const { sessions, processes } = getData();
1801
+ const firstId = sessions[0]?.id;
1802
+ return c.html(`${layoutHead}
1803
+ <body>
1804
+ <div class="header">
1805
+ <div class="header-left">
1806
+ <h1>\u{1F916} <span>Copilot Agent</span></h1>
1807
+ <div class="live-badge"><div class="live-dot"></div> Live</div>
1808
+ </div>
1809
+ <div class="clock" id="clock"></div>
1810
+ </div>
1811
+
1812
+ <div class="container"
1813
+ hx-ext="sse"
1814
+ sse-connect="/events">
1815
+
1816
+ <div class="stats"
1817
+ sse-swap="stats"
1818
+ hx-swap="innerHTML">
1819
+ ${renderStats(sessions)}
1820
+ </div>
1821
+
1822
+ <div class="procs">
1823
+ <div class="procs-header">
1824
+ \u2B24 Active Processes <span class="count" sse-swap="proc-count" hx-swap="innerHTML">${processes.length}</span>
1825
+ </div>
1826
+ <div class="procs-body"
1827
+ sse-swap="procs"
1828
+ hx-swap="innerHTML">
1829
+ ${renderProcesses(processes)}
1830
+ </div>
1831
+ </div>
1832
+
1833
+ <div class="main">
1834
+ <div class="sidebar">
1835
+ <div class="sidebar-header">
1836
+ \u{1F4CB} Sessions <span class="count">${sessions.length}</span>
1837
+ </div>
1838
+ ${renderSessionList(sessions, firstId)}
1839
+ </div>
1840
+ <div class="detail" id="detail">
1841
+ ${firstId ? renderDetail(sessions[0]) : '<div class="empty-detail">No sessions</div>'}
1842
+ </div>
1843
+ </div>
1844
+ </div>
1845
+
1846
+ <script>
1847
+ setInterval(() => {
1848
+ document.getElementById('clock').textContent = new Date().toLocaleTimeString('en-GB');
1849
+ }, 1000);
1850
+ document.getElementById('clock').textContent = new Date().toLocaleTimeString('en-GB');
1851
+ </script>
1852
+ </body>
1853
+ ${layoutFoot}`);
1854
+ });
1855
+ try {
1856
+ serve({ fetch: app.fetch, port }, () => {
1857
+ const url = `http://localhost:${port}`;
1858
+ ok(`Web dashboard \u2192 ${url}`);
1859
+ info("Press Ctrl+C to stop");
1860
+ if (autoOpen) {
1861
+ spawn2("open", [url], { detached: true, stdio: "ignore" }).unref();
1862
+ }
1863
+ });
1864
+ } catch (err) {
1865
+ fail(`Server error: ${err instanceof Error ? err.message : err}`);
1866
+ process.exit(1);
1867
+ }
1868
+ }
1869
+
1082
1870
  // src/index.ts
1083
1871
  var program = new Command();
1084
- program.name("copilot-agent").version("0.5.0").description("Autonomous GitHub Copilot CLI agent \u2014 auto-resume, task discovery, overnight runs");
1872
+ program.name("copilot-agent").version("0.7.0").description("Autonomous GitHub Copilot CLI agent \u2014 auto-resume, task discovery, overnight runs");
1085
1873
  registerStatusCommand(program);
1086
1874
  registerWatchCommand(program);
1087
1875
  registerRunCommand(program);
1088
1876
  registerOvernightCommand(program);
1089
1877
  registerResearchCommand(program);
1878
+ registerReportCommand(program);
1879
+ registerDashboardCommand(program);
1880
+ registerWebCommand(program);
1090
1881
  program.parse();
1091
1882
  //# sourceMappingURL=index.js.map