@myrialabs/clopen 0.2.2 → 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 (62) hide show
  1. package/.dockerignore +5 -0
  2. package/.env.example +2 -5
  3. package/CONTRIBUTING.md +4 -0
  4. package/README.md +4 -2
  5. package/backend/database/queries/message-queries.ts +42 -0
  6. package/backend/database/utils/connection.ts +5 -5
  7. package/backend/engine/adapters/claude/environment.ts +3 -4
  8. package/backend/engine/adapters/claude/stream.ts +107 -0
  9. package/backend/engine/adapters/opencode/server.ts +7 -1
  10. package/backend/engine/adapters/opencode/stream.ts +81 -1
  11. package/backend/engine/types.ts +17 -0
  12. package/backend/git/git-executor.ts +2 -1
  13. package/backend/git/git-service.ts +2 -1
  14. package/backend/index.ts +10 -10
  15. package/backend/snapshot/blob-store.ts +2 -2
  16. package/backend/utils/env.ts +13 -15
  17. package/backend/utils/index.ts +4 -1
  18. package/backend/utils/paths.ts +11 -0
  19. package/backend/utils/port-utils.ts +19 -6
  20. package/backend/ws/git/commit-message.ts +108 -0
  21. package/backend/ws/git/index.ts +3 -1
  22. package/backend/ws/messages/crud.ts +52 -0
  23. package/backend/ws/system/index.ts +7 -1
  24. package/backend/ws/system/operations.ts +28 -2
  25. package/bin/clopen.ts +15 -15
  26. package/docker-compose.yml +31 -0
  27. package/frontend/App.svelte +3 -0
  28. package/frontend/components/auth/SetupPage.svelte +45 -13
  29. package/frontend/components/chat/input/ChatInput.svelte +1 -1
  30. package/frontend/components/chat/message/ChatMessage.svelte +64 -16
  31. package/frontend/components/chat/widgets/FloatingTodoList.svelte +124 -10
  32. package/frontend/components/checkpoint/ConflictResolutionModal.svelte +189 -0
  33. package/frontend/components/checkpoint/TimelineModal.svelte +7 -162
  34. package/frontend/components/common/feedback/RestartRequiredModal.svelte +53 -0
  35. package/frontend/components/common/feedback/UpdateBanner.svelte +19 -8
  36. package/frontend/components/git/BranchManager.svelte +143 -155
  37. package/frontend/components/git/CommitForm.svelte +61 -11
  38. package/frontend/components/history/HistoryModal.svelte +30 -78
  39. package/frontend/components/history/HistoryView.svelte +45 -92
  40. package/frontend/components/settings/SettingsModal.svelte +1 -1
  41. package/frontend/components/settings/SettingsView.svelte +1 -1
  42. package/frontend/components/settings/appearance/AppearanceSettings.svelte +2 -2
  43. package/frontend/components/settings/engines/AIEnginesSettings.svelte +2 -2
  44. package/frontend/components/settings/general/UpdateSettings.svelte +10 -3
  45. package/frontend/components/settings/git/GitSettings.svelte +392 -0
  46. package/frontend/components/settings/model/EngineModelPicker.svelte +275 -0
  47. package/frontend/components/settings/model/ModelSettings.svelte +172 -289
  48. package/frontend/components/workspace/PanelHeader.svelte +1 -3
  49. package/frontend/components/workspace/WorkspaceLayout.svelte +11 -1
  50. package/frontend/components/workspace/panels/FilesPanel.svelte +41 -3
  51. package/frontend/components/workspace/panels/GitPanel.svelte +53 -8
  52. package/frontend/main.ts +4 -0
  53. package/frontend/stores/features/auth.svelte.ts +28 -0
  54. package/frontend/stores/features/settings.svelte.ts +13 -2
  55. package/frontend/stores/ui/settings-modal.svelte.ts +9 -9
  56. package/frontend/stores/ui/update.svelte.ts +51 -4
  57. package/package.json +2 -2
  58. package/scripts/dev.ts +3 -2
  59. package/scripts/start.ts +24 -0
  60. package/shared/types/git.ts +15 -0
  61. package/shared/types/stores/settings.ts +12 -0
  62. package/vite.config.ts +2 -2
