create-walle 0.9.26 → 0.9.28

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 (45) hide show
  1. package/README.md +1 -0
  2. package/package.json +1 -1
  3. package/template/claude-task-manager/api-prompts.js +11 -6
  4. package/template/claude-task-manager/docs/session-status-redesign.html +554 -0
  5. package/template/claude-task-manager/docs/terminal-rendering-redesign.html +529 -0
  6. package/template/claude-task-manager/lib/flush-redraw-markers.js +72 -0
  7. package/template/claude-task-manager/lib/macos-capabilities.js +190 -0
  8. package/template/claude-task-manager/lib/session-messages-projection.js +224 -3
  9. package/template/claude-task-manager/lib/ttl-memo.js +61 -0
  10. package/template/claude-task-manager/public/index.html +892 -11
  11. package/template/claude-task-manager/public/js/activation-render-check.js +40 -2
  12. package/template/claude-task-manager/public/js/session-phase.js +370 -0
  13. package/template/claude-task-manager/public/js/setup.js +74 -1
  14. package/template/claude-task-manager/public/js/stream-view.js +56 -2
  15. package/template/claude-task-manager/server.js +643 -68
  16. package/template/claude-task-manager/workers/read-pool-worker.js +10 -0
  17. package/template/package.json +1 -1
  18. package/template/wall-e/agent.js +130 -24
  19. package/template/wall-e/api-walle.js +12 -1
  20. package/template/wall-e/brain.js +290 -4
  21. package/template/wall-e/chat.js +30 -25
  22. package/template/wall-e/coding/session-plan.js +79 -0
  23. package/template/wall-e/coding-orchestrator.js +9 -3
  24. package/template/wall-e/coding-prompts.js +10 -3
  25. package/template/wall-e/embeddings.js +192 -17
  26. package/template/wall-e/http/model-admin.js +109 -0
  27. package/template/wall-e/lib/event-loop-monitor.js +2 -2
  28. package/template/wall-e/lib/scheduler-worker-jobs.js +156 -121
  29. package/template/wall-e/lib/scheduler.js +226 -13
  30. package/template/wall-e/lib/worker-thread-pool.js +58 -4
  31. package/template/wall-e/llm/ollama-library.js +126 -0
  32. package/template/wall-e/llm/ollama.js +13 -0
  33. package/template/wall-e/llm/provider-backpressure.js +134 -0
  34. package/template/wall-e/llm/provider-health-state.js +24 -0
  35. package/template/wall-e/loops/backfill.js +43 -16
  36. package/template/wall-e/loops/initiative.js +1 -0
  37. package/template/wall-e/loops/think.js +38 -5
  38. package/template/wall-e/mcp-server.js +20 -4
  39. package/template/wall-e/skills/skill-fallback.js +34 -1
  40. package/template/wall-e/skills/skill-planner.js +60 -2
  41. package/template/wall-e/sources/jsonl-utils.js +84 -11
  42. package/template/wall-e/telemetry.js +42 -7
  43. package/template/wall-e/tools/local-tools.js +16 -0
  44. package/template/wall-e/workers/runtime-worker.js +33 -1
  45. package/template/website/index.html +5 -0
