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.
Files changed (73) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/QUICKSTART.md +135 -0
  3. package/README.md +26 -4
  4. package/dist/adapters/amp.js +1 -1
  5. package/dist/adapters/antigravity.js +2 -0
  6. package/dist/adapters/claude.js +19 -3
  7. package/dist/adapters/codex.js +9 -1
  8. package/dist/adapters/cursor.js +12 -5
  9. package/dist/adapters/custom.js +4 -2
  10. package/dist/adapters/droid.js +1 -0
  11. package/dist/adapters/goose.js +2 -0
  12. package/dist/adapters/index.js +44 -18
  13. package/dist/adapters/kiro.js +1 -1
  14. package/dist/adapters/opencode.js +1 -1
  15. package/dist/app/live.js +142 -0
  16. package/dist/app/page.js +701 -0
  17. package/dist/app/server.js +262 -0
  18. package/dist/args.js +18 -0
  19. package/dist/attach/connect.js +7 -4
  20. package/dist/attach/guard.js +21 -2
  21. package/dist/attach/live.js +5 -1
  22. package/dist/attach/plug.js +21 -3
  23. package/dist/attach/screen.js +150 -0
  24. package/dist/attach/session.js +7 -2
  25. package/dist/cli.js +134 -42
  26. package/dist/mcp/context-tools.js +79 -0
  27. package/dist/mcp/server.js +57 -11
  28. package/dist/orchestrator/ask.js +13 -10
  29. package/dist/orchestrator/brain.js +16 -4
  30. package/dist/orchestrator/budget.js +283 -0
  31. package/dist/orchestrator/busy.js +106 -0
  32. package/dist/orchestrator/council.js +434 -87
  33. package/dist/orchestrator/goal.js +196 -0
  34. package/dist/orchestrator/handoff.js +2 -2
  35. package/dist/orchestrator/launcher.js +31 -13
  36. package/dist/orchestrator/learn.js +101 -0
  37. package/dist/orchestrator/planner.js +54 -9
  38. package/dist/orchestrator/qa.js +71 -15
  39. package/dist/orchestrator/radar.js +719 -0
  40. package/dist/orchestrator/scheduler.js +575 -50
  41. package/dist/orchestrator/takeover.js +155 -0
  42. package/dist/platform.js +377 -0
  43. package/dist/room/context.js +1285 -0
  44. package/dist/room/contract.js +132 -0
  45. package/dist/room/db.js +79 -16
  46. package/dist/room/followup.js +233 -0
  47. package/dist/room/identity.js +2 -2
  48. package/dist/room/lessons.js +275 -0
  49. package/dist/room/live.js +75 -20
  50. package/dist/room/mail.js +225 -0
  51. package/dist/room/progress.js +40 -1
  52. package/dist/room/server.js +220 -39
  53. package/dist/room/tunnel.js +6 -5
  54. package/dist/room/user.js +54 -0
  55. package/dist/sandbox/sync.js +4 -4
  56. package/dist/sandbox/worktree.js +145 -14
  57. package/dist/ui/app.js +963 -159
  58. package/dist/ui/brief.js +494 -0
  59. package/dist/ui/catalog.js +62 -24
  60. package/dist/ui/dashboard.js +269 -55
  61. package/dist/ui/doctor.js +15 -11
  62. package/dist/ui/export.js +1194 -0
  63. package/dist/ui/lessons.js +81 -0
  64. package/dist/ui/narrator.js +56 -0
  65. package/dist/ui/paste.js +515 -0
  66. package/dist/ui/remote.js +11 -5
  67. package/dist/ui/setup.js +152 -0
  68. package/dist/ui/story.js +716 -94
  69. package/dist/ui/talk.js +176 -0
  70. package/dist/ui/term.js +182 -16
  71. package/dist/ui/transcript.js +434 -0
  72. package/dist/ui/watch.js +149 -48
  73. package/package.json +6 -3
