@oar-sh/oar 0.9.10 → 0.9.11
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 +314 -1
- package/README.md +156 -972
- package/package.json +13 -13
- package/server/README.md +95 -22
- package/server/claude-worker/claude-session-process.mjs +89 -11
- package/server/claude-worker/claude-session-worker.mjs +9 -1
- package/server/copilot-worker/copilot-sdk-session-process.mjs +76 -11
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +6 -0
- package/server/db-schema.mjs +59 -2
- package/server/grok-worker/acp-client.mjs +2 -1
- package/server/grok-worker/acp-host-services.mjs +4 -3
- package/server/migrations/0013-previews.mjs +130 -0
- package/server/migrations/0014-conversation-groups.mjs +89 -0
- package/server/migrations/0015-conversation-group-position.mjs +76 -0
- package/server/migrations/0016-share-access-log.mjs +110 -0
- package/server/public/app/about-ui.js +73 -0
- package/server/public/app/about-view.mjs +234 -0
- package/server/public/app/action-confirmations.js +31 -14
- package/server/public/app/agent-exchange-fold-view.mjs +40 -101
- package/server/public/app/api-client.js +251 -16
- package/server/public/app/ask-user-view.js +17 -15
- package/server/public/app/bootstrap.js +158 -32
- package/server/public/app/chat-actions-menu-state.mjs +20 -10
- package/server/public/app/context-menu.mjs +90 -6
- package/server/public/app/conversation-group-drag.mjs +191 -0
- package/server/public/app/conversation-group-order.mjs +265 -0
- package/server/public/app/conversation-group-state.mjs +100 -0
- package/server/public/app/conversation-list-drag.mjs +449 -0
- package/server/public/app/conversation-turn-state.mjs +81 -0
- package/server/public/app/conversation-view.js +136 -93
- package/server/public/app/cwd-picker.js +1 -1
- package/server/public/app/hidden-messages-view.mjs +312 -0
- package/server/public/app/host-suspend-ui.js +7 -1
- package/server/public/app/journal-view.js +787 -22
- package/server/public/app/preview-cards.mjs +117 -31
- package/server/public/app/preview-command.mjs +10 -1
- package/server/public/app/relay-board-view.js +19 -6
- package/server/public/app/relay-restart-error.mjs +16 -0
- package/server/public/app/settings-modal.js +40 -8
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/share-format.mjs +49 -0
- package/server/public/app/share-preview-marks.mjs +167 -0
- package/server/public/app/share-state.js +109 -0
- package/server/public/app/shared-conversations-settings.js +480 -0
- package/server/public/app/socket-handlers.js +80 -14
- package/server/public/app/store.js +96 -30
- package/server/public/app/subagent-row.mjs +3 -3
- package/server/public/app/subagent-viewer.mjs +30 -6
- package/server/public/app/thinking-follow.js +0 -4
- package/server/public/app/transcript-jump-bottom-view.mjs +194 -20
- package/server/public/app/transcript-jump-bottom.mjs +132 -21
- package/server/public/app/transcript-viewport-anchor.mjs +140 -0
- package/server/public/index.html +386 -15
- package/server/relay-tools.md +2 -1
- package/server/relay.mjs +2 -1
- package/server/repositories/conversation-group-repository.mjs +444 -0
- package/server/repositories/message-repository.mjs +1 -0
- package/server/repositories/session-repository.mjs +12 -37
- package/server/repositories/share-repository.mjs +322 -0
- package/server/repositories/share-visibility-statements.mjs +10 -0
- package/server/routes/about-routes.mjs +15 -0
- package/server/routes/cache-routes.mjs +4 -0
- package/server/routes/conversation-group-routes.mjs +171 -0
- package/server/routes/messages-routes.mjs +184 -65
- package/server/routes/preview-routes.mjs +9 -1
- package/server/routes/sessions-routes.mjs +771 -193
- package/server/server-runtime.mjs +316 -100
- package/server/services/about-service.mjs +461 -0
- package/server/services/background-task-store.mjs +128 -0
- package/server/services/background-work-view.mjs +228 -0
- package/server/services/cache-rebuild-service.mjs +30 -0
- package/server/services/delete-archive-service.mjs +4 -1
- package/server/services/detached-worker-watch-service.mjs +150 -0
- package/server/services/hidden-message-list.mjs +21 -0
- package/server/services/host-suspend-command.mjs +2 -1
- package/server/services/message-pin-list.mjs +1 -1
- package/server/services/preview-health-probe.mjs +8 -1
- package/server/services/preview-proxy-server.mjs +8 -4
- package/server/services/preview-registry-service.mjs +676 -26
- package/server/services/preview-static-handler.mjs +40 -7
- package/server/services/preview-store.mjs +274 -0
- package/server/services/rate-limit-service.mjs +12 -1
- package/server/services/relay-auth.mjs +183 -0
- package/server/services/relay-cli-launcher-service.mjs +3 -1
- package/server/services/relay-event-service.mjs +17 -0
- package/server/services/remote-relay-dispatcher.mjs +57 -5
- package/server/services/request-boundary.mjs +183 -0
- package/server/services/session-activity-tracker.mjs +227 -19
- package/server/services/session-worker-launch-service.mjs +190 -6
- package/server/services/session-worker-process-service.mjs +10 -6
- package/server/services/share-access-log-service.mjs +123 -0
- package/server/services/share-path-redaction.mjs +424 -0
- package/server/services/status-event-service.mjs +7 -3
- package/server/services/tunnel-worker-path-guard.mjs +24 -41
- package/server/services/windows-autostart-service.mjs +6 -1
- package/server/services/windows-boot-autostart-service.mjs +207 -23
- package/shared/preview-tool-core.mjs +15 -5
- package/shared/remote-relay-contract.mjs +3 -1
- package/shared/share-path-privacy.mjs +30 -0
- package/shared/windows-system-tools.mjs +106 -0
- package/shared/worker-runtime/background-task-digest.mjs +30 -0
- package/shared/worker-runtime/heartbeat.mjs +24 -0
- package/shared/worker-runtime/kept-posts.mjs +83 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +47 -15
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
// Messages hidden from shared viewers: the per-conversation list, the 🙈
|
|
2
|
+
// header button with its count, and the list modal (the shared summary modal,
|
|
3
|
+
// kind 'hidden-messages') with Jump, Unhide and Unhide all. Fed by the
|
|
4
|
+
// conversation payload's `hiddenMessages` on load and by the
|
|
5
|
+
// `conversation_hidden_messages_updated` socket event (REPLACE semantics per
|
|
6
|
+
// conversation), so a message hidden on another device shows here without a
|
|
7
|
+
// reload. Once a conversation's list is known it is what a bubble's Hide /
|
|
8
|
+
// Unhide label is drawn from. Owner-only: nothing of it in the shared view.
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
IS_SHARED_VIEW,
|
|
12
|
+
closeSummaryModal,
|
|
13
|
+
escHtml,
|
|
14
|
+
fmtDate,
|
|
15
|
+
openSummaryModal,
|
|
16
|
+
renderSummaryModalContent,
|
|
17
|
+
showTransientRelayNotice,
|
|
18
|
+
summaryModalState,
|
|
19
|
+
} from './store.js';
|
|
20
|
+
import { unhideAllMessagesFromShares, updateMessageShareVisibility } from './api-client.js';
|
|
21
|
+
import { jumpToPinnedMessage } from './pinned-messages-view.mjs';
|
|
22
|
+
|
|
23
|
+
export const HIDDEN_MESSAGES_MODAL_KIND = 'hidden-messages';
|
|
24
|
+
|
|
25
|
+
const hiddenByConversation = new Map();
|
|
26
|
+
// Ordered like the pin lists: an older list (a load that left before the
|
|
27
|
+
// change) must not win over the change's own socket event.
|
|
28
|
+
const hiddenRevisionByConversation = new Map();
|
|
29
|
+
// Conversations whose list arrived at least once (an empty one included).
|
|
30
|
+
const knownConversations = new Set();
|
|
31
|
+
const unhidesInFlight = new Set();
|
|
32
|
+
let unhideAllInFlight = false;
|
|
33
|
+
let confirmingUnhideAll = false;
|
|
34
|
+
let currentConversationId = null;
|
|
35
|
+
let modalBodyBound = false;
|
|
36
|
+
// conversation-view.js imports this module; it hands its side in through
|
|
37
|
+
// initHiddenMessagesView.
|
|
38
|
+
let handlers = {
|
|
39
|
+
syncRenderedShareVisibility: null,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function normalizeHiddenMessages(list) {
|
|
43
|
+
return (Array.isArray(list) ? list : [])
|
|
44
|
+
.map((item) => ({ ...item, messageId: String(item?.messageId || '').trim() }))
|
|
45
|
+
.filter((item) => item.messageId);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getConversationHiddenMessages(conversationId) {
|
|
49
|
+
return hiddenByConversation.get(String(conversationId || '').trim()) || [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// null while the conversation's list has not arrived: the caller falls back
|
|
53
|
+
// to what the message itself says.
|
|
54
|
+
export function isMessageHiddenFromShares(conversationId, messageId) {
|
|
55
|
+
const conversationKey = String(conversationId || '').trim();
|
|
56
|
+
const id = String(messageId || '').trim();
|
|
57
|
+
if (!conversationKey || !id || !knownConversations.has(conversationKey)) return null;
|
|
58
|
+
return getConversationHiddenMessages(conversationKey).some((item) => item.messageId === id);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function hiddenCountLabel(count) {
|
|
62
|
+
const total = Number(count) || 0;
|
|
63
|
+
return total ? `${total} hidden from shared viewers` : '';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function roleLabel(role) {
|
|
67
|
+
return String(role || '').trim().toLowerCase() === 'user' ? 'You' : 'Agent';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function renderHiddenListHtml(list, { confirming = false, busy = false } = {}) {
|
|
71
|
+
const items = normalizeHiddenMessages(list);
|
|
72
|
+
if (!items.length) {
|
|
73
|
+
return '<div class="pinned-empty">No hidden messages. Shared viewers see the whole conversation.</div>';
|
|
74
|
+
}
|
|
75
|
+
const count = items.length;
|
|
76
|
+
const actionsHtml = confirming
|
|
77
|
+
? `<div class="hidden-unhide-all-confirm">
|
|
78
|
+
<p>Show ${count === 1 ? 'this message' : `all ${count} messages`} to shared viewers again?</p>
|
|
79
|
+
<div class="summary-modal-actions">
|
|
80
|
+
<button type="button" class="chat-title-action-btn" data-hidden-unhide-all-confirm="1"${busy ? ' disabled' : ''}>${busy ? 'Unhiding…' : 'Unhide all'}</button>
|
|
81
|
+
<button type="button" class="chat-title-action-btn" data-hidden-unhide-all-cancel="1"${busy ? ' disabled' : ''}>Cancel</button>
|
|
82
|
+
</div>
|
|
83
|
+
</div>`
|
|
84
|
+
: `<div class="hidden-list-actions">
|
|
85
|
+
<button type="button" class="chat-title-action-btn" data-hidden-unhide-all="1">Unhide all</button>
|
|
86
|
+
</div>`;
|
|
87
|
+
return `${actionsHtml}<div class="pinned-list">${items.map((item) => {
|
|
88
|
+
const id = escHtml(item.messageId);
|
|
89
|
+
const when = fmtDate(item.timestamp);
|
|
90
|
+
const attachmentCount = Number(item.attachmentCount) || 0;
|
|
91
|
+
const meta = [
|
|
92
|
+
escHtml(roleLabel(item.role)),
|
|
93
|
+
when ? escHtml(when) : '',
|
|
94
|
+
attachmentCount ? `📎 ${attachmentCount}` : '',
|
|
95
|
+
].filter(Boolean).join(' · ');
|
|
96
|
+
const preview = String(item.preview || '').trim();
|
|
97
|
+
const previewHtml = preview
|
|
98
|
+
? escHtml(preview)
|
|
99
|
+
: '<span class="pinned-row-no-text">(no text)</span>';
|
|
100
|
+
const unhiding = unhidesInFlight.has(`${currentConversationId}:${item.messageId}`);
|
|
101
|
+
return `
|
|
102
|
+
<div class="pinned-row">
|
|
103
|
+
<button type="button" class="pinned-row-jump" data-hidden-jump="${id}">
|
|
104
|
+
<span class="pinned-row-meta">${meta}</span>
|
|
105
|
+
<span class="pinned-row-preview">${previewHtml}</span>
|
|
106
|
+
</button>
|
|
107
|
+
<button type="button" class="hidden-row-unhide" data-hidden-unhide="${id}" title="Show this message to shared viewers again"${unhiding ? ' disabled' : ''}>${unhiding ? 'Unhiding…' : 'Unhide'}</button>
|
|
108
|
+
</div>`;
|
|
109
|
+
}).join('')}</div>`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function syncHiddenHeaderButton() {
|
|
113
|
+
const button = document.getElementById('hidden-messages-btn');
|
|
114
|
+
if (!button) return;
|
|
115
|
+
const count = getConversationHiddenMessages(currentConversationId).length;
|
|
116
|
+
button.hidden = IS_SHARED_VIEW || !count;
|
|
117
|
+
const countEl = button.querySelector('.header-icon-count');
|
|
118
|
+
if (countEl) countEl.textContent = count ? String(count) : '';
|
|
119
|
+
const label = count ? `Hidden from shared viewers (${count})` : 'Hidden from shared viewers';
|
|
120
|
+
button.setAttribute('title', label);
|
|
121
|
+
button.setAttribute('aria-label', label);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function hiddenModalContent() {
|
|
125
|
+
const list = getConversationHiddenMessages(currentConversationId);
|
|
126
|
+
return {
|
|
127
|
+
title: '🙈 Hidden messages',
|
|
128
|
+
subtitle: hiddenCountLabel(list.length),
|
|
129
|
+
bodyHtml: renderHiddenListHtml(list, { confirming: confirmingUnhideAll, busy: unhideAllInFlight }),
|
|
130
|
+
kind: HIDDEN_MESSAGES_MODAL_KIND,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function rerenderOpenModal() {
|
|
135
|
+
if (summaryModalState.kind === HIDDEN_MESSAGES_MODAL_KIND) {
|
|
136
|
+
renderSummaryModalContent(hiddenModalContent());
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function syncCurrentConversationHiddenMessages() {
|
|
141
|
+
syncHiddenHeaderButton();
|
|
142
|
+
if (!getConversationHiddenMessages(currentConversationId).length) confirmingUnhideAll = false;
|
|
143
|
+
rerenderOpenModal();
|
|
144
|
+
if (knownConversations.has(String(currentConversationId || ''))) {
|
|
145
|
+
handlers.syncRenderedShareVisibility?.(
|
|
146
|
+
new Set(getConversationHiddenMessages(currentConversationId).map((item) => item.messageId)),
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function setConversationHiddenMessages(conversationId, list, revision = 0) {
|
|
152
|
+
const id = String(conversationId || '').trim();
|
|
153
|
+
if (!id) return;
|
|
154
|
+
const nextRevision = Number(revision) || 0;
|
|
155
|
+
if (nextRevision) {
|
|
156
|
+
if (nextRevision < (hiddenRevisionByConversation.get(id) || 0)) return;
|
|
157
|
+
hiddenRevisionByConversation.set(id, nextRevision);
|
|
158
|
+
}
|
|
159
|
+
const normalized = normalizeHiddenMessages(list);
|
|
160
|
+
const wasKnown = knownConversations.has(id);
|
|
161
|
+
knownConversations.add(id);
|
|
162
|
+
// The list arrives with every conversation load, the live poll included: an
|
|
163
|
+
// unchanged one must not redraw the open modal under the user's finger.
|
|
164
|
+
if (wasKnown && JSON.stringify(normalized) === JSON.stringify(getConversationHiddenMessages(id))) return;
|
|
165
|
+
if (normalized.length) hiddenByConversation.set(id, normalized);
|
|
166
|
+
else hiddenByConversation.delete(id);
|
|
167
|
+
if (id === currentConversationId) syncCurrentConversationHiddenMessages();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function setHiddenMessagesConversation(conversationId) {
|
|
171
|
+
const id = String(conversationId || '').trim() || null;
|
|
172
|
+
if (id === currentConversationId) return;
|
|
173
|
+
currentConversationId = id;
|
|
174
|
+
confirmingUnhideAll = false;
|
|
175
|
+
// The conversation was closed (deleted, new chat): its list goes with it.
|
|
176
|
+
if (!id && summaryModalState.kind === HIDDEN_MESSAGES_MODAL_KIND) closeSummaryModal();
|
|
177
|
+
syncCurrentConversationHiddenMessages();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function openHiddenMessagesModal() {
|
|
181
|
+
if (IS_SHARED_VIEW) return;
|
|
182
|
+
confirmingUnhideAll = false;
|
|
183
|
+
openSummaryModal(hiddenModalContent());
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function refusalText(result, fallback) {
|
|
187
|
+
return String(result?.error || '').trim() || fallback;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// The answer carries the conversation's whole hidden list, stored under that
|
|
191
|
+
// conversation's id: if the user has switched meanwhile, the open one is not
|
|
192
|
+
// touched.
|
|
193
|
+
export async function unhideHiddenMessage(conversationId, messageId) {
|
|
194
|
+
const conversationKey = String(conversationId || '').trim();
|
|
195
|
+
const targetMessageId = String(messageId || '').trim();
|
|
196
|
+
if (!conversationKey || !targetMessageId) return;
|
|
197
|
+
const flightKey = `${conversationKey}:${targetMessageId}`;
|
|
198
|
+
if (unhidesInFlight.has(flightKey)) return;
|
|
199
|
+
|
|
200
|
+
unhidesInFlight.add(flightKey);
|
|
201
|
+
if (conversationKey === currentConversationId) rerenderOpenModal();
|
|
202
|
+
const result = await updateMessageShareVisibility(conversationKey, targetMessageId, false);
|
|
203
|
+
unhidesInFlight.delete(flightKey);
|
|
204
|
+
if (!result?.ok) {
|
|
205
|
+
if (conversationKey === currentConversationId) rerenderOpenModal();
|
|
206
|
+
showTransientRelayNotice(refusalText(result, 'Could not unhide the message.'));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
showTransientRelayNotice('Message visible to shared viewers.');
|
|
210
|
+
if ('hiddenMessages' in result) {
|
|
211
|
+
setConversationHiddenMessages(conversationKey, result.hiddenMessages, result.hiddenMessagesRevision);
|
|
212
|
+
}
|
|
213
|
+
if (conversationKey === currentConversationId) rerenderOpenModal();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function skippedPreview(conversationKey, messageId) {
|
|
217
|
+
const item = getConversationHiddenMessages(conversationKey).find((entry) => entry.messageId === messageId);
|
|
218
|
+
const preview = String(item?.preview || '').trim();
|
|
219
|
+
if (!preview) return '';
|
|
220
|
+
const points = Array.from(preview);
|
|
221
|
+
return points.length > 40 ? `${points.slice(0, 39).join('').trimEnd()}…` : preview;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export async function unhideAllHiddenMessages(conversationId) {
|
|
225
|
+
const conversationKey = String(conversationId || '').trim();
|
|
226
|
+
if (!conversationKey || unhideAllInFlight) return;
|
|
227
|
+
unhideAllInFlight = true;
|
|
228
|
+
if (conversationKey === currentConversationId) rerenderOpenModal();
|
|
229
|
+
const result = await unhideAllMessagesFromShares(conversationKey);
|
|
230
|
+
unhideAllInFlight = false;
|
|
231
|
+
if (conversationKey === currentConversationId) confirmingUnhideAll = false;
|
|
232
|
+
if (!result?.ok) {
|
|
233
|
+
if (conversationKey === currentConversationId) rerenderOpenModal();
|
|
234
|
+
showTransientRelayNotice(refusalText(result, 'Could not unhide the messages.'));
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const unhidden = Array.isArray(result.unhidden) ? result.unhidden.length : 0;
|
|
238
|
+
const skipped = Array.isArray(result.skipped) ? result.skipped : [];
|
|
239
|
+
if (skipped.length) {
|
|
240
|
+
// Named before the new list replaces the old one: the preview is in the old.
|
|
241
|
+
const names = skipped
|
|
242
|
+
.map((entry) => skippedPreview(conversationKey, String(entry?.messageId || '').trim()))
|
|
243
|
+
.map((preview) => (preview ? `“${preview}”` : 'one message'))
|
|
244
|
+
.join(', ');
|
|
245
|
+
const lead = unhidden
|
|
246
|
+
? `${unhidden} message${unhidden === 1 ? '' : 's'} unhidden. `
|
|
247
|
+
: '';
|
|
248
|
+
showTransientRelayNotice(`${lead}Still hidden until its turn finishes: ${names}.`, 8000);
|
|
249
|
+
} else {
|
|
250
|
+
showTransientRelayNotice(unhidden
|
|
251
|
+
? `${unhidden} message${unhidden === 1 ? '' : 's'} visible to shared viewers.`
|
|
252
|
+
: 'No hidden messages left.');
|
|
253
|
+
}
|
|
254
|
+
if ('hiddenMessages' in result) {
|
|
255
|
+
setConversationHiddenMessages(conversationKey, result.hiddenMessages, result.hiddenMessagesRevision);
|
|
256
|
+
}
|
|
257
|
+
if (conversationKey === currentConversationId) rerenderOpenModal();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function handleHiddenModalClick(event) {
|
|
261
|
+
if (summaryModalState.kind !== HIDDEN_MESSAGES_MODAL_KIND) return;
|
|
262
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
263
|
+
const unhideBtn = target?.closest('[data-hidden-unhide]');
|
|
264
|
+
if (unhideBtn) {
|
|
265
|
+
event.preventDefault();
|
|
266
|
+
void unhideHiddenMessage(currentConversationId, unhideBtn.getAttribute('data-hidden-unhide'));
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (target?.closest('[data-hidden-unhide-all]')) {
|
|
270
|
+
event.preventDefault();
|
|
271
|
+
confirmingUnhideAll = true;
|
|
272
|
+
rerenderOpenModal();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (target?.closest('[data-hidden-unhide-all-cancel]')) {
|
|
276
|
+
event.preventDefault();
|
|
277
|
+
confirmingUnhideAll = false;
|
|
278
|
+
rerenderOpenModal();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (target?.closest('[data-hidden-unhide-all-confirm]')) {
|
|
282
|
+
event.preventDefault();
|
|
283
|
+
void unhideAllHiddenMessages(currentConversationId);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const jumpBtn = target?.closest('[data-hidden-jump]');
|
|
287
|
+
if (jumpBtn) {
|
|
288
|
+
event.preventDefault();
|
|
289
|
+
// The pin list's jump: scroll there, or load the window around it.
|
|
290
|
+
void jumpToPinnedMessage(jumpBtn.getAttribute('data-hidden-jump'));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function initHiddenMessagesView(nextHandlers = {}) {
|
|
295
|
+
handlers = { ...handlers, ...nextHandlers };
|
|
296
|
+
const button = document.getElementById('hidden-messages-btn');
|
|
297
|
+
if (button && button.dataset.bound !== '1') {
|
|
298
|
+
button.dataset.bound = '1';
|
|
299
|
+
button.addEventListener('click', (event) => {
|
|
300
|
+
event.preventDefault();
|
|
301
|
+
openHiddenMessagesModal();
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
// #summary-modal-body outlives every modal, so it is bound exactly once; the
|
|
305
|
+
// handler no-ops unless the hidden list is the modal currently mounted.
|
|
306
|
+
const modalBody = document.getElementById('summary-modal-body');
|
|
307
|
+
if (modalBody && !modalBodyBound) {
|
|
308
|
+
modalBodyBound = true;
|
|
309
|
+
modalBody.addEventListener('click', handleHiddenModalClick);
|
|
310
|
+
}
|
|
311
|
+
syncHiddenHeaderButton();
|
|
312
|
+
}
|
|
@@ -170,7 +170,13 @@ async function runCancelRelayShutdown() {
|
|
|
170
170
|
cancelInFlight = true;
|
|
171
171
|
renderPendingActionBanner();
|
|
172
172
|
try {
|
|
173
|
-
|
|
173
|
+
let result = null;
|
|
174
|
+
try {
|
|
175
|
+
result = await cancelRelayRestart();
|
|
176
|
+
} catch (error) {
|
|
177
|
+
showTransientRelayNotice(`Failed to cancel the queued relay restart: ${error?.message || 'unknown error'}`, 6000);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
174
180
|
if (!result?.ok) {
|
|
175
181
|
showTransientRelayNotice('Failed to cancel the queued relay restart.', 5000);
|
|
176
182
|
return;
|