@co0ontty/wand 1.21.10 → 1.21.12

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.
@@ -1233,6 +1233,13 @@
1233
1233
  '<p id="login-error" class="error-message hidden" role="alert"></p>' +
1234
1234
  '</div>' +
1235
1235
  '<button id="login-button" class="btn btn-primary btn-block">进入控制台</button>' +
1236
+ (hasNativeSwitchServer() ?
1237
+ '<button id="login-switch-server-button" class="btn btn-ghost btn-block login-switch-server" type="button">' +
1238
+ '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="8" rx="2"/><rect x="2" y="13" width="20" height="8" rx="2"/><line x1="6" y1="7" x2="6.01" y2="7"/><line x1="6" y1="17" x2="6.01" y2="17"/></svg>' +
1239
+ '<span>切换服务器</span>' +
1240
+ '</button>'
1241
+ : ''
1242
+ ) +
1236
1243
  '</div>' +
1237
1244
  '</div>' +
1238
1245
  '</div>';
@@ -1300,6 +1307,13 @@
1300
1307
  '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>' +
1301
1308
  '<span>安装</span>' +
1302
1309
  '</button>' +
1310
+ (hasNativeSwitchServer() ?
1311
+ '<button id="switch-server-button" class="btn btn-ghost btn-sm sidebar-switch-server" type="button" title="切换服务器">' +
1312
+ '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="8" rx="2"/><rect x="2" y="13" width="20" height="8" rx="2"/><line x1="6" y1="7" x2="6.01" y2="7"/><line x1="6" y1="17" x2="6.01" y2="17"/></svg>' +
1313
+ '<span>切换</span>' +
1314
+ '</button>'
1315
+ : ''
1316
+ ) +
1303
1317
  '<button id="logout-button" class="btn btn-ghost btn-sm sidebar-logout" type="button" title="退出登录">' +
1304
1318
  '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>' +
1305
1319
  '<span>退出</span>' +
@@ -1338,6 +1352,7 @@
1338
1352
  '<button class="topbar-more-item" data-action="settings" type="button" role="menuitem"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg><span>设置</span></button>' +
1339
1353
  '<button class="topbar-more-item" data-action="refresh" type="button" role="menuitem"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg><span>刷新</span></button>' +
1340
1354
  '<button class="topbar-more-item' + (state.showInstallPrompt && state.deferredPrompt ? '' : ' hidden') + '" id="topbar-install-item" data-action="install" type="button" role="menuitem"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg><span>安装应用</span></button>' +
1355
+ (hasNativeSwitchServer() ? '<button class="topbar-more-item" data-action="switch-server" type="button" role="menuitem"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="8" rx="2"/><rect x="2" y="13" width="20" height="8" rx="2"/><line x1="6" y1="7" x2="6.01" y2="7"/><line x1="6" y1="17" x2="6.01" y2="17"/></svg><span>切换服务器</span></button>' : '') +
1341
1356
  '<button class="topbar-more-item topbar-more-item-danger" data-action="logout" type="button" role="menuitem"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg><span>退出</span></button>' +
1342
1357
  '</div>' +
1343
1358
  '</div>' +
@@ -1982,6 +1997,13 @@
1982
1997
  '<code id="android-connect-code" class="settings-connect-url-text">-</code>' +
1983
1998
  '<button id="copy-connect-code-button" class="btn btn-secondary btn-sm" type="button" title="复制连接码">复制</button>' +
1984
1999
  '</div>' +
2000
+ '<div class="settings-connect-qr-box">' +
2001
+ '<div class="settings-connect-qr-wrap" id="android-connect-qr-wrap" title="点击放大">' +
2002
+ '<canvas id="android-connect-qr" width="180" height="180"></canvas>' +
2003
+ '<div class="settings-connect-qr-empty" id="android-connect-qr-empty">生成中…</div>' +
2004
+ '</div>' +
2005
+ '<p class="settings-connect-qr-hint">用 Wand App 扫一扫,即可一键填入服务器地址与连接码。</p>' +
2006
+ '</div>' +
1985
2007
  '</div>' +
1986
2008
  '</div>' +
1987
2009
 
@@ -2147,10 +2169,13 @@
2147
2169
  '<label class="settings-toggle-title" for="cfg-inherit-env">继承环境变量</label>' +
2148
2170
  '<span class="settings-toggle-desc">启动 PTY / 结构化子进程时,把当前服务进程的环境变量传给 claude / codex。关闭后子进程仅获得最小可用环境(PATH/HOME/SHELL/LANG/TERM 等),可用于隔离 API key 等敏感凭据。</span>' +
2149
2171
  '</div>' +
