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.
Files changed (98) hide show
  1. package/README.md +8 -3
  2. package/bin/create-walle.js +232 -32
  3. package/bin/mcp-inject.js +18 -53
  4. package/package.json +3 -1
  5. package/template/claude-task-manager/api-prompts.js +11 -2
  6. package/template/claude-task-manager/approval-agent.js +7 -0
  7. package/template/claude-task-manager/db.js +94 -75
  8. package/template/claude-task-manager/docs/session-standup-command-center-design.md +242 -0
  9. package/template/claude-task-manager/docs/session-tooltip-freshness-design.md +224 -0
  10. package/template/claude-task-manager/docs/session-ux-issue-review-2026-05-01.md +369 -0
  11. package/template/claude-task-manager/fuzzy-utils.js +10 -2
  12. package/template/claude-task-manager/git-utils.js +140 -10
  13. package/template/claude-task-manager/lib/agent-capabilities.js +1 -1
  14. package/template/claude-task-manager/lib/agent-presets.js +38 -5
  15. package/template/claude-task-manager/lib/codex-terminal-final.js +53 -0
  16. package/template/claude-task-manager/lib/ctm-session-context-api.js +222 -0
  17. package/template/claude-task-manager/lib/session-diagnostics.js +56 -0
  18. package/template/claude-task-manager/lib/session-history.js +309 -16
  19. package/template/claude-task-manager/lib/session-standup.js +409 -0
  20. package/template/claude-task-manager/lib/session-stream.js +253 -20
  21. package/template/claude-task-manager/lib/standup-attention.js +200 -0
  22. package/template/claude-task-manager/lib/status-hooks.js +8 -2
  23. package/template/claude-task-manager/lib/update-telemetry.js +114 -0
  24. package/template/claude-task-manager/lib/walle-ctm-history.js +49 -6
  25. package/template/claude-task-manager/lib/walle-default-model.js +55 -0
  26. package/template/claude-task-manager/lib/walle-mcp-auto-config.js +66 -0
  27. package/template/claude-task-manager/lib/walle-supervisor.js +86 -19
  28. package/template/claude-task-manager/lib/walle-transcript.js +1 -3
  29. package/template/claude-task-manager/lib/worktree-cwd.js +82 -0
  30. package/template/claude-task-manager/package.json +1 -0
  31. package/template/claude-task-manager/providers/codex-mcp.js +104 -0
  32. package/template/claude-task-manager/providers/index.js +2 -0
  33. package/template/claude-task-manager/public/css/setup.css +2 -1
  34. package/template/claude-task-manager/public/css/walle.css +71 -0
  35. package/template/claude-task-manager/public/index.html +2388 -429
  36. package/template/claude-task-manager/public/js/message-renderer.js +314 -35
  37. package/template/claude-task-manager/public/js/session-search-utils.js +185 -3
  38. package/template/claude-task-manager/public/js/session-status-precedence.js +125 -0
  39. package/template/claude-task-manager/public/js/setup.js +62 -19
  40. package/template/claude-task-manager/public/js/stream-view.js +396 -55
  41. package/template/claude-task-manager/public/js/terminal-restore-state.js +57 -0
  42. package/template/claude-task-manager/public/js/walle-session.js +234 -26
  43. package/template/claude-task-manager/public/js/walle.js +143 -2
  44. package/template/claude-task-manager/server.js +1402 -433
  45. package/template/claude-task-manager/session-integrity.js +77 -28
  46. package/template/claude-task-manager/workers/approval-widget-validator.js +15 -5
  47. package/template/claude-task-manager/workers/scrollback-worker.js +5 -6
  48. package/template/claude-task-manager/workers/state-detectors/codex.js +6 -0
  49. package/template/package.json +1 -1
  50. package/template/wall-e/agent-runners/claude-code.js +2 -0
  51. package/template/wall-e/agent.js +63 -8
  52. package/template/wall-e/api-walle.js +330 -52
  53. package/template/wall-e/brain.js +291 -42
  54. package/template/wall-e/chat.js +172 -15
  55. package/template/wall-e/coding/compaction-service.js +19 -5
  56. package/template/wall-e/coding/stream-processor.js +22 -2
  57. package/template/wall-e/coding/workspace-replay.js +1 -4
  58. package/template/wall-e/coding-orchestrator.js +250 -80
  59. package/template/wall-e/compat.js +0 -28
  60. package/template/wall-e/context/context-builder.js +3 -1
  61. package/template/wall-e/embeddings.js +2 -7
  62. package/template/wall-e/eval/agent-runner.js +30 -9
  63. package/template/wall-e/eval/benchmark-generator.js +21 -1
  64. package/template/wall-e/eval/benchmarks/chat-eval.json +66 -6
  65. package/template/wall-e/eval/benchmarks/coding-agent.json +0 -596
  66. package/template/wall-e/eval/cc-replay.js +1 -0
  67. package/template/wall-e/eval/codex-cli-baseline.js +633 -0
  68. package/template/wall-e/eval/debug-agent003.js +1 -0
  69. package/template/wall-e/eval/eval-orchestrator.js +3 -3
  70. package/template/wall-e/eval/run-agent-benchmarks.js +11 -3
  71. package/template/wall-e/eval/run-codex-cli-baseline.js +177 -0
  72. package/template/wall-e/eval/run-model-comparison.js +1 -0
  73. package/template/wall-e/eval/swebench-adapter.js +1 -0
  74. package/template/wall-e/evaluation/quorum-evaluator.js +0 -1
  75. package/template/wall-e/extraction/knowledge-extractor.js +1 -2
  76. package/template/wall-e/lib/mcp-integration.js +336 -0
  77. package/template/wall-e/llm/ollama.js +47 -8
  78. package/template/wall-e/llm/ollama.plugin.json +1 -1
  79. package/template/wall-e/llm/tool-adapter.js +1 -0
  80. package/template/wall-e/loops/ingest.js +42 -8
  81. package/template/wall-e/loops/initiative.js +87 -2
  82. package/template/wall-e/mcp-server.js +872 -19
  83. package/template/wall-e/memory/ctm-context-client.js +230 -0
  84. package/template/wall-e/memory/ctm-session-context.js +1376 -0
  85. package/template/wall-e/prompts/coding/memory-protocol.md +6 -0
  86. package/template/wall-e/server.js +30 -1
  87. package/template/wall-e/skills/_bundled/memory-search/SKILL.md +8 -0
  88. package/template/wall-e/skills/_bundled/scan-ctm-sessions/SKILL.md +20 -0
  89. package/template/wall-e/skills/_bundled/scan-ctm-sessions/run.js +43 -0
  90. package/template/wall-e/skills/_bundled/slack-mentions/run.js +471 -188
  91. package/template/wall-e/skills/skill-planner.js +86 -4
  92. package/template/wall-e/slack/socket-mode-listener.js +276 -0
  93. package/template/wall-e/telemetry.js +70 -2
  94. package/template/wall-e/tools/builtin-middleware.js +55 -2
  95. package/template/wall-e/tools/shell-policy.js +1 -1
  96. package/template/wall-e/tools/slack-owner.js +104 -0
  97. package/template/website/index.html +4 -4
  98. 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
