openclacky 1.3.6 → 1.3.8

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 (112) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +54 -0
  3. data/lib/clacky/agent/hook_manager.rb +10 -1
  4. data/lib/clacky/agent/system_prompt_builder.rb +1 -1
  5. data/lib/clacky/agent.rb +17 -3
  6. data/lib/clacky/agent_profile.rb +124 -53
  7. data/lib/clacky/billing/billing_store.rb +3 -2
  8. data/lib/clacky/brand_config.rb +309 -6
  9. data/lib/clacky/cli.rb +4 -57
  10. data/lib/clacky/default_extensions/coding/agents/coding/avatar.png +0 -0
  11. data/lib/clacky/default_extensions/coding/ext.yml +19 -0
  12. data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/avatar.png +0 -0
  13. data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +95 -0
  14. data/lib/clacky/default_extensions/ext-studio/api/handler.rb +254 -0
  15. data/lib/clacky/default_extensions/ext-studio/ext.yml +33 -0
  16. data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +1288 -0
  17. data/lib/clacky/default_extensions/ext-studio/skills/ext-debug/SKILL.md +71 -0
  18. data/lib/clacky/default_extensions/ext-studio/skills/ext-publish/SKILL.md +74 -0
  19. data/lib/clacky/default_extensions/ext-studio/skills/ext-scaffold/SKILL.md +65 -0
  20. data/lib/clacky/default_extensions/general/agents/general/avatar.png +0 -0
  21. data/lib/clacky/default_extensions/general/ext.yml +19 -0
  22. data/lib/clacky/default_extensions/git/ext.yml +12 -0
  23. data/lib/clacky/{default_agents/_panels/git/panel.js → default_extensions/git/panels/git/view.js} +4 -5
  24. data/lib/clacky/default_extensions/meeting/{handler.rb → api/handler.rb} +78 -26
  25. data/lib/clacky/default_extensions/meeting/ext.yml +15 -0
  26. data/lib/clacky/default_extensions/meeting/{meeting.js → panels/meeting/view.js} +266 -90
  27. data/lib/clacky/default_extensions/meeting/skills/meeting-summarizer/SKILL.md +2 -1
  28. data/lib/clacky/default_extensions/time_machine/ext.yml +12 -0
  29. data/lib/clacky/{default_agents/_panels/time_machine/panel.js → default_extensions/time_machine/panels/time_machine/view.js} +58 -47
  30. data/lib/clacky/default_skills/channel-manager/discord_setup.rb +1 -1
  31. data/lib/clacky/default_skills/extend-openclacky/SKILL.md +83 -18
  32. data/lib/clacky/{api_extension.rb → extension/api_extension.rb} +27 -11
  33. data/lib/clacky/extension/api_loader.rb +136 -0
  34. data/lib/clacky/extension/cli_commands.rb +226 -0
  35. data/lib/clacky/{server/api_extension_dispatcher.rb → extension/dispatcher.rb} +22 -9
  36. data/lib/clacky/extension/hook_loader.rb +77 -0
  37. data/lib/clacky/extension/loader.rb +489 -0
  38. data/lib/clacky/extension/packager.rb +226 -0
  39. data/lib/clacky/{patch_loader.rb → extension/patch_loader.rb} +45 -0
  40. data/lib/clacky/extension/scaffold/template_renderer.rb +64 -0
  41. data/lib/clacky/extension/scaffold/templates/full/README.md.erb +48 -0
  42. data/lib/clacky/extension/scaffold/templates/full/agents/designer.md +5 -0
  43. data/lib/clacky/extension/scaffold/templates/full/api/handler.rb.erb +16 -0
  44. data/lib/clacky/extension/scaffold/templates/full/channels/noop.rb.erb +40 -0
  45. data/lib/clacky/extension/scaffold/templates/full/ext.yml.erb +41 -0
  46. data/lib/clacky/extension/scaffold/templates/full/hooks/audit.rb +9 -0
  47. data/lib/clacky/extension/scaffold/templates/full/panels/dashboard/view.js.erb +194 -0
  48. data/lib/clacky/extension/scaffold/templates/full/patches/audit.rb +15 -0
  49. data/lib/clacky/extension/scaffold/templates/full/skills/__slug__-skill/SKILL.md.erb +8 -0
  50. data/lib/clacky/extension/scaffold/templates/hello/api/handler.rb.erb +9 -0
  51. data/lib/clacky/extension/scaffold/templates/hello/ext.yml.erb +12 -0
  52. data/lib/clacky/extension/scaffold/templates/hello/panels/hello/view.js.erb +31 -0
  53. data/lib/clacky/extension/scaffold.rb +55 -0
  54. data/lib/clacky/extension/verifier.rb +196 -0
  55. data/lib/clacky/identity.rb +70 -0
  56. data/lib/clacky/locales/en.rb +19 -1
  57. data/lib/clacky/locales/zh.rb +19 -1
  58. data/lib/clacky/platform_http_client.rb +18 -15
  59. data/lib/clacky/server/channel/extension_adapter_loader.rb +32 -0
  60. data/lib/clacky/server/channel.rb +6 -0
  61. data/lib/clacky/server/http_server.rb +439 -146
  62. data/lib/clacky/server/session_registry.rb +9 -2
  63. data/lib/clacky/shell_hook_loader.rb +266 -22
  64. data/lib/clacky/skill_loader.rb +28 -1
  65. data/lib/clacky/ui2/components/welcome_banner.rb +1 -1
  66. data/lib/clacky/ui2/ui_controller.rb +9 -6
  67. data/lib/clacky/utils/workspace_rules.rb +2 -2
  68. data/lib/clacky/version.rb +1 -1
  69. data/lib/clacky/web/app.css +1034 -675
  70. data/lib/clacky/web/app.js +63 -15
  71. data/lib/clacky/web/auth.js +2 -0
  72. data/lib/clacky/web/components/notify.js +2 -0
  73. data/lib/clacky/web/components/sidebar.js +1 -5
  74. data/lib/clacky/web/core/ext.js +432 -89
  75. data/lib/clacky/web/features/backup/store.js +1 -0
  76. data/lib/clacky/web/features/backup/view.js +0 -1
  77. data/lib/clacky/web/features/brand/view.js +13 -5
  78. data/lib/clacky/web/features/extensions/store.js +184 -0
  79. data/lib/clacky/web/features/extensions/view.js +390 -0
  80. data/lib/clacky/web/features/new-session/store.js +199 -0
  81. data/lib/clacky/web/features/new-session/view.js +619 -0
  82. data/lib/clacky/web/features/share/view.js +63 -14
  83. data/lib/clacky/web/features/skills/store.js +3 -1
  84. data/lib/clacky/web/features/skills/view.js +1 -1
  85. data/lib/clacky/web/features/tasks/view.js +12 -0
  86. data/lib/clacky/web/features/trash/store.js +24 -6
  87. data/lib/clacky/web/features/trash/view.js +48 -12
  88. data/lib/clacky/web/features/workspace/store.js +2 -0
  89. data/lib/clacky/web/features/workspace/view.js +0 -1
  90. data/lib/clacky/web/i18n.js +152 -106
  91. data/lib/clacky/web/index.html +172 -190
  92. data/lib/clacky/web/sessions.js +67 -272
  93. data/lib/clacky/web/settings.js +1 -1
  94. data/lib/clacky/web/skills.js +88 -48
  95. data/lib/clacky/web/theme.js +5 -0
  96. data/lib/clacky/web/ws-dispatcher.js +12 -2
  97. data/lib/clacky/web/ws.js +2 -0
  98. data/lib/clacky.rb +21 -10
  99. metadata +52 -21
  100. data/benchmark/runner.rb +0 -441
  101. data/lib/clacky/api_extension_loader.rb +0 -168
  102. data/lib/clacky/default_agents/SOUL.md +0 -3
  103. data/lib/clacky/default_agents/USER.md +0 -1
  104. data/lib/clacky/default_agents/coding/profile.yml +0 -5
  105. data/lib/clacky/default_agents/coding/webui/.gitkeep +0 -0
  106. data/lib/clacky/default_agents/general/profile.yml +0 -2
  107. data/lib/clacky/default_extensions/meeting/meta.yml +0 -3
  108. data/lib/clacky/web/features/creator/store.js +0 -81
  109. data/lib/clacky/web/features/creator/view.js +0 -380
  110. /data/lib/clacky/{default_agents → default_extensions/coding/agents}/coding/system_prompt.md +0 -0
  111. /data/lib/clacky/{default_agents → default_extensions/general/agents}/general/system_prompt.md +0 -0
  112. /data/lib/clacky/{default_agents/base_prompt.md → prompts/base.md} +0 -0
