create-walle 0.9.11 → 0.9.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/package.json +2 -2
- package/template/bin/dev.sh +7 -1
- package/template/bin/setup.js +53 -9
- package/template/bin/sync-images.js +53 -0
- package/template/builder-journal.md +17 -0
- package/template/claude-task-manager/api-prompts.js +98 -13
- package/template/claude-task-manager/api-reviews.js +82 -5
- package/template/claude-task-manager/db.js +32 -5
- package/template/claude-task-manager/docs/session-capture-foundation-design.md +1273 -0
- package/template/claude-task-manager/lib/claude-desktop-sessions.js +696 -0
- package/template/claude-task-manager/lib/coding-agent-models.js +49 -1
- package/template/claude-task-manager/lib/session-capture.js +421 -0
- package/template/claude-task-manager/lib/session-history.js +135 -15
- package/template/claude-task-manager/lib/session-jobs.js +10 -5
- package/template/claude-task-manager/lib/session-stream.js +87 -19
- package/template/claude-task-manager/lib/setup-provider-config.js +115 -0
- package/template/claude-task-manager/lib/walle-ctm-history.js +72 -0
- package/template/claude-task-manager/lib/walle-session-context.js +61 -0
- package/template/claude-task-manager/lib/walle-transcript.js +176 -0
- package/template/claude-task-manager/public/css/setup.css +35 -8
- package/template/claude-task-manager/public/css/walle-session.css +56 -0
- package/template/claude-task-manager/public/css/walle.css +120 -0
- package/template/claude-task-manager/public/index.html +814 -181
- package/template/claude-task-manager/public/js/message-renderer.js +148 -19
- package/template/claude-task-manager/public/js/reviews.js +120 -62
- package/template/claude-task-manager/public/js/setup.js +75 -31
- package/template/claude-task-manager/public/js/stream-view.js +115 -55
- package/template/claude-task-manager/public/js/walle-session.js +84 -2
- package/template/claude-task-manager/public/js/walle.js +308 -54
- package/template/claude-task-manager/server.js +1092 -146
- package/template/claude-task-manager/session-integrity.js +181 -54
- package/template/claude-task-manager/session-utils.js +123 -41
- package/template/claude-task-manager/workers/state-detectors/codex.js +5 -2
- package/template/package.json +1 -1
- package/template/wall-e/adapters/ctm.js +39 -18
- package/template/wall-e/agent-runners/contract.js +17 -0
- package/template/wall-e/agent-runners/index.js +22 -0
- package/template/wall-e/agent-runtime/harness.js +212 -0
- package/template/wall-e/agent-runtime/index.js +8 -0
- package/template/wall-e/agent-runtime/registry.js +67 -0
- package/template/wall-e/agent-runtime/session-store.js +179 -0
- package/template/wall-e/agent-runtime/spawn.js +208 -0
- package/template/wall-e/api-walle.js +174 -7
- package/template/wall-e/brain.js +266 -28
- package/template/wall-e/channels/policy.js +88 -0
- package/template/wall-e/channels/registry.js +15 -1
- package/template/wall-e/channels/reply-dispatcher.js +70 -0
- package/template/wall-e/channels/session-bindings.js +51 -0
- package/template/wall-e/chat/code-review-context.js +29 -0
- package/template/wall-e/chat.js +188 -42
- package/template/wall-e/coding/acp-adapter.js +188 -0
- package/template/wall-e/coding/agent-catalog.js +129 -0
- package/template/wall-e/coding/compaction-service.js +247 -0
- package/template/wall-e/coding/execution-trace.js +3 -0
- package/template/wall-e/coding/instruction-service.js +224 -0
- package/template/wall-e/coding/model-message.js +67 -0
- package/template/wall-e/coding/permission-rules-store.js +111 -0
- package/template/wall-e/coding/permission-service.js +266 -0
- package/template/wall-e/coding/prompt-bundle.js +67 -0
- package/template/wall-e/coding/prompt-runtime.js +243 -0
- package/template/wall-e/coding/provider-transform.js +188 -0
- package/template/wall-e/coding/runtime-mode.js +132 -0
- package/template/wall-e/coding/snapshot-service.js +155 -0
- package/template/wall-e/coding/stream-processor.js +268 -0
- package/template/wall-e/coding/task-tool.js +255 -0
- package/template/wall-e/coding/tool-registry.js +361 -0
- package/template/wall-e/coding/transcript-writer.js +143 -0
- package/template/wall-e/coding/workspace-replay.js +324 -0
- package/template/wall-e/coding-context.js +4 -22
- package/template/wall-e/coding-orchestrator.js +307 -18
- package/template/wall-e/coding-prompts.js +44 -3
- package/template/wall-e/context/context-builder.js +43 -1
- package/template/wall-e/context/topic-matcher.js +1 -1
- package/template/wall-e/eval/agent-runner.js +59 -13
- package/template/wall-e/eval/benchmarks/memory-retrieval.json +155 -57
- package/template/wall-e/eval/benchmarks.js +100 -16
- package/template/wall-e/eval/eval-orchestrator.js +218 -8
- package/template/wall-e/eval/harvester.js +62 -5
- package/template/wall-e/eval/head-to-head.js +23 -2
- package/template/wall-e/eval/humaneval-adapter.js +30 -5
- package/template/wall-e/eval/livecodebench-adapter.js +29 -5
- package/template/wall-e/eval/manifest.js +186 -0
- package/template/wall-e/eval/run-agent-benchmarks.js +66 -2
- package/template/wall-e/eval/session-retrieval-benchmark.js +150 -0
- package/template/wall-e/eval/session-transcripts.js +57 -4
- package/template/wall-e/eval/swebench-adapter.js +109 -3
- package/template/wall-e/evaluation/agent-router.js +53 -1
- package/template/wall-e/evaluation/coding-quorum.js +48 -1
- package/template/wall-e/evaluation/router.js +4 -2
- package/template/wall-e/evaluation/tier-selector.js +11 -1
- package/template/wall-e/extraction/contradiction.js +2 -2
- package/template/wall-e/extraction/indexer.js +2 -1
- package/template/wall-e/extraction/knowledge-extractor.js +2 -2
- package/template/wall-e/hooks/cli.js +92 -0
- package/template/wall-e/hooks/discovery.js +119 -0
- package/template/wall-e/hooks/index.js +7 -0
- package/template/wall-e/hooks/manifest.js +55 -0
- package/template/wall-e/hooks/runtime.js +84 -0
- package/template/wall-e/hooks/session-memory.js +225 -0
- package/template/wall-e/http/auth.js +6 -2
- package/template/wall-e/http/chat-api.js +54 -8
- package/template/wall-e/integrations/claude-plugin/hooks/hooks.json +27 -0
- package/template/wall-e/integrations/claude-plugin/hooks/walle-precompact-hook.sh +5 -0
- package/template/wall-e/integrations/claude-plugin/hooks/walle-stop-hook.sh +5 -0
- package/template/wall-e/integrations/codex-plugin/hooks/walle-hook.sh +7 -0
- package/template/wall-e/integrations/codex-plugin/hooks.json +37 -0
- package/template/wall-e/listening/calendar.js +3 -1
- package/template/wall-e/llm/client.js +64 -10
- package/template/wall-e/llm/google.js +39 -5
- package/template/wall-e/llm/ollama.js +1 -1
- package/template/wall-e/llm/ollama.plugin.json +1 -1
- package/template/wall-e/llm/provider-availability.js +10 -0
- package/template/wall-e/llm/provider-error.js +269 -0
- package/template/wall-e/llm/tool-adapter.js +48 -12
- package/template/wall-e/loops/boot.js +2 -1
- package/template/wall-e/loops/initiative.js +2 -2
- package/template/wall-e/loops/tasks.js +8 -47
- package/template/wall-e/loops/workspace-prompts.js +20 -0
- package/template/wall-e/mcp-server.js +442 -1
- package/template/wall-e/memory/session-ingest-service.js +159 -0
- package/template/wall-e/memory/source-indexer.js +289 -0
- package/template/wall-e/plugins/discovery.js +83 -0
- package/template/wall-e/plugins/manifest-loader.js +50 -10
- package/template/wall-e/plugins/manifest-schema.js +69 -0
- package/template/wall-e/plugins/model-catalog.js +55 -0
- package/template/wall-e/prompts/coding/base.txt +2 -0
- package/template/wall-e/prompts/coding/deepseek.txt +1 -0
- package/template/wall-e/prompts/coding/memory-protocol.md +9 -0
- package/template/wall-e/prompts/coding/plan.txt +1 -0
- package/template/wall-e/runtime/execution-trace.js +220 -0
- package/template/wall-e/security/audit.js +266 -0
- package/template/wall-e/security/ssrf.js +236 -0
- package/template/wall-e/session-files.js +303 -0
- package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +3 -0
- package/template/wall-e/skills/_bundled/slack-sync/SKILL.md +3 -0
- package/template/wall-e/skills/internal-skill-registry.js +2 -2
- package/template/wall-e/skills/script-skill-runner.js +143 -0
- package/template/wall-e/skills/skill-executor.js +5 -6
- package/template/wall-e/skills/skill-fallback.js +3 -1
- package/template/wall-e/skills/skill-harness-registry.js +7 -8
- package/template/wall-e/skills/skill-planner.js +52 -4
- package/template/wall-e/skills/slack-ingest.js +11 -3
- package/template/wall-e/sources/base.js +90 -0
- package/template/wall-e/sources/builtin.js +33 -0
- package/template/wall-e/sources/claude-code-jsonl.js +78 -0
- package/template/wall-e/sources/codex-jsonl.js +125 -0
- package/template/wall-e/sources/coding-session-utils.js +117 -0
- package/template/wall-e/sources/contract-suite.js +59 -0
- package/template/wall-e/sources/gemini-jsonl.js +85 -0
- package/template/wall-e/sources/index.js +9 -0
- package/template/wall-e/sources/jsonl-utils.js +181 -0
- package/template/wall-e/sources/record-types.js +252 -0
- package/template/wall-e/sources/registry.js +92 -0
- package/template/wall-e/sources/transforms.js +100 -0
- package/template/wall-e/sources/walle-jsonl.js +108 -0
- package/template/wall-e/tools/coding-middleware.js +31 -1
- package/template/wall-e/tools/file-tracker.js +25 -1
- package/template/wall-e/tools/local-tools.js +75 -47
- package/template/wall-e/tools/session-sharing.js +68 -1
- package/template/wall-e/tools/shell-analyzer.js +1 -1
- package/template/wall-e/tools/shell-policy.js +47 -0
- package/template/wall-e/tools/snapshot.js +42 -0
- package/template/wall-e/training/harvester.js +62 -5
- package/template/wall-e/utils/repair.js +253 -1
- package/template/website/index.html +3 -3
- package/template/wall-e/skills/_bundled/slack-mentions/.watched-threads.json +0 -18
|
@@ -191,6 +191,15 @@
|
|
|
191
191
|
transition: all 0.15s;
|
|
192
192
|
}
|
|
193
193
|
.btn:hover { background: var(--border); }
|
|
194
|
+
.btn[aria-disabled="true"],
|
|
195
|
+
.btn:disabled {
|
|
196
|
+
opacity: 0.55;
|
|
197
|
+
cursor: not-allowed;
|
|
198
|
+
}
|
|
199
|
+
.btn[aria-disabled="true"]:hover,
|
|
200
|
+
.btn:disabled:hover {
|
|
201
|
+
background: var(--bg-lighter);
|
|
202
|
+
}
|
|
194
203
|
.pe-copilot-tab.active { background: var(--accent); color: #1a1b26; border-color: var(--accent); }
|
|
195
204
|
.pe-copilot-tab.active:hover { background: var(--accent-hover); }
|
|
196
205
|
.btn.primary { background: var(--accent); color: #1a1b26; border-color: var(--accent); }
|
|
@@ -302,6 +311,7 @@
|
|
|
302
311
|
.session-item.active .idle-hint { color: #1a1b26; opacity: 0.6; }
|
|
303
312
|
.session-item .label {
|
|
304
313
|
flex: 1;
|
|
314
|
+
min-width: 0;
|
|
305
315
|
overflow: hidden;
|
|
306
316
|
text-overflow: ellipsis;
|
|
307
317
|
white-space: nowrap;
|
|
@@ -414,6 +424,10 @@
|
|
|
414
424
|
#session-list .agent-badge { display: none; }
|
|
415
425
|
/* Branch badge */
|
|
416
426
|
.branch-badge {
|
|
427
|
+
display: inline-block;
|
|
428
|
+
max-width: 92px;
|
|
429
|
+
overflow: hidden;
|
|
430
|
+
text-overflow: ellipsis;
|
|
417
431
|
font-size: 9px;
|
|
418
432
|
background: rgba(125, 211, 252, 0.15);
|
|
419
433
|
color: #7dd3fc;
|
|
@@ -423,7 +437,38 @@
|
|
|
423
437
|
flex-shrink: 0;
|
|
424
438
|
margin-left: 2px;
|
|
425
439
|
}
|
|
440
|
+
.session-item.has-worktree-attn .branch-badge { max-width: 60px; }
|
|
426
441
|
.session-item.active .branch-badge { color: #0ea5e9; background: rgba(14, 165, 233, 0.15); }
|
|
442
|
+
.worktree-attn-badge {
|
|
443
|
+
display: inline-flex;
|
|
444
|
+
align-items: center;
|
|
445
|
+
gap: 4px;
|
|
446
|
+
min-width: 0;
|
|
447
|
+
max-width: 70px;
|
|
448
|
+
font-size: 9px;
|
|
449
|
+
font-weight: 700;
|
|
450
|
+
line-height: 1.3;
|
|
451
|
+
letter-spacing: 0.01em;
|
|
452
|
+
background: rgba(245, 158, 11, 0.14);
|
|
453
|
+
border: 1px solid rgba(245, 158, 11, 0.28);
|
|
454
|
+
color: #f6c177;
|
|
455
|
+
padding: 1px 5px;
|
|
456
|
+
border-radius: 5px;
|
|
457
|
+
white-space: nowrap;
|
|
458
|
+
overflow: hidden;
|
|
459
|
+
text-overflow: ellipsis;
|
|
460
|
+
flex-shrink: 0;
|
|
461
|
+
}
|
|
462
|
+
.worktree-attn-badge .wt-part {
|
|
463
|
+
display: inline-flex;
|
|
464
|
+
align-items: center;
|
|
465
|
+
gap: 1px;
|
|
466
|
+
}
|
|
467
|
+
.session-item.active .worktree-attn-badge {
|
|
468
|
+
background: rgba(245, 158, 11, 0.28);
|
|
469
|
+
border-color: rgba(120, 53, 15, 0.22);
|
|
470
|
+
color: #3f2a05;
|
|
471
|
+
}
|
|
427
472
|
/* cwd conflict banner */
|
|
428
473
|
.cwd-conflict-banner {
|
|
429
474
|
background: rgba(251, 191, 36, 0.12);
|
|
@@ -997,6 +1042,18 @@
|
|
|
997
1042
|
opacity: 0.6;
|
|
998
1043
|
}
|
|
999
1044
|
.thought-group:hover { opacity: 0.8; }
|
|
1045
|
+
.thought-group.tool-activity-group {
|
|
1046
|
+
background: rgba(224, 175, 104, 0.025);
|
|
1047
|
+
border-left-color: rgba(224, 175, 104, 0.32);
|
|
1048
|
+
}
|
|
1049
|
+
.thought-group.tool-activity-group .thought-group-label {
|
|
1050
|
+
color: var(--yellow);
|
|
1051
|
+
}
|
|
1052
|
+
.thought-group.tool-activity-group .thought-group-preview {
|
|
1053
|
+
font-family: 'SF Mono', ui-monospace, monospace;
|
|
1054
|
+
font-style: normal;
|
|
1055
|
+
font-size: 10.5px;
|
|
1056
|
+
}
|
|
1000
1057
|
.thought-group .thought-group-header {
|
|
1001
1058
|
padding: 6px 12px;
|
|
1002
1059
|
cursor: pointer;
|
|
@@ -1065,6 +1122,14 @@
|
|
|
1065
1122
|
.thought-group .thought-group-items .review-msg .msg-header {
|
|
1066
1123
|
margin-bottom: 2px;
|
|
1067
1124
|
}
|
|
1125
|
+
.thought-group.tool-activity-group .thought-group-items .review-msg {
|
|
1126
|
+
background: rgba(224, 175, 104, 0.025);
|
|
1127
|
+
}
|
|
1128
|
+
.thought-group .tool-result-item .msg-text {
|
|
1129
|
+
color: var(--fg-dim);
|
|
1130
|
+
font-family: 'SF Mono', ui-monospace, monospace;
|
|
1131
|
+
font-size: 11.5px;
|
|
1132
|
+
}
|
|
1068
1133
|
|
|
1069
1134
|
/* Summary/system messages */
|
|
1070
1135
|
.review-msg.summary {
|
|
@@ -2090,6 +2155,78 @@
|
|
|
2090
2155
|
gap: 8px;
|
|
2091
2156
|
margin-top: 16px;
|
|
2092
2157
|
}
|
|
2158
|
+
.update-wizard-overlay { z-index: 130; }
|
|
2159
|
+
.update-wizard-modal {
|
|
2160
|
+
width: min(520px, 92vw);
|
|
2161
|
+
min-width: 0;
|
|
2162
|
+
padding: 0;
|
|
2163
|
+
overflow: hidden;
|
|
2164
|
+
}
|
|
2165
|
+
.update-wizard-head {
|
|
2166
|
+
display: flex;
|
|
2167
|
+
gap: 12px;
|
|
2168
|
+
padding: 18px 20px 14px;
|
|
2169
|
+
border-bottom: 1px solid var(--border);
|
|
2170
|
+
background: linear-gradient(135deg, rgba(122,162,247,0.14), rgba(187,154,247,0.07));
|
|
2171
|
+
}
|
|
2172
|
+
.update-wizard-icon {
|
|
2173
|
+
width: 34px;
|
|
2174
|
+
height: 34px;
|
|
2175
|
+
border-radius: 8px;
|
|
2176
|
+
background: var(--accent);
|
|
2177
|
+
color: #1a1b26;
|
|
2178
|
+
display: flex;
|
|
2179
|
+
align-items: center;
|
|
2180
|
+
justify-content: center;
|
|
2181
|
+
font-size: 18px;
|
|
2182
|
+
font-weight: 800;
|
|
2183
|
+
flex-shrink: 0;
|
|
2184
|
+
}
|
|
2185
|
+
.update-wizard-title h3 { margin: 0 0 4px; font-size: 16px; }
|
|
2186
|
+
.update-wizard-title p { margin: 0; color: var(--fg-dim); font-size: 12px; line-height: 1.5; }
|
|
2187
|
+
.update-wizard-body { padding: 16px 20px 18px; }
|
|
2188
|
+
.update-version-grid {
|
|
2189
|
+
display: grid;
|
|
2190
|
+
grid-template-columns: 1fr 1fr;
|
|
2191
|
+
gap: 8px;
|
|
2192
|
+
margin-bottom: 12px;
|
|
2193
|
+
}
|
|
2194
|
+
.update-version-cell {
|
|
2195
|
+
border: 1px solid var(--border);
|
|
2196
|
+
border-radius: 8px;
|
|
2197
|
+
padding: 10px 12px;
|
|
2198
|
+
background: var(--bg);
|
|
2199
|
+
}
|
|
2200
|
+
.update-version-cell span {
|
|
2201
|
+
display: block;
|
|
2202
|
+
color: var(--fg-dim);
|
|
2203
|
+
font-size: 10px;
|
|
2204
|
+
text-transform: uppercase;
|
|
2205
|
+
letter-spacing: 0.04em;
|
|
2206
|
+
margin-bottom: 4px;
|
|
2207
|
+
}
|
|
2208
|
+
.update-version-cell strong {
|
|
2209
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
2210
|
+
color: var(--fg);
|
|
2211
|
+
font-size: 13px;
|
|
2212
|
+
}
|
|
2213
|
+
.update-wizard-note {
|
|
2214
|
+
color: var(--fg-dim);
|
|
2215
|
+
font-size: 12px;
|
|
2216
|
+
line-height: 1.5;
|
|
2217
|
+
margin: 0;
|
|
2218
|
+
}
|
|
2219
|
+
.update-wizard-actions {
|
|
2220
|
+
display: flex;
|
|
2221
|
+
justify-content: flex-end;
|
|
2222
|
+
gap: 8px;
|
|
2223
|
+
margin-top: 16px;
|
|
2224
|
+
}
|
|
2225
|
+
@media (max-width: 520px) {
|
|
2226
|
+
.update-version-grid { grid-template-columns: 1fr; }
|
|
2227
|
+
.update-wizard-actions { flex-wrap: wrap; }
|
|
2228
|
+
.update-wizard-actions .btn { flex: 1 1 auto; }
|
|
2229
|
+
}
|
|
2093
2230
|
|
|
2094
2231
|
/* Agent Picker Grid */
|
|
2095
2232
|
.ns-agent-grid {
|
|
@@ -2827,6 +2964,35 @@
|
|
|
2827
2964
|
<button id="update-apply-btn" onclick="applyUpdate()" style="background:#7aa2f7;color:#1a1b26;border:none;padding:3px 10px;border-radius:4px;font-size:11px;cursor:pointer;font-weight:600;">Update Now</button>
|
|
2828
2965
|
<button onclick="dismissUpdate()" style="background:none;border:none;color:var(--fg-dim);cursor:pointer;margin-left:auto;opacity:0.6;font-size:14px;">×</button>
|
|
2829
2966
|
</div>
|
|
2967
|
+
<div class="modal-overlay update-wizard-overlay hidden" id="update-wizard" role="dialog" aria-modal="true" aria-labelledby="update-wizard-heading">
|
|
2968
|
+
<div class="modal update-wizard-modal">
|
|
2969
|
+
<div class="update-wizard-head">
|
|
2970
|
+
<div class="update-wizard-icon">↑</div>
|
|
2971
|
+
<div class="update-wizard-title">
|
|
2972
|
+
<h3 id="update-wizard-heading">Upgrade CTM?</h3>
|
|
2973
|
+
<p>A newer CTM release is available.</p>
|
|
2974
|
+
</div>
|
|
2975
|
+
</div>
|
|
2976
|
+
<div class="update-wizard-body">
|
|
2977
|
+
<div class="update-version-grid">
|
|
2978
|
+
<div class="update-version-cell">
|
|
2979
|
+
<span>Installed</span>
|
|
2980
|
+
<strong id="update-current-version">v?</strong>
|
|
2981
|
+
</div>
|
|
2982
|
+
<div class="update-version-cell">
|
|
2983
|
+
<span>Available</span>
|
|
2984
|
+
<strong id="update-latest-version">v?</strong>
|
|
2985
|
+
</div>
|
|
2986
|
+
</div>
|
|
2987
|
+
<p class="update-wizard-note">The updater will run in the background and CTM will restart when the upgrade is ready.</p>
|
|
2988
|
+
<div class="update-wizard-actions">
|
|
2989
|
+
<button class="btn" onclick="snoozeUpdateWizard()">Later</button>
|
|
2990
|
+
<button class="btn" onclick="dismissUpdate()">Skip This Version</button>
|
|
2991
|
+
<button class="btn primary" id="update-wizard-apply-btn" onclick="applyUpdate()">Upgrade CTM</button>
|
|
2992
|
+
</div>
|
|
2993
|
+
</div>
|
|
2994
|
+
</div>
|
|
2995
|
+
</div>
|
|
2830
2996
|
<div id="main">
|
|
2831
2997
|
<div id="sidebar">
|
|
2832
2998
|
<div class="sidebar-section">
|
|
@@ -3035,7 +3201,7 @@
|
|
|
3035
3201
|
</div>
|
|
3036
3202
|
<div style="display:flex;gap:8px;align-items:center;">
|
|
3037
3203
|
<button id="wt-prune-all-btn" class="btn" style="display:none;font-size:12px;padding:6px 10px;color:var(--red,#f7768e);border-color:var(--red,#f7768e);" onclick="submitPruneGhosts()" title="Remove ghost worktrees with corrupt or missing paths">🗑 Prune ghosts</button>
|
|
3038
|
-
<button id="wt-sync-all-btn" class="btn" style="display:none;font-size:12px;padding:6px 10px;background:rgba(224,175,104,0.10);color:#e0af68;border:1px solid rgba(224,175,104,0.35);" onclick="submitSyncAllWorktrees()" title="Sync every clean branch that is behind main">↓ Sync all</button>
|
|
3204
|
+
<button id="wt-sync-all-btn" class="btn" style="display:none;font-size:12px;padding:6px 10px;background:rgba(224,175,104,0.10);color:#e0af68;border:1px solid rgba(224,175,104,0.35);" onclick="submitSyncAllWorktrees()" title="Sync every clean inactive branch that is behind main">↓ Sync all</button>
|
|
3039
3205
|
<button class="btn" style="font-size:12px;padding:6px 10px;" onclick="loadWorktreesPanel()" title="Refresh worktree list">↻</button>
|
|
3040
3206
|
<button class="btn primary" style="font-size:12px;padding:6px 14px;" onclick="showCreateWorktreeDialog()">+ New Worktree</button>
|
|
3041
3207
|
</div>
|
|
@@ -3095,7 +3261,7 @@
|
|
|
3095
3261
|
<span class="pcard-default-tag" data-default-tag="anthropic" style="display:none;font-size:10px;background:var(--accent);color:#0d1117;padding:1px 6px;border-radius:3px;font-weight:600;letter-spacing:0.5px;">DEFAULT</span>
|
|
3096
3262
|
</div>
|
|
3097
3263
|
<div style="display:flex;align-items:center;gap:8px;">
|
|
3098
|
-
<button class="pcard-star" data-default-btn="anthropic" title="Set as default provider" aria-label="Set Anthropic as default" style="background:none;border:none;cursor:pointer;font-size:18px;color:var(--fg-dim);padding:0 4px;line-height:1;">☆</button>
|
|
3264
|
+
<button class="pcard-star" data-default-btn="anthropic" title="Set as default Wall-E provider" aria-label="Set Anthropic as default Wall-E provider" style="background:none;border:none;cursor:pointer;font-size:18px;color:var(--fg-dim);padding:0 4px;line-height:1;">☆</button>
|
|
3099
3265
|
<label class="pcard-toggle" style="position:relative;display:inline-block;width:36px;height:20px;flex-shrink:0;">
|
|
3100
3266
|
<input type="checkbox" data-toggle="anthropic" style="opacity:0;width:0;height:0;">
|
|
3101
3267
|
<span class="pcard-slider" style="position:absolute;cursor:pointer;inset:0;background:var(--border);border-radius:20px;transition:0.2s;"></span>
|
|
@@ -3183,7 +3349,7 @@
|
|
|
3183
3349
|
<span class="pcard-default-tag" data-default-tag="openai" style="display:none;font-size:10px;background:var(--accent);color:#0d1117;padding:1px 6px;border-radius:3px;font-weight:600;letter-spacing:0.5px;">DEFAULT</span>
|
|
3184
3350
|
</div>
|
|
3185
3351
|
<div style="display:flex;align-items:center;gap:8px;">
|
|
3186
|
-
<button class="pcard-star" data-default-btn="openai" title="Set as default provider" aria-label="Set OpenAI as default" style="background:none;border:none;cursor:pointer;font-size:18px;color:var(--fg-dim);padding:0 4px;line-height:1;">☆</button>
|
|
3352
|
+
<button class="pcard-star" data-default-btn="openai" title="Set as default Wall-E provider" aria-label="Set OpenAI as default Wall-E provider" style="background:none;border:none;cursor:pointer;font-size:18px;color:var(--fg-dim);padding:0 4px;line-height:1;">☆</button>
|
|
3187
3353
|
<label class="pcard-toggle" style="position:relative;display:inline-block;width:36px;height:20px;flex-shrink:0;">
|
|
3188
3354
|
<input type="checkbox" data-toggle="openai" style="opacity:0;width:0;height:0;">
|
|
3189
3355
|
<span class="pcard-slider" style="position:absolute;cursor:pointer;inset:0;background:var(--border);border-radius:20px;transition:0.2s;"></span>
|
|
@@ -3245,7 +3411,7 @@
|
|
|
3245
3411
|
<span class="pcard-default-tag" data-default-tag="google" style="display:none;font-size:10px;background:var(--accent);color:#0d1117;padding:1px 6px;border-radius:3px;font-weight:600;letter-spacing:0.5px;">DEFAULT</span>
|
|
3246
3412
|
</div>
|
|
3247
3413
|
<div style="display:flex;align-items:center;gap:8px;">
|
|
3248
|
-
<button class="pcard-star" data-default-btn="google" title="Set as default provider" aria-label="Set Google as default" style="background:none;border:none;cursor:pointer;font-size:18px;color:var(--fg-dim);padding:0 4px;line-height:1;">☆</button>
|
|
3414
|
+
<button class="pcard-star" data-default-btn="google" title="Set as default Wall-E provider" aria-label="Set Google as default Wall-E provider" style="background:none;border:none;cursor:pointer;font-size:18px;color:var(--fg-dim);padding:0 4px;line-height:1;">☆</button>
|
|
3249
3415
|
<label class="pcard-toggle" style="position:relative;display:inline-block;width:36px;height:20px;flex-shrink:0;">
|
|
3250
3416
|
<input type="checkbox" data-toggle="google" style="opacity:0;width:0;height:0;">
|
|
3251
3417
|
<span class="pcard-slider" style="position:absolute;cursor:pointer;inset:0;background:var(--border);border-radius:20px;transition:0.2s;"></span>
|
|
@@ -3306,7 +3472,7 @@
|
|
|
3306
3472
|
<span class="pcard-default-tag" data-default-tag="ollama" style="display:none;font-size:10px;background:var(--accent);color:#0d1117;padding:1px 6px;border-radius:3px;font-weight:600;letter-spacing:0.5px;">DEFAULT</span>
|
|
3307
3473
|
</div>
|
|
3308
3474
|
<div style="display:flex;align-items:center;gap:8px;">
|
|
3309
|
-
<button class="pcard-star" data-default-btn="ollama" title="Set as default provider" aria-label="Set Ollama as default" style="background:none;border:none;cursor:pointer;font-size:18px;color:var(--fg-dim);padding:0 4px;line-height:1;">☆</button>
|
|
3475
|
+
<button class="pcard-star" data-default-btn="ollama" title="Set as default Wall-E provider" aria-label="Set Ollama as default Wall-E provider" style="background:none;border:none;cursor:pointer;font-size:18px;color:var(--fg-dim);padding:0 4px;line-height:1;">☆</button>
|
|
3310
3476
|
<label class="pcard-toggle" style="position:relative;display:inline-block;width:36px;height:20px;flex-shrink:0;">
|
|
3311
3477
|
<input type="checkbox" data-toggle="ollama" style="opacity:0;width:0;height:0;">
|
|
3312
3478
|
<span class="pcard-slider" style="position:absolute;cursor:pointer;inset:0;background:var(--border);border-radius:20px;transition:0.2s;"></span>
|
|
@@ -3349,7 +3515,7 @@
|
|
|
3349
3515
|
<span class="pcard-default-tag" data-default-tag="deepseek" style="display:none;font-size:10px;background:var(--accent);color:#0d1117;padding:1px 6px;border-radius:3px;font-weight:600;letter-spacing:0.5px;">DEFAULT</span>
|
|
3350
3516
|
</div>
|
|
3351
3517
|
<div style="display:flex;align-items:center;gap:8px;">
|
|
3352
|
-
<button class="pcard-star" data-default-btn="deepseek" title="Set as default provider" aria-label="Set DeepSeek as default" style="background:none;border:none;cursor:pointer;font-size:18px;color:var(--fg-dim);padding:0 4px;line-height:1;">☆</button>
|
|
3518
|
+
<button class="pcard-star" data-default-btn="deepseek" title="Set as default Wall-E provider" aria-label="Set DeepSeek as default Wall-E provider" style="background:none;border:none;cursor:pointer;font-size:18px;color:var(--fg-dim);padding:0 4px;line-height:1;">☆</button>
|
|
3353
3519
|
<label class="pcard-toggle" style="position:relative;display:inline-block;width:36px;height:20px;flex-shrink:0;">
|
|
3354
3520
|
<input type="checkbox" data-toggle="deepseek" style="opacity:0;width:0;height:0;">
|
|
3355
3521
|
<span class="pcard-slider" style="position:absolute;cursor:pointer;inset:0;background:var(--border);border-radius:20px;transition:0.2s;"></span>
|
|
@@ -4290,32 +4456,102 @@ function showCwdConflict(msg) {
|
|
|
4290
4456
|
}
|
|
4291
4457
|
|
|
4292
4458
|
// --- Update Banner ---
|
|
4293
|
-
|
|
4459
|
+
function _safeStorageGet(storage, key) {
|
|
4460
|
+
try { return storage.getItem(key) || ''; } catch { return ''; }
|
|
4461
|
+
}
|
|
4462
|
+
function _safeStorageSet(storage, key, value) {
|
|
4463
|
+
try { storage.setItem(key, value); } catch {}
|
|
4464
|
+
}
|
|
4465
|
+
|
|
4466
|
+
let _updateDismissedVersion = _safeStorageGet(localStorage, 'update_dismissed_version');
|
|
4467
|
+
let _updateWizardSnoozedVersion = _safeStorageGet(sessionStorage, 'update_wizard_snoozed_version');
|
|
4468
|
+
let _updateCurrentVersion = '';
|
|
4469
|
+
let _updateLatestVersion = '';
|
|
4470
|
+
let _updateApplying = false;
|
|
4471
|
+
|
|
4472
|
+
function _setUpdateVersions(current, latest) {
|
|
4473
|
+
_updateCurrentVersion = current || '';
|
|
4474
|
+
_updateLatestVersion = latest || '';
|
|
4475
|
+
|
|
4476
|
+
const banner = document.getElementById('update-banner');
|
|
4477
|
+
if (banner) {
|
|
4478
|
+
banner.dataset.currentVersion = _updateCurrentVersion;
|
|
4479
|
+
banner.dataset.latestVersion = _updateLatestVersion;
|
|
4480
|
+
}
|
|
4481
|
+
|
|
4482
|
+
const cur = document.getElementById('update-current-version');
|
|
4483
|
+
const lat = document.getElementById('update-latest-version');
|
|
4484
|
+
if (cur) cur.textContent = _updateCurrentVersion ? `v${_updateCurrentVersion}` : 'v?';
|
|
4485
|
+
if (lat) lat.textContent = _updateLatestVersion ? `v${_updateLatestVersion}` : 'v?';
|
|
4486
|
+
}
|
|
4487
|
+
|
|
4488
|
+
function _hideUpdateWizard() {
|
|
4489
|
+
const wizard = document.getElementById('update-wizard');
|
|
4490
|
+
if (wizard) wizard.classList.add('hidden');
|
|
4491
|
+
}
|
|
4492
|
+
|
|
4493
|
+
function _hideUpdatePrompts() {
|
|
4494
|
+
const banner = document.getElementById('update-banner');
|
|
4495
|
+
if (banner) banner.style.display = 'none';
|
|
4496
|
+
_hideUpdateWizard();
|
|
4497
|
+
}
|
|
4498
|
+
|
|
4499
|
+
function _setUpdateApplying(applying) {
|
|
4500
|
+
_updateApplying = applying;
|
|
4501
|
+
const bannerBtn = document.getElementById('update-apply-btn');
|
|
4502
|
+
const wizardBtn = document.getElementById('update-wizard-apply-btn');
|
|
4503
|
+
if (bannerBtn) {
|
|
4504
|
+
bannerBtn.textContent = applying ? 'Updating...' : 'Update Now';
|
|
4505
|
+
bannerBtn.disabled = applying;
|
|
4506
|
+
}
|
|
4507
|
+
if (wizardBtn) {
|
|
4508
|
+
wizardBtn.textContent = applying ? 'Updating...' : 'Upgrade CTM';
|
|
4509
|
+
wizardBtn.disabled = applying;
|
|
4510
|
+
}
|
|
4511
|
+
}
|
|
4294
4512
|
|
|
4295
4513
|
function showUpdateBanner(current, latest) {
|
|
4296
|
-
if (_updateDismissedVersion === latest) return;
|
|
4514
|
+
if (!latest || _updateDismissedVersion === latest) return;
|
|
4515
|
+
_setUpdateVersions(current, latest);
|
|
4297
4516
|
const banner = document.getElementById('update-banner');
|
|
4298
4517
|
const msg = document.getElementById('update-banner-msg');
|
|
4299
4518
|
if (!banner || !msg) return;
|
|
4300
4519
|
msg.textContent = `Update available: v${current} \u2192 v${latest}`;
|
|
4301
4520
|
banner.style.display = 'flex';
|
|
4521
|
+
showUpdateWizard(current, latest);
|
|
4522
|
+
}
|
|
4523
|
+
|
|
4524
|
+
function showUpdateWizard(current, latest) {
|
|
4525
|
+
if (!latest || _updateDismissedVersion === latest || _updateWizardSnoozedVersion === latest || _updateApplying) return;
|
|
4526
|
+
_setUpdateVersions(current, latest);
|
|
4527
|
+
const wizard = document.getElementById('update-wizard');
|
|
4528
|
+
if (!wizard) return;
|
|
4529
|
+
wizard.classList.remove('hidden');
|
|
4530
|
+
setTimeout(() => {
|
|
4531
|
+
const btn = document.getElementById('update-wizard-apply-btn');
|
|
4532
|
+
if (btn && !btn.disabled) btn.focus();
|
|
4533
|
+
}, 0);
|
|
4534
|
+
}
|
|
4535
|
+
|
|
4536
|
+
function snoozeUpdateWizard() {
|
|
4537
|
+
if (_updateLatestVersion) {
|
|
4538
|
+
_updateWizardSnoozedVersion = _updateLatestVersion;
|
|
4539
|
+
_safeStorageSet(sessionStorage, 'update_wizard_snoozed_version', _updateLatestVersion);
|
|
4540
|
+
}
|
|
4541
|
+
_hideUpdateWizard();
|
|
4302
4542
|
}
|
|
4303
4543
|
|
|
4304
4544
|
function dismissUpdate() {
|
|
4305
|
-
|
|
4306
|
-
if (
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
const m = msg.match(/v([\d.]+)$/);
|
|
4310
|
-
if (m) {
|
|
4311
|
-
_updateDismissedVersion = m[1];
|
|
4312
|
-
localStorage.setItem('update_dismissed_version', m[1]);
|
|
4545
|
+
_hideUpdatePrompts();
|
|
4546
|
+
if (_updateLatestVersion) {
|
|
4547
|
+
_updateDismissedVersion = _updateLatestVersion;
|
|
4548
|
+
_safeStorageSet(localStorage, 'update_dismissed_version', _updateLatestVersion);
|
|
4313
4549
|
}
|
|
4314
4550
|
}
|
|
4315
4551
|
|
|
4316
4552
|
async function applyUpdate() {
|
|
4317
|
-
|
|
4318
|
-
|
|
4553
|
+
if (_updateApplying) return;
|
|
4554
|
+
_setUpdateApplying(true);
|
|
4319
4555
|
try {
|
|
4320
4556
|
const resp = await fetch('/api/updates/apply', { method: 'POST' });
|
|
4321
4557
|
const data = await resp.json();
|
|
@@ -4324,11 +4560,12 @@ async function applyUpdate() {
|
|
|
4324
4560
|
dismissUpdate();
|
|
4325
4561
|
} else {
|
|
4326
4562
|
toast('Already up to date.', { type: 'success' });
|
|
4327
|
-
|
|
4563
|
+
_hideUpdatePrompts();
|
|
4564
|
+
_setUpdateApplying(false);
|
|
4328
4565
|
}
|
|
4329
4566
|
} catch (e) {
|
|
4330
4567
|
toast('Update failed: ' + e.message, { type: 'error' });
|
|
4331
|
-
|
|
4568
|
+
_setUpdateApplying(false);
|
|
4332
4569
|
}
|
|
4333
4570
|
}
|
|
4334
4571
|
|
|
@@ -4356,6 +4593,7 @@ const state = window._ctmState = {
|
|
|
4356
4593
|
token: getCookie('ctm_token'),
|
|
4357
4594
|
sessions: new Map(), // id -> { term, fitAddon, container }
|
|
4358
4595
|
activeTab: null, // session id or 'rules'
|
|
4596
|
+
lastActiveWorkSessionId: null, // last non-Wall-E session used as repo context
|
|
4359
4597
|
tabOrder: [], // session ids in tab order
|
|
4360
4598
|
reviewingSessionId: null, // currently reviewed session
|
|
4361
4599
|
sidebarCollapsed: false,
|
|
@@ -4491,7 +4729,7 @@ function connect() {
|
|
|
4491
4729
|
(state._sessionsListDone || Promise.resolve()).then(() => onServerReady());
|
|
4492
4730
|
break;
|
|
4493
4731
|
case 'walle-error':
|
|
4494
|
-
if (msg.error && (msg.error.includes('API key') || msg.error.includes('ANTHROPIC_API_KEY')))
|
|
4732
|
+
if (!msg.providerError && msg.error && (msg.error.includes('API key') || msg.error.includes('ANTHROPIC_API_KEY')))
|
|
4495
4733
|
msg.error = 'Wall-E needs an API key configured. Go to Settings to add one.';
|
|
4496
4734
|
WalleSession.handleError(msg); break;
|
|
4497
4735
|
}
|
|
@@ -4924,6 +5162,7 @@ function _clientDetectAgentType(cmd) {
|
|
|
4924
5162
|
|
|
4925
5163
|
const CLIENT_AGENT_CAPABILITIES = {
|
|
4926
5164
|
claude: { structuredTranscript: true, promptNavigation: 'transcript', review: true, resume: true },
|
|
5165
|
+
'claude-desktop': { structuredTranscript: true, promptNavigation: 'transcript', review: true, resume: false },
|
|
4927
5166
|
codex: { structuredTranscript: true, promptNavigation: 'transcript', review: true, resume: true },
|
|
4928
5167
|
gemini: { structuredTranscript: false, promptNavigation: 'terminal', review: false, resume: true },
|
|
4929
5168
|
walle: { structuredTranscript: true, promptNavigation: 'none', review: true, resume: false },
|
|
@@ -4935,6 +5174,7 @@ function _clientNormalizeAgentType(value) {
|
|
|
4935
5174
|
const v = String(value).toLowerCase().replace(/_/g, '-');
|
|
4936
5175
|
if (CLIENT_AGENT_CAPABILITIES[v]) return v;
|
|
4937
5176
|
if (v === 'claude-code') return 'claude';
|
|
5177
|
+
if (v === 'claude-desktop-session' || v === 'desktop') return 'claude-desktop';
|
|
4938
5178
|
if (v === 'gemini-cli') return 'gemini';
|
|
4939
5179
|
return _clientDetectAgentType(v);
|
|
4940
5180
|
}
|
|
@@ -4961,7 +5201,9 @@ function _stripAnsiForActivity(data) {
|
|
|
4961
5201
|
|
|
4962
5202
|
const _CLAUDE_STATUS_FRAGMENT_RE = /^(?:[✻✳✢✶◐◓◑◒⏺●○■□✔✓]+|Run|Runn|Runni|Runnin|Running\.{0,3}|unning|Thinking\.{0,3}|\(?ctrl\+o\s*to\s*expand\)?|>?\s*esc\s+to\s+interrupt|[⏵>]+\s*(?:accept edits|auto mode)\s+on\s+\(shift\+tab\s+to\s+cycle\)(?:\s*·\s*esc\s+to\s+interrupt)?(?:\s*·\s*ctrl\+t\s+to\s+(?:show|hide)\s+task)?|ctrl\+t\s+to\s+show\s+tasks?|\?\s+for\s+shortcuts|Native installation exists but .*? is not in your PATH\. Run:|echo\s+['"]export\s+PATH=.*)$/i;
|
|
4963
5203
|
const _CODEX_STATUS_FRAGMENT_RE = /^[\s\d•◦·∙●○WwOoRrKkIiNnGg]+$/u;
|
|
5204
|
+
const _CODEX_BUSY_STATUS_LINE_RE = /^(?:working(?:\s*\([^)]*\))?(?:\s*[•◦·∙●○]\s*esc\s+to\s+interrupt)?|(?:working\s*)?esc\s+to\s+interrupt)$/iu;
|
|
4964
5205
|
const _CODEX_BUSY_WORD = 'working';
|
|
5206
|
+
const _CODEX_BUSY_HINT_RE = /esc\s+to\s+interrupt/i;
|
|
4965
5207
|
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;
|
|
4966
5208
|
|
|
4967
5209
|
function _isClaudeRedraw(data) {
|
|
@@ -4982,6 +5224,7 @@ function _isCodexRedraw(data) {
|
|
|
4982
5224
|
}
|
|
4983
5225
|
|
|
4984
5226
|
function _hasCodexBusyStatusFragment(text) {
|
|
5227
|
+
if (_CODEX_BUSY_HINT_RE.test(String(text || ''))) return true;
|
|
4985
5228
|
const letters = String(text || '').toLowerCase().replace(/[^a-z]/g, '');
|
|
4986
5229
|
if (letters.length < 3) return false;
|
|
4987
5230
|
if (letters.includes(_CODEX_BUSY_WORD)) return true;
|
|
@@ -4996,8 +5239,8 @@ function _isClientCodexStatusOnlyOutput(session, data) {
|
|
|
4996
5239
|
if (_clientAgentTypeForSession(session) !== 'codex') return false;
|
|
4997
5240
|
const stripped = _stripAnsiForActivity(data).trim();
|
|
4998
5241
|
return (
|
|
4999
|
-
stripped.length <=
|
|
5000
|
-
_CODEX_STATUS_FRAGMENT_RE.test(stripped) &&
|
|
5242
|
+
stripped.length <= 160 &&
|
|
5243
|
+
(_CODEX_STATUS_FRAGMENT_RE.test(stripped) || _CODEX_BUSY_STATUS_LINE_RE.test(stripped)) &&
|
|
5001
5244
|
_isCodexRedraw(data)
|
|
5002
5245
|
);
|
|
5003
5246
|
}
|
|
@@ -5022,8 +5265,8 @@ function _isClientActiveOutput(session, data) {
|
|
|
5022
5265
|
return false;
|
|
5023
5266
|
}
|
|
5024
5267
|
if (agentType === 'codex' &&
|
|
5025
|
-
stripped.length <=
|
|
5026
|
-
_CODEX_STATUS_FRAGMENT_RE.test(stripped) &&
|
|
5268
|
+
stripped.length <= 160 &&
|
|
5269
|
+
(_CODEX_STATUS_FRAGMENT_RE.test(stripped) || _CODEX_BUSY_STATUS_LINE_RE.test(stripped)) &&
|
|
5027
5270
|
_isCodexRedraw(data)) {
|
|
5028
5271
|
return _hasCodexBusyStatusFragment(stripped);
|
|
5029
5272
|
}
|
|
@@ -5393,7 +5636,7 @@ function createTerminal(id, opts) {
|
|
|
5393
5636
|
const size = s.term.options.fontSize;
|
|
5394
5637
|
s.term.options.fontSize = size + 1;
|
|
5395
5638
|
s.term.options.fontSize = size;
|
|
5396
|
-
|
|
5639
|
+
_fitTerminalPreservingViewport(s, id);
|
|
5397
5640
|
}
|
|
5398
5641
|
});
|
|
5399
5642
|
});
|
|
@@ -5528,13 +5771,13 @@ function createTerminal(id, opts) {
|
|
|
5528
5771
|
s.writer.queue = '';
|
|
5529
5772
|
s.writer.scheduled = false;
|
|
5530
5773
|
if (batch) chunkedWrite(s, batch, () => {
|
|
5531
|
-
if (s.writer.followMode && !s.writer._userScrollLocked && s.term) s
|
|
5774
|
+
if (s.writer.followMode && !s.writer._userScrollLocked && s.term) _ensureScrolledToBottom(s);
|
|
5532
5775
|
});
|
|
5533
5776
|
});
|
|
5534
5777
|
}
|
|
5535
5778
|
// Skip scrollToBottom if the RAF batcher is already scheduled —
|
|
5536
5779
|
// it will handle scrolling, and doing it here too competes for main thread.
|
|
5537
|
-
if (!s.writer.scheduled) s
|
|
5780
|
+
if (!s.writer.scheduled) _ensureScrolledToBottom(s);
|
|
5538
5781
|
}
|
|
5539
5782
|
});
|
|
5540
5783
|
|
|
@@ -5551,11 +5794,11 @@ function createTerminal(id, opts) {
|
|
|
5551
5794
|
requestAnimationFrame(() => {
|
|
5552
5795
|
const s = state.sessions.get(id);
|
|
5553
5796
|
if (s && s.term) {
|
|
5554
|
-
s.fitAddon.fit();
|
|
5555
5797
|
// Returning from alt screen — scroll to bottom and re-enable follow
|
|
5556
5798
|
s.writer.followMode = true;
|
|
5557
5799
|
s.writer._userScrollLocked = false;
|
|
5558
|
-
s
|
|
5800
|
+
_fitTerminalPreservingViewport(s, id);
|
|
5801
|
+
_ensureScrolledToBottom(s);
|
|
5559
5802
|
// Re-focus after fit — fitAddon.fit() can cause textarea focus loss
|
|
5560
5803
|
// during the alt→normal buffer transition, leaving input unresponsive.
|
|
5561
5804
|
if (state.activeTab === id) focusTerminalIfSafe(id);
|
|
@@ -5591,8 +5834,9 @@ function createTerminal(id, opts) {
|
|
|
5591
5834
|
// Scroll: handle locally instead of forwarding as mouse escape sequences
|
|
5592
5835
|
screenEl.addEventListener('wheel', (e) => {
|
|
5593
5836
|
const buf = term.buffer.active;
|
|
5837
|
+
const current = state.sessions.get(id) || { _id: id, term, writer, container };
|
|
5594
5838
|
const hasScrollback = buf.baseY > 0;
|
|
5595
|
-
const atBottom =
|
|
5839
|
+
const atBottom = _isAtTerminalFollowBottom(current);
|
|
5596
5840
|
if (e.deltaY < 0 && hasScrollback) {
|
|
5597
5841
|
// Scrolling UP — only lock follow mode if there's scrollback to scroll through.
|
|
5598
5842
|
// Without this guard, scrolling up on a terminal with no scrollback (e.g., during
|
|
@@ -5609,7 +5853,7 @@ function createTerminal(id, opts) {
|
|
|
5609
5853
|
term.scrollLines(Math.max(1, Math.round(Math.abs(e.deltaY) / 20)));
|
|
5610
5854
|
}
|
|
5611
5855
|
// Check if user scrolled back to bottom — unlock follow mode
|
|
5612
|
-
if (
|
|
5856
|
+
if (_isAtTerminalFollowBottom(current) && writer._userScrollLocked) {
|
|
5613
5857
|
writer.followMode = true;
|
|
5614
5858
|
writer._userScrollLocked = false;
|
|
5615
5859
|
}
|
|
@@ -5623,9 +5867,10 @@ function createTerminal(id, opts) {
|
|
|
5623
5867
|
// both user and programmatic scrolls (fit reflow, scrollToLine, etc.).
|
|
5624
5868
|
// Unlock only happens via the wheel handler (real user scroll to bottom).
|
|
5625
5869
|
term.onScroll(() => {
|
|
5870
|
+
if (writer._programmaticScrollDepth) return;
|
|
5626
5871
|
if (writer._userScrollLocked) return; // locked — only wheel can unlock
|
|
5627
|
-
const
|
|
5628
|
-
writer.followMode =
|
|
5872
|
+
const current = state.sessions.get(id) || { _id: id, term, writer, container };
|
|
5873
|
+
writer.followMode = _isAtTerminalFollowBottom(current);
|
|
5629
5874
|
});
|
|
5630
5875
|
|
|
5631
5876
|
state.sessions.set(id, { _id: id, term, fitAddon, searchAddon, container, needsFontRefresh: true, writer, promptLines: [], promptNavIdx: -1, _webglAddon, _loadWebgl });
|
|
@@ -6047,6 +6292,14 @@ function activateTab(id) {
|
|
|
6047
6292
|
|
|
6048
6293
|
const specialPanels = ['rules', 'insights', 'permissions', 'prompts', 'codereview', 'walle', 'models', 'backups', 'worktrees', 'setup'];
|
|
6049
6294
|
const isPanel = specialPanels.includes(id);
|
|
6295
|
+
if (state.activeTab && state.activeTab !== id && state.sessions.has(state.activeTab)) {
|
|
6296
|
+
const prev = state.sessions.get(state.activeTab);
|
|
6297
|
+
if (prev && prev.meta?.type !== 'walle') state.lastActiveWorkSessionId = state.activeTab;
|
|
6298
|
+
}
|
|
6299
|
+
if (!isPanel && state.sessions.has(id)) {
|
|
6300
|
+
const next = state.sessions.get(id);
|
|
6301
|
+
if (next && next.meta?.type !== 'walle') state.lastActiveWorkSessionId = id;
|
|
6302
|
+
}
|
|
6050
6303
|
|
|
6051
6304
|
// Phase 2B: dispose the PREVIOUS session's xterm.js terminal to free memory.
|
|
6052
6305
|
// Only one xterm.js instance should exist at a time (tmux-style). The headless
|
|
@@ -6244,6 +6497,7 @@ function activateTab(id) {
|
|
|
6244
6497
|
const savedQueue = s.writer.queue; // preserve output buffered while stub
|
|
6245
6498
|
// Preserve status state — createTerminal replaces the session object entirely
|
|
6246
6499
|
const savedWaiting = s._waitingForInput;
|
|
6500
|
+
const savedWaitingAt = s._waitingForInputAt;
|
|
6247
6501
|
const savedLastOut = s._lastOutputAt;
|
|
6248
6502
|
const savedLastIn = s._lastInputAt;
|
|
6249
6503
|
const savedAuthoritativeSource = s._authoritativeSource;
|
|
@@ -6262,6 +6516,7 @@ function activateTab(id) {
|
|
|
6262
6516
|
s2.meta = savedMeta;
|
|
6263
6517
|
s2.writer.queue = savedQueue; // restore buffered output for flush
|
|
6264
6518
|
s2._waitingForInput = savedWaiting;
|
|
6519
|
+
s2._waitingForInputAt = savedWaitingAt;
|
|
6265
6520
|
s2._lastOutputAt = savedLastOut;
|
|
6266
6521
|
s2._lastInputAt = savedLastIn;
|
|
6267
6522
|
s2._authoritativeSource = savedAuthoritativeSource;
|
|
@@ -7583,6 +7838,37 @@ function _buildSparklineSVG(data) {
|
|
|
7583
7838
|
return '<svg width="' + w + '" height="' + ht + '" viewBox="0 0 ' + w + ' ' + ht + '" style="vertical-align:middle;"><polyline points="' + points + '" fill="none" stroke="#7aa2f7" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
7584
7839
|
}
|
|
7585
7840
|
|
|
7841
|
+
function _benchScoreValue(b) {
|
|
7842
|
+
var v = b && b.trusted_avg_score != null ? b.trusted_avg_score : b && b.avg_score;
|
|
7843
|
+
v = Number(v);
|
|
7844
|
+
return isFinite(v) ? v : null;
|
|
7845
|
+
}
|
|
7846
|
+
function _benchTrustStatus(b) {
|
|
7847
|
+
if (b && b.trust_status) return String(b.trust_status);
|
|
7848
|
+
var trusted = Number((b && b.trusted_evals) || 0);
|
|
7849
|
+
var minTrusted = Number((b && b.min_trusted_evals) || 10);
|
|
7850
|
+
if (trusted >= minTrusted) return 'trusted';
|
|
7851
|
+
return trusted > 0 ? 'provisional' : 'legacy';
|
|
7852
|
+
}
|
|
7853
|
+
function _benchTrustColor(status) {
|
|
7854
|
+
if (status === 'trusted') return '#9ece6a';
|
|
7855
|
+
if (status === 'provisional') return '#e0af68';
|
|
7856
|
+
return '#f7768e';
|
|
7857
|
+
}
|
|
7858
|
+
function _benchCiLabel(b) {
|
|
7859
|
+
var low = b && b.trusted_score_confidence_low != null ? b.trusted_score_confidence_low : b && b.score_confidence_low;
|
|
7860
|
+
var high = b && b.trusted_score_confidence_high != null ? b.trusted_score_confidence_high : b && b.score_confidence_high;
|
|
7861
|
+
low = Number(low);
|
|
7862
|
+
high = Number(high);
|
|
7863
|
+
if (!isFinite(low) || !isFinite(high)) return '-';
|
|
7864
|
+
return low.toFixed(2) + '-' + high.toFixed(2);
|
|
7865
|
+
}
|
|
7866
|
+
function _benchMetaValue(v) {
|
|
7867
|
+
if (Array.isArray(v)) return v.length ? v.join(', ') : '-';
|
|
7868
|
+
if (v == null || v === '') return '-';
|
|
7869
|
+
return String(v);
|
|
7870
|
+
}
|
|
7871
|
+
|
|
7586
7872
|
function renderScorecardDashboard(scorecard, models, benchmarks, benchmarkCoverage) {
|
|
7587
7873
|
var el = document.getElementById('models-scorecard-dashboard');
|
|
7588
7874
|
var filtered = scorecard.filter(function(m) { return m.stats && m.stats.total_evals > 0; });
|
|
@@ -7630,12 +7916,25 @@ function renderScorecardDashboard(scorecard, models, benchmarks, benchmarkCovera
|
|
|
7630
7916
|
if (coverageBits.length) {
|
|
7631
7917
|
h += '<div style="margin:-2px 0 8px 0;color:var(--fg-dim,#565f89);font-size:11px;">' + escHtml(coverageBits.join(' | ')) + '</div>';
|
|
7632
7918
|
}
|
|
7919
|
+
var trustedGroups = 0, provisionalGroups = 0, legacyGroups = 0;
|
|
7920
|
+
benchmarks.forEach(function(b) {
|
|
7921
|
+
var status = _benchTrustStatus(b);
|
|
7922
|
+
if (status === 'trusted') trustedGroups++;
|
|
7923
|
+
else if (status === 'provisional') provisionalGroups++;
|
|
7924
|
+
else legacyGroups++;
|
|
7925
|
+
});
|
|
7926
|
+
h += '<div style="margin:-2px 0 8px 0;color:var(--fg-dim,#565f89);font-size:11px;">Trust coverage: ' +
|
|
7927
|
+
'<span style="color:#9ece6a;">' + trustedGroups + ' trusted</span>, ' +
|
|
7928
|
+
'<span style="color:#e0af68;">' + provisionalGroups + ' provisional</span>, ' +
|
|
7929
|
+
'<span style="color:#f7768e;">' + legacyGroups + ' legacy</span> model groups</div>';
|
|
7633
7930
|
|
|
7634
7931
|
// Comparison table
|
|
7635
7932
|
h += '<div style="overflow-x:auto;margin-bottom:14px;"><table style="width:100%;border-collapse:collapse;font-size:12px;">' +
|
|
7636
7933
|
'<thead><tr style="background:var(--bg-card,#24283b);color:var(--fg-dim,#565f89);text-align:left;">' +
|
|
7637
7934
|
'<th style="padding:8px 14px;font-weight:500;">Model</th>' +
|
|
7638
7935
|
'<th style="padding:8px;font-weight:500;">Composite</th>' +
|
|
7936
|
+
'<th style="padding:8px;font-weight:500;">Trust</th>' +
|
|
7937
|
+
'<th style="padding:8px;font-weight:500;">CI</th>' +
|
|
7639
7938
|
'<th style="padding:8px;font-weight:500;">Code Gen</th>' +
|
|
7640
7939
|
'<th style="padding:8px;font-weight:500;">Tool Use</th>' +
|
|
7641
7940
|
'<th style="padding:8px;font-weight:500;">Planning</th>' +
|
|
@@ -7646,10 +7945,17 @@ function renderScorecardDashboard(scorecard, models, benchmarks, benchmarkCovera
|
|
|
7646
7945
|
'</tr></thead><tbody>';
|
|
7647
7946
|
|
|
7648
7947
|
benchmarks.forEach(function(b, idx) {
|
|
7649
|
-
var q = b
|
|
7650
|
-
var qColor = _qualityZoneColor(q);
|
|
7948
|
+
var q = _benchScoreValue(b);
|
|
7949
|
+
var qColor = _qualityZoneColor(q || 0);
|
|
7651
7950
|
var provBadge = '<span style="font-size:9px;padding:1px 5px;border-radius:3px;background:' + (pColors[b.provider] || '#565f89') + '22;color:' + (pColors[b.provider] || '#565f89') + ';margin-left:6px;">' + escHtml(b.provider) + '</span>';
|
|
7652
7951
|
var rankBadge = idx === 0 ? '<span style="font-size:9px;padding:1px 5px;border-radius:3px;background:#9ece6a22;color:#9ece6a;margin-left:4px;">#1</span>' : '';
|
|
7952
|
+
var trustStatus = _benchTrustStatus(b);
|
|
7953
|
+
var trustColor = _benchTrustColor(trustStatus);
|
|
7954
|
+
var trustedEvals = Number(b.trusted_evals || 0);
|
|
7955
|
+
var minTrusted = Number(b.min_trusted_evals || 10);
|
|
7956
|
+
var trustBadge = '<span style="font-size:9px;padding:1px 5px;border-radius:3px;background:' + trustColor + '22;color:' + trustColor + ';font-weight:600;">' + escHtml(trustStatus.toUpperCase()) + '</span>';
|
|
7957
|
+
var trustDetail = trustedEvals + '/' + minTrusted + ' trusted';
|
|
7958
|
+
var ciLabel = _benchCiLabel(b);
|
|
7653
7959
|
|
|
7654
7960
|
var successfulEvals = b.successful_evals != null ? b.successful_evals : b.total_evals;
|
|
7655
7961
|
var errorCount = b.errors || 0;
|
|
@@ -7658,7 +7964,9 @@ function renderScorecardDashboard(scorecard, models, benchmarks, benchmarkCovera
|
|
|
7658
7964
|
|
|
7659
7965
|
h += '<tr data-bench-idx="' + idx + '" style="border-top:1px solid var(--border,#292e42);cursor:pointer;">' +
|
|
7660
7966
|
'<td style="padding:6px 14px;color:var(--fg,#c0caf5);font-weight:500;">' + escHtml(b.model) + provBadge + rankBadge + '</td>' +
|
|
7661
|
-
'<td style="padding:6px 8px;color:' + qColor + ';font-weight:600;">' + (successfulEvals ? q.toFixed(3) : '-') + '</td>'
|
|
7967
|
+
'<td style="padding:6px 8px;color:' + qColor + ';font-weight:600;">' + (successfulEvals && q != null ? q.toFixed(3) : '-') + '</td>' +
|
|
7968
|
+
'<td style="padding:6px 8px;">' + trustBadge + '<div style="font-size:9px;color:var(--fg-dim,#565f89);margin-top:2px;">' + escHtml(trustDetail) + '</div></td>' +
|
|
7969
|
+
'<td style="padding:6px 8px;color:var(--fg-dim,#565f89);font-variant-numeric:tabular-nums;">' + escHtml(ciLabel) + '</td>';
|
|
7662
7970
|
|
|
7663
7971
|
// Category scores with inline bars
|
|
7664
7972
|
['codeGen', 'toolUse', 'planning', 'efficiency'].forEach(function(cat) {
|
|
@@ -7680,7 +7988,7 @@ function renderScorecardDashboard(scorecard, models, benchmarks, benchmarkCovera
|
|
|
7680
7988
|
'</tr>';
|
|
7681
7989
|
|
|
7682
7990
|
// Expandable detail row with radar + all 11 dimensions
|
|
7683
|
-
h += '<tr data-bench-detail="' + idx + '" style="display:none;"><td colspan="
|
|
7991
|
+
h += '<tr data-bench-detail="' + idx + '" style="display:none;"><td colspan="11" style="padding:0;background:var(--bg,#1a1b26);"></td></tr>';
|
|
7684
7992
|
});
|
|
7685
7993
|
h += '</tbody></table></div>';
|
|
7686
7994
|
|
|
@@ -7759,7 +8067,41 @@ function renderScorecardDashboard(scorecard, models, benchmarks, benchmarkCovera
|
|
|
7759
8067
|
detailRow.style.display = '';
|
|
7760
8068
|
var td = detailRow.querySelector('td');
|
|
7761
8069
|
var b = benchmarks[idx];
|
|
7762
|
-
if (!b
|
|
8070
|
+
if (!b) { td.textContent = 'No benchmark data.'; return; }
|
|
8071
|
+
|
|
8072
|
+
var detailScore = _benchScoreValue(b);
|
|
8073
|
+
var detailTrust = _benchTrustStatus(b);
|
|
8074
|
+
var detailTrustColor = _benchTrustColor(detailTrust);
|
|
8075
|
+
var metaItems = [
|
|
8076
|
+
['Score Source', b.trusted_avg_score != null ? 'trusted mean' : 'successful mean'],
|
|
8077
|
+
['Composite', detailScore != null ? detailScore.toFixed(3) : '-'],
|
|
8078
|
+
['Trust', detailTrust + ' (' + Number(b.trusted_evals || 0) + '/' + Number(b.min_trusted_evals || 10) + ')'],
|
|
8079
|
+
['95% CI', _benchCiLabel(b)],
|
|
8080
|
+
['Samples', Number(b.sample_count || 0) + ' unique / ' + Number(b.total_evals || 0) + ' rows'],
|
|
8081
|
+
['Datasets', _benchMetaValue(b.dataset_versions)],
|
|
8082
|
+
['Scorers', _benchMetaValue(b.scorer_versions)],
|
|
8083
|
+
['Methods', _benchMetaValue(b.scoring_methods)],
|
|
8084
|
+
['Evaluators', _benchMetaValue(b.evaluator_versions)],
|
|
8085
|
+
['Artifacts', String(b.artifact_count || 0)],
|
|
8086
|
+
['Last Eval', _benchMetaValue(b.last_eval_at)]
|
|
8087
|
+
];
|
|
8088
|
+
var metaHtml = '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:8px;margin-bottom:14px;">';
|
|
8089
|
+
metaItems.forEach(function(item) {
|
|
8090
|
+
var color = item[0] === 'Trust' ? detailTrustColor : 'var(--fg,#c0caf5)';
|
|
8091
|
+
metaHtml += '<div style="min-width:0;border-top:1px solid var(--border,#292e42);padding-top:6px;">' +
|
|
8092
|
+
'<div style="font-size:9px;color:var(--fg-dim,#565f89);text-transform:uppercase;">' + escHtml(item[0]) + '</div>' +
|
|
8093
|
+
'<div style="font-size:11px;color:' + color + ';overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="' + escHtml(item[1]) + '">' + escHtml(item[1]) + '</div>' +
|
|
8094
|
+
'</div>';
|
|
8095
|
+
});
|
|
8096
|
+
metaHtml += '</div>';
|
|
8097
|
+
|
|
8098
|
+
if (!b.dimensions) {
|
|
8099
|
+
var noDimsHtml = '<div style="padding:14px;">' + metaHtml +
|
|
8100
|
+
'<div style="font-size:11px;color:var(--fg-dim,#565f89);">No dimension data for this benchmark group.</div>' +
|
|
8101
|
+
'</div>';
|
|
8102
|
+
td.innerHTML = DOMPurify.sanitize(noDimsHtml, { ADD_TAGS: svgTags, ADD_ATTR: svgAttrs });
|
|
8103
|
+
return;
|
|
8104
|
+
}
|
|
7763
8105
|
|
|
7764
8106
|
var dimNames = ['correctness', 'codeQuality', 'diffAccuracy', 'partialProgress', 'toolEfficiency', 'contextManagement', 'errorHandling', 'planQuality', 'turnEconomy', 'iterativeRefinement', 'costEfficiency'];
|
|
7765
8107
|
var dimLabels = ['Correct', 'Code Qual', 'Diff Acc', 'Partial', 'Tool Eff', 'Ctx Mgmt', 'Err Hand', 'Plan Qual', 'Turn Econ', 'Iter Refine', 'Cost Eff'];
|
|
@@ -7821,9 +8163,12 @@ function renderScorecardDashboard(scorecard, models, benchmarks, benchmarkCovera
|
|
|
7821
8163
|
});
|
|
7822
8164
|
barsHtml += '</div>';
|
|
7823
8165
|
|
|
7824
|
-
var detailHtml = '<div style="padding:14px;
|
|
8166
|
+
var detailHtml = '<div style="padding:14px;">' +
|
|
8167
|
+
metaHtml +
|
|
8168
|
+
'<div style="display:flex;gap:20px;align-items:flex-start;flex-wrap:wrap;">' +
|
|
7825
8169
|
'<div>' + radarSvg + '</div>' +
|
|
7826
8170
|
'<div style="flex:1;min-width:200px;">' + barsHtml + '</div>' +
|
|
8171
|
+
'</div>' +
|
|
7827
8172
|
'</div>';
|
|
7828
8173
|
|
|
7829
8174
|
td.innerHTML = DOMPurify.sanitize(detailHtml, { ADD_TAGS: svgTags, ADD_ATTR: svgAttrs });
|
|
@@ -8990,6 +9335,9 @@ function _restoreBackup(type, name) {
|
|
|
8990
9335
|
var _wtCache = { repoRoot: '', items: [], counts: {} };
|
|
8991
9336
|
var _wtModalState = { branch: '', name: '', mode: null, cwd: '' };
|
|
8992
9337
|
var _wtCurrentFilter = 'all';
|
|
9338
|
+
var _wtLoadSeq = 0;
|
|
9339
|
+
var _wtRefreshSeq = 0;
|
|
9340
|
+
var _wtLastActionNotice = null;
|
|
8993
9341
|
var _wtSanitize = function(s) {
|
|
8994
9342
|
return String(s || '').trim().replace(/[^a-zA-Z0-9_-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
8995
9343
|
};
|
|
@@ -9070,7 +9418,7 @@ function _wtRunRecommendedAction(wt) {
|
|
|
9070
9418
|
if (action.kind === 'prune') return submitPruneGhosts();
|
|
9071
9419
|
if (action.kind === 'recover_branch') return submitRecoverDetached(wt);
|
|
9072
9420
|
if (action.kind === 'sync_branch') return openSyncModal(wt);
|
|
9073
|
-
if (action.kind === 'finish_work') return
|
|
9421
|
+
if (action.kind === 'finish_work') return _wtOpenMergeOrExplain(wt);
|
|
9074
9422
|
if (action.kind === 'cleanup') return openDeleteModal(wt);
|
|
9075
9423
|
if (action.kind === 'update_main' || action.kind === 'push_main' || action.kind === 'reconcile_main') {
|
|
9076
9424
|
toast(action.reason || 'Review local main in a terminal before worktree operations.', { type: 'warning', title: 'Main needs attention' });
|
|
@@ -9116,14 +9464,56 @@ function _wtSyncAllEligible(wts) {
|
|
|
9116
9464
|
});
|
|
9117
9465
|
}
|
|
9118
9466
|
|
|
9119
|
-
|
|
9467
|
+
function _wtWorktreesListUrl(token) {
|
|
9468
|
+
var params = new URLSearchParams();
|
|
9469
|
+
params.set('token', token || '');
|
|
9470
|
+
params.set('_wt_refresh', String(Date.now()) + '-' + (++_wtRefreshSeq));
|
|
9471
|
+
return '/api/worktrees?' + params.toString();
|
|
9472
|
+
}
|
|
9473
|
+
|
|
9474
|
+
function _wtSkippedReasonSummary(skippedBranches) {
|
|
9475
|
+
var counts = {};
|
|
9476
|
+
var rows = Array.isArray(skippedBranches) ? skippedBranches : [];
|
|
9477
|
+
rows.forEach(function(row) {
|
|
9478
|
+
var reason = row && row.reason ? String(row.reason) : 'skipped';
|
|
9479
|
+
counts[reason] = (counts[reason] || 0) + 1;
|
|
9480
|
+
});
|
|
9481
|
+
return Object.keys(counts).map(function(reason) {
|
|
9482
|
+
return counts[reason] + ' ' + reason;
|
|
9483
|
+
}).join(', ');
|
|
9484
|
+
}
|
|
9485
|
+
|
|
9486
|
+
function _wtBuildSyncAllNotice(d) {
|
|
9487
|
+
var failed = d.failed || 0;
|
|
9488
|
+
var skipped = d.skipped || 0;
|
|
9489
|
+
var synced = d.synced || 0;
|
|
9490
|
+
var msg = 'Sync all finished: ' + synced + ' synced';
|
|
9491
|
+
if (failed) msg += ', ' + failed + ' failed';
|
|
9492
|
+
if (skipped) {
|
|
9493
|
+
var reasons = _wtSkippedReasonSummary(d.skippedBranches || []);
|
|
9494
|
+
msg += ', ' + skipped + ' skipped';
|
|
9495
|
+
if (reasons) msg += ' (' + reasons + ')';
|
|
9496
|
+
msg += '. Skipped worktrees can still show behind until active sessions are closed and dirty files are committed or stashed';
|
|
9497
|
+
}
|
|
9498
|
+
return {
|
|
9499
|
+
tone: failed ? 'warning' : (skipped ? 'warning' : 'success'),
|
|
9500
|
+
title: 'Worktrees refreshed',
|
|
9501
|
+
message: msg + '.',
|
|
9502
|
+
};
|
|
9503
|
+
}
|
|
9504
|
+
|
|
9505
|
+
async function loadWorktreesPanel(opts) {
|
|
9506
|
+
opts = opts || {};
|
|
9120
9507
|
var body = document.getElementById('worktrees-body');
|
|
9121
9508
|
if (!body) return;
|
|
9122
|
-
|
|
9509
|
+
var requestId = ++_wtLoadSeq;
|
|
9510
|
+
if (!opts.silent) body.textContent = 'Loading…';
|
|
9123
9511
|
try {
|
|
9124
9512
|
var token = (state && state.token) ? state.token : '';
|
|
9125
|
-
var r = await fetch(
|
|
9513
|
+
var r = await fetch(_wtWorktreesListUrl(token), { cache: 'no-store' });
|
|
9514
|
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
9126
9515
|
var d = await r.json();
|
|
9516
|
+
if (requestId !== _wtLoadSeq) return;
|
|
9127
9517
|
var wts = d.worktrees || [];
|
|
9128
9518
|
_wtCache = { repoRoot: d.cwd || '', items: wts, counts: d.counts || {}, mainRemote: d.mainRemote || null };
|
|
9129
9519
|
|
|
@@ -9136,18 +9526,36 @@ async function loadWorktreesPanel() {
|
|
|
9136
9526
|
syncAllBtn.textContent = '↓ Sync all' + (syncTargets.length > 0 ? ' (' + syncTargets.length + ')' : '');
|
|
9137
9527
|
syncAllBtn.disabled = false;
|
|
9138
9528
|
syncAllBtn.title = syncTargets.length > 0
|
|
9139
|
-
? 'Sync ' + syncTargets.length + ' clean branch(es) that are behind main'
|
|
9140
|
-
: 'No clean branches are behind main';
|
|
9529
|
+
? 'Sync ' + syncTargets.length + ' clean inactive branch(es) that are behind main'
|
|
9530
|
+
: 'No clean inactive branches are behind main';
|
|
9141
9531
|
}
|
|
9142
9532
|
|
|
9143
9533
|
_wtRenderFilterChips(d.counts || {}, wts);
|
|
9144
9534
|
_wtRenderBody(body, wts);
|
|
9145
9535
|
_wtUpdateNavBadge(d.counts || {});
|
|
9146
9536
|
} catch (e) {
|
|
9537
|
+
if (requestId !== _wtLoadSeq) return;
|
|
9147
9538
|
body.textContent = 'Failed to load worktrees: ' + e.message;
|
|
9148
9539
|
}
|
|
9149
9540
|
}
|
|
9150
9541
|
|
|
9542
|
+
function _wtRenderActionNotice(frag) {
|
|
9543
|
+
var notice = _wtLastActionNotice;
|
|
9544
|
+
if (!notice || !notice.message) return;
|
|
9545
|
+
var color = notice.tone === 'success' ? '#9ece6a' : notice.tone === 'warning' ? '#e0af68' : '#7aa2f7';
|
|
9546
|
+
var bg = notice.tone === 'success' ? 'rgba(158,206,106,0.08)' : notice.tone === 'warning' ? 'rgba(224,175,104,0.08)' : 'rgba(122,162,247,0.08)';
|
|
9547
|
+
var wrap = document.createElement('div');
|
|
9548
|
+
wrap.style.cssText = 'border:1px solid var(--border);border-left:3px solid ' + color + ';border-radius:8px;background:' + bg + ';padding:10px 12px;margin-bottom:12px;font-size:12px;color:var(--fg,#c0caf5);line-height:1.45;';
|
|
9549
|
+
var title = document.createElement('div');
|
|
9550
|
+
title.style.cssText = 'font-weight:600;margin-bottom:2px;color:' + color + ';';
|
|
9551
|
+
title.textContent = notice.title || 'Worktree action';
|
|
9552
|
+
var msg = document.createElement('div');
|
|
9553
|
+
msg.style.cssText = 'color:var(--fg-dim,#a9b1d6);';
|
|
9554
|
+
msg.textContent = notice.message;
|
|
9555
|
+
wrap.append(title, msg);
|
|
9556
|
+
frag.appendChild(wrap);
|
|
9557
|
+
}
|
|
9558
|
+
|
|
9151
9559
|
function _wtRenderBody(body, wts) {
|
|
9152
9560
|
if (wts.length === 0) {
|
|
9153
9561
|
_wtClearChildren(body);
|
|
@@ -9158,6 +9566,7 @@ function _wtRenderBody(body, wts) {
|
|
|
9158
9566
|
return;
|
|
9159
9567
|
}
|
|
9160
9568
|
var frag = document.createDocumentFragment();
|
|
9569
|
+
_wtRenderActionNotice(frag);
|
|
9161
9570
|
_wtRenderSummary(frag, wts, _wtCache.counts || {});
|
|
9162
9571
|
var visible = 0;
|
|
9163
9572
|
for (var wt of wts) {
|
|
@@ -9282,7 +9691,7 @@ function _wtRenderCard(frag, wt) {
|
|
|
9282
9691
|
if (wt.state === 'ahead') {
|
|
9283
9692
|
pill.style.cursor = 'pointer';
|
|
9284
9693
|
pill.title = 'Click to merge into main';
|
|
9285
|
-
pill.onclick = function() {
|
|
9694
|
+
pill.onclick = function() { _wtOpenMergeOrExplain(wt); };
|
|
9286
9695
|
} else if (wt.state === 'behind' || wt.state === 'diverged') {
|
|
9287
9696
|
pill.style.cursor = 'pointer';
|
|
9288
9697
|
pill.title = 'Click to sync from main';
|
|
@@ -9382,12 +9791,15 @@ function _wtRenderCard(frag, wt) {
|
|
|
9382
9791
|
mergeBtn.className = 'btn';
|
|
9383
9792
|
mergeBtn.style.cssText = 'font-size:11px;padding:4px 10px;background:rgba(74,222,128,0.12);color:#4ade80;border:1px solid rgba(74,222,128,0.3);';
|
|
9384
9793
|
mergeBtn.textContent = 'Merge';
|
|
9385
|
-
|
|
9386
|
-
if (
|
|
9387
|
-
|
|
9388
|
-
|
|
9389
|
-
else
|
|
9390
|
-
|
|
9794
|
+
var mergeBlockReason = _wtMergeBlockReason(wt);
|
|
9795
|
+
if (mergeBlockReason) {
|
|
9796
|
+
mergeBtn.setAttribute('aria-disabled', 'true');
|
|
9797
|
+
mergeBtn.title = mergeBlockReason;
|
|
9798
|
+
} else {
|
|
9799
|
+
mergeBtn.removeAttribute('aria-disabled');
|
|
9800
|
+
mergeBtn.title = 'Merge this branch into main';
|
|
9801
|
+
}
|
|
9802
|
+
mergeBtn.onclick = function() { _wtOpenMergeOrExplain(wt); };
|
|
9391
9803
|
actions.appendChild(mergeBtn);
|
|
9392
9804
|
}
|
|
9393
9805
|
|
|
@@ -9482,6 +9894,7 @@ async function submitSyncWorktree() {
|
|
|
9482
9894
|
}
|
|
9483
9895
|
if (!r.ok || d.error) throw new Error(d.error || ('HTTP ' + r.status));
|
|
9484
9896
|
closeModal('wt-sync-modal');
|
|
9897
|
+
_wtLastActionNotice = { tone: 'success', title: 'Worktrees refreshed', message: 'Synced ' + st.branch + ' from main.' };
|
|
9485
9898
|
toast('Synced ' + st.branch + ' from main', { type: 'success' });
|
|
9486
9899
|
await loadWorktreesPanel();
|
|
9487
9900
|
} catch (e) {
|
|
@@ -9494,10 +9907,10 @@ async function submitSyncWorktree() {
|
|
|
9494
9907
|
async function submitSyncAllWorktrees() {
|
|
9495
9908
|
var targets = _wtSyncAllEligible((_wtCache && _wtCache.items) || []);
|
|
9496
9909
|
if (targets.length === 0) {
|
|
9497
|
-
toast('No clean branches are behind main', { type: 'info' });
|
|
9910
|
+
toast('No clean inactive branches are behind main', { type: 'info' });
|
|
9498
9911
|
return;
|
|
9499
9912
|
}
|
|
9500
|
-
if (!confirm('Sync ' + targets.length + ' clean branch(es) from main? Dirty, detached, and ghost worktrees will be skipped.')) return;
|
|
9913
|
+
if (!confirm('Sync ' + targets.length + ' clean branch(es) from main? Dirty, active-session, detached, and ghost worktrees will be skipped.')) return;
|
|
9501
9914
|
var btn = document.getElementById('wt-sync-all-btn');
|
|
9502
9915
|
if (btn) { btn.disabled = true; btn.textContent = 'Syncing all…'; }
|
|
9503
9916
|
try {
|
|
@@ -9517,6 +9930,11 @@ async function submitSyncAllWorktrees() {
|
|
|
9517
9930
|
var msg = 'Synced ' + synced + ' branch(es)';
|
|
9518
9931
|
if (failed) msg += ', ' + failed + ' failed';
|
|
9519
9932
|
if (skipped) msg += ', ' + skipped + ' skipped';
|
|
9933
|
+
if (skipped && d.skippedBranches) {
|
|
9934
|
+
var reasons = _wtSkippedReasonSummary(d.skippedBranches);
|
|
9935
|
+
if (reasons) msg += ' (' + reasons + ')';
|
|
9936
|
+
}
|
|
9937
|
+
_wtLastActionNotice = _wtBuildSyncAllNotice(d);
|
|
9520
9938
|
toast(msg, { type: failed ? 'warning' : 'success', duration: failed ? 10000 : 5000 });
|
|
9521
9939
|
await loadWorktreesPanel();
|
|
9522
9940
|
} catch (e) {
|
|
@@ -9783,7 +10201,7 @@ async function wtFinishAction(action) {
|
|
|
9783
10201
|
}
|
|
9784
10202
|
|
|
9785
10203
|
closeModal('wt-finish-modal');
|
|
9786
|
-
if (action === 'merge') return
|
|
10204
|
+
if (action === 'merge') return _wtOpenMergeOrExplain(resolved);
|
|
9787
10205
|
if (action === 'pr') return openCreatePRModal(resolved);
|
|
9788
10206
|
if (action === 'delete') return openDeleteModal(resolved);
|
|
9789
10207
|
}
|
|
@@ -9855,7 +10273,31 @@ async function submitCreateWorktree() {
|
|
|
9855
10273
|
}
|
|
9856
10274
|
|
|
9857
10275
|
// ── Merge modal ─────────────────────────────────────────────────────
|
|
10276
|
+
function _wtMergeBlockReason(wt) {
|
|
10277
|
+
wt = wt || {};
|
|
10278
|
+
if (!wt.branch || wt.branch === 'HEAD') return 'Recover this worktree onto a branch before merging.';
|
|
10279
|
+
if (wt.sessionId) return 'Close the active session before merging: ' + (wt.sessionLabel || wt.branch) + '.';
|
|
10280
|
+
if ((wt.dirtyFiles || 0) > 0) return 'Commit or stash dirty files before merging.';
|
|
10281
|
+
if ((wt.behind || 0) > 0) return 'Sync from main before merging.';
|
|
10282
|
+
if ((wt.unmergedCommits || 0) === 0 && (wt.ahead || 0) === 0) return 'Nothing to merge — this branch is already integrated with main.';
|
|
10283
|
+
return '';
|
|
10284
|
+
}
|
|
10285
|
+
|
|
10286
|
+
function _wtOpenMergeOrExplain(wt) {
|
|
10287
|
+
var reason = _wtMergeBlockReason(wt);
|
|
10288
|
+
if (reason) {
|
|
10289
|
+
toast(reason, { type: 'warning', title: 'Merge unavailable', duration: 7000 });
|
|
10290
|
+
return;
|
|
10291
|
+
}
|
|
10292
|
+
openMergeModal(wt);
|
|
10293
|
+
}
|
|
10294
|
+
|
|
9858
10295
|
function openMergeModal(wt) {
|
|
10296
|
+
var blockReason = _wtMergeBlockReason(wt);
|
|
10297
|
+
if (blockReason) {
|
|
10298
|
+
toast(blockReason, { type: 'warning', title: 'Merge unavailable', duration: 7000 });
|
|
10299
|
+
return;
|
|
10300
|
+
}
|
|
9859
10301
|
_wtModalState = { branch: wt.branch, name: wt.branch, worktreeName: wt.worktreeName || '', mode: 'merge', cwd: _wtCache.repoRoot || '' };
|
|
9860
10302
|
var sum = document.getElementById('wt-merge-summary');
|
|
9861
10303
|
if (sum) sum.textContent = 'Merge "' + wt.branch + '" into main.';
|
|
@@ -10071,8 +10513,7 @@ function onCreated(msg) {
|
|
|
10071
10513
|
|
|
10072
10514
|
requestAnimationFrame(() => {
|
|
10073
10515
|
const s = state.sessions.get(id);
|
|
10074
|
-
s
|
|
10075
|
-
send({ type: 'resize', id, cols: term.cols, rows: term.rows });
|
|
10516
|
+
_fitTerminalPreservingViewport(s, id, { sendResize: true });
|
|
10076
10517
|
});
|
|
10077
10518
|
|
|
10078
10519
|
// Notify the prompt editor so it can send pending prompts
|
|
@@ -10211,7 +10652,7 @@ function chunkedWrite(s, data, onDone) {
|
|
|
10211
10652
|
s.writer.scheduled = false;
|
|
10212
10653
|
if (!next) return;
|
|
10213
10654
|
chunkedWrite(s, next, () => {
|
|
10214
|
-
if (f && !s.writer._userScrollLocked) s
|
|
10655
|
+
if (f && !s.writer._userScrollLocked) _ensureScrolledToBottom(s);
|
|
10215
10656
|
});
|
|
10216
10657
|
});
|
|
10217
10658
|
}
|
|
@@ -10245,7 +10686,7 @@ function onOutput(msg) {
|
|
|
10245
10686
|
&& (data.length <= 10 || _visibleEchoChars(data).length <= 64)) {
|
|
10246
10687
|
s.term.write(data);
|
|
10247
10688
|
s._lastOutputAt = Date.now(); // keystroke echoes are always real activity
|
|
10248
|
-
if (s.writer.followMode && !s.writer._userScrollLocked) s
|
|
10689
|
+
if (s.writer.followMode && !s.writer._userScrollLocked) _ensureScrolledToBottom(s);
|
|
10249
10690
|
return;
|
|
10250
10691
|
}
|
|
10251
10692
|
|
|
@@ -10289,7 +10730,7 @@ function onOutput(msg) {
|
|
|
10289
10730
|
s.writer.scheduled = false;
|
|
10290
10731
|
if (follow) {
|
|
10291
10732
|
chunkedWrite(s, batch, () => {
|
|
10292
|
-
if (follow && !s.writer._userScrollLocked) s
|
|
10733
|
+
if (follow && !s.writer._userScrollLocked) _ensureScrolledToBottom(s);
|
|
10293
10734
|
});
|
|
10294
10735
|
} else {
|
|
10295
10736
|
// Not following — write without moving viewport
|
|
@@ -10309,28 +10750,12 @@ function onOutput(msg) {
|
|
|
10309
10750
|
try {
|
|
10310
10751
|
const dims = s.fitAddon.proposeDimensions();
|
|
10311
10752
|
if (dims && (dims.cols !== s.term.cols || dims.rows !== s.term.rows)) {
|
|
10312
|
-
|
|
10313
|
-
const wasAtBottom = buf.viewportY >= buf.baseY;
|
|
10314
|
-
const oldCols = s.term.cols;
|
|
10315
|
-
const savedLine2 = buf.viewportY;
|
|
10316
|
-
const savedAnchor = wasAtBottom ? null : _getScrollAnchor(s.term);
|
|
10317
|
-
const savedFollow2 = s.writer.followMode;
|
|
10318
|
-
s.fitAddon.fit();
|
|
10319
|
-
s.writer.followMode = savedFollow2;
|
|
10320
|
-
send({ type: 'resize', id: sid, cols: s.term.cols, rows: s.term.rows });
|
|
10321
|
-
if (wasAtBottom) {
|
|
10322
|
-
s.term.scrollToBottom();
|
|
10323
|
-
} else if (s.term.cols !== oldCols) {
|
|
10324
|
-
_restoreScrollAnchor(s.term, savedAnchor);
|
|
10325
|
-
s._promptLinesResolved = false;
|
|
10326
|
-
} else {
|
|
10327
|
-
s.term.scrollToLine(savedLine2);
|
|
10328
|
-
}
|
|
10753
|
+
_fitTerminalPreservingViewport(s, sid, { sendResize: true });
|
|
10329
10754
|
return;
|
|
10330
10755
|
}
|
|
10331
10756
|
} catch (_) { /* proposeDimensions may fail if container hidden */ }
|
|
10332
10757
|
if (s.writer.followMode) {
|
|
10333
|
-
s
|
|
10758
|
+
_ensureScrolledToBottom(s);
|
|
10334
10759
|
}
|
|
10335
10760
|
}
|
|
10336
10761
|
}, 1500);
|
|
@@ -10341,11 +10766,7 @@ function onOutput(msg) {
|
|
|
10341
10766
|
if (banner) {
|
|
10342
10767
|
banner.remove();
|
|
10343
10768
|
s._bannerCleared = true;
|
|
10344
|
-
|
|
10345
|
-
const ab = s.writer.followMode;
|
|
10346
|
-
s.fitAddon.fit();
|
|
10347
|
-
s.writer.followMode = ab; // restore after fit reflow
|
|
10348
|
-
if (!ab) s.term.scrollToLine(ln);
|
|
10769
|
+
_fitTerminalPreservingViewport(s, sid);
|
|
10349
10770
|
} else {
|
|
10350
10771
|
s._bannerCleared = true; // no banner exists, skip future checks
|
|
10351
10772
|
}
|
|
@@ -10366,8 +10787,7 @@ setInterval(() => {
|
|
|
10366
10787
|
if (!id) return;
|
|
10367
10788
|
const s = state.sessions.get(id);
|
|
10368
10789
|
if (!s || !s.term) return;
|
|
10369
|
-
const
|
|
10370
|
-
const atBottom = buf.baseY > 0 && buf.viewportY >= buf.baseY;
|
|
10790
|
+
const atBottom = _isAtTerminalFollowBottom(s);
|
|
10371
10791
|
|
|
10372
10792
|
// Fix 1: followMode is false but viewport IS at bottom — unlock
|
|
10373
10793
|
if (!s.writer.followMode && atBottom && !s.writer._chunking) {
|
|
@@ -10390,31 +10810,177 @@ setInterval(() => {
|
|
|
10390
10810
|
s.writer.queue = '';
|
|
10391
10811
|
s.writer.scheduled = false;
|
|
10392
10812
|
if (batch) chunkedWrite(s, batch, () => {
|
|
10393
|
-
if (s.writer.followMode && !s.writer._userScrollLocked && s.term) s
|
|
10813
|
+
if (s.writer.followMode && !s.writer._userScrollLocked && s.term) _ensureScrolledToBottom(s);
|
|
10394
10814
|
});
|
|
10395
10815
|
});
|
|
10396
10816
|
}
|
|
10397
10817
|
}, 2000);
|
|
10398
10818
|
|
|
10399
|
-
|
|
10400
|
-
|
|
10401
|
-
|
|
10402
|
-
|
|
10403
|
-
|
|
10819
|
+
function _terminalFollowViewportTarget(s) {
|
|
10820
|
+
if (!s || !s.term) return 0;
|
|
10821
|
+
const buf = s.term.buffer.active;
|
|
10822
|
+
const baseY = buf.baseY || 0;
|
|
10823
|
+
const rows = s.term.rows || 0;
|
|
10824
|
+
if (rows <= 0) return baseY;
|
|
10825
|
+
|
|
10826
|
+
const agentType = _clientAgentTypeForSession(s);
|
|
10827
|
+
if (agentType !== 'codex') return baseY;
|
|
10828
|
+
|
|
10829
|
+
// Codex restores often carry a screen drawn at the previous PTY height. When
|
|
10830
|
+
// CTM opens a taller browser viewport, xterm appends blank screen rows below
|
|
10831
|
+
// the live prompt. Literal scrollToBottom then anchors those blanks, making
|
|
10832
|
+
// the input look stuck halfway up the terminal. Anchor the last meaningful
|
|
10833
|
+
// screen row at the viewport bottom when the blank tail is large.
|
|
10834
|
+
const screenStart = baseY;
|
|
10835
|
+
const screenEnd = baseY + rows - 1;
|
|
10836
|
+
let lastMeaningful = -1;
|
|
10837
|
+
for (let row = screenEnd; row >= screenStart; row--) {
|
|
10838
|
+
const line = buf.getLine(row);
|
|
10839
|
+
if (line && line.translateToString(true).trim()) {
|
|
10840
|
+
lastMeaningful = row;
|
|
10841
|
+
break;
|
|
10842
|
+
}
|
|
10843
|
+
}
|
|
10844
|
+
if (lastMeaningful < 0) return baseY;
|
|
10845
|
+
const cursorAbs = baseY + (buf.cursorY || 0);
|
|
10846
|
+
const anchor = Math.max(lastMeaningful, cursorAbs);
|
|
10847
|
+
const blankTailRows = screenEnd - anchor;
|
|
10848
|
+
const threshold = Math.max(6, Math.floor(rows * 0.20));
|
|
10849
|
+
if (blankTailRows < threshold) return baseY;
|
|
10850
|
+
return Math.max(0, Math.min(baseY, anchor - rows + 1));
|
|
10851
|
+
}
|
|
10852
|
+
|
|
10853
|
+
function _withProgrammaticTerminalScroll(s, fn) {
|
|
10854
|
+
if (!s || !s.writer) {
|
|
10855
|
+
try { fn(); } catch {}
|
|
10856
|
+
return;
|
|
10857
|
+
}
|
|
10858
|
+
s.writer._programmaticScrollDepth = (s.writer._programmaticScrollDepth || 0) + 1;
|
|
10859
|
+
try { fn(); } catch {}
|
|
10860
|
+
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
10861
|
+
if (!s.writer) return;
|
|
10862
|
+
s.writer._programmaticScrollDepth = Math.max(0, (s.writer._programmaticScrollDepth || 1) - 1);
|
|
10863
|
+
}));
|
|
10864
|
+
}
|
|
10865
|
+
|
|
10866
|
+
function _scrollTerminalToFollowBottom(s) {
|
|
10867
|
+
if (!s || !s.term) return;
|
|
10868
|
+
const target = _terminalFollowViewportTarget(s);
|
|
10869
|
+
_withProgrammaticTerminalScroll(s, () => {
|
|
10870
|
+
s.term.scrollToLine(target);
|
|
10871
|
+
const buf = s.term.buffer.active;
|
|
10872
|
+
const vp = s.container ? s.container.querySelector('.xterm-viewport') : null;
|
|
10873
|
+
if (vp && target >= buf.baseY) {
|
|
10874
|
+
try { vp.scrollTop = vp.scrollHeight; } catch {}
|
|
10875
|
+
}
|
|
10876
|
+
});
|
|
10877
|
+
}
|
|
10878
|
+
|
|
10879
|
+
function _isAtTerminalFollowBottom(s) {
|
|
10880
|
+
if (!s || !s.term) return false;
|
|
10881
|
+
const target = _terminalFollowViewportTarget(s);
|
|
10882
|
+
const buf = s.term.buffer.active;
|
|
10883
|
+
const viewportY = buf.viewportY || 0;
|
|
10884
|
+
const baseY = buf.baseY || 0;
|
|
10885
|
+
return Math.abs(viewportY - target) <= 1 || Math.abs(viewportY - baseY) <= 1;
|
|
10886
|
+
}
|
|
10887
|
+
|
|
10888
|
+
function _findCodexInternalBlankGap(s) {
|
|
10889
|
+
if (!s || !s.term) return null;
|
|
10890
|
+
if (_clientAgentTypeForSession(s) !== 'codex') return null;
|
|
10891
|
+
if (s.writer && s.writer._userScrollLocked) return null;
|
|
10892
|
+
const buf = s.term.buffer.active;
|
|
10893
|
+
const rows = s.term.rows || 0;
|
|
10894
|
+
const baseY = buf.baseY || 0;
|
|
10895
|
+
if (rows < 20 || Math.abs((buf.viewportY || 0) - baseY) > 1) return null;
|
|
10896
|
+
|
|
10897
|
+
const meaningful = [];
|
|
10898
|
+
let promptAbs = -1;
|
|
10899
|
+
let hasCompletedTurnOutputAfterPrompt = false;
|
|
10900
|
+
for (let offset = 0; offset < rows; offset++) {
|
|
10901
|
+
const abs = baseY + offset;
|
|
10902
|
+
const line = buf.getLine(abs);
|
|
10903
|
+
const text = line ? line.translateToString(true) : '';
|
|
10904
|
+
if (text.trim().startsWith('›')) promptAbs = abs;
|
|
10905
|
+
if (text.trim()) meaningful.push(abs);
|
|
10906
|
+
}
|
|
10907
|
+
if (promptAbs < 0 || meaningful.length < 2) return null;
|
|
10908
|
+
for (const abs of meaningful) {
|
|
10909
|
+
if (abs <= promptAbs) continue;
|
|
10910
|
+
const line = buf.getLine(abs);
|
|
10911
|
+
const text = line ? line.translateToString(true).trim() : '';
|
|
10912
|
+
if (!text) continue;
|
|
10913
|
+
if (/^gpt-[\w.-]+\s+/i.test(text) || /\b(x?high|medium|low)\b.*\s-\s/.test(text)) continue;
|
|
10914
|
+
hasCompletedTurnOutputAfterPrompt = true;
|
|
10915
|
+
break;
|
|
10916
|
+
}
|
|
10917
|
+
if (!hasCompletedTurnOutputAfterPrompt) return null;
|
|
10918
|
+
|
|
10919
|
+
let best = null;
|
|
10920
|
+
for (let i = 1; i < meaningful.length; i++) {
|
|
10921
|
+
const prev = meaningful[i - 1];
|
|
10922
|
+
const next = meaningful[i];
|
|
10923
|
+
if (next > promptAbs) break;
|
|
10924
|
+
const gapRows = next - prev - 1;
|
|
10925
|
+
if (!best || gapRows > best.gapRows) best = { startAbs: prev + 1, nextAbs: next, gapRows };
|
|
10926
|
+
}
|
|
10927
|
+
const threshold = Math.max(8, Math.floor(rows * 0.18));
|
|
10928
|
+
if (!best || best.gapRows < threshold) return null;
|
|
10929
|
+
const keepRows = 2;
|
|
10930
|
+
const deleteRows = best.gapRows - keepRows;
|
|
10931
|
+
if (deleteRows <= 0) return null;
|
|
10932
|
+
return { startAbs: best.startAbs, deleteRows };
|
|
10933
|
+
}
|
|
10934
|
+
|
|
10935
|
+
function _compactCodexInternalBlankGap(s, onDone) {
|
|
10936
|
+
if (!s || !s.term || s._codexBlankGapCompacting) return false;
|
|
10937
|
+
const gap = _findCodexInternalBlankGap(s);
|
|
10938
|
+
if (!gap) return false;
|
|
10939
|
+
const buf = s.term.buffer.active;
|
|
10940
|
+
const baseY = buf.baseY || 0;
|
|
10941
|
+
const deleteScreenRow = Math.max(1, gap.startAbs - baseY + 1);
|
|
10942
|
+
const cursorAbs = baseY + (buf.cursorY || 0);
|
|
10943
|
+
const nextCursorAbs = cursorAbs >= gap.startAbs ? Math.max(baseY, cursorAbs - gap.deleteRows) : cursorAbs;
|
|
10944
|
+
const nextCursorRow = Math.max(1, nextCursorAbs - baseY + 1);
|
|
10945
|
+
const nextCursorCol = Math.max(1, (buf.cursorX || 0) + 1);
|
|
10946
|
+
const seq = '\x1b[' + deleteScreenRow + ';1H\x1b[' + gap.deleteRows + 'M\x1b[' + nextCursorRow + ';' + nextCursorCol + 'H';
|
|
10947
|
+
|
|
10948
|
+
s._codexBlankGapCompacting = true;
|
|
10949
|
+
if (s.writer) s.writer._programmaticScrollDepth = (s.writer._programmaticScrollDepth || 0) + 1;
|
|
10950
|
+
let finished = false;
|
|
10951
|
+
const finish = () => {
|
|
10952
|
+
if (finished) return;
|
|
10953
|
+
finished = true;
|
|
10954
|
+
s._codexBlankGapCompacting = false;
|
|
10955
|
+
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
10956
|
+
if (s.writer) s.writer._programmaticScrollDepth = Math.max(0, (s.writer._programmaticScrollDepth || 1) - 1);
|
|
10957
|
+
}));
|
|
10958
|
+
if (onDone) onDone();
|
|
10959
|
+
};
|
|
10960
|
+
try {
|
|
10961
|
+
s.term.write(seq, finish);
|
|
10962
|
+
setTimeout(finish, 500);
|
|
10963
|
+
} catch (_) {
|
|
10964
|
+
finish();
|
|
10965
|
+
}
|
|
10966
|
+
return true;
|
|
10967
|
+
}
|
|
10968
|
+
|
|
10969
|
+
// Ensure the terminal viewport is scrolled to the user's active bottom. For
|
|
10970
|
+
// most terminals this is xterm's literal bottom. For Codex restored TUI
|
|
10971
|
+
// screens, it is the last meaningful row before a large blank tail.
|
|
10404
10972
|
//
|
|
10405
10973
|
// Skip if the user has manually scrolled (`_userScrollLocked`) — we don't want
|
|
10406
10974
|
// to yank them back to the bottom if they were reading scrollback.
|
|
10407
10975
|
function _ensureScrolledToBottom(s) {
|
|
10408
10976
|
if (!s || !s.term) return;
|
|
10409
10977
|
if (s.writer && s.writer._userScrollLocked) return;
|
|
10410
|
-
|
|
10411
|
-
|
|
10412
|
-
if (vp) { try { vp.scrollTop = vp.scrollHeight; } catch {} }
|
|
10978
|
+
if (_compactCodexInternalBlankGap(s, () => _ensureScrolledToBottom(s))) return;
|
|
10979
|
+
_scrollTerminalToFollowBottom(s);
|
|
10413
10980
|
requestAnimationFrame(() => {
|
|
10414
10981
|
if (!s.term) return;
|
|
10415
10982
|
if (s.writer && s.writer._userScrollLocked) return;
|
|
10416
|
-
|
|
10417
|
-
if (vp) { try { vp.scrollTop = vp.scrollHeight; } catch {} }
|
|
10983
|
+
_scrollTerminalToFollowBottom(s);
|
|
10418
10984
|
});
|
|
10419
10985
|
}
|
|
10420
10986
|
|
|
@@ -10518,7 +11084,7 @@ function onSnapshot(msg) {
|
|
|
10518
11084
|
s._autoReflowAttempted = true;
|
|
10519
11085
|
send({ type: 'snapshot', id: msg.id, cols: s.term.cols, rows: s.term.rows });
|
|
10520
11086
|
} else {
|
|
10521
|
-
s
|
|
11087
|
+
_ensureScrolledToBottom(s);
|
|
10522
11088
|
}
|
|
10523
11089
|
return;
|
|
10524
11090
|
}
|
|
@@ -10532,7 +11098,7 @@ function onSnapshot(msg) {
|
|
|
10532
11098
|
try {
|
|
10533
11099
|
const dims = s.fitAddon.proposeDimensions();
|
|
10534
11100
|
if (dims && (dims.cols !== s.term.cols || dims.rows !== s.term.rows)) {
|
|
10535
|
-
s
|
|
11101
|
+
_fitTerminalPreservingViewport(s, id);
|
|
10536
11102
|
}
|
|
10537
11103
|
} catch {}
|
|
10538
11104
|
// [DO-NOT-REMOVE] misformatted-output fix — fixes user-reported bug:
|
|
@@ -10589,7 +11155,6 @@ function onSnapshot(msg) {
|
|
|
10589
11155
|
const snapshotDone = () => {
|
|
10590
11156
|
s.writer.followMode = true;
|
|
10591
11157
|
s.writer._userScrollLocked = false;
|
|
10592
|
-
s.term.scrollToBottom();
|
|
10593
11158
|
// Double-RAF the scroll — the browser may reset .xterm-viewport scrollTop to 0
|
|
10594
11159
|
// when the container goes display:none → display:flex during tab switch.
|
|
10595
11160
|
// xterm's internal viewportY is correct, but the DOM scrollTop can lag a frame
|
|
@@ -10897,6 +11462,29 @@ function tabOrderSortCmp(aEntry, bEntry) {
|
|
|
10897
11462
|
return (ai === -1 ? 9999 : ai) - (bi === -1 ? 9999 : bi);
|
|
10898
11463
|
}
|
|
10899
11464
|
|
|
11465
|
+
function worktreeAttentionBadge(s) {
|
|
11466
|
+
const wt = s?.meta?.worktreeStatus;
|
|
11467
|
+
if (!wt || !wt.needsAttention) return '';
|
|
11468
|
+
const branch = wt.branch || s?.meta?.branch || '';
|
|
11469
|
+
if (!branch || branch === 'main' || branch === 'master') return '';
|
|
11470
|
+
const dirtyFiles = Number(wt.dirtyFiles || 0);
|
|
11471
|
+
const unmergedCommits = Number(wt.unmergedCommits || 0);
|
|
11472
|
+
if (dirtyFiles <= 0 && unmergedCommits <= 0) return '';
|
|
11473
|
+
|
|
11474
|
+
const parts = [];
|
|
11475
|
+
const titleParts = [];
|
|
11476
|
+
if (dirtyFiles > 0) {
|
|
11477
|
+
parts.push(`<span class="wt-part" aria-hidden="true">✎${dirtyFiles}</span>`);
|
|
11478
|
+
titleParts.push(`${dirtyFiles} uncommitted file${dirtyFiles === 1 ? '' : 's'}`);
|
|
11479
|
+
}
|
|
11480
|
+
if (unmergedCommits > 0) {
|
|
11481
|
+
parts.push(`<span class="wt-part" aria-hidden="true">↑${unmergedCommits}</span>`);
|
|
11482
|
+
titleParts.push(`${unmergedCommits} commit${unmergedCommits === 1 ? '' : 's'} not on main`);
|
|
11483
|
+
}
|
|
11484
|
+
const title = `${branch}: ${titleParts.join(', ')}`;
|
|
11485
|
+
return `<span class="worktree-attn-badge" title="${escHtml(title)}" aria-label="${escHtml(title)}">${parts.join('')}</span>`;
|
|
11486
|
+
}
|
|
11487
|
+
|
|
10900
11488
|
let _renderSessionListTimer = null;
|
|
10901
11489
|
function renderSessionList(force) {
|
|
10902
11490
|
if (!force) {
|
|
@@ -10977,11 +11565,13 @@ function renderSessionList(force) {
|
|
|
10977
11565
|
).join('');
|
|
10978
11566
|
const branchName = s.meta?.branch || '';
|
|
10979
11567
|
const branchBadge = branchName && branchName !== 'main' ? `<span class="branch-badge" title="Branch: ${escHtml(branchName)}">☍ ${escHtml(branchName.length > 15 ? branchName.slice(0, 15) + '...' : branchName)}</span>` : '';
|
|
10980
|
-
|
|
11568
|
+
const worktreeBadge = worktreeAttentionBadge(s);
|
|
11569
|
+
return `${groupSep}<div class="session-group${isActive ? ' active' : ''}" data-session-id="${id}"><div class="session-item ${isActive ? 'active' : ''} ${isStale ? 'stale' : ''} ${sStatus.cls === 'running' ? 'running' : ''} ${worktreeBadge ? 'has-worktree-attn' : ''}" data-session-id="${id}" data-agent="${agentType}" onclick="sessionItemClick('${id}', event)" ondblclick="sessionItemDblClick('${id}', event)">
|
|
10981
11570
|
<span class="dot"></span>
|
|
10982
11571
|
${providerIconSvg(agentType, 14)}
|
|
10983
11572
|
<span class="label">${escHtml(label)}</span>
|
|
10984
11573
|
${branchBadge}
|
|
11574
|
+
${worktreeBadge}
|
|
10985
11575
|
${statusTag}
|
|
10986
11576
|
<span class="agent-badge ${agentType}">${agentLabel}</span>
|
|
10987
11577
|
${idleHint}
|
|
@@ -11047,6 +11637,13 @@ function getSessionStatus(s) {
|
|
|
11047
11637
|
}
|
|
11048
11638
|
}
|
|
11049
11639
|
|
|
11640
|
+
const serverWorkingAt = s._serverWorkingAt || 0;
|
|
11641
|
+
const waitingAt = s._waitingForInputAt || 0;
|
|
11642
|
+
const serverWorking = serverWorkingAt &&
|
|
11643
|
+
(now - serverWorkingAt) < 10000 &&
|
|
11644
|
+
(!s._waitingForInput || !waitingAt || serverWorkingAt >= waitingAt);
|
|
11645
|
+
if (serverWorking) return { cls: 'running', text: 'Running' };
|
|
11646
|
+
|
|
11050
11647
|
if (s._waitingForInput) return { cls: 'waiting', text: 'Waiting' };
|
|
11051
11648
|
|
|
11052
11649
|
// Primary: SessionStream status (received via WS stream-status events).
|
|
@@ -11061,8 +11658,6 @@ function getSessionStatus(s) {
|
|
|
11061
11658
|
// Fallback: old PTY-based signals (for sessions not tracked by SessionStream)
|
|
11062
11659
|
const lastOut = Math.max(s._lastOutputAt || 0, s.meta?.lastActivity || 0);
|
|
11063
11660
|
const recentOutput = lastOut && (now - lastOut) < 5000;
|
|
11064
|
-
const serverWorking = s._serverWorkingAt && (now - s._serverWorkingAt) < 10000;
|
|
11065
|
-
if (serverWorking) return { cls: 'running', text: 'Running' };
|
|
11066
11661
|
if (s._waitingForInput && !recentOutput) return { cls: 'waiting', text: 'Waiting' };
|
|
11067
11662
|
if (recentOutput) return { cls: 'running', text: 'Running' };
|
|
11068
11663
|
const justSentInput = s._lastInputAt && (now - s._lastInputAt) < 3000;
|
|
@@ -11383,11 +11978,7 @@ function showCompactBannerIfStale(id, s) {
|
|
|
11383
11978
|
s.container.prepend(banner);
|
|
11384
11979
|
// Refit terminal since banner takes space — preserve scroll position
|
|
11385
11980
|
requestAnimationFrame(() => {
|
|
11386
|
-
|
|
11387
|
-
const atBot = s.writer.followMode;
|
|
11388
|
-
s.fitAddon.fit();
|
|
11389
|
-
s.writer.followMode = atBot; // restore after fit reflow
|
|
11390
|
-
if (!atBot) s.term.scrollToLine(ln);
|
|
11981
|
+
_fitTerminalPreservingViewport(s, id);
|
|
11391
11982
|
});
|
|
11392
11983
|
}
|
|
11393
11984
|
|
|
@@ -11404,11 +11995,7 @@ function dismissCompactBanner(id) {
|
|
|
11404
11995
|
if (banner) {
|
|
11405
11996
|
banner.remove();
|
|
11406
11997
|
requestAnimationFrame(() => {
|
|
11407
|
-
|
|
11408
|
-
const atBot = s.writer.followMode;
|
|
11409
|
-
s.fitAddon.fit();
|
|
11410
|
-
s.writer.followMode = atBot; // restore after fit reflow
|
|
11411
|
-
if (!atBot) s.term.scrollToLine(ln);
|
|
11998
|
+
_fitTerminalPreservingViewport(s, id);
|
|
11412
11999
|
});
|
|
11413
12000
|
}
|
|
11414
12001
|
}
|
|
@@ -12140,20 +12727,7 @@ function toggleSidebar() {
|
|
|
12140
12727
|
setTimeout(() => {
|
|
12141
12728
|
const s = state.sessions.get(state.activeTab);
|
|
12142
12729
|
if (!s) return;
|
|
12143
|
-
|
|
12144
|
-
const savedLine = s.term.buffer.active.viewportY;
|
|
12145
|
-
const oldCols = s.term.cols;
|
|
12146
|
-
const savedAnchor = wasAtBottom ? null : _getScrollAnchor(s.term);
|
|
12147
|
-
s.fitAddon.fit();
|
|
12148
|
-
s.writer.followMode = wasAtBottom; // restore after fit reflow
|
|
12149
|
-
if (!wasAtBottom) {
|
|
12150
|
-
if (s.term.cols !== oldCols) {
|
|
12151
|
-
_restoreScrollAnchor(s.term, savedAnchor);
|
|
12152
|
-
s._promptLinesResolved = false;
|
|
12153
|
-
} else {
|
|
12154
|
-
s.term.scrollToLine(savedLine);
|
|
12155
|
-
}
|
|
12156
|
-
}
|
|
12730
|
+
_fitTerminalPreservingViewport(s, state.activeTab);
|
|
12157
12731
|
}, 100);
|
|
12158
12732
|
}
|
|
12159
12733
|
}
|
|
@@ -12606,8 +13180,7 @@ function renderPaneTree() {
|
|
|
12606
13180
|
s.term.options.fontSize = sz;
|
|
12607
13181
|
s._suppressResize = false;
|
|
12608
13182
|
}
|
|
12609
|
-
s.
|
|
12610
|
-
send({ type: 'resize', id: leaf.sessionId, cols: s.term.cols, rows: s.term.rows });
|
|
13183
|
+
_fitTerminalPreservingViewport(s, leaf.sessionId, { sendResize: true });
|
|
12611
13184
|
});
|
|
12612
13185
|
});
|
|
12613
13186
|
}
|
|
@@ -12714,11 +13287,51 @@ function fitAllVisibleTerminals() {
|
|
|
12714
13287
|
if (!leaf.sessionId) return;
|
|
12715
13288
|
var s = state.sessions.get(leaf.sessionId);
|
|
12716
13289
|
if (!s || !s.fitAddon || !s.term) return;
|
|
12717
|
-
s.
|
|
12718
|
-
send({ type: 'resize', id: leaf.sessionId, cols: s.term.cols, rows: s.term.rows });
|
|
13290
|
+
_fitTerminalPreservingViewport(s, leaf.sessionId, { sendResize: true });
|
|
12719
13291
|
});
|
|
12720
13292
|
}
|
|
12721
13293
|
|
|
13294
|
+
function _isTerminalFollowingVisualBottom(s) {
|
|
13295
|
+
if (!s || !s.term) return false;
|
|
13296
|
+
if (s.writer && s.writer._userScrollLocked) return false;
|
|
13297
|
+
return !!(s.writer && s.writer.followMode) || _isAtTerminalFollowBottom(s);
|
|
13298
|
+
}
|
|
13299
|
+
|
|
13300
|
+
function _fitTerminalPreservingViewport(s, sessionId, opts) {
|
|
13301
|
+
opts = opts || {};
|
|
13302
|
+
if (!s || !s.term || !s.fitAddon) return;
|
|
13303
|
+
const buf = s.term.buffer.active;
|
|
13304
|
+
const wasFollowing = _isTerminalFollowingVisualBottom(s);
|
|
13305
|
+
const savedLine = buf.viewportY;
|
|
13306
|
+
const oldCols = s.term.cols;
|
|
13307
|
+
const savedAnchor = wasFollowing ? null : _getScrollAnchor(s.term);
|
|
13308
|
+
const savedFollow = s.writer ? !!s.writer.followMode : false;
|
|
13309
|
+
const savedLocked = s.writer ? !!s.writer._userScrollLocked : false;
|
|
13310
|
+
|
|
13311
|
+
s.fitAddon.fit();
|
|
13312
|
+
|
|
13313
|
+
if (s.writer) {
|
|
13314
|
+
s.writer.followMode = savedFollow;
|
|
13315
|
+
s.writer._userScrollLocked = savedLocked;
|
|
13316
|
+
}
|
|
13317
|
+
if (opts.sendResize && sessionId) {
|
|
13318
|
+
send({ type: 'resize', id: sessionId, cols: s.term.cols, rows: s.term.rows });
|
|
13319
|
+
}
|
|
13320
|
+
|
|
13321
|
+
if (wasFollowing) {
|
|
13322
|
+
if (s.writer) {
|
|
13323
|
+
s.writer.followMode = true;
|
|
13324
|
+
s.writer._userScrollLocked = false;
|
|
13325
|
+
}
|
|
13326
|
+
_ensureScrolledToBottom(s);
|
|
13327
|
+
} else if (s.term.cols !== oldCols) {
|
|
13328
|
+
_withProgrammaticTerminalScroll(s, () => _restoreScrollAnchor(s.term, savedAnchor));
|
|
13329
|
+
s._promptLinesResolved = false;
|
|
13330
|
+
} else {
|
|
13331
|
+
_withProgrammaticTerminalScroll(s, () => s.term.scrollToLine(savedLine));
|
|
13332
|
+
}
|
|
13333
|
+
}
|
|
13334
|
+
|
|
12722
13335
|
function saveSplitLayout() {
|
|
12723
13336
|
// Save whichever layout is active (current or saved/suspended)
|
|
12724
13337
|
var layout = state.splitRoot || state.savedSplitLayout;
|
|
@@ -12946,26 +13559,7 @@ function fitActiveTerminal() {
|
|
|
12946
13559
|
if (state.activeTab && state.sessions.has(state.activeTab)) {
|
|
12947
13560
|
const s = state.sessions.get(state.activeTab);
|
|
12948
13561
|
if (!s.term || !s.fitAddon) return; // stub session — no terminal yet
|
|
12949
|
-
|
|
12950
|
-
const wasAtBottom = buf.viewportY >= buf.baseY;
|
|
12951
|
-
const savedLine = buf.viewportY;
|
|
12952
|
-
const oldCols = s.term.cols;
|
|
12953
|
-
const savedAnchor = wasAtBottom ? null : _getScrollAnchor(s.term);
|
|
12954
|
-
const savedFollow = s.writer.followMode;
|
|
12955
|
-
s.fitAddon.fit();
|
|
12956
|
-
s.writer.followMode = savedFollow; // restore after fit reflow
|
|
12957
|
-
send({ type: 'resize', id: state.activeTab, cols: s.term.cols, rows: s.term.rows });
|
|
12958
|
-
if (wasAtBottom) {
|
|
12959
|
-
s.term.scrollToBottom();
|
|
12960
|
-
} else if (s.term.cols !== oldCols) {
|
|
12961
|
-
// Cols changed — reflow shifted content, use anchor search
|
|
12962
|
-
_restoreScrollAnchor(s.term, savedAnchor);
|
|
12963
|
-
// Invalidate prompt line positions — reflow shifts them
|
|
12964
|
-
s._promptLinesResolved = false;
|
|
12965
|
-
} else {
|
|
12966
|
-
// Same cols — just restore line position
|
|
12967
|
-
s.term.scrollToLine(savedLine);
|
|
12968
|
-
}
|
|
13562
|
+
_fitTerminalPreservingViewport(s, state.activeTab, { sendResize: true });
|
|
12969
13563
|
}
|
|
12970
13564
|
});
|
|
12971
13565
|
}
|
|
@@ -13050,8 +13644,7 @@ window.addEventListener('resize', updateTabOverflowBtn);
|
|
|
13050
13644
|
const size = s.term.options.fontSize;
|
|
13051
13645
|
s.term.options.fontSize = size + 1;
|
|
13052
13646
|
s.term.options.fontSize = size;
|
|
13053
|
-
|
|
13054
|
-
send({ type: 'resize', id: sid, cols: s.term.cols, rows: s.term.rows });
|
|
13647
|
+
_fitTerminalPreservingViewport(s, sid, { sendResize: true });
|
|
13055
13648
|
}, 200);
|
|
13056
13649
|
}
|
|
13057
13650
|
// Re-watch with the new DPI value
|
|
@@ -13376,8 +13969,9 @@ async function loadRecentSessions() {
|
|
|
13376
13969
|
populateModelFilter(allRecentSessions);
|
|
13377
13970
|
renderFilteredSessions();
|
|
13378
13971
|
|
|
13379
|
-
// Resolve pending hash
|
|
13380
|
-
|
|
13972
|
+
// Resolve pending review hash sessions here. Active #session=<id> hashes are
|
|
13973
|
+
// handled by onServerReady(), after live PTY sessions have been restored.
|
|
13974
|
+
if (state.pendingHashSession && state.pendingHashType === 'review') {
|
|
13381
13975
|
const s = allRecentSessions.find(x => x.sessionId === state.pendingHashSession)
|
|
13382
13976
|
|| allRecentSessions.find(x => x.provisionalId === state.pendingHashSession);
|
|
13383
13977
|
if (s) {
|
|
@@ -14854,7 +15448,7 @@ function escHtml(s) {
|
|
|
14854
15448
|
// element (.session-item or .tab) sets color via CSS so the icon inherits the
|
|
14855
15449
|
// per-provider hue and inverts cleanly when the row is active (light bg, dark text).
|
|
14856
15450
|
// Returns an HTML string with title for screen-readers + hover tooltip.
|
|
14857
|
-
const AGENT_LABELS = { walle: 'Wall-E', codex: 'Codex', gemini: 'Gemini', claude: 'Claude Code', shell: 'Shell' };
|
|
15451
|
+
const AGENT_LABELS = { walle: 'Wall-E', codex: 'Codex', gemini: 'Gemini', claude: 'Claude Code', 'claude-desktop': 'Claude Desktop', shell: 'Shell' };
|
|
14858
15452
|
function getAgentType(s) {
|
|
14859
15453
|
if (!s) return 'shell';
|
|
14860
15454
|
if (s.meta?.type === 'walle') return 'walle';
|
|
@@ -14870,6 +15464,7 @@ function providerIconSvg(agentType, sizePx) {
|
|
|
14870
15464
|
// 16x16 viewBox, fill / stroke = currentColor.
|
|
14871
15465
|
let inner;
|
|
14872
15466
|
switch (agentType) {
|
|
15467
|
+
case 'claude-desktop':
|
|
14873
15468
|
case 'claude':
|
|
14874
15469
|
// Anthropic / Claude brand mark (the asterisk-sparkle from claude.ai's
|
|
14875
15470
|
// favicon). Original path is on a 248×248 grid; we keep that viewBox
|
|
@@ -15652,7 +16247,7 @@ function onSkillSessionCreated(msg) {
|
|
|
15652
16247
|
const newWidth = Math.max(150, Math.min(600, e.clientX));
|
|
15653
16248
|
sidebar.style.width = newWidth + 'px';
|
|
15654
16249
|
if (state.activeTab && state.sessions.has(state.activeTab)) {
|
|
15655
|
-
state.sessions.get(state.activeTab).
|
|
16250
|
+
_fitTerminalPreservingViewport(state.sessions.get(state.activeTab), state.activeTab);
|
|
15656
16251
|
}
|
|
15657
16252
|
});
|
|
15658
16253
|
|
|
@@ -17136,13 +17731,18 @@ function onDataChanged(msg) {
|
|
|
17136
17731
|
// Track active title flash so it can be cancelled when input is sent
|
|
17137
17732
|
let _titleFlashStop = null;
|
|
17138
17733
|
|
|
17139
|
-
// Clear waiting state for a session (tab indicator + title flash)
|
|
17140
|
-
|
|
17734
|
+
// Clear waiting state for a session (tab indicator + title flash).
|
|
17735
|
+
// markInput is only for actual user input paths; server-side resume signals
|
|
17736
|
+
// should not manufacture a "just sent input" running window.
|
|
17737
|
+
function clearWaitingState(sessionId, opts = {}) {
|
|
17141
17738
|
const s = state.sessions.get(sessionId);
|
|
17142
17739
|
if (!s) return;
|
|
17143
17740
|
const wasWaiting = s._waitingForInput;
|
|
17144
17741
|
s._waitingForInput = false;
|
|
17145
|
-
s.
|
|
17742
|
+
s._waitingForInputAt = 0;
|
|
17743
|
+
const now = opts.timestamp || Date.now();
|
|
17744
|
+
if (opts.markInput !== false) s._lastInputAt = now;
|
|
17745
|
+
if (opts.markWorking) s._serverWorkingAt = now;
|
|
17146
17746
|
// Only do DOM work if the session was actually in waiting state
|
|
17147
17747
|
if (wasWaiting) {
|
|
17148
17748
|
const tabs = document.querySelectorAll('#tabbar .tab');
|
|
@@ -17205,10 +17805,18 @@ function onSessionActivity(msg) {
|
|
|
17205
17805
|
if (!s) continue;
|
|
17206
17806
|
const oldBucket = activeActivityBucket(s);
|
|
17207
17807
|
const oldStatus = getSessionStatus(s).cls;
|
|
17208
|
-
|
|
17808
|
+
const serverTs = SessionActivityUtils.parseTimeMs(ts) || Date.now();
|
|
17809
|
+
if (serverState === 'thinking' || serverState === 'active') {
|
|
17810
|
+
const waitingAt = s._waitingForInputAt || 0;
|
|
17811
|
+
const activityIsNewerThanWaiting = !s._waitingForInput || !waitingAt || serverTs >= waitingAt;
|
|
17812
|
+
if (activityIsNewerThanWaiting) s._serverWorkingAt = Date.now();
|
|
17813
|
+
if (s._waitingForInput && activityIsNewerThanWaiting) {
|
|
17814
|
+
clearWaitingState(id, { markInput: false, markWorking: true });
|
|
17815
|
+
}
|
|
17816
|
+
}
|
|
17209
17817
|
if (s.meta && ts) {
|
|
17210
17818
|
const prev = SessionActivityUtils.parseTimeMs(s.meta.lastActivity);
|
|
17211
|
-
const next =
|
|
17819
|
+
const next = serverTs;
|
|
17212
17820
|
if (next && next > prev) s.meta.lastActivity = ts;
|
|
17213
17821
|
}
|
|
17214
17822
|
if (currentActiveSort === 'by_activity' && (oldBucket !== activeActivityBucket(s) || oldStatus !== getSessionStatus(s).cls)) {
|
|
@@ -17223,7 +17831,7 @@ function onSessionActivity(msg) {
|
|
|
17223
17831
|
// cannot reliably distinguish TUI redraws from real output.
|
|
17224
17832
|
function onSessionResumed(msg) {
|
|
17225
17833
|
const s = state.sessions.get(msg.id);
|
|
17226
|
-
if (s)
|
|
17834
|
+
if (s) clearWaitingState(msg.id, { markInput: false, markWorking: true, timestamp: msg.timestamp || Date.now() });
|
|
17227
17835
|
}
|
|
17228
17836
|
|
|
17229
17837
|
// Authoritative session state (hooks or OTEL). Wins over the regex fallback.
|
|
@@ -17236,7 +17844,7 @@ function onAuthoritativeStatus(msg) {
|
|
|
17236
17844
|
s._authoritativeStatusAt = msg.timestamp || Date.now();
|
|
17237
17845
|
// When the agent is working, explicitly clear "waiting for input" state —
|
|
17238
17846
|
// the regex fallback may have left it set before hooks took over.
|
|
17239
|
-
if (s._working && s._waitingForInput) clearWaitingState(msg.id);
|
|
17847
|
+
if (s._working && s._waitingForInput) clearWaitingState(msg.id, { markInput: false, markWorking: true, timestamp: s._authoritativeStatusAt });
|
|
17240
17848
|
// Update sidebar dot class
|
|
17241
17849
|
const item = document.querySelector('.session-item[data-session-id="' + CSS.escape(msg.id) + '"]');
|
|
17242
17850
|
if (item) {
|
|
@@ -17324,7 +17932,10 @@ const _lastNotif = {};
|
|
|
17324
17932
|
function onWaitingForInput(msg) {
|
|
17325
17933
|
const sessionId = msg.id;
|
|
17326
17934
|
const session = state.sessions.get(sessionId);
|
|
17327
|
-
if (session)
|
|
17935
|
+
if (session) {
|
|
17936
|
+
session._waitingForInput = true;
|
|
17937
|
+
session._waitingForInputAt = msg.timestamp || Date.now();
|
|
17938
|
+
}
|
|
17328
17939
|
// Re-scan prompts — JSONL is fully written when Claude yields back to user.
|
|
17329
17940
|
// Invalidate cache so we get fresh data (not stale 30s cached results).
|
|
17330
17941
|
for (const key of Object.keys(_promptScanCache)) {
|
|
@@ -17525,8 +18136,30 @@ window.addEventListener('message', (e) => {
|
|
|
17525
18136
|
// --- Hash routing ---
|
|
17526
18137
|
const NAV_TARGETS = ['sessions', 'prompts', 'rules', 'insights', 'permissions', 'codereview', 'walle', 'models', 'backups', 'worktrees', 'setup'];
|
|
17527
18138
|
|
|
17528
|
-
function
|
|
18139
|
+
function _parseHashRoute() {
|
|
17529
18140
|
const hash = location.hash.slice(1);
|
|
18141
|
+
const parts = hash ? hash.split('&') : [];
|
|
18142
|
+
const firstPart = parts[0] || '';
|
|
18143
|
+
const isNav = NAV_TARGETS.includes(firstPart);
|
|
18144
|
+
const params = {};
|
|
18145
|
+
for (const part of (isNav ? parts.slice(1) : parts)) {
|
|
18146
|
+
const eq = part.indexOf('=');
|
|
18147
|
+
if (eq > 0) params[part.slice(0, eq)] = decodeURIComponent(part.slice(eq + 1));
|
|
18148
|
+
}
|
|
18149
|
+
return { hash, firstPart, isNav, params };
|
|
18150
|
+
}
|
|
18151
|
+
|
|
18152
|
+
function captureInitialSessionHashIntent() {
|
|
18153
|
+
const route = _parseHashRoute();
|
|
18154
|
+
if (route.params.session) {
|
|
18155
|
+
state.pendingHashSession = route.params.session;
|
|
18156
|
+
state.pendingHashType = 'active';
|
|
18157
|
+
}
|
|
18158
|
+
}
|
|
18159
|
+
|
|
18160
|
+
function handleHashRoute() {
|
|
18161
|
+
const route = _parseHashRoute();
|
|
18162
|
+
const hash = route.hash;
|
|
17530
18163
|
|
|
17531
18164
|
// No hash — fall back to saved nav pref from DB
|
|
17532
18165
|
if (!hash) {
|
|
@@ -17545,16 +18178,9 @@ function handleHashRoute() {
|
|
|
17545
18178
|
}
|
|
17546
18179
|
|
|
17547
18180
|
// Parse: first segment may be a nav target, rest are key=value params
|
|
17548
|
-
const
|
|
17549
|
-
const
|
|
17550
|
-
const
|
|
17551
|
-
|
|
17552
|
-
// Parse key=value params (skip first part if it's a nav target)
|
|
17553
|
-
const params = {};
|
|
17554
|
-
for (const part of (isNav ? parts.slice(1) : parts)) {
|
|
17555
|
-
const eq = part.indexOf('=');
|
|
17556
|
-
if (eq > 0) params[part.slice(0, eq)] = decodeURIComponent(part.slice(eq + 1));
|
|
17557
|
-
}
|
|
18181
|
+
const firstPart = route.firstPart;
|
|
18182
|
+
const isNav = route.isNav;
|
|
18183
|
+
const params = route.params;
|
|
17558
18184
|
|
|
17559
18185
|
// Check for nav target: #permissions, #prompts, #rules, #insights, #sessions, #codereview
|
|
17560
18186
|
if (isNav && !Object.keys(params).length) {
|
|
@@ -17687,6 +18313,7 @@ if (sessionStorage.getItem('ctm_restarting')) {
|
|
|
17687
18313
|
}
|
|
17688
18314
|
}, 30000);
|
|
17689
18315
|
}
|
|
18316
|
+
captureInitialSessionHashIntent();
|
|
17690
18317
|
connect();
|
|
17691
18318
|
state._prefsLoaded = loadPrefs().then(() => {
|
|
17692
18319
|
loadRecentSessions();
|
|
@@ -17706,7 +18333,7 @@ function onQueueState(msg) {
|
|
|
17706
18333
|
// Scroll terminal to bottom when queue is active (only if user hasn't scrolled up)
|
|
17707
18334
|
if (msg.sessionId) {
|
|
17708
18335
|
const s = state.sessions.get(msg.sessionId);
|
|
17709
|
-
if (s && s.writer.followMode) s
|
|
18336
|
+
if (s && s.writer.followMode) _ensureScrolledToBottom(s);
|
|
17710
18337
|
}
|
|
17711
18338
|
}
|
|
17712
18339
|
|
|
@@ -18165,7 +18792,7 @@ async function refreshQpPromptItems() {
|
|
|
18165
18792
|
if (p.images && p.images.length > 0) {
|
|
18166
18793
|
const newImages = p.images.map(img => ({
|
|
18167
18794
|
id: img.id, filename: img.filename, path: img.file_path,
|
|
18168
|
-
url:
|
|
18795
|
+
url: qpImageUrl(img),
|
|
18169
18796
|
}));
|
|
18170
18797
|
if (JSON.stringify(item.images || []) !== JSON.stringify(newImages)) {
|
|
18171
18798
|
item.images = newImages;
|
|
@@ -18199,6 +18826,12 @@ function promptContentToText(prompt) {
|
|
|
18199
18826
|
return prompt.content || '';
|
|
18200
18827
|
}
|
|
18201
18828
|
|
|
18829
|
+
function qpImageUrl(img) {
|
|
18830
|
+
const source = String(img?.path || img?.file_path || img?.filename || '');
|
|
18831
|
+
const basename = source.split(/[\\/]/).filter(Boolean).pop() || '';
|
|
18832
|
+
return '/api/images/file/' + encodeURIComponent(basename);
|
|
18833
|
+
}
|
|
18834
|
+
|
|
18202
18835
|
async function addSelectedPromptToQp() {
|
|
18203
18836
|
const sel = document.getElementById('qp-prompt-select');
|
|
18204
18837
|
const promptId = parseInt(sel.value);
|
|
@@ -18213,7 +18846,7 @@ async function addSelectedPromptToQp() {
|
|
|
18213
18846
|
text: promptContentToText(prompt),
|
|
18214
18847
|
images: (prompt.images || []).map(img => ({
|
|
18215
18848
|
id: img.id, filename: img.filename, path: img.file_path,
|
|
18216
|
-
url:
|
|
18849
|
+
url: qpImageUrl(img),
|
|
18217
18850
|
})),
|
|
18218
18851
|
});
|
|
18219
18852
|
saveQpDraft();
|
|
@@ -18789,7 +19422,7 @@ async function sendQueueFromPanel() {
|
|
|
18789
19422
|
item.status = 'sent';
|
|
18790
19423
|
saveQpDraft();
|
|
18791
19424
|
renderQpItems();
|
|
18792
|
-
session
|
|
19425
|
+
_ensureScrolledToBottom(session);
|
|
18793
19426
|
} catch (e) {
|
|
18794
19427
|
toast('Failed to send: ' + e.message, { type: 'error' });
|
|
18795
19428
|
}
|
|
@@ -18828,7 +19461,7 @@ activateTab = function(id) {
|
|
|
18828
19461
|
// Re-fit terminal after queue panel may have changed layout
|
|
18829
19462
|
requestAnimationFrame(() => {
|
|
18830
19463
|
const s = state.sessions.get(id);
|
|
18831
|
-
if (s && s.fitAddon) s
|
|
19464
|
+
if (s && s.fitAddon) _fitTerminalPreservingViewport(s, id);
|
|
18832
19465
|
});
|
|
18833
19466
|
};
|
|
18834
19467
|
|