@dreb/coding-agent 2.67.0 → 2.68.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +1 -1
  2. package/dist/core/agent-session.d.ts +1 -0
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js.map +1 -1
  5. package/dist/core/daily-cost-tracker.d.ts +58 -7
  6. package/dist/core/daily-cost-tracker.d.ts.map +1 -1
  7. package/dist/core/daily-cost-tracker.js +146 -88
  8. package/dist/core/daily-cost-tracker.js.map +1 -1
  9. package/dist/core/footer-data-provider.d.ts +11 -3
  10. package/dist/core/footer-data-provider.d.ts.map +1 -1
  11. package/dist/core/footer-data-provider.js +19 -3
  12. package/dist/core/footer-data-provider.js.map +1 -1
  13. package/dist/core/session-manager.d.ts +14 -0
  14. package/dist/core/session-manager.d.ts.map +1 -1
  15. package/dist/core/session-manager.js +182 -88
  16. package/dist/core/session-manager.js.map +1 -1
  17. package/dist/core/subagent-session-cost-tracker.d.ts +22 -0
  18. package/dist/core/subagent-session-cost-tracker.d.ts.map +1 -0
  19. package/dist/core/subagent-session-cost-tracker.js +94 -0
  20. package/dist/core/subagent-session-cost-tracker.js.map +1 -0
  21. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  22. package/dist/modes/interactive/components/footer.js +8 -2
  23. package/dist/modes/interactive/components/footer.js.map +1 -1
  24. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  25. package/dist/modes/interactive/interactive-mode.js +7 -2
  26. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  27. package/dist/modes/rpc/rpc-client.d.ts +7 -2
  28. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  29. package/dist/modes/rpc/rpc-client.js +3 -2
  30. package/dist/modes/rpc/rpc-client.js.map +1 -1
  31. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  32. package/dist/modes/rpc/rpc-mode.js +29 -6
  33. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  34. package/dist/modes/rpc/rpc-types.d.ts +2 -0
  35. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  36. package/dist/modes/rpc/rpc-types.js.map +1 -1
  37. package/docs/dashboard.md +21 -15
  38. package/docs/rpc.md +8 -2
  39. package/docs/settings.md +1 -1
  40. package/package.json +1 -1
@@ -2,7 +2,7 @@ import { spawnSync } from "node:child_process";
2
2
  import { readdir, readFile, stat, unlink } from "node:fs/promises";
3
3
  import { randomUUID } from "crypto";
4
4
  import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, readSync, statSync, writeFileSync, } from "fs";
5
- import { join, resolve } from "path";
5
+ import { dirname, join, resolve, sep } from "path";
6
6
  import { getAgentDir as getDefaultAgentDir, getSessionsDir } from "../config.js";
7
7
  import { createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage, } from "./messages.js";
8
8
  export const CURRENT_SESSION_VERSION = 3;