2150
- '<label class="settings-switch">' +
2151
- '<input id="cfg-inherit-env" type="checkbox" class="switch-toggle" />' +
2152
- '<span class="switch-slider"></span>' +
2153
- '</label>' +
2172
+ '<div class="settings-toggle-aside">' +
2173
+ '<button type="button" id="cfg-view-env-btn" class="btn btn-secondary btn-sm" title="查看实际会注入到子进程的环境变量">查看</button>' +
2174
+ '<label class="settings-switch">' +
2175
+ '<input id="cfg-inherit-env" type="checkbox" class="switch-toggle" />' +
2176
+ '<span class="switch-slider"></span>' +
2177
+ '</label>' +
2178
+ '</div>' +
2154
2179
  '</div>' +
2155
2180
  '<div class="field">' +
2156
2181
  '<label class="field-label" for="cfg-default-model">默认模型</label>' +
@@ -3809,6 +3834,8 @@
3809
3834
  var loginButton = document.getElementById("login-button");
3810
3835
  if (loginButton) {
3811
3836
  loginButton.addEventListener("click", login);
3837
+ var loginSwitchServerBtn = document.getElementById("login-switch-server-button");
3838
+ if (loginSwitchServerBtn) loginSwitchServerBtn.addEventListener("click", switchServer);
3812
3839
  var passwordEl = document.getElementById("password");
3813
3840
  var togglePasswordButton = document.getElementById("toggle-password-button");
3814
3841
  if (togglePasswordButton && passwordEl) {
@@ -3971,6 +3998,8 @@
3971
3998
  });
3972
3999
  var logoutBtn = document.getElementById("logout-button");
3973
4000
  if (logoutBtn) logoutBtn.addEventListener("click", logout);
4001
+ var switchServerBtn = document.getElementById("switch-server-button");
4002
+ if (switchServerBtn) switchServerBtn.addEventListener("click", switchServer);
3974
4003
  var settingsBtn = document.getElementById("settings-button");
3975
4004
  if (settingsBtn) settingsBtn.addEventListener("click", openSettingsModal);
3976
4005
  var closeSettingsBtn = document.getElementById("close-settings-button");
@@ -3994,6 +4023,8 @@
3994
4023
  if (saveConfigBtn) saveConfigBtn.addEventListener("click", saveConfigSettings);
3995
4024
  var defaultModelRefreshBtn = document.getElementById("cfg-default-model-refresh");
3996
4025
  if (defaultModelRefreshBtn) defaultModelRefreshBtn.addEventListener("click", refreshAvailableModels);
4026
+ var viewEnvBtn = document.getElementById("cfg-view-env-btn");
4027
+ if (viewEnvBtn) viewEnvBtn.addEventListener("click", openEnvPreviewModal);
3997
4028
  var saveDisplayBtn = document.getElementById("save-display-button");
3998
4029
  if (saveDisplayBtn) saveDisplayBtn.addEventListener("click", saveDisplaySettings);
3999
4030
  // App icon picker (APK only)
@@ -4408,6 +4439,9 @@
4408
4439
  case "logout":
4409
4440
  logout();
4410
4441
  break;
4442
+ case "switch-server":
4443
+ switchServer();
4444
+ break;
4411
4445
  }
4412
4446
  });
4413
4447
  // Close on outside click
@@ -5918,6 +5952,15 @@
5918
5952
  });
5919
5953
  }
5920
5954
 
5955
+ function hasNativeSwitchServer() {
5956
+ return typeof WandNative !== "undefined" && typeof WandNative.switchServer === "function";
5957
+ }
5958
+
5959
+ function switchServer() {
5960
+ if (!hasNativeSwitchServer()) return;
5961
+ try { WandNative.switchServer(); } catch (e) {}
5962
+ }
5963
+
5921
5964
  function logout() {
5922
5965
  fetch("/api/logout", { method: "POST", credentials: "same-origin" }).catch(function() {});
5923
5966
  stopPolling();
@@ -5964,31 +6007,14 @@
5964
6007
  }
5965
6008
 
5966
6009
  function getComposerPlaceholder(session, terminalInteractive) {
5967
- if (terminalInteractive) {
5968
- return "终端交互模式开启中,请直接在终端中输入";
6010
+ // Keep placeholders short so they don't wrap on portrait mobile screens.
6011
+ // Only show informative state hints; drop the redundant "send to X" labels.
6012
+ if (terminalInteractive) return "终端交互中";
6013
+ if (session && session.status !== "running") {
6014
+ if (canAutoResumeSession(session)) return "";
6015
+ return "会话已结束";
5969
6016
  }
5970
- if (session && isStructuredSession(session)) {
5971
- return session.provider === "codex"
5972
- ? "向 Codex 发送消息;chat 为结构化对话视图"
5973
- : "向 Claude 发送消息;chat 为结构化对话视图";
5974
- }
5975
- if (session && session.provider === "codex") {
5976
- if (session.status !== "running") {
5977
- return "Codex 会话已结束,无法继续发送";
5978
- }
5979
- return state.currentView === "terminal"
5980
- ? "向 Codex 发送输入;terminal 为原始 TUI 输出"
5981
- : "向 Codex 发送输入;chat 为解析后的阅读视图";
5982
- }
5983
- if (session && !isStructuredSession(session) && session.status !== "running") {
5984
- if (canAutoResumeSession(session)) {
5985
- return "输入消息...";
5986
- }
5987
- return "会话已结束,无法继续发送";
5988
- }
5989
- return session && isStructuredSession(session) && session.structuredState && session.structuredState.inFlight
5990
- ? "思考中 · 发送新消息将中断当前回复"
5991
- : "输入消息...";
6017
+ return "";
5992
6018
  }
5993
6019
 
5994
6020
  function getToolModeHint(tool, mode) {
@@ -6169,6 +6195,143 @@
6169
6195
  });
