openclacky 1.0.0 → 1.0.2

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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +39 -0
  3. data/README.md +87 -53
  4. data/lib/clacky/agent/cost_tracker.rb +19 -2
  5. data/lib/clacky/agent/llm_caller.rb +218 -0
  6. data/lib/clacky/agent/message_compressor_helper.rb +32 -2
  7. data/lib/clacky/agent.rb +54 -22
  8. data/lib/clacky/client.rb +44 -5
  9. data/lib/clacky/default_parsers/pdf_parser.rb +58 -17
  10. data/lib/clacky/default_parsers/pdf_parser_ocr.py +103 -0
  11. data/lib/clacky/default_parsers/pdf_parser_plumber.py +62 -0
  12. data/lib/clacky/default_skills/deploy/SKILL.md +201 -77
  13. data/lib/clacky/default_skills/new/SKILL.md +3 -114
  14. data/lib/clacky/default_skills/onboard/SKILL.md +349 -133
  15. data/lib/clacky/default_skills/onboard/scripts/import_external_skills.rb +371 -0
  16. data/lib/clacky/default_skills/onboard/scripts/install_builtin_skills.rb +175 -0
  17. data/lib/clacky/default_skills/skill-add/scripts/install_from_zip.rb +59 -26
  18. data/lib/clacky/message_format/anthropic.rb +72 -8
  19. data/lib/clacky/message_format/bedrock.rb +6 -3
  20. data/lib/clacky/providers.rb +146 -3
  21. data/lib/clacky/server/channel/adapters/feishu/adapter.rb +14 -0
  22. data/lib/clacky/server/channel/adapters/feishu/bot.rb +10 -0
  23. data/lib/clacky/server/channel/adapters/feishu/message_parser.rb +1 -0
  24. data/lib/clacky/server/channel/channel_manager.rb +12 -4
  25. data/lib/clacky/server/channel/channel_ui_controller.rb +8 -2
  26. data/lib/clacky/server/http_server.rb +746 -13
  27. data/lib/clacky/server/session_registry.rb +55 -24
  28. data/lib/clacky/skill.rb +10 -9
  29. data/lib/clacky/skill_loader.rb +23 -11
  30. data/lib/clacky/tools/file_reader.rb +232 -127
  31. data/lib/clacky/tools/security.rb +42 -64
  32. data/lib/clacky/tools/terminal/persistent_session.rb +15 -4
  33. data/lib/clacky/tools/terminal/safe_rm.sh +106 -0
  34. data/lib/clacky/tools/terminal/session_manager.rb +8 -3
  35. data/lib/clacky/tools/terminal.rb +263 -16
  36. data/lib/clacky/ui2/layout_manager.rb +8 -1
  37. data/lib/clacky/ui2/output_buffer.rb +83 -23
  38. data/lib/clacky/ui2/ui_controller.rb +74 -7
  39. data/lib/clacky/utils/file_processor.rb +14 -40
  40. data/lib/clacky/utils/model_pricing.rb +215 -0
  41. data/lib/clacky/utils/parser_manager.rb +70 -6
  42. data/lib/clacky/utils/string_matcher.rb +23 -1
  43. data/lib/clacky/version.rb +1 -1
  44. data/lib/clacky/web/app.css +673 -9
  45. data/lib/clacky/web/app.js +40 -1608
  46. data/lib/clacky/web/i18n.js +209 -0
  47. data/lib/clacky/web/index.html +166 -2
  48. data/lib/clacky/web/onboard.js +77 -1
  49. data/lib/clacky/web/profile.js +442 -0
  50. data/lib/clacky/web/sessions.js +1034 -2
  51. data/lib/clacky/web/settings.js +127 -6
  52. data/lib/clacky/web/sidebar.js +39 -0
  53. data/lib/clacky/web/skills.js +460 -0
  54. data/lib/clacky/web/trash.js +343 -0
  55. data/lib/clacky/web/ws-dispatcher.js +255 -0
  56. data/lib/clacky.rb +5 -3
  57. metadata +16 -17
  58. data/lib/clacky/clacky_auth_client.rb +0 -152
  59. data/lib/clacky/clacky_cloud_config.rb +0 -123
  60. data/lib/clacky/cloud_project_client.rb +0 -169
  61. data/lib/clacky/default_skills/deploy/scripts/rails_deploy.rb +0 -1377
  62. data/lib/clacky/default_skills/deploy/tools/check_health.rb +0 -116
  63. data/lib/clacky/default_skills/deploy/tools/create_database_service.rb +0 -341
  64. data/lib/clacky/default_skills/deploy/tools/execute_deployment.rb +0 -99
  65. data/lib/clacky/default_skills/deploy/tools/fetch_runtime_logs.rb +0 -77
  66. data/lib/clacky/default_skills/deploy/tools/list_services.rb +0 -67
  67. data/lib/clacky/default_skills/deploy/tools/report_deploy_status.rb +0 -67
  68. data/lib/clacky/default_skills/deploy/tools/set_deploy_variables.rb +0 -189
  69. data/lib/clacky/default_skills/new/scripts/cloud_project_init.sh +0 -74
  70. data/lib/clacky/deploy_api_client.rb +0 -484
