openclacky 1.2.17 → 1.3.0

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -0
  3. data/lib/clacky/agent/skill_manager.rb +1 -1
  4. data/lib/clacky/agent/time_machine.rb +256 -74
  5. data/lib/clacky/agent/tool_executor.rb +12 -0
  6. data/lib/clacky/agent.rb +21 -31
  7. data/lib/clacky/agent_config.rb +18 -0
  8. data/lib/clacky/cli.rb +55 -3
  9. data/lib/clacky/default_skills/media-gen/SKILL.md +173 -5
  10. data/lib/clacky/default_skills/skill-creator/SKILL.md +1 -0
  11. data/lib/clacky/media/base.rb +125 -0
  12. data/lib/clacky/media/dashscope.rb +243 -0
  13. data/lib/clacky/media/gemini.rb +10 -0
  14. data/lib/clacky/media/generator.rb +75 -0
  15. data/lib/clacky/media/openai_compat.rb +160 -0
  16. data/lib/clacky/message_history.rb +12 -7
  17. data/lib/clacky/providers.rb +28 -0
  18. data/lib/clacky/rich_ui_controller.rb +3 -1
  19. data/lib/clacky/server/backup_manager.rb +200 -0
  20. data/lib/clacky/server/channel/adapters/feishu/adapter.rb +10 -2
  21. data/lib/clacky/server/channel/adapters/feishu/bot.rb +68 -15
  22. data/lib/clacky/server/channel/channel_manager.rb +180 -81
  23. data/lib/clacky/server/http_server.rb +348 -15
  24. data/lib/clacky/server/scheduler.rb +19 -0
  25. data/lib/clacky/server/session_registry.rb +8 -4
  26. data/lib/clacky/session_manager.rb +40 -2
  27. data/lib/clacky/skill.rb +3 -1
  28. data/lib/clacky/tools/trash_manager.rb +14 -0
  29. data/lib/clacky/ui2/components/command_suggestions.rb +1 -0
  30. data/lib/clacky/ui2/components/modal_component.rb +34 -7
  31. data/lib/clacky/ui2/ui_controller.rb +150 -19
  32. data/lib/clacky/utils/file_processor.rb +75 -4
  33. data/lib/clacky/version.rb +1 -1
  34. data/lib/clacky/web/app.css +2038 -1147
  35. data/lib/clacky/web/app.js +22 -1
  36. data/lib/clacky/web/backup.js +119 -0
  37. data/lib/clacky/web/billing.js +94 -7
  38. data/lib/clacky/web/channels.js +81 -11
  39. data/lib/clacky/web/design-sample.css +247 -0
  40. data/lib/clacky/web/design-sample.html +127 -0
  41. data/lib/clacky/web/favicon.svg +16 -0
  42. data/lib/clacky/web/i18n.js +159 -31
  43. data/lib/clacky/web/index.html +175 -55
  44. data/lib/clacky/web/logo_nav_dark.png +0 -0
  45. data/lib/clacky/web/onboard.js +114 -28
  46. data/lib/clacky/web/sessions.js +436 -192
  47. data/lib/clacky/web/settings.js +21 -1
  48. data/lib/clacky/web/skills.js +6 -6
  49. data/lib/clacky/web/tasks.js +129 -61
  50. data/lib/clacky/web/utils.js +72 -0
  51. data/lib/clacky/web/ws-dispatcher.js +6 -0
  52. data/lib/clacky.rb +1 -0
  53. metadata +8 -3
  54. data/lib/clacky/server/channel/group_message_buffer.rb +0 -53
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title id="page-title">{{BRAND_NAME}}</title>
7
- <link rel="icon" href="/favicon.ico">
7
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg">
8
8
  <link rel="apple-touch-icon" href="/apple-touch-icon-180.png">
9
9
  <link rel="stylesheet" href="/vendor/katex/katex.min.css">
10
10
  <link rel="stylesheet" href="/vendor/hljs/hljs-theme.css">
@@ -43,6 +43,13 @@
43
43
  title="Creator / Owner" data-i18n-title="header.owner.tooltip">OWNER</button>
