bosun 0.41.2 → 0.41.4

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 (73) hide show
  1. package/.env.example +1 -1
  2. package/agent/agent-pool.mjs +9 -2
  3. package/agent/agent-prompt-catalog.mjs +971 -0
  4. package/agent/agent-prompts.mjs +2 -970
  5. package/agent/agent-supervisor.mjs +119 -6
  6. package/agent/autofix-git.mjs +33 -0
  7. package/agent/autofix-prompts.mjs +151 -0
  8. package/agent/autofix.mjs +11 -175
  9. package/agent/bosun-skills.mjs +3 -2
  10. package/bosun.config.example.json +17 -0
  11. package/bosun.schema.json +87 -188
  12. package/cli.mjs +34 -1
  13. package/config/config-doctor.mjs +5 -250
  14. package/config/config-file-names.mjs +5 -0
  15. package/config/config.mjs +89 -493
  16. package/config/executor-config.mjs +493 -0
  17. package/config/repo-root.mjs +1 -2
  18. package/config/workspace-health.mjs +242 -0
  19. package/git/git-safety.mjs +15 -0
  20. package/github/github-oauth-portal.mjs +46 -0
  21. package/infra/library-manager-utils.mjs +22 -0
  22. package/infra/library-manager-well-known-sources.mjs +578 -0
  23. package/infra/library-manager.mjs +512 -1030
  24. package/infra/monitor.mjs +35 -9
  25. package/infra/session-tracker.mjs +10 -7
  26. package/kanban/kanban-adapter.mjs +17 -1
  27. package/lib/codebase-audit-manifests.mjs +117 -0
  28. package/lib/codebase-audit.mjs +18 -115
  29. package/package.json +18 -3
  30. package/server/setup-web-server.mjs +58 -5
  31. package/server/ui-server.mjs +1394 -79
  32. package/shell/codex-config-file.mjs +178 -0
  33. package/shell/codex-config.mjs +538 -575
  34. package/task/task-cli.mjs +54 -3
  35. package/task/task-executor.mjs +143 -13
  36. package/task/task-store.mjs +409 -1
  37. package/telegram/telegram-bot.mjs +127 -0
  38. package/tools/apply-pr-suggestions.mjs +401 -0
  39. package/tools/syntax-check.mjs +28 -9
  40. package/ui/app.js +3 -14
  41. package/ui/components/kanban-board.js +227 -4
  42. package/ui/components/session-list.js +85 -5
  43. package/ui/demo-defaults.js +338 -84
  44. package/ui/demo.html +155 -0
  45. package/ui/modules/session-api.js +96 -0
  46. package/ui/modules/settings-schema.js +1 -2
  47. package/ui/modules/state.js +43 -3
  48. package/ui/setup.html +4 -5
  49. package/ui/styles/components.css +58 -4
  50. package/ui/tabs/agents.js +12 -15
  51. package/ui/tabs/control.js +1 -0
  52. package/ui/tabs/library.js +484 -22
  53. package/ui/tabs/manual-flows.js +105 -29
  54. package/ui/tabs/tasks.js +848 -141
  55. package/ui/tabs/telemetry.js +129 -11
  56. package/ui/tabs/workflow-canvas-utils.mjs +130 -0
  57. package/ui/tabs/workflows.js +293 -23
  58. package/voice/voice-tool-definitions.mjs +757 -0
  59. package/voice/voice-tools.mjs +34 -778
  60. package/workflow/manual-flow-audit.mjs +165 -0
  61. package/workflow/manual-flows.mjs +164 -259
  62. package/workflow/workflow-engine.mjs +147 -58
  63. package/workflow/workflow-nodes/definitions.mjs +1207 -0
  64. package/workflow/workflow-nodes/transforms.mjs +612 -0
  65. package/workflow/workflow-nodes.mjs +358 -63
  66. package/workflow/workflow-templates.mjs +313 -191
  67. package/workflow-templates/_helpers.mjs +154 -0
  68. package/workflow-templates/agents.mjs +61 -4
  69. package/workflow-templates/code-quality.mjs +7 -7
  70. package/workflow-templates/github.mjs +20 -10
  71. package/workflow-templates/task-batch.mjs +44 -11
  72. package/workflow-templates/task-lifecycle.mjs +31 -6
  73. package/workspace/worktree-manager.mjs +277 -3