6170
6196
  }
6171
6197
 
6198
+ // ── Environment-variable preview modal ──
6199
+ // Lazily creates a modal showing the exact env vars wand will inject
6200
+ // into PTY / structured child processes (mirrors buildChildEnv()).
6201
+ function openEnvPreviewModal() {
6202
+ var modal = document.getElementById("env-preview-modal");
6203
+ if (!modal) {
6204
+ modal = document.createElement("section");
6205
+ modal.id = "env-preview-modal";
6206
+ modal.className = "modal-backdrop hidden";
6207
+ modal.innerHTML =
6208
+ '<div class="modal env-preview-modal" role="dialog" aria-labelledby="env-preview-title" aria-modal="true">' +
6209
+ '<div class="modal-header">' +
6210
+ '<div>' +
6211
+ '<h2 class="modal-title" id="env-preview-title">将注入子进程的环境变量</h2>' +
6212
+ '<p class="modal-subtitle" id="env-preview-subtitle">这些变量会被传给 claude / codex(PTY 与结构化运行器一致)。</p>' +
6213
+ '</div>' +
6214
+ '<button id="env-preview-close" class="btn btn-ghost btn-icon modal-close-btn" type="button" aria-label="关闭">' +
6215
+ '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true">' +
6216
+ '<line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/>' +
6217
+ '</svg>' +
6218
+ '</button>' +
6219
+ '</div>' +
6220
+ '<div class="modal-body env-preview-body">' +
6221
+ '<div class="env-preview-toolbar">' +
6222
+ '<div class="env-preview-meta" id="env-preview-meta">加载中…</div>' +
6223
+ '<div class="env-preview-controls">' +
6224
+ '<input id="env-preview-search" class="env-preview-search" type="search" placeholder="搜索变量名…" />' +
6225
+ '<label class="env-preview-reveal">' +
6226
+ '<input id="env-preview-reveal-toggle" type="checkbox" />' +
6227
+ '<span>显示敏感值</span>' +
6228
+ '</label>' +
6229
+ '</div>' +
6230
+ '</div>' +
6231
+ '<div class="env-preview-list" id="env-preview-list" tabindex="0">' +
6232
+ '<div class="env-preview-loading">加载中…</div>' +
6233
+ '</div>' +
6234
+ '</div>' +
6235
+ '<div class="modal-footer env-preview-footer">' +
6236
+ '<span class="env-preview-hint">敏感字段(含 KEY/TOKEN/SECRET 等)默认掩码,可勾选「显示敏感值」临时还原。</span>' +
6237
+ '<button id="env-preview-close-2" class="btn btn-secondary btn-sm" type="button">关闭</button>' +
6238
+ '</div>' +
6239
+ '</div>';
6240
+ document.body.appendChild(modal);
6241
+
6242
+ // Click outside to close
6243
+ modal.addEventListener("click", function(e) {
6244
+ if (e.target === modal) closeEnvPreviewModal();
6245
+ });
6246
+ var closeBtn = modal.querySelector("#env-preview-close");
6247
+ if (closeBtn) closeBtn.addEventListener("click", closeEnvPreviewModal);
6248
+ var closeBtn2 = modal.querySelector("#env-preview-close-2");
6249
+ if (closeBtn2) closeBtn2.addEventListener("click", closeEnvPreviewModal);
6250
+ var searchEl = modal.querySelector("#env-preview-search");
6251
+ if (searchEl) searchEl.addEventListener("input", function() { renderEnvPreviewList(); });
6252
+ var revealEl = modal.querySelector("#env-preview-reveal-toggle");
6253
+ if (revealEl) revealEl.addEventListener("change", function() { loadEnvPreview(revealEl.checked); });
6254
+ }
6255
+
6256
+ modal.classList.remove("closing");
6257
+ modal.classList.remove("hidden");
6258
+ var revealEl = modal.querySelector("#env-preview-reveal-toggle");
6259
+ if (revealEl) revealEl.checked = false;
6260
+ var searchEl = modal.querySelector("#env-preview-search");
6261
+ if (searchEl) searchEl.value = "";
6262
+ loadEnvPreview(false);
6263
+ }
6264
+
6265
+ function closeEnvPreviewModal() {
6266
+ var modal = document.getElementById("env-preview-modal");
6267
+ if (!modal) return;
6268
+ animateModalClose(modal);
6269
+ }
6270
+
6271
+ function loadEnvPreview(reveal) {
6272
+ var listEl = document.getElementById("env-preview-list");
6273
+ var metaEl = document.getElementById("env-preview-meta");
6274
+ if (listEl) listEl.innerHTML = '<div class="env-preview-loading">加载中…</div>';
6275
+ if (metaEl) metaEl.textContent = "加载中…";
6276
+ var url = "/api/settings/env-preview" + (reveal ? "?reveal=1" : "");
6277
+ fetch(url, { credentials: "same-origin" })
6278
+ .then(function(res) { return res.json(); })
6279
+ .then(function(data) {
6280
+ if (!data || !Array.isArray(data.entries)) {
6281
+ if (listEl) listEl.innerHTML = '<div class="env-preview-empty">读取失败。</div>';
6282
+ if (metaEl) metaEl.textContent = "读取失败";
6283
+ return;
6284
+ }
6285
+ state._envPreview = data;
6286
+ if (metaEl) {
6287
+ var inheritLabel = data.inheritEnv ? "继承父进程" : "最小白名单";
6288
+ metaEl.innerHTML =
6289
+ '<span class="env-preview-pill ' + (data.inheritEnv ? "is-inherit" : "is-minimal") + '">' + inheritLabel + '</span>' +
6290
+ '<span class="env-preview-count">共 ' + data.total + ' 项</span>';
6291
+ }
6292
+ renderEnvPreviewList();
6293
+ })
6294
+ .catch(function() {
6295
+ if (listEl) listEl.innerHTML = '<div class="env-preview-empty">读取失败,请稍后重试。</div>';
6296
+ if (metaEl) metaEl.textContent = "读取失败";
6297
+ });
6298
+ }
6299
+
6300
+ function renderEnvPreviewList() {
6301
+ var listEl = document.getElementById("env-preview-list");
6302
+ if (!listEl) return;
6303
+ var data = state._envPreview;
6304
+ if (!data || !Array.isArray(data.entries)) {
6305
+ listEl.innerHTML = '<div class="env-preview-empty">尚未加载。</div>';
6306
+ return;
6307
+ }
6308
+ var searchEl = document.getElementById("env-preview-search");
6309
+ var query = (searchEl && searchEl.value || "").trim().toLowerCase();
6310
+ var html = "";
6311
+ var shown = 0;
6312
+ for (var i = 0; i < data.entries.length; i++) {
6313
+ var entry = data.entries[i];
6314
+ if (query && entry.name.toLowerCase().indexOf(query) === -1) continue;
6315
+ shown++;
6316
+ var isPlaceholder = typeof entry.value === "string" && entry.value.charAt(0) === "<" && entry.value.charAt(entry.value.length - 1) === ">";
6317
+ html += '<div class="env-preview-row' + (entry.sensitive ? " is-sensitive" : "") + '">' +
6318
+ '<div class="env-preview-name">' +
6319
+ escapeHtml(entry.name) +
6320
+ (entry.sensitive ? '<span class="env-preview-badge" title="被识别为敏感字段">敏感</span>' : '') +
6321
+ (isPlaceholder ? '<span class="env-preview-badge env-preview-badge-runtime" title="按会话动态注入">运行时</span>' : '') +
6322
+ '</div>' +
6323
+ '<div class="env-preview-value' + (isPlaceholder ? " is-runtime" : "") + '" title="' + escapeHtml(String(entry.value)) + '">' +
6324
+ escapeHtml(String(entry.value)) +
6325
+ '</div>' +
6326
+ '<div class="env-preview-len">' + entry.length + ' 字符</div>' +
6327
+ '</div>';
6328
+ }
6329
+ if (shown === 0) {
6330
+ html = '<div class="env-preview-empty">没有匹配的变量。</div>';
6331
+ }
6332
+ listEl.innerHTML = html;
6333
+ }
6334
+
6172
6335
  function updateSettingsDefaultModelSelect(data) {
6173
6336
  var select = document.getElementById("cfg-default-model");
6174
6337
  if (!select) return;
@@ -7336,6 +7499,78 @@
7336
7499
  }
