@oar-sh/oar 0.9.19 → 0.9.21
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/.github/extensions/web-relay/polling/polling-loop.mjs +2 -26
- package/.github/extensions/web-relay/skills/question-routing-hooks.mjs +2 -26
- package/CHANGELOG.md +529 -0
- package/README.md +14 -14
- package/bin/oar.js +392 -98
- package/package.json +4 -2
- package/server/claude-cloud-worker/claude-cloud-event-normalizer.mjs +60 -0
- package/server/claude-cloud-worker/claude-cloud-session-process.mjs +258 -1
- package/server/claude-cloud-worker/claude-cloud-session-worker.mjs +22 -1
- package/server/claude-cloud-worker/claude-cloud-worker-steering.mjs +34 -0
- package/server/claude-worker/claude-remote-relay-tool.mjs +1 -0
- package/server/claude-worker/claude-session-process.mjs +10 -0
- package/server/claude-worker/claude-session-worker-main.mjs +8 -1
- package/server/claude-worker/claude-turn-publisher.mjs +163 -8
- package/server/claude-worker/claude-usage-totals.mjs +58 -0
- package/server/copilot-worker/copilot-plan-board.mjs +4 -7
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +86 -3
- package/server/copilot-worker/copilot-sdk-session-process.mjs +131 -5
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +8 -1
- package/server/cursor-worker/cursor-session-worker.mjs +12 -1
- package/server/cursor-worker/cursor-turn-runner.mjs +42 -6
- package/server/cursor-worker/cursor-worker-steering.mjs +20 -0
- package/server/db-schema.mjs +18 -0
- package/server/deepseek-worker/deepseek-claude-backend.mjs +14 -0
- package/server/grok-worker/grok-background-tasks.mjs +9 -0
- package/server/grok-worker/grok-session-worker.mjs +12 -1
- package/server/grok-worker/grok-turn-runner.mjs +47 -6
- package/server/grok-worker/grok-worker-steering.mjs +23 -0
- package/server/migrations/0001-add-structured-answer.mjs +5 -7
- package/server/migrations/0002-recent-workspace-roots-path-key.mjs +5 -7
- package/server/migrations/0003-push-subscriptions.mjs +5 -7
- package/server/migrations/0004-steer-settle-markers.mjs +5 -7
- package/server/migrations/0005-remote-relays.mjs +5 -7
- package/server/migrations/0006-workspace-root-settings.mjs +5 -7
- package/server/migrations/0007-claude-cloud.mjs +5 -7
- package/server/migrations/0008-message-pins.mjs +5 -7
- package/server/migrations/0009-subagent-timeline.mjs +5 -7
- package/server/migrations/0010-subagent-timeline-pruning.mjs +5 -7
- package/server/migrations/0011-relay-event-subscriptions.mjs +5 -7
- package/server/migrations/0012-cross-relay-event-subscriptions.mjs +5 -7
- package/server/migrations/0013-previews.mjs +5 -7
- package/server/migrations/0014-conversation-groups.mjs +5 -7
- package/server/migrations/0015-conversation-group-position.mjs +5 -7
- package/server/migrations/0016-share-access-log.mjs +5 -7
- package/server/migrations/0017-conversation-history-indexes.mjs +5 -7
- package/server/migrations/0018-background-task-rows.mjs +5 -7
- package/server/migrations/0019-relay-event-subscription-options.mjs +5 -7
- package/server/migrations/0020-conversation-may-create-sessions.mjs +5 -7
- package/server/migrations/0021-conversation-group-agent-marks.mjs +5 -7
- package/server/migrations/0022-relay-schedules.mjs +5 -7
- package/server/migrations/0023-relay-asks.mjs +5 -7
- package/server/migrations/0024-deepseek-usage-totals.mjs +5 -7
- package/server/migrations/0025-deepseek-usage-requests-daily.mjs +5 -7
- package/server/migrations/0026-deepseek-usage-served-models.mjs +5 -7
- package/server/migrations/0027-deepseek-usage-checkpoint-runs.mjs +5 -7
- package/server/migrations/0028-deepseek-usage-checkpoint-request-runs.mjs +5 -7
- package/server/migrations/0029-deepseek-balance-rises.mjs +5 -7
- package/server/migrations/0030-deepseek-usage-actuals.mjs +5 -7
- package/server/migrations/0031-queue-provider.mjs +5 -7
- package/server/migrations/0032-runtime-session-billing-switch.mjs +5 -7
- package/server/migrations/0033-claude-api-usage-checkpoints.mjs +5 -7
- package/server/migrations/0034-claude-api-usage-totals.mjs +5 -7
- package/server/migrations/0035-conversation-plan-only.mjs +87 -0
- package/server/migrations/0036-token-usage.mjs +267 -0
- package/server/public/app/about-ui.js +1 -1
- package/server/public/app/about-view.mjs +106 -3
- package/server/public/app/action-confirmations.js +1 -1
- package/server/public/app/api-client.js +6 -3
- package/server/public/app/ask-user-view.js +20 -6
- package/server/public/app/attachments-view.js +1 -1
- package/server/public/app/back-guard.mjs +31 -5
- package/server/public/app/bootstrap.js +611 -70
- package/server/public/app/chat-actions-menu-state.mjs +139 -4
- package/server/public/app/chat-header-meta.mjs +27 -0
- package/server/public/app/claude-billing-toggle-ui.js +2 -1
- package/server/public/app/claude-cloud-conversation-ui.js +2 -2
- package/server/public/app/composer-control-state.mjs +12 -2
- package/server/public/app/composer-settings-label.mjs +90 -0
- package/server/public/app/composer-settings-ui.js +442 -0
- package/server/public/app/context-menu.mjs +36 -5
- package/server/public/app/context-usage-view.mjs +16 -0
- package/server/public/app/conversation-deep-link.mjs +15 -11
- package/server/public/app/conversation-group-drag.mjs +12 -2
- package/server/public/app/conversation-group-order.mjs +9 -2
- package/server/public/app/conversation-group-state.mjs +47 -10
- package/server/public/app/conversation-origin-ui.js +17 -1
- package/server/public/app/conversation-view.js +20 -2
- package/server/public/app/cwd-menu-placement.mjs +18 -6
- package/server/public/app/cwd-picker.js +36 -6
- package/server/public/app/emoji-view.js +1 -1
- package/server/public/app/font-scaling.js +26 -0
- package/server/public/app/header-priority-actions.mjs +234 -0
- package/server/public/app/hidden-messages-view.mjs +4 -0
- package/server/public/app/inbox-ui.js +50 -2
- package/server/public/app/inbox-view.mjs +141 -5
- package/server/public/app/journal-view.js +311 -58
- package/server/public/app/phone-landscape-ui.js +225 -0
- package/server/public/app/phone-landscape.mjs +99 -0
- package/server/public/app/phone-typing-state.mjs +104 -0
- package/server/public/app/pinned-messages-view.mjs +5 -0
- package/server/public/app/plan-board-shared.mjs +49 -0
- package/server/public/app/plan-usage-view.mjs +1 -1
- package/server/public/app/push-settings.js +2 -2
- package/server/public/app/pwa-install.js +24 -3
- package/server/public/app/relay-board-inline.mjs +4 -1
- package/server/public/app/relay-board-view.js +38 -10
- package/server/public/app/relay-event-subscriptions-ui.js +71 -22
- package/server/public/app/relay-event-view.mjs +42 -0
- package/server/public/app/relay-restart-ui.js +2 -2
- package/server/public/app/relay-schedule-view.mjs +32 -6
- package/server/public/app/relay-schedules-ui.js +69 -20
- package/server/public/app/remote-relay-origin-view.mjs +41 -23
- package/server/public/app/session-history-ui.js +250 -0
- package/server/public/app/session-history.mjs +599 -0
- package/server/public/app/settings-modal.js +61 -6
- package/server/public/app/settings-tabs.js +12 -0
- package/server/public/app/share-state.js +1 -1
- package/server/public/app/shared-conversations-settings.js +2 -2
- package/server/public/app/socket-handlers.js +15 -4
- package/server/public/app/status-view.mjs +18 -2
- package/server/public/app/store.js +87 -16
- package/server/public/app/subagent-viewer.mjs +27 -2
- package/server/public/app/tab-strip-fade.mjs +101 -0
- package/server/public/app/tap-actions.js +66 -0
- package/server/public/app/usage-limit-ui.js +63 -2
- package/server/public/app/viewer-history.mjs +13 -2
- package/server/public/index.html +2258 -1585
- package/server/public/sw.js +8 -0
- package/server/relay-heap-guard.mjs +14 -1
- package/server/relay-self-restart.mjs +83 -2
- package/server/relay-tools.md +5 -3
- package/server/repositories/message-repository.mjs +54 -1
- package/server/repositories/remote-relay-repository.mjs +10 -0
- package/server/repositories/session-repository.mjs +25 -1
- package/server/routes/claude-cloud-routes.mjs +19 -0
- package/server/routes/messages-routes.mjs +144 -1
- package/server/routes/relay-ask-routes.mjs +138 -8
- package/server/routes/relay-board-routes.mjs +55 -40
- package/server/routes/sessions-routes.mjs +144 -14
- package/server/server-runtime.mjs +322 -51
- package/server/services/about-service.mjs +81 -53
- package/server/services/cache-rebuild-service.mjs +7 -0
- package/server/services/claude-billing-swap-service.mjs +7 -20
- package/server/services/claude-cloud-session-service.mjs +47 -5
- package/server/services/conversation-preferences-service.mjs +31 -0
- package/server/services/database-file.mjs +413 -0
- package/server/services/db-backup-service.mjs +809 -0
- package/server/services/deepseek-usage-import.mjs +6 -9
- package/server/services/delete-archive-service.mjs +3 -0
- package/server/services/earlier-database-service.mjs +69 -126
- package/server/services/oar-cli-helpers.mjs +214 -12
- package/server/services/oar-cli-menu.mjs +1 -0
- package/server/services/oar-state-migration-service.mjs +33 -13
- package/server/services/package-data-rescue.mjs +4 -2
- package/server/services/plan-usage-claude-api.mjs +14 -15
- package/server/services/plan-usage-contract.mjs +6 -2
- package/server/services/plan-usage-deepseek.mjs +1 -1
- package/server/services/plan-usage-tokens.mjs +248 -0
- package/server/services/push-dispatch-service.mjs +134 -9
- package/server/services/relay-cli-launcher-service.mjs +3 -0
- package/server/services/relay-event-service.mjs +30 -0
- package/server/services/relay-question-context.mjs +11 -0
- package/server/services/relay-restart-picture.mjs +67 -22
- package/server/services/remote-inbox-watch-service.mjs +204 -0
- package/server/services/remote-relay-caller-context.mjs +80 -7
- package/server/services/remote-relay-dispatcher.mjs +101 -19
- package/server/services/restart-resume-service.mjs +5 -2
- package/server/services/session-activity-tracker.mjs +1 -1
- package/server/services/session-launch-mutex-service.mjs +23 -0
- package/server/services/session-worker-launch-service.mjs +12 -3
- package/server/services/session-worker-registry-service.mjs +36 -0
- package/server/services/session-worker-stop-service.mjs +27 -6
- package/server/services/session-worker-websocket-service.mjs +40 -16
- package/server/services/token-usage-service.mjs +430 -0
- package/server/services/update-install-service.mjs +13 -0
- package/server/services/usage-limit-pause-service.mjs +8 -1
- package/server/services/worker-recycle-service.mjs +772 -0
- package/server/services/worker-replacement.mjs +197 -0
- package/server/services/worker-scope.mjs +77 -0
- package/shared/build-id.mjs +128 -0
- package/shared/plan-board-actions.mjs +130 -0
- package/shared/plan-only.mjs +43 -0
- package/shared/relay-asks.mjs +3 -0
- package/shared/relay-events.mjs +38 -4
- package/shared/remote-relay-contract.mjs +66 -11
- package/shared/turn-token-usage.mjs +144 -0
- package/shared/worker-runtime/heartbeat.mjs +5 -0
- package/shared/worker-runtime/simple-steering-state.mjs +38 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +23 -0
- package/shared/worker-runtime/working-directory.mjs +1 -1
|
@@ -13,12 +13,18 @@
|
|
|
13
13
|
import { apiFetch, answerRelayQuestion } from './api-client.js';
|
|
14
14
|
import { currentConvId, IS_SHARED_VIEW, setCurrentConv, showTransientRelayNotice } from './store.js';
|
|
15
15
|
import { isStatusViewActive, leaveStatusView } from './status-view.mjs';
|
|
16
|
+
import { actOnRelayBoard } from './relay-board-view.js';
|
|
16
17
|
import {
|
|
17
18
|
buildAskCard,
|
|
19
|
+
captureInboxFolds,
|
|
18
20
|
createCoalescedRefresh,
|
|
19
21
|
inboxPendingCount,
|
|
20
22
|
INBOX_FILTERS,
|
|
23
|
+
INBOX_FOLD_SELECTOR,
|
|
24
|
+
noteInboxFold,
|
|
25
|
+
pruneInboxFolds,
|
|
21
26
|
renderInbox,
|
|
27
|
+
restoreInboxFolds,
|
|
22
28
|
sessionAsksFromInbox,
|
|
23
29
|
} from './inbox-view.mjs';
|
|
24
30
|
|
|
@@ -37,6 +43,10 @@ let clickBound = false;
|
|
|
37
43
|
let started = false;
|
|
38
44
|
// Typed answers, kept across re-renders: `${relayId}:${itemId}` → text.
|
|
39
45
|
const drafts = new Map();
|
|
46
|
+
// The open folds (a card's Details, a plan card's plan), kept across
|
|
47
|
+
// re-renders the same way: every socket change and the minute's poll draw
|
|
48
|
+
// the cards anew, closed. Forgotten when the item leaves the list.
|
|
49
|
+
const openFolds = new Set();
|
|
40
50
|
// This relay's asks for the session cards: id → ask (each Inbox load
|
|
41
51
|
// replaces it; the socket updates single asks in between).
|
|
42
52
|
let sessionAsks = new Map();
|
|
@@ -104,9 +114,11 @@ function renderInboxNow({ opening = false } = {}) {
|
|
|
104
114
|
const host = document.getElementById('messages');
|
|
105
115
|
if (!host) return;
|
|
106
116
|
const focused = captureDrafts(host);
|
|
117
|
+
if (host.querySelector('.inbox-view')) captureInboxFolds(openFolds, host);
|
|
107
118
|
const top = opening ? inboxScrollTop : host.scrollTop;
|
|
108
119
|
host.replaceChildren(renderInbox(document, { relays, filter, now: Date.now(), loading: !loaded }));
|
|
109
120
|
restoreDrafts(host, focused);
|
|
121
|
+
restoreInboxFolds(openFolds, host);
|
|
110
122
|
host.scrollTop = top;
|
|
111
123
|
const title = document.getElementById('chat-title');
|
|
112
124
|
if (title) {
|
|
@@ -122,6 +134,7 @@ async function loadInbox() {
|
|
|
122
134
|
loaded = true;
|
|
123
135
|
// Built anew from each load: an ask withdrawn or cancelled meanwhile goes.
|
|
124
136
|
sessionAsks = sessionAsksFromInbox(relays);
|
|
137
|
+
pruneInboxFolds(openFolds, relays);
|
|
125
138
|
}
|
|
126
139
|
syncInboxBadge();
|
|
127
140
|
renderInboxNow();
|
|
@@ -173,12 +186,28 @@ export function leaveInboxView() {
|
|
|
173
186
|
return true;
|
|
174
187
|
}
|
|
175
188
|
|
|
176
|
-
|
|
189
|
+
/**
|
|
190
|
+
* Nothing to go back to (opened with nothing open, as after an unknown
|
|
191
|
+
* link): the header and the transcript give way to the empty screen.
|
|
192
|
+
* Loaded on demand: the transcript's module is already in the page.
|
|
193
|
+
*/
|
|
194
|
+
async function showNothingOpen() {
|
|
195
|
+
const { clearOpenConversation } = await import('./journal-view.js');
|
|
196
|
+
clearOpenConversation({ history: false });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Opens the Inbox, or leaves it for the conversation it was opened from.
|
|
201
|
+
* Opened from Status there is none stored: `returnTo` (the session the
|
|
202
|
+
* history stands on) is opened instead, so the transcript never keeps the list.
|
|
203
|
+
*/
|
|
204
|
+
export async function toggleInboxView({ returnTo = '' } = {}) {
|
|
177
205
|
if (inboxActive) {
|
|
178
206
|
leaveInboxView();
|
|
179
|
-
const restoreId = previousConversationId;
|
|
207
|
+
const restoreId = previousConversationId || String(returnTo || '').trim();
|
|
180
208
|
previousConversationId = '';
|
|
181
209
|
if (restoreId) await window.openConversation?.(restoreId, { restoreScroll: true });
|
|
210
|
+
else await showNothingOpen();
|
|
182
211
|
return;
|
|
183
212
|
}
|
|
184
213
|
if (isStatusViewActive()) leaveStatusView();
|
|
@@ -268,6 +297,15 @@ async function answerCard(card, data, { choice = '' } = {}) {
|
|
|
268
297
|
return true;
|
|
269
298
|
}
|
|
270
299
|
|
|
300
|
+
// A plan card's button in the Inbox: the card's own action, with
|
|
301
|
+
// the plan-only question first; a card settled meanwhile just shows so.
|
|
302
|
+
async function actOnPlanItem(data) {
|
|
303
|
+
const own = relays.find((relay) => relay?.self === true);
|
|
304
|
+
const item = (Array.isArray(own?.items) ? own.items : []).find((entry) => entry?.kind === 'plan' && entry.id === data.boardId) || null;
|
|
305
|
+
const board = item ? { ...item, conversationId: item.session?.id } : null;
|
|
306
|
+
return !!(await actOnRelayBoard(data.boardId, data.actionId, { board }));
|
|
307
|
+
}
|
|
308
|
+
|
|
271
309
|
async function handleAction(target) {
|
|
272
310
|
const data = { ...target.dataset };
|
|
273
311
|
const card = target.closest('.inbox-item');
|
|
@@ -294,6 +332,7 @@ async function handleAction(target) {
|
|
|
294
332
|
else if (data.inboxAction === 'dismiss') done = await answerAsk(card, data, { dismiss: true });
|
|
295
333
|
else if (data.inboxAction === 'card-choice') done = await answerCard(card, data, { choice: data.choice });
|
|
296
334
|
else if (data.inboxAction === 'card-answer') done = await answerCard(card, data);
|
|
335
|
+
else if (data.inboxAction === 'plan-action') done = await actOnPlanItem(data);
|
|
297
336
|
} finally {
|
|
298
337
|
if (!done) for (const node of buttons) node.disabled = false;
|
|
299
338
|
}
|
|
@@ -318,6 +357,13 @@ function bindClicks() {
|
|
|
318
357
|
const key = field ? draftKey(field) : '';
|
|
319
358
|
if (key) drafts.set(key, field.value);
|
|
320
359
|
});
|
|
360
|
+
// A fold opened or closed by hand. `toggle` does not bubble: caught on the
|
|
361
|
+
// way down. Also covers a session's card that a transcript render dropped
|
|
362
|
+
// before renderSessionAskCards drew it again.
|
|
363
|
+
host?.addEventListener('toggle', (event) => {
|
|
364
|
+
const details = event.target instanceof Element && event.target.matches(INBOX_FOLD_SELECTOR) ? event.target : null;
|
|
365
|
+
if (details && details.isConnected) noteInboxFold(openFolds, details);
|
|
366
|
+
}, true);
|
|
321
367
|
}
|
|
322
368
|
|
|
323
369
|
// ─── The card in the session ──────────────────────────────────────────────────
|
|
@@ -341,6 +387,7 @@ export function renderSessionAskCards() {
|
|
|
341
387
|
if (key === sessionCardKey && existing.length === shown.length && [...existing].every((node) => node.isConnected)) return;
|
|
342
388
|
sessionCardKey = key;
|
|
343
389
|
const focused = captureDrafts(host);
|
|
390
|
+
existing.forEach((node) => captureInboxFolds(openFolds, node));
|
|
344
391
|
existing.forEach((node) => node.remove());
|
|
345
392
|
const firstQuestion = host.querySelector('.relay-question-container');
|
|
346
393
|
for (const ask of shown) {
|
|
@@ -350,6 +397,7 @@ export function renderSessionAskCards() {
|
|
|
350
397
|
wrapper.append(buildAskCard(document, { ...ask, session: ask.session || { id } }, { relay: { id: 'self', self: true }, mode: 'session', now: Date.now() }));
|
|
351
398
|
if (firstQuestion) host.insertBefore(wrapper, firstQuestion);
|
|
352
399
|
else host.append(wrapper);
|
|
400
|
+
restoreInboxFolds(openFolds, wrapper);
|
|
353
401
|
}
|
|
354
402
|
restoreDrafts(host, focused);
|
|
355
403
|
}
|
|
@@ -11,11 +11,26 @@
|
|
|
11
11
|
// values of shared/relay-asks.mjs this needs are kept here, and
|
|
12
12
|
// inbox-view.test.mjs asserts they match.
|
|
13
13
|
|
|
14
|
+
import { boardOutcomeLine } from './relay-board-inline.mjs';
|
|
15
|
+
|
|
14
16
|
export const RELAY_ASK_SOCKET_EVENT = 'relay_ask_updated';
|
|
17
|
+
export const INBOX_REMOTE_CHANGED_EVENT = 'inbox_remote_changed';
|
|
15
18
|
export const INBOX_ANSWERED_KEEP_MS = 24 * 60 * 60_000;
|
|
16
19
|
export const INBOX_FILTERS = Object.freeze(['all', 'mine', 'agents']);
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
// The head label of a paired relay's approval card by its `variant`
|
|
22
|
+
// (REMOTE_RELAY_APPROVAL_VARIANTS of shared/remote-relay-contract.mjs).
|
|
23
|
+
export const INBOX_APPROVAL_VARIANT_LABELS = Object.freeze({
|
|
24
|
+
create_session: 'New session',
|
|
25
|
+
loop_guard: 'Loop guard',
|
|
26
|
+
write: 'Write',
|
|
27
|
+
session_settings: 'Session settings',
|
|
28
|
+
watch: 'Watch',
|
|
29
|
+
local_sessions: 'Start sessions',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// A plan card that was acted on is closed too.
|
|
33
|
+
const CLOSED_STATUSES = new Set(['answered', 'dismissed', 'acted']);
|
|
19
34
|
|
|
20
35
|
function toText(value) {
|
|
21
36
|
return String(value ?? '').trim();
|
|
@@ -71,6 +86,11 @@ function isClosed(item) {
|
|
|
71
86
|
return CLOSED_STATUSES.has(toText(item?.status));
|
|
72
87
|
}
|
|
73
88
|
|
|
89
|
+
// When an item was closed: answered, a plan card acted on, else made.
|
|
90
|
+
function closedAt(item) {
|
|
91
|
+
return item?.answeredAt || item?.actedAt || item?.createdAt;
|
|
92
|
+
}
|
|
93
|
+
|
|
74
94
|
function newestFirst(a, b) {
|
|
75
95
|
return toText(b?.createdAt).localeCompare(toText(a?.createdAt));
|
|
76
96
|
}
|
|
@@ -85,8 +105,8 @@ export function inboxSections(relays, { filter = 'all', now = Date.now() } = {})
|
|
|
85
105
|
const items = (Array.isArray(relay?.items) ? relay.items : []).filter((item) => item && toText(item.id) && inboxFilterMatches(item, filter));
|
|
86
106
|
const pending = items.filter((item) => toText(item.status) === 'pending').sort(newestFirst);
|
|
87
107
|
const closed = items
|
|
88
|
-
.filter((item) => isClosed(item) && now - (Date.parse(toText(item
|
|
89
|
-
.sort((a, b) => toText(b
|
|
108
|
+
.filter((item) => isClosed(item) && now - (Date.parse(toText(closedAt(item))) || 0) <= INBOX_ANSWERED_KEEP_MS)
|
|
109
|
+
.sort((a, b) => toText(closedAt(b)).localeCompare(toText(closedAt(a))));
|
|
90
110
|
return {
|
|
91
111
|
relay: {
|
|
92
112
|
id: toText(relay?.id),
|
|
@@ -147,6 +167,59 @@ export function sessionAsksFromInbox(relays) {
|
|
|
147
167
|
return asks;
|
|
148
168
|
}
|
|
149
169
|
|
|
170
|
+
// The folds a card has: an ask's `Details` and a plan card's `The plan`.
|
|
171
|
+
export const INBOX_FOLD_SELECTOR = 'details.inbox-item-detail, details.inbox-plan-body';
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* A fold's key: its card's relay and id, and which fold it is. Every render
|
|
175
|
+
* builds the cards anew, closed; the keys of the open ones carry them over.
|
|
176
|
+
*/
|
|
177
|
+
export function inboxFoldKey(details) {
|
|
178
|
+
const card = details?.closest?.('.inbox-item');
|
|
179
|
+
const itemId = toText(card?.dataset?.itemId);
|
|
180
|
+
if (!itemId) return '';
|
|
181
|
+
const fold = details.classList?.contains('inbox-plan-body') ? 'plan' : 'detail';
|
|
182
|
+
return `${toText(card.dataset.relayId) || 'self'}\n${itemId}\n${fold}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Notes whether `details` is open in `openFolds` (a Set of fold keys). */
|
|
186
|
+
export function noteInboxFold(openFolds, details) {
|
|
187
|
+
const key = inboxFoldKey(details);
|
|
188
|
+
if (!key) return;
|
|
189
|
+
if (details.open) openFolds.add(key);
|
|
190
|
+
else openFolds.delete(key);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Notes every fold under `root`, as it is now. */
|
|
194
|
+
export function captureInboxFolds(openFolds, root) {
|
|
195
|
+
for (const details of root?.querySelectorAll?.(INBOX_FOLD_SELECTOR) || []) noteInboxFold(openFolds, details);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Opens the folds under `root` that were open before its re-render. */
|
|
199
|
+
export function restoreInboxFolds(openFolds, root) {
|
|
200
|
+
for (const details of root?.querySelectorAll?.(INBOX_FOLD_SELECTOR) || []) {
|
|
201
|
+
if (openFolds.has(inboxFoldKey(details))) details.open = true;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Forgets the folds of items no longer listed (an Inbox load, GET
|
|
207
|
+
* /api/inbox). A relay that did not answer keeps its folds: its items did
|
|
208
|
+
* not leave, they are just not shown.
|
|
209
|
+
*/
|
|
210
|
+
export function pruneInboxFolds(openFolds, relays, { now = Date.now() } = {}) {
|
|
211
|
+
const listed = new Set();
|
|
212
|
+
const kept = new Set();
|
|
213
|
+
for (const section of inboxSections(relays, { now })) {
|
|
214
|
+
if (!section.relay.reachable) kept.add(section.relay.id || 'self');
|
|
215
|
+
for (const item of [...section.pending, ...section.closed]) listed.add(`${section.relay.id || 'self'}\n${toText(item.id)}`);
|
|
216
|
+
}
|
|
217
|
+
for (const key of [...openFolds]) {
|
|
218
|
+
const [relayId, itemId] = key.split('\n');
|
|
219
|
+
if (!kept.has(relayId) && !listed.has(`${relayId}\n${itemId}`)) openFolds.delete(key);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
150
223
|
/** Pending items across every relay (the sidebar's badge), whatever the filter. */
|
|
151
224
|
export function inboxPendingCount(relays) {
|
|
152
225
|
return (Array.isArray(relays) ? relays : [])
|
|
@@ -269,10 +342,17 @@ export function buildCardItem(doc, item, { relay = null, now = Date.now() } = {}
|
|
|
269
342
|
card.dataset.kind = toText(item.kind) || 'ask_user';
|
|
270
343
|
if (relay?.id) card.dataset.relayId = relay.id;
|
|
271
344
|
card.dataset.status = toText(item.status) || 'pending';
|
|
272
|
-
|
|
345
|
+
// A paired relay's approval says what it is about; one from a
|
|
346
|
+
// relay without summaries (0.9.19) keeps the plain text.
|
|
347
|
+
const summary = approval && !relay?.self ? toText(item.summary) : '';
|
|
348
|
+
const variant = toText(item.variant);
|
|
349
|
+
const variantLabel = Object.hasOwn(INBOX_APPROVAL_VARIANT_LABELS, variant) ? INBOX_APPROVAL_VARIANT_LABELS[variant] : '';
|
|
350
|
+
const kindLabel = summary ? (variantLabel || 'Approval') : (approval ? 'Approval' : 'Question');
|
|
351
|
+
card.append(headLine(doc, item, { relay, now, label: `${kindLabel} · the session waits` }));
|
|
273
352
|
card.append(sessionLine(doc, item, relay));
|
|
274
353
|
if (toText(item.header)) card.append(el(doc, 'div', 'inbox-item-header', item.header));
|
|
275
|
-
if (
|
|
354
|
+
if (summary) card.append(el(doc, 'p', 'inbox-item-question inbox-item-summary', `${toText(relay?.name) || 'that relay'} · ${summary}`));
|
|
355
|
+
else if (toText(item.question)) card.append(el(doc, 'p', 'inbox-item-question', item.question));
|
|
276
356
|
else if (approval) card.append(el(doc, 'p', 'inbox-item-question', 'An Allow / Deny card on that relay.'));
|
|
277
357
|
const outcome = outcomeLine(doc, item);
|
|
278
358
|
if (outcome) card.append(outcome);
|
|
@@ -321,8 +401,64 @@ export function buildCardItem(doc, item, { relay = null, now = Date.now() } = {}
|
|
|
321
401
|
return card;
|
|
322
402
|
}
|
|
323
403
|
|
|
404
|
+
/**
|
|
405
|
+
* A plan card in the Inbox: the plan folded, and the card's own
|
|
406
|
+
* buttons, which act as the card in its session does (a plan-only session
|
|
407
|
+
* asks first). Settled, it says what was chosen. A paired relay's shows its
|
|
408
|
+
* title alone: "Open there".
|
|
409
|
+
*/
|
|
410
|
+
export function buildPlanItem(doc, item, { relay = null, now = Date.now() } = {}) {
|
|
411
|
+
const card = el(doc, 'article', 'inbox-item inbox-card inbox-plan');
|
|
412
|
+
card.dataset.itemId = toText(item.id);
|
|
413
|
+
card.dataset.kind = 'plan';
|
|
414
|
+
if (relay?.id) card.dataset.relayId = relay.id;
|
|
415
|
+
card.dataset.status = toText(item.status) || 'pending';
|
|
416
|
+
card.append(headLine(doc, item, { relay, now, label: 'Plan · ready for review' }));
|
|
417
|
+
card.append(sessionLine(doc, item, relay));
|
|
418
|
+
card.append(el(doc, 'div', 'inbox-item-header', toText(item.title) || 'Plan ready for review'));
|
|
419
|
+
if (relay?.self && toText(item.body)) {
|
|
420
|
+
const details = el(doc, 'details', 'inbox-plan-body');
|
|
421
|
+
details.append(el(doc, 'summary', '', 'The plan'));
|
|
422
|
+
details.append(el(doc, 'div', 'inbox-plan-text', item.body));
|
|
423
|
+
card.append(details);
|
|
424
|
+
}
|
|
425
|
+
if (item.status !== 'pending') {
|
|
426
|
+
const outcome = boardOutcomeLine(item);
|
|
427
|
+
if (outcome) card.append(el(doc, 'div', 'inbox-item-outcome', outcome));
|
|
428
|
+
return card;
|
|
429
|
+
}
|
|
430
|
+
if (!relay?.self) {
|
|
431
|
+
const url = inboxSessionUrl(relay?.url, item?.session?.id);
|
|
432
|
+
const actions = el(doc, 'div', 'inbox-item-actions');
|
|
433
|
+
if (url) {
|
|
434
|
+
const link = el(doc, 'a', 'inbox-open-there inbox-open-there-button', 'Open there');
|
|
435
|
+
link.href = url;
|
|
436
|
+
link.target = '_blank';
|
|
437
|
+
link.rel = 'noopener noreferrer';
|
|
438
|
+
actions.append(link);
|
|
439
|
+
} else {
|
|
440
|
+
actions.append(el(doc, 'span', 'inbox-item-note', 'Answer it on that relay.'));
|
|
441
|
+
}
|
|
442
|
+
card.append(actions);
|
|
443
|
+
return card;
|
|
444
|
+
}
|
|
445
|
+
const recommended = toText(item.recommendedAction).toLowerCase();
|
|
446
|
+
const row = el(doc, 'div', 'inbox-item-options inbox-plan-actions');
|
|
447
|
+
for (const action of Array.isArray(item.actions) ? item.actions : []) {
|
|
448
|
+
const actionId = toText(action?.id).toLowerCase();
|
|
449
|
+
if (!actionId) continue;
|
|
450
|
+
const node = button(doc, `inbox-choice inbox-plan-action${actionId === recommended ? ' inbox-plan-action-recommended' : ''}`, toText(action?.label) || actionId, {
|
|
451
|
+
relayId: 'self', inboxAction: 'plan-action', boardId: item.id, actionId, sessionId: item?.session?.id,
|
|
452
|
+
});
|
|
453
|
+
row.append(node);
|
|
454
|
+
}
|
|
455
|
+
card.append(row);
|
|
456
|
+
return card;
|
|
457
|
+
}
|
|
458
|
+
|
|
324
459
|
function buildItem(doc, item, relay, now) {
|
|
325
460
|
if (item.kind === 'ask') return buildAskCard(doc, item, { relay, now, mode: 'inbox', answerable: true });
|
|
461
|
+
if (item.kind === 'plan') return buildPlanItem(doc, item, { relay, now });
|
|
326
462
|
return buildCardItem(doc, item, { relay, now });
|
|
327
463
|
}
|
|
328
464
|
|