44
44
  </div>
45
45
  </div>
46
+ <div id="header-center">
47
+ <button id="header-cmdbar" type="button" title="Search sessions">
48
+ <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
49
+ <span class="cmdbar-ph" data-i18n="header.cmdbar.placeholder">Search sessions…</span>
50
+ <span class="cmdbar-kbd">⌘K</span>
51
+ </button>
52
+ </div>
46
53
  <div id="header-right">
47
54
  <button id="share-toggle-header" class="theme-toggle-btn" data-i18n-title="share.tooltip" title="Share">
48
55
  <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-sm">
@@ -62,6 +69,42 @@
62
69
  </div>
63
70
  </header>
64
71
 
72
+ <!-- ── Command-palette search overlay (⌘K / top cmdbar) ─────────────────── -->
73
+ <!-- Search lives here, decoupled from the sidebar list: results render in
74
+ #session-search-results and the left session list is never replaced. -->
75
+ <div id="session-search-overlay" class="cmd-palette-overlay" hidden>
76
+ <div class="cmd-palette" role="dialog" aria-modal="true" aria-label="Search sessions">
77
+ <div class="search-panel-card">
78
+ <!-- Input row -->
79
+ <div class="search-input-row">
80
+ <span class="search-icon">🔍</span>
81
+ <input id="session-search-q" type="text" class="search-input"
82
+ data-i18n-placeholder="sessions.search.placeholder" placeholder="Search sessions…"
83
+ autocomplete="off" />
84
+ <button id="btn-search-q-clear" class="btn-search-q-clear" aria-label="Clear text" hidden>✕</button>
85
+ <button id="btn-session-search-close" class="cmd-palette-esc" type="button" aria-label="Close">ESC</button>
86
+ </div>
87
+ <!-- Filter row -->
88
+ <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>
97
+ <div class="search-date-wrap">
98
+ <button id="session-search-date" class="search-date datepicker-trigger" type="button" data-value="" data-i18n="sessions.search.datePlaceholder"></button>
99
+ </div>
100
+ <button id="btn-search-clear-all" class="btn-search-clear-all" aria-label="Clear filters" hidden>✕</button>
101
+ </div>
102
+ </div>
103
+ <!-- Results render here (independent from the sidebar session list) -->
104
+ <div id="session-search-results" class="cmd-palette-results"></div>
105
+ </div>
106
+ </div>
107
+
65
108
  <!-- ── Sidebar overlay (mobile only) ───────────────────────────────────── -->
66
109
  <div id="sidebar-overlay"></div>
67
110
 
@@ -74,17 +117,10 @@
74
117
  <div id="sidebar-list">
75
118
  <!-- Chat Group -->
76
119
  <div id="chat-section">
77
- <!-- Header: "Sessions" label + 🔍 + [+ ▾] split button -->
120
+ <!-- Header: "Sessions" label + [+ ▾] split button -->
78
121
  <div class="sidebar-divider">
79
122
  <span data-i18n="sidebar.chat">Sessions</span>
80
123
  <div class="sidebar-divider-actions">
81
- <!-- Magnifier toggle (shown when ≥10 sessions, managed by JS) -->
82
- <button id="btn-session-search-toggle" class="btn-icon-sm" title="Search sessions" style="display:none" aria-label="Search sessions">
83
- <svg width="13" height="13" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
84
- <circle cx="6.5" cy="6.5" r="4.5" stroke="currentColor" stroke-width="1.6"/>
85
- <line x1="10.3" y1="10.3" x2="14" y2="14" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
86
- </svg>
87
- </button>
88
124
  <div class="btn-split-wrap">
89
125
  <button id="btn-new-session-inline" class="btn-split-main" title="New Session" data-i18n="sessions.newSession">+ New Session</button>
90
126
  <button id="btn-new-session-arrow" class="btn-split-arrow" title="Options">▾</button>
@@ -101,35 +137,6 @@
101
137
  </div>
102
138
  </div>
103
139
 
