@myrialabs/clopen 0.2.3 → 0.2.4

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 (35) hide show
  1. package/backend/engine/adapters/claude/stream.ts +107 -0
  2. package/backend/engine/adapters/opencode/stream.ts +81 -1
  3. package/backend/engine/types.ts +17 -0
  4. package/backend/git/git-service.ts +2 -1
  5. package/backend/ws/git/commit-message.ts +108 -0
  6. package/backend/ws/git/index.ts +3 -1
  7. package/backend/ws/system/index.ts +7 -1
  8. package/backend/ws/system/operations.ts +28 -2
  9. package/frontend/App.svelte +3 -0
  10. package/frontend/components/auth/SetupPage.svelte +2 -2
  11. package/frontend/components/chat/input/ChatInput.svelte +1 -1
  12. package/frontend/components/chat/message/ChatMessage.svelte +64 -16
  13. package/frontend/components/checkpoint/ConflictResolutionModal.svelte +189 -0
  14. package/frontend/components/checkpoint/TimelineModal.svelte +7 -162
  15. package/frontend/components/common/feedback/RestartRequiredModal.svelte +53 -0
  16. package/frontend/components/common/feedback/UpdateBanner.svelte +17 -6
  17. package/frontend/components/git/BranchManager.svelte +143 -155
  18. package/frontend/components/git/CommitForm.svelte +61 -11
  19. package/frontend/components/settings/SettingsModal.svelte +1 -1
  20. package/frontend/components/settings/SettingsView.svelte +1 -1
  21. package/frontend/components/settings/engines/AIEnginesSettings.svelte +2 -2
  22. package/frontend/components/settings/general/UpdateSettings.svelte +10 -3
  23. package/frontend/components/settings/git/GitSettings.svelte +392 -0
  24. package/frontend/components/settings/model/EngineModelPicker.svelte +275 -0
  25. package/frontend/components/settings/model/ModelSettings.svelte +172 -289
  26. package/frontend/components/workspace/PanelHeader.svelte +1 -3
  27. package/frontend/components/workspace/WorkspaceLayout.svelte +11 -1
  28. package/frontend/components/workspace/panels/GitPanel.svelte +12 -5
  29. package/frontend/main.ts +4 -0
  30. package/frontend/stores/features/settings.svelte.ts +13 -2
  31. package/frontend/stores/ui/settings-modal.svelte.ts +9 -9
  32. package/frontend/stores/ui/update.svelte.ts +45 -4
  33. package/package.json +1 -1
  34. package/shared/types/git.ts +15 -0
  35. package/shared/types/stores/settings.ts +12 -0
@@ -35,7 +35,6 @@
35
35
  let showAddRemoteForm = $state(false);
36
36
  let newRemoteName = $state('origin');
37
37
  let newRemoteUrl = $state('');
38
- let showRemoteSection = $state(false);
39
38
 
40
39
  // Confirm dialog
41
40
  let showConfirmDialog = $state(false);
@@ -168,49 +167,6 @@
168
167
  {/snippet}
169
168
 
