create-walle 0.9.20 → 0.9.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/template/claude-task-manager/db.js +131 -0
  4. package/template/claude-task-manager/docs/microsoft-dev-tunnel-phone-access-design.md +58 -50
  5. package/template/claude-task-manager/docs/phone-access-design.md +23 -7
  6. package/template/claude-task-manager/docs/walle-session-model-preferences.md +119 -0
  7. package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +32 -48
  8. package/template/claude-task-manager/lib/remote-relay-protocol.js +5 -0
  9. package/template/claude-task-manager/lib/walle-external-actions.js +20 -3
  10. package/template/claude-task-manager/public/index.html +25 -0
  11. package/template/claude-task-manager/public/js/setup.js +16 -12
  12. package/template/claude-task-manager/public/js/walle-session.js +31 -3
  13. package/template/claude-task-manager/public/js/walle.js +93 -23
  14. package/template/claude-task-manager/public/m/app.css +417 -21
  15. package/template/claude-task-manager/public/m/app.js +831 -44
  16. package/template/claude-task-manager/public/m/claim.html +1 -1
  17. package/template/claude-task-manager/public/m/index.html +41 -7
  18. package/template/claude-task-manager/public/m/sw.js +1 -1
  19. package/template/claude-task-manager/server.js +377 -30
  20. package/template/claude-task-manager/workers/state-detectors/codex.js +18 -3
  21. package/template/package.json +1 -1
  22. package/template/wall-e/chat.js +32 -2
  23. package/template/wall-e/coding/stream-processor.js +36 -0
  24. package/template/wall-e/coding-orchestrator.js +45 -0
  25. package/template/wall-e/docs/external-action-controller.md +60 -2
  26. package/template/wall-e/external-action-controller.js +23 -1
  27. package/template/wall-e/external-action-gateway.js +163 -0
  28. package/template/wall-e/fly.toml +1 -0
  29. package/template/wall-e/tools/local-tools.js +122 -4
  30. package/template/website/index.html +2 -2
@@ -19,7 +19,6 @@ const HOST_READY_TIMEOUT_MS = 45000;
19
19
  const WATCHDOG_CHECK_INTERVAL_MS = 30000;
20
20
  const WATCHDOG_WAKE_DRIFT_MS = 90000;
21
21
  const PUBLIC_PROBE_TIMEOUT_MS = 6000;
22
- const ANONYMOUS_ACCESS_EXPIRATION = '30d';
23
22
  const HOST_HEADER_MODE_PASSTHROUGH = 'passthrough';
24
23
  const HOST_HEADER_MODE_DEFAULT = 'default';
25
24
 
@@ -232,16 +231,8 @@ function devTunnelAccessListArgs(tunnelId, options = {}) {
232
231
  return ['access', 'list', tunnelId, '-p', String(options.port || 3456), '-j'];
233
232
  }
234
233
 
