create-walle 0.9.13 → 0.9.14

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 (58) hide show
  1. package/README.md +6 -1
  2. package/bin/create-walle.js +195 -30
  3. package/bin/mcp-inject.js +18 -53
  4. package/package.json +3 -1
  5. package/template/claude-task-manager/approval-agent.js +7 -0
  6. package/template/claude-task-manager/docs/session-standup-command-center-design.md +242 -0
  7. package/template/claude-task-manager/git-utils.js +111 -3
  8. package/template/claude-task-manager/lib/session-history.js +144 -16
  9. package/template/claude-task-manager/lib/session-standup.js +409 -0
  10. package/template/claude-task-manager/lib/standup-attention.js +200 -0
  11. package/template/claude-task-manager/lib/status-hooks.js +8 -2
  12. package/template/claude-task-manager/lib/update-telemetry.js +114 -0
  13. package/template/claude-task-manager/lib/walle-default-model.js +55 -0
  14. package/template/claude-task-manager/lib/walle-mcp-auto-config.js +62 -0
  15. package/template/claude-task-manager/lib/walle-supervisor.js +83 -19
  16. package/template/claude-task-manager/lib/worktree-cwd.js +82 -0
  17. package/template/claude-task-manager/providers/codex-mcp.js +104 -0
  18. package/template/claude-task-manager/providers/index.js +2 -0
  19. package/template/claude-task-manager/public/css/setup.css +2 -1
  20. package/template/claude-task-manager/public/css/walle.css +5 -0
  21. package/template/claude-task-manager/public/index.html +1596 -283
  22. package/template/claude-task-manager/public/js/session-search-utils.js +171 -1
  23. package/template/claude-task-manager/public/js/setup.js +62 -19
  24. package/template/claude-task-manager/public/js/stream-view.js +55 -6
  25. package/template/claude-task-manager/public/js/walle-session.js +73 -16
  26. package/template/claude-task-manager/public/js/walle.js +34 -2
  27. package/template/claude-task-manager/server.js +780 -177
  28. package/template/claude-task-manager/session-integrity.js +58 -15
  29. package/template/claude-task-manager/workers/approval-widget-validator.js +15 -5
  30. package/template/claude-task-manager/workers/state-detectors/codex.js +6 -0
  31. package/template/package.json +1 -1
  32. package/template/wall-e/agent.js +36 -7
  33. package/template/wall-e/api-walle.js +72 -20
  34. package/template/wall-e/coding/stream-processor.js +22 -2
  35. package/template/wall-e/coding-orchestrator.js +26 -6
  36. package/template/wall-e/eval/agent-runner.js +16 -4
  37. package/template/wall-e/eval/benchmark-generator.js +21 -1
  38. package/template/wall-e/eval/benchmarks/coding-agent.json +0 -596
  39. package/template/wall-e/eval/codex-cli-baseline.js +633 -0
  40. package/template/wall-e/eval/eval-orchestrator.js +3 -3
  41. package/template/wall-e/eval/run-agent-benchmarks.js +11 -3
  42. package/template/wall-e/eval/run-codex-cli-baseline.js +177 -0
  43. package/template/wall-e/lib/mcp-integration.js +220 -0
  44. package/template/wall-e/llm/ollama.js +47 -8
  45. package/template/wall-e/llm/ollama.plugin.json +1 -1
  46. package/template/wall-e/llm/tool-adapter.js +1 -0
  47. package/template/wall-e/loops/ingest.js +42 -8
  48. package/template/wall-e/mcp-server.js +272 -10
  49. package/template/wall-e/memory/ctm-session-context.js +910 -0
  50. package/template/wall-e/server.js +26 -1
  51. package/template/wall-e/skills/_bundled/scan-ctm-sessions/SKILL.md +20 -0
  52. package/template/wall-e/skills/_bundled/scan-ctm-sessions/run.js +43 -0
  53. package/template/wall-e/skills/skill-planner.js +52 -3
  54. package/template/wall-e/tools/builtin-middleware.js +55 -2
  55. package/template/wall-e/tools/shell-policy.js +1 -1
  56. package/template/wall-e/tools/slack-owner.js +104 -0
  57. package/template/website/index.html +2 -2
  58. 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 {
@@ -1589,10 +1623,28 @@
1589
1623
  transition: all 0.1s;
1590
1624
  }
1591
1625
  .tab .tab-label {
1626
+ flex: 1 1 auto;
1592
1627
  overflow: hidden;
1593
1628
  text-overflow: ellipsis;
1594
1629
  min-width: 0;
1595
1630
  }
1631
+ .tab > .branch-badge { max-width: 64px; }
1632
+ .tab.tab-title-clipped > .branch-badge { display: none; }
1633
+ .tab.pinned-tab {
1634
+ flex: 0 0 auto;
1635
+ min-width: 86px;
1636
+ }
1637
+ .tab.pinned-tab::before {
1638
+ content: '';
1639
+ display: inline-block;
1640
+ width: 6px;
1641
+ height: 6px;
1642
+ border-radius: 999px;
1643
+ background: var(--accent);
1644
+ opacity: 0.75;
1645
+ flex: 0 0 auto;
1646
+ }
1647
+ .tab.pinned-tab .tab-label { flex: 0 0 auto; }
1596
1648
  .tab:hover { color: var(--fg); background: var(--bg-light); }
1597
1649
  .tab.active {
1598
1650
  color: var(--fg);
@@ -2022,14 +2074,293 @@
2022
2074
  #welcome {
2023
2075
  display: flex;
2024
2076
  flex-direction: column;
2025
- align-items: center;
2026
- justify-content: center;
2077
+ align-items: stretch;
2078
+ justify-content: flex-start;
2027
2079
  flex: 1;
2028
- gap: 16px;
2080
+ gap: 12px;
2081
+ overflow: auto;
2082
+ padding: 18px;
2029
2083
  color: var(--fg-dim);
2030
2084
  }
2031
2085
  #welcome h2 { color: var(--fg); font-size: 18px; font-weight: 600; }
2032
2086
  #welcome p { font-size: 13px; max-width: 400px; text-align: center; line-height: 1.6; }
2087
+ .standup-dashboard {
2088
+ width: 100%;
2089
+ max-width: 1280px;
2090
+ margin: 0 auto;
2091
+ display: flex;
2092
+ flex-direction: column;
2093
+ gap: 12px;
2094
+ min-height: 0;
2095
+ }
2096
+ .standup-header {
2097
+ display: flex;
2098
+ align-items: flex-start;
2099
+ justify-content: space-between;
2100
+ gap: 14px;
2101
+ border: 1px solid var(--border);
2102
+ background: rgba(255,255,255,0.025);
2103
+ border-radius: 8px;
2104
+ padding: 14px;
2105
+ }
2106
+ .standup-heading { min-width: 0; }
2107
+ .standup-eyebrow {
2108
+ color: var(--accent);
2109
+ font-size: 11px;
2110
+ font-weight: 700;
2111
+ letter-spacing: 0;
2112
+ text-transform: uppercase;
2113
+ margin-bottom: 4px;
2114
+ }
2115
+ .standup-heading h2 {
2116
+ margin: 0;
2117
+ font-size: 20px;
2118
+ line-height: 1.2;
2119
+ }
2120
+ .standup-meta {
2121
+ display: flex;
2122
+ flex-wrap: wrap;
2123
+ align-items: center;
2124
+ gap: 8px;
2125
+ justify-content: flex-end;
2126
+ }
2127
+ .standup-counts {
2128
+ display: flex;
2129
+ flex-wrap: wrap;
2130
+ gap: 6px;
2131
+ }
2132
+ .standup-count {
2133
+ border: 1px solid var(--border);
2134
+ border-radius: 6px;
2135
+ padding: 5px 7px;
2136
+ background: var(--bg-light);
2137
+ color: var(--fg);
2138
+ font-size: 11px;
2139
+ white-space: nowrap;
2140
+ }
2141
+ .standup-count strong {
2142
+ font-size: 13px;
2143
+ margin-right: 4px;
2144
+ }
2145
+ .standup-updated {
2146
+ font-size: 11px;
2147
+ color: var(--fg-dim);
2148
+ white-space: nowrap;
2149
+ }
2150
+ .standup-actions {
2151
+ display: flex;
2152
+ gap: 6px;
2153
+ flex-wrap: wrap;
2154
+ justify-content: flex-end;
2155
+ }
2156
+ .standup-action-btn {
2157
+ background: var(--bg-light);
2158
+ color: var(--fg);
2159
+ border: 1px solid var(--border);
2160
+ border-radius: 6px;
2161
+ padding: 6px 9px;
2162
+ font-size: 12px;
2163
+ cursor: pointer;
2164
+ min-height: 30px;
2165
+ }
2166
+ .standup-action-btn:hover {
2167
+ border-color: var(--accent);
2168
+ color: var(--accent);
2169
+ }
2170
+ .standup-action-btn.primary {
2171
+ background: var(--accent);
2172
+ border-color: var(--accent);
2173
+ color: var(--bg);
2174
+ font-weight: 700;
2175
+ }
2176
+ .standup-attention {
2177
+ display: none;
2178
+ border: 1px solid rgba(224, 175, 104, 0.35);
2179
+ background: rgba(224, 175, 104, 0.08);
2180
+ border-radius: 8px;
2181
+ padding: 10px 12px;
2182
+ color: var(--fg);
2183
+ gap: 10px;
2184
+ align-items: center;
2185
+ justify-content: space-between;
2186
+ }
2187
+ .standup-attention.active { display: flex; }
2188
+ .standup-attention-main {
2189
+ min-width: 0;
2190
+ display: flex;
2191
+ flex-direction: column;
2192
+ gap: 3px;
2193
+ }
2194
+ .standup-attention-title {
2195
+ font-size: 13px;
2196
+ font-weight: 700;
2197
+ color: var(--yellow);
2198
+ }
2199
+ .standup-attention-body {
2200
+ font-size: 12px;
2201
+ color: var(--fg-dim);
2202
+ overflow-wrap: anywhere;
2203
+ }
2204
+ .standup-lanes {
2205
+ display: grid;
2206
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
2207
+ gap: 10px;
2208
+ align-items: start;
2209
+ }
2210
+ .standup-lane {
2211
+ border: 1px solid var(--border);
2212
+ border-radius: 8px;
2213
+ background: rgba(255,255,255,0.018);
2214
+ min-width: 0;
2215
+ overflow: hidden;
2216
+ }
2217
+ .standup-lane-header {
2218
+ padding: 10px 11px;
2219
+ display: flex;
2220
+ align-items: center;
2221
+ justify-content: space-between;
2222
+ gap: 8px;
2223
+ border-bottom: 1px solid var(--border);
2224
+ }
2225
+ .standup-lane-title {
2226
+ display: flex;
2227
+ align-items: center;
2228
+ gap: 7px;
2229
+ min-width: 0;
2230
+ color: var(--fg);
2231
+ font-weight: 700;
2232
+ font-size: 13px;
2233
+ }
2234
+ .standup-lane-dot {
2235
+ width: 7px;
2236
+ height: 7px;
2237
+ border-radius: 50%;
2238
+ flex-shrink: 0;
2239
+ background: var(--fg-dim);
2240
+ }
2241
+ .standup-lane[data-lane="needs_user"] .standup-lane-dot { background: var(--yellow); }
2242
+ .standup-lane[data-lane="ready_review"] .standup-lane-dot { background: var(--green); }
2243
+ .standup-lane[data-lane="running"] .standup-lane-dot { background: var(--blue); }
2244
+ .standup-lane[data-lane="continue_later"] .standup-lane-dot { background: var(--purple); }
2245
+ .standup-lane-count {
2246
+ color: var(--fg-dim);
2247
+ font-size: 11px;
2248
+ border: 1px solid var(--border);
2249
+ border-radius: 999px;
2250
+ padding: 2px 7px;
2251
+ background: var(--bg);
2252
+ }
2253
+ .standup-lane-body {
2254
+ padding: 8px;
2255
+ display: flex;
2256
+ flex-direction: column;
2257
+ gap: 8px;
2258
+ }
2259
+ .standup-card {
2260
+ border: 1px solid var(--border);
2261
+ border-radius: 8px;
2262
+ background: var(--bg-light);
2263
+ padding: 10px;
2264
+ display: flex;
2265
+ flex-direction: column;
2266
+ gap: 8px;
2267
+ min-width: 0;
2268
+ }
2269
+ .standup-card-top {
2270
+ display: flex;
2271
+ align-items: flex-start;
2272
+ justify-content: space-between;
2273
+ gap: 8px;
2274
+ min-width: 0;
2275
+ }
2276
+ .standup-card-title {
2277
+ min-width: 0;
2278
+ color: var(--fg);
2279
+ font-size: 13px;
2280
+ font-weight: 700;
2281
+ line-height: 1.25;
2282
+ overflow-wrap: anywhere;
2283
+ }
2284
+ .standup-card-subtitle {
2285
+ color: var(--fg-dim);
2286
+ font-size: 11px;
2287
+ margin-top: 3px;
2288
+ overflow: hidden;
2289
+ text-overflow: ellipsis;
2290
+ white-space: nowrap;
2291
+ }
2292
+ .standup-badge {
2293
+ flex-shrink: 0;
2294
+ border: 1px solid var(--border);
2295
+ border-radius: 999px;
2296
+ padding: 2px 7px;
2297
+ font-size: 10px;
2298
+ color: var(--fg-dim);
2299
+ background: var(--bg);
2300
+ white-space: nowrap;
2301
+ max-width: 110px;
2302
+ overflow: hidden;
2303
+ text-overflow: ellipsis;
2304
+ }
2305
+ .standup-badge.status-running { color: var(--blue); border-color: rgba(122,162,247,0.45); }
2306
+ .standup-badge.status-waiting,
2307
+ .standup-badge.status-waiting_input { color: var(--yellow); border-color: rgba(224,175,104,0.45); }
2308
+ .standup-badge.status-idle { color: var(--green); border-color: rgba(158,206,106,0.35); }
2309
+ .standup-card-text {
2310
+ font-size: 12px;
2311
+ line-height: 1.4;
2312
+ color: var(--fg-dim);
2313
+ overflow-wrap: anywhere;
2314
+ }
2315
+ .standup-card-text strong {
2316
+ color: var(--fg);
2317
+ font-weight: 600;
2318
+ }
2319
+ .standup-evidence {
2320
+ display: flex;
2321
+ flex-wrap: wrap;
2322
+ gap: 5px;
2323
+ }
2324
+ .standup-chip {
2325
+ font-size: 10px;
2326
+ color: var(--fg-dim);
2327
+ border: 1px solid rgba(255,255,255,0.08);
2328
+ border-radius: 999px;
2329
+ padding: 2px 6px;
2330
+ background: rgba(0,0,0,0.14);
2331
+ max-width: 100%;
2332
+ overflow: hidden;
2333
+ text-overflow: ellipsis;
2334
+ white-space: nowrap;
2335
+ }
2336
+ .standup-card-actions {
2337
+ display: flex;
2338
+ gap: 6px;
2339
+ flex-wrap: wrap;
2340
+ }
2341
+ .standup-card-actions .standup-action-btn {
2342
+ padding: 5px 8px;
2343
+ min-height: 28px;
2344
+ font-size: 11px;
2345
+ }
2346
+ .standup-empty,
2347
+ .standup-loading,
2348
+ .standup-error {
2349
+ border: 1px dashed var(--border);
2350
+ border-radius: 8px;
2351
+ padding: 20px;
2352
+ text-align: center;
2353
+ color: var(--fg-dim);
2354
+ font-size: 13px;
2355
+ background: rgba(255,255,255,0.018);
2356
+ }
2357
+ .standup-error { color: var(--red); border-color: rgba(247,118,142,0.45); }
2358
+ @media (max-width: 760px) {
2359
+ #welcome { padding: 12px; }
2360
+ .standup-header { flex-direction: column; }
2361
+ .standup-meta { justify-content: flex-start; }
2362
+ .standup-attention { align-items: flex-start; flex-direction: column; }
2363
+ }
2033
2364
  .shortcut-grid {
2034
2365
  display: grid;
2035
2366
  grid-template-columns: auto auto;
@@ -2921,6 +3252,7 @@
2921
3252
  </div>
2922
3253
  </div>
2923
3254
  <span class="logo">CTM</span>
3255
+ <span class="app-version" id="app-version-label" title="Installed CTM / Wall-E bundle version">v?</span>
2924
3256
  </div>
2925
3257
  <nav class="topbar-nav" id="topbar-nav">
2926
3258
  <button class="nav-pill active" data-nav="sessions" onclick="navTo('sessions')" title="Terminal sessions">Sessions</button>
@@ -2961,16 +3293,16 @@
2961
3293
  <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
3294
  <span style="color:#bb9af7;">&#x2191;</span>
2963
3295
  <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>
3296
+ <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>
3297
+ <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
3298
  </div>
2967
3299
  <div class="modal-overlay update-wizard-overlay hidden" id="update-wizard" role="dialog" aria-modal="true" aria-labelledby="update-wizard-heading">
2968
3300
  <div class="modal update-wizard-modal">
2969
3301
  <div class="update-wizard-head">
2970
3302
  <div class="update-wizard-icon">&#x2191;</div>
2971
3303
  <div class="update-wizard-title">
2972
- <h3 id="update-wizard-heading">Upgrade CTM?</h3>
2973
- <p>A newer CTM release is available.</p>
3304
+ <h3 id="update-wizard-heading">Upgrade CTM / Wall-E?</h3>
3305
+ <p>A newer create-walle release is available.</p>
2974
3306
  </div>
2975
3307
  </div>
2976
3308
  <div class="update-wizard-body">
@@ -2987,8 +3319,8 @@
2987
3319
  <p class="update-wizard-note">The updater will run in the background and CTM will restart when the upgrade is ready.</p>
2988
3320
  <div class="update-wizard-actions">
2989
3321
  <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>
3322
+ <button class="btn" onclick="dismissUpdate('wizard')">Skip This Version</button>
3323
+ <button class="btn primary" id="update-wizard-apply-btn" onclick="applyUpdate('wizard')">Upgrade CTM</button>
2992
3324
  </div>
2993
3325
  </div>
2994
3326
  </div>
@@ -3028,8 +3360,8 @@
3028
3360
  <option value="name">A-Z</option>
3029
3361
  <option value="messages">Most Used</option>
3030
3362
  </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>
3363
+ <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;">
3364
+ <option value="">All Agents</option>
3033
3365
  </select>
3034
3366
  </div>
3035
3367
  <div class="display-toggle">
@@ -3068,21 +3400,27 @@
3068
3400
  </div>
3069
3401
  <div id="terminal-area">
3070
3402
  <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>
3403
+ <div class="standup-dashboard" id="standup-dashboard" onclick="standupHandleDashboardClick(event)">
3404
+ <div class="standup-header">
3405
+ <div class="standup-heading">
3406
+ <div class="standup-eyebrow">Session Overview</div>
3407
+ <h2>Standup</h2>
3408
+ </div>
3409
+ <div class="standup-meta">
3410
+ <div class="standup-counts" id="standup-counts"></div>
3411
+ <span class="standup-updated" id="standup-updated"></span>
3412
+ <div class="standup-actions">
3413
+ <button class="standup-action-btn" type="button" data-standup-action="refresh">Refresh</button>
3414
+ <button class="standup-action-btn primary" type="button" onclick="showNewSessionModal()">New Session</button>
3415
+ </div>
3416
+ </div>
3082
3417
  </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>
3418
+ <div class="standup-attention" id="standup-attention"></div>
3419
+ <div class="standup-loading" id="standup-loading">Loading sessions...</div>
3420
+ <div class="standup-error" id="standup-error" style="display:none;"></div>
3421
+ <div class="standup-lanes" id="standup-lanes"></div>
3422
+ <div class="standup-empty" id="standup-empty" style="display:none;">
3423
+ No active sessions.
3086
3424
  </div>
3087
3425
  </div>
3088
3426
  </div>
@@ -3212,7 +3550,10 @@
3212
3550
  <div id="setup-panel">
3213
3551
  <div class="setup-header">
3214
3552
  <div style="display:flex;justify-content:space-between;align-items:baseline;">
3215
- <h2>Wall-E Setup</h2>
3553
+ <div style="display:flex;align-items:center;gap:8px;min-width:0;">
3554
+ <h2>Wall-E Setup</h2>
3555
+ <span class="setup-version-pill" id="setup-version-label" title="Installed CTM / Wall-E bundle version">Version loading...</span>
3556
+ </div>
3216
3557
  <div style="display:flex;align-items:center;gap:6px;">
3217
3558
  <span class="status-dot ok" id="setup-owner-dot"></span>
3218
3559
  <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 +3934,13 @@
3593
3934
  <div id="setup-mcp-integrations"><div style="color:var(--fg-dim);font-size:12px;padding:8px 0;">Loading…</div></div>
3594
3935
  <div class="btn-row" style="margin-top:8px;">
3595
3936
  <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>
3937
+ <button class="setup-btn setup-btn-secondary" id="setup-mcp-fix-btn" onclick="SETUP.fixMcpConfigs()" style="display:none">Repair Configs</button>
3597
3938
  <span class="test-result" id="setup-mcp-test-result"></span>
3598
3939
  </div>
3599
3940
  <details style="margin-top:14px;">
3600
3941
  <summary style="cursor:pointer;font-size:12px;color:var(--fg-dim);">Manual setup instructions</summary>
3601
3942
  <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:
3943
+ Add this to JSON-based MCP configs:
3603
3944
  <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
3945
  "mcpServers": {
3605
3946
  "wall-e": {
@@ -3608,9 +3949,14 @@
3608
3949
  }
3609
3950
  }
3610
3951
  }</code></pre>
