kaizen-agent 1.39.0 → 1.40.0

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.
@@ -222,22 +222,22 @@ export async function openSources() {
222
222
  : `<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>`;
223
223
  const kind = st === "sheet" ? "sheet" : st === "doc" ? "doc" : st === "social" ? "at" : "link",
224
224
  bare = s.url.replace(/^https?:\/\/(www\.)?/, "");
225
- return `<div class="src-card" id="src-card-${i}"><div class="src-hd"><span class="src-ico" title="${st === "social" ? esc(PLAT[s.social] || "social") : st}">${fav(s.url, kind, s.social)}</span><div class="src-info"><span class="src-title" title="${esc(title)}">${esc(title)}</span><a class="src-url" href="${esc(s.url)}" target="_blank" rel="noopener noreferrer" title="${esc(s.url)}"><span>${esc(bare)}</span>${ico("ext")}</a></div><div class="src-acts">${s.social ? `<button class="ib sm" id="src-ag-${i}" data-ag="${i}" aria-label="Gather ${esc(title)} with your agent" title="${chatOn ? "Gather with your ChatGPT plan" : "Gather: opens your agent on /kaizen gather"}">${ico("refresh")}</button>` : `<button class="ib sm" id="src-chk-${i}" data-chk="${i}" aria-label="Check ${esc(title)} now" title="Check now">${ico("refresh")}</button>`}<button class="ib sm danger" id="src-rm-${i}" data-rm="${i}" aria-label="Remove ${esc(title)}" title="Remove source">${ico("trash")}</button></div></div><div class="src-status">${statusHtml}</div><div class="src-fields"><label class="src-f" title="Category"><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="${esc(platOf(s))}" maxlength="40" aria-label="Category of ${esc(title)}"><button class="btn sm pri" id="src-cs-${i}" aria-label="Save the category of ${esc(title)}" disabled>Save</button></label><label class="src-f" title="Project"><span>Project</span>${plogo(s.dir, s.project || "")}<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 pri" id="src-mb-${i}" aria-label="Move ${esc(title)} to the chosen project" disabled>Move</button></label>${st === "sheet" || /github\.com/.test(s.url) ? `<label class="src-wb"><input type="checkbox" id="src-wb-${i}" data-wb="${i}"${s.writeback ? " checked" : ""}> ${st === "sheet" ? "Mark the row Done" : "Comment and close the issue"} when its run is done</label>` : ""}</div></div>`;
225
+ return `<div class="src-card" id="src-card-${i}"><div class="src-hd"><span class="src-ico" title="${st === "social" ? esc(PLAT[s.social] || "social") : st}">${fav(s.url, kind, s.social)}</span><div class="src-info"><span class="src-title" title="${esc(title)}">${esc(title)}</span><a class="src-url" href="${esc(s.url)}" target="_blank" rel="noopener noreferrer" title="${esc(s.url)}"><span>${esc(bare)}</span>${ico("ext")}</a></div><div class="src-acts">${s.social ? `<button class="ib sm" id="src-ag-${i}" data-ag="${i}" aria-label="Gather ${esc(title)} with your agent" title="${chatOn ? "Gather with your ChatGPT plan" : "Gather: opens your agent on /kaizen gather"}">${ico("refresh")}</button>` : `<button class="ib sm" id="src-chk-${i}" data-chk="${i}" aria-label="Check ${esc(title)} now" title="Check now">${ico("refresh")}</button>`}<button class="ib sm danger" id="src-rm-${i}" data-rm="${i}" aria-label="Remove ${esc(title)}" title="Remove source">${ico("trash")}</button></div></div><div class="src-status">${statusHtml}</div><div class="src-fields"><label class="src-f" title="Category"><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="${esc(platOf(s))}" maxlength="40" aria-label="Category of ${esc(title)}"><button class="btn sm pri" id="src-cs-${i}" aria-label="Save the category of ${esc(title)}" disabled>Save</button></label><label class="src-f" title="Project"><span>Project</span>${plogo(s.dir, s.project || "")}<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 pri" id="src-mb-${i}" aria-label="Move ${esc(title)} to the chosen project" disabled>Move</button></label>${st === "sheet" || /github\.com/.test(s.url) ? `<label class="src-wb"><input type="checkbox" id="src-wb-${i}" data-wb="${i}"${s.writeback ? " checked" : ""}> Write back</label>` : ""}</div></div>`;
226
226
  })