@@ -340,78 +340,164 @@ function getSessionCreatedDate(header, statsMtime) {
340
340
  function resolveForDeletion(filePath) {
341
341
  return resolve(filePath);
342
342
  }
343
+ const sessionMetadataCache = new Map();
344
+ const sessionMetadataInflight = new Map();
345
+ function cloneSessionMetadata(metadata) {
346
+ return {
347
+ ...metadata,
348
+ created: new Date(metadata.created),
349
+ modified: new Date(metadata.modified),
350
+ };
351
+ }
352
+ function toSessionMetadata(info) {
353
+ const { allMessagesText: _allMessagesText, ...metadata } = info;
354
+ return metadata;
355
+ }
356
+ async function parseSessionInfo(filePath, stats, includeAllMessagesText) {
357
+ const content = await readFile(filePath, "utf8");
358
+ const entries = [];
359
+ const lines = content.trim().split("\n");
360
+ for (const line of lines) {
361
+ if (!line.trim())
362
+ continue;
363
+ try {
364
+ entries.push(JSON.parse(line));
365
+ }
366
+ catch {
367
+ // Skip malformed lines
368
+ }
369
+ }
370
+ if (entries.length === 0)
371
+ return null;
372
+ const header = entries[0];
373
+ if (header.type !== "session")
374
+ return null;
375
+ let messageCount = 0;
376
+ let firstMessage = "";
377
+ const allMessages = [];
378
+ let name;
379
+ for (const entry of entries) {
380
+ // Extract session name (use latest, including explicit clears)
381
+ if (entry.type === "session_info") {
382
+ const infoEntry = entry;
383
+ name = infoEntry.name?.trim() || undefined;
384
+ }
385
+ if (entry.type !== "message")
386
+ continue;
387
+ messageCount++;
388
+ const message = entry.message;
389
+ if (!isMessageWithContent(message))
390
+ continue;
391
+ if (message.role !== "user" && message.role !== "assistant")
392
+ continue;
393
+ const textContent = extractTextContent(message);
394
+ if (!textContent)
395
+ continue;
396
+ if (includeAllMessagesText)
397
+ allMessages.push(textContent);
398
+ if (!firstMessage && message.role === "user") {
399
+ firstMessage = textContent;
400
+ }
401
+ }
402
+ const cwd = typeof header.cwd === "string" ? header.cwd : "";
403
+ const parentSessionPath = header.parentSession;
404
+ const modified = getSessionModifiedDate(entries, header, stats.mtime);
405
+ const created = getSessionCreatedDate(header, stats.mtime);
406
+ return {
407
+ path: filePath,
408
+ id: header.id,
409
+ cwd,
410
+ name,
411
+ parentSessionPath,
412
+ created,
413
+ modified,
414
+ messageCount,
415
+ firstMessage: firstMessage || "(no messages)",
416
+ allMessagesText: allMessages.join(" "),
417
+ };
418
+ }
419
+ function isFilesystemError(error) {
420
+ return typeof error?.code === "string";
421
+ }
343
422
  async function buildSessionInfo(filePath, throwOnFilesystemError = false) {
344
423
  try {
345
- const content = await readFile(filePath, "utf8");
346
- const entries = [];
347
- const lines = content.trim().split("\n");
348
- for (const line of lines) {
349
- if (!line.trim())
350
- continue;
351
- try {
352
- entries.push(JSON.parse(line));
353
- }
354
- catch {
355
- // Skip malformed lines
356
- }
357
- }
358
- if (entries.length === 0)
359
- return null;
360
- const header = entries[0];
361
- if (header.type !== "session")
362
- return null;
363
424
  const stats = await stat(filePath);
364
- let messageCount = 0;
365
- let firstMessage = "";
366
- const allMessages = [];
367
- let name;
368
- for (const entry of entries) {
369
- // Extract session name (use latest, including explicit clears)
370
- if (entry.type === "session_info") {
371
- const infoEntry = entry;
372
- name = infoEntry.name?.trim() || undefined;
373
- }
374
- if (entry.type !== "message")
375
- continue;
376
- messageCount++;
377
- const message = entry.message;
378
- if (!isMessageWithContent(message))
379
- continue;
380
- if (message.role !== "user" && message.role !== "assistant")
381
- continue;
382
- const textContent = extractTextContent(message);
383
- if (!textContent)
384
- continue;
385
- allMessages.push(textContent);
386
- if (!firstMessage && message.role === "user") {
387
- firstMessage = textContent;
388
- }
389
- }
390
- const cwd = typeof header.cwd === "string" ? header.cwd : "";
391
- const parentSessionPath = header.parentSession;
392
- const modified = getSessionModifiedDate(entries, header, stats.mtime);
393
- const created = getSessionCreatedDate(header, stats.mtime);
394
- return {
395
- path: filePath,
396
- id: header.id,
397
- cwd,
398
- name,
399
- parentSessionPath,
400
- created,
401
- modified,
402
- messageCount,
403
- firstMessage: firstMessage || "(no messages)",
404
- allMessagesText: allMessages.join(" "),
405
- };
425
+ return await parseSessionInfo(filePath, stats, true);
406
426
  }
407
427
  catch (error) {
408
- if (throwOnFilesystemError && typeof error?.code === "string") {
428
+ if (throwOnFilesystemError && isFilesystemError(error))
409
429
  throw error;
430
+ /* Corrupt session file — exclude from listing */
431
+ return null;
432
+ }
433
+ }
434
+ async function buildSessionMetadata(filePath, throwOnFilesystemError = false) {
435
+ try {
436
+ const stats = await stat(filePath);
437
+ const cached = sessionMetadataCache.get(filePath);
438
+ if (cached?.mtimeMs === stats.mtimeMs && cached.size === stats.size) {
439
+ return cached.metadata ? cloneSessionMetadata(cached.metadata) : null;
440
+ }
441
+ const inflight = sessionMetadataInflight.get(filePath);
442
+ if (inflight?.mtimeMs === stats.mtimeMs && inflight.size === stats.size) {
443
+ const metadata = await inflight.promise;
444
+ return metadata ? cloneSessionMetadata(metadata) : null;
445
+ }
446
+ const promise = parseSessionInfo(filePath, stats, false).then((info) => {
447
+ const metadata = info ? toSessionMetadata(info) : null;
448
+ sessionMetadataCache.set(filePath, {
449
+ mtimeMs: stats.mtimeMs,
450
+ size: stats.size,
451
+ metadata: metadata ? cloneSessionMetadata(metadata) : null,
452
+ });
453
+ return metadata;
454
+ });
455
+ const current = { mtimeMs: stats.mtimeMs, size: stats.size, promise };
456
+ sessionMetadataInflight.set(filePath, current);
457
+ try {
458
+ const metadata = await promise;
459
+ return metadata ? cloneSessionMetadata(metadata) : null;
410
460
  }
461
+ finally {
462
+ if (sessionMetadataInflight.get(filePath) === current)
463
+ sessionMetadataInflight.delete(filePath);
464
+ }
465
+ }
466
+ catch (error) {
467
+ if (throwOnFilesystemError && isFilesystemError(error))
468
+ throw error;
411
469
  /* Corrupt session file — exclude from listing */
412
470
  return null;
413
471
  }
414
472
  }
473
+ async function collectSessionMetadata(files, isInScope, onProgress, throwOnFilesystemError = false) {
474
+ // Prune cache entries for deleted files, but only within the scanned scope so
475
+ // default and custom-directory listings don't evict each other's entries.
476
+ const currentFiles = new Set(files);
477
+ for (const cachedPath of sessionMetadataCache.keys()) {
478
+ if (isInScope(cachedPath) && !currentFiles.has(cachedPath))
479
+ sessionMetadataCache.delete(cachedPath);
480
+ }
481
+ let loaded = 0;
482
+ const results = await Promise.all(files.map(async (file) => {
483
+ const metadata = await buildSessionMetadata(file, throwOnFilesystemError);
484
+ loaded++;
485
+ onProgress?.(loaded, files.length);
486
+ return metadata;
487
+ }));
488
+ const sessions = results.filter((metadata) => metadata !== null);
489
+ sessions.sort((a, b) => b.modified.getTime() - a.modified.getTime());
490
+ return sessions;
491
+ }
492
+ async function listAllSessionFiles() {
493
+ const sessionsDir = getSessionsDir();
494
+ if (!existsSync(sessionsDir))
495
+ return [];
496
+ const entries = await readdir(sessionsDir, { withFileTypes: true });
497
+ const dirs = entries.filter((entry) => entry.isDirectory()).map((entry) => join(sessionsDir, entry.name));
498
+ const dirFiles = await Promise.all(dirs.map(async (dir) => (await readdir(dir)).filter((file) => file.endsWith(".jsonl")).map((file) => join(dir, file))));
499
+ return dirFiles.flat();
500
+ }
415
501
  async function listSessionsFromDir(dir, onProgress, progressOffset = 0, progressTotal, throwOnFilesystemError = false) {
416
502
  const sessions = [];
417
503
  if (!throwOnFilesystemError && !existsSync(dir)) {
@@ -1233,46 +1319,54 @@ export class SessionManager {
1233
1319
  sessions.sort((a, b) => b.modified.getTime() - a.modified.getTime());
1234
1320
  return sessions;
1235
1321
  }
1322
+ /**
1323
+ * List lightweight metadata for every session stored directly in one explicit flat directory.
1324
+ * Uses the same mtime/size cache as listAllMetadata(); filesystem errors propagate.
1325
+ */
1326
+ static async listAllMetadataFromDir(dir, onProgress) {
1327
+ let dirEntries;
1328
+ try {
1329
+ dirEntries = await readdir(dir);
1330
+ }
1331
+ catch (error) {
1332
+ if (error?.code === "ENOENT")
1333
+ return [];
1334
+ throw error;
1335
+ }
1336
+ const files = dirEntries.filter((file) => file.endsWith(".jsonl")).map((file) => join(dir, file));
1337
+ const scope = resolve(dir);
1338
+ return collectSessionMetadata(files, (cachedPath) => dirname(resolve(cachedPath)) === scope, onProgress, true);
1339
+ }
1236
1340
  /**
1237
1341
  * List all sessions across all project directories.
1238
1342
  * @param onProgress Optional callback for progress updates (loaded, total)
1239
1343
  */
1240
1344
  static async listAll(onProgress) {
1241
- const sessionsDir = getSessionsDir();
1242
1345
  // A missing sessions directory is a legitimate empty state (fresh install), not a
1243
- // failure — return []. Any actual I/O failure below propagates loudly so callers can
1244
- // distinguish "no sessions" from "listing failed" (RPC responds success:false; the TUI
1245
- // selector surfaces "Failed to load sessions"). No silent catch-and-return-[].
1246
- if (!existsSync(sessionsDir)) {
1247
- return [];
1248
- }
1249
- const entries = await readdir(sessionsDir, { withFileTypes: true });
1250
- const dirs = entries.filter((e) => e.isDirectory()).map((e) => join(sessionsDir, e.name));
1251
- // Count total files first for accurate progress
1252
- let totalFiles = 0;
1253
- const dirFiles = [];
1254
- for (const dir of dirs) {
1255
- const files = (await readdir(dir)).filter((f) => f.endsWith(".jsonl"));
1256
- dirFiles.push(files.map((f) => join(dir, f)));
1257
- totalFiles += files.length;
1258
- }
1259
- // Process all files with progress tracking
1346
+ // failure. Actual I/O failures propagate so callers can distinguish "no sessions"
1347
+ // from "listing failed".
1348
+ const allFiles = await listAllSessionFiles();
1260
1349
  let loaded = 0;
1261
- const sessions = [];
1262
- const allFiles = dirFiles.flat();
1263
1350
  const results = await Promise.all(allFiles.map(async (file) => {
1264
1351
  const info = await buildSessionInfo(file);
1265
1352
  loaded++;
1266
- onProgress?.(loaded, totalFiles);
1353
+ onProgress?.(loaded, allFiles.length);
1267
1354
  return info;
1268
1355
  }));
1269
- for (const info of results) {
1270
- if (info) {
1271
- sessions.push(info);
1272
- }
1273
- }
1356
+ const sessions = results.filter((info) => info !== null);
1274
1357
  sessions.sort((a, b) => b.modified.getTime() - a.modified.getTime());
1275
1358
  return sessions;
1276
1359
  }
1360
+ /**
1361
+ * List lightweight metadata across all project directories.
1362
+ *
1363
+ * Results are cached by file mtime and size, so dashboard inventory refreshes
1364
+ * stat every session but only reread and parse files that changed.
1365
+ */
1366
+ static async listAllMetadata(onProgress) {
1367
+ const allFiles = await listAllSessionFiles();
1368
+ const scope = resolve(getSessionsDir()) + sep;
1369
+ return collectSessionMetadata(allFiles, (cachedPath) => resolve(cachedPath).startsWith(scope), onProgress);
1370
+ }
1277
1371
  }
1278
1372
  //# sourceMappingURL=session-manager.js.map