kaizen-agent 1.33.1 → 1.34.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.
@@ -134,7 +134,9 @@ export async function openSources() {
134
134
  return "link";
135
135
  };
136
136
  const list = (d && d.sources) || [],
137
- ice = (d && d.icebox) || [],
137
+ // Dismissed items ride along, shown only under their own category.
138
+ ice = [...((d && d.icebox) || []), ...((d && d.dismissed) || [])],
139
+ waiting = ((d && d.icebox) || []).length,
138
140
  G = (d && d.gatherer) || {},
139
141
  chatOn = G.use === "chatgpt" && G.connected;
140
142
  // A source belongs to one project, named on its row. Picking another only arms Move: a select
@@ -186,7 +188,7 @@ export async function openSources() {
186
188
  srcKey = (it) => it.dir + " " + it.source;
187
189
  // Filters narrow in turn: project (the board's own picker, all projects only), then
188
190
  // category, then source; each chip counts what the filters before it leave.
189
- const catOf = (it) => it.category || "Uncategorised",
191
+ const catOf = (it) => (it.status === "dismissed" ? "Dismissed" : it.category || "Uncategorised"),
190
192
  count = (xs, f) => {
191
193
  const m = new Map();
192
194
  xs.forEach((it) => {
@@ -213,7 +215,7 @@ export async function openSources() {
213
215
  if (iceForm && !keys.has(iceForm)) iceForm = null;
214
216
  const groups = new Map();
215
217
  ice.forEach((it, i) => {
216
- if (!inProj(it) || (iceCat && catOf(it) !== iceCat) || (iceSrc && srcKey(it) !== iceSrc)) return;
218
+ if (!inProj(it) || (iceCat ? catOf(it) !== iceCat : it.status === "dismissed") || (iceSrc && srcKey(it) !== iceSrc)) return;
217
219
  const k = catOf(it);
218
220
  groups.has(k) || groups.set(k, []);
219
221
  groups.get(k).push(i);
@@ -227,20 +229,13 @@ export async function openSources() {
227
229
  }
228
230
  if (!order.some((i) => iceKey(ice[i]) === iceCur)) iceCur = order.length ? iceKey(ice[order[0]]) : null;
229
231
  const nSel = order.filter((i) => iceSel.has(iceKey(ice[i]))).length;
230
- // Filters are plain dropdowns, each shown only when it has a choice to make. The project one
231
- // is the board's own picker: one project, or all; several picked on the board read as such.
232
+ // Filters are plain dropdowns, each shown only when it has a choice to make. Projects are
233
+ // narrowed by the board's own Showing picker above, not here.
232
234
  const many = byProj.size > 1,
233
235
  opt = (v, on, t) => `<option value="${esc(v)}"${on ? " selected" : ""}>${esc(t)}</option>`;
234
236
  const pick = (id, label, xs) =>
235
237
  `<select class="sel ice-sel" id="${id}" aria-label="${label}">${xs.join("")}</select>`;
236
238
  const chips =
237
- (many
238
- ? pick("ice-fp", "Project", [
239
- opt("", !store.shown.size, "All projects"),
240
- ...(store.shown.size > 1 ? [opt("*", true, store.shown.size + " projects")] : []),
241
- ...[...byProj].map(([k, { it }]) => opt(k, store.shown.size === 1 && store.shown.has(k), it.project || k)),
242
- ])
243
- : "") +
244
239
  (byCat.size > 1
245
240
  ? pick("ice-fc", "Category", [
246
241
  opt("", !iceCat, "All categories"),
@@ -253,7 +248,7 @@ export async function openSources() {
253
248
  ...[...bySrc].map(([k, { it }]) => opt(k, iceSrc === k, srcName(it))),
254
249
  ])
255
250
  : "");
256
- 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">${order.length < ice.length ? `${order.length} of ${ice.length}` : ice.length} 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>`;
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} dismissed` : `${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>`;
257
252
  const bulk = iceSel.size
258
253
  ? `<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>`
259
254
  : "";
@@ -282,16 +277,16 @@ export async function openSources() {
282
277
  k = iceKey(it),
283
278
  open = iceForm === k;
284
279
  return (
285
- `<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"><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><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>` +
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"><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 === "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>` +
286
281
  (open
287
- ? `<div class="ice-open">${it.notes ? `<div class="ice-notes">${esc(it.notes)}</div>` : ""}<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>`
282
+ ? `<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) => `<a href="${esc(u)}" target="_blank" rel="noopener noreferrer"><img data-src="${esc(u)}" 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>`
288
283
  : "")
289
284
  );
290
285
  };
291
286
  const inbox =
292
- ice.length && !order.length
293
- ? `<div class="ice-zero"><h4>Nothing matches</h4><p>${ice.length} waiting outside these filters.</p><p style="margin-top:10px"><button class="btn sm" id="ice-unf">Clear filters</button></p></div>`
294
- : ice.length
287
+ waiting && !order.length
288
+ ? `<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>`
289
+ : order.length
295
290
  ? `<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>`
296
291
  : `<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>`;
297
292
  // The sources, in the right-hand drawer: shut until asked for, open by itself while there
@@ -398,14 +393,6 @@ export async function openSources() {
398
393
  iceCat = e.target.value;
399
394
  openSources();
400
395
  };
401
- if ($("ice-fp"))
402
- $("ice-fp").onchange = (e) => {
403
- const v = e.target.value;
404
- if (v === "*") return;
405
- store.shown = new Set(v ? [v] : []);
406
- setShown();
407
- openSources();
408
- };
409
396
  if ($("ice-unf"))
410
397
  $("ice-unf").onclick = () => {
411
398
  iceCat = iceSrc = "";
@@ -573,6 +560,10 @@ export async function openSources() {
573
560
  $("pbody")
574
561
  .querySelectorAll("[data-o]")
575
562
  .forEach((b) => (b.onclick = () => toggle(+b.dataset.o)));
563
+ // Images from a source load only when asked: fetching one tells its host the item was opened.
564
+ $("pbody")
565
+ .querySelectorAll("details.ice-imgs")
566
+ .forEach((d) => (d.ontoggle = () => d.open && d.querySelectorAll("img[data-src]").forEach((i) => (i.src = i.dataset.src))));
576
567
  $("pbody")
577
568
  .querySelectorAll("[data-pr]")
578
569
  .forEach(
@@ -591,6 +582,15 @@ export async function openSources() {
591
582
  iceAct("dismiss", ice[+b.dataset.ds], "Dismissed");
592
583
  }),
593
584
  );
585
+ $("pbody")
586
+ .querySelectorAll("[data-rs]")
587
+ .forEach(
588
+ (b) =>
589
+ (b.onclick = () => {
590
+ b.disabled = true;
591
+ iceAct("restore", ice[+b.dataset.rs], "Restored");
592
+ }),
593
+ );
594
594
  const open1 = ice.find((it) => iceKey(it) === iceForm);
595
595
  if ($("ice-rp"))
596
596
  $("ice-rp").onclick = async () => {
@@ -2,7 +2,7 @@ import { store } from "./store.ts";
2
2
  import { load, post } from "./api.ts";
3
3
  import { render } from "./cards.ts";
4
4
  import { $, esc } from "./dom.ts";
5
- import { openSources } from "./icebox.ts";
5
+ import { openSources, srcOpen } from "./icebox.ts";
6
6
  import { toast } from "./toast.ts";
7
7
  import { view } from "./views.ts";
8
8
 
@@ -144,6 +144,8 @@ export function setShown() {
144
144
  localStorage.setItem("kz-show", JSON.stringify([...store.shown]));
145
145
  } catch (e) {}
146
146
  render();
147
+ // The Icebox narrows by the same picker, so it redraws too.
148
+ if (srcOpen()) openSources();
147
149
  }
148
150
 
149
151
  export function initProjects() {
package/web/board.html CHANGED
@@ -2,8 +2,15 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width,initial-scale=1">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
6
6
  <title>kaizen</title>
7
+ <link rel="manifest" href="/manifest.webmanifest">
8
+ <meta name="theme-color" content="#f6f8fc" media="(prefers-color-scheme: light)">
9
+ <meta name="theme-color" content="#0c162f" media="(prefers-color-scheme: dark)">
10
+ <meta name="mobile-web-app-capable" content="yes">
11
+ <meta name="apple-mobile-web-app-capable" content="yes">
12
+ <meta name="apple-mobile-web-app-title" content="kaizen">
13
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
7
14
  <link rel="icon" href="/favicon.ico" sizes="any">
8
15
  <link rel="icon" type="image/png" href="/logo.png">
9
16
  <link rel="apple-touch-icon" href="/logo.png">
@@ -49,7 +56,7 @@ svg{width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:1.6;stroke
49
56
  /* end side menu */
50
57
  /* the work area: an inset rounded panel on the canvas the side menu sits on */
51
58
  .app{flex:1;min-width:0;display:flex;flex-direction:column;margin:8px 8px 8px 0;background:var(--bg);border:1px solid var(--line);border-radius:var(--r);overflow:hidden}
52
- @media(max-width:640px){.app{margin:0;border:0;border-radius:0}}
59
+ @media(max-width:640px){.app{margin:0;border:0;border-radius:0}body{padding:env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left)}aside:not(.docked){padding:env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) 0}}
53
60
  /* header */
54
61
  .hdr{display:flex;align-items:center;gap:12px;padding:10px 20px 10px 28px;border-bottom:1px solid color-mix(in srgb,var(--line) 60%,transparent);flex-wrap:wrap}
55
62
  pre.mark{margin:0;font:700 8px/1 var(--mono)}
@@ -378,6 +385,7 @@ body:not(.paged) .src-sec,body:not(.paged) .src-scrim{display:none}
378
385
  .ice-form-acts .sel{width:auto;flex:none}
379
386
  .ice-form-acts .end{flex:1}
380
387
  .ice-form-acts .btn{white-space:nowrap}
388
+ .ice-imgs{font-size:12px;color:var(--dim)}.ice-imgs[open]{display:flex;gap:6px;flex-wrap:wrap}.ice-imgs summary{cursor:pointer;flex-basis:100%}.ice-imgs img{height:72px;max-width:120px;object-fit:cover;border-radius:var(--r-s);border:1px solid var(--line)}
381
389
  .ice-notes{font-size:12px;color:var(--dim);line-height:1.45;overflow-wrap:anywhere;white-space:pre-wrap}
382
390
  .src-empty{padding:26px 16px;text-align:center;background:var(--bg);border:1px dashed var(--line);border-radius:var(--r);margin:12px 0 16px}
383
391
  .src-empty-ico{width:36px;height:36px;margin:0 auto 10px;color:var(--dim);display:flex;align-items:center;justify-content:center}