@@ -260,6 +260,15 @@ function formatBytes(n) {
260
260
  return `${n}`;
261
261
  }
262
262
 
263
+ function formatUsd(value) {
264
+ const amount = Number(value);
265
+ if (!Number.isFinite(amount)) return "–";
266
+ if (amount === 0) return "$0.00";
267
+ if (Math.abs(amount) < 0.01) return `$${amount.toFixed(4)}`;
268
+ if (Math.abs(amount) < 1) return `$${amount.toFixed(3)}`;
269
+ return `$${amount.toFixed(2)}`;
270
+ }
271
+
263
272
  function formatShreddingLabel(value) {
264
273
  return String(value || "unknown")
265
274
  .replace(/[_-]+/g, " ")
@@ -318,10 +327,16 @@ function ShreddingPanel({ period }) {
318
327
  const {
319
328
  totalEvents = 0,
320
329
  totalOriginalChars = 0,
330
+ totalCompressedChars = 0,
321
331
  totalSavedChars = 0,
322
332
  avgSavedPct = 0,
323
333
  sortedDates = [],
334
+ dailyOriginal = {},
335
+ dailyCompressed = {},
324
336
  dailySaved = {},
337
+ dailySavedTokensEstimated = {},
338
+ dailyCostSavedUsd = {},
339
+ dailyReductionPct = {},
325
340
  dailyCounts = {},
326
341
  topAgents = [],
327
342
  stageCounts = {},
@@ -330,16 +345,38 @@ function ShreddingPanel({ period }) {
330
345
  liveCompaction = {},
331
346
  recentEvents = [],
332
347
  diagnostics = {},
348
+ totals = {},
349
+ estimation = {},
333
350
  } = data;
334
351
 
335
- const sparkValues = sortedDates.map((d) => dailySaved[d] || 0);
352
+ const volumeSeriesMap = {
353
+ original: sortedDates.map((d) => dailyOriginal[d] || 0),
354
+ compressed: sortedDates.map((d) => dailyCompressed[d] || 0),
355
+ saved: sortedDates.map((d) => dailySaved[d] || 0),
356
+ };
357
+ const reductionSeriesMap = {
358
+ "reduction %": sortedDates.map((d) => dailyReductionPct[d] || 0),
359
+ };
360
+ const tokenSeriesMap = {
361
+ "tokens saved": sortedDates.map((d) => dailySavedTokensEstimated[d] || 0),
362
+ };
363
+ const costSeriesMap = {
364
+ "cost avoided": sortedDates.map((d) => dailyCostSavedUsd[d] || 0),
365
+ };
336
366
  const sparkCounts = sortedDates.map((d) => dailyCounts[d] || 0);
337
367
  const stageItems = Object.entries(stageCounts)
338
368
  .sort((a, b) => b[1] - a[1])
339
369
  .map(([name, count]) => ({ name: formatShreddingLabel(name), count }));
340
370
  const liveEvents = liveCompaction?.totalEvents || stageCounts.live_tool_compaction || 0;
341
371
  const liveSavedChars = liveCompaction?.totalSavedChars || 0;
372
+ const liveSavedTokensEstimated = liveCompaction?.savedTokensEstimated || 0;
342
373
  const liveAvgSavedPct = liveCompaction?.avgSavedPct || 0;
374
+ const totalSavedTokensEstimated = totals?.savedTokensEstimated || 0;
375
+ const totalEstimatedCostSavedUsd = totals?.estimatedCostSavedUsd ?? null;
376
+ const hasCostEstimate = Number.isFinite(Number(totalEstimatedCostSavedUsd))
377
+ && Number(totalEstimatedCostSavedUsd) > 0;
378
+ const hasCostTrend = sortedDates.some((day) => Number(dailyCostSavedUsd?.[day] || 0) > 0);
379
+ const observedCostRate = estimation?.blendedCostPerMillionTokensUsd ?? null;
343
380
 
344
381
  return html`
345
382
  <${Paper} elevation=${1} sx=${{ p: 2, mb: 2 }}>
@@ -355,11 +392,14 @@ function ShreddingPanel({ period }) {
355
392
 
356
393
  <${Stack} direction=${{ xs: "column", sm: "row" }} spacing=${1.5} sx=${{ mb: 2, flexWrap: "wrap" }}>
357
394
  <${AnalyticsStat} icon="✂" label="Events" value=${formatCount(totalEvents)} />
395
+ <${AnalyticsStat} icon="📦" label="Original Chars" value=${formatBytes(totalOriginalChars)} />
396
+ <${AnalyticsStat} icon="🧱" label="Compressed Chars" value=${formatBytes(totalCompressedChars)} />
358
397
  <${AnalyticsStat} icon="📉" label="Chars Saved" value=${formatBytes(totalSavedChars)} />
359
398
  <${AnalyticsStat} icon="%" label="Avg Reduction" value=${avgSavedPct > 0 ? `${avgSavedPct}%` : "–"} />
360
- <${AnalyticsStat} icon="📦" label="Original Chars" value=${formatBytes(totalOriginalChars)} />
399
+ <${AnalyticsStat} icon="🧮" label="Est. Tokens Saved" value=${formatCount(totalSavedTokensEstimated)} />
400
+ <${AnalyticsStat} icon="💵" label="Est. Cost Avoided" value=${hasCostEstimate ? formatUsd(totalEstimatedCostSavedUsd) : "Unavailable"} />
361
401
  <${AnalyticsStat} icon="⚡" label="Live Events" value=${formatCount(liveEvents)} />
362
- <${AnalyticsStat} icon="🧠" label="Live Avg" value=${liveAvgSavedPct > 0 ? `${liveAvgSavedPct}%` : "–"} />
402
+ <${AnalyticsStat} icon="🧠" label="Live Saved Tokens" value=${formatCount(liveSavedTokensEstimated)} />
363
403
  <//>
364
404
 
365
405
  ${(diagnostics?.excludedSynthetic || diagnostics?.excludedNoop || diagnostics?.unknownAttribution)
@@ -373,14 +413,24 @@ function ShreddingPanel({ period }) {
373
413
  `
374
414
  : null}
375
415
 
416
+ <${Alert} severity=${hasCostEstimate ? "success" : "info"} sx=${{ mb: 2 }}>
417
+ Token savings are estimated from characters using ${estimation?.charsPerToken || 4} chars per token.
418
+ ${hasCostEstimate
419
+ ? ` Cost avoided uses the observed blended session rate of ${formatUsd((observedCostRate || 0) / 1_000_000)} per token (${formatUsd(observedCostRate)} per million) across ${formatCount(estimation?.pricedSessions || 0)} priced session${(estimation?.pricedSessions || 0) === 1 ? "" : "s"}.`
420
+ : " Cost avoided is unavailable because recent completed sessions did not record usable token-and-cost pairs."}
421
+ <//>
422
+
376
423
  <${Stack} direction=${{ xs: "column", md: "row" }} spacing=${2} sx=${{ mb: 2 }}>
377
424
  <${Paper} variant="outlined" sx=${{ p: 1.5, flex: 1 }}>
378
- <${Typography} variant="subtitle2" gutterBottom>Chars Saved per Day<//>
379
- ${sparkValues.length > 1 ? html`
425
+ <${Typography} variant="subtitle2" gutterBottom>Context Volume per Day<//>
426
+ <${Typography} variant="caption" color="text.secondary">
427
+ Original context versus compressed output and net savings.
428
+ <//>
429
+ ${sortedDates.length > 1 ? html`
380
430
  <${Box} sx=${{ overflow: "hidden" }}>
381
431
  <${TrendLines}
382
432
  dates=${sortedDates}
383
- seriesMap=${{ "chars saved": sparkValues }}
433
+ seriesMap=${volumeSeriesMap}
384
434
  palette=${SHRED_PALETTE}
385
435
  />
386
436
  <//>
@@ -388,13 +438,35 @@ function ShreddingPanel({ period }) {
388
438
  <//>
389
439
 
390
440
  <${Paper} variant="outlined" sx=${{ p: 1.5, flex: 1 }}>
391
- <${Typography} variant="subtitle2" gutterBottom>By Agent Type<//>
392
- <${TopBarChart} items=${topAgents} palette=${SHRED_PALETTE} title="By Agent" />
441
+ <${Typography} variant="subtitle2" gutterBottom>Reduction Efficiency<//>
442
+ <${Typography} variant="caption" color="text.secondary">
443
+ Daily reduction rate with ${sparkCounts.length ? sparkCounts.reduce((sum, value) => sum + value, 0) : 0} tracked events in this window.
444
+ <//>
445
+ ${sortedDates.length > 1 ? html`
446
+ <${Box} sx=${{ overflow: "hidden" }}>
447
+ <${TrendLines}
448
+ dates=${sortedDates}
449
+ seriesMap=${reductionSeriesMap}
450
+ palette=${SHRED_PALETTE}
451
+ />
452
+ <//>
453
+ ` : html`<${EmptyState} title="Not enough data" description="Need ≥2 days of events." />`}
393
454
  <//>
394
455
 
395
456
  <${Paper} variant="outlined" sx=${{ p: 1.5, flex: 1 }}>
396
- <${Typography} variant="subtitle2" gutterBottom>By Stage<//>
397
- <${TopBarChart} items=${stageItems} palette=${SHRED_PALETTE} title="By Stage" />
457
+ <${Typography} variant="subtitle2" gutterBottom>Estimated Input Tokens Saved<//>
458
+ <${Typography} variant="caption" color="text.secondary">
459
+ Estimated prompt-token savings from context compaction.
460
+ <//>
461
+ ${sortedDates.length > 1 ? html`
462
+ <${Box} sx=${{ overflow: "hidden" }}>
463
+ <${TrendLines}
464
+ dates=${sortedDates}
465
+ seriesMap=${tokenSeriesMap}
466
+ palette=${SHRED_PALETTE}
467
+ />
468
+ <//>
469
+ ` : html`<${EmptyState} title="Not enough data" description="Need ≥2 days of events." />`}
398
470
  <//>
399
471
  <//>
400
472
 
@@ -416,6 +488,29 @@ function ShreddingPanel({ period }) {
416
488
  <//>
417
489
  ` : null}
418
490
 
491
+ <${Stack} direction=${{ xs: "column", md: "row" }} spacing=${2} sx=${{ mb: 2 }}>
492
+ <${Paper} variant="outlined" sx=${{ p: 1.5, flex: 1 }}>
493
+ <${Typography} variant="subtitle2" gutterBottom>Estimated Cost Avoided per Day<//>
494
+ ${hasCostTrend ? html`
495
+ <${Box} sx=${{ overflow: "hidden" }}>
496
+ <${TrendLines}
497
+ dates=${sortedDates}
498
+ seriesMap=${costSeriesMap}
499
+ palette=${SHRED_PALETTE}
500
+ />
501
+ <//>
502
+ ` : html`<${EmptyState} title="No cost estimate yet" description="Need recent session pricing data to project API cost savings." />`}
503
+ <//>
504
+ <${Paper} variant="outlined" sx=${{ p: 1.5, flex: 1 }}>
505
+ <${Typography} variant="subtitle2" gutterBottom>By Agent Type<//>
506
+ <${TopBarChart} items=${topAgents} palette=${SHRED_PALETTE} title="By Agent" />
507
+ <//>
508
+ <${Paper} variant="outlined" sx=${{ p: 1.5, flex: 1 }}>
509
+ <${Typography} variant="subtitle2" gutterBottom>By Stage<//>
510
+ <${TopBarChart} items=${stageItems} palette=${SHRED_PALETTE} title="By Stage" />
511
+ <//>
512
+ <//>
513
+
419
514
  ${(liveEvents > 0 || topCompactionFamilies.length > 0 || topCommandFamilies.length > 0) ? html`
420
515
  <${Stack} direction=${{ xs: "column", md: "row" }} spacing=${2} sx=${{ mb: 2 }}>
421
516
  <${Paper} variant="outlined" sx=${{ p: 1.5, flex: 1 }}>
@@ -433,7 +528,7 @@ function ShreddingPanel({ period }) {
433
528
  Saved ${formatBytes(liveSavedChars)} across ${formatCount(liveEvents)} live-compacted outputs.
434
529
  <//>
435
530
  <${Typography} variant="caption" color="text.secondary">
436
- Daily samples: ${sparkCounts.length ? sparkCounts.reduce((sum, value) => sum + value, 0) : 0} tracked shredding events in this window.
531
+ Estimated ${formatCount(liveSavedTokensEstimated)} input tokens avoided during live compaction.
437
532
  <//>
438
533
  <${Chip}
439
534
  label=${liveAvgSavedPct > 0 ? `${liveAvgSavedPct}% average live reduction` : "Live reductions pending"}
@@ -456,8 +551,11 @@ function ShreddingPanel({ period }) {
456
551
  <${TableCell}>Stage<//>
457
552
  <${TableCell}>Family<//>
458
553
  <${TableCell} align="right">Original<//>
554
+ <${TableCell} align="right">Compressed<//>
459
555
  <${TableCell} align="right">Saved<//>
460
556
  <${TableCell} align="right">Reduction<//>
557
+ <${TableCell} align="right">Est. Tokens<//>
558
+ <${TableCell} align="right">Est. Cost<//>
461
559
  <${TableCell}>Agent<//>
462
560
  </${TableRow}>
463
561
  <//>
@@ -480,6 +578,9 @@ function ShreddingPanel({ period }) {
480
578
  <${TableCell} align="right">
481
579
  <${Typography} variant="caption">${formatBytes(ev.originalChars)}<//>
482
580
  <//>
581
+ <${TableCell} align="right">
582
+ <${Typography} variant="caption">${formatBytes(ev.compressedChars)}<//>
583
+ <//>
483
584
  <${TableCell} align="right">
484
585
  <${Typography} variant="caption" color="success.main">
485
586
  ${ev.savedChars > 0 ? `-${formatBytes(ev.savedChars)}` : "0"}
@@ -493,6 +594,14 @@ function ShreddingPanel({ period }) {
493
594
  variant="outlined"
494
595
  />
495
596
  <//>
597
+ <${TableCell} align="right">
598
+ <${Typography} variant="caption">${formatCount(ev.estimatedSavedTokens || 0)}<//>
599
+ <//>
600
+ <${TableCell} align="right">
601
+ <${Typography} variant="caption">
602
+ ${Number.isFinite(Number(ev.estimatedCostSavedUsd)) ? formatUsd(ev.estimatedCostSavedUsd) : "–"}
603
+ <//>
604
+ <//>
496
605
  <${TableCell}>
497
606
  <${Typography} variant="caption" color="text.secondary">
498
607
  ${ev.agentType || "–"}
@@ -608,6 +717,15 @@ export function TelemetryTab() {
608
717
  value=${formatDurationMs(lifetimeTotals?.durationMs || 0)} />
609
718
  <//>
610
719
 
720
+ ${data?.diagnostics?.agentRunSource ? html`
721
+ <${Alert} severity="info" sx=${{ mb: 2 }}>
722
+ Agent Runs are currently sourced from ${data.diagnostics.agentRunSource === "completed_sessions" ? "the persistent completed-session ledger" : "session-start telemetry"}.
723
+ ${data.diagnostics.agentRunSource === "completed_sessions"
724
+ ? ` Counted ${formatCount(data.diagnostics.completedSessions || 0)} completed sessions in this window.`
725
+ : ` Counted ${formatCount(data.diagnostics.sessionStarts || 0)} session-start events in this window.`}
726
+ <//>
727
+ ` : null}
728
+
611
729
  <!-- Activity trend chart -->
612
730
  <${Paper} elevation=${1} sx=${{ p: 2, mb: 2 }}>
613
731
  <${Typography} variant="h6" gutterBottom>Activity Trend<//>
@@ -35,6 +35,118 @@ function getLabel(type) {
35
35
  return String(type || "").split(".").pop()?.replace(/_/g, " ") || String(type || "");
36
36
  }
37
37
 
38
+ function normalizePreviewLine(text, maxLength = 84) {
39
+ const compact = String(text || "").replace(/\s+/g, " ").trim();
40
+ if (!compact) return "";
41
+ return compact.length > maxLength
42
+ ? `${compact.slice(0, Math.max(0, maxLength - 1))}…`
43
+ : compact;
44
+ }
45
+
46
+ function collectPreviewText(value, lines = [], seen = new Set(), depth = 0) {
47
+ if (value == null || depth > 2 || lines.length >= 6) return lines;
48
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
49
+ const rawLines = String(value)
50
+ .split(/\r?\n/)
51
+ .map((line) => normalizePreviewLine(line))
52
+ .filter(Boolean);
53
+ for (const line of rawLines) {
54
+ if (seen.has(line)) continue;
55
+ seen.add(line);
56
+ lines.push(line);
57
+ if (lines.length >= 6) break;
58
+ }
59
+ return lines;
60
+ }
61
+ if (Array.isArray(value)) {
62
+ for (const item of value.slice(0, 4)) {
63
+ collectPreviewText(item, lines, seen, depth + 1);
64
+ if (lines.length >= 6) break;
65
+ }
66
+ return lines;
67
+ }
68
+ if (typeof value !== "object") return lines;
69
+
70
+ const prioritizedKeys = [
71
+ "summary",
72
+ "narrative",
73
+ "text",
74
+ "message",
75
+ "content",
76
+ "output",
77
+ "result",
78
+ "answer",
79
+ "response",
80
+ "stdout",
81
+ "stderr",
82
+ ];
83
+ let matched = false;
84
+ for (const key of prioritizedKeys) {
85
+ if (!Object.prototype.hasOwnProperty.call(value, key)) continue;
86
+ matched = true;
87
+ collectPreviewText(value[key], lines, seen, depth + 1);
88
+ if (lines.length >= 6) return lines;
89
+ }
90
+ if (Array.isArray(value.lines)) {
91
+ matched = true;
92
+ collectPreviewText(value.lines, lines, seen, depth + 1);
93
+ }
94
+ if (value.preview && typeof value.preview === "object") {
95
+ matched = true;
96
+ collectPreviewText(value.preview, lines, seen, depth + 1);
97
+ }
98
+ if (!matched) {
99
+ try {
100
+ const json = JSON.stringify(value);
101
+ if (json) collectPreviewText(json, lines, seen, depth + 1);
102
+ } catch {}
103
+ }
104
+ return lines;
105
+ }
106
+
107
+ function extractPreviewTokenCount(value) {
108
+ if (!value || typeof value !== "object") return null;
109
+ const candidates = [
110
+ value.tokenCount,
111
+ value.totalTokens,
112
+ value.total_tokens,
113
+ value.tokens,
114
+ value.usage?.total_tokens,
115
+ value.usage?.totalTokens,
116
+ value.tokenUsage?.totalTokens,
117
+ value.metrics?.total_tokens,
118
+ value.metrics?.totalTokens,
119
+ ];
120
+ for (const candidate of candidates) {
121
+ const parsed = Number(candidate);
122
+ if (Number.isFinite(parsed) && parsed >= 0) {
123
+ return Math.max(0, Math.round(parsed));
124
+ }
125
+ }
126
+ const input = Number(
127
+ value.inputTokens
128
+ ?? value.input_tokens
129
+ ?? value.promptTokens
130
+ ?? value.prompt_tokens
131
+ ?? value.usage?.prompt_tokens
132
+ ?? value.usage?.inputTokens
133
+ ?? value.tokenUsage?.inputTokens,
134
+ );
135
+ const output = Number(
136
+ value.outputTokens
137
+ ?? value.output_tokens
138
+ ?? value.completionTokens
139
+ ?? value.completion_tokens
140
+ ?? value.usage?.completion_tokens
141
+ ?? value.usage?.outputTokens
142
+ ?? value.tokenUsage?.outputTokens,
143
+ );
144
+ if (Number.isFinite(input) || Number.isFinite(output)) {
145
+ return Math.max(0, Math.round((Number.isFinite(input) ? input : 0) + (Number.isFinite(output) ? output : 0)));
146
+ }
147
+ return null;
148
+ }
149
+
38
150
  export function createGraphSnapshot(nodes = [], edges = []) {
39
151
  return {
40
152
  nodes: normalizeGraphValue(nodes),
@@ -217,3 +329,21 @@ export function buildNodeStatusesFromRunDetail(run) {
217
329
 
218
330
  return statuses;
219
331
  }
332
+
333
+ export function resolveNodeOutputPreview(_nodeType, livePreview = null, rawOutput = null) {
334
+ const liveLines = Array.isArray(livePreview?.lines)
335
+ ? livePreview.lines.map((line) => normalizePreviewLine(line)).filter(Boolean)
336
+ : [];
337
+ const liveTokenCount = extractPreviewTokenCount(livePreview);
338
+ if (liveLines.length || liveTokenCount != null) {
339
+ return {
340
+ lines: liveLines.slice(0, 3),
341
+ tokenCount: liveTokenCount,
342
+ };
343
+ }
344
+
345
+ return {
346
+ lines: collectPreviewText(rawOutput).slice(0, 3),
347
+ tokenCount: extractPreviewTokenCount(rawOutput),
348
+ };
349
+ }