@co0ontty/wand 1.36.0 → 1.39.0

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.
@@ -370,6 +370,10 @@
370
370
  sessionsManageMode: false,
371
371
  selectedSessionIds: {},
372
372
  selectedClaudeHistoryIds: {},
373
+ codexHistory: [],
374
+ codexHistoryLoaded: false,
375
+ codexHistoryExpandedDirs: {},
376
+ selectedCodexHistoryIds: {},
373
377
  askUserSelections: {}, // { toolUseId: { 0: [optIdx...], submitted: false } }
374
378
  queueEpoch: 0, // Monotonic counter for queue state freshness
375
379
  pendingAttachments: [], // [{ file, previewUrl, name, size }]
@@ -384,6 +388,81 @@
384
388
  })()
385
389
  };
386
390
 
391
+ // ── 前端 i18n(最小化)──
392
+ // 后端 config.language 是给 Claude 用的"回答语言"偏好("中文" / "English" / 任意字符串),
393
+ // 之前 frontend 完全没收 → UI label 一直 hardcoded 中文 + 个别英文("SUBAGENT" 那个 tag)。
394
+ // 用户设的是中文时,"SUBAGENT" 这类英文残留就和"配置语言不一致"。
395
+ //
396
+ // 设计取舍:
397
+ // - 只维护两套:中文(默认) + 英文。其它取值("日本語"、"Français"等)回退到英文,
398
+ // 因为 Claude 会按用户语言回答,UI 至少不卡在中文上让英语圈用户看不懂。
399
+ // - 不引入 i18n 库,几十个 key 用平铺对象,t(key, params) 是个十行 helper。
400
+ // - params 支持 "{name}" 占位符替换,避免在调用点拼字符串。
401
+ // - 缺 key 时回退到中文表,再没有就返回 key 本身(debug 友好)。
402
+ var I18N_DEFAULT_LANG = "中文";
403
+ var I18N = {
404
+ "中文": {
405
+ "subagent.tag": "子代理",
406
+ "subagent.handoff": "{parent} 让 {sub} 帮忙",
407
+ "subagent.handoff.with_desc": "{parent} 让 {sub} 帮忙:",
408
+ "subagent.continued": "继续输出",
409
+ "subagent.task.done": "任务完成",
410
+ "subagent.task.failed": "任务失败",
411
+ "subagent.running": "运行中",
412
+ "subagent.no_output": "(无输出)",
413
+ "subagent.helper_fallback_prefix": "协作猫·",
414
+ "subagent.title_aria": "点击展开 / 收起子代理输出",
415
+ "subagent.tag_title": "子代理 / subagent",
416
+ "ui.expand": "展开",
417
+ "ui.collapse": "收起",
418
+ "ui.expand_panel_aria": "展开子代理输出",
419
+ "ui.collapse_panel_aria": "收起子代理输出"
420
+ },
421
+ "English": {
422
+ "subagent.tag": "Subagent",
423
+ "subagent.handoff": "{parent} asked {sub} for help",
424
+ "subagent.handoff.with_desc": "{parent} asked {sub} for help with: ",
425
+ "subagent.continued": "continued",
426
+ "subagent.task.done": "Task complete",
427
+ "subagent.task.failed": "Task failed",
428
+ "subagent.running": "Running",
429
+ "subagent.no_output": "(no output)",
430
+ "subagent.helper_fallback_prefix": "Helper·",
431
+ "subagent.title_aria": "Click to expand / collapse subagent output",
432
+ "subagent.tag_title": "Subagent",
433
+ "ui.expand": "Expand",
434
+ "ui.collapse": "Collapse",
435
+ "ui.expand_panel_aria": "Expand subagent output",
436
+ "ui.collapse_panel_aria": "Collapse subagent output"
437
+ }
438
+ };
439
+ function getActiveLang() {
440
+ var raw = state.config && typeof state.config.language === "string" ? state.config.language.trim() : "";
441
+ if (!raw) return I18N_DEFAULT_LANG;
442
+ if (I18N[raw]) return raw;
443
+ // 模糊匹配:用户可能写 "english" / "en" / "ENG"
444
+ var lower = raw.toLowerCase();
445
+ if (lower === "english" || lower === "en" || lower.indexOf("english") === 0 || lower.indexOf("英") === 0) return "English";
446
+ if (lower === "中文" || lower === "zh" || lower.indexOf("zh") === 0 || lower.indexOf("中") === 0 || lower.indexOf("chinese") === 0) return "中文";
447
+ return "English"; // 其它语言走英文 fallback(Claude 会按 raw 回答,UI 至少英文不卡)
448
+ }
449
+ function t(key, params) {
450
+ var lang = getActiveLang();
451
+ var table = I18N[lang] || I18N[I18N_DEFAULT_LANG];
452
+ var template = table && key in table ? table[key] : null;
453
+ if (template == null) {
454
+ var def = I18N[I18N_DEFAULT_LANG];
455
+ template = def && key in def ? def[key] : key;
456
+ }
457
+ if (params && typeof template === "string") {
458
+ for (var k in params) {
459
+ if (!Object.prototype.hasOwnProperty.call(params, k)) continue;
460
+ template = template.split("{" + k + "}").join(params[k]);
461
+ }
462
+ }
463
+ return template;
464
+ }
465
+
387
466
  // ── 统一线性图标库 ──
388
467
  // 替代页面里散落的 emoji(🛡 / ⌨ / 📁 / 🔔 …)。这些 emoji 在系统字体里渲染成
