@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.
- package/dist/git-quick-commit.d.ts +7 -1
- package/dist/git-quick-commit.js +143 -47
- package/dist/server-session-routes.js +2 -2
- package/dist/types.d.ts +0 -4
- package/dist/web-ui/content/scripts.js +393 -77
- package/dist/web-ui/content/styles.css +212 -5
- package/package.json +1 -1
|
@@ -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
|
|
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 {};
|
package/dist/git-quick-commit.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
330
|
-
|
|
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
|
|
381
|
-
res.json(
|
|
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
|
-
|
|
1755
|
-
var
|
|
1756
|
-
|
|
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
|
|
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="
|
|
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
|
-
|
|
2647
|
-
|
|
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
|
-
|
|
2656
|
-
|
|
2657
|
-
}
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
.
|
|
2679
|
-
|
|
2680
|
-
state.selectedId
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
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
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
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
|
-
|
|
5530
|
-
|
|
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
|
-
|
|
5534
|
-
|
|
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
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
/* 结束标记 */
|