227
227
  .join("");
228
228
  const fails = ((d && d.failed) || [])
229
229
  .map((f) => `<p style="color:var(--dim)">The sources of ${esc(f)} could not be read.</p>`)
230
230
  .join("");
231
- const emptyState = `<div class="src-empty"><div class="src-empty-ico">${ico("link")}</div><h4>No request sources yet</h4><p>Connect shared links to gather requests into the icebox, where you choose which become ideas. Nothing runs automatically until you start it.</p><div class="src-empty-grid"><div class="src-empty-item"><strong>Google Sheets</strong>Public sheets with idea or task rows. Rows marked Done are skipped.</div><div class="src-empty-item"><strong>Google Docs</strong>Shared public docs with items or notes written line by line.</div><div class="src-empty-item"><strong>Public links</strong>TSV, CSV, or raw text links containing requests.</div><div class="src-empty-item"><strong>Social media</strong>A keyword, #hashtag or @account on X, TikTok, Instagram or Threads, read by your agent.</div></div></div>`;
231
+ const emptyState = `<div class="src-empty"><div class="src-empty-ico">${ico("link")}</div><h4>No sources yet</h4></div>`;
232
232
  const social = srcKind === "social";
233
233
  const addOn = !list.length || srcAdd || [typed[0], typed[1], typed[4], typed[6]].some(Boolean);
234
- const addBox = `<div class="src-add" id="src-addbox"${addOn ? "" : " hidden"}><div class="hd"><h4>Add a source</h4><div class="src-kind" role="group" aria-label="Kind of source"><button id="src-k-link" data-kind="link" aria-pressed="${!social}">Link</button><button id="src-k-social" data-kind="social" aria-pressed="${social}">Social</button></div></div><label for="src-url"${social ? " hidden" : ""}><span>Link</span><input class="sel" id="src-url" placeholder="https://docs.google.com/spreadsheets/d/… or any public link" spellcheck="false" autocomplete="off"></label><div class="two plat"${social ? "" : " hidden"}><label for="src-plat"><span>Platform</span><select class="sel" id="src-plat">${Object.entries(
234
+ const addBox = `<div class="src-add" id="src-addbox"${addOn ? "" : " hidden"}><div class="hd"><h4>Add a source</h4><div class="src-kind" role="group" aria-label="Kind of source"><button id="src-k-link" data-kind="link" aria-pressed="${!social}">Link</button><button id="src-k-social" data-kind="social" aria-pressed="${social}">Social</button></div></div><label for="src-url"${social ? " hidden" : ""}><span>Link</span><input class="sel" id="src-url" placeholder="Link" spellcheck="false" autocomplete="off"></label><div class="two plat"${social ? "" : " hidden"}><label for="src-plat"><span>Platform</span><select class="sel" id="src-plat">${Object.entries(
235
235
  PLAT,
236
236
  )
237
237
  .map(([k, v]) => `<option value="${k}">${v}</option>`)
238
238
  .join(
239
239
  "",
240
- )}</select></label><label for="src-q"><span>Follow</span><input class="sel" id="src-q" placeholder="keyword, #hashtag or @account" maxlength="100" spellcheck="false" autocomplete="off"></label></div><div class="two"><label for="src-label"><span>Label <i>optional</i></span><input class="sel" id="src-label" placeholder="${social ? "e.g. Mentions on X" : "e.g. Feedback Q4"}"></label><label for="src-cat"><span>Category <i>optional</i></span><input class="sel" id="src-cat" placeholder="${social ? "e.g. Social" : "e.g. Bugs"}" maxlength="40"></label></div><label for="src-dir"><span>Project</span><select class="sel" id="src-dir">${dir ? "" : `<option value="">Choose project…</option>`}${opts(dir)}</select></label><div class="acts"><span class="hint">${social ? (chatOn ? "Your ChatGPT plan reads public posts when you press Gather on the source." : "Kaizen signs in nowhere. Your agent reads public posts when you press Gather on the source.") : "Public links only. For private docs, run /kaizen gather."}</span><button class="btn pri" id="src-add">Add source</button></div></div>`;
240
+ )}</select></label><label for="src-q"><span>Follow</span><input class="sel" id="src-q" placeholder="keyword, #hashtag or @account" maxlength="100" spellcheck="false" autocomplete="off"></label></div><div class="two"><label for="src-label"><span>Label <i>optional</i></span><input class="sel" id="src-label" placeholder="${social ? "e.g. Mentions on X" : "e.g. Feedback Q4"}"></label><label for="src-cat"><span>Category <i>optional</i></span><input class="sel" id="src-cat" placeholder="${social ? "e.g. Social" : "e.g. Bugs"}" maxlength="40"></label></div><label for="src-dir"><span>Project</span><select class="sel" id="src-dir">${dir ? "" : `<option value="">Choose project…</option>`}${opts(dir)}</select></label><div class="acts"><span class="end"></span><button class="btn pri" id="src-add">Add source</button></div></div>`;
241
241
  // The inbox: one row per request, by project when showing all, then by category,
