create-walle 0.9.13 → 0.9.15
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 +8 -3
- package/bin/create-walle.js +232 -32
- package/bin/mcp-inject.js +18 -53
- package/package.json +3 -1
- package/template/claude-task-manager/api-prompts.js +11 -2
- package/template/claude-task-manager/approval-agent.js +7 -0
- package/template/claude-task-manager/db.js +94 -75
- package/template/claude-task-manager/docs/session-standup-command-center-design.md +242 -0
- package/template/claude-task-manager/docs/session-tooltip-freshness-design.md +224 -0
- package/template/claude-task-manager/docs/session-ux-issue-review-2026-05-01.md +369 -0
- package/template/claude-task-manager/fuzzy-utils.js +10 -2
- package/template/claude-task-manager/git-utils.js +140 -10
- package/template/claude-task-manager/lib/agent-capabilities.js +1 -1
- package/template/claude-task-manager/lib/agent-presets.js +38 -5
- package/template/claude-task-manager/lib/codex-terminal-final.js +53 -0
- package/template/claude-task-manager/lib/ctm-session-context-api.js +222 -0
- package/template/claude-task-manager/lib/session-diagnostics.js +56 -0
- package/template/claude-task-manager/lib/session-history.js +309 -16
- package/template/claude-task-manager/lib/session-standup.js +409 -0
- package/template/claude-task-manager/lib/session-stream.js +253 -20
- package/template/claude-task-manager/lib/standup-attention.js +200 -0
- package/template/claude-task-manager/lib/status-hooks.js +8 -2
- package/template/claude-task-manager/lib/update-telemetry.js +114 -0
- package/template/claude-task-manager/lib/walle-ctm-history.js +49 -6
- package/template/claude-task-manager/lib/walle-default-model.js +55 -0
- package/template/claude-task-manager/lib/walle-mcp-auto-config.js +66 -0
- package/template/claude-task-manager/lib/walle-supervisor.js +86 -19
- package/template/claude-task-manager/lib/walle-transcript.js +1 -3
- package/template/claude-task-manager/lib/worktree-cwd.js +82 -0
- package/template/claude-task-manager/package.json +1 -0
- package/template/claude-task-manager/providers/codex-mcp.js +104 -0
- package/template/claude-task-manager/providers/index.js +2 -0
- package/template/claude-task-manager/public/css/setup.css +2 -1
- package/template/claude-task-manager/public/css/walle.css +71 -0
- package/template/claude-task-manager/public/index.html +2388 -429
- package/template/claude-task-manager/public/js/message-renderer.js +314 -35
- package/template/claude-task-manager/public/js/session-search-utils.js +185 -3
- package/template/claude-task-manager/public/js/session-status-precedence.js +125 -0
- package/template/claude-task-manager/public/js/setup.js +62 -19
- package/template/claude-task-manager/public/js/stream-view.js +396 -55
- package/template/claude-task-manager/public/js/terminal-restore-state.js +57 -0
- package/template/claude-task-manager/public/js/walle-session.js +234 -26
- package/template/claude-task-manager/public/js/walle.js +143 -2
- package/template/claude-task-manager/server.js +1402 -433
- package/template/claude-task-manager/session-integrity.js +77 -28
- package/template/claude-task-manager/workers/approval-widget-validator.js +15 -5
- package/template/claude-task-manager/workers/scrollback-worker.js +5 -6
- package/template/claude-task-manager/workers/state-detectors/codex.js +6 -0
- package/template/package.json +1 -1
- package/template/wall-e/agent-runners/claude-code.js +2 -0
- package/template/wall-e/agent.js +63 -8
- package/template/wall-e/api-walle.js +330 -52
- package/template/wall-e/brain.js +291 -42
- package/template/wall-e/chat.js +172 -15
- package/template/wall-e/coding/compaction-service.js +19 -5
- package/template/wall-e/coding/stream-processor.js +22 -2
- package/template/wall-e/coding/workspace-replay.js +1 -4
- package/template/wall-e/coding-orchestrator.js +250 -80
- package/template/wall-e/compat.js +0 -28
- package/template/wall-e/context/context-builder.js +3 -1
- package/template/wall-e/embeddings.js +2 -7
- package/template/wall-e/eval/agent-runner.js +30 -9
- package/template/wall-e/eval/benchmark-generator.js +21 -1
- package/template/wall-e/eval/benchmarks/chat-eval.json +66 -6
- package/template/wall-e/eval/benchmarks/coding-agent.json +0 -596
- package/template/wall-e/eval/cc-replay.js +1 -0
- package/template/wall-e/eval/codex-cli-baseline.js +633 -0
- package/template/wall-e/eval/debug-agent003.js +1 -0
- package/template/wall-e/eval/eval-orchestrator.js +3 -3
- package/template/wall-e/eval/run-agent-benchmarks.js +11 -3
- package/template/wall-e/eval/run-codex-cli-baseline.js +177 -0
- package/template/wall-e/eval/run-model-comparison.js +1 -0
- package/template/wall-e/eval/swebench-adapter.js +1 -0
- package/template/wall-e/evaluation/quorum-evaluator.js +0 -1
- package/template/wall-e/extraction/knowledge-extractor.js +1 -2
- package/template/wall-e/lib/mcp-integration.js +336 -0
- package/template/wall-e/llm/ollama.js +47 -8
- package/template/wall-e/llm/ollama.plugin.json +1 -1
- package/template/wall-e/llm/tool-adapter.js +1 -0
- package/template/wall-e/loops/ingest.js +42 -8
- package/template/wall-e/loops/initiative.js +87 -2
- package/template/wall-e/mcp-server.js +872 -19
- package/template/wall-e/memory/ctm-context-client.js +230 -0
- package/template/wall-e/memory/ctm-session-context.js +1376 -0
- package/template/wall-e/prompts/coding/memory-protocol.md +6 -0
- package/template/wall-e/server.js +30 -1
- package/template/wall-e/skills/_bundled/memory-search/SKILL.md +8 -0
- package/template/wall-e/skills/_bundled/scan-ctm-sessions/SKILL.md +20 -0
- package/template/wall-e/skills/_bundled/scan-ctm-sessions/run.js +43 -0
- package/template/wall-e/skills/_bundled/slack-mentions/run.js +471 -188
- package/template/wall-e/skills/skill-planner.js +86 -4
- package/template/wall-e/slack/socket-mode-listener.js +276 -0
- package/template/wall-e/telemetry.js +70 -2
- package/template/wall-e/tools/builtin-middleware.js +55 -2
- package/template/wall-e/tools/shell-policy.js +1 -1
- package/template/wall-e/tools/slack-owner.js +104 -0
- package/template/website/index.html +4 -4
- package/template/builder-journal.md +0 -17
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
--green: #9ece6a;
|
|
31
31
|
--red: #f7768e;
|
|
32
32
|
--yellow: #e0af68;
|
|
33
|
+
--purple: #bb9af7;
|
|
33
34
|
--border: #3b4261;
|
|
34
35
|
--tab-height: 38px;
|
|
35
36
|
--sidebar-width: 260px;
|
|
@@ -71,6 +72,36 @@
|
|
|
71
72
|
white-space: nowrap;
|
|
72
73
|
margin-right: 4px;
|
|
73
74
|
}
|
|
75
|
+
#topbar .app-version {
|
|
76
|
+
color: var(--fg-dim);
|
|
77
|
+
font-size: 11px;
|
|
78
|
+
font-weight: 600;
|
|
79
|
+
letter-spacing: 0;
|
|
80
|
+
line-height: 1;
|
|
81
|
+
white-space: nowrap;
|
|
82
|
+
border: 1px solid rgba(122, 162, 247, 0.24);
|
|
83
|
+
border-radius: 5px;
|
|
84
|
+
padding: 3px 6px;
|
|
85
|
+
background: rgba(122, 162, 247, 0.08);
|
|
86
|
+
}
|
|
87
|
+
#topbar .app-version.update-available {
|
|
88
|
+
color: var(--yellow);
|
|
89
|
+
border-color: rgba(224, 175, 104, 0.45);
|
|
90
|
+
background: rgba(224, 175, 104, 0.10);
|
|
91
|
+
}
|
|
92
|
+
.setup-version-pill {
|
|
93
|
+
display: inline-flex;
|
|
94
|
+
align-items: center;
|
|
95
|
+
gap: 4px;
|
|
96
|
+
color: var(--fg-dim);
|
|
97
|
+
font-size: 12px;
|
|
98
|
+
font-weight: 600;
|
|
99
|
+
border: 1px solid var(--border);
|
|
100
|
+
border-radius: 6px;
|
|
101
|
+
padding: 3px 8px;
|
|
102
|
+
background: rgba(122, 162, 247, 0.08);
|
|
103
|
+
white-space: nowrap;
|
|
104
|
+
}
|
|
74
105
|
.sidebar-toggle {
|
|
75
106
|
background: none;
|
|
76
107
|
border: none;
|
|
@@ -266,6 +297,7 @@
|
|
|
266
297
|
.session-item[data-agent="walle"] { border-left-color: #f59e0b; }
|
|
267
298
|
.session-item[data-agent="codex"] { border-left-color: #22c55e; }
|
|
268
299
|
.session-item[data-agent="gemini"] { border-left-color: #3b82f6; }
|
|
300
|
+
.session-item[data-agent="opencode"] { border-left-color: #a78bfa; }
|
|
269
301
|
.session-item[data-agent="shell"] { border-left-color: #6b7280; }
|
|
270
302
|
.session-item.active[data-agent] { border-left-color: rgba(26,27,38,0.3); }
|
|
271
303
|
/* Provider icon: monochrome SVG; color comes from currentColor.
|
|
@@ -282,6 +314,7 @@
|
|
|
282
314
|
.session-item[data-agent="walle"] .provider-icon { color: #f59e0b; }
|
|
283
315
|
.session-item[data-agent="codex"] .provider-icon { color: #22c55e; }
|
|
284
316
|
.session-item[data-agent="gemini"] .provider-icon { color: #3b82f6; }
|
|
317
|
+
.session-item[data-agent="opencode"] .provider-icon { color: #a78bfa; }
|
|
285
318
|
.session-item[data-agent="shell"] .provider-icon { color: #9ca3af; }
|
|
286
319
|
.session-item.active .provider-icon { color: #1a1b26; opacity: 0.85; }
|
|
287
320
|
/* Tab header icon — inherits the same per-agent color. */
|
|
@@ -290,6 +323,7 @@
|
|
|
290
323
|
.tab[data-agent="walle"] .provider-icon { color: #f59e0b; }
|
|
291
324
|
.tab[data-agent="codex"] .provider-icon { color: #22c55e; }
|
|
292
325
|
.tab[data-agent="gemini"] .provider-icon { color: #3b82f6; }
|
|
326
|
+
.tab[data-agent="opencode"] .provider-icon { color: #a78bfa; }
|
|
293
327
|
.tab[data-agent="shell"] .provider-icon { color: #9ca3af; }
|
|
294
328
|
.tab.active .provider-icon { color: var(--fg); opacity: 0.95; }
|
|
295
329
|
.session-item .dot {
|
|
@@ -439,6 +473,9 @@
|
|
|
439
473
|
}
|
|
440
474
|
.session-item.has-worktree-attn .branch-badge { max-width: 60px; }
|
|
441
475
|
.session-item.active .branch-badge { color: #0ea5e9; background: rgba(14, 165, 233, 0.15); }
|
|
476
|
+
.branch-badge.namespaced {
|
|
477
|
+
border: 1px solid rgba(125, 211, 252, 0.18);
|
|
478
|
+
}
|
|
442
479
|
.worktree-attn-badge {
|
|
443
480
|
display: inline-flex;
|
|
444
481
|
align-items: center;
|
|
@@ -742,15 +779,17 @@
|
|
|
742
779
|
.review-actions .action-btn.pinned-active { color: var(--yellow); border-color: var(--yellow); }
|
|
743
780
|
.review-actions .action-btn.active-mode { color: var(--accent); border-color: var(--accent); background: rgba(122,162,247,0.12); }
|
|
744
781
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
782
|
+
/* Truncate mode */
|
|
783
|
+
#review-messages.truncate-mode .prompt-turn:not(.setup-turn) .prompt-turn-header,
|
|
784
|
+
#review-messages.truncate-mode .review-msg.user .msg-header,
|
|
785
|
+
#review-messages.truncate-mode .review-msg.user.key-msg .msg-header {
|
|
786
|
+
position: relative;
|
|
787
|
+
cursor: pointer;
|
|
788
|
+
}
|
|
789
|
+
#review-messages.truncate-mode .prompt-turn:not(.setup-turn) .prompt-turn-header::after,
|
|
790
|
+
#review-messages.truncate-mode .review-msg.user .msg-header::after {
|
|
791
|
+
content: '✂ Cut from here';
|
|
792
|
+
position: absolute;
|
|
754
793
|
right: 0;
|
|
755
794
|
top: 50%;
|
|
756
795
|
transform: translateY(-50%);
|
|
@@ -764,13 +803,16 @@
|
|
|
764
803
|
transition: opacity 0.15s;
|
|
765
804
|
pointer-events: none;
|
|
766
805
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
806
|
+
#review-messages.truncate-mode .review-msg.user .msg-header:hover::after {
|
|
807
|
+
opacity: 1;
|
|
808
|
+
}
|
|
809
|
+
#review-messages.truncate-mode .prompt-turn:not(.setup-turn) .prompt-turn-header:hover::after {
|
|
810
|
+
opacity: 1;
|
|
811
|
+
}
|
|
812
|
+
#review-messages.truncate-mode .prompt-turn:not(.setup-turn) .prompt-turn-header:hover,
|
|
813
|
+
#review-messages.truncate-mode .review-msg.user .msg-header:hover {
|
|
814
|
+
background: rgba(122,162,247,0.08);
|
|
815
|
+
}
|
|
774
816
|
/* Marked-for-deletion state */
|
|
775
817
|
#review-messages .review-msg.truncate-remove {
|
|
776
818
|
opacity: 0.3;
|
|
@@ -949,12 +991,115 @@
|
|
|
949
991
|
scroll-behavior: smooth;
|
|
950
992
|
}
|
|
951
993
|
#review-messages::-webkit-scrollbar { width: 6px; }
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
994
|
+
#review-messages::-webkit-scrollbar-track { background: transparent; }
|
|
995
|
+
#review-messages::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
996
|
+
#review-messages::-webkit-scrollbar-thumb:hover { background: var(--fg-dim); }
|
|
997
|
+
|
|
998
|
+
.conversation-view > .conversation-load-older,
|
|
999
|
+
.conversation-view > .prompt-turn,
|
|
1000
|
+
.conversation-view > .review-msg,
|
|
1001
|
+
.conversation-view > .thought-group,
|
|
1002
|
+
.conversation-view > [data-conversation-state] {
|
|
1003
|
+
flex: 0 0 auto;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
.prompt-turn {
|
|
1007
|
+
margin-bottom: 14px;
|
|
1008
|
+
border: 1px solid rgba(65, 72, 104, 0.75);
|
|
1009
|
+
border-left: 3px solid var(--accent);
|
|
1010
|
+
border-radius: 8px;
|
|
1011
|
+
background: rgba(36, 40, 59, 0.36);
|
|
1012
|
+
overflow: hidden;
|
|
1013
|
+
}
|
|
1014
|
+
.prompt-turn.setup-turn {
|
|
1015
|
+
border-left-color: var(--purple, #bb9af7);
|
|
1016
|
+
background: rgba(187, 154, 247, 0.035);
|
|
1017
|
+
}
|
|
1018
|
+
.prompt-turn-header {
|
|
1019
|
+
display: grid;
|
|
1020
|
+
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
1021
|
+
align-items: start;
|
|
1022
|
+
gap: 8px;
|
|
1023
|
+
padding: 8px 10px 8px 8px;
|
|
1024
|
+
cursor: pointer;
|
|
1025
|
+
user-select: none;
|
|
1026
|
+
}
|
|
1027
|
+
.prompt-turn-header:hover { background: rgba(255,255,255,0.025); }
|
|
1028
|
+
.prompt-turn-header:focus-visible {
|
|
1029
|
+
outline: 2px solid var(--accent);
|
|
1030
|
+
outline-offset: -2px;
|
|
1031
|
+
}
|
|
1032
|
+
.prompt-turn-chevron {
|
|
1033
|
+
display: inline-block;
|
|
1034
|
+
margin-top: 17px;
|
|
1035
|
+
color: var(--fg-dim);
|
|
1036
|
+
font-size: 10px;
|
|
1037
|
+
transition: transform 0.15s;
|
|
1038
|
+
}
|
|
1039
|
+
.prompt-turn.expanded .prompt-turn-chevron { transform: rotate(90deg); }
|
|
1040
|
+
.prompt-turn-head-main { min-width: 0; }
|
|
1041
|
+
.prompt-turn-prompt.review-msg {
|
|
1042
|
+
margin: 0;
|
|
1043
|
+
padding: 8px 10px;
|
|
1044
|
+
border-left-width: 2px;
|
|
1045
|
+
border-radius: 6px;
|
|
1046
|
+
background: rgba(122, 162, 247, 0.07);
|
|
1047
|
+
}
|
|
1048
|
+
.prompt-turn-prompt .msg-header { margin-bottom: 5px; }
|
|
1049
|
+
.prompt-turn-meta {
|
|
1050
|
+
display: flex;
|
|
1051
|
+
flex-wrap: wrap;
|
|
1052
|
+
justify-content: flex-end;
|
|
1053
|
+
gap: 4px;
|
|
1054
|
+
min-width: 118px;
|
|
1055
|
+
padding-top: 5px;
|
|
1056
|
+
}
|
|
1057
|
+
.prompt-turn-badge {
|
|
1058
|
+
display: inline-flex;
|
|
1059
|
+
align-items: center;
|
|
1060
|
+
min-height: 18px;
|
|
1061
|
+
padding: 1px 7px;
|
|
1062
|
+
border: 1px solid var(--border);
|
|
1063
|
+
border-radius: 999px;
|
|
1064
|
+
color: var(--fg-dim);
|
|
1065
|
+
background: rgba(0, 0, 0, 0.10);
|
|
1066
|
+
font-size: 10.5px;
|
|
1067
|
+
white-space: nowrap;
|
|
1068
|
+
}
|
|
1069
|
+
.prompt-turn-alert.warning {
|
|
1070
|
+
color: var(--yellow);
|
|
1071
|
+
border-color: rgba(224, 175, 104, 0.48);
|
|
1072
|
+
background: rgba(224, 175, 104, 0.08);
|
|
1073
|
+
}
|
|
1074
|
+
.prompt-turn-alert.error {
|
|
1075
|
+
color: var(--red);
|
|
1076
|
+
border-color: rgba(247, 118, 118, 0.55);
|
|
1077
|
+
background: rgba(247, 118, 118, 0.08);
|
|
1078
|
+
}
|
|
1079
|
+
.prompt-turn-response {
|
|
1080
|
+
display: none;
|
|
1081
|
+
padding: 0 12px 12px 29px;
|
|
1082
|
+
border-top: 1px solid rgba(65, 72, 104, 0.45);
|
|
1083
|
+
}
|
|
1084
|
+
.prompt-turn.expanded .prompt-turn-response { display: block; }
|
|
1085
|
+
.prompt-turn-response > .review-msg,
|
|
1086
|
+
.prompt-turn-response > .thought-group {
|
|
1087
|
+
margin-top: 8px;
|
|
1088
|
+
margin-bottom: 0;
|
|
1089
|
+
}
|
|
1090
|
+
.prompt-turn-empty,
|
|
1091
|
+
.prompt-turn-setup-title {
|
|
1092
|
+
color: var(--fg-dim);
|
|
1093
|
+
font-size: 12px;
|
|
1094
|
+
padding: 8px 10px;
|
|
1095
|
+
}
|
|
1096
|
+
.prompt-turn-setup-title {
|
|
1097
|
+
color: var(--purple, #bb9af7);
|
|
1098
|
+
font-weight: 600;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/* Scroll to bottom button */
|
|
1102
|
+
.scroll-bottom-btn {
|
|
958
1103
|
display: none;
|
|
959
1104
|
position: absolute;
|
|
960
1105
|
bottom: 20px;
|
|
@@ -1008,11 +1153,17 @@
|
|
|
1008
1153
|
.prompt-nav-list-item.current { color: var(--accent); font-weight: 600; }
|
|
1009
1154
|
.prompt-nav-list-item.not-in-buffer { color: var(--fg-dim); cursor: default; }
|
|
1010
1155
|
.prompt-nav-list-item.not-in-buffer:hover { background: rgba(255,255,255,0.03); }
|
|
1156
|
+
.prompt-nav-list-item.latest-unmapped { color: var(--fg); cursor: pointer; }
|
|
1011
1157
|
.prompt-nav-badge-unreachable {
|
|
1012
1158
|
display: inline-block; font-size: 9px; font-weight: 600; padding: 1px 5px; border-radius: 3px;
|
|
1013
1159
|
background: rgba(224,175,104,0.12); color: #e0af68; vertical-align: baseline;
|
|
1014
1160
|
letter-spacing: 0.03em; margin-right: 4px; flex-shrink: 0;
|
|
1015
1161
|
}
|
|
1162
|
+
.prompt-nav-badge-latest {
|
|
1163
|
+
display: inline-block; font-size: 9px; font-weight: 600; padding: 1px 5px; border-radius: 3px;
|
|
1164
|
+
background: rgba(122,162,247,0.14); color: var(--accent); vertical-align: baseline;
|
|
1165
|
+
letter-spacing: 0.03em; margin-right: 4px; flex-shrink: 0;
|
|
1166
|
+
}
|
|
1016
1167
|
.prompt-nav-list-item.not-in-buffer .prompt-text { color: var(--fg-dim); }
|
|
1017
1168
|
|
|
1018
1169
|
.review-msg {
|
|
@@ -1589,10 +1740,28 @@
|
|
|
1589
1740
|
transition: all 0.1s;
|
|
1590
1741
|
}
|
|
1591
1742
|
.tab .tab-label {
|
|
1743
|
+
flex: 1 1 auto;
|
|
1592
1744
|
overflow: hidden;
|
|
1593
1745
|
text-overflow: ellipsis;
|
|
1594
1746
|
min-width: 0;
|
|
1595
1747
|
}
|
|
1748
|
+
.tab > .branch-badge { max-width: 64px; }
|
|
1749
|
+
.tab.tab-title-clipped > .branch-badge { display: none; }
|
|
1750
|
+
.tab.pinned-tab {
|
|
1751
|
+
flex: 0 0 auto;
|
|
1752
|
+
min-width: 86px;
|
|
1753
|
+
}
|
|
1754
|
+
.tab.pinned-tab::before {
|
|
1755
|
+
content: '';
|
|
1756
|
+
display: inline-block;
|
|
1757
|
+
width: 6px;
|
|
1758
|
+
height: 6px;
|
|
1759
|
+
border-radius: 999px;
|
|
1760
|
+
background: var(--accent);
|
|
1761
|
+
opacity: 0.75;
|
|
1762
|
+
flex: 0 0 auto;
|
|
1763
|
+
}
|
|
1764
|
+
.tab.pinned-tab .tab-label { flex: 0 0 auto; }
|
|
1596
1765
|
.tab:hover { color: var(--fg); background: var(--bg-light); }
|
|
1597
1766
|
.tab.active {
|
|
1598
1767
|
color: var(--fg);
|
|
@@ -1624,6 +1793,7 @@
|
|
|
1624
1793
|
.tab[draggable="true"] { cursor: grab; }
|
|
1625
1794
|
.tab[draggable="true"]:active { cursor: grabbing; }
|
|
1626
1795
|
.tab.tab-drag-over { border-left: 2px solid var(--accent); }
|
|
1796
|
+
.tab.tab-drop-after { border-right: 2px solid var(--accent); }
|
|
1627
1797
|
.tab .tab-icon {
|
|
1628
1798
|
color: var(--green, #9ece6a);
|
|
1629
1799
|
font-size: 10px;
|
|
@@ -1761,6 +1931,49 @@
|
|
|
1761
1931
|
}
|
|
1762
1932
|
.term-container.active { display: flex; flex-direction: column; }
|
|
1763
1933
|
.term-container .xterm { flex: 1; height: 0; min-height: 0; overflow: hidden; }
|
|
1934
|
+
.codex-final-panel {
|
|
1935
|
+
display: none;
|
|
1936
|
+
flex: 0 0 auto;
|
|
1937
|
+
max-height: 34vh;
|
|
1938
|
+
margin: 8px 12px 0;
|
|
1939
|
+
border: 1px solid rgba(126, 203, 255, 0.28);
|
|
1940
|
+
border-left: 3px solid var(--accent, #7aa2f7);
|
|
1941
|
+
border-radius: 6px;
|
|
1942
|
+
background: rgba(17, 19, 31, 0.96);
|
|
1943
|
+
overflow: hidden;
|
|
1944
|
+
color: var(--fg, #c0caf5);
|
|
1945
|
+
}
|
|
1946
|
+
.codex-final-panel.visible { display: flex; flex-direction: column; }
|
|
1947
|
+
.codex-final-header {
|
|
1948
|
+
display: flex; align-items: center; justify-content: space-between; gap: 12px;
|
|
1949
|
+
padding: 7px 10px;
|
|
1950
|
+
border-bottom: 1px solid rgba(126, 203, 255, 0.16);
|
|
1951
|
+
color: var(--accent, #7aa2f7);
|
|
1952
|
+
font-size: 11px;
|
|
1953
|
+
font-weight: 700;
|
|
1954
|
+
text-transform: uppercase;
|
|
1955
|
+
letter-spacing: 0;
|
|
1956
|
+
}
|
|
1957
|
+
.codex-final-close {
|
|
1958
|
+
border: 0;
|
|
1959
|
+
background: transparent;
|
|
1960
|
+
color: var(--fg-dim, #565f89);
|
|
1961
|
+
cursor: pointer;
|
|
1962
|
+
font-size: 14px;
|
|
1963
|
+
line-height: 1;
|
|
1964
|
+
padding: 2px 4px;
|
|
1965
|
+
}
|
|
1966
|
+
.codex-final-close:hover { color: var(--fg, #c0caf5); }
|
|
1967
|
+
.codex-final-body {
|
|
1968
|
+
flex: 1 1 auto;
|
|
1969
|
+
min-height: 0;
|
|
1970
|
+
padding: 10px 12px 12px;
|
|
1971
|
+
overflow: auto;
|
|
1972
|
+
white-space: pre-wrap;
|
|
1973
|
+
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
|
|
1974
|
+
font-size: 12px;
|
|
1975
|
+
line-height: 1.55;
|
|
1976
|
+
}
|
|
1764
1977
|
/* Prevent macOS elastic overscroll from hijacking trackpad scroll inside terminal */
|
|
1765
1978
|
.term-container .xterm-viewport { overscroll-behavior: contain; }
|
|
1766
1979
|
/* Loading overlay for tab-switch snapshot restore */
|
|
@@ -2022,14 +2235,298 @@
|
|
|
2022
2235
|
#welcome {
|
|
2023
2236
|
display: flex;
|
|
2024
2237
|
flex-direction: column;
|
|
2025
|
-
align-items:
|
|
2026
|
-
justify-content:
|
|
2238
|
+
align-items: stretch;
|
|
2239
|
+
justify-content: flex-start;
|
|
2027
2240
|
flex: 1;
|
|
2028
|
-
gap:
|
|
2241
|
+
gap: 12px;
|
|
2242
|
+
overflow: auto;
|
|
2243
|
+
padding: 18px;
|
|
2029
2244
|
color: var(--fg-dim);
|
|
2030
2245
|
}
|
|
2246
|
+
#welcome h1,
|
|
2031
2247
|
#welcome h2 { color: var(--fg); font-size: 18px; font-weight: 600; }
|
|
2032
2248
|
#welcome p { font-size: 13px; max-width: 400px; text-align: center; line-height: 1.6; }
|
|
2249
|
+
.standup-dashboard {
|
|
2250
|
+
width: 100%;
|
|
2251
|
+
max-width: 1280px;
|
|
2252
|
+
margin: 0 auto;
|
|
2253
|
+
display: flex;
|
|
2254
|
+
flex-direction: column;
|
|
2255
|
+
gap: 12px;
|
|
2256
|
+
min-height: 0;
|
|
2257
|
+
}
|
|
2258
|
+
.standup-header {
|
|
2259
|
+
display: flex;
|
|
2260
|
+
align-items: center;
|
|
2261
|
+
justify-content: space-between;
|
|
2262
|
+
gap: 18px;
|
|
2263
|
+
border: 1px solid var(--border);
|
|
2264
|
+
background: rgba(122,162,247,0.045);
|
|
2265
|
+
border-radius: 8px;
|
|
2266
|
+
padding: 18px 20px;
|
|
2267
|
+
}
|
|
2268
|
+
.standup-heading {
|
|
2269
|
+
min-width: 0;
|
|
2270
|
+
display: flex;
|
|
2271
|
+
align-items: center;
|
|
2272
|
+
}
|
|
2273
|
+
#welcome .standup-title {
|
|
2274
|
+
margin: 0;
|
|
2275
|
+
font-size: 30px;
|
|
2276
|
+
line-height: 1.05;
|
|
2277
|
+
font-weight: 750;
|
|
2278
|
+
letter-spacing: 0;
|
|
2279
|
+
color: var(--fg);
|
|
2280
|
+
}
|
|
2281
|
+
.standup-meta {
|
|
2282
|
+
display: flex;
|
|
2283
|
+
flex-wrap: wrap;
|
|
2284
|
+
align-items: center;
|
|
2285
|
+
gap: 8px;
|
|
2286
|
+
justify-content: flex-end;
|
|
2287
|
+
}
|
|
2288
|
+
.standup-counts {
|
|
2289
|
+
display: flex;
|
|
2290
|
+
flex-wrap: wrap;
|
|
2291
|
+
gap: 6px;
|
|
2292
|
+
}
|
|
2293
|
+
.standup-count {
|
|
2294
|
+
border: 1px solid var(--border);
|
|
2295
|
+
border-radius: 6px;
|
|
2296
|
+
padding: 5px 7px;
|
|
2297
|
+
background: var(--bg-light);
|
|
2298
|
+
color: var(--fg);
|
|
2299
|
+
font-size: 11px;
|
|
2300
|
+
white-space: nowrap;
|
|
2301
|
+
}
|
|
2302
|
+
.standup-count strong {
|
|
2303
|
+
font-size: 13px;
|
|
2304
|
+
margin-right: 4px;
|
|
2305
|
+
}
|
|
2306
|
+
.standup-updated {
|
|
2307
|
+
font-size: 11px;
|
|
2308
|
+
color: var(--fg-dim);
|
|
2309
|
+
white-space: nowrap;
|
|
2310
|
+
}
|
|
2311
|
+
.standup-actions {
|
|
2312
|
+
display: flex;
|
|
2313
|
+
gap: 6px;
|
|
2314
|
+
flex-wrap: wrap;
|
|
2315
|
+
justify-content: flex-end;
|
|
2316
|
+
}
|
|
2317
|
+
.standup-action-btn {
|
|
2318
|
+
background: var(--bg-light);
|
|
2319
|
+
color: var(--fg);
|
|
2320
|
+
border: 1px solid var(--border);
|
|
2321
|
+
border-radius: 6px;
|
|
2322
|
+
padding: 6px 9px;
|
|
2323
|
+
font-size: 12px;
|
|
2324
|
+
cursor: pointer;
|
|
2325
|
+
min-height: 30px;
|
|
2326
|
+
}
|
|
2327
|
+
.standup-action-btn:hover {
|
|
2328
|
+
border-color: var(--accent);
|
|
2329
|
+
color: var(--accent);
|
|
2330
|
+
}
|
|
2331
|
+
.standup-action-btn.primary {
|
|
2332
|
+
background: var(--accent);
|
|
2333
|
+
border-color: var(--accent);
|
|
2334
|
+
color: var(--bg);
|
|
2335
|
+
font-weight: 700;
|
|
2336
|
+
}
|
|
2337
|
+
.standup-attention {
|
|
2338
|
+
display: none;
|
|
2339
|
+
border: 1px solid rgba(224, 175, 104, 0.35);
|
|
2340
|
+
background: rgba(224, 175, 104, 0.08);
|
|
2341
|
+
border-radius: 8px;
|
|
2342
|
+
padding: 10px 12px;
|
|
2343
|
+
color: var(--fg);
|
|
2344
|
+
gap: 10px;
|
|
2345
|
+
align-items: center;
|
|
2346
|
+
justify-content: space-between;
|
|
2347
|
+
}
|
|
2348
|
+
.standup-attention.active { display: flex; }
|
|
2349
|
+
.standup-attention-main {
|
|
2350
|
+
min-width: 0;
|
|
2351
|
+
display: flex;
|
|
2352
|
+
flex-direction: column;
|
|
2353
|
+
gap: 3px;
|
|
2354
|
+
}
|
|
2355
|
+
.standup-attention-title {
|
|
2356
|
+
font-size: 13px;
|
|
2357
|
+
font-weight: 700;
|
|
2358
|
+
color: var(--yellow);
|
|
2359
|
+
}
|
|
2360
|
+
.standup-attention-body {
|
|
2361
|
+
font-size: 12px;
|
|
2362
|
+
color: var(--fg-dim);
|
|
2363
|
+
overflow-wrap: anywhere;
|
|
2364
|
+
}
|
|
2365
|
+
.standup-lanes {
|
|
2366
|
+
display: grid;
|
|
2367
|
+
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
|
2368
|
+
gap: 10px;
|
|
2369
|
+
align-items: start;
|
|
2370
|
+
}
|
|
2371
|
+
.standup-lane {
|
|
2372
|
+
border: 1px solid var(--border);
|
|
2373
|
+
border-radius: 8px;
|
|
2374
|
+
background: rgba(255,255,255,0.018);
|
|
2375
|
+
min-width: 0;
|
|
2376
|
+
overflow: hidden;
|
|
2377
|
+
}
|
|
2378
|
+
.standup-lane-header {
|
|
2379
|
+
padding: 10px 11px;
|
|
2380
|
+
display: flex;
|
|
2381
|
+
align-items: center;
|
|
2382
|
+
justify-content: space-between;
|
|
2383
|
+
gap: 8px;
|
|
2384
|
+
border-bottom: 1px solid var(--border);
|
|
2385
|
+
}
|
|
2386
|
+
.standup-lane-title {
|
|
2387
|
+
display: flex;
|
|
2388
|
+
align-items: center;
|
|
2389
|
+
gap: 7px;
|
|
2390
|
+
min-width: 0;
|
|
2391
|
+
color: var(--fg);
|
|
2392
|
+
font-weight: 700;
|
|
2393
|
+
font-size: 13px;
|
|
2394
|
+
}
|
|
2395
|
+
.standup-lane-dot {
|
|
2396
|
+
width: 7px;
|
|
2397
|
+
height: 7px;
|
|
2398
|
+
border-radius: 50%;
|
|
2399
|
+
flex-shrink: 0;
|
|
2400
|
+
background: var(--fg-dim);
|
|
2401
|
+
}
|
|
2402
|
+
.standup-lane[data-lane="needs_user"] .standup-lane-dot { background: var(--yellow); }
|
|
2403
|
+
.standup-lane[data-lane="ready_review"] .standup-lane-dot { background: var(--green); }
|
|
2404
|
+
.standup-lane[data-lane="running"] .standup-lane-dot { background: var(--blue); }
|
|
2405
|
+
.standup-lane[data-lane="continue_later"] .standup-lane-dot { background: var(--purple); }
|
|
2406
|
+
.standup-lane-count {
|
|
2407
|
+
color: var(--fg-dim);
|
|
2408
|
+
font-size: 11px;
|
|
2409
|
+
border: 1px solid var(--border);
|
|
2410
|
+
border-radius: 999px;
|
|
2411
|
+
padding: 2px 7px;
|
|
2412
|
+
background: var(--bg);
|
|
2413
|
+
}
|
|
2414
|
+
.standup-lane-body {
|
|
2415
|
+
padding: 8px;
|
|
2416
|
+
display: flex;
|
|
2417
|
+
flex-direction: column;
|
|
2418
|
+
gap: 8px;
|
|
2419
|
+
}
|
|
2420
|
+
.standup-card {
|
|
2421
|
+
border: 1px solid var(--border);
|
|
2422
|
+
border-radius: 8px;
|
|
2423
|
+
background: var(--bg-light);
|
|
2424
|
+
padding: 10px;
|
|
2425
|
+
display: flex;
|
|
2426
|
+
flex-direction: column;
|
|
2427
|
+
gap: 8px;
|
|
2428
|
+
min-width: 0;
|
|
2429
|
+
}
|
|
2430
|
+
.standup-card-top {
|
|
2431
|
+
display: flex;
|
|
2432
|
+
align-items: flex-start;
|
|
2433
|
+
justify-content: space-between;
|
|
2434
|
+
gap: 8px;
|
|
2435
|
+
min-width: 0;
|
|
2436
|
+
}
|
|
2437
|
+
.standup-card-title {
|
|
2438
|
+
min-width: 0;
|
|
2439
|
+
color: var(--fg);
|
|
2440
|
+
font-size: 13px;
|
|
2441
|
+
font-weight: 700;
|
|
2442
|
+
line-height: 1.25;
|
|
2443
|
+
overflow-wrap: anywhere;
|
|
2444
|
+
}
|
|
2445
|
+
.standup-card-subtitle {
|
|
2446
|
+
color: var(--fg-dim);
|
|
2447
|
+
font-size: 11px;
|
|
2448
|
+
margin-top: 3px;
|
|
2449
|
+
overflow: hidden;
|
|
2450
|
+
text-overflow: ellipsis;
|
|
2451
|
+
white-space: nowrap;
|
|
2452
|
+
}
|
|
2453
|
+
.standup-badge {
|
|
2454
|
+
flex-shrink: 0;
|
|
2455
|
+
border: 1px solid var(--border);
|
|
2456
|
+
border-radius: 999px;
|
|
2457
|
+
padding: 2px 7px;
|
|
2458
|
+
font-size: 10px;
|
|
2459
|
+
color: var(--fg-dim);
|
|
2460
|
+
background: var(--bg);
|
|
2461
|
+
white-space: nowrap;
|
|
2462
|
+
max-width: 110px;
|
|
2463
|
+
overflow: hidden;
|
|
2464
|
+
text-overflow: ellipsis;
|
|
2465
|
+
}
|
|
2466
|
+
.standup-badge.status-running { color: var(--blue); border-color: rgba(122,162,247,0.45); }
|
|
2467
|
+
.standup-badge.status-waiting,
|
|
2468
|
+
.standup-badge.status-waiting_input { color: var(--yellow); border-color: rgba(224,175,104,0.45); }
|
|
2469
|
+
.standup-badge.status-idle { color: var(--green); border-color: rgba(158,206,106,0.35); }
|
|
2470
|
+
.standup-card-text {
|
|
2471
|
+
font-size: 12px;
|
|
2472
|
+
line-height: 1.4;
|
|
2473
|
+
color: var(--fg-dim);
|
|
2474
|
+
overflow-wrap: anywhere;
|
|
2475
|
+
}
|
|
2476
|
+
.standup-card-text strong {
|
|
2477
|
+
color: var(--fg);
|
|
2478
|
+
font-weight: 600;
|
|
2479
|
+
}
|
|
2480
|
+
.standup-evidence {
|
|
2481
|
+
display: flex;
|
|
2482
|
+
flex-wrap: wrap;
|
|
2483
|
+
gap: 5px;
|
|
2484
|
+
}
|
|
2485
|
+
.standup-chip {
|
|
2486
|
+
font-size: 10px;
|
|
2487
|
+
color: var(--fg-dim);
|
|
2488
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
2489
|
+
border-radius: 999px;
|
|
2490
|
+
padding: 2px 6px;
|
|
2491
|
+
background: rgba(0,0,0,0.14);
|
|
2492
|
+
max-width: 100%;
|
|
2493
|
+
overflow: hidden;
|
|
2494
|
+
text-overflow: ellipsis;
|
|
2495
|
+
white-space: nowrap;
|
|
2496
|
+
}
|
|
2497
|
+
.standup-card-actions {
|
|
2498
|
+
display: flex;
|
|
2499
|
+
gap: 6px;
|
|
2500
|
+
flex-wrap: wrap;
|
|
2501
|
+
}
|
|
2502
|
+
.standup-card-actions .standup-action-btn {
|
|
2503
|
+
padding: 5px 8px;
|
|
2504
|
+
min-height: 28px;
|
|
2505
|
+
font-size: 11px;
|
|
2506
|
+
}
|
|
2507
|
+
.standup-empty,
|
|
2508
|
+
.standup-loading,
|
|
2509
|
+
.standup-error {
|
|
2510
|
+
border: 1px dashed var(--border);
|
|
2511
|
+
border-radius: 8px;
|
|
2512
|
+
padding: 20px;
|
|
2513
|
+
text-align: center;
|
|
2514
|
+
color: var(--fg-dim);
|
|
2515
|
+
font-size: 13px;
|
|
2516
|
+
background: rgba(255,255,255,0.018);
|
|
2517
|
+
}
|
|
2518
|
+
.standup-error { color: var(--red); border-color: rgba(247,118,142,0.45); }
|
|
2519
|
+
@media (max-width: 760px) {
|
|
2520
|
+
#welcome { padding: 12px; }
|
|
2521
|
+
.standup-header {
|
|
2522
|
+
flex-direction: column;
|
|
2523
|
+
align-items: stretch;
|
|
2524
|
+
padding: 16px;
|
|
2525
|
+
}
|
|
2526
|
+
#welcome .standup-title { font-size: 24px; }
|
|
2527
|
+
.standup-meta { justify-content: flex-start; }
|
|
2528
|
+
.standup-attention { align-items: flex-start; flex-direction: column; }
|
|
2529
|
+
}
|
|
2033
2530
|
.shortcut-grid {
|
|
2034
2531
|
display: grid;
|
|
2035
2532
|
grid-template-columns: auto auto;
|
|
@@ -2921,6 +3418,7 @@
|
|
|
2921
3418
|
</div>
|
|
2922
3419
|
</div>
|
|
2923
3420
|
<span class="logo">CTM</span>
|
|
3421
|
+
<span class="app-version" id="app-version-label" title="Installed CTM / Wall-E bundle version">v?</span>
|
|
2924
3422
|
</div>
|
|
2925
3423
|
<nav class="topbar-nav" id="topbar-nav">
|
|
2926
3424
|
<button class="nav-pill active" data-nav="sessions" onclick="navTo('sessions')" title="Terminal sessions">Sessions</button>
|
|
@@ -2961,16 +3459,16 @@
|
|
|
2961
3459
|
<div id="update-banner" style="display:none;background:linear-gradient(90deg,#1a1b2e,#1e2030);border-bottom:1px solid var(--border);padding:6px 16px;font-size:12px;color:var(--fg-dim,#a9b1d6);align-items:center;gap:10px;">
|
|
2962
3460
|
<span style="color:#bb9af7;">↑</span>
|
|
2963
3461
|
<span id="update-banner-msg">Update available</span>
|
|
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>
|
|
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>
|
|
3462
|
+
<button id="update-apply-btn" onclick="applyUpdate('banner')" style="background:#7aa2f7;color:#1a1b26;border:none;padding:3px 10px;border-radius:4px;font-size:11px;cursor:pointer;font-weight:600;">Update Now</button>
|
|
3463
|
+
<button onclick="dismissUpdate('banner')" style="background:none;border:none;color:var(--fg-dim);cursor:pointer;margin-left:auto;opacity:0.6;font-size:14px;">×</button>
|
|
2966
3464
|
</div>
|
|
2967
3465
|
<div class="modal-overlay update-wizard-overlay hidden" id="update-wizard" role="dialog" aria-modal="true" aria-labelledby="update-wizard-heading">
|
|
2968
3466
|
<div class="modal update-wizard-modal">
|
|
2969
3467
|
<div class="update-wizard-head">
|
|
2970
3468
|
<div class="update-wizard-icon">↑</div>
|
|
2971
3469
|
<div class="update-wizard-title">
|
|
2972
|
-
<h3 id="update-wizard-heading">Upgrade CTM?</h3>
|
|
2973
|
-
<p>A newer
|
|
3470
|
+
<h3 id="update-wizard-heading">Upgrade CTM / Wall-E?</h3>
|
|
3471
|
+
<p>A newer create-walle release is available.</p>
|
|
2974
3472
|
</div>
|
|
2975
3473
|
</div>
|
|
2976
3474
|
<div class="update-wizard-body">
|
|
@@ -2987,8 +3485,8 @@
|
|
|
2987
3485
|
<p class="update-wizard-note">The updater will run in the background and CTM will restart when the upgrade is ready.</p>
|
|
2988
3486
|
<div class="update-wizard-actions">
|
|
2989
3487
|
<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>
|
|
3488
|
+
<button class="btn" onclick="dismissUpdate('wizard')">Skip This Version</button>
|
|
3489
|
+
<button class="btn primary" id="update-wizard-apply-btn" onclick="applyUpdate('wizard')">Upgrade CTM</button>
|
|
2992
3490
|
</div>
|
|
2993
3491
|
</div>
|
|
2994
3492
|
</div>
|
|
@@ -3028,8 +3526,8 @@
|
|
|
3028
3526
|
<option value="name">A-Z</option>
|
|
3029
3527
|
<option value="messages">Most Used</option>
|
|
3030
3528
|
</select>
|
|
3031
|
-
<select id="model-filter" onchange="
|
|
3032
|
-
<option value="">All
|
|
3529
|
+
<select id="model-filter" onchange="setAgentFilter(this.value)" title="Filter by coding agent" style="background:var(--bg);color:var(--fg-dim);border:1px solid var(--border);padding:2px 4px;border-radius:3px;font-size:10px;max-width:150px;cursor:pointer;">
|
|
3530
|
+
<option value="">All Agents</option>
|
|
3033
3531
|
</select>
|
|
3034
3532
|
</div>
|
|
3035
3533
|
<div class="display-toggle">
|
|
@@ -3068,21 +3566,26 @@
|
|
|
3068
3566
|
</div>
|
|
3069
3567
|
<div id="terminal-area">
|
|
3070
3568
|
<div id="welcome">
|
|
3071
|
-
<
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
<div
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3569
|
+
<div class="standup-dashboard" id="standup-dashboard" onclick="standupHandleDashboardClick(event)">
|
|
3570
|
+
<div class="standup-header">
|
|
3571
|
+
<div class="standup-heading">
|
|
3572
|
+
<h1 class="standup-title">Session Overview</h1>
|
|
3573
|
+
</div>
|
|
3574
|
+
<div class="standup-meta">
|
|
3575
|
+
<div class="standup-counts" id="standup-counts"></div>
|
|
3576
|
+
<span class="standup-updated" id="standup-updated"></span>
|
|
3577
|
+
<div class="standup-actions">
|
|
3578
|
+
<button class="standup-action-btn" type="button" data-standup-action="refresh">Refresh</button>
|
|
3579
|
+
<button class="standup-action-btn primary" type="button" onclick="showNewSessionModal()">New Session</button>
|
|
3580
|
+
</div>
|
|
3581
|
+
</div>
|
|
3082
3582
|
</div>
|
|
3083
|
-
<div
|
|
3084
|
-
|
|
3085
|
-
|
|
3583
|
+
<div class="standup-attention" id="standup-attention"></div>
|
|
3584
|
+
<div class="standup-loading" id="standup-loading">Loading sessions...</div>
|
|
3585
|
+
<div class="standup-error" id="standup-error" style="display:none;"></div>
|
|
3586
|
+
<div class="standup-lanes" id="standup-lanes"></div>
|
|
3587
|
+
<div class="standup-empty" id="standup-empty" style="display:none;">
|
|
3588
|
+
No active sessions.
|
|
3086
3589
|
</div>
|
|
3087
3590
|
</div>
|
|
3088
3591
|
</div>
|
|
@@ -3212,7 +3715,10 @@
|
|
|
3212
3715
|
<div id="setup-panel">
|
|
3213
3716
|
<div class="setup-header">
|
|
3214
3717
|
<div style="display:flex;justify-content:space-between;align-items:baseline;">
|
|
3215
|
-
<
|
|
3718
|
+
<div style="display:flex;align-items:center;gap:8px;min-width:0;">
|
|
3719
|
+
<h2>Wall-E Setup</h2>
|
|
3720
|
+
<span class="setup-version-pill" id="setup-version-label" title="Installed CTM / Wall-E bundle version">Version loading...</span>
|
|
3721
|
+
</div>
|
|
3216
3722
|
<div style="display:flex;align-items:center;gap:6px;">
|
|
3217
3723
|
<span class="status-dot ok" id="setup-owner-dot"></span>
|
|
3218
3724
|
<input type="text" id="setup-owner-name" placeholder="Your Name" style="background:transparent;border:none;border-bottom:1px solid var(--border);color:var(--fg);font-size:14px;padding:2px 0;width:120px;outline:none;text-align:right;" title="Owner name">
|
|
@@ -3593,13 +4099,13 @@
|
|
|
3593
4099
|
<div id="setup-mcp-integrations"><div style="color:var(--fg-dim);font-size:12px;padding:8px 0;">Loading…</div></div>
|
|
3594
4100
|
<div class="btn-row" style="margin-top:8px;">
|
|
3595
4101
|
<button class="setup-btn setup-btn-secondary" id="setup-mcp-test-btn" onclick="SETUP.testMcpConnection()" disabled>Test Connection</button>
|
|
3596
|
-
<button class="setup-btn setup-btn-secondary" id="setup-mcp-fix-btn" onclick="SETUP.fixMcpConfigs()" style="display:none">
|
|
4102
|
+
<button class="setup-btn setup-btn-secondary" id="setup-mcp-fix-btn" onclick="SETUP.fixMcpConfigs()" style="display:none">Repair Configs</button>
|
|
3597
4103
|
<span class="test-result" id="setup-mcp-test-result"></span>
|
|
3598
4104
|
</div>
|
|
3599
4105
|
<details style="margin-top:14px;">
|
|
3600
4106
|
<summary style="cursor:pointer;font-size:12px;color:var(--fg-dim);">Manual setup instructions</summary>
|
|
3601
4107
|
<div style="margin-top:8px;font-size:12px;color:var(--fg-dim);line-height:1.6;">
|
|
3602
|
-
Add this to
|
|
4108
|
+
Add this to JSON-based MCP configs:
|
|
3603
4109
|
<pre style="background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:10px 12px;margin-top:6px;font-size:11px;color:var(--fg);overflow-x:auto;"><code>{
|
|
3604
4110
|
"mcpServers": {
|
|
3605
4111
|
"wall-e": {
|
|
@@ -3608,9 +4114,14 @@
|
|
|
3608
4114
|
}
|
|
3609
4115
|
}
|
|
3610
4116
|
}</code></pre>
|
|
4117
|
+
<div style="margin-top:8px;">For Codex, add this to <code style="font-size:11px;">~/.codex/config.toml</code>:</div>
|
|
4118
|
+
<pre style="background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:10px 12px;margin-top:6px;font-size:11px;color:var(--fg);overflow-x:auto;"><code>[mcp_servers."wall-e"]
|
|
4119
|
+
url = "http://localhost:<span id="setup-mcp-port-display-toml">3457</span>/mcp"</code></pre>
|
|
3611
4120
|
<div style="margin-top:6px;">
|
|
3612
4121
|
<strong style="color:var(--fg);">Config file locations:</strong><br>
|
|
3613
|
-
Claude Code: <code style="font-size:11px;">~/.claude/mcp.json</code><br>
|
|
4122
|
+
Claude Code project: <code style="font-size:11px;">~/.claude/mcp.json</code><br>
|
|
4123
|
+
Claude Code global: <code style="font-size:11px;">~/.claude.json</code><br>
|
|
4124
|
+
Codex: <code style="font-size:11px;">~/.codex/config.toml</code><br>
|
|
3614
4125
|
Cursor: <code style="font-size:11px;">~/.cursor/mcp.json</code><br>
|
|
3615
4126
|
Windsurf: <code style="font-size:11px;">~/.codeium/windsurf/mcp_config.json</code><br>
|
|
3616
4127
|
Claude Desktop: <code style="font-size:11px;">~/Library/Application Support/Claude/claude_desktop_config.json</code>
|
|
@@ -3682,8 +4193,6 @@
|
|
|
3682
4193
|
</div>
|
|
3683
4194
|
</div>
|
|
3684
4195
|
</div>
|
|
3685
|
-
|
|
3686
|
-
<div id="setup-version-label"></div>
|
|
3687
4196
|
</div>
|
|
3688
4197
|
</div>
|
|
3689
4198
|
</div>
|
|
@@ -4392,6 +4901,8 @@
|
|
|
4392
4901
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
4393
4902
|
<script src="js/session-search-utils.js"></script>
|
|
4394
4903
|
<script src="js/session-activity-utils.js"></script>
|
|
4904
|
+
<script src="js/session-status-precedence.js"></script>
|
|
4905
|
+
<script src="js/terminal-restore-state.js"></script>
|
|
4395
4906
|
<script src="js/walle-session.js"></script>
|
|
4396
4907
|
<script src="js/message-renderer.js"></script>
|
|
4397
4908
|
<script src="js/stream-view.js"></script>
|
|
@@ -4463,15 +4974,87 @@ function _safeStorageSet(storage, key, value) {
|
|
|
4463
4974
|
try { storage.setItem(key, value); } catch {}
|
|
4464
4975
|
}
|
|
4465
4976
|
|
|
4977
|
+
function setAppVersion(version, info = {}) {
|
|
4978
|
+
const cleanVersion = String(version || '').trim();
|
|
4979
|
+
const label = cleanVersion ? `v${cleanVersion}` : 'v?';
|
|
4980
|
+
const product = info.product || 'create-walle';
|
|
4981
|
+
const latest = String(info.latestVersion || '').trim();
|
|
4982
|
+
|
|
4983
|
+
const topbar = document.getElementById('app-version-label');
|
|
4984
|
+
if (topbar) {
|
|
4985
|
+
topbar.textContent = label;
|
|
4986
|
+
topbar.classList.toggle('update-available', !!(latest && cleanVersion && latest !== cleanVersion));
|
|
4987
|
+
topbar.title = cleanVersion
|
|
4988
|
+
? `Installed CTM / Wall-E bundle version: ${product} ${label}`
|
|
4989
|
+
: 'Installed CTM / Wall-E bundle version unknown';
|
|
4990
|
+
if (latest && cleanVersion && latest !== cleanVersion) {
|
|
4991
|
+
topbar.title += `; update available: v${latest}`;
|
|
4992
|
+
}
|
|
4993
|
+
if (info.components) {
|
|
4994
|
+
const ctm = info.components.ctm ? `CTM package ${info.components.ctm}` : '';
|
|
4995
|
+
const walle = info.components.wallE ? `Wall-E package ${info.components.wallE}` : '';
|
|
4996
|
+
const parts = [ctm, walle].filter(Boolean);
|
|
4997
|
+
if (parts.length) topbar.title += ` (${parts.join(', ')})`;
|
|
4998
|
+
}
|
|
4999
|
+
}
|
|
5000
|
+
|
|
5001
|
+
const setup = document.getElementById('setup-version-label');
|
|
5002
|
+
if (setup) {
|
|
5003
|
+
setup.textContent = cleanVersion ? `CTM / Wall-E ${label}` : 'Version unknown';
|
|
5004
|
+
setup.title = topbar ? topbar.title : 'Installed CTM / Wall-E bundle version';
|
|
5005
|
+
}
|
|
5006
|
+
}
|
|
5007
|
+
window.setAppVersion = setAppVersion;
|
|
5008
|
+
|
|
5009
|
+
function loadAppVersion() {
|
|
5010
|
+
fetch('/api/app/version')
|
|
5011
|
+
.then(r => r.json())
|
|
5012
|
+
.then(data => {
|
|
5013
|
+
if (data && data.version) setAppVersion(data.version, data);
|
|
5014
|
+
})
|
|
5015
|
+
.catch(() => {});
|
|
5016
|
+
}
|
|
5017
|
+
|
|
4466
5018
|
let _updateDismissedVersion = _safeStorageGet(localStorage, 'update_dismissed_version');
|
|
4467
5019
|
let _updateWizardSnoozedVersion = _safeStorageGet(sessionStorage, 'update_wizard_snoozed_version');
|
|
4468
5020
|
let _updateCurrentVersion = '';
|
|
4469
5021
|
let _updateLatestVersion = '';
|
|
4470
5022
|
let _updateApplying = false;
|
|
5023
|
+
const _updateTelemetryShown = new Set();
|
|
5024
|
+
|
|
5025
|
+
function _trackUpdateTelemetry(event, fields = {}) {
|
|
5026
|
+
const payload = {
|
|
5027
|
+
event,
|
|
5028
|
+
currentVersion: _updateCurrentVersion,
|
|
5029
|
+
latestVersion: _updateLatestVersion,
|
|
5030
|
+
...fields,
|
|
5031
|
+
};
|
|
5032
|
+
fetch('/api/updates/telemetry', {
|
|
5033
|
+
method: 'POST',
|
|
5034
|
+
headers: { 'Content-Type': 'application/json' },
|
|
5035
|
+
body: JSON.stringify(payload),
|
|
5036
|
+
}).catch(() => {});
|
|
5037
|
+
}
|
|
5038
|
+
|
|
5039
|
+
function _trackUpdatePromptShown(surface) {
|
|
5040
|
+
if (!_updateLatestVersion) return;
|
|
5041
|
+
const key = `${surface}:${_updateLatestVersion}`;
|
|
5042
|
+
if (_updateTelemetryShown.has(key)) return;
|
|
5043
|
+
_updateTelemetryShown.add(key);
|
|
5044
|
+
_trackUpdateTelemetry('prompt_shown', { surface });
|
|
5045
|
+
}
|
|
5046
|
+
|
|
5047
|
+
function _rememberDismissedUpdate() {
|
|
5048
|
+
if (_updateLatestVersion) {
|
|
5049
|
+
_updateDismissedVersion = _updateLatestVersion;
|
|
5050
|
+
_safeStorageSet(localStorage, 'update_dismissed_version', _updateLatestVersion);
|
|
5051
|
+
}
|
|
5052
|
+
}
|
|
4471
5053
|
|
|
4472
5054
|
function _setUpdateVersions(current, latest) {
|
|
4473
5055
|
_updateCurrentVersion = current || '';
|
|
4474
5056
|
_updateLatestVersion = latest || '';
|
|
5057
|
+
setAppVersion(_updateCurrentVersion, { latestVersion: _updateLatestVersion });
|
|
4475
5058
|
|
|
4476
5059
|
const banner = document.getElementById('update-banner');
|
|
4477
5060
|
if (banner) {
|
|
@@ -4518,6 +5101,7 @@ function showUpdateBanner(current, latest) {
|
|
|
4518
5101
|
if (!banner || !msg) return;
|
|
4519
5102
|
msg.textContent = `Update available: v${current} \u2192 v${latest}`;
|
|
4520
5103
|
banner.style.display = 'flex';
|
|
5104
|
+
_trackUpdatePromptShown('banner');
|
|
4521
5105
|
showUpdateWizard(current, latest);
|
|
4522
5106
|
}
|
|
4523
5107
|
|
|
@@ -4527,6 +5111,7 @@ function showUpdateWizard(current, latest) {
|
|
|
4527
5111
|
const wizard = document.getElementById('update-wizard');
|
|
4528
5112
|
if (!wizard) return;
|
|
4529
5113
|
wizard.classList.remove('hidden');
|
|
5114
|
+
_trackUpdatePromptShown('wizard');
|
|
4530
5115
|
setTimeout(() => {
|
|
4531
5116
|
const btn = document.getElementById('update-wizard-apply-btn');
|
|
4532
5117
|
if (btn && !btn.disabled) btn.focus();
|
|
@@ -4534,6 +5119,7 @@ function showUpdateWizard(current, latest) {
|
|
|
4534
5119
|
}
|
|
4535
5120
|
|
|
4536
5121
|
function snoozeUpdateWizard() {
|
|
5122
|
+
_trackUpdateTelemetry('action', { action: 'later', surface: 'wizard' });
|
|
4537
5123
|
if (_updateLatestVersion) {
|
|
4538
5124
|
_updateWizardSnoozedVersion = _updateLatestVersion;
|
|
4539
5125
|
_safeStorageSet(sessionStorage, 'update_wizard_snoozed_version', _updateLatestVersion);
|
|
@@ -4541,38 +5127,44 @@ function snoozeUpdateWizard() {
|
|
|
4541
5127
|
_hideUpdateWizard();
|
|
4542
5128
|
}
|
|
4543
5129
|
|
|
4544
|
-
function dismissUpdate() {
|
|
5130
|
+
function dismissUpdate(surface = 'unknown') {
|
|
5131
|
+
_trackUpdateTelemetry('action', { action: 'skip', surface });
|
|
4545
5132
|
_hideUpdatePrompts();
|
|
4546
|
-
|
|
4547
|
-
_updateDismissedVersion = _updateLatestVersion;
|
|
4548
|
-
_safeStorageSet(localStorage, 'update_dismissed_version', _updateLatestVersion);
|
|
4549
|
-
}
|
|
5133
|
+
_rememberDismissedUpdate();
|
|
4550
5134
|
}
|
|
4551
5135
|
|
|
4552
|
-
async function applyUpdate() {
|
|
5136
|
+
async function applyUpdate(surface = 'unknown') {
|
|
4553
5137
|
if (_updateApplying) return;
|
|
5138
|
+
_trackUpdateTelemetry('action', { action: 'apply', surface });
|
|
4554
5139
|
_setUpdateApplying(true);
|
|
4555
5140
|
try {
|
|
4556
5141
|
const resp = await fetch('/api/updates/apply', { method: 'POST' });
|
|
4557
5142
|
const data = await resp.json();
|
|
4558
5143
|
if (data.status === 'updating') {
|
|
5144
|
+
_trackUpdateTelemetry('action', { action: 'apply_started', surface });
|
|
4559
5145
|
toast('Update started. CTM will restart shortly...', { type: 'info', duration: 8000 });
|
|
4560
|
-
|
|
5146
|
+
_hideUpdatePrompts();
|
|
5147
|
+
_rememberDismissedUpdate();
|
|
4561
5148
|
} else {
|
|
5149
|
+
_trackUpdateTelemetry('action', { action: 'already_up_to_date', surface });
|
|
4562
5150
|
toast('Already up to date.', { type: 'success' });
|
|
4563
5151
|
_hideUpdatePrompts();
|
|
4564
5152
|
_setUpdateApplying(false);
|
|
4565
5153
|
}
|
|
4566
5154
|
} catch (e) {
|
|
5155
|
+
_trackUpdateTelemetry('action', { action: 'apply_failed', surface });
|
|
4567
5156
|
toast('Update failed: ' + e.message, { type: 'error' });
|
|
4568
5157
|
_setUpdateApplying(false);
|
|
4569
5158
|
}
|
|
4570
5159
|
}
|
|
4571
5160
|
|
|
4572
5161
|
function checkForUpdates() {
|
|
4573
|
-
fetch('/api/updates/check')
|
|
5162
|
+
fetch('/api/updates/check?refresh=1')
|
|
4574
5163
|
.then(r => r.json())
|
|
4575
5164
|
.then(data => {
|
|
5165
|
+
if (data.currentVersion) {
|
|
5166
|
+
setAppVersion(data.currentVersion, { latestVersion: data.latestVersion });
|
|
5167
|
+
}
|
|
4576
5168
|
if (data.updateAvailable) {
|
|
4577
5169
|
showUpdateBanner(data.currentVersion, data.latestVersion);
|
|
4578
5170
|
}
|
|
@@ -4581,6 +5173,7 @@ function checkForUpdates() {
|
|
|
4581
5173
|
}
|
|
4582
5174
|
|
|
4583
5175
|
// Check on page load
|
|
5176
|
+
setTimeout(loadAppVersion, 0);
|
|
4584
5177
|
setTimeout(checkForUpdates, 3000);
|
|
4585
5178
|
|
|
4586
5179
|
// --- State ---
|
|
@@ -4594,6 +5187,7 @@ const state = window._ctmState = {
|
|
|
4594
5187
|
sessions: new Map(), // id -> { term, fitAddon, container }
|
|
4595
5188
|
activeTab: null, // session id or 'rules'
|
|
4596
5189
|
lastActiveWorkSessionId: null, // last non-Wall-E session used as repo context
|
|
5190
|
+
standup: { loading: false, data: null, lastLoadedAt: 0 },
|
|
4597
5191
|
tabOrder: [], // session ids in tab order
|
|
4598
5192
|
reviewingSessionId: null, // currently reviewed session
|
|
4599
5193
|
sidebarCollapsed: false,
|
|
@@ -4683,6 +5277,7 @@ function connect() {
|
|
|
4683
5277
|
case 'created': onCreated(msg); break;
|
|
4684
5278
|
case 'output': onOutput(msg); break;
|
|
4685
5279
|
case 'snapshot': onSnapshot(msg); break;
|
|
5280
|
+
case 'codex-terminal-final': onCodexTerminalFinal(msg); break;
|
|
4686
5281
|
case 'exit': onExit(msg); break;
|
|
4687
5282
|
case 'restarting': onRestarting(msg); break;
|
|
4688
5283
|
case 'sessions': state._sessionsListDone = onSessionsList(msg); break;
|
|
@@ -4715,6 +5310,7 @@ function connect() {
|
|
|
4715
5310
|
case 'walle-progress': WalleSession.handleProgress(msg); break;
|
|
4716
5311
|
case 'walle-response': WalleSession.handleResponse(msg); break;
|
|
4717
5312
|
case 'walle-history': WalleSession.handleHistory(msg); break;
|
|
5313
|
+
case 'walle-model': WalleSession.handleModel(msg); break;
|
|
4718
5314
|
case 'server-restarting':
|
|
4719
5315
|
// Server broadcast this BEFORE closing connections — set flag so onclose
|
|
4720
5316
|
// knows this is intentional and shows the overlay immediately.
|
|
@@ -4834,7 +5430,7 @@ function onServerReady() {
|
|
|
4834
5430
|
}
|
|
4835
5431
|
|
|
4836
5432
|
// 2. Restore active tab — single source of truth for session activation.
|
|
4837
|
-
// Priority:
|
|
5433
|
+
// Priority: explicit hash > saved session > pinned Sessions overview.
|
|
4838
5434
|
// SKIP this step if the user is on a non-session nav panel (walle, prompts, etc.):
|
|
4839
5435
|
// handleHashRoute already navigated there, and activateTab would switch away.
|
|
4840
5436
|
const currentNav = document.querySelector('.nav-pill.active')?.dataset?.nav || 'sessions';
|
|
@@ -4848,13 +5444,15 @@ function onServerReady() {
|
|
|
4848
5444
|
// auto-activating in onSessionsList.
|
|
4849
5445
|
if (hashTarget && state.sessions.has(hashTarget)) {
|
|
4850
5446
|
activateTab(hashTarget);
|
|
5447
|
+
} else if (state._forceSessionsOverview) {
|
|
5448
|
+
state._forceSessionsOverview = false;
|
|
5449
|
+
showStandupDashboard({ skipHash: true });
|
|
4851
5450
|
} else if (savedTarget && state.sessions.has(savedTarget)) {
|
|
4852
5451
|
activateTab(savedTarget);
|
|
4853
5452
|
} else if (savedTarget === 'review' && state.tabOrder.includes('review')) {
|
|
4854
5453
|
activateTab('review');
|
|
4855
|
-
} else if (!state.activeTab || !state.sessions.has(state.activeTab)) {
|
|
4856
|
-
|
|
4857
|
-
if (first) activateTab(first);
|
|
5454
|
+
} else if (!state.activeTab || (!state.sessions.has(state.activeTab) && state.activeTab !== SESSIONS_OVERVIEW_TAB_ID)) {
|
|
5455
|
+
showStandupDashboard({ skipHash: true });
|
|
4858
5456
|
}
|
|
4859
5457
|
}
|
|
4860
5458
|
|
|
@@ -5156,6 +5754,7 @@ function _clientDetectAgentType(cmd) {
|
|
|
5156
5754
|
if (c.includes('claude')) return 'claude';
|
|
5157
5755
|
if (c.includes('codex')) return 'codex';
|
|
5158
5756
|
if (c.includes('gemini')) return 'gemini';
|
|
5757
|
+
if (c.includes('opencode') || c.includes('open-code')) return 'opencode';
|
|
5159
5758
|
if (c.includes('wall-e') || c.includes('walle')) return 'walle';
|
|
5160
5759
|
return null;
|
|
5161
5760
|
}
|
|
@@ -5165,7 +5764,8 @@ const CLIENT_AGENT_CAPABILITIES = {
|
|
|
5165
5764
|
'claude-desktop': { structuredTranscript: true, promptNavigation: 'transcript', review: true, resume: false },
|
|
5166
5765
|
codex: { structuredTranscript: true, promptNavigation: 'transcript', review: true, resume: true },
|
|
5167
5766
|
gemini: { structuredTranscript: false, promptNavigation: 'terminal', review: false, resume: true },
|
|
5168
|
-
|
|
5767
|
+
opencode: { structuredTranscript: false, promptNavigation: 'terminal', review: false, resume: true },
|
|
5768
|
+
walle: { structuredTranscript: true, promptNavigation: 'chat', review: true, resume: false },
|
|
5169
5769
|
shell: { structuredTranscript: false, promptNavigation: 'terminal', review: false, resume: false },
|
|
5170
5770
|
};
|
|
5171
5771
|
|
|
@@ -5176,6 +5776,7 @@ function _clientNormalizeAgentType(value) {
|
|
|
5176
5776
|
if (v === 'claude-code') return 'claude';
|
|
5177
5777
|
if (v === 'claude-desktop-session' || v === 'desktop') return 'claude-desktop';
|
|
5178
5778
|
if (v === 'gemini-cli') return 'gemini';
|
|
5779
|
+
if (v === 'open-code' || v === 'opencode-cli') return 'opencode';
|
|
5179
5780
|
return _clientDetectAgentType(v);
|
|
5180
5781
|
}
|
|
5181
5782
|
|
|
@@ -5206,6 +5807,41 @@ const _CODEX_BUSY_WORD = 'working';
|
|
|
5206
5807
|
const _CODEX_BUSY_HINT_RE = /esc\s+to\s+interrupt/i;
|
|
5207
5808
|
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;
|
|
5208
5809
|
|
|
5810
|
+
function _inputMayResolveWaiting(data, session) {
|
|
5811
|
+
const text = String(data || '');
|
|
5812
|
+
if (!text) return false;
|
|
5813
|
+
if (text.indexOf('\r') >= 0 || text.indexOf('\n') >= 0) return true;
|
|
5814
|
+
if (text.indexOf('\x03') >= 0 || text.indexOf('\x04') >= 0) return true;
|
|
5815
|
+
const reason = session?._waitingReason || '';
|
|
5816
|
+
if (reason === 'approval' || reason === 'choice') {
|
|
5817
|
+
return /^[\s]*[12yYnN\u001b][\s]*$/.test(text);
|
|
5818
|
+
}
|
|
5819
|
+
return false;
|
|
5820
|
+
}
|
|
5821
|
+
|
|
5822
|
+
const _CODEX_MUTED_PROMPT_BG = new Set(['237', '238']);
|
|
5823
|
+
function _normalizeCodexPromptBackground(session, data) {
|
|
5824
|
+
if (_clientAgentTypeForSession(session) !== 'codex') return data;
|
|
5825
|
+
const text = String(data || '');
|
|
5826
|
+
if (text.indexOf('\x1b[') < 0 || text.indexOf('48;5;23') < 0) return data;
|
|
5827
|
+
return text.replace(/\x1b\[([0-9;]*)m/g, (seq, rawParams) => {
|
|
5828
|
+
const params = rawParams === '' ? ['0'] : rawParams.split(';');
|
|
5829
|
+
const hasMutedPromptBg = params.some((value, i) => (
|
|
5830
|
+
value === '48' && params[i + 1] === '5' && _CODEX_MUTED_PROMPT_BG.has(params[i + 2])
|
|
5831
|
+
));
|
|
5832
|
+
if (!hasMutedPromptBg) return seq;
|
|
5833
|
+
const next = [];
|
|
5834
|
+
for (let i = 0; i < params.length; i++) {
|
|
5835
|
+
if (params[i] === '48' && params[i + 1] === '5' && _CODEX_MUTED_PROMPT_BG.has(params[i + 2])) {
|
|
5836
|
+
i += 2;
|
|
5837
|
+
continue;
|
|
5838
|
+
}
|
|
5839
|
+
next.push(params[i]);
|
|
5840
|
+
}
|
|
5841
|
+
return next.length ? `\x1b[${next.join(';')}m` : '';
|
|
5842
|
+
});
|
|
5843
|
+
}
|
|
5844
|
+
|
|
5209
5845
|
function _isClaudeRedraw(data) {
|
|
5210
5846
|
const s = String(data || '');
|
|
5211
5847
|
return (
|
|
@@ -5283,7 +5919,8 @@ function _zerolagConfigFor(agentType) {
|
|
|
5283
5919
|
// Prompt character per agent — the addon scans the buffer bottom-up for this
|
|
5284
5920
|
// char. offset 2 = input starts 2 cells after the prompt char (char + space).
|
|
5285
5921
|
if (agentType === 'claude') return { type: 'character', char: '❯', offset: 2 }; // ❯
|
|
5286
|
-
|
|
5922
|
+
// Codex intentionally disabled: its ratatui redraws can leave stale lower
|
|
5923
|
+
// prompt markers, and the character scanner can strand typed text there.
|
|
5287
5924
|
// Gemini intentionally disabled until we verify its prompt format.
|
|
5288
5925
|
// Re-enable after a quick live-session check of its actual TUI layout.
|
|
5289
5926
|
return null;
|
|
@@ -5354,6 +5991,65 @@ function _createTerminalStub(id) {
|
|
|
5354
5991
|
});
|
|
5355
5992
|
}
|
|
5356
5993
|
|
|
5994
|
+
function createCodexFinalPanel(id) {
|
|
5995
|
+
const panel = document.createElement('div');
|
|
5996
|
+
panel.className = 'codex-final-panel';
|
|
5997
|
+
panel.dataset.sessionId = id;
|
|
5998
|
+
|
|
5999
|
+
const header = document.createElement('div');
|
|
6000
|
+
header.className = 'codex-final-header';
|
|
6001
|
+
const title = document.createElement('span');
|
|
6002
|
+
title.textContent = 'Latest Codex Answer';
|
|
6003
|
+
const close = document.createElement('button');
|
|
6004
|
+
close.type = 'button';
|
|
6005
|
+
close.className = 'codex-final-close';
|
|
6006
|
+
close.title = 'Dismiss';
|
|
6007
|
+
close.textContent = '×';
|
|
6008
|
+
close.onclick = function() {
|
|
6009
|
+
const s = state.sessions.get(id);
|
|
6010
|
+
if (s && s._codexFinal) s._codexFinalDismissed = s._codexFinal.fingerprint || true;
|
|
6011
|
+
_renderCodexFinalPanel(id);
|
|
6012
|
+
};
|
|
6013
|
+
header.appendChild(title);
|
|
6014
|
+
header.appendChild(close);
|
|
6015
|
+
|
|
6016
|
+
const body = document.createElement('div');
|
|
6017
|
+
body.className = 'codex-final-body';
|
|
6018
|
+
panel.appendChild(header);
|
|
6019
|
+
panel.appendChild(body);
|
|
6020
|
+
return panel;
|
|
6021
|
+
}
|
|
6022
|
+
|
|
6023
|
+
function _renderCodexFinalPanel(id) {
|
|
6024
|
+
const s = state.sessions.get(id);
|
|
6025
|
+
if (!s || !s.container) return;
|
|
6026
|
+
const panel = s.container.querySelector('.codex-final-panel');
|
|
6027
|
+
if (!panel) return;
|
|
6028
|
+
const final = s._codexFinal;
|
|
6029
|
+
const hiddenByView = typeof _streamState !== 'undefined' && _streamState.activeView.get(id) === 'conversation';
|
|
6030
|
+
const dismissed = final && s._codexFinalDismissed === final.fingerprint;
|
|
6031
|
+
const shouldShow = !!(final && final.text && !dismissed && !hiddenByView);
|
|
6032
|
+
panel.classList.toggle('visible', shouldShow);
|
|
6033
|
+
if (!shouldShow) return;
|
|
6034
|
+
const body = panel.querySelector('.codex-final-body');
|
|
6035
|
+
if (body) body.textContent = final.text;
|
|
6036
|
+
}
|
|
6037
|
+
|
|
6038
|
+
function onCodexTerminalFinal(msg) {
|
|
6039
|
+
const id = msg && msg.id;
|
|
6040
|
+
if (!id) return;
|
|
6041
|
+
const s = state.sessions.get(id);
|
|
6042
|
+
if (!s) return;
|
|
6043
|
+
const existingFingerprint = s._codexFinal && s._codexFinal.fingerprint;
|
|
6044
|
+
s._codexFinal = {
|
|
6045
|
+
text: String(msg.text || ''),
|
|
6046
|
+
timestamp: msg.timestamp || Date.now(),
|
|
6047
|
+
fingerprint: msg.fingerprint || String(msg.text || '').slice(0, 64),
|
|
6048
|
+
};
|
|
6049
|
+
if (existingFingerprint !== s._codexFinal.fingerprint) s._codexFinalDismissed = null;
|
|
6050
|
+
_renderCodexFinalPanel(id);
|
|
6051
|
+
}
|
|
6052
|
+
|
|
5357
6053
|
function createTerminal(id, opts) {
|
|
5358
6054
|
opts = opts || {};
|
|
5359
6055
|
// Phase 1B: if a stub exists, remove its container first
|
|
@@ -5411,6 +6107,7 @@ function createTerminal(id, opts) {
|
|
|
5411
6107
|
// snapshot message. No PTY round-trip — instant snapshot restore.
|
|
5412
6108
|
s.term.clear();
|
|
5413
6109
|
try { s.term.clearTextureAtlas(); } catch {}
|
|
6110
|
+
_markClientUiRefreshOutputSuppression(s);
|
|
5414
6111
|
send({ type: 'reflow', id: id, cols: s.term.cols, rows: s.term.rows });
|
|
5415
6112
|
};
|
|
5416
6113
|
toolbar.appendChild(reflowBtn);
|
|
@@ -5543,6 +6240,7 @@ function createTerminal(id, opts) {
|
|
|
5543
6240
|
});
|
|
5544
6241
|
|
|
5545
6242
|
// Add conversation view inside the container so it travels with pane reparenting
|
|
6243
|
+
container.appendChild(createCodexFinalPanel(id));
|
|
5546
6244
|
if (typeof createConversationView === 'function') {
|
|
5547
6245
|
container.appendChild(createConversationView(id));
|
|
5548
6246
|
}
|
|
@@ -5753,8 +6451,9 @@ function createTerminal(id, opts) {
|
|
|
5753
6451
|
// The addon scans the xterm buffer for the prompt char and clears pending text
|
|
5754
6452
|
// once the server echo catches up — safe alongside CTM's immediate-send model.
|
|
5755
6453
|
_zerolagFeedInput(_s, data);
|
|
5756
|
-
// Only
|
|
5757
|
-
|
|
6454
|
+
// Only resolve waiting on submit/choice keystrokes. Plain prompt typing
|
|
6455
|
+
// should keep the sidebar in Waiting, not manufacture a Running window.
|
|
6456
|
+
if (_s && _s._waitingForInput && _inputMayResolveWaiting(data, _s)) clearWaitingState(id);
|
|
5758
6457
|
// User typed something — re-enable follow mode and scroll to bottom.
|
|
5759
6458
|
const s = _s;
|
|
5760
6459
|
if (s) {
|
|
@@ -5784,6 +6483,7 @@ function createTerminal(id, opts) {
|
|
|
5784
6483
|
term.onResize(({ cols, rows }) => {
|
|
5785
6484
|
const s = state.sessions.get(id);
|
|
5786
6485
|
if (s && s._suppressResize) return; // Skip during font metric refresh
|
|
6486
|
+
_markClientUiRefreshOutputSuppression(s);
|
|
5787
6487
|
send({ type: 'resize', id, cols, rows });
|
|
5788
6488
|
});
|
|
5789
6489
|
|
|
@@ -5871,10 +6571,18 @@ function createTerminal(id, opts) {
|
|
|
5871
6571
|
if (writer._userScrollLocked) return; // locked — only wheel can unlock
|
|
5872
6572
|
const current = state.sessions.get(id) || { _id: id, term, writer, container };
|
|
5873
6573
|
writer.followMode = _isAtTerminalFollowBottom(current);
|
|
6574
|
+
requestAnimationFrame(() => _alignXtermHelperTextareaToViewport(current));
|
|
5874
6575
|
});
|
|
5875
6576
|
|
|
5876
|
-
|
|
5877
|
-
|
|
6577
|
+
const sessionEntry = { _id: id, term, fitAddon, searchAddon, container, needsFontRefresh: true, writer, promptLines: [], promptNavIdx: -1, _webglAddon, _loadWebgl };
|
|
6578
|
+
state.sessions.set(id, sessionEntry);
|
|
6579
|
+
if (typeof term.onWriteParsed === 'function') {
|
|
6580
|
+
sessionEntry._helperAlignDisposer = term.onWriteParsed(() => {
|
|
6581
|
+
const current = state.sessions.get(id);
|
|
6582
|
+
if (current) _alignXtermHelperTextareaToViewport(current);
|
|
6583
|
+
});
|
|
6584
|
+
}
|
|
6585
|
+
// Attach local echo overlay for known agent types (Claude today; Codex/Gemini pending prompt verification).
|
|
5878
6586
|
_attachZerolag(state.sessions.get(id), id);
|
|
5879
6587
|
return { term, fitAddon, container };
|
|
5880
6588
|
}
|
|
@@ -5904,6 +6612,12 @@ function _extractPromptPreview(text) {
|
|
|
5904
6612
|
// (e.g., system-reminder, local-command-caveat, available-deferred-tools)
|
|
5905
6613
|
// User text never contains these hyphenated XML tag patterns.
|
|
5906
6614
|
let stripped = text
|
|
6615
|
+
// User prompts can start with image attachments before the actual text.
|
|
6616
|
+
// Drop the attachment block so prompt navigation indexes the user's words,
|
|
6617
|
+
// not the image placeholder.
|
|
6618
|
+
.replace(/<image\b[^>]*>[\s\S]*?<\/image>/gi, '')
|
|
6619
|
+
.replace(/<\/?image\b[^>]*>/gi, '')
|
|
6620
|
+
.replace(/^\s*\[Image #\d+\]\s*$/gmi, '')
|
|
5907
6621
|
// Remove matched open/close tag pairs with hyphenated names (system tags)
|
|
5908
6622
|
.replace(/<([a-z][a-z0-9]*(?:-[a-z0-9]+)+)[^>]*>[\s\S]*?<\/\1>/gi, '')
|
|
5909
6623
|
// Remove any remaining orphaned hyphenated system tags
|
|
@@ -5956,11 +6670,64 @@ function _isTerminalPromptLine(agentType, text) {
|
|
|
5956
6670
|
// API results are cached and only re-fetched every 30s to avoid input lag.
|
|
5957
6671
|
const _promptScanCache = {}; // { [sessionId]: { ts, previews } }
|
|
5958
6672
|
|
|
5959
|
-
function
|
|
6673
|
+
function _promptPreviewsEqual(a, b) {
|
|
6674
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
6675
|
+
for (let i = 0; i < a.length; i++) {
|
|
6676
|
+
if (a[i] !== b[i]) return false;
|
|
6677
|
+
}
|
|
6678
|
+
return true;
|
|
6679
|
+
}
|
|
6680
|
+
|
|
6681
|
+
function _promptCacheKeyMatchesSession(key, id, s) {
|
|
6682
|
+
if (!key || key === id || key.startsWith(id + ':')) return true;
|
|
6683
|
+
if (s?.meta?.claudeSessionId && key.startsWith(s.meta.claudeSessionId)) return true;
|
|
6684
|
+
if (s?.meta?.agentSessionId && key.startsWith(s.meta.agentSessionId)) return true;
|
|
6685
|
+
if (s?.meta?.agentSessionToken && key.startsWith(s.meta.agentSessionToken)) return true;
|
|
6686
|
+
return false;
|
|
6687
|
+
}
|
|
6688
|
+
|
|
6689
|
+
function invalidatePromptScanCacheForSession(id) {
|
|
6690
|
+
const s = state.sessions.get(id);
|
|
6691
|
+
for (const key of Object.keys(_promptScanCache)) {
|
|
6692
|
+
if (_promptCacheKeyMatchesSession(key, id, s)) delete _promptScanCache[key];
|
|
6693
|
+
}
|
|
6694
|
+
}
|
|
6695
|
+
|
|
6696
|
+
function _recordLivePromptPreview(id, text) {
|
|
6697
|
+
const s = state.sessions.get(id);
|
|
6698
|
+
if (!s) return;
|
|
6699
|
+
const cleaned = _extractPromptPreview(text);
|
|
6700
|
+
if (!cleaned) return;
|
|
6701
|
+
const previews = Array.isArray(s.promptPreviews) ? s.promptPreviews.slice() : [];
|
|
6702
|
+
if (previews[previews.length - 1] === cleaned) return;
|
|
6703
|
+
previews.push(cleaned);
|
|
6704
|
+
s.promptPreviews = previews;
|
|
6705
|
+
s.promptLines = previews.map((_, i) => Array.isArray(s.promptLines) && i < s.promptLines.length ? s.promptLines[i] : -1);
|
|
6706
|
+
s.promptNavIdx = -1;
|
|
6707
|
+
s._promptLinesResolved = false;
|
|
6708
|
+
promptNavUpdateBadge(id);
|
|
6709
|
+
|
|
6710
|
+
for (const key of Object.keys(_promptScanCache)) {
|
|
6711
|
+
if (_promptCacheKeyMatchesSession(key, id, s)) {
|
|
6712
|
+
_promptScanCache[key] = { ts: Date.now(), previews: previews.slice() };
|
|
6713
|
+
}
|
|
6714
|
+
}
|
|
6715
|
+
}
|
|
6716
|
+
|
|
6717
|
+
window._ctmRecordLivePromptPreview = _recordLivePromptPreview;
|
|
6718
|
+
|
|
6719
|
+
function scanPromptLines(id, opts) {
|
|
6720
|
+
opts = opts || {};
|
|
5960
6721
|
const s = state.sessions.get(id);
|
|
5961
6722
|
if (!s) return;
|
|
5962
6723
|
const agentType = _clientAgentTypeForSession(s);
|
|
5963
6724
|
const caps = _clientAgentCaps(agentType);
|
|
6725
|
+
if (caps.promptNavigation === 'chat') {
|
|
6726
|
+
if (window.WalleSession && typeof window.WalleSession.updatePromptNav === 'function') {
|
|
6727
|
+
window.WalleSession.updatePromptNav(id);
|
|
6728
|
+
}
|
|
6729
|
+
return;
|
|
6730
|
+
}
|
|
5964
6731
|
// Find projectEntry and Claude session ID for the API call
|
|
5965
6732
|
// The CTM tab ID (id) may differ from the Claude JSONL session ID
|
|
5966
6733
|
let projectEntry = null;
|
|
@@ -5985,8 +6752,8 @@ function scanPromptLines(id) {
|
|
|
5985
6752
|
const apiProjectEntry = agentType === 'codex' ? '' : projectEntry;
|
|
5986
6753
|
const cacheKey = claudeId + ':' + (apiProjectEntry || 'codex');
|
|
5987
6754
|
const cache = _promptScanCache[cacheKey];
|
|
5988
|
-
if (cache && Date.now() - cache.ts < 30000) {
|
|
5989
|
-
if (!s.promptPreviews
|
|
6755
|
+
if (!opts.force && cache && Date.now() - cache.ts < 30000) {
|
|
6756
|
+
if (!_promptPreviewsEqual(s.promptPreviews, cache.previews)) {
|
|
5990
6757
|
s.promptLines = cache.previews.map(() => -1);
|
|
5991
6758
|
s.promptPreviews = cache.previews;
|
|
5992
6759
|
s.promptNavIdx = -1;
|
|
@@ -5994,7 +6761,7 @@ function scanPromptLines(id) {
|
|
|
5994
6761
|
promptNavUpdateBadge(id);
|
|
5995
6762
|
return;
|
|
5996
6763
|
}
|
|
5997
|
-
_scanPromptLinesFromAPI(id, apiProjectEntry, claudeId);
|
|
6764
|
+
return _scanPromptLinesFromAPI(id, apiProjectEntry, claudeId);
|
|
5998
6765
|
} else {
|
|
5999
6766
|
_scanPromptLinesFromTerminal(id);
|
|
6000
6767
|
}
|
|
@@ -6005,7 +6772,13 @@ async function _scanPromptLinesFromAPI(id, projectEntry, claudeId) {
|
|
|
6005
6772
|
if (!s) return;
|
|
6006
6773
|
const apiId = claudeId || id; // Use Claude session ID for the API, tab ID for state
|
|
6007
6774
|
try {
|
|
6008
|
-
const
|
|
6775
|
+
const params = new URLSearchParams({
|
|
6776
|
+
id: apiId,
|
|
6777
|
+
project: projectEntry || '',
|
|
6778
|
+
token: state.token,
|
|
6779
|
+
nocache: '1',
|
|
6780
|
+
});
|
|
6781
|
+
const res = await fetch(`/api/session/messages?${params.toString()}`);
|
|
6009
6782
|
const raw = await res.json();
|
|
6010
6783
|
const messages = Array.isArray(raw) ? raw : (Array.isArray(raw.messages) ? raw.messages : null);
|
|
6011
6784
|
if (raw.error || !messages) {
|
|
@@ -6153,11 +6926,16 @@ function _ensurePromptBufferPositions(id) {
|
|
|
6153
6926
|
|
|
6154
6927
|
function promptNavGo(id, dir) {
|
|
6155
6928
|
const s = state.sessions.get(id);
|
|
6156
|
-
if (!s
|
|
6929
|
+
if (!s) return;
|
|
6930
|
+
if (_clientAgentTypeForSession(s) === 'walle' && window.WalleSession && typeof window.WalleSession.promptNavGo === 'function') {
|
|
6931
|
+
window.WalleSession.promptNavGo(id, dir);
|
|
6932
|
+
return;
|
|
6933
|
+
}
|
|
6934
|
+
if (!Array.isArray(s.promptLines) || s.promptLines.length === 0) return;
|
|
6157
6935
|
// Always re-resolve from current buffer (may have grown)
|
|
6158
6936
|
_applyPromptCache(id);
|
|
6159
6937
|
|
|
6160
|
-
const total = s.promptLines.length;
|
|
6938
|
+
const total = Array.isArray(s.promptLines) ? s.promptLines.length : 0;
|
|
6161
6939
|
let newIdx;
|
|
6162
6940
|
if (s.promptNavIdx < 0) {
|
|
6163
6941
|
// No prompt selected yet: up goes to last, down goes to first
|
|
@@ -6179,6 +6957,7 @@ function promptNavGo(id, dir) {
|
|
|
6179
6957
|
promptNavUpdateBadge(id);
|
|
6180
6958
|
return;
|
|
6181
6959
|
}
|
|
6960
|
+
if (_jumpToLatestUnmappedPrompt(s, id, newIdx)) return;
|
|
6182
6961
|
newIdx += dir; // Skip non-navigable, continue in same direction
|
|
6183
6962
|
}
|
|
6184
6963
|
}
|
|
@@ -6186,6 +6965,7 @@ function promptNavGo(id, dir) {
|
|
|
6186
6965
|
function promptNavUpdateBadge(id) {
|
|
6187
6966
|
const s = state.sessions.get(id);
|
|
6188
6967
|
if (!s) return;
|
|
6968
|
+
if (!s.container || typeof s.container.querySelector !== 'function') return;
|
|
6189
6969
|
const nav = s.container.querySelector('.prompt-nav');
|
|
6190
6970
|
if (!nav) return;
|
|
6191
6971
|
const badge = nav.querySelector('.prompt-nav-badge');
|
|
@@ -6198,16 +6978,40 @@ function promptNavUpdateBadge(id) {
|
|
|
6198
6978
|
nextBtn.disabled = total === 0;
|
|
6199
6979
|
}
|
|
6200
6980
|
|
|
6201
|
-
function
|
|
6981
|
+
function _isLatestPromptIndex(s, idx) {
|
|
6982
|
+
return !!s && Array.isArray(s.promptLines) && idx === s.promptLines.length - 1;
|
|
6983
|
+
}
|
|
6984
|
+
|
|
6985
|
+
function _jumpToLatestUnmappedPrompt(s, id, idx) {
|
|
6986
|
+
if (!_isLatestPromptIndex(s, idx) || !s.term) return false;
|
|
6987
|
+
s.promptNavIdx = idx;
|
|
6988
|
+
if (s.writer) {
|
|
6989
|
+
s.writer.followMode = true;
|
|
6990
|
+
s.writer._userScrollLocked = false;
|
|
6991
|
+
}
|
|
6992
|
+
_ensureScrolledToBottom(s);
|
|
6993
|
+
if (s.term.refresh && s.term.rows) s.term.refresh(0, s.term.rows - 1);
|
|
6994
|
+
promptNavUpdateBadge(id);
|
|
6995
|
+
return true;
|
|
6996
|
+
}
|
|
6997
|
+
|
|
6998
|
+
async function promptNavToggleList(id) {
|
|
6202
6999
|
const s = state.sessions.get(id);
|
|
6203
7000
|
if (!s) return;
|
|
6204
|
-
// Always re-resolve positions from current buffer (buffer may have grown)
|
|
6205
|
-
_applyPromptCache(id);
|
|
6206
7001
|
const nav = s.container.querySelector('.prompt-nav');
|
|
6207
7002
|
if (!nav) return;
|
|
6208
7003
|
// Close existing list
|
|
6209
7004
|
const existing = nav.querySelector('.prompt-nav-list');
|
|
6210
7005
|
if (existing) { existing.remove(); return; }
|
|
7006
|
+
|
|
7007
|
+
// The user opens this list specifically to see the current prompt history.
|
|
7008
|
+
// Bypass the 30s prompt cache here; otherwise a newly submitted prompt can
|
|
7009
|
+
// be visible in Conversation while this dropdown still shows the previous
|
|
7010
|
+
// transcript snapshot.
|
|
7011
|
+
await scanPromptLines(id, { force: true });
|
|
7012
|
+
// Re-resolve positions from current buffer (buffer may have grown) after the
|
|
7013
|
+
// fresh transcript read updates previews.
|
|
7014
|
+
_applyPromptCache(id);
|
|
6211
7015
|
if (s.promptLines.length === 0) return;
|
|
6212
7016
|
|
|
6213
7017
|
// Build list showing preview text for each prompt (deduplicated)
|
|
@@ -6235,13 +7039,14 @@ function promptNavToggleList(id) {
|
|
|
6235
7039
|
if (seenTexts.has(text)) continue;
|
|
6236
7040
|
seenTexts.add(text);
|
|
6237
7041
|
const inBuffer = s.promptLines[i] >= 0;
|
|
7042
|
+
const latestUnmapped = !inBuffer && _isLatestPromptIndex(s, i);
|
|
6238
7043
|
const item = document.createElement('div');
|
|
6239
|
-
item.className = 'prompt-nav-list-item' + (i === s.promptNavIdx ? ' current' : '') + (inBuffer ? '' : ' not-in-buffer');
|
|
7044
|
+
item.className = 'prompt-nav-list-item' + (i === s.promptNavIdx ? ' current' : '') + (inBuffer ? '' : (latestUnmapped ? ' latest-unmapped' : ' not-in-buffer'));
|
|
6240
7045
|
if (!inBuffer) {
|
|
6241
7046
|
item.style.display = 'flex'; item.style.alignItems = 'baseline';
|
|
6242
7047
|
const badge = document.createElement('span');
|
|
6243
|
-
badge.className = 'prompt-nav-badge-unreachable';
|
|
6244
|
-
badge.textContent = 'unreachable';
|
|
7048
|
+
badge.className = latestUnmapped ? 'prompt-nav-badge-latest' : 'prompt-nav-badge-unreachable';
|
|
7049
|
+
badge.textContent = latestUnmapped ? 'latest' : 'unreachable';
|
|
6245
7050
|
const textSpan = document.createElement('span');
|
|
6246
7051
|
textSpan.className = 'prompt-text';
|
|
6247
7052
|
textSpan.textContent = text;
|
|
@@ -6264,8 +7069,10 @@ function promptNavToggleList(id) {
|
|
|
6264
7069
|
s.term.refresh(0, s.term.rows - 1);
|
|
6265
7070
|
promptNavUpdateBadge(id);
|
|
6266
7071
|
list.remove();
|
|
7072
|
+
} else if (_jumpToLatestUnmappedPrompt(s, id, idx)) {
|
|
7073
|
+
list.remove();
|
|
6267
7074
|
}
|
|
6268
|
-
// If still not in buffer, keep dropdown open (don't silently close)
|
|
7075
|
+
// If an older prompt is still not in buffer, keep dropdown open (don't silently close)
|
|
6269
7076
|
}; })(i);
|
|
6270
7077
|
list.appendChild(item);
|
|
6271
7078
|
}
|
|
@@ -6292,6 +7099,9 @@ function activateTab(id) {
|
|
|
6292
7099
|
|
|
6293
7100
|
const specialPanels = ['rules', 'insights', 'permissions', 'prompts', 'codereview', 'walle', 'models', 'backups', 'worktrees', 'setup'];
|
|
6294
7101
|
const isPanel = specialPanels.includes(id);
|
|
7102
|
+
if (!isPanel && state.sessions.has(id)) {
|
|
7103
|
+
state._savedActiveSession = id;
|
|
7104
|
+
}
|
|
6295
7105
|
if (state.activeTab && state.activeTab !== id && state.sessions.has(state.activeTab)) {
|
|
6296
7106
|
const prev = state.sessions.get(state.activeTab);
|
|
6297
7107
|
if (prev && prev.meta?.type !== 'walle') state.lastActiveWorkSessionId = state.activeTab;
|
|
@@ -6323,6 +7133,8 @@ function activateTab(id) {
|
|
|
6323
7133
|
// Trim trailing empty lines to keep the saved text compact
|
|
6324
7134
|
while (lines.length && lines[lines.length - 1] === '') lines.pop();
|
|
6325
7135
|
prevSession._savedScrollbackText = lines.join('\r\n');
|
|
7136
|
+
prevSession._savedScrollbackStats = _terminalPlainTextStats(prevSession._savedScrollbackText);
|
|
7137
|
+
prevSession._savedScrollbackCapturedAt = Date.now();
|
|
6326
7138
|
} catch {}
|
|
6327
7139
|
// Overlay holds a DOM ref inside the xterm container; detach before disposing
|
|
6328
7140
|
// the terminal so it doesn't try to re-render against a dead _renderService.
|
|
@@ -6480,6 +7292,11 @@ function activateTab(id) {
|
|
|
6480
7292
|
if (!s.container || !document.getElementById('walle-session-' + id)) {
|
|
6481
7293
|
WalleSession.renderSession(id);
|
|
6482
7294
|
}
|
|
7295
|
+
if (s.needsAttach || !s._walleHistoryRequested) {
|
|
7296
|
+
s.needsAttach = false;
|
|
7297
|
+
s._walleHistoryRequested = true;
|
|
7298
|
+
send({ type: 'attach', id });
|
|
7299
|
+
}
|
|
6483
7300
|
s.container.classList.add('active');
|
|
6484
7301
|
history.replaceState(null, '', location.pathname + location.search + '#session=' + id);
|
|
6485
7302
|
savePref('active_session', id);
|
|
@@ -6508,6 +7325,11 @@ function activateTab(id) {
|
|
|
6508
7325
|
const savedCachedSnapshot = s._cachedSnapshot;
|
|
6509
7326
|
const savedCachedSnapshotCols = s._cachedSnapshotCols;
|
|
6510
7327
|
const savedCachedSnapshotRows = s._cachedSnapshotRows;
|
|
7328
|
+
const savedScrollbackText = s._savedScrollbackText;
|
|
7329
|
+
const savedScrollbackStats = s._savedScrollbackStats;
|
|
7330
|
+
const savedScrollbackCapturedAt = s._savedScrollbackCapturedAt;
|
|
7331
|
+
const savedCodexFinal = s._codexFinal;
|
|
7332
|
+
const savedCodexFinalDismissed = s._codexFinalDismissed;
|
|
6511
7333
|
// Make container visible first — term.open() needs non-zero dimensions
|
|
6512
7334
|
s.container.classList.add('active');
|
|
6513
7335
|
createTerminal(id, { active: true }); // replaces stub in state.sessions, cleans up old container
|
|
@@ -6527,8 +7349,14 @@ function activateTab(id) {
|
|
|
6527
7349
|
s2._cachedSnapshot = savedCachedSnapshot;
|
|
6528
7350
|
s2._cachedSnapshotCols = savedCachedSnapshotCols;
|
|
6529
7351
|
s2._cachedSnapshotRows = savedCachedSnapshotRows;
|
|
7352
|
+
s2._savedScrollbackText = savedScrollbackText;
|
|
7353
|
+
s2._savedScrollbackStats = savedScrollbackStats;
|
|
7354
|
+
s2._savedScrollbackCapturedAt = savedScrollbackCapturedAt;
|
|
7355
|
+
s2._codexFinal = savedCodexFinal;
|
|
7356
|
+
s2._codexFinalDismissed = savedCodexFinalDismissed;
|
|
6530
7357
|
s2.needsAttach = true;
|
|
6531
7358
|
s2.needsFontRefresh = true;
|
|
7359
|
+
_renderCodexFinalPanel(id);
|
|
6532
7360
|
}
|
|
6533
7361
|
}
|
|
6534
7362
|
// Re-read in case stub was replaced
|
|
@@ -6541,7 +7369,17 @@ function activateTab(id) {
|
|
|
6541
7369
|
|
|
6542
7370
|
const canRestoreExitedText = !!(sReal._exited && sReal._savedScrollbackText && sReal.term);
|
|
6543
7371
|
const hasCachedSnapshot = !!(sReal._cachedSnapshot && sReal.term);
|
|
6544
|
-
|
|
7372
|
+
const shouldRestoreSavedText = !!(!sReal._exited && sReal._savedScrollbackText && sReal.term
|
|
7373
|
+
&& (!hasCachedSnapshot || _snapshotLooksShorterThanSaved(sReal, sReal._cachedSnapshot)));
|
|
7374
|
+
const shouldShowInitialOverlay = typeof TerminalRestoreState !== 'undefined'
|
|
7375
|
+
? TerminalRestoreState.shouldShowInitialOverlay({
|
|
7376
|
+
hasTerminal: !!sReal.term,
|
|
7377
|
+
canRestoreExitedText,
|
|
7378
|
+
hasCachedSnapshot,
|
|
7379
|
+
shouldRestoreSavedText,
|
|
7380
|
+
})
|
|
7381
|
+
: (!canRestoreExitedText && !hasCachedSnapshot && !shouldRestoreSavedText && sReal.term);
|
|
7382
|
+
if (shouldShowInitialOverlay) {
|
|
6545
7383
|
// No cached snapshot — first-ever activation. Show loading overlay.
|
|
6546
7384
|
_showLoadingOverlay(sReal);
|
|
6547
7385
|
}
|
|
@@ -6581,14 +7419,23 @@ function activateTab(id) {
|
|
|
6581
7419
|
// match the local terminal. Otherwise wait for the fresh server snapshot
|
|
6582
7420
|
// requested below; painting an old-width full-screen TUI is what causes
|
|
6583
7421
|
// Codex/Claude output to appear garbled until a manual reflow.
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
7422
|
+
const cachedSnapshotDimsMatch = hasCachedSnapshot
|
|
7423
|
+
? _snapshotDimsMatchTerm(sReal, sReal._cachedSnapshotCols, sReal._cachedSnapshotRows)
|
|
7424
|
+
: false;
|
|
7425
|
+
const restoreDecision = typeof TerminalRestoreState !== 'undefined'
|
|
7426
|
+
? TerminalRestoreState.activationRestoreDecision({
|
|
7427
|
+
canRestoreExitedText,
|
|
7428
|
+
shouldRestoreSavedText,
|
|
7429
|
+
hasCachedSnapshot,
|
|
7430
|
+
cachedSnapshotDimsMatch,
|
|
7431
|
+
})
|
|
7432
|
+
: null;
|
|
7433
|
+
if ((restoreDecision?.action === 'restore-saved-scrollback' || !restoreDecision) && canRestoreExitedText && sReal._savedScrollbackText && sReal.term) {
|
|
7434
|
+
_restoreSavedScrollbackText(sReal, () => _ensureScrolledToBottom(sReal));
|
|
7435
|
+
} else if ((restoreDecision?.action === 'restore-saved-scrollback' || !restoreDecision) && shouldRestoreSavedText) {
|
|
7436
|
+
_restoreSavedScrollbackText(sReal, () => _ensureScrolledToBottom(sReal));
|
|
6590
7437
|
} else if (hasCachedSnapshot && sReal._cachedSnapshot && sReal.term) {
|
|
6591
|
-
if (
|
|
7438
|
+
if (restoreDecision?.action === 'restore-cached-snapshot' || (!restoreDecision && cachedSnapshotDimsMatch)) {
|
|
6592
7439
|
_restoreSnapshotData(sReal, sReal._cachedSnapshot, () => _ensureScrolledToBottom(sReal));
|
|
6593
7440
|
} else {
|
|
6594
7441
|
_showLoadingOverlay(sReal);
|
|
@@ -6621,6 +7468,7 @@ function activateTab(id) {
|
|
|
6621
7468
|
try { sReal.term.refresh(0, Math.max(0, sReal.term.rows - 1)); } catch {}
|
|
6622
7469
|
}
|
|
6623
7470
|
|
|
7471
|
+
_markClientUiRefreshOutputSuppression(sReal);
|
|
6624
7472
|
send({ type: 'resize', id, cols: sReal.term.cols, rows: sReal.term.rows });
|
|
6625
7473
|
sReal.writer.followMode = true;
|
|
6626
7474
|
sReal.writer._userScrollLocked = false;
|
|
@@ -6630,8 +7478,11 @@ function activateTab(id) {
|
|
|
6630
7478
|
// (We previously experimented with flushing this into the terminal first,
|
|
6631
7479
|
// but that races against the authoritative snapshot replay and can leave
|
|
6632
7480
|
// interleaved/corrupted state.)
|
|
6633
|
-
|
|
6634
|
-
|
|
7481
|
+
const requestDecision = typeof TerminalRestoreState !== 'undefined'
|
|
7482
|
+
? TerminalRestoreState.activationRequestDecision({ needsAttach: !!sReal.needsAttach })
|
|
7483
|
+
: { messageType: sReal.needsAttach ? 'attach' : 'snapshot', clearStaleQueue: true };
|
|
7484
|
+
if (requestDecision.clearStaleQueue) sReal.writer.queue = '';
|
|
7485
|
+
if (requestDecision.messageType === 'attach') {
|
|
6635
7486
|
// Attach already sends a snapshot response — no separate snapshot request needed.
|
|
6636
7487
|
sReal.needsAttach = false;
|
|
6637
7488
|
send({ type: 'attach', id, cols: sReal.term.cols, rows: sReal.term.rows });
|
|
@@ -6699,6 +7550,7 @@ function activateTab(id) {
|
|
|
6699
7550
|
sReal.term.clear();
|
|
6700
7551
|
sReal.term.clearTextureAtlas();
|
|
6701
7552
|
} catch {}
|
|
7553
|
+
_markClientUiRefreshOutputSuppression(sReal);
|
|
6702
7554
|
send({ type: 'reflow', id, cols: sReal.term.cols, rows: sReal.term.rows });
|
|
6703
7555
|
}, 6000);
|
|
6704
7556
|
focusTerminalIfSafe(id);
|
|
@@ -6785,6 +7637,233 @@ function updateTopbarContext(activeId) {
|
|
|
6785
7637
|
}
|
|
6786
7638
|
}
|
|
6787
7639
|
|
|
7640
|
+
const STANDUP_LANE_LABELS = {
|
|
7641
|
+
needs_user: 'Needs User',
|
|
7642
|
+
ready_review: 'Ready Review',
|
|
7643
|
+
running: 'Running',
|
|
7644
|
+
continue_later: 'Continue Later',
|
|
7645
|
+
};
|
|
7646
|
+
const SESSIONS_OVERVIEW_TAB_ID = '__sessions_overview__';
|
|
7647
|
+
|
|
7648
|
+
function standupEsc(value) {
|
|
7649
|
+
return escHtml(String(value == null ? '' : value));
|
|
7650
|
+
}
|
|
7651
|
+
|
|
7652
|
+
function standupStatusClass(status) {
|
|
7653
|
+
return 'status-' + String(status || 'unknown').toLowerCase().replace(/[^a-z0-9_-]/g, '-');
|
|
7654
|
+
}
|
|
7655
|
+
|
|
7656
|
+
function standupIsVisible() {
|
|
7657
|
+
const welcome = document.getElementById('welcome');
|
|
7658
|
+
return !!(welcome && welcome.style.display !== 'none');
|
|
7659
|
+
}
|
|
7660
|
+
|
|
7661
|
+
function isSessionsOverviewActive() {
|
|
7662
|
+
return state.activeTab === SESSIONS_OVERVIEW_TAB_ID && standupIsVisible();
|
|
7663
|
+
}
|
|
7664
|
+
|
|
7665
|
+
function showStandupDashboard(opts) {
|
|
7666
|
+
opts = opts || {};
|
|
7667
|
+
if (!opts.skipHash) {
|
|
7668
|
+
history.replaceState(null, '', location.pathname + location.search);
|
|
7669
|
+
}
|
|
7670
|
+
if (!opts.skipPersist) {
|
|
7671
|
+
state._savedActiveNav = 'sessions';
|
|
7672
|
+
savePref('active_nav', 'sessions');
|
|
7673
|
+
}
|
|
7674
|
+
for (const [, s] of state.sessions) {
|
|
7675
|
+
if (s.container) s.container.classList.remove('active');
|
|
7676
|
+
}
|
|
7677
|
+
['rules-panel', 'review-panel', 'codereview-panel', 'insights-panel', 'permissions-panel', 'prompts-panel', 'walle-panel'].forEach(id => {
|
|
7678
|
+
const el = document.getElementById(id);
|
|
7679
|
+
if (el) el.classList.remove('active');
|
|
7680
|
+
});
|
|
7681
|
+
['models-panel', 'backups-panel', 'worktrees-panel', 'setup-panel'].forEach(id => {
|
|
7682
|
+
const el = document.getElementById(id);
|
|
7683
|
+
if (el) { el.classList.remove('active'); el.style.display = 'none'; }
|
|
7684
|
+
});
|
|
7685
|
+
|
|
7686
|
+
state.activeTab = SESSIONS_OVERVIEW_TAB_ID;
|
|
7687
|
+
_subscribeVisibleSessions();
|
|
7688
|
+
const welcome = document.getElementById('welcome');
|
|
7689
|
+
if (welcome) welcome.style.display = 'flex';
|
|
7690
|
+
const sidebar = document.getElementById('sidebar');
|
|
7691
|
+
if (sidebar && !state.sidebarManuallyHidden) {
|
|
7692
|
+
sidebar.classList.remove('collapsed');
|
|
7693
|
+
document.getElementById('sidebar-resize').style.display = '';
|
|
7694
|
+
}
|
|
7695
|
+
const tabbar = document.getElementById('tabbar');
|
|
7696
|
+
if (tabbar) tabbar.style.display = '';
|
|
7697
|
+
const queueBtn = document.getElementById('topbar-queue-btn');
|
|
7698
|
+
if (queueBtn) queueBtn.style.display = 'none';
|
|
7699
|
+
const queuePanel = document.getElementById('queue-panel');
|
|
7700
|
+
const queueResize = document.getElementById('queue-panel-resize');
|
|
7701
|
+
if (queuePanel) queuePanel.style.display = 'none';
|
|
7702
|
+
if (queueResize) queueResize.style.display = 'none';
|
|
7703
|
+
state.queuePanelOpen = false;
|
|
7704
|
+
if (typeof updateQueueBtnHighlight === 'function') updateQueueBtnHighlight();
|
|
7705
|
+
|
|
7706
|
+
syncNavPills('sessions');
|
|
7707
|
+
updateTopbarContext('sessions');
|
|
7708
|
+
renderTabs();
|
|
7709
|
+
renderSessionList();
|
|
7710
|
+
loadStandupDashboard({ silent: !!state.standup.data });
|
|
7711
|
+
}
|
|
7712
|
+
|
|
7713
|
+
function refreshStandupIfVisible() {
|
|
7714
|
+
if (standupIsVisible()) loadStandupDashboard({ silent: !!state.standup.data });
|
|
7715
|
+
}
|
|
7716
|
+
|
|
7717
|
+
let _standupRefreshTimer = null;
|
|
7718
|
+
function scheduleStandupRefresh(delayMs = 600) {
|
|
7719
|
+
if (!standupIsVisible()) return;
|
|
7720
|
+
if (_standupRefreshTimer) return;
|
|
7721
|
+
_standupRefreshTimer = setTimeout(() => {
|
|
7722
|
+
_standupRefreshTimer = null;
|
|
7723
|
+
refreshStandupIfVisible();
|
|
7724
|
+
}, delayMs);
|
|
7725
|
+
}
|
|
7726
|
+
|
|
7727
|
+
async function loadStandupDashboard(opts) {
|
|
7728
|
+
opts = opts || {};
|
|
7729
|
+
if (state.standup.loading) return;
|
|
7730
|
+
state.standup.loading = true;
|
|
7731
|
+
const loading = document.getElementById('standup-loading');
|
|
7732
|
+
const error = document.getElementById('standup-error');
|
|
7733
|
+
if (loading && (!state.standup.data || !opts.silent)) loading.style.display = '';
|
|
7734
|
+
if (error) { error.style.display = 'none'; error.textContent = ''; }
|
|
7735
|
+
try {
|
|
7736
|
+
const url = '/api/sessions/standup' + (opts.force ? '?force=1' : '');
|
|
7737
|
+
const resp = await fetch(url);
|
|
7738
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
7739
|
+
const data = await resp.json();
|
|
7740
|
+
state.standup.data = data;
|
|
7741
|
+
state.standup.lastLoadedAt = Date.now();
|
|
7742
|
+
renderStandupDashboard(data);
|
|
7743
|
+
} catch (e) {
|
|
7744
|
+
if (error) {
|
|
7745
|
+
error.textContent = 'Standup refresh failed: ' + (e.message || e);
|
|
7746
|
+
error.style.display = '';
|
|
7747
|
+
}
|
|
7748
|
+
} finally {
|
|
7749
|
+
state.standup.loading = false;
|
|
7750
|
+
if (loading) loading.style.display = 'none';
|
|
7751
|
+
}
|
|
7752
|
+
}
|
|
7753
|
+
|
|
7754
|
+
function renderStandupDashboard(data) {
|
|
7755
|
+
const countsEl = document.getElementById('standup-counts');
|
|
7756
|
+
const updatedEl = document.getElementById('standup-updated');
|
|
7757
|
+
const attentionEl = document.getElementById('standup-attention');
|
|
7758
|
+
const lanesEl = document.getElementById('standup-lanes');
|
|
7759
|
+
const emptyEl = document.getElementById('standup-empty');
|
|
7760
|
+
if (!countsEl || !lanesEl) return;
|
|
7761
|
+
|
|
7762
|
+
const counts = data.counts || {};
|
|
7763
|
+
countsEl.innerHTML = [
|
|
7764
|
+
['total', 'Total'],
|
|
7765
|
+
['needs_user', 'Needs User'],
|
|
7766
|
+
['ready_review', 'Review'],
|
|
7767
|
+
['running', 'Running'],
|
|
7768
|
+
['continue_later', 'Later'],
|
|
7769
|
+
].map(([key, label]) => (
|
|
7770
|
+
`<span class="standup-count"><strong>${standupEsc(counts[key] || 0)}</strong>${standupEsc(label)}</span>`
|
|
7771
|
+
)).join('');
|
|
7772
|
+
if (updatedEl) updatedEl.textContent = data.generatedAt ? `Updated ${timeAgo(data.generatedAt)}` : '';
|
|
7773
|
+
|
|
7774
|
+
const laneSessions = (data.lanes || []).flatMap(lane => lane.sessions || []);
|
|
7775
|
+
const sessions = (data.sessions && data.sessions.length) ? data.sessions : laneSessions;
|
|
7776
|
+
const attention = sessions.find(s => s.lane === 'needs_user');
|
|
7777
|
+
if (attentionEl) {
|
|
7778
|
+
if (attention) {
|
|
7779
|
+
attentionEl.classList.add('active');
|
|
7780
|
+
attentionEl.innerHTML = `
|
|
7781
|
+
<div class="standup-attention-main">
|
|
7782
|
+
<div class="standup-attention-title">${standupEsc(attention.actionLabel)}: ${standupEsc(attention.title)}</div>
|
|
7783
|
+
<div class="standup-attention-body">${standupEsc(attention.recommendation)}</div>
|
|
7784
|
+
</div>
|
|
7785
|
+
<button class="standup-action-btn primary" type="button" data-standup-action="open" data-session-id="${standupEsc(attention.id)}">Open</button>
|
|
7786
|
+
`;
|
|
7787
|
+
} else {
|
|
7788
|
+
attentionEl.classList.remove('active');
|
|
7789
|
+
attentionEl.innerHTML = '';
|
|
7790
|
+
}
|
|
7791
|
+
}
|
|
7792
|
+
|
|
7793
|
+
if (emptyEl) emptyEl.style.display = sessions.length ? 'none' : '';
|
|
7794
|
+
lanesEl.innerHTML = (data.lanes || []).map(renderStandupLane).join('');
|
|
7795
|
+
}
|
|
7796
|
+
|
|
7797
|
+
function renderStandupLane(lane) {
|
|
7798
|
+
const sessions = lane.sessions || [];
|
|
7799
|
+
const body = sessions.length
|
|
7800
|
+
? sessions.map(renderStandupCard).join('')
|
|
7801
|
+
: '<div class="standup-card-text" style="padding:4px 2px;">Clear</div>';
|
|
7802
|
+
return `
|
|
7803
|
+
<section class="standup-lane" data-lane="${standupEsc(lane.id)}">
|
|
7804
|
+
<div class="standup-lane-header">
|
|
7805
|
+
<div class="standup-lane-title"><span class="standup-lane-dot"></span><span>${standupEsc(lane.title || STANDUP_LANE_LABELS[lane.id] || lane.id)}</span></div>
|
|
7806
|
+
<span class="standup-lane-count">${standupEsc(sessions.length)}</span>
|
|
7807
|
+
</div>
|
|
7808
|
+
<div class="standup-lane-body">${body}</div>
|
|
7809
|
+
</section>
|
|
7810
|
+
`;
|
|
7811
|
+
}
|
|
7812
|
+
|
|
7813
|
+
function renderStandupCard(card) {
|
|
7814
|
+
const subtitle = [card.agent, card.model || card.provider, card.branch].filter(Boolean).join(' / ');
|
|
7815
|
+
const progress = card.progress || card.intent || '';
|
|
7816
|
+
const canReview = card.capabilities && card.capabilities.review;
|
|
7817
|
+
const chips = (card.evidence || []).map(item => `<span class="standup-chip" title="${standupEsc(item)}">${standupEsc(item)}</span>`).join('');
|
|
7818
|
+
return `
|
|
7819
|
+
<article class="standup-card">
|
|
7820
|
+
<div class="standup-card-top">
|
|
7821
|
+
<div style="min-width:0;">
|
|
7822
|
+
<div class="standup-card-title">${standupEsc(card.title || card.id)}</div>
|
|
7823
|
+
<div class="standup-card-subtitle" title="${standupEsc(subtitle)}">${standupEsc(subtitle || card.cwd || '')}</div>
|
|
7824
|
+
</div>
|
|
7825
|
+
<span class="standup-badge ${standupStatusClass(card.status)}">${standupEsc(card.status || 'unknown')}</span>
|
|
7826
|
+
</div>
|
|
7827
|
+
<div class="standup-card-text"><strong>${standupEsc(card.actionLabel || 'Next')}</strong> ${standupEsc(card.recommendation || '')}</div>
|
|
7828
|
+
${progress ? `<div class="standup-card-text">${standupEsc(progress)}</div>` : ''}
|
|
7829
|
+
${chips ? `<div class="standup-evidence">${chips}</div>` : ''}
|
|
7830
|
+
<div class="standup-card-actions">
|
|
7831
|
+
<button class="standup-action-btn primary" type="button" data-standup-action="open" data-session-id="${standupEsc(card.id)}">Open</button>
|
|
7832
|
+
${canReview ? `<button class="standup-action-btn" type="button" data-standup-action="review" data-session-id="${standupEsc(card.id)}">Review</button>` : ''}
|
|
7833
|
+
</div>
|
|
7834
|
+
</article>
|
|
7835
|
+
`;
|
|
7836
|
+
}
|
|
7837
|
+
|
|
7838
|
+
function standupHandleDashboardClick(event) {
|
|
7839
|
+
const btn = event.target.closest('[data-standup-action]');
|
|
7840
|
+
if (!btn) return;
|
|
7841
|
+
const action = btn.dataset.standupAction;
|
|
7842
|
+
const sessionId = btn.dataset.sessionId;
|
|
7843
|
+
if (action === 'refresh') {
|
|
7844
|
+
loadStandupDashboard({ force: true });
|
|
7845
|
+
} else if (action === 'open') {
|
|
7846
|
+
standupOpenSession(sessionId);
|
|
7847
|
+
} else if (action === 'review') {
|
|
7848
|
+
standupReviewSession(sessionId);
|
|
7849
|
+
}
|
|
7850
|
+
}
|
|
7851
|
+
|
|
7852
|
+
function standupOpenSession(sessionId) {
|
|
7853
|
+
if (!sessionId || !state.sessions.has(sessionId)) {
|
|
7854
|
+
toast('Session is no longer active', { type: 'warning' });
|
|
7855
|
+
loadStandupDashboard({ force: true });
|
|
7856
|
+
return false;
|
|
7857
|
+
}
|
|
7858
|
+
activateTab(sessionId);
|
|
7859
|
+
return true;
|
|
7860
|
+
}
|
|
7861
|
+
|
|
7862
|
+
function standupReviewSession(sessionId) {
|
|
7863
|
+
if (!standupOpenSession(sessionId)) return;
|
|
7864
|
+
setTimeout(() => openSessionReview(sessionId), 0);
|
|
7865
|
+
}
|
|
7866
|
+
|
|
6788
7867
|
let _prevNav = null; // track previous nav section for Alt+Tab swap
|
|
6789
7868
|
|
|
6790
7869
|
function toggleNavMore() {
|
|
@@ -6802,23 +7881,45 @@ function _closeNavMoreOutside(e) {
|
|
|
6802
7881
|
if (!document.getElementById('nav-more-wrap').contains(e.target)) closeNavMore();
|
|
6803
7882
|
}
|
|
6804
7883
|
|
|
7884
|
+
function showSessionsWorkspace() {
|
|
7885
|
+
const candidates = [
|
|
7886
|
+
state.activeTab,
|
|
7887
|
+
state.lastActiveWorkSessionId,
|
|
7888
|
+
state._savedActiveSession,
|
|
7889
|
+
].filter(Boolean);
|
|
7890
|
+
for (const id of candidates) {
|
|
7891
|
+
if (state.sessions.has(id)) {
|
|
7892
|
+
activateTab(id);
|
|
7893
|
+
return true;
|
|
7894
|
+
}
|
|
7895
|
+
}
|
|
7896
|
+
const fromTabs = state.tabOrder.slice().reverse().find(id => state.sessions.has(id));
|
|
7897
|
+
if (fromTabs) {
|
|
7898
|
+
activateTab(fromTabs);
|
|
7899
|
+
return true;
|
|
7900
|
+
}
|
|
7901
|
+
showStandupDashboard();
|
|
7902
|
+
return false;
|
|
7903
|
+
}
|
|
7904
|
+
|
|
6805
7905
|
function navTo(target, opts) {
|
|
6806
7906
|
// Stop dictation when switching tabs (dictation follows focus)
|
|
6807
7907
|
if (typeof LI !== 'undefined' && LI.isRecording()) LI.stopRecording();
|
|
6808
7908
|
// Track previous nav for Alt+Tab toggle
|
|
6809
7909
|
const currentNav = document.querySelector('.nav-pill.active')?.dataset?.nav || 'sessions';
|
|
6810
|
-
|
|
7910
|
+
const effectiveTarget = target === 'command' ? 'sessions' : target;
|
|
7911
|
+
if (effectiveTarget !== currentNav) _prevNav = currentNav;
|
|
6811
7912
|
// Update URL hash
|
|
6812
7913
|
if (!opts || !opts.skipHash) {
|
|
6813
|
-
if (
|
|
7914
|
+
if (effectiveTarget === 'sessions') {
|
|
6814
7915
|
history.replaceState(null, '', location.pathname + location.search);
|
|
6815
7916
|
} else {
|
|
6816
|
-
history.replaceState(null, '', location.pathname + location.search + '#' +
|
|
7917
|
+
history.replaceState(null, '', location.pathname + location.search + '#' + effectiveTarget);
|
|
6817
7918
|
}
|
|
6818
7919
|
}
|
|
6819
7920
|
// Persist active nav target so refresh restores it
|
|
6820
7921
|
if (!opts || !opts.skipPersist) {
|
|
6821
|
-
savePref('active_nav',
|
|
7922
|
+
savePref('active_nav', effectiveTarget);
|
|
6822
7923
|
}
|
|
6823
7924
|
// Save per-tab deep state when navigating away
|
|
6824
7925
|
if (state.activeTab === 'prompts' && target !== 'prompts' && typeof PE !== 'undefined' && PE.state.currentPromptId) {
|
|
@@ -6831,40 +7932,12 @@ function navTo(target, opts) {
|
|
|
6831
7932
|
// overwrite the correct value (both are fire-and-forget async PUTs).
|
|
6832
7933
|
}
|
|
6833
7934
|
if (target === 'sessions') {
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
|
-
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
// Reset to welcome
|
|
6841
|
-
state.activeTab = null;
|
|
6842
|
-
document.getElementById('welcome').style.display = '';
|
|
6843
|
-
document.getElementById('rules-panel').classList.remove('active');
|
|
6844
|
-
document.getElementById('insights-panel').classList.remove('active');
|
|
6845
|
-
document.getElementById('permissions-panel').classList.remove('active');
|
|
6846
|
-
document.getElementById('prompts-panel').classList.remove('active');
|
|
6847
|
-
document.getElementById('review-panel').classList.remove('active');
|
|
6848
|
-
document.getElementById('codereview-panel').classList.remove('active');
|
|
6849
|
-
document.getElementById('walle-panel').classList.remove('active');
|
|
6850
|
-
var mdlPanel = document.getElementById('models-panel');
|
|
6851
|
-
if (mdlPanel) { mdlPanel.classList.remove('active'); mdlPanel.style.display = 'none'; }
|
|
6852
|
-
var bkPanel = document.getElementById('backups-panel');
|
|
6853
|
-
if (bkPanel) bkPanel.classList.remove('active');
|
|
6854
|
-
var stPanel = document.getElementById('setup-panel');
|
|
6855
|
-
if (stPanel) { stPanel.classList.remove('active'); stPanel.style.display = 'none'; }
|
|
6856
|
-
// Restore sidebar & tabbar
|
|
6857
|
-
const sidebar = document.getElementById('sidebar');
|
|
6858
|
-
if (!state.sidebarManuallyHidden) {
|
|
6859
|
-
sidebar.classList.remove('collapsed');
|
|
6860
|
-
document.getElementById('sidebar-resize').style.display = '';
|
|
6861
|
-
}
|
|
6862
|
-
document.getElementById('tabbar').style.display = '';
|
|
6863
|
-
syncNavPills('sessions');
|
|
6864
|
-
updateTopbarContext('sessions');
|
|
6865
|
-
renderTabs();
|
|
6866
|
-
renderSessionList();
|
|
6867
|
-
}
|
|
7935
|
+
showSessionsWorkspace();
|
|
7936
|
+
} else if (target === 'command') {
|
|
7937
|
+
// Backward-compatible alias for old #command links. The surface now lives as
|
|
7938
|
+
// the pinned Overview tab inside Sessions.
|
|
7939
|
+
state._forceSessionsOverview = true;
|
|
7940
|
+
showStandupDashboard({ skipHash: true, skipPersist: true });
|
|
6868
7941
|
} else if (target === 'prompts') {
|
|
6869
7942
|
openPromptEditor();
|
|
6870
7943
|
} else if (target === 'rules') {
|
|
@@ -9332,8 +10405,8 @@ function _restoreBackup(type, name) {
|
|
|
9332
10405
|
|
|
9333
10406
|
// Rolling cache of the last fetched worktree list so modals (merge/delete)
|
|
9334
10407
|
// can look up metadata by branch name without re-querying the server.
|
|
9335
|
-
var _wtCache = { repoRoot: '', items: [], counts: {} };
|
|
9336
|
-
var _wtModalState = { branch: '', name: '', mode: null, cwd: '' };
|
|
10408
|
+
var _wtCache = { repoRoot: '', items: [], counts: {}, namespace: 'claude' };
|
|
10409
|
+
var _wtModalState = { branch: '', name: '', mode: null, cwd: '', namespace: 'claude' };
|
|
9337
10410
|
var _wtCurrentFilter = 'all';
|
|
9338
10411
|
var _wtLoadSeq = 0;
|
|
9339
10412
|
var _wtRefreshSeq = 0;
|
|
@@ -9417,7 +10490,7 @@ function _wtRunRecommendedAction(wt) {
|
|
|
9417
10490
|
if (action.kind === 'open_session' || action.kind === 'review_dirty') return _wtOpenSessionFor(wt);
|
|
9418
10491
|
if (action.kind === 'prune') return submitPruneGhosts();
|
|
9419
10492
|
if (action.kind === 'recover_branch') return submitRecoverDetached(wt);
|
|
9420
|
-
if (action.kind === 'sync_branch') return
|
|
10493
|
+
if (action.kind === 'sync_branch') return _wtOpenSyncOrExplain(wt);
|
|
9421
10494
|
if (action.kind === 'finish_work') return _wtOpenMergeOrExplain(wt);
|
|
9422
10495
|
if (action.kind === 'cleanup') return openDeleteModal(wt);
|
|
9423
10496
|
if (action.kind === 'update_main' || action.kind === 'push_main' || action.kind === 'reconcile_main') {
|
|
@@ -9437,6 +10510,24 @@ function _wtRecommendedButton(wt) {
|
|
|
9437
10510
|
return btn;
|
|
9438
10511
|
}
|
|
9439
10512
|
|
|
10513
|
+
function _wtSyncBlockReason(wt) {
|
|
10514
|
+
if (!wt || wt.isMain) return '';
|
|
10515
|
+
if (wt.sessionId) return 'Close the active session before syncing from main.';
|
|
10516
|
+
if ((wt.dirtyFiles || 0) > 0) return 'Commit or stash dirty files before syncing from main.';
|
|
10517
|
+
if (!wt.branch || wt.branch === 'HEAD' || wt.state === 'detached') return 'Recover this worktree onto a branch before syncing from main.';
|
|
10518
|
+
if (wt.isGhost || wt.state === 'ghost') return 'Prune or recover this ghost worktree before syncing from main.';
|
|
10519
|
+
return '';
|
|
10520
|
+
}
|
|
10521
|
+
|
|
10522
|
+
function _wtOpenSyncOrExplain(wt) {
|
|
10523
|
+
var reason = _wtSyncBlockReason(wt);
|
|
10524
|
+
if (reason) {
|
|
10525
|
+
toast(reason, { type: 'warning', title: 'Sync unavailable', duration: 7000 });
|
|
10526
|
+
return;
|
|
10527
|
+
}
|
|
10528
|
+
openSyncModal(wt);
|
|
10529
|
+
}
|
|
10530
|
+
|
|
9440
10531
|
function _wtMetric(label, value, tone) {
|
|
9441
10532
|
var chip = document.createElement('span');
|
|
9442
10533
|
var color = tone === 'good' ? '#9ece6a' : tone === 'warn' ? '#e0af68' : tone === 'bad' ? '#f7768e' : 'var(--fg-dim,#a9b1d6)';
|
|
@@ -9467,10 +10558,27 @@ function _wtSyncAllEligible(wts) {
|
|
|
9467
10558
|
function _wtWorktreesListUrl(token) {
|
|
9468
10559
|
var params = new URLSearchParams();
|
|
9469
10560
|
params.set('token', token || '');
|
|
10561
|
+
var cwd = _wtCwdForRequest();
|
|
10562
|
+
if (cwd) params.set('cwd', cwd);
|
|
9470
10563
|
params.set('_wt_refresh', String(Date.now()) + '-' + (++_wtRefreshSeq));
|
|
9471
10564
|
return '/api/worktrees?' + params.toString();
|
|
9472
10565
|
}
|
|
9473
10566
|
|
|
10567
|
+
function _wtCwdForRequest() {
|
|
10568
|
+
var cwd = '';
|
|
10569
|
+
try {
|
|
10570
|
+
if (currentProjectFilter) cwd = currentProjectFilter;
|
|
10571
|
+
} catch (_) {}
|
|
10572
|
+
if (!cwd) {
|
|
10573
|
+
try { cwd = getLastSessionCwd(); } catch (_) {}
|
|
10574
|
+
}
|
|
10575
|
+
if (!cwd && _wtCache && _wtCache.repoRoot) cwd = _wtCache.repoRoot;
|
|
10576
|
+
if (cwd) {
|
|
10577
|
+
try { cwd = _stripWorktreePath(cwd); } catch (_) {}
|
|
10578
|
+
}
|
|
10579
|
+
return cwd || '';
|
|
10580
|
+
}
|
|
10581
|
+
|
|
9474
10582
|
function _wtSkippedReasonSummary(skippedBranches) {
|
|
9475
10583
|
var counts = {};
|
|
9476
10584
|
var rows = Array.isArray(skippedBranches) ? skippedBranches : [];
|
|
@@ -9515,7 +10623,7 @@ async function loadWorktreesPanel(opts) {
|
|
|
9515
10623
|
var d = await r.json();
|
|
9516
10624
|
if (requestId !== _wtLoadSeq) return;
|
|
9517
10625
|
var wts = d.worktrees || [];
|
|
9518
|
-
_wtCache = { repoRoot: d.cwd || '', items: wts, counts: d.counts || {}, mainRemote: d.mainRemote || null };
|
|
10626
|
+
_wtCache = { repoRoot: d.cwd || '', items: wts, counts: d.counts || {}, mainRemote: d.mainRemote || null, namespace: 'claude' };
|
|
9519
10627
|
|
|
9520
10628
|
var pruneBtn = document.getElementById('wt-prune-all-btn');
|
|
9521
10629
|
if (pruneBtn) pruneBtn.style.display = (d.counts && d.counts.ghost > 0) ? '' : 'none';
|
|
@@ -9681,10 +10789,23 @@ function _wtRenderCard(frag, wt) {
|
|
|
9681
10789
|
nameWrap.style.cssText = 'display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0;';
|
|
9682
10790
|
|
|
9683
10791
|
var branchEl = document.createElement('span');
|
|
9684
|
-
branchEl.style.cssText = 'font-weight:600;font-size:14px;color:var(--fg,#c0caf5);';
|
|
9685
|
-
|
|
10792
|
+
branchEl.style.cssText = 'font-weight:600;font-size:14px;color:var(--fg,#c0caf5);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:280px;';
|
|
10793
|
+
var branchInfo = branchDisplayParts(wt.branch || '');
|
|
10794
|
+
branchEl.textContent = branchInfo.full ? branchInfo.short : '(detached)';
|
|
10795
|
+
if (branchInfo.full) {
|
|
10796
|
+
branchEl.title = branchDisplayTitle(branchInfo.full);
|
|
10797
|
+
branchEl.dataset.branchFull = branchInfo.full;
|
|
10798
|
+
}
|
|
9686
10799
|
nameWrap.appendChild(branchEl);
|
|
9687
10800
|
|
|
10801
|
+
if (branchInfo.compactNamespace) {
|
|
10802
|
+
var nsBadge = document.createElement('span');
|
|
10803
|
+
nsBadge.style.cssText = 'font-size:10px;background:rgba(125,211,252,0.10);color:#7dd3fc;padding:1px 6px;border-radius:4px;max-width:86px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;';
|
|
10804
|
+
nsBadge.textContent = branchInfo.compactNamespace;
|
|
10805
|
+
nsBadge.title = branchDisplayTitle(branchInfo.full);
|
|
10806
|
+
nameWrap.appendChild(nsBadge);
|
|
10807
|
+
}
|
|
10808
|
+
|
|
9688
10809
|
var pill = document.createElement('span');
|
|
9689
10810
|
pill.style.cssText = 'font-size:10px;background:' + stateInfo.bg + ';color:' + stateInfo.color + ';padding:2px 8px;border-radius:10px;font-weight:500;cursor:default;';
|
|
9690
10811
|
pill.textContent = stateInfo.label;
|
|
@@ -9693,9 +10814,14 @@ function _wtRenderCard(frag, wt) {
|
|
|
9693
10814
|
pill.title = 'Click to merge into main';
|
|
9694
10815
|
pill.onclick = function() { _wtOpenMergeOrExplain(wt); };
|
|
9695
10816
|
} else if (wt.state === 'behind' || wt.state === 'diverged') {
|
|
9696
|
-
|
|
9697
|
-
|
|
9698
|
-
|
|
10817
|
+
var pillSyncBlockReason = _wtSyncBlockReason(wt);
|
|
10818
|
+
if (pillSyncBlockReason) {
|
|
10819
|
+
pill.title = pillSyncBlockReason;
|
|
10820
|
+
} else {
|
|
10821
|
+
pill.style.cursor = 'pointer';
|
|
10822
|
+
pill.title = 'Click to sync from main';
|
|
10823
|
+
pill.onclick = function() { openSyncModal(wt); };
|
|
10824
|
+
}
|
|
9699
10825
|
} else if (wt.state === 'ghost') {
|
|
9700
10826
|
pill.style.cursor = 'pointer';
|
|
9701
10827
|
pill.title = 'Click to prune all ghosts';
|
|
@@ -9707,7 +10833,7 @@ function _wtRenderCard(frag, wt) {
|
|
|
9707
10833
|
var nonCanon = document.createElement('span');
|
|
9708
10834
|
nonCanon.style.cssText = 'font-size:10px;background:rgba(224,175,104,0.12);color:#e0af68;padding:1px 6px;border-radius:4px;';
|
|
9709
10835
|
nonCanon.textContent = 'non-standard path';
|
|
9710
|
-
nonCanon.title = 'Lives outside .claude
|
|
10836
|
+
nonCanon.title = 'Lives outside agent-owned .claude/.walle worktrees — consider git worktree move';
|
|
9711
10837
|
nameWrap.appendChild(nonCanon);
|
|
9712
10838
|
}
|
|
9713
10839
|
if (stale && wt.state !== 'primary') {
|
|
@@ -9765,12 +10891,20 @@ function _wtRenderCard(frag, wt) {
|
|
|
9765
10891
|
}
|
|
9766
10892
|
|
|
9767
10893
|
if ((wt.state === 'behind' || wt.state === 'diverged') && (!wt.recommendedAction || wt.recommendedAction.kind !== 'sync_branch')) {
|
|
10894
|
+
var syncBlockReason = _wtSyncBlockReason(wt);
|
|
9768
10895
|
var syncBtn = document.createElement('button');
|
|
9769
10896
|
syncBtn.className = 'btn';
|
|
9770
10897
|
syncBtn.style.cssText = 'font-size:11px;padding:4px 10px;background:rgba(224,175,104,0.10);color:#e0af68;border:1px solid rgba(224,175,104,0.3);';
|
|
9771
|
-
|
|
9772
|
-
|
|
9773
|
-
|
|
10898
|
+
if (syncBlockReason) {
|
|
10899
|
+
syncBtn.textContent = 'Sync blocked';
|
|
10900
|
+
syncBtn.setAttribute('aria-disabled', 'true');
|
|
10901
|
+
syncBtn.title = syncBlockReason;
|
|
10902
|
+
syncBtn.onclick = function() { _wtOpenSyncOrExplain(wt); };
|
|
10903
|
+
} else {
|
|
10904
|
+
syncBtn.textContent = '↓ Sync';
|
|
10905
|
+
syncBtn.title = 'Pull main into this worktree';
|
|
10906
|
+
syncBtn.onclick = function() { openSyncModal(wt); };
|
|
10907
|
+
}
|
|
9774
10908
|
actions.appendChild(syncBtn);
|
|
9775
10909
|
}
|
|
9776
10910
|
|
|
@@ -9821,7 +10955,10 @@ function _wtRenderCard(frag, wt) {
|
|
|
9821
10955
|
|
|
9822
10956
|
var summaryEl = document.createElement('div');
|
|
9823
10957
|
summaryEl.style.cssText = 'font-size:12px;color:' + stateInfo.color + ';margin:2px 0 6px;font-weight:500;';
|
|
9824
|
-
|
|
10958
|
+
var summaryText = wt.summary || '';
|
|
10959
|
+
var summarySyncBlockReason = (wt.state === 'behind' || wt.state === 'diverged') ? _wtSyncBlockReason(wt) : '';
|
|
10960
|
+
if (summarySyncBlockReason) summaryText += (summaryText ? ' - ' : '') + summarySyncBlockReason;
|
|
10961
|
+
summaryEl.textContent = summaryText;
|
|
9825
10962
|
card.appendChild(summaryEl);
|
|
9826
10963
|
|
|
9827
10964
|
var metrics = document.createElement('div');
|
|
@@ -9863,6 +11000,11 @@ function _wtRenderCard(frag, wt) {
|
|
|
9863
11000
|
}
|
|
9864
11001
|
|
|
9865
11002
|
function openSyncModal(wt) {
|
|
11003
|
+
var syncBlockReason = _wtSyncBlockReason(wt);
|
|
11004
|
+
if (syncBlockReason) {
|
|
11005
|
+
toast(syncBlockReason, { type: 'warning', title: 'Sync unavailable', duration: 7000 });
|
|
11006
|
+
return;
|
|
11007
|
+
}
|
|
9866
11008
|
_wtModalState = { branch: wt.branch, name: wt.branch, mode: 'sync', cwd: _wtCache.repoRoot || '' };
|
|
9867
11009
|
var sum = document.getElementById('wt-sync-summary');
|
|
9868
11010
|
if (sum) sum.textContent = 'Pull main into "' + wt.branch + '" (' + wt.behind + ' commits behind).';
|
|
@@ -9947,7 +11089,10 @@ async function submitPruneGhosts() {
|
|
|
9947
11089
|
if (!confirm('Remove all ghost worktrees? This cleans up corrupt or missing worktree entries from git.')) return;
|
|
9948
11090
|
try {
|
|
9949
11091
|
var token = (state && state.token) ? state.token : '';
|
|
9950
|
-
var
|
|
11092
|
+
var params = new URLSearchParams();
|
|
11093
|
+
params.set('token', token);
|
|
11094
|
+
if (_wtCache.repoRoot) params.set('cwd', _wtCache.repoRoot);
|
|
11095
|
+
var r = await fetch('/api/worktrees/prune?' + params.toString(), { method: 'POST' });
|
|
9951
11096
|
var d = await r.json();
|
|
9952
11097
|
if (!r.ok || d.error) throw new Error(d.error || ('HTTP ' + r.status));
|
|
9953
11098
|
var n = (d.prunedPaths || []).length;
|
|
@@ -10208,7 +11353,7 @@ async function wtFinishAction(action) {
|
|
|
10208
11353
|
|
|
10209
11354
|
// ── Create modal ────────────────────────────────────────────────────
|
|
10210
11355
|
function showCreateWorktreeDialog() {
|
|
10211
|
-
_wtModalState = { branch: '', name: '', mode: 'create', cwd: _wtCache.repoRoot || '' };
|
|
11356
|
+
_wtModalState = { branch: '', name: '', mode: 'create', cwd: _wtCache.repoRoot || '', namespace: _wtCache.namespace || 'claude' };
|
|
10212
11357
|
var name = document.getElementById('wt-create-name'); if (name) name.value = '';
|
|
10213
11358
|
var base = document.getElementById('wt-create-base'); if (base) base.value = '';
|
|
10214
11359
|
var err = document.getElementById('wt-create-err'); if (err) err.textContent = '';
|
|
@@ -10223,7 +11368,8 @@ function _wtPreviewPath() {
|
|
|
10223
11368
|
var raw = (document.getElementById('wt-create-name') || {}).value || '';
|
|
10224
11369
|
var sanitized = _wtSanitize(raw);
|
|
10225
11370
|
var preview = document.getElementById('wt-create-path-preview');
|
|
10226
|
-
|
|
11371
|
+
var ns = (_wtModalState.namespace === 'walle') ? '.walle' : '.claude';
|
|
11372
|
+
if (preview) preview.textContent = ns + '/worktrees/' + (sanitized || '<name>');
|
|
10227
11373
|
var nameErr = document.getElementById('wt-create-name-err');
|
|
10228
11374
|
if (!nameErr) return;
|
|
10229
11375
|
if (raw && !sanitized) nameErr.textContent = 'Name must contain letters, digits, underscore, or hyphen.';
|
|
@@ -10244,6 +11390,7 @@ async function submitCreateWorktree() {
|
|
|
10244
11390
|
var body = { name: name };
|
|
10245
11391
|
if (base) body.base_branch = base;
|
|
10246
11392
|
if (_wtCache.repoRoot) body.cwd = _wtCache.repoRoot;
|
|
11393
|
+
if (_wtModalState.namespace) body.namespace = _wtModalState.namespace;
|
|
10247
11394
|
var token = (state && state.token) ? state.token : '';
|
|
10248
11395
|
var r = await fetch('/api/worktrees/create?token=' + encodeURIComponent(token), {
|
|
10249
11396
|
method: 'POST',
|
|
@@ -10431,14 +11578,14 @@ async function moveToWorktree(sessionId) {
|
|
|
10431
11578
|
var sess = state.sessions.get(sessionId);
|
|
10432
11579
|
if (!sess) return;
|
|
10433
11580
|
var cwd = (sess.meta && sess.meta.cwd) || '';
|
|
10434
|
-
if (!cwd || cwd
|
|
11581
|
+
if (!cwd || _nsIsAgentWorktreePath(cwd)) {
|
|
10435
11582
|
toast('Session is already in a worktree', { type: 'warning' });
|
|
10436
11583
|
return;
|
|
10437
11584
|
}
|
|
10438
11585
|
var sessLabel = (sess.meta && sess.meta.label) || '';
|
|
10439
11586
|
var defaultName = _wtSanitize(sessLabel.toLowerCase()) || ('wt-' + Date.now().toString(36));
|
|
10440
11587
|
// Reuse the create modal but with the session's cwd + prefilled name.
|
|
10441
|
-
_wtCache = { repoRoot: cwd, items: _wtCache.items };
|
|
11588
|
+
_wtCache = { repoRoot: cwd, items: _wtCache.items, namespace: (sess.meta && sess.meta.type === 'walle') ? 'walle' : 'claude' };
|
|
10442
11589
|
showCreateWorktreeDialog();
|
|
10443
11590
|
var nameInput = document.getElementById('wt-create-name');
|
|
10444
11591
|
if (nameInput) { nameInput.value = defaultName; _wtPreviewPath(); }
|
|
@@ -10460,7 +11607,17 @@ async function moveToWorktree(sessionId) {
|
|
|
10460
11607
|
function onCreated(msg) {
|
|
10461
11608
|
if (msg.sessionType === 'walle') {
|
|
10462
11609
|
const { id, label, cwd } = msg;
|
|
10463
|
-
const s = {
|
|
11610
|
+
const s = {
|
|
11611
|
+
meta: {
|
|
11612
|
+
label: label,
|
|
11613
|
+
cwd: cwd,
|
|
11614
|
+
type: 'walle',
|
|
11615
|
+
agentType: 'walle',
|
|
11616
|
+
model_id: msg.model_id || null,
|
|
11617
|
+
model_provider: msg.model_provider || null,
|
|
11618
|
+
},
|
|
11619
|
+
walleState: null,
|
|
11620
|
+
};
|
|
10464
11621
|
const container = document.createElement('div');
|
|
10465
11622
|
container.className = 'walle-session';
|
|
10466
11623
|
container.id = 'walle-session-' + id;
|
|
@@ -10484,7 +11641,7 @@ function onCreated(msg) {
|
|
|
10484
11641
|
if (s && !s.meta) {
|
|
10485
11642
|
s.meta = {
|
|
10486
11643
|
id,
|
|
10487
|
-
label,
|
|
11644
|
+
label: cleanSessionLabelForBranch(label, msg.branch || ''),
|
|
10488
11645
|
pid,
|
|
10489
11646
|
cwd,
|
|
10490
11647
|
cmd: msg.cmd || '',
|
|
@@ -10492,6 +11649,7 @@ function onCreated(msg) {
|
|
|
10492
11649
|
model_id: msg.model_id || null,
|
|
10493
11650
|
model_provider: msg.model_provider || null,
|
|
10494
11651
|
branch: msg.branch || null,
|
|
11652
|
+
userRenamed: !!msg.userRenamed,
|
|
10495
11653
|
agentType: msg.agentType || null,
|
|
10496
11654
|
agentCapabilities: msg.agentCapabilities || null,
|
|
10497
11655
|
claudeSessionId: msg.claudeSessionId || null,
|
|
@@ -10647,6 +11805,7 @@ function chunkedWrite(s, data, onDone) {
|
|
|
10647
11805
|
s.writer.scheduled = true;
|
|
10648
11806
|
requestAnimationFrame(() => {
|
|
10649
11807
|
const next = s.writer.queue;
|
|
11808
|
+
_clearStaleTerminalScrollLock(s);
|
|
10650
11809
|
const f = s.writer.followMode;
|
|
10651
11810
|
s.writer.queue = '';
|
|
10652
11811
|
s.writer.scheduled = false;
|
|
@@ -10673,7 +11832,7 @@ function onOutput(msg) {
|
|
|
10673
11832
|
return;
|
|
10674
11833
|
}
|
|
10675
11834
|
|
|
10676
|
-
let data = msg.data;
|
|
11835
|
+
let data = _normalizeCodexPromptBackground(s, msg.data);
|
|
10677
11836
|
|
|
10678
11837
|
// Direct-write bypass for keystroke echoes (ECHO_DIRECT_WRITE).
|
|
10679
11838
|
// If this is small output arriving shortly after input, and no chunking is in progress,
|
|
@@ -10693,8 +11852,11 @@ function onOutput(msg) {
|
|
|
10693
11852
|
// Keep local status activity aligned with the server's provider detectors.
|
|
10694
11853
|
// Claude/Codex idle redraws can leak printable "Running"/"Working" fragments;
|
|
10695
11854
|
// those should paint the terminal, but they must not keep status stuck Running.
|
|
10696
|
-
|
|
10697
|
-
|
|
11855
|
+
const suppressUiRefreshOutput = _shouldSuppressClientUiRefreshOutput(s, data);
|
|
11856
|
+
if (_isClientActiveOutput(s, data) && !suppressUiRefreshOutput && !(s._waitingForInput && _isClientCodexStatusOnlyOutput(s, data))) {
|
|
11857
|
+
const now = Date.now();
|
|
11858
|
+
s._lastOutputAt = now;
|
|
11859
|
+
if (!s._waitingForInput) _markClientCodexRunningEvidence(s, now, now);
|
|
10698
11860
|
// Don't clear _waitingForInput here — TUI redraws still leak visible chars
|
|
10699
11861
|
// (spinner glyphs, prompt text). Only server-sent 'session-resumed' should clear it.
|
|
10700
11862
|
}
|
|
@@ -10725,6 +11887,7 @@ function onOutput(msg) {
|
|
|
10725
11887
|
s.writer.scheduled = true;
|
|
10726
11888
|
requestAnimationFrame(() => {
|
|
10727
11889
|
const batch = s.writer.queue;
|
|
11890
|
+
_clearStaleTerminalScrollLock(s);
|
|
10728
11891
|
const follow = s.writer.followMode;
|
|
10729
11892
|
s.writer.queue = '';
|
|
10730
11893
|
s.writer.scheduled = false;
|
|
@@ -10787,19 +11950,8 @@ setInterval(() => {
|
|
|
10787
11950
|
if (!id) return;
|
|
10788
11951
|
const s = state.sessions.get(id);
|
|
10789
11952
|
if (!s || !s.term) return;
|
|
10790
|
-
const
|
|
10791
|
-
|
|
10792
|
-
// Fix 1: followMode is false but viewport IS at bottom — unlock
|
|
10793
|
-
if (!s.writer.followMode && atBottom && !s.writer._chunking) {
|
|
10794
|
-
s.writer.followMode = true;
|
|
10795
|
-
s.writer._userScrollLocked = false;
|
|
10796
|
-
}
|
|
10797
|
-
|
|
10798
|
-
// Fix 2: _userScrollLocked is true but viewport is at bottom — unlock
|
|
10799
|
-
if (s.writer._userScrollLocked && atBottom) {
|
|
10800
|
-
s.writer._userScrollLocked = false;
|
|
10801
|
-
s.writer.followMode = true;
|
|
10802
|
-
}
|
|
11953
|
+
const unlockedAtBottom = _clearStaleTerminalScrollLock(s);
|
|
11954
|
+
if (unlockedAtBottom) _ensureScrolledToBottom(s);
|
|
10803
11955
|
|
|
10804
11956
|
// Fix 3: queue has data, nothing is draining it — force flush
|
|
10805
11957
|
if (s.writer.queue && s.writer.queue.length > 0 && !s.writer.scheduled && !s.writer._chunking) {
|
|
@@ -10847,7 +11999,68 @@ function _terminalFollowViewportTarget(s) {
|
|
|
10847
11999
|
const blankTailRows = screenEnd - anchor;
|
|
10848
12000
|
const threshold = Math.max(6, Math.floor(rows * 0.20));
|
|
10849
12001
|
if (blankTailRows < threshold) return baseY;
|
|
10850
|
-
|
|
12002
|
+
const target = Math.max(0, Math.min(baseY, anchor - rows + 1));
|
|
12003
|
+
return _codexViewportHasPromptGaps(s, target) ? baseY : target;
|
|
12004
|
+
}
|
|
12005
|
+
|
|
12006
|
+
function _codexViewportHasPromptGaps(s, start) {
|
|
12007
|
+
if (!s || !s.term) return false;
|
|
12008
|
+
const buf = s.term.buffer.active;
|
|
12009
|
+
const rows = s.term.rows || 0;
|
|
12010
|
+
if (rows <= 0) return false;
|
|
12011
|
+
const first = Math.max(0, start || 0);
|
|
12012
|
+
const last = first + rows - 1;
|
|
12013
|
+
let promptCount = 0;
|
|
12014
|
+
let maxBlankRun = 0;
|
|
12015
|
+
let currentBlankRun = 0;
|
|
12016
|
+
let seenText = false;
|
|
12017
|
+
for (let row = first; row <= last; row++) {
|
|
12018
|
+
const line = buf.getLine(row);
|
|
12019
|
+
const text = line ? line.translateToString(true) : '';
|
|
12020
|
+
const trimmed = text.trim();
|
|
12021
|
+
if (trimmed.startsWith('›')) promptCount += 1;
|
|
12022
|
+
if (trimmed) {
|
|
12023
|
+
if (seenText) maxBlankRun = Math.max(maxBlankRun, currentBlankRun);
|
|
12024
|
+
seenText = true;
|
|
12025
|
+
currentBlankRun = 0;
|
|
12026
|
+
} else if (seenText) {
|
|
12027
|
+
currentBlankRun += 1;
|
|
12028
|
+
}
|
|
12029
|
+
}
|
|
12030
|
+
const threshold = Math.max(8, Math.floor(rows * 0.18));
|
|
12031
|
+
return promptCount >= 2 && maxBlankRun >= threshold;
|
|
12032
|
+
}
|
|
12033
|
+
|
|
12034
|
+
function _alignXtermHelperTextareaToViewport(s) {
|
|
12035
|
+
if (!s || !s.term || !s.container) return;
|
|
12036
|
+
if (_clientAgentTypeForSession(s) !== 'codex') return;
|
|
12037
|
+
const buf = s.term.buffer.active;
|
|
12038
|
+
const baseY = buf.baseY || 0;
|
|
12039
|
+
const viewportY = buf.viewportY || 0;
|
|
12040
|
+
const visualRow = baseY + (buf.cursorY || 0) - viewportY;
|
|
12041
|
+
if (visualRow < 0 || visualRow >= (s.term.rows || 0)) return;
|
|
12042
|
+
const ta = s.container.querySelector('.xterm-helper-textarea');
|
|
12043
|
+
if (!ta) return;
|
|
12044
|
+
let cellHeight = 0;
|
|
12045
|
+
let cellWidth = 0;
|
|
12046
|
+
try {
|
|
12047
|
+
const cell = s.term._core?._renderService?.dimensions?.css?.cell || {};
|
|
12048
|
+
cellHeight = cell.height || 0;
|
|
12049
|
+
cellWidth = cell.width || 0;
|
|
12050
|
+
} catch {}
|
|
12051
|
+
if (!cellHeight || !cellWidth) {
|
|
12052
|
+
const screen = s.container.querySelector('.xterm-screen');
|
|
12053
|
+
if (screen) {
|
|
12054
|
+
const rect = screen.getBoundingClientRect();
|
|
12055
|
+
cellHeight = cellHeight || (s.term.rows ? rect.height / s.term.rows : 0);
|
|
12056
|
+
cellWidth = cellWidth || (s.term.cols ? rect.width / s.term.cols : 0);
|
|
12057
|
+
}
|
|
12058
|
+
}
|
|
12059
|
+
if (!cellHeight || !cellWidth) return;
|
|
12060
|
+
ta.style.left = ((buf.cursorX || 0) * cellWidth) + 'px';
|
|
12061
|
+
ta.style.top = (visualRow * cellHeight) + 'px';
|
|
12062
|
+
ta.style.height = cellHeight + 'px';
|
|
12063
|
+
ta.style.lineHeight = cellHeight + 'px';
|
|
10851
12064
|
}
|
|
10852
12065
|
|
|
10853
12066
|
function _withProgrammaticTerminalScroll(s, fn) {
|
|
@@ -10874,6 +12087,7 @@ function _scrollTerminalToFollowBottom(s) {
|
|
|
10874
12087
|
try { vp.scrollTop = vp.scrollHeight; } catch {}
|
|
10875
12088
|
}
|
|
10876
12089
|
});
|
|
12090
|
+
requestAnimationFrame(() => _alignXtermHelperTextareaToViewport(s));
|
|
10877
12091
|
}
|
|
10878
12092
|
|
|
10879
12093
|
function _isAtTerminalFollowBottom(s) {
|
|
@@ -10885,10 +12099,19 @@ function _isAtTerminalFollowBottom(s) {
|
|
|
10885
12099
|
return Math.abs(viewportY - target) <= 1 || Math.abs(viewportY - baseY) <= 1;
|
|
10886
12100
|
}
|
|
10887
12101
|
|
|
12102
|
+
function _clearStaleTerminalScrollLock(s) {
|
|
12103
|
+
if (!s || !s.term || !s.writer || s.writer._chunking) return false;
|
|
12104
|
+
if ((s.writer._userScrollLocked || !s.writer.followMode) && _isAtTerminalFollowBottom(s)) {
|
|
12105
|
+
s.writer._userScrollLocked = false;
|
|
12106
|
+
s.writer.followMode = true;
|
|
12107
|
+
return true;
|
|
12108
|
+
}
|
|
12109
|
+
return false;
|
|
12110
|
+
}
|
|
12111
|
+
|
|
10888
12112
|
function _findCodexInternalBlankGap(s) {
|
|
10889
12113
|
if (!s || !s.term) return null;
|
|
10890
12114
|
if (_clientAgentTypeForSession(s) !== 'codex') return null;
|
|
10891
|
-
if (s.writer && s.writer._userScrollLocked) return null;
|
|
10892
12115
|
const buf = s.term.buffer.active;
|
|
10893
12116
|
const rows = s.term.rows || 0;
|
|
10894
12117
|
const baseY = buf.baseY || 0;
|
|
@@ -10896,25 +12119,30 @@ function _findCodexInternalBlankGap(s) {
|
|
|
10896
12119
|
|
|
10897
12120
|
const meaningful = [];
|
|
10898
12121
|
let promptAbs = -1;
|
|
10899
|
-
let
|
|
12122
|
+
let promptCount = 0;
|
|
12123
|
+
let hasNonStatusAfterPrompt = false;
|
|
10900
12124
|
for (let offset = 0; offset < rows; offset++) {
|
|
10901
12125
|
const abs = baseY + offset;
|
|
10902
12126
|
const line = buf.getLine(abs);
|
|
10903
12127
|
const text = line ? line.translateToString(true) : '';
|
|
10904
|
-
if (text.trim().startsWith('›'))
|
|
12128
|
+
if (text.trim().startsWith('›')) {
|
|
12129
|
+
promptAbs = abs;
|
|
12130
|
+
promptCount += 1;
|
|
12131
|
+
}
|
|
10905
12132
|
if (text.trim()) meaningful.push(abs);
|
|
10906
12133
|
}
|
|
10907
12134
|
if (promptAbs < 0 || meaningful.length < 2) return null;
|
|
12135
|
+
if (_codexHasActiveSkillPickerAfterPrompt(s, meaningful, promptAbs)) return null;
|
|
10908
12136
|
for (const abs of meaningful) {
|
|
10909
12137
|
if (abs <= promptAbs) continue;
|
|
10910
12138
|
const line = buf.getLine(abs);
|
|
10911
12139
|
const text = line ? line.translateToString(true).trim() : '';
|
|
10912
12140
|
if (!text) continue;
|
|
10913
12141
|
if (/^gpt-[\w.-]+\s+/i.test(text) || /\b(x?high|medium|low)\b.*\s-\s/.test(text)) continue;
|
|
10914
|
-
|
|
12142
|
+
hasNonStatusAfterPrompt = true;
|
|
10915
12143
|
break;
|
|
10916
12144
|
}
|
|
10917
|
-
if (!
|
|
12145
|
+
if (!hasNonStatusAfterPrompt && promptCount < 2) return null;
|
|
10918
12146
|
|
|
10919
12147
|
let best = null;
|
|
10920
12148
|
for (let i = 1; i < meaningful.length; i++) {
|
|
@@ -10932,6 +12160,22 @@ function _findCodexInternalBlankGap(s) {
|
|
|
10932
12160
|
return { startAbs: best.startAbs, deleteRows };
|
|
10933
12161
|
}
|
|
10934
12162
|
|
|
12163
|
+
function _codexHasActiveSkillPickerAfterPrompt(s, meaningfulRows, promptAbs) {
|
|
12164
|
+
if (!s || !s.term || !Array.isArray(meaningfulRows)) return false;
|
|
12165
|
+
const buf = s.term.buffer.active;
|
|
12166
|
+
let sawSkill = false;
|
|
12167
|
+
let sawHint = false;
|
|
12168
|
+
for (const abs of meaningfulRows) {
|
|
12169
|
+
if (abs <= promptAbs) continue;
|
|
12170
|
+
const line = buf.getLine(abs);
|
|
12171
|
+
const text = line ? line.translateToString(true).trim() : '';
|
|
12172
|
+
if (!text) continue;
|
|
12173
|
+
if (/\[Skill\]/.test(text)) sawSkill = true;
|
|
12174
|
+
if (/^press\s+enter\s+to\s+insert(?:\s+or\s+esc\s+to\s+close)?$/i.test(text)) sawHint = true;
|
|
12175
|
+
}
|
|
12176
|
+
return sawSkill && sawHint;
|
|
12177
|
+
}
|
|
12178
|
+
|
|
10935
12179
|
function _compactCodexInternalBlankGap(s, onDone) {
|
|
10936
12180
|
if (!s || !s.term || s._codexBlankGapCompacting) return false;
|
|
10937
12181
|
const gap = _findCodexInternalBlankGap(s);
|
|
@@ -10970,17 +12214,22 @@ function _compactCodexInternalBlankGap(s, onDone) {
|
|
|
10970
12214
|
// most terminals this is xterm's literal bottom. For Codex restored TUI
|
|
10971
12215
|
// screens, it is the last meaningful row before a large blank tail.
|
|
10972
12216
|
//
|
|
10973
|
-
// Skip if the user has manually scrolled
|
|
10974
|
-
// to yank them back
|
|
12217
|
+
// Skip if the user has manually scrolled away from the active bottom — we don't
|
|
12218
|
+
// want to yank them back while they are reading scrollback.
|
|
10975
12219
|
function _ensureScrolledToBottom(s) {
|
|
10976
12220
|
if (!s || !s.term) return;
|
|
10977
|
-
|
|
12221
|
+
_clearStaleTerminalScrollLock(s);
|
|
12222
|
+
// Repair a corrupted Codex current screen even if follow mode is locked.
|
|
12223
|
+
// The scroll lock should prevent viewport jumps, not preserve synthetic blank
|
|
12224
|
+
// rows left by TUI clear/redraw races.
|
|
10978
12225
|
if (_compactCodexInternalBlankGap(s, () => _ensureScrolledToBottom(s))) return;
|
|
12226
|
+
if (s.writer && s.writer._userScrollLocked) return;
|
|
10979
12227
|
_scrollTerminalToFollowBottom(s);
|
|
10980
12228
|
requestAnimationFrame(() => {
|
|
10981
12229
|
if (!s.term) return;
|
|
10982
12230
|
if (s.writer && s.writer._userScrollLocked) return;
|
|
10983
12231
|
_scrollTerminalToFollowBottom(s);
|
|
12232
|
+
requestAnimationFrame(() => _alignXtermHelperTextareaToViewport(s));
|
|
10984
12233
|
});
|
|
10985
12234
|
}
|
|
10986
12235
|
|
|
@@ -11016,8 +12265,69 @@ function _snapshotDimsMatchTerm(s, cols, rows) {
|
|
|
11016
12265
|
return c === s.term.cols && r === s.term.rows;
|
|
11017
12266
|
}
|
|
11018
12267
|
|
|
12268
|
+
function _terminalPlainTextStats(text) {
|
|
12269
|
+
const raw = String(text || '');
|
|
12270
|
+
const lines = raw.split(/\r\n|\n|\r/);
|
|
12271
|
+
let nonBlank = 0;
|
|
12272
|
+
let printableChars = 0;
|
|
12273
|
+
let first = '';
|
|
12274
|
+
let last = '';
|
|
12275
|
+
for (const line of lines) {
|
|
12276
|
+
const trimmed = String(line || '').trim();
|
|
12277
|
+
if (!trimmed) continue;
|
|
12278
|
+
nonBlank++;
|
|
12279
|
+
printableChars += trimmed.length;
|
|
12280
|
+
if (!first) first = trimmed;
|
|
12281
|
+
last = trimmed;
|
|
12282
|
+
}
|
|
12283
|
+
return { nonBlank, printableChars, first, last };
|
|
12284
|
+
}
|
|
12285
|
+
|
|
12286
|
+
function _terminalAnsiTextStats(data) {
|
|
12287
|
+
const text = String(data || '')
|
|
12288
|
+
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '')
|
|
12289
|
+
.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, (seq) => {
|
|
12290
|
+
const final = seq[seq.length - 1];
|
|
12291
|
+
return (final === 'H' || final === 'f') ? '\n' : '';
|
|
12292
|
+
})
|
|
12293
|
+
.replace(/\x1bc/g, '\n')
|
|
12294
|
+
.replace(/\r\n/g, '\n')
|
|
12295
|
+
.replace(/\r/g, '\n')
|
|
12296
|
+
.replace(/[\x00-\x08\x0b-\x1f\x7f]/g, '');
|
|
12297
|
+
return _terminalPlainTextStats(text);
|
|
12298
|
+
}
|
|
12299
|
+
|
|
12300
|
+
function _snapshotLooksShorterThanSaved(s, data) {
|
|
12301
|
+
if (!s || !s._savedScrollbackText || !data) return false;
|
|
12302
|
+
const saved = s._savedScrollbackStats || _terminalPlainTextStats(s._savedScrollbackText);
|
|
12303
|
+
if (!saved || saved.nonBlank < 20 || saved.printableChars < 500) return false;
|
|
12304
|
+
// The tab-away capture is only a guard for the restore immediately after
|
|
12305
|
+
// disposal. Do not let an old local copy override future authoritative
|
|
12306
|
+
// snapshots after the session has moved on.
|
|
12307
|
+
if (s._savedScrollbackCapturedAt && Date.now() - s._savedScrollbackCapturedAt > 5 * 60 * 1000) return false;
|
|
12308
|
+
const snapshot = _terminalAnsiTextStats(data);
|
|
12309
|
+
if (!snapshot) return false;
|
|
12310
|
+
const lostManyLines = snapshot.nonBlank < Math.floor(saved.nonBlank * 0.65);
|
|
12311
|
+
const lostManyChars = snapshot.printableChars < Math.floor(saved.printableChars * 0.75);
|
|
12312
|
+
return lostManyLines && lostManyChars && (saved.nonBlank - snapshot.nonBlank) >= 12;
|
|
12313
|
+
}
|
|
12314
|
+
|
|
12315
|
+
function _restoreSavedScrollbackText(s, onDone) {
|
|
12316
|
+
if (!s || !s.term || !s._savedScrollbackText) { if (onDone) onDone(); return; }
|
|
12317
|
+
try { s.term.clearTextureAtlas(); } catch {}
|
|
12318
|
+
try {
|
|
12319
|
+
if (s.writer) {
|
|
12320
|
+
s.writer.queue = '';
|
|
12321
|
+
s.writer._snapshotGen = (s.writer._snapshotGen || 0) + 1;
|
|
12322
|
+
}
|
|
12323
|
+
} catch {}
|
|
12324
|
+
const text = s._savedScrollbackText + '\r\n';
|
|
12325
|
+
s.term.write(TERMINAL_FULL_RESET + text, onDone);
|
|
12326
|
+
}
|
|
12327
|
+
|
|
11019
12328
|
function _restoreSnapshotData(s, data, onDone) {
|
|
11020
12329
|
if (!s || !s.term || !data) { if (onDone) onDone(); return; }
|
|
12330
|
+
data = _normalizeCodexPromptBackground(s, data);
|
|
11021
12331
|
try { s.term.clearTextureAtlas(); } catch {}
|
|
11022
12332
|
try {
|
|
11023
12333
|
if (s.writer) {
|
|
@@ -11134,6 +12444,7 @@ function onSnapshot(msg) {
|
|
|
11134
12444
|
const ptyRows = msg.ptyRows || msg.rows || localRows;
|
|
11135
12445
|
const dimsMismatch = localCols !== ptyCols || localRows !== ptyRows;
|
|
11136
12446
|
if (dimsMismatch) {
|
|
12447
|
+
_markClientUiRefreshOutputSuppression(s);
|
|
11137
12448
|
send({ type: 'resize', id: msg.id, cols: localCols, rows: localRows });
|
|
11138
12449
|
_showLoadingOverlay(s);
|
|
11139
12450
|
if (!s._dimFixPending) {
|
|
@@ -11141,6 +12452,7 @@ function onSnapshot(msg) {
|
|
|
11141
12452
|
setTimeout(() => {
|
|
11142
12453
|
s._dimFixPending = false;
|
|
11143
12454
|
if (state.activeTab === msg.id || isSessionVisibleInSplit(msg.id)) {
|
|
12455
|
+
_markClientUiRefreshOutputSuppression(s);
|
|
11144
12456
|
send({ type: 'reflow', id: msg.id, cols: s.term.cols, rows: s.term.rows });
|
|
11145
12457
|
}
|
|
11146
12458
|
}, 500);
|
|
@@ -11166,7 +12478,11 @@ function onSnapshot(msg) {
|
|
|
11166
12478
|
_forceTerminalPaint(s);
|
|
11167
12479
|
scanPromptLines(msg.id);
|
|
11168
12480
|
};
|
|
11169
|
-
|
|
12481
|
+
if (_snapshotLooksShorterThanSaved(s, msg.data)) {
|
|
12482
|
+
_restoreSavedScrollbackText(s, snapshotDone);
|
|
12483
|
+
} else {
|
|
12484
|
+
_restoreSnapshotData(s, msg.data, snapshotDone);
|
|
12485
|
+
}
|
|
11170
12486
|
}
|
|
11171
12487
|
|
|
11172
12488
|
// --- Loading overlay for tab-switch restore ---
|
|
@@ -11259,13 +12575,14 @@ async function onSessionsList(msg) {
|
|
|
11259
12575
|
|
|
11260
12576
|
// Add tabs for sessions we don't have terminals for (reconnect scenario)
|
|
11261
12577
|
for (const sess of msg.sessions) {
|
|
12578
|
+
if (sess && sess.label) sess.label = cleanSessionLabelForBranch(sess.label, sess.branch || '');
|
|
11262
12579
|
if (!state.sessions.has(sess.id)) {
|
|
11263
12580
|
if (sess.type === 'walle') {
|
|
11264
12581
|
const container = document.createElement('div');
|
|
11265
12582
|
container.className = 'walle-session';
|
|
11266
12583
|
container.id = 'walle-session-' + sess.id;
|
|
11267
12584
|
document.getElementById('terminal-area').appendChild(container);
|
|
11268
|
-
const s = { meta: { ...sess, type: 'walle' }, walleState: null, container: container };
|
|
12585
|
+
const s = { meta: { ...sess, type: 'walle' }, walleState: null, container: container, needsAttach: true };
|
|
11269
12586
|
state.sessions.set(sess.id, s);
|
|
11270
12587
|
WalleSession.renderSession(sess.id);
|
|
11271
12588
|
} else {
|
|
@@ -11284,6 +12601,11 @@ async function onSessionsList(msg) {
|
|
|
11284
12601
|
if (existing) {
|
|
11285
12602
|
existing.meta = sess;
|
|
11286
12603
|
if (sess.type === 'walle') existing.meta.type = 'walle';
|
|
12604
|
+
const liveStatus = normalizeLiveSessionStatus(sess.liveStatus);
|
|
12605
|
+
if (liveStatus) {
|
|
12606
|
+
existing._serverLiveStatus = liveStatus;
|
|
12607
|
+
existing._serverLiveStatusAt = SessionActivityUtils.parseTimeMs(sess.liveStatusAt) || Date.now();
|
|
12608
|
+
}
|
|
11287
12609
|
}
|
|
11288
12610
|
}
|
|
11289
12611
|
|
|
@@ -11325,6 +12647,7 @@ async function onSessionsList(msg) {
|
|
|
11325
12647
|
|
|
11326
12648
|
// Refresh queue builder session list
|
|
11327
12649
|
if (typeof refreshQpSessionList === 'function') refreshQpSessionList();
|
|
12650
|
+
if (typeof refreshStandupIfVisible === 'function') refreshStandupIfVisible();
|
|
11328
12651
|
|
|
11329
12652
|
// Auto-activate from hash (only for active PTY sessions).
|
|
11330
12653
|
// During post-restart, defer activation to onServerReady() which has the correct
|
|
@@ -11485,6 +12808,164 @@ function worktreeAttentionBadge(s) {
|
|
|
11485
12808
|
return `<span class="worktree-attn-badge" title="${escHtml(title)}" aria-label="${escHtml(title)}">${parts.join('')}</span>`;
|
|
11486
12809
|
}
|
|
11487
12810
|
|
|
12811
|
+
function escapeRegExpText(value) {
|
|
12812
|
+
return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
12813
|
+
}
|
|
12814
|
+
|
|
12815
|
+
function branchDisplayParts(branch) {
|
|
12816
|
+
const full = String(branch || '').trim().replace(/^refs\/heads\//, '');
|
|
12817
|
+
if (!full) {
|
|
12818
|
+
return { full: '', short: '', namespace: '', displayNamespace: '', compactNamespace: '', isMain: false, hasNamespace: false };
|
|
12819
|
+
}
|
|
12820
|
+
const isMain = full === 'main' || full === 'master';
|
|
12821
|
+
const parts = full.split('/').filter(Boolean);
|
|
12822
|
+
const short = parts.length > 0 ? parts[parts.length - 1] : full;
|
|
12823
|
+
const namespaceParts = parts.length > 1 ? parts.slice(0, -1) : [];
|
|
12824
|
+
const displayNamespaceParts = namespaceParts[0] === 'ctm' ? namespaceParts.slice(1) : namespaceParts;
|
|
12825
|
+
const abbrev = (segment) => {
|
|
12826
|
+
const s = String(segment || '').trim();
|
|
12827
|
+
const key = s.toLowerCase();
|
|
12828
|
+
const known = {
|
|
12829
|
+
codex: 'cx',
|
|
12830
|
+
'wall-e': 'we',
|
|
12831
|
+
walle: 'we',
|
|
12832
|
+
claude: 'cc',
|
|
12833
|
+
'claude-code': 'cc',
|
|
12834
|
+
opencode: 'oc',
|
|
12835
|
+
'open-code': 'oc',
|
|
12836
|
+
gemini: 'gm',
|
|
12837
|
+
};
|
|
12838
|
+
if (known[key]) return known[key];
|
|
12839
|
+
if (s.length <= 6) return s;
|
|
12840
|
+
const initials = s.split(/[-_.\s]+/).filter(Boolean).map(p => p[0]).join('');
|
|
12841
|
+
if (initials.length >= 2 && initials.length <= 4) return initials.toLowerCase();
|
|
12842
|
+
return s.slice(0, 2).toLowerCase();
|
|
12843
|
+
};
|
|
12844
|
+
const namespace = namespaceParts.join('/');
|
|
12845
|
+
const displayNamespace = displayNamespaceParts.join('/');
|
|
12846
|
+
const compactNamespace = displayNamespaceParts.map(abbrev).filter(Boolean).join('/');
|
|
12847
|
+
return {
|
|
12848
|
+
full,
|
|
12849
|
+
short,
|
|
12850
|
+
namespace,
|
|
12851
|
+
displayNamespace,
|
|
12852
|
+
compactNamespace,
|
|
12853
|
+
isMain,
|
|
12854
|
+
hasNamespace: namespaceParts.length > 0,
|
|
12855
|
+
};
|
|
12856
|
+
}
|
|
12857
|
+
|
|
12858
|
+
function branchDisplayTitle(branch) {
|
|
12859
|
+
const info = branchDisplayParts(branch);
|
|
12860
|
+
if (!info.full) return '';
|
|
12861
|
+
const parts = ['Branch: ' + info.full];
|
|
12862
|
+
if (info.displayNamespace) parts.push('Namespace: ' + info.displayNamespace);
|
|
12863
|
+
if (info.short && info.short !== info.full) parts.push('Name: ' + info.short);
|
|
12864
|
+
return parts.join('\n');
|
|
12865
|
+
}
|
|
12866
|
+
|
|
12867
|
+
function branchBadgeHtml(branch, icon) {
|
|
12868
|
+
const info = branchDisplayParts(branch);
|
|
12869
|
+
if (!info.full || info.isMain) return '';
|
|
12870
|
+
const title = branchDisplayTitle(info.full);
|
|
12871
|
+
const classes = 'branch-badge' + (info.hasNamespace ? ' namespaced' : '');
|
|
12872
|
+
const text = (icon || '') + (icon ? ' ' : '') + info.short;
|
|
12873
|
+
return `<span class="${classes}" title="${escHtml(title)}" aria-label="${escHtml(title)}" data-branch-full="${escHtml(info.full)}" data-branch-short="${escHtml(info.short)}" data-branch-namespace="${escHtml(info.displayNamespace || '')}">${escHtml(text)}</span>`;
|
|
12874
|
+
}
|
|
12875
|
+
|
|
12876
|
+
function branchMetaHtml(branch) {
|
|
12877
|
+
const info = branchDisplayParts(branch);
|
|
12878
|
+
if (!info.full) return '';
|
|
12879
|
+
return `<span title="${escHtml(branchDisplayTitle(info.full))}" data-branch-full="${escHtml(info.full)}">${escHtml(info.short)}</span>`;
|
|
12880
|
+
}
|
|
12881
|
+
|
|
12882
|
+
function stripBranchFromSessionLabel(label, branch) {
|
|
12883
|
+
const text = String(label || '').replace(/\s+/g, ' ').trim();
|
|
12884
|
+
const branchText = String(branch || '').trim();
|
|
12885
|
+
if (!text || !branchText || branchText === 'main' || branchText === 'master') return text;
|
|
12886
|
+
const branchInfo = branchDisplayParts(branchText);
|
|
12887
|
+
const candidates = Array.from(new Set([
|
|
12888
|
+
branchInfo.full || branchText,
|
|
12889
|
+
branchInfo.short,
|
|
12890
|
+
branchInfo.displayNamespace && branchInfo.short ? `${branchInfo.displayNamespace}/${branchInfo.short}` : '',
|
|
12891
|
+
branchInfo.compactNamespace && branchInfo.short ? `${branchInfo.compactNamespace}/${branchInfo.short}` : '',
|
|
12892
|
+
branchText.length > 12 ? branchText.slice(0, 12) + '..' : branchText,
|
|
12893
|
+
branchText.length > 15 ? branchText.slice(0, 15) + '...' : branchText,
|
|
12894
|
+
].filter(Boolean))).sort((a, b) => b.length - a.length);
|
|
12895
|
+
let cleaned = text;
|
|
12896
|
+
for (const candidate of candidates) {
|
|
12897
|
+
if (cleaned.toLowerCase() === candidate.toLowerCase()) {
|
|
12898
|
+
cleaned = '';
|
|
12899
|
+
break;
|
|
12900
|
+
}
|
|
12901
|
+
const re = new RegExp('(?:\\s*[\\u25ED\\u260D]\\s*' + escapeRegExpText(candidate) + '\\s*)+$', 'i');
|
|
12902
|
+
cleaned = cleaned.replace(re, '').trim();
|
|
12903
|
+
}
|
|
12904
|
+
return cleaned;
|
|
12905
|
+
}
|
|
12906
|
+
|
|
12907
|
+
function cleanSessionLabelForBranch(label, branch) {
|
|
12908
|
+
return stripBranchFromSessionLabel(label, branch) || String(label || '').replace(/\s+/g, ' ').trim();
|
|
12909
|
+
}
|
|
12910
|
+
|
|
12911
|
+
function activeSessionFallbackLabel(s, id) {
|
|
12912
|
+
const meta = s?.meta || {};
|
|
12913
|
+
if (meta.type === 'walle') return 'Wall-E session';
|
|
12914
|
+
const cmd = String(meta.cmd || '').toLowerCase();
|
|
12915
|
+
if (cmd.includes('codex')) return 'Codex session';
|
|
12916
|
+
if (cmd.includes('gemini')) return 'Gemini session';
|
|
12917
|
+
if (cmd.includes('opencode') || cmd.includes('open-code')) return 'OpenCode session';
|
|
12918
|
+
if (cmd.includes('claude')) return 'Claude Code session';
|
|
12919
|
+
return id ? `Session ${String(id).slice(0, 8)}` : 'Session';
|
|
12920
|
+
}
|
|
12921
|
+
|
|
12922
|
+
function activeSessionHasUserRenamedLabel(s, id) {
|
|
12923
|
+
const meta = s?.meta || {};
|
|
12924
|
+
if (meta.userRenamed) return true;
|
|
12925
|
+
const agentId = meta.agentSessionId || meta.agentSessionToken || meta.claudeSessionId || '';
|
|
12926
|
+
if (typeof allRecentSessions === 'undefined' || !Array.isArray(allRecentSessions)) return false;
|
|
12927
|
+
return allRecentSessions.some(r => r && r.userRenamed && (
|
|
12928
|
+
r.sessionId === id ||
|
|
12929
|
+
r.provisionalId === id ||
|
|
12930
|
+
(agentId && (r.sessionId === agentId || r.agentSessionId === agentId))
|
|
12931
|
+
));
|
|
12932
|
+
}
|
|
12933
|
+
|
|
12934
|
+
function activeSessionDisplayLabel(s, id) {
|
|
12935
|
+
const branch = s?.meta?.branch || '';
|
|
12936
|
+
const raw = String(s?.meta?.label || '').replace(/\s+/g, ' ').trim();
|
|
12937
|
+
const cleaned = stripBranchFromSessionLabel(raw, branch);
|
|
12938
|
+
if (activeSessionHasUserRenamedLabel(s, id) && raw) return cleaned || raw;
|
|
12939
|
+
return cleaned || activeSessionFallbackLabel(s, id);
|
|
12940
|
+
}
|
|
12941
|
+
|
|
12942
|
+
function updateTabTitleTooltips() {
|
|
12943
|
+
const labels = document.querySelectorAll('#tabbar-scroll .tab .tab-label');
|
|
12944
|
+
labels.forEach(label => {
|
|
12945
|
+
if (label.querySelector('input')) return;
|
|
12946
|
+
const tab = label.closest('.tab');
|
|
12947
|
+
if (tab) tab.classList.remove('tab-title-clipped');
|
|
12948
|
+
const fullTitle = (label.dataset.fullTitle || label.textContent || '').trim();
|
|
12949
|
+
if (!fullTitle) {
|
|
12950
|
+
label.removeAttribute('title');
|
|
12951
|
+
label.removeAttribute('aria-label');
|
|
12952
|
+
return;
|
|
12953
|
+
}
|
|
12954
|
+
let isClipped = label.scrollWidth > label.clientWidth + 1;
|
|
12955
|
+
if (isClipped && tab && tab.querySelector(':scope > .branch-badge')) {
|
|
12956
|
+
tab.classList.add('tab-title-clipped');
|
|
12957
|
+
isClipped = label.scrollWidth > label.clientWidth + 1;
|
|
12958
|
+
}
|
|
12959
|
+
if (isClipped) {
|
|
12960
|
+
label.title = fullTitle;
|
|
12961
|
+
label.setAttribute('aria-label', fullTitle);
|
|
12962
|
+
} else {
|
|
12963
|
+
label.removeAttribute('title');
|
|
12964
|
+
label.removeAttribute('aria-label');
|
|
12965
|
+
}
|
|
12966
|
+
});
|
|
12967
|
+
}
|
|
12968
|
+
|
|
11488
12969
|
let _renderSessionListTimer = null;
|
|
11489
12970
|
function renderSessionList(force) {
|
|
11490
12971
|
if (!force) {
|
|
@@ -11541,7 +13022,8 @@ function renderSessionList(force) {
|
|
|
11541
13022
|
}
|
|
11542
13023
|
}
|
|
11543
13024
|
const isActive = state.activeTab === id;
|
|
11544
|
-
const
|
|
13025
|
+
const branchName = s.meta?.branch || '';
|
|
13026
|
+
const label = activeSessionDisplayLabel(s, id);
|
|
11545
13027
|
const lastAct = SessionActivityUtils.sessionTouchedAtMs(s) || s.meta?.lastActivity || s.meta?.createdAt || 0;
|
|
11546
13028
|
const idleMs = Date.now() - lastAct;
|
|
11547
13029
|
const isStale = idleMs > 24 * 60 * 60 * 1000;
|
|
@@ -11563,8 +13045,7 @@ function renderSessionList(force) {
|
|
|
11563
13045
|
}).map(p =>
|
|
11564
13046
|
`<span class="prompt-badge" onclick="event.stopPropagation();openPromptInEditor(${p.prompt_id})" title="${escHtml(p.title || 'Prompt')}">${escHtml((p.title || 'Prompt').slice(0, 20))}</span>`
|
|
11565
13047
|
).join('');
|
|
11566
|
-
const
|
|
11567
|
-
const branchBadge = branchName && branchName !== 'main' ? `<span class="branch-badge" title="Branch: ${escHtml(branchName)}">☍ ${escHtml(branchName.length > 15 ? branchName.slice(0, 15) + '...' : branchName)}</span>` : '';
|
|
13048
|
+
const branchBadge = branchBadgeHtml(branchName, '\u260D');
|
|
11568
13049
|
const worktreeBadge = worktreeAttentionBadge(s);
|
|
11569
13050
|
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)">
|
|
11570
13051
|
<span class="dot"></span>
|
|
@@ -11615,17 +13096,149 @@ function setActiveSort(mode) {
|
|
|
11615
13096
|
// Primary source: SessionStream status (server-side, computed from JSONL events + PTY activity)
|
|
11616
13097
|
// Fallback: local PTY signals (_lastOutputAt, _waitingForInput) for sessions not yet tracked
|
|
11617
13098
|
const AUTHORITATIVE_STATUS_TTL_MS = 120000;
|
|
13099
|
+
const SERVER_LIVE_STATUS_TTL_MS = 10000;
|
|
13100
|
+
const CODEX_RUNNING_HOLD_MS = 15000;
|
|
13101
|
+
const UI_REFRESH_STATUS_ONLY_SUPPRESS_MS = 2500;
|
|
13102
|
+
function normalizeLiveSessionStatus(status) {
|
|
13103
|
+
if (typeof SessionStatusPrecedence !== 'undefined' && SessionStatusPrecedence.normalizeLiveSessionStatus) {
|
|
13104
|
+
return SessionStatusPrecedence.normalizeLiveSessionStatus(status);
|
|
13105
|
+
}
|
|
13106
|
+
const text = String(status || '').toLowerCase();
|
|
13107
|
+
if (!text) return '';
|
|
13108
|
+
if (text === 'busy' || text === 'active' || text === 'thinking') return 'running';
|
|
13109
|
+
if (text === 'waiting_input' || text === 'waiting-for-input') return 'waiting';
|
|
13110
|
+
if (['running', 'waiting', 'idle', 'exited'].includes(text)) return text;
|
|
13111
|
+
return '';
|
|
13112
|
+
}
|
|
13113
|
+
|
|
13114
|
+
function liveStatusResult(status) {
|
|
13115
|
+
if (typeof SessionStatusPrecedence !== 'undefined' && SessionStatusPrecedence.liveStatusResult) {
|
|
13116
|
+
const resolved = SessionStatusPrecedence.liveStatusResult(status);
|
|
13117
|
+
return resolved ? { cls: resolved.cls, text: resolved.text } : null;
|
|
13118
|
+
}
|
|
13119
|
+
const normalized = normalizeLiveSessionStatus(status);
|
|
13120
|
+
if (!normalized) return null;
|
|
13121
|
+
const statusMap = { running: 'Running', waiting: 'Waiting', idle: 'Idle', exited: 'Exited' };
|
|
13122
|
+
return { cls: normalized, text: statusMap[normalized] || 'Idle' };
|
|
13123
|
+
}
|
|
13124
|
+
|
|
13125
|
+
function _clientTimeMs(value, fallback) {
|
|
13126
|
+
if (typeof SessionStatusPrecedence !== 'undefined' && SessionStatusPrecedence.parseTimeMs) {
|
|
13127
|
+
const parser = typeof SessionActivityUtils !== 'undefined' ? SessionActivityUtils.parseTimeMs : undefined;
|
|
13128
|
+
return SessionStatusPrecedence.parseTimeMs(value, fallback, parser);
|
|
13129
|
+
}
|
|
13130
|
+
if (value == null || value === '') return fallback || 0;
|
|
13131
|
+
if (typeof SessionActivityUtils !== 'undefined' && SessionActivityUtils.parseTimeMs) {
|
|
13132
|
+
const parsed = SessionActivityUtils.parseTimeMs(value);
|
|
13133
|
+
if (parsed) return parsed;
|
|
13134
|
+
}
|
|
13135
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
13136
|
+
const parsed = Date.parse(String(value));
|
|
13137
|
+
return Number.isFinite(parsed) ? parsed : (fallback || 0);
|
|
13138
|
+
}
|
|
13139
|
+
|
|
13140
|
+
function _isClientBlockingWaitingReason(reason) {
|
|
13141
|
+
return reason === 'approval' || reason === 'choice';
|
|
13142
|
+
}
|
|
13143
|
+
|
|
13144
|
+
function _isClientCodexSession(s) {
|
|
13145
|
+
return _clientAgentTypeForSession(s) === 'codex';
|
|
13146
|
+
}
|
|
13147
|
+
|
|
13148
|
+
function _markClientCodexRunningEvidence(s, eventTimestamp, now = Date.now()) {
|
|
13149
|
+
if (!_isClientCodexSession(s)) return;
|
|
13150
|
+
const eventAt = _clientTimeMs(eventTimestamp, now) || now;
|
|
13151
|
+
const previousEventAt = s._codexRunningEvidenceAt || 0;
|
|
13152
|
+
if (previousEventAt && eventAt <= previousEventAt) {
|
|
13153
|
+
s._codexRunningLastDetectionAt = Math.max(s._codexRunningLastDetectionAt || 0, now);
|
|
13154
|
+
return;
|
|
13155
|
+
}
|
|
13156
|
+
s._codexRunningEvidenceAt = Math.max(s._codexRunningEvidenceAt || 0, eventAt);
|
|
13157
|
+
s._codexRunningLastDetectionAt = Math.max(s._codexRunningLastDetectionAt || 0, now);
|
|
13158
|
+
s._codexRunningHoldUntil = Math.max(
|
|
13159
|
+
s._codexRunningHoldUntil || 0,
|
|
13160
|
+
now + CODEX_RUNNING_HOLD_MS
|
|
13161
|
+
);
|
|
13162
|
+
}
|
|
13163
|
+
|
|
13164
|
+
function _clientCodexRunningHoldResult(s, now = Date.now()) {
|
|
13165
|
+
if (!_isClientCodexSession(s)) return null;
|
|
13166
|
+
if (_isClientBlockingWaitingReason(s._waitingReason || '')) return null;
|
|
13167
|
+
const recentPromptTypingAt = s._waitingForInput && s.writer ? (s.writer._lastInputAt || 0) : 0;
|
|
13168
|
+
if (recentPromptTypingAt && (now - recentPromptTypingAt) < 3000) return null;
|
|
13169
|
+
const holdUntil = s._codexRunningHoldUntil || 0;
|
|
13170
|
+
if (holdUntil > now) return { cls: 'running', text: 'Running' };
|
|
13171
|
+
return null;
|
|
13172
|
+
}
|
|
13173
|
+
|
|
13174
|
+
function _markClientUiRefreshOutputSuppression(s) {
|
|
13175
|
+
if (!s) return;
|
|
13176
|
+
s._uiRefreshStatusOnlySuppressUntil = Date.now() + UI_REFRESH_STATUS_ONLY_SUPPRESS_MS;
|
|
13177
|
+
}
|
|
13178
|
+
|
|
13179
|
+
function _shouldSuppressClientUiRefreshOutput(s, data) {
|
|
13180
|
+
if (!s) return false;
|
|
13181
|
+
const until = s._uiRefreshStatusOnlySuppressUntil || 0;
|
|
13182
|
+
return !!(until && Date.now() < until && _isClientCodexStatusOnlyOutput(s, data));
|
|
13183
|
+
}
|
|
13184
|
+
|
|
11618
13185
|
function getSessionStatus(s) {
|
|
13186
|
+
if (typeof SessionStatusPrecedence !== 'undefined' && SessionStatusPrecedence.resolveSessionStatus) {
|
|
13187
|
+
const now = Date.now();
|
|
13188
|
+
const codexRunningHold = _clientCodexRunningHoldResult(s, now);
|
|
13189
|
+
const resolved = SessionStatusPrecedence.resolveSessionStatus({
|
|
13190
|
+
metaType: s.meta?.type,
|
|
13191
|
+
walleGenerating: !!s.walleState?.isGenerating,
|
|
13192
|
+
serverLiveStatus: s._serverLiveStatus,
|
|
13193
|
+
serverLiveStatusAt: s._serverLiveStatusAt,
|
|
13194
|
+
serverWorkingAt: s._serverWorkingAt,
|
|
13195
|
+
serverWorkingEventAt: s._serverWorkingEventAt || s._serverWorkingAt,
|
|
13196
|
+
waitingForInput: !!s._waitingForInput,
|
|
13197
|
+
waitingForInputAt: s._waitingForInputAt,
|
|
13198
|
+
streamStatus: s._streamStatus,
|
|
13199
|
+
streamStatusAt: s._streamStatusAt,
|
|
13200
|
+
lastOutputAt: s._lastOutputAt,
|
|
13201
|
+
metaLastPtyActivity: s.meta?.lastPtyActivity,
|
|
13202
|
+
authoritativeSource: s._authoritativeSource,
|
|
13203
|
+
authoritativeStatusAt: s._authoritativeStatusAt,
|
|
13204
|
+
working: !!s._working,
|
|
13205
|
+
codexRunningHold: !!codexRunningHold,
|
|
13206
|
+
lastInputAt: s._lastInputAt || s.writer?._lastInputAt,
|
|
13207
|
+
}, {
|
|
13208
|
+
now,
|
|
13209
|
+
parseTimeMs: typeof SessionActivityUtils !== 'undefined' ? SessionActivityUtils.parseTimeMs : undefined,
|
|
13210
|
+
});
|
|
13211
|
+
if (resolved) return { cls: resolved.cls, text: resolved.text };
|
|
13212
|
+
}
|
|
11619
13213
|
if (s.meta?.type === 'walle' && s.walleState) {
|
|
11620
13214
|
if (s.walleState.isGenerating) return { cls: 'running', text: 'Running' };
|
|
11621
|
-
return { cls: '
|
|
13215
|
+
return { cls: 'idle', text: 'Idle' };
|
|
11622
13216
|
}
|
|
11623
13217
|
const now = Date.now();
|
|
13218
|
+
const statusMap = { running: 'Running', waiting: 'Waiting', idle: 'Idle', exited: 'Exited' };
|
|
13219
|
+
|
|
13220
|
+
const serverLiveStatus = liveStatusResult(s._serverLiveStatus);
|
|
13221
|
+
if (serverLiveStatus && s._serverLiveStatusAt && (now - s._serverLiveStatusAt) < SERVER_LIVE_STATUS_TTL_MS) {
|
|
13222
|
+
return serverLiveStatus;
|
|
13223
|
+
}
|
|
13224
|
+
|
|
13225
|
+
const serverWorkingAt = s._serverWorkingAt || 0;
|
|
13226
|
+
const serverWorkingEventAt = s._serverWorkingEventAt || serverWorkingAt;
|
|
13227
|
+
const waitingAt = s._waitingForInputAt || 0;
|
|
13228
|
+
const serverWorking = serverWorkingAt &&
|
|
13229
|
+
(now - serverWorkingAt) < 10000 &&
|
|
13230
|
+
(!s._waitingForInput || !waitingAt || serverWorkingEventAt >= waitingAt);
|
|
13231
|
+
const streamFresh = !!(s._streamStatus && s._streamStatusAt && (now - s._streamStatusAt) < 60000);
|
|
13232
|
+
const streamRunning = streamFresh && normalizeLiveSessionStatus(s._streamStatus) === 'running';
|
|
13233
|
+
const lastOut = Math.max(s._lastOutputAt || 0, s.meta?.lastPtyActivity || 0);
|
|
13234
|
+
const recentOutput = lastOut && (now - lastOut) < 5000;
|
|
13235
|
+
const codexRunningHold = _clientCodexRunningHoldResult(s, now);
|
|
11624
13236
|
|
|
11625
|
-
//
|
|
13237
|
+
// Hook/OTEL signals are next after the server's unified live projection.
|
|
11626
13238
|
// Trust fresh hook/OTEL signals, but do not let a lost stop-hook pin a session
|
|
11627
13239
|
// on Running forever. Stale authoritative signals fall through to SessionStream
|
|
11628
|
-
// and local PTY evidence.
|
|
13240
|
+
// and local PTY evidence. Conversely, a fresh authoritative idle signal must
|
|
13241
|
+
// not mask newer Codex/PTY evidence that is visibly still working.
|
|
11629
13242
|
if (s._authoritativeSource) {
|
|
11630
13243
|
const authAt = s._authoritativeStatusAt || 0;
|
|
11631
13244
|
const authFresh = authAt && (now - authAt) < AUTHORITATIVE_STATUS_TTL_MS;
|
|
@@ -11633,31 +13246,29 @@ function getSessionStatus(s) {
|
|
|
11633
13246
|
if (s._working) return { cls: 'running', text: 'Running' };
|
|
11634
13247
|
// Not working — distinguish "waiting for input" (regex or hook 'menu') from "idle".
|
|
11635
13248
|
if (s._waitingForInput) return { cls: 'waiting', text: 'Waiting' };
|
|
13249
|
+
const newerRunningEvidence =
|
|
13250
|
+
serverWorking ||
|
|
13251
|
+
(streamRunning && (!authAt || (s._streamStatusAt || 0) >= authAt)) ||
|
|
13252
|
+
(recentOutput && (!authAt || lastOut >= authAt));
|
|
13253
|
+
if (newerRunningEvidence) return { cls: 'running', text: 'Running' };
|
|
11636
13254
|
return { cls: 'idle', text: 'Idle' };
|
|
11637
13255
|
}
|
|
11638
13256
|
}
|
|
11639
13257
|
|
|
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
13258
|
if (serverWorking) return { cls: 'running', text: 'Running' };
|
|
11646
13259
|
|
|
11647
13260
|
if (s._waitingForInput) return { cls: 'waiting', text: 'Waiting' };
|
|
13261
|
+
if (codexRunningHold) return codexRunningHold;
|
|
11648
13262
|
|
|
11649
13263
|
// Primary: SessionStream status (received via WS stream-status events).
|
|
11650
13264
|
// Trust it if received within the last 60s (reconciliation runs every 60s).
|
|
11651
13265
|
// Do not override fresh stream status with local PTY output; that produced
|
|
11652
13266
|
// running/idle flicker when local echo arrived after a settled stream status.
|
|
11653
|
-
if (
|
|
11654
|
-
const statusMap = { running: 'Running', waiting: 'Waiting', idle: 'Idle', exited: 'Exited' };
|
|
13267
|
+
if (streamFresh) {
|
|
11655
13268
|
return { cls: s._streamStatus, text: statusMap[s._streamStatus] || 'Idle' };
|
|
11656
13269
|
}
|
|
11657
13270
|
|
|
11658
13271
|
// Fallback: old PTY-based signals (for sessions not tracked by SessionStream)
|
|
11659
|
-
const lastOut = Math.max(s._lastOutputAt || 0, s.meta?.lastActivity || 0);
|
|
11660
|
-
const recentOutput = lastOut && (now - lastOut) < 5000;
|
|
11661
13272
|
if (s._waitingForInput && !recentOutput) return { cls: 'waiting', text: 'Waiting' };
|
|
11662
13273
|
if (recentOutput) return { cls: 'running', text: 'Running' };
|
|
11663
13274
|
const justSentInput = s._lastInputAt && (now - s._lastInputAt) < 3000;
|
|
@@ -11731,34 +13342,35 @@ function recentItemDblClick(id, event) {
|
|
|
11731
13342
|
// Shared helper: attach event listeners for inline rename inputs
|
|
11732
13343
|
function setupRenameInput(input, currentText, finish) {
|
|
11733
13344
|
// Stop events from bubbling to parent (e.g., activateTab → steal focus)
|
|
11734
|
-
for (const evt of ['click', 'mousedown', 'dblclick']) {
|
|
13345
|
+
for (const evt of ['click', 'mousedown', 'mouseup', 'pointerdown', 'dblclick']) {
|
|
11735
13346
|
input.addEventListener(evt, (e) => e.stopPropagation());
|
|
11736
13347
|
}
|
|
11737
13348
|
input.addEventListener('blur', finish);
|
|
11738
13349
|
input.addEventListener('keydown', (e) => {
|
|
11739
|
-
|
|
11740
|
-
if (e.key === '
|
|
13350
|
+
e.stopPropagation();
|
|
13351
|
+
if (e.key === 'Enter') { e.preventDefault(); finish(); }
|
|
13352
|
+
if (e.key === 'Escape') { e.preventDefault(); input.value = currentText; finish(); }
|
|
11741
13353
|
});
|
|
13354
|
+
input.addEventListener('keyup', (e) => e.stopPropagation());
|
|
11742
13355
|
}
|
|
11743
13356
|
|
|
11744
13357
|
function startRenameSession(sessionId, labelEl) {
|
|
11745
13358
|
// Guard: already editing
|
|
11746
13359
|
if (labelEl.querySelector('input')) return;
|
|
11747
|
-
const
|
|
13360
|
+
const branch = state.sessions.get(sessionId)?.meta?.branch || '';
|
|
13361
|
+
const currentText = cleanSessionLabelForBranch(labelEl.textContent.trim(), branch);
|
|
11748
13362
|
const input = document.createElement('input');
|
|
11749
13363
|
input.type = 'text';
|
|
11750
13364
|
input.value = currentText;
|
|
11751
13365
|
input.style.cssText = 'width:100%;background:var(--bg);color:var(--fg);border:1px solid var(--accent);border-radius:3px;padding:1px 4px;font-size:12px;outline:none;';
|
|
11752
13366
|
labelEl.textContent = '';
|
|
11753
13367
|
labelEl.appendChild(input);
|
|
11754
|
-
input.focus();
|
|
11755
|
-
input.select();
|
|
11756
13368
|
|
|
11757
13369
|
let done = false;
|
|
11758
13370
|
function finish() {
|
|
11759
13371
|
if (done) return;
|
|
11760
13372
|
done = true;
|
|
11761
|
-
const newName = input.value.trim();
|
|
13373
|
+
const newName = cleanSessionLabelForBranch(input.value.trim(), branch);
|
|
11762
13374
|
if (newName && newName !== currentText) {
|
|
11763
13375
|
// Persist to DB via REST API
|
|
11764
13376
|
fetch(`/api/sessions/rename?token=${state.token}`, {
|
|
@@ -11768,7 +13380,7 @@ function startRenameSession(sessionId, labelEl) {
|
|
|
11768
13380
|
});
|
|
11769
13381
|
// Update active session label
|
|
11770
13382
|
const active = state.sessions.get(sessionId);
|
|
11771
|
-
if (active && active.meta) active.meta.label = newName;
|
|
13383
|
+
if (active && active.meta) { active.meta.label = newName; active.meta.userRenamed = true; }
|
|
11772
13384
|
// Update recent session list
|
|
11773
13385
|
const recent = allRecentSessions.find(x => x.sessionId === sessionId);
|
|
11774
13386
|
if (recent) { recent.aiTitle = newName; recent.userRenamed = true; }
|
|
@@ -11793,6 +13405,8 @@ function startRenameSession(sessionId, labelEl) {
|
|
|
11793
13405
|
}
|
|
11794
13406
|
|
|
11795
13407
|
setupRenameInput(input, currentText, finish);
|
|
13408
|
+
input.focus();
|
|
13409
|
+
input.select();
|
|
11796
13410
|
}
|
|
11797
13411
|
|
|
11798
13412
|
function startRenameReviewTitle(titleEl) {
|
|
@@ -11807,8 +13421,6 @@ function startRenameReviewTitle(titleEl) {
|
|
|
11807
13421
|
input.style.cssText = 'width:100%;background:var(--bg);color:var(--fg);border:1px solid var(--accent);border-radius:5px;padding:4px 8px;font-size:15px;font-weight:600;outline:none;';
|
|
11808
13422
|
titleEl.textContent = '';
|
|
11809
13423
|
titleEl.appendChild(input);
|
|
11810
|
-
input.focus();
|
|
11811
|
-
input.select();
|
|
11812
13424
|
|
|
11813
13425
|
let done = false;
|
|
11814
13426
|
function finish() {
|
|
@@ -11824,7 +13436,7 @@ function startRenameReviewTitle(titleEl) {
|
|
|
11824
13436
|
const s = allRecentSessions.find(x => x.sessionId === sessionId);
|
|
11825
13437
|
if (s) { s.aiTitle = newName; s.userRenamed = true; }
|
|
11826
13438
|
const active = state.sessions.get(sessionId);
|
|
11827
|
-
if (active && active.meta) active.meta.label = newName;
|
|
13439
|
+
if (active && active.meta) { active.meta.label = newName; active.meta.userRenamed = true; }
|
|
11828
13440
|
titleEl.textContent = newName;
|
|
11829
13441
|
renderFilteredSessions();
|
|
11830
13442
|
renderSessionList();
|
|
@@ -11835,6 +13447,8 @@ function startRenameReviewTitle(titleEl) {
|
|
|
11835
13447
|
}
|
|
11836
13448
|
|
|
11837
13449
|
setupRenameInput(input, currentText, finish);
|
|
13450
|
+
input.focus();
|
|
13451
|
+
input.select();
|
|
11838
13452
|
}
|
|
11839
13453
|
|
|
11840
13454
|
function startRenameReviewTabLabel(labelEl) {
|
|
@@ -11848,8 +13462,6 @@ function startRenameReviewTabLabel(labelEl) {
|
|
|
11848
13462
|
input.style.cssText = 'width:100%;background:var(--bg);color:var(--fg);border:1px solid var(--accent);border-radius:3px;padding:1px 4px;font-size:12px;outline:none;';
|
|
11849
13463
|
labelEl.textContent = '';
|
|
11850
13464
|
labelEl.appendChild(input);
|
|
11851
|
-
input.focus();
|
|
11852
|
-
input.select();
|
|
11853
13465
|
|
|
11854
13466
|
let done = false;
|
|
11855
13467
|
function finish() {
|
|
@@ -11865,7 +13477,7 @@ function startRenameReviewTabLabel(labelEl) {
|
|
|
11865
13477
|
const s = allRecentSessions.find(x => x.sessionId === sessionId);
|
|
11866
13478
|
if (s) { s.aiTitle = newName; s.userRenamed = true; }
|
|
11867
13479
|
const active = state.sessions.get(sessionId);
|
|
11868
|
-
if (active && active.meta) active.meta.label = newName;
|
|
13480
|
+
if (active && active.meta) { active.meta.label = newName; active.meta.userRenamed = true; }
|
|
11869
13481
|
const reviewTitleEl = document.getElementById('review-title');
|
|
11870
13482
|
if (reviewTitleEl) reviewTitleEl.textContent = newName;
|
|
11871
13483
|
}
|
|
@@ -11879,6 +13491,8 @@ function startRenameReviewTabLabel(labelEl) {
|
|
|
11879
13491
|
}
|
|
11880
13492
|
|
|
11881
13493
|
setupRenameInput(input, rawText, finish);
|
|
13494
|
+
input.focus();
|
|
13495
|
+
input.select();
|
|
11882
13496
|
}
|
|
11883
13497
|
|
|
11884
13498
|
function startRenameRecentSession(sessionId, spanEl) {
|
|
@@ -11889,8 +13503,6 @@ function startRenameRecentSession(sessionId, spanEl) {
|
|
|
11889
13503
|
input.style.cssText = 'width:100%;background:var(--bg);color:var(--fg);border:1px solid var(--accent);border-radius:3px;padding:2px 4px;font-size:12px;outline:none;';
|
|
11890
13504
|
spanEl.textContent = '';
|
|
11891
13505
|
spanEl.appendChild(input);
|
|
11892
|
-
input.focus();
|
|
11893
|
-
input.select();
|
|
11894
13506
|
|
|
11895
13507
|
let done = false;
|
|
11896
13508
|
function finish() {
|
|
@@ -11909,7 +13521,7 @@ function startRenameRecentSession(sessionId, spanEl) {
|
|
|
11909
13521
|
if (s) { s.aiTitle = newName; s.userRenamed = true; }
|
|
11910
13522
|
// Also update active session if applicable
|
|
11911
13523
|
const active = state.sessions.get(sessionId);
|
|
11912
|
-
if (active && active.meta) active.meta.label = newName;
|
|
13524
|
+
if (active && active.meta) { active.meta.label = newName; active.meta.userRenamed = true; }
|
|
11913
13525
|
// Update review title if this session is being reviewed
|
|
11914
13526
|
if (state.reviewingSessionId === sessionId) {
|
|
11915
13527
|
const reviewTitleEl = document.getElementById('review-title');
|
|
@@ -11929,6 +13541,8 @@ function startRenameRecentSession(sessionId, spanEl) {
|
|
|
11929
13541
|
}
|
|
11930
13542
|
|
|
11931
13543
|
setupRenameInput(input, currentText, finish);
|
|
13544
|
+
input.focus();
|
|
13545
|
+
input.select();
|
|
11932
13546
|
}
|
|
11933
13547
|
|
|
11934
13548
|
async function loadSessionPrompts(sessionId) {
|
|
@@ -12001,6 +13615,37 @@ function dismissCompactBanner(id) {
|
|
|
12001
13615
|
}
|
|
12002
13616
|
}
|
|
12003
13617
|
|
|
13618
|
+
function createSessionsOverviewTab() {
|
|
13619
|
+
const tab = document.createElement('div');
|
|
13620
|
+
tab.className = `tab pinned-tab sessions-overview-tab ${isSessionsOverviewActive() ? 'active' : ''}`;
|
|
13621
|
+
tab.dataset.sessionId = SESSIONS_OVERVIEW_TAB_ID;
|
|
13622
|
+
tab.dataset.pinned = 'true';
|
|
13623
|
+
tab.draggable = false;
|
|
13624
|
+
tab.title = 'Sessions overview';
|
|
13625
|
+
tab.onclick = () => showStandupDashboard();
|
|
13626
|
+
|
|
13627
|
+
const tabLabel = document.createElement('span');
|
|
13628
|
+
tabLabel.className = 'tab-label';
|
|
13629
|
+
tabLabel.textContent = 'Overview';
|
|
13630
|
+
tabLabel.dataset.fullTitle = 'Overview';
|
|
13631
|
+
tab.appendChild(tabLabel);
|
|
13632
|
+
tab.ondragover = function(e) {
|
|
13633
|
+
if (!isMovableTabDrag(_tabDragId)) return;
|
|
13634
|
+
e.preventDefault();
|
|
13635
|
+
e.dataTransfer.dropEffect = 'move';
|
|
13636
|
+
tab.classList.add('tab-drop-after');
|
|
13637
|
+
};
|
|
13638
|
+
tab.ondragleave = function() { tab.classList.remove('tab-drop-after'); };
|
|
13639
|
+
tab.ondrop = function(e) {
|
|
13640
|
+
e.preventDefault();
|
|
13641
|
+
tab.classList.remove('tab-drop-after');
|
|
13642
|
+
if (moveTabToStart(_tabDragId)) {
|
|
13643
|
+
_tabDragId = null;
|
|
13644
|
+
}
|
|
13645
|
+
};
|
|
13646
|
+
return tab;
|
|
13647
|
+
}
|
|
13648
|
+
|
|
12004
13649
|
let _renderTabsTimer = null;
|
|
12005
13650
|
let _renderTabsLastHash = '';
|
|
12006
13651
|
function renderTabs(force) {
|
|
@@ -12029,6 +13674,8 @@ function renderTabs(force) {
|
|
|
12029
13674
|
// Remove old tabs
|
|
12030
13675
|
scrollContainer.querySelectorAll('.tab').forEach(t => t.remove());
|
|
12031
13676
|
|
|
13677
|
+
scrollContainer.insertBefore(createSessionsOverviewTab(), addBtn);
|
|
13678
|
+
|
|
12032
13679
|
for (const id of state.tabOrder) {
|
|
12033
13680
|
if (id === 'rules') {
|
|
12034
13681
|
const tab = document.createElement('div');
|
|
@@ -12105,7 +13752,7 @@ function renderTabs(force) {
|
|
|
12105
13752
|
|
|
12106
13753
|
const s = state.sessions.get(id);
|
|
12107
13754
|
if (!s) continue;
|
|
12108
|
-
const label = s
|
|
13755
|
+
const label = activeSessionDisplayLabel(s, id);
|
|
12109
13756
|
|
|
12110
13757
|
const tab = document.createElement('div');
|
|
12111
13758
|
tab.className = `tab ${state.activeTab === id ? 'active' : ''}`;
|
|
@@ -12113,22 +13760,27 @@ function renderTabs(force) {
|
|
|
12113
13760
|
tab.dataset.agent = getAgentType(s);
|
|
12114
13761
|
tab.draggable = true;
|
|
12115
13762
|
const tabBranch = s.meta?.branch || '';
|
|
12116
|
-
const tabBranchText = tabBranch && tabBranch !== 'main' ? ' [' + escHtml(tabBranch.length > 12 ? tabBranch.slice(0, 12) + '..' : tabBranch) + ']' : '';
|
|
12117
13763
|
tab.textContent = '';
|
|
12118
13764
|
const tabIcon = document.createElement('span');
|
|
12119
13765
|
tabIcon.className = 'tab-icon';
|
|
12120
13766
|
tabIcon.innerHTML = providerIconSvg(tab.dataset.agent, 12);
|
|
12121
|
-
const tabLabel = document.createElement('span'); tabLabel.className = 'tab-label'; tabLabel.textContent = label
|
|
12122
|
-
|
|
12123
|
-
|
|
12124
|
-
|
|
12125
|
-
|
|
12126
|
-
|
|
12127
|
-
|
|
13767
|
+
const tabLabel = document.createElement('span'); tabLabel.className = 'tab-label'; tabLabel.textContent = label; tabLabel.dataset.fullTitle = label;
|
|
13768
|
+
let tabBranchEl = null;
|
|
13769
|
+
const tabBranchInfo = branchDisplayParts(tabBranch);
|
|
13770
|
+
if (tabBranchInfo.full && !tabBranchInfo.isMain) {
|
|
13771
|
+
tabBranchEl = document.createElement('span');
|
|
13772
|
+
tabBranchEl.className = 'branch-badge' + (tabBranchInfo.hasNamespace ? ' namespaced' : '');
|
|
13773
|
+
tabBranchEl.style.cssText = 'font-size:9px;margin-left:3px';
|
|
13774
|
+
tabBranchEl.textContent = '\u25ED ' + tabBranchInfo.short;
|
|
13775
|
+
tabBranchEl.title = branchDisplayTitle(tabBranchInfo.full);
|
|
13776
|
+
tabBranchEl.setAttribute('aria-label', tabBranchEl.title);
|
|
13777
|
+
tabBranchEl.dataset.branchFull = tabBranchInfo.full;
|
|
13778
|
+
tabBranchEl.dataset.branchShort = tabBranchInfo.short;
|
|
13779
|
+
tabBranchEl.dataset.branchNamespace = tabBranchInfo.displayNamespace || '';
|
|
12128
13780
|
}
|
|
12129
13781
|
const tabClose = document.createElement('span'); tabClose.className = 'close-tab'; tabClose.textContent = '\u00d7';
|
|
12130
13782
|
tabClose.onclick = function(e) { e.stopPropagation(); killSession(id); };
|
|
12131
|
-
tab.appendChild(tabIcon); tab.appendChild(tabLabel); tab.appendChild(tabClose);
|
|
13783
|
+
tab.appendChild(tabIcon); tab.appendChild(tabLabel); if (tabBranchEl) tab.appendChild(tabBranchEl); tab.appendChild(tabClose);
|
|
12132
13784
|
tab.onclick = function(e) { sessionItemClick(id, e); };
|
|
12133
13785
|
tab.ondblclick = function(e) {
|
|
12134
13786
|
e.preventDefault();
|
|
@@ -12188,6 +13840,7 @@ function updateTabOverflowBtn() {
|
|
|
12188
13840
|
const countEl = document.getElementById('tab-overflow-count');
|
|
12189
13841
|
const tabs = scrollContainer.querySelectorAll('.tab');
|
|
12190
13842
|
const isOverflowing = scrollContainer.scrollWidth > scrollContainer.clientWidth + 2;
|
|
13843
|
+
updateTabTitleTooltips();
|
|
12191
13844
|
btn.classList.toggle('visible', isOverflowing);
|
|
12192
13845
|
if (isOverflowing && countEl) {
|
|
12193
13846
|
countEl.textContent = tabs.length;
|
|
@@ -12204,6 +13857,12 @@ function toggleTabOverflow(e) {
|
|
|
12204
13857
|
menu = document.createElement('div');
|
|
12205
13858
|
menu.className = 'tab-overflow-menu';
|
|
12206
13859
|
|
|
13860
|
+
const overviewItem = document.createElement('div');
|
|
13861
|
+
overviewItem.className = 'tab-overflow-item' + (isSessionsOverviewActive() ? ' active' : '');
|
|
13862
|
+
overviewItem.innerHTML = `<span class="overflow-dot" style="background:var(--accent)"></span><span class="overflow-label">Overview</span>`;
|
|
13863
|
+
overviewItem.onclick = function() { menu.remove(); showStandupDashboard(); };
|
|
13864
|
+
menu.appendChild(overviewItem);
|
|
13865
|
+
|
|
12207
13866
|
for (const id of state.tabOrder) {
|
|
12208
13867
|
// Skip tabs that aren't rendered
|
|
12209
13868
|
if (id === 'codereview' || id === 'walle') continue;
|
|
@@ -12257,6 +13916,22 @@ document.getElementById('tabbar-scroll').addEventListener('wheel', function(e) {
|
|
|
12257
13916
|
|
|
12258
13917
|
// --- Tab drag-and-drop reorder ---
|
|
12259
13918
|
let _tabDragId = null;
|
|
13919
|
+
function isMovableTabDrag(id) {
|
|
13920
|
+
return !!id && state.tabOrder.includes(id);
|
|
13921
|
+
}
|
|
13922
|
+
|
|
13923
|
+
function moveTabToStart(id) {
|
|
13924
|
+
const from = state.tabOrder.indexOf(id);
|
|
13925
|
+
if (from === -1) return false;
|
|
13926
|
+
if (from > 0) {
|
|
13927
|
+
state.tabOrder.splice(from, 1);
|
|
13928
|
+
state.tabOrder.unshift(id);
|
|
13929
|
+
saveTabOrder();
|
|
13930
|
+
renderTabs();
|
|
13931
|
+
}
|
|
13932
|
+
return true;
|
|
13933
|
+
}
|
|
13934
|
+
|
|
12260
13935
|
function saveTabOrder() {
|
|
12261
13936
|
// Save session IDs and review tab (for position restore after restart)
|
|
12262
13937
|
const sessionOrder = state.tabOrder.filter(id => state.sessions.has(id) || id === 'review');
|
|
@@ -12265,7 +13940,7 @@ function saveTabOrder() {
|
|
|
12265
13940
|
|
|
12266
13941
|
// --- Actions ---
|
|
12267
13942
|
function getLastSessionCwd() {
|
|
12268
|
-
const isWorktree = (p) => p
|
|
13943
|
+
const isWorktree = (p) => _nsIsAgentWorktreePath(p);
|
|
12269
13944
|
// Try most recently active session's cwd, preferring non-worktree paths
|
|
12270
13945
|
let latest = null;
|
|
12271
13946
|
let latestNonWt = null;
|
|
@@ -12304,6 +13979,14 @@ function _nsNormalizeCwdForCompare(p) {
|
|
|
12304
13979
|
return String(p || '').replace(/\/+$/, '');
|
|
12305
13980
|
}
|
|
12306
13981
|
|
|
13982
|
+
function _nsIsAgentWorktreePath(p) {
|
|
13983
|
+
return /\/\.(?:claude|walle)\/worktrees\//.test(String(p || ''));
|
|
13984
|
+
}
|
|
13985
|
+
|
|
13986
|
+
function _nsWorktreeNamespaceForAgent(agentType) {
|
|
13987
|
+
return agentType === 'walle' ? 'walle' : 'claude';
|
|
13988
|
+
}
|
|
13989
|
+
|
|
12307
13990
|
function _nsAgentUsesCodeWorkspace() {
|
|
12308
13991
|
var val = document.getElementById('ns-agent').value || 'claude:';
|
|
12309
13992
|
var agent = val.replace(/:$/, '');
|
|
@@ -12334,8 +14017,8 @@ async function _maybeRecommendWorktreeForNewSession(force) {
|
|
|
12334
14017
|
if (!modal || modal.classList.contains('hidden')) return;
|
|
12335
14018
|
var seq = ++_nsWorktreeRecommendSeq;
|
|
12336
14019
|
var cwd = _nsNormalizeCwdForCompare((document.getElementById('ns-cwd') || {}).value || '');
|
|
12337
|
-
if (!cwd || cwd
|
|
12338
|
-
_nsApplyWorktreeRecommendation(false, cwd && cwd
|
|
14020
|
+
if (!cwd || _nsIsAgentWorktreePath(cwd) || !_nsAgentUsesCodeWorkspace()) {
|
|
14021
|
+
_nsApplyWorktreeRecommendation(false, cwd && _nsIsAgentWorktreePath(cwd)
|
|
12339
14022
|
? 'Already inside a worktree'
|
|
12340
14023
|
: 'Isolated branch & files for parallel work');
|
|
12341
14024
|
return;
|
|
@@ -12346,7 +14029,10 @@ async function _maybeRecommendWorktreeForNewSession(force) {
|
|
|
12346
14029
|
|
|
12347
14030
|
try {
|
|
12348
14031
|
var token = (state && state.token) ? state.token : '';
|
|
12349
|
-
var
|
|
14032
|
+
var params = new URLSearchParams();
|
|
14033
|
+
params.set('token', token);
|
|
14034
|
+
params.set('cwd', cwd);
|
|
14035
|
+
var r = await fetch('/api/worktrees?' + params.toString());
|
|
12350
14036
|
var d = await r.json();
|
|
12351
14037
|
if (seq !== _nsWorktreeRecommendSeq || _nsWorktreeTouched) return;
|
|
12352
14038
|
var latestCwd = _nsNormalizeCwdForCompare((document.getElementById('ns-cwd') || {}).value || '');
|
|
@@ -12483,6 +14169,8 @@ function onAgentChange() {
|
|
|
12483
14169
|
var val = document.getElementById('ns-agent').value || '';
|
|
12484
14170
|
var isCustom = val.startsWith('custom:');
|
|
12485
14171
|
document.getElementById('ns-custom-fields').style.display = isCustom ? 'block' : 'none';
|
|
14172
|
+
toggleWorktreeFields({ silent: true });
|
|
14173
|
+
_maybeRecommendWorktreeForNewSession(false);
|
|
12486
14174
|
}
|
|
12487
14175
|
|
|
12488
14176
|
function createSession() {
|
|
@@ -12532,7 +14220,12 @@ function createSession() {
|
|
|
12532
14220
|
fetch('/api/worktrees/create', {
|
|
12533
14221
|
method: 'POST',
|
|
12534
14222
|
headers: { 'Content-Type': 'application/json' },
|
|
12535
|
-
body: JSON.stringify({
|
|
14223
|
+
body: JSON.stringify({
|
|
14224
|
+
name: wtName,
|
|
14225
|
+
cwd: cwd,
|
|
14226
|
+
namespace: _nsWorktreeNamespaceForAgent(agentType),
|
|
14227
|
+
agentType: agentType,
|
|
14228
|
+
}),
|
|
12536
14229
|
})
|
|
12537
14230
|
.then(function(r) { return r.json(); })
|
|
12538
14231
|
.then(function(d) {
|
|
@@ -12595,8 +14288,7 @@ function killSession(id) {
|
|
|
12595
14288
|
const next = nextSession || state.tabOrder[state.tabOrder.length - 1];
|
|
12596
14289
|
if (next) activateTab(next);
|
|
12597
14290
|
else {
|
|
12598
|
-
|
|
12599
|
-
document.getElementById('welcome').style.display = 'flex';
|
|
14291
|
+
showStandupDashboard();
|
|
12600
14292
|
}
|
|
12601
14293
|
}
|
|
12602
14294
|
renderTabs();
|
|
@@ -12627,10 +14319,7 @@ function closeAllTabs() {
|
|
|
12627
14319
|
// Close special tabs too
|
|
12628
14320
|
state.tabOrder = state.tabOrder.filter(t => !['rules', 'insights', 'permissions'].includes(t) && !t.startsWith('review'));
|
|
12629
14321
|
saveTabOrder();
|
|
12630
|
-
|
|
12631
|
-
document.getElementById('welcome').style.display = 'flex';
|
|
12632
|
-
renderTabs();
|
|
12633
|
-
renderSessionList();
|
|
14322
|
+
showStandupDashboard();
|
|
12634
14323
|
}
|
|
12635
14324
|
|
|
12636
14325
|
function closeOtherTabs(keepId) {
|
|
@@ -12677,7 +14366,7 @@ function _showTabContextMenu(e, tabId) {
|
|
|
12677
14366
|
|
|
12678
14367
|
const sess = state.sessions.get(tabId);
|
|
12679
14368
|
const label = sess?.meta?.label || tabId.slice(0, 8);
|
|
12680
|
-
const hasWorktree = !!(sess?.meta?.cwd && sess.meta.cwd
|
|
14369
|
+
const hasWorktree = !!(sess?.meta?.cwd && _nsIsAgentWorktreePath(sess.meta.cwd));
|
|
12681
14370
|
menu.innerHTML = `
|
|
12682
14371
|
<div class="ctx-item" onclick="killSession('${escAttr(tabId)}')">Close</div>
|
|
12683
14372
|
<div class="ctx-item${sessionCount <= 1 ? ' disabled' : ''}" onclick="${sessionCount > 1 ? `closeOtherTabs('${escAttr(tabId)}')` : ''}" ${sessionCount <= 1 ? 'style="opacity:0.3;pointer-events:none"' : ''}>Close Others</div>
|
|
@@ -12898,48 +14587,179 @@ function _createSearchBarEl() {
|
|
|
12898
14587
|
return bar;
|
|
12899
14588
|
}
|
|
12900
14589
|
|
|
14590
|
+
function _isConversationSearchVisible(s) {
|
|
14591
|
+
if (!s || !s.container) return false;
|
|
14592
|
+
const convView = s.container.querySelector('.conversation-view');
|
|
14593
|
+
if (!convView) return false;
|
|
14594
|
+
if (convView.style.display && convView.style.display !== 'none') return true;
|
|
14595
|
+
try {
|
|
14596
|
+
return window.getComputedStyle(convView).display !== 'none';
|
|
14597
|
+
} catch {
|
|
14598
|
+
return false;
|
|
14599
|
+
}
|
|
14600
|
+
}
|
|
14601
|
+
|
|
14602
|
+
function _clearConversationSearchHighlights(root) {
|
|
14603
|
+
if (!root) return;
|
|
14604
|
+
root.querySelectorAll('.review-search-highlight').forEach(el => {
|
|
14605
|
+
const parent = el.parentNode;
|
|
14606
|
+
if (!parent) return;
|
|
14607
|
+
parent.replaceChild(document.createTextNode(el.textContent), el);
|
|
14608
|
+
parent.normalize();
|
|
14609
|
+
});
|
|
14610
|
+
}
|
|
14611
|
+
|
|
14612
|
+
function _clearSearchBarEffects(s, mode) {
|
|
14613
|
+
if (!s) return;
|
|
14614
|
+
if (mode === 'conversation') {
|
|
14615
|
+
_clearConversationSearchHighlights(s.container && s.container.querySelector('.conversation-view'));
|
|
14616
|
+
} else if (s.searchAddon) {
|
|
14617
|
+
s.searchAddon.clearDecorations();
|
|
14618
|
+
}
|
|
14619
|
+
}
|
|
14620
|
+
|
|
14621
|
+
function _conversationSearchTargets(root) {
|
|
14622
|
+
if (!root) return [];
|
|
14623
|
+
return Array.from(root.querySelectorAll([
|
|
14624
|
+
'.msg-text',
|
|
14625
|
+
'.conversation-state',
|
|
14626
|
+
].join(',')));
|
|
14627
|
+
}
|
|
14628
|
+
|
|
14629
|
+
function _wireTerminalSearchBar(s, bar) {
|
|
14630
|
+
const input = bar.querySelector('input');
|
|
14631
|
+
const countEl = bar.querySelector('.search-count');
|
|
14632
|
+
let _searchDebounce = null;
|
|
14633
|
+
input.addEventListener('input', () => {
|
|
14634
|
+
clearTimeout(_searchDebounce);
|
|
14635
|
+
_searchDebounce = setTimeout(() => {
|
|
14636
|
+
const q = input.value;
|
|
14637
|
+
if (q) {
|
|
14638
|
+
s.searchAddon.findNext(q, { regex: false, caseSensitive: false, incremental: true });
|
|
14639
|
+
} else {
|
|
14640
|
+
s.searchAddon.clearDecorations();
|
|
14641
|
+
countEl.textContent = '';
|
|
14642
|
+
}
|
|
14643
|
+
}, 100);
|
|
14644
|
+
});
|
|
14645
|
+
input.addEventListener('keydown', (ev) => {
|
|
14646
|
+
if (ev.key === 'Enter') {
|
|
14647
|
+
ev.preventDefault();
|
|
14648
|
+
if (input.value) {
|
|
14649
|
+
if (ev.shiftKey) s.searchAddon.findPrevious(input.value, { regex: false, caseSensitive: false });
|
|
14650
|
+
else s.searchAddon.findNext(input.value, { regex: false, caseSensitive: false });
|
|
14651
|
+
}
|
|
14652
|
+
}
|
|
14653
|
+
if (ev.key === 'Escape') { ev.preventDefault(); closeTermSearch(); }
|
|
14654
|
+
});
|
|
14655
|
+
bar.querySelector('.search-next').onclick = () => { if (input.value) s.searchAddon.findNext(input.value, { regex: false, caseSensitive: false }); };
|
|
14656
|
+
bar.querySelector('.search-prev').onclick = () => { if (input.value) s.searchAddon.findPrevious(input.value, { regex: false, caseSensitive: false }); };
|
|
14657
|
+
bar.querySelector('.search-close').onclick = () => closeTermSearch();
|
|
14658
|
+
}
|
|
14659
|
+
|
|
14660
|
+
function _scrollToConversationSearchMatch(bar, idx) {
|
|
14661
|
+
const state = bar._conversationSearchState;
|
|
14662
|
+
if (!state || idx < 0 || idx >= state.matches.length) return;
|
|
14663
|
+
state.matches.forEach(m => m.classList.remove('current'));
|
|
14664
|
+
const match = state.matches[idx];
|
|
14665
|
+
match.classList.add('current');
|
|
14666
|
+
|
|
14667
|
+
const msgText = match.closest('.msg-text');
|
|
14668
|
+
if (msgText && msgText.classList.contains('collapsed')) {
|
|
14669
|
+
msgText.classList.remove('collapsed');
|
|
14670
|
+
const btn = msgText.nextElementSibling;
|
|
14671
|
+
if (btn && btn.classList.contains('msg-expand')) btn.textContent = 'Show less';
|
|
14672
|
+
}
|
|
14673
|
+
|
|
14674
|
+
const msgEl = match.closest('.review-msg');
|
|
14675
|
+
if (msgEl && msgEl.style.display === 'none') msgEl.style.display = '';
|
|
14676
|
+
|
|
14677
|
+
const turnEl = match.closest('.prompt-turn');
|
|
14678
|
+
if (turnEl) setPromptTurnExpanded(turnEl, true);
|
|
14679
|
+
|
|
14680
|
+
const group = match.closest('.thought-group, .review-msg.skill-body, .review-msg.local-cmd, .review-msg.summary');
|
|
14681
|
+
if (group && !group.classList.contains('expanded')) group.classList.add('expanded');
|
|
14682
|
+
|
|
14683
|
+
match.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
14684
|
+
const countEl = bar.querySelector('.search-count');
|
|
14685
|
+
if (countEl) countEl.textContent = `${idx + 1} / ${state.matches.length}`;
|
|
14686
|
+
}
|
|
14687
|
+
|
|
14688
|
+
function _wireConversationSearchBar(s, bar) {
|
|
14689
|
+
const input = bar.querySelector('input');
|
|
14690
|
+
const countEl = bar.querySelector('.search-count');
|
|
14691
|
+
const searchState = { matches: [], idx: -1 };
|
|
14692
|
+
bar._conversationSearchState = searchState;
|
|
14693
|
+
|
|
14694
|
+
const update = () => {
|
|
14695
|
+
const convView = s.container && s.container.querySelector('.conversation-view');
|
|
14696
|
+
_clearConversationSearchHighlights(convView);
|
|
14697
|
+
searchState.matches = [];
|
|
14698
|
+
searchState.idx = -1;
|
|
14699
|
+
|
|
14700
|
+
const query = input.value.trim();
|
|
14701
|
+
if (!query || !convView) {
|
|
14702
|
+
countEl.textContent = '';
|
|
14703
|
+
return;
|
|
14704
|
+
}
|
|
14705
|
+
|
|
14706
|
+
const queryLower = query.toLowerCase();
|
|
14707
|
+
_conversationSearchTargets(convView).forEach(el => highlightInNode(el, queryLower));
|
|
14708
|
+
searchState.matches = Array.from(convView.querySelectorAll('.review-search-highlight'));
|
|
14709
|
+
const count = searchState.matches.length;
|
|
14710
|
+
countEl.textContent = count > 0 ? `${count} found` : 'No results';
|
|
14711
|
+
if (count > 0) {
|
|
14712
|
+
searchState.idx = 0;
|
|
14713
|
+
_scrollToConversationSearchMatch(bar, 0);
|
|
14714
|
+
}
|
|
14715
|
+
};
|
|
14716
|
+
|
|
14717
|
+
const nav = (dir) => {
|
|
14718
|
+
if (!searchState.matches.length) return;
|
|
14719
|
+
searchState.idx = (searchState.idx + dir + searchState.matches.length) % searchState.matches.length;
|
|
14720
|
+
_scrollToConversationSearchMatch(bar, searchState.idx);
|
|
14721
|
+
};
|
|
14722
|
+
|
|
14723
|
+
input.addEventListener('input', update);
|
|
14724
|
+
input.addEventListener('keydown', (ev) => {
|
|
14725
|
+
if (ev.key === 'Enter') {
|
|
14726
|
+
ev.preventDefault();
|
|
14727
|
+
nav(ev.shiftKey ? -1 : 1);
|
|
14728
|
+
}
|
|
14729
|
+
if (ev.key === 'Escape') { ev.preventDefault(); closeTermSearch(); }
|
|
14730
|
+
});
|
|
14731
|
+
bar.querySelector('.search-next').onclick = () => nav(1);
|
|
14732
|
+
bar.querySelector('.search-prev').onclick = () => nav(-1);
|
|
14733
|
+
bar.querySelector('.search-close').onclick = () => closeTermSearch();
|
|
14734
|
+
}
|
|
14735
|
+
|
|
14736
|
+
function _ensureSearchBarForMode(s, mode) {
|
|
14737
|
+
let bar = s.container.querySelector('.term-search-bar');
|
|
14738
|
+
if (bar && bar.dataset.searchMode === mode) return bar;
|
|
14739
|
+
if (bar) {
|
|
14740
|
+
_clearSearchBarEffects(s, bar.dataset.searchMode);
|
|
14741
|
+
bar.remove();
|
|
14742
|
+
}
|
|
14743
|
+
bar = _createSearchBarEl();
|
|
14744
|
+
bar.dataset.searchMode = mode;
|
|
14745
|
+
s.container.style.position = 'relative';
|
|
14746
|
+
s.container.appendChild(bar);
|
|
14747
|
+
if (mode === 'conversation') _wireConversationSearchBar(s, bar);
|
|
14748
|
+
else _wireTerminalSearchBar(s, bar);
|
|
14749
|
+
return bar;
|
|
14750
|
+
}
|
|
14751
|
+
|
|
12901
14752
|
function openTermSearch() {
|
|
12902
14753
|
const id = state.activeTab;
|
|
12903
14754
|
const s = id && state.sessions.get(id);
|
|
12904
|
-
if (!s || !s.
|
|
12905
|
-
|
|
12906
|
-
if (!
|
|
12907
|
-
|
|
12908
|
-
s.container.style.position = 'relative';
|
|
12909
|
-
s.container.appendChild(bar);
|
|
12910
|
-
const input = bar.querySelector('input');
|
|
12911
|
-
const countEl = bar.querySelector('.search-count');
|
|
12912
|
-
let _searchDebounce = null;
|
|
12913
|
-
input.addEventListener('input', () => {
|
|
12914
|
-
clearTimeout(_searchDebounce);
|
|
12915
|
-
_searchDebounce = setTimeout(() => {
|
|
12916
|
-
const q = input.value;
|
|
12917
|
-
if (q) {
|
|
12918
|
-
s.searchAddon.findNext(q, { regex: false, caseSensitive: false, incremental: true });
|
|
12919
|
-
} else {
|
|
12920
|
-
s.searchAddon.clearDecorations();
|
|
12921
|
-
countEl.textContent = '';
|
|
12922
|
-
}
|
|
12923
|
-
}, 100);
|
|
12924
|
-
});
|
|
12925
|
-
input.addEventListener('keydown', (ev) => {
|
|
12926
|
-
if (ev.key === 'Enter') {
|
|
12927
|
-
ev.preventDefault();
|
|
12928
|
-
if (input.value) {
|
|
12929
|
-
if (ev.shiftKey) s.searchAddon.findPrevious(input.value, { regex: false, caseSensitive: false });
|
|
12930
|
-
else s.searchAddon.findNext(input.value, { regex: false, caseSensitive: false });
|
|
12931
|
-
}
|
|
12932
|
-
}
|
|
12933
|
-
if (ev.key === 'Escape') { ev.preventDefault(); closeTermSearch(); }
|
|
12934
|
-
});
|
|
12935
|
-
bar.querySelector('.search-next').onclick = () => { if (input.value) s.searchAddon.findNext(input.value, { regex: false, caseSensitive: false }); };
|
|
12936
|
-
bar.querySelector('.search-prev').onclick = () => { if (input.value) s.searchAddon.findPrevious(input.value, { regex: false, caseSensitive: false }); };
|
|
12937
|
-
bar.querySelector('.search-close').onclick = () => closeTermSearch();
|
|
12938
|
-
}
|
|
14755
|
+
if (!s || !s.container) return;
|
|
14756
|
+
const mode = _isConversationSearchVisible(s) ? 'conversation' : 'terminal';
|
|
14757
|
+
if (mode === 'terminal' && !s.searchAddon) return;
|
|
14758
|
+
const bar = _ensureSearchBarForMode(s, mode);
|
|
12939
14759
|
bar.style.display = 'flex';
|
|
12940
14760
|
const input = bar.querySelector('input');
|
|
12941
14761
|
input.focus();
|
|
12942
|
-
if (s.term.hasSelection()) {
|
|
14762
|
+
if (mode === 'terminal' && s.term && s.term.hasSelection && s.term.hasSelection()) {
|
|
12943
14763
|
input.value = s.term.getSelection();
|
|
12944
14764
|
if (input.value) {
|
|
12945
14765
|
input.dispatchEvent(new Event('input'));
|
|
@@ -12951,16 +14771,16 @@ function openTermSearch() {
|
|
|
12951
14771
|
function closeTermSearch() {
|
|
12952
14772
|
// Close search on whichever session has a visible bar (may differ from activeTab after tab switch)
|
|
12953
14773
|
for (const [, s] of state.sessions) {
|
|
12954
|
-
if (!s.container
|
|
14774
|
+
if (!s.container) continue;
|
|
12955
14775
|
const bar = s.container.querySelector('.term-search-bar');
|
|
12956
14776
|
if (bar && bar.style.display !== 'none') {
|
|
12957
14777
|
bar.style.display = 'none';
|
|
12958
|
-
s.
|
|
14778
|
+
_clearSearchBarEffects(s, bar.dataset.searchMode);
|
|
12959
14779
|
bar.querySelector('.search-count').textContent = '';
|
|
12960
14780
|
}
|
|
12961
14781
|
}
|
|
12962
14782
|
const active = state.activeTab && state.sessions.get(state.activeTab);
|
|
12963
|
-
if (active && active.term) focusTerminalIfSafe(state.activeTab, { force: true });
|
|
14783
|
+
if (active && active.term && !_isConversationSearchVisible(active)) focusTerminalIfSafe(state.activeTab, { force: true });
|
|
12964
14784
|
}
|
|
12965
14785
|
|
|
12966
14786
|
|
|
@@ -13315,6 +15135,7 @@ function _fitTerminalPreservingViewport(s, sessionId, opts) {
|
|
|
13315
15135
|
s.writer._userScrollLocked = savedLocked;
|
|
13316
15136
|
}
|
|
13317
15137
|
if (opts.sendResize && sessionId) {
|
|
15138
|
+
_markClientUiRefreshOutputSuppression(s);
|
|
13318
15139
|
send({ type: 'resize', id: sessionId, cols: s.term.cols, rows: s.term.rows });
|
|
13319
15140
|
}
|
|
13320
15141
|
|
|
@@ -13794,9 +15615,10 @@ async function loadPrefs() {
|
|
|
13794
15615
|
if (sortSel) sortSel.value = prefs.session_sort;
|
|
13795
15616
|
}
|
|
13796
15617
|
|
|
13797
|
-
// Restore
|
|
13798
|
-
|
|
13799
|
-
|
|
15618
|
+
// Restore agent filter. Ignore the legacy model_filter value so stale
|
|
15619
|
+
// raw model IDs (for example fake-model) never reappear as filters.
|
|
15620
|
+
if (prefs.agent_filter) {
|
|
15621
|
+
currentAgentFilter = SessionSearchUtils.normalizeRecentAgentType(prefs.agent_filter) || '';
|
|
13800
15622
|
}
|
|
13801
15623
|
|
|
13802
15624
|
// (empty sessions hidden by default — no pref needed)
|
|
@@ -13965,8 +15787,9 @@ async function loadRecentSessions() {
|
|
|
13965
15787
|
pinnedSessionIds.push(s.sessionId);
|
|
13966
15788
|
}
|
|
13967
15789
|
}
|
|
13968
|
-
|
|
13969
|
-
|
|
15790
|
+
const sidebarSessions = getRecentSidebarSessions();
|
|
15791
|
+
populateProjectFilter(sidebarSessions);
|
|
15792
|
+
populateAgentFilter(sidebarSessions);
|
|
13970
15793
|
renderFilteredSessions();
|
|
13971
15794
|
|
|
13972
15795
|
// Resolve pending review hash sessions here. Active #session=<id> hashes are
|
|
@@ -13995,7 +15818,7 @@ async function loadRecentSessions() {
|
|
|
13995
15818
|
// If not post-restart (e.g. normal page load with saved review), restore active tab now
|
|
13996
15819
|
if (!state._postRestart) {
|
|
13997
15820
|
const _hashNav = location.hash.slice(1).split('&')[0];
|
|
13998
|
-
const _isHashPanel = ['rules', 'insights', 'permissions', 'prompts', 'codereview', 'walle', 'models', 'backups'].includes(_hashNav);
|
|
15821
|
+
const _isHashPanel = ['command', 'rules', 'insights', 'permissions', 'prompts', 'codereview', 'walle', 'models', 'backups', 'worktrees', 'setup'].includes(_hashNav);
|
|
13999
15822
|
if (_isHashPanel) {
|
|
14000
15823
|
navTo(_hashNav, { skipHash: true });
|
|
14001
15824
|
} else if (state._savedActiveSession && state._savedActiveSession !== 'review') {
|
|
@@ -14034,6 +15857,8 @@ async function loadRecentSessions() {
|
|
|
14034
15857
|
scanPromises.push(_scanPromptLinesFromAPI(id, apiProjectEntry, claudeId));
|
|
14035
15858
|
} else if (caps.promptNavigation === 'terminal') {
|
|
14036
15859
|
_scanPromptLinesFromTerminal(id);
|
|
15860
|
+
} else if (caps.promptNavigation === 'chat' && window.WalleSession && typeof window.WalleSession.updatePromptNav === 'function') {
|
|
15861
|
+
window.WalleSession.updatePromptNav(id);
|
|
14037
15862
|
}
|
|
14038
15863
|
}
|
|
14039
15864
|
}
|
|
@@ -14073,7 +15898,7 @@ function _recentSidebarFilterState() {
|
|
|
14073
15898
|
isCtmSession,
|
|
14074
15899
|
emptyMode: showEmptyOnly ? 'only' : 'exclude',
|
|
14075
15900
|
project: currentProjectFilter,
|
|
14076
|
-
|
|
15901
|
+
agent: currentAgentFilter,
|
|
14077
15902
|
};
|
|
14078
15903
|
}
|
|
14079
15904
|
|
|
@@ -14082,7 +15907,7 @@ function _applyRecentSidebarFilters(sessions, filters) {
|
|
|
14082
15907
|
}
|
|
14083
15908
|
|
|
14084
15909
|
function getFilteredSessions() {
|
|
14085
|
-
return _applyRecentSidebarFilters(
|
|
15910
|
+
return _applyRecentSidebarFilters(getRecentSidebarSessions());
|
|
14086
15911
|
}
|
|
14087
15912
|
|
|
14088
15913
|
let titleGenInProgress = false;
|
|
@@ -14105,6 +15930,7 @@ function _activeTabSessionCandidate(id, s) {
|
|
|
14105
15930
|
sessionId: id,
|
|
14106
15931
|
provisionalId: id,
|
|
14107
15932
|
agentSessionId: s?.meta?.agentSessionId || s?.meta?.agentSessionToken || s?.meta?.claudeSessionId || '',
|
|
15933
|
+
agent: _clientAgentTypeForSession(s),
|
|
14108
15934
|
project: s?.meta?.cwd || '',
|
|
14109
15935
|
projectEntry: '',
|
|
14110
15936
|
cwd: s?.meta?.cwd || '',
|
|
@@ -14121,24 +15947,39 @@ function _activeTabSessionCandidate(id, s) {
|
|
|
14121
15947
|
fileModifiedAt,
|
|
14122
15948
|
timestamp: new Date(s?.meta?.createdAt || Date.now()).toISOString(),
|
|
14123
15949
|
version: '',
|
|
14124
|
-
gitBranch: '',
|
|
15950
|
+
gitBranch: s?.meta?.branch || '',
|
|
14125
15951
|
fileSize: s?.meta?.fileSize || 0,
|
|
14126
15952
|
};
|
|
14127
15953
|
}
|
|
14128
15954
|
|
|
15955
|
+
function _activeTabSessionCandidates() {
|
|
15956
|
+
const candidates = [];
|
|
15957
|
+
for (const [id, s] of state.sessions) candidates.push(_activeTabSessionCandidate(id, s));
|
|
15958
|
+
return candidates;
|
|
15959
|
+
}
|
|
15960
|
+
|
|
15961
|
+
function getRecentSidebarSessions() {
|
|
15962
|
+
return SessionSearchUtils.mergeRecentSessionCandidates(allRecentSessions, _activeTabSessionCandidates());
|
|
15963
|
+
}
|
|
15964
|
+
|
|
14129
15965
|
function renderFilteredSessions() {
|
|
14130
15966
|
// Skip re-render if user just clicked a result (250ms timer in flight)
|
|
14131
15967
|
if (_recentClickTimer) return;
|
|
14132
15968
|
// Skip re-render if user is actively renaming a session in the recent list
|
|
14133
15969
|
const recentList = document.getElementById('recent-list');
|
|
14134
15970
|
if (recentList && recentList.querySelector('input[type="text"]')) return;
|
|
14135
|
-
const q = document.getElementById('recent-search').value
|
|
15971
|
+
const q = SessionSearchUtils.normalizeSearchValue(document.getElementById('recent-search').value);
|
|
14136
15972
|
const sidebarFilters = _recentSidebarFilterState();
|
|
14137
|
-
|
|
15973
|
+
const sidebarSessions = getRecentSidebarSessions();
|
|
15974
|
+
populateAgentFilter(sidebarSessions);
|
|
15975
|
+
let sessions = _applyRecentSidebarFilters(sidebarSessions, sidebarFilters);
|
|
14138
15976
|
if (q && !aiSearchMode) {
|
|
14139
15977
|
// First filter local metadata
|
|
14140
15978
|
const metaMatches = new Set();
|
|
14141
|
-
const recentIds = new Set(
|
|
15979
|
+
const recentIds = new Set();
|
|
15980
|
+
for (const s of sessions) {
|
|
15981
|
+
for (const id of SessionSearchUtils.getSearchableSessionIds(s)) recentIds.add(id);
|
|
15982
|
+
}
|
|
14142
15983
|
sessions = sessions.filter(s => {
|
|
14143
15984
|
// Also check active session label (tab name)
|
|
14144
15985
|
const activeLabel = _activeSessionLabel(s.sessionId, state.sessions.get(s.sessionId));
|
|
@@ -14315,7 +16156,7 @@ function sessionItemHtml(s) {
|
|
|
14315
16156
|
const ago = timeAgo(SessionActivityUtils.sessionTouchedValue(s));
|
|
14316
16157
|
const project = s.project.replace(/^\/Users\/[^/]+\//, '~/');
|
|
14317
16158
|
const displayText = sessionDisplayText(s, '(empty session)');
|
|
14318
|
-
const branch = s.gitBranch ?
|
|
16159
|
+
const branch = s.gitBranch ? branchMetaHtml(s.gitBranch) : '';
|
|
14319
16160
|
const emptyTag = s.isEmpty ? '<span style="color:var(--yellow);font-size:9px;margin-left:4px">[empty]</span>' : '';
|
|
14320
16161
|
const deviceTag = (!thisDevice && s.hostname) ? `<span style="color:var(--accent);font-size:9px;margin-left:4px">${escHtml(s.hostname)}</span>` : '';
|
|
14321
16162
|
const msgCount = s.userMsgCount > 0 ? `<span>${s.userMsgCount} msgs</span>` : '';
|
|
@@ -14334,11 +16175,30 @@ function sessionItemHtml(s) {
|
|
|
14334
16175
|
</div>`;
|
|
14335
16176
|
}
|
|
14336
16177
|
|
|
16178
|
+
function recentSearchEmptyHtml(label) {
|
|
16179
|
+
const input = document.getElementById('recent-search');
|
|
16180
|
+
const rawQuery = (input?.value || '').trim();
|
|
16181
|
+
const normalizedQuery = SessionSearchUtils.normalizeSearchValue(rawQuery);
|
|
16182
|
+
const details = [];
|
|
16183
|
+
if (rawQuery && normalizedQuery && rawQuery.toLowerCase() !== normalizedQuery) {
|
|
16184
|
+
details.push('searching as "' + escHtml(normalizedQuery) + '"');
|
|
16185
|
+
}
|
|
16186
|
+
if (currentAgentFilter) details.push('agent ' + escHtml(SessionSearchUtils.recentAgentFilterLabel(currentAgentFilter)));
|
|
16187
|
+
if (currentProjectFilter) details.push('project ' + escHtml(currentProjectFilter.replace(/^\/Users\/[^/]+/, '~')));
|
|
16188
|
+
details.push(showEmptyOnly ? 'empty only' : 'non-empty');
|
|
16189
|
+
if (document.getElementById('this-device')?.checked !== false) details.push('this device');
|
|
16190
|
+
if (document.getElementById('hide-ctm')?.checked) details.push('hide CTM');
|
|
16191
|
+
const detailHtml = rawQuery && details.length
|
|
16192
|
+
? '<div style="margin-top:6px;color:var(--fg-dim);line-height:1.35">Filters: ' + details.join(', ') + '</div>'
|
|
16193
|
+
: '';
|
|
16194
|
+
return '<div style="padding:10px;font-size:12px;color:var(--fg-dim)">' + escHtml(label || 'No sessions found') + detailHtml + '</div>';
|
|
16195
|
+
}
|
|
16196
|
+
|
|
14337
16197
|
function renderRecentSessions(sessions) {
|
|
14338
16198
|
const list = document.getElementById('recent-session-list');
|
|
14339
16199
|
|
|
14340
16200
|
if (sessions.length === 0) {
|
|
14341
|
-
list.innerHTML = '
|
|
16201
|
+
list.innerHTML = recentSearchEmptyHtml('No sessions found');
|
|
14342
16202
|
return;
|
|
14343
16203
|
}
|
|
14344
16204
|
|
|
@@ -14768,15 +16628,20 @@ async function reviewSession(sessionId, projectEntry, title, sessionData, opts)
|
|
|
14768
16628
|
return;
|
|
14769
16629
|
}
|
|
14770
16630
|
|
|
14771
|
-
|
|
14772
|
-
|
|
14773
|
-
|
|
14774
|
-
|
|
14775
|
-
|
|
16631
|
+
// Chunked rendering: show first batch immediately, render rest progressively.
|
|
16632
|
+
// The top-level unit is now a prompt turn: user prompt first, with
|
|
16633
|
+
// assistant/tool/system detail collapsed underneath.
|
|
16634
|
+
const turns = groupMessagesIntoTurns(messages);
|
|
16635
|
+
const FIRST_BATCH = 20;
|
|
16636
|
+
const CHUNK_SIZE = 30;
|
|
16637
|
+
const container = document.getElementById('review-messages');
|
|
16638
|
+
container.dataset.turnMode = 'review';
|
|
16639
|
+
container.dataset.sessionId = sessionId;
|
|
14776
16640
|
|
|
14777
|
-
|
|
14778
|
-
|
|
14779
|
-
|
|
16641
|
+
// renderReviewTurn returns sanitized inner message HTML (uses DOMPurify
|
|
16642
|
+
// internally via formatMsgText); sanitize the wrapper before insertion.
|
|
16643
|
+
const firstHtml = turns.slice(0, FIRST_BATCH).map(renderReviewTurn).join('');
|
|
16644
|
+
container.innerHTML = DOMPurify.sanitize(firstHtml, { ADD_ATTR: ['style', 'data-idx', 'data-role', 'data-turn-id', 'data-msg-idx', 'data-parent-uuid', 'role', 'tabindex', 'aria-expanded'] });
|
|
14780
16645
|
|
|
14781
16646
|
// Show partial load indicator if backend couldn't load all files
|
|
14782
16647
|
if (isPartial) {
|
|
@@ -14788,20 +16653,20 @@ async function reviewSession(sessionId, projectEntry, title, sessionData, opts)
|
|
|
14788
16653
|
}
|
|
14789
16654
|
|
|
14790
16655
|
// Render remaining groups in async chunks to avoid blocking the main thread
|
|
14791
|
-
|
|
14792
|
-
|
|
14793
|
-
|
|
14794
|
-
|
|
14795
|
-
|
|
14796
|
-
|
|
14797
|
-
|
|
14798
|
-
|
|
14799
|
-
|
|
14800
|
-
|
|
14801
|
-
|
|
14802
|
-
|
|
14803
|
-
|
|
14804
|
-
|
|
16656
|
+
if (turns.length > FIRST_BATCH) {
|
|
16657
|
+
let offset = FIRST_BATCH;
|
|
16658
|
+
const renderNextChunk = () => {
|
|
16659
|
+
if (offset >= turns.length) {
|
|
16660
|
+
toggleToolMsgs();
|
|
16661
|
+
reviewPromptNavReset();
|
|
16662
|
+
return;
|
|
16663
|
+
}
|
|
16664
|
+
const end = Math.min(offset + CHUNK_SIZE, turns.length);
|
|
16665
|
+
const chunkHtml = turns.slice(offset, end).map(renderReviewTurn).join('');
|
|
16666
|
+
container.insertAdjacentHTML('beforeend', DOMPurify.sanitize(chunkHtml, { ADD_ATTR: ['style', 'data-idx', 'data-role', 'data-turn-id', 'data-msg-idx', 'data-parent-uuid', 'role', 'tabindex', 'aria-expanded'] }));
|
|
16667
|
+
offset = end;
|
|
16668
|
+
requestAnimationFrame(renderNextChunk);
|
|
16669
|
+
};
|
|
14805
16670
|
requestAnimationFrame(renderNextChunk);
|
|
14806
16671
|
}
|
|
14807
16672
|
|
|
@@ -14847,14 +16712,16 @@ function _wireReviewLiveStream(sessionId) {
|
|
|
14847
16712
|
}
|
|
14848
16713
|
}
|
|
14849
16714
|
|
|
14850
|
-
// Pre-process messages into renderable groups (cheap — no markdown yet)
|
|
14851
|
-
// groupMessages was extracted to public/js/message-renderer.js (Phase 1).
|
|
14852
|
-
function groupMessages(messages) { return MR.groupMessages(messages); }
|
|
16715
|
+
// Pre-process messages into renderable groups (cheap — no markdown yet)
|
|
16716
|
+
// groupMessages was extracted to public/js/message-renderer.js (Phase 1).
|
|
16717
|
+
function groupMessages(messages) { return MR.groupMessages(messages); }
|
|
16718
|
+
function groupMessagesIntoTurns(messages) { return MR.groupMessagesIntoTurns(messages); }
|
|
14853
16719
|
|
|
14854
|
-
// Render a single group to HTML string
|
|
14855
|
-
function renderGroup(g) { return MR.renderGroup(g); }
|
|
16720
|
+
// Render a single group to HTML string
|
|
16721
|
+
function renderGroup(g) { return MR.renderGroup(g); }
|
|
16722
|
+
function renderReviewTurn(turn, i) { return MR.renderReviewTurn(turn, i); }
|
|
14856
16723
|
|
|
14857
|
-
function renderGroupedMessages(messages) { return MR.groupMessages(messages).map(MR.renderGroup).join(''); }
|
|
16724
|
+
function renderGroupedMessages(messages) { return MR.groupMessages(messages).map(MR.renderGroup).join(''); }
|
|
14858
16725
|
|
|
14859
16726
|
function renderSelfThoughtMsg(m, i, stripped) { return MR.renderSelfThoughtMsg(m, i, stripped); }
|
|
14860
16727
|
|
|
@@ -14864,12 +16731,48 @@ function classifyMessage(m, stripped, isToolOnly) { return MR.classifyMessage(m,
|
|
|
14864
16731
|
|
|
14865
16732
|
function renderReviewMsg(m, i, msgType) { return MR.renderReviewMsg(m, i, msgType); }
|
|
14866
16733
|
|
|
14867
|
-
function toggleMsgExpand(btn) {
|
|
14868
|
-
|
|
14869
|
-
|
|
14870
|
-
|
|
14871
|
-
|
|
14872
|
-
}
|
|
16734
|
+
function toggleMsgExpand(btn) {
|
|
16735
|
+
const textEl = btn.previousElementSibling;
|
|
16736
|
+
const isCollapsed = textEl.classList.contains('collapsed');
|
|
16737
|
+
textEl.classList.toggle('collapsed');
|
|
16738
|
+
btn.textContent = isCollapsed ? 'Show less' : 'Show more';
|
|
16739
|
+
}
|
|
16740
|
+
|
|
16741
|
+
function setPromptTurnExpanded(turnEl, expanded) {
|
|
16742
|
+
if (!turnEl) return;
|
|
16743
|
+
if (typeof MR !== 'undefined' && typeof MR.setPromptTurnExpanded === 'function') {
|
|
16744
|
+
MR.setPromptTurnExpanded(turnEl, expanded);
|
|
16745
|
+
return;
|
|
16746
|
+
}
|
|
16747
|
+
turnEl.classList.toggle('expanded', !!expanded);
|
|
16748
|
+
const header = turnEl.querySelector('.prompt-turn-header');
|
|
16749
|
+
if (header) header.setAttribute('aria-expanded', expanded ? 'true' : 'false');
|
|
16750
|
+
}
|
|
16751
|
+
|
|
16752
|
+
function togglePromptTurn(headerEl) {
|
|
16753
|
+
const turnEl = headerEl && headerEl.closest('.prompt-turn');
|
|
16754
|
+
if (!turnEl) return;
|
|
16755
|
+
setPromptTurnExpanded(turnEl, !turnEl.classList.contains('expanded'));
|
|
16756
|
+
}
|
|
16757
|
+
|
|
16758
|
+
document.addEventListener('click', (e) => {
|
|
16759
|
+
const header = e.target.closest && e.target.closest('#review-messages .prompt-turn-header');
|
|
16760
|
+
if (!header) return;
|
|
16761
|
+
const reviewMessages = document.getElementById('review-messages');
|
|
16762
|
+
if (reviewMessages && reviewMessages.classList.contains('truncate-mode')) return;
|
|
16763
|
+
if (e.target.closest('a,button,input,textarea,select')) return;
|
|
16764
|
+
togglePromptTurn(header);
|
|
16765
|
+
});
|
|
16766
|
+
|
|
16767
|
+
document.addEventListener('keydown', (e) => {
|
|
16768
|
+
const header = e.target.closest && e.target.closest('#review-messages .prompt-turn-header');
|
|
16769
|
+
if (!header) return;
|
|
16770
|
+
const reviewMessages = document.getElementById('review-messages');
|
|
16771
|
+
if (reviewMessages && reviewMessages.classList.contains('truncate-mode')) return;
|
|
16772
|
+
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
16773
|
+
e.preventDefault();
|
|
16774
|
+
togglePromptTurn(header);
|
|
16775
|
+
});
|
|
14873
16776
|
|
|
14874
16777
|
// --- Review Search ---
|
|
14875
16778
|
let _reviewSearchMatches = [];
|
|
@@ -14963,19 +16866,26 @@ function scrollToSearchMatch(idx) {
|
|
|
14963
16866
|
const match = _reviewSearchMatches[idx];
|
|
14964
16867
|
match.classList.add('current');
|
|
14965
16868
|
|
|
14966
|
-
|
|
14967
|
-
|
|
14968
|
-
|
|
14969
|
-
|
|
16869
|
+
// Expand collapsed message if match is inside one
|
|
16870
|
+
const msgText = match.closest('.msg-text');
|
|
16871
|
+
if (msgText && msgText.classList.contains('collapsed')) {
|
|
16872
|
+
msgText.classList.remove('collapsed');
|
|
14970
16873
|
const btn = msgText.nextElementSibling;
|
|
14971
16874
|
if (btn && btn.classList.contains('msg-expand')) btn.textContent = 'Show less';
|
|
14972
16875
|
}
|
|
14973
16876
|
|
|
14974
|
-
|
|
14975
|
-
|
|
14976
|
-
|
|
16877
|
+
// Unhide tool-only messages if match is inside one
|
|
16878
|
+
const msgEl = match.closest('.review-msg');
|
|
16879
|
+
if (msgEl && msgEl.style.display === 'none') msgEl.style.display = '';
|
|
14977
16880
|
|
|
14978
|
-
|
|
16881
|
+
// Search includes collapsed response detail. Expand the owning prompt
|
|
16882
|
+
// turn and any nested collapsible group before scrolling to the match.
|
|
16883
|
+
const turnEl = match.closest('.prompt-turn');
|
|
16884
|
+
if (turnEl) setPromptTurnExpanded(turnEl, true);
|
|
16885
|
+
const group = match.closest('.thought-group, .review-msg.skill-body, .review-msg.local-cmd, .review-msg.summary');
|
|
16886
|
+
if (group && !group.classList.contains('expanded')) group.classList.add('expanded');
|
|
16887
|
+
|
|
16888
|
+
match.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
14979
16889
|
document.getElementById('review-search-count').textContent = `${idx + 1} / ${_reviewSearchMatches.length}`;
|
|
14980
16890
|
}
|
|
14981
16891
|
|
|
@@ -15034,16 +16944,16 @@ function toggleTruncateMode(sessionId, project) {
|
|
|
15034
16944
|
document.getElementById('truncate-info').textContent = 'Click a user message to set the cut point.';
|
|
15035
16945
|
document.getElementById('truncate-do-btn').style.display = 'none';
|
|
15036
16946
|
|
|
15037
|
-
|
|
15038
|
-
|
|
15039
|
-
|
|
15040
|
-
|
|
15041
|
-
|
|
15042
|
-
}
|
|
16947
|
+
// Add click handlers to prompt turn headers
|
|
16948
|
+
msgEl.querySelectorAll('.prompt-turn:not(.setup-turn) .prompt-turn-header').forEach(header => {
|
|
16949
|
+
header._truncateClick = () => selectTruncatePoint(header);
|
|
16950
|
+
header.addEventListener('click', header._truncateClick);
|
|
16951
|
+
});
|
|
16952
|
+
}
|
|
15043
16953
|
|
|
15044
|
-
function selectTruncatePoint(headerEl) {
|
|
15045
|
-
|
|
15046
|
-
|
|
16954
|
+
function selectTruncatePoint(headerEl) {
|
|
16955
|
+
const msgEl = headerEl.closest('.prompt-turn') || headerEl.closest('.review-msg');
|
|
16956
|
+
const container = document.getElementById('review-messages');
|
|
15047
16957
|
|
|
15048
16958
|
// Find the message index from data-msg-idx on the msg-text child
|
|
15049
16959
|
const textEl = msgEl.querySelector('[data-msg-idx]');
|
|
@@ -15056,9 +16966,9 @@ function selectTruncatePoint(headerEl) {
|
|
|
15056
16966
|
container.querySelectorAll('.truncate-remove').forEach(el => el.classList.remove('truncate-remove'));
|
|
15057
16967
|
container.querySelectorAll('.truncate-cut-line').forEach(el => el.remove());
|
|
15058
16968
|
|
|
15059
|
-
|
|
15060
|
-
|
|
15061
|
-
|
|
16969
|
+
// Use direct children of the container to avoid nested-element double-counting
|
|
16970
|
+
const directChildren = Array.from(container.children).filter(el =>
|
|
16971
|
+
el.classList.contains('prompt-turn') || el.classList.contains('review-msg') || el.classList.contains('thought-group'));
|
|
15062
16972
|
|
|
15063
16973
|
// "Cut from here" — the clicked message and everything after it gets removed.
|
|
15064
16974
|
// We keep the previous assistant response as the last kept item.
|
|
@@ -15113,10 +17023,10 @@ function cancelTruncate() {
|
|
|
15113
17023
|
msgEl.querySelectorAll('.truncate-cut-line').forEach(el => el.remove());
|
|
15114
17024
|
|
|
15115
17025
|
// Remove click handlers
|
|
15116
|
-
|
|
15117
|
-
|
|
15118
|
-
|
|
15119
|
-
|
|
17026
|
+
msgEl.querySelectorAll('.prompt-turn:not(.setup-turn) .prompt-turn-header').forEach(header => {
|
|
17027
|
+
if (header._truncateClick) {
|
|
17028
|
+
header.removeEventListener('click', header._truncateClick);
|
|
17029
|
+
delete header._truncateClick;
|
|
15120
17030
|
}
|
|
15121
17031
|
});
|
|
15122
17032
|
|
|
@@ -15162,12 +17072,12 @@ function scrollReviewToBottom() {
|
|
|
15162
17072
|
let _reviewPromptEls = [];
|
|
15163
17073
|
let _reviewPromptIdx = -1;
|
|
15164
17074
|
|
|
15165
|
-
function reviewPromptNavReset() {
|
|
15166
|
-
|
|
15167
|
-
|
|
15168
|
-
|
|
15169
|
-
|
|
15170
|
-
}
|
|
17075
|
+
function reviewPromptNavReset() {
|
|
17076
|
+
const container = document.getElementById('review-messages');
|
|
17077
|
+
_reviewPromptEls = container ? Array.from(container.querySelectorAll('.prompt-turn:not(.setup-turn)')) : [];
|
|
17078
|
+
_reviewPromptIdx = -1;
|
|
17079
|
+
reviewPromptNavUpdateBadge();
|
|
17080
|
+
}
|
|
15171
17081
|
|
|
15172
17082
|
function reviewPromptNavGo(dir) {
|
|
15173
17083
|
if (_reviewPromptEls.length === 0) return;
|
|
@@ -15224,12 +17134,12 @@ function reviewPromptNavToggleList() {
|
|
|
15224
17134
|
if (existing) { existing.remove(); return; }
|
|
15225
17135
|
if (_reviewPromptEls.length === 0) return;
|
|
15226
17136
|
|
|
15227
|
-
|
|
15228
|
-
|
|
15229
|
-
|
|
15230
|
-
|
|
15231
|
-
|
|
15232
|
-
|
|
17137
|
+
const list = document.createElement('div');
|
|
17138
|
+
list.className = 'prompt-nav-list';
|
|
17139
|
+
// Show most recent first
|
|
17140
|
+
for (let i = _reviewPromptEls.length - 1; i >= 0; i--) {
|
|
17141
|
+
const msgText = _reviewPromptEls[i].querySelector('.prompt-turn-prompt .msg-text');
|
|
17142
|
+
let text = msgText ? msgText.textContent.trim() : '(empty)';
|
|
15233
17143
|
if (text.length > 80) text = text.slice(0, 80) + '\u2026';
|
|
15234
17144
|
const item = document.createElement('div');
|
|
15235
17145
|
item.className = 'prompt-nav-list-item' + (i === _reviewPromptIdx ? ' current' : '');
|
|
@@ -15288,65 +17198,60 @@ function setSessionSort(sort) {
|
|
|
15288
17198
|
renderFilteredSessions();
|
|
15289
17199
|
}
|
|
15290
17200
|
|
|
15291
|
-
let
|
|
15292
|
-
function
|
|
15293
|
-
|
|
15294
|
-
savePref('
|
|
17201
|
+
let currentAgentFilter = '';
|
|
17202
|
+
function setAgentFilter(agent) {
|
|
17203
|
+
currentAgentFilter = SessionSearchUtils.normalizeRecentAgentType(agent) || '';
|
|
17204
|
+
savePref('agent_filter', currentAgentFilter);
|
|
15295
17205
|
refreshRecentSearchAfterFilterChange();
|
|
15296
17206
|
}
|
|
15297
17207
|
|
|
15298
|
-
|
|
15299
|
-
|
|
15300
|
-
|
|
15301
|
-
|
|
15302
|
-
function modelFilterPriority(model) {
|
|
15303
|
-
const m = String(model || '').toLowerCase();
|
|
15304
|
-
if (/^(gpt-|o[1-9]|codex-)/.test(m)) return 0;
|
|
15305
|
-
if (/^claude-/.test(m)) return 1;
|
|
15306
|
-
if (/^gemini-/.test(m)) return 2;
|
|
15307
|
-
return 3;
|
|
15308
|
-
}
|
|
15309
|
-
|
|
15310
|
-
function modelFilterLabel(model) {
|
|
15311
|
-
if (/^claude-/.test(model)) return model.replace(/^claude-/, '');
|
|
15312
|
-
return model;
|
|
17208
|
+
// Back-compat for older cached markup/tests that still call the old handler.
|
|
17209
|
+
function setModelFilter(agent) {
|
|
17210
|
+
setAgentFilter(agent);
|
|
15313
17211
|
}
|
|
15314
17212
|
|
|
15315
|
-
function
|
|
17213
|
+
function populateAgentFilter(sessions) {
|
|
15316
17214
|
const sel = document.getElementById('model-filter');
|
|
15317
17215
|
if (!sel) return;
|
|
15318
|
-
const
|
|
17216
|
+
const agents = new Map();
|
|
15319
17217
|
for (const s of sessions) {
|
|
15320
|
-
|
|
15321
|
-
|
|
15322
|
-
}
|
|
17218
|
+
const agent = SessionSearchUtils.getRecentSessionAgentType(s);
|
|
17219
|
+
agents.set(agent, (agents.get(agent) || 0) + 1);
|
|
15323
17220
|
}
|
|
15324
|
-
const sorted = [...
|
|
15325
|
-
const pa =
|
|
15326
|
-
const pb =
|
|
17221
|
+
const sorted = [...agents.entries()].sort((a, b) => {
|
|
17222
|
+
const pa = SessionSearchUtils.recentAgentFilterPriority(a[0]);
|
|
17223
|
+
const pb = SessionSearchUtils.recentAgentFilterPriority(b[0]);
|
|
15327
17224
|
if (pa !== pb) return pa - pb;
|
|
15328
17225
|
if (b[1] !== a[1]) return b[1] - a[1];
|
|
15329
|
-
return
|
|
17226
|
+
return SessionSearchUtils.recentAgentFilterLabel(a[0]).localeCompare(
|
|
17227
|
+
SessionSearchUtils.recentAgentFilterLabel(b[0]),
|
|
17228
|
+
undefined,
|
|
17229
|
+
{ numeric: true, sensitivity: 'base' }
|
|
17230
|
+
);
|
|
15330
17231
|
});
|
|
15331
17232
|
const prev = sel.value;
|
|
15332
17233
|
// Build options safely using DOM APIs
|
|
15333
17234
|
while (sel.options.length > 0) sel.remove(0);
|
|
15334
17235
|
const allOpt = document.createElement('option');
|
|
15335
17236
|
allOpt.value = '';
|
|
15336
|
-
allOpt.textContent = 'All
|
|
17237
|
+
allOpt.textContent = 'All Agents (' + sessions.length + ')';
|
|
15337
17238
|
sel.add(allOpt);
|
|
15338
|
-
for (const [
|
|
17239
|
+
for (const [agent, count] of sorted) {
|
|
15339
17240
|
const opt = document.createElement('option');
|
|
15340
|
-
opt.value =
|
|
15341
|
-
opt.textContent =
|
|
17241
|
+
opt.value = agent;
|
|
17242
|
+
opt.textContent = SessionSearchUtils.recentAgentFilterLabel(agent) + ' (' + count + ')';
|
|
15342
17243
|
sel.add(opt);
|
|
15343
17244
|
}
|
|
15344
|
-
const nextValue = prev ||
|
|
15345
|
-
sel.value =
|
|
15346
|
-
if (
|
|
17245
|
+
const nextValue = SessionSearchUtils.normalizeRecentAgentType(prev || currentAgentFilter || '');
|
|
17246
|
+
sel.value = agents.has(nextValue) ? nextValue : '';
|
|
17247
|
+
if (currentAgentFilter && !agents.has(currentAgentFilter)) currentAgentFilter = '';
|
|
17248
|
+
}
|
|
17249
|
+
|
|
17250
|
+
function populateModelFilter(sessions) {
|
|
17251
|
+
populateAgentFilter(sessions);
|
|
15347
17252
|
}
|
|
15348
17253
|
|
|
15349
|
-
// Strip worktree suffix from project path: /repo/.claude/worktrees/name → /repo
|
|
17254
|
+
// Strip worktree suffix from project path: /repo/.claude|.walle/worktrees/name → /repo
|
|
15350
17255
|
function _stripWorktreePath(p) {
|
|
15351
17256
|
return SessionSearchUtils.stripWorktreePath(p);
|
|
15352
17257
|
}
|
|
@@ -15448,13 +17353,14 @@ function escHtml(s) {
|
|
|
15448
17353
|
// element (.session-item or .tab) sets color via CSS so the icon inherits the
|
|
15449
17354
|
// per-provider hue and inverts cleanly when the row is active (light bg, dark text).
|
|
15450
17355
|
// Returns an HTML string with title for screen-readers + hover tooltip.
|
|
15451
|
-
const AGENT_LABELS = { walle: 'Wall-E', codex: 'Codex', gemini: 'Gemini', claude: 'Claude Code', 'claude-desktop': 'Claude Desktop', shell: 'Shell' };
|
|
17356
|
+
const AGENT_LABELS = { walle: 'Wall-E', codex: 'Codex', gemini: 'Gemini', opencode: 'OpenCode', claude: 'Claude Code', 'claude-desktop': 'Claude Desktop', shell: 'Shell' };
|
|
15452
17357
|
function getAgentType(s) {
|
|
15453
17358
|
if (!s) return 'shell';
|
|
15454
17359
|
if (s.meta?.type === 'walle') return 'walle';
|
|
15455
|
-
const cmd = s.meta?.cmd || '';
|
|
17360
|
+
const cmd = String(s.meta?.cmd || '').toLowerCase();
|
|
15456
17361
|
if (cmd.includes('codex')) return 'codex';
|
|
15457
17362
|
if (cmd.includes('gemini')) return 'gemini';
|
|
17363
|
+
if (cmd.includes('opencode') || cmd.includes('open-code')) return 'opencode';
|
|
15458
17364
|
if (cmd.includes('claude')) return 'claude';
|
|
15459
17365
|
return 'shell';
|
|
15460
17366
|
}
|
|
@@ -15500,6 +17406,12 @@ function providerIconSvg(agentType, sizePx) {
|
|
|
15500
17406
|
// provider icons in the same row.
|
|
15501
17407
|
inner = '<path fill="currentColor" d="M8 0.8 L9 7 L15.2 8 L9 9 L8 15.2 L7 9 L0.8 8 L7 7 Z"/>';
|
|
15502
17408
|
break;
|
|
17409
|
+
case 'opencode':
|
|
17410
|
+
// Code brackets with a center dot: compact enough for tabs, distinct
|
|
17411
|
+
// from the generic terminal prompt used for plain shell sessions.
|
|
17412
|
+
inner = '<path fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" d="M6 4 L3 8 L6 12 M10 4 L13 8 L10 12"/>'
|
|
17413
|
+
+ '<circle cx="8" cy="8" r="1.2" fill="currentColor"/>';
|
|
17414
|
+
break;
|
|
15503
17415
|
case 'shell':
|
|
15504
17416
|
// >_ prompt — universal terminal metaphor.
|
|
15505
17417
|
inner = '<path fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" d="M2.5 5 L6 8 L2.5 11 M8.5 12 L13.5 12"/>';
|
|
@@ -15765,7 +17677,7 @@ function _matchesPaletteQuery(item, q) {
|
|
|
15765
17677
|
}
|
|
15766
17678
|
|
|
15767
17679
|
function searchSessionPalette(query) {
|
|
15768
|
-
const q = (query
|
|
17680
|
+
const q = SessionSearchUtils.normalizeSearchValue(query);
|
|
15769
17681
|
const { activeItems, recentItems } = _sessionPaletteSourceItems();
|
|
15770
17682
|
const filteredActive = activeItems.filter((it) => _matchesPaletteQuery(it, q));
|
|
15771
17683
|
const filteredRecent = recentItems.filter((it) => _matchesPaletteQuery(it, q));
|
|
@@ -15900,19 +17812,19 @@ async function performServerSearch(query) {
|
|
|
15900
17812
|
if (data.results && data.results.length > 0) {
|
|
15901
17813
|
_serverSearchActive = true;
|
|
15902
17814
|
// Apply the exact same sidebar filters to server results as the local
|
|
15903
|
-
// list. Otherwise the badge can count rows hidden by Empty/project/
|
|
17815
|
+
// list. Otherwise the badge can count rows hidden by Empty/project/agent
|
|
15904
17816
|
// filters while the rendered list says "No sessions found".
|
|
15905
17817
|
const sidebarFilters = _recentSidebarFilterState();
|
|
15906
|
-
const serverFiltered = _applyRecentSidebarFilters(data.results, sidebarFilters);
|
|
17818
|
+
const serverFiltered = SessionSearchUtils.dedupeSessionCandidates(_applyRecentSidebarFilters(data.results, sidebarFilters));
|
|
15907
17819
|
if (serverFiltered.length === 0) {
|
|
15908
|
-
// All server results were filtered out (e.g., Empty/project/
|
|
17820
|
+
// All server results were filtered out (e.g., Empty/project/agent).
|
|
15909
17821
|
// Fall back to local-only rendering
|
|
15910
17822
|
_serverSearchActive = false;
|
|
15911
17823
|
renderFilteredSessions();
|
|
15912
17824
|
return;
|
|
15913
17825
|
}
|
|
15914
17826
|
// Merge server results with local matches so local hits aren't lost
|
|
15915
|
-
const lq =
|
|
17827
|
+
const lq = SessionSearchUtils.normalizeSearchValue(query);
|
|
15916
17828
|
const localSessions = getFilteredSessions().filter(s => {
|
|
15917
17829
|
const activeLabel = _activeSessionLabel(s.sessionId, state.sessions.get(s.sessionId));
|
|
15918
17830
|
return SessionSearchUtils.sessionMatchesRecentSearchQuery(s, lq, activeLabel);
|
|
@@ -15931,11 +17843,12 @@ async function performServerSearch(query) {
|
|
|
15931
17843
|
localSessions.push(candidate);
|
|
15932
17844
|
}
|
|
15933
17845
|
}
|
|
15934
|
-
const serverIds = new Set(serverFiltered.map(r => r.sessionId));
|
|
15935
17846
|
const merged = [...serverFiltered];
|
|
17847
|
+
const mergedIdentityIndex = SessionSearchUtils.createSessionIdentityIndex(merged);
|
|
15936
17848
|
let localOnlyCount = 0;
|
|
15937
17849
|
for (const ls of localSessions) {
|
|
15938
|
-
|
|
17850
|
+
const existing = SessionSearchUtils.findSessionIdentityMatch(mergedIdentityIndex, ls);
|
|
17851
|
+
if (!existing) {
|
|
15939
17852
|
const idScore = SessionSearchUtils.scoreSessionIdMatch(ls, lq);
|
|
15940
17853
|
if (idScore > 0) {
|
|
15941
17854
|
ls._score = idScore / 1000;
|
|
@@ -15950,7 +17863,10 @@ async function performServerSearch(query) {
|
|
|
15950
17863
|
ls._matchField = exactTitle ? 'title (exact)' : titleContains ? 'title' : 'metadata';
|
|
15951
17864
|
}
|
|
15952
17865
|
merged.push(ls);
|
|
17866
|
+
for (const id of SessionSearchUtils.getSearchableSessionIds(ls)) mergedIdentityIndex.set(id, ls);
|
|
15953
17867
|
localOnlyCount++;
|
|
17868
|
+
} else {
|
|
17869
|
+
SessionSearchUtils.mergeRecentSessionMetadata(existing, ls);
|
|
15954
17870
|
}
|
|
15955
17871
|
}
|
|
15956
17872
|
merged.sort((a, b) => (b._score || 0) - (a._score || 0) || SessionActivityUtils.sessionTouchedAtMs(b) - SessionActivityUtils.sessionTouchedAtMs(a));
|
|
@@ -15975,7 +17891,7 @@ function renderServerSearchResults(results, total, query) {
|
|
|
15975
17891
|
return;
|
|
15976
17892
|
}
|
|
15977
17893
|
const list = document.getElementById('recent-session-list');
|
|
15978
|
-
const terms =
|
|
17894
|
+
const terms = SessionSearchUtils.normalizeSearchValue(query).split(/\s+/).filter(t => t.length >= 2);
|
|
15979
17895
|
updateSearchCount(total);
|
|
15980
17896
|
|
|
15981
17897
|
function highlightTerms(text) {
|
|
@@ -15993,7 +17909,7 @@ function renderServerSearchResults(results, total, query) {
|
|
|
15993
17909
|
const displayText = sessionDisplayText(s, '(empty session)');
|
|
15994
17910
|
const project = s.project.replace(/^\/Users\/[^/]+\//, '~/');
|
|
15995
17911
|
const ago = timeAgo(SessionActivityUtils.sessionTouchedValue(s));
|
|
15996
|
-
const branch = s.gitBranch ?
|
|
17912
|
+
const branch = s.gitBranch ? branchMetaHtml(s.gitBranch) : '';
|
|
15997
17913
|
const msgCount = s.userMsgCount > 0 ? `<span>${s.userMsgCount} msgs</span>` : '';
|
|
15998
17914
|
const isPinned = pinnedSessionIds.includes(s.sessionId);
|
|
15999
17915
|
const pinCls = isPinned ? ' pinned' : '';
|
|
@@ -16024,7 +17940,7 @@ function renderServerSearchResults(results, total, query) {
|
|
|
16024
17940
|
|
|
16025
17941
|
// Note: html is safe — all user data is escaped via escHtml(), highlights use <mark> only.
|
|
16026
17942
|
// DOMPurify was stripping onclick/ondblclick/oncontextmenu handlers, breaking click-to-review.
|
|
16027
|
-
list.innerHTML = html || '
|
|
17943
|
+
list.innerHTML = html || recentSearchEmptyHtml('No results found');
|
|
16028
17944
|
}
|
|
16029
17945
|
|
|
16030
17946
|
function updateSearchCount(count) {
|
|
@@ -17724,6 +19640,9 @@ function onDataChanged(msg) {
|
|
|
17724
19640
|
}
|
|
17725
19641
|
if (r === 'models' || r === 'model-registry' || r === 'providers') {
|
|
17726
19642
|
_modelRegistryCache = null; // Invalidate cache so switchers re-fetch
|
|
19643
|
+
if (typeof WalleSession !== 'undefined' && WalleSession.invalidateModelCache) {
|
|
19644
|
+
WalleSession.invalidateModelCache();
|
|
19645
|
+
}
|
|
17727
19646
|
}
|
|
17728
19647
|
}
|
|
17729
19648
|
|
|
@@ -17740,9 +19659,16 @@ function clearWaitingState(sessionId, opts = {}) {
|
|
|
17740
19659
|
const wasWaiting = s._waitingForInput;
|
|
17741
19660
|
s._waitingForInput = false;
|
|
17742
19661
|
s._waitingForInputAt = 0;
|
|
19662
|
+
s._waitingReason = '';
|
|
17743
19663
|
const now = opts.timestamp || Date.now();
|
|
17744
19664
|
if (opts.markInput !== false) s._lastInputAt = now;
|
|
17745
|
-
if (opts.markWorking)
|
|
19665
|
+
if (opts.markWorking) {
|
|
19666
|
+
s._serverWorkingAt = now;
|
|
19667
|
+
s._serverWorkingEventAt = opts.eventTimestamp || now;
|
|
19668
|
+
s._serverLiveStatus = 'running';
|
|
19669
|
+
s._serverLiveStatusAt = now;
|
|
19670
|
+
_markClientCodexRunningEvidence(s, opts.eventTimestamp || now, now);
|
|
19671
|
+
}
|
|
17746
19672
|
// Only do DOM work if the session was actually in waiting state
|
|
17747
19673
|
if (wasWaiting) {
|
|
17748
19674
|
const tabs = document.querySelectorAll('#tabbar .tab');
|
|
@@ -17800,18 +19726,38 @@ function playNotificationSound(type) {
|
|
|
17800
19726
|
function onSessionActivity(msg) {
|
|
17801
19727
|
if (!msg.sessions) return;
|
|
17802
19728
|
let shouldRerender = false;
|
|
17803
|
-
for (const { id, ts, state: serverState } of msg.sessions) {
|
|
19729
|
+
for (const { id, ts, state: serverState, status } of msg.sessions) {
|
|
17804
19730
|
const s = state.sessions.get(id);
|
|
17805
19731
|
if (!s) continue;
|
|
17806
19732
|
const oldBucket = activeActivityBucket(s);
|
|
17807
19733
|
const oldStatus = getSessionStatus(s).cls;
|
|
17808
19734
|
const serverTs = SessionActivityUtils.parseTimeMs(ts) || Date.now();
|
|
17809
|
-
|
|
17810
|
-
|
|
17811
|
-
|
|
17812
|
-
|
|
19735
|
+
const liveStatus = normalizeLiveSessionStatus(status || serverState);
|
|
19736
|
+
const waitingAt = s._waitingForInputAt || 0;
|
|
19737
|
+
const activityIsNewerThanWaiting = !s._waitingForInput || !waitingAt || serverTs >= waitingAt;
|
|
19738
|
+
if (liveStatus && (liveStatus !== 'running' || activityIsNewerThanWaiting)) {
|
|
19739
|
+
s._serverLiveStatus = liveStatus;
|
|
19740
|
+
s._serverLiveStatusAt = Date.now();
|
|
19741
|
+
if (s.meta) s.meta.liveStatus = liveStatus;
|
|
19742
|
+
if (liveStatus !== 'running') {
|
|
19743
|
+
s._serverWorkingAt = 0;
|
|
19744
|
+
s._serverWorkingEventAt = 0;
|
|
19745
|
+
s._codexRunningHoldUntil = 0;
|
|
19746
|
+
}
|
|
19747
|
+
}
|
|
19748
|
+
if (liveStatus === 'waiting') {
|
|
19749
|
+
if (!s._waitingForInput) s._waitingForInputAt = serverTs;
|
|
19750
|
+
s._waitingForInput = true;
|
|
19751
|
+
}
|
|
19752
|
+
if (liveStatus === 'running') {
|
|
19753
|
+
if (activityIsNewerThanWaiting) {
|
|
19754
|
+
const receivedAt = Date.now();
|
|
19755
|
+
s._serverWorkingAt = receivedAt;
|
|
19756
|
+
s._serverWorkingEventAt = serverTs;
|
|
19757
|
+
_markClientCodexRunningEvidence(s, serverTs, receivedAt);
|
|
19758
|
+
}
|
|
17813
19759
|
if (s._waitingForInput && activityIsNewerThanWaiting) {
|
|
17814
|
-
clearWaitingState(id, { markInput: false, markWorking: true });
|
|
19760
|
+
clearWaitingState(id, { markInput: false, markWorking: true, eventTimestamp: serverTs });
|
|
17815
19761
|
}
|
|
17816
19762
|
}
|
|
17817
19763
|
if (s.meta && ts) {
|
|
@@ -17824,6 +19770,7 @@ function onSessionActivity(msg) {
|
|
|
17824
19770
|
}
|
|
17825
19771
|
}
|
|
17826
19772
|
if (shouldRerender) renderSessionList();
|
|
19773
|
+
if (typeof scheduleStandupRefresh === 'function') scheduleStandupRefresh();
|
|
17827
19774
|
}
|
|
17828
19775
|
|
|
17829
19776
|
// Server signals that a previously-idle session has resumed generating output.
|
|
@@ -17832,6 +19779,7 @@ function onSessionActivity(msg) {
|
|
|
17832
19779
|
function onSessionResumed(msg) {
|
|
17833
19780
|
const s = state.sessions.get(msg.id);
|
|
17834
19781
|
if (s) clearWaitingState(msg.id, { markInput: false, markWorking: true, timestamp: msg.timestamp || Date.now() });
|
|
19782
|
+
if (typeof scheduleStandupRefresh === 'function') scheduleStandupRefresh();
|
|
17835
19783
|
}
|
|
17836
19784
|
|
|
17837
19785
|
// Authoritative session state (hooks or OTEL). Wins over the regex fallback.
|
|
@@ -17842,6 +19790,11 @@ function onAuthoritativeStatus(msg) {
|
|
|
17842
19790
|
s._authoritativeSource = msg.source || 'hook';
|
|
17843
19791
|
s._working = !!msg.working;
|
|
17844
19792
|
s._authoritativeStatusAt = msg.timestamp || Date.now();
|
|
19793
|
+
s._serverLiveStatus = s._working ? 'running' : 'idle';
|
|
19794
|
+
s._serverLiveStatusAt = s._authoritativeStatusAt;
|
|
19795
|
+
if (s.meta) s.meta.liveStatus = s._serverLiveStatus;
|
|
19796
|
+
if (s._working) _markClientCodexRunningEvidence(s, s._authoritativeStatusAt, Date.now());
|
|
19797
|
+
else s._codexRunningHoldUntil = 0;
|
|
17845
19798
|
// When the agent is working, explicitly clear "waiting for input" state —
|
|
17846
19799
|
// the regex fallback may have left it set before hooks took over.
|
|
17847
19800
|
if (s._working && s._waitingForInput) clearWaitingState(msg.id, { markInput: false, markWorking: true, timestamp: s._authoritativeStatusAt });
|
|
@@ -17852,6 +19805,7 @@ function onAuthoritativeStatus(msg) {
|
|
|
17852
19805
|
item.classList.toggle('idle', !msg.working);
|
|
17853
19806
|
item.classList.remove('stale'); // authoritative signal supersedes staleness
|
|
17854
19807
|
}
|
|
19808
|
+
if (typeof scheduleStandupRefresh === 'function') scheduleStandupRefresh();
|
|
17855
19809
|
}
|
|
17856
19810
|
|
|
17857
19811
|
// Agent's internal session ID captured via OTEL — lets us surface real resume IDs in UI.
|
|
@@ -17891,9 +19845,7 @@ function onAgentLinked(msg) {
|
|
|
17891
19845
|
if (msg.model_provider) s.meta.model_provider = msg.model_provider;
|
|
17892
19846
|
populateModelSwitcher(ctmId);
|
|
17893
19847
|
}
|
|
17894
|
-
|
|
17895
|
-
if (key === ctmId || key.startsWith(ctmId + ':') || key.startsWith(agentId)) delete _promptScanCache[key];
|
|
17896
|
-
}
|
|
19848
|
+
invalidatePromptScanCacheForSession(ctmId);
|
|
17897
19849
|
scanPromptLines(ctmId);
|
|
17898
19850
|
}
|
|
17899
19851
|
for (const recent of allRecentSessions || []) {
|
|
@@ -17935,17 +19887,16 @@ function onWaitingForInput(msg) {
|
|
|
17935
19887
|
if (session) {
|
|
17936
19888
|
session._waitingForInput = true;
|
|
17937
19889
|
session._waitingForInputAt = msg.timestamp || Date.now();
|
|
19890
|
+
session._waitingReason = msg.reason || 'input';
|
|
19891
|
+
session._serverLiveStatus = 'waiting';
|
|
19892
|
+
session._serverLiveStatusAt = session._waitingForInputAt;
|
|
19893
|
+
session._codexRunningHoldUntil = 0;
|
|
19894
|
+
if (session.meta) session.meta.liveStatus = 'waiting';
|
|
17938
19895
|
}
|
|
19896
|
+
if (typeof scheduleStandupRefresh === 'function') scheduleStandupRefresh();
|
|
17939
19897
|
// Re-scan prompts — JSONL is fully written when Claude yields back to user.
|
|
17940
19898
|
// Invalidate cache so we get fresh data (not stale 30s cached results).
|
|
17941
|
-
|
|
17942
|
-
if (key === sessionId || key.startsWith(sessionId + ':') ||
|
|
17943
|
-
(session?.meta?.claudeSessionId && key.startsWith(session.meta.claudeSessionId)) ||
|
|
17944
|
-
(session?.meta?.agentSessionId && key.startsWith(session.meta.agentSessionId)) ||
|
|
17945
|
-
(session?.meta?.agentSessionToken && key.startsWith(session.meta.agentSessionToken))) {
|
|
17946
|
-
delete _promptScanCache[key];
|
|
17947
|
-
}
|
|
17948
|
-
}
|
|
19899
|
+
invalidatePromptScanCacheForSession(sessionId);
|
|
17949
19900
|
scanPromptLines(sessionId);
|
|
17950
19901
|
const label = msg.label || session?.meta?.label || sessionId.slice(0, 8);
|
|
17951
19902
|
const reason = msg.reason || 'input';
|
|
@@ -18134,7 +20085,9 @@ window.addEventListener('message', (e) => {
|
|
|
18134
20085
|
});
|
|
18135
20086
|
|
|
18136
20087
|
// --- Hash routing ---
|
|
18137
|
-
|
|
20088
|
+
// Keep "command" as a route alias for old links; the UI now renders this as
|
|
20089
|
+
// the pinned Overview tab inside Sessions.
|
|
20090
|
+
const NAV_TARGETS = ['sessions', 'command', 'prompts', 'rules', 'insights', 'permissions', 'codereview', 'walle', 'models', 'backups', 'worktrees', 'setup'];
|
|
18138
20091
|
|
|
18139
20092
|
function _parseHashRoute() {
|
|
18140
20093
|
const hash = location.hash.slice(1);
|
|
@@ -18163,10 +20116,15 @@ function handleHashRoute() {
|
|
|
18163
20116
|
|
|
18164
20117
|
// No hash — fall back to saved nav pref from DB
|
|
18165
20118
|
if (!hash) {
|
|
18166
|
-
if (state._savedActiveNav
|
|
18167
|
-
navTo(
|
|
20119
|
+
if (state._savedActiveNav === 'command') {
|
|
20120
|
+
navTo('command', { skipHash: false, skipPersist: true });
|
|
20121
|
+
return;
|
|
20122
|
+
}
|
|
20123
|
+
const savedNav = state._savedActiveNav === 'command' ? 'sessions' : state._savedActiveNav;
|
|
20124
|
+
if (savedNav && NAV_TARGETS.includes(savedNav) && savedNav !== 'sessions') {
|
|
20125
|
+
navTo(savedNav, { skipHash: false, skipPersist: true });
|
|
18168
20126
|
// Restore deep state (e.g., open prompt) after nav
|
|
18169
|
-
if (
|
|
20127
|
+
if (savedNav === 'prompts' && state._savedActivePrompt) {
|
|
18170
20128
|
setTimeout(() => PE.openPrompt(state._savedActivePrompt), 200);
|
|
18171
20129
|
}
|
|
18172
20130
|
} else if (state._savedActiveSession) {
|
|
@@ -18182,7 +20140,7 @@ function handleHashRoute() {
|
|
|
18182
20140
|
const isNav = route.isNav;
|
|
18183
20141
|
const params = route.params;
|
|
18184
20142
|
|
|
18185
|
-
// Check for nav target: #permissions, #prompts, #rules, #insights, #sessions, #codereview
|
|
20143
|
+
// Check for nav target: #command alias, #permissions, #prompts, #rules, #insights, #sessions, #codereview
|
|
18186
20144
|
if (isNav && !Object.keys(params).length) {
|
|
18187
20145
|
navTo(firstPart, { skipHash: true });
|
|
18188
20146
|
// For prompts without explicit prompt param, restore saved prompt from DB
|
|
@@ -18319,6 +20277,7 @@ state._prefsLoaded = loadPrefs().then(() => {
|
|
|
18319
20277
|
loadRecentSessions();
|
|
18320
20278
|
// Restore hash from saved nav if no hash present
|
|
18321
20279
|
handleHashRoute();
|
|
20280
|
+
refreshStandupIfVisible();
|
|
18322
20281
|
});
|
|
18323
20282
|
refreshSessionPrompts();
|
|
18324
20283
|
|