kaizen-agent 1.38.1 → 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.
- package/cli/board.test.ts +53 -1
- package/cli/pty.test.ts +9 -1
- package/cli/pty.ts +27 -5
- package/cli/settings.ts +29 -29
- package/cli/sources.test.ts +1 -1
- package/cli/state.test.ts +52 -9
- package/cli/state.ts +40 -11
- package/cli/web.ts +3 -1
- package/package.json +1 -1
- package/skills/kaizen/SKILL.md +3 -0
- package/skills/kaizen/spec-build.md +6 -1
- package/skills/kaizen/spec-main.md +14 -0
- package/web/board/cards.ts +85 -15
- package/web/board/icebox.ts +14 -15
- package/web/board/keys.ts +5 -0
- package/web/board/main.ts +3 -0
- package/web/board/settings.ts +11 -31
- package/web/board/update.ts +39 -40
- package/web/board.html +29 -6
- package/web/board.js +23 -22
- package/web/notebook.js +1 -1
- package/web/notes.html +8 -0
- package/web/term.js +1 -1
package/web/board/cards.ts
CHANGED
|
@@ -9,8 +9,10 @@ import { brief } from "./markdown.ts";
|
|
|
9
9
|
import { open, toggleArchive } from "./panel.ts";
|
|
10
10
|
import { ideaCombo, list, newDir, plogo, short, showList, showable } from "./projects.ts";
|
|
11
11
|
import { askNotify, toast } from "./toast.ts";
|
|
12
|
+
import { resumable, resume } from "./approvals.ts";
|
|
12
13
|
|
|
13
14
|
let filter = null;
|
|
15
|
+
let query = ""; // the header filter box, matched against a card's title and request
|
|
14
16
|
// A column shows this many cards, then a "more" line; Done is where it matters.
|
|
15
17
|
const FOLD = 6;
|
|
16
18
|
const unfolded = new Set();
|
|
@@ -39,6 +41,12 @@ const nested = (c) =>
|
|
|
39
41
|
store.S.cards.some((p) => p.kind === "run" && p.id === c.parent && p.state === c.state && !p.archived && p.column !== 4);
|
|
40
42
|
const kidsOpen = new Set();
|
|
41
43
|
|
|
44
|
+
// What opening a waiting run does, in words: the card's button and its menu say the same.
|
|
45
|
+
const actionOf = (c) =>
|
|
46
|
+
c.status !== "waiting"
|
|
47
|
+
? "Open"
|
|
48
|
+
: { "approvals.plan": "Review plan", findings: "See findings", "approvals.review": "Review work" }[c.awaiting] || "Open";
|
|
49
|
+
|
|
42
50
|
function card(c) {
|
|
43
51
|
const k = key(c),
|
|
44
52
|
el = document.createElement("div");
|
|
@@ -51,7 +59,7 @@ function card(c) {
|
|
|
51
59
|
el.dataset.k = k;
|
|
52
60
|
el.setAttribute("role", "button");
|
|
53
61
|
const st = ICON[c.status] || "idea",
|
|
54
|
-
idt = c.kind === "idea" ? "
|
|
62
|
+
idt = c.kind === "idea" ? "" : c.text.replace(/-/g, " ");
|
|
55
63
|
// The status icon carries the state; what a waiting run needs is its tooltip.
|
|
56
64
|
const sttl = c.status === "waiting" ? need(c.awaiting) || "needs you" : c.status || "idea";
|
|
57
65
|
// Footer: words on the left (project, what it needs), icon-only marks on the right.
|
|
@@ -76,7 +84,9 @@ function card(c) {
|
|
|
76
84
|
marks.push(`<span class="mk warn" title="Started in Yolo: the agent runs commands without asking">yolo</span>`);
|
|
77
85
|
else chips.push(`<span class="chip">${esc(NAMES[t] || t)}</span>`);
|
|
78
86
|
}
|
|
79
|
-
if (c.pty && c.pty.alive)
|
|
87
|
+
if (c.pty && c.pty.alive && c.pty.asking)
|
|
88
|
+
marks.push(mk("i-ask", "The agent is asking you something: open it, terminal button at the top", " warn ask"));
|
|
89
|
+
else if (c.pty && c.pty.alive)
|
|
80
90
|
marks.push(mk("i-term", "The agent is running in this card: open it, terminal button at the top", " run"));
|
|
81
91
|
// A gathered idea's notes open with the stamp promote() writes (ingest() wrote it before
|
|
82
92
|
// the icebox); its chip names the source.
|
|
@@ -98,10 +108,22 @@ function card(c) {
|
|
|
98
108
|
);
|
|
99
109
|
if (c.open) marks.push(mk("i-bug", `${c.open} open finding${c.open === 1 ? "" : "s"} to fix`, " warn"));
|
|
100
110
|
const pick = picking && !archive && c.column === 4;
|
|
111
|
+
// Done cards are one line on the board; the panel still shows everything.
|
|
112
|
+
if (c.column === 4 && !archive) el.classList.add("compact");
|
|
113
|
+
// The one thing a card asks of you, on its face: what a waiting run needs, or resuming a stalled one.
|
|
114
|
+
const act =
|
|
115
|
+
c.kind === "run" && !archive && !pick
|
|
116
|
+
? c.status === "waiting"
|
|
117
|
+
? `<button class="btn sm pri act" data-a="open">${actionOf(c)}</button>`
|
|
118
|
+
: c.status === "stalled" && resumable(c)
|
|
119
|
+
? `<button class="btn sm act" data-a="resume">Resume</button>`
|
|
120
|
+
: ""
|
|
121
|
+
: "";
|
|
101
122
|
el.innerHTML =
|
|
102
123
|
(pick ? `<input type="checkbox" class="pick" ${picked.has(k) ? "checked" : ""} aria-label="Select">` : "") +
|
|
103
124
|
`<div class="c-id"><span class="st d-${st}" role="img" title="${esc(sttl)}" aria-label="${esc(sttl)}"><svg><use href="#s-${st}"/></svg></span><span class="idt">${esc(idt)}</span><span class="age">${c.kind === "run" ? age(c.moved || store.S.now, store.S.now) : ""}</span><button class="ib sm more" data-a="menu" title="Actions" aria-label="Actions" aria-haspopup="menu" aria-expanded="${k === store.menuKey}">${ico("more")}</button></div>
|
|
104
125
|
<div class="c-title"${c.kind === "idea" && brief(c.text).parts.length ? ` title="${esc(c.text)}"` : ""}>${esc(c.kind === "idea" ? brief(c.text).head : c.title || c.id)}</div>
|
|
126
|
+
${act}
|
|
105
127
|
${chips.length || marks.length ? `<div class="c-foot"><span class="c-chips">${chips.join("")}</span><span class="c-marks">${marks.join("")}</span></div>` : ""}`;
|
|
106
128
|
const kids = kidsOf(c);
|
|
107
129
|
if (kids.length) {
|
|
@@ -160,17 +182,7 @@ function card(c) {
|
|
|
160
182
|
["delete", "Remove", ico("trash"), "danger"],
|
|
161
183
|
);
|
|
162
184
|
} else {
|
|
163
|
-
|
|
164
|
-
c.status === "waiting"
|
|
165
|
-
? c.awaiting === "approvals.plan"
|
|
166
|
-
? "Review plan"
|
|
167
|
-
: c.awaiting === "findings"
|
|
168
|
-
? "See findings"
|
|
169
|
-
: c.awaiting === "approvals.review"
|
|
170
|
-
? "Review work"
|
|
171
|
-
: "Open"
|
|
172
|
-
: "Open";
|
|
173
|
-
items.push(["open", openLabel, ico("open")]);
|
|
185
|
+
items.push(["open", actionOf(c), ico("open")]);
|
|
174
186
|
items.push(["archive", "Archive", ico("archive")]);
|
|
175
187
|
if (c.status === "waiting" || c.status === "running" || c.status === "stalled") {
|
|
176
188
|
items.push(null, ["reject", "Abandon", ico("x"), "danger"]);
|
|
@@ -194,6 +206,10 @@ function card(c) {
|
|
|
194
206
|
open(c);
|
|
195
207
|
return;
|
|
196
208
|
}
|
|
209
|
+
if (a === "resume") {
|
|
210
|
+
resume(c, bt);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
197
213
|
if (a === "archive" || a === "restore") {
|
|
198
214
|
toggleArchive(c, a === "archive");
|
|
199
215
|
return;
|
|
@@ -383,6 +399,8 @@ async function startRun(c, kind) {
|
|
|
383
399
|
load();
|
|
384
400
|
}
|
|
385
401
|
|
|
402
|
+
const HINT = { 1: "Ideas you run are planned here", 2: "Approved plans are built here", 3: "Finished builds are checked here" };
|
|
403
|
+
|
|
386
404
|
export function render() {
|
|
387
405
|
// A closed form leaves no draft behind; an open one is reopened after a reload.
|
|
388
406
|
// A form whose card is gone (restored after a reload, or deleted elsewhere) has nowhere to show.
|
|
@@ -422,6 +440,10 @@ export function render() {
|
|
|
422
440
|
`<button class="${archive ? "on" : ""}" data-f="__archive" title="${cs.filter((c) => c.kind === "idea").length} ideas · ${cs.filter((c) => c.kind === "run").length} runs on the board">${archive ? "Back to board" : "Archive " + nArch}</button>`,
|
|
423
441
|
);
|
|
424
442
|
$("strip").innerHTML = strip.join("");
|
|
443
|
+
// The phone's status button: amber dot while a run waits, accent while a filter or the archive is on.
|
|
444
|
+
$("fbtn").hidden = false;
|
|
445
|
+
$("fbtn").classList.toggle("on", !!filter || archive);
|
|
446
|
+
$("fbtn").classList.toggle("wait", !filter && !archive && n("waiting") > 0);
|
|
425
447
|
$("strip")
|
|
426
448
|
.querySelectorAll("button")
|
|
427
449
|
.forEach(
|
|
@@ -468,12 +490,21 @@ export function render() {
|
|
|
468
490
|
}
|
|
469
491
|
COLS.forEach((name, ci) => {
|
|
470
492
|
const s = document.createElement("section");
|
|
471
|
-
const
|
|
493
|
+
const q = query.trim().toLowerCase();
|
|
494
|
+
const list = cs.filter(
|
|
495
|
+
(c) =>
|
|
496
|
+
c.column === ci &&
|
|
497
|
+
(!filter || c.status === filter) &&
|
|
498
|
+
(archive || !nested(c)) &&
|
|
499
|
+
(!q || `${c.title || ""} ${c.text || ""} ${c.id || ""}`.toLowerCase().includes(q)),
|
|
500
|
+
);
|
|
472
501
|
// Runs age by their last move, ideas by inbox position (new ones are appended).
|
|
473
502
|
if (ci < 4) {
|
|
474
503
|
const at = (c) => (c.kind === "run" ? c.moved || 0 : store.S.cards.indexOf(c)),
|
|
475
504
|
old = order[ci] === "old";
|
|
476
|
-
|
|
505
|
+
// Runs waiting on you go first, then the column's own order.
|
|
506
|
+
const w = (c) => (c.status === "waiting" ? 1 : 0);
|
|
507
|
+
list.sort((a, b) => w(b) - w(a) || (old ? at(a) - at(b) : at(b) - at(a)));
|
|
477
508
|
}
|
|
478
509
|
const clearable = ci === 4 && !archive && list.length;
|
|
479
510
|
const olderThan = (days) => list.filter((c) => store.S.now - (c.moved || store.S.now) > days * d);
|
|
@@ -652,12 +683,51 @@ export function render() {
|
|
|
652
683
|
};
|
|
653
684
|
ul.appendChild(li);
|
|
654
685
|
}
|
|
686
|
+
// An empty middle column says what lands in it, so it does not look broken.
|
|
687
|
+
if (!list.length && ci >= 1 && ci <= 3 && cs.length && !filter && !q && !archive)
|
|
688
|
+
ul.innerHTML = `<li class="hint">${HINT[ci]}</li>`;
|
|
655
689
|
s.appendChild(ul);
|
|
656
690
|
b.appendChild(s);
|
|
657
691
|
});
|
|
692
|
+
// The tab says how many runs wait on you, so a background tab still tells you.
|
|
693
|
+
const nw = store.S.cards.filter((c) => c.kind === "run" && c.status === "waiting" && !c.archived).length;
|
|
694
|
+
document.title = nw ? `(${nw}) kaizen` : "kaizen";
|
|
658
695
|
stopDictation(false); // forms were just rebuilt; a listener whose field is gone would write into nothing
|
|
659
696
|
if (fk && document.activeElement === document.body) cardEl(fk)?.focus({ preventScroll: true });
|
|
660
697
|
}
|
|
698
|
+
// The header filter box lives outside #board, so a redraw keeps what is typed; draft() keeps it over a reload.
|
|
699
|
+
export function filterBox() {
|
|
700
|
+
const el = $("bq");
|
|
701
|
+
draft(el, "bq");
|
|
702
|
+
query = el.value;
|
|
703
|
+
el.addEventListener("input", () => {
|
|
704
|
+
query = el.value;
|
|
705
|
+
render();
|
|
706
|
+
});
|
|
707
|
+
el.addEventListener("keydown", (e) => {
|
|
708
|
+
if (e.key === "Escape") {
|
|
709
|
+
e.stopPropagation();
|
|
710
|
+
el.value = "";
|
|
711
|
+
el.dispatchEvent(new Event("input"));
|
|
712
|
+
el.blur();
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
// Phones: the footer's status strip opens as a sheet under the filter button; a press outside closes it.
|
|
717
|
+
export function statusSheet() {
|
|
718
|
+
const b = $("fbtn"),
|
|
719
|
+
f = document.querySelector(".ftr");
|
|
720
|
+
const set = (on) => {
|
|
721
|
+
f.classList.toggle("on", on);
|
|
722
|
+
b.setAttribute("aria-expanded", on);
|
|
723
|
+
if (on) f.style.top = b.getBoundingClientRect().bottom + 6 + "px";
|
|
724
|
+
};
|
|
725
|
+
b.onclick = () => set(!f.classList.contains("on"));
|
|
726
|
+
document.addEventListener("pointerdown", (e) => {
|
|
727
|
+
if (f.classList.contains("on") && !f.contains(e.target) && !b.contains(e.target)) set(false);
|
|
728
|
+
});
|
|
729
|
+
document.addEventListener("keydown", (e) => e.key === "Escape" && f.classList.contains("on") && set(false));
|
|
730
|
+
}
|
|
661
731
|
export const focusedKey = () => document.activeElement?.closest?.("#board .card")?.dataset.k;
|
|
662
732
|
export const cardEl = (k) => k && $("board").querySelector('.card[data-k="' + CSS.escape(k) + '"]');
|
|
663
733
|
// Arrows and hjkl, as on the terminal board: up/down within a column, left/right to the
|
package/web/board/icebox.ts
CHANGED
|
@@ -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" : ""}>
|
|
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
|
|
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="
|
|
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="
|
|
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}
|
|
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="
|
|
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"
|
|
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"
|
|
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>
|
|
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>
|
|
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
|
|
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(
|
|
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 ? "
|
|
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
|
|
969
|
+
toast("Source added");
|
|
971
970
|
load();
|
|
972
971
|
} else if (j.ok) {
|
|
973
972
|
$("src-url").value = $("src-label").value = $("src-cat").value = "";
|
package/web/board/keys.ts
CHANGED
|
@@ -39,6 +39,11 @@ export function initKeys() {
|
|
|
39
39
|
$("sq").focus();
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
42
|
+
if (e.key === "/" && view === "board" && $("bq")) {
|
|
43
|
+
e.preventDefault();
|
|
44
|
+
$("bq").focus();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
42
47
|
if (e.key === "Escape") {
|
|
43
48
|
$("help").classList.remove("on");
|
|
44
49
|
if (store.mode) {
|
package/web/board/main.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { initViews } from "./views.ts";
|
|
|
9
9
|
import { initProjects } from "./projects.ts";
|
|
10
10
|
import { initUpdate } from "./update.ts";
|
|
11
11
|
import { initKeys } from "./keys.ts";
|
|
12
|
+
import { filterBox, statusSheet } from "./cards.ts";
|
|
12
13
|
import { initLive } from "./live.ts";
|
|
13
14
|
import { store } from "./store.ts";
|
|
14
15
|
|
|
@@ -18,6 +19,8 @@ initProjects();
|
|
|
18
19
|
initUpdate();
|
|
19
20
|
initKeys();
|
|
20
21
|
initLive();
|
|
22
|
+
filterBox();
|
|
23
|
+
statusSheet();
|
|
21
24
|
|
|
22
25
|
// One popup at a time: a press anywhere first shuts every open popup it is not inside,
|
|
23
26
|
// before any handler runs (several stop propagation), so the one it opens is the only one
|
package/web/board/settings.ts
CHANGED
|
@@ -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 = "
|
|
48
|
-
$("psub").title =
|
|
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}
|
|
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)}"
|
|
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">`, "
|
|
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>`, "
|
|
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>`,
|
|
104
|
-
<h3 class="sg">Browser</h3><div class="sgrp">${row("Theme", ctl("th", ["system", "light", "dark"], cur), "
|
|
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
|
|
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();
|
package/web/board/update.ts
CHANGED
|
@@ -9,53 +9,52 @@ import { view } from "./views.ts";
|
|
|
9
9
|
// is a quiet row whose Check for updates asks npm now instead of the server's 6 h cache.
|
|
10
10
|
let upd = null,
|
|
11
11
|
ver = null;
|
|
12
|
-
function gotVer(v) {
|
|
12
|
+
export function gotVer(v) {
|
|
13
13
|
ver = v;
|
|
14
|
-
|
|
15
|
-
upd = v;
|
|
14
|
+
upd = v.update ? v : null;
|
|
16
15
|
const a = document.querySelector('.rail a[data-view="settings"]');
|
|
17
|
-
a.classList.
|
|
18
|
-
a.title = `Settings: kaizen ${v.latest} is available
|
|
19
|
-
return
|
|
16
|
+
a.classList.toggle("upd", !!upd);
|
|
17
|
+
a.title = upd ? `Settings: kaizen ${v.latest} is available` : "Settings";
|
|
18
|
+
return !!upd;
|
|
20
19
|
}
|
|
21
20
|
export function updBar() {
|
|
21
|
+
const d = document.createElement("div");
|
|
22
|
+
d.className = `updbar${upd ? "" : " quiet"}`;
|
|
22
23
|
if (!upd) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
$(
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const v = await fetch("/version?fresh=1")
|
|
32
|
-
.then((r) => r.json())
|
|
33
|
-
.catch(() => null);
|
|
34
|
-
if (!d.isConnected) return;
|
|
35
|
-
b.disabled = false;
|
|
36
|
-
b.textContent = "Check for updates";
|
|
37
|
-
if (v && gotVer(v)) {
|
|
38
|
-
d.remove();
|
|
39
|
-
updBar();
|
|
40
|
-
$("upd-go").focus();
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
$("upd-msg").textContent =
|
|
44
|
-
!v || !v.latest ? "Could not reach npm. Try again later." : `kaizen ${v.version} is up to date.`;
|
|
45
|
-
};
|
|
46
|
-
return;
|
|
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
|
+
} else {
|
|
26
|
+
// What's new: each newer release's one-line title, shown on hover, focus or click.
|
|
27
|
+
const notes = (upd.notes || []).map((n) => `<li><b>${esc(n.v)}</b> ${esc(n.text)}</li>`).join("");
|
|
28
|
+
const news = notes
|
|
29
|
+
? `<span class="updnew"><button class="btn" aria-describedby="upd-notes">What's new</button><span class="updpop" id="upd-notes" role="tooltip"><ul>${notes}</ul><a href="https://github.com/hfadhlullah/kaizen/releases" target="_blank" rel="noopener">All release notes</a></span></span>`
|
|
30
|
+
: "";
|
|
31
|
+
d.innerHTML = `<span>kaizen <b>${esc(upd.latest)}</b> is available. You are running ${esc(upd.version)}.</span><span class="updact">${news}<button class="btn" id="upd-chk">Check for updates</button><button class="btn pri" id="upd-go">Update kaizen</button></span>`;
|
|
47
32
|
}
|
|
48
|
-
const d = document.createElement("div");
|
|
49
|
-
d.className = "updbar";
|
|
50
|
-
// What's new: each newer release's one-line title, shown on hover, focus or click.
|
|
51
|
-
const notes = (upd.notes || [])
|
|
52
|
-
.map((n) => `<li><b>${esc(n.v)}</b> ${esc(n.text)}</li>`)
|
|
53
|
-
.join("");
|
|
54
|
-
const news = notes
|
|
55
|
-
? `<span class="updnew"><button class="btn" aria-describedby="upd-notes">What's new</button><span class="updpop" id="upd-notes" role="tooltip"><ul>${notes}</ul><a href="https://github.com/hfadhlullah/kaizen/releases" target="_blank" rel="noopener">All release notes</a></span></span>`
|
|
56
|
-
: "";
|
|
57
|
-
d.innerHTML = `<span>kaizen <b>${esc(upd.latest)}</b> is available. You are running ${esc(upd.version)}.</span><span class="updact">${news}<button class="btn pri" id="upd-go">Update kaizen</button></span>`;
|
|
58
33
|
$("pbody").prepend(d);
|
|
34
|
+
const check = $("upd-chk");
|
|
35
|
+
check.onclick = async () => {
|
|
36
|
+
check.disabled = true;
|
|
37
|
+
check.textContent = "Checking…";
|
|
38
|
+
const v = await fetch("/version?fresh=1")
|
|
39
|
+
.then((r) => r.json())
|
|
40
|
+
.catch(() => null);
|
|
41
|
+
if (!d.isConnected) return;
|
|
42
|
+
check.disabled = false;
|
|
43
|
+
if (!v || !v.latest) {
|
|
44
|
+
if (upd) check.textContent = "Could not check. Try again";
|
|
45
|
+
else $("upd-msg").textContent = "Could not reach npm. Try again later.";
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
gotVer(v);
|
|
49
|
+
d.remove();
|
|
50
|
+
updBar();
|
|
51
|
+
if (upd) $("upd-go").focus();
|
|
52
|
+
else {
|
|
53
|
+
$("upd-msg").textContent = `kaizen ${v.version} is up to date.`;
|
|
54
|
+
$("upd-chk").focus();
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
if (!upd) return;
|
|
59
58
|
const b = $("upd-go");
|
|
60
59
|
b.onclick = async () => {
|
|
61
60
|
if (!confirm(`Update kaizen to ${upd.latest}? The board restarts, and agents running inside cards stop.`)) return;
|