242
242
  // narrowed to one source by its chip. Checked, open and cursor items are kept by key.
243
243
  const srcName = (it) => {
@@ -340,21 +340,21 @@ export async function openSources() {
340
340
  .join(""),
341
341
  )
342
342
  : "");
343
- 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>`;
343
+ 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><span class="end"></span>${chips ? `<details class="ice-filt"><summary class="ib${iceCat || iceSrc ? " on" : ""}" aria-label="Filter requests" title="Filter requests">${ico("filter")}</summary><div class="ice-fpop">${chips}</div></details>` : ""}<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>`;
344
344
  const bulk = iceSel.size
345
- ? `<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>`
345
+ ? `<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="Promote (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>`
346
346
  : "";
347
347
  // The open item's form: a clean title for the idea's card and the person's own details;
348
348
  // what was gathered is added to the idea's notes by the server.
349
349
  const form = (it) =>
350
- `<div class="ice-form"><label for="ice-title" class="hint" style="margin:0">Idea title</label><input class="sel" id="ice-title" data-k="${esc(iceKey(it))}" maxlength="200" value="${esc(it.text)}" aria-label="Idea title"><label for="ice-details" class="hint" style="margin:0">Details</label><textarea class="sel" id="ice-details" rows="3" placeholder="What, for whom, why" aria-label="Details"></textarea><div class="ice-form-acts"><span class="hint">Source goes into the notes.</span><button class="btn ghost sm" id="ice-cancel">Cancel</button><button class="btn pri sm" id="ice-ok">Promote</button></div></div>`;
350
+ `<div class="ice-form"><label for="ice-title" class="hint" style="margin:0">Idea title</label><input class="sel" id="ice-title" data-k="${esc(iceKey(it))}" maxlength="200" value="${esc(it.text)}" aria-label="Idea title"><label for="ice-details" class="hint" style="margin:0">Details</label><textarea class="sel" id="ice-details" rows="3" aria-label="Details"></textarea><div class="ice-form-acts"><span class="end"></span><button class="btn ghost sm" id="ice-cancel">Cancel</button><button class="btn pri sm" id="ice-ok">Promote</button></div></div>`;
351
351
  // A gathered post can be answered: Reply drafts, the box is the approval, Send reply posts that text.