7337
7500
 
7338
7501
 
7502
+ function renderConnectQrCode(code) {
7503
+ var canvas = document.getElementById("android-connect-qr");
7504
+ var empty = document.getElementById("android-connect-qr-empty");
7505
+ var lib = window.QRCodeLib;
7506
+ if (!canvas) return;
7507
+ if (!lib || typeof lib.toCanvas !== "function") {
7508
+ if (empty) empty.textContent = "二维码库未加载";
7509
+ return;
7510
+ }
7511
+ try {
7512
+ lib.toCanvas(canvas, code, {
7513
+ width: 220,
7514
+ margin: 1,
7515
+ errorCorrectionLevel: "M",
7516
+ color: { dark: "#1f1b17", light: "#ffffff00" }
7517
+ }, function(err) {
7518
+ if (err) {
7519
+ if (empty) {
7520
+ empty.textContent = "二维码生成失败";
7521
+ empty.style.display = "";
7522
+ }
7523
+ canvas.style.visibility = "hidden";
7524
+ return;
7525
+ }
7526
+ if (empty) empty.style.display = "none";
7527
+ canvas.style.visibility = "visible";
7528
+ });
7529
+ } catch (e) {
7530
+ if (empty) {
7531
+ empty.textContent = "二维码生成失败";
7532
+ empty.style.display = "";
7533
+ }
7534
+ canvas.style.visibility = "hidden";
7535
+ }
7536
+ }
7537
+
7538
+ function showConnectQrModal(code) {
7539
+ var lib = window.QRCodeLib;
7540
+ if (!lib || typeof lib.toCanvas !== "function") return;
7541
+ // Reuse existing overlay if open
7542
+ var existing = document.getElementById("connect-qr-modal");
7543
+ if (existing) existing.remove();
7544
+ var overlay = document.createElement("div");
7545
+ overlay.id = "connect-qr-modal";
7546
+ overlay.className = "connect-qr-modal-overlay";
7547
+ overlay.innerHTML =
7548
+ '<div class="connect-qr-modal-card">' +
7549
+ '<canvas id="connect-qr-modal-canvas"></canvas>' +
7550
+ '<p class="connect-qr-modal-hint">用 Wand App 扫一扫,连接当前服务器</p>' +
7551
+ '<button type="button" class="btn btn-secondary btn-sm connect-qr-modal-close">关闭</button>' +
7552
+ '</div>';
7553
+ document.body.appendChild(overlay);
7554
+ var modalCanvas = overlay.querySelector("#connect-qr-modal-canvas");
7555
+ var size = Math.min(window.innerWidth, window.innerHeight) * 0.7;
7556
+ if (size < 240) size = 240;
7557
+ if (size > 480) size = 480;
7558
+ try {
7559
+ lib.toCanvas(modalCanvas, code, {
7560
+ width: size,
7561
+ margin: 2,
7562
+ errorCorrectionLevel: "M",
7563
+ color: { dark: "#1f1b17", light: "#ffffff" }
7564
+ });
7565
+ } catch (e) {}
7566
+ function close() { overlay.remove(); }
7567
+ overlay.addEventListener("click", function(e) {
7568
+ if (e.target === overlay) close();
7569
+ });
7570
+ var closeBtn = overlay.querySelector(".connect-qr-modal-close");
7571
+ if (closeBtn) closeBtn.addEventListener("click", close);
7572
+ }
7573
+
7339
7574
  function copyToClipboard(text, triggerBtn, successCallback) {
7340
7575
  if (!text) return;
7341
7576
  function onSuccess() {
@@ -7521,12 +7756,32 @@
7521
7756
 
7522
7757
  // App connect code (encrypted)
7523
7758
  var connectCodeEl = document.getElementById("android-connect-code");
7759
+ var connectQrCanvas = document.getElementById("android-connect-qr");
7760
+ var connectQrEmpty = document.getElementById("android-connect-qr-empty");
7761
+ var connectQrWrap = document.getElementById("android-connect-qr-wrap");
7524
7762
  if (connectCodeEl) {
7525
7763
  connectCodeEl.textContent = "加载中...";
7764
+ if (connectQrEmpty) connectQrEmpty.textContent = "生成中…";
7765
+ if (connectQrCanvas) connectQrCanvas.style.visibility = "hidden";
7526
7766
  fetch("/api/app-connect-code").then(function(r) { return r.json(); }).then(function(d) {
7527
- if (d.code) connectCodeEl.textContent = d.code;
7528
- else connectCodeEl.textContent = "生成失败";
7529
- }).catch(function() { connectCodeEl.textContent = "获取失败"; });
7767
+ if (d.code) {
7768
+ connectCodeEl.textContent = d.code;
7769
+ state.androidConnectCode = d.code;
7770
+ renderConnectQrCode(d.code);
7771
+ } else {
7772
+ connectCodeEl.textContent = "生成失败";
7773
+ if (connectQrEmpty) connectQrEmpty.textContent = "生成失败";
7774
+ }
7775
+ }).catch(function() {
7776
+ connectCodeEl.textContent = "获取失败";
7777
+ if (connectQrEmpty) connectQrEmpty.textContent = "获取失败";
7778
+ });
7779
+ }
7780
+ if (connectQrWrap && !connectQrWrap.dataset.bound) {
7781
+ connectQrWrap.dataset.bound = "1";
7782
+ connectQrWrap.addEventListener("click", function() {
7783
+ if (state.androidConnectCode) showConnectQrModal(state.androidConnectCode);
7784
+ });
7530
7785
  }
