openclacky 1.2.18 → 1.3.1

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -0
  3. data/lib/clacky/agent/time_machine.rb +256 -74
  4. data/lib/clacky/agent/tool_executor.rb +12 -0
  5. data/lib/clacky/agent.rb +15 -20
  6. data/lib/clacky/agent_config.rb +18 -0
  7. data/lib/clacky/cli.rb +55 -3
  8. data/lib/clacky/default_skills/media-gen/SKILL.md +172 -5
  9. data/lib/clacky/media/base.rb +93 -0
  10. data/lib/clacky/media/gemini.rb +10 -0
  11. data/lib/clacky/media/generator.rb +57 -0
  12. data/lib/clacky/media/openai_compat.rb +160 -0
  13. data/lib/clacky/message_history.rb +12 -7
  14. data/lib/clacky/providers.rb +29 -1
  15. data/lib/clacky/rich_ui_controller.rb +3 -1
  16. data/lib/clacky/server/backup_manager.rb +200 -0
  17. data/lib/clacky/server/channel/adapters/feishu/adapter.rb +10 -2
  18. data/lib/clacky/server/channel/adapters/feishu/bot.rb +68 -15
  19. data/lib/clacky/server/channel/channel_manager.rb +65 -50
  20. data/lib/clacky/server/http_server.rb +356 -14
  21. data/lib/clacky/server/scheduler.rb +19 -0
  22. data/lib/clacky/server/session_registry.rb +8 -4
  23. data/lib/clacky/session_manager.rb +40 -2
  24. data/lib/clacky/tools/trash_manager.rb +14 -0
  25. data/lib/clacky/ui2/components/command_suggestions.rb +1 -0
  26. data/lib/clacky/ui2/components/modal_component.rb +34 -7
  27. data/lib/clacky/ui2/ui_controller.rb +150 -19
  28. data/lib/clacky/utils/file_processor.rb +75 -4
  29. data/lib/clacky/version.rb +1 -1
  30. data/lib/clacky/web/app.css +2283 -1277
  31. data/lib/clacky/web/app.js +73 -1
  32. data/lib/clacky/web/backup.js +119 -0
  33. data/lib/clacky/web/billing.js +224 -11
  34. data/lib/clacky/web/channels.js +81 -11
  35. data/lib/clacky/web/design-sample.css +247 -0
  36. data/lib/clacky/web/design-sample.html +127 -0
  37. data/lib/clacky/web/favicon.svg +16 -0
  38. data/lib/clacky/web/i18n.js +167 -31
  39. data/lib/clacky/web/index.html +176 -55
  40. data/lib/clacky/web/logo_nav_dark.png +0 -0
  41. data/lib/clacky/web/onboard.js +121 -28
  42. data/lib/clacky/web/sessions.js +447 -192
  43. data/lib/clacky/web/settings.js +21 -1
  44. data/lib/clacky/web/skills.js +34 -1
  45. data/lib/clacky/web/tasks.js +129 -61
  46. data/lib/clacky/web/utils.js +72 -0
  47. data/lib/clacky/web/ws-dispatcher.js +6 -0
  48. data/lib/clacky.rb +1 -0
  49. metadata +9 -8
  50. 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">
@@ -284,6 +291,7 @@
284
291
  </span>
285
292
  </div>
286
293
  </div>
294
+ <div id="sidebar-resize-handle"></div>
287
295
  </aside>
288
296
 
289
297
  <!-- ── MAIN ─────────────────────────────────────────────────────────── -->
@@ -295,10 +303,30 @@
295
303
 
296
304
  <!-- Welcome screen -->
297
305
  <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>
306
+ <div class="ce-mark">
307
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">
308
+ <path d="M12 2l2.4 6.6L21 11l-6.6 2.4L12 20l-2.4-6.6L3 11l6.6-2.4z"/>
309
+ </svg>
310
+ </div>
311
+ <div class="ce-head">
312
+ <h2 id="welcome-title" class="ce-title" data-i18n="welcome.title" data-i18n-vars="brand={{BRAND_NAME}}">What should we build?</h2>
313
+ <p class="ce-sub" data-i18n="welcome.body">Your agent is standing by. Pick a starting point or just type.</p>
314
+ </div>
315
+ <div class="chips">
316
+ <button class="chip" data-welcome-prompt="welcome.chip.code.prompt">
317
+ <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>
318
+ <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>
319
+ </button>
320
+ <button class="chip" data-welcome-prompt="welcome.chip.task.prompt">
321
+ <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>
322
+ <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>
323
+ </button>
324
+ <button class="chip" data-welcome-prompt="welcome.chip.research.prompt">
325
+ <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>
326
+ <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>
327
+ </button>
328
+ </div>
329
+ <button id="btn-welcome-new" class="ce-blank" data-i18n="welcome.btn">Start a blank session</button>
302
330
  </div>