104
- <!-- Search panel (hidden by default, toggled by magnifier button) -->
105
- <div id="session-search-bar" class="session-search-panel" hidden>
106
- <div class="search-panel-card">
107
- <!-- Input row -->
108
- <div class="search-input-row">
109
- <span class="search-icon">🔍</span>
110
- <input id="session-search-q" type="text" class="search-input"
111
- data-i18n-placeholder="sessions.search.placeholder" placeholder="Search sessions…"
112
- autocomplete="off" />
113
- <button id="btn-search-q-clear" class="btn-search-q-clear" aria-label="Clear text" hidden>✕</button>
114
- </div>
115
- <!-- Filter row -->
116
- <div class="search-filter-row">
117
- <select id="session-search-type" class="search-select">
118
- <option value="" data-i18n="sessions.search.typeAll">All types</option>
119
- <option value="manual" data-i18n="sessions.search.typeManual">Default</option>
120
- <option value="cron" data-i18n="sessions.search.typeCron">Scheduled</option>
121
- <option value="channel" data-i18n="sessions.search.typeChannel">Channel</option>
122
- <option value="setup" data-i18n="sessions.search.typeSetup">Setup</option>
123
- <option value="coding" data-i18n="sessions.search.typeCoding">Coding</option>
124
- </select>
125
- <div class="search-date-wrap">
126
- <button id="session-search-date" class="search-date datepicker-trigger" type="button" data-value="" data-i18n="sessions.search.datePlaceholder"></button>
127
- </div>
128
- <button id="btn-search-clear-all" class="btn-search-clear-all" aria-label="Clear filters" hidden>✕</button>
129
- </div>
130
- </div>
131
- </div>
132
-
133
140
  <!-- Cron view header (hidden by default, shown when viewing cron sessions) -->
134
141
  <div id="cron-view-header" class="sidebar-divider" style="display:none">
135
142
  <button id="btn-cron-back" class="btn-icon-sm" title="Back" aria-label="Back to session list">
@@ -295,10 +302,30 @@
295
302
 
296
303
  <!-- Welcome screen -->
297
304
  <div id="welcome" class="centered">
298
- <div class="welcome-icon" style="display:none"></div>
299
- <h2 id="welcome-title" data-i18n="welcome.title" data-i18n-vars="brand={{BRAND_NAME}}">Welcome to {{BRAND_NAME}}</h2>
300
- <p data-i18n="welcome.body">Create a new session or select one from the sidebar.</p>
301
- <button id="btn-welcome-new" data-i18n="welcome.btn">New Session</button>
305
+ <div class="ce-mark">
306
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">
307
+ <path d="M12 2l2.4 6.6L21 11l-6.6 2.4L12 20l-2.4-6.6L3 11l6.6-2.4z"/>
308
+ </svg>
309
+ </div>
310
+ <div class="ce-head">
311
+ <h2 id="welcome-title" class="ce-title" data-i18n="welcome.title" data-i18n-vars="brand={{BRAND_NAME}}">What should we build?</h2>
312
+ <p class="ce-sub" data-i18n="welcome.body">Your agent is standing by. Pick a starting point or just type.</p>
313
+ </div>
314
+ <div class="chips">
315
+ <button class="chip" data-welcome-prompt="welcome.chip.code.prompt">
316
+ <span class="ci"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M16 18l6-6-6-6M8 6l-6 6 6 6"/></svg></span>
317
+ <span class="ct-wrap"><span class="ct" data-i18n="welcome.chip.code.title">Write code</span><span class="cd" data-i18n="welcome.chip.code.desc">build, refactor, debug</span></span>
318
+ </button>
319
+ <button class="chip" data-welcome-prompt="welcome.chip.task.prompt">
320
+ <span class="ci"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg></span>
321
+ <span class="ct-wrap"><span class="ct" data-i18n="welcome.chip.task.title">Automate a task</span><span class="cd" data-i18n="welcome.chip.task.desc">schedule, run, repeat</span></span>
322
+ </button>
323
+ <button class="chip" data-welcome-prompt="welcome.chip.research.prompt">
324
+ <span class="ci"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5V6a2 2 0 0 1 2-2h12v16H6a2 2 0 0 1-2-2z"/><path d="M8 7h7"/></svg></span>
325
+ <span class="ct-wrap"><span class="ct" data-i18n="welcome.chip.research.title">Research</span><span class="cd" data-i18n="welcome.chip.research.desc">search, read, summarize</span></span>
326
+ </button>
327
+ </div>
328
+ <button id="btn-welcome-new" class="ce-blank" data-i18n="welcome.btn">Start a blank session</button>
302
329
  </div>
