openclacky 1.3.5 → 1.3.7

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 (108) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -0
  3. data/lib/clacky/agent/message_compressor.rb +32 -8
  4. data/lib/clacky/agent/message_compressor_helper.rb +113 -12
  5. data/lib/clacky/agent.rb +8 -4
  6. data/lib/clacky/agent_profile.rb +121 -53
  7. data/lib/clacky/brand_config.rb +309 -6
  8. data/lib/clacky/cli.rb +4 -58
  9. data/lib/clacky/default_extensions/coding/ext.yml +18 -0
  10. data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +95 -0
  11. data/lib/clacky/default_extensions/ext-studio/api/handler.rb +254 -0
  12. data/lib/clacky/default_extensions/ext-studio/ext.yml +32 -0
  13. data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +1269 -0
  14. data/lib/clacky/default_extensions/ext-studio/skills/ext-debug/SKILL.md +71 -0
  15. data/lib/clacky/default_extensions/ext-studio/skills/ext-publish/SKILL.md +74 -0
  16. data/lib/clacky/default_extensions/ext-studio/skills/ext-scaffold/SKILL.md +65 -0
  17. data/lib/clacky/default_extensions/general/ext.yml +18 -0
  18. data/lib/clacky/default_extensions/git/ext.yml +12 -0
  19. data/lib/clacky/{default_agents/_panels/git/panel.js → default_extensions/git/panels/git/view.js} +1 -2
  20. data/lib/clacky/default_extensions/meeting/{handler.rb → api/handler.rb} +78 -26
  21. data/lib/clacky/default_extensions/meeting/ext.yml +15 -0
  22. data/lib/clacky/default_extensions/meeting/{meeting.js → panels/meeting/view.js} +266 -89
  23. data/lib/clacky/default_extensions/meeting/skills/meeting-summarizer/SKILL.md +2 -1
  24. data/lib/clacky/default_extensions/time_machine/ext.yml +12 -0
  25. data/lib/clacky/{default_agents/_panels/time_machine/panel.js → default_extensions/time_machine/panels/time_machine/view.js} +58 -47
  26. data/lib/clacky/default_skills/channel-manager/discord_setup.rb +1 -1
  27. data/lib/clacky/default_skills/extend-openclacky/SKILL.md +83 -18
  28. data/lib/clacky/{api_extension.rb → extension/api_extension.rb} +27 -11
  29. data/lib/clacky/extension/api_loader.rb +136 -0
  30. data/lib/clacky/extension/cli_commands.rb +226 -0
  31. data/lib/clacky/{server/api_extension_dispatcher.rb → extension/dispatcher.rb} +22 -9
  32. data/lib/clacky/extension/hook_loader.rb +77 -0
  33. data/lib/clacky/extension/loader.rb +483 -0
  34. data/lib/clacky/extension/packager.rb +226 -0
  35. data/lib/clacky/{patch_loader.rb → extension/patch_loader.rb} +45 -0
  36. data/lib/clacky/extension/scaffold/template_renderer.rb +64 -0
  37. data/lib/clacky/extension/scaffold/templates/full/README.md.erb +48 -0
  38. data/lib/clacky/extension/scaffold/templates/full/agents/designer.md +5 -0
  39. data/lib/clacky/extension/scaffold/templates/full/api/handler.rb.erb +16 -0
  40. data/lib/clacky/extension/scaffold/templates/full/channels/noop.rb.erb +40 -0
  41. data/lib/clacky/extension/scaffold/templates/full/ext.yml.erb +41 -0
  42. data/lib/clacky/extension/scaffold/templates/full/hooks/audit.rb +9 -0
  43. data/lib/clacky/extension/scaffold/templates/full/panels/dashboard/view.js.erb +194 -0
  44. data/lib/clacky/extension/scaffold/templates/full/patches/audit.rb +15 -0
  45. data/lib/clacky/extension/scaffold/templates/full/skills/__slug__-skill/SKILL.md.erb +8 -0
  46. data/lib/clacky/extension/scaffold/templates/hello/api/handler.rb.erb +9 -0
  47. data/lib/clacky/extension/scaffold/templates/hello/ext.yml.erb +12 -0
  48. data/lib/clacky/extension/scaffold/templates/hello/panels/hello/view.js.erb +31 -0
  49. data/lib/clacky/extension/scaffold.rb +55 -0
  50. data/lib/clacky/extension/verifier.rb +196 -0
  51. data/lib/clacky/identity.rb +70 -0
  52. data/lib/clacky/locales/en.rb +19 -1
  53. data/lib/clacky/locales/zh.rb +19 -1
  54. data/lib/clacky/platform_http_client.rb +18 -15
  55. data/lib/clacky/server/channel/extension_adapter_loader.rb +32 -0
  56. data/lib/clacky/server/channel.rb +6 -0
  57. data/lib/clacky/server/http_server.rb +435 -263
  58. data/lib/clacky/server/session_registry.rb +59 -3
  59. data/lib/clacky/session_manager.rb +35 -4
  60. data/lib/clacky/skill_loader.rb +28 -1
  61. data/lib/clacky/ui2/layout_manager.rb +0 -5
  62. data/lib/clacky/ui2/progress_handle.rb +0 -3
  63. data/lib/clacky/ui2/ui_controller.rb +9 -15
  64. data/lib/clacky/version.rb +1 -1
  65. data/lib/clacky/web/app.css +782 -554
  66. data/lib/clacky/web/app.js +63 -15
  67. data/lib/clacky/web/auth.js +2 -0
  68. data/lib/clacky/web/components/notify.js +2 -0
  69. data/lib/clacky/web/components/sidebar.js +2 -8
  70. data/lib/clacky/web/core/ext.js +316 -79
  71. data/lib/clacky/web/features/backup/store.js +24 -0
  72. data/lib/clacky/web/features/backup/view.js +49 -23
  73. data/lib/clacky/web/features/brand/view.js +13 -5
  74. data/lib/clacky/web/features/extensions/store.js +184 -0
  75. data/lib/clacky/web/features/extensions/view.js +389 -0
  76. data/lib/clacky/web/features/new-session/store.js +186 -0
  77. data/lib/clacky/web/features/new-session/view.js +319 -0
  78. data/lib/clacky/web/features/share/view.js +63 -14
  79. data/lib/clacky/web/features/skills/store.js +3 -1
  80. data/lib/clacky/web/features/skills/view.js +1 -1
  81. data/lib/clacky/web/features/tasks/view.js +88 -27
  82. data/lib/clacky/web/features/trash/store.js +24 -6
  83. data/lib/clacky/web/features/trash/view.js +46 -9
  84. data/lib/clacky/web/features/workspace/store.js +2 -0
  85. data/lib/clacky/web/features/workspace/view.js +0 -1
  86. data/lib/clacky/web/i18n.js +140 -108
  87. data/lib/clacky/web/index.html +190 -214
  88. data/lib/clacky/web/sessions.js +99 -304
  89. data/lib/clacky/web/settings.js +1 -1
  90. data/lib/clacky/web/skills.js +2 -0
  91. data/lib/clacky/web/theme.js +2 -0
  92. data/lib/clacky/web/ws-dispatcher.js +2 -1
  93. data/lib/clacky/web/ws.js +2 -0
  94. data/lib/clacky.rb +21 -10
  95. metadata +55 -24
  96. data/benchmark/runner.rb +0 -441
  97. data/lib/clacky/api_extension_loader.rb +0 -168
  98. data/lib/clacky/default_agents/SOUL.md +0 -3
  99. data/lib/clacky/default_agents/USER.md +0 -1
  100. data/lib/clacky/default_agents/coding/profile.yml +0 -5
  101. data/lib/clacky/default_agents/coding/webui/.gitkeep +0 -0
  102. data/lib/clacky/default_agents/general/profile.yml +0 -2
  103. data/lib/clacky/default_extensions/meeting/meta.yml +0 -3
  104. data/lib/clacky/web/features/creator/store.js +0 -81
  105. data/lib/clacky/web/features/creator/view.js +0 -380
  106. /data/lib/clacky/{default_agents → default_extensions/coding/agents}/coding/system_prompt.md +0 -0
  107. /data/lib/clacky/{default_agents → default_extensions/general/agents}/general/system_prompt.md +0 -0
  108. /data/lib/clacky/{default_agents/base_prompt.md → prompts/base.md} +0 -0