303
331
 
304
332
  <!-- Scheduled Tasks list panel (shown when user clicks "Scheduled Tasks") -->
@@ -406,9 +434,9 @@
406
434
  </button>
407
435
  <textarea id="user-input" rows="1"
408
436
  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>
437
+ placeholder="Message… (Enter to send, Shift-Enter for newline)"></textarea>
438
+ <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>
439
+ <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
440
  </div>
413
441
  </div>
414
442
  </div><!-- /#chat-main -->
@@ -777,7 +805,7 @@
777
805
  <!-- Models section -->
778
806
  <section class="settings-section">
779
807
  <div class="settings-section-title">
780
- <span data-i18n="settings.models.title">AI Models</span>
808
+ <span data-i18n="settings.models.title">Primary Model</span>
781
809
  <button id="btn-add-model" class="btn-settings-add" data-i18n="settings.models.add">+ Add Model</button>
782
810
  </div>
783
811
  <div id="model-cards"></div>
@@ -786,10 +814,10 @@
786
814
  <!-- Media generation section -->
787
815
  <section class="settings-section" id="media-section">
788
816
  <div class="settings-section-title">
789
- <span data-i18n="settings.media.title">Media Generation</span>
817
+ <span data-i18n="settings.media.title">Secondary Models</span>
790
818
  </div>
791
819
  <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.
820
+ Optional. Image / video / audio / vision models.
793
821
  </div>
794
822
  <div id="media-rows"></div>
795
823
  </section>
@@ -898,6 +926,33 @@
898
926
  </div>
899
927
  </section>
900
928
 
929
+ <!-- Backup section -->
930
+ <section class="settings-section" id="backup-section">
931
+ <div class="settings-section-title">
932
+ <span data-i18n="settings.backup.title">Backup</span>
933
+ </div>
934
+ <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>
935
+
936
+ <div class="backup-auto-row">
937
+ <label class="toggle-switch">
938
+ <input type="checkbox" id="backup-auto-toggle">
939
+ <span class="toggle-slider"></span>
940
+ </label>
941
+ <span class="backup-auto-label" data-i18n="settings.backup.autoLabel">Automatic backup</span>
942
+ <span class="backup-auto-hint" data-i18n="settings.backup.autoHint">Daily at 03:00, keeps the latest 7</span>
943
+ </div>
944
+
945
+ <label class="backup-option">
946
+ <input type="checkbox" id="backup-include-sessions">
947
+ <span data-i18n="settings.backup.includeSessions">Include session history (larger archive)</span>
948
+ </label>
949
+
950
+ <div class="backup-actions">
951
+ <button id="btn-backup-now" class="btn-settings-action" data-i18n="settings.backup.runNow">💾 Download backup</button>
952
+ <span id="backup-status" class="model-test-result"></span>
953
+ </div>
954
+ </section>
955
+
901
956
  <!-- Brand & License section -->
902
957
  <section class="settings-section" id="brand-license-section">
903
958
  <div class="settings-section-title">
@@ -1153,6 +1208,63 @@
1153
1208
  <div id="setup-phase-key" style="display:none">
1154
1209
  <p class="setup-phase-label" data-i18n="onboard.key.title">Connect your AI model</p>
1155
1210
 