7531
7786
 
7532
7787
  // Config fields
@@ -9123,7 +9378,7 @@
9123
9378
  var todoEl = document.getElementById("todo-progress");
9124
9379
  if (todoEl) todoEl.classList.add("hidden");
9125
9380
  welcomeInput.value = "";
9126
- welcomeInput.placeholder = "正在启动会话...";
9381
+ welcomeInput.placeholder = "正在启动…";
9127
9382
  welcomeInput.disabled = true;
9128
9383
  var mode = state.chatMode || "managed";
9129
9384
  var defaultCwd = getEffectiveCwd();
@@ -9144,7 +9399,7 @@
9144
9399
  .then(function(data) {
9145
9400
  if (data.error) {
9146
9401
  showToast(data.error, "error");
9147
- welcomeInput.placeholder = "输入你的问题,按 Enter 发送...";
9402
+ welcomeInput.placeholder = "输入消息";
9148
9403
  welcomeInput.disabled = false;
9149
9404
  return;
9150
9405
  }
@@ -9157,7 +9412,7 @@
9157
9412
  switchToSessionView(data.id);
9158
9413
  subscribeToSession(data.id);
9159
9414
  loadOutput(data.id).then(function() {
9160
- welcomeInput.placeholder = "输入你的问题,按 Enter 发送...";
9415
+ welcomeInput.placeholder = "输入消息";
9161
9416
  welcomeInput.disabled = false;
9162
9417
  focusInputBox(true);
9163
9418
  });
@@ -9166,7 +9421,7 @@
9166
9421
  showToast((error && error.message) || (preferredTool === "codex"
9167
9422
  ? "无法启动 Codex 会话。"
9168
9423
  : "无法启动 Claude 会话。"), "error");
9169
- welcomeInput.placeholder = "输入你的问题,按 Enter 发送...";
9424
+ welcomeInput.placeholder = "输入消息";
9170
9425
  welcomeInput.disabled = false;
9171
9426
  });
