huddle-cli 0.1.1 → 0.1.3
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/CHANGELOG.md +38 -0
- package/QUICKSTART.md +34 -1
- package/README.md +3 -0
- package/dist/adapters/codex.js +3 -0
- package/dist/app/live.js +142 -0
- package/dist/app/page.js +701 -0
- package/dist/app/server.js +262 -0
- package/dist/args.js +18 -0
- package/dist/attach/connect.js +3 -3
- package/dist/attach/plug.js +8 -1
- package/dist/cli.js +26 -2
- package/dist/mcp/server.js +19 -0
- package/dist/orchestrator/brain.js +3 -1
- package/dist/orchestrator/budget.js +283 -0
- package/dist/orchestrator/busy.js +106 -0
- package/dist/orchestrator/council.js +10 -4
- package/dist/orchestrator/launcher.js +3 -1
- package/dist/orchestrator/learn.js +3 -7
- package/dist/orchestrator/planner.js +31 -4
- package/dist/orchestrator/qa.js +9 -1
- package/dist/orchestrator/scheduler.js +82 -16
- package/dist/platform.js +104 -0
- package/dist/room/mail.js +19 -6
- package/dist/room/server.js +53 -2
- package/dist/room/user.js +54 -0
- package/dist/ui/app.js +452 -69
- package/dist/ui/brief.js +494 -0
- package/dist/ui/dashboard.js +92 -21
- package/dist/ui/export.js +1194 -0
- package/dist/ui/narrator.js +56 -0
- package/dist/ui/story.js +118 -93
- package/dist/ui/talk.js +176 -0
- package/dist/ui/term.js +31 -0
- package/dist/ui/transcript.js +434 -0
- package/dist/ui/watch.js +4 -0
- package/package.json +1 -1
package/dist/ui/dashboard.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// It only reads: the Room's board and memory, the run's live stats, and the
|
|
12
12
|
// activity feed. Pressing q hands the terminal back to the prompt. Every row
|
|
13
13
|
// is cut to the screen's width — a row that wraps scrolls the whole frame.
|
|
14
|
-
import { C, dot, pad, truncate, width, AGENT_COLOR } from "./term.js";
|
|
14
|
+
import { BARS, C, bar, dot, pad, truncate, width, AGENT_COLOR } from "./term.js";
|
|
15
15
|
import { fmtDuration, fmtTokens } from "../adapters/usage.js";
|
|
16
16
|
import { taskViews } from "../room/progress.js";
|
|
17
17
|
import { getContract } from "../room/contract.js";
|
|
@@ -53,6 +53,76 @@ export function runPhase(db, busy) {
|
|
|
53
53
|
return "DONE";
|
|
54
54
|
return phase;
|
|
55
55
|
}
|
|
56
|
+
/** QA's own "QA round 2: check the build" rows: checks, not work to merge. */
|
|
57
|
+
export const isQaCheck = (t) => /^QA round \d+:/.test(t.title) && /"qa"/.test(t.prefers ?? "");
|
|
58
|
+
/** Merged / failed / all of the board's work tasks. */
|
|
59
|
+
export function taskCounts(tasks) {
|
|
60
|
+
const work = tasks.filter((t) => !isQaCheck(t));
|
|
61
|
+
return { done: work.filter((t) => t.status === "merged").length, total: work.length, failed: work.filter((t) => t.status === "failed").length };
|
|
62
|
+
}
|
|
63
|
+
const COUNCIL_STEP = { "council.proposal": 1, "council.critique": 2, "council.adopt": 2, "council.flaw": 2, "council.ratify": 4 };
|
|
64
|
+
/** The run's progress from the Room alone (huddle view, the ctrl-t view). */
|
|
65
|
+
export function runProgress(db, busy) {
|
|
66
|
+
const phase = runPhase(db, busy);
|
|
67
|
+
if (!db || phase === "WAITING")
|
|
68
|
+
return { phase, done: 0, total: 0, failed: 0 };
|
|
69
|
+
try {
|
|
70
|
+
const since = (db.currentSession()?.created_at ?? 1) - 1;
|
|
71
|
+
const p = { phase, ...taskCounts(db.listTasks()) };
|
|
72
|
+
if (phase === "COUNCIL")
|
|
73
|
+
p.step = COUNCIL_STEP[db.eventsOfType(Object.keys(COUNCIL_STEP), since, 1).at(-1)?.type ?? ""] ?? 1;
|
|
74
|
+
const rounds = db.eventsOfType(["qa.round"], since, 50).length;
|
|
75
|
+
if (rounds)
|
|
76
|
+
p.round = rounds;
|
|
77
|
+
return p;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return { phase, done: 0, total: 0, failed: 0 }; // a closed db
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The overall run bar, one line of at most `W` cells:
|
|
85
|
+
* BUILD ███████░░░░░ 6/9 tasks · 12m03s
|
|
86
|
+
* COUNCIL ██████░░░░ step 3/4 · 2m10s
|
|
87
|
+
* QA ████████████░ round 2 · 9/10 tasks · 1 failed · 31m
|
|
88
|
+
* ✓ DONE █████████████ 10/10 tasks · 41m
|
|
89
|
+
* The bar gives way first, then the word "tasks"; with bars off (NO_COLOR,
|
|
90
|
+
* piped) it's the words alone: "BUILD 6/9 tasks · 12m03s". "" while waiting.
|
|
91
|
+
*/
|
|
92
|
+
export function progressLine(p, ms, W, o = {}) {
|
|
93
|
+
if (p.phase === "WAITING")
|
|
94
|
+
return "";
|
|
95
|
+
const done = p.phase === "DONE";
|
|
96
|
+
const head = done ? (p.failed ? C.yellow("◎ DONE") : C.green("✓ DONE")) : C.bold(C.violet(p.phase));
|
|
97
|
+
const frac = p.phase === "COUNCIL" ? (p.step ?? 1) / 4 : p.total ? p.done / p.total : done ? 1 : null;
|
|
98
|
+
const tasks = (word) => (p.total ? `${p.done}/${p.total}${word ? " tasks" : ""}` : p.phase === "PLAN" ? "planning" : "");
|
|
99
|
+
const label = (word) => p.phase === "COUNCIL" ? `step ${p.step ?? 1}/4` : [p.phase === "QA" && p.round ? `round ${p.round}` : "", tasks(word)].filter(Boolean).join(" · ");
|
|
100
|
+
const text = (word) => [label(word), p.failed ? C.red(`${p.failed} failed`) : "", ms === null ? "" : C.faint(fmtDuration(ms))].filter(Boolean).join(C.faint(" · "));
|
|
101
|
+
const tone = done ? (p.failed ? "failed" : "done") : "run";
|
|
102
|
+
for (const word of [true, false]) {
|
|
103
|
+
const w = Math.min(o.barMax ?? 24, W - width(head) - width(text(word)) - 2);
|
|
104
|
+
if (w >= 4 && (o.on ?? BARS.on))
|
|
105
|
+
return `${head} ${bar(frac, w, { tone, on: o.on })} ${text(word)}`;
|
|
106
|
+
}
|
|
107
|
+
return truncate(`${head} ${text(W >= 40)}`, W);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* A task's mini bar and its number, the same everywhere: "████░░░░ 60%";
|
|
111
|
+
* never reported "········ —"; merged a full green bar and ✓; failed red.
|
|
112
|
+
* With bars off it's the number alone ("60%", "—", "✓").
|
|
113
|
+
*/
|
|
114
|
+
export function taskMeter(pct, w, status) {
|
|
115
|
+
const tone = status === "merged" ? "done" : status === "failed" ? "failed" : "run";
|
|
116
|
+
const num = status === "merged" ? C.green(" ✓") : pct === null ? C.faint(" —") : (status === "failed" ? C.red : C.violet)(`${String(Math.round(pct)).padStart(3)}%`);
|
|
117
|
+
const b = bar(pct === null ? null : pct / 100, w, { tone });
|
|
118
|
+
return b ? `${b} ${num}` : num;
|
|
119
|
+
}
|
|
120
|
+
/** "est 5m" for a task with an estimate, yellow "over est 5m" once it runs past it; "" with none. */
|
|
121
|
+
export function estHint(estS, ranMs) {
|
|
122
|
+
if (!estS || estS <= 0)
|
|
123
|
+
return "";
|
|
124
|
+
return ranMs > estS * 1000 ? C.yellow(`over est ${fmtDuration(estS * 1000)}`) : C.faint(`est ${fmtDuration(estS * 1000)}`);
|
|
125
|
+
}
|
|
56
126
|
const hexColor = (id) => C.hex(AGENT_COLOR[id.split("/").pop().split("#")[0]] ?? "#8A857C");
|
|
57
127
|
/** The shared screen's 📎/📌 rows in the view's glyphs (emoji are two cells wide and vary by font). */
|
|
58
128
|
const screenGlyphs = (l) => l.replace(/^📎/u, C.violet("+")).replace(/^📌/u, C.violet("◆"));
|
|
@@ -133,7 +203,6 @@ export class Dashboard {
|
|
|
133
203
|
const db = this.src.db();
|
|
134
204
|
const stats = this.src.stats();
|
|
135
205
|
const busy = !!stats && [...stats.agents.values()].some((a) => a.busySince);
|
|
136
|
-
const phase = runPhase(db, busy);
|
|
137
206
|
let goal = "";
|
|
138
207
|
try {
|
|
139
208
|
goal = db?.currentSession()?.goal ?? "";
|
|
@@ -144,19 +213,20 @@ export class Dashboard {
|
|
|
144
213
|
const tabs = ["agents", "plan", "board"]
|
|
145
214
|
.map((t, i) => (t === tab ? C.bold(C.violet(`[${i + 1} ${t}]`)) : C.faint(` ${i + 1} ${t} `)))
|
|
146
215
|
.join(" ");
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const right =
|
|
154
|
-
|
|
216
|
+
// The overall bar: the run's own live numbers when it's this process's run, else the Room's.
|
|
217
|
+
const prog = stats?.run ?? runProgress(db, busy);
|
|
218
|
+
const ms = stats ? (stats.endedAt ?? now) - stats.startedAt : null;
|
|
219
|
+
// On a narrow screen the live chip is just its dot: the bar needs the room.
|
|
220
|
+
const chip = prog.phase === "DONE" || prog.phase === "WAITING" ? "" : busy ? C.green(W >= 100 ? "● live" : "●") : C.faint(W >= 100 ? "○ between steps" : "○");
|
|
221
|
+
const head = ` ${C.violet("⬢")} ${C.bold("huddle")} ${tabs}`;
|
|
222
|
+
const right = prog.phase === "WAITING"
|
|
223
|
+
? C.faint("○ waiting for a run")
|
|
224
|
+
: [progressLine(prog, ms, Math.max(12, W - width(head) - width(chip) - 5), { barMax: W >= 110 ? 12 : 10 }), chip].filter(Boolean).join(C.faint(" · "));
|
|
155
225
|
// The goal takes whatever room is left between the tabs and the status.
|
|
156
|
-
const room = W - width(
|
|
226
|
+
const room = W - width(head) - width(right) - 4;
|
|
157
227
|
const goalS = goal && room >= 12 ? C.faint(truncate(goal.replace(/\s+/g, " "), room)) : "";
|
|
158
|
-
const gap = Math.max(1, W - width(
|
|
159
|
-
const top = goalS ? `${
|
|
228
|
+
const gap = Math.max(1, W - width(head) - width(goalS) - width(right) - 3);
|
|
229
|
+
const top = goalS ? `${head} ${goalS}${" ".repeat(gap)}${right}` : `${head}${" ".repeat(Math.max(1, W - width(head) - width(right) - 1))}${right}`;
|
|
160
230
|
const bodyH = H - 4;
|
|
161
231
|
const body = tab === "agents" ? this.agents(W, bodyH, stats, db, now) : tab === "plan" ? this.plan(W, bodyH, db) : this.board(W, bodyH, db);
|
|
162
232
|
const lastRoom = [...this.feed].reverse().find((a) => !a.agent);
|
|
@@ -174,8 +244,10 @@ export class Dashboard {
|
|
|
174
244
|
if (!ids.length)
|
|
175
245
|
return ["", C.faint(` ${this.src.empty ?? "no agents working — give Huddle a goal"}`)];
|
|
176
246
|
let views = [];
|
|
247
|
+
let ests = new Map();
|
|
177
248
|
try {
|
|
178
249
|
views = db ? taskViews(db, now) : [];
|
|
250
|
+
ests = new Map((db?.listTasks() ?? []).map((t) => [t.id, t.est_seconds]));
|
|
179
251
|
}
|
|
180
252
|
catch {
|
|
181
253
|
/* a closed db: no pinned task line */
|
|
@@ -186,13 +258,13 @@ export class Dashboard {
|
|
|
186
258
|
const paneH = Math.max(4, Math.floor(H / rows));
|
|
187
259
|
const out = [];
|
|
188
260
|
for (let r = 0; r < rows; r++) {
|
|
189
|
-
const panes = ids.slice(r * cols, r * cols + cols).map((id) => this.pane(id, paneW, paneH, stats?.agents.get(id), views, now));
|
|
261
|
+
const panes = ids.slice(r * cols, r * cols + cols).map((id) => this.pane(id, paneW, paneH, stats?.agents.get(id), views, now, ests));
|
|
190
262
|
for (let i = 0; i < paneH; i++)
|
|
191
263
|
out.push(panes.map((p) => p[i] ?? " ".repeat(paneW)).join(""));
|
|
192
264
|
}
|
|
193
265
|
return out;
|
|
194
266
|
}
|
|
195
|
-
pane(id, w, h, a, views, now) {
|
|
267
|
+
pane(id, w, h, a, views, now, ests) {
|
|
196
268
|
const color = hexColor(id);
|
|
197
269
|
const status = a?.busySince
|
|
198
270
|
? `${C.violet("working")} ${fmtDuration(now - a.busySince)}${a.liveTokens > 0 ? " · " + fmtTokens(a.liveTokens) + " tok" : ""}`
|
|
@@ -206,9 +278,10 @@ export class Dashboard {
|
|
|
206
278
|
const on = a?.busySince ? views.find((v) => v.owner === id && (v.status === "claimed" || v.status === "in_progress")) : undefined;
|
|
207
279
|
const pinned = [];
|
|
208
280
|
if (on) {
|
|
209
|
-
const
|
|
210
|
-
const said = on.last_note ? C.faint(` · note ${fmtDuration(on.last_update_s * 1000)} ago`) : "";
|
|
211
|
-
const
|
|
281
|
+
const meter = taskMeter(on.reported ? on.progress_pct : null, inner >= 70 ? 10 : 6);
|
|
282
|
+
const said = on.reported ? (on.last_note ? C.faint(` · note ${fmtDuration(on.last_update_s * 1000)} ago`) : "") : C.faint(" · no report yet");
|
|
283
|
+
const est = estHint(ests.get(on.id), (on.age_s ?? 0) * 1000);
|
|
284
|
+
const tail = ` ${meter}${said}${est ? C.faint(" · ") + est : ""}`;
|
|
212
285
|
pinned.push(`${C.violet("▸")} ${C.bold(truncate(on.title, Math.max(8, inner - 4 - width(tail))))}${tail}`);
|
|
213
286
|
}
|
|
214
287
|
const room = h - 2 - pinned.length;
|
|
@@ -269,12 +342,10 @@ export class Dashboard {
|
|
|
269
342
|
}
|
|
270
343
|
const bw = W >= 100 ? 14 : 8;
|
|
271
344
|
for (const t of views) {
|
|
272
|
-
const filled = Math.round((t.progress_pct / 100) * bw);
|
|
273
|
-
const barS = t.status === "merged" ? C.green("█".repeat(bw)) : C.violet("█".repeat(filled)) + C.faint("░".repeat(bw - filled));
|
|
274
345
|
// Never reported (no huddle_progress yet): say so instead of showing a made-up 0%.
|
|
275
346
|
const silent = ["claimed", "in_progress"].includes(t.status) && !t.reported;
|
|
276
347
|
const who = t.owner ? ` ${dot(t.owner)} ${C.faint(t.owner)}` : "";
|
|
277
|
-
left.push(` ${mark(t.status, !!t.owner)} ${
|
|
348
|
+
left.push(` ${mark(t.status, !!t.owner)} ${taskMeter(silent ? null : t.progress_pct, bw, t.status)} ${t.title}${who}`);
|
|
278
349
|
}
|
|
279
350
|
if (!views.length)
|
|
280
351
|
left.push(C.faint(" no tasks yet"));
|