@lovek4d/kickflip 0.7.1 → 0.7.4
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 +25 -21
- package/SKILL.md +13 -9
- package/bin/event-lines.mjs +1 -1
- package/bin/install.mjs +3 -3
- package/bin/kickflip.mjs +1 -1
- package/bin/wait-events.mjs +2 -2
- package/board/bin/board.mjs +68 -50
- package/board/domain.mjs +7 -7
- package/board/finish.mjs +4 -4
- package/board/headless.mjs +8 -8
- package/board/hub-lines.mjs +1 -1
- package/board/onboarding.mjs +3 -3
- package/board/permissions.mjs +2 -2
- package/board/public/app-asks.js +2 -14
- package/board/public/app-banner.js +8 -3
- package/board/public/app-cards.js +7 -20
- package/board/public/app-core.js +2 -2
- package/board/public/app-lanes.js +7 -15
- package/board/public/app-reader.js +7 -3
- package/board/public/app-snapshot.js +1 -1
- package/board/public/app.js +1 -1
- package/board/public/attach.js +18 -2
- package/board/public/dialogs.js +10 -12
- package/board/public/hub.css +0 -3
- package/board/public/linear-key.js +1 -1
- package/board/public/sidebar.css +1 -1
- package/board/public/style.css +42 -36
- package/board/public/ui-cards.js +33 -27
- package/board/public/ui-enums.js +2 -2
- package/board/public/ui-links.js +0 -1
- package/board/public/ui-order.js +1 -1
- package/board/public/ui-questions.js +8 -6
- package/board/public/ui-sidebar.js +1 -1
- package/board/public/ui-threads.js +26 -22
- package/board/public/ui.js +15 -9
- package/board/public/view-hub-settings.js +25 -30
- package/board/public/view-hub.js +23 -5
- package/board/public/view-questions.js +1 -1
- package/board/public/view-settings.js +4 -4
- package/board/server.mjs +18 -18
- package/board/store.mjs +30 -27
- package/board/tickets.mjs +3 -3
- package/board/worktree.mjs +15 -15
- package/hub/board-repo.mjs +1 -1
- package/hub/github.mjs +3 -3
- package/hub/hub.mjs +11 -11
- package/hub/linear.mjs +1 -1
- package/hub/new-board.mjs +2 -2
- package/hub/pr-report.mjs +2 -2
- package/hub/readiness.mjs +1 -1
- package/hub/repos.mjs +3 -3
- package/hub/review-send.mjs +1 -1
- package/hub/schedule.mjs +1 -1
- package/hub/slack.mjs +1 -1
- package/hub/try.mjs +8 -6
- package/hub/views.mjs +1 -1
- package/package.json +1 -1
- package/shared/fake-gh.mjs +19 -4
- package/shared/gh.mjs +4 -0
- package/shared/owner-check.mjs +4 -3
- package/shared/service.mjs +12 -12
- package/starter/server.mjs +3 -3
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
// HubSettings:
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// hours' times and hold to /_hub/settings/quiet, and
|
|
1
|
+
// HubSettings: summary rows open centred settings modals ([hub settings open as modals](docs/adr/2026-11-01-1221-hub-settings-open-as-modals.md)), using
|
|
2
|
+
// settingsView's words from GET /_hub/api/settings. Linear's key and Slack's token open the same modal from the button on their Integrations rows (view-hub.js),
|
|
3
|
+
// with no summary row of their own ([Linear and Slack keys open in modals from their Integrations rows](docs/adr/2026-11-09-2230-linear-and-slack-keys-open-in-modals-from-their-integrations-rows.md)). Pause hub, first, posts to /_hub/settings/hub-pause. Each toggle and snooze posts to /_hub/settings/notifications, Quiet
|
|
4
|
+
// hours' times and hold to /_hub/settings/quiet, and saves at once, as Linear's key and Slack's token do through linear-key.js; Done waits for
|
|
5
|
+
// a key's save to land. The rules editor saves through POST /_hub/settings/rules
|
|
5
6
|
// and marks [data-dirty] while an edit is unsaved, so the shell asks before leaving then. The data refreshes every 30 seconds while nothing is being edited.
|
|
6
7
|
(function () {
|
|
7
8
|
// The last data, which a return to the view paints at once.
|
|
@@ -15,10 +16,6 @@
|
|
|
15
16
|
action: '/_hub/settings/hub-pause', name: 'pause', choices: p.choices, on: p.on, button: 'Resume' });
|
|
16
17
|
const snoozeRow = (d) => durationPicker({ id: 'snooze', title: 'Snooze', icon: HubUI.tile('snooze', '#a855f7'), action: ACTION, name: 'snooze',
|
|
17
18
|
choices: d.snooze?.choices, on: d.snoozed, button: 'End' });
|
|
18
|
-
// A checkbox row like Push notifications, grey while off and orange while on, as the machine working
|
|
19
|
-
// (docs/adr/2026-11-01-1817-colour-says-whos-waiting.md). Unchecked posts nothing, which the hub reads as off.
|
|
20
|
-
const awakeRow = (a) => `<form class="row field" method="post" action="/_hub/settings/keep-awake">${HubUI.tile('coffee', a.on ? 'var(--color-notice)' : 'var(--color-quiet)')}<label class="body"><span class="text"><span class="title">Keep awake</span></span><input type="checkbox" id="awake" name="keep_awake" value="on" class="toggle" data-submit${a.on ? ' checked' : ''}></label></form>`;
|
|
21
|
-
|
|
22
19
|
// The on/off, snooze and weekend rows (docs/adr/2026-10-29-1639-notifications-can-be-off-snoozed-or-off-on-weekends.md).
|
|
23
20
|
function notificationRows(d) {
|
|
24
21
|
const { tile } = HubUI;
|
|
@@ -41,13 +38,13 @@
|
|
|
41
38
|
}
|
|
42
39
|
|
|
43
40
|
const sections = [['pause', 'Pause hub'], ['notifications', 'Notifications'], ['quiet', 'Quiet hours'], ['rules', 'Rules for every board'], ['closed', 'Closed boards']];
|
|
41
|
+
// Linear and Slack open from their Integrations rows, so they have modals and no summary rows.
|
|
42
|
+
const keys = [['linear', 'Linear'], ['slack', 'Slack']];
|
|
44
43
|
const sectionOf = (hash) => {
|
|
45
44
|
const id = { push: 'notifications', 'closed-boards': 'closed', polling: 'quiet' }[hash?.slice(1)] || hash?.slice(1);
|
|
46
|
-
return sections.some(([section]) => section === id) ? id : null;
|
|
45
|
+
return [...sections, ...keys].some(([section]) => section === id) ? id : null;
|
|
47
46
|
};
|
|
48
|
-
const markup = () => `<
|
|
49
|
-
<h2 class="label" id="hubSettingsLabel">Hub settings</h2>
|
|
50
|
-
${sections.map(([id, title]) => `<button type="button" class="hub-setting" id="${id}" data-settings-open="${id}" aria-haspopup="dialog"><span class="text"><span class="title">${title}</span><span class="sub" data-summary="${id}">Loading…</span></span><span class="chev" aria-hidden="true">›</span></button>`).join('')}
|
|
47
|
+
const markup = () => `${sections.map(([id, title]) => `<button type="button" class="hub-setting" id="${id}" data-settings-open="${id}" aria-haspopup="dialog"><span class="text"><span class="title">${title}</span><span class="sub" data-summary="${id}">Loading…</span></span><span class="chev" aria-hidden="true">›</span></button>`).join('')}
|
|
51
48
|
<dialog class="settings-modal hub-modal" id="hubSettingsModal" aria-labelledby="hubSettingsTitle"><div class="settings-head"><div><h2 id="hubSettingsTitle" tabindex="-1"></h2><p class="muted small">Hub settings</p></div><button type="button" class="ghost" data-settings-dismiss aria-label="Close hub settings">✕</button></div><div class="set settings-body"><div class="hs-flash"></div><div class="setting-content"></div></div><div class="settings-footer"><button type="button" data-settings-dismiss>Done</button></div></dialog>`;
|
|
52
49
|
|
|
53
50
|
globalThis.HubSettings = {
|
|
@@ -63,7 +60,7 @@ ${sections.map(([id, title]) => `<button type="button" class="hub-setting" id="$
|
|
|
63
60
|
let flash = null;
|
|
64
61
|
let rules = null;
|
|
65
62
|
let rulesJSON = '';
|
|
66
|
-
|
|
63
|
+
let tokenSlot = null;
|
|
67
64
|
const pickers = HubKit.durationPickers(B);
|
|
68
65
|
|
|
69
66
|
function paint() {
|
|
@@ -71,13 +68,19 @@ ${sections.map(([id, title]) => `<button type="button" class="hub-setting" id="$
|
|
|
71
68
|
const { row, tile, deviceRow } = HubUI;
|
|
72
69
|
top.innerHTML = HubUI.flash(flash);
|
|
73
70
|
const scrollTop = B.$('.settings-body').scrollTop;
|
|
74
|
-
paintInline();
|
|
75
71
|
if (active === 'pause') {
|
|
76
72
|
content.innerHTML = `<div class="group">${pauseRow(last.pause)}</div><p class="note">Running steps finish. No new builders, planners or pulls start on any board until then, as when usage reaches the pause line.</p>`;
|
|
77
73
|
} else if (active === 'notifications') {
|
|
78
74
|
content.innerHTML = `<div class="group">${deviceRow(device.state(last.device), { compact: true })}${notificationRows(last)}</div>`;
|
|
79
75
|
} else if (active === 'quiet') {
|
|
80
76
|
content.innerHTML = quietRows(last.quiet);
|
|
77
|
+
} else if (active === 'linear' || active === 'slack') {
|
|
78
|
+
if (tokenSlot) tokenSlot.set(last[active]);
|
|
79
|
+
else {
|
|
80
|
+
tokenSlot = LinearKey.mount(content, { kind: active, state: last[active],
|
|
81
|
+
saved(out) { last = out; flash = null; paint(); ctx.sidebarChanged?.(); },
|
|
82
|
+
busy: (on) => LinearKey.doneBusy(modal.querySelectorAll('[data-settings-dismiss]'), on) });
|
|
83
|
+
}
|
|
81
84
|
} else if (active === 'closed') {
|
|
82
85
|
content.innerHTML = `<div class="group">${(last.closed || []).length ? last.closed.map((b) => `<div class="row"><span class="body"><span class="text"><span class="title">${esc(b.title)}</span><span class="sub wrap">/${esc(b.route)}</span></span><button class="btn" type="button" data-reopen="${esc(b.route)}"${reopening === b.route ? BoardUI.BUSY : reopening ? ' disabled' : ''}>Reopen</button></span></div>`).join('') : '<p class="note">No closed boards.</p>'}</div>`;
|
|
83
86
|
}
|
|
@@ -116,19 +119,6 @@ ${sections.map(([id, title]) => `<button type="button" class="hub-setting" id="$
|
|
|
116
119
|
}
|
|
117
120
|
}
|
|
118
121
|
|
|
119
|
-
// Keep awake's row and note, and Linear's and Slack's token slots, drawn into the page itself.
|
|
120
|
-
function paintInline() {
|
|
121
|
-
B.$('#keep-awake').innerHTML = last.awake.unsupported ? `<p class="note">${esc(last.awake.unsupported)}.</p>`
|
|
122
|
-
: `<div class="group">${awakeRow(last.awake)}</div><p class="note">Runs <code>caffeinate -s</code> on the host so it doesn’t sleep while boards work, only while it is on AC power. On battery it holds nothing. The screen can still turn off.</p>`;
|
|
123
|
-
for (const kind of ['linear', 'slack']) {
|
|
124
|
-
if (tokenSlots[kind]) tokenSlots[kind].set(last[kind]);
|
|
125
|
-
else {
|
|
126
|
-
tokenSlots[kind] = LinearKey.mount(B.$(`#${kind}`), { kind, state: last[kind],
|
|
127
|
-
saved(out) { last = out; flash = null; paint(); ctx.sidebarChanged?.(); } });
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
122
|
// hub-kit.js device() titles its row 'This device' only where this device can take pushes.
|
|
133
123
|
function deviceSummary() {
|
|
134
124
|
const d = device.state('');
|
|
@@ -140,6 +130,7 @@ ${sections.map(([id, title]) => `<button type="button" class="hub-setting" id="$
|
|
|
140
130
|
function closeModal(restore = true) {
|
|
141
131
|
unfitModal?.(); unfitModal = null;
|
|
142
132
|
modal.close();
|
|
133
|
+
tokenSlot?.destroy(); tokenSlot = null;
|
|
143
134
|
content.innerHTML = '';
|
|
144
135
|
rules = null;
|
|
145
136
|
active = null;
|
|
@@ -157,7 +148,8 @@ ${sections.map(([id, title]) => `<button type="button" class="hub-setting" id="$
|
|
|
157
148
|
active = id;
|
|
158
149
|
if (id) {
|
|
159
150
|
returnFocus = opener || B.$(`#${id}`);
|
|
160
|
-
B.$('#hubSettingsTitle').textContent = sections.find(([section]) => section === id)[1];
|
|
151
|
+
B.$('#hubSettingsTitle').textContent = [...sections, ...keys].find(([section]) => section === id)[1];
|
|
152
|
+
B.$('.settings-head .muted').textContent = keys.some(([key]) => key === id) ? 'Integrations' : 'Hub settings';
|
|
161
153
|
content.innerHTML = id === 'rules' ? '<div id="hubRules" class="rules-box"></div>' : '<p class="note">Loading…</p>';
|
|
162
154
|
flash = null;
|
|
163
155
|
paint();
|
|
@@ -172,7 +164,10 @@ ${sections.map(([id, title]) => `<button type="button" class="hub-setting" id="$
|
|
|
172
164
|
history.replaceState(history.state, '', settingsURL);
|
|
173
165
|
return true;
|
|
174
166
|
};
|
|
175
|
-
const dismiss = () => {
|
|
167
|
+
const dismiss = () => {
|
|
168
|
+
if (!tokenSlot?.busy()) { B.openSettings({ kind: 'hub' }); return; }
|
|
169
|
+
tokenSlot.settled().then(() => { if (B.alive()) B.openSettings({ kind: 'hub' }); });
|
|
170
|
+
};
|
|
176
171
|
B.on(modal, 'cancel', (e) => { e.preventDefault(); dismiss(); });
|
|
177
172
|
B.on(root, 'click', (e) => {
|
|
178
173
|
const opener = e.target.closest?.('[data-settings-open]');
|
|
@@ -187,7 +182,7 @@ ${sections.map(([id, title]) => `<button type="button" class="hub-setting" id="$
|
|
|
187
182
|
if (!(await B.openSettings(AppRouter.viewOf(location.href))) && B.alive()) history.replaceState(history.state, '', settingsURL);
|
|
188
183
|
});
|
|
189
184
|
let reopening = null;
|
|
190
|
-
B.atUnmount(() =>
|
|
185
|
+
B.atUnmount(() => closeModal(false));
|
|
191
186
|
const device = HubKit.device(B, { paint, changed: () => ctx.sidebarChanged?.() });
|
|
192
187
|
device.ready.then(() => { if (B.alive()) paint(); });
|
|
193
188
|
B.on(root, 'click', async (e) => {
|
package/board/public/view-hub.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// The hub page, /_hub/, a view of the app shell (shell.js): a banner while pushes are held or snoozed, the Usage card with Claude model and Codex share, Needs you for the pending
|
|
2
2
|
// prompts of sessions that own no board, Health, Integrations (readiness.mjs words), each site's health and board links,
|
|
3
|
-
// then
|
|
3
|
+
// then hub settings summary rows ([hub settings open as modals](docs/adr/2026-11-01-1221-hub-settings-open-as-modals.md)). Health carries the Keep awake toggle, and Linear's and Slack's Integrations rows
|
|
4
|
+
// a button that opens their key modal ([Linear and Slack keys open in modals from their Integrations rows](docs/adr/2026-11-09-2230-linear-and-slack-keys-open-in-modals-from-their-integrations-rows.md)). A board's row can carry Restart (hub/restart-board.mjs), and
|
|
4
5
|
// the Hub row Restart hub while the hub runs older code than its files. Health comes from GET /_hub/api/hub.
|
|
5
6
|
// Its data refreshes every 30 seconds while it shows (hub-kit.js live).
|
|
6
7
|
(function () {
|
|
@@ -43,14 +44,27 @@
|
|
|
43
44
|
const mine = (() => { try { return localStorage.getItem('kickflip.push.id'); } catch { return null; } })();
|
|
44
45
|
const deviceLine = (d) => `<span class="rd-device">${esc([d.kind, d.browser, `seen ${BoardUI.ago(d.added)}`].filter(Boolean).join(' · '))}${d.id === mine ? ' · <b>this device</b>' : ''} <button type="button" class="link" data-forget-device="${esc(d.id)}">Remove</button></span>`;
|
|
45
46
|
const devices = (it) => it.devices?.length ? `<span class="links">${it.devices.map(deviceLine).join('')}</span>` : '';
|
|
46
|
-
|
|
47
|
+
// Linear and Slack carry a key button instead of a fix, in both states.
|
|
48
|
+
const keyButton = (it) => it.keyButton ? `<button type="button" class="btn quiet" data-settings-open="${esc(it.id)}" aria-haspopup="dialog">${esc(it.keyButton)}</button>` : '';
|
|
49
|
+
const missingLinks = (it) => it.fix || it.waits
|
|
50
|
+
? `<span class="links">${it.fix ? `<span class="rd-fix">${fixHtml(it.fix)}</span>` : ''}${it.waits ? `<span class="rd-waits">${esc(it.waits)}</span>` : ''}</span>` : '';
|
|
51
|
+
// GitHub's row while its rate limit holds is red, a fault, and says when it resets
|
|
52
|
+
// (docs/adr/2026-11-09-1626-gh-refuses-while-githubs-hourly-limit-holds.md).
|
|
53
|
+
const rows = integrations.map((it) => row({ icon: tile(it.id, it.ready ? 'var(--color-quiet)' : it.limited ? 'var(--color-danger)' : 'var(--color-notice)'), title: esc(it.name), wrap: true, trail: keyButton(it),
|
|
47
54
|
sub: it.ready ? `<span class="st ok">ready</span> · ${esc(it.detail)}${devices(it)}`
|
|
48
|
-
:
|
|
55
|
+
: `${it.limited ? '<span class="st down">rate limited</span>' : '<span class="st wait">not ready</span>'} · ${esc(it.missing)}${missingLinks(it)}` })).join('');
|
|
49
56
|
return `<h2 class="label" id="integrations">Integrations</h2><div class="group">${rows}</div>
|
|
50
57
|
<p class="note">A feature that needs an integration stays off until it’s ready, and says why.</p>
|
|
51
58
|
`;
|
|
52
59
|
}
|
|
53
60
|
|
|
61
|
+
// Keep awake (docs/adr/2026-11-09-1200-keep-awake-is-a-toggle-that-holds-the-mac-awake-only-on-ac-power.md): grey while off, orange while on as the machine working
|
|
62
|
+
// (docs/adr/2026-11-01-1817-colour-says-whos-waiting.md). Unchecked posts nothing, which the hub reads as off. Off macOS it says why instead of a toggle.
|
|
63
|
+
const AWAKE_NOTE = 'Runs <code>caffeinate -s</code> on the host so it doesn’t sleep while boards work, only while it is on AC power. On battery it holds nothing. The screen can still turn off.';
|
|
64
|
+
const awakeRow = (a) => a.unsupported
|
|
65
|
+
? HubUI.row({ icon: HubUI.tile('coffee', 'var(--color-quiet)'), title: 'Keep awake', sub: esc(a.unsupported), wrap: true, attrs: ' id="keep-awake"' })
|
|
66
|
+
: `<form class="row field" id="keep-awake" method="post" action="/_hub/settings/keep-awake">${HubUI.tile('coffee', a.on ? 'var(--color-notice)' : 'var(--color-quiet)')}<label class="body"><span class="text"><span class="title">Keep awake</span><span class="sub wrap">${AWAKE_NOTE}</span></span><input type="checkbox" id="awake" name="keep_awake" value="on" class="toggle" data-submit${a.on ? ' checked' : ''}></label></form>`;
|
|
67
|
+
|
|
54
68
|
// A pending prompt from a session that owns no board waits here (prompts.js).
|
|
55
69
|
function promptsHtml(list, prompts) {
|
|
56
70
|
const shown = list.filter(prompts.shows);
|
|
@@ -69,9 +83,10 @@
|
|
|
69
83
|
? `<button type="button" class="st ${esc(d.hub.tone)}" data-restart-hub${restartingHub ? BoardUI.BUSY : ''}>${esc(d.hub.word)}</button>`
|
|
70
84
|
: `<span class="st ${esc(d.hub.tone)}">${esc(d.hub.word)}</span>`;
|
|
71
85
|
return `${HubUI.flash(flash)}${HubUI.pauseBanner(d.pause)}${HubUI.holdBanner(d.hold)}${usage}${promptsHtml(d.prompts || [], prompts)}<h2 class="label"${usage ? '' : ' style="margin-top:4px"'}>Health</h2>
|
|
72
|
-
<div class="group">${row({ icon: tile('power', { ok: '#22c55e', wait: '#f59e0b' }[d.hub.tone] || '#ef4444'), title: 'Hub', sub: `${word} ·${esc(d.hub.detail)}
|
|
86
|
+
<div class="group">${row({ icon: tile('power', { ok: '#22c55e', wait: '#f59e0b' }[d.hub.tone] || '#ef4444'), title: 'Hub', sub: `${word} ·${esc(d.hub.detail)}`, wrap: true,
|
|
73
87
|
trail: d.hub.restart ? restartHubButton(restartingHub) : '' })}
|
|
74
88
|
${HubUI.machineRow(d.pause.machine)}
|
|
89
|
+
${awakeRow(d.awake)}
|
|
75
90
|
${d.empty ? row({ title: 'No sites yet', sub: 'In a Claude session, run <code>/kickflip <route></code>.' }) : ''}</div>
|
|
76
91
|
${integrationsHtml(d.integrations)}${sitesHtml(d.groups, restarting)}${d.remembered?.length ? `<h2 class="label">Sessions without a board</h2><div class="group">${rememberedHtml(d.remembered)}</div>` : ''}<p class="note">The hub runs every site and restarts one that stops. A board's links are what the hub owes it: GitHub polling, the fetch of its repo, events (its session and a watcher run, or the hub can wake it), daily wakes and reviewer links. One that just broke reads settling first.</p>`;
|
|
77
92
|
}
|
|
@@ -166,10 +181,13 @@ ${integrationsHtml(d.integrations)}${sitesHtml(d.groups, restarting)}${d.remembe
|
|
|
166
181
|
if (restarter) restart(restarter.dataset.restart, restarter.dataset.title);
|
|
167
182
|
});
|
|
168
183
|
const foot = B.$('.hub-foot');
|
|
184
|
+
// A #keep-awake link scrolls to Health's Keep awake row once it is drawn.
|
|
185
|
+
let scrollToAwake = ctx.view?.hash === '#keep-awake';
|
|
169
186
|
const paint = () => {
|
|
170
187
|
if (!last) return;
|
|
171
188
|
main.innerHTML = html(last, { flash, prompts, restarting, restartingHub });
|
|
172
189
|
foot.innerHTML = footer(last);
|
|
190
|
+
if (scrollToAwake) { scrollToAwake = false; B.$('#keep-awake')?.scrollIntoView(); }
|
|
173
191
|
};
|
|
174
192
|
const prompts = PromptAnswers.wire(B, {
|
|
175
193
|
list: () => last?.prompts || [],
|
|
@@ -178,7 +196,7 @@ ${integrationsHtml(d.integrations)}${sitesHtml(d.groups, restarting)}${d.remembe
|
|
|
178
196
|
after: () => load(),
|
|
179
197
|
});
|
|
180
198
|
const forms = HubKit.forms(B, {
|
|
181
|
-
accepts: (action, form) => ['/_hub/settings/codex', '/_hub/settings/claude-model', '/_hub/settings/pause-at', '/_hub/settings/keep-going'].includes(action) || Boolean(form?.closest?.('.hold-banner')),
|
|
199
|
+
accepts: (action, form) => ['/_hub/settings/codex', '/_hub/settings/claude-model', '/_hub/settings/pause-at', '/_hub/settings/keep-going', '/_hub/settings/keep-awake'].includes(action) || Boolean(form?.closest?.('.hold-banner')),
|
|
182
200
|
answer(out, action) {
|
|
183
201
|
if (['/_hub/settings/codex', '/_hub/settings/claude-model', '/_hub/settings/pause-at', '/_hub/settings/keep-going'].includes(action)) {
|
|
184
202
|
last = out; flash = null; paint();
|
|
@@ -153,7 +153,7 @@
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
// A toast names a topic by its card's ref, the title up to its colon; an ask by its time.
|
|
156
|
-
const toastName = (t) =>
|
|
156
|
+
const toastName = (t) => String(t.title).match(/^(\S+):/)?.[1] || t.title;
|
|
157
157
|
// The topic a tap on this page acts for, the pills before it and its name, which moveOn reads once it is recorded.
|
|
158
158
|
function leaving() {
|
|
159
159
|
const from = soloId ? topicOf(soloId)?.id : topicId;
|
|
@@ -290,7 +290,7 @@
|
|
|
290
290
|
const r = await fetch(site.url('api/settings'), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ tickets: value }) });
|
|
291
291
|
const out = await r.json();
|
|
292
292
|
if (!r.ok) throw new Error(out.error || 'Could not save tickets.');
|
|
293
|
-
savedTickets = out.settings.tickets;
|
|
293
|
+
savedTickets = out.settings.tickets || { source: 'none' };
|
|
294
294
|
return true;
|
|
295
295
|
} catch (e) {
|
|
296
296
|
if (B.alive()) ticketsStatus.textContent = e.message || 'Could not save. Is the host awake?';
|
|
@@ -486,8 +486,8 @@
|
|
|
486
486
|
pauseChoices = d.pause_choices || [];
|
|
487
487
|
paintPause(d.paused_until);
|
|
488
488
|
if (!B.alive()) return;
|
|
489
|
-
savedTickets = d.settings?.tickets ||
|
|
490
|
-
tickets = BoardDialogs.ticketsControl($('#boardTickets'), { value: savedTickets
|
|
489
|
+
savedTickets = d.settings?.tickets || { source: 'none' };
|
|
490
|
+
tickets = BoardDialogs.ticketsControl($('#boardTickets'), { value: savedTickets, repo: () => repo.github || '', title: () => ctx.siteTitle || '',
|
|
491
491
|
loadLinear: async () => (await get('/_hub/api/linear')).body,
|
|
492
492
|
keyState: async () => (await get('/_hub/api/settings')).body.linear,
|
|
493
493
|
change(_value, _valid, { loaded, typing } = {}) {
|
|
@@ -783,7 +783,7 @@
|
|
|
783
783
|
});
|
|
784
784
|
}).catch(() => { $('#rules').innerHTML = '<p class="muted">Offline. Is the host awake?</p>'; });
|
|
785
785
|
B.watch((events) => {
|
|
786
|
-
$('#rulesReplies').innerHTML = (events || []).slice(0, 3).map((e) => `<div class="reply">${e.reply ? (e.reply
|
|
786
|
+
$('#rulesReplies').innerHTML = (events || []).slice(0, 3).map((e) => `<div class="reply${e.reply?.md ? ' md' : ''}">${e.reply ? BoardUI.replyBody(e.reply) : '<span class="muted small">Waiting for Claude…</span>'}</div>`).join('');
|
|
787
787
|
}, { kind: 'rules' });
|
|
788
788
|
}),
|
|
789
789
|
};
|