@@ -16,58 +16,119 @@ const TasksView = (() => {
16
16
  if (!cron) return cron;
17
17
  const parts = cron.trim().split(/\s+/);
18
18
  if (parts.length !== 5) return cron;
19
- const [min, hour, dom, month, dow] = parts;
19
+ let [min, hour, dom, month, dow] = parts;
20
+
21
+ // normalize */1 → *
22
+ if (min === "*/1") min = "*";
23
+ if (hour === "*/1") hour = "*";
24
+ if (dom === "*/1") dom = "*";
25
+ if (month === "*/1") month = "*";
26
+ if (dow === "*/1") dow = "*";
20
27
 
21
28
  const isAny = v => v === "*";
22
- const isNum = v => /^\d+$/.test(v);
29
+ const isInt = v => /^\d+$/.test(v);
23
30
  const pad = n => String(n).padStart(2, "0");
24
31
 
25
32
  const lang = (typeof I18n !== "undefined" && I18n.lang()) || "zh";
26
33
  const isZh = lang === "zh";
27
34
 
28
- const dowNames = isZh
29
- ? ["周日","周一","周二","周三","周四","周五","周六"]
30
- : ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];
35
+ const DOW_ZH = ["周日","周一","周二","周三","周四","周五","周六"];
36
+ const DOW_EN = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];
37
+ const MON_ZH = ["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"];
38
+ const MON_EN = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
31
39
 
