@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
package/server/public/index.html
CHANGED
|
@@ -280,6 +280,7 @@
|
|
|
280
280
|
--warn: #d29922;
|
|
281
281
|
--conversation-title-offline: #f0f6fc;
|
|
282
282
|
--conversation-title-ready: #3fb950;
|
|
283
|
+
--conversation-title-idle: #238636;
|
|
283
284
|
--conversation-title-question: #f85149;
|
|
284
285
|
--conversation-title-error: #d29922;
|
|
285
286
|
/* Relay reachability tones, shared by the sidebar dot and the mobile
|
|
@@ -347,6 +348,8 @@
|
|
|
347
348
|
--warn: #9a6700;
|
|
348
349
|
--conversation-title-offline: #1F2328;
|
|
349
350
|
--conversation-title-ready: #1a7f37;
|
|
351
|
+
/* Muted, not darker: a darker green would stand out more on white. */
|
|
352
|
+
--conversation-title-idle: #4c8b5c;
|
|
350
353
|
--conversation-title-question: #cf222e;
|
|
351
354
|
--conversation-title-error: #9a6700;
|
|
352
355
|
--provider-github: #0969da;
|
|
@@ -842,17 +845,17 @@
|
|
|
842
845
|
.conv-provider-indicator[data-provider="claude-cloud"] { color: var(--provider-claude-cloud); }
|
|
843
846
|
.conv-item.worker-ui-offline .conv-title,
|
|
844
847
|
#chat-title[data-ui-state="offline"] { color: var(--conversation-title-offline); }
|
|
848
|
+
/* Ready and idle: the darker green. Ready and working (a turn or
|
|
849
|
+
background work): the bright one. :where() keeps the working rule as
|
|
850
|
+
specific as the others, so an archived row stays muted. */
|
|
845
851
|
.conv-item.worker-ui-ready .conv-title,
|
|
846
|
-
#chat-title[data-ui-state="ready"] { color: var(--conversation-title-
|
|
852
|
+
#chat-title[data-ui-state="ready"] { color: var(--conversation-title-idle); }
|
|
853
|
+
.conv-item.worker-ui-ready:where(.conv-working) .conv-title,
|
|
854
|
+
#chat-title[data-ui-state="ready"][data-working] { color: var(--conversation-title-ready); }
|
|
847
855
|
.conv-item.worker-ui-question .conv-title,
|
|
848
856
|
#chat-title[data-ui-state="question"] { color: var(--conversation-title-question); }
|
|
849
857
|
.conv-item.worker-ui-error .conv-title,
|
|
850
858
|
#chat-title[data-ui-state="error"] { color: var(--conversation-title-error); }
|
|
851
|
-
.conv-processing-dots {
|
|
852
|
-
display: inline-block;
|
|
853
|
-
min-width: 1.8ch;
|
|
854
|
-
letter-spacing: 0.02em;
|
|
855
|
-
}
|
|
856
859
|
.conv-delete {
|
|
857
860
|
position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
|
|
858
861
|
background: none; border: none; color: var(--muted); cursor: pointer; font-size: var(--fs-ui-icon);
|
|
@@ -2981,6 +2984,15 @@
|
|
|
2981
2984
|
}
|
|
2982
2985
|
.pending-action-text { overflow-wrap: anywhere; }
|
|
2983
2986
|
.pending-action-cancel { margin-left: 0; }
|
|
2987
|
+
/* The queued relay restart's row (relay-restart-ui.js): the headline, the
|
|
2988
|
+
sessions it waits for (links), held messages, background tasks, buttons. */
|
|
2989
|
+
.relay-restart-row { flex-direction: column; gap: 3px; }
|
|
2990
|
+
.relay-restart-head { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; }
|
|
2991
|
+
.relay-restart-waited { opacity: 0.85; }
|
|
2992
|
+
.relay-restart-detail { color: var(--text); overflow-wrap: anywhere; max-height: 4.5em; overflow-y: auto; }
|
|
2993
|
+
.relay-restart-session-link { color: inherit; text-decoration: underline; }
|
|
2994
|
+
.relay-restart-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; }
|
|
2995
|
+
.relay-restart-row[data-asking="1"] .relay-restart-head { font-weight: 600; }
|
|
2984
2996
|
.suspend-host-blockers {
|
|
2985
2997
|
margin: 6px 0 10px;
|
|
2986
2998
|
padding-left: 20px;
|
|
@@ -3379,6 +3391,189 @@
|
|
|
3379
3391
|
.subagent-viewer-dialog { width: 100%; height: 100%; border: 0; border-radius: 0; }
|
|
3380
3392
|
#subagent-viewer-body { padding: 8px 10px 28px; }
|
|
3381
3393
|
}
|
|
3394
|
+
/* The task console: a shell or Monitor task's output (task-console.mjs).
|
|
3395
|
+
Same shell as the sub-agent viewer; the 16 terminal colours follow the
|
|
3396
|
+
theme so red and green stay readable on Day and Night. */
|
|
3397
|
+
:root {
|
|
3398
|
+
--tc-ansi-0: #484f58;
|
|
3399
|
+
--tc-ansi-1: #ff7b72;
|
|
3400
|
+
--tc-ansi-2: #3fb950;
|
|
3401
|
+
--tc-ansi-3: #d29922;
|
|
3402
|
+
--tc-ansi-4: #58a6ff;
|
|
3403
|
+
--tc-ansi-5: #bc8cff;
|
|
3404
|
+
--tc-ansi-6: #39c5cf;
|
|
3405
|
+
--tc-ansi-7: #b1bac4;
|
|
3406
|
+
--tc-ansi-8: #6e7681;
|
|
3407
|
+
--tc-ansi-9: #ffa198;
|
|
3408
|
+
--tc-ansi-10: #56d364;
|
|
3409
|
+
--tc-ansi-11: #e3b341;
|
|
3410
|
+
--tc-ansi-12: #79c0ff;
|
|
3411
|
+
--tc-ansi-13: #d2a8ff;
|
|
3412
|
+
--tc-ansi-14: #56d4dd;
|
|
3413
|
+
--tc-ansi-15: #f0f6fc;
|
|
3414
|
+
}
|
|
3415
|
+
:root[data-theme="light"] {
|
|
3416
|
+
--tc-ansi-0: #24292f;
|
|
3417
|
+
--tc-ansi-1: #cf222e;
|
|
3418
|
+
--tc-ansi-2: #116329;
|
|
3419
|
+
--tc-ansi-3: #4d2d00;
|
|
3420
|
+
--tc-ansi-4: #0969da;
|
|
3421
|
+
--tc-ansi-5: #8250df;
|
|
3422
|
+
--tc-ansi-6: #1b7c83;
|
|
3423
|
+
--tc-ansi-7: #6e7781;
|
|
3424
|
+
--tc-ansi-8: #57606a;
|
|
3425
|
+
--tc-ansi-9: #a40e26;
|
|
3426
|
+
--tc-ansi-10: #1a7f37;
|
|
3427
|
+
--tc-ansi-11: #633c01;
|
|
3428
|
+
--tc-ansi-12: #218bff;
|
|
3429
|
+
--tc-ansi-13: #a475f9;
|
|
3430
|
+
--tc-ansi-14: #3192aa;
|
|
3431
|
+
--tc-ansi-15: #8c959f;
|
|
3432
|
+
}
|
|
3433
|
+
#task-console-modal {
|
|
3434
|
+
position: fixed; inset: 0; z-index: 10900; display: none;
|
|
3435
|
+
align-items: center; justify-content: center;
|
|
3436
|
+
background: rgba(0, 0, 0, 0.68); padding: 18px;
|
|
3437
|
+
}
|
|
3438
|
+
#task-console-modal.visible { display: flex; }
|
|
3439
|
+
#task-console-title { flex: 1; min-width: 0; margin: 0; font-size: 1.05rem; font-weight: 600; overflow-wrap: anywhere; }
|
|
3440
|
+
.task-console-meta-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; color: var(--muted); font-size: var(--fs-meta-sm); }
|
|
3441
|
+
#task-console-meta { overflow-wrap: anywhere; }
|
|
3442
|
+
.tc-stall { color: var(--muted); }
|
|
3443
|
+
.tc-stall-warn { color: var(--warn); font-weight: 600; }
|
|
3444
|
+
#task-console-chip { cursor: pointer; font: inherit; }
|
|
3445
|
+
#task-console-failure {
|
|
3446
|
+
background: none; border: 0; padding: 0; color: var(--accent); cursor: pointer;
|
|
3447
|
+
font-size: var(--fs-meta-sm); text-decoration: underline; text-underline-offset: 3px;
|
|
3448
|
+
}
|
|
3449
|
+
#task-console-stop:hover:not(:disabled) { border-color: #f85149; color: #f85149; }
|
|
3450
|
+
/* "Load earlier" within reach while following the end; the one at the top
|
|
3451
|
+
of the output stays. */
|
|
3452
|
+
#task-console-earlier-top {
|
|
3453
|
+
background: none; border: 0; padding: 0; color: var(--accent); cursor: pointer;
|
|
3454
|
+
font-size: var(--fs-meta-sm); text-decoration: underline; text-underline-offset: 3px;
|
|
3455
|
+
}
|
|
3456
|
+
#task-console-earlier-top:disabled { color: var(--muted); text-decoration: none; cursor: default; }
|
|
3457
|
+
#task-console-earlier-top[hidden] { display: none; }
|
|
3458
|
+
/* Where the output of a command piped into tail stops until it ends. */
|
|
3459
|
+
#task-console-hold {
|
|
3460
|
+
margin: 8px 0 0; padding: 6px 10px; border-left: 3px solid var(--warn); border-radius: 4px;
|
|
3461
|
+
background: var(--bg2); color: var(--text); font-size: var(--fs-meta-sm);
|
|
3462
|
+
}
|
|
3463
|
+
#task-console-hold[hidden] { display: none; }
|
|
3464
|
+
.tc-hold-text { font-weight: 600; }
|
|
3465
|
+
.tc-hold-hint { color: var(--muted); margin-top: 2px; }
|
|
3466
|
+
/* Wider screens: the console ends above the composer's open task row, so
|
|
3467
|
+
the row's chip, clock and Stop stay in view (task-console.mjs sets the
|
|
3468
|
+
bottom). Phones keep the full-screen console. */
|
|
3469
|
+
#task-console-modal.tc-above-panel .task-console-dialog { height: 100%; }
|
|
3470
|
+
#task-console-scroll {
|
|
3471
|
+
flex: 1; overflow: auto; overscroll-behavior: contain; padding: 8px 14px 28px;
|
|
3472
|
+
background: var(--bg);
|
|
3473
|
+
}
|
|
3474
|
+
#task-console-earlier {
|
|
3475
|
+
display: block; margin: 0 auto 8px; border: 1px solid var(--border); background: var(--bg2);
|
|
3476
|
+
color: var(--text); border-radius: 999px; padding: 3px 14px; font-size: var(--fs-meta-sm); cursor: pointer;
|
|
3477
|
+
}
|
|
3478
|
+
#task-console-earlier:disabled { opacity: 0.6; cursor: default; }
|
|
3479
|
+
#task-console-earlier[hidden] { display: none; }
|
|
3480
|
+
#task-console-body {
|
|
3481
|
+
margin: 0; font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
|
|
3482
|
+
font-size: var(--fs-meta-sm); line-height: 1.45; color: var(--text);
|
|
3483
|
+
white-space: pre-wrap; overflow-wrap: anywhere;
|
|
3484
|
+
}
|
|
3485
|
+
.tc-line { min-height: 1.45em; }
|
|
3486
|
+
.tc-line-partial { opacity: 0.85; }
|
|
3487
|
+
.tc-line-target { background: rgba(248, 81, 73, 0.16); border-radius: 3px; }
|
|
3488
|
+
.tc-line-count {
|
|
3489
|
+
display: inline-block; margin-left: 8px; padding: 0 6px; border-radius: 999px;
|
|
3490
|
+
border: 1px solid var(--border); color: var(--muted); font-size: 0.85em; user-select: none;
|
|
3491
|
+
}
|
|
3492
|
+
.tc-bold { font-weight: 700; }
|
|
3493
|
+
.tc-dim { opacity: 0.65; }
|
|
3494
|
+
.tc-italic { font-style: italic; }
|
|
3495
|
+
.tc-underline { text-decoration: underline; }
|
|
3496
|
+
.tc-fg-0 { color: var(--tc-ansi-0); }
|
|
3497
|
+
.tc-fg-1 { color: var(--tc-ansi-1); }
|
|
3498
|
+
.tc-fg-2 { color: var(--tc-ansi-2); }
|
|
3499
|
+
.tc-fg-3 { color: var(--tc-ansi-3); }
|
|
3500
|
+
.tc-fg-4 { color: var(--tc-ansi-4); }
|
|
3501
|
+
.tc-fg-5 { color: var(--tc-ansi-5); }
|
|
3502
|
+
.tc-fg-6 { color: var(--tc-ansi-6); }
|
|
3503
|
+
.tc-fg-7 { color: var(--tc-ansi-7); }
|
|
3504
|
+
.tc-fg-8 { color: var(--tc-ansi-8); }
|
|
3505
|
+
.tc-fg-9 { color: var(--tc-ansi-9); }
|
|
3506
|
+
.tc-fg-10 { color: var(--tc-ansi-10); }
|
|
3507
|
+
.tc-fg-11 { color: var(--tc-ansi-11); }
|
|
3508
|
+
.tc-fg-12 { color: var(--tc-ansi-12); }
|
|
3509
|
+
.tc-fg-13 { color: var(--tc-ansi-13); }
|
|
3510
|
+
.tc-fg-14 { color: var(--tc-ansi-14); }
|
|
3511
|
+
.tc-fg-15 { color: var(--tc-ansi-15); }
|
|
3512
|
+
.tc-bg-0 { background: var(--tc-ansi-0); }
|
|
3513
|
+
.tc-bg-1 { background: var(--tc-ansi-1); }
|
|
3514
|
+
.tc-bg-2 { background: var(--tc-ansi-2); }
|
|
3515
|
+
.tc-bg-3 { background: var(--tc-ansi-3); }
|
|
3516
|
+
.tc-bg-4 { background: var(--tc-ansi-4); }
|
|
3517
|
+
.tc-bg-5 { background: var(--tc-ansi-5); }
|
|
3518
|
+
.tc-bg-6 { background: var(--tc-ansi-6); }
|
|
3519
|
+
.tc-bg-7 { background: var(--tc-ansi-7); }
|
|
3520
|
+
.tc-bg-8 { background: var(--tc-ansi-8); }
|
|
3521
|
+
.tc-bg-9 { background: var(--tc-ansi-9); }
|
|
3522
|
+
.tc-bg-10 { background: var(--tc-ansi-10); }
|
|
3523
|
+
.tc-bg-11 { background: var(--tc-ansi-11); }
|
|
3524
|
+
.tc-bg-12 { background: var(--tc-ansi-12); }
|
|
3525
|
+
.tc-bg-13 { background: var(--tc-ansi-13); }
|
|
3526
|
+
.tc-bg-14 { background: var(--tc-ansi-14); }
|
|
3527
|
+
.tc-bg-15 { background: var(--tc-ansi-15); }
|
|
3528
|
+
/* Inverse text (SGR 7) with a side left unset: the console's own colours, swapped. */
|
|
3529
|
+
.tc-fg-inverse { color: var(--bg); }
|
|
3530
|
+
.tc-bg-inverse { background: var(--text); }
|
|
3531
|
+
#task-console-status { color: var(--muted); padding: 10px 0; font-size: var(--fs-meta-sm); }
|
|
3532
|
+
#task-console-status[hidden] { display: none; }
|
|
3533
|
+
#task-console-jump {
|
|
3534
|
+
position: absolute; left: 50%; bottom: 14px; transform: translateX(-50%);
|
|
3535
|
+
border: 1px solid var(--accent); background: var(--bg3); color: var(--accent);
|
|
3536
|
+
border-radius: 999px; padding: 5px 14px; font-size: var(--fs-meta-sm); cursor: pointer;
|
|
3537
|
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
|
|
3538
|
+
}
|
|
3539
|
+
#task-console-jump[hidden] { display: none; }
|
|
3540
|
+
/* Rows in the composer panel: the console opener, the health hints, the
|
|
3541
|
+
ended rows. */
|
|
3542
|
+
.bg-task-desc.task-console-open {
|
|
3543
|
+
display: block; -webkit-line-clamp: unset; background: none; border: 0; padding: 0; margin: 0;
|
|
3544
|
+
font: inherit; color: var(--text); text-align: left; cursor: pointer;
|
|
3545
|
+
text-decoration: underline; text-decoration-color: transparent; text-underline-offset: 3px;
|
|
3546
|
+
}
|
|
3547
|
+
.bg-task-desc.task-console-open:hover, .bg-task-desc.task-console-open:focus-visible {
|
|
3548
|
+
color: var(--accent); text-decoration-color: currentColor; outline: none;
|
|
3549
|
+
}
|
|
3550
|
+
.bg-task-desc-no-output { cursor: help; }
|
|
3551
|
+
.bg-task-steps { color: var(--muted); flex-shrink: 0; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
3552
|
+
.bg-task-stall { color: var(--muted); flex-shrink: 0; white-space: nowrap; }
|
|
3553
|
+
.bg-task-stall[hidden] { display: none; }
|
|
3554
|
+
.bg-task-stall-warn { color: var(--warn); font-weight: 600; }
|
|
3555
|
+
.bg-task-chip {
|
|
3556
|
+
flex-shrink: 0; border: 1px solid var(--success); color: var(--success); background: transparent;
|
|
3557
|
+
border-radius: 999px; padding: 0 7px; font-size: var(--fs-meta-sm); white-space: nowrap;
|
|
3558
|
+
font-variant-numeric: tabular-nums;
|
|
3559
|
+
}
|
|
3560
|
+
.bg-task-chip[hidden] { display: none; }
|
|
3561
|
+
.bg-task-chip-fail { border-color: var(--danger); color: var(--danger); }
|
|
3562
|
+
.bg-task-row-ended { opacity: 0.62; }
|
|
3563
|
+
.bg-task-row-ended:hover, .bg-task-row-ended:focus-within { opacity: 0.9; }
|
|
3564
|
+
.bg-task-outcome { flex-shrink: 0; font-weight: 700; }
|
|
3565
|
+
.bg-task-outcome-ok { color: var(--success); }
|
|
3566
|
+
.bg-task-outcome-fail { color: var(--danger); }
|
|
3567
|
+
.bg-task-dismiss {
|
|
3568
|
+
flex-shrink: 0; background: transparent; border: 1px solid var(--border); color: var(--muted);
|
|
3569
|
+
border-radius: 8px; width: 26px; height: 24px; line-height: 1; cursor: pointer; font-size: 1rem;
|
|
3570
|
+
}
|
|
3571
|
+
.bg-task-dismiss:hover { color: var(--text); border-color: var(--text); }
|
|
3572
|
+
@media (max-width: 680px) {
|
|
3573
|
+
#task-console-modal { padding: 0; }
|
|
3574
|
+
#task-console-modal .subagent-viewer-dialog { width: 100%; height: 100%; border: 0; border-radius: 0; }
|
|
3575
|
+
#task-console-scroll { padding: 8px 10px 28px; }
|
|
3576
|
+
}
|
|
3382
3577
|
/* Preview cards: same row grammar as a background task, with a link-shaped
|
|
3383
3578
|
primary action. Shared by the composer panel and the settings list. */
|
|
3384
3579
|
.preview-row {
|
|
@@ -3549,6 +3744,8 @@
|
|
|
3549
3744
|
.relay-event-time { flex: 0 0 auto; }
|
|
3550
3745
|
.relay-event-lines { margin: 2px 0 4px; padding-left: 18px; }
|
|
3551
3746
|
.relay-event-line { margin: 2px 0; overflow-wrap: anywhere; white-space: pre-wrap; }
|
|
3747
|
+
/* The relay's note to carry on after a restart (relay-restart-note-view.mjs). */
|
|
3748
|
+
.relay-restart-note-text { padding-left: 0; overflow-wrap: anywhere; white-space: pre-wrap; }
|
|
3552
3749
|
.relay-event-line.relay-event-question-asked,
|
|
3553
3750
|
.relay-event-line.relay-event-error,
|
|
3554
3751
|
.relay-event-line.relay-event-task-failed,
|
|
@@ -7005,6 +7202,22 @@
|
|
|
7005
7202
|
>
|
|
7006
7203
|
</div>
|
|
7007
7204
|
<div class="settings-help" style="margin-top: 6px;">A conversation only scrolls by itself while you are at its end. Anywhere else it stays where you are reading, and the round button above the composer shows that something new or a question is waiting below. Your own message is the exception: sending one takes you to the end. Switch this off to stay put then too. Saved in this browser.</div>
|
|
7205
|
+
<div class="settings-row settings-row-column" style="margin-top: 12px; margin-bottom: 0;">
|
|
7206
|
+
<label for="conversation-window-retention-slider">Messages kept per conversation for switching back <span id="conversation-window-retention-value">200 messages</span></label>
|
|
7207
|
+
<input
|
|
7208
|
+
id="conversation-window-retention-slider"
|
|
7209
|
+
type="range"
|
|
7210
|
+
min="100"
|
|
7211
|
+
max="1100"
|
|
7212
|
+
step="100"
|
|
7213
|
+
value="200"
|
|
7214
|
+
aria-label="Messages kept per conversation for switching back"
|
|
7215
|
+
aria-valuetext="200 messages"
|
|
7216
|
+
oninput="previewConversationWindowRetention(this.value)"
|
|
7217
|
+
onchange="updateConversationWindowRetention(this.value)"
|
|
7218
|
+
>
|
|
7219
|
+
</div>
|
|
7220
|
+
<div class="settings-help" style="margin-top: 6px;">A conversation you switch away from and back to opens where you were reading, with the older messages you had loaded. This many are kept around that place for each of the last eight conversations; No limit keeps them all, at the cost of memory. Saved in this browser.</div>
|
|
7008
7221
|
<div class="settings-row settings-row-column" style="margin-top: 12px; margin-bottom: 0;">
|
|
7009
7222
|
<label for="turn-ceiling-slider">Max turn duration <span id="turn-ceiling-value">No limit</span></label>
|
|
7010
7223
|
<input
|
|
@@ -7035,6 +7248,45 @@
|
|
|
7035
7248
|
>
|
|
7036
7249
|
</div>
|
|
7037
7250
|
<div class="settings-help" style="margin-top: 6px;">How long background tasks may keep a session alive after its turn ends: backgrounded commands, subagents and monitors on Claude; background agents and detached shells on Copilot's SDK engine. When the limit is reached the tasks are stopped. Set to 0 for no limit — tasks then run until they finish or you stop them from the composer panel.</div>
|
|
7251
|
+
<div class="settings-row settings-row-column" style="margin-top: 12px; margin-bottom: 0;">
|
|
7252
|
+
<label for="task-stall-minutes-slider">Mark a background task as quiet after <span id="task-stall-minutes-value">10 min</span></label>
|
|
7253
|
+
<input
|
|
7254
|
+
id="task-stall-minutes-slider"
|
|
7255
|
+
type="range"
|
|
7256
|
+
min="1"
|
|
7257
|
+
max="120"
|
|
7258
|
+
step="1"
|
|
7259
|
+
value="10"
|
|
7260
|
+
aria-label="Minutes without output before a background task is marked as quiet"
|
|
7261
|
+
oninput="previewTaskStallMinutesSetting(this.value)"
|
|
7262
|
+
onchange="updateTaskStallMinutesSetting(this.value)"
|
|
7263
|
+
>
|
|
7264
|
+
</div>
|
|
7265
|
+
<div class="settings-help" style="margin-top: 6px;">A shell or Monitor task that has written no output for this long gets an amber "no output for N min" note in the composer panel and its console.</div>
|
|
7266
|
+
<div class="settings-row" style="margin-top: 12px; margin-bottom: 0;">
|
|
7267
|
+
<label for="restart-resume-toggle">After a relay restart, tell interrupted sessions to carry on</label>
|
|
7268
|
+
<input
|
|
7269
|
+
id="restart-resume-toggle"
|
|
7270
|
+
type="checkbox"
|
|
7271
|
+
onchange="updateRestartResumeSettingFromToggle(this.checked)"
|
|
7272
|
+
aria-label="After a relay restart, tell interrupted sessions to carry on"
|
|
7273
|
+
>
|
|
7274
|
+
</div>
|
|
7275
|
+
<div class="settings-help" style="margin-top: 6px;">Most turns a restart interrupts run again by themselves. A turn that cannot (the agent had already taken the message, or had started on its own after a background task) just stops. With this on, such a session gets one relay note once the relay is back, saying when it restarted and asking the agent to check what it was doing and carry on. Your message is never sent again; idle, archived and usage-limit-paused sessions get nothing. Off by default; a setting of this relay.</div>
|
|
7276
|
+
<div class="settings-row" style="margin-top: 12px; margin-bottom: 0;">
|
|
7277
|
+
<label for="relay-restart-max-wait-select">Ask after a queued relay restart has waited</label>
|
|
7278
|
+
<select id="relay-restart-max-wait-select" onchange="updateRelayRestartMaxWaitSetting(this.value)" aria-label="Ask after a queued relay restart has waited">
|
|
7279
|
+
<option value="5">5 min</option>
|
|
7280
|
+
<option value="10">10 min</option>
|
|
7281
|
+
<option value="15">15 min</option>
|
|
7282
|
+
<option value="30" selected>30 min</option>
|
|
7283
|
+
<option value="60">1 h</option>
|
|
7284
|
+
<option value="120">2 h</option>
|
|
7285
|
+
<option value="240">4 h</option>
|
|
7286
|
+
<option value="0">No limit</option>
|
|
7287
|
+
</select>
|
|
7288
|
+
</div>
|
|
7289
|
+
<div class="settings-help" style="margin-top: 6px;">A queued relay restart waits for the running turns, and new messages wait for the restart. After this long the banner asks whether to restart now, keep waiting or cancel; it never restarts while the turns run, and still restarts as soon as they end. No limit waits until the turns end.</div>
|
|
7038
7290
|
<div class="settings-row" style="margin-top: 12px; margin-bottom: 0;">
|
|
7039
7291
|
<label for="subagent-record-retention-select">Keep sub-agent tool output</label>
|
|
7040
7292
|
<select id="subagent-record-retention-select" onchange="updateSubagentRecordRetentionSetting(this.value)" aria-label="Keep sub-agent tool output">
|
|
@@ -7372,6 +7624,18 @@
|
|
|
7372
7624
|
<option value="off">Off — no attribution</option>
|
|
7373
7625
|
</select>
|
|
7374
7626
|
<div id="claude-attribution-example" class="settings-help"></div>
|
|
7627
|
+
<label for="claude-background-command-limit-slider" style="margin-top: 8px;">Claude background command limit <span id="claude-background-command-limit-value">2 h</span></label>
|
|
7628
|
+
<input
|
|
7629
|
+
id="claude-background-command-limit-slider"
|
|
7630
|
+
type="range"
|
|
7631
|
+
min="30"
|
|
7632
|
+
max="480"
|
|
7633
|
+
step="30"
|
|
7634
|
+
value="120"
|
|
7635
|
+
aria-label="Claude background command limit in minutes"
|
|
7636
|
+
oninput="window.previewClaudeBackgroundCommandLimit?.(this.value)"
|
|
7637
|
+
>
|
|
7638
|
+
<div class="settings-help">How long a command Claude runs in the background may take before Claude Code stops it, unless Claude gives it its own timeout (up to 8 h). Commands in the foreground move to the background after 2 min. Applies to Claude sessions started after you save; a running session keeps its limit until it restarts.</div>
|
|
7375
7639
|
<div id="claude-settings-status" class="settings-help settings-provider-status">Loading Claude settings…</div>
|
|
7376
7640
|
<div style="display: flex; gap: 8px; margin-top: 8px;">
|
|
7377
7641
|
<button id="claude-save-btn" type="button" class="summary-btn" onclick="saveClaudeSettings()">Save Claude settings</button>
|
|
@@ -7867,6 +8131,10 @@
|
|
|
7867
8131
|
<label for="push-event-host-suspend-toggle">Host suspend fired or was dropped</label>
|
|
7868
8132
|
<input id="push-event-host-suspend-toggle" type="checkbox" onchange="updatePushPreferencesFromControls()" checked disabled>
|
|
7869
8133
|
</div>
|
|
8134
|
+
<div class="settings-row" style="width: 100%; margin-top: 6px; margin-bottom: 0;">
|
|
8135
|
+
<label for="push-event-relay-restart-toggle">Queued relay restart waited its maximum time</label>
|
|
8136
|
+
<input id="push-event-relay-restart-toggle" type="checkbox" onchange="updatePushPreferencesFromControls()" checked disabled>
|
|
8137
|
+
</div>
|
|
7870
8138
|
<label style="margin-top: 10px;">Devices</label>
|
|
7871
8139
|
<div id="push-device-list" style="width: 100%;"></div>
|
|
7872
8140
|
<div class="settings-help">Notifications are only sent while no device has the app in the foreground. Content defaults to generic text; message previews are opt-in per device.</div>
|
|
@@ -8029,6 +8297,38 @@
|
|
|
8029
8297
|
</div>
|
|
8030
8298
|
</div>
|
|
8031
8299
|
|
|
8300
|
+
<!-- Task console: a shell or Monitor task's output, live while it runs
|
|
8301
|
+
(task-console.mjs). -->
|
|
8302
|
+
<div id="task-console-modal" aria-hidden="true">
|
|
8303
|
+
<div class="subagent-viewer-dialog task-console-dialog" role="dialog" aria-modal="true" aria-labelledby="task-console-title">
|
|
8304
|
+
<div class="subagent-viewer-header">
|
|
8305
|
+
<div class="subagent-viewer-title-row">
|
|
8306
|
+
<span aria-hidden="true">⌨️</span>
|
|
8307
|
+
<h2 id="task-console-title">Task</h2>
|
|
8308
|
+
<button id="task-console-stop" class="subagent-viewer-btn" type="button" hidden>Stop</button>
|
|
8309
|
+
<button class="subagent-viewer-btn task-console-close" type="button" aria-label="Close">×</button>
|
|
8310
|
+
</div>
|
|
8311
|
+
<div class="task-console-meta-row">
|
|
8312
|
+
<span id="task-console-meta"></span>
|
|
8313
|
+
<span id="task-console-stall" class="tc-stall" hidden></span>
|
|
8314
|
+
<button id="task-console-chip" class="bg-task-chip" type="button" hidden></button>
|
|
8315
|
+
<button id="task-console-failure" type="button" hidden>first failure</button>
|
|
8316
|
+
<button id="task-console-earlier-top" type="button" hidden>Load earlier</button>
|
|
8317
|
+
</div>
|
|
8318
|
+
</div>
|
|
8319
|
+
<div id="task-console-scroll">
|
|
8320
|
+
<button id="task-console-earlier" type="button" hidden>Load earlier</button>
|
|
8321
|
+
<pre id="task-console-body"></pre>
|
|
8322
|
+
<div id="task-console-hold" role="note" hidden>
|
|
8323
|
+
<div class="tc-hold-text"></div>
|
|
8324
|
+
<div class="tc-hold-hint"></div>
|
|
8325
|
+
</div>
|
|
8326
|
+
<div id="task-console-status" hidden></div>
|
|
8327
|
+
</div>
|
|
8328
|
+
<button id="task-console-jump" type="button" hidden>Jump to end ↓</button>
|
|
8329
|
+
</div>
|
|
8330
|
+
</div>
|
|
8331
|
+
|
|
8032
8332
|
<!-- Fullscreen file viewer. The content fills the screen; the chrome bars
|
|
8033
8333
|
float over it and a tap on the content shows or hides them. -->
|
|
8034
8334
|
<div id="file-preview-modal" aria-hidden="true">
|
package/server/relay-tools.md
CHANGED
|
@@ -22,6 +22,8 @@ Note: shutdown is queued and only completes when the current turn finishes, so i
|
|
|
22
22
|
|
|
23
23
|
Use `restart: true` in the request body when the user wants a real relay restart rather than a plain shutdown. Example request body: `{ "reason": "manual-restart", "requestedBy": "localhost-api", "restart": true }`.
|
|
24
24
|
|
|
25
|
+
Never pass `immediate` in that request body: it restarts without waiting for the running turns, which only the user may decide, with Restart now in the web UI.
|
|
26
|
+
|
|
25
27
|
## Preview servers
|
|
26
28
|
|
|
27
29
|
Publish a local web server or a static directory on a public preview URL so the user can open it from any device, or list/close existing previews. Use this whenever the user wants to see, try, or share something with a web UI — never ask them to open localhost or forward a port. For a dev server: call this FIRST with {action:"create", port} to get the basePath, then start the server configured to serve under that basePath (Vite --base, Next basePath, Express mount prefix); the proxy forwards X-Forwarded-Prefix but does not rewrite bodies, so root-absolute asset paths will not work. For plain files or a build output: pass {action:"create", dir} instead and the relay serves the directory itself — no dev server needed. Always tell the user the returned URL and that the link is public: anyone who has it can reach the app without logging in. Publishing the same port or directory again in this conversation returns the same link. Previews never expire and survive a relay restart (a port preview until the machine reboots); close them with {action:"close"} when the user is done.
|
|
@@ -39,8 +41,12 @@ This relay itself is listed with "self": true when its owner switched agent sess
|
|
|
39
41
|
- `create_session` starts a session with any provider that `relay_info` lists: a local one in a folder ("cwd"), or provider "claude-cloud" with "repo" (a GitHub URL or owner/repo) and an optional "branch". At most 4 sessions you started may work at the same time; a fifth is refused until one finishes.
|
|
40
42
|
- The first `create_session` on this relay shows the user an approval card, once per conversation. A session that an agent created cannot create sessions itself (one level only).
|
|
41
43
|
- Sessions you start here are grouped under this session in the user's sidebar. `list_sessions` lists in the sidebar's order (a group sorts by its newest session, head first) and shows the group as a "group" field (id, name, role); `read_session` adds the group's live members with their state. Agents only read groups, the user arranges them.
|
|
44
|
+
- A "model" for `create_session` or `send` must be an id `relay_info` lists for the provider (Claude also takes "opus", "sonnet", "haiku"). An unknown id is refused with the close matches ("unknown model 'gpt-5-4' for Copilot; did you mean 'gpt-5.4'?") and nothing is created or sent: pick from the list instead of guessing. If the answer has a "modelNote" saying the model could not be checked, the relay had no model list yet.
|
|
42
45
|
- A new session knows nothing of this conversation: write a self-contained prompt. A Claude Cloud session works on a fresh clone in a sandbox at Anthropic, so name the branch it should work on and push.
|
|
43
46
|
- Follow each session with `wait` (the message id comes back from `create_session` and `send`) and `read_session`; "wait_seconds" is limited to the maximum set on the relay (default 600).
|
|
44
47
|
- Sessions run on the relay owner's accounts and cost their usage: start only what the task needs.
|
|
48
|
+
- `read_task_output` ("session", "task_id" from `read_session`'s tasks, optional "lines") reads the end of a background shell or Monitor task's output, also for an hour after it ended: the last lines, minutes since its last output, and pass/fail counts for a test run. Credentials are scrubbed out.
|
|
49
|
+
|
|
50
|
+
On this relay and on a paired one, `subscribe` wakes you when something happens in the sessions you watch, instead of polling them. Leave "events" out unless you need more: the default (question_asked, error, turn_end, subagent_finished, task_failed, stalled, all_idle) wakes you for questions, errors, replies, finished sub-agents, failed tasks (a non-zero exit included), stalls and all_idle, but not for each shell command that ended well. To get those too, pass that list plus "task_finished" (and "task_started" for the starts); each one costs you a turn, so do it only if you must follow every command.
|
|
45
51
|
|
|
46
52
|
Write actions may first wait for the user's approval. If a result lists `pendingQuestions`, the other agent is waiting for an answer: ask the user (unless they told you to decide), then pass it on with `answer_question`. Always tell the user which relay and session you worked on.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { createShareVisibilityStatements } from './share-visibility-statements.mjs';
|
|
4
4
|
import { createMessagePinStatements } from './message-pin-statements.mjs';
|
|
5
|
+
import { createConversationHistoryPageStatements } from '../services/conversation-history-page.mjs';
|
|
5
6
|
import { sessionWorkerProviderSqlList } from '../../shared/provider-routing.mjs';
|
|
6
7
|
|
|
7
8
|
export function createMessageRepository(db) {
|
|
@@ -27,6 +28,8 @@ export function createMessageRepository(db) {
|
|
|
27
28
|
// messages
|
|
28
29
|
getMessages: db.prepare(`SELECT * FROM messages WHERE conversation_id = ? ORDER BY timestamp ASC`),
|
|
29
30
|
getSharedMessages: shareVisibility.getSharedMessages,
|
|
31
|
+
// One history page in SQL (conversation-history-page.mjs).
|
|
32
|
+
conversationHistoryPage: createConversationHistoryPageStatements(db),
|
|
30
33
|
getMessageByConversation: db.prepare(`SELECT * FROM messages WHERE id = ? AND conversation_id = ? LIMIT 1`),
|
|
31
34
|
setMessageShareVisibility: shareVisibility.setMessageShareVisibility,
|
|
32
35
|
listHiddenMessages: shareVisibility.listHiddenMessages,
|
|
@@ -34,6 +37,9 @@ export function createMessageRepository(db) {
|
|
|
34
37
|
listPinnedMessages: messagePins.listPinnedMessages,
|
|
35
38
|
countPinnedMessages: messagePins.countPinnedMessages,
|
|
36
39
|
getLatestConversationModel: db.prepare(`SELECT model FROM messages WHERE conversation_id = ? AND model IS NOT NULL AND model != '' ORDER BY timestamp DESC LIMIT 1`),
|
|
40
|
+
// The model of the conversation's latest user turn: one of the
|
|
41
|
+
// fallbacks for a send that names no model.
|
|
42
|
+
getLatestUserMessageModel: db.prepare(`SELECT model FROM messages WHERE conversation_id = ? AND role = 'user' AND model IS NOT NULL AND model != '' ORDER BY timestamp DESC LIMIT 1`),
|
|
37
43
|
getRecentMessagesDesc: db.prepare(`SELECT id, role, text, timestamp FROM messages WHERE conversation_id = ? ORDER BY timestamp DESC LIMIT ?`),
|
|
38
44
|
insertMsg: db.prepare(`INSERT INTO messages (id, conversation_id, role, text, model, mode, attachments, timestamp, model_requested, model_actual, model_origin) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
|
|
39
45
|
// Provenance is derived from the authenticated responder identity at
|
|
@@ -116,6 +122,11 @@ export function createMessageRepository(db) {
|
|
|
116
122
|
ORDER BY captured_at DESC
|
|
117
123
|
LIMIT 1
|
|
118
124
|
`),
|
|
125
|
+
getMessageUsageSnapshotByResponse: db.prepare(`
|
|
126
|
+
SELECT *
|
|
127
|
+
FROM message_usage_snapshots
|
|
128
|
+
WHERE response_message_id = ? AND conversation_id = ?
|
|
129
|
+
`),
|
|
119
130
|
listMessageUsageSnapshotsByConversation: db.prepare(`
|
|
120
131
|
SELECT *
|
|
121
132
|
FROM message_usage_snapshots
|
|
@@ -174,6 +174,10 @@ export function createQuestionRepository(db) {
|
|
|
174
174
|
WHERE response_message_id = ?
|
|
175
175
|
ORDER BY created_at ASC, id ASC
|
|
176
176
|
`),
|
|
177
|
+
// Every card of a conversation, for the shared view's link redaction:
|
|
178
|
+
// a paged transcript hydrates only its page's cards, yet a reply on
|
|
179
|
+
// the page may quote the link of a card outside it.
|
|
180
|
+
listPreviewCardJsonByConversation: db.prepare(`SELECT preview_json FROM preview_cards WHERE conversation_id = ?`),
|
|
177
181
|
deleteConvPreviewCards: db.prepare(`DELETE FROM preview_cards WHERE conversation_id = ?`),
|
|
178
182
|
|
|
179
183
|
// relay boards
|
|
@@ -27,7 +27,9 @@ function parseOrigin(json) {
|
|
|
27
27
|
|
|
28
28
|
export function createRemoteRelayRepository(db) {
|
|
29
29
|
const hasUnlock = db.prepare(`SELECT 1 FROM conversation_remote_relay_unlocks WHERE conversation_id = ? AND remote_relay_id = ?`);
|
|
30
|
-
|
|
30
|
+
// Two unlocks can share a millisecond (Windows' clock is coarse); the rowid
|
|
31
|
+
// keeps them in the order they were recorded rather than by relay id.
|
|
32
|
+
const listUnlocks = db.prepare(`SELECT remote_relay_id, message_id, created_at FROM conversation_remote_relay_unlocks WHERE conversation_id = ? ORDER BY created_at ASC, rowid ASC`);
|
|
31
33
|
const insertUnlock = db.prepare(`INSERT OR IGNORE INTO conversation_remote_relay_unlocks (conversation_id, remote_relay_id, message_id, created_at) VALUES (?, ?, ?, ?)`);
|
|
32
34
|
// The relay's unlocks and the approvals stored under its id (`<id>:watch`).
|
|
33
35
|
const deleteUnlocksForRelay = db.prepare(`
|
|
@@ -64,7 +66,7 @@ export function createRemoteRelayRepository(db) {
|
|
|
64
66
|
hasUnlock(conversationId, remoteRelayId) {
|
|
65
67
|
return !!hasUnlock.get(String(conversationId || ''), String(remoteRelayId || ''));
|
|
66
68
|
},
|
|
67
|
-
/** `[{ remoteRelayId, messageId, createdAt }]`, oldest first. */
|
|
69
|
+
/** `[{ remoteRelayId, messageId, createdAt }]`, oldest first, ties in the order recorded. */
|
|
68
70
|
listUnlocks(conversationId) {
|
|
69
71
|
return listUnlocks.all(String(conversationId || '')).map((row) => ({
|
|
70
72
|
remoteRelayId: row.remote_relay_id,
|