kaizen-agent 1.28.0 → 1.30.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/web/board.html CHANGED
@@ -197,7 +197,7 @@ aside.wide{width:min(960px,100%)}#pbody .term{height:100%;min-height:320px;backg
197
197
 
198
198
  /* footer */
199
199
  .ftr{display:flex;justify-content:space-between;gap:16px;padding:6px 28px;border-top:1px solid var(--line);color:var(--dim);font-size:12px;flex-wrap:wrap;align-items:center}
200
- .ftr span+span,.ftr a+a{margin-left:14px}.ftr .strip{margin-right:14px;vertical-align:middle;font-size:12px}
200
+ .ftr span+span,.ftr a+a{margin-left:14px}.ftr .strip{font-size:12px}.ftr .strip .st{display:inline-grid}
201
201
  /* The footer is out of the eye's path, so the waiting alert is filled, not just outlined. */
202
202
  .ftr .strip .attn{color:var(--ink);background:color-mix(in srgb,var(--warn) 18%,transparent);box-shadow:inset 0 0 0 1px var(--warn)}.ftr .strip .attn:hover,.ftr .strip .attn.on{background:color-mix(in srgb,var(--warn) 30%,transparent)}.ftr .strip .attn svg{color:var(--warn)}.ftr .st svg{width:12px;height:12px}
203
203
  .ftr a{color:var(--dim);cursor:pointer;white-space:nowrap;text-decoration:none}.ftr a:hover{color:var(--ink)}
@@ -486,7 +486,7 @@ body:not(.paged) .src-sec,body:not(.paged) .src-scrim{display:none}
486
486
  </div>
487
487
  <main id="board" aria-live="polite"></main>
488
488
 
489
- <div class="ftr"><span id="legend"></span><span><span class="strip" id="strip"></span><a href="/issue" target="_blank" rel="noopener">Report an issue</a><a id="kbd">Keyboard shortcuts</a></span></div>
489
+ <div class="ftr"><span class="strip" id="strip"></span><span><a href="/issue" target="_blank" rel="noopener">Report an issue</a><a id="kbd">Keyboard shortcuts</a></span></div>
490
490
 
491
491
  <div class="scrim" id="scrim"></div>
492
492
  <aside id="panel" aria-label="detail">
@@ -745,10 +745,11 @@ function render(){
745
745
  const nArch=S.cards.filter(c=>c.archived).length;
746
746
  const n=s=>cs.filter(c=>c.status===s).length;
747
747
  const strip=[];
748
- if(n("waiting"))strip.push(`<button class="attn ${filter==="waiting"?"on":""}" data-f="waiting"><svg><use href="#s-wait"/></svg>${n("waiting")} waiting on you</button>`);
749
- strip.push(`<button class="${filter==="running"?"on":""}" data-f="running"><span class="d-run"><span class="pulse"></span></span>${n("running")} running</button>`);
750
- if(n("stalled"))strip.push(`<button class="${filter==="stalled"?"on":""}" data-f="stalled"><svg><use href="#s-stall"/></svg>${n("stalled")} stalled</button>`);
751
- strip.push(`<span>${cs.filter(c=>c.kind==="idea").length} ideas · ${cs.filter(c=>c.kind==="run").length} runs</span>`,`<button class="${archive?"on":""}" data-f="__archive">${archive?"Back to board":"Archive "+nArch}</button>`);
748
+ // One chip per status on the board: its mark, its count, and a click filters to it. Running always shows.
749
+ for(const st of ["waiting","running","stalled","starting","idea","done","abandoned"]){const k=n(st);if(!k&&st!=="running")continue;
750
+ const mark=st==="running"?`<span class="d-run"><span class="pulse"></span></span>`:`<span class="st d-${ICON[st]}"><svg><use href="#s-${ICON[st]}"/></svg></span>`;
751
+ strip.push(`<button class="${st==="waiting"?"attn ":""}${filter===st?"on":""}" data-f="${st}" title="Show only ${WORD[st]}">${mark}${k} ${WORD[st]}${st==="idea"&&k!==1?"s":""}</button>`)}
752
+ strip.push(`<button class="${archive?"on":""}" data-f="__archive" title="${cs.filter(c=>c.kind==="idea").length} ideas · ${cs.filter(c=>c.kind==="run").length} runs on the board">${archive?"Back to board":"Archive "+nArch}</button>`);
752
753
  $("strip").innerHTML=strip.join("");
753
754
  $("strip").querySelectorAll("button").forEach(c=>c.onclick=()=>{if(c.dataset.f==="__archive"){archive=!archive;filter=null;render();return}filter=filter===c.dataset.f?null:c.dataset.f;render()});
754
755
  // [label, folder, from the projects folder]: the button names the current one, the open list is redrawn.
@@ -788,8 +789,6 @@ function render(){
788
789
  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)}
