huddle-cli 0.1.3 → 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 +28 -0
- package/QUICKSTART.md +29 -1
- package/README.md +12 -0
- package/dist/app/page.js +86 -12
- package/dist/app/qr.js +47 -0
- package/dist/app/server.js +180 -9
- package/dist/args.js +2 -2
- package/dist/cli.js +20 -2
- 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 +23 -3
- package/dist/orchestrator/council.js +1 -1
- package/dist/orchestrator/planner.js +1 -1
- package/dist/orchestrator/scheduler.js +23 -3
- package/dist/room/server.js +29 -0
- package/dist/ui/app.js +61 -2
- package/dist/ui/catalog.js +1 -1
- package/dist/ui/undo.js +131 -0
- package/dist/update.js +84 -0
- package/package.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.5 — 2026-10-16
|
|
4
|
+
|
|
5
|
+
Run it from your phone; undo and GitHub issues in the dashboard.
|
|
6
|
+
|
|
7
|
+
- **`huddle app --phone`: run it from your phone.** The dashboard also listens on your Wi-Fi and prints a QR code; scan it to watch the run, answer an agent's question, talk to the agents, or start and stop a run. Every page and call still needs the session key (longer on the network), and only this computer's own addresses are let in. It says plainly that anyone with the link can control the run, and ctrl-c stops sharing. `--anywhere` opens a Cloudflare quick tunnel instead (https, works off your Wi-Fi) when `cloudflared` is installed, and says how to get it when not. Checked at phone width (390 px).
|
|
8
|
+
- **Undo from the dashboard.** A past run has **Undo this run** when it can be undone (otherwise the reason, in small print). It lists the commits and asks in the page first, then undoes exactly as `huddle undo` does: one revert commit, recorded the same way. Refused while a run is going, or if the run changed since you looked.
|
|
9
|
+
- **Fix a GitHub issue from the dashboard.** With `gh` signed in, the start screen lists the repo's open issues (up to 20, checked at most once a minute); **Fix this** starts a run on it, like `huddle fix #12`. The sidebar shows which connectors are set up.
|
|
10
|
+
- Fixed: undoing an older run (`huddle undo --run 1` after run 2) marked the newest run as undone instead.
|
|
11
|
+
- Fixed: `huddle app --help` opened a dashboard; it prints the options now.
|
|
12
|
+
- Live: a run started from the phone link (real Claude) built `--max N` for a slug CLI, and the dashboard's undo took it back. Suite: 457 tests, 0 failed.
|
|
13
|
+
|
|
14
|
+
## 0.1.4 — 2026-10-16
|
|
15
|
+
|
|
16
|
+
Your team's tools, for every agent.
|
|
17
|
+
|
|
18
|
+
- **Connectors.** Every agent in a run (Claude, Codex, Cursor, any of them) gets the same outside tools through the Plug. The Room runs each connector once per run, so there's one sign-in and no per-vendor config.
|
|
19
|
+
- **GitHub, built in:** `github_issues`, `github_issue` (with comments), `github_prs`, `github_pr` (files, reviews, optional diff), `github_ci`, through the `gh` CLI you're signed in to. Huddle keeps no token.
|
|
20
|
+
- **Any MCP server:** `huddle connectors add atlassian` (Jira + Confluence), `linear`, `notion`, `miro`, `sentry`, any `https://` MCP URL (through `mcp-remote`, which does the sign-in), or `-- <command…>` for a local server. A connector that doesn't start is left out with a note; the run goes on.
|
|
21
|
+
- **Read-only until you allow more.** Tools that post or change things are hidden until `--write` (`huddle connectors github write` for comments and new issues). What agents post is signed "via Huddle".
|
|
22
|
+
- **Only your own agents** reach them; a teammate's agent in your room doesn't. Every call shows live (`codex github: read issue #12`) and is recorded (`connector.call`).
|
|
23
|
+
- **Reviewers see what the author read.** A cross-review has no tools, so the ticket or issue the author read goes into the reviewer's prompt, verbatim. Found live: Codex sent back WEB-7 because "the ticket's contents are missing".
|
|
24
|
+
- **The planner knows what agents can read**, so a goal that names a ticket gets tasks that read it first rather than guessing.
|
|
25
|
+
- Live: "do Jira ticket WEB-7" with a stand-in Jira. Claude, then Codex (reviewed by Cursor), read the ticket and its PM comment through the connector and built all of it, first review approved.
|
|
26
|
+
- **`huddle fix #12`**: a GitHub issue (title, body, comments) is the goal. A goal that mentions `#12` keeps your words and gets the issue attached.
|
|
27
|
+
- **`huddle undo`** takes back what the last run merged (`--run n` for another): one revert commit, listed and confirmed first. If later commits build on the run, it changes nothing and says how to revert by hand.
|
|
28
|
+
- **`huddle update`** (also `huddle --update`) installs the newest Huddle from npm. It says so when you're already on it, and a source checkout is told to `git pull` instead.
|
|
29
|
+
- Tests pass in a terminal that sets `FORCE_COLOR` (found publishing 0.1.3).
|
|
30
|
+
|
|
3
31
|
## 0.1.3 — 2026-10-16
|
|
4
32
|
|
|
5
33
|
You can follow a run, and take part in it, without being an engineer. Live capstone: 29/29 with real Claude, Codex and Cursor (13 merged, 0 failed, 22 min, 5.6M tokens, down from 6.6M).
|
package/QUICKSTART.md
CHANGED
|
@@ -67,6 +67,30 @@ Or skip the prompt: `huddle build a temperature converter with tests` (no quotes
|
|
|
67
67
|
|
|
68
68
|
**The dashboard:** `huddle app` opens Huddle in your browser. You get the run going now (who's on what, how far along, a question waiting for you), every past run as a conversation, the plan and what each agent cost. Type there to start a run or talk to the agents; hold Space (or the mic) to say it instead. It works for runs started in a terminal too. Runs keep going if you close the page.
|
|
69
69
|
|
|
70
|
+
- **From your phone:** `huddle app --phone` prints a QR code. Scan it with your phone's camera (same Wi-Fi as the computer) to watch the run, answer questions and talk to the agents from the couch. Anyone with that link can control the run, so keep it to yourself; ctrl-c stops sharing. Not on the same Wi-Fi? `huddle app --anywhere` uses a free Cloudflare link instead (needs `cloudflared`: `brew install cloudflared`).
|
|
71
|
+
- **Undo a run:** open a past run and press **Undo this run**. It shows the commits it would take back and asks first, the same as `huddle undo`.
|
|
72
|
+
- **Fix an issue:** when `gh` is signed in, the start screen lists the repo's open GitHub issues; **Fix this** starts a run on one.
|
|
73
|
+
|
|
74
|
+
**Your team's tools: GitHub, Jira, Linear…** Every agent in a run can read the repo's GitHub issues, pull requests and CI through the `gh` CLI you're signed in to. Nothing to set up:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
huddle fix #12 # GitHub issue 12 is the goal: its text and comments, word for word
|
|
78
|
+
huddle build a login page for #12 and #15 # your words, with both issues attached
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
(In bash, `#` starts a comment: type `huddle fix issue 12` there.)
|
|
82
|
+
|
|
83
|
+
Add the other tools your team uses; each signs you in through your browser once:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
huddle connectors add atlassian # Jira + Confluence (also: linear, notion, miro, sentry)
|
|
87
|
+
huddle connectors # what's on, and how many tools each one gives
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Agents only get the tools that *read* until you allow more: `huddle connectors add --write atlassian`, `huddle connectors github write`. What they post is signed "via Huddle". Huddle keeps no tokens; `gh` and each service's own sign-in do.
|
|
91
|
+
|
|
92
|
+
**Undo a run:** `huddle undo` takes back everything the last run merged, as one new commit (it shows the commits and asks first). `huddle undo --run 3` for an older one.
|
|
93
|
+
|
|
70
94
|
**A second view** (another terminal, same folder):
|
|
71
95
|
|
|
72
96
|
```bash
|
|
@@ -117,14 +141,18 @@ Not on the same Wi-Fi? Install [Tailscale](https://tailscale.com) on both machin
|
|
|
117
141
|
| `huddle doctor` | check Node, git and your agents |
|
|
118
142
|
| `huddle agents` · `huddle agents skip <id>` · `use <id>` | who's ready, and which agents Huddle uses |
|
|
119
143
|
| `huddle app` | the dashboard in your browser: live run, history, talk to the agents (or speak) |
|
|
144
|
+
| `huddle app --phone` | the same on your phone: scan the QR code (`--anywhere`: off your Wi-Fi too) |
|
|
120
145
|
| `huddle view` | watch a run live |
|
|
121
146
|
| `huddle story` | replay the last run |
|
|
122
147
|
| `huddle brief` · `huddle brief off` | what the last run left for your next solo agent · stop pointing CLAUDE.md / AGENTS.md at it |
|
|
123
148
|
| `huddle runs` · `huddle export [run]` | list past runs · save one as a chat (`--level summary\|chat\|full`, `--format md\|html\|json`) |
|
|
124
149
|
| `huddle board` | print the task board |
|
|
150
|
+
| `huddle fix #12` | a GitHub issue as the goal |
|
|
151
|
+
| `huddle connectors` · `add atlassian` · `remove <name>` | the outside tools every agent gets (GitHub is built in) |
|
|
152
|
+
| `huddle undo` | take back what the last run merged (one revert commit; asks first) |
|
|
125
153
|
| `add <file>` / `ctx` | (at the prompt) put a file on the shared screen / see what's on it |
|
|
126
154
|
| `huddle add <file\|->` / `huddle ctx` | running claude/codex/cursor yourself (`huddle connect`): put a file, folder or stdin (`cat err.log \| huddle add -`) on the shared screen every session reads / see it |
|
|
127
|
-
| `
|
|
155
|
+
| `huddle update` | update to the newest version |
|
|
128
156
|
| `npm uninstall -g huddle-cli` | remove it (your repos keep their work) |
|
|
129
157
|
|
|
130
158
|
## If something goes wrong
|
package/README.md
CHANGED
|
@@ -86,6 +86,18 @@ None of these steps blocks the run. If an agent doesn't answer or doesn't follow
|
|
|
86
86
|
|
|
87
87
|
**Pasting and files: one shared screen, not N copies.** Paste a stack trace, a doc or a block of code at the prompt and it becomes one item on the Room's *shared screen* (`📎 pasted 214 lines (ctx-3 'TypeError…')`) instead of 214 commands; what you type next is the goal. Drag a file or screenshot into the terminal, or `add <file|folder|glob>`. Prompts carry only a one-line index per item; the planner links items to the tasks that need them, and agents open them with `huddle_context_read` (400 lines a call, images and PDFs as a file path; Codex also gets linked screenshots via `--image`), search them, and pin notes on them that everyone sees. `ctx` lists the screen (`ctx show|rm|note|clear`). Likely secrets (API keys, tokens, private keys, `.env` lines) are flagged before anything is shown to agents, and redacted unless you say no. One-shot: `huddle join "fix this" --with error.log --with shot.png`, or `cat log.txt | huddle join "fix this"`. Items live in `.huddle/context/` (git-ignored); caps: 2 MB of text, 25 MB per file, 200 MB in all (`HUDDLE_CTX_MAX_*_MB`).
|
|
88
88
|
|
|
89
|
+
**Connectors: your team's tools for every agent.** One run's agents (Claude, Codex, Cursor, any of them) share the same outside tools through the Plug; the Room runs each connector once per run.
|
|
90
|
+
|
|
91
|
+
- **GitHub, built in:** issues, pull requests (with diffs) and CI runs, through the `gh` CLI you're signed in to. `huddle fix #12` makes issue 12 the goal (title, body and comments, verbatim); a goal that mentions `#12` gets the issue attached.
|
|
92
|
+
- **Any MCP server:** `huddle connectors add atlassian` (Jira + Confluence), `linear`, `notion`, `miro`, `sentry`; or `huddle connectors add <name> <https-url>`, or `huddle connectors add <name> -- <command…>` for a local one. Remote ones run through `mcp-remote`, which signs you in in the browser once and keeps the token. Huddle keeps none.
|
|
93
|
+
- **Read-only until you say so.** A tool that posts or changes something (a comment, a new ticket) is hidden until `--write` (`huddle connectors github write`). The server's own annotations decide what reads; without them, the tool's name does. What agents post is signed "via Huddle".
|
|
94
|
+
- **Only your agents.** A teammate's agent working from your room never reaches your connectors. Every call shows in the run (`codex github: read issue #12`) and is kept in the Room's record of the run.
|
|
95
|
+
- Config: `~/.huddle/connectors.json`. `huddle connectors` lists what's on and how many tools each gives.
|
|
96
|
+
|
|
97
|
+
**`huddle undo`** takes back what a run merged: one commit that reverts the run's commits (it lists them and asks first; `--yes` doesn't; `--run n` for an older run). History stays, nothing is force-pushed. If later work builds on the run, it says so and changes nothing.
|
|
98
|
+
|
|
99
|
+
**`huddle app`, the dashboard,** shows the run going now and every past run in your browser. A past run has **Undo this run** (the same undo, asked in the page first, refused while a run is going); with `gh` signed in, the start screen lists the repo's open issues with **Fix this**. **`huddle app --phone`** also opens it to your Wi-Fi and prints a QR code: scan it to follow and steer a run from your phone. The link carries a long per-session key and every page and call needs it; anyone with the link can control the run, and ctrl-c stops sharing. `--anywhere` uses a Cloudflare quick tunnel (an https link that works off your Wi-Fi) when `cloudflared` is installed.
|
|
100
|
+
|
|
89
101
|
**Running claude, codex and cursor yourself, in one repo at once (attach mode):**
|
|
90
102
|
|
|
91
103
|
```bash
|
package/dist/app/page.js
CHANGED
|
@@ -162,22 +162,38 @@ pre.contract{font:12.5px/1.6 var(--mono);white-space:pre-wrap;margin:0;padding:1
|
|
|
162
162
|
.ib.rec{background:var(--bad);color:#fff;animation:pulse 1.4s infinite}
|
|
163
163
|
.listening{display:none;align-items:center;gap:8px;color:var(--bad);font-size:12.5px;font-weight:600}.listening.on{display:flex}
|
|
164
164
|
|
|
165
|
-
.hero{max-width:640px;margin:0 auto;padding:17vh 32px
|
|
165
|
+
.hero{max-width:640px;margin:0 auto;padding:17vh 32px 170px;text-align:center}
|
|
166
166
|
.hero .mark{width:46px;height:46px;border-radius:14px;margin:0 auto 24px}
|
|
167
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
168
|
.ex{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;margin-top:28px}
|
|
169
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
170
|
.ex button:hover{color:var(--fg);border-color:var(--line2)}
|
|
171
171
|
|
|
172
|
+
.quiet{color:var(--faint);font-size:12.5px}
|
|
173
|
+
.confirm{margin-top:20px;padding:18px 20px;border-radius:14px;border:1px solid var(--line2);background:var(--panel);box-shadow:var(--shadow-lg)}
|
|
174
|
+
.confirm .q{font:500 18px/1.35 var(--serif);margin:0 0 6px}.confirm p{margin:0;color:var(--mute);font-size:13.5px}
|
|
175
|
+
.confirm ul{margin:12px 0 14px;padding:0;list-style:none;max-height:220px;overflow:auto}.confirm li{font:12.5px/1.5 var(--mono);color:var(--mute);padding:2px 0;overflow-wrap:anywhere}
|
|
176
|
+
.confirm .acts{display:flex;flex-wrap:wrap;gap:8px;align-items:center}.confirm .err{color:var(--bad);font-size:13px;margin-top:10px}
|
|
177
|
+
.btn.danger{background:var(--bad);color:#fff}.btn:disabled{opacity:.5;cursor:default}
|
|
178
|
+
.issues{margin:34px auto 0;max-width:560px;text-align:left}
|
|
179
|
+
.issues .k{text-align:center}
|
|
180
|
+
.issue{display:flex;align-items:center;gap:10px;padding:9px 6px 9px 12px;border-top:1px solid var(--line);font-size:14px}.issue:first-of-type{border-top:0}
|
|
181
|
+
.issues .card{padding:2px 0}
|
|
182
|
+
.issue .no{color:var(--faint);font-variant-numeric:tabular-nums;font-size:13px;flex:none}.issue .ti{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
183
|
+
.issue .btn{padding:6px 11px;font-size:12.5px;flex:none;margin-right:6px}
|
|
184
|
+
.tools{color:var(--faint);font-size:11.5px;padding:10px 6px 0;line-height:1.5}
|
|
185
|
+
.hero-menu{position:absolute;top:14px;left:12px}
|
|
186
|
+
.acts-r{display:flex;flex-wrap:wrap;gap:2px}
|
|
172
187
|
.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
188
|
.side-toggle{display:none}
|
|
174
189
|
@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}
|
|
190
|
+
.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}
|
|
191
|
+
.top{flex-wrap:wrap;gap:8px 12px}.ask .h{flex-wrap:wrap}.ask .h .pill{margin-left:0}.top .acts-r{order:3;flex-basis:100%;margin-left:-8px}.top .pop{right:auto;left:0}.hero{padding-top:64px}.issue .ti{white-space:normal;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}}
|
|
176
192
|
`;
|
|
177
193
|
const JS = String.raw `
|
|
178
194
|
"use strict";
|
|
179
195
|
var TOKEN = window.__HUDDLE_TOKEN__, COLORS = window.__HUDDLE_COLORS__;
|
|
180
|
-
var S = { state: null, doc: null, docKey: "", sel: null, tab: "story", open: {}, fresh: false };
|
|
196
|
+
var S = { state: null, doc: null, docKey: "", sel: null, tab: "story", open: {}, fresh: false, gh: null, ghAt: 0, undo: null };
|
|
181
197
|
var $ = function (sel) { return document.querySelector(sel); };
|
|
182
198
|
|
|
183
199
|
// ---- a tiny DOM kit ----------------------------------------------------------------
|
|
@@ -214,7 +230,8 @@ var ICONS = {
|
|
|
214
230
|
msg: "M21 12a8 8 0 0 1-11.6 7.1L4 21l1.9-5.4A8 8 0 1 1 21 12z",
|
|
215
231
|
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
232
|
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"
|
|
233
|
+
flag: "M5 21V4M5 4h11l-2 4 2 4H5",
|
|
234
|
+
undo: "M9 14L4 9l5-5M4 9h11a5 5 0 0 1 0 10h-3"
|
|
218
235
|
};
|
|
219
236
|
function icon(name, size, color) {
|
|
220
237
|
var NS = "http://www.w3.org/2000/svg", s = document.createElementNS(NS, "svg");
|
|
@@ -288,7 +305,7 @@ function refresh() {
|
|
|
288
305
|
var n = viewRunIndex();
|
|
289
306
|
if (n == null) { S.doc = null; S.docKey = ""; return null; }
|
|
290
307
|
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) : "");
|
|
308
|
+
var key = n + ":" + row.tokens + ":" + row.merged + ":" + st.undone + ":" + (live ? "L" : "") + (S.sel === "live" && live ? Math.floor(live.updatedAt / 3000) : "");
|
|
292
309
|
if (key === S.docKey) return null;
|
|
293
310
|
return api("/api/run?n=" + n).then(function (d) { S.doc = d; S.docKey = key; });
|
|
294
311
|
}).catch(function (e) { toast(e.message); }).then(function () {
|
|
@@ -316,6 +333,7 @@ function render() {
|
|
|
316
333
|
if (!live && !S.doc) { main.appendChild(hero()); setComposer(false); return; }
|
|
317
334
|
var c = h("div", { class: "col" });
|
|
318
335
|
c.appendChild(header(live));
|
|
336
|
+
if (!live && S.undo && S.undo.n === viewRunIndex() && S.doc.undo && S.doc.undo.ok) c.appendChild(undoCard(S.undo.n, S.doc.undo));
|
|
319
337
|
var plain = live ? live.plain : S.doc && S.doc.plain;
|
|
320
338
|
var report = !live && S.doc && S.doc.report;
|
|
321
339
|
if (plain && plain.what && !report) c.appendChild(h("div", { class: "prose" }, h("span", { class: "k", text: "What you'll get" }), plain.what,
|
|
@@ -344,6 +362,15 @@ function renderSide(st) {
|
|
|
344
362
|
var ready = a.state === "ready";
|
|
345
363
|
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
364
|
});
|
|
365
|
+
// The outside tools every agent gets (huddle connectors), in small print.
|
|
366
|
+
var tools = $("#tools"), cn = st.connectors || { github: "off", others: [] }, gh = S.gh, names = [];
|
|
367
|
+
if (cn.github !== "off" && gh && gh.ready) names.push(h("span", { title: gh.note, text: "GitHub" + (cn.github === "write" ? " (can post)" : "") }));
|
|
368
|
+
cn.others.forEach(function (c) { names.push(h("span", { text: c.name + (c.write ? " (can post)" : "") })); });
|
|
369
|
+
tools.textContent = "";
|
|
370
|
+
if (!names.length && !gh) return;
|
|
371
|
+
add(tools, "Connectors: ");
|
|
372
|
+
if (!names.length) add(tools, h("span", { title: gh && gh.note ? "GitHub: " + gh.note : "", text: "none · huddle connectors" }));
|
|
373
|
+
names.forEach(function (x, i) { if (i) add(tools, " · "); add(tools, x); });
|
|
347
374
|
}
|
|
348
375
|
function runBtn(key, goal, when, tone) {
|
|
349
376
|
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(); } },
|
|
@@ -361,9 +388,15 @@ function header(live) {
|
|
|
361
388
|
h("span", { text: dur(d.run.ms) }), h("span", { text: tok(r.tokens) + " tokens" }));
|
|
362
389
|
if (r.qa) sub.push(h("span", { class: "pill " + r.qa.tone, text: r.qa.text }));
|
|
363
390
|
}
|
|
364
|
-
var right = h("div", {
|
|
391
|
+
var right = h("div", { class: "acts-r" });
|
|
365
392
|
if (live) right.appendChild(h("button", { class: "ghost stop", onclick: stop }, icon("stop", 14), "Stop"));
|
|
366
393
|
var n = viewRunIndex();
|
|
394
|
+
// Undo: offered only when ui/undo.ts says it can be done; otherwise why not, in small print.
|
|
395
|
+
var u = !live && d && d.undo;
|
|
396
|
+
if (u && u.ok && S.state.live) sub.push(h("span", { class: "quiet", text: "Undo once the run going now is finished" }));
|
|
397
|
+
else if (u && u.ok) right.appendChild(h("button", { class: "ghost", onclick: function () { S.undo = { n: n, busy: false, err: "" }; render(); } }, icon("undo", 14), "Undo this run"));
|
|
398
|
+
else if (u && /already undone/.test(u.why)) sub.push(h("span", { class: "pill", text: "Undone" }));
|
|
399
|
+
else if (u && u.why) sub.push(h("span", { class: "quiet", text: "Can't undo: " + u.why.replace(/^run \d+('s)?\s*/, function (m, s) { return s ? "its " : ""; }) }));
|
|
367
400
|
if (n != null) right.appendChild(exportMenu(n));
|
|
368
401
|
return h("div", { class: "top" }, h("button", { class: "ib side-toggle", onclick: function () { $("aside").classList.add("open"); }, "aria-label": "Runs" }, icon("menu", 18)),
|
|
369
402
|
h("div", { class: "grow" }, h("h1", { text: title }), h("div", { class: "sub" }, sub)), right);
|
|
@@ -417,6 +450,19 @@ function askCard(q) {
|
|
|
417
450
|
h("div", { class: "own" }, own, h("button", { class: "btn", text: "Send", onclick: function () { if (own.value.trim()) answer(q, own.value.trim()); } })));
|
|
418
451
|
}
|
|
419
452
|
|
|
453
|
+
/** Asking before an undo, in the page (a browser's confirm box can't be styled, and the Mac app has none). */
|
|
454
|
+
function undoCard(n, u) {
|
|
455
|
+
var c = u.commits || [], shown = c.slice(0, 12);
|
|
456
|
+
return h("div", { class: "confirm" },
|
|
457
|
+
h("div", { class: "q", text: "Undo run " + n + "?" }),
|
|
458
|
+
h("p", { text: "This adds one new commit that takes back " + (c.length === 1 ? "this commit" : "these " + c.length + " commits") + ". Nothing is deleted: you can undo the undo later." }),
|
|
459
|
+
h("ul", null, shown.map(function (x) { return h("li", { text: x, title: x }); }), c.length > shown.length ? h("li", { text: "… and " + (c.length - shown.length) + " more" }) : null),
|
|
460
|
+
h("div", { class: "acts" },
|
|
461
|
+
h("button", { class: "btn danger", disabled: S.undo.busy, text: S.undo.busy ? "Undoing…" : "Undo it", onclick: function () { undoRun(n, u.end); } }),
|
|
462
|
+
h("button", { class: "ghost", text: "Keep it", onclick: function () { S.undo = null; render(); } })),
|
|
463
|
+
S.undo.err ? h("div", { class: "err", text: S.undo.err }) : null);
|
|
464
|
+
}
|
|
465
|
+
|
|
420
466
|
function tabs() {
|
|
421
467
|
function t(k, label) { return h("button", { class: "tab" + (S.tab === k ? " on" : ""), text: label, onclick: function () { S.tab = k; render(); } }); }
|
|
422
468
|
return h("div", { class: "tabs" }, t("story", "Story"), t("plan", "Plan"), t("cost", "Cost"), t("log", "Full log"));
|
|
@@ -599,23 +645,50 @@ function logTab(c, live) {
|
|
|
599
645
|
|
|
600
646
|
function hero() {
|
|
601
647
|
var ex = ["A todo app with a REST API and tests", "A CLI that converts temperatures", "Fix the failing tests in this repo"];
|
|
602
|
-
|
|
648
|
+
loadGithub();
|
|
649
|
+
var gh = S.gh, issues = gh && gh.ready ? gh.issues : [];
|
|
650
|
+
return h("div", { class: "hero" },
|
|
651
|
+
h("button", { class: "ib side-toggle hero-menu", onclick: function () { $("aside").classList.add("open"); }, "aria-label": "Runs" }, icon("menu", 18)),
|
|
652
|
+
logo(22),
|
|
603
653
|
h("h2", { text: "What should the team build?" }),
|
|
604
654
|
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(); } }); }))
|
|
655
|
+
h("div", { class: "ex" }, ex.map(function (x) { return h("button", { text: x, onclick: function () { var t = $("#say"); t.value = x; grow(); t.focus(); } }); })),
|
|
656
|
+
issues.length ? h("div", { class: "issues" }, h("span", { class: "k", text: "Or fix an open issue" + (gh.note ? " · " + gh.note : "") }),
|
|
657
|
+
h("div", { class: "card" }, issues.map(function (i) {
|
|
658
|
+
return h("div", { class: "issue" }, h("span", { class: "no", text: "#" + i.number }), h("span", { class: "ti", text: i.title, title: i.title }),
|
|
659
|
+
h("button", { class: "btn", text: "Fix this", onclick: function () { start("#" + i.number); } }));
|
|
660
|
+
}))) : null);
|
|
606
661
|
}
|
|
607
662
|
|
|
608
663
|
// ---- actions -------------------------------------------------------------------
|
|
609
664
|
function setComposer(live) {
|
|
610
|
-
|
|
665
|
+
// A phone has room for the short version only.
|
|
666
|
+
$("#say").placeholder = live ? (window.innerWidth < 600 ? "Tell the agents something…" : "Tell the agents something… (@codex for just one)") : "Describe what to build…";
|
|
611
667
|
$("#hint").textContent = live ? "Goes to every agent working now" : "Enter to start · Shift+Enter for a new line";
|
|
612
668
|
$("#send").disabled = !$("#say").value.trim();
|
|
613
669
|
}
|
|
614
670
|
function send() {
|
|
615
671
|
var t = $("#say"), text = t.value.trim(); if (!text) return;
|
|
616
672
|
var live = S.state && S.state.live;
|
|
617
|
-
|
|
618
|
-
|
|
673
|
+
if (!live) { start(text, function () { t.value = ""; grow(); }); return; }
|
|
674
|
+
api("/api/say", { text: text }).then(function () { t.value = ""; grow(); toast("Sent to the agents"); }).catch(function (e) { toast(e.message); });
|
|
675
|
+
}
|
|
676
|
+
function start(goal, then) {
|
|
677
|
+
api("/api/goal", { goal: goal }).then(function () { if (then) then(); toast("Starting — your agents are connecting"); S.sel = "live"; S.fresh = false; setTimeout(refresh, 1500); }).catch(function (e) { toast(e.message); });
|
|
678
|
+
}
|
|
679
|
+
function undoRun(n, end) {
|
|
680
|
+
S.undo.busy = true; S.undo.err = ""; render();
|
|
681
|
+
api("/api/undo", { n: n, end: end }).then(function (r) {
|
|
682
|
+
S.undo = null; S.docKey = "";
|
|
683
|
+
toast("Undone in " + r.commit + (r.pushedTo ? " · git push sends it to " + r.pushedTo : ""));
|
|
684
|
+
refresh();
|
|
685
|
+
}).catch(function (e) { if (S.undo) { S.undo.busy = false; S.undo.err = e.message; } render(); });
|
|
686
|
+
}
|
|
687
|
+
/** The repo's open GitHub issues and whether GitHub is on: asked at most once a minute (the server keeps gh's answer as long). */
|
|
688
|
+
function loadGithub() {
|
|
689
|
+
if (S.ghAt && Date.now() - S.ghAt < 60e3) return;
|
|
690
|
+
S.ghAt = Date.now();
|
|
691
|
+
api("/api/github").then(function (g) { S.gh = g; render(); }).catch(function () {});
|
|
619
692
|
}
|
|
620
693
|
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
694
|
function stop() { api("/api/stop", {}).then(function () { toast("Stopping — what's merged stays"); }).catch(function (e) { toast(e.message); }); }
|
|
@@ -662,6 +735,7 @@ window.addEventListener("load", function () {
|
|
|
662
735
|
setInterval(function () { if (S.state && S.state.live && S.sel === "live") render(); }, 1000);
|
|
663
736
|
grow();
|
|
664
737
|
refresh();
|
|
738
|
+
loadGithub();
|
|
665
739
|
});
|
|
666
740
|
`;
|
|
667
741
|
export function appPage(token) {
|
|
@@ -680,7 +754,7 @@ export function appPage(token) {
|
|
|
680
754
|
<button class="new" id="new"><span class="plus"></span>New build</button>
|
|
681
755
|
<div class="label">Recent</div>
|
|
682
756
|
<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>
|
|
757
|
+
<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 class="tools" id="tools"></div></div>
|
|
684
758
|
</aside>
|
|
685
759
|
<main>
|
|
686
760
|
<div class="scroll" id="main"></div>
|
package/dist/app/qr.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// A QR code in the terminal, for `huddle app --phone`: point the phone's
|
|
2
|
+
// camera at it instead of typing a 70-character link.
|
|
3
|
+
//
|
|
4
|
+
// The encoder is the one qrcode-terminal ships (Kazuhiko Arase's, MIT); the
|
|
5
|
+
// drawing is ours: two rows of the code per line of text (▀ with a top and a
|
|
6
|
+
// bottom color), so a phone link fits in about 20 lines. In color it paints
|
|
7
|
+
// black on bright white itself, so it scans on a light or a dark terminal.
|
|
8
|
+
// Without color it draws the light parts with blocks, which reads right on a
|
|
9
|
+
// dark terminal (phone cameras also read the inverted code a light one shows).
|
|
10
|
+
import { createRequire } from "node:module";
|
|
11
|
+
const require = createRequire(import.meta.url);
|
|
12
|
+
const QRCode = require("qrcode-terminal/vendor/QRCode/index.js");
|
|
13
|
+
const LEVEL = require("qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel.js");
|
|
14
|
+
/** Light space around the code: scanners need some to find its corners. */
|
|
15
|
+
export const QUIET = 2;
|
|
16
|
+
/** The code's modules, true = dark, with the quiet zone around them. */
|
|
17
|
+
export function qrMatrix(text, level = "L") {
|
|
18
|
+
const q = new QRCode(-1, LEVEL[level]); // -1: the smallest size that fits
|
|
19
|
+
q.addData(text);
|
|
20
|
+
q.make();
|
|
21
|
+
const n = q.getModuleCount();
|
|
22
|
+
const size = n + QUIET * 2;
|
|
23
|
+
return Array.from({ length: size }, (_, r) => Array.from({ length: size }, (_, c) => {
|
|
24
|
+
const y = r - QUIET;
|
|
25
|
+
const x = c - QUIET;
|
|
26
|
+
return y >= 0 && x >= 0 && y < n && x < n && q.isDark(y, x);
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
/** The code as lines of text, two rows per line. */
|
|
30
|
+
export function qrLines(text, opts) {
|
|
31
|
+
const m = qrMatrix(text);
|
|
32
|
+
const lines = [];
|
|
33
|
+
for (let r = 0; r < m.length; r += 2) {
|
|
34
|
+
let line = "";
|
|
35
|
+
for (let c = 0; c < m.length; c++) {
|
|
36
|
+
const top = m[r][c];
|
|
37
|
+
const bottom = r + 1 < m.length ? m[r + 1][c] : false;
|
|
38
|
+
if (opts.color)
|
|
39
|
+
line += `\x1b[${top ? 30 : 97};${bottom ? 40 : 107}m▀`;
|
|
40
|
+
else
|
|
41
|
+
line += !top && !bottom ? "█" : !top ? "▀" : !bottom ? "▄" : " ";
|
|
42
|
+
}
|
|
43
|
+
lines.push(opts.color ? line + "\x1b[0m" : line);
|
|
44
|
+
}
|
|
45
|
+
return lines;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=qr.js.map
|