303
330
 
304
331
  <!-- Scheduled Tasks list panel (shown when user clicks "Scheduled Tasks") -->
@@ -406,9 +433,9 @@
406
433
  </button>
407
434
  <textarea id="user-input" rows="1"
408
435
  data-i18n-placeholder="chat.input.placeholder"
409
- placeholder="Message… (Enter to send, Shift+Enter for newline)"></textarea>
410
- <button id="btn-send" data-i18n="chat.btn.send">Send</button>
411
- <button id="btn-interrupt" style="display:none" title="Stop"></button>
436
+ placeholder="Message… (Enter to send, Shift-Enter for newline)"></textarea>
437
+ <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>
438
+ <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>
412
439
  </div>
413
440
  </div>
414
441
  </div><!-- /#chat-main -->
@@ -777,7 +804,7 @@
777
804
  <!-- Models section -->
778
805
  <section class="settings-section">
779
806
  <div class="settings-section-title">
780
- <span data-i18n="settings.models.title">AI Models</span>
807
+ <span data-i18n="settings.models.title">Primary Model</span>
781
808
  <button id="btn-add-model" class="btn-settings-add" data-i18n="settings.models.add">+ Add Model</button>
782
809
  </div>
783
810
  <div id="model-cards"></div>
@@ -786,10 +813,10 @@
786
813
  <!-- Media generation section -->
787
814
  <section class="settings-section" id="media-section">
788
815
  <div class="settings-section-title">
789
- <span data-i18n="settings.media.title">Media Generation</span>
816
+ <span data-i18n="settings.media.title">Secondary Models</span>
790
817
  </div>
791
818
  <div class="settings-section-desc" data-i18n="settings.media.desc">
792
- Generate images, video, and audio using your configured providers. "Auto" follows your default chat model.
819
+ Optional. Image / video / audio / vision models.
793
820
  </div>
794
821
  <div id="media-rows"></div>
795
822
  </section>
@@ -898,6 +925,33 @@
898
925
  </div>
899
926
  </section>
900
927
 
928
+ <!-- Backup section -->
929
+ <section class="settings-section" id="backup-section">
930
+ <div class="settings-section-title">
931
+ <span data-i18n="settings.backup.title">Backup</span>
932
+ </div>
933
+ <p class="settings-section-desc" data-i18n="settings.backup.desc">Back up your ~/.clacky directory (config, skills, memories, tasks, sessions). Regenerable caches and logs are excluded.</p>
934
+
935
+ <div class="backup-auto-row">
936
+ <label class="toggle-switch">
937
+ <input type="checkbox" id="backup-auto-toggle">
938
+ <span class="toggle-slider"></span>
939
+ </label>
940
+ <span class="backup-auto-label" data-i18n="settings.backup.autoLabel">Automatic backup</span>
941
+ <span class="backup-auto-hint" data-i18n="settings.backup.autoHint">Daily at 03:00, keeps the latest 7</span>
942
+ </div>
943
+
944
+ <label class="backup-option">
945
+ <input type="checkbox" id="backup-include-sessions">
946
+ <span data-i18n="settings.backup.includeSessions">Include session history (larger archive)</span>
947
+ </label>
948
+
949
+ <div class="backup-actions">
950
+ <button id="btn-backup-now" class="btn-settings-action" data-i18n="settings.backup.runNow">💾 Download backup</button>
951
+ <span id="backup-status" class="model-test-result"></span>
952
+ </div>
953
+ </section>
954
+
901
955
  <!-- Brand & License section -->
902
956
  <section class="settings-section" id="brand-license-section">
903
957
  <div class="settings-section-title">
@@ -1153,6 +1207,63 @@
1153
1207
  <div id="setup-phase-key" style="display:none">
