kaizen-agent 1.10.0 → 1.11.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/web/board.html CHANGED
@@ -26,6 +26,11 @@ pre.mark span:nth-child(1){color:var(--ink-hi)}pre.mark span:nth-child(2){color:
26
26
  .sel{border:1px solid var(--line);border-radius:var(--r-s);padding:6px 12px;background-color:var(--bg-2);color:var(--ink);font-size:13px}
27
27
  select.sel,button.sel{cursor:pointer;appearance:none;padding-right:30px;max-width:100%;background-image:linear-gradient(45deg,transparent 50%,var(--dim) 50%),linear-gradient(135deg,var(--dim) 50%,transparent 50%);background-position:calc(100% - 16px) 50%,calc(100% - 11px) 50%;background-size:5px 5px;background-repeat:no-repeat}
28
28
  .sel:hover{border-color:var(--dim)}
29
+ .agents{display:flex;flex-wrap:wrap;gap:6px}
30
+ .agent{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--line);border-radius:var(--r-s);padding:6px 12px;background:var(--bg-2);color:var(--dim);font-size:13px;cursor:pointer}
31
+ .agent svg{width:14px;height:14px}.agent:hover{border-color:var(--dim)}
32
+ .yolo{display:inline-flex;align-items:center;gap:8px;margin-right:auto;font-size:13px;color:var(--ink);cursor:pointer;white-space:nowrap}
33
+ .agent[aria-checked=true]{border-color:var(--accent);color:var(--ink);background:color-mix(in srgb,var(--accent) 14%,var(--bg-2))}
29
34
  .hdr .sel{max-width:min(320px,100%);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left}
30
35
  /* project picker: a select-shaped button over a searchable list */
31
36
  .combo{position:relative;max-width:100%;display:flex}
@@ -55,14 +60,14 @@ select.sel,button.sel{cursor:pointer;appearance:none;padding-right:30px;max-widt
55
60
  .btn.ghost{background:none;border-color:transparent;color:var(--dim);box-shadow:none}.btn.ghost:hover{color:var(--ink);background:var(--hover)}
56
61
 
57
62
  /* summary strip: each count is a pill that filters the board */
58
- .strip{display:flex;gap:8px;padding:10px 28px;flex-wrap:wrap;align-items:center;font-size:13px;color:var(--dim)}
63
+ .strip{display:inline-flex;gap:8px;flex-wrap:wrap;align-items:center;font-size:13px;color:var(--dim)}
59
64
  .strip button{display:inline-flex;align-items:center;gap:6px;color:var(--dim);padding:4px 12px;border-radius:var(--r-pill);box-shadow:inset 0 0 0 1px var(--line);transition:background .12s,color .12s}
60
65
  .strip button:hover,.strip button.on{color:var(--ink);background:var(--hover)}
61
66
  .strip button.attn{color:var(--warn);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--warn) 45%,transparent)}
62
67
  .strip svg{width:14px;height:14px;stroke-width:1.8}
63
68
 
64
69
  /* board */
65
- main{flex:1;min-height:0;display:grid;grid-template-columns:repeat(5,minmax(200px,1fr));gap:24px;padding:0 28px 20px;overflow:auto;align-content:start}
70
+ main{flex:1;min-height:0;display:grid;grid-template-columns:repeat(5,minmax(200px,1fr));gap:24px;padding:16px 28px 20px;overflow:auto;align-content:start}
66
71
  section h2::before{content:"";display:block;height:16px;margin-top:-16px}
67
72
  section{padding-top:16px}
68
73
  section h2{margin:0;font-size:13px;color:var(--ink);font-weight:600;display:flex;align-items:center;gap:8px;height:30px;position:sticky;top:0;background:var(--bg);z-index:1}
@@ -118,7 +123,7 @@ section h2 .clear{margin-left:auto;min-height:22px;padding:0 8px}
118
123
  .menu-pop button{text-align:left;padding:7px 10px;font-size:13px;font-weight:400;color:var(--ink);border-radius:var(--r-s);display:flex;gap:8px;align-items:center}
119
124
  .menu-pop button:hover{background:var(--hover)}
120
125
  .menu-pop button.danger{color:var(--danger)}
121
- .menu-pop svg{width:15px;height:15px}
126
+ .menu-pop svg{width:15px;height:15px;flex-shrink:0}
122
127
  .menu-pop hr{border:0;border-top:1px solid var(--line);margin:4px 0}
123
128
  .more-btn{width:100%;padding:8px;color:var(--dim);font-size:12px;border:1px dashed var(--line);border-radius:var(--r)}
124
129
  .more-btn:hover{color:var(--ink);border-color:var(--dim)}
@@ -145,8 +150,10 @@ section h2 .clear{margin-left:auto;min-height:22px;padding:0 8px}
145
150
 
146
151
  /* footer */
147
152
  .ftr{display:flex;justify-content:space-between;gap:16px;padding:10px 28px;border-top:1px solid var(--line);color:var(--dim);font-size:12px;flex-wrap:wrap;align-items:center}
148
- .ftr span+span{margin-left:14px}.ftr .st svg{width:12px;height:12px}
149
- .ftr a{color:var(--dim);cursor:pointer;text-decoration:underline;text-underline-offset:2px}.ftr a:hover{color:var(--ink)}
153
+ .ftr span+span,.ftr a+a{margin-left:14px}.ftr .strip{margin-right:14px;vertical-align:middle;font-size:12px}
154
+ /* The footer is out of the eye's path, so the waiting alert is filled, not just outlined. */
155
+ .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}
156
+ .ftr a{color:var(--dim);cursor:pointer;white-space:nowrap;text-decoration:underline;text-underline-offset:2px}.ftr a:hover{color:var(--ink)}
150
157
 
151
158
  /* side panel */
152
159
  .scrim{position:fixed;inset:0;z-index:10;background:color-mix(in srgb,var(--bg) 60%,transparent);backdrop-filter:blur(3px);display:none}
@@ -202,7 +209,8 @@ aside .foot .more .menu-pop{left:0;right:auto;top:auto;bottom:calc(100% + 4px)}.
202
209
  .srow>:nth-child(2){grid-column:2;grid-row:1/span 2;align-self:center;justify-self:end}
203
210
  .srow .hint{grid-column:1;margin:0;font-size:12px;color:var(--dim);line-height:1.45;max-width:46ch}
204
211
  .srow input.sel{width:170px}
205
- .srow[hidden],.sg[hidden],.sjump button[hidden]{display:none}
212
+ .fpick{display:inline-flex;gap:6px;align-items:center}
213
+ .srow[hidden],.yolo[hidden],.sg[hidden],.sjump button[hidden]{display:none}
206
214
  /* search and section chips: pinned to the top of the panel while the rows scroll under it */
207
215
  .sq{position:sticky;top:-24px;z-index:1;background:var(--bg-2);padding:24px 0 12px;margin:-24px 0 4px;border-bottom:1px solid var(--line)}.sq input{width:100%;max-width:none}.sq+.sg{margin-top:16px}
208
216
  .sjump{display:flex;flex-wrap:wrap;gap:6px;margin-top:10px}.sjump button{padding:3px 10px;border:1px solid var(--line);border-radius:var(--r-pill);font-size:12px;color:var(--dim)}.sjump button:hover{background:var(--hover);color:var(--ink)}
@@ -222,10 +230,57 @@ aside .foot .more .menu-pop{left:0;right:auto;top:auto;bottom:calc(100% + 4px)}.
222
230
  .car i{display:flex;gap:3px}.car u{width:14px;height:3px;border-radius:2px;background:var(--line)}.car u.on{background:var(--accent)}