235
- function devTunnelAnonymousAccessArgs(tunnelId, options = {}) {
236
- return [
237
- 'access',
238
- 'create',
239
- tunnelId,
240
- '-p', String(options.port || 3456),
241
- '--anonymous',
242
- '--expiration', ANONYMOUS_ACCESS_EXPIRATION,
243
- '--scopes', 'connect',
244
- ];
234
+ function devTunnelPrivateAccessResetArgs(tunnelId, options = {}) {
235
+ return ['access', 'reset', tunnelId, '-p', String(options.port || 3456), '-j'];
245
236
  }
246
237
 
247
238
  function caffeinateArgs(options = {}) {
@@ -492,13 +483,14 @@ async function annotateWatchdogState(partial, options = {}) {
492
483
  function managedTunnelAccessSummary(partial = {}, options = {}) {
493
484
  const port = Number(options.port || 3456);
494
485
  return {
495
- mode: 'ctm_authenticated_public',
496
- anonymous_connect: partial.anonymous_connect === true,
486
+ mode: 'private_microsoft',
487
+ anonymous_connect: false,
497
488
  port,
498
- expiration: ANONYMOUS_ACCESS_EXPIRATION,
499
489
  checked_at: partial.checked_at || new Date().toISOString(),
500
490
  already_configured: !!partial.already_configured,
501
- created: !!partial.created,
491
+ reset: !!partial.reset,
492
+ anonymous_removed: !!partial.anonymous_removed,
493
+ list_error: partial.list_error || '',
502
494
  error: partial.error || '',
503
495
  };
504
496
  }
@@ -508,7 +500,7 @@ function persistManagedTunnelAccess(tunnelId, access, options = {}) {
508
500
  if (!state || normalizeTunnelId(state.tunnel_id || '') !== normalizeTunnelId(tunnelId || '')) return;
509
501
  writeManagedTunnelState({
510
502
  ...state,
511
- access_mode: access.mode || 'ctm_authenticated_public',
503
+ access_mode: access.mode || 'private_microsoft',
512
504
  access,
513
505
  last_access_check_at: access.checked_at || new Date().toISOString(),
514
506
  access_error: access.error || '',
@@ -789,7 +781,7 @@ function microsoftDevTunnelProbeDiagnosis(status, wwwAuthenticate, location) {
789
781
  diagnosis: 'tunnel_auth_required',
790
782
  blocked_by_tunnel_auth: true,
791
783
  ctm_reachable: false,
792
- message: 'Microsoft Dev Tunnels is reachable, but its access gate is blocking browser access before CTM receives the request.',
784
+ message: 'Microsoft Dev Tunnels is reachable and the private access gate is asking the browser to sign in before CTM receives the request.',
793
785
  };
794
786
  }
795
787
  if (status >= 300 && status < 400 && /(?:tunnels\.api\.visualstudio\.com\/auth|\/auth\/(?:github|microsoft|entra))/i.test(redirect)) {
@@ -1137,7 +1129,7 @@ function microsoftDevTunnelCommands(options = {}) {
1137
1129
  login: command('devtunnel', ['user', 'login', '-g']),
1138
1130
  device_login: command('devtunnel', ['user', 'login', '-g', '-d']),
1139
1131
  microsoft_device_login: command('devtunnel', ['user', 'login', '-e', '-d']),
1140
- access_anonymous: command('devtunnel', devTunnelAnonymousAccessArgs('TUNNELID', { port })),
1132
+ access_private_reset: command('devtunnel', devTunnelPrivateAccessResetArgs('TUNNELID', { port })),
1141
1133
  host_temporary: command('devtunnel', [
1142
1134
  'host',
1143
1135
  '-p', String(port),
@@ -1169,49 +1161,42 @@ function ignoreAlreadyExists(err) {
1169
1161
  return /already\s+exists|conflict/i.test(text);
1170
1162
  }
1171
1163
 
1172
- async function ensureAnonymousConnectAccess(tunnelId, options = {}) {
1164
+ async function ensurePrivateConnectAccess(tunnelId, options = {}) {
1173
1165
  const checkedAt = new Date().toISOString();
1174
1166
  const listArgs = devTunnelAccessListArgs(tunnelId, options);
1167
+ let listError = '';
1175
1168
  try {
1176
1169
  const list = await execDevTunnel(listArgs, options);
1177
- if (hasAnonymousConnectAccess(list?.stdout || list?.stderr || '')) {
1170
+ const hasAnonymous = hasAnonymousConnectAccess(list?.stdout || list?.stderr || '');
1171
+ if (!hasAnonymous) {
1178
1172
  const access = managedTunnelAccessSummary({
1179
- anonymous_connect: true,
1180
1173
  already_configured: true,
1181
1174
  checked_at: checkedAt,
1182
1175
  }, options);
1183
1176
  persistManagedTunnelAccess(tunnelId, access, options);
1184
1177
  return access;
1185
1178
  }
1186
- } catch {
1187
- // Older CLI builds and transient service errors can make listing fail even
1188
- // when creating the desired rule would succeed. The create call below is
1189
- // still the authoritative repair operation.
1179
+ } catch (err) {
1180
+ listError = String(err?.stderr || err?.message || err || 'Could not list Microsoft tunnel access.').slice(0, 500);
1181
+ // Older CLI builds and transient service errors can make listing fail. The
1182
+ // reset below is still safe: it returns the port to Dev Tunnels defaults.
1190
1183
  }
1191
1184
 
1192
1185
  try {
1193
- await execDevTunnel(devTunnelAnonymousAccessArgs(tunnelId, options), options);
1186
+ await execDevTunnel(devTunnelPrivateAccessResetArgs(tunnelId, options), options);
1194
1187
  const access = managedTunnelAccessSummary({
1195
- anonymous_connect: true,
1196
- created: true,
1188
+ reset: true,
1189
+ anonymous_removed: !listError,
1190
+ list_error: listError,
1197
1191
  checked_at: checkedAt,
1198
1192
  }, options);
1199
1193
  persistManagedTunnelAccess(tunnelId, access, options);
1200
1194
  return access;
1201
1195
  } catch (err) {
1202
- if (ignoreAlreadyExists(err)) {
1203
- const access = managedTunnelAccessSummary({
1204
- anonymous_connect: true,
1205
- already_configured: true,
1206
- checked_at: checkedAt,
1207
- }, options);
1208
- persistManagedTunnelAccess(tunnelId, access, options);
1209
- return access;
1210
- }
1211
- const message = String(err?.stderr || err?.message || err || 'Could not enable anonymous Microsoft tunnel access.').slice(0, 800);
1196
+ const message = String(err?.stderr || err?.message || err || 'Could not reset Microsoft tunnel access to private.').slice(0, 800);
1212
1197
  const access = managedTunnelAccessSummary({
1213
- anonymous_connect: false,
1214
1198
  checked_at: checkedAt,
1199
+ list_error: listError,
1215
1200
  error: message,
1216
1201
  }, options);
1217
1202
  persistManagedTunnelAccess(tunnelId, access, options);
@@ -1236,7 +1221,7 @@ async function ensurePersistentTunnel(tunnelId, options = {}) {
1236
1221
  }
1237
1222
  }
1238
1223
  }
1239
- const access = await ensureAnonymousConnectAccess(tunnelId, options);
1224
+ const access = await ensurePrivateConnectAccess(tunnelId, options);
1240
1225
  return { access };
1241
1226
  }
1242
1227
 
@@ -1383,7 +1368,7 @@ async function applyMicrosoftDevTunnelSetup(input = {}, options = {}) {
1383
1368
  if (previousProcess && managedTunnelProcessUsable(previousProcess, previous) && previous.origin) {
1384
1369
  let access = null;
1385
1370
  try {
1386
- access = await ensureAnonymousConnectAccess(previous.tunnel_id, options);
1371
+ access = await ensurePrivateConnectAccess(previous.tunnel_id, options);
1387
1372
  } catch (err) {
1388
1373
  return {
1389
1374
  ok: false,
@@ -1445,7 +1430,7 @@ async function restoreMicrosoftDevTunnelHost(options = {}) {
1445
1430
  } else if (managedTunnelProcessUsable(managed, state)) {
1446
1431
  let access = managed.access || null;
1447
1432
  if (options.repairAccess === true) {
1448
- access = await ensureAnonymousConnectAccess(state.tunnel_id, options);
1433
+ access = await ensurePrivateConnectAccess(state.tunnel_id, options);
1449
1434
  }
1450
1435
  const latest = await detectManagedMicrosoftDevTunnel(options);
1451
1436
  return {
@@ -1538,7 +1523,7 @@ async function checkMicrosoftDevTunnelAvailability(options = {}) {
1538
1523
  let managed = before;
1539
1524
  if (options.repairAccess === true) {
1540
1525
  try {
1541
- const access = await ensureAnonymousConnectAccess(state.tunnel_id, options);
1526
+ const access = await ensurePrivateConnectAccess(state.tunnel_id, options);
1542
1527
  const latest = await detectManagedMicrosoftDevTunnel(options);
1543
1528
  managed = { ...latest, access, access_mode: access.mode, last_access_check_at: access.checked_at, access_error: access.error || '' };
1544
1529
  } catch (err) {
@@ -1572,14 +1557,13 @@ async function checkMicrosoftDevTunnelAvailability(options = {}) {
1572
1557
  };
1573
1558
  }
1574
1559
  if (probe.blocked_by_tunnel_auth) {
1575
- const error = probe.message || 'Microsoft tunnel access is blocking the phone URL before CTM.';
1576
- await annotateWatchdogState({ last_watchdog_check_at: checkedAt, watchdog_error: error }, options);
1560
+ await annotateWatchdogState({ last_watchdog_check_at: checkedAt, watchdog_error: '' }, options);
1577
1561
  return {
1578
- ok: false,
1562
+ ok: true,
1579
1563
  checked: true,
1580
1564
  recovered: false,
1581
- reason: 'tunnel_auth_required',
1582
- error,
1565
+ reason: 'private_tunnel_auth_required',
1566
+ public_probe: probe,
1583
1567
  keep_awake: await getMicrosoftDevTunnelKeepAwakeStatus(options),
1584
1568
  managed_tunnel: managed,
1585
1569
  };
@@ -15,6 +15,7 @@ const MESSAGE_REGISTRY = Object.freeze({
15
15
  'session.cancel_stream': rule('respond', 'medium', false),
16
16
  'session.prompt_history': rule('respond', 'medium', false),
17
17
  'wall_e.send_message': rule('respond', 'low', false),
18
+ 'wall_e.set_model': rule('respond', 'low', false),
18
19
 
19
20
  'approval.respond': rule('respond', 'high', true, { enabled: false }),
20
21
  'session.kill': rule('admin', 'high', true, { enabled: false }),
@@ -139,6 +140,10 @@ async function dispatchMessage(message, context = {}) {
139
140
  return handlers.sendWalleMessage
140
141
  ? handlers.sendWalleMessage(sessionIdFromBody(message.body), String(message.body.text || message.body.message || ''), message.body)
141
142
  : safeError('handler_missing');
143
+ case 'wall_e.set_model':
144
+ return handlers.setWalleModel
145
+ ? handlers.setWalleModel(sessionIdFromBody(message.body), message.body)
146
+ : safeError('handler_missing');
142
147
  default:
143
148
  return safeError('remote_action_not_enabled');
144
149
  }
@@ -208,9 +208,10 @@ function isSuccessfulExternalActionResult(toolCall) {
208
208
  if (parsed?.external_action === true && parsed?.blocked === true) return false;
209
209
  if (parsed && (parsed.error || parsed.ok === false || parsed.success === false)) return false;
210
210
  if (!SIDE_EFFECT_TOOL_NAMES.has(name)) return false;
211
- if (!parsed) return /approved action executed/i.test(toolCall.summary || '');
212
- if (parsed.sent === true || parsed.created === true || parsed.alreadyExecuted === true) return true;
213
- return parsed.ok === true || parsed.success === true || /approved action executed/i.test(toolCall.summary || '');
211
+ if (!parsed) return /approved action (?:was already executed|executed and verified)/i.test(toolCall.summary || '');
212
+ if (parsed.alreadyExecuted === true) return true;
213
+ if (parsed.verified === true || parsed.verification?.ok === true) return true;
214
+ return /approved action (?:was already executed|executed and verified)/i.test(toolCall.summary || '');
214
215
  }
215
216
 
216
217
  function hasExecutedExternalActionToolResult(toolCalls = []) {
@@ -218,6 +219,14 @@ function hasExecutedExternalActionToolResult(toolCalls = []) {
218
219
  return toolCalls.some(isSuccessfulExternalActionResult);
219
220
  }
220
221
 
222
+ function hasBlockedExternalActionGatewayToolResult(toolCalls = []) {
223
+ if (!Array.isArray(toolCalls)) return false;
224
+ return toolCalls.some((toolCall) => {
225
+ const parsed = parseToolResultPayload(toolCall);
226
+ return parsed?.external_action_gateway === true && parsed?.blocked === true;
227
+ });
228
+ }
229
+
221
230
  function externalActionCompletionClaim(text) {
222
231
  const value = String(text || '').trim();
223
232
  if (!value || NEGATED_COMPLETION_RE.test(value)) return false;
@@ -253,6 +262,13 @@ function externalActionLabel(action = {}) {
253
262
 
254
263
  function buildPendingExternalActionNotExecutedReply({ actions = [] } = {}) {
255
264
  const pending = Array.isArray(actions) ? actions : [];
265
+ if (!pending.length) {
266
+ return [
267
+ 'I do not have verified evidence that those external actions completed.',
268
+ 'Wall-E now requires external side effects to go through the dedicated action tools and read-after-write verification before it can summarize success.',
269
+ 'Please retry with the dedicated mail/calendar action path, or ask me to stage the action again.',
270
+ ].join('\n');
271
+ }
256
272
  const lines = [
257
273
  'I did not execute those external actions.',
258
274
  'The previous turn still has pending approval requests:',
@@ -271,6 +287,7 @@ module.exports = {
271
287
  extractLatestPendingExternalActions,
272
288
  externalActionCompletionClaim,
273
289
  externalActionFromToolCall,
290
+ hasBlockedExternalActionGatewayToolResult,
274
291
  hasExecutedExternalActionToolResult,
275
292
  isExternalActionConfirmationText,
276
293
  };
@@ -3585,6 +3585,24 @@
3585
3585
  justify-content: space-between;
3586
3586
  }
3587
3587
  .standup-attention.active { display: flex; }
3588
+ .standup-service-alerts {
3589
+ display: none;
3590
+ margin: 0;
3591
+ border-color: rgba(224, 175, 104, 0.35);
3592
+ background: rgba(224, 175, 104, 0.08);
3593
+ }
3594
+ .standup-service-alerts.active {
3595
+ display: block;
3596
+ }
3597
+ .standup-service-alerts .we-service-alerts-title {
3598
+ color: var(--yellow);
3599
+ }
3600
+ .standup-service-alerts .we-service-alert-item {
3601
+ padding: 6px 0;
3602
+ }
3603
+ .standup-service-alerts .we-service-alert-text {
3604
+ color: var(--fg);
3605
+ }
3588
3606
  .standup-attention-main {
3589
3607
  min-width: 0;
3590
3608
  display: flex;
@@ -3898,6 +3916,9 @@
3898
3916
  min-width: 0;
3899
3917
  white-space: normal;
3900
3918
  }
3919
+ .standup-service-alerts .we-service-alert-item {
3920
+ align-items: flex-start;
3921
+ }
3901
3922
  .standup-attention { align-items: flex-start; flex-direction: column; }
3902
3923
  .standup-attention-actions { width: 100%; justify-content: flex-end; }
3903
3924
  }
@@ -5187,6 +5208,7 @@
5187
5208
  </div>
5188
5209
  <div class="standup-search-status" id="standup-search-status" aria-live="polite"></div>
5189
5210
  </div>
5211
+ <div class="standup-service-alerts" id="standup-service-alerts"></div>
5190
5212
  <div class="standup-attention" id="standup-attention"></div>
5191
5213
  <div class="standup-loading" id="standup-loading">Loading sessions...</div>
5192
5214
  <div class="standup-error" id="standup-error" style="display:none;"></div>
@@ -19443,6 +19465,9 @@ function onCreated(msg) {
19443
19465
  agentType: 'walle',
19444
19466
  model_id: msg.model_id || null,
19445
19467
  model_provider: msg.model_provider || null,
19468
+ model_registry_id: msg.model_registry_id || msg.modelRegistryId || '',
19469
+ model_provider_id: msg.model_provider_id || msg.modelProviderId || '',
19470
+ model_pinned: msg.model_pinned === true || msg.modelPinned === true,
19446
19471
  },
19447
19472
  walleState: null,
19448
19473
  };
@@ -2094,7 +2094,10 @@ function _microsoftAvailabilityCopy(ms, ready) {
2094
2094
  }
2095
2095
 
2096
2096
  function _microsoftTunnelPhoneAuthText(ms) {
2097
- return 'No Microsoft or GitHub sign-in is needed on the phone; CTM protects the URL with the pairing QR, Face ID/passkey, and device permissions.';
2097
+ var account = _microsoftTunnelAccountText(ms);
2098
+ return account
2099
+ ? 'If Microsoft asks you to sign in, use this same account: ' + account + '. CTM still requires the pairing QR and device permissions.'
2100
+ : 'If Microsoft asks you to sign in, use the same Microsoft or GitHub account shown on this Mac. CTM still requires the pairing QR and device permissions.';
2098
2101
  }
2099
2102
 
2100
2103
  function _microsoftTunnelOrigin(d) {
@@ -2482,15 +2485,15 @@ function _renderMicrosoftTunnelTraffic(traffic) {
2482
2485
  function _microsoftProbeTone(probe) {
2483
2486
  if (!probe || !probe.checked) return '';
2484
2487
  if (probe.ctm_reachable) return 'status-ok';
2485
- if (probe.blocked_by_tunnel_auth || probe.diagnosis === 'tunnel_auth_required') return 'status-warn';
2488
+ if (probe.blocked_by_tunnel_auth || probe.diagnosis === 'tunnel_auth_required') return 'status-ok';
2486
2489
  return 'status-error';
2487
2490
  }
2488
2491
 
2489
2492
  function _microsoftProbeStatusText(probe, ms) {
2490
- if (_microsoftProbeInFlight) return 'Checking the public phone URL...';
2493
+ if (_microsoftProbeInFlight) return 'Checking the private phone URL...';
2491
2494
  if (!probe || !probe.checked) return 'Run this when the phone cannot load the tunnel URL.';
2492
2495
  if (probe.ctm_reachable) return 'CTM is reachable through the tunnel from this Mac.';
2493
- if (probe.blocked_by_tunnel_auth || probe.diagnosis === 'tunnel_auth_required') return 'Microsoft tunnel access is blocking the phone URL.';
2496
+ if (probe.blocked_by_tunnel_auth || probe.diagnosis === 'tunnel_auth_required') return 'Private Microsoft sign-in is active before CTM.';
2494
2497
  if (probe.diagnosis === 'timeout') return 'The tunnel URL timed out before CTM responded.';
2495
2498
  if (probe.diagnosis === 'not_configured') return 'Start the tunnel before checking the phone URL.';
2496
2499
  return probe.status ? ('Tunnel check returned HTTP ' + probe.status + '.') : 'The tunnel URL is not reachable from this Mac.';
@@ -2499,15 +2502,15 @@ function _microsoftProbeStatusText(probe, ms) {
2499
2502
  function _microsoftProbeNoteText(probe, ms) {
2500
2503
  if (_microsoftProbeInFlight) return 'CTM is checking the same devtunnels.ms URL your phone opens.';
2501
2504
  if (!probe || !probe.checked) {
2502
- return 'If this reports a Microsoft gate, the phone request has not reached CTM yet; use Recover Now to re-apply CTM phone access for this tunnel.';
2505
+ return 'If this reports a Microsoft gate, that is expected for a private tunnel. On the phone, sign in with the same account shown above.';
2503
2506
  }
2504
2507
  if (probe.ctm_reachable) {
2505
2508
  return 'The Mac-side check reached CTM. If the phone still fails, refresh the phone page and check the CTM traffic list below.';
2506
2509
  }
2507
2510
  if (probe.blocked_by_tunnel_auth || probe.diagnosis === 'tunnel_auth_required') {
2508
- return 'This is Microsoft Dev Tunnels blocking the request before CTM. Use Recover Now or Set Up again; CTM will re-apply the port-scoped phone access rule.';
2511
+ return 'The tunnel is private. This Mac-side check is stopped by Microsoft because it is not a signed-in browser session; your phone should sign in with the same Microsoft/GitHub account, then CTM will load.';
2509
2512
  }
2510
- return probe.message || 'Use Recover Now if the tunnel should be running, then check the URL again.';
2513
+ return probe.message || 'Use Recover Now if the tunnel process should be running, then check the URL again.';
2511
2514
  }
2512
2515
 
2513
2516
  function _renderMicrosoftTunnelProbe(probe, ms, ready) {
@@ -2564,9 +2567,9 @@ function _renderMicrosoftDevTunnelSetup(ms, d) {
2564
2567
  } else if (!signedIn) {
2565
2568
  summary.textContent = 'Sign in once with GitHub on this Mac. After sign-in finishes, CTM starts the tunnel and creates the phone pairing QR.';
2566
2569
  } else if (ready) {
2567
- summary.textContent = 'Microsoft tunnel is running. Open the phone URL in Safari or Chrome and pair with CTM.';
2570
+ summary.textContent = 'Private Microsoft tunnel is running. Open the phone URL, sign in with the same account if asked, then pair with CTM.';
2568
2571
  } else {
2569
- summary.textContent = 'CTM can start the tunnel now and create the phone pairing QR.';
2572
+ summary.textContent = 'CTM can start the private tunnel now, remove stale anonymous access, and create the phone pairing QR.';
2570
2573
  }
2571
2574
  }
2572
2575
  if (account) {
@@ -2622,8 +2625,8 @@ function _renderMicrosoftDevTunnelSetup(ms, d) {
2622
2625
  }
2623
2626
  if (securityNote) {
2624
2627
  securityNote.textContent = ready
2625
- ? 'CTM opens only this tunnel port to browsers, then requires the CTM pairing QR, Face ID/passkey, device token, and route permissions before phone actions work.'
2626
- : 'Set Up creates a port-scoped browser access rule for the CTM tunnel. Do not share the pairing QR or claim link.';
2628
+ ? 'Private Microsoft access is on. CTM also requires the pairing QR, device token, route permissions, and passkey step-up for high-risk actions.'
2629
+ : 'Set Up keeps anonymous access off and resets stale public access before the tunnel is marked ready. Do not share the pairing QR or claim link.';
2627
2630
  }
2628
2631
  _renderMicrosoftTunnelTraffic(ms.traffic || {});
2629
2632
  _renderMicrosoftTunnelProbe(_microsoftTunnelProbe || ms.public_probe || null, ms, ready);
@@ -3333,6 +3336,7 @@ async function startMicrosoftTunnel() {
3333
3336
  installed: true,
3334
3337
  available: true,
3335
3338
  signed_in: true,
3339
+ account: updatedNetwork.microsoft_dev_tunnel && updatedNetwork.microsoft_dev_tunnel.account || {},
3336
3340
  origin: values.origin,
3337
3341
  mobile_url: values.mobile_url,
3338
3342
  inspect_url: values.inspect_url,
@@ -3451,7 +3455,7 @@ async function probeMicrosoftTunnel() {
3451
3455
  }
3452
3456
  _renderMicrosoftDevTunnelSetup((_lastNetworkSettings || {}).microsoft_dev_tunnel || ms, _lastNetworkSettings || latest);
3453
3457
  if (probe.blocked_by_tunnel_auth || probe.diagnosis === 'tunnel_auth_required') {
3454
- _setMicrosoftActionStatus('Microsoft tunnel access is blocking the phone URL before CTM sees it. Use Recover Now to re-apply phone access.', 'warning');
3458
+ _setMicrosoftActionStatus('Private Microsoft sign-in is active before CTM. On the phone, sign in with the same Microsoft/GitHub account shown here.', 'ok');
3455
3459
  } else if (probe.ctm_reachable) {
3456
3460
  _setMicrosoftActionStatus('Phone URL reached CTM from this Mac.', 'ok');
3457
3461
  } else {
@@ -40,12 +40,13 @@ window.WalleSession = (function() {
40
40
  // ---------- state helper ----------
41
41
  function initialModelFromSession(s) {
42
42
  var meta = (s && s.meta) || {};
43
- var model = meta.model_registry_id || meta.modelRegistryId || meta.model_id || '';
43
+ var model = meta.model_id || meta.model || '';
44
44
  return {
45
45
  model: model,
46
46
  registryId: meta.model_registry_id || meta.modelRegistryId || '',
47
47
  providerType: meta.model_provider || '',
48
48
  provider: meta.model_provider ? providerLabel(meta.model_provider) : '',
49
+ pinned: meta.model_pinned === true || meta.modelPinned === true,
49
50
  };
50
51
  }
51
52
 
@@ -67,7 +68,7 @@ window.WalleSession = (function() {
67
68
  selectedModelProvider: initialModel.provider,
68
69
  _currentAssistant: null,
69
70
  _modelHydrated: true,
70
- _modelManual: false,
71
+ _modelManual: !!initialModel.pinned,
71
72
  promptNavIdx: -1,
72
73
  inputHistory: [],
73
74
  inputHistoryIdx: -1,
@@ -98,6 +99,9 @@ window.WalleSession = (function() {
98
99
  if (typeof s.walleState.isPreparingLocation !== 'boolean') s.walleState.isPreparingLocation = false;
99
100
  if (typeof s.walleState.selectedModelRegistryId !== 'string') s.walleState.selectedModelRegistryId = '';
100
101
  if (typeof s.walleState.selectedModelProviderType !== 'string') s.walleState.selectedModelProviderType = '';
102
+ if ((s.meta && (s.meta.model_pinned === true || s.meta.modelPinned === true)) && s.walleState.selectedModel) {
103
+ s.walleState._modelManual = true;
104
+ }
101
105
  if (s.walleState.composerHeightPx != null && (!isFinite(Number(s.walleState.composerHeightPx)) || Number(s.walleState.composerHeightPx) <= 0)) {
102
106
  s.walleState.composerHeightPx = null;
103
107
  }
@@ -3008,6 +3012,7 @@ window.WalleSession = (function() {
3008
3012
  label: label,
3009
3013
  baseLabel: stripModelAlias(label),
3010
3014
  provider: provider,
3015
+ providerId: m.provider_id || m.providerId || '',
3011
3016
  providerLabel: m.provider_name || providerLabel(provider),
3012
3017
  capabilities: normalizeModelCapabilities(m.capabilities),
3013
3018
  source: m.source || 'live',
@@ -3159,6 +3164,19 @@ window.WalleSession = (function() {
3159
3164
  ws.selectedModelProvider = item ? item.providerLabel : '';
3160
3165
  ws._modelManual = true;
3161
3166
  syncWalleModelButtons(id);
3167
+ if (item && typeof send === 'function') {
3168
+ send({
3169
+ type: 'model-change',
3170
+ id: id,
3171
+ agent_type: 'walle',
3172
+ model_id: item.modelId,
3173
+ model_provider: item.provider,
3174
+ model_registry_id: item.id || '',
3175
+ model_provider_id: item.providerId || '',
3176
+ scope: 'session',
3177
+ pinned: true,
3178
+ });
3179
+ }
3162
3180
  closeModelPicker();
3163
3181
  }
3164
3182
 
@@ -3671,13 +3689,23 @@ window.WalleSession = (function() {
3671
3689
  s.meta = s.meta || {};
3672
3690
  if (msg.model_id) s.meta.model_id = msg.model_id;
3673
3691
  if (msg.model_provider) s.meta.model_provider = msg.model_provider;
3692
+ if (msg.model_registry_id || msg.modelRegistryId) s.meta.model_registry_id = msg.model_registry_id || msg.modelRegistryId;
3693
+ if (msg.model_provider_id || msg.modelProviderId) s.meta.model_provider_id = msg.model_provider_id || msg.modelProviderId;
3694
+ if (typeof msg.model_pinned === 'boolean' || typeof msg.modelPinned === 'boolean') {
3695
+ s.meta.model_pinned = msg.model_pinned === true || msg.modelPinned === true;
3696
+ }
3674
3697
  var ws = getState(id);
3675
- if (!ws || ws._modelManual || !msg.model_id) return;
3698
+ if (!ws || !msg.model_id) return;
3699
+ var sameManualModel = ws._modelManual
3700
+ && ws.selectedModel === msg.model_id
3701
+ && (!msg.model_provider || ws.selectedModelProviderType === msg.model_provider);
3702
+ if (ws._modelManual && !sameManualModel) return;
3676
3703
  ws.selectedModel = msg.model_id;
3677
3704
  ws.selectedModelRegistryId = msg.model_registry_id || msg.modelRegistryId || '';
3678
3705
  ws.selectedModelLabel = '';
3679
3706
  ws.selectedModelProviderType = msg.model_provider || ws.selectedModelProviderType || '';
3680
3707
  ws.selectedModelProvider = msg.model_provider ? providerLabel(msg.model_provider) : ws.selectedModelProvider;
3708
+ if (msg.model_pinned === true || msg.modelPinned === true) ws._modelManual = true;
3681
3709
  syncWalleModelButtons(id);
3682
3710
  }
3683
3711
 
@@ -6489,35 +6489,105 @@ function checkServiceAlerts() {
6489
6489
  });
6490
6490
  }
6491
6491
 
6492
+ function serviceAlertPresentation(alert) {
6493
+ var providerIssue = _normalizeProviderIssue(alert);
6494
+ var kind = providerIssue ? 'provider' : (alert.type === 'auth_expired' ? 'error' : alert.type === 'update_available' ? 'info' : 'warning');
6495
+ return {
6496
+ providerIssue: providerIssue,
6497
+ kind: kind,
6498
+ icon: kind === 'provider' || kind === 'error' ? '!' : kind === 'info' ? '&#8593;' : '&#9679;',
6499
+ message: providerIssue ? providerIssue.message : (alert.message || ''),
6500
+ };
6501
+ }
6502
+
6503
+ function safeServiceAlertId(alert) {
6504
+ return esc(alert && alert.id != null ? alert.id : '').replace(/'/g, '&#39;');
6505
+ }
6506
+
6507
+ function serviceAlertActionHtml(alert, safeId) {
6508
+ var actionBtn = '';
6509
+ var actionLabel = esc(alert.action_label || 'Fix');
6510
+ if (alert.action === 'repair_slack_owner') {
6511
+ var actionName = esc(alert.action || 'custom').replace(/'/g, '&#39;');
6512
+ actionBtn = ' <button class="we-service-alert-action" type="button" onclick="WE._runAlertAction(\'' + safeId + '\', \'' + actionName + '\')">' + actionLabel + '</button>';
6513
+ } else if (alert.action === 'gws_reauth') {
6514
+ actionBtn = ' <button class="we-service-alert-action" type="button" onclick="WE._reconnectGwsAlert(\'' + safeId + '\')">' + actionLabel + '</button>';
6515
+ } else if (alert.action === 'show_update_wizard') {
6516
+ actionBtn = ' <button class="we-service-alert-action" type="button" onclick="WE._showUpdateWizardFromAlert(\'' + safeId + '\')">' + actionLabel + '</button>';
6517
+ } else if (alert.action_url && /^(\/|https?:\/\/)/.test(alert.action_url)) {
6518
+ actionBtn = ' <a href="' + esc(alert.action_url) + '" class="we-service-alert-action">' + actionLabel + '</a>';
6519
+ }
6520
+ return actionBtn;
6521
+ }
6522
+
6523
+ function serviceAlertItemHtml(alert) {
6524
+ var presentation = serviceAlertPresentation(alert);
6525
+ var safeId = safeServiceAlertId(alert);
6526
+ var dismissBtn = '<button class="we-service-alert-dismiss" type="button" onclick="WE._dismissAlert(\'' + safeId + '\')" title="Dismiss">&times;</button>';
6527
+ return '<div class="we-service-alert-item ' + presentation.kind + '">'
6528
+ + '<span class="we-service-alert-icon">' + presentation.icon + '</span>'
6529
+ + '<span class="we-service-alert-text">' + esc(presentation.message) + serviceAlertActionHtml(alert, safeId) + '</span>'
6530
+ + dismissBtn + '</div>';
6531
+ }
6532
+
6533
+ function serviceAlertSearchText(alert) {
6534
+ return [
6535
+ alert && alert.id,
6536
+ alert && alert.type,
6537
+ alert && alert.service,
6538
+ alert && alert.provider,
6539
+ alert && alert.integration,
6540
+ alert && alert.action,
6541
+ alert && alert.message,
6542
+ ].filter(Boolean).join(' ').toLowerCase();
6543
+ }
6544
+
6545
+ function isSessionsIntegrationAlert(alert) {
6546
+ if (!alert || alert.type === 'update_available') return false;
6547
+ var action = String(alert.action || '').toLowerCase();
6548
+ var type = String(alert.type || '').toLowerCase();
6549
+ if (action === 'gws_reauth' || action === 'repair_slack_owner') return true;
6550
+ if (type === 'auth_expired') return true;
6551
+ var text = serviceAlertSearchText(alert);
6552
+ var namesIntegration = /(slack|gws|google|gmail|calendar|drive|oauth)/.test(text);
6553
+ var needsAuth = /(auth|reauth|reconnect|expired|token|owner_identity_missing)/.test(text);
6554
+ return namesIntegration && needsAuth;
6555
+ }
6556
+
6557
+ function ensureStandupServiceAlertsContainer() {
6558
+ var existing = document.getElementById('standup-service-alerts');
6559
+ if (existing) return existing;
6560
+ var search = document.getElementById('standup-search');
6561
+ if (!search || !search.parentNode) return null;
6562
+ var node = document.createElement('div');
6563
+ node.id = 'standup-service-alerts';
6564
+ node.className = 'standup-service-alerts';
6565
+ search.parentNode.insertBefore(node, search.nextSibling);
6566
+ return node;
6567
+ }
6568
+
6569
+ function renderStandupServiceAlerts(alerts) {
6570
+ var container = ensureStandupServiceAlertsContainer();
6571
+ if (!container) return;
6572
+ var integrationAlerts = (alerts || []).filter(isSessionsIntegrationAlert);
6573
+ if (!integrationAlerts.length) {
6574
+ container.className = 'standup-service-alerts';
6575
+ container.innerHTML = '';
6576
+ return;
6577
+ }
6578
+ container.className = 'standup-service-alerts active we-service-alerts';
6579
+ container.innerHTML = '<div class="we-service-alerts-title">Integration Alerts</div>'
6580
+ + integrationAlerts.map(serviceAlertItemHtml).join('');
6581
+ }
6582
+
6492
6583
  function renderServiceAlerts(alerts) {
6493
6584
  var existing = document.getElementById('walle-service-alerts');
6585
+ renderStandupServiceAlerts(alerts);
6494
6586
  if (!alerts || alerts.length === 0) {
6495
6587
  if (existing) existing.remove();
6496
6588
  return;
6497
6589
  }
6498
- var items = alerts.map(function(a) {
6499
- var providerIssue = _normalizeProviderIssue(a);
6500
- var kind = providerIssue ? 'provider' : (a.type === 'auth_expired' ? 'error' : a.type === 'update_available' ? 'info' : 'warning');
6501
- var icon = kind === 'provider' || kind === 'error' ? '!' : kind === 'info' ? '&#8593;' : '&#9679;';
6502
- var safeId = esc(a.id).replace(/'/g, '&#39;');
6503
- var dismissBtn = '<button class="we-service-alert-dismiss" onclick="WE._dismissAlert(\'' + safeId + '\')" title="Dismiss">&times;</button>';
6504
- var actionBtn = '';
6505
- var actionLabel = esc(a.action_label || 'Fix');
6506
- if (a.action === 'repair_slack_owner') {
6507
- var actionName = esc(a.action || 'custom').replace(/'/g, '&#39;');
6508
- actionBtn = ' <button class="we-service-alert-action" onclick="WE._runAlertAction(\'' + safeId + '\', \'' + actionName + '\')">' + actionLabel + '</button>';
6509
- } else if (a.action === 'gws_reauth') {
6510
- actionBtn = ' <button class="we-service-alert-action" onclick="WE._reconnectGwsAlert(\'' + safeId + '\')">' + actionLabel + '</button>';
6511
- } else if (a.action === 'show_update_wizard') {
6512
- actionBtn = ' <button class="we-service-alert-action" onclick="WE._showUpdateWizardFromAlert(\'' + safeId + '\')">' + actionLabel + '</button>';
6513
- } else if (a.action_url && /^(\/|https?:\/\/)/.test(a.action_url)) {
6514
- actionBtn = ' <a href="' + esc(a.action_url) + '" class="we-service-alert-action">' + actionLabel + '</a>';
6515
- }
6516
- return '<div class="we-service-alert-item ' + kind + '">'
6517
- + '<span class="we-service-alert-icon">' + icon + '</span>'
6518
- + '<span class="we-service-alert-text">' + esc(providerIssue ? providerIssue.message : a.message) + actionBtn + '</span>'
6519
- + dismissBtn + '</div>';
6520
- }).join('');
6590
+ var items = alerts.map(serviceAlertItemHtml).join('');
6521
6591
 
6522
6592
  var html = '<div id="walle-service-alerts" class="we-service-alerts">'
6523
6593
  + '<div class="we-service-alerts-title">Service Alerts</div>' + items + '</div>';