3952
+ <div style="margin-top:8px;">For Codex, add this to <code style="font-size:11px;">~/.codex/config.toml</code>:</div>
3953
+ <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"]
3954
+ url = "http://localhost:<span id="setup-mcp-port-display-toml">3457</span>/mcp"</code></pre>
3611
3955
  <div style="margin-top:6px;">
3612
3956
  <strong style="color:var(--fg);">Config file locations:</strong><br>
3613
- Claude Code: <code style="font-size:11px;">~/.claude/mcp.json</code><br>
3957
+ Claude Code project: <code style="font-size:11px;">~/.claude/mcp.json</code><br>
3958
+ Claude Code global: <code style="font-size:11px;">~/.claude.json</code><br>
3959
+ Codex: <code style="font-size:11px;">~/.codex/config.toml</code><br>
3614
3960
  Cursor: <code style="font-size:11px;">~/.cursor/mcp.json</code><br>
3615
3961
  Windsurf: <code style="font-size:11px;">~/.codeium/windsurf/mcp_config.json</code><br>
3616
3962
  Claude Desktop: <code style="font-size:11px;">~/Library/Application Support/Claude/claude_desktop_config.json</code>
@@ -3682,8 +4028,6 @@
3682
4028
  </div>
3683
4029
  </div>
3684
4030
  </div>
3685
-
3686
- <div id="setup-version-label"></div>
3687
4031
  </div>
3688
4032
  </div>
3689
4033
  </div>
@@ -4463,15 +4807,87 @@ function _safeStorageSet(storage, key, value) {
4463
4807
  try { storage.setItem(key, value); } catch {}
4464
4808
  }
4465
4809
 
4810
+ function setAppVersion(version, info = {}) {
4811
+ const cleanVersion = String(version || '').trim();
4812
+ const label = cleanVersion ? `v${cleanVersion}` : 'v?';
4813
+ const product = info.product || 'create-walle';
4814
+ const latest = String(info.latestVersion || '').trim();
4815
+
4816
+ const topbar = document.getElementById('app-version-label');
4817
+ if (topbar) {
4818
+ topbar.textContent = label;
4819
+ topbar.classList.toggle('update-available', !!(latest && cleanVersion && latest !== cleanVersion));
4820
+ topbar.title = cleanVersion
4821
+ ? `Installed CTM / Wall-E bundle version: ${product} ${label}`
4822
+ : 'Installed CTM / Wall-E bundle version unknown';
4823
+ if (latest && cleanVersion && latest !== cleanVersion) {
4824
+ topbar.title += `; update available: v${latest}`;
4825
+ }
4826
+ if (info.components) {
4827
+ const ctm = info.components.ctm ? `CTM package ${info.components.ctm}` : '';
4828
+ const walle = info.components.wallE ? `Wall-E package ${info.components.wallE}` : '';
4829
+ const parts = [ctm, walle].filter(Boolean);
4830
+ if (parts.length) topbar.title += ` (${parts.join(', ')})`;
4831
+ }
4832
+ }
4833
+
4834
+ const setup = document.getElementById('setup-version-label');
4835
+ if (setup) {
4836
+ setup.textContent = cleanVersion ? `CTM / Wall-E ${label}` : 'Version unknown';
4837
+ setup.title = topbar ? topbar.title : 'Installed CTM / Wall-E bundle version';
4838
+ }
4839
+ }
4840
+ window.setAppVersion = setAppVersion;
4841
+
4842
+ function loadAppVersion() {
4843
+ fetch('/api/app/version')
4844
+ .then(r => r.json())
4845
+ .then(data => {
4846
+ if (data && data.version) setAppVersion(data.version, data);
4847
+ })
4848
+ .catch(() => {});
4849
+ }
4850
+
4466
4851
  let _updateDismissedVersion = _safeStorageGet(localStorage, 'update_dismissed_version');
4467
4852
  let _updateWizardSnoozedVersion = _safeStorageGet(sessionStorage, 'update_wizard_snoozed_version');
4468
4853
  let _updateCurrentVersion = '';
4469
4854
  let _updateLatestVersion = '';
4470
4855
  let _updateApplying = false;
4856
+ const _updateTelemetryShown = new Set();
4857
+
4858
+ function _trackUpdateTelemetry(event, fields = {}) {
4859
+ const payload = {
4860
+ event,
4861
+ currentVersion: _updateCurrentVersion,
4862
+ latestVersion: _updateLatestVersion,
4863
+ ...fields,
4864
+ };
4865
+ fetch('/api/updates/telemetry', {
4866
+ method: 'POST',
4867
+ headers: { 'Content-Type': 'application/json' },
4868
+ body: JSON.stringify(payload),
4869
+ }).catch(() => {});
4870
+ }
4871
+
4872
+ function _trackUpdatePromptShown(surface) {
4873
+ if (!_updateLatestVersion) return;
4874
+ const key = `${surface}:${_updateLatestVersion}`;
4875
+ if (_updateTelemetryShown.has(key)) return;
4876
+ _updateTelemetryShown.add(key);
4877
+ _trackUpdateTelemetry('prompt_shown', { surface });
4878
+ }
4879
+
4880
+ function _rememberDismissedUpdate() {
4881
+ if (_updateLatestVersion) {
4882
+ _updateDismissedVersion = _updateLatestVersion;
4883
+ _safeStorageSet(localStorage, 'update_dismissed_version', _updateLatestVersion);
4884
+ }
4885
+ }
4471
4886
 
