symbiot 0.63.7 → 0.63.8
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 +5 -0
- package/agents.mjs +9 -1
- package/handover.mjs +5 -1
- package/home.mjs +2 -41
- package/package.json +2 -1
- package/phone.mjs +1 -1
- package/plain.mjs +48 -0
- package/ui.mjs +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
What each Symbiot release brought, newest first. When Approve bumps the version, it writes that release here from the tasks it approved, worded as release notes by your connected AI (or in the tasks' own words without one). The app shows the releases newer than yours under "What's new" when it offers an update, and once more after you update.
|
|
4
4
|
|
|
5
|
+
## 0.63.8
|
|
6
|
+
|
|
7
|
+
- **No more file names on permission questions, anywhere.** When an agent asked for a permission, Home said it in plain words with an Allow button, but the same question on the Workdesk, on a project's card and on your phone still read "allow the list in .symbiot/allowlist.proposed.json". Every screen now says what the agent wants to do ("connect to the computer at 13.51.104.186") with Allow.
|
|
8
|
+
- **Allow sticks.** Saying Allow along with a few words of your own that sounded like git ("check the deployed commit, git status and served files") was taken for a git chore and handed back to the agent, which can't give itself permission, so it asked you again. A permission is now always turned on as one.
|
|
9
|
+
|
|
5
10
|
## 0.63.7 — 2026-10-12
|
|
6
11
|
|
|
7
12
|
- **`symbiot capture Symbiot` works in one go.** It used to refuse, because your running Symbiot shows your real work, and needed a copy started by hand. It now starts a throwaway Symbiot with a made-up company in it (Acme: its repos, its agents' questions, what arrived, its week, a Marketing draft), captures Home, the Workdesk, the Dashboard, the Week, the Map and Marketing, then stops it and deletes it. Symbiot's own tray can be brought up to date after every release.
|
package/agents.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import { randomBytes, createHash } from "node:crypto";
|
|
|
12
12
|
import { CONFIG_DIR, loadConfig, saveConfig, loadTasks, TASK_MAX, clipWords, sameTask, taskWords, sh, hasCmd } from "./core.mjs";
|
|
13
13
|
import { parseRun, lastRunText, readRunLog, briefPlan, stepOf, runLine } from "./work.mjs";
|
|
14
14
|
import { noteDuration, kindOf } from "./estimate.mjs";
|
|
15
|
+
import { plainAsk } from "./plain.mjs";
|
|
15
16
|
import { parseFacts, leftToYou, gitWork, agentsGitWork, agentsPosting, linkedAsks, linkedChore, PUSH_OK } from "./handover.mjs";
|
|
16
17
|
import { scanRoots, repoPathMap } from "./scan.mjs";
|
|
17
18
|
import { knowledgeFolders } from "./knowledge.mjs";
|
|
@@ -1079,6 +1080,13 @@ function skipIdea(path, text) {
|
|
|
1079
1080
|
}
|
|
1080
1081
|
return { ok: true };
|
|
1081
1082
|
}
|
|
1083
|
+
// A question as every screen shows it (plain.mjs): `shown`, its words with no file in them,
|
|
1084
|
+
// and `labels`, its choices the same way ("Allow" for an allow list it proposed). q and
|
|
1085
|
+
// options stay as the agent wrote them: an answer is saved under the agent's own words.
|
|
1086
|
+
function plainly(path, x) {
|
|
1087
|
+
let p = {}; try { p = plainAsk(path, x, { max: x.options.length }); } catch {}
|
|
1088
|
+
return { ...x, ...(p.raw ? { shown: p.q } : {}), ...(p.labels ? { labels: p.labels } : {}) };
|
|
1089
|
+
}
|
|
1082
1090
|
function agentQuestions(path, repo) {
|
|
1083
1091
|
const p = parseQuestions(readSymbiot(path, "QUESTIONS.md"));
|
|
1084
1092
|
const done = new Set([...readSymbiot(path, "ANSWERS.md").matchAll(/^###\s+(.+)$/gm)].map((m) => qKey(m[1])));
|
|
@@ -1087,7 +1095,7 @@ function agentQuestions(path, repo) {
|
|
|
1087
1095
|
const ideas = p.suggestions.map(suggestionTarget).filter(({ text }) => !skipped.some((s) => sameTask(s, text)))
|
|
1088
1096
|
.map(({ repo: to, text }) => ({ text, repo: to || repo, other: !!to && to !== repo, added: tasks.some((t) => t.repo === (to || repo) && sameTask(t.text, text)) })).filter((s) => !s.added);
|
|
1089
1097
|
return {
|
|
1090
|
-
questions: open.slice(0, QUESTIONS_MAX).map((x) => ({ ...x, options: x.options.slice(0, OPTIONS_SHOWN) })), answered: p.questions.length - open.length,
|
|
1098
|
+
questions: open.slice(0, QUESTIONS_MAX).map((x) => plainly(path, { ...x, options: x.options.slice(0, OPTIONS_SHOWN) })), answered: p.questions.length - open.length,
|
|
1091
1099
|
suggestions: ideas, ideasShown: IDEAS_SHOWN,
|
|
1092
1100
|
};
|
|
1093
1101
|
}
|
package/handover.mjs
CHANGED
|
@@ -66,7 +66,11 @@ function parseAwaiting(text) {
|
|
|
66
66
|
// is still theirs.
|
|
67
67
|
const GIT_WORK = /\bgit\s+(?:add|commit|push|pull|fetch|merge|rebase|stash|restore|checkout|switch)\b|\b(?:stage|commit|push|pull)(?:es|ed|ing)?\b(?:[^.;:]{0,60})\b(?:changes?|diff|fix|files?|branch|main|master|origin|upstream|remote|commits?|them|it)\b/i;
|
|
68
68
|
const IDENTITY = /\bpassword\b|\b2fa\b|\btwo-factor\b|\b(?:api )?token\b|\bsign(?:ing)? in\b|\blog ?in\b|\bssh key\b|\bpassphrase\b|\bsudo\b/i;
|
|
69
|
-
|
|
69
|
+
// A permission is never git work, whatever its words: "allow the list … to check the
|
|
70
|
+
// deployed commit, git status and served files" was taken for a commit, and handed back to
|
|
71
|
+
// an agent that can't give itself permission, which asked again (WinLove, 2026-10-12).
|
|
72
|
+
const PERMISSION = /allowlist\.proposed|only you: a permission/i;
|
|
73
|
+
const gitWork = (text) => { const t = String(text || ""); return GIT_WORK.test(t) && !IDENTITY.test(t) && !PERMISSION.test(t); };
|
|
70
74
|
const PUSH_OK = "asking for your OK on the Workdesk, with the files, before it pushes";
|
|
71
75
|
// A "👤 You: …" option that's local git work, as the agent's: "🤖 Agent: … (asking for
|
|
72
76
|
// your OK … before it pushes)". Dropped instead when another option already has the
|
package/home.mjs
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// how you use them; this is the live data around them. Home's talk goes to the
|
|
11
11
|
// same Symbiot as every chat (mind.mjs converse), told what home shows.
|
|
12
12
|
import { claudeSetup } from "./claudesetup.mjs";
|
|
13
|
+
import { plainAsk } from "./plain.mjs";
|
|
13
14
|
import { join } from "node:path";
|
|
14
15
|
import { homedir } from "node:os";
|
|
15
16
|
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
@@ -409,48 +410,8 @@ function noteAnswer(path, heading, text) {
|
|
|
409
410
|
const f = join(path, ".symbiot", "ANSWERS.md"); let had = ""; try { had = readFileSync(f, "utf8"); } catch {}
|
|
410
411
|
try { writeFileSync(f, `${(had || "# Answers\n").replace(/\s*$/, "")}\n\n### ${String(heading).replace(/\s+/g, " ").trim()}\n${text}\n_answered ${new Date().toISOString().slice(0, 10)}_\n`); } catch {}
|
|
411
412
|
}
|
|
412
|
-
// An ask in words for someone who doesn't code: a permission request lists what the agent
|
|
413
|
-
// wants to run, with Allow / Don't allow; any file path left in the question or choices
|
|
414
|
-
// becomes "a file". The answer sent back is still the choice's own text (labels only change what shows).
|
|
415
|
-
const PATHISH = /`?(?:~|\.{0,2})?\/?(?:[\w.-]+\/)+[\w.-]+\.(?:json|md|mjs|js|env|txt|sh)`?|`?\b[\w-]+\.(?:json|mjs)\b`?/g;
|
|
416
|
-
// What a rule lets an agent do, said the way you'd say it to someone who doesn't code.
|
|
417
|
-
const base = (p) => String(p || "").replace(/[/*]+$/, "").split("/").filter(Boolean).pop() || "your files";
|
|
418
|
-
function PLAIN_RULE(r) {
|
|
419
|
-
const m = String(r).match(/^(\w+)\((.*)\)$/); if (!m) return "work in " + base(r);
|
|
420
|
-
const [, tool, arg] = m, a = arg.replace(/:?\*+$/, "").trim(), first = a.split(/\s+/)[0].replace(/:\*?$/, "").split("/").pop();
|
|
421
|
-
if (tool === "Read") return `read files in ${base(a)}`;
|
|
422
|
-
if (/^(Edit|Write|MultiEdit)$/.test(tool)) return `change files in ${base(a)}`;
|
|
423
|
-
if (tool !== "Bash") return `use ${tool}`;
|
|
424
|
-
const host = (a.match(/@([\w.-]+)/) || [])[1];
|
|
425
|
-
if (/^(ssh|scp|rsync)$/.test(first)) return host ? `connect to the computer at ${host}` : "connect to another computer";
|
|
426
|
-
if (first === "adb") return "use your phone over the USB cable";
|
|
427
|
-
if (/^(flutter|gradle|gradlew|godot|xcodebuild)$/.test(first) || /\bbuild\b/.test(a)) return "build the app";
|
|
428
|
-
if (first === "ffmpeg" || first === "ffprobe") return "make and trim video clips";
|
|
429
|
-
if (first === "git" && /\bpush\b/.test(a)) return "send code to GitHub";
|
|
430
|
-
if (/^(npm|yarn|pnpm)$/.test(first) && /\bpublish\b/.test(a)) return "publish a new version";
|
|
431
|
-
if (/^(bash|sh|zsh)$/.test(first)) return "run a script it wrote";
|
|
432
|
-
if (/^(python\d*|node)$/.test(first)) return "run code in this project";
|
|
433
|
-
if (/^(rm|shred)$/.test(first)) return "delete files";
|
|
434
|
-
if (first === "sudo") return "make changes as administrator";
|
|
435
|
-
return `run ${first}`;
|
|
436
|
-
}
|
|
437
413
|
// a label without its "(recommended)": a second button isn't the recommended one
|
|
438
414
|
const blobLabel = (l) => String(l || "").replace(/\s*\(recommended\)\s*$/i, "");
|
|
439
|
-
function plainAsk(path, q) {
|
|
440
|
-
const text = String(q.q || ""), opts = (q.options || []).slice(0, 2);
|
|
441
|
-
let ask = text, labels = opts.map((o) => o.replace(PATHISH, "a file"));
|
|
442
|
-
if (opts.some((o) => /allowlist\.proposed/i.test(o))) {
|
|
443
|
-
let rules = []; try { const p = JSON.parse(readFileSync(join(path, ".symbiot", "allowlist.proposed.json"), "utf8")).permissions || {}; rules = [...new Set([...(p.allow || []).map(PLAIN_RULE), ...(p.additionalDirectories || []).map((d) => `work in ${base(d)}`)])]; } catch {}
|
|
444
|
-
ask = `${text.replace(PATHISH, "a file")}${rules.length ? ` It wants to ${rules.slice(0, 6).join(", ")}${rules.length > 6 ? `, and ${rules.length - 6} more things` : ""}.` : ""} If you say no, it skips that part and tells you what it left out.`;
|
|
445
|
-
labels = opts.map((o) => (/allowlist\.proposed/i.test(o) ? "Allow" + (/recommended/i.test(o) ? " (recommended)" : "") : o.replace(PATHISH, "a file")));
|
|
446
|
-
}
|
|
447
|
-
// raw: the question as the agent wrote it, the key its answer is saved under (agents.mjs
|
|
448
|
-
// answerQuestions). Sending the plain words back matched nothing, so a permission card's
|
|
449
|
-
// Allow, and Send under it, did nothing at all (whatsapp_module, 2026-10-12).
|
|
450
|
-
const shown = ask.replace(PATHISH, "a file");
|
|
451
|
-
return { q: shown, ...(shown !== text ? { raw: text } : {}), ...(labels.some((l, i) => l !== opts[i]) ? { labels } : {}) };
|
|
452
|
-
}
|
|
453
|
-
|
|
454
415
|
function homeAnswer(id, { pick, text = "" } = {}, deps = {}) {
|
|
455
416
|
id = String(id || ""); text = String(text || "").trim().slice(0, 2000);
|
|
456
417
|
const go = text ? !SAYS_NO.test(text) : Number(pick) === 0, ref = id.replace(/^\w+:/, "");
|
|
@@ -688,7 +649,7 @@ function workScene({ deps = {}, full = false } = {}) {
|
|
|
688
649
|
const line = plain(String(t).split(/\n\s*\n/)[0], 220);
|
|
689
650
|
if (line) { p.summary = line; p.state = a.status === "running" ? "at work" : qs.length ? "asks you" : "done"; p.path = a.path; }
|
|
690
651
|
}
|
|
691
|
-
for (const q of qs) { p.qs = p.qs || []; if (p.qs.length < 3) p.qs.push({ q: q.q, options: (q.options || []).slice(0, 3), path: a.path, ...signInCard([q.q, ...(q.options || []).slice(0, 3)]) }); }
|
|
652
|
+
for (const q of qs) { p.qs = p.qs || []; if (p.qs.length < 3) p.qs.push({ q: q.shown || q.q, ...(q.shown ? { raw: q.q } : {}), options: (q.options || []).slice(0, 3), ...(q.labels ? { labels: q.labels } : {}), path: a.path, ...signInCard([q.q, ...(q.options || []).slice(0, 3)]) }); }
|
|
692
653
|
for (const s of ideas) { p.ideas = p.ideas || []; if (p.ideas.length < 4) p.ideas.push({ text: plain(s.text, 200), full: String(s.text), repo: s.repo || l, path: a.path }); }
|
|
693
654
|
}
|
|
694
655
|
// what each lane handed over and hasn't heard back on, with where it stands and how far the run on it has got
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "symbiot",
|
|
3
|
-
"version": "0.63.
|
|
3
|
+
"version": "0.63.8",
|
|
4
4
|
"description": "Your week, written from your real work. Reads your local git and writes it up with the AI of your choice \u2014 Claude, OpenAI, Gemini, or a local model.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"away.mjs",
|
|
47
47
|
"marketing.mjs",
|
|
48
48
|
"tray.mjs",
|
|
49
|
+
"plain.mjs",
|
|
49
50
|
"capture.mjs",
|
|
50
51
|
"demo.mjs",
|
|
51
52
|
"postinstall.mjs",
|
package/phone.mjs
CHANGED
|
@@ -283,7 +283,7 @@ async function snapshot({ now = Date.now(), fresh = false } = {}) {
|
|
|
283
283
|
const asks = [];
|
|
284
284
|
for (const y of (await tryOr(() => agents.agentsList(), [])).filter((a, i, all) => all.findIndex((b) => b.path === a.path) === i)) {
|
|
285
285
|
const qs = await tryOr(() => agents.agentQuestions(y.path, "").questions, []);
|
|
286
|
-
if (qs.length) asks.push({ id: askId(y.path), path: y.path, name: plainText(String(y.name || "an agent").replace(/^Agent:\s*/, ""), 60), questions: qs.slice(0, 5).map((q) => ({ q: plainText(q.q, 300), options: (q.options || []).slice(0, 2).map((o) => plainText(o, 200)) })) });
|
|
286
|
+
if (qs.length) asks.push({ id: askId(y.path), path: y.path, name: plainText(String(y.name || "an agent").replace(/^Agent:\s*/, ""), 60), questions: qs.slice(0, 5).map((q) => ({ q: plainText(q.q, 300), ...(q.shown ? { shown: plainText(q.shown, 400) } : {}), options: (q.options || []).slice(0, 2).map((o) => plainText(o, 200)), ...(q.labels ? { labels: q.labels.slice(0, 2).map((o) => plainText(o, 200)) } : {}) })) });
|
|
287
287
|
}
|
|
288
288
|
const approves = (await tryOr(() => tasksMod.pendingReview(), [])).filter((r) => r.path && !r.running && ((r.tasks || []).length || (r.files || []).length))
|
|
289
289
|
.map((r) => ({ repo: r.repo, tasks: (r.tasks || []).map((t) => ({ id: t.id, text: plainText(t.text, 200) })), files: (r.files || []).length, stat: plainText(r.stat, 120), ...(r.untasked ? { untasked: true } : {}), sum: approveSum(r) }));
|
package/plain.mjs
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// A question an agent left, in words for someone who doesn't code (plainAsk): Home,
|
|
2
|
+
// the Workdesk's agent blocks, a project's card and the phone all show it this way, so
|
|
3
|
+
// "allow the list in .symbiot/allowlist.proposed.json" never reaches a screen. It was
|
|
4
|
+
// only Home's list: the same ask on the Workdesk still read as a file (2026-10-12).
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
|
|
8
|
+
// An ask in words for someone who doesn't code: a permission request lists what the agent
|
|
9
|
+
// wants to run, with Allow / Don't allow; any file path left in the question or choices
|
|
10
|
+
// becomes "a file". The answer sent back is still the choice's own text (labels only change what shows).
|
|
11
|
+
const PATHISH = /`?(?:~|\.{0,2})?\/?(?:[\w.-]+\/)+[\w.-]+\.(?:json|md|mjs|js|env|txt|sh)`?|`?\b[\w-]+\.(?:json|mjs)\b`?/g;
|
|
12
|
+
// What a rule lets an agent do, said the way you'd say it to someone who doesn't code.
|
|
13
|
+
const base = (p) => String(p || "").replace(/[/*]+$/, "").split("/").filter(Boolean).pop() || "your files";
|
|
14
|
+
function PLAIN_RULE(r) {
|
|
15
|
+
const m = String(r).match(/^(\w+)\((.*)\)$/); if (!m) return "work in " + base(r);
|
|
16
|
+
const [, tool, arg] = m, a = arg.replace(/:?\*+$/, "").trim(), first = a.split(/\s+/)[0].replace(/:\*?$/, "").split("/").pop();
|
|
17
|
+
if (tool === "Read") return `read files in ${base(a)}`;
|
|
18
|
+
if (/^(Edit|Write|MultiEdit)$/.test(tool)) return `change files in ${base(a)}`;
|
|
19
|
+
if (tool !== "Bash") return `use ${tool}`;
|
|
20
|
+
const host = (a.match(/@([\w.-]+)/) || [])[1];
|
|
21
|
+
if (/^(ssh|scp|rsync)$/.test(first)) return host ? `connect to the computer at ${host}` : "connect to another computer";
|
|
22
|
+
if (first === "adb") return "use your phone over the USB cable";
|
|
23
|
+
if (/^(flutter|gradle|gradlew|godot|xcodebuild)$/.test(first) || /\bbuild\b/.test(a)) return "build the app";
|
|
24
|
+
if (first === "ffmpeg" || first === "ffprobe") return "make and trim video clips";
|
|
25
|
+
if (first === "git" && /\bpush\b/.test(a)) return "send code to GitHub";
|
|
26
|
+
if (/^(npm|yarn|pnpm)$/.test(first) && /\bpublish\b/.test(a)) return "publish a new version";
|
|
27
|
+
if (/^(bash|sh|zsh)$/.test(first)) return "run a script it wrote";
|
|
28
|
+
if (/^(python\d*|node)$/.test(first)) return "run code in this project";
|
|
29
|
+
if (/^(rm|shred)$/.test(first)) return "delete files";
|
|
30
|
+
if (first === "sudo") return "make changes as administrator";
|
|
31
|
+
return `run ${first}`;
|
|
32
|
+
}
|
|
33
|
+
function plainAsk(path, q, { max = 2 } = {}) {
|
|
34
|
+
const text = String(q.q || ""), opts = (q.options || []).slice(0, max);
|
|
35
|
+
let ask = text, labels = opts.map((o) => o.replace(PATHISH, "a file"));
|
|
36
|
+
if (opts.some((o) => /allowlist\.proposed/i.test(o))) {
|
|
37
|
+
let rules = []; try { const p = JSON.parse(readFileSync(join(path, ".symbiot", "allowlist.proposed.json"), "utf8")).permissions || {}; rules = [...new Set([...(p.allow || []).map(PLAIN_RULE), ...(p.additionalDirectories || []).map((d) => `work in ${base(d)}`)])]; } catch {}
|
|
38
|
+
ask = `${text.replace(PATHISH, "a file")}${rules.length ? ` It wants to ${rules.slice(0, 6).join(", ")}${rules.length > 6 ? `, and ${rules.length - 6} more things` : ""}.` : ""} If you say no, it skips that part and tells you what it left out.`;
|
|
39
|
+
labels = opts.map((o) => (/allowlist\.proposed/i.test(o) ? "Allow" + (/recommended/i.test(o) ? " (recommended)" : "") : o.replace(PATHISH, "a file")));
|
|
40
|
+
}
|
|
41
|
+
// raw: the question as the agent wrote it, the key its answer is saved under (agents.mjs
|
|
42
|
+
// answerQuestions). Sending the plain words back matched nothing, so a permission card's
|
|
43
|
+
// Allow, and Send under it, did nothing at all (whatsapp_module, 2026-10-12).
|
|
44
|
+
const shown = ask.replace(PATHISH, "a file");
|
|
45
|
+
return { q: shown, ...(shown !== text ? { raw: text } : {}), ...(labels.some((l, i) => l !== opts[i]) ? { labels } : {}) };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export { PATHISH, PLAIN_RULE, plainAsk };
|
package/ui.mjs
CHANGED
|
@@ -1890,8 +1890,8 @@ function relHtml(rl){return "<div class='qrel"+(rl.waiting?" wait":"")+"'>Needs
|
|
|
1890
1890
|
function askHtml(a){var k=a.ask;if(!k)return '';var qs=k.questions||[],ss=k.suggestions||[];if(!qs.length&&!ss.length)return '';
|
|
1891
1891
|
var h="<div class='aq' data-id='"+esc(a.id)+"'>";
|
|
1892
1892
|
if(qs.length){h+="<h4><i class=ic-ask></i> "+qs.length+" question"+(qs.length>1?"s":"")+" for you</h4>";
|
|
1893
|
-
qs.forEach(function(q,i){var rl=q.release;h+="<div class='q' data-i='"+i+"'><div class='qt'>"+esc(q.q)+"</div>"+(q.context?"<div class='qc'>"+esc(q.context)+"</div>":"")+(rl?relHtml(rl):"");
|
|
1894
|
-
(q.options||[]).forEach(function(o,j){var off=rl&&rl.waiting&&DONEOPT.test(o);h+="<label class='opt"+(off?" off":"")+"'><input type='radio' name='q_"+esc(a.id)+"_"+i+"' value='"+j+"'"+(off?" disabled":"")+"><span>"+whoHtml(o)+(off?" <i>(once "+esc(rl.name+" "+rl.needs)+" is installed)</i>":"")+"</span></label>";});
|
|
1893
|
+
qs.forEach(function(q,i){var rl=q.release;h+="<div class='q' data-i='"+i+"'><div class='qt'>"+esc(q.shown||q.q)+"</div>"+(q.context?"<div class='qc'>"+esc(q.context)+"</div>":"")+(rl?relHtml(rl):"");
|
|
1894
|
+
(q.options||[]).forEach(function(o,j){var off=rl&&rl.waiting&&DONEOPT.test(o);h+="<label class='opt"+(off?" off":"")+"'><input type='radio' name='q_"+esc(a.id)+"_"+i+"' value='"+j+"'"+(off?" disabled":"")+"><span>"+whoHtml((q.labels&&q.labels[j])||o)+(off?" <i>(once "+esc(rl.name+" "+rl.needs)+" is installed)</i>":"")+"</span></label>";});
|
|
1895
1895
|
h+="<input class='qother' placeholder='"+((q.options&&q.options.length)?"or answer in your own words":"your answer")+"'></div>";});
|
|
1896
1896
|
h+="<div class='row'><button class='act qsend' title='save the answers and hand the repo back to your agent'>Send answers & continue</button><button class='ghost qsave' title='save the answers for the next run'>Save only</button></div>";}
|
|
1897
1897
|
if(ss.length){var shown=IDEASOPEN[a.path]?ss.length:(k.ideasShown||ss.length);h+="<h4><i class=ic-idea></i> Ideas from the agent</h4>";ss.forEach(function(s,i){if(i>=shown)return;h+="<div class='idea'><span style='flex:1'>"+esc(s.text)+(s.other?" <span class='tag' title='this idea is for another project, so + task adds it to that one'>for "+esc(s.repo)+"</span>":"")+"</span>"+(s.added?"<span class='tag'>in Tasks</span>":"<button class='ghost qidea' data-i='"+i+"' style='padding:3px 9px;font-size:12px'>+ task</button><button class='ghost qskip' data-i='"+i+"' title='turn this idea down: the next one moves up, and the agent is told not to suggest it again' style='padding:3px 9px;font-size:12px'>Skip</button>")+"</div>";});
|
|
@@ -2175,8 +2175,8 @@ var h='',W="<span class='pcwait'> waiting to send</span>";
|
|
|
2175
2175
|
(c.notes||[]).forEach(function(n){h+="<div class='task' data-note='"+escQ(n.id)+"'><span class='t err'>"+esc(n.error)+(n.what?" <span class='muted'>("+esc(n.what)+")</span>":"")+"</span><button class='ghost pcnote'>OK</button></div>";});
|
|
2176
2176
|
var asks=c.asks||[],aps=c.approves||[],needs=c.needs||[];
|
|
2177
2177
|
if(asks.length||aps.length||needs.length){h+="<div class='mkneeds lit'><div class='pshead'><span>Needs you · "+(asks.length+aps.length+needs.length)+"</span><small>on "+esc(d.name)+"</small></div>";
|
|
2178
|
-
asks.forEach(function(a,i){var q=(a.questions||[])[0]||{};h+="<div class='pcq' data-ask='"+i+"'><div class='qn'>"+esc(a.name)+" asks"+((a.questions||[]).length>1?" · 1 of "+a.questions.length:"")+"</div><div class='qq'>"+esc(q.q||'')+"</div>"+
|
|
2179
|
-
(a.waiting?"<div class='note' style='margin:0'>You answered: "+esc((a.answered||[])[0]||'')+W+"</div>":"<div class='opts'>"+(q.options||[]).map(function(o,j){return "<button class='"+(j?'ghost':'act')+" pcopt' data-j='"+j+"'>"+esc(o)+"</button>";}).join('')+"</div><form class='pcown'><input placeholder='Or say it in your own words' maxlength='2000'><button class='ghost' type='submit'>Send</button></form>")+"</div>";});
|
|
2178
|
+
asks.forEach(function(a,i){var q=(a.questions||[])[0]||{};h+="<div class='pcq' data-ask='"+i+"'><div class='qn'>"+esc(a.name)+" asks"+((a.questions||[]).length>1?" · 1 of "+a.questions.length:"")+"</div><div class='qq'>"+esc(q.shown||q.q||'')+"</div>"+
|
|
2179
|
+
(a.waiting?"<div class='note' style='margin:0'>You answered: "+esc((a.answered||[])[0]||'')+W+"</div>":"<div class='opts'>"+(q.options||[]).map(function(o,j){return "<button class='"+(j?'ghost':'act')+" pcopt' data-j='"+j+"'>"+esc((q.labels&&q.labels[j])||o)+"</button>";}).join('')+"</div><form class='pcown'><input placeholder='Or say it in your own words' maxlength='2000'><button class='ghost' type='submit'>Send</button></form>")+"</div>";});
|
|
2180
2180
|
aps.forEach(function(a,i){h+="<div class='pcq' data-ap='"+i+"'><div class='qn'>"+esc(a.repo)+" · waiting for your Approve</div><div class='qq'>"+(a.tasks.length?a.tasks.length+" task"+(a.tasks.length>1?"s":"")+" done":"Changes without a task")+" · <span class='muted'>"+esc(a.stat||(a.files+" file"+(a.files===1?"":"s")+" changed"))+"</span></div>"+(a.tasks.length?"<ul>"+a.tasks.map(function(t){return "<li>"+esc(t.text)+"</li>";}).join('')+"</ul>":"")+
|
|
2181
2181
|
(a.waiting?"<div class='note' style='margin:0'>Approved here"+W+"</div>":"<div class='row' style='margin:0'><button class='act pcapprove'>Approve</button><span class='muted' style='font-size:12px'>exactly this: if the work changes before your computer gets it, it isn’t approved</span></div>")+"</div>";});
|
|
2182
2182
|
needs.forEach(function(n){h+="<div class='task'><span class='t'>"+esc(n.title)+" <span class='muted' style='font-size:12px'>"+esc(n.sub)+" · on your computer</span></span></div>";});
|
|
@@ -3118,13 +3118,13 @@ if(!ps.length){el.innerHTML='';return;}
|
|
|
3118
3118
|
el.innerHTML=ps.map(function(p,i){var nm=p.name||p.repo,q=(p.qs||[])[0],st=p.parked?'parked':(p.qs&&p.qs.length)?'asks you':p.ready?'ready for your OK':p.state||'';
|
|
3119
3119
|
var h="<section class='rly"+((p.qs&&p.qs.length)||p.ready?' lit':'')+"' data-i='"+i+"'><div class='rlh'><b>"+esc(nm)+"</b>"+(st?"<span class='rls'>"+esc(st)+"</span>":"")+"</div>";
|
|
3120
3120
|
if(p.summary)h+="<p class='rlsum' title='"+escQ(p.summary)+"'>"+esc(p.summary)+"</p>";
|
|
3121
|
-
if(q)h+="<div class='lqblob rlask'>"+blobBody({kind:'ask',name:nm,repo:p.repo,q:q.q,options:q.options,path:q.path,fix:q.fix,id:q.id,sub:q.sub,draft:q.draft,signInTo:q.signInTo,more:Math.max(0,(p.asks||p.qs.length)-1)},false)+"</div>";
|
|
3121
|
+
if(q)h+="<div class='lqblob rlask'>"+blobBody({kind:'ask',name:nm,repo:p.repo,q:q.q,raw:q.raw,labels:q.labels,options:q.options,path:q.path,fix:q.fix,id:q.id,sub:q.sub,draft:q.draft,signInTo:q.signInTo,more:Math.max(0,(p.asks||p.qs.length)-1)},false)+"</div>";
|
|
3122
3122
|
if(p.ready)h+="<button type='button' class='act rlok'>Review and approve</button>";
|
|
3123
3123
|
if(p.handing&&p.handing.length)h+="<div class='rlk'>Handed over</div>"+p.handing.map(function(x){var pg=handProg(x);return "<p class='rlsum rlhand' title='"+escQ(x.text)+"'><b>"+esc(x.lane)+"</b>: "+esc(x.why)+(pg?" \u00b7 "+esc(pg):"")+"</p>";}).join('');
|
|
3124
3124
|
if(p.ideas&&p.ideas.length)h+="<div class='rlk'>Extra tasks it suggests</div>"+p.ideas.map(function(d,j){return "<div class='rli' data-j='"+j+"'><span title='"+escQ(d.full)+"'>"+esc(d.text)+"</span><span class='rlia'><button type='button' class='rladd'>+ task</button><button type='button' class='rlskip'>Skip</button></span></div>";}).join('');
|
|
3125
3125
|
return h+"<div class='rlf'><button type='button' class='rlopen'>Its tasks ›</button><button type='button' class='rlag'>Its agents ›</button></div></section>";}).join('');
|
|
3126
3126
|
el.querySelectorAll('.rly').forEach(function(box){var p=ps[+box.getAttribute('data-i')],q=(p.qs||[])[0],nm=p.name||p.repo;
|
|
3127
|
-
var ab=box.querySelector('.rlask');if(ab&&q)wireBlob(ab,{kind:'ask',name:nm,repo:p.repo,q:q.q,options:q.options,path:q.path,fix:q.fix,id:q.id},{done:function(){LQ.relaySig='';lqLoadWork();}});
|
|
3127
|
+
var ab=box.querySelector('.rlask');if(ab&&q)wireBlob(ab,{kind:'ask',name:nm,repo:p.repo,q:q.q,raw:q.raw,labels:q.labels,options:q.options,path:q.path,fix:q.fix,id:q.id},{done:function(){LQ.relaySig='';lqLoadWork();}});
|
|
3128
3128
|
var ok=box.querySelector('.rlok');if(ok)ok.addEventListener('click',function(){lqOpen({kind:'proj',repo:p.repo,lit:true});});
|
|
3129
3129
|
box.querySelector('.rlopen').addEventListener('click',function(){lqOpen({kind:'proj',repo:p.repo});});
|
|
3130
3130
|
box.querySelector('.rlag').addEventListener('click',function(){lqPool('agents');});
|