389
468
  // 彩色卡通形态,与项目温暖米色 + 棕橙的复古主题视觉冲突明显。这里集中维护
@@ -1070,6 +1149,10 @@
1070
1149
  }
1071
1150
  case "tool-group":
1072
1151
  return el.getAttribute("data-expanded") === "true";
1152
+ case "subagent-reply":
1153
+ return el.getAttribute("data-expanded") === "true";
1154
+ case "subagent-panel":
1155
+ return el.getAttribute("data-expanded") === "true";
1073
1156
  default:
1074
1157
  return false;
1075
1158
  }
@@ -1118,6 +1201,35 @@
1118
1201
  if (chevron) chevron.style.transform = expanded ? "rotate(180deg)" : "";
1119
1202
  break;
1120
1203
  }
1204
+ case "subagent-reply": {
1205
+ el.setAttribute("data-expanded", expanded ? "true" : "false");
1206
+ var subLabel = el.querySelector(".subagent-reply-toggle-label");
1207
+ if (subLabel) subLabel.textContent = expanded ? "收起" : "展开";
1208
+ var subToggleBtn = el.querySelector(".subagent-reply-toggle");
1209
+ if (subToggleBtn) {
1210
+ subToggleBtn.setAttribute("aria-expanded", expanded ? "true" : "false");
1211
+ subToggleBtn.setAttribute("aria-label", expanded ? "收起子代理回复" : "展开子代理回复全文");
1212
+ }
1213
+ break;
1214
+ }
1215
+ case "subagent-panel": {
1216
+ el.setAttribute("data-expanded", expanded ? "true" : "false");
1217
+ // 头/尾两个按钮都得同步——label、aria-expanded、aria-label
1218
+ var panelBtns = el.querySelectorAll(".subagent-panel-toggle");
1219
+ for (var pbi = 0; pbi < panelBtns.length; pbi++) {
1220
+ var pb = panelBtns[pbi];
1221
+ pb.setAttribute("aria-expanded", expanded ? "true" : "false");
1222
+ pb.setAttribute("aria-label", expanded ? "收起子代理输出" : "展开子代理输出");
1223
+ var pblbl = pb.querySelector(".subagent-panel-toggle-label");
1224
+ if (pblbl) pblbl.textContent = expanded ? "收起" : "展开";
1225
+ }
1226
+ // 展开时把 body 滚到顶,避免延续上次的滚动位置造成"展开后看到一半"
1227
+ if (expanded) {
1228
+ var pbody = el.querySelector(".subagent-panel-body");
1229
+ if (pbody) pbody.scrollTop = 0;
1230
+ }
1231
+ break;
1232
+ }
1121
1233
  }
1122
1234
  }
1123
1235
 
@@ -2250,6 +2362,10 @@
2250
2362
  state.quickCommitForm.makeTag = tagCb.checked;
2251
2363
  var row = document.getElementById("quick-commit-tag-row");
2252
2364
  if (row) row.classList.toggle("hidden", !tagCb.checked);
2365
+ if (tagCb.checked) {
2366
+ var input = document.getElementById("quick-commit-tag");
2367
+ if (input) setTimeout(function() { input.focus(); }, 0);
2368
+ }
2253
2369
  });
2254
2370
  var tagInput = document.getElementById("quick-commit-tag");
2255
2371
  if (tagInput) tagInput.addEventListener("input", function() {
@@ -2377,9 +2493,8 @@
2377
2493
  state.quickCommitForm.customMessage = aiMessage;
2378
2494
  }
2379
2495
  var currentTag = (state.quickCommitForm.tag || "").trim();
2380
- if (!currentTag && aiTag) {
2496
+ if (state.quickCommitForm.makeTag && !currentTag && aiTag) {
2381
2497
  state.quickCommitForm.tag = aiTag;
2382
- state.quickCommitForm.makeTag = true;
2383
2498
  }
2384
2499
  })