223
231
  .toast{position:fixed;z-index:12;left:28px;bottom:56px;max-width:calc(100% - 56px);background:var(--ink);color:var(--bg);border-radius:var(--r);padding:10px 16px;font-size:13px;box-shadow:var(--shadow-2);opacity:0;transition:opacity .2s;pointer-events:none}
224
232
  .toast.on{opacity:1}
233
+ .toast.on.rep{pointer-events:auto}.toast a{color:inherit;margin-left:8px;text-decoration:underline;text-underline-offset:2px}
225
234
  .help{position:fixed;inset:0;z-index:12;display:none;align-items:center;justify-content:center;background:color-mix(in srgb,var(--bg) 60%,transparent);backdrop-filter:blur(3px)}
226
- .help.on{display:flex}.help div{background:var(--bg-2);border:1px solid var(--line);border-radius:var(--r);padding:24px 28px;line-height:2.2;box-shadow:var(--shadow-2)}
227
- .help kbd{display:inline-block;min-width:40px;margin-right:12px;padding:0 6px;text-align:center;border:1px solid var(--line);border-radius:var(--r-s);background:var(--bg);font:12px/20px var(--mono);color:var(--ink)}
228
- @media(max-width:900px){main{grid-template-columns:1fr;padding-inline:16px;gap:8px}.hdr,.strip,.ftr{padding-inline:16px}.more{display:grid}.card:hover .age{display:inline}.toast{left:16px;max-width:calc(100% - 32px)}}
235
+ .help.on{display:flex}.help div{background:var(--bg-2);border:1px solid var(--line);border-radius:var(--r);padding:24px 28px;box-shadow:var(--shadow-2)}
236
+ .help p{margin:0 0 16px;color:var(--dim)}.help dl{display:grid;grid-template-columns:auto 1fr;gap:10px 20px;align-items:center;margin:0}.help dt{display:flex;gap:6px;justify-content:flex-end}.help dd{margin:0}
237
+ .help kbd{font:12px/1 ui-monospace,monospace;padding:4px 7px;border:1px solid var(--line);border-bottom-width:2px;border-radius:var(--r-s);background:var(--bg);color:var(--ink-hi)}
238
+ /* request sources: cards, status badges, top bar, and add-box */
239
+ .src-topbar{display:flex;justify-content:space-between;align-items:center;padding:10px 14px;background:var(--bg);border:1px solid var(--line);border-radius:var(--r);margin:14px 0 16px;gap:12px}
240
+ .src-topbar .meta{display:flex;flex-direction:column;gap:2px}
241
+ .src-topbar .count{font-size:13px;font-weight:600;color:var(--ink)}
242
+ .src-topbar .sched{font-size:12px;color:var(--dim)}
243
+ .src-topbar .sched a{color:var(--accent);text-decoration:none;cursor:pointer}
244
+ .src-topbar .sched a:hover{text-decoration:underline}
245
+ .src-topbar .btn{display:inline-flex;align-items:center;gap:6px}
246
+ .src-topbar .btn svg{width:14px;height:14px}
247
+ .src-list{display:flex;flex-direction:column;gap:10px}
248
+ .src-card{background:var(--bg-2);border:1px solid var(--line);border-radius:var(--r);padding:14px;box-shadow:var(--shadow-1);display:flex;flex-direction:column;gap:9px;transition:border-color .15s}
249
+ .src-card:hover{border-color:color-mix(in srgb,var(--line) 70%,var(--ink))}
250
+ .src-hd{display:flex;justify-content:space-between;align-items:center;gap:10px}
251
+ .src-info{display:flex;align-items:center;gap:8px;min-width:0}
252
+ .src-ico{width:20px;height:20px;flex-shrink:0;color:var(--accent);display:inline-flex;align-items:center;justify-content:center}
253
+ .src-ico svg{width:18px;height:18px}
254
+ .src-title{font-size:14px;font-weight:600;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
255
+ .src-acts{display:flex;align-items:center;gap:4px;flex-shrink:0}
256
+ .src-acts .btn{display:inline-flex;align-items:center;gap:5px}
257
+ .src-acts .btn svg{width:14px;height:14px}
258
+ .src-url{font-size:12px;color:var(--dim);overflow-wrap:anywhere;line-height:1.4}
259
+ .src-url a{color:inherit;text-decoration:none;display:inline-flex;align-items:center;gap:4px}
260
+ .src-url a:hover{color:var(--ink);text-decoration:underline}
261
+ .src-url svg{width:12px;height:12px;stroke-width:1.8;flex-shrink:0}
262
+ .src-status{display:flex;flex-wrap:wrap;align-items:center;gap:6px;font-size:12px}
263
+ .src-badge{display:inline-flex;align-items:center;gap:5px;padding:2px 8px;border-radius:var(--r-pill);font-size:11px;font-weight:500}
264
+ .src-badge.ok{background:color-mix(in srgb,var(--ok) 14%,transparent);color:var(--ok)}
265
+ .src-badge.warn{background:color-mix(in srgb,var(--warn) 20%,transparent);color:var(--ink);font-weight:500}
266
+ .src-badge.warn svg{color:var(--warn)}
267
+ .src-badge.dim{background:color-mix(in srgb,var(--dim) 14%,transparent);color:var(--dim)}
268
+ .src-badge svg{width:12px;height:12px;stroke-width:2;flex-shrink:0}
269
+ .src-dest{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--dim);padding-top:8px;border-top:1px solid var(--line)}
270
+ .src-dest .sel{flex:1;max-width:240px;height:28px;font-size:12px;padding:2px 8px}
271
+ .src-dest .btn{height:28px;padding:0 10px;font-size:12px}
272
+ .src-empty{padding:26px 16px;text-align:center;background:var(--bg);border:1px dashed var(--line);border-radius:var(--r);margin:12px 0 16px}
273
+ .src-empty-ico{width:36px;height:36px;margin:0 auto 10px;color:var(--dim);display:flex;align-items:center;justify-content:center}
274
+ .src-empty-ico svg{width:32px;height:32px}
275
+ .src-empty h4{margin:0 0 6px;font-size:14px;font-weight:600;color:var(--ink)}
276
+ .src-empty p{margin:0 0 12px;font-size:12px;color:var(--dim);line-height:1.5}
277
+ .src-empty-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:8px;margin-top:12px;text-align:left}
278
+ .src-empty-item{background:var(--bg-2);border:1px solid var(--line);border-radius:var(--r-s);padding:8px 10px;font-size:11px;color:var(--dim);line-height:1.4}
279
+ .src-empty-item strong{display:block;color:var(--ink);font-weight:500;margin-bottom:2px}
280
+ .src-add-box{background:var(--bg-2);border:1px solid var(--line);border-radius:var(--r);padding:16px;margin-top:16px;box-shadow:var(--shadow-1)}
281
+ .src-add-box h3{margin:0 0 12px;font-size:13px;font-weight:600;color:var(--ink);display:flex;align-items:center;gap:6px}
282
+ .src-add-box h3 svg{width:16px;height:16px}
283
+ @media(max-width:900px){main{grid-template-columns:1fr;padding-inline:16px;gap:8px}.hdr,.ftr{padding-inline:16px}.more{display:grid}.card:hover .age{display:inline}.toast{left:16px;max-width:calc(100% - 32px)}}
229
284
  @media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}
230
285
  </style>
231
286
  </head>
@@ -243,6 +298,13 @@ aside .foot .more .menu-pop{left:0;right:auto;top:auto;bottom:calc(100% + 4px)}.
243
298
  <symbol id="i-trash" viewBox="0 0 24 24"><path d="M3 6h18M8 6V4h8v2M6 6l1 14h10l1-14M10 11v6M14 11v6"/></symbol>