9172
9427
  }
@@ -10542,7 +10797,7 @@
10542
10797
  function createSessionFromWelcomeInput(value) {
10543
10798
  var welcomeInput = document.getElementById("welcome-input");
10544
10799
  if (!welcomeInput) return;
10545
- welcomeInput.placeholder = "Claude 正在思考,请稍候...";
10800
+ welcomeInput.placeholder = "正在思考…";
10546
10801
  welcomeInput.disabled = true;
10547
10802
  var mode = state.chatMode || "managed";
10548
10803
  var defaultCwd = getEffectiveCwd();
@@ -10564,7 +10819,7 @@
10564
10819
  .then(function(data) {
10565
10820
  if (data.error) {
10566
10821
  showToast(data.error, "error");
10567
- welcomeInput.placeholder = "输入你的问题,按 Enter 发送...";
10822
+ welcomeInput.placeholder = "输入消息";
10568
10823
  welcomeInput.disabled = false;
10569
10824
  return null;
10570
10825
  }
@@ -10572,11 +10827,11 @@
10572
10827
  })
10573
10828
  .catch(function(error) {
10574
10829
  showToast((error && error.message) || "无法启动会话。", "error");
10575
- welcomeInput.placeholder = "输入你的问题,按 Enter 发送...";
10830
+ welcomeInput.placeholder = "输入消息";
10576
10831
  welcomeInput.disabled = false;
10577
10832
  })
10578
10833
  .finally(function() {
10579
- welcomeInput.placeholder = "输入你的问题,按 Enter 发送...";
10834
+ welcomeInput.placeholder = "输入消息";
10580
10835
  welcomeInput.disabled = false;
10581
10836
  });
10582
10837
  }
@@ -15842,43 +16097,87 @@
15842
16097
  playNotificationSound();
15843
16098
 
15844
16099
  var id = ++notificationIdCounter;