2385
2500
  .catch(function(error) {
@@ -2740,19 +2855,21 @@
2740
2855
  '<div class="qc-files-wrap">' + fileRows + '</div>' +
2741
2856
  '<div class="qc-message-row" id="quick-commit-message-row">' +
2742
2857
  '<div class="qc-message-header"><label class="field-label" for="quick-commit-message">commit message</label>' +
2743
- '<button type="button" id="quick-commit-ai-btn" class="btn btn-ghost btn-sm"' + (state.quickCommitGenerating ? ' disabled' : '') + '>' + (state.quickCommitGenerating ? '生成中…' : 'AI 生成') + '</button>' +
2858
+ '<div class="qc-ai-controls">' +
2859
+ '<button type="button" id="quick-commit-ai-btn" class="btn btn-ghost btn-sm"' + (state.quickCommitGenerating ? ' disabled' : '') + '>' + (state.quickCommitGenerating ? '生成中…' : 'AI 生成') + '</button>' +
2860
+ '<label class="qc-ai-tag-toggle" title="开启后,AI 会一并建议 tag,提交时会为本次 commit 打 tag">' +
2861
+ '<span class="qc-ai-tag-label">含 tag</span>' +
2862
+ '<span class="qc-switch qc-switch--compact">' +
2863
+ '<input type="checkbox" id="quick-commit-make-tag" class="switch-toggle" aria-label="同时为本次 commit 打 tag"' + (f.makeTag ? ' checked' : '') + ((state.quickCommitSubmitting || state.quickCommitGenerating) ? ' disabled' : '') + '>' +
2864
+ '<span class="switch-slider"></span>' +
2865
+ '</span>' +
2866
+ '</label>' +
2867
+ '</div>' +
2744
2868
  '</div>' +
2745
2869
  '<textarea id="quick-commit-message" class="field-input" rows="2" placeholder="输入 commit message 或点击 AI 生成"' + (state.quickCommitSubmitting ? ' disabled' : '') + '>' + escapeHtml(f.customMessage || "") + '</textarea>' +
2746
2870
  '</div>' +
2747
- '<div class="qc-checkbox-row">' +
2748
- '<label class="qc-checkbox-label" for="quick-commit-make-tag">同时为本次 commit 打 tag</label>' +
2749
- '<label class="qc-switch">' +
2750
- '<input type="checkbox" id="quick-commit-make-tag" class="switch-toggle"' + (f.makeTag ? ' checked' : '') + '>' +
2751
- '<span class="switch-slider"></span>' +
2752
- '</label>' +
2753
- '</div>' +
2754
2871
  '<div class="qc-tag-row' + (f.makeTag ? '' : ' hidden') + '" id="quick-commit-tag-row">' +
2755
- '<input type="text" id="quick-commit-tag" class="field-input" placeholder="输入 tag 名称;留空将由 AI 在提交时自动生成" value="' + escapeHtml(f.tag || "") + '"' + (state.quickCommitSubmitting ? ' disabled' : '') + '>' +
2872
+ '<input type="text" id="quick-commit-tag" class="field-input" placeholder="输入 tag 名称;留空则提交时由 AI 自动生成" value="' + escapeHtml(f.tag || "") + '"' + (state.quickCommitSubmitting ? ' disabled' : '') + '>' +
2756
2873
  '</div>' +
2757
2874
  (state.quickCommitError ? '<p class="error-message">' + escapeHtml(state.quickCommitError) + '</p>' : '') +
2758
2875
  '<div class="qc-section-actions">' +
@@ -3532,7 +3649,9 @@
3532
3649
  var visibleHistory = getClaudeHistoryRegionItems();
3533
3650
  var expanded = !!state.claudeHistoryExpanded;
3534
3651
  var loaded = !!state.claudeHistoryLoaded;
3535
- var count = loaded ? visibleHistory.length : 0;
3652
+ var codexVisible = getVisibleCodexHistorySessions();
3653
+ var codexLoaded = !!state.codexHistoryLoaded;
3654
+ var count = (loaded ? visibleHistory.length : 0) + (codexLoaded ? codexVisible.length : 0);
3536
3655
 
3537
3656
  var badgeCls = "history-bubble";
3538
3657
  var badgeContent;
@@ -3558,7 +3677,7 @@
3558
3677
  '</button>';
3559
3678
 
3560
3679
  var body = expanded
3561
- ? '<div class="sidebar-history-body" id="sidebar-history-body">' + renderClaudeHistoryBodyContent(visibleHistory) + '</div>'
3680
+ ? '<div class="sidebar-history-body" id="sidebar-history-body">' + renderClaudeHistoryBodyContent(visibleHistory) + renderCodexHistoryBodyContent(codexVisible) + '</div>'
3562
3681
  : '';
3563
3682
 
3564
3683
  return header + body;
@@ -3789,6 +3908,44 @@
3789
3908
  });
3790
3909
  }
3791
3910
 
3911
+ function renderCodexHistoryDirectoryHeader(cwd, cwdShort, count, isExpanded) {
3912
+ var chevron = isExpanded ? "\u25be" : "\u25b8";
3913
+ return '<div class="claude-history-directory-header codex-history-directory-header" data-action="toggle-codex-history-directory" data-cwd="' + escapeHtml(cwd) + '" role="button" tabindex="0">' +
3914
+ '<div class="session-group-title claude-history-directory-title">' +
3915
+ '<span class="chevron">' + chevron + '</span>' +
3916
+ '<span class="claude-history-directory-label">' + escapeHtml(cwdShort) + ' (' + count + ')</span>' +
3917
+ '</div>' +
3918
+ '</div>';
3919
+ }
3920
+
3921
+ function renderCodexHistoryBodyContent(visibleHistory) {
3922
+ if (!state.codexHistoryLoaded) {
3923
+ return '<div class="claude-history-loading">\u626b\u63cf Codex \u5386\u53f2\u4f1a\u8bdd\u4e2d\u2026</div>';
3924
+ }
3925
+ if (visibleHistory.length === 0) {
3926
+ return '';
3927
+ }
3928
+ var groups = {};
3929
+ var groupOrder = [];
3930
+ visibleHistory.forEach(function(s) {
3931
+ if (!groups[s.cwd]) {
3932
+ groups[s.cwd] = [];
3933
+ groupOrder.push(s.cwd);
3934
+ }
3935
+ groups[s.cwd].push(s);
3936
+ });
3937
+ var listHtml = '<div class="sidebar-history-section-label">Codex</div>';
3938
+ groupOrder.forEach(function(cwd) {
3939
+ var cwdShort = cwd.split("/").filter(Boolean).slice(-3).join("/");
3940
+ var isDirExpanded = !!state.codexHistoryExpandedDirs[cwd];
3941
+ listHtml += renderCodexHistoryDirectoryHeader(cwd, cwdShort, groups[cwd].length, isDirExpanded);
3942
+ if (isDirExpanded) {
3943
+ listHtml += groups[cwd].map(function(session) { return renderClaudeHistoryItem(session, "codex"); }).join("");
3944
+ }
3945
+ });
3946
+ return '<div class="sidebar-history-scroll codex-history-scroll">' + listHtml + '</div>';
3947
+ }
3948
+
3792
3949
  function renderClaudeHistoryDirectoryHeader(cwd, cwdShort, count, isExpanded) {
3793
3950
  var chevron = isExpanded ? "&#9662;" : "&#9656;";
3794
3951
  return '<div class="claude-history-directory-header" data-action="toggle-history-directory" data-cwd="' + escapeHtml(cwd) + '" role="button" tabindex="0">' +
@@ -3802,19 +3959,23 @@
3802
3959
  }
