openclacky 1.4.1 → 1.5.1

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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -0
  3. data/lib/clacky/agent/llm_caller.rb +3 -3
  4. data/lib/clacky/agent/session_serializer.rb +39 -5
  5. data/lib/clacky/brand_config.rb +49 -7
  6. data/lib/clacky/client.rb +22 -16
  7. data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +135 -84
  8. data/lib/clacky/default_extensions/ext-studio/ext.yml +2 -4
  9. data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +522 -0
  10. data/lib/clacky/extension/api_extension.rb +5 -6
  11. data/lib/clacky/extension/loader.rb +16 -2
  12. data/lib/clacky/extension/scaffold/templates/full/api/handler.rb.erb +16 -0
  13. data/lib/clacky/mcp/http_transport.rb +1 -1
  14. data/lib/clacky/providers.rb +109 -1
  15. data/lib/clacky/server/http_server.rb +115 -19
  16. data/lib/clacky/server/session_registry.rb +9 -0
  17. data/lib/clacky/utils/model_pricing.rb +36 -0
  18. data/lib/clacky/version.rb +1 -1
  19. data/lib/clacky/web/app.css +321 -32
  20. data/lib/clacky/web/app.js +29 -6
  21. data/lib/clacky/web/components/onboard.js +7 -3
  22. data/lib/clacky/web/features/extensions/store.js +54 -2
  23. data/lib/clacky/web/features/extensions/view.js +40 -5
  24. data/lib/clacky/web/features/new-session/store.js +8 -4
  25. data/lib/clacky/web/features/version/view.js +5 -1
  26. data/lib/clacky/web/i18n.js +52 -18
  27. data/lib/clacky/web/index.html +51 -5
  28. data/lib/clacky/web/sessions.js +40 -11
  29. data/lib/clacky/web/settings.js +22 -3
  30. data/lib/clacky/web/theme.js +27 -58
  31. data/scripts/build/src/uninstall.sh.cc +1 -1
  32. metadata +2 -5
  33. data/lib/clacky/default_extensions/ext-studio/skills/ext-debug/SKILL.md +0 -71
  34. data/lib/clacky/default_extensions/ext-studio/skills/ext-publish/SKILL.md +0 -73
  35. data/lib/clacky/default_extensions/ext-studio/skills/ext-scaffold/SKILL.md +0 -65
  36. data/lib/clacky/default_skills/extend-openclacky/SKILL.md +0 -106
