@co0ontty/wand 1.21.11 → 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.
package/dist/server.js CHANGED
@@ -13,6 +13,7 @@ import { WebSocketServer } from "ws";
13
13
  import { ensureAvatarSeed, getAvatarSvg } from "./avatar.js";
14
14
  import { createSession, revokeSession, setAuthStorage, validateSession } from "./auth.js";
15
15
  import { ensureCertificates } from "./cert.js";
16
+ import { buildChildEnv } from "./env-utils.js";
16
17
  import { isExecutionMode, PREFERENCE_KEYS, resolveConfigDir, saveConfig, writePreferenceToStorage, } from "./config.js";
17
18
  import { getCachedModels, refreshModels } from "./models.js";
18
19
  import { ProcessManager } from "./process-manager.js";
@@ -804,6 +805,45 @@ export async function startServer(config, configPath) {
804
805
  github: ghApk ? { fileName: ghApk.fileName, version: ghApk.version, size: ghApk.size, downloadUrl: ghApk.downloadUrl } : null,
805
806
  });
806
807
  });
808
+ // 返回当前 inheritEnv 配置下,wand 启动 PTY / 结构化子进程时实际会传给
809
+ // claude / codex 的环境变量集合。值会按下面的规则做掩码:
810
+ // - 名字里含 KEY/TOKEN/SECRET/PASSWORD/AUTH/CREDENTIAL/COOKIE/SESSION 的视为敏感
811
+ // - 敏感值默认显示为 ***(保留长度提示),可通过 ?reveal=1 取消掩码
812
+ // 即使开启 reveal,仍只对已认证用户可见(路由由全局 requireAuth 保护)。
813
+ app.get("/api/settings/env-preview", (req, res) => {
814
+ const inheritEnv = config.inheritEnv !== false;
815
+ // 复用与 process-manager / structured-session-manager 相同的组装逻辑,
816
+ // 这样 UI 上看到的就是真正会被注入到子进程的那一份环境。
817
+ const env = buildChildEnv(inheritEnv, {
818
+ // PTY runner 还会注入 WAND_* 用于 mode 协调,这里也展示出来便于排查。
819
+ WAND_MODE: "<runtime>",
820
+ WAND_AUTO_CONFIRM: "<runtime>",
821
+ WAND_AUTO_EDIT: "<runtime>",
822
+ });
823
+ const reveal = req.query.reveal === "1" || req.query.reveal === "true";
824
+ const SENSITIVE_PATTERN = /(KEY|TOKEN|SECRET|PASSWORD|AUTH|CREDENTIAL|COOKIE|SESSION)/i;
825
+ const entries = Object.keys(env)
826
+ .sort()
827
+ .map((name) => {
828
+ const raw = env[name] ?? "";
829
+ const sensitive = SENSITIVE_PATTERN.test(name);
830
+ const masked = sensitive && !reveal;
831
+ // WAND_* 占位值不算敏感,保持原样。
832
+ const isPlaceholder = raw.startsWith("<") && raw.endsWith(">");
833
+ return {
834
+ name,
835
+ value: masked && !isPlaceholder ? "***" : raw,
836
+ length: raw.length,
837
+ sensitive,
838
+ };
839
+ });
840
+ res.json({
841
+ inheritEnv,
842
+ total: entries.length,
843
+ reveal,
844
+ entries,
845
+ });
846
+ });
807
847
  app.get("/api/app-connect-code", requireAuth, (req, res) => {
808
848
  const dbPassword = storage.getPassword();
809
849
  const effectivePassword = dbPassword ?? config.password;
@@ -1536,9 +1536,13 @@ export class StructuredSessionManager {
1536
1536
  : `Please respond in ${language}. Use ${language} for all your explanations, comments, and conversational text.`);
1537
1537
  }
1538
1538
  const sdkClaudeBinary = resolveSdkClaudeBinary();
1539
+ // SDK 默认会把整个 process.env 透传给 claude 子进程;这里显式按 inheritEnv 配置组装,
1540
+ // 否则关闭"继承环境变量"开关时 SDK 路径会被静默忽略。
1541
+ const sdkEnv = buildChildEnv(this.config.inheritEnv !== false);
1539
1542
  const sdkOptions = {
1540
1543
  cwd: session.cwd,
1541
1544
  abortController,
1545
+ env: sdkEnv,
1542
1546
  permissionMode,
1543
1547
  ...(permissionMode === "bypassPermissions" ? { allowDangerouslySkipPermissions: true } : {}),
1544
1548
  ...(allowedToolsForRoot ? { allowedTools: allowedToolsForRoot } : {}),
@@ -2169,10 +2169,13 @@
2169
2169
  '<label class="settings-toggle-title" for="cfg-inherit-env">继承环境变量</label>' +
2170
2170
  '<span class="settings-toggle-desc">启动 PTY / 结构化子进程时,把当前服务进程的环境变量传给 claude / codex。关闭后子进程仅获得最小可用环境(PATH/HOME/SHELL/LANG/TERM 等),可用于隔离 API key 等敏感凭据。</span>' +
2171
2171
  '</div>' +
2172
- '<label class="settings-switch">' +
2173
- '<input id="cfg-inherit-env" type="checkbox" class="switch-toggle" />' +
2174
- '<span class="switch-slider"></span>' +
2175
- '</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>' +
2176
2179
  '</div>' +
2177
2180
  '<div class="field">' +
2178
2181
  '<label class="field-label" for="cfg-default-model">默认模型</label>' +
@@ -4020,6 +4023,8 @@
4020
4023
  if (saveConfigBtn) saveConfigBtn.addEventListener("click", saveConfigSettings);
4021
4024
  var defaultModelRefreshBtn = document.getElementById("cfg-default-model-refresh");
4022
4025
  if (defaultModelRefreshBtn) defaultModelRefreshBtn.addEventListener("click", refreshAvailableModels);
4026
+ var viewEnvBtn = document.getElementById("cfg-view-env-btn");
4027
+ if (viewEnvBtn) viewEnvBtn.addEventListener("click", openEnvPreviewModal);
4023
4028
  var saveDisplayBtn = document.getElementById("save-display-button");
4024
4029
  if (saveDisplayBtn) saveDisplayBtn.addEventListener("click", saveDisplaySettings);
4025
4030
  // App icon picker (APK only)
@@ -6002,31 +6007,14 @@
6002
6007
  }
6003
6008
 
6004
6009
  function getComposerPlaceholder(session, terminalInteractive) {
6005
- if (terminalInteractive) {
6006
- return "终端交互模式开启中,请直接在终端中输入";
6007
- }
6008
- if (session && isStructuredSession(session)) {
6009
- return session.provider === "codex"
6010
- ? "向 Codex 发送消息;chat 为结构化对话视图"
6011
- : "向 Claude 发送消息;chat 为结构化对话视图";
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 "会话已结束";
6012
6016
  }
6013
- if (session && session.provider === "codex") {
6014
- if (session.status !== "running") {
6015
- return "Codex 会话已结束,无法继续发送";
6016
- }
6017
- return state.currentView === "terminal"
6018
- ? "向 Codex 发送输入;terminal 为原始 TUI 输出"
6019
- : "向 Codex 发送输入;chat 为解析后的阅读视图";
6020
- }
6021
- if (session && !isStructuredSession(session) && session.status !== "running") {
6022
- if (canAutoResumeSession(session)) {
6023
- return "输入消息...";
6024
- }
6025
- return "会话已结束,无法继续发送";
6026
- }
6027
- return session && isStructuredSession(session) && session.structuredState && session.structuredState.inFlight
6028
- ? "思考中 · 发送新消息将中断当前回复"
6029
- : "输入消息...";
6017
+ return "";
6030
6018
  }
6031
6019
 
6032
6020
  function getToolModeHint(tool, mode) {
@@ -6207,6 +6195,143 @@
6207
6195
  });
