create-walle 0.9.26 → 0.9.28
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/README.md +1 -0
- package/package.json +1 -1
- package/template/claude-task-manager/api-prompts.js +11 -6
- package/template/claude-task-manager/docs/session-status-redesign.html +554 -0
- package/template/claude-task-manager/docs/terminal-rendering-redesign.html +529 -0
- package/template/claude-task-manager/lib/flush-redraw-markers.js +72 -0
- package/template/claude-task-manager/lib/macos-capabilities.js +190 -0
- package/template/claude-task-manager/lib/session-messages-projection.js +224 -3
- package/template/claude-task-manager/lib/ttl-memo.js +61 -0
- package/template/claude-task-manager/public/index.html +892 -11
- package/template/claude-task-manager/public/js/activation-render-check.js +40 -2
- package/template/claude-task-manager/public/js/session-phase.js +370 -0
- package/template/claude-task-manager/public/js/setup.js +74 -1
- package/template/claude-task-manager/public/js/stream-view.js +56 -2
- package/template/claude-task-manager/server.js +643 -68
- package/template/claude-task-manager/workers/read-pool-worker.js +10 -0
- package/template/package.json +1 -1
- package/template/wall-e/agent.js +130 -24
- package/template/wall-e/api-walle.js +12 -1
- package/template/wall-e/brain.js +290 -4
- package/template/wall-e/chat.js +30 -25
- package/template/wall-e/coding/session-plan.js +79 -0
- package/template/wall-e/coding-orchestrator.js +9 -3
- package/template/wall-e/coding-prompts.js +10 -3
- package/template/wall-e/embeddings.js +192 -17
- package/template/wall-e/http/model-admin.js +109 -0
- package/template/wall-e/lib/event-loop-monitor.js +2 -2
- package/template/wall-e/lib/scheduler-worker-jobs.js +156 -121
- package/template/wall-e/lib/scheduler.js +226 -13
- package/template/wall-e/lib/worker-thread-pool.js +58 -4
- package/template/wall-e/llm/ollama-library.js +126 -0
- package/template/wall-e/llm/ollama.js +13 -0
- package/template/wall-e/llm/provider-backpressure.js +134 -0
- package/template/wall-e/llm/provider-health-state.js +24 -0
- package/template/wall-e/loops/backfill.js +43 -16
- package/template/wall-e/loops/initiative.js +1 -0
- package/template/wall-e/loops/think.js +38 -5
- package/template/wall-e/mcp-server.js +20 -4
- package/template/wall-e/skills/skill-fallback.js +34 -1
- package/template/wall-e/skills/skill-planner.js +60 -2
- package/template/wall-e/sources/jsonl-utils.js +84 -11
- package/template/wall-e/telemetry.js +42 -7
- package/template/wall-e/tools/local-tools.js +16 -0
- package/template/wall-e/workers/runtime-worker.js +33 -1
- package/template/website/index.html +5 -0
|
@@ -802,6 +802,115 @@
|
|
|
802
802
|
color: var(--fg);
|
|
803
803
|
background: color-mix(in srgb, var(--danger) 18%, var(--bg-light));
|
|
804
804
|
}
|
|
805
|
+
.setup-phone-url-card {
|
|
806
|
+
display: flex;
|
|
807
|
+
align-items: center;
|
|
808
|
+
justify-content: space-between;
|
|
809
|
+
gap: 12px;
|
|
810
|
+
margin: 0 0 14px;
|
|
811
|
+
padding: 12px 12px 12px 14px;
|
|
812
|
+
border: 1px solid color-mix(in srgb, var(--accent) 22%, var(--border));
|
|
813
|
+
border-radius: 12px;
|
|
814
|
+
background:
|
|
815
|
+
linear-gradient(135deg, color-mix(in srgb, var(--accent) 10%, transparent), transparent 58%),
|
|
816
|
+
color-mix(in srgb, var(--bg-light) 88%, #000 12%);
|
|
817
|
+
box-shadow: inset 0 1px 0 rgba(255,255,255,0.04);
|
|
818
|
+
}
|
|
819
|
+
.setup-phone-url-copy {
|
|
820
|
+
display: flex;
|
|
821
|
+
align-items: center;
|
|
822
|
+
flex-wrap: wrap;
|
|
823
|
+
gap: 8px;
|
|
824
|
+
min-width: 0;
|
|
825
|
+
color: var(--fg-dim);
|
|
826
|
+
font-size: 13px;
|
|
827
|
+
line-height: 1.4;
|
|
828
|
+
}
|
|
829
|
+
.setup-phone-url-label {
|
|
830
|
+
color: color-mix(in srgb, var(--fg) 70%, var(--fg-dim));
|
|
831
|
+
font-weight: 800;
|
|
832
|
+
}
|
|
833
|
+
.setup-phone-url-value {
|
|
834
|
+
max-width: min(100%, 720px);
|
|
835
|
+
padding: 4px 8px;
|
|
836
|
+
border: 1px solid color-mix(in srgb, var(--accent) 18%, var(--border));
|
|
837
|
+
border-radius: 8px;
|
|
838
|
+
background: color-mix(in srgb, var(--bg) 86%, #000 14%);
|
|
839
|
+
color: var(--fg);
|
|
840
|
+
font-size: 12px;
|
|
841
|
+
line-height: 1.35;
|
|
842
|
+
word-break: break-all;
|
|
843
|
+
}
|
|
844
|
+
.setup-copy-chip {
|
|
845
|
+
flex: 0 0 auto;
|
|
846
|
+
display: inline-flex;
|
|
847
|
+
align-items: center;
|
|
848
|
+
justify-content: center;
|
|
849
|
+
gap: 7px;
|
|
850
|
+
min-height: 32px;
|
|
851
|
+
padding: 7px 12px;
|
|
852
|
+
border: 1px solid color-mix(in srgb, var(--accent) 38%, var(--border));
|
|
853
|
+
border-radius: 999px;
|
|
854
|
+
background:
|
|
855
|
+
linear-gradient(180deg, color-mix(in srgb, var(--accent) 14%, transparent), transparent),
|
|
856
|
+
color-mix(in srgb, var(--bg-light) 88%, #000 12%);
|
|
857
|
+
color: color-mix(in srgb, var(--accent) 72%, var(--fg));
|
|
858
|
+
cursor: pointer;
|
|
859
|
+
font-size: 12px;
|
|
860
|
+
font-weight: 850;
|
|
861
|
+
letter-spacing: 0.01em;
|
|
862
|
+
transition: border-color 140ms ease, background-color 140ms ease, color 140ms ease, opacity 140ms ease, transform 140ms ease;
|
|
863
|
+
}
|
|
864
|
+
.setup-copy-chip:not(:disabled):hover {
|
|
865
|
+
border-color: color-mix(in srgb, var(--accent) 62%, var(--border));
|
|
866
|
+
background: color-mix(in srgb, var(--accent) 15%, var(--bg-light));
|
|
867
|
+
color: var(--fg);
|
|
868
|
+
transform: translateY(-1px);
|
|
869
|
+
}
|
|
870
|
+
.setup-copy-chip:disabled {
|
|
871
|
+
opacity: 0.45;
|
|
872
|
+
cursor: not-allowed;
|
|
873
|
+
transform: none;
|
|
874
|
+
}
|
|
875
|
+
.setup-copy-chip.is-copied {
|
|
876
|
+
border-color: color-mix(in srgb, var(--success) 55%, var(--border));
|
|
877
|
+
background: color-mix(in srgb, var(--success) 15%, var(--bg-light));
|
|
878
|
+
color: color-mix(in srgb, var(--success) 72%, var(--fg));
|
|
879
|
+
}
|
|
880
|
+
.setup-copy-glyph {
|
|
881
|
+
position: relative;
|
|
882
|
+
width: 13px;
|
|
883
|
+
height: 13px;
|
|
884
|
+
display: inline-block;
|
|
885
|
+
}
|
|
886
|
+
.setup-copy-glyph::before,
|
|
887
|
+
.setup-copy-glyph::after {
|
|
888
|
+
content: "";
|
|
889
|
+
position: absolute;
|
|
890
|
+
width: 8px;
|
|
891
|
+
height: 8px;
|
|
892
|
+
border: 1.5px solid currentColor;
|
|
893
|
+
border-radius: 3px;
|
|
894
|
+
}
|
|
895
|
+
.setup-copy-glyph::before {
|
|
896
|
+
left: 1px;
|
|
897
|
+
top: 4px;
|
|
898
|
+
opacity: 0.72;
|
|
899
|
+
}
|
|
900
|
+
.setup-copy-glyph::after {
|
|
901
|
+
right: 1px;
|
|
902
|
+
top: 1px;
|
|
903
|
+
background: color-mix(in srgb, var(--bg-light) 75%, transparent);
|
|
904
|
+
}
|
|
905
|
+
@media (max-width: 760px) {
|
|
906
|
+
.setup-phone-url-card {
|
|
907
|
+
align-items: stretch;
|
|
908
|
+
flex-direction: column;
|
|
909
|
+
}
|
|
910
|
+
.setup-copy-chip {
|
|
911
|
+
width: 100%;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
805
914
|
.backup-db-grid {
|
|
806
915
|
display: grid;
|
|
807
916
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
@@ -6254,6 +6363,27 @@
|
|
|
6254
6363
|
background: none; color: var(--fg-dim); cursor: pointer;
|
|
6255
6364
|
}
|
|
6256
6365
|
.session-esc-x:hover { color: var(--fg); }
|
|
6366
|
+
/* App-level Full Disk Access banner — fixed top bar. Shown only when macOS FDA is
|
|
6367
|
+
missing AND there is at least one network/Dropbox-backed session (so it never nags
|
|
6368
|
+
users who don't need it). One grant stops the repeating "network volume" prompt. */
|
|
6369
|
+
.fda-capability-banner {
|
|
6370
|
+
position: fixed; left: 0; right: 0; bottom: 0; z-index: 4100;
|
|
6371
|
+
display: flex; align-items: center; gap: 10px;
|
|
6372
|
+
padding: 8px 14px; font-size: 12.5px; color: var(--fg);
|
|
6373
|
+
background: rgba(45,37,15,0.97); backdrop-filter: blur(3px);
|
|
6374
|
+
border-top: 1px solid var(--yellow);
|
|
6375
|
+
box-shadow: 0 -2px 12px rgba(0,0,0,0.4);
|
|
6376
|
+
}
|
|
6377
|
+
.fda-banner-icon { font-size: 15px; }
|
|
6378
|
+
.fda-banner-text { flex: 1; }
|
|
6379
|
+
.fda-banner-text strong { font-family: var(--mono, monospace); }
|
|
6380
|
+
.fda-capability-banner button { font-size: 11px; padding: 4px 11px; border-radius: 5px; white-space: nowrap; cursor: pointer; border: 1px solid transparent; }
|
|
6381
|
+
.fda-banner-grant { border-color: rgba(158,206,106,0.6); background: rgba(158,206,106,0.2); color: var(--fg); }
|
|
6382
|
+
.fda-banner-grant:hover { background: rgba(158,206,106,0.34); }
|
|
6383
|
+
.fda-banner-later { border-color: rgba(122,162,247,0.5); background: rgba(122,162,247,0.14); color: var(--fg); }
|
|
6384
|
+
.fda-banner-later:hover { background: rgba(122,162,247,0.28); }
|
|
6385
|
+
.fda-banner-never { border: none; background: none; color: var(--fg-dim); }
|
|
6386
|
+
.fda-banner-never:hover { color: var(--fg); text-decoration: underline; }
|
|
6257
6387
|
/* Pending-approval banner: real permission approvals only. Normal terminal
|
|
6258
6388
|
choices (AskUserQuestion) stay in the terminal/choice flow and must not use
|
|
6259
6389
|
approval wording or Approve/Deny actions. */
|
|
@@ -7723,8 +7853,15 @@ url = "http://localhost:<span id="setup-mcp-port-display-toml">3457</span>/mcp"<
|
|
|
7723
7853
|
<div class="setup-phone-guidance-title" id="setup-phone-guidance-title">Phone setup</div>
|
|
7724
7854
|
<div class="setup-phone-guidance-body" id="setup-phone-guidance-body">Choose an access method above to see the phone steps.</div>
|
|
7725
7855
|
</div>
|
|
7726
|
-
<div id="setup-phone-recommended" class="setup-
|
|
7727
|
-
<
|
|
7856
|
+
<div id="setup-phone-recommended" class="setup-phone-url-card" aria-live="polite">
|
|
7857
|
+
<div class="setup-phone-url-copy">
|
|
7858
|
+
<span id="setup-phone-best-label" class="setup-phone-url-label">Recommended phone URL</span>
|
|
7859
|
+
<code id="setup-phone-best-url" class="setup-phone-url-value">Resolving…</code>
|
|
7860
|
+
</div>
|
|
7861
|
+
<button class="setup-copy-chip" id="setup-phone-best-copy" type="button" onclick="SETUP.copyRecommendedPhoneUrl()" disabled aria-label="Copy recommended phone URL">
|
|
7862
|
+
<span class="setup-copy-glyph" aria-hidden="true"></span>
|
|
7863
|
+
<span id="setup-phone-best-copy-label">Copy</span>
|
|
7864
|
+
</button>
|
|
7728
7865
|
</div>
|
|
7729
7866
|
<div class="input-row">
|
|
7730
7867
|
<label>New Phone Label</label>
|
|
@@ -8700,6 +8837,7 @@ url = "http://localhost:<span id="setup-mcp-port-display-toml">3457</span>/mcp"<
|
|
|
8700
8837
|
<script src="js/session-search-utils.js"></script>
|
|
8701
8838
|
<script src="js/session-activity-utils.js"></script>
|
|
8702
8839
|
<script src="js/session-status-precedence.js"></script>
|
|
8840
|
+
<script src="js/session-phase.js"></script>
|
|
8703
8841
|
<script src="js/terminal-restore-state.js"></script>
|
|
8704
8842
|
<script src="js/terminal-reconciler.js"></script>
|
|
8705
8843
|
<script src="js/stream-resize-policy.js"></script>
|
|
@@ -9394,6 +9532,14 @@ function broadcastInstalledUpdateReload(server, reason) {
|
|
|
9394
9532
|
|
|
9395
9533
|
function handleServerHello(msg) {
|
|
9396
9534
|
if (msg && msg.clientId) state._clientId = msg.clientId;
|
|
9535
|
+
// Option A "keyframe on every boundary": the server is the source of truth for whether
|
|
9536
|
+
// keyframe-sync is on (CTM_KEYFRAME_SYNC). Mirror it into the client flag so _keyframeSyncEnabled()
|
|
9537
|
+
// gates the boundary-render path + retires the heal overlay. Set before any heartbeat/snapshot
|
|
9538
|
+
// is processed (hello is the first message on the connection).
|
|
9539
|
+
if (msg && typeof msg.keyframeSync === 'boolean') window.CTM_KEYFRAME_SYNC = msg.keyframeSync;
|
|
9540
|
+
// Option A.5 "streaming keyframe heartbeat": mirror whether the server pushes keyframes
|
|
9541
|
+
// mid-stream (CTM_STREAM_KEYFRAME) so _streamKeyframeEnabled() gates the mid-stream apply.
|
|
9542
|
+
if (msg && typeof msg.streamKeyframe === 'boolean') window.CTM_STREAM_KEYFRAME = msg.streamKeyframe;
|
|
9397
9543
|
const server = normalizeAppVersionInfo(msg && msg.appVersion);
|
|
9398
9544
|
if (!server) return;
|
|
9399
9545
|
if (!state.appRuntime.loaded) {
|
|
@@ -9631,6 +9777,8 @@ function _markTerminalLayoutChanged(s, reason) {
|
|
|
9631
9777
|
function _markTerminalContentChanged(s, reason) {
|
|
9632
9778
|
const v = _bumpTerminalViewEpoch(s, 'contentEpoch', reason || 'content');
|
|
9633
9779
|
if (v) _setTerminalViewPhase(s, _terminalRestorePhases().PAINT_SETTLING, reason || 'content');
|
|
9780
|
+
// Content changed = the previously validated render frame is no longer authoritative.
|
|
9781
|
+
if (typeof _markRenderUnstable === 'function') _markRenderUnstable(s, reason || 'content');
|
|
9634
9782
|
}
|
|
9635
9783
|
|
|
9636
9784
|
function _updateTerminalVisibilityState(id, reason) {
|
|
@@ -9689,6 +9837,7 @@ function _markTerminalRestoreRequest(s, source, extra) {
|
|
|
9689
9837
|
if (nextPhase) _setTerminalViewPhase(s, nextPhase, 'restore-request:' + type);
|
|
9690
9838
|
}
|
|
9691
9839
|
_terminalRenderMarkDirty(s, 'restore-request:' + s._snapshotRequestSource);
|
|
9840
|
+
if (typeof _markRenderUnstable === 'function') _markRenderUnstable(s, 'restore-request:' + s._snapshotRequestSource);
|
|
9692
9841
|
_maybeDismissRestartOverlay('restore-request');
|
|
9693
9842
|
return now;
|
|
9694
9843
|
}
|
|
@@ -10186,6 +10335,8 @@ function connect() {
|
|
|
10186
10335
|
_sendQueue = [];
|
|
10187
10336
|
if (_sendRetryTimer) { clearTimeout(_sendRetryTimer); _sendRetryTimer = null; }
|
|
10188
10337
|
}
|
|
10338
|
+
// One-shot Full Disk Access check after boot settles (off the critical attach path).
|
|
10339
|
+
if (!_fdaCapabilityChecked) { _fdaCapabilityChecked = true; setTimeout(checkFdaCapability, 4000); }
|
|
10189
10340
|
};
|
|
10190
10341
|
|
|
10191
10342
|
ws.onmessage = (e) => {
|
|
@@ -10998,6 +11149,82 @@ function formatUptime(seconds) {
|
|
|
10998
11149
|
return h + 'h ' + m + 'm';
|
|
10999
11150
|
}
|
|
11000
11151
|
|
|
11152
|
+
// ── Full Disk Access capability banner ─────────────────────────────────────
|
|
11153
|
+
// Many sessions live under ~/Library/CloudStorage/Dropbox (a File Provider = "network volume"
|
|
11154
|
+
// to macOS) or NFS mounts, so macOS re-prompts for file access on every restart. Full Disk
|
|
11155
|
+
// Access is a superset that, under the stable Dev-ID identity, persists — one grant ends it.
|
|
11156
|
+
// We can only detect + guide; Apple forbids granting FDA programmatically.
|
|
11157
|
+
let _fdaCapabilityChecked = false;
|
|
11158
|
+
const FDA_BANNER_DISMISS_KEY = 'ctm_fda_banner_dismissed_v1';
|
|
11159
|
+
|
|
11160
|
+
async function checkFdaCapability() {
|
|
11161
|
+
try {
|
|
11162
|
+
if (localStorage.getItem(FDA_BANNER_DISMISS_KEY) === '1') return;
|
|
11163
|
+
const r = await fetch(`/api/capabilities?token=${state.token}`);
|
|
11164
|
+
if (!r.ok) return;
|
|
11165
|
+
const cap = await r.json();
|
|
11166
|
+
if (cap && cap.needsAttention) showFdaBanner(cap);
|
|
11167
|
+
else hideFdaBanner();
|
|
11168
|
+
} catch { /* best-effort — never block the UI on this */ }
|
|
11169
|
+
}
|
|
11170
|
+
|
|
11171
|
+
function hideFdaBanner() {
|
|
11172
|
+
const b = document.getElementById('fda-capability-banner');
|
|
11173
|
+
if (b) b.remove();
|
|
11174
|
+
}
|
|
11175
|
+
|
|
11176
|
+
function showFdaBanner(cap) {
|
|
11177
|
+
if (document.getElementById('fda-capability-banner')) return;
|
|
11178
|
+
const count = Number(cap.networkSessionCount) || 0;
|
|
11179
|
+
const denials = Array.isArray(cap.accessDenials) ? cap.accessDenials : [];
|
|
11180
|
+
// Prefer the specific, observed cause (e.g. Codex couldn't read ~/.codex) over the generic
|
|
11181
|
+
// count — it's actionable and explains why a session just failed to load.
|
|
11182
|
+
let message;
|
|
11183
|
+
if (denials.length) {
|
|
11184
|
+
const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => (
|
|
11185
|
+
{ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
11186
|
+
const d = denials[0];
|
|
11187
|
+
const who = d.provider ? d.provider.charAt(0).toUpperCase() + d.provider.slice(1) : 'An agent';
|
|
11188
|
+
const where = (d.path || '').replace(/\/config\.toml$/, '') || d.path || 'a file';
|
|
11189
|
+
message = `${esc(who)} couldn't read <code>${esc(where)}</code> (it's on Dropbox/iCloud or a network volume), so the session failed to load. <strong>Grant Full Disk Access once</strong> to fix it.`;
|
|
11190
|
+
} else {
|
|
11191
|
+
message = `CTM has ${count} session${count === 1 ? '' : 's'} on a network/Dropbox volume, so macOS keeps asking for file access on every restart. <strong>Grant Full Disk Access once</strong> to stop it.`;
|
|
11192
|
+
}
|
|
11193
|
+
const banner = document.createElement('div');
|
|
11194
|
+
banner.id = 'fda-capability-banner';
|
|
11195
|
+
banner.className = 'fda-capability-banner';
|
|
11196
|
+
banner.innerHTML = `
|
|
11197
|
+
<span class="fda-banner-icon">🔒</span>
|
|
11198
|
+
<span class="fda-banner-text">${message}</span>
|
|
11199
|
+
<button class="fda-banner-grant" type="button">Grant Full Disk Access</button>
|
|
11200
|
+
<button class="fda-banner-later" type="button">Later</button>
|
|
11201
|
+
<button class="fda-banner-never" type="button" title="Don't show again">Don't show again</button>`;
|
|
11202
|
+
document.body.appendChild(banner);
|
|
11203
|
+
banner.querySelector('.fda-banner-grant').addEventListener('click', () => grantFdaAccess(cap));
|
|
11204
|
+
banner.querySelector('.fda-banner-later').addEventListener('click', hideFdaBanner);
|
|
11205
|
+
banner.querySelector('.fda-banner-never').addEventListener('click', () => {
|
|
11206
|
+
try { localStorage.setItem(FDA_BANNER_DISMISS_KEY, '1'); } catch {}
|
|
11207
|
+
hideFdaBanner();
|
|
11208
|
+
});
|
|
11209
|
+
}
|
|
11210
|
+
|
|
11211
|
+
async function grantFdaAccess(cap) {
|
|
11212
|
+
const fallbackHint = (cap && cap.hint) ||
|
|
11213
|
+
'Open System Settings → Privacy & Security → Full Disk Access and add "Coding Task Manager".';
|
|
11214
|
+
try {
|
|
11215
|
+
const r = await fetch(`/api/capabilities/open-fda?token=${state.token}`, { method: 'POST' });
|
|
11216
|
+
const d = await r.json().catch(() => ({}));
|
|
11217
|
+
if (d && d.ok) {
|
|
11218
|
+
toast('Add "Coding Task Manager" to the opened Full Disk Access list, then restart CTM once — macOS will stop asking.', { type: 'info', title: 'Full Disk Access', duration: 12000 });
|
|
11219
|
+
} else {
|
|
11220
|
+
toast(fallbackHint, { type: 'warning', title: 'Grant Full Disk Access', duration: 14000 });
|
|
11221
|
+
}
|
|
11222
|
+
} catch {
|
|
11223
|
+
toast(fallbackHint, { type: 'warning', title: 'Grant Full Disk Access', duration: 14000 });
|
|
11224
|
+
}
|
|
11225
|
+
hideFdaBanner();
|
|
11226
|
+
}
|
|
11227
|
+
|
|
11001
11228
|
async function pollServiceStatus() {
|
|
11002
11229
|
try {
|
|
11003
11230
|
const r = await fetch(`/api/services/status?token=${state.token}`);
|
|
@@ -12327,6 +12554,15 @@ const _CODEX_BUSY_STATUS_LINE_RE = /^(?:(?:waiting\s+for\s+background\s+terminal
|
|
|
12327
12554
|
const _CODEX_BUSY_COMPACT_STATUS_LINE_RE = /^working\s+(?:\d+(?::\d+){0,2}|\d+(?:\.\d+)?\s*(?:ms|s|sec|secs|m|min|mins|h|hr|hrs))$/iu;
|
|
12328
12555
|
const _CODEX_BUSY_WORD = 'working';
|
|
12329
12556
|
const _CODEX_BUSY_HINT_RE = /esc\s+to\s+interrupt/i;
|
|
12557
|
+
// A width-reflow strand can paint the Codex "Working (12s • esc to interrupt)"
|
|
12558
|
+
// status line over trailing scrollback content, leaving a merged line such as
|
|
12559
|
+
// "Working RSA-1ES046-SHA256:DHE-RSA-AES256-SHA256:HIGH:!aNULL:!…". The strict
|
|
12560
|
+
// anchored REs above miss it, so a genuinely-running turn whose PTY has gone
|
|
12561
|
+
// briefly quiet gets demoted to Idle. Recognize the surviving "Working" prefix
|
|
12562
|
+
// when the trailing remainder is clearly NOT prose (code/cipher-like garble),
|
|
12563
|
+
// and never for ordinary "Working <word>" output ("working tree", "working on").
|
|
12564
|
+
const _CODEX_BUSY_GARBLE_PROSE_RE = /^working\s+(?:tree|dir|directory|copy|set|version|branch|on|in|with|through|from|for|as|at|to|and|but|so|the|a|an|my|your|our|this|that|it|now|here|hard|fine|well|great|correctly|properly)\b/i;
|
|
12565
|
+
const _CODEX_BUSY_GARBLE_SIGNAL_RE = /[:!]|[A-Z0-9]{3,}[-:]|\besc\b/;
|
|
12330
12566
|
const _CODEX_SKILL_WARNING_RE = /^(?:⚠\s*)?Skipped loading\s+\d+\s+skill\(s\)\s+due to invalid SKILL\.md files\./i;
|
|
12331
12567
|
const _CODEX_SKILL_DESCRIPTION_RE = /^\/.*\/SKILL\.md:\s+invalid description:\s+exceeds maximum length of\s+\d+\s+characters$/i;
|
|
12332
12568
|
const _GEMINI_STATUS_FRAGMENT_RE = /^(?:[\s\d•◦·∙●○✦✧◆◇◐◓◑◒|\/\\-]+|Thinking\.{0,3}|Working\.{0,3}|Running\.{0,3}|Responding\.{0,3}|Loading\.{0,3}|esc\s+to\s+(?:cancel|interrupt)|ctrl\+c\s+to\s+(?:quit|cancel)|press\s+enter\s+to\s+send|shift\+enter\s+for\s+newline)$/i;
|
|
@@ -12357,11 +12593,18 @@ function _normalizeCodexStatusLineText(text) {
|
|
|
12357
12593
|
.replace(/^[\s•◦·∙●○]+\s*/, '');
|
|
12358
12594
|
}
|
|
12359
12595
|
|
|
12596
|
+
function _isGarbledCodexBusyStatusLine(line) {
|
|
12597
|
+
if (!/^working\b/i.test(line)) return false;
|
|
12598
|
+
if (_CODEX_BUSY_GARBLE_PROSE_RE.test(line)) return false;
|
|
12599
|
+
return _CODEX_BUSY_GARBLE_SIGNAL_RE.test(line.slice(_CODEX_BUSY_WORD.length));
|
|
12600
|
+
}
|
|
12601
|
+
|
|
12360
12602
|
function _isCodexBusyStatusLineText(text) {
|
|
12361
12603
|
const line = _normalizeCodexStatusLineText(text);
|
|
12362
12604
|
if (!line || line.length > 240) return false;
|
|
12363
12605
|
return _CODEX_BUSY_STATUS_LINE_RE.test(line) ||
|
|
12364
|
-
_CODEX_BUSY_COMPACT_STATUS_LINE_RE.test(line)
|
|
12606
|
+
_CODEX_BUSY_COMPACT_STATUS_LINE_RE.test(line) ||
|
|
12607
|
+
_isGarbledCodexBusyStatusLine(line);
|
|
12365
12608
|
}
|
|
12366
12609
|
|
|
12367
12610
|
function _inputMayResolveWaiting(data, session) {
|
|
@@ -16491,6 +16734,8 @@ function _confirmClaudePersistentViewportGarble(s, now) {
|
|
|
16491
16734
|
}
|
|
16492
16735
|
|
|
16493
16736
|
function _maybeRecoverClaudeStaleWidthRender(id, reason) {
|
|
16737
|
+
// Option A: keyframes replace the stale-width garble heals (typeof-guarded for source-slice tests).
|
|
16738
|
+
if (typeof _keyframeSyncEnabled === 'function' && _keyframeSyncEnabled()) return false;
|
|
16494
16739
|
const s = state.sessions.get(id);
|
|
16495
16740
|
if (!s || !s.term) return false;
|
|
16496
16741
|
// Width arbitration: a SECONDARY viewer must never drive a redraw-reflow — it
|
|
@@ -19841,6 +20086,10 @@ function createTerminal(id, opts) {
|
|
|
19841
20086
|
if (s && s._suppressResize) return; // Skip during font metric refresh
|
|
19842
20087
|
_markTerminalLayoutChanged(s, 'xterm-resize');
|
|
19843
20088
|
_sendTerminalResizeIfChanged(s, id, 'xterm-resize', { cols, rows });
|
|
20089
|
+
// Option A: a resize is a render boundary — xterm reflows locally (can shear an idle
|
|
20090
|
+
// frame), so PULL an authoritative keyframe at the new dims (debounced to coalesce a
|
|
20091
|
+
// drag). No-op unless keyframe-sync is on.
|
|
20092
|
+
_requestKeyframeDebounced(s, id, 'resize');
|
|
19844
20093
|
});
|
|
19845
20094
|
|
|
19846
20095
|
// Alt screen buffer switch listener — when Claude Code's TUI (Ink) exits,
|
|
@@ -21066,9 +21315,14 @@ window._ctmEnsureTerminalRestoreForVisibleView = _ensureTerminalRestoreForVisibl
|
|
|
21066
21315
|
|
|
21067
21316
|
function _markTerminalActivationRestoreScheduled(s, id, source) {
|
|
21068
21317
|
if (!s) return;
|
|
21069
|
-
|
|
21318
|
+
const now = Date.now();
|
|
21319
|
+
s._activationRestoreScheduledAt = now;
|
|
21070
21320
|
s._activationRestoreScheduledId = id || s._id || '';
|
|
21071
21321
|
s._activationRestoreScheduledSource = source || 'activation';
|
|
21322
|
+
s._terminalActivationSeq = Number(s._terminalActivationSeq || 0) + 1;
|
|
21323
|
+
s._activatedAt = now;
|
|
21324
|
+
s._reconciledSinceActivation = false;
|
|
21325
|
+
if (typeof _markRenderUnstable === 'function') _markRenderUnstable(s, 'activation:' + s._activationRestoreScheduledSource);
|
|
21072
21326
|
}
|
|
21073
21327
|
|
|
21074
21328
|
function _clearTerminalActivationRestoreScheduled(s) {
|
|
@@ -21751,6 +22005,11 @@ function activateTab(id) {
|
|
|
21751
22005
|
// INSIDE the activation grace so a malformed switch heals in <1s instead of
|
|
21752
22006
|
// 5-10s. No-op when the session is already latched stable (nothing changed).
|
|
21753
22007
|
_scheduleActivationRenderCheck(sReal, id);
|
|
22008
|
+
// Option A: tab activation is a render boundary — PULL an authoritative keyframe at
|
|
22009
|
+
// our current dims so the frame is correct-by-construction (no divergence-detect heal
|
|
22010
|
+
// needed). No-op unless keyframe-sync is on. This is the structural fix for the
|
|
22011
|
+
// "switch to an idle tab → stale/stranded frame" bug.
|
|
22012
|
+
_requestKeyframe(sReal, id, 'activation');
|
|
21754
22013
|
if (!_sessionConversationViewActive(id)) focusTerminalIfSafe(id);
|
|
21755
22014
|
}));
|
|
21756
22015
|
// Final safety net — regardless of which restore path fired, make sure
|
|
@@ -28524,6 +28783,248 @@ function _modelsProviderEditPayload(type, options) {
|
|
|
28524
28783
|
return body;
|
|
28525
28784
|
}
|
|
28526
28785
|
|
|
28786
|
+
// Ollama-only "Manage models" dialog: browse the ollama.com library, download
|
|
28787
|
+
// (pull) models with live progress, and uninstall local models from disk. All
|
|
28788
|
+
// requests go through CTM's /api/models/* proxy to Wall-E (which owns the model
|
|
28789
|
+
// registry and the Ollama client). On any install/remove, the parent Edit
|
|
28790
|
+
// dialog's DEFAULT MODEL dropdown is refreshed via the registry.
|
|
28791
|
+
function showOllamaManageModelsDialog(providerType, baseUrl) {
|
|
28792
|
+
providerType = String(providerType || 'ollama').trim().toLowerCase();
|
|
28793
|
+
var existing = document.getElementById('ollama-manage-dialog');
|
|
28794
|
+
if (existing) existing.remove();
|
|
28795
|
+
|
|
28796
|
+
var SANITIZE_OPTS = { ADD_ATTR: ['style', 'id', 'type', 'placeholder', 'value', 'title', 'data-action', 'data-model'] };
|
|
28797
|
+
function fmtSize(b) {
|
|
28798
|
+
b = Number(b) || 0;
|
|
28799
|
+
if (!b) return '';
|
|
28800
|
+
var u = ['B', 'KB', 'MB', 'GB', 'TB'], i = 0;
|
|
28801
|
+
while (b >= 1024 && i < u.length - 1) { b /= 1024; i++; }
|
|
28802
|
+
return (b >= 10 || i === 0 ? Math.round(b) : b.toFixed(1)) + ' ' + u[i];
|
|
28803
|
+
}
|
|
28804
|
+
|
|
28805
|
+
var dialog = document.createElement('div');
|
|
28806
|
+
dialog.id = 'ollama-manage-dialog';
|
|
28807
|
+
dialog.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.62);display:flex;align-items:center;justify-content:center;z-index:10000;padding:18px;';
|
|
28808
|
+
var inner = document.createElement('div');
|
|
28809
|
+
inner.style.cssText = 'background:#24283b;border:1px solid #414868;border-radius:10px;width:640px;max-width:96vw;max-height:88vh;overflow:auto;box-shadow:0 24px 80px rgba(0,0,0,0.35);padding:20px;';
|
|
28810
|
+
inner.innerHTML = DOMPurify.sanitize(
|
|
28811
|
+
'<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:6px;">' +
|
|
28812
|
+
'<div><div style="font-size:17px;font-weight:700;color:#c0caf5;">Manage Ollama models</div>' +
|
|
28813
|
+
'<div style="font-size:12px;color:#565f89;margin-top:2px;">Download models from the Ollama library or uninstall local ones.</div></div>' +
|
|
28814
|
+
'<button type="button" id="btn-close-ollama-manage" style="padding:5px 10px;border-radius:6px;border:1px solid #414868;background:#1a1b26;color:#c0caf5;cursor:pointer;">✕</button>' +
|
|
28815
|
+
'</div>' +
|
|
28816
|
+
'<input id="ollama-manage-filter" type="search" placeholder="Filter models..." autocomplete="off" spellcheck="false" style="width:100%;box-sizing:border-box;margin:8px 0;background:#1a1b26;color:#c0caf5;border:1px solid #414868;border-radius:6px;padding:7px 9px;font-size:12px;">' +
|
|
28817
|
+
'<div id="ollama-manage-status" style="min-height:16px;font-size:12px;color:#565f89;margin-bottom:6px;"></div>' +
|
|
28818
|
+
'<div id="ollama-manage-body"><div style="color:#565f89;font-size:12px;padding:10px 0;">Loading…</div></div>' +
|
|
28819
|
+
'<div style="border-top:1px solid #2a2e44;margin-top:12px;padding-top:12px;">' +
|
|
28820
|
+
'<div style="font-size:11px;color:#565f89;text-transform:uppercase;letter-spacing:0.4px;margin-bottom:5px;">Pull by name</div>' +
|
|
28821
|
+
'<div style="display:flex;gap:8px;">' +
|
|
28822
|
+
'<input id="ollama-pull-name" type="text" placeholder="e.g. llama3.2:3b" autocomplete="off" spellcheck="false" style="flex:1;box-sizing:border-box;background:#1a1b26;color:#c0caf5;border:1px solid #414868;border-radius:6px;padding:7px 9px;font-size:12px;">' +
|
|
28823
|
+
'<button type="button" id="ollama-pull-name-btn" style="padding:7px 14px;border-radius:6px;background:#7aa2f7;color:#1a1b26;border:none;cursor:pointer;font-weight:700;">Pull</button>' +
|
|
28824
|
+
'</div>' +
|
|
28825
|
+
'</div>',
|
|
28826
|
+
SANITIZE_OPTS
|
|
28827
|
+
);
|
|
28828
|
+
dialog.appendChild(inner);
|
|
28829
|
+
document.body.appendChild(dialog);
|
|
28830
|
+
|
|
28831
|
+
var bodyEl = inner.querySelector('#ollama-manage-body');
|
|
28832
|
+
var statusEl = inner.querySelector('#ollama-manage-status');
|
|
28833
|
+
var filterEl = inner.querySelector('#ollama-manage-filter');
|
|
28834
|
+
var dirty = false; // set when something was installed/removed → refresh registry on close
|
|
28835
|
+
var pullCount = 0; // in-flight downloads; auto-reload only when idle
|
|
28836
|
+
var lastInstalled = [];
|
|
28837
|
+
var lastLibrary = { models: [], degraded: false };
|
|
28838
|
+
|
|
28839
|
+
function setStatus(msg, kind) {
|
|
28840
|
+
statusEl.textContent = msg || '';
|
|
28841
|
+
statusEl.style.color = kind === 'error' ? '#f7768e' : (kind === 'success' ? '#9ece6a' : '#565f89');
|
|
28842
|
+
}
|
|
28843
|
+
|
|
28844
|
+
function close() {
|
|
28845
|
+
dialog.remove();
|
|
28846
|
+
if (dirty) {
|
|
28847
|
+
// Refresh the registry so the parent Edit dialog's model dropdown updates.
|
|
28848
|
+
_modelsRegistryLoaded = false;
|
|
28849
|
+
_ensureModelsRegistryLoaded(_modelsLoadSeq).then(function() {
|
|
28850
|
+
if (document.getElementById('edit-provider-dialog')) showModelProviderEditDialog(providerType);
|
|
28851
|
+
});
|
|
28852
|
+
}
|
|
28853
|
+
}
|
|
28854
|
+
|
|
28855
|
+
function familyInstalled(family, installedIds) {
|
|
28856
|
+
var f = String(family).toLowerCase();
|
|
28857
|
+
return installedIds.some(function(id) {
|
|
28858
|
+
id = String(id).toLowerCase();
|
|
28859
|
+
return id === f || id.indexOf(f + ':') === 0;
|
|
28860
|
+
});
|
|
28861
|
+
}
|
|
28862
|
+
|
|
28863
|
+
function render() {
|
|
28864
|
+
var q = String(filterEl.value || '').trim().toLowerCase();
|
|
28865
|
+
var installed = lastInstalled || [];
|
|
28866
|
+
var installedIds = installed.map(function(m) { return m.id || m.name; });
|
|
28867
|
+
var available = (lastLibrary.models || []).filter(function(m) { return !familyInstalled(m.name, installedIds); });
|
|
28868
|
+
var instShown = installed.filter(function(m) { return !q || String(m.id || m.name).toLowerCase().indexOf(q) >= 0; });
|
|
28869
|
+
var availShown = available.filter(function(m) {
|
|
28870
|
+
return !q || String(m.name).toLowerCase().indexOf(q) >= 0 || String(m.description || '').toLowerCase().indexOf(q) >= 0;
|
|
28871
|
+
});
|
|
28872
|
+
|
|
28873
|
+
var h = '';
|
|
28874
|
+
h += '<div style="font-size:11px;color:#565f89;text-transform:uppercase;letter-spacing:0.4px;margin:4px 0 6px;">Installed (' + installed.length + ')</div>';
|
|
28875
|
+
if (!instShown.length) {
|
|
28876
|
+
h += '<div style="color:#565f89;font-size:12px;padding:2px 0 8px;">' + (installed.length ? 'No matches.' : 'No models installed locally.') + '</div>';
|
|
28877
|
+
} else {
|
|
28878
|
+
instShown.forEach(function(m) {
|
|
28879
|
+
var id = m.id || m.name;
|
|
28880
|
+
h += '<div class="ollama-row" style="display:flex;align-items:center;justify-content:space-between;gap:10px;padding:7px 0;border-bottom:1px solid #2a2e44;">' +
|
|
28881
|
+
'<div style="min-width:0;"><span style="color:#c0caf5;font-size:13px;">' + escHtml(id) + '</span>' +
|
|
28882
|
+
(m.size ? ' <span style="color:#565f89;font-size:11px;">' + escHtml(fmtSize(m.size)) + '</span>' : '') + '</div>' +
|
|
28883
|
+
'<div class="ollama-row-action" style="flex:none;">' +
|
|
28884
|
+
'<button type="button" data-action="remove" data-model="' + escHtml(id) + '" style="padding:4px 10px;border-radius:5px;border:1px solid #f7768e;background:transparent;color:#f7768e;cursor:pointer;font-size:11px;">Remove</button>' +
|
|
28885
|
+
'</div></div>';
|
|
28886
|
+
});
|
|
28887
|
+
}
|
|
28888
|
+
|
|
28889
|
+
h += '<div style="font-size:11px;color:#565f89;text-transform:uppercase;letter-spacing:0.4px;margin:14px 0 6px;">Available to download</div>';
|
|
28890
|
+
if (lastLibrary.degraded) {
|
|
28891
|
+
h += '<div style="color:#e0af68;font-size:12px;padding:2px 0 8px;">Catalog unavailable right now — use “Pull by name” below to download a specific tag.</div>';
|
|
28892
|
+
}
|
|
28893
|
+
if (!availShown.length && !lastLibrary.degraded) {
|
|
28894
|
+
h += '<div style="color:#565f89;font-size:12px;padding:2px 0 8px;">' + ((lastLibrary.models || []).length ? 'No matches.' : 'No catalog models available.') + '</div>';
|
|
28895
|
+
} else {
|
|
28896
|
+
availShown.slice(0, 200).forEach(function(m) {
|
|
28897
|
+
var sizes = Array.isArray(m.sizes) ? m.sizes : [];
|
|
28898
|
+
var chips = sizes.length
|
|
28899
|
+
? sizes.map(function(s) {
|
|
28900
|
+
var tag = m.name + ':' + s;
|
|
28901
|
+
return '<button type="button" data-action="pull" data-model="' + escHtml(tag) + '" style="padding:3px 9px;border-radius:5px;border:1px solid #414868;background:#1a1b26;color:#7aa2f7;cursor:pointer;font-size:11px;">' + escHtml(s) + ' ↓</button>';
|
|
28902
|
+
}).join(' ')
|
|
28903
|
+
: '<button type="button" data-action="pull" data-model="' + escHtml(m.name) + '" style="padding:4px 10px;border-radius:5px;border:1px solid #7aa2f7;background:transparent;color:#7aa2f7;cursor:pointer;font-size:11px;">Download</button>';
|
|
28904
|
+
h += '<div class="ollama-row" style="padding:8px 0;border-bottom:1px solid #2a2e44;">' +
|
|
28905
|
+
'<div style="display:flex;align-items:baseline;justify-content:space-between;gap:10px;">' +
|
|
28906
|
+
'<span style="color:#c0caf5;font-size:13px;font-weight:600;">' + escHtml(m.name) + '</span>' +
|
|
28907
|
+
'<span style="color:#565f89;font-size:11px;flex:none;">' + escHtml(m.pulls ? m.pulls + ' pulls' : '') + '</span>' +
|
|
28908
|
+
'</div>' +
|
|
28909
|
+
(m.description ? '<div style="color:#787c99;font-size:11px;margin:3px 0 6px;">' + escHtml(m.description) + '</div>' : '') +
|
|
28910
|
+
'<div class="ollama-row-action" style="display:flex;flex-wrap:wrap;gap:6px;align-items:center;">' + chips + '</div>' +
|
|
28911
|
+
'</div>';
|
|
28912
|
+
});
|
|
28913
|
+
if (availShown.length > 200) {
|
|
28914
|
+
h += '<div style="color:#565f89;font-size:11px;padding:6px 0;">Showing first 200 of ' + availShown.length + ' — filter to narrow.</div>';
|
|
28915
|
+
}
|
|
28916
|
+
}
|
|
28917
|
+
bodyEl.innerHTML = DOMPurify.sanitize(h, SANITIZE_OPTS);
|
|
28918
|
+
}
|
|
28919
|
+
|
|
28920
|
+
function reload(force) {
|
|
28921
|
+
var instUrl = _modelsApiUrl('/api/models/ollama/models');
|
|
28922
|
+
var libUrl = _modelsApiUrl('/api/models/ollama/library' + (force ? '?force=1' : ''));
|
|
28923
|
+
return Promise.all([
|
|
28924
|
+
fetch(instUrl).then(function(r) { return r.json(); }).catch(function() { return { models: [] }; }),
|
|
28925
|
+
fetch(libUrl).then(function(r) { return r.json(); }).catch(function() { return { models: [], degraded: true }; }),
|
|
28926
|
+
]).then(function(res) {
|
|
28927
|
+
lastInstalled = (res[0] && res[0].models) || [];
|
|
28928
|
+
lastLibrary = res[1] || { models: [], degraded: true };
|
|
28929
|
+
render();
|
|
28930
|
+
});
|
|
28931
|
+
}
|
|
28932
|
+
|
|
28933
|
+
function pull(model, actionCell) {
|
|
28934
|
+
model = String(model || '').trim();
|
|
28935
|
+
if (!model) return;
|
|
28936
|
+
var report = function(txt) { if (actionCell) actionCell.textContent = txt; else setStatus(txt, 'info'); };
|
|
28937
|
+
pullCount++;
|
|
28938
|
+
report('Starting…');
|
|
28939
|
+
fetch(_modelsApiUrl('/api/models/ollama/pull'), {
|
|
28940
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
28941
|
+
body: JSON.stringify({ model: model, baseUrl: baseUrl || undefined }),
|
|
28942
|
+
}).then(function(resp) {
|
|
28943
|
+
if (!resp.ok || !resp.body) throw new Error('Download failed (' + resp.status + ')');
|
|
28944
|
+
var reader = resp.body.getReader();
|
|
28945
|
+
var dec = new TextDecoder();
|
|
28946
|
+
var buf = '';
|
|
28947
|
+
var lastErr = null;
|
|
28948
|
+
function pump() {
|
|
28949
|
+
return reader.read().then(function(r) {
|
|
28950
|
+
if (r.done) return;
|
|
28951
|
+
buf += dec.decode(r.value, { stream: true });
|
|
28952
|
+
var lines = buf.split('\n');
|
|
28953
|
+
buf = lines.pop();
|
|
28954
|
+
lines.forEach(function(ln) {
|
|
28955
|
+
ln = ln.trim();
|
|
28956
|
+
if (!ln) return;
|
|
28957
|
+
var j; try { j = JSON.parse(ln); } catch (e) { return; }
|
|
28958
|
+
if (j.status === 'error' || j.error) { lastErr = j.error || j.message || 'Download error'; }
|
|
28959
|
+
else if (typeof j.progress === 'number') { report('Downloading ' + j.progress + '%'); }
|
|
28960
|
+
else if (j.message) { report(j.message); }
|
|
28961
|
+
});
|
|
28962
|
+
return pump();
|
|
28963
|
+
});
|
|
28964
|
+
}
|
|
28965
|
+
return pump().then(function() { if (lastErr) throw new Error(lastErr); });
|
|
28966
|
+
}).then(function() {
|
|
28967
|
+
dirty = true;
|
|
28968
|
+
report('Installed ✓');
|
|
28969
|
+
setStatus(model + ' downloaded.', 'success');
|
|
28970
|
+
}).catch(function(e) {
|
|
28971
|
+
setStatus(e && e.message ? e.message : 'Download failed', 'error');
|
|
28972
|
+
if (actionCell) actionCell.textContent = 'Failed';
|
|
28973
|
+
}).then(function() {
|
|
28974
|
+
pullCount--;
|
|
28975
|
+
if (pullCount === 0) reload(false);
|
|
28976
|
+
});
|
|
28977
|
+
}
|
|
28978
|
+
|
|
28979
|
+
function remove(model) {
|
|
28980
|
+
model = String(model || '').trim();
|
|
28981
|
+
if (!model) return;
|
|
28982
|
+
if (!window.confirm('Uninstall ' + model + ' from disk? This frees space and removes it from CTM.')) return;
|
|
28983
|
+
setStatus('Removing ' + model + '…', 'info');
|
|
28984
|
+
fetch(_modelsApiUrl('/api/models/ollama/delete'), {
|
|
28985
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
28986
|
+
body: JSON.stringify({ model: model, baseUrl: baseUrl || undefined }),
|
|
28987
|
+
}).then(function(resp) {
|
|
28988
|
+
return resp.json().catch(function() { return {}; }).then(function(d) {
|
|
28989
|
+
if (!resp.ok || d.error) throw new Error(d.error || 'Remove failed (' + resp.status + ')');
|
|
28990
|
+
return d;
|
|
28991
|
+
});
|
|
28992
|
+
}).then(function() {
|
|
28993
|
+
dirty = true;
|
|
28994
|
+
setStatus(model + ' removed.', 'success');
|
|
28995
|
+
return reload(false);
|
|
28996
|
+
}).catch(function(e) {
|
|
28997
|
+
setStatus(e && e.message ? e.message : 'Remove failed', 'error');
|
|
28998
|
+
});
|
|
28999
|
+
}
|
|
29000
|
+
|
|
29001
|
+
bodyEl.addEventListener('click', function(e) {
|
|
29002
|
+
var btn = e.target.closest('button[data-action]');
|
|
29003
|
+
if (!btn) return;
|
|
29004
|
+
var action = btn.getAttribute('data-action');
|
|
29005
|
+
var model = btn.getAttribute('data-model');
|
|
29006
|
+
if (action === 'pull') {
|
|
29007
|
+
var cell = btn.closest('.ollama-row') ? btn.closest('.ollama-row').querySelector('.ollama-row-action') : null;
|
|
29008
|
+
pull(model, cell);
|
|
29009
|
+
} else if (action === 'remove') {
|
|
29010
|
+
remove(model);
|
|
29011
|
+
}
|
|
29012
|
+
});
|
|
29013
|
+
filterEl.addEventListener('input', render);
|
|
29014
|
+
inner.querySelector('#ollama-pull-name-btn').addEventListener('click', function() {
|
|
29015
|
+
var nameInput = inner.querySelector('#ollama-pull-name');
|
|
29016
|
+
var val = String(nameInput.value || '').trim();
|
|
29017
|
+
if (val) { pull(val, null); nameInput.value = ''; }
|
|
29018
|
+
});
|
|
29019
|
+
inner.querySelector('#ollama-pull-name').addEventListener('keydown', function(e) {
|
|
29020
|
+
if (e.key === 'Enter') { e.preventDefault(); inner.querySelector('#ollama-pull-name-btn').click(); }
|
|
29021
|
+
});
|
|
29022
|
+
inner.querySelector('#btn-close-ollama-manage').addEventListener('click', close);
|
|
29023
|
+
dialog.addEventListener('click', function(e) { if (e.target === dialog) close(); });
|
|
29024
|
+
|
|
29025
|
+
reload(false);
|
|
29026
|
+
}
|
|
29027
|
+
|
|
28527
29028
|
function showModelProviderEditDialog(type) {
|
|
28528
29029
|
type = String(type || '').trim().toLowerCase();
|
|
28529
29030
|
if (!type) return;
|
|
@@ -28663,7 +29164,10 @@ function showModelProviderEditDialog(type) {
|
|
|
28663
29164
|
'<select id="edit-provider-default-model"' + defaultDisabledAttr + ' style="width:100%;box-sizing:border-box;background:#24283b;color:#c0caf5;border:1px solid #414868;border-radius:7px;padding:8px 10px;font-size:13px;"></select>' +
|
|
28664
29165
|
'<div style="display:flex;justify-content:space-between;gap:8px;align-items:center;flex-wrap:wrap;margin-top:7px;">' +
|
|
28665
29166
|
'<div style="font-size:11px;color:#565f89;">' + escHtml(modelHelp) + '</div>' +
|
|
28666
|
-
'<
|
|
29167
|
+
'<div style="display:flex;gap:6px;align-items:center;">' +
|
|
29168
|
+
(type === 'ollama' ? '<button type="button" id="btn-manage-ollama-models" style="padding:4px 8px;border-radius:5px;border:1px solid #414868;background:transparent;color:#9ece6a;cursor:pointer;font-size:11px;">Manage models →</button>' : '') +
|
|
29169
|
+
'<button type="button" id="btn-refresh-provider-models" style="padding:4px 8px;border-radius:5px;border:1px solid #414868;background:transparent;color:#7aa2f7;cursor:pointer;font-size:11px;">Refresh models</button>' +
|
|
29170
|
+
'</div>' +
|
|
28667
29171
|
'</div>' +
|
|
28668
29172
|
unsupportedNote +
|
|
28669
29173
|
'</div>' +
|
|
@@ -28772,6 +29276,12 @@ function showModelProviderEditDialog(type) {
|
|
|
28772
29276
|
if (document.getElementById('edit-provider-dialog')) showModelProviderEditDialog(type);
|
|
28773
29277
|
});
|
|
28774
29278
|
});
|
|
29279
|
+
var manageOllamaBtn = document.getElementById('btn-manage-ollama-models');
|
|
29280
|
+
if (manageOllamaBtn) {
|
|
29281
|
+
manageOllamaBtn.addEventListener('click', function() {
|
|
29282
|
+
showOllamaManageModelsDialog(type, baseUrl);
|
|
29283
|
+
});
|
|
29284
|
+
}
|
|
28775
29285
|
var detectButton = document.getElementById('btn-detect-edit-provider');
|
|
28776
29286
|
var maybeDetectGeminiCli = async function() {
|
|
28777
29287
|
if (type !== 'google' || !authSelect || authSelect.value !== 'gemini_cli' || (keyInput && keyInput.value.trim())) return null;
|
|
@@ -33154,6 +33664,11 @@ function onHeartbeat(msg) {
|
|
|
33154
33664
|
s._serverSeq = msg.seq;
|
|
33155
33665
|
s._serverFpCols = msg.cols;
|
|
33156
33666
|
s._serverFpRows = msg.rows;
|
|
33667
|
+
// Option A: the heartbeat fingerprint exists only to drive the divergence-detect heal
|
|
33668
|
+
// overlay. With keyframe-sync on, correctness comes from authoritative keyframe pushes
|
|
33669
|
+
// (output-quiet / activation / resize), so we record the fp for status/diagnostics but
|
|
33670
|
+
// skip the dims-reassert + reconcile + settle-recheck heal cascade entirely.
|
|
33671
|
+
if (_keyframeSyncEnabled()) return;
|
|
33157
33672
|
// Adopt a pure-lifecycle-bump heartbeat into our localSeq. A status change
|
|
33158
33673
|
// (idle / waiting-for-input) bumps the server's terminal lifecycle and emits this
|
|
33159
33674
|
// heartbeat at the new lifecycle, but carries NO new output bytes — and localSeq
|
|
@@ -33227,6 +33742,9 @@ function _reconcileConfirmAction(divergedSince, now) {
|
|
|
33227
33742
|
return 'fire';
|
|
33228
33743
|
}
|
|
33229
33744
|
function _maybeReconcile(s, id, reason, extra) {
|
|
33745
|
+
// Option A: keyframes replace divergence-driven reconcile. typeof-guarded so the source-
|
|
33746
|
+
// slice unit tests (which extract this fn standalone, without the helper) stay flag-off.
|
|
33747
|
+
if (typeof _keyframeSyncEnabled === 'function' && _keyframeSyncEnabled()) return;
|
|
33230
33748
|
if (!s || !s.term || !window.TerminalReconciler) return;
|
|
33231
33749
|
// Width arbitration: a SECONDARY viewer is rendered from server-pushed reflowed
|
|
33232
33750
|
// snapshots at its own width. The heartbeat fingerprint is measured at the
|
|
@@ -33241,7 +33759,32 @@ function _maybeReconcile(s, id, reason, extra) {
|
|
|
33241
33759
|
// Only compare when the server measured at the client's CURRENT dims; otherwise
|
|
33242
33760
|
// the fingerprints legitimately differ on dims and a resize/reconcile is already
|
|
33243
33761
|
// in flight elsewhere. Comparing across dims would cause a reconcile loop.
|
|
33244
|
-
if (s._serverFpCols != null && s._serverFpRows != null && (s.term.cols !== s._serverFpCols || s.term.rows !== s._serverFpRows))
|
|
33762
|
+
if (s._serverFpCols != null && s._serverFpRows != null && (s.term.cols !== s._serverFpCols || s.term.rows !== s._serverFpRows)) {
|
|
33763
|
+
// The server's fingerprint was measured at a DIFFERENT width than our grid now
|
|
33764
|
+
// holds — e.g. a background-tab pane-resize refit the grid on switch but no fresh
|
|
33765
|
+
// heartbeat has arrived at the new width. We still skip the cross-dims compare
|
|
33766
|
+
// (it would loop), but a plain skip here leaves an idle session you just switched
|
|
33767
|
+
// to STUCK forever: no output ⇒ no heartbeat ⇒ the onHeartbeat dims-reassert never
|
|
33768
|
+
// runs, so the server keeps serializing its stale-wide frame and the stranded
|
|
33769
|
+
// composer / branch chip never heals (the on-switch "check + redraw" the user
|
|
33770
|
+
// expects). Re-assert our dims ONCE here too (rate-limited via the SAME
|
|
33771
|
+
// _dimsReassertAt the heartbeat path uses) so the PTY resizes, the agent repaints
|
|
33772
|
+
// clean at our width, and the next heartbeat is comparable. Skipped while streaming
|
|
33773
|
+
// (force bypasses the SIGWINCH-during-stream deferral → duplicate scrollback); the
|
|
33774
|
+
// heartbeat reassert covers that case once output quiesces. Uniform for every
|
|
33775
|
+
// provider — unlike the claude-only stale-width detector, this needs no agent type
|
|
33776
|
+
// or stranded-fragment shape, so it also unsticks an idle Codex frame.
|
|
33777
|
+
const nowTs = Date.now();
|
|
33778
|
+
if (!_isSessionStreaming(s)
|
|
33779
|
+
&& (!s._dimsReassertAt || nowTs - s._dimsReassertAt > 2000)
|
|
33780
|
+
&& Number.isFinite(s.term.cols) && Number.isFinite(s.term.rows)) {
|
|
33781
|
+
s._dimsReassertAt = nowTs;
|
|
33782
|
+
try { _sendTerminalResizeIfChanged(s, id, 'reconcile-dims-reassert', { force: true, cols: s.term.cols, rows: s.term.rows }); } catch {}
|
|
33783
|
+
_markRenderUnstable(s, 'reconcile-dims-reassert');
|
|
33784
|
+
_scheduleDimsReassertRecheck(s, id);
|
|
33785
|
+
}
|
|
33786
|
+
return;
|
|
33787
|
+
}
|
|
33245
33788
|
let clientFp;
|
|
33246
33789
|
try { clientFp = window.TerminalReconciler.clientFingerprint(s.term); } catch { return; }
|
|
33247
33790
|
const focusHelper = (typeof _activeTerminalHasFocus === 'function') ? _activeTerminalHasFocus(s) : (document.hasFocus() && state.activeTab === id);
|
|
@@ -33286,7 +33829,7 @@ function _maybeReconcile(s, id, reason, extra) {
|
|
|
33286
33829
|
s._benignDivergeFp = null; // frame converged — drop any benign-divergence give-up memo
|
|
33287
33830
|
s._claudeStableDivergeFp = null; // frame converged — drop any stable-divergence redraw arm
|
|
33288
33831
|
s._activationHealPending = 0; // converged — drop the one-shot on-switch heal marker
|
|
33289
|
-
_markRenderStable(s); // confirmed good — latch
|
|
33832
|
+
_markRenderStable(s, id); // confirmed good — latch this exact activation/render state
|
|
33290
33833
|
return;
|
|
33291
33834
|
}
|
|
33292
33835
|
if (verdict === 'reconcile') {
|
|
@@ -33422,6 +33965,8 @@ window._ctmMaybeReconcile = _maybeReconcile;
|
|
|
33422
33965
|
// idle safety net; the 4s in-flight debounce + flash-loop backoff bound any churn.
|
|
33423
33966
|
const SETTLE_RECHECK_DELAY_MS = RECONCILE_DIVERGENCE_CONFIRM_MS + 150;
|
|
33424
33967
|
function _scheduleSettleRecheck(s, id) {
|
|
33968
|
+
// Option A: no divergence confirm/heal under keyframe-sync (typeof-guarded for source-slice tests).
|
|
33969
|
+
if (typeof _keyframeSyncEnabled === 'function' && _keyframeSyncEnabled()) return;
|
|
33425
33970
|
if (!s || s._exited) return;
|
|
33426
33971
|
// A reconcile/redraw is already in flight — its result will re-settle the frame; a
|
|
33427
33972
|
// re-check now would only no-op against the in-flight debounce. Skip to stay quiet.
|
|
@@ -33440,6 +33985,24 @@ function _scheduleSettleRecheck(s, id) {
|
|
|
33440
33985
|
// matching window._ctmMaybeReconcile / window._ctmMaybeRecoverClaudeStaleWidth.
|
|
33441
33986
|
window._ctmScheduleSettleRecheck = _scheduleSettleRecheck;
|
|
33442
33987
|
|
|
33988
|
+
const DIMS_REASSERT_RECHECK_MS = 350;
|
|
33989
|
+
function _scheduleDimsReassertRecheck(s, id) {
|
|
33990
|
+
if (!s || s._exited || !id) return;
|
|
33991
|
+
if (!_activationRenderCheckEnabled()) return;
|
|
33992
|
+
if (s._dimsReassertRecheckTimer) clearTimeout(s._dimsReassertRecheckTimer);
|
|
33993
|
+
s._dimsReassertRecheckTimer = setTimeout(() => {
|
|
33994
|
+
s._dimsReassertRecheckTimer = null;
|
|
33995
|
+
if (!_activationRenderCheckEnabled()) return;
|
|
33996
|
+
if (s._exited) return;
|
|
33997
|
+
if (state.activeTab !== id && !isSessionVisibleInSplit(id)) return;
|
|
33998
|
+
try { _maybeReconcile(s, id, 'dims-reassert-recheck'); } catch {}
|
|
33999
|
+
try { _maybeRecoverClaudeStaleWidthRender(id, 'dims-reassert-recheck'); } catch {}
|
|
34000
|
+
if (s._reconcileDivergedSince || s._claudeViewportGarbleSince) _scheduleSettleRecheck(s, id);
|
|
34001
|
+
}, DIMS_REASSERT_RECHECK_MS);
|
|
34002
|
+
if (s._dimsReassertRecheckTimer && s._dimsReassertRecheckTimer.unref) s._dimsReassertRecheckTimer.unref();
|
|
34003
|
+
}
|
|
34004
|
+
window._ctmScheduleDimsReassertRecheck = _scheduleDimsReassertRecheck;
|
|
34005
|
+
|
|
33443
34006
|
// --- Input-settle re-check (heal promptly after the user stops typing) ---
|
|
33444
34007
|
// While the user types, the editing-defer keeps _maybeReconcile from healing a diverged
|
|
33445
34008
|
// frame (so a reconcile can't paint over the live composer). After they stop, the next heal
|
|
@@ -33494,35 +34057,152 @@ const CTM_ACTIVATION_RENDER_CHECK_MS = 200;
|
|
|
33494
34057
|
// cleared on convergence, and self-expires so it can never bypass the cooldown later.
|
|
33495
34058
|
const ACTIVATION_HEAL_PENDING_MAX_MS = 3500;
|
|
33496
34059
|
function _activationRenderCheckEnabled() {
|
|
34060
|
+
// Option A retires the activation-render-check (and the rest of the heal overlay) when
|
|
34061
|
+
// keyframe-sync is on — the server pushes ground truth on every boundary instead.
|
|
34062
|
+
// typeof-guarded so source-slice unit tests that extract this fn stay flag-off.
|
|
34063
|
+
if (typeof _keyframeSyncEnabled === 'function' && _keyframeSyncEnabled()) return false;
|
|
33497
34064
|
return typeof window === 'undefined' || window.CTM_ACTIVATION_RENDER_CHECK !== false;
|
|
33498
34065
|
}
|
|
34066
|
+
// Option A — "keyframe on every boundary". When ON (server sets it via hello), the client
|
|
34067
|
+
// renders the server's authoritative keyframe on activation/resize/quiet and SKIPS the
|
|
34068
|
+
// ~13-path divergence-detect heal overlay (which only existed to repair byte-stream drift).
|
|
34069
|
+
// Default OFF — keyframe-off is byte-for-byte the legacy render path.
|
|
34070
|
+
function _keyframeSyncEnabled() {
|
|
34071
|
+
return typeof window !== 'undefined' && window.CTM_KEYFRAME_SYNC === true;
|
|
34072
|
+
}
|
|
34073
|
+
// Option A.5 — "streaming keyframe heartbeat". The server pushes authoritative keyframes
|
|
34074
|
+
// (restoreReason 'stream-keyframe') at a controlled frame rate DURING sustained output, so a
|
|
34075
|
+
// mid-turn byte-stream shear self-corrects within one interval rather than persisting until the
|
|
34076
|
+
// turn quiets. Requires keyframe-sync; default OFF.
|
|
34077
|
+
function _streamKeyframeEnabled() {
|
|
34078
|
+
return _keyframeSyncEnabled() && typeof window !== 'undefined' && window.CTM_STREAM_KEYFRAME === true;
|
|
34079
|
+
}
|
|
34080
|
+
// Pure decision core (unit-testable without the DOM): given the safety signals, return what to
|
|
34081
|
+
// do with a streaming keyframe. PAINT only when the client frame is SETTLED (write queue drained
|
|
34082
|
+
// — so its fingerprint is a reliable divergence signal), the user is NOT mid-input (so we never
|
|
34083
|
+
// clobber an un-acked composer/draft), the keyframe is not behind what we've applied, and its
|
|
34084
|
+
// grid matches ours. Anything else defers to the next keyframe (or the final output-quiet one),
|
|
34085
|
+
// so this can only make an inevitable correction land sooner — never paint something unsafe.
|
|
34086
|
+
function _streamKeyframeActionCore(f) {
|
|
34087
|
+
if (!f || !f.enabled) return 'skip-disabled';
|
|
34088
|
+
if (!f.hasData) return 'skip-no-data';
|
|
34089
|
+
if (!f.visible) return 'skip-hidden';
|
|
34090
|
+
if (f.typing) return 'skip-typing';
|
|
34091
|
+
if (f.busy) return 'skip-busy'; // writer still applying bytes → fp compare unreliable
|
|
34092
|
+
if (f.behind) return 'skip-stale'; // marker older than what we've already applied
|
|
34093
|
+
if (f.dimsMismatch) return 'skip-dims'; // a resize keyframe / boundary path owns dim fixes
|
|
34094
|
+
return 'paint';
|
|
34095
|
+
}
|
|
34096
|
+
function _streamKeyframeAction(s, msg) {
|
|
34097
|
+
if (!s || !msg) return 'skip-no-data';
|
|
34098
|
+
const id = msg.id;
|
|
34099
|
+
const incomingMarker = msg.snapshotCurrentMarker != null ? msg.snapshotCurrentMarker : msg.snapshotMarker;
|
|
34100
|
+
const localCols = s.term ? s.term.cols : 0;
|
|
34101
|
+
const localRows = s.term ? s.term.rows : 0;
|
|
34102
|
+
const ptyCols = msg.ptyCols || msg.cols || localCols;
|
|
34103
|
+
const ptyRows = msg.ptyRows || msg.rows || localRows;
|
|
34104
|
+
let behind = false;
|
|
34105
|
+
try {
|
|
34106
|
+
behind = incomingMarker != null && s._localSeq != null && window.TerminalReconciler
|
|
34107
|
+
&& typeof window.TerminalReconciler.seqLess === 'function'
|
|
34108
|
+
&& window.TerminalReconciler.seqLess(incomingMarker, s._localSeq);
|
|
34109
|
+
} catch {}
|
|
34110
|
+
return _streamKeyframeActionCore({
|
|
34111
|
+
enabled: _streamKeyframeEnabled() && !!s.term,
|
|
34112
|
+
hasData: !!msg.data,
|
|
34113
|
+
visible: state.activeTab === id || isSessionVisibleInSplit(id),
|
|
34114
|
+
typing: _terminalRecentUserInputActive(s) || _terminalInputDisplayHoldActive(s),
|
|
34115
|
+
busy: !!(s.writer && (s.writer.scheduled || s.writer._chunking || s.writer.queue)),
|
|
34116
|
+
behind: !!behind,
|
|
34117
|
+
dimsMismatch: Math.abs(localCols - ptyCols) > 0 || Math.abs(localRows - ptyRows) > 0,
|
|
34118
|
+
});
|
|
34119
|
+
}
|
|
34120
|
+
window._ctmStreamKeyframeAction = _streamKeyframeAction;
|
|
34121
|
+
// Option A: PULL an authoritative keyframe at the client's CURRENT dims on a render
|
|
34122
|
+
// boundary (activation / resize). Reuses the 'reconcile' request — the server serializes
|
|
34123
|
+
// the headless mirror at our dims and returns a snapshot (sent even if output lands
|
|
34124
|
+
// mid-serialize). This is the deterministic on-boundary I-frame that replaces the
|
|
34125
|
+
// fingerprint-divergence heal paths; the live byte stream still applies between keyframes.
|
|
34126
|
+
function _requestKeyframe(s, id, reason) {
|
|
34127
|
+
if (!_keyframeSyncEnabled() || !s || !s.term || s._exited) return;
|
|
34128
|
+
if (!Number.isFinite(s.term.cols) || !Number.isFinite(s.term.rows)) return;
|
|
34129
|
+
if (state.activeTab !== id && !isSessionVisibleInSplit(id)) return;
|
|
34130
|
+
try { _sendTerminalRestoreRequest(id, 'reconcile', { cols: s.term.cols, rows: s.term.rows }); } catch {}
|
|
34131
|
+
}
|
|
34132
|
+
const KEYFRAME_REQUEST_DEBOUNCE_MS = 160;
|
|
34133
|
+
function _requestKeyframeDebounced(s, id, reason) {
|
|
34134
|
+
if (!_keyframeSyncEnabled() || !s || s._exited) return;
|
|
34135
|
+
if (s._keyframeReqTimer) clearTimeout(s._keyframeReqTimer);
|
|
34136
|
+
s._keyframeReqTimer = setTimeout(() => {
|
|
34137
|
+
s._keyframeReqTimer = null;
|
|
34138
|
+
_requestKeyframe(s, id, reason);
|
|
34139
|
+
}, KEYFRAME_REQUEST_DEBOUNCE_MS);
|
|
34140
|
+
if (s._keyframeReqTimer && s._keyframeReqTimer.unref) s._keyframeReqTimer.unref();
|
|
34141
|
+
}
|
|
34142
|
+
window._ctmRequestKeyframe = _requestKeyframe;
|
|
33499
34143
|
// Stable latch: set on convergence (_maybeReconcile verdict 'ok'); cleared by
|
|
33500
34144
|
// _markRenderUnstable on the situation-changed triggers (output / resize / reflow /
|
|
33501
34145
|
// reconnect). Sole consumer is _scheduleActivationRenderCheck.
|
|
33502
|
-
function
|
|
34146
|
+
function _terminalActivationStableKey(s, id) {
|
|
34147
|
+
if (!s) return null;
|
|
34148
|
+
const sid = id || s._id || '';
|
|
34149
|
+
const v = _sessionViewState(s, sid);
|
|
34150
|
+
const tuple = _terminalEpochTuple(v || {});
|
|
34151
|
+
return {
|
|
34152
|
+
id: sid,
|
|
34153
|
+
visibility: _sessionVisibility(sid),
|
|
34154
|
+
activationSeq: Number(s._terminalActivationSeq || 0),
|
|
34155
|
+
mutationSeq: Number(s._terminalMutationSeq || 0),
|
|
34156
|
+
localSeq: s._localSeq == null ? null : String(s._localSeq),
|
|
34157
|
+
serverSeq: s._serverSeq == null ? null : String(s._serverSeq),
|
|
34158
|
+
serverFp: s._serverFp || null,
|
|
34159
|
+
serverCols: Number(s._serverFpCols || 0),
|
|
34160
|
+
serverRows: Number(s._serverFpRows || 0),
|
|
34161
|
+
cols: Number(s.term?.cols || 0),
|
|
34162
|
+
rows: Number(s.term?.rows || 0),
|
|
34163
|
+
viewerRole: s._viewerRole || 'primary',
|
|
34164
|
+
contentEpoch: Number(tuple.contentEpoch || 0),
|
|
34165
|
+
layoutEpoch: Number(tuple.layoutEpoch || 0),
|
|
34166
|
+
rendererEpoch: Number(tuple.rendererEpoch || 0),
|
|
34167
|
+
attachEpoch: Number(tuple.attachEpoch || 0),
|
|
34168
|
+
restoreEpoch: Number(tuple.restoreEpoch || 0),
|
|
34169
|
+
restoreRequestSeq: Number(s._terminalRestoreRequestSeq || 0),
|
|
34170
|
+
};
|
|
34171
|
+
}
|
|
34172
|
+
function _markRenderStable(s, id) {
|
|
33503
34173
|
if (!s) return;
|
|
34174
|
+
const key = _terminalActivationStableKey(s, id || s._id || '');
|
|
33504
34175
|
s._renderStableSince = window.ActivationRenderCheck
|
|
33505
34176
|
? window.ActivationRenderCheck.markStable(Date.now())
|
|
33506
34177
|
: Date.now();
|
|
34178
|
+
s._renderStableKey = window.ActivationRenderCheck && window.ActivationRenderCheck.markStableKey
|
|
34179
|
+
? window.ActivationRenderCheck.markStableKey(key)
|
|
34180
|
+
: key;
|
|
33507
34181
|
}
|
|
33508
|
-
function _markRenderUnstable(s) {
|
|
34182
|
+
function _markRenderUnstable(s, reason) {
|
|
33509
34183
|
if (!s) return;
|
|
33510
34184
|
s._renderStableSince = window.ActivationRenderCheck
|
|
33511
34185
|
? window.ActivationRenderCheck.markUnstable()
|
|
33512
34186
|
: 0;
|
|
34187
|
+
s._renderStableKey = null;
|
|
34188
|
+
s._renderUnstableReason = reason || '';
|
|
33513
34189
|
}
|
|
33514
34190
|
function _scheduleActivationRenderCheck(s, id) {
|
|
33515
34191
|
if (!s || s._exited) return;
|
|
34192
|
+
const currentKey = _terminalActivationStableKey(s, id || s._id || '');
|
|
33516
34193
|
const should = window.ActivationRenderCheck
|
|
33517
34194
|
? window.ActivationRenderCheck.shouldScheduleCheck({
|
|
33518
34195
|
enabled: _activationRenderCheckEnabled(),
|
|
33519
34196
|
renderStableSince: s._renderStableSince,
|
|
34197
|
+
stableKey: s._renderStableKey,
|
|
34198
|
+
currentKey,
|
|
33520
34199
|
timerPending: !!s._activationRenderCheckTimer,
|
|
33521
34200
|
})
|
|
33522
34201
|
: (_activationRenderCheckEnabled() && !s._renderStableSince && !s._activationRenderCheckTimer);
|
|
33523
34202
|
if (!should) return;
|
|
33524
34203
|
s._activationRenderCheckTimer = setTimeout(() => {
|
|
33525
34204
|
s._activationRenderCheckTimer = null;
|
|
34205
|
+
if (!_activationRenderCheckEnabled()) return;
|
|
33526
34206
|
if (s._exited) return;
|
|
33527
34207
|
if (state.activeTab !== id && !isSessionVisibleInSplit(id)) return;
|
|
33528
34208
|
// Run both validators (same ones the loop uses). The FIRST sighting only ARMS a
|
|
@@ -35568,6 +36248,20 @@ function _needsAuthoritativeSnapshotRecovery(s) {
|
|
|
35568
36248
|
));
|
|
35569
36249
|
}
|
|
35570
36250
|
|
|
36251
|
+
// Reflow stale-skip backoff: a reflow can come back {stale:true} (output arrived after the
|
|
36252
|
+
// request) or time out. The post-/compact burst case wants a FAST retry (~900ms) so the
|
|
36253
|
+
// render converges as soon as output quiets. But a pathologically large, continuously-
|
|
36254
|
+
// repainting Codex session (12k+ history lines, ~800KB/serialize) NEVER goes quiet — every
|
|
36255
|
+
// retry re-serializes ~800KB, saturating the single worker so a clean frame can never land
|
|
36256
|
+
// (the "fixes then reverts, stays bad" loop). So we back the retry off exponentially per
|
|
36257
|
+
// consecutive non-convergence and RESET the moment a fresh frame lands — fast when it can
|
|
36258
|
+
// converge, patient (giving the worker a quiet window) when it can't.
|
|
36259
|
+
const REFLOW_STALE_BACKOFF_BASE_MS = 900;
|
|
36260
|
+
const REFLOW_STALE_BACKOFF_CAP_MS = 15000;
|
|
36261
|
+
function _reflowStaleBackoffMs(s) {
|
|
36262
|
+
const streak = Math.max(1, Number(s && s._reflowStaleStreak) || 1);
|
|
36263
|
+
return Math.min(REFLOW_STALE_BACKOFF_BASE_MS * Math.pow(2, Math.min(streak - 1, 5)), REFLOW_STALE_BACKOFF_CAP_MS);
|
|
36264
|
+
}
|
|
35571
36265
|
function _scheduleAuthoritativeSnapshotRecovery(s, id, reason, opts) {
|
|
35572
36266
|
opts = opts || {};
|
|
35573
36267
|
if (!s || !id || !s.term) return false;
|
|
@@ -35682,6 +36376,18 @@ function _snapshotClientRestoreSeq(msg) {
|
|
|
35682
36376
|
return Number.isFinite(seq) && seq > 0 ? seq : 0;
|
|
35683
36377
|
}
|
|
35684
36378
|
|
|
36379
|
+
function _snapshotAuthoritativeMarker(msg) {
|
|
36380
|
+
if (!msg) return null;
|
|
36381
|
+
if (msg.snapshotCurrentMarker != null) return msg.snapshotCurrentMarker;
|
|
36382
|
+
if (msg.snapshotMarker != null) return msg.snapshotMarker;
|
|
36383
|
+
return null;
|
|
36384
|
+
}
|
|
36385
|
+
|
|
36386
|
+
function _snapshotPositiveDimension(value) {
|
|
36387
|
+
const n = Number(value);
|
|
36388
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
|
|
36389
|
+
}
|
|
36390
|
+
|
|
35685
36391
|
function _snapshotRestoreSource(s, msg) {
|
|
35686
36392
|
const explicit = String((msg && (msg.restoreSource || msg.source)) || '');
|
|
35687
36393
|
if (explicit) return explicit;
|
|
@@ -35749,6 +36455,22 @@ function _recordSnapshotRestoreApplied(s, msg, source) {
|
|
|
35749
36455
|
const seq = _snapshotClientRestoreSeq(msg);
|
|
35750
36456
|
if (seq) s._terminalRestoreAppliedSeq = Math.max(Number(s._terminalRestoreAppliedSeq || 0), seq);
|
|
35751
36457
|
const restoreSource = source || _snapshotRestoreSource(s, msg);
|
|
36458
|
+
// A non-interim snapshot/reflow is authoritative for the client grid it paints.
|
|
36459
|
+
// Keep the reconciler's server-side fingerprint/seq aligned with that accepted
|
|
36460
|
+
// frame; otherwise the 3s self-check can compare the just-fixed terminal against
|
|
36461
|
+
// a stale heartbeat and request a reconcile that reverts the render a few seconds
|
|
36462
|
+
// after a successful reflow.
|
|
36463
|
+
if (msg.fp) {
|
|
36464
|
+
s._serverFp = String(msg.fp);
|
|
36465
|
+
const cols = _snapshotPositiveDimension(msg.ptyCols) || _snapshotPositiveDimension(msg.cols) || _snapshotPositiveDimension(s.term && s.term.cols);
|
|
36466
|
+
const rows = _snapshotPositiveDimension(msg.ptyRows) || _snapshotPositiveDimension(msg.rows) || _snapshotPositiveDimension(s.term && s.term.rows);
|
|
36467
|
+
if (cols) s._serverFpCols = cols;
|
|
36468
|
+
if (rows) s._serverFpRows = rows;
|
|
36469
|
+
const marker = _snapshotAuthoritativeMarker(msg);
|
|
36470
|
+
if (marker != null) s._serverSeq = marker;
|
|
36471
|
+
s._snapshotAdoptedFpAt = Date.now();
|
|
36472
|
+
s._snapshotAdoptedFpSource = restoreSource;
|
|
36473
|
+
}
|
|
35752
36474
|
if (restoreSource === 'reflow') {
|
|
35753
36475
|
s._lastReflowSnapshotAppliedAt = Date.now();
|
|
35754
36476
|
s._lastReflowSnapshotSeq = seq || Number(s._terminalRestoreRequestSeq || 0);
|
|
@@ -35758,6 +36480,7 @@ function _recordSnapshotRestoreApplied(s, msg, source) {
|
|
|
35758
36480
|
s._terminalFinalSnapshotProtectedSource = restoreSource;
|
|
35759
36481
|
}
|
|
35760
36482
|
}
|
|
36483
|
+
window._ctmRecordSnapshotRestoreApplied = _recordSnapshotRestoreApplied;
|
|
35761
36484
|
|
|
35762
36485
|
function _snapshotRestoreReason(msg) {
|
|
35763
36486
|
return String((msg && (msg.restoreReason || msg.reason || msg.snapshotReason)) || '');
|
|
@@ -35904,6 +36627,58 @@ function onSnapshot(msg) {
|
|
|
35904
36627
|
}
|
|
35905
36628
|
}
|
|
35906
36629
|
const earlyRestoreSource = _snapshotRestoreSource(s, msg);
|
|
36630
|
+
// Option A idempotency: an authoritative keyframe that already matches the on-screen
|
|
36631
|
+
// frame needs no repaint — skip it so the PRIMARY viewer (which rendered the same output
|
|
36632
|
+
// via the live byte stream) doesn't flash on every output-quiet boundary. fp parity is
|
|
36633
|
+
// guaranteed (terminal-reconciler ↔ terminal-fingerprint). Only when output is quiet
|
|
36634
|
+
// (no pending writes), so we never compare against a mid-stream frame.
|
|
36635
|
+
if (_keyframeSyncEnabled() && (earlyRestoreSource === 'keyframe' || earlyRestoreSource === 'reconcile')
|
|
36636
|
+
&& msg.fp && s.term && window.TerminalReconciler && typeof window.TerminalReconciler.clientFingerprint === 'function'
|
|
36637
|
+
&& !(s.writer && (s.writer.scheduled || s.writer._chunking || s.writer.queue))) {
|
|
36638
|
+
let clientFp = null;
|
|
36639
|
+
try { clientFp = window.TerminalReconciler.clientFingerprint(s.term); } catch {}
|
|
36640
|
+
if (clientFp && clientFp === msg.fp) {
|
|
36641
|
+
s._keyframeIdempotentSkipAt = Date.now();
|
|
36642
|
+
_terminalRenderCheckFullyRendered(s, 'keyframe-idempotent-skip');
|
|
36643
|
+
_dismissLoadingOverlay(s);
|
|
36644
|
+
return;
|
|
36645
|
+
}
|
|
36646
|
+
}
|
|
36647
|
+
// Option A.5: a streaming keyframe (pushed mid-turn at a controlled frame rate). It must
|
|
36648
|
+
// NOT fall into the late-snapshot rejection gates below (clobber/stale/hot-activation) —
|
|
36649
|
+
// those assume a cold restore and would drop every mid-stream frame. Route it through a
|
|
36650
|
+
// narrow, settled-frame-only apply: paint ONLY when the client frame is quiescent and its
|
|
36651
|
+
// fingerprint has actually diverged (a confirmed byte-stream shear); otherwise defer.
|
|
36652
|
+
// _restoreSnapshotData's own fp-match guard makes the converged case a hard no-op, so this
|
|
36653
|
+
// never flickers a correct frame. Boundary keyframes (activation/resize/quiet) are unchanged.
|
|
36654
|
+
if (msg.restoreReason === 'stream-keyframe') {
|
|
36655
|
+
const action = _streamKeyframeAction(s, msg);
|
|
36656
|
+
s._streamKeyframeLastAction = action;
|
|
36657
|
+
s._streamKeyframeLastActionAt = Date.now();
|
|
36658
|
+
if (action === 'paint') {
|
|
36659
|
+
const intent = _captureTerminalViewportIntent(s);
|
|
36660
|
+
const incomingMarker = msg.snapshotCurrentMarker != null ? msg.snapshotCurrentMarker : msg.snapshotMarker;
|
|
36661
|
+
_restoreSnapshotData(s, msg.data, (info) => {
|
|
36662
|
+
if (!(info && info.skipped)) {
|
|
36663
|
+
if (!_restoreTerminalViewportIntent(s, intent)) { _ensureScrolledToBottom(s); }
|
|
36664
|
+
s._streamKeyframeAppliedAt = Date.now();
|
|
36665
|
+
}
|
|
36666
|
+
// Advance our applied-marker so a later stale frame can't revert this correction.
|
|
36667
|
+
try {
|
|
36668
|
+
if (incomingMarker != null && window.TerminalReconciler
|
|
36669
|
+
&& !window.TerminalReconciler.seqLess(incomingMarker, s._localSeq || 0)) {
|
|
36670
|
+
s._localSeq = incomingMarker;
|
|
36671
|
+
}
|
|
36672
|
+
} catch {}
|
|
36673
|
+
_terminalRenderCheckFullyRendered(s, 'stream-keyframe');
|
|
36674
|
+
}, { fp: msg.fp, cols: msg.ptyCols || msg.cols, rows: msg.ptyRows || msg.rows, source: 'stream-keyframe' });
|
|
36675
|
+
} else {
|
|
36676
|
+
_terminalRenderCheckFullyRendered(s, 'stream-keyframe-' + action);
|
|
36677
|
+
}
|
|
36678
|
+
_dismissLoadingOverlay(s);
|
|
36679
|
+
_primeLatestAnswerForTerminalView(msg.id);
|
|
36680
|
+
return;
|
|
36681
|
+
}
|
|
35907
36682
|
if (_snapshotShouldRejectStaleData(s, msg, earlyRestoreSource)) {
|
|
35908
36683
|
_dismissLoadingOverlay(s);
|
|
35909
36684
|
_rememberSkippedSnapshot(s, 'stale-snapshot-data', _snapshotRestoreReason(msg) || earlyRestoreSource);
|
|
@@ -35986,6 +36761,20 @@ function onSnapshot(msg) {
|
|
|
35986
36761
|
_terminalRenderCheckFullyRendered(s, 'snapshot-stale-no-data-skip');
|
|
35987
36762
|
if (state.activeTab === msg.id) focusTerminalIfSafe(msg.id);
|
|
35988
36763
|
_primeLatestAnswerForTerminalView(msg.id);
|
|
36764
|
+
// The server skipped this reflow because output arrived after the request — the agent is
|
|
36765
|
+
// still emitting (e.g. a post-/compact burst). Nothing else re-requests promptly, so the
|
|
36766
|
+
// render (and the user's typed-input echo) can lag until the 3s self-check. Schedule an
|
|
36767
|
+
// authoritative recovery so a fresh reflow lands soon after the output quiets; it
|
|
36768
|
+
// rate-limits itself, defers while typing, and — because each retry that ALSO stale-skips
|
|
36769
|
+
// re-enters this branch — forms a retry-until-settle loop that self-terminates when a fresh
|
|
36770
|
+
// frame lands (snapshot-done clears _needsAuthoritativeSnapshot at ~36319, and resets the
|
|
36771
|
+
// streak below). The retry interval BACKS OFF exponentially per consecutive stale-skip so a
|
|
36772
|
+
// never-quiet giant Codex session stops re-serializing ~800KB every ~900ms (worker
|
|
36773
|
+
// saturation = the frame can never converge); the FIRST stale-skip still retries fast.
|
|
36774
|
+
s._needsAuthoritativeSnapshot = true;
|
|
36775
|
+
s._reflowStaleStreak = (Number(s._reflowStaleStreak) || 0) + 1;
|
|
36776
|
+
const reflowBackoff = _reflowStaleBackoffMs(s);
|
|
36777
|
+
_scheduleAuthoritativeSnapshotRecovery(s, msg.id, 'reflow-stale-skip', { delayMs: reflowBackoff, minGapMs: reflowBackoff });
|
|
35989
36778
|
return;
|
|
35990
36779
|
}
|
|
35991
36780
|
// A hot terminal that already has healthy buffer content does not need a
|
|
@@ -36062,6 +36851,10 @@ function onSnapshot(msg) {
|
|
|
36062
36851
|
}
|
|
36063
36852
|
// Reset auto-reflow flag on successful snapshot
|
|
36064
36853
|
s._autoReflowAttempted = false;
|
|
36854
|
+
// A fresh AUTHORITATIVE (non-interim) frame converged → clear the reflow stale-skip
|
|
36855
|
+
// backoff streak so the next stall retries fast again (interim/cache bridges are stale,
|
|
36856
|
+
// so they must NOT reset it).
|
|
36857
|
+
if (msg.data && !msg.interim) s._reflowStaleStreak = 0;
|
|
36065
36858
|
const restoreSource = _snapshotRestoreSource(s, msg);
|
|
36066
36859
|
|
|
36067
36860
|
// Sequence guard: never apply a snapshot that is strictly BEHIND what we've
|
|
@@ -36277,6 +37070,14 @@ function onSnapshot(msg) {
|
|
|
36277
37070
|
const snapshotDone = (info) => {
|
|
36278
37071
|
_setTerminalViewPhase(s, _terminalRestorePhases().PAINT_SETTLING, 'snapshot-done');
|
|
36279
37072
|
if (info && info.skipped) {
|
|
37073
|
+
if (info.fpMatch) {
|
|
37074
|
+
_recordSnapshotRestoreApplied(s, msg, restoreSource);
|
|
37075
|
+
const marker = _snapshotAuthoritativeMarker(msg);
|
|
37076
|
+
if (marker != null) s._localSeq = marker;
|
|
37077
|
+
if (msg.restoreReason === 'reconcile' || msg.restoreSource === 'reconcile') {
|
|
37078
|
+
s._reconcileInFlightUntil = 0;
|
|
37079
|
+
}
|
|
37080
|
+
}
|
|
36280
37081
|
if (snapshotIncludesExitBanner) _markExitBannerRendered(s, msg.exitCode);
|
|
36281
37082
|
if (state.activeTab === msg.id) focusTerminalIfSafe(msg.id);
|
|
36282
37083
|
scanPromptLines(msg.id);
|
|
@@ -36303,8 +37104,8 @@ function onSnapshot(msg) {
|
|
|
36303
37104
|
// Advance the reconciler's local seq to the server's snapshot marker so we
|
|
36304
37105
|
// don't immediately re-detect divergence against pre-snapshot seq. Prefer the
|
|
36305
37106
|
// server's current marker; fall back to the request-time marker.
|
|
36306
|
-
|
|
36307
|
-
|
|
37107
|
+
const marker = _snapshotAuthoritativeMarker(msg);
|
|
37108
|
+
if (marker != null) s._localSeq = marker;
|
|
36308
37109
|
// A reconcile-sourced snapshot is the resolution of an in-flight reconcile;
|
|
36309
37110
|
// release the guard so future divergence can trigger a fresh request.
|
|
36310
37111
|
if (msg.restoreReason === 'reconcile' || msg.restoreSource === 'reconcile') {
|
|
@@ -36912,6 +37713,14 @@ async function onSessionsList(msg) {
|
|
|
36912
37713
|
existing._serverLiveStatusAt = _clientTimeMs(sess.liveStatusAt) || Date.now();
|
|
36913
37714
|
existing._serverLiveStatusEventAt = liveStatusEventAt;
|
|
36914
37715
|
}
|
|
37716
|
+
// P2: server-derived phase is the single source of truth (stored unconditionally).
|
|
37717
|
+
if (sess.phase) {
|
|
37718
|
+
existing._serverPhase = sess.phase;
|
|
37719
|
+
existing._serverPhaseCls = sess.phaseCls || '';
|
|
37720
|
+
existing._serverPhaseSource = sess.phaseSource || '';
|
|
37721
|
+
existing._serverPhaseReason = sess.phaseReason || '';
|
|
37722
|
+
existing._serverPhaseAt = _clientTimeMs(sess.phaseAt) || Date.now();
|
|
37723
|
+
}
|
|
36915
37724
|
existing._restoreResuming = !!(sess.restoreResuming || sess.restore_resuming);
|
|
36916
37725
|
existing._restoreStarting = !!(sess.restoreStarting || sess.restore_starting);
|
|
36917
37726
|
existing._restoreStatus = sess.restoreStatus || sess.restore_status || '';
|
|
@@ -38755,7 +39564,65 @@ function _walleSessionHasRunningWork(s) {
|
|
|
38755
39564
|
return active.some(t => String(t && t.status || '').toLowerCase() === 'running');
|
|
38756
39565
|
}
|
|
38757
39566
|
|
|
39567
|
+
// P2 (redesign — docs/session-status-redesign.html): the server's derived phase is
|
|
39568
|
+
// the single source of truth. The client renders it directly and may apply ONLY an
|
|
39569
|
+
// optimistic, escalate-only overlay (idle→running for instant local feedback). It
|
|
39570
|
+
// NEVER demotes or second-guesses the server. This supersedes both the keyframe
|
|
39571
|
+
// liveStatus shortcut and the precedence ladder below, which remain as the degraded
|
|
39572
|
+
// fallback for when no fresh server phase is available (just-opened tab, dropped ws).
|
|
39573
|
+
const SERVER_PHASE_TTL_MS = 15000;
|
|
39574
|
+
|
|
39575
|
+
// Escalate-only: a fresh keystroke or local PTY byte may lift a server idle to
|
|
39576
|
+
// running until the next server push reconciles it. Never the reverse.
|
|
39577
|
+
function _clientHasFreshOptimisticRunning(s, now) {
|
|
39578
|
+
now = now || Date.now();
|
|
39579
|
+
const lastInput = s._lastStatusInputAt || 0;
|
|
39580
|
+
const lastOut = Math.max(s._lastOutputAt || 0, s.meta?.lastPtyActivity || 0);
|
|
39581
|
+
return (lastInput && (now - lastInput) < 3000) || (lastOut && (now - lastOut) < 5000);
|
|
39582
|
+
}
|
|
39583
|
+
|
|
39584
|
+
function _clientServerPhaseStatus(s) {
|
|
39585
|
+
if (typeof SessionPhase === 'undefined' || !s || !s._serverPhase || !s._serverPhaseAt) return null;
|
|
39586
|
+
const now = Date.now();
|
|
39587
|
+
if ((now - s._serverPhaseAt) >= SERVER_PHASE_TTL_MS) return null; // stale → fall back
|
|
39588
|
+
let phase = s._serverPhase;
|
|
39589
|
+
if (phase === 'idle' && _clientHasFreshOptimisticRunning(s, now)) phase = 'running';
|
|
39590
|
+
const cls = SessionPhase.phaseToCls(phase);
|
|
39591
|
+
if (!cls) return null;
|
|
39592
|
+
return {
|
|
39593
|
+
cls,
|
|
39594
|
+
text: SessionPhase.PHASE_TEXT[phase] || 'Idle',
|
|
39595
|
+
source: 'server-phase:' + (s._serverPhaseSource || phase),
|
|
39596
|
+
phase,
|
|
39597
|
+
};
|
|
39598
|
+
}
|
|
39599
|
+
|
|
38758
39600
|
function getSessionStatus(s) {
|
|
39601
|
+
// Single source of truth: render the server's derived phase when fresh.
|
|
39602
|
+
if (s && !(s._exited || s.meta?.liveStatus === 'exited' || s.meta?.status === 'exited')) {
|
|
39603
|
+
const serverPhase = _clientServerPhaseStatus(s);
|
|
39604
|
+
if (serverPhase) return statusAfterAttentionDismissal(s, serverPhase);
|
|
39605
|
+
}
|
|
39606
|
+
// Phase 4 (Option C): ONE status owner. With keyframe-sync on, trust the server's
|
|
39607
|
+
// authoritative liveStatus directly when fresh — skipping the client-side busy-line /
|
|
39608
|
+
// running-hold inference ladder (the "second state machine"). The server already computes
|
|
39609
|
+
// a complete status (_standupLiveStatusWithReasonForSession). Exited + Wall-E special
|
|
39610
|
+
// cases still apply; an explicit server-flagged waiting prompt still wins over a generic
|
|
39611
|
+
// idle heartbeat; and if the server status is missing/stale we fall through to the
|
|
39612
|
+
// existing resolver as a safety net. Flag-off keeps the legacy precedence machine.
|
|
39613
|
+
if (_keyframeSyncEnabled() && s
|
|
39614
|
+
&& !(s._exited || s.meta?.liveStatus === 'exited' || s.meta?.status === 'exited')
|
|
39615
|
+
&& s.meta?.type !== 'walle') {
|
|
39616
|
+
const now = Date.now();
|
|
39617
|
+
const live = liveStatusResult(s._serverLiveStatus);
|
|
39618
|
+
if (live && s._serverLiveStatusAt && (now - s._serverLiveStatusAt) < SERVER_LIVE_STATUS_TTL_MS) {
|
|
39619
|
+
if (live.cls === 'idle' && s._waitingForInput) {
|
|
39620
|
+
return statusAfterAttentionDismissal(s, { cls: 'waiting', text: 'Needs You' });
|
|
39621
|
+
}
|
|
39622
|
+
return statusAfterAttentionDismissal(s, live);
|
|
39623
|
+
}
|
|
39624
|
+
// else: server status silent/stale → fall through to the full resolver below.
|
|
39625
|
+
}
|
|
38759
39626
|
if (typeof SessionStatusPrecedence !== 'undefined' && SessionStatusPrecedence.resolveSessionStatus) {
|
|
38760
39627
|
const now = Date.now();
|
|
38761
39628
|
const codexRunningHold = _clientCodexRunningHoldResult(s, now);
|
|
@@ -48514,12 +49381,26 @@ function onSessionActivity(msg) {
|
|
|
48514
49381
|
waitingForInput,
|
|
48515
49382
|
waitingForInputAt,
|
|
48516
49383
|
waitingReason,
|
|
49384
|
+
phase,
|
|
49385
|
+
phaseCls,
|
|
49386
|
+
phaseSource,
|
|
49387
|
+
phaseReason,
|
|
48517
49388
|
tokens,
|
|
48518
49389
|
} of msg.sessions) {
|
|
48519
49390
|
const s = state.sessions.get(id);
|
|
48520
49391
|
if (!s) continue;
|
|
48521
49392
|
if (tokens !== undefined) _applySessionTokens(s, tokens);
|
|
48522
49393
|
s._serverLiveStatusReason = statusReason || status_reason || '';
|
|
49394
|
+
// P2: the server's derived phase is the single source of truth. Store it
|
|
49395
|
+
// unconditionally — the server already did all staleness/authority reasoning,
|
|
49396
|
+
// so the client must NOT re-gate or second-guess it here.
|
|
49397
|
+
if (phase) {
|
|
49398
|
+
s._serverPhase = phase;
|
|
49399
|
+
s._serverPhaseCls = phaseCls || '';
|
|
49400
|
+
s._serverPhaseSource = phaseSource || '';
|
|
49401
|
+
s._serverPhaseReason = phaseReason || '';
|
|
49402
|
+
s._serverPhaseAt = Date.now();
|
|
49403
|
+
}
|
|
48523
49404
|
const oldBucket = activeActivityBucket(s);
|
|
48524
49405
|
const oldStatus = getSessionStatus(s).cls;
|
|
48525
49406
|
const hasServerTs = ts !== null && ts !== undefined && ts !== '';
|