789
790
  else if(unfolded.has(ci)&&list.length>FOLD+1){const li=document.createElement("li");li.innerHTML=`<button class="more-btn">Show less</button>`;li.firstChild.onclick=()=>{unfolded.delete(ci);render()};ul.appendChild(li)}
790
791
  s.appendChild(ul);b.appendChild(s)});
791
- const seen=new Map();cs.forEach(c=>seen.set(c.status,1));
792
- $("legend").innerHTML=[...seen.keys()].map(s=>`<span class="st d-${ICON[s]}" style="display:inline-grid;vertical-align:-2px"><svg><use href="#s-${ICON[s]}"/></svg></span> ${WORD[s]||s}`).join(" ");
793
792
  stopDictation(false); // forms were just rebuilt; a listener whose field is gone would write into nothing
794
793
  if(fk&&document.activeElement===document.body)cardEl(fk)?.focus({preventScroll:true});
795
794
  }
@@ -931,13 +930,16 @@ async function openRun(c,refresh,again){
931
930
  // A plan that asks questions takes their answers with the approval.
932
931
  // One the board cannot read is said, so it is answered in the terminal instead of skipped.
933
932
  const pq=c.status==="waiting"&&c.awaiting==="approvals.plan"?planQuestions(runDoc.plan):[],qs=pq||[];
933
+ // Its in-card agent ended on a waiting or done run: reopen that conversation, nothing sent.
934
+ const rs=c.pty&&!c.pty.alive?`<button class="btn" data-a="resume">Resume session</button>`:"";
934
935
  if(c.archived)foot.innerHTML=arch;
935
936
  else if((c.status==="waiting"||c.awaiting==="findings")&&APPROVAL[c.awaiting]){const sent=decided.has(answer(c)),asks=!pq?" It has open questions the board cannot show: answer them in the terminal running this run, or approve and your agent asks them.":qs.length?` It asks ${qs.length===1?"one question":qs.length+" questions"}: Approve opens ${qs.length===1?"it":"them"}.`:"";
936
937
  // gitBtn's buttons restyled as menu rows.
937
938
  const more=gitBtn(c).replace(/class="btn( pri)?"/g,"")+`<button data-a="archive">Archive</button><hr><button data-a="ab" class="danger">Abandon</button>`;
938
- 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><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>`}
939
- else if(c.status==="waiting")foot.innerHTML=`<span class="note">Waiting on you: ${esc(need(c.awaiting))}. Answer in the ${c.pty&&c.pty.alive?"terminal (button at the top)":"terminal running this run"}.</span>${arch}<button class="btn ghost" data-a="ab">Abandon</button>`;
940
- else if(c.status==="done"||c.status==="abandoned")foot.innerHTML=arch;
939
+ 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>`}
940
+ else if(c.status==="waiting")foot.innerHTML=`<span class="note">Waiting on you: ${esc(need(c.awaiting))}. Answer in the ${c.pty&&c.pty.alive?"terminal (button at the top)":"terminal running this run"}.</span>${arch}<button class="btn ghost" data-a="ab">Abandon</button>${rs}`;
941
+ else if(c.status==="done")foot.innerHTML=arch+rs;
942
+ else if(c.status==="abandoned")foot.innerHTML=arch;
941
943
  else 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>`:""}`;
942
944
  foot.querySelectorAll("[data-a]").forEach(b=>b.onclick=()=>{const a=b.dataset.a;if(a==="archive"||a==="restore"){toggleArchive(c,a==="archive");return}if(a==="push"){commitPush(c,"",b);return}if(a==="commit"){confirmCommit(c);return}if(a==="approve"){if(qs.length)confirmAnswers(c,qs);else decide(c,b);return}if(a==="revise"){confirmRevise(c);return}if(a==="resume"){resume(c,b);return}close();mode="reject";modeKey=key(c);render()})}
943
945
  // Answering an approval from the panel opens your agent on the run, told the decision and,
@@ -1150,6 +1152,8 @@ const SRC="Icebox";
1150
1152
  // whether a bulk action is still posting.
1151
1153
  // The category filter, and whether the add form takes a link or a social account/tag. The
1152
1154
  // project filter is the board's own `shown`, so the board and the icebox narrow alike.
1155
+ // The reply being written to a gathered post: its item, the text, and whether it is out.
1156
+ let iceReply=null;
1153
1157
  let iceForm=null,iceCur=null,iceSrc="",iceCat="",srcKind="link",srcShow=null,iceBusy=false,iceNav=null;const iceSel=new Set();