@@ -209,8 +209,11 @@ const ExtensionsView = (() => {
209
209
  if (remove) {
210
210
  remove.addEventListener("click", async () => {
211
211
  const id = remove.getAttribute("data-ext-remove");
212
- const ok = await Modal.confirm(I18n.t("extensions.action.removeConfirm"));
213
- if (ok) Extensions.uninstall(id);
212
+ const { ok, checked } = await Modal.confirmWithCheckbox(
213
+ I18n.t("extensions.action.removeConfirm"),
214
+ I18n.t("extensions.action.removePurgeData")
215
+ );
216
+ if (ok) Extensions.uninstall(id, checked);
214
217
  });
215
218
  }
216
219
 
@@ -313,7 +316,9 @@ const ExtensionsView = (() => {
313
316
  function _renderActions(ext) {
314
317
  const id = ext.id != null ? String(ext.id) : (ext.name || ext.slug || "");
315
318
  if (!ext.installed) {
316
- if (ext.origin === "marketplace" && ext.download_url) {
319
+ // Show install button for both marketplace and brand-private (origin=self) extensions,
320
+ // as long as a download_url is available.
321
+ if (ext.download_url) {
317
322
  return `
318
323
  <div class="extension-detail-actions">
319
324
  <button type="button" class="extension-action extension-action-install" data-ext-install="${escapeHtml(id)}">${escapeHtml(I18n.t("extensions.action.install"))}</button>
@@ -407,6 +412,17 @@ const ExtensionsView = (() => {
407
412
  </div>`;
408
413
  }
409
414
 
415
+ // Show/hide the brand filter tab based on brand status from the store.
416
+ async function _applyBrandTab() {
417
+ try {
418
+ const data = await Extensions.fetchBrandStatus();
419
+ const brandTab = $("tab-extensions-brand");
420
+ if (brandTab) brandTab.style.display = data.branded ? "" : "none";
421
+ } catch (_e) {
422
+ // On error, keep tab hidden.
423
+ }
424
+ }
425
+
410
426
  function _wireDom() {
411
427
  if (_domWired) return;
412
428
 
@@ -432,10 +448,25 @@ const ExtensionsView = (() => {
432
448
  btn.addEventListener("click", () => {
433
449
  document.querySelectorAll(".extensions-filter-tab").forEach(b => b.classList.remove("extensions-filter-tab-active"));
434
450
  btn.classList.add("extensions-filter-tab-active");
435
- Extensions.setFilterInstalled(btn.dataset.filter === "installed");
451
+ const filter = btn.dataset.filter;
452
+ if (filter === "installed") {
453
+ Extensions.setFilterInstalled(true);
454
+ } else if (filter === "brand") {
455
+ Extensions.setFilterBrand(true);
456
+ } else {
457
+ Extensions.setFilterInstalled(false);
458
+ }
436
459
  });
437
460
  });
438
461
 
462
+ // Subscribe to brand status changes to show/hide the brand tab.
463
+ if (window.Skills && Skills.on) {
464
+ Skills.on("brandStatus:changed", (p) => {
465
+ const brandTab = $("tab-extensions-brand");
466
+ if (brandTab) brandTab.style.display = p.branded ? "" : "none";
467
+ });
468
+ }
469
+
439
470
  const list = $("extensions-list");
440
471
  if (list) {
441
472
  list.addEventListener("click", (e) => {
@@ -466,8 +497,12 @@ const ExtensionsView = (() => {
466
497
  }
467
498
 
468
499
  const viewApi = {
469
- onPanelShow(opts) {
500
+ async onPanelShow(opts) {
470
501
  _wireDom();
502
+ // Apply brand tab visibility based on current status (fast path),
503
+ // then also refresh in background so it stays up-to-date.
504
+ _applyBrandTab();
505
+ if (window.Skills && Skills.refreshBrandStatus) Skills.refreshBrandStatus();
471
506
  const detailId = opts && opts.detailId;
472
507
  if (detailId) {
473
508
  Extensions.load();
@@ -122,19 +122,23 @@ const NewSessionStore = (() => {
122
122
  return "Session " + (maxN + 1);
123
123
  }
124
124
 
125
- async function createSession({ existingSessions } = {}) {
125
+ async function createSession({ existingSessions, useDefaults = false } = {}) {
126
126
  if (_state.creating) return null;
127
127
  _state.creating = true;
128
128
  _emit("newSession:creating", { creating: true });
129
129
 
130
130
  try {
131
- const agentId = _state.selectedAgentId || "general";
131
+ // useDefaults=true: quick-create from sidebar — use the default agent and
132
+ // skip advanced options (model / dir) so the session always starts clean.
133
+ const agentId = useDefaults ? "general" : (_state.selectedAgentId || "general");
132
134
  const adv = _state.advanced;
133
135
  const name = adv.name.trim() || _autoName(existingSessions);
134
136
 
135
137
  const payload = { name, agent_profile: agentId, source: "manual" };
136
- if (adv.workingDir.trim()) payload.working_dir = adv.workingDir.trim();
137
- if (adv.modelId) payload.model_id = adv.modelId;
138
+ if (!useDefaults) {
139
+ if (adv.workingDir.trim()) payload.working_dir = adv.workingDir.trim();
140
+ if (adv.modelId) payload.model_id = adv.modelId;
141
+ }
138
142
 
139
143
  const res = await fetch("/api/sessions", {
140
144
  method: "POST",
@@ -237,7 +237,11 @@ const VersionView = (() => {
237
237
  _closePopover();
238
238
  if (!_autoReloaded) {
239
239
  _autoReloaded = true;
240
- setTimeout(() => window.location.reload(), 800);
240
+ // Delay reload to ensure the server is fully ready after restart.
241
+ // /api/version responding OK does not guarantee the full server stack
242
+ // (static assets, routes) is ready; a short extra wait avoids a blank
243
+ // page caused by the browser hitting a not-yet-ready server.
244
+ setTimeout(() => window.location.reload(), 3000);
241
245
  }
242
246
  return;
243
247
  }
@@ -75,12 +75,11 @@ const I18n = (() => {
75
75
  "chat.send": "Send",
76
76
  "chat.edit.warn": "Editing this message will discard the conversation history, file snapshots, and token costs after it. Continue?",
77
77
  "chat.edit.warnSkip": "Don't show this again",
78
- "chat.empty.title": "Start the conversation",
79
- "chat.empty.subtitle": "Ask anything, or use a skill to get going.",
80
- "chat.empty.tip1": "Type / to browse skills",
81
- "chat.empty.tip2": "Attach images, PDFs, or docs for context",
82
- "chat.empty.tip3": "Shift+Enter for a new line",
83
- "chat.empty.tip4": "Each session is isolated, but long-term memories are shared across sessions",
78
+ "chat.empty.title": "How can I help you today?",
79
+ "chat.empty.subtitle": "Ask a question, or start with a skill.",
80
+ "chat.empty.tip1": "Research, write, and translate",
81
+ "chat.empty.tip2": "Summarize and organize information",
82
+ "chat.empty.tip3": "Upload an image or file for me to analyze",
84
83
 
85
84
  // ── Session list ──
86
85
  "sessions.empty": "No sessions yet",
@@ -162,6 +161,11 @@ const I18n = (() => {
162
161
  "sib.variant.default": "default",
163
162
  "sib.signal.tooltip": "Recent LLM latency",
164
163
  "sib.reasoning.tooltip": "Click to change reasoning effort",
164
+ "sib.id.label": "Session file",
165
+ "sib.status.tooltip": "Current session status",
166
+ "sib.mode.tooltip": "Permission mode — controls whether the agent asks before making changes",
167
+ "sib.tasks.tooltip": "Total number of tasks completed in this session",
168
+ "sib.cost.tooltip": "Estimated token cost for this session",
165
169
  "sib.reasoning.label": "Reasoning",
166
170
  "sib.reasoning.heading": "Reasoning effort",
167
171
  "sib.reasoning.hint": "Higher effort = deeper thinking, slower response.",
@@ -520,6 +524,7 @@ const I18n = (() => {
520
524
  "extensions.sort.downloads": "Most installed",
521
525
  "extensions.filter.all": "All",
522
526
  "extensions.filter.installed": "Installed",
527
+ "extensions.filter.brand": "Brand",
523
528
  "extensions.empty": "No extensions available yet.",
524
529
  "extensions.noResults": "No extensions match your search.",
525
530
  "extensions.loadFailed": "Could not reach the extension store.",
@@ -554,6 +559,7 @@ const I18n = (() => {
554
559
  "extensions.action.enable": "Enable",
555
560
  "extensions.action.remove": "Remove",
556
561
  "extensions.action.removeConfirm": "Remove this extension? Its files will be deleted. You can reinstall it later.",
562
+ "extensions.action.removePurgeData": "Also delete this extension's data (kept by default; reinstalling restores it)",
557
563
  "mcp.loading": "Loading…",
558
564
  "mcp.empty.title": "No MCP servers configured",
559
565
  "mcp.empty.body": "Create ~/.clacky/mcp.json to plug in MCP servers. The format matches Claude Desktop and Cursor, so existing configs work as-is.",
@@ -572,10 +578,10 @@ const I18n = (() => {
572
578
  "mcp.btn.hide": "Hide tools",
573
579
  "mcp.btn.refresh": "Refresh",
574
580
  "mcp.btn.remove": "Remove",
575
- "mcp.btn.askClacky": "Let Clacky help me set this up",
581
+ "mcp.btn.askClacky": "Let AI help me set this up",
576
582
  "mcp.cta.title": "Want to add a new MCP server?",
577
- "mcp.cta.body": "Tell Clacky in plain words — it'll pick the right package, ask for the parameters that matter, and verify it works.",
578
- "mcp.cta.button": "Ask Clacky to add one",
583
+ "mcp.cta.body": "Describe what you need in plain words — the AI will pick the right package, ask for the parameters that matter, and verify it works.",
584
+ "mcp.cta.button": "Ask AI to add one",
579
585
  "mcp.remove.confirm": "Remove MCP server '{{name}}'? Its tools will no longer be available.",
580
586
  "mcp.remove.error": "Failed to remove: {{msg}}",
581
587
  "mcp.loadError": "Failed to load MCP servers: {{msg}}",
@@ -657,6 +663,9 @@ const I18n = (() => {
657
663
  "settings.models.baseurl.variant.mainland_cn_coding": "Mainland · Coding Plan",
658
664
  "settings.models.baseurl.variant.international_payg": "International · Pay-as-you-go",
659
665
  "settings.models.baseurl.variant.international_coding": "International · Coding Plan",
666
+ "settings.models.baseurl.variant.ark_payg": "Pay-as-you-go",
667
+ "settings.models.baseurl.variant.ark_coding": "Coding Plan",
668
+ "settings.models.baseurl.variant.ark_agent": "Agent Plan",
660
669
  "settings.models.btn.save": "Save",
661
670
  "settings.models.btn.saving": "Saving…",
662
671
  "settings.models.btn.saved": "Saved ✓",
@@ -778,6 +787,13 @@ const I18n = (() => {
778
787
 
779
788
  "settings.accentColor.title": "Accent Color",
780
789
 
790
+ "settings.bgTheme.title": "Background",
791
+ "settings.bgTheme.light": "Light",
792
+ "settings.bgTheme.dark": "Dark",
793
+ "settings.bgTheme.dim": "Dim",
794
+ "settings.bgTheme.warm": "Warm",
795
+ "settings.bgTheme.black": "Blue Dark",
796
+
781
797
  // ── Onboard ──
782
798
  "onboard.title": "Welcome to {{brand}}",
783
799
  "onboard.subtitle": "Let's get you set up in a minute.",
@@ -812,6 +828,7 @@ const I18n = (() => {
812
828
  "onboard.manual.toggle": "Configure manually with your own API key",
813
829
  "onboard.provider.custom": "Custom",
814
830
  "provider.recommended": "Recommended",
831
+ "provider.name.volcengine_ark": "Volcengine Ark (Doubao)",
815
832
  "provider.promo.openclacky.title": "OpenClacky AI Keys",
816
833
  "provider.promo.openclacky.1": "All frontier models, one key — Claude, Gemini, DeepSeek & more.",
817
834
  "provider.promo.openclacky.2": "Top reasoning, best value, best vision — unified in one API.",
@@ -1049,12 +1066,11 @@ const I18n = (() => {
1049
1066
  "chat.send": "发送",
1050
1067
  "chat.edit.warn": "编辑此消息将丢弃后续对话历史、文件快照及已产生的 token 费用,是否继续?",
1051
1068
  "chat.edit.warnSkip": "不再提示",
1052
- "chat.empty.title": "开始新会话",
1053
- "chat.empty.subtitle": "直接提问,或用一个 Skill 启动。",
1054
- "chat.empty.tip1": "输入 / 浏览 Skill",
1055
- "chat.empty.tip2": "可粘贴或拖入图片、PDF、文档",
1056
- "chat.empty.tip3": "Shift+Enter 换行",
1057
- "chat.empty.tip4": "每个会话上下文独立,但长期记忆全局共享",
1069
+ "chat.empty.title": "有什么可以帮你?",
1070
+ "chat.empty.subtitle": "直接提问,或用一个技能开始。",
1071
+ "chat.empty.tip1": "查资料、写文案、做翻译",
1072
+ "chat.empty.tip2": "归纳总结、整理信息",
1073
+ "chat.empty.tip3": "上传图片或文件,让我帮你分析",
1058
1074
 
1059
1075
  // ── Session list ──
1060
1076
  "sessions.empty": "暂无会话",
@@ -1136,6 +1152,11 @@ const I18n = (() => {
1136
1152
  "sib.variant.default": "默认",
1137
1153
  "sib.signal.tooltip": "最近一次 LLM 响应延迟",
1138
1154
  "sib.reasoning.tooltip": "点击调整思考等级",
1155
+ "sib.id.label": "会话文件",
1156
+ "sib.status.tooltip": "当前会话状态",
1157
+ "sib.mode.tooltip": "权限模式 — 控制 Agent 执行操作前是否需要确认",
1158
+ "sib.tasks.tooltip": "本次会话累计完成的任务数",
1159
+ "sib.cost.tooltip": "本次会话的预估 Token 费用",
1139
1160
  "sib.reasoning.label": "思考",
1140
1161
  "sib.reasoning.heading": "思考等级",
1141
1162
  "sib.reasoning.hint": "等级越高,思考越深,响应越慢。",
@@ -1493,6 +1514,7 @@ const I18n = (() => {
1493
1514
  "extensions.sort.downloads": "安装最多",
1494
1515
  "extensions.filter.all": "全部",
1495
1516
  "extensions.filter.installed": "已安装",
1517
+ "extensions.filter.brand": "品牌扩展",
1496
1518
  "extensions.empty": "暂无可用扩展。",
1497
1519
  "extensions.noResults": "没有匹配的扩展。",
1498
1520
  "extensions.loadFailed": "无法连接扩展市场。",
@@ -1527,6 +1549,7 @@ const I18n = (() => {
1527
1549
  "extensions.action.enable": "启用",
1528
1550
  "extensions.action.remove": "移除",
1529
1551
  "extensions.action.removeConfirm": "确定移除此扩展?相关文件会被删除,之后可重新安装。",
1552
+ "extensions.action.removePurgeData": "同时删除该扩展的数据(默认保留,重新安装会自动恢复)",
1530
1553
  "mcp.loading": "加载中…",
1531
1554
  "mcp.empty.title": "尚未配置 MCP 服务",
1532
1555
  "mcp.empty.body": "新建 ~/.clacky/mcp.json 即可接入 MCP 服务。格式与 Claude Desktop、Cursor 一致,现有配置可直接复用。",
@@ -1545,10 +1568,10 @@ const I18n = (() => {
1545
1568
  "mcp.btn.hide": "收起",
1546
1569
  "mcp.btn.refresh": "刷新",
1547
1570
  "mcp.btn.remove": "移除",
1548
- "mcp.btn.askClacky": "让 Clacky 帮我配置",
1571
+ "mcp.btn.askClacky": "让 AI 帮我配置",
1549
1572
  "mcp.cta.title": "想接入新的 MCP 服务?",
1550
- "mcp.cta.body": "用一句大白话告诉 Clacky 想做什么——它会挑合适的包、问关键参数、跑通后回复你。",
1551
- "mcp.cta.button": "让 Clacky 帮我添加",
1573
+ "mcp.cta.body": "用一句大白话描述你想做什么——AI 会挑合适的包、问关键参数、跑通后回复你。",
1574
+ "mcp.cta.button": "让 AI 帮我添加",
1552
1575
  "mcp.remove.confirm": "移除 MCP 服务 '{{name}}'?相关工具将不再可用。",
1553
1576
  "mcp.remove.error": "移除失败:{{msg}}",
1554
1577
  "mcp.loadError": "加载 MCP 服务失败:{{msg}}",
@@ -1637,6 +1660,9 @@ const I18n = (() => {
1637
1660
  "settings.models.baseurl.variant.mainland_cn_coding": "大陆 · Coding Plan",
1638
1661
  "settings.models.baseurl.variant.international_payg": "海外 · 按量付费",
1639
1662
  "settings.models.baseurl.variant.international_coding": "海外 · Coding Plan",
1663
+ "settings.models.baseurl.variant.ark_payg": "按量付费",
1664
+ "settings.models.baseurl.variant.ark_coding": "Coding Plan",
1665
+ "settings.models.baseurl.variant.ark_agent": "Agent Plan",
1640
1666
  "settings.models.btn.save": "保存",
1641
1667
  "settings.models.btn.saving": "保存中…",
1642
1668
  "settings.models.btn.saved": "已保存 ✓",
@@ -1758,6 +1784,13 @@ const I18n = (() => {
1758
1784
 
1759
1785
  "settings.accentColor.title": "主色",
1760
1786
 
1787
+ "settings.bgTheme.title": "背景",
1788
+ "settings.bgTheme.light": "浅色",
1789
+ "settings.bgTheme.dark": "深色",
1790
+ "settings.bgTheme.dim": "暗灰",
1791
+ "settings.bgTheme.warm": "暖色",
1792
+ "settings.bgTheme.black": "蓝黑",
1793
+
1761
1794
  // ── Onboard ──
1762
1795
  "onboard.title": "欢迎使用 {{brand}}",
1763
1796
  "onboard.subtitle": "一分钟完成配置,马上开始。",
@@ -1792,6 +1825,7 @@ const I18n = (() => {
1792
1825
  "onboard.manual.toggle": "使用自己的 API Key 手动配置",
1793
1826
  "onboard.provider.custom": "自定义",
1794
1827
  "provider.recommended": "推荐",
1828
+ "provider.name.volcengine_ark": "火山方舟(豆包)",
1795
1829
  "provider.promo.openclacky.title": "推荐 OpenClacky AI Keys",
1796
1830
  "provider.promo.openclacky.1": "一个 Key 直通全球顶级模型 — Claude、Gemini、DeepSeek 等。",
1797
1831
  "provider.promo.openclacky.2": "最强推理 × 最优性价比 × 最佳视觉 — 一站式统一接入。",
@@ -54,6 +54,14 @@
54
54
  <div id="header-right">
55
55
  <span id="ext-slot-header-right" data-slot="header.right"></span>
56
56
 
57
+ <button id="reload-header" class="theme-toggle-btn" title="Reload" onclick="location.reload()">
58
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-sm">
59
+ <path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/>
60
+ <path d="M21 3v5h-5"/>
61
+ <path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/>
62
+ <path d="M8 16H3v5"/>
63
+ </svg>
64
+ </button>
57
65
  <button id="share-toggle-header" class="theme-toggle-btn" data-i18n-title="share.tooltip" title="Share">
58
66
  <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-sm">
59
67
  <circle cx="18" cy="5" r="3"/>
@@ -64,11 +72,7 @@
64
72
  </svg>
65
73
  </button>
66
74
  <button id="notify-toggle-header" class="theme-toggle-btn" data-i18n-title="notify.tooltip.off" title="Sound on task complete"></button>
67
- <button id="theme-toggle-header" class="theme-toggle-btn" title="Toggle theme">
68
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-sm">
69
- <path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>
70
- </svg>
71
- </button>
75
+
72
76
  </div>
73
77
  </header>
74
78
 
@@ -680,6 +684,7 @@
680
684
  <div class="extensions-toolbar">
681
685
  <div class="extensions-filter-tabs">
682
686
  <button type="button" class="extensions-filter-tab extensions-filter-tab-active" data-filter="all" data-i18n="extensions.filter.all">All</button>
687
+ <button type="button" class="extensions-filter-tab" data-filter="brand" id="tab-extensions-brand" data-i18n="extensions.filter.brand" style="display:none">Brand</button>
683
688
  <button type="button" class="extensions-filter-tab" data-filter="installed" data-i18n="extensions.filter.installed">Installed</button>
684
689
  </div>
685
690
  <div class="extensions-toolbar-right">
@@ -949,6 +954,43 @@
949
954
  </div>
950
955
  </section>
951
956
 
957
+ <!-- Background Theme section -->
958
+ <section class="settings-section" id="bg-theme-section">
959
+ <div class="settings-section-title">
960
+ <span data-i18n="settings.bgTheme.title">Background</span>
961
+ </div>
962
+ <div class="settings-bg-theme-cards">
963
+ <button class="settings-bg-theme-card bg-theme-dark" data-bg-theme="dark">
964
+ <div class="bg-card-preview">
965
+ <div class="bg-card-sidebar"></div>
966
+ <div class="bg-card-main"></div>
967
+ </div>
968
+ <span class="bg-card-label" data-i18n="settings.bgTheme.dark">Dark</span>
969
+ </button>
970
+ <button class="settings-bg-theme-card bg-theme-dim" data-bg-theme="dim">
971
+ <div class="bg-card-preview">
972
+ <div class="bg-card-sidebar"></div>
973
+ <div class="bg-card-main"></div>
974
+ </div>
975
+ <span class="bg-card-label" data-i18n="settings.bgTheme.dim">Dim</span>
976
+ </button>
977
+ <button class="settings-bg-theme-card bg-theme-warm" data-bg-theme="warm">
978
+ <div class="bg-card-preview">
979
+ <div class="bg-card-sidebar"></div>
980
+ <div class="bg-card-main"></div>
981
+ </div>
982
+ <span class="bg-card-label" data-i18n="settings.bgTheme.warm">Warm</span>
983
+ </button>
984
+ <button class="settings-bg-theme-card bg-theme-light" data-bg-theme="light">
985
+ <div class="bg-card-preview">
986
+ <div class="bg-card-sidebar"></div>
987
+ <div class="bg-card-main"></div>
988
+ </div>
989
+ <span class="bg-card-label" data-i18n="settings.bgTheme.light">Light</span>
990
+ </button>
991
+ </div>
992
+ </section>
993
+
952
994
  <!-- Accent Color section -->
953
995
  <section class="settings-section" id="accent-color-section">
954
996
  <div class="settings-section-title">
@@ -961,6 +1003,10 @@
961
1003
  <button class="settings-accent-swatch swatch-sunrise-orange" data-color="#F59E0B" title="Sunrise Orange"></button>
962
1004
  <button class="settings-accent-swatch swatch-rose-violet" data-color="#8B5CF6" title="Rose Violet"></button>
963
1005
  <button class="settings-accent-swatch swatch-coral-red" data-color="#EF4444" title="Coral Red"></button>
1006
+ <button class="settings-accent-swatch swatch-sky-blue" data-color="#38BDF8" title="Sky Blue"></button>
1007
+ <button class="settings-accent-swatch swatch-hot-pink" data-color="#EC4899" title="Hot Pink"></button>
1008
+ <button class="settings-accent-swatch swatch-warm-brown" data-color="#92745A" title="Warm Brown"></button>
1009
+ <button class="settings-accent-swatch swatch-slate-gray" data-color="#6B7280" title="Slate Gray"></button>
964
1010
  </div>
965
1011
  </section>
966
1012
  </div>
@@ -390,11 +390,13 @@ const Sessions = (() => {
390
390
  const tip1 = I18n.t("chat.empty.tip1");
391
391
  const tip2 = I18n.t("chat.empty.tip2");
392
392
  const tip3 = I18n.t("chat.empty.tip3");
393
- const tip4 = I18n.t("chat.empty.tip4");
394
393
  return `
395
394
  <div class="chat-empty-icon" aria-hidden="true">
396
- <svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
397
- <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
395
+ <svg xmlns="http://www.w3.org/2000/svg" width="34" height="34" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
396
+ <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/>
397
+ <circle cx="8.5" cy="12" r="0.9" fill="currentColor" stroke="none"/>
398
+ <circle cx="12" cy="12" r="0.9" fill="currentColor" stroke="none"/>
399
+ <circle cx="15.5" cy="12" r="0.9" fill="currentColor" stroke="none"/>
398
400
  </svg>
399
401
  </div>
400
402
  <div class="chat-empty-title">${escapeHtml(title)}</div>
@@ -403,7 +405,6 @@ const Sessions = (() => {
403
405
  <li>${escapeHtml(tip1)}</li>
404
406
  <li>${escapeHtml(tip2)}</li>
405
407
  <li>${escapeHtml(tip3)}</li>
406
- <li>${escapeHtml(tip4)}</li>
407
408
  </ul>
408
409
  `;
409
410
  }
@@ -503,7 +504,7 @@ const Sessions = (() => {
503
504
  if (_btnNewInline.disabled) return;
504
505
  _btnNewInline.disabled = true;
505
506
  try {
506
- const session = await NewSessionStore.createSession({ existingSessions: _sessions });
507
+ const session = await NewSessionStore.createSession({ existingSessions: _sessions, useDefaults: true });
507
508
  if (!session) return;
508
509
  NewSessionStore.reset();
509
510
  // Add to local list immediately so renderList shows it and findOrFetch
@@ -1271,11 +1272,16 @@ const Sessions = (() => {
1271
1272
 
1272
1273
  // Mark the last tool-item in a group as done (update status indicator).
1273
1274
  // collapsed: true → keep stdout hidden (history mode); false → show immediately (live mode).
1274
- function _completeLastToolItem(group, result, { collapsed = false } = {}) {
1275
+ function _completeLastToolItem(group, result, opts = {}) {
1275
1276
  const body = group.querySelector(".tool-group-body");
1276
1277
  const items = body.querySelectorAll(".tool-item");
1277
1278
  if (!items.length) return;
1278
- const last = items[items.length - 1];
1279
+ _completeToolItem(items[items.length - 1], result, opts);
1280
+ }
1281
+
1282
+ // Mark a specific tool-item element as done (update status + render result).
1283
+ function _completeToolItem(last, result, { collapsed = false } = {}) {
1284
+ if (!last) return;
1279
1285
  const status = last.querySelector(".tool-item-status");
1280
1286
  if (status) {
1281
1287
  status.className = "tool-item-status ok";
@@ -3248,13 +3254,19 @@ const Sessions = (() => {
3248
3254
  if (sibStatus) {
3249
3255
  sibStatus.innerHTML = `<span class="sib-dot"></span>${s.status || "idle"}`;
3250
3256
  sibStatus.className = `sib-status-${s.status || "idle"}`;
3257
+ sibStatus.title = I18n.t("sib.status.tooltip");
3251
3258
  }
3252
3259
 
3253
- // Session ID (shortfirst 8 chars). The span itself is the click
3254
- // trigger for the session actions dropdown (download, etc.).
3260
+ // Session ID labelshows localised "Session file" label with short hash suffix.
3261
+ // The span itself is the click trigger for the session actions dropdown (download, etc.).
3255
3262
  const sibId = $("sib-id");
3256
3263
  if (sibId) {
3257
- sibId.textContent = s.id ? s.id.slice(0, 8) : "";
3264
+ const shortHash = s.id ? s.id.slice(0, 8) : "";
3265
+ const lp = I18n.lang() === "zh" ? "(" : " (";
3266
+ const rp = I18n.lang() === "zh" ? ")" : ")";
3267
+ sibId.textContent = shortHash
3268
+ ? `${I18n.t("sib.id.label")}${lp}${shortHash}${rp}`
3269
+ : I18n.t("sib.id.label");
3258
3270
  sibId.title = s.id || "";
3259
3271
  if (s.id) {
3260
3272
  sibId.dataset.sessionId = s.id;
@@ -3277,6 +3289,7 @@ const Sessions = (() => {
3277
3289
  const sibSepAfterMode = document.querySelector(".sib-sep-after-mode");
3278
3290
  if (sibMode) {
3279
3291
  sibMode.textContent = s.permission_mode || "";
3292
+ sibMode.title = s.permission_mode ? I18n.t("sib.mode.tooltip") : "";
3280
3293
  sibMode.style.display = s.permission_mode ? "" : "none";
3281
3294
  }
3282
3295
  if (sibSepAfterMode) {
@@ -3331,7 +3344,10 @@ const Sessions = (() => {
3331
3344
 
3332
3345
  // Tasks
3333
3346
  const sibTasks = $("sib-tasks");
3334
- if (sibTasks) sibTasks.textContent = I18n.t("sessions.metaTasks", { n: s.total_tasks || 0 });
3347
+ if (sibTasks) {
3348
+ sibTasks.textContent = I18n.t("sessions.metaTasks", { n: s.total_tasks || 0 });
3349
+ sibTasks.title = I18n.t("sib.tasks.tooltip");
3350
+ }
3335
3351
 
3336
3352
  // Cost — show N/A when pricing is unknown (estimated)
3337
3353
  const sibCost = $("sib-cost");
@@ -3343,6 +3359,7 @@ const Sessions = (() => {
3343
3359
  } else {
3344
3360
  sibCost.textContent = "N/A";
3345
3361
  }
3362
+ sibCost.title = I18n.t("sib.cost.tooltip");
3346
3363
  }
3347
3364
 
3348
3365
  const bar = $("session-info-bar");
@@ -3484,9 +3501,21 @@ const Sessions = (() => {
3484
3501
  },
3485
3502
 
3486
3503
  // Update the last tool-item with a result status tick.
3504
+ // If _liveToolGroup was collapsed by an intervening info message (e.g.
3505
+ // "Subagent start/completed" during invoke_skill), fall back to the last
3506
+ // still-running .tool-item in the DOM.
3487
3507
  appendToolResult(result) {
3488
3508
  if (Sessions._liveToolGroup && Sessions._liveLastToolItem) {
3489
3509
  _completeLastToolItem(Sessions._liveToolGroup, result);
3510
+ return;
3511
+ }
3512
+ const messages = RenderTarget.current();
3513
+ if (messages) {
3514
+ const running = messages.querySelectorAll(".tool-item-status.running");
3515
+ if (running.length > 0) {
3516
+ const item = running[running.length - 1].closest(".tool-item");
3517
+ if (item) { _completeToolItem(item, result); return; }
3518
+ }
3490
3519
  }
3491
3520
  },
3492
3521
 
@@ -59,9 +59,15 @@ const Settings = (() => {
59
59
  _models.forEach((m, i) => _renderCard(container, m, i));
60
60
  }
61
61
 
62
+ function _providerDisplayName(p) {
63
+ if (!p) return I18n.t("settings.models.provider.custom");
64
+ const translated = p.name_key ? I18n.t(p.name_key) : null;
65
+ return (translated && translated !== p.name_key) ? translated : p.name;
66
+ }
67
+
62
68
  function _getProviderName(model) {
63
69
  const p = _findProviderByBaseUrl(model.base_url);
64
- return p ? p.name : I18n.t("settings.models.provider.custom");
70
+ return _providerDisplayName(p);
65
71
  }
66
72
 
67
73
  function _findProviderByBaseUrl(baseUrl) {
@@ -82,7 +88,7 @@ const Settings = (() => {
82
88
  const isDefault = model.type === "default";
83
89
  const isLite = model.type === "lite";
84
90
  const provider = _findProviderByBaseUrl(model.base_url);
85
- const providerName = provider ? provider.name : I18n.t("settings.models.provider.custom");
91
+ const providerName = _providerDisplayName(provider);
86
92
  const websiteUrl = provider && provider.website_url;
87
93
  const displayName = model.model || I18n.t("settings.models.unnamed");
88
94
 
@@ -266,7 +272,7 @@ const Settings = (() => {
266
272
  const dropdown = document.getElementById("model-modal-provider-dropdown");
267
273
  dropdown.innerHTML = `
268
274
  <div class="custom-select-option" data-value="">${I18n.t("settings.models.placeholder.provider")}</div>
269
- ${_providers.map(p => `<div class="custom-select-option" data-value="${p.id}" data-label="${_esc(p.name)}">${_esc(p.name)}${p.id === "openclacky" ? ` <span class="provider-badge-recommended">${I18n.t("provider.recommended")}</span>` : ""}</div>`).join("")}
275
+ ${_providers.map(p => { const dn = _providerDisplayName(p); return `<div class="custom-select-option" data-value="${p.id}" data-label="${_esc(dn)}">${_esc(dn)}${p.id === "openclacky" ? ` <span class="provider-badge-recommended">${I18n.t("provider.recommended")}</span>` : ""}</div>`; }).join("")}
270
276
  <div class="custom-select-option" data-value="custom">${I18n.t("settings.models.custom")}</div>
271
277
  `;
272
278
 
@@ -2207,6 +2213,7 @@ const Settings = (() => {
2207
2213
  _initFontBtns();
2208
2214
  _initCurrencyBtns();
2209
2215
  _initAccentColorBtns();
2216
+ _initBgThemeCards();
2210
2217
 
2211
2218
  // Re-render model cards when language changes (dynamic HTML, not data-i18n)
2212
2219
  document.addEventListener("langchange", () => {
@@ -2396,6 +2403,18 @@ const Settings = (() => {
2396
2403
  });
2397
2404
  }
2398
2405
 
2406
+ // ── Background Theme Cards ────────────────────────────────────────────
2407
+ function _initBgThemeCards() {
2408
+ // Sync initial active state from Theme module.
2409
+ const current = Theme.current();
2410
+ document.querySelectorAll("#bg-theme-section .settings-bg-theme-card").forEach(btn => {
2411
+ btn.classList.toggle("active", btn.dataset.bgTheme === current);
2412
+ btn.addEventListener("click", () => {
2413
+ Theme.applyBg(btn.dataset.bgTheme);
2414
+ });
2415
+ });
2416
+ }
2417
+
2399
2418
  // ── QR Code Lightbox ───────────────────────────────────────────────────
2400
2419
  // Sets up click-to-enlarge behaviour for the support QR code.
2401
2420
  // Safe to call multiple times — idempotent via a data attribute guard.