3803
3960
 
3804
3961
  function renderClaudeHistoryItem(session, kind) {
3962
+ var isCodex = kind === "codex";
3963
+ var rAct = isCodex ? "resume-codex-history" : "resume-history";
3964
+ var dAct = isCodex ? "delete-codex-history" : "delete-history";
3965
+ var selMap = isCodex ? state.selectedCodexHistoryIds : state.selectedClaudeHistoryIds;
3805
3966
  var shortId = session.claudeSessionId.slice(0, 8);
3806
3967
  var preview = session.firstUserMessage || "(空会话)";
3807
3968
  var timeStr = formatHistoryTime(session.timestamp);
3808
3969
  var checkbox = renderManageCheckbox(kind, session.claudeSessionId, "选择历史会话 " + preview);
3809
3970
  var deleteButton = state.sessionsManageMode ? '' :
3810
- '<button class="session-action-btn delete-btn" data-action="delete-history" data-claude-session-id="' +
3971
+ '<button class="session-action-btn delete-btn" data-action="' + dAct + '" data-claude-session-id="' +
3811
3972
  session.claudeSessionId + '" type="button" aria-label="删除会话" title="隐藏此历史会话"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/></svg></button>';
3812
3973
  var resumeButton = state.sessionsManageMode ? '' :
3813
- '<button class="session-action-btn" data-action="resume-history" data-claude-session-id="' +
3974
+ '<button class="session-action-btn" data-action="' + rAct + '" data-claude-session-id="' +
3814
3975
  session.claudeSessionId + '" data-cwd="' + escapeHtml(session.cwd) +
3815
- '" type="button" aria-label="恢复会话" title="恢复此 Claude 历史会话"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 4v6h6"/><path d="M3.51 15a9 9 0 105.64-11.36L3 10"/></svg></button>';
3976
+ '" type="button" aria-label="恢复会话" title="' + (isCodex ? "恢复此 Codex 历史会话" : "恢复此 Claude 历史会话") + '"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 4v6h6"/><path d="M3.51 15a9 9 0 105.64-11.36L3 10"/></svg></button>';
3816
3977
 
3817
- return '<div class="session-item claude-history-item' + (state.sessionsManageMode && state.selectedClaudeHistoryIds[session.claudeSessionId] ? ' selected' : '') + '" data-claude-history-id="' + session.claudeSessionId + '" data-cwd="' + escapeHtml(session.cwd) + '" role="button" tabindex="0">' +
3978
+ return '<div class="session-item claude-history-item' + (state.sessionsManageMode && selMap[session.claudeSessionId] ? ' selected' : '') + '" data-claude-history-id="' + session.claudeSessionId + '" data-cwd="' + escapeHtml(session.cwd) + '" role="button" tabindex="0">' +
3818
3979
  '<div class="session-item-content">' +
3819
3980
  '<div class="session-item-row">' +
3820
3981
  checkbox +
@@ -3872,6 +4033,7 @@
3872
4033
  // 且在已加载时立即 resolve。
3873
4034
  var _claudeHistoryLoadingPromise = null;
3874
4035
  function ensureClaudeHistoryLoaded() {
4036
+ ensureCodexHistoryLoaded();
3875
4037
  if (state.claudeHistoryLoaded) return Promise.resolve();
3876
4038
  if (_claudeHistoryLoadingPromise) return _claudeHistoryLoadingPromise;
3877
4039
  _claudeHistoryLoadingPromise = loadClaudeHistory().then(function() {
@@ -3882,6 +4044,46 @@
3882
4044
  return _claudeHistoryLoadingPromise;
3883
4045
  }
3884
4046
 
4047
+ function loadCodexHistory() {
4048
+ return fetch("/api/codex-history", { credentials: "same-origin" })
4049
+ .then(function(res) {
4050
+ if (!res.ok) return [];
4051
+ return res.json();
4052
+ })
4053
+ .then(function(sessions) {
4054
+ state.codexHistory = sessions || [];
4055
+ state.codexHistoryLoaded = true;
4056
+ updateSessionsList();
4057
+ })
4058
+ .catch(function() {
4059
+ state.codexHistoryLoaded = true;
4060
+ state.codexHistory = [];
4061
+ updateSessionsList();
4062
+ });
4063
+ }
4064
+
4065
+ var _codexHistoryLoadingPromise = null;
4066
+ function ensureCodexHistoryLoaded() {
4067
+ if (state.codexHistoryLoaded) return Promise.resolve();
4068
+ if (_codexHistoryLoadingPromise) return _codexHistoryLoadingPromise;
4069
+ _codexHistoryLoadingPromise = loadCodexHistory().then(function() {
4070
+ _codexHistoryLoadingPromise = null;
4071
+ }, function() {
4072
+ _codexHistoryLoadingPromise = null;
4073
+ });
4074
+ return _codexHistoryLoadingPromise;
4075
+ }
4076
+
4077
+ function getVisibleCodexHistorySessions() {
4078
+ var managedIds = new Set();
4079
+ state.sessions.forEach(function(s) {
4080
+ if (s.claudeSessionId) managedIds.add(s.claudeSessionId);
4081
+ });
4082
+ return state.codexHistory.filter(function(s) {
4083
+ return s.hasConversation && !s.managedByWand && !managedIds.has(s.claudeSessionId);
4084
+ });
4085
+ }
4086
+
3885
4087
  function isMobileLayout() {
3886
4088
  return window.innerWidth <= 768;
3887
4089
  }
@@ -5658,35 +5860,28 @@
5658
5860
  }
5659
5861
  persistElementExpandState(el, "thinking");
5660
5862
  };
5661
- // Toggle function for subagent reply bubbles — cycles previewexpanded → collapsed.
5662
- // 三态循环(preview 默认 ~5 行可滚 / expanded 大区可滚 / collapsed 完全收起)。
5663
- window.__subagentReplyCycle = function(e, btn) {
5863
+ // Toggle function for subagent reply bubbles — simple two-state preview/expanded.
5864
+ // 参考 opencode 的折叠面板:默认固定高度预览(含底部渐隐 mask),点击切到全文展开。
5865
+ // 状态写在 data-expanded 上,配套 CSS 控制 max-height + mask;用 data-expand-key
5866
+ // 走通用持久化通道(applyPersistedExpandState 会自动恢复用户上次的选择)。
5867
+ window.__subagentReplyToggle = function(e, target) {
5664
5868
  if (e) { e.preventDefault(); e.stopPropagation(); }
5665
- var bubble = btn.closest(".subagent-reply");
5869
+ var bubble = target && target.closest ? target.closest(".subagent-reply") : null;
5666
5870
  if (!bubble) return;
5667
- var modes = ["preview", "expanded", "collapsed"];
5668
- var current = bubble.getAttribute("data-collapse-mode") || "preview";
5669
- var idx = modes.indexOf(current);
5670
- if (idx < 0) idx = 0;
5671
- var next = modes[(idx + 1) % modes.length];
5672
- bubble.setAttribute("data-collapse-mode", next);
5673
- var label = btn.querySelector(".subagent-reply-cycle-label");
5674
- var icon = btn.querySelector(".subagent-reply-cycle-icon");
5675
- if (label) {
5676
- label.textContent = next === "preview" ? "展开"
5677
- : next === "expanded" ? "收起"
5678
- : "预览";
5679
- }
5680
- if (icon) {
5681
- icon.textContent = next === "collapsed" ? "▸"
5682
- : next === "expanded" ? "▴"
5683
- : "▾";
5684
- }
5685
- btn.setAttribute("aria-label",
5686
- next === "preview" ? "点击展开全部" :
5687
- next === "expanded" ? "点击完全收起" :
5688
- "点击切回预览"
5689
- );
5871
+ var expanded = bubble.getAttribute("data-expanded") === "true";
5872
+ applyExpandedState(bubble, "subagent-reply", !expanded);
5873
+ persistElementExpandState(bubble, "subagent-reply");
5874
+ };
5875
+ // Toggle function for whole subagent panel (handoff + body + footer)。
5876
+ // 头部 / 尾部按钮、整条 header 都绑这个;data-expanded 一变,CSS 切 body max-height
5877
+ // + 旋转 chevron,applyExpandedState 走通用通道写持久化。
5878
+ window.__subagentPanelToggle = function(e, target) {
5879
+ if (e) { e.preventDefault(); e.stopPropagation(); }
5880
+ var panel = target && target.closest ? target.closest(".subagent-panel") : null;
5881
+ if (!panel) return;
5882
+ var expanded = panel.getAttribute("data-expanded") === "true";
5883
+ applyExpandedState(panel, "subagent-panel", !expanded);
5884
+ persistElementExpandState(panel, "subagent-panel");
5690
5885
  };
5691
5886
  // Toggle function for inline tool rows (Read, Glob, Grep, etc.)
5692
5887
  window.__inlineToolToggle = function(el) {
@@ -7143,6 +7338,14 @@
7143
7338
  handleResumeAction(actionButton);
7144
7339
  } else if (actionButton.dataset.action === "resume-history" && actionButton.dataset.claudeSessionId) {
7145
7340
  handleResumeHistoryAction(actionButton);
7341
+ } else if (actionButton.dataset.action === "resume-codex-history" && actionButton.dataset.claudeSessionId) {
7342
+ handleResumeCodexHistoryAction(actionButton);
7343
+ } else if (actionButton.dataset.action === "delete-codex-history" && actionButton.dataset.claudeSessionId) {
7344
+ handleDeleteCodexHistoryAction(actionButton);
7345
+ } else if (actionButton.dataset.action === "toggle-codex-history-directory" && actionButton.dataset.cwd) {
7346
+ var codexDirCwd = actionButton.dataset.cwd;
7347
+ state.codexHistoryExpandedDirs[codexDirCwd] = !state.codexHistoryExpandedDirs[codexDirCwd];
7348
+ updateSessionsList();
7146
7349
  } else if (actionButton.dataset.action === "worktree-merge" && actionButton.dataset.sessionId) {
7147
7350
  openWorktreeMergeModal(actionButton.dataset.sessionId);
7148
7351
  } else if (actionButton.dataset.action === "worktree-cleanup" && actionButton.dataset.sessionId) {
@@ -14543,6 +14746,81 @@
14543
14746
  });
14544
14747
  }