1154
1208
  <p class="setup-phase-label" data-i18n="onboard.key.title">Connect your AI model</p>
1155
1209
 
1210
+ <!-- Primary path: one-click device login -->
1211
+ <div id="setup-device-block">
1212
+ <div id="setup-device-card" class="setup-device-card">
1213
+ <div class="setup-device-card-head">
1214
+ <span class="setup-device-card-title" data-i18n="onboard.device.card.title">OpenClacky AI Keys</span>
1215
+ <span class="setup-device-card-badge" data-i18n="provider.recommended">Recommended</span>
1216
+ </div>
1217
+ <p class="setup-device-card-lead" data-i18n="onboard.device.card.lead">Sign up and get $1 free credit · no subscription · pay as you go</p>
1218
+ <ul class="setup-device-card-points">
1219
+ <li data-i18n="onboard.device.card.point1">One key for all frontier models — Claude, Gemini, DeepSeek &amp; more</li>
1220
+ <li data-i18n="onboard.device.card.point2">Same pricing as official APIs, switch models instantly</li>
1221
+ </ul>
1222
+ <button id="setup-btn-device-login" class="setup-submit-btn setup-device-btn">
1223
+ <span data-i18n="onboard.device.btn">Get started free →</span>
1224
+ </button>
1225
+ </div>
1226
+
1227
+ <!-- Pending state: shown while waiting for browser approval -->
1228
+ <div id="setup-device-pending" style="display:none">
1229
+ <div class="setup-device-spinner"></div>
1230
+ <p class="setup-device-pending-text" data-i18n="onboard.device.pending">Waiting for confirmation in your browser…</p>
1231
+ <p class="setup-device-code">
1232
+ <span data-i18n="onboard.device.code">Verification code:</span>
1233
+ <strong id="setup-device-usercode">—</strong>
1234
+ </p>
1235
+ <a id="setup-device-link" href="#" target="_blank" rel="noopener" class="setup-device-reopen" data-i18n="onboard.device.reopen">Open browser again →</a>
1236
+ <button id="setup-device-cancel" class="setup-back-btn" data-i18n="onboard.device.cancel">Cancel</button>
1237
+ </div>
1238
+
1239
+ <!-- Success state: shown after approval, before launching onboard session -->
1240
+ <div id="setup-device-success" style="display:none" class="setup-device-success">
1241
+ <div class="setup-device-success-head">
1242
+ <span class="setup-device-success-icon">✓</span>
1243
+ <span class="setup-device-success-title" data-i18n="onboard.device.success.title">You're in!</span>
1244
+ </div>
1245
+ <p class="setup-device-success-lead" data-i18n="onboard.device.success.lead">Your account is connected. Here's what you got:</p>
1246
+ <ul class="setup-device-success-points">
1247
+ <li>
1248
+ <strong data-i18n="onboard.device.success.credit.label">Free trial credit:</strong>
1249
+ <span data-i18n="onboard.device.success.credit.value">$1.00 (one-time, on us)</span>
1250
+ </li>
1251
+ <li>
1252
+ <strong data-i18n="onboard.device.success.model.label">Trial model:</strong>
1253
+ <span id="setup-device-success-model">or-gemini-3-5-flash</span>
1254
+ </li>
1255
+ <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>
1256
+ </ul>
1257
+ <button id="setup-btn-device-continue" class="setup-submit-btn" data-i18n="onboard.device.success.btn">Start using Clacky →</button>
1258
+ </div>
1259
+
1260
+ <div id="setup-device-error" class="setup-test-result" style="min-height:0;"></div>
1261
+ </div>
1262
+
1263
+ <!-- Secondary path: manual key entry, collapsed by default -->
1264
+ <details id="setup-manual-details">
1265
+ <summary id="setup-manual-summary" data-i18n="onboard.manual.toggle">Configure manually with your own API key</summary>
1266
+
1156
1267
  <div class="setup-field">
1157
1268
  <label class="setup-label" data-i18n="onboard.key.provider">Provider</label>
1158
1269
  <div class="custom-select-wrapper" id="setup-provider-wrapper">
