kaizen-agent 1.34.2 → 1.34.4
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/cli/google.ts +99 -0
- package/cli/install.ts +14 -0
- package/cli/settings.ts +4 -0
- package/cli/sources.test.ts +39 -10
- package/cli/sources.ts +37 -9
- package/cli/state.test.ts +15 -1
- package/cli/state.ts +20 -8
- package/cli/web.ts +43 -7
- package/cli/worktree.test.ts +113 -0
- package/cli/worktree.ts +128 -0
- package/cli/writeback.test.ts +102 -0
- package/cli/writeback.ts +79 -0
- package/package.json +1 -1
- package/skills/kaizen/SKILL.md +4 -11
- package/skills/kaizen/config.default.yml +5 -0
- package/skills/kaizen/settings.md +1 -0
- package/skills/kaizen/spec-build.md +22 -8
- package/skills/kaizen/spec-main.md +12 -6
- package/skills/kaizen/spec-review.md +3 -1
- package/skills/kaizen/spec.md +4 -12
- package/web/board/cards.ts +41 -4
- package/web/board/icebox.ts +53 -7
- package/web/board/panel.ts +1 -0
- package/web/board/settings.ts +26 -1
- package/web/board.html +1 -1
- package/web/board.js +24 -21
- package/web/notebook.js +1 -1
- package/web/term.js +18 -18
- package/web/term.ts +42 -1
package/web/board/cards.ts
CHANGED
|
@@ -43,7 +43,7 @@ function card(c) {
|
|
|
43
43
|
el.dataset.k = k;
|
|
44
44
|
el.setAttribute("role", "button");
|
|
45
45
|
const st = ICON[c.status] || "idea",
|
|
46
|
-
idt = c.kind === "idea" ? "idea" : c.text;
|
|
46
|
+
idt = c.kind === "idea" ? "idea" : c.text.replace(/-/g, " ");
|
|
47
47
|
// The status icon carries the state; what a waiting run needs is its tooltip.
|
|
48
48
|
const sttl = c.status === "waiting" ? need(c.awaiting) || "needs you" : c.status || "idea";
|
|
49
49
|
// Footer: words on the left (project, what it needs), icon-only marks on the right.
|
|
@@ -73,6 +73,7 @@ function card(c) {
|
|
|
73
73
|
// A gathered idea's notes open with the stamp promote() writes (ingest() wrote it before
|
|
74
74
|
// the icebox); its chip names the source.
|
|
75
75
|
const g = c.notes && /^\[[^\]]*\] (?:Gathered from|Promoted from the icebox:) (.+?)\. Outside text/.exec(c.notes);
|
|
76
|
+
if (c.branch) chips.push(`<span class="chip" title="Built on its own branch, ${esc(c.branch)}">branch</span>`);
|
|
76
77
|
if (g) chips.push(`<span class="chip" title="From ${esc(g[1])}">gathered</span>`);
|
|
77
78
|
else if (c.notes) chips.push(`<span class="chip">notes</span>`);
|
|
78
79
|
// Information only: the panel's button is where it is committed and pushed.
|
|
@@ -506,9 +507,10 @@ export function render() {
|
|
|
506
507
|
if (ci === 0 && store.mode === "new") {
|
|
507
508
|
const f = document.createElement("li");
|
|
508
509
|
f.className = "inl";
|
|
509
|
-
f.innerHTML = `${store.all ? `<div class="combo" id="ndc"><button class="sel" id="newdir" aria-label="Project for this idea" aria-haspopup="listbox" aria-expanded="false"></button><div class="menu-pop" hidden><input class="sel" aria-label="Search projects" placeholder="Search projects…" spellcheck="false" autocomplete="off"><div id="nds" role="listbox"></div></div></div>` : ""}<input id="newidea" placeholder="What would you like done?"><textarea id="newnotes" placeholder="Notes: links, paths, background (optional)"></textarea><div class="row"><button class="btn ghost" id="nc">Cancel</button><button class="btn pri" id="ns">Add idea</button></div>`;
|
|
510
|
+
f.innerHTML = `${store.all ? `<div class="combo" id="ndc"><button class="sel" id="newdir" aria-label="Project for this idea" aria-haspopup="listbox" aria-expanded="false"></button><div class="menu-pop" hidden><input class="sel" aria-label="Search projects" placeholder="Search projects…" spellcheck="false" autocomplete="off"><div id="nds" role="listbox"></div></div></div>` : ""}<input id="newidea" placeholder="What would you like done?"><textarea id="newnotes" placeholder="Notes: links, paths, background (optional)"></textarea><div class="att" id="newatt"></div><input type="file" id="newfile" multiple hidden><div class="row"><button class="btn ghost" id="newpick">Attach</button><button class="btn ghost" id="nc">Cancel</button><button class="btn pri" id="ns">Add idea</button></div>`;
|
|
510
511
|
ul.appendChild(f);
|
|
511
|
-
const inp = f.querySelector("#newidea")
|
|
512
|
+
const inp = f.querySelector("#newidea"),
|
|
513
|
+
files = [];
|
|
512
514
|
draft(inp, "f:new:0");
|
|
513
515
|
draft(f.querySelector("textarea"), "f:new:1");
|
|
514
516
|
const save = async () => {
|
|
@@ -518,9 +520,44 @@ export function render() {
|
|
|
518
520
|
store.mode = null;
|
|
519
521
|
const dir = store.all ? newDir : null;
|
|
520
522
|
const r = await post("/inbox", dir ? { op: "add", dir, text, notes } : { op: "add", text, notes });
|
|
521
|
-
if (r.ok)
|
|
523
|
+
if (r.ok) {
|
|
524
|
+
toast("Idea added");
|
|
525
|
+
// The idea exists first: a failed upload leaves it as plain text.
|
|
526
|
+
if (files.length) {
|
|
527
|
+
const enc = await Promise.all(
|
|
528
|
+
files.map(
|
|
529
|
+
(x) =>
|
|
530
|
+
new Promise((res) => {
|
|
531
|
+
const rd = new FileReader();
|
|
532
|
+
rd.onload = () => res({ name: x.name, data: rd.result });
|
|
533
|
+
rd.readAsDataURL(x);
|
|
534
|
+
}),
|
|
535
|
+
),
|
|
536
|
+
);
|
|
537
|
+
const n = await post("/note", { dir, for: text, text: "", files: enc });
|
|
538
|
+
if (!n.ok) toast("Idea added, files not attached");
|
|
539
|
+
}
|
|
540
|
+
}
|
|
522
541
|
load();
|
|
523
542
|
};
|
|
543
|
+
const add = (x) => {
|
|
544
|
+
files.push(x);
|
|
545
|
+
f.querySelector("#newatt").textContent = files.map((y) => y.name).join(" · ");
|
|
546
|
+
};
|
|
547
|
+
f.querySelector("#newpick").onclick = () => f.querySelector("#newfile").click();
|
|
548
|
+
f.querySelector("#newfile").onchange = (e) => {
|
|
549
|
+
[...e.target.files].forEach(add);
|
|
550
|
+
e.target.value = "";
|
|
551
|
+
};
|
|
552
|
+
for (const el of [inp, f.querySelector("textarea")])
|
|
553
|
+
el.onpaste = (e) => {
|
|
554
|
+
for (const it of e.clipboardData.items)
|
|
555
|
+
if (it.kind === "file") {
|
|
556
|
+
const x = it.getAsFile();
|
|
557
|
+
add(new File([x], x.name === "image.png" ? `paste-${Date.now()}.png` : x.name, { type: x.type }));
|
|
558
|
+
e.preventDefault();
|
|
559
|
+
}
|
|
560
|
+
};
|
|
524
561
|
if (store.all) ideaCombo(f);
|
|
525
562
|
f.querySelector("#ns").onclick = save;
|
|
526
563
|
f.querySelector("#nc").onclick = () => {
|
package/web/board/icebox.ts
CHANGED
|
@@ -162,7 +162,7 @@ export async function openSources() {
|
|
|
162
162
|
: s.last.ok
|
|
163
163
|
? `<span class="src-badge ok"><svg viewBox="0 0 16 16"><path d="M3.5 8.5l3 3 6-6" stroke="currentColor" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>${esc(s.last.note)}</span><span class="hint" style="margin:0">${ago(s.last.at)}</span>`
|
|
164
164
|
: `<span class="src-badge warn"><svg viewBox="0 0 16 16"><circle cx="8" cy="8" r="6" stroke="currentColor" fill="none" stroke-width="1.8"/><path d="M8 5v3.5M8 11v.1" stroke="currentColor" fill="none" stroke-width="2" stroke-linecap="round"/></svg>Failed: ${esc(s.last.note)}</span><span class="hint" style="margin:0">${ago(s.last.at)}</span>`;
|
|
165
|
-
return `<div class="src-card" id="src-card-${i}"><div class="src-hd"><div class="src-info"><span class="src-ico" title="${st === "social" ? esc(PLAT[s.social] || "social") : st}">${fav(s.url, st === "sheet" ? "sheet" : st === "doc" ? "doc" : st === "social" ? "at" : "link", s.social)}</span><span class="src-title" title="${esc(title)}">${esc(title)}</span></div><div class="src-acts">${s.social ? `<button class="btn ghost sm" id="src-ag-${i}" data-ag="${i}" aria-label="Gather ${esc(title)} with your agent" title="${chatOn ? "Read this source with your ChatGPT plan" : "Open your agent to run /kaizen gather for this project"}">${ico("refresh")} Gather</button>` : `<button class="btn ghost sm" id="src-chk-${i}" data-chk="${i}" aria-label="Check ${esc(title)} now" title="Check this source now">${ico("refresh")} Check</button>`}<button class="btn ghost danger sm" id="src-rm-${i}" data-rm="${i}" aria-label="Remove ${esc(title)}" title="Remove source">${ico("trash")}</button></div></div><div class="src-url"><a href="${esc(s.url)}" target="_blank" rel="noopener noreferrer" title="${esc(s.url)}"><span>${esc(s.url)}</span>${ico("ext")}</a></div><div class="src-status">${statusHtml}</div><div class="src-dest"><span>Category</span><input class="sel" id="src-ct-${i}" data-ct="${i}" data-k="${esc(s.dir + " " + s.url)}" value="${esc(s.category || "")}" placeholder="e.g. Feedback" maxlength="40" aria-label="Category of ${esc(title)}"><button class="btn sm" id="src-cs-${i}" aria-label="Save the category of ${esc(title)}" disabled>Save</button></div><div class="src-dest"><span>Project</span><select class="sel" id="src-mv-${i}" data-mv="${i}" data-k="${esc(s.dir + " " + s.url)}" aria-label="Project of ${esc(title)}">${opts(s.dir, s.project)}</select><button class="btn sm" id="src-mb-${i}" aria-label="Move ${esc(title)} to the chosen project" disabled>Move</button></div></div>`;
|
|
165
|
+
return `<div class="src-card" id="src-card-${i}"><div class="src-hd"><div class="src-info"><span class="src-ico" title="${st === "social" ? esc(PLAT[s.social] || "social") : st}">${fav(s.url, st === "sheet" ? "sheet" : st === "doc" ? "doc" : st === "social" ? "at" : "link", s.social)}</span><span class="src-title" title="${esc(title)}">${esc(title)}</span></div><div class="src-acts">${s.social ? `<button class="btn ghost sm" id="src-ag-${i}" data-ag="${i}" aria-label="Gather ${esc(title)} with your agent" title="${chatOn ? "Read this source with your ChatGPT plan" : "Open your agent to run /kaizen gather for this project"}">${ico("refresh")} Gather</button>` : `<button class="btn ghost sm" id="src-chk-${i}" data-chk="${i}" aria-label="Check ${esc(title)} now" title="Check this source now">${ico("refresh")} Check</button>`}<button class="btn ghost danger sm" id="src-rm-${i}" data-rm="${i}" aria-label="Remove ${esc(title)}" title="Remove source">${ico("trash")}</button></div></div><div class="src-url"><a href="${esc(s.url)}" target="_blank" rel="noopener noreferrer" title="${esc(s.url)}"><span>${esc(s.url)}</span>${ico("ext")}</a></div><div class="src-status">${statusHtml}</div><div class="src-dest"><span>Category</span><input class="sel" id="src-ct-${i}" data-ct="${i}" data-k="${esc(s.dir + " " + s.url)}" value="${esc(s.category || "")}" placeholder="e.g. Feedback" maxlength="40" aria-label="Category of ${esc(title)}"><button class="btn sm" id="src-cs-${i}" aria-label="Save the category of ${esc(title)}" disabled>Save</button></div>${st === "sheet" || /github\.com/.test(s.url) ? `<div class="src-dest"><span>Write back</span><label class="hint" style="margin:0"><input type="checkbox" id="src-wb-${i}" data-wb="${i}"${s.writeback ? " checked" : ""}> ${st === "sheet" ? "Put Done in the row's Status column" : "Comment and close the issue"} when its run is done</label></div>` : ""}<div class="src-dest"><span>Project</span><select class="sel" id="src-mv-${i}" data-mv="${i}" data-k="${esc(s.dir + " " + s.url)}" aria-label="Project of ${esc(title)}">${opts(s.dir, s.project)}</select><button class="btn sm" id="src-mb-${i}" aria-label="Move ${esc(title)} to the chosen project" disabled>Move</button></div></div>`;
|
|
166
166
|
})
|
|
167
167
|
.join("");
|
|
168
168
|
const fails = ((d && d.failed) || [])
|
|
@@ -188,7 +188,7 @@ export async function openSources() {
|
|
|
188
188
|
srcKey = (it) => it.dir + " " + it.source;
|
|
189
189
|
// Filters narrow in turn: project (the board's own picker, all projects only), then
|
|
190
190
|
// category, then source; each chip counts what the filters before it leave.
|
|
191
|
-
const catOf = (it) => (it.status === "dismissed" ? "Dismissed" : it.category || "Uncategorised"),
|
|
191
|
+
const catOf = (it) => (it.status === "dismissed" ? "Dismissed" : it.status === "done" ? "Done" : it.category || "Uncategorised"),
|
|
192
192
|
count = (xs, f) => {
|
|
193
193
|
const m = new Map();
|
|
194
194
|
xs.forEach((it) => {
|
|
@@ -215,7 +215,7 @@ export async function openSources() {
|
|
|
215
215
|
if (iceForm && !keys.has(iceForm)) iceForm = null;
|
|
216
216
|
const groups = new Map();
|
|
217
217
|
ice.forEach((it, i) => {
|
|
218
|
-
if (!inProj(it) || (iceCat ? catOf(it) !== iceCat : it.status === "dismissed") || (iceSrc && srcKey(it) !== iceSrc)) return;
|
|
218
|
+
if (!inProj(it) || (iceCat ? catOf(it) !== iceCat : it.status === "dismissed" || it.status === "done") || (iceSrc && srcKey(it) !== iceSrc)) return;
|
|
219
219
|
const k = catOf(it);
|
|
220
220
|
groups.has(k) || groups.set(k, []);
|
|
221
221
|
groups.get(k).push(i);
|
|
@@ -236,7 +236,8 @@ export async function openSources() {
|
|
|
236
236
|
const pick = (id, label, xs) =>
|
|
237
237
|
`<select class="sel ice-sel" id="${id}" aria-label="${label}">${xs.join("")}</select>`;
|
|
238
238
|
const chips =
|
|
239
|
-
|
|
239
|
+
// Dismissed and Done are hidden until picked, so even a lone one is a choice.
|
|
240
|
+
(byCat.size > 1 || byCat.has("Dismissed") || byCat.has("Done")
|
|
240
241
|
? pick("ice-fc", "Category", [
|
|
241
242
|
opt("", !iceCat, "All categories"),
|
|
242
243
|
...[...byCat.keys()].map((k) => opt(k, iceCat === k, k)),
|
|
@@ -248,7 +249,7 @@ export async function openSources() {
|
|
|
248
249
|
...[...bySrc].map(([k, { it }]) => opt(k, iceSrc === k, srcName(it))),
|
|
249
250
|
])
|
|
250
251
|
: "");
|
|
251
|
-
const bar = `<div class="ice-bar">${order.length ? `<input type="checkbox" id="ice-all" aria-label="Select every request shown"${nSel && nSel === order.length ? " checked" : ""}>` : ""}<span class="count">${iceCat === "Dismissed" ? `${order.length}
|
|
252
|
+
const bar = `<div class="ice-bar">${order.length ? `<input type="checkbox" id="ice-all" aria-label="Select every request shown"${nSel && nSel === order.length ? " checked" : ""}>` : ""}<span class="count">${iceCat === "Dismissed" || iceCat === "Done" ? `${order.length} ${iceCat.toLowerCase()}` : `${order.length < waiting ? `${order.length} of ${waiting}` : waiting} waiting`}</span>${chips}<span class="end"></span><button class="btn sm" id="src-go"${list.length ? "" : " disabled"}>${ico("refresh")} Gather all</button><button class="btn ghost sm" id="src-tog" aria-expanded="${srcShow ?? !list.length}" aria-controls="src-sec">${ico("link")} Sources · ${list.length}</button></div>`;
|
|
252
253
|
const bulk = iceSel.size
|
|
253
254
|
? `<div class="ice-bulk" role="toolbar" aria-label="Selected requests"><span>${iceSel.size} selected</span><span class="end"></span><button class="btn sm" id="ice-bp" title="Each becomes its own idea, titled as gathered (p)">${ico("plus")} Promote</button><button class="btn ghost sm" id="ice-bd" title="Dismiss (d)">${ico("x")} Dismiss</button><button class="btn ghost sm" id="ice-bc" title="Clear selection (Esc)">Clear</button></div>`
|
|
254
255
|
: "";
|
|
@@ -277,7 +278,7 @@ export async function openSources() {
|
|
|
277
278
|
k = iceKey(it),
|
|
278
279
|
open = iceForm === k;
|
|
279
280
|
return (
|
|
280
|
-
`<div class="ice-row${iceCur === k ? " cur" : ""}${iceSel.has(k) ? " sel" : ""}" data-i="${i}" data-k="${esc(k)}"><input type="checkbox" id="ice-ck-${i}" data-ck="${i}"${iceSel.has(k) ? " checked" : ""} aria-label="Select: ${esc(it.text)}"><span class="ice-src" title="${esc(srcName(it))}">${fav(it.source, "link", list.find((x) => x.dir === it.dir && x.url === it.source)?.social)}</span><button class="ice-t" id="ice-o-${i}" data-o="${i}" aria-expanded="${open}" title="${esc(it.text)}">${esc(it.text)}</button><span class="ice-meta">${many ? `<span class="nm">${esc(it.project || "")}</span>` : ""}${it.replied ? `<span title="Replied ${esc(ago(it.replied))}">replied</span>` : ""}<span class="ice-age">${ago(it.at)}</span><span class="ice-ra"
|
|
281
|
+
`<div class="ice-row${iceCur === k ? " cur" : ""}${iceSel.has(k) ? " sel" : ""}" data-i="${i}" data-k="${esc(k)}"><input type="checkbox" id="ice-ck-${i}" data-ck="${i}"${iceSel.has(k) ? " checked" : ""} aria-label="Select: ${esc(it.text)}"><span class="ice-src" title="${esc(srcName(it))}">${fav(it.source, "link", list.find((x) => x.dir === it.dir && x.url === it.source)?.social)}</span><button class="ice-t" id="ice-o-${i}" data-o="${i}" aria-expanded="${open}" title="${esc(it.text)}">${esc(it.text)}</button><span class="ice-meta">${many ? `<span class="nm">${esc(it.project || "")}</span>` : ""}${it.status === "done" && it.written ? `<span title="Written back ${esc(ago(it.written))}">written back</span>` : ""}${it.status === "done" && it.writeError ? `<span title="${esc(it.writeError)}">write-back failed</span>` : ""}${it.status === "promoted" ? `<span title="Promoted to Ideas: ${esc(it.idea || "")}">promoted</span>` : ""}${it.replied ? `<span title="Replied ${esc(ago(it.replied))}">replied</span>` : ""}<span class="ice-age">${ago(it.at)}</span><span class="ice-ra">${it.status === "promoted" || it.status === "done" ? "" : `<button class="btn ghost sm" id="ice-pr-${i}" data-pr="${i}" title="Promote to Ideas as gathered (p)" aria-label="Promote to Ideas: ${esc(it.text)}">${ico("plus")}</button>`}${it.status === "promoted" || it.status === "done" ? "" : it.status === "dismissed" ? `<button class="btn ghost sm" id="ice-rs-${i}" data-rs="${i}" title="Restore to waiting" aria-label="Restore: ${esc(it.text)}">Restore</button>` : `<button class="btn ghost sm" id="ice-ds-${i}" data-ds="${i}" title="Dismiss (d)" aria-label="Dismiss: ${esc(it.text)}">${ico("x")}</button>`}</span></span></div>` +
|
|
281
282
|
(open
|
|
282
283
|
? `<div class="ice-open">${it.notes ? `<div class="ice-notes">${esc(it.notes)}</div>` : ""}${(it.images || []).filter((u) => /^https:\/\//.test(u)).length ? `<details class="ice-imgs"><summary>Show images (${it.images.filter((u) => /^https:\/\//.test(u)).length}) — loads them from the source's site</summary>${it.images.filter((u) => /^https:\/\//.test(u)).map((u, n) => { const src = `/ice-image?dir=${encodeURIComponent(it.dir)}&id=${encodeURIComponent(it.id)}&i=${n}`; return `<a href="${esc(src)}" target="_blank" rel="noopener noreferrer"><img data-src="${esc(src)}" alt="" referrerpolicy="no-referrer"></a>`; }).join("")}</details>` : ""}<span class="hint" style="margin:0">From ${esc(srcName(it))} · ${ago(it.at)}${it.post ? ` · <a href="${esc(it.post)}" target="_blank" rel="noopener">open post</a>` : ""}</span>${replyBox(it)}${form(it)}</div>`
|
|
283
284
|
: "")
|
|
@@ -293,7 +294,7 @@ export async function openSources() {
|
|
|
293
294
|
// are none until closed. Slid in only when opened; a rebuild while open shows it in place.
|
|
294
295
|
const drawer = srcShow ?? !list.length,
|
|
295
296
|
slide = drawer && !$("src-sec")?.classList.contains("open");
|
|
296
|
-
const sec = `<div class="src-scrim${drawer && !slide ? " on" : ""}" id="src-scrim"></div><aside class="src-sec${drawer && !slide ? " open" : ""}" id="src-sec" aria-label="Sources"${drawer ? "" : " inert"}><header><div class="t"><b>Sources</b><div class="sub">${d && d.every && d.every !== "off" ? `Auto-check every ${esc(d.every)}` : "Auto-check off"} · <a id="src-to-settings" role="button" tabindex="0">Settings</a> · ${G.connected ? `ChatGPT${G.email ? " " + esc(G.email) : ""} <a id="cg-out" role="button" tabindex="0">Disconnect</a>` : `<a id="cg-in" role="button" tabindex="0">Connect ChatGPT</a>`}</div></div><div class="src-hda">${list.length ? `<button class="btn sm" id="src-new" aria-expanded="${addOn}" aria-controls="src-addbox">${ico("plus")} Add</button>` : ""}<button class="ib" id="src-x" aria-label="Close sources" title="Close">${ico("close")}</button></div></header><div class="body">${addBox}${list.length ? `<div class="src-list">${cards}</div>` : emptyState}</div></aside>`;
|
|
297
|
+
const sec = `<div class="src-scrim${drawer && !slide ? " on" : ""}" id="src-scrim"></div><aside class="src-sec${drawer && !slide ? " open" : ""}" id="src-sec" aria-label="Sources"${drawer ? "" : " inert"}><header><div class="t"><b>Sources</b><div class="sub">${d && d.every && d.every !== "off" ? `Auto-check every ${esc(d.every)}` : "Auto-check off"} · <a id="src-to-settings" role="button" tabindex="0">Settings</a> · ${G.connected ? `ChatGPT${G.email ? " " + esc(G.email) : ""} <a id="cg-out" role="button" tabindex="0">Disconnect</a>` : `<a id="cg-in" role="button" tabindex="0">Connect ChatGPT</a>`} · ${(d.google || {}).connected ? `Google <a id="gg-out" role="button" tabindex="0">Disconnect</a>` : `<a id="gg-in" role="button" tabindex="0">Connect Google</a>`}</div></div><div class="src-hda">${list.length ? `<button class="btn sm" id="src-new" aria-expanded="${addOn}" aria-controls="src-addbox">${ico("plus")} Add</button>` : ""}<button class="ib" id="src-x" aria-label="Close sources" title="Close">${ico("close")}</button></div></header><div class="body">${addBox}${list.length ? `<div class="src-list">${cards}</div>` : emptyState}</div></aside>`;
|
|
297
298
|
// First visit: offer the ChatGPT plan for social sources, or keep the agent.
|
|
298
299
|
const onb =
|
|
299
300
|
G.use === "ask" && !G.connected
|
|
@@ -335,6 +336,32 @@ export async function openSources() {
|
|
|
335
336
|
}
|
|
336
337
|
};
|
|
337
338
|
for (const id of ["cg-go", "cg-in"]) if ($(id)) $(id).onclick = connect;
|
|
339
|
+
// Google needs the user's own OAuth client (Desktop app, Sheets API on); asked once.
|
|
340
|
+
if ($("gg-in"))
|
|
341
|
+
$("gg-in").onclick = async () => {
|
|
342
|
+
if (!(d.google || {}).hasClient) {
|
|
343
|
+
toast("Add your Google client in Settings first");
|
|
344
|
+
store.setQ = "google";
|
|
345
|
+
go("settings", true);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const w = window.open("", "_blank");
|
|
349
|
+
const j = await post("/google", { op: "connect" });
|
|
350
|
+
if (j.ok && w) w.location = j.url;
|
|
351
|
+
else {
|
|
352
|
+
w?.close();
|
|
353
|
+
if (j.ok) location.href = j.url;
|
|
354
|
+
else toast(j.error || "Could not start Google sign-in");
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
if ($("gg-out"))
|
|
358
|
+
$("gg-out").onclick = async () => {
|
|
359
|
+
if (!confirm("Disconnect Google? Done requests stop being written to sheets.")) return;
|
|
360
|
+
if ((await post("/google", { op: "disconnect" })).ok) {
|
|
361
|
+
toast("Google disconnected");
|
|
362
|
+
openSources();
|
|
363
|
+
}
|
|
364
|
+
};
|
|
338
365
|
if ($("cg-skip"))
|
|
339
366
|
$("cg-skip").onclick = async () => {
|
|
340
367
|
if ((await post("/chatgpt", { op: "skip" })).ok) {
|
|
@@ -433,6 +460,25 @@ export async function openSources() {
|
|
|
433
460
|
toast(`Source moved. Requests already gathered stay in ${s.project || "the old project"}.`),
|
|
434
461
|
);
|
|
435
462
|
});
|
|
463
|
+
$("pbody")
|
|
464
|
+
.querySelectorAll("[data-wb]")
|
|
465
|
+
.forEach((m) => {
|
|
466
|
+
const s = list[+m.dataset.wb];
|
|
467
|
+
m.onchange = () => {
|
|
468
|
+
if (m.checked && s.social === undefined && /docs\.google\.com/.test(s.url) && !(d.google || {}).connected) {
|
|
469
|
+
alert("Connect Google first (Sources header), so kaizen can write to the sheet.");
|
|
470
|
+
m.checked = false;
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
if (m.checked && !confirm("When a promoted request's run is done, kaizen will write that back to this source. A closed issue or a changed cell is visible to others. Turn on?")) {
|
|
474
|
+
m.checked = false;
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
act({ op: "writeback", dir: s.dir, url: s.url, on: m.checked }, () =>
|
|
478
|
+
toast(m.checked ? "Done requests will be written back" : "Write back off"),
|
|
479
|
+
);
|
|
480
|
+
};
|
|
481
|
+
});
|
|
436
482
|
// The category saves on its button or Enter, never per keystroke.
|
|
437
483
|
$("pbody")
|
|
438
484
|
.querySelectorAll("[data-ct]")
|
package/web/board/panel.ts
CHANGED
|
@@ -138,6 +138,7 @@ export async function openRun(c, refresh, again) {
|
|
|
138
138
|
pt.hidden = !c.pty;
|
|
139
139
|
pt.classList.toggle("on", store.tab === "term");
|
|
140
140
|
pt.setAttribute("aria-pressed", store.tab === "term");
|
|
141
|
+
pt.title = pt.ariaLabel = store.tab === "term" ? "Close terminal" : "Terminal";
|
|
141
142
|
pt.onclick = () => {
|
|
142
143
|
store.tab = store.tab === "term" ? store.lastTab : "term";
|
|
143
144
|
openRun(c, true);
|
package/web/board/settings.ts
CHANGED
|
@@ -40,6 +40,9 @@ export async function openSettings() {
|
|
|
40
40
|
const cfg = await fetch("/settings")
|
|
41
41
|
.then((r) => r.json())
|
|
42
42
|
.catch(() => null);
|
|
43
|
+
const gg = await fetch("/google")
|
|
44
|
+
.then((r) => r.json())
|
|
45
|
+
.catch(() => ({}));
|
|
43
46
|
if (gen !== store.pageGen) return;
|
|
44
47
|
$("psub").textContent = "Theme applies to this browser. Everything else is saved to your kaizen settings.";
|
|
45
48
|
$("psub").title = cfg ? cfg.file : "";
|
|
@@ -69,7 +72,7 @@ export async function openSettings() {
|
|
|
69
72
|
const ntHelp =
|
|
70
73
|
"A desktop notification when a run starts waiting on you." +
|
|
71
74
|
(np ? (np === "denied" ? " Blocked in this browser's site settings." : "") : " Not supported in this browser.");
|
|
72
|
-
const names = [...(cfg ? groups(cfg.rows).map(([g]) => g) : []), "Browser"];
|
|
75
|
+
const names = [...(cfg ? groups(cfg.rows).map(([g]) => g) : []), "Google", "Browser"];
|
|
73
76
|
$("pbody").innerHTML =
|
|
74
77
|
`<div class="sq"><input class="sel" id="sq" type="search" placeholder="Search settings" aria-label="Search settings" spellcheck="false" autocomplete="off" value="${esc(store.setQ)}"><div class="sjump" role="group" aria-label="Jump to section">${names.map((g) => `<button data-g="${esc(g)}">${esc(g)}</button>`).join("")}</div></div>
|
|
75
78
|
${
|
|
@@ -97,6 +100,9 @@ export async function openSettings() {
|
|
|
97
100
|
.join("")
|
|
98
101
|
: `<p class="snone">Settings could not be read. Run <code>kaizen settings</code>.</p>`
|
|
99
102
|
}
|
|
103
|
+
<h3 class="sg">Google</h3>${row("OAuth client id", `<input class="sel" id="gg-id" placeholder="${gg.hasClient ? "saved; type to replace" : "from Google Cloud, Desktop app"}" spellcheck="false" autocomplete="off">`, "Kaizen writes Done into a sheet through your own Google client: create an OAuth client of type Desktop app in Google Cloud and enable the Sheets API. Nothing is shared with other installs.", "gg-id", "google oauth client id sheets write back")}
|
|
104
|
+
${row("OAuth client secret", `<span class="fpick"><input class="sel" id="gg-secret" type="password" placeholder="${gg.hasClient ? "saved; type to replace" : ""}" spellcheck="false" autocomplete="off"><button class="btn" id="gg-save" type="button">Save</button></span>`, "Kept in ~/.kaizen/google.json, readable only by you. Saving a new client signs Google out.", "gg-secret", "google oauth client secret")}
|
|
105
|
+
${row("Google account", `<button class="btn${gg.connected ? "" : " pri"}" id="gg-conn" type="button"${gg.hasClient || gg.connected ? "" : " disabled"}>${gg.connected ? "Disconnect" : "Connect"}</button>`, gg.connected ? "Connected. Sources with Write back on put Done in the sheet row once their run is done." : "Connect after saving the client. Needed only for Write back on sheet sources.", "gg-conn", "google account connect sheets write back")}
|
|
100
106
|
<h3 class="sg">Browser</h3>${row("Theme", ctl("th", ["system", "light", "dark"], cur), "Light or dark for this browser only; system follows your device.", "th", "theme light dark system browser")}
|
|
101
107
|
${row("Notifications", `<button class="chk${np === "granted" ? " on" : ""}" id="nt" role="switch" aria-checked="${np === "granted"}"${np && np !== "denied" ? "" : " disabled"}></button>`, ntHelp, "nt", "notifications " + ntHelp + " browser")}
|
|
102
108
|
<p class="snone" id="snone" hidden></p>`;
|
|
@@ -156,6 +162,25 @@ export async function openSettings() {
|
|
|
156
162
|
f.focus();
|
|
157
163
|
f.setSelectionRange(f.value.length, f.value.length);
|
|
158
164
|
}
|
|
165
|
+
$("gg-save").onclick = async () => {
|
|
166
|
+
const j = await post("/google", { op: "client", id: $("gg-id").value, secret: $("gg-secret").value });
|
|
167
|
+
if (j.ok) toast("Saved");
|
|
168
|
+
else toast(j.error || "Enter both the client id and the secret");
|
|
169
|
+
openSettings();
|
|
170
|
+
};
|
|
171
|
+
$("gg-conn").onclick = async () => {
|
|
172
|
+
if (gg.connected) {
|
|
173
|
+
await post("/google", { op: "disconnect" });
|
|
174
|
+
return openSettings();
|
|
175
|
+
}
|
|
176
|
+
const w = window.open("", "_blank");
|
|
177
|
+
const j = await post("/google", { op: "connect" });
|
|
178
|
+
if (j.ok && w) w.location = j.url;
|
|
179
|
+
else {
|
|
180
|
+
w?.close();
|
|
181
|
+
toast(j.error || "Could not start Google sign-in");
|
|
182
|
+
}
|
|
183
|
+
};
|
|
159
184
|
$("th").onchange = (e) => setTheme(e.target.value);
|
|
160
185
|
$("nt").onclick = () => {
|
|
161
186
|
askNotify();
|
package/web/board.html
CHANGED
|
@@ -189,7 +189,7 @@ section h2 .clear{margin-left:auto;min-height:22px;padding:0 8px}
|
|
|
189
189
|
.notes{display:flex;flex-direction:column;gap:8px}
|
|
190
190
|
#pbody img{max-width:100%;border-radius:var(--r-s)}
|
|
191
191
|
/* the in-card agent: a wider panel, and the terminal filling its body */
|
|
192
|
-
aside.wide{width:
|
|
192
|
+
aside.wide{width:100%;border-left:0}#pbody:has(.term){display:flex;flex-direction:column}#pbody .term{flex:1;min-height:0;min-height:320px;background:var(--bg);border-radius:var(--r-s)}#pbody .term .xterm{height:100%;padding:4px}#pbody .term .xterm-viewport{background:transparent!important}.termkeys{display:grid;grid-template-columns:repeat(6,1fr);gap:8px;padding:10px 0 6px}.termkeys button{height:56px;padding:0;border:1px solid var(--line);border-bottom-width:4px;border-radius:10px;background:var(--bg-2);color:var(--ink-hi);font:600 18px var(--mono);touch-action:manipulation}.termkeys button:active{transform:translateY(2px);border-bottom-width:2px;background:var(--hover)}.termkeys button.on{color:var(--on-accent);background:var(--accent);border-color:var(--accent)}
|
|
193
193
|
.desc{border-bottom:1px solid var(--line);padding-bottom:6px;margin-bottom:14px}.desc h3{margin-top:14px}
|
|
194
194
|
.chat{display:flex;flex-direction:column;gap:10px;margin:0 0 14px}.msg{border:1px solid var(--line);border-radius:var(--r);padding:10px 12px;background:var(--bg)}.msg time{display:block;font-size:11px;color:var(--dim);margin-bottom:4px}.msg p:last-child{margin-bottom:0}.msg img{max-width:100%;max-height:260px;border-radius:var(--r-s);display:block;margin:4px 0}
|
|
195
195
|
.notes .tools{display:flex;gap:6px;align-items:center}.notes .tools span{flex:1;font-size:11px;color:var(--dim)}.notes .att{font-size:11px;color:var(--dim)}
|