- /* Truncate mode */
746
- #review-messages.truncate-mode .review-msg.user .msg-header,
747
- #review-messages.truncate-mode .review-msg.user.key-msg .msg-header {
748
- position: relative;
749
- cursor: pointer;
750
- }
751
- #review-messages.truncate-mode .review-msg.user .msg-header::after {
752
- content: '✂ Cut from here';
753
- position: absolute;
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
- #review-messages.truncate-mode .review-msg.user .msg-header:hover::after {
768
- opacity: 1;
769
- }
770
- #review-messages.truncate-mode .review-msg.user .msg-header:hover {
771
- background: rgba(122,162,247,0.08);
772
- border-radius: 4px;
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
- #review-messages::-webkit-scrollbar-track { background: transparent; }
953
- #review-messages::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
954
- #review-messages::-webkit-scrollbar-thumb:hover { background: var(--fg-dim); }
955
-
956
- /* Scroll to bottom button */
957
- .scroll-bottom-btn {
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: center;
2026
- justify-content: center;
2238
+ align-items: stretch;
2239
+ justify-content: flex-start;
2027
2240
  flex: 1;
2028
- gap: 16px;
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;">&#x2191;</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;">&times;</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;">&times;</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">&#x2191;</div>
2971
3469
  <div class="update-wizard-title">
2972
- <h3 id="update-wizard-heading">Upgrade CTM?</h3>
2973
- <p>A newer CTM release is available.</p>
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="setModelFilter(this.value)" title="Filter by model" style="background:var(--bg);color:var(--fg-dim);border:1px solid var(--border);padding:2px 4px;border-radius:3px;font-size:10px;max-width:130px;cursor:pointer;">
3032
- <option value="">All Models</option>
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
- <h2 style="font-size:24px;margin-bottom:4px;">Welcome to CTM</h2>
3072
- <p style="color:var(--fg-dim);margin-bottom:20px;">Manage Claude Code sessions, prompts, and your AI assistant Wall-E.</p>
3073
- <button class="btn primary" onclick="showNewSessionModal()" style="font-size:15px;padding:8px 24px;margin-bottom:28px;">New Session</button>
3074
- <div style="display:flex;gap:16px;max-width:680px;">
3075
- <div onclick="navTo('sessions')" style="flex:1;padding:14px;background:rgba(255,255,255,0.03);border:1px solid var(--border);border-radius:8px;cursor:pointer;">
3076
- <div style="font-weight:600;margin-bottom:4px;color:var(--fg);">Sessions</div>
3077
- <div style="font-size:12px;color:var(--fg-dim);">Run and manage Claude Code terminal sessions</div>
3078
- </div>
3079
- <div onclick="navTo('prompts')" style="flex:1;padding:14px;background:rgba(255,255,255,0.03);border:1px solid var(--border);border-radius:8px;cursor:pointer;">
3080
- <div style="font-weight:600;margin-bottom:4px;color:var(--fg);">Prompts</div>
3081
- <div style="font-size:12px;color:var(--fg-dim);">Save, organize, and send prompts to Claude</div>
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 onclick="navTo('walle')" style="flex:1;padding:14px;background:rgba(255,255,255,0.03);border:1px solid var(--border);border-radius:8px;cursor:pointer;">
3084
- <div style="font-weight:600;margin-bottom:4px;color:var(--fg);display:flex;align-items:center;gap:6px;"><img src="/walle-icon.svg" width="18" height="18"> WALL-E</div>
3085
- <div style="font-size:12px;color:var(--fg-dim);">Your personal AI assistant — chat, tasks, and insights</div>
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
- <h2>Wall-E Setup</h2>
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">Fix All</button>
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 your tool's MCP config:
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
- if (_updateLatestVersion) {
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
- dismissUpdate();
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: saved pref > pending hash > first session in tab order.
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
- const first = state.tabOrder.find(t => state.sessions.has(t));
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
- walle: { structuredTranscript: true, promptNavigation: 'none', review: true, resume: false },
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
- if (agentType === 'codex') return { type: 'character', char: '›', offset: 2 };
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 clear waiting state if actually waiting (avoid DOM work on every keystroke)
5757
- if (_s && _s._waitingForInput) clearWaitingState(id);
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
- state.sessions.set(id, { _id: id, term, fitAddon, searchAddon, container, needsFontRefresh: true, writer, promptLines: [], promptNavIdx: -1, _webglAddon, _loadWebgl });
5877
- // Attach local echo overlay for known agent types (claude today; codex/gemini pending prompt verification).
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 scanPromptLines(id) {
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 || s.promptPreviews.length !== cache.previews.length) {
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 res = await fetch(`/api/session/messages?id=${apiId}&project=${encodeURIComponent(projectEntry || '')}&token=${state.token}`);
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 || s.promptLines.length === 0) return;
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 promptNavToggleList(id) {
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
- if (!canRestoreExitedText && !hasCachedSnapshot && sReal.term) {
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
- if (canRestoreExitedText && sReal._savedScrollbackText && sReal.term) {
6585
- const text = sReal._savedScrollbackText + '\r\n';
6586
- // Keep the reset inside xterm's write queue. A synchronous reset can
6587
- // run before older queued writes have parsed, leaving stale bytes
6588
- // ahead of the restored text.
6589
- sReal.term.write(TERMINAL_FULL_RESET + text, () => _ensureScrolledToBottom(sReal));
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 (_snapshotDimsMatchTerm(sReal, sReal._cachedSnapshotCols, sReal._cachedSnapshotRows)) {
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
- sReal.writer.queue = '';
6634
- if (sReal.needsAttach) {
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
- if (target !== currentNav) _prevNav = currentNav;
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 (target === 'sessions') {
7914
+ if (effectiveTarget === 'sessions') {
6814
7915
  history.replaceState(null, '', location.pathname + location.search);
6815
7916
  } else {
6816
- history.replaceState(null, '', location.pathname + location.search + '#' + target);
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', target);
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
- // Switch to last active session (prefer saved, then first available), or show welcome
6835
- const savedSession = state._savedActiveSession && state.sessions.has(state._savedActiveSession) ? state._savedActiveSession : null;
6836
- const lastSession = savedSession || state.tabOrder.find(t => state.sessions.has(t));
6837
- if (lastSession) {
6838
- activateTab(lastSession);
6839
- } else {
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 openSyncModal(wt);
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
- branchEl.textContent = wt.branch || '(detached)';
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
- pill.style.cursor = 'pointer';
9697
- pill.title = 'Click to sync from main';
9698
- pill.onclick = function() { openSyncModal(wt); };
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/worktrees/ — consider git worktree move';
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
- syncBtn.textContent = '↓ Sync';
9772
- syncBtn.title = 'Pull main into this worktree';
9773
- syncBtn.onclick = function() { openSyncModal(wt); };
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
- summaryEl.textContent = wt.summary || '';
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 r = await fetch('/api/worktrees/prune?token=' + encodeURIComponent(token), { method: 'POST' });
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
- if (preview) preview.textContent = '.claude/worktrees/' + (sanitized || '<name>');
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.includes('.claude/worktrees/')) {
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 = { meta: { label: label, cwd: cwd, type: 'walle' }, walleState: null };
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
- if (_isClientActiveOutput(s, data) && !(s._waitingForInput && _isClientCodexStatusOnlyOutput(s, data))) {
10697
- s._lastOutputAt = Date.now();
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 atBottom = _isAtTerminalFollowBottom(s);
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
- return Math.max(0, Math.min(baseY, anchor - rows + 1));
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 hasCompletedTurnOutputAfterPrompt = false;
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('›')) promptAbs = abs;
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
- hasCompletedTurnOutputAfterPrompt = true;
12142
+ hasNonStatusAfterPrompt = true;
10915
12143
  break;
10916
12144
  }
10917
- if (!hasCompletedTurnOutputAfterPrompt) return null;
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 (`_userScrollLocked`) — we don't want
10974
- // to yank them back to the bottom if they were reading scrollback.
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
- if (s.writer && s.writer._userScrollLocked) return;
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
- _restoreSnapshotData(s, msg.data, snapshotDone);
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 label = s.meta?.label || id.slice(0, 8);
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 branchName = s.meta?.branch || '';
11567
- const branchBadge = branchName && branchName !== 'main' ? `<span class="branch-badge" title="Branch: ${escHtml(branchName)}">&#9741; ${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: 'waiting', text: 'Waiting' };
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
- // Authoritative (hook or OTEL) is the strongest signal CliDeck-style.
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 (s._streamStatus && s._streamStatusAt && (now - s._streamStatusAt) < 60000) {
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
- if (e.key === 'Enter') { e.preventDefault(); input.blur(); }
11740
- if (e.key === 'Escape') { input.value = currentText; input.blur(); }
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 currentText = labelEl.textContent.trim();
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.meta?.label || id.slice(0, 8);
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 + (tabBranchText ? '' : '');
12122
- if (tabBranch && tabBranch !== 'main') {
12123
- const branchEl = document.createElement('span');
12124
- branchEl.className = 'branch-badge';
12125
- branchEl.style.cssText = 'font-size:9px;margin-left:3px';
12126
- branchEl.textContent = '\u25ED ' + (tabBranch.length > 12 ? tabBranch.slice(0, 12) + '..' : tabBranch);
12127
- tabLabel.appendChild(branchEl);
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 && p.includes('/.claude/worktrees/');
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.indexOf('/.claude/worktrees/') !== -1 || !_nsAgentUsesCodeWorkspace()) {
12338
- _nsApplyWorktreeRecommendation(false, cwd && cwd.indexOf('/.claude/worktrees/') !== -1
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 r = await fetch('/api/worktrees?token=' + encodeURIComponent(token));
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({ name: wtName, cwd: cwd }),
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
- state.activeTab = null;
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
- state.activeTab = null;
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.includes('.claude/worktrees/'));
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.searchAddon) return;
12905
- let bar = s.container.querySelector('.term-search-bar');
12906
- if (!bar) {
12907
- bar = _createSearchBarEl();
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 || !s.searchAddon) continue;
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.searchAddon.clearDecorations();
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 model filter
13798
- if (prefs.model_filter) {
13799
- currentModelFilter = prefs.model_filter;
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
- populateProjectFilter(allRecentSessions);
13969
- populateModelFilter(allRecentSessions);
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
- model: currentModelFilter,
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(allRecentSessions);
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.toLowerCase();
15971
+ const q = SessionSearchUtils.normalizeSearchValue(document.getElementById('recent-search').value);
14136
15972
  const sidebarFilters = _recentSidebarFilterState();
14137
- let sessions = getFilteredSessions();
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(sessions.map(s => s.sessionId));
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 ? `<span>${escHtml(s.gitBranch)}</span>` : '';
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 = '<div style="padding:10px;font-size:12px;color:var(--fg-dim)">No sessions found</div>';
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
- // Chunked rendering: show first batch immediately, render rest progressively
14772
- const groups = groupMessages(messages);
14773
- const FIRST_BATCH = 20;
14774
- const CHUNK_SIZE = 30;
14775
- const container = document.getElementById('review-messages');
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
- // renderGroup returns sanitized HTML (uses DOMPurify internally)
14778
- const firstHtml = groups.slice(0, FIRST_BATCH).map(renderGroup).join('');
14779
- container.innerHTML = DOMPurify.sanitize(firstHtml, { ADD_ATTR: ['style', 'data-idx', 'data-role'] });
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
- if (groups.length > FIRST_BATCH) {
14792
- let offset = FIRST_BATCH;
14793
- const renderNextChunk = () => {
14794
- if (offset >= groups.length) {
14795
- toggleToolMsgs();
14796
- reviewPromptNavReset();
14797
- return;
14798
- }
14799
- const end = Math.min(offset + CHUNK_SIZE, groups.length);
14800
- const chunkHtml = groups.slice(offset, end).map(renderGroup).join('');
14801
- container.insertAdjacentHTML('beforeend', DOMPurify.sanitize(chunkHtml, { ADD_ATTR: ['style', 'data-idx', 'data-role'] }));
14802
- offset = end;
14803
- requestAnimationFrame(renderNextChunk);
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
- const textEl = btn.previousElementSibling;
14869
- const isCollapsed = textEl.classList.contains('collapsed');
14870
- textEl.classList.toggle('collapsed');
14871
- btn.textContent = isCollapsed ? 'Show less' : 'Show more';
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
- // Expand collapsed message if match is inside one
14967
- const msgText = match.closest('.msg-text');
14968
- if (msgText && msgText.classList.contains('collapsed')) {
14969
- msgText.classList.remove('collapsed');
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
- // Unhide tool-only messages if match is inside one
14975
- const msgEl = match.closest('.review-msg');
14976
- if (msgEl && msgEl.style.display === 'none') msgEl.style.display = '';
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
- match.scrollIntoView({ behavior: 'smooth', block: 'center' });
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
- // Add click handlers to user message headers
15038
- msgEl.querySelectorAll('.review-msg.user .msg-header').forEach(header => {
15039
- header._truncateClick = () => selectTruncatePoint(header);
15040
- header.addEventListener('click', header._truncateClick);
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
- const msgEl = headerEl.closest('.review-msg');
15046
- const container = document.getElementById('review-messages');
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
- // Use direct children of the container to avoid nested-element double-counting
15060
- const directChildren = Array.from(container.children).filter(el =>
15061
- el.classList.contains('review-msg') || el.classList.contains('thought-group'));
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
- msgEl.querySelectorAll('.review-msg.user .msg-header').forEach(header => {
15117
- if (header._truncateClick) {
15118
- header.removeEventListener('click', header._truncateClick);
15119
- delete header._truncateClick;
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
- const container = document.getElementById('review-messages');
15167
- _reviewPromptEls = container ? Array.from(container.querySelectorAll('.review-msg.user')) : [];
15168
- _reviewPromptIdx = -1;
15169
- reviewPromptNavUpdateBadge();
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
- const list = document.createElement('div');
15228
- list.className = 'prompt-nav-list';
15229
- // Show most recent first
15230
- for (let i = _reviewPromptEls.length - 1; i >= 0; i--) {
15231
- const msgText = _reviewPromptEls[i].querySelector('.msg-text');
15232
- let text = msgText ? msgText.textContent.trim() : '(empty)';
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 currentModelFilter = '';
15292
- function setModelFilter(model) {
15293
- currentModelFilter = model;
15294
- savePref('model_filter', model);
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
- function isSyntheticModelName(model) {
15299
- return typeof model === 'string' && /^<[^>]+>$/.test(model.trim());
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 populateModelFilter(sessions) {
17213
+ function populateAgentFilter(sessions) {
15316
17214
  const sel = document.getElementById('model-filter');
15317
17215
  if (!sel) return;
15318
- const models = new Map();
17216
+ const agents = new Map();
15319
17217
  for (const s of sessions) {
15320
- if (s.model && !isSyntheticModelName(s.model)) {
15321
- models.set(s.model, (models.get(s.model) || 0) + 1);
15322
- }
17218
+ const agent = SessionSearchUtils.getRecentSessionAgentType(s);
17219
+ agents.set(agent, (agents.get(agent) || 0) + 1);
15323
17220
  }
15324
- const sorted = [...models.entries()].sort((a, b) => {
15325
- const pa = modelFilterPriority(a[0]);
15326
- const pb = modelFilterPriority(b[0]);
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 String(b[0]).localeCompare(String(a[0]), undefined, { numeric: true, sensitivity: 'base' });
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 Models (' + sessions.length + ')';
17237
+ allOpt.textContent = 'All Agents (' + sessions.length + ')';
15337
17238
  sel.add(allOpt);
15338
- for (const [m, count] of sorted) {
17239
+ for (const [agent, count] of sorted) {
15339
17240
  const opt = document.createElement('option');
15340
- opt.value = m;
15341
- opt.textContent = modelFilterLabel(m) + ' (' + count + ')';
17241
+ opt.value = agent;
17242
+ opt.textContent = SessionSearchUtils.recentAgentFilterLabel(agent) + ' (' + count + ')';
15342
17243
  sel.add(opt);
15343
17244
  }
15344
- const nextValue = prev || currentModelFilter || '';
15345
- sel.value = models.has(nextValue) ? nextValue : '';
15346
- if (currentModelFilter && !models.has(currentModelFilter)) currentModelFilter = '';
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 || '').trim().toLowerCase();
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/model
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/model).
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 = query.toLowerCase();
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
- if (!serverIds.has(ls.sessionId)) {
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 = query.toLowerCase().split(/\s+/).filter(t => t.length >= 2);
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 ? `<span>${escHtml(s.gitBranch)}</span>` : '';
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 || '<div style="padding:10px;font-size:12px;color:var(--fg-dim)">No results found</div>';
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) s._serverWorkingAt = now;
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
- if (serverState === 'thinking' || serverState === 'active') {
17810
- const waitingAt = s._waitingForInputAt || 0;
17811
- const activityIsNewerThanWaiting = !s._waitingForInput || !waitingAt || serverTs >= waitingAt;
17812
- if (activityIsNewerThanWaiting) s._serverWorkingAt = Date.now();
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
- for (const key of Object.keys(_promptScanCache)) {
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
- for (const key of Object.keys(_promptScanCache)) {
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
- const NAV_TARGETS = ['sessions', 'prompts', 'rules', 'insights', 'permissions', 'codereview', 'walle', 'models', 'backups', 'worktrees', 'setup'];
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 && NAV_TARGETS.includes(state._savedActiveNav) && state._savedActiveNav !== 'sessions') {
18167
- navTo(state._savedActiveNav, { skipHash: false, skipPersist: true });
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 (state._savedActiveNav === 'prompts' && state._savedActivePrompt) {
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