1154
1158
  const PLAT={x:"X",tiktok:"TikTok",instagram:"Instagram",threads:"Threads"};
1155
1159
  const srcOpen=()=>$("panel").classList.contains("open")&&$("ptitle").textContent===SRC;
@@ -1227,9 +1231,15 @@ async function openSources(){
1227
1231
  // The open item's form: a clean title for the idea's card and the person's own details;
1228
1232
  // what was gathered is added to the idea's notes by the server.
1229
1233
  const form=it=>`<div class="ice-form"><label for="ice-title" class="hint" style="margin:0">Idea title</label><input class="sel" id="ice-title" data-k="${esc(iceKey(it))}" maxlength="200" value="${esc(it.text)}" aria-label="Idea title"><label for="ice-details" class="hint" style="margin:0">Details (optional)</label><textarea class="sel" id="ice-details" rows="3" placeholder="In your own words: what is needed, for whom, why" aria-label="Details (optional)"></textarea><div class="ice-form-acts"><span class="hint">What was gathered is added to the idea's notes.</span><button class="btn ghost sm" id="ice-cancel">Cancel</button><button class="btn pri sm" id="ice-ok">Promote</button></div></div>`;
1234
+ // A gathered post can be answered: Reply drafts, the box is the approval, Send reply posts that text.
1235
+ const replyBox=it=>{if(!it.post)return"";if(it.replied)return`<span class="hint" style="margin:0">You replied to this post ${ago(it.replied)}.</span>`;
1236
+ const r=iceReply&&iceReply.k===iceKey(it)?iceReply:null;
1237
+ if(!r)return`<div class="ice-form-acts"><span class="hint">Answer this post from your signed-in kaizen browser. You see the reply before it goes out.</span><button class="btn sm" id="ice-rp">Reply</button></div>`;
1238
+ if(r.wait)return`<div class="ice-form-acts"><span class="hint">Writing a draft…</span></div>`;
1239
+ return`<div class="ice-form"><label for="ice-reply" class="hint" style="margin:0">Your reply${r.note?` · ${esc(r.note)}`:""}</label><textarea class="sel" id="ice-reply" rows="3" maxlength="2000" aria-label="Your reply"${r.busy?" disabled":""}>${esc(r.text)}</textarea><div class="ice-form-acts"><span class="hint" id="ice-rn">${r.text.length} characters · nothing is posted until you press Send reply</span><button class="btn ghost sm" id="ice-rc"${r.busy?" disabled":""}>Cancel</button><button class="btn pri sm" id="ice-rs"${r.busy||!r.text.trim()?" disabled":""}>${r.busy?"Sending…":"Send reply"}</button></div></div>`};
1230
1240
  const row=i=>{const it=ice[i],k=iceKey(it),open=iceForm===k;
1231
- return `<div class="ice-row${iceCur===k?" cur":""}${iceSel.has(k)?" sel":""}" data-i="${i}" data-k="${esc(k)}"><input type="checkbox" id="ice-ck-${i}" data-ck="${i}"${iceSel.has(k)?" checked":""} aria-label="Select: ${esc(it.text)}"><button class="ice-t" id="ice-o-${i}" data-o="${i}" aria-expanded="${open}" title="${esc(it.text)}">${esc(it.text)}</button><span class="ice-src" title="${esc(srcName(it))}">${fav(it.source,"link",list.find(x=>x.dir===it.dir&&x.url===it.source)?.social)}<span class="nm">${esc(many?it.project||srcName(it):srcName(it))}</span></span><span class="ice-age">${ago(it.at)}</span><span class="ice-ra"><button class="btn ghost sm" id="ice-pr-${i}" data-pr="${i}" title="Promote to Ideas as gathered (p)" aria-label="Promote to Ideas: ${esc(it.text)}">${ico("plus")}</button><button class="btn ghost sm" id="ice-ds-${i}" data-ds="${i}" title="Dismiss (d)" aria-label="Dismiss: ${esc(it.text)}">${ico("x")}</button></span></div>`
1232
- +(open?`<div class="ice-open">${it.notes?`<div class="ice-notes">${esc(it.notes)}</div>`:""}<span class="hint" style="margin:0">From ${esc(srcName(it))} · ${ago(it.at)}</span>${form(it)}</div>`:"")};
1241
+ return `<div class="ice-row${iceCur===k?" cur":""}${iceSel.has(k)?" sel":""}" data-i="${i}" data-k="${esc(k)}"><input type="checkbox" id="ice-ck-${i}" data-ck="${i}"${iceSel.has(k)?" checked":""} aria-label="Select: ${esc(it.text)}"><button class="ice-t" id="ice-o-${i}" data-o="${i}" aria-expanded="${open}" title="${esc(it.text)}">${esc(it.text)}</button><span class="ice-src" title="${esc(srcName(it))}">${fav(it.source,"link",list.find(x=>x.dir===it.dir&&x.url===it.source)?.social)}<span class="nm">${esc(many?it.project||srcName(it):srcName(it))}</span></span>${it.replied?`<span class="chip" title="Replied ${esc(ago(it.replied))}">replied</span>`:""}<span class="ice-age">${ago(it.at)}</span><span class="ice-ra"><button class="btn ghost sm" id="ice-pr-${i}" data-pr="${i}" title="Promote to Ideas as gathered (p)" aria-label="Promote to Ideas: ${esc(it.text)}">${ico("plus")}</button><button class="btn ghost sm" id="ice-ds-${i}" data-ds="${i}" title="Dismiss (d)" aria-label="Dismiss: ${esc(it.text)}">${ico("x")}</button></span></div>`
1242
+ +(open?`<div class="ice-open">${it.notes?`<div class="ice-notes">${esc(it.notes)}</div>`:""}<span class="hint" style="margin:0">From ${esc(srcName(it))} · ${ago(it.at)}${it.post?` · <a href="${esc(it.post)}" target="_blank" rel="noopener">open post</a>`:""}</span>${replyBox(it)}${form(it)}</div>`:"")};
1233
1243
  const inbox=ice.length&&!order.length?`<div class="ice-zero"><h4>Nothing matches</h4><p>${ice.length} waiting outside these filters.</p><p style="margin-top:10px"><button class="btn sm" id="ice-unf">Clear filters</button></p></div>`:ice.length?`<div class="ice-list">${[...groups].map(([g,ix])=>`<div class="ice-cat">${esc(g)}<i>${ix.length}</i></div>${ix.map(row).join("")}`).join("")}</div>`
