@os-eco/overstory-cli 0.7.6 → 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.
- package/README.md +3 -1
- package/package.json +1 -1
- package/src/commands/dashboard.test.ts +101 -10
- package/src/commands/dashboard.ts +95 -61
- package/src/index.ts +1 -1
- package/src/runtimes/claude.test.ts +1 -1
- package/src/runtimes/codex.test.ts +741 -0
- package/src/runtimes/codex.ts +228 -0
- package/src/runtimes/pi.test.ts +1 -1
- package/src/runtimes/registry.test.ts +6 -6
- package/src/runtimes/registry.ts +2 -0
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@ Requires [Bun](https://bun.sh) v1.0+, git, and tmux. At least one supported agen
|
|
|
17
17
|
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (`claude` CLI)
|
|
18
18
|
- [Pi](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent) (`pi` CLI)
|
|
19
19
|
- [GitHub Copilot](https://github.com/features/copilot) (`copilot` CLI)
|
|
20
|
+
- [Codex](https://github.com/openai/codex) (`codex` CLI)
|
|
20
21
|
|
|
21
22
|
```bash
|
|
22
23
|
bun install -g @os-eco/overstory-cli
|
|
@@ -173,6 +174,7 @@ Overstory is runtime-agnostic. The `AgentRuntime` interface (`src/runtimes/types
|
|
|
173
174
|
| Claude Code | `claude` | `settings.local.json` hooks | Stable |
|
|
174
175
|
| Pi | `pi` | `.pi/extensions/` guard extension | Active development |
|
|
175
176
|
| Copilot | `copilot` | (none — `--allow-all-tools`) | Active development |
|
|
177
|
+
| Codex | `codex` | OS-level sandbox (Seatbelt/Landlock) | Active development |
|
|
176
178
|
|
|
177
179
|
## How It Works
|
|
178
180
|
|
|
@@ -269,7 +271,7 @@ overstory/
|
|
|
269
271
|
metrics/ SQLite metrics + pricing + transcript parsing
|
|
270
272
|
doctor/ Health check modules (11 checks)
|
|
271
273
|
insights/ Session insight analyzer for auto-expertise
|
|
272
|
-
runtimes/ AgentRuntime abstraction (registry + adapters: Claude, Pi, Copilot)
|
|
274
|
+
runtimes/ AgentRuntime abstraction (registry + adapters: Claude, Pi, Copilot, Codex)
|
|
273
275
|
tracker/ Pluggable task tracker (beads + seeds backends)
|
|
274
276
|
mulch/ mulch client (programmatic API + CLI wrapper)
|
|
275
277
|
e2e/ End-to-end lifecycle tests
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@os-eco/overstory-cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.7",
|
|
4
4
|
"description": "Multi-agent orchestration for AI coding agents — spawn workers in git worktrees via tmux, coordinate through SQLite mail, merge with tiered conflict resolution. Pluggable runtime adapters for Claude Code, Pi, and more.",
|
|
5
5
|
"author": "Jaymin West",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
computeAgentPanelHeight,
|
|
22
22
|
dashboardCommand,
|
|
23
23
|
dimBox,
|
|
24
|
+
EventBuffer,
|
|
24
25
|
filterAgentsByRun,
|
|
25
26
|
horizontalLine,
|
|
26
27
|
openDashboardStores,
|
|
@@ -242,28 +243,28 @@ describe("dimBox", () => {
|
|
|
242
243
|
|
|
243
244
|
describe("computeAgentPanelHeight", () => {
|
|
244
245
|
test("0 agents: clamps to minimum 8", () => {
|
|
245
|
-
// max(8, min(floor(30*0.
|
|
246
|
+
// max(8, min(floor(30*0.35)=10, 0+4)) = max(8, min(10,4)) = max(8,4) = 8
|
|
246
247
|
expect(computeAgentPanelHeight(30, 0)).toBe(8);
|
|
247
248
|
});
|
|
248
249
|
|
|
249
250
|
test("4 agents: still clamps to minimum 8", () => {
|
|
250
|
-
// max(8, min(
|
|
251
|
+
// max(8, min(10, 4+4)) = max(8, 8) = 8
|
|
251
252
|
expect(computeAgentPanelHeight(30, 4)).toBe(8);
|
|
252
253
|
});
|
|
253
254
|
|
|
254
|
-
test("20 agents with height 30: clamps to floor(height*0.
|
|
255
|
-
// max(8, min(
|
|
256
|
-
expect(computeAgentPanelHeight(30, 20)).toBe(
|
|
255
|
+
test("20 agents with height 30: clamps to floor(height*0.35)", () => {
|
|
256
|
+
// max(8, min(floor(30*0.35)=10, 24)) = max(8,10) = 10
|
|
257
|
+
expect(computeAgentPanelHeight(30, 20)).toBe(10);
|
|
257
258
|
});
|
|
258
259
|
|
|
259
260
|
test("10 agents with height 30: grows with agent count", () => {
|
|
260
|
-
// max(8, min(
|
|
261
|
-
expect(computeAgentPanelHeight(30, 10)).toBe(
|
|
261
|
+
// max(8, min(10, 14)) = max(8,10) = 10
|
|
262
|
+
expect(computeAgentPanelHeight(30, 10)).toBe(10);
|
|
262
263
|
});
|
|
263
264
|
|
|
264
|
-
test("small height: respects
|
|
265
|
-
// height=20: max(8, min(
|
|
266
|
-
expect(computeAgentPanelHeight(20, 20)).toBe(
|
|
265
|
+
test("small height: respects 35% cap", () => {
|
|
266
|
+
// height=20: max(8, min(floor(20*0.35)=7, 24)) = max(8,7) = 8
|
|
267
|
+
expect(computeAgentPanelHeight(20, 20)).toBe(8);
|
|
267
268
|
});
|
|
268
269
|
});
|
|
269
270
|
|
|
@@ -302,6 +303,7 @@ function makeDashboardData(
|
|
|
302
303
|
metrics: { totalSessions: 0, avgDuration: 0, byCapability: {} },
|
|
303
304
|
tasks: overrides.tasks ?? [],
|
|
304
305
|
recentEvents: (overrides.recentEvents as never[]) ?? [],
|
|
306
|
+
feedColorMap: new Map(),
|
|
305
307
|
};
|
|
306
308
|
}
|
|
307
309
|
|
|
@@ -366,6 +368,7 @@ describe("renderFeedPanel", () => {
|
|
|
366
368
|
const data = makeDashboardData({ recentEvents: [] });
|
|
367
369
|
const out = renderFeedPanel(data, 1, 80, 8, 1);
|
|
368
370
|
expect(out).toContain("Feed");
|
|
371
|
+
expect(out).toContain("(live)");
|
|
369
372
|
});
|
|
370
373
|
|
|
371
374
|
test("renders event agent name when events are present", () => {
|
|
@@ -554,6 +557,94 @@ describe("closeDashboardStores", () => {
|
|
|
554
557
|
});
|
|
555
558
|
});
|
|
556
559
|
|
|
560
|
+
describe("EventBuffer", () => {
|
|
561
|
+
let tempDir: string;
|
|
562
|
+
|
|
563
|
+
beforeEach(async () => {
|
|
564
|
+
tempDir = await mkdtemp(join(tmpdir(), "event-buffer-test-"));
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
afterEach(async () => {
|
|
568
|
+
await cleanupTempDir(tempDir);
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
function makeEvent(agentName: string) {
|
|
572
|
+
return {
|
|
573
|
+
agentName,
|
|
574
|
+
eventType: "tool_end" as const,
|
|
575
|
+
level: "info" as const,
|
|
576
|
+
runId: null,
|
|
577
|
+
sessionId: null,
|
|
578
|
+
toolName: null,
|
|
579
|
+
toolArgs: null,
|
|
580
|
+
toolDurationMs: null,
|
|
581
|
+
data: null,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
test("starts empty", () => {
|
|
586
|
+
const buf = new EventBuffer();
|
|
587
|
+
expect(buf.size).toBe(0);
|
|
588
|
+
expect(buf.getEvents()).toEqual([]);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
test("poll adds events from event store", async () => {
|
|
592
|
+
const overstoryDir = join(tempDir, ".overstory");
|
|
593
|
+
await mkdir(overstoryDir, { recursive: true });
|
|
594
|
+
const store = createEventStore(join(overstoryDir, "events.db"));
|
|
595
|
+
store.insert(makeEvent("agent-a"));
|
|
596
|
+
|
|
597
|
+
const buf = new EventBuffer();
|
|
598
|
+
buf.poll(store);
|
|
599
|
+
expect(buf.size).toBe(1);
|
|
600
|
+
store.close();
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
test("deduplicates by lastSeenId (double poll returns same count)", async () => {
|
|
604
|
+
const overstoryDir = join(tempDir, ".overstory");
|
|
605
|
+
await mkdir(overstoryDir, { recursive: true });
|
|
606
|
+
const store = createEventStore(join(overstoryDir, "events.db"));
|
|
607
|
+
store.insert(makeEvent("agent-a"));
|
|
608
|
+
|
|
609
|
+
const buf = new EventBuffer();
|
|
610
|
+
buf.poll(store);
|
|
611
|
+
buf.poll(store); // second poll should not duplicate
|
|
612
|
+
expect(buf.size).toBe(1);
|
|
613
|
+
store.close();
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
test("trims to maxSize keeping most recent events", async () => {
|
|
617
|
+
const overstoryDir = join(tempDir, ".overstory");
|
|
618
|
+
await mkdir(overstoryDir, { recursive: true });
|
|
619
|
+
const store = createEventStore(join(overstoryDir, "events.db"));
|
|
620
|
+
for (let i = 0; i < 5; i++) {
|
|
621
|
+
store.insert(makeEvent(`agent-${i}`));
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const buf = new EventBuffer(3);
|
|
625
|
+
buf.poll(store);
|
|
626
|
+
expect(buf.size).toBe(3);
|
|
627
|
+
store.close();
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
test("builds color map across polls", async () => {
|
|
631
|
+
const overstoryDir = join(tempDir, ".overstory");
|
|
632
|
+
await mkdir(overstoryDir, { recursive: true });
|
|
633
|
+
const store = createEventStore(join(overstoryDir, "events.db"));
|
|
634
|
+
store.insert(makeEvent("agent-x"));
|
|
635
|
+
|
|
636
|
+
const buf = new EventBuffer();
|
|
637
|
+
buf.poll(store);
|
|
638
|
+
expect(buf.getColorMap().has("agent-x")).toBe(true);
|
|
639
|
+
|
|
640
|
+
store.insert(makeEvent("agent-y"));
|
|
641
|
+
buf.poll(store);
|
|
642
|
+
expect(buf.getColorMap().has("agent-x")).toBe(true);
|
|
643
|
+
expect(buf.getColorMap().has("agent-y")).toBe(true);
|
|
644
|
+
store.close();
|
|
645
|
+
});
|
|
646
|
+
});
|
|
647
|
+
|
|
557
648
|
// Type check: DashboardStores includes eventStore
|
|
558
649
|
test("DashboardStores type includes eventStore field", () => {
|
|
559
650
|
const stores: DashboardStores = {
|
|
@@ -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.
|
|
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.
|
|
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
|
|
499
|
+
// Load recent events via incremental buffer (or fallback to empty)
|
|
454
500
|
let recentEvents: StoredEvent[] = [];
|
|
455
|
-
|
|
501
|
+
let feedColorMap: Map<string, (s: string) => string> = new Map();
|
|
502
|
+
if (eventBuffer && stores.eventStore) {
|
|
456
503
|
try {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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 =
|
|
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")} (
|
|
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 =
|
|
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
|
-
|
|
737
|
-
|
|
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
|
-
|
|
791
|
-
|
|
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
|
|
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 +
|
|
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 +
|
|
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
|
|
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
|
-
//
|
|
894
|
-
const
|
|
895
|
-
const
|
|
896
|
-
const
|
|
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
|
-
|
|
899
|
-
|
|
900
|
-
const feedHeight = agentPanelHeight - rightHalf;
|
|
937
|
+
const feedWidth = Math.floor(width * 0.6);
|
|
938
|
+
output += renderFeedPanel(data, 1, feedWidth, middleHeight, middleStart);
|
|
901
939
|
|
|
902
|
-
|
|
903
|
-
|
|
940
|
+
const taskWidth = width - feedWidth;
|
|
941
|
+
const taskStartCol = feedWidth + 1;
|
|
942
|
+
output += renderTasksPanel(data, taskStartCol, taskWidth, middleHeight, middleStart);
|
|
904
943
|
|
|
905
|
-
//
|
|
906
|
-
|
|
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
|
-
|
|
909
|
-
|
|
949
|
+
const mergeStartCol = mailWidth + 1;
|
|
950
|
+
const mergeWidth = width - mailWidth;
|
|
951
|
+
output += renderMergeQueuePanel(
|
|
910
952
|
data,
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
953
|
+
mergeWidth,
|
|
954
|
+
compactPanelHeight,
|
|
955
|
+
compactStart,
|
|
956
|
+
mergeStartCol,
|
|
915
957
|
);
|
|
916
958
|
|
|
917
|
-
//
|
|
918
|
-
const
|
|
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
|
}
|
package/src/index.ts
CHANGED
|
@@ -45,7 +45,7 @@ import { OverstoryError, WorktreeError } from "./errors.ts";
|
|
|
45
45
|
import { jsonError } from "./json.ts";
|
|
46
46
|
import { brand, chalk, muted, setQuiet } from "./logging/color.ts";
|
|
47
47
|
|
|
48
|
-
export const VERSION = "0.7.
|
|
48
|
+
export const VERSION = "0.7.7";
|
|
49
49
|
|
|
50
50
|
const rawArgs = process.argv.slice(2);
|
|
51
51
|
|
|
@@ -651,7 +651,7 @@ describe("ClaudeRuntime integration: registry resolves 'claude' as default", ()
|
|
|
651
651
|
|
|
652
652
|
test("getRuntime rejects unknown runtimes", async () => {
|
|
653
653
|
const { getRuntime } = await import("./registry.ts");
|
|
654
|
-
expect(() => getRuntime("codex")).toThrow('Unknown runtime: "codex"');
|
|
655
654
|
expect(() => getRuntime("opencode")).toThrow('Unknown runtime: "opencode"');
|
|
655
|
+
expect(() => getRuntime("aider")).toThrow('Unknown runtime: "aider"');
|
|
656
656
|
});
|
|
657
657
|
});
|