32
- const monthNames = isZh
33
- ? ["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"]
34
- : ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
40
+ // dow label, e.g. "1-5"→"工作日", "1,3,5"→"周一、周三、周五", "1"→"每周一"
41
+ function dowLabel(d) {
42
+ if (isAny(d)) return null;
43
+ if (d === "1-5") return isZh ? "工作日" : "Weekdays";
44
+ if (d === "0,6" || d === "6,0") return isZh ? "周末" : "Weekends";
45
+ if (isInt(d)) {
46
+ const name = (isZh ? DOW_ZH : DOW_EN)[parseInt(d, 10)] || d;
47
+ return isZh ? `每${name}` : name;
48
+ }
49
+ if (/^[\d,]+$/.test(d)) {
50
+ const names = d.split(",").map(n => (isZh ? DOW_ZH : DOW_EN)[parseInt(n, 10)] || n);
51
+ return isZh ? names.join("、") : names.join("/");
52
+ }
53
+ return d;
54
+ }
35
55
 
36
- const timeStr = (isNum(hour) && isNum(min)) ? `${pad(hour)}:${pad(min)}` : null;
56
+ // build HH:MM string; supports single hour or comma-list like "10,14"
57
+ function timeStr() {
58
+ if (!isInt(min)) return null;
59
+ if (isInt(hour)) return `${pad(hour)}:${pad(min)}`;
60
+ if (/^[\d,]+$/.test(hour))
61
+ return hour.split(",").map(h => `${pad(h)}:${pad(min)}`).join(isZh ? "、" : "/");
62
+ return null;
63
+ }
37
64
 
65
+ // ── every-N-minutes ───────────────────────────────────────────────────
38
66
  if (min.startsWith("*/") && isAny(hour) && isAny(dom) && isAny(month) && isAny(dow)) {
39
67
  const n = min.slice(2);
40
68
  return isZh ? `每 ${n} 分钟` : `Every ${n} min`;
41
69
  }
