@oar-sh/oar 0.9.9 → 0.9.10

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.
Files changed (85) hide show
  1. package/CHANGELOG.md +247 -0
  2. package/README.md +78 -9
  3. package/bin/bat/oar.bat +46 -46
  4. package/package.json +2 -2
  5. package/server/README.md +286 -3
  6. package/server/claude-worker/claude-remote-relay-tool.mjs +10 -0
  7. package/server/claude-worker/claude-session-process.mjs +35 -1
  8. package/server/claude-worker/claude-turn-publisher.mjs +3 -0
  9. package/server/claude-worker/sdk-message-normalizer.mjs +31 -4
  10. package/server/claude-worker/subagent-timeline-recorder.mjs +572 -0
  11. package/server/copilot-worker/copilot-sdk-session-process.mjs +3 -1
  12. package/server/cursor-worker/cursor-sdk-adapter.mjs +56 -2
  13. package/server/db-schema.mjs +36 -0
  14. package/server/migrations/0009-subagent-timeline.mjs +135 -0
  15. package/server/migrations/0010-subagent-timeline-pruning.mjs +89 -0
  16. package/server/migrations/0011-relay-event-subscriptions.mjs +125 -0
  17. package/server/migrations/0012-cross-relay-event-subscriptions.mjs +116 -0
  18. package/server/public/app/agent-exchange-fold-view.mjs +248 -0
  19. package/server/public/app/api-client.js +61 -0
  20. package/server/public/app/archive-confirm.mjs +28 -0
  21. package/server/public/app/ask-user-view.js +3 -0
  22. package/server/public/app/attachments-view.js +516 -6
  23. package/server/public/app/background-tasks-view.mjs +18 -26
  24. package/server/public/app/bootstrap.js +186 -35
  25. package/server/public/app/chat-actions-menu-state.mjs +116 -0
  26. package/server/public/app/claude-cloud-conversation-ui.js +10 -22
  27. package/server/public/app/composer-control-state.mjs +16 -0
  28. package/server/public/app/composer-keyboard.mjs +54 -0
  29. package/server/public/app/conversation-view.js +361 -85
  30. package/server/public/app/explorer-edit-state.mjs +68 -0
  31. package/server/public/app/host-suspend-ui.js +6 -3
  32. package/server/public/app/journal-view.js +146 -26
  33. package/server/public/app/no-conversation-state.mjs +58 -0
  34. package/server/public/app/provider-readiness.mjs +120 -0
  35. package/server/public/app/relay-board-view.js +3 -0
  36. package/server/public/app/relay-event-subscriptions-ui.js +184 -0
  37. package/server/public/app/relay-event-view.mjs +234 -0
  38. package/server/public/app/settings-modal.js +53 -1
  39. package/server/public/app/socket-handlers.js +59 -28
  40. package/server/public/app/store.js +19 -2
  41. package/server/public/app/subagent-row.mjs +138 -0
  42. package/server/public/app/subagent-timeline-view.mjs +186 -0
  43. package/server/public/app/subagent-viewer-watch.mjs +50 -0
  44. package/server/public/app/subagent-viewer.mjs +534 -0
  45. package/server/public/app/task-row-format.mjs +29 -0
  46. package/server/public/app/thinking-follow.js +4 -0
  47. package/server/public/app/transcript-jump-bottom-view.mjs +290 -0
  48. package/server/public/app/transcript-jump-bottom.mjs +99 -0
  49. package/server/public/app/transcript-reload-freshness.mjs +25 -0
  50. package/server/public/app/update-ui.js +1 -1
  51. package/server/public/app/viewer-history.mjs +5 -3
  52. package/server/public/index.html +396 -5
  53. package/server/repositories/message-repository.mjs +29 -3
  54. package/server/repositories/question-repository.mjs +9 -3
  55. package/server/repositories/remote-relay-repository.mjs +6 -2
  56. package/server/repositories/subagent-timeline-statements.mjs +170 -0
  57. package/server/routes/ask-user-routes.mjs +3 -0
  58. package/server/routes/messages-routes.mjs +515 -50
  59. package/server/routes/relay-event-routes.mjs +157 -0
  60. package/server/routes/remote-relay-routes.mjs +15 -0
  61. package/server/routes/sessions-routes.mjs +72 -0
  62. package/server/server-runtime.mjs +245 -62
  63. package/server/services/explorer-entry-name.mjs +26 -0
  64. package/server/services/explorer-mutations.mjs +122 -0
  65. package/server/services/preview-proxy-server.mjs +41 -23
  66. package/server/services/push-dispatch-service.mjs +18 -0
  67. package/server/services/relay-event-link-service.mjs +289 -0
  68. package/server/services/relay-event-service.mjs +1935 -0
  69. package/server/services/relay-shutdown-gate.mjs +31 -0
  70. package/server/services/remote-relay-caller-context.mjs +52 -7
  71. package/server/services/remote-relay-dispatcher.mjs +494 -2
  72. package/server/services/remote-relay-registry-service.mjs +11 -1
  73. package/server/services/session-activity-tracker.mjs +487 -0
  74. package/server/services/session-history-refresh-service.mjs +7 -0
  75. package/server/services/stale-attempt-log-limiter.mjs +96 -0
  76. package/server/services/stale-recovery-clock-guard.mjs +57 -0
  77. package/server/services/stale-subagent-run-sweep.mjs +109 -0
  78. package/server/services/subagent-record-retention.mjs +164 -0
  79. package/server/services/subagent-timeline-service.mjs +328 -0
  80. package/server/services/subagent-viewer-rooms.mjs +119 -0
  81. package/server/services/upgrade-socket-guard.mjs +16 -0
  82. package/shared/relay-events.mjs +371 -0
  83. package/shared/remote-relay-contract.mjs +157 -5
  84. package/shared/subagent-record-retention.mjs +31 -0
  85. package/shared/subagent-timeline.mjs +79 -0
@@ -587,6 +587,8 @@
587
587
  /* The display rule above outranks the UA [hidden] rule, same trap as the
588
588
  other [hidden] overrides in this file. */
