@myrialabs/clopen 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/.dockerignore +5 -0
  2. package/.env.example +2 -5
  3. package/CONTRIBUTING.md +4 -0
  4. package/README.md +4 -2
  5. package/backend/database/queries/message-queries.ts +42 -0
  6. package/backend/database/utils/connection.ts +5 -5
  7. package/backend/engine/adapters/claude/environment.ts +3 -4
  8. package/backend/engine/adapters/claude/stream.ts +107 -0
  9. package/backend/engine/adapters/opencode/server.ts +7 -1
  10. package/backend/engine/adapters/opencode/stream.ts +81 -1
  11. package/backend/engine/types.ts +17 -0
  12. package/backend/git/git-executor.ts +2 -1
  13. package/backend/git/git-service.ts +2 -1
  14. package/backend/index.ts +10 -10
  15. package/backend/snapshot/blob-store.ts +2 -2
  16. package/backend/utils/env.ts +13 -15
  17. package/backend/utils/index.ts +4 -1
  18. package/backend/utils/paths.ts +11 -0
  19. package/backend/utils/port-utils.ts +19 -6
  20. package/backend/ws/git/commit-message.ts +108 -0
  21. package/backend/ws/git/index.ts +3 -1
  22. package/backend/ws/messages/crud.ts +52 -0
  23. package/backend/ws/system/index.ts +7 -1
  24. package/backend/ws/system/operations.ts +28 -2
  25. package/bin/clopen.ts +15 -15
  26. package/docker-compose.yml +31 -0
  27. package/frontend/App.svelte +3 -0
  28. package/frontend/components/auth/SetupPage.svelte +45 -13
  29. package/frontend/components/chat/input/ChatInput.svelte +1 -1
  30. package/frontend/components/chat/message/ChatMessage.svelte +64 -16
  31. package/frontend/components/chat/widgets/FloatingTodoList.svelte +124 -10
  32. package/frontend/components/checkpoint/ConflictResolutionModal.svelte +189 -0
  33. package/frontend/components/checkpoint/TimelineModal.svelte +7 -162
  34. package/frontend/components/common/feedback/RestartRequiredModal.svelte +53 -0
  35. package/frontend/components/common/feedback/UpdateBanner.svelte +19 -8
  36. package/frontend/components/git/BranchManager.svelte +143 -155
  37. package/frontend/components/git/CommitForm.svelte +61 -11
  38. package/frontend/components/history/HistoryModal.svelte +30 -78
  39. package/frontend/components/history/HistoryView.svelte +45 -92
  40. package/frontend/components/settings/SettingsModal.svelte +1 -1
  41. package/frontend/components/settings/SettingsView.svelte +1 -1
  42. package/frontend/components/settings/appearance/AppearanceSettings.svelte +2 -2
  43. package/frontend/components/settings/engines/AIEnginesSettings.svelte +2 -2
  44. package/frontend/components/settings/general/UpdateSettings.svelte +10 -3
  45. package/frontend/components/settings/git/GitSettings.svelte +392 -0
  46. package/frontend/components/settings/model/EngineModelPicker.svelte +275 -0
  47. package/frontend/components/settings/model/ModelSettings.svelte +172 -289
  48. package/frontend/components/workspace/PanelHeader.svelte +1 -3
  49. package/frontend/components/workspace/WorkspaceLayout.svelte +11 -1
  50. package/frontend/components/workspace/panels/FilesPanel.svelte +41 -3
  51. package/frontend/components/workspace/panels/GitPanel.svelte +53 -8
  52. package/frontend/main.ts +4 -0
  53. package/frontend/stores/features/auth.svelte.ts +28 -0
  54. package/frontend/stores/features/settings.svelte.ts +13 -2
  55. package/frontend/stores/ui/settings-modal.svelte.ts +9 -9
  56. package/frontend/stores/ui/update.svelte.ts +51 -4
  57. package/package.json +2 -2
  58. package/scripts/dev.ts +3 -2
  59. package/scripts/start.ts +24 -0
  60. package/shared/types/git.ts +15 -0
  61. package/shared/types/stores/settings.ts +12 -0
  62. package/vite.config.ts +2 -2
@@ -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
- await loadStatus();
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
- // Also refresh branches in case of branch switch/create/delete
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
- ? 'w-72 flex-shrink-0 h-full overflow-hidden border-r border-slate-200 dark:border-slate-700 flex flex-col'
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
- <!-- Right panel: Diff viewer -->
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
@@ -6,6 +6,10 @@ import { mount } from 'svelte';
6
6
  import './app.css';
7
7
  import App from './App.svelte';
8
8
 
9
+ if (import.meta.env.DEV) {
10
+ document.title = 'Clopen - DEV';
11
+ }
12
+
9
13
  const app = mount(App, {
10
14
  target: document.getElementById('app')!
11
15
  });
@@ -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
- Object.assign(settings, { ...defaultSettings, ...serverSettings });
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
- | 'model'
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: 'model',
35
- label: 'Model',
36
- icon: 'lucide:cpu',
37
- description: 'AI engine and model'
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: 'AI Engine',
60
- icon: 'lucide:bot',
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: 'model'
90
+ activeSection: 'models'
91
91
  });
92
92
 
93
93
  // Helper functions
94
- export function openSettingsModal(section: SettingsSection = 'model') {
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
- // Auto-update if enabled and update is available
50
- if (result.updateAvailable && systemSettings.autoUpdate) {
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.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": "NODE_ENV=production bun backend/index.ts",
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) : 9151;
13
- const desiredFrontend = process.env.PORT_FRONTEND ? parseInt(process.env.PORT_FRONTEND) : 9141;
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
  };
@@ -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');
@@ -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 || '9141');
7
- const backendPort = parseInt(process.env.PORT_BACKEND || '9151');
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()],