14545
14748
 
14749
+ function handleResumeCodexHistoryAction(actionButton) {
14750
+ var threadId = actionButton.dataset.claudeSessionId;
14751
+ var cwd = actionButton.dataset.cwd;
14752
+ console.log("[WAND] handleResumeCodexHistoryAction threadId:", threadId, "cwd:", cwd);
14753
+ if (!threadId) return;
14754
+ actionButton.disabled = true;
14755
+ resumeCodexHistorySession(threadId, cwd)
14756
+ .then(function(data) {
14757
+ if (data && data.id) {
14758
+ state.codexHistory = state.codexHistory.filter(function(s) {
14759
+ return s.claudeSessionId !== threadId;
14760
+ });
14761
+ state.selectedId = data.id;
14762
+ persistSelectedId();
14763
+ state.drafts[data.id] = "";
14764
+ activateSession(data).then(function() {
14765
+ dismissDrawerIfOverlay();
14766
+ });
14767
+ }
14768
+ })
14769
+ .finally(function() {
14770
+ actionButton.disabled = false;
14771
+ });
14772
+ }
14773
+
14774
+ function resumeCodexHistorySession(threadId, cwd) {
14775
+ return fetch("/api/codex-sessions/" + encodeURIComponent(threadId) + "/resume", {
14776
+ method: "POST",
14777
+ headers: { "Content-Type": "application/json" },
14778
+ credentials: "same-origin",
14779
+ body: JSON.stringify(withTerminalDimensions({
14780
+ mode: state.chatMode || (state.config && state.config.defaultMode) || "default",
14781
+ cwd: cwd
14782
+ }))
14783
+ })
14784
+ .then(function(res) { return res.json(); })
14785
+ .then(function(data) {
14786
+ if (data.error) {
14787
+ showToast(data.error, "error");
14788
+ return null;
14789
+ }
14790
+ return data;
14791
+ })
14792
+ .catch(function(error) {
14793
+ showToast((error && error.message) || "无法恢复历史会话。", "error");
14794
+ return null;
14795
+ });
14796
+ }
14797
+
14798
+ function handleDeleteCodexHistoryAction(actionButton) {
14799
+ var threadId = actionButton.dataset.claudeSessionId;
14800
+ if (!threadId) return;
14801
+ console.log("[WAND] handleDeleteCodexHistoryAction threadId:", threadId);
14802
+ var item = actionButton.closest(".claude-history-item");
14803
+ if (item) item.style.opacity = "0.5";
14804
+ fetch("/api/codex-history/" + encodeURIComponent(threadId), {
14805
+ method: "DELETE",
14806
+ credentials: "same-origin"
14807
+ })
14808
+ .then(function(res) { return res.json(); })
14809
+ .then(function(data) {
14810
+ if (data && data.ok) {
14811
+ state.codexHistory = state.codexHistory.filter(function(s) {
14812
+ return s.claudeSessionId !== threadId;
14813
+ });
14814
+ updateSessionsList();
14815
+ } else if (item) {
14816
+ item.style.opacity = "1";
14817
+ }
14818
+ })
14819
+ .catch(function() {
14820
+ if (item) item.style.opacity = "1";
14821
+ });
14822
+ }
14823
+
14546
14824
  function handleResumeHistoryAction(actionButton) {
14547
14825
  var claudeSessionId = actionButton.dataset.claudeSessionId;
14548
14826
  var cwd = actionButton.dataset.cwd;
@@ -18905,8 +19183,8 @@
18905
19183
  var agentType = sub.agentType || "";
18906
19184
  if (agentType && SUBAGENT_NAME_MAP[agentType]) return SUBAGENT_NAME_MAP[agentType];
18907
19185
  if (agentType) return agentType;
18908
- var tail = (sub.taskId || "").slice(-4) || "未知";
18909
- return "协作猫·" + tail;
19186
+ var tail = (sub.taskId || "").slice(-4) || (getActiveLang() === "English" ? "?" : "未知");
19187
+ return t("subagent.helper_fallback_prefix") + tail;
18910
19188
  }
