create-walle 0.9.21 → 0.9.22
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 +5 -5
- package/package.json +2 -2
- package/template/claude-task-manager/api-prompts.js +13 -0
- package/template/claude-task-manager/api-reviews.js +5 -2
- package/template/claude-task-manager/db.js +348 -15
- package/template/claude-task-manager/docs/app-update-refresh-protocol.md +69 -0
- package/template/claude-task-manager/docs/image-paste-ux.md +3 -0
- package/template/claude-task-manager/docs/ipad-web-preview.md +88 -0
- package/template/claude-task-manager/git-utils.js +146 -17
- package/template/claude-task-manager/lib/auth-rate-limit.js +23 -3
- package/template/claude-task-manager/lib/auth-rules.js +3 -0
- package/template/claude-task-manager/lib/document-review.js +33 -2
- package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +83 -0
- package/template/claude-task-manager/lib/mobile-auth-api.js +14 -0
- package/template/claude-task-manager/lib/restart-guard.js +68 -0
- package/template/claude-task-manager/lib/session-standup.js +36 -13
- package/template/claude-task-manager/lib/session-stream.js +11 -4
- package/template/claude-task-manager/lib/transport-security.js +50 -0
- package/template/claude-task-manager/lib/walle-transcript.js +16 -0
- package/template/claude-task-manager/lib/worktree-active-sync.js +6 -3
- package/template/claude-task-manager/public/css/reviews.css +10 -0
- package/template/claude-task-manager/public/css/setup.css +13 -0
- package/template/claude-task-manager/public/css/walle.css +145 -0
- package/template/claude-task-manager/public/index.html +539 -44
- package/template/claude-task-manager/public/ipad.html +363 -0
- package/template/claude-task-manager/public/js/document-review-links.js +196 -0
- package/template/claude-task-manager/public/js/message-renderer.js +14 -3
- package/template/claude-task-manager/public/js/reviews.js +30 -6
- package/template/claude-task-manager/public/js/setup.js +42 -2
- package/template/claude-task-manager/public/js/stream-view.js +20 -1
- package/template/claude-task-manager/public/js/walle.js +314 -18
- package/template/claude-task-manager/public/m/app.css +789 -11
- package/template/claude-task-manager/public/m/app.js +1070 -67
- package/template/claude-task-manager/public/m/claim.html +9 -2
- package/template/claude-task-manager/public/m/index.html +17 -10
- package/template/claude-task-manager/public/m/sw.js +1 -1
- package/template/claude-task-manager/server.js +365 -95
- package/template/claude-task-manager/session-integrity.js +4 -0
- package/template/docs/designs/2026-05-17-portkey-gateway-provider-ux.md +86 -35
- package/template/package.json +1 -1
- package/template/wall-e/api-walle.js +19 -1
- package/template/wall-e/brain.js +152 -6
- package/template/wall-e/chat.js +85 -0
- package/template/wall-e/coding-orchestrator.js +106 -12
- package/template/wall-e/http/model-admin.js +131 -0
- package/template/wall-e/lib/service-health.js +194 -0
- package/template/wall-e/llm/anthropic.js +7 -0
- package/template/wall-e/llm/client.js +46 -12
- package/template/wall-e/llm/openai.js +17 -2
- package/template/wall-e/llm/portkey-sync.js +201 -0
- package/template/wall-e/server.js +13 -0
- package/template/website/index.html +10 -10
|
@@ -40,6 +40,9 @@
|
|
|
40
40
|
walle: {
|
|
41
41
|
sessionId: null,
|
|
42
42
|
draft: '',
|
|
43
|
+
alerts: [],
|
|
44
|
+
health: null,
|
|
45
|
+
healthTest: null,
|
|
43
46
|
},
|
|
44
47
|
detailScrollLock: {
|
|
45
48
|
locked: false,
|
|
@@ -122,13 +125,22 @@
|
|
|
122
125
|
lastTapX: 0,
|
|
123
126
|
lastTapY: 0,
|
|
124
127
|
},
|
|
128
|
+
appRuntime: {
|
|
129
|
+
loaded: null,
|
|
130
|
+
latest: null,
|
|
131
|
+
reloadRequired: false,
|
|
132
|
+
reloadReason: '',
|
|
133
|
+
},
|
|
125
134
|
};
|
|
126
135
|
|
|
127
136
|
const $ = (id) => document.getElementById(id);
|
|
128
|
-
const MOBILE_ASSET_VERSION = '20260519-walle-
|
|
137
|
+
const MOBILE_ASSET_VERSION = '20260519-walle-default-chat';
|
|
138
|
+
const APP_RELOAD_CHANNEL_NAME = 'ctm-app-update';
|
|
129
139
|
const THEME_STORAGE_KEY = 'ctm.theme.mode';
|
|
130
140
|
const MOBILE_ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
|
|
131
141
|
const SEND_LONG_PRESS_MS = 520;
|
|
142
|
+
const DETAIL_SPLIT_QUERY = '(min-width: 900px) and (min-height: 650px)';
|
|
143
|
+
const WALLE_SINGLETON_CHAT_ID = 'default';
|
|
132
144
|
const MOBILE_SESSION_CACHE_KEY = 'ctm.mobile.sessionSnapshot.v1';
|
|
133
145
|
const MOBILE_SESSION_CACHE_MAX_AGE_MS = 12 * 60 * 60 * 1000;
|
|
134
146
|
const MOBILE_SESSION_CACHE_MAX_SESSIONS = 500;
|
|
@@ -137,6 +149,15 @@
|
|
|
137
149
|
const REMOTE_OUTBOX_RETRY_DELAYS_MS = [1200, 3000, 7000, 15000, 30000, 60000, 120000];
|
|
138
150
|
const SW_RELOAD_SESSION_KEY = 'ctm.mobile.swReloaded';
|
|
139
151
|
const SW_RELOAD_SESSION_VALUE = MOBILE_ASSET_VERSION;
|
|
152
|
+
const AUTH_RECOVERY_CODES = new Set([
|
|
153
|
+
'ip_locked',
|
|
154
|
+
'unauthorized',
|
|
155
|
+
'invalid_token',
|
|
156
|
+
'missing_token',
|
|
157
|
+
'token_revoked',
|
|
158
|
+
'token_expired',
|
|
159
|
+
'device_token_lookup_failed',
|
|
160
|
+
]);
|
|
140
161
|
const PROVIDER_GENERATED_USER_CONTEXT_PATTERNS = [
|
|
141
162
|
/^<environment_context>/i,
|
|
142
163
|
/^<permissions instructions>/i,
|
|
@@ -146,11 +167,193 @@
|
|
|
146
167
|
/^<skill>\s*<name>/i,
|
|
147
168
|
/^# AGENTS\.md instructions for\b/i,
|
|
148
169
|
];
|
|
170
|
+
let appReloadChannel = null;
|
|
171
|
+
|
|
172
|
+
function normalizeAppVersionInfo(info) {
|
|
173
|
+
if (!info || typeof info !== 'object') return null;
|
|
174
|
+
const version = String(info.version || '').trim();
|
|
175
|
+
const buildId = String(info.buildId || '').trim();
|
|
176
|
+
const product = String(info.product || 'create-walle').trim();
|
|
177
|
+
const components = info.components && typeof info.components === 'object' ? {
|
|
178
|
+
ctm: String(info.components.ctm || '').trim(),
|
|
179
|
+
wallE: String(info.components.wallE || '').trim(),
|
|
180
|
+
} : {};
|
|
181
|
+
if (!version && !buildId) return null;
|
|
182
|
+
return { version, buildId, product, components };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function appVersionIdentity(info) {
|
|
186
|
+
const normalized = normalizeAppVersionInfo(info);
|
|
187
|
+
if (!normalized) return '';
|
|
188
|
+
return normalized.buildId || `${normalized.product}:${normalized.version}:${normalized.components.ctm || ''}:${normalized.components.wallE || ''}`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function sameAppVersionIdentity(a, b) {
|
|
192
|
+
const aid = appVersionIdentity(a);
|
|
193
|
+
const bid = appVersionIdentity(b);
|
|
194
|
+
return !!aid && !!bid && aid === bid;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function initAppReloadChannel() {
|
|
198
|
+
if (appReloadChannel || typeof BroadcastChannel === 'undefined') return appReloadChannel;
|
|
199
|
+
try {
|
|
200
|
+
appReloadChannel = new BroadcastChannel(APP_RELOAD_CHANNEL_NAME);
|
|
201
|
+
appReloadChannel.onmessage = (event) => {
|
|
202
|
+
const data = event?.data;
|
|
203
|
+
if (!data || data.type !== 'reload-required') return;
|
|
204
|
+
requestInstalledUpdateReload({
|
|
205
|
+
server: data.server,
|
|
206
|
+
reason: data.reason || 'peer-detected',
|
|
207
|
+
source: 'broadcast',
|
|
208
|
+
rebroadcast: false,
|
|
209
|
+
});
|
|
210
|
+
};
|
|
211
|
+
} catch {
|
|
212
|
+
appReloadChannel = null;
|
|
213
|
+
}
|
|
214
|
+
return appReloadChannel;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function broadcastInstalledUpdateReload(server, reason) {
|
|
218
|
+
const channel = initAppReloadChannel();
|
|
219
|
+
if (!channel) return;
|
|
220
|
+
try {
|
|
221
|
+
channel.postMessage({ type: 'reload-required', server, reason, at: Date.now() });
|
|
222
|
+
} catch {}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function handleServerHello(msg) {
|
|
226
|
+
markHostReachable();
|
|
227
|
+
const server = normalizeAppVersionInfo(msg?.appVersion);
|
|
228
|
+
if (!server) return;
|
|
229
|
+
if (!state.appRuntime.loaded) {
|
|
230
|
+
state.appRuntime.loaded = server;
|
|
231
|
+
state.appRuntime.latest = server;
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
state.appRuntime.latest = server;
|
|
235
|
+
if (!sameAppVersionIdentity(state.appRuntime.loaded, server)) {
|
|
236
|
+
requestInstalledUpdateReload({ server, reason: 'server-version-changed', source: 'hello' });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function isEditableElement(el) {
|
|
241
|
+
if (!el || el === document.body || el === document.documentElement) return false;
|
|
242
|
+
if (el.isContentEditable) return true;
|
|
243
|
+
const tag = String(el.tagName || '').toLowerCase();
|
|
244
|
+
if (tag === 'textarea' || tag === 'input' || tag === 'select') return true;
|
|
245
|
+
return el.getAttribute?.('role') === 'textbox';
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function isMobileReloadUnsafe() {
|
|
249
|
+
if (isEditableElement(document.activeElement)) return true;
|
|
250
|
+
if (state.activeSession || state.activeTab === 'walle') return true;
|
|
251
|
+
if (!$('send-menu')?.hidden) return true;
|
|
252
|
+
for (const id of ['stepup-sheet', 'model-picker-sheet', 'new-session-sheet']) {
|
|
253
|
+
const sheet = $(id);
|
|
254
|
+
if (sheet && !sheet.hidden) return true;
|
|
255
|
+
}
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function showInstalledUpdateReloadBanner(server) {
|
|
260
|
+
const banner = $('app-reload-banner');
|
|
261
|
+
const msg = $('app-reload-msg');
|
|
262
|
+
if (!banner) return;
|
|
263
|
+
const loaded = state.appRuntime.loaded || {};
|
|
264
|
+
const latest = normalizeAppVersionInfo(server) || state.appRuntime.latest || {};
|
|
265
|
+
if (msg) {
|
|
266
|
+
const from = loaded.version ? `v${loaded.version}` : 'old UI';
|
|
267
|
+
const to = latest.version ? `v${latest.version}` : 'the installed update';
|
|
268
|
+
msg.textContent = `Reload to use ${to}. This page is still running ${from}.`;
|
|
269
|
+
}
|
|
270
|
+
banner.hidden = false;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function requestInstalledUpdateReload(opts = {}) {
|
|
274
|
+
const server = normalizeAppVersionInfo(opts.server) || state.appRuntime.latest;
|
|
275
|
+
state.appRuntime.latest = server || state.appRuntime.latest;
|
|
276
|
+
state.appRuntime.reloadRequired = true;
|
|
277
|
+
state.appRuntime.reloadReason = opts.reason || 'app-updated';
|
|
278
|
+
if (opts.rebroadcast !== false) broadcastInstalledUpdateReload(server, state.appRuntime.reloadReason);
|
|
279
|
+
if (!isMobileReloadUnsafe()) {
|
|
280
|
+
reloadForInstalledUpdate(opts.source || 'auto');
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
showInstalledUpdateReloadBanner(server);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function reloadForInstalledUpdate(source = 'button') {
|
|
287
|
+
const detail = {
|
|
288
|
+
source,
|
|
289
|
+
reason: state.appRuntime.reloadReason || 'app-updated',
|
|
290
|
+
loaded: state.appRuntime.loaded,
|
|
291
|
+
latest: state.appRuntime.latest,
|
|
292
|
+
};
|
|
293
|
+
try { sessionStorage.setItem('ctm_mobile_app_update_reload', JSON.stringify({ ...detail, at: Date.now() })); } catch {}
|
|
294
|
+
if (typeof window.__ctmAppReload === 'function') {
|
|
295
|
+
window.__ctmAppReload(detail);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
location.reload();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
window.__ctmMobileHandleServerHello = handleServerHello;
|
|
302
|
+
window.__ctmMobileRequestInstalledUpdateReload = requestInstalledUpdateReload;
|
|
303
|
+
window.__ctmMobileSetLoadedAppForTest = (info) => {
|
|
304
|
+
state.appRuntime.loaded = normalizeAppVersionInfo(info);
|
|
305
|
+
};
|
|
306
|
+
window.__ctmMobileAppRuntime = state.appRuntime;
|
|
149
307
|
|
|
150
308
|
function isDetailOpen() {
|
|
151
309
|
return !!$('detail-view')?.classList.contains('active');
|
|
152
310
|
}
|
|
153
311
|
|
|
312
|
+
function isDetailSplitLayout() {
|
|
313
|
+
try {
|
|
314
|
+
return !!(window.matchMedia && window.matchMedia(DETAIL_SPLIT_QUERY).matches);
|
|
315
|
+
} catch {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function setBackgroundInteractive(interactive) {
|
|
321
|
+
const main = $('app-main');
|
|
322
|
+
const nav = document.querySelector('.bottom-nav');
|
|
323
|
+
if (interactive) {
|
|
324
|
+
main?.removeAttribute('aria-hidden');
|
|
325
|
+
nav?.removeAttribute('aria-hidden');
|
|
326
|
+
if (main && 'inert' in main) main.inert = false;
|
|
327
|
+
if (nav && 'inert' in nav) nav.inert = false;
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
main?.setAttribute('aria-hidden', 'true');
|
|
331
|
+
nav?.setAttribute('aria-hidden', 'true');
|
|
332
|
+
if (main && 'inert' in main) main.inert = true;
|
|
333
|
+
if (nav && 'inert' in nav) nav.inert = true;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function syncDetailLayoutMode() {
|
|
337
|
+
const split = isDetailSplitLayout();
|
|
338
|
+
const detail = $('detail-view');
|
|
339
|
+
document.documentElement.classList.toggle('detail-split-layout', split);
|
|
340
|
+
document.body.classList.toggle('detail-split-layout', split);
|
|
341
|
+
const open = isDetailOpen();
|
|
342
|
+
document.documentElement.classList.toggle('detail-open', open);
|
|
343
|
+
document.body.classList.toggle('detail-open', open);
|
|
344
|
+
if (detail) {
|
|
345
|
+
detail.setAttribute('role', split ? 'region' : 'dialog');
|
|
346
|
+
detail.setAttribute('aria-modal', split ? 'false' : 'true');
|
|
347
|
+
}
|
|
348
|
+
if (!open) return;
|
|
349
|
+
if (split) {
|
|
350
|
+
unlockDetailBackgroundScroll();
|
|
351
|
+
setBackgroundInteractive(true);
|
|
352
|
+
} else {
|
|
353
|
+
lockDetailBackgroundScroll();
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
154
357
|
function activeTimelineBox() {
|
|
155
358
|
if (isDetailOpen()) return $('detail-messages');
|
|
156
359
|
if (state.activeTab === 'walle') return $('walle-messages') || $('detail-messages');
|
|
@@ -184,7 +387,6 @@
|
|
|
184
387
|
attachments: 'composer-attachments',
|
|
185
388
|
clear: 'detail-clear',
|
|
186
389
|
send: 'detail-send',
|
|
187
|
-
model: 'detail-model',
|
|
188
390
|
picker: 'mobile-skill-picker',
|
|
189
391
|
};
|
|
190
392
|
return $(ids[kind]);
|
|
@@ -430,6 +632,10 @@
|
|
|
430
632
|
}
|
|
431
633
|
|
|
432
634
|
function lockDetailBackgroundScroll() {
|
|
635
|
+
if (isDetailSplitLayout()) {
|
|
636
|
+
setBackgroundInteractive(true);
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
433
639
|
if (state.detailScrollLock.locked) return;
|
|
434
640
|
const scrollY = window.scrollY || document.documentElement.scrollTop || 0;
|
|
435
641
|
state.detailScrollLock.locked = true;
|
|
@@ -441,12 +647,7 @@
|
|
|
441
647
|
document.body.style.left = '0';
|
|
442
648
|
document.body.style.right = '0';
|
|
443
649
|
document.body.style.width = '100%';
|
|
444
|
-
|
|
445
|
-
const nav = document.querySelector('.bottom-nav');
|
|
446
|
-
main?.setAttribute('aria-hidden', 'true');
|
|
447
|
-
nav?.setAttribute('aria-hidden', 'true');
|
|
448
|
-
if (main && 'inert' in main) main.inert = true;
|
|
449
|
-
if (nav && 'inert' in nav) nav.inert = true;
|
|
650
|
+
setBackgroundInteractive(false);
|
|
450
651
|
}
|
|
451
652
|
|
|
452
653
|
function unlockDetailBackgroundScroll() {
|
|
@@ -461,12 +662,7 @@
|
|
|
461
662
|
document.body.style.left = '';
|
|
462
663
|
document.body.style.right = '';
|
|
463
664
|
document.body.style.width = '';
|
|
464
|
-
|
|
465
|
-
const nav = document.querySelector('.bottom-nav');
|
|
466
|
-
main?.removeAttribute('aria-hidden');
|
|
467
|
-
nav?.removeAttribute('aria-hidden');
|
|
468
|
-
if (main && 'inert' in main) main.inert = false;
|
|
469
|
-
if (nav && 'inert' in nav) nav.inert = false;
|
|
665
|
+
setBackgroundInteractive(true);
|
|
470
666
|
window.scrollTo(0, scrollY);
|
|
471
667
|
}
|
|
472
668
|
|
|
@@ -506,11 +702,39 @@
|
|
|
506
702
|
const err = new Error(json?.error || json?.message || `HTTP ${res.status}`);
|
|
507
703
|
err.status = res.status;
|
|
508
704
|
err.payload = json;
|
|
705
|
+
if (shouldRecoverMobileAuth(err)) beginMobileAuthRecovery(err);
|
|
509
706
|
throw err;
|
|
510
707
|
}
|
|
511
708
|
return json;
|
|
512
709
|
}
|
|
513
710
|
|
|
711
|
+
function shouldRecoverMobileAuth(err) {
|
|
712
|
+
const code = String(err?.payload?.error || err?.message || '');
|
|
713
|
+
return err?.status === 401 || AUTH_RECOVERY_CODES.has(code);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
let mobileAuthRecoveryStarted = false;
|
|
717
|
+
function beginMobileAuthRecovery(err) {
|
|
718
|
+
if (mobileAuthRecoveryStarted) return;
|
|
719
|
+
mobileAuthRecoveryStarted = true;
|
|
720
|
+
const code = String(err?.payload?.error || err?.message || 'auth_required');
|
|
721
|
+
state.auth = null;
|
|
722
|
+
setConnection('offline', 'Sign in');
|
|
723
|
+
toast(code === 'ip_locked'
|
|
724
|
+
? 'This phone session is locked after failed auth. Pair again from the Mac.'
|
|
725
|
+
: 'This phone session is signed out. Pair again from the Mac.');
|
|
726
|
+
fetch('/api/auth/logout-local', {
|
|
727
|
+
method: 'POST',
|
|
728
|
+
credentials: 'same-origin',
|
|
729
|
+
headers: { 'content-type': 'application/json' },
|
|
730
|
+
body: '{}',
|
|
731
|
+
}).catch(() => {}).finally(() => {
|
|
732
|
+
const target = new URL('/m/claim', window.location.origin);
|
|
733
|
+
target.searchParams.set('reason', code);
|
|
734
|
+
window.location.href = target.toString();
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
|
|
514
738
|
function ageLabel(value) {
|
|
515
739
|
const ms = value ? Date.parse(value) || Number(value) || 0 : 0;
|
|
516
740
|
if (!ms) return '';
|
|
@@ -620,10 +844,17 @@
|
|
|
620
844
|
.trim();
|
|
621
845
|
}
|
|
622
846
|
|
|
847
|
+
function modelProviderLabel(provider) {
|
|
848
|
+
const key = providerKey(provider);
|
|
849
|
+
if (key === 'anthropic') return 'Claude';
|
|
850
|
+
if (key === 'openai') return 'GPT';
|
|
851
|
+
return providerLabel(key);
|
|
852
|
+
}
|
|
853
|
+
|
|
623
854
|
function modelButtonLabel(card) {
|
|
624
855
|
const provider = providerKey(card?.walle_model_provider || card?.model_provider || card?.modelProvider || card?.providerType || '');
|
|
625
856
|
const model = modelDisplayLabel(card?.walle_model_id || card?.model_id || card?.model || '');
|
|
626
|
-
const providerText =
|
|
857
|
+
const providerText = modelProviderLabel(provider);
|
|
627
858
|
if (providerText && model) return `${providerText} · ${model}`;
|
|
628
859
|
if (model) return model;
|
|
629
860
|
return 'Auto model';
|
|
@@ -666,15 +897,35 @@
|
|
|
666
897
|
return `${count} ${singular}${count === 1 ? '' : 's'}`;
|
|
667
898
|
}
|
|
668
899
|
|
|
900
|
+
function isMainBranch(branch, wt) {
|
|
901
|
+
const value = String(branch || '').trim();
|
|
902
|
+
return wt?.isMain === true || value === 'main' || value === 'master';
|
|
903
|
+
}
|
|
904
|
+
|
|
669
905
|
function sessionWorktreeDiff(card) {
|
|
670
906
|
const wt = card?.worktreeStatus || card?.worktree || card?.meta?.worktreeStatus || null;
|
|
671
|
-
if (!wt
|
|
672
|
-
const branch = String(wt.branch || card?.branch || card?.gitBranch || '').trim();
|
|
673
|
-
if (!branch
|
|
907
|
+
if (!wt) return null;
|
|
908
|
+
const branch = String(wt.branch || card?.branch || card?.gitBranch || (wt.isMain ? 'main' : '')).trim();
|
|
909
|
+
if (!branch) return null;
|
|
910
|
+
const isMain = isMainBranch(branch, wt);
|
|
674
911
|
const dirtyFiles = positiveCount(wt.dirtyFiles);
|
|
675
|
-
const unmergedCommits =
|
|
912
|
+
const unmergedCommits = isMain
|
|
913
|
+
? (
|
|
914
|
+
positiveCount(wt.unpushedCommits) ||
|
|
915
|
+
positiveCount(wt.mainRemote?.ahead) ||
|
|
916
|
+
positiveCount(wt.ahead) ||
|
|
917
|
+
positiveCount(wt.unmergedCommits)
|
|
918
|
+
)
|
|
919
|
+
: (positiveCount(wt.unmergedCommits) || positiveCount(wt.ahead));
|
|
676
920
|
if (dirtyFiles <= 0 && unmergedCommits <= 0) return null;
|
|
677
|
-
return {
|
|
921
|
+
return {
|
|
922
|
+
branch,
|
|
923
|
+
dirtyFiles,
|
|
924
|
+
unmergedCommits,
|
|
925
|
+
isMain,
|
|
926
|
+
worktreeName: wt.worktreeName || '',
|
|
927
|
+
worktreePath: wt.worktreePath || card?.project || card?.cwd || '',
|
|
928
|
+
};
|
|
678
929
|
}
|
|
679
930
|
|
|
680
931
|
function sessionDiffTags(card) {
|
|
@@ -683,15 +934,20 @@
|
|
|
683
934
|
const chips = [];
|
|
684
935
|
if (diff.dirtyFiles > 0) {
|
|
685
936
|
const label = pluralize(diff.dirtyFiles, 'file');
|
|
686
|
-
const title =
|
|
937
|
+
const title = diff.isMain
|
|
938
|
+
? `${diff.branch}: ${pluralize(diff.dirtyFiles, 'uncommitted file')} on main`
|
|
939
|
+
: `${diff.branch}: ${pluralize(diff.dirtyFiles, 'uncommitted file')}`;
|
|
687
940
|
chips.push(`<span class="session-diff-chip files" title="${esc(title)}" aria-label="${esc(title)}">${esc(label)}</span>`);
|
|
688
941
|
}
|
|
689
942
|
if (diff.unmergedCommits > 0) {
|
|
690
943
|
const label = pluralize(diff.unmergedCommits, 'commit');
|
|
691
|
-
const title =
|
|
944
|
+
const title = diff.isMain
|
|
945
|
+
? `${diff.branch}: ${pluralize(diff.unmergedCommits, 'local commit')} on main`
|
|
946
|
+
: `${diff.branch}: ${pluralize(diff.unmergedCommits, 'commit')} not on main`;
|
|
692
947
|
chips.push(`<span class="session-diff-chip commits" title="${esc(title)}" aria-label="${esc(title)}">${esc(label)}</span>`);
|
|
693
948
|
}
|
|
694
|
-
|
|
949
|
+
const label = diff.isMain ? `Main branch changes for ${diff.branch}` : `Branch diff for ${diff.branch}`;
|
|
950
|
+
return `<span class="session-diff-tags" aria-label="${esc(label)}">${chips.join('')}</span>`;
|
|
695
951
|
}
|
|
696
952
|
|
|
697
953
|
function randomId() {
|
|
@@ -713,6 +969,11 @@
|
|
|
713
969
|
$('app-title').textContent = tab === 'walle' ? 'Wall-E' : tab.charAt(0).toUpperCase() + tab.slice(1);
|
|
714
970
|
if (tab === 'search') setTimeout(() => $('search-input')?.focus(), 50);
|
|
715
971
|
render();
|
|
972
|
+
if (tab === 'walle') {
|
|
973
|
+
refreshWalleHealth({ quiet: true }).then((payload) => {
|
|
974
|
+
if (payload && state.activeTab === 'walle') renderWalle();
|
|
975
|
+
}).catch(() => {});
|
|
976
|
+
}
|
|
716
977
|
if (tab === 'more') loadPushState().then(() => renderMore()).catch(() => renderMore());
|
|
717
978
|
}
|
|
718
979
|
|
|
@@ -785,6 +1046,10 @@
|
|
|
785
1046
|
worktree.worktreePath,
|
|
786
1047
|
worktree.state,
|
|
787
1048
|
worktree.summary,
|
|
1049
|
+
worktree.isMain ? 'main branch local work' : '',
|
|
1050
|
+
worktree.dirtyFiles ? `${worktree.dirtyFiles} changed files` : '',
|
|
1051
|
+
worktree.unpushedCommits ? `${worktree.unpushedCommits} local commits` : '',
|
|
1052
|
+
worktree.unmergedCommits ? `${worktree.unmergedCommits} unmerged commits` : '',
|
|
788
1053
|
...(card.evidence || []),
|
|
789
1054
|
...snippets,
|
|
790
1055
|
].filter(Boolean).join(' '));
|
|
@@ -888,8 +1153,9 @@
|
|
|
888
1153
|
const diffTags = sessionDiffTags(card);
|
|
889
1154
|
const searchMatch = renderHomeSearchMatch(card, opts.searchTokens || []);
|
|
890
1155
|
const action = opts.action || card.actionLabel || 'Open';
|
|
1156
|
+
const selected = state.activeSession && state.activeSession.id === card.id;
|
|
891
1157
|
return `
|
|
892
|
-
<button class="session-row" type="button" data-open-session="${esc(card.id)}">
|
|
1158
|
+
<button class="session-row ${selected ? 'selected' : ''}" type="button" data-open-session="${esc(card.id)}" ${selected ? 'aria-pressed="true"' : ''}>
|
|
893
1159
|
<span class="rail ${esc(lane)}"></span>
|
|
894
1160
|
<span class="session-main">
|
|
895
1161
|
<span class="session-title-line">
|
|
@@ -909,6 +1175,72 @@
|
|
|
909
1175
|
`;
|
|
910
1176
|
}
|
|
911
1177
|
|
|
1178
|
+
function worktreeSummaryItems(sessions) {
|
|
1179
|
+
const seen = new Set();
|
|
1180
|
+
const items = [];
|
|
1181
|
+
for (const card of sessions || []) {
|
|
1182
|
+
const diff = sessionWorktreeDiff(card);
|
|
1183
|
+
if (!diff || !card?.id) continue;
|
|
1184
|
+
const key = `${diff.branch}\n${diff.worktreePath || card.project || card.cwd || card.id}`;
|
|
1185
|
+
if (seen.has(key)) continue;
|
|
1186
|
+
seen.add(key);
|
|
1187
|
+
const changedCount = diff.dirtyFiles + diff.unmergedCommits;
|
|
1188
|
+
const lastActivity = Date.parse(card.lastActivity || '') || 0;
|
|
1189
|
+
items.push({ card, diff, changedCount, lastActivity });
|
|
1190
|
+
}
|
|
1191
|
+
return items.sort((a, b) => {
|
|
1192
|
+
if (a.diff.isMain !== b.diff.isMain) return a.diff.isMain ? -1 : 1;
|
|
1193
|
+
if (b.changedCount !== a.changedCount) return b.changedCount - a.changedCount;
|
|
1194
|
+
return b.lastActivity - a.lastActivity;
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function renderWorktreeSummary(view) {
|
|
1199
|
+
const items = worktreeSummaryItems(view.sessions);
|
|
1200
|
+
if (!items.length) return '';
|
|
1201
|
+
const totalFiles = items.reduce((sum, item) => sum + item.diff.dirtyFiles, 0);
|
|
1202
|
+
const totalCommits = items.reduce((sum, item) => sum + item.diff.unmergedCommits, 0);
|
|
1203
|
+
const stats = [
|
|
1204
|
+
totalFiles > 0 ? pluralize(totalFiles, 'file') : '',
|
|
1205
|
+
totalCommits > 0 ? pluralize(totalCommits, 'commit') : '',
|
|
1206
|
+
].filter(Boolean).join(' · ');
|
|
1207
|
+
const rows = items.slice(0, 4).map(({ card, diff }) => {
|
|
1208
|
+
const title = diff.isMain ? 'Main branch' : diff.branch;
|
|
1209
|
+
const metaParts = [
|
|
1210
|
+
diff.isMain ? 'primary checkout' : (diff.worktreeName || 'worktree'),
|
|
1211
|
+
compactPath(diff.worktreePath || card.project || card.cwd || ''),
|
|
1212
|
+
].filter(Boolean);
|
|
1213
|
+
return `
|
|
1214
|
+
<button class="worktree-summary-row ${diff.isMain ? 'main' : ''}" type="button" data-open-worktree-session="${esc(card.id)}" data-worktree-branch="${esc(diff.branch)}" aria-label="${esc(`Open ${title} changes`)}">
|
|
1215
|
+
<span class="worktree-summary-copy">
|
|
1216
|
+
<span class="worktree-summary-title">${esc(title)}</span>
|
|
1217
|
+
<span class="worktree-summary-meta">${esc(metaParts.join(' · '))}</span>
|
|
1218
|
+
</span>
|
|
1219
|
+
${sessionDiffTags(card)}
|
|
1220
|
+
</button>
|
|
1221
|
+
`;
|
|
1222
|
+
}).join('');
|
|
1223
|
+
const remaining = items.length - 4;
|
|
1224
|
+
const more = remaining > 0
|
|
1225
|
+
? `<div class="worktree-summary-more">${esc(`+${remaining} more changed ${remaining === 1 ? 'worktree' : 'worktrees'}`)}</div>`
|
|
1226
|
+
: '';
|
|
1227
|
+
return `
|
|
1228
|
+
<section class="worktree-summary" aria-label="Changed files and commits">
|
|
1229
|
+
<div class="worktree-summary-head">
|
|
1230
|
+
<span>
|
|
1231
|
+
<span class="worktree-summary-kicker">Local work</span>
|
|
1232
|
+
<strong>${view.searchActive ? 'Matching branch changes' : 'Branch changes'}</strong>
|
|
1233
|
+
</span>
|
|
1234
|
+
<span class="worktree-summary-stats">${esc(stats || pluralize(items.length, 'worktree'))}</span>
|
|
1235
|
+
</div>
|
|
1236
|
+
<div class="worktree-summary-list">
|
|
1237
|
+
${rows}
|
|
1238
|
+
${more}
|
|
1239
|
+
</div>
|
|
1240
|
+
</section>
|
|
1241
|
+
`;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
912
1244
|
function renderLoadingRows(label = 'Loading CTM sessions...') {
|
|
913
1245
|
return `<div class="empty-state">${esc(label)}</div>`;
|
|
914
1246
|
}
|
|
@@ -933,6 +1265,7 @@
|
|
|
933
1265
|
const byLane = (lane) => view.sessions.filter((s) => s.lane === lane);
|
|
934
1266
|
const empty = (text) => view.searchActive ? 'No matches.' : text;
|
|
935
1267
|
$('status-content').innerHTML = [
|
|
1268
|
+
renderWorktreeSummary(view),
|
|
936
1269
|
section('Needs you', counts.needs, byLane('needs_user').map((card) => sessionRow(card, { action: card.actionKind === 'approval_needed' ? 'Review' : 'Open', searchTokens: view.tokens })), empty('No agents are waiting on you.'), 'needs_user'),
|
|
937
1270
|
section('Running', counts.running, byLane('running').map((card) => sessionRow(card, { action: 'Watch', searchTokens: view.tokens })), empty('No active agents right now.'), 'running'),
|
|
938
1271
|
section('Ready review', counts.review, byLane('ready_review').map((card) => sessionRow(card, { action: 'Review', searchTokens: view.tokens })), empty('No finished work is waiting for review.'), 'ready_review'),
|
|
@@ -1241,17 +1574,253 @@
|
|
|
1241
1574
|
$('agents-content').innerHTML = rows.length ? rows.join('') : '<div class="empty-state">No CTM sessions are available.</div>';
|
|
1242
1575
|
}
|
|
1243
1576
|
|
|
1577
|
+
function isWalleSingletonChat(card) {
|
|
1578
|
+
if (!card) return false;
|
|
1579
|
+
if (card._singleton_walle_chat === true || card.singletonWalleChat === true) return true;
|
|
1580
|
+
const id = String(card.id || card.sessionId || '').trim();
|
|
1581
|
+
return id === WALLE_SINGLETON_CHAT_ID && hasWalleChatIdentity(card) && !hasWalleCodingIdentity(card);
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
function singletonWalleChatCard() {
|
|
1585
|
+
return {
|
|
1586
|
+
id: WALLE_SINGLETON_CHAT_ID,
|
|
1587
|
+
title: 'Wall-E',
|
|
1588
|
+
label: 'Wall-E',
|
|
1589
|
+
agent: 'walle',
|
|
1590
|
+
provider: 'walle',
|
|
1591
|
+
type: 'walle',
|
|
1592
|
+
session_type: 'walle',
|
|
1593
|
+
runtime_type: 'walle',
|
|
1594
|
+
native_walle_session: true,
|
|
1595
|
+
nativeWalleSession: true,
|
|
1596
|
+
walle_chat_session: true,
|
|
1597
|
+
walleChatSession: true,
|
|
1598
|
+
agentMode: 'chat',
|
|
1599
|
+
agentKind: 'walle-chat',
|
|
1600
|
+
taskType: 'chat',
|
|
1601
|
+
project: '',
|
|
1602
|
+
cwd: '',
|
|
1603
|
+
lane: 'idle',
|
|
1604
|
+
status: 'idle',
|
|
1605
|
+
actionLabel: 'Open',
|
|
1606
|
+
goal: 'Wall-E chat',
|
|
1607
|
+
evidence: ['default chat'],
|
|
1608
|
+
lastActivity: state.standupGeneratedAt || new Date().toISOString(),
|
|
1609
|
+
singletonWalleChat: true,
|
|
1610
|
+
_singleton_walle_chat: true,
|
|
1611
|
+
agentCapabilities: {
|
|
1612
|
+
structuredTranscript: true,
|
|
1613
|
+
promptNavigation: 'none',
|
|
1614
|
+
review: false,
|
|
1615
|
+
resume: false,
|
|
1616
|
+
terminalInput: false,
|
|
1617
|
+
terminalControl: false,
|
|
1618
|
+
},
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1244
1622
|
function walleSessionCards() {
|
|
1245
|
-
|
|
1623
|
+
const cards = state.sessions.filter((s) => isWalleChatSession(s));
|
|
1624
|
+
return cards.length ? cards : [singletonWalleChatCard()];
|
|
1246
1625
|
}
|
|
1247
1626
|
|
|
1248
1627
|
function selectedWalleSession(cards = walleSessionCards()) {
|
|
1249
|
-
const preferred = state.walle.sessionId || (
|
|
1628
|
+
const preferred = state.walle.sessionId || (isWalleChatSession(state.activeSession) ? state.activeSession.id : '');
|
|
1250
1629
|
return cards.find((s) => s.id === preferred) || cards[0] || null;
|
|
1251
1630
|
}
|
|
1252
1631
|
|
|
1632
|
+
function defaultWalleChatCwd() {
|
|
1633
|
+
const project = state.projects.find((item) => item && (item.path || item.cwd));
|
|
1634
|
+
return project?.path || project?.cwd || state.activeSession?.project || state.activeSession?.cwd || '';
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
function localWalleChatCard(id, cwd) {
|
|
1638
|
+
const now = new Date().toISOString();
|
|
1639
|
+
return {
|
|
1640
|
+
id,
|
|
1641
|
+
title: 'Wall-E Chat',
|
|
1642
|
+
label: 'Wall-E Chat',
|
|
1643
|
+
agent: 'walle',
|
|
1644
|
+
provider: 'walle',
|
|
1645
|
+
type: 'walle',
|
|
1646
|
+
session_type: 'walle',
|
|
1647
|
+
runtime_type: 'walle',
|
|
1648
|
+
native_walle_session: true,
|
|
1649
|
+
nativeWalleSession: true,
|
|
1650
|
+
walle_chat_session: true,
|
|
1651
|
+
walleChatSession: true,
|
|
1652
|
+
agentMode: 'chat',
|
|
1653
|
+
agentKind: 'walle-chat',
|
|
1654
|
+
taskType: 'chat',
|
|
1655
|
+
project: cwd || '',
|
|
1656
|
+
cwd: cwd || '',
|
|
1657
|
+
lane: 'running',
|
|
1658
|
+
status: 'starting',
|
|
1659
|
+
actionLabel: 'Open',
|
|
1660
|
+
goal: 'Wall-E chat is starting.',
|
|
1661
|
+
evidence: ['chat'],
|
|
1662
|
+
lastActivity: now,
|
|
1663
|
+
agentCapabilities: {
|
|
1664
|
+
structuredTranscript: true,
|
|
1665
|
+
promptNavigation: 'none',
|
|
1666
|
+
review: false,
|
|
1667
|
+
resume: false,
|
|
1668
|
+
terminalInput: false,
|
|
1669
|
+
terminalControl: false,
|
|
1670
|
+
},
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
async function startWalleChat() {
|
|
1675
|
+
if (!state.auth?.scopes?.includes('create') && !state.auth?.scopes?.includes('admin')) {
|
|
1676
|
+
toast('This phone token needs create scope. Upgrade it on the Mac.');
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
const cwd = defaultWalleChatCwd();
|
|
1680
|
+
await withStepUp({
|
|
1681
|
+
title: 'Start Wall-E chat',
|
|
1682
|
+
copy: `Start a Wall-E chat${cwd ? ` in ${compactPath(cwd)}` : ''}.`,
|
|
1683
|
+
label: 'Face ID & Start',
|
|
1684
|
+
action: async () => {
|
|
1685
|
+
const id = randomId();
|
|
1686
|
+
const payload = {
|
|
1687
|
+
type: 'create',
|
|
1688
|
+
id,
|
|
1689
|
+
cwd: cwd || undefined,
|
|
1690
|
+
cmd: 'walle',
|
|
1691
|
+
args: [],
|
|
1692
|
+
label: 'Wall-E Chat',
|
|
1693
|
+
agentType: 'walle',
|
|
1694
|
+
agentMode: 'chat',
|
|
1695
|
+
agentKind: 'walle-chat',
|
|
1696
|
+
taskType: 'chat',
|
|
1697
|
+
_skipProjectConfig: true,
|
|
1698
|
+
};
|
|
1699
|
+
await sendWs(payload);
|
|
1700
|
+
const card = localWalleChatCard(id, cwd);
|
|
1701
|
+
state.sessions = [card, ...state.sessions.filter((session) => session.id !== id)];
|
|
1702
|
+
state.walle.sessionId = id;
|
|
1703
|
+
renderWalle();
|
|
1704
|
+
},
|
|
1705
|
+
});
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
function walleHealthCountMeta(health) {
|
|
1709
|
+
const counts = health?.counts || {};
|
|
1710
|
+
const parts = [];
|
|
1711
|
+
if (health?.current_issue) parts.push('current blocker');
|
|
1712
|
+
if (counts.disabled_skills) parts.push(`${counts.disabled_skills} skill${counts.disabled_skills === 1 ? '' : 's'}`);
|
|
1713
|
+
if (counts.integration) parts.push(`${counts.integration} integration${counts.integration === 1 ? '' : 's'}`);
|
|
1714
|
+
if (counts.provider_history) parts.push(`${counts.provider_history} older provider`);
|
|
1715
|
+
if (counts.system) parts.push(`${counts.system} system`);
|
|
1716
|
+
return parts.join(' · ');
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
function walleHealthProviderLabel(provider) {
|
|
1720
|
+
const normalized = String(provider || '').trim();
|
|
1721
|
+
if (!normalized) return 'provider';
|
|
1722
|
+
if (normalized === 'deepseek') return 'DeepSeek';
|
|
1723
|
+
if (normalized === 'openai') return 'OpenAI';
|
|
1724
|
+
if (normalized === 'moonshot') return 'Moonshot / Kimi';
|
|
1725
|
+
return normalized.charAt(0).toUpperCase() + normalized.slice(1);
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
function walleHealthSkillName(item) {
|
|
1729
|
+
if (!item) return '';
|
|
1730
|
+
if (item.skill) return String(item.skill);
|
|
1731
|
+
const service = String(item.service || '');
|
|
1732
|
+
if (service && service !== 'ai_provider' && service !== 'system') return service;
|
|
1733
|
+
const message = String(item.message || '');
|
|
1734
|
+
const quoted = message.match(/Skill\s+"([^"]+)"/i);
|
|
1735
|
+
if (quoted) return quoted[1];
|
|
1736
|
+
const named = message.match(/Skill\s+([A-Za-z0-9_.-]+)/i);
|
|
1737
|
+
return named ? named[1] : '';
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
function walleHealthItemActionHtml(item) {
|
|
1741
|
+
const id = item?.id ? esc(item.id) : '';
|
|
1742
|
+
const type = String(item?.type || '').toLowerCase();
|
|
1743
|
+
const action = String(item?.action || '').toLowerCase();
|
|
1744
|
+
if (type === 'skill_disabled' && id) {
|
|
1745
|
+
return `<button type="button" class="mobile-health-row-action" data-walle-health-action="enable-skill" data-walle-health-alert-id="${id}">Re-enable</button>`;
|
|
1746
|
+
}
|
|
1747
|
+
if (action === 'repair_slack_owner' && id) {
|
|
1748
|
+
return `<button type="button" class="mobile-health-row-action" data-walle-health-action="repair-alert" data-walle-health-alert-id="${id}">Fix</button>`;
|
|
1749
|
+
}
|
|
1750
|
+
if (action === 'gws_reauth' || item?.action_url) {
|
|
1751
|
+
return '<a class="mobile-health-row-action" href="/setup.html">Setup</a>';
|
|
1752
|
+
}
|
|
1753
|
+
return '';
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
function walleHealthGroupHtml(title, items, options = {}) {
|
|
1757
|
+
if (!Array.isArray(items) || !items.length) return '';
|
|
1758
|
+
const open = options.open ? ' open' : '';
|
|
1759
|
+
const rows = items.slice(0, options.limit || 8).map((item) => `
|
|
1760
|
+
<div class="mobile-health-row">
|
|
1761
|
+
<span class="mobile-health-row-dot" aria-hidden="true"></span>
|
|
1762
|
+
<span class="mobile-health-row-copy">${esc(item.message || item.title || item.skill || item.service || 'Service alert')}</span>
|
|
1763
|
+
${walleHealthItemActionHtml(item)}
|
|
1764
|
+
</div>
|
|
1765
|
+
`).join('');
|
|
1766
|
+
const more = items.length > (options.limit || 8)
|
|
1767
|
+
? `<div class="mobile-health-more">${items.length - (options.limit || 8)} more in desktop Wall-E</div>`
|
|
1768
|
+
: '';
|
|
1769
|
+
return `
|
|
1770
|
+
<details class="mobile-health-group"${open}>
|
|
1771
|
+
<summary><span>${esc(title)}</span><strong>${items.length}</strong></summary>
|
|
1772
|
+
<div class="mobile-health-group-body">${rows}${more}</div>
|
|
1773
|
+
</details>
|
|
1774
|
+
`;
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
function walleHealthHtml() {
|
|
1778
|
+
const health = state.walle.health;
|
|
1779
|
+
const alerts = state.walle.alerts || [];
|
|
1780
|
+
if (!health || (!alerts.length && health.level === 'ok')) return '';
|
|
1781
|
+
const level = health.level || 'info';
|
|
1782
|
+
const issue = health.current_issue;
|
|
1783
|
+
const meta = walleHealthCountMeta(health);
|
|
1784
|
+
const provider = issue?.provider || health.active_provider?.provider || '';
|
|
1785
|
+
const model = issue?.model || health.active_provider?.model || '';
|
|
1786
|
+
const test = state.walle.healthTest;
|
|
1787
|
+
return `
|
|
1788
|
+
<section id="walle-health-card" class="mobile-health-card ${esc(level)}" aria-label="Wall-E service health">
|
|
1789
|
+
<div class="mobile-health-header">
|
|
1790
|
+
<span class="mobile-health-dot" aria-hidden="true"></span>
|
|
1791
|
+
<div class="mobile-health-copy">
|
|
1792
|
+
<div class="mobile-health-kicker">Service health</div>
|
|
1793
|
+
<h3>${esc(health.title || 'Wall-E services')}</h3>
|
|
1794
|
+
<p>${esc(health.message || '')}</p>
|
|
1795
|
+
${meta ? `<div class="mobile-health-meta">${esc(meta)}</div>` : ''}
|
|
1796
|
+
</div>
|
|
1797
|
+
</div>
|
|
1798
|
+
${issue ? `
|
|
1799
|
+
<div class="mobile-health-current">
|
|
1800
|
+
<div class="mobile-health-current-meta">${esc([provider, model].filter(Boolean).join(' / '))}</div>
|
|
1801
|
+
<div class="mobile-health-actions">
|
|
1802
|
+
<button type="button" class="mobile-health-btn primary" data-walle-health-action="test-provider">Test ${esc(walleHealthProviderLabel(provider))}</button>
|
|
1803
|
+
<a class="mobile-health-btn" href="/setup.html">Setup</a>
|
|
1804
|
+
<button type="button" class="mobile-health-icon-btn" data-walle-health-action="dismiss-current" aria-label="Dismiss current Wall-E service alert">×</button>
|
|
1805
|
+
</div>
|
|
1806
|
+
</div>
|
|
1807
|
+
` : ''}
|
|
1808
|
+
${test ? `<div class="mobile-health-test ${esc(test.ok ? 'ok' : 'error')}">${esc(test.message || '')}</div>` : ''}
|
|
1809
|
+
${walleHealthGroupHtml('Integrations', health.integration_alerts, { open: !issue })}
|
|
1810
|
+
${walleHealthGroupHtml('Disabled skills', health.disabled_skills, { open: !issue && !(health.integration_alerts || []).length })}
|
|
1811
|
+
${walleHealthGroupHtml('Older provider history', health.provider_history)}
|
|
1812
|
+
${walleHealthGroupHtml('System alerts', health.system_alerts)}
|
|
1813
|
+
<div class="mobile-health-footer">
|
|
1814
|
+
<a href="/#walle" class="mobile-health-link">Open full Wall-E panel</a>
|
|
1815
|
+
</div>
|
|
1816
|
+
</section>
|
|
1817
|
+
`;
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1253
1820
|
function walleContextHtml(active, cards) {
|
|
1254
|
-
const meta =
|
|
1821
|
+
const meta = isWalleSingletonChat(active)
|
|
1822
|
+
? 'Main Wall-E chat'
|
|
1823
|
+
: compactPath(active.project || active.cwd || active.projectPath || '');
|
|
1255
1824
|
const status = active.lane || active.status || 'idle';
|
|
1256
1825
|
const picker = cards.length > 1
|
|
1257
1826
|
? `
|
|
@@ -1262,6 +1831,9 @@
|
|
|
1262
1831
|
</select>
|
|
1263
1832
|
</label>`
|
|
1264
1833
|
: '';
|
|
1834
|
+
const action = isWalleSingletonChat(active)
|
|
1835
|
+
? '<a class="row-action" href="/#walle" aria-label="Open full Wall-E panel">Full panel</a>'
|
|
1836
|
+
: `<button class="row-action" type="button" data-open-session="${esc(active.id)}" aria-label="Open full Wall-E session">Open</button>`;
|
|
1265
1837
|
return `
|
|
1266
1838
|
<div class="walle-context-main">
|
|
1267
1839
|
<span class="walle-avatar" aria-hidden="true">◆</span>
|
|
@@ -1276,7 +1848,7 @@
|
|
|
1276
1848
|
<div class="walle-context-actions">
|
|
1277
1849
|
<span class="state-chip ${esc(status)}">${esc(active.status || active.lane || 'idle')}</span>
|
|
1278
1850
|
${picker}
|
|
1279
|
-
|
|
1851
|
+
${action}
|
|
1280
1852
|
</div>
|
|
1281
1853
|
`;
|
|
1282
1854
|
}
|
|
@@ -1287,6 +1859,7 @@
|
|
|
1287
1859
|
<section id="walle-context" class="walle-chat-context" aria-label="Wall-E session context">
|
|
1288
1860
|
${walleContextHtml(active, cards)}
|
|
1289
1861
|
</section>
|
|
1862
|
+
${walleHealthHtml()}
|
|
1290
1863
|
<section id="walle-messages" class="detail-messages walle-chat-messages" aria-live="polite">
|
|
1291
1864
|
<div class="empty-state">Loading Wall-E conversation...</div>
|
|
1292
1865
|
</section>
|
|
@@ -1333,7 +1906,8 @@
|
|
|
1333
1906
|
}
|
|
1334
1907
|
if (state.activeStreamSessionId && state.activeStreamSessionId !== active.id) unsubscribeActiveStream(state.activeStreamSessionId);
|
|
1335
1908
|
state.activeSession = active;
|
|
1336
|
-
|
|
1909
|
+
const singletonChat = isWalleSingletonChat(active);
|
|
1910
|
+
state.activeStreamSessionId = singletonChat ? null : active.id;
|
|
1337
1911
|
resetDetailTimeline(active.id);
|
|
1338
1912
|
resetPromptHistory(active.id);
|
|
1339
1913
|
clearComposerAttachments();
|
|
@@ -1341,8 +1915,10 @@
|
|
|
1341
1915
|
hideMobileSkillPicker();
|
|
1342
1916
|
const box = $('walle-messages');
|
|
1343
1917
|
if (box) box.innerHTML = '<div class="empty-state">Connecting to Wall-E conversation...</div>';
|
|
1344
|
-
|
|
1345
|
-
|
|
1918
|
+
if (!singletonChat) {
|
|
1919
|
+
subscribeActiveStream(active.id).catch(() => {});
|
|
1920
|
+
warmPromptHistory(active).catch(() => {});
|
|
1921
|
+
}
|
|
1346
1922
|
loadMessages(active).catch(() => {});
|
|
1347
1923
|
return true;
|
|
1348
1924
|
}
|
|
@@ -1351,10 +1927,17 @@
|
|
|
1351
1927
|
const cards = walleSessionCards();
|
|
1352
1928
|
const active = selectedWalleSession(cards);
|
|
1353
1929
|
if (!active) {
|
|
1930
|
+
const hasCodingSessions = state.sessions.some((session) => isWalleOwnedSession(session) && !isWalleChatSession(session));
|
|
1354
1931
|
$('walle-content').innerHTML = `
|
|
1355
1932
|
<div class="walle-empty-chat">
|
|
1356
|
-
<
|
|
1357
|
-
<
|
|
1933
|
+
<span class="walle-empty-icon" aria-hidden="true">◆</span>
|
|
1934
|
+
<h3>Wall-E chat</h3>
|
|
1935
|
+
<p>Chat with Wall-E here. Coding and skills sessions stay in Status and Agents so this tab stays focused.</p>
|
|
1936
|
+
<div class="walle-empty-actions">
|
|
1937
|
+
<button id="walle-start-chat" class="primary-action" type="button">Start chat</button>
|
|
1938
|
+
</div>
|
|
1939
|
+
${walleHealthHtml()}
|
|
1940
|
+
${hasCodingSessions ? '<p class="walle-empty-note">Existing Wall-E coding sessions are still available from Status and Agents.</p>' : ''}
|
|
1358
1941
|
</div>
|
|
1359
1942
|
`;
|
|
1360
1943
|
return;
|
|
@@ -1366,6 +1949,11 @@
|
|
|
1366
1949
|
renderWalleShell(active, cards);
|
|
1367
1950
|
} else {
|
|
1368
1951
|
updateWalleContext(active, cards);
|
|
1952
|
+
const health = $('walle-health-card');
|
|
1953
|
+
const nextHealth = walleHealthHtml();
|
|
1954
|
+
if (health && nextHealth) health.outerHTML = nextHealth;
|
|
1955
|
+
else if (health && !nextHealth) health.remove();
|
|
1956
|
+
else if (!health && nextHealth) $('walle-context')?.insertAdjacentHTML('afterend', nextHealth);
|
|
1369
1957
|
}
|
|
1370
1958
|
const activated = activateWalleChat(active);
|
|
1371
1959
|
if (!activated) {
|
|
@@ -1541,11 +2129,25 @@
|
|
|
1541
2129
|
}
|
|
1542
2130
|
if (restart?.status === 'blocked') {
|
|
1543
2131
|
const count = Number(restart.activeSessions || 0);
|
|
1544
|
-
const noun = count === 1 ? '
|
|
2132
|
+
const noun = count === 1 ? 'session' : 'sessions';
|
|
2133
|
+
const blockers = Array.isArray(restart.blockingSessions) ? restart.blockingSessions : [];
|
|
2134
|
+
const restorable = Number(restart.restorableSessions || 0);
|
|
2135
|
+
const blockerNames = blockers
|
|
2136
|
+
.map((item) => String(item?.label || item?.id || '').trim())
|
|
2137
|
+
.filter(Boolean)
|
|
2138
|
+
.slice(0, 3);
|
|
2139
|
+
const blockerText = blockerNames.length
|
|
2140
|
+
? `<span>Blocking: ${esc(blockerNames.join(', '))}${blockers.length > blockerNames.length ? '...' : ''}</span>`
|
|
2141
|
+
: '';
|
|
2142
|
+
const restorableText = restorable > 0
|
|
2143
|
+
? `<span>${restorable} idle/review ${restorable === 1 ? 'session can' : 'sessions can'} reconnect after restart.</span>`
|
|
2144
|
+
: '';
|
|
1545
2145
|
return `
|
|
1546
2146
|
<div class="restart-notice blocked" role="alert">
|
|
1547
|
-
<span class="restart-notice-title">${count || 'Some'} ${noun}
|
|
1548
|
-
<span>Normal restart was blocked to avoid interrupting
|
|
2147
|
+
<span class="restart-notice-title">${count || 'Some'} ${noun} still active</span>
|
|
2148
|
+
<span>Normal restart was blocked to avoid interrupting running work or a waiting approval.</span>
|
|
2149
|
+
${blockerText}
|
|
2150
|
+
${restorableText}
|
|
1549
2151
|
<button id="restart-force-btn" class="row-action warn" type="button">Force restart</button>
|
|
1550
2152
|
</div>
|
|
1551
2153
|
`;
|
|
@@ -1613,6 +2215,9 @@
|
|
|
1613
2215
|
state.restart = {
|
|
1614
2216
|
status: 'blocked',
|
|
1615
2217
|
activeSessions: Number(err.payload?.active_sessions || 0),
|
|
2218
|
+
blockingSessions: Array.isArray(err.payload?.blocking_sessions) ? err.payload.blocking_sessions : [],
|
|
2219
|
+
restorableSessions: Number(err.payload?.restorable_sessions || 0),
|
|
2220
|
+
totalSessions: Number(err.payload?.total_sessions || 0),
|
|
1616
2221
|
message: err.payload?.error || err.message || 'Restart blocked by active sessions.',
|
|
1617
2222
|
};
|
|
1618
2223
|
renderMore();
|
|
@@ -1642,6 +2247,10 @@
|
|
|
1642
2247
|
const copy = {};
|
|
1643
2248
|
for (const key of [
|
|
1644
2249
|
'id', 'sessionId', 'title', 'aiTitle', 'displayTitle', 'userRenamed',
|
|
2250
|
+
'type', 'session_type', 'sessionType', 'runtime_type', 'runtimeType',
|
|
2251
|
+
'walle_owned', 'walleOwned', 'native_walle_session', 'nativeWalleSession',
|
|
2252
|
+
'walle_chat_session', 'walleChatSession',
|
|
2253
|
+
'agentMode', 'agent_mode', 'agentKind', 'agent_kind', 'taskType', 'task_type',
|
|
1645
2254
|
'agent', 'provider', 'model', 'model_id',
|
|
1646
2255
|
'modelProvider', 'model_provider', 'providerType', 'llmProvider',
|
|
1647
2256
|
'walle_model_id', 'walle_model_provider', 'runtime_model_id', 'runtime_model_provider',
|
|
@@ -1656,7 +2265,8 @@
|
|
|
1656
2265
|
copy.worktreeStatus = {};
|
|
1657
2266
|
for (const key of [
|
|
1658
2267
|
'branch', 'worktreeName', 'worktreePath', 'state', 'summary',
|
|
1659
|
-
'dirtyFiles', 'unmergedCommits', '
|
|
2268
|
+
'isMain', 'dirtyFiles', 'unmergedCommits', 'unpushedCommits', 'ahead', 'behind',
|
|
2269
|
+
'needsAttention', 'mainRemote',
|
|
1660
2270
|
]) {
|
|
1661
2271
|
if (wt[key] != null) copy.worktreeStatus[key] = wt[key];
|
|
1662
2272
|
}
|
|
@@ -2220,6 +2830,141 @@
|
|
|
2220
2830
|
}, delayMs);
|
|
2221
2831
|
}
|
|
2222
2832
|
|
|
2833
|
+
function applyWalleHealth(payload) {
|
|
2834
|
+
const alerts = Array.isArray(payload?.alerts) ? payload.alerts : [];
|
|
2835
|
+
state.walle.alerts = alerts;
|
|
2836
|
+
state.walle.health = payload?.summary || null;
|
|
2837
|
+
if (!state.walle.health && alerts.length) {
|
|
2838
|
+
state.walle.health = {
|
|
2839
|
+
level: 'warning',
|
|
2840
|
+
title: 'Wall-E service alerts',
|
|
2841
|
+
message: 'Review current provider, integration, and skill alerts.',
|
|
2842
|
+
counts: { total: alerts.length },
|
|
2843
|
+
current_issue: null,
|
|
2844
|
+
disabled_skills: alerts.filter((a) => String(a?.type || '') === 'skill_disabled'),
|
|
2845
|
+
integration_alerts: alerts.filter((a) => /(auth|owner|reauth|slack|gws|google)/i.test(`${a?.type || ''} ${a?.service || ''} ${a?.action || ''}`)),
|
|
2846
|
+
provider_history: alerts.filter((a) => String(a?.service || '') === 'ai_provider'),
|
|
2847
|
+
system_alerts: [],
|
|
2848
|
+
};
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
async function refreshWalleHealth(options = {}) {
|
|
2853
|
+
try {
|
|
2854
|
+
const payload = await api('/api/wall-e/alerts');
|
|
2855
|
+
applyWalleHealth(payload);
|
|
2856
|
+
return payload;
|
|
2857
|
+
} catch (err) {
|
|
2858
|
+
if (!options.quiet) toast(err?.message || 'Could not load Wall-E health.');
|
|
2859
|
+
return null;
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
async function testWalleHealthProvider() {
|
|
2864
|
+
const health = state.walle.health || {};
|
|
2865
|
+
const issue = health.current_issue || {};
|
|
2866
|
+
const provider = String(issue.provider || health.active_provider?.provider || '').trim();
|
|
2867
|
+
if (!provider) {
|
|
2868
|
+
state.walle.healthTest = { ok: false, message: 'No active provider to test.' };
|
|
2869
|
+
renderWalle();
|
|
2870
|
+
return;
|
|
2871
|
+
}
|
|
2872
|
+
state.walle.healthTest = { ok: true, message: `Testing ${walleHealthProviderLabel(provider)}...` };
|
|
2873
|
+
renderWalle();
|
|
2874
|
+
const params = new URLSearchParams({ provider });
|
|
2875
|
+
const result = await api(`/api/setup/test-key?${params.toString()}`).catch((err) => ({ ok: false, error: err?.message || 'Provider test failed.' }));
|
|
2876
|
+
if (result?.ok) {
|
|
2877
|
+
state.walle.healthTest = { ok: true, message: `${walleHealthProviderLabel(provider)} is reachable.` };
|
|
2878
|
+
if (issue.id) await api(`/api/wall-e/alerts/${encodeURIComponent(issue.id)}`, { method: 'DELETE' }).catch(() => null);
|
|
2879
|
+
await refreshWalleHealth({ quiet: true });
|
|
2880
|
+
} else {
|
|
2881
|
+
state.walle.healthTest = { ok: false, message: result?.error || 'Provider test failed.' };
|
|
2882
|
+
}
|
|
2883
|
+
renderWalle();
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
async function dismissCurrentWalleHealthAlert() {
|
|
2887
|
+
const id = state.walle.health?.current_issue?.id;
|
|
2888
|
+
if (!id) return;
|
|
2889
|
+
await api(`/api/wall-e/alerts/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
2890
|
+
state.walle.healthTest = null;
|
|
2891
|
+
await refreshWalleHealth({ quiet: true });
|
|
2892
|
+
renderWalle();
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
function findWalleHealthAlert(alertId) {
|
|
2896
|
+
const id = String(alertId || '');
|
|
2897
|
+
if (!id) return null;
|
|
2898
|
+
const health = state.walle.health || {};
|
|
2899
|
+
const pools = [
|
|
2900
|
+
state.walle.alerts || [],
|
|
2901
|
+
health.current_issue ? [health.current_issue] : [],
|
|
2902
|
+
health.disabled_skills || [],
|
|
2903
|
+
health.integration_alerts || [],
|
|
2904
|
+
health.provider_history || [],
|
|
2905
|
+
health.system_alerts || [],
|
|
2906
|
+
];
|
|
2907
|
+
for (const pool of pools) {
|
|
2908
|
+
const found = pool.find((item) => String(item?.id || '') === id);
|
|
2909
|
+
if (found) return found;
|
|
2910
|
+
}
|
|
2911
|
+
return null;
|
|
2912
|
+
}
|
|
2913
|
+
|
|
2914
|
+
async function enableWalleHealthSkill(alertId) {
|
|
2915
|
+
const alert = findWalleHealthAlert(alertId);
|
|
2916
|
+
const skillName = walleHealthSkillName(alert);
|
|
2917
|
+
if (!skillName) throw new Error('Could not identify the disabled skill.');
|
|
2918
|
+
state.walle.healthTest = { ok: true, message: `Re-enabling ${skillName}...` };
|
|
2919
|
+
renderWalle();
|
|
2920
|
+
const response = await api('/api/wall-e/skills?enabled=0');
|
|
2921
|
+
const skills = response?.data || response?.skills || [];
|
|
2922
|
+
const match = skills.find((skill) => String(skill?.name || '').toLowerCase() === skillName.toLowerCase());
|
|
2923
|
+
if (!match?.id) throw new Error(`${skillName} is not listed as disabled.`);
|
|
2924
|
+
await api(`/api/wall-e/skills/${encodeURIComponent(match.id)}`, {
|
|
2925
|
+
method: 'PUT',
|
|
2926
|
+
body: { enabled: 1 },
|
|
2927
|
+
});
|
|
2928
|
+
if (alertId) await api(`/api/wall-e/alerts/${encodeURIComponent(alertId)}`, { method: 'DELETE' }).catch(() => null);
|
|
2929
|
+
state.walle.healthTest = { ok: true, message: `${skillName} is enabled.` };
|
|
2930
|
+
await refreshWalleHealth({ quiet: true });
|
|
2931
|
+
renderWalle();
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
async function repairWalleHealthAlert(alertId) {
|
|
2935
|
+
const alert = findWalleHealthAlert(alertId);
|
|
2936
|
+
const action = String(alert?.action || '').toLowerCase();
|
|
2937
|
+
if (action !== 'repair_slack_owner') throw new Error('Open the full Wall-E panel to fix this alert.');
|
|
2938
|
+
state.walle.healthTest = { ok: true, message: 'Fixing Slack owner identity...' };
|
|
2939
|
+
renderWalle();
|
|
2940
|
+
const result = await api('/api/wall-e/slack/repair-owner', { method: 'POST' });
|
|
2941
|
+
if (!result?.ok) throw new Error(result?.error || 'Could not fix Slack owner identity.');
|
|
2942
|
+
state.walle.healthTest = { ok: true, message: 'Slack owner identity is fixed.' };
|
|
2943
|
+
await refreshWalleHealth({ quiet: true });
|
|
2944
|
+
renderWalle();
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
function handleWalleHealthAction(action, alertId) {
|
|
2948
|
+
if (action === 'test-provider') {
|
|
2949
|
+
testWalleHealthProvider().catch((err) => {
|
|
2950
|
+
state.walle.healthTest = { ok: false, message: err?.message || 'Provider test failed.' };
|
|
2951
|
+
renderWalle();
|
|
2952
|
+
});
|
|
2953
|
+
} else if (action === 'dismiss-current') {
|
|
2954
|
+
dismissCurrentWalleHealthAlert().catch((err) => toast(err?.message || 'Could not dismiss alert.'));
|
|
2955
|
+
} else if (action === 'enable-skill') {
|
|
2956
|
+
enableWalleHealthSkill(alertId).catch((err) => {
|
|
2957
|
+
state.walle.healthTest = { ok: false, message: err?.message || 'Could not re-enable skill.' };
|
|
2958
|
+
renderWalle();
|
|
2959
|
+
});
|
|
2960
|
+
} else if (action === 'repair-alert') {
|
|
2961
|
+
repairWalleHealthAlert(alertId).catch((err) => {
|
|
2962
|
+
state.walle.healthTest = { ok: false, message: err?.message || 'Could not fix alert.' };
|
|
2963
|
+
renderWalle();
|
|
2964
|
+
});
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2223
2968
|
async function refresh() {
|
|
2224
2969
|
if (state.refreshInFlight) return state.refreshInFlight;
|
|
2225
2970
|
state.refreshInFlight = (async () => {
|
|
@@ -2227,11 +2972,14 @@
|
|
|
2227
2972
|
if (!navigator.onLine) setConnection('offline');
|
|
2228
2973
|
else if (!state.wsReady) setConnection('stale', 'Syncing');
|
|
2229
2974
|
const activeBefore = state.activeSession;
|
|
2230
|
-
const
|
|
2975
|
+
const shouldLoadWalleHealth = state.activeTab === 'walle';
|
|
2976
|
+
const [auth, standup, walleHealth] = await Promise.all([
|
|
2231
2977
|
api('/api/auth/me'),
|
|
2232
2978
|
api(standupRequestPath()),
|
|
2979
|
+
shouldLoadWalleHealth ? refreshWalleHealth({ quiet: true }) : Promise.resolve(null),
|
|
2233
2980
|
]);
|
|
2234
2981
|
state.auth = auth.auth || null;
|
|
2982
|
+
if (walleHealth) applyWalleHealth(walleHealth);
|
|
2235
2983
|
applyStandupSnapshot(standup);
|
|
2236
2984
|
state.sessionsLoaded = true;
|
|
2237
2985
|
if (activeBefore && findSessionIndex(activeBefore.id) < 0) {
|
|
@@ -2469,7 +3217,7 @@
|
|
|
2469
3217
|
let msg = null;
|
|
2470
3218
|
try { msg = JSON.parse(event.data); } catch { return; }
|
|
2471
3219
|
if (msg.type === 'hello') {
|
|
2472
|
-
|
|
3220
|
+
handleServerHello(msg);
|
|
2473
3221
|
return;
|
|
2474
3222
|
}
|
|
2475
3223
|
if (msg.type === 'server-restarting') {
|
|
@@ -2750,7 +3498,29 @@
|
|
|
2750
3498
|
updateRemoteOutboxStatus();
|
|
2751
3499
|
}
|
|
2752
3500
|
|
|
3501
|
+
function reconcileRemoteOutboxEntryTransport(entry) {
|
|
3502
|
+
if (!entry || entry.type !== 'wall_e.send_message') return true;
|
|
3503
|
+
const card = findSession(entry.sessionId);
|
|
3504
|
+
if ((!card || card._placeholder) && !state.sessionsLoaded) {
|
|
3505
|
+
entry.nextAttemptAt = Date.now() + 500;
|
|
3506
|
+
persistRemoteOutbox();
|
|
3507
|
+
return false;
|
|
3508
|
+
}
|
|
3509
|
+
if (card && isWalleOwnedSession(card) && !isNativeWalleSession(card)) {
|
|
3510
|
+
entry.type = 'session.send_message';
|
|
3511
|
+
entry.body = {
|
|
3512
|
+
session_id: entry.sessionId,
|
|
3513
|
+
text: entry.text,
|
|
3514
|
+
};
|
|
3515
|
+
entry.lastError = '';
|
|
3516
|
+
entry.status = 'queued';
|
|
3517
|
+
persistRemoteOutbox();
|
|
3518
|
+
}
|
|
3519
|
+
return true;
|
|
3520
|
+
}
|
|
3521
|
+
|
|
2753
3522
|
async function deliverRemoteOutboxEntry(entry) {
|
|
3523
|
+
if (!reconcileRemoteOutboxEntryTransport(entry)) return false;
|
|
2754
3524
|
entry.status = 'sending';
|
|
2755
3525
|
entry.attempts = Math.max(0, Number(entry.attempts || 0) || 0) + 1;
|
|
2756
3526
|
entry.lastError = '';
|
|
@@ -2772,6 +3542,9 @@
|
|
|
2772
3542
|
});
|
|
2773
3543
|
}
|
|
2774
3544
|
if (!updateRemoteOutboxStatus()) setComposerStatus('Sent to Mac.', 'ok');
|
|
3545
|
+
if (isWalleSingletonChat(state.activeSession)) {
|
|
3546
|
+
loadMessages(state.activeSession, { background: true, fresh: true }).catch(() => {});
|
|
3547
|
+
}
|
|
2775
3548
|
}
|
|
2776
3549
|
return true;
|
|
2777
3550
|
} catch (err) {
|
|
@@ -2841,6 +3614,7 @@
|
|
|
2841
3614
|
if (state.activeStreamSessionId && state.activeStreamSessionId !== card.id) unsubscribeActiveStream(state.activeStreamSessionId);
|
|
2842
3615
|
state.activeSession = card;
|
|
2843
3616
|
state.activeStreamSessionId = card.id;
|
|
3617
|
+
render();
|
|
2844
3618
|
resetDetailTimeline(card.id);
|
|
2845
3619
|
resetPromptHistory(card.id);
|
|
2846
3620
|
updateDetailChrome(card);
|
|
@@ -2850,10 +3624,11 @@
|
|
|
2850
3624
|
setComposerText('', { focus: false, cursor: false });
|
|
2851
3625
|
autoSizeComposer();
|
|
2852
3626
|
updateComposerState();
|
|
2853
|
-
lockDetailBackgroundScroll();
|
|
2854
3627
|
updateRemoteOutboxStatus();
|
|
2855
3628
|
$('detail-view').classList.add('active');
|
|
2856
3629
|
$('detail-view').setAttribute('aria-hidden', 'false');
|
|
3630
|
+
syncDetailLayoutMode();
|
|
3631
|
+
lockDetailBackgroundScroll();
|
|
2857
3632
|
updateComposerState();
|
|
2858
3633
|
$('detail-messages').innerHTML = card._placeholder
|
|
2859
3634
|
? '<div class="empty-state">Loading session context. You can still type and send a prompt.</div>'
|
|
@@ -2884,7 +3659,9 @@
|
|
|
2884
3659
|
updateComposerState();
|
|
2885
3660
|
$('detail-view').classList.remove('active');
|
|
2886
3661
|
$('detail-view').setAttribute('aria-hidden', 'true');
|
|
3662
|
+
syncDetailLayoutMode();
|
|
2887
3663
|
unlockDetailBackgroundScroll();
|
|
3664
|
+
render();
|
|
2888
3665
|
}
|
|
2889
3666
|
|
|
2890
3667
|
function renderDetailAlert(card) {
|
|
@@ -2903,17 +3680,135 @@
|
|
|
2903
3680
|
}
|
|
2904
3681
|
|
|
2905
3682
|
function isWalleSession(card) {
|
|
2906
|
-
|
|
2907
|
-
|
|
3683
|
+
return isWalleOwnedSession(card) || isNativeWalleSession(card);
|
|
3684
|
+
}
|
|
3685
|
+
|
|
3686
|
+
function normalizedCardType(card) {
|
|
3687
|
+
return String(card?.session_type || card?.sessionType || card?.runtime_type || card?.runtimeType || card?.type || '')
|
|
3688
|
+
.trim()
|
|
3689
|
+
.toLowerCase()
|
|
3690
|
+
.replace(/_/g, '-');
|
|
3691
|
+
}
|
|
3692
|
+
|
|
3693
|
+
function cardCapabilities(card) {
|
|
3694
|
+
return card?.agentCapabilities || card?.capabilities || {};
|
|
3695
|
+
}
|
|
3696
|
+
|
|
3697
|
+
function walleIdentityText(card) {
|
|
3698
|
+
return [
|
|
3699
|
+
card?.agent,
|
|
3700
|
+
card?.provider,
|
|
3701
|
+
card?.type,
|
|
3702
|
+
card?.session_type,
|
|
3703
|
+
card?.sessionType,
|
|
3704
|
+
card?.runtime_type,
|
|
3705
|
+
card?.runtimeType,
|
|
3706
|
+
card?.agentMode,
|
|
3707
|
+
card?.agent_mode,
|
|
3708
|
+
card?.agentKind,
|
|
3709
|
+
card?.agent_kind,
|
|
3710
|
+
card?.taskType,
|
|
3711
|
+
card?.task_type,
|
|
3712
|
+
card?.title,
|
|
3713
|
+
card?.label,
|
|
3714
|
+
card?.branch,
|
|
3715
|
+
].filter(Boolean).join(' ').toLowerCase();
|
|
3716
|
+
}
|
|
3717
|
+
|
|
3718
|
+
function wallePathText(card) {
|
|
3719
|
+
return [
|
|
3720
|
+
card?.cwd,
|
|
3721
|
+
card?.project,
|
|
3722
|
+
card?.projectPath,
|
|
3723
|
+
card?.project_path,
|
|
3724
|
+
card?.worktree_path,
|
|
3725
|
+
card?.worktreeStatus?.worktreePath,
|
|
3726
|
+
].filter(Boolean).join(' ').replace(/\\/g, '/').toLowerCase();
|
|
3727
|
+
}
|
|
3728
|
+
|
|
3729
|
+
function hasWalleCodingIdentity(card) {
|
|
3730
|
+
if (!card) return false;
|
|
3731
|
+
const type = normalizedCardType(card);
|
|
3732
|
+
const mode = String(card.agentMode || card.agent_mode || '').trim().toLowerCase();
|
|
3733
|
+
const taskType = String(card.taskType || card.task_type || '').trim().toLowerCase();
|
|
3734
|
+
const identity = walleIdentityText(card);
|
|
3735
|
+
const pathText = wallePathText(card);
|
|
3736
|
+
if (mode === 'coding' || taskType === 'coding') return true;
|
|
3737
|
+
if (type === 'walle-coding' || type === 'wall-e-coding') return true;
|
|
3738
|
+
if (/wall-?e[-\s]?coding|walle[-\s]?coding|\bcoding agent\b/.test(identity)) return true;
|
|
3739
|
+
if (/\/\.(?:walle|claude)\/worktrees\//.test(pathText)) return true;
|
|
3740
|
+
return false;
|
|
3741
|
+
}
|
|
3742
|
+
|
|
3743
|
+
function hasWalleChatIdentity(card) {
|
|
3744
|
+
if (!card) return false;
|
|
3745
|
+
const type = normalizedCardType(card);
|
|
3746
|
+
const mode = String(card.agentMode || card.agent_mode || '').trim().toLowerCase();
|
|
3747
|
+
const taskType = String(card.taskType || card.task_type || '').trim().toLowerCase();
|
|
3748
|
+
const identity = walleIdentityText(card);
|
|
3749
|
+
if (mode === 'coding' || taskType === 'coding') return false;
|
|
3750
|
+
if (card.walle_chat_session === true || card.walleChatSession === true) return true;
|
|
3751
|
+
if (mode === 'chat' || taskType === 'chat') return true;
|
|
3752
|
+
if (type === 'walle-chat' || type === 'wall-e-chat') return true;
|
|
3753
|
+
return /wall-?e[-\s]?chat|walle[-\s]?chat/.test(identity);
|
|
3754
|
+
}
|
|
3755
|
+
|
|
3756
|
+
function isNativeWalleSession(card) {
|
|
3757
|
+
if (!card) return false;
|
|
3758
|
+
if (card.native_walle_session === true || card.nativeWalleSession === true) return true;
|
|
3759
|
+
const type = normalizedCardType(card);
|
|
3760
|
+
if (type === 'walle' || type === 'wall-e' || type === 'walle-chat' || type === 'wall-e-chat') return true;
|
|
3761
|
+
const agentKind = String(card.agentKind || card.agent_kind || card.taskType || card.task_type || '').toLowerCase();
|
|
3762
|
+
if (/walle-chat|wall-e-chat/.test(agentKind)) return true;
|
|
3763
|
+
const agent = String(card.agent || '').toLowerCase();
|
|
3764
|
+
const provider = String(card.provider || '').toLowerCase();
|
|
3765
|
+
const caps = cardCapabilities(card);
|
|
3766
|
+
const hasTerminalCapability = caps.terminalControl === true || caps.terminalInput === true || caps.canSendEscape === true;
|
|
3767
|
+
const looksLikeCodingAgent = /codex|claude|gemini|opencode|terminal|pty|coding/.test(`${agent} ${card.title || ''} ${agentKind}`);
|
|
3768
|
+
return /^(walle|wall-e)$/.test(agent || provider) && !hasTerminalCapability && !looksLikeCodingAgent;
|
|
3769
|
+
}
|
|
3770
|
+
|
|
3771
|
+
function isWalleChatSession(card) {
|
|
3772
|
+
if (!card) return false;
|
|
3773
|
+
if (!isNativeWalleSession(card) && !isWalleOwnedSession(card)) return false;
|
|
3774
|
+
if (hasWalleChatIdentity(card)) return true;
|
|
3775
|
+
if (hasWalleCodingIdentity(card)) return false;
|
|
3776
|
+
if (card.native_walle_session === true || card.nativeWalleSession === true) return true;
|
|
3777
|
+
const type = normalizedCardType(card);
|
|
3778
|
+
if (type === 'walle' || type === 'wall-e') return true;
|
|
3779
|
+
const agent = String(card.agent || '').toLowerCase();
|
|
3780
|
+
const provider = String(card.provider || '').toLowerCase();
|
|
3781
|
+
const caps = cardCapabilities(card);
|
|
3782
|
+
const hasTerminalCapability = caps.terminalControl === true || caps.terminalInput === true || caps.canSendEscape === true;
|
|
3783
|
+
const looksLikeCodingAgent = /codex|claude|gemini|opencode|terminal|pty|coding/.test(walleIdentityText(card));
|
|
3784
|
+
return /^(walle|wall-e)$/.test(agent || provider) && !hasTerminalCapability && !looksLikeCodingAgent;
|
|
3785
|
+
}
|
|
3786
|
+
|
|
3787
|
+
function isWalleOwnedSession(card) {
|
|
3788
|
+
if (!card) return false;
|
|
3789
|
+
if (card.walle_owned === true || card.walleOwned === true) return true;
|
|
3790
|
+
if (isNativeWalleSession(card)) return true;
|
|
3791
|
+
const kind = `${card.agent || ''} ${card.provider || ''} ${card.type || ''} ${card.agentKind || ''} ${card.taskType || ''} ${card.title || ''} ${card.branch || ''} ${card.cwd || ''} ${card.project || ''}`;
|
|
3792
|
+
if (/walle|wall-e/i.test(`${card.provider || ''} ${card.agentKind || ''} ${card.taskType || ''}`)) return true;
|
|
3793
|
+
if (/walle[-\s]?coding|wall[-\s]?e[-\s]?coding/i.test(kind)) return true;
|
|
3794
|
+
if (card.walle_model_id || card.walle_model_provider) return true;
|
|
3795
|
+
return false;
|
|
2908
3796
|
}
|
|
2909
3797
|
|
|
2910
3798
|
function isWalleCodingSession(card) {
|
|
2911
|
-
if (!card || !
|
|
2912
|
-
|
|
3799
|
+
if (!card || !isWalleOwnedSession(card)) return false;
|
|
3800
|
+
if (isWalleSingletonChat(card)) return false;
|
|
3801
|
+
const caps = cardCapabilities(card);
|
|
2913
3802
|
if (caps.structuredTranscript === false && caps.terminalInput === false && caps.review === false) return false;
|
|
2914
3803
|
return true;
|
|
2915
3804
|
}
|
|
2916
3805
|
|
|
3806
|
+
function walleTransportSessionId(card) {
|
|
3807
|
+
return isWalleSingletonChat(card)
|
|
3808
|
+
? WALLE_SINGLETON_CHAT_ID
|
|
3809
|
+
: String(card?.id || card?.sessionId || '').trim();
|
|
3810
|
+
}
|
|
3811
|
+
|
|
2917
3812
|
function terminalControlCapability(card) {
|
|
2918
3813
|
const caps = card?.capabilities || card?.agentCapabilities || {};
|
|
2919
3814
|
if (caps.terminalControl === true || caps.terminalInput === true || caps.canSendEscape === true) return true;
|
|
@@ -2923,7 +3818,7 @@
|
|
|
2923
3818
|
|
|
2924
3819
|
function isTerminalControlSession(card) {
|
|
2925
3820
|
if (!card) return false;
|
|
2926
|
-
if (
|
|
3821
|
+
if (isNativeWalleSession(card)) return false;
|
|
2927
3822
|
const explicit = terminalControlCapability(card);
|
|
2928
3823
|
if (explicit !== null) return explicit;
|
|
2929
3824
|
const kind = `${card.agent || ''} ${card.provider || ''} ${card.title || ''} ${card.type || ''}`;
|
|
@@ -2950,12 +3845,24 @@
|
|
|
2950
3845
|
}
|
|
2951
3846
|
try {
|
|
2952
3847
|
const fresh = opts.fresh ? '&nocache=1' : '';
|
|
2953
|
-
const data =
|
|
3848
|
+
const data = isWalleSingletonChat(card)
|
|
3849
|
+
? await api(`/api/wall-e/chat/history?session_id=${encodeURIComponent(WALLE_SINGLETON_CHAT_ID)}&limit=200${fresh}`)
|
|
3850
|
+
: await api(`/api/session/messages?id=${encodeURIComponent(card.id)}&offset=0&limit=80${fresh}`);
|
|
2954
3851
|
if (state.activeSession?.id !== card.id) return;
|
|
2955
|
-
const
|
|
3852
|
+
const rawMessages = Array.isArray(data) ? data : (data.data || data.messages || data.items || []);
|
|
3853
|
+
const messages = isWalleSingletonChat(card)
|
|
3854
|
+
? rawMessages.map((message) => ({
|
|
3855
|
+
role: message.role,
|
|
3856
|
+
text: message.content || message.text || '',
|
|
3857
|
+
timestamp: message.created_at || message.createdAt || message.timestamp || new Date().toISOString(),
|
|
3858
|
+
uuid: message.id || message.uuid || undefined,
|
|
3859
|
+
agentLabel: message.role === 'assistant' ? 'Wall-E' : undefined,
|
|
3860
|
+
attachments: message.attachments,
|
|
3861
|
+
}))
|
|
3862
|
+
: rawMessages;
|
|
2956
3863
|
renderMessages(messages, { forceBottom: opts.forceBottom !== false });
|
|
2957
3864
|
seedPromptHistoryFromTimeline();
|
|
2958
|
-
if (!background) warmPromptHistory(card).catch(() => {});
|
|
3865
|
+
if (!background && !isWalleSingletonChat(card)) warmPromptHistory(card).catch(() => {});
|
|
2959
3866
|
} catch (err) {
|
|
2960
3867
|
if (state.activeSession?.id !== card.id) return;
|
|
2961
3868
|
if (state.detail.messages.length || state.detail.liveTail || hasTimelineContent(box)) {
|
|
@@ -3210,6 +4117,38 @@
|
|
|
3210
4117
|
return recent.includes(live);
|
|
3211
4118
|
}
|
|
3212
4119
|
|
|
4120
|
+
function normalizedTimelineTextMatches(candidate, reference) {
|
|
4121
|
+
const left = normalizeCompareText(candidate);
|
|
4122
|
+
const right = normalizeCompareText(reference);
|
|
4123
|
+
if (!left || !right || Math.min(left.length, right.length) < 12) return false;
|
|
4124
|
+
if (left === right) return true;
|
|
4125
|
+
if (Math.min(left.length, right.length) < 24) return false;
|
|
4126
|
+
return left.includes(right) || right.includes(left);
|
|
4127
|
+
}
|
|
4128
|
+
|
|
4129
|
+
function timelineMessageMatchesText(item, text, roles = null) {
|
|
4130
|
+
if (Array.isArray(roles) && !roles.includes(mobileReviewRole(item))) return false;
|
|
4131
|
+
return normalizedTimelineTextMatches(messageText(item), text);
|
|
4132
|
+
}
|
|
4133
|
+
|
|
4134
|
+
function latestDurableUserMessageIndex() {
|
|
4135
|
+
for (let index = state.detail.messages.length - 1; index >= 0; index -= 1) {
|
|
4136
|
+
if (mobileReviewRole(state.detail.messages[index]) === 'user') return index;
|
|
4137
|
+
}
|
|
4138
|
+
return -1;
|
|
4139
|
+
}
|
|
4140
|
+
|
|
4141
|
+
function durableUserPromptIndex(text) {
|
|
4142
|
+
for (let index = state.detail.messages.length - 1; index >= 0; index -= 1) {
|
|
4143
|
+
if (timelineMessageMatchesText(state.detail.messages[index], text, ['user'])) return index;
|
|
4144
|
+
}
|
|
4145
|
+
return -1;
|
|
4146
|
+
}
|
|
4147
|
+
|
|
4148
|
+
function durableTimelineCoversMessageText(text, roles = null) {
|
|
4149
|
+
return state.detail.messages.some((item) => timelineMessageMatchesText(item, text, roles));
|
|
4150
|
+
}
|
|
4151
|
+
|
|
3213
4152
|
function cleanTerminalTail(tail) {
|
|
3214
4153
|
const rows = stripTerminalPromptPlaceholderRows(cleanLiveTerminalRows(normalizeLiveTerminalRows(tail?.rows, stripAnsi(tail?.text || ''))));
|
|
3215
4154
|
return {
|
|
@@ -3449,10 +4388,14 @@
|
|
|
3449
4388
|
}
|
|
3450
4389
|
|
|
3451
4390
|
function promptContinuationFallback(promptText, rows, promptIndex) {
|
|
4391
|
+
// Codex can echo pasted or soft-wrapped prompts as several PTY rows where
|
|
4392
|
+
// only the first row has the prompt glyph. Treat likely continuation rows
|
|
4393
|
+
// as user text until a terminal boundary appears, so prompt fragments are
|
|
4394
|
+
// not rendered as assistant output.
|
|
3452
4395
|
const text = String(promptText || '').trim();
|
|
3453
4396
|
const nextRow = rows[promptIndex + 1];
|
|
3454
4397
|
const next = String(nextRow?.text || '').trim();
|
|
3455
|
-
if (!text || !next
|
|
4398
|
+
if (!text || !next) return false;
|
|
3456
4399
|
if (!isPromptContinuationRow(nextRow)) return false;
|
|
3457
4400
|
const statusIndex = findBusyStatusBeforeNextPrompt(rows, promptIndex);
|
|
3458
4401
|
if (statusIndex > promptIndex + 1 && text.length >= 64) return true;
|
|
@@ -3505,20 +4448,21 @@
|
|
|
3505
4448
|
role: 'user',
|
|
3506
4449
|
rows: promptText ? [{ text: promptText, wrapped: false, wrapKnown: row.wrapKnown }] : [],
|
|
3507
4450
|
fallbackContinuation: promptContinuationFallback(promptText, rows, index),
|
|
3508
|
-
fallbackCount: 0,
|
|
3509
4451
|
};
|
|
3510
4452
|
continue;
|
|
3511
4453
|
}
|
|
3512
4454
|
if (isTerminalDividerLine(line)) continue;
|
|
3513
|
-
if (isTerminalChromeLine(line))
|
|
4455
|
+
if (isTerminalChromeLine(line) || isTerminalStatusLine(line)) {
|
|
4456
|
+
flush();
|
|
4457
|
+
continue;
|
|
4458
|
+
}
|
|
3514
4459
|
if (current?.role === 'user') {
|
|
3515
|
-
if (row.wrapped || (current.fallbackContinuation &&
|
|
4460
|
+
if (row.wrapped || (current.fallbackContinuation && isPromptContinuationRow(row))) {
|
|
3516
4461
|
current.rows.push({
|
|
3517
4462
|
text: line,
|
|
3518
4463
|
wrapped: true,
|
|
3519
4464
|
wrapKnown: row.wrapKnown,
|
|
3520
4465
|
});
|
|
3521
|
-
if (!row.wrapped) current.fallbackCount += 1;
|
|
3522
4466
|
continue;
|
|
3523
4467
|
}
|
|
3524
4468
|
flush();
|
|
@@ -3574,12 +4518,14 @@
|
|
|
3574
4518
|
}
|
|
3575
4519
|
|
|
3576
4520
|
function renderLiveTerminalPromptTurn(promptTurn, responseTurns, tail, index) {
|
|
4521
|
+
// A prompt echo without any agent output is only provisional terminal state. The
|
|
4522
|
+
// grouped conversation timeline should show durable turns or live answer content,
|
|
4523
|
+
// not a duplicate "You live / no replies" card for the active composer prompt.
|
|
4524
|
+
if (!responseTurns.length) return '';
|
|
3577
4525
|
const key = mobileLiveTurnKey(tail, promptTurn?.text || '', index);
|
|
3578
4526
|
const expanded = isTimelineTurnExpanded(key);
|
|
3579
4527
|
const promptBody = esc(promptTurn?.text || '').replace(/\n/g, '<br>');
|
|
3580
|
-
const responseHtml = responseTurns.length
|
|
3581
|
-
? responseTurns.map((turn, responseIndex) => renderLiveTerminalResponse(turn, tail, responseIndex, responseTurns.length)).join('')
|
|
3582
|
-
: '<div class="prompt-turn-empty">Live response is still streaming.</div>';
|
|
4528
|
+
const responseHtml = responseTurns.map((turn, responseIndex) => renderLiveTerminalResponse(turn, tail, responseIndex, responseTurns.length)).join('');
|
|
3583
4529
|
return `
|
|
3584
4530
|
<section class="prompt-turn live-prompt-turn live-tail-row${expanded ? ' expanded' : ''}" data-mobile-turn-key="${esc(key)}" data-turn-id="${esc(key)}" data-live-tail="true" data-live-tail-kind="prompt">
|
|
3585
4531
|
<div class="prompt-turn-header" role="button" tabindex="0" aria-expanded="${expanded ? 'true' : 'false'}">
|
|
@@ -3633,10 +4579,13 @@
|
|
|
3633
4579
|
|
|
3634
4580
|
function renderLiveTerminalConversationTurns(turns, tail) {
|
|
3635
4581
|
const rows = [];
|
|
4582
|
+
const latestUserIndex = latestDurableUserMessageIndex();
|
|
3636
4583
|
for (let index = 0; index < turns.length; index += 1) {
|
|
3637
4584
|
const turn = turns[index];
|
|
3638
4585
|
if (turn?.role !== 'user') {
|
|
3639
|
-
|
|
4586
|
+
if (!durableTimelineCoversMessageText(turn?.text || '', ['assistant', 'summary', 'system'])) {
|
|
4587
|
+
rows.push(renderLiveTerminalStandaloneTurn(turn, tail, index));
|
|
4588
|
+
}
|
|
3640
4589
|
continue;
|
|
3641
4590
|
}
|
|
3642
4591
|
const responses = [];
|
|
@@ -3645,7 +4594,19 @@
|
|
|
3645
4594
|
responses.push(turns[cursor]);
|
|
3646
4595
|
cursor += 1;
|
|
3647
4596
|
}
|
|
3648
|
-
|
|
4597
|
+
const visibleResponses = responses.filter((response) => (
|
|
4598
|
+
!durableTimelineCoversMessageText(response?.text || '', ['assistant', 'summary', 'system'])
|
|
4599
|
+
));
|
|
4600
|
+
const durablePromptIndex = durableUserPromptIndex(turn?.text || '');
|
|
4601
|
+
if (durablePromptIndex >= 0) {
|
|
4602
|
+
if (durablePromptIndex >= latestUserIndex) {
|
|
4603
|
+
visibleResponses.forEach((response, responseIndex) => {
|
|
4604
|
+
rows.push(renderLiveTerminalStandaloneTurn(response, tail, `${index}-${responseIndex}`));
|
|
4605
|
+
});
|
|
4606
|
+
}
|
|
4607
|
+
} else {
|
|
4608
|
+
rows.push(renderLiveTerminalPromptTurn(turn, visibleResponses, tail, index));
|
|
4609
|
+
}
|
|
3649
4610
|
index = cursor - 1;
|
|
3650
4611
|
}
|
|
3651
4612
|
return rows.join('');
|
|
@@ -3831,8 +4792,10 @@
|
|
|
3831
4792
|
rememberTimelineTurnExpanded(turn.dataset.mobileTurnKey || turn.dataset.turnId || '', next);
|
|
3832
4793
|
};
|
|
3833
4794
|
const toggle = (ev) => {
|
|
4795
|
+
if (window.MR?.shouldIgnorePromptTurnHeaderToggle?.(ev, header)) return;
|
|
3834
4796
|
if (ev?.target?.closest?.('a,button,input,textarea,select')) return;
|
|
3835
|
-
if (
|
|
4797
|
+
if (ev?.target?.closest?.('.prompt-turn-prompt .msg-text')) return;
|
|
4798
|
+
if (window.MR?.hasTextSelectionInside?.(header)) return;
|
|
3836
4799
|
const next = !turn.classList.contains('expanded');
|
|
3837
4800
|
setExpanded(next);
|
|
3838
4801
|
};
|
|
@@ -4122,10 +5085,11 @@
|
|
|
4122
5085
|
const button = $('detail-send');
|
|
4123
5086
|
if (!menu || !button || button.disabled) return false;
|
|
4124
5087
|
hideMobileSkillPicker();
|
|
5088
|
+
updateModelPickerButtons();
|
|
4125
5089
|
menu.hidden = false;
|
|
4126
5090
|
button.setAttribute('aria-expanded', 'true');
|
|
4127
5091
|
state.sendMenu.suppressNextClick = opts.suppressNextClick !== false;
|
|
4128
|
-
if (opts.focus !== false) (
|
|
5092
|
+
if (opts.focus !== false) menu.querySelector('.send-menu-item:not([hidden]):not(:disabled)')?.focus({ preventScroll: true });
|
|
4129
5093
|
return true;
|
|
4130
5094
|
}
|
|
4131
5095
|
|
|
@@ -4243,9 +5207,19 @@
|
|
|
4243
5207
|
|
|
4244
5208
|
function updateModelPickerButtons() {
|
|
4245
5209
|
const visible = isWalleCodingSession(state.activeSession);
|
|
5210
|
+
const detailVisible = visible && isDetailOpen();
|
|
4246
5211
|
const label = visible ? modelButtonShortLabel(state.activeSession) : 'Model';
|
|
5212
|
+
const fullLabel = visible ? modelButtonLabel(state.activeSession) : 'Choose model';
|
|
5213
|
+
const sendModelOption = $('send-model-option');
|
|
5214
|
+
if (sendModelOption) {
|
|
5215
|
+
sendModelOption.hidden = !detailVisible;
|
|
5216
|
+
sendModelOption.disabled = !detailVisible || state.composerSending;
|
|
5217
|
+
sendModelOption.title = detailVisible ? `Choose model: ${fullLabel}` : 'Choose model';
|
|
5218
|
+
sendModelOption.setAttribute('aria-expanded', detailVisible && state.modelPicker.open ? 'true' : 'false');
|
|
5219
|
+
const value = $('send-model-label');
|
|
5220
|
+
if (value) value.textContent = detailVisible ? fullLabel : 'Auto model';
|
|
5221
|
+
}
|
|
4247
5222
|
const controls = [
|
|
4248
|
-
{ input: $('detail-input'), form: $('detail-composer'), button: $('detail-model'), surface: 'detail' },
|
|
4249
5223
|
{ input: $('walle-input'), form: $('walle-chat-composer'), button: $('walle-model'), surface: 'walle' },
|
|
4250
5224
|
];
|
|
4251
5225
|
for (const control of controls) {
|
|
@@ -4260,6 +5234,8 @@
|
|
|
4260
5234
|
if (value) value.textContent = label;
|
|
4261
5235
|
form.classList.toggle('has-model-picker', show);
|
|
4262
5236
|
}
|
|
5237
|
+
const detailForm = $('detail-composer');
|
|
5238
|
+
if (detailForm) detailForm.classList.remove('has-model-picker');
|
|
4263
5239
|
}
|
|
4264
5240
|
|
|
4265
5241
|
function renderModelPickerSheet() {
|
|
@@ -5266,7 +6242,7 @@
|
|
|
5266
6242
|
}
|
|
5267
6243
|
const outboundText = buildComposerTransportText(text, attachments);
|
|
5268
6244
|
try {
|
|
5269
|
-
const messageType =
|
|
6245
|
+
const messageType = isNativeWalleSession(card) ? 'wall_e.send_message' : 'session.send_message';
|
|
5270
6246
|
const body = { session_id: card.id, text: outboundText };
|
|
5271
6247
|
if (messageType === 'wall_e.send_message') {
|
|
5272
6248
|
if (attachments.length) body.attachments = attachments;
|
|
@@ -5300,7 +6276,7 @@
|
|
|
5300
6276
|
if (state.activeSession?.id !== card.id) activateWalleChat(card);
|
|
5301
6277
|
const outboundText = buildComposerTransportText(text, attachments);
|
|
5302
6278
|
try {
|
|
5303
|
-
const body = { session_id: card
|
|
6279
|
+
const body = { session_id: walleTransportSessionId(card), text: outboundText };
|
|
5304
6280
|
if (attachments.length) body.attachments = attachments;
|
|
5305
6281
|
appendWalleModelSelection(body, card);
|
|
5306
6282
|
const entry = enqueueRemoteReply('wall_e.send_message', body);
|
|
@@ -5654,10 +6630,11 @@
|
|
|
5654
6630
|
btn.addEventListener('click', () => setTab(btn.dataset.tab));
|
|
5655
6631
|
});
|
|
5656
6632
|
document.body.addEventListener('click', (event) => {
|
|
5657
|
-
const open = event.target.closest('[data-open-session]');
|
|
5658
|
-
if (open) openDetail(open.dataset.openSession);
|
|
6633
|
+
const open = event.target.closest('[data-open-session], [data-open-worktree-session]');
|
|
6634
|
+
if (open) openDetail(open.dataset.openSession || open.dataset.openWorktreeSession);
|
|
5659
6635
|
});
|
|
5660
6636
|
$('refresh-btn').addEventListener('click', refresh);
|
|
6637
|
+
$('app-reload-now-btn')?.addEventListener('click', () => reloadForInstalledUpdate('banner'));
|
|
5661
6638
|
$('detail-back').addEventListener('click', closeDetail);
|
|
5662
6639
|
$('detail-title')?.addEventListener('dblclick', startDetailTitleEdit);
|
|
5663
6640
|
$('detail-title')?.addEventListener('pointerup', handleDetailTitlePointerUp);
|
|
@@ -5676,7 +6653,6 @@
|
|
|
5676
6653
|
$('detail-send').addEventListener('click', handleSendButtonClick);
|
|
5677
6654
|
$('detail-send').addEventListener('contextmenu', handleSendButtonContextMenu);
|
|
5678
6655
|
$('detail-send').addEventListener('keydown', handleSendButtonKeydown);
|
|
5679
|
-
$('detail-model')?.addEventListener('click', () => openModelPickerForActiveSession($('detail-input')));
|
|
5680
6656
|
$('model-picker-close')?.addEventListener('click', closeModelPicker);
|
|
5681
6657
|
$('model-picker-sheet')?.addEventListener('click', (event) => {
|
|
5682
6658
|
if (event.target?.id === 'model-picker-sheet') {
|
|
@@ -5693,6 +6669,7 @@
|
|
|
5693
6669
|
$('prompt-history-next')?.addEventListener('click', () => applyPromptHistoryNavigation('next'));
|
|
5694
6670
|
$('prompt-history-edit')?.addEventListener('click', finishPromptHistoryEditing);
|
|
5695
6671
|
$('prompt-history-close')?.addEventListener('click', closePromptHistoryStrip);
|
|
6672
|
+
$('send-model-option')?.addEventListener('click', () => openModelPickerForActiveSession($('detail-input')));
|
|
5696
6673
|
$('send-copy-url-option')?.addEventListener('click', copySessionUrlFromMenu);
|
|
5697
6674
|
$('send-esc-option')?.addEventListener('click', sendEscapeToSession);
|
|
5698
6675
|
$('send-attach-image-option')?.addEventListener('click', () => triggerAttachmentInput('image'));
|
|
@@ -5748,6 +6725,19 @@
|
|
|
5748
6725
|
updateMobileSkillPicker(event.target);
|
|
5749
6726
|
});
|
|
5750
6727
|
$('walle-content')?.addEventListener('click', (event) => {
|
|
6728
|
+
const healthAction = event.target.closest?.('[data-walle-health-action]');
|
|
6729
|
+
if (healthAction) {
|
|
6730
|
+
event.preventDefault();
|
|
6731
|
+
handleWalleHealthAction(
|
|
6732
|
+
healthAction.dataset.walleHealthAction,
|
|
6733
|
+
healthAction.dataset.walleHealthAlertId,
|
|
6734
|
+
);
|
|
6735
|
+
return;
|
|
6736
|
+
}
|
|
6737
|
+
if (event.target.closest?.('#walle-start-chat')) {
|
|
6738
|
+
startWalleChat().catch((err) => toast(err?.message || 'Could not start Wall-E chat.'));
|
|
6739
|
+
return;
|
|
6740
|
+
}
|
|
5751
6741
|
const input = $('walle-input');
|
|
5752
6742
|
if (event.target?.id === 'walle-input') updateMobileSkillPicker(event.target);
|
|
5753
6743
|
if (event.target.closest?.('#walle-model')) {
|
|
@@ -5828,7 +6818,15 @@
|
|
|
5828
6818
|
applySearchSuggestion(chip.dataset.searchSuggestion || '');
|
|
5829
6819
|
});
|
|
5830
6820
|
window.addEventListener('hashchange', handleDeepLink);
|
|
5831
|
-
window.addEventListener('resize',
|
|
6821
|
+
window.addEventListener('resize', () => {
|
|
6822
|
+
updateMobileViewportInset();
|
|
6823
|
+
syncDetailLayoutMode();
|
|
6824
|
+
});
|
|
6825
|
+
try {
|
|
6826
|
+
const detailSplitMedia = window.matchMedia && window.matchMedia(DETAIL_SPLIT_QUERY);
|
|
6827
|
+
if (detailSplitMedia?.addEventListener) detailSplitMedia.addEventListener('change', syncDetailLayoutMode);
|
|
6828
|
+
else detailSplitMedia?.addListener?.(syncDetailLayoutMode);
|
|
6829
|
+
} catch {}
|
|
5832
6830
|
window.visualViewport?.addEventListener?.('resize', updateMobileViewportInset);
|
|
5833
6831
|
window.visualViewport?.addEventListener?.('scroll', updateMobileViewportInset);
|
|
5834
6832
|
window.addEventListener('online', () => {
|
|
@@ -5865,12 +6863,14 @@
|
|
|
5865
6863
|
async function init() {
|
|
5866
6864
|
loadThemePreference();
|
|
5867
6865
|
updateMobileViewportInset();
|
|
6866
|
+
syncDetailLayoutMode();
|
|
5868
6867
|
configureComposerInput();
|
|
5869
6868
|
bindEvents();
|
|
5870
6869
|
loadRemoteOutbox();
|
|
5871
6870
|
restoreSessionSnapshot();
|
|
5872
6871
|
handleDeepLink();
|
|
5873
6872
|
render();
|
|
6873
|
+
initAppReloadChannel();
|
|
5874
6874
|
connectWs().catch(() => {});
|
|
5875
6875
|
const firstRefresh = refresh().catch(() => {});
|
|
5876
6876
|
loadProjects().catch(() => {});
|
|
@@ -5891,6 +6891,9 @@
|
|
|
5891
6891
|
normalizeMobileSkillItems,
|
|
5892
6892
|
getComposerText: () => getComposerText(),
|
|
5893
6893
|
recoverMobileConnection,
|
|
6894
|
+
handleServerHello,
|
|
6895
|
+
requestInstalledUpdateReload,
|
|
6896
|
+
isMobileReloadUnsafe,
|
|
5894
6897
|
handleWsMessage: (msg) => onWsMessage({ data: JSON.stringify(msg) }),
|
|
5895
6898
|
sessionSnapshot: () => state.sessions.map((item) => ({
|
|
5896
6899
|
id: item.id,
|