huddle-cli 0.1.1 → 0.1.5
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 +66 -0
- package/QUICKSTART.md +63 -2
- package/README.md +15 -0
- package/dist/adapters/codex.js +3 -0
- package/dist/app/live.js +142 -0
- package/dist/app/page.js +775 -0
- package/dist/app/qr.js +47 -0
- package/dist/app/server.js +433 -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 +46 -4
- package/dist/connectors/cli.js +121 -0
- package/dist/connectors/github.js +162 -0
- package/dist/connectors/hub.js +234 -0
- package/dist/connectors/types.js +3 -0
- package/dist/mcp/context-tools.js +2 -0
- package/dist/mcp/server.js +42 -3
- 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 +11 -5
- package/dist/orchestrator/launcher.js +3 -1
- package/dist/orchestrator/learn.js +3 -7
- package/dist/orchestrator/planner.js +32 -5
- package/dist/orchestrator/qa.js +9 -1
- package/dist/orchestrator/scheduler.js +105 -19
- package/dist/platform.js +104 -0
- package/dist/room/mail.js +19 -6
- package/dist/room/server.js +82 -2
- package/dist/room/user.js +54 -0
- package/dist/ui/app.js +511 -69
- package/dist/ui/brief.js +494 -0
- package/dist/ui/catalog.js +1 -1
- 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/undo.js +131 -0
- package/dist/ui/watch.js +4 -0
- package/dist/update.js +84 -0
- package/package.json +3 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// One plain-English line on top of a live run: what's happening now, and
|
|
2
|
+
// whether you're needed.
|
|
3
|
+
//
|
|
4
|
+
// Found live (0.1.2 test): with four agents' notes scrolling by, the user
|
|
5
|
+
// couldn't tell what the team was doing, or whether it was waiting on them.
|
|
6
|
+
// The narrator answers both, every second, with words, not engineering:
|
|
7
|
+
// Now: the agents are each sketching how they'd build it · nothing needed from you
|
|
8
|
+
// Now: building — 2 of 8 parts done · claude: argv parser · codex: conversion core 25%
|
|
9
|
+
// ⚑ claude has a question for you — answer below · picks option 1 in 41s
|
|
10
|
+
import { fmtDuration } from "../adapters/usage.js";
|
|
11
|
+
import { C, truncate } from "./term.js";
|
|
12
|
+
const COUNCIL = {
|
|
13
|
+
1: "the agents are each sketching how they'd build it",
|
|
14
|
+
2: "they're reading each other's ideas: what to borrow, what could go wrong",
|
|
15
|
+
3: "one agent is writing the plan from the best ideas",
|
|
16
|
+
4: "the others are double-checking the plan before anyone writes code",
|
|
17
|
+
};
|
|
18
|
+
/** "Build strict positional argv parser with UsageError" → "strict positional argv parser". */
|
|
19
|
+
function shortTitle(t, n = 32) {
|
|
20
|
+
const s = t.replace(/^(build|create|write|add|implement|make|set up)\s+(the\s+|a\s+|an\s+)?/i, "").replace(/\s+(with|and|for|that)\s.*$/i, "");
|
|
21
|
+
return s.length > n ? s.slice(0, n - 1) + "…" : s;
|
|
22
|
+
}
|
|
23
|
+
/** The narrator's words for the run as it stands (no colors), and whether it's waiting on you. */
|
|
24
|
+
export function narration(stats, now = Date.now()) {
|
|
25
|
+
const ask = stats.ask;
|
|
26
|
+
if (ask && ask.until > now)
|
|
27
|
+
return { needsYou: true, text: `${ask.from} has a question for you — answer below · picks option 1 in ${fmtDuration(ask.until - now)}` };
|
|
28
|
+
const r = stats.run;
|
|
29
|
+
if (!r)
|
|
30
|
+
return { needsYou: false, text: "getting started" };
|
|
31
|
+
if (r.phase === "COUNCIL")
|
|
32
|
+
return { needsYou: false, text: COUNCIL[r.step ?? 1] ?? COUNCIL[1] };
|
|
33
|
+
if (r.phase === "PLAN")
|
|
34
|
+
return { needsYou: false, text: "splitting the work into parts" };
|
|
35
|
+
if (r.phase === "DONE")
|
|
36
|
+
return { needsYou: false, text: "done — the summary is below" };
|
|
37
|
+
const busy = [...stats.agents].filter(([, a]) => a.busySince && a.doing);
|
|
38
|
+
const who = busy.map(([id, a]) => `${id}: ${shortTitle(a.doing)}${a.pct ? ` ${a.pct}%` : ""}`);
|
|
39
|
+
if (r.phase === "QA")
|
|
40
|
+
return { needsYou: false, text: `testing the finished build: agents try to break parts they didn't write${r.round ? ` (round ${r.round})` : ""}${who.length ? " · " + who.join(" · ") : ""}` };
|
|
41
|
+
const done = r.total ? `${r.done} of ${r.total} parts done` : "starting";
|
|
42
|
+
return { needsYou: false, text: `building — ${done}${r.failed ? `, ${r.failed} stuck` : ""}${who.length ? " · " + who.join(" · ") : ""}` };
|
|
43
|
+
}
|
|
44
|
+
/** The narrator line, cut to `W` cells: " Now: … · nothing needed from you". */
|
|
45
|
+
export function narratorLine(stats, W, now = Date.now()) {
|
|
46
|
+
const n = narration(stats, now);
|
|
47
|
+
if (n.needsYou)
|
|
48
|
+
return truncate(` ${C.yellow("⚑ " + n.text)}`, W);
|
|
49
|
+
const calm = C.faint(" · nothing needed from you");
|
|
50
|
+
const head = ` ${C.bold(C.violet("Now:"))} `;
|
|
51
|
+
// The reassurance stays; the detail gives way.
|
|
52
|
+
const room = W - 2 - 5 - 27;
|
|
53
|
+
const text = n.text.length > room && room > 12 ? n.text.slice(0, room - 1) + "…" : n.text;
|
|
54
|
+
return truncate(`${head}${text}${calm}`, W);
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=narrator.js.map
|
package/dist/ui/story.js
CHANGED
|
@@ -17,7 +17,7 @@ import { C, dot, pad, truncate, AGENT_COLOR } from "./term.js";
|
|
|
17
17
|
import { fmtDuration, fmtTokens } from "../adapters/usage.js";
|
|
18
18
|
import { isHelpOffer } from "../room/progress.js";
|
|
19
19
|
/** The scheduler's own event types, persisted as "run.<type>" (scheduler.ts). */
|
|
20
|
-
const CORE_RUN = new Set(["start", "declined", "tokens", "review", "reviewed", "merged", "failed", "stuck", "conflict", "resolved", "handoff"]);
|
|
20
|
+
export const CORE_RUN = new Set(["start", "declined", "tokens", "review", "reviewed", "merged", "failed", "stuck", "conflict", "resolved", "handoff"]);
|
|
21
21
|
const NOT_AGENTS = new Set(["huddle", "room", "all", "you", "council", ""]);
|
|
22
22
|
function parse(e) {
|
|
23
23
|
let p = {};
|
|
@@ -40,9 +40,9 @@ function parse(e) {
|
|
|
40
40
|
delete p.type;
|
|
41
41
|
return { ts: e.ts, type, agent: e.agent_id, task: e.task_id, p };
|
|
42
42
|
}
|
|
43
|
-
const str = (v) => (v === undefined || v === null ? "" : typeof v === "string" ? v : typeof v === "object" ? JSON.stringify(v) : String(v));
|
|
43
|
+
export const str = (v) => (v === undefined || v === null ? "" : typeof v === "string" ? v : typeof v === "object" ? JSON.stringify(v) : String(v));
|
|
44
44
|
/** First non-empty field among `keys`. */
|
|
45
|
-
const pick = (p, ...keys) => {
|
|
45
|
+
export const pick = (p, ...keys) => {
|
|
46
46
|
for (const k of keys) {
|
|
47
47
|
const v = p[k];
|
|
48
48
|
if (v !== undefined && v !== null && str(v).trim())
|
|
@@ -50,7 +50,7 @@ const pick = (p, ...keys) => {
|
|
|
50
50
|
}
|
|
51
51
|
return "";
|
|
52
52
|
};
|
|
53
|
-
const oneLine = (s, n = 140) => {
|
|
53
|
+
export const oneLine = (s, n = 140) => {
|
|
54
54
|
const l = s.replace(/\s+/g, " ").trim();
|
|
55
55
|
return l.length <= n ? l : l.slice(0, n - 1) + "…";
|
|
56
56
|
};
|
|
@@ -74,7 +74,7 @@ function qaEyes(checkers, authors) {
|
|
|
74
74
|
return "(didn't write it)";
|
|
75
75
|
return mine.length === checkers.length ? "(wrote some of it too — checking anyway)" : `(${mine.join(", ")} wrote some of it too)`;
|
|
76
76
|
}
|
|
77
|
-
const list = (v) => {
|
|
77
|
+
export const list = (v) => {
|
|
78
78
|
if (Array.isArray(v))
|
|
79
79
|
return v.map(str);
|
|
80
80
|
if (typeof v === "string") {
|
|
@@ -373,6 +373,8 @@ export function buildStory(run, runs = 1) {
|
|
|
373
373
|
case "memory.added": {
|
|
374
374
|
const author = pick(p, "author") || e.agent || "";
|
|
375
375
|
const body = pick(p, "body");
|
|
376
|
+
if (author === "you")
|
|
377
|
+
break; // your words: already their message or answer
|
|
376
378
|
if (author === "huddle") {
|
|
377
379
|
// Mostly the scheduler restating what run.* says; the red gate is news.
|
|
378
380
|
if (/red test gate/.test(body))
|
|
@@ -452,6 +454,19 @@ export function buildStory(run, runs = 1) {
|
|
|
452
454
|
st(to).received++;
|
|
453
455
|
break;
|
|
454
456
|
}
|
|
457
|
+
// An agent's question for the person at the keyboard (huddle_ask_user), and what came back.
|
|
458
|
+
case "user.ask": {
|
|
459
|
+
const from = pick(p, "from") || e.agent || "?";
|
|
460
|
+
const opts = list(p.options);
|
|
461
|
+
add({ ts, kind: "message", type: e.type, agent: from, to: "you", text: oneLine(pick(p, "question"), 400), detail: `question for you · ${opts.map((o, i) => `${i + 1}. ${o}`).join(" · ")}` });
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
case "user.answer": {
|
|
465
|
+
const to = pick(p, "from") || e.agent || "?";
|
|
466
|
+
const mine = pick(p, "by") === "you";
|
|
467
|
+
add({ ts, kind: "message", type: e.type, agent: mine ? "you" : "huddle", to, text: oneLine(pick(p, "answer"), 400), detail: mine ? "your answer" : "no answer in time · went with option 1" });
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
455
470
|
case "handoff": {
|
|
456
471
|
const from = pick(p, "from") || e.agent;
|
|
457
472
|
const to = pick(p, "to");
|
|
@@ -562,6 +577,14 @@ export function buildStory(run, runs = 1) {
|
|
|
562
577
|
case "run.stuck":
|
|
563
578
|
add({ ts, kind: "fail", type: e.type, agent: null, text: `! blocked by failures: ${list(p.blocked).join(" · ")}` });
|
|
564
579
|
break;
|
|
580
|
+
case "busy": // quiet past the silence window, but working: not taken over (busy.ts)
|
|
581
|
+
add({ ts, kind: "progress", type: e.type, agent: pick(p, "agent") || e.agent, text: `⧗ running a long command: quiet ${Math.round((Number(p.quietMs) || 0) / 60_000) || "<1"} min, but busy — not taken over`, detail: oneLine(pick(p, "why"), 200) || undefined });
|
|
582
|
+
break;
|
|
583
|
+
case "budget": {
|
|
584
|
+
const left = list(p.left);
|
|
585
|
+
add({ ts, kind: "fail", type: e.type, agent: null, text: `! token budget ${p.level === "hard" ? "ran over: agents stopped" : "nearly spent: no new work started"} (${Math.round(((Number(p.tokens) || 0) / (Number(p.limit) || 1)) * 100)}% of ${fmtTokens(Number(p.limit) || 0)})`, detail: left.length ? `left undone: ${left.join(" · ")}` : undefined });
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
565
588
|
case "run.conflict": {
|
|
566
589
|
const a = pick(p, "agent") || e.agent || "?";
|
|
567
590
|
const author = pick(p, "author");
|
|
@@ -922,7 +945,7 @@ export function collabLines(s) {
|
|
|
922
945
|
out.push({ label: "credit", text: [...c.credit.entries()].sort((a, b) => b[1] - a[1]).map(([a, k]) => `${a} ${n(k, "task")}`).join(" · ") });
|
|
923
946
|
return out;
|
|
924
947
|
}
|
|
925
|
-
function crewRows(s) {
|
|
948
|
+
export function crewRows(s) {
|
|
926
949
|
const done = s.items.filter((it) => it.type === "qa.done");
|
|
927
950
|
return s.agents.map((a) => {
|
|
928
951
|
const x = s.stats.get(a);
|
|
@@ -930,14 +953,14 @@ function crewRows(s) {
|
|
|
930
953
|
return { agent: a, merged: x.merged, failed: x.failed, reviews: x.reviews, qa, ms: x.ms, tokens: x.tokens, cached: x.cached };
|
|
931
954
|
});
|
|
932
955
|
}
|
|
933
|
-
const plural = (k, one, many = one + "s") => `${k} ${k === 1 ? one : many}`;
|
|
956
|
+
export const plural = (k, one, many = one + "s") => `${k} ${k === 1 ? one : many}`;
|
|
934
957
|
/** "5 merged · 3 reviews · 1 QA" — what an agent did, nothing for what it didn't. */
|
|
935
|
-
function didLine(c) {
|
|
958
|
+
export function didLine(c) {
|
|
936
959
|
const parts = [`${c.merged} merged`, c.failed ? `${c.failed} failed` : "", c.reviews ? plural(c.reviews, "review") : "", c.qa ? `${c.qa} QA` : ""];
|
|
937
960
|
return parts.filter(Boolean).join(" · ");
|
|
938
961
|
}
|
|
939
962
|
/** The run cut into its phases: the build, then QA (from the first QA round on). */
|
|
940
|
-
function phases(s) {
|
|
963
|
+
export function phases(s) {
|
|
941
964
|
// The council's decisions and risks, written to shared memory as the build starts: part of the plan.
|
|
942
965
|
const agreed = s.items.filter((it) => it.agent === "council" && it.type === "memory.added");
|
|
943
966
|
const rest = s.items.filter((it) => !agreed.includes(it));
|
|
@@ -945,14 +968,14 @@ function phases(s) {
|
|
|
945
968
|
return i < 0 ? { agreed, build: rest, qa: [] } : { agreed, build: rest.slice(0, i), qa: rest.slice(i) };
|
|
946
969
|
}
|
|
947
970
|
/** A council decision or risk from shared memory, as "decided" / "risk (by)". */
|
|
948
|
-
function agreement(it) {
|
|
971
|
+
export function agreement(it) {
|
|
949
972
|
const risk = /^gotcha: council risk:/.test(it.text);
|
|
950
973
|
return { risk, by: risk ? /flagged by (\S+)/.exec(it.detail ?? "")?.[1] : undefined, body: it.text.replace(/^\w+: council( risk)?:\s*/, "") };
|
|
951
974
|
}
|
|
952
975
|
/** The council is recorded in one go when it's done: its items share one instant, so they get no clock. */
|
|
953
976
|
const councilInstant = (s) => s.design.length > 1 && s.design.at(-1).ts - s.design[0].ts < 2000;
|
|
954
977
|
/** How the run ended: totals, merges gated on tests, QA's last word. */
|
|
955
|
-
function outcome(s) {
|
|
978
|
+
export function outcome(s) {
|
|
956
979
|
const rows = crewRows(s);
|
|
957
980
|
const tokens = rows.reduce((n, c) => n + c.tokens, 0);
|
|
958
981
|
const cached = rows.reduce((n, c) => n + c.cached, 0);
|
|
@@ -964,7 +987,7 @@ function outcome(s) {
|
|
|
964
987
|
return { tokens, cached, merges: merges.length, green, qa, rounds, ms: s.run.end - s.run.start };
|
|
965
988
|
}
|
|
966
989
|
/** The teamwork as numbers, for the summary line and the HTML tiles. Only what happened (or was looked for). */
|
|
967
|
-
function tally(s) {
|
|
990
|
+
export function tally(s) {
|
|
968
991
|
const c = s.collab;
|
|
969
992
|
const out = [];
|
|
970
993
|
if (c.adopts.length || c.flaws.length) {
|
|
@@ -997,7 +1020,7 @@ function tally(s) {
|
|
|
997
1020
|
return out;
|
|
998
1021
|
}
|
|
999
1022
|
/** Each buddy note with how it ended: fixed in code, answered, ignored — or just sent. */
|
|
1000
|
-
function buddyNotes(s) {
|
|
1023
|
+
export function buddyNotes(s) {
|
|
1001
1024
|
const left = s.collab.followups.filter((f) => f.kind !== "radar");
|
|
1002
1025
|
return s.collab.buddy
|
|
1003
1026
|
.filter((b) => b.notes)
|
|
@@ -1010,7 +1033,7 @@ function buddyNotes(s) {
|
|
|
1010
1033
|
});
|
|
1011
1034
|
}
|
|
1012
1035
|
// ---- terminal -----------------------------------------------------------------
|
|
1013
|
-
const clock = (ms) => {
|
|
1036
|
+
export const clock = (ms) => {
|
|
1014
1037
|
const s = Math.max(0, Math.round(ms / 1000));
|
|
1015
1038
|
return `+${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
|
|
1016
1039
|
};
|
|
@@ -1208,10 +1231,88 @@ export function renderRunList(runs) {
|
|
|
1208
1231
|
.join("\n");
|
|
1209
1232
|
}
|
|
1210
1233
|
// ---- HTML (a static, self-contained report file) --------------------------------
|
|
1211
|
-
const esc = (s) => str(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
1212
|
-
const colorOf = (a) => (a ? AGENT_COLOR[a.split("#")[0]] ?? "#8A857C" : "#7B6CF6");
|
|
1234
|
+
export const esc = (s) => str(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
1235
|
+
export const colorOf = (a) => (a ? AGENT_COLOR[a.split("#")[0]] ?? "#8A857C" : "#7B6CF6");
|
|
1213
1236
|
/** Lines that are the agents thinking out loud, hidden behind a toggle so the story reads first. */
|
|
1214
1237
|
const NOISE = (it) => it.kind === "progress" || it.kind === "announce" || it.kind === "memory" || it.type === "memory.added";
|
|
1238
|
+
/** The story page's look — shared with `huddle export --format html` (src/ui/export.ts). */
|
|
1239
|
+
export const STORY_CSS = `:root{--bg:#f6f5f1;--fg:#1d1b18;--mute:#6f6a62;--line:#e4e0d8;--card:#fff;--soft:#efede7;--ok:#23864c;--bad:#c93f30;--warn:#a86f05;--brand:#6a5af0;color-scheme:light}
|
|
1240
|
+
@media (prefers-color-scheme: dark){:root:not([data-theme="light"]){--bg:#121117;--fg:#ece9e3;--mute:#9a948b;--line:#2b2932;--card:#1b1a21;--soft:#23222a;--ok:#4cc27c;--bad:#f06a5b;--warn:#e0a53a;--brand:#9488ff;color-scheme:dark}}
|
|
1241
|
+
:root[data-theme="dark"]{--bg:#121117;--fg:#ece9e3;--mute:#9a948b;--line:#2b2932;--card:#1b1a21;--soft:#23222a;--ok:#4cc27c;--bad:#f06a5b;--warn:#e0a53a;--brand:#9488ff;color-scheme:dark}
|
|
1242
|
+
*{box-sizing:border-box}
|
|
1243
|
+
body{margin:0;background:var(--bg);color:var(--fg);font:15px/1.5 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;-webkit-text-size-adjust:100%}
|
|
1244
|
+
main{max-width:1100px;margin:0 auto;padding:24px 16px 60px}
|
|
1245
|
+
h1{font-size:26px;line-height:1.25;margin:6px 0 4px;word-break:break-word}
|
|
1246
|
+
h2{font-size:13px;text-transform:uppercase;letter-spacing:.08em;color:var(--brand);margin:36px 0 12px}
|
|
1247
|
+
h2 small{text-transform:none;letter-spacing:0}
|
|
1248
|
+
h3{font-size:14px;margin:0 0 10px}
|
|
1249
|
+
.brand{font-weight:700;color:var(--brand);font-size:13px;letter-spacing:.04em}
|
|
1250
|
+
.sub,small,.detail,time,.meta,.none,.legend{color:var(--mute)}small{font-size:12px;margin-left:6px;font-weight:400}
|
|
1251
|
+
.none{font-size:13px;margin:6px 0}
|
|
1252
|
+
.who{display:inline-flex;align-items:center;gap:5px;font-weight:600;white-space:nowrap}.who i{width:9px;height:9px;border-radius:50%;display:inline-block;flex:none}
|
|
1253
|
+
.hero{background:var(--card);border:1px solid var(--line);border-radius:16px;padding:20px}
|
|
1254
|
+
.hero details{margin:8px 0}
|
|
1255
|
+
.kpis,.tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:10px;margin-top:16px}
|
|
1256
|
+
.kpi,.tile{background:var(--soft);border-radius:12px;padding:10px 12px;min-width:0}
|
|
1257
|
+
.kpi b,.tile b{display:block;font-size:24px;line-height:1.2;font-variant-numeric:tabular-nums}
|
|
1258
|
+
.kpi span,.tile span{font-size:12px;color:var(--mute)}
|
|
1259
|
+
.kpi.ok b,.tile.ok b{color:var(--ok)}.kpi.bad b,.tile.bad b{color:var(--bad)}.kpi.warn b,.tile.warn b{color:var(--warn)}
|
|
1260
|
+
.verdict{margin:14px 0 0;font-weight:600}.verdict.ok{color:var(--ok)}.verdict.warn{color:var(--warn)}
|
|
1261
|
+
.crew{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:10px;margin-top:12px}
|
|
1262
|
+
.agent{background:var(--card);border:1px solid var(--line);border-top:4px solid var(--c);border-radius:12px;padding:10px 14px;min-width:0}
|
|
1263
|
+
.agent .an{display:flex;align-items:center;gap:7px;font-weight:700;font-size:16px}.agent .an i{width:11px;height:11px;border-radius:50%;background:var(--c)}
|
|
1264
|
+
.agent .as{font-size:14px}.agent .am{font-size:12px;color:var(--mute);font-variant-numeric:tabular-nums}
|
|
1265
|
+
.cards{columns:320px 3;column-gap:12px;margin-top:12px}.cards .card{break-inside:avoid;margin-bottom:12px}
|
|
1266
|
+
.card{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:14px 16px;min-width:0}
|
|
1267
|
+
.list li{font-size:14px;padding:7px 0;border-top:1px solid var(--line);word-break:break-word}.list li:first-child{border-top:0}
|
|
1268
|
+
.list li.mute{color:var(--mute)}
|
|
1269
|
+
.badge{display:inline-block;font-size:11px;font-weight:700;padding:1px 8px;border-radius:999px;background:var(--soft);color:var(--mute);white-space:nowrap}
|
|
1270
|
+
.badge.ok{color:var(--ok);background:color-mix(in srgb,var(--ok) 14%,transparent)}.badge.bad{color:var(--bad);background:color-mix(in srgb,var(--bad) 14%,transparent)}.badge.warn{color:var(--warn);background:color-mix(in srgb,var(--warn) 16%,transparent)}
|
|
1271
|
+
.grid td,.grid th{text-align:center}.grid th:first-child{text-align:left}
|
|
1272
|
+
.ad,.fl{display:inline-block;min-width:22px;padding:1px 6px;margin:1px;border-radius:6px;font-weight:700;font-size:12px}
|
|
1273
|
+
.ad{color:var(--ok);background:color-mix(in srgb,var(--ok) 15%,transparent)}.fl{color:var(--warn);background:color-mix(in srgb,var(--warn) 17%,transparent)}.z{color:var(--line)}
|
|
1274
|
+
.legend{font-size:12px;margin:8px 0}
|
|
1275
|
+
.quote{font-size:13px;padding:6px 0 6px 10px;border-left:3px solid var(--line);margin-top:6px;word-break:break-word}
|
|
1276
|
+
.scroll{overflow-x:auto;-webkit-overflow-scrolling:touch}
|
|
1277
|
+
.lanesbox{border:1px solid var(--line);border-radius:12px;background:var(--card)}
|
|
1278
|
+
.lanes{min-width:640px;padding:8px 12px 12px}
|
|
1279
|
+
.axis{position:relative;height:18px;margin-left:120px;font-size:11px;color:var(--mute)}.axis span{position:absolute;transform:translateX(-50%)}.axis span:first-child{transform:none}.axis span:last-child{transform:translateX(-100%)}
|
|
1280
|
+
.lane{display:flex;align-items:center;height:38px;border-top:1px solid var(--line)}
|
|
1281
|
+
.lane .name{width:120px;flex:none;overflow:hidden;text-overflow:ellipsis;font-size:13px}
|
|
1282
|
+
.track{position:relative;flex:1;height:26px}
|
|
1283
|
+
.bar{position:absolute;top:3px;height:20px;border-radius:5px;background:color-mix(in srgb,var(--c) 30%,transparent);border:1px solid var(--c);overflow:hidden;font-size:11px;line-height:18px;padding:0 4px;white-space:nowrap}
|
|
1284
|
+
.bar.review{border-style:dashed}.bar.conflict{border-color:var(--warn)}.bar.qa{border-style:dotted}
|
|
1285
|
+
.bar.ok{box-shadow:inset -3px 0 0 var(--ok)}.bar.bad{box-shadow:inset -3px 0 0 var(--bad)}.bar.warn{box-shadow:inset -3px 0 0 var(--warn)}
|
|
1286
|
+
.mark{position:absolute;top:-3px;width:7px;height:7px;margin-left:-3px;border-radius:50%;background:var(--brand)}
|
|
1287
|
+
.mark.merge{background:var(--ok)}.mark.fail{background:var(--bad)}.mark.qa,.mark.takeover{background:var(--warn)}
|
|
1288
|
+
ol,ul{padding:0;margin:0;list-style:none}
|
|
1289
|
+
.phase{margin-top:22px}
|
|
1290
|
+
.phase h3{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;font-weight:400;color:var(--mute);font-size:13px}
|
|
1291
|
+
.tag{font-weight:800;letter-spacing:.08em;color:#fff;background:var(--brand);border-radius:6px;padding:2px 8px;font-size:12px}
|
|
1292
|
+
.timeline li{display:flex;gap:10px;padding:6px 0;font-size:14px;border-top:1px solid var(--line);min-width:0}
|
|
1293
|
+
.timeline li>div{min-width:0;word-break:break-word}
|
|
1294
|
+
.timeline time{flex:none;width:46px;font-variant-numeric:tabular-nums;font-size:13px}
|
|
1295
|
+
.timeline .merge .text{color:var(--ok);font-weight:600}.timeline .fail .text{color:var(--bad)}.timeline .conflict .text,.timeline .qa .text,.timeline .takeover .text{color:var(--warn)}
|
|
1296
|
+
.timeline .progress .text,.timeline .announce .text,.timeline .team .text,.timeline .other .text,.timeline .memory .text{color:var(--mute)}
|
|
1297
|
+
#noise{position:absolute;opacity:0;pointer-events:none}
|
|
1298
|
+
.toggle{display:inline-block;font-size:13px;color:var(--mute);border:1px solid var(--line);border-radius:999px;padding:3px 12px;cursor:pointer;user-select:none;margin-top:4px}
|
|
1299
|
+
.toggle::before{content:"○ "}#noise:checked~.toggle::before{content:"● "}
|
|
1300
|
+
#noise:focus-visible~.toggle{outline:2px solid var(--brand)}
|
|
1301
|
+
.timeline li.noise{display:none}#noise:checked~.phases .timeline li.noise{display:flex}
|
|
1302
|
+
.arrow{color:var(--mute)}.detail{font-size:13px;white-space:pre-wrap;word-break:break-word}
|
|
1303
|
+
.plan{counter-reset:n}.plan li{padding:6px 0;border-top:1px solid var(--line);word-break:break-word}.plan li::before{counter-increment:n;content:counter(n) ". ";color:var(--mute)}
|
|
1304
|
+
.contract{margin:10px 0 0}
|
|
1305
|
+
.result li{font-size:15px}.result li.ok{color:var(--ok)}.result li.warn{color:var(--warn)}
|
|
1306
|
+
details{border:1px solid var(--line);border-radius:12px;background:var(--card);margin:10px 0;padding:8px 12px}summary{cursor:pointer;font-weight:600}
|
|
1307
|
+
.thread{display:flex;flex-direction:column;gap:8px;margin-top:10px}
|
|
1308
|
+
.thread li{max-width:85%;padding:8px 10px;border-radius:12px;border:1px solid var(--line);border-left:4px solid var(--c);word-break:break-word}
|
|
1309
|
+
.thread li.r{align-self:flex-end;border-left:1px solid var(--line);border-right:4px solid var(--c)}.thread .meta{font-size:12px}
|
|
1310
|
+
table{border-collapse:collapse;width:100%;font-size:13px;font-variant-numeric:tabular-nums}th,td{padding:6px 8px;border-bottom:1px solid var(--line);text-align:left;white-space:nowrap}
|
|
1311
|
+
table.team td{white-space:normal;word-break:break-word}table.team th{color:var(--mute);font-weight:600;vertical-align:top}
|
|
1312
|
+
td.hot{font-weight:700;color:var(--brand)}td.self{color:var(--mute)}
|
|
1313
|
+
footer{margin-top:40px;font-size:12px;color:var(--mute);text-align:center}
|
|
1314
|
+
@media (max-width:520px){h1{font-size:21px}.hero{padding:16px}.kpi b,.tile b{font-size:20px}.kpis,.tiles{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
|
1315
|
+
`;
|
|
1215
1316
|
export function renderHtml(s) {
|
|
1216
1317
|
const r = s.run;
|
|
1217
1318
|
const c = s.collab;
|
|
@@ -1370,83 +1471,7 @@ export function renderHtml(s) {
|
|
|
1370
1471
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1371
1472
|
<title>Huddle story</title>
|
|
1372
1473
|
<style>
|
|
1373
|
-
|
|
1374
|
-
@media (prefers-color-scheme: dark){:root:not([data-theme="light"]){--bg:#121117;--fg:#ece9e3;--mute:#9a948b;--line:#2b2932;--card:#1b1a21;--soft:#23222a;--ok:#4cc27c;--bad:#f06a5b;--warn:#e0a53a;--brand:#9488ff;color-scheme:dark}}
|
|
1375
|
-
:root[data-theme="dark"]{--bg:#121117;--fg:#ece9e3;--mute:#9a948b;--line:#2b2932;--card:#1b1a21;--soft:#23222a;--ok:#4cc27c;--bad:#f06a5b;--warn:#e0a53a;--brand:#9488ff;color-scheme:dark}
|
|
1376
|
-
*{box-sizing:border-box}
|
|
1377
|
-
body{margin:0;background:var(--bg);color:var(--fg);font:15px/1.5 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;-webkit-text-size-adjust:100%}
|
|
1378
|
-
main{max-width:1100px;margin:0 auto;padding:24px 16px 60px}
|
|
1379
|
-
h1{font-size:26px;line-height:1.25;margin:6px 0 4px;word-break:break-word}
|
|
1380
|
-
h2{font-size:13px;text-transform:uppercase;letter-spacing:.08em;color:var(--brand);margin:36px 0 12px}
|
|
1381
|
-
h2 small{text-transform:none;letter-spacing:0}
|
|
1382
|
-
h3{font-size:14px;margin:0 0 10px}
|
|
1383
|
-
.brand{font-weight:700;color:var(--brand);font-size:13px;letter-spacing:.04em}
|
|
1384
|
-
.sub,small,.detail,time,.meta,.none,.legend{color:var(--mute)}small{font-size:12px;margin-left:6px;font-weight:400}
|
|
1385
|
-
.none{font-size:13px;margin:6px 0}
|
|
1386
|
-
.who{display:inline-flex;align-items:center;gap:5px;font-weight:600;white-space:nowrap}.who i{width:9px;height:9px;border-radius:50%;display:inline-block;flex:none}
|
|
1387
|
-
.hero{background:var(--card);border:1px solid var(--line);border-radius:16px;padding:20px}
|
|
1388
|
-
.hero details{margin:8px 0}
|
|
1389
|
-
.kpis,.tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:10px;margin-top:16px}
|
|
1390
|
-
.kpi,.tile{background:var(--soft);border-radius:12px;padding:10px 12px;min-width:0}
|
|
1391
|
-
.kpi b,.tile b{display:block;font-size:24px;line-height:1.2;font-variant-numeric:tabular-nums}
|
|
1392
|
-
.kpi span,.tile span{font-size:12px;color:var(--mute)}
|
|
1393
|
-
.kpi.ok b,.tile.ok b{color:var(--ok)}.kpi.bad b,.tile.bad b{color:var(--bad)}.kpi.warn b,.tile.warn b{color:var(--warn)}
|
|
1394
|
-
.verdict{margin:14px 0 0;font-weight:600}.verdict.ok{color:var(--ok)}.verdict.warn{color:var(--warn)}
|
|
1395
|
-
.crew{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:10px;margin-top:12px}
|
|
1396
|
-
.agent{background:var(--card);border:1px solid var(--line);border-top:4px solid var(--c);border-radius:12px;padding:10px 14px;min-width:0}
|
|
1397
|
-
.agent .an{display:flex;align-items:center;gap:7px;font-weight:700;font-size:16px}.agent .an i{width:11px;height:11px;border-radius:50%;background:var(--c)}
|
|
1398
|
-
.agent .as{font-size:14px}.agent .am{font-size:12px;color:var(--mute);font-variant-numeric:tabular-nums}
|
|
1399
|
-
.cards{columns:320px 3;column-gap:12px;margin-top:12px}.cards .card{break-inside:avoid;margin-bottom:12px}
|
|
1400
|
-
.card{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:14px 16px;min-width:0}
|
|
1401
|
-
.list li{font-size:14px;padding:7px 0;border-top:1px solid var(--line);word-break:break-word}.list li:first-child{border-top:0}
|
|
1402
|
-
.list li.mute{color:var(--mute)}
|
|
1403
|
-
.badge{display:inline-block;font-size:11px;font-weight:700;padding:1px 8px;border-radius:999px;background:var(--soft);color:var(--mute);white-space:nowrap}
|
|
1404
|
-
.badge.ok{color:var(--ok);background:color-mix(in srgb,var(--ok) 14%,transparent)}.badge.bad{color:var(--bad);background:color-mix(in srgb,var(--bad) 14%,transparent)}.badge.warn{color:var(--warn);background:color-mix(in srgb,var(--warn) 16%,transparent)}
|
|
1405
|
-
.grid td,.grid th{text-align:center}.grid th:first-child{text-align:left}
|
|
1406
|
-
.ad,.fl{display:inline-block;min-width:22px;padding:1px 6px;margin:1px;border-radius:6px;font-weight:700;font-size:12px}
|
|
1407
|
-
.ad{color:var(--ok);background:color-mix(in srgb,var(--ok) 15%,transparent)}.fl{color:var(--warn);background:color-mix(in srgb,var(--warn) 17%,transparent)}.z{color:var(--line)}
|
|
1408
|
-
.legend{font-size:12px;margin:8px 0}
|
|
1409
|
-
.quote{font-size:13px;padding:6px 0 6px 10px;border-left:3px solid var(--line);margin-top:6px;word-break:break-word}
|
|
1410
|
-
.scroll{overflow-x:auto;-webkit-overflow-scrolling:touch}
|
|
1411
|
-
.lanesbox{border:1px solid var(--line);border-radius:12px;background:var(--card)}
|
|
1412
|
-
.lanes{min-width:640px;padding:8px 12px 12px}
|
|
1413
|
-
.axis{position:relative;height:18px;margin-left:120px;font-size:11px;color:var(--mute)}.axis span{position:absolute;transform:translateX(-50%)}.axis span:first-child{transform:none}.axis span:last-child{transform:translateX(-100%)}
|
|
1414
|
-
.lane{display:flex;align-items:center;height:38px;border-top:1px solid var(--line)}
|
|
1415
|
-
.lane .name{width:120px;flex:none;overflow:hidden;text-overflow:ellipsis;font-size:13px}
|
|
1416
|
-
.track{position:relative;flex:1;height:26px}
|
|
1417
|
-
.bar{position:absolute;top:3px;height:20px;border-radius:5px;background:color-mix(in srgb,var(--c) 30%,transparent);border:1px solid var(--c);overflow:hidden;font-size:11px;line-height:18px;padding:0 4px;white-space:nowrap}
|
|
1418
|
-
.bar.review{border-style:dashed}.bar.conflict{border-color:var(--warn)}.bar.qa{border-style:dotted}
|
|
1419
|
-
.bar.ok{box-shadow:inset -3px 0 0 var(--ok)}.bar.bad{box-shadow:inset -3px 0 0 var(--bad)}.bar.warn{box-shadow:inset -3px 0 0 var(--warn)}
|
|
1420
|
-
.mark{position:absolute;top:-3px;width:7px;height:7px;margin-left:-3px;border-radius:50%;background:var(--brand)}
|
|
1421
|
-
.mark.merge{background:var(--ok)}.mark.fail{background:var(--bad)}.mark.qa,.mark.takeover{background:var(--warn)}
|
|
1422
|
-
ol,ul{padding:0;margin:0;list-style:none}
|
|
1423
|
-
.phase{margin-top:22px}
|
|
1424
|
-
.phase h3{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;font-weight:400;color:var(--mute);font-size:13px}
|
|
1425
|
-
.tag{font-weight:800;letter-spacing:.08em;color:#fff;background:var(--brand);border-radius:6px;padding:2px 8px;font-size:12px}
|
|
1426
|
-
.timeline li{display:flex;gap:10px;padding:6px 0;font-size:14px;border-top:1px solid var(--line);min-width:0}
|
|
1427
|
-
.timeline li>div{min-width:0;word-break:break-word}
|
|
1428
|
-
.timeline time{flex:none;width:46px;font-variant-numeric:tabular-nums;font-size:13px}
|
|
1429
|
-
.timeline .merge .text{color:var(--ok);font-weight:600}.timeline .fail .text{color:var(--bad)}.timeline .conflict .text,.timeline .qa .text,.timeline .takeover .text{color:var(--warn)}
|
|
1430
|
-
.timeline .progress .text,.timeline .announce .text,.timeline .team .text,.timeline .other .text,.timeline .memory .text{color:var(--mute)}
|
|
1431
|
-
#noise{position:absolute;opacity:0;pointer-events:none}
|
|
1432
|
-
.toggle{display:inline-block;font-size:13px;color:var(--mute);border:1px solid var(--line);border-radius:999px;padding:3px 12px;cursor:pointer;user-select:none;margin-top:4px}
|
|
1433
|
-
.toggle::before{content:"○ "}#noise:checked~.toggle::before{content:"● "}
|
|
1434
|
-
#noise:focus-visible~.toggle{outline:2px solid var(--brand)}
|
|
1435
|
-
.timeline li.noise{display:none}#noise:checked~.phases .timeline li.noise{display:flex}
|
|
1436
|
-
.arrow{color:var(--mute)}.detail{font-size:13px;white-space:pre-wrap;word-break:break-word}
|
|
1437
|
-
.plan{counter-reset:n}.plan li{padding:6px 0;border-top:1px solid var(--line);word-break:break-word}.plan li::before{counter-increment:n;content:counter(n) ". ";color:var(--mute)}
|
|
1438
|
-
.contract{margin:10px 0 0}
|
|
1439
|
-
.result li{font-size:15px}.result li.ok{color:var(--ok)}.result li.warn{color:var(--warn)}
|
|
1440
|
-
details{border:1px solid var(--line);border-radius:12px;background:var(--card);margin:10px 0;padding:8px 12px}summary{cursor:pointer;font-weight:600}
|
|
1441
|
-
.thread{display:flex;flex-direction:column;gap:8px;margin-top:10px}
|
|
1442
|
-
.thread li{max-width:85%;padding:8px 10px;border-radius:12px;border:1px solid var(--line);border-left:4px solid var(--c);word-break:break-word}
|
|
1443
|
-
.thread li.r{align-self:flex-end;border-left:1px solid var(--line);border-right:4px solid var(--c)}.thread .meta{font-size:12px}
|
|
1444
|
-
table{border-collapse:collapse;width:100%;font-size:13px;font-variant-numeric:tabular-nums}th,td{padding:6px 8px;border-bottom:1px solid var(--line);text-align:left;white-space:nowrap}
|
|
1445
|
-
table.team td{white-space:normal;word-break:break-word}table.team th{color:var(--mute);font-weight:600;vertical-align:top}
|
|
1446
|
-
td.hot{font-weight:700;color:var(--brand)}td.self{color:var(--mute)}
|
|
1447
|
-
footer{margin-top:40px;font-size:12px;color:var(--mute);text-align:center}
|
|
1448
|
-
@media (max-width:520px){h1{font-size:21px}.hero{padding:16px}.kpi b,.tile b{font-size:20px}.kpis,.tiles{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
|
1449
|
-
</style>
|
|
1474
|
+
${STORY_CSS}</style>
|
|
1450
1475
|
</head>
|
|
1451
1476
|
<body>
|
|
1452
1477
|
<main>
|
package/dist/ui/talk.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// You and the room, during a run: the agents' questions for you
|
|
2
|
+
// (huddle_ask_user) and what you type to them.
|
|
3
|
+
//
|
|
4
|
+
// A question shows like the ones Claude Code, Codex and Cursor ask, with
|
|
5
|
+
// numbered options, the agent's recommendation first:
|
|
6
|
+
//
|
|
7
|
+
// ⚑ claude asks you · should it also convert Rankine?
|
|
8
|
+
// 1. No, just C, F and K (recommended)
|
|
9
|
+
// 2. Yes, add Rankine too
|
|
10
|
+
// type 1-2 · Enter = 1 · or answer in your own words · picks 1 in 90s
|
|
11
|
+
//
|
|
12
|
+
// Unanswered, it goes with option 1 when the time runs out: the run never
|
|
13
|
+
// waits on someone who walked away. Anything else you type during a run goes
|
|
14
|
+
// to every working agent, sent as "you" (room/user.ts); "@codex …" to one.
|
|
15
|
+
import { fmtDuration } from "../adapters/usage.js";
|
|
16
|
+
import { C } from "./term.js";
|
|
17
|
+
export class Talk {
|
|
18
|
+
now;
|
|
19
|
+
queue = [];
|
|
20
|
+
/** A question is on screen (the first one asked; the rest wait their turn). */
|
|
21
|
+
onShow = () => { };
|
|
22
|
+
/** It was answered, by you or by the clock. */
|
|
23
|
+
onSettled = () => { };
|
|
24
|
+
/** Set by the run while it's on: what you typed for the agents, and `details`. */
|
|
25
|
+
say = null;
|
|
26
|
+
details = null;
|
|
27
|
+
constructor(now = () => Date.now()) {
|
|
28
|
+
this.now = now;
|
|
29
|
+
}
|
|
30
|
+
/** The question on screen and when it times out, if any. */
|
|
31
|
+
current() {
|
|
32
|
+
const p = this.queue[0];
|
|
33
|
+
return p ? { q: p.q, until: p.until } : null;
|
|
34
|
+
}
|
|
35
|
+
ask(q) {
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
const p = { q, until: 0, settle: resolve, timer: undefined };
|
|
38
|
+
this.queue.push(p);
|
|
39
|
+
if (this.queue.length === 1)
|
|
40
|
+
this.show();
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/** The time starts when a question reaches the screen, not when it was asked. */
|
|
44
|
+
show() {
|
|
45
|
+
const p = this.queue[0];
|
|
46
|
+
if (!p)
|
|
47
|
+
return;
|
|
48
|
+
p.until = this.now() + p.q.waitMs;
|
|
49
|
+
p.timer = setTimeout(() => this.settle({ choice: 1, text: "", by: "default" }), p.q.waitMs);
|
|
50
|
+
this.onShow(p.q, p.until);
|
|
51
|
+
}
|
|
52
|
+
settle(a) {
|
|
53
|
+
const p = this.queue.shift();
|
|
54
|
+
if (!p)
|
|
55
|
+
return;
|
|
56
|
+
clearTimeout(p.timer);
|
|
57
|
+
p.settle(a);
|
|
58
|
+
this.onSettled(p.q, a);
|
|
59
|
+
this.show();
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* A line you typed. With a question up, it's the answer: a number picks that
|
|
63
|
+
* option, Enter picks 1, anything else is your answer in your own words.
|
|
64
|
+
* Returns false when no question is up (the line is for someone else).
|
|
65
|
+
*/
|
|
66
|
+
answer(line) {
|
|
67
|
+
const p = this.queue[0];
|
|
68
|
+
if (!p)
|
|
69
|
+
return false;
|
|
70
|
+
const t = line.trim();
|
|
71
|
+
const k = Number(t);
|
|
72
|
+
if (!t)
|
|
73
|
+
this.settle({ choice: 1, text: "", by: "you" });
|
|
74
|
+
else if (/^\d$/.test(t) && k >= 1 && k <= p.q.options.length)
|
|
75
|
+
this.settle({ choice: k, text: "", by: "you" });
|
|
76
|
+
else
|
|
77
|
+
this.settle({ choice: null, text: t, by: "you" });
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
/** Every open question goes with option 1 (the run ended or was stopped). */
|
|
81
|
+
close() {
|
|
82
|
+
while (this.queue.length)
|
|
83
|
+
this.settle({ choice: 1, text: "", by: "default" });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** The question as it's printed. */
|
|
87
|
+
export function questionLines(q, until, now = Date.now()) {
|
|
88
|
+
const out = [` ${C.yellow("⚑")} ${C.bold(q.from)} ${C.yellow("asks you")} ${C.faint("·")} ${q.question}`];
|
|
89
|
+
if (q.why)
|
|
90
|
+
out.push(C.faint(` ${q.why}`));
|
|
91
|
+
q.options.forEach((o, i) => out.push(` ${C.bold(String(i + 1) + ".")} ${o}${i === 0 ? C.faint(" (recommended)") : ""}`));
|
|
92
|
+
out.push(C.faint(` type 1-${q.options.length} · Enter = 1 · or answer in your own words · picks 1 in ${fmtDuration(Math.max(0, until - now))}`));
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
/** " → you chose 2: Yes, add Rankine too" / " → no answer in 1m30s: claude goes with 1: …" */
|
|
96
|
+
export function settledLine(q, a) {
|
|
97
|
+
if (a.by === "default")
|
|
98
|
+
return C.faint(` → no answer in ${fmtDuration(q.waitMs)}: ${q.from} goes with 1 · ${q.options[0]}`);
|
|
99
|
+
return ` ${C.green("→")} ${a.choice ? `you chose ${a.choice} · ${q.options[a.choice - 1]}` : `you said: ${a.text}`} ${C.faint(`· sent to ${q.from}`)}`;
|
|
100
|
+
}
|
|
101
|
+
/** "@codex use 2 decimals" → to codex; anything else → everyone working. */
|
|
102
|
+
export function addressee(line) {
|
|
103
|
+
const m = /^@([\w#./-]+)[,:]?\s+([\s\S]+)$/.exec(line.trim());
|
|
104
|
+
return m ? { to: m[1], text: m[2].trim() } : { text: line.trim() };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* A typing line under a one-shot run's pinned block (`huddle join` on a
|
|
108
|
+
* terminal), so you can answer questions and talk to the agents there too.
|
|
109
|
+
* Raw keys: Enter sends, Backspace and ctrl-u edit, ctrl-c stops the run.
|
|
110
|
+
*/
|
|
111
|
+
export class KeyLine {
|
|
112
|
+
onLine;
|
|
113
|
+
onChange;
|
|
114
|
+
onInterrupt;
|
|
115
|
+
input;
|
|
116
|
+
buf = "";
|
|
117
|
+
on = false;
|
|
118
|
+
onData = (d) => this.feed(String(d));
|
|
119
|
+
constructor(onLine, onChange,
|
|
120
|
+
/** ctrl-c: raw mode keeps the terminal from sending SIGINT, so the run decides. */
|
|
121
|
+
onInterrupt = () => process.exit(130), input = process.stdin) {
|
|
122
|
+
this.onLine = onLine;
|
|
123
|
+
this.onChange = onChange;
|
|
124
|
+
this.onInterrupt = onInterrupt;
|
|
125
|
+
this.input = input;
|
|
126
|
+
}
|
|
127
|
+
start() {
|
|
128
|
+
if (this.on || !this.input.isTTY)
|
|
129
|
+
return this;
|
|
130
|
+
this.on = true;
|
|
131
|
+
this.input.setRawMode?.(true);
|
|
132
|
+
this.input.on("data", this.onData);
|
|
133
|
+
this.input.resume();
|
|
134
|
+
return this;
|
|
135
|
+
}
|
|
136
|
+
stop() {
|
|
137
|
+
if (!this.on)
|
|
138
|
+
return;
|
|
139
|
+
this.on = false;
|
|
140
|
+
this.input.off("data", this.onData);
|
|
141
|
+
this.input.setRawMode?.(false);
|
|
142
|
+
this.input.pause();
|
|
143
|
+
}
|
|
144
|
+
/** What the line shows: what you've typed so far, or how to use it. */
|
|
145
|
+
line() {
|
|
146
|
+
if (!this.on)
|
|
147
|
+
return "";
|
|
148
|
+
return this.buf
|
|
149
|
+
? ` ${C.violet("›")} ${this.buf}${C.faint("▏")}`
|
|
150
|
+
: ` ${C.violet("›")} ${C.faint("type to talk to the agents (@claude for one) · details · ctrl-c stops")}`;
|
|
151
|
+
}
|
|
152
|
+
feed(s) {
|
|
153
|
+
// Arrows and other escape keys: not text.
|
|
154
|
+
for (const ch of s.replace(/\x1b\[[0-9;?]*[A-Za-z~]|\x1bO?./g, "")) {
|
|
155
|
+
if (ch === "\r" || ch === "\n") {
|
|
156
|
+
const l = this.buf;
|
|
157
|
+
this.buf = "";
|
|
158
|
+
this.onLine(l);
|
|
159
|
+
}
|
|
160
|
+
else if (ch === "\x7f" || ch === "\b")
|
|
161
|
+
this.buf = [...this.buf].slice(0, -1).join("");
|
|
162
|
+
else if (ch === "\x15")
|
|
163
|
+
this.buf = "";
|
|
164
|
+
else if (ch === "\x03") {
|
|
165
|
+
this.buf = "";
|
|
166
|
+
this.onInterrupt();
|
|
167
|
+
if (!this.on)
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
else if (ch >= " ")
|
|
171
|
+
this.buf += ch;
|
|
172
|
+
}
|
|
173
|
+
this.onChange();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=talk.js.map
|
package/dist/ui/term.js
CHANGED
|
@@ -137,6 +137,32 @@ export function fit(s, out = process.stdout) {
|
|
|
137
137
|
export function pad(s, n) {
|
|
138
138
|
return s + " ".repeat(Math.max(0, n - width(s)));
|
|
139
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Whether progress bars draw block characters. Off when colors are (NO_COLOR,
|
|
142
|
+
* or output piped to a log): there a bar is just its label, "6/9 tasks".
|
|
143
|
+
* Tests and samples flip it to see the bars.
|
|
144
|
+
*/
|
|
145
|
+
export const BARS = { on: color, ascii: ASCII };
|
|
146
|
+
/**
|
|
147
|
+
* A progress bar exactly `w` cells wide — "█████░░░" — or "" when bars are off
|
|
148
|
+
* or there's no room (under 2 cells). `frac` null = unknown: a faint dotted
|
|
149
|
+
* track, never a made-up 0%. Anything above 0 shows at least one block and
|
|
150
|
+
* anything below 1 leaves at least one empty, so 1% isn't nothing and 99%
|
|
151
|
+
* isn't done. Legacy consoles get "#####...".
|
|
152
|
+
*/
|
|
153
|
+
export function bar(frac, w, o = {}) {
|
|
154
|
+
w = Math.floor(w);
|
|
155
|
+
if (!(o.on ?? BARS.on) || !(w >= 2))
|
|
156
|
+
return "";
|
|
157
|
+
const ascii = o.ascii ?? BARS.ascii;
|
|
158
|
+
const [full, empty, unknown] = ascii ? ["#", ".", "-"] : ["█", "░", "·"];
|
|
159
|
+
if (frac === null || !Number.isFinite(frac))
|
|
160
|
+
return C.faint(unknown.repeat(w));
|
|
161
|
+
const f = o.tone === "done" ? 1 : Math.min(1, Math.max(0, frac));
|
|
162
|
+
const n = f <= 0 ? 0 : f >= 1 ? w : Math.min(w - 1, Math.max(1, Math.round(f * w)));
|
|
163
|
+
const fill = o.tone === "done" ? C.green : o.tone === "failed" ? C.red : C.violet;
|
|
164
|
+
return (n ? fill(full.repeat(n)) : "") + (n < w ? C.faint(empty.repeat(w - n)) : "");
|
|
165
|
+
}
|
|
140
166
|
/** Rounded box with a colored border, Claude-Code style. */
|
|
141
167
|
export function box(lines, opts = {}) {
|
|
142
168
|
const border = opts.border ?? C.violet;
|
|
@@ -232,6 +258,11 @@ export class Dock {
|
|
|
232
258
|
this.timer.unref?.();
|
|
233
259
|
return this;
|
|
234
260
|
}
|
|
261
|
+
/** Repaint now (something it shows changed, e.g. a key was typed). */
|
|
262
|
+
redraw() {
|
|
263
|
+
if (this.up)
|
|
264
|
+
this.out.write(this.clear() + this.paint());
|
|
265
|
+
}
|
|
235
266
|
log(line) {
|
|
236
267
|
if (!this.up)
|
|
237
268
|
return void this.out.write(line + "\n");
|