4472
4887
  function _setUpdateVersions(current, latest) {
4473
4888
  _updateCurrentVersion = current || '';
4474
4889
  _updateLatestVersion = latest || '';
4890
+ setAppVersion(_updateCurrentVersion, { latestVersion: _updateLatestVersion });
4475
4891
 
4476
4892
  const banner = document.getElementById('update-banner');
4477
4893
  if (banner) {
@@ -4518,6 +4934,7 @@ function showUpdateBanner(current, latest) {
4518
4934
  if (!banner || !msg) return;
4519
4935
  msg.textContent = `Update available: v${current} \u2192 v${latest}`;
4520
4936
  banner.style.display = 'flex';
4937
+ _trackUpdatePromptShown('banner');
4521
4938
  showUpdateWizard(current, latest);
4522
4939
  }
4523
4940
 
@@ -4527,6 +4944,7 @@ function showUpdateWizard(current, latest) {
4527
4944
  const wizard = document.getElementById('update-wizard');
4528
4945
  if (!wizard) return;
4529
4946
  wizard.classList.remove('hidden');
4947
+ _trackUpdatePromptShown('wizard');
4530
4948
  setTimeout(() => {
4531
4949
  const btn = document.getElementById('update-wizard-apply-btn');
4532
4950
  if (btn && !btn.disabled) btn.focus();
@@ -4534,6 +4952,7 @@ function showUpdateWizard(current, latest) {
4534
4952
  }
4535
4953
 
4536
4954
  function snoozeUpdateWizard() {
4955
+ _trackUpdateTelemetry('action', { action: 'later', surface: 'wizard' });
4537
4956
  if (_updateLatestVersion) {
4538
4957
  _updateWizardSnoozedVersion = _updateLatestVersion;
4539
4958
  _safeStorageSet(sessionStorage, 'update_wizard_snoozed_version', _updateLatestVersion);
@@ -4541,38 +4960,44 @@ function snoozeUpdateWizard() {
4541
4960
  _hideUpdateWizard();
4542
4961
  }
4543
4962
 
4544
- function dismissUpdate() {
4963
+ function dismissUpdate(surface = 'unknown') {
4964
+ _trackUpdateTelemetry('action', { action: 'skip', surface });
4545
4965
  _hideUpdatePrompts();
4546
- if (_updateLatestVersion) {
4547
- _updateDismissedVersion = _updateLatestVersion;
4548
- _safeStorageSet(localStorage, 'update_dismissed_version', _updateLatestVersion);
4549
- }
4966
+ _rememberDismissedUpdate();
4550
4967
  }
4551
4968
 
4552
- async function applyUpdate() {
4969
+ async function applyUpdate(surface = 'unknown') {
4553
4970
  if (_updateApplying) return;
4971
+ _trackUpdateTelemetry('action', { action: 'apply', surface });
4554
4972
  _setUpdateApplying(true);
4555
4973
  try {
4556
4974
  const resp = await fetch('/api/updates/apply', { method: 'POST' });
4557
4975
  const data = await resp.json();
4558
4976
  if (data.status === 'updating') {
4977
+ _trackUpdateTelemetry('action', { action: 'apply_started', surface });
4559
4978
  toast('Update started. CTM will restart shortly...', { type: 'info', duration: 8000 });
4560
- dismissUpdate();
4979
+ _hideUpdatePrompts();
4980
+ _rememberDismissedUpdate();
4561
4981
  } else {
4982
+ _trackUpdateTelemetry('action', { action: 'already_up_to_date', surface });
4562
4983
  toast('Already up to date.', { type: 'success' });
4563
4984
  _hideUpdatePrompts();
4564
4985
  _setUpdateApplying(false);
4565
4986
  }
4566
4987
  } catch (e) {
4988
+ _trackUpdateTelemetry('action', { action: 'apply_failed', surface });
4567
4989
  toast('Update failed: ' + e.message, { type: 'error' });
4568
4990
  _setUpdateApplying(false);
4569
4991
  }
4570
4992
  }
4571
4993
 
4572
4994
  function checkForUpdates() {
4573
- fetch('/api/updates/check')
4995
+ fetch('/api/updates/check?refresh=1')
4574
4996
  .then(r => r.json())
4575
4997
  .then(data => {
4998
+ if (data.currentVersion) {
4999
+ setAppVersion(data.currentVersion, { latestVersion: data.latestVersion });
5000
+ }
4576
5001
  if (data.updateAvailable) {
4577
5002
  showUpdateBanner(data.currentVersion, data.latestVersion);
4578
5003
  }
@@ -4581,6 +5006,7 @@ function checkForUpdates() {
4581
5006
  }
4582
5007
 
4583
5008
  // Check on page load
5009
+ setTimeout(loadAppVersion, 0);
4584
5010
  setTimeout(checkForUpdates, 3000);
4585
5011
 
4586
5012
  // --- State ---
@@ -4594,6 +5020,7 @@ const state = window._ctmState = {
4594
5020
  sessions: new Map(), // id -> { term, fitAddon, container }
4595
5021
  activeTab: null, // session id or 'rules'
4596
5022
  lastActiveWorkSessionId: null, // last non-Wall-E session used as repo context
5023
+ standup: { loading: false, data: null, lastLoadedAt: 0 },
4597
5024
  tabOrder: [], // session ids in tab order
4598
5025
  reviewingSessionId: null, // currently reviewed session
4599
5026
  sidebarCollapsed: false,
@@ -4715,6 +5142,7 @@ function connect() {
4715
5142
  case 'walle-progress': WalleSession.handleProgress(msg); break;
4716
5143
  case 'walle-response': WalleSession.handleResponse(msg); break;
4717
5144
  case 'walle-history': WalleSession.handleHistory(msg); break;
5145
+ case 'walle-model': WalleSession.handleModel(msg); break;
4718
5146
  case 'server-restarting':
4719
5147
  // Server broadcast this BEFORE closing connections — set flag so onclose
4720
5148
  // knows this is intentional and shows the overlay immediately.
@@ -4834,7 +5262,7 @@ function onServerReady() {
4834
5262
  }
4835
5263
 
4836
5264
  // 2. Restore active tab — single source of truth for session activation.
4837
- // Priority: saved pref > pending hash > first session in tab order.
5265
+ // Priority: explicit hash > saved session > pinned Sessions overview.
4838
5266
  // SKIP this step if the user is on a non-session nav panel (walle, prompts, etc.):
4839
5267
  // handleHashRoute already navigated there, and activateTab would switch away.
4840
5268
  const currentNav = document.querySelector('.nav-pill.active')?.dataset?.nav || 'sessions';
@@ -4848,13 +5276,15 @@ function onServerReady() {
4848
5276
  // auto-activating in onSessionsList.
4849
5277
  if (hashTarget && state.sessions.has(hashTarget)) {
4850
5278
  activateTab(hashTarget);
5279
+ } else if (state._forceSessionsOverview) {
5280
+ state._forceSessionsOverview = false;
5281
+ showStandupDashboard({ skipHash: true });
4851
5282
  } else if (savedTarget && state.sessions.has(savedTarget)) {
4852
5283
  activateTab(savedTarget);
4853
5284
  } else if (savedTarget === 'review' && state.tabOrder.includes('review')) {
4854
5285
  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);
5286
+ } else if (!state.activeTab || (!state.sessions.has(state.activeTab) && state.activeTab !== SESSIONS_OVERVIEW_TAB_ID)) {
5287
+ showStandupDashboard({ skipHash: true });
4858
5288
  }
4859
5289
  }
4860
5290
 
@@ -5156,6 +5586,7 @@ function _clientDetectAgentType(cmd) {
5156
5586
  if (c.includes('claude')) return 'claude';
5157
5587
  if (c.includes('codex')) return 'codex';
5158
5588
  if (c.includes('gemini')) return 'gemini';
5589
+ if (c.includes('opencode') || c.includes('open-code')) return 'opencode';
5159
5590
  if (c.includes('wall-e') || c.includes('walle')) return 'walle';
5160
5591
  return null;
5161
5592
  }
@@ -5165,6 +5596,7 @@ const CLIENT_AGENT_CAPABILITIES = {
5165
5596
  'claude-desktop': { structuredTranscript: true, promptNavigation: 'transcript', review: true, resume: false },
5166
5597
  codex: { structuredTranscript: true, promptNavigation: 'transcript', review: true, resume: true },
5167
5598
  gemini: { structuredTranscript: false, promptNavigation: 'terminal', review: false, resume: true },
5599
+ opencode: { structuredTranscript: false, promptNavigation: 'terminal', review: false, resume: true },
5168
5600
  walle: { structuredTranscript: true, promptNavigation: 'none', review: true, resume: false },
5169
5601
  shell: { structuredTranscript: false, promptNavigation: 'terminal', review: false, resume: false },
5170
5602
  };
@@ -5176,6 +5608,7 @@ function _clientNormalizeAgentType(value) {
5176
5608
  if (v === 'claude-code') return 'claude';
5177
5609
  if (v === 'claude-desktop-session' || v === 'desktop') return 'claude-desktop';
5178
5610
  if (v === 'gemini-cli') return 'gemini';
5611
+ if (v === 'open-code' || v === 'opencode-cli') return 'opencode';
5179
5612
  return _clientDetectAgentType(v);
5180
5613
  }
5181
5614
 
@@ -5206,6 +5639,41 @@ const _CODEX_BUSY_WORD = 'working';
5206
5639
  const _CODEX_BUSY_HINT_RE = /esc\s+to\s+interrupt/i;
5207
5640
  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
5641
 
5642
+ function _inputMayResolveWaiting(data, session) {
5643
+ const text = String(data || '');
5644
+ if (!text) return false;
5645
+ if (text.indexOf('\r') >= 0 || text.indexOf('\n') >= 0) return true;
5646
+ if (text.indexOf('\x03') >= 0 || text.indexOf('\x04') >= 0) return true;
5647
+ const reason = session?._waitingReason || '';
5648
+ if (reason === 'approval' || reason === 'choice') {
5649
+ return /^[\s]*[12yYnN\u001b][\s]*$/.test(text);
5650
+ }
5651
+ return false;
5652
+ }
5653
+
5654
+ const _CODEX_MUTED_PROMPT_BG = new Set(['237', '238']);
5655
+ function _normalizeCodexPromptBackground(session, data) {
5656
+ if (_clientAgentTypeForSession(session) !== 'codex') return data;
5657
+ const text = String(data || '');
5658
+ if (text.indexOf('\x1b[') < 0 || text.indexOf('48;5;23') < 0) return data;
5659
+ return text.replace(/\x1b\[([0-9;]*)m/g, (seq, rawParams) => {
5660
+ const params = rawParams === '' ? ['0'] : rawParams.split(';');
5661
+ const hasMutedPromptBg = params.some((value, i) => (
5662
+ value === '48' && params[i + 1] === '5' && _CODEX_MUTED_PROMPT_BG.has(params[i + 2])
5663
+ ));
5664
+ if (!hasMutedPromptBg) return seq;
5665
+ const next = [];
5666
+ for (let i = 0; i < params.length; i++) {
5667
+ if (params[i] === '48' && params[i + 1] === '5' && _CODEX_MUTED_PROMPT_BG.has(params[i + 2])) {
5668
+ i += 2;
5669
+ continue;
5670
+ }
5671
+ next.push(params[i]);
5672
+ }
5673
+ return next.length ? `\x1b[${next.join(';')}m` : '';
5674
+ });
5675
+ }
5676
+
5209
5677
  function _isClaudeRedraw(data) {
5210
5678
  const s = String(data || '');
5211
5679
  return (
@@ -5283,7 +5751,8 @@ function _zerolagConfigFor(agentType) {
5283
5751
  // Prompt character per agent — the addon scans the buffer bottom-up for this
5284
5752
  // char. offset 2 = input starts 2 cells after the prompt char (char + space).
5285
5753
  if (agentType === 'claude') return { type: 'character', char: '❯', offset: 2 }; // ❯
5286
- if (agentType === 'codex') return { type: 'character', char: '›', offset: 2 };
5754
+ // Codex intentionally disabled: its ratatui redraws can leave stale lower
5755
+ // prompt markers, and the character scanner can strand typed text there.
5287
5756
  // Gemini intentionally disabled until we verify its prompt format.
5288
5757
  // Re-enable after a quick live-session check of its actual TUI layout.
5289
5758
  return null;
@@ -5411,6 +5880,7 @@ function createTerminal(id, opts) {
5411
5880
  // snapshot message. No PTY round-trip — instant snapshot restore.
5412
5881
  s.term.clear();
5413
5882
  try { s.term.clearTextureAtlas(); } catch {}
5883
+ _markClientUiRefreshOutputSuppression(s);
5414
5884
  send({ type: 'reflow', id: id, cols: s.term.cols, rows: s.term.rows });
5415
5885
  };
5416
5886
  toolbar.appendChild(reflowBtn);
@@ -5753,8 +6223,9 @@ function createTerminal(id, opts) {
5753
6223
  // The addon scans the xterm buffer for the prompt char and clears pending text
5754
6224
  // once the server echo catches up — safe alongside CTM's immediate-send model.
5755
6225
  _zerolagFeedInput(_s, data);
5756
- // Only clear waiting state if actually waiting (avoid DOM work on every keystroke)
5757
- if (_s && _s._waitingForInput) clearWaitingState(id);
6226
+ // Only resolve waiting on submit/choice keystrokes. Plain prompt typing
6227
+ // should keep the sidebar in Waiting, not manufacture a Running window.
6228
+ if (_s && _s._waitingForInput && _inputMayResolveWaiting(data, _s)) clearWaitingState(id);
5758
6229
  // User typed something — re-enable follow mode and scroll to bottom.
5759
6230
  const s = _s;
5760
6231
  if (s) {
@@ -5784,6 +6255,7 @@ function createTerminal(id, opts) {
5784
6255
  term.onResize(({ cols, rows }) => {
5785
6256
  const s = state.sessions.get(id);
5786
6257
  if (s && s._suppressResize) return; // Skip during font metric refresh
6258
+ _markClientUiRefreshOutputSuppression(s);
5787
6259
  send({ type: 'resize', id, cols, rows });
5788
6260
  });
5789
6261
 
@@ -5871,10 +6343,18 @@ function createTerminal(id, opts) {
5871
6343
  if (writer._userScrollLocked) return; // locked — only wheel can unlock
5872
6344
  const current = state.sessions.get(id) || { _id: id, term, writer, container };
5873
6345
  writer.followMode = _isAtTerminalFollowBottom(current);
6346
+ requestAnimationFrame(() => _alignXtermHelperTextareaToViewport(current));
5874
6347
  });
5875
6348
 
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).
6349
+ const sessionEntry = { _id: id, term, fitAddon, searchAddon, container, needsFontRefresh: true, writer, promptLines: [], promptNavIdx: -1, _webglAddon, _loadWebgl };
6350
+ state.sessions.set(id, sessionEntry);
6351
+ if (typeof term.onWriteParsed === 'function') {
6352
+ sessionEntry._helperAlignDisposer = term.onWriteParsed(() => {
6353
+ const current = state.sessions.get(id);
6354
+ if (current) _alignXtermHelperTextareaToViewport(current);
6355
+ });
6356
+ }
6357
+ // Attach local echo overlay for known agent types (Claude today; Codex/Gemini pending prompt verification).
5878
6358
  _attachZerolag(state.sessions.get(id), id);
5879
6359
  return { term, fitAddon, container };
5880
6360
  }
@@ -5956,7 +6436,54 @@ function _isTerminalPromptLine(agentType, text) {
5956
6436
  // API results are cached and only re-fetched every 30s to avoid input lag.
5957
6437
  const _promptScanCache = {}; // { [sessionId]: { ts, previews } }
5958
6438
 
5959
- function scanPromptLines(id) {
6439
+ function _promptPreviewsEqual(a, b) {
6440
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
6441
+ for (let i = 0; i < a.length; i++) {
6442
+ if (a[i] !== b[i]) return false;
6443
+ }
6444
+ return true;
6445
+ }
6446
+
6447
+ function _promptCacheKeyMatchesSession(key, id, s) {
6448
+ if (!key || key === id || key.startsWith(id + ':')) return true;
6449
+ if (s?.meta?.claudeSessionId && key.startsWith(s.meta.claudeSessionId)) return true;
6450
+ if (s?.meta?.agentSessionId && key.startsWith(s.meta.agentSessionId)) return true;
6451
+ if (s?.meta?.agentSessionToken && key.startsWith(s.meta.agentSessionToken)) return true;
6452
+ return false;
6453
+ }
6454
+
6455
+ function invalidatePromptScanCacheForSession(id) {
6456
+ const s = state.sessions.get(id);
6457
+ for (const key of Object.keys(_promptScanCache)) {
6458
+ if (_promptCacheKeyMatchesSession(key, id, s)) delete _promptScanCache[key];
6459
+ }
6460
+ }
6461
+
6462
+ function _recordLivePromptPreview(id, text) {
6463
+ const s = state.sessions.get(id);
6464
+ if (!s) return;
6465
+ const cleaned = _extractPromptPreview(text);
6466
+ if (!cleaned) return;
6467
+ const previews = Array.isArray(s.promptPreviews) ? s.promptPreviews.slice() : [];
6468
+ if (previews[previews.length - 1] === cleaned) return;
6469
+ previews.push(cleaned);
6470
+ s.promptPreviews = previews;
6471
+ s.promptLines = previews.map((_, i) => Array.isArray(s.promptLines) && i < s.promptLines.length ? s.promptLines[i] : -1);
6472
+ s.promptNavIdx = -1;
6473
+ s._promptLinesResolved = false;
6474
+ promptNavUpdateBadge(id);
6475
+
6476
+ for (const key of Object.keys(_promptScanCache)) {
6477
+ if (_promptCacheKeyMatchesSession(key, id, s)) {
6478
+ _promptScanCache[key] = { ts: Date.now(), previews: previews.slice() };
6479
+ }
6480
+ }
6481
+ }
6482
+
6483
+ window._ctmRecordLivePromptPreview = _recordLivePromptPreview;
6484
+
6485
+ function scanPromptLines(id, opts) {
6486
+ opts = opts || {};
5960
6487
  const s = state.sessions.get(id);
5961
6488
  if (!s) return;
5962
6489
  const agentType = _clientAgentTypeForSession(s);
@@ -5985,8 +6512,8 @@ function scanPromptLines(id) {
5985
6512
  const apiProjectEntry = agentType === 'codex' ? '' : projectEntry;
5986
6513
  const cacheKey = claudeId + ':' + (apiProjectEntry || 'codex');
5987
6514
  const cache = _promptScanCache[cacheKey];
5988
- if (cache && Date.now() - cache.ts < 30000) {
5989
- if (!s.promptPreviews || s.promptPreviews.length !== cache.previews.length) {
6515
+ if (!opts.force && cache && Date.now() - cache.ts < 30000) {
6516
+ if (!_promptPreviewsEqual(s.promptPreviews, cache.previews)) {
5990
6517
  s.promptLines = cache.previews.map(() => -1);
5991
6518
  s.promptPreviews = cache.previews;
5992
6519
  s.promptNavIdx = -1;
@@ -5994,7 +6521,7 @@ function scanPromptLines(id) {
5994
6521
  promptNavUpdateBadge(id);
5995
6522
  return;
5996
6523
  }
5997
- _scanPromptLinesFromAPI(id, apiProjectEntry, claudeId);
6524
+ return _scanPromptLinesFromAPI(id, apiProjectEntry, claudeId);
5998
6525
  } else {
5999
6526
  _scanPromptLinesFromTerminal(id);
6000
6527
  }
@@ -6005,7 +6532,13 @@ async function _scanPromptLinesFromAPI(id, projectEntry, claudeId) {
6005
6532
  if (!s) return;
6006
6533
  const apiId = claudeId || id; // Use Claude session ID for the API, tab ID for state
6007
6534
  try {
6008
- const res = await fetch(`/api/session/messages?id=${apiId}&project=${encodeURIComponent(projectEntry || '')}&token=${state.token}`);
6535
+ const params = new URLSearchParams({
6536
+ id: apiId,
6537
+ project: projectEntry || '',
6538
+ token: state.token,
6539
+ nocache: '1',
6540
+ });
6541
+ const res = await fetch(`/api/session/messages?${params.toString()}`);
6009
6542
  const raw = await res.json();
6010
6543
  const messages = Array.isArray(raw) ? raw : (Array.isArray(raw.messages) ? raw.messages : null);
6011
6544
  if (raw.error || !messages) {
@@ -6198,16 +6731,23 @@ function promptNavUpdateBadge(id) {
6198
6731
  nextBtn.disabled = total === 0;
6199
6732
  }
6200
6733
 
6201
- function promptNavToggleList(id) {
6734
+ async function promptNavToggleList(id) {
6202
6735
  const s = state.sessions.get(id);
6203
6736
  if (!s) return;
6204
- // Always re-resolve positions from current buffer (buffer may have grown)
6205
- _applyPromptCache(id);
6206
6737
  const nav = s.container.querySelector('.prompt-nav');
6207
6738
  if (!nav) return;
6208
6739
  // Close existing list
6209
6740
  const existing = nav.querySelector('.prompt-nav-list');
6210
6741
  if (existing) { existing.remove(); return; }
6742
+
6743
+ // The user opens this list specifically to see the current prompt history.
6744
+ // Bypass the 30s prompt cache here; otherwise a newly submitted prompt can
6745
+ // be visible in Conversation while this dropdown still shows the previous
6746
+ // transcript snapshot.
6747
+ await scanPromptLines(id, { force: true });
6748
+ // Re-resolve positions from current buffer (buffer may have grown) after the
6749
+ // fresh transcript read updates previews.
6750
+ _applyPromptCache(id);
6211
6751
  if (s.promptLines.length === 0) return;
6212
6752
 
6213
6753
  // Build list showing preview text for each prompt (deduplicated)
@@ -6292,6 +6832,9 @@ function activateTab(id) {
6292
6832
 
6293
6833
  const specialPanels = ['rules', 'insights', 'permissions', 'prompts', 'codereview', 'walle', 'models', 'backups', 'worktrees', 'setup'];
6294
6834
  const isPanel = specialPanels.includes(id);
6835
+ if (!isPanel && state.sessions.has(id)) {
6836
+ state._savedActiveSession = id;
6837
+ }
6295
6838
  if (state.activeTab && state.activeTab !== id && state.sessions.has(state.activeTab)) {
6296
6839
  const prev = state.sessions.get(state.activeTab);
6297
6840
  if (prev && prev.meta?.type !== 'walle') state.lastActiveWorkSessionId = state.activeTab;
@@ -6323,6 +6866,8 @@ function activateTab(id) {
6323
6866
  // Trim trailing empty lines to keep the saved text compact
6324
6867
  while (lines.length && lines[lines.length - 1] === '') lines.pop();
6325
6868
  prevSession._savedScrollbackText = lines.join('\r\n');
6869
+ prevSession._savedScrollbackStats = _terminalPlainTextStats(prevSession._savedScrollbackText);
6870
+ prevSession._savedScrollbackCapturedAt = Date.now();
6326
6871
  } catch {}
6327
6872
  // Overlay holds a DOM ref inside the xterm container; detach before disposing
6328
6873
  // the terminal so it doesn't try to re-render against a dead _renderService.
@@ -6508,6 +7053,9 @@ function activateTab(id) {
6508
7053
  const savedCachedSnapshot = s._cachedSnapshot;
6509
7054
  const savedCachedSnapshotCols = s._cachedSnapshotCols;
6510
7055
  const savedCachedSnapshotRows = s._cachedSnapshotRows;
7056
+ const savedScrollbackText = s._savedScrollbackText;
7057
+ const savedScrollbackStats = s._savedScrollbackStats;
7058
+ const savedScrollbackCapturedAt = s._savedScrollbackCapturedAt;
6511
7059
  // Make container visible first — term.open() needs non-zero dimensions
6512
7060
  s.container.classList.add('active');
6513
7061
  createTerminal(id, { active: true }); // replaces stub in state.sessions, cleans up old container
@@ -6527,6 +7075,9 @@ function activateTab(id) {
6527
7075
  s2._cachedSnapshot = savedCachedSnapshot;
6528
7076
  s2._cachedSnapshotCols = savedCachedSnapshotCols;
6529
7077
  s2._cachedSnapshotRows = savedCachedSnapshotRows;
7078
+ s2._savedScrollbackText = savedScrollbackText;
7079
+ s2._savedScrollbackStats = savedScrollbackStats;
7080
+ s2._savedScrollbackCapturedAt = savedScrollbackCapturedAt;
6530
7081
  s2.needsAttach = true;
6531
7082
  s2.needsFontRefresh = true;
6532
7083
  }
@@ -6541,7 +7092,9 @@ function activateTab(id) {
6541
7092
 
6542
7093
  const canRestoreExitedText = !!(sReal._exited && sReal._savedScrollbackText && sReal.term);
6543
7094
  const hasCachedSnapshot = !!(sReal._cachedSnapshot && sReal.term);
6544
- if (!canRestoreExitedText && !hasCachedSnapshot && sReal.term) {
7095
+ const shouldRestoreSavedText = !!(!sReal._exited && sReal._savedScrollbackText && sReal.term
7096
+ && (!hasCachedSnapshot || _snapshotLooksShorterThanSaved(sReal, sReal._cachedSnapshot)));
7097
+ if (!canRestoreExitedText && !hasCachedSnapshot && !shouldRestoreSavedText && sReal.term) {
6545
7098
  // No cached snapshot — first-ever activation. Show loading overlay.
6546
7099
  _showLoadingOverlay(sReal);
6547
7100
  }
@@ -6582,11 +7135,9 @@ function activateTab(id) {
6582
7135
  // requested below; painting an old-width full-screen TUI is what causes
6583
7136
  // Codex/Claude output to appear garbled until a manual reflow.
6584
7137
  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));
7138
+ _restoreSavedScrollbackText(sReal, () => _ensureScrolledToBottom(sReal));
7139
+ } else if (shouldRestoreSavedText) {
7140
+ _restoreSavedScrollbackText(sReal, () => _ensureScrolledToBottom(sReal));
6590
7141
  } else if (hasCachedSnapshot && sReal._cachedSnapshot && sReal.term) {
6591
7142
  if (_snapshotDimsMatchTerm(sReal, sReal._cachedSnapshotCols, sReal._cachedSnapshotRows)) {
6592
7143
  _restoreSnapshotData(sReal, sReal._cachedSnapshot, () => _ensureScrolledToBottom(sReal));
@@ -6621,6 +7172,7 @@ function activateTab(id) {
6621
7172
  try { sReal.term.refresh(0, Math.max(0, sReal.term.rows - 1)); } catch {}
6622
7173
  }
6623
7174
 
7175
+ _markClientUiRefreshOutputSuppression(sReal);
6624
7176
  send({ type: 'resize', id, cols: sReal.term.cols, rows: sReal.term.rows });
6625
7177
  sReal.writer.followMode = true;
6626
7178
  sReal.writer._userScrollLocked = false;
@@ -6699,6 +7251,7 @@ function activateTab(id) {
6699
7251
  sReal.term.clear();
6700
7252
  sReal.term.clearTextureAtlas();
6701
7253
  } catch {}
7254
+ _markClientUiRefreshOutputSuppression(sReal);
6702
7255
  send({ type: 'reflow', id, cols: sReal.term.cols, rows: sReal.term.rows });
6703
7256
  }, 6000);
6704
7257
  focusTerminalIfSafe(id);
@@ -6755,34 +7308,291 @@ function activateTab(id) {
6755
7308
  renderSessionList();
6756
7309
  }
6757
7310
 
6758
- function syncNavPills(activeId) {
6759
- const navPills = document.querySelectorAll('#topbar-nav .nav-pill');
6760
- const specialPanels = ['rules', 'insights', 'permissions', 'prompts', 'codereview', 'walle', 'models', 'backups', 'worktrees', 'setup'];
6761
- const navId = specialPanels.includes(activeId) ? activeId : 'sessions';
6762
- navPills.forEach(pill => {
6763
- pill.classList.toggle('active', pill.dataset.nav === navId);
6764
- });
7311
+ function syncNavPills(activeId) {
7312
+ const navPills = document.querySelectorAll('#topbar-nav .nav-pill');
7313
+ const specialPanels = ['rules', 'insights', 'permissions', 'prompts', 'codereview', 'walle', 'models', 'backups', 'worktrees', 'setup'];
7314
+ const navId = specialPanels.includes(activeId) ? activeId : 'sessions';
7315
+ navPills.forEach(pill => {
7316
+ pill.classList.toggle('active', pill.dataset.nav === navId);
7317
+ });
7318
+ }
7319
+
7320
+ function updateTopbarContext(activeId) {
7321
+ const ctxBtns = document.getElementById('topbar-context-btns');
7322
+ const newSessionBtn = document.getElementById('topbar-new-session-btn');
7323
+ const divider = document.getElementById('topbar-divider');
7324
+ if (!ctxBtns) return;
7325
+
7326
+ if (activeId === 'prompts') {
7327
+ ctxBtns.innerHTML = `
7328
+ <button class="topbar-util-btn" onclick="PE.showView('conversations')" title="Browse prompt conversations">Conversations</button>
7329
+ `;
7330
+ divider.style.display = '';
7331
+ newSessionBtn.innerHTML = '+ New Prompt';
7332
+ newSessionBtn.onclick = () => PE.createNewPrompt();
7333
+ } else {
7334
+ ctxBtns.innerHTML = '';
7335
+ divider.style.display = 'none';
7336
+ newSessionBtn.innerHTML = '+ New Session';
7337
+ newSessionBtn.onclick = () => showNewSessionModal();
7338
+ }
7339
+ }
7340
+
7341
+ const STANDUP_LANE_LABELS = {
7342
+ needs_user: 'Needs User',
7343
+ ready_review: 'Ready Review',
7344
+ running: 'Running',
7345
+ continue_later: 'Continue Later',
7346
+ };
7347
+ const SESSIONS_OVERVIEW_TAB_ID = '__sessions_overview__';
7348
+
7349
+ function standupEsc(value) {
7350
+ return escHtml(String(value == null ? '' : value));
7351
+ }
7352
+
7353
+ function standupStatusClass(status) {
7354
+ return 'status-' + String(status || 'unknown').toLowerCase().replace(/[^a-z0-9_-]/g, '-');
7355
+ }
7356
+
7357
+ function standupIsVisible() {
7358
+ const welcome = document.getElementById('welcome');
7359
+ return !!(welcome && welcome.style.display !== 'none');
7360
+ }
7361
+
7362
+ function isSessionsOverviewActive() {
7363
+ return state.activeTab === SESSIONS_OVERVIEW_TAB_ID && standupIsVisible();
7364
+ }
7365
+
7366
+ function showStandupDashboard(opts) {
7367
+ opts = opts || {};
7368
+ if (!opts.skipHash) {
7369
+ history.replaceState(null, '', location.pathname + location.search);
7370
+ }
7371
+ if (!opts.skipPersist) {
7372
+ state._savedActiveNav = 'sessions';
7373
+ savePref('active_nav', 'sessions');
7374
+ }
7375
+ for (const [, s] of state.sessions) {
7376
+ if (s.container) s.container.classList.remove('active');
7377
+ }
7378
+ ['rules-panel', 'review-panel', 'codereview-panel', 'insights-panel', 'permissions-panel', 'prompts-panel', 'walle-panel'].forEach(id => {
7379
+ const el = document.getElementById(id);
7380
+ if (el) el.classList.remove('active');
7381
+ });
7382
+ ['models-panel', 'backups-panel', 'worktrees-panel', 'setup-panel'].forEach(id => {
7383
+ const el = document.getElementById(id);
7384
+ if (el) { el.classList.remove('active'); el.style.display = 'none'; }
7385
+ });
7386
+
7387
+ state.activeTab = SESSIONS_OVERVIEW_TAB_ID;
7388
+ _subscribeVisibleSessions();
7389
+ const welcome = document.getElementById('welcome');
7390
+ if (welcome) welcome.style.display = 'flex';
7391
+ const sidebar = document.getElementById('sidebar');
7392
+ if (sidebar && !state.sidebarManuallyHidden) {
7393
+ sidebar.classList.remove('collapsed');
7394
+ document.getElementById('sidebar-resize').style.display = '';
7395
+ }
7396
+ const tabbar = document.getElementById('tabbar');
7397
+ if (tabbar) tabbar.style.display = '';
7398
+ const queueBtn = document.getElementById('topbar-queue-btn');
7399
+ if (queueBtn) queueBtn.style.display = 'none';
7400
+ const queuePanel = document.getElementById('queue-panel');
7401
+ const queueResize = document.getElementById('queue-panel-resize');
7402
+ if (queuePanel) queuePanel.style.display = 'none';
7403
+ if (queueResize) queueResize.style.display = 'none';
7404
+ state.queuePanelOpen = false;
7405
+ if (typeof updateQueueBtnHighlight === 'function') updateQueueBtnHighlight();
7406
+
7407
+ syncNavPills('sessions');
7408
+ updateTopbarContext('sessions');
7409
+ renderTabs();
7410
+ renderSessionList();
7411
+ loadStandupDashboard({ silent: !!state.standup.data });
7412
+ }
7413
+
7414
+ function refreshStandupIfVisible() {
7415
+ if (standupIsVisible()) loadStandupDashboard({ silent: !!state.standup.data });
7416
+ }
7417
+
7418
+ let _standupRefreshTimer = null;
7419
+ function scheduleStandupRefresh(delayMs = 600) {
7420
+ if (!standupIsVisible()) return;
7421
+ if (_standupRefreshTimer) return;
7422
+ _standupRefreshTimer = setTimeout(() => {
7423
+ _standupRefreshTimer = null;
7424
+ refreshStandupIfVisible();
7425
+ }, delayMs);
7426
+ }
7427
+
7428
+ async function loadStandupDashboard(opts) {
7429
+ opts = opts || {};
7430
+ if (state.standup.loading) return;
7431
+ state.standup.loading = true;
7432
+ const loading = document.getElementById('standup-loading');
7433
+ const error = document.getElementById('standup-error');
7434
+ if (loading && (!state.standup.data || !opts.silent)) loading.style.display = '';
7435
+ if (error) { error.style.display = 'none'; error.textContent = ''; }
7436
+ try {
7437
+ const url = '/api/sessions/standup' + (opts.force ? '?force=1' : '');
7438
+ const resp = await fetch(url);
7439
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
7440
+ const data = await resp.json();
7441
+ state.standup.data = data;
7442
+ state.standup.lastLoadedAt = Date.now();
7443
+ renderStandupDashboard(data);
7444
+ } catch (e) {
7445
+ if (error) {
7446
+ error.textContent = 'Standup refresh failed: ' + (e.message || e);
7447
+ error.style.display = '';
7448
+ }
7449
+ } finally {
7450
+ state.standup.loading = false;
7451
+ if (loading) loading.style.display = 'none';
7452
+ }
7453
+ }
7454
+
7455
+ function renderStandupDashboard(data) {
7456
+ const countsEl = document.getElementById('standup-counts');
7457
+ const updatedEl = document.getElementById('standup-updated');
7458
+ const attentionEl = document.getElementById('standup-attention');
7459
+ const lanesEl = document.getElementById('standup-lanes');
7460
+ const emptyEl = document.getElementById('standup-empty');
7461
+ if (!countsEl || !lanesEl) return;
7462
+
7463
+ const counts = data.counts || {};
7464
+ countsEl.innerHTML = [
7465
+ ['total', 'Total'],
7466
+ ['needs_user', 'Needs User'],
7467
+ ['ready_review', 'Review'],
7468
+ ['running', 'Running'],
7469
+ ['continue_later', 'Later'],
7470
+ ].map(([key, label]) => (
7471
+ `<span class="standup-count"><strong>${standupEsc(counts[key] || 0)}</strong>${standupEsc(label)}</span>`
7472
+ )).join('');
7473
+ if (updatedEl) updatedEl.textContent = data.generatedAt ? `Updated ${timeAgo(data.generatedAt)}` : '';
7474
+
7475
+ const laneSessions = (data.lanes || []).flatMap(lane => lane.sessions || []);
7476
+ const sessions = (data.sessions && data.sessions.length) ? data.sessions : laneSessions;
7477
+ const attention = sessions.find(s => s.lane === 'needs_user');
7478
+ if (attentionEl) {
7479
+ if (attention) {
7480
+ attentionEl.classList.add('active');
7481
+ attentionEl.innerHTML = `
7482
+ <div class="standup-attention-main">
7483
+ <div class="standup-attention-title">${standupEsc(attention.actionLabel)}: ${standupEsc(attention.title)}</div>
7484
+ <div class="standup-attention-body">${standupEsc(attention.recommendation)}</div>
7485
+ </div>
7486
+ <button class="standup-action-btn primary" type="button" data-standup-action="open" data-session-id="${standupEsc(attention.id)}">Open</button>
7487
+ `;
7488
+ } else {
7489
+ attentionEl.classList.remove('active');
7490
+ attentionEl.innerHTML = '';
7491
+ }
7492
+ }
7493
+
7494
+ if (emptyEl) emptyEl.style.display = sessions.length ? 'none' : '';
7495
+ lanesEl.innerHTML = (data.lanes || []).map(renderStandupLane).join('');
7496
+ }
7497
+
7498
+ function renderStandupLane(lane) {
7499
+ const sessions = lane.sessions || [];
7500
+ const body = sessions.length
7501
+ ? sessions.map(renderStandupCard).join('')
7502
+ : '<div class="standup-card-text" style="padding:4px 2px;">Clear</div>';
7503
+ return `
7504
+ <section class="standup-lane" data-lane="${standupEsc(lane.id)}">
7505
+ <div class="standup-lane-header">
7506
+ <div class="standup-lane-title"><span class="standup-lane-dot"></span><span>${standupEsc(lane.title || STANDUP_LANE_LABELS[lane.id] || lane.id)}</span></div>
7507
+ <span class="standup-lane-count">${standupEsc(sessions.length)}</span>
7508
+ </div>
7509
+ <div class="standup-lane-body">${body}</div>
7510
+ </section>
7511
+ `;
7512
+ }
7513
+
7514
+ function standupPromptActionLabel(card) {
7515
+ const actionKind = card && card.actionKind;
7516
+ if (actionKind === 'approval_needed' || actionKind === 'needs_input') return 'Respond';
7517
+ if (actionKind === 'investigate' || actionKind === 'warning' || actionKind === 'resume') return 'Add Prompt';
7518
+ return '';
6765
7519
  }
6766
7520
 
6767
- function updateTopbarContext(activeId) {
6768
- const ctxBtns = document.getElementById('topbar-context-btns');
6769
- const newSessionBtn = document.getElementById('topbar-new-session-btn');
6770
- const divider = document.getElementById('topbar-divider');
6771
- if (!ctxBtns) return;
7521
+ function renderStandupCard(card) {
7522
+ const subtitle = [card.agent, card.model || card.provider, card.branch].filter(Boolean).join(' / ');
7523
+ const progress = card.progress || card.intent || '';
7524
+ const canReview = card.capabilities && card.capabilities.review;
7525
+ const promptActionLabel = standupPromptActionLabel(card);
7526
+ const chips = (card.evidence || []).map(item => `<span class="standup-chip" title="${standupEsc(item)}">${standupEsc(item)}</span>`).join('');
7527
+ return `
7528
+ <article class="standup-card">
7529
+ <div class="standup-card-top">
7530
+ <div style="min-width:0;">
7531
+ <div class="standup-card-title">${standupEsc(card.title || card.id)}</div>
7532
+ <div class="standup-card-subtitle" title="${standupEsc(subtitle)}">${standupEsc(subtitle || card.cwd || '')}</div>
7533
+ </div>
7534
+ <span class="standup-badge ${standupStatusClass(card.status)}">${standupEsc(card.status || 'unknown')}</span>
7535
+ </div>
7536
+ <div class="standup-card-text"><strong>${standupEsc(card.actionLabel || 'Next')}</strong> ${standupEsc(card.recommendation || '')}</div>
7537
+ ${progress ? `<div class="standup-card-text">${standupEsc(progress)}</div>` : ''}
7538
+ ${chips ? `<div class="standup-evidence">${chips}</div>` : ''}
7539
+ <div class="standup-card-actions">
7540
+ <button class="standup-action-btn primary" type="button" data-standup-action="open" data-session-id="${standupEsc(card.id)}">Open</button>
7541
+ ${canReview ? `<button class="standup-action-btn" type="button" data-standup-action="review" data-session-id="${standupEsc(card.id)}">Review</button>` : ''}
7542
+ ${promptActionLabel ? `<button class="standup-action-btn" type="button" data-standup-action="instruct" data-session-id="${standupEsc(card.id)}" title="Open Queue Builder for this session">${standupEsc(promptActionLabel)}</button>` : ''}
7543
+ </div>
7544
+ </article>
7545
+ `;
7546
+ }
6772
7547
 
6773
- if (activeId === 'prompts') {
6774
- ctxBtns.innerHTML = `
6775
- <button class="topbar-util-btn" onclick="PE.showView('conversations')" title="Browse prompt conversations">Conversations</button>
6776
- `;
6777
- divider.style.display = '';
6778
- newSessionBtn.innerHTML = '+ New Prompt';
6779
- newSessionBtn.onclick = () => PE.createNewPrompt();
6780
- } else {
6781
- ctxBtns.innerHTML = '';
6782
- divider.style.display = 'none';
6783
- newSessionBtn.innerHTML = '+ New Session';
6784
- newSessionBtn.onclick = () => showNewSessionModal();
7548
+ function standupHandleDashboardClick(event) {
7549
+ const btn = event.target.closest('[data-standup-action]');
7550
+ if (!btn) return;
7551
+ const action = btn.dataset.standupAction;
7552
+ const sessionId = btn.dataset.sessionId;
7553
+ if (action === 'refresh') {
7554
+ loadStandupDashboard({ force: true });
7555
+ } else if (action === 'open') {
7556
+ standupOpenSession(sessionId);
7557
+ } else if (action === 'review') {
7558
+ standupReviewSession(sessionId);
7559
+ } else if (action === 'instruct') {
7560
+ standupInstructSession(sessionId);
7561
+ }
7562
+ }
7563
+
7564
+ function standupOpenSession(sessionId) {
7565
+ if (!sessionId || !state.sessions.has(sessionId)) {
7566
+ toast('Session is no longer active', { type: 'warning' });
7567
+ loadStandupDashboard({ force: true });
7568
+ return false;
6785
7569
  }
7570
+ activateTab(sessionId);
7571
+ return true;
7572
+ }
7573
+
7574
+ function standupReviewSession(sessionId) {
7575
+ if (!standupOpenSession(sessionId)) return;
7576
+ setTimeout(() => openSessionReview(sessionId), 0);
7577
+ }
7578
+
7579
+ function standupInstructSession(sessionId) {
7580
+ if (!standupOpenSession(sessionId)) return;
7581
+ setTimeout(() => {
7582
+ if (typeof toggleQueuePanel === 'function') {
7583
+ if (!state.queuePanelOpen) toggleQueuePanel();
7584
+ else refreshQpSessionList();
7585
+ const sel = document.getElementById('qp-session-select');
7586
+ if (sel) {
7587
+ sel.value = sessionId;
7588
+ if (typeof loadQpDraftForSession === 'function') loadQpDraftForSession(sessionId);
7589
+ }
7590
+ const input = document.getElementById('qp-inline-input');
7591
+ if (input) input.focus();
7592
+ } else {
7593
+ focusTerminalIfSafe(sessionId, { force: true });
7594
+ }
7595
+ }, 80);
6786
7596
  }
6787
7597
 
6788
7598
  let _prevNav = null; // track previous nav section for Alt+Tab swap
@@ -6802,23 +7612,45 @@ function _closeNavMoreOutside(e) {
6802
7612
  if (!document.getElementById('nav-more-wrap').contains(e.target)) closeNavMore();
6803
7613
  }
6804
7614
 
7615
+ function showSessionsWorkspace() {
7616
+ const candidates = [
7617
+ state.activeTab,
7618
+ state.lastActiveWorkSessionId,
7619
+ state._savedActiveSession,
7620
+ ].filter(Boolean);
7621
+ for (const id of candidates) {
7622
+ if (state.sessions.has(id)) {
7623
+ activateTab(id);
7624
+ return true;
7625
+ }
7626
+ }
7627
+ const fromTabs = state.tabOrder.slice().reverse().find(id => state.sessions.has(id));
7628
+ if (fromTabs) {
7629
+ activateTab(fromTabs);
7630
+ return true;
7631
+ }
7632
+ showStandupDashboard();
7633
+ return false;
7634
+ }
7635
+
6805
7636
  function navTo(target, opts) {
6806
7637
  // Stop dictation when switching tabs (dictation follows focus)
6807
7638
  if (typeof LI !== 'undefined' && LI.isRecording()) LI.stopRecording();
6808
7639
  // Track previous nav for Alt+Tab toggle
6809
7640
  const currentNav = document.querySelector('.nav-pill.active')?.dataset?.nav || 'sessions';
6810
- if (target !== currentNav) _prevNav = currentNav;
7641
+ const effectiveTarget = target === 'command' ? 'sessions' : target;
7642
+ if (effectiveTarget !== currentNav) _prevNav = currentNav;
6811
7643
  // Update URL hash
6812
7644
  if (!opts || !opts.skipHash) {
6813
- if (target === 'sessions') {
7645
+ if (effectiveTarget === 'sessions') {
6814
7646
  history.replaceState(null, '', location.pathname + location.search);
6815
7647
  } else {
6816
- history.replaceState(null, '', location.pathname + location.search + '#' + target);
7648
+ history.replaceState(null, '', location.pathname + location.search + '#' + effectiveTarget);
6817
7649
  }
6818
7650
  }
6819
7651
  // Persist active nav target so refresh restores it
6820
7652
  if (!opts || !opts.skipPersist) {
6821
- savePref('active_nav', target);
7653
+ savePref('active_nav', effectiveTarget);
6822
7654
  }
6823
7655
  // Save per-tab deep state when navigating away
6824
7656
  if (state.activeTab === 'prompts' && target !== 'prompts' && typeof PE !== 'undefined' && PE.state.currentPromptId) {
@@ -6831,40 +7663,12 @@ function navTo(target, opts) {
6831
7663
  // overwrite the correct value (both are fire-and-forget async PUTs).
6832
7664
  }
6833
7665
  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
- }
7666
+ showSessionsWorkspace();
7667
+ } else if (target === 'command') {
7668
+ // Backward-compatible alias for old #command links. The surface now lives as
7669
+ // the pinned Overview tab inside Sessions.
7670
+ state._forceSessionsOverview = true;
7671
+ showStandupDashboard({ skipHash: true, skipPersist: true });
6868
7672
  } else if (target === 'prompts') {
6869
7673
  openPromptEditor();
6870
7674
  } else if (target === 'rules') {
@@ -9417,7 +10221,7 @@ function _wtRunRecommendedAction(wt) {
9417
10221
  if (action.kind === 'open_session' || action.kind === 'review_dirty') return _wtOpenSessionFor(wt);
9418
10222
  if (action.kind === 'prune') return submitPruneGhosts();
9419
10223
  if (action.kind === 'recover_branch') return submitRecoverDetached(wt);
9420
- if (action.kind === 'sync_branch') return openSyncModal(wt);
10224
+ if (action.kind === 'sync_branch') return _wtOpenSyncOrExplain(wt);
9421
10225
  if (action.kind === 'finish_work') return _wtOpenMergeOrExplain(wt);
9422
10226
  if (action.kind === 'cleanup') return openDeleteModal(wt);
9423
10227
  if (action.kind === 'update_main' || action.kind === 'push_main' || action.kind === 'reconcile_main') {
@@ -9437,6 +10241,24 @@ function _wtRecommendedButton(wt) {
9437
10241
  return btn;
9438
10242
  }
9439
10243
 
10244
+ function _wtSyncBlockReason(wt) {
10245
+ if (!wt || wt.isMain) return '';
10246
+ if (wt.sessionId) return 'Close the active session before syncing from main.';
10247
+ if ((wt.dirtyFiles || 0) > 0) return 'Commit or stash dirty files before syncing from main.';
10248
+ if (!wt.branch || wt.branch === 'HEAD' || wt.state === 'detached') return 'Recover this worktree onto a branch before syncing from main.';
10249
+ if (wt.isGhost || wt.state === 'ghost') return 'Prune or recover this ghost worktree before syncing from main.';
10250
+ return '';
10251
+ }
10252
+
10253
+ function _wtOpenSyncOrExplain(wt) {
10254
+ var reason = _wtSyncBlockReason(wt);
10255
+ if (reason) {
10256
+ toast(reason, { type: 'warning', title: 'Sync unavailable', duration: 7000 });
10257
+ return;
10258
+ }
10259
+ openSyncModal(wt);
10260
+ }
10261
+
9440
10262
  function _wtMetric(label, value, tone) {
9441
10263
  var chip = document.createElement('span');
9442
10264
  var color = tone === 'good' ? '#9ece6a' : tone === 'warn' ? '#e0af68' : tone === 'bad' ? '#f7768e' : 'var(--fg-dim,#a9b1d6)';
@@ -9467,10 +10289,27 @@ function _wtSyncAllEligible(wts) {
9467
10289
  function _wtWorktreesListUrl(token) {
9468
10290
  var params = new URLSearchParams();
9469
10291
  params.set('token', token || '');
10292
+ var cwd = _wtCwdForRequest();
10293
+ if (cwd) params.set('cwd', cwd);
9470
10294
  params.set('_wt_refresh', String(Date.now()) + '-' + (++_wtRefreshSeq));
9471
10295
  return '/api/worktrees?' + params.toString();
9472
10296
  }
9473
10297
 
10298
+ function _wtCwdForRequest() {
10299
+ var cwd = '';
10300
+ try {
10301
+ if (currentProjectFilter) cwd = currentProjectFilter;
10302
+ } catch (_) {}
10303
+ if (!cwd) {
10304
+ try { cwd = getLastSessionCwd(); } catch (_) {}
10305
+ }
10306
+ if (!cwd && _wtCache && _wtCache.repoRoot) cwd = _wtCache.repoRoot;
10307
+ if (cwd) {
10308
+ try { cwd = _stripWorktreePath(cwd); } catch (_) {}
10309
+ }
10310
+ return cwd || '';
10311
+ }
10312
+
9474
10313
  function _wtSkippedReasonSummary(skippedBranches) {
9475
10314
  var counts = {};
9476
10315
  var rows = Array.isArray(skippedBranches) ? skippedBranches : [];
@@ -9693,9 +10532,14 @@ function _wtRenderCard(frag, wt) {
9693
10532
  pill.title = 'Click to merge into main';
9694
10533
  pill.onclick = function() { _wtOpenMergeOrExplain(wt); };
9695
10534
  } 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); };
10535
+ var pillSyncBlockReason = _wtSyncBlockReason(wt);
10536
+ if (pillSyncBlockReason) {
10537
+ pill.title = pillSyncBlockReason;
10538
+ } else {
10539
+ pill.style.cursor = 'pointer';
10540
+ pill.title = 'Click to sync from main';
10541
+ pill.onclick = function() { openSyncModal(wt); };
10542
+ }
9699
10543
  } else if (wt.state === 'ghost') {
9700
10544
  pill.style.cursor = 'pointer';
9701
10545
  pill.title = 'Click to prune all ghosts';
@@ -9765,12 +10609,20 @@ function _wtRenderCard(frag, wt) {
9765
10609
  }
9766
10610
 
9767
10611
  if ((wt.state === 'behind' || wt.state === 'diverged') && (!wt.recommendedAction || wt.recommendedAction.kind !== 'sync_branch')) {
10612
+ var syncBlockReason = _wtSyncBlockReason(wt);
9768
10613
  var syncBtn = document.createElement('button');
9769
10614
  syncBtn.className = 'btn';
9770
10615
  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); };
10616
+ if (syncBlockReason) {
10617
+ syncBtn.textContent = 'Sync blocked';
10618
+ syncBtn.setAttribute('aria-disabled', 'true');
10619
+ syncBtn.title = syncBlockReason;
10620
+ syncBtn.onclick = function() { _wtOpenSyncOrExplain(wt); };
10621
+ } else {
10622
+ syncBtn.textContent = '↓ Sync';
10623
+ syncBtn.title = 'Pull main into this worktree';
10624
+ syncBtn.onclick = function() { openSyncModal(wt); };
10625
+ }
9774
10626
  actions.appendChild(syncBtn);
9775
10627
  }
9776
10628
 
@@ -9821,7 +10673,10 @@ function _wtRenderCard(frag, wt) {
9821
10673
 
9822
10674
  var summaryEl = document.createElement('div');
9823
10675
  summaryEl.style.cssText = 'font-size:12px;color:' + stateInfo.color + ';margin:2px 0 6px;font-weight:500;';
9824
- summaryEl.textContent = wt.summary || '';
10676
+ var summaryText = wt.summary || '';
10677
+ var summarySyncBlockReason = (wt.state === 'behind' || wt.state === 'diverged') ? _wtSyncBlockReason(wt) : '';
10678
+ if (summarySyncBlockReason) summaryText += (summaryText ? ' - ' : '') + summarySyncBlockReason;
10679
+ summaryEl.textContent = summaryText;
9825
10680
  card.appendChild(summaryEl);
9826
10681
 
9827
10682
  var metrics = document.createElement('div');
@@ -9863,6 +10718,11 @@ function _wtRenderCard(frag, wt) {
9863
10718
  }
9864
10719
 
9865
10720
  function openSyncModal(wt) {
10721
+ var syncBlockReason = _wtSyncBlockReason(wt);
10722
+ if (syncBlockReason) {
10723
+ toast(syncBlockReason, { type: 'warning', title: 'Sync unavailable', duration: 7000 });
10724
+ return;
10725
+ }
9866
10726
  _wtModalState = { branch: wt.branch, name: wt.branch, mode: 'sync', cwd: _wtCache.repoRoot || '' };
9867
10727
  var sum = document.getElementById('wt-sync-summary');
9868
10728
  if (sum) sum.textContent = 'Pull main into "' + wt.branch + '" (' + wt.behind + ' commits behind).';
@@ -9947,7 +10807,10 @@ async function submitPruneGhosts() {
9947
10807
  if (!confirm('Remove all ghost worktrees? This cleans up corrupt or missing worktree entries from git.')) return;
9948
10808
  try {
9949
10809
  var token = (state && state.token) ? state.token : '';
9950
- var r = await fetch('/api/worktrees/prune?token=' + encodeURIComponent(token), { method: 'POST' });
10810
+ var params = new URLSearchParams();
10811
+ params.set('token', token);
10812
+ if (_wtCache.repoRoot) params.set('cwd', _wtCache.repoRoot);
10813
+ var r = await fetch('/api/worktrees/prune?' + params.toString(), { method: 'POST' });
9951
10814
  var d = await r.json();
9952
10815
  if (!r.ok || d.error) throw new Error(d.error || ('HTTP ' + r.status));
9953
10816
  var n = (d.prunedPaths || []).length;
@@ -10460,7 +11323,17 @@ async function moveToWorktree(sessionId) {
10460
11323
  function onCreated(msg) {
10461
11324
  if (msg.sessionType === 'walle') {
10462
11325
  const { id, label, cwd } = msg;
10463
- const s = { meta: { label: label, cwd: cwd, type: 'walle' }, walleState: null };
11326
+ const s = {
11327
+ meta: {
11328
+ label: label,
11329
+ cwd: cwd,
11330
+ type: 'walle',
11331
+ agentType: 'walle',
11332
+ model_id: msg.model_id || null,
11333
+ model_provider: msg.model_provider || null,
11334
+ },
11335
+ walleState: null,
11336
+ };
10464
11337
  const container = document.createElement('div');
10465
11338
  container.className = 'walle-session';
10466
11339
  container.id = 'walle-session-' + id;
@@ -10484,7 +11357,7 @@ function onCreated(msg) {
10484
11357
  if (s && !s.meta) {
10485
11358
  s.meta = {
10486
11359
  id,
10487
- label,
11360
+ label: cleanSessionLabelForBranch(label, msg.branch || ''),
10488
11361
  pid,
10489
11362
  cwd,
10490
11363
  cmd: msg.cmd || '',
@@ -10492,6 +11365,7 @@ function onCreated(msg) {
10492
11365
  model_id: msg.model_id || null,
10493
11366
  model_provider: msg.model_provider || null,
10494
11367
  branch: msg.branch || null,
11368
+ userRenamed: !!msg.userRenamed,
10495
11369
  agentType: msg.agentType || null,
10496
11370
  agentCapabilities: msg.agentCapabilities || null,
10497
11371
  claudeSessionId: msg.claudeSessionId || null,
@@ -10647,6 +11521,7 @@ function chunkedWrite(s, data, onDone) {
10647
11521
  s.writer.scheduled = true;
10648
11522
  requestAnimationFrame(() => {
10649
11523
  const next = s.writer.queue;
11524
+ _clearStaleTerminalScrollLock(s);
10650
11525
  const f = s.writer.followMode;
10651
11526
  s.writer.queue = '';
10652
11527
  s.writer.scheduled = false;
@@ -10673,7 +11548,7 @@ function onOutput(msg) {
10673
11548
  return;
10674
11549
  }
10675
11550
 
10676
- let data = msg.data;
11551
+ let data = _normalizeCodexPromptBackground(s, msg.data);
10677
11552
 
10678
11553
  // Direct-write bypass for keystroke echoes (ECHO_DIRECT_WRITE).
10679
11554
  // If this is small output arriving shortly after input, and no chunking is in progress,
@@ -10693,8 +11568,11 @@ function onOutput(msg) {
10693
11568
  // Keep local status activity aligned with the server's provider detectors.
10694
11569
  // Claude/Codex idle redraws can leak printable "Running"/"Working" fragments;
10695
11570
  // 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();
11571
+ const suppressUiRefreshOutput = _shouldSuppressClientUiRefreshOutput(s, data);
11572
+ if (_isClientActiveOutput(s, data) && !suppressUiRefreshOutput && !(s._waitingForInput && _isClientCodexStatusOnlyOutput(s, data))) {
11573
+ const now = Date.now();
11574
+ s._lastOutputAt = now;
11575
+ if (!s._waitingForInput) _markClientCodexRunningEvidence(s, now, now);
10698
11576
  // Don't clear _waitingForInput here — TUI redraws still leak visible chars
10699
11577
  // (spinner glyphs, prompt text). Only server-sent 'session-resumed' should clear it.
10700
11578
  }
@@ -10725,6 +11603,7 @@ function onOutput(msg) {
10725
11603
  s.writer.scheduled = true;
10726
11604
  requestAnimationFrame(() => {
10727
11605
  const batch = s.writer.queue;
11606
+ _clearStaleTerminalScrollLock(s);
10728
11607
  const follow = s.writer.followMode;
10729
11608
  s.writer.queue = '';
10730
11609
  s.writer.scheduled = false;
@@ -10787,19 +11666,8 @@ setInterval(() => {
10787
11666
  if (!id) return;
10788
11667
  const s = state.sessions.get(id);
10789
11668
  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
- }
11669
+ const unlockedAtBottom = _clearStaleTerminalScrollLock(s);
11670
+ if (unlockedAtBottom) _ensureScrolledToBottom(s);
10803
11671
 
10804
11672
  // Fix 3: queue has data, nothing is draining it — force flush
10805
11673
  if (s.writer.queue && s.writer.queue.length > 0 && !s.writer.scheduled && !s.writer._chunking) {
@@ -10847,7 +11715,68 @@ function _terminalFollowViewportTarget(s) {
10847
11715
  const blankTailRows = screenEnd - anchor;
10848
11716
  const threshold = Math.max(6, Math.floor(rows * 0.20));
10849
11717
  if (blankTailRows < threshold) return baseY;
10850
- return Math.max(0, Math.min(baseY, anchor - rows + 1));
11718
+ const target = Math.max(0, Math.min(baseY, anchor - rows + 1));
11719
+ return _codexViewportHasPromptGaps(s, target) ? baseY : target;
11720
+ }
11721
+
11722
+ function _codexViewportHasPromptGaps(s, start) {
11723
+ if (!s || !s.term) return false;
11724
+ const buf = s.term.buffer.active;
11725
+ const rows = s.term.rows || 0;
11726
+ if (rows <= 0) return false;
11727
+ const first = Math.max(0, start || 0);
11728
+ const last = first + rows - 1;
11729
+ let promptCount = 0;
11730
+ let maxBlankRun = 0;
11731
+ let currentBlankRun = 0;
11732
+ let seenText = false;
11733
+ for (let row = first; row <= last; row++) {
11734
+ const line = buf.getLine(row);
11735
+ const text = line ? line.translateToString(true) : '';
11736
+ const trimmed = text.trim();
11737
+ if (trimmed.startsWith('›')) promptCount += 1;
11738
+ if (trimmed) {
11739
+ if (seenText) maxBlankRun = Math.max(maxBlankRun, currentBlankRun);
11740
+ seenText = true;
11741
+ currentBlankRun = 0;
11742
+ } else if (seenText) {
11743
+ currentBlankRun += 1;
11744
+ }
11745
+ }
11746
+ const threshold = Math.max(8, Math.floor(rows * 0.18));
11747
+ return promptCount >= 2 && maxBlankRun >= threshold;
11748
+ }
11749
+
11750
+ function _alignXtermHelperTextareaToViewport(s) {
11751
+ if (!s || !s.term || !s.container) return;
11752
+ if (_clientAgentTypeForSession(s) !== 'codex') return;
11753
+ const buf = s.term.buffer.active;
11754
+ const baseY = buf.baseY || 0;
11755
+ const viewportY = buf.viewportY || 0;
11756
+ const visualRow = baseY + (buf.cursorY || 0) - viewportY;
11757
+ if (visualRow < 0 || visualRow >= (s.term.rows || 0)) return;
11758
+ const ta = s.container.querySelector('.xterm-helper-textarea');
11759
+ if (!ta) return;
11760
+ let cellHeight = 0;
11761
+ let cellWidth = 0;
11762
+ try {
11763
+ const cell = s.term._core?._renderService?.dimensions?.css?.cell || {};
11764
+ cellHeight = cell.height || 0;
11765
+ cellWidth = cell.width || 0;
11766
+ } catch {}
11767
+ if (!cellHeight || !cellWidth) {
11768
+ const screen = s.container.querySelector('.xterm-screen');
11769
+ if (screen) {
11770
+ const rect = screen.getBoundingClientRect();
11771
+ cellHeight = cellHeight || (s.term.rows ? rect.height / s.term.rows : 0);
11772
+ cellWidth = cellWidth || (s.term.cols ? rect.width / s.term.cols : 0);
11773
+ }
11774
+ }
11775
+ if (!cellHeight || !cellWidth) return;
11776
+ ta.style.left = ((buf.cursorX || 0) * cellWidth) + 'px';
11777
+ ta.style.top = (visualRow * cellHeight) + 'px';
11778
+ ta.style.height = cellHeight + 'px';
11779
+ ta.style.lineHeight = cellHeight + 'px';
10851
11780
  }
10852
11781
 
10853
11782
  function _withProgrammaticTerminalScroll(s, fn) {
@@ -10874,6 +11803,7 @@ function _scrollTerminalToFollowBottom(s) {
10874
11803
  try { vp.scrollTop = vp.scrollHeight; } catch {}
10875
11804
  }
10876
11805
  });
11806
+ requestAnimationFrame(() => _alignXtermHelperTextareaToViewport(s));
10877
11807
  }
10878
11808
 
10879
11809
  function _isAtTerminalFollowBottom(s) {
@@ -10885,10 +11815,19 @@ function _isAtTerminalFollowBottom(s) {
10885
11815
  return Math.abs(viewportY - target) <= 1 || Math.abs(viewportY - baseY) <= 1;
10886
11816
  }
10887
11817
 
11818
+ function _clearStaleTerminalScrollLock(s) {
11819
+ if (!s || !s.term || !s.writer || s.writer._chunking) return false;
11820
+ if ((s.writer._userScrollLocked || !s.writer.followMode) && _isAtTerminalFollowBottom(s)) {
11821
+ s.writer._userScrollLocked = false;
11822
+ s.writer.followMode = true;
11823
+ return true;
11824
+ }
11825
+ return false;
11826
+ }
11827
+
10888
11828
  function _findCodexInternalBlankGap(s) {
10889
11829
  if (!s || !s.term) return null;
10890
11830
  if (_clientAgentTypeForSession(s) !== 'codex') return null;
10891
- if (s.writer && s.writer._userScrollLocked) return null;
10892
11831
  const buf = s.term.buffer.active;
10893
11832
  const rows = s.term.rows || 0;
10894
11833
  const baseY = buf.baseY || 0;
@@ -10896,25 +11835,30 @@ function _findCodexInternalBlankGap(s) {
10896
11835
 
10897
11836
  const meaningful = [];
10898
11837
  let promptAbs = -1;
10899
- let hasCompletedTurnOutputAfterPrompt = false;
11838
+ let promptCount = 0;
11839
+ let hasNonStatusAfterPrompt = false;
10900
11840
  for (let offset = 0; offset < rows; offset++) {
10901
11841
  const abs = baseY + offset;
10902
11842
  const line = buf.getLine(abs);
10903
11843
  const text = line ? line.translateToString(true) : '';
10904
- if (text.trim().startsWith('›')) promptAbs = abs;
11844
+ if (text.trim().startsWith('›')) {
11845
+ promptAbs = abs;
11846
+ promptCount += 1;
11847
+ }
10905
11848
  if (text.trim()) meaningful.push(abs);
10906
11849
  }
10907
11850
  if (promptAbs < 0 || meaningful.length < 2) return null;
11851
+ if (_codexHasActiveSkillPickerAfterPrompt(s, meaningful, promptAbs)) return null;
10908
11852
  for (const abs of meaningful) {
10909
11853
  if (abs <= promptAbs) continue;
10910
11854
  const line = buf.getLine(abs);
10911
11855
  const text = line ? line.translateToString(true).trim() : '';
10912
11856
  if (!text) continue;
10913
11857
  if (/^gpt-[\w.-]+\s+/i.test(text) || /\b(x?high|medium|low)\b.*\s-\s/.test(text)) continue;
10914
- hasCompletedTurnOutputAfterPrompt = true;
11858
+ hasNonStatusAfterPrompt = true;
10915
11859
  break;
10916
11860
  }
10917
- if (!hasCompletedTurnOutputAfterPrompt) return null;
11861
+ if (!hasNonStatusAfterPrompt && promptCount < 2) return null;
10918
11862
 
10919
11863
  let best = null;
10920
11864
  for (let i = 1; i < meaningful.length; i++) {
@@ -10932,6 +11876,22 @@ function _findCodexInternalBlankGap(s) {
10932
11876
  return { startAbs: best.startAbs, deleteRows };
10933
11877
  }
10934
11878
 
11879
+ function _codexHasActiveSkillPickerAfterPrompt(s, meaningfulRows, promptAbs) {
11880
+ if (!s || !s.term || !Array.isArray(meaningfulRows)) return false;
11881
+ const buf = s.term.buffer.active;
11882
+ let sawSkill = false;
11883
+ let sawHint = false;
11884
+ for (const abs of meaningfulRows) {
11885
+ if (abs <= promptAbs) continue;
11886
+ const line = buf.getLine(abs);
11887
+ const text = line ? line.translateToString(true).trim() : '';
11888
+ if (!text) continue;
11889
+ if (/\[Skill\]/.test(text)) sawSkill = true;
11890
+ if (/^press\s+enter\s+to\s+insert(?:\s+or\s+esc\s+to\s+close)?$/i.test(text)) sawHint = true;
11891
+ }
11892
+ return sawSkill && sawHint;
11893
+ }
11894
+
10935
11895
  function _compactCodexInternalBlankGap(s, onDone) {
10936
11896
  if (!s || !s.term || s._codexBlankGapCompacting) return false;
10937
11897
  const gap = _findCodexInternalBlankGap(s);
@@ -10970,17 +11930,22 @@ function _compactCodexInternalBlankGap(s, onDone) {
10970
11930
  // most terminals this is xterm's literal bottom. For Codex restored TUI
10971
11931
  // screens, it is the last meaningful row before a large blank tail.
10972
11932
  //
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.
11933
+ // Skip if the user has manually scrolled away from the active bottom — we don't
11934
+ // want to yank them back while they are reading scrollback.
10975
11935
  function _ensureScrolledToBottom(s) {
10976
11936
  if (!s || !s.term) return;
10977
- if (s.writer && s.writer._userScrollLocked) return;
11937
+ _clearStaleTerminalScrollLock(s);
11938
+ // Repair a corrupted Codex current screen even if follow mode is locked.
11939
+ // The scroll lock should prevent viewport jumps, not preserve synthetic blank
11940
+ // rows left by TUI clear/redraw races.
10978
11941
  if (_compactCodexInternalBlankGap(s, () => _ensureScrolledToBottom(s))) return;
11942
+ if (s.writer && s.writer._userScrollLocked) return;
10979
11943
  _scrollTerminalToFollowBottom(s);
10980
11944
  requestAnimationFrame(() => {
10981
11945
  if (!s.term) return;
10982
11946
  if (s.writer && s.writer._userScrollLocked) return;
10983
11947
  _scrollTerminalToFollowBottom(s);
11948
+ requestAnimationFrame(() => _alignXtermHelperTextareaToViewport(s));
10984
11949
  });
10985
11950
  }
10986
11951
 
@@ -11016,8 +11981,69 @@ function _snapshotDimsMatchTerm(s, cols, rows) {
11016
11981
  return c === s.term.cols && r === s.term.rows;
11017
11982
  }
11018
11983
 
11984
+ function _terminalPlainTextStats(text) {
11985
+ const raw = String(text || '');
11986
+ const lines = raw.split(/\r\n|\n|\r/);
11987
+ let nonBlank = 0;
11988
+ let printableChars = 0;
11989
+ let first = '';
11990
+ let last = '';
11991
+ for (const line of lines) {
11992
+ const trimmed = String(line || '').trim();
11993
+ if (!trimmed) continue;
11994
+ nonBlank++;
11995
+ printableChars += trimmed.length;
11996
+ if (!first) first = trimmed;
11997
+ last = trimmed;
11998
+ }
11999
+ return { nonBlank, printableChars, first, last };
12000
+ }
12001
+
12002
+ function _terminalAnsiTextStats(data) {
12003
+ const text = String(data || '')
12004
+ .replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '')
12005
+ .replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, (seq) => {
12006
+ const final = seq[seq.length - 1];
12007
+ return (final === 'H' || final === 'f') ? '\n' : '';
12008
+ })
12009
+ .replace(/\x1bc/g, '\n')
12010
+ .replace(/\r\n/g, '\n')
12011
+ .replace(/\r/g, '\n')
12012
+ .replace(/[\x00-\x08\x0b-\x1f\x7f]/g, '');
12013
+ return _terminalPlainTextStats(text);
12014
+ }
12015
+
12016
+ function _snapshotLooksShorterThanSaved(s, data) {
12017
+ if (!s || !s._savedScrollbackText || !data) return false;
12018
+ const saved = s._savedScrollbackStats || _terminalPlainTextStats(s._savedScrollbackText);
12019
+ if (!saved || saved.nonBlank < 20 || saved.printableChars < 500) return false;
12020
+ // The tab-away capture is only a guard for the restore immediately after
12021
+ // disposal. Do not let an old local copy override future authoritative
12022
+ // snapshots after the session has moved on.
12023
+ if (s._savedScrollbackCapturedAt && Date.now() - s._savedScrollbackCapturedAt > 5 * 60 * 1000) return false;
12024
+ const snapshot = _terminalAnsiTextStats(data);
12025
+ if (!snapshot) return false;
12026
+ const lostManyLines = snapshot.nonBlank < Math.floor(saved.nonBlank * 0.65);
12027
+ const lostManyChars = snapshot.printableChars < Math.floor(saved.printableChars * 0.75);
12028
+ return lostManyLines && lostManyChars && (saved.nonBlank - snapshot.nonBlank) >= 12;
12029
+ }
12030
+
12031
+ function _restoreSavedScrollbackText(s, onDone) {
12032
+ if (!s || !s.term || !s._savedScrollbackText) { if (onDone) onDone(); return; }
12033
+ try { s.term.clearTextureAtlas(); } catch {}
12034
+ try {
12035
+ if (s.writer) {
12036
+ s.writer.queue = '';
12037
+ s.writer._snapshotGen = (s.writer._snapshotGen || 0) + 1;
12038
+ }
12039
+ } catch {}
12040
+ const text = s._savedScrollbackText + '\r\n';
12041
+ s.term.write(TERMINAL_FULL_RESET + text, onDone);
12042
+ }
12043
+
11019
12044
  function _restoreSnapshotData(s, data, onDone) {
11020
12045
  if (!s || !s.term || !data) { if (onDone) onDone(); return; }
12046
+ data = _normalizeCodexPromptBackground(s, data);
11021
12047
  try { s.term.clearTextureAtlas(); } catch {}
11022
12048
  try {
11023
12049
  if (s.writer) {
@@ -11134,6 +12160,7 @@ function onSnapshot(msg) {
11134
12160
  const ptyRows = msg.ptyRows || msg.rows || localRows;
11135
12161
  const dimsMismatch = localCols !== ptyCols || localRows !== ptyRows;
11136
12162
  if (dimsMismatch) {
12163
+ _markClientUiRefreshOutputSuppression(s);
11137
12164
  send({ type: 'resize', id: msg.id, cols: localCols, rows: localRows });
11138
12165
  _showLoadingOverlay(s);
11139
12166
  if (!s._dimFixPending) {
@@ -11141,6 +12168,7 @@ function onSnapshot(msg) {
11141
12168
  setTimeout(() => {
11142
12169
  s._dimFixPending = false;
11143
12170
  if (state.activeTab === msg.id || isSessionVisibleInSplit(msg.id)) {
12171
+ _markClientUiRefreshOutputSuppression(s);
11144
12172
  send({ type: 'reflow', id: msg.id, cols: s.term.cols, rows: s.term.rows });
11145
12173
  }
11146
12174
  }, 500);
@@ -11166,7 +12194,11 @@ function onSnapshot(msg) {
11166
12194
  _forceTerminalPaint(s);
11167
12195
  scanPromptLines(msg.id);
11168
12196
  };
11169
- _restoreSnapshotData(s, msg.data, snapshotDone);
12197
+ if (_snapshotLooksShorterThanSaved(s, msg.data)) {
12198
+ _restoreSavedScrollbackText(s, snapshotDone);
12199
+ } else {
12200
+ _restoreSnapshotData(s, msg.data, snapshotDone);
12201
+ }
11170
12202
  }
11171
12203
 
11172
12204
  // --- Loading overlay for tab-switch restore ---
@@ -11259,6 +12291,7 @@ async function onSessionsList(msg) {
11259
12291
 
11260
12292
  // Add tabs for sessions we don't have terminals for (reconnect scenario)
11261
12293
  for (const sess of msg.sessions) {
12294
+ if (sess && sess.label) sess.label = cleanSessionLabelForBranch(sess.label, sess.branch || '');
11262
12295
  if (!state.sessions.has(sess.id)) {
11263
12296
  if (sess.type === 'walle') {
11264
12297
  const container = document.createElement('div');
@@ -11284,6 +12317,11 @@ async function onSessionsList(msg) {
11284
12317
  if (existing) {
11285
12318
  existing.meta = sess;
11286
12319
  if (sess.type === 'walle') existing.meta.type = 'walle';
12320
+ const liveStatus = normalizeLiveSessionStatus(sess.liveStatus);
12321
+ if (liveStatus) {
12322
+ existing._serverLiveStatus = liveStatus;
12323
+ existing._serverLiveStatusAt = SessionActivityUtils.parseTimeMs(sess.liveStatusAt) || Date.now();
12324
+ }
11287
12325
  }
11288
12326
  }
11289
12327
 
@@ -11325,6 +12363,7 @@ async function onSessionsList(msg) {
11325
12363
 
11326
12364
  // Refresh queue builder session list
11327
12365
  if (typeof refreshQpSessionList === 'function') refreshQpSessionList();
12366
+ if (typeof refreshStandupIfVisible === 'function') refreshStandupIfVisible();
11328
12367
 
11329
12368
  // Auto-activate from hash (only for active PTY sessions).
11330
12369
  // During post-restart, defer activation to onServerReady() which has the correct
@@ -11485,6 +12524,93 @@ function worktreeAttentionBadge(s) {
11485
12524
  return `<span class="worktree-attn-badge" title="${escHtml(title)}" aria-label="${escHtml(title)}">${parts.join('')}</span>`;
11486
12525
  }
11487
12526
 
12527
+ function escapeRegExpText(value) {
12528
+ return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
12529
+ }
12530
+
12531
+ function stripBranchFromSessionLabel(label, branch) {
12532
+ const text = String(label || '').replace(/\s+/g, ' ').trim();
12533
+ const branchText = String(branch || '').trim();
12534
+ if (!text || !branchText || branchText === 'main' || branchText === 'master') return text;
12535
+ const candidates = Array.from(new Set([
12536
+ branchText,
12537
+ branchText.length > 12 ? branchText.slice(0, 12) + '..' : branchText,
12538
+ branchText.length > 15 ? branchText.slice(0, 15) + '...' : branchText,
12539
+ ].filter(Boolean))).sort((a, b) => b.length - a.length);
12540
+ let cleaned = text;
12541
+ for (const candidate of candidates) {
12542
+ if (cleaned.toLowerCase() === candidate.toLowerCase()) {
12543
+ cleaned = '';
12544
+ break;
12545
+ }
12546
+ const re = new RegExp('(?:\\s*[\\u25ED\\u260D]\\s*' + escapeRegExpText(candidate) + '\\s*)+$', 'i');
12547
+ cleaned = cleaned.replace(re, '').trim();
12548
+ }
12549
+ return cleaned;
12550
+ }
12551
+
12552
+ function cleanSessionLabelForBranch(label, branch) {
12553
+ return stripBranchFromSessionLabel(label, branch) || String(label || '').replace(/\s+/g, ' ').trim();
12554
+ }
12555
+
12556
+ function activeSessionFallbackLabel(s, id) {
12557
+ const meta = s?.meta || {};
12558
+ if (meta.type === 'walle') return 'Wall-E session';
12559
+ const cmd = String(meta.cmd || '').toLowerCase();
12560
+ if (cmd.includes('codex')) return 'Codex session';
12561
+ if (cmd.includes('gemini')) return 'Gemini session';
12562
+ if (cmd.includes('opencode') || cmd.includes('open-code')) return 'OpenCode session';
12563
+ if (cmd.includes('claude')) return 'Claude Code session';
12564
+ return id ? `Session ${String(id).slice(0, 8)}` : 'Session';
12565
+ }
12566
+
12567
+ function activeSessionHasUserRenamedLabel(s, id) {
12568
+ const meta = s?.meta || {};
12569
+ if (meta.userRenamed) return true;
12570
+ const agentId = meta.agentSessionId || meta.agentSessionToken || meta.claudeSessionId || '';
12571
+ if (typeof allRecentSessions === 'undefined' || !Array.isArray(allRecentSessions)) return false;
12572
+ return allRecentSessions.some(r => r && r.userRenamed && (
12573
+ r.sessionId === id ||
12574
+ r.provisionalId === id ||
12575
+ (agentId && (r.sessionId === agentId || r.agentSessionId === agentId))
12576
+ ));
12577
+ }
12578
+
12579
+ function activeSessionDisplayLabel(s, id) {
12580
+ const branch = s?.meta?.branch || '';
12581
+ const raw = String(s?.meta?.label || '').replace(/\s+/g, ' ').trim();
12582
+ const cleaned = stripBranchFromSessionLabel(raw, branch);
12583
+ if (activeSessionHasUserRenamedLabel(s, id) && raw) return cleaned || raw;
12584
+ return cleaned || activeSessionFallbackLabel(s, id);
12585
+ }
12586
+
12587
+ function updateTabTitleTooltips() {
12588
+ const labels = document.querySelectorAll('#tabbar-scroll .tab .tab-label');
12589
+ labels.forEach(label => {
12590
+ if (label.querySelector('input')) return;
12591
+ const tab = label.closest('.tab');
12592
+ if (tab) tab.classList.remove('tab-title-clipped');
12593
+ const fullTitle = (label.dataset.fullTitle || label.textContent || '').trim();
12594
+ if (!fullTitle) {
12595
+ label.removeAttribute('title');
12596
+ label.removeAttribute('aria-label');
12597
+ return;
12598
+ }
12599
+ let isClipped = label.scrollWidth > label.clientWidth + 1;
12600
+ if (isClipped && tab && tab.querySelector(':scope > .branch-badge')) {
12601
+ tab.classList.add('tab-title-clipped');
12602
+ isClipped = label.scrollWidth > label.clientWidth + 1;
12603
+ }
12604
+ if (isClipped) {
12605
+ label.title = fullTitle;
12606
+ label.setAttribute('aria-label', fullTitle);
12607
+ } else {
12608
+ label.removeAttribute('title');
12609
+ label.removeAttribute('aria-label');
12610
+ }
12611
+ });
12612
+ }
12613
+
11488
12614
  let _renderSessionListTimer = null;
11489
12615
  function renderSessionList(force) {
11490
12616
  if (!force) {
@@ -11541,7 +12667,8 @@ function renderSessionList(force) {
11541
12667
  }
11542
12668
  }
11543
12669
  const isActive = state.activeTab === id;
11544
- const label = s.meta?.label || id.slice(0, 8);
12670
+ const branchName = s.meta?.branch || '';
12671
+ const label = activeSessionDisplayLabel(s, id);
11545
12672
  const lastAct = SessionActivityUtils.sessionTouchedAtMs(s) || s.meta?.lastActivity || s.meta?.createdAt || 0;
11546
12673
  const idleMs = Date.now() - lastAct;
11547
12674
  const isStale = idleMs > 24 * 60 * 60 * 1000;
@@ -11563,7 +12690,6 @@ function renderSessionList(force) {
11563
12690
  }).map(p =>
11564
12691
  `<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
12692
  ).join('');
11566
- const branchName = s.meta?.branch || '';
11567
12693
  const branchBadge = branchName && branchName !== 'main' ? `<span class="branch-badge" title="Branch: ${escHtml(branchName)}">&#9741; ${escHtml(branchName.length > 15 ? branchName.slice(0, 15) + '...' : branchName)}</span>` : '';
11568
12694
  const worktreeBadge = worktreeAttentionBadge(s);
11569
12695
  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)">
@@ -11615,17 +12741,111 @@ function setActiveSort(mode) {
11615
12741
  // Primary source: SessionStream status (server-side, computed from JSONL events + PTY activity)
11616
12742
  // Fallback: local PTY signals (_lastOutputAt, _waitingForInput) for sessions not yet tracked
11617
12743
  const AUTHORITATIVE_STATUS_TTL_MS = 120000;
12744
+ const SERVER_LIVE_STATUS_TTL_MS = 10000;
12745
+ const CODEX_RUNNING_HOLD_MS = 15000;
12746
+ const UI_REFRESH_STATUS_ONLY_SUPPRESS_MS = 2500;
12747
+ function normalizeLiveSessionStatus(status) {
12748
+ const text = String(status || '').toLowerCase();
12749
+ if (!text) return '';
12750
+ if (text === 'busy' || text === 'active' || text === 'thinking') return 'running';
12751
+ if (text === 'waiting_input' || text === 'waiting-for-input') return 'waiting';
12752
+ if (['running', 'waiting', 'idle', 'exited'].includes(text)) return text;
12753
+ return '';
12754
+ }
12755
+
12756
+ function liveStatusResult(status) {
12757
+ const normalized = normalizeLiveSessionStatus(status);
12758
+ if (!normalized) return null;
12759
+ const statusMap = { running: 'Running', waiting: 'Waiting', idle: 'Idle', exited: 'Exited' };
12760
+ return { cls: normalized, text: statusMap[normalized] || 'Idle' };
12761
+ }
12762
+
12763
+ function _clientTimeMs(value, fallback) {
12764
+ if (value == null || value === '') return fallback || 0;
12765
+ if (typeof SessionActivityUtils !== 'undefined' && SessionActivityUtils.parseTimeMs) {
12766
+ const parsed = SessionActivityUtils.parseTimeMs(value);
12767
+ if (parsed) return parsed;
12768
+ }
12769
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
12770
+ const parsed = Date.parse(String(value));
12771
+ return Number.isFinite(parsed) ? parsed : (fallback || 0);
12772
+ }
12773
+
12774
+ function _isClientBlockingWaitingReason(reason) {
12775
+ return reason === 'approval' || reason === 'choice';
12776
+ }
12777
+
12778
+ function _isClientCodexSession(s) {
12779
+ return _clientAgentTypeForSession(s) === 'codex';
12780
+ }
12781
+
12782
+ function _markClientCodexRunningEvidence(s, eventTimestamp, now = Date.now()) {
12783
+ if (!_isClientCodexSession(s)) return;
12784
+ const eventAt = _clientTimeMs(eventTimestamp, now) || now;
12785
+ const previousEventAt = s._codexRunningEvidenceAt || 0;
12786
+ if (previousEventAt && eventAt <= previousEventAt) {
12787
+ s._codexRunningLastDetectionAt = Math.max(s._codexRunningLastDetectionAt || 0, now);
12788
+ return;
12789
+ }
12790
+ s._codexRunningEvidenceAt = Math.max(s._codexRunningEvidenceAt || 0, eventAt);
12791
+ s._codexRunningLastDetectionAt = Math.max(s._codexRunningLastDetectionAt || 0, now);
12792
+ s._codexRunningHoldUntil = Math.max(
12793
+ s._codexRunningHoldUntil || 0,
12794
+ now + CODEX_RUNNING_HOLD_MS
12795
+ );
12796
+ }
12797
+
12798
+ function _clientCodexRunningHoldResult(s, now = Date.now()) {
12799
+ if (!_isClientCodexSession(s)) return null;
12800
+ if (_isClientBlockingWaitingReason(s._waitingReason || '')) return null;
12801
+ const recentPromptTypingAt = s._waitingForInput && s.writer ? (s.writer._lastInputAt || 0) : 0;
12802
+ if (recentPromptTypingAt && (now - recentPromptTypingAt) < 3000) return null;
12803
+ const holdUntil = s._codexRunningHoldUntil || 0;
12804
+ if (holdUntil > now) return { cls: 'running', text: 'Running' };
12805
+ return null;
12806
+ }
12807
+
12808
+ function _markClientUiRefreshOutputSuppression(s) {
12809
+ if (!s) return;
12810
+ s._uiRefreshStatusOnlySuppressUntil = Date.now() + UI_REFRESH_STATUS_ONLY_SUPPRESS_MS;
12811
+ }
12812
+
12813
+ function _shouldSuppressClientUiRefreshOutput(s, data) {
12814
+ if (!s) return false;
12815
+ const until = s._uiRefreshStatusOnlySuppressUntil || 0;
12816
+ return !!(until && Date.now() < until && _isClientCodexStatusOnlyOutput(s, data));
12817
+ }
12818
+
11618
12819
  function getSessionStatus(s) {
11619
12820
  if (s.meta?.type === 'walle' && s.walleState) {
11620
12821
  if (s.walleState.isGenerating) return { cls: 'running', text: 'Running' };
11621
- return { cls: 'waiting', text: 'Waiting' };
12822
+ return { cls: 'idle', text: 'Idle' };
11622
12823
  }
11623
12824
  const now = Date.now();
12825
+ const statusMap = { running: 'Running', waiting: 'Waiting', idle: 'Idle', exited: 'Exited' };
12826
+
12827
+ const serverLiveStatus = liveStatusResult(s._serverLiveStatus);
12828
+ if (serverLiveStatus && s._serverLiveStatusAt && (now - s._serverLiveStatusAt) < SERVER_LIVE_STATUS_TTL_MS) {
12829
+ return serverLiveStatus;
12830
+ }
12831
+
12832
+ const serverWorkingAt = s._serverWorkingAt || 0;
12833
+ const serverWorkingEventAt = s._serverWorkingEventAt || serverWorkingAt;
12834
+ const waitingAt = s._waitingForInputAt || 0;
12835
+ const serverWorking = serverWorkingAt &&
12836
+ (now - serverWorkingAt) < 10000 &&
12837
+ (!s._waitingForInput || !waitingAt || serverWorkingEventAt >= waitingAt);
12838
+ const streamFresh = !!(s._streamStatus && s._streamStatusAt && (now - s._streamStatusAt) < 60000);
12839
+ const streamRunning = streamFresh && normalizeLiveSessionStatus(s._streamStatus) === 'running';
12840
+ const lastOut = Math.max(s._lastOutputAt || 0, s.meta?.lastPtyActivity || 0);
12841
+ const recentOutput = lastOut && (now - lastOut) < 5000;
12842
+ const codexRunningHold = _clientCodexRunningHoldResult(s, now);
11624
12843
 
11625
- // Authoritative (hook or OTEL) is the strongest signal CliDeck-style.
12844
+ // Hook/OTEL signals are next after the server's unified live projection.
11626
12845
  // Trust fresh hook/OTEL signals, but do not let a lost stop-hook pin a session
11627
12846
  // on Running forever. Stale authoritative signals fall through to SessionStream
11628
- // and local PTY evidence.
12847
+ // and local PTY evidence. Conversely, a fresh authoritative idle signal must
12848
+ // not mask newer Codex/PTY evidence that is visibly still working.
11629
12849
  if (s._authoritativeSource) {
11630
12850
  const authAt = s._authoritativeStatusAt || 0;
11631
12851
  const authFresh = authAt && (now - authAt) < AUTHORITATIVE_STATUS_TTL_MS;
@@ -11633,31 +12853,29 @@ function getSessionStatus(s) {
11633
12853
  if (s._working) return { cls: 'running', text: 'Running' };
11634
12854
  // Not working — distinguish "waiting for input" (regex or hook 'menu') from "idle".
11635
12855
  if (s._waitingForInput) return { cls: 'waiting', text: 'Waiting' };
12856
+ const newerRunningEvidence =
12857
+ serverWorking ||
12858
+ (streamRunning && (!authAt || (s._streamStatusAt || 0) >= authAt)) ||
12859
+ (recentOutput && (!authAt || lastOut >= authAt));
12860
+ if (newerRunningEvidence) return { cls: 'running', text: 'Running' };
11636
12861
  return { cls: 'idle', text: 'Idle' };
11637
12862
  }
11638
12863
  }
11639
12864
 
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
12865
  if (serverWorking) return { cls: 'running', text: 'Running' };
11646
12866
 
11647
12867
  if (s._waitingForInput) return { cls: 'waiting', text: 'Waiting' };
12868
+ if (codexRunningHold) return codexRunningHold;
11648
12869
 
11649
12870
  // Primary: SessionStream status (received via WS stream-status events).
11650
12871
  // Trust it if received within the last 60s (reconciliation runs every 60s).
11651
12872
  // Do not override fresh stream status with local PTY output; that produced
11652
12873
  // 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' };
12874
+ if (streamFresh) {
11655
12875
  return { cls: s._streamStatus, text: statusMap[s._streamStatus] || 'Idle' };
11656
12876
  }
11657
12877
 
11658
12878
  // 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
12879
  if (s._waitingForInput && !recentOutput) return { cls: 'waiting', text: 'Waiting' };
11662
12880
  if (recentOutput) return { cls: 'running', text: 'Running' };
11663
12881
  const justSentInput = s._lastInputAt && (now - s._lastInputAt) < 3000;
@@ -11731,34 +12949,35 @@ function recentItemDblClick(id, event) {
11731
12949
  // Shared helper: attach event listeners for inline rename inputs
11732
12950
  function setupRenameInput(input, currentText, finish) {
11733
12951
  // Stop events from bubbling to parent (e.g., activateTab → steal focus)
11734
- for (const evt of ['click', 'mousedown', 'dblclick']) {
12952
+ for (const evt of ['click', 'mousedown', 'mouseup', 'pointerdown', 'dblclick']) {
11735
12953
  input.addEventListener(evt, (e) => e.stopPropagation());
11736
12954
  }
11737
12955
  input.addEventListener('blur', finish);
11738
12956
  input.addEventListener('keydown', (e) => {
11739
- if (e.key === 'Enter') { e.preventDefault(); input.blur(); }
11740
- if (e.key === 'Escape') { input.value = currentText; input.blur(); }
12957
+ e.stopPropagation();
12958
+ if (e.key === 'Enter') { e.preventDefault(); finish(); }
12959
+ if (e.key === 'Escape') { e.preventDefault(); input.value = currentText; finish(); }
11741
12960
  });
12961
+ input.addEventListener('keyup', (e) => e.stopPropagation());
11742
12962
  }
11743
12963
 
11744
12964
  function startRenameSession(sessionId, labelEl) {
11745
12965
  // Guard: already editing
11746
12966
  if (labelEl.querySelector('input')) return;
11747
- const currentText = labelEl.textContent.trim();
12967
+ const branch = state.sessions.get(sessionId)?.meta?.branch || '';
12968
+ const currentText = cleanSessionLabelForBranch(labelEl.textContent.trim(), branch);
11748
12969
  const input = document.createElement('input');
11749
12970
  input.type = 'text';
11750
12971
  input.value = currentText;
11751
12972
  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
12973
  labelEl.textContent = '';
11753
12974
  labelEl.appendChild(input);
11754
- input.focus();
11755
- input.select();
11756
12975
 
11757
12976
  let done = false;
11758
12977
  function finish() {
11759
12978
  if (done) return;
11760
12979
  done = true;
11761
- const newName = input.value.trim();
12980
+ const newName = cleanSessionLabelForBranch(input.value.trim(), branch);
11762
12981
  if (newName && newName !== currentText) {
11763
12982
  // Persist to DB via REST API
11764
12983
  fetch(`/api/sessions/rename?token=${state.token}`, {
@@ -11768,7 +12987,7 @@ function startRenameSession(sessionId, labelEl) {
11768
12987
  });
11769
12988
  // Update active session label
11770
12989
  const active = state.sessions.get(sessionId);
11771
- if (active && active.meta) active.meta.label = newName;
12990
+ if (active && active.meta) { active.meta.label = newName; active.meta.userRenamed = true; }
11772
12991
  // Update recent session list
11773
12992
  const recent = allRecentSessions.find(x => x.sessionId === sessionId);
11774
12993
  if (recent) { recent.aiTitle = newName; recent.userRenamed = true; }
@@ -11793,6 +13012,8 @@ function startRenameSession(sessionId, labelEl) {
11793
13012
  }
11794
13013
 
11795
13014
  setupRenameInput(input, currentText, finish);
13015
+ input.focus();
13016
+ input.select();
11796
13017
  }
11797
13018
 
11798
13019
  function startRenameReviewTitle(titleEl) {
@@ -11807,8 +13028,6 @@ function startRenameReviewTitle(titleEl) {
11807
13028
  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
13029
  titleEl.textContent = '';
11809
13030
  titleEl.appendChild(input);
11810
- input.focus();
11811
- input.select();
11812
13031
 
11813
13032
  let done = false;
11814
13033
  function finish() {
@@ -11824,7 +13043,7 @@ function startRenameReviewTitle(titleEl) {
11824
13043
  const s = allRecentSessions.find(x => x.sessionId === sessionId);
11825
13044
  if (s) { s.aiTitle = newName; s.userRenamed = true; }
11826
13045
  const active = state.sessions.get(sessionId);
11827
- if (active && active.meta) active.meta.label = newName;
13046
+ if (active && active.meta) { active.meta.label = newName; active.meta.userRenamed = true; }
11828
13047
  titleEl.textContent = newName;
11829
13048
  renderFilteredSessions();
11830
13049
  renderSessionList();
@@ -11835,6 +13054,8 @@ function startRenameReviewTitle(titleEl) {
11835
13054
  }
11836
13055
 
11837
13056
  setupRenameInput(input, currentText, finish);
13057
+ input.focus();
13058
+ input.select();
11838
13059
  }
11839
13060
 
11840
13061
  function startRenameReviewTabLabel(labelEl) {
@@ -11848,8 +13069,6 @@ function startRenameReviewTabLabel(labelEl) {
11848
13069
  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
13070
  labelEl.textContent = '';
11850
13071
  labelEl.appendChild(input);
11851
- input.focus();
11852
- input.select();
11853
13072
 
11854
13073
  let done = false;
11855
13074
  function finish() {
@@ -11865,7 +13084,7 @@ function startRenameReviewTabLabel(labelEl) {
11865
13084
  const s = allRecentSessions.find(x => x.sessionId === sessionId);
11866
13085
  if (s) { s.aiTitle = newName; s.userRenamed = true; }
11867
13086
  const active = state.sessions.get(sessionId);
11868
- if (active && active.meta) active.meta.label = newName;
13087
+ if (active && active.meta) { active.meta.label = newName; active.meta.userRenamed = true; }
11869
13088
  const reviewTitleEl = document.getElementById('review-title');
11870
13089
  if (reviewTitleEl) reviewTitleEl.textContent = newName;
11871
13090
  }
@@ -11879,6 +13098,8 @@ function startRenameReviewTabLabel(labelEl) {
11879
13098
  }
11880
13099
 
11881
13100
  setupRenameInput(input, rawText, finish);
13101
+ input.focus();
13102
+ input.select();
11882
13103
  }
11883
13104
 
11884
13105
  function startRenameRecentSession(sessionId, spanEl) {
@@ -11889,8 +13110,6 @@ function startRenameRecentSession(sessionId, spanEl) {
11889
13110
  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
13111
  spanEl.textContent = '';
11891
13112
  spanEl.appendChild(input);
11892
- input.focus();
11893
- input.select();
11894
13113
 
11895
13114
  let done = false;
11896
13115
  function finish() {
@@ -11909,7 +13128,7 @@ function startRenameRecentSession(sessionId, spanEl) {
11909
13128
  if (s) { s.aiTitle = newName; s.userRenamed = true; }
11910
13129
  // Also update active session if applicable
11911
13130
  const active = state.sessions.get(sessionId);
11912
- if (active && active.meta) active.meta.label = newName;
13131
+ if (active && active.meta) { active.meta.label = newName; active.meta.userRenamed = true; }
11913
13132
  // Update review title if this session is being reviewed
11914
13133
  if (state.reviewingSessionId === sessionId) {
11915
13134
  const reviewTitleEl = document.getElementById('review-title');
@@ -11929,6 +13148,8 @@ function startRenameRecentSession(sessionId, spanEl) {
11929
13148
  }
11930
13149
 
11931
13150
  setupRenameInput(input, currentText, finish);
13151
+ input.focus();
13152
+ input.select();
11932
13153
  }
11933
13154
 
11934
13155
  async function loadSessionPrompts(sessionId) {
@@ -12001,6 +13222,23 @@ function dismissCompactBanner(id) {
12001
13222
  }
12002
13223
  }
12003
13224
 
13225
+ function createSessionsOverviewTab() {
13226
+ const tab = document.createElement('div');
13227
+ tab.className = `tab pinned-tab sessions-overview-tab ${isSessionsOverviewActive() ? 'active' : ''}`;
13228
+ tab.dataset.sessionId = SESSIONS_OVERVIEW_TAB_ID;
13229
+ tab.dataset.pinned = 'true';
13230
+ tab.draggable = false;
13231
+ tab.title = 'Sessions overview';
13232
+ tab.onclick = () => showStandupDashboard();
13233
+
13234
+ const tabLabel = document.createElement('span');
13235
+ tabLabel.className = 'tab-label';
13236
+ tabLabel.textContent = 'Overview';
13237
+ tabLabel.dataset.fullTitle = 'Overview';
13238
+ tab.appendChild(tabLabel);
13239
+ return tab;
13240
+ }
13241
+
12004
13242
  let _renderTabsTimer = null;
12005
13243
  let _renderTabsLastHash = '';
12006
13244
  function renderTabs(force) {
@@ -12029,6 +13267,8 @@ function renderTabs(force) {
12029
13267
  // Remove old tabs
12030
13268
  scrollContainer.querySelectorAll('.tab').forEach(t => t.remove());
12031
13269
 
13270
+ scrollContainer.insertBefore(createSessionsOverviewTab(), addBtn);
13271
+
12032
13272
  for (const id of state.tabOrder) {
12033
13273
  if (id === 'rules') {
12034
13274
  const tab = document.createElement('div');
@@ -12105,7 +13345,7 @@ function renderTabs(force) {
12105
13345
 
12106
13346
  const s = state.sessions.get(id);
12107
13347
  if (!s) continue;
12108
- const label = s.meta?.label || id.slice(0, 8);
13348
+ const label = activeSessionDisplayLabel(s, id);
12109
13349
 
12110
13350
  const tab = document.createElement('div');
12111
13351
  tab.className = `tab ${state.activeTab === id ? 'active' : ''}`;
@@ -12113,22 +13353,21 @@ function renderTabs(force) {
12113
13353
  tab.dataset.agent = getAgentType(s);
12114
13354
  tab.draggable = true;
12115
13355
  const tabBranch = s.meta?.branch || '';
12116
- const tabBranchText = tabBranch && tabBranch !== 'main' ? ' [' + escHtml(tabBranch.length > 12 ? tabBranch.slice(0, 12) + '..' : tabBranch) + ']' : '';
12117
13356
  tab.textContent = '';
12118
13357
  const tabIcon = document.createElement('span');
12119
13358
  tabIcon.className = 'tab-icon';
12120
13359
  tabIcon.innerHTML = providerIconSvg(tab.dataset.agent, 12);
12121
- const tabLabel = document.createElement('span'); tabLabel.className = 'tab-label'; tabLabel.textContent = label + (tabBranchText ? '' : '');
13360
+ const tabLabel = document.createElement('span'); tabLabel.className = 'tab-label'; tabLabel.textContent = label; tabLabel.dataset.fullTitle = label;
13361
+ let tabBranchEl = null;
12122
13362
  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);
13363
+ tabBranchEl = document.createElement('span');
13364
+ tabBranchEl.className = 'branch-badge';
13365
+ tabBranchEl.style.cssText = 'font-size:9px;margin-left:3px';
13366
+ tabBranchEl.textContent = '\u25ED ' + (tabBranch.length > 12 ? tabBranch.slice(0, 12) + '..' : tabBranch);
12128
13367
  }
12129
13368
  const tabClose = document.createElement('span'); tabClose.className = 'close-tab'; tabClose.textContent = '\u00d7';
12130
13369
  tabClose.onclick = function(e) { e.stopPropagation(); killSession(id); };
12131
- tab.appendChild(tabIcon); tab.appendChild(tabLabel); tab.appendChild(tabClose);
13370
+ tab.appendChild(tabIcon); tab.appendChild(tabLabel); if (tabBranchEl) tab.appendChild(tabBranchEl); tab.appendChild(tabClose);
12132
13371
  tab.onclick = function(e) { sessionItemClick(id, e); };
12133
13372
  tab.ondblclick = function(e) {
12134
13373
  e.preventDefault();
@@ -12188,6 +13427,7 @@ function updateTabOverflowBtn() {
12188
13427
  const countEl = document.getElementById('tab-overflow-count');
12189
13428
  const tabs = scrollContainer.querySelectorAll('.tab');
12190
13429
  const isOverflowing = scrollContainer.scrollWidth > scrollContainer.clientWidth + 2;
13430
+ updateTabTitleTooltips();
12191
13431
  btn.classList.toggle('visible', isOverflowing);
12192
13432
  if (isOverflowing && countEl) {
12193
13433
  countEl.textContent = tabs.length;
@@ -12204,6 +13444,12 @@ function toggleTabOverflow(e) {
12204
13444
  menu = document.createElement('div');
12205
13445
  menu.className = 'tab-overflow-menu';
12206
13446
 
13447
+ const overviewItem = document.createElement('div');
13448
+ overviewItem.className = 'tab-overflow-item' + (isSessionsOverviewActive() ? ' active' : '');
13449
+ overviewItem.innerHTML = `<span class="overflow-dot" style="background:var(--accent)"></span><span class="overflow-label">Overview</span>`;
13450
+ overviewItem.onclick = function() { menu.remove(); showStandupDashboard(); };
13451
+ menu.appendChild(overviewItem);
13452
+
12207
13453
  for (const id of state.tabOrder) {
12208
13454
  // Skip tabs that aren't rendered
12209
13455
  if (id === 'codereview' || id === 'walle') continue;
@@ -12346,7 +13592,10 @@ async function _maybeRecommendWorktreeForNewSession(force) {
12346
13592
 
12347
13593
  try {
12348
13594
  var token = (state && state.token) ? state.token : '';
12349
- var r = await fetch('/api/worktrees?token=' + encodeURIComponent(token));
13595
+ var params = new URLSearchParams();
13596
+ params.set('token', token);
13597
+ params.set('cwd', cwd);
13598
+ var r = await fetch('/api/worktrees?' + params.toString());
12350
13599
  var d = await r.json();
12351
13600
  if (seq !== _nsWorktreeRecommendSeq || _nsWorktreeTouched) return;
12352
13601
  var latestCwd = _nsNormalizeCwdForCompare((document.getElementById('ns-cwd') || {}).value || '');
@@ -12595,8 +13844,7 @@ function killSession(id) {
12595
13844
  const next = nextSession || state.tabOrder[state.tabOrder.length - 1];
12596
13845
  if (next) activateTab(next);
12597
13846
  else {
12598
- state.activeTab = null;
12599
- document.getElementById('welcome').style.display = 'flex';
13847
+ showStandupDashboard();
12600
13848
  }
12601
13849
  }
12602
13850
  renderTabs();
@@ -12627,10 +13875,7 @@ function closeAllTabs() {
12627
13875
  // Close special tabs too
12628
13876
  state.tabOrder = state.tabOrder.filter(t => !['rules', 'insights', 'permissions'].includes(t) && !t.startsWith('review'));
12629
13877
  saveTabOrder();
12630
- state.activeTab = null;
12631
- document.getElementById('welcome').style.display = 'flex';
12632
- renderTabs();
12633
- renderSessionList();
13878
+ showStandupDashboard();
12634
13879
  }
12635
13880
 
12636
13881
  function closeOtherTabs(keepId) {
@@ -13315,6 +14560,7 @@ function _fitTerminalPreservingViewport(s, sessionId, opts) {
13315
14560
  s.writer._userScrollLocked = savedLocked;
13316
14561
  }
13317
14562
  if (opts.sendResize && sessionId) {
14563
+ _markClientUiRefreshOutputSuppression(s);
13318
14564
  send({ type: 'resize', id: sessionId, cols: s.term.cols, rows: s.term.rows });
13319
14565
  }
13320
14566
 
@@ -13794,9 +15040,10 @@ async function loadPrefs() {
13794
15040
  if (sortSel) sortSel.value = prefs.session_sort;
13795
15041
  }
13796
15042
 
13797
- // Restore model filter
13798
- if (prefs.model_filter) {
13799
- currentModelFilter = prefs.model_filter;
15043
+ // Restore agent filter. Ignore the legacy model_filter value so stale
15044
+ // raw model IDs (for example fake-model) never reappear as filters.
15045
+ if (prefs.agent_filter) {
15046
+ currentAgentFilter = SessionSearchUtils.normalizeRecentAgentType(prefs.agent_filter) || '';
13800
15047
  }
13801
15048
 
13802
15049
  // (empty sessions hidden by default — no pref needed)
@@ -13965,8 +15212,9 @@ async function loadRecentSessions() {
13965
15212
  pinnedSessionIds.push(s.sessionId);
13966
15213
  }
13967
15214
  }
13968
- populateProjectFilter(allRecentSessions);
13969
- populateModelFilter(allRecentSessions);
15215
+ const sidebarSessions = getRecentSidebarSessions();
15216
+ populateProjectFilter(sidebarSessions);
15217
+ populateAgentFilter(sidebarSessions);
13970
15218
  renderFilteredSessions();
13971
15219
 
13972
15220
  // Resolve pending review hash sessions here. Active #session=<id> hashes are
@@ -13995,7 +15243,7 @@ async function loadRecentSessions() {
13995
15243
  // If not post-restart (e.g. normal page load with saved review), restore active tab now
13996
15244
  if (!state._postRestart) {
13997
15245
  const _hashNav = location.hash.slice(1).split('&')[0];
13998
- const _isHashPanel = ['rules', 'insights', 'permissions', 'prompts', 'codereview', 'walle', 'models', 'backups'].includes(_hashNav);
15246
+ const _isHashPanel = ['command', 'rules', 'insights', 'permissions', 'prompts', 'codereview', 'walle', 'models', 'backups', 'worktrees', 'setup'].includes(_hashNav);
13999
15247
  if (_isHashPanel) {
14000
15248
  navTo(_hashNav, { skipHash: true });
14001
15249
  } else if (state._savedActiveSession && state._savedActiveSession !== 'review') {
@@ -14073,7 +15321,7 @@ function _recentSidebarFilterState() {
14073
15321
  isCtmSession,
14074
15322
  emptyMode: showEmptyOnly ? 'only' : 'exclude',
14075
15323
  project: currentProjectFilter,
14076
- model: currentModelFilter,
15324
+ agent: currentAgentFilter,
14077
15325
  };
14078
15326
  }
14079
15327
 
@@ -14082,7 +15330,7 @@ function _applyRecentSidebarFilters(sessions, filters) {
14082
15330
  }
14083
15331
 
14084
15332
  function getFilteredSessions() {
14085
- return _applyRecentSidebarFilters(allRecentSessions);
15333
+ return _applyRecentSidebarFilters(getRecentSidebarSessions());
14086
15334
  }
14087
15335
 
14088
15336
  let titleGenInProgress = false;
@@ -14105,6 +15353,7 @@ function _activeTabSessionCandidate(id, s) {
14105
15353
  sessionId: id,
14106
15354
  provisionalId: id,
14107
15355
  agentSessionId: s?.meta?.agentSessionId || s?.meta?.agentSessionToken || s?.meta?.claudeSessionId || '',
15356
+ agent: _clientAgentTypeForSession(s),
14108
15357
  project: s?.meta?.cwd || '',
14109
15358
  projectEntry: '',
14110
15359
  cwd: s?.meta?.cwd || '',
@@ -14126,6 +15375,16 @@ function _activeTabSessionCandidate(id, s) {
14126
15375
  };
14127
15376
  }
14128
15377
 
15378
+ function _activeTabSessionCandidates() {
15379
+ const candidates = [];
15380
+ for (const [id, s] of state.sessions) candidates.push(_activeTabSessionCandidate(id, s));
15381
+ return candidates;
15382
+ }
15383
+
15384
+ function getRecentSidebarSessions() {
15385
+ return SessionSearchUtils.mergeRecentSessionCandidates(allRecentSessions, _activeTabSessionCandidates());
15386
+ }
15387
+
14129
15388
  function renderFilteredSessions() {
14130
15389
  // Skip re-render if user just clicked a result (250ms timer in flight)
14131
15390
  if (_recentClickTimer) return;
@@ -14134,11 +15393,16 @@ function renderFilteredSessions() {
14134
15393
  if (recentList && recentList.querySelector('input[type="text"]')) return;
14135
15394
  const q = document.getElementById('recent-search').value.toLowerCase();
14136
15395
  const sidebarFilters = _recentSidebarFilterState();
14137
- let sessions = getFilteredSessions();
15396
+ const sidebarSessions = getRecentSidebarSessions();
15397
+ populateAgentFilter(sidebarSessions);
15398
+ let sessions = _applyRecentSidebarFilters(sidebarSessions, sidebarFilters);
14138
15399
  if (q && !aiSearchMode) {
14139
15400
  // First filter local metadata
14140
15401
  const metaMatches = new Set();
14141
- const recentIds = new Set(sessions.map(s => s.sessionId));
15402
+ const recentIds = new Set();
15403
+ for (const s of sessions) {
15404
+ for (const id of SessionSearchUtils.getSearchableSessionIds(s)) recentIds.add(id);
15405
+ }
14142
15406
  sessions = sessions.filter(s => {
14143
15407
  // Also check active session label (tab name)
14144
15408
  const activeLabel = _activeSessionLabel(s.sessionId, state.sessions.get(s.sessionId));
@@ -15288,62 +16552,57 @@ function setSessionSort(sort) {
15288
16552
  renderFilteredSessions();
15289
16553
  }
15290
16554
 
15291
- let currentModelFilter = '';
15292
- function setModelFilter(model) {
15293
- currentModelFilter = model;
15294
- savePref('model_filter', model);
16555
+ let currentAgentFilter = '';
16556
+ function setAgentFilter(agent) {
16557
+ currentAgentFilter = SessionSearchUtils.normalizeRecentAgentType(agent) || '';
16558
+ savePref('agent_filter', currentAgentFilter);
15295
16559
  refreshRecentSearchAfterFilterChange();
15296
16560
  }
15297
16561
 
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;
16562
+ // Back-compat for older cached markup/tests that still call the old handler.
16563
+ function setModelFilter(agent) {
16564
+ setAgentFilter(agent);
15313
16565
  }
15314
16566
 
15315
- function populateModelFilter(sessions) {
16567
+ function populateAgentFilter(sessions) {
15316
16568
  const sel = document.getElementById('model-filter');
15317
16569
  if (!sel) return;
15318
- const models = new Map();
16570
+ const agents = new Map();
15319
16571
  for (const s of sessions) {
15320
- if (s.model && !isSyntheticModelName(s.model)) {
15321
- models.set(s.model, (models.get(s.model) || 0) + 1);
15322
- }
16572
+ const agent = SessionSearchUtils.getRecentSessionAgentType(s);
16573
+ agents.set(agent, (agents.get(agent) || 0) + 1);
15323
16574
  }
15324
- const sorted = [...models.entries()].sort((a, b) => {
15325
- const pa = modelFilterPriority(a[0]);
15326
- const pb = modelFilterPriority(b[0]);
16575
+ const sorted = [...agents.entries()].sort((a, b) => {
16576
+ const pa = SessionSearchUtils.recentAgentFilterPriority(a[0]);
16577
+ const pb = SessionSearchUtils.recentAgentFilterPriority(b[0]);
15327
16578
  if (pa !== pb) return pa - pb;
15328
16579
  if (b[1] !== a[1]) return b[1] - a[1];
15329
- return String(b[0]).localeCompare(String(a[0]), undefined, { numeric: true, sensitivity: 'base' });
16580
+ return SessionSearchUtils.recentAgentFilterLabel(a[0]).localeCompare(
16581
+ SessionSearchUtils.recentAgentFilterLabel(b[0]),
16582
+ undefined,
16583
+ { numeric: true, sensitivity: 'base' }
16584
+ );
15330
16585
  });
15331
16586
  const prev = sel.value;
15332
16587
  // Build options safely using DOM APIs
15333
16588
  while (sel.options.length > 0) sel.remove(0);
15334
16589
  const allOpt = document.createElement('option');
15335
16590
  allOpt.value = '';
15336
- allOpt.textContent = 'All Models (' + sessions.length + ')';
16591
+ allOpt.textContent = 'All Agents (' + sessions.length + ')';
15337
16592
  sel.add(allOpt);
15338
- for (const [m, count] of sorted) {
16593
+ for (const [agent, count] of sorted) {
15339
16594
  const opt = document.createElement('option');
15340
- opt.value = m;
15341
- opt.textContent = modelFilterLabel(m) + ' (' + count + ')';
16595
+ opt.value = agent;
16596
+ opt.textContent = SessionSearchUtils.recentAgentFilterLabel(agent) + ' (' + count + ')';
15342
16597
  sel.add(opt);
15343
16598
  }
15344
- const nextValue = prev || currentModelFilter || '';
15345
- sel.value = models.has(nextValue) ? nextValue : '';
15346
- if (currentModelFilter && !models.has(currentModelFilter)) currentModelFilter = '';
16599
+ const nextValue = SessionSearchUtils.normalizeRecentAgentType(prev || currentAgentFilter || '');
16600
+ sel.value = agents.has(nextValue) ? nextValue : '';
16601
+ if (currentAgentFilter && !agents.has(currentAgentFilter)) currentAgentFilter = '';
16602
+ }
16603
+
16604
+ function populateModelFilter(sessions) {
16605
+ populateAgentFilter(sessions);
15347
16606
  }
15348
16607
 
15349
16608
  // Strip worktree suffix from project path: /repo/.claude/worktrees/name → /repo
@@ -15448,13 +16707,14 @@ function escHtml(s) {
15448
16707
  // element (.session-item or .tab) sets color via CSS so the icon inherits the
15449
16708
  // per-provider hue and inverts cleanly when the row is active (light bg, dark text).
15450
16709
  // 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' };
16710
+ const AGENT_LABELS = { walle: 'Wall-E', codex: 'Codex', gemini: 'Gemini', opencode: 'OpenCode', claude: 'Claude Code', 'claude-desktop': 'Claude Desktop', shell: 'Shell' };
15452
16711
  function getAgentType(s) {
15453
16712
  if (!s) return 'shell';
15454
16713
  if (s.meta?.type === 'walle') return 'walle';
15455
- const cmd = s.meta?.cmd || '';
16714
+ const cmd = String(s.meta?.cmd || '').toLowerCase();
15456
16715
  if (cmd.includes('codex')) return 'codex';
15457
16716
  if (cmd.includes('gemini')) return 'gemini';
16717
+ if (cmd.includes('opencode') || cmd.includes('open-code')) return 'opencode';
15458
16718
  if (cmd.includes('claude')) return 'claude';
15459
16719
  return 'shell';
15460
16720
  }
@@ -15500,6 +16760,12 @@ function providerIconSvg(agentType, sizePx) {
15500
16760
  // provider icons in the same row.
15501
16761
  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
16762
  break;
16763
+ case 'opencode':
16764
+ // Code brackets with a center dot: compact enough for tabs, distinct
16765
+ // from the generic terminal prompt used for plain shell sessions.
16766
+ 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"/>'
16767
+ + '<circle cx="8" cy="8" r="1.2" fill="currentColor"/>';
16768
+ break;
15503
16769
  case 'shell':
15504
16770
  // >_ prompt — universal terminal metaphor.
15505
16771
  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"/>';
@@ -15900,12 +17166,12 @@ async function performServerSearch(query) {
15900
17166
  if (data.results && data.results.length > 0) {
15901
17167
  _serverSearchActive = true;
15902
17168
  // 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
17169
+ // list. Otherwise the badge can count rows hidden by Empty/project/agent
15904
17170
  // filters while the rendered list says "No sessions found".
15905
17171
  const sidebarFilters = _recentSidebarFilterState();
15906
- const serverFiltered = _applyRecentSidebarFilters(data.results, sidebarFilters);
17172
+ const serverFiltered = SessionSearchUtils.dedupeSessionCandidates(_applyRecentSidebarFilters(data.results, sidebarFilters));
15907
17173
  if (serverFiltered.length === 0) {
15908
- // All server results were filtered out (e.g., Empty/project/model).
17174
+ // All server results were filtered out (e.g., Empty/project/agent).
15909
17175
  // Fall back to local-only rendering
15910
17176
  _serverSearchActive = false;
15911
17177
  renderFilteredSessions();
@@ -15931,11 +17197,12 @@ async function performServerSearch(query) {
15931
17197
  localSessions.push(candidate);
15932
17198
  }
15933
17199
  }
15934
- const serverIds = new Set(serverFiltered.map(r => r.sessionId));
15935
17200
  const merged = [...serverFiltered];
17201
+ const mergedIdentityIndex = SessionSearchUtils.createSessionIdentityIndex(merged);
15936
17202
  let localOnlyCount = 0;
15937
17203
  for (const ls of localSessions) {
15938
- if (!serverIds.has(ls.sessionId)) {
17204
+ const existing = SessionSearchUtils.findSessionIdentityMatch(mergedIdentityIndex, ls);
17205
+ if (!existing) {
15939
17206
  const idScore = SessionSearchUtils.scoreSessionIdMatch(ls, lq);
15940
17207
  if (idScore > 0) {
15941
17208
  ls._score = idScore / 1000;
@@ -15950,7 +17217,10 @@ async function performServerSearch(query) {
15950
17217
  ls._matchField = exactTitle ? 'title (exact)' : titleContains ? 'title' : 'metadata';
15951
17218
  }
15952
17219
  merged.push(ls);
17220
+ for (const id of SessionSearchUtils.getSearchableSessionIds(ls)) mergedIdentityIndex.set(id, ls);
15953
17221
  localOnlyCount++;
17222
+ } else {
17223
+ SessionSearchUtils.mergeRecentSessionMetadata(existing, ls);
15954
17224
  }
15955
17225
  }
15956
17226
  merged.sort((a, b) => (b._score || 0) - (a._score || 0) || SessionActivityUtils.sessionTouchedAtMs(b) - SessionActivityUtils.sessionTouchedAtMs(a));
@@ -17724,6 +18994,9 @@ function onDataChanged(msg) {
17724
18994
  }
17725
18995
  if (r === 'models' || r === 'model-registry' || r === 'providers') {
17726
18996
  _modelRegistryCache = null; // Invalidate cache so switchers re-fetch
18997
+ if (typeof WalleSession !== 'undefined' && WalleSession.invalidateModelCache) {
18998
+ WalleSession.invalidateModelCache();
18999
+ }
17727
19000
  }
17728
19001
  }
17729
19002
 
@@ -17740,9 +19013,16 @@ function clearWaitingState(sessionId, opts = {}) {
17740
19013
  const wasWaiting = s._waitingForInput;
17741
19014
  s._waitingForInput = false;
17742
19015
  s._waitingForInputAt = 0;
19016
+ s._waitingReason = '';
17743
19017
  const now = opts.timestamp || Date.now();
17744
19018
  if (opts.markInput !== false) s._lastInputAt = now;
17745
- if (opts.markWorking) s._serverWorkingAt = now;
19019
+ if (opts.markWorking) {
19020
+ s._serverWorkingAt = now;
19021
+ s._serverWorkingEventAt = opts.eventTimestamp || now;
19022
+ s._serverLiveStatus = 'running';
19023
+ s._serverLiveStatusAt = now;
19024
+ _markClientCodexRunningEvidence(s, opts.eventTimestamp || now, now);
19025
+ }
17746
19026
  // Only do DOM work if the session was actually in waiting state
17747
19027
  if (wasWaiting) {
17748
19028
  const tabs = document.querySelectorAll('#tabbar .tab');
@@ -17800,18 +19080,38 @@ function playNotificationSound(type) {
17800
19080
  function onSessionActivity(msg) {
17801
19081
  if (!msg.sessions) return;
17802
19082
  let shouldRerender = false;
17803
- for (const { id, ts, state: serverState } of msg.sessions) {
19083
+ for (const { id, ts, state: serverState, status } of msg.sessions) {
17804
19084
  const s = state.sessions.get(id);
17805
19085
  if (!s) continue;
17806
19086
  const oldBucket = activeActivityBucket(s);
17807
19087
  const oldStatus = getSessionStatus(s).cls;
17808
19088
  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();
19089
+ const liveStatus = normalizeLiveSessionStatus(status || serverState);
19090
+ const waitingAt = s._waitingForInputAt || 0;
19091
+ const activityIsNewerThanWaiting = !s._waitingForInput || !waitingAt || serverTs >= waitingAt;
19092
+ if (liveStatus && (liveStatus !== 'running' || activityIsNewerThanWaiting)) {
19093
+ s._serverLiveStatus = liveStatus;
19094
+ s._serverLiveStatusAt = Date.now();
19095
+ if (s.meta) s.meta.liveStatus = liveStatus;
19096
+ if (liveStatus !== 'running') {
19097
+ s._serverWorkingAt = 0;
19098
+ s._serverWorkingEventAt = 0;
19099
+ s._codexRunningHoldUntil = 0;
19100
+ }
19101
+ }
19102
+ if (liveStatus === 'waiting') {
19103
+ if (!s._waitingForInput) s._waitingForInputAt = serverTs;
19104
+ s._waitingForInput = true;
19105
+ }
19106
+ if (liveStatus === 'running') {
19107
+ if (activityIsNewerThanWaiting) {
19108
+ const receivedAt = Date.now();
19109
+ s._serverWorkingAt = receivedAt;
19110
+ s._serverWorkingEventAt = serverTs;
19111
+ _markClientCodexRunningEvidence(s, serverTs, receivedAt);
19112
+ }
17813
19113
  if (s._waitingForInput && activityIsNewerThanWaiting) {
17814
- clearWaitingState(id, { markInput: false, markWorking: true });
19114
+ clearWaitingState(id, { markInput: false, markWorking: true, eventTimestamp: serverTs });
17815
19115
  }
17816
19116
  }
17817
19117
  if (s.meta && ts) {
@@ -17824,6 +19124,7 @@ function onSessionActivity(msg) {
17824
19124
  }
17825
19125
  }
17826
19126
  if (shouldRerender) renderSessionList();
19127
+ if (typeof scheduleStandupRefresh === 'function') scheduleStandupRefresh();
17827
19128
  }
17828
19129
 
17829
19130
  // Server signals that a previously-idle session has resumed generating output.
@@ -17832,6 +19133,7 @@ function onSessionActivity(msg) {
17832
19133
  function onSessionResumed(msg) {
17833
19134
  const s = state.sessions.get(msg.id);
17834
19135
  if (s) clearWaitingState(msg.id, { markInput: false, markWorking: true, timestamp: msg.timestamp || Date.now() });
19136
+ if (typeof scheduleStandupRefresh === 'function') scheduleStandupRefresh();
17835
19137
  }
17836
19138
 
17837
19139
  // Authoritative session state (hooks or OTEL). Wins over the regex fallback.
@@ -17842,6 +19144,11 @@ function onAuthoritativeStatus(msg) {
17842
19144
  s._authoritativeSource = msg.source || 'hook';
17843
19145
  s._working = !!msg.working;
17844
19146
  s._authoritativeStatusAt = msg.timestamp || Date.now();
19147
+ s._serverLiveStatus = s._working ? 'running' : 'idle';
19148
+ s._serverLiveStatusAt = s._authoritativeStatusAt;
19149
+ if (s.meta) s.meta.liveStatus = s._serverLiveStatus;
19150
+ if (s._working) _markClientCodexRunningEvidence(s, s._authoritativeStatusAt, Date.now());
19151
+ else s._codexRunningHoldUntil = 0;
17845
19152
  // When the agent is working, explicitly clear "waiting for input" state —
17846
19153
  // the regex fallback may have left it set before hooks took over.
17847
19154
  if (s._working && s._waitingForInput) clearWaitingState(msg.id, { markInput: false, markWorking: true, timestamp: s._authoritativeStatusAt });
@@ -17852,6 +19159,7 @@ function onAuthoritativeStatus(msg) {
17852
19159
  item.classList.toggle('idle', !msg.working);
17853
19160
  item.classList.remove('stale'); // authoritative signal supersedes staleness
17854
19161
  }
19162
+ if (typeof scheduleStandupRefresh === 'function') scheduleStandupRefresh();
17855
19163
  }
17856
19164
 
17857
19165
  // Agent's internal session ID captured via OTEL — lets us surface real resume IDs in UI.
@@ -17891,9 +19199,7 @@ function onAgentLinked(msg) {
17891
19199
  if (msg.model_provider) s.meta.model_provider = msg.model_provider;
17892
19200
  populateModelSwitcher(ctmId);
17893
19201
  }
17894
- for (const key of Object.keys(_promptScanCache)) {
17895
- if (key === ctmId || key.startsWith(ctmId + ':') || key.startsWith(agentId)) delete _promptScanCache[key];
17896
- }
19202
+ invalidatePromptScanCacheForSession(ctmId);
17897
19203
  scanPromptLines(ctmId);
17898
19204
  }
17899
19205
  for (const recent of allRecentSessions || []) {
@@ -17935,17 +19241,16 @@ function onWaitingForInput(msg) {
17935
19241
  if (session) {
17936
19242
  session._waitingForInput = true;
17937
19243
  session._waitingForInputAt = msg.timestamp || Date.now();
19244
+ session._waitingReason = msg.reason || 'input';
19245
+ session._serverLiveStatus = 'waiting';
19246
+ session._serverLiveStatusAt = session._waitingForInputAt;
19247
+ session._codexRunningHoldUntil = 0;
19248
+ if (session.meta) session.meta.liveStatus = 'waiting';
17938
19249
  }
19250
+ if (typeof scheduleStandupRefresh === 'function') scheduleStandupRefresh();
17939
19251
  // Re-scan prompts — JSONL is fully written when Claude yields back to user.
17940
19252
  // 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
- }
19253
+ invalidatePromptScanCacheForSession(sessionId);
17949
19254
  scanPromptLines(sessionId);
17950
19255
  const label = msg.label || session?.meta?.label || sessionId.slice(0, 8);
17951
19256
  const reason = msg.reason || 'input';
@@ -18134,7 +19439,9 @@ window.addEventListener('message', (e) => {
18134
19439
  });
18135
19440
 
18136
19441
  // --- Hash routing ---
18137
- const NAV_TARGETS = ['sessions', 'prompts', 'rules', 'insights', 'permissions', 'codereview', 'walle', 'models', 'backups', 'worktrees', 'setup'];
19442
+ // Keep "command" as a route alias for old links; the UI now renders this as
19443
+ // the pinned Overview tab inside Sessions.
19444
+ const NAV_TARGETS = ['sessions', 'command', 'prompts', 'rules', 'insights', 'permissions', 'codereview', 'walle', 'models', 'backups', 'worktrees', 'setup'];
18138
19445
 
18139
19446
  function _parseHashRoute() {
18140
19447
  const hash = location.hash.slice(1);
@@ -18163,10 +19470,15 @@ function handleHashRoute() {
18163
19470
 
18164
19471
  // No hash — fall back to saved nav pref from DB
18165
19472
  if (!hash) {
18166
- if (state._savedActiveNav && NAV_TARGETS.includes(state._savedActiveNav) && state._savedActiveNav !== 'sessions') {
18167
- navTo(state._savedActiveNav, { skipHash: false, skipPersist: true });
19473
+ if (state._savedActiveNav === 'command') {
19474
+ navTo('command', { skipHash: false, skipPersist: true });
19475
+ return;
19476
+ }
19477
+ const savedNav = state._savedActiveNav === 'command' ? 'sessions' : state._savedActiveNav;
19478
+ if (savedNav && NAV_TARGETS.includes(savedNav) && savedNav !== 'sessions') {
19479
+ navTo(savedNav, { skipHash: false, skipPersist: true });
18168
19480
  // Restore deep state (e.g., open prompt) after nav
18169
- if (state._savedActiveNav === 'prompts' && state._savedActivePrompt) {
19481
+ if (savedNav === 'prompts' && state._savedActivePrompt) {
18170
19482
  setTimeout(() => PE.openPrompt(state._savedActivePrompt), 200);
18171
19483
  }
18172
19484
  } else if (state._savedActiveSession) {
@@ -18182,7 +19494,7 @@ function handleHashRoute() {
18182
19494
  const isNav = route.isNav;
18183
19495
  const params = route.params;
18184
19496
 
18185
- // Check for nav target: #permissions, #prompts, #rules, #insights, #sessions, #codereview
19497
+ // Check for nav target: #command alias, #permissions, #prompts, #rules, #insights, #sessions, #codereview
18186
19498
  if (isNav && !Object.keys(params).length) {
18187
19499
  navTo(firstPart, { skipHash: true });
18188
19500
  // For prompts without explicit prompt param, restore saved prompt from DB
@@ -18319,6 +19631,7 @@ state._prefsLoaded = loadPrefs().then(() => {
18319
19631
  loadRecentSessions();
18320
19632
  // Restore hash from saved nav if no hash present
18321
19633
  handleHashRoute();
19634
+ refreshStandupIfVisible();
18322
19635
  });
18323
19636
  refreshSessionPrompts();
18324
19637