6208
6196
  }
6209
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
+
6210
6335
  function updateSettingsDefaultModelSelect(data) {
6211
6336
  var select = document.getElementById("cfg-default-model");
6212
6337
  if (!select) return;
@@ -9253,7 +9378,7 @@
9253
9378
  var todoEl = document.getElementById("todo-progress");
9254
9379
  if (todoEl) todoEl.classList.add("hidden");
9255
9380
  welcomeInput.value = "";
9256
- welcomeInput.placeholder = "正在启动会话...";
9381
+ welcomeInput.placeholder = "正在启动…";
9257
9382
  welcomeInput.disabled = true;
9258
9383
  var mode = state.chatMode || "managed";
9259
9384
  var defaultCwd = getEffectiveCwd();
@@ -9274,7 +9399,7 @@
9274
9399
  .then(function(data) {
9275
9400
  if (data.error) {
9276
9401
  showToast(data.error, "error");
9277
- welcomeInput.placeholder = "输入你的问题,按 Enter 发送...";
9402
+ welcomeInput.placeholder = "输入消息";
9278
9403
  welcomeInput.disabled = false;
9279
9404
  return;
9280
9405
  }
@@ -9287,7 +9412,7 @@
9287
9412
  switchToSessionView(data.id);
9288
9413
  subscribeToSession(data.id);
9289
9414
  loadOutput(data.id).then(function() {
9290
- welcomeInput.placeholder = "输入你的问题,按 Enter 发送...";
9415
+ welcomeInput.placeholder = "输入消息";
9291
9416
  welcomeInput.disabled = false;
9292
9417
  focusInputBox(true);
9293
9418
  });
@@ -9296,7 +9421,7 @@
9296
9421
  showToast((error && error.message) || (preferredTool === "codex"
9297
9422
  ? "无法启动 Codex 会话。"
9298
9423
  : "无法启动 Claude 会话。"), "error");
9299
- welcomeInput.placeholder = "输入你的问题,按 Enter 发送...";
9424
+ welcomeInput.placeholder = "输入消息";
9300
9425
  welcomeInput.disabled = false;
9301
9426
  });
9302
9427
  }
