create-walle 0.9.30 → 0.9.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/package.json +1 -1
  2. package/template/claude-task-manager/api-prompts.js +410 -5
  3. package/template/claude-task-manager/api-reviews.js +251 -3
  4. package/template/claude-task-manager/approval-agent.js +322 -24
  5. package/template/claude-task-manager/db.js +77 -13
  6. package/template/claude-task-manager/docs/codex-app-server-approvals.md +35 -1
  7. package/template/claude-task-manager/docs/postgres-concurrency-dgx-spark.html +407 -0
  8. package/template/claude-task-manager/docs/prompt-manager-redesign-proposal.html +667 -0
  9. package/template/claude-task-manager/docs/resume-ux-redesign.html +493 -0
  10. package/template/claude-task-manager/docs/review-redesign-proposal.html +892 -0
  11. package/template/claude-task-manager/docs/unified-auto-approver-proposal.html +520 -0
  12. package/template/claude-task-manager/git-utils.js +164 -26
  13. package/template/claude-task-manager/lib/agent-capabilities.js +26 -5
  14. package/template/claude-task-manager/lib/agent-cli-cache.js +1 -1
  15. package/template/claude-task-manager/lib/agent-presets.js +12 -0
  16. package/template/claude-task-manager/lib/agent-version-service.js +650 -0
  17. package/template/claude-task-manager/lib/approval-drift.js +57 -0
  18. package/template/claude-task-manager/lib/approval-hook.js +13 -0
  19. package/template/claude-task-manager/lib/auth-rules.js +6 -0
  20. package/template/claude-task-manager/lib/broadcast-payload-memo.js +109 -0
  21. package/template/claude-task-manager/lib/coding-agent-models.js +89 -8
  22. package/template/claude-task-manager/lib/db-owner-cooperative-scheduler.js +10 -1
  23. package/template/claude-task-manager/lib/document-review.js +75 -1
  24. package/template/claude-task-manager/lib/escalation-review.js +33 -3
  25. package/template/claude-task-manager/lib/headless-term-service.js +54 -9
  26. package/template/claude-task-manager/lib/mcp-risk.js +128 -0
  27. package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +51 -2
  28. package/template/claude-task-manager/lib/native-agent-model-args.js +149 -0
  29. package/template/claude-task-manager/lib/opencode-conversation-store.js +269 -0
  30. package/template/claude-task-manager/lib/permission-match.js +77 -5
  31. package/template/claude-task-manager/lib/read-pool-client.js +15 -6
  32. package/template/claude-task-manager/lib/restore-backoff.js +82 -0
  33. package/template/claude-task-manager/lib/runtime-registry.js +1 -0
  34. package/template/claude-task-manager/lib/scheduled-wake.js +42 -0
  35. package/template/claude-task-manager/lib/session-capture.js +33 -6
  36. package/template/claude-task-manager/lib/session-history.js +60 -3
  37. package/template/claude-task-manager/lib/session-jobs.js +19 -0
  38. package/template/claude-task-manager/lib/session-restore.js +5 -4
  39. package/template/claude-task-manager/lib/session-standup.js +2 -0
  40. package/template/claude-task-manager/lib/session-stream.js +9 -0
  41. package/template/claude-task-manager/lib/state-sync/cell-diff.js +31 -4
  42. package/template/claude-task-manager/lib/state-sync/restore-frame-hold.js +39 -0
  43. package/template/claude-task-manager/lib/tui-input-modes.js +40 -0
  44. package/template/claude-task-manager/lib/walle-ctm-history.js +103 -36
  45. package/template/claude-task-manager/lib/walle-session-model-catalog.js +7 -1
  46. package/template/claude-task-manager/lib/walle-token-chip.js +65 -0
  47. package/template/claude-task-manager/package.json +1 -1
  48. package/template/claude-task-manager/providers/claude-code.js +20 -1
  49. package/template/claude-task-manager/providers/codex-mcp-extract.js +33 -0
  50. package/template/claude-task-manager/providers/codex-mcp.js +23 -20
  51. package/template/claude-task-manager/providers/codex.js +65 -1
  52. package/template/claude-task-manager/public/css/prompts.css +345 -0
  53. package/template/claude-task-manager/public/css/reviews.css +248 -40
  54. package/template/claude-task-manager/public/css/walle-session.css +90 -26
  55. package/template/claude-task-manager/public/index.html +2031 -428
  56. package/template/claude-task-manager/public/js/document-review-links.js +255 -13
  57. package/template/claude-task-manager/public/js/file-context-menu.js +185 -0
  58. package/template/claude-task-manager/public/js/message-renderer.js +46 -1
  59. package/template/claude-task-manager/public/js/mobile-review-core.js +54 -0
  60. package/template/claude-task-manager/public/js/prompt-diff.js +55 -0
  61. package/template/claude-task-manager/public/js/prompt-editor-chrome.js +108 -0
  62. package/template/claude-task-manager/public/js/prompt-editor-modes.js +87 -0
  63. package/template/claude-task-manager/public/js/prompt-organize.js +179 -0
  64. package/template/claude-task-manager/public/js/prompt-reuse.js +77 -0
  65. package/template/claude-task-manager/public/js/prompts.js +683 -215
  66. package/template/claude-task-manager/public/js/resume-state.js +186 -0
  67. package/template/claude-task-manager/public/js/reviews.js +402 -121
  68. package/template/claude-task-manager/public/js/screenshot-router.js +91 -0
  69. package/template/claude-task-manager/public/js/session-search-utils.js +81 -0
  70. package/template/claude-task-manager/public/js/state-sync-client.js +134 -17
  71. package/template/claude-task-manager/public/js/stream-view.js +93 -20
  72. package/template/claude-task-manager/public/js/walle-session.js +183 -32
  73. package/template/claude-task-manager/public/js/walle.js +21 -9
  74. package/template/claude-task-manager/public/m/app.css +117 -0
  75. package/template/claude-task-manager/public/m/app.js +309 -3
  76. package/template/claude-task-manager/public/m/index.html +56 -1
  77. package/template/claude-task-manager/server.js +724 -305
  78. package/template/claude-task-manager/workers/db-owner-worker.js +8 -0
  79. package/template/claude-task-manager/workers/read-pool-worker.js +4 -0
  80. package/template/claude-task-manager/workers/state-detectors/claude-code.js +10 -1
  81. package/template/docs/proposals/2026-06-23-local-model-right-path.html +340 -0
  82. package/template/docs/proposals/2026-06-24-qlora-done-right.md +255 -0
  83. package/template/package.json +1 -1
  84. package/template/wall-e/api-walle.js +21 -3
  85. package/template/wall-e/bin/train-gemma-e4b-tooluse.js +27 -4
  86. package/template/wall-e/brain.js +50 -5
  87. package/template/wall-e/chat/force-compact.js +73 -0
  88. package/template/wall-e/chat.js +137 -9
  89. package/template/wall-e/coding/action-memory-policy.js +120 -1
  90. package/template/wall-e/coding/compaction-service.js +10 -3
  91. package/template/wall-e/coding/model-router.js +116 -0
  92. package/template/wall-e/coding-orchestrator.js +303 -11
  93. package/template/wall-e/coding-prompts.js +1 -1
  94. package/template/wall-e/docs/skill-self-heal-design.html +429 -0
  95. package/template/wall-e/llm/client.js +115 -6
  96. package/template/wall-e/llm/codex-cli.js +71 -23
  97. package/template/wall-e/llm/codex-cli.plugin.json +1 -0
  98. package/template/wall-e/llm/default-fallback.js +71 -11
  99. package/template/wall-e/llm/mlx-worker.js +0 -0
  100. package/template/wall-e/llm/mlx.js +46 -12
  101. package/template/wall-e/llm/provider-error.js +9 -3
  102. package/template/wall-e/llm/provider-health-state.js +561 -11
  103. package/template/wall-e/llm/registry.js +6 -0
  104. package/template/wall-e/llm/routing-policy.js +255 -25
  105. package/template/wall-e/llm/tool-call-validator.js +156 -0
  106. package/template/wall-e/skills/skill-dispatch-decision.js +82 -2
  107. package/template/wall-e/skills/skill-planner.js +72 -5
  108. package/template/wall-e/tools/command-registry.js +34 -0
  109. package/template/wall-e/training/gemma-e4b-qlora.js +197 -1
  110. package/template/wall-e/training/mlx_lora_launch.py +196 -0
  111. package/template/wall-e/training/training-coexistence.sh +87 -0
  112. package/template/wall-e/training/training-mem-guard.sh +76 -0
