@oar-sh/oar 0.9.11 → 0.9.13
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 +332 -0
- package/README.md +10 -10
- package/package.json +1 -1
- package/server/README.md +201 -12
- package/server/claude-worker/claude-remote-relay-tool.mjs +2 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +57 -4
- package/server/claude-worker/claude-session-process.mjs +179 -0
- package/server/claude-worker/claude-session-worker.mjs +17 -0
- package/server/claude-worker/task-output-discovery.mjs +152 -0
- package/server/copilot-worker/copilot-continuation-signals.mjs +11 -5
- package/server/copilot-worker/copilot-sdk-session-process.mjs +169 -3
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +2 -0
- package/server/copilot-worker/copilot-shell-output-mirror.mjs +94 -0
- package/server/copilot-worker/copilot-task-settle.mjs +96 -0
- package/server/db-schema.mjs +19 -0
- package/server/grok-worker/acp-host-services.mjs +48 -6
- package/server/grok-worker/grok-background-tasks.mjs +248 -0
- package/server/grok-worker/grok-sdk-adapter.mjs +3 -1
- package/server/grok-worker/grok-session-worker.mjs +33 -1
- package/server/grok-worker/grok-turn-runner.mjs +11 -0
- package/server/migrations/0017-conversation-history-indexes.mjs +89 -0
- package/server/migrations/0018-background-task-rows.mjs +101 -0
- package/server/public/app/action-confirmations.js +12 -15
- package/server/public/app/api-client.js +70 -3
- package/server/public/app/background-tasks-view.mjs +334 -16
- package/server/public/app/bootstrap.js +102 -5
- package/server/public/app/claude-background-command-limit-options.mjs +21 -0
- package/server/public/app/conversation-payload-cache.mjs +357 -0
- package/server/public/app/conversation-preferences.mjs +10 -4
- package/server/public/app/conversation-turn-state.mjs +1 -1
- package/server/public/app/conversation-view.js +462 -8
- package/server/public/app/conversation-window-retention.mjs +96 -0
- package/server/public/app/host-suspend-ui.js +45 -44
- package/server/public/app/journal-view.js +259 -67
- package/server/public/app/push-settings.js +1 -0
- package/server/public/app/relay-prompt-sanitizer.mjs +20 -6
- package/server/public/app/relay-restart-note-view.mjs +36 -0
- package/server/public/app/relay-restart-ui.js +568 -0
- package/server/public/app/settings-modal.js +162 -2
- package/server/public/app/socket-handlers.js +59 -7
- package/server/public/app/store.js +25 -0
- package/server/public/app/subagent-viewer.mjs +7 -1
- package/server/public/app/task-console-render.mjs +401 -0
- package/server/public/app/task-console-watch.mjs +42 -0
- package/server/public/app/task-console.mjs +730 -0
- package/server/public/app/task-output-text.mjs +243 -0
- package/server/public/app/transcript-island.mjs +73 -0
- package/server/public/app/transcript-jump-bottom-view.mjs +42 -8
- package/server/public/app/transcript-jump-bottom.mjs +68 -11
- package/server/public/app/transcript-reading-anchor.mjs +256 -0
- package/server/public/app/transcript-reload-freshness.mjs +17 -1
- package/server/public/app/transcript-viewport-anchor.mjs +20 -1
- package/server/public/index.html +306 -6
- package/server/relay-tools.md +6 -0
- package/server/repositories/message-repository.mjs +11 -0
- package/server/repositories/question-repository.mjs +4 -0
- package/server/repositories/remote-relay-repository.mjs +4 -2
- package/server/routes/messages-routes.mjs +397 -23
- package/server/routes/relay-board-routes.mjs +19 -2
- package/server/routes/relay-restart-routes.mjs +73 -0
- package/server/routes/sessions-routes.mjs +208 -66
- package/server/server-runtime.mjs +269 -137
- package/server/services/about-service.mjs +13 -12
- package/server/services/background-task-persistence.mjs +133 -0
- package/server/services/background-task-store.mjs +470 -6
- package/server/services/cli-process-runner.mjs +4 -21
- package/server/services/conversation-history-page.mjs +199 -0
- package/server/services/conversation-model-fallback.mjs +57 -0
- package/server/services/host-suspend-command.mjs +2 -1
- package/server/services/preview-proxy-server.mjs +14 -1
- package/server/services/process-list-service.mjs +2 -1
- package/server/services/provider-model-selection.mjs +195 -0
- package/server/services/push-dispatch-service.mjs +9 -2
- package/server/services/relay-event-service.mjs +27 -9
- package/server/services/relay-prompt-sanitizer.mjs +18 -4
- package/server/services/relay-restart-picture.mjs +209 -0
- package/server/services/relay-restart-queue.mjs +380 -0
- package/server/services/relay-shutdown-gate.mjs +4 -1
- package/server/services/remote-relay-dispatcher.mjs +316 -20
- package/server/services/restart-resume-service.mjs +303 -0
- package/server/services/session-activity-tracker.mjs +6 -2
- package/server/services/session-worker-process-service.mjs +6 -4
- package/server/services/task-output-rooms.mjs +259 -0
- package/server/services/task-output-service.mjs +761 -0
- package/shared/claude-background-command-limit.mjs +83 -0
- package/shared/relay-events.mjs +55 -2
- package/shared/remote-relay-contract.mjs +47 -10
- package/shared/task-output-text.mjs +850 -0
- package/shared/worker-runtime/task-output-spool.mjs +181 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +96 -20
|
@@ -29,6 +29,10 @@ import {
|
|
|
29
29
|
updateTurnCeilingSetting as requestTurnCeilingSetting,
|
|
30
30
|
loadBackgroundTaskTimeoutSetting,
|
|
31
31
|
updateBackgroundTaskTimeoutSetting as requestBackgroundTaskTimeoutSetting,
|
|
32
|
+
loadTaskStallMinutesSetting,
|
|
33
|
+
updateTaskStallMinutesSetting as requestTaskStallMinutesSetting,
|
|
34
|
+
loadRestartResumeSetting,
|
|
35
|
+
updateRestartResumeSetting as requestRestartResumeSetting,
|
|
32
36
|
loadSubagentRecordRetentionSetting,
|
|
33
37
|
updateSubagentRecordRetentionSetting as requestSubagentRecordRetentionSetting,
|
|
34
38
|
loadFeatureFlagSettings,
|
|
@@ -36,7 +40,14 @@ import {
|
|
|
36
40
|
requestRelayRestart,
|
|
37
41
|
} from './api-client.js';
|
|
38
42
|
import { DEFAULT_BACKGROUND_TASK_TIMEOUT_MINUTES } from './background-task-timeout-options.mjs';
|
|
43
|
+
import {
|
|
44
|
+
DEFAULT_CLAUDE_BACKGROUND_COMMAND_LIMIT_MINUTES,
|
|
45
|
+
formatClaudeBackgroundCommandLimitLabel,
|
|
46
|
+
} from './claude-background-command-limit-options.mjs';
|
|
47
|
+
import { DEFAULT_TASK_STALL_MINUTES } from './task-console-render.mjs';
|
|
48
|
+
import { setTaskStallThreshold } from './background-tasks-view.mjs';
|
|
39
49
|
import { describeRelayRestartError } from './relay-restart-error.mjs';
|
|
50
|
+
import { refreshRelayRestartMaxWaitSetting } from './relay-restart-ui.js';
|
|
40
51
|
import { syncFontScaleSelect } from './font-scaling.js';
|
|
41
52
|
import { syncInstalledAppUiState, syncPwaAppNameInput } from './pwa-install.js';
|
|
42
53
|
import { normalizeKnownCwdPath } from './cwd-picker.js';
|
|
@@ -51,6 +62,7 @@ import { refreshRemoteRelaysSection } from './remote-relays-settings.js';
|
|
|
51
62
|
import { refreshAgentSessionsSection } from './agent-sessions-settings-ui.js';
|
|
52
63
|
import { refreshSharedConversationsSection } from './shared-conversations-settings.js';
|
|
53
64
|
import { syncScrollOnSendToggle } from './transcript-jump-bottom.mjs';
|
|
65
|
+
import { syncWindowRetentionSlider } from './conversation-window-retention.mjs';
|
|
54
66
|
import {
|
|
55
67
|
getPreviews,
|
|
56
68
|
renderPreviewRowsInto,
|
|
@@ -202,6 +214,120 @@ export async function refreshBackgroundTaskTimeoutSetting() {
|
|
|
202
214
|
}
|
|
203
215
|
}
|
|
204
216
|
|
|
217
|
+
// Minutes without output after which a background task's row turns amber.
|
|
218
|
+
// Starts at the relay's default; bounds come from the API response.
|
|
219
|
+
let taskStallMinutes = DEFAULT_TASK_STALL_MINUTES;
|
|
220
|
+
let taskStallMinutesUpdateInFlight = false;
|
|
221
|
+
|
|
222
|
+
export function formatTaskStallMinutesLabel(value) {
|
|
223
|
+
const minutes = Math.max(1, Math.round(Number(value) || DEFAULT_TASK_STALL_MINUTES));
|
|
224
|
+
return `${minutes} min`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function syncTaskStallMinutesSlider() {
|
|
228
|
+
const slider = document.getElementById('task-stall-minutes-slider');
|
|
229
|
+
const label = document.getElementById('task-stall-minutes-value');
|
|
230
|
+
if (slider instanceof HTMLInputElement) {
|
|
231
|
+
slider.value = String(taskStallMinutes);
|
|
232
|
+
slider.disabled = taskStallMinutesUpdateInFlight;
|
|
233
|
+
}
|
|
234
|
+
if (label) label.textContent = formatTaskStallMinutesLabel(taskStallMinutes);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Live label feedback while dragging; nothing is persisted until change fires. */
|
|
238
|
+
export function previewTaskStallMinutesSetting(value) {
|
|
239
|
+
const label = document.getElementById('task-stall-minutes-value');
|
|
240
|
+
if (label) label.textContent = formatTaskStallMinutesLabel(value);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export async function refreshTaskStallMinutesSetting() {
|
|
244
|
+
syncTaskStallMinutesSlider();
|
|
245
|
+
try {
|
|
246
|
+
const result = await loadTaskStallMinutesSetting();
|
|
247
|
+
if (!result || !Number.isFinite(Number(result.minutes))) return;
|
|
248
|
+
taskStallMinutes = Number(result.minutes);
|
|
249
|
+
setTaskStallThreshold(taskStallMinutes);
|
|
250
|
+
const slider = document.getElementById('task-stall-minutes-slider');
|
|
251
|
+
if (slider instanceof HTMLInputElement) {
|
|
252
|
+
if (Number.isFinite(Number(result.min))) slider.min = String(result.min);
|
|
253
|
+
if (Number.isFinite(Number(result.max))) slider.max = String(result.max);
|
|
254
|
+
}
|
|
255
|
+
} catch {
|
|
256
|
+
// Keep the last known value; the setting is not critical.
|
|
257
|
+
} finally {
|
|
258
|
+
syncTaskStallMinutesSlider();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export async function updateTaskStallMinutesSetting(value) {
|
|
263
|
+
if (taskStallMinutesUpdateInFlight) {
|
|
264
|
+
syncTaskStallMinutesSlider();
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const requested = Math.max(1, Math.round(Number(value) || DEFAULT_TASK_STALL_MINUTES));
|
|
268
|
+
taskStallMinutesUpdateInFlight = true;
|
|
269
|
+
syncTaskStallMinutesSlider();
|
|
270
|
+
try {
|
|
271
|
+
const result = await requestTaskStallMinutesSetting(requested);
|
|
272
|
+
if (!result) throw new Error('Failed to update when a background task counts as quiet.');
|
|
273
|
+
taskStallMinutes = Number.isFinite(Number(result.minutes)) ? Number(result.minutes) : requested;
|
|
274
|
+
// The open conversation's rows follow at once, not on the next load.
|
|
275
|
+
setTaskStallThreshold(taskStallMinutes);
|
|
276
|
+
showTransientRelayNotice(`Background tasks are marked as quiet after ${formatTaskStallMinutesLabel(taskStallMinutes)} without output.`);
|
|
277
|
+
} catch (error) {
|
|
278
|
+
alert(error?.message || 'Failed to update when a background task counts as quiet.');
|
|
279
|
+
} finally {
|
|
280
|
+
taskStallMinutesUpdateInFlight = false;
|
|
281
|
+
syncTaskStallMinutesSlider();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// "After a relay restart, tell interrupted sessions to carry on": a relay
|
|
286
|
+
// setting, off by default (server/services/restart-resume-service.mjs).
|
|
287
|
+
let restartResumeEnabled = false;
|
|
288
|
+
let restartResumeUpdateInFlight = false;
|
|
289
|
+
|
|
290
|
+
function syncRestartResumeToggle() {
|
|
291
|
+
const toggle = document.getElementById('restart-resume-toggle');
|
|
292
|
+
if (!(toggle instanceof HTMLInputElement)) return;
|
|
293
|
+
toggle.checked = restartResumeEnabled;
|
|
294
|
+
toggle.disabled = restartResumeUpdateInFlight;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export async function refreshRestartResumeSetting() {
|
|
298
|
+
syncRestartResumeToggle();
|
|
299
|
+
try {
|
|
300
|
+
const result = await loadRestartResumeSetting();
|
|
301
|
+
if (result && typeof result.enabled === 'boolean') restartResumeEnabled = result.enabled;
|
|
302
|
+
} catch {
|
|
303
|
+
// Keep the last known value; the toggle shows it.
|
|
304
|
+
} finally {
|
|
305
|
+
syncRestartResumeToggle();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export async function updateRestartResumeSettingFromToggle(checked) {
|
|
310
|
+
if (restartResumeUpdateInFlight) {
|
|
311
|
+
syncRestartResumeToggle();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
restartResumeUpdateInFlight = true;
|
|
315
|
+
syncRestartResumeToggle();
|
|
316
|
+
try {
|
|
317
|
+
const result = await requestRestartResumeSetting(checked === true);
|
|
318
|
+
if (!result || typeof result.enabled !== 'boolean') throw new Error('Failed to update the restart setting.');
|
|
319
|
+
restartResumeEnabled = result.enabled;
|
|
320
|
+
showTransientRelayNotice(restartResumeEnabled
|
|
321
|
+
? 'After a relay restart, sessions whose turn it cut off are told to carry on.'
|
|
322
|
+
: 'After a relay restart, interrupted sessions are left as they are.');
|
|
323
|
+
} catch (error) {
|
|
324
|
+
alert(error?.message || 'Failed to update the restart setting.');
|
|
325
|
+
} finally {
|
|
326
|
+
restartResumeUpdateInFlight = false;
|
|
327
|
+
syncRestartResumeToggle();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
205
331
|
// Mirrors shared/subagent-record-retention.mjs (the browser cannot import it):
|
|
206
332
|
// the select starts at the relay's default until the relay's value loads.
|
|
207
333
|
const DEFAULT_SUBAGENT_RECORD_RETENTION_DAYS = 90;
|
|
@@ -479,6 +605,7 @@ let claudeSettingsState = {
|
|
|
479
605
|
enabled: false,
|
|
480
606
|
model: 'claude-sonnet-5',
|
|
481
607
|
models: [],
|
|
608
|
+
backgroundCommandLimitMinutes: DEFAULT_CLAUDE_BACKGROUND_COMMAND_LIMIT_MINUTES,
|
|
482
609
|
};
|
|
483
610
|
let claudeSettingsInputsDirty = false;
|
|
484
611
|
|
|
@@ -492,7 +619,7 @@ function ensureClaudeSettingsInputTracking() {
|
|
|
492
619
|
}
|
|
493
620
|
|
|
494
621
|
function setClaudeSettingsControlsDisabled(disabled) {
|
|
495
|
-
for (const id of ['claude-model-input', 'claude-enabled-toggle', 'claude-attribution-select', 'claude-save-btn']) {
|
|
622
|
+
for (const id of ['claude-model-input', 'claude-enabled-toggle', 'claude-attribution-select', 'claude-background-command-limit-slider', 'claude-save-btn']) {
|
|
496
623
|
const element = document.getElementById(id);
|
|
497
624
|
if (element) element.disabled = disabled;
|
|
498
625
|
}
|
|
@@ -507,6 +634,9 @@ export function applyClaudeSettingsState(settings = {}, { resetInputs = false }
|
|
|
507
634
|
models: Array.isArray(settings?.models) ? settings.models : claudeSettingsState.models,
|
|
508
635
|
attributionMode: ['oar', 'vanilla', 'off'].includes(settings?.attributionMode) ? settings.attributionMode : (claudeSettingsState.attributionMode || 'oar'),
|
|
509
636
|
attributionExample: String(settings?.attributionExample || claudeSettingsState.attributionExample || '').trim(),
|
|
637
|
+
backgroundCommandLimitMinutes: Number(settings?.backgroundCommandLimitMinutes) > 0
|
|
638
|
+
? Number(settings.backgroundCommandLimitMinutes)
|
|
639
|
+
: (claudeSettingsState.backgroundCommandLimitMinutes || DEFAULT_CLAUDE_BACKGROUND_COMMAND_LIMIT_MINUTES),
|
|
510
640
|
};
|
|
511
641
|
const modelInput = document.getElementById('claude-model-input');
|
|
512
642
|
const toggle = document.getElementById('claude-enabled-toggle');
|
|
@@ -514,6 +644,15 @@ export function applyClaudeSettingsState(settings = {}, { resetInputs = false }
|
|
|
514
644
|
const attributionSelect = document.getElementById('claude-attribution-select');
|
|
515
645
|
if (attributionSelect && (!claudeSettingsInputsDirty || resetInputs)) attributionSelect.value = claudeSettingsState.attributionMode;
|
|
516
646
|
renderClaudeAttributionExample(attributionSelect?.value || claudeSettingsState.attributionMode);
|
|
647
|
+
const limitSlider = document.getElementById('claude-background-command-limit-slider');
|
|
648
|
+
if (limitSlider instanceof HTMLInputElement) {
|
|
649
|
+
// Bounds come from the relay; the markup's are only the pre-load copy.
|
|
650
|
+
for (const [attribute, field] of [['min', 'backgroundCommandLimitMinMinutes'], ['max', 'backgroundCommandLimitMaxMinutes'], ['step', 'backgroundCommandLimitStepMinutes']]) {
|
|
651
|
+
if (Number(settings?.[field]) > 0) limitSlider[attribute] = String(settings[field]);
|
|
652
|
+
}
|
|
653
|
+
if (!claudeSettingsInputsDirty || resetInputs) limitSlider.value = String(claudeSettingsState.backgroundCommandLimitMinutes);
|
|
654
|
+
}
|
|
655
|
+
renderClaudeBackgroundCommandLimitLabel(limitSlider?.value || claudeSettingsState.backgroundCommandLimitMinutes);
|
|
517
656
|
if (modelInput && (!claudeSettingsInputsDirty || resetInputs)) modelInput.value = claudeSettingsState.model;
|
|
518
657
|
if (resetInputs) claudeSettingsInputsDirty = false;
|
|
519
658
|
if (toggle) {
|
|
@@ -561,6 +700,19 @@ function renderClaudeAttributionExample(mode) {
|
|
|
561
700
|
example.textContent = claudeAttributionExampleText(mode, claudeSettingsState.attributionExample);
|
|
562
701
|
}
|
|
563
702
|
|
|
703
|
+
function renderClaudeBackgroundCommandLimitLabel(minutes) {
|
|
704
|
+
const label = document.getElementById('claude-background-command-limit-value');
|
|
705
|
+
if (label) label.textContent = formatClaudeBackgroundCommandLimitLabel(minutes);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/** Live label feedback while dragging; saved with the other Claude settings. */
|
|
709
|
+
export function previewClaudeBackgroundCommandLimit(value) {
|
|
710
|
+
claudeSettingsInputsDirty = true;
|
|
711
|
+
renderClaudeBackgroundCommandLimitLabel(value);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
window.previewClaudeBackgroundCommandLimit = previewClaudeBackgroundCommandLimit;
|
|
715
|
+
|
|
564
716
|
window.previewClaudeAttribution = (mode) => {
|
|
565
717
|
claudeSettingsInputsDirty = true;
|
|
566
718
|
renderClaudeAttributionExample(mode);
|
|
@@ -572,10 +724,12 @@ export async function saveClaudeSettings() {
|
|
|
572
724
|
const model = String(modelInput?.value || '').trim() || 'claude-sonnet-5';
|
|
573
725
|
const attributionSelect = document.getElementById('claude-attribution-select');
|
|
574
726
|
const attributionMode = ['oar', 'vanilla', 'off'].includes(attributionSelect?.value) ? attributionSelect.value : undefined;
|
|
727
|
+
const limitSlider = document.getElementById('claude-background-command-limit-slider');
|
|
728
|
+
const backgroundCommandLimitMinutes = Number(limitSlider?.value) > 0 ? Number(limitSlider.value) : undefined;
|
|
575
729
|
claudeSettingsUpdateInFlight = true;
|
|
576
730
|
setClaudeSettingsControlsDisabled(true);
|
|
577
731
|
try {
|
|
578
|
-
const result = await updateClaudeSettings({ model, attributionMode });
|
|
732
|
+
const result = await updateClaudeSettings({ model, attributionMode, backgroundCommandLimitMinutes });
|
|
579
733
|
if (!result) throw new Error('Failed to save Claude settings.');
|
|
580
734
|
applyClaudeSettingsState(result, { resetInputs: true });
|
|
581
735
|
showTransientRelayNotice(
|
|
@@ -1757,6 +1911,7 @@ export function openSettingsModal(tab, providerTab) {
|
|
|
1757
1911
|
}
|
|
1758
1912
|
syncSuspendHostVisibility();
|
|
1759
1913
|
syncScrollOnSendToggle();
|
|
1914
|
+
syncWindowRetentionSlider();
|
|
1760
1915
|
syncFontScaleSelect();
|
|
1761
1916
|
syncPwaAppNameInput();
|
|
1762
1917
|
syncDefaultSessionWorkspaceRootInput();
|
|
@@ -1789,8 +1944,13 @@ export function openSettingsModal(tab, providerTab) {
|
|
|
1789
1944
|
void refreshTurnCeilingSetting();
|
|
1790
1945
|
syncBackgroundTaskTimeoutSlider();
|
|
1791
1946
|
void refreshBackgroundTaskTimeoutSetting();
|
|
1947
|
+
syncTaskStallMinutesSlider();
|
|
1948
|
+
void refreshTaskStallMinutesSetting();
|
|
1949
|
+
syncRestartResumeToggle();
|
|
1950
|
+
void refreshRestartResumeSetting();
|
|
1792
1951
|
syncSubagentRecordRetentionSelect();
|
|
1793
1952
|
void refreshSubagentRecordRetentionSetting();
|
|
1953
|
+
void refreshRelayRestartMaxWaitSetting();
|
|
1794
1954
|
void refreshPushSettingsSection();
|
|
1795
1955
|
void refreshPreviewsSection();
|
|
1796
1956
|
void refreshSharedConversationsSection();
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
applySessionWorkerSteering,
|
|
31
31
|
refreshSummaryModal,
|
|
32
32
|
summaryModalState,
|
|
33
|
+
IS_SHARED_VIEW,
|
|
33
34
|
} from './store.js';
|
|
34
35
|
import { scheduleContextUsageRefresh } from './api-client.js';
|
|
35
36
|
import { capLiveActivityEntries } from './activity-replay-state.mjs';
|
|
@@ -43,7 +44,8 @@ import {
|
|
|
43
44
|
updatePendingQuestionBanner,
|
|
44
45
|
} from './ask-user-view.js';
|
|
45
46
|
import { upsertRelayBoard, loadRelayBoards, renderRelayBoards } from './relay-board-view.js';
|
|
46
|
-
import { setConversationBackgroundTasks } from './background-tasks-view.mjs';
|
|
47
|
+
import { setConversationBackgroundTasks, setConversationTaskHealth } from './background-tasks-view.mjs';
|
|
48
|
+
import { handleTaskConsoleReconnect, handleTaskOutputChunk } from './task-console.mjs';
|
|
47
49
|
import { setConversationPins } from './pinned-messages-view.mjs';
|
|
48
50
|
import { setConversationHiddenMessages } from './hidden-messages-view.mjs';
|
|
49
51
|
import { setPreviews } from './preview-cards.mjs';
|
|
@@ -83,6 +85,8 @@ import {
|
|
|
83
85
|
noteTranscriptLiveRow,
|
|
84
86
|
applyIncomingConversationDraftUpdate,
|
|
85
87
|
getRenderedConversationMessageFingerprints,
|
|
88
|
+
holdBackLiveRow,
|
|
89
|
+
isConversationWindowAtTail,
|
|
86
90
|
clearBubbleCancelState,
|
|
87
91
|
removeUserBubbleCancelButton,
|
|
88
92
|
updateSubagentBubbleFromStatus,
|
|
@@ -96,6 +100,17 @@ import { clearMessageSearchRuntimeState } from './message-search-view.js';
|
|
|
96
100
|
import { stripRelayPromptContext } from './relay-prompt-sanitizer.mjs';
|
|
97
101
|
import { isLikelyLiveDuplicateMessage } from './live-message-dedupe.mjs';
|
|
98
102
|
import { mergeRelayThoughts } from './relay-thoughts.mjs';
|
|
103
|
+
import { conversationPayloadCache } from './conversation-payload-cache.mjs';
|
|
104
|
+
|
|
105
|
+
// A live row of the conversation on screen joins its kept window, so a
|
|
106
|
+
// switch away and back still shows it. Rows of other conversations do not:
|
|
107
|
+
// their kept copy is what the reader saw, and the switch back fetches the rest.
|
|
108
|
+
function keepLiveRow(conversationId, message, messageId) {
|
|
109
|
+
if (IS_SHARED_VIEW || !message || typeof message !== 'object') return;
|
|
110
|
+
const id = String(message.id || messageId || '').trim();
|
|
111
|
+
if (!id) return;
|
|
112
|
+
conversationPayloadCache.upsertMessage(conversationId, message.id ? message : { ...message, id });
|
|
113
|
+
}
|
|
99
114
|
import {
|
|
100
115
|
handleSubagentStatusEventForViewer,
|
|
101
116
|
handleSubagentTimelineEvent,
|
|
@@ -335,6 +350,9 @@ export async function connectSocket(overrideDeps) {
|
|
|
335
350
|
// session, whose restored socket.data the server deliberately resets.
|
|
336
351
|
emitDeviceVisibility(document.visibilityState === 'visible');
|
|
337
352
|
clearMessageSearchRuntimeState();
|
|
353
|
+
// Events missed while the socket was down may have changed any of the
|
|
354
|
+
// conversations kept for a quick switch back.
|
|
355
|
+
conversationPayloadCache.clear();
|
|
338
356
|
setRelayOnline(true);
|
|
339
357
|
setCliOnline(true);
|
|
340
358
|
renderConvList();
|
|
@@ -346,6 +364,8 @@ export async function connectSocket(overrideDeps) {
|
|
|
346
364
|
refreshModelCatalog().catch(() => {});
|
|
347
365
|
// An open sub-agent viewer fetches whatever it missed while away.
|
|
348
366
|
handleSubagentViewerReconnect();
|
|
367
|
+
// An open task console watches again and reads what it missed.
|
|
368
|
+
void handleTaskConsoleReconnect();
|
|
349
369
|
// Subscriptions that changed while away.
|
|
350
370
|
void loadRelayEventSubscriptions();
|
|
351
371
|
// Shares made or stopped while away.
|
|
@@ -519,11 +539,15 @@ export async function connectSocket(overrideDeps) {
|
|
|
519
539
|
})) {
|
|
520
540
|
return;
|
|
521
541
|
}
|
|
542
|
+
// A window that ends above the conversation's end takes no live row:
|
|
543
|
+
// the newer pages bring it, and the jump-to-bottom badge counts it.
|
|
544
|
+
if (holdBackLiveRow(conversationId, messageId || normalizedMessage.id)) return;
|
|
522
545
|
// Another client's message (an agent's prompt included) follows the
|
|
523
546
|
// end like any live row: a reader scrolled up stays where they are,
|
|
524
547
|
// and the jump-to-bottom badge counts it.
|
|
525
548
|
appendMessage(normalizedMessage, followsTranscriptEnd(), messageId);
|
|
526
549
|
noteTranscriptLiveRow();
|
|
550
|
+
keepLiveRow(conversationId, normalizedMessage, messageId);
|
|
527
551
|
}
|
|
528
552
|
});
|
|
529
553
|
socket.on('assistant_message', ({ conversationId, message, messageId, sourceMessageId }) => {
|
|
@@ -554,12 +578,15 @@ export async function connectSocket(overrideDeps) {
|
|
|
554
578
|
restoreLiveReplyReadingPosition(reading);
|
|
555
579
|
return;
|
|
556
580
|
}
|
|
557
|
-
|
|
581
|
+
// Held back under a window that ends above the conversation's end, as
|
|
582
|
+
// in user_message.
|
|
583
|
+
if (isCurrentConversation && !holdBackLiveRow(conversationId, messageId || message?.id)) {
|
|
558
584
|
appendMessage(message, autoScroll, messageId || null, false, sourceMessageId || null);
|
|
559
585
|
restoreLiveReplyReadingPosition(reading);
|
|
560
586
|
noteTranscriptLiveRow();
|
|
561
|
-
|
|
587
|
+
keepLiveRow(conversationId, message, messageId);
|
|
562
588
|
}
|
|
589
|
+
if (isCurrentConversation) scheduleContextUsageRefresh(conversationId, 120);
|
|
563
590
|
if (sourceMessageId) relayActivities.delete(sourceMessageId);
|
|
564
591
|
if (sourceMessageId) relayThoughts.delete(sourceMessageId);
|
|
565
592
|
if (sourceMessageId) clearRelayStreamStateForMessage(sourceMessageId);
|
|
@@ -574,11 +601,22 @@ export async function connectSocket(overrideDeps) {
|
|
|
574
601
|
// cannot be started): same id, new text or kind. Not on the page yet (the
|
|
575
602
|
// conversation was opened after it was written): appended like any other.
|
|
576
603
|
socket.on('message_updated', ({ conversationId, messageId, message }) => {
|
|
577
|
-
|
|
604
|
+
// A row rewritten in place. Another conversation's kept window would show
|
|
605
|
+
// the old one: it is dropped. The window on screen takes the new row as
|
|
606
|
+
// the page does, so a switch away and back keeps both the row and the
|
|
607
|
+
// window around it.
|
|
608
|
+
if (!messageId || conversationId !== currentConvId || !message) {
|
|
609
|
+
conversationPayloadCache.forget(conversationId);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
578
612
|
if (!replaceRenderedMessage(message, messageId)) {
|
|
613
|
+
// A row outside a window that ends above the conversation's end: the
|
|
614
|
+
// newer pages bring it as it is now.
|
|
615
|
+
if (!isConversationWindowAtTail()) return;
|
|
579
616
|
appendMessage(message, followsTranscriptEnd(), messageId, false);
|
|
580
617
|
}
|
|
581
618
|
noteTranscriptLiveRow();
|
|
619
|
+
keepLiveRow(conversationId, message, messageId);
|
|
582
620
|
});
|
|
583
621
|
socket.on('relay_question', ({ question }) => upsertRelayQuestion(question));
|
|
584
622
|
socket.on('relay_question_updated', ({ question }) => upsertRelayQuestion(question));
|
|
@@ -594,8 +632,8 @@ export async function connectSocket(overrideDeps) {
|
|
|
594
632
|
socket.on('relay_event_subscriptions', ({ subscriptions }) => {
|
|
595
633
|
applyRelayEventSubscriptions(subscriptions);
|
|
596
634
|
});
|
|
597
|
-
socket.on('background_tasks', ({ conversationId, tasks }) => {
|
|
598
|
-
setConversationBackgroundTasks(conversationId, tasks);
|
|
635
|
+
socket.on('background_tasks', ({ conversationId, tasks, endedTasks }) => {
|
|
636
|
+
setConversationBackgroundTasks(conversationId, tasks, { endedTasks, event: true });
|
|
599
637
|
// The sidebar's row (and its group's head) count background work as
|
|
600
638
|
// running; the set arrives under the conversation or its session id.
|
|
601
639
|
const id = String(conversationId || '').trim();
|
|
@@ -604,6 +642,16 @@ export async function connectSocket(overrideDeps) {
|
|
|
604
642
|
|| null;
|
|
605
643
|
if (record && applyBackgroundTasksToConversation(record, tasks)) renderConvList();
|
|
606
644
|
});
|
|
645
|
+
// The relay's health poller: last output time and test-runner counts of
|
|
646
|
+
// shell and Monitor tasks, only when they changed.
|
|
647
|
+
socket.on('background_tasks_health', ({ conversationId, health }) => {
|
|
648
|
+
setConversationTaskHealth(conversationId, health);
|
|
649
|
+
});
|
|
650
|
+
// New bytes of the task the console shows; the relay sends them only while
|
|
651
|
+
// this page's console watches that task.
|
|
652
|
+
socket.on('task_output_chunk', (payload) => {
|
|
653
|
+
handleTaskOutputChunk(payload);
|
|
654
|
+
});
|
|
607
655
|
// A message was pinned or unpinned (any client): the conversation's whole
|
|
608
656
|
// pin list, replacing what this page had.
|
|
609
657
|
socket.on('conversation_pins_updated', ({ conversationId, pins, revision }) => {
|
|
@@ -738,6 +786,8 @@ export async function connectSocket(overrideDeps) {
|
|
|
738
786
|
});
|
|
739
787
|
socket.on('conversation_compacted', async ({ sourceConversationId, targetConversationId }) => {
|
|
740
788
|
if (!sourceConversationId || !targetConversationId) return;
|
|
789
|
+
conversationPayloadCache.forget(sourceConversationId);
|
|
790
|
+
conversationPayloadCache.forget(targetConversationId);
|
|
741
791
|
await refreshConversations();
|
|
742
792
|
if (currentConvId === sourceConversationId) {
|
|
743
793
|
await openConversation(targetConversationId);
|
|
@@ -827,7 +877,7 @@ export async function connectSocket(overrideDeps) {
|
|
|
827
877
|
applyConversationTurnStatus({ conversationId, messageId, status });
|
|
828
878
|
if (conversationId && conversations[conversationId]) {
|
|
829
879
|
const conversation = conversations[conversationId];
|
|
830
|
-
// The row's turn fields follow the event, open or not: the
|
|
880
|
+
// The row's turn fields follow the event, open or not: the title colour, a
|
|
831
881
|
// group head's pill and its fold read them between list fetches.
|
|
832
882
|
applyTurnStatusToConversation(conversation, normalizedStatus);
|
|
833
883
|
if (normalizedStatus === 'processing') {
|
|
@@ -892,6 +942,7 @@ export async function connectSocket(overrideDeps) {
|
|
|
892
942
|
if (!id) return;
|
|
893
943
|
const conversation = conversations[id];
|
|
894
944
|
if (conversation) conversation.archived = archived !== false;
|
|
945
|
+
conversationPayloadCache.forget(id);
|
|
895
946
|
renderConvList();
|
|
896
947
|
if (currentConvId === id) {
|
|
897
948
|
syncChatTitleControls();
|
|
@@ -900,6 +951,7 @@ export async function connectSocket(overrideDeps) {
|
|
|
900
951
|
});
|
|
901
952
|
socket.on('conversation_deleted', ({ conversationId }) => {
|
|
902
953
|
delete conversations[conversationId];
|
|
954
|
+
conversationPayloadCache.forget(conversationId);
|
|
903
955
|
for (const [id, question] of relayQuestions.entries()) {
|
|
904
956
|
if (question?.conversationId === conversationId) relayQuestions.delete(id);
|
|
905
957
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolveNearBottomThresholdPx } from './transcript-viewport-anchor.mjs';
|
|
2
|
+
import { normalizeReadingAnchor } from './transcript-reading-anchor.mjs';
|
|
2
3
|
import { recordCliLifecycleEvent, recordRelayLifecycleEvent } from './status-store.mjs';
|
|
3
4
|
import { resolveRelayDotState } from './relay-dot-state.mjs';
|
|
4
5
|
import { readRepoBrowserPreferences } from './repo-browser-preferences.mjs';
|
|
@@ -313,6 +314,7 @@ function readConversationViewState(conversationId) {
|
|
|
313
314
|
loadedMessageCount: Number.isFinite(Number(parsed.loadedMessageCount)) && Number(parsed.loadedMessageCount) >= 0
|
|
314
315
|
? Math.trunc(Number(parsed.loadedMessageCount))
|
|
315
316
|
: null,
|
|
317
|
+
anchor: normalizeReadingAnchor(parsed.anchor),
|
|
316
318
|
};
|
|
317
319
|
}
|
|
318
320
|
} catch {
|
|
@@ -323,6 +325,7 @@ function readConversationViewState(conversationId) {
|
|
|
323
325
|
return {
|
|
324
326
|
scrollTop: Math.trunc(legacyScrollTop),
|
|
325
327
|
loadedMessageCount: null,
|
|
328
|
+
anchor: null,
|
|
326
329
|
};
|
|
327
330
|
}
|
|
328
331
|
|
|
@@ -337,6 +340,7 @@ function writeConversationViewState(conversationId, nextState = {}) {
|
|
|
337
340
|
loadedMessageCount: Number.isFinite(Number(nextState.loadedMessageCount)) && Number(nextState.loadedMessageCount) >= 0
|
|
338
341
|
? Math.trunc(Number(nextState.loadedMessageCount))
|
|
339
342
|
: current.loadedMessageCount ?? null,
|
|
343
|
+
anchor: 'anchor' in nextState ? normalizeReadingAnchor(nextState.anchor) : (current.anchor ?? null),
|
|
340
344
|
};
|
|
341
345
|
localStorage.setItem(key, JSON.stringify(next));
|
|
342
346
|
return true;
|
|
@@ -373,6 +377,27 @@ export function loadConversationLoadedMessageCount(conversationId) {
|
|
|
373
377
|
return readConversationViewState(conversationId)?.loadedMessageCount ?? null;
|
|
374
378
|
}
|
|
375
379
|
|
|
380
|
+
// The message read and its offset, or "at the end" (transcript-reading-anchor.mjs):
|
|
381
|
+
// what a switch back and a reload restore. scrollTop is still written for
|
|
382
|
+
// older pages of this app reading the same storage.
|
|
383
|
+
export function saveConversationReadingAnchor(conversationId, anchor) {
|
|
384
|
+
const value = normalizeReadingAnchor(anchor);
|
|
385
|
+
if (!value) return false;
|
|
386
|
+
try {
|
|
387
|
+
return writeConversationViewState(conversationId, { anchor: value });
|
|
388
|
+
} catch {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export function loadConversationReadingAnchor(conversationId) {
|
|
394
|
+
try {
|
|
395
|
+
return readConversationViewState(conversationId)?.anchor ?? null;
|
|
396
|
+
} catch {
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
376
401
|
function normalizePendingMessageText(text) {
|
|
377
402
|
return String(text || '').replace(/\s+/g, ' ').trim().toLowerCase();
|
|
378
403
|
}
|
|
@@ -21,7 +21,7 @@ import { createViewerHistory } from './viewer-history.mjs';
|
|
|
21
21
|
import { getSocket } from './socket-handlers.js';
|
|
22
22
|
import { createSubagentViewerWatch } from './subagent-viewer-watch.mjs';
|
|
23
23
|
import { formatTaskModelLabel, formatTaskTokens } from './task-row-format.mjs';
|
|
24
|
-
import { getConversationBackgroundTasks } from './background-tasks-view.mjs';
|
|
24
|
+
import { getConversationBackgroundTasks, setSubagentStepCounter } from './background-tasks-view.mjs';
|
|
25
25
|
import { formatRunDuration, isRunningSubagentStatus, subagentRowHtml, subagentStatusLabel } from './subagent-row.mjs';
|
|
26
26
|
import {
|
|
27
27
|
createTimelineState,
|
|
@@ -488,6 +488,12 @@ export function syncSubagentRowClock() {
|
|
|
488
488
|
export function initSubagentViewer() {
|
|
489
489
|
if (initialized) return;
|
|
490
490
|
initialized = true;
|
|
491
|
+
// A sub-agent row without the worker's `steps` counts the run's activity
|
|
492
|
+
// lines, the number its reply bubble shows.
|
|
493
|
+
setSubagentStepCounter((runId) => {
|
|
494
|
+
const activities = getSubagentRun(runId)?.activities;
|
|
495
|
+
return Array.isArray(activities) ? activities.length : null;
|
|
496
|
+
});
|
|
491
497
|
viewerHistory = createViewerHistory({
|
|
492
498
|
history: window.history,
|
|
493
499
|
mark: 'oarSubagentViewer',
|