@@ -10672,7 +10797,7 @@
10672
10797
  function createSessionFromWelcomeInput(value) {
10673
10798
  var welcomeInput = document.getElementById("welcome-input");
10674
10799
  if (!welcomeInput) return;
10675
- welcomeInput.placeholder = "Claude 正在思考,请稍候...";
10800
+ welcomeInput.placeholder = "正在思考…";
10676
10801
  welcomeInput.disabled = true;
10677
10802
  var mode = state.chatMode || "managed";
10678
10803
  var defaultCwd = getEffectiveCwd();
@@ -10694,7 +10819,7 @@
10694
10819
  .then(function(data) {
10695
10820
  if (data.error) {
10696
10821
  showToast(data.error, "error");
10697
- welcomeInput.placeholder = "输入你的问题,按 Enter 发送...";
10822
+ welcomeInput.placeholder = "输入消息";
10698
10823
  welcomeInput.disabled = false;
10699
10824
  return null;
10700
10825
  }
@@ -10702,11 +10827,11 @@
10702
10827
  })
10703
10828
  .catch(function(error) {
10704
10829
  showToast((error && error.message) || "无法启动会话。", "error");
10705
- welcomeInput.placeholder = "输入你的问题,按 Enter 发送...";
10830
+ welcomeInput.placeholder = "输入消息";
10706
10831
  welcomeInput.disabled = false;
10707
10832
  })
10708
10833
  .finally(function() {
10709
- welcomeInput.placeholder = "输入你的问题,按 Enter 发送...";
10834
+ welcomeInput.placeholder = "输入消息";
10710
10835
  welcomeInput.disabled = false;
10711
10836
  });
10712
10837
  }
@@ -15972,43 +16097,87 @@
15972
16097
  playNotificationSound();
15973
16098
 
15974
16099
  var id = ++notificationIdCounter;
15975
- var bubble = document.createElement("div");
15976
- bubble.className = "notification-bubble";
15977
- bubble.setAttribute("data-nid", id);
15978
-
15979
- bubble.innerHTML =
15980
- '<div class="notification-bubble-header">' +
15981
- '<span class="notification-bubble-icon info">\u2191</span>' +
15982
- '<span class="notification-bubble-title">\u53d1\u73b0\u65b0\u7248\u672c</span>' +
15983
- '<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>' +
15984
16129
  '</div>' +
15985
- '<div class="notification-bubble-body">' +
15986
- 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>' +
15987
16132
  '</div>' +
15988
- '<div class="notification-bubble-actions">' +
15989
- '<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>' +
15990
16138
  '</div>';
15991
16139
 
15992
- document.body.appendChild(bubble);
16140
+ document.body.appendChild(card);
15993
16141
 
15994
- var entry = { id: id, el: bubble };
16142
+ var entry = { id: id, el: card };
15995
16143
  notificationStack.push(entry);
15996
16144
  repositionNotifications();
15997
16145
 