@@ -0,0 +1,493 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>CTM Resume UX — Redesign Proposal</title>
7
+ <style>
8
+ :root{
9
+ --bg:#0f1115; --panel:#171a21; --panel2:#1d212b; --ink:#e6e9ef; --muted:#9aa3b2;
10
+ --line:#2a2f3a; --accent:#5b9cff; --good:#3ecf8e; --warn:#ffb454; --bad:#ff6b6b;
11
+ --run:#3ecf8e; --needs:#ffb454; --idle:#7d869c; --resume:#5b9cff; --review:#b388ff;
12
+ --mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
13
+ --sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
14
+ }
15
+ *{box-sizing:border-box}
16
+ html{scroll-behavior:smooth}
17
+ body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--sans);line-height:1.6;font-size:16px}
18
+ .wrap{max-width:1080px;margin:0 auto;padding:0 24px 120px}
19
+ header.hero{padding:56px 24px 40px;border-bottom:1px solid var(--line);background:
20
+ radial-gradient(900px 300px at 20% -10%, rgba(91,156,255,.18), transparent 60%),
21
+ radial-gradient(700px 260px at 90% -20%, rgba(62,207,142,.14), transparent 60%);}
22
+ .hero .wrap{padding-bottom:0}
23
+ .eyebrow{font-family:var(--mono);font-size:12px;letter-spacing:.18em;text-transform:uppercase;color:var(--accent);margin:0 0 12px}
24
+ h1{font-size:38px;line-height:1.15;margin:0 0 12px;font-weight:700;letter-spacing:-.02em}
25
+ .sub{font-size:18px;color:var(--muted);max-width:780px;margin:0}
26
+ .meta{margin-top:20px;display:flex;gap:10px;flex-wrap:wrap}
27
+ .pill{font-family:var(--mono);font-size:12px;border:1px solid var(--line);background:var(--panel);
28
+ padding:5px 10px;border-radius:999px;color:var(--muted)}
29
+ nav.toc{position:sticky;top:0;z-index:20;background:rgba(15,17,21,.88);backdrop-filter:blur(8px);
30
+ border-bottom:1px solid var(--line);padding:10px 0}
31
+ nav.toc .wrap{padding-top:0;padding-bottom:0;display:flex;gap:6px;flex-wrap:wrap}
32
+ nav.toc a{font-size:13px;color:var(--muted);text-decoration:none;padding:6px 10px;border-radius:8px;white-space:nowrap}
33
+ nav.toc a:hover{color:var(--ink);background:var(--panel)}
34
+ section{padding-top:48px}
35
+ h2{font-size:26px;margin:0 0 6px;letter-spacing:-.01em}
36
+ h2 .num{font-family:var(--mono);color:var(--accent);font-size:18px;margin-right:10px}
37
+ h3{font-size:18px;margin:28px 0 8px;color:var(--ink)}
38
+ p{margin:12px 0}
39
+ .lead{font-size:17px;color:#cbd2de}
40
+ code,kbd{font-family:var(--mono);font-size:.88em;background:var(--panel2);padding:1px 6px;border-radius:5px;border:1px solid var(--line)}
41
+ .card{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:20px 22px;margin:18px 0}
42
+ .card.tight{padding:14px 18px}
43
+ .grid2{display:grid;grid-template-columns:1fr 1fr;gap:16px}
44
+ .grid3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px}
45
+ @media(max-width:780px){.grid2,.grid3{grid-template-columns:1fr}}
46
+ .callout{border-left:3px solid var(--accent);background:var(--panel2);padding:14px 18px;border-radius:0 10px 10px 0;margin:16px 0}
47
+ .callout.bad{border-color:var(--bad)} .callout.good{border-color:var(--good)} .callout.warn{border-color:var(--warn)}
48
+ .callout h4{margin:0 0 6px;font-size:14px;letter-spacing:.04em;text-transform:uppercase;color:var(--muted)}
49
+ .tag{display:inline-block;font-family:var(--mono);font-size:11px;padding:2px 8px;border-radius:6px;border:1px solid var(--line);color:var(--muted)}
50
+ .tag.run{color:var(--run);border-color:rgba(62,207,142,.4)}
51
+ .tag.needs{color:var(--needs);border-color:rgba(255,180,84,.4)}
52
+ .tag.idle{color:var(--idle)}
53
+ .tag.resume{color:var(--resume);border-color:rgba(91,156,255,.4)}
54
+ .tag.review{color:var(--review);border-color:rgba(179,136,255,.4)}
55
+ .tag.srv{color:#7fd3ff;border-color:rgba(127,211,255,.3)}
56
+ .tag.cli{color:#ffd27f;border-color:rgba(255,210,127,.3)}
57
+ table{width:100%;border-collapse:collapse;font-size:13.5px;margin:12px 0}
58
+ th,td{text-align:left;padding:9px 10px;border-bottom:1px solid var(--line);vertical-align:top}
59
+ th{color:var(--muted);font-weight:600;font-size:12px;letter-spacing:.04em;text-transform:uppercase;position:sticky;top:42px;background:var(--panel)}
60
+ tbody tr:hover{background:var(--panel2)}
61
+ td code{font-size:12px;white-space:nowrap}
62
+ details{border:1px solid var(--line);border-radius:12px;background:var(--panel);margin:16px 0;overflow:hidden}
63
+ details>summary{cursor:pointer;padding:14px 18px;font-weight:600;list-style:none;display:flex;align-items:center;gap:10px}
64
+ details>summary::-webkit-details-marker{display:none}
65
+ details>summary::before{content:"▶";color:var(--accent);font-size:11px;transition:transform .15s}
66
+ details[open]>summary::before{transform:rotate(90deg)}
67
+ details .body{padding:0 18px 16px;border-top:1px solid var(--line)}
68
+ .legend{display:flex;gap:14px;flex-wrap:wrap;margin:10px 0;font-size:13px;color:var(--muted)}
69
+ .legend span{display:inline-flex;align-items:center;gap:6px}
70
+ .dot{width:10px;height:10px;border-radius:3px;display:inline-block}
71
+ .ref{font-family:var(--mono);font-size:12px;color:var(--muted)}
72
+ ul.clean{margin:10px 0;padding-left:20px} ul.clean li{margin:6px 0}
73
+ .kicker{font-family:var(--mono);font-size:12px;color:var(--accent);text-transform:uppercase;letter-spacing:.14em;margin:0 0 4px}
74
+ .footnote{color:var(--muted);font-size:13px}
75
+ a.src{color:var(--accent);text-decoration:none;border-bottom:1px dotted rgba(91,156,255,.5)}
76
+ a.src:hover{border-bottom-style:solid}
77
+ hr.sep{border:0;border-top:1px solid var(--line);margin:40px 0}
78
+ .phase{display:flex;gap:14px;margin:14px 0;align-items:flex-start}
79
+ .phase .badge{flex:0 0 auto;font-family:var(--mono);font-weight:700;font-size:13px;width:40px;height:40px;border-radius:10px;
80
+ display:grid;place-items:center;background:var(--panel2);border:1px solid var(--line);color:var(--accent)}
81
+ .phase .pbody{flex:1}
82
+ .phase h4{margin:2px 0 4px;font-size:16px}
83
+ .num-stat{display:flex;gap:18px;flex-wrap:wrap;margin:8px 0}
84
+ .num-stat .n{font-size:30px;font-weight:700;line-height:1;color:var(--bad)}
85
+ .num-stat .l{font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em}
86
+ .num-stat>div{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:14px 16px;min-width:130px}
87
+ /* resume-doc specifics */
88
+ pre.ascii{font-family:var(--mono);font-size:12.5px;line-height:1.45;background:#0b0d11;border:1px solid var(--line);
89
+ border-radius:12px;padding:16px 18px;overflow:auto;color:#cbd2de;margin:14px 0}
90
+ .term{font-family:var(--mono);font-size:12.5px;line-height:1.5;background:#0b0d11;border:1px solid var(--line);
91
+ border-radius:12px;overflow:hidden;margin:14px 0}
92
+ .term .bar{display:flex;align-items:center;gap:8px;padding:8px 12px;background:#13161c;border-bottom:1px solid var(--line);
93
+ font-size:11.5px;color:var(--muted)}
94
+ .term .bar .dots{display:flex;gap:5px}
95
+ .term .bar .dots i{width:9px;height:9px;border-radius:50%;display:inline-block}
96
+ .term .body{padding:14px 16px;white-space:pre-wrap;color:#cbd2de}
97
+ .term .dim{color:#5a6475}
98
+ .term .blank{color:#3a4150}
99
+ .term .foot{color:var(--accent)}
100
+ .term .skh{color:var(--resume);font-weight:600}
101
+ @keyframes blink{0%,100%{opacity:.45}50%{opacity:1}}
102
+ .pulse{animation:blink 1.6s ease-in-out infinite}
103
+ .badtag,.goodtag{font-family:var(--mono);font-size:11px;padding:2px 8px;border-radius:6px}
104
+ .badtag{color:var(--bad);border:1px solid rgba(255,107,107,.4)}
105
+ .goodtag{color:var(--good);border:1px solid rgba(62,207,142,.4)}
106
+ .statebox{display:inline-block;font-family:var(--mono);font-size:12px;padding:4px 10px;border-radius:8px;
107
+ border:1px solid var(--line);background:var(--panel2);color:var(--ink);margin:2px}
108
+ .statebox.live{color:var(--run);border-color:rgba(62,207,142,.5)}
109
+ .statebox.slow{color:var(--warn);border-color:rgba(255,180,84,.5)}
110
+ .statebox.err{color:var(--bad);border-color:rgba(255,107,107,.5)}
111
+ .arrow{color:var(--accent);font-family:var(--mono)}
112
+ </style>
113
+ </head>
114
+ <body>
115
+
116
+ <header class="hero">
117
+ <div class="wrap">
118
+ <p class="eyebrow">CTM · Design Proposal · 2026-06-23</p>
119
+ <h1>Resuming, Redesigned</h1>
120
+ <p class="sub">Reopen a Claude Code or Codex session and you wait — staring at a blank terminal or a lone <em style="color:var(--resume)">Resuming session…</em> line — with no idea what's happening or how long it will take. Yet the conversation you're resuming is <strong>already composed on the server</strong>. This document maps the current resume mechanism for both agents, explains why it feels broken, and proposes a single-source-of-truth <strong>skeleton-preview</strong> redesign that shows you your work instantly.</p>
121
+ <div class="meta">
122
+ <span class="pill">Resume UX</span>
123
+ <span class="pill">Claude Code + Codex</span>
124
+ <span class="pill">Skeleton preview</span>
125
+ <span class="pill">One resume state machine</span>
126
+ <span class="pill">Proposal — review before implementation</span>
127
+ </div>
128
+ </div>
129
+ </header>
130
+
131
+ <nav class="toc">
132
+ <div class="wrap">
133
+ <a href="#today">1 · What you see today</a>
134
+ <a href="#current">2 · Current mechanism</a>
135
+ <a href="#problems">3 · Why it's bad</a>
136
+ <a href="#principles">4 · Principles</a>
137
+ <a href="#design">5 · Proposed design</a>
138
+ <a href="#plan">6 · Implementation plan</a>
139
+ <a href="#verify">7 · Verification</a>
140
+ </div>
141
+ </nav>
142
+
143
+ <div class="wrap">
144
+
145
+ <!-- ============================================================ -->
146
+ <section id="today">
147
+ <p class="kicker">The experience</p>
148
+ <h2><span class="num">1</span>What you see today</h2>
149
+ <p class="lead">Three things go wrong, and they compound.</p>
150
+
151
+ <div class="num-stat">
152
+ <div><div class="n">~10</div><div class="l">functions own the resume UI</div></div>
153
+ <div><div class="n">4</div><div class="l">redundant "is resuming" flags</div></div>
154
+ <div><div class="n">2</div><div class="l">separate overlay systems</div></div>
155
+ <div><div class="n">3</div><div class="l">places guarding one blank-page race</div></div>
156
+ </div>
157
+
158
+ <div class="grid3">
159
+ <div class="card tight">
160
+ <span class="badtag">BLANK</span>
161
+ <h3 style="margin-top:10px">A blank page</h3>
162
+ <p class="footnote">Large Codex sessions go silent for minutes while the rollout loads into memory. A blank snapshot can slip through and wipe the loading state, leaving an empty terminal until the load finishes.</p>
163
+ </div>
164
+ <div class="card tight">
165
+ <span class="badtag">SPINNER</span>
166
+ <h3 style="margin-top:10px">A lonely "Resuming…"</h3>
167
+ <p class="footnote">A placeholder line plus a pulsing overlay — both saying the same thing — float over emptiness. No preview of the conversation, no sense of progress.</p>
168
+ </div>
169
+ <div class="card tight">
170
+ <span class="badtag">JITTER</span>
171
+ <h3 style="margin-top:10px">An unpredictable wait</h3>
172
+ <p class="footnote">The wait stutters through a chain of timers (250–2500&nbsp;ms, 600&nbsp;ms, 8×700&nbsp;ms, 15&nbsp;s). Irregular delay is harder to tolerate than a steady one.</p>
173
+ </div>
174
+ </div>
175
+
176
+ <div class="callout">
177
+ <h4>The core irony</h4>
178
+ <p style="margin:0">The server has <strong>already rendered the prior conversation</strong> by the time the user is staring at nothing. For Codex it composes the rollout history (<code>_composeCodexRolloutResp</code>); for Claude it holds the last terminal snapshot (<code>_readScrollbackSnapshotForRestore</code>). We then <em>hide</em> that content behind a spinner instead of showing it. The fix is mostly about <strong>presenting what we already have</strong>.</p>
179
+ </div>
180
+ </section>
181
+
182
+ <!-- ============================================================ -->
183
+ <section id="current">
184
+ <p class="kicker">How it works now</p>
185
+ <h2><span class="num">2</span>Current mechanism</h2>
186
+ <p>Both agents share a presentation layer but diverge in how prior content is produced. Here is the end-to-end flow, traced through <code>server.js</code>, <code>public/index.html</code>, and <code>public/js/state-sync-client.js</code>.</p>
187
+
188
+ <h3>The shared flow</h3>
189
+ <pre class="ascii">User clicks a session tab
190
+ │ <span style="color:var(--muted)">activateTab(id) — index.html:21766</span>
191
+
192
+ Terminal div becomes visible (EMPTY)
193
+ ├─ if resuming &amp; no content → _writeResumingPlaceholder() <span style="color:var(--muted)">"⟳ Resuming session…" written INTO the xterm buffer</span>
194
+ └─ _showLoadingOverlay({resuming:true}) <span style="color:var(--muted)">pulsing "Resuming session…" DOM overlay on top</span>
195
+
196
+ Server handleCreate() spawns the PTY <span style="color:var(--muted)">server.js:24151</span>
197
+ ├─ resume preflight + restore-from-.bak
198
+ ├─ _readScrollbackSnapshotForRestore() <span style="color:var(--muted)">seeds the headless mirror</span>
199
+ └─ Codex large rollout → stamp _resumeLoading=true + 10-min timeout <span style="color:var(--muted)">server.js:24885</span>
200
+
201
+ 'created' sent to client (does NOT wait for agent output)
202
+
203
+ Client requests a fresh snapshot + seeds scrollback
204
+ ├─ 600 ms settle, then up to 8 × 700 ms retries <span style="color:var(--muted)">_maybeSeedStateSyncResumeScrollback</span>
205
+ └─ DURING Codex silent-load this seeding is SKIPPED (mirror is blank)
206
+
207
+ Live frames flow → overlay dismissed when "meaningful content" (&gt;80 chars) appears
208
+ └─ unless _resumeLoadingDismissBlocked() holds it (Codex only)</pre>
209
+
210
+ <div class="grid2">
211
+ <div class="card">
212
+ <span class="tag resume">CLAUDE CODE</span>
213
+ <h3 style="margin-top:8px">Prior content = terminal snapshot</h3>
214
+ <ul class="clean footnote">
215
+ <li>A binary terminal snapshot from the last session is read at spawn and seeded into the headless mirror.</li>
216
+ <li>The state-sync client then re-requests a scrollback seed at the live terminal's dimensions (600 ms + retries) so history paints under the live frames.</li>
217
+ <li>Server <em>holds</em> live frames during the repaint window: <code>250 ms</code> settle, <code>2500 ms</code> cap (<code>server.js:19776</code>).</li>
218
+ <li>Typically ready in a few seconds — but the seed path is the source of the documented "scatter / ghost / blank" rendering family.</li>
219
+ </ul>
220
+ </div>
221
+ <div class="card">
222
+ <span class="tag" style="color:#ffd27f;border-color:rgba(255,210,127,.3)">CODEX</span>
223
+ <h3 style="margin-top:8px">Prior content = composed rollout history</h3>
224
+ <ul class="clean footnote">
225
+ <li>History is composed from DB rows or a JSONL tail (≤16 MB), capped to ~1000 lines, rendered to ANSI, de-duplicated against the live viewport: <code>_composeCodexRolloutResp</code> (<code>server.js:27924</code>), <code>_composeCodexHistorySnapshot</code>.</li>
226
+ <li>The composed result is cached for the session's lifetime — cheap to re-emit on tab switch / reflow.</li>
227
+ <li>But a multi-GB rollout takes <strong>minutes</strong> of silent CPU before the first byte — and that's exactly when the blank-page race bites.</li>
228
+ </ul>
229
+ </div>
230
+ </div>
231
+
232
+ <h3>The timing chain (where the jitter comes from)</h3>
233
+ <table>
234
+ <thead><tr><th>Stage</th><th>Delay</th><th>Where</th><th>Purpose</th></tr></thead>
235
+ <tbody>
236
+ <tr><td>Scrollback seed — first attempt</td><td><code>600 ms</code></td><td>index.html (state-sync seed)</td><td>Wait for server repaint + resize to settle</td></tr>
237
+ <tr><td>Scrollback seed — retries</td><td><code>700 ms × 8</code></td><td><code>STATE_SYNC_RESUME_SEED_*</code></td><td>Re-request until a matched-width seed applies</td></tr>
238
+ <tr><td>Server frame-hold settle</td><td><code>250 ms</code></td><td><code>server.js:19776</code></td><td>Hold live frames until repaint quiets</td></tr>
239
+ <tr><td>Server frame-hold cap</td><td><code>2500 ms</code></td><td><code>server.js:19778</code></td><td>Release even if the agent streams continuously</td></tr>
240
+ <tr><td>Overlay "Still resuming…" degrade</td><td><code>15 s</code></td><td>index.html (degrade tick)</td><td>Change copy when content hasn't arrived</td></tr>
241
+ <tr><td>Codex degraded re-check loop</td><td><code>2 s</code></td><td>index.html (degrade tick)</td><td>Update elapsed timer / re-arm overlay</td></tr>
242
+ <tr><td>Snapshot request timeout</td><td><code>3 s</code></td><td><code>activateTab</code></td><td>Retry the snapshot if no response</td></tr>
243
+ <tr><td>Blank watchdog → auto-reflow</td><td><code>6 s</code></td><td><code>activateTab</code></td><td>Force a reflow if still blank</td></tr>
244
+ <tr><td>Codex resume-loading safety timeout</td><td><code>10 min</code></td><td><code>server.js:24129</code></td><td>Auto-clear the silent-load flag</td></tr>
245
+ </tbody>
246
+ </table>
247
+
248
+ <details>
249
+ <summary>The "is this session resuming?" signal sprawl</summary>
250
+ <div class="body">
251
+ <p>There is no single answer. Each consumer checks a different subset:</p>
252
+ <ul class="clean footnote">
253
+ <li><strong>Client flags:</strong> <code>_restoreDeferred</code>, <code>_restoreResuming</code>, <code>_restoreStarting</code>, <code>_restoreDeferredStarting</code> — plus <code>s.meta.*</code> mirrors of each.</li>
254
+ <li><strong>Client timestamp:</strong> <code>_resumeLoadStartedAt</code> (the anchor that the blank-page race zeroes).</li>
255
+ <li><strong>Server flag:</strong> <code>session._resumeLoading</code> + <code>_resumeLoadingFileSize</code>, broadcast in the session list.</li>
256
+ <li><strong>Codex heuristics:</strong> <code>_isLargeCodexResumeStillLoading</code>, <code>_codexLargeResumeLoadingCore</code>, <code>_resumeLoadingDismissBlocked</code>.</li>
257
+ <li><strong>Two overlays:</strong> <code>#reconnect-overlay</code> (WebSocket drop) and the dynamic <code>.snapshot-loading-overlay</code> (resume) — similarly named, conceptually conflated.</li>
258
+ </ul>
259
+ </div>
260
+ </details>
261
+ </section>
262
+
263
+ <!-- ============================================================ -->
264
+ <section id="problems">
265
+ <p class="kicker">Diagnosis</p>
266
+ <h2><span class="num">3</span>Why it's bad — four findings</h2>
267
+
268
+ <div class="card">
269
+ <h3 style="margin-top:0">① We show a spinner, but we already have the content</h3>
270
+ <p>The user sees a blank xterm + the <code>⟳ Resuming session…</code> placeholder + a pulsing <code>Resuming session…</code> overlay. Meanwhile the prior conversation is composed and cached server-side. We gate that real content behind the 600 ms + 8×700 ms seed retries, and <em>actively skip it</em> during Codex silent-load. The content the user is resuming sits ready while they stare at nothing.</p>
271
+ <p class="ref">index.html — _writeResumingPlaceholder, _showLoadingOverlay · server.js:27924 _composeCodexRolloutResp · state-sync seed STATE_SYNC_RESUME_SEED_*</p>
272
+ </div>
273
+
274
+ <div class="card">
275
+ <h3 style="margin-top:0">② No single source of truth — the logic is scattered</h3>
276
+ <p>"Is this resuming, and what should the user see?" is answered by 4 redundant client flags (each mirrored on <code>s.meta</code>), a server flag, a client timestamp, and a cluster of Codex-only heuristics — spread across ~10 functions and two overlay systems. Double "Resuming" feedback (placeholder <em>and</em> overlay) shows at once. Every past fix added another guard rather than removing the ambiguity.</p>
277
+ <p class="ref">4 flags + meta mirrors · 2 overlays · _isLargeCodexResumeStillLoading / _renderCodexResumeLoadingOverlay / _resumeLoadingDismissBlocked</p>
278
+ </div>
279
+
280
+ <div class="card">
281
+ <h3 style="margin-top:0">③ The blank-page race</h3>
282
+ <p>A blank snapshot applied mid-resume calls <code>_dismissLoadingOverlay</code>, which zeros <code>_resumeLoadStartedAt</code>. Once that anchor is zero, <code>_isLargeCodexResumeStillLoading()</code> returns <code>false</code> <em>forever</em> → the overlay never re-arms → blank for minutes until the load finishes. It's defended in three fragile places (server <code>_serializedSnapshotIsBlank</code> ×2 + client <code>_resumeLoadingDismissBlocked</code>); miss any one and the bug returns. This is the recurring "overlay then blank for minutes" regression.</p>
283
+ <p class="ref">server.js:20476 / 20553 _serializedSnapshotIsBlank · index.html _resumeLoadingDismissBlocked · anchor _resumeLoadStartedAt</p>
284
+ </div>
285
+
286
+ <div class="card">
287
+ <h3 style="margin-top:0">④ Jitter, not steady progress</h3>
288
+ <p>The wait stutters through 250–2500 ms (frame-hold), 600 ms (seed), 700 ms×8 (retries), 15 s (degrade), 2 s (re-check). Large Codex shows only a bare elapsed counter for minutes with no staging or reassurance. Irregular delay (jitter) is measurably harder to tolerate than a longer but predictable one.</p>
289
+ </div>
290
+
291
+ <h3>What the research says</h3>
292
+ <div class="grid3">
293
+ <div class="card tight">
294
+ <h4 style="margin:0 0 6px;color:var(--muted)">Skeleton &gt; spinner</h4>
295
+ <p class="footnote" style="margin:0">Previewing the actual layout/content makes an identical wait feel 30–50% shorter. A 3 s skeleton ≈ a 1.5 s spinner in perceived time. Spinners create time-uncertainty; skeletons signal progress.</p>
296
+ </div>
297
+ <div class="card tight">
298
+ <h4 style="margin:0 0 6px;color:var(--muted)">Feedback within 100 ms</h4>
299
+ <p class="footnote" style="margin:0">Any user action should produce a visible response within ~100 ms. Spinners suit &lt;10 s blocking actions; past 10 s, show a determinate/staged indicator — not an open-ended spinner.</p>
300
+ </div>
301
+ <div class="card tight">
302
+ <h4 style="margin:0 0 6px;color:var(--muted)">Restore without a flash</h4>
303
+ <p class="footnote" style="margin:0">Terminal session-restore must not dump the buffer into scrollback or flash blank (Windows Terminal #17274). Jitter hurts more than steady latency — even sub-conscious jitter degrades the feel.</p>
304
+ </div>
305
+ </div>
306
+ <p class="footnote">Sources:
307
+ <a class="src" href="https://www.nngroup.com/articles/skeleton-screens/">NN/g — Skeleton Screens 101</a> ·
308
+ <a class="src" href="https://blog.logrocket.com/ux-design/skeleton-loading-screen-design/">LogRocket — Skeleton loading &amp; perceived performance</a> ·
309
+ <a class="src" href="https://www.onething.design/post/skeleton-screens-vs-loading-spinners">Onething — Skeletons vs spinners</a> ·
310
+ <a class="src" href="https://github.com/microsoft/terminal/issues/17274">microsoft/terminal #17274 — buffer restore without scrollback flash</a>
311
+ </p>
312
+ </section>
313
+
314
+ <!-- ============================================================ -->
315
+ <section id="principles">
316
+ <p class="kicker">What good looks like</p>
317
+ <h2><span class="num">4</span>Three principles</h2>
318
+ <div class="grid3">
319
+ <div class="card">
320
+ <h3 style="margin-top:0">Content-wait, not spinner-wait</h3>
321
+ <p class="footnote">If we have the prior conversation (we do), show it immediately as a dimmed, read-only skeleton. The user reads "where I left off" instead of watching a spinner. The live terminal fades in on top when ready.</p>
322
+ </div>
323
+ <div class="card">
324
+ <h3 style="margin-top:0">One source of truth</h3>
325
+ <p class="footnote">A single pure reducer derives the resume state from a small set of signals. Every consumer (overlay, skeleton, sidebar tag, input-enable) reads from it. No more flag sprawl, no more per-consumer disagreement.</p>
326
+ </div>
327
+ <div class="card">
328
+ <h3 style="margin-top:0">Eliminate the race, don't guard it</h3>
329
+ <p class="footnote">Make the state machine the <em>only</em> thing that can dismiss. A blank snapshot then simply fails to advance state — it can't wipe the anchor. The bug becomes structurally impossible, not defended in three places.</p>
330
+ </div>
331
+ </div>
332
+ </section>
333
+
334
+ <!-- ============================================================ -->
335
+ <section id="design">
336
+ <p class="kicker">The proposal</p>
337
+ <h2><span class="num">5</span>Proposed design</h2>
338
+ <p class="lead">Turn resume into a content-wait: show the prior conversation as a skeleton instantly, governed by one resume state machine, and cross-fade to the live terminal when it paints.</p>
339
+
340
+ <h3>A · One resume state machine — <code>public/js/resume-state.js</code></h3>
341
+ <p>A new pure module mirroring the existing <code>session-phase.js</code> SSOT pattern. A single reducer maps signals to everything the UI needs:</p>
342
+ <pre class="ascii">deriveResumeState(signals) → { state, showSkeleton, showSlowProgress, headerText, inputEnabled, dismissBlocked }</pre>
343
+ <p>States, and the transitions between them:</p>
344
+ <p>
345
+ <span class="statebox">idle</span><span class="arrow"> → </span>
346
+ <span class="statebox">spawning</span><span class="arrow"> → </span>
347
+ <span class="statebox" style="color:var(--resume);border-color:rgba(91,156,255,.5)">preview</span><span class="arrow"> → </span>
348
+ <span class="statebox">live-pending</span><span class="arrow"> → </span>
349
+ <span class="statebox live">live</span>
350
+ &nbsp;&nbsp;<span class="arrow">⤷ </span><span class="statebox slow">slow</span> &nbsp;<span class="arrow">⤷ </span><span class="statebox err">error</span>
351
+ </p>
352
+ <table>
353
+ <thead><tr><th>State</th><th>Meaning</th><th>User sees</th></tr></thead>
354
+ <tbody>
355
+ <tr><td><code>spawning</code></td><td>PTY requested, no history yet</td><td>Header + subtle pulse (≤100 ms window)</td></tr>
356
+ <tr><td><code>preview</code></td><td>Prior conversation available</td><td><strong>Dimmed skeleton of the conversation</strong> + "reconnecting live session…"</td></tr>
357
+ <tr><td><code>live-pending</code></td><td>Live frames arriving, not yet settled</td><td>Skeleton holds; live paints underneath</td></tr>
358
+ <tr><td><code>live</code></td><td>First settled live paint</td><td>Cross-fade out; input enabled</td></tr>
359
+ <tr><td><code>slow</code></td><td>Genuine large-Codex silent-load</td><td>Skeleton + steady staged progress + escape hatches</td></tr>
360
+ <tr><td><code>error</code></td><td>Preflight/spawn failed</td><td>Clear error + "Start fresh here"</td></tr>
361
+ </tbody>
362
+ </table>
363
+ <p class="footnote">Signals in: <code>agentType, hasHistorySnapshot, serverResumeLoading, firstLiveByteAt, meaningfulContent, fileSize, elapsedMs, preflightError</code>. This single function subsumes the 4 flags, <code>_isLargeCodexResumeStillLoading</code>, and <code>_resumeLoadingDismissBlocked</code>.</p>
364
+
365
+ <h3>B · Skeleton as a dedicated preview layer (decoupled from the xterm buffer)</h3>
366
+ <p>The skeleton renders the already-composed prior content in a <strong>separate styled layer above the terminal</strong> — <em>not</em> by seeding the live xterm buffer. This is deliberate: seeding history into the live buffer is the source of the documented scatter / spinner-ghost / blank-on-seed bugs. A decoupled layer sidesteps that family entirely and gives a clean cross-fade. The existing xterm seed still runs underneath; the user watches the preview until the live keyframe settles, then the preview fades and input enables.</p>
367
+
368
+ <div class="grid2">
369
+ <div>
370
+ <p class="footnote" style="margin-bottom:6px"><strong>Claude resume — the moment you click</strong></p>
371
+ <div class="term">
372
+ <div class="bar"><span class="dots"><i style="background:#ff5f57"></i><i style="background:#febc2e"></i><i style="background:#28c840"></i></span><span>Resuming: refactor auth module</span></div>
373
+ <div class="body"><span class="skh">┌─ Resuming where you left off ───────────────┐</span>
374
+ <span class="dim">› you: add rate limiting to /login</span>
375
+ <span class="dim">● claude: I'll add a token-bucket limiter…</span>
376
+ <span class="dim"> ⎿ edited auth.js, middleware.js (+47 −3)</span>
377
+ <span class="dim">› you: now add tests for the limiter</span>
378
+ <span class="dim">● claude: Added 6 cases to auth.test.js…</span>
379
+
380
+ <span class="foot pulse">░░░ reconnecting live session… ░░░</span></div>
381
+ </div>
382
+ <p class="footnote">Dimmed, read-only, instant. Input disabled until live.</p>
383
+ </div>
384
+ <div>
385
+ <p class="footnote" style="margin-bottom:6px"><strong>Large Codex — the honest slow case</strong></p>
386
+ <div class="term">
387
+ <div class="bar"><span class="dots"><i style="background:#ff5f57"></i><i style="background:#febc2e"></i><i style="background:#28c840"></i></span><span>Resuming: data-pipeline migration</span></div>
388
+ <div class="body"><span class="skh">┌─ Resuming where you left off ───────────────┐</span>
389
+ <span class="dim">› you: migrate the ingest job to the new schema</span>
390
+ <span class="dim">• codex: ran migration, 3 tables altered…</span>
391
+ <span class="dim"> ⎿ schema.sql, ingest.py changed</span>
392
+
393
+ <span style="color:var(--warn)">⏳ Loading 4.2 GB session into memory — this</span>
394
+ <span style="color:var(--warn)"> can take a few minutes. (elapsed 0:48)</span>
395
+
396
+ <span class="foot">[ Start fresh here ] [ Reload render ]</span></div>
397
+ </div>
398
+ <p class="footnote">Steady staged copy + existing escape hatches. No bare counter over a void.</p>
399
+ </div>
400
+ </div>
401
+
402
+ <h3>C · Dismissal owned solely by the state machine</h3>
403
+ <p>Individual snapshot handlers stop calling <code>_dismissLoadingOverlay</code> and stop zeroing the anchor. The skeleton is removed only when the reducer transitions to <code>live</code> (first settled meaningful paint). A blank snapshot can no longer wipe anything — it simply doesn't advance the state, so the session stays in <code>preview</code>/<code>slow</code>. The three scattered blank-guards collapse into this one rule.</p>
404
+
405
+ <h3>D · Consolidation</h3>
406
+ <ul class="clean">
407
+ <li>Merge the terminal placeholder + the resume overlay into the single skeleton component.</li>
408
+ <li>Centralize every copy string in one map (no more hard-coded "Resuming…" / "Still resuming…" scattered inline).</li>
409
+ <li>Keep <code>#reconnect-overlay</code> as a clearly separate WebSocket-reconnect concern; rename to remove the conceptual conflation.</li>
410
+ <li>Collapse the 4 client flags (+ <code>meta</code> mirrors) into the reducer's input signals.</li>
411
+ </ul>
412
+ </section>
413
+
414
+ <!-- ============================================================ -->
415
+ <section id="plan">
416
+ <p class="kicker">Execution</p>
417
+ <h2><span class="num">6</span>Implementation plan</h2>
418
+ <p>Four phases, smallest-risk first. Each is independently shippable and validated on a <code>/ctm-dev</code> instance (never primary).</p>
419
+
420
+ <div class="phase">
421
+ <div class="badge">0</div>
422
+ <div class="pbody">
423
+ <h4>SSOT module + tests — no behavior change</h4>
424
+ <p>Add <code>public/js/resume-state.js</code> (pure reducer, states, centralized strings) and <code>tests/resume-state.test.js</code>. Mirror the structure and test style of <code>session-phase.js</code> / <code>session-phase.test.js</code>. Nothing wired in yet.</p>
425
+ <p class="ref">NEW public/js/resume-state.js · NEW tests/resume-state.test.js</p>
426
+ </div>
427
+ </div>
428
+
429
+ <div class="phase">
430
+ <div class="badge">1</div>
431
+ <div class="pbody">
432
+ <h4>Consolidate + kill the race</h4>
433
+ <p>Route the 4 client flags + server <code>_resumeLoading</code> + <code>_resumeLoadStartedAt</code> through <code>deriveResumeState</code>. Make the state machine the <strong>sole owner of dismissal</strong> — no handler zeroes the anchor. Merge placeholder + overlay into one component; centralize copy. Port the blank-page-race and dismiss-block scenarios into the new test so the regression can't return.</p>
434
+ <p class="ref">index.html — activateTab, _showLoadingOverlay/_dismissLoadingOverlay, _writeResumingPlaceholder, the Codex overlay cluster</p>
435
+ </div>
436
+ </div>
437
+
438
+ <div class="phase">
439
+ <div class="badge">2</div>
440
+ <div class="pbody">
441
+ <h4>Skeleton preview — the win</h4>
442
+ <p>New preview layer that renders the already-composed prior content immediately: Codex reuses the cached rollout compose; Claude reads the scrollback snapshot at spawn and sends an early <code>restore-preview</code> message. Dimmed, read-only, header <code>Resuming: &lt;title&gt;</code>, footer <code>reconnecting live session…</code>. Cross-fade to the live xterm on the first settled paint. Validate against the render-test suite to confirm the decoupled layer avoids the seed-corruption family.</p>
443
+ <p class="ref">server.js — handleCreate resume path, _composeCodexRolloutResp, _readScrollbackSnapshotForRestore · lib/codex-rollout-snapshot.js · lib/session-history.js · state-sync-client.js</p>
444
+ </div>
445
+ </div>
446
+
447
+ <div class="phase">
448
+ <div class="badge">3</div>
449
+ <div class="pbody">
450
+ <h4>Slow-case polish + de-jitter</h4>
451
+ <p>Steady staged progress for multi-GB Codex (a determinate hint if one is derivable from bytes-read). Drop the 600 ms pre-preview gate — we already have content to show. Keep the server frame-hold, but now it's invisible because the skeleton covers the repaint window.</p>
452
+ <p class="ref">server.js frame-hold (19776) · seed timing constants</p>
453
+ </div>
454
+ </div>
455
+
456
+ <h3>Files touched</h3>
457
+ <table>
458
+ <thead><tr><th>File</th><th>Role</th></tr></thead>
459
+ <tbody>
460
+ <tr><td><code>public/js/resume-state.js</code> <span class="goodtag">NEW</span></td><td>The reducer / SSOT for resume state + copy</td></tr>
461
+ <tr><td><code>tests/resume-state.test.js</code> <span class="goodtag">NEW</span></td><td>Unit coverage incl. ported race/dismiss scenarios</td></tr>
462
+ <tr><td><code>public/index.html</code></td><td>activateTab, overlay + placeholder → skeleton layer, flags → signals</td></tr>
463
+ <tr><td><code>public/js/state-sync-client.js</code></td><td>Seed runs underneath; preview decoupled from it</td></tr>
464
+ <tr><td><code>server.js</code></td><td>handleCreate resume path, early restore-preview, blank-guard cleanup, frame-hold</td></tr>
465
+ <tr><td><code>lib/codex-rollout-snapshot.js</code>, <code>lib/session-history.js</code></td><td>Reused as-is for Codex history compose</td></tr>
466
+ </tbody>
467
+ </table>
468
+ </section>
469
+
470
+ <!-- ============================================================ -->
471
+ <section id="verify">
472
+ <p class="kicker">Proving it</p>
473
+ <h2><span class="num">7</span>Verification</h2>
474
+ <ol class="clean">
475
+ <li><strong>Unit:</strong> <code>cd claude-task-manager &amp;&amp; npm test</code> — <code>resume-state.test.js</code> green; every ported race/dismiss scenario passes.</li>
476
+ <li><strong>Dev only — never primary (3456/3457):</strong> a <code>/ctm-dev</code> instance on a random port. Resume a Claude session and a large Codex session; confirm the skeleton shows the prior conversation within ~100 ms, no blank window, clean cross-fade to live, input disabled until live.</li>
477
+ <li><strong>Render safety:</strong> run the <code>ctm-render-test</code> Playwright suite — no regression in the scrollback/seed rendering family.</li>
478
+ <li><strong>Differential:</strong> snapshot the resume visual states before/after on dev; any blank window is a regression to explain before merge.</li>
479
+ <li><strong>Deploy:</strong> do not restart primary; the user authorizes any primary deploy explicitly.</li>
480
+ </ol>
481
+
482
+ <div class="callout good">
483
+ <h4>The outcome</h4>
484
+ <p style="margin:0">Click a session and you immediately see the conversation you're resuming — dimmed, with a clear "reconnecting" footer — then the live terminal fades in. No blank page, no double "Resuming…", no jitter. One state machine decides it all, and the blank-page race is gone by construction.</p>
485
+ </div>
486
+
487
+ <hr class="sep">
488
+ <p class="footnote">CTM · Resume UX Redesign · design proposal, review before implementation · companion docs: <code>session-status-redesign.html</code>, <code>terminal-rendering-redesign.html</code>, <code>conversation-log-redesign.html</code>.</p>
489
+ </section>
490
+
491
+ </div>
492
+ </body>
493
+ </html>