@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
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
// Provenance of work another relay's agent started here: the badge on a user
|
|
2
2
|
// bubble ("↗ from win-test · “report builder” · claude-sonnet-5", linking back
|
|
3
|
-
// to the source session) and the
|
|
3
|
+
// to the source session) and the line in the sidebar row's ⓘ menu of a
|
|
4
4
|
// conversation such an agent created. Both read the `origin` the relay stores
|
|
5
5
|
// with the message or conversation (docs/plans/2026-09-27-remote-relays.md §4).
|
|
6
6
|
//
|
|
7
7
|
// An agent on THIS relay can start sessions too. Such an origin carries
|
|
8
|
-
// `local: true` and names the conversation the agent runs in: the
|
|
9
|
-
// reads "via agent · “title”" and opens that conversation inside the app
|
|
8
|
+
// `local: true` and names the conversation the agent runs in: the header
|
|
9
|
+
// marker then reads "via agent · “title”" and opens that conversation inside the app
|
|
10
10
|
// (window.openOriginConversation, journal-view.js) instead of linking to
|
|
11
11
|
// another relay. An origin without `local` renders exactly as before.
|
|
12
12
|
|
|
13
13
|
import { remoteConversationUrl } from './remote-relay-shared.mjs';
|
|
14
|
+
import { chatMetaLabelHtml } from './chat-header-meta.mjs';
|
|
14
15
|
|
|
15
16
|
function escapeHtml(value) {
|
|
16
17
|
return String(value ?? '')
|
|
@@ -81,8 +82,8 @@ export function localAgentOriginModel(origin, {
|
|
|
81
82
|
|
|
82
83
|
// A button while the orchestrating conversation can be opened, plain text
|
|
83
84
|
// otherwise. `tooltip` is a full sentence.
|
|
84
|
-
function localAgentElementHtml(model, { className, text = model.text, tooltip }) {
|
|
85
|
-
const label = escapeHtml(text);
|
|
85
|
+
function localAgentElementHtml(model, { className, text = model.text, tooltip, labelHtml = null }) {
|
|
86
|
+
const label = labelHtml ?? escapeHtml(text);
|
|
86
87
|
if (model.linkable) {
|
|
87
88
|
return `<button type="button" class="${className}" data-origin-conversation-id="${escapeHtml(model.conversationId)}" title="${escapeHtml(`${tooltip} Opens that conversation.`)}" onclick="openOriginConversation(event, this)">${label}</button>`;
|
|
88
89
|
}
|
|
@@ -159,34 +160,51 @@ export function renderRemoteOriginBadgeHtml(origin, { linkable = true, conversat
|
|
|
159
160
|
}
|
|
160
161
|
|
|
161
162
|
/**
|
|
162
|
-
* The sidebar
|
|
163
|
-
*
|
|
163
|
+
* The line of the sidebar row's ⓘ menu for a conversation an agent created,
|
|
164
|
+
* or null. Plain text, for the menu to escape:
|
|
165
|
+
* - an agent on this relay: `{ kind: 'local', label: 'Started by an agent in
|
|
166
|
+
* “title”', conversationId, linkable }` with the whole title, never cut;
|
|
167
|
+
* while linkable, the line opens that conversation;
|
|
168
|
+
* - another relay's agent: `{ kind: 'remote', label: 'Started by an agent on
|
|
169
|
+
* relay “name”', href }`; with an href, the line opens its session there.
|
|
170
|
+
* `linkable: false` (the shared view) gives lines without an action.
|
|
164
171
|
*/
|
|
165
|
-
export function
|
|
166
|
-
const local = localAgentOriginModel(conversation?.origin, {
|
|
167
|
-
conversations,
|
|
168
|
-
linkable,
|
|
169
|
-
ownId: conversation?.id,
|
|
170
|
-
maxTitleLength: ORIGIN_TITLE_MAX_SIDEBAR,
|
|
171
|
-
});
|
|
172
|
+
export function conversationOriginInfo(conversation, { conversations = null, linkable = true } = {}) {
|
|
173
|
+
const local = localAgentOriginModel(conversation?.origin, { conversations, linkable, ownId: conversation?.id });
|
|
172
174
|
if (local) {
|
|
173
|
-
return
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
return {
|
|
176
|
+
kind: 'local',
|
|
177
|
+
// The whole title: the menu wraps its lines, and nothing else in the
|
|
178
|
+
// list shows it.
|
|
179
|
+
label: local.fullTitle ? `Started by an agent in “${local.fullTitle}”` : 'Started by an agent in another conversation',
|
|
180
|
+
conversationId: local.conversationId,
|
|
181
|
+
linkable: local.linkable,
|
|
182
|
+
};
|
|
177
183
|
}
|
|
178
184
|
const normalized = normalizeRemoteOrigin(conversation?.origin);
|
|
179
|
-
if (!normalized) return
|
|
180
|
-
|
|
181
|
-
|
|
185
|
+
if (!normalized) return null;
|
|
186
|
+
return {
|
|
187
|
+
kind: 'remote',
|
|
188
|
+
label: `Started by an agent on relay “${normalized.relayName}”`,
|
|
189
|
+
href: linkable !== false ? remoteOriginLink(normalized) : '',
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** The phone's short header label: "↗ title" (the sidebar's length), or "↗ agent". */
|
|
194
|
+
export function conversationHeaderOriginShortText(model) {
|
|
195
|
+
if (!model) return '';
|
|
196
|
+
const title = truncateOriginTitle(model.fullTitle, ORIGIN_TITLE_MAX_SIDEBAR);
|
|
197
|
+
return title ? `↗ ${title}` : '↗ agent';
|
|
182
198
|
}
|
|
183
199
|
|
|
184
200
|
/**
|
|
185
201
|
* The same marker for the conversation header, with room for more of the
|
|
186
|
-
* title. Only a local origin has one; '' for everything else.
|
|
202
|
+
* title. Only a local origin has one; '' for everything else. It carries a
|
|
203
|
+
* long and a short label (chat-header-meta.mjs); CSS shows one of them.
|
|
187
204
|
*/
|
|
188
205
|
export function renderConversationHeaderOriginHtml(conversation, { conversations = null, linkable = true } = {}) {
|
|
189
206
|
const local = localAgentOriginModel(conversation?.origin, { conversations, linkable, ownId: conversation?.id });
|
|
190
207
|
if (!local) return '';
|
|
191
|
-
|
|
208
|
+
const labelHtml = chatMetaLabelHtml(escapeHtml(local.text), escapeHtml(conversationHeaderOriginShortText(local)));
|
|
209
|
+
return localAgentElementHtml(local, { className: 'conv-origin-agent', tooltip: startedByTooltip(local), labelHtml });
|
|
192
210
|
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// Wires session-history.mjs to the page: the popstate listener, the
|
|
2
|
+
// overlays it watches, the document title, and the hooks the open path calls
|
|
3
|
+
// (journal-view.js). Before initSessionHistory() runs (the shared reader, the
|
|
4
|
+
// unit tests) every hook here does nothing.
|
|
5
|
+
|
|
6
|
+
import { conversations } from './store.js';
|
|
7
|
+
import { closeContextMenu } from './context-menu.mjs';
|
|
8
|
+
import { isStatusViewActive, toggleStatusView } from './status-view.mjs';
|
|
9
|
+
import {
|
|
10
|
+
armVisitBaseOnFirstInteraction,
|
|
11
|
+
createGoneList,
|
|
12
|
+
createOverlayWatch,
|
|
13
|
+
createSessionHistory,
|
|
14
|
+
} from './session-history.mjs';
|
|
15
|
+
|
|
16
|
+
const PHONE_QUERY = '(max-width: 680px)';
|
|
17
|
+
|
|
18
|
+
let sessionHistory = null;
|
|
19
|
+
let baseTitle = '';
|
|
20
|
+
// The relay knew no conversation the page tried to open, and said so: the
|
|
21
|
+
// empty screen stays until the reader opens one (no question jumps in).
|
|
22
|
+
let autoOpenHeld = false;
|
|
23
|
+
|
|
24
|
+
function tabStorage() {
|
|
25
|
+
try {
|
|
26
|
+
return typeof sessionStorage === 'undefined' ? null : sessionStorage;
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Kept in the tab across reloads, as its history is.
|
|
33
|
+
const goneConversationIds = createGoneList({ storage: tabStorage() });
|
|
34
|
+
|
|
35
|
+
function isPhone() {
|
|
36
|
+
try {
|
|
37
|
+
return !!window.matchMedia?.(PHONE_QUERY)?.matches;
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function titleFor(id) {
|
|
44
|
+
if (!id) return baseTitle;
|
|
45
|
+
return String(conversations[id]?.title || '').trim() || baseTitle;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function setDocumentTitle(id) {
|
|
49
|
+
const next = titleFor(id);
|
|
50
|
+
if (document.title !== next) document.title = next;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const isVisible = (element) => !!element?.classList?.contains('visible');
|
|
54
|
+
const isShown = (element) => !!element && !element.hidden;
|
|
55
|
+
const byId = (id) => () => document.getElementById(id);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The Inbox's and Status's closer, told where to go back to: the session the
|
|
59
|
+
* history stands on. One view opened from the other has no session of its
|
|
60
|
+
* own to return to (the first one had already left it).
|
|
61
|
+
*/
|
|
62
|
+
function backToSession(close) {
|
|
63
|
+
if (typeof close !== 'function') return undefined;
|
|
64
|
+
return () => close({ returnTo: sessionHistory?.conversation || '' });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Each overlay back closes before it switches sessions: `find()` names its
|
|
69
|
+
* element (null while there is none: some are built on first use), `isOpen`
|
|
70
|
+
* reads it. `closers` come from bootstrap.js, which holds most of them.
|
|
71
|
+
* Opened in the same moment, the earlier one in this list lies below.
|
|
72
|
+
*/
|
|
73
|
+
function overlayDescriptors(closers) {
|
|
74
|
+
return [
|
|
75
|
+
{ name: 'sidebar', find: byId('sidebar'), isOpen: (el) => isPhone() && !!el?.classList?.contains('open'), close: closers.closeSidebar },
|
|
76
|
+
// The Inbox takes the transcript's place: back leaves it for the session it came from.
|
|
77
|
+
{ name: 'inbox', find: () => document.body, isOpen: (el) => !!el?.classList?.contains('inbox-view-active'), close: backToSession(closers.closeInboxView) },
|
|
78
|
+
// Status the same way.
|
|
79
|
+
{ name: 'status', find: () => document.body, isOpen: (el) => !!el?.classList?.contains('status-view-active'), close: backToSession(closers.closeStatusView) },
|
|
80
|
+
{ name: 'composer-settings', find: byId('composer-settings'), isOpen: isShown, close: closers.closeComposerSettings },
|
|
81
|
+
{ name: 'chat-actions-menu', find: byId('chat-actions-menu'), isOpen: isShown, close: closers.closeChatActionsMenu },
|
|
82
|
+
{ name: 'summary-modal', find: byId('summary-modal'), isOpen: isVisible, close: closers.closeSummaryModal },
|
|
83
|
+
// The Change CWD dialog's list of known folders (inside the summary modal).
|
|
84
|
+
{
|
|
85
|
+
name: 'change-cwd-menu',
|
|
86
|
+
find: byId('change-cwd-menu'),
|
|
87
|
+
isOpen: (el) => isShown(el) && isVisible(document.getElementById('summary-modal')),
|
|
88
|
+
close: closers.closeChangeCwdMenu,
|
|
89
|
+
},
|
|
90
|
+
{ name: 'settings-modal', find: byId('settings-modal'), isOpen: isVisible, close: closers.closeSettingsModal },
|
|
91
|
+
{ name: 'message-search-modal', find: byId('message-search-modal'), isOpen: isVisible, close: closers.closeMessageSearchModal },
|
|
92
|
+
{ name: 'repo-browser-modal', find: byId('repo-browser-modal'), isOpen: isVisible, close: closers.closeRepoBrowser },
|
|
93
|
+
{ name: 'new-conversation-model-modal', find: byId('new-conversation-model-modal'), isOpen: isVisible, close: closers.closeNewConversationModelModal },
|
|
94
|
+
{ name: 'git-changes-modal', find: byId('git-changes-modal'), isOpen: isVisible, close: closers.closeGitChangesModal },
|
|
95
|
+
{ name: 'git-diff-modal', find: byId('git-diff-modal'), isOpen: isVisible, close: closers.closeGitDiffViewer },
|
|
96
|
+
{ name: 'tmux-inspector-modal', find: byId('tmux-inspector-modal'), isOpen: isVisible, close: closers.closeTmuxInspector },
|
|
97
|
+
{ name: 'image-annotate-modal', find: byId('image-annotate-modal'), isOpen: isVisible, close: closers.closeAnnotateEditor },
|
|
98
|
+
{ name: 'relay-watch-panel', find: byId('relay-watch-panel'), isOpen: isShown, close: closers.closeRelayWatchPanel },
|
|
99
|
+
{ name: 'relay-schedule-panel', find: byId('relay-schedule-panel'), isOpen: isShown, close: closers.closeRelaySchedulePanel },
|
|
100
|
+
{ name: 'emoji-popup', find: byId('emoji-popup'), isOpen: isVisible, close: closers.closeEmojiPicker },
|
|
101
|
+
// A row's menu (right-click, long press, ⓘ): on <body> while open.
|
|
102
|
+
{ name: 'context-menu', find: () => document.querySelector('body > .context-menu'), isOpen: (el) => !!el, close: closeContextMenu },
|
|
103
|
+
];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Where the overlays built on first use appear: the panels and the row menu
|
|
107
|
+
// on <body>, the folder list inside the summary modal.
|
|
108
|
+
const OVERLAY_HOSTS = [
|
|
109
|
+
['body', { attributes: true, attributeFilter: ['class'], childList: true }],
|
|
110
|
+
['summary-modal', { attributes: true, attributeFilter: ['class', 'hidden'], childList: true, subtree: true }],
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
function watchOverlays(closers) {
|
|
114
|
+
const overlays = overlayDescriptors(closers)
|
|
115
|
+
.filter((descriptor) => typeof descriptor.close === 'function')
|
|
116
|
+
.map((descriptor) => ({
|
|
117
|
+
name: descriptor.name,
|
|
118
|
+
find: descriptor.find,
|
|
119
|
+
isOpen: () => {
|
|
120
|
+
const element = descriptor.find();
|
|
121
|
+
return !!element && descriptor.isOpen(element);
|
|
122
|
+
},
|
|
123
|
+
close: () => descriptor.close(),
|
|
124
|
+
}));
|
|
125
|
+
const watch = createOverlayWatch({
|
|
126
|
+
overlays,
|
|
127
|
+
onOpened: (overlay) => sessionHistory?.overlayOpened(overlay),
|
|
128
|
+
onClosed: (name) => sessionHistory?.overlayClosed(name),
|
|
129
|
+
});
|
|
130
|
+
if (typeof MutationObserver === 'function') {
|
|
131
|
+
const observed = new WeakSet();
|
|
132
|
+
let observer = null;
|
|
133
|
+
const observe = (element, options = { attributes: true, attributeFilter: ['class', 'hidden'] }) => {
|
|
134
|
+
if (!element || observed.has(element)) return;
|
|
135
|
+
observed.add(element);
|
|
136
|
+
observer.observe(element, options);
|
|
137
|
+
};
|
|
138
|
+
// An overlay's element built since: watched from now on.
|
|
139
|
+
const observeOverlays = () => {
|
|
140
|
+
for (const overlay of overlays) {
|
|
141
|
+
const element = overlay.find();
|
|
142
|
+
if (element && element !== document.body) observe(element);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
observer = new MutationObserver(() => {
|
|
146
|
+
observeOverlays();
|
|
147
|
+
watch.check();
|
|
148
|
+
});
|
|
149
|
+
for (const [id, options] of OVERLAY_HOSTS) {
|
|
150
|
+
observe(id === 'body' ? document.body : document.getElementById(id), options);
|
|
151
|
+
}
|
|
152
|
+
observeOverlays();
|
|
153
|
+
}
|
|
154
|
+
// The sidebar is an overlay on a phone only.
|
|
155
|
+
try {
|
|
156
|
+
window.matchMedia?.(PHONE_QUERY)?.addEventListener?.('change', () => watch.check());
|
|
157
|
+
} catch {}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Leaving Status goes back to the conversation it was opened from, or else
|
|
161
|
+
// to `returnTo`.
|
|
162
|
+
function closeStatusViewDefault({ returnTo = '' } = {}) {
|
|
163
|
+
if (isStatusViewActive()) void toggleStatusView({ returnTo });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function initSessionHistory({ openConversation, ...given } = {}) {
|
|
167
|
+
if (sessionHistory || typeof window === 'undefined' || !window.history) return sessionHistory;
|
|
168
|
+
const closers = { closeStatusView: closeStatusViewDefault, ...given };
|
|
169
|
+
baseTitle = document.title;
|
|
170
|
+
sessionHistory = createSessionHistory({
|
|
171
|
+
history: window.history,
|
|
172
|
+
location: window.location,
|
|
173
|
+
open: (id) => {
|
|
174
|
+
void Promise.resolve(openConversation?.(id, { history: 'none' })).catch(() => {});
|
|
175
|
+
},
|
|
176
|
+
isGone: (id) => goneConversationIds.has(id),
|
|
177
|
+
// Only where the list is an overlay: on a wide screen (a phone turned)
|
|
178
|
+
// openSidebar would un-collapse the desktop sidebar for good.
|
|
179
|
+
onList: () => { if (isPhone()) closers.openSidebar?.(); },
|
|
180
|
+
onLeaveList: () => closers.closeSidebar?.(),
|
|
181
|
+
setTitle: setDocumentTitle,
|
|
182
|
+
schedule: (fn, ms) => window.setTimeout(fn, ms),
|
|
183
|
+
cancel: (handle) => window.clearTimeout(handle),
|
|
184
|
+
});
|
|
185
|
+
window.addEventListener('popstate', () => sessionHistory.onPopState());
|
|
186
|
+
watchOverlays(closers);
|
|
187
|
+
return sessionHistory;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** After the back guard's arming, so the list entry lies above its floor. */
|
|
191
|
+
export function armSessionHistoryOnFirstInteraction(target = window) {
|
|
192
|
+
if (!sessionHistory) return null;
|
|
193
|
+
return armVisitBaseOnFirstInteraction({ target, sessionHistory, isPhone });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** The entry the back guard pushes again above its floor. */
|
|
197
|
+
export function sessionHistoryEntryAgain() {
|
|
198
|
+
return sessionHistory ? sessionHistory.entryAgain() : null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** openConversation's hook: `mode` is 'push' (default), 'replace' or 'none'. */
|
|
202
|
+
export function noteConversationShown(id, mode = 'push') {
|
|
203
|
+
if (String(id || '').trim()) autoOpenHeld = false;
|
|
204
|
+
sessionHistory?.shown(id, { mode: mode || 'push' });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Nothing is open any more (the open one was deleted). */
|
|
208
|
+
export function noteConversationCleared() {
|
|
209
|
+
sessionHistory?.shown(null, { mode: 'replace' });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** A deleted conversation: back and forward step over its entries. */
|
|
213
|
+
export function noteConversationGone(id) {
|
|
214
|
+
goneConversationIds.add(id);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* The relay knows no conversation `id` (deleted elsewhere, a wrong link):
|
|
219
|
+
* its entries are stepped over from now on. True when back or forward led
|
|
220
|
+
* onto it and history steps on past it; false when the caller clears the
|
|
221
|
+
* view (and with it the entry and the address).
|
|
222
|
+
*/
|
|
223
|
+
export function noteConversationMissing(id) {
|
|
224
|
+
goneConversationIds.add(id);
|
|
225
|
+
// Nothing opens by itself after the notice, until the reader opens one.
|
|
226
|
+
autoOpenHeld = true;
|
|
227
|
+
return sessionHistory ? sessionHistory.missing(id) : false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* True after an unknown conversation left the view empty and said so, until
|
|
232
|
+
* the next conversation opens: a waiting question does not open its own in
|
|
233
|
+
* the meantime (ask-user-view.js).
|
|
234
|
+
*/
|
|
235
|
+
export function isConversationAutoOpenHeld() {
|
|
236
|
+
return autoOpenHeld;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The session the history stands on: the one the Inbox or Status was opened
|
|
241
|
+
* over, though the store holds none while either shows.
|
|
242
|
+
*/
|
|
243
|
+
export function sessionHistoryConversation() {
|
|
244
|
+
return sessionHistory?.conversation || '';
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** The open conversation was renamed. */
|
|
248
|
+
export function syncSessionDocumentTitle() {
|
|
249
|
+
if (sessionHistory) setDocumentTitle(sessionHistory.conversation);
|
|
250
|
+
}
|