@@ -19,6 +19,10 @@ const I18n = (() => {
19
19
  "sidebar.tasks": "Task Management",
20
20
  "sidebar.skills": "Skill Management",
21
21
  "sidebar.channels": "Channel Management",
22
+ "sidebar.trash": "File Recall",
23
+ "sidebar.profile": "Assistant Memory",
24
+ "sidebar.memories": "Memories",
25
+ "sidebar.dataSection": "My Data",
22
26
  "sidebar.creator": "Creator Hub",
23
27
  "sidebar.creatorSection": "Creator",
24
28
  "sidebar.settings": "Settings",
@@ -195,6 +199,100 @@ const I18n = (() => {
195
199
  "creator.promo.link": "Learn more →",
196
200
  "creator.cloud.locked": "Brand license required to publish cloud skills.",
197
201
 
202
+ // ── Creator trash ──
203
+ "trash.title": "File Recall",
204
+ "trash.subtitle": "Files the agent moved to trash across all projects. Recall them back to where they were, or clear the ones you don't need.",
205
+ "trash.refresh": "Refresh",
206
+ "trash.emptyOld": "Empty >7 days",
207
+ "trash.emptyOrphans":"Clean orphans",
208
+ "trash.emptyAll": "Empty all",
209
+ "trash.empty": "Nothing in the trash.",
210
+ "trash.loading": "Loading trash…",
211
+ "trash.summary": "{{count}} file(s), {{size}}",
212
+ "trash.summaryOrphans": "{{count}} orphan(s)",
213
+ "trash.orphan": "orphan",
214
+ "trash.orphanHint": "Original project directory no longer exists (likely a test temp dir). Safe to permanently delete.",
215
+ "trash.noOrphans": "No orphan trash entries detected.",
216
+ "trash.project": "Project",
217
+ "trash.deletedAt": "Deleted",
218
+ "trash.restore": "Restore",
219
+ "trash.delete": "Delete",
220
+ "trash.restoreOk": "Restored: {{path}}",
221
+ "trash.restoreFail": "Restore failed: {{msg}}",
222
+ "trash.confirmDeleteOne": "Permanently delete \"{{name}}\"?\nThis cannot be undone.",
223
+ "trash.confirmEmptyOld": "Permanently delete all trashed files older than 7 days?\nThis cannot be undone.",
224
+ "trash.confirmEmptyAll": "Permanently delete ALL trashed files across every project?\nThis cannot be undone.",
225
+ "trash.confirmEmptyOrphans": "Permanently delete {{count}} orphan trash entries whose original project directory is gone?\nThis cannot be undone.",
226
+ "trash.emptied": "Permanently deleted {{count}} file(s), freed {{size}}.",
227
+ "trash.orphansCleaned": "Cleaned {{count}} orphan entries, freed {{size}}. ({{failed}} failed)",
228
+ "trash.nothingOld": "No files older than 7 days.",
229
+
230
+ // ── Profile (unified read-only view of USER.md / SOUL.md / memories) ──
231
+ "profile.title": "Assistant Memory",
232
+ "profile.subtitle": "A window into the assistant's inner life: who it is, who you are, and what it remembers about your work together.",
233
+ "profile.whoIAm": "Who I am",
234
+ "profile.whoYouAre": "Who you are",
235
+ "profile.statusDefault": "Using built-in default",
236
+ "profile.statusCustom": "Custom override active",
237
+ "profile.loadFail": "Failed to load",
238
+ "profile.emptyContent": "(empty)",
239
+
240
+ // Profile tabs
241
+ "profile.tab.soul": "Soul",
242
+ "profile.tab.user": "User",
243
+ "profile.tab.memories": "Memories",
244
+
245
+ // Per-tab curate actions
246
+ "profile.soul.curateHint": "Not quite right? Let the assistant curate this through a short conversation.",
247
+ "profile.soul.curateBtn": "Have the assistant curate this",
248
+ "profile.user.curateHint": "Changed jobs? Picked up new interests? Let the assistant update your profile.",
249
+ "profile.user.curateBtn": "Have the assistant update this",
250
+ "profile.curateFail": "Could not start the curation session",
251
+ "profile.curateName.soul": "Curate soul",
252
+ "profile.curateName.user": "Curate profile",
253
+
254
+ // Legacy update keys — retained for backward compat, no longer used by UI.
255
+ "profile.update": "Update profile",
256
+ "profile.updateFail": "Could not start the update session",
257
+ // Legacy editor keys (still referenced by old tests / translations).
258
+ "profile.user.placeholder": "Tell the assistant about yourself — name, role, preferences, working style…",
259
+ "profile.soul.placeholder": "Describe the assistant's personality — tone, values, communication style…",
260
+ "profile.save": "Save",
261
+ "profile.reset": "Reset to default",
262
+ "profile.savedOk": "Saved ✓",
263
+ "profile.resetOk": "Reset to default ✓",
264
+ "profile.saveFail": "Save failed",
265
+ "profile.confirmReset": "Clear your custom override and fall back to the built-in default?",
266
+
267
+ // ── Memories (shown inside Profile panel) ──
268
+ "memories.title": "Memories",
269
+ "memories.subtitle": "Long-term memories stored under ~/.clacky/memories/. Most recent first. Use \"Curate\" to let the assistant tidy a memory, or \"Delete\" to drop one.",
270
+ "memories.curate": "Curate",
271
+ "memories.curateTitle": "Open a session and let the assistant tidy this memory with you",
272
+ "memories.curateFail": "Could not start a memory session",
273
+ "memories.curateName": "Curate memory",
274
+ "memories.delete": "Delete",
275
+ "memories.deleteTitle": "Delete this memory",
276
+ "memories.deleteFail": "Delete failed",
277
+ "memories.confirmDelete": "Delete memory \"{{name}}\"?\nIt will be moved to File Recall and can still be recovered from there.",
278
+ "memories.expandTitle": "Show full content",
279
+ "memories.reloadList": "↻ Reload",
280
+ "memories.loading": "Loading…",
281
+ "memories.empty": "No memories yet. The assistant will create them as you work.",
282
+ "memories.emptyHint": "",
283
+ "memories.summary": "{{count}} memory file(s)",
284
+ // Legacy keys — kept for compatibility with any external callers.
285
+ "memories.refresh": "Curate",
286
+ "memories.refreshTitle": "Open a session and let the assistant tidy this memory with you",
287
+ "memories.refreshFail": "Could not start a memory session",
288
+ "memories.new": "+ New Memory",
289
+ "memories.edit": "Edit",
290
+ "memories.save": "Save",
291
+ "memories.cancel": "Cancel",
292
+ "memories.filenamePh": "topic-name.md",
293
+ "memories.contentPh": "Markdown content with optional YAML frontmatter…",
294
+ "memories.filenameRequired": "Filename is required (e.g. topic-name.md).",
295
+
198
296
  "skills.btn.new": "New Skill",
199
297
  "skills.btn.create": "Create",
200
298
  "skills.btn.import": "Import",
@@ -288,6 +386,13 @@ const I18n = (() => {
288
386
  "settings.models.placeholder.baseurl": "https://api.anthropic.com",
289
387
  "settings.models.placeholder.apikey": "sk-…",
290
388
  "settings.models.custom": "Custom",
389
+ "settings.models.baseurl.noVariants": "No preset endpoints available",
390
+ "settings.models.baseurl.variant.mainland_cn": "Mainland China",
391
+ "settings.models.baseurl.variant.international": "International",
392
+ "settings.models.baseurl.variant.mainland_cn_payg": "Mainland · Pay-as-you-go",
393
+ "settings.models.baseurl.variant.mainland_cn_coding": "Mainland · Coding Plan",
394
+ "settings.models.baseurl.variant.international_payg": "International · Pay-as-you-go",
395
+ "settings.models.baseurl.variant.international_coding": "International · Coding Plan",
291
396
  "settings.models.btn.save": "Save",
292
397
  "settings.models.btn.saving": "Saving…",
293
398
  "settings.models.btn.saved": "Saved ✓",
@@ -412,6 +517,10 @@ const I18n = (() => {
412
517
  "sidebar.tasks": "任务管理",
413
518
  "sidebar.skills": "技能管理",
414
519
  "sidebar.channels": "频道管理",
520
+ "sidebar.trash": "文件召回",
521
+ "sidebar.profile": "助理记忆",
522
+ "sidebar.memories": "记忆",
523
+ "sidebar.dataSection": "我的数据",
415
524
  "sidebar.creator": "创作者中心",
416
525
  "sidebar.creatorSection": "创作者",
417
526
  "sidebar.settings": "设置",
@@ -588,6 +697,99 @@ const I18n = (() => {
588
697
  "creator.promo.link": "了解更多 →",
589
698
  "creator.cloud.locked": "需要品牌授权才能发布云端 Skill。",
590
699
 
700
+ // ── Creator trash ──
701
+ "trash.title": "文件召回",
702
+ "trash.subtitle": "AI 代理在各个项目里丢进回收站的文件。可以把它们召回到原位置,或彻底清理不再需要的。",
703
+ "trash.refresh": "刷新",
704
+ "trash.emptyOld": "清理 >7 天",
705
+ "trash.emptyOrphans":"清理孤儿",
706
+ "trash.emptyAll": "全部清空",
707
+ "trash.empty": "回收站空空如也。",
708
+ "trash.loading": "加载回收站…",
709
+ "trash.summary": "共 {{count}} 个文件,{{size}}",
710
+ "trash.summaryOrphans": "其中 {{count}} 条孤儿",
711
+ "trash.orphan": "孤儿",
712
+ "trash.orphanHint": "原项目目录已不存在(通常来自测试临时目录)。可以安全地永久删除。",
713
+ "trash.noOrphans": "未检测到孤儿条目。",
714
+ "trash.project": "项目",
715
+ "trash.deletedAt": "删除于",
716
+ "trash.restore": "恢复",
717
+ "trash.delete": "删除",
718
+ "trash.restoreOk": "已恢复:{{path}}",
719
+ "trash.restoreFail": "恢复失败:{{msg}}",
720
+ "trash.confirmDeleteOne": "永久删除 \"{{name}}\"?\n此操作不可撤销。",
721
+ "trash.confirmEmptyOld": "永久删除所有超过 7 天的回收站文件?\n此操作不可撤销。",
722
+ "trash.confirmEmptyAll": "永久删除所有项目的全部回收站文件?\n此操作不可撤销。",
723
+ "trash.confirmEmptyOrphans": "永久删除 {{count}} 条原项目已消失的孤儿条目?\n此操作不可撤销。",
724
+ "trash.emptied": "已永久删除 {{count}} 个文件,释放 {{size}}。",
725
+ "trash.orphansCleaned": "已清理 {{count}} 条孤儿,释放 {{size}}。(失败 {{failed}})",
726
+ "trash.nothingOld": "没有超过 7 天的文件。",
727
+
728
+ // ── Profile (助理记忆 — USER.md / SOUL.md / 记忆 统一只读视图) ──
729
+ "profile.title": "助理记忆",
730
+ "profile.subtitle": "这是一扇通往 AI 内心的窗口:它是谁、你是谁、以及它在和你共事中记住了什么。",
731
+ "profile.whoIAm": "我是谁",
732
+ "profile.whoYouAre": "你是谁",
733
+ "profile.statusDefault": "当前使用内置默认",
734
+ "profile.statusCustom": "已启用自定义",
735
+ "profile.loadFail": "加载失败",
736
+ "profile.emptyContent": "(无内容)",
737
+
738
+ // Profile tabs
739
+ "profile.tab.soul": "助理性格",
740
+ "profile.tab.user": "主人设定",
741
+ "profile.tab.memories": "记忆管理",
742
+
743
+ // Per-tab curate actions
744
+ "profile.soul.curateHint": "感觉不太对?让助理通过一段简短对话帮你调整。",
745
+ "profile.soul.curateBtn": "让助理整理性格",
746
+ "profile.user.curateHint": "换工作了?有新兴趣了?让助理帮你更新主人档案。",
747
+ "profile.user.curateBtn": "让助理更新档案",
748
+ "profile.curateFail": "无法启动整理会话",
749
+ "profile.curateName.soul": "整理性格",
750
+ "profile.curateName.user": "整理主人档案",
751
+
752
+ // 旧 keys(兼容保留)
753
+ "profile.update": "更新资料",
754
+ "profile.updateFail": "无法启动更新会话",
755
+ "profile.user.placeholder": "告诉助手你的信息 —— 姓名、角色、偏好、工作风格……",
756
+ "profile.soul.placeholder": "描述助手的性格 —— 语气、价值观、沟通风格……",
757
+ "profile.save": "保存",
758
+ "profile.reset": "恢复默认",
759
+ "profile.savedOk": "已保存 ✓",
760
+ "profile.resetOk": "已恢复默认 ✓",
761
+ "profile.saveFail": "保存失败",
762
+ "profile.confirmReset": "确认清空自定义内容,恢复为内置默认吗?",
763
+
764
+ // ── Memories(嵌在助理记忆页) ──
765
+ "memories.title": "记忆",
766
+ "memories.subtitle": "保存在 ~/.clacky/memories/ 的长期记忆,按最近更新倒序。点击「整理」让助理帮你打理这条记忆,或点击「删除」丢弃。",
767
+ "memories.curate": "整理",
768
+ "memories.curateTitle": "开启一个会话,让助理和你一起整理这条记忆",
769
+ "memories.curateFail": "无法启动记忆会话",
770
+ "memories.curateName": "整理记忆",
771
+ "memories.delete": "删除",
772
+ "memories.deleteTitle": "删除这条记忆",
773
+ "memories.deleteFail": "删除失败",
774
+ "memories.confirmDelete": "删除记忆 \"{{name}}\" 吗?\n会先放入「文件召回」,仍可从那里找回。",
775
+ "memories.expandTitle": "展开完整内容",
776
+ "memories.reloadList": "↻ 刷新列表",
777
+ "memories.loading": "加载中…",
778
+ "memories.empty": "还没有记忆。助手会在后续工作中自动创建。",
779
+ "memories.emptyHint": "",
780
+ "memories.summary": "共 {{count}} 条记忆",
781
+ // 旧 keys(兼容保留)
782
+ "memories.refresh": "整理",
783
+ "memories.refreshTitle": "开启一个会话,让助理和你一起整理这条记忆",
784
+ "memories.refreshFail": "无法启动记忆会话",
785
+ "memories.new": "+ 新建记忆",
786
+ "memories.edit": "编辑",
787
+ "memories.save": "保存",
788
+ "memories.cancel": "取消",
789
+ "memories.filenamePh": "topic-name.md",
790
+ "memories.contentPh": "Markdown 内容,可带 YAML frontmatter…",
791
+ "memories.filenameRequired": "请填写文件名(如 topic-name.md)。",
792
+
591
793
  "skills.btn.new": "新建技能",
592
794
  "skills.btn.create": "创建",
593
795
  "skills.btn.import": "导入",
@@ -681,6 +883,13 @@ const I18n = (() => {
681
883
  "settings.models.placeholder.baseurl": "https://api.anthropic.com",
682
884
  "settings.models.placeholder.apikey": "sk-…",
683
885
  "settings.models.custom": "自定义",
886
+ "settings.models.baseurl.noVariants": "暂无预设端点",
887
+ "settings.models.baseurl.variant.mainland_cn": "中国大陆",
888
+ "settings.models.baseurl.variant.international": "海外",
889
+ "settings.models.baseurl.variant.mainland_cn_payg": "大陆 · 按量付费",
890
+ "settings.models.baseurl.variant.mainland_cn_coding": "大陆 · Coding Plan",
891
+ "settings.models.baseurl.variant.international_payg": "海外 · 按量付费",
892
+ "settings.models.baseurl.variant.international_coding": "海外 · Coding Plan",
684
893
  "settings.models.btn.save": "保存",
685
894
  "settings.models.btn.saving": "保存中…",
686
895
  "settings.models.btn.saved": "已保存 ✓",
@@ -161,6 +161,42 @@
161
161
  </div>
162
162
  </div>
163
163
 
164
+ <!-- My Data Group — personal data management (profile, memories, trash) -->
165
+ <div id="data-section">
166
+ <div class="sidebar-divider"><span data-i18n="sidebar.dataSection">My Data</span></div>
167
+ <div id="data-nav-items">
168
+ <div id="profile-sidebar-item" class="task-item task-item-summary">
169
+ <div class="task-row">
170
+ <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="task-icon">
171
+ <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
172
+ <circle cx="12" cy="7" r="4"/>
173
+ </svg>
174
+ <div class="task-info">
175
+ <span class="task-name" id="profile-sidebar-label" data-i18n="sidebar.profile">Profile &amp; Soul</span>
176
+ </div>
177
+ </div>
178
+ </div>
179
+ <div id="memories-sidebar-item" class="task-item task-item-summary" style="display:none" aria-hidden="true">
180
+ <!-- Deprecated: Memories is now merged into the Profile panel.
181
+ Kept as a hidden placeholder so any legacy CSS selector that
182
+ targets this id still resolves harmlessly. -->
183
+ </div>
184
+ <div id="trash-sidebar-item" class="task-item task-item-summary">
185
+ <div class="task-row">
186
+ <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="task-icon">
187
+ <polyline points="3 6 5 6 21 6"/>
188
+ <path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
189
+ <path d="M10 11v6"/><path d="M14 11v6"/>
190
+ <path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>
191
+ </svg>
192
+ <div class="task-info">
193
+ <span class="task-name" id="trash-sidebar-label" data-i18n="sidebar.trash">File Recall</span>
194
+ </div>
195
+ </div>
196
+ </div>
197
+ </div>
198
+ </div>
199
+
164
200
  <!-- Creator Group — only shown when user_licensed -->
165
201
  <div id="creator-section" style="display:none">
166
202
  <div class="sidebar-divider"><span data-i18n="sidebar.creatorSection">Creator</span></div>
@@ -324,7 +360,7 @@
324
360
  data-i18n-placeholder="chat.input.placeholder"
325
361
  placeholder="Message… (Enter to send, Shift+Enter for newline)"></textarea>
326
362
  <button id="btn-send" data-i18n="chat.btn.send">Send</button>
327
- <button id="btn-interrupt" style="display:none" title="Stop">■</button>
363
+ <button id="btn-interrupt" style="display:none" title="Stop"></button>
328
364
  </div>
329
365
  </div>
330
366
  </div>
@@ -490,6 +526,124 @@
490
526
  </div>
491
527
  </div>
492
528
 
529
+ <!-- ── TRASH PANEL — Recently deleted files across all projects ─── -->
530
+ <div id="trash-panel" style="display:none">
531
+ <div id="trash-body">
532
+ <div class="channels-page-header">
533
+ <h2 class="channels-page-title" data-i18n="trash.title">File Recall</h2>
534
+ <p class="channels-page-subtitle" data-i18n="trash.subtitle">Files the agent moved to trash across all projects. Recall them back to where they were, or clear the ones you don't need.</p>
535
+ </div>
536
+
537
+ <div class="trash-toolbar">
538
+ <span class="trash-summary" id="trash-summary"></span>
539
+ <div class="trash-actions">
540
+ <button id="btn-trash-refresh" class="btn-trash-action" title="Refresh">
541
+ <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
542
+ <path d="M21 2v6h-6"/><path d="M3 12a9 9 0 0 1 15-6.7L21 8"/>
543
+ <path d="M3 22v-6h6"/><path d="M21 12a9 9 0 0 1-15 6.7L3 16"/>
544
+ </svg>
545
+ <span data-i18n="trash.refresh">Refresh</span>
546
+ </button>
547
+ <button id="btn-trash-empty-old" class="btn-trash-action" title="Permanently delete files older than 7 days">
548
+ <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
549
+ <circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>
550
+ </svg>
551
+ <span data-i18n="trash.emptyOld">Empty &gt;7 days</span>
552
+ </button>
553
+ <button id="btn-trash-empty-orphans" class="btn-trash-action" title="Permanently delete entries whose original project directory no longer exists (e.g. test temp dirs)">
554
+ <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
555
+ <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
556
+ <line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>
557
+ </svg>
558
+ <span data-i18n="trash.emptyOrphans">Clean orphans</span>
559
+ </button>
560
+ <button id="btn-trash-empty-all" class="btn-trash-action btn-trash-danger" title="Permanently delete all trashed files">
561
+ <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
562
+ <polyline points="3 6 5 6 21 6"/>
563
+ <path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
564
+ <path d="M10 11v6"/><path d="M14 11v6"/>
565
+ <path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>
566
+ </svg>
567
+ <span data-i18n="trash.emptyAll">Empty all</span>
568
+ </button>
569
+ </div>
570
+ </div>
571
+
572
+ <div id="trash-list"></div>
573
+ </div>
574
+ </div>
575
+
576
+ <!-- ── PROFILE PANEL — "Assistant Memory": a 3-tab window into the agent's inner state.
577
+ All mutations happen through sessions (never in-browser editors):
578
+ SOUL tab → [让助理整理性格] → /onboard scope:soul
579
+ USER tab → [让助理更新档案] → /onboard scope:user
580
+ Memory tab → per-card [整理] → /onboard path:<abs>
581
+ per-card [删除] → direct DELETE /api/memories/:filename (with confirm)
582
+ ────────────────────────────────────────────────────────────── -->
583
+ <div id="profile-panel" style="display:none">
584
+ <div id="profile-body">
585
+ <div class="channels-page-header">
586
+ <h2 class="channels-page-title" data-i18n="profile.title">Assistant Memory</h2>
587
+ <p class="channels-page-subtitle" data-i18n="profile.subtitle">A window into the assistant's inner life: who it is, who you are, and what it remembers about your work together.</p>
588
+ </div>
589
+
590
+ <!-- Tab bar -->
591
+ <div class="profile-tabs" role="tablist">
592
+ <button class="profile-tab active" role="tab" aria-selected="true"
593
+ data-tab="soul" id="profile-tab-soul" data-i18n="profile.tab.soul">Soul</button>
594
+ <button class="profile-tab" role="tab" aria-selected="false"
595
+ data-tab="user" id="profile-tab-user" data-i18n="profile.tab.user">User</button>
596
+ <button class="profile-tab" role="tab" aria-selected="false"
597
+ data-tab="memories" id="profile-tab-memories" data-i18n="profile.tab.memories">Memories</button>
598
+ </div>
599
+
600
+ <!-- Tab: SOUL.md -->
601
+ <section class="profile-tab-panel active" id="profile-pane-soul" role="tabpanel" aria-labelledby="profile-tab-soul">
602
+ <header class="profile-section-head">
603
+ <h3 class="profile-section-title" data-i18n="profile.whoIAm">Who I am</h3>
604
+ <div class="profile-section-meta">
605
+ <span class="profile-path" id="profile-soul-path"></span>
606
+ <span class="profile-status" id="profile-soul-status"></span>
607
+ </div>
608
+ </header>
609
+ <div id="profile-soul-body" class="profile-markdown"></div>
610
+ <div class="profile-pane-footer">
611
+ <p class="profile-pane-footer-hint" data-i18n="profile.soul.curateHint">Not quite right? Let the assistant curate this through a short conversation.</p>
612
+ <button id="btn-profile-curate-soul" class="btn-profile-update" data-i18n="profile.soul.curateBtn">Have the assistant curate this</button>
613
+ </div>
614
+ </section>
615
+
616
+ <!-- Tab: USER.md -->
617
+ <section class="profile-tab-panel" id="profile-pane-user" role="tabpanel" aria-labelledby="profile-tab-user" style="display:none">
618
+ <header class="profile-section-head">
619
+ <h3 class="profile-section-title" data-i18n="profile.whoYouAre">Who you are</h3>
620
+ <div class="profile-section-meta">
621
+ <span class="profile-path" id="profile-user-path"></span>
622
+ <span class="profile-status" id="profile-user-status"></span>
623
+ </div>
624
+ </header>
625
+ <div id="profile-user-body" class="profile-markdown"></div>
626
+ <div class="profile-pane-footer">
627
+ <p class="profile-pane-footer-hint" data-i18n="profile.user.curateHint">Changed jobs? Picked up new interests? Let the assistant update your profile.</p>
628
+ <button id="btn-profile-curate-user" class="btn-profile-update" data-i18n="profile.user.curateBtn">Have the assistant update this</button>
629
+ </div>
630
+ </section>
631
+
632
+ <!-- Tab: Memories -->
633
+ <section class="profile-tab-panel" id="profile-pane-memories" role="tabpanel" aria-labelledby="profile-tab-memories" style="display:none">
634
+ <header class="profile-section-head">
635
+ <h3 class="profile-section-title" data-i18n="memories.title">Memories</h3>
636
+ <div class="profile-section-meta">
637
+ <span class="memories-summary" id="memories-summary"></span>
638
+ <button id="btn-memories-refresh-list" class="btn-memories-mini" data-i18n-title="memories.reloadList" data-i18n="memories.reloadList">Reload</button>
639
+ </div>
640
+ </header>
641
+ <p class="profile-section-hint" data-i18n="memories.subtitle">Long-term memories stored under ~/.clacky/memories/. Most recent first. Use "Curate" to let the assistant tidy a memory, or "Delete" to drop one.</p>
642
+ <div id="memories-list"></div>
643
+ </section>
644
+ </div>
645
+ </div>
646
+
493
647
  <!-- ── SETTINGS PANEL ──────────────────────────────────────────────── -->
494
648
  <div id="settings-panel" style="display:none">
495
649
  <header id="settings-header">
@@ -704,7 +858,13 @@
704
858
 
705
859
  <div class="setup-field">
706
860
  <label class="setup-label" data-i18n="onboard.key.baseurl">Base URL</label>
707
- <input id="setup-base-url" type="text" class="setup-input" data-i18n-placeholder="settings.models.placeholder.baseurl" placeholder="https://api.anthropic.com">
861
+ <div class="base-url-combobox">
862
+ <input id="setup-base-url" type="text" class="setup-input base-url-input" data-i18n-placeholder="settings.models.placeholder.baseurl" placeholder="https://api.anthropic.com">
863
+ <button type="button" id="setup-base-url-dropdown-btn" class="base-url-dropdown-btn" aria-label="Select preset endpoint">
864
+ <svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
865
+ </button>
866
+ <div id="setup-base-url-dropdown" class="base-url-dropdown" style="display:none"></div>
867
+ </div>
708
868
  </div>
709
869
 
710
870
  <div class="setup-field">
@@ -815,6 +975,7 @@
815
975
  <script src="/auth.js"></script>
816
976
  <script src="/theme.js"></script>
817
977
  <script src="/ws.js"></script>
978
+ <script src="/ws-dispatcher.js"></script>
818
979
  <script src="/sessions.js"></script>
819
980
  <script src="/tasks.js"></script>
820
981
  <script src="/skills.js"></script>
@@ -823,7 +984,10 @@
823
984
  <script src="/onboard.js"></script>
824
985
  <script src="/brand.js"></script>
825
986
  <script src="/creator.js"></script>
987
+ <script src="/trash.js"></script>
988
+ <script src="/profile.js"></script>
826
989
  <script src="/version.js"></script>
990
+ <script src="/sidebar.js"></script>
827
991
  <script src="/app.js"></script>
828
992
  </body>
829
993
  </html>
@@ -171,6 +171,60 @@ const Onboard = (() => {
171
171
  });
172
172
  }
173
173
 
174
+ // ── Populate Base URL dropdown options from preset.endpoint_variants ──────
175
+ // Mirrors settings.js _renderBaseUrlDropdown but for the single-shot
176
+ // onboarding combobox. Called whenever the provider changes so the dropdown
177
+ // reflects the currently selected preset (empty for Custom / presets with
178
+ // no variants — button stays inert in that case).
179
+ function _updateSetupBaseUrlDropdown(preset) {
180
+ const dd = $("setup-base-url-dropdown");
181
+ if (!dd) return;
182
+ dd.innerHTML = "";
183
+
184
+ const variants = preset && Array.isArray(preset.endpoint_variants)
185
+ ? preset.endpoint_variants
186
+ : [];
187
+
188
+ if (variants.length === 0) {
189
+ // Leave dd empty; the dropdown-btn still toggles but shows a no-variant
190
+ // hint to signal "this provider has only one endpoint".
191
+ const empty = document.createElement("div");
192
+ empty.className = "model-dropdown-empty";
193
+ empty.textContent = I18n.t("settings.models.baseurl.noVariants");
194
+ dd.appendChild(empty);
195
+ return;
196
+ }
197
+
198
+ variants.forEach(v => {
199
+ // Prefer i18n key (localised per UI language); fall back to literal
200
+ // `label` (shipped English copy) then base_url for safety.
201
+ const translated = v.label_key ? I18n.t(v.label_key) : null;
202
+ const labelText = (translated && translated !== v.label_key) ? translated : (v.label || v.base_url);
203
+
204
+ const opt = document.createElement("div");
205
+ opt.className = "model-dropdown-option base-url-dropdown-option";
206
+ opt.dataset.value = v.base_url;
207
+
208
+ const lbl = document.createElement("div");
209
+ lbl.className = "base-url-dropdown-label";
210
+ lbl.textContent = labelText;
211
+
212
+ const url = document.createElement("div");
213
+ url.className = "base-url-dropdown-url";
214
+ url.textContent = v.base_url;
215
+
216
+ opt.appendChild(lbl);
217
+ opt.appendChild(url);
218
+
219
+ opt.addEventListener("click", (e) => {
220
+ e.stopPropagation();
221
+ $("setup-base-url").value = v.base_url;
222
+ dd.style.display = "none";
223
+ });
224
+ dd.appendChild(opt);
225
+ });
226
+ }
227
+
174
228
  function _bindCustomDropdown() {
175
229
  if (_dropdownBound) return; // listeners already attached
176
230
  _dropdownBound = true;
@@ -207,6 +261,7 @@ const Onboard = (() => {
207
261
  $("setup-model").value = "";
208
262
  $("setup-base-url").value = "";
209
263
  _updateSetupModelDropdown([]);
264
+ _updateSetupBaseUrlDropdown(null);
210
265
  if (getApiKeyLink) getApiKeyLink.style.display = "none";
211
266
  } else if (value) {
212
267
  const preset = _providers.find(p => p.id === value);
@@ -214,6 +269,7 @@ const Onboard = (() => {
214
269
  $("setup-model").value = preset.default_model || "";
215
270
  $("setup-base-url").value = preset.base_url || "";
216
271
  _updateSetupModelDropdown(preset.models || []);
272
+ _updateSetupBaseUrlDropdown(preset);
217
273
  // Show "how to get" link if provider has a website_url
218
274
  if (getApiKeyLink && preset.website_url) {
219
275
  getApiKeyLink.href = preset.website_url;
@@ -262,11 +318,30 @@ const Onboard = (() => {
262
318
  modelDropdownBtn.addEventListener("click", (e) => {
263
319
  e.stopPropagation();
264
320
  const isOpen = modelDropdown.style.display === "block";
321
+ // Close sibling dropdown to avoid overlap.
322
+ $("setup-base-url-dropdown").style.display = "none";
265
323
  modelDropdown.style.display = isOpen ? "none" : "block";
266
324
  });
267
325
 
268
- document.addEventListener("click", () => {
326
+ // ── Base URL combobox dropdown ────────────────────────────────────────────
327
+ // Shows preset.endpoint_variants (e.g. GLM mainland vs Z.ai international).
328
+ // Rendering is driven by the currently selected provider — see the
329
+ // provider custom-select handler above, which calls
330
+ // _updateSetupBaseUrlDropdown(preset) on every switch.
331
+ const baseUrlDropdownBtn = $("setup-base-url-dropdown-btn");
332
+ const baseUrlDropdown = $("setup-base-url-dropdown");
333
+
334
+ baseUrlDropdownBtn.addEventListener("click", (e) => {
335
+ e.stopPropagation();
336
+ const isOpen = baseUrlDropdown.style.display === "block";
337
+ // Close sibling dropdown (model combobox) to avoid overlap.
269
338
  modelDropdown.style.display = "none";
339
+ baseUrlDropdown.style.display = isOpen ? "none" : "block";
340
+ });
341
+
342
+ document.addEventListener("click", () => {
343
+ modelDropdown.style.display = "none";
344
+ baseUrlDropdown.style.display = "none";
270
345
  });
271
346
 
272
347
  $("setup-btn-test").addEventListener("click", _testAndSave);
@@ -388,6 +463,7 @@ const Onboard = (() => {
388
463
  // Boot the normal UI (WS + sessions sidebar + tasks + skills).
389
464
  function _bootUI() {
390
465
  document.body.classList.remove("setup-mode");
466
+ SkillAC.init();
391
467
  WS.connect();
392
468
  Tasks.load();
393
469
  Skills.load();