kaizen-agent 1.43.0 → 1.43.2
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/agents/kaizen-reviewer.md +1 -1
- package/cli/board.test.ts +3 -2
- package/cli/browser.test.ts +1 -1
- package/cli/browser.ts +12 -7
- package/cli/pty.test.ts +10 -0
- package/cli/pty.ts +15 -6
- package/cli/settings.test.ts +38 -0
- package/cli/settings.ts +37 -30
- package/cli/state.test.ts +25 -1
- package/cli/state.ts +32 -2
- package/cli/web.ts +11 -1
- package/package.json +1 -1
- package/skills/kaizen/README.md +0 -4
- package/skills/kaizen/SKILL.md +4 -1
- package/skills/kaizen/config.default.yml +2 -51
- package/skills/kaizen/docs.md +19 -5
- package/skills/kaizen/settings.md +10 -3
- package/skills/kaizen/spec-build.md +3 -2
- package/skills/kaizen/spec-main.md +68 -5
- package/skills/kaizen/spec-plan.md +95 -1
- package/skills/kaizen/spec-review.md +2 -1
- package/skills/kaizen-help/SKILL.md +1 -5
- package/web/board/agent-term.ts +1 -1
- package/web/board/api.ts +2 -0
- package/web/board/cards.ts +50 -7
- package/web/board/git.ts +1 -1
- package/web/board/icebox.ts +1 -1
- package/web/board/keys.ts +2 -0
- package/web/board/live.ts +3 -2
- package/web/board/projects.ts +2 -2
- package/web/board/settings.ts +47 -12
- package/web/board/terms.ts +297 -0
- package/web/board/views.ts +19 -5
- package/web/board.html +29 -2
- package/web/board.js +25 -24
- package/web/notebook.js +1 -1
- package/web/notes.html +2 -0
- package/web/term.js +1 -1
package/web/board/keys.ts
CHANGED
|
@@ -54,6 +54,8 @@ export function initKeys() {
|
|
|
54
54
|
} else close();
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
|
+
// The Terminal page has no cards to move between; its keys are the terminals'.
|
|
58
|
+
if (view === "terminal") return;
|
|
57
59
|
if (e.key === "n" && !document.querySelector("dialog[open]")) {
|
|
58
60
|
if (view !== "board") go("board", true);
|
|
59
61
|
newIdea();
|
package/web/board/live.ts
CHANGED
|
@@ -34,9 +34,10 @@ const v0 = ((v) => (v === "icebox" ? "sources" : v))(new URLSearchParams(locatio
|
|
|
34
34
|
export function initLive() {
|
|
35
35
|
// Back from a terminal: what was committed or pushed there touched nothing the server watches.
|
|
36
36
|
addEventListener("focus", () => load(true));
|
|
37
|
-
markView(["settings", "sources"].includes(v0) ? v0 : "board");
|
|
38
|
-
document.body.classList.toggle("paged", ["settings", "sources"].includes(v0));
|
|
37
|
+
markView(["settings", "sources", "terminal"].includes(v0) ? v0 : "board");
|
|
38
|
+
document.body.classList.toggle("paged", ["settings", "sources", "terminal"].includes(v0));
|
|
39
39
|
document.body.classList.toggle("ice", v0 === "sources");
|
|
40
|
+
document.body.classList.toggle("termv", v0 === "terminal");
|
|
40
41
|
(async () => {
|
|
41
42
|
let d;
|
|
42
43
|
try {
|
package/web/board/projects.ts
CHANGED
|
@@ -57,7 +57,7 @@ async function pick(v) {
|
|
|
57
57
|
} else if (v === "__new") {
|
|
58
58
|
// A name, and where it goes: the projects folder unless typed or picked.
|
|
59
59
|
const dl = modal(
|
|
60
|
-
`<b>New project</b><input name="name" placeholder="Project name" aria-label="Project name" spellcheck="false" autocomplete="off"><div class="row"><input name="to" placeholder="
|
|
60
|
+
`<b>New project</b><input name="name" placeholder="Project name" aria-label="Project name" spellcheck="false" autocomplete="off"><div class="row"><input name="to" placeholder="Folder to create" aria-label="Create in" spellcheck="false" autocomplete="off"><button type="button" class="btn ghost" data-a="pick">Pick…</button></div><div class="row"><button type="button" class="btn ghost" data-a="x">Cancel</button><button class="btn pri">Create</button></div>`,
|
|
61
61
|
);
|
|
62
62
|
const f = dl.querySelector("form"),
|
|
63
63
|
name = f.querySelector("[name=name]"),
|
|
@@ -96,7 +96,7 @@ async function pick(v) {
|
|
|
96
96
|
} else if (v === "__clone") {
|
|
97
97
|
// One form: the link, then where it goes. The folder fills in once the link names a repo.
|
|
98
98
|
const dl = modal(
|
|
99
|
-
`<b>Clone from GitHub</b><input name="url" placeholder="https://github.com/owner/repo
|
|
99
|
+
`<b>Clone from GitHub</b><input name="url" placeholder="https://github.com/owner/repo" aria-label="GitHub link (https or ssh)" spellcheck="false" autocomplete="off"><div class="row"><input name="to" placeholder="Folder to clone into" aria-label="Clone into" spellcheck="false" autocomplete="off"><button type="button" class="btn ghost" data-a="pick">Pick…</button></div><div class="row"><button type="button" class="btn ghost" data-a="x">Cancel</button><button class="btn pri">Clone</button></div>`,
|
|
100
100
|
);
|
|
101
101
|
const f = dl.querySelector("form"),
|
|
102
102
|
url = f.querySelector("[name=url]"),
|
package/web/board/settings.ts
CHANGED
|
@@ -47,7 +47,8 @@ export async function openSettings() {
|
|
|
47
47
|
$("psub").textContent = "";
|
|
48
48
|
$("psub").title = "";
|
|
49
49
|
// One control per row: a switch for on/off, one segment per value for a small number,
|
|
50
|
-
//
|
|
50
|
+
// ‹ value › for a choice, stepping and wrapping like a game's options screen. The row's
|
|
51
|
+
// label names the control; its help sits under it.
|
|
51
52
|
const ctl = (id, vals, on) => {
|
|
52
53
|
// The preset is a segmented control over the tiers; custom, which only an edit to another row makes, lights none.
|
|
53
54
|
if (id === "s-preset")
|
|
@@ -60,10 +61,12 @@ export async function openSettings() {
|
|
|
60
61
|
}
|
|
61
62
|
if (vals.every((v) => /^\d+$/.test(v)))
|
|
62
63
|
return html`<span class="bar" id="${id}" role="group">${vals.map((v) => html`<button data-v="${v}" class="${v === on ? "on" : ""}" aria-pressed="${String(v === on)}">${v}</button>`)}</span>`;
|
|
63
|
-
|
|
64
|
+
// Wired by stepper() below; the group takes its name from the row's label.
|
|
65
|
+
return html`<span class="step" id="${id}" role="group" aria-labelledby="${id}-l"><button id="${id}-prev" aria-label="Previous">‹</button><output aria-live="polite">${on === "" ? "—" : on}</output><button id="${id}-next" aria-label="Next">›</button></span>`;
|
|
64
66
|
};
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
// `off`: the row has no effect while another setting stands as it is; dimmed, still editable.
|
|
68
|
+
const row = (label, control, hint, id, q, off = false) =>
|
|
69
|
+
html`<div class="srow${off ? " off" : ""}" data-q="${q.toLowerCase()}"><label for="${id}" id="${id}-l">${label}</label>${control}${hint ? html`<p class="hint">${hint}</p>` : ""}</div>`;
|
|
67
70
|
const hadFocus = document.activeElement?.id === "sq";
|
|
68
71
|
const np = typeof Notification === "undefined" ? null : Notification.permission;
|
|
69
72
|
const ntHelp = np ? (np === "denied" ? "Blocked by this browser" : "") : "Not supported in this browser";
|
|
@@ -83,16 +86,17 @@ export async function openSettings() {
|
|
|
83
86
|
? html`<span class="fpick"><input class="sel" id="${id}" value="${r.value}" placeholder="not set" spellcheck="false"><button class="btn" id="${id}-pick" type="button">Browse…</button></span>`
|
|
84
87
|
: html`<input class="sel" id="${id}" value="${r.value}" placeholder="not set" spellcheck="false">`
|
|
85
88
|
: ctl(id, r.values, r.value),
|
|
86
|
-
r.help,
|
|
89
|
+
[r.help, r.idle].filter(Boolean).join(" · "),
|
|
87
90
|
id,
|
|
88
91
|
[r.label, r.help, r.key, g, ...r.values].join(" "),
|
|
92
|
+
!!r.idle,
|
|
89
93
|
);
|
|
90
94
|
})}</div>`,
|
|
91
95
|
)
|
|
92
96
|
: html`<p class="snone">Settings could not be read. Run <code>kaizen settings</code>.</p>`
|
|
93
97
|
}
|
|
94
|
-
<h3 class="sg">Google</h3><div class="sgrp">${row("OAuth client id", html`<input class="sel" id="gg-id" placeholder="${gg.hasClient ? "
|
|
95
|
-
${row("OAuth client secret", html`<span class="fpick"><input class="sel" id="gg-secret" type="password" placeholder="${gg.hasClient ? "
|
|
98
|
+
<h3 class="sg">Google</h3><div class="sgrp">${row("OAuth client id", html`<input class="sel" id="gg-id" placeholder="${gg.hasClient ? "Saved. Paste a new one to replace it" : "Desktop app client id from Google Cloud"}" spellcheck="false" autocomplete="off">`, "", "gg-id", "google oauth client id sheets write back")}
|
|
99
|
+
${row("OAuth client secret", html`<span class="fpick"><input class="sel" id="gg-secret" type="password" placeholder="${gg.hasClient ? "Saved. Paste a new one to replace it" : ""}" spellcheck="false" autocomplete="off"><button class="btn" id="gg-save" type="button">Save</button></span>`, "", "gg-secret", "google oauth client secret")}
|
|
96
100
|
${row("Google account", html`<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>
|
|
97
101
|
<h3 class="sg">Browser</h3><div class="sgrp">${row("Theme", ctl("th", ["system", "light", "dark"], cur), "", "th", "theme light dark system browser")}
|
|
98
102
|
${row("Notifications", html`<button class="chk${np === "granted" ? " on" : ""}" id="nt" role="switch" aria-checked="${String(np === "granted")}"${np && np !== "denied" ? "" : " disabled"}></button>`, ntHelp, "nt", "notifications " + ntHelp + " browser")}</div>
|
|
@@ -154,7 +158,32 @@ export async function openSettings() {
|
|
|
154
158
|
toast(j.error || "Could not start Google sign-in");
|
|
155
159
|
}
|
|
156
160
|
};
|
|
157
|
-
|
|
161
|
+
// A stepper keeps its own position, so each click moves one value even before a save
|
|
162
|
+
// re-renders the panel; the shown value changes at once and apply() gets the new one.
|
|
163
|
+
// ←/→ press its arrows; focus moves to the arrow pressed.
|
|
164
|
+
const stepper = (el, vals, on, apply) => {
|
|
165
|
+
let i = Math.max(vals.indexOf(on), 0);
|
|
166
|
+
const step = (d) => {
|
|
167
|
+
i = (i + d + vals.length) % vals.length;
|
|
168
|
+
el.querySelector("output").textContent = vals[i] === "" ? "—" : vals[i];
|
|
169
|
+
apply(vals[i]);
|
|
170
|
+
};
|
|
171
|
+
$(el.id + "-prev").onclick = () => step(-1);
|
|
172
|
+
$(el.id + "-next").onclick = () => step(1);
|
|
173
|
+
el.onkeydown = (e) => {
|
|
174
|
+
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
|
|
175
|
+
e.preventDefault();
|
|
176
|
+
const b = $(el.id + (e.key === "ArrowLeft" ? "-prev" : "-next"));
|
|
177
|
+
b.focus();
|
|
178
|
+
b.click();
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
// Re-rendering replaces the arrows, so the one pressed gets focus back.
|
|
182
|
+
const refocus = async (f) => {
|
|
183
|
+
await openSettings();
|
|
184
|
+
if (f && /^s-preset-|-(prev|next)$/.test(f)) $(f)?.focus();
|
|
185
|
+
};
|
|
186
|
+
stepper($("th"), ["system", "light", "dark"], cur, setTheme);
|
|
158
187
|
$("nt").onclick = () => {
|
|
159
188
|
askNotify();
|
|
160
189
|
setTimeout(openSettings, 300);
|
|
@@ -166,16 +195,22 @@ export async function openSettings() {
|
|
|
166
195
|
const f = document.activeElement?.id;
|
|
167
196
|
const j = await post("/settings", { key: r.key, value: v });
|
|
168
197
|
if (j.ok) toast("Saved");
|
|
169
|
-
await
|
|
170
|
-
if (f?.startsWith("s-preset-")) $(f)?.focus();
|
|
198
|
+
await refocus(f);
|
|
171
199
|
};
|
|
172
|
-
if (el.tagName === "
|
|
200
|
+
if (el.tagName === "INPUT")
|
|
173
201
|
el.onchange = async () => {
|
|
174
202
|
await save(el.value.trim());
|
|
175
|
-
|
|
203
|
+
load();
|
|
176
204
|
};
|
|
177
205
|
else el.querySelectorAll("[data-v]").forEach((b) => (b.onclick = () => save(b.dataset.v)));
|
|
178
206
|
if (el.matches(".chk")) el.onclick = () => save(el.dataset.v);
|
|
207
|
+
// Clicks in quick succession save once, the value they settle on.
|
|
208
|
+
let t;
|
|
209
|
+
if (el.matches(".step"))
|
|
210
|
+
stepper(el, r.values, r.value, (v) => {
|
|
211
|
+
clearTimeout(t);
|
|
212
|
+
t = setTimeout(() => save(v), 300);
|
|
213
|
+
});
|
|
179
214
|
// The system's own folder dialog, opened by the board: a page cannot read a real path.
|
|
180
215
|
if (r.folder)
|
|
181
216
|
$(el.id + "-pick").onclick = async () => {
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { store } from "./store.ts";
|
|
2
|
+
import { $, html, ico } from "./dom.ts";
|
|
3
|
+
import { load } from "./api.ts";
|
|
4
|
+
import { termLoad } from "./agent-term.ts";
|
|
5
|
+
import { open } from "./panel.ts";
|
|
6
|
+
import { resume } from "./approvals.ts";
|
|
7
|
+
|
|
8
|
+
// ---- the Terminal page: every in-card agent session the board knows, Ideas to Done, tiled.
|
|
9
|
+
// A tile is one more kzTerm mount on /pty beside the card panel's (attachPty takes many
|
|
10
|
+
// listeners). Mounts outlive redraws: a /state tick moves tiles around, it never reconnects.
|
|
11
|
+
// Layout is per browser in kz-terms: tile order, hidden tiles, the maximised one, and the
|
|
12
|
+
// split sizes per grid shape.
|
|
13
|
+
// ponytail: a √n grid with draggable borders, not an i3-style split tree; add one if a grid falls short.
|
|
14
|
+
// mode: grid (√n), cols (side by side) or rows (stacked, so borders drag up and down).
|
|
15
|
+
type Lay = { mode: string; order: string[]; hidden: string[]; max: string | null; sizes: Record<string, { c: number[]; r: number[] }> };
|
|
16
|
+
const MODES = [["grid", "Grid"], ["cols", "Side by side"], ["rows", "Stacked"]];
|
|
17
|
+
const GAP = 6;
|
|
18
|
+
let lay: Lay = { mode: "grid", order: [], hidden: [], max: null, sizes: {} };
|
|
19
|
+
try {
|
|
20
|
+
lay = { ...lay, ...JSON.parse(localStorage.getItem("kz-terms") || "{}") };
|
|
21
|
+
} catch (e) {}
|
|
22
|
+
const save = () => {
|
|
23
|
+
lay.order = lay.order.slice(-100);
|
|
24
|
+
lay.hidden = lay.hidden.slice(-100);
|
|
25
|
+
try {
|
|
26
|
+
localStorage.setItem("kz-terms", JSON.stringify(lay));
|
|
27
|
+
} catch (e) {}
|
|
28
|
+
};
|
|
29
|
+
// key -> its tile and the function that unmounts its terminal.
|
|
30
|
+
const tiles = new Map<string, { el: HTMLElement; off: (() => void) | null }>();
|
|
31
|
+
let showing = false;
|
|
32
|
+
// A phone shows one terminal at a time, picked from tabs: stacked, each terminal took every
|
|
33
|
+
// swipe and the page could only be scrolled by its thin headers. The pick is per visit.
|
|
34
|
+
// Made on first show, so importing this module needs no window.
|
|
35
|
+
let phone: MediaQueryList | null = null,
|
|
36
|
+
pick: string | null = null;
|
|
37
|
+
|
|
38
|
+
// The header: run name - project name. A run's name is its id without the date, as card chips name runs.
|
|
39
|
+
const nameOf = (c) =>
|
|
40
|
+
c.kind === "run"
|
|
41
|
+
? String(c.id || c.text).replace(/^\d{4}-\d\d-\d\d-/, "").replace(/-/g, " ")
|
|
42
|
+
: String(c.text).split("\n")[0].slice(0, 80);
|
|
43
|
+
const tst = (c) => (!c.pty.alive ? "ended" : c.pty.asking ? "asking" : "running");
|
|
44
|
+
const projOf = (c) => String(c.where || "").split(/[\\/]/).filter(Boolean).pop() || c.where || "";
|
|
45
|
+
// One card per session; the newest card wins a shared key.
|
|
46
|
+
function sessions() {
|
|
47
|
+
const m = new Map();
|
|
48
|
+
for (const c of store.S.cards) if (c.pty && !c.archived) m.set(c.pty.key, c);
|
|
49
|
+
return m;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function termsStop() {
|
|
53
|
+
showing = false;
|
|
54
|
+
for (const t of tiles.values()) t.off?.();
|
|
55
|
+
tiles.clear();
|
|
56
|
+
$("terms").innerHTML = "";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function openTerms() {
|
|
60
|
+
showing = true;
|
|
61
|
+
if (!phone) (phone = matchMedia("(max-width:640px)")).onchange = () => showing && openTerms();
|
|
62
|
+
// A /state tick mid-drag would rebuild the handle being dragged.
|
|
63
|
+
if (document.body.classList.contains("tdrag")) return;
|
|
64
|
+
const box = $("terms"),
|
|
65
|
+
all = sessions();
|
|
66
|
+
for (const [k, t] of tiles)
|
|
67
|
+
if (!all.has(k) || lay.hidden.includes(k)) {
|
|
68
|
+
t.off?.();
|
|
69
|
+
t.el.remove();
|
|
70
|
+
tiles.delete(k);
|
|
71
|
+
}
|
|
72
|
+
const rank = (k) => (lay.order.includes(k) ? lay.order.indexOf(k) : 1e9);
|
|
73
|
+
// Live tiles first, ended ones after; each in the saved order.
|
|
74
|
+
const ended = (k) => (all.get(k).pty.alive ? 0 : 1);
|
|
75
|
+
const keys = [...all.keys()].filter((k) => !lay.hidden.includes(k)).sort((a, b) => ended(a) - ended(b) || rank(a) - rank(b));
|
|
76
|
+
for (const k of keys) if (!lay.order.includes(k)) lay.order.push(k);
|
|
77
|
+
if (lay.max && !keys.includes(lay.max)) lay.max = null;
|
|
78
|
+
// The one tile showing: on a phone the tab picked (the first by default), else the maximised one.
|
|
79
|
+
const one = phone.matches ? (pick && keys.includes(pick) ? pick : keys[0]) : lay.max;
|
|
80
|
+
let grid = box.querySelector<HTMLElement>(".tgrid"),
|
|
81
|
+
bar = box.querySelector<HTMLElement>(".tbar");
|
|
82
|
+
if (!grid) {
|
|
83
|
+
box.innerHTML = `<div class="tbar"></div><div class="tgrid"></div>`;
|
|
84
|
+
grid = box.querySelector(".tgrid")!;
|
|
85
|
+
bar = box.querySelector(".tbar")!;
|
|
86
|
+
}
|
|
87
|
+
// An ended session closed with × has nothing to come back to: no chip for it.
|
|
88
|
+
const hid = [...all.keys()].filter((k) => lay.hidden.includes(k) && all.get(k).pty.alive);
|
|
89
|
+
bar!.innerHTML = html`<span class="bar" role="group" aria-label="Layout">${MODES.map(([v, l]) => html`<button data-mode="${v}" class="${lay.mode === v ? "on" : ""}" aria-pressed="${String(lay.mode === v)}">${l}</button>`)}</span>${hid.length ? html`<span class="dim">Hidden</span>${hid.map((k) => html`<button class="chip" data-show="${k}" title="Show this terminal">${ico("plus")}${nameOf(all.get(k))} - ${projOf(all.get(k))}</button>`)}` : ""}${phone.matches && keys.length > 1 ? html`<span class="ttabs" role="group" aria-label="Terminals">${keys.map((k) => html`<button class="${k === one ? "on" : ""}" data-pick="${k}" aria-pressed="${String(k === one)}"><span class="tdot ${tst(all.get(k))}"></span>${nameOf(all.get(k))} - ${projOf(all.get(k))}</button>`)}</span>` : ""}`.s;
|
|
90
|
+
bar!.querySelectorAll<HTMLElement>("[data-pick]").forEach(
|
|
91
|
+
(b) =>
|
|
92
|
+
(b.onclick = () => {
|
|
93
|
+
pick = b.dataset.pick!;
|
|
94
|
+
openTerms();
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
bar!.querySelector(".ttabs .on")?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
98
|
+
bar!.querySelectorAll<HTMLElement>("[data-mode]").forEach(
|
|
99
|
+
(b) =>
|
|
100
|
+
(b.onclick = () => {
|
|
101
|
+
lay.mode = b.dataset.mode!;
|
|
102
|
+
lay.max = null;
|
|
103
|
+
save();
|
|
104
|
+
openTerms();
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
bar!.querySelectorAll<HTMLElement>("[data-show]").forEach(
|
|
108
|
+
(b) =>
|
|
109
|
+
(b.onclick = () => {
|
|
110
|
+
lay.hidden = lay.hidden.filter((k) => k !== b.dataset.show);
|
|
111
|
+
save();
|
|
112
|
+
openTerms();
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
grid.querySelector(".tempty")?.remove();
|
|
116
|
+
if (!keys.length) {
|
|
117
|
+
grid.style.cssText = "";
|
|
118
|
+
grid.querySelectorAll(".tsplit").forEach((s) => s.remove());
|
|
119
|
+
grid.insertAdjacentHTML(
|
|
120
|
+
"beforeend",
|
|
121
|
+
html`<div class="empty tempty"><div class="mascot"></div><b>${hid.length ? "Every terminal is hidden" : "No terminals running"}</b><p>Runs started with the agent in its card show here, from Ideas to Done.</p></div>`.s,
|
|
122
|
+
);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
for (const k of keys) {
|
|
126
|
+
const c = all.get(k);
|
|
127
|
+
let t = tiles.get(k);
|
|
128
|
+
if (!t) {
|
|
129
|
+
const el = document.createElement("section");
|
|
130
|
+
el.className = "tile";
|
|
131
|
+
el.dataset.k = k;
|
|
132
|
+
el.innerHTML = `<header draggable="true"></header><div class="term" aria-label="Agent terminal"></div>`;
|
|
133
|
+
t = { el, off: null };
|
|
134
|
+
tiles.set(k, t);
|
|
135
|
+
mount(k, t);
|
|
136
|
+
wire(el);
|
|
137
|
+
}
|
|
138
|
+
const st = tst(c),
|
|
139
|
+
off = st === "ended";
|
|
140
|
+
const hd = t.el.querySelector("header")!;
|
|
141
|
+
hd.title = (c.kind === "run" ? c.id : c.text) + " · " + c.where;
|
|
142
|
+
// An ended run can be resumed from here; an idea whose agent ended has no run to resume.
|
|
143
|
+
hd.innerHTML = html`<span class="tdot ${st}" role="img" title="${st}" aria-label="${st}"></span><b>${nameOf(c)} - ${projOf(c)}</b>${off ? html`<span class="tend">ended</span>` : ""}<span class="sp"></span>${off && c.kind === "run" ? html`<button class="btn sm" data-a="resume" title="Continue this run's agent">Resume</button>` : ""}<button class="ib sm" data-a="card" title="Open card" aria-label="Open card">${ico("open")}</button><button class="ib sm" data-a="max" title="${lay.max === k ? "Restore" : "Maximise"}" aria-label="${lay.max === k ? "Restore" : "Maximise"}">${ico(lay.max === k ? "min" : "max")}</button><button class="ib sm" data-a="hide" title="${off ? "Close" : "Hide"}" aria-label="${off ? "Close" : "Hide"}">${ico("close")}</button>`.s;
|
|
144
|
+
t.el.classList.toggle("ended", off);
|
|
145
|
+
t.el.classList.toggle("off", !!one && one !== k);
|
|
146
|
+
t.el.style.gridColumn = "";
|
|
147
|
+
grid.append(t.el); // in order
|
|
148
|
+
}
|
|
149
|
+
// Grid is √n, the last tile spanning what its row leaves empty; side by side is one row, stacked one column.
|
|
150
|
+
const n = one ? 1 : keys.length,
|
|
151
|
+
cols = lay.mode === "cols" ? n : lay.mode === "rows" ? 1 : Math.ceil(Math.sqrt(n)),
|
|
152
|
+
rows = Math.ceil(n / cols);
|
|
153
|
+
// The column where the last row's tile starts spanning; borders right of it are not inner ones there.
|
|
154
|
+
let spanFrom = cols;
|
|
155
|
+
if (!one && cols * rows > n) {
|
|
156
|
+
tiles.get(keys[keys.length - 1])!.el.style.gridColumn = `span ${cols * rows - n + 1}`;
|
|
157
|
+
spanFrom = n - (rows - 1) * cols - 1;
|
|
158
|
+
}
|
|
159
|
+
const shape = `${lay.mode}:${cols}x${rows}`,
|
|
160
|
+
sz = lay.sizes[shape];
|
|
161
|
+
const c = sz?.c?.length === cols ? sz.c : Array(cols).fill(1),
|
|
162
|
+
r = sz?.r?.length === rows ? sz.r : Array(rows).fill(1);
|
|
163
|
+
layout(grid, c, r, shape, spanFrom, !one);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function mount(k, t) {
|
|
167
|
+
termLoad()
|
|
168
|
+
.then((m) => {
|
|
169
|
+
if (tiles.get(k) !== t) return;
|
|
170
|
+
t.off = m(t.el.querySelector(".term"), k, () => load(true));
|
|
171
|
+
})
|
|
172
|
+
.catch(() => (t.el.querySelector(".term")!.textContent = "The terminal could not load."));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function wire(el: HTMLElement) {
|
|
176
|
+
const k = el.dataset.k!;
|
|
177
|
+
el.querySelector("header")!.onclick = (e) => {
|
|
178
|
+
const a = (e.target as HTMLElement).closest<HTMLElement>("[data-a]")?.dataset.a;
|
|
179
|
+
if (a === "max") lay.max = lay.max === k ? null : k;
|
|
180
|
+
else if (a === "hide") {
|
|
181
|
+
lay.hidden.push(k);
|
|
182
|
+
if (lay.max === k) lay.max = null;
|
|
183
|
+
} else if (a === "card") {
|
|
184
|
+
const c = sessions().get(k);
|
|
185
|
+
if (c) open(c);
|
|
186
|
+
return;
|
|
187
|
+
} else if (a === "resume") {
|
|
188
|
+
const c = sessions().get(k);
|
|
189
|
+
// resume() asks the board to open the card on its new session; here the new tile shows it.
|
|
190
|
+
if (c) resume(c, (e.target as HTMLElement).closest("button")).then(() => (store.wantTerm = null));
|
|
191
|
+
return;
|
|
192
|
+
} else return;
|
|
193
|
+
save();
|
|
194
|
+
openTerms();
|
|
195
|
+
};
|
|
196
|
+
el.querySelector("header")!.ondblclick = (e) => {
|
|
197
|
+
// A phone shows one tile anyway: a double-tap there would only leave desktop maximised.
|
|
198
|
+
if ((e.target as HTMLElement).closest("button") || phone?.matches) return;
|
|
199
|
+
lay.max = lay.max === k ? null : k;
|
|
200
|
+
save();
|
|
201
|
+
openTerms();
|
|
202
|
+
};
|
|
203
|
+
// Drag a header onto another tile: the two swap places.
|
|
204
|
+
el.querySelector("header")!.ondragstart = (e) => {
|
|
205
|
+
e.dataTransfer!.setData("text/kz-term", k);
|
|
206
|
+
e.dataTransfer!.effectAllowed = "move";
|
|
207
|
+
};
|
|
208
|
+
el.ondragover = (e) => {
|
|
209
|
+
if (!e.dataTransfer!.types.includes("text/kz-term")) return;
|
|
210
|
+
e.preventDefault();
|
|
211
|
+
el.classList.add("drop");
|
|
212
|
+
};
|
|
213
|
+
el.ondragleave = () => el.classList.remove("drop");
|
|
214
|
+
el.ondrop = (e) => {
|
|
215
|
+
el.classList.remove("drop");
|
|
216
|
+
const from = e.dataTransfer!.getData("text/kz-term");
|
|
217
|
+
if (!from || from === k) return;
|
|
218
|
+
e.preventDefault();
|
|
219
|
+
const i = lay.order.indexOf(from),
|
|
220
|
+
j = lay.order.indexOf(k);
|
|
221
|
+
if (i < 0 || j < 0) return;
|
|
222
|
+
[lay.order[i], lay.order[j]] = [lay.order[j], lay.order[i]];
|
|
223
|
+
save();
|
|
224
|
+
openTerms();
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Track sizes in fr, and a handle on each inner border. A handle's centre is placed in CSS:
|
|
229
|
+
// the tracks before it share (100% - gaps) by their fr, plus the gaps before it. A column
|
|
230
|
+
// handle stops above the last row where that row's spanning tile covers the border.
|
|
231
|
+
const at = (tr: number[], i: number) => {
|
|
232
|
+
const F = tr.reduce((a, b) => a + b, 0),
|
|
233
|
+
S = tr.slice(0, i + 1).reduce((a, b) => a + b, 0);
|
|
234
|
+
return `calc((100% - ${(tr.length - 1) * GAP}px) * ${S / F} + ${i * GAP + GAP / 2}px)`;
|
|
235
|
+
};
|
|
236
|
+
function layout(grid: HTMLElement, c: number[], r: number[], shape: string, spanFrom: number, splits: boolean) {
|
|
237
|
+
grid.querySelectorAll(".tsplit").forEach((s) => s.remove());
|
|
238
|
+
const place = () => {
|
|
239
|
+
grid.style.gridTemplateColumns = c.map((x) => x + "fr").join(" ");
|
|
240
|
+
grid.style.gridTemplateRows = r.map((x) => x + "fr").join(" ");
|
|
241
|
+
grid.querySelectorAll<HTMLElement>(".tsplit").forEach((h) => {
|
|
242
|
+
const i = +h.dataset.i!;
|
|
243
|
+
if (h.dataset.axis === "r") return void (h.style.top = at(r, i));
|
|
244
|
+
h.style.left = at(c, i);
|
|
245
|
+
h.style.bottom = i >= spanFrom ? `calc(100% - ${at(r, r.length - 2)} + ${GAP / 2}px)` : "0px";
|
|
246
|
+
});
|
|
247
|
+
};
|
|
248
|
+
if (splits)
|
|
249
|
+
for (const [axis, tr] of [["c", c], ["r", r]] as const)
|
|
250
|
+
for (let i = 0; i < tr.length - 1; i++) {
|
|
251
|
+
const h = document.createElement("div");
|
|
252
|
+
h.className = "tsplit " + axis;
|
|
253
|
+
h.dataset.axis = axis;
|
|
254
|
+
h.dataset.i = String(i);
|
|
255
|
+
h.setAttribute("role", "separator");
|
|
256
|
+
h.setAttribute("aria-orientation", axis === "c" ? "vertical" : "horizontal");
|
|
257
|
+
h.title = "Drag to resize, double-click to even out";
|
|
258
|
+
drag(h, grid, axis, tr, i, place, () => {
|
|
259
|
+
lay.sizes[shape] = { c: [...c], r: [...r] };
|
|
260
|
+
save();
|
|
261
|
+
});
|
|
262
|
+
grid.append(h);
|
|
263
|
+
}
|
|
264
|
+
place();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function drag(h: HTMLElement, grid: HTMLElement, axis: "c" | "r", tr: number[], i: number, place: () => void, keep: () => void) {
|
|
268
|
+
h.ondblclick = () => {
|
|
269
|
+
tr.fill(1);
|
|
270
|
+
keep();
|
|
271
|
+
place();
|
|
272
|
+
};
|
|
273
|
+
h.onpointerdown = (e) => {
|
|
274
|
+
e.preventDefault();
|
|
275
|
+
h.setPointerCapture(e.pointerId);
|
|
276
|
+
document.body.classList.add("tdrag");
|
|
277
|
+
const F = tr.reduce((a, b) => a + b, 0),
|
|
278
|
+
span = (axis === "c" ? grid.clientWidth : grid.clientHeight) - (tr.length - 1) * GAP,
|
|
279
|
+
min = F * 0.1,
|
|
280
|
+
from = axis === "c" ? e.clientX : e.clientY,
|
|
281
|
+
a = tr[i],
|
|
282
|
+
b = tr[i + 1];
|
|
283
|
+
h.onpointermove = (m) => {
|
|
284
|
+
const d = Math.max(min - a, Math.min(b - min, (((axis === "c" ? m.clientX : m.clientY) - from) / span) * F));
|
|
285
|
+
tr[i] = a + d;
|
|
286
|
+
tr[i + 1] = b - d;
|
|
287
|
+
place();
|
|
288
|
+
};
|
|
289
|
+
h.onpointerup = h.onpointercancel = () => {
|
|
290
|
+
h.onpointermove = h.onpointerup = h.onpointercancel = null;
|
|
291
|
+
document.body.classList.remove("tdrag");
|
|
292
|
+
keep();
|
|
293
|
+
};
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export const termsShowing = () => showing;
|
package/web/board/views.ts
CHANGED
|
@@ -3,8 +3,9 @@ import { $ } from "./dom.ts";
|
|
|
3
3
|
import { openSources } from "./icebox.ts";
|
|
4
4
|
import { close } from "./panel.ts";
|
|
5
5
|
import { openSettings } from "./settings.ts";
|
|
6
|
+
import { openTerms, termsStop } from "./terms.ts";
|
|
6
7
|
|
|
7
|
-
// ---- views: the board,
|
|
8
|
+
// ---- views: the board, Settings and the Icebox as pages in the docked panel, or the Terminal page; each with its own address
|
|
8
9
|
export let view = "board";
|
|
9
10
|
export const markView = (v) => {
|
|
10
11
|
document
|
|
@@ -13,28 +14,41 @@ export const markView = (v) => {
|
|
|
13
14
|
a.dataset.view === v ? a.setAttribute("aria-current", "page") : a.removeAttribute("aria-current"),
|
|
14
15
|
);
|
|
15
16
|
// The Icebox is titled in the header, top left, like the board.
|
|
16
|
-
document.querySelector(".hdr h1").textContent = v === "sources" ? "Icebox" : "Board";
|
|
17
|
+
document.querySelector(".hdr h1").textContent = v === "sources" ? "Icebox" : v === "terminal" ? "Terminal" : "Board";
|
|
17
18
|
};
|
|
18
19
|
export function go(v, push) {
|
|
19
20
|
store.pageGen++;
|
|
20
21
|
if (v === "icebox") v = "sources";
|
|
21
|
-
if (!["settings", "sources"].includes(v)) v = "board";
|
|
22
|
+
if (!["settings", "sources", "terminal"].includes(v)) v = "board";
|
|
22
23
|
const was = view;
|
|
23
24
|
view = v;
|
|
24
25
|
markView(v);
|
|
25
26
|
document.body.classList.toggle("paged", v !== "board");
|
|
26
27
|
document.body.classList.toggle("ice", v === "sources");
|
|
28
|
+
document.body.classList.toggle("termv", v === "terminal");
|
|
27
29
|
if (push && v !== was)
|
|
28
30
|
history.pushState(null, "", v === "board" ? location.pathname : "?view=" + (v === "sources" ? "icebox" : v));
|
|
29
31
|
const p = $("panel");
|
|
30
|
-
if (v === "
|
|
31
|
-
|
|
32
|
+
if (v === "terminal") openTerms();
|
|
33
|
+
else termsStop();
|
|
34
|
+
// The Terminal page is the board's own area, not the docked panel: the panel closes as on the board.
|
|
35
|
+
if (v === "board" || v === "terminal") {
|
|
36
|
+
if (was === "board" || was === "terminal") {
|
|
37
|
+
if (v === "terminal" && was === "board") {
|
|
38
|
+
view = "board";
|
|
39
|
+
close();
|
|
40
|
+
view = v;
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
32
44
|
// Undocked without the slide: it was a page, not a drawer leaving.
|
|
33
45
|
p.style.transition = "none";
|
|
34
46
|
p.classList.remove("open", "docked");
|
|
35
47
|
p.offsetWidth;
|
|
36
48
|
p.style.transition = "";
|
|
49
|
+
view = "board";
|
|
37
50
|
close();
|
|
51
|
+
view = v;
|
|
38
52
|
return;
|
|
39
53
|
}
|
|
40
54
|
p.classList.add("docked");
|
package/web/board.html
CHANGED
|
@@ -229,7 +229,8 @@ section li.hint{display:block;padding:14px;border:1px dashed var(--line);border-
|
|
|
229
229
|
.inl textarea,.notes textarea{min-height:72px;resize:vertical}
|
|
230
230
|
.notes textarea{min-height:160px}
|
|
231
231
|
.inl input::placeholder,.inl textarea::placeholder,.notes textarea::placeholder{color:var(--dim)}
|
|
232
|
-
.inl .row,.notes .row{display:flex;gap:6px;justify-content:flex-end;align-items:center}
|
|
232
|
+
.inl .row,.notes .row{display:flex;flex-wrap:wrap;gap:6px;justify-content:flex-end;align-items:center}
|
|
233
|
+
.split{position:relative;display:inline-flex}.split>#ns{border-top-right-radius:0;border-bottom-right-radius:0}.split>#nm{border-top-left-radius:0;border-bottom-left-radius:0;border-left-color:color-mix(in srgb,var(--on-accent) 35%,var(--accent));padding:0 7px;display:inline-flex;align-items:center}.split>#nm svg{width:14px;height:14px;stroke-width:2}.split .menu-pop{right:0;top:calc(100% + 4px);z-index:9;white-space:nowrap;min-width:0;padding:4px}.split .menu-pop button{padding:5px 8px;font-size:12px;gap:6px}.split .menu-pop svg{width:13px;height:13px}.split .menu-pop button:focus-visible{outline-width:1px;outline-offset:-1px}.split .menu-pop[hidden]{display:none}
|
|
233
234
|
.inl .row .mic{margin-right:0}.inl .row #newpick{margin-right:auto}.inl .row .btn{white-space:nowrap}.inl .att{font-size:12px;color:var(--dim)}.inl .att{font-size:12px;color:var(--dim)}.inl .att:empty{display:none}
|
|
234
235
|
dialog.kz{border:0;padding:0;background:none;width:min(480px,calc(100vw - 32px));max-height:calc(100dvh - 32px);overflow:visible;color:var(--ink)}dialog.kz::backdrop{background:rgb(0 0 0/.45)}dialog.kz .row input{flex:1;min-width:0}dialog.kz #ndc{align-self:flex-start}dialog.viewer{width:min(1100px,calc(100vw - 32px));height:calc(100dvh - 32px);background:var(--bg);border-radius:var(--r);display:none;flex-direction:column;overflow:hidden}dialog.viewer[open]{display:flex}.viewer-hd{display:flex;gap:12px;align-items:center;padding:8px 12px;border-bottom:1px solid var(--line)}.viewer-hd span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600}dialog.viewer img{flex:1;min-height:0;width:100%;object-fit:contain}dialog.viewer iframe{flex:1;border:0;width:100%;background:#fff}
|
|
235
236
|
.inl .run-actions{display:grid;gap:8px}.inl .run-actions .row{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}.inl .run-actions .pri{grid-column:1/-1}
|
|
@@ -268,6 +269,23 @@ aside.open{transform:none;visibility:visible}
|
|
|
268
269
|
body.paged #board,body.paged .ftr,body.paged .scrim,body.paged #bq{display:none}
|
|
269
270
|
/* The Icebox keeps the project bar with its own title, without New idea or the panel's header; Settings has none */
|
|
270
271
|
body.paged:not(.ice) .hdr,body.ice aside.docked>header{display:none}body.ice #newtop{visibility:hidden}
|
|
272
|
+
/* The Terminal page: the project bar stays (it picks whose terminals show), the board gives way to the tiles */
|
|
273
|
+
#terms{display:none}body.termv.paged .hdr{display:flex}body.termv #newtop,body.termv #fbtn{display:none!important}
|
|
274
|
+
body.termv #terms{flex:1;min-height:0;display:flex;flex-direction:column;gap:8px;padding:10px 20px 14px 28px}
|
|
275
|
+
.tbar{display:flex;flex-wrap:wrap;align-items:center;gap:6px;font-size:12px}.tbar .bar{margin-right:8px}.tbar .bar button{padding:0 10px;font-size:12px}.tbar .chip{cursor:pointer;background:none;border:0;font:inherit;font-size:11px}.tbar .chip:hover{color:var(--ink)}.tbar svg{width:12px;height:12px}
|
|
276
|
+
.tgrid{position:relative;flex:1;min-height:0;display:grid;gap:6px}
|
|
277
|
+
.tile{display:flex;flex-direction:column;min-width:0;min-height:0;border:1px solid var(--line);border-radius:var(--r-s);background:var(--bg);overflow:hidden}.tile.off{display:none}.tile.drop{outline:2px solid var(--accent);outline-offset:-2px}
|
|
278
|
+
.tile header{display:flex;align-items:center;gap:8px;min-height:30px;padding:0 4px 0 10px;border-bottom:1px solid var(--line);background:var(--bg-2);cursor:grab;user-select:none}
|
|
279
|
+
.tile header b{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;font-weight:600}.tile header .sp{flex:1}
|
|
280
|
+
.tile.ended .term{opacity:.6}.tend{flex:none;padding:0 6px;border-radius:9px;font-size:11px;color:var(--dim);box-shadow:inset 0 0 0 1px var(--line)}.tile header .btn.sm{height:22px;padding:0 8px;font-size:11px}
|
|
281
|
+
.tdot{flex:none;width:8px;height:8px;border-radius:50%;background:var(--run)}.tdot.asking{background:var(--warn)}.tdot.ended{background:var(--dim);opacity:.6}
|
|
282
|
+
.tile .term{flex:1;min-height:0}.tile .term .xterm{height:100%;padding:4px}.tile .term .xterm-viewport{background:transparent!important}
|
|
283
|
+
.tsplit{position:absolute;z-index:2;touch-action:none}.tsplit.c{top:0;width:10px;margin-left:-5px;cursor:col-resize}.tsplit.r{left:0;right:0;height:10px;margin-top:-5px;cursor:row-resize}.tsplit:hover,.tdrag .tsplit:active{background:color-mix(in srgb,var(--accent) 35%,transparent)}
|
|
284
|
+
body.tdrag{user-select:none}body.tdrag .tile{pointer-events:none}
|
|
285
|
+
@media(max-width:900px){body.termv #terms{padding-inline:16px}}
|
|
286
|
+
/* A phone has no room for a grid or a stack: one terminal fills the page, and tabs above pick which */
|
|
287
|
+
.ttabs{display:none}
|
|
288
|
+
@media(max-width:640px){.tbar .bar,.tile [data-a=max],.tsplit{display:none}.ttabs{display:flex;order:-1;flex:1 0 100%;min-width:0;gap:6px;overflow-x:auto;scrollbar-width:none}.ttabs button{flex:none;display:inline-flex;align-items:center;gap:6px;height:30px;padding:0 10px;border:1px solid var(--line);border-radius:var(--r-pill);background:none;color:var(--dim);font:inherit;font-size:12px;white-space:nowrap}.ttabs button.on{color:var(--ink);background:var(--hover);border-color:var(--accent)}.tgrid{display:flex;flex-direction:column}.tile{flex:1}.tile.off{display:none}}
|
|
271
289
|
aside.docked{position:static;z-index:auto;flex:1;min-height:0;width:auto;transform:none;visibility:visible;transition:none;box-shadow:none;border:0;background:none}
|
|
272
290
|
aside.docked>header,aside.docked>.body{padding-inline:28px}
|
|
273
291
|
aside.docked>header{border-bottom:0;padding-top:28px;padding-bottom:12px}
|
|
@@ -323,6 +341,7 @@ aside .foot .more .menu-pop{left:0;right:auto;top:auto;bottom:calc(100% + 4px)}.
|
|
|
323
341
|
.srow label{font-weight:400;font-size:15px;align-self:center}
|
|
324
342
|
.srow>:nth-child(2){grid-column:2;grid-row:1;align-self:center;justify-self:end}
|
|
325
343
|
.srow .hint{grid-column:1/-1;grid-row:2;margin:0;font-size:12px;color:var(--dim);line-height:1.45;max-width:46ch}
|
|
344
|
+
.srow.off>label,.srow.off>:nth-child(2){opacity:.5}
|
|
326
345
|
.srow input.sel,.srow select.sel{width:170px;border:0;background-color:transparent;text-align:right;color:var(--dim);box-shadow:none}
|
|
327
346
|
.srow select.sel{width:auto;text-align-last:right}
|
|
328
347
|
.fpick{display:inline-flex;gap:6px;align-items:center}
|
|
@@ -338,6 +357,9 @@ aside .foot .more .menu-pop{left:0;right:auto;top:auto;bottom:calc(100% + 4px)}.
|
|
|
338
357
|
/* a small number: one segment per value */
|
|
339
358
|
.bar{display:inline-flex;padding:2px;gap:2px;background:var(--hover);border-radius:var(--r-s)}
|
|
340
359
|
.bar button{min-width:32px;height:28px;border-radius:6px;font-size:13px;color:var(--ink);font-variant-numeric:tabular-nums}
|
|
360
|
+
.step{display:inline-flex;align-items:center;padding:2px;gap:2px;background:var(--hover);border-radius:var(--r-s)}
|
|
361
|
+
.step button{width:28px;height:28px;border-radius:6px;font-size:16px;color:var(--dim)}.step button:hover{background:var(--bg-2);color:var(--ink)}.step button:focus-visible{outline-offset:-2px;outline-color:var(--ink)}
|
|
362
|
+
.step output{min-width:88px;padding:0 6px;text-align:center;font-size:13px;color:var(--ink);white-space:nowrap}
|
|
341
363
|
.bar button:hover{background:var(--hover);color:var(--ink)}.bar button.on,.bar button.on:hover{background:var(--bg-2);box-shadow:var(--shadow-1);font-weight:600}.bar button:focus-visible{outline-offset:-2px;outline-color:var(--ink)}
|
|
342
364
|
#s-preset button{padding:0 12px;text-transform:capitalize}
|
|
343
365
|
.toast{position:fixed;z-index:12;left:28px;bottom:56px;max-width:calc(100% - 56px);background:var(--ink);color:var(--bg);border-radius:var(--r);padding:10px 16px;font-size:13px;box-shadow:var(--shadow-2);opacity:0;transition:opacity .2s;pointer-events:none}
|
|
@@ -511,6 +533,8 @@ a.schip:hover{color:var(--ink);border-color:color-mix(in srgb,var(--line) 60%,va
|
|
|
511
533
|
<symbol id="i-trash" viewBox="0 0 24 24"><path d="M3 6h18M8 6V4h8v2M6 6l1 14h10l1-14M10 11v6M14 11v6"/></symbol>
|
|
512
534
|
<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>
|
|
513
535
|
<symbol id="i-term" viewBox="0 0 24 24"><path d="M4 17l6-5-6-5M12 19h8"/></symbol>
|
|
536
|
+
<symbol id="i-max" viewBox="0 0 24 24"><path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/></symbol>
|
|
537
|
+
<symbol id="i-min" viewBox="0 0 24 24"><path d="M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5"/></symbol>
|
|
514
538
|
<symbol id="i-close" viewBox="0 0 24 24"><path d="M18 6L6 18M6 6l12 12"/></symbol>
|
|
515
539
|
<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>
|
|
516
540
|
<symbol id="i-open" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M15 3v18"/></symbol>
|
|
@@ -553,6 +577,7 @@ a.schip:hover{color:var(--ink);border-color:color-mix(in srgb,var(--line) 60%,va
|
|
|
553
577
|
<a class="it" href="/?view=icebox" data-view="sources" title="Icebox: requests gathered from your sources"><svg><use href="#i-inbox"/></svg><span class="lbl">Icebox</span></a>
|
|
554
578
|
<a class="it" href="/" data-view="board" title="Board"><svg><use href="#i-board"/></svg><span class="lbl">Board</span></a>
|
|
555
579
|
<a class="it" href="/notebook" data-view="notes" title="Notes"><svg><use href="#i-note"/></svg><span class="lbl">Notes</span></a>
|
|
580
|
+
<a class="it" href="/?view=terminal" data-view="terminal" title="Terminal: every agent running in its card"><svg><use href="#i-term"/></svg><span class="lbl">Terminal</span></a>
|
|
556
581
|
<a class="it" href="/?view=settings" data-view="settings" title="Settings"><svg><use href="#i-gear"/></svg><span class="lbl">Settings</span></a>
|
|
557
582
|
<span class="end"></span>
|
|
558
583
|
<div class="theme" role="group" aria-label="Theme"><button data-t="light" title="Light theme"><svg><use href="#i-sun"/></svg><span class="lbl">Light</span></button><button data-t="dark" title="Dark theme"><svg><use href="#i-moon"/></svg><span class="lbl">Dark</span></button></div>
|
|
@@ -579,12 +604,14 @@ a.schip:hover{color:var(--ink);border-color:color-mix(in srgb,var(--line) 60%,va
|
|
|
579
604
|
<div class="combo" id="combo"><button class="sel" id="proj" aria-label="project" aria-haspopup="listbox" aria-expanded="false"></button><div class="menu-pop" id="projpop" hidden><input class="sel" id="projq" aria-label="Search projects" placeholder="Search projects…" spellcheck="false" autocomplete="off"><div id="projs" role="listbox"></div></div></div>
|
|
580
605
|
<div class="combo" id="showc" hidden><button class="sel" id="show" aria-label="Projects shown" aria-haspopup="true" aria-expanded="false"></button><div class="menu-pop" id="showpop" hidden><input class="sel" id="showq" aria-label="Search projects" placeholder="Search projects…" spellcheck="false" autocomplete="off"><div id="shows"></div><div class="pop-ft"><span id="showcount"></span><button id="showall">Show all</button></div></div></div>
|
|
581
606
|
<button class="ib" id="fbtn" aria-label="Filter by status" title="Filter by status" aria-expanded="false" hidden><svg><use href="#i-filter"/></svg></button>
|
|
582
|
-
<input type="search" class="sel" id="bq" placeholder="Filter cards
|
|
607
|
+
<input type="search" class="sel" id="bq" placeholder="Filter cards (press /)" aria-label="Filter cards" spellcheck="false" autocomplete="off">
|
|
583
608
|
<span class="sp"></span>
|
|
584
609
|
<button class="btn pri" id="newtop">New idea</button>
|
|
585
610
|
</div>
|
|
586
611
|
<main id="board" aria-live="polite" aria-busy="true"><section aria-hidden="true"><span class="skel" style="height:14px;width:40%;margin:4px 0 12px"></span><span class="skel" style="height:72px;margin-bottom:8px"></span><span class="skel" style="height:72px"></span></section><section aria-hidden="true"><span class="skel" style="height:14px;width:40%;margin:4px 0 12px"></span><span class="skel" style="height:72px;margin-bottom:8px"></span><span class="skel" style="height:72px"></span></section><section aria-hidden="true"><span class="skel" style="height:14px;width:40%;margin:4px 0 12px"></span><span class="skel" style="height:72px;margin-bottom:8px"></span><span class="skel" style="height:72px"></span></section><section aria-hidden="true"><span class="skel" style="height:14px;width:40%;margin:4px 0 12px"></span><span class="skel" style="height:72px;margin-bottom:8px"></span><span class="skel" style="height:72px"></span></section><section aria-hidden="true"><span class="skel" style="height:14px;width:40%;margin:4px 0 12px"></span><span class="skel" style="height:72px;margin-bottom:8px"></span><span class="skel" style="height:72px"></span></section></main>
|
|
587
612
|
|
|
613
|
+
<div id="terms" aria-label="Terminals"></div>
|
|
614
|
+
|
|
588
615
|
<div class="ftr"><span class="strip" id="strip"></span><span><a href="/issue" target="_blank" rel="noopener"><svg><use href="#i-bug"/></svg>Report an issue</a><a id="kbd"><svg><use href="#i-kbd"/></svg>Keyboard shortcuts</a></span></div>
|
|
589
616
|
|
|
590
617
|
<div class="scrim" id="scrim"></div>
|