18911
19189
  function getSubagentPalette(sub) {
18912
19190
  // 哈希优先用 agentType,让同类型 agent 跨 turn 颜色稳定;没有 agentType 时
@@ -18924,33 +19202,35 @@
18924
19202
  '</div>';
18925
19203
  }
18926
19204
 
18927
- // subagent tool_result 独立 reply 气泡(markdown 渲染)。出错时显示红色错误体,
18928
- // 没文本时显示打字指示器(subagent 还在跑)。
19205
+ // subagent 最终回复(父 Task tool_result)——现在外层 .subagent-panel 已经
19206
+ // 负责整段折叠 / 滚动,这里只需把"任务完成 / 失败"做个轻量标记块,markdown
19207
+ // 内容平铺,让 panel 的 body 滚动条统一接管。
18929
19208
  function renderSubagentReplyBubble(block, role) {
18930
19209
  if (!block || block.type !== "tool_result") return "";
18931
19210
  var text = extractToolResultText(block.content);
18932
19211
  var isError = block.is_error === true;
18933
-
18934
- if (isError) {
18935
- return '<div class="subagent-reply error">' +
18936
- '<span class="subagent-reply-icon">✗</span>' +
18937
- '<div class="subagent-reply-body">' + (text ? renderMarkdown(text) : escapeHtml("(无输出)")) + '</div>' +
19212
+ var rawText = typeof text === "string" ? text : (text == null ? "" : String(text));
19213
+
19214
+ // pending:subagent 还在跑,没收到结果。在 panel body 里画一个 typing 指示器
19215
+ // 占位,告诉用户"还在跑"
19216
+ if (!isError && !rawText.trim()) {
19217
+ return '<div class="subagent-reply pending">' +
19218
+ '<span class="subagent-reply-marker pending">' + escapeHtml(t("subagent.running")) + '</span>' +
19219
+ '<span class="typing-indicator"><span></span><span></span><span></span></span>' +
18938
19220
  '</div>';
18939
19221
  }
18940
19222
 
18941
- if (!text || !String(text).trim()) {
18942
- return '<div class="subagent-reply pending"><span class="typing-indicator"><span></span><span></span><span></span></span></div>';
18943
- }
19223
+ var displayText = rawText.trim() ? rawText : t("subagent.no_output");
19224
+ var bodyHtml = rawText.trim() ? renderMarkdown(displayText) : escapeHtml(displayText);
19225
+ var markerLabel = isError ? t("subagent.task.failed") : t("subagent.task.done");
19226
+ var markerSymbol = isError ? "✗" : "✓";
18944
19227
 
18945
- // 三态折叠:preview(默认 ~5 行预览,内部可滚)→ expanded(高一些上限,可滚)→
18946
- // collapsed(完全收起,只剩工具条)→ preview。按钮一直可见在右下,状态写在
18947
- // data-collapse-mode 上,配套 CSS 控制 max-height。
18948
- return '<div class="subagent-reply collapsible" data-collapse-mode="preview">' +
18949
- '<div class="subagent-reply-scroll">' + renderMarkdown(text) + '</div>' +
18950
- '<button type="button" class="subagent-reply-cycle" onclick="__subagentReplyCycle(event, this)" title="展开 / 收起">' +
18951
- '<span class="subagent-reply-cycle-label">展开</span>' +
18952
- '<span class="subagent-reply-cycle-icon" aria-hidden="true">▾</span>' +
18953
- '</button>' +
19228
+ return '<div class="subagent-reply final' + (isError ? ' error' : '') + '">' +
19229
+ '<div class="subagent-reply-marker ' + (isError ? 'error' : 'done') + '">' +
19230
+ '<span class="subagent-reply-marker-icon" aria-hidden="true">' + markerSymbol + '</span>' +
19231
+ '<span class="subagent-reply-marker-label">' + escapeHtml(markerLabel) + '</span>' +
19232
+ '</div>' +
19233
+ '<div class="subagent-reply-content">' + bodyHtml + '</div>' +
18954
19234
  '</div>';
18955
19235
  }
18956
19236
  var PIXEL_AVATAR = {
@@ -19301,23 +19581,12 @@
19301
19581
  // 跳过整段。否则会渲染出"只有头像没内容"的空气泡。
19302
19582
  if (!segHtml || !segHtml.trim()) continue;
19303
19583
  if (seg.subagent) {
19304
- var subPalette = getSubagentPalette(seg.subagent);
19305
- if (showHandoff && lastSubId !== seg.subagent.taskId) {
19306
- var subName = getSubagentDisplayName(seg.subagent);
19307
- var desc = seg.subagent.taskDescription
19308
- ? ':<span class="chat-handoff-desc">' + escapeHtml(seg.subagent.taskDescription) + '</span>'
19309
- : '';
19310
- html += '<div class="chat-handoff" style="--agent-color:' + subPalette.primary + '">' +
19311
- '<span class="chat-handoff-arrow">↳</span> ' +
19312
- escapeHtml(parentPersonaName) + ' 让 <strong>' + escapeHtml(subName) + '</strong>' +
19313
- '<span class="chat-handoff-tag" title="子代理 / subagent">subagent</span>' +
19314
- '帮忙' + desc +
19315
- '</div>';
19316
- }
19317
- html += '<div class="chat-message-segment subagent" data-agent-id="' + escapeHtml(seg.subagent.taskId) + '" style="--agent-color:' + subPalette.primary + '">' +
19318
- subagentAvatarHtml(seg.subagent) +
19319
- '<div class="chat-message-content">' + segHtml + '</div>' +
19320
- '</div>';
19584
+ // 整段 subagent 输出包成一个统一的可折叠面板:
19585
+ // 头部 = handoff title + 展开按钮;body = segHtml(所有工具卡、文本、最终回复
19586
+ // 都在 body 里);footer = 同款按钮。同一 taskId 的连续段(极少出现的
19587
+ // parent→sub→parent→sub 交错)只在第一次露 handoff title。
19588
+ var includeHandoff = showHandoff && lastSubId !== seg.subagent.taskId;
19589
+ html += buildSubagentPanelHtml(seg, parentPersonaName, segHtml, messageKey, includeHandoff);
19321
19590
  lastSubId = seg.subagent.taskId;
19322
19591
  } else {
19323
19592
  html += '<div class="chat-message-segment parent">' +
@@ -19330,6 +19599,85 @@
19330
19599
  return html;
19331
19600
  }
19332
19601
 
19602
+ // 渲染整段 subagent 输出为一个可折叠面板:
19603
+ // ┌─ subagent-panel ──────────────────────────────────┐
19604
+ // │ [🐱] ↳ 勤劳初二 让 协作猫 帮忙:xxx [展开 ▾] │ ← header (always visible)
19605
+ // ├───────────────────────────────────────────────────┤
19606
+ // │ <tool 卡 1> │
19607
+ // │ <text> │ ← body
19608
+ // │ <tool 卡 2> │ 默认 max-height 22em
19609
+ // │ <... 最终回复 ...> │ + 内部 overflow-y:auto
19610
+ // ├───────────────────────────────────────────────────┤
19611
+ // │ [展开 ▾] │ ← footer (always visible)
19612
+ // └───────────────────────────────────────────────────┘
19613
+ // 状态写在 .subagent-panel[data-expanded],按 messageKey + taskId 持久化。
19614
+ // 默认折叠(preview);用户点头/尾按钮或头部标题条都能切。
19615
+ function buildSubagentPanelHtml(seg, parentPersonaName, segHtml, messageKey, includeHandoff) {
19616
+ var sub = seg.subagent;
19617
+ var subPalette = getSubagentPalette(sub);
19618
+ var subName = getSubagentDisplayName(sub);
19619
+ var taskId = sub.taskId || "";
19620
+ var avatarSvg = buildPixelSvg(buildCatGrid(subPalette));
19621
+
19622
+ var titleHtml;
19623
+ if (includeHandoff) {
19624
+ // 用 i18n 模板拼 handoff title。{parent}/{sub} 用占位符避免在调用点拼字符串导致
19625
+ // 顺序错乱(中文 "X 让 Y 帮忙",英文 "X asked Y for help",词序完全不同)。
19626
+ // tag 是单独的彩色 chip,所以把模板里的 {sub} 替换成空 span 占位,然后再 splice
19627
+ // 进 <strong> + chip——保证模板可读、调用点不脏。
19628
+ var subInlineHtml = '<strong class="subagent-panel-name">' + escapeHtml(subName) + '</strong>' +
19629
+ '<span class="subagent-panel-tag" title="' + escapeHtml(t("subagent.tag_title")) + '">' + escapeHtml(t("subagent.tag")) + '</span>';
19630
+ var hasDesc = !!(sub.taskDescription && String(sub.taskDescription).trim());
19631
+ var handoffTpl = hasDesc ? t("subagent.handoff.with_desc", { parent: escapeHtml(parentPersonaName), sub: subInlineHtml })
19632
+ : t("subagent.handoff", { parent: escapeHtml(parentPersonaName), sub: subInlineHtml });
19633
+ var descSpan = hasDesc
19634
+ ? '<span class="subagent-panel-task-desc">' + escapeHtml(sub.taskDescription) + '</span>'
19635
+ : '';
19636
+ titleHtml = '<span class="subagent-panel-arrow" aria-hidden="true">↳</span>' +
19637
+ '<span class="subagent-panel-attribution">' + handoffTpl + descSpan + '</span>';
19638
+ } else {
19639
+ titleHtml = '<span class="subagent-panel-attribution">' +
19640
+ '<strong class="subagent-panel-name">' + escapeHtml(subName) + '</strong>' +
19641
+ '<span class="subagent-panel-task-desc"> ' + escapeHtml(t("subagent.continued")) + '</span>' +
19642
+ '</span>';
19643
+ }
19644
+
19645
+ var expandKey = buildExpandKey("subagent-panel", [messageKey, taskId]);
19646
+ var persisted = getPersistedExpandState(expandKey);
19647
+ var expanded = persisted === null ? false : !!persisted;
19648
+
19649
+ function toggleBtnHtml(position) {
19650
+ return '<button type="button" class="subagent-panel-toggle" ' +
19651
+ 'data-position="' + position + '" ' +
19652
+ 'onclick="__subagentPanelToggle(event, this)" ' +
19653
+ 'aria-expanded="' + (expanded ? "true" : "false") + '" ' +
19654
+ 'aria-label="' + escapeHtml(expanded ? t("ui.collapse_panel_aria") : t("ui.expand_panel_aria")) + '">' +
19655
+ '<span class="subagent-panel-toggle-label">' + escapeHtml(expanded ? t("ui.collapse") : t("ui.expand")) + '</span>' +
19656
+ '<svg class="subagent-panel-toggle-icon" width="10" height="10" viewBox="0 0 10 10" fill="none" aria-hidden="true">' +
19657
+ '<path d="M2.5 3.75L5 6.25L7.5 3.75" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>' +
19658
+ '</svg>' +
19659
+ '</button>';
19660
+ }
19661
+
19662
+ return '<div class="subagent-panel" ' +
19663
+ 'data-expand-kind="subagent-panel" ' +
19664
+ 'data-expand-key="' + escapeHtml(expandKey) + '" ' +
19665
+ 'data-agent-id="' + escapeHtml(taskId) + '" ' +
19666
+ 'data-expanded="' + (expanded ? "true" : "false") + '" ' +
19667
+ 'style="--agent-color:' + subPalette.primary + '">' +
19668
+ '<div class="subagent-panel-header" onclick="__subagentPanelToggle(event, this)" role="button" tabindex="0" aria-label="' + escapeHtml(t("subagent.title_aria")) + '">' +
19669
+ '<span class="subagent-panel-avatar" aria-hidden="true">' + avatarSvg + '</span>' +
19670
+ titleHtml +
19671
+ toggleBtnHtml("top") +
19672
+ '</div>' +
19673
+ '<div class="subagent-panel-body">' + segHtml + '</div>' +
19674
+ '<div class="subagent-panel-footer">' +
19675
+ '<span class="subagent-panel-footer-hint" aria-hidden="true">— ' + escapeHtml(subName) + ' —</span>' +
19676
+ toggleBtnHtml("bottom") +
19677
+ '</div>' +
19678
+ '</div>';
19679
+ }
19680
+
19333
19681
  function renderStructuredMessage(msg, roundUsage, messageIndex, legacyTaskMap) {
19334
19682
  var role = msg.role;
19335
19683
  var messageKey = getMessageKey(msg, messageIndex);