15998
- var closeBtn = bubble.querySelector(".notification-bubble-close");
16146
+ var closeBtn = card.querySelector(".update-card-close");
15999
16147
  if (closeBtn) closeBtn.onclick = function() {
16000
16148
  dismissNotification(id);
16001
16149
  state._updateBubbleShown = false;
16002
16150
  };
16003
16151
 
16004
- var actionBtn = bubble.querySelector("#update-bubble-action");
16005
- 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
+ }
16006
16172
 
16007
16173
  if (actionBtn) actionBtn.onclick = function() {
16008
16174
  // Phase 1: Performing update
16009
16175
  actionBtn.disabled = true;
16010
- actionBtn.textContent = "\u66f4\u65b0\u4e2d\u2026";
16011
- 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("");
16012
16181
 
16013
16182
  fetch("/api/update", {
16014
16183
  method: "POST",
@@ -16017,39 +16186,58 @@
16017
16186
  })
16018
16187
  .then(function(res) { return res.json(); })
16019
16188
  .then(function(data) {
16189
+ setProgress(false);
16190
+ card.classList.remove("is-busy");
16020
16191
  if (data.error) {
16021
16192
  // Update failed
16022
- if (bodyEl) {
16023
- bodyEl.textContent = data.error;
16024
- bodyEl.style.color = "var(--error)";
16025
- }
16193
+ setSubtitle("\u66f4\u65b0\u672a\u5b8c\u6210");
16194
+ setStatus(data.error, "error");
16026
16195
  actionBtn.disabled = false;
16027
- actionBtn.textContent = "\u91cd\u8bd5";
16196
+ if (actionLabel) actionLabel.textContent = "\u91cd\u8bd5";
16028
16197
  return;
16029
16198
  }
16030
16199
  // Phase 2: Update succeeded, show restart button
16031
- if (bodyEl) {
16032
- bodyEl.textContent = data.message || "\u66f4\u65b0\u5b8c\u6210";
16033
- bodyEl.style.color = "var(--success)";
16034
- }
16035
- 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";
16036
16204
  actionBtn.disabled = false;
16037
- actionBtn.className = "primary success";
16038
16205
  actionBtn.onclick = function() {
16039
- 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);
16040
16211
  };
16041
16212
  })
16042
16213
  .catch(function() {
16043
- if (bodyEl) {
16044
- bodyEl.textContent = "\u66f4\u65b0\u5931\u8d25\uff0c\u8bf7\u68c0\u67e5\u7f51\u7edc\u8fde\u63a5\u3002";
16045
- bodyEl.style.color = "var(--error)";
16046
- }
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");
16047
16218
  actionBtn.disabled = false;
16048
- actionBtn.textContent = "\u91cd\u8bd5";
16219
+ if (actionLabel) actionLabel.textContent = "\u91cd\u8bd5";
16049
16220
  });
16050
16221
  };
16051
16222
  }
16052
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
+
16053
16241
  /**
16054
16242
  * Call POST /api/restart and show the restart overlay.
16055
16243
  */
@@ -9185,6 +9185,258 @@
9185
9185
 
9186
9186
  /* .btn-success defined globally in the button system above */
9187
9187
 