352
352
  const replyBox = (it) => {
353
353
  if (!it.post) return "";
354
354
  if (it.replied) return `<span class="hint" style="margin:0">You replied to this post ${ago(it.replied)}.</span>`;
355
355
  const r = iceReply && iceReply.k === iceKey(it) ? iceReply : null;
356
356
  const style = (d) =>
357
- `<select class="sel" id="ice-rst" aria-label="Reply style" title="Promo names kaizen; Helpful may mention it when it fits; Casual and Fun never do"${d ? " disabled" : ""}>${Object.entries(
357
+ `<select class="sel" id="ice-rst" aria-label="Reply style"${d ? " disabled" : ""}>${Object.entries(
358
358
  STYLES,
359
359
  )
360
360
  .map(([v, l]) => `<option value="${v}"${v === rStyle(it.dir) ? " selected" : ""}>${l}</option>`)
@@ -377,10 +377,10 @@ export async function openSources() {
377
377
  };
378
378
  const inbox =
379
379
  waiting && !order.length
380
- ? `<div class="ice-zero"><h4>Nothing matches</h4><p>${waiting} waiting outside these filters.</p><p style="margin-top:10px"><button class="btn sm" id="ice-unf">Clear filters</button></p></div>`
380
+ ? `<div class="ice-zero"><h4>No matches</h4><p style="margin-top:10px"><button class="btn sm" id="ice-unf">Clear filters</button></p></div>`
381
381
  : order.length
382
382
  ? `<div class="ice-list">${[...groups].map(([g, ix]) => `<div class="ice-cat">${esc(g)}<i>${ix.length}</i></div>${ix.map(row).join("")}`).join("")}</div>`
383
- : `<div class="ice-zero"><h4>Inbox zero</h4><p>Nothing waiting. Gather all checks your sources for new requests; /kaizen discover in your agent sorts what comes in.</p></div>`;
383
+ : `<div class="ice-zero"><h4>Nothing waiting</h4></div>`;
384
384
  // The sources, in the right-hand drawer: shut until asked for, open by itself while there
385
385
  // are none until closed. Slid in only when opened; a rebuild while open shows it in place.
386
386
  const drawer = srcShow ?? !list.length,
@@ -389,7 +389,7 @@ export async function openSources() {
389
389
  // First visit: offer the ChatGPT plan for social sources, or keep the agent.
390
390
  const onb =
391
391
  G.use === "ask" && !G.connected
392
- ? `<div class="cg-onb"><b>Gather social posts with your ChatGPT plan</b><p>Connect ChatGPT Plus or Pro and Gather reads X, TikTok, Instagram and Threads sources right here, counted against your plan, with no agent to open. Skip it and Gather keeps using your agent. You can change this in Settings.</p><div class="acts"><button class="btn ghost" id="cg-skip">Skip for now</button><button class="btn pri" id="cg-go">Continue with ChatGPT</button></div></div>`
392
+ ? `<div class="cg-onb"><div class="acts"><b>Gather social posts with ChatGPT?</b><span class="end"></span><button class="btn ghost" id="cg-skip">Skip</button><button class="btn pri" id="cg-go">Connect</button></div></div>`
393
393
  : "";
394
394
  // A rebuild replaces the drawer: keep where it was scrolled to.
395
395
  const at = $("src-sec")?.querySelector(".body")?.scrollTop ?? 0;
@@ -460,7 +460,6 @@ export async function openSources() {
460
460
  if ($("cg-skip"))
461
461
  $("cg-skip").onclick = async () => {
462
462
  if ((await post("/chatgpt", { op: "skip" })).ok) {
463
- toast("Gather keeps using your agent");
464
463
  openSources();
465
464
  }
466
465
  };
@@ -550,7 +549,7 @@ export async function openSources() {
550
549
  })();
551
550
  b.onclick = () =>
552
551
  act({ op: "move", dir: s.dir, to: m.value, url: s.url }, () =>
553
- toast(`Source moved. Requests already gathered stay in ${s.project || "the old project"}.`),
552
+ toast("Source moved"),
554
553
  );
555
554
  });
556
555
  $("pbody")
@@ -568,7 +567,7 @@ export async function openSources() {
568
567
  return;
569
568
  }
570
569
  act({ op: "writeback", dir: s.dir, url: s.url, on: m.checked }, () =>
571
- toast(m.checked ? "Done requests will be written back" : "Write back off"),
570
+ toast(m.checked ? "Write back on" : "Write back off"),
572
571
  );
573
572
  };
574
573
  });
@@ -967,7 +966,7 @@ export async function openSources() {
967
966
  );