244
299
  <symbol id="i-close" viewBox="0 0 24 24"><path d="M18 6L6 18M6 6l12 12"/></symbol>
245
300
  <symbol id="i-more" viewBox="0 0 24 24"><circle cx="5" cy="12" r="1.2" fill="currentColor"/><circle cx="12" cy="12" r="1.2" fill="currentColor"/><circle cx="19" cy="12" r="1.2" fill="currentColor"/></symbol>
301
+ <symbol id="i-open" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M15 3v18"/></symbol>
302
+ <symbol id="i-archive" viewBox="0 0 24 24"><rect x="2" y="3" width="20" height="5" rx="1"/><path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8M10 12h4"/></symbol>
303
+ <symbol id="i-restore" viewBox="0 0 24 24"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></symbol>
304
+ <symbol id="i-sheet" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M3 15h18M9 3v18"/></symbol>
305
+ <symbol id="i-doc" viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6M16 13H8M16 17H8M10 9H8"/></symbol>
306
+ <symbol id="i-refresh" viewBox="0 0 24 24"><path d="M21 3v5h-5M3 21v-5h5M20.5 13A9 9 0 0 1 5.7 18.3L3 16M3.5 11A9 9 0 0 1 18.3 5.7L21 8"/></symbol>
307
+ <symbol id="i-ext" viewBox="0 0 24 24"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14L21 3"/></symbol>
246
308
  <symbol id="s-idea" viewBox="0 0 16 16"><circle cx="8" cy="8" r="6" stroke-dasharray="2 2.5"/></symbol>
247
309
  <symbol id="s-start" viewBox="0 0 16 16"><circle cx="8" cy="8" r="6" stroke-dasharray="2 2.5"/><path d="M6.5 5.5v5l4-2.5z" fill="currentColor" stroke="none"/></symbol>
248
310
  <symbol id="s-run" viewBox="0 0 16 16"><circle cx="8" cy="8" r="6"/><path d="M8 2a6 6 0 0 1 0 12z" fill="currentColor" stroke="none"/></symbol>
@@ -273,11 +335,9 @@ aside .foot .more .menu-pop{left:0;right:auto;top:auto;bottom:calc(100% + 4px)}.
273
335
  <button class="ib" id="sources" title="Request sources" aria-label="Request sources"><svg><use href="#i-link"/></svg></button>
274
336
  <button class="ib" id="settings" title="Settings" aria-label="Settings"><svg><use href="#i-gear"/></svg></button>
275
337
  </div>
276
- <div class="strip" id="strip"></div>
277
-
278
338
  <main id="board" aria-live="polite"></main>
279
339
 
280
- <div class="ftr"><span id="legend"></span><span><a id="kbd">Keyboard shortcuts</a></span></div>
340
+ <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>
281
341
 
282
342
  <div class="scrim" id="scrim"></div>
283
343
  <aside id="panel" aria-label="detail">
@@ -288,8 +348,8 @@ aside .foot .more .menu-pop{left:0;right:auto;top:auto;bottom:calc(100% + 4px)}.
288
348
  </aside>
289
349
  <div class="toast" id="toast" role="status"></div>
290
350
  <div class="help" id="help"><div>
291
- Optional. Everything here is also a button.<br>
292
- <kbd>←↓↑→</kbd><kbd>hjkl</kbd>move between cards<br><kbd>Enter</kbd>open<br><kbd>←→</kbd><kbd>hl</kbd>tabs, in an open run<br><kbd>↑↓</kbd><kbd>jk</kbd>scroll, in an open run<br><kbd>n</kbd>new idea<br><kbd>e</kbd>edit<br><kbd>x</kbd>reject / abandon<br><kbd>r</kbd>run<br><kbd>a</kbd>all projects<br><kbd>Esc</kbd>close<br><kbd>?</kbd>this list</div></div>
351
+ <p>Optional. Everything here is also a button.</p>
352
+ <dl><dt><kbd>←↓↑→</kbd><kbd>hjkl</kbd></dt><dd>move between cards</dd><dt><kbd>Enter</kbd></dt><dd>open</dd><dt><kbd>←→</kbd><kbd>hl</kbd></dt><dd>tabs, in an open run</dd><dt><kbd>↑↓</kbd><kbd>jk</kbd></dt><dd>scroll, in an open run</dd><dt><kbd>n</kbd></dt><dd>new idea</dd><dt><kbd>e</kbd></dt><dd>edit</dd><dt><kbd>x</kbd></dt><dd>reject / abandon</dd><dt><kbd>r</kbd></dt><dd>run</dd><dt><kbd>a</kbd></dt><dd>all projects</dd><dt><kbd>Esc</kbd></dt><dd>close</dd><dt><kbd>?</kbd></dt><dd>this list</dd></dl></div></div>
293
353
 
294
354
  <script>
295
355
  // The page never touches the filesystem. Everything comes from /state and the run
@@ -374,7 +434,7 @@ function mic(fields){
374
434
  return b}
375
435
  const m=60e3,h=3600e3,d=86400e3;
376
436
  const age=(t,now)=>{const x=now-t;return x<h?Math.round(x/m)+"m":x<d?Math.round(x/h)+"h":Math.round(x/d)+"d"};
377
- const NAMES={claude:"Claude Code",codex:"Codex",agy:"Antigravity",opencode:"OpenCode",gemini:"Gemini",cursor:"Cursor",unrechecked:"fix not rechecked"};
437
+ const NAMES={claude:"Claude Code",codex:"Codex",agy:"Antigravity",opencode:"OpenCode",gemini:"Gemini",cursor:"Cursor",yolo:"yolo",unrechecked:"fix not rechecked"};
378
438
  const MARKS={claude:1,codex:1,agy:1,opencode:1,gemini:1,cursor:1,full:1,lite:1};
379
439
  const COLS=["Ideas","Planning","Building","Review","Done"];
380
440
  // state.ts statuses onto the icon set in the <defs> above.
@@ -425,7 +485,7 @@ async function follow(idea){
425
485
  if(openKey===k)close()}
426
486
  // A tab outliving its server would otherwise swallow every click: say so.
427
487
  const GONE="The board server is not running — start it with kaizen web (or the shortcut), then reload.";
428
- async function post(path,body){let r;try{r=await fetch(path,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(body)})}catch{toast(GONE);return{ok:false}}const j=await r.json().catch(()=>({}));if(!r.ok){toast(j.error||j.why||"That did not work");if(j.manual)console.log("run manually:",j.manual)}return j}
488
+ async function post(path,body){let r;try{r=await fetch(path,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(body)})}catch{toast(GONE);return{ok:false}}const j=await r.json().catch(()=>({}));if(!r.ok){const m=j.error||j.why||"That did not work";toast(m,{title:m.slice(0,80),what:`POST ${path} failed (HTTP ${r.status}): ${m}`});if(j.manual)console.log("run manually:",j.manual)}return j}
429
489
 
