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.
- package/README.md +1 -0
- package/package.json +1 -1
- package/template/claude-task-manager/api-prompts.js +11 -6
- package/template/claude-task-manager/docs/session-status-redesign.html +554 -0
- package/template/claude-task-manager/docs/terminal-rendering-redesign.html +529 -0
- package/template/claude-task-manager/lib/flush-redraw-markers.js +72 -0
- package/template/claude-task-manager/lib/macos-capabilities.js +190 -0
- package/template/claude-task-manager/lib/session-messages-projection.js +224 -3
- package/template/claude-task-manager/lib/ttl-memo.js +61 -0
- package/template/claude-task-manager/public/index.html +892 -11
- package/template/claude-task-manager/public/js/activation-render-check.js +40 -2
- package/template/claude-task-manager/public/js/session-phase.js +370 -0
- package/template/claude-task-manager/public/js/setup.js +74 -1
- package/template/claude-task-manager/public/js/stream-view.js +56 -2
- package/template/claude-task-manager/server.js +643 -68
- package/template/claude-task-manager/workers/read-pool-worker.js +10 -0
- package/template/package.json +1 -1
- package/template/wall-e/agent.js +130 -24
- package/template/wall-e/api-walle.js +12 -1
- package/template/wall-e/brain.js +290 -4
- package/template/wall-e/chat.js +30 -25
- package/template/wall-e/coding/session-plan.js +79 -0
- package/template/wall-e/coding-orchestrator.js +9 -3
- package/template/wall-e/coding-prompts.js +10 -3
- package/template/wall-e/embeddings.js +192 -17
- package/template/wall-e/http/model-admin.js +109 -0
- package/template/wall-e/lib/event-loop-monitor.js +2 -2
- package/template/wall-e/lib/scheduler-worker-jobs.js +156 -121
- package/template/wall-e/lib/scheduler.js +226 -13
- package/template/wall-e/lib/worker-thread-pool.js +58 -4
- package/template/wall-e/llm/ollama-library.js +126 -0
- package/template/wall-e/llm/ollama.js +13 -0
- package/template/wall-e/llm/provider-backpressure.js +134 -0
- package/template/wall-e/llm/provider-health-state.js +24 -0
- package/template/wall-e/loops/backfill.js +43 -16
- package/template/wall-e/loops/initiative.js +1 -0
- package/template/wall-e/loops/think.js +38 -5
- package/template/wall-e/mcp-server.js +20 -4
- package/template/wall-e/skills/skill-fallback.js +34 -1
- package/template/wall-e/skills/skill-planner.js +60 -2
- package/template/wall-e/sources/jsonl-utils.js +84 -11
- package/template/wall-e/telemetry.js +42 -7
- package/template/wall-e/tools/local-tools.js +16 -0
- package/template/wall-e/workers/runtime-worker.js +33 -1
- package/template/website/index.html +5 -0
|
@@ -20,16 +20,48 @@
|
|
|
20
20
|
(function (global) {
|
|
21
21
|
'use strict';
|
|
22
22
|
|
|
23
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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,
|