589
589
  #pinned-messages-btn[hidden] { display: none; }
590
+ /* Same trap: without this the ⋯ trigger stays drawn as a dead button. */
591
+ #chat-actions-menu-btn[hidden] { display: none; }
590
592
  #cli-dot {
591
593
  width: 12px;
592
594
  height: 12px;
@@ -868,7 +870,8 @@
868
870
  /* The conversation context menu (context-menu.mjs): right-click / long press. */
869
871
  .context-menu {
870
872
  position: fixed;
871
- z-index: 1200;
873
+ /* Above every modal: the explorer (10001, 12400 as a CWD picker) opens one too. */
874
+ z-index: 13000;
872
875
  min-width: 200px;
873
876
  max-width: min(320px, calc(100vw - 8px));
874
877
  padding: 4px;
@@ -1442,7 +1445,27 @@
1442
1445
  }
1443
1446
  .empty-state .icon { font-size: 3rem; margin-bottom: 12px; }
1444
1447
  .empty-state h3 { font-size: var(--fs-core-chat-title); margin-bottom: 8px; color: var(--text); }
1445
- .empty-state p { font-size: var(--fs-meta-md); line-height: 1.5; }
1448
+ .empty-state p { font-size: var(--fs-meta-md); line-height: 1.5; max-width: 36em; }
1449
+ /* The actions of the "no conversation open" screen: Settings, New chat, the
1450
+ list on the phone. Same shapes as .summary-btn and #new-conv-btn. */
1451
+ .empty-state-actions {
1452
+ display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; align-items: center;
1453
+ margin-top: 18px;
1454
+ }
1455
+ .empty-state-btn {
1456
+ border: 1px solid var(--border); background: var(--bg2); color: var(--text);
1457
+ border-radius: 8px; height: 36px; padding: 0 14px; font-size: var(--fs-meta-md);
1458
+ cursor: pointer;
1459
+ }
1460
+ .empty-state-btn:hover { border-color: var(--accent); color: var(--accent); }
1461
+ .empty-state-btn.empty-state-btn-accent {
1462
+ background: var(--accent); color: #000; border-color: var(--accent); font-weight: 600;
1463
+ }
1464
+ .empty-state-btn.empty-state-btn-accent:hover { color: #000; opacity: 0.85; }
1465
+ .empty-state-btn.empty-state-link {
1466
+ border-color: transparent; background: transparent; color: var(--muted); text-decoration: underline;
1467
+ }
1468
+ .empty-state-btn.empty-state-link:hover { color: var(--accent); }
1446
1469
 
1447
1470
  .msg {
1448
1471
  display: flex;
@@ -2147,6 +2170,69 @@
2147
2170
  opacity: 1;
2148
2171
  transition: width 0.2s ease, background-size 0.2s ease, box-shadow 0.2s ease;
2149
2172
  }
2173
+ /* Jump to the end of the transcript (transcript-jump-bottom-view.mjs).
2174
+ Anchored to the composer's top edge, so it rides above the composer and
2175
+ the background-task panel at whatever height they have, and above the
2176
+ on-screen keyboard with them. Below the composer popups (z-index 30). */
2177
+ #jump-to-bottom-btn {
2178
+ position: absolute;
2179
+ right: calc(20px + env(safe-area-inset-right, 0px));
2180
+ bottom: calc(100% + 12px);
2181
+ z-index: 25;
2182
+ width: 40px;
2183
+ height: 40px;
2184
+ padding: 0;
2185
+ border-radius: 50%;
2186
+ border: 1px solid var(--border);
2187
+ background: var(--bg2);
2188
+ color: var(--text);
2189
+ box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
2190
+ display: flex;
2191
+ align-items: center;
2192
+ justify-content: center;
2193
+ cursor: pointer;
2194
+ touch-action: manipulation;
2195
+ -webkit-tap-highlight-color: transparent;
2196
+ opacity: 0;
2197
+ visibility: hidden;
2198
+ transform: translateY(6px);
2199
+ transition: opacity 0.18s ease, transform 0.18s ease, visibility 0s linear 0.18s, border-color 0.15s ease, color 0.15s ease;
2200
+ }
2201
+ #jump-to-bottom-btn.visible {
2202
+ opacity: 1;
2203
+ visibility: visible;
2204
+ transform: none;
2205
+ transition: opacity 0.18s ease, transform 0.18s ease, visibility 0s, border-color 0.15s ease, color 0.15s ease;
2206
+ }
2207
+ #jump-to-bottom-btn:hover,
2208
+ #jump-to-bottom-btn:focus-visible { border-color: var(--accent); color: var(--accent); }
2209
+ .jump-to-bottom-icon {
2210
+ font-size: 26px;
2211
+ line-height: 1;
2212
+ /* The arrowhead sits low in its em box. */
2213
+ transform: translateY(-4px);
2214
+ }
2215
+ .jump-to-bottom-badge {
2216
+ position: absolute;
2217
+ top: -6px;
2218
+ right: -6px;
2219
+ min-width: 18px;
2220
+ height: 18px;
2221
+ padding: 0 5px;
2222
+ box-sizing: border-box;
2223
+ border-radius: 9px;
2224
+ background: var(--accent);
2225
+ color: #000;
2226
+ font-size: 11px;
2227
+ font-weight: 700;
2228
+ line-height: 18px;
2229
+ text-align: center;
2230
+ }
2231
+ .jump-to-bottom-badge[hidden] { display: none; }
2232
+ @media (prefers-reduced-motion: reduce) {
2233
+ #jump-to-bottom-btn,
2234
+ #jump-to-bottom-btn.visible { transition: none; }
2235
+ }
2150
2236
  /* ── Context usage modal ── */
2151
2237
  .ctx-usage-model {
2152
2238
  font-size: 0.95rem;
@@ -2994,6 +3080,110 @@
2994
3080
  }
