kaizen-agent 1.33.0 → 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.
- package/README.md +33 -3
- package/cli/browser.test.ts +1 -0
- package/cli/browser.ts +12 -8
- package/cli/sources.test.ts +60 -3
- package/cli/sources.ts +124 -25
- package/cli/state.test.ts +31 -0
- package/cli/state.ts +10 -0
- package/cli/web.ts +27 -8
- package/package.json +1 -1
- package/skills/kaizen/gather.md +4 -1
- package/skills/kaizen/spec.md +5 -1
- package/web/board/icebox.ts +26 -26
- package/web/board/projects.ts +3 -1
- package/web/board.html +10 -2
- package/web/board.js +18 -18
- package/web/notebook.js +1 -1
- package/web/term.js +1 -1
package/cli/web.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
launchMode, readSessions, ownerOf, resumePrompt, runAgent, firstSession,
|
|
11
11
|
} from "./state.ts";
|
|
12
12
|
import { attachPty, writePty, resizePty, ptyState, ptyKeys, adoptLoginEnv } from "./pty.ts";
|
|
13
|
-
import { readSources, addSource, removeSource, moveSource, setCategory, readIcebox, promote, dismiss, markReplied, gatherSource, gatherAll, gatherDirs, due, socialUrl, socialOf } from "./sources.ts";
|
|
13
|
+
import { readSources, addSource, removeSource, moveSource, setCategory, readIcebox, promote, dismiss, restore, markReplied, gatherSource, gatherAll, gatherDirs, due, socialUrl, socialOf } from "./sources.ts";
|
|
14
14
|
import * as chatgpt from "./chatgpt.ts";
|
|
15
15
|
import { postLink, sendReply } from "./browser.ts";
|
|
16
16
|
import { listNotes, saveNote, renameNote, deleteNote, boardIndex, linksTo } from "./notes.ts";
|
|
@@ -130,6 +130,11 @@ export async function web(repoDir: string, opts: Opts = {}) {
|
|
|
130
130
|
: [state]
|
|
131
131
|
).filter((d, i, arr) => arr.indexOf(d) === i && existsSync(d));
|
|
132
132
|
|
|
133
|
+
// Hostnames the user put in KAIZEN_WEB_HOSTS (e.g. a `tailscale serve` name):
|
|
134
|
+
// their explicit opt-in, matched exactly, empty when unset.
|
|
135
|
+
const extraHosts = new Set((process.env.KAIZEN_WEB_HOSTS ?? "").split(",")
|
|
136
|
+
.map((s) => s.trim().toLowerCase().replace(/:\d+$/, "")).filter(Boolean));
|
|
137
|
+
|
|
133
138
|
// Any tab on this machine can reach loopback. GET only reads the user's own
|
|
134
139
|
// files, but a POST from a page on the wider web must not write them.
|
|
135
140
|
const local = (req: Request) => {
|
|
@@ -137,7 +142,7 @@ export async function web(repoDir: string, opts: Opts = {}) {
|
|
|
137
142
|
if (!origin) return true; // curl, same-origin fetch on old browsers
|
|
138
143
|
try {
|
|
139
144
|
const h = new URL(origin).hostname;
|
|
140
|
-
return h === "127.0.0.1" || h === "localhost" || h === "[::1]" || h === "::1";
|
|
145
|
+
return h === "127.0.0.1" || h === "localhost" || h === "[::1]" || h === "::1" || extraHosts.has(h);
|
|
141
146
|
} catch { return false; }
|
|
142
147
|
};
|
|
143
148
|
const json = (body: unknown, status = 200) =>
|
|
@@ -281,8 +286,8 @@ export async function web(repoDir: string, opts: Opts = {}) {
|
|
|
281
286
|
async fetch(req, srv) {
|
|
282
287
|
// Host first: a hostname rebound to 127.0.0.1 by an attacker's DNS still
|
|
283
288
|
// arrives with that hostname in Host, and gets nothing.
|
|
284
|
-
const hostname = (req.headers.get("host") ?? "").replace(/:\d+$/, "");
|
|
285
|
-
if (hostname !== "127.0.0.1" && hostname !== "localhost") return new Response("forbidden", { status: 403 });
|
|
289
|
+
const hostname = (req.headers.get("host") ?? "").toLowerCase().replace(/:\d+$/, "");
|
|
290
|
+
if (hostname !== "127.0.0.1" && hostname !== "localhost" && !extraHosts.has(hostname)) return new Response("forbidden", { status: 403 });
|
|
286
291
|
const url = new URL(req.url);
|
|
287
292
|
const path = url.pathname;
|
|
288
293
|
|
|
@@ -355,6 +360,15 @@ export async function web(repoDir: string, opts: Opts = {}) {
|
|
|
355
360
|
return new Response(Bun.file(LOGO), { headers: { "content-type": "image/png", "cache-control": "max-age=86400" } });
|
|
356
361
|
}
|
|
357
362
|
|
|
363
|
+
// Lets a phone add the board to its home screen as a standalone app.
|
|
364
|
+
if (req.method === "GET" && path === "/manifest.webmanifest") {
|
|
365
|
+
return new Response(JSON.stringify({
|
|
366
|
+
name: "kaizen", short_name: "kaizen", start_url: "/", scope: "/", display: "standalone",
|
|
367
|
+
background_color: "#0c162f", theme_color: "#0c162f",
|
|
368
|
+
icons: [{ src: "/logo.png", sizes: "1024x1024", type: "image/png", purpose: "any" }],
|
|
369
|
+
}), { headers: { "content-type": "application/manifest+json" } });
|
|
370
|
+
}
|
|
371
|
+
|
|
358
372
|
if (req.method === "GET" && path === "/state") {
|
|
359
373
|
const all = url.searchParams.get("all") === "1";
|
|
360
374
|
// Before the project list: it registers folders that turn out to be set up.
|
|
@@ -417,16 +431,21 @@ export async function web(repoDir: string, opts: Opts = {}) {
|
|
|
417
431
|
// says which project it belongs to. Nothing is assumed from where the board started.
|
|
418
432
|
const dir = url.searchParams.get("dir");
|
|
419
433
|
if (dir && !states(true).includes(dir)) return bad("unknown state dir", 404);
|
|
420
|
-
const sources = [], icebox = [], failed = [];
|
|
434
|
+
const sources = [], icebox = [], dismissed = [], failed = [];
|
|
421
435
|
for (const d of dir ? [dir] : states(true)) {
|
|
422
436
|
// The seen list is bookkeeping, not something the page shows.
|
|
423
437
|
try { sources.push(...readSources(d).map(({ seen, ...s }) => ({ ...s, social: socialOf(s.url), dir: d, project: label(d) }))); }
|
|
424
438
|
catch { failed.push(label(d)); }
|
|
425
439
|
// Only what still waits for a decision; promoted and dismissed items are record.
|
|
426
|
-
|
|
440
|
+
// Dismissed ones come apart, for the Dismissed category, where they can be restored.
|
|
441
|
+
try {
|
|
442
|
+
const all = readIcebox(d).map((s) => ({ ...s, post: postLink(s.notes), dir: d, project: label(d) }));
|
|
443
|
+
icebox.push(...all.filter((s) => s.status === "new"));
|
|
444
|
+
dismissed.push(...all.filter((s) => s.status === "dismissed"));
|
|
445
|
+
}
|
|
427
446
|
catch { failed.push(label(d) + " (icebox)"); }
|
|
428
447
|
}
|
|
429
|
-
return json({ sources, icebox, failed, every: await every(), gatherer: { use: await setting("sources.agent"), ...chatgpt.status() } });
|
|
448
|
+
return json({ sources, icebox, dismissed, failed, every: await every(), gatherer: { use: await setting("sources.agent"), ...chatgpt.status() } });
|
|
430
449
|
}
|
|
431
450
|
|
|
432
451
|
// The Run form's choices for one project: installed tools and their models.
|
|
@@ -690,7 +709,7 @@ export async function web(repoDir: string, opts: Opts = {}) {
|
|
|
690
709
|
const ids = Array.isArray(body.ids) ? body.ids.filter((i: unknown) => typeof i === "string") : [];
|
|
691
710
|
try {
|
|
692
711
|
const why = body.op === "promote" ? promote(body.dir, ids, typeof body.text === "string" ? body.text : undefined, typeof body.details === "string" ? body.details : undefined)
|
|
693
|
-
: body.op === "dismiss" ? dismiss(body.dir, ids) : "unknown op";
|
|
712
|
+
: body.op === "dismiss" ? dismiss(body.dir, ids) : body.op === "restore" ? restore(body.dir, ids) : "unknown op";
|
|
694
713
|
if (why) return bad(why, why === "no such item" ? 404 : why === "unknown op" ? 400 : 409);
|
|
695
714
|
} catch (e) { return bad((e as Error).message, 500); }
|
|
696
715
|
changed();
|
package/package.json
CHANGED
package/skills/kaizen/gather.md
CHANGED
|
@@ -31,7 +31,10 @@ cannot judge on its own.
|
|
|
31
31
|
account or a key the user has not set up for this.
|
|
32
32
|
3. Hand what you read to kaizen. With your file-writing tool, not the shell, write the
|
|
33
33
|
requests to a new temporary file outside the project, under a name you choose: one
|
|
34
|
-
request per line, optionally a tab and then notes for that request.
|
|
34
|
+
request per line, optionally a tab and then notes for that request. A picture the source
|
|
35
|
+
shows beside a request goes in its notes as `img https://its-address`, one token per
|
|
36
|
+
image; kaizen keeps those as the item's images. Lines from `kaizen gather --posts`
|
|
37
|
+
already carry theirs: copy them unchanged. Leave out any
|
|
35
38
|
request the source marks Done or Finish. Then run:
|
|
36
39
|
|
|
37
40
|
```
|
package/skills/kaizen/spec.md
CHANGED
|
@@ -86,7 +86,9 @@ Record the track in `state.json` as `track`, with the three answers written into
|
|
|
86
86
|
|
|
87
87
|
Create the run id as `YYYY-MM-DD-<short-kebab-slug>`. Write the request verbatim —
|
|
88
88
|
do not summarize it, since later stages read this instead of the conversation and a
|
|
89
|
-
summary is where requirements quietly disappear.
|
|
89
|
+
summary is where requirements quietly disappear. The board also finds a run's card by
|
|
90
|
+
this text: a request that does not contain the user's words leaves their idea as a
|
|
91
|
+
second card. Context you add goes below the verbatim text, never in place of it.
|
|
90
92
|
|
|
91
93
|
Verbatim includes the mess. Requests arrive as thinking out loud: half-formed,
|
|
92
94
|
out of order, self-correcting, with the decisive constraint tucked into an aside near
|
|
@@ -110,6 +112,8 @@ Write into `00-request.md`: the verbatim request, the deliverable, what done mea
|
|
|
110
112
|
and what wrong means. Record in `state.json`: `id`, `mode`, `runner`, `agent`, `track`,
|
|
111
113
|
`stage: "plan"`, `awaiting: null`, `iteration: 0`, and `updated` — the time of this
|
|
112
114
|
write, refreshed on every later one.
|
|
115
|
+
Where the environment has `CLAUDE_CODE_SESSION_ID`, also record it as `session`: the
|
|
116
|
+
board keeps the run on the card it was launched from by that id.
|
|
113
117
|
|
|
114
118
|
`stage` takes exactly one of five words, and boards key their columns on them:
|
|
115
119
|
`plan`, `build`, `review`, `done`, `abandoned`. Set it when the stage starts —
|
package/web/board/icebox.ts
CHANGED
|
@@ -134,7 +134,9 @@ export async function openSources() {
|
|
|
134
134
|
return "link";
|
|
135
135
|
};
|
|
136
136
|
const list = (d && d.sources) || [],
|
|
137
|
-
|
|
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
|
|
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.
|
|
231
|
-
//
|
|
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
|
|
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
|
|
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
|
-
|
|
293
|
-
? `<div class="ice-zero"><h4>Nothing matches</h4><p>${
|
|
294
|
-
:
|
|
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 () => {
|
package/web/board/projects.ts
CHANGED
|
@@ -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}
|