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.
- package/README.md +6 -1
- package/bin/create-walle.js +195 -30
- package/bin/mcp-inject.js +18 -53
- package/package.json +3 -1
- package/template/claude-task-manager/approval-agent.js +7 -0
- package/template/claude-task-manager/docs/session-standup-command-center-design.md +242 -0
- package/template/claude-task-manager/git-utils.js +111 -3
- package/template/claude-task-manager/lib/session-history.js +144 -16
- package/template/claude-task-manager/lib/session-standup.js +409 -0
- package/template/claude-task-manager/lib/standup-attention.js +200 -0
- package/template/claude-task-manager/lib/status-hooks.js +8 -2
- package/template/claude-task-manager/lib/update-telemetry.js +114 -0
- package/template/claude-task-manager/lib/walle-default-model.js +55 -0
- package/template/claude-task-manager/lib/walle-mcp-auto-config.js +62 -0
- package/template/claude-task-manager/lib/walle-supervisor.js +83 -19
- package/template/claude-task-manager/lib/worktree-cwd.js +82 -0
- package/template/claude-task-manager/providers/codex-mcp.js +104 -0
- package/template/claude-task-manager/providers/index.js +2 -0
- package/template/claude-task-manager/public/css/setup.css +2 -1
- package/template/claude-task-manager/public/css/walle.css +5 -0
- package/template/claude-task-manager/public/index.html +1596 -283
- package/template/claude-task-manager/public/js/session-search-utils.js +171 -1
- package/template/claude-task-manager/public/js/setup.js +62 -19
- package/template/claude-task-manager/public/js/stream-view.js +55 -6
- package/template/claude-task-manager/public/js/walle-session.js +73 -16
- package/template/claude-task-manager/public/js/walle.js +34 -2
- package/template/claude-task-manager/server.js +780 -177
- package/template/claude-task-manager/session-integrity.js +58 -15
- package/template/claude-task-manager/workers/approval-widget-validator.js +15 -5
- package/template/claude-task-manager/workers/state-detectors/codex.js +6 -0
- package/template/package.json +1 -1
- package/template/wall-e/agent.js +36 -7
- package/template/wall-e/api-walle.js +72 -20
- package/template/wall-e/coding/stream-processor.js +22 -2
- package/template/wall-e/coding-orchestrator.js +26 -6
- package/template/wall-e/eval/agent-runner.js +16 -4
- package/template/wall-e/eval/benchmark-generator.js +21 -1
- package/template/wall-e/eval/benchmarks/coding-agent.json +0 -596
- package/template/wall-e/eval/codex-cli-baseline.js +633 -0
- package/template/wall-e/eval/eval-orchestrator.js +3 -3
- package/template/wall-e/eval/run-agent-benchmarks.js +11 -3
- package/template/wall-e/eval/run-codex-cli-baseline.js +177 -0
- package/template/wall-e/lib/mcp-integration.js +220 -0
- package/template/wall-e/llm/ollama.js +47 -8
- package/template/wall-e/llm/ollama.plugin.json +1 -1
- package/template/wall-e/llm/tool-adapter.js +1 -0
- package/template/wall-e/loops/ingest.js +42 -8
- package/template/wall-e/mcp-server.js +272 -10
- package/template/wall-e/memory/ctm-session-context.js +910 -0
- package/template/wall-e/server.js +26 -1
- package/template/wall-e/skills/_bundled/scan-ctm-sessions/SKILL.md +20 -0
- package/template/wall-e/skills/_bundled/scan-ctm-sessions/run.js +43 -0
- package/template/wall-e/skills/skill-planner.js +52 -3
- package/template/wall-e/tools/builtin-middleware.js +55 -2
- package/template/wall-e/tools/shell-policy.js +1 -1
- package/template/wall-e/tools/slack-owner.js +104 -0
- package/template/website/index.html +2 -2
- 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:
|
|
2026
|
-
justify-content:
|
|
2077
|
+
align-items: stretch;
|
|
2078
|
+
justify-content: flex-start;
|
|
2027
2079
|
flex: 1;
|
|
2028
|
-
gap:
|
|
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;">↑</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;">×</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;">×</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">↑</div>
|
|
2971
3303
|
<div class="update-wizard-title">
|
|
2972
|
-
<h3 id="update-wizard-heading">Upgrade CTM?</h3>
|
|
2973
|
-
<p>A newer
|
|
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="
|
|
3032
|
-
<option value="">All
|
|
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
|
-
<
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
<div
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
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
|
|
3084
|
-
|
|
3085
|
-
|
|
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
|
-
<
|
|
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">
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
5757
|
-
|
|
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
|
-
|
|
5877
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
6586
|
-
|
|
6587
|
-
|
|
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
|
|
6768
|
-
const
|
|
6769
|
-
const
|
|
6770
|
-
const
|
|
6771
|
-
|
|
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
|
-
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
} else {
|
|
6781
|
-
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
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
|
-
|
|
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 (
|
|
7645
|
+
if (effectiveTarget === 'sessions') {
|
|
6814
7646
|
history.replaceState(null, '', location.pathname + location.search);
|
|
6815
7647
|
} else {
|
|
6816
|
-
history.replaceState(null, '', location.pathname + location.search + '#' +
|
|
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',
|
|
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
|
-
|
|
6835
|
-
|
|
6836
|
-
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
// Reset to welcome
|
|
6841
|
-
state.activeTab = null;
|
|
6842
|
-
document.getElementById('welcome').style.display = '';
|
|
6843
|
-
document.getElementById('rules-panel').classList.remove('active');
|
|
6844
|
-
document.getElementById('insights-panel').classList.remove('active');
|
|
6845
|
-
document.getElementById('permissions-panel').classList.remove('active');
|
|
6846
|
-
document.getElementById('prompts-panel').classList.remove('active');
|
|
6847
|
-
document.getElementById('review-panel').classList.remove('active');
|
|
6848
|
-
document.getElementById('codereview-panel').classList.remove('active');
|
|
6849
|
-
document.getElementById('walle-panel').classList.remove('active');
|
|
6850
|
-
var mdlPanel = document.getElementById('models-panel');
|
|
6851
|
-
if (mdlPanel) { mdlPanel.classList.remove('active'); mdlPanel.style.display = 'none'; }
|
|
6852
|
-
var bkPanel = document.getElementById('backups-panel');
|
|
6853
|
-
if (bkPanel) bkPanel.classList.remove('active');
|
|
6854
|
-
var stPanel = document.getElementById('setup-panel');
|
|
6855
|
-
if (stPanel) { stPanel.classList.remove('active'); stPanel.style.display = 'none'; }
|
|
6856
|
-
// Restore sidebar & tabbar
|
|
6857
|
-
const sidebar = document.getElementById('sidebar');
|
|
6858
|
-
if (!state.sidebarManuallyHidden) {
|
|
6859
|
-
sidebar.classList.remove('collapsed');
|
|
6860
|
-
document.getElementById('sidebar-resize').style.display = '';
|
|
6861
|
-
}
|
|
6862
|
-
document.getElementById('tabbar').style.display = '';
|
|
6863
|
-
syncNavPills('sessions');
|
|
6864
|
-
updateTopbarContext('sessions');
|
|
6865
|
-
renderTabs();
|
|
6866
|
-
renderSessionList();
|
|
6867
|
-
}
|
|
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
|
|
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
|
-
|
|
9697
|
-
|
|
9698
|
-
|
|
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
|
-
|
|
9772
|
-
|
|
9773
|
-
|
|
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
|
-
|
|
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
|
|
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 = {
|
|
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
|
-
|
|
10697
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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('›'))
|
|
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
|
-
|
|
11858
|
+
hasNonStatusAfterPrompt = true;
|
|
10915
11859
|
break;
|
|
10916
11860
|
}
|
|
10917
|
-
if (!
|
|
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
|
|
10974
|
-
// to yank them back
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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)}">☍ ${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: '
|
|
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
|
-
//
|
|
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 (
|
|
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
|
-
|
|
11740
|
-
if (e.key === '
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
12124
|
-
|
|
12125
|
-
|
|
12126
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
13798
|
-
|
|
13799
|
-
|
|
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
|
-
|
|
13969
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
15292
|
-
function
|
|
15293
|
-
|
|
15294
|
-
savePref('
|
|
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
|
-
|
|
15299
|
-
|
|
15300
|
-
|
|
15301
|
-
|
|
15302
|
-
function modelFilterPriority(model) {
|
|
15303
|
-
const m = String(model || '').toLowerCase();
|
|
15304
|
-
if (/^(gpt-|o[1-9]|codex-)/.test(m)) return 0;
|
|
15305
|
-
if (/^claude-/.test(m)) return 1;
|
|
15306
|
-
if (/^gemini-/.test(m)) return 2;
|
|
15307
|
-
return 3;
|
|
15308
|
-
}
|
|
15309
|
-
|
|
15310
|
-
function modelFilterLabel(model) {
|
|
15311
|
-
if (/^claude-/.test(model)) return model.replace(/^claude-/, '');
|
|
15312
|
-
return model;
|
|
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
|
|
16567
|
+
function populateAgentFilter(sessions) {
|
|
15316
16568
|
const sel = document.getElementById('model-filter');
|
|
15317
16569
|
if (!sel) return;
|
|
15318
|
-
const
|
|
16570
|
+
const agents = new Map();
|
|
15319
16571
|
for (const s of sessions) {
|
|
15320
|
-
|
|
15321
|
-
|
|
15322
|
-
}
|
|
16572
|
+
const agent = SessionSearchUtils.getRecentSessionAgentType(s);
|
|
16573
|
+
agents.set(agent, (agents.get(agent) || 0) + 1);
|
|
15323
16574
|
}
|
|
15324
|
-
const sorted = [...
|
|
15325
|
-
const pa =
|
|
15326
|
-
const pb =
|
|
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
|
|
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
|
|
16591
|
+
allOpt.textContent = 'All Agents (' + sessions.length + ')';
|
|
15337
16592
|
sel.add(allOpt);
|
|
15338
|
-
for (const [
|
|
16593
|
+
for (const [agent, count] of sorted) {
|
|
15339
16594
|
const opt = document.createElement('option');
|
|
15340
|
-
opt.value =
|
|
15341
|
-
opt.textContent =
|
|
16595
|
+
opt.value = agent;
|
|
16596
|
+
opt.textContent = SessionSearchUtils.recentAgentFilterLabel(agent) + ' (' + count + ')';
|
|
15342
16597
|
sel.add(opt);
|
|
15343
16598
|
}
|
|
15344
|
-
const nextValue = prev ||
|
|
15345
|
-
sel.value =
|
|
15346
|
-
if (
|
|
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/
|
|
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/
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
17810
|
-
|
|
17811
|
-
|
|
17812
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
18167
|
-
navTo(
|
|
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 (
|
|
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
|
|