1234
1244
  :`<div class="ice-zero"><h4>Inbox zero</h4><p>Nothing waiting. Gather all checks your sources for new requests; /kaizen discover in your agent sorts what comes in.</p></div>`;
1235
1245
  // The sources, in the right-hand drawer: shut until asked for, open by itself while there
@@ -1293,6 +1303,17 @@ async function openSources(){
1293
1303
  $("pbody").querySelectorAll("[data-pr]").forEach(b=>b.onclick=()=>{b.disabled=true;iceAct("promote",ice[+b.dataset.pr],"Promoted to Ideas")});
1294
1304
  $("pbody").querySelectorAll("[data-ds]").forEach(b=>b.onclick=()=>{b.disabled=true;iceAct("dismiss",ice[+b.dataset.ds],"Dismissed")});
1295
1305
  const open1=ice.find(it=>iceKey(it)===iceForm);
1306
+ if($("ice-rp"))$("ice-rp").onclick=async()=>{const it=open1,k=iceKey(it);iceReply={k,text:"",wait:true};openSources();
1307
+ const j=await post("/reply",{op:"draft",dir:it.dir,id:it.id});if(!iceReply||iceReply.k!==k)return;
1308
+ iceReply={k,text:j.text||"",note:j.ok?j.note||"":"",wait:false};openSources().then(()=>$("ice-reply")?.focus())};
1309
+ if($("ice-reply")){const ta=$("ice-reply");ta.oninput=()=>{iceReply.text=ta.value;$("ice-rn").textContent=`${ta.value.length} characters · nothing is posted until you press Send reply`;$("ice-rs").disabled=!ta.value.trim()};
1310
+ ta.onkeydown=e=>{if(e.key==="Escape"){e.stopPropagation();$("ice-rc").click()}};
1311
+ $("ice-rc").onclick=()=>{iceReply=null;openSources()};
1312
+ $("ice-rs").onclick=async()=>{const it=open1,r=iceReply;r.busy=true;openSources();
1313
+ const j=await post("/reply",{op:"send",dir:it.dir,id:it.id,text:r.text});
1314
+ if(j.ok){iceReply=null;toast("Reply posted")}else if(j.sent)iceReply=null;else r.busy=false;
1315
+ load();if(srcOpen())openSources()}}
1316
+
1296
1317
  if($("ice-ok")){
1297
1318
  const ok=()=>{const title=$("ice-title").value.trim();if(!title){$("ice-title").focus();return}$("ice-ok").disabled=true;iceAct("promote",open1,"Promoted to Ideas",{text:title,details:$("ice-details").value})};
1298
1319
  $("ice-ok").onclick=ok;