@myrialabs/clopen 0.2.3 → 0.2.5
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/message-converter.ts +37 -2
- 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/backend/ws/user/crud.ts +6 -3
- 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/chat/tools/components/FileHeader.svelte +19 -5
- package/frontend/components/chat/widgets/FloatingTodoList.svelte +30 -26
- 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/common/media/MediaPreview.svelte +187 -0
- package/frontend/components/files/FileViewer.svelte +11 -143
- package/frontend/components/git/BranchManager.svelte +143 -155
- package/frontend/components/git/CommitForm.svelte +61 -11
- package/frontend/components/git/DiffViewer.svelte +50 -130
- package/frontend/components/git/FileChangeItem.svelte +22 -0
- 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/DesktopNavigator.svelte +27 -1
- package/frontend/components/workspace/PanelHeader.svelte +1 -3
- package/frontend/components/workspace/WorkspaceLayout.svelte +14 -2
- package/frontend/components/workspace/panels/FilesPanel.svelte +76 -1
- package/frontend/components/workspace/panels/GitPanel.svelte +84 -33
- package/frontend/main.ts +4 -0
- package/frontend/stores/core/files.svelte.ts +15 -1
- package/frontend/stores/features/settings.svelte.ts +13 -2
- package/frontend/stores/ui/settings-modal.svelte.ts +9 -9
- package/frontend/stores/ui/todo-panel.svelte.ts +39 -0
- package/frontend/stores/ui/update.svelte.ts +45 -4
- package/frontend/utils/file-type.ts +68 -0
- package/index.html +1 -0
- package/package.json +1 -1
- package/shared/constants/binary-extensions.ts +40 -0
- package/shared/types/git.ts +15 -0
- package/shared/types/messaging/tool.ts +1 -0
- package/shared/types/stores/settings.ts +12 -0
- package/shared/utils/file-type-detection.ts +9 -1
- package/static/manifest.json +16 -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>
|
|
@@ -26,6 +26,9 @@
|
|
|
26
26
|
let projectToDelete = $state<Project | null>(null);
|
|
27
27
|
let searchQuery = $state('');
|
|
28
28
|
let showTunnelModal = $state(false);
|
|
29
|
+
let hoveredProject = $state<Project | null>(null);
|
|
30
|
+
let tooltipY = $state(0);
|
|
31
|
+
let tooltipX = $state(0);
|
|
29
32
|
|
|
30
33
|
// Derived
|
|
31
34
|
const isCollapsed = $derived(workspaceState.navigatorCollapsed);
|
|
@@ -145,6 +148,17 @@
|
|
|
145
148
|
// Single word: take first 2 letters
|
|
146
149
|
return name.substring(0, 2).toUpperCase();
|
|
147
150
|
}
|
|
151
|
+
|
|
152
|
+
function showProjectTooltip(project: Project, event: MouseEvent) {
|
|
153
|
+
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
|
|
154
|
+
tooltipX = rect.right + 8;
|
|
155
|
+
tooltipY = rect.top + rect.height / 2;
|
|
156
|
+
hoveredProject = project;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function hideProjectTooltip() {
|
|
160
|
+
hoveredProject = null;
|
|
161
|
+
}
|
|
148
162
|
</script>
|
|
149
163
|
|
|
150
164
|
<!-- Project Navigator Sidebar -->
|
|
@@ -315,7 +329,8 @@
|
|
|
315
329
|
? 'bg-violet-500/10 dark:bg-violet-500/20 text-violet-700 dark:text-violet-300'
|
|
316
330
|
: 'bg-slate-200/50 dark:bg-slate-800/50 text-slate-600 dark:text-slate-400 hover:bg-violet-500/10 hover:text-slate-900 dark:hover:text-slate-100'}"
|
|
317
331
|
onclick={() => selectProject(project)}
|
|
318
|
-
|
|
332
|
+
onmouseenter={(e) => showProjectTooltip(project, e)}
|
|
333
|
+
onmouseleave={hideProjectTooltip}
|
|
319
334
|
>
|
|
320
335
|
<span>{getProjectInitials(project.name)}</span>
|
|
321
336
|
<span
|
|
@@ -349,6 +364,17 @@
|
|
|
349
364
|
</nav>
|
|
350
365
|
</aside>
|
|
351
366
|
|
|
367
|
+
<!-- Collapsed project tooltip (fixed position to avoid overflow clipping) -->
|
|
368
|
+
{#if hoveredProject}
|
|
369
|
+
<div
|
|
370
|
+
class="fixed z-50 pointer-events-none flex flex-col py-1.5 px-2.5 rounded-lg bg-white dark:bg-slate-700 border border-slate-200 dark:border-slate-600 shadow-lg whitespace-nowrap"
|
|
371
|
+
style="left: {tooltipX}px; top: {tooltipY}px; transform: translateY(-50%);"
|
|
372
|
+
>
|
|
373
|
+
<span class="text-xs font-semibold text-slate-900 dark:text-slate-100">{hoveredProject.name}</span>
|
|
374
|
+
<span class="text-3xs font-mono text-slate-500 dark:text-slate-400">{hoveredProject.path}</span>
|
|
375
|
+
</div>
|
|
376
|
+
{/if}
|
|
377
|
+
|
|
352
378
|
<!-- Folder Browser (includes its own Modal) -->
|
|
353
379
|
<FolderBrowser
|
|
354
380
|
bind:isOpen={showFolderBrowser}
|
|
@@ -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,14 +19,16 @@
|
|
|
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';
|
|
31
|
+
import { applyTodoPanelState } from '$frontend/stores/ui/todo-panel.svelte';
|
|
30
32
|
import { initPresence } from '$frontend/stores/core/presence.svelte';
|
|
31
33
|
import ws from '$frontend/utils/ws';
|
|
32
34
|
import { debug } from '$shared/utils/logger';
|
|
@@ -83,7 +85,7 @@
|
|
|
83
85
|
|
|
84
86
|
// Step 3: Restore user state from server
|
|
85
87
|
setProgress(30, 'Restoring state...');
|
|
86
|
-
let serverState: { currentProjectId: string | null; lastView: string | null; settings: any; unreadSessions: any } | null = null;
|
|
88
|
+
let serverState: { currentProjectId: string | null; lastView: string | null; settings: any; unreadSessions: any; todoPanelState: any } | null = null;
|
|
87
89
|
try {
|
|
88
90
|
serverState = await ws.http('user:restore-state', {});
|
|
89
91
|
debug.log('workspace', 'Server state restored:', serverState);
|
|
@@ -96,6 +98,7 @@
|
|
|
96
98
|
if (serverState?.settings) {
|
|
97
99
|
applyServerSettings(serverState.settings);
|
|
98
100
|
}
|
|
101
|
+
applyTodoPanelState(serverState?.todoPanelState);
|
|
99
102
|
restoreLastView(serverState?.lastView);
|
|
100
103
|
restoreUnreadSessions(serverState?.unreadSessions);
|
|
101
104
|
await loadSystemSettings();
|
|
@@ -163,3 +166,12 @@
|
|
|
163
166
|
|
|
164
167
|
<!-- History Modal -->
|
|
165
168
|
<HistoryModal bind:isOpen={showHistoryModal} onClose={closeHistoryModal} />
|
|
169
|
+
|
|
170
|
+
<!-- Toast Notifications -->
|
|
171
|
+
{#if notificationStore.notifications.length > 0}
|
|
172
|
+
<div class="fixed top-4 right-4 z-[200] flex flex-col gap-2">
|
|
173
|
+
{#each notificationStore.notifications as notification (notification.id)}
|
|
174
|
+
<NotificationToast {notification} />
|
|
175
|
+
{/each}
|
|
176
|
+
</div>
|
|
177
|
+
{/if}
|