@co0ontty/wand 1.21.16 → 1.21.18

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.
@@ -6,6 +6,7 @@ interface QuickCommitOptions {
6
6
  autoMessage: boolean;
7
7
  customMessage?: string;
8
8
  tag?: string;
9
+ /** When `tag` is empty, ask Claude to generate one based on the diff + commit message. */
9
10
  autoTag?: boolean;
10
11
  push?: boolean;
11
12
  }
@@ -13,6 +14,11 @@ export declare class QuickCommitError extends Error {
13
14
  readonly code: string;
14
15
  constructor(message: string, code: string);
15
16
  }
16
- export declare function generateCommitMessageOnly(cwd: string, language: string): Promise<string>;
17
+ export interface GenerateCommitMessageResult {
18
+ message: string;
19
+ /** AI-suggested next tag derived from the staged diff and the latest existing tag. */
20
+ suggestedTag?: string;
21
+ }
22
+ export declare function generateCommitMessageOnly(cwd: string, language: string): Promise<GenerateCommitMessageResult>;
17
23
  export declare function runQuickCommit(opts: QuickCommitOptions): Promise<QuickCommitResult>;
18
24
  export {};
@@ -141,17 +141,6 @@ export function getGitStatus(cwd) {
141
141
  }
142
142
  const allEntries = parsePorcelainV2(porcelain);
143
143
  const files = allEntries.slice(0, MAX_FILE_ENTRIES);
144
- let latestTag;
145
- let suggestedNextTag;
146
- try {
147
- latestTag = runGit(["describe", "--tags", "--abbrev=0"], cwd);
148
- }
149
- catch {
150
- latestTag = undefined;
151
- }
152
- if (latestTag) {
153
- suggestedNextTag = bumpPatchTag(latestTag);
154
- }
155
144
  return {
156
145
  isGit: true,
157
146
  branch,
@@ -160,20 +149,8 @@ export function getGitStatus(cwd) {
160
149
  head,
161
150
  repoRoot,
162
151
  initialCommit,
163
- latestTag,
164
- suggestedNextTag,
165
152
  };
166
153
  }
167
- function bumpPatchTag(tag) {
168
- const m = tag.match(/^(v?)(\d+)\.(\d+)\.(\d+)(.*)/);
169
- if (!m)
170
- return "";
171
- const prefix = m[1];
172
- const major = m[2];
173
- const minor = m[3];
174
- const patch = parseInt(m[4], 10) + 1;
175
- return `${prefix}${major}.${minor}.${patch}`;
176
- }
177
154
  export class QuickCommitError extends Error {
178
155
  code;
179
156
  constructor(message, code) {
@@ -210,7 +187,7 @@ function callClaudeText(prompt, cwd) {
210
187
  child.stdin?.end(prompt);
211
188
  });
212
189
  }
