kaizen-agent 1.42.2 → 1.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/backlog.test.ts +133 -0
- package/cli/board.test.ts +7 -2
- package/cli/html.test.ts +19 -0
- package/cli/notes.ts +2 -6
- package/cli/state.test.ts +61 -2
- package/cli/state.ts +219 -45
- package/cli/web.ts +58 -15
- package/package.json +1 -1
- package/web/board/approvals.ts +3 -3
- package/web/board/cards.ts +240 -107
- 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 +51 -69
- package/web/board/keys.ts +7 -6
- package/web/board/main.ts +1 -0
- package/web/board/panel.ts +22 -16
- package/web/board/projects.ts +9 -14
- package/web/board/settings.ts +18 -22
- package/web/board/store.ts +2 -0
- package/web/board/update.ts +7 -7
- package/web/board.html +12 -0
- package/web/board.js +23 -23
- package/web/notebook.js +1 -1
- package/web/term.js +1 -1
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="feat(scope): 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;
|
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,9 @@ export function initKeys() {
|
|
|
52
54
|
} else close();
|
|
53
55
|
return;
|
|
54
56
|
}
|
|
55
|
-
if (e.key === "n") {
|
|
57
|
+
if (e.key === "n" && !document.querySelector("dialog[open]")) {
|
|
56
58
|
if (view !== "board") go("board", true);
|
|
57
|
-
|
|
58
|
-
render();
|
|
59
|
+
newIdea();
|
|
59
60
|
}
|
|
60
61
|
if (e.key === "a") {
|
|
61
62
|
store.all = !store.all;
|
package/web/board/main.ts
CHANGED
package/web/board/panel.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { APPROVAL, answer, confirmAnswers, confirmRevise, decide, decided, resum
|
|
|
5
5
|
import { cardEl, key, render } from "./cards.ts";
|
|
6
6
|
import { chat } from "./chat.ts";
|
|
7
7
|
import { stopDictation } from "./dictation.ts";
|
|
8
|
-
import { $,
|
|
8
|
+
import { $, html, raw } from "./dom.ts";
|
|
9
9
|
import { fixForm, fixOff } from "./fixes.ts";
|
|
10
10
|
import { commitPush, confirmCommit, gitBtn } from "./git.ts";
|
|
11
11
|
import { ICON, WORD, need } from "./labels.ts";
|
|
@@ -51,7 +51,7 @@ export function open(c, keepTab) {
|
|
|
51
51
|
$("pterm").hidden = true;
|
|
52
52
|
if (c.kind === "idea") {
|
|
53
53
|
store.runDoc = null;
|
|
54
|
-
$("psub").innerHTML = `<span class="pst"><span class="st d-idea"><svg><use href="#s-idea"/></svg></span>Idea</span><span class="pst dim">${
|
|
54
|
+
$("psub").innerHTML = html`<span class="pst"><span class="st d-idea"><svg><use href="#s-idea"/></svg></span>Idea</span><span class="pst dim">${c.where.split(/[\\/]/).pop() || c.where}</span>`;
|
|
55
55
|
$("ptabs").innerHTML = "";
|
|
56
56
|
ideaBody(c);
|
|
57
57
|
const foot = $("pfoot");
|
|
@@ -97,7 +97,7 @@ function showProgress(c) {
|
|
|
97
97
|
st: [...src.matchAll(/^\s*-\s*(todo|doing|done):/gm)].map((m) => m[1]),
|
|
98
98
|
w: +/width:(\d+)%/.exec(h)[1],
|
|
99
99
|
};
|
|
100
|
-
$("pbody").innerHTML = h
|
|
100
|
+
$("pbody").innerHTML = html`${raw(h)}${store.runDoc.backlog ? html`<h3>Backlog</h3>${raw(md(store.runDoc.backlog, true))}` : ""}`;
|
|
101
101
|
const bar = $("pbody").querySelector(".pbar i");
|
|
102
102
|
if (prev && prev.w !== lastProg.w) {
|
|
103
103
|
bar.style.width = prev.w + "%";
|
|
@@ -129,9 +129,9 @@ export async function openRun(c, refresh, again) {
|
|
|
129
129
|
if (refresh && ta && document.activeElement === ta) return;
|
|
130
130
|
// The board's own status mark, then what it waits on: chips, not a dotted line.
|
|
131
131
|
$("psub").innerHTML =
|
|
132
|
-
`<span class="pst"><span class="st d-${ICON[c.status] || "idea"}"><svg><use href="#s-${ICON[c.status] || "idea"}"/></svg></span>${
|
|
133
|
-
(c.awaiting ? `<span class="pst dim">${
|
|
134
|
-
(c.parent ? `<span class="pst link" title="Filed by this run">${
|
|
132
|
+
html`<span class="pst"><span class="st d-${ICON[c.status] || "idea"}"><svg><use href="#s-${ICON[c.status] || "idea"}"/></svg></span>${cap(WORD[c.status] || c.status)}</span>` +
|
|
133
|
+
(c.awaiting ? html`<span class="pst dim">${cap(need(c.awaiting))}</span>` : "") +
|
|
134
|
+
(c.parent ? html`<span class="pst link" title="Filed by this run">${c.parent}</span>` : "");
|
|
135
135
|
// Only tabs with something in them show. A card whose agent runs in the board gets its
|
|
136
136
|
// terminal behind the header's terminal button, chosen once right after a launch from here.
|
|
137
137
|
const tabs = TABS.filter(
|
|
@@ -162,7 +162,7 @@ export async function openRun(c, refresh, again) {
|
|
|
162
162
|
openRun(c, true);
|
|
163
163
|
};
|
|
164
164
|
$("ptabs").innerHTML = tabs
|
|
165
|
-
.map((t) => `<button class="${t[1] === store.tab ? "on" : ""}" data-k="${t[1]}">${t[0]}</button>`)
|
|
165
|
+
.map((t) => html`<button class="${t[1] === store.tab ? "on" : ""}" data-k="${t[1]}">${t[0]}</button>`)
|
|
166
166
|
.join("");
|
|
167
167
|
[...$("ptabs").children].forEach(
|
|
168
168
|
(b) =>
|
|
@@ -180,15 +180,15 @@ export async function openRun(c, refresh, again) {
|
|
|
180
180
|
chat(c, doc, ro);
|
|
181
181
|
} else
|
|
182
182
|
$("pbody").innerHTML = doc
|
|
183
|
-
? md(doc, k === "backlog")
|
|
184
|
-
: `<p style="color:var(--dim)">No ${{ request: "request", plan: "plan", impl: "work report", preview: "preview", review: "review", backlog: "backlog" }[k]} yet.</p>`;
|
|
183
|
+
? raw(md(doc, k === "backlog"))
|
|
184
|
+
: html`<p style="color:var(--dim)">No ${{ request: "request", plan: "plan", impl: "work report", preview: "preview", review: "review", backlog: "backlog" }[k]} yet.</p>`;
|
|
185
185
|
if (k === "backlog") showProgress(c);
|
|
186
186
|
if (k === "review") fixForm(c);
|
|
187
187
|
// The notebook's notes that link here, each opening that note: above the run's own notes.
|
|
188
188
|
if (k === "notes" && store.runDoc.linkedFrom && store.runDoc.linkedFrom.length)
|
|
189
189
|
$("pbody").insertAdjacentHTML(
|
|
190
190
|
"afterbegin",
|
|
191
|
-
`<h3>Linked from notes</h3><ul class="lnk">${store.runDoc.linkedFrom.map((n) => `<li><a href="/notebook?dir=${encodeURIComponent(c.state)}&note=${encodeURIComponent(n)}">${
|
|
191
|
+
html`<h3>Linked from notes</h3><ul class="lnk">${store.runDoc.linkedFrom.map((n) => html`<li><a href="/notebook?dir=${encodeURIComponent(c.state)}&note=${encodeURIComponent(n)}">${n}</a></li>`)}</ul>`,
|
|
192
192
|
);
|
|
193
193
|
// Only the panel the link asked for: another card opened meanwhile drops the mark.
|
|
194
194
|
if (hit && !refresh) {
|
|
@@ -209,7 +209,7 @@ export async function openRun(c, refresh, again) {
|
|
|
209
209
|
qs = pq || [];
|
|
210
210
|
// Its in-card agent ended on a waiting or done run: reopen that conversation, nothing sent.
|
|
211
211
|
const rs = c.pty && !c.pty.alive ? `<button class="btn" data-a="resume">Resume session</button>` : "";
|
|
212
|
-
if (c.archived) foot.innerHTML = arch;
|
|
212
|
+
if (c.archived) foot.innerHTML = raw(arch);
|
|
213
213
|
else if ((c.status === "waiting" || c.awaiting === "findings") && APPROVAL[c.awaiting]) {
|
|
214
214
|
const sent = decided.has(answer(c)),
|
|
215
215
|
asks = !pq
|
|
@@ -221,13 +221,13 @@ export async function openRun(c, refresh, again) {
|
|
|
221
221
|
const more =
|
|
222
222
|
gitBtn(c).replace(/class="btn( pri)?"/g, "") +
|
|
223
223
|
`<button data-a="archive">Archive</button><hr><button data-a="ab" class="danger">Abandon</button>`;
|
|
224
|
-
foot.innerHTML = `<span class="note">${sent ? "Sent to your agent." : APPROVAL[c.awaiting] + asks + " Answering here opens a new agent session."}</span><details class="more"><summary class="btn ghost">More</summary><div class="menu-pop">${more}</div></details>${rs}<span class="dec"><button class="btn" data-a="revise"${sent ? " disabled" : ""}>Revise</button><button class="btn pri" data-a="approve"${sent ? " disabled" : ""}>${c.awaiting === "findings" ? "Approve as-is" : "Approve"}</button></span>`;
|
|
224
|
+
foot.innerHTML = html`<span class="note">${sent ? "Sent to your agent." : APPROVAL[c.awaiting] + asks + " Answering here opens a new agent session."}</span><details class="more"><summary class="btn ghost">More</summary><div class="menu-pop">${raw(more)}</div></details>${raw(rs)}<span class="dec"><button class="btn" data-a="revise"${sent ? " disabled" : ""}>Revise</button><button class="btn pri" data-a="approve"${sent ? " disabled" : ""}>${c.awaiting === "findings" ? "Approve as-is" : "Approve"}</button></span>`;
|
|
225
225
|
} else if (c.status === "waiting")
|
|
226
|
-
foot.innerHTML = `<span class="note">Waiting on you: ${
|
|
227
|
-
else if (c.status === "done") foot.innerHTML = arch + rs;
|
|
228
|
-
else if (c.status === "abandoned") foot.innerHTML = arch;
|
|
226
|
+
foot.innerHTML = html`<span class="note">Waiting on you: ${need(c.awaiting)}. Answer in the ${c.pty && c.pty.alive ? "terminal (button at the top)" : "terminal running this run"}.</span>${raw(arch)}<button class="btn ghost" data-a="ab">Abandon</button>${raw(rs)}`;
|
|
227
|
+
else if (c.status === "done") foot.innerHTML = raw(arch + rs);
|
|
228
|
+
else if (c.status === "abandoned") foot.innerHTML = raw(arch);
|
|
229
229
|
else
|
|
230
|
-
foot.innerHTML = `<span class="note">${c.pty && !c.pty.alive ? "Its agent ended before the stage finished. Resume continues it here." : c.status === "stalled" ? "No progress for a while. Resume it, or abandon it." : c.pty ? "" : "Running in your agent."}</span>${arch}<button class="btn ghost" data-a="ab">Abandon run</button>${resumable(c) ? `<button class="btn pri" data-a="resume">Resume</button>` : ""}`;
|
|
230
|
+
foot.innerHTML = html`<span class="note">${c.pty && !c.pty.alive ? "Its agent ended before the stage finished. Resume continues it here." : c.status === "stalled" ? "No progress for a while. Resume it, or abandon it." : c.pty ? "" : "Running in your agent."}</span>${raw(arch)}<button class="btn ghost" data-a="ab">Abandon run</button>${resumable(c) ? html`<button class="btn pri" data-a="resume">Resume</button>` : ""}`;
|
|
231
231
|
foot.querySelectorAll("[data-a]").forEach(
|
|
232
232
|
(b) =>
|
|
233
233
|
(b.onclick = () => {
|
|
@@ -290,6 +290,12 @@ export function deep() {
|
|
|
290
290
|
hit = m ? { type: m[1], n: +m[2], k: key(c) } : null;
|
|
291
291
|
open(c, !!t);
|
|
292
292
|
}
|
|
293
|
+
// A run's panel on the tab of one of its items, that item scrolled to and marked.
|
|
294
|
+
export function openAt(c, type, n) {
|
|
295
|
+
store.tab = type === "finding" ? "review" : "backlog";
|
|
296
|
+
hit = { type, n, k: key(c) };
|
|
297
|
+
open(c, true);
|
|
298
|
+
}
|
|
293
299
|
// What md() leaves of a line as text: no backticks or stars, a link reduced to its words.
|
|
294
300
|
function mark(h) {
|
|
295
301
|
hit = null;
|