9188
+ /* ── Update Card (richer than the generic notification bubble) ── */
9189
+ .notification-bubble.update-card {
9190
+ width: min(360px, calc(100vw - 24px));
9191
+ max-width: min(360px, calc(100vw - 24px));
9192
+ min-width: 0;
9193
+ padding: 14px 14px 12px;
9194
+ border-radius: var(--radius-md);
9195
+ background:
9196
+ radial-gradient(120% 120% at 0% 0%, var(--accent-muted) 0%, transparent 55%),
9197
+ linear-gradient(180deg, var(--bg-elevated) 0%, var(--bg-primary) 100%);
9198
+ border: 1px solid var(--border-strong);
9199
+ box-shadow: var(--shadow-xl), 0 0 0 1px rgba(255,255,255,0.4) inset;
9200
+ overflow: hidden;
9201
+ isolation: isolate;
9202
+ }
9203
+ .update-card-shine {
9204
+ position: absolute;
9205
+ inset: 0;
9206
+ pointer-events: none;
9207
+ background: linear-gradient(115deg,
9208
+ transparent 0%,
9209
+ rgba(255, 255, 255, 0.18) 45%,
9210
+ rgba(255, 255, 255, 0.32) 50%,
9211
+ rgba(255, 255, 255, 0.18) 55%,
9212
+ transparent 100%);
9213
+ background-size: 220% 100%;
9214
+ background-position: 120% 0;
9215
+ mix-blend-mode: overlay;
9216
+ opacity: 0.65;
9217
+ animation: update-card-shine 6s ease-in-out infinite;
9218
+ z-index: 0;
9219
+ }
9220
+ @keyframes update-card-shine {
9221
+ 0% { background-position: 120% 0; }
9222
+ 55% { background-position: -20% 0; }
9223
+ 100% { background-position: -20% 0; }
9224
+ }
9225
+ .update-card.is-success {
9226
+ background:
9227
+ radial-gradient(120% 120% at 0% 0%, var(--success-muted) 0%, transparent 55%),
9228
+ linear-gradient(180deg, var(--bg-elevated) 0%, var(--bg-primary) 100%);
9229
+ border-color: var(--success);
9230
+ box-shadow: var(--shadow-xl), 0 0 0 1px rgba(255,255,255,0.4) inset, var(--shadow-glow-success);
9231
+ }
9232
+ .update-card-header {
9233
+ position: relative;
9234
+ z-index: 1;
9235
+ display: flex;
9236
+ align-items: flex-start;
9237
+ gap: 10px;
9238
+ }
9239
+ .update-card-icon {
9240
+ width: 32px;
9241
+ height: 32px;
9242
+ flex-shrink: 0;
9243
+ border-radius: var(--radius-sm);
9244
+ display: flex;
9245
+ align-items: center;
9246
+ justify-content: center;
9247
+ color: white;
9248
+ background: linear-gradient(135deg, var(--accent) 0%, var(--accent-hover) 100%);
9249
+ box-shadow: 0 4px 12px var(--accent-glow), inset 0 1px 0 rgba(255,255,255,0.35);
9250
+ }
9251
+ .update-card.is-success .update-card-icon {
9252
+ background: linear-gradient(135deg, var(--success) 0%, var(--success-hover) 100%);
9253
+ box-shadow: 0 4px 12px var(--success-glow), inset 0 1px 0 rgba(255,255,255,0.35);
9254
+ }
9255
+ .update-card-heading {
9256
+ flex: 1;
9257
+ min-width: 0;
9258
+ display: flex;
9259
+ flex-direction: column;
9260
+ gap: 2px;
9261
+ }
9262
+ .update-card-title {
9263
+ font-size: 0.88rem;
9264
+ font-weight: 700;
9265
+ color: var(--text-primary);
9266
+ letter-spacing: 0.01em;
9267
+ line-height: 1.25;
9268
+ }
9269
+ .update-card-subtitle {
9270
+ font-size: 0.72rem;
9271
+ color: var(--text-tertiary);
9272
+ line-height: 1.35;
9273
+ }
9274
+ .update-card-close {
9275
+ flex-shrink: 0;
9276
+ width: 24px;
9277
+ height: 24px;
9278
+ border: none;
9279
+ border-radius: var(--radius-sm);
9280
+ background: transparent;
9281
+ color: var(--text-muted);
9282
+ cursor: pointer;
9283
+ display: flex;
9284
+ align-items: center;
9285
+ justify-content: center;
9286
+ padding: 0;
9287
+ transition: background 0.15s, color 0.15s;
9288
+ }
9289
+ .update-card-close:hover {
9290
+ background: var(--bg-tertiary);
9291
+ color: var(--text-primary);
9292
+ }
9293
+
9294
+ .update-card-version {
9295
+ position: relative;
9296
+ z-index: 1;
9297
+ margin: 12px 0 10px;
9298
+ display: flex;
9299
+ align-items: center;
9300
+ gap: 8px;
9301
+ flex-wrap: nowrap;
9302
+ }
9303
+ .update-card-version-chip {
9304
+ font-family: var(--font-mono, ui-monospace, "SF Mono", Menlo, Consolas, monospace);
9305
+ font-size: 0.74rem;
9306
+ font-weight: 600;
9307
+ padding: 3px 10px;
9308
+ border-radius: var(--radius-full);
9309
+ border: 1px solid var(--border-subtle);
9310
+ white-space: nowrap;
9311
+ }
9312
+ .update-card-version-current {
9313
+ color: var(--text-tertiary);
9314
+ background: var(--bg-tertiary);
9315
+ }
9316
+ .update-card-version-latest {
9317
+ color: var(--accent);
9318
+ background: var(--accent-muted);
9319
+ border-color: var(--accent);
9320
+ box-shadow: 0 1px 6px var(--accent-glow);
9321
+ }
9322
+ .update-card.is-success .update-card-version-latest {
9323
+ color: var(--success);
9324
+ background: var(--success-muted);
9325
+ border-color: var(--success);
9326
+ box-shadow: 0 1px 6px var(--success-glow);
9327
+ }
9328
+ .update-card-version-arrow {
9329
+ color: var(--text-muted);
9330
+ flex-shrink: 0;
9331
+ }
9332
+
9333
+ .update-card-progress {
9334
+ position: relative;
9335
+ z-index: 1;
9336
+ height: 0;
9337
+ overflow: hidden;
9338
+ margin: 0;
9339
+ transition: height 0.25s ease, margin 0.25s ease;
9340
+ }
9341
+ .update-card-progress.active {
9342
+ height: 6px;
9343
+ margin: 0 0 10px;
9344
+ }
9345
+ .update-card-progress-track {
9346
+ position: relative;
9347
+ width: 100%;
9348
+ height: 100%;
9349
+ background: var(--bg-tertiary);
9350
+ border-radius: var(--radius-full);
9351
+ overflow: hidden;
9352
+ }
9353
+ .update-card-progress-fill {
9354
+ position: absolute;
9355
+ top: 0;
9356
+ left: -40%;
9357
+ width: 40%;
9358
+ height: 100%;
9359
+ border-radius: var(--radius-full);
9360
+ background: linear-gradient(90deg, var(--accent) 0%, var(--accent-hover) 100%);
9361
+ box-shadow: 0 0 10px var(--accent-glow);
9362
+ animation: update-card-progress-sweep 1.4s cubic-bezier(0.65, 0.05, 0.35, 1) infinite;
9363
+ }
9364
+ @keyframes update-card-progress-sweep {
9365
+ 0% { left: -45%; width: 40%; }
9366
+ 50% { left: 25%; width: 55%; }
9367
+ 100% { left: 105%; width: 40%; }
9368
+ }
9369
+
9370
+ .update-card-status {
9371
+ position: relative;
9372
+ z-index: 1;
9373
+ font-size: 0.72rem;
9374
+ line-height: 1.4;
9375
+ padding: 6px 10px;
9376
+ border-radius: var(--radius-sm);
9377
+ margin-bottom: 10px;
9378
+ background: var(--bg-tertiary);
9379
+ color: var(--text-secondary);
9380
+ border: 1px solid var(--border-subtle);
9381
+ }
9382
+ .update-card-status.error {
9383
+ background: var(--danger-muted);
9384
+ color: var(--danger);
9385
+ border-color: var(--danger);
9386
+ }
9387
+ .update-card-status.success {
9388
+ background: var(--success-muted);
9389
+ color: var(--success);
9390
+ border-color: var(--success);
9391
+ }
9392
+
9393
+ .update-card-actions {
9394
+ position: relative;
9395
+ z-index: 1;
9396
+ display: flex;
9397
+ justify-content: flex-end;
9398
+ gap: 8px;
9399
+ }
9400
+ .update-card-action {
9401
+ font-size: 0.78rem;
9402
+ font-weight: 600;
9403
+ padding: 7px 16px;
9404
+ border-radius: var(--radius-sm);
9405
+ border: 1px solid transparent;
9406
+ cursor: pointer;
9407
+ display: inline-flex;
9408
+ align-items: center;
9409
+ gap: 6px;
9410
+ transition: transform 0.12s ease, filter 0.15s ease, box-shadow 0.15s ease;
9411
+ }
9412
+ .update-card-action-primary {
9413
+ background: linear-gradient(135deg, var(--accent) 0%, var(--accent-hover) 100%);
9414
+ color: white;
9415
+ box-shadow: 0 4px 12px var(--accent-glow), inset 0 1px 0 rgba(255,255,255,0.25);
9416
+ }
9417
+ .update-card-action-primary:hover:not(:disabled) {
9418
+ filter: brightness(1.05);
9419
+ transform: translateY(-1px);
9420
+ box-shadow: 0 6px 16px var(--accent-glow), inset 0 1px 0 rgba(255,255,255,0.3);
9421
+ }
9422
+ .update-card-action-primary:active:not(:disabled) {
9423
+ transform: translateY(0);
9424
+ filter: brightness(0.96);
9425
+ }
9426
+ .update-card-action:disabled {
9427
+ opacity: 0.7;
9428
+ cursor: progress;
9429
+ filter: none;
9430
+ transform: none;
9431
+ }
9432
+ .update-card.is-success .update-card-action-primary {
9433
+ background: linear-gradient(135deg, var(--success) 0%, var(--success-hover) 100%);
9434
+ box-shadow: 0 4px 12px var(--success-glow), inset 0 1px 0 rgba(255,255,255,0.25);
9435
+ }
9436
+ .update-card.is-success .update-card-action-primary:hover:not(:disabled) {
9437
+ box-shadow: 0 6px 16px var(--success-glow), inset 0 1px 0 rgba(255,255,255,0.3);
9438
+ }
9439
+
9188
9440
  /* Restart overlay */
