openclacky 1.5.11 → 1.5.13

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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +82 -0
  3. data/lib/clacky/access_key.rb +59 -0
  4. data/lib/clacky/agent/cost_tracker.rb +2 -0
  5. data/lib/clacky/agent/message_compressor_helper.rb +53 -2
  6. data/lib/clacky/agent/session_serializer.rb +72 -19
  7. data/lib/clacky/agent/time_machine.rb +10 -1
  8. data/lib/clacky/agent.rb +107 -26
  9. data/lib/clacky/agent_config.rb +40 -0
  10. data/lib/clacky/agent_profile.rb +5 -5
  11. data/lib/clacky/billing/billing_record.rb +3 -0
  12. data/lib/clacky/billing/billing_store.rb +31 -5
  13. data/lib/clacky/billing/platform_billing.rb +231 -0
  14. data/lib/clacky/brand_config.rb +4 -3
  15. data/lib/clacky/cancel_flag.rb +23 -0
  16. data/lib/clacky/cli.rb +4 -2
  17. data/lib/clacky/client.rb +107 -0
  18. data/lib/clacky/default_extensions/advisor/advisors/general.md +23 -17
  19. data/lib/clacky/default_extensions/advisor/hooks/advisor.rb +104 -5
  20. data/lib/clacky/default_extensions/advisor/panels/advisor/view.js +92 -75
  21. data/lib/clacky/default_extensions/ext-studio/api/handler.rb +31 -0
  22. data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +74 -24
  23. data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +16 -37
  24. data/lib/clacky/default_extensions/git/ext.yml +1 -1
  25. data/lib/clacky/default_extensions/git/panels/git/view.js +483 -67
  26. data/lib/clacky/default_extensions/meeting/ext.yml +2 -1
  27. data/lib/clacky/default_extensions/time_machine/ext.yml +1 -1
  28. data/lib/clacky/default_extensions/time_machine/panels/time_machine/view.js +186 -208
  29. data/lib/clacky/extension/packager.rb +4 -2
  30. data/lib/clacky/extension/scaffold/templates/full/panels/dashboard/view.js.erb +2 -3
  31. data/lib/clacky/extension/verifier.rb +14 -1
  32. data/lib/clacky/fanout.rb +49 -2
  33. data/lib/clacky/json_ui_controller.rb +3 -2
  34. data/lib/clacky/message_format/open_ai.rb +5 -1
  35. data/lib/clacky/message_format/open_ai_responses.rb +409 -0
  36. data/lib/clacky/message_history.rb +43 -1
  37. data/lib/clacky/openai_responses_stream_aggregator.rb +294 -0
  38. data/lib/clacky/plain_ui_controller.rb +15 -5
  39. data/lib/clacky/providers.rb +24 -7
  40. data/lib/clacky/rich_ui/rich_ui_controller.rb +2 -2
  41. data/lib/clacky/server/channel/channel_manager.rb +93 -18
  42. data/lib/clacky/server/channel/channel_ui_controller.rb +16 -3
  43. data/lib/clacky/server/dir_picker.rb +154 -0
  44. data/lib/clacky/server/git_panel.rb +61 -9
  45. data/lib/clacky/server/http_server.rb +366 -42
  46. data/lib/clacky/server/web_ui_controller.rb +7 -5
  47. data/lib/clacky/session_manager.rb +74 -17
  48. data/lib/clacky/tools/ask_user.rb +3 -2
  49. data/lib/clacky/tools/terminal.rb +30 -3
  50. data/lib/clacky/ui2/markdown_renderer.rb +15 -12
  51. data/lib/clacky/ui2/strings_cjk_patch.rb +132 -0
  52. data/lib/clacky/ui2/themes/hacker_theme.rb +1 -1
  53. data/lib/clacky/ui2/themes/minimal_theme.rb +1 -1
  54. data/lib/clacky/ui2/ui_controller.rb +2 -2
  55. data/lib/clacky/ui_interface.rb +2 -2
  56. data/lib/clacky/utils/model_pricing.rb +92 -4
  57. data/lib/clacky/version.rb +1 -1
  58. data/lib/clacky/web/app.css +915 -142
  59. data/lib/clacky/web/components/code-editor.js +33 -1
  60. data/lib/clacky/web/components/composer.js +315 -0
  61. data/lib/clacky/web/components/custom-select.js +136 -0
  62. data/lib/clacky/web/components/mentions.js +655 -0
  63. data/lib/clacky/web/components/model-picker.js +527 -0
  64. data/lib/clacky/web/components/onboard.js +29 -55
  65. data/lib/clacky/web/core/aside.js +3 -3
  66. data/lib/clacky/web/core/ext.js +38 -0
  67. data/lib/clacky/web/features/billing/store.js +2 -6
  68. data/lib/clacky/web/features/billing/view.js +32 -16
  69. data/lib/clacky/web/features/extensions/view.js +22 -10
  70. data/lib/clacky/web/features/new-session/store.js +15 -1
  71. data/lib/clacky/web/features/new-session/view.js +80 -34
  72. data/lib/clacky/web/features/trash/view.js +31 -19
  73. data/lib/clacky/web/features/workspace/store.js +1 -0
  74. data/lib/clacky/web/features/workspace/view.js +766 -80
  75. data/lib/clacky/web/i18n.js +107 -17
  76. data/lib/clacky/web/index.html +125 -45
  77. data/lib/clacky/web/sessions.js +704 -635
  78. data/lib/clacky/web/settings.js +164 -190
  79. data/lib/clacky/web/skills.js +11 -17
  80. data/lib/clacky/web/ws-dispatcher.js +11 -33
  81. data/lib/clacky.rb +4 -0
  82. metadata +12 -1
