@lovek4d/kickflip 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTEXT.md +151 -115
- package/README.md +23 -14
- package/SKILL.md +37 -37
- package/bin/doctor.mjs +4 -4
- package/bin/install.mjs +3 -3
- package/bin/kickflip.mjs +34 -26
- package/bin/wait-events.mjs +2 -2
- package/board/arch-review.mjs +1 -1
- package/board/auto-review.mjs +2 -0
- package/board/bin/board.mjs +186 -69
- package/board/domain.mjs +8 -5
- package/board/finish.mjs +3 -0
- package/board/headless.mjs +3 -0
- package/board/on-host.mjs +1 -1
- package/board/permissions.mjs +2 -0
- package/board/public/app-arch.js +2 -2
- package/board/public/app-asks.js +57 -38
- package/board/public/app-banner.js +2 -2
- package/board/public/app-cards.js +156 -72
- package/board/public/app-core.js +2 -4
- package/board/public/app-header.js +12 -5
- package/board/public/app-lanes.js +201 -47
- package/board/public/app-needs.js +2 -1
- package/board/public/app-snapshot.js +2 -2
- package/board/public/app-threads.js +11 -14
- package/board/public/app.html +2 -1
- package/board/public/app.js +53 -83
- package/board/public/attach.js +39 -3
- package/board/public/dialogs.js +59 -14
- package/board/public/hub-kit.js +9 -6
- package/board/public/hub.css +5 -18
- package/board/public/linear-key.js +6 -4
- package/board/public/log.html +48 -0
- package/board/public/prompts.js +9 -8
- package/board/public/prototype-kit.js +6 -6
- package/board/public/prototype-switcher.js +13 -5
- package/board/public/prototype.html +1 -0
- package/board/public/router.js +8 -7
- package/board/public/rules-editor.js +3 -4
- package/board/public/shell.js +20 -6
- package/board/public/sidebar.css +21 -1
- package/board/public/sidebar.js +64 -31
- package/board/public/style.css +155 -62
- package/board/public/toasts.js +2 -2
- package/board/public/ui-cards.js +176 -71
- package/board/public/ui-enums.js +5 -4
- package/board/public/ui-header.js +10 -10
- package/board/public/ui-hub.js +4 -4
- package/board/public/ui-markdown.js +1 -1
- package/board/public/ui-order.js +9 -6
- package/board/public/ui-page-header.js +10 -7
- package/board/public/ui-patch.js +53 -0
- package/board/public/ui-phase-bars.js +17 -0
- package/board/public/ui-prompts.js +8 -8
- package/board/public/ui-questions.js +26 -23
- package/board/public/ui-sidebar.js +46 -22
- package/board/public/ui-snapshot.js +20 -9
- package/board/public/ui-threads.js +17 -11
- package/board/public/ui.js +80 -12
- package/board/public/view-frame.js +1 -1
- package/board/public/view-hub-settings.js +25 -18
- package/board/public/view-hub.js +53 -12
- package/board/public/view-notes.js +1 -1
- package/board/public/view-questions.js +101 -50
- package/board/public/view-settings.js +167 -49
- package/board/review.mjs +1 -1
- package/board/server.mjs +18 -12
- package/board/store.mjs +13 -12
- package/board/tickets.mjs +3 -3
- package/board/worktree.mjs +20 -14
- package/docs/hub-session.md +4 -4
- package/hooks-plugin/hooks/codex-share-auto.mjs +1 -1
- package/hooks-plugin/hooks/watcher-stop.mjs +2 -2
- package/hub/board-repo.mjs +1 -1
- package/hub/boards.mjs +4 -4
- package/hub/close-board.mjs +1 -1
- package/hub/github.mjs +4 -4
- package/hub/home.mjs +1 -1
- package/hub/hub.mjs +12 -12
- package/hub/incoming.mjs +2 -0
- package/hub/linear.mjs +2 -2
- package/hub/live.mjs +2 -2
- package/hub/new-board.mjs +2 -2
- package/hub/permission-denied-hook.mjs +2 -0
- package/hub/pr-report.mjs +3 -1
- package/hub/presence.mjs +1 -1
- package/hub/prompts.mjs +1 -1
- package/hub/push.mjs +2 -2
- package/hub/readiness.mjs +2 -2
- package/hub/repos.mjs +3 -3
- package/hub/restart-board.mjs +1 -1
- package/hub/schedule.mjs +1 -1
- package/hub/sessions.mjs +2 -2
- package/hub/sw.js +1 -1
- package/hub/try.mjs +8 -4
- package/hub/usage.mjs +3 -3
- package/hub/views.mjs +1 -1
- package/package.json +5 -1
- package/shared/card-prs.mjs +1 -0
- package/shared/colors.css +5 -4
- package/shared/fake-gh.mjs +59 -8
- package/shared/repos.mjs +1 -1
- package/shared/usage-gate.mjs +1 -1
- package/board/public/view-polling.js +0 -87
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Board Settings, a modal the app shell (shell.js) mounts over its board for /<route>/settings.html.
|
|
2
|
-
// Saving reviewers sends a `reviewers` event {add: [name], remove: [name]};
|
|
3
|
-
//
|
|
2
|
+
// Saving reviewers sends a `reviewers` event {add: [name], remove: [name]}; the server removes names itself and the list
|
|
3
|
+
// redraws from its answer, while Claude finds each new person's Slack and GitHub accounts, adds them with board.mjs
|
|
4
|
+
// reviewers, and replies. Tickets save on each change, and Done, ✕,
|
|
4
5
|
// Escape and the backdrop wait for that save to land before closing. An unsaved edit marks [data-dirty], and the shell
|
|
5
6
|
// asks before closing the modal then.
|
|
6
7
|
(globalThis.AppViews ||= {}).settings = {
|
|
@@ -14,29 +15,49 @@
|
|
|
14
15
|
<h2>Tickets</h2>
|
|
15
16
|
<p class="muted small">Every card on this board has a ticket from here. A new card gets a new ticket unless you name one.</p>
|
|
16
17
|
<form id="ticketsForm" class="tk-section"><div id="boardTickets">Loading…</div></form>
|
|
18
|
+
<div class="tk-inline" id="ticketsKey" hidden></div>
|
|
17
19
|
<p class="muted small" id="ticketsStatus" role="status"></p>
|
|
18
|
-
<h2>
|
|
19
|
-
<label class="who" for="
|
|
20
|
-
<span
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
<h2>Auto</h2>
|
|
21
|
+
<label class="who" for="autoPull"><span><span>Auto implement Todo cards</span>
|
|
22
|
+
<span class="muted small sub">Claude starts a Todo card as soon as it’s ready and nothing blocks or holds it, while an implementer slot is free.</span></span>
|
|
23
|
+
<input type="checkbox" class="toggle" id="autoPull" disabled></label>
|
|
24
|
+
<p class="muted small" id="autoPullStatus" role="status"></p>
|
|
25
|
+
<label class="who" for="maxImplementers"><span><span>Maximum parallel implementers</span>
|
|
26
|
+
<span class="muted small sub">Cards building or waiting in Orchestrator review count. Grills and cards waiting on you don’t.</span></span>
|
|
27
|
+
<input type="number" class="count" id="maxImplementers" min="1" max="99" step="1" inputmode="numeric" disabled></label>
|
|
28
|
+
<p class="muted small" id="maxImplementersStatus" role="status"></p>
|
|
29
|
+
<label class="who" for="headlessBuilders"><span><span>Builders launch headless</span>
|
|
30
|
+
<span class="muted small sub">The board launches each ready card’s builder from its notes, and the builder lands its own work. Ready needs a size and notes.</span></span>
|
|
31
|
+
<input type="checkbox" class="toggle" id="headlessBuilders" disabled></label>
|
|
32
|
+
<p class="muted small" id="headlessBuildersStatus" role="status"></p>
|
|
26
33
|
<label class="who" for="autoBuild"><span><span>Auto-build cards</span>
|
|
27
34
|
<span class="muted small sub" id="autoBuildSub">${BoardUI.esc(BoardUI.tryWords('web').autoSub)}</span></span>
|
|
28
35
|
<input type="checkbox" class="toggle" id="autoBuild" disabled></label>
|
|
29
36
|
<p class="muted small" id="autoBuildStatus" role="status"></p>
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
<label class="who" for="autoReview"><span><span>Auto review</span>
|
|
38
|
+
<span class="muted small sub">Claude reviews teammates’ PRs that touch this board’s work. May use a lot of usage.</span></span>
|
|
39
|
+
<input type="checkbox" class="toggle" id="autoReview" disabled></label>
|
|
40
|
+
<p class="muted small" id="autoReviewStatus" role="status"></p>
|
|
41
|
+
<label class="who" for="autoMerge"><span><span>Auto-merge</span>
|
|
42
|
+
<span class="muted small sub">Claude merges a PR once it is approved, green and merges cleanly.</span></span>
|
|
43
|
+
<input type="checkbox" class="toggle" id="autoMerge" disabled></label>
|
|
44
|
+
<p class="muted small" id="autoMergeStatus" role="status"></p>
|
|
45
|
+
<div id="skipDraftRow" hidden><label class="who" for="skipDraft"><span><span>Skip the draft</span>
|
|
46
|
+
<span class="muted small sub">A builder’s PR opens as a draft and is marked ready for review once CI passes, so it goes to reviewers without waiting for you.</span></span>
|
|
47
|
+
<input type="checkbox" class="toggle" id="skipDraft" disabled></label>
|
|
48
|
+
<p class="muted small">With Auto-merge on too, a builder’s PR can merge once reviewers approve it, without you seeing it.</p>
|
|
49
|
+
<p class="muted small" id="skipDraftStatus" role="status"></p></div>
|
|
50
|
+
<label class="who" for="autoTriage"><span><span>Triage overnight</span>
|
|
51
|
+
<span class="muted small sub">Once a night, when two or more cards wait on your answers, Claude merges their overlapping questions and duplicate cards, and tidies Todo.</span></span>
|
|
38
52
|
<input type="checkbox" class="toggle" id="autoTriage" disabled></label>
|
|
39
53
|
<p class="muted small" id="autoTriageStatus" role="status"></p>
|
|
54
|
+
<label class="who" for="idleNotification"><span><span>Idle notification</span>
|
|
55
|
+
<span class="muted small sub">Pushes once when the board has been idle for 15 minutes, with what closed since the last one.</span></span>
|
|
56
|
+
<input type="checkbox" class="toggle" id="idleNotification" disabled></label>
|
|
57
|
+
<p class="muted small" id="idleNotificationStatus" role="status"></p>
|
|
58
|
+
<p class="muted small">The hub runs <code>.kickflip/build</code> from each card’s branch and from main. Claude wrote it when it set up this board; if builds break, file a card.</p>
|
|
59
|
+
<h2>Permissions</h2>
|
|
60
|
+
<div id="permissions"><p class="muted small">Loading…</p></div>
|
|
40
61
|
<h2>Reviewers</h2>
|
|
41
62
|
<p class="muted small">Everyone here gets each PR link on this board once it's ready, and the weekday digest. Someone you add also gets the links of PRs already in review.</p>
|
|
42
63
|
<div id="list"><p class="muted small">Loading…</p></div>
|
|
@@ -139,11 +160,15 @@
|
|
|
139
160
|
e.preventDefault();
|
|
140
161
|
if (closing || typed.value !== closure?.title) return;
|
|
141
162
|
closing = true;
|
|
142
|
-
|
|
143
|
-
|
|
163
|
+
// Close board waits until the hub has stopped the board's session and server.
|
|
164
|
+
const post = (async () => {
|
|
144
165
|
const r = await fetch('/_hub/close-board', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ route, name: typed.value }) });
|
|
145
166
|
const out = await r.json();
|
|
146
167
|
if (!r.ok) throw new Error(out.error || 'Could not close board.');
|
|
168
|
+
return out;
|
|
169
|
+
})();
|
|
170
|
+
try {
|
|
171
|
+
const out = await BoardUI.waitOn(closeConfirm, post);
|
|
147
172
|
if (!B.alive()) return;
|
|
148
173
|
closeSheet.close();
|
|
149
174
|
ctx.sidebarChanged?.();
|
|
@@ -159,7 +184,11 @@
|
|
|
159
184
|
const repoURL = `/_hub/board-repo/${encodeURIComponent(B.route)}`;
|
|
160
185
|
let repo = {}, closeRepo = () => {};
|
|
161
186
|
let tickets, savedTickets, savingTickets = null, ticketsTouched = false;
|
|
162
|
-
|
|
187
|
+
// Skip the draft is only for a board with pull requests, on GitHub (docs/adr/2026-11-04-1913-a-board-can-skip-the-draft.md).
|
|
188
|
+
const showRepo = () => {
|
|
189
|
+
$('#boardRepoValue').textContent = repo.github || repo.remote ? `${repo.github || repo.remote} · ${repo.branch || 'Branch not recorded'}` : 'No GitHub repo';
|
|
190
|
+
$('#skipDraftRow').hidden = !repo.github;
|
|
191
|
+
};
|
|
163
192
|
get(repoURL).then(({ body }) => {
|
|
164
193
|
if (!B.alive()) return;
|
|
165
194
|
repo = body;
|
|
@@ -202,11 +231,21 @@
|
|
|
202
231
|
return false;
|
|
203
232
|
}
|
|
204
233
|
}
|
|
234
|
+
// A save waits on the control last tapped or changed, found by selector since ticketsControl redraws it, and every
|
|
235
|
+
// other tickets control is disabled meanwhile (ui.js waitOn).
|
|
236
|
+
let ticketsTapped = '[data-tk-source][aria-pressed="true"]';
|
|
237
|
+
const tapped = (e) => {
|
|
238
|
+
const source = e.target.closest?.('[data-tk-source]');
|
|
239
|
+
if (source) ticketsTapped = `[data-tk-source="${source.dataset.tkSource}"]`;
|
|
240
|
+
else if (e.target.dataset?.tk) ticketsTapped = `[data-tk="${e.target.dataset.tk}"]`;
|
|
241
|
+
};
|
|
242
|
+
B.on(ticketsForm, 'click', tapped, true);
|
|
243
|
+
B.on(ticketsForm, 'change', tapped, true);
|
|
205
244
|
// One save at a time: a change made while one is out saves once it lands.
|
|
206
245
|
function saveTickets() {
|
|
207
246
|
syncTickets();
|
|
208
247
|
if (savingTickets || !unsaved() || !tickets.valid()) return;
|
|
209
|
-
ticketsStatus.
|
|
248
|
+
ticketsStatus.textContent = '';
|
|
210
249
|
LinearKey.doneBusy(modal.querySelectorAll('[data-settings-dismiss]'), true);
|
|
211
250
|
savingTickets = (async () => {
|
|
212
251
|
let saved, value;
|
|
@@ -220,6 +259,8 @@
|
|
|
220
259
|
LinearKey.doneBusy(modal.querySelectorAll('[data-settings-dismiss]'), false);
|
|
221
260
|
syncTickets();
|
|
222
261
|
})();
|
|
262
|
+
BoardUI.waitOn(ticketsForm.querySelector(ticketsTapped) || ticketsForm.querySelector('[data-tk-source][aria-pressed="true"]'), savingTickets,
|
|
263
|
+
{ group: ticketsForm.querySelectorAll('button, select, input') });
|
|
223
264
|
}
|
|
224
265
|
B.on(ticketsForm, 'submit', (e) => { e.preventDefault(); saveTickets(); });
|
|
225
266
|
let buildKind = 'web';
|
|
@@ -263,24 +304,28 @@
|
|
|
263
304
|
B.on($('#save'), 'click', async () => {
|
|
264
305
|
const status = $('#saveStatus');
|
|
265
306
|
try {
|
|
266
|
-
await site.send('reviewers', { add: [...add], remove: [...remove] });
|
|
307
|
+
const out = await BoardUI.waitOn($('#save'), site.send('reviewers', { add: [...add], remove: [...remove] }));
|
|
308
|
+
status.textContent = add.size ? 'Sent. Claude updates the list and replies below.' : 'Saved';
|
|
309
|
+
current = out.board.reviewers;
|
|
267
310
|
add.clear(); remove.clear(); render();
|
|
268
|
-
|
|
269
|
-
} catch { status.textContent = 'Could not send. Is the host awake?'; }
|
|
311
|
+
} catch (e) { status.textContent = e?.reason || 'Could not send. Is the host awake?'; }
|
|
270
312
|
});
|
|
271
313
|
B.watch((events) => {
|
|
272
314
|
$('#replies').innerHTML = events.map((e) => `<div class="reply">${e.reply ? (e.reply.html ?? '') : '<span class="muted small">Waiting for Claude…</span>'}</div>`).join('');
|
|
273
315
|
}, { kind: 'reviewers' });
|
|
274
316
|
|
|
275
317
|
// The toggle saves at once through POST api/settings, which writes board.json itself, so it takes effect with no
|
|
276
|
-
// session awake (docs/adr/
|
|
318
|
+
// session awake (docs/adr/2026-10-29-1752-auto-merge-is-a-board-setting-off-by-default.md).
|
|
277
319
|
const autoMerge = $('#autoMerge');
|
|
278
320
|
const autoMergeStatus = $('#autoMergeStatus');
|
|
321
|
+
let savedMax = '';
|
|
279
322
|
get(site.url('api/settings'), waitIn('#autoMergeStatus')).then(({ body: d }) => {
|
|
280
323
|
if (!B.alive()) return;
|
|
281
324
|
savedTickets = d.settings?.tickets || null;
|
|
282
325
|
tickets = BoardDialogs.ticketsControl($('#boardTickets'), { value: savedTickets || { source: 'internal', prefix: 'INT' }, repo: () => repo.github || '', title: () => ctx.siteTitle || '',
|
|
283
326
|
loadLinear: async () => (await get('/_hub/api/linear')).body,
|
|
327
|
+
keyState: async () => (await get('/_hub/api/settings')).body.linear, keySlot: $('#ticketsKey'),
|
|
328
|
+
keyBusy: (on) => LinearKey.doneBusy(modal.querySelectorAll('[data-settings-dismiss]'), on),
|
|
284
329
|
change(_value, _valid, { loaded, typing } = {}) {
|
|
285
330
|
if (!loaded) ticketsTouched = true;
|
|
286
331
|
if (typing) syncTickets(); else saveTickets();
|
|
@@ -297,9 +342,27 @@
|
|
|
297
342
|
$('#autoBuild').disabled = false;
|
|
298
343
|
$('#autoPull').checked = d.settings?.auto_pull === true;
|
|
299
344
|
$('#autoPull').disabled = false;
|
|
345
|
+
$('#maxImplementers').value = savedMax = String(d.settings?.max_implementers);
|
|
346
|
+
$('#maxImplementers').disabled = false;
|
|
300
347
|
$('#autoTriage').checked = d.settings?.auto_triage === true;
|
|
301
348
|
$('#autoTriage').disabled = false;
|
|
349
|
+
$('#autoReview').checked = d.settings?.auto_review === true;
|
|
350
|
+
$('#autoReview').disabled = false;
|
|
351
|
+
$('#headlessBuilders').checked = d.settings?.headless_builders === true;
|
|
352
|
+
$('#headlessBuilders').disabled = false;
|
|
353
|
+
$('#idleNotification').checked = d.settings?.idle_notification === true;
|
|
354
|
+
$('#idleNotification').disabled = false;
|
|
355
|
+
$('#skipDraft').checked = d.settings?.skip_draft === true;
|
|
356
|
+
$('#skipDraft').disabled = false;
|
|
302
357
|
}).catch(() => { autoMergeStatus.textContent = ticketsStatus.textContent = 'Offline. Is the host awake?'; });
|
|
358
|
+
// An Auto toggle keeps its face, disabled with a spinner beside it (ui.js waitOn), while POST api/settings saves it.
|
|
359
|
+
// → the saved settings
|
|
360
|
+
const saveToggle = (toggle, body) => BoardUI.waitOn(toggle, (async () => {
|
|
361
|
+
const r = await fetch(site.url('api/settings'), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
|
|
362
|
+
const out = await r.json().catch(() => ({}));
|
|
363
|
+
if (!r.ok) throw new Error(out.error);
|
|
364
|
+
return out.settings;
|
|
365
|
+
})());
|
|
303
366
|
B.on(autoMerge, 'change', async () => {
|
|
304
367
|
const want = autoMerge.checked;
|
|
305
368
|
if (want && !(await BoardDialogs.confirm('Turn on auto-merge? Claude merges each approved PR on this board once CI is green and it merges cleanly, without asking you.'))) {
|
|
@@ -307,59 +370,114 @@
|
|
|
307
370
|
return;
|
|
308
371
|
}
|
|
309
372
|
if (!B.alive()) return;
|
|
310
|
-
autoMerge.disabled = true;
|
|
311
373
|
try {
|
|
312
|
-
|
|
313
|
-
const out = await r.json().catch(() => ({}));
|
|
314
|
-
if (!r.ok) throw new Error(out.error);
|
|
315
|
-
autoMerge.checked = out.settings.auto_merge === true;
|
|
374
|
+
autoMerge.checked = (await saveToggle(autoMerge, { auto_merge: want })).auto_merge === true;
|
|
316
375
|
autoMergeStatus.textContent = autoMerge.checked ? 'Saved. Claude merges approved, green PRs itself.' : 'Saved. Approved, green PRs wait for your Merge.';
|
|
317
376
|
} catch (e) {
|
|
318
377
|
autoMerge.checked = !want;
|
|
319
378
|
autoMergeStatus.textContent = e.message || 'Could not save. Is the host awake?';
|
|
320
379
|
}
|
|
321
|
-
autoMerge.disabled = false;
|
|
322
380
|
});
|
|
323
381
|
|
|
324
382
|
B.on($('#autoBuild'), 'change', async () => {
|
|
325
383
|
const toggle = $('#autoBuild');
|
|
326
384
|
const want = toggle.checked;
|
|
327
|
-
toggle.disabled = true;
|
|
328
385
|
try {
|
|
329
|
-
const
|
|
330
|
-
const out = await r.json();
|
|
331
|
-
if (!r.ok) throw new Error(out.error);
|
|
386
|
+
const settings = await saveToggle(toggle, { auto_build: want });
|
|
332
387
|
if (!B.alive()) return;
|
|
333
|
-
toggle.checked =
|
|
388
|
+
toggle.checked = settings.auto_build === true;
|
|
334
389
|
$('#autoBuildStatus').textContent = BoardUI.tryWords(buildKind).saved(toggle.checked);
|
|
335
390
|
} catch (e) {
|
|
336
391
|
if (!B.alive()) return;
|
|
337
392
|
toggle.checked = !want;
|
|
338
393
|
$('#autoBuildStatus').textContent = e.message || 'Could not save. Is the host awake?';
|
|
339
|
-
}
|
|
394
|
+
}
|
|
340
395
|
});
|
|
341
396
|
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
|
|
397
|
+
// These Auto toggles save with no confirmation: docs/adr/2026-11-02-1754-an-idle-board-pulls-in-its-top-todo-card.md,
|
|
398
|
+
// docs/adr/2026-11-04-1900-triage-runs-once-overnight-on-cards-waiting-on-your-answers.md and
|
|
399
|
+
// docs/adr/2026-11-03-1731-each-board-reviews-incoming-prs-that-touch-its-work.md; Idle notification too:
|
|
400
|
+
// docs/adr/2026-11-04-0257-an-idle-board-pushes-once-when-it-has-gone-idle.md.
|
|
401
|
+
const autoToggle = (id, key, saved) => B.on($(`#${id}`), 'change', async () => {
|
|
345
402
|
const toggle = $(`#${id}`);
|
|
346
403
|
const want = toggle.checked;
|
|
347
|
-
toggle.disabled = true;
|
|
348
404
|
try {
|
|
349
|
-
const
|
|
350
|
-
const out = await r.json().catch(() => ({}));
|
|
351
|
-
if (!r.ok) throw new Error(out.error);
|
|
405
|
+
const settings = await saveToggle(toggle, { [key]: want });
|
|
352
406
|
if (!B.alive()) return;
|
|
353
|
-
toggle.checked =
|
|
407
|
+
toggle.checked = settings[key] === true;
|
|
354
408
|
$(`#${id}Status`).textContent = saved(toggle.checked);
|
|
355
409
|
} catch (e) {
|
|
356
410
|
if (!B.alive()) return;
|
|
357
411
|
toggle.checked = !want;
|
|
358
412
|
$(`#${id}Status`).textContent = e.message || 'Could not save. Is the host awake?';
|
|
359
|
-
}
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
autoToggle('autoPull', 'auto_pull', (on) => (on ? 'Saved. Claude starts ready Todo cards while an implementer slot is free.' : 'Saved. Todo cards wait for you.'));
|
|
416
|
+
|
|
417
|
+
// docs/adr/2026-11-04-0057-auto-implement-fills-in-progress-up-to-a-cap.md: Maximum parallel implementers saves on
|
|
418
|
+
// change, waiting on the server like a toggle; a number outside 1 to 99 or a refusal puts the saved one back.
|
|
419
|
+
B.on($('#maxImplementers'), 'change', async () => {
|
|
420
|
+
const input = $('#maxImplementers');
|
|
421
|
+
const status = $('#maxImplementersStatus');
|
|
422
|
+
const want = /^\d+$/.test(input.value) ? Number(input.value) : 0;
|
|
423
|
+
if (want < 1 || want > 99) { input.value = savedMax; status.textContent = 'Pick a whole number from 1 to 99.'; return; }
|
|
424
|
+
try {
|
|
425
|
+
const settings = await saveToggle(input, { max_implementers: want });
|
|
426
|
+
if (!B.alive()) return;
|
|
427
|
+
input.value = savedMax = String(settings.max_implementers);
|
|
428
|
+
status.textContent = `Saved. Up to ${settings.max_implementers === 1 ? '1 card builds or waits' : `${settings.max_implementers} cards build or wait`} in Orchestrator review at once.`;
|
|
429
|
+
} catch (e) {
|
|
430
|
+
if (!B.alive()) return;
|
|
431
|
+
input.value = savedMax;
|
|
432
|
+
status.textContent = e.message || 'Could not save. Is the host awake?';
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
autoToggle('autoReview', 'auto_review', (on) => (on ? 'Saved. When idle, Claude reviews incoming PRs that touch this board’s work.' : 'Saved. Claude leaves incoming PRs alone.'));
|
|
436
|
+
autoToggle('headlessBuilders', 'headless_builders', (on) => (on ? 'Saved. The board launches each ready card’s builder itself, and the builder lands its own work.' : 'Saved. Claude briefs and launches each builder.'));
|
|
437
|
+
autoToggle('idleNotification', 'idle_notification', (on) => (on ? 'Saved. One push once the board has been idle for 15 minutes.' : 'Saved. No push when the board goes idle.'));
|
|
438
|
+
autoToggle('autoTriage', 'auto_triage', (on) => (on ? 'Saved. Once a night, when cards wait on your answers, Claude triages them.' : 'Saved. Questions and Todo are left as they are.'));
|
|
439
|
+
autoToggle('skipDraft', 'skip_draft', (on) => (on ? 'Saved. A builder’s draft PR is marked ready for review once CI passes.' : 'Saved. A builder’s draft PR waits for your review.'));
|
|
440
|
+
|
|
441
|
+
// Each switch writes or removes one allow rule in the board folder's .claude/settings.local.json through POST
|
|
442
|
+
// api/permissions, which only the host's own browser may send; elsewhere the switches are read-only
|
|
443
|
+
// (docs/adr/2026-11-03-1921-board-settings-grant-a-boards-session-permissions.md). A tap waits on the server with the
|
|
444
|
+
// switch as it was and a spinner beside it, then draws the server's answer.
|
|
445
|
+
const permissions = $('#permissions');
|
|
446
|
+
const REFUSED = {
|
|
447
|
+
denied: (command) => `Auto mode still refused this once: ${command}. Approve it under /permissions, Recently blocked.`,
|
|
448
|
+
asked: (command) => `The session still asked before running ${command}, so it hasn’t picked the rule up yet. It applies once the session restarts.`,
|
|
449
|
+
};
|
|
450
|
+
const permRow = (p, owner) => `<label class="who" for="perm-${esc(p.id)}"><span><span>${esc(p.name)}</span>
|
|
451
|
+
<span class="muted small sub">${esc(p.allows)} Writes <code>${esc(p.rule)}</code></span></span>
|
|
452
|
+
<span class="toggle-slot"><span class="try-spin toggle-spin" aria-hidden="true"></span><input type="checkbox" class="toggle" data-perm="${esc(p.id)}" id="perm-${esc(p.id)}"${p.on ? ' checked' : ''}${owner ? '' : ' disabled'}></span></label>
|
|
453
|
+
<p class="muted small" id="perm-${esc(p.id)}Status" role="status"></p>
|
|
454
|
+
${p.refused ? `<p class="muted small refused-note">${esc(REFUSED[p.refused.how](p.refused.command))}</p>` : ''}`;
|
|
455
|
+
const drawPermissions = (d) => {
|
|
456
|
+
permissions.innerHTML = d.permissions.map((p) => permRow(p, d.owner)).join('')
|
|
457
|
+
+ (d.owner ? '' : '<p class="muted small" id="permissionsNotOwner">Only the signed-in owner can change these.</p>');
|
|
458
|
+
};
|
|
459
|
+
get(site.url('api/permissions'), waitIn('#permissions')).then(({ ok, body }) => {
|
|
460
|
+
if (!B.alive()) return;
|
|
461
|
+
if (!ok) { permissions.innerHTML = `<p class="muted small">${esc(body.error || 'Could not load permissions.')}</p>`; return; }
|
|
462
|
+
drawPermissions(body);
|
|
463
|
+
}).catch(() => { if (B.alive()) permissions.innerHTML = '<p class="muted">Offline. Is the host awake?</p>'; });
|
|
464
|
+
B.on(permissions, 'change', async (e) => {
|
|
465
|
+
const toggle = e.target.closest?.('input[data-perm]');
|
|
466
|
+
if (!toggle) return;
|
|
467
|
+
const id = toggle.dataset.perm;
|
|
468
|
+
const on = toggle.checked;
|
|
469
|
+
toggle.checked = !on;
|
|
470
|
+
try {
|
|
471
|
+
const r = await BoardUI.waitOn(toggle, fetch(site.url('api/permissions'), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id, on }) }));
|
|
472
|
+
const out = await r.json().catch(() => ({}));
|
|
473
|
+
if (!r.ok) throw new Error(out.error);
|
|
474
|
+
if (!B.alive()) return;
|
|
475
|
+
drawPermissions(out);
|
|
476
|
+
$(`#perm-${id}Status`).textContent = on ? 'Saved. The session runs it without asking.' : 'Saved. The session asks first again.';
|
|
477
|
+
} catch (error) {
|
|
478
|
+
if (B.alive()) $(`#perm-${id}Status`).textContent = error.message || 'Could not save. Is the host awake?';
|
|
479
|
+
}
|
|
360
480
|
});
|
|
361
|
-
todoToggle('autoPull', 'auto_pull', (on) => (on ? 'Saved. When no implementer runs, Claude pulls in the top Todo card.' : 'Saved. Todo cards wait until they are unblocked, answered or bumped.'));
|
|
362
|
-
todoToggle('autoTriage', 'auto_triage', (on) => (on ? 'Saved. Once a day, when the board is idle and Todo changed, Claude triages it.' : 'Saved. Todo is left as it is.'));
|
|
363
481
|
|
|
364
482
|
// Saving writes board.json through POST api/rules, whose `rules {scope: "board"}` event wakes Claude to acknowledge
|
|
365
483
|
// the change; replies show below, newest first.
|
package/board/review.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var d=Object.defineProperty;var l=(t,e)=>d(t,"name",{value:e,configurable:!0});import m from"node:fs";import p from"node:path";import w from"node:os";import{routeRecord as a}from"../hub/routes.mjs";import{storedPRs as b}from"../shared/card-prs.mjs";function N(){const t=p.join(w.homedir(),".claude","slack-bridge","topics"),e=Object.create(null);let r=[];try{r=m.readdirSync(t).filter(o=>o.endsWith(".json")).sort()}catch{}for(const o of r)try{const i=JSON.parse(m.readFileSync(p.join(t,o),"utf8"));for(const[f,s]of Object.entries(i.items||{}))e[f]={...e[f],...s}}catch{}return e}l(N,"topicItems");function k(t,e,r,o={}){const i=Object.create(null),f=[e?.review_link_ts,e?.review_sent,a(t?.review_link_ts,b(e)[0]?.number),a(t?.review_link_ts,r),a(o,r)?.review_link_ts];for(const s of f)for(const[u,n]of Array.isArray(s)?s.map(c=>[c,null]):Object.entries(s||{})){if(Object.hasOwn(i,u))continue;const c=typeof n=="string"?n:n?.ts;n!==null&&(typeof c!="string"||!/^\d+(?:\.\d+)?$/.test(c))||(i[u]={ts:c||null,...typeof n?.channel=="string"?{channel:n.channel}:{}})}return i}l(k,"reviewLinks");const g=l(t=>{const e=t?Number(t)*1e3:NaN;return Number.isFinite(e)&&!Number.isNaN(new Date(e).getTime())?new Date(e).toISOString():null},"reviewTime");function S(t,e=[]){return[...new Map([...e,...t.reviewers||[]].map(r=>[r.name,r])).values()]}l(S,"reviewersOf");export{k as reviewLinks,g as reviewTime,S as reviewersOf,N as topicItems};
|