openclacky 1.5.2 → 1.5.4

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/.dockerignore +13 -0
  3. data/CHANGELOG.md +50 -0
  4. data/Dockerfile +21 -1
  5. data/README.md +26 -7
  6. data/README_CN.md +26 -7
  7. data/README_JA.md +26 -7
  8. data/lib/clacky/agent/message_compressor.rb +4 -29
  9. data/lib/clacky/agent/message_compressor_helper.rb +13 -15
  10. data/lib/clacky/agent/session_serializer.rb +6 -0
  11. data/lib/clacky/agent.rb +13 -1
  12. data/lib/clacky/agent_config.rb +42 -11
  13. data/lib/clacky/brand_config.rb +32 -12
  14. data/lib/clacky/client.rb +7 -0
  15. data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +15 -0
  16. data/lib/clacky/default_extensions/ext-studio/api/handler.rb +2 -6
  17. data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +11 -4
  18. data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +5 -1
  19. data/lib/clacky/default_parsers/xlsx_parser.py +66 -0
  20. data/lib/clacky/extension/api_extension.rb +3 -2
  21. data/lib/clacky/message_format/anthropic.rb +27 -1
  22. data/lib/clacky/message_format/open_ai.rb +92 -4
  23. data/lib/clacky/providers.rb +67 -14
  24. data/lib/clacky/server/http_server.rb +361 -36
  25. data/lib/clacky/server/project_manager.rb +150 -0
  26. data/lib/clacky/server/server_master.rb +7 -0
  27. data/lib/clacky/server/session_registry.rb +14 -1
  28. data/lib/clacky/session_manager.rb +5 -4
  29. data/lib/clacky/tools/file_reader.rb +2 -1
  30. data/lib/clacky/utils/model_pricing.rb +77 -18
  31. data/lib/clacky/utils/parser_manager.rb +115 -7
  32. data/lib/clacky/version.rb +1 -1
  33. data/lib/clacky/web/app.css +1053 -143
  34. data/lib/clacky/web/app.js +19 -5
  35. data/lib/clacky/web/components/code-editor.js +50 -12
  36. data/lib/clacky/web/components/notify.js +139 -22
  37. data/lib/clacky/web/core/aside.js +21 -0
  38. data/lib/clacky/web/features/billing/view.js +2 -1
  39. data/lib/clacky/web/features/brand/store.js +3 -1
  40. data/lib/clacky/web/features/brand/view.js +29 -5
  41. data/lib/clacky/web/features/extensions/store.js +15 -6
  42. data/lib/clacky/web/features/extensions/view.js +8 -4
  43. data/lib/clacky/web/features/new-session/view.js +2 -2
  44. data/lib/clacky/web/features/trash/store.js +23 -9
  45. data/lib/clacky/web/features/trash/view.js +51 -0
  46. data/lib/clacky/web/features/workspace/store.js +9 -0
  47. data/lib/clacky/web/features/workspace/view.js +8 -1
  48. data/lib/clacky/web/i18n.js +99 -4
  49. data/lib/clacky/web/index.html +70 -17
  50. data/lib/clacky/web/projects.js +1143 -0
  51. data/lib/clacky/web/sessions.js +318 -537
  52. data/lib/clacky/web/settings.js +146 -7
  53. data/lib/clacky/web/ws-dispatcher.js +59 -1
  54. data/scripts/build/src/install_system_deps.sh.cc +11 -3
  55. data/scripts/install_system_deps.sh +11 -3
  56. metadata +5 -2
  57. data/lib/clacky/default_parsers/xlsx_parser.rb +0 -121