@@ -1166,7 +1277,6 @@
1166
1277
  <div class="custom-select-option" data-value="" data-i18n="onboard.key.provider.placeholder">— Choose provider —</div>
1167
1278
  </div>
1168
1279
  </div>
1169
- <div id="setup-provider-promo" class="provider-promo-hint"></div>
1170
1280
  </div>
1171
1281
 
1172
1282
  <div class="setup-field">
@@ -1218,6 +1328,7 @@
1218
1328
  <button id="setup-btn-back" class="setup-back-btn" data-i18n="onboard.key.btn.back">← Back</button>
1219
1329
  <button id="setup-btn-test" class="setup-submit-btn" data-i18n="onboard.key.btn.test">Test & Continue →</button>
1220
1330
  </div>
1331
+ </details>
1221
1332
  </div>
1222
1333
 
1223
1334
  </div>
@@ -1254,9 +1365,14 @@
1254
1365
 
1255
1366
  <div class="modal-field">
1256
1367
  <label class="modal-label" data-i18n="sessions.modal.directory">Working Directory</label>
1257
- <input id="new-session-directory" type="text" class="modal-input"
1258
- data-i18n-placeholder="sessions.modal.directory.placeholder"
1259
- placeholder="~/workspace/my-project">
1368
+ <div class="modal-input-row">
1369
+ <input id="new-session-directory" type="text" class="modal-input"
1370
+ data-i18n-placeholder="sessions.modal.directory.placeholder"
1371
+ placeholder="~/workspace/my-project">
1372
+ <button id="new-session-browse-btn" type="button" class="modal-browse-btn" data-i18n-title="sessions.modal.directory.browse" title="Browse…" aria-label="Browse">
1373
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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"/></svg>
1374
+ </button>
1375
+ </div>
1260
1376
  </div>
1261
1377
 
1262
1378
  <div id="new-session-init-project-field" class="modal-field-checkbox" style="display:none">
@@ -1302,7 +1418,9 @@
1302
1418
  <!-- Rename Session Modal -->
1303
1419
  <div id="rename-modal-overlay" class="modal-overlay" style="display:none">
1304
1420
  <div class="modal-box sm">
1305
- <div class="modal-title" data-i18n="sessions.actions.rename">Rename</div>
1421
+ <div class="modal-header">
1422
+ <h3 class="modal-title" data-i18n="sessions.actions.rename">Rename</h3>
1423
+ </div>
1306
1424
  <div class="modal-body">
1307
1425
  <div class="modal-field">
1308
1426
  <label class="modal-label" for="rename-modal-input">
@@ -1312,14 +1430,14 @@
1312
1430
  <input type="text" id="rename-modal-input" class="modal-input" autocomplete="off" spellcheck="false">
1313
1431
  </div>
1314
1432
  </div>
1315
- <div class="modal-actions">
1433
+ <div class="modal-footer">
1316
1434
  <button id="rename-modal-cancel" class="btn-secondary" data-i18n="modal.cancel">Cancel</button>
1317
1435
  <button id="rename-modal-save" class="btn-primary" data-i18n="modal.ok">OK</button>
1318
1436
  </div>
1319
1437
  </div>
1320
1438
  </div>
1321
1439
 
1322
-
1440
+ <div id="tooltip" style="display:none"></div>
1323
1441
 
1324
1442
  <script src="/marked.min.js"></script>
1325
1443
  <script src="/vendor/hljs/highlight.min.js"></script>
@@ -1339,6 +1457,7 @@
1339
1457
  <script src="/skills.js"></script>
1340
1458
  <script src="/channels.js"></script>
1341
1459
  <script src="/mcp.js"></script>
1460
+ <script src="/backup.js"></script>
1342
1461
  <script src="/model-tester.js"></script>
1343
1462
  <script src="/settings.js"></script>
1344
1463
  <script src="/billing.js"></script>
@@ -1352,5 +1471,6 @@
1352
1471
  <script src="/vendor/qrcode/qrcode.min.js"></script>
1353
1472
  <script src="/share.js"></script>