@@ -13,6 +13,11 @@
13
13
  // ───────────────────────────────────────────────────────────────────────────
14
14
 
15
15
  const ShareView = (() => {
16
+ const THEME_DOT = {
17
+ geek: "#38bdf8",
18
+ light: "#6ee7b7",
19
+ warm: "#ea580c"
20
+ };
16
21
  const THEME_KEY = "clacky-share-theme";
17
22
  const DRAFT_KEY = "clacky-share-draft";
18
23
 
@@ -158,32 +163,49 @@ const ShareView = (() => {
158
163
 
159
164
  _paintBackground(ctx, W, H, t, t.bg);
160
165
 
166
+ // ── Brand name ──
161
167
  ctx.fillStyle = t.title;
162
- ctx.textAlign = "center";
163
- ctx.font = "700 60px -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif";
164
- ctx.fillText(_brandName(), W / 2, 150);
168
+ ctx.textAlign = "left";
169
+ ctx.font = "800 72px -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif";
170
+ ctx.fillText(_brandName(), 64, 140);
171
+
172
+ // ── Divider line ──
173
+ ctx.strokeStyle = t.hero;
174
+ ctx.lineWidth = 4;
175
+ ctx.lineCap = "round";
176
+ ctx.beginPath();
177
+ ctx.moveTo(64, 172);
178
+ ctx.lineTo(160, 172);
179
+ ctx.stroke();
165
180
 
181
+ // ── Copy text ──
166
182
  const text = (copy || "").trim() || I18n.t("share.poster.tagline", { brand: _brandName() });
167
183
  ctx.fillStyle = t.tagline;
168
- _drawAutoText(ctx, text, W / 2, 250, W - 120, 480 - 250);
184
+ ctx.textAlign = "left";
185
+ _drawAutoTextLeft(ctx, text, 64, 210, W - 128, 420);
169
186
 
170
187
  const url = _shareUrl();
171
188
  if (url) {
172
- const qrSize = 300;
173
- const qrX = (W - qrSize) / 2;
174
- const qrY = 560;
189
+ // ── QR code — bottom right ──
190
+ const qrSize = 180;
191
+ const qrX = W - qrSize - 56;
192
+ const qrY = H - qrSize - 100;
175
193
  ctx.fillStyle = "#ffffff";
176
- _roundRect(ctx, qrX - 24, qrY - 24, qrSize + 48, qrSize + 48, 22);
194
+ _roundRect(ctx, qrX - 14, qrY - 14, qrSize + 28, qrSize + 28, 16);
177
195
  ctx.fill();
178
196
  _drawQrToCanvas(ctx, url, qrX, qrY, qrSize);
179
197
 
198
+ // ── Scan label under QR ──
180
199
  ctx.fillStyle = t.scan;
181
- ctx.font = "400 28px -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif";
182
- ctx.fillText(I18n.t("share.poster.scan"), W / 2, qrY + qrSize + 80);
200
+ ctx.textAlign = "center";
201
+ ctx.font = "400 22px -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif";
202
+ ctx.fillText(I18n.t("share.poster.scan"), qrX + qrSize / 2, qrY + qrSize + 44);
183
203
 
204
+ // ── Domain — bottom left ──
184
205
  ctx.fillStyle = t.scan;
185
- ctx.font = "400 24px -apple-system, sans-serif";
186
- ctx.fillText(url.replace(/^https?:\/\//, "").replace(/\/$/, ""), W / 2, H - 60);
206
+ ctx.textAlign = "left";
207
+ ctx.font = "300 24px -apple-system, sans-serif";
208
+ ctx.fillText(url.replace(/^https?:\/\//, "").replace(/\/$/, ""), 64, H - 56);
187
209
  }
188
210
 
189
211
  return canvas.toDataURL("image/png");
@@ -331,6 +353,27 @@ const ShareView = (() => {
331
353
  }
332
354
  }
333
355
 
356
+ function _drawAutoTextLeft(ctx, text, x, startY, maxWidth, maxHeight) {
357
+ const sizes = [52, 46, 40, 36, 32, 28, 24];
358
+ let chosen = null;
359
+ for (const size of sizes) {
360
+ ctx.font = "600 " + size + "px -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif";
361
+ const lh = Math.round(size * 1.55);
362
+ const lines = _wrapLines(ctx, text, maxWidth);
363
+ if (lines.length * lh <= maxHeight || size === sizes[sizes.length - 1]) {
364
+ chosen = { size, lh, lines };
365
+ break;
366
+ }
367
+ }
368
+ ctx.font = "600 " + chosen.size + "px -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif";
369
+ ctx.textAlign = "left";
370
+ let y = startY + chosen.lh * 0.8;
371
+ for (const line of chosen.lines) {
372
+ ctx.fillText(line, x, y);
373
+ y += chosen.lh;
374
+ }
375
+ }
376
+
334
377
  function _wrapLines(ctx, text, maxWidth) {
335
378
  const out = [];
336
379
  for (const para of String(text).split("\n")) {
@@ -469,9 +512,10 @@ const ShareView = (() => {
469
512
  const themeChips = THEME_ORDER.map((id) => {
470
513
  const th = THEMES[id];
471
514
  const on = id === _themeId() ? " is-active" : "";
515
+ const dot = '<span class="share-theme-dot" style="background:' + THEME_DOT[id] + '"></span>';
472
516
  return '<button type="button" class="share-theme-chip' + on + '" data-theme="' + id + '"' +
473
517
  ' style="background:' + th.swatch + '" title="' + _esc(I18n.t(th.labelKey)) + '">' +
474
- '<span class="share-theme-name">' + _esc(I18n.t(th.labelKey)) + '</span></button>';
518
+ dot + '<span class="share-theme-name">' + _esc(I18n.t(th.labelKey)) + '</span></button>';
475
519
  }).join("");
476
520
 
477
521
  const platformTabs = PLATFORMS.map((p) => {
@@ -486,7 +530,9 @@ const ShareView = (() => {
486
530
  '<h3 class="share-title">' + _esc(I18n.t(titleKey, { brand: _brandName() })) + '</h3>' +
487
531
  '<p class="share-subtitle">' + _esc(I18n.t(subtitleKey)) + '</p>' +
488
532
  '<div class="share-body">' +
489
- '<div class="share-poster-wrap"><img class="share-poster-img" alt="poster"/></div>' +
533
+ '<div class="share-poster-wrap">' +
534
+ '<img class="share-poster-img" alt="poster"/>' +
535
+ '</div>' +
490
536
  '<div class="share-controls">' +
491
537
  '<div class="share-theme-row">' +
492
538
  '<span class="share-row-label">' + _esc(I18n.t("share.theme.label")) + '</span>' +
@@ -517,6 +563,9 @@ const ShareView = (() => {
517
563
  const img = o.querySelector(".share-poster-img");
518
564
  const textarea = o.querySelector(".share-text");
519
565
 
566
+ img.style.cursor = "pointer";
567
+ img.addEventListener("click", () => _downloadPoster((textarea.value || "").trim()));
568
+
520
569
  const renderPoster = () => {
521
570
  const copy = (textarea.value || "").trim();
522
571
  try { img.src = _scorecard() ? _buildScorecardPoster(copy) : _buildPoster(copy); }
@@ -30,7 +30,7 @@ const SkillsStore = (() => {
30
30
  let _brandActivated = false; // whether a license is currently active
31
31
  let _freeMode = false; // brand-skills tab is showing free-mode skills
32
32
  let _paidSkillsCount = 0; // premium (encrypted) skills locked behind activation
33
- let _showSystemSkills = false; // whether system (source=default) skills are shown
33
+ let _showSystemSkills = localStorage.getItem("skill-ac-show-system") === "true";
34
34
 
35
35
  // ── Internal event bus ──────────────────────────────────────────────────
36
36
  // Always live (unlike Clacky.ext, which is silenced under ?pure=true). The
@@ -173,6 +173,7 @@ const SkillsStore = (() => {
173
173
  /** Toggle visibility of system skills; emit so the view re-renders. */
174
174
  setShowSystemSkills(show) {
175
175
  _showSystemSkills = !!show;
176
+ localStorage.setItem("skill-ac-show-system", _showSystemSkills ? "true" : "false");
176
177
  _emit("skills:changed", { skills: _skills });
177
178
  },
178
179
 
@@ -314,3 +315,4 @@ const SkillsStore = (() => {
314
315
 
315
316
  // Expose the facade under its historical global name.
316
317
  const Skills = SkillsStore;
318
+ Clacky.Skills = Skills;
@@ -265,7 +265,7 @@ const SkillsView = (() => {
265
265
  btn.setAttribute("data-i18n", "skills.brand.activateBtn");
266
266
  btn.addEventListener("click", () => {
267
267
  if (typeof Brand !== "undefined" && Brand.goToLicenseInput) Brand.goToLicenseInput();
268
- else Router.navigate("settings");
268
+ else window.Clacky.Router.navigate("settings");
269
269
  });
270
270
 
271
271
  hint.appendChild(msgEl);
@@ -76,6 +76,12 @@ const TasksView = (() => {
76
76
  return isZh ? `每 ${hour.slice(2)} 小时` : `Every ${hour.slice(2)} hr`;
77
77
  }
78
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}`;
84
+ }
79
85
  // ── every minute ──────────────────────────────────────────────────────
80
86
  if (isAny(min) && isAny(hour) && isAny(dom) && isAny(month) && isAny(dow)) {
81
87
  return isZh ? "每分钟" : "Every minute";
@@ -84,6 +90,12 @@ const TasksView = (() => {
84
90
  if (isInt(min) && isAny(hour) && isAny(dom) && isAny(month) && isAny(dow)) {
85
91
  return isZh ? `每小时 :${pad(min)}` : `Hourly at :${pad(min)}`;
86
92
  }
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}`;
98
+ }
87
99
 
88
100
  // ── every-N-min within hour range on certain days e.g. */1 9-14 * * 1-5
89
101
  if (min.startsWith("*/") && /^\d+-\d+$/.test(hour) && isAny(dom) && isAny(month)) {
@@ -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";
@@ -72,12 +77,8 @@ const TrashView = (() => {
72
77
 
73
78
  function _load() {
74
79
  if (_activeTab === "file-trash") {
75
- const list = $("trash-list");
76
- if (list) list.innerHTML = `<div class="creator-loading">${_t("trash.loading")}</div>`;
77
80
  Trash.loadFiles();
78
81
  } else {
79
- const list = $("trash-session-list");
80
- if (list) list.innerHTML = `<div class="creator-loading">${_t("trash.loading")}</div>`;
81
82
  Trash.loadSessions();
82
83
  }
83
84
  }
@@ -109,7 +110,7 @@ const TrashView = (() => {
109
110
  if (btnAll) btnAll.disabled = files.length === 0;
110
111
 
111
112
  if (files.length === 0) {
112
- list.innerHTML = `<div class="creator-empty">${_t("trash.empty")}</div>`;
113
+ list.innerHTML = `<div class="trash-empty">${_t("trash.empty")}</div>`;
113
114
  return;
114
115
  }
115
116
 
@@ -176,11 +177,34 @@ const TrashView = (() => {
176
177
 
177
178
  // ── Session trash rendering ──────────────────────────────────────────
178
179
 
180
+ function _renderSkeleton() {
181
+ const isSession = _activeTab === "session-trash";
182
+ const list = $(isSession ? "trash-session-list" : "trash-list");
183
+ if (!list) return;
184
+ let html = "";
185
+ for (let i = 0; i < 3; i++) {
186
+ html += `
187
+ <div class="trash-session-skeleton">
188
+ <div class="trash-session-skeleton-info">
189
+ <div class="skel skel-title"></div>
190
+ <div class="skel skel-subtitle"></div>
191
+ <div class="skel skel-text-sm"></div>
192
+ </div>
193
+ <div class="trash-skeleton-actions">
194
+ <div class="skel trash-skeleton-btn"></div>
195
+ <div class="skel trash-skeleton-btn"></div>
196
+ </div>
197
+ </div>`;
198
+ }
199
+ list.innerHTML = html;
200
+ }
201
+
179
202
  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");
203
+ const list = $("trash-session-list");
204
+ const summary = $("trash-summary");
205
+ const btnOld = $("btn-trash-empty-old");
206
+ const btnAll = $("btn-trash-empty-all");
207
+ const filterBar = $("trash-filter-bar");
184
208
  if (!list) return;
185
209
 
186
210
  const sessions = Trash.state.sessions;
@@ -196,9 +220,10 @@ const TrashView = (() => {
196
220
  }
197
221
  if (btnOld) btnOld.disabled = sessions.length === 0;
198
222
  if (btnAll) btnAll.disabled = sessions.length === 0;
223
+ if (filterBar) filterBar.style.display = "";
199
224
 
200
225
  if (sessions.length === 0) {
201
- list.innerHTML = `<div class="creator-empty">${_t("trash.noSessionTrash")}</div>`;
226
+ list.innerHTML = `<div class="trash-empty">${_t("trash.noSessionTrash")}</div>`;
202
227
  return;
203
228
  }
204
229
 
@@ -385,18 +410,29 @@ const TrashView = (() => {
385
410
  if (btnAll) btnAll.addEventListener("click",
386
411
  () => _emptyBulk(0, _activeTab === "session-trash"
387
412
  ? "trash.confirmEmptySessionAll" : "trash.confirmEmptyAll"));
413
+
414
+ // Filter chips
415
+ document.querySelectorAll(".trash-filter-chip").forEach(chip => {
416
+ chip.addEventListener("click", () => {
417
+ const val = chip.dataset.filter;
418
+ Trash.setFilterSource(val);
419
+ document.querySelectorAll(".trash-filter-chip").forEach(c => c.classList.toggle("active", c.dataset.filter === val));
420
+ });
421
+ });
388
422
  }
389
423
 
390
424
  function _subscribe() {
425
+ Trash.on("trash:filesLoading", _renderSkeleton);
426
+ Trash.on("trash:sessionsLoading", _renderSkeleton);
391
427
  Trash.on("trash:filesChanged", _renderFiles);
392
428
  Trash.on("trash:sessionsChanged", _renderSessions);
393
429
  Trash.on("trash:filesError", (e) => {
394
430
  const list = $("trash-list");
395
- if (list) list.innerHTML = `<div class="creator-empty creator-error">${escapeHtml(e.message)}</div>`;
431
+ if (list) list.innerHTML = `<div class="trash-empty trash-error">${escapeHtml(e.message)}</div>`;
396
432
  });
397
433
  Trash.on("trash:sessionsError", (e) => {
398
434
  const list = $("trash-session-list");
399
- if (list) list.innerHTML = `<div class="creator-empty creator-error">${escapeHtml(e.message)}</div>`;
435
+ if (list) list.innerHTML = `<div class="trash-empty trash-error">${escapeHtml(e.message)}</div>`;
400
436
  });
401
437
  }
402
438
 
@@ -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;