@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
|
@@ -129,6 +129,8 @@
|
|
|
129
129
|
// Container width for responsive layout (same threshold as Files: 800)
|
|
130
130
|
let containerRef = $state<HTMLDivElement | null>(null);
|
|
131
131
|
let containerWidth = $state(0);
|
|
132
|
+
let leftPanelWidth = $state(288); // default w-72
|
|
133
|
+
let isResizing = $state(false);
|
|
132
134
|
const TWO_COLUMN_THRESHOLD = $derived(Math.round(600 * (settings.fontSize / 13)));
|
|
133
135
|
const isTwoColumnMode = $derived(containerWidth >= TWO_COLUMN_THRESHOLD);
|
|
134
136
|
|
|
@@ -672,7 +674,7 @@
|
|
|
672
674
|
isPulling = true;
|
|
673
675
|
try {
|
|
674
676
|
const prevBehind = branchInfo?.behind ?? 0;
|
|
675
|
-
const result = await ws.http('git:pull', { projectId, remote: selectedRemote });
|
|
677
|
+
const result = await ws.http('git:pull', { projectId, remote: selectedRemote, branch: branchInfo?.current });
|
|
676
678
|
if (!result.success) {
|
|
677
679
|
if (result.message.includes('conflict')) {
|
|
678
680
|
await loadAll();
|
|
@@ -702,7 +704,7 @@
|
|
|
702
704
|
isPushing = true;
|
|
703
705
|
try {
|
|
704
706
|
const prevAhead = branchInfo?.ahead ?? 0;
|
|
705
|
-
const result = await ws.http('git:push', { projectId, remote: selectedRemote });
|
|
707
|
+
const result = await ws.http('git:push', { projectId, remote: selectedRemote, branch: branchInfo?.current });
|
|
706
708
|
if (!result.success) {
|
|
707
709
|
showError('Push Failed', result.message);
|
|
708
710
|
} else {
|
|
@@ -969,8 +971,14 @@
|
|
|
969
971
|
if (changeDebounce) clearTimeout(changeDebounce);
|
|
970
972
|
changeDebounce = setTimeout(async () => {
|
|
971
973
|
changeDebounce = null;
|
|
972
|
-
// Refresh git status
|
|
973
|
-
|
|
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
|
+
}
|
|
974
982
|
|
|
975
983
|
// Refresh the active diff tab if currently viewing one
|
|
976
984
|
if (activeTab && !activeTab.isLoading && activeTab.section !== 'commit') {
|
|
@@ -1014,8 +1022,9 @@
|
|
|
1014
1022
|
const unsub = ws.on('git:changed', (payload: any) => {
|
|
1015
1023
|
if (payload.projectId !== projectId || !isRepo) return;
|
|
1016
1024
|
scheduleGitRefresh();
|
|
1017
|
-
//
|
|
1025
|
+
// Refresh branches and remotes in case of branch switch/create/delete
|
|
1018
1026
|
loadBranches();
|
|
1027
|
+
loadRemotes();
|
|
1019
1028
|
// Refresh log if it was already loaded (History tab was visited)
|
|
1020
1029
|
if (commits.length > 0) {
|
|
1021
1030
|
loadLog(true);
|
|
@@ -1025,6 +1034,26 @@
|
|
|
1025
1034
|
return () => unsub();
|
|
1026
1035
|
});
|
|
1027
1036
|
|
|
1037
|
+
function startColumnResize(e: MouseEvent) {
|
|
1038
|
+
isResizing = true;
|
|
1039
|
+
const startX = e.clientX;
|
|
1040
|
+
const startWidth = leftPanelWidth;
|
|
1041
|
+
|
|
1042
|
+
function onMouseMove(e: MouseEvent) {
|
|
1043
|
+
const delta = e.clientX - startX;
|
|
1044
|
+
leftPanelWidth = Math.max(120, Math.min(startWidth + delta, containerWidth - 120));
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
function onMouseUp() {
|
|
1048
|
+
isResizing = false;
|
|
1049
|
+
window.removeEventListener('mousemove', onMouseMove);
|
|
1050
|
+
window.removeEventListener('mouseup', onMouseUp);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
window.addEventListener('mousemove', onMouseMove);
|
|
1054
|
+
window.addEventListener('mouseup', onMouseUp);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1028
1057
|
// Monitor container width
|
|
1029
1058
|
onMount(() => {
|
|
1030
1059
|
let resizeObserver: ResizeObserver | null = null;
|
|
@@ -1442,18 +1471,34 @@
|
|
|
1442
1471
|
{:else}
|
|
1443
1472
|
<div class="flex-1 overflow-hidden">
|
|
1444
1473
|
<!-- Unified layout: always render both panels to preserve state (like Files panel) -->
|
|
1445
|
-
<div class="h-full flex">
|
|
1474
|
+
<div class="h-full flex" class:select-none={isResizing} class:cursor-col-resize={isResizing}>
|
|
1446
1475
|
<!-- Left panel: Changes list -->
|
|
1447
1476
|
<div
|
|
1448
1477
|
class={isTwoColumnMode
|
|
1449
|
-
? '
|
|
1478
|
+
? 'flex-shrink-0 h-full overflow-hidden flex flex-col'
|
|
1450
1479
|
: (viewMode === 'list' ? 'w-full h-full overflow-hidden flex flex-col' : 'hidden')}
|
|
1480
|
+
style={isTwoColumnMode ? `width: ${leftPanelWidth}px` : undefined}
|
|
1451
1481
|
>
|
|
1452
1482
|
{@render viewTabBar()}
|
|
1453
1483
|
{@render changesList()}
|
|
1454
1484
|
</div>
|
|
1455
1485
|
|
|
1456
|
-
|
|
1486
|
+
{#if isTwoColumnMode}
|
|
1487
|
+
<!-- Column resize handle -->
|
|
1488
|
+
<div
|
|
1489
|
+
class="relative flex-shrink-0 h-full w-px cursor-col-resize group"
|
|
1490
|
+
role="separator"
|
|
1491
|
+
aria-orientation="vertical"
|
|
1492
|
+
onmousedown={startColumnResize}
|
|
1493
|
+
>
|
|
1494
|
+
<!-- Invisible extended hit area (6px each side) -->
|
|
1495
|
+
<div class="absolute inset-y-0 -left-1.5 -right-1.5 cursor-col-resize z-10"></div>
|
|
1496
|
+
<!-- Visual line: 1px default, expands to 4px on hover -->
|
|
1497
|
+
<div class="absolute inset-y-0 left-1/2 -translate-x-1/2 w-px group-hover:w-1 bg-slate-200 dark:bg-slate-700 group-hover:bg-blue-400 dark:group-hover:bg-blue-500 transition-all duration-150"></div>
|
|
1498
|
+
</div>
|
|
1499
|
+
{/if}
|
|
1500
|
+
|
|
1501
|
+
<!-- Right panel: Diff viewer -->
|
|
1457
1502
|
<div
|
|
1458
1503
|
class={isTwoColumnMode
|
|
1459
1504
|
? 'flex-1 h-full overflow-hidden flex flex-col'
|
package/frontend/main.ts
CHANGED
|
@@ -187,6 +187,34 @@ export const authStore = {
|
|
|
187
187
|
debug.log('auth', `No-auth setup complete: ${result.user.name}`);
|
|
188
188
|
},
|
|
189
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Switch to with-auth mode mid-wizard (e.g. user changed selection after refresh).
|
|
192
|
+
* Regenerates PAT for the existing no-auth admin and updates authMode setting.
|
|
193
|
+
*/
|
|
194
|
+
async switchToWithAuth() {
|
|
195
|
+
const { loadSystemSettings, updateSystemSettings } = await import('$frontend/stores/features/settings.svelte');
|
|
196
|
+
await loadSystemSettings();
|
|
197
|
+
await updateSystemSettings({ authMode: 'required' });
|
|
198
|
+
|
|
199
|
+
const result = await ws.http('auth:regenerate-pat', {});
|
|
200
|
+
personalAccessToken = result.personalAccessToken;
|
|
201
|
+
authMode = 'required';
|
|
202
|
+
debug.log('auth', 'Switched to with-auth mode, PAT regenerated');
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Switch to no-auth mode mid-wizard (e.g. user changed selection after refresh).
|
|
207
|
+
* Only updates the authMode setting; existing user remains unchanged.
|
|
208
|
+
*/
|
|
209
|
+
async switchToNoAuth() {
|
|
210
|
+
const { loadSystemSettings, updateSystemSettings } = await import('$frontend/stores/features/settings.svelte');
|
|
211
|
+
await loadSystemSettings();
|
|
212
|
+
await updateSystemSettings({ authMode: 'none' });
|
|
213
|
+
|
|
214
|
+
authMode = 'none';
|
|
215
|
+
debug.log('auth', 'Switched to no-auth mode');
|
|
216
|
+
},
|
|
217
|
+
|
|
190
218
|
/**
|
|
191
219
|
* Complete setup — transition to ready state after wizard is done.
|
|
192
220
|
* Saves onboardingComplete flag so wizard won't show again.
|
|
@@ -32,7 +32,13 @@ const defaultSettings: AppSettings = {
|
|
|
32
32
|
soundNotifications: true,
|
|
33
33
|
pushNotifications: false,
|
|
34
34
|
layoutPresetVisibility: createDefaultPresetVisibility(),
|
|
35
|
-
fontSize: 13
|
|
35
|
+
fontSize: 13,
|
|
36
|
+
commitGenerator: {
|
|
37
|
+
useCustomModel: false,
|
|
38
|
+
engine: 'claude-code',
|
|
39
|
+
model: 'claude-code:haiku',
|
|
40
|
+
format: 'single-line'
|
|
41
|
+
}
|
|
36
42
|
};
|
|
37
43
|
|
|
38
44
|
// Default system settings
|
|
@@ -63,7 +69,12 @@ export function applyFontSize(size: number): void {
|
|
|
63
69
|
export function applyServerSettings(serverSettings: Partial<AppSettings> | null): void {
|
|
64
70
|
if (serverSettings && typeof serverSettings === 'object') {
|
|
65
71
|
// Merge with defaults to ensure all properties exist
|
|
66
|
-
|
|
72
|
+
const merged = { ...defaultSettings, ...serverSettings };
|
|
73
|
+
// Deep merge nested objects so new default fields are preserved
|
|
74
|
+
if (serverSettings.commitGenerator) {
|
|
75
|
+
merged.commitGenerator = { ...defaultSettings.commitGenerator, ...serverSettings.commitGenerator };
|
|
76
|
+
}
|
|
77
|
+
Object.assign(settings, merged);
|
|
67
78
|
applyFontSize(settings.fontSize);
|
|
68
79
|
debug.log('settings', 'Applied server settings');
|
|
69
80
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import type { IconName } from '$shared/types/ui/icons';
|
|
7
7
|
|
|
8
8
|
export type SettingsSection =
|
|
9
|
-
| '
|
|
9
|
+
| 'models'
|
|
10
10
|
| 'engines'
|
|
11
11
|
| 'appearance'
|
|
12
12
|
| 'notifications'
|
|
@@ -31,10 +31,10 @@ export interface SettingsSectionMeta {
|
|
|
31
31
|
|
|
32
32
|
export const settingsSections: SettingsSectionMeta[] = [
|
|
33
33
|
{
|
|
34
|
-
id: '
|
|
35
|
-
label: '
|
|
36
|
-
icon: 'lucide:
|
|
37
|
-
description: '
|
|
34
|
+
id: 'models',
|
|
35
|
+
label: 'Models',
|
|
36
|
+
icon: 'lucide:sparkles',
|
|
37
|
+
description: 'Chat and commit model'
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
id: 'appearance',
|
|
@@ -56,8 +56,8 @@ export const settingsSections: SettingsSectionMeta[] = [
|
|
|
56
56
|
},
|
|
57
57
|
{
|
|
58
58
|
id: 'engines',
|
|
59
|
-
label: '
|
|
60
|
-
icon: 'lucide:
|
|
59
|
+
label: 'Engines',
|
|
60
|
+
icon: 'lucide:plug',
|
|
61
61
|
description: 'Installation and accounts',
|
|
62
62
|
adminOnly: true
|
|
63
63
|
},
|
|
@@ -87,11 +87,11 @@ export const settingsSections: SettingsSectionMeta[] = [
|
|
|
87
87
|
// Create the state using Svelte 5 runes
|
|
88
88
|
export const settingsModalState = $state<SettingsModalState>({
|
|
89
89
|
isOpen: false,
|
|
90
|
-
activeSection: '
|
|
90
|
+
activeSection: 'models'
|
|
91
91
|
});
|
|
92
92
|
|
|
93
93
|
// Helper functions
|
|
94
|
-
export function openSettingsModal(section: SettingsSection = '
|
|
94
|
+
export function openSettingsModal(section: SettingsSection = 'models') {
|
|
95
95
|
settingsModalState.isOpen = true;
|
|
96
96
|
settingsModalState.activeSection = section;
|
|
97
97
|
}
|
|
@@ -15,8 +15,12 @@ interface UpdateState {
|
|
|
15
15
|
updating: boolean;
|
|
16
16
|
dismissed: boolean;
|
|
17
17
|
error: string | null;
|
|
18
|
+
errorType: 'check' | 'update' | null;
|
|
18
19
|
updateOutput: string | null;
|
|
19
20
|
updateSuccess: boolean;
|
|
21
|
+
pendingRestart: boolean;
|
|
22
|
+
pendingVersions: { from: string; to: string } | null;
|
|
23
|
+
showRestartModal: boolean;
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
export const updateState = $state<UpdateState>({
|
|
@@ -27,8 +31,12 @@ export const updateState = $state<UpdateState>({
|
|
|
27
31
|
updating: false,
|
|
28
32
|
dismissed: false,
|
|
29
33
|
error: null,
|
|
34
|
+
errorType: null,
|
|
30
35
|
updateOutput: null,
|
|
31
|
-
updateSuccess: false
|
|
36
|
+
updateSuccess: false,
|
|
37
|
+
pendingRestart: false,
|
|
38
|
+
pendingVersions: null,
|
|
39
|
+
showRestartModal: false
|
|
32
40
|
});
|
|
33
41
|
|
|
34
42
|
let checkInterval: ReturnType<typeof setInterval> | null = null;
|
|
@@ -39,6 +47,7 @@ export async function checkForUpdate(): Promise<void> {
|
|
|
39
47
|
|
|
40
48
|
updateState.checking = true;
|
|
41
49
|
updateState.error = null;
|
|
50
|
+
updateState.errorType = null;
|
|
42
51
|
|
|
43
52
|
try {
|
|
44
53
|
const result = await ws.http('system:check-update', {});
|
|
@@ -46,13 +55,23 @@ export async function checkForUpdate(): Promise<void> {
|
|
|
46
55
|
updateState.latestVersion = result.latestVersion;
|
|
47
56
|
updateState.updateAvailable = result.updateAvailable;
|
|
48
57
|
|
|
49
|
-
//
|
|
50
|
-
if (result.
|
|
58
|
+
// Restore pending restart state from backend (survives page refresh)
|
|
59
|
+
if (result.pendingRestart && result.pendingUpdate) {
|
|
60
|
+
updateState.pendingRestart = true;
|
|
61
|
+
updateState.pendingVersions = { from: result.pendingUpdate.fromVersion, to: result.pendingUpdate.toVersion };
|
|
62
|
+
updateState.updateSuccess = true;
|
|
63
|
+
updateState.latestVersion = result.pendingUpdate.toVersion;
|
|
64
|
+
updateState.showRestartModal = true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Auto-update if enabled and update is available (skip if already pending restart)
|
|
68
|
+
if (result.updateAvailable && systemSettings.autoUpdate && !result.pendingRestart) {
|
|
51
69
|
debug.log('server', 'Auto-update enabled, starting update...');
|
|
52
70
|
await runUpdate();
|
|
53
71
|
}
|
|
54
72
|
} catch (err) {
|
|
55
73
|
updateState.error = err instanceof Error ? err.message : 'Failed to check for updates';
|
|
74
|
+
updateState.errorType = 'check';
|
|
56
75
|
debug.error('server', 'Update check failed:', err);
|
|
57
76
|
} finally {
|
|
58
77
|
updateState.checking = false;
|
|
@@ -65,6 +84,7 @@ export async function runUpdate(): Promise<void> {
|
|
|
65
84
|
|
|
66
85
|
updateState.updating = true;
|
|
67
86
|
updateState.error = null;
|
|
87
|
+
updateState.errorType = null;
|
|
68
88
|
updateState.updateOutput = null;
|
|
69
89
|
|
|
70
90
|
try {
|
|
@@ -72,22 +92,49 @@ export async function runUpdate(): Promise<void> {
|
|
|
72
92
|
updateState.updateOutput = result.output;
|
|
73
93
|
updateState.updateSuccess = true;
|
|
74
94
|
updateState.updateAvailable = false;
|
|
95
|
+
updateState.pendingRestart = true;
|
|
96
|
+
updateState.pendingVersions = { from: updateState.currentVersion, to: result.newVersion };
|
|
75
97
|
updateState.latestVersion = result.newVersion;
|
|
98
|
+
updateState.showRestartModal = true;
|
|
76
99
|
|
|
77
100
|
debug.log('server', 'Update completed successfully');
|
|
78
101
|
} catch (err) {
|
|
79
102
|
updateState.error = err instanceof Error ? err.message : 'Update failed';
|
|
103
|
+
updateState.errorType = 'update';
|
|
80
104
|
debug.error('server', 'Update failed:', err);
|
|
81
105
|
} finally {
|
|
82
106
|
updateState.updating = false;
|
|
83
107
|
}
|
|
84
108
|
}
|
|
85
109
|
|
|
86
|
-
/** Dismiss the update banner */
|
|
110
|
+
/** Dismiss the update banner (not allowed when restart is pending) */
|
|
87
111
|
export function dismissUpdate(): void {
|
|
112
|
+
if (updateState.pendingRestart) return;
|
|
88
113
|
updateState.dismissed = true;
|
|
89
114
|
}
|
|
90
115
|
|
|
116
|
+
/** Show the restart instructions modal */
|
|
117
|
+
export function showRestartModal(): void {
|
|
118
|
+
updateState.showRestartModal = true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Hide the restart instructions modal */
|
|
122
|
+
export function hideRestartModal(): void {
|
|
123
|
+
updateState.showRestartModal = false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Listen for update-completed broadcast (notifies other tabs/clients)
|
|
127
|
+
ws.on('system:update-completed', (payload) => {
|
|
128
|
+
debug.log('server', 'Update completed broadcast received');
|
|
129
|
+
updateState.updateSuccess = true;
|
|
130
|
+
updateState.updateAvailable = false;
|
|
131
|
+
updateState.pendingRestart = true;
|
|
132
|
+
updateState.pendingVersions = { from: payload.fromVersion, to: payload.toVersion };
|
|
133
|
+
updateState.latestVersion = payload.toVersion;
|
|
134
|
+
updateState.showRestartModal = true;
|
|
135
|
+
updateState.dismissed = false;
|
|
136
|
+
});
|
|
137
|
+
|
|
91
138
|
/** Start periodic update checks (every 30 minutes) */
|
|
92
139
|
export function startUpdateChecker(): void {
|
|
93
140
|
// Initial check after 5 seconds (let the app settle)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myrialabs/clopen",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "All-in-one web workspace for Claude Code & OpenCode — chat, terminal, git, browser preview, checkpoints, and real-time collaboration",
|
|
5
5
|
"author": "Myria Labs",
|
|
6
6
|
"license": "MIT",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"dev:backend": "bun --watch backend/index.ts",
|
|
50
50
|
"dev:frontend": "bunx vite dev",
|
|
51
51
|
"build": "vite build",
|
|
52
|
-
"start": "
|
|
52
|
+
"start": "bun scripts/start.ts",
|
|
53
53
|
"check": "svelte-check --tsconfig ./tsconfig.json",
|
|
54
54
|
"lint": "eslint .",
|
|
55
55
|
"lint:fix": "eslint . --fix",
|
package/scripts/dev.ts
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
import concurrently from 'concurrently';
|
|
10
10
|
import { findAvailablePort } from '../backend/utils/port-utils';
|
|
11
11
|
|
|
12
|
-
const desiredBackend = process.env.PORT_BACKEND ? parseInt(process.env.PORT_BACKEND) :
|
|
13
|
-
const desiredFrontend = process.env.PORT_FRONTEND ? parseInt(process.env.PORT_FRONTEND) :
|
|
12
|
+
const desiredBackend = process.env.PORT_BACKEND ? parseInt(process.env.PORT_BACKEND) : 9161;
|
|
13
|
+
const desiredFrontend = process.env.PORT_FRONTEND ? parseInt(process.env.PORT_FRONTEND) : 9151;
|
|
14
14
|
|
|
15
15
|
// Resolve available ports
|
|
16
16
|
const backendPort = await findAvailablePort(desiredBackend);
|
|
@@ -33,6 +33,7 @@ console.log(`Frontend: http://localhost:${frontendPort}`);
|
|
|
33
33
|
console.log();
|
|
34
34
|
|
|
35
35
|
const portEnv = {
|
|
36
|
+
NODE_ENV: 'development',
|
|
36
37
|
PORT_BACKEND: String(backendPort),
|
|
37
38
|
PORT_FRONTEND: String(frontendPort),
|
|
38
39
|
};
|
package/scripts/start.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Production start script — resolves available port before starting backend.
|
|
5
|
+
* Mirrors scripts/dev.ts: ensures port is truly available (IPv4 + IPv6)
|
|
6
|
+
* before the backend binds, avoiding silent hangs from zombie processes.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { findAvailablePort } from '../backend/utils/port-utils';
|
|
10
|
+
|
|
11
|
+
const desiredPort = process.env.PORT ? parseInt(process.env.PORT) : 9141;
|
|
12
|
+
|
|
13
|
+
const port = await findAvailablePort(desiredPort);
|
|
14
|
+
|
|
15
|
+
if (port !== desiredPort) {
|
|
16
|
+
console.log(`⚠️ Port ${desiredPort} in use, using ${port}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Set resolved values before importing backend (env.ts reads at import time)
|
|
20
|
+
process.env.PORT = String(port);
|
|
21
|
+
process.env.HOST = process.env.HOST || 'localhost';
|
|
22
|
+
process.env.NODE_ENV = 'production';
|
|
23
|
+
|
|
24
|
+
await import('../backend/index.ts');
|
package/shared/types/git.ts
CHANGED
|
@@ -169,3 +169,18 @@ export interface GitTag {
|
|
|
169
169
|
date: string;
|
|
170
170
|
isAnnotated: boolean;
|
|
171
171
|
}
|
|
172
|
+
|
|
173
|
+
// ============================================
|
|
174
|
+
// Commit Message Generation
|
|
175
|
+
// ============================================
|
|
176
|
+
|
|
177
|
+
/** Format for AI-generated commit messages */
|
|
178
|
+
export type CommitMessageFormat = 'single-line' | 'multi-line';
|
|
179
|
+
|
|
180
|
+
/** Structured commit message output from AI */
|
|
181
|
+
export interface GeneratedCommitMessage {
|
|
182
|
+
type: string;
|
|
183
|
+
scope: string;
|
|
184
|
+
subject: string;
|
|
185
|
+
body: string;
|
|
186
|
+
}
|
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import type { EngineType } from '$shared/types/engine';
|
|
2
|
+
import type { CommitMessageFormat } from '$shared/types/git';
|
|
3
|
+
|
|
4
|
+
/** AI commit message generator settings */
|
|
5
|
+
export interface CommitGeneratorSettings {
|
|
6
|
+
/** When false, uses the chat model (selectedEngine/selectedModel). When true, uses custom engine/model below. */
|
|
7
|
+
useCustomModel: boolean;
|
|
8
|
+
engine: EngineType;
|
|
9
|
+
model: string;
|
|
10
|
+
format: CommitMessageFormat;
|
|
11
|
+
}
|
|
2
12
|
|
|
3
13
|
/** Per-user settings (stored per user) */
|
|
4
14
|
export interface AppSettings {
|
|
@@ -13,6 +23,8 @@ export interface AppSettings {
|
|
|
13
23
|
layoutPresetVisibility: Record<string, boolean>;
|
|
14
24
|
/** Base font size in pixels (10–20). Default: 13. */
|
|
15
25
|
fontSize: number;
|
|
26
|
+
/** AI commit message generator configuration */
|
|
27
|
+
commitGenerator: CommitGeneratorSettings;
|
|
16
28
|
}
|
|
17
29
|
|
|
18
30
|
/** Authentication mode */
|
package/vite.config.ts
CHANGED
|
@@ -3,8 +3,8 @@ import { defineConfig } from 'vite';
|
|
|
3
3
|
import tailwindcss from '@tailwindcss/vite';
|
|
4
4
|
import { resolve } from 'path';
|
|
5
5
|
|
|
6
|
-
const frontendPort = parseInt(process.env.PORT_FRONTEND || '
|
|
7
|
-
const backendPort = parseInt(process.env.PORT_BACKEND || '
|
|
6
|
+
const frontendPort = parseInt(process.env.PORT_FRONTEND || '9151');
|
|
7
|
+
const backendPort = parseInt(process.env.PORT_BACKEND || '9161');
|
|
8
8
|
|
|
9
9
|
export default defineConfig({
|
|
10
10
|
plugins: [tailwindcss(), svelte()],
|