kaizen-agent 1.18.0 → 1.19.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/README.md +2 -0
- package/cli/install.test.ts +56 -1
- package/cli/install.ts +70 -67
- package/package.json +1 -1
- package/web/board.html +20 -7
package/README.md
CHANGED
|
@@ -230,6 +230,8 @@ Anthropic, OpenAI and Requesty are supported. Setup, privacy and settings are in
|
|
|
230
230
|
|
|
231
231
|
In a terminal: `kaizen` (dashboard), `kaizen web` (board), `kaizen settings`, `kaizen upgrade`.
|
|
232
232
|
|
|
233
|
+
Stuck on 1.18.0 or older? If `kaizen upgrade` says *could not pull* and stays on the old version, run this once: `bunx kaizen-agent@latest`. Your old install is kept as `~/kaizen.old`; from then on `kaizen upgrade` works by itself.
|
|
234
|
+
|
|
233
235
|
## Learn more
|
|
234
236
|
|
|
235
237
|
- **[The guide](https://github.com/hfadhlullah/kaizen/blob/main/docs/guide.md)**: the board, request sources, notebook, settings, installer flags, uninstall
|
package/cli/install.test.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// Skipped on Windows: there uninstall also walks every drive (searchRoots) and
|
|
5
5
|
// strips kaizen links from real projects, whatever HOME says.
|
|
6
6
|
import { test, expect } from "bun:test";
|
|
7
|
-
import { mkdtempSync, mkdirSync, writeFileSync, existsSync, rmSync, symlinkSync } from "node:fs";
|
|
7
|
+
import { mkdtempSync, mkdirSync, writeFileSync, existsSync, rmSync, symlinkSync, cpSync, readFileSync } from "node:fs";
|
|
8
8
|
import { join, dirname } from "node:path";
|
|
9
9
|
import { tmpdir } from "node:os";
|
|
10
10
|
|
|
@@ -83,3 +83,58 @@ it.skipIf(!Bun.which("git"))("uninstall removes a clean clone of kaizen and keep
|
|
|
83
83
|
expect(unpushed.left).toBe(true);
|
|
84
84
|
expect(unpushed.out).toContain("commits not pushed");
|
|
85
85
|
});
|
|
86
|
+
|
|
87
|
+
// An npm package of this checkout, at a version no real release has: no .git, so its
|
|
88
|
+
// installer runs as `bunx kaizen-agent` would. Scratch HOME only; ~/kaizen is a fake.
|
|
89
|
+
const fromPackage = (make: (k: string) => void, custom = false) => {
|
|
90
|
+
const root = mkdtempSync(join(tmpdir(), "kz-replace-"));
|
|
91
|
+
const home = join(root, "home"), pkg = join(root, "pkg");
|
|
92
|
+
const k = custom ? join(root, "kaizen") : join(home, "kaizen");
|
|
93
|
+
mkdirSync(home);
|
|
94
|
+
const src = dirname(dirname(import.meta.path));
|
|
95
|
+
for (const d of ["cli", "skills", "agents", "commands", "web", "package.json"]) {
|
|
96
|
+
cpSync(join(src, d), join(pkg, d), { recursive: true, filter: (p) => !p.includes("node_modules") });
|
|
97
|
+
}
|
|
98
|
+
const pj = join(pkg, "package.json");
|
|
99
|
+
writeFileSync(pj, readFileSync(pj, "utf8").replace(/"version": "[^"]*"/, '"version": "9.9.9"'));
|
|
100
|
+
make(k);
|
|
101
|
+
const env: Record<string, string> = { PATH: process.env.PATH ?? "", HOME: home, USERPROFILE: home, TMPDIR: root };
|
|
102
|
+
if (custom) env.KAIZEN_HOME = k;
|
|
103
|
+
const p = Bun.spawnSync([process.execPath, join(pkg, "cli", "install.ts"), "--yes", "--global"], { cwd: home, env });
|
|
104
|
+
const r = {
|
|
105
|
+
out: p.stdout.toString(),
|
|
106
|
+
version: existsSync(join(k, "package.json")) ? JSON.parse(readFileSync(join(k, "package.json"), "utf8")).version : null,
|
|
107
|
+
oldKept: existsSync(join(`${k}.old`, "mine.txt")),
|
|
108
|
+
mineInPlace: existsSync(join(k, "mine.txt")),
|
|
109
|
+
};
|
|
110
|
+
rmSync(root, { recursive: true, force: true });
|
|
111
|
+
return r;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
it.skipIf(!Bun.which("git"))("an npm package replaces a ~/kaizen clone with local edits, keeping it as .old", () => {
|
|
115
|
+
const r = fromPackage(clone(REPO, true));
|
|
116
|
+
expect(r.version).toBe("9.9.9");
|
|
117
|
+
expect(r.mineInPlace).toBe(false);
|
|
118
|
+
expect(r.oldKept).toBe(true);
|
|
119
|
+
expect(r.out).toContain("replaced");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it.skipIf(!Bun.which("git"))("an npm package never replaces a development checkout", () => {
|
|
123
|
+
expect(fromPackage(clone(REPO, true), true).mineInPlace).toBe(true);
|
|
124
|
+
const unpushed = fromPackage(clone(REPO, true, commit));
|
|
125
|
+
expect(unpushed.mineInPlace).toBe(true);
|
|
126
|
+
expect(unpushed.out).toContain("commits not pushed");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("upgrade with npm unreachable says not upgraded, never up to date", () => {
|
|
130
|
+
const root = mkdtempSync(join(tmpdir(), "kz-offline-"));
|
|
131
|
+
const home = join(root, "home");
|
|
132
|
+
mkdirSync(home);
|
|
133
|
+
kaizenFiles(join(home, "kaizen"));
|
|
134
|
+
const env = { PATH: process.env.PATH ?? "", HOME: home, TMPDIR: root, HTTPS_PROXY: "http://127.0.0.1:9", https_proxy: "http://127.0.0.1:9" };
|
|
135
|
+
const p = Bun.spawnSync([process.execPath, script, "upgrade"], { cwd: home, env });
|
|
136
|
+
rmSync(root, { recursive: true, force: true });
|
|
137
|
+
expect(p.exitCode).not.toBe(0);
|
|
138
|
+
expect(p.stdout.toString()).toContain("Not upgraded");
|
|
139
|
+
expect(p.stdout.toString()).not.toContain("Up to date");
|
|
140
|
+
});
|
package/cli/install.ts
CHANGED
|
@@ -407,35 +407,40 @@ if (interactive && !upgrade && Bun.argv.length === 2) {
|
|
|
407
407
|
|
|
408
408
|
if (upgrade) await clearBunxCache();
|
|
409
409
|
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
|
|
414
|
-
|
|
410
|
+
// Upgrade means the newest release on npm, whatever the install on disk is: a clone
|
|
411
|
+
// with local edits used to fail its pull and still report "Up to date". The npm
|
|
412
|
+
// package's own installer replaces the install (see resolveRepo), so hand it over.
|
|
413
|
+
if (upgrade) {
|
|
414
|
+
const dest = installDir();
|
|
415
|
+
const now = versionOf(dest);
|
|
416
|
+
const why = await devCheckout(dest);
|
|
417
|
+
if (why) {
|
|
418
|
+
console.log(`\n ${c.bold("Not upgraded")} ${c.dim(`${tilde(dest)} is ${why}; update it with git`)}\n`);
|
|
419
|
+
process.exit(1);
|
|
420
|
+
}
|
|
415
421
|
const latest = await npmLatest();
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
if (latest
|
|
421
|
-
|
|
422
|
-
step(`fetching kaizen ${want} from npm`);
|
|
423
|
-
// --force so a stale package manifest cannot resolve the version away, and the
|
|
424
|
-
// failure is printed rather than swallowed: a silent catch here looked exactly
|
|
425
|
-
// like a successful upgrade that changed nothing.
|
|
426
|
-
const r = await Bun.$`${process.execPath} x --force kaizen-agent@${want} --yes`
|
|
427
|
-
.quiet().nothrow();
|
|
428
|
-
const out = clean(r.stdout.toString() + r.stderr.toString()).filter((l) => l.trim());
|
|
429
|
-
for (const l of out) console.log(l.startsWith(" ") ? l : ` ${l}`);
|
|
430
|
-
if (r.exitCode !== 0) {
|
|
431
|
-
console.log(`\n ${c.bold(`could not fetch kaizen ${want}`)}`);
|
|
432
|
-
console.log(` ${c.dim("try:")} ${c.cyan(`bunx kaizen-agent@${want}`)}\n`);
|
|
433
|
-
process.exit(1);
|
|
434
|
-
}
|
|
435
|
-
const after = versionOf(process.env.KAIZEN_HOME ?? join(home, "kaizen"));
|
|
436
|
-
if (after === now) console.log(`\n ${c.bold(`kaizen ${now} is the latest on npm`)}\n`);
|
|
422
|
+
if (!latest) {
|
|
423
|
+
console.log(`\n ${c.bold("Not upgraded")} ${c.dim(`could not reach npm (kaizen ${now})`)}\n`);
|
|
424
|
+
process.exit(1);
|
|
425
|
+
}
|
|
426
|
+
if (latest === now) {
|
|
427
|
+
console.log(`\n ${c.bold("Up to date")}${c.dim(` — kaizen ${now}`)}\n`);
|
|
437
428
|
process.exit(0);
|
|
438
429
|
}
|
|
430
|
+
step(`fetching kaizen ${latest} from npm`);
|
|
431
|
+
// --force so a stale package manifest cannot resolve the version away, and the
|
|
432
|
+
// failure is printed rather than swallowed.
|
|
433
|
+
const r = await Bun.$`${process.execPath} x --force kaizen-agent@${latest} --yes`.quiet().nothrow();
|
|
434
|
+
const out = clean(r.stdout.toString() + r.stderr.toString()).filter((l) => l.trim());
|
|
435
|
+
for (const l of out) console.log(l.startsWith(" ") ? l : ` ${l}`);
|
|
436
|
+
const after = versionOf(dest);
|
|
437
|
+
if (r.exitCode !== 0 || after !== latest) {
|
|
438
|
+
console.log(`\n ${c.bold(`could not install kaizen ${latest}`)}`);
|
|
439
|
+
console.log(` ${c.dim("try:")} ${c.cyan(`bunx kaizen-agent@${latest}`)}\n`);
|
|
440
|
+
process.exit(1);
|
|
441
|
+
}
|
|
442
|
+
console.log(`\n ${c.bold("Upgraded")} ${c.dim(now)} → ${c.green(after)}\n ${c.dim("Restart Claude Code to pick up any new slash commands.")}\n`);
|
|
443
|
+
process.exit(0);
|
|
439
444
|
}
|
|
440
445
|
if (!check && !upgrade) await welcome();
|
|
441
446
|
|
|
@@ -597,41 +602,34 @@ async function resolveRepoQuietly() {
|
|
|
597
602
|
|
|
598
603
|
async function resolveRepo() {
|
|
599
604
|
const here = dirname(import.meta.dir);
|
|
600
|
-
const dest =
|
|
601
|
-
//
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
step(`updated ${tilde(dir)}`);
|
|
612
|
-
} catch {
|
|
613
|
-
step(`kept ${tilde(dir)} ${c.dim("(could not pull)")}`);
|
|
605
|
+
const dest = installDir();
|
|
606
|
+
// Running from a checkout (development, or the launcher's old clone): link it as is.
|
|
607
|
+
if (existsSync(join(here, ".git"))) return here;
|
|
608
|
+
// Running from an npm package: that package is the version wanted, so it replaces
|
|
609
|
+
// whatever ~/kaizen holds -- local edits included -- rather than a pull that any
|
|
610
|
+
// edit makes fail. The previous install is kept as <dest>.old, one deep.
|
|
611
|
+
if (here !== dest && existsSync(dest)) {
|
|
612
|
+
const why = await devCheckout(dest);
|
|
613
|
+
if (why) {
|
|
614
|
+
step(`kept ${tilde(dest)} ${c.dim(`(${why}; not replaced)`)}`);
|
|
615
|
+
return dest;
|
|
614
616
|
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
try {
|
|
617
|
+
const fresh = `${dest}.new`, old = `${dest}.old`;
|
|
618
|
+
rmSync(fresh, { recursive: true, force: true });
|
|
619
|
+
cpSync(here, fresh, { recursive: true, dereference: true, filter: (src) => basename(src) !== "node_modules" });
|
|
620
|
+
rmSync(old, { recursive: true, force: true });
|
|
621
|
+
// Windows refuses to move a folder a running kaizen still holds open. The
|
|
622
|
+
// install stays as it was; say how to finish rather than throwing.
|
|
623
|
+
try { renameSync(dest, old); } catch {
|
|
623
624
|
rmSync(fresh, { recursive: true, force: true });
|
|
624
|
-
|
|
625
|
-
rmSync(dest, { recursive: true, force: true });
|
|
626
|
-
renameSync(fresh, dest);
|
|
627
|
-
step(`re-cloned ${tilde(dest)} ${c.dim("(git is available now)")}`);
|
|
625
|
+
step(`kept ${tilde(dest)} ${c.dim("(in use; close kaizen and any dashboard, then run bunx kaizen-agent@latest)")}`);
|
|
628
626
|
return dest;
|
|
629
|
-
} catch {
|
|
630
|
-
rmSync(fresh, { recursive: true, force: true });
|
|
631
|
-
// Fall through to the copy below: an unreachable network is not a reason to
|
|
632
|
-
// leave the install untouched.
|
|
633
627
|
}
|
|
628
|
+
renameSync(fresh, dest);
|
|
629
|
+
step(`replaced ${tilde(dest)} with kaizen ${versionOf(dest)} ${c.dim(`(previous install kept in ${tilde(old)})`)}`);
|
|
630
|
+
return dest;
|
|
634
631
|
}
|
|
632
|
+
if (here === dest) return dest;
|
|
635
633
|
|
|
636
634
|
// Cloning needs an empty directory, and a copy made on a machine that had no git
|
|
637
635
|
// is anything but: installing git later must not turn every run into a fatal.
|
|
@@ -906,14 +904,7 @@ function writePointer() {
|
|
|
906
904
|
|
|
907
905
|
// ---------------------------------------------------------------- done
|
|
908
906
|
|
|
909
|
-
|
|
910
|
-
let version = "";
|
|
911
|
-
try { version = JSON.parse(readFileSync(join(repo, "package.json"), "utf8")).version; } catch {}
|
|
912
|
-
console.log(`
|
|
913
|
-
${c.bold("Up to date")}${version ? c.dim(` — kaizen ${version}`) : ""}
|
|
914
|
-
${c.dim("Restart Claude Code to pick up any new slash commands.")}
|
|
915
|
-
`);
|
|
916
|
-
} else {
|
|
907
|
+
{
|
|
917
908
|
console.log(`
|
|
918
909
|
${c.bold("Next")}
|
|
919
910
|
1 restart Claude Code ${c.dim("— skills load live, slash commands only at session start")}
|
|
@@ -948,14 +939,26 @@ async function runUpgrade(repoDir: string) {
|
|
|
948
939
|
.map((l) => c.dim(l.trim())),
|
|
949
940
|
];
|
|
950
941
|
if (latest && latest !== after) {
|
|
951
|
-
lines.push("", c.dim(
|
|
952
|
-
? `npm publishes ${latest}; this install follows the git clone.`
|
|
953
|
-
: `npm publishes ${latest} — run: bunx kaizen-agent@${latest}`));
|
|
942
|
+
lines.push("", c.dim(`npm publishes ${latest} — run: bunx kaizen-agent@${latest}`));
|
|
954
943
|
}
|
|
955
944
|
lines.push("", c.dim("Restart your agent to pick up new slash commands."));
|
|
956
945
|
return lines;
|
|
957
946
|
}
|
|
958
947
|
|
|
948
|
+
function installDir() { return process.env.KAIZEN_HOME ?? join(home, "kaizen"); }
|
|
949
|
+
|
|
950
|
+
// A folder someone develops kaizen in must never be replaced by a release: a git
|
|
951
|
+
// checkout KAIZEN_HOME points at, or any checkout holding commits on no remote.
|
|
952
|
+
// ~/kaizen as the installer cloned it is fair game, edits and all (kept in .old).
|
|
953
|
+
async function devCheckout(dir: string) {
|
|
954
|
+
if (!existsSync(join(dir, ".git"))) return "";
|
|
955
|
+
if (resolve(dir) !== join(home, "kaizen")) return "a git checkout named by KAIZEN_HOME";
|
|
956
|
+
try {
|
|
957
|
+
const ahead = (await Bun.$`git -C ${dir} rev-list --branches --not --remotes`.quiet().text()).trim();
|
|
958
|
+
return ahead ? "a git checkout with commits not pushed" : "";
|
|
959
|
+
} catch { return ""; }
|
|
960
|
+
}
|
|
961
|
+
|
|
959
962
|
function versionOf(dir: string) {
|
|
960
963
|
try { return JSON.parse(readFileSync(join(dir, "package.json"), "utf8")).version as string; }
|
|
961
964
|
catch { return "?"; }
|
package/package.json
CHANGED
package/web/board.html
CHANGED
|
@@ -37,11 +37,12 @@ select.sel,button.sel{cursor:pointer;appearance:none;padding-right:30px;max-widt
|
|
|
37
37
|
.combo .menu-pop{left:0;right:auto;top:calc(100% + 4px);width:320px;max-width:calc(100vw - 32px);gap:6px;z-index:9}
|
|
38
38
|
.combo[hidden],.combo .menu-pop[hidden]{display:none}
|
|
39
39
|
.combo input.sel{width:100%;max-width:none}
|
|
40
|
-
#
|
|
41
|
-
#projs
|
|
42
|
-
#projs button
|
|
40
|
+
#ndc>.sel{width:100%;max-width:none;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
41
|
+
#projs,#nds{display:flex;flex-direction:column;max-height:300px;overflow-y:auto}
|
|
42
|
+
#projs button,#nds button{display:block;flex:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
43
|
+
#projs button.on,#nds button.on{background:var(--hover)}
|
|
43
44
|
#shows{display:flex;flex-direction:column;max-height:300px;overflow-y:auto}#shows label{display:flex;gap:8px;align-items:center;padding:6px 10px;font-size:13px;border-radius:var(--r-s);cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}#shows label:hover{background:var(--hover)}#shows .grp{padding:8px 10px 4px;font-size:11px;color:var(--dim)}
|
|
44
|
-
#projs .grp{padding:8px 10px 4px;font-size:11px;color:var(--dim)}
|
|
45
|
+
#projs .grp,#nds .grp{padding:8px 10px 4px;font-size:11px;color:var(--dim)}
|
|
45
46
|
.sp{flex:1}
|
|
46
47
|
.ib{width:34px;height:34px;display:grid;place-items:center;color:var(--dim);border-radius:var(--r-s);transition:background .12s,color .12s}
|
|
47
48
|
.ib:hover{color:var(--ink);background:var(--hover)}
|
|
@@ -629,9 +630,9 @@ function render(){
|
|
|
629
630
|
s.querySelectorAll("[data-p]").forEach(bt=>bt.onclick=async e=>{e.stopPropagation();if(bt.dataset.p==="cancel"){picking=false;picked.clear();render();return}
|
|
630
631
|
const set=list.filter(c=>picked.has(key(c)));picking=false;picked.clear();for(const c of set)await post("/archive",{dir:c.state,id:c.id});toast("Archived "+set.length);load()});}
|
|
631
632
|
const ul=document.createElement("ul");
|
|
632
|
-
if(ci===0&&mode==="new"){const f=document.createElement("li");f.className="inl";f.innerHTML
|
|
633
|
-
const inp=f.querySelector("
|
|
634
|
-
f.querySelector("#ns").onclick=save;f.querySelector("#nc").onclick=()=>{mode=null;render()};{const m=mic([inp,f.querySelector("textarea")]);if(m)f.querySelector(".row").prepend(m)}inp.onkeydown=e=>{if(e.key==="Enter")save();if(e.key==="Escape"){mode=null;render()}};setTimeout(()=>inp.focus())}
|
|
633
|
+
if(ci===0&&mode==="new"){const f=document.createElement("li");f.className="inl";f.innerHTML=`${all?`<div class="combo" id="ndc"><button class="sel" id="newdir" aria-label="Project for this idea" 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>`:""}<input id="newidea" placeholder="What would you like done?"><textarea id="newnotes" placeholder="Notes: links, paths, background (optional)"></textarea><div class="row"><button class="btn ghost" id="nc">Cancel</button><button class="btn pri" id="ns">Add idea</button></div>`;ul.appendChild(f);
|
|
634
|
+
const inp=f.querySelector("#newidea");draft(inp,"f:new:0");draft(f.querySelector("textarea"),"f:new:1");const save=async()=>{const text=inp.value.trim();if(!text)return;const notes=f.querySelector("textarea").value.trim();mode=null;const dir=all?newDir:null;const r=await post("/inbox",dir?{op:"add",dir,text,notes}:{op:"add",text,notes});if(r.ok)toast("Idea added");load()};
|
|
635
|
+
if(all)ideaCombo(f);f.querySelector("#ns").onclick=save;f.querySelector("#nc").onclick=()=>{mode=null;render()};{const m=mic([inp,f.querySelector("textarea")]);if(m)f.querySelector(".row").prepend(m)}inp.onkeydown=e=>{if(e.key==="Enter")save();if(e.key==="Escape"){mode=null;render()}};setTimeout(()=>inp.focus())}
|
|
635
636
|
const shown=unfolded.has(ci)||list.length<=FOLD+1?list:list.slice(0,FOLD);
|
|
636
637
|
shown.forEach(c=>{if(modeKey===key(c)&&mode){ul.appendChild(inlineForm(c));return}const li=document.createElement("li");li.appendChild(card(c));ul.appendChild(li)});
|
|
637
638
|
if(shown.length<list.length){const li=document.createElement("li");li.innerHTML=`<button class="more-btn">Show ${list.length-shown.length} more</button>`;li.firstChild.onclick=()=>{unfolded.add(ci);render()};ul.appendChild(li)}
|
|
@@ -1021,6 +1022,18 @@ async function pick(v){
|
|
|
1021
1022
|
// The all-projects filter: tick the projects to show; none ticked shows every one. Kept per browser.
|
|
1022
1023
|
let shown=new Set((()=>{try{return JSON.parse(localStorage.getItem("kz-show"))||[]}catch(e){return[]}})());
|
|
1023
1024
|
// Known projects, plus any folder a card comes from that is not one (the global inbox).
|
|
1025
|
+
// New idea in All projects: the projects being shown, else every one.
|
|
1026
|
+
let newDir=null;
|
|
1027
|
+
// Same combo as the header's project picker: a button, then a searchable list.
|
|
1028
|
+
function ideaCombo(f){const ps=ideaDirs(),btn=f.querySelector("#newdir"),pp=f.querySelector("#ndc .menu-pop"),q=pp.querySelector("input"),box=f.querySelector("#nds");
|
|
1029
|
+
if(!ps.some(p=>p.dir===newDir))newDir=ps[0]?.dir??null;
|
|
1030
|
+
const name=()=>{btn.textContent=(ps.find(p=>p.dir===newDir)||{label:"Choose project"}).label};
|
|
1031
|
+
const draw=()=>{const t=q.value.trim().toLowerCase();box.innerHTML=ps.filter(p=>p.label.toLowerCase().includes(t)).map(p=>`<button type="button" role="option" data-d="${esc(p.dir)}" title="${esc(p.label)}" class="${p.dir===newDir?"on":""}">${esc(p.label)}</button>`).join("")||`<div class="grp">No match</div>`;
|
|
1032
|
+
box.querySelectorAll("button").forEach(b=>b.onclick=()=>{newDir=b.dataset.d;name();open(false);f.querySelector("#newidea").focus()})};
|
|
1033
|
+
const open=on=>{pp.hidden=!on;btn.setAttribute("aria-expanded",on);if(on){q.value="";draw();q.focus()}};
|
|
1034
|
+
btn.onclick=e=>{e.preventDefault();open(pp.hidden)};q.oninput=draw;q.onkeydown=e=>{if(e.key==="Escape"){e.stopPropagation();open(false)}if(e.key==="Enter"){e.preventDefault();box.querySelector("button")?.click()}};
|
|
1035
|
+
name()}
|
|
1036
|
+
function ideaDirs(){const sp=showable(),pick=sp.filter(p=>shown.has(p.dir));return pick.length?pick:sp}
|
|
1024
1037
|
function showable(){const l=[...S.projects];for(const c of S.cards)if(!l.some(p=>p.dir===c.state))l.push({dir:c.state,label:c.where});return l}
|
|
1025
1038
|
let SP=[];
|
|
1026
1039
|
function showList(){const q=$("showq").value.trim().toLowerCase();SP=showable();
|