kaizen-agent 1.42.2 → 1.43.1
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/backlog.test.ts +133 -0
- package/cli/board.test.ts +10 -4
- package/cli/browser.test.ts +1 -1
- package/cli/browser.ts +12 -7
- package/cli/html.test.ts +19 -0
- package/cli/notes.ts +2 -6
- package/cli/pty.ts +2 -1
- package/cli/settings.test.ts +38 -0
- package/cli/settings.ts +37 -30
- package/cli/state.test.ts +86 -3
- package/cli/state.ts +245 -45
- package/cli/web.ts +67 -16
- 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 +1 -50
- package/skills/kaizen/docs.md +3 -2
- package/skills/kaizen/settings.md +10 -3
- package/skills/kaizen/spec-build.md +3 -2
- package/skills/kaizen/spec-main.md +40 -5
- package/skills/kaizen/spec-review.md +2 -1
- package/skills/kaizen-help/SKILL.md +0 -4
- package/web/board/agent-term.ts +1 -1
- package/web/board/api.ts +2 -0
- package/web/board/approvals.ts +3 -3
- package/web/board/cards.ts +284 -108
- package/web/board/chat.ts +4 -4
- package/web/board/dom.ts +17 -5
- package/web/board/fixes.ts +6 -8
- package/web/board/git.ts +3 -3
- package/web/board/icebox.ts +52 -70
- package/web/board/keys.ts +9 -6
- package/web/board/live.ts +3 -2
- package/web/board/main.ts +1 -0
- package/web/board/panel.ts +22 -16
- package/web/board/projects.ts +11 -16
- package/web/board/settings.ts +60 -29
- package/web/board/store.ts +2 -0
- package/web/board/terms.ts +279 -0
- package/web/board/update.ts +7 -7
- package/web/board/views.ts +19 -5
- package/web/board.html +40 -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/chat.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { load, post } from "./api.ts";
|
|
2
2
|
import { key } from "./cards.ts";
|
|
3
3
|
import { mic, stopDictation } from "./dictation.ts";
|
|
4
|
-
import { $,
|
|
4
|
+
import { $, html, raw as rawHtml } from "./dom.ts";
|
|
5
5
|
import { DK, draft } from "./drafts.ts";
|
|
6
6
|
import { attach, brief, md } from "./markdown.ts";
|
|
7
7
|
import { toast } from "./toast.ts";
|
|
@@ -28,12 +28,12 @@ export function chat(c, src, ro) {
|
|
|
28
28
|
const b = c.kind === "idea" ? brief(c.text) : { parts: [] };
|
|
29
29
|
let h =
|
|
30
30
|
(b.parts.length
|
|
31
|
-
? `<section class="desc">${md(b.parts.map((p) => `### ${p.label}\n${p.body}`).join("\n\n"))}</section>`
|
|
31
|
+
? html`<section class="desc">${rawHtml(md(b.parts.map((p) => `### ${p.label}\n${p.body}`).join("\n\n")))}</section>`
|
|
32
32
|
: "") +
|
|
33
|
-
`<div class="chat">${entries.map((n) => `<div class="msg">${n.at ? `<time>${
|
|
33
|
+
html`<div class="chat">${entries.map((n) => html`<div class="msg">${n.at ? html`<time>${n.at}</time>` : ""}${rawHtml(md(fix(n.text)))}</div>`)}${entries.length ? "" : html`<p style="color:var(--dim)">No notes yet.</p>`}</div>`;
|
|
34
34
|
if (!ro)
|
|
35
35
|
h += `<div class="notes"><textarea placeholder="Add a note. Paste a screenshot, or attach files."></textarea><div class="att" id="natt"></div><div class="tools"><input type="file" id="nfile" multiple hidden><button class="btn ghost" id="npick">Attach</button><span>Enter sends, Shift+Enter newline</span><button class="btn pri" id="nsend">Send</button></div></div>`;
|
|
36
|
-
$("pbody").innerHTML = h;
|
|
36
|
+
$("pbody").innerHTML = rawHtml(h);
|
|
37
37
|
if (ro) return;
|
|
38
38
|
const ta = $("pbody").querySelector("textarea"),
|
|
39
39
|
files = [];
|
package/web/board/dom.ts
CHANGED
|
@@ -2,13 +2,25 @@
|
|
|
2
2
|
export const $ = <T extends HTMLElement = any>(id: string) => document.getElementById(id) as T;
|
|
3
3
|
export const esc = (s) =>
|
|
4
4
|
String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
5
|
-
|
|
5
|
+
// Trusted markup. Everything `html` interpolates is escaped unless it is a Safe.
|
|
6
|
+
export class Safe {
|
|
7
|
+
constructor(readonly s: string) {}
|
|
8
|
+
toString() {
|
|
9
|
+
return this.s;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export const raw = (s) => new Safe(String(s));
|
|
13
|
+
const part = (v): string => (v instanceof Safe ? v.s : Array.isArray(v) ? v.map(part).join("") : v == null || v === false ? "" : esc(v));
|
|
14
|
+
// Escape-by-default template: html`<b>${title}</b>` cannot inject markup. Keep attributes double-quoted.
|
|
15
|
+
export const html = (strs: TemplateStringsArray, ...vals) => raw(strs.reduce((a, s, i) => a + part(vals[i - 1]) + s));
|
|
16
|
+
export const ico = (n) => raw(`<svg><use href="#i-${n}"/></svg>`);
|
|
6
17
|
// Placeholder rows for a panel whose body is still being fetched.
|
|
7
|
-
export const SKEL =
|
|
18
|
+
export const SKEL = raw(
|
|
8
19
|
'<div aria-busy="true" aria-label="Loading">' +
|
|
9
20
|
'<span class="skel" style="height:16px;width:35%;margin:20px 0 14px"></span>' +
|
|
10
21
|
'<span class="skel" style="height:44px;margin-bottom:10px"></span>'.repeat(4) +
|
|
11
|
-
"</div>"
|
|
22
|
+
"</div>",
|
|
23
|
+
);
|
|
12
24
|
// Files as {name, data-URL}, the shape /note and /icebox take.
|
|
13
25
|
export const dataUrls = (files: File[]) =>
|
|
14
26
|
Promise.all(
|
|
@@ -22,10 +34,10 @@ export const dataUrls = (files: File[]) =>
|
|
|
22
34
|
),
|
|
23
35
|
);
|
|
24
36
|
// A modal form: the native <dialog>, removed from the page once closed.
|
|
25
|
-
export function modal(
|
|
37
|
+
export function modal(body: string | Safe) {
|
|
26
38
|
const dl = document.createElement("dialog");
|
|
27
39
|
dl.className = "kz";
|
|
28
|
-
dl.innerHTML = `<form class="inl" method="dialog">${
|
|
40
|
+
dl.innerHTML = `<form class="inl" method="dialog">${body}</form>`;
|
|
29
41
|
dl.onclose = () => dl.remove();
|
|
30
42
|
// A tap on the backdrop. Braces matter: an on* handler that returns false cancels the click,
|
|
31
43
|
// which stopped every submit button inside the form.
|
package/web/board/fixes.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { store } from "./store.ts";
|
|
|
2
2
|
import { where } from "./agent-term.ts";
|
|
3
3
|
import { post } from "./api.ts";
|
|
4
4
|
import { key, via } from "./cards.ts";
|
|
5
|
-
import {
|
|
5
|
+
import { $, html, raw } from "./dom.ts";
|
|
6
6
|
import { md } from "./markdown.ts";
|
|
7
7
|
import { openRun } from "./panel.ts";
|
|
8
8
|
import { askNotify, toast } from "./toast.ts";
|
|
@@ -35,17 +35,15 @@ export function fixForm(c) {
|
|
|
35
35
|
return;
|
|
36
36
|
const el = document.createElement("div");
|
|
37
37
|
el.className = "inl fix";
|
|
38
|
-
el.innerHTML =
|
|
39
|
-
`<div class="q">Fix findings</div><div class="q dim">Opens your agent in this project, told to fix the ticked findings and recheck them.</div>${c.status === "waiting" ? `<div class="q dim">A session is already waiting on this run in a terminal; this opens another one.</div>` : ""}` +
|
|
38
|
+
el.innerHTML = html`<div class="q">Fix findings</div><div class="q dim">Opens your agent in this project, told to fix the ticked findings and recheck them.</div>${c.status === "waiting" ? html`<div class="q dim">A session is already waiting on this run in a terminal; this opens another one.</div>` : ""}${
|
|
40
39
|
fs
|
|
41
40
|
.map((f) => {
|
|
42
41
|
const x = f.done ? " (closed)" : sent(f) ? " (sent)" : "";
|
|
43
|
-
return `<label class="${f.done ? "off done" : x ? "off" : ""}"><input type="checkbox" data-n="${f.n}" ${x ? "checked disabled" : fixOff.has(k(f.n)) ? "" : "checked"}><span>${f.n}. ${md(f.text).replace(/^<p>|<\/p>$/g, "")}${x}</span></label>`;
|
|
42
|
+
return html`<label class="${f.done ? "off done" : x ? "off" : ""}"><input type="checkbox" data-n="${f.n}" ${x ? "checked disabled" : fixOff.has(k(f.n)) ? "" : "checked"}><span>${f.n}. ${raw(md(f.text).replace(/^<p>|<\/p>$/g, ""))}${x}</span></label>`;
|
|
44
43
|
})
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
: "");
|
|
44
|
+
}${open.length
|
|
45
|
+
? html`<div class="row"><button class="btn ghost" data-a="all"></button><button class="btn pri" data-a="s"></button></div>`
|
|
46
|
+
: ""}`;
|
|
49
47
|
if (!open.length) return $("pbody").appendChild(el);
|
|
50
48
|
const go = el.querySelector("[data-a=s]"),
|
|
51
49
|
allb = el.querySelector("[data-a=all]"),
|
package/web/board/git.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { store } from "./store.ts";
|
|
2
2
|
import { post } from "./api.ts";
|
|
3
3
|
import { key } from "./cards.ts";
|
|
4
|
-
import { $,
|
|
4
|
+
import { $, html } from "./dom.ts";
|
|
5
5
|
import { commitMsg } from "./markdown.ts";
|
|
6
6
|
import { openRun } from "./panel.ts";
|
|
7
7
|
import { toast } from "./toast.ts";
|
|
@@ -15,7 +15,7 @@ export function gitBtn(c) {
|
|
|
15
15
|
// A run already pushed keeps saying so; work changed since is offered beside it.
|
|
16
16
|
if (k && k.pushed)
|
|
17
17
|
return (
|
|
18
|
-
`<button class="btn" disabled title="${
|
|
18
|
+
html`<button class="btn" disabled title="${k.sha}">Committed & pushed</button>` +
|
|
19
19
|
(g.files.length
|
|
20
20
|
? `<button class="btn" data-a="commit">Commit new changes</button>`
|
|
21
21
|
: g.ahead > 0
|
|
@@ -31,7 +31,7 @@ export function gitBtn(c) {
|
|
|
31
31
|
export function confirmCommit(c, msg) {
|
|
32
32
|
const g = store.runDoc.git,
|
|
33
33
|
foot = $("pfoot");
|
|
34
|
-
foot.innerHTML = `<div class="inl" style="flex:1"><div class="q">Commit ${g.files.length} changed file${g.files.length === 1 ? "" : "s"} on ${
|
|
34
|
+
foot.innerHTML = html`<div class="inl" style="flex:1"><div class="q">Commit ${g.files.length} changed file${g.files.length === 1 ? "" : "s"} on ${g.branch || "this branch"} and push to ${g.remote}?</div><div class="q dim">Only the files this run's report names.${g.others ? ` ${g.others} other changed file${g.others === 1 ? " is" : "s are"} left alone.` : ""}</div><pre class="files">${g.files.join("\n")}</pre>${g.shared && g.shared.length ? html`<div class="q">These files also hold other runs' changes, and a file is committed whole:</div><pre class="files">${g.shared.join("\n")}</pre>` : ""}<textarea aria-label="Commit message" placeholder="Describe what changed">${msg || commitMsg(store.runDoc.impl) || c.title || c.text}</textarea><div class="q dim" id="clong" hidden>The first line is over 72 characters; logs will cut it short.</div><div class="row"><button class="btn ghost" data-a="c">Cancel</button><button class="btn pri" data-a="s">Commit & push</button></div></div>`;
|
|
35
35
|
const inp = foot.querySelector("textarea"),
|
|
36
36
|
go = foot.querySelector("[data-a=s]");
|
|
37
37
|
inp.focus();
|
package/web/board/icebox.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { termStop } from "./agent-term.ts";
|
|
|
3
3
|
import { load, post } from "./api.ts";
|
|
4
4
|
import { via } from "./cards.ts";
|
|
5
5
|
import { mic, stopDictation } from "./dictation.ts";
|
|
6
|
-
import { $, dataUrls,
|
|
6
|
+
import { $, dataUrls, html, ico, modal, raw, SKEL } from "./dom.ts";
|
|
7
7
|
import { ideaCombo, newDir, plogo, setShown, short, showable } from "./projects.ts";
|
|
8
8
|
import { toast } from "./toast.ts";
|
|
9
9
|
import { go, view } from "./views.ts";
|
|
@@ -167,7 +167,7 @@ export async function openSources() {
|
|
|
167
167
|
// Sheets and Docs, whose site icon is generic): nothing but the host reaches anyone. A social
|
|
168
168
|
// platform's mark is drawn here instead: Meta's favicons are same-origin only.
|
|
169
169
|
const fav = (u, fb, soc) => {
|
|
170
|
-
if (PLAT[soc]) return `<span class="fav">${ico("b-" + soc)}</span>`;
|
|
170
|
+
if (PLAT[soc]) return html`<span class="fav">${ico("b-" + soc)}</span>`;
|
|
171
171
|
let h;
|
|
172
172
|
try {
|
|
173
173
|
const x = new URL(u);
|
|
@@ -180,7 +180,7 @@ export async function openSources() {
|
|
|
180
180
|
: u.includes("docs.google.com/document")
|
|
181
181
|
? "https://ssl.gstatic.com/docs/documents/images/kix-favicon7.ico"
|
|
182
182
|
: h + "/favicon.ico";
|
|
183
|
-
return `<span class="fav">${src ? `<img src="${
|
|
183
|
+
return html`<span class="fav">${src ? html`<img src="${src}" alt="" loading="lazy" referrerpolicy="no-referrer" onerror="this.remove()">` : ""}${ico(fb)}</span>`;
|
|
184
184
|
};
|
|
185
185
|
// What a source is called when nothing names it: its platform, else its site.
|
|
186
186
|
const platOf = (s) => PLAT[s?.social] || (s ? host(s.url).replace(/^www\./, "") : "") || "Other";
|
|
@@ -201,12 +201,7 @@ export async function openSources() {
|
|
|
201
201
|
// changes on every arrow key, and each change would have moved the source. The names are the picker's;
|
|
202
202
|
// a row whose project the picker does not list still gets its own, selected, so Move never points elsewhere.
|
|
203
203
|
const opts = (on, name) =>
|
|
204
|
-
|
|
205
|
-
? `<option value="${esc(on)}" title="${esc(name || on)}" selected>${esc(short(name || on))}</option>`
|
|
206
|
-
: "") +
|
|
207
|
-
store.S.projects
|
|
208
|
-
.map((p) => `<option value="${esc(p.dir)}" title="${esc(p.label)}"${p.dir === on ? " selected" : ""}>${esc(short(p.label))}</option>`)
|
|
209
|
-
.join("");
|
|
204
|
+
html`${on && !store.S.projects.some((p) => p.dir === on) ? html`<option value="${on}" title="${name || on}" selected>${short(name || on)}</option>` : ""}${store.S.projects.map((p) => html`<option value="${p.dir}" title="${p.label}"${p.dir === on ? " selected" : ""}>${short(p.label)}</option>`)}`;
|
|
210
205
|
const cards = list
|
|
211
206
|
.map((s, i) => {
|
|
212
207
|
const st = srcType(s),
|
|
@@ -214,30 +209,24 @@ export async function openSources() {
|
|
|
214
209
|
// A social source is never read by kaizen, so its not-ok is the agent's turn, not a failure.
|
|
215
210
|
const statusHtml =
|
|
216
211
|
s.social && (!s.last || (!s.last.ok && /gathered by your agent/i.test(s.last.note)))
|
|
217
|
-
? `<span class="src-badge dim">${ico("at")}${chatOn ? "Gathered with ChatGPT" : "Gathered by your agent"}</span>`
|
|
212
|
+
? html`<span class="src-badge dim">${ico("at")}${chatOn ? "Gathered with ChatGPT" : "Gathered by your agent"}</span>`
|
|
218
213
|
: !s.last
|
|
219
|
-
? `<span class="src-badge dim">Not gathered yet</span>`
|
|
214
|
+
? html`<span class="src-badge dim">Not gathered yet</span>`
|
|
220
215
|
: s.last.ok
|
|
221
|
-
? `<span class="src-badge ok"><svg viewBox="0 0 16 16"><path d="M3.5 8.5l3 3 6-6" stroke="currentColor" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>${
|
|
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: ${
|
|
216
|
+
? html`<span class="src-badge ok"><svg viewBox="0 0 16 16"><path d="M3.5 8.5l3 3 6-6" stroke="currentColor" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>${s.last.note}</span><span class="hint" style="margin:0">${ago(s.last.at)}</span>`
|
|
217
|
+
: html`<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: ${s.last.note}</span><span class="hint" style="margin:0">${ago(s.last.at)}</span>`;
|
|
223
218
|
const kind = st === "sheet" ? "sheet" : st === "doc" ? "doc" : st === "social" ? "at" : "link",
|
|
224
219
|
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" ?
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
.map((f) => `<p style="color:var(--dim)">The sources of ${esc(f)} could not be read.</p>`)
|
|
230
|
-
.join("");
|
|
231
|
-
const emptyState = `<div class="src-empty"><div class="src-empty-ico">${ico("link")}</div><h4>No sources yet</h4></div>`;
|
|
220
|
+
return html`<div class="src-card" id="src-card-${i}"><div class="src-hd"><span class="src-ico" title="${st === "social" ? (PLAT[s.social] || "social") : st}">${fav(s.url, kind, s.social)}</span><div class="src-info"><span class="src-title" title="${title}">${title}</span><a class="src-url" href="${s.url}" target="_blank" rel="noopener noreferrer" title="${s.url}"><span>${bare}</span>${ico("ext")}</a></div><div class="src-acts">${s.social ? html`<button class="ib sm" id="src-ag-${i}" data-ag="${i}" aria-label="Gather ${title} with your agent" title="${chatOn ? "Gather with your ChatGPT plan" : "Gather: opens your agent on /kaizen gather"}">${ico("refresh")}</button>` : html`<button class="ib sm" id="src-chk-${i}" data-chk="${i}" aria-label="Check ${title} now" title="Check now">${ico("refresh")}</button>`}<button class="ib sm danger" id="src-rm-${i}" data-rm="${i}" aria-label="Remove ${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="${s.dir + " " + s.url}" value="${s.category || ""}" placeholder="${platOf(s)}" maxlength="40" aria-label="Category of ${title}"><button class="btn sm pri" id="src-cs-${i}" aria-label="Save the category of ${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="${s.dir + " " + s.url}" aria-label="Project of ${title}">${opts(s.dir, s.project)}</select><button class="btn sm pri" id="src-mb-${i}" aria-label="Move ${title} to the chosen project" disabled>Move</button></label>${st === "sheet" || /github\.com/.test(s.url) ? html`<label class="src-wb"><input type="checkbox" id="src-wb-${i}" data-wb="${i}"${s.writeback ? " checked" : ""}> Write back</label>` : ""}</div></div>`;
|
|
221
|
+
});
|
|
222
|
+
const fails = ((d && d.failed) || []).map((f) => html`<p style="color:var(--dim)">The sources of ${f} could not be read.</p>`);
|
|
223
|
+
const emptyState = html`<div class="src-empty"><div class="src-empty-ico">${ico("link")}</div><h4>No sources yet</h4></div>`;
|
|
232
224
|
const social = srcKind === "social";
|
|
233
225
|
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="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(
|
|
226
|
+
const addBox = html`<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="${String(!social)}">Link</button><button id="src-k-social" data-kind="social" aria-pressed="${String(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
227
|
PLAT,
|
|
236
228
|
)
|
|
237
|
-
.map(([k, v]) => `<option value="${k}">${v}</option>`)
|
|
238
|
-
.join(
|
|
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="end"></span><button class="btn pri" id="src-add">Add source</button></div></div>`;
|
|
229
|
+
.map(([k, v]) => html`<option value="${k}">${v}</option>`)}</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 ? "" : html`<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
230
|
// The inbox: one row per request, by project when showing all, then by category,
|
|
242
231
|
// narrowed to one source by its chip. Checked, open and cursor items are kept by key.
|
|
243
232
|
const srcName = (it) => {
|
|
@@ -302,9 +291,9 @@ export async function openSources() {
|
|
|
302
291
|
const many = byProj.size > 1;
|
|
303
292
|
// Filters are searchable pickers (the header's combo): a button, then a search over the list.
|
|
304
293
|
const pick = (id, label, cur, rows) =>
|
|
305
|
-
`<div class="combo ice-combo" id="${id}"><button class="sel" aria-label="${label}" aria-haspopup="listbox" aria-expanded="false">${cur}</button><div class="menu-pop" hidden><input class="sel" aria-label="Search ${label.toLowerCase()}" placeholder="Search…" spellcheck="false" autocomplete="off"><div class="pick-list" role="listbox">${rows}</div></div></div>`;
|
|
306
|
-
const opt = (v, on,
|
|
307
|
-
`<button type="button" role="option" data-v="${
|
|
294
|
+
html`<div class="combo ice-combo" id="${id}"><button class="sel" aria-label="${label}" aria-haspopup="listbox" aria-expanded="false">${cur}</button><div class="menu-pop" hidden><input class="sel" aria-label="Search ${label.toLowerCase()}" placeholder="Search…" spellcheck="false" autocomplete="off"><div class="pick-list" role="listbox">${rows}</div></div></div>`;
|
|
295
|
+
const opt = (v, on, inner, q, cls = "") =>
|
|
296
|
+
html`<button type="button" role="option" data-v="${v}" data-q="${q.toLowerCase()}" class="${on ? "on" : ""}${cls}" aria-selected="${String(on)}">${inner}</button>`;
|
|
308
297
|
// Sources group by platform: two Threads searches sit under one Threads.
|
|
309
298
|
const srcGroups = new Map();
|
|
310
299
|
for (const [k, { it }] of bySrc) {
|
|
@@ -314,91 +303,84 @@ export async function openSources() {
|
|
|
314
303
|
}
|
|
315
304
|
const srcIco = (it) => fav(it.source, "link", srcOf(it)?.social);
|
|
316
305
|
const curSrc = iceSrc && [...bySrc].find(([k]) => k === iceSrc)?.[1].it;
|
|
317
|
-
const chips =
|
|
306
|
+
const chips = html`${
|
|
318
307
|
// Dismissed and Done are hidden until picked, so even a lone one is a choice.
|
|
319
|
-
|
|
308
|
+
byCat.size > 1 || byCat.has("Dismissed") || byCat.has("Done")
|
|
320
309
|
? pick(
|
|
321
310
|
"ice-fc",
|
|
322
311
|
"Category",
|
|
323
|
-
`<span>${
|
|
324
|
-
opt("", !iceCat, `<span>All categories</span>`, "all categories")
|
|
325
|
-
[...byCat].map(([k, { n }]) => opt(k, iceCat === k, `<span>${esc(k)}</span><i>${n}</i>`, k)).join(""),
|
|
312
|
+
html`<span>${iceCat || "All categories"}</span>`,
|
|
313
|
+
html`${opt("", !iceCat, html`<span>All categories</span>`, "all categories")}${[...byCat].map(([k, { n }]) => opt(k, iceCat === k, html`<span>${k}</span><i>${n}</i>`, k))}`,
|
|
326
314
|
)
|
|
327
|
-
: ""
|
|
328
|
-
|
|
315
|
+
: ""}${
|
|
316
|
+
bySrc.size > 1
|
|
329
317
|
? pick(
|
|
330
318
|
"ice-fs",
|
|
331
319
|
"Source",
|
|
332
|
-
curSrc ? `${srcIco(curSrc)}<span>${
|
|
333
|
-
opt("", !iceSrc, `<span>All sources</span>`, "all sources")
|
|
334
|
-
[
|
|
335
|
-
.map(
|
|
336
|
-
|
|
337
|
-
`<div class="grp" data-g="${esc(g.toLowerCase())}">${srcIco(xs[0][1])}${esc(g)}</div>` +
|
|
338
|
-
xs.map(([k, it]) => opt(k, iceSrc === k, `${srcIco(it)}<span>${esc(srcName(it).toLowerCase().startsWith(g.toLowerCase() + ":") ? srcName(it).slice(g.length + 1).trim() : srcName(it))}</span><i>${bySrc.get(k).n}</i>`, g + " " + srcName(it), " sub")).join(""),
|
|
339
|
-
)
|
|
340
|
-
.join(""),
|
|
320
|
+
curSrc ? html`${srcIco(curSrc)}<span>${srcName(curSrc)}</span>` : html`<span>All sources</span>`,
|
|
321
|
+
html`${opt("", !iceSrc, html`<span>All sources</span>`, "all sources")}${[...srcGroups].map(
|
|
322
|
+
([g, xs]) =>
|
|
323
|
+
html`<div class="grp" data-g="${g.toLowerCase()}">${srcIco(xs[0][1])}${g}</div>${xs.map(([k, it]) => opt(k, iceSrc === k, html`${srcIco(it)}<span>${srcName(it).toLowerCase().startsWith(g.toLowerCase() + ":") ? srcName(it).slice(g.length + 1).trim() : srcName(it)}</span><i>${bySrc.get(k).n}</i>`, g + " " + srcName(it), " sub"))}`,
|
|
324
|
+
)}`,
|
|
341
325
|
)
|
|
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><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 ghost sm" id="ice-new" title="Add a request by hand">${ico("plus")} Add</button><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>`;
|
|
326
|
+
: ""}`;
|
|
327
|
+
const bar = html`<div class="ice-bar">${order.length ? html`<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>${String(chips) ? html`<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 ghost sm" id="ice-new" title="Add a request by hand">${ico("plus")} Add</button><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="${String(srcShow ?? !list.length)}" aria-controls="src-sec">${ico("link")} Sources · ${list.length}</button></div>`;
|
|
344
328
|
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="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>`
|
|
329
|
+
? html`<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
330
|
: "";
|
|
347
331
|
// The open item's form: a clean title for the idea's card and the person's own details;
|
|
348
332
|
// what was gathered is added to the idea's notes by the server.
|
|
349
333
|
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="${
|
|
334
|
+
html`<div class="ice-form"><label for="ice-title" class="hint" style="margin:0">Idea title</label><input class="sel" id="ice-title" data-k="${iceKey(it)}" maxlength="200" value="${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
335
|
// A gathered post can be answered: Reply drafts, the box is the approval, Send reply posts that text.
|
|
352
336
|
const replyBox = (it) => {
|
|
353
337
|
if (!it.post) return "";
|
|
354
|
-
if (it.replied) return `<span class="hint" style="margin:0">You replied to this post ${ago(it.replied)}.</span>`;
|
|
338
|
+
if (it.replied) return html`<span class="hint" style="margin:0">You replied to this post ${ago(it.replied)}.</span>`;
|
|
355
339
|
const r = iceReply && iceReply.k === iceKey(it) ? iceReply : null;
|
|
356
340
|
const style = (d) =>
|
|
357
|
-
`<select class="sel" id="ice-rst" aria-label="Reply style"${d ? " disabled" : ""}>${Object.entries(
|
|
341
|
+
html`<select class="sel" id="ice-rst" aria-label="Reply style"${d ? " disabled" : ""}>${Object.entries(
|
|
358
342
|
STYLES,
|
|
359
343
|
)
|
|
360
|
-
.map(([v, l]) => `<option value="${v}"${v === rStyle(it.dir) ? " selected" : ""}>${l}</option>`)
|
|
361
|
-
.join("")}</select>`;
|
|
344
|
+
.map(([v, l]) => html`<option value="${v}"${v === rStyle(it.dir) ? " selected" : ""}>${l}</option>`)}</select>`;
|
|
362
345
|
if (!r)
|
|
363
|
-
return `<div class="ice-form-acts"><span class="hint">You review it before it posts.</span>${G.connected ? style(false) : ""}<button class="btn sm" id="ice-rp">Reply</button></div>`;
|
|
364
|
-
if (r.wait) return `<div class="ice-form-acts"><span class="hint">Drafting…</span></div>`;
|
|
365
|
-
return `<div class="ice-form"><label for="ice-reply" class="hint" style="margin:0">Your reply${r.note ? ` · ${
|
|
346
|
+
return html`<div class="ice-form-acts"><span class="hint">You review it before it posts.</span>${G.connected ? style(false) : ""}<button class="btn sm" id="ice-rp">Reply</button></div>`;
|
|
347
|
+
if (r.wait) return html`<div class="ice-form-acts"><span class="hint">Drafting…</span></div>`;
|
|
348
|
+
return html`<div class="ice-form"><label for="ice-reply" class="hint" style="margin:0">Your reply${r.note ? ` · ${r.note}` : ""}</label><textarea class="sel" id="ice-reply" rows="3" maxlength="2000" aria-label="Your reply"${r.busy || r.again ? " disabled" : ""}>${r.text}</textarea><span class="hint" id="ice-rn" style="margin:0">${r.again ? "Writing another…" : `${r.text.length} chars`}</span><div class="ice-form-acts"><span class="end"></span>${G.connected ? html`${style(r.busy || r.again)}<button class="btn ghost sm" id="ice-rg"${r.busy || r.again ? " disabled" : ""}>Regenerate</button>` : ""}<button class="btn ghost sm" id="ice-rc"${r.busy ? " disabled" : ""}>Cancel</button><button class="btn pri sm" id="ice-rs"${r.busy || r.again || !r.text.trim() ? " disabled" : ""}>${r.busy ? "Sending…" : "Send reply"}</button></div></div>`;
|
|
366
349
|
};
|
|
367
350
|
const row = (i) => {
|
|
368
351
|
const it = ice[i],
|
|
369
352
|
k = iceKey(it),
|
|
370
353
|
open = iceForm === k;
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
(open
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
);
|
|
354
|
+
const head = html`<div class="ice-row${iceCur === k ? " cur" : ""}${iceSel.has(k) ? " sel" : ""}" data-i="${i}" data-k="${k}"><input type="checkbox" id="ice-ck-${i}" data-ck="${i}"${iceSel.has(k) ? " checked" : ""} aria-label="Select: ${it.text}"><span class="ice-src" title="${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="${String(open)}" title="${it.text}">${it.text}</button><span class="ice-meta">${many ? html`<span class="nm" title="${it.project || ""}">${plogo(it.dir, it.project || "")}${short(it.project || "")}</span>` : ""}${it.status === "done" && it.written ? html`<span title="Written back ${ago(it.written)}">written back</span>` : ""}${it.status === "done" && it.writeError ? html`<span title="${it.writeError}">write-back failed</span>` : ""}${it.status === "promoted" ? html`<span title="Promoted to Ideas: ${it.idea || ""}">promoted</span>` : ""}${it.replied ? html`<span title="Replied ${ago(it.replied)}">replied</span>` : ""}<span class="ice-age">${ago(it.at)}</span><span class="ice-ra">${it.status === "promoted" || it.status === "done" ? "" : html`<button class="btn ghost sm" id="ice-pr-${i}" data-pr="${i}" title="Promote to Ideas (p)" aria-label="Promote to Ideas: ${it.text}">${ico("plus")}</button>`}${it.status === "promoted" || it.status === "done" ? "" : it.status === "dismissed" ? html`<button class="btn ghost sm" id="ice-rs-${i}" data-rs="${i}" title="Restore to waiting" aria-label="Restore: ${it.text}">Restore</button>` : html`<button class="btn ghost sm" id="ice-ds-${i}" data-ds="${i}" title="Dismiss (d)" aria-label="Dismiss: ${it.text}">${ico("x")}</button>`}</span></span></div>`;
|
|
355
|
+
const more = open
|
|
356
|
+
? html`<div class="ice-open">${it.notes ? html`<div class="ice-notes">${it.notes}</div>` : ""}${(it.images || []).filter((u) => /^https:\/\//.test(u)).length ? html`<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, n) => { const src = `/ice-image?dir=${encodeURIComponent(it.dir)}&id=${encodeURIComponent(it.id)}&i=${n}`; return html`<a href="${src}" target="_blank" rel="noopener noreferrer"><img data-src="${src}" alt="" referrerpolicy="no-referrer"></a>`; })}</details>` : ""}<span class="hint" style="margin:0">From ${srcName(it)} · ${ago(it.at)}${it.post ? html` · <a href="${it.post}" target="_blank" rel="noopener">open post</a>` : ""}</span>${replyBox(it)}${icePro === k ? form(it) : ""}</div>`
|
|
357
|
+
: "";
|
|
358
|
+
return html`${head}${more}`;
|
|
377
359
|
};
|
|
378
360
|
const inbox =
|
|
379
361
|
waiting && !order.length
|
|
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>`
|
|
362
|
+
? html`<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
363
|
: order.length
|
|
382
|
-
? `<div class="ice-list">${[...groups].map(([g, ix]) => `<div class="ice-cat">${
|
|
383
|
-
: `<div class="ice-zero"><h4>Nothing waiting</h4></div>`;
|
|
364
|
+
? html`<div class="ice-list">${[...groups].map(([g, ix]) => html`<div class="ice-cat">${g}<i>${ix.length}</i></div>${ix.map(row)}`)}</div>`
|
|
365
|
+
: html`<div class="ice-zero"><h4>Nothing waiting</h4></div>`;
|
|
384
366
|
// The sources, in the right-hand drawer: shut until asked for, open by itself while there
|
|
385
367
|
// are none until closed. Slid in only when opened; a rebuild while open shows it in place.
|
|
386
368
|
const drawer = srcShow ?? !list.length,
|
|
387
369
|
slide = drawer && !$("src-sec")?.classList.contains("open");
|
|
388
|
-
const sec = `<div class="src-scrim${drawer && !slide ? " on" : ""}" id="src-scrim"></div><aside class="src-sec${drawer && !slide ? " open" : ""}" id="src-sec" aria-label="Sources"${drawer ? "" : " inert"}><header><div class="t"><b>Sources</b><div class="src-chips"><a id="src-to-settings" class="schip" role="button" tabindex="0" title="${d && d.every && d.every !== "off" ? `Checked every ${
|
|
370
|
+
const sec = html`<div class="src-scrim${drawer && !slide ? " on" : ""}" id="src-scrim"></div><aside class="src-sec${drawer && !slide ? " open" : ""}" id="src-sec" aria-label="Sources"${drawer ? "" : " inert"}><header><div class="t"><b>Sources</b><div class="src-chips"><a id="src-to-settings" class="schip" role="button" tabindex="0" title="${d && d.every && d.every !== "off" ? `Checked every ${d.every}` : "Auto-check off"}: change in Settings">${ico("clock")}${d && d.every && d.every !== "off" ? (d.every) : "Off"}</a>${G.connected ? html`<span class="schip on" title="ChatGPT${G.email ? ": " + (G.email) : ""}">${ico("ai")}ChatGPT<a id="cg-out" role="button" tabindex="0" aria-label="Disconnect ChatGPT" title="Disconnect">${ico("x")}</a></span>` : html`<a id="cg-in" class="schip" role="button" tabindex="0" title="Connect ChatGPT">${ico("ai")}ChatGPT</a>`}${(d.google || {}).connected ? html`<span class="schip on" title="Google connected">${ico("google")}Google<a id="gg-out" role="button" tabindex="0" aria-label="Disconnect Google" title="Disconnect">${ico("x")}</a></span>` : html`<a id="gg-in" class="schip" role="button" tabindex="0" title="Connect Google">${ico("google")}Google</a>`}</div></div><div class="src-hda">${list.length ? html`<button class="btn sm" id="src-new" aria-expanded="${String(addOn)}" aria-controls="src-addbox">${ico("plus")} Add</button>` : ""}<button class="ib" id="src-x" aria-label="Close sources" title="Close">${ico("close")}</button></div></header><div class="body">${addBox}${list.length ? html`<div class="src-list">${cards}</div>` : emptyState}</div></aside>`;
|
|
389
371
|
// First visit: offer the ChatGPT plan for social sources, or keep the agent.
|
|
390
372
|
const onb =
|
|
391
373
|
G.use === "ask" && !G.connected
|
|
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>`
|
|
374
|
+
? html`<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
375
|
: "";
|
|
394
376
|
// A rebuild replaces the drawer: keep where it was scrolled to.
|
|
395
377
|
const at = $("src-sec")?.querySelector(".body")?.scrollTop ?? 0;
|
|
396
378
|
$("pbody").innerHTML =
|
|
397
379
|
d && d.error
|
|
398
|
-
? `<p style="color:var(--dim)">${
|
|
380
|
+
? html`<p style="color:var(--dim)">${d.error}</p>`
|
|
399
381
|
: !d
|
|
400
|
-
? `<p style="color:var(--dim)">The icebox could not be read.</p>`
|
|
401
|
-
: onb
|
|
382
|
+
? html`<p style="color:var(--dim)">The icebox could not be read.</p>`
|
|
383
|
+
: html`${onb}${bar}${bulk}${fails}${inbox}${sec}`;
|
|
402
384
|
if (!d || d.error) return;
|
|
403
385
|
const body = $("src-sec")?.querySelector(".body");
|
|
404
386
|
if (body) body.scrollTop = at;
|
|
@@ -993,7 +975,7 @@ export async function openSources() {
|
|
|
993
975
|
if (!dir && !showable().length) return toast("Pick a project first");
|
|
994
976
|
// The new-idea form, cut down: the request, files, dictation and, in All projects, the project.
|
|
995
977
|
const dl = modal(
|
|
996
|
-
`${all ? `<div class="combo" id="ndc"><button type="button" class="sel" id="newdir" aria-label="Project for this request" aria-haspopup="listbox" aria-expanded="false"></button><div class="menu-pop" hidden><input class="sel" aria-label="Search projects" placeholder="Search projects…" spellcheck="false" autocomplete="off"><div id="nds" role="listbox"></div></div></div>` : ""}<textarea id="newidea" placeholder="
|
|
978
|
+
`${all ? `<div class="combo" id="ndc"><button type="button" class="sel" id="newdir" aria-label="Project for this request" aria-haspopup="listbox" aria-expanded="false"></button><div class="menu-pop" hidden><input class="sel" aria-label="Search projects" placeholder="Search projects…" spellcheck="false" autocomplete="off"><div id="nds" role="listbox"></div></div></div>` : ""}<textarea id="newidea" placeholder="What did someone ask for?" aria-label="Request"></textarea><div class="att"></div><input type="file" multiple hidden><div class="row"><button type="button" class="ib" id="newpick" aria-label="Attach files" title="Attach files">${ico("clip")}</button><button type="button" class="btn ghost" value="">Cancel</button><button class="btn pri">Add</button></div>`,
|
|
997
979
|
);
|
|
998
980
|
const f = dl.querySelector("form"),
|
|
999
981
|
inp = f.querySelector("textarea"),
|
package/web/board/keys.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { store } from "./store.ts";
|
|
2
2
|
import { load } from "./api.ts";
|
|
3
|
-
import { focusedKey, key, nav, render } from "./cards.ts";
|
|
3
|
+
import { focusedKey, key, nav, newIdea, render } from "./cards.ts";
|
|
4
4
|
import { $ } from "./dom.ts";
|
|
5
5
|
import { iceNav, srcOpen } from "./icebox.ts";
|
|
6
6
|
import { close, openRun } from "./panel.ts";
|
|
@@ -11,9 +11,10 @@ export function initKeys() {
|
|
|
11
11
|
$("kbd").onclick = () => $("help").classList.add("on");
|
|
12
12
|
$("help").onclick = () => $("help").classList.remove("on");
|
|
13
13
|
document.addEventListener("click", () => {
|
|
14
|
-
if (store.menuKey || store.clearMenu) {
|
|
14
|
+
if (store.menuKey || store.clearMenu || store.sevMenu) {
|
|
15
15
|
store.menuKey = null;
|
|
16
16
|
store.clearMenu = false;
|
|
17
|
+
store.sevMenu = false;
|
|
17
18
|
render();
|
|
18
19
|
}
|
|
19
20
|
});
|
|
@@ -29,7 +30,8 @@ export function initKeys() {
|
|
|
29
30
|
iceNav(e)
|
|
30
31
|
)
|
|
31
32
|
return;
|
|
32
|
-
|
|
33
|
+
// A dialog handles its own keys; Escape there must not also close the panel.
|
|
34
|
+
if (e.target.matches("input,select,textarea,[contenteditable]") || e.target.closest("dialog")) return;
|
|
33
35
|
if (e.key === "?") {
|
|
34
36
|
$("help").classList.toggle("on");
|
|
35
37
|
return;
|
|
@@ -52,10 +54,11 @@ export function initKeys() {
|
|
|
52
54
|
} else close();
|
|
53
55
|
return;
|
|
54
56
|
}
|
|
55
|
-
|
|
57
|
+
// The Terminal page has no cards to move between; its keys are the terminals'.
|
|
58
|
+
if (view === "terminal") return;
|
|
59
|
+
if (e.key === "n" && !document.querySelector("dialog[open]")) {
|
|
56
60
|
if (view !== "board") go("board", true);
|
|
57
|
-
|
|
58
|
-
render();
|
|
61
|
+
newIdea();
|
|
59
62
|
}
|
|
60
63
|
if (e.key === "a") {
|
|
61
64
|
store.all = !store.all;
|
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 {
|