@@ -0,0 +1,275 @@
1
+ <script lang="ts">
2
+ import { modelStore } from '$frontend/stores/features/models.svelte';
3
+ import { ENGINES } from '$shared/constants/engines';
4
+ import type { EngineType } from '$shared/types/engine';
5
+ import type { EngineModel } from '$shared/types/engine';
6
+
7
+ interface Props {
8
+ engine: EngineType;
9
+ model: string;
10
+ onEngineChange: (engine: EngineType) => void;
11
+ onModelChange: (modelId: string) => void;
12
+ }
13
+
14
+ const { engine, model, onEngineChange, onModelChange }: Props = $props();
15
+
16
+ let searchQuery = $state('');
17
+ let refreshing = $state(false);
18
+ let collapsedProviders = $state<Set<string>>(new Set());
19
+
20
+ // Models for the selected engine, filtered by search
21
+ const filteredModels = $derived.by(() => {
22
+ const models = modelStore.getByEngine(engine);
23
+ if (!searchQuery.trim()) return models;
24
+ const q = searchQuery.toLowerCase();
25
+ return models.filter(m =>
26
+ m.name.toLowerCase().includes(q) ||
27
+ m.modelId.toLowerCase().includes(q) ||
28
+ m.provider.toLowerCase().includes(q) ||
29
+ m.capabilities.some(c => c.toLowerCase().includes(q))
30
+ );
31
+ });
32
+
33
+ // Group models by provider
34
+ const groupedModels = $derived.by(() => {
35
+ const groups = new Map<string, EngineModel[]>();
36
+ for (const m of filteredModels) {
37
+ const key = m.provider;
38
+ if (!groups.has(key)) groups.set(key, []);
39
+ groups.get(key)!.push(m);
40
+ }
41
+ return groups;
42
+ });
43
+
44
+ // Fetch models when engine changes (for non-claude-code)
45
+ $effect(() => {
46
+ if (engine !== 'claude-code') {
47
+ modelStore.fetchModels(engine);
48
+ }
49
+ });
50
+
51
+ // Sync accordion state when search or models change
52
+ $effect(() => {
53
+ if (searchQuery.trim()) {
54
+ collapsedProviders = new Set();
55
+ } else if (groupedModels.size > 0) {
56
+ syncAccordionState();
57
+ }
58
+ });
59
+
60
+ function syncAccordionState() {
61
+ const allProviders = [...groupedModels.keys()];
62
+ let selectedProvider: string | null = null;
63
+ for (const [provider, models] of groupedModels) {
64
+ if (models.some(m => m.id === model)) {
65
+ selectedProvider = provider;
66
+ break;
67
+ }
68
+ }
69
+ const collapsed = new Set(allProviders);
70
+ if (selectedProvider) collapsed.delete(selectedProvider);
71
+ collapsedProviders = collapsed;
72
+ }
73
+
74
+ function toggleProvider(provider: string) {
75
+ const next = new Set(collapsedProviders);
76
+ if (next.has(provider)) next.delete(provider);
77
+ else next.add(provider);
78
+ collapsedProviders = next;
79
+ }
80
+
81
+ function formatProvider(provider: string): string {
82
+ return provider.split(/[-_]/).map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
83
+ }
84
+
85
+ async function handleEngineChange(engineType: EngineType) {
86
+ searchQuery = '';
87
+ onEngineChange(engineType);
88
+
89
+ if (engineType !== 'claude-code') {
90
+ await modelStore.fetchModels(engineType);
91
+ }
92
+
93
+ syncAccordionState();
94
+ }
95
+
96
+ async function handleRefresh() {
97
+ refreshing = true;
98
+ try {
99
+ await modelStore.refreshModels(engine);
100
+ } finally {
101
+ refreshing = false;
102
+ }
103
+ }
104
+ </script>
105
+
106
+ <!-- Engine Selection -->
107
+ <div class="mb-6">
108
+ <label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2">Engine</label>
109
+ <div class="flex gap-3">
110
+ {#each ENGINES as eng (eng.type)}
111
+ {@const isActive = engine === eng.type}
112
+ <button
113
+ type="button"
114
+ class="flex-1 flex items-center gap-3 p-3.5 overflow-hidden border-2 rounded-xl text-left cursor-pointer transition-all duration-200
115
+ {isActive
116
+ ? 'border-violet-600 bg-gradient-to-br from-violet-500/10 to-purple-500/5 dark:from-violet-500/12 dark:to-purple-500/8'
117
+ : 'border-slate-200 dark:border-slate-800 bg-slate-100/80 dark:bg-slate-800/80 hover:border-violet-500/20 dark:hover:border-violet-500/35'}"
118
+ onclick={() => handleEngineChange(eng.type)}
119
+ >
120
+ <div>
121
+ <div class="flex dark:hidden items-center justify-center w-5 h-5">{@html eng.icon.light}</div>
122
+ <div class="hidden dark:flex items-center justify-center w-5 h-5">{@html eng.icon.dark}</div>
123
+ </div>
124
+ <div>
125
+ <div class="font-bold text-sm text-slate-900 dark:text-slate-100">{eng.name}</div>
126
+ <div class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">{eng.description}</div>
127
+ </div>
128
+ {#if isActive}
129
+ <div class="flex items-center justify-center w-5 h-5 bg-gradient-to-br from-violet-600 to-purple-600 rounded-full text-white ml-auto flex-shrink-0">
130
+ <svg viewBox="0 0 24 24" fill="none" class="w-3 h-3" aria-hidden="true">
131
+ <path d="M5 13l4 4L19 7" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" />
132
+ </svg>
133
+ </div>
134
+ {/if}
135
+ </button>
136
+ {/each}
137
+ </div>
138
+ </div>
139
+
140
+ <!-- Model Selection -->
141
+ <div>
142
+ <div class="flex items-center justify-between mb-1.5">
143
+ <label class="text-sm font-semibold text-slate-700 dark:text-slate-300">Model</label>
144
+ <button
145
+ type="button"
146
+ class="flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium rounded-lg transition-colors cursor-pointer
147
+ text-slate-500 hover:text-violet-600 hover:bg-violet-500/10 dark:hover:text-violet-400 dark:hover:bg-violet-500/15
148
+ disabled:opacity-50 disabled:cursor-not-allowed"
149
+ onclick={handleRefresh}
150
+ disabled={refreshing || modelStore.loading}
151
+ >
152
+ <svg viewBox="0 0 24 24" fill="none" class="w-3.5 h-3.5 {refreshing ? 'animate-spin' : ''}" aria-hidden="true">
153
+ <path d="M21 12a9 9 0 11-2.636-6.364M21 3v5h-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
154
+ </svg>
155
+ {refreshing ? 'Refreshing...' : 'Refresh'}
156
+ </button>
157
+ </div>
158
+ <p class="text-sm text-slate-600 dark:text-slate-500 mb-3">
159
+ Select the model for the {ENGINES.find(e => e.type === engine)?.name || 'selected'} engine
160
+ </p>
161
+
162
+ <!-- Search -->
163
+ <div class="relative mb-3">
164
+ <svg viewBox="0 0 24 24" fill="none" class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 pointer-events-none" aria-hidden="true">
165
+ <circle cx="11" cy="11" r="7" stroke="currentColor" stroke-width="2" />
166
+ <path d="M21 21l-4.35-4.35" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
167
+ </svg>
168
+ <input
169
+ type="text"
170
+ bind:value={searchQuery}
171
+ placeholder="Search models..."
172
+ class="w-full pl-9 pr-3 py-2 text-sm bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg outline-none focus:ring-2 focus:ring-violet-500/20 focus:border-violet-600 transition-colors text-slate-900 dark:text-slate-100 placeholder-slate-400"
173
+ />
174
+ </div>
175
+
176
+ <!-- Model List -->
177
+ <div class="flex flex-col gap-1.5">
178
+ {#if modelStore.loading && engine !== 'claude-code' && !refreshing}
179
+ <!-- Loading skeleton -->
180
+ <div class="border border-slate-200/80 dark:border-slate-700/50 rounded-lg overflow-hidden">
181
+ <div class="bg-white/80 dark:bg-slate-800/40 px-3 py-3 flex items-center gap-3">
182
+ <div class="w-4 h-4 rounded-full bg-slate-200 dark:bg-slate-700 animate-pulse"></div>
183
+ <div class="h-3.5 w-32 rounded bg-slate-200 dark:bg-slate-700 animate-pulse"></div>
184
+ </div>
185
+ <div class="px-4 py-2.5 space-y-2.5">
186
+ {#each Array(3) as _}
187
+ <div class="flex items-center gap-3 py-2">
188
+ <div class="w-4 h-4 rounded-full bg-slate-200/80 dark:bg-slate-700/60 animate-pulse"></div>
189
+ <div class="flex-1 space-y-1.5">
190
+ <div class="h-3.5 w-40 rounded bg-slate-200/80 dark:bg-slate-700/60 animate-pulse"></div>
191
+ <div class="flex gap-1.5">
192
+ <div class="h-3 w-14 rounded bg-slate-200/60 dark:bg-slate-700/40 animate-pulse"></div>
193
+ <div class="h-3 w-12 rounded bg-slate-200/60 dark:bg-slate-700/40 animate-pulse"></div>
194
+ </div>
195
+ </div>
196
+ </div>
197
+ {/each}
198
+ </div>
199
+ </div>
200
+ {:else if filteredModels.length === 0}
201
+ <div class="py-4 text-sm text-slate-500 text-center">
202
+ {searchQuery ? 'No models matching your search.' : 'No models available for this engine.'}
203
+ </div>
204
+ {:else}
205
+ {#each [...groupedModels.entries()] as [provider, providerModels] (provider)}
206
+ {@const isCollapsed = collapsedProviders.has(provider)}
207
+ {@const hasSelectedModel = providerModels.some(m => m.id === model)}
208
+ <div class="border border-slate-200/80 dark:border-slate-700/50 rounded-lg overflow-hidden">
209
+ <!-- Accordion header -->
210
+ <button
211
+ type="button"
212
+ class="flex items-center gap-2.5 w-full px-3 py-2.5 text-left cursor-pointer transition-colors
213
+ bg-white/80 dark:bg-slate-800/40 hover:bg-white dark:hover:bg-slate-800/60"
214
+ onclick={() => toggleProvider(provider)}
215
+ >
216
+ <svg viewBox="0 0 24 24" fill="none"
217
+ class="w-4 h-4 text-slate-400 transition-transform duration-200 flex-shrink-0
218
+ {isCollapsed ? '' : 'rotate-90'}"
219
+ aria-hidden="true">
220
+ <path d="M9 18l6-6-6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
221
+ </svg>
222
+ <span class="text-sm font-semibold text-slate-800 dark:text-slate-200">
223
+ {formatProvider(provider)}
224
+ </span>
225
+ <span class="text-xs text-slate-400 dark:text-slate-500">
226
+ {providerModels.length} {providerModels.length === 1 ? 'model' : 'models'}
227
+ </span>
228
+ {#if hasSelectedModel}
229
+ <div class="w-1.5 h-1.5 rounded-full bg-violet-500 ml-auto flex-shrink-0"></div>
230
+ {/if}
231
+ </button>
232
+
233
+ <!-- Accordion body -->
234
+ {#if !isCollapsed}
235
+ <div class="flex flex-col bg-white/40 dark:bg-slate-800/20">
236
+ {#each providerModels as mdl (mdl.id)}
237
+ {@const isSelected = model === mdl.id}
238
+ {@const caps = mdl.capabilities}
239
+ <button
240
+ type="button"
241
+ class="flex items-start gap-3 px-3 py-2.5 text-left cursor-pointer transition-all duration-150
242
+ {isSelected
243
+ ? 'bg-violet-500/10 dark:bg-violet-500/12'
244
+ : 'hover:bg-slate-100/80 dark:hover:bg-slate-700/30'}"
245
+ onclick={() => onModelChange(mdl.id)}
246
+ >
247
+ <div class="flex-shrink-0 w-4 h-4 rounded-full border-2 flex items-center justify-center mt-0.5
248
+ {isSelected ? 'border-violet-600' : 'border-slate-300 dark:border-slate-600'}">
249
+ {#if isSelected}
250
+ <div class="w-2 h-2 rounded-full bg-violet-600"></div>
251
+ {/if}
252
+ </div>
253
+ <div class="flex-1 min-w-0">
254
+ <div class="flex items-center gap-2">
255
+ <span class="text-sm font-medium text-slate-900 dark:text-slate-100">{mdl.name}</span>
256
+ </div>
257
+ {#if caps.length > 0}
258
+ <div class="flex flex-wrap gap-1 mt-1.5">
259
+ {#each caps as cap}
260
+ <span class="px-1.5 py-0.5 text-2xs rounded bg-slate-100 dark:bg-slate-700/50 text-slate-500 dark:text-slate-400 leading-none">
261
+ {cap}
262
+ </span>
263
+ {/each}
264
+ </div>
265
+ {/if}
266
+ </div>
267
+ </button>
268
+ {/each}
269
+ </div>
270
+ {/if}
271
+ </div>
272
+ {/each}
273
+ {/if}
274
+ </div>
275
+ </div>