170
169
  {#snippet children()}
171
- <!-- Create branch form -->
172
- {#if showCreateForm}
173
- <div class="mb-4 p-3 bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700 rounded-lg space-y-2">
174
- <input
175
- type="text"
176
- bind:value={newBranchName}
177
- placeholder="New branch name..."
178
- class="w-full px-3 py-2 text-sm bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-lg text-slate-900 dark:text-slate-100 outline-none focus:border-violet-500/40"
179
- onkeydown={(e) => e.key === 'Enter' && handleCreate()}
180
- autofocus
181
- />
182
- <div class="flex gap-2">
183
- <button
184
- type="button"
185
- class="flex-1 px-3 py-2 text-sm font-medium rounded-lg transition-colors cursor-pointer border-none
186
- {newBranchName.trim()
187
- ? 'bg-violet-600 text-white hover:bg-violet-700'
188
- : 'bg-slate-200 dark:bg-slate-700 text-slate-400 dark:text-slate-500 cursor-not-allowed'}"
189
- onclick={handleCreate}
190
- disabled={!newBranchName.trim()}
191
- >
192
- Create Branch
193
- </button>
194
- <button
195
- type="button"
196
- class="px-3 py-2 text-sm font-medium bg-transparent border border-slate-200 dark:border-slate-700 text-slate-600 dark:text-slate-400 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors cursor-pointer"
197
- onclick={() => { showCreateForm = false; newBranchName = ''; }}
198
- >
199
- Cancel
200
- </button>
201
- </div>
202
- </div>
203
- {:else}
204
- <button
205
- type="button"
206
- class="flex items-center justify-center gap-2 w-full mb-4 py-2.5 px-3 border border-dashed border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-500 hover:text-violet-600 hover:border-violet-400 transition-colors cursor-pointer bg-transparent"
207
- onclick={() => showCreateForm = true}
208
- >
209
- <Icon name="lucide:plus" class="w-4 h-4" />
210
- <span>Create New Branch</span>
211
- </button>
212
- {/if}
213
-
214
170
  <!-- Search -->
215
171
  <div class="mb-4">
216
172
  <div class="flex items-center gap-2 py-2.5 px-3.5 bg-slate-100/80 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-800 rounded-lg">
@@ -242,7 +198,7 @@
242
198
  {activeTab === 'local'
243
199
  ? 'bg-violet-500/10 text-violet-600'
244
200
  : 'bg-transparent text-slate-500 hover:text-slate-700 dark:hover:text-slate-300'}"
245
- onclick={() => activeTab = 'local'}
201
+ onclick={() => { activeTab = 'local'; showCreateForm = false; newBranchName = ''; }}
246
202
  >
247
203
  Local ({filteredLocal.length})
248
204
  </button>
@@ -261,6 +217,49 @@
261
217
  <!-- Branch list -->
262
218
  <div class="space-y-1.5 max-h-80 overflow-y-auto">
263
219
  {#if activeTab === 'local'}
220
+ <!-- Create branch form (Local tab only) -->
221
+ {#if showCreateForm}
222
+ <div class="mb-2 p-3 bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700 rounded-lg space-y-2">
223
+ <input
224
+ type="text"
225
+ bind:value={newBranchName}
226
+ placeholder="New branch name..."
227
+ class="w-full px-3 py-2 text-sm bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-lg text-slate-900 dark:text-slate-100 outline-none focus:border-violet-500/40"
228
+ onkeydown={(e) => e.key === 'Enter' && handleCreate()}
229
+ autofocus
230
+ />
231
+ <div class="flex gap-2">
232
+ <button
233
+ type="button"
234
+ class="flex-1 px-3 py-2 text-sm font-medium rounded-lg transition-colors cursor-pointer border-none
235
+ {newBranchName.trim()
236
+ ? 'bg-violet-600 text-white hover:bg-violet-700'
237
+ : 'bg-slate-200 dark:bg-slate-700 text-slate-400 dark:text-slate-500 cursor-not-allowed'}"
238
+ onclick={handleCreate}
239
+ disabled={!newBranchName.trim()}
240
+ >
241
+ Create Branch
242
+ </button>
243
+ <button
244
+ type="button"
245
+ class="px-3 py-2 text-sm font-medium bg-transparent border border-slate-200 dark:border-slate-700 text-slate-600 dark:text-slate-400 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors cursor-pointer"
246
+ onclick={() => { showCreateForm = false; newBranchName = ''; }}
247
+ >
248
+ Cancel
249
+ </button>
250
+ </div>
251
+ </div>
252
+ {:else}
253
+ <button
254
+ type="button"
255
+ class="flex items-center justify-center gap-2 w-full mb-2 py-2.5 px-3 border border-dashed border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-500 hover:text-violet-600 hover:border-violet-400 transition-colors cursor-pointer bg-transparent"
256
+ onclick={() => showCreateForm = true}
257
+ >
258
+ <Icon name="lucide:plus" class="w-4 h-4" />
259
+ <span>Create New Branch</span>
260
+ </button>
261
+ {/if}
262
+
264
263
  {#each filteredLocal as branch (branch.name)}
265
264
  <div
266
265
  class="group flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors
@@ -320,129 +319,118 @@
320
319
  </div>
321
320
  {/each}
322
321
  {:else if activeTab === 'remote'}
323
- {#each filteredRemote as branch (branch.name)}
324
- <div
325
- class="group flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800/50 transition-colors border border-slate-200 dark:border-slate-700"
326
- >
327
- <div class="flex items-center gap-2.5 flex-1 min-w-0">
328
- <Icon name="lucide:cloud" class="w-4 h-4 text-slate-400 shrink-0" />
329
- <span class="text-sm text-slate-900 dark:text-slate-100 truncate">{branch.name}</span>
330
- </div>
331
-
332
- <div class="items-center gap-1 shrink-0 hidden group-hover:flex">
333
- <button
334
- type="button"
335
- class="flex items-center justify-center w-8 h-8 rounded-lg text-slate-500 hover:bg-violet-500/10 hover:text-violet-600 transition-colors cursor-pointer bg-transparent border-none"
336
- onclick={() => handleSwitchRemote(branch.name)}
337
- title="Checkout remote branch"
338
- >
339
- <Icon name="lucide:arrow-right" class="w-4 h-4" />
340
- </button>
341
- </div>
322
+ {#if isLoadingRemotes}
323
+ <div class="flex items-center justify-center py-8">
324
+ <div class="w-4 h-4 border-2 border-slate-200 dark:border-slate-700 border-t-violet-600 rounded-full animate-spin"></div>
342
325
  </div>
343
- {:else}
326
+ {:else if remotes.length === 0}
344
327
  <div class="flex flex-col items-center gap-2 py-8 text-slate-500 dark:text-slate-400 text-sm">
345
- <Icon name="lucide:search-x" class="w-10 h-10 opacity-40" />
346
- <p class="font-medium">No remote branches found</p>
328
+ <Icon name="lucide:server-off" class="w-10 h-10 opacity-40" />
329
+ <p class="font-medium">No remote connections</p>
330
+ <p class="text-xs text-center opacity-70">Add a remote below to track remote branches</p>
347
331
  </div>
348
- {/each}
349
- {/if}
350
- </div>
351
-
352
- <!-- Remote Connections Section -->
353
- <div class="mt-4 pt-4 border-t border-slate-200 dark:border-slate-700">
354
- <button
355
- type="button"
356
- class="flex items-center gap-2 w-full text-left bg-transparent border-none cursor-pointer px-0 py-1"
357
- onclick={() => showRemoteSection = !showRemoteSection}
358
- >
359
- <Icon name={showRemoteSection ? 'lucide:chevron-down' : 'lucide:chevron-right'} class="w-3.5 h-3.5 text-slate-400" />
360
- <Icon name="lucide:server" class="w-3.5 h-3.5 text-slate-500" />
361
- <span class="text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide">
362
- Remote Servers ({remotes.length})
363
- </span>
364
- </button>
365
-
366
- {#if showRemoteSection}
367
- <div class="mt-2 space-y-2">
368
- {#if isLoadingRemotes}
369
- <div class="flex items-center justify-center py-4">
370
- <div class="w-4 h-4 border-2 border-slate-200 dark:border-slate-700 border-t-violet-600 rounded-full animate-spin"></div>
371
- </div>
372
- {:else}
373
- <!-- Existing remotes -->
374
- {#each remotes as remote (remote.name)}
375
- <div class="group flex items-center gap-2.5 px-3 py-2 rounded-lg border border-slate-200 dark:border-slate-700 bg-slate-50/50 dark:bg-slate-800/30">
376
- <div class="flex items-center justify-center w-6 h-6 rounded-md bg-slate-100 dark:bg-slate-800">
377
- <Icon name="lucide:globe" class="w-3.5 h-3.5 text-slate-500" />
378
- </div>
379
- <div class="flex-1 min-w-0">
380
- <div class="text-sm font-medium text-slate-900 dark:text-slate-100">{remote.name}</div>
381
- <div class="text-xs text-slate-500 dark:text-slate-400 truncate font-mono">{remote.fetchUrl}</div>
382
- </div>
383
- <button
384
- type="button"
385
- class="flex items-center justify-center w-7 h-7 rounded-md text-slate-400 hover:bg-red-500/10 hover:text-red-500 transition-colors cursor-pointer bg-transparent border-none shrink-0 opacity-0 group-hover:opacity-100"
386
- onclick={() => handleRemoveRemote(remote.name)}
387
- title="Disconnect remote"
388
- >
389
- <Icon name="lucide:unlink" class="w-3.5 h-3.5" />
390
- </button>
391
- </div>
392
- {/each}
393
-
394
- <!-- Add remote form -->
395
- {#if showAddRemoteForm}
396
- <div class="p-3 bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700 rounded-lg space-y-2">
397
- <input
398
- type="text"
399
- bind:value={newRemoteName}
400
- placeholder="Name (e.g. origin)"
401
- class="w-full px-3 py-2 text-sm bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-lg text-slate-900 dark:text-slate-100 outline-none focus:border-violet-500/40"
402
- />
403
- <input
404
- type="text"
405
- bind:value={newRemoteUrl}
406
- placeholder="URL (e.g. https://github.com/user/repo.git)"
407
- class="w-full px-3 py-2 text-sm bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-lg text-slate-900 dark:text-slate-100 outline-none focus:border-violet-500/40 font-mono"
408
- onkeydown={(e) => e.key === 'Enter' && handleAddRemote()}
409
- autofocus
410
- />
411
- <div class="flex gap-2">
412
- <button
413
- type="button"
414
- class="flex-1 px-3 py-2 text-sm font-medium rounded-lg transition-colors cursor-pointer border-none
415
- {newRemoteName.trim() && newRemoteUrl.trim()
416
- ? 'bg-violet-600 text-white hover:bg-violet-700'
417
- : 'bg-slate-200 dark:bg-slate-700 text-slate-400 dark:text-slate-500 cursor-not-allowed'}"
418
- onclick={handleAddRemote}
419
- disabled={!newRemoteName.trim() || !newRemoteUrl.trim()}
420
- >
421
- Connect
422
- </button>
332
+ {:else}
333
+ {#each remotes as remote (remote.name)}
334
+ {@const remoteBranches = filteredRemote.filter(b => b.name.startsWith(remote.name + '/'))}
335
+ {#if !searchQuery || remoteBranches.length > 0}
336
+ <div class="space-y-1">
337
+ <!-- Remote group header -->
338
+ <div class="group flex items-center gap-2 px-2 py-1.5 rounded-lg">
339
+ <Icon name="lucide:server" class="w-3.5 h-3.5 text-slate-400 shrink-0" />
340
+ <span class="text-xs font-semibold text-slate-600 dark:text-slate-300">{remote.name}</span>
341
+ <span class="text-xs text-slate-400 dark:text-slate-500 font-mono truncate flex-1">{remote.fetchUrl}</span>
423
342
  <button
424
343
  type="button"
425
- class="px-3 py-2 text-sm font-medium bg-transparent border border-slate-200 dark:border-slate-700 text-slate-600 dark:text-slate-400 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors cursor-pointer"
426
- onclick={() => { showAddRemoteForm = false; newRemoteName = 'origin'; newRemoteUrl = ''; }}
344
+ class="flex items-center justify-center w-6 h-6 rounded-md text-slate-400 hover:bg-red-500/10 hover:text-red-500 transition-colors cursor-pointer bg-transparent border-none shrink-0 opacity-0 group-hover:opacity-100"
345
+ onclick={() => handleRemoveRemote(remote.name)}
346
+ title="Disconnect remote"
427
347
  >
428
- Cancel
348
+ <Icon name="lucide:unlink" class="w-3.5 h-3.5" />
429
349
  </button>
430
350
  </div>
351
+
352
+ <!-- Branches under this remote -->
353
+ {#if remoteBranches.length > 0}
354
+ <div class="ml-5 space-y-1">
355
+ {#each remoteBranches as branch (branch.name)}
356
+ <div class="group flex items-center gap-2.5 px-3 py-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800/50 transition-colors border border-slate-200 dark:border-slate-700">
357
+ <Icon name="lucide:git-branch" class="w-3.5 h-3.5 text-slate-400 shrink-0" />
358
+ <span class="text-sm text-slate-900 dark:text-slate-100 truncate flex-1">
359
+ {branch.name.substring(remote.name.length + 1)}
360
+ </span>
361
+ <button
362
+ type="button"
363
+ class="flex items-center justify-center w-7 h-7 rounded-lg text-slate-400 hover:bg-violet-500/10 hover:text-violet-600 transition-colors cursor-pointer bg-transparent border-none shrink-0 opacity-0 group-hover:opacity-100"
364
+ onclick={() => handleSwitchRemote(branch.name)}
365
+ title="Checkout remote branch locally"
366
+ >
367
+ <Icon name="lucide:arrow-right" class="w-4 h-4" />
368
+ </button>
369
+ </div>
370
+ {/each}
371
+ </div>
372
+ {:else if !searchQuery}
373
+ <p class="ml-7 text-xs text-slate-400 dark:text-slate-500 py-1">No branches — try Fetch</p>
374
+ {/if}
431
375
  </div>
432
- {:else}
376
+ {/if}
377
+ {/each}
378
+ {/if}
379
+ {/if}
380
+ </div>
381
+
382
+ <!-- Add remote (only shown in Remote tab) -->
383
+ {#if activeTab === 'remote'}
384
+ <div class="mt-3 pt-3 border-t border-slate-200 dark:border-slate-700">
385
+ {#if showAddRemoteForm}
386
+ <div class="p-3 bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700 rounded-lg space-y-2">
387
+ <input
388
+ type="text"
389
+ bind:value={newRemoteName}
390
+ placeholder="Name (e.g. origin)"
391
+ class="w-full px-3 py-2 text-sm bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-lg text-slate-900 dark:text-slate-100 outline-none focus:border-violet-500/40"
392
+ />
393
+ <input
394
+ type="text"
395
+ bind:value={newRemoteUrl}
396
+ placeholder="URL (e.g. https://github.com/user/repo.git)"
397
+ class="w-full px-3 py-2 text-sm bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-lg text-slate-900 dark:text-slate-100 outline-none focus:border-violet-500/40 font-mono"
398
+ onkeydown={(e) => e.key === 'Enter' && handleAddRemote()}
399
+ autofocus
400
+ />
401
+ <div class="flex gap-2">
433
402
  <button
434
403
  type="button"
435
- class="flex items-center gap-2 w-full px-3 py-2 text-xs text-slate-500 hover:text-violet-600 hover:bg-violet-500/5 rounded-lg transition-colors cursor-pointer bg-transparent border-none"
436
- onclick={() => { showAddRemoteForm = true; showRemoteSection = true; }}
404
+ class="flex-1 px-3 py-2 text-sm font-medium rounded-lg transition-colors cursor-pointer border-none
405
+ {newRemoteName.trim() && newRemoteUrl.trim()
406
+ ? 'bg-violet-600 text-white hover:bg-violet-700'
407
+ : 'bg-slate-200 dark:bg-slate-700 text-slate-400 dark:text-slate-500 cursor-not-allowed'}"
408
+ onclick={handleAddRemote}
409
+ disabled={!newRemoteName.trim() || !newRemoteUrl.trim()}
437
410
  >
438
- <Icon name="lucide:plus" class="w-3.5 h-3.5" />
439
- <span>Add Remote Server</span>
411
+ Connect
440
412
  </button>
441
- {/if}
442
- {/if}
443
- </div>
444
- {/if}
445
- </div>
413
+ <button
414
+ type="button"
415
+ class="px-3 py-2 text-sm font-medium bg-transparent border border-slate-200 dark:border-slate-700 text-slate-600 dark:text-slate-400 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors cursor-pointer"
416
+ onclick={() => { showAddRemoteForm = false; newRemoteName = 'origin'; newRemoteUrl = ''; }}
417
+ >
418
+ Cancel
419
+ </button>
420
+ </div>
421
+ </div>
422
+ {:else}
423
+ <button
424
+ type="button"
425
+ class="flex items-center gap-2 w-full px-3 py-2 text-xs text-slate-500 hover:text-violet-600 hover:bg-violet-500/5 rounded-lg transition-colors cursor-pointer bg-transparent border-none"
426
+ onclick={() => showAddRemoteForm = true}
427
+ >
428
+ <Icon name="lucide:plus" class="w-3.5 h-3.5" />
429
+ <span>Add Remote</span>
430
+ </button>
431
+ {/if}
432
+ </div>
433
+ {/if}
446
434
  {/snippet}
447
435
  </Modal>
448
436
 
@@ -1,5 +1,10 @@
1
1
  <script lang="ts">
2
+ import { tick } from 'svelte';
2
3
  import Icon from '$frontend/components/common/display/Icon.svelte';
4
+ import { settings } from '$frontend/stores/features/settings.svelte';
5
+ import { projectState } from '$frontend/stores/core/projects.svelte';
6
+ import { showError } from '$frontend/stores/ui/notification.svelte';
7
+ import ws from '$frontend/utils/ws';
3
8
 
4
9
  interface Props {
5
10
  stagedCount: number;
@@ -11,6 +16,7 @@
11
16
 
12
17
  let commitMessage = $state('');
13
18
  let textareaEl = $state<HTMLTextAreaElement | null>(null);
19
+ let isGenerating = $state(false);
14
20
 
15
21
  function handleCommit() {
16
22
  if (!commitMessage.trim() || stagedCount === 0) return;
@@ -42,21 +48,65 @@
42
48
  function handleInput() {
43
49
  autoResize();
44
50
  }
51
+
52
+ async function generateCommitMessage() {
53
+ const projectId = projectState.currentProject?.id;
54
+ if (!projectId || stagedCount === 0 || isGenerating) return;
55
+
56
+ isGenerating = true;
57
+ try {
58
+ const { useCustomModel, engine, model, format } = settings.commitGenerator;
59
+ const resolvedEngine = useCustomModel ? engine : settings.selectedEngine;
60
+ const resolvedModel = useCustomModel ? model : settings.selectedModel;
61
+ const result = await ws.http('git:generate-commit-message', {
62
+ projectId,
63
+ engine: resolvedEngine,
64
+ model: resolvedModel,
65
+ format
66
+ });
67
+ commitMessage = result.message;
68
+ await tick();
69
+ autoResize();
70
+ } catch (err) {
71
+ showError('Generate Failed', err instanceof Error ? err.message : 'Failed to generate commit message');
72
+ } finally {
73
+ isGenerating = false;
74
+ }
75
+ }
45
76
  </script>
46
77
 
47
78
  <div class="px-2 py-2">
48
79
  <div class="flex flex-col gap-1.5">
49
- <textarea
50
- bind:this={textareaEl}
51
- bind:value={commitMessage}
52
- placeholder="Commit message..."
53
- class="w-full px-2.5 py-2 text-sm bg-white dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700 rounded-md text-slate-900 dark:text-slate-100 placeholder:text-slate-400 dark:placeholder:text-slate-600 resize-none outline-none focus:ring-1 focus:ring-violet-500 transition-colors"
54
- rows="1"
55
- style="overflow-y: hidden;"
56
- onkeydown={handleKeydown}
57
- oninput={handleInput}
58
- disabled={isCommitting}
59
- ></textarea>
80
+ <div class="relative">
81
+ <textarea
82
+ bind:this={textareaEl}
83
+ bind:value={commitMessage}
84
+ placeholder="Commit message..."
85
+ class="w-full px-2.5 py-2 pr-8 text-sm bg-white dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700 rounded-md text-slate-900 dark:text-slate-100 placeholder:text-slate-400 dark:placeholder:text-slate-600 resize-none outline-none focus:ring-1 focus:ring-violet-500 transition-colors"
86
+ rows="1"
87
+ style="overflow-y: hidden;"
88
+ onkeydown={handleKeydown}
89
+ oninput={handleInput}
90
+ disabled={isCommitting || isGenerating}
91
+ ></textarea>
92
+ <!-- AI generate button -->
93
+ <button
94
+ type="button"
95
+ class="absolute top-1.5 right-1.5 flex items-center justify-center w-6 h-6 rounded transition-all duration-150
96
+ {stagedCount > 0 && !isGenerating && !isCommitting
97
+ ? 'text-slate-400 hover:text-violet-500 hover:bg-violet-500/10 cursor-pointer'
98
+ : 'text-slate-300 dark:text-slate-700 cursor-not-allowed'}"
99
+ onclick={generateCommitMessage}
100
+ disabled={stagedCount === 0 || isGenerating || isCommitting}
101
+ title="Generate commit message with AI"
102
+ >
103
+ {#if isGenerating}
104
+ <div class="w-3.5 h-3.5 border-2 border-violet-400/30 border-t-violet-500 rounded-full animate-spin"></div>
105
+ {:else}
106
+ <Icon name="lucide:sparkles" class="w-3.5 h-3.5" />
107
+ {/if}
108
+ </button>
109
+ </div>
60
110
  <button
61
111
  type="button"
62
112
  class="flex items-center justify-center gap-1.5 w-full py-1.5 px-3 rounded-md text-xs font-medium transition-all duration-150
@@ -219,7 +219,7 @@
219
219
  <!-- Content Area -->
220
220
  <main class="flex-1 flex flex-col min-w-0 overflow-hidden">
221
221
  <div class="flex-1 overflow-y-auto p-4 md:p-5">
222
- {#if activeSection === 'model'}
222
+ {#if activeSection === 'models'}
223
223
  <div in:fly={{ x: 20, duration: 200 }}>
224
224
  <ModelSettings />
225
225
  </div>
@@ -23,7 +23,7 @@
23
23
  <div class="flex-1 overflow-auto">
24
24
  <div class="space-y-6">
25
25
 
26
- <!-- Model Configuration -->
26
+ <!-- AI Model (Assistant + Commit Message) -->
27
27
  <ModelSettings />
28
28
 
29
29
  <!-- Appearance Configuration -->
@@ -383,9 +383,9 @@
383
383
 
384
384
  <div class="space-y-6">
385
385
  <!-- Header -->
386
- <h3 class="text-base font-bold text-slate-900 dark:text-slate-100 mb-1.5">AI Engine</h3>
386
+ <h3 class="text-base font-bold text-slate-900 dark:text-slate-100 mb-1.5">Engines</h3>
387
387
  <p class="text-sm text-slate-600 dark:text-slate-500 mb-4">
388
- Manage AI engine installations and accounts
388
+ Manage engine installations and accounts
389
389
  </p>
390
390
 
391
391
  <!-- Claude Code Card -->
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { systemSettings, updateSystemSettings } from '$frontend/stores/features/settings.svelte';
3
- import { updateState, checkForUpdate, runUpdate } from '$frontend/stores/ui/update.svelte';
3
+ import { updateState, checkForUpdate, runUpdate, showRestartModal } from '$frontend/stores/ui/update.svelte';
4
4
  import Icon from '../../common/display/Icon.svelte';
5
5
 
6
6
  function toggleAutoUpdate() {
@@ -88,10 +88,17 @@
88
88
  </div>
89
89
  {/if}
90
90
 
91
- {#if updateState.updateSuccess}
91
+ {#if updateState.updateSuccess || updateState.pendingRestart}
92
92
  <div class="mt-3 flex items-center gap-2 px-3 py-2 bg-emerald-500/10 border border-emerald-500/20 rounded-lg">
93
93
  <Icon name="lucide:circle-check" class="w-4 h-4 text-emerald-500 shrink-0" />
94
- <span class="text-xs text-emerald-600 dark:text-emerald-400">Updated successfully — restart clopen to apply</span>
94
+ <span class="text-xs text-emerald-600 dark:text-emerald-400">Updated to v{updateState.latestVersion} — restart required</span>
95
+ <button
96
+ type="button"
97
+ onclick={() => showRestartModal()}
98
+ class="text-xs font-semibold text-emerald-600 dark:text-emerald-400 underline underline-offset-2 hover:text-emerald-700 dark:hover:text-emerald-300 transition-colors"
99
+ >
100
+ How to restart
101
+ </button>
95
102
  </div>
96
103
  {/if}
97
104
  </div>