@os-eco/overstory-cli 0.7.5 → 0.7.7

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.
@@ -24,6 +24,7 @@ import { createEventStore } from "../events/store.ts";
24
24
  import { accent, brand, color, visibleLength } from "../logging/color.ts";
25
25
  import {
26
26
  buildAgentColorMap,
27
+ extendAgentColorMap,
27
28
  formatDuration,
28
29
  formatEventLine,
29
30
  formatRelativeTime,
@@ -128,10 +129,10 @@ export { pad, truncate, horizontalLine };
128
129
 
129
130
  /**
130
131
  * Compute agent panel height from screen height and agent count.
131
- * min 8 rows, max floor(height * 0.5), grows with agent count (+4 for chrome).
132
+ * min 8 rows, max floor(height * 0.35), grows with agent count (+4 for chrome).
132
133
  */
133
134
  export function computeAgentPanelHeight(height: number, agentCount: number): number {
134
- return Math.max(8, Math.min(Math.floor(height * 0.5), agentCount + 4));
135
+ return Math.max(8, Math.min(Math.floor(height * 0.35), agentCount + 4));
135
136
  }
136
137
 
137
138
  /**
@@ -241,6 +242,49 @@ export function closeDashboardStores(stores: DashboardStores): void {
241
242
  }
242
243
  }
243
244
 
245
+ /**
246
+ * Rolling event buffer with incremental dedup by lastSeenId.
247
+ * Maintains a fixed-size window of the most recent events.
248
+ */
249
+ export class EventBuffer {
250
+ private events: StoredEvent[] = [];
251
+ private lastSeenId = 0;
252
+ private colorMap: Map<string, (s: string) => string> = new Map();
253
+ private readonly maxSize: number;
254
+
255
+ constructor(maxSize = 100) {
256
+ this.maxSize = maxSize;
257
+ }
258
+
259
+ poll(eventStore: EventStore): void {
260
+ const since = new Date(Date.now() - 60 * 1000).toISOString();
261
+ const allEvents = eventStore.getTimeline({ since, limit: 1000 });
262
+ const newEvents = allEvents.filter((e) => e.id > this.lastSeenId);
263
+
264
+ if (newEvents.length === 0) return;
265
+
266
+ extendAgentColorMap(this.colorMap, newEvents);
267
+ this.events = [...this.events, ...newEvents].slice(-this.maxSize);
268
+
269
+ const lastEvent = newEvents[newEvents.length - 1];
270
+ if (lastEvent) {
271
+ this.lastSeenId = lastEvent.id;
272
+ }
273
+ }
274
+
275
+ getEvents(): StoredEvent[] {
276
+ return this.events;
277
+ }
278
+
279
+ getColorMap(): Map<string, (s: string) => string> {
280
+ return this.colorMap;
281
+ }
282
+
283
+ get size(): number {
284
+ return this.events.length;
285
+ }
286
+ }
287
+
244
288
  /** Tracker data cached between dashboard ticks (10s TTL). */
245
289
  interface TrackerCache {
246
290
  tasks: TrackerIssue[];
@@ -263,6 +307,7 @@ interface DashboardData {
263
307
  };
264
308
  tasks: TrackerIssue[];
265
309
  recentEvents: StoredEvent[];
310
+ feedColorMap: Map<string, (s: string) => string>;
266
311
  }
267
312
 
268
313
  /**
@@ -289,6 +334,7 @@ async function loadDashboardData(
289
334
  stores: DashboardStores,
290
335
  runId?: string | null,
291
336
  thresholds?: { staleMs: number; zombieMs: number },
337
+ eventBuffer?: EventBuffer,
292
338
  ): Promise<DashboardData> {
293
339
  // Get all sessions from the pre-opened session store
294
340
  const allSessions = stores.sessionStore.getAll();
@@ -450,13 +496,14 @@ async function loadDashboardData(
450
496
  tasks = trackerCache.tasks;
451
497
  }
452
498
 
453
- // Load recent events from event store
499
+ // Load recent events via incremental buffer (or fallback to empty)
454
500
  let recentEvents: StoredEvent[] = [];
455
- if (stores.eventStore) {
501
+ let feedColorMap: Map<string, (s: string) => string> = new Map();
502
+ if (eventBuffer && stores.eventStore) {
456
503
  try {
457
- const fiveMinAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString();
458
- const events = stores.eventStore.getTimeline({ since: fiveMinAgo, limit: 20 });
459
- recentEvents = [...events].reverse(); // most recent first
504
+ eventBuffer.poll(stores.eventStore);
505
+ recentEvents = [...eventBuffer.getEvents()].reverse();
506
+ feedColorMap = eventBuffer.getColorMap();
460
507
  } catch {
461
508
  /* best effort */
462
509
  }