968
967
  if (j.ok && social) {
969
968
  $("src-q").value = $("src-label").value = $("src-cat").value = "";
970
- toast("Source added. Click Gather on it to have your agent read its posts.");
969
+ toast("Source added");
971
970
  load();
972
971
  } else if (j.ok) {
973
972
  $("src-url").value = $("src-label").value = $("src-cat").value = "";
@@ -44,8 +44,8 @@ export async function openSettings() {
44
44
  .then((r) => r.json())
45
45
  .catch(() => ({}));
46
46
  if (gen !== store.pageGen) return;
47
- $("psub").textContent = "Theme applies to this browser. Everything else is saved to your kaizen settings.";
48
- $("psub").title = cfg ? cfg.file : "";
47
+ $("psub").textContent = "";
48
+ $("psub").title = "";
49
49
  // One control per row: a switch for on/off, one segment per value for a small number,
50
50
  // a select for a choice. The row's label names the control; its help sits under it.
51
51
  const ctl = (id, vals, on) => {
@@ -64,15 +64,12 @@ export async function openSettings() {
64
64
  return `<select class="sel" id="${id}">${vals.map((v) => `<option value="${esc(v)}"${v === on ? " selected" : ""}>${v === "" ? "—" : esc(v)}</option>`).join("")}</select>`;
65
65
  };
66
66
  const row = (label, control, hint, id, q) =>
67
- `<div class="srow" data-q="${esc(q.toLowerCase())}"><label for="${id}">${esc(label)}</label>${control}<p class="hint">${hint}</p></div>`;
67
+ `<div class="srow" data-q="${esc(q.toLowerCase())}"><label for="${id}">${esc(label)}</label>${control}${hint ? `<p class="hint">${hint}</p>` : ""}</div>`;
68
68
  const hadFocus = document.activeElement?.id === "sq";
69
69
  const np = typeof Notification === "undefined" ? null : Notification.permission;
70
- const ntHelp =
71
- "A desktop notification when a run starts waiting on you." +
72
- (np ? (np === "denied" ? " Blocked in this browser's site settings." : "") : " Not supported in this browser.");
73
- const names = [...(cfg ? groups(cfg.rows).map(([g]) => g) : []), "Google", "Browser"];
70
+ const ntHelp = np ? (np === "denied" ? "Blocked by this browser" : "") : "Not supported in this browser";
74
71
  $("pbody").innerHTML =
75
- `<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>
72
+ `<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>
76
73
  ${
77
74
  cfg
78
75
  ? groups(cfg.rows)
@@ -98,47 +95,30 @@ export async function openSettings() {
98
95
  .join("")
99
96
  : `<p class="snone">Settings could not be read. Run <code>kaizen settings</code>.</p>`
100
97
  }
101
- <h3 class="sg">Google</h3><div class="sgrp">${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")}
102
- ${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")}
103
- ${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")}</div>
104
- <h3 class="sg">Browser</h3><div class="sgrp">${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")}
98
+ <h3 class="sg">Google</h3><div class="sgrp">${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">`, "", "gg-id", "google oauth client id sheets write back")}
99
+ ${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>`, "", "gg-secret", "google oauth client secret")}
100
+ ${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-conn", "google account connect sheets write back")}</div>
101
+ <h3 class="sg">Browser</h3><div class="sgrp">${row("Theme", ctl("th", ["system", "light", "dark"], cur), "", "th", "theme light dark system browser")}
105
102
  ${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")}</div>
106
103
  <p class="snone" id="snone" hidden></p>`;
107
104
  // Hides rows that do not match, then any group (and its chip) left with none.
108
105
  updBar();
109
- const heads = [...$("pbody").querySelectorAll(".sg")],
110
- chips = [...$("pbody").querySelectorAll(".sjump button")];
106
+ const heads = [...$("pbody").querySelectorAll(".sg")];
111
107
  const filter = () => {
112
108
  const q = store.setQ.trim().toLowerCase();
113
109
  let any = false;
114
- heads.forEach((h, i) => {
110
+ heads.forEach((h) => {
115
111
  let vis = false;
116
112
  for (const n of h.nextElementSibling.children) {
117
113
  n.hidden = !!q && !n.dataset.q.includes(q);
118
114
  vis ||= !n.hidden;
119
115
  }
120
116
  h.hidden = !vis;
121
- if (chips[i]) chips[i].hidden = !vis;
122
117
  any ||= vis;
123
118
  });
124
119
  $("snone").hidden = any;
125
120
  $("snone").textContent = `No setting matches “${store.setQ.trim()}”.`;
126
121
  };
127
- // Scrolled by measure: the pinned bar's height changes as the chips wrap.
128
- chips.forEach(
129
- (b, i) =>
130
- (b.onclick = () => {
131
- const pb = $("pbody");
132
- pb.scrollTo({
133
- top:
134
- pb.scrollTop +
135
- heads[i].getBoundingClientRect().top -
136
- pb.querySelector(".sq").getBoundingClientRect().bottom -
137
- 8,
138
- behavior: "smooth",
139
- });
140
- }),
141
- );
142
122
  $("sq").oninput = (e) => {
143
123
  store.setQ = e.target.value;
144
124
  filter();
@@ -21,7 +21,7 @@ export function updBar() {
21
21
  const d = document.createElement("div");
22
22
  d.className = `updbar${upd ? "" : " quiet"}`;
23
23
  if (!upd) {
24
- d.innerHTML = `<span id="upd-msg" aria-live="polite">${ver ? `You are running kaizen ${esc(ver.version)}.` : "kaizen"}</span><button class="btn" id="upd-chk">Check for updates</button>`;
24
+ d.innerHTML = `<span id="upd-msg" aria-live="polite">${ver ? `kaizen ${esc(ver.version)}` : "kaizen"}</span><button class="btn" id="upd-chk">Check for updates</button>`;
25
25
  } else {
26
26
  // What's new: each newer release's one-line title, shown on hover, focus or click.
27
27
  const notes = (upd.notes || []).map((n) => `<li><b>${esc(n.v)}</b> ${esc(n.text)}</li>`).join("");
package/web/board.html CHANGED
@@ -184,7 +184,7 @@ section li.hint{display:block;padding:14px;border:1px dashed var(--line);border-
184
184
  .c-marks{display:flex;align-items:center;gap:6px;color:var(--dim);margin-left:auto}
185
185
  .mk{display:grid;place-items:center;font-size:11px;font-weight:500}
186
186
  .mk svg{width:13px;height:13px;stroke-width:1.7}
187
- .mk.accent{color:var(--accent)}.mk.ok{color:var(--ok)}.mk.warn{color:var(--warn)}.mk.run{color:var(--run)}
187
+ .mk.accent{color:var(--accent)}.mk.ok{color:var(--ok)}.mk.warn{color:var(--warn)}.mk.run{color:var(--run)}.mk.ask{animation:askp 1.6s ease-in-out infinite}@keyframes askp{50%{opacity:.35}}@media (prefers-reduced-motion:reduce){.mk.ask{animation:none}}
188
188
  .chip.proj{color:var(--ink);background:color-mix(in srgb,var(--dim) 12%,transparent);box-shadow:none;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:inline-block;overflow-wrap:normal}
189
189
  .chip{display:inline-flex;align-items:center;gap:5px;min-height:20px;padding:1px 8px;border-radius:11px;overflow-wrap:anywhere;font-size:11px;font-weight:500;color:var(--dim);box-shadow:inset 0 0 0 1px var(--line)}
190
190
  .chip i{width:6px;height:6px;border-radius:50%;background:var(--dim)}
@@ -307,7 +307,7 @@ aside .foot .more .menu-pop{left:0;right:auto;top:auto;bottom:calc(100% + 4px)}.
307
307
  .sgrp{background:color-mix(in srgb,var(--dim) 9%,var(--bg-2));border-radius:var(--r);overflow:hidden}
308
308
  .srow{display:grid;grid-template-columns:1fr auto;gap:2px 16px;padding:11px 16px;min-height:44px}
309
309
  .srow:not([hidden])~.srow:not([hidden]){border-top:1px solid var(--line);margin-left:16px;padding-left:0;padding-right:16px}
310
- .srow label{font-weight:400;font-size:15px}
310
+ .srow label{font-weight:400;font-size:15px;align-self:center}
311
311
  .srow>:nth-child(2){grid-column:2;grid-row:1;align-self:center;justify-self:end}
312
312
  .srow .hint{grid-column:1/-1;grid-row:2;margin:0;font-size:12px;color:var(--dim);line-height:1.45;max-width:46ch}
313
313
  .srow input.sel,.srow select.sel{width:170px;border:0;background-color:transparent;text-align:right;color:var(--dim);box-shadow:none}
@@ -336,7 +336,7 @@ aside .foot .more .menu-pop{left:0;right:auto;top:auto;bottom:calc(100% + 4px)}.
336
336
  .updpop{display:none;position:absolute;right:0;top:calc(100% + 6px);z-index:20;width:min(360px,80vw);padding:10px 14px;border:1px solid var(--line);border-radius:var(--r);background:var(--bg-2);box-shadow:var(--shadow-1);font-size:13px;line-height:1.5}
337
337
  .updnew:hover .updpop,.updnew:focus-within .updpop{display:block}
338
338
  .updpop ul{margin:0 0 8px;padding-left:18px}.updpop li{margin:2px 0}.updpop a{color:var(--accent)}
339
- .updbar.quiet{border-color:var(--line);background:var(--bg-2);color:var(--dim)}
339
+ .updbar.quiet{border:0;background:none;padding:0;margin-bottom:32px;color:var(--dim)}#psub:empty{display:none}
340
340
  .toast.on.rep{pointer-events:auto}.toast a{color:inherit;margin-left:8px;text-decoration:underline;text-underline-offset:2px}
341
341
  .help{position:fixed;inset:0;z-index:12;display:none;align-items:center;justify-content:center;background:color-mix(in srgb,var(--bg) 60%,transparent);backdrop-filter:blur(3px)}
342
342
  .help.on{display:flex}.help div{background:var(--bg-2);border:1px solid var(--line);border-radius:var(--r);padding:24px 28px;box-shadow:var(--shadow-2)}
@@ -347,7 +347,9 @@ aside .foot .more .menu-pop{left:0;right:auto;top:auto;bottom:calc(100% + 4px)}.
347
347
  .ice-bar{display:flex;align-items:center;gap:8px;margin:8px 0 8px}
348
348
  .cg-onb{border:1px solid color-mix(in srgb,var(--accent) 45%,var(--line));background:color-mix(in srgb,var(--accent) 8%,var(--bg-2));border-radius:var(--r);padding:14px 16px;margin:4px 0 12px}
349
349
  .cg-onb b{font-size:14px;color:var(--ink-hi)}.cg-onb p{margin:6px 0 12px;font-size:13px;color:var(--dim);line-height:1.5}
350
- .cg-onb .acts{display:flex;justify-content:flex-end;gap:8px;flex-wrap:wrap}
350
+ .cg-onb .acts{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.cg-onb .end,.src-add .acts .end,.ice-form-acts .end{flex:1}
351
+ .ice-filt{position:relative}.ice-filt>summary{list-style:none;cursor:pointer;position:relative}.ice-filt>summary::-webkit-details-marker{display:none}.ice-filt>summary.on::after{content:"";position:absolute;right:6px;top:6px;width:7px;height:7px;border-radius:50%;background:var(--accent);box-shadow:0 0 0 2px var(--bg)}
352
+ .ice-fpop{position:absolute;right:0;top:calc(100% + 4px);z-index:9;display:flex;flex-direction:column;gap:6px;padding:8px;background:var(--bg-2);border:1px solid var(--line);border-radius:var(--r);box-shadow:var(--shadow-2)}.ice-fpop .ice-combo .menu-pop{left:auto;right:0}
351
353
  .ice-bar .count{font-size:13px;font-weight:600;color:var(--ink)}
352
354
  .ice-bar .end{flex:1}
353
355
  .ice-bar .btn,.ice-bulk .btn,.ice-ra .btn{display:inline-flex;align-items:center;gap:6px}
@@ -493,6 +495,7 @@ a.schip:hover{color:var(--ink);border-color:color-mix(in srgb,var(--line) 60%,va
493
495
  <symbol id="i-filter" viewBox="0 0 24 24"><path d="M4 5h16l-6 8v6l-4-2v-4z"/></symbol>
494
496
  <symbol id="i-note" viewBox="0 0 24 24"><path d="M6 3h9l4 4v14H6z"/><path d="M14 3v5h5M9 13h6M9 17h6"/></symbol>
495
497
  <symbol id="i-trash" viewBox="0 0 24 24"><path d="M3 6h18M8 6V4h8v2M6 6l1 14h10l1-14M10 11v6M14 11v6"/></symbol>
498
+ <symbol id="i-ask" viewBox="0 0 24 24"><path d="M21 12a8 8 0 0 1-11.6 7.1L4 20l1-4.6A8 8 0 1 1 21 12z"/><path d="M9.8 9.5a2.3 2.3 0 0 1 4.4.8c0 1.5-2.2 2-2.2 3.2M12 16.5h.01"/></symbol>
496
499
  <symbol id="i-term" viewBox="0 0 24 24"><path d="M4 17l6-5-6-5M12 19h8"/></symbol>
497
500
  <symbol id="i-close" viewBox="0 0 24 24"><path d="M18 6L6 18M6 6l12 12"/></symbol>
498
501
  <symbol id="i-more" viewBox="0 0 24 24"><circle cx="5" cy="12" r="1.2" fill="currentColor"/><circle cx="12" cy="12" r="1.2" fill="currentColor"/><circle cx="19" cy="12" r="1.2" fill="currentColor"/></symbol>