2995
3081
  .bg-task-stop:hover:not(:disabled) { border-color: #f85149; color: #f85149; }
2996
3082
  .bg-task-stop:disabled { opacity: 0.5; cursor: default; }
3083
+ /* Sub-agent rows (reply bubble, viewer): the task panel's row grammar. The
3084
+ title is a button that opens the agent's record in the viewer. */
3085
+ .subagent-rows { display: flex; flex-direction: column; margin-top: 4px; }
3086
+ .subagent-row-bubble { background: var(--bg3); border: 1px solid var(--border); }
3087
+ .subagent-row-timeline { margin-top: 0; background: var(--bg3); border: 1px solid var(--border); }
3088
+ .bg-task-desc.subagent-open {
3089
+ display: block; -webkit-line-clamp: unset; background: none; border: 0; padding: 0; margin: 0;
3090
+ font: inherit; color: var(--text); text-align: left; cursor: pointer;
3091
+ text-decoration: underline; text-decoration-color: transparent; text-underline-offset: 3px;
3092
+ }
3093
+ .bg-task-desc.subagent-open:hover, .bg-task-desc.subagent-open:focus-visible {
3094
+ color: var(--accent); text-decoration-color: currentColor; outline: none;
3095
+ }
3096
+ .subagent-row-status.subagent-status-failed { color: #f85149; }
3097
+ .subagent-row-status.subagent-status-completed { color: #3fb950; }
3098
+ /* The sub-agent viewer: one agent's thoughts, tool calls and reply in order.
3099
+ Below the file viewer, so a file opened from the record comes on top. */
3100
+ #subagent-viewer-modal {
3101
+ position: fixed; inset: 0; z-index: 10900; display: none;
3102
+ align-items: center; justify-content: center;
3103
+ background: rgba(0, 0, 0, 0.68); padding: 18px;
3104
+ }
3105
+ #subagent-viewer-modal.visible { display: flex; }
3106
+ .subagent-viewer-dialog {
3107
+ position: relative; width: min(980px, 100%); height: min(90vh, 1000px);
3108
+ display: flex; flex-direction: column; overflow: hidden;
3109
+ background: var(--bg2); border: 1px solid var(--border); border-radius: 14px;
3110
+ }
3111
+ .subagent-viewer-header {
3112
+ display: flex; flex-direction: column; gap: 4px; padding: 10px 14px;
3113
+ border-bottom: 1px solid var(--border); background: var(--bg3);
3114
+ }
3115
+ .subagent-viewer-title-row { display: flex; align-items: center; gap: 10px; min-width: 0; }
3116
+ #subagent-viewer-title { flex: 1; min-width: 0; margin: 0; font-size: 1.05rem; font-weight: 600; overflow-wrap: anywhere; }
3117
+ #subagent-viewer-meta { color: var(--muted); font-size: var(--fs-meta-sm); overflow-wrap: anywhere; }
3118
+ .subagent-viewer-btn {
3119
+ flex-shrink: 0; border: 1px solid var(--border); background: var(--bg2); color: var(--text);
3120
+ border-radius: 8px; height: 32px; padding: 0 10px; font-size: var(--fs-meta-sm); cursor: pointer;
3121
+ }
3122
+ .subagent-viewer-btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
3123
+ #subagent-viewer-stop:hover:not(:disabled) { border-color: #f85149; color: #f85149; }
3124
+ .subagent-viewer-btn:disabled { opacity: 0.5; cursor: default; }
3125
+ .subagent-viewer-close { font-size: 1.2rem; line-height: 1; }
3126
+ #subagent-viewer-parent {
3127
+ align-self: flex-start; background: none; border: 0; padding: 0; color: var(--accent);
3128
+ cursor: pointer; font-size: var(--fs-meta-sm);
3129
+ }
3130
+ #subagent-viewer-body { flex: 1; overflow-y: auto; overscroll-behavior: contain; padding: 8px 14px 28px; }
3131
+ #subagent-viewer-status { color: var(--muted); padding: 14px 0; font-size: var(--fs-meta-sm); }
3132
+ #subagent-viewer-jump {
3133
+ position: absolute; left: 50%; bottom: 14px; transform: translateX(-50%);
3134
+ border: 1px solid var(--accent); background: var(--bg3); color: var(--accent);
3135
+ border-radius: 999px; padding: 5px 14px; font-size: var(--fs-meta-sm); cursor: pointer;
3136
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
3137
+ }
3138
+ .sav-entry { margin: 6px 0; min-width: 0; }
3139
+ .sav-entry details > summary {
3140
+ cursor: pointer; display: flex; align-items: baseline; gap: 6px; min-width: 0; list-style: none;
3141
+ }
3142
+ .sav-entry details > summary::-webkit-details-marker { display: none; }
3143
+ .sav-entry details > summary::before { content: '▸'; color: var(--muted); flex-shrink: 0; }
3144
+ .sav-entry details[open] > summary::before { content: '▾'; }
3145
+ .sav-call-line { display: flex; align-items: baseline; gap: 6px; min-width: 0; }
3146
+ .sav-kind { flex-shrink: 0; }
3147
+ .sav-tool-name { font-weight: 600; flex-shrink: 0; }
3148
+ .sav-summary-text, .sav-tool-summary {
3149
+ min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
3150
+ }
3151
+ .sav-tool-summary { color: var(--muted); font-family: var(--mono, monospace); font-size: var(--fs-meta-sm); }
3152
+ .sav-pre {
3153
+ margin: 6px 0 0; padding: 8px 10px; max-height: 50vh; overflow: auto;
3154
+ white-space: pre-wrap; overflow-wrap: anywhere; font-family: var(--mono, monospace);
3155
+ font-size: var(--fs-meta-sm); background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
3156
+ }
3157
+ .sav-entry-tool_result { margin-left: 22px; color: var(--muted); font-size: var(--fs-meta-sm); }
3158
+ .sav-entry-tool_result .sav-pre { color: var(--text); }
3159
+ .sav-entry-tool_result.is-error .sav-result-label { color: #f85149; }
3160
+ .sav-entry-thought { color: var(--muted); }
3161
+ .sav-thought-body { padding: 4px 0 0 18px; color: var(--text); opacity: 0.85; }
3162
+ .sav-text {
3163
+ padding: 8px 12px; border-radius: 10px; background: var(--bg3); border: 1px solid var(--border);
3164
+ overflow-wrap: anywhere;
3165
+ }
3166
+ .sav-entry.is-open .sav-text::after,
3167
+ .sav-entry-thought.is-open .sav-thought-body::after {
3168
+ content: '▍'; margin-left: 2px; animation: bg-task-pulse 1s ease-in-out infinite;
3169
+ }
3170
+ /* Markdown in the record reads like a reply bubble. */
3171
+ .sav-text p, .sav-thought-body p { margin: 0 0 0.5em; }
3172
+ .sav-text p:last-child, .sav-thought-body p:last-child { margin-bottom: 0; }
3173
+ .sav-text ul, .sav-text ol, .sav-thought-body ul, .sav-thought-body ol { padding-left: 1.4em; margin: 0 0 0.5em; }
3174
+ .sav-text h1, .sav-text h2, .sav-text h3 { margin: 0.5em 0 0.25em; font-size: 1em; }
3175
+ .sav-text pre, .sav-thought-body pre { margin: 0.5em 0; border-radius: 8px; max-width: 100%; overflow-x: auto; }
3176
+ .sav-text code:not(pre code), .sav-thought-body code:not(pre code) {
3177
+ background: var(--inline-code-bg); padding: 2px 6px; border-radius: 4px; font-size: 0.875em; overflow-wrap: anywhere;
3178
+ }
3179
+ .sav-note { color: var(--muted); font-size: var(--fs-meta-sm); font-style: italic; }
3180
+ .sav-note-failed { color: #f85149; }
3181
+ .sav-muted { color: var(--muted); }
3182
+ @media (max-width: 680px) {
3183
+ #subagent-viewer-modal { padding: 0; }
3184
+ .subagent-viewer-dialog { width: 100%; height: 100%; border: 0; border-radius: 0; }
3185
+ #subagent-viewer-body { padding: 8px 10px 28px; }
3186
+ }
2997
3187
  /* Preview cards: same row grammar as a background task, with a link-shaped
2998
3188
  primary action. Shared by the composer panel and the settings list. */
2999
3189
  .preview-row {
@@ -3115,6 +3305,102 @@
3115
3305
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
3116
3306
  }
3117
3307
  #chat-title-origin button.conv-origin-agent { padding: 3px 0; }
3308
+ /* Relay events: the event rows the relay writes into a watcher's
3309
+ conversation (never a bubble of the user's), the "watching" chip with its
3310
+ list, and the "watched" badge. */
3311
+ .msg.relay-event { align-self: stretch; max-width: 100%; align-items: stretch; }
3312
+ .relay-event-row {
3313
+ border: 1px dashed var(--border); border-left: 3px solid var(--accent);
3314
+ border-radius: 6px; background: var(--bg2); color: var(--muted);
3315
+ font-size: var(--fs-meta-xs); padding: 2px 8px;
3316
+ }
3317
+ .relay-event-row.relay-event-urgent { border-left-color: var(--warn); }
3318
+ .relay-event-summary {
3319
+ cursor: pointer; display: flex; gap: 8px; align-items: baseline; min-width: 0;
3320
+ padding: 3px 0; list-style-position: inside;
3321
+ }
3322
+ .relay-event-tag {
3323
+ flex: 0 0 auto; text-transform: uppercase; letter-spacing: 0.04em;
3324
+ font-size: var(--fs-ui-chip); color: var(--accent);
3325
+ }
3326
+ .relay-event-urgent .relay-event-tag { color: var(--warn); }
3327
+ .relay-event-text { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3328
+ .relay-event-time { flex: 0 0 auto; }
3329
+ .relay-event-lines { margin: 2px 0 4px; padding-left: 18px; }
3330
+ .relay-event-line { margin: 2px 0; overflow-wrap: anywhere; white-space: pre-wrap; }
3331
+ .relay-event-line.relay-event-question-asked,
3332
+ .relay-event-line.relay-event-error,
3333
+ .relay-event-line.relay-event-task-failed,
3334
+ .relay-event-line.relay-event-stalled { color: var(--text, inherit); }
3335
+ /* Agent exchanges: an agent's prompt and this session's reply folded into
3336
+ one row in the event rows' look (agent-exchange-fold-view.mjs). The row
3337
+ is the first child of the prompt's own node; closed, the prompt's bubble
3338
+ and the exchange's other rows (the reply, the live bubble) are hidden.
3339
+ The prompt's node spans the pane for the row; its bubble keeps a user
3340
+ bubble's width. */
3341
+ .msg.agent-exchange-head { align-self: stretch; max-width: 100%; }
3342
+ .msg.agent-exchange-head > .msg-bubble { max-width: 82%; }
3343
+ .msg.agent-exchange-head.agent-exchange-closed > .msg-bubble,
3344
+ .msg.agent-exchange-head.agent-exchange-closed > .msg-label,
3345
+ #messages > .msg.agent-exchange-member.agent-exchange-closed { display: none; }
3346
+ .agent-exchange-row {
3347
+ align-self: stretch; min-width: 0;
3348
+ border: 1px dashed var(--border); border-left: 3px solid var(--accent);
3349
+ border-radius: 6px; background: var(--bg2); color: var(--muted);
3350
+ font-size: var(--fs-meta-xs); padding: 2px 8px;
3351
+ }
3352
+ .msg.agent-exchange-head:not(.agent-exchange-closed) > .agent-exchange-row { margin-bottom: 6px; }
3353
+ .agent-exchange-summary {
3354
+ cursor: pointer; display: flex; gap: 8px; align-items: baseline; min-width: 0;
3355
+ padding: 3px 0; list-style: none;
3356
+ }
3357
+ .agent-exchange-summary::-webkit-details-marker { display: none; }
3358
+ .agent-exchange-summary::before { content: '▸'; flex: 0 0 auto; color: var(--accent); }
3359
+ .agent-exchange-row[open] > .agent-exchange-summary::before { content: '▾'; }
3360
+ .agent-exchange-text { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3361
+ .agent-exchange-status,
3362
+ .agent-exchange-time { flex: 0 0 auto; }
3363
+ .agent-exchange-status { color: var(--text, inherit); }
3364
+ .agent-exchange-row.agent-exchange-failed,
3365
+ .agent-exchange-row.agent-exchange-stopped { border-left-color: var(--warn); }
3366
+ .agent-exchange-failed .agent-exchange-status,
3367
+ .agent-exchange-stopped .agent-exchange-status { color: var(--warn); }
3368
+ @media (max-width: 680px) {
3369
+ .msg.agent-exchange-head > .msg-bubble { max-width: 95%; }
3370
+ }
3371
+ #chat-title-relay-events {
3372
+ display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-top: 2px;
3373
+ font-size: var(--fs-meta-xs); line-height: 1.2; color: var(--muted); position: relative;
3374
+ }
3375
+ #chat-title-relay-events[hidden] { display: none; }
3376
+ .relay-watch-panel[hidden] { display: none; }
3377
+ .relay-watch-chip {
3378
+ font: inherit; color: var(--accent); background: transparent; cursor: pointer;
3379
+ border: 1px solid var(--border); border-radius: 10px; padding: 2px 8px;
3380
+ }
3381
+ .relay-watch-panel {
3382
+ position: fixed; top: 0; left: 0; z-index: 60; overflow-y: auto; box-sizing: border-box;
3383
+ min-width: min(260px, calc(100vw - 16px)); max-width: min(calc(100vw - 16px), 480px);
3384
+ font-size: var(--fs-meta-xs); line-height: 1.2; color: var(--muted);
3385
+ background: var(--bg2); border: 1px solid var(--border); border-radius: 8px;
3386
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.25); padding: 6px;
3387
+ }
3388
+ .relay-watch-list { list-style: none; margin: 0; padding: 0; }
3389
+ .relay-watch-item {
3390
+ display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 2px 8px; align-items: center;
3391
+ padding: 6px 4px; border-bottom: 1px solid var(--border);
3392
+ }
3393
+ .relay-watch-item:last-child { border-bottom: none; }
3394
+ .relay-watch-what { color: var(--text, inherit); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3395
+ .relay-watch-events { grid-column: 1; font-size: var(--fs-ui-chip); }
3396
+ .relay-watch-unsubscribe {
3397
+ grid-column: 2; grid-row: 1 / span 2; font: inherit; cursor: pointer;
3398
+ color: var(--danger); background: transparent; border: 1px solid var(--border); border-radius: 6px; padding: 4px 8px;
3399
+ }
3400
+ .conv-watched-badge {
3401
+ flex: 0 0 auto; font-size: var(--fs-ui-chip); color: var(--accent);
3402
+ border: 1px solid var(--border); border-radius: 8px; padding: 0 5px;
3403
+ }
3118
3404
  /* Agent sessions (Settings → Relays). The slider spans the row so a thumb
3119
3405
  has room to travel at phone width, with a taller touch area than the
3120
3406
  track itself. */
@@ -4069,6 +4355,28 @@
4069
4355
  border-color: var(--accent);
4070
4356
  color: var(--accent);
4071
4357
  }
4358
+ /* The one main action of a dialog, in #new-conv-btn's colours. */
4359
+ .summary-btn.summary-btn-accent {
4360
+ background: var(--accent);
4361
+ color: #000;
4362
+ border-color: var(--accent);
4363
+ font-weight: 600;
4364
+ }
4365
+ .summary-btn.summary-btn-accent:hover { opacity: 0.85; }
4366
+ #new-conversation-open-settings[hidden] { display: none; }
4367
+ /* New Chat with no provider ready: the note above the buttons. */
4368
+ .new-conversation-setup-note {
4369
+ margin-top: 14px;
4370
+ padding: 10px 12px;
4371
+ border: 1px solid var(--border);
4372
+ border-left: 3px solid var(--accent);
4373
+ border-radius: 8px;
4374
+ background: var(--bg3);
4375
+ color: var(--text);
4376
+ font-size: var(--fs-meta-md);
4377
+ line-height: 1.5;
4378
+ }
4379
+ .new-conversation-setup-note[hidden] { display: none; }
4072
4380
  .summary-close {
4073
4381
  width: 32px;
4074
4382
  padding: 0;
@@ -5050,6 +5358,43 @@
5050
5358
  font-size: var(--fs-meta-md);
5051
5359
  padding: 10px 6px;
5052
5360
  }
5361
+ /* Explorer edits: the New-folder row at the top of the folder pane and the in-place rename. */
5362
+ .repo-tree-summary, .repo-entry-row, .repo-card { -webkit-touch-callout: none; user-select: none; -webkit-user-select: none; }
5363
+ .repo-inline-edit {
5364
+ display: grid;
5365
+ grid-template-columns: auto minmax(0, 1fr) auto auto;
5366
+ align-items: center;
5367
+ gap: 8px;
5368
+ margin: 0 0 10px;
5369
+ padding: 8px 10px;
5370
+ border: 1px solid var(--accent);
5371
+ border-radius: 10px;
5372
+ background: rgba(88, 166, 255, 0.08);
5373
+ }
5374
+ .repo-inline-edit input,
5375
+ .repo-rename-input,
5376
+ .repo-delete-confirm-input {
5377
+ min-width: 0;
5378
+ width: 100%;
5379
+ border: 1px solid var(--border);
5380
+ border-radius: 8px;
5381
+ background: var(--bg);
5382
+ color: var(--text);
5383
+ padding: 6px 8px;
5384
+ font: inherit;
5385
+ font-size: var(--fs-meta-md);
5386
+ }
5387
+ .repo-inline-edit input:focus, .repo-rename-input:focus, .repo-delete-confirm-input:focus { outline: none; border-color: var(--accent); }
5388
+ .repo-inline-edit-error { grid-column: 1 / -1; color: var(--danger); font-size: var(--fs-meta-xs); min-height: 0; }
5389
+ .repo-inline-edit-error:empty { display: none; }
5390
+ .repo-rename-input { display: inline-block; width: auto; max-width: 100%; padding: 2px 6px; }
5391
+ .repo-tree-summary .repo-rename-input { width: calc(100% - 2.2em); }
5392
+ .repo-delete-confirm-label { display: block; margin: 10px 0 0; font-size: var(--fs-meta-md); color: var(--muted); }
5393
+ .repo-delete-confirm-input { margin-top: 6px; }
5394
+ @media (max-width: 720px) {
5395
+ .repo-inline-edit { grid-template-columns: auto minmax(0, 1fr); }
5396
+ .repo-inline-edit .repo-browser-btn { grid-column: span 1; }
5397
+ }
5053
5398
 
5054
5399
  .msg-mode {
5055
5400
  display: inline-block; font-size: var(--fs-ui-chip); background: var(--bg3);
@@ -5362,6 +5707,10 @@
5362
5707
  pointer-events: auto;
5363
5708
  }
5364
5709
  #sidebar-resizer { display: none; }
5710
+ /* The "no conversation open" actions stack on the phone. */
5711
+ .empty-state { padding: 16px; }
5712
+ .empty-state-actions { flex-direction: column; align-items: stretch; width: min(100%, 320px); }
5713
+ .empty-state-btn { height: 40px; }
5365
5714
  #fullscreen-btn { display: inline-flex; align-items: center; justify-content: center; }
5366
5715
  #sidebar-overlay { display: block; opacity: 0; pointer-events: none; transition: opacity 0.25s; }
5367
5716
  #sidebar-overlay.visible { opacity: 1; pointer-events: all; }
@@ -5542,6 +5891,7 @@
5542
5891
  .chat-actions-menu-item,
5543
5892
  .chat-actions-menu-meta { min-height: 30px; }
5544
5893
  #input-area { padding: 2px 10px calc(6px + env(safe-area-inset-bottom, 0px)); }
5894
+ #jump-to-bottom-btn { width: 44px; height: 44px; right: calc(12px + env(safe-area-inset-right, 0px)); bottom: calc(100% + 10px); }
5545
5895
  .input-row {
5546
5896
  display: flex;
5547
5897
  flex-wrap: wrap;
@@ -6058,7 +6408,7 @@
6058
6408
  <button id="message-search-btn" class="header-icon-btn" onclick="openMessageSearchModal()" title="Search messages" aria-label="Search messages">🔍</button>
6059
6409
  <button id="context-btn" class="header-icon-btn" onclick="showContext()" title="Check current context" aria-label="Check current context">🧠</button>
6060
6410
  <div id="chat-actions-menu-wrap">
6061
- <button id="chat-actions-menu-btn" class="header-icon-btn" type="button" title="Conversation actions" aria-label="Conversation actions" aria-haspopup="menu" aria-expanded="false">⋯</button>
6411
+ <button id="chat-actions-menu-btn" class="header-icon-btn" type="button" title="Menu" aria-label="Menu" aria-haspopup="menu" aria-expanded="false">⋯</button>
6062
6412
  <div id="chat-actions-menu" role="menu" hidden>
6063
6413
  <div id="chat-menu-pwa-version" class="chat-actions-menu-meta chat-actions-menu-meta-stacked" aria-hidden="true"><span class="chat-actions-menu-meta-primary">PWA shell version</span><span class="chat-actions-menu-meta-secondary" id="chat-menu-pwa-version-value">v?</span></div>
6064
6414
  <div id="chat-menu-queue-status" class="chat-actions-menu-meta chat-actions-menu-meta-stacked" aria-hidden="true"><div class="chat-actions-menu-meta-top"><span class="chat-actions-menu-meta-primary">Queue</span><button id="chat-menu-empty-queue" class="chat-actions-menu-meta-label" type="button">🚮 Empty queue</button></div><span class="chat-actions-menu-meta-secondary" id="chat-menu-queue-status-value">pending=?, processing=?, parked=?</span></div>
@@ -6085,13 +6435,15 @@
6085
6435
  <button id="pending-question-banner" onclick="openPendingQuestionFromBanner()" title="Open pending question"></button>
6086
6436
  </div>
6087
6437
  <div id="messages">
6438
+ <!-- Replaced on start by conversation-view.js (no-conversation-state.mjs),
6439
+ which knows whether a provider is ready and whether chats exist. -->
6088
6440
  <div class="empty-state">
6089
6441
  <div class="icon">💬</div>
6090
- <h3>No conversation selected</h3>
6091
- <p>Click <b>+ New Conversation</b> to start chatting with your Copilot CLI</p>
6442
+ <h3>No conversation open</h3>
6092
6443
  </div>
6093
6444
  </div>
6094
6445
  <div id="input-area">
6446
+ <button id="jump-to-bottom-btn" type="button" title="Jump to the latest message" aria-label="Jump to the latest message"><span class="jump-to-bottom-icon" aria-hidden="true">⌄</span><span class="jump-to-bottom-badge" hidden></span></button>
6095
6447
  <div id="offline-banner">⚠️ CLI is offline — messages will queue until it reconnects</div>
6096
6448
  <div id="model-banner"></div>
6097
6449
  <div id="usage-limit-banner" role="status" aria-live="polite" hidden></div>
@@ -6167,6 +6519,7 @@
6167
6519
  <button id="repo-toggle-hidden-btn" class="repo-browser-btn" onclick="toggleRepoBrowserHidden()">Hidden: Off</button>
6168
6520
  <button id="repo-toggle-heavy-btn" class="repo-browser-btn" onclick="toggleRepoBrowserHeavy()">Heavy: Off</button>
6169
6521
  <button class="repo-browser-btn" onclick="refreshRepoBrowser()">Refresh</button>
6522
+ <button id="repo-new-folder-btn" class="repo-browser-btn" onclick="startRepoNewFolder()" title="Create a folder inside the open folder">📁+ New folder</button>
6170
6523
  </div>
6171
6524
  <div class="repo-browser-layout">
6172
6525
  <section class="repo-pane">
@@ -6356,6 +6709,17 @@
6356
6709
  >
6357
6710
  </div>
6358
6711
  <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>
6712
+ <div class="settings-row" style="margin-top: 12px; margin-bottom: 0;">
6713
+ <label for="subagent-record-retention-select">Keep sub-agent tool output</label>
6714
+ <select id="subagent-record-retention-select" onchange="updateSubagentRecordRetentionSetting(this.value)" aria-label="Keep sub-agent tool output">
6715
+ <option value="7">7 days</option>
6716
+ <option value="14">14 days</option>
6717
+ <option value="30">30 days</option>
6718
+ <option value="90" selected>90 days</option>
6719
+ <option value="0">Always</option>
6720
+ </select>
6721
+ </div>
6722
+ <div class="settings-help" style="margin-top: 6px;">A sub-agent's record keeps every tool call's full input and the first 4 KB of each result. After this many days the relay removes them from finished agents, once a day; the thoughts, the reply, notes and the one-line summary of each call stay. Always keeps everything.</div>
6359
6723
  <div id="windows-autostart-setting" hidden>
6360
6724
  <div class="settings-row settings-row-column" style="margin-top: 12px; margin-bottom: 0;">
6361
6725
  <label>Autostart (Windows)</label>
@@ -7215,8 +7579,13 @@
7215
7579
  <select id="new-conversation-size-select"></select>
7216
7580
  <div id="new-conversation-size-status" style="margin-top:8px;font-size:0.78rem;color:var(--muted);">Image size used for generated outputs in this chat.</div>
7217
7581
  </div>
7582
+ <!-- No provider known to be ready (journal-view.js): the note says so and
7583
+ Settings is the main button; Start stays as "anyway", because the
7584
+ Copilot signal can be wrong. -->
7585
+ <div id="new-conversation-setup-note" class="new-conversation-setup-note" role="status" hidden></div>
7218
7586
  <div class="summary-modal-actions">
7219
7587
  <button type="button" class="summary-btn" onclick="closeNewConversationModelModal()">Cancel</button>
7588
+ <button id="new-conversation-open-settings" type="button" class="summary-btn summary-btn-accent" hidden>Open Settings → Providers</button>
7220
7589
  <button id="new-conversation-model-confirm" type="button" class="summary-btn" onclick="confirmNewConversationModel()">Start chat</button>
7221
7590
  </div>
7222
7591
  </div>
@@ -7267,6 +7636,28 @@
7267
7636
  </div>
7268
7637
  </div>
7269
7638
 
7639
+ <!-- Sub-agent viewer: one sub-agent's thoughts, tool calls with their results
7640
+ and reply, in order, live while it runs (subagent-viewer.mjs). -->
7641
+ <div id="subagent-viewer-modal" aria-hidden="true">
7642
+ <div class="subagent-viewer-dialog" role="dialog" aria-modal="true" aria-labelledby="subagent-viewer-title">
7643
+ <div class="subagent-viewer-header">
7644
+ <button id="subagent-viewer-parent" type="button" hidden></button>
7645
+ <div class="subagent-viewer-title-row">
7646
+ <span aria-hidden="true">🤖</span>
7647
+ <h2 id="subagent-viewer-title">Sub-agent</h2>
7648
+ <button id="subagent-viewer-stop" class="subagent-viewer-btn" type="button" hidden>Stop</button>
7649
+ <button class="subagent-viewer-btn subagent-viewer-close" type="button" aria-label="Close">×</button>
7650
+ </div>
7651
+ <div id="subagent-viewer-meta"></div>
7652
+ </div>
7653
+ <div id="subagent-viewer-body">
7654
+ <div id="subagent-viewer-timeline"></div>
7655
+ <div id="subagent-viewer-status"></div>
7656
+ </div>
7657
+ <button id="subagent-viewer-jump" type="button" hidden>New entries ↓</button>
7658
+ </div>
7659
+ </div>
7660
+
7270
7661
  <!-- Fullscreen file viewer. The content fills the screen; the chrome bars
7271
7662
  float over it and a tap on the content shows or hides them. -->
7272
7663
  <div id="file-preview-modal" aria-hidden="true">
@@ -346,6 +346,12 @@ export function createMessageRepository(db) {
346
346
  //
347
347
  // A turn blocked on an unanswered AskUserQuestion is never stale — it is
348
348
  // waiting on the human, and relay_questions carries its own expiry.
349
+ //
350
+ // The ceiling counts from the last human interaction with the turn, not
351
+ // from its start: answering a question restarts the clock. Otherwise an
352
+ // interview that waited hours for its answers would be cut off by the
353
+ // ceiling the moment the last answer arrived (live 2026-10-10: a
354
+ // five-question interview lost its reply that way).
349
355
  listRecoverableProcessing: db.prepare(`
350
356
  SELECT id, conversation_id, kind, relay_mode, model, retry_count, consumed_at
351
357
  FROM queue
@@ -353,7 +359,13 @@ export function createMessageRepository(db) {
353
359
  AND COALESCE(kind, '') != 'continuation'
354
360
  AND (
355
361
  COALESCE(owner_last_claimed_at, processing_at, timestamp) < @inactiveBefore
356
- OR (@ceilingBefore IS NOT NULL AND COALESCE(processing_at, timestamp) < @ceilingBefore)
362
+ OR (
363
+ @ceilingBefore IS NOT NULL
364
+ AND MAX(
365
+ COALESCE(processing_at, timestamp),
366
+ COALESCE((SELECT MAX(answered_at) FROM relay_questions WHERE queue_id = queue.id), '')
367
+ ) < @ceilingBefore
368
+ )
357
369
  )
358
370
  AND id NOT IN (SELECT queue_id FROM relay_questions WHERE status = 'pending')
359
371
  `),
@@ -431,15 +443,29 @@ export function createMessageRepository(db) {
431
443
  // A continuation with no live worker can only be torn down — replaying
432
444
  // it as deliverable work would send the CLI's own bookkeeping text back
433
445
  // to the CLI as a user prompt.
446
+ //
447
+ // Same staleness rules as listRecoverableProcessing: a continuation
448
+ // waiting on an unanswered question is never stale, and an answer
449
+ // restarts the ceiling clock. A background-agent interview hangs its
450
+ // question cards on the continuation row, so without this a long
451
+ // interview was torn down by the ceiling while the human was still
452
+ // answering (live 2026-10-10).
434
453
  listStaleProcessingContinuations: db.prepare(`
435
- SELECT id, conversation_id
454
+ SELECT id, conversation_id, timestamp, processing_at, owner_last_claimed_at
436
455
  FROM queue
437
456
  WHERE status = 'processing'
438
457
  AND kind = 'continuation'
439
458
  AND (
440
459
  COALESCE(owner_last_claimed_at, processing_at, timestamp) < @inactiveBefore
441
- OR (@ceilingBefore IS NOT NULL AND COALESCE(processing_at, timestamp) < @ceilingBefore)
460
+ OR (
461
+ @ceilingBefore IS NOT NULL
462
+ AND MAX(
463
+ COALESCE(processing_at, timestamp),
464
+ COALESCE((SELECT MAX(answered_at) FROM relay_questions WHERE queue_id = queue.id), '')
465
+ ) < @ceilingBefore
466
+ )
442
467
  )
468
+ AND id NOT IN (SELECT queue_id FROM relay_questions WHERE status = 'pending')
443
469
  `),
444
470
  listQueueForPauseDrop: db.prepare(`SELECT id, conversation_id FROM queue WHERE status IN ('pending', 'processing', 'parked')`),
445
471
  // Authoritative "is a turn in flight" signal for the conversation list.
@@ -1,7 +1,13 @@
1
1
  'use strict';
2
2
 
3
+ import { createSubagentTimelineStatements } from './subagent-timeline-statements.mjs';
4
+
3
5
  export function createQuestionRepository(db) {
6
+ const subagentTimeline = createSubagentTimelineStatements(db);
7
+ const runColumns = subagentTimeline.subagentRunSelectColumns;
8
+ const runColumnsAs = (alias) => runColumns.split(', ').map((column) => `${alias}.${column}`).join(', ');
4
9
  return {
10
+ ...subagentTimeline,
5
11
  // relay questions
6
12
  insertQuestion: db.prepare(`INSERT INTO relay_questions (id, queue_id, conversation_id, message_id, relay_mode, prompt, choices, request, request_schema, status, answer, structured_answer, sdk_session_id, owner_worker_id, continuation_id, continuation_question_id, created_at, answered_at, expires_at, attempt_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', NULL, NULL, ?, ?, ?, ?, ?, NULL, ?, ?)`),
7
13
  getQuestion: db.prepare(`SELECT * FROM relay_questions WHERE id = ?`),
@@ -99,7 +105,7 @@ export function createQuestionRepository(db) {
99
105
  WHERE id = ?
100
106
  `),
101
107
  listSubagentRunsByQueueMessage: db.prepare(`
102
- SELECT id, queue_message_id, conversation_id, parent_subagent_id, display_name, status, started_at, updated_at, completed_at
108
+ SELECT ${runColumns}
103
109
  FROM subagent_runs
104
110
  WHERE queue_message_id = ?
105
111
  ORDER BY started_at ASC, id ASC
@@ -108,7 +114,7 @@ export function createQuestionRepository(db) {
108
114
  // abort) also ends its subagent runs — nothing else ever will, and an
109
115
  // un-reconciled row renders as a bubble stuck "running" forever.
110
116
  listRunningSubagentRunsByQueueMessage: db.prepare(`
111
- SELECT id, conversation_id, parent_subagent_id, display_name
117
+ SELECT ${runColumns}
112
118
  FROM subagent_runs
113
119
  WHERE queue_message_id = ? AND status = 'running'
114
120
  `),
@@ -121,7 +127,7 @@ export function createQuestionRepository(db) {
121
127
  WHERE queue_message_id = ? AND status = 'running'
122
128
  `),
123
129
  listSubagentRunsByResponse: db.prepare(`
124
- SELECT sr.id, sr.queue_message_id, sr.conversation_id, sr.parent_subagent_id, sr.display_name, sr.status, sr.started_at, sr.updated_at, sr.completed_at
130
+ SELECT ${runColumnsAs('sr')}
125
131
  FROM subagent_runs sr
126
132
  WHERE sr.queue_message_id IN (
127
133
  SELECT id FROM queue WHERE response_message_id = ?
@@ -29,7 +29,10 @@ 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
  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`);
31
31
  const insertUnlock = db.prepare(`INSERT OR IGNORE INTO conversation_remote_relay_unlocks (conversation_id, remote_relay_id, message_id, created_at) VALUES (?, ?, ?, ?)`);
32
- const deleteUnlocksForRelay = db.prepare(`DELETE FROM conversation_remote_relay_unlocks WHERE remote_relay_id = ?`);
32
+ // The relay's unlocks and the approvals stored under its id (`<id>:watch`).
33
+ const deleteUnlocksForRelay = db.prepare(`
34
+ DELETE FROM conversation_remote_relay_unlocks WHERE remote_relay_id = ? OR substr(remote_relay_id, 1, length(?) + 1) = ? || ':'
35
+ `);
33
36
  const deleteUnlocksForConversation = db.prepare(`DELETE FROM conversation_remote_relay_unlocks WHERE conversation_id = ?`);
34
37
  const setMessageOrigin = db.prepare(`UPDATE messages SET origin_json = ? WHERE id = ?`);
35
38
  const getMessageOrigin = db.prepare(`SELECT origin_json FROM messages WHERE id = ?`);
@@ -101,7 +104,8 @@ export function createRemoteRelayRepository(db) {
101
104
  .map((row) => String(row.id));
102
105
  },
103
106
  forgetRelay(remoteRelayId) {
104
- return deleteUnlocksForRelay.run(String(remoteRelayId || '')).changes;
107
+ const id = String(remoteRelayId || '');
108
+ return deleteUnlocksForRelay.run(id, id, id).changes;
105
109
  },
106
110
  forgetConversation(conversationId) {
107
111
  return deleteUnlocksForConversation.run(String(conversationId || '')).changes;