huddle-cli 0.1.0 → 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 +69 -0
- package/QUICKSTART.md +135 -0
- package/README.md +26 -4
- package/dist/adapters/amp.js +1 -1
- package/dist/adapters/antigravity.js +2 -0
- package/dist/adapters/claude.js +19 -3
- package/dist/adapters/codex.js +9 -1
- package/dist/adapters/cursor.js +12 -5
- package/dist/adapters/custom.js +4 -2
- package/dist/adapters/droid.js +1 -0
- package/dist/adapters/goose.js +2 -0
- package/dist/adapters/index.js +44 -18
- package/dist/adapters/kiro.js +1 -1
- package/dist/adapters/opencode.js +1 -1
- 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 +7 -4
- package/dist/attach/guard.js +21 -2
- package/dist/attach/live.js +5 -1
- package/dist/attach/plug.js +21 -3
- package/dist/attach/screen.js +150 -0
- package/dist/attach/session.js +7 -2
- package/dist/cli.js +134 -42
- package/dist/mcp/context-tools.js +79 -0
- package/dist/mcp/server.js +57 -11
- package/dist/orchestrator/ask.js +13 -10
- package/dist/orchestrator/brain.js +16 -4
- package/dist/orchestrator/budget.js +283 -0
- package/dist/orchestrator/busy.js +106 -0
- package/dist/orchestrator/council.js +434 -87
- package/dist/orchestrator/goal.js +196 -0
- package/dist/orchestrator/handoff.js +2 -2
- package/dist/orchestrator/launcher.js +31 -13
- package/dist/orchestrator/learn.js +101 -0
- package/dist/orchestrator/planner.js +54 -9
- package/dist/orchestrator/qa.js +71 -15
- package/dist/orchestrator/radar.js +719 -0
- package/dist/orchestrator/scheduler.js +575 -50
- package/dist/orchestrator/takeover.js +155 -0
- package/dist/platform.js +377 -0
- package/dist/room/context.js +1285 -0
- package/dist/room/contract.js +132 -0
- package/dist/room/db.js +79 -16
- package/dist/room/followup.js +233 -0
- package/dist/room/identity.js +2 -2
- package/dist/room/lessons.js +275 -0
- package/dist/room/live.js +75 -20
- package/dist/room/mail.js +225 -0
- package/dist/room/progress.js +40 -1
- package/dist/room/server.js +220 -39
- package/dist/room/tunnel.js +6 -5
- package/dist/room/user.js +54 -0
- package/dist/sandbox/sync.js +4 -4
- package/dist/sandbox/worktree.js +145 -14
- package/dist/ui/app.js +963 -159
- package/dist/ui/brief.js +494 -0
- package/dist/ui/catalog.js +62 -24
- package/dist/ui/dashboard.js +269 -55
- package/dist/ui/doctor.js +15 -11
- package/dist/ui/export.js +1194 -0
- package/dist/ui/lessons.js +81 -0
- package/dist/ui/narrator.js +56 -0
- package/dist/ui/paste.js +515 -0
- package/dist/ui/remote.js +11 -5
- package/dist/ui/setup.js +152 -0
- package/dist/ui/story.js +716 -94
- package/dist/ui/talk.js +176 -0
- package/dist/ui/term.js +182 -16
- package/dist/ui/transcript.js +434 -0
- package/dist/ui/watch.js +149 -48
- package/package.json +6 -3
package/dist/ui/dashboard.js
CHANGED
|
@@ -1,18 +1,131 @@
|
|
|
1
1
|
// The live view — the full-screen dashboard from the launch film.
|
|
2
2
|
//
|
|
3
|
-
// ⬢ huddle
|
|
4
|
-
//
|
|
5
|
-
// │ ▸ Add greet(name) with tests
|
|
6
|
-
// │ plans: CommonJS greet.js …
|
|
7
|
-
//
|
|
8
|
-
// ◆
|
|
3
|
+
// ⬢ huddle 1 agents 2 plan 3 board BUILD · ● live · ⏱ 1m12s
|
|
4
|
+
// ╭─ ● claude working 40s · 31k tok ──────╮╭─ ● codex idle · 1 merged ───╮
|
|
5
|
+
// │ ▸ Add greet(name) with tests 60% ││ ✓ merged Add farewell… │
|
|
6
|
+
// │ plans: CommonJS greet.js … ││ │
|
|
7
|
+
// ╰────────────────────────────────────────╯╰───────────────────────────────╯
|
|
8
|
+
// ◆ codex → claude: what shape is the Order type?
|
|
9
9
|
// 1 2 3 switch · q back to the prompt · ctrl-c stop agents
|
|
10
10
|
//
|
|
11
11
|
// It only reads: the Room's board and memory, the run's live stats, and the
|
|
12
|
-
// activity feed. Pressing q hands the terminal back to the prompt.
|
|
13
|
-
|
|
12
|
+
// activity feed. Pressing q hands the terminal back to the prompt. Every row
|
|
13
|
+
// is cut to the screen's width — a row that wraps scrolls the whole frame.
|
|
14
|
+
import { BARS, C, bar, dot, pad, truncate, width, AGENT_COLOR } from "./term.js";
|
|
14
15
|
import { fmtDuration, fmtTokens } from "../adapters/usage.js";
|
|
16
|
+
import { taskViews } from "../room/progress.js";
|
|
17
|
+
import { getContract } from "../room/contract.js";
|
|
18
|
+
import { screenPanel } from "../room/context.js";
|
|
19
|
+
const PHASE_OF = {
|
|
20
|
+
"council.proposal": "COUNCIL",
|
|
21
|
+
"council.critique": "COUNCIL",
|
|
22
|
+
"council.adopt": "COUNCIL",
|
|
23
|
+
"council.flaw": "COUNCIL",
|
|
24
|
+
"council.ratify": "COUNCIL",
|
|
25
|
+
"council.plan": "PLAN",
|
|
26
|
+
"plan.made": "PLAN",
|
|
27
|
+
"contract.set": "PLAN",
|
|
28
|
+
"run.start": "BUILD",
|
|
29
|
+
"qa.round": "QA",
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* The run's phase: the latest phase-marking event since the session began,
|
|
33
|
+
* and DONE once every task on the board is merged or failed and no agent is busy.
|
|
34
|
+
*/
|
|
35
|
+
export function runPhase(db, busy) {
|
|
36
|
+
if (!db)
|
|
37
|
+
return "WAITING";
|
|
38
|
+
let tasks = [];
|
|
39
|
+
let since = 0;
|
|
40
|
+
try {
|
|
41
|
+
tasks = db.listTasks();
|
|
42
|
+
since = (db.currentSession()?.created_at ?? 1) - 1;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return "WAITING"; // a closed db
|
|
46
|
+
}
|
|
47
|
+
const last = db.eventsOfType(Object.keys(PHASE_OF), since, 1).at(-1);
|
|
48
|
+
let phase = last ? PHASE_OF[last.type] : "WAITING";
|
|
49
|
+
// A QA round's fix tasks are BUILD work, but the run is still in QA.
|
|
50
|
+
if (phase !== "QA" && phase !== "WAITING" && db.eventsOfType(["qa.round"], since, 1).length)
|
|
51
|
+
phase = "QA";
|
|
52
|
+
if (!busy && tasks.length && tasks.every((t) => t.status === "merged" || t.status === "failed"))
|
|
53
|
+
return "DONE";
|
|
54
|
+
return phase;
|
|
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
|
+
}
|
|
15
126
|
const hexColor = (id) => C.hex(AGENT_COLOR[id.split("/").pop().split("#")[0]] ?? "#8A857C");
|
|
127
|
+
/** The shared screen's 📎/📌 rows in the view's glyphs (emoji are two cells wide and vary by font). */
|
|
128
|
+
const screenGlyphs = (l) => l.replace(/^📎/u, C.violet("+")).replace(/^📌/u, C.violet("◆"));
|
|
16
129
|
export class Dashboard {
|
|
17
130
|
src;
|
|
18
131
|
tab = "agents";
|
|
@@ -82,105 +195,206 @@ export class Dashboard {
|
|
|
82
195
|
render() {
|
|
83
196
|
if (!this.open)
|
|
84
197
|
return;
|
|
85
|
-
const
|
|
86
|
-
|
|
198
|
+
const lines = this.frame(process.stdout.columns || 100, process.stdout.rows || 30);
|
|
199
|
+
process.stdout.write("\x1b[H" + lines.map((l) => "\x1b[2K" + l).join("\n"));
|
|
200
|
+
}
|
|
201
|
+
/** One full screen, `H` rows each at most `W` cells (also how tests and samples render it). */
|
|
202
|
+
frame(W, H, tab = this.tab, now = Date.now()) {
|
|
87
203
|
const db = this.src.db();
|
|
88
204
|
const stats = this.src.stats();
|
|
89
|
-
const
|
|
205
|
+
const busy = !!stats && [...stats.agents.values()].some((a) => a.busySince);
|
|
206
|
+
let goal = "";
|
|
207
|
+
try {
|
|
208
|
+
goal = db?.currentSession()?.goal ?? "";
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
/* a closed db */
|
|
212
|
+
}
|
|
90
213
|
const tabs = ["agents", "plan", "board"]
|
|
91
|
-
.map((t, i) => (t ===
|
|
214
|
+
.map((t, i) => (t === tab ? C.bold(C.violet(`[${i + 1} ${t}]`)) : C.faint(` ${i + 1} ${t} `)))
|
|
92
215
|
.join(" ");
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
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(" · "));
|
|
225
|
+
// The goal takes whatever room is left between the tabs and the status.
|
|
226
|
+
const room = W - width(head) - width(right) - 4;
|
|
227
|
+
const goalS = goal && room >= 12 ? C.faint(truncate(goal.replace(/\s+/g, " "), room)) : "";
|
|
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}`;
|
|
97
230
|
const bodyH = H - 4;
|
|
98
|
-
const body =
|
|
231
|
+
const body = tab === "agents" ? this.agents(W, bodyH, stats, db, now) : tab === "plan" ? this.plan(W, bodyH, db) : this.board(W, bodyH, db);
|
|
99
232
|
const lastRoom = [...this.feed].reverse().find((a) => !a.agent);
|
|
100
|
-
const strip = lastRoom ? ` ${
|
|
101
|
-
const hint = C.faint(` ${this.src.hint ?? "1 2 3 switch · q back to the prompt · ctrl-c stop agents"}
|
|
102
|
-
const lines = [
|
|
103
|
-
while (lines.length < H)
|
|
104
|
-
lines.
|
|
105
|
-
|
|
233
|
+
const strip = lastRoom ? ` ${lastRoom.text}` : C.faint(" the room is quiet");
|
|
234
|
+
const hint = C.faint(` ${this.src.hint ?? "1 2 3 switch · q back to the prompt · ctrl-c stop agents"} `) + this.src.modeChip();
|
|
235
|
+
const lines = [top, C.faint("─".repeat(W)), ...body.slice(0, bodyH)];
|
|
236
|
+
while (lines.length < H - 2)
|
|
237
|
+
lines.push("");
|
|
238
|
+
lines.push(strip, hint);
|
|
239
|
+
return lines.slice(0, H).map((l) => truncate(l.replace(/[\r\n]+/g, " "), W));
|
|
106
240
|
}
|
|
107
241
|
/** Tab 1: a pane per agent — what each one is doing right now. */
|
|
108
|
-
agents(W, H, stats) {
|
|
242
|
+
agents(W, H, stats, db, now) {
|
|
109
243
|
const ids = stats ? [...stats.agents.keys()] : [...new Set(this.feed.filter((a) => a.agent).map((a) => a.agent))];
|
|
110
244
|
if (!ids.length)
|
|
111
|
-
return [C.faint("
|
|
245
|
+
return ["", C.faint(` ${this.src.empty ?? "no agents working — give Huddle a goal"}`)];
|
|
246
|
+
let views = [];
|
|
247
|
+
let ests = new Map();
|
|
248
|
+
try {
|
|
249
|
+
views = db ? taskViews(db, now) : [];
|
|
250
|
+
ests = new Map((db?.listTasks() ?? []).map((t) => [t.id, t.est_seconds]));
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
/* a closed db: no pinned task line */
|
|
254
|
+
}
|
|
112
255
|
const cols = W >= 110 && ids.length > 1 ? 2 : 1;
|
|
113
256
|
const rows = Math.ceil(ids.length / cols);
|
|
114
257
|
const paneW = Math.floor(W / cols);
|
|
115
258
|
const paneH = Math.max(4, Math.floor(H / rows));
|
|
116
259
|
const out = [];
|
|
117
260
|
for (let r = 0; r < rows; r++) {
|
|
118
|
-
const panes = ids.slice(r * cols, r * cols + cols).map((id) => this.pane(id, paneW, paneH, stats?.agents.get(id)));
|
|
261
|
+
const panes = ids.slice(r * cols, r * cols + cols).map((id) => this.pane(id, paneW, paneH, stats?.agents.get(id), views, now, ests));
|
|
119
262
|
for (let i = 0; i < paneH; i++)
|
|
120
263
|
out.push(panes.map((p) => p[i] ?? " ".repeat(paneW)).join(""));
|
|
121
264
|
}
|
|
122
265
|
return out;
|
|
123
266
|
}
|
|
124
|
-
pane(id, w, h, a) {
|
|
267
|
+
pane(id, w, h, a, views, now, ests) {
|
|
125
268
|
const color = hexColor(id);
|
|
126
269
|
const status = a?.busySince
|
|
127
|
-
? `${C.violet("working")} ${fmtDuration(
|
|
128
|
-
: a && (a.tasks || a.reviews)
|
|
129
|
-
? C.green(`
|
|
270
|
+
? `${C.violet("working")} ${fmtDuration(now - a.busySince)}${a.liveTokens > 0 ? " · " + fmtTokens(a.liveTokens) + " tok" : ""}`
|
|
271
|
+
: a && (a.tasks || a.reviews || a.qa)
|
|
272
|
+
? C.green(`idle · ${a.tasks} merged${a.reviews ? ` · ${a.reviews} review${a.reviews === 1 ? "" : "s"}` : ""}${a.qa ? ` · ${a.qa} QA` : ""}`)
|
|
130
273
|
: C.faint("idle");
|
|
131
|
-
const title = ` ${dot(id)} ${C.bold(id)} ${status} `;
|
|
132
274
|
const inner = w - 2;
|
|
275
|
+
const title = truncate(` ${dot(id)} ${C.bold(id)} ${status} `, inner - 1);
|
|
133
276
|
const topLine = color("╭─") + title + color("─".repeat(Math.max(0, inner - 1 - width(title))) + "╮");
|
|
134
|
-
|
|
277
|
+
// The task it's on stays pinned at the top: title, %, how long since it last said anything.
|
|
278
|
+
const on = a?.busySince ? views.find((v) => v.owner === id && (v.status === "claimed" || v.status === "in_progress")) : undefined;
|
|
279
|
+
const pinned = [];
|
|
280
|
+
if (on) {
|
|
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 : ""}`;
|
|
285
|
+
pinned.push(`${C.violet("▸")} ${C.bold(truncate(on.title, Math.max(8, inner - 4 - width(tail))))}${tail}`);
|
|
286
|
+
}
|
|
287
|
+
const room = h - 2 - pinned.length;
|
|
288
|
+
const lines = [...pinned, ...this.feed.filter((x) => x.agent === id).slice(-room).map((x) => x.text)];
|
|
135
289
|
const body = Array.from({ length: h - 2 }, (_, i) => {
|
|
136
|
-
const l = lines[i] ? " " + truncate(lines[i]
|
|
290
|
+
const l = lines[i] ? " " + truncate(lines[i], inner - 2) : "";
|
|
137
291
|
return color("│") + pad(l, inner) + color("│");
|
|
138
292
|
});
|
|
139
293
|
return [topLine, ...body, color("╰" + "─".repeat(inner) + "╯")];
|
|
140
294
|
}
|
|
141
|
-
/** Tab 2: the plan
|
|
295
|
+
/** Tab 2: the plan, and the room talking (side by side when there's room, stacked when not). */
|
|
142
296
|
plan(W, H, db) {
|
|
143
|
-
const leftW = Math.floor(W * 0.55);
|
|
144
297
|
const left = [C.violet(" PLAN")];
|
|
145
|
-
|
|
298
|
+
let tasks = [];
|
|
299
|
+
try {
|
|
300
|
+
tasks = db?.listTasks() ?? [];
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
/* a closed db */
|
|
304
|
+
}
|
|
146
305
|
const idx = new Map(tasks.map((t, i) => [t.id, i + 1]));
|
|
147
306
|
tasks.forEach((t, i) => {
|
|
148
307
|
const deps = JSON.parse(t.depends_on || "[]");
|
|
149
308
|
const after = deps.length ? C.faint(` after ${deps.map((d) => idx.get(d)).join(",")}`) : "";
|
|
150
309
|
const who = t.claimed_by ? ` ${dot(t.claimed_by)} ${C.faint(t.claimed_by)}` : "";
|
|
151
|
-
left.push(` ${C.faint(String(i + 1) + ".")} ${t.title}${after}${who}`);
|
|
152
|
-
left.push(C.faint(` ${t.status}`));
|
|
310
|
+
left.push(` ${mark(t.status, !!t.claimed_by)} ${C.faint(String(i + 1).padStart(2) + ".")} ${t.title}${after}${who}`);
|
|
153
311
|
});
|
|
154
312
|
if (!tasks.length)
|
|
155
313
|
left.push(C.faint(" no plan yet"));
|
|
314
|
+
let contract = null;
|
|
315
|
+
try {
|
|
316
|
+
contract = db ? getContract(db) : null;
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
/* a closed db: no contract line */
|
|
320
|
+
}
|
|
321
|
+
if (contract && (contract.text || contract.amendments.length)) {
|
|
322
|
+
left.push("", `${C.violet(" CONTRACT")} ${C.faint(`v${contract.version} · ${contract.amendments.length} amendment(s)`)}`);
|
|
323
|
+
for (const a of contract.amendments.slice(-3))
|
|
324
|
+
left.push(C.faint(` v${a.version} ${a.by}: ${a.section}: ${a.change}`));
|
|
325
|
+
}
|
|
156
326
|
const right = [C.violet(" ROOM") + C.faint(" · agents & people talking")];
|
|
157
327
|
for (const a of this.feed.filter((x) => !x.agent).slice(-(H - 1)))
|
|
158
328
|
right.push(" " + a.text);
|
|
159
|
-
|
|
329
|
+
if (right.length === 1)
|
|
330
|
+
right.push(C.faint(" nothing said yet"));
|
|
331
|
+
return columns(left, right, W, H, true);
|
|
160
332
|
}
|
|
161
|
-
/** Tab 3: progress
|
|
333
|
+
/** Tab 3: progress, and the shared screen + memory. */
|
|
162
334
|
board(W, H, db) {
|
|
163
|
-
const leftW = Math.floor(W * 0.55);
|
|
164
335
|
const left = [C.violet(" BOARD") + C.faint(" · live progress")];
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
336
|
+
let views = [];
|
|
337
|
+
try {
|
|
338
|
+
views = db ? taskViews(db) : [];
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
/* a closed db */
|
|
342
|
+
}
|
|
343
|
+
const bw = W >= 100 ? 14 : 8;
|
|
344
|
+
for (const t of views) {
|
|
345
|
+
// Never reported (no huddle_progress yet): say so instead of showing a made-up 0%.
|
|
346
|
+
const silent = ["claimed", "in_progress"].includes(t.status) && !t.reported;
|
|
347
|
+
const who = t.owner ? ` ${dot(t.owner)} ${C.faint(t.owner)}` : "";
|
|
348
|
+
left.push(` ${mark(t.status, !!t.owner)} ${taskMeter(silent ? null : t.progress_pct, bw, t.status)} ${t.title}${who}`);
|
|
349
|
+
}
|
|
350
|
+
if (!views.length)
|
|
351
|
+
left.push(C.faint(" no tasks yet"));
|
|
352
|
+
const right = [];
|
|
353
|
+
// The shared screen: what the user put up, and the notes pinned on it.
|
|
354
|
+
let screen = [];
|
|
355
|
+
let memory = [];
|
|
356
|
+
try {
|
|
357
|
+
screen = db ? screenPanel(db, Math.min(8, Math.floor(H / 2))) : [];
|
|
358
|
+
memory = db?.recall() ?? [];
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
/* a closed db */
|
|
362
|
+
}
|
|
363
|
+
if (screen.length)
|
|
364
|
+
right.push(C.violet(" SCREEN") + C.faint(" · pastes & files every agent reads"), ...screen.map((l) => " " + screenGlyphs(l)), "");
|
|
365
|
+
right.push(C.violet(" SHARED MEMORY") + C.faint(" · everyone reads & writes"));
|
|
366
|
+
for (const m of memory.slice(0, H - 1)) {
|
|
367
|
+
right.push(` ${C.violet("◆")} ${m.author ? C.faint(m.author + ": ") : ""}${m.body}`);
|
|
177
368
|
if (m.why)
|
|
178
369
|
right.push(C.faint(` why: ${m.why}`));
|
|
179
370
|
}
|
|
180
|
-
|
|
371
|
+
if (!memory.length)
|
|
372
|
+
right.push(C.faint(" nothing yet"));
|
|
373
|
+
return columns(left, right, W, H);
|
|
181
374
|
}
|
|
182
375
|
}
|
|
183
|
-
|
|
376
|
+
/** A task's state as one glyph, the same ones the run's output uses. */
|
|
377
|
+
function mark(status, claimed) {
|
|
378
|
+
if (status === "merged")
|
|
379
|
+
return C.green("✓");
|
|
380
|
+
if (status === "failed")
|
|
381
|
+
return C.red("✗");
|
|
382
|
+
if (status === "needs_you")
|
|
383
|
+
return C.yellow("!");
|
|
384
|
+
if (status === "review")
|
|
385
|
+
return C.violet("◎");
|
|
386
|
+
return claimed ? C.violet("▸") : C.faint("○");
|
|
387
|
+
}
|
|
388
|
+
/** Two columns side by side on a wide screen; on a narrow one, the left on top of the right. */
|
|
389
|
+
function columns(left, right, W, H, latest = false) {
|
|
390
|
+
if (W < 100) {
|
|
391
|
+
const top = left.slice(0, Math.max(3, Math.min(left.length, H - Math.min(right.length, Math.floor(H / 2)) - 1)));
|
|
392
|
+
const n = Math.max(0, H - top.length - 1);
|
|
393
|
+
// A feed keeps its newest lines (its header stays).
|
|
394
|
+
const rest = latest && right.length > n ? [right[0], ...right.slice(1).slice(-(n - 1))].slice(0, n) : right.slice(0, n);
|
|
395
|
+
return [...top, C.faint("─".repeat(W)), ...rest].map((l) => truncate(l, W));
|
|
396
|
+
}
|
|
397
|
+
const leftW = Math.floor(W * 0.55);
|
|
184
398
|
const out = [];
|
|
185
399
|
for (let i = 0; i < H; i++) {
|
|
186
400
|
const l = truncate(left[i] ?? "", leftW - 1);
|
package/dist/ui/doctor.js
CHANGED
|
@@ -6,7 +6,8 @@ import { spawnSync } from "node:child_process";
|
|
|
6
6
|
import { LEGACY } from "../adapters/index.js";
|
|
7
7
|
import { packageInfo } from "../preflight.js";
|
|
8
8
|
import { sqliteStatus } from "../room/sqlite.js";
|
|
9
|
-
import { connectAgents } from "./app.js";
|
|
9
|
+
import { connectAgents, installLine } from "./app.js";
|
|
10
|
+
import { gitFix } from "../platform.js";
|
|
10
11
|
import { C, dot } from "./term.js";
|
|
11
12
|
export async function runDoctor() {
|
|
12
13
|
let blocked = false;
|
|
@@ -23,7 +24,7 @@ export async function runDoctor() {
|
|
|
23
24
|
if (gitNum === null) {
|
|
24
25
|
blocked = true;
|
|
25
26
|
console.log(C.red("\n✗ git isn't installed.") + " Huddle gives every agent its own git worktree.");
|
|
26
|
-
console.log(C.faint("
|
|
27
|
+
console.log(C.faint(" " + gitFix()));
|
|
27
28
|
}
|
|
28
29
|
if (!db.ok) {
|
|
29
30
|
blocked = true;
|
|
@@ -35,20 +36,23 @@ export async function runDoctor() {
|
|
|
35
36
|
return blocked ? 1 : 0;
|
|
36
37
|
}
|
|
37
38
|
/** What to do when agents are missing or signed out. Returns true if at least one is ready. */
|
|
38
|
-
function nextSteps(results) {
|
|
39
|
+
export function nextSteps(results, print = console.log) {
|
|
39
40
|
const ready = results.filter((r) => r.state === "ready");
|
|
41
|
+
const pending = results.filter((r) => r.state === "not_authed" || (r.state === "not_installed" && !LEGACY.has(r.adapter.id)));
|
|
40
42
|
if (ready.length) {
|
|
41
|
-
|
|
43
|
+
print(C.faint(`\n ${ready.length} agent(s) ready — cd into a repo and run: huddle`));
|
|
44
|
+
if (pending.length)
|
|
45
|
+
print(C.faint(` set up the rest (${pending.map((r) => r.adapter.id).join(", ")}): huddle setup`));
|
|
42
46
|
return true;
|
|
43
47
|
}
|
|
44
48
|
const missing = results.filter((r) => r.state === "not_installed" && !LEGACY.has(r.adapter.id));
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
print(`\n${C.yellow("No agents ready yet.")} Huddle runs the coding agents you already use,`);
|
|
50
|
+
print("each signed in to its own account.");
|
|
51
|
+
print(` ${C.bold("huddle setup")} ${C.faint("installs and signs in the ones you pick (asks first). Or by hand:")}`);
|
|
52
|
+
for (const r of missing)
|
|
53
|
+
print(` ${dot(r.adapter.id)} ${C.bold(r.adapter.id.padEnd(12))} ${C.faint("$")} ${installLine(r.adapter)}`);
|
|
54
|
+
print(C.faint(" Then sign in with the agent itself — Huddle never sees your credentials —"));
|
|
55
|
+
print(C.faint(" and run `huddle doctor` again."));
|
|
52
56
|
return false;
|
|
53
57
|
}
|
|
54
58
|
//# sourceMappingURL=doctor.js.map
|