@oar-sh/oar 0.9.2 → 0.9.4
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/extension.mjs +6 -1
- package/.github/extensions/web-relay/skills/tool-activity.mjs +7 -0
- package/CHANGELOG.md +336 -0
- package/README.md +455 -423
- package/bin/oar.js +25 -10
- package/package.json +11 -5
- package/server/README.md +44 -4
- package/server/claude-worker/claude-remote-relay-tool.mjs +99 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +26 -4
- package/server/claude-worker/claude-session-process.mjs +1073 -69
- package/server/claude-worker/claude-session-worker.mjs +45 -4
- package/server/claude-worker/claude-turn-publisher.mjs +81 -20
- package/server/claude-worker/claude-worker-link-wiring.mjs +9 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +7 -0
- package/server/copilot-worker/copilot-ask-user-tool.mjs +89 -0
- package/server/copilot-worker/copilot-continuation-signals.mjs +55 -0
- package/server/copilot-worker/copilot-question-bridge.mjs +34 -12
- package/server/copilot-worker/copilot-remote-relay-tool.mjs +59 -0
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +113 -14
- package/server/copilot-worker/copilot-sdk-session-process.mjs +2349 -498
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +82 -20
- package/server/cursor-worker/cursor-remote-relay-tool.mjs +65 -0
- package/server/cursor-worker/cursor-turn-runner.mjs +45 -2
- package/server/cursor-worker/sdk-message-normalizer.mjs +25 -0
- package/server/db-schema.mjs +28 -0
- package/server/grok-worker/acp-client.mjs +92 -6
- package/server/grok-worker/grok-turn-runner.mjs +59 -8
- package/server/grok-worker/sdk-message-normalizer.mjs +13 -0
- package/server/mcp/oar-mcp-server.mjs +473 -0
- package/server/migrations/0004-steer-settle-markers.mjs +119 -0
- package/server/migrations/0005-remote-relays.mjs +90 -0
- package/server/public/app/action-confirmations.js +141 -13
- package/server/public/app/api-client.js +125 -2
- package/server/public/app/ask-user-view.js +102 -13
- package/server/public/app/attachments-view.js +37 -2
- package/server/public/app/background-task-timeout-options.mjs +9 -0
- package/server/public/app/background-tasks-view.mjs +33 -18
- package/server/public/app/bootstrap.js +55 -14
- package/server/public/app/composer-control-state.mjs +52 -34
- package/server/public/app/conversation-deep-link.mjs +42 -0
- package/server/public/app/conversation-draft-sync.mjs +187 -0
- package/server/public/app/conversation-list-filter.mjs +86 -0
- package/server/public/app/conversation-provider-indicator.mjs +8 -0
- package/server/public/app/conversation-view.js +1016 -200
- package/server/public/app/emoji-view.js +2 -0
- package/server/public/app/font-scaling.js +4 -37
- package/server/public/app/host-suspend-ui.js +272 -0
- package/server/public/app/journal-view.js +149 -6
- package/server/public/app/mention-autocomplete.mjs +298 -0
- package/server/public/app/model-selector-options.mjs +1 -1
- package/server/public/app/push-settings.js +1 -0
- package/server/public/app/pwa-install.js +8 -3
- package/server/public/app/question-multi-select.mjs +52 -0
- package/server/public/app/relay-prompt-sanitizer.mjs +61 -3
- package/server/public/app/remote-relay-origin-view.mjs +76 -0
- package/server/public/app/remote-relay-shared.mjs +85 -0
- package/server/public/app/remote-relays-settings.js +618 -0
- package/server/public/app/remote-relays-store.mjs +126 -0
- package/server/public/app/settings-modal.js +12 -8
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +37 -1
- package/server/public/app/steered-turn-merge.mjs +100 -0
- package/server/public/app/store.js +126 -15
- package/server/public/app/sync-outbox.mjs +16 -8
- package/server/public/app/thinking-follow.js +260 -0
- package/server/public/app/tool-activity-emoji.mjs +40 -0
- package/server/public/app/transcript-viewport-anchor.mjs +210 -0
- package/server/public/index.html +463 -17
- package/server/public/sw.js +7 -0
- package/server/relay-tools.md +7 -1
- package/server/repositories/message-repository.mjs +68 -18
- package/server/repositories/question-repository.mjs +18 -0
- package/server/repositories/remote-relay-repository.mjs +75 -0
- package/server/repositories/session-repository.mjs +13 -32
- package/server/routes/ask-user-routes.mjs +42 -2
- package/server/routes/messages-routes.mjs +572 -56
- package/server/routes/remote-relay-routes.mjs +133 -0
- package/server/routes/sessions-routes.mjs +378 -84
- package/server/server-runtime.mjs +303 -25
- package/server/services/github-ci-activity-service.mjs +199 -0
- package/server/services/host-suspend-command.mjs +38 -0
- package/server/services/host-suspend-service.mjs +254 -0
- package/server/services/live-turn-picker.mjs +47 -0
- package/server/services/push-dispatch-service.mjs +37 -4
- package/server/services/relay-prompt-sanitizer.mjs +17 -2
- package/server/services/relay-question-context.mjs +46 -0
- package/server/services/remote-relay-caller-context.mjs +341 -0
- package/server/services/remote-relay-client.mjs +332 -0
- package/server/services/remote-relay-dispatcher.mjs +1500 -0
- package/server/services/remote-relay-inbound.mjs +187 -0
- package/server/services/remote-relay-pairing.mjs +189 -0
- package/server/services/remote-relay-registry-service.mjs +581 -0
- package/server/services/sdk-session-import-service.mjs +49 -7
- package/server/services/session-worker-launch-service.mjs +131 -12
- package/server/services/session-worker-registry-service.mjs +75 -0
- package/server/services/session-worker-supervisor-service.mjs +7 -1
- package/server/services/session-worker-websocket-service.mjs +23 -1
- package/server/services/steer-resend.mjs +45 -0
- package/shared/background-task-timeout.mjs +12 -4
- package/shared/control-poller.mjs +4 -1
- package/shared/question-multi-select.mjs +16 -0
- package/shared/relay-stop-failure.mjs +15 -0
- package/shared/remote-relay-contract.mjs +539 -0
- package/shared/remote-relay-mentions.mjs +129 -0
- package/shared/remote-relay-tool-core.mjs +314 -0
- package/shared/slash-command-guard.mjs +76 -0
- package/shared/steer-note.mjs +66 -0
- package/shared/steer-settle-failure.mjs +89 -0
- package/shared/steer-settle-markers.mjs +18 -0
- package/shared/worker-crash-guard.mjs +40 -25
- package/shared/worker-runtime/api-client.mjs +112 -22
- package/shared/worker-runtime/heartbeat.mjs +55 -1
- package/shared/worker-runtime/runner-link-wiring.mjs +44 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +119 -8
- package/server/claude-worker/claude-session-test-harness.mjs +0 -250
- package/server/claude-worker/fixtures/workflow-run-record.json +0 -1
- package/server/copilot-worker/copilot-sdk-test-harness.mjs +0 -382
- package/server/copilot-worker/fixtures/abort-turn.json +0 -588
- package/server/copilot-worker/fixtures/ask-user-turn.json +0 -1154
- package/server/copilot-worker/fixtures/background-timer-continuation.json +0 -445
- package/server/copilot-worker/fixtures/background-timer-turn.json +0 -497
- package/server/copilot-worker/fixtures/happy-turn.json +0 -566
- package/server/copilot-worker/fixtures/quota-turn.json +0 -647
- package/server/copilot-worker/fixtures/reasoning-turn.json +0 -625
- package/server/copilot-worker/fixtures/resume-turn.json +0 -700
- package/server/copilot-worker/fixtures/subagent-turn.json +0 -1378
- package/server/copilot-worker/fixtures/tool-permission-turn.json +0 -1293
- package/server/routes/messages-routes-test-harness.mjs +0 -111
- package/server/services/fixtures/claude-auth-stub.cmd +0 -1
- package/server/services/fixtures/claude-auth-stub.mjs +0 -76
- package/server/services/fixtures/claude-auth-stub.sh +0 -4
- package/server/services/fixtures/cli-install-stub.cmd +0 -1
- package/server/services/fixtures/cli-install-stub.mjs +0 -98
- package/server/services/fixtures/cli-install-stub.sh +0 -4
- package/server/services/fixtures/grok-stub.cmd +0 -1
- package/server/services/fixtures/grok-stub.mjs +0 -89
- package/server/services/fixtures/grok-stub.sh +0 -4
- package/shared/fixtures/copilot-catalog-raw-2026-09-07.json +0 -2363
|
@@ -10,11 +10,23 @@ import { loadRelayQuestions as loadRelayQuestionsApi, answerRelayQuestion, answe
|
|
|
10
10
|
import { enqueueOutboxRequest, registerOutboxSync } from './sync-outbox.mjs';
|
|
11
11
|
import { renderLinkedPlainText } from './router.js';
|
|
12
12
|
import { schemaFieldsFromQuestion } from './question-schema-view.mjs';
|
|
13
|
+
import { composeMultiSelectAnswer, isMultiSelectQuestion, offersMultiSelectToggle } from './question-multi-select.mjs';
|
|
13
14
|
import { isChatInteractionHeld } from './selection-guard.mjs';
|
|
15
|
+
import { questionCardHeadLabel } from './conversation-provider-indicator.mjs';
|
|
14
16
|
|
|
15
17
|
let relayQuestionRenderHash = '';
|
|
16
18
|
let renderedRelayQuestionIds = new Set();
|
|
17
19
|
const relayQuestionStructuredDrafts = new Map();
|
|
20
|
+
// Ticked choices of a multi-select card (question id → Set of labels), kept
|
|
21
|
+
// across re-renders until the card is answered.
|
|
22
|
+
const relayQuestionMultiDrafts = new Map();
|
|
23
|
+
// The "Select several" switch position per card (question id → boolean) on
|
|
24
|
+
// cards that offer it (Copilot); absent = the provider's flag decides.
|
|
25
|
+
const relayQuestionMultiMode = new Map();
|
|
26
|
+
|
|
27
|
+
function multiModeFor(question) {
|
|
28
|
+
return question ? relayQuestionMultiMode.get(question.id) : undefined;
|
|
29
|
+
}
|
|
18
30
|
const RELAY_QUESTION_AUTO_SCROLL_THRESHOLD_PX = 80;
|
|
19
31
|
|
|
20
32
|
function distanceFromBottom(el) {
|
|
@@ -75,6 +87,8 @@ export function upsertRelayQuestion(question) {
|
|
|
75
87
|
relayQuestions.set(question.id, question);
|
|
76
88
|
updatePendingQuestionBanner();
|
|
77
89
|
window.renderConvList?.();
|
|
90
|
+
// A question opening or settling flips the composer's steering hold.
|
|
91
|
+
window.syncComposerButtonState?.();
|
|
78
92
|
renderRelayQuestions();
|
|
79
93
|
}
|
|
80
94
|
|
|
@@ -98,10 +112,19 @@ export async function loadRelayQuestions(conversationId) {
|
|
|
98
112
|
relayQuestionStructuredDrafts.delete(questionId);
|
|
99
113
|
}
|
|
100
114
|
}
|
|
115
|
+
// Multi-select ticks and the "Select several" switch position die with the
|
|
116
|
+
// card, whoever settled it (this device, another one, a timeout).
|
|
117
|
+
for (const draftMap of [relayQuestionMultiDrafts, relayQuestionMultiMode]) {
|
|
118
|
+
for (const questionId of draftMap.keys()) {
|
|
119
|
+
const question = next.get(questionId);
|
|
120
|
+
if (!question || question.status !== 'pending') draftMap.delete(questionId);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
101
123
|
relayQuestions.clear();
|
|
102
124
|
for (const [id, q] of next.entries()) relayQuestions.set(id, q);
|
|
103
125
|
updatePendingQuestionBanner();
|
|
104
126
|
window.renderConvList?.();
|
|
127
|
+
window.syncComposerButtonState?.();
|
|
105
128
|
|
|
106
129
|
if (!conversationId && pending.length) {
|
|
107
130
|
const latest = pending
|
|
@@ -212,6 +235,7 @@ export function renderRelayQuestions() {
|
|
|
212
235
|
answeredAt: q.answeredAt || '',
|
|
213
236
|
createdAt: q.createdAt || '',
|
|
214
237
|
choices: Array.isArray(q.choices) ? q.choices : [],
|
|
238
|
+
multiSelect: isMultiSelectQuestion(q, multiModeFor(q)),
|
|
215
239
|
schema: q.requestSchema || null,
|
|
216
240
|
}))
|
|
217
241
|
);
|
|
@@ -249,24 +273,41 @@ export function renderRelayQuestions() {
|
|
|
249
273
|
interactiveHtml = renderMultiFieldForm(question);
|
|
250
274
|
} else {
|
|
251
275
|
const choices = Array.isArray(question.choices) ? question.choices : [];
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
276
|
+
// A multi-select card (the provider said several may be picked) offers
|
|
277
|
+
// checkmarks and ONE Reply; ticking never answers by itself.
|
|
278
|
+
const multiSelect = isMultiSelectQuestion(question, multiModeFor(question));
|
|
279
|
+
const ticked = relayQuestionMultiDrafts.get(question.id) || new Set();
|
|
280
|
+
// Copilot cards: a switch between one-click answers and checkmarks,
|
|
281
|
+
// pre-set from the model's flag or the question's wording.
|
|
282
|
+
const toggleHtml = question.status === 'pending' && offersMultiSelectToggle(question)
|
|
283
|
+
? `<label class="relay-question-multi-toggle"><input type="checkbox" data-relay-focus-key="multi-toggle"${multiSelect ? ' checked' : ''} onchange="setRelayQuestionMultiMode('${question.id}', this.checked)"> Select several</label>`
|
|
260
284
|
: '';
|
|
285
|
+
let choiceHtml = '';
|
|
286
|
+
if (choices.length && question.status === 'pending' && multiSelect) {
|
|
287
|
+
choiceHtml = `<div class="relay-question-choices relay-question-multi" id="relay-question-multi-${question.id}">${
|
|
288
|
+
choices.map((choice) => `<label class="relay-question-choice relay-question-choice-check"><input type="checkbox" data-choice-value="${escHtml(choice)}" data-relay-focus-key="multi-${escHtml(choice)}"${ticked.has(choice) ? ' checked' : ''} onchange="onRelayQuestionMultiSelectChange('${question.id}', this.dataset.choiceValue, this.checked)"> ${escHtml(choice)}</label>`).join('')
|
|
289
|
+
}</div>`;
|
|
290
|
+
} else if (choices.length && question.status === 'pending') {
|
|
291
|
+
choiceHtml = `<div class="relay-question-choices">${
|
|
292
|
+
choices.map((choice) => `<button class="relay-question-choice" data-choice="${escHtml(choice)}" data-relay-focus-key="choice-${escHtml(choice)}" onclick="submitRelayQuestionChoice('${question.id}', this.dataset.choice)">${escHtml(choice)}</button>`).join('')
|
|
293
|
+
}</div>`;
|
|
294
|
+
} else if (choices.length) {
|
|
295
|
+
choiceHtml = `<div class="relay-question-choices">${
|
|
296
|
+
choices.map((choice) => `<button class="relay-question-choice" disabled>${escHtml(choice)}</button>`).join('')
|
|
297
|
+
}</div>`;
|
|
298
|
+
}
|
|
261
299
|
const showReplyControls = question.status === 'pending';
|
|
262
300
|
const draftText = String(relayQuestionDrafts.get(question.id) || '');
|
|
301
|
+
const replyPlaceholder = multiSelect
|
|
302
|
+
? 'Add another answer or a note (optional)… (Ctrl/Cmd+Enter to send)'
|
|
303
|
+
: 'Type a reply… (Ctrl/Cmd+Enter to send)';
|
|
263
304
|
const replyHtml = showReplyControls
|
|
264
305
|
? `<div class="relay-question-reply">
|
|
265
|
-
<textarea id="relay-question-input-${question.id}" placeholder="
|
|
266
|
-
<button class="relay-question-submit" onclick="submitRelayQuestionAnswer('${question.id}')"
|
|
306
|
+
<textarea id="relay-question-input-${question.id}" placeholder="${replyPlaceholder}" onkeydown="handleRelayQuestionKey(event, '${question.id}')" oninput="onRelayQuestionDraftInput('${question.id}', this.value)">${escHtml(draftText)}</textarea>
|
|
307
|
+
<button class="relay-question-submit" onclick="${multiSelect ? 'submitRelayQuestionMultiSelect' : 'submitRelayQuestionAnswer'}('${question.id}')">${multiSelect ? 'Reply with selection' : 'Reply'}</button>
|
|
267
308
|
</div>`
|
|
268
309
|
: '';
|
|
269
|
-
interactiveHtml = `${choiceHtml}${replyHtml}`;
|
|
310
|
+
interactiveHtml = `${choiceHtml}${toggleHtml}${replyHtml}`;
|
|
270
311
|
}
|
|
271
312
|
|
|
272
313
|
const answeredHtml = question.status === 'answered' && question.answer
|
|
@@ -278,7 +319,7 @@ export function renderRelayQuestions() {
|
|
|
278
319
|
|
|
279
320
|
wrapper.innerHTML = `
|
|
280
321
|
<div class="relay-question-card${question.status === 'timed_out' ? ' relay-question-timed-out' : ''}">
|
|
281
|
-
<div class="relay-question-head"
|
|
322
|
+
<div class="relay-question-head">${questionCardHeadLabel(conversations[question.conversationId])}${modeTag} · ${fmtDate(question.createdAt)}</div>
|
|
282
323
|
<div class="relay-question-body">${renderLinkedPlainText(question.prompt || '')}</div>
|
|
283
324
|
${contextHtml}
|
|
284
325
|
${answeredHtml}
|
|
@@ -467,6 +508,7 @@ export async function submitRelayStructuredAnswer(questionId) {
|
|
|
467
508
|
if (r.question) relayQuestions.set(questionId, r.question);
|
|
468
509
|
updatePendingQuestionBanner();
|
|
469
510
|
window.renderConvList?.();
|
|
511
|
+
window.syncComposerButtonState?.();
|
|
470
512
|
renderRelayQuestions();
|
|
471
513
|
window.showTransientRelayNotice?.(`✅ Answer received · Agent continuing…`, 7000);
|
|
472
514
|
} catch (e) {
|
|
@@ -480,6 +522,44 @@ export async function submitRelayQuestionChoice(questionId, choice) {
|
|
|
480
522
|
await submitRelayQuestionAnswer(questionId, choice);
|
|
481
523
|
}
|
|
482
524
|
|
|
525
|
+
/** Ticked labels of a multi-select card, in the card's own order. */
|
|
526
|
+
function tickedMultiSelectLabels(questionId) {
|
|
527
|
+
const container = document.getElementById(`relay-question-multi-${questionId}`);
|
|
528
|
+
if (!container) return [...(relayQuestionMultiDrafts.get(questionId) || [])];
|
|
529
|
+
return Array.from(container.querySelectorAll('input[type="checkbox"]:checked'))
|
|
530
|
+
.map((el) => String(el.getAttribute('data-choice-value') || ''));
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* The "Select several" switch: flip the card between one-click answers and
|
|
535
|
+
* checkmarks. Ticks made so far are kept, and the card re-renders in place.
|
|
536
|
+
*/
|
|
537
|
+
export function setRelayQuestionMultiMode(questionId, enabled) {
|
|
538
|
+
const id = String(questionId || '');
|
|
539
|
+
if (!id) return;
|
|
540
|
+
relayQuestionMultiMode.set(id, enabled === true);
|
|
541
|
+
renderRelayQuestions();
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
export function onRelayQuestionMultiSelectChange(questionId, label, checked) {
|
|
545
|
+
const id = String(questionId || '');
|
|
546
|
+
const set = relayQuestionMultiDrafts.get(id) || new Set();
|
|
547
|
+
if (checked) set.add(String(label || ''));
|
|
548
|
+
else set.delete(String(label || ''));
|
|
549
|
+
relayQuestionMultiDrafts.set(id, set);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Answer a multi-select card: every ticked choice, plus whatever was typed as
|
|
554
|
+
* an extra answer, joined the way the provider's own UI would ("A, C").
|
|
555
|
+
*/
|
|
556
|
+
export async function submitRelayQuestionMultiSelect(questionId) {
|
|
557
|
+
const input = document.getElementById(`relay-question-input-${questionId}`);
|
|
558
|
+
const answer = composeMultiSelectAnswer(tickedMultiSelectLabels(questionId), input?.value || '');
|
|
559
|
+
if (!answer) return;
|
|
560
|
+
await submitRelayQuestionAnswer(questionId, answer);
|
|
561
|
+
}
|
|
562
|
+
|
|
483
563
|
// Offline fallback: park the answer in the durable outbox so Background Sync
|
|
484
564
|
// (or the next page load) delivers it. A replay against an already-answered
|
|
485
565
|
// or expired question gets a 4xx and is dropped, so this cannot double-answer.
|
|
@@ -499,7 +579,13 @@ async function enqueueRelayAnswerForSync(questionId, body) {
|
|
|
499
579
|
|
|
500
580
|
export async function submitRelayQuestionAnswer(questionId, presetAnswer = null) {
|
|
501
581
|
const input = document.getElementById(`relay-question-input-${questionId}`);
|
|
502
|
-
|
|
582
|
+
// Ctrl/Cmd+Enter in a multi-select card's text field sends the ticked
|
|
583
|
+
// choices along with the typed text, exactly like its Reply button.
|
|
584
|
+
const pendingQuestion = relayQuestions.get(questionId) || null;
|
|
585
|
+
const composed = presetAnswer == null && isMultiSelectQuestion(pendingQuestion, multiModeFor(pendingQuestion))
|
|
586
|
+
? composeMultiSelectAnswer(tickedMultiSelectLabels(questionId), input?.value || '')
|
|
587
|
+
: null;
|
|
588
|
+
const answer = String(presetAnswer != null ? presetAnswer : (composed ?? (input?.value || ''))).trim();
|
|
503
589
|
if (!answer) return;
|
|
504
590
|
|
|
505
591
|
const card = document.querySelector(`.relay-question-container[data-question-id="${questionId}"]`);
|
|
@@ -522,9 +608,12 @@ export async function submitRelayQuestionAnswer(questionId, presetAnswer = null)
|
|
|
522
608
|
}
|
|
523
609
|
relayQuestionDrafts.delete(questionId);
|
|
524
610
|
relayQuestionStructuredDrafts.delete(questionId);
|
|
611
|
+
relayQuestionMultiDrafts.delete(questionId);
|
|
612
|
+
relayQuestionMultiMode.delete(questionId);
|
|
525
613
|
relayQuestions.set(questionId, r.question);
|
|
526
614
|
updatePendingQuestionBanner();
|
|
527
615
|
window.renderConvList?.();
|
|
616
|
+
window.syncComposerButtonState?.();
|
|
528
617
|
renderRelayQuestions();
|
|
529
618
|
window.showTransientRelayNotice?.(`✅ Answer received: ${answer} · Agent continuing…`, 7000);
|
|
530
619
|
} catch (e) {
|
|
@@ -221,7 +221,7 @@ export function renderAttachmentPreview() {
|
|
|
221
221
|
if (!selectedAttachments.length) {
|
|
222
222
|
el.innerHTML = '';
|
|
223
223
|
el.classList.remove('visible');
|
|
224
|
-
window.
|
|
224
|
+
window.syncComposerButtonState?.();
|
|
225
225
|
syncComposerAttachmentWarning();
|
|
226
226
|
return;
|
|
227
227
|
}
|
|
@@ -251,7 +251,7 @@ export function renderAttachmentPreview() {
|
|
|
251
251
|
`;
|
|
252
252
|
}).join('');
|
|
253
253
|
el.classList.add('visible');
|
|
254
|
-
window.
|
|
254
|
+
window.syncComposerButtonState?.();
|
|
255
255
|
syncComposerAttachmentWarning();
|
|
256
256
|
}
|
|
257
257
|
|
|
@@ -293,6 +293,41 @@ export function clearAttachments() {
|
|
|
293
293
|
renderAttachmentPreview();
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
+
function composerAttachmentSha(attachment) {
|
|
297
|
+
return String(attachment?.sha256 || attachment?.uploaded?.sha256 || '').trim().toLowerCase();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Removes the composer entries a send took with it and keeps anything added
|
|
302
|
+
* after that snapshot, uploading or not. Matches by identity first, then by
|
|
303
|
+
* content hash — switching away and back mid-send rebuilds the composer from
|
|
304
|
+
* the draft cache as new objects — and removes one entry per sent attachment,
|
|
305
|
+
* so a genuine second copy survives. Persisting what remains is the caller's
|
|
306
|
+
* job.
|
|
307
|
+
*/
|
|
308
|
+
export function removeComposerAttachments(sent = []) {
|
|
309
|
+
const pending = (Array.isArray(sent) ? sent : []).filter(Boolean);
|
|
310
|
+
if (!pending.length) return;
|
|
311
|
+
const removed = new Set();
|
|
312
|
+
const unmatched = [];
|
|
313
|
+
for (const att of pending) {
|
|
314
|
+
if (selectedAttachments.includes(att) && !removed.has(att)) removed.add(att);
|
|
315
|
+
else unmatched.push(att);
|
|
316
|
+
}
|
|
317
|
+
for (const att of unmatched) {
|
|
318
|
+
const sha = composerAttachmentSha(att);
|
|
319
|
+
if (!sha) continue;
|
|
320
|
+
const match = selectedAttachments.find((candidate) => !removed.has(candidate) && composerAttachmentSha(candidate) === sha);
|
|
321
|
+
if (match) removed.add(match);
|
|
322
|
+
}
|
|
323
|
+
if (!removed.size) return;
|
|
324
|
+
const kept = selectedAttachments.filter((att) => !removed.has(att));
|
|
325
|
+
for (const att of removed) releaseAttachmentPreviewUrl(att);
|
|
326
|
+
selectedAttachments.length = 0;
|
|
327
|
+
selectedAttachments.push(...kept);
|
|
328
|
+
renderAttachmentPreview();
|
|
329
|
+
}
|
|
330
|
+
|
|
296
331
|
/**
|
|
297
332
|
* Replaces composer attachments wholesale, used when hydrating a conversation's
|
|
298
333
|
* cached draft attachments. Object URLs from the outgoing set are released.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Mirrors shared/background-task-timeout.mjs, which the browser cannot import
|
|
2
|
+
// (only server/public is served). shared/background-task-timeout.test.mjs
|
|
3
|
+
// asserts the two stay identical.
|
|
4
|
+
//
|
|
5
|
+
// The settings slider shows this until the relay's own value loads, and keeps
|
|
6
|
+
// showing it if that request fails, so it must be the default the relay
|
|
7
|
+
// enforces: 4 hours. 0 means no limit only when chosen explicitly.
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_BACKGROUND_TASK_TIMEOUT_MINUTES = 240;
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// `backgroundTasks` on reload; hidden whenever the set is empty.
|
|
6
6
|
|
|
7
7
|
import { apiFetch } from './api-client.js';
|
|
8
|
+
import { humanizeModelLabel } from './model-selector-options.mjs';
|
|
9
|
+
import { prefixToolActivityEmoji } from './tool-activity-emoji.mjs';
|
|
8
10
|
import {
|
|
9
11
|
getPreviewsForConversation,
|
|
10
12
|
renderPreviewRowsInto,
|
|
@@ -69,11 +71,26 @@ function taskKindLabel(task) {
|
|
|
69
71
|
return label === 'Subagent' && subagentType ? subagentType : label;
|
|
70
72
|
}
|
|
71
73
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
74
|
+
// "claude-opus-5-5" → "Opus 5.5". A bare alias ("opus") is what a spawn pin
|
|
75
|
+
// carries until the subagent's first answer names the real model.
|
|
76
|
+
export function formatTaskModelLabel(model) {
|
|
77
|
+
const value = String(model || '').trim();
|
|
78
|
+
if (!value) return '';
|
|
79
|
+
if (/^claude-/i.test(value)) return humanizeModelLabel(value);
|
|
80
|
+
if (/^[a-z]+(\[[^\]]+\])?$/i.test(value)) return value.charAt(0).toUpperCase() + value.slice(1);
|
|
81
|
+
return humanizeModelLabel(value);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Detail line: the live tool call when the worker saw one ("🪓 Tool
|
|
85
|
+
// (PowerShell): cd …", cropped to the row by CSS), else the SDK's progress
|
|
86
|
+
// summary, else the bare tool name.
|
|
87
|
+
export function taskActivityText(task) {
|
|
88
|
+
const toolCall = String(task?.lastToolCall || '').trim();
|
|
89
|
+
if (toolCall) return { text: prefixToolActivityEmoji(toolCall), toolCall: true };
|
|
90
|
+
const summary = String(task?.summary || '').trim();
|
|
91
|
+
if (summary) return { text: summary, toolCall: false };
|
|
92
|
+
const lastToolName = String(task?.lastToolName || '').trim();
|
|
93
|
+
return { text: lastToolName ? `using ${lastToolName}` : '', toolCall: false };
|
|
77
94
|
}
|
|
78
95
|
|
|
79
96
|
// Compact token counts: "812 tok", "62.2k tok", "1.2M tok". Exported for the
|
|
@@ -180,7 +197,7 @@ function buildWorkflowAgentRow(agent) {
|
|
|
180
197
|
const activityText = state === 'running' && lastToolName ? `— using ${lastToolName}` : '';
|
|
181
198
|
|
|
182
199
|
const meta = [];
|
|
183
|
-
const model =
|
|
200
|
+
const model = formatTaskModelLabel(agent.model);
|
|
184
201
|
if (model) meta.push(model);
|
|
185
202
|
const tokens = formatTaskTokens(agent.tokens);
|
|
186
203
|
if (tokens) meta.push(tokens);
|
|
@@ -364,16 +381,11 @@ function renderTaskRow(conversationId, task) {
|
|
|
364
381
|
const key = `${conversationId}:${task.taskId}`;
|
|
365
382
|
const stopping = stopsInFlight.has(key);
|
|
366
383
|
const elapsed = formatElapsed(task.startedAt);
|
|
367
|
-
//
|
|
368
|
-
//
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
const
|
|
372
|
-
const lastToolName = String(task.lastToolName || '').trim();
|
|
373
|
-
const detail = [
|
|
374
|
-
summary || (lastToolName ? `using ${lastToolName}` : ''),
|
|
375
|
-
formatTaskModel(task),
|
|
376
|
-
].filter(Boolean).join(' · ');
|
|
384
|
+
// Token usage and the model render on the row's right side instead of
|
|
385
|
+
// riding the detail line, so a long tool call can never crowd them out.
|
|
386
|
+
const activity = taskActivityText(task);
|
|
387
|
+
const modelLabel = formatTaskModelLabel(task.model);
|
|
388
|
+
const modelTitle = task.modelInherited === true ? `${task.model} (inherited from the session)` : task.model;
|
|
377
389
|
const tokens = formatTaskTokens(task.totalTokens);
|
|
378
390
|
// Workflow rows with a digest get a fold chevron plus an (initially empty)
|
|
379
391
|
// tree container right below the row; renderBackgroundTasksPanel fills the
|
|
@@ -392,10 +404,13 @@ function renderTaskRow(conversationId, task) {
|
|
|
392
404
|
${foldButton}<span class="bg-task-icon" title="${escHtml(task.taskType || 'task')}">${taskIcon(task.taskType)}</span>
|
|
393
405
|
<span class="bg-task-main">
|
|
394
406
|
<span class="bg-task-desc">${escHtml(task.description || task.taskId)}</span>
|
|
395
|
-
${
|
|
407
|
+
${activity.text ? `<span class="bg-task-detail${activity.toolCall ? ' bg-task-toolcall' : ''}" title="${escHtml(activity.text)}">${escHtml(activity.text)}</span>` : ''}
|
|
396
408
|
</span>
|
|
397
409
|
<span class="bg-task-side">
|
|
398
|
-
<span class="bg-task-
|
|
410
|
+
<span class="bg-task-side-head">
|
|
411
|
+
${modelLabel ? `<span class="bg-task-model" title="${escHtml(modelTitle)}">${escHtml(modelLabel)}</span>` : ''}
|
|
412
|
+
<span class="bg-task-badge">${escHtml(taskKindLabel(task))}</span>
|
|
413
|
+
</span>
|
|
399
414
|
<span class="bg-task-side-status">
|
|
400
415
|
${tokens ? `<span class="bg-task-tokens">${escHtml(tokens)}</span>` : ''}
|
|
401
416
|
<span class="bg-task-elapsed">${escHtml(elapsed)}</span>
|
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
imageEditTarget,
|
|
52
52
|
hasPendingUserMessageForConversation,
|
|
53
53
|
} from './store.js';
|
|
54
|
+
import { createTranscriptResizeKeeper } from './transcript-viewport-anchor.mjs';
|
|
54
55
|
import {
|
|
55
56
|
verifyExistingSession,
|
|
56
57
|
verifyToken,
|
|
@@ -76,7 +77,7 @@ import {
|
|
|
76
77
|
reportSharedViewerPresence,
|
|
77
78
|
setNetworkRequestsEnabled,
|
|
78
79
|
} from './api-client.js';
|
|
79
|
-
import { loadConversations, refreshConversations, openConversation, renderConvList, applyLoadedConversationState, initConversationListLazyLoading } from './journal-view.js';
|
|
80
|
+
import { loadConversations, refreshConversations, openConversation, renderConvList, applyLoadedConversationState, initConversationListLazyLoading, initConversationFilter } from './journal-view.js';
|
|
80
81
|
import {
|
|
81
82
|
newConversation,
|
|
82
83
|
confirmNewConversationModel,
|
|
@@ -87,7 +88,7 @@ import {
|
|
|
87
88
|
loadRelayQuestions,
|
|
88
89
|
getPendingQuestionCountsByConversation,
|
|
89
90
|
} from './ask-user-view.js';
|
|
90
|
-
import { openPendingQuestionFromBanner, submitRelayQuestionChoice, submitRelayQuestionAnswer, submitRelayStructuredAnswer, onRelayQuestionDraftInput, handleRelayQuestionKey } from './ask-user-view.js';
|
|
91
|
+
import { openPendingQuestionFromBanner, submitRelayQuestionChoice, submitRelayQuestionAnswer, submitRelayQuestionMultiSelect, submitRelayStructuredAnswer, onRelayQuestionDraftInput, onRelayQuestionMultiSelectChange, setRelayQuestionMultiMode, handleRelayQuestionKey } from './ask-user-view.js';
|
|
91
92
|
import { loadRelayBoards, submitRelayBoardAction } from './relay-board-view.js';
|
|
92
93
|
import {
|
|
93
94
|
restoreInFlightThinking,
|
|
@@ -99,7 +100,10 @@ import {
|
|
|
99
100
|
updateComposerSlashMenu,
|
|
100
101
|
getConversationLoadedMessageCount,
|
|
101
102
|
loadOlderConversationMessages,
|
|
102
|
-
|
|
103
|
+
syncComposerButtonState,
|
|
104
|
+
syncCancellableSteerButtons,
|
|
105
|
+
syncComposerAfterUserEdit,
|
|
106
|
+
isConversationDraftHeldBySend,
|
|
103
107
|
persistComposerAttachments,
|
|
104
108
|
flushConversationDraft,
|
|
105
109
|
initConversationHistoryLazyLoading,
|
|
@@ -115,6 +119,14 @@ import { loadRepoBrowserTree, openRepoBrowser, closeRepoBrowser, setRepoBrowserS
|
|
|
115
119
|
import { handleAttachmentInput, retryAttachmentUpload, openAnnotateEditorForPending, openAnnotateEditorForPreview, handleComposerPaste, handleComposerDrop, refreshComposerAttachmentWarning, removeAttachment, clearAttachments, openUploadedAttachmentViewer, setFilePreviewMode, toggleFilePreviewHtml, copyFilePreviewImage, closeFilePreview, goBackFilePreview, openWorkspaceFilePreview, openWorkspaceFilePreviewFromRepo, setRepoBrowserRoot, setRepoBrowserViewMode, toggleRepoBrowserHidden, toggleRepoBrowserHeavy, refreshRepoBrowser, focusRepoTree, setRepoCurrentPath, confirmRepoBrowserCwdPick } from './attachments-view.js';
|
|
116
120
|
import { initEmojiPicker, toggleEmojiPicker } from './emoji-view.js';
|
|
117
121
|
import { initSlashAutocomplete } from './slash-autocomplete.mjs';
|
|
122
|
+
import { initMentionAutocomplete } from './mention-autocomplete.mjs';
|
|
123
|
+
import { consumeConversationDeepLink } from './conversation-deep-link.mjs';
|
|
124
|
+
import {
|
|
125
|
+
addRemoteRelayFromForm,
|
|
126
|
+
saveRemoteRelayPublicUrl,
|
|
127
|
+
showRemoteRelayTokenField,
|
|
128
|
+
toggleRemoteRelayInbound,
|
|
129
|
+
} from './remote-relays-settings.js';
|
|
118
130
|
import { dataTransferHasFiles } from './composer-paste.mjs';
|
|
119
131
|
import { isReasoningOffUnsupported, reasoningEffortOptionLabel, reasoningEffortOptionTitle } from './reasoning-effort-labels.mjs';
|
|
120
132
|
import {
|
|
@@ -297,7 +309,9 @@ import {
|
|
|
297
309
|
confirmEmptyQueue,
|
|
298
310
|
openSuspendHostConfirmation,
|
|
299
311
|
confirmSuspendHost,
|
|
312
|
+
cancelQueuedHostSuspend,
|
|
300
313
|
} from './action-confirmations.js';
|
|
314
|
+
import { applyPendingActionsFromStatus, initHostSuspendUi } from './host-suspend-ui.js';
|
|
301
315
|
import {
|
|
302
316
|
startClaudeRelogin,
|
|
303
317
|
submitClaudeLoginCodeFromInput,
|
|
@@ -694,13 +708,10 @@ function initPushNotificationClientHooks() {
|
|
|
694
708
|
});
|
|
695
709
|
}
|
|
696
710
|
|
|
711
|
+
// ?push_conv=<id> (a notification tap) and ?conv=<id> (another relay's
|
|
712
|
+
// provenance badge links here) both open that conversation once.
|
|
697
713
|
function consumePushConversationDeepLink() {
|
|
698
|
-
|
|
699
|
-
const conversationId = String(url.searchParams.get('push_conv') || '').trim();
|
|
700
|
-
if (!conversationId) return '';
|
|
701
|
-
url.searchParams.delete('push_conv');
|
|
702
|
-
history.replaceState(null, document.title, `${url.pathname}${url.search}${url.hash}`);
|
|
703
|
-
return conversationId;
|
|
714
|
+
return consumeConversationDeepLink();
|
|
704
715
|
}
|
|
705
716
|
|
|
706
717
|
function handleForegroundTransition(reason, { immediate = false } = {}) {
|
|
@@ -1139,7 +1150,7 @@ function syncModelMetadataBlocker(message = '') {
|
|
|
1139
1150
|
reasoningSelect.disabled = blocked;
|
|
1140
1151
|
reasoningSelect.title = blocked ? 'Reasoning metadata unavailable' : 'Reasoning effort';
|
|
1141
1152
|
}
|
|
1142
|
-
window.
|
|
1153
|
+
window.syncComposerButtonState?.();
|
|
1143
1154
|
}
|
|
1144
1155
|
|
|
1145
1156
|
function currentConversationHasMessages() {
|
|
@@ -3238,8 +3249,13 @@ async function refreshSessionWorkerStatus() {
|
|
|
3238
3249
|
const status = await refreshWorkspaceRootHints();
|
|
3239
3250
|
if (!status) return;
|
|
3240
3251
|
syncQueueStatusMenuEntry(status);
|
|
3252
|
+
applyPendingActionsFromStatus(status);
|
|
3241
3253
|
if (setSessionWorkerStatesFromStatusPayload(status.sessionWorker)) {
|
|
3242
3254
|
renderConvList();
|
|
3255
|
+
// A worker steering snapshot may have changed; the composer's Steer
|
|
3256
|
+
// button (gate, hold) and the pushed rows' Cancel controls derive from it.
|
|
3257
|
+
syncComposerButtonState();
|
|
3258
|
+
syncCancellableSteerButtons();
|
|
3243
3259
|
}
|
|
3244
3260
|
updateCliStatus();
|
|
3245
3261
|
const nextWorkerState = currentSdkSessionId ? getSessionWorkerState(currentSdkSessionId) : null;
|
|
@@ -3311,9 +3327,18 @@ function startLiveConversationPolling() {
|
|
|
3311
3327
|
}, 900);
|
|
3312
3328
|
}
|
|
3313
3329
|
|
|
3330
|
+
const transcriptResizeKeeper = createTranscriptResizeKeeper({
|
|
3331
|
+
getElement: () => document.getElementById('messages'),
|
|
3332
|
+
});
|
|
3333
|
+
|
|
3314
3334
|
function setupViewportTracking() {
|
|
3315
3335
|
syncViewportMetrics();
|
|
3316
|
-
|
|
3336
|
+
// After the metrics: --app-height decides the transcript's height, and the
|
|
3337
|
+
// keeper's restore reads the post-resize layout.
|
|
3338
|
+
const update = () => {
|
|
3339
|
+
syncViewportMetrics();
|
|
3340
|
+
transcriptResizeKeeper.handleResize();
|
|
3341
|
+
};
|
|
3317
3342
|
window.addEventListener('resize', update, { passive: true });
|
|
3318
3343
|
window.addEventListener('orientationchange', update, { passive: true });
|
|
3319
3344
|
if (window.visualViewport) {
|
|
@@ -3352,7 +3377,7 @@ function setupViewportTracking() {
|
|
|
3352
3377
|
if (input && input.dataset.viewportBound !== '1') {
|
|
3353
3378
|
input.dataset.viewportBound = '1';
|
|
3354
3379
|
input.addEventListener('input', () => {
|
|
3355
|
-
|
|
3380
|
+
syncComposerAfterUserEdit();
|
|
3356
3381
|
}, { passive: true });
|
|
3357
3382
|
input.addEventListener('focus', () => {
|
|
3358
3383
|
document.body.classList.add('keyboard-open');
|
|
@@ -3427,6 +3452,7 @@ function initMessageScrollPersistence() {
|
|
|
3427
3452
|
if (!el || el.dataset.scrollPersistenceBound === '1') return;
|
|
3428
3453
|
el.dataset.scrollPersistenceBound = '1';
|
|
3429
3454
|
el.addEventListener('scroll', () => {
|
|
3455
|
+
transcriptResizeKeeper.recordScroll();
|
|
3430
3456
|
const convId = String(currentConvId || '').trim();
|
|
3431
3457
|
if (!convId) return;
|
|
3432
3458
|
saveConversationScrollTop(convId, el.scrollTop);
|
|
@@ -3932,6 +3958,7 @@ async function initApp() {
|
|
|
3932
3958
|
syncQueueStatusMenuEntry();
|
|
3933
3959
|
if (!sharedMode) {
|
|
3934
3960
|
syncSuspendHostVisibility();
|
|
3961
|
+
initHostSuspendUi();
|
|
3935
3962
|
}
|
|
3936
3963
|
setupViewportTracking();
|
|
3937
3964
|
bindChatSelectionGuard();
|
|
@@ -3944,7 +3971,11 @@ async function initApp() {
|
|
|
3944
3971
|
// The direct flush below can be killed mid-flight by the browser; the
|
|
3945
3972
|
// queued copy survives and is replayed by Background Sync. If the direct
|
|
3946
3973
|
// write wins, the replay hits a draft version conflict and is dropped.
|
|
3947
|
-
if (!appSharedMode)
|
|
3974
|
+
if (!appSharedMode) {
|
|
3975
|
+
void enqueueDraftFlushForBackgroundSync(currentConvId, {
|
|
3976
|
+
afterPendingSend: isConversationDraftHeldBySend(currentConvId),
|
|
3977
|
+
});
|
|
3978
|
+
}
|
|
3948
3979
|
void flushConversationDraft(currentConvId);
|
|
3949
3980
|
closeTmuxInspectorView();
|
|
3950
3981
|
});
|
|
@@ -4163,7 +4194,9 @@ async function initApp() {
|
|
|
4163
4194
|
initChatTitleCopy();
|
|
4164
4195
|
initEmojiPicker();
|
|
4165
4196
|
initSlashAutocomplete();
|
|
4197
|
+
initMentionAutocomplete();
|
|
4166
4198
|
initConversationListLazyLoading();
|
|
4199
|
+
initConversationFilter();
|
|
4167
4200
|
initConversationHistoryLazyLoading();
|
|
4168
4201
|
initBubbleActionHandlers();
|
|
4169
4202
|
initMessageScrollPersistence();
|
|
@@ -4294,6 +4327,10 @@ window.togglePushOnThisDevice = togglePushOnThisDevice;
|
|
|
4294
4327
|
window.updatePushPreferencesFromControls = updatePushPreferencesFromControls;
|
|
4295
4328
|
window.openSettingsModal = openSettingsModal;
|
|
4296
4329
|
window.closeSettingsModal = closeSettingsModal;
|
|
4330
|
+
window.addRemoteRelayFromForm = addRemoteRelayFromForm;
|
|
4331
|
+
window.showRemoteRelayTokenField = showRemoteRelayTokenField;
|
|
4332
|
+
window.saveRemoteRelayPublicUrl = saveRemoteRelayPublicUrl;
|
|
4333
|
+
window.toggleRemoteRelayInbound = toggleRemoteRelayInbound;
|
|
4297
4334
|
window.doAuth = doAuth;
|
|
4298
4335
|
window.initApp = initApp;
|
|
4299
4336
|
window.connectSocket = connectSocket;
|
|
@@ -4358,14 +4395,17 @@ window.confirmRepoBrowserCwdPick = confirmRepoBrowserCwdPick;
|
|
|
4358
4395
|
window.toggleEmojiPicker = toggleEmojiPicker;
|
|
4359
4396
|
window.submitRelayQuestionChoice = submitRelayQuestionChoice;
|
|
4360
4397
|
window.submitRelayQuestionAnswer = submitRelayQuestionAnswer;
|
|
4398
|
+
window.submitRelayQuestionMultiSelect = submitRelayQuestionMultiSelect;
|
|
4361
4399
|
window.submitRelayStructuredAnswer = submitRelayStructuredAnswer;
|
|
4362
4400
|
window.onRelayQuestionDraftInput = onRelayQuestionDraftInput;
|
|
4401
|
+
window.onRelayQuestionMultiSelectChange = onRelayQuestionMultiSelectChange;
|
|
4402
|
+
window.setRelayQuestionMultiMode = setRelayQuestionMultiMode;
|
|
4363
4403
|
window.handleRelayQuestionKey = handleRelayQuestionKey;
|
|
4364
4404
|
window.openPendingQuestionFromBanner = openPendingQuestionFromBanner;
|
|
4365
4405
|
window.submitRelayBoardAction = submitRelayBoardAction;
|
|
4366
4406
|
window.compactCurrentConversation = compactCurrentConversation;
|
|
4367
4407
|
window.sendMessage = sendMessage;
|
|
4368
|
-
window.
|
|
4408
|
+
window.syncComposerButtonState = syncComposerButtonState;
|
|
4369
4409
|
window.syncComposerPlaceholder = syncComposerPlaceholder;
|
|
4370
4410
|
window.persistComposerAttachments = persistComposerAttachments;
|
|
4371
4411
|
window.appendMessage = appendMessage;
|
|
@@ -4387,6 +4427,7 @@ window.openSuspendHostConfirmation = openSuspendHostConfirmation;
|
|
|
4387
4427
|
window.confirmKillCurrentSession = confirmKillCurrentSession;
|
|
4388
4428
|
window.confirmRestartWebRelay = confirmRestartWebRelay;
|
|
4389
4429
|
window.confirmSuspendHost = confirmSuspendHost;
|
|
4430
|
+
window.cancelQueuedHostSuspend = cancelQueuedHostSuspend;
|
|
4390
4431
|
window.confirmEmptyQueue = confirmEmptyQueue;
|
|
4391
4432
|
window.openMessageSearchModal = openMessageSearchModal;
|
|
4392
4433
|
window.closeMessageSearchModal = closeMessageSearchModal;
|