@@ -20,6 +20,7 @@ const TrashStore = (() => {
20
20
  let _sessions = [];
21
21
  let _sessionTotals = { count: 0, size: 0 };
22
22
  let _filterSource = SOURCES.ALL;
23
+ let _filterProjectId = null; // null = all projects
23
24
  let _loading = false;
24
25
 
25
26
  const _listeners = {};
@@ -48,19 +49,23 @@ const TrashStore = (() => {
48
49
  get files() { return _files; },
49
50
  get totals() { return _totals; },
50
51
  get sessions() {
51
- if (_filterSource === SOURCES.ALL) return _sessions;
52
- return _filterSource === SOURCES.CRON
53
- ? _sessions.filter(s => s.source === SOURCES.CRON)
54
- : _sessions.filter(s => s.source !== SOURCES.CRON);
52
+ let list = _sessions;
53
+ if (_filterSource !== SOURCES.ALL) {
54
+ list = _filterSource === SOURCES.CRON
55
+ ? list.filter(s => s.source === SOURCES.CRON)
56
+ : list.filter(s => s.source !== SOURCES.CRON);
57
+ }
58
+ if (_filterProjectId !== null) {
59
+ list = list.filter(s => String(s.project_id ?? "") === _filterProjectId);
60
+ }
61
+ return list;
55
62
  },
56
63
  get sessionTotals() {
57
- const filtered = _filterSource === SOURCES.ALL ? _sessions
58
- : _filterSource === SOURCES.CRON
59
- ? _sessions.filter(s => s.source === SOURCES.CRON)
60
- : _sessions.filter(s => s.source !== SOURCES.CRON);
64
+ const filtered = state.sessions;
61
65
  return { count: filtered.length, size: filtered.reduce((sum, s) => sum + (s.file_size || 0), 0) };
62
66
  },
63
- get filterSource() { return _filterSource; },
67
+ get filterSource() { return _filterSource; },
68
+ get filterProjectId() { return _filterProjectId; },
64
69
  orphanCount() { return _files.filter(f => _isOrphanRoot(f.project_root)).length; },
65
70
  isOrphan(file) { return _isOrphanRoot(file.project_root); },
66
71
  };
@@ -76,6 +81,14 @@ const TrashStore = (() => {
76
81
  _emit("trash:sessionsChanged");
77
82
  },
78
83
 
84
+ setFilterProject(projectId) {
85
+ // projectId: string/number ID or null/empty (= all)
86
+ const normalized = (projectId != null && projectId !== "") ? String(projectId) : null;
87
+ if (normalized === _filterProjectId) return;
88
+ _filterProjectId = normalized;
89
+ _emit("trash:sessionsChanged");
90
+ },
91
+
79
92
  async loadFiles() {
80
93
  if (_loading) return;
81
94
  _loading = true;
@@ -166,6 +179,7 @@ const TrashStore = (() => {
166
179
  if (restored && typeof Sessions !== "undefined") {
167
180
  Sessions.add(restored);
168
181
  Sessions.renderList();
182
+ if (typeof Projects !== "undefined") Projects.renderSection();
169
183
  }
170
184
  return { ok: true, session: restored };
171
185
  } catch (e) {
@@ -49,6 +49,32 @@ const TrashView = (() => {
49
49
  return d.toLocaleDateString();
50
50
  }
51
51
 
52
+ // ── Project filter select helpers ────────────────────────────────────
53
+
54
+ function _refreshProjectOptions() {
55
+ const sel = $("trash-filter-project");
56
+ if (!sel) return;
57
+
58
+ const projects = (typeof Projects !== "undefined") ? Projects.all() : [];
59
+ const currentVal = sel.value;
60
+
61
+ // Rebuild options: keep the "all" placeholder, then one per project
62
+ sel.innerHTML = `<option value="">${I18n.t ? I18n.t("trash.filterAllProjects") : "所有项目"}</option>`;
63
+ projects.forEach(p => {
64
+ const opt = document.createElement("option");
65
+ opt.value = p.id;
66
+ opt.textContent = p.name || p.id;
67
+ sel.appendChild(opt);
68
+ });
69
+
70
+ // Restore previous selection if still valid
71
+ if (currentVal && projects.some(p => String(p.id) === currentVal)) {
72
+ sel.value = currentVal;
73
+ } else {
74
+ sel.value = "";
75
+ }
76
+ }
77
+
52
78
  // ── Tab switching ────────────────────────────────────────────────────
53
79
 
54
80
  function _switchTab(tab) {
@@ -221,6 +247,7 @@ const TrashView = (() => {
221
247
  if (btnOld) btnOld.disabled = sessions.length === 0;
222
248
  if (btnAll) btnAll.disabled = sessions.length === 0;
223
249
  if (filterBar) filterBar.style.display = "";
250
+ _refreshProjectOptions();
224
251
 
225
252
  if (sessions.length === 0) {
226
253
  list.innerHTML = `<div class="trash-empty">${_t("trash.noSessionTrash")}</div>`;
@@ -242,6 +269,21 @@ const TrashView = (() => {
242
269
  const whenStr = _humanTime(session.deleted_at || session.created_at);
243
270
  const taskLabel = I18n.t("trash.sessionTasks", { n: taskCount });
244
271
 
272
+ // Project badge — resolve name + color from Projects store if available
273
+ let projectBadgeHtml = "";
274
+ if (session.project_id && typeof Projects !== "undefined") {
275
+ const proj = Projects.find(session.project_id);
276
+ if (proj) {
277
+ const projName = escapeHtml(proj.name || "");
278
+ const dotColor = proj.color ? `style="background:${escapeHtml(proj.color)}"` : "";
279
+ projectBadgeHtml = `
280
+ <span class="trash-session-project-badge" title="${projName}">
281
+ <span class="trash-session-project-dot" ${dotColor}></span>
282
+ ${projName}
283
+ </span>`;
284
+ }
285
+ }
286
+
245
287
  card.innerHTML = `
246
288
  <div class="trash-session-card-info">
247
289
  <div class="trash-session-card-name" title="${escapeHtml(name)}">${escapeHtml(name)}</div>
@@ -250,6 +292,7 @@ const TrashView = (() => {
250
292
  <span>${escapeHtml(taskLabel)}</span>
251
293
  <span>${sizeStr}</span>
252
294
  <span title="${escapeHtml(session.deleted_at || session.created_at || '')}">${escapeHtml(whenStr)}</span>
295
+ ${projectBadgeHtml}
253
296
  </div>
254
297
  </div>
255
298
  <div class="trash-session-card-actions">
@@ -419,6 +462,14 @@ const TrashView = (() => {
419
462
  document.querySelectorAll(".trash-filter-chip").forEach(c => c.classList.toggle("active", c.dataset.filter === val));
420
463
  });
421
464
  });
465
+
466
+ // Project filter select
467
+ const projSel = $("trash-filter-project");
468
+ if (projSel) {
469
+ projSel.addEventListener("change", () => {
470
+ Trash.setFilterProject(projSel.value || null);
471
+ });
472
+ }
422
473
  }
423
474
 
424
475
  function _subscribe() {
@@ -102,6 +102,15 @@ const WorkspaceStore = (() => {
102
102
  if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
103
103
  return resp.text();
104
104
  },
105
+
106
+ async saveFileText(entry, content) {
107
+ const resp = await fetch("/api/file-action", {
108
+ method: "POST",
109
+ headers: { "Content-Type": "application/json" },
110
+ body: JSON.stringify({ path: _absPath(entry.path), action: "save", content })
111
+ });
112
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
113
+ },
105
114
  };
106
115
 
107
116
  return Workspace;
@@ -226,7 +226,14 @@ const WorkspaceView = (() => {
226
226
  }
227
227
  try {
228
228
  const text = await Workspace.fetchFileText(entry);
229
- CodeEditor.open({ filename: entry.name, title: entry.name, content: text, readOnly: true });
229
+ CodeEditor.open({
230
+ filename: entry.name,
231
+ title: entry.name,
232
+ content: text,
233
+ onSave: async (content) => {
234
+ await Workspace.saveFileText(entry, content);
235
+ }
236
+ });
230
237
  } catch (err) {
231
238
  console.error("preview failed:", err);
232
239
  Modal.toast(t("workspace.previewFailed"), "error");
@@ -15,6 +15,7 @@ const I18n = (() => {
15
15
  en: {
16
16
  // ── Sidebar ──
17
17
  "sidebar.chat": "Chat",
18
+ "sidebar.projects": "Projects",
18
19
  "sidebar.config": "Config",
19
20
  "sidebar.tasks": "Task Management",
20
21
  "sidebar.skills": "Skill Management",
@@ -28,6 +29,31 @@ const I18n = (() => {
28
29
  "sidebar.settings": "Settings",
29
30
  "sidebar.billing": "Usage",
30
31
 
32
+ // ── Projects ──
33
+ "projects.defaultName": "New Project",
34
+ "projects.create.title": "New Project",
35
+ "projects.create.label": "Project name",
36
+ "projects.create.placeholder": "Project name",
37
+ "projects.create.dirPlaceholder": "Working directory (optional)",
38
+ "projects.create.sourceFolders": "Source folders",
39
+ "projects.create.folderHint": "Add a working folder",
40
+ "projects.create.submit": "Create Project",
41
+ "projects.create.colorLabel": "Color",
42
+ "projects.create.iconLabel": "Icon",
43
+ "projects.picker.done": "Done",
44
+ "projects.emptyHint": "No projects yet",
45
+ "projects.noSessions": "No sessions",
46
+ "projects.tooltip.sessions": "{n} sessions",
47
+ "projects.organize.grouped": "By Project",
48
+ "projects.organize.flat": "In a flat list",
49
+ "projects.organize.sortLabel": "Sort by",
50
+ "projects.organize.sortPriority": "Priority",
51
+ "projects.organize.sortRecent": "Last updated",
52
+ "projects.menu.rename": "Rename",
53
+ "projects.menu.delete": "Delete",
54
+ "projects.delete.confirm": "Delete project \"{{name}}\"? All sessions in this project will also be deleted.",
55
+ "projects.modal.name": "Project Name",
56
+
31
57
  // ── Welcome screen ──
32
58
  "sessions.new.title": "Start a new session",
33
59
  "sessions.new.subtitle": "Pick who you want to chat with, then type your first message.",
@@ -338,6 +364,7 @@ const I18n = (() => {
338
364
  "trash.filterAll": "All",
339
365
  "trash.filterCron": "Scheduled",
340
366
  "trash.filterManual": "Regular",
367
+ "trash.filterAllProjects": "All Projects",
341
368
  "trash.sessionAutoCleanHint": "The system keeps the most recent 200 sessions; older ones are moved here.",
342
369
 
343
370
  // ── Relative time (used by _humanTime helper) ──
@@ -561,6 +588,7 @@ const I18n = (() => {
561
588
  "extensions.action.remove": "Remove",
562
589
  "extensions.action.removeConfirm": "Remove this extension? Its files will be deleted. You can reinstall it later.",
563
590
  "extensions.action.removePurgeData": "Also delete this extension's data (kept by default; reinstalling restores it)",
591
+ "ext.refresh_hint": "Reload Extensions",
564
592
  "mcp.loading": "Loading…",
565
593
  "mcp.empty.title": "No MCP servers configured",
566
594
  "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.",
@@ -669,6 +697,8 @@ const I18n = (() => {
669
697
  "settings.models.baseurl.variant.ark_agent": "Agent Plan",
670
698
  "settings.models.baseurl.variant.openclacky_primary": "Primary (Global)",
671
699
  "settings.models.baseurl.variant.openclacky_secondary": "Secondary (China)",
700
+ "settings.models.baseurl.variant.mimo_payg": "Pay-as-you-go",
701
+ "settings.models.baseurl.variant.mimo_token_plan": "Token Plan",
672
702
  "settings.models.btn.save": "Save",
673
703
  "settings.models.btn.saving": "Saving…",
674
704
  "settings.models.btn.saved": "Saved ✓",
@@ -740,6 +770,17 @@ const I18n = (() => {
740
770
  "settings.backup.lastOk": "Last backup: {{time}}",
741
771
  "settings.backup.lastError": "Backup failed: {{msg}}",
742
772
  "settings.brand.title": "Brand & License",
773
+ "settings.brand.source.desc": "Choose the platform source for licenses and marketplace content. Changing it unbinds the current license and removes brand Skills.",
774
+ "settings.brand.source.label": "Platform Source",
775
+ "settings.brand.source.save": "Save",
776
+ "settings.brand.source.restore": "Restore",
777
+ "settings.brand.source.saving": "Switching…",
778
+ "settings.brand.source.saved": "Source saved.",
779
+ "settings.brand.source.required": "Enter a platform source URL.",
780
+ "settings.brand.source.invalid": "Use an HTTP/HTTPS origin without credentials, a path, query parameters, or a fragment.",
781
+ "settings.brand.source.confirm": "Changing the platform source will unbind the current license and remove all installed brand Skills. You will need to activate again on the new source. Continue?",
782
+ "settings.brand.source.reconnecting": "Source updated. Reconnecting…",
783
+ "settings.brand.source.reconnectFailed": "The source was saved, but the server is still reconnecting. Refresh this page in a moment.",
743
784
  "settings.brand.label.brand": "Brand",
744
785
  "settings.brand.label.status": "Status",
745
786
  "settings.brand.label.expires": "Expires",
@@ -869,8 +910,15 @@ const I18n = (() => {
869
910
 
870
911
  "header.owner.tooltip": "Creator — click to open Extension & Creation",
871
912
  "header.cmdbar.placeholder": "Search sessions…",
872
- "notify.tooltip.on": "Sound on task complete: ON (click to mute)",
873
- "notify.tooltip.off": "Sound on task complete: OFF (click to enable)",
913
+ "notify.tooltip.on": "Task alerts ON — sound + desktop notification (click to mute)",
914
+ "notify.tooltip.off": "Task alerts OFF — click to enable sound + desktop notification",
915
+ "notify.tooltip.onPending": "Task alerts ON — sound enabled; allow desktop notifications when the browser prompts",
916
+ "notify.tooltip.onDenied": "Task alerts ON — sound only (desktop notifications blocked in browser settings)",
917
+ "notify.tooltip.onNoDesktop":"Task alerts ON — sound only (this browser has no desktop notifications)",
918
+ "notify.toast.denied": "Sound alerts enabled. Desktop notifications are blocked in browser settings.",
919
+ "notify.toast.noDesktop": "Sound alerts enabled. This browser has no desktop notifications.",
920
+ "notify.desktop.title": "Task complete",
921
+ "notify.desktop.body": "A background task finished. Click to open it.",
874
922
 
875
923
  "header.refresh": "Refresh",
876
924
  "header.theme.toDark": "Switch to dark mode",
@@ -1013,6 +1061,7 @@ const I18n = (() => {
1013
1061
  zh: {
1014
1062
  // ── Sidebar ──
1015
1063
  "sidebar.chat": "会话",
1064
+ "sidebar.projects": "项目",
1016
1065
  "sidebar.config": "配置",
1017
1066
  "sidebar.tasks": "任务管理",
1018
1067
  "sidebar.skills": "技能管理",
@@ -1026,6 +1075,30 @@ const I18n = (() => {
1026
1075
  "sidebar.settings": "设置",
1027
1076
  "sidebar.billing": "用量信息",
1028
1077
 
1078
+ // ── Projects ──
1079
+ "projects.defaultName": "新项目",
1080
+ "projects.create.title": "新建项目",
1081
+ "projects.create.label": "项目名称",
1082
+ "projects.create.placeholder": "项目名称",
1083
+ "projects.create.sourceFolders": "工作目录",
1084
+ "projects.create.folderHint": "添加工作文件夹",
1085
+ "projects.create.submit": "创建项目",
1086
+ "projects.create.colorLabel": "颜色",
1087
+ "projects.create.iconLabel": "图标",
1088
+ "projects.picker.done": "完成",
1089
+ "projects.emptyHint": "暂无项目",
1090
+ "projects.noSessions": "暂无会话",
1091
+ "projects.tooltip.sessions": "{n} 个会话",
1092
+ "projects.organize.grouped": "按项目分组",
1093
+ "projects.organize.flat": "在一个列表中",
1094
+ "projects.organize.sortLabel": "排序方式",
1095
+ "projects.organize.sortPriority": "优先级",
1096
+ "projects.organize.sortRecent": "最近更新",
1097
+ "projects.menu.rename": "重命名",
1098
+ "projects.menu.delete": "删除",
1099
+ "projects.delete.confirm": "删除项目「{{name}}」?该项目下的所有会话也将被删除。",
1100
+ "projects.modal.name": "项目名称",
1101
+
1029
1102
  // ── Welcome screen ──
1030
1103
  "sessions.new.title": "开始新会话",
1031
1104
  "sessions.new.subtitle": "选一个 Agent,然后输入第一条消息。",
@@ -1334,6 +1407,7 @@ const I18n = (() => {
1334
1407
  "trash.filterAll": "全部",
1335
1408
  "trash.filterCron": "定时任务",
1336
1409
  "trash.filterManual": "普通会话",
1410
+ "trash.filterAllProjects": "所有项目",
1337
1411
  "trash.sessionAutoCleanHint": "系统自动保留最近 200 个会话,超出后自动移入此处。",
1338
1412
 
1339
1413
  // ── Relative time (used by _humanTime helper) ──
@@ -1556,6 +1630,7 @@ const I18n = (() => {
1556
1630
  "extensions.action.remove": "移除",
1557
1631
  "extensions.action.removeConfirm": "确定移除此扩展?相关文件会被删除,之后可重新安装。",
1558
1632
  "extensions.action.removePurgeData": "同时删除该扩展的数据(默认保留,重新安装会自动恢复)",
1633
+ "ext.refresh_hint": "刷新扩展",
1559
1634
  "mcp.loading": "加载中…",
1560
1635
  "mcp.empty.title": "尚未配置 MCP 服务",
1561
1636
  "mcp.empty.body": "新建 ~/.clacky/mcp.json 即可接入 MCP 服务。格式与 Claude Desktop、Cursor 一致,现有配置可直接复用。",
@@ -1671,6 +1746,8 @@ const I18n = (() => {
1671
1746
  "settings.models.baseurl.variant.ark_agent": "Agent Plan",
1672
1747
  "settings.models.baseurl.variant.openclacky_primary": "主节点(全球)",
1673
1748
  "settings.models.baseurl.variant.openclacky_secondary": "备用节点(中国)",
1749
+ "settings.models.baseurl.variant.mimo_payg": "按量付费",
1750
+ "settings.models.baseurl.variant.mimo_token_plan": "Token Plan",
1674
1751
  "settings.models.btn.save": "保存",
1675
1752
  "settings.models.btn.saving": "保存中…",
1676
1753
  "settings.models.btn.saved": "已保存 ✓",
@@ -1742,6 +1819,17 @@ const I18n = (() => {
1742
1819
  "settings.backup.lastOk": "上次备份:{{time}}",
1743
1820
  "settings.backup.lastError": "备份失败:{{msg}}",
1744
1821
  "settings.brand.title": "品牌 & 授权",
1822
+ "settings.brand.source.desc": "选择获取授权和市场内容的平台源。切换后将解绑当前许可证,并删除品牌 Skill。",
1823
+ "settings.brand.source.label": "平台源",
1824
+ "settings.brand.source.save": "保存",
1825
+ "settings.brand.source.restore": "还原",
1826
+ "settings.brand.source.saving": "正在切换…",
1827
+ "settings.brand.source.saved": "平台源已保存。",
1828
+ "settings.brand.source.required": "请输入平台源地址。",
1829
+ "settings.brand.source.invalid": "请填写不含凭据、路径、查询参数或片段的 HTTP/HTTPS Origin。",
1830
+ "settings.brand.source.confirm": "切换平台源将解绑当前许可证,并删除所有已安装的品牌 Skill。切换后需要在新源重新激活,是否继续?",
1831
+ "settings.brand.source.reconnecting": "平台源已更新,正在重新连接…",
1832
+ "settings.brand.source.reconnectFailed": "平台源已保存,但服务仍在重新连接,请稍后刷新页面。",
1745
1833
  "settings.brand.label.brand": "品牌",
1746
1834
  "settings.brand.label.status": "状态",
1747
1835
  "settings.brand.label.expires": "到期时间",
@@ -1871,8 +1959,15 @@ const I18n = (() => {
1871
1959
 
1872
1960
  "header.owner.tooltip": "创作者 — 点击进入扩展与创作",
1873
1961
  "header.cmdbar.placeholder": "搜索会话…",
1874
- "notify.tooltip.on": "任务完成提示音:已开启(点击关闭)",
1875
- "notify.tooltip.off": "任务完成提示音:已关闭(点击开启)",
1962
+ "notify.tooltip.on": "任务完成提醒:已开启(声音 + 桌面通知,点击关闭)",
1963
+ "notify.tooltip.off": "任务完成提醒:已关闭(点击开启声音 + 桌面通知)",
1964
+ "notify.tooltip.onPending": "任务完成提醒:已开启声音;请在浏览器弹窗中允许桌面通知",
1965
+ "notify.tooltip.onDenied": "任务完成提醒:仅声音(桌面通知已被浏览器拦截,可在设置中允许)",
1966
+ "notify.tooltip.onNoDesktop":"任务完成提醒:仅声音(当前浏览器不支持桌面通知)",
1967
+ "notify.toast.denied": "已开启声音提醒。桌面通知被浏览器拦截,可在设置中允许。",
1968
+ "notify.toast.noDesktop": "已开启声音提醒。当前浏览器不支持桌面通知。",
1969
+ "notify.desktop.title": "任务完成",
1970
+ "notify.desktop.body": "有后台任务已完成,点击打开。",
1876
1971
 
1877
1972
  "header.refresh": "刷新",
1878
1973
  "header.theme.toDark": "切换到深色模式",
@@ -54,14 +54,7 @@
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
58
  <button id="share-toggle-header" class="theme-toggle-btn" data-i18n-title="share.tooltip" title="Share">
66
59
  <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">
67
60
  <circle cx="18" cy="5" r="3"/>
@@ -71,7 +64,7 @@
71
64
  <line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
72
65
  </svg>
73
66
  </button>
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="notify-toggle-header" class="theme-toggle-btn" data-i18n-title="notify.tooltip.off" title="Task alerts"></button>
75
68
 
76
69
  </div>
77
70
  </header>
@@ -129,15 +122,21 @@
129
122
  <!-- Chat Group -->
130
123
  <div id="chat-section">
131
124
  <!-- Header: "Sessions" label + [+ New] button -->
132
- <div class="sidebar-divider">
133
- <span data-i18n="sidebar.chat">Sessions</span>
125
+ <div class="sidebar-divider sidebar-divider--no-rule" id="chat-section-header">
126
+ <span class="chat-section-title">
127
+ <span data-i18n="sidebar.chat">Sessions</span>
128
+ <!-- Chevron: hidden by default, shown on hover or when collapsed -->
129
+ <svg class="chat-section-chevron" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
130
+ <path d="M6 9l6 6 6-6"/>
131
+ </svg>
132
+ </span>
134
133
  <div class="sidebar-divider-actions">
135
134
  <div class="btn-split-wrap">
136
135
  <button id="btn-new-session-inline" class="btn-split-main" title="New Session">
137
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
138
- <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
139
- <line x1="12" y1="8" x2="12" y2="14" stroke="currentColor" stroke-width="1.75" stroke-linecap="round"/>
140
- <line x1="9" y1="11" x2="15" y2="11" stroke="currentColor" stroke-width="1.75" stroke-linecap="round"/>
136
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
137
+ <path d="M7.9 20A9 9 0 1 0 4 16.1L2 22z"/>
138
+ <path d="M12 8v8"/>
139
+ <path d="M8 12h8"/>
141
140
  </svg>
142
141
  <span data-i18n="sessions.newSession">New Session</span>
143
142
  </button>
@@ -173,6 +172,31 @@
173
172
  <!-- Load more button rendered dynamically by Sessions.renderList() -->
174
173
  </div>
175
174
 
175
+ <!-- Projects Group -->
176
+ <div id="project-section">
177
+ <div class="sidebar-divider" id="projects-section-header">
178
+ <span class="projects-section-title">
179
+ <span data-i18n="sidebar.projects">Projects</span>
180
+ <svg class="projects-section-chevron" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
181
+ <path d="M6 9l6 6 6-6"/>
182
+ </svg>
183
+ </span>
184
+ <div class="sidebar-divider-actions">
185
+ <button id="btn-projects-organize" class="btn-icon-sm" title="Organize" aria-label="Organize projects">
186
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
187
+ <circle cx="5" cy="12" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="19" cy="12" r="2"/>
188
+ </svg>
189
+ </button>
190
+ <button id="btn-new-project" class="btn-icon-sm" title="New Project" aria-label="New project">
191
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
192
+ <line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
193
+ </svg>
194
+ </button>
195
+ </div>
196
+ </div>
197
+ <div id="project-list"></div>
198
+ </div>
199
+
176
200
  <!-- MAIN nav slot: default landing spot for extension menus. Between
177
201
  Sessions and Config so ext entries feel like standard nav items. -->
178
202
  <div id="ext-slot-sidebar-nav" data-slot="sidebar.nav"></div>
@@ -728,6 +752,14 @@
728
752
  <button class="trash-filter-chip active" data-filter="all" data-i18n="trash.filterAll">All</button>
729
753
  <button class="trash-filter-chip" data-filter="manual" data-i18n="trash.filterManual">Regular</button>
730
754
  <button class="trash-filter-chip" data-filter="cron" data-i18n="trash.filterCron">Scheduled</button>
755
+ <div class="trash-filter-project-wrap" id="trash-filter-project-wrap">
756
+ <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="trash-filter-project-icon">
757
+ <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
758
+ </svg>
759
+ <select id="trash-filter-project" class="trash-filter-project-select">
760
+ <option value="" data-i18n="trash.filterAllProjects">所有项目</option>
761
+ </select>
762
+ </div>
731
763
  </div>
732
764
  <span class="trash-summary" id="trash-summary"></span>
733
765
  <div class="trash-actions">
@@ -1073,6 +1105,26 @@
1073
1105
  <span data-i18n="settings.brand.title">Brand &amp; License</span>
1074
1106
  </div>
1075
1107
 
1108
+ <!-- Platform source -->
1109
+ <div class="platform-source-card">
1110
+ <p class="platform-source-desc" data-i18n="settings.brand.source.desc">
1111
+ Choose the platform source for licenses and marketplace content. Changing it unbinds the current license and removes brand Skills.
1112
+ </p>
1113
+ <div class="platform-source-field">
1114
+ <label class="platform-source-label" for="settings-clacky-license-server" data-i18n="settings.brand.source.label">Platform Source</label>
1115
+ <div class="platform-source-row">
1116
+ <input type="url" id="settings-clacky-license-server" class="field-input"
1117
+ placeholder="https://www.openclacky.com"
1118
+ autocomplete="off" spellcheck="false">
1119
+ <button type="button" id="btn-save-clacky-license-server" class="btn-settings-action"
1120
+ data-i18n="settings.brand.source.save">Save</button>
1121
+ <button type="button" id="btn-restore-clacky-license-server" class="btn-settings-action"
1122
+ data-i18n="settings.brand.source.restore">Restore</button>
1123
+ </div>
1124
+ <div id="settings-clacky-license-server-status" class="model-test-result"></div>
1125
+ </div>
1126
+ </div>
1127
+
1076
1128
  <!-- Current brand status (shown when already activated) -->
1077
1129
  <div id="brand-status-card" class="brand-status-card" style="display:none">
1078
1130
  <div class="brand-status-main">
@@ -1311,8 +1363,7 @@
1311
1363
  <input type="password" id="model-modal-apikey" class="field-input api-key-input" placeholder="sk-..." autocomplete="off">
1312
1364
  <button class="btn-toggle-key" id="model-modal-toggle-key" type="button" title="Show/hide key">
1313
1365
  <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1314
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
1315
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
1366
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"></path>
1316
1367
  </svg>
1317
1368
  </button>
1318
1369
  </div>
@@ -1531,6 +1582,7 @@
1531
1582
  <div id="toast-stack" class="toast-stack" aria-live="polite" aria-atomic="true"></div>
1532
1583
 
1533
1584
  <!-- Rename Session Modal -->
1585
+ <!-- Project picker popup (floats near the trigger button) -->
1534
1586
  <div id="rename-modal-overlay" class="modal-overlay" style="display:none">
1535
1587
  <div class="modal-box sm">
1536
1588
  <div class="modal-header">
@@ -1570,6 +1622,7 @@
1570
1622
  <script src="/ws.js"></script>
1571
1623
  <script src="/ws-dispatcher.js"></script>
1572
1624
  <script src="/sessions.js"></script>
1625
+ <script src="/projects.js"></script>
1573
1626
  <script src="/features/workspace/store.js"></script>
1574
1627
  <script src="/features/workspace/view.js"></script>
1575
1628
  <script src="/components/datepicker.js"></script>