1211
+ <!-- Primary path: one-click device login -->
1212
+ <div id="setup-device-block">
1213
+ <div id="setup-device-card" class="setup-device-card">
1214
+ <div class="setup-device-card-head">
1215
+ <span class="setup-device-card-title" data-i18n="onboard.device.card.title">OpenClacky AI Keys</span>
1216
+ <span class="setup-device-card-badge" data-i18n="provider.recommended">Recommended</span>
1217
+ </div>
1218
+ <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>
1219
+ <ul class="setup-device-card-points">
1220
+ <li data-i18n="onboard.device.card.point1">One key for all frontier models — Claude, Gemini, DeepSeek &amp; more</li>
1221
+ <li data-i18n="onboard.device.card.point2">Same pricing as official APIs, switch models instantly</li>
1222
+ </ul>
1223
+ <button id="setup-btn-device-login" class="setup-submit-btn setup-device-btn">
1224
+ <span data-i18n="onboard.device.btn">Get started free →</span>
1225
+ </button>
1226
+ </div>
1227
+
1228
+ <!-- Pending state: shown while waiting for browser approval -->
1229
+ <div id="setup-device-pending" style="display:none">
1230
+ <div class="setup-device-spinner"></div>
1231
+ <p class="setup-device-pending-text" data-i18n="onboard.device.pending">Waiting for confirmation in your browser…</p>
1232
+ <p class="setup-device-code">
1233
+ <span data-i18n="onboard.device.code">Verification code:</span>
1234
+ <strong id="setup-device-usercode">—</strong>
1235
+ </p>
1236
+ <a id="setup-device-link" href="#" target="_blank" rel="noopener" class="setup-device-reopen" data-i18n="onboard.device.reopen">Open browser again →</a>
1237
+ <button id="setup-device-cancel" class="setup-back-btn" data-i18n="onboard.device.cancel">Cancel</button>
1238
+ </div>
1239
+
1240
+ <!-- Success state: shown after approval, before launching onboard session -->
1241
+ <div id="setup-device-success" style="display:none" class="setup-device-success">
1242
+ <div class="setup-device-success-head">
1243
+ <span class="setup-device-success-icon">✓</span>
1244
+ <span class="setup-device-success-title" data-i18n="onboard.device.success.title">You're in!</span>
1245
+ </div>
1246
+ <p class="setup-device-success-lead" data-i18n="onboard.device.success.lead">Your account is connected. Here's what you got:</p>
1247
+ <ul class="setup-device-success-points">
1248
+ <li>
1249
+ <strong data-i18n="onboard.device.success.credit.label">Free trial credit:</strong>
1250
+ <span data-i18n="onboard.device.success.credit.value">$1.00 (one-time, on us)</span>
1251
+ </li>
1252
+ <li>
1253
+ <strong data-i18n="onboard.device.success.model.label">Trial model:</strong>
1254
+ <span id="setup-device-success-model">or-gemini-3-5-flash</span>
1255
+ </li>
1256
+ <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>
1257
+ </ul>
1258
+ <button id="setup-btn-device-continue" class="setup-submit-btn" data-i18n="onboard.device.success.btn">Start using Clacky →</button>
1259
+ </div>
1260
+
1261
+ <div id="setup-device-error" class="setup-test-result" style="min-height:0;"></div>
1262
+ </div>
1263
+
1264
+ <!-- Secondary path: manual key entry, collapsed by default -->
1265
+ <details id="setup-manual-details">
1266
+ <summary id="setup-manual-summary" data-i18n="onboard.manual.toggle">Configure manually with your own API key</summary>
1267
+
1156
1268
  <div class="setup-field">
1157
1269
  <label class="setup-label" data-i18n="onboard.key.provider">Provider</label>
1158
1270
  <div class="custom-select-wrapper" id="setup-provider-wrapper">
@@ -1166,7 +1278,6 @@
1166
1278
  <div class="custom-select-option" data-value="" data-i18n="onboard.key.provider.placeholder">— Choose provider —</div>
1167
1279
  </div>
1168
1280
  </div>
1169
- <div id="setup-provider-promo" class="provider-promo-hint"></div>
1170
1281
  </div>
1171
1282
 
1172
1283
  <div class="setup-field">
@@ -1218,6 +1329,7 @@
1218
1329
  <button id="setup-btn-back" class="setup-back-btn" data-i18n="onboard.key.btn.back">← Back</button>
1219
1330
  <button id="setup-btn-test" class="setup-submit-btn" data-i18n="onboard.key.btn.test">Test & Continue →</button>
1220
1331
  </div>
1332
+ </details>
1221
1333
  </div>
1222
1334
 
1223
1335
  </div>
@@ -1254,9 +1366,14 @@
1254
1366
 
1255
1367
  <div class="modal-field">
1256
1368
  <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">
1369
+ <div class="modal-input-row">
1370
+ <input id="new-session-directory" type="text" class="modal-input"
1371
+ data-i18n-placeholder="sessions.modal.directory.placeholder"
1372
+ placeholder="~/workspace/my-project">
1373
+ <button id="new-session-browse-btn" type="button" class="modal-browse-btn" data-i18n-title="sessions.modal.directory.browse" title="Browse…" aria-label="Browse">
1374
+ <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>
1375
+ </button>
1376
+ </div>
1260
1377
  </div>
1261
1378
 
1262
1379
  <div id="new-session-init-project-field" class="modal-field-checkbox" style="display:none">
@@ -1302,7 +1419,9 @@
1302
1419
  <!-- Rename Session Modal -->
1303
1420
  <div id="rename-modal-overlay" class="modal-overlay" style="display:none">
1304
1421
  <div class="modal-box sm">
1305
- <div class="modal-title" data-i18n="sessions.actions.rename">Rename</div>
1422
+ <div class="modal-header">
1423
+ <h3 class="modal-title" data-i18n="sessions.actions.rename">Rename</h3>
1424
+ </div>
1306
1425
  <div class="modal-body">
1307
1426
  <div class="modal-field">
1308
1427
  <label class="modal-label" for="rename-modal-input">
