@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.
- package/.dockerignore +5 -0
- package/.env.example +2 -5
- package/CONTRIBUTING.md +4 -0
- package/README.md +4 -2
- package/backend/database/queries/message-queries.ts +42 -0
- package/backend/database/utils/connection.ts +5 -5
- package/backend/engine/adapters/claude/environment.ts +3 -4
- package/backend/engine/adapters/claude/stream.ts +107 -0
- package/backend/engine/adapters/opencode/server.ts +7 -1
- package/backend/engine/adapters/opencode/stream.ts +81 -1
- package/backend/engine/types.ts +17 -0
- package/backend/git/git-executor.ts +2 -1
- package/backend/git/git-service.ts +2 -1
- package/backend/index.ts +10 -10
- package/backend/snapshot/blob-store.ts +2 -2
- package/backend/utils/env.ts +13 -15
- package/backend/utils/index.ts +4 -1
- package/backend/utils/paths.ts +11 -0
- package/backend/utils/port-utils.ts +19 -6
- package/backend/ws/git/commit-message.ts +108 -0
- package/backend/ws/git/index.ts +3 -1
- package/backend/ws/messages/crud.ts +52 -0
- package/backend/ws/system/index.ts +7 -1
- package/backend/ws/system/operations.ts +28 -2
- package/bin/clopen.ts +15 -15
- package/docker-compose.yml +31 -0
- package/frontend/App.svelte +3 -0
- package/frontend/components/auth/SetupPage.svelte +45 -13
- package/frontend/components/chat/input/ChatInput.svelte +1 -1
- package/frontend/components/chat/message/ChatMessage.svelte +64 -16
- package/frontend/components/chat/widgets/FloatingTodoList.svelte +124 -10
- package/frontend/components/checkpoint/ConflictResolutionModal.svelte +189 -0
- package/frontend/components/checkpoint/TimelineModal.svelte +7 -162
- package/frontend/components/common/feedback/RestartRequiredModal.svelte +53 -0
- package/frontend/components/common/feedback/UpdateBanner.svelte +19 -8
- package/frontend/components/git/BranchManager.svelte +143 -155
- package/frontend/components/git/CommitForm.svelte +61 -11
- package/frontend/components/history/HistoryModal.svelte +30 -78
- package/frontend/components/history/HistoryView.svelte +45 -92
- package/frontend/components/settings/SettingsModal.svelte +1 -1
- package/frontend/components/settings/SettingsView.svelte +1 -1
- package/frontend/components/settings/appearance/AppearanceSettings.svelte +2 -2
- package/frontend/components/settings/engines/AIEnginesSettings.svelte +2 -2
- package/frontend/components/settings/general/UpdateSettings.svelte +10 -3
- package/frontend/components/settings/git/GitSettings.svelte +392 -0
- package/frontend/components/settings/model/EngineModelPicker.svelte +275 -0
- package/frontend/components/settings/model/ModelSettings.svelte +172 -289
- package/frontend/components/workspace/PanelHeader.svelte +1 -3
- package/frontend/components/workspace/WorkspaceLayout.svelte +11 -1
- package/frontend/components/workspace/panels/FilesPanel.svelte +41 -3
- package/frontend/components/workspace/panels/GitPanel.svelte +53 -8
- package/frontend/main.ts +4 -0
- package/frontend/stores/features/auth.svelte.ts +28 -0
- package/frontend/stores/features/settings.svelte.ts +13 -2
- package/frontend/stores/ui/settings-modal.svelte.ts +9 -9
- package/frontend/stores/ui/update.svelte.ts +51 -4
- package/package.json +2 -2
- package/scripts/dev.ts +3 -2
- package/scripts/start.ts +24 -0
- package/shared/types/git.ts +15 -0
- package/shared/types/stores/settings.ts +12 -0
- 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>
|