1354
1473
  <script src="/app.js"></script>
1474
+ <script>Tooltip.init();</script>
1355
1475
  </body>
1356
1476
  </html>
Binary file
@@ -132,9 +132,6 @@ const Onboard = (() => {
132
132
  _renderProviderOptions();
133
133
  // Bind event listeners only once (delegation-based, safe to skip on re-entry)
134
134
  _bindCustomDropdown();
135
- // Show promo hint by default (no provider selected on initial entry)
136
- const promoHint = $("setup-provider-promo");
137
- if (promoHint && !promoHint.classList.contains("visible")) _showPromoHint(promoHint);
138
135
  }
139
136
 
140
137
  function _renderProviderOptions() {
@@ -249,18 +246,6 @@ const Onboard = (() => {
249
246
  });
250
247
  }
251
248
 
252
- function _showPromoHint(promoHint) {
253
- const items = [
254
- I18n.t("provider.promo.openclacky.1"),
255
- I18n.t("provider.promo.openclacky.2"),
256
- I18n.t("provider.promo.openclacky.3"),
257
- ];
258
- const title = `<div class="promo-title">${I18n.t("provider.promo.openclacky.title")}</div>`;
259
- const body = items.map(s => `<div class="promo-item"><span class="promo-icon">✦</span>${s}</div>`).join("");
260
- promoHint.innerHTML = `<div class="promo-inner">${title}${body}</div>`;
261
- promoHint.classList.add("visible");
262
- }
263
-
264
249
  function _bindCustomDropdown() {
265
250
  if (_dropdownBound) return; // listeners already attached
266
251
  _dropdownBound = true;
@@ -292,7 +277,6 @@ const Onboard = (() => {
292
277
  trigger.classList.remove("open");
293
278
 
294
279
  const getApiKeyLink = $("setup-get-apikey-link");
295
- const promoHint = $("setup-provider-promo");
296
280
  if (value === "__custom__") {
297
281
  // Custom: clear presets so the user can fill in their own values
298
282
  $("setup-model").value = "";
@@ -300,7 +284,6 @@ const Onboard = (() => {
300
284
  _updateSetupModelDropdown([]);
301
285
  _updateSetupBaseUrlDropdown(null);
302
286
  if (getApiKeyLink) getApiKeyLink.style.display = "none";
303
- if (promoHint) promoHint.classList.remove("visible");
304
287
  } else if (value) {
305
288
  const preset = _providers.find(p => p.id === value);
306
289
  if (preset) {
@@ -316,18 +299,8 @@ const Onboard = (() => {
316
299
  getApiKeyLink.style.display = "none";
317
300
  }
318
301
  }
319
- // Show promo hint for openclacky, hide for others
320
- if (promoHint) {
321
- if (value === "openclacky") {
322
- _showPromoHint(promoHint);
323
- } else {
324
- promoHint.classList.remove("visible");
325
- }
326
- }
327
302
  } else {
328
303
  if (getApiKeyLink) getApiKeyLink.style.display = "none";
329
- // Show promo hint when no provider selected (default state)
330
- if (promoHint) _showPromoHint(promoHint);
331
304
  }
332
305
  });
333
306
 
@@ -398,6 +371,119 @@ const Onboard = (() => {
398
371
  $("setup-btn-back").addEventListener("click", () => {
399
372
  _showSetupStep("lang");
400
373
  });
374
+
375
+ _bindDeviceStep();
376
+ }
377
+
378
+ // ── Device-authorization login (primary onboarding path) ──────────────────
379
+ let _devicePolling = false;
380
+
381
+ function _bindDeviceStep() {
382
+ const btn = $("setup-btn-device-login");
383
+ if (btn) btn.addEventListener("click", _startDeviceLogin);
384
+
385
+ const cancel = $("setup-device-cancel");
386
+ if (cancel) cancel.addEventListener("click", () => {
387
+ _devicePolling = false;
388
+ _showDevicePending(false);
389
+ });
390
+
391
+ const continueBtn = $("setup-btn-device-continue");
392
+ if (continueBtn) continueBtn.addEventListener("click", () => {
393
+ _launchOnboardSession();
394
+ });
395
+ }
396
+
397
+ function _showDevicePending(on) {
398
+ const pending = $("setup-device-pending");
399
+ const card = $("setup-device-card");
400
+ if (pending) pending.style.display = on ? "" : "none";
401
+ if (card) card.style.display = on ? "none" : "";
402
+ }
403
+
404
+ function _showDeviceSuccess(model) {
405
+ const pending = $("setup-device-pending");
406
+ const card = $("setup-device-card");
407
+ const success = $("setup-device-success");
408
+ const modelEl = $("setup-device-success-model");
409
+ if (pending) pending.style.display = "none";
410
+ if (card) card.style.display = "none";
411
+ if (success) success.style.display = "";
412
+ if (modelEl && model) modelEl.textContent = model;
413
+ }
414
+
415
+ function _setDeviceError(msg) {
416
+ const el = $("setup-device-error");
417
+ if (!el) return;
418
+ el.textContent = msg ? "✗ " + msg : "";
419
+ el.className = "setup-test-result" + (msg ? " result-fail" : "");
420
+ }
421
+
422
+ async function _startDeviceLogin() {
423
+ const zh = _selectedLang === "zh";
424
+ _setDeviceError("");
425
+
426
+ let data;
427
+ try {
428
+ const res = await fetch("/api/onboard/device/start", { method: "POST" });
429
+ data = await res.json();
430
+ } catch (_) {
431
+ data = null;
432
+ }
433
+
434
+ if (!data || !data.ok) {
435
+ _setDeviceError((data && data.error) || (zh ? "无法发起登录,请稍后重试。" : "Could not start login. Please try again."));
436
+ return;
437
+ }
438
+
439
+ const url = data.verification_uri_complete || data.verification_uri;
440
+ const codeEl = $("setup-device-usercode");
441
+ if (codeEl) codeEl.textContent = data.user_code || "—";
442
+ const link = $("setup-device-link");
443
+ if (link && url) link.href = url;
444
+
445
+ _showDevicePending(true);
446
+ if (url) window.open(url, "_blank", "noopener");
447
+
448
+ _devicePolling = true;
449
+ _pollDevice(data.device_code, (data.interval || 5) * 1000);
450
+ }
451
+
452
+ async function _pollDevice(deviceCode, intervalMs) {
453
+ const zh = _selectedLang === "zh";
454
+
455
+ while (_devicePolling) {
456
+ await new Promise(r => setTimeout(r, intervalMs));
457
+ if (!_devicePolling) return;
458
+
459
+ let data;
460
+ try {
461
+ const res = await fetch("/api/onboard/device/poll", {
462
+ method: "POST",
463
+ headers: { "Content-Type": "application/json" },
464
+ body: JSON.stringify({ device_code: deviceCode })
465
+ });
466
+ data = await res.json();
467
+ } catch (_) {
468
+ continue; // transient network error — keep polling
469
+ }
470
+
471
+ if (data.status === "approved") {
472
+ _devicePolling = false;
473
+ _showDeviceSuccess(data.default_model);
474
+ return;
475
+ }
476
+ if (data.status === "pending") continue;
477
+
478
+ // denied / expired / consumed / error
479
+ _devicePolling = false;
480
+ _showDevicePending(false);
481
+ const msg = data.status === "denied"
482
+ ? (zh ? "授权已被拒绝。" : "Authorization was denied.")
483
+ : (zh ? "授权已过期,请重新登录。" : "Authorization expired. Please try again.");
484
+ _setDeviceError(data.error || msg);
485
+ return;
486
+ }
401
487
  }
402
488
 
403
489
  async function _testAndSave() {
@@ -469,7 +555,7 @@ const Onboard = (() => {
469
555
  const res = await fetch("/api/sessions", {
470
556
  method: "POST",
471
557
  headers: { "Content-Type": "application/json" },
472
- body: JSON.stringify({ name: "Onboard", source: "setup" })
558
+ body: JSON.stringify({ name: "Onboard", source: "setup" })
473
559
  });
474
560
  const data = await res.json();
475
561
  const session = data.session;