@@ -470,6 +517,7 @@ async function loadDashboardData(
470
517
  metrics: { totalSessions, avgDuration, byCapability },
471
518
  tasks,
472
519
  recentEvents,
520
+ feedColorMap,
473
521
  };
474
522
  }
475
523
 
@@ -496,7 +544,7 @@ export function renderAgentPanel(
496
544
  panelHeight: number,
497
545
  startRow: number,
498
546
  ): string {
499
- const leftWidth = Math.floor(fullWidth * 0.6);
547
+ const leftWidth = fullWidth;
500
548
  let output = "";
501
549
 
502
550
  // Panel header
@@ -650,7 +698,7 @@ export function renderFeedPanel(
650
698
  let output = "";
651
699
 
652
700
  // Header
653
- const headerLine = `${dimBox.vertical} ${brand.bold("Feed")} (last 5 min)`;
701
+ const headerLine = `${dimBox.vertical} ${brand.bold("Feed")} (live)`;
654
702
  const headerPadding = " ".repeat(
655
703
  Math.max(0, panelWidth - visibleLength(headerLine) - visibleLength(dimBox.vertical)),
656
704
  );
@@ -676,7 +724,8 @@ export function renderFeedPanel(
676
724
  return output;
677
725
  }
678
726
 
679
- const colorMap = buildAgentColorMap(data.recentEvents);
727
+ const colorMap =
728
+ data.feedColorMap.size > 0 ? data.feedColorMap : buildAgentColorMap(data.recentEvents);
680
729
  const visibleEvents = data.recentEvents.slice(0, maxRows);
681
730
 
682
731
  for (let i = 0; i < visibleEvents.length; i++) {
@@ -733,12 +782,10 @@ export function renderFeedPanel(
733
782
  */
734
783
  function renderMailPanel(
735
784
  data: DashboardData,
736
- width: number,
737
- height: number,
785
+ panelWidth: number,
786
+ panelHeight: number,
738
787
  startRow: number,
739
788
  ): string {
740
- const panelHeight = Math.floor(height * 0.3);
741
- const panelWidth = Math.floor(width * 0.5);
742
789
  let output = "";
743
790
 
744
791
  const unreadCount = data.status.unreadMailCount;
@@ -787,13 +834,11 @@ function renderMailPanel(
787
834
  */
788
835
  function renderMergeQueuePanel(
789
836
  data: DashboardData,
790
- width: number,
791
- height: number,
837
+ panelWidth: number,
838
+ panelHeight: number,
792
839
  startRow: number,
793
840
  startCol: number,
794
841
  ): string {
795
- const panelHeight = Math.floor(height * 0.3);
796
- const panelWidth = width - startCol + 1;
797
842
  let output = "";
798
843
 
799
844
  const headerLine = `${dimBox.vertical} ${brand.bold("Merge Queue")} (${data.mergeQueue.length})`;
@@ -847,26 +892,20 @@ function renderMetricsPanel(
847
892
  const separator = dimHorizontalLine(width, dimBox.tee, dimBox.teeRight);
848
893
  output += `${CURSOR.cursorTo(startRow, 1)}${separator}\n`;
849
894
 
850
- const headerLine = `${dimBox.vertical} ${brand.bold("Metrics")}`;
851
- const headerPadding = " ".repeat(
852
- Math.max(0, width - visibleLength(headerLine) - visibleLength(dimBox.vertical)),
853
- );
854
- output += `${CURSOR.cursorTo(startRow + 1, 1)}${headerLine}${headerPadding}${dimBox.vertical}\n`;
855
-
856
895
  const totalSessions = data.metrics.totalSessions;
857
896
  const avgDur = formatDuration(data.metrics.avgDuration);
858
897
  const byCapability = Object.entries(data.metrics.byCapability)
859
898
  .map(([cap, count]) => `${cap}:${count}`)
860
899
  .join(", ");
861
900
 
862
- const metricsLine = `${dimBox.vertical} Total sessions: ${totalSessions} | Avg duration: ${avgDur} | By capability: ${byCapability}`;
901
+ const metricsLine = `${dimBox.vertical} ${brand.bold("Metrics")} Total: ${totalSessions} | Avg: ${avgDur} | ${byCapability}`;
863
902
  const metricsPadding = " ".repeat(
864
903
  Math.max(0, width - visibleLength(metricsLine) - visibleLength(dimBox.vertical)),
865
904
  );
866
- output += `${CURSOR.cursorTo(startRow + 2, 1)}${metricsLine}${metricsPadding}${dimBox.vertical}\n`;
905
+ output += `${CURSOR.cursorTo(startRow + 1, 1)}${metricsLine}${metricsPadding}${dimBox.vertical}\n`;
867
906
 
868
907
  const bottomBorder = dimHorizontalLine(width, dimBox.bottomLeft, dimBox.bottomRight);
869
- output += `${CURSOR.cursorTo(startRow + 3, 1)}${bottomBorder}\n`;
908
+ output += `${CURSOR.cursorTo(startRow + 2, 1)}${bottomBorder}\n`;
870
909
 
871
910
  return output;
872
911
  }
@@ -883,50 +922,42 @@ function renderDashboard(data: DashboardData, interval: number): void {
883
922
  // Header (rows 1-2)
884
923
  output += renderHeader(width, interval, data.currentRunId);
885
924
 
886
- // Agent panel start row
925
+ // Agent panel: full width, capped at 35% of height
887
926
  const agentPanelStart = 3;
888
-
889
- // Dynamic agent panel height
890
927
  const agentCount = data.status.agents.length;
891
928
  const agentPanelHeight = computeAgentPanelHeight(height, agentCount);
929
+ output += renderAgentPanel(data, width, agentPanelHeight, agentPanelStart);
892
930
 
893
- // Column widths
894
- const leftWidth = Math.floor(width * 0.6);
895
- const rightWidth = width - leftWidth;
896
- const rightStartCol = leftWidth + 1;
931
+ // Middle zone: Feed (left 60%) | Tasks (right 40%)
932
+ const middleStart = agentPanelStart + agentPanelHeight + 1;
933
+ const compactPanelHeight = 5; // fixed for mail/merge panels
934
+ const metricsHeight = 3; // separator + data + border
935
+ const middleHeight = Math.max(6, height - middleStart - compactPanelHeight - metricsHeight);
897
936
 
898
- // Right column split (Tasks upper / Feed lower)
899
- const rightHalf = Math.floor(agentPanelHeight / 2);
900
- const feedHeight = agentPanelHeight - rightHalf;
937
+ const feedWidth = Math.floor(width * 0.6);
938
+ output += renderFeedPanel(data, 1, feedWidth, middleHeight, middleStart);
901
939
 
902
- // Render left: agents (60% wide, dynamic height)
903
- output += renderAgentPanel(data, width, agentPanelHeight, agentPanelStart);
940
+ const taskWidth = width - feedWidth;
941
+ const taskStartCol = feedWidth + 1;
942
+ output += renderTasksPanel(data, taskStartCol, taskWidth, middleHeight, middleStart);
904
943
 
905
- // Render right-upper: tasks
906
- output += renderTasksPanel(data, rightStartCol, rightWidth, rightHalf, agentPanelStart);
944
+ // Compact panels: Mail (left 50%) | Merge Queue (right 50%) — fixed 5 rows
945
+ const compactStart = middleStart + middleHeight;
946
+ const mailWidth = Math.floor(width * 0.5);
947
+ output += renderMailPanel(data, mailWidth, compactPanelHeight, compactStart);
907
948
 
908
- // Render right-lower: feed
909
- output += renderFeedPanel(
949
+ const mergeStartCol = mailWidth + 1;
950
+ const mergeWidth = width - mailWidth;
951
+ output += renderMergeQueuePanel(
910
952
  data,
911
- rightStartCol,
912
- rightWidth,
913
- feedHeight,
914
- agentPanelStart + rightHalf,
953
+ mergeWidth,
954
+ compactPanelHeight,
955
+ compactStart,
956
+ mergeStartCol,
915
957
  );
916
958
 
917
- // Middle panels (mail/merge) start after agent block
918
- const middlePanelStart = agentPanelStart + agentPanelHeight + 1;
919
-
920
- // Mail panel (left 50%)
921
- output += renderMailPanel(data, width, height, middlePanelStart);
922
-
923
- // Merge queue panel (right 50%)
924
- const mergeQueueCol = Math.floor(width * 0.5) + 1;
925
- output += renderMergeQueuePanel(data, width, height, middlePanelStart, mergeQueueCol);
926
-
927
- // Metrics panel (bottom strip)
928
- const middlePanelHeight = Math.floor(height * 0.3);
929
- const metricsStart = middlePanelStart + middlePanelHeight + 1;
959
+ // Metrics footer
960
+ const metricsStart = compactStart + compactPanelHeight;
930
961
  output += renderMetricsPanel(data, width, height, metricsStart);
931
962
 
932
963
  process.stdout.write(output);
@@ -963,6 +994,9 @@ async function executeDashboard(opts: DashboardOpts): Promise<void> {
963
994
  // Open stores once for the entire poll loop lifetime
964
995
  const stores = openDashboardStores(root);
965
996
 
997
+ // Create rolling event buffer (persisted across poll ticks)
998
+ const eventBuffer = new EventBuffer(100);
999
+
966
1000
  // Compute health thresholds once from config (reused across poll ticks)
967
1001
  const thresholds = {
968
1002
  staleMs: config.watchdog.staleThresholdMs,
@@ -984,7 +1018,7 @@ async function executeDashboard(opts: DashboardOpts): Promise<void> {
984
1018
 
985
1019
  // Poll loop
986
1020
  while (running) {
987
- const data = await loadDashboardData(root, stores, runId, thresholds);
1021
+ const data = await loadDashboardData(root, stores, runId, thresholds, eventBuffer);
988
1022
  renderDashboard(data, interval);
989
1023
  await Bun.sleep(interval);
990
1024
  }
@@ -15,6 +15,7 @@ import { checkDependencies } from "../doctor/dependencies.ts";
15
15
  import { checkEcosystem } from "../doctor/ecosystem.ts";
16
16
  import { checkLogs } from "../doctor/logs.ts";
17
17
  import { checkMergeQueue } from "../doctor/merge-queue.ts";
18
+ import { checkProviders } from "../doctor/providers.ts";
18
19
  import { checkStructure } from "../doctor/structure.ts";
19
20
  import type { DoctorCategory, DoctorCheck, DoctorCheckFn } from "../doctor/types.ts";
20
21
  import { checkVersion } from "../doctor/version.ts";
@@ -35,6 +36,7 @@ const ALL_CHECKS: Array<{ category: DoctorCategory; fn: DoctorCheckFn }> = [
35
36
  { category: "logs", fn: checkLogs },
36
37
  { category: "version", fn: checkVersion },
37
38
  { category: "ecosystem", fn: checkEcosystem },
39
+ { category: "providers", fn: checkProviders },
38
40
  ];
39
41
 
40
42
  /**
@@ -166,7 +168,7 @@ export function createDoctorCommand(options?: DoctorCommandOptions): Command {
166
168
  .option("--fix", "Attempt to auto-fix issues")
167
169
  .addHelpText(
168
170
  "after",
169
- "\nCategories: dependencies, structure, config, databases, consistency, agents, merge, logs, version, ecosystem",
171
+ "\nCategories: dependencies, structure, config, databases, consistency, agents, merge, logs, version, ecosystem, providers",
170
172
  )
171
173
  .action(
172
174
  async (opts: { json?: boolean; verbose?: boolean; category?: string; fix?: boolean }) => {