@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.
- package/backend/engine/adapters/claude/stream.ts +107 -0
- package/backend/engine/adapters/opencode/stream.ts +81 -1
- package/backend/engine/types.ts +17 -0
- package/backend/git/git-service.ts +2 -1
- package/backend/ws/git/commit-message.ts +108 -0
- package/backend/ws/git/index.ts +3 -1
- package/backend/ws/system/index.ts +7 -1
- package/backend/ws/system/operations.ts +28 -2
- package/frontend/App.svelte +3 -0
- package/frontend/components/auth/SetupPage.svelte +2 -2
- package/frontend/components/chat/input/ChatInput.svelte +1 -1
- package/frontend/components/chat/message/ChatMessage.svelte +64 -16
- 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 +17 -6
- package/frontend/components/git/BranchManager.svelte +143 -155
- package/frontend/components/git/CommitForm.svelte +61 -11
- package/frontend/components/settings/SettingsModal.svelte +1 -1
- package/frontend/components/settings/SettingsView.svelte +1 -1
- 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/GitPanel.svelte +12 -5
- package/frontend/main.ts +4 -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 +45 -4
- package/package.json +1 -1
- package/shared/types/git.ts +15 -0
- package/shared/types/stores/settings.ts +12 -0
|
@@ -1,36 +1,44 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import Icon from '$frontend/components/common/display/Icon.svelte';
|
|
2
3
|
import { settings, updateSettings } from '$frontend/stores/features/settings.svelte';
|
|
3
4
|
import { modelStore } from '$frontend/stores/features/models.svelte';
|
|
4
|
-
import { ENGINES
|
|
5
|
-
import type {
|
|
5
|
+
import { ENGINES } from '$shared/constants/engines';
|
|
6
|
+
import type { EngineType } from '$shared/types/engine';
|
|
7
|
+
import type { CommitMessageFormat } from '$shared/types/git';
|
|
8
|
+
import type { IconName } from '$shared/types/ui/icons';
|
|
9
|
+
import EngineModelPicker from './EngineModelPicker.svelte';
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
let refreshing = $state(false);
|
|
9
|
-
let collapsedProviders = $state<Set<string>>(new Set());
|
|
11
|
+
type Tab = 'assistant' | 'commit-message';
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
const tabs: { id: Tab; label: string; icon: IconName }[] = [
|
|
14
|
+
{ id: 'assistant', label: 'Assistant', icon: 'lucide:bot' },
|
|
15
|
+
{ id: 'commit-message', label: 'Commit Message', icon: 'lucide:git-branch' }
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
let activeTab = $state<Tab>('assistant');
|
|
19
|
+
|
|
20
|
+
// --- Assistant ---
|
|
21
|
+
|
|
22
|
+
function handleAssistantEngineChange(engineType: EngineType) {
|
|
13
23
|
updateSettings({ selectedEngine: engineType });
|
|
14
|
-
searchQuery = '';
|
|
15
24
|
|
|
16
|
-
// Restore remembered model for this engine
|
|
17
25
|
const memory = settings.engineModelMemory || {};
|
|
18
26
|
const remembered = memory[engineType];
|
|
19
27
|
|
|
20
28
|
if (engineType !== 'claude-code') {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
29
|
+
modelStore.fetchModels(engineType).then(models => {
|
|
30
|
+
const target = (remembered && models.find(m => m.id === remembered))
|
|
31
|
+
|| models.find(m => m.recommended)
|
|
32
|
+
|| models[0];
|
|
33
|
+
if (target) {
|
|
34
|
+
updateSettings({
|
|
35
|
+
selectedModel: target.id,
|
|
36
|
+
engineModelMemory: { ...memory, [engineType]: target.id }
|
|
37
|
+
});
|
|
38
|
+
} else {
|
|
39
|
+
updateSettings({ selectedModel: '' });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
34
42
|
} else {
|
|
35
43
|
const models = modelStore.getByEngine('claude-code');
|
|
36
44
|
const target = (remembered && models.find(m => m.id === remembered))
|
|
@@ -42,17 +50,12 @@
|
|
|
42
50
|
engineModelMemory: { ...memory, [engineType]: target.id }
|
|
43
51
|
});
|
|
44
52
|
} else {
|
|
45
|
-
// No models available — clear the model selection
|
|
46
53
|
updateSettings({ selectedModel: '' });
|
|
47
54
|
}
|
|
48
55
|
}
|
|
49
|
-
|
|
50
|
-
// Open accordion for the selected model's provider
|
|
51
|
-
syncAccordionState();
|
|
52
56
|
}
|
|
53
57
|
|
|
54
|
-
|
|
55
|
-
function selectModel(modelId: string) {
|
|
58
|
+
function handleAssistantModelChange(modelId: string) {
|
|
56
59
|
const memory = settings.engineModelMemory || {};
|
|
57
60
|
updateSettings({
|
|
58
61
|
selectedModel: modelId,
|
|
@@ -60,298 +63,178 @@
|
|
|
60
63
|
});
|
|
61
64
|
}
|
|
62
65
|
|
|
63
|
-
//
|
|
64
|
-
async function handleRefresh() {
|
|
65
|
-
refreshing = true;
|
|
66
|
-
try {
|
|
67
|
-
await modelStore.refreshModels(settings.selectedEngine);
|
|
68
|
-
} finally {
|
|
69
|
-
refreshing = false;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
66
|
+
// --- Commit Message ---
|
|
72
67
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
next.delete(provider);
|
|
78
|
-
} else {
|
|
79
|
-
next.add(provider);
|
|
80
|
-
}
|
|
81
|
-
collapsedProviders = next;
|
|
82
|
-
}
|
|
68
|
+
const formatOptions: { id: CommitMessageFormat; label: string; desc: string; icon: IconName }[] = [
|
|
69
|
+
{ id: 'single-line', label: 'Single Line', desc: 'type(scope): subject', icon: 'lucide:minus' },
|
|
70
|
+
{ id: 'multi-line', label: 'Multi Line', desc: 'Subject + body', icon: 'lucide:align-left' }
|
|
71
|
+
];
|
|
83
72
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const allProviders = [...groupedModels.keys()];
|
|
87
|
-
const selectedModel = settings.selectedModel;
|
|
88
|
-
let selectedProvider: string | null = null;
|
|
73
|
+
const commitGen = $derived(settings.commitGenerator);
|
|
74
|
+
const useCustomModel = $derived(commitGen.useCustomModel);
|
|
89
75
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
76
|
+
// Resolve which model is being used for display
|
|
77
|
+
const activeEngine = $derived(useCustomModel ? commitGen.engine : settings.selectedEngine);
|
|
78
|
+
const activeModel = $derived(useCustomModel ? commitGen.model : settings.selectedModel);
|
|
79
|
+
const activeEngineMeta = $derived(ENGINES.find(e => e.type === activeEngine));
|
|
80
|
+
const activeModelMeta = $derived(modelStore.getById(activeModel));
|
|
96
81
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
collapsedProviders = collapsed;
|
|
82
|
+
function toggleCustomModel() {
|
|
83
|
+
updateSettings({
|
|
84
|
+
commitGenerator: { ...commitGen, useCustomModel: !useCustomModel }
|
|
85
|
+
});
|
|
103
86
|
}
|
|
104
87
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
// Group models by provider
|
|
120
|
-
const groupedModels = $derived.by(() => {
|
|
121
|
-
const groups = new Map<string, EngineModel[]>();
|
|
122
|
-
for (const model of filteredModels) {
|
|
123
|
-
const key = model.provider;
|
|
124
|
-
if (!groups.has(key)) groups.set(key, []);
|
|
125
|
-
groups.get(key)!.push(model);
|
|
126
|
-
}
|
|
127
|
-
return groups;
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
// Fetch models on mount for non-claude-code, then sync accordion
|
|
131
|
-
$effect(() => {
|
|
132
|
-
if (settings.selectedEngine !== 'claude-code') {
|
|
133
|
-
modelStore.fetchModels(settings.selectedEngine);
|
|
134
|
-
}
|
|
135
|
-
});
|
|
88
|
+
function handleCommitEngineChange(engineType: EngineType) {
|
|
89
|
+
const models = modelStore.getByEngine(engineType);
|
|
90
|
+
const defaultModel = engineType === 'claude-code'
|
|
91
|
+
? 'claude-code:haiku'
|
|
92
|
+
: (models[0]?.id || '');
|
|
93
|
+
updateSettings({
|
|
94
|
+
commitGenerator: {
|
|
95
|
+
...commitGen,
|
|
96
|
+
engine: engineType,
|
|
97
|
+
model: defaultModel
|
|
98
|
+
}
|
|
99
|
+
});
|
|
136
100
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
101
|
+
if (engineType !== 'claude-code') {
|
|
102
|
+
modelStore.fetchModels(engineType).then(fetched => {
|
|
103
|
+
if (fetched.length > 0) {
|
|
104
|
+
updateSettings({
|
|
105
|
+
commitGenerator: { ...settings.commitGenerator, model: fetched[0].id }
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
});
|
|
145
109
|
}
|
|
146
|
-
}
|
|
110
|
+
}
|
|
147
111
|
|
|
148
|
-
function
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
112
|
+
function handleCommitModelChange(modelId: string) {
|
|
113
|
+
updateSettings({
|
|
114
|
+
commitGenerator: { ...commitGen, model: modelId }
|
|
115
|
+
});
|
|
152
116
|
}
|
|
153
117
|
|
|
154
|
-
function
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
.join(' ');
|
|
118
|
+
function selectFormat(format: CommitMessageFormat) {
|
|
119
|
+
updateSettings({
|
|
120
|
+
commitGenerator: { ...commitGen, format }
|
|
121
|
+
});
|
|
159
122
|
}
|
|
160
123
|
</script>
|
|
161
124
|
|
|
162
|
-
<!-- Claude SVG logo (Anthropic) -->
|
|
163
|
-
{#snippet claudeLogo(active: boolean)}
|
|
164
|
-
<svg viewBox="0 0 24 24" fill="none" class="w-5 h-5" aria-hidden="true">
|
|
165
|
-
<path d="M16.091 4L9.115 20h-1.19L14.901 4h1.19zm-5.726 5.2L14.8 20h-1.218L9.2 9.6l1.165-.4z"
|
|
166
|
-
fill={active ? '#8b5cf6' : 'currentColor'} />
|
|
167
|
-
</svg>
|
|
168
|
-
{/snippet}
|
|
169
|
-
|
|
170
|
-
<!-- OpenCode SVG logo -->
|
|
171
|
-
{#snippet opencodeLogo(active: boolean)}
|
|
172
|
-
<svg viewBox="0 0 24 24" fill="none" class="w-5 h-5" aria-hidden="true">
|
|
173
|
-
<path d="M8.5 6L3 12l5.5 6M15.5 6L21 12l-5.5 6M13.5 4l-3 16"
|
|
174
|
-
stroke={active ? '#8b5cf6' : 'currentColor'}
|
|
175
|
-
stroke-width="2"
|
|
176
|
-
stroke-linecap="round"
|
|
177
|
-
stroke-linejoin="round" />
|
|
178
|
-
</svg>
|
|
179
|
-
{/snippet}
|
|
180
|
-
|
|
181
125
|
<div class="py-1">
|
|
182
|
-
<!--
|
|
183
|
-
<
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
</p>
|
|
187
|
-
|
|
188
|
-
<div class="flex gap-3 mb-6">
|
|
189
|
-
{#each ENGINES as engine (engine.type)}
|
|
190
|
-
{@const isActive = settings.selectedEngine === engine.type}
|
|
126
|
+
<!-- Tab Switcher -->
|
|
127
|
+
<div class="flex gap-1 p-1 mb-5 bg-slate-100 dark:bg-slate-800/60 rounded-lg">
|
|
128
|
+
{#each tabs as tab (tab.id)}
|
|
129
|
+
{@const isActive = activeTab === tab.id}
|
|
191
130
|
<button
|
|
192
131
|
type="button"
|
|
193
|
-
class="flex-1 flex items-center
|
|
132
|
+
class="flex-1 flex items-center justify-center gap-2 py-2 px-3 text-sm font-medium rounded-md transition-all duration-200 cursor-pointer
|
|
194
133
|
{isActive
|
|
195
|
-
? '
|
|
196
|
-
: '
|
|
197
|
-
onclick={() =>
|
|
134
|
+
? 'bg-white dark:bg-slate-700 text-slate-900 dark:text-slate-100 shadow-sm'
|
|
135
|
+
: 'text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-300'}"
|
|
136
|
+
onclick={() => activeTab = tab.id}
|
|
198
137
|
>
|
|
199
|
-
<
|
|
200
|
-
|
|
201
|
-
<div class="hidden dark:flex items-center justify-center w-5 h-5">{@html engine.icon.dark}</div>
|
|
202
|
-
</div>
|
|
203
|
-
<div>
|
|
204
|
-
<div class="font-bold text-sm text-slate-900 dark:text-slate-100">{engine.name}</div>
|
|
205
|
-
<div class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">{engine.description}</div>
|
|
206
|
-
</div>
|
|
207
|
-
{#if isActive}
|
|
208
|
-
<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">
|
|
209
|
-
<svg viewBox="0 0 24 24" fill="none" class="w-3 h-3" aria-hidden="true">
|
|
210
|
-
<path d="M5 13l4 4L19 7" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" />
|
|
211
|
-
</svg>
|
|
212
|
-
</div>
|
|
213
|
-
{/if}
|
|
138
|
+
<Icon name={tab.icon} class="w-4 h-4 {isActive ? 'text-violet-600' : ''}" />
|
|
139
|
+
{tab.label}
|
|
214
140
|
</button>
|
|
215
141
|
{/each}
|
|
216
142
|
</div>
|
|
217
143
|
|
|
218
|
-
<!--
|
|
219
|
-
|
|
220
|
-
<
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
<svg viewBox="0 0 24 24" fill="none" class="w-3.5 h-3.5 {refreshing ? 'animate-spin' : ''}" aria-hidden="true">
|
|
230
|
-
<path d="M21 12a9 9 0 11-2.636-6.364M21 3v5h-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
|
231
|
-
</svg>
|
|
232
|
-
{refreshing ? 'Refreshing...' : 'Refresh'}
|
|
233
|
-
</button>
|
|
234
|
-
</div>
|
|
235
|
-
<p class="text-sm text-slate-600 dark:text-slate-500 mb-3">
|
|
236
|
-
Select the AI model for the {ENGINES.find(e => e.type === settings.selectedEngine)?.name || 'selected'} engine
|
|
237
|
-
</p>
|
|
238
|
-
|
|
239
|
-
<!-- Search -->
|
|
240
|
-
<div class="relative mb-3">
|
|
241
|
-
<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">
|
|
242
|
-
<circle cx="11" cy="11" r="7" stroke="currentColor" stroke-width="2" />
|
|
243
|
-
<path d="M21 21l-4.35-4.35" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
|
244
|
-
</svg>
|
|
245
|
-
<input
|
|
246
|
-
type="text"
|
|
247
|
-
bind:value={searchQuery}
|
|
248
|
-
placeholder="Search models..."
|
|
249
|
-
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"
|
|
144
|
+
<!-- ===== ASSISTANT TAB ===== -->
|
|
145
|
+
{#if activeTab === 'assistant'}
|
|
146
|
+
<p class="text-sm text-slate-600 dark:text-slate-500 mb-4">
|
|
147
|
+
Configure the engine and model for chat
|
|
148
|
+
</p>
|
|
149
|
+
|
|
150
|
+
<EngineModelPicker
|
|
151
|
+
engine={settings.selectedEngine}
|
|
152
|
+
model={settings.selectedModel}
|
|
153
|
+
onEngineChange={handleAssistantEngineChange}
|
|
154
|
+
onModelChange={handleAssistantModelChange}
|
|
250
155
|
/>
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
<!--
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
<div class="w-4 h-4 rounded-full bg-slate-200/80 dark:bg-slate-700/60 animate-pulse"></div>
|
|
266
|
-
<div class="flex-1 space-y-1.5">
|
|
267
|
-
<div class="h-3.5 w-40 rounded bg-slate-200/80 dark:bg-slate-700/60 animate-pulse"></div>
|
|
268
|
-
<div class="flex gap-1.5">
|
|
269
|
-
<div class="h-3 w-14 rounded bg-slate-200/60 dark:bg-slate-700/40 animate-pulse"></div>
|
|
270
|
-
<div class="h-3 w-12 rounded bg-slate-200/60 dark:bg-slate-700/40 animate-pulse"></div>
|
|
271
|
-
</div>
|
|
272
|
-
</div>
|
|
273
|
-
</div>
|
|
274
|
-
{/each}
|
|
275
|
-
</div>
|
|
276
|
-
</div>
|
|
277
|
-
{:else if filteredModels.length === 0}
|
|
278
|
-
<div class="py-4 text-sm text-slate-500 text-center">
|
|
279
|
-
{searchQuery ? 'No models matching your search.' : 'No models available for this engine.'}
|
|
280
|
-
</div>
|
|
281
|
-
{:else}
|
|
282
|
-
<!-- Grouped by provider with accordion -->
|
|
283
|
-
{#each [...groupedModels.entries()] as [provider, providerModels] (provider)}
|
|
284
|
-
{@const isCollapsed = collapsedProviders.has(provider)}
|
|
285
|
-
{@const hasSelectedModel = providerModels.some(m => m.id === settings.selectedModel)}
|
|
286
|
-
<div class="border border-slate-200/80 dark:border-slate-700/50 rounded-lg overflow-hidden">
|
|
287
|
-
<!-- Accordion header -->
|
|
156
|
+
{/if}
|
|
157
|
+
|
|
158
|
+
<!-- ===== COMMIT MESSAGE TAB ===== -->
|
|
159
|
+
{#if activeTab === 'commit-message'}
|
|
160
|
+
<p class="text-sm text-slate-600 dark:text-slate-500 mb-4">
|
|
161
|
+
Configure the engine, model, and format for commits
|
|
162
|
+
</p>
|
|
163
|
+
|
|
164
|
+
<!-- Format Selection -->
|
|
165
|
+
<div class="mb-5">
|
|
166
|
+
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2">Message Format</label>
|
|
167
|
+
<div class="flex gap-2">
|
|
168
|
+
{#each formatOptions as fmt (fmt.id)}
|
|
169
|
+
{@const isActive = commitGen.format === fmt.id}
|
|
288
170
|
<button
|
|
289
171
|
type="button"
|
|
290
|
-
class="flex items-center gap-2.5
|
|
291
|
-
|
|
292
|
-
|
|
172
|
+
class="flex-1 flex items-center gap-2.5 p-3 border-2 rounded-xl text-left cursor-pointer transition-all duration-200
|
|
173
|
+
{isActive
|
|
174
|
+
? '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'
|
|
175
|
+
: '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'}"
|
|
176
|
+
onclick={() => selectFormat(fmt.id)}
|
|
293
177
|
>
|
|
294
|
-
<
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
</svg>
|
|
300
|
-
<span class="text-sm font-semibold text-slate-800 dark:text-slate-200">
|
|
301
|
-
{formatProvider(provider)}
|
|
302
|
-
</span>
|
|
303
|
-
<span class="text-xs text-slate-400 dark:text-slate-500">
|
|
304
|
-
{providerModels.length} {providerModels.length === 1 ? 'model' : 'models'}
|
|
305
|
-
</span>
|
|
306
|
-
{#if hasSelectedModel}
|
|
307
|
-
<div class="w-1.5 h-1.5 rounded-full bg-violet-500 ml-auto flex-shrink-0"></div>
|
|
308
|
-
{/if}
|
|
178
|
+
<Icon name={fmt.icon} class="w-4 h-4 {isActive ? 'text-violet-600' : 'text-slate-400'}" />
|
|
179
|
+
<div>
|
|
180
|
+
<div class="text-sm font-medium text-slate-900 dark:text-slate-100">{fmt.label}</div>
|
|
181
|
+
<div class="text-xs text-slate-500 dark:text-slate-400 font-mono">{fmt.desc}</div>
|
|
182
|
+
</div>
|
|
309
183
|
</button>
|
|
184
|
+
{/each}
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
310
187
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
{#if caps.length > 0}
|
|
340
|
-
<div class="flex flex-wrap gap-1 mt-1.5">
|
|
341
|
-
{#each caps as cap}
|
|
342
|
-
<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">
|
|
343
|
-
{cap}
|
|
344
|
-
</span>
|
|
345
|
-
{/each}
|
|
346
|
-
</div>
|
|
347
|
-
{/if}
|
|
348
|
-
</div>
|
|
349
|
-
</button>
|
|
350
|
-
{/each}
|
|
188
|
+
<!-- Custom Model Toggle -->
|
|
189
|
+
<div class="mb-5">
|
|
190
|
+
<button
|
|
191
|
+
type="button"
|
|
192
|
+
class="flex items-center gap-3 w-full text-left"
|
|
193
|
+
onclick={toggleCustomModel}
|
|
194
|
+
>
|
|
195
|
+
<div class="relative w-9 h-5 rounded-full transition-colors duration-200 flex-shrink-0
|
|
196
|
+
{useCustomModel ? 'bg-violet-600' : 'bg-slate-300 dark:bg-slate-600'}">
|
|
197
|
+
<div class="absolute top-0.5 w-4 h-4 rounded-full bg-white shadow-sm transition-transform duration-200
|
|
198
|
+
{useCustomModel ? 'translate-x-4.5' : 'translate-x-0.5'}"></div>
|
|
199
|
+
</div>
|
|
200
|
+
<div>
|
|
201
|
+
<span class="text-sm font-medium text-slate-900 dark:text-slate-100">Use custom model</span>
|
|
202
|
+
<p class="text-xs text-slate-500 dark:text-slate-400">Use a different engine and model instead of the assistant model</p>
|
|
203
|
+
</div>
|
|
204
|
+
</button>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<!-- Current Model Info (hidden when custom model is active) -->
|
|
208
|
+
{#if !useCustomModel}
|
|
209
|
+
<div class="mb-2">
|
|
210
|
+
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2">Model</label>
|
|
211
|
+
<div class="flex items-center gap-3 px-3.5 py-2.5 border border-slate-200 dark:border-slate-700 rounded-lg bg-slate-50 dark:bg-slate-800/50">
|
|
212
|
+
{#if activeEngineMeta}
|
|
213
|
+
<div class="flex-shrink-0">
|
|
214
|
+
<div class="flex dark:hidden items-center justify-center w-4 h-4">{@html activeEngineMeta.icon.light}</div>
|
|
215
|
+
<div class="hidden dark:flex items-center justify-center w-4 h-4">{@html activeEngineMeta.icon.dark}</div>
|
|
351
216
|
</div>
|
|
352
217
|
{/if}
|
|
218
|
+
<div class="flex-1 min-w-0">
|
|
219
|
+
<span class="text-sm font-medium text-slate-900 dark:text-slate-100">
|
|
220
|
+
{activeModelMeta?.name || activeModel}
|
|
221
|
+
</span>
|
|
222
|
+
<span class="text-xs text-slate-500 dark:text-slate-400 ml-1.5">(same as assistant)</span>
|
|
223
|
+
</div>
|
|
353
224
|
</div>
|
|
354
|
-
|
|
225
|
+
</div>
|
|
355
226
|
{/if}
|
|
356
|
-
|
|
227
|
+
|
|
228
|
+
<!-- Custom Engine & Model Selection (only when toggled on) -->
|
|
229
|
+
{#if useCustomModel}
|
|
230
|
+
<div class="mb-2">
|
|
231
|
+
<EngineModelPicker
|
|
232
|
+
engine={commitGen.engine}
|
|
233
|
+
model={commitGen.model}
|
|
234
|
+
onEngineChange={handleCommitEngineChange}
|
|
235
|
+
onModelChange={handleCommitModelChange}
|
|
236
|
+
/>
|
|
237
|
+
</div>
|
|
238
|
+
{/if}
|
|
239
|
+
{/if}
|
|
357
240
|
</div>
|
|
@@ -548,7 +548,7 @@
|
|
|
548
548
|
{@const gitRemotes = gitPanelRef?.panelActions?.getRemotes() || []}
|
|
549
549
|
|
|
550
550
|
<!-- Remote selector -->
|
|
551
|
-
{#if hasRemotes}
|
|
551
|
+
{#if hasRemotes && gitRemotes.length > 1}
|
|
552
552
|
<div class="relative">
|
|
553
553
|
<button
|
|
554
554
|
type="button"
|
|
@@ -586,8 +586,6 @@
|
|
|
586
586
|
</div>
|
|
587
587
|
{/if}
|
|
588
588
|
</div>
|
|
589
|
-
{:else if gitPanelRef?.panelActions?.getIsRepo()}
|
|
590
|
-
<span class="text-xs text-slate-400 italic px-1">no remote</span>
|
|
591
589
|
{/if}
|
|
592
590
|
|
|
593
591
|
<!-- Fetch -->
|
|
@@ -19,13 +19,14 @@
|
|
|
19
19
|
import ModalProvider from '$frontend/components/common/overlay/ModalProvider.svelte';
|
|
20
20
|
import SettingsModal from '$frontend/components/settings/SettingsModal.svelte';
|
|
21
21
|
import HistoryModal from '$frontend/components/history/HistoryModal.svelte';
|
|
22
|
+
import NotificationToast from '$frontend/components/common/feedback/NotificationToast.svelte';
|
|
22
23
|
|
|
23
24
|
// Services
|
|
24
25
|
import { initializeTheme } from '$frontend/utils/theme';
|
|
25
26
|
import { initializeStore } from '$frontend/stores/core/app.svelte';
|
|
26
27
|
import { initializeProjects } from '$frontend/stores/core/projects.svelte';
|
|
27
28
|
import { initializeSessions } from '$frontend/stores/core/sessions.svelte';
|
|
28
|
-
import { initializeNotifications } from '$frontend/stores/ui/notification.svelte';
|
|
29
|
+
import { initializeNotifications, notificationStore } from '$frontend/stores/ui/notification.svelte';
|
|
29
30
|
import { applyServerSettings, loadSystemSettings } from '$frontend/stores/features/settings.svelte';
|
|
30
31
|
import { initPresence } from '$frontend/stores/core/presence.svelte';
|
|
31
32
|
import ws from '$frontend/utils/ws';
|
|
@@ -163,3 +164,12 @@
|
|
|
163
164
|
|
|
164
165
|
<!-- History Modal -->
|
|
165
166
|
<HistoryModal bind:isOpen={showHistoryModal} onClose={closeHistoryModal} />
|
|
167
|
+
|
|
168
|
+
<!-- Toast Notifications -->
|
|
169
|
+
{#if notificationStore.notifications.length > 0}
|
|
170
|
+
<div class="fixed top-4 right-4 z-[200] flex flex-col gap-2">
|
|
171
|
+
{#each notificationStore.notifications as notification (notification.id)}
|
|
172
|
+
<NotificationToast {notification} />
|
|
173
|
+
{/each}
|
|
174
|
+
</div>
|
|
175
|
+
{/if}
|
|
@@ -674,7 +674,7 @@
|
|
|
674
674
|
isPulling = true;
|
|
675
675
|
try {
|
|
676
676
|
const prevBehind = branchInfo?.behind ?? 0;
|
|
677
|
-
const result = await ws.http('git:pull', { projectId, remote: selectedRemote });
|
|
677
|
+
const result = await ws.http('git:pull', { projectId, remote: selectedRemote, branch: branchInfo?.current });
|
|
678
678
|
if (!result.success) {
|
|
679
679
|
if (result.message.includes('conflict')) {
|
|
680
680
|
await loadAll();
|
|
@@ -704,7 +704,7 @@
|
|
|
704
704
|
isPushing = true;
|
|
705
705
|
try {
|
|
706
706
|
const prevAhead = branchInfo?.ahead ?? 0;
|
|
707
|
-
const result = await ws.http('git:push', { projectId, remote: selectedRemote });
|
|
707
|
+
const result = await ws.http('git:push', { projectId, remote: selectedRemote, branch: branchInfo?.current });
|
|
708
708
|
if (!result.success) {
|
|
709
709
|
showError('Push Failed', result.message);
|
|
710
710
|
} else {
|
|
@@ -971,8 +971,14 @@
|
|
|
971
971
|
if (changeDebounce) clearTimeout(changeDebounce);
|
|
972
972
|
changeDebounce = setTimeout(async () => {
|
|
973
973
|
changeDebounce = null;
|
|
974
|
-
// Refresh git status
|
|
975
|
-
|
|
974
|
+
// Refresh git status and branches (branch switch also modifies working tree)
|
|
975
|
+
const prevBranch = branchInfo?.current;
|
|
976
|
+
await Promise.all([loadStatus(), loadBranches()]);
|
|
977
|
+
|
|
978
|
+
// If branch changed, also refresh remotes
|
|
979
|
+
if (branchInfo?.current !== prevBranch) {
|
|
980
|
+
loadRemotes();
|
|
981
|
+
}
|
|
976
982
|
|
|
977
983
|
// Refresh the active diff tab if currently viewing one
|
|
978
984
|
if (activeTab && !activeTab.isLoading && activeTab.section !== 'commit') {
|
|
@@ -1016,8 +1022,9 @@
|
|
|
1016
1022
|
const unsub = ws.on('git:changed', (payload: any) => {
|
|
1017
1023
|
if (payload.projectId !== projectId || !isRepo) return;
|
|
1018
1024
|
scheduleGitRefresh();
|
|
1019
|
-
//
|
|
1025
|
+
// Refresh branches and remotes in case of branch switch/create/delete
|
|
1020
1026
|
loadBranches();
|
|
1027
|
+
loadRemotes();
|
|
1021
1028
|
// Refresh log if it was already loaded (History tab was visited)
|
|
1022
1029
|
if (commits.length > 0) {
|
|
1023
1030
|
loadLog(true);
|
package/frontend/main.ts
CHANGED