@@ -1312,14 +1431,14 @@
1312
1431
  <input type="text" id="rename-modal-input" class="modal-input" autocomplete="off" spellcheck="false">
1313
1432
  </div>
1314
1433
  </div>
1315
- <div class="modal-actions">
1434
+ <div class="modal-footer">
1316
1435
  <button id="rename-modal-cancel" class="btn-secondary" data-i18n="modal.cancel">Cancel</button>
1317
1436
  <button id="rename-modal-save" class="btn-primary" data-i18n="modal.ok">OK</button>
1318
1437
  </div>
1319
1438
  </div>
1320
1439
  </div>
1321
1440
 
1322
-
1441
+ <div id="tooltip" style="display:none"></div>
1323
1442
 
1324
1443
  <script src="/marked.min.js"></script>
1325
1444
  <script src="/vendor/hljs/highlight.min.js"></script>
@@ -1339,6 +1458,7 @@
1339
1458
  <script src="/skills.js"></script>
1340
1459
  <script src="/channels.js"></script>
1341
1460
  <script src="/mcp.js"></script>
1461
+ <script src="/backup.js"></script>
1342
1462
  <script src="/model-tester.js"></script>
1343
1463
  <script src="/settings.js"></script>
1344
1464
  <script src="/billing.js"></script>
@@ -1352,5 +1472,6 @@
1352
1472
  <script src="/vendor/qrcode/qrcode.min.js"></script>
1353
1473
  <script src="/share.js"></script>
1354
1474
  <script src="/app.js"></script>
1475
+ <script>Tooltip.init();</script>
1355
1476
  </body>
1356
1477
  </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,126 @@ 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
+ const w = window.open("about:blank", "_blank");
427
+
428
+ let data;
429
+ try {
430
+ const res = await fetch("/api/onboard/device/start", { method: "POST" });
431
+ data = await res.json();
432
+ } catch (_) {
433
+ data = null;
434
+ }
435
+
436
+ if (!data || !data.ok) {
437
+ if (w && !w.closed) w.close();
438
+ _setDeviceError((data && data.error) || (zh ? "无法发起登录,请稍后重试。" : "Could not start login. Please try again."));
439
+ return;
440
+ }
441
+
442
+ const url = data.verification_uri_complete || data.verification_uri;
443
+ const codeEl = $("setup-device-usercode");
444
+ if (codeEl) codeEl.textContent = data.user_code || "—";
445
+ const link = $("setup-device-link");
446
+ if (link && url) link.href = url;
447
+
448
+ _showDevicePending(true);
449
+ if (w && !w.closed) {
450
+ w.location.href = url;
451
+ } else {
452
+ window.open(url, "_blank");
453
+ }
454
+
455
+ _devicePolling = true;
456
+ _pollDevice(data.device_code, (data.interval || 5) * 1000);
457
+ }
458
+
459
+ async function _pollDevice(deviceCode, intervalMs) {
460
+ const zh = _selectedLang === "zh";
461
+
462
+ while (_devicePolling) {
463
+ await new Promise(r => setTimeout(r, intervalMs));
464
+ if (!_devicePolling) return;
465
+
466
+ let data;
467
+ try {
468
+ const res = await fetch("/api/onboard/device/poll", {
469
+ method: "POST",
470
+ headers: { "Content-Type": "application/json" },
471
+ body: JSON.stringify({ device_code: deviceCode })
472
+ });
473
+ data = await res.json();
474
+ } catch (_) {
475
+ continue; // transient network error — keep polling
476
+ }
477
+
478
+ if (data.status === "approved") {
479
+ _devicePolling = false;
480
+ _showDeviceSuccess(data.default_model);
481
+ return;
482
+ }
483
+ if (data.status === "pending") continue;
484
+
485
+ // denied / expired / consumed / error
486
+ _devicePolling = false;
487
+ _showDevicePending(false);
488
+ const msg = data.status === "denied"
489
+ ? (zh ? "授权已被拒绝。" : "Authorization was denied.")
490
+ : (zh ? "授权已过期,请重新登录。" : "Authorization expired. Please try again.");
491
+ _setDeviceError(data.error || msg);
492
+ return;
493
+ }
401
494
  }
402
495
 
403
496
  async function _testAndSave() {
@@ -469,7 +562,7 @@ const Onboard = (() => {
469
562
  const res = await fetch("/api/sessions", {
470
563
  method: "POST",
471
564
  headers: { "Content-Type": "application/json" },
472
- body: JSON.stringify({ name: "Onboard", source: "setup" })
565
+ body: JSON.stringify({ name: "Onboard", source: "setup" })
473
566
  });
474
567
  const data = await res.json();
475
568
  const session = data.session;