@oar-sh/oar 0.9.9 → 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 +561 -1
- package/README.md +162 -909
- package/bin/bat/oar.bat +46 -46
- package/package.json +14 -14
- package/server/README.md +369 -13
- package/server/claude-worker/claude-remote-relay-tool.mjs +10 -0
- package/server/claude-worker/claude-session-process.mjs +121 -9
- package/server/claude-worker/claude-session-worker.mjs +9 -1
- package/server/claude-worker/claude-turn-publisher.mjs +3 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +31 -4
- package/server/claude-worker/subagent-timeline-recorder.mjs +572 -0
- package/server/copilot-worker/copilot-sdk-session-process.mjs +79 -12
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +6 -0
- package/server/cursor-worker/cursor-sdk-adapter.mjs +56 -2
- package/server/db-schema.mjs +95 -2
- package/server/grok-worker/acp-client.mjs +2 -1
- package/server/grok-worker/acp-host-services.mjs +4 -3
- package/server/migrations/0009-subagent-timeline.mjs +135 -0
- package/server/migrations/0010-subagent-timeline-pruning.mjs +89 -0
- package/server/migrations/0011-relay-event-subscriptions.mjs +125 -0
- package/server/migrations/0012-cross-relay-event-subscriptions.mjs +116 -0
- 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 +187 -0
- package/server/public/app/api-client.js +312 -16
- package/server/public/app/archive-confirm.mjs +28 -0
- package/server/public/app/ask-user-view.js +17 -12
- package/server/public/app/attachments-view.js +516 -6
- package/server/public/app/background-tasks-view.mjs +18 -26
- package/server/public/app/bootstrap.js +336 -59
- package/server/public/app/chat-actions-menu-state.mjs +126 -0
- package/server/public/app/claude-cloud-conversation-ui.js +10 -22
- package/server/public/app/composer-control-state.mjs +16 -0
- package/server/public/app/composer-keyboard.mjs +54 -0
- 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 +436 -117
- package/server/public/app/cwd-picker.js +1 -1
- package/server/public/app/explorer-edit-state.mjs +68 -0
- package/server/public/app/hidden-messages-view.mjs +312 -0
- package/server/public/app/host-suspend-ui.js +13 -4
- package/server/public/app/journal-view.js +930 -45
- package/server/public/app/no-conversation-state.mjs +58 -0
- package/server/public/app/preview-cards.mjs +117 -31
- package/server/public/app/preview-command.mjs +10 -1
- package/server/public/app/provider-readiness.mjs +120 -0
- package/server/public/app/relay-board-view.js +19 -3
- package/server/public/app/relay-event-subscriptions-ui.js +184 -0
- package/server/public/app/relay-event-view.mjs +234 -0
- package/server/public/app/relay-restart-error.mjs +16 -0
- package/server/public/app/settings-modal.js +93 -9
- 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 +137 -40
- package/server/public/app/store.js +115 -32
- package/server/public/app/subagent-row.mjs +138 -0
- package/server/public/app/subagent-timeline-view.mjs +186 -0
- package/server/public/app/subagent-viewer-watch.mjs +50 -0
- package/server/public/app/subagent-viewer.mjs +558 -0
- package/server/public/app/task-row-format.mjs +29 -0
- package/server/public/app/transcript-jump-bottom-view.mjs +464 -0
- package/server/public/app/transcript-jump-bottom.mjs +210 -0
- package/server/public/app/transcript-reload-freshness.mjs +25 -0
- package/server/public/app/transcript-viewport-anchor.mjs +140 -0
- package/server/public/app/update-ui.js +1 -1
- package/server/public/app/viewer-history.mjs +5 -3
- package/server/public/index.html +774 -12
- 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 +30 -3
- package/server/repositories/question-repository.mjs +9 -3
- package/server/repositories/remote-relay-repository.mjs +6 -2
- 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/repositories/subagent-timeline-statements.mjs +170 -0
- package/server/routes/about-routes.mjs +15 -0
- package/server/routes/ask-user-routes.mjs +3 -0
- package/server/routes/cache-routes.mjs +4 -0
- package/server/routes/conversation-group-routes.mjs +171 -0
- package/server/routes/messages-routes.mjs +698 -114
- package/server/routes/preview-routes.mjs +9 -1
- package/server/routes/relay-event-routes.mjs +157 -0
- package/server/routes/remote-relay-routes.mjs +15 -0
- package/server/routes/sessions-routes.mjs +843 -193
- package/server/server-runtime.mjs +559 -160
- 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/explorer-entry-name.mjs +26 -0
- package/server/services/explorer-mutations.mjs +122 -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 +49 -27
- 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/push-dispatch-service.mjs +18 -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-link-service.mjs +289 -0
- package/server/services/relay-event-service.mjs +1952 -0
- package/server/services/relay-shutdown-gate.mjs +31 -0
- package/server/services/remote-relay-caller-context.mjs +52 -7
- package/server/services/remote-relay-dispatcher.mjs +551 -7
- package/server/services/remote-relay-registry-service.mjs +11 -1
- package/server/services/request-boundary.mjs +183 -0
- package/server/services/session-activity-tracker.mjs +695 -0
- package/server/services/session-history-refresh-service.mjs +7 -0
- 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/stale-attempt-log-limiter.mjs +96 -0
- package/server/services/stale-recovery-clock-guard.mjs +57 -0
- package/server/services/stale-subagent-run-sweep.mjs +109 -0
- package/server/services/status-event-service.mjs +7 -3
- package/server/services/subagent-record-retention.mjs +164 -0
- package/server/services/subagent-timeline-service.mjs +328 -0
- package/server/services/subagent-viewer-rooms.mjs +119 -0
- package/server/services/tunnel-worker-path-guard.mjs +24 -41
- package/server/services/upgrade-socket-guard.mjs +16 -0
- 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/relay-events.mjs +371 -0
- package/shared/remote-relay-contract.mjs +160 -6
- package/shared/share-path-privacy.mjs +30 -0
- package/shared/subagent-record-retention.mjs +31 -0
- package/shared/subagent-timeline.mjs +79 -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
|
@@ -608,7 +608,7 @@ async function submitChangeCwd(launchAfterChange = false) {
|
|
|
608
608
|
? `Next launch CWD saved as ${updatedPath}. The running CLI keeps its current CWD.`
|
|
609
609
|
: `Next launch CWD saved as ${updatedPath}.`);
|
|
610
610
|
} catch (error) {
|
|
611
|
-
alert(error?.message
|
|
611
|
+
alert(error?.message ? `Failed to update the launch CWD: ${error.message}` : 'Failed to update the launch CWD');
|
|
612
612
|
} finally {
|
|
613
613
|
changeCwdInFlight = false;
|
|
614
614
|
setSummaryModalLoading(false);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Path arithmetic for the explorer's edits (new folder, rename, delete), on
|
|
2
|
+
// the path forms the explorer already uses: workspace paths are relative and
|
|
3
|
+
// `/`-joined with `''` for the root; drive paths are absolute (`/x/y` or
|
|
4
|
+
// `C:/x/y`, `C:` for a drive root). Pure, so it runs under node in tests.
|
|
5
|
+
|
|
6
|
+
/** The name part of a path. */
|
|
7
|
+
export function entryNameOf(pathValue) {
|
|
8
|
+
const value = String(pathValue || '');
|
|
9
|
+
const trimmed = value.endsWith('/') && value.length > 1 ? value.slice(0, -1) : value;
|
|
10
|
+
const index = trimmed.lastIndexOf('/');
|
|
11
|
+
return index === -1 ? trimmed : trimmed.slice(index + 1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The parent of a path: `''` above a top-level workspace entry or a drive root, `/` above `/x`. */
|
|
15
|
+
export function parentPathOf(pathValue) {
|
|
16
|
+
const value = String(pathValue || '');
|
|
17
|
+
const index = value.lastIndexOf('/');
|
|
18
|
+
if (index === -1) return '';
|
|
19
|
+
if (index === 0) return '/';
|
|
20
|
+
return value.slice(0, index);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The path of `name` inside `parentPath`. */
|
|
24
|
+
export function childPathOf(parentPath, name) {
|
|
25
|
+
const parent = String(parentPath || '');
|
|
26
|
+
const child = String(name || '');
|
|
27
|
+
if (!parent) return child;
|
|
28
|
+
return parent.endsWith('/') ? `${parent}${child}` : `${parent}/${child}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** True when `pathValue` is `prefix` or lies below it. */
|
|
32
|
+
export function isSameOrBelow(pathValue, prefix) {
|
|
33
|
+
const value = String(pathValue || '');
|
|
34
|
+
const base = String(prefix || '');
|
|
35
|
+
if (value === base) return true;
|
|
36
|
+
if (!base) return value !== '';
|
|
37
|
+
return value.startsWith(base.endsWith('/') ? base : `${base}/`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** `pathValue` with `oldPrefix` replaced by `newPrefix` when it lies at or below it; otherwise unchanged. */
|
|
41
|
+
export function rewritePathPrefix(pathValue, oldPrefix, newPrefix) {
|
|
42
|
+
const value = String(pathValue || '');
|
|
43
|
+
if (!isSameOrBelow(value, oldPrefix) || oldPrefix === '') return value;
|
|
44
|
+
return `${newPrefix}${value.slice(String(oldPrefix).length)}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A new Set with every member rewritten through `rewritePathPrefix`. */
|
|
48
|
+
export function rewritePathSet(paths, oldPrefix, newPrefix) {
|
|
49
|
+
const out = new Set();
|
|
50
|
+
for (const value of paths || []) out.add(rewritePathPrefix(value, oldPrefix, newPrefix));
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A new Set without the members at or below `prefix`. */
|
|
55
|
+
export function dropPathsBelow(paths, prefix) {
|
|
56
|
+
const out = new Set();
|
|
57
|
+
for (const value of paths || []) {
|
|
58
|
+
if (prefix === '' || !isSameOrBelow(value, prefix)) out.add(value);
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The descendants of `dirPath` among `paths`, shallowest first, for a re-expansion walk. */
|
|
64
|
+
export function descendantsOf(paths, dirPath) {
|
|
65
|
+
return [...(paths || [])]
|
|
66
|
+
.filter((value) => value && value !== dirPath && isSameOrBelow(value, dirPath))
|
|
67
|
+
.sort((a, b) => a.length - b.length);
|
|
68
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -103,10 +103,13 @@ export function relayShutdownBannerText(state) {
|
|
|
103
103
|
if (String(state.status || '') === 'shutting_down') {
|
|
104
104
|
return `${icon} Relay ${action === 'restart' ? 'restarting' : 'shutting down'} now…`;
|
|
105
105
|
}
|
|
106
|
+
// Only the processing turns hold it; queued messages wait for the restart.
|
|
106
107
|
const queue = state.queue || {};
|
|
107
|
-
const turns = Number(queue.
|
|
108
|
-
const
|
|
109
|
-
|
|
108
|
+
const turns = Number(queue.processingCount || 0);
|
|
109
|
+
const held = Number(queue.pendingCount || 0) + Number(queue.parkedCount || 0);
|
|
110
|
+
const waiting = turns > 0 ? `waiting for ${turns} running turn${turns === 1 ? '' : 's'} to finish` : 'nothing is running, going down now';
|
|
111
|
+
const queued = held > 0 ? `; ${held} queued message${held === 1 ? '' : 's'} wait${held === 1 ? 's' : ''} for it` : '';
|
|
112
|
+
return `${icon} Relay ${action} queued — ${waiting}${queued}`;
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
function ensureBanner() {
|
|
@@ -167,7 +170,13 @@ async function runCancelRelayShutdown() {
|
|
|
167
170
|
cancelInFlight = true;
|
|
168
171
|
renderPendingActionBanner();
|
|
169
172
|
try {
|
|
170
|
-
|
|
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
|
+
}
|
|
171
180
|
if (!result?.ok) {
|
|
172
181
|
showTransientRelayNotice('Failed to cancel the queued relay restart.', 5000);
|
|
173
182
|
return;
|