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/README.md +65 -572
- package/cli/board.test.ts +67 -0
- package/cli/dashboard.ts +1 -0
- package/cli/install.ts +14 -0
- package/cli/settings.ts +2 -2
- package/cli/state.test.ts +112 -1
- package/cli/state.ts +158 -41
- package/cli/web.ts +65 -3
- package/package.json +1 -1
- package/skills/kaizen/SKILL.md +15 -1
- package/skills/kaizen/spec-build.md +5 -0
- package/skills/kaizen/spec-main.md +5 -2
- package/skills/kaizen/spec.md +16 -2
- package/web/board.html +162 -47
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;
|
|
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:
|
|
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 .
|
|
149
|
-
|
|
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
|
-
.
|
|
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;
|
|
227
|
-
.help
|
|
228
|
-
|
|
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
|
|
292
|
-
<kbd>←↓↑→</kbd><kbd>hjkl</kbd>move between cards
|
|
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){
|
|
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){
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
else
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
|
478
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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"
|
|
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
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
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
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
$("
|
|
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").
|
|
851
|
-
|
|
852
|
-
|
|
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.
|