@oar-sh/oar 0.9.20 โ 0.9.22
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/CHANGELOG.md +325 -0
- package/README.md +13 -13
- package/bin/oar.js +2 -2
- package/package.json +1 -1
- package/server/claude-cloud-worker/claude-cloud-session-process.mjs +231 -1
- package/server/claude-cloud-worker/claude-cloud-session-worker.mjs +31 -2
- package/server/claude-cloud-worker/claude-cloud-worker-steering.mjs +34 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +149 -4
- package/server/claude-worker/claude-session-process.mjs +16 -4
- package/server/claude-worker/claude-session-worker-main.mjs +18 -2
- package/server/copilot-worker/copilot-sdk-session-process.mjs +75 -1
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +18 -2
- package/server/cursor-worker/cursor-session-worker.mjs +21 -2
- package/server/cursor-worker/cursor-worker-steering.mjs +20 -0
- package/server/db-schema.mjs +19 -0
- package/server/grok-worker/grok-background-tasks.mjs +10 -0
- package/server/grok-worker/grok-session-worker.mjs +22 -2
- package/server/grok-worker/grok-turn-runner.mjs +4 -0
- package/server/grok-worker/grok-worker-steering.mjs +23 -0
- package/server/migrations/0037-relay-asks-queue-id.mjs +66 -0
- package/server/migrations/0038-turn-seq.mjs +92 -0
- package/server/public/app/about-ui.js +1 -1
- package/server/public/app/about-view.mjs +19 -0
- package/server/public/app/action-confirmations.js +1 -1
- package/server/public/app/activity-replay-state.mjs +160 -42
- package/server/public/app/api-client.js +11 -2
- package/server/public/app/ask-user-view.js +24 -9
- package/server/public/app/attachments-view.js +1 -1
- package/server/public/app/back-guard.mjs +31 -5
- package/server/public/app/bootstrap.js +618 -72
- package/server/public/app/chat-actions-menu-state.mjs +141 -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-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 +68 -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 +309 -49
- 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 +238 -0
- package/server/public/app/held-details.mjs +202 -0
- package/server/public/app/hidden-messages-view.mjs +4 -0
- package/server/public/app/inbox-ui.js +88 -81
- package/server/public/app/inbox-view.mjs +35 -19
- package/server/public/app/journal-view.js +328 -58
- package/server/public/app/live-activity-cap.mjs +103 -0
- 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/push-settings.js +2 -2
- package/server/public/app/pwa-install.js +24 -3
- package/server/public/app/questions-panel-ui.js +634 -0
- package/server/public/app/questions-stub-view.mjs +40 -0
- package/server/public/app/questions-view.mjs +89 -0
- package/server/public/app/relay-board-view.js +12 -2
- package/server/public/app/relay-event-subscriptions-ui.js +71 -22
- package/server/public/app/relay-event-view.mjs +38 -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/relay-thoughts.mjs +15 -1
- package/server/public/app/remote-relay-origin-view.mjs +41 -23
- package/server/public/app/session-history-ui.js +251 -0
- package/server/public/app/session-history.mjs +599 -0
- package/server/public/app/settings-modal.js +67 -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 +47 -13
- package/server/public/app/status-view.mjs +18 -2
- package/server/public/app/store.js +87 -16
- package/server/public/app/stream-view-preference.mjs +60 -0
- package/server/public/app/subagent-fold.mjs +6 -3
- package/server/public/app/subagent-timeline-view.mjs +12 -5
- package/server/public/app/subagent-viewer.mjs +35 -8
- package/server/public/app/tab-strip-fade.mjs +101 -0
- package/server/public/app/tap-actions.js +66 -0
- package/server/public/app/thoughts-preference.mjs +79 -0
- package/server/public/app/turn-stream-view.mjs +124 -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 +2603 -1767
- package/server/relay-tools.md +1 -0
- package/server/repositories/message-repository.mjs +33 -0
- package/server/repositories/question-repository.mjs +51 -7
- package/server/repositories/relay-turn-rows.mjs +56 -0
- package/server/repositories/session-repository.mjs +19 -0
- package/server/routes/ask-user-routes.mjs +34 -5
- package/server/routes/claude-cloud-routes.mjs +19 -0
- package/server/routes/messages-routes.mjs +92 -15
- package/server/routes/relay-ask-routes.mjs +15 -5
- package/server/routes/sessions-routes.mjs +117 -12
- package/server/server-runtime.mjs +367 -170
- package/server/services/about-service.mjs +22 -53
- package/server/services/claude-billing-swap-service.mjs +7 -20
- package/server/services/claude-cloud-session-service.mjs +47 -5
- package/server/services/cloudflared-tunnel-service.mjs +7 -0
- package/server/services/idle-timers.mjs +184 -0
- package/server/services/queue-wake-hook.mjs +145 -0
- package/server/services/queue-waker.mjs +127 -0
- package/server/services/relay-ask-service.mjs +64 -5
- package/server/services/relay-event-service.mjs +4 -1
- package/server/services/remote-relay-dispatcher.mjs +84 -18
- package/server/services/session-activity-tracker.mjs +1 -1
- package/server/services/session-history-refresh-service.mjs +4 -0
- package/server/services/session-launch-mutex-service.mjs +23 -0
- package/server/services/session-worker-registry-service.mjs +36 -0
- package/server/services/session-worker-supervisor-service.mjs +36 -0
- package/server/services/session-worker-websocket-service.mjs +183 -31
- package/server/services/ssh-tunnel-manager-service.mjs +7 -0
- package/server/services/worker-recycle-service.mjs +772 -0
- package/server/services/worker-replacement.mjs +197 -0
- package/shared/ask-user-bridge.mjs +9 -6
- package/shared/build-id.mjs +128 -0
- package/shared/relay-asks.mjs +10 -0
- package/shared/relay-events.mjs +1 -0
- package/shared/worker-runtime/heartbeat.mjs +31 -0
- package/shared/worker-runtime/simple-steering-state.mjs +38 -0
- package/shared/worker-runtime/worker-keepalive.mjs +108 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +44 -10
- package/shared/worker-runtime/working-directory.mjs +1 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
// The Inbox in the page: the sidebar's "Inbox" button with its count
|
|
2
|
-
// view it opens in place of a transcript (like Status)
|
|
3
|
-
//
|
|
4
|
-
// builds the DOM; this module loads,
|
|
1
|
+
// The Inbox in the page: the sidebar's "Inbox" button with its count and
|
|
2
|
+
// the view it opens in place of a transcript (like Status). A session's own
|
|
3
|
+
// questions that don't block are in its ๐ (questions-panel-ui.js), which
|
|
4
|
+
// answers through here. inbox-view.mjs builds the DOM; this module loads,
|
|
5
|
+
// keeps and answers.
|
|
5
6
|
//
|
|
6
7
|
// The list comes from GET /api/inbox (this relay and every paired relay that
|
|
7
8
|
// answers), again on every change of a question here (socket) and every
|
|
@@ -15,17 +16,15 @@ import { currentConvId, IS_SHARED_VIEW, setCurrentConv, showTransientRelayNotice
|
|
|
15
16
|
import { isStatusViewActive, leaveStatusView } from './status-view.mjs';
|
|
16
17
|
import { actOnRelayBoard } from './relay-board-view.js';
|
|
17
18
|
import {
|
|
18
|
-
buildAskCard,
|
|
19
19
|
captureInboxFolds,
|
|
20
20
|
createCoalescedRefresh,
|
|
21
|
-
|
|
21
|
+
inboxBadge,
|
|
22
22
|
INBOX_FILTERS,
|
|
23
23
|
INBOX_FOLD_SELECTOR,
|
|
24
24
|
noteInboxFold,
|
|
25
25
|
pruneInboxFolds,
|
|
26
26
|
renderInbox,
|
|
27
27
|
restoreInboxFolds,
|
|
28
|
-
sessionAsksFromInbox,
|
|
29
28
|
} from './inbox-view.mjs';
|
|
30
29
|
|
|
31
30
|
const FILTER_STORAGE_KEY = 'oar_inbox_filter';
|
|
@@ -47,14 +46,18 @@ const drafts = new Map();
|
|
|
47
46
|
// re-renders the same way: every socket change and the minute's poll draw
|
|
48
47
|
// the cards anew, closed. Forgotten when the item leaves the list.
|
|
49
48
|
const openFolds = new Set();
|
|
50
|
-
// This relay's asks for the session cards: id โ ask (each Inbox load
|
|
51
|
-
// replaces it; the socket updates single asks in between).
|
|
52
|
-
let sessionAsks = new Map();
|
|
53
|
-
let sessionCardKey = '';
|
|
54
49
|
// Where the Inbox was scrolled when it was last left; the next opening
|
|
55
50
|
// starts there (the first one at the top). The transcript's own scroll
|
|
56
51
|
// height is the transcript's, never the Inbox's.
|
|
57
52
|
let inboxScrollTop = 0;
|
|
53
|
+
// The ๐ panel's side (questions-panel-ui.js), handed in so the two modules
|
|
54
|
+
// do not import each other: an ask the answer routes returned, "open" on an
|
|
55
|
+
// ask's session, and that open having run (or failed).
|
|
56
|
+
let askHooks = { onAskUpdated: null, onOpenAsk: null, onOpenSettled: null };
|
|
57
|
+
|
|
58
|
+
export function setInboxAskHooks(hooks = {}) {
|
|
59
|
+
askHooks = { ...askHooks, ...hooks };
|
|
60
|
+
}
|
|
58
61
|
|
|
59
62
|
function readFilter() {
|
|
60
63
|
try {
|
|
@@ -69,15 +72,24 @@ export function isInboxViewActive() {
|
|
|
69
72
|
return inboxActive;
|
|
70
73
|
}
|
|
71
74
|
|
|
72
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* The sidebar button's count: everything pending, here and on paired relays;
|
|
77
|
+
* while an urgent ask waits, the urgent count in red.
|
|
78
|
+
*/
|
|
73
79
|
function syncInboxBadge() {
|
|
74
|
-
const count =
|
|
80
|
+
const { text, urgent, pending: count, urgentCount } = inboxBadge(relays);
|
|
75
81
|
const badge = document.getElementById('inbox-count');
|
|
76
82
|
const buttonEl = document.getElementById('inbox-btn');
|
|
77
|
-
if (badge)
|
|
83
|
+
if (badge) {
|
|
84
|
+
badge.textContent = text;
|
|
85
|
+
badge.classList.toggle('is-urgent', urgent);
|
|
86
|
+
}
|
|
78
87
|
if (buttonEl) {
|
|
79
88
|
buttonEl.classList.toggle('has-pending', count > 0);
|
|
80
|
-
|
|
89
|
+
const label = urgent
|
|
90
|
+
? `Inbox, ${urgentCount} urgent of ${count} waiting`
|
|
91
|
+
: (count ? `Inbox, ${count} question${count === 1 ? '' : 's'} waiting` : 'Inbox');
|
|
92
|
+
buttonEl.setAttribute('aria-label', label);
|
|
81
93
|
buttonEl.setAttribute('aria-pressed', inboxActive ? 'true' : 'false');
|
|
82
94
|
}
|
|
83
95
|
}
|
|
@@ -87,16 +99,18 @@ function draftKey(node) {
|
|
|
87
99
|
return card ? `${card.dataset.relayId || 'self'}:${card.dataset.itemId || ''}` : '';
|
|
88
100
|
}
|
|
89
101
|
|
|
90
|
-
function captureDrafts(host) {
|
|
102
|
+
export function captureDrafts(host) {
|
|
103
|
+
// Only fields typed into: the same ask can be drawn twice (its session's
|
|
104
|
+
// card and the ๐ panel), and the untouched copy must not wipe the draft.
|
|
91
105
|
for (const field of host.querySelectorAll('.inbox-answer-text')) {
|
|
92
106
|
const key = draftKey(field);
|
|
93
|
-
if (key) drafts.set(key, field.value);
|
|
107
|
+
if (key && field.dataset.draftTouched === '1') drafts.set(key, field.value);
|
|
94
108
|
}
|
|
95
109
|
const active = document.activeElement;
|
|
96
110
|
return active?.classList?.contains('inbox-answer-text') && host.contains(active) ? draftKey(active) : '';
|
|
97
111
|
}
|
|
98
112
|
|
|
99
|
-
function restoreDrafts(host, focusedKey) {
|
|
113
|
+
export function restoreDrafts(host, focusedKey) {
|
|
100
114
|
for (const field of host.querySelectorAll('.inbox-answer-text')) {
|
|
101
115
|
const key = draftKey(field);
|
|
102
116
|
if (drafts.has(key)) field.value = drafts.get(key);
|
|
@@ -122,8 +136,8 @@ function renderInboxNow({ opening = false } = {}) {
|
|
|
122
136
|
host.scrollTop = top;
|
|
123
137
|
const title = document.getElementById('chat-title');
|
|
124
138
|
if (title) {
|
|
125
|
-
const count =
|
|
126
|
-
title.textContent = count ? `Inbox ยท ${count} waiting` : 'Inbox';
|
|
139
|
+
const { pending: count, urgentCount } = inboxBadge(relays);
|
|
140
|
+
title.textContent = count ? `Inbox ยท ${urgentCount ? `${urgentCount} urgent ยท ` : ''}${count} waiting` : 'Inbox';
|
|
127
141
|
}
|
|
128
142
|
}
|
|
129
143
|
|
|
@@ -132,13 +146,10 @@ async function loadInbox() {
|
|
|
132
146
|
if (response && Array.isArray(response.relays)) {
|
|
133
147
|
relays = response.relays;
|
|
134
148
|
loaded = true;
|
|
135
|
-
// Built anew from each load: an ask withdrawn or cancelled meanwhile goes.
|
|
136
|
-
sessionAsks = sessionAsksFromInbox(relays);
|
|
137
149
|
pruneInboxFolds(openFolds, relays);
|
|
138
150
|
}
|
|
139
151
|
syncInboxBadge();
|
|
140
152
|
renderInboxNow();
|
|
141
|
-
renderSessionAskCards();
|
|
142
153
|
return response;
|
|
143
154
|
}
|
|
144
155
|
|
|
@@ -160,10 +171,7 @@ function scheduleRefresh() {
|
|
|
160
171
|
}
|
|
161
172
|
|
|
162
173
|
/** Socket: an ask of this relay changed (created, answered, dismissed, withdrawn). */
|
|
163
|
-
export function noteRelayAskUpdated(
|
|
164
|
-
const ask = payload?.ask;
|
|
165
|
-
if (ask?.id) sessionAsks.set(ask.id, ask);
|
|
166
|
-
renderSessionAskCards();
|
|
174
|
+
export function noteRelayAskUpdated() {
|
|
167
175
|
scheduleRefresh();
|
|
168
176
|
}
|
|
169
177
|
|
|
@@ -186,12 +194,28 @@ export function leaveInboxView() {
|
|
|
186
194
|
return true;
|
|
187
195
|
}
|
|
188
196
|
|
|
189
|
-
|
|
197
|
+
/**
|
|
198
|
+
* Nothing to go back to (opened with nothing open, as after an unknown
|
|
199
|
+
* link): the header and the transcript give way to the empty screen.
|
|
200
|
+
* Loaded on demand: the transcript's module is already in the page.
|
|
201
|
+
*/
|
|
202
|
+
async function showNothingOpen() {
|
|
203
|
+
const { clearOpenConversation } = await import('./journal-view.js');
|
|
204
|
+
clearOpenConversation({ history: false });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Opens the Inbox, or leaves it for the conversation it was opened from.
|
|
209
|
+
* Opened from Status there is none stored: `returnTo` (the session the
|
|
210
|
+
* history stands on) is opened instead, so the transcript never keeps the list.
|
|
211
|
+
*/
|
|
212
|
+
export async function toggleInboxView({ returnTo = '' } = {}) {
|
|
190
213
|
if (inboxActive) {
|
|
191
214
|
leaveInboxView();
|
|
192
|
-
const restoreId = previousConversationId;
|
|
215
|
+
const restoreId = previousConversationId || String(returnTo || '').trim();
|
|
193
216
|
previousConversationId = '';
|
|
194
217
|
if (restoreId) await window.openConversation?.(restoreId, { restoreScroll: true });
|
|
218
|
+
else await showNothingOpen();
|
|
195
219
|
return;
|
|
196
220
|
}
|
|
197
221
|
if (isStatusViewActive()) leaveStatusView();
|
|
@@ -255,7 +279,9 @@ async function answerAsk(card, data, { choice = '', dismiss = false } = {}) {
|
|
|
255
279
|
return false;
|
|
256
280
|
}
|
|
257
281
|
drafts.delete(`${relayId}:${id}`);
|
|
258
|
-
if (relayId === 'self' && result.response?.question?.id)
|
|
282
|
+
if (relayId === 'self' && result.response?.question?.id) {
|
|
283
|
+
try { askHooks.onAskUpdated?.(result.response.question); } catch {}
|
|
284
|
+
}
|
|
259
285
|
return true;
|
|
260
286
|
}
|
|
261
287
|
|
|
@@ -302,9 +328,17 @@ async function handleAction(target) {
|
|
|
302
328
|
if (data.inboxAction === 'open') {
|
|
303
329
|
const id = String(data.sessionId || '').trim();
|
|
304
330
|
if (!id) return;
|
|
331
|
+
// An ask's session opens with ๐ open at that ask.
|
|
332
|
+
const askId = card?.dataset.kind === 'ask' && (card.dataset.relayId || 'self') === 'self' ? String(card.dataset.itemId || '') : '';
|
|
333
|
+
if (askId) askHooks.onOpenAsk?.(id, askId);
|
|
305
334
|
leaveInboxView();
|
|
306
335
|
previousConversationId = '';
|
|
307
|
-
|
|
336
|
+
try {
|
|
337
|
+
await window.openConversation?.(id);
|
|
338
|
+
} finally {
|
|
339
|
+
// Failed, or landed elsewhere: the request to open ๐ there is dropped.
|
|
340
|
+
if (askId) askHooks.onOpenSettled?.(id);
|
|
341
|
+
}
|
|
308
342
|
return;
|
|
309
343
|
}
|
|
310
344
|
const buttons = card ? [...card.querySelectorAll('button')] : [target];
|
|
@@ -320,73 +354,46 @@ async function handleAction(target) {
|
|
|
320
354
|
} finally {
|
|
321
355
|
if (!done) for (const node of buttons) node.disabled = false;
|
|
322
356
|
}
|
|
323
|
-
if (done)
|
|
324
|
-
renderSessionAskCards();
|
|
325
|
-
await refreshInbox();
|
|
326
|
-
}
|
|
357
|
+
if (done) await refreshInbox();
|
|
327
358
|
}
|
|
328
359
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
360
|
+
/**
|
|
361
|
+
* The Inbox's buttons, typed answers and folds inside `host`: the transcript
|
|
362
|
+
* (the Inbox view) or another host that draws the
|
|
363
|
+
* same cards (the ๐ panel). `scope`: the selector a click must be inside.
|
|
364
|
+
*/
|
|
365
|
+
export function bindInboxActionHost(host, { scope = '.inbox-view' } = {}) {
|
|
366
|
+
if (!host || host.dataset.inboxActionsBound === '1') return;
|
|
367
|
+
host.dataset.inboxActionsBound = '1';
|
|
368
|
+
host.addEventListener('click', (event) => {
|
|
334
369
|
const target = event.target instanceof Element ? event.target.closest('[data-inbox-action], [data-inbox-filter]') : null;
|
|
335
|
-
if (!target || !target.closest(
|
|
370
|
+
if (!target || !target.closest(scope)) return;
|
|
336
371
|
event.preventDefault();
|
|
337
372
|
void handleAction(target);
|
|
338
373
|
});
|
|
339
|
-
host
|
|
374
|
+
host.addEventListener('input', (event) => {
|
|
340
375
|
const field = event.target instanceof Element && event.target.classList.contains('inbox-answer-text') ? event.target : null;
|
|
341
376
|
const key = field ? draftKey(field) : '';
|
|
342
|
-
if (key)
|
|
377
|
+
if (key) {
|
|
378
|
+
field.dataset.draftTouched = '1';
|
|
379
|
+
drafts.set(key, field.value);
|
|
380
|
+
}
|
|
343
381
|
});
|
|
344
382
|
// A fold opened or closed by hand. `toggle` does not bubble: caught on the
|
|
345
|
-
// way down.
|
|
346
|
-
|
|
347
|
-
host?.addEventListener('toggle', (event) => {
|
|
383
|
+
// way down.
|
|
384
|
+
host.addEventListener('toggle', (event) => {
|
|
348
385
|
const details = event.target instanceof Element && event.target.matches(INBOX_FOLD_SELECTOR) ? event.target : null;
|
|
349
386
|
if (details && details.isConnected) noteInboxFold(openFolds, details);
|
|
350
387
|
}, true);
|
|
351
388
|
}
|
|
352
389
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
* transcript (before the cards it waits on). Called with every render of the
|
|
358
|
-
* question cards; a no-op while nothing changed.
|
|
359
|
-
*/
|
|
360
|
-
export function renderSessionAskCards() {
|
|
361
|
-
const host = document.getElementById('messages');
|
|
362
|
-
if (!host || IS_SHARED_VIEW) return;
|
|
363
|
-
const id = String(currentConvId || '').trim();
|
|
364
|
-
const shown = inboxActive || isStatusViewActive() || !id
|
|
365
|
-
? []
|
|
366
|
-
: [...sessionAsks.values()]
|
|
367
|
-
.filter((ask) => ask && (ask.conversationId || ask.session?.id) === id && ask.status === 'pending')
|
|
368
|
-
.sort((a, b) => String(a.createdAt || '').localeCompare(String(b.createdAt || '')));
|
|
369
|
-
const key = JSON.stringify([id, shown.map((ask) => [ask.id, ask.status])]);
|
|
370
|
-
const existing = host.querySelectorAll('.relay-ask-container');
|
|
371
|
-
if (key === sessionCardKey && existing.length === shown.length && [...existing].every((node) => node.isConnected)) return;
|
|
372
|
-
sessionCardKey = key;
|
|
373
|
-
const focused = captureDrafts(host);
|
|
374
|
-
existing.forEach((node) => captureInboxFolds(openFolds, node));
|
|
375
|
-
existing.forEach((node) => node.remove());
|
|
376
|
-
const firstQuestion = host.querySelector('.relay-question-container');
|
|
377
|
-
for (const ask of shown) {
|
|
378
|
-
const wrapper = document.createElement('div');
|
|
379
|
-
wrapper.className = 'msg relay-ask-container';
|
|
380
|
-
wrapper.dataset.askId = ask.id;
|
|
381
|
-
wrapper.append(buildAskCard(document, { ...ask, session: ask.session || { id } }, { relay: { id: 'self', self: true }, mode: 'session', now: Date.now() }));
|
|
382
|
-
if (firstQuestion) host.insertBefore(wrapper, firstQuestion);
|
|
383
|
-
else host.append(wrapper);
|
|
384
|
-
restoreInboxFolds(openFolds, wrapper);
|
|
385
|
-
}
|
|
386
|
-
restoreDrafts(host, focused);
|
|
390
|
+
function bindClicks() {
|
|
391
|
+
if (clickBound) return;
|
|
392
|
+
clickBound = true;
|
|
393
|
+
bindInboxActionHost(document.getElementById('messages'));
|
|
387
394
|
}
|
|
388
395
|
|
|
389
|
-
/** Boot: the count
|
|
396
|
+
/** Boot: the count and a refresh every minute (paired relays). */
|
|
390
397
|
export function startInbox() {
|
|
391
398
|
if (started || IS_SHARED_VIEW) return;
|
|
392
399
|
started = true;
|
|
@@ -153,20 +153,6 @@ export function createCoalescedRefresh(load) {
|
|
|
153
153
|
return refresh;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
/**
|
|
157
|
-
* This relay's asks out of an Inbox load (GET /api/inbox), by id: what the
|
|
158
|
-
* session cards show. Built anew from each load, so an ask that was
|
|
159
|
-
* withdrawn or cancelled (no longer listed) drops out.
|
|
160
|
-
*/
|
|
161
|
-
export function sessionAsksFromInbox(relays) {
|
|
162
|
-
const own = (Array.isArray(relays) ? relays : []).find((relay) => relay?.self === true);
|
|
163
|
-
const asks = new Map();
|
|
164
|
-
for (const item of Array.isArray(own?.items) ? own.items : []) {
|
|
165
|
-
if (item?.kind === 'ask' && toText(item.id)) asks.set(item.id, { ...item, conversationId: item.session?.id });
|
|
166
|
-
}
|
|
167
|
-
return asks;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
156
|
// The folds a card has: an ask's `Details` and a plan card's `The plan`.
|
|
171
157
|
export const INBOX_FOLD_SELECTOR = 'details.inbox-item-detail, details.inbox-plan-body';
|
|
172
158
|
|
|
@@ -226,6 +212,25 @@ export function inboxPendingCount(relays) {
|
|
|
226
212
|
.reduce((sum, relay) => sum + (Array.isArray(relay?.items) ? relay.items : []).filter((item) => toText(item?.status) === 'pending').length, 0);
|
|
227
213
|
}
|
|
228
214
|
|
|
215
|
+
/**
|
|
216
|
+
* The sidebar badge: the urgent count, marked, while an urgent item waits;
|
|
217
|
+
* else every pending item. Only asks can be urgent (`urgency: 'now'`); a
|
|
218
|
+
* card a turn waits on or a plan card counts as a normal one.
|
|
219
|
+
*/
|
|
220
|
+
export function inboxBadge(relays) {
|
|
221
|
+
let pending = 0;
|
|
222
|
+
let urgent = 0;
|
|
223
|
+
for (const relay of Array.isArray(relays) ? relays : []) {
|
|
224
|
+
for (const item of Array.isArray(relay?.items) ? relay.items : []) {
|
|
225
|
+
if (toText(item?.status) !== 'pending') continue;
|
|
226
|
+
pending += 1;
|
|
227
|
+
if (toText(item.kind) === 'ask' && item.urgency === 'now') urgent += 1;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (urgent > 0) return { text: String(urgent), urgent: true, pending, urgentCount: urgent };
|
|
231
|
+
return { text: pending ? String(pending) : '', urgent: false, pending, urgentCount: 0 };
|
|
232
|
+
}
|
|
233
|
+
|
|
229
234
|
function headLine(doc, item, { relay, now, label }) {
|
|
230
235
|
const head = el(doc, 'div', 'inbox-item-head');
|
|
231
236
|
head.append(el(doc, 'span', 'inbox-item-kind', label));
|
|
@@ -263,9 +268,17 @@ function sessionLine(doc, item, relay, { withLink = true } = {}) {
|
|
|
263
268
|
return line;
|
|
264
269
|
}
|
|
265
270
|
|
|
266
|
-
function outcomeLine(doc, item) {
|
|
271
|
+
function outcomeLine(doc, item, mode = 'inbox') {
|
|
267
272
|
if (item.status === 'dismissed') return el(doc, 'div', 'inbox-item-outcome', 'Dismissed');
|
|
273
|
+
// The ๐ panel lists every close of the last day; the Inbox only these two.
|
|
274
|
+
if (mode === 'panel' && item.status === 'withdrawn') return el(doc, 'div', 'inbox-item-outcome', 'Withdrawn');
|
|
275
|
+
if (mode === 'panel' && item.status === 'cancelled') return el(doc, 'div', 'inbox-item-outcome', 'Cancelled');
|
|
268
276
|
if (item.status !== 'answered') return null;
|
|
277
|
+
if (mode === 'panel') {
|
|
278
|
+
// The choice, or the answer's first line.
|
|
279
|
+
const short = toText(item.choice) || toText(String(item.answer || '').split(/\r?\n/).find((line) => line.trim()) || '');
|
|
280
|
+
return el(doc, 'div', 'inbox-item-outcome', `Answered: ${short}`);
|
|
281
|
+
}
|
|
269
282
|
const answer = toText(item.answer) || toText(item.choice);
|
|
270
283
|
const via = toText(item.answeredVia);
|
|
271
284
|
return el(doc, 'div', 'inbox-item-outcome', `Answered: ${answer}${via ? ` (from ${via})` : ''}`);
|
|
@@ -273,8 +286,9 @@ function outcomeLine(doc, item) {
|
|
|
273
286
|
|
|
274
287
|
/**
|
|
275
288
|
* A question that does not block, as a card: in the Inbox (`mode` "inbox",
|
|
276
|
-
* with its session and relay)
|
|
277
|
-
*
|
|
289
|
+
* with its session and relay), in its session ("session") or in the
|
|
290
|
+
* session's ๐ panel ("panel": its Details open). `answerable`: the buttons
|
|
291
|
+
* answer it (this relay's, or a paired relay's ask).
|
|
278
292
|
*/
|
|
279
293
|
export function buildAskCard(doc, item, { relay = null, mode = 'inbox', now = Date.now(), answerable = true } = {}) {
|
|
280
294
|
const card = el(doc, 'article', `inbox-item inbox-ask${mode === 'session' ? ' relay-ask-card' : ''}`);
|
|
@@ -282,7 +296,8 @@ export function buildAskCard(doc, item, { relay = null, mode = 'inbox', now = Da
|
|
|
282
296
|
card.dataset.kind = 'ask';
|
|
283
297
|
if (relay?.id) card.dataset.relayId = relay.id;
|
|
284
298
|
card.dataset.status = toText(item.status) || 'pending';
|
|
285
|
-
|
|
299
|
+
const label = mode === 'session' ? 'Question for you ยท does not block this session' : (mode === 'panel' ? 'Question for you' : 'Question');
|
|
300
|
+
card.append(headLine(doc, item, { relay, now, label }));
|
|
286
301
|
if (mode === 'inbox') card.append(sessionLine(doc, item, relay));
|
|
287
302
|
else if (item.onBehalf && item.askedBy) card.append(sessionLine(doc, item, relay, { withLink: false }));
|
|
288
303
|
card.append(el(doc, 'p', 'inbox-item-question', item.question || item.prompt || ''));
|
|
@@ -295,9 +310,10 @@ export function buildAskCard(doc, item, { relay = null, mode = 'inbox', now = Da
|
|
|
295
310
|
details.append(el(doc, 'summary', '', 'Details'));
|
|
296
311
|
// Plain text, line breaks kept: never parsed as markup.
|
|
297
312
|
details.append(el(doc, 'pre', 'inbox-item-detail-text', item.detail));
|
|
313
|
+
if (mode === 'panel') details.open = true;
|
|
298
314
|
card.append(details);
|
|
299
315
|
}
|
|
300
|
-
const outcome = outcomeLine(doc, item);
|
|
316
|
+
const outcome = outcomeLine(doc, item, mode);
|
|
301
317
|
if (outcome) card.append(outcome);
|
|
302
318
|
if (item.status === 'pending' && answerable) {
|
|
303
319
|
const data = { relayId: relay?.id || 'self', itemId: item.id };
|