9189
9441
  .restart-overlay {
9190
9442
  position: fixed;
@@ -11676,6 +11928,202 @@
11676
11928
  cursor: pointer;
11677
11929
  display: inline-block;
11678
11930
  }
11931
+ /* Toggle 行右侧附属操作(如「查看」按钮 + 开关同列) */
11932
+ .settings-toggle-aside {
11933
+ flex: 0 0 auto;
11934
+ display: flex;
11935
+ align-items: center;
11936
+ gap: 10px;
11937
+ }
11938
+
11939
+ /* ── Env preview modal ── */
11940
+ .env-preview-modal {
11941
+ max-width: 720px;
11942
+ }
11943
+ .env-preview-body {
11944
+ display: flex;
11945
+ flex-direction: column;
11946
+ gap: 12px;
11947
+ padding: 18px 24px 8px;
11948
+ min-height: 0;
11949
+ }
11950
+ .env-preview-toolbar {
11951
+ display: flex;
11952
+ align-items: center;
11953
+ justify-content: space-between;
11954
+ gap: 12px;
11955
+ flex-wrap: wrap;
11956
+ }
11957
+ .env-preview-meta {
11958
+ display: inline-flex;
11959
+ align-items: center;
11960
+ gap: 8px;
11961
+ font-size: 0.78rem;
11962
+ color: var(--text-secondary);
11963
+ }
11964
+ .env-preview-pill {
11965
+ display: inline-flex;
11966
+ align-items: center;
11967
+ padding: 2px 10px;
11968
+ border-radius: var(--radius-full);
11969
+ font-size: 0.72rem;
11970
+ font-weight: 600;
11971
+ letter-spacing: 0.02em;
11972
+ border: 1px solid var(--border-subtle);
11973
+ }
11974
+ .env-preview-pill.is-inherit {
11975
+ color: var(--accent);
11976
+ background: var(--accent-muted);
11977
+ border-color: var(--accent);
11978
+ }
11979
+ .env-preview-pill.is-minimal {
11980
+ color: var(--success);
11981
+ background: var(--success-muted);
11982
+ border-color: var(--success);
11983
+ }
11984
+ .env-preview-count {
11985
+ font-size: 0.72rem;
11986
+ color: var(--text-muted);
11987
+ }
11988
+ .env-preview-controls {
11989
+ display: flex;
11990
+ align-items: center;
11991
+ gap: 10px;
11992
+ flex-wrap: wrap;
11993
+ }
11994
+ .env-preview-search {
11995
+ width: 200px;
11996
+ max-width: 100%;
11997
+ padding: 6px 10px;
11998
+ border-radius: var(--radius-sm);
11999
+ border: 1px solid var(--border);
12000
+ background: var(--bg-elevated);
12001
+ font-size: 0.78rem;
12002
+ color: var(--text-primary);
12003
+ }
12004
+ .env-preview-search:focus {
12005
+ outline: none;
12006
+ border-color: var(--border-focus);
12007
+ box-shadow: 0 0 0 3px var(--accent-glow);
12008
+ }
12009
+ .env-preview-reveal {
12010
+ display: inline-flex;
12011
+ align-items: center;
12012
+ gap: 6px;
12013
+ font-size: 0.74rem;
12014
+ color: var(--text-secondary);
12015
+ cursor: pointer;
12016
+ user-select: none;
12017
+ }
12018
+ .env-preview-reveal input {
12019
+ accent-color: var(--accent);
12020
+ cursor: pointer;
12021
+ }
12022
+ .env-preview-list {
12023
+ max-height: min(56vh, 460px);
12024
+ overflow-y: auto;
12025
+ border-radius: var(--radius-sm);
12026
+ border: 1px solid var(--border-subtle);
12027
+ background: var(--bg-elevated);
12028
+ padding: 4px;
12029
+ }
12030
+ .env-preview-list:focus { outline: none; }
12031
+ .env-preview-row {
12032
+ display: grid;
12033
+ grid-template-columns: minmax(0, 220px) minmax(0, 1fr) auto;
12034
+ gap: 12px;
12035
+ align-items: center;
12036
+ padding: 8px 10px;
12037
+ border-radius: var(--radius-xs);
12038
+ font-family: var(--font-mono, ui-monospace, "SF Mono", Menlo, Consolas, monospace);
12039
+ font-size: 0.74rem;
12040
+ transition: background 0.12s;
12041
+ }
12042
+ .env-preview-row:hover {
12043
+ background: var(--bg-tertiary);
12044
+ }
12045
+ .env-preview-row + .env-preview-row {
12046
+ border-top: 1px dashed var(--border-subtle);
12047
+ }
12048
+ .env-preview-name {
12049
+ color: var(--text-primary);
12050
+ font-weight: 600;
12051
+ display: inline-flex;
12052
+ align-items: center;
12053
+ gap: 6px;
12054
+ flex-wrap: wrap;
12055
+ overflow-wrap: anywhere;
12056
+ }
12057
+ .env-preview-row.is-sensitive .env-preview-name {
12058
+ color: var(--warning);
12059
+ }
12060
+ .env-preview-badge {
12061
+ display: inline-flex;
12062
+ align-items: center;
12063
+ padding: 1px 6px;
12064
+ border-radius: var(--radius-full);
12065
+ font-family: inherit;
12066
+ font-size: 0.62rem;
12067
+ font-weight: 600;
12068
+ letter-spacing: 0.04em;
12069
+ background: var(--warning-muted);
12070
+ color: var(--warning);
12071
+ border: 1px solid var(--warning);
12072
+ }
12073
+ .env-preview-badge-runtime {
12074
+ background: var(--info-muted);
12075
+ color: var(--info);
12076
+ border-color: var(--info);
12077
+ }
12078
+ .env-preview-value {
12079
+ color: var(--text-secondary);
12080
+ overflow: hidden;
12081
+ text-overflow: ellipsis;
12082
+ white-space: nowrap;
12083
+ min-width: 0;
12084
+ }
12085
+ .env-preview-value.is-runtime {
12086
+ font-style: italic;
12087
+ color: var(--text-muted);
12088
+ }
12089
+ .env-preview-len {
12090
+ color: var(--text-muted);
12091
+ font-size: 0.68rem;
12092
+ white-space: nowrap;
12093
+ }
12094
+ .env-preview-loading,
12095
+ .env-preview-empty {
12096
+ padding: 24px 8px;
12097
+ text-align: center;
12098
+ color: var(--text-muted);
12099
+ font-size: 0.78rem;
12100
+ }
12101
+ .env-preview-footer {
12102
+ display: flex;
12103
+ align-items: center;
12104
+ justify-content: space-between;
12105
+ gap: 12px;
12106
+ padding: 14px 24px 18px;
12107
+ }
12108
+ .env-preview-hint {
12109
+ font-size: 0.7rem;
12110
+ color: var(--text-muted);
12111
+ flex: 1;
12112
+ min-width: 0;
12113
+ }
12114
+
12115
+ @media (max-width: 640px) {
12116
+ .env-preview-modal { max-width: calc(100vw - 24px); }
12117
+ .env-preview-row {
12118
+ grid-template-columns: 1fr;
12119
+ gap: 4px;
12120
+ }
12121
+ .env-preview-len {
12122
+ justify-self: end;
12123
+ }
12124
+ .env-preview-search { width: 100%; }
12125
+ .env-preview-controls { width: 100%; }
12126
+ }
11679
12127
 
11680
12128
  /* Range 行(音量等) */
11681
12129
  .settings-range-row {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@co0ontty/wand",
3
- "version": "1.21.11",
3
+ "version": "1.21.12",
4
4
  "description": "A web terminal for local CLI tools like Claude.",
5
5
  "type": "module",
6
6
  "bin": {