213
- async function generateCommitMessage(cwd, language) {
190
+ function collectStagedDiff(cwd) {
214
191
  let diff;
215
192
  try {
216
193
  diff = runGit(["diff", "--cached", "--submodule=log"], cwd, 5000);
@@ -229,6 +206,10 @@ async function generateCommitMessage(cwd, language) {
229
206
  if (diff.length > MAX_DIFF_FOR_AI) {
230
207
  diff = diff.slice(0, MAX_DIFF_FOR_AI) + "\n\n... (diff truncated) ...";
231
208
  }
209
+ return diff;
210
+ }
211
+ async function generateCommitMessage(cwd, language) {
212
+ const diff = collectStagedDiff(cwd);
232
213
  const lang = language.trim() || "中文";
233
214
  const prompt = `阅读以下 git diff,用${lang}写一条简洁的 commit message。要求:祈使句,不超过 50 字,描述「做了什么」。只输出 message 本身,不要引号、不要 Markdown 格式、不要任何额外说明。\n\n${diff}`;
234
215
  const raw = await callClaudeText(prompt, cwd);
@@ -238,6 +219,73 @@ async function generateCommitMessage(cwd, language) {
238
219
  }
239
220
  return message;
240
221
  }
222
+ function tryParseJson(raw) {
223
+ let text = raw.trim();
224
+ // Strip ```json … ``` or ``` … ``` fences if Claude wrapped the response
225
+ text = text.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
226
+ // Find the first balanced-looking JSON object substring
227
+ const start = text.indexOf("{");
228
+ const end = text.lastIndexOf("}");
229
+ if (start === -1 || end === -1 || end <= start)
230
+ return null;
231
+ try {
232
+ return JSON.parse(text.slice(start, end + 1));
233
+ }
234
+ catch {
235
+ return null;
236
+ }
237
+ }
238
+ function sanitizeSuggestedTag(value) {
239
+ if (typeof value !== "string")
240
+ return undefined;
241
+ const cleaned = value.trim().replace(/^["'`]+|["'`]+$/g, "").trim();
242
+ if (!cleaned)
243
+ return undefined;
244
+ // Accept common semver-ish forms (v1.2.3, 1.2.3, v1.2.3-rc.1, v1.2.3+build.5)
245
+ if (!/^v?\d+\.\d+\.\d+([.\-+][0-9A-Za-z.\-+]*)?$/.test(cleaned))
246
+ return undefined;
247
+ return cleaned;
248
+ }
249
+ async function generateCommitMessageWithTag(cwd, language) {
250
+ const diff = collectStagedDiff(cwd);
251
+ let latestTag;
252
+ try {
253
+ latestTag = runGit(["describe", "--tags", "--abbrev=0"], cwd) || undefined;
254
+ }
255
+ catch {
256
+ latestTag = undefined;
257
+ }
258
+ const lang = language.trim() || "中文";
259
+ const tagHint = latestTag
260
+ ? `当前最新 tag 是 \`${latestTag}\`,请基于它给出下一个版本号(保持原有前缀风格,例如有 \`v\` 就保留 \`v\`)。`
261
+ : `仓库还没有任何 tag,请直接给一个起始版本号(建议 \`v0.0.1\` / \`v0.1.0\` / \`v1.0.0\` 之一,按改动幅度选择)。`;
262
+ const prompt = `阅读以下 git diff,完成两件事:
263
+ 1. 用${lang}写一条简洁的 commit message(祈使句,不超过 50 字,描述「做了什么」)。
264
+ 2. 根据改动幅度推荐下一个语义化版本 tag(破坏性变更 → 升 major;新增功能 → 升 minor;修复 / 文档 / 重构 / 维护 → 升 patch)。${tagHint}
265
+
266
+ 请严格输出**单行 JSON 对象**,不要 Markdown 代码块、不要任何解释文字、不要多余引号。格式:
267
+ {"message":"...","tag":"v1.2.3"}
268
+
269
+ git diff:
270
+ ${diff}`;
271
+ const raw = await callClaudeText(prompt, cwd);
272
+ const parsed = tryParseJson(raw);
273
+ let message;
274
+ let suggestedTag;
275
+ if (parsed && typeof parsed.message === "string") {
276
+ message = parsed.message.replace(/^["'`]+|["'`]+$/g, "").trim();
277
+ suggestedTag = sanitizeSuggestedTag(parsed.tag);
278
+ }
279
+ else {
280
+ // Fallback: treat whole output as message, no tag suggestion
281
+ message = raw.replace(/^["'`]+|["'`]+$/g, "").trim();
282
+ suggestedTag = undefined;
283
+ }
284
+ if (!message) {
285
+ throw new QuickCommitError("Claude 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
286
+ }
287
+ return { message, suggestedTag };
288
+ }
241
289
  export async function generateCommitMessageOnly(cwd, language) {
242
290
  if (!cwd || !existsSync(cwd)) {
243
291
  throw new QuickCommitError("工作目录不存在。", "CWD_MISSING");
@@ -248,7 +296,65 @@ export async function generateCommitMessageOnly(cwd, language) {
248
296
  catch {
249
297
  // best-effort staging so the diff is complete
250
298
  }
251
- return generateCommitMessage(cwd, language);
299
+ return generateCommitMessageWithTag(cwd, language);
300
+ }
301
+ /**
302
+ * Ask Claude for a single tag string. Called from `runQuickCommit` after the commit has
303
+ * already landed, so we look at `git show HEAD` and use `HEAD~1` for the previous tag.
304
+ */
305
+ async function generateTagAfterCommit(cwd, language, commitMessage) {
306
+ let diff;
307
+ try {
308
+ diff = runGit(["show", "HEAD", "--no-color", "--submodule=log"], cwd, 5000);
309
+ }
310
+ catch {
311
+ diff = "";
312
+ }
313
+ if (!diff) {
314
+ try {
315
+ diff = runGit(["show", "HEAD", "--name-only"], cwd, 3000);
316
+ }
317
+ catch {
318
+ diff = "(no diff available)";
319
+ }
320
+ }
321
+ if (diff.length > MAX_DIFF_FOR_AI) {
322
+ diff = diff.slice(0, MAX_DIFF_FOR_AI) + "\n\n... (diff truncated) ...";
323
+ }
324
+ let latestTag;
325
+ try {
326
+ // We just made a commit, so look for the most recent tag reachable from HEAD~1.
327
+ latestTag = runGit(["describe", "--tags", "--abbrev=0", "HEAD~1"], cwd) || undefined;
328
+ }
329
+ catch {
330
+ latestTag = undefined;
331
+ }
332
+ const lang = language.trim() || "中文";
333
+ const tagHint = latestTag
334
+ ? `当前最新 tag 是 \`${latestTag}\`,请基于它给出下一个版本号(保持原有前缀风格,例如有 \`v\` 就保留 \`v\`)。`
335
+ : `仓库还没有任何 tag,请给一个起始版本号(建议 \`v0.0.1\` / \`v0.1.0\` / \`v1.0.0\` 之一,按改动幅度选择)。`;
336
+ const prompt = `根据以下 commit message 和 git diff 推荐一个语义化版本 tag(破坏性变更 → 升 major;新增功能 → 升 minor;修复 / 文档 / 重构 / 维护 → 升 patch)。${tagHint}
337
+
338
+ 请用${lang}思考但严格输出**单行 JSON 对象**,不要 Markdown 代码块、不要任何解释文字、不要多余引号。格式:
339
+ {"tag":"v1.2.3"}
340
+
341
+ commit message:${commitMessage}
342
+
343
+ git diff:
344
+ ${diff}`;
345
+ const raw = await callClaudeText(prompt, cwd);
346
+ const parsed = tryParseJson(raw);
347
+ let suggested;
348
+ if (parsed && typeof parsed.tag === "string") {
349
+ suggested = sanitizeSuggestedTag(parsed.tag);
350
+ }
351
+ else {
352
+ suggested = sanitizeSuggestedTag(raw);
353
+ }
354
+ if (!suggested) {
355
+ throw new QuickCommitError("AI 没有给出合法的 tag,请手动填写。", "INVALID_AI_TAG");
356
+ }
357
+ return suggested;
252
358
  }
253
359
  // ── Direct git operations ──
254
360
  export async function runQuickCommit(opts) {
@@ -310,29 +416,19 @@ export async function runQuickCommit(opts) {
310
416
  commitHash = "";
311
417
  }
312
418
  // Step 5: tag
313
- const makeTag = !!(autoTag || (tag && tag.trim()));
314
- let tagName = "";
315
- if (makeTag) {
316
- if (tag && tag.trim()) {
317
- tagName = tag.trim();
318
- }
319
- else {
320
- let latestTag;
321
- try {
322
- latestTag = runGit(["describe", "--tags", "--abbrev=0"], cwd);
323
- }
324
- catch {
325
- latestTag = undefined;
326
- }
327
- tagName = bumpPatchTag(latestTag || "v0.0.0");
419
+ // - explicit `tag` wins
420
+ // - if `tag` is empty and `autoTag` is on, ask Claude to generate one
421
+ // - otherwise no tag
422
+ let tagName = (tag || "").trim();
423
+ if (!tagName && autoTag) {
424
+ tagName = await generateTagAfterCommit(cwd, language, message);
425
+ }
426
+ if (tagName) {
427
+ try {
428
+ runGit(["tag", tagName], cwd);
328
429
  }
329
- if (tagName) {
330
- try {
331
- runGit(["tag", tagName], cwd);
332
- }
333
- catch (error) {
334
- throw new QuickCommitError(`git tag 失败:${getGitErrorMessage(error)}`, "GIT_TAG_FAILED");
335
- }
430
+ catch (error) {
431
+ throw new QuickCommitError(`git tag 失败:${getGitErrorMessage(error)}`, "GIT_TAG_FAILED");
336
432
  }
337
433
  }
338
434
  // Step 6: push
@@ -377,8 +377,8 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
377
377
  return;
378
378
  }
379
379
  try {
380
- const message = await generateCommitMessageOnly(snapshot.cwd, config.language ?? "");
381
- res.json({ message });
380
+ const result = await generateCommitMessageOnly(snapshot.cwd, config.language ?? "");
381
+ res.json(result);
382
382
  }
383
383
  catch (error) {
384
384
  if (error instanceof QuickCommitError) {
package/dist/types.d.ts CHANGED
@@ -169,10 +169,6 @@ export interface GitStatusResult {
169
169
  repoRoot?: string;
170
170
  /** Truthy when the repo has no commits yet (initial state). */
171
171
  initialCommit?: boolean;
172
- /** Most recent reachable tag (e.g. "v1.2.3"). */
173
- latestTag?: string;
174
- /** Auto-suggested next tag derived by bumping the patch segment. */
175
- suggestedNextTag?: string;
176
172
  error?: string;
177
173
  }
178
174
  export interface QuickCommitResult {
@@ -1735,8 +1735,11 @@
1735
1735
 
1736
1736
  function generateCommitMessageAI() {
1737
1737
  if (!state.selectedId || state.quickCommitGenerating) return;
1738
+ // Sync any in-flight DOM input back into state so "empty?" checks read the latest value
1738
1739
  var msgEl = document.getElementById("quick-commit-message");
1739
1740
  if (msgEl) state.quickCommitForm.customMessage = msgEl.value;
1741
+ var tagEl = document.getElementById("quick-commit-tag");
1742
+ if (tagEl) state.quickCommitForm.tag = tagEl.value;
1740
1743
  state.quickCommitGenerating = true;
1741
1744
  state.quickCommitError = "";
1742
1745
  rerenderQuickCommitModal();
@@ -1751,9 +1754,19 @@
1751
1754
  })
1752
1755
  .then(function(result) {
1753
1756
  if (!result.ok) throw new Error((result.data && result.data.error) || "AI 生成失败。");
1754
- state.quickCommitForm.customMessage = (result.data && result.data.message) || "";
1755
- var currentMsgEl = document.getElementById("quick-commit-message");
1756
- if (currentMsgEl) currentMsgEl.value = state.quickCommitForm.customMessage;
1757
+ var data = result.data || {};
1758
+ var aiMessage = typeof data.message === "string" ? data.message : "";
1759
+ var aiTag = typeof data.suggestedTag === "string" ? data.suggestedTag.trim() : "";
1760
+ // Only fill fields that are currently empty; never overwrite user input.
1761
+ var currentMessage = (state.quickCommitForm.customMessage || "").trim();
1762
+ if (!currentMessage && aiMessage) {
1763
+ state.quickCommitForm.customMessage = aiMessage;
1764
+ }
1765
+ var currentTag = (state.quickCommitForm.tag || "").trim();
1766
+ if (!currentTag && aiTag) {
1767
+ state.quickCommitForm.tag = aiTag;
1768
+ state.quickCommitForm.makeTag = true;
1769
+ }
1757
1770
  })
1758
1771
  .catch(function(error) {
1759
1772
  state.quickCommitError = (error && error.message) || "AI 生成失败。";
@@ -1768,21 +1781,24 @@
1768
1781
  if (!state.selectedId || state.quickCommitSubmitting) return;
1769
1782
  var msgEl = document.getElementById("quick-commit-message");
1770
1783
  if (msgEl) state.quickCommitForm.customMessage = msgEl.value;
1784
+ var tagEl = document.getElementById("quick-commit-tag");
1785
+ if (tagEl) state.quickCommitForm.tag = tagEl.value;
1771
1786
  var form = state.quickCommitForm || {};
1772
1787
  var userTag = form.makeTag ? (form.tag || "").trim() : "";
1773
1788
  var message = (form.customMessage || "").trim();
1789
+ if (!message) {
1790
+ state.quickCommitError = "请填写 commit message,或点击 AI 生成。";
1791
+ rerenderQuickCommitModal();
1792
+ return;
1793
+ }
1794
+ // 开了 tag 开关但没填 → 由后端在提交时调 AI 生成
1774
1795
  var payload = {
1775
1796
  autoMessage: false,
1776
1797
  customMessage: message,
1777
1798
  tag: userTag,
1778
- autoTag: form.makeTag && !userTag,
1799
+ autoTag: !!(form.makeTag && !userTag),
1779
1800
  push: !!form.push
1780
1801
  };
1781
- if (!message) {
1782
- state.quickCommitError = "请填写 commit message,或点击 AI 生成。";
1783
- rerenderQuickCommitModal();
1784
- return;
1785
- }
1786
1802
  state.quickCommitSubmitting = true;
1787
1803
  state.quickCommitError = "";
1788
1804
  rerenderQuickCommitModal();
@@ -1869,14 +1885,14 @@
1869
1885
  '<textarea id="quick-commit-message" class="field-input" rows="2" placeholder="输入 commit message 或点击 AI 生成">' + escapeHtml(f.customMessage || "") + '</textarea>' +
1870
1886
  '</div>' +
1871
1887
  '<div class="qc-checkbox-row">' +
1872
- '<label class="qc-checkbox-label" for="quick-commit-make-tag">提交后打 tag' + (s.latestTag ? '(当前:' + escapeHtml(s.latestTag) + ')' : '') + '</label>' +
1888
+ '<label class="qc-checkbox-label" for="quick-commit-make-tag">提交后打 tag</label>' +
1873
1889
  '<label class="qc-switch">' +
1874
1890
  '<input type="checkbox" id="quick-commit-make-tag" class="switch-toggle"' + (f.makeTag ? ' checked' : '') + '>' +
1875
1891
  '<span class="switch-slider"></span>' +
1876
1892
  '</label>' +
1877
1893
  '</div>' +
1878
1894
  '<div class="qc-tag-row' + (f.makeTag ? '' : ' hidden') + '" id="quick-commit-tag-row">' +
1879
- '<input type="text" id="quick-commit-tag" class="field-input" placeholder="留空自动 bump patch' + (s.suggestedNextTag ? '(如 ' + escapeHtml(s.suggestedNextTag) + ')' : '') + '" value="' + escapeHtml(f.tag || "") + '">' +
1895
+ '<input type="text" id="quick-commit-tag" class="field-input" placeholder="输入 tag 名称;留空将由 AI 在提交时自动生成" value="' + escapeHtml(f.tag || "") + '">' +
1880
1896
  '</div>' +
1881
1897
  '<div class="qc-checkbox-row">' +
1882
1898
  '<label class="qc-checkbox-label" for="quick-commit-push">提交后 push 到远端</label>' +
@@ -2643,8 +2659,13 @@
2643
2659
  '</label>';
2644
2660
  }
2645
2661
 
2646
- function confirmDelete(message) {
2647
- return window.confirm(message);
2662
+ // Returns a Promise<boolean>. Uses the Liquid Glass styled wandConfirm
2663
+ // when available, falls back to native confirm during early page boot.
2664
+ function confirmDelete(message, options) {
2665
+ if (typeof window.wandConfirm === "function") {
2666
+ return window.wandConfirm(message, Object.assign({ type: "danger", danger: true, okLabel: "删除" }, options || {}));
2667
+ }
2668
+ return Promise.resolve(window.confirm(message));
2648
2669
  }
2649
2670
 
2650
2671
  function batchDeleteSelected() {
@@ -2652,44 +2673,46 @@
2652
2673
  var historyIds = getSelectedClaudeHistoryIds();
2653
2674
  var total = sessionIds.length + historyIds.length;
2654
2675
  if (!total) return;
2655
- if (!confirmDelete('确认删除所选 ' + total + ' 项吗?')) {
2656
- return;
2657
- }
2658
-
2659
- var requests = [];
2660
- if (sessionIds.length > 0) {
2661
- requests.push(fetch('/api/sessions/batch-delete', {
2662
- method: 'POST',
2663
- headers: { 'Content-Type': 'application/json' },
2664
- credentials: 'same-origin',
2665
- body: JSON.stringify({ sessionIds: sessionIds })
2666
- }).then(function(res) { return res.json(); }));
2667
- }
2668
- if (historyIds.length > 0) {
2669
- requests.push(fetch('/api/claude-history/batch-delete', {
2670
- method: 'POST',
2671
- headers: { 'Content-Type': 'application/json' },
2672
- credentials: 'same-origin',
2673
- body: JSON.stringify({ claudeSessionIds: historyIds })
2674
- }).then(function(res) { return res.json(); }));
2675
- }
2676
-
2677
- Promise.all(requests)
2678
- .then(function() {
2679
- if (sessionIds.indexOf(state.selectedId) !== -1) {
2680
- state.selectedId = null;
2681
- persistSelectedId();
2682
- }
2683
- state.claudeHistory = state.claudeHistory.filter(function(session) {
2684
- return historyIds.indexOf(session.claudeSessionId) === -1;
2676
+ confirmDelete('确认删除所选 ' + total + ' 项吗?此操作无法撤销。', {
2677
+ title: "删除所选 " + total + " 项",
2678
+ }).then(function(ok) {
2679
+ if (!ok) return;
2680
+
2681
+ var requests = [];
2682
+ if (sessionIds.length > 0) {
2683
+ requests.push(fetch('/api/sessions/batch-delete', {
2684
+ method: 'POST',
2685
+ headers: { 'Content-Type': 'application/json' },
2686
+ credentials: 'same-origin',
2687
+ body: JSON.stringify({ sessionIds: sessionIds })
2688
+ }).then(function(res) { return res.json(); }));
2689
+ }
2690
+ if (historyIds.length > 0) {
2691
+ requests.push(fetch('/api/claude-history/batch-delete', {
2692
+ method: 'POST',
2693
+ headers: { 'Content-Type': 'application/json' },
2694
+ credentials: 'same-origin',
2695
+ body: JSON.stringify({ claudeSessionIds: historyIds })
2696
+ }).then(function(res) { return res.json(); }));
2697
+ }
2698
+
2699
+ Promise.all(requests)
2700
+ .then(function() {
2701
+ if (sessionIds.indexOf(state.selectedId) !== -1) {
2702
+ state.selectedId = null;
2703
+ persistSelectedId();
2704
+ }
2705
+ state.claudeHistory = state.claudeHistory.filter(function(session) {
2706
+ return historyIds.indexOf(session.claudeSessionId) === -1;
2707
+ });
2708
+ clearManageSelections();
2709
+ return refreshAll();
2710
+ })
2711
+ .catch(function() {
2712
+ var errorEl = document.getElementById('action-error');
2713
+ showError(errorEl, '无法批量删除所选项目。');
2685
2714
  });
2686
- clearManageSelections();
2687
- return refreshAll();
2688
- })
2689
- .catch(function() {
2690
- var errorEl = document.getElementById('action-error');
2691
- showError(errorEl, '无法批量删除所选项目。');
2692
- });
2715
+ });
2693
2716
  }
2694
2717
 
2695
2718
  function clearAllClaudeHistory() {
@@ -2698,11 +2721,13 @@
2698
2721
  return !s.timestamp || new Date(s.timestamp).getTime() <= cutoff;
2699
2722
  });
2700
2723
  if (!visibleHistory.length) return;
2701
- if (!confirmDelete('确认清空当前显示的 ' + visibleHistory.length + ' 条 Claude 历史吗?')) {
2702
- return;
2703
- }
2704
- var deleteIds = visibleHistory.map(function(session) { return session.claudeSessionId; });
2705
- return fetch('/api/claude-history/batch-delete', {
2724
+ return confirmDelete('确认清空当前显示的 ' + visibleHistory.length + ' 条 Claude 历史吗?', {
2725
+ title: "清空 Claude 历史",
2726
+ okLabel: "清空",
2727
+ }).then(function(ok) {
2728
+ if (!ok) return;
2729
+ var deleteIds = visibleHistory.map(function(session) { return session.claudeSessionId; });
2730
+ return fetch('/api/claude-history/batch-delete', {
2706
2731
  method: 'POST',
2707
2732
  headers: { 'Content-Type': 'application/json' },
2708
2733
  credentials: 'same-origin',
@@ -2723,6 +2748,7 @@
2723
2748
  var errorEl = document.getElementById('action-error');
2724
2749
  showError(errorEl, '无法清空历史会话。');
2725
2750
  });
2751
+ });
2726
2752
  }
2727
2753
 
2728
2754
  function renderClaudeHistoryDirectoryHeader(cwd, cwdShort, count, isExpanded) {
@@ -4989,10 +5015,10 @@
4989
5015
  break;
4990
5016
  case "delete-session":
4991
5017
  if (state.selectedId) {
4992
- var pendingId = state.selectedId;
4993
- if (confirm("确定要删除当前会话吗?")) {
4994
- deleteSession(pendingId);
4995
- }
5018
+ (function(pendingId) {
5019
+ confirmDelete("确定要删除当前会话吗?此操作无法撤销。", { title: "删除当前会话" })
5020
+ .then(function(ok) { if (ok) deleteSession(pendingId); });
5021
+ })(state.selectedId);
4996
5022
  }
4997
5023
  break;
4998
5024
  }
@@ -5526,25 +5552,34 @@
5526
5552
  } else if (actionButton.dataset.action === "toggle-selection") {
5527
5553
  toggleManagedItemSelection(actionButton.dataset.kind, actionButton.dataset.id);
5528
5554
  } else if (actionButton.dataset.action === "delete-session" && actionButton.dataset.sessionId) {
5529
- if (confirmDelete("确认删除这个会话吗?")) {
5530
- deleteSession(actionButton.dataset.sessionId);
5531
- }
5555
+ (function(sid) {
5556
+ confirmDelete("确认删除这个会话吗?此操作无法撤销。", { title: "删除会话" }).then(function(ok) {
5557
+ if (ok) deleteSession(sid);
5558
+ });
5559
+ })(actionButton.dataset.sessionId);
5532
5560
  } else if (actionButton.dataset.action === "delete-history" && actionButton.dataset.claudeSessionId) {
5533
- if (confirmDelete("确认隐藏这条 Claude 历史吗?")) {
5534
- executeDeleteHistory(actionButton.dataset.claudeSessionId, actionButton.closest(".session-item"));
5535
- }
5561
+ (function(cid, item) {
5562
+ confirmDelete("确认隐藏这条 Claude 历史吗?", { title: "隐藏历史会话", okLabel: "隐藏" }).then(function(ok) {
5563
+ if (ok) executeDeleteHistory(cid, item);
5564
+ });
5565
+ })(actionButton.dataset.claudeSessionId, actionButton.closest(".session-item"));
5536
5566
  } else if (actionButton.dataset.action === "toggle-history-directory" && actionButton.dataset.cwd) {
5537
5567
  var dirCwd = actionButton.dataset.cwd;
5538
5568
  state.claudeHistoryExpandedDirs[dirCwd] = !state.claudeHistoryExpandedDirs[dirCwd];
5539
5569
  updateSessionsList();
5540
5570
  } else if (actionButton.dataset.action === "delete-history-directory" && actionButton.dataset.cwd) {
5541
- var deleteCwd = actionButton.dataset.cwd;
5542
- var items = getHistoryItemsByCwd(deleteCwd);
5543
- var dirCount = getVisibleClaudeHistorySessions().filter(function(s) { return s.cwd === deleteCwd; }).length;
5544
- if (confirmDelete("确认清空此目录下的 " + dirCount + " 条 Claude 历史吗?")) {
5545
- setDeletingState(items, true);
5546
- deleteClaudeHistoryDirectory(deleteCwd, actionButton, items);
5547
- }
5571
+ (function(deleteCwd, btn) {
5572
+ var items = getHistoryItemsByCwd(deleteCwd);
5573
+ var dirCount = getVisibleClaudeHistorySessions().filter(function(s) { return s.cwd === deleteCwd; }).length;
5574
+ confirmDelete("确认清空此目录下的 " + dirCount + " 条 Claude 历史吗?", {
5575
+ title: "清空目录历史",
5576
+ okLabel: "清空",
5577
+ }).then(function(ok) {
5578
+ if (!ok) return;
5579
+ setDeletingState(items, true);
5580
+ deleteClaudeHistoryDirectory(deleteCwd, btn, items);
5581
+ });
5582
+ })(actionButton.dataset.cwd, actionButton);
5548
5583
  } else if (actionButton.dataset.action === "clear-all-history") {
5549
5584
  clearAllClaudeHistory();
5550
5585
  } else if (actionButton.dataset.action === "toggle-archived-group") {
@@ -8257,7 +8292,13 @@
8257
8292
  try {
8258
8293
  WandNative.downloadUpdate(androidApk.github.downloadUrl, androidApk.github.fileName || "wand-update.apk", "github");
8259
8294
  } catch (e) {
8260
- alert("调用下载失败: " + e.message);
8295
+ if (typeof window.wandAlert === "function") {
8296
+ window.wandAlert("调用下载失败: " + e.message, { type: "danger", title: "下载失败" });
8297
+ } else if (typeof showToast === "function") {
8298
+ showToast("调用下载失败: " + e.message, "error");
8299
+ } else {
8300
+ alert("调用下载失败: " + e.message);
8301
+ }
8261
8302
  }
8262
8303
  };
8263
8304
  }
@@ -8275,7 +8316,13 @@
8275
8316
  try {
8276
8317
  WandNative.downloadUpdate(androidApk.local.downloadUrl, androidApk.local.fileName || "wand-update.apk", "local");
8277
8318
  } catch (e) {
8278
- alert("调用下载失败: " + e.message);
8319
+ if (typeof window.wandAlert === "function") {
8320
+ window.wandAlert("调用下载失败: " + e.message, { type: "danger", title: "下载失败" });
8321
+ } else if (typeof showToast === "function") {
8322
+ showToast("调用下载失败: " + e.message, "error");
8323
+ } else {
8324
+ alert("调用下载失败: " + e.message);
8325
+ }
8279
8326
  }
8280
8327
  };
8281
8328
  }
@@ -14069,9 +14116,10 @@
14069
14116
  }
14070
14117
  }
14071
14118
 
14072
- // 显示当前执行步骤 = 已完成 + 正在进行(如果有)
14073
- var currentStep = completed + inProgress;
14119
+ // 显示"正在干第 N 个"(1-indexed):已完成 + 1,封顶到总数
14120
+ // 这样 pending → in_progress → completed 的过渡空档不会出现 0/N 或漏一位的现象
14074
14121
  var allDone = completed === todos.length;
14122
+ var currentStep = allDone ? todos.length : Math.min(completed + 1, todos.length);
14075
14123
  if (allDone) {
14076
14124
  // Hide todo when all tasks are completed
14077
14125
  container.classList.add("hidden");
@@ -16161,6 +16209,274 @@
16161
16209
  }, type === "error" ? 4000 : 2200);
16162
16210
  }
16163
16211
 
16212
+ // ── Wand Dialog (alert / confirm / prompt) ──
16213
+ // Replaces window.alert / window.confirm / window.prompt with a Liquid Glass
16214
+ // styled modal so confirmations and notices match the rest of wand's UI.
16215
+
16216
+ var _wandDialogStack = [];
16217
+
16218
+ function _wandDialogIcon(type) {
16219
+ switch (type) {
16220
+ case "warning": return "!";
16221
+ case "danger": return "⚠︎"; // ⚠ (text style)
16222
+ case "success": return "✓"; // ✓
16223
+ case "question": return "?";
16224
+ default: return "i";
16225
+ }
16226
+ }
16227
+
16228
+ /**
16229
+ * Open a Liquid-Glass styled dialog. Returns a Promise resolving to the
16230
+ * value returned by the clicked button's `value`, or `cancelValue` if
16231
+ * dismissed via Esc / backdrop click / cancel button.
16232
+ *
16233
+ * @param {object} opts
16234
+ * @param {string} [opts.title]
16235
+ * @param {string} [opts.message]
16236
+ * @param {"info"|"warning"|"danger"|"success"|"question"} [opts.type]
16237
+ * @param {string} [opts.icon] - Override icon glyph.
16238
+ * @param {Array<{label:string, value:any, kind?:"primary"|"secondary"|"ghost"|"outline"|"danger", autofocus?:boolean}>} opts.buttons
16239
+ * @param {boolean} [opts.input] - Show a single-line text input (prompt).
16240
+ * @param {string} [opts.inputValue] - Initial text input value.
16241
+ * @param {string} [opts.inputPlaceholder]
16242
+ * @param {any} [opts.cancelValue] - Value resolved on Esc / backdrop click. Default: false for confirm, null for prompt, undefined for alert.
16243
+ * @param {boolean} [opts.dismissable] - Allow Esc / backdrop close (default true).
16244
+ * @returns {Promise<any>}
16245
+ */
16246
+ function openWandDialog(opts) {
16247
+ opts = opts || {};
16248
+ var dismissable = opts.dismissable !== false;
16249
+ var type = opts.type || "info";
16250
+ var iconChar = opts.icon || _wandDialogIcon(type);
16251
+ var hasInput = !!opts.input;
16252
+
16253
+ return new Promise(function(resolve) {
16254
+ var backdrop = document.createElement("div");
16255
+ backdrop.className = "wand-dialog-backdrop";
16256
+ backdrop.setAttribute("role", "presentation");
16257
+
16258
+ var dialog = document.createElement("div");
16259
+ dialog.className = "wand-dialog";
16260
+ dialog.setAttribute("role", hasInput ? "dialog" : "alertdialog");
16261
+ dialog.setAttribute("aria-modal", "true");
16262
+ if (opts.title) dialog.setAttribute("aria-label", opts.title);
16263
+
16264
+ // Header
16265
+ var header = document.createElement("div");
16266
+ header.className = "wand-dialog-header";
16267
+
16268
+ var iconEl = document.createElement("div");
16269
+ iconEl.className = "wand-dialog-icon " + type;
16270
+ iconEl.textContent = iconChar;
16271
+ header.appendChild(iconEl);
16272
+
16273
+ var textWrap = document.createElement("div");
16274
+ textWrap.className = "wand-dialog-textwrap";
16275
+ if (opts.title) {
16276
+ var titleEl = document.createElement("div");
16277
+ titleEl.className = "wand-dialog-title";
16278
+ titleEl.textContent = opts.title;
16279
+ textWrap.appendChild(titleEl);
16280
+ }
16281
+ if (opts.message) {
16282
+ var msgEl = document.createElement("div");
16283
+ msgEl.className = "wand-dialog-message";
16284
+ msgEl.textContent = opts.message;
16285
+ textWrap.appendChild(msgEl);
16286
+ }
16287
+ header.appendChild(textWrap);
16288
+ dialog.appendChild(header);
16289
+
16290
+ // Optional input (prompt mode)
16291
+ var inputEl = null;
16292
+ if (hasInput) {
16293
+ var bodyEl = document.createElement("div");
16294
+ bodyEl.className = "wand-dialog-body";
16295
+ inputEl = document.createElement("input");
16296
+ inputEl.type = "text";
16297
+ inputEl.className = "wand-dialog-input";
16298
+ if (opts.inputPlaceholder) inputEl.placeholder = opts.inputPlaceholder;
16299
+ if (opts.inputValue != null) inputEl.value = String(opts.inputValue);
16300
+ bodyEl.appendChild(inputEl);
16301
+ dialog.appendChild(bodyEl);
16302
+ }
16303
+
16304
+ // Footer / buttons
16305
+ var buttons = (opts.buttons && opts.buttons.length) ? opts.buttons : [
16306
+ { label: "好", value: true, kind: "primary", autofocus: true },
16307
+ ];
16308
+
16309
+ var footer = document.createElement("div");
16310
+ footer.className = "wand-dialog-footer";
16311
+
16312
+ var firstFocusable = null;
16313
+ var autofocusTarget = null;
16314
+
16315
+ function close(value) {
16316
+ if (backdrop.classList.contains("closing")) return;
16317
+ backdrop.classList.add("closing");
16318
+ document.removeEventListener("keydown", keyHandler, true);
16319
+ var idx = _wandDialogStack.indexOf(close);
16320
+ if (idx >= 0) _wandDialogStack.splice(idx, 1);
16321
+ setTimeout(function() {
16322
+ if (backdrop.parentNode) backdrop.parentNode.removeChild(backdrop);
16323
+ resolve(value);
16324
+ }, 160);
16325
+ }
16326
+
16327
+ buttons.forEach(function(btnSpec) {
16328
+ var btn = document.createElement("button");
16329
+ var kind = btnSpec.kind || "secondary";
16330
+ btn.className = "btn btn-" + kind;
16331
+ btn.type = "button";
16332
+ btn.textContent = btnSpec.label;
16333
+ btn.addEventListener("click", function() {
16334
+ if (hasInput && btnSpec.kind === "primary") {
16335
+ close(inputEl ? inputEl.value : "");
16336
+ } else {
16337
+ close(btnSpec.value);
16338
+ }
16339
+ });
16340
+ if (!firstFocusable) firstFocusable = btn;
16341
+ if (btnSpec.autofocus) autofocusTarget = btn;
16342
+ footer.appendChild(btn);
16343
+ });
16344
+ dialog.appendChild(footer);
16345
+
16346
+ backdrop.appendChild(dialog);
16347
+
16348
+ // Backdrop click → cancel (if dismissable)
16349
+ backdrop.addEventListener("click", function(e) {
16350
+ if (e.target === backdrop && dismissable) {
16351
+ close(opts.cancelValue !== undefined ? opts.cancelValue : (hasInput ? null : false));
16352
+ }
16353
+ });
16354
+
16355
+ // Key handler — Esc cancels, Enter triggers primary (when not in textarea-like input)
16356
+ function keyHandler(e) {
16357
+ // Only handle keys for the topmost dialog
16358
+ if (_wandDialogStack[_wandDialogStack.length - 1] !== close) return;
16359
+ if (e.key === "Escape" && dismissable) {
16360
+ e.preventDefault();
16361
+ e.stopPropagation();
16362
+ close(opts.cancelValue !== undefined ? opts.cancelValue : (hasInput ? null : false));
16363
+ return;
16364
+ }
16365
+ if (e.key === "Enter") {
16366
+ // For prompt: Enter on input → submit. For alert/confirm: Enter → primary action.
16367
+ var primary = null;
16368
+ for (var i = 0; i < buttons.length; i++) {
16369
+ if (buttons[i].kind === "primary") {
16370
+ primary = buttons[i];
16371
+ break;
16372
+ }
16373
+ }
16374
+ if (!primary) primary = buttons[buttons.length - 1];
16375
+ if (primary) {
16376
+ e.preventDefault();
16377
+ e.stopPropagation();
16378
+ if (hasInput && primary.kind === "primary") {
16379
+ close(inputEl ? inputEl.value : "");
16380
+ } else {
16381
+ close(primary.value);
16382
+ }
16383
+ }
16384
+ }
16385
+ }
16386
+ document.addEventListener("keydown", keyHandler, true);
16387
+ _wandDialogStack.push(close);
16388
+
16389
+ document.body.appendChild(backdrop);
16390
+
16391
+ // Focus the most appropriate target
16392
+ requestAnimationFrame(function() {
16393
+ if (hasInput && inputEl) {
16394
+ inputEl.focus();
16395
+ try { inputEl.select(); } catch (e2) {}
16396
+ } else if (autofocusTarget) {
16397
+ autofocusTarget.focus();
16398
+ } else if (firstFocusable) {
16399
+ firstFocusable.focus();
16400
+ }
16401
+ });
16402
+ });
16403
+ }
16404
+
16405
+ /**
16406
+ * Liquid-glass replacement for window.alert.
16407
+ * @param {string} message
16408
+ * @param {object} [options] - { title, type, okLabel }
16409
+ * @returns {Promise<void>}
16410
+ */
16411
+ function wandAlert(message, options) {
16412
+ options = options || {};
16413
+ return openWandDialog({
16414
+ title: options.title || "提示",
16415
+ message: typeof message === "string" ? message : String(message == null ? "" : message),
16416
+ type: options.type || "info",
16417
+ buttons: [
16418
+ { label: options.okLabel || "好", value: undefined, kind: "primary", autofocus: true },
16419
+ ],
16420
+ });
16421
+ }
16422
+
16423
+ /**
16424
+ * Liquid-glass replacement for window.confirm. Resolves to true/false.
16425
+ * @param {string} message
16426
+ * @param {object} [options] - { title, type ("question"|"warning"|"danger"), okLabel, cancelLabel, danger }
16427
+ * @returns {Promise<boolean>}
16428
+ */
16429
+ function wandConfirm(message, options) {
16430
+ options = options || {};
16431
+ var danger = !!options.danger || options.type === "danger";
16432
+ return openWandDialog({
16433
+ title: options.title || (danger ? "确认操作" : "请确认"),
16434
+ message: typeof message === "string" ? message : String(message == null ? "" : message),
16435
+ type: options.type || (danger ? "danger" : "question"),
16436
+ cancelValue: false,
16437
+ buttons: [
16438
+ { label: options.cancelLabel || "取消", value: false, kind: "secondary" },
16439
+ {
16440
+ label: options.okLabel || (danger ? "删除" : "确定"),
16441
+ value: true,
16442
+ kind: danger ? "danger" : "primary",
16443
+ autofocus: !danger,
16444
+ },
16445
+ ],
16446
+ }).then(function(v) { return v === true; });
16447
+ }
16448
+
16449
+ /**
16450
+ * Liquid-glass replacement for window.prompt. Resolves to entered string,
16451
+ * or null if cancelled.
16452
+ * @param {string} message
16453
+ * @param {string} [defaultValue]
16454
+ * @param {object} [options] - { title, placeholder, okLabel, cancelLabel }
16455
+ * @returns {Promise<string|null>}
16456
+ */
16457
+ function wandPrompt(message, defaultValue, options) {
16458
+ options = options || {};
16459
+ return openWandDialog({
16460
+ title: options.title || "请输入",
16461
+ message: typeof message === "string" ? message : String(message == null ? "" : message),
16462
+ type: options.type || "question",
16463
+ input: true,
16464
+ inputValue: defaultValue == null ? "" : String(defaultValue),
16465
+ inputPlaceholder: options.placeholder || "",
16466
+ cancelValue: null,
16467
+ buttons: [
16468
+ { label: options.cancelLabel || "取消", value: null, kind: "secondary" },
16469
+ { label: options.okLabel || "确定", value: undefined, kind: "primary" },
16470
+ ],
16471
+ });
16472
+ }
16473
+
16474
+ // Expose globally for ad-hoc use from inline handlers / future code
16475
+ window.wandAlert = wandAlert;
16476
+ window.wandConfirm = wandConfirm;
16477
+ window.wandPrompt = wandPrompt;
16478
+ window.openWandDialog = openWandDialog;
16479
+
16164
16480
  // ── Notification Bubble System ──
16165
16481
 
16166
16482
  var notificationStack = [];
@@ -16448,7 +16764,7 @@
16448
16764
  // ── Native Live Progress Sync ──
16449
16765
 
16450
16766
  var _progressSyncTimers = {};
16451
- var _PROGRESS_SYNC_DEBOUNCE_MS = 100;
16767
+ var _PROGRESS_SYNC_DEBOUNCE_MS = 30;
16452
16768
 
16453
16769
  // Strip markdown formatting and clamp to a single short line so the
16454
16770
  // native Live Activity / lock-screen card stays readable. 100 chars
@@ -9050,21 +9050,22 @@
9050
9050
  }
9051
9051
  .toast-message {
9052
9052
  position: fixed;
9053
- bottom: 24px;
9053
+ top: 24px;
9054
9054
  left: 50%;
9055
9055
  transform: translateX(-50%);
9056
9056
  padding: 10px 20px;
9057
9057
  border-radius: var(--radius-md);
9058
9058
  font-size: 0.875rem;
9059
9059
  z-index: 9999;
9060
- animation: toast-in 0.2s ease;
9060
+ box-shadow: var(--shadow-lg);
9061
+ animation: toast-in 0.22s cubic-bezier(0.34, 1.56, 0.64, 1);
9061
9062
  }
9062
9063
  .toast-error {
9063
9064
  background: var(--danger);
9064
9065
  color: white;
9065
9066
  }
9066
9067
  @keyframes toast-in {
9067
- from { opacity: 0; transform: translateX(-50%) translateY(8px); }
9068
+ from { opacity: 0; transform: translateX(-50%) translateY(-12px); }
9068
9069
  to { opacity: 1; transform: translateX(-50%) translateY(0); }
9069
9070
  }
9070
9071
 
@@ -10963,9 +10964,9 @@
10963
10964
  /* ── Mini toast for in-app feedback ── */
10964
10965
  .wand-mini-toast {
10965
10966
  position: fixed;
10966
- bottom: 30px;
10967
+ top: 30px;
10967
10968
  left: 50%;
10968
- transform: translate(-50%, 14px);
10969
+ transform: translate(-50%, -14px);
10969
10970
  z-index: 1200;
10970
10971
  padding: 8px 16px;
10971
10972
  background: rgba(20, 20, 20, 0.92);
@@ -12946,4 +12947,210 @@
12946
12947
  .qc-files-wrap { max-height: 160px; }
12947
12948
  }
12948
12949
 
12950
+ /* ============================================================ */
12951
+ /* ── Wand Dialog (alert / confirm / prompt) ── */
12952
+ /* Replaces window.alert / window.confirm / window.prompt with */
12953
+ /* a Liquid Glass styled modal that matches the rest of wand. */
12954
+ /* ============================================================ */
12955
+
12956
+ .wand-dialog-backdrop {
12957
+ position: fixed;
12958
+ inset: 0;
12959
+ z-index: 9000;
12960
+ display: flex;
12961
+ align-items: center;
12962
+ justify-content: center;
12963
+ padding: 24px;
12964
+ background: rgba(20, 14, 8, 0.34);
12965
+ backdrop-filter: blur(18px) saturate(140%);
12966
+ -webkit-backdrop-filter: blur(18px) saturate(140%);
12967
+ animation: wandDialogBackdropIn 0.22s cubic-bezier(0.16, 1, 0.3, 1) both;
12968
+ }
12969
+ .wand-dialog-backdrop.closing {
12970
+ animation: wandDialogBackdropOut 0.16s cubic-bezier(0.4, 0, 0.2, 1) both;
12971
+ pointer-events: none;
12972
+ }
12973
+ @keyframes wandDialogBackdropIn { from { opacity: 0; } to { opacity: 1; } }
12974
+ @keyframes wandDialogBackdropOut { from { opacity: 1; } to { opacity: 0; } }
12975
+
12976
+ .wand-dialog {
12977
+ position: relative;
12978
+ width: 100%;
12979
+ max-width: 420px;
12980
+ display: flex;
12981
+ flex-direction: column;
12982
+ background:
12983
+ radial-gradient(120% 120% at 0% 0%, var(--accent-muted) 0%, transparent 55%),
12984
+ linear-gradient(180deg, var(--bg-elevated) 0%, var(--bg-secondary) 100%);
12985
+ border: 0.5px solid var(--border-subtle);
12986
+ border-radius: 22px;
12987
+ overflow: hidden;
12988
+ backdrop-filter: blur(40px) saturate(180%);
12989
+ -webkit-backdrop-filter: blur(40px) saturate(180%);
12990
+ box-shadow:
12991
+ 0 24px 64px -12px rgba(89, 58, 32, 0.28),
12992
+ 0 6px 20px -6px rgba(89, 58, 32, 0.16),
12993
+ 0 0 0 0.5px rgba(125, 91, 57, 0.08),
12994
+ inset 0 1px 0 rgba(255, 255, 255, 0.7);
12995
+ animation: wandDialogIn 0.28s cubic-bezier(0.34, 1.4, 0.64, 1) both;
12996
+ }
12997
+ .wand-dialog-backdrop.closing .wand-dialog {
12998
+ animation: wandDialogOut 0.16s cubic-bezier(0.4, 0, 0.2, 1) both;
12999
+ }
13000
+ @keyframes wandDialogIn {
13001
+ from { opacity: 0; transform: scale(0.92) translateY(10px); }
13002
+ to { opacity: 1; transform: scale(1) translateY(0); }
13003
+ }
13004
+ @keyframes wandDialogOut {
13005
+ from { opacity: 1; transform: scale(1); }
13006
+ to { opacity: 0; transform: scale(0.96) translateY(4px); }
13007
+ }
13008
+
13009
+ .wand-dialog-header {
13010
+ display: flex;
13011
+ align-items: flex-start;
13012
+ gap: 14px;
13013
+ padding: 22px 24px 12px;
13014
+ }
13015
+ .wand-dialog-icon {
13016
+ flex-shrink: 0;
13017
+ width: 40px;
13018
+ height: 40px;
13019
+ border-radius: 50%;
13020
+ display: inline-flex;
13021
+ align-items: center;
13022
+ justify-content: center;
13023
+ font-size: 20px;
13024
+ font-weight: 700;
13025
+ line-height: 1;
13026
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5);
13027
+ }
13028
+ .wand-dialog-icon.info {
13029
+ background: linear-gradient(180deg, var(--accent-soft) 0%, var(--accent-hover) 100%);
13030
+ color: #fff;
13031
+ }
13032
+ .wand-dialog-icon.warning {
13033
+ background: linear-gradient(180deg, #f0c891 0%, var(--warning) 100%);
13034
+ color: #fff;
13035
+ }
13036
+ .wand-dialog-icon.danger {
13037
+ background: linear-gradient(180deg, #d98178 0%, var(--danger) 100%);
13038
+ color: #fff;
13039
+ }
13040
+ .wand-dialog-icon.success {
13041
+ background: linear-gradient(180deg, #8caf94 0%, var(--success) 100%);
13042
+ color: #fff;
13043
+ }
13044
+ .wand-dialog-icon.question {
13045
+ background: linear-gradient(180deg, var(--accent-soft) 0%, var(--accent) 100%);
13046
+ color: #fff;
13047
+ }
13048
+ .wand-dialog-textwrap {
13049
+ flex: 1 1 auto;
13050
+ min-width: 0;
13051
+ display: flex;
13052
+ flex-direction: column;
13053
+ gap: 6px;
13054
+ padding-top: 4px;
13055
+ }
13056
+ .wand-dialog-title {
13057
+ font-size: 1.0625rem;
13058
+ font-weight: 700;
13059
+ letter-spacing: -0.01em;
13060
+ color: var(--text-primary);
13061
+ line-height: 1.3;
13062
+ word-break: break-word;
13063
+ }
13064
+ .wand-dialog-message {
13065
+ font-size: 0.9rem;
13066
+ font-weight: 400;
13067
+ color: var(--text-secondary);
13068
+ line-height: 1.55;
13069
+ white-space: pre-wrap;
13070
+ word-break: break-word;
13071
+ }
13072
+
13073
+ .wand-dialog-body {
13074
+ padding: 4px 24px 14px;
13075
+ }
13076
+ .wand-dialog-input {
13077
+ width: 100%;
13078
+ background: var(--bg-surface);
13079
+ border: 1px solid var(--border-subtle);
13080
+ border-radius: 12px;
13081
+ padding: 11px 14px;
13082
+ font-family: var(--font-sans);
13083
+ font-size: 0.9rem;
13084
+ color: var(--text-primary);
13085
+ box-shadow: inset 0 1px 1.5px rgba(125, 91, 57, 0.04);
13086
+ transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
13087
+ box-sizing: border-box;
13088
+ }
13089
+ .wand-dialog-input:focus {
13090
+ outline: none;
13091
+ background: var(--bg-elevated);
13092
+ border-color: var(--accent);
13093
+ box-shadow:
13094
+ inset 0 1px 1.5px rgba(125, 91, 57, 0.04),
13095
+ 0 0 0 3px var(--accent-glow);
13096
+ }
13097
+
13098
+ .wand-dialog-footer {
13099
+ display: flex;
13100
+ gap: 8px;
13101
+ justify-content: flex-end;
13102
+ padding: 14px 20px 20px;
13103
+ border-top: 1px solid var(--border-subtle);
13104
+ margin-top: 6px;
13105
+ }
13106
+ .wand-dialog-footer .btn {
13107
+ min-width: 84px;
13108
+ }
13109
+ /* Danger primary variant for destructive confirms */
13110
+ .wand-dialog .btn-danger {
13111
+ background: linear-gradient(180deg, var(--danger-hover) 0%, var(--danger) 100%);
13112
+ color: var(--text-inverse);
13113
+ border-color: transparent;
13114
+ box-shadow:
13115
+ 0 1px 2px var(--danger-muted),
13116
+ 0 4px 10px -2px var(--danger-muted),
13117
+ inset 0 1px 0 rgba(255, 255, 255, 0.22),
13118
+ inset 0 -1px 0 rgba(96, 30, 24, 0.18);
13119
+ }
13120
+ .wand-dialog .btn-danger:hover {
13121
+ background: linear-gradient(180deg, #c96055 0%, #b24f45 100%);
13122
+ transform: translateY(-1px);
13123
+ box-shadow:
13124
+ 0 2px 4px var(--danger-muted),
13125
+ 0 8px 20px -4px var(--danger-glow),
13126
+ inset 0 1px 0 rgba(255, 255, 255, 0.28),
13127
+ inset 0 -1px 0 rgba(96, 30, 24, 0.18);
13128
+ }
13129
+ .wand-dialog .btn-danger:active {
13130
+ transform: translateY(0);
13131
+ background: linear-gradient(180deg, var(--danger) 0%, #93392f 100%);
13132
+ transition-duration: 0.06s;
13133
+ }
13134
+
13135
+ @media (prefers-reduced-motion: reduce) {
13136
+ .wand-dialog-backdrop,
13137
+ .wand-dialog-backdrop.closing,
13138
+ .wand-dialog,
13139
+ .wand-dialog-backdrop.closing .wand-dialog {
13140
+ animation: none !important;
13141
+ }
13142
+ }
13143
+
13144
+ @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
13145
+ .wand-dialog-backdrop { background: rgba(20, 14, 8, 0.6); }
13146
+ .wand-dialog { background: var(--bg-elevated); border-color: var(--border-subtle); }
13147
+ }
13148
+
13149
+ @media (max-width: 480px) {
13150
+ .wand-dialog-backdrop { padding: 16px; align-items: center; }
13151
+ .wand-dialog { max-width: 100%; border-radius: 20px; }
13152
+ .wand-dialog-footer { flex-wrap: wrap; }
13153
+ .wand-dialog-footer .btn { flex: 1 1 auto; }
13154
+ }
13155
+
12949
13156
  /* 结束标记 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@co0ontty/wand",
3
- "version": "1.21.16",
3
+ "version": "1.21.18",
4
4
  "description": "A web terminal for local CLI tools like Claude.",
5
5
  "type": "module",
6
6
  "bin": {