15845
- var bubble = document.createElement("div");
15846
- bubble.className = "notification-bubble";
15847
- bubble.setAttribute("data-nid", id);
15848
-
15849
- bubble.innerHTML =
15850
- '<div class="notification-bubble-header">' +
15851
- '<span class="notification-bubble-icon info">\u2191</span>' +
15852
- '<span class="notification-bubble-title">\u53d1\u73b0\u65b0\u7248\u672c</span>' +
15853
- '<button class="notification-bubble-close" title="\u5173\u95ed">\u00d7</button>' +
16100
+ var card = document.createElement("div");
16101
+ // Reuse the notification stacking system but with a richer card style.
16102
+ card.className = "notification-bubble update-card";
16103
+ card.setAttribute("data-nid", id);
16104
+
16105
+ card.innerHTML =
16106
+ '<div class="update-card-shine" aria-hidden="true"></div>' +
16107
+ '<div class="update-card-header">' +
16108
+ '<div class="update-card-icon" aria-hidden="true">' +
16109
+ '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">' +
16110
+ '<path d="M12 19V5"/><path d="M5 12l7-7 7 7"/>' +
16111
+ '</svg>' +
16112
+ '</div>' +
16113
+ '<div class="update-card-heading">' +
16114
+ '<div class="update-card-title">\u53d1\u73b0\u65b0\u7248\u672c</div>' +
16115
+ '<div class="update-card-subtitle" id="update-card-subtitle">\u70b9\u51fb\u4e0b\u65b9\u6309\u94ae\u4e00\u952e\u66f4\u65b0</div>' +
16116
+ '</div>' +
16117
+ '<button class="update-card-close" title="\u7a0d\u540e\u63d0\u9192" aria-label="\u5173\u95ed">' +
16118
+ '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">' +
16119
+ '<path d="M18 6L6 18"/><path d="M6 6l12 12"/>' +
16120
+ '</svg>' +
16121
+ '</button>' +
16122
+ '</div>' +
16123
+ '<div class="update-card-version">' +
16124
+ '<span class="update-card-version-chip update-card-version-current">v' + escapeHtml(String(currentVer).replace(/^v/, "")) + '</span>' +
16125
+ '<svg class="update-card-version-arrow" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
16126
+ '<path d="M5 12h14"/><path d="M13 5l7 7-7 7"/>' +
16127
+ '</svg>' +
16128
+ '<span class="update-card-version-chip update-card-version-latest">v' + escapeHtml(String(latestVer).replace(/^v/, "")) + '</span>' +
15854
16129
  '</div>' +
15855
- '<div class="notification-bubble-body">' +
15856
- escapeHtml(currentVer) + ' \u2192 ' + escapeHtml(latestVer) +
16130
+ '<div class="update-card-progress" id="update-card-progress" aria-hidden="true">' +
16131
+ '<div class="update-card-progress-track"><div class="update-card-progress-fill"></div></div>' +
15857
16132
  '</div>' +
15858
- '<div class="notification-bubble-actions">' +
15859
- '<button class="primary" id="update-bubble-action">\u7acb\u5373\u66f4\u65b0</button>' +
16133
+ '<div class="update-card-status hidden" id="update-card-status"></div>' +
16134
+ '<div class="update-card-actions">' +
16135
+ '<button class="update-card-action update-card-action-primary" id="update-bubble-action" type="button">' +
16136
+ '<span class="update-card-action-label">\u7acb\u5373\u66f4\u65b0</span>' +
16137
+ '</button>' +
15860
16138
  '</div>';
15861
16139
 
15862
- document.body.appendChild(bubble);
16140
+ document.body.appendChild(card);
15863
16141
 
15864
- var entry = { id: id, el: bubble };
16142
+ var entry = { id: id, el: card };
15865
16143
  notificationStack.push(entry);
15866
16144
  repositionNotifications();
15867
16145
 
15868
- var closeBtn = bubble.querySelector(".notification-bubble-close");
16146
+ var closeBtn = card.querySelector(".update-card-close");
15869
16147
  if (closeBtn) closeBtn.onclick = function() {
15870
16148
  dismissNotification(id);
15871
16149
  state._updateBubbleShown = false;
15872
16150
  };
15873
16151
 
15874
- var actionBtn = bubble.querySelector("#update-bubble-action");
15875
- var bodyEl = bubble.querySelector(".notification-bubble-body");
16152
+ var actionBtn = card.querySelector("#update-bubble-action");
16153
+ var actionLabel = card.querySelector(".update-card-action-label");
16154
+ var subtitleEl = card.querySelector("#update-card-subtitle");
16155
+ var statusEl = card.querySelector("#update-card-status");
16156
+ var progressEl = card.querySelector("#update-card-progress");
16157
+
16158
+ function setStatus(text, kind) {
16159
+ if (!statusEl) return;
16160
+ statusEl.textContent = text || "";
16161
+ statusEl.classList.remove("hidden", "error", "success");
16162
+ if (!text) { statusEl.classList.add("hidden"); return; }
16163
+ if (kind) statusEl.classList.add(kind);
16164
+ }
16165
+ function setSubtitle(text) {
16166
+ if (subtitleEl) subtitleEl.textContent = text || "";
16167
+ }
16168
+ function setProgress(active) {
16169
+ if (!progressEl) return;
16170
+ progressEl.classList.toggle("active", !!active);
16171
+ }
15876
16172
 