@@ -66,6 +66,7 @@ const I18n = (() => {
66
66
 
67
67
  // ── Welcome screen ──
68
68
  "sessions.new.title": "Start a new session",
69
+ "sessions.new.title.named": "Hi, {{name}}. Ready to get started?",
69
70
  "sessions.new.subtitle": "Pick an agent to chat with, then tell it what you'd like to do.",
70
71
  "sessions.new.placeholder": "Type your first message to {{agent}}…",
71
72
  "sessions.new.moreOptions": "More options",
@@ -76,9 +77,9 @@ const I18n = (() => {
76
77
  "chat.status.idle": "idle",
77
78
  "chat.status.running": "running",
78
79
  "chat.status.error": "error",
79
- "chat.input.placeholder": "Message… (Enter to send, Shift+Enter for newline)",
80
+ "chat.input.placeholder": "What can I help you with today? @ reference a chat or file, / for skills, Enter to send, Shift + Enter for newline",
80
81
  "chat.input.placeholderRunning": "AI is working — you can still send extra info anytime...",
81
- "chat.input.placeholderMobile": "Message…",
82
+ "chat.input.placeholderMobile": "What can I help you with today?",
82
83
  "chat.input.placeholderRunningMobile": "Add more info…",
83
84
  "chat.btn.send": "Send",
84
85
  "chat.thinking": "Thinking…",
@@ -119,8 +120,9 @@ const I18n = (() => {
119
120
  "chat.send": "Send",
120
121
  "chat.edit.warn": "Editing this message will discard the conversation history, file snapshots, and token costs after it. Continue?",
121
122
  "chat.edit.warnSkip": "Don't show this again",
122
- "chat.empty.title": "How can I help you today?",
123
- "chat.empty.subtitle": "Ask a question, or start with a skill.",
123
+ "chat.empty.title": "How can I help you today?",
124
+ "chat.empty.title.named": "Hi, {{name}}. Ready to get started?",
125
+ "chat.empty.subtitle": "Ask a question, or start with a skill.",
124
126
  "chat.empty.tip1": "Research, write, and translate",
125
127
  "chat.empty.tip2": "Summarize and organize information",
126
128
  "chat.empty.tip3": "Upload an image or file for me to analyze",
@@ -155,6 +157,12 @@ const I18n = (() => {
155
157
  "sib.dir.default": "Default workspace",
156
158
  "sib.dir.current": "Current workspace",
157
159
  "sib.dir.inputPlaceholder": "Type or select a directory path",
160
+ "sib.dir.back": "Back",
161
+ "sib.dir.forward": "Forward",
162
+ "sib.dir.refresh": "Refresh",
163
+ "sib.dir.addFavorite": "Add to favorites",
164
+ "sib.dir.removeFavorite": "Remove from favorites",
165
+ "sib.dir.userFavorites": "My favorites",
158
166
  "sib.dir.loading": "Loading…",
159
167
  "sib.dir.empty": "Empty directory",
160
168
  "sib.dir.loadError": "Failed to load",
@@ -164,24 +172,39 @@ const I18n = (() => {
164
172
  "sib.dir.newFolder": "New folder",
165
173
  "sib.dir.newFolderDefault": "New Folder",
166
174
  "sib.dir.mkdirError": "Failed to create folder: {{msg}}",
175
+ "sib.dir.favorites": "Quick access",
176
+ "sib.dir.places.home": "Home",
177
+ "sib.dir.places.desktop": "Desktop",
178
+ "sib.dir.places.downloads": "Downloads",
179
+ "sib.dir.places.documents": "Documents",
180
+ "sib.dir.drives": "Drives",
181
+ "sib.dir.places.drive": "{{letter}}:",
167
182
  "workspace.title": "Files",
168
183
  "workspace.expand": "Open workspace",
169
184
  "workspace.collapse": "Collapse workspace",
170
185
  "workspace.refresh": "Refresh",
171
186
  "workspace.empty": "This folder is empty",
172
187
  "workspace.loading": "Loading…",
188
+ "workspace.search": "Search files",
189
+ "workspace.noResults": "No matching files",
173
190
  "workspace.error": "Failed to load files",
174
191
  "workspace.downloadFailed": "Download failed",
175
192
  "workspace.previewFailed": "Failed to preview file",
176
193
  "workspace.previewUnsupported": "Cannot preview this file type",
177
194
  "workspace.download": "Download",
178
- "workspace.contextMenuHint": "Right-click for more options",
179
195
  "workspace.copyPath": "Copy Path",
180
196
  "workspace.copyRelPath": "Copy Relative Path",
181
197
  "aside.collapse": "Collapse panel",
182
198
  "aside.expand": "Open panel",
183
199
  "workspace.revealInFinder": "Reveal in Finder",
184
200
  "workspace.revealFailed": "Failed to reveal file",
201
+ "workspace.closeTab": "Close",
202
+ "workspace.copyContent": "Copy content",
203
+ "workspace.copied": "Copied",
204
+ "workspace.copyFailed": "Copy failed",
205
+ "workspace.saved": "Saved",
206
+ "workspace.saveFailed": "Save failed",
207
+ "workspace.dirtyCloseConfirm": "This file has unsaved changes. Close it anyway?",
185
208
  "changes.tab": "Git",
186
209
  "changes.loading": "Loading changes…",
187
210
  "changes.error": "Failed to load changes",
@@ -198,9 +221,23 @@ const I18n = (() => {
198
221
  "changes.save.prefix": "Manual snapshot",
199
222
  "changes.save.btn": "Save snapshot",
200
223
  "changes.save.saving": "Saving…",
201
- "changes.save.done": "Saved — restore it from History",
224
+ "changes.save.done": "Saved",
202
225
  "changes.save.failed": "Save failed",
203
- "tm.tab": "History",
226
+ "changes.select.all": "All",
227
+ "changes.select.count": "{{n}}/{{total}} selected",
228
+ "changes.save.btnN": "Save {{n}} selected file(s)",
229
+ "changes.save.promptMsg": "Describe this snapshot",
230
+ "changes.diff.loading": "Loading diff…",
231
+ "changes.diff.fail": "Failed to load diff",
232
+ "changes.diff.same": "No content change in this file.",
233
+ "changes.diff.binary": "Binary file - line diff not available.",
234
+ "changes.diff.truncated": "Diff too long - showing first {{n}} lines",
235
+ "changes.restore.title": "Revert this file's changes",
236
+ "changes.restore.confirm": "Discard?",
237
+ "changes.restore.diffBtn": "Discard changes",
238
+ "changes.restore.doing": "Reverting…",
239
+ "changes.restore.done": "Reverted: {{path}}",
240
+ "changes.restore.failed": "Revert failed", "tm.tab": "History",
204
241
  "sib.model.tooltip": "Click to switch model",
205
242
  "sib.model.tooltip.busy": "Model switching is disabled while the agent is responding",
206
243
  "sib.variant.header": "Quick switch",
@@ -240,6 +277,8 @@ const I18n = (() => {
240
277
  "sessions.badge.channel": "Channel",
241
278
  "sessions.badge.coding": "Coding",
242
279
  "sessions.badge.setup": "Setup",
280
+ "sessions.setup.name": "Onboard",
281
+ "sessions.setup.browser.name": "🌐 Browser Setup",
243
282
  "sessions.newSession": "New Session",
244
283
  "sessions.advancedOptions": "Advanced options",
245
284
  "sessions.actions.pin": "Pin",
@@ -750,6 +789,7 @@ const I18n = (() => {
750
789
  "settings.models.apiFormat.auto": "Auto (detect from provider & model)",
751
790
  "settings.models.apiFormat.anthropicMessages": "Anthropic Messages API",
752
791
  "settings.models.apiFormat.openaiCompletions": "OpenAI Chat Completions",
792
+ "settings.models.apiFormat.openaiResponses": "OpenAI Responses API",
753
793
  "settings.models.field.getApiKey": "How to get →",
754
794
  "settings.models.field.docsGuide.question": "New to AI keys?",
755
795
  "settings.models.field.docsGuide.cta": "See the guide →",
@@ -956,7 +996,7 @@ const I18n = (() => {
956
996
  "onboard.device.success.credit.label": "Free trial credit:",
957
997
  "onboard.device.success.credit.value": "$1.00 (one-time, on us)",
958
998
  "onboard.device.success.model.label": "Trial model:",
959
- "onboard.device.success.note": "During the trial only Gemini 3.5 Flash is available — top up any amount to unlock all frontier models on this same key.",
999
+ "onboard.device.success.note": "During the trial only Gemini 3.8 Flash is available — top up any amount to unlock all frontier models on this same key.",
960
1000
  "onboard.device.success.btn": "Start using Clacky →",
961
1001
  "onboard.manual.toggle": "Configure manually with your own API key",
962
1002
  "onboard.provider.custom": "Custom",
@@ -1199,6 +1239,7 @@ const I18n = (() => {
1199
1239
 
1200
1240
  // ── Welcome screen ──
1201
1241
  "sessions.new.title": "开始新会话",
1242
+ "sessions.new.title.named": "你好,{{name}}。准备好开始了吗?",
1202
1243
  "sessions.new.subtitle": "选一个想聊的 Agent,说说你想做什么。",
1203
1244
  "sessions.new.placeholder": "输入第一条消息发给 {{agent}}…",
1204
1245
  "sessions.new.moreOptions": "更多选项",
@@ -1209,9 +1250,9 @@ const I18n = (() => {
1209
1250
  "chat.status.idle": "空闲",
1210
1251
  "chat.status.running": "运行中",
1211
1252
  "chat.status.error": "出错",
1212
- "chat.input.placeholder": "输入消息…(Enter 发送,Shift+Enter 换行)",
1253
+ "chat.input.placeholder": "今天帮你做些什么?@ 引用会话或文件,/ 调用技能,Enter 发送,Shift + Enter 换行",
1213
1254
  "chat.input.placeholderRunning": "AI 正在工作,你仍然可以随时补充新信息给它...",
1214
- "chat.input.placeholderMobile": "输入消息…",
1255
+ "chat.input.placeholderMobile": "今天帮你做些什么?",
1215
1256
  "chat.input.placeholderRunningMobile": "可输入补充消息…",
1216
1257
  "chat.btn.send": "发送",
1217
1258
  "chat.thinking": "思考中…",
@@ -1250,8 +1291,9 @@ const I18n = (() => {
1250
1291
  "chat.send": "发送",
1251
1292
  "chat.edit.warn": "编辑此消息将丢弃后续对话历史、文件快照及已产生的 token 费用,是否继续?",
1252
1293
  "chat.edit.warnSkip": "不再提示",
1253
- "chat.empty.title": "有什么可以帮你?",
1254
- "chat.empty.subtitle": "直接提问,或用一个技能开始。",
1294
+ "chat.empty.title": "有什么可以帮你?",
1295
+ "chat.empty.title.named": "你好,{{name}}。准备好开始了吗?",
1296
+ "chat.empty.subtitle": "直接提问,或用一个技能开始。",
1255
1297
  "chat.empty.tip1": "查资料、写文案、做翻译",
1256
1298
  "chat.empty.tip2": "归纳总结、整理信息",
1257
1299
  "chat.empty.tip3": "上传图片或文件,让我帮你分析",
@@ -1286,6 +1328,12 @@ const I18n = (() => {
1286
1328
  "sib.dir.default": "默认工作目录",
1287
1329
  "sib.dir.current": "当前工作目录",
1288
1330
  "sib.dir.inputPlaceholder": "输入或选择目录路径",
1331
+ "sib.dir.back": "后退",
1332
+ "sib.dir.forward": "前进",
1333
+ "sib.dir.refresh": "刷新",
1334
+ "sib.dir.addFavorite": "添加到收藏",
1335
+ "sib.dir.removeFavorite": "从收藏中移除",
1336
+ "sib.dir.userFavorites": "我的收藏",
1289
1337
  "sib.dir.loading": "加载中…",
1290
1338
  "sib.dir.empty": "空目录",
1291
1339
  "sib.dir.loadError": "加载失败",
@@ -1295,24 +1343,39 @@ const I18n = (() => {
1295
1343
  "sib.dir.newFolder": "新建文件夹",
1296
1344
  "sib.dir.newFolderDefault": "新建文件夹",
1297
1345
  "sib.dir.mkdirError": "新建文件夹失败:{{msg}}",
1346
+ "sib.dir.favorites": "快捷访问",
1347
+ "sib.dir.places.home": "主目录",
1348
+ "sib.dir.places.desktop": "桌面",
1349
+ "sib.dir.places.downloads": "下载",
1350
+ "sib.dir.places.documents": "文稿",
1351
+ "sib.dir.drives": "驱动器",
1352
+ "sib.dir.places.drive": "{{letter}}盘",
1298
1353
  "workspace.title": "文件",
1299
1354
  "workspace.expand": "打开工作区",
1300
1355
  "workspace.collapse": "收起工作区",
1301
1356
  "workspace.refresh": "刷新",
1302
1357
  "workspace.empty": "此文件夹为空",
1303
1358
  "workspace.loading": "加载中…",
1359
+ "workspace.search": "搜索文件",
1360
+ "workspace.noResults": "没有匹配的文件",
1304
1361
  "workspace.error": "加载文件失败",
1305
1362
  "workspace.downloadFailed": "下载失败",
1306
1363
  "workspace.previewFailed": "文件预览失败",
1307
1364
  "workspace.previewUnsupported": "无法预览此类型文件",
1308
1365
  "workspace.download": "下载",
1309
- "workspace.contextMenuHint": "右键显示更多菜单",
1310
1366
  "workspace.copyPath": "复制路径",
1311
1367
  "workspace.copyRelPath": "复制相对路径",
1312
1368
  "aside.collapse": "收起面板",
1313
1369
  "aside.expand": "打开面板",
1314
1370
  "workspace.revealInFinder": "打开所在文件夹",
1315
1371
  "workspace.revealFailed": "无法打开文件位置",
1372
+ "workspace.closeTab": "关闭",
1373
+ "workspace.copyContent": "复制内容",
1374
+ "workspace.copied": "已复制",
1375
+ "workspace.copyFailed": "复制失败",
1376
+ "workspace.saved": "已保存",
1377
+ "workspace.saveFailed": "保存失败",
1378
+ "workspace.dirtyCloseConfirm": "文件有未保存的修改,确定关闭?",
1316
1379
  "changes.tab": "Git 管理",
1317
1380
  "changes.loading": "正在读取改动…",
1318
1381
  "changes.error": "读取改动失败",
@@ -1329,8 +1392,23 @@ const I18n = (() => {
1329
1392
  "changes.save.prefix": "手动存档",
1330
1393
  "changes.save.btn": "存档当前版本",
1331
1394
  "changes.save.saving": "正在存档…",
1332
- "changes.save.done": "已存档,可在「时光机」里回到这个版本",
1395
+ "changes.save.done": "已存档",
1333
1396
  "changes.save.failed": "存档失败",
1397
+ "changes.select.all": "全选",
1398
+ "changes.select.count": "已选 {{n}}/{{total}}",
1399
+ "changes.save.btnN": "存档选中的 {{n}} 个文件",
1400
+ "changes.save.promptMsg": "给这个版本写句说明",
1401
+ "changes.diff.loading": "正在读取差异…",
1402
+ "changes.diff.fail": "读取差异失败",
1403
+ "changes.diff.same": "这个文件的内容没有变化。",
1404
+ "changes.diff.binary": "二进制文件,无法逐行对比。",
1405
+ "changes.diff.truncated": "差异过大,仅显示前 {{n}} 行",
1406
+ "changes.restore.title": "撤销此文件的改动",
1407
+ "changes.restore.confirm": "确认丢弃?",
1408
+ "changes.restore.diffBtn": "撤销改动",
1409
+ "changes.restore.doing": "正在撤销…",
1410
+ "changes.restore.done": "已撤销:{{path}}",
1411
+ "changes.restore.failed": "撤销失败",
1334
1412
  "tm.tab": "时光机",
1335
1413
  "sib.model.tooltip": "点击切换模型",
1336
1414
  "sib.model.tooltip.busy": "Agent 回复中,暂时无法切换模型",
@@ -1355,7 +1433,7 @@ const I18n = (() => {
1355
1433
  "sib.reasoning.medium": "中",
1356
1434
  "sib.reasoning.high": "高",
1357
1435
  "sib.reasoning.xhigh": "超高",
1358
- "sib.reasoning.max": "Max",
1436
+ "sib.reasoning.max": "极高",
1359
1437
  "sessions.thinking": "思考中…",
1360
1438
  "sessions.default_name": "会话 {{n}}",
1361
1439
  "sessions.group.cron": "定时任务",
@@ -1371,6 +1449,8 @@ const I18n = (() => {
1371
1449
  "sessions.badge.channel": "频道",
1372
1450
  "sessions.badge.coding": "Coding",
1373
1451
  "sessions.badge.setup": "配置",
1452
+ "sessions.setup.name": "个性化引导",
1453
+ "sessions.setup.browser.name": "🌐 浏览器配置",
1374
1454
  "sessions.newSession": "新会话",
1375
1455
  "sessions.advancedOptions": "高级选项",
1376
1456
  "sessions.loadMore": "加载更多会话",
@@ -1887,6 +1967,7 @@ const I18n = (() => {
1887
1967
  "settings.models.apiFormat.auto": "自动(根据服务商和模型检测)",
1888
1968
  "settings.models.apiFormat.anthropicMessages": "Anthropic Messages API",
1889
1969
  "settings.models.apiFormat.openaiCompletions": "OpenAI Chat Completions",
1970
+ "settings.models.apiFormat.openaiResponses": "OpenAI Responses API",
1890
1971
  "settings.models.field.getApiKey": "如何获取 →",
1891
1972
  "settings.models.field.docsGuide.question": "不会配置 AI 模型?",
1892
1973
  "settings.models.field.docsGuide.cta": "查看指南 →",
@@ -2093,7 +2174,7 @@ const I18n = (() => {
2093
2174
  "onboard.device.success.credit.label": "免费试用额度:",
2094
2175
  "onboard.device.success.credit.value": "$1.00(一次性赠送)",
2095
2176
  "onboard.device.success.model.label": "试用模型:",
2096
- "onboard.device.success.note": "试用期内仅可使用 Gemini 3.5 Flash —— 任意金额充值后,同一 Key 立即解锁全部前沿模型。",
2177
+ "onboard.device.success.note": "试用期内仅可使用 Gemini 3.8 Flash —— 任意金额充值后,同一 Key 立即解锁全部前沿模型。",
2097
2178
  "onboard.device.success.btn": "开始使用",
2098
2179
  "onboard.manual.toggle": "使用自己的 API Key 手动配置",
2099
2180
  "onboard.provider.custom": "自定义",
@@ -2285,7 +2366,14 @@ const I18n = (() => {
2285
2366
  }
2286
2367
  };
2287
2368
  // ── State ──────────────────────────────────────────────────────────────────
2288
- let _lang = localStorage.getItem(STORAGE_KEY) || DEFAULT_LANG;
2369
+ // On first visit (no saved preference), match the OS/browser language.
2370
+ function _detectLang() {
2371
+ const saved = localStorage.getItem(STORAGE_KEY);
2372
+ if (saved) return TRANSLATIONS[saved] ? saved : DEFAULT_LANG;
2373
+ const nav = (navigator.language || "").toLowerCase();
2374
+ return nav.startsWith("zh") ? "zh" : DEFAULT_LANG;
2375
+ }
2376
+ let _lang = _detectLang();
2289
2377
 
2290
2378
  // ── Core functions ─────────────────────────────────────────────────────────
2291
2379
 
@@ -2347,6 +2435,8 @@ const I18n = (() => {
2347
2435
  ph = ph.replace(/\s*[\((].*[\))]/, "").trim();
2348
2436
  }
2349
2437
  el.placeholder = ph;
2438
+ // contenteditable composers render their placeholder via CSS :empty::before.
2439
+ if (el.isContentEditable) el.setAttribute("data-placeholder", ph);
2350
2440
  });
2351
2441
  document.querySelectorAll("[data-i18n-title]").forEach(el => {
2352
2442
  const key = el.getAttribute("data-i18n-title");
@@ -19,6 +19,20 @@
19
19
  </script>
20
20
  </head>
21
21
  <body>
22
+ <script>
23
+ // Detect whether scrollbars float over content (macOS overlay) or occupy
24
+ // layout width (Windows/classic). The latter keeps the sidebar scrollbar
25
+ // always visible.
26
+ (function () {
27
+ var probe = document.createElement("div");
28
+ probe.style.cssText = "position:absolute;top:-9999px;left:0;width:100px;height:100px;overflow:scroll;";
29
+ document.body.appendChild(probe);
30
+ if (probe.offsetWidth !== probe.clientWidth) {
31
+ document.documentElement.classList.add("no-overlay-scrollbar");
32
+ }
33
+ document.body.removeChild(probe);
34
+ })();
35
+ </script>
22
36
  <div id="offline-banner" style="display:none" data-i18n="offline.banner"></div>
23
37
  <div id="app" style="visibility:hidden">
24
38
 
@@ -86,14 +100,22 @@
86
100
  </div>
87
101
  <!-- Filter row -->
88
102
  <div class="search-filter-row">
89
- <select id="session-search-type" class="search-select">
90
- <option value="" data-i18n="sessions.search.typeAll">All types</option>
91
- <option value="manual" data-i18n="sessions.search.typeManual">Default</option>
92
- <option value="cron" data-i18n="sessions.search.typeCron">Scheduled</option>
93
- <option value="channel" data-i18n="sessions.search.typeChannel">Channel</option>
94
- <option value="setup" data-i18n="sessions.search.typeSetup">Setup</option>
95
- <option value="coding" data-i18n="sessions.search.typeCoding">Coding</option>
96
- </select>
103
+ <div class="custom-select-wrapper search-select" id="session-search-type">
104
+ <div class="custom-select-trigger custom-select-trigger--bare" tabindex="0">
105
+ <span class="custom-select-value placeholder" data-i18n="sessions.search.typeAll">All types</span>
106
+ <svg class="custom-select-arrow" fill="none" stroke="currentColor" viewBox="0 0 24 24">
107
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
108
+ </svg>
109
+ </div>
110
+ <div class="custom-select-dropdown">
111
+ <div class="custom-select-option" data-value="" data-i18n="sessions.search.typeAll">All types</div>
112
+ <div class="custom-select-option" data-value="manual" data-i18n="sessions.search.typeManual">Default</div>
113
+ <div class="custom-select-option" data-value="cron" data-i18n="sessions.search.typeCron">Scheduled</div>
114
+ <div class="custom-select-option" data-value="channel" data-i18n="sessions.search.typeChannel">Channel</div>
115
+ <div class="custom-select-option" data-value="setup" data-i18n="sessions.search.typeSetup">Setup</div>
116
+ <div class="custom-select-option" data-value="coding" data-i18n="sessions.search.typeCoding">Coding</div>
117
+ </div>
118
+ </div>
97
119
  <div class="search-date-wrap">
98
120
  <button id="session-search-date" class="search-date datepicker-trigger" type="button" data-value="" data-i18n="sessions.search.datePlaceholder"></button>
99
121
  </div>
@@ -126,7 +148,7 @@
126
148
  <span class="chat-section-title">
127
149
  <span data-i18n="sidebar.chat">Sessions</span>
128
150
  <!-- Chevron: hidden by default, shown on hover or when collapsed -->
129
- <svg class="chat-section-chevron" width="13" height="13" 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">
151
+ <svg class="chat-section-chevron" width="14" height="14" 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
152
  <path d="M6 9l6 6 6-6"/>
131
153
  </svg>
132
154
  </span>
@@ -177,7 +199,7 @@
177
199
  <div class="sidebar-divider" id="projects-section-header">
178
200
  <span class="projects-section-title">
179
201
  <span data-i18n="sidebar.projects">Projects</span>
180
- <svg class="projects-section-chevron" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
202
+ <svg class="projects-section-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
181
203
  <path d="M6 9l6 6 6-6"/>
182
204
  </svg>
183
205
  </span>
@@ -398,17 +420,21 @@
398
420
  <button id="ns-btn-slash" type="button" class="new-session-composer-btn" title="Insert skill command (/)">
399
421
  <span>/</span>
400
422
  </button>
401
- <textarea id="new-session-input"
402
- class="new-session-textarea"
403
- rows="1"
404
- data-i18n-placeholder="chat.input.placeholder"
405
- placeholder="Message… (Enter to send, Shift+Enter for newline)"></textarea>
423
+ <div id="new-session-input"
424
+ class="new-session-textarea"
425
+ contenteditable="true"
426
+ role="textbox"
427
+ aria-multiline="true"
428
+ data-i18n-placeholder="chat.input.placeholder"
429
+ data-placeholder="Message… (Enter to send, Shift+Enter for newline)"></div>
406
430
  <button id="new-session-send" class="new-session-send" type="button" title="Send message" disabled>
407
431
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
408
432
  </button>
409
433
  </div><!-- /.new-session-input-bar -->
410
434
  </div><!-- /.ns-input-bar-card -->
411
435
  </div><!-- /.ns-composer-frame -->
436
+ <!-- @ mention menu (Files & Folders / Past Chats) -->
437
+ <div id="ns-mention-autocomplete" class="mention-autocomplete" style="display:none" role="listbox" aria-label="Mention"></div>
412
438
  </div><!-- /.new-session-composer -->
413
439
  </div>
414
440
  </div>
@@ -512,6 +538,8 @@
512
538
  </div>
513
539
  <!-- Image preview strip (hidden when empty) -->
514
540
  <div id="image-preview-strip" style="display:none"></div>
541
+ <!-- @ mention menu (Files & Folders / Past Chats) -->
542
+ <div id="mention-autocomplete" class="mention-autocomplete" style="display:none" role="listbox" aria-label="Mention"></div>
515
543
  <div id="input-bar">
516
544
  <!-- Hidden file picker -->
517
545
  <input type="file" id="image-file-input" accept="*/*" multiple style="display:none">
@@ -523,9 +551,12 @@
523
551
  <button id="btn-slash" title="Insert skill command (/)">
524
552
  <span>/</span>
525
553
  </button>
526
- <textarea id="user-input" rows="1"
527
- data-i18n-placeholder="chat.input.placeholder"
528
- placeholder="Message… (Enter to send, Shift-Enter for newline)"></textarea>
554
+ <div id="user-input"
555
+ contenteditable="true"
556
+ role="textbox"
557
+ aria-multiline="true"
558
+ data-i18n-placeholder="chat.input.placeholder"
559
+ data-placeholder="Message… (Enter to send, Shift-Enter for newline)"></div>
529
560
  <button id="btn-send" title="Send message"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg></button>
530
561
  <button id="btn-interrupt" style="display:none" title="Stop"><svg viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="1.5"/></svg></button>
531
562
  </div>
@@ -697,15 +728,31 @@
697
728
  </div>
698
729
  <div class="extensions-toolbar-right">
699
730
  <input type="text" id="extensions-search-input" class="extensions-search" placeholder="Search extensions…" />
700
- <select id="extensions-sort" class="extensions-sort">
701
- <option value="downloads" data-i18n="extensions.sort.downloads">Most downloaded</option>
702
- <option value="newest" data-i18n="extensions.sort.newest">Newest</option>
703
- <option value="updated" data-i18n="extensions.sort.updated">Recently updated</option>
704
- </select>
705
- <select id="extensions-installed-filter" class="extensions-sort">
706
- <option value="all" data-i18n="extensions.filter.all">All</option>
707
- <option value="installed" data-i18n="extensions.filter.installed">Installed</option>
708
- </select>
731
+ <div class="custom-select-wrapper extensions-select" id="extensions-sort">
732
+ <div class="custom-select-trigger" tabindex="0">
733
+ <span class="custom-select-value" data-i18n="extensions.sort.downloads">Most downloaded</span>
734
+ <svg class="custom-select-arrow" fill="none" stroke="currentColor" viewBox="0 0 24 24">
735
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
736
+ </svg>
737
+ </div>
738
+ <div class="custom-select-dropdown">
739
+ <div class="custom-select-option" data-value="downloads" data-i18n="extensions.sort.downloads">Most downloaded</div>
740
+ <div class="custom-select-option" data-value="newest" data-i18n="extensions.sort.newest">Newest</div>
741
+ <div class="custom-select-option" data-value="updated" data-i18n="extensions.sort.updated">Recently updated</div>
742
+ </div>
743
+ </div>
744
+ <div class="custom-select-wrapper extensions-select" id="extensions-installed-filter">
745
+ <div class="custom-select-trigger" tabindex="0">
746
+ <span class="custom-select-value" data-i18n="extensions.filter.all">All</span>
747
+ <svg class="custom-select-arrow" fill="none" stroke="currentColor" viewBox="0 0 24 24">
748
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
749
+ </svg>
750
+ </div>
751
+ <div class="custom-select-dropdown">
752
+ <div class="custom-select-option" data-value="all" data-i18n="extensions.filter.all">All</div>
753
+ <div class="custom-select-option" data-value="installed" data-i18n="extensions.filter.installed">Installed</div>
754
+ </div>
755
+ </div>
709
756
  </div>
710
757
  </div>
711
758
 
@@ -741,9 +788,15 @@
741
788
  <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">
742
789
  <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"/>
743
790
  </svg>
744
- <select id="trash-filter-project" class="trash-filter-project-select">
745
- <option value="" data-i18n="trash.filterAllProjects">所有项目</option>
746
- </select>
791
+ <div class="custom-select-wrapper" id="trash-filter-project">
792
+ <div class="custom-select-trigger custom-select-trigger--bare" tabindex="0">
793
+ <span class="custom-select-value placeholder" data-i18n="trash.filterAllProjects">所有项目</span>
794
+ <svg class="custom-select-arrow" fill="none" stroke="currentColor" viewBox="0 0 24 24">
795
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
796
+ </svg>
797
+ </div>
798
+ <div class="custom-select-dropdown"></div>
799
+ </div>
747
800
  </div>
748
801
  </div>
749
802
  <span class="trash-summary" id="trash-summary"></span>
@@ -911,9 +964,15 @@
911
964
  data-i18n-placeholder="settings.models.search.placeholder"
912
965
  placeholder="Search model / provider / remark / base URL" autocomplete="off">
913
966
  </div>
914
- <select id="model-provider-filter" class="model-provider-filter" aria-label="Filter by provider">
915
- <option value="">All providers</option>
916
- </select>
967
+ <div class="custom-select-wrapper model-provider-filter" id="model-provider-filter">
968
+ <div class="custom-select-trigger" tabindex="0">
969
+ <span class="custom-select-value placeholder" data-i18n="settings.models.filter.all">All providers</span>
970
+ <svg class="custom-select-arrow" fill="none" stroke="currentColor" viewBox="0 0 24 24">
971
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
972
+ </svg>
973
+ </div>
974
+ <div class="custom-select-dropdown"></div>
975
+ </div>
917
976
  <button id="btn-add-model" class="btn-settings-add" data-i18n="settings.models.add">+ Add Model</button>
918
977
  </div>
919
978
  </div>
@@ -1099,11 +1158,19 @@
1099
1158
  <div class="settings-network">
1100
1159
  <p class="settings-network-desc" data-i18n="settings.search.desc">By default web_search scrapes DuckDuckGo and Bing, which needs no key but returns lower-quality results. Select a search provider to use an API instead — it falls back to the built-in engines if the provider fails.</p>
1101
1160
  <div class="settings-network-url">
1102
- <label class="settings-network-url-label" for="settings-search-provider" data-i18n="settings.search.provider">Provider</label>
1161
+ <label class="settings-network-url-label" for="settings-search-provider-trigger" data-i18n="settings.search.provider">Provider</label>
1103
1162
  <div class="settings-network-url-row">
1104
- <select id="settings-search-provider" class="field-select">
1105
- <option value="" data-i18n="settings.search.builtin">Built-in (DuckDuckGo / Bing)</option>
1106
- </select>
1163
+ <div class="custom-select-wrapper" id="settings-search-provider-wrapper">
1164
+ <div class="custom-select-trigger" tabindex="0" id="settings-search-provider-trigger">
1165
+ <span class="custom-select-value placeholder" id="settings-search-provider-value" data-i18n="settings.search.builtin">Built-in (DuckDuckGo / Bing)</span>
1166
+ <svg class="custom-select-arrow" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1167
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
1168
+ </svg>
1169
+ </div>
1170
+ <div class="custom-select-dropdown" id="settings-search-provider-dropdown">
1171
+ <div class="custom-select-option" data-value="" data-i18n="settings.search.builtin">Built-in (DuckDuckGo / Bing)</div>
1172
+ </div>
1173
+ </div>
1107
1174
  <button type="button" id="btn-save-search" class="btn-settings-action" data-i18n="settings.search.save">Save</button>
1108
1175
  </div>
1109
1176
  <div id="settings-search-status" class="model-test-result"></div>
@@ -1345,7 +1412,7 @@
1345
1412
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
1346
1413
  </svg>
1347
1414
  </div>
1348
- <div class="custom-select-dropdown" id="model-modal-provider-dropdown"></div>
1415
+ <div class="custom-select-dropdown custom-select-dropdown--footer" id="model-modal-provider-dropdown"></div>
1349
1416
  </div>
1350
1417
  <div id="model-modal-promo-hint" class="provider-promo-hint" data-index="modal"></div>
1351
1418
  </label>
@@ -1393,11 +1460,20 @@
1393
1460
 
1394
1461
  <label class="model-field" id="model-modal-apiformat-field">
1395
1462
  <span class="field-label" data-i18n="settings.models.field.apiFormat">API Format</span>
1396
- <select id="model-modal-api-format" class="field-input field-select">
1397
- <option value="" data-i18n="settings.models.apiFormat.auto">Auto (detect from provider &amp; model)</option>
1398
- <option value="anthropic-messages" data-i18n="settings.models.apiFormat.anthropicMessages">Anthropic Messages API</option>
1399
- <option value="openai-completions" data-i18n="settings.models.apiFormat.openaiCompletions">OpenAI Chat Completions</option>
1400
- </select>
1463
+ <div class="custom-select-wrapper" id="model-modal-api-format-wrapper">
1464
+ <div class="custom-select-trigger" tabindex="0" id="model-modal-api-format-trigger">
1465
+ <span class="custom-select-value placeholder" id="model-modal-api-format-value" data-i18n="settings.models.apiFormat.auto">Auto (detect from provider &amp; model)</span>
1466
+ <svg class="custom-select-arrow" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1467
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
1468
+ </svg>
1469
+ </div>
1470
+ <div class="custom-select-dropdown" id="model-modal-api-format-dropdown">
1471
+ <div class="custom-select-option" data-value="" data-i18n="settings.models.apiFormat.auto">Auto (detect from provider &amp; model)</div>
1472
+ <div class="custom-select-option" data-value="anthropic-messages" data-i18n="settings.models.apiFormat.anthropicMessages">Anthropic Messages API</div>
1473
+ <div class="custom-select-option" data-value="openai-completions" data-i18n="settings.models.apiFormat.openaiCompletions">OpenAI Chat Completions</div>
1474
+ <div class="custom-select-option" data-value="openai-responses" data-i18n="settings.models.apiFormat.openaiResponses">OpenAI Responses API</div>
1475
+ </div>
1476
+ </div>
1401
1477
  </label>
1402
1478
 
1403
1479
  <label class="model-field">
@@ -1508,9 +1584,9 @@
1508
1584
  </li>
1509
1585
  <li>
1510
1586
  <strong data-i18n="onboard.device.success.model.label">Trial model:</strong>
1511
- <span id="setup-device-success-model">or-gemini-3-5-flash</span>
1587
+ <span id="setup-device-success-model">or-gemini-3-8-flash</span>
1512
1588
  </li>
1513
- <li class="setup-device-success-note" data-i18n="onboard.device.success.note">During the trial only Gemini 3.5 Flash is available — top up any amount to unlock all frontier models on this same key.</li>
1589
+ <li class="setup-device-success-note" data-i18n="onboard.device.success.note">During the trial only Gemini 3.8 Flash is available — top up any amount to unlock all frontier models on this same key.</li>
1514
1590
  </ul>
1515
1591
  <button id="setup-btn-device-continue" class="setup-submit-btn" data-i18n="onboard.device.success.btn">Start using Clacky →</button>
1516
1592
  </div>
@@ -1656,12 +1732,16 @@
1656
1732
  <script src="/utils.js"></script>
1657
1733
  <script src="/core/ext.js"></script>
1658
1734
  <script src="/core/aside.js"></script>
1735
+ <script src="/components/composer.js"></script>
1736
+ <script src="/components/mentions.js"></script>
1659
1737
  <script src="/i18n.js"></script>
1738
+ <script src="/components/model-picker.js"></script>
1660
1739
  <script src="/auth.js"></script>
1661
1740
  <script src="/theme.js"></script>
1662
1741
  <script src="/components/notify.js"></script>
1663
1742
  <script src="/ws.js"></script>
1664
1743
  <script src="/ws-dispatcher.js"></script>
1744
+ <script src="/components/custom-select.js"></script>
1665
1745
  <script src="/sessions.js"></script>
1666
1746
  <script src="/projects.js"></script>
1667
1747
  <script src="/features/workspace/store.js"></script>