42
- if ((isAny(min) || isNum(min)) && hour.startsWith("*/") && isAny(dom) && isAny(month) && isAny(dow)) {
43
- const n = hour.slice(2);
44
- return isZh ? `每 ${n} 小时` : `Every ${n} hr`;
70
+ // ── every-N-hours ─────────────────────────────────────────────────────
71
+ if (isAny(dom) && isAny(month) && isAny(dow)) {
72
+ if (isAny(min) && hour.startsWith("*/")) {
73
+ return isZh ? `每 ${hour.slice(2)} 小时` : `Every ${hour.slice(2)} hr`;
74
+ }
75
+ if (isInt(min) && hour.startsWith("*/")) {
76
+ return isZh ? `每 ${hour.slice(2)} 小时` : `Every ${hour.slice(2)} hr`;
77
+ }
78
+ }
79
+ // ── every-N-hours on specific days e.g. 0 */3 * * 1-5 ────────────
80
+ if (isAny(dom) && isAny(month) && !isAny(dow) && hour.startsWith("*/")) {
81
+ const dl = dowLabel(dow);
82
+ const ev = isZh ? `每 ${hour.slice(2)} 小时` : `Every ${hour.slice(2)} hr`;
83
+ if (dl) return `${dl} ${ev}`;
45
84
  }
85
+ // ── every minute ──────────────────────────────────────────────────────
46
86
  if (isAny(min) && isAny(hour) && isAny(dom) && isAny(month) && isAny(dow)) {
47
87
  return isZh ? "每分钟" : "Every minute";
48
88
  }
49
- if (isNum(min) && isAny(hour) && isAny(dom) && isAny(month) && isAny(dow)) {
89
+ // ── hourly at :MM ─────────────────────────────────────────────────────
90
+ if (isInt(min) && isAny(hour) && isAny(dom) && isAny(month) && isAny(dow)) {
50
91
  return isZh ? `每小时 :${pad(min)}` : `Hourly at :${pad(min)}`;
51
92
  }
52
- if (timeStr && isAny(dom) && isAny(month) && isNum(dow)) {
53
- const d = dowNames[parseInt(dow, 10)] || dow;
54
- return isZh ? `每${d} ${timeStr}` : `${d} ${timeStr}`;
93
+ // ── hourly at :MM on specific days e.g. 30 * * * 1-5 ─────────────
94
+ if (isInt(min) && isAny(hour) && isAny(dom) && isAny(month) && !isAny(dow)) {
95
+ const dl = dowLabel(dow);
96
+ const tm = isZh ? `每小时 :${pad(min)}` : `Hourly at :${pad(min)}`;
97
+ if (dl) return `${dl} ${tm}`;
55
98
  }
56
- if (timeStr && isAny(dom) && isAny(month) && dow === "1-5") {
57
- return isZh ? `工作日 ${timeStr}` : `Weekdays ${timeStr}`;
99
+
100
+ // ── every-N-min within hour range on certain days e.g. */1 9-14 * * 1-5
101
+ if (min.startsWith("*/") && /^\d+-\d+$/.test(hour) && isAny(dom) && isAny(month)) {
102
+ const n = min.slice(2);
103
+ const dl = dowLabel(dow);
104
+ const hr = isZh ? `${hour}时` : `${hour}h`;
105
+ const ev = isZh ? `每 ${n} 分钟` : `Every ${n} min`;
106
+ return dl ? `${dl} ${hr} ${ev}` : `${hr} ${ev}`;
58
107
  }
59
- if (timeStr && isAny(dom) && isAny(month) && (dow === "0,6" || dow === "6,0")) {
60
- return isZh ? `周末 ${timeStr}` : `Weekends ${timeStr}`;
108
+ // ── every minute within hour range e.g. * 9-14 * * 1-5
109
+ if (isAny(min) && /^\d+-\d+$/.test(hour) && isAny(dom) && isAny(month)) {
110
+ const dl = dowLabel(dow);
111
+ const hr = isZh ? `${hour}时` : `${hour}h`;
112
+ const ev = isZh ? "每分钟" : "Every minute";
113
+ return dl ? `${dl} ${hr} ${ev}` : `${hr} ${ev}`;
61
114
  }
62
- if (timeStr && isAny(dom) && isAny(month) && isAny(dow)) {
63
- return isZh ? `每天 ${timeStr}` : `Daily ${timeStr}`;
115
+
116
+ const ts = timeStr();
117
+ const dl = dowLabel(dow);
118
+
119
+ // ── fixed time, variable days ─────────────────────────────────────────
120
+ if (ts && isAny(dom) && isAny(month)) {
121
+ if (dl) return `${dl} ${ts}`;
122
+ return isZh ? `每天 ${ts}` : `Daily ${ts}`;
64
123
  }
65
- if (timeStr && isNum(dom) && isAny(month) && isAny(dow)) {
66
- return isZh ? `每月 ${dom} ${timeStr}` : `Monthly day ${dom} ${timeStr}`;
124
+ // ── fixed time, fixed day-of-month ────────────────────────────────────
125
+ if (ts && isInt(dom) && isAny(month) && isAny(dow)) {
126
+ return isZh ? `每月 ${dom} 日 ${ts}` : `Monthly day ${dom} ${ts}`;
67
127
  }
68
- if (timeStr && isNum(dom) && isNum(month) && isAny(dow)) {
69
- const m = monthNames[parseInt(month, 10) - 1] || month;
70
- return isZh ? `${m}${dom}日 ${timeStr}` : `${m} ${dom} ${timeStr}`;
128
+ // ── fixed time, fixed date ────────────────────────────────────────────
129
+ if (ts && isInt(dom) && isInt(month) && isAny(dow)) {
130
+ const m = (isZh ? MON_ZH : MON_EN)[parseInt(month, 10) - 1] || month;
131
+ return isZh ? `${m}${dom}日 ${ts}` : `${m} ${dom} ${ts}`;
71
132
  }
72
133
 
73
134
  return cron;
@@ -13,10 +13,13 @@
13
13
  // ───────────────────────────────────────────────────────────────────────────
14
14
 
15
15
  const TrashStore = (() => {
16
+ const SOURCES = Object.freeze({ ALL: "all", CRON: "cron", MANUAL: "manual" });
17
+
16
18
  let _files = [];
17
19
  let _totals = { count: 0, size: 0 };
18
20
  let _sessions = [];
19
21
  let _sessionTotals = { count: 0, size: 0 };
22
+ let _filterSource = SOURCES.ALL;
20
23
  let _loading = false;
21
24
 
22
25
  const _listeners = {};
@@ -44,8 +47,20 @@ const TrashStore = (() => {
44
47
  const state = {
45
48
  get files() { return _files; },
46
49
  get totals() { return _totals; },
47
- get sessions() { return _sessions; },
48
- get sessionTotals() { return _sessionTotals; },
50
+ 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);
55
+ },
56
+ 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);
61
+ return { count: filtered.length, size: filtered.reduce((sum, s) => sum + (s.file_size || 0), 0) };
62
+ },
63
+ get filterSource() { return _filterSource; },
49
64
  orphanCount() { return _files.filter(f => _isOrphanRoot(f.project_root)).length; },
50
65
  isOrphan(file) { return _isOrphanRoot(file.project_root); },
51
66
  };
@@ -53,6 +68,13 @@ const TrashStore = (() => {
53
68
  const Trash = {
54
69
  on: _on,
55
70
  state,
71
+ SOURCES,
72
+
73
+ setFilterSource(source) {
74
+ if (source === _filterSource) return;
75
+ _filterSource = source;
76
+ _emit("trash:sessionsChanged");
77
+ },
56
78
 
57
79
  async loadFiles() {
58
80
  if (_loading) return;
@@ -230,10 +252,6 @@ const TrashStore = (() => {
230
252
 
231
253
  function _removeSessionLocal(session) {
232
254
  _sessions = _sessions.filter(s => s.session_id !== session.session_id);
233
- _sessionTotals = {
234
- count: Math.max(0, _sessionTotals.count - 1),
235
- size: Math.max(0, _sessionTotals.size - (session.file_size || 0))
236
- };
237
255
  }
238
256
 
239
257
  return Trash;
@@ -61,8 +61,13 @@ const TrashView = (() => {
61
61
 
62
62
  const filePane = $("trash-tab-file");
63
63
  const sessionPane = $("trash-tab-session");
64
+ const filterBar = $("trash-filter-bar");
64
65
  if (filePane) filePane.style.display = tab === "file-trash" ? "" : "none";
65
66
  if (sessionPane) sessionPane.style.display = tab === "session-trash" ? "" : "none";
67
+ if (filterBar) filterBar.style.display = tab === "session-trash" ? "" : "none";
68
+
69
+ const hint = $("trash-session-hint");
70
+ if (hint) hint.style.display = tab === "session-trash" ? "" : "none";
66
71
 
67
72
  const btnOrphans = $("btn-trash-empty-orphans");
68
73
  if (btnOrphans) btnOrphans.style.display = tab === "file-trash" ? "" : "none";
@@ -73,7 +78,7 @@ const TrashView = (() => {
73
78
  function _load() {
74
79
  if (_activeTab === "file-trash") {
75
80
  const list = $("trash-list");
76
- if (list) list.innerHTML = `<div class="creator-loading">${_t("trash.loading")}</div>`;
81
+ if (list) list.innerHTML = `<div class="trash-loading">${_t("trash.loading")}</div>`;
77
82
  Trash.loadFiles();
78
83
  } else {
79
84
  const list = $("trash-session-list");
@@ -109,7 +114,7 @@ const TrashView = (() => {
109
114
  if (btnAll) btnAll.disabled = files.length === 0;
110
115
 
111
116
  if (files.length === 0) {
112
- list.innerHTML = `<div class="creator-empty">${_t("trash.empty")}</div>`;
117
+ list.innerHTML = `<div class="trash-empty">${_t("trash.empty")}</div>`;
113
118
  return;
114
119
  }
115
120
 
@@ -176,11 +181,33 @@ const TrashView = (() => {
176
181
 
177
182
  // ── Session trash rendering ──────────────────────────────────────────
178
183
 
184
+ function _renderSkeleton() {
185
+ const list = $("trash-session-list");
186
+ if (!list) return;
187
+ let html = "";
188
+ for (let i = 0; i < 3; i++) {
189
+ html += `
190
+ <div class="trash-session-skeleton">
191
+ <div class="trash-session-skeleton-info">
192
+ <div class="skel skel-title"></div>
193
+ <div class="skel skel-subtitle"></div>
194
+ <div class="skel skel-text-sm"></div>
195
+ </div>
196
+ <div class="trash-skeleton-actions">
197
+ <div class="skel trash-skeleton-btn"></div>
198
+ <div class="skel trash-skeleton-btn"></div>
199
+ </div>
200
+ </div>`;
201
+ }
202
+ list.innerHTML = html;
203
+ }
204
+
179
205
  function _renderSessions() {
180
- const list = $("trash-session-list");
181
- const summary = $("trash-summary");
182
- const btnOld = $("btn-trash-empty-old");
183
- const btnAll = $("btn-trash-empty-all");
206
+ const list = $("trash-session-list");
207
+ const summary = $("trash-summary");
208
+ const btnOld = $("btn-trash-empty-old");
209
+ const btnAll = $("btn-trash-empty-all");
210
+ const filterBar = $("trash-filter-bar");
184
211
  if (!list) return;
185
212
 
186
213
  const sessions = Trash.state.sessions;
@@ -196,9 +223,10 @@ const TrashView = (() => {
196
223
  }
197
224
  if (btnOld) btnOld.disabled = sessions.length === 0;
198
225
  if (btnAll) btnAll.disabled = sessions.length === 0;
226
+ if (filterBar) filterBar.style.display = "";
199
227
 
200
228
  if (sessions.length === 0) {
201
- list.innerHTML = `<div class="creator-empty">${_t("trash.noSessionTrash")}</div>`;
229
+ list.innerHTML = `<div class="trash-empty">${_t("trash.noSessionTrash")}</div>`;
202
230
  return;
203
231
  }
204
232
 
@@ -385,6 +413,15 @@ const TrashView = (() => {
385
413
  if (btnAll) btnAll.addEventListener("click",
386
414
  () => _emptyBulk(0, _activeTab === "session-trash"
387
415
  ? "trash.confirmEmptySessionAll" : "trash.confirmEmptyAll"));
416
+
417
+ // Filter chips
418
+ document.querySelectorAll(".trash-filter-chip").forEach(chip => {
419
+ chip.addEventListener("click", () => {
420
+ const val = chip.dataset.filter;
421
+ Trash.setFilterSource(val);
422
+ document.querySelectorAll(".trash-filter-chip").forEach(c => c.classList.toggle("active", c.dataset.filter === val));
423
+ });
424
+ });
388
425
  }
389
426
 
390
427
  function _subscribe() {
@@ -392,11 +429,11 @@ const TrashView = (() => {
392
429
  Trash.on("trash:sessionsChanged", _renderSessions);
393
430
  Trash.on("trash:filesError", (e) => {
394
431
  const list = $("trash-list");
395
- if (list) list.innerHTML = `<div class="creator-empty creator-error">${escapeHtml(e.message)}</div>`;
432
+ if (list) list.innerHTML = `<div class="trash-empty trash-error">${escapeHtml(e.message)}</div>`;
396
433
  });
397
434
  Trash.on("trash:sessionsError", (e) => {
398
435
  const list = $("trash-session-list");
399
- if (list) list.innerHTML = `<div class="creator-empty creator-error">${escapeHtml(e.message)}</div>`;
436
+ if (list) list.innerHTML = `<div class="trash-empty trash-error">${escapeHtml(e.message)}</div>`;
400
437
  });
401
438
  }
402
439
 
@@ -97,3 +97,5 @@ const WorkspaceStore = (() => {
97
97
  })();
98
98
 
99
99
  const Workspace = WorkspaceStore;
100
+ Clacky.Workspace = Workspace;
101
+ Clacky.WorkspaceStore = WorkspaceStore;
@@ -302,4 +302,3 @@ if (window.Clacky && Clacky.ext) {
302
302
  // Workspace.onSession on every session switch). Rendering is driven by the
303
303
  // slot re-render, so this only updates state.
304
304
  Workspace.onSession = (session) => { Workspace.setSession(session); };
305
- window.Workspace = Workspace;