15877
16173
  if (actionBtn) actionBtn.onclick = function() {
15878
16174
  // Phase 1: Performing update
15879
16175
  actionBtn.disabled = true;
15880
- actionBtn.textContent = "\u66f4\u65b0\u4e2d\u2026";
15881
- if (bodyEl) bodyEl.textContent = "\u6b63\u5728\u4e0b\u8f7d\u5e76\u5b89\u88c5\u65b0\u7248\u672c\u2026";
16176
+ card.classList.add("is-busy");
16177
+ if (actionLabel) actionLabel.textContent = "\u66f4\u65b0\u4e2d\u2026";
16178
+ setSubtitle("\u6b63\u5728\u4e0b\u8f7d\u5e76\u5b89\u88c5\u65b0\u7248\u672c\u2026");
16179
+ setProgress(true);
16180
+ setStatus("");
15882
16181
 
15883
16182
  fetch("/api/update", {
15884
16183
  method: "POST",
@@ -15887,39 +16186,58 @@
15887
16186
  })
15888
16187
  .then(function(res) { return res.json(); })
15889
16188
  .then(function(data) {
16189
+ setProgress(false);
16190
+ card.classList.remove("is-busy");
15890
16191
  if (data.error) {
15891
16192
  // Update failed
15892
- if (bodyEl) {
15893
- bodyEl.textContent = data.error;
15894
- bodyEl.style.color = "var(--error)";
15895
- }
16193
+ setSubtitle("\u66f4\u65b0\u672a\u5b8c\u6210");
16194
+ setStatus(data.error, "error");
15896
16195
  actionBtn.disabled = false;
15897
- actionBtn.textContent = "\u91cd\u8bd5";
16196
+ if (actionLabel) actionLabel.textContent = "\u91cd\u8bd5";
15898
16197
  return;
15899
16198
  }
15900
16199
  // Phase 2: Update succeeded, show restart button
15901
- if (bodyEl) {
15902
- bodyEl.textContent = data.message || "\u66f4\u65b0\u5b8c\u6210";
15903
- bodyEl.style.color = "var(--success)";
15904
- }
15905
- actionBtn.textContent = "\u91cd\u542f\u751f\u6548";
16200
+ setSubtitle(data.message || "\u66f4\u65b0\u5b8c\u6210\uff0c\u91cd\u542f\u540e\u751f\u6548");
16201
+ setStatus("");
16202
+ card.classList.add("is-success");
16203
+ if (actionLabel) actionLabel.textContent = "\u91cd\u542f\u751f\u6548";
15906
16204
  actionBtn.disabled = false;
15907
- actionBtn.className = "primary success";
15908
16205
  actionBtn.onclick = function() {
15909
- performRestart(actionBtn, bodyEl);
16206
+ actionBtn.disabled = true;
16207
+ if (actionLabel) actionLabel.textContent = "\u6b63\u5728\u91cd\u542f\u2026";
16208
+ setSubtitle("\u670d\u52a1\u6b63\u5728\u91cd\u542f\u2026");
16209
+ setProgress(true);
16210
+ performRestartCard(actionBtn, actionLabel, subtitleEl, statusEl, progressEl);
15910
16211
  };
15911
16212
  })
15912
16213
  .catch(function() {
15913
- if (bodyEl) {
15914
- bodyEl.textContent = "\u66f4\u65b0\u5931\u8d25\uff0c\u8bf7\u68c0\u67e5\u7f51\u7edc\u8fde\u63a5\u3002";
15915
- bodyEl.style.color = "var(--error)";
15916
- }
16214
+ setProgress(false);
16215
+ card.classList.remove("is-busy");
16216
+ setSubtitle("\u66f4\u65b0\u672a\u5b8c\u6210");
16217
+ setStatus("\u8bf7\u68c0\u67e5\u7f51\u7edc\u8fde\u63a5\u540e\u91cd\u8bd5", "error");
15917
16218
  actionBtn.disabled = false;
15918
- actionBtn.textContent = "\u91cd\u8bd5";
16219
+ if (actionLabel) actionLabel.textContent = "\u91cd\u8bd5";
15919
16220
  });
15920
16221
  };
15921
16222
  }
15922
16223
 
16224
+ // Restart driver used by the new update card.
16225
+ function performRestartCard(btn, labelEl, subtitleEl, statusEl, progressEl) {
16226
+ fetch("/api/restart", {
16227
+ method: "POST",
16228
+ headers: { "Content-Type": "application/json" },
16229
+ credentials: "same-origin"
16230
+ })
16231
+ .then(function(res) { return res.json(); })
16232
+ .then(function() {
16233
+ showRestartOverlay();
16234
+ })
16235
+ .catch(function() {
16236
+ // Network error likely means the server already shut down \u2014 show overlay anyway
16237
+ showRestartOverlay();
16238
+ });
16239
+ }
16240
+
15923
16241
  /**
15924
16242
  * Call POST /api/restart and show the restart overlay.
15925
16243
  */