@@ -0,0 +1,701 @@
1
+ // The dashboard page: one self-contained HTML document, no build step and
2
+ // nothing fetched from the internet. The client code below avoids template
3
+ // literals (the page itself is one) and builds every node with textContent
4
+ // or createElementNS, so nothing an agent wrote can run as HTML.
5
+ //
6
+ // What it shows, top to bottom: the run's name; what you'll get (or got), in
7
+ // prose; the live status (a sentence, the four steps, one chip per agent);
8
+ // a question waiting on you; then the run as a story: the design ideas, the
9
+ // plan, and the build grouped by part, each part with who built it and what
10
+ // teammates and reviewers said about it. The raw log is its own tab.
11
+ import { AGENT_COLOR } from "../ui/term.js";
12
+ const CSS = String.raw `
13
+ :root{
14
+ --bg:#faf9f5;--side:#f3f1ea;--panel:#ffffff;--raise:#fdfcf9;--hover:#ebe8df;
15
+ --line:#e7e3d9;--line2:#d9d4c7;--fg:#1f1e1b;--mute:#6e6b63;--faint:#a29e94;
16
+ --accent:#5b4bdb;--accent-soft:#eeebfd;--ok:#2f7d4f;--ok-soft:#e6f2ea;--warn:#a86a00;--warn-soft:#fbf0d9;--bad:#b9382a;--bad-soft:#f9e5e1;
17
+ --serif:ui-serif,"New York","Iowan Old Style","Palatino Linotype",Georgia,serif;
18
+ --sans:-apple-system,BlinkMacSystemFont,"SF Pro Text","Segoe UI",Inter,system-ui,sans-serif;
19
+ --mono:ui-monospace,"SF Mono",SFMono-Regular,Menlo,Consolas,monospace;
20
+ --shadow:0 1px 1px rgba(31,30,27,.04),0 2px 10px rgba(31,30,27,.05);
21
+ --shadow-lg:0 2px 4px rgba(31,30,27,.04),0 12px 32px rgba(31,30,27,.09);
22
+ color-scheme:light
23
+ }
24
+ @media (prefers-color-scheme:dark){:root:not([data-theme=light]){
25
+ --bg:#1c1b19;--side:#161513;--panel:#252420;--raise:#22211e;--hover:#2e2c28;
26
+ --line:#33312c;--line2:#403d37;--fg:#ecebe6;--mute:#a7a39a;--faint:#77736b;
27
+ --accent:#9b90ff;--accent-soft:#2b2846;--ok:#5cc489;--ok-soft:#1f3328;--warn:#e3ac43;--warn-soft:#3a2f18;--bad:#ef7564;--bad-soft:#3d221d;
28
+ --shadow:none;--shadow-lg:0 12px 32px rgba(0,0,0,.35);color-scheme:dark}}
29
+ *{box-sizing:border-box}
30
+ html,body{margin:0;height:100%;overflow:hidden;background:var(--bg);color:var(--fg);font:14.5px/1.55 var(--sans);-webkit-font-smoothing:antialiased}
31
+ button{font:inherit;color:inherit;cursor:pointer;background:none;border:0;padding:0}
32
+ svg{flex:none}
33
+ code{font:12.5px/1.4 var(--mono);background:var(--hover);padding:1px 5px;border-radius:5px}
34
+ .app{display:grid;grid-template-columns:264px minmax(0,1fr);height:100%}
35
+
36
+ aside{background:var(--side);border-right:1px solid var(--line);display:flex;flex-direction:column;min-height:0}
37
+ .brand{display:flex;align-items:center;gap:10px;padding:18px 18px 10px}
38
+ .mark{width:28px;height:28px;border-radius:9px;background:linear-gradient(140deg,#7a6cf5,#4c3cc9);display:grid;place-items:center;color:#fff;box-shadow:0 1px 2px rgba(76,60,201,.35);flex:none}
39
+ .brand .nm{font:600 15px/1.1 var(--sans);letter-spacing:-.01em}.brand .repo{color:var(--mute);font-size:12.5px;margin-top:2px;max-width:170px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
40
+ .new{display:flex;align-items:center;gap:9px;margin:6px 12px 10px;padding:8px 10px;border-radius:10px;color:var(--fg);font-weight:550}
41
+ .new:hover{background:var(--hover)}.new .plus{width:22px;height:22px;border-radius:7px;background:var(--accent);color:#fff;display:grid;place-items:center}
42
+ .label{padding:12px 20px 6px;color:var(--faint);font-size:11.5px;font-weight:600;letter-spacing:.02em}
43
+ .runs{overflow:auto;flex:1;min-height:0;padding:0 10px 10px}
44
+ .run{display:flex;gap:9px;align-items:flex-start;width:100%;text-align:left;border-radius:9px;padding:7px 10px;color:var(--fg)}
45
+ .run:hover{background:var(--hover)}.run.sel{background:var(--panel);box-shadow:var(--shadow)}
46
+ .run .ti{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:13.5px}
47
+ .run .when{color:var(--faint);font-size:11.5px;white-space:nowrap;padding-top:1px}
48
+ .st{width:7px;height:7px;border-radius:50%;margin-top:7px;flex:none;background:var(--line2)}
49
+ .st.ok{background:var(--ok)}.st.warn{background:var(--warn)}.st.live{background:var(--accent);animation:pulse 1.6s infinite}
50
+ @keyframes pulse{0%{box-shadow:0 0 0 0 rgba(91,75,219,.45)}70%{box-shadow:0 0 0 6px rgba(91,75,219,0)}100%{box-shadow:0 0 0 0 rgba(91,75,219,0)}}
51
+ .crew{border-top:1px solid var(--line);padding:12px 14px 14px}
52
+ .crew .row{display:flex;align-items:center;justify-content:space-between;color:var(--faint);font-size:11.5px;font-weight:600;padding:0 6px 8px}
53
+ .crew .row button{color:var(--faint);display:grid;place-items:center;width:22px;height:22px;border-radius:6px}.crew .row button:hover{background:var(--hover);color:var(--fg)}
54
+ .people{display:flex;flex-wrap:wrap;gap:6px;padding:0 4px}
55
+ .who{display:inline-flex;align-items:center;gap:6px;padding:3px 9px 3px 4px;border-radius:99px;background:var(--panel);border:1px solid var(--line);font-size:12.5px}
56
+ .who.off{opacity:.5}
57
+ .av{width:20px;height:20px;border-radius:50%;display:inline-grid;place-items:center;color:#fff;font:700 10.5px/1 var(--sans);flex:none}
58
+
59
+ main{position:relative;display:flex;flex-direction:column;min-width:0;min-height:0}
60
+ .scroll{flex:1;overflow:auto;min-height:0}
61
+ .col{max-width:780px;margin:0 auto;padding:40px 32px 170px}
62
+ .top{display:flex;gap:14px;align-items:flex-start}
63
+ .top .grow{flex:1;min-width:0}
64
+ h1{font:500 28px/1.25 var(--serif);letter-spacing:-.015em;margin:0}
65
+ .sub{display:flex;flex-wrap:wrap;align-items:center;gap:6px 14px;color:var(--mute);font-size:13px;margin-top:9px}
66
+ .pill{display:inline-flex;align-items:center;gap:5px;border-radius:99px;padding:2px 9px;font-size:12px;font-weight:600;background:var(--hover);color:var(--mute);white-space:nowrap}
67
+ .pill.ok{background:var(--ok-soft);color:var(--ok)}.pill.warn{background:var(--warn-soft);color:var(--warn)}.pill.bad{background:var(--bad-soft);color:var(--bad)}.pill.live{background:var(--accent-soft);color:var(--accent)}
68
+ .ghost{display:inline-flex;align-items:center;gap:6px;border-radius:9px;padding:6px 10px;color:var(--mute);font-size:13px;font-weight:550}
69
+ .ghost:hover{background:var(--hover);color:var(--fg)}.ghost.stop:hover{color:var(--bad)}
70
+ .menu{position:relative}.pop{position:absolute;right:0;top:38px;background:var(--panel);border:1px solid var(--line);border-radius:12px;box-shadow:var(--shadow-lg);padding:6px;z-index:5;width:250px;display:none}
71
+ .menu.open .pop{display:block}.pop a{display:block;padding:8px 10px;border-radius:8px;color:var(--fg);text-decoration:none;font-size:13.5px}.pop a:hover{background:var(--hover)}.pop a span{display:block;color:var(--mute);font-size:12px}
72
+
73
+ .prose{font:17.5px/1.6 var(--serif);margin:28px 0 0;color:var(--fg)}
74
+ .k{display:block;font:600 11.5px/1 var(--sans);color:var(--faint);letter-spacing:.05em;text-transform:uppercase;margin-bottom:10px}
75
+ .example{display:flex;align-items:center;flex-wrap:wrap;gap:10px;margin-top:12px;font:14px var(--sans);color:var(--mute)}
76
+ .example code{font-size:13px;padding:4px 9px;background:var(--panel);border:1px solid var(--line)}
77
+ .report{margin-top:28px;padding:20px 22px;background:var(--panel);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow);font-size:15px}
78
+
79
+ .status{margin-top:28px;padding:18px 20px;background:var(--panel);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow)}
80
+ .now{display:flex;gap:11px;align-items:flex-start;font-size:15.5px}
81
+ .now .dotlive{width:8px;height:8px;border-radius:50%;background:var(--accent);margin-top:8px;animation:pulse 1.6s infinite;flex:none}
82
+ .now .calm{color:var(--ok);font-size:12.5px;white-space:nowrap;margin-left:auto;padding-top:3px;font-weight:550}
83
+ .now.need .dotlive{background:var(--warn)}.now.need{color:var(--warn)}
84
+ .steps{display:grid;grid-template-columns:repeat(4,1fr);gap:6px;margin-top:16px}
85
+ .step .bar{height:4px;border-radius:99px;background:var(--hover);overflow:hidden}.step .bar i{display:block;height:100%;background:var(--accent);border-radius:99px;transition:width .6s ease}
86
+ .step .nm{font-size:12px;color:var(--faint);margin-top:6px;display:flex;justify-content:space-between}.step.on .nm{color:var(--fg);font-weight:600}.step.done .nm{color:var(--mute)}
87
+ .team{display:flex;flex-wrap:wrap;gap:8px;margin-top:16px}
88
+ .mate{display:flex;align-items:center;gap:8px;padding:4px 12px 4px 4px;border-radius:99px;border:1px solid var(--line);background:var(--raise);max-width:100%}
89
+ .mate .what{font-size:12.5px;color:var(--mute);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:240px}.mate b{font-size:12.5px;font-weight:600}
90
+ .ring{width:28px;height:28px;border-radius:50%;display:grid;place-items:center;flex:none}
91
+ .mate .tk{color:var(--faint);font-size:11.5px;font-variant-numeric:tabular-nums}
92
+
93
+ .ask{margin-top:18px;padding:18px 20px;border-radius:14px;border:1px solid var(--warn);background:var(--panel);box-shadow:0 0 0 4px var(--warn-soft)}
94
+ .ask .h{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--mute)}.ask .h b{color:var(--fg)}.ask .h .pill{margin-left:auto}
95
+ .ask .q{font:500 19px/1.35 var(--serif);margin:10px 0 4px}.ask .why{color:var(--mute);font-size:13.5px}
96
+ .opts{display:flex;flex-direction:column;gap:8px;margin:14px 0 10px}
97
+ .opt{display:flex;gap:12px;align-items:center;text-align:left;border:1px solid var(--line);background:var(--raise);border-radius:11px;padding:11px 13px;font-size:14.5px}
98
+ .opt:hover{border-color:var(--accent);background:var(--accent-soft)}.opt .n{width:22px;height:22px;border-radius:7px;border:1px solid var(--line2);display:grid;place-items:center;font-size:12px;font-weight:650;color:var(--mute);flex:none}
99
+ .opt .rec{margin-left:auto;font-size:11.5px;color:var(--accent);font-weight:650}
100
+ .own{display:flex;gap:8px}.own input{flex:1;border:1px solid var(--line);background:var(--raise);color:var(--fg);border-radius:10px;padding:9px 12px;font:inherit;outline:0}.own input:focus{border-color:var(--accent)}
101
+ .btn{border-radius:10px;padding:8px 14px;background:var(--fg);color:var(--bg);font-weight:600;font-size:13.5px}
102
+
103
+ .tabs{display:flex;gap:2px;margin:40px 0 4px;padding:3px;background:var(--hover);border-radius:11px;width:max-content;max-width:100%}
104
+ .tab{padding:5px 13px;border-radius:8px;color:var(--mute);font-weight:550;font-size:13px}.tab.on{background:var(--panel);color:var(--fg);box-shadow:var(--shadow)}
105
+
106
+ .sec{margin-top:32px}
107
+ .sec>.hd{display:flex;align-items:baseline;flex-wrap:wrap;gap:4px 10px;margin-bottom:12px}
108
+ .sec>.hd h2{font:500 19px/1.3 var(--serif);margin:0}.sec>.hd span{color:var(--faint);font-size:12.5px}
109
+ .ideas{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:10px}
110
+ .idea{border:1px solid var(--line);background:var(--panel);border-radius:12px;padding:12px 14px;box-shadow:var(--shadow);text-align:left;display:flex;flex-direction:column;gap:7px}
111
+ .idea:hover{border-color:var(--line2)}
112
+ .idea .by{display:flex;align-items:center;gap:7px;font-weight:600;font-size:13px}.idea .by .rd{margin-left:auto;color:var(--faint);font-weight:500;font-size:11.5px}
113
+ .idea .tx{font-size:13.5px;color:var(--mute);display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;overflow:hidden}
114
+ .idea.open{grid-column:1/-1;font-size:14px}
115
+ .notes{margin-top:14px;display:flex;flex-direction:column;gap:2px}
116
+ .note{display:flex;gap:10px;align-items:flex-start;font-size:13.5px;padding:5px 2px;color:var(--mute)}
117
+ .note b{color:var(--fg);font-weight:600}.note .ic{margin-top:3px}
118
+ .more{color:var(--accent);font-size:13px;font-weight:550;padding:4px 0;text-align:left}.more:hover{text-decoration:underline}
119
+
120
+ .card{border:1px solid var(--line);background:var(--panel);border-radius:14px;box-shadow:var(--shadow);overflow:hidden}
121
+ .task{display:grid;grid-template-columns:24px minmax(0,1fr) auto;gap:12px;align-items:center;padding:12px 16px;border-top:1px solid var(--line);text-align:left;width:100%}
122
+ .task:first-child{border-top:0}
123
+ button.task:hover{background:var(--raise)}
124
+ .task .num{width:24px;height:24px;border-radius:7px;background:var(--hover);display:grid;place-items:center;font-size:12px;font-weight:650;color:var(--mute)}
125
+ .task.done .num{background:var(--ok-soft);color:var(--ok)}.task.bad .num{background:var(--bad-soft);color:var(--bad)}.task.going .num{background:var(--accent-soft);color:var(--accent)}
126
+ .task .t{font-size:14px}.task .m{color:var(--faint);font-size:12px;margin-top:2px;display:flex;align-items:center;gap:6px}.task .m .av{width:16px;height:16px;font-size:9px}
127
+ .part{margin-top:10px}
128
+ .part>.task{border:1px solid var(--line);border-radius:14px;background:var(--panel);box-shadow:var(--shadow)}
129
+ .part.openp>.task{border-radius:14px 14px 0 0}
130
+ .thread{border:1px solid var(--line);border-top:0;border-radius:0 0 14px 14px;background:var(--raise);padding:4px 16px 8px 52px}
131
+ .ev{display:flex;gap:10px;padding:8px 0;font-size:13.5px;align-items:flex-start}
132
+ .ev .body{flex:1;min-width:0}.ev .line{color:var(--mute)}.ev .line b{color:var(--fg);font-weight:600}
133
+ .ev .said{margin-top:4px;color:var(--fg);white-space:pre-wrap;word-wrap:break-word}
134
+ .ev .t{color:var(--faint);font-size:11.5px;white-space:nowrap;padding-top:2px}
135
+ .ev.you .said{background:var(--accent-soft);padding:7px 11px;border-radius:10px;display:inline-block}
136
+
137
+ .md p{margin:0 0 8px}.md p:last-child{margin-bottom:0}.md ul{margin:4px 0 10px;padding-left:20px}.md li{margin:3px 0}.md h4{font:600 13.5px/1.3 var(--sans);margin:14px 0 4px}.md .lbl{font-weight:650}
138
+
139
+ .log{margin-top:16px}
140
+ .lg{display:grid;grid-template-columns:64px 20px minmax(0,1fr);gap:9px;padding:6px 0;font-size:13px;border-bottom:1px solid var(--line)}
141
+ .lg .t{color:var(--faint);font-variant-numeric:tabular-nums;font-size:12px;padding-top:1px}.lg .x{color:var(--mute)}.lg .x b{color:var(--fg);font-weight:600}
142
+ .lg .full{white-space:pre-wrap;color:var(--fg);margin-top:3px;word-wrap:break-word}
143
+ .cost .row{display:grid;grid-template-columns:140px minmax(0,1fr) auto;gap:14px;align-items:center;padding:12px 16px;border-top:1px solid var(--line);font-size:13.5px}.cost .row:first-child{border-top:0}
144
+ .cost .bar{height:6px;border-radius:99px;background:var(--hover);overflow:hidden}.cost .bar i{display:block;height:100%;border-radius:99px}
145
+ .cost .num{font-variant-numeric:tabular-nums;color:var(--mute);font-size:12.5px;text-align:right;min-width:110px}
146
+ .big{font:500 36px/1.1 var(--serif);letter-spacing:-.02em}.big span{font:500 14px var(--sans);color:var(--mute);margin-left:8px;letter-spacing:0}
147
+ pre.contract{font:12.5px/1.6 var(--mono);white-space:pre-wrap;margin:0;padding:16px 18px;max-height:460px;overflow:auto}
148
+ .empty{color:var(--mute);padding:28px 0;font-size:14px}
149
+
150
+ .dock{position:absolute;left:0;right:0;bottom:0;padding:0 32px 18px;background:linear-gradient(to bottom,rgba(250,249,245,0),var(--bg) 38%);pointer-events:none}
151
+ @media (prefers-color-scheme:dark){.dock{background:linear-gradient(to bottom,rgba(28,27,25,0),var(--bg) 38%)}}
152
+ .dock .in{max-width:780px;margin:0 auto;padding-top:28px;pointer-events:auto}
153
+ .box{background:var(--panel);border:1px solid var(--line2);border-radius:18px;box-shadow:var(--shadow-lg);padding:12px 12px 10px 16px;transition:border-color .15s}
154
+ .box:focus-within{border-color:var(--accent)}
155
+ .box textarea{display:block;width:100%;border:0;outline:0;background:none;color:var(--fg);font:15px/1.5 var(--sans);resize:none;max-height:200px;min-height:24px;padding:2px 0}
156
+ .box textarea::placeholder{color:var(--faint)}
157
+ .bar2{display:flex;align-items:center;gap:6px;margin-top:8px}
158
+ .bar2 .hint{color:var(--faint);font-size:12px;flex:1}
159
+ .ib{width:34px;height:34px;border-radius:10px;display:grid;place-items:center;color:var(--mute)}
160
+ .ib:hover{background:var(--hover);color:var(--fg)}
161
+ .ib.send{background:var(--accent);color:#fff}.ib.send:hover{background:var(--accent);color:#fff;filter:brightness(1.08)}.ib.send:disabled{opacity:.3;cursor:default;filter:none}
162
+ .ib.rec{background:var(--bad);color:#fff;animation:pulse 1.4s infinite}
163
+ .listening{display:none;align-items:center;gap:8px;color:var(--bad);font-size:12.5px;font-weight:600}.listening.on{display:flex}
164
+
165
+ .hero{max-width:640px;margin:0 auto;padding:17vh 32px 0;text-align:center}
166
+ .hero .mark{width:46px;height:46px;border-radius:14px;margin:0 auto 24px}
167
+ .hero h2{font:500 34px/1.2 var(--serif);letter-spacing:-.02em;margin:0 0 12px}.hero p{color:var(--mute);margin:0 auto;max-width:470px;font-size:15px}
168
+ .ex{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;margin-top:28px}
169
+ .ex button{border:1px solid var(--line);background:var(--panel);border-radius:99px;padding:7px 14px;font-size:13.5px;color:var(--mute);box-shadow:var(--shadow)}
170
+ .ex button:hover{color:var(--fg);border-color:var(--line2)}
171
+
172
+ .toast{position:fixed;bottom:124px;left:calc(50% + 132px);transform:translate(-50%,6px);background:var(--fg);color:var(--bg);padding:9px 15px;border-radius:11px;font-size:13px;opacity:0;transition:all .2s;pointer-events:none;box-shadow:var(--shadow-lg)}.toast.on{opacity:1;transform:translate(-50%,0)}
173
+ .side-toggle{display:none}
174
+ @media (max-width:860px){.app{grid-template-columns:1fr}aside{position:fixed;inset:0 auto 0 0;width:280px;z-index:9;transform:translateX(-100%);transition:transform .2s;box-shadow:var(--shadow-lg)}aside.open{transform:none}
175
+ .side-toggle{display:grid}.col{padding:22px 16px 170px}.dock{padding:0 16px 14px}.toast{left:50%}.steps{grid-template-columns:repeat(2,1fr)}h1{font-size:24px}}
176
+ `;
177
+ const JS = String.raw `
178
+ "use strict";
179
+ var TOKEN = window.__HUDDLE_TOKEN__, COLORS = window.__HUDDLE_COLORS__;
180
+ var S = { state: null, doc: null, docKey: "", sel: null, tab: "story", open: {}, fresh: false };
181
+ var $ = function (sel) { return document.querySelector(sel); };
182
+
183
+ // ---- a tiny DOM kit ----------------------------------------------------------------
184
+ function h(tag, attrs) {
185
+ var el = document.createElement(tag);
186
+ if (attrs) for (var k in attrs) {
187
+ var v = attrs[k];
188
+ if (v == null || v === false) continue;
189
+ if (k === "class") el.className = v;
190
+ else if (k === "text") el.textContent = v;
191
+ else if (k.slice(0, 2) === "on") el.addEventListener(k.slice(2), v);
192
+ else el.setAttribute(k, v === true ? "" : v);
193
+ }
194
+ for (var i = 2; i < arguments.length; i++) add(el, arguments[i]);
195
+ return el;
196
+ }
197
+ function add(el, c) {
198
+ if (c == null || c === false) return;
199
+ if (Array.isArray(c)) { c.forEach(function (x) { add(el, x); }); return; }
200
+ el.appendChild(typeof c === "string" || typeof c === "number" ? document.createTextNode(String(c)) : c);
201
+ }
202
+ var ICONS = {
203
+ plus: "M12 5v14M5 12h14",
204
+ mic: "M12 3a3 3 0 0 0-3 3v6a3 3 0 0 0 6 0V6a3 3 0 0 0-3-3zM19 11a7 7 0 0 1-14 0M12 18v3",
205
+ up: "M12 19V5M5 12l7-7 7 7",
206
+ stop: "M7 7h10v10H7z",
207
+ down: "M12 5v14M5 12l7 7 7-7",
208
+ refresh: "M20 11a8 8 0 1 0-2.3 5.7M20 4v7h-7",
209
+ menu: "M4 6h16M4 12h16M4 18h16",
210
+ check: "M5 12.5l4.5 4.5L19 7.5",
211
+ x: "M6 6l12 12M18 6L6 18",
212
+ bulb: "M9 18h6M10 21h4M12 3a6 6 0 0 0-3.5 10.9c.6.5 1 1.2 1 2.1h5c0-.9.4-1.6 1-2.1A6 6 0 0 0 12 3z",
213
+ alert: "M12 9v4M12 17h.01M10.3 3.9L2.4 18a2 2 0 0 0 1.7 3h15.8a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z",
214
+ msg: "M21 12a8 8 0 0 1-11.6 7.1L4 21l1.9-5.4A8 8 0 1 1 21 12z",
215
+ merge: "M6 3v12M18 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM18 9a9 9 0 0 1-9 9",
216
+ bug: "M8 9V7a4 4 0 0 1 8 0v2M12 9v12M6 9h12v5a6 6 0 0 1-12 0V9zM3 13h3M18 13h3",
217
+ flag: "M5 21V4M5 4h11l-2 4 2 4H5"
218
+ };
219
+ function icon(name, size, color) {
220
+ var NS = "http://www.w3.org/2000/svg", s = document.createElementNS(NS, "svg");
221
+ s.setAttribute("viewBox", "0 0 24 24"); s.setAttribute("width", size || 16); s.setAttribute("height", size || 16);
222
+ s.setAttribute("fill", "none"); s.setAttribute("stroke", color || "currentColor"); s.setAttribute("stroke-width", "1.8");
223
+ s.setAttribute("stroke-linecap", "round"); s.setAttribute("stroke-linejoin", "round"); s.setAttribute("class", "ic");
224
+ var p = document.createElementNS(NS, "path"); p.setAttribute("d", ICONS[name]); s.appendChild(p);
225
+ return s;
226
+ }
227
+ function logo(size) { var m = h("div", { class: "mark" }); m.appendChild(icon("msg", size || 15, "#fff")); return m; }
228
+ function color(id) { if (!id) return "#8a857c"; if (id === "you") return "#5b4bdb"; return COLORS[String(id).split("#")[0]] || "#8a857c"; }
229
+ function av(id) { var n = id === "you" ? "Y" : id === "huddle" ? "h" : String(id || "?").charAt(0).toUpperCase(); return h("span", { class: "av", style: "background:" + color(id), title: id, text: n }); }
230
+ function name(id) { return id === "you" ? "You" : id === "huddle" ? "Huddle" : id; }
231
+ function cap(s) { s = String(s || ""); return s.charAt(0).toUpperCase() + s.slice(1); }
232
+
233
+ /** Agents write light Markdown: headings, bullets, LABEL lines, code spans, bold. Built as DOM, never parsed as HTML. */
234
+ function md(text) {
235
+ var root = h("div", { class: "md" }), list = null;
236
+ String(text || "").split("\n").forEach(function (raw) {
237
+ var line = raw.replace(/\s+$/, ""), m;
238
+ if (!line.trim()) { list = null; return; }
239
+ if ((m = /^\s*#{1,6}\s+(.*)$/.exec(line))) { list = null; root.appendChild(inline(h("h4"), m[1])); return; }
240
+ if ((m = /^\s*(?:[-*•]|\d+[.)])\s+(.*)$/.exec(line))) { if (!list) { list = h("ul"); root.appendChild(list); } list.appendChild(inline(h("li"), m[1])); return; }
241
+ list = null;
242
+ if ((m = /^([A-Z][A-Z /&-]{2,30}):\s*(.*)$/.exec(line))) { var p = h("p"); p.appendChild(h("span", { class: "lbl", text: cap(m[1].toLowerCase()) + (m[2] ? ": " : "") })); inline(p, m[2]); root.appendChild(p); return; }
243
+ root.appendChild(inline(h("p"), line));
244
+ });
245
+ return root;
246
+ }
247
+ function inline(el, s) {
248
+ var re = /(\x60[^\x60]+\x60|\*\*[^*]+\*\*)/g, last = 0, m;
249
+ while ((m = re.exec(s))) {
250
+ if (m.index > last) el.appendChild(document.createTextNode(s.slice(last, m.index)));
251
+ var t = m[0];
252
+ el.appendChild(t.charAt(0) === "\x60" ? h("code", { text: t.slice(1, -1) }) : h("b", { text: t.slice(2, -2) }));
253
+ last = m.index + t.length;
254
+ }
255
+ if (last < s.length) el.appendChild(document.createTextNode(s.slice(last)));
256
+ return el;
257
+ }
258
+ /** The first sentence of an idea, without its "ARCHITECTURE:" label or code ticks. */
259
+ function gist(text, n) {
260
+ n = n || 220;
261
+ var t = String(text || "").replace(/^\s*[A-Z][A-Z /&-]{2,30}:\s*/, "").replace(/\x60/g, "").replace(/\s+/g, " ").trim();
262
+ var cut = t.search(/[.!?](\s|$)/);
263
+ if (cut > 30 && cut < n) return t.slice(0, cut + 1);
264
+ return t.length > n ? t.slice(0, n - 1).replace(/\s+\S*$/, "") + "…" : t;
265
+ }
266
+
267
+ function api(path, body) {
268
+ return fetch(path, { method: body ? "POST" : "GET", headers: { "content-type": "application/json", "x-huddle-token": TOKEN }, body: body ? JSON.stringify(body) : undefined })
269
+ .then(function (r) { return r.json().then(function (j) { if (!r.ok) throw new Error(j.error || r.statusText); return j; }); });
270
+ }
271
+ function tok(n) { n = n || 0; return n >= 1e6 ? (n / 1e6).toFixed(n >= 1e7 ? 0 : 1) + "M" : n >= 1e3 ? Math.round(n / 1e3) + "k" : String(n); }
272
+ function dur(ms) { var s = Math.max(0, Math.round((ms || 0) / 1000)); if (s < 60) return s + "s"; var m = Math.floor(s / 60); if (m < 60) return m + "m" + (s % 60 ? " " + (s % 60) + "s" : ""); return Math.floor(m / 60) + "h " + (m % 60) + "m"; }
273
+ function ago(ts) { var d = Date.now() - ts; if (d < 60e3) return "now"; if (d < 3600e3) return Math.round(d / 60e3) + "m"; if (d < 86400e3) return Math.round(d / 3600e3) + "h"; return new Date(ts).toLocaleDateString(undefined, { month: "short", day: "numeric" }); }
274
+ function clock(ts) { return new Date(ts).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" }); }
275
+ var toastT;
276
+ function toast(t) { var el = $(".toast"); el.textContent = t; el.classList.add("on"); clearTimeout(toastT); toastT = setTimeout(function () { el.classList.remove("on"); }, 2600); }
277
+
278
+ // ---- data ----------------------------------------------------------------------
279
+ var busy = false, again = false;
280
+ function refresh() {
281
+ if (busy) { again = true; return; }
282
+ busy = true;
283
+ api("/api/state").then(function (st) {
284
+ S.state = st;
285
+ var live = st.live;
286
+ if (S.sel === null && !S.fresh) S.sel = live ? "live" : st.runs[0] ? st.runs[0].index : null;
287
+ if (S.sel === "live" && !live) S.sel = st.runs[0] ? st.runs[0].index : null;
288
+ var n = viewRunIndex();
289
+ if (n == null) { S.doc = null; S.docKey = ""; return null; }
290
+ var row = st.runs.find(function (r) { return r.index === n; }) || {};
291
+ var key = n + ":" + row.tokens + ":" + row.merged + ":" + (S.sel === "live" && live ? Math.floor(live.updatedAt / 3000) : "");
292
+ if (key === S.docKey) return null;
293
+ return api("/api/run?n=" + n).then(function (d) { S.doc = d; S.docKey = key; });
294
+ }).catch(function (e) { toast(e.message); }).then(function () {
295
+ busy = false; render();
296
+ if (again) { again = false; refresh(); }
297
+ });
298
+ }
299
+ function viewRunIndex() {
300
+ var st = S.state; if (!st || S.sel == null) return null;
301
+ if (S.sel === "live") {
302
+ var l = st.live; if (!l || !l.session) return null;
303
+ var r = st.runs.find(function (x) { return x.id === l.session; });
304
+ return r ? r.index : null;
305
+ }
306
+ return S.sel;
307
+ }
308
+
309
+ // ---- render --------------------------------------------------------------------
310
+ function render() {
311
+ var st = S.state; if (!st) return;
312
+ renderSide(st);
313
+ var main = $("#main"), keep = main.scrollTop;
314
+ main.textContent = "";
315
+ var live = S.sel === "live" ? st.live : null;
316
+ if (!live && !S.doc) { main.appendChild(hero()); setComposer(false); return; }
317
+ var c = h("div", { class: "col" });
318
+ c.appendChild(header(live));
319
+ var plain = live ? live.plain : S.doc && S.doc.plain;
320
+ var report = !live && S.doc && S.doc.report;
321
+ if (plain && plain.what && !report) c.appendChild(h("div", { class: "prose" }, h("span", { class: "k", text: "What you'll get" }), plain.what,
322
+ plain.example ? h("div", { class: "example" }, "For example", h("code", { text: plain.example })) : null));
323
+ if (report) c.appendChild(h("div", { class: "report" }, h("span", { class: "k", text: "What you got" }), md(report)));
324
+ if (live) { c.appendChild(status(live)); if (live.ask) c.appendChild(askCard(live.ask)); }
325
+ c.appendChild(tabs());
326
+ if (S.tab === "story") story(c, live); else if (S.tab === "plan") planTab(c); else if (S.tab === "cost") costTab(c, live); else logTab(c, live);
327
+ main.appendChild(c);
328
+ main.scrollTop = keep;
329
+ setComposer(!!live);
330
+ }
331
+
332
+ function renderSide(st) {
333
+ $("#repo").textContent = st.repo.name;
334
+ var runs = $("#runs"); runs.textContent = "";
335
+ if (st.live) runs.appendChild(runBtn("live", st.live.goal, "live", "live"));
336
+ st.runs.forEach(function (r) {
337
+ if (st.live && st.live.session && r.id === st.live.session) return;
338
+ runs.appendChild(runBtn(r.index, r.goal, ago(r.start), r.failed ? "warn" : r.merged ? "ok" : ""));
339
+ });
340
+ if (!st.live && !st.runs.length) runs.appendChild(h("div", { class: "empty", style: "padding:6px 10px;font-size:13px", text: "Your runs will show up here." }));
341
+ var ppl = $("#people"); ppl.textContent = "";
342
+ if (st.probing) ppl.appendChild(h("span", { class: "who off", text: "checking…" }));
343
+ st.agents.forEach(function (a) {
344
+ var ready = a.state === "ready";
345
+ ppl.appendChild(h("span", { class: "who" + (ready ? "" : " off"), title: ready ? a.id + " is ready" : a.id + ": " + (a.state === "not_installed" ? "not installed" : a.state === "skipped" ? "skipped" : "not signed in") }, av(a.id), a.id));
346
+ });
347
+ }
348
+ function runBtn(key, goal, when, tone) {
349
+ return h("button", { class: "run" + (S.sel === key ? " sel" : ""), title: goal, onclick: function () { S.sel = key; S.fresh = false; S.docKey = ""; S.tab = "story"; $("aside").classList.remove("open"); $("#main").scrollTop = 0; refresh(); } },
350
+ h("span", { class: "st " + tone }), h("span", { class: "ti", text: goal }), h("span", { class: "when", text: when }));
351
+ }
352
+
353
+ function header(live) {
354
+ var d = S.doc, title = live ? live.goal : d.run.title, sub = [];
355
+ if (live) {
356
+ sub.push(h("span", { class: "pill live", text: "Live" }), h("span", { text: "started " + clock(live.startedAt) }), h("span", { text: tok(live.tokens) + " tokens" + (live.budget ? " of " + tok(live.budget.limit) : "") }));
357
+ } else {
358
+ var r = d.summary.result;
359
+ sub.push(h("span", { class: "pill " + (r.failed ? "warn" : r.merged ? "ok" : ""), text: r.merged + " merged" + (r.failed ? " · " + r.failed + " failed" : "") }),
360
+ h("span", { text: new Date(d.run.start).toLocaleString(undefined, { weekday: "short", month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }) }),
361
+ h("span", { text: dur(d.run.ms) }), h("span", { text: tok(r.tokens) + " tokens" }));
362
+ if (r.qa) sub.push(h("span", { class: "pill " + r.qa.tone, text: r.qa.text }));
363
+ }
364
+ var right = h("div", { style: "display:flex;gap:2px" });
365
+ if (live) right.appendChild(h("button", { class: "ghost stop", onclick: stop }, icon("stop", 14), "Stop"));
366
+ var n = viewRunIndex();
367
+ if (n != null) right.appendChild(exportMenu(n));
368
+ return h("div", { class: "top" }, h("button", { class: "ib side-toggle", onclick: function () { $("aside").classList.add("open"); }, "aria-label": "Runs" }, icon("menu", 18)),
369
+ h("div", { class: "grow" }, h("h1", { text: title }), h("div", { class: "sub" }, sub)), right);
370
+ }
371
+ function exportMenu(n) {
372
+ var m = h("div", { class: "menu" });
373
+ function link(label, sub, q) { return h("a", { href: "/api/export?t=" + encodeURIComponent(TOKEN) + "&n=" + n + "&" + q }, label, h("span", { text: sub })); }
374
+ m.appendChild(h("button", { class: "ghost", onclick: function (e) { e.stopPropagation(); m.classList.toggle("open"); } }, icon("down", 14), "Export"));
375
+ m.appendChild(h("div", { class: "pop" },
376
+ link("Conversation", "a web page you can share", "format=html&level=chat"),
377
+ link("Markdown", "for notes or a pull request", "format=md&level=chat"),
378
+ link("Summary", "result, decisions, cost", "format=md&level=summary"),
379
+ link("Everything", "with each agent's own session", "format=html&level=full"),
380
+ link("JSON", "for tools", "format=json&level=chat")));
381
+ return m;
382
+ }
383
+
384
+ var STEPS = [["COUNCIL", "Design"], ["PLAN", "Plan"], ["BUILD", "Build"], ["QA", "Test"]];
385
+ function status(l) {
386
+ var n = l.narrator || { text: "", needsYou: false };
387
+ var r = l.run || { phase: "WAITING", done: 0, total: 0 };
388
+ var at = STEPS.findIndex(function (s) { return s[0] === r.phase; });
389
+ if (r.phase === "DONE") at = 4;
390
+ var steps = h("div", { class: "steps" }, STEPS.map(function (s, i) {
391
+ var pct = i < at ? 100 : i > at ? 0 : s[0] === "COUNCIL" ? ((r.step || 1) - 0.5) / 4 * 100 : s[0] === "BUILD" ? (r.total ? Math.max(4, r.done / r.total * 100) : 4) : 50;
392
+ var note = i === at && s[0] === "BUILD" && r.total ? r.done + " of " + r.total : i === at && s[0] === "COUNCIL" ? (r.step || 1) + " of 4" : "";
393
+ return h("div", { class: "step" + (i === at ? " on" : i < at ? " done" : "") }, h("div", { class: "bar" }, h("i", { style: "width:" + Math.round(pct) + "%" })),
394
+ h("div", { class: "nm" }, h("span", { text: s[1] }), h("span", { text: note })));
395
+ }));
396
+ var team = h("div", { class: "team" }, l.agents.map(function (a) {
397
+ var p = a.busy ? Math.max(6, a.pct || 0) : a.tasks ? 100 : 0;
398
+ return h("div", { class: "mate", title: a.doing || "" },
399
+ h("span", { class: "ring", style: "background:conic-gradient(" + color(a.id) + " " + p + "%, var(--line) 0)" }, av(a.id)),
400
+ h("b", { text: a.id }), h("span", { class: "what", text: a.busy ? (a.doing || "working") : a.tasks ? "done · " + a.tasks + " merged" : "waiting" }),
401
+ a.tokens ? h("span", { class: "tk", text: tok(a.tokens) }) : null);
402
+ }));
403
+ return h("div", { class: "status" },
404
+ h("div", { class: "now" + (n.needsYou ? " need" : "") }, h("span", { class: "dotlive" }), h("span", { text: cap(n.text || "getting started") }),
405
+ n.needsYou ? null : h("span", { class: "calm", text: "Nothing needed from you" })),
406
+ steps, team);
407
+ }
408
+
409
+ function askCard(q) {
410
+ var own = h("input", { placeholder: "Or answer in your own words…", onkeydown: function (e) { if (e.key === "Enter" && own.value.trim()) answer(q, own.value.trim()); } });
411
+ return h("div", { class: "ask" },
412
+ h("div", { class: "h" }, av(q.from), h("b", { text: q.from }), "has a question for you", h("span", { class: "pill warn", text: "picks 1 in " + dur(q.until - Date.now()) })),
413
+ h("div", { class: "q", text: q.question }), q.why ? h("div", { class: "why", text: q.why }) : null,
414
+ h("div", { class: "opts" }, q.options.map(function (o, i) {
415
+ return h("button", { class: "opt", onclick: function () { answer(q, String(i + 1)); } }, h("span", { class: "n", text: String(i + 1) }), h("span", { text: o }), i === 0 ? h("span", { class: "rec", text: "Recommended" }) : null);
416
+ })),
417
+ h("div", { class: "own" }, own, h("button", { class: "btn", text: "Send", onclick: function () { if (own.value.trim()) answer(q, own.value.trim()); } })));
418
+ }
419
+
420
+ function tabs() {
421
+ function t(k, label) { return h("button", { class: "tab" + (S.tab === k ? " on" : ""), text: label, onclick: function () { S.tab = k; render(); } }); }
422
+ return h("div", { class: "tabs" }, t("story", "Story"), t("plan", "Plan"), t("cost", "Cost"), t("log", "Full log"));
423
+ }
424
+ function sec(title, sub) { return h("div", { class: "sec" }, h("div", { class: "hd" }, h("h2", { text: title }), sub ? h("span", { text: sub }) : null)); }
425
+ function toggle(key) { S.open[key] = !S.open[key]; render(); }
426
+
427
+ // ---- the story -------------------------------------------------------------------
428
+ function story(c, live) {
429
+ var msgs = S.doc ? S.doc.messages : live ? councilMessages(live) : [];
430
+ if (!msgs.length) { c.appendChild(h("div", { class: "empty", text: "The agents are getting started…" })); return; }
431
+ var by = function (k) { return msgs.filter(function (m) { return m.kind === k; }); };
432
+
433
+ // 1. Designing together: each agent's idea, then what they took from and found wrong in each other's.
434
+ var props = by("proposal");
435
+ if (props.length) {
436
+ var s = sec("They designed it together", props.length + " ideas · " + by("adopt").length + " borrowed · " + by("flaw").length + " problems spotted");
437
+ s.appendChild(h("div", { class: "ideas" }, props.map(function (m) {
438
+ var k = "idea" + m.i, open = S.open[k];
439
+ return h("button", { class: "idea" + (open ? " open" : ""), onclick: function () { toggle(k); } },
440
+ h("div", { class: "by" }, av(m.from), m.from, h("span", { class: "rd", text: open ? "Show less" : "Read" })),
441
+ open ? md(m.text) : h("div", { class: "tx", text: gist(m.text, 260) }));
442
+ })));
443
+ var notes = by("adopt").concat(by("flaw"), by("ratify")).sort(function (a, b) { return a.i - b.i; });
444
+ if (notes.length) {
445
+ var all = S.open.notes, list = h("div", { class: "notes" });
446
+ (all ? notes : notes.slice(0, 4)).forEach(function (m) {
447
+ var adopt = m.kind === "adopt", flaw = m.kind === "flaw", who = m.to && m.to[0];
448
+ list.appendChild(h("div", { class: "note" }, icon(adopt ? "bulb" : flaw ? "alert" : "flag", 15, adopt ? "var(--ok)" : "var(--warn)"),
449
+ h("div", null, h("b", { text: m.from }), adopt ? " took " + who + "'s idea: " : flaw ? " spotted a problem in " + who + "'s: " : " " + m.label + ": ", gist(m.text, 200))));
450
+ });
451
+ if (notes.length > 4) list.appendChild(h("button", { class: "more", text: all ? "Show less" : "Show all " + notes.length, onclick: function () { toggle("notes"); } }));
452
+ s.appendChild(list);
453
+ }
454
+ c.appendChild(s);
455
+ }
456
+
457
+ // 2. The plan, built one part at a time: who built each part and what was said about it.
458
+ var tasks = S.doc ? S.doc.summary.plan.tasks : [];
459
+ var parts = buildParts(msgs, tasks);
460
+ if (parts.length) {
461
+ var planner = S.doc.summary.plan.by.split("→").pop().trim();
462
+ var bs = sec("The plan, part by part", parts.filter(function (p) { return p.task.status === "merged"; }).length + " of " + parts.length + " merged" + (planner ? " · planned by " + planner : "") + " · open a part to see what was said");
463
+ parts.forEach(function (p) {
464
+ var k = "part" + p.id, open = S.open[k] !== undefined ? S.open[k] : !!live && p.task.status !== "merged";
465
+ var wrap = h("div", { class: "part" + (open && p.events.length ? " openp" : "") });
466
+ wrap.appendChild(taskRow(p.task, p.n, true, function () { S.open[k] = !open; render(); }, p.events.length));
467
+ if (open && p.events.length) wrap.appendChild(h("div", { class: "thread" }, p.events.map(evRow)));
468
+ bs.appendChild(wrap);
469
+ });
470
+ c.appendChild(bs);
471
+ }
472
+
473
+ // 4. Testing.
474
+ var bugs = by("bug").concat(by("qa"));
475
+ if (bugs.length) {
476
+ var qs = sec("Then they tested it", by("bug").length + " bug" + (by("bug").length === 1 ? "" : "s") + " found");
477
+ qs.appendChild(h("div", { class: "card", style: "padding:4px 16px" }, bugs.map(evRow)));
478
+ c.appendChild(qs);
479
+ }
480
+ }
481
+
482
+ var STATUS = { merged: ["done", "Merged"], failed: ["bad", "Failed"], in_progress: ["going", "Building"], claimed: ["going", "Starting"], review: ["going", "In review"], backlog: ["", "Waiting"], needs_you: ["bad", "Needs you"] };
483
+ function taskRow(t, i, clickable, onclick, count) {
484
+ var s = STATUS[t.status] || ["", t.status ? cap(t.status) : "Not started"];
485
+ var who = t.doneBy || "";
486
+ var meta = [];
487
+ if (who) meta.push(av(who), who);
488
+ if (count) meta.push((who ? " · " : "") + count + " update" + (count === 1 ? "" : "s"));
489
+ return h(clickable ? "button" : "div", { class: "task " + s[0], onclick: clickable ? onclick : null },
490
+ h("span", { class: "num" }, s[0] === "done" ? icon("check", 13) : s[0] === "bad" ? icon("x", 13) : String(i + 1)),
491
+ h("div", null, h("div", { class: "t", text: t.title }), meta.length ? h("div", { class: "m" }, meta) : null),
492
+ h("span", { class: "pill " + (s[0] === "done" ? "ok" : s[0] === "bad" ? "bad" : s[0] === "going" ? "live" : ""), text: s[1] }));
493
+ }
494
+
495
+ /** The build's messages, grouped by the part they're about (an untagged message goes with its sender's current part). */
496
+ function buildParts(msgs, tasks) {
497
+ var parts = [], byId = {}, current = {};
498
+ var SHOW = { progress: 1, buddy: 1, review: 1, message: 1, question: 1, answer: 1, merge: 1, fail: 1, radar: 1, done: 1 };
499
+ function part(id, title) {
500
+ if (!byId[id]) {
501
+ var n = tasks.findIndex(function (t) { return t.id === id; });
502
+ var t = n >= 0 ? tasks[n] : { id: id, title: title, status: "" };
503
+ byId[id] = { id: id, n: n >= 0 ? n : 99 + parts.length, task: t, events: [] };
504
+ parts.push(byId[id]);
505
+ }
506
+ return byId[id];
507
+ }
508
+ tasks.forEach(function (t) { part(t.id, t.title); });
509
+ msgs.forEach(function (m) {
510
+ if (m.phase !== "build" && m.phase !== "qa") return;
511
+ var p = null;
512
+ if (m.task) { p = part(m.task.id, m.task.title); if (m.kind === "announce" || m.kind === "progress") current[m.from] = p; }
513
+ else if (current[m.from]) p = current[m.from];
514
+ if (!p || !SHOW[m.kind]) return;
515
+ p.events.push(m);
516
+ });
517
+ parts.forEach(function (p) {
518
+ // Only a part's newest progress note: the others are on the way there.
519
+ var prog = p.events.filter(function (m) { return m.kind === "progress"; });
520
+ if (prog.length > 1) p.events = p.events.filter(function (m) { return m.kind !== "progress" || m === prog[prog.length - 1]; });
521
+ });
522
+ return parts.sort(function (a, b) { return a.n - b.n; });
523
+ }
524
+ function evRow(m) {
525
+ var IC = { merge: "merge", fail: "x", bug: "bug" };
526
+ var tone = m.kind === "merge" ? "var(--ok)" : "var(--bad)";
527
+ var head = m.kind === "merge" ? [h("b", { text: "Merged" }), " · " + m.text] : [h("b", { text: name(m.from) }), " " + plainLabel(m)];
528
+ var said = m.kind === "merge" ? "" : m.text || "", k = "ev" + m.i, long = said.length > 600 && !S.open[k];
529
+ return h("div", { class: "ev" + (m.from === "you" ? " you" : "") }, IC[m.kind] ? icon(IC[m.kind], 18, tone) : av(m.from),
530
+ h("div", { class: "body" }, h("div", { class: "line" }, head),
531
+ said ? h("div", { class: "said", text: long ? said.slice(0, 520).replace(/\s+\S*$/, "") + "…" : said }) : null,
532
+ long ? h("button", { class: "more", text: "Show all", onclick: function () { toggle(k); } }) : null),
533
+ h("span", { class: "t", text: m.ts ? clock(m.ts) : "" }));
534
+ }
535
+ /** In a part's thread the part is already named, so labels say what happened, not to what. */
536
+ function plainLabel(m) {
537
+ var to = m.to && m.to[0] && m.to[0] !== "room" && m.to[0] !== "all" ? m.to[0] : "";
538
+ switch (m.kind) {
539
+ case "progress": return "is " + ((/^(\d+)%/.exec(m.label) || ["", "?"])[1]) + "% done";
540
+ case "review": return /approv/i.test(m.label) ? "reviewed it and approved" : /chang/i.test(m.label) ? "reviewed it and asked for changes" : m.label;
541
+ case "buddy": return "took a look mid-way" + (/looks good/i.test(m.label) ? ": looks good" : "");
542
+ case "message": return to ? "wrote to " + to : "said";
543
+ case "question": return to === "you" ? "asked you" : "asked" + (to ? " " + to : "");
544
+ case "answer": return "answered" + (to ? " " + to : "");
545
+ case "done": return "finished it";
546
+ default: return m.label;
547
+ }
548
+ }
549
+ function councilMessages(l) {
550
+ return l.council.map(function (c, i) {
551
+ var kind = c.kind === "ok" || c.kind === "object" || c.kind === "skipped" ? "ratify" : c.kind;
552
+ return { i: i + 1, ts: l.startedAt, phase: "council", kind: kind, from: c.agent, to: c.of ? [c.of] : [], label: c.kind === "ok" ? "approved the plan" : c.kind === "object" ? "objected to the plan" : c.kind === "skipped" ? "didn't answer in time" : c.kind, text: c.text };
553
+ });
554
+ }
555
+
556
+ // ---- plan, cost, log ------------------------------------------------------------------
557
+ function planTab(c) {
558
+ var d = S.doc;
559
+ if (!d) { c.appendChild(h("div", { class: "empty", text: "The plan appears once the agents agree on it." })); return; }
560
+ var s = sec("Who does what", "planned by " + d.summary.plan.by);
561
+ s.appendChild(h("div", { class: "card" }, d.summary.plan.tasks.map(function (t, i) { return taskRow(t, i, false); })));
562
+ c.appendChild(s);
563
+ var dec = d.summary.decisions;
564
+ if (dec.length) {
565
+ var a = sec("What they agreed", "every agent builds to these");
566
+ a.appendChild(h("div", { class: "card", style: "padding:6px 16px" }, dec.map(function (x) {
567
+ return h("div", { class: "note" }, icon(x.risk ? "alert" : "check", 15, x.risk ? "var(--warn)" : "var(--ok)"), h("div", null, x.risk ? h("b", { text: "Risk" + (x.by ? " (" + x.by + ")" : "") + ": " }) : null, inline(h("span"), x.body)));
568
+ })));
569
+ c.appendChild(a);
570
+ }
571
+ var k = d.summary.contract;
572
+ if (k && k.text) { var cs = sec("The contract", "the exact names every part uses · by " + k.by); cs.appendChild(h("div", { class: "card" }, h("pre", { class: "contract", text: k.text }))); c.appendChild(cs); }
573
+ }
574
+ function costTab(c, live) {
575
+ var rows = live ? live.agents.map(function (a) { return { agent: a.id, tokens: a.tokens, ms: 0 }; }) : S.doc ? S.doc.summary.cost : [];
576
+ var max = Math.max.apply(null, rows.map(function (r) { return r.tokens; }).concat([1]));
577
+ var total = rows.reduce(function (n, r) { return n + r.tokens; }, 0);
578
+ var s = sec("What it cost", "each agent bills its own account");
579
+ s.appendChild(h("div", { class: "big", style: "margin:2px 0 16px" }, tok(total), h("span", { text: "tokens" + (live && live.budget ? " of a " + tok(live.budget.limit) + " budget" : "") })));
580
+ s.appendChild(h("div", { class: "card cost" }, rows.map(function (r) {
581
+ return h("div", { class: "row" }, h("span", { style: "display:flex;align-items:center;gap:8px" }, av(r.agent), r.agent),
582
+ h("div", { class: "bar" }, h("i", { style: "width:" + Math.max(2, Math.round(r.tokens / max * 100)) + "%;background:" + color(r.agent) })),
583
+ h("span", { class: "num", text: tok(r.tokens) + (r.ms ? " · " + dur(r.ms) : "") }));
584
+ })));
585
+ c.appendChild(s);
586
+ }
587
+ function logTab(c, live) {
588
+ var msgs = S.doc ? S.doc.messages : live ? councilMessages(live) : [];
589
+ var box = h("div", { class: "log" });
590
+ msgs.forEach(function (m) {
591
+ var k = "lg" + m.i, open = S.open[k], long = m.text && m.text.length > 160;
592
+ box.appendChild(h("div", { class: "lg", onclick: long ? function () { toggle(k); } : null, style: long ? "cursor:pointer" : null },
593
+ h("span", { class: "t", text: m.ts ? clock(m.ts) : "" }), av(m.from),
594
+ h("div", { class: "x" }, h("b", { text: name(m.from) }), " " + m.label + (m.to && m.to.length && m.label.indexOf(m.to[0]) < 0 ? " → " + m.to.join(", ") : ""),
595
+ m.text ? h("div", { class: "full", text: open || !long ? m.text : m.text.slice(0, 160).replace(/\s+\S*$/, "") + "…" }) : null)));
596
+ });
597
+ c.appendChild(box);
598
+ }
599
+
600
+ function hero() {
601
+ var ex = ["A todo app with a REST API and tests", "A CLI that converts temperatures", "Fix the failing tests in this repo"];
602
+ return h("div", { class: "hero" }, logo(22),
603
+ h("h2", { text: "What should the team build?" }),
604
+ h("p", { text: "Describe it in plain words. Your agents design it together, split the work, review each other, and merge only what passes the tests." }),
605
+ h("div", { class: "ex" }, ex.map(function (x) { return h("button", { text: x, onclick: function () { var t = $("#say"); t.value = x; grow(); t.focus(); } }); })));
606
+ }
607
+
608
+ // ---- actions -------------------------------------------------------------------
609
+ function setComposer(live) {
610
+ $("#say").placeholder = live ? "Tell the agents something… (@codex for just one)" : "Describe what to build…";
611
+ $("#hint").textContent = live ? "Goes to every agent working now" : "Enter to start · Shift+Enter for a new line";
612
+ $("#send").disabled = !$("#say").value.trim();
613
+ }
614
+ function send() {
615
+ var t = $("#say"), text = t.value.trim(); if (!text) return;
616
+ var live = S.state && S.state.live;
617
+ var p = live ? api("/api/say", { text: text }) : api("/api/goal", { goal: text });
618
+ p.then(function () { t.value = ""; grow(); toast(live ? "Sent to the agents" : "Starting — your agents are connecting"); if (!live) { S.sel = "live"; S.fresh = false; setTimeout(refresh, 1500); } }).catch(function (e) { toast(e.message); });
619
+ }
620
+ function answer(q, line) { api("/api/answer", { id: q.id, line: line }).then(function () { toast("Sent to " + q.from); }).catch(function (e) { toast(e.message); }); }
621
+ function stop() { api("/api/stop", {}).then(function () { toast("Stopping — what's merged stays"); }).catch(function (e) { toast(e.message); }); }
622
+ function grow() { var t = $("#say"); t.style.height = "auto"; t.style.height = Math.min(200, t.scrollHeight) + "px"; $("#send").disabled = !t.value.trim(); }
623
+
624
+ // ---- voice: hold the mic (or Space when you're not typing) and talk ---------------
625
+ var Rec = window.SpeechRecognition || window.webkitSpeechRecognition, rec = null, base = "";
626
+ function listen(on) {
627
+ if (!Rec) return;
628
+ var t = $("#say"), b = $("#mic");
629
+ if (on && !rec) {
630
+ rec = new Rec(); rec.interimResults = true; rec.continuous = true; rec.lang = navigator.language || "en-US";
631
+ base = t.value ? t.value.replace(/\s*$/, " ") : "";
632
+ rec.onresult = function (e) { var s = ""; for (var i = 0; i < e.results.length; i++) s += e.results[i][0].transcript; t.value = base + s; grow(); };
633
+ rec.onerror = function (e) { if (e.error === "not-allowed" || e.error === "service-not-allowed") toast("Allow the microphone to talk to Huddle"); };
634
+ rec.onend = function () { rec = null; b.classList.remove("rec"); $(".listening").classList.remove("on"); t.focus(); };
635
+ try { rec.start(); b.classList.add("rec"); $(".listening").classList.add("on"); } catch (e) { rec = null; }
636
+ } else if (!on && rec) rec.stop();
637
+ }
638
+
639
+ window.addEventListener("load", function () {
640
+ var t = $("#say");
641
+ if (/[?&]native=1/.test(location.search)) document.body.classList.add("native");
642
+ $(".brand").insertBefore(logo(), $(".brand").firstChild);
643
+ $("#new .plus").appendChild(icon("plus", 14, "#fff"));
644
+ $("#mic").appendChild(icon("mic", 17)); $("#send").appendChild(icon("up", 17, "#fff")); $("#refresh").appendChild(icon("refresh", 13));
645
+ t.addEventListener("input", grow);
646
+ t.addEventListener("keydown", function (e) { if (e.key === "Enter" && !e.shiftKey && !e.isComposing) { e.preventDefault(); send(); } });
647
+ $("#send").addEventListener("click", send);
648
+ $("#new").addEventListener("click", function () { $("aside").classList.remove("open"); if (S.state && S.state.live) { S.sel = "live"; render(); } else { S.sel = null; S.fresh = true; S.doc = null; render(); } t.focus(); });
649
+ $("#refresh").addEventListener("click", function () { api("/api/agents", {}).then(function (st) { S.state = st; render(); }); });
650
+ var mic = $("#mic");
651
+ if (!Rec) mic.style.display = "none";
652
+ mic.addEventListener("pointerdown", function (e) { e.preventDefault(); listen(true); });
653
+ window.addEventListener("pointerup", function () { listen(false); });
654
+ document.addEventListener("keydown", function (e) {
655
+ var typing = /^(INPUT|TEXTAREA)$/.test(document.activeElement && document.activeElement.tagName);
656
+ if (e.code === "Space" && !typing && !e.repeat && Rec) { e.preventDefault(); listen(true); }
657
+ });
658
+ document.addEventListener("keyup", function (e) { if (e.code === "Space" && rec) listen(false); });
659
+ document.addEventListener("click", function () { document.querySelectorAll(".menu.open").forEach(function (m) { m.classList.remove("open"); }); });
660
+ var es = new EventSource("/api/events?t=" + encodeURIComponent(TOKEN));
661
+ es.onmessage = refresh;
662
+ setInterval(function () { if (S.state && S.state.live && S.sel === "live") render(); }, 1000);
663
+ grow();
664
+ refresh();
665
+ });
666
+ `;
667
+ export function appPage(token) {
668
+ return `<!doctype html>
669
+ <html lang="en">
670
+ <head>
671
+ <meta charset="utf-8">
672
+ <meta name="viewport" content="width=device-width,initial-scale=1">
673
+ <title>Huddle</title>
674
+ <style>${CSS}</style>
675
+ </head>
676
+ <body>
677
+ <div class="app">
678
+ <aside>
679
+ <div class="brand"><div><div class="nm">Huddle</div><div class="repo" id="repo"></div></div></div>
680
+ <button class="new" id="new"><span class="plus"></span>New build</button>
681
+ <div class="label">Recent</div>
682
+ <div class="runs" id="runs"></div>
683
+ <div class="crew"><div class="row"><span>Your agents</span><button id="refresh" title="Check again" aria-label="Check your agents again"></button></div><div class="people" id="people"></div></div>
684
+ </aside>
685
+ <main>
686
+ <div class="scroll" id="main"></div>
687
+ <div class="dock"><div class="in"><div class="box">
688
+ <textarea id="say" rows="1" placeholder="Describe what to build…"></textarea>
689
+ <div class="bar2"><span class="hint" id="hint"></span><span class="listening">Listening… release to stop</span>
690
+ <button class="ib" id="mic" title="Hold to talk (or hold Space)" aria-label="Hold to talk"></button>
691
+ <button class="ib send" id="send" aria-label="Send"></button></div>
692
+ </div></div></div>
693
+ </main>
694
+ </div>
695
+ <div class="toast"></div>
696
+ <script>window.__HUDDLE_TOKEN__=${JSON.stringify(token)};window.__HUDDLE_COLORS__=${JSON.stringify(AGENT_COLOR)};</script>
697
+ <script>${JS}</script>
698
+ </body>
699
+ </html>`;
700
+ }
701
+ //# sourceMappingURL=page.js.map