430
490
  function card(c){
431
491
  const k=key(c),el=document.createElement("div");el.className="card"+(k===openKey?" open":"")+(k===menuKey?" menu":"")+(c.archived?" archived":"");el.tabIndex=0;el.dataset.k=k;el.setAttribute("role","button");
@@ -434,25 +494,35 @@ function card(c){
434
494
  if(c.status==="waiting")chips.push(`<span class="chip warn"><i></i>${esc(need(c.awaiting)||"needs you")}</span>`);
435
495
  else if(c.status==="running")chips.push(`<span class="chip run"><i></i>running</span>`);
436
496
  if(all)chips.push(`<span class="chip">${esc(c.where)}</span>`);
437
- chips.push(...(c.tags||[]).map(t=>{const mark=MARKS[t]?`<svg><use href="#c-${t}"/></svg>`:"";return `<span class="chip${t==="full"||t==="lite"?" accent":""}">${mark}${esc(NAMES[t]||t)}</span>`}));
497
+ chips.push(...(c.tags||[]).map(t=>{const mark=MARKS[t]?`<svg><use href="#c-${t}"/></svg>`:"";return `<span class="chip${t==="full"||t==="lite"?" accent":t==="yolo"?" warn":""}"${t==="yolo"?' title="Started in Yolo: the agent runs commands without asking"':""}>${mark}${esc(NAMES[t]||t)}</span>`}));
438
498
  // A gathered idea's notes open with the stamp ingest() writes; its chip names the source.
439
499
  const g=c.notes&&/^\[[^\]]*\] Gathered from (.+?)\. Outside text/.exec(c.notes);if(g)chips.push(`<span class="chip" title="From ${esc(g[1])}">gathered</span>`);else if(c.notes)chips.push(`<span class="chip">notes</span>`);
440
500
  // Information only: the panel's button is where it is committed and pushed.
441
501
  if(c.git){const t=c.git==="pushed"?"Committed & pushed":"Not committed & pushed yet";chips.push(`<span class="chip ${c.git==="pushed"?"ok":"warn"}" role="img" title="${t}" aria-label="${t}"><svg><use href="#c-git"/></svg></span>`)}
442
502
  const pick=picking&&!archive&&c.column===4;
443
- el.innerHTML=(pick?`<input type="checkbox" class="pick" ${picked.has(k)?"checked":""} aria-label="Select">`:"")+`<div class="c-id"><span class="st d-${st}"><svg><use href="#s-${st}"/></svg></span><span class="idt">${esc(idt)}</span><span class="age">${c.kind==="run"?age(c.moved||S.now,S.now):""}</span><button class="ib sm more" data-a="menu" title="Actions" aria-label="Actions">${ico("more")}</button></div>
503
+ el.innerHTML=(pick?`<input type="checkbox" class="pick" ${picked.has(k)?"checked":""} aria-label="Select">`:"")+`<div class="c-id"><span class="st d-${st}"><svg><use href="#s-${st}"/></svg></span><span class="idt">${esc(idt)}</span><span class="age">${c.kind==="run"?age(c.moved||S.now,S.now):""}</span><button class="ib sm more" data-a="menu" title="Actions" aria-label="Actions" aria-haspopup="menu" aria-expanded="${k===menuKey}">${ico("more")}</button></div>
444
504
  <div class="c-title">${esc(c.kind==="idea"?c.text:(c.title||c.id))}</div>
445
505
  <div class="c-chips">${chips.join("")}</div>`;
446
506
  if(k===menuKey){
447
507
  const items=[];
448
- if(archive){items.push(["restore","Restore",ico("play")]);if(c.kind==="run")items.push(["open","Open",ico("play")]);}
449
- else if(c.kind==="idea"&&c.status==="idea")items.push(["run","Run",ico("play")],["edit","Edit",ico("pen")],["reject","Reject",ico("x")],null,["delete","Delete",ico("trash"),"danger"]);
450
- else if(c.kind==="idea")items.push(["run","Run again",ico("play")],null,["delete","Remove",ico("trash"),"danger"]);
451
- else if(c.status==="waiting"||c.status==="running"||c.status==="stalled")items.push(["open","Open",ico("play")],null,["reject","Abandon",ico("x"),"danger"]);
452
- else items.push(["open","Open",ico("play")]);
453
- if(!archive&&!(c.kind==="idea"&&c.status!=="idea"))items.push(null,["archive","Archive",ico("x")]);
454
- el.innerHTML+=`<div class="menu-pop">${items.map(i=>i?`<button data-a="${i[0]}" class="${i[3]||""}">${i[2]}${i[1]}</button>`:"<hr>").join("")}</div>`}
455
- el.querySelectorAll("[data-a]").forEach(bt=>bt.onclick=async e=>{e.stopPropagation();const a=bt.dataset.a;menuKey=null;if(a==="menu"){menuKey=k;render();return}if(a==="open"){open(c);return}
508
+ if(archive){
509
+ items.push(["restore","Restore",ico("restore")]);
510
+ items.push(["open","Open",ico("open")]);
511
+ if(c.kind==="idea")items.push(null,["delete","Delete",ico("trash"),"danger"]);
512
+ }else if(c.kind==="idea"&&c.status==="idea"){
513
+ items.push(["run","Run",ico("play")],["edit","Edit",ico("pen")],["archive","Archive",ico("archive")],null,["reject","Reject",ico("x")],["delete","Delete",ico("trash"),"danger"]);
514
+ }else if(c.kind==="idea"){
515
+ items.push(["open","Open",ico("open")],["run","Run again",ico("play")],["archive","Archive",ico("archive")],null,["delete","Remove",ico("trash"),"danger"]);
516
+ }else{
517
+ const openLabel=c.status==="waiting"?(c.awaiting==="approvals.plan"?"Review plan":c.awaiting==="findings"?"See findings":c.awaiting==="approvals.review"?"Review work":"Open"):"Open";
518
+ items.push(["open",openLabel,ico("open")]);
519
+ items.push(["archive","Archive",ico("archive")]);
520
+ if(c.status==="waiting"||c.status==="running"||c.status==="stalled"){
521
+ items.push(null,["reject","Abandon",ico("x"),"danger"]);
522
+ }
523
+ }
524
+ el.innerHTML+=`<div class="menu-pop" role="menu">${items.map(i=>i?`<button role="menuitem" data-a="${i[0]}" class="${i[3]||""}">${i[2]}${i[1]}</button>`:"<hr>").join("")}</div>`}
525
+ el.querySelectorAll("[data-a]").forEach(bt=>bt.onclick=async e=>{e.stopPropagation();const a=bt.dataset.a;const was=menuKey;menuKey=null;if(a==="menu"){menuKey=was===k?null:k;render();return}if(a==="open"){open(c);return}
456
526
  if(a==="archive"||a==="restore"){toggleArchive(c,a==="archive");return}
457
527
  mode=a;modeKey=k;render()});
458
528
  if(pick){el.classList.toggle("picked",picked.has(k));el.onclick=()=>{picked.has(k)?picked.delete(k):picked.add(k);render()};return el}
@@ -464,7 +534,7 @@ function inlineForm(c){
464
534
  if(mode==="edit"){li.innerHTML=`<input value="${esc(c.text)}"><textarea placeholder="Notes: links, paths, background">${esc(c.notes||"")}</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">Save</button></div>`;li.querySelector('[data-a=s]').onclick=async()=>{const next=li.querySelector("input").value.trim(),notes=li.querySelector("textarea").value.trim();mode=null;if(next&&(next!==c.text||notes!==(c.notes||"")))await post("/inbox",{op:"edit",dir,text:c.text,next,notes});load()}}
465
535
  if(mode==="reject"){const idea=c.kind==="idea";li.innerHTML=`<div class="q">${idea?"Reject this idea?":"Abandon this run?"} The reason is recorded.</div><input placeholder="Reason"><div class="row"><button class="btn ghost" data-a="c">Keep it</button><button class="btn warn" data-a="s">${idea?"Reject":"Abandon"}</button></div>`;li.querySelector('[data-a=s]').onclick=async()=>{const why=li.querySelector("input").value.trim();if(!why){toast("A reason is required");return}mode=null;const r=idea?await post("/inbox",{op:"reject",dir,text:c.text,reason:why}):await post("/abort",{dir,id:c.id,why});if(r.ok)toast(idea?"Rejected":"Abandoned, reason recorded");load()}}
466
536
  if(mode==="delete"){li.innerHTML=`<div class="q">Delete "${esc(c.text)}"?</div><div class="q dim">Not recorded anywhere.</div><div class="row"><button class="btn ghost" data-a="c">Keep it</button><button class="btn danger" data-a="s">Delete</button></div>`;li.querySelector('[data-a=s]').onclick=async()=>{mode=null;const r=await post("/inbox",{op:"delete",dir,text:c.text});if(r.ok)toast("Deleted");load()}}
467
- if(mode==="run"){li.innerHTML=`<div class="q">Start a run for "${esc(c.text)}"</div><div class="q dim">Opens your agent in a new terminal, or a herdr tab or tmux window when one is running, with the request typed in.</div><div class="q dim">Full: planned, built, then checked by a separate reviewer. Lite: one session does it all, faster and cheaper.</div><div class="row"><button class="btn ghost" data-a="c">Cancel</button><button class="btn" data-a="l">Lite</button><button class="btn pri" data-a="s">Full run</button></div>`;li.querySelector('[data-a=s]').onclick=()=>startRun(c,"full");li.querySelector('[data-a=l]').onclick=()=>startRun(c,"lite")}
537
+ if(mode==="run"){li.innerHTML=`<div class="q">Start a run for "${esc(c.text)}"</div><div class="q dim">Opens your agent in a new terminal, or a herdr tab or tmux window when one is running, with the request typed in.</div><div class="q dim">Full: planned, built, then checked by a separate reviewer. Lite: one session does it all, faster and cheaper.</div><div id="run-pick" style="display:grid;gap:6px"><div id="run-agent" class="agents" role="radiogroup" aria-label="Agent"></div></div><div class="row"><label class="yolo" id="run-yolo" title="Yolo: the agent runs every command and edit without asking" hidden><button type="button" class="chk" role="switch" aria-checked="false"></button>Yolo</label><button class="btn ghost" data-a="c">Cancel</button><button class="btn" data-a="l">Lite</button><button class="btn pri" data-a="s">Full run</button></div>`;li.querySelector('[data-a=s]').onclick=()=>startRun(c,"full");li.querySelector('[data-a=l]').onclick=()=>startRun(c,"lite");runForm(li,c)}
468
538
  li.querySelector('[data-a=c]').onclick=()=>{mode=null;render()};
469
539
  // Edit and reject take dictation; the run prompt has no field.
470
540
  li.querySelectorAll("input,textarea").forEach((f,i)=>draft(f,"f:"+mode+":"+modeKey+":"+i));
@@ -472,16 +542,42 @@ function inlineForm(c){
472
542
  const inp=li.querySelector("input");if(inp){inp.onkeydown=e=>{if(e.key==="Enter")li.querySelector('[data-a=s]').click();if(e.key==="Escape"){mode=null;render()}};setTimeout(()=>inp.focus())}
473
543
  return li;}
474
544
 
545
+ // The agent and Basic/Yolo picked in the open Run form. Kept here, not in the form:
546
+ // the board is rebuilt on every server change, and the choice must outlive that.
547
+ // Yolo never outlives the form: render() drops it once the form is closed.
548
+ let runPick=null;
549
+ // The tool a launch opened, and why when it is not the one asked for.
550
+ const via=a=>a.name+(a.note?". "+a.note:"");
551
+ async function runForm(li,c){
552
+ const k=key(c),box=li.querySelector("#run-agent");
553
+ const d=await fetch("/agents?dir="+encodeURIComponent(c.state)).then(r=>r.json()).catch(()=>null);
554
+ if(!d||!d.agents||!d.agents.length){li.querySelector("#run-pick").hidden=true;return}
555
+ // Antigravity and Gemini CLI are both Google's agent on Gemini; offer one of them.
556
+ if(d.agents.some(a=>a.cmd==="agy"))d.agents=d.agents.filter(a=>a.cmd!=="gemini");
557
+ if(!runPick||runPick.k!==k)runPick={k,agent:d.agents.some(a=>a.cmd===d.default)?d.default:d.agents[0].cmd};
558
+ box.innerHTML=d.agents.map(a=>`<button type="button" class="agent" role="radio" aria-checked="${a.cmd===runPick.agent}" data-cmd="${esc(a.cmd)}"><svg><use href="#c-${esc(a.cmd)}"/></svg>${esc(a.name)}</button>`).join("");
559
+ const yb=li.querySelector("#run-yolo");
560
+ // Yolo only where the picked tool has a skip-permissions mode; Basic otherwise.
561
+ const sw=yb.querySelector(".chk");
562
+ const showYolo=()=>{const can=!!d.agents.find(a=>a.cmd===runPick.agent)?.yolo,on=!!runPick.yolo;yb.hidden=!can;sw.classList.toggle("on",on);sw.setAttribute("aria-checked",on)};
563
+ box.querySelectorAll(".agent").forEach(b=>b.onclick=()=>{runPick.agent=b.dataset.cmd;box.querySelectorAll(".agent").forEach(x=>x.setAttribute("aria-checked",x===b));showYolo()});
564
+ sw.onclick=()=>{runPick.yolo=!runPick.yolo;showYolo()};
565
+ showYolo();
566
+ box.onkeydown=sw.onkeydown=e=>{if(e.key==="Escape"){mode=null;render()}}}
567
+
475
568
  async function startRun(c,kind){
476
569
  mode=null;askNotify();
477
- const r=await post("/run",{dir:c.state,text:c.text,kind});
478
- if(r.ok)toast((kind==="full"?"Full":"Lite")+" run opened in your terminal: "+r.agent.name);
570
+ const p=runPick&&runPick.k===key(c)?runPick:null;runPick=null;
571
+ const yolo=!!(p&&p.yolo&&!$("run-yolo")?.hidden);
572
+ const r=await post("/run",{dir:c.state,text:c.text,kind,agent:p?p.agent:undefined,yolo});
573
+ if(r.ok)toast((kind==="full"?"Full":"Lite")+(yolo?" yolo":"")+" run opened in your terminal: "+via(r.agent));
479
574
  load()}
480
575
 
481
576
  function render(){
482
577
  // A closed form leaves no draft behind; an open one is reopened after a reload.
483
578
  // A form whose card is gone (restored after a reload, or deleted elsewhere) has nowhere to show.
484
579
  if(mode&&mode!=="new"&&!S.cards.some(c=>key(c)===modeKey))mode=null;
580
+ if(mode!=="run"&&runPick)runPick.yolo=false;
485
581
  if(!mode)undraft("f:");try{sessionStorage.setItem("kz-form",JSON.stringify(["new","edit","reject"].includes(mode)?[mode,modeKey]:null))}catch(e){}
486
582
  // The board is rebuilt below; the card that had focus gets it back.
487
583
  const fk=focusedKey();
@@ -492,7 +588,7 @@ function render(){
492
588
  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>`);
493
589
  strip.push(`<button class="${filter==="running"?"on":""}" data-f="running"><span class="d-run"><span class="pulse"></span></span>${n("running")} running</button>`);
494
590
  if(n("stalled"))strip.push(`<button class="${filter==="stalled"?"on":""}" data-f="stalled"><svg><use href="#s-stall"/></svg>${n("stalled")} stalled</button>`);
495
- strip.push(`<span class="sp"></span><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>`);
591
+ 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>`);
496
592
  $("strip").innerHTML=strip.join("");
497
593
  $("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()});
498
594
  // [label, folder, from the projects folder]: the button names the current one, the open list is redrawn.
@@ -653,11 +749,11 @@ async function openRun(c,refresh,again){
653
749
  // run last moved: the same approval reached again after a revise is a new question.
654
750
  // The footer's More menu is a <details>: close it on a click anywhere else.
655
751
  document.addEventListener("click",e=>{for(const d of document.querySelectorAll(".more[open]"))if(!d.contains(e.target))d.open=false});
656
- const APPROVAL={"approvals.plan":"The plan is ready. Read it in the Plan tab, then approve or send it back.","approvals.review":"The work is reviewed. Approve to finish it, or revise with what to change."},decided=new Set(),answer=c=>key(c)+":"+c.awaiting+":"+c.moved;
752
+ const APPROVAL={"approvals.plan":"The plan is ready. Read it in the Plan tab, then approve or send it back.","approvals.review":"The work is ready to check. Approve to finish it, or revise with what to change."},decided=new Set(),answer=c=>key(c)+":"+c.awaiting+":"+c.moved;
657
753
  async function decide(c,b,why,answers){b.disabled=true;askNotify();const r=await post("/decide",{dir:c.state,id:c.id,awaiting:c.awaiting,...(why===undefined?{}:{why}),...(answers?{answers}:{})});
658
754
  // A failed send keeps the footer, and with it a typed comment.
659
755
  if(!r.ok){b.disabled=false;return}
660
- decided.add(answer(c));toast((why===undefined?"Approved":"Sent back")+" in your terminal: "+r.agent.name);
756
+ decided.add(answer(c));toast((why===undefined?"Approved":"Sent back")+" in your terminal: "+via(r.agent));
661
757
  if(openKey===key(c)){$("pfoot").innerHTML="";openRun(c,true)}}
662
758
  function confirmRevise(c){const foot=$("pfoot"),plan=c.awaiting==="approvals.plan";
663
759
  foot.innerHTML=`<div class="inl" style="flex:1"><div class="q">${plan?"What should the plan do differently?":"What should change in the work?"}</div><div class="q dim">Opens your agent on this run with your comment.${plan?" It revises the plan and comes back here for approval.":" It changes the work, rechecks it and comes back here."}</div><textarea aria-label="Revise comment"></textarea><div class="row"><button class="btn ghost" data-a="c">Cancel</button><button class="btn pri" data-a="s">Send back</button></div></div>`;
@@ -706,7 +802,7 @@ function fixForm(c){const k=n=>c.id+":"+n,sent=f=>fixSent.has(k(f.n)),fs=runDoc.
706
802
  allb.onclick=()=>{const none=picks().length===open.length;open.forEach(f=>none?fixOff.add(k(f.n)):fixOff.delete(k(f.n)));sync()};
707
803
  go.onclick=async()=>{const nums=picks();go.disabled=true;askNotify();const r=await post("/fix",{dir:c.state,id:c.id,nums});
708
804
  // Sent findings lock as sent, so a second press cannot open a second agent on them.
709
- if(r.ok){toast("Fixing "+nums.length+" finding"+(nums.length===1?"":"s")+" in your terminal: "+r.agent.name);nums.forEach(n=>fixSent.add(k(n)));try{sessionStorage.setItem(SK,JSON.stringify([...fixSent]))}catch(e){}openRun(c,true)}else sync()};
805
+ if(r.ok){toast("Fixing "+nums.length+" finding"+(nums.length===1?"":"s")+" in your terminal: "+via(r.agent));nums.forEach(n=>fixSent.add(k(n)));try{sessionStorage.setItem(SK,JSON.stringify([...fixSent]))}catch(e){}openRun(c,true)}else sync()};
710
806
  sync();$("pbody").appendChild(el)}
711
807
  // Commit and push, for a finished run in a project with a git remote. What the board
712
808
  // did is recorded per run, so the settled label outlives later changes to the tree.
@@ -787,7 +883,7 @@ async function openSettings(){
787
883
  const ntHelp="A desktop notification when a run starts waiting on you."+(np?np==="denied"?" Blocked in this browser's site settings.":"":" Not supported in this browser.");
788
884
  const names=[...(cfg?groups(cfg.rows).map(([g])=>g):[]),"Browser"];
789
885
  $("pbody").innerHTML=`<div class="sq"><input class="sel" id="sq" type="search" placeholder="Search settings" aria-label="Search settings" spellcheck="false" autocomplete="off" value="${esc(setQ)}"><div class="sjump" role="group" aria-label="Jump to section">${names.map(g=>`<button data-g="${esc(g)}">${esc(g)}</button>`).join("")}</div></div>
790
- ${cfg?groups(cfg.rows).map(([g,rows])=>`<h3 class="sg">${esc(g)}</h3>${rows.map(r=>{const id="s-"+r.key.replace(/\./g,"-");return row(r.label,r.text?`<input class="sel" id="${id}" value="${esc(r.value)}" placeholder="not set" spellcheck="false">`:ctl(id,r.values,r.value),esc(r.help),id,[r.label,r.help,r.key,g,...r.values].join(" "))}).join("")}`).join(""):`<p class="snone">Settings could not be read. Run <code>kaizen settings</code>.</p>`}
886
+ ${cfg?groups(cfg.rows).map(([g,rows])=>`<h3 class="sg">${esc(g)}</h3>${rows.map(r=>{const id="s-"+r.key.replace(/\./g,"-");return row(r.label,r.text?(r.folder?`<span class="fpick"><input class="sel" id="${id}" value="${esc(r.value)}" placeholder="not set" spellcheck="false"><button class="btn" id="${id}-pick" type="button">Browse…</button></span>`:`<input class="sel" id="${id}" value="${esc(r.value)}" placeholder="not set" spellcheck="false">`):ctl(id,r.values,r.value),esc(r.help),id,[r.label,r.help,r.key,g,...r.values].join(" "))}).join("")}`).join(""):`<p class="snone">Settings could not be read. Run <code>kaizen settings</code>.</p>`}
791
887
  <h3 class="sg">Browser</h3>${row("Theme",ctl("th",["system","light","dark"],cur),"Light or dark for this browser only; system follows your device.","th","theme light dark system browser")}
792
888
  ${row("Notifications",`<button class="chk${np==="granted"?" on":""}" id="nt" role="switch" aria-checked="${np==="granted"}"${np&&np!=="denied"?"":" disabled"}></button>`,ntHelp,"nt","notifications "+ntHelp+" browser")}
793
889
  <p class="snone" id="snone" hidden></p>`;
@@ -806,7 +902,9 @@ async function openSettings(){
806
902
  $("th").onchange=e=>setTheme(e.target.value);
807
903
  $("nt").onclick=()=>{askNotify();setTimeout(openSettings,300)};
808
904
  if(cfg)cfg.rows.forEach(r=>{const el=$("s-"+r.key.replace(/\./g,"-"));const save=async v=>{const f=document.activeElement?.id;const j=await post("/settings",{key:r.key,value:v});if(j.ok)toast("Saved");await openSettings();if(f?.startsWith("s-preset-"))$(f)?.focus()};
809
- if(el.tagName==="SELECT"||el.tagName==="INPUT")el.onchange=async()=>{await save(el.value.trim());if(el.tagName==="INPUT")load()};else el.querySelectorAll("[data-v]").forEach(b=>b.onclick=()=>save(b.dataset.v));if(el.matches(".chk"))el.onclick=()=>save(el.dataset.v)})}
905
+ if(el.tagName==="SELECT"||el.tagName==="INPUT")el.onchange=async()=>{await save(el.value.trim());if(el.tagName==="INPUT")load()};else el.querySelectorAll("[data-v]").forEach(b=>b.onclick=()=>save(b.dataset.v));if(el.matches(".chk"))el.onclick=()=>save(el.dataset.v);
906
+ // The system's own folder dialog, opened by the board: a page cannot read a real path.
907
+ if(r.folder)$(el.id+"-pick").onclick=async()=>{const j=await post("/pick-folder",{});if(j.path){await save(j.path);load()}}})}
810
908
  // ---- request sources: links kaizen reads for new ideas. Same panel as settings.
811
909
  const SRC="Request sources";
812
910
  const srcOpen=()=>$("panel").classList.contains("open")&&$("ptitle").textContent===SRC;
@@ -823,33 +921,50 @@ async function openSources(){
823
921
  const d=await fetch("/sources"+q).then(r=>r.json()).catch(()=>null);
824
922
  if(!srcOpen())return;
825
923
  const host=u=>{try{return new URL(u).hostname}catch{return u}};
924
+ const srcType=u=>{if(u.includes("docs.google.com/spreadsheets")||u.endsWith(".csv")||u.endsWith(".tsv"))return "sheet";if(u.includes("docs.google.com/document"))return "doc";return "link"};
826
925
  const list=d&&d.sources||[];
827
926
  // A source belongs to one project, named on its row. Picking another only arms Move: a select
828
927
  // changes on every arrow key, and each change would have moved the source. The names are the picker's;
829
928
  // a row whose project the picker does not list still gets its own, selected, so Move never points elsewhere.
830
929
  const opts=(on,name)=>(on&&!S.projects.some(p=>p.dir===on)?`<option value="${esc(on)}" selected>${esc(name||on)}</option>`:"")+S.projects.map(p=>`<option value="${esc(p.dir)}"${p.dir===on?" selected":""}>${esc(p.label)}</option>`).join("");
831
- const rows=list.map((s,i)=>`<div class="srow"><label>${esc(s.label||host(s.url))}</label><button class="btn ghost" id="src-rm-${i}" data-rm="${i}" aria-label="Remove ${esc(s.label||host(s.url))}">Remove</button><p class="hint" style="overflow-wrap:anywhere">${s.url.startsWith("https://")?`<a href="${esc(s.url)}" target="_blank" rel="noopener noreferrer" style="color:inherit">${esc(s.url)}</a>`:esc(s.url)}<br>${s.last?(s.last.ok?esc(s.last.note):`<span style="color:var(--ink);font-weight:500">Failed: ${esc(s.last.note)}</span>`)+" · "+ago(s.last.at):"Not gathered yet"}</p><p class="hint" style="margin-top:6px">Ideas go to <select class="sel" id="src-mv-${i}" data-mv="${i}" data-k="${esc(s.dir+" "+s.url)}" aria-label="Project of ${esc(s.label||host(s.url))}">${opts(s.dir,s.project)}</select> <button class="btn" id="src-mb-${i}" aria-label="Move ${esc(s.label||host(s.url))} to the chosen project" disabled>Move</button></p></div>`).join("")+(d&&d.failed||[]).map(f=>`<p style="color:var(--dim)">The sources of ${esc(f)} could not be read.</p>`).join("");
832
- $("pbody").innerHTML=(d&&d.error?`<p style="color:var(--dim)">${esc(d.error)}</p>`:!d?`<p style="color:var(--dim)">Sources could not be read.</p>`:rows||`<p style="color:var(--dim)">No sources yet. Paste a link to a Google Sheet, a Doc, or a shared note.</p>`)
833
- +`<h3 class="sg">Add a source</h3><div style="display:grid;gap:8px;margin-top:10px"><input class="sel" id="src-url" aria-label="Link" placeholder="https://docs.google.com/…" spellcheck="false" autocomplete="off" style="width:100%;max-width:none"><input class="sel" id="src-label" aria-label="Label (optional)" placeholder="Label (optional)" style="width:100%;max-width:none"><select class="sel" id="src-dir" aria-label="Project this source belongs to" style="width:100%;max-width:none">${dir?"":`<option value="">Choose the project it belongs to…</option>`}${opts(dir)}</select><div style="display:flex;gap:8px"><button class="btn pri" id="src-add">Add source</button><button class="btn" id="src-go"${list.length?"":" disabled"}>Gather now</button></div><p class="hint" style="font-size:12px;color:var(--dim);margin:0">${d&&d.every&&d.every!=="off"?`Checked every ${esc(d.every)} while the board is running.`:"Automatic checks are off."} Change it in Settings.</p></div>`;
834
- $("src-url").value=typed[0];$("src-label").value=typed[1];if(typed[3]!==undefined)$("src-dir").value=typed[3];
930
+ const topBar=`<div class="src-topbar"><div class="meta"><span class="count">${list.length} source${list.length===1?"":"s"}${dir?` in ${esc(S.projects.find(p=>p.dir===dir)?.label||dir)}`:""}</span><span class="sched">${d&&d.every&&d.every!=="off"?`Auto-check: every ${esc(d.every)}`:"Auto-check: off"} · <a id="src-to-settings" role="button" tabindex="0">Settings</a></span></div><button class="btn sm" id="src-go"${list.length?"":" disabled"}>${ico("refresh")} Gather all</button></div>`;
931
+ const cards=list.map((s,i)=>{
932
+ const st=srcType(s.url),title=s.label||host(s.url);
933
+ const statusHtml=!s.last?`<span class="src-badge dim">Not gathered yet</span>`:s.last.ok?`<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>${esc(s.last.note)}</span><span class="hint" style="margin:0">${ago(s.last.at)}</span>`:`<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: ${esc(s.last.note)}</span><span class="hint" style="margin:0">${ago(s.last.at)}</span>`;
934
+ return `<div class="src-card" id="src-card-${i}"><div class="src-hd"><div class="src-info"><span class="src-ico" title="${st}">${ico(st==="sheet"?"sheet":st==="doc"?"doc":"link")}</span><span class="src-title" title="${esc(title)}">${esc(title)}</span></div><div class="src-acts"><button class="btn ghost sm" id="src-chk-${i}" data-chk="${i}" aria-label="Check ${esc(title)} now" title="Check this source now">${ico("refresh")} Check</button><button class="btn ghost danger sm" id="src-rm-${i}" data-rm="${i}" aria-label="Remove ${esc(title)}" title="Remove source">${ico("trash")}</button></div></div><div class="src-url"><a href="${esc(s.url)}" target="_blank" rel="noopener noreferrer" title="${esc(s.url)}"><span>${esc(s.url)}</span>${ico("ext")}</a></div><div class="src-status">${statusHtml}</div><div class="src-dest"><span>Ideas go to</span><select class="sel" id="src-mv-${i}" data-mv="${i}" data-k="${esc(s.dir+" "+s.url)}" aria-label="Project of ${esc(title)}">${opts(s.dir,s.project)}</select><button class="btn sm" id="src-mb-${i}" aria-label="Move ${esc(title)} to the chosen project" disabled>Move</button></div></div>`;
935
+ }).join("")+(d&&d.failed||[]).map(f=>`<p style="color:var(--dim)">The sources of ${esc(f)} could not be read.</p>`).join("");
936
+ const emptyState=`<div class="src-empty"><div class="src-empty-ico">${ico("link")}</div><h4>No request sources yet</h4><p>Connect shared links to gather requests into your Ideas backlog. Nothing runs automatically until you start it.</p><div class="src-empty-grid"><div class="src-empty-item"><strong>📊 Google Sheets</strong>Public sheets with idea or task rows. Rows marked Done are skipped.</div><div class="src-empty-item"><strong>📄 Google Docs</strong>Shared public docs with items or notes written line by line.</div><div class="src-empty-item"><strong>🌐 Public links</strong>TSV, CSV, or raw text links containing requests.</div></div></div>`;
937
+ const addBox=`<div class="src-add-box"><h3>${ico("plus")} Add a source</h3><div style="display:grid;gap:8px"><div><label for="src-url" class="hint" style="display:block;margin-bottom:4px;font-weight:500;color:var(--ink)">Link</label><input class="sel" id="src-url" aria-label="Link" placeholder="https://docs.google.com/spreadsheets/d/… or public URL" spellcheck="false" autocomplete="off" style="width:100%;max-width:none"></div><div style="display:grid;grid-template-columns:1fr 1fr;gap:8px"><div><label for="src-label" class="hint" style="display:block;margin-bottom:4px">Label (optional)</label><input class="sel" id="src-label" aria-label="Label (optional)" placeholder="e.g. Feedback Q4" style="width:100%;max-width:none"></div><div><label for="src-dir" class="hint" style="display:block;margin-bottom:4px">Target project</label><select class="sel" id="src-dir" aria-label="Project this source belongs to" style="width:100%;max-width:none">${dir?"":`<option value="">Choose project…</option>`}${opts(dir)}</select></div></div><div style="display:flex;align-items:center;justify-content:space-between;margin-top:6px"><button class="btn pri" id="src-add">Add source</button><span class="hint" style="font-size:11px;color:var(--dim);margin:0">Public links only. For private docs, run /kaizen gather.</span></div></div></div>`;
938
+ $("pbody").innerHTML=(d&&d.error?`<p style="color:var(--dim)">${esc(d.error)}</p>`:!d?`<p style="color:var(--dim)">Sources could not be read.</p>`:topBar+(list.length?`<div class="src-list">${cards}</div>`:emptyState))+addBox;
939
+ $("src-url").value=typed[0];$("src-label").value=typed[1];if(typed[3]!==undefined&&$("src-dir"))$("src-dir").value=typed[3];
940
+ if($("src-to-settings"))$("src-to-settings").onclick=()=>{setQ="sources";openSettings()};
941
+ const act=async(body,done)=>{const j=await post("/sources",body);if(j.ok){done(j);load()}if(srcOpen())openSources()};
835
942
  $("pbody").querySelectorAll("[data-mv]").forEach(m=>{const s=list[+m.dataset.mv],b=$("src-mb-"+m.dataset.mv),to=picked.get(m.dataset.k);
836
943
  if(to&&[...m.options].some(o=>o.value===to))m.value=to;
837
944
  (m.onchange=()=>{b.disabled=m.value===s.dir})();
838
945
  b.onclick=()=>act({op:"move",dir:s.dir,to:m.value,url:s.url},()=>toast(`Source moved. Ideas already gathered stay in ${s.project||"the old project"}.`))});
839
- // A Move button that went back to disabled cannot take focus; its select can.
840
- const was=typed[2]&&$(typed[2]);if(was)(was.disabled?$(was.id.replace("src-mb-","src-mv-")):was).focus();
841
- // Every action names the project it acts on; only "gather everything" goes without one.
842
- const act=async(body,done)=>{const j=await post("/sources",body);if(j.ok){done(j);load()}if(srcOpen())openSources()};
843
- $("src-add").onclick=()=>{const url=$("src-url").value.trim();if(!url){$("src-url").focus();return}const to=$("src-dir").value;if(!to){$("src-dir").focus();return}// A new source is read at once, so the user learns now whether the link works.
946
+ $("pbody").querySelectorAll("[data-chk]").forEach(b=>{const s=list[+b.dataset.chk];
947
+ b.onclick=()=>{b.disabled=true;b.innerHTML=ico("refresh")+" Checking…";
948
+ act({op:"gather",dir:s.dir,url:s.url},j=>{const r=j.results&&j.results[0];
949
+ toast(r?(r.ok?`${s.label||host(s.url)}: ${r.note}`:`Failed: ${r.note}`):"Checked")})}});
950
+ $("pbody").querySelectorAll("[data-rm]").forEach(b=>{const s=list[+b.dataset.rm];
951
+ b.onclick=()=>{if(!confirm(`Remove ${s.label||host(s.url)}? Ideas it gathered stay; added again later, it may bring back ones you deleted.`))return;
952
+ act({op:"remove",dir:list[+b.dataset.rm].dir,url:list[+b.dataset.rm].url},()=>toast("Source removed"))}});
953
+ const was=typed[2]&&$(typed[2]);if(was)(was.disabled?$(was.id.replace("src-mb-","src-mv-")):was)?.focus();
954
+ $("src-add").onclick=()=>{const url=$("src-url").value.trim();if(!url){$("src-url").focus();return}const to=$("src-dir").value;if(!to){$("src-dir").focus();return}
844
955
  $("src-add").disabled=true;$("src-add").textContent="Adding…";
845
956
  (async()=>{const j=await post("/sources",{op:"add",dir:to,url,label:$("src-label").value.trim()});
846
957
  if(j.ok){$("src-url").value=$("src-label").value="";const g=await post("/sources",{op:"gather",dir:to,url}),r=g.results&&g.results[0];
847
958
  toast(r?(r.ok?`Source added: ${r.note}`:`Source added, but: ${r.note}`):"Source added");load()}
959
+ else toast(j.error||"Could not add source");
848
960
  if(srcOpen())openSources()})()};
849
961
  $("src-url").onkeydown=$("src-label").onkeydown=e=>{if(e.key==="Enter")$("src-add").click()};
850
- $("src-go").onclick=()=>{$("src-go").disabled=true;$("src-go").textContent="Gathering…";act({op:"gather",dir:dir||undefined},j=>{const n=(j.results||[]).reduce((a,r)=>a+r.added,0),bad=(j.results||[]).filter(r=>!r.ok).length;toast((n?`${n} new idea${n===1?"":"s"}`:"Nothing new")+(bad?`, ${bad} source${bad===1?"":"s"} could not be read`:""))})};
851
- $("pbody").querySelectorAll("[data-rm]").forEach(b=>b.onclick=()=>{const s=list[+b.dataset.rm];if(!confirm(`Remove ${s.label||host(s.url)}? Ideas it gathered stay; added again later, it may bring back ones you deleted.`))return;act({op:"remove",dir:list[+b.dataset.rm].dir,url:list[+b.dataset.rm].url},()=>toast("Source removed"))})}
852
- let tt;function toast(s){const t=$("toast");t.textContent=s;t.classList.add("on");clearTimeout(tt);tt=setTimeout(()=>t.classList.remove("on"),4000)}
962
+ if($("src-go"))$("src-go").onclick=()=>{$("src-go").disabled=true;$("src-go").innerHTML=ico("refresh")+" Gathering…";
963
+ act({op:"gather",dir:dir||undefined},j=>{const n=(j.results||[]).reduce((a,r)=>a+r.added,0),bad=(j.results||[]).filter(r=>!r.ok).length;
964
+ toast((n?`${n} new idea${n===1?"":"s"}`:"Nothing new")+(bad?`, ${bad} source${bad===1?"":"s"} could not be read`:""))})}}
965
+ // With `report`, the toast carries a link to a prefilled GitHub bug form (/issue) and
966
+ // stays long enough to click it.
967
+ let tt;function toast(s,report){const t=$("toast");t.textContent=s;t.classList.toggle("rep",!!report);if(report){const a=document.createElement("a");a.href="/issue?"+new URLSearchParams(report);a.target="_blank";a.rel="noopener";a.textContent="Report";t.append(" ",a)}t.classList.add("on");clearTimeout(tt);tt=setTimeout(()=>t.classList.remove("on"),report?8000:4000)}
853
968
  function askNotify(){try{if(typeof Notification!=="undefined"&&Notification.permission==="default")Notification.requestPermission()}catch(e){}}
854
969
  function notifyUser(body){try{if(typeof Notification!=="undefined"&&Notification.permission==="granted")new Notification("kaizen",{body,icon:"/logo.png"})}catch(e){}}
855
970
  // The project picker: a button shaped like a select that opens a search field over the list.