symbiot 0.63.1 → 0.63.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/agents.mjs +30 -8
- package/home.mjs +38 -2
- package/package.json +1 -1
- package/ui.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
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.3
|
|
6
|
+
|
|
7
|
+
- **Allow works on a request that takes access away.** When an agent asked to lock itself out of something (a deny rule, with nothing new allowed), Symbiot dropped the request, so clicking Allow did nothing and the agent waited for good. Now it's turned on, without asking you, and the agent carries on.
|
|
8
|
+
- **A script in your project no longer reads as "outside your work".** Running `python3 supabase/scripts/x.py`, or checking a public web address, was taken for a folder elsewhere on your computer, so you got a permission card. Now it turns on by itself. A web request that sends something, and anything about a live project, still asks you.
|
|
9
|
+
|
|
10
|
+
## 0.63.2
|
|
11
|
+
|
|
12
|
+
- **Permission cards stop landing on you for Symbiot's own plumbing.** An agent connecting to a computer this one already connects to (your own server) no longer asks you, and a request too broad to allow safely goes back to the agent to spell out, not to you.
|
|
13
|
+
- **What still needs you reads in plain words.** A permission card says what it lets the agent do, like "use your phone over the USB cable" or "connect to the computer at 92.4.133.97", with an Allow button. No file names or commands, on the card or when you hover over it.
|
|
14
|
+
|
|
5
15
|
## 0.63.1
|
|
6
16
|
|
|
7
17
|
- **Fewer things put on your plate.** A permission an agent needs for your own work (building your app, using your phone over USB, recording its screen) now turns on by itself instead of showing up as a card: a tool installed in `~/.local/bin` and the marketing lane's own folder no longer count as "outside your work".
|
package/agents.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// tab, and the questions an unattended agent leaves for the user.
|
|
4
4
|
import { MARKETING_DIR } from "./marketing.mjs";
|
|
5
5
|
import { licenceState, isPro, can, canWorkIn } from "./licence.mjs";
|
|
6
|
-
import { spawn } from "node:child_process";
|
|
6
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { join, dirname, resolve, relative } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
@@ -1077,7 +1077,7 @@ function installAllowlist(path) {
|
|
|
1077
1077
|
let p; try { p = JSON.parse(readFileSync(join(path, ".symbiot", "allowlist.proposed.json"), "utf8")).permissions || {}; } catch { return null; }
|
|
1078
1078
|
const list = (x) => (Array.isArray(x) ? x.filter((r) => typeof r === "string" && r.trim()).map((r) => r.trim()) : []);
|
|
1079
1079
|
const allow = list(p.allow), deny = list(p.deny), dirs = list(p.additionalDirectories);
|
|
1080
|
-
if (!allow.length && !dirs.length) return null;
|
|
1080
|
+
if (!allow.length && !dirs.length && !deny.length) return null;
|
|
1081
1081
|
const wide = allow.filter(wideRule);
|
|
1082
1082
|
if (wide.length) return { refused: wide };
|
|
1083
1083
|
const f = join(path, ".claude", "settings.local.json");
|
|
@@ -1085,7 +1085,8 @@ function installAllowlist(path) {
|
|
|
1085
1085
|
const perm = cur.permissions || {}, merge = (a, b) => [...new Set([...list(a), ...b])];
|
|
1086
1086
|
cur.permissions = { ...perm, allow: merge(perm.allow, allow), deny: merge(perm.deny, [...deny, ...GUARD()]), additionalDirectories: merge(perm.additionalDirectories, dirs) };
|
|
1087
1087
|
try { mkdirSync(join(path, ".claude"), { recursive: true }); writeFileSync(f, JSON.stringify(cur, null, 2) + "\n", { mode: 0o600 }); } catch { return null; }
|
|
1088
|
-
|
|
1088
|
+
const n = allow.length + deny.length;
|
|
1089
|
+
return [`the allow list for this folder (${n} rule${n === 1 ? "" : "s"})`];
|
|
1089
1090
|
}
|
|
1090
1091
|
// ---- the owner's work, and allow lists inside it ---------------------------------------
|
|
1091
1092
|
// What the owner told Symbiot is their work: the folders it scans (Settings → Folders) and
|
|
@@ -1114,14 +1115,27 @@ function inWork(p, roots = workRoots()) {
|
|
|
1114
1115
|
// A Bash rule's own program, when it lives in a bin folder (~/.local/bin/adb), is a tool
|
|
1115
1116
|
// it runs, not a place it works in: only the paths after it count.
|
|
1116
1117
|
const BIN_PROGRAM = /^(Bash\(\s*)(?:~|\/)[^\s:()]*\/s?bin\/[^\s:()/]+/;
|
|
1117
|
-
|
|
1118
|
+
// A path starts a word: the "/scripts" in a relative supabase/scripts/x.py is in the lane, and
|
|
1119
|
+
// the "//host" in https://host is a site, not a folder. /dev/null is nobody's.
|
|
1120
|
+
const rulePaths = (r) => (String(r).replace(BIN_PROGRAM, "$1").match(/(?<=^|[\s(="'])(?:~|\/)[^\s:*()"',]*/g) || []).filter((x) => (x === "~" || x.length > 1) && !/^\/dev\/(null|std(in|out|err))$/.test(x));
|
|
1121
|
+
// curl or wget that sends something (a POST, a form, a file) goes out, like a push.
|
|
1122
|
+
const SENDS = /^Bash\(\s*(curl|wget)\b.*\s(-X\s*(POST|PUT|PATCH|DELETE)|-d|--data\S*|-F|--form|-T|--upload-file|--post-\w+)\b/i;
|
|
1123
|
+
// An ssh, scp or rsync rule whose every remote (user@host, user@host:path) is a host this
|
|
1124
|
+
// computer already connects to (it's in ~/.ssh/known_hosts) reaches the owner's own
|
|
1125
|
+
// machine, not another one: it counts as their work. A wildcard host never does.
|
|
1126
|
+
function knownHost(host) { try { return !/[*?]/.test(host) && spawnSync("ssh-keygen", ["-F", host], { encoding: "utf8", timeout: 5000 }).status === 0; } catch { return false; } }
|
|
1127
|
+
function toKnownHost(r, known = knownHost) {
|
|
1128
|
+
const m = String(r).match(/^Bash\(\s*(ssh|scp|rsync)\b(.*)\)$/); if (!m) return false;
|
|
1129
|
+
const hosts = m[2].split(/\s+/).filter((t) => t.includes("@") && !t.startsWith("-")).map((t) => t.split("@").pop().split(":")[0]).filter(Boolean);
|
|
1130
|
+
return hosts.length > 0 && hosts.every((h) => known(h));
|
|
1131
|
+
}
|
|
1118
1132
|
// Whether a proposed list can be turned on without asking: { ok } or { why }.
|
|
1119
1133
|
function allowlistInWork(path) {
|
|
1120
1134
|
let p; try { p = JSON.parse(readFileSync(join(path, ".symbiot", "allowlist.proposed.json"), "utf8")).permissions || {}; } catch { return { why: "no list" }; }
|
|
1121
1135
|
const list = (x) => (Array.isArray(x) ? x.filter((r) => typeof r === "string" && r.trim()).map((r) => r.trim()) : []);
|
|
1122
1136
|
const allow = list(p.allow), dirs = list(p.additionalDirectories), roots = [...workRoots(), path];
|
|
1123
1137
|
const wide = allow.filter(wideRule); if (wide.length) return { why: "wide: " + wide.join(", ") };
|
|
1124
|
-
const pub = allow.filter((r) => PUBLISH_RULE.test(r)); if (pub.length) return { why: "publishes or reaches another machine: " + pub.join(", ") };
|
|
1138
|
+
const pub = allow.filter((r) => (PUBLISH_RULE.test(r) || SENDS.test(r)) && !toKnownHost(r)); if (pub.length) return { why: "publishes or reaches another machine: " + pub.join(", ") };
|
|
1125
1139
|
const out = [...dirs, ...allow.flatMap(rulePaths)].filter((x) => !inWork(x, roots));
|
|
1126
1140
|
if (out.length) return { why: "outside your work: " + [...new Set(out)].slice(0, 3).join(", ") };
|
|
1127
1141
|
return { ok: true, allow, dirs };
|
|
@@ -1136,7 +1150,15 @@ function autoAllow(path) {
|
|
|
1136
1150
|
if (!q) return null;
|
|
1137
1151
|
const mark = join(path, ".symbiot", "autoallow.json"), d = digest(raw); let done = []; try { done = JSON.parse(readFileSync(mark, "utf8")); } catch {}
|
|
1138
1152
|
if (done.includes(d)) return null;
|
|
1139
|
-
const v = allowlistInWork(path);
|
|
1153
|
+
const v = allowlistInWork(path);
|
|
1154
|
+
if (!v.ok && /^wide: /.test(v.why)) { // the agent's to fix, not the user's: it writes the commands out and asks again
|
|
1155
|
+
try { writeFileSync(mark, JSON.stringify([...done, d].slice(-20))); } catch {}
|
|
1156
|
+
try { writeFileSync(join(path, ".symbiot", "ANSWERS.md"), (readSymbiot(path, "ANSWERS.md") || "# Answers from the user\nAnswers to the questions in QUESTIONS.md, newest last. Follow them; ask again in QUESTIONS.md if one is unclear.\n").replace(/\s*$/, "\n") + `\n### ${q.q}\nSymbiot didn't turn your list on, and didn't pass it to the user: ${v.why.slice(6)} would let you run anything (a script or a shell can hold any command). Write each command out exactly in .symbiot/allowlist.proposed.json instead, with the files and hosts it touches named (for a script, the commands inside it), and ask again.\n_${new Date().toISOString().slice(0, 10)}_\n`); } catch {}
|
|
1157
|
+
try { unlinkSync(join(path, ".symbiot", WAITING)); } catch {}
|
|
1158
|
+
const e = runHandoff(path, { force: true });
|
|
1159
|
+
return { narrower: true, rerun: e && e.id ? e.id : null };
|
|
1160
|
+
}
|
|
1161
|
+
if (!v.ok) return { asked: true, why: v.why };
|
|
1140
1162
|
const g = installAllowlist(path); if (!g || g.refused) return null;
|
|
1141
1163
|
try { writeFileSync(mark, JSON.stringify([...done, d].slice(-20))); } catch {}
|
|
1142
1164
|
const day = new Date().toISOString().slice(0, 10);
|
|
@@ -1151,7 +1173,7 @@ function autoAllow(path) {
|
|
|
1151
1173
|
// recommended option is a step it takes itself (not 👤), and nothing in the question or
|
|
1152
1174
|
// that option is an approval, goes out, costs money, touches an account or a machine, or
|
|
1153
1175
|
// can't be undone, Symbiot picks it and the agent carries on. Everything else still comes to the user.
|
|
1154
|
-
const OUTWARD = /\b(approv\w*|publish\w*|releas\w*|push\w*|merg\w*|send\w*|sent|post\w*|repl\w*|e-?mail\w*|pay\w*|spend\w*|buy\w*|money|bill\w*|cost\w*|delet\w*|remov\w*|uninstall\w*|wip\w*|drop\w*|password\w*|2fa|token\w*|keys?|sign(?:ed|s)?[- ]?in|log(?:ged)?[- ]?in|accounts?|subscription|plan|tunnel\w*|prox\w*|ssh|vm|servers?|restart\w*|stop\w*|kill\w*|broker\w*|trad\w*|bank\w*|npm|deploy\w*|install\w*|phone|whatsapp|linkedin|github\.com)\b|\byour (?:default|settings?|profile)\b/i;
|
|
1176
|
+
const OUTWARD = /\b(approv\w*|publish\w*|releas\w*|push\w*|merg\w*|send\w*|sent|post\w*|repl\w*|e-?mail\w*|pay\w*|spend\w*|buy\w*|money|bill\w*|cost\w*|delet\w*|remov\w*|uninstall\w*|wip\w*|drop\w*|password\w*|2fa|token\w*|keys?|sign(?:ed|s)?[- ]?in|log(?:ged)?[- ]?in|accounts?|subscription|plan|tunnel\w*|prox\w*|ssh|vm|servers?|restart\w*|stop\w*|kill\w*|broker\w*|trad\w*|bank\w*|npm|deploy\w*|install\w*|phone|whatsapp|linkedin|github\.com|live|production|prod)\b|\byour (?:default|settings?|profile)\b/i;
|
|
1155
1177
|
function autoPick(q) {
|
|
1156
1178
|
const rec = (q.options || []).filter((o) => /\(recommended\b/i.test(o));
|
|
1157
1179
|
if (rec.length !== 1 || yourStep(rec[0])) return "";
|
|
@@ -1305,4 +1327,4 @@ function agentsList() {
|
|
|
1305
1327
|
}).concat(earlierRuns());
|
|
1306
1328
|
}
|
|
1307
1329
|
|
|
1308
|
-
export { continueLanes, autoPick, autoDecide, FACTS, factsOf, knownRun, withStream, workOf, HANDOFFS, HANDOFF_PROMPT, QUESTIONS_MAX, OPTIONS_SHOWN, IDEAS_SHOWN, shSingle, CLAUDE_CMD, ORCA_CLAUDE_CMD, handoffCmd, setHandoffCmd, grantAgent, grantRule, allowTool, claudeConnectors, withConnectors, linkedConnectors, connectorsLine, connectorsInfo, linkReach, fillHandoff, runHandoff, nothingToDo, PARKED_NOTE, parkedPaths, isParked, parkLane, agentMissing, blockedAgain, runningHandoff, loadRuns, earlierRuns, namedFiles, waitingFor, startWaiting, noteUntracked, untrackedBefore, forgetUntracked, writeTasks, droppedTasks, releaseHeldTasks, startHeldTasks, detectHandoffs, pickAgent, findOrcaCli, orcaHandoffCmd, migrateOrcaCmd, migrateClaudeCmd, track, agentChanges, parseQuestions, suggestionTarget, skipIdea, agentQuestions, answerQuestions, agentsList, needsOf, settleNeeds, NEEDS_FOR, readLastWords, readNeeds, parseRead, READ_FILE, autoAllow, autoAllowSweep, allowlistInWork, installAllowlist, inWork, withScope, withTrust, sandboxFor, sandboxEnv, sandboxNeeds, sandboxState, sandboxWrites, SANDBOX_DIR, resumeFor, noteSession, trustFull, GUARD_SETTINGS, isUrgent, urgentFirst, urgentUndo, urgentDone, urgentSweep, urgentState , licenceGate };
|
|
1330
|
+
export { toKnownHost, continueLanes, autoPick, autoDecide, FACTS, factsOf, knownRun, withStream, workOf, HANDOFFS, HANDOFF_PROMPT, QUESTIONS_MAX, OPTIONS_SHOWN, IDEAS_SHOWN, shSingle, CLAUDE_CMD, ORCA_CLAUDE_CMD, handoffCmd, setHandoffCmd, grantAgent, grantRule, allowTool, claudeConnectors, withConnectors, linkedConnectors, connectorsLine, connectorsInfo, linkReach, fillHandoff, runHandoff, nothingToDo, PARKED_NOTE, parkedPaths, isParked, parkLane, agentMissing, blockedAgain, runningHandoff, loadRuns, earlierRuns, namedFiles, waitingFor, startWaiting, noteUntracked, untrackedBefore, forgetUntracked, writeTasks, droppedTasks, releaseHeldTasks, startHeldTasks, detectHandoffs, pickAgent, findOrcaCli, orcaHandoffCmd, migrateOrcaCmd, migrateClaudeCmd, track, agentChanges, parseQuestions, suggestionTarget, skipIdea, agentQuestions, answerQuestions, agentsList, needsOf, settleNeeds, NEEDS_FOR, readLastWords, readNeeds, parseRead, READ_FILE, autoAllow, autoAllowSweep, allowlistInWork, installAllowlist, inWork, withScope, withTrust, sandboxFor, sandboxEnv, sandboxNeeds, sandboxState, sandboxWrites, SANDBOX_DIR, resumeFor, noteSession, trustFull, GUARD_SETTINGS, isUrgent, urgentFirst, urgentUndo, urgentDone, urgentSweep, urgentState , licenceGate };
|
package/home.mjs
CHANGED
|
@@ -355,7 +355,7 @@ function homeState({ now = Date.now(), fresh = false, deps = {} } = {}) {
|
|
|
355
355
|
if (!q) continue;
|
|
356
356
|
const lane = laneOfPath(a.path, map), repo = lane || (lane === "" ? "" : a.name);
|
|
357
357
|
you.push({ kind: "ask", id: "ask:" + a.path, path: a.path, repo, name: repo ? named(lane ? map[lane] : a.path, repo) : plain(String(a.name || "an agent").replace(/^Agent:\s*/, ""), 60),
|
|
358
|
-
title: `${a.name} asks`, sub: String(q.q || "").slice(0, 90),
|
|
358
|
+
title: `${a.name} asks`, sub: String(q.q || "").slice(0, 90), ...plainAsk(a.path, q), options: (q.options || []).slice(0, 2), ...(qs.length > 1 ? { more: qs.length - 1 } : {}), ...signInCard([q.q, ...(q.options || []).slice(0, 2)]), shape: "agents" });
|
|
359
359
|
}
|
|
360
360
|
// What stopped and can't go on without you, as a question too: a handover that
|
|
361
361
|
// couldn't start (lanes.mjs stuckHandovers: "Allow this run access to ~/Company?"),
|
|
@@ -404,6 +404,42 @@ function noteAnswer(path, heading, text) {
|
|
|
404
404
|
const f = join(path, ".symbiot", "ANSWERS.md"); let had = ""; try { had = readFileSync(f, "utf8"); } catch {}
|
|
405
405
|
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 {}
|
|
406
406
|
}
|
|
407
|
+
// An ask in words for someone who doesn't code: a permission request lists what the agent
|
|
408
|
+
// wants to run, with Allow / Don't allow; any file path left in the question or choices
|
|
409
|
+
// becomes "a file". The answer sent back is still the choice's own text (labels only change what shows).
|
|
410
|
+
const PATHISH = /`?(?:~|\.{0,2})?\/?(?:[\w.-]+\/)+[\w.-]+\.(?:json|md|mjs|js|env|txt|sh)`?|`?\b[\w-]+\.(?:json|mjs)\b`?/g;
|
|
411
|
+
// What a rule lets an agent do, said the way you'd say it to someone who doesn't code.
|
|
412
|
+
const base = (p) => String(p || "").replace(/[/*]+$/, "").split("/").filter(Boolean).pop() || "your files";
|
|
413
|
+
function PLAIN_RULE(r) {
|
|
414
|
+
const m = String(r).match(/^(\w+)\((.*)\)$/); if (!m) return "work in " + base(r);
|
|
415
|
+
const [, tool, arg] = m, a = arg.replace(/:?\*+$/, "").trim(), first = a.split(/\s+/)[0].replace(/:\*?$/, "").split("/").pop();
|
|
416
|
+
if (tool === "Read") return `read files in ${base(a)}`;
|
|
417
|
+
if (/^(Edit|Write|MultiEdit)$/.test(tool)) return `change files in ${base(a)}`;
|
|
418
|
+
if (tool !== "Bash") return `use ${tool}`;
|
|
419
|
+
const host = (a.match(/@([\w.-]+)/) || [])[1];
|
|
420
|
+
if (/^(ssh|scp|rsync)$/.test(first)) return host ? `connect to the computer at ${host}` : "connect to another computer";
|
|
421
|
+
if (first === "adb") return "use your phone over the USB cable";
|
|
422
|
+
if (/^(flutter|gradle|gradlew|godot|xcodebuild)$/.test(first) || /\bbuild\b/.test(a)) return "build the app";
|
|
423
|
+
if (first === "ffmpeg" || first === "ffprobe") return "make and trim video clips";
|
|
424
|
+
if (first === "git" && /\bpush\b/.test(a)) return "send code to GitHub";
|
|
425
|
+
if (/^(npm|yarn|pnpm)$/.test(first) && /\bpublish\b/.test(a)) return "publish a new version";
|
|
426
|
+
if (/^(bash|sh|zsh)$/.test(first)) return "run a script it wrote";
|
|
427
|
+
if (/^(python\d*|node)$/.test(first)) return "run code in this project";
|
|
428
|
+
if (/^(rm|shred)$/.test(first)) return "delete files";
|
|
429
|
+
if (first === "sudo") return "make changes as administrator";
|
|
430
|
+
return `run ${first}`;
|
|
431
|
+
}
|
|
432
|
+
function plainAsk(path, q) {
|
|
433
|
+
const text = String(q.q || ""), opts = (q.options || []).slice(0, 2);
|
|
434
|
+
let ask = text, labels = opts.map((o) => o.replace(PATHISH, "a file"));
|
|
435
|
+
if (opts.some((o) => /allowlist\.proposed/i.test(o))) {
|
|
436
|
+
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 {}
|
|
437
|
+
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.`;
|
|
438
|
+
labels = opts.map((o) => (/allowlist\.proposed/i.test(o) ? "Allow" + (/recommended/i.test(o) ? " (recommended)" : "") : o.replace(PATHISH, "a file")));
|
|
439
|
+
}
|
|
440
|
+
return { q: ask.replace(PATHISH, "a file"), ...(labels.some((l, i) => l !== opts[i]) ? { labels } : {}) };
|
|
441
|
+
}
|
|
442
|
+
|
|
407
443
|
function homeAnswer(id, { pick, text = "" } = {}, deps = {}) {
|
|
408
444
|
id = String(id || ""); text = String(text || "").trim().slice(0, 2000);
|
|
409
445
|
const go = text ? !SAYS_NO.test(text) : Number(pick) === 0, ref = id.replace(/^\w+:/, "");
|
|
@@ -805,4 +841,4 @@ function setOnboarding({ step, skip, unskip, skipRest, done, restart } = {}) {
|
|
|
805
841
|
return onboarding();
|
|
806
842
|
}
|
|
807
843
|
|
|
808
|
-
export { reportIdeasAdd, reportAsk, reportDraftAnswer, workTick, waitWhy, AUTO_GRACE, STUCK_AFTER, marketingState, marketingGo, marketingDraftAnswer, marketingTask, moveToMarketing, goLane, homeState, homeContext, homeAsk, homeAnswer, homeNext, nextUp, laneNamed, NEXT_FILE, workScene, workGo, displayName, firstSteps, onboarding, setOnboarding, startOnboarding, ONB_STEPS, ONB_STEPS_PHONE, onbSteps, phoneBits , phoneSetupFirst };
|
|
844
|
+
export { plainAsk, reportIdeasAdd, reportAsk, reportDraftAnswer, workTick, waitWhy, AUTO_GRACE, STUCK_AFTER, marketingState, marketingGo, marketingDraftAnswer, marketingTask, moveToMarketing, goLane, homeState, homeContext, homeAsk, homeAnswer, homeNext, nextUp, laneNamed, NEXT_FILE, workScene, workGo, displayName, firstSteps, onboarding, setOnboarding, startOnboarding, ONB_STEPS, ONB_STEPS_PHONE, onbSteps, phoneBits , phoneSetupFirst };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "symbiot",
|
|
3
|
-
"version": "0.63.
|
|
3
|
+
"version": "0.63.3",
|
|
4
4
|
"description": "Your week, written from your real work. Reads your local git and writes it up with the AI of your choice — Claude, OpenAI, Gemini, or a local model.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/ui.mjs
CHANGED
|
@@ -3213,7 +3213,7 @@ if(y.kind!=='ask')return h+"<div class='bq'>"+esc(y.kind==='approve'?String(y.su
|
|
|
3213
3213
|
var os=y.options||[];if(y.draft)h+=draftHtml(y.draft);h+="<div class='bq' title='"+escQ((y.q||'')+(y.why?' ('+y.why+')':''))+"'>"+esc(y.q||y.sub||'')+"</div>";
|
|
3214
3214
|
if(y.fix==='handover'&&y.sub)h+="<div class='bfor' title='"+escQ(y.sub)+"'>for: "+esc(y.sub)+"</div>";
|
|
3215
3215
|
if(y.signInTo)h+="<div class='bo'><button type='button' class='nopt bsign' title='opens "+escQ(y.signInTo.name)+" in the Symbiot Browser: sign in there, then click Done'><span class='bt'>Sign in to "+esc(y.signInTo.name)+" ›</span></button></div>";
|
|
3216
|
-
if(os.length)h+="<div class='bo'>"+os.map(function(o,j){return "<button type='button' class='nopt"+(j===0&&/[(]recommended[)]/i.test(o)?' rec':'')+"' data-j='"+j+"' title='"+escQ(o)+"'><span class='bt'>"+whoHtml(blobOpt(o))+"</span></button>";}).join('')+"</div><button type='button' class='bfree' aria-expanded='false'>"+(y.draft?'Change it':'or answer in your own words')+"</button>";
|
|
3216
|
+
if(os.length)h+="<div class='bo'>"+os.map(function(o,j){return "<button type='button' class='nopt"+(j===0&&/[(]recommended[)]/i.test(o)?' rec':'')+"' data-j='"+j+"' title='"+escQ((y.labels&&y.labels[j])||o)+"'><span class='bt'>"+whoHtml(blobOpt((y.labels&&y.labels[j])||o))+"</span></button>";}).join('')+"</div><button type='button' class='bfree' aria-expanded='false'>"+(y.draft?'Change it':'or answer in your own words')+"</button>";
|
|
3217
3217
|
h+="<form class='bfx"+(os.length?' hidden':'')+"'><input placeholder='"+(y.draft?'what to change':'your answer')+"' aria-label='"+(y.draft?'what to change in the reply':'your answer to '+escQ(nm))+"'><button type='submit'>"+(y.draft?'Redraft':'Send')+"</button></form>";
|
|
3218
3218
|
if(home&&y.more)h+="<span class='bmore'>+"+y.more+" more question"+(y.more>1?'s':'')+" after this one</span>";
|
|
3219
3219
|
return h;}
|