@@ -20,16 +20,48 @@
20
20
  (function (global) {
21
21
  'use strict';
22
22
 
23
- // shouldScheduleCheck({ enabled, renderStableSince, timerPending }) -> boolean
23
+ function normalizeStableKey(value) {
24
+ if (value == null) return value == null ? null : value;
25
+ var t = typeof value;
26
+ if (t === 'number') return Number.isFinite(value) ? value : null;
27
+ if (t === 'string' || t === 'boolean') return value;
28
+ if (Array.isArray(value)) return value.map(normalizeStableKey);
29
+ if (t === 'object') {
30
+ var out = {};
31
+ Object.keys(value).sort().forEach(function (key) {
32
+ var normalized = normalizeStableKey(value[key]);
33
+ if (normalized !== undefined) out[key] = normalized;
34
+ });
35
+ return out;
36
+ }
37
+ return null;
38
+ }
39
+
40
+ function stableKeysEqual(a, b) {
41
+ try {
42
+ return JSON.stringify(normalizeStableKey(a)) === JSON.stringify(normalizeStableKey(b));
43
+ } catch (_) {
44
+ return false;
45
+ }
46
+ }
47
+
48
+ // shouldScheduleCheck({ enabled, renderStableSince, timerPending, stableKey, currentKey }) -> boolean
24
49
  // - enabled: the CTM_ACTIVATION_RENDER_CHECK flag (default on).
25
50
  // - renderStableSince: latch timestamp; truthy => already confirmed good for this
26
51
  // activation and nothing has changed since, so the proactive check is redundant.
27
52
  // - timerPending: a check is already armed for this activation (coalesce to one).
53
+ // - stableKey/currentKey: optional render-state key. When present, the latch suppresses
54
+ // only if it was recorded for the same activation/restoration/content state.
28
55
  function shouldScheduleCheck(opts) {
29
56
  var o = opts || {};
30
57
  if (!o.enabled) return false;
31
58
  if (o.timerPending) return false;
32
- if (o.renderStableSince) return false;
59
+ if (o.renderStableSince) {
60
+ if (Object.prototype.hasOwnProperty.call(o, 'currentKey')) {
61
+ return !stableKeysEqual(o.stableKey, o.currentKey);
62
+ }
63
+ return false;
64
+ }
33
65
  return true;
34
66
  }
35
67
 
@@ -43,11 +75,17 @@
43
75
  function markUnstable() {
44
76
  return 0;
45
77
  }
78
+ function markStableKey(currentKey) {
79
+ return normalizeStableKey(currentKey);
80
+ }
46
81
 
47
82
  var api = {
48
83
  shouldScheduleCheck: shouldScheduleCheck,
49
84
  markStable: markStable,
50
85
  markUnstable: markUnstable,
86
+ markStableKey: markStableKey,
87
+ normalizeStableKey: normalizeStableKey,
88
+ stableKeysEqual: stableKeysEqual,
51
89
  };
52
90
  global.ActivationRenderCheck = api;
53
91
  // Dual-environment: node tests require this same file.
@@ -0,0 +1,370 @@
1
+ 'use strict';
2
+
3
+ /*
4
+ * session-phase.js — the single source of truth for "what phase is this session in?"
5
+ *
6
+ * Redesign goal (see docs/session-status-redesign.html): replace the patch-on-patch
7
+ * web of ~40 heuristics across 5 engines with ONE model:
8
+ *
9
+ * 1. Raw signals are mapped to independent CONDITIONS, each carrying a CONFIDENCE
10
+ * tier ({value, at, source, confidence}). buildSessionConditions() does this.
11
+ * 2. derivePhase() is the ONLY producer of a phase string. It ranks conditions by
12
+ * confidence, never letting a weaker signal demote a stronger one.
13
+ *
14
+ * Two design rules encode the operator's guidance:
15
+ * - "Rank signals by confidence; don't trust a signal 100% if unsure." → CONFIDENCE
16
+ * tiers; PTY-silence and the garble-prone Codex "Working" footer are LOW.
17
+ * - "Don't miss events." → an authoritative `idle` (e.g. a Stop hook) may still be
18
+ * PROMOTED by strictly-newer liveness evidence, and every such promotion is logged
19
+ * via opts.log so a genuinely-missed lifecycle event can be learned from later.
20
+ *
21
+ * Isomorphic: same dual node/browser export as session-status-precedence.js, so the
22
+ * server and the client derive phases from identical code.
23
+ */
24
+
25
+ (function initSessionPhase(root, factory) {
26
+ if (typeof module === 'object' && module.exports) {
27
+ module.exports = factory();
28
+ } else {
29
+ root.SessionPhase = factory();
30
+ }
31
+ })(typeof globalThis !== 'undefined' ? globalThis : this, function buildSessionPhase() {
32
+ // ---- confidence tiers ---------------------------------------------------
33
+ const CONFIDENCE = { HIGH: 3, MEDIUM: 2, LOW: 1 };
34
+
35
+ // ---- freshness windows (one home for every TTL) -------------------------
36
+ const AUTHORITATIVE_STATUS_TTL_MS = 120000; // hook/telemetry verdict trusted for 2m
37
+ const SERVER_WORKING_TTL_MS = 10000;
38
+ const STREAM_STATUS_TTL_MS = 60000;
39
+ const RECENT_OUTPUT_TTL_MS = 5000;
40
+ const RECENT_INPUT_TTL_MS = 3000;
41
+ const CODEX_RUNNING_HOLD_MS = 15000;
42
+ const ACTIVE_TURN_MAX_QUIET_MS = 45 * 60 * 1000;
43
+ const WAITING_INPUT_STALE_MS = 30 * 60 * 1000;
44
+ const OBSERVED_TIME_FUTURE_SKEW_MS = 60 * 1000;
45
+
46
+ // ---- phases -------------------------------------------------------------
47
+ // Strength = priority WITHIN one confidence tier. An actionable prompt outranks
48
+ // an in-progress turn, which outranks a transient resume, etc.
49
+ const PHASE_STRENGTH = { needs_you: 5, resuming: 4, running: 3, review: 2, idle: 1 };
50
+ const PHASE_TEXT = {
51
+ running: 'Running',
52
+ needs_you: 'Needs You',
53
+ resuming: 'Resuming',
54
+ review: 'Review',
55
+ idle: 'Idle',
56
+ exited: 'Exited',
57
+ };
58
+ // Legacy status class the existing UI/CSS understands.
59
+ const PHASE_TO_CLS = {
60
+ running: 'running',
61
+ needs_you: 'waiting',
62
+ resuming: 'resuming',
63
+ review: 'review',
64
+ idle: 'idle',
65
+ exited: 'exited',
66
+ };
67
+ const LIVENESS_PHASES = new Set(['running', 'needs_you', 'resuming']);
68
+
69
+ function phaseToCls(phase) { return PHASE_TO_CLS[phase] || ''; }
70
+
71
+ // ---- time helpers (shared with the legacy module's semantics) -----------
72
+ function parseTimeMs(value, fallback = 0, parser) {
73
+ if (value == null || value === '') return fallback || 0;
74
+ if (typeof parser === 'function') {
75
+ const parsed = parser(value);
76
+ if (parsed) return parsed;
77
+ }
78
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
79
+ const parsed = Date.parse(String(value));
80
+ return Number.isFinite(parsed) ? parsed : (fallback || 0);
81
+ }
82
+
83
+ function normalizeObservedTimeMs(value, fallback = 0, parser, now = Date.now()) {
84
+ const parsed = parseTimeMs(value, fallback, parser);
85
+ if (!parsed) return 0;
86
+ if (parsed > now + OBSERVED_TIME_FUTURE_SKEW_MS) return 0; // impossible future → ignore
87
+ return parsed > now ? now : parsed; // small skew clamps to now
88
+ }
89
+
90
+ function isFresh(at, now, ttl) {
91
+ return Boolean(at && at <= now && (now - at) < ttl);
92
+ }
93
+
94
+ function isBlockingWaitingReason(reason) {
95
+ const text = String(reason || '').toLowerCase();
96
+ return text === 'approval' || text === 'choice' || text === 'plan' ||
97
+ /\bapproval\b/.test(text) || /\bpermission\b/.test(text);
98
+ }
99
+
100
+ function normalizeStreamPhase(status) {
101
+ const text = String(status || '').toLowerCase().replace(/[-\s]+/g, '_');
102
+ if (!text) return '';
103
+ if (text === 'busy' || text === 'active' || text === 'thinking' || text === 'running') return 'running';
104
+ if (text === 'restoring' || text === 'resuming' || text === 'restarting') return 'resuming';
105
+ if (text === 'waiting' || text === 'waiting_input' || text === 'waiting_for_input') return 'needs_you';
106
+ if (text === 'idle') return 'idle';
107
+ if (text === 'review' || text === 'ready_review') return 'review';
108
+ if (text === 'exited') return 'exited';
109
+ return '';
110
+ }
111
+
112
+ // ------------------------------------------------------------------------
113
+ // derivePhase — the ONLY place a phase is decided.
114
+ // ------------------------------------------------------------------------
115
+ function condition(phase, confidence, at, source, extra) {
116
+ return Object.assign({ phase, confidence, at: at || 0, source: source || phase }, extra || {});
117
+ }
118
+
119
+ function stronger(a, b) {
120
+ // returns the dominant of two conditions (higher confidence, then phase
121
+ // strength, then freshest evidence).
122
+ if (!a) return b;
123
+ if (!b) return a;
124
+ if (a.confidence !== b.confidence) return a.confidence > b.confidence ? a : b;
125
+ const sa = PHASE_STRENGTH[a.phase] || 0;
126
+ const sb = PHASE_STRENGTH[b.phase] || 0;
127
+ if (sa !== sb) return sa > sb ? a : b;
128
+ return (a.at || 0) >= (b.at || 0) ? a : b;
129
+ }
130
+
131
+ function derivePhase(conditions, options = {}) {
132
+ const now = options.now || Date.now();
133
+ const log = typeof options.log === 'function' ? options.log : null;
134
+ const list = (conditions || []).filter(Boolean);
135
+
136
+ // Terminal state is absolute.
137
+ const exited = list.find((c) => c.phase === 'exited');
138
+ if (exited) {
139
+ return { phase: 'exited', source: exited.source, confidence: exited.confidence, at: exited.at, reason: 'exited' };
140
+ }
141
+ if (!list.length) {
142
+ return { phase: '', source: 'none', confidence: 0, at: 0, reason: 'no-conditions' };
143
+ }
144
+
145
+ let best = null;
146
+ for (const c of list) best = stronger(best, c);
147
+
148
+ let winner = best;
149
+ let reason = best.source;
150
+
151
+ // "Don't miss events": an authoritative idle (Stop hook etc.) must not bury a
152
+ // genuinely-newer sign of life. Promote — but only upward, only when strictly
153
+ // newer — and log it, because if events were complete this should not happen.
154
+ if (best.phase === 'idle') {
155
+ let promotion = null;
156
+ for (const c of list) {
157
+ if (!LIVENESS_PHASES.has(c.phase)) continue;
158
+ if ((c.at || 0) <= (best.at || 0)) continue; // must be strictly newer than the idle
159
+ promotion = stronger(promotion, c);
160
+ }
161
+ if (promotion) {
162
+ winner = promotion;
163
+ reason = `idle-promoted:${promotion.source}`;
164
+ if (log) {
165
+ log({
166
+ event: 'phase:idle-promoted',
167
+ from: { phase: best.phase, source: best.source, confidence: best.confidence, at: best.at },
168
+ to: { phase: promotion.phase, source: promotion.source, confidence: promotion.confidence, at: promotion.at },
169
+ now,
170
+ note: 'newer liveness evidence overrode an authoritative idle — possible missed lifecycle event',
171
+ });
172
+ }
173
+ }
174
+ }
175
+
176
+ return {
177
+ phase: winner.phase,
178
+ source: winner.source,
179
+ confidence: winner.confidence,
180
+ at: winner.at,
181
+ reason,
182
+ };
183
+ }
184
+
185
+ // ------------------------------------------------------------------------
186
+ // buildSessionConditions — map raw signals to confidence-ranked conditions.
187
+ // Accepts the same flat signal shape both server and client can produce.
188
+ // ------------------------------------------------------------------------
189
+ function buildSessionConditions(signals = {}, options = {}) {
190
+ const now = options.now || Date.now();
191
+ const parser = options.parseTimeMs;
192
+ const withBaseline = options.baseline !== false;
193
+ const at = (v, fb = 0) => normalizeObservedTimeMs(v, fb, parser, now);
194
+ const out = [];
195
+
196
+ if (signals.exited) {
197
+ out.push(condition('exited', CONFIDENCE.HIGH, now, 'exited'));
198
+ return out;
199
+ }
200
+
201
+ // Wall-E chat tabs have no PTY — their state is fully owned by us (HIGH).
202
+ if (signals.metaType === 'walle') {
203
+ if (signals.wallePendingPermission) {
204
+ out.push(condition('needs_you', CONFIDENCE.HIGH, now, 'walle:pending-permission', { blocking: true }));
205
+ } else if (signals.walleWaitingInput) {
206
+ out.push(condition('needs_you', CONFIDENCE.HIGH, now, 'walle:waiting-input', { blocking: true }));
207
+ } else if (signals.walleGenerating) {
208
+ out.push(condition('running', CONFIDENCE.HIGH, now, 'walle:generating'));
209
+ } else {
210
+ out.push(condition('idle', CONFIDENCE.HIGH, now, 'walle:idle'));
211
+ }
212
+ return out;
213
+ }
214
+
215
+ // --- AwaitingInput -----------------------------------------------------
216
+ if (signals.waitingForInput) {
217
+ const waitingAt = at(signals.waitingForInputAt, now);
218
+ const blocking = isBlockingWaitingReason(signals.waitingReason) || !!signals.waitingHasUnsubmittedInput;
219
+ const staleMs = Math.max(0, Number(signals.waitingInputStaleMs) || WAITING_INPUT_STALE_MS);
220
+ if (blocking) {
221
+ // A real approval/permission prompt is sticky — never ages out on its own.
222
+ out.push(condition('needs_you', CONFIDENCE.HIGH, waitingAt, `waiting:${signals.waitingReason || 'blocking'}`, { blocking: true }));
223
+ } else if (!waitingAt || (now - waitingAt) <= staleMs) {
224
+ out.push(condition('needs_you', CONFIDENCE.MEDIUM, waitingAt, `waiting:${signals.waitingReason || 'input'}`));
225
+ }
226
+ // stale non-blocking prompt → omitted → ages back to idle
227
+ }
228
+
229
+ // --- Restoring ---------------------------------------------------------
230
+ if (signals.restoreStarting || signals.restoreResuming || normalizeStreamPhase(signals.restoreStatus) === 'resuming') {
231
+ out.push(condition('resuming', CONFIDENCE.HIGH, at(signals.restoreStartedAt, now), 'restore'));
232
+ }
233
+
234
+ // --- Authoritative hook / telemetry verdict ----------------------------
235
+ if (signals.authoritativeSource) {
236
+ const authAt = at(signals.authoritativeStatusAt);
237
+ if (isFresh(authAt, now, AUTHORITATIVE_STATUS_TTL_MS)) {
238
+ if (signals.working) {
239
+ out.push(condition('running', CONFIDENCE.HIGH, authAt, `authoritative:${signals.authoritativeSource}`));
240
+ } else {
241
+ out.push(condition('idle', CONFIDENCE.HIGH, authAt, `authoritative:${signals.authoritativeSource}`));
242
+ }
243
+ }
244
+ }
245
+
246
+ // --- TurnOpen (hook/detector-driven) -----------------------------------
247
+ if (signals.activeTurnOpen) {
248
+ const started = at(signals.activeTurnStartedAt);
249
+ const lastEvidence = at(signals.activeTurnLastEvidenceAt);
250
+ const evidenceAt = Math.max(started, lastEvidence);
251
+ const maxQuiet = Math.max(0, Number(signals.activeTurnMaxQuietMs) || ACTIVE_TURN_MAX_QUIET_MS);
252
+ if (evidenceAt && maxQuiet && (now - evidenceAt) <= maxQuiet) {
253
+ const authoritative = signals.activeTurnSource === 'hook' || signals.activeTurnSource === 'telemetry';
254
+ out.push(condition('running', authoritative ? CONFIDENCE.HIGH : CONFIDENCE.MEDIUM, evidenceAt,
255
+ `turn-open:${signals.activeTurnSource || 'evidence'}`));
256
+ }
257
+ }
258
+
259
+ // --- SessionStateBus ---------------------------------------------------
260
+ const busPhase = normalizeStreamPhase(signals.busState);
261
+ if (busPhase === 'running') {
262
+ out.push(condition('running', CONFIDENCE.MEDIUM, at(signals.busStateAt, now), 'bus:running'));
263
+ } else if (busPhase === 'resuming') {
264
+ out.push(condition('resuming', CONFIDENCE.HIGH, at(signals.busStateAt, now), 'bus:resuming'));
265
+ } else if (busPhase === 'needs_you') {
266
+ out.push(condition('needs_you', CONFIDENCE.MEDIUM, at(signals.busStateAt, now), 'bus:waiting-input'));
267
+ }
268
+
269
+ // --- Server-observed working signal ------------------------------------
270
+ const serverWorkingAt = at(signals.serverWorkingAt);
271
+ if (serverWorkingAt && (now - serverWorkingAt) < SERVER_WORKING_TTL_MS) {
272
+ out.push(condition('running', CONFIDENCE.MEDIUM, serverWorkingAt, 'server-working'));
273
+ }
274
+
275
+ // --- SessionStream status ----------------------------------------------
276
+ const streamPhase = normalizeStreamPhase(signals.streamStatus);
277
+ const streamAt = at(signals.streamStatusAt);
278
+ if (streamPhase && isFresh(streamAt, now, STREAM_STATUS_TTL_MS)) {
279
+ out.push(condition(streamPhase, CONFIDENCE.MEDIUM, streamAt, 'stream'));
280
+ }
281
+
282
+ // --- Codex "Working" terminal footer (garble-prone → LOW) --------------
283
+ if (signals.terminalBusyStatus) {
284
+ out.push(condition('running', CONFIDENCE.LOW, at(signals.terminalBusyStatusAt, now), 'terminal-busy'));
285
+ }
286
+
287
+ // --- Running holds (inference bridging output bursts → LOW) -------------
288
+ const holdAt = at(signals.codexRunningEvidenceAt);
289
+ if (signals.codexRunningHold && holdAt && (now - holdAt) < CODEX_RUNNING_HOLD_MS) {
290
+ out.push(condition('running', CONFIDENCE.LOW, holdAt, 'codex-running-hold'));
291
+ }
292
+ const providerHoldAt = at(signals.providerRunningEvidenceAt);
293
+ if (signals.providerRunningHold && providerHoldAt && (now - providerHoldAt) < CODEX_RUNNING_HOLD_MS) {
294
+ out.push(condition('running', CONFIDENCE.LOW, providerHoldAt, 'provider-running-hold'));
295
+ }
296
+
297
+ // --- Recent PTY output (silence-inference → LOW) -----------------------
298
+ const lastOut = Math.max(at(signals.lastOutputAt), at(signals.metaLastPtyActivity));
299
+ if (lastOut && (now - lastOut) < RECENT_OUTPUT_TTL_MS) {
300
+ out.push(condition('running', CONFIDENCE.LOW, lastOut, 'recent-output'));
301
+ }
302
+
303
+ // --- Optimistic local input echo (client-only → LOW) -------------------
304
+ const lastInput = at(signals.lastInputAt);
305
+ if (lastInput && (now - lastInput) < RECENT_INPUT_TTL_MS) {
306
+ out.push(condition('running', CONFIDENCE.LOW, lastInput, 'recent-input'));
307
+ }
308
+
309
+ // --- Activation-preserved running (tab-switch bridge → LOW) -------------
310
+ if (normalizeStreamPhase(signals.activationPreservedStatus) === 'running') {
311
+ const until = parseTimeMs(signals.activationPreservedStatusUntil, 0, parser);
312
+ if (until > now) {
313
+ out.push(condition('running', CONFIDENCE.LOW, at(signals.activationPreservedStatusCapturedAt, now), 'activation-preserved'));
314
+ }
315
+ }
316
+
317
+ // --- Baseline idle -----------------------------------------------------
318
+ // The weakest possible floor so a session with no positive evidence reads idle
319
+ // instead of unknown. Suppressed with {baseline:false} when the server wants to
320
+ // explicitly defer ("no opinion") to a downstream consumer.
321
+ if (withBaseline) {
322
+ out.push(condition('idle', CONFIDENCE.LOW, lastOut || at(signals.lastInputAt) || 0, 'default-idle'));
323
+ }
324
+
325
+ return out;
326
+ }
327
+
328
+ // ------------------------------------------------------------------------
329
+ // resolve — convenience wrapper returning the legacy {cls,text,...} contract.
330
+ // ------------------------------------------------------------------------
331
+ function resolve(signals = {}, options = {}) {
332
+ const conditions = buildSessionConditions(signals, options);
333
+ const derived = derivePhase(conditions, options);
334
+ const phase = derived.phase || 'idle';
335
+ return {
336
+ cls: phaseToCls(phase),
337
+ text: PHASE_TEXT[phase] || 'Idle',
338
+ phase,
339
+ source: derived.source,
340
+ confidence: derived.confidence,
341
+ reason: derived.reason,
342
+ conditions,
343
+ };
344
+ }
345
+
346
+ return {
347
+ CONFIDENCE,
348
+ PHASE_STRENGTH,
349
+ PHASE_TEXT,
350
+ PHASE_TO_CLS,
351
+ AUTHORITATIVE_STATUS_TTL_MS,
352
+ SERVER_WORKING_TTL_MS,
353
+ STREAM_STATUS_TTL_MS,
354
+ RECENT_OUTPUT_TTL_MS,
355
+ RECENT_INPUT_TTL_MS,
356
+ CODEX_RUNNING_HOLD_MS,
357
+ ACTIVE_TURN_MAX_QUIET_MS,
358
+ WAITING_INPUT_STALE_MS,
359
+ OBSERVED_TIME_FUTURE_SKEW_MS,
360
+ phaseToCls,
361
+ isBlockingWaitingReason,
362
+ normalizeObservedTimeMs,
363
+ normalizeStreamPhase,
364
+ parseTimeMs,
365
+ condition,
366
+ derivePhase,
367
+ buildSessionConditions,
368
+ resolve,
369
+ };
370
+ });
@@ -122,6 +122,9 @@ async function tryOtherMicrosoftLoginProvider() {
122
122
  var _microsoftTunnelAccessMode = 'ctm_authenticated';
123
123
  var _microsoftTunnelAccessModeTouched = false;
124
124
  var _activeDeviceClaim = null;
125
+ var _recommendedPhoneCopiedValue = '';
126
+ var _recommendedPhoneCopiedUntil = 0;
127
+ var _recommendedPhoneCopyTimer = null;
125
128
  var _deviceClaimScopeUpdateTimer = null;
126
129
  var _deviceClaimScopeUpdateSeq = 0;
127
130
  var _deviceScopeControlsBound = false;
@@ -2459,6 +2462,49 @@ function _phoneOriginEmptyHint(method) {
2459
2462
  return 'Sign in to Tailscale to generate a phone URL';
2460
2463
  }
2461
2464
 
2465
+ function _recommendedPhoneUrlIsCopyable(value) {
2466
+ return /^https?:\/\//i.test(String(value || '').trim());
2467
+ }
2468
+
2469
+ function _renderRecommendedPhoneCopy(value) {
2470
+ var button = document.getElementById('setup-phone-best-copy');
2471
+ var label = document.getElementById('setup-phone-best-copy-label');
2472
+ if (!button && !label) return;
2473
+ var text = String(value || '').trim();
2474
+ var copyable = _recommendedPhoneUrlIsCopyable(text);
2475
+ var copied = copyable
2476
+ && text === _recommendedPhoneCopiedValue
2477
+ && Date.now() < _recommendedPhoneCopiedUntil;
2478
+ if (button) {
2479
+ button.disabled = !copyable;
2480
+ button.classList.toggle('is-copied', copied);
2481
+ button.setAttribute('data-copy-value', copyable ? text : '');
2482
+ button.setAttribute('aria-label', copyable ? ('Copy phone URL ' + text) : 'No phone URL available to copy');
2483
+ }
2484
+ if (label) label.textContent = copied ? 'Copied' : 'Copy';
2485
+ }
2486
+
2487
+ async function _writeTextToClipboard(text) {
2488
+ if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
2489
+ await navigator.clipboard.writeText(text);
2490
+ return;
2491
+ }
2492
+ var area = document.createElement('textarea');
2493
+ area.value = text;
2494
+ area.setAttribute('readonly', '');
2495
+ area.style.position = 'fixed';
2496
+ area.style.left = '-9999px';
2497
+ area.style.top = '0';
2498
+ document.body.appendChild(area);
2499
+ area.select();
2500
+ try {
2501
+ var ok = document.execCommand && document.execCommand('copy');
2502
+ if (!ok) throw new Error('Copy command failed');
2503
+ } finally {
2504
+ area.remove();
2505
+ }
2506
+ }
2507
+
2462
2508
  function _renderRecommendedPhoneOrigin(origin, method) {
2463
2509
  // Until /api/setup/network resolves, _lastNetworkSettings is null and we
2464
2510
  // cannot know the real origin yet — show an explicit loading state instead
@@ -2470,11 +2516,13 @@ function _renderRecommendedPhoneOrigin(origin, method) {
2470
2516
  ? (walleReady ? 'Walle Remote URL' : 'Walle Remote status')
2471
2517
  : (method === 'cloudflare' ? 'Recommended Cloudflare URL' : (method === 'microsoft' ? 'Microsoft tunnel URL' : 'Recommended Tailscale URL'));
2472
2518
  var best = document.getElementById('setup-phone-best-url');
2473
- if (best) best.textContent = !loaded
2519
+ var bestText = !loaded
2474
2520
  ? 'Resolving…'
2475
2521
  : (method === 'walle'
2476
2522
  ? (walleReady ? _walleRemoteUsableMobileUrl(_lastNetworkSettings || {}) : 'Hosted relay unavailable')
2477
2523
  : (origin ? origin.replace(/\/+$/, '') + '/m/' : (method === 'microsoft' ? 'Start tunnel first' : '')));
2524
+ if (best) best.textContent = bestText;
2525
+ _renderRecommendedPhoneCopy(bestText);
2478
2526
  var phoneInput = document.getElementById('setup-device-origin');
2479
2527
  if (phoneInput) phoneInput.placeholder = origin
2480
2528
  ? ''
@@ -3785,6 +3833,7 @@ async function loadNetworkSettings() {
3785
3833
  // the Pairing Origin field doesn't stay stuck on it. Let the user type one.
3786
3834
  var bestUrl = document.getElementById('setup-phone-best-url');
3787
3835
  if (bestUrl && bestUrl.textContent === 'Resolving…') bestUrl.textContent = 'Unavailable';
3836
+ _renderRecommendedPhoneCopy((bestUrl && bestUrl.textContent) || 'Unavailable');
3788
3837
  var phoneInput = document.getElementById('setup-device-origin');
3789
3838
  if (phoneInput && phoneInput.placeholder === 'Resolving phone URL…') {
3790
3839
  phoneInput.placeholder = 'Enter your phone URL manually';
@@ -5053,6 +5102,29 @@ async function copyDeviceClaimUrl() {
5053
5102
  }
5054
5103
  }
5055
5104
 
5105
+ async function copyRecommendedPhoneUrl() {
5106
+ var button = document.getElementById('setup-phone-best-copy');
5107
+ var best = document.getElementById('setup-phone-best-url');
5108
+ var text = String((button && button.getAttribute('data-copy-value')) || (best && best.textContent) || '').trim();
5109
+ if (!_recommendedPhoneUrlIsCopyable(text)) {
5110
+ _renderRecommendedPhoneCopy(text);
5111
+ return;
5112
+ }
5113
+ try {
5114
+ await _writeTextToClipboard(text);
5115
+ _recommendedPhoneCopiedValue = text;
5116
+ _recommendedPhoneCopiedUntil = Date.now() + 1800;
5117
+ _renderRecommendedPhoneCopy(text);
5118
+ if (_recommendedPhoneCopyTimer) clearTimeout(_recommendedPhoneCopyTimer);
5119
+ _recommendedPhoneCopyTimer = setTimeout(function() {
5120
+ _renderRecommendedPhoneCopy(text);
5121
+ }, 1850);
5122
+ setupToast('Phone URL copied');
5123
+ } catch (_) {
5124
+ setupToast('Copy failed', 'error');
5125
+ }
5126
+ }
5127
+
5056
5128
  // ── MCP Integrations ────────────────────────────────────────────────
5057
5129
  var MCP_ICONS = {
5058
5130
  'Claude Code': '\uD83E\uDD16',
@@ -6419,6 +6491,7 @@ SETUP.removeDeviceConnection = removeDeviceConnection;
6419
6491
  SETUP.revokeDevice = revokeDevice;
6420
6492
  SETUP.createDeviceClaim = createDeviceClaim;
6421
6493
  SETUP.copyDeviceClaimUrl = copyDeviceClaimUrl;
6494
+ SETUP.copyRecommendedPhoneUrl = copyRecommendedPhoneUrl;
6422
6495
  SETUP.loadWalleRemoteStatus = loadWalleRemoteStatus;
6423
6496
  SETUP.createWalleRemoteClaim = createWalleRemoteClaim;
6424
6497
  SETUP.copyWalleRemoteClaimUrl = copyWalleRemoteClaimUrl;
@@ -1221,6 +1221,56 @@ function populateConversationView(container, events) {
1221
1221
  container.scrollTop = container.scrollHeight;
1222
1222
  }
1223
1223
 
1224
+ function _conversationRenderYield() {
1225
+ return new Promise((resolve) => {
1226
+ if (typeof requestAnimationFrame === 'function') requestAnimationFrame(() => resolve());
1227
+ else setTimeout(resolve, 0);
1228
+ });
1229
+ }
1230
+
1231
+ async function populateConversationViewChunked(container, events, opts) {
1232
+ opts = opts || {};
1233
+ container.textContent = '';
1234
+ if (container.dataset) container.dataset.turnMode = container.dataset.turnMode || 'conversation';
1235
+ const sessionId = container.dataset?.sessionId || '';
1236
+ if (!events || events.length === 0) {
1237
+ _renderConversationState(container, 'No transcript messages found for this session.');
1238
+ container.scrollTop = container.scrollHeight;
1239
+ return true;
1240
+ }
1241
+ const isCurrent = typeof opts.isCurrent === 'function' ? opts.isCurrent : () => true;
1242
+ const batchSize = Math.max(10, Number(opts.batchSize) || 80);
1243
+ const budgetMs = Math.max(4, Number(opts.budgetMs) || 12);
1244
+ let batchStartedAt = Date.now();
1245
+ for (let i = 0; i < events.length; i += 1) {
1246
+ if (!isCurrent()) return false;
1247
+ const evt = events[i];
1248
+ if (_isPromptTurnContainer(container)) {
1249
+ if (_appendConversationEventToTurns(container, evt, { expandPrompt: false, collapseExisting: false, expandSetup: false })) {
1250
+ _markConversationEventSeen(sessionId, evt);
1251
+ }
1252
+ } else if (_applyStructuredToolEvent(container, evt)) {
1253
+ _markConversationEventSeen(sessionId, evt);
1254
+ } else {
1255
+ const el = renderConversationEvent(evt);
1256
+ if (el) {
1257
+ if (evt.data?.parentUuid) _assignConversationParentUuid(el, evt.data.parentUuid);
1258
+ if (!_mergeConsecutiveToolGroups(container, el)) container.appendChild(el);
1259
+ _markConversationEventSeen(sessionId, evt);
1260
+ }
1261
+ }
1262
+ if ((i + 1) % batchSize === 0 || (Date.now() - batchStartedAt) >= budgetMs) {
1263
+ await _conversationRenderYield();
1264
+ batchStartedAt = Date.now();
1265
+ }
1266
+ }
1267
+ if (!isCurrent()) return false;
1268
+ _linkConversationDocumentReferences(container, container);
1269
+ if (_isPromptTurnContainer(container)) _refreshLatestPromptTurnStatus(container.dataset?.sessionId || '', container);
1270
+ container.scrollTop = container.scrollHeight;
1271
+ return true;
1272
+ }
1273
+
1224
1274
  // --- WS Integration ---
1225
1275
 
1226
1276
  function subscribeToStream(ws, sessionId) {
@@ -2295,7 +2345,7 @@ async function _fetchConversationPage(sessionId, offset, opts) {
2295
2345
  let firstEmptyPage = null;
2296
2346
  for (const candidate of candidates) {
2297
2347
  let url = `/api/session/messages?id=${encodeURIComponent(candidate.id)}`
2298
- + `&offset=${offset}&limit=${CONVERSATION_TURN_PAGE_SIZE}&mode=turns`;
2348
+ + `&offset=${offset}&limit=${CONVERSATION_TURN_PAGE_SIZE}&mode=turns&compact=conversation`;
2299
2349
  if (candidate.projectEntry) url += `&project=${encodeURIComponent(candidate.projectEntry)}`;
2300
2350
  if (opts.fresh && offset === 0) url += '&nocache=1';
2301
2351
  const resp = await fetch(url, { cache: 'no-store' });
@@ -2449,7 +2499,10 @@ async function _primeConversationView(sessionId, convView, opts) {
2449
2499
  _hydrateLatestAnswerFromConversationMessages(sessionId, page.messages, 'conversation-tab');
2450
2500
  _syncPromptNavFromConversationMessages(sessionId, page);
2451
2501
  const events = _messagesToEvents(page.messages);
2452
- populateConversationView(convView, events);
2502
+ const rendered = await populateConversationViewChunked(convView, events, {
2503
+ isCurrent: () => _primeStillCurrent(sessionId, token, convView),
2504
+ });
2505
+ if (!rendered) return;
2453
2506
 
2454
2507
  // Seed the parentUuid dedup set from the primed history so live
2455
2508
  // stream-events that cover the same turns don't re-append.
@@ -2656,6 +2709,7 @@ if (typeof module !== 'undefined' && module.exports) {
2656
2709
  _fetchConversationPage,
2657
2710
  renderConversationEvent,
2658
2711
  populateConversationView,
2712
+ populateConversationViewChunked,
2659
2713
  _mergeConsecutiveToolGroups,
2660
2714
  _captureConversationScrollAnchor,
2661
2715
  _restoreConversationScrollAnchor,