@myrialabs/clopen 0.1.4 → 0.1.6
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/lib/chat/stream-manager.ts +8 -0
- package/backend/lib/database/migrations/022_add_snapshot_changes_column.ts +35 -0
- package/backend/lib/database/migrations/index.ts +7 -0
- package/backend/lib/database/queries/snapshot-queries.ts +7 -4
- package/backend/lib/files/file-watcher.ts +34 -0
- package/backend/lib/project/status-manager.ts +6 -4
- package/backend/lib/snapshot/snapshot-service.ts +471 -316
- package/backend/lib/terminal/pty-session-manager.ts +1 -32
- package/backend/ws/chat/stream.ts +45 -2
- package/backend/ws/snapshot/restore.ts +77 -67
- package/backend/ws/system/operations.ts +95 -0
- package/frontend/App.svelte +24 -7
- package/frontend/lib/components/chat/ChatInterface.svelte +14 -14
- package/frontend/lib/components/chat/input/ChatInput.svelte +2 -2
- package/frontend/lib/components/chat/input/components/ChatInputActions.svelte +1 -1
- package/frontend/lib/components/chat/input/components/EngineModelPicker.svelte +8 -3
- package/frontend/lib/components/chat/input/composables/use-textarea-resize.svelte.ts +12 -2
- package/frontend/lib/components/chat/tools/AskUserQuestionTool.svelte +3 -8
- package/frontend/lib/components/checkpoint/TimelineModal.svelte +222 -30
- package/frontend/lib/components/common/ConnectionBanner.svelte +55 -0
- package/frontend/lib/components/common/MonacoEditor.svelte +14 -0
- package/frontend/lib/components/common/UpdateBanner.svelte +88 -0
- package/frontend/lib/components/common/xterm/XTerm.svelte +9 -0
- package/frontend/lib/components/common/xterm/xterm-service.ts +9 -0
- package/frontend/lib/components/git/DiffViewer.svelte +16 -2
- package/frontend/lib/components/history/HistoryModal.svelte +8 -4
- package/frontend/lib/components/settings/SettingsModal.svelte +2 -2
- package/frontend/lib/components/settings/appearance/AppearanceSettings.svelte +59 -0
- package/frontend/lib/components/settings/general/GeneralSettings.svelte +6 -1
- package/frontend/lib/components/settings/general/UpdateSettings.svelte +123 -0
- package/frontend/lib/components/terminal/Terminal.svelte +1 -7
- package/frontend/lib/components/workspace/DesktopNavigator.svelte +11 -19
- package/frontend/lib/components/workspace/MobileNavigator.svelte +4 -15
- package/frontend/lib/components/workspace/WorkspaceLayout.svelte +1 -1
- package/frontend/lib/components/workspace/panels/FilesPanel.svelte +3 -2
- package/frontend/lib/components/workspace/panels/GitPanel.svelte +3 -2
- package/frontend/lib/services/notification/global-stream-monitor.ts +56 -16
- package/frontend/lib/services/snapshot/snapshot.service.ts +71 -32
- package/frontend/lib/stores/core/app.svelte.ts +47 -0
- package/frontend/lib/stores/core/presence.svelte.ts +80 -1
- package/frontend/lib/stores/core/projects.svelte.ts +10 -2
- package/frontend/lib/stores/core/sessions.svelte.ts +15 -2
- package/frontend/lib/stores/features/settings.svelte.ts +10 -1
- package/frontend/lib/stores/features/terminal.svelte.ts +6 -0
- package/frontend/lib/stores/ui/connection.svelte.ts +40 -0
- package/frontend/lib/stores/ui/update.svelte.ts +124 -0
- package/frontend/lib/stores/ui/workspace.svelte.ts +4 -3
- package/frontend/lib/utils/ws.ts +5 -1
- package/index.html +1 -1
- package/package.json +1 -1
- package/shared/types/database/schema.ts +18 -0
- package/shared/types/stores/settings.ts +4 -0
- package/shared/utils/ws-client.ts +16 -2
- package/vite.config.ts +1 -0
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
role="dialog"
|
|
84
84
|
aria-labelledby="settings-title"
|
|
85
85
|
tabindex="-1"
|
|
86
|
-
class="flex flex-col w-full max-w-225 h-[
|
|
86
|
+
class="flex flex-col w-full max-w-225 h-[85dvh] max-h-175 bg-slate-50 dark:bg-slate-950 border border-violet-500/20 rounded-2xl overflow-hidden shadow-[0_25px_50px_-12px_rgba(0,0,0,0.25)] dark:shadow-[0_25px_50px_-12px_rgba(0,0,0,0.5)] max-md:max-w-full max-md:h-dvh max-md:max-h-dvh max-md:rounded-none"
|
|
87
87
|
onclick={(e) => e.stopPropagation()}
|
|
88
88
|
onkeydown={(e) => e.stopPropagation()}
|
|
89
89
|
in:scale={{ duration: 250, easing: cubicOut, start: 0.95 }}
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
onclick={() => (isMobileMenuOpen = !isMobileMenuOpen)}
|
|
101
101
|
aria-label="Toggle menu"
|
|
102
102
|
>
|
|
103
|
-
<Icon name={isMobileMenuOpen ? 'lucide:
|
|
103
|
+
<Icon name={isMobileMenuOpen ? 'lucide:arrow-left' : 'lucide:menu'} class="w-5 h-5" />
|
|
104
104
|
</button>
|
|
105
105
|
<h2
|
|
106
106
|
id="settings-title"
|
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { themeStore, toggleDarkMode } from '$frontend/lib/stores/ui/theme.svelte';
|
|
3
|
+
import { settings, updateSettings, applyFontSize } from '$frontend/lib/stores/features/settings.svelte';
|
|
3
4
|
import Icon from '../../common/Icon.svelte';
|
|
5
|
+
|
|
6
|
+
const FONT_SIZE_MIN = 10;
|
|
7
|
+
const FONT_SIZE_MAX = 20;
|
|
8
|
+
|
|
9
|
+
function handleFontSizeChange(e: Event) {
|
|
10
|
+
const value = Number((e.target as HTMLInputElement).value);
|
|
11
|
+
applyFontSize(value);
|
|
12
|
+
updateSettings({ fontSize: value });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function fontSizePercent() {
|
|
16
|
+
return ((settings.fontSize - FONT_SIZE_MIN) / (FONT_SIZE_MAX - FONT_SIZE_MIN)) * 100;
|
|
17
|
+
}
|
|
4
18
|
</script>
|
|
5
19
|
|
|
6
20
|
<div class="py-1">
|
|
@@ -41,5 +55,50 @@
|
|
|
41
55
|
></span>
|
|
42
56
|
</label>
|
|
43
57
|
</div>
|
|
58
|
+
|
|
59
|
+
<!-- Font Size -->
|
|
60
|
+
<div
|
|
61
|
+
class="flex flex-col gap-3 p-4 bg-slate-100/80 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-800 rounded-xl transition-all duration-150 hover:border-violet-500/20"
|
|
62
|
+
>
|
|
63
|
+
<div class="flex items-center gap-3.5">
|
|
64
|
+
<div
|
|
65
|
+
class="flex items-center justify-center w-10 h-10 bg-violet-500/10 dark:bg-violet-500/15 rounded-lg text-violet-600 dark:text-violet-400"
|
|
66
|
+
>
|
|
67
|
+
<Icon name="lucide:type" class="w-5 h-5" />
|
|
68
|
+
</div>
|
|
69
|
+
<div class="flex flex-col gap-0.5 flex-1">
|
|
70
|
+
<div class="text-sm font-semibold text-slate-900 dark:text-slate-100">Font Size</div>
|
|
71
|
+
<div class="text-xs text-slate-600 dark:text-slate-500">Adjust the base font size of the application</div>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="text-sm font-semibold text-violet-600 dark:text-violet-400 shrink-0 w-10 text-right">
|
|
74
|
+
{settings.fontSize}px
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div class="flex items-center gap-2.5 px-0.5">
|
|
79
|
+
<span class="text-xs text-slate-500 dark:text-slate-500 shrink-0">A</span>
|
|
80
|
+
<div class="relative flex-1 h-1.5">
|
|
81
|
+
<div class="absolute inset-0 bg-slate-300 dark:bg-slate-700 rounded-full"></div>
|
|
82
|
+
<div
|
|
83
|
+
class="absolute inset-y-0 left-0 bg-gradient-to-r from-violet-500 to-purple-500 rounded-full"
|
|
84
|
+
style="width: {fontSizePercent()}%"
|
|
85
|
+
></div>
|
|
86
|
+
<input
|
|
87
|
+
type="range"
|
|
88
|
+
min={FONT_SIZE_MIN}
|
|
89
|
+
max={FONT_SIZE_MAX}
|
|
90
|
+
step="1"
|
|
91
|
+
value={settings.fontSize}
|
|
92
|
+
oninput={handleFontSizeChange}
|
|
93
|
+
class="absolute inset-0 w-full opacity-0 cursor-pointer h-full"
|
|
94
|
+
/>
|
|
95
|
+
<div
|
|
96
|
+
class="absolute top-1/2 -translate-y-1/2 w-4 h-4 bg-white border-2 border-violet-500 rounded-full shadow-sm pointer-events-none"
|
|
97
|
+
style="left: calc({fontSizePercent()}% - {fontSizePercent() / 100 * 16}px)"
|
|
98
|
+
></div>
|
|
99
|
+
</div>
|
|
100
|
+
<span class="text-base text-slate-500 dark:text-slate-500 shrink-0">A</span>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
44
103
|
</div>
|
|
45
104
|
</div>
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import DataManagementSettings from './DataManagementSettings.svelte';
|
|
3
3
|
import AdvancedSettings from './AdvancedSettings.svelte';
|
|
4
|
+
import UpdateSettings from './UpdateSettings.svelte';
|
|
4
5
|
</script>
|
|
5
6
|
|
|
6
|
-
<
|
|
7
|
+
<UpdateSettings />
|
|
8
|
+
|
|
9
|
+
<div class="mt-6">
|
|
10
|
+
<DataManagementSettings />
|
|
11
|
+
</div>
|
|
7
12
|
|
|
8
13
|
<div class="mt-6">
|
|
9
14
|
<AdvancedSettings />
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { settings, updateSettings } from '$frontend/lib/stores/features/settings.svelte';
|
|
3
|
+
import { updateState, checkForUpdate, runUpdate } from '$frontend/lib/stores/ui/update.svelte';
|
|
4
|
+
import Icon from '../../common/Icon.svelte';
|
|
5
|
+
|
|
6
|
+
function toggleAutoUpdate() {
|
|
7
|
+
updateSettings({ autoUpdate: !settings.autoUpdate });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function handleCheckNow() {
|
|
11
|
+
checkForUpdate();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function handleUpdateNow() {
|
|
15
|
+
runUpdate();
|
|
16
|
+
}
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<div class="py-1">
|
|
20
|
+
<h3 class="text-base font-bold text-slate-900 dark:text-slate-100 mb-1.5">Updates</h3>
|
|
21
|
+
<p class="text-sm text-slate-600 dark:text-slate-500 mb-5">Check for new versions and configure automatic updates</p>
|
|
22
|
+
|
|
23
|
+
<div class="flex flex-col gap-3.5">
|
|
24
|
+
<!-- Version Info -->
|
|
25
|
+
<div class="p-4 bg-slate-100/80 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-800 rounded-xl">
|
|
26
|
+
<div class="flex items-start gap-3.5">
|
|
27
|
+
<div class="flex items-center justify-center w-10 h-10 rounded-lg shrink-0 bg-violet-400/15 text-violet-500">
|
|
28
|
+
<Icon name="lucide:package" class="w-5 h-5" />
|
|
29
|
+
</div>
|
|
30
|
+
<div class="flex flex-col gap-1 min-w-0 flex-1">
|
|
31
|
+
<div class="text-sm font-semibold text-slate-900 dark:text-slate-100">
|
|
32
|
+
@myrialabs/clopen
|
|
33
|
+
</div>
|
|
34
|
+
<div class="text-xs text-slate-600 dark:text-slate-500">
|
|
35
|
+
{#if updateState.currentVersion}
|
|
36
|
+
Current version: <span class="font-mono font-medium text-slate-700 dark:text-slate-400">v{updateState.currentVersion}</span>
|
|
37
|
+
{:else}
|
|
38
|
+
Version info will appear after checking
|
|
39
|
+
{/if}
|
|
40
|
+
</div>
|
|
41
|
+
{#if updateState.updateAvailable}
|
|
42
|
+
<div class="text-xs">
|
|
43
|
+
<span class="inline-flex items-center gap-1 px-1.5 py-0.5 bg-violet-500/15 text-violet-600 dark:text-violet-400 rounded text-2xs font-semibold">
|
|
44
|
+
v{updateState.latestVersion} available
|
|
45
|
+
</span>
|
|
46
|
+
</div>
|
|
47
|
+
{/if}
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div class="flex items-center gap-2 shrink-0">
|
|
51
|
+
{#if updateState.updateAvailable}
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
onclick={handleUpdateNow}
|
|
55
|
+
disabled={updateState.updating}
|
|
56
|
+
class="inline-flex items-center gap-1.5 py-2 px-3.5 bg-violet-500/10 border border-violet-500/20 rounded-lg text-violet-600 dark:text-violet-400 text-xs font-semibold cursor-pointer transition-all duration-150 hover:bg-violet-500/20 hover:border-violet-600/40 disabled:opacity-60 disabled:cursor-not-allowed"
|
|
57
|
+
>
|
|
58
|
+
{#if updateState.updating}
|
|
59
|
+
<div class="w-3.5 h-3.5 border-2 border-violet-600/30 border-t-violet-600 rounded-full animate-spin"></div>
|
|
60
|
+
Updating...
|
|
61
|
+
{:else}
|
|
62
|
+
<Icon name="lucide:download" class="w-3.5 h-3.5" />
|
|
63
|
+
Update
|
|
64
|
+
{/if}
|
|
65
|
+
</button>
|
|
66
|
+
{/if}
|
|
67
|
+
<button
|
|
68
|
+
type="button"
|
|
69
|
+
onclick={handleCheckNow}
|
|
70
|
+
disabled={updateState.checking}
|
|
71
|
+
class="inline-flex items-center gap-1.5 py-2 px-3.5 bg-slate-200/80 dark:bg-slate-700/80 border border-slate-300 dark:border-slate-600 rounded-lg text-slate-700 dark:text-slate-300 text-xs font-semibold cursor-pointer transition-all duration-150 hover:bg-slate-300 dark:hover:bg-slate-600 disabled:opacity-60 disabled:cursor-not-allowed"
|
|
72
|
+
>
|
|
73
|
+
{#if updateState.checking}
|
|
74
|
+
<div class="w-3.5 h-3.5 border-2 border-slate-600/30 border-t-slate-600 dark:border-slate-400/30 dark:border-t-slate-400 rounded-full animate-spin"></div>
|
|
75
|
+
Checking...
|
|
76
|
+
{:else}
|
|
77
|
+
<Icon name="lucide:refresh-cw" class="w-3.5 h-3.5" />
|
|
78
|
+
Check now
|
|
79
|
+
{/if}
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{#if updateState.error}
|
|
85
|
+
<div class="mt-3 flex items-center gap-2 px-3 py-2 bg-red-500/10 border border-red-500/20 rounded-lg">
|
|
86
|
+
<Icon name="lucide:circle-alert" class="w-4 h-4 text-red-500 shrink-0" />
|
|
87
|
+
<span class="text-xs text-red-600 dark:text-red-400">{updateState.error}</span>
|
|
88
|
+
</div>
|
|
89
|
+
{/if}
|
|
90
|
+
|
|
91
|
+
{#if updateState.updateSuccess}
|
|
92
|
+
<div class="mt-3 flex items-center gap-2 px-3 py-2 bg-emerald-500/10 border border-emerald-500/20 rounded-lg">
|
|
93
|
+
<Icon name="lucide:circle-check" class="w-4 h-4 text-emerald-500 shrink-0" />
|
|
94
|
+
<span class="text-xs text-emerald-600 dark:text-emerald-400">Updated successfully — restart clopen to apply</span>
|
|
95
|
+
</div>
|
|
96
|
+
{/if}
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<!-- Auto-Update Toggle -->
|
|
100
|
+
<div class="flex items-center justify-between gap-4 py-3 px-4 bg-slate-100/50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700/50 rounded-lg">
|
|
101
|
+
<div class="flex items-center gap-3">
|
|
102
|
+
<Icon name="lucide:refresh-cw" class="w-4.5 h-4.5 text-slate-600 dark:text-slate-400" />
|
|
103
|
+
<div class="text-left">
|
|
104
|
+
<div class="text-sm font-medium text-slate-900 dark:text-slate-100">Auto-update</div>
|
|
105
|
+
<div class="text-xs text-slate-600 dark:text-slate-400">Automatically install new versions when available</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
<button
|
|
109
|
+
type="button"
|
|
110
|
+
role="switch"
|
|
111
|
+
aria-checked={settings.autoUpdate}
|
|
112
|
+
onclick={toggleAutoUpdate}
|
|
113
|
+
class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-violet-500/30
|
|
114
|
+
{settings.autoUpdate ? 'bg-violet-600' : 'bg-slate-300 dark:bg-slate-600'}"
|
|
115
|
+
>
|
|
116
|
+
<span
|
|
117
|
+
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out
|
|
118
|
+
{settings.autoUpdate ? 'translate-x-5' : 'translate-x-0'}"
|
|
119
|
+
></span>
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
@@ -284,16 +284,10 @@
|
|
|
284
284
|
/>
|
|
285
285
|
</div>
|
|
286
286
|
{:else}
|
|
287
|
-
<!-- No active session -->
|
|
287
|
+
<!-- No active session - will auto-connect -->
|
|
288
288
|
<div class="flex-1 flex items-center justify-center font-mono">
|
|
289
289
|
<div class="text-center text-slate-600 dark:text-slate-400">
|
|
290
290
|
<p>No terminal sessions available</p>
|
|
291
|
-
<button
|
|
292
|
-
class="mt-2 px-4 py-2 bg-violet-600 hover:bg-violet-700 text-white rounded-md transition-colors"
|
|
293
|
-
onclick={handleNewSession}
|
|
294
|
-
>
|
|
295
|
-
Create New Terminal
|
|
296
|
-
</button>
|
|
297
291
|
</div>
|
|
298
292
|
</div>
|
|
299
293
|
{/if}
|
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
import { addNotification } from '$frontend/lib/stores/ui/notification.svelte';
|
|
8
8
|
import { openSettingsModal } from '$frontend/lib/stores/ui/settings-modal.svelte';
|
|
9
9
|
import { projectStatusService } from '$frontend/lib/services/project';
|
|
10
|
-
import { presenceState } from '$frontend/lib/stores/core/presence.svelte';
|
|
10
|
+
import { presenceState, getProjectStatusColor } from '$frontend/lib/stores/core/presence.svelte';
|
|
11
11
|
import type { Project } from '$shared/types/database/schema';
|
|
12
12
|
import { debug } from '$shared/utils/logger';
|
|
13
|
+
import { settings } from '$frontend/lib/stores/features/settings.svelte';
|
|
13
14
|
import FolderBrowser from '$frontend/lib/components/common/FolderBrowser.svelte';
|
|
14
15
|
import Dialog from '$frontend/lib/components/common/Dialog.svelte';
|
|
15
16
|
import ViewMenu from '$frontend/lib/components/workspace/ViewMenu.svelte';
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
const isCollapsed = $derived(workspaceState.navigatorCollapsed);
|
|
31
32
|
const currentProjectId = $derived(projectState.currentProject?.id);
|
|
32
33
|
const navigatorWidth = $derived(
|
|
33
|
-
workspaceState.navigatorCollapsed ? 48 : workspaceState.navigatorWidth
|
|
34
|
+
workspaceState.navigatorCollapsed ? 48 : Math.round(workspaceState.navigatorWidth * (settings.fontSize / 13))
|
|
34
35
|
);
|
|
35
36
|
|
|
36
37
|
const filteredProjects = $derived(() => {
|
|
@@ -110,18 +111,7 @@
|
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
|
|
113
|
-
//
|
|
114
|
-
// Shows real-time status for ALL projects, not just the active one.
|
|
115
|
-
// Uses backend-computed isWaitingInput so background sessions are accurate
|
|
116
|
-
// even when the frontend hasn't received their chat events.
|
|
117
|
-
function getStatusColor(projectId: string): string {
|
|
118
|
-
const status = presenceState.statuses.get(projectId);
|
|
119
|
-
if (!status?.streams) return 'bg-slate-500/30';
|
|
120
|
-
const activeStreams = status.streams.filter((s: any) => s.status === 'active');
|
|
121
|
-
if (activeStreams.length === 0) return 'bg-slate-500/30';
|
|
122
|
-
const hasWaitingInput = activeStreams.some((s: any) => s.isWaitingInput);
|
|
123
|
-
return hasWaitingInput ? 'bg-amber-500' : 'bg-emerald-500';
|
|
124
|
-
}
|
|
114
|
+
// Status color for project indicator — uses shared helper from presence store
|
|
125
115
|
|
|
126
116
|
// Close folder browser
|
|
127
117
|
function closeFolderBrowser() {
|
|
@@ -244,7 +234,7 @@
|
|
|
244
234
|
<div class="relative shrink-0">
|
|
245
235
|
<Icon name="lucide:folder" class="w-4 h-4" />
|
|
246
236
|
<span
|
|
247
|
-
class="absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-slate-50 dark:border-slate-900/95 {
|
|
237
|
+
class="absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-slate-50 dark:border-slate-900/95 {getProjectStatusColor(project.id ?? '')}"
|
|
248
238
|
></span>
|
|
249
239
|
</div>
|
|
250
240
|
|
|
@@ -301,7 +291,7 @@
|
|
|
301
291
|
</footer>
|
|
302
292
|
{:else}
|
|
303
293
|
<!-- Collapsed State: Icon Buttons -->
|
|
304
|
-
<div class="flex
|
|
294
|
+
<div class="flex flex-col items-center pt-4 px-2 shrink-0">
|
|
305
295
|
<button
|
|
306
296
|
type="button"
|
|
307
297
|
class="flex items-center justify-center w-9 h-9 bg-transparent border-none rounded-lg text-slate-500 cursor-pointer transition-all duration-150 relative hover:bg-violet-500/10 hover:text-slate-900 dark:hover:text-slate-100"
|
|
@@ -312,13 +302,15 @@
|
|
|
312
302
|
</button>
|
|
313
303
|
|
|
314
304
|
<div class="w-6 h-px bg-violet-500/10 my-1"></div>
|
|
305
|
+
</div>
|
|
315
306
|
|
|
316
|
-
|
|
307
|
+
<div class="flex-1 flex flex-col items-center gap-2 px-2 pb-4 min-h-0 overflow-y-auto">
|
|
308
|
+
{#each existingProjects as project (project.id)}
|
|
317
309
|
{@const projectStatus = presenceState.statuses.get(project.id ?? '')}
|
|
318
310
|
{@const activeUserCount = (projectStatus?.activeUsers || []).length}
|
|
319
311
|
<button
|
|
320
312
|
type="button"
|
|
321
|
-
class="flex items-center justify-center w-9 h-9 border-none rounded-lg cursor-pointer transition-all duration-150 relative font-semibold text-sm
|
|
313
|
+
class="flex items-center justify-center w-9 h-9 shrink-0 border-none rounded-lg cursor-pointer transition-all duration-150 relative font-semibold text-sm
|
|
322
314
|
{currentProjectId === project.id
|
|
323
315
|
? 'bg-violet-500/10 dark:bg-violet-500/20 text-violet-700 dark:text-violet-300'
|
|
324
316
|
: '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'}"
|
|
@@ -327,7 +319,7 @@
|
|
|
327
319
|
>
|
|
328
320
|
<span>{getProjectInitials(project.name)}</span>
|
|
329
321
|
<span
|
|
330
|
-
class="absolute bottom-1 right-1 w-2.5 h-2.5 rounded-full border-2 border-slate-50 dark:border-slate-900/95 {
|
|
322
|
+
class="absolute bottom-1 right-1 w-2.5 h-2.5 rounded-full border-2 border-slate-50 dark:border-slate-900/95 {getProjectStatusColor(project.id ?? '')}"
|
|
331
323
|
></span>
|
|
332
324
|
{#if activeUserCount > 0}
|
|
333
325
|
<span
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
type PanelId
|
|
9
9
|
} from '$frontend/lib/stores/ui/workspace.svelte';
|
|
10
10
|
import { projectState, removeProject } from '$frontend/lib/stores/core/projects.svelte';
|
|
11
|
-
import { presenceState } from '$frontend/lib/stores/core/presence.svelte';
|
|
11
|
+
import { presenceState, getProjectStatusColor } from '$frontend/lib/stores/core/presence.svelte';
|
|
12
12
|
import { openSettingsModal } from '$frontend/lib/stores/ui/settings-modal.svelte';
|
|
13
13
|
import { addNotification } from '$frontend/lib/stores/ui/notification.svelte';
|
|
14
14
|
import type { IconName } from '$shared/types/ui/icons';
|
|
@@ -65,18 +65,7 @@
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
//
|
|
69
|
-
// Shows real-time status for ALL projects, not just the active one.
|
|
70
|
-
// Uses backend-computed isWaitingInput so background sessions are accurate
|
|
71
|
-
// even when the frontend hasn't received their chat events.
|
|
72
|
-
function getStatusColor(projectId: string): string {
|
|
73
|
-
const status = presenceState.statuses.get(projectId);
|
|
74
|
-
if (!status?.streams) return 'bg-slate-500/30';
|
|
75
|
-
const activeStreams = status.streams.filter((s: any) => s.status === 'active');
|
|
76
|
-
if (activeStreams.length === 0) return 'bg-slate-500/30';
|
|
77
|
-
const hasWaitingInput = activeStreams.some((s: any) => s.isWaitingInput);
|
|
78
|
-
return hasWaitingInput ? 'bg-amber-500' : 'bg-emerald-500';
|
|
79
|
-
}
|
|
68
|
+
// Status color for project indicator — uses shared helper from presence store
|
|
80
69
|
|
|
81
70
|
function openAddProject() {
|
|
82
71
|
showProjectMenu = false;
|
|
@@ -162,7 +151,7 @@
|
|
|
162
151
|
aria-expanded={showProjectMenu}
|
|
163
152
|
aria-haspopup="menu"
|
|
164
153
|
>
|
|
165
|
-
<div class="relative shrink-0"><Icon name="lucide:folder-open" class="w-4 h-4" /><span class="absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-slate-100 dark:border-slate-800 {
|
|
154
|
+
<div class="relative shrink-0"><Icon name="lucide:folder-open" class="w-4 h-4" /><span class="absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-slate-100 dark:border-slate-800 {getProjectStatusColor(projectState.currentProject?.id ?? '')}"></span></div>
|
|
166
155
|
<span class="flex-1 text-left overflow-hidden text-ellipsis whitespace-nowrap">
|
|
167
156
|
{projectState.currentProject?.name ?? 'No Project'}
|
|
168
157
|
</span>
|
|
@@ -315,7 +304,7 @@
|
|
|
315
304
|
>
|
|
316
305
|
<Icon name="lucide:folder" class="text-violet-600 dark:text-violet-400 w-4 h-4" />
|
|
317
306
|
<span
|
|
318
|
-
class="absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-white dark:border-slate-900 {
|
|
307
|
+
class="absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-white dark:border-slate-900 {getProjectStatusColor(project.id ?? '')}"
|
|
319
308
|
></span>
|
|
320
309
|
</div>
|
|
321
310
|
<div class="flex-1 min-w-0">
|
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
|
|
139
139
|
<!-- Main Workspace Layout -->
|
|
140
140
|
<div
|
|
141
|
-
class="h-
|
|
141
|
+
class="h-full w-full overflow-hidden {isMobile ? 'bg-white/90 dark:bg-slate-900/98' : 'bg-slate-50 dark:bg-slate-900/70'} text-slate-900 dark:text-slate-100 font-sans"
|
|
142
142
|
>
|
|
143
143
|
<!-- Skip link for accessibility -->
|
|
144
144
|
<a
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import Dialog from '$frontend/lib/components/common/Dialog.svelte';
|
|
13
13
|
import type { FileNode } from '$shared/types/filesystem';
|
|
14
14
|
import { debug } from '$shared/utils/logger';
|
|
15
|
+
import { settings } from '$frontend/lib/stores/features/settings.svelte';
|
|
15
16
|
import { onMount, onDestroy } from 'svelte';
|
|
16
17
|
import ws from '$frontend/lib/utils/ws';
|
|
17
18
|
import { showConfirm } from '$frontend/lib/stores/ui/dialog.svelte';
|
|
@@ -138,7 +139,7 @@
|
|
|
138
139
|
// Container width detection for 2-column layout
|
|
139
140
|
let containerRef = $state<HTMLDivElement | null>(null);
|
|
140
141
|
let containerWidth = $state(0);
|
|
141
|
-
const TWO_COLUMN_THRESHOLD =
|
|
142
|
+
const TWO_COLUMN_THRESHOLD = $derived(Math.round(600 * (settings.fontSize / 13)));
|
|
142
143
|
|
|
143
144
|
// FileTree ref
|
|
144
145
|
let fileTreeRef = $state<any>(null);
|
|
@@ -1185,7 +1186,7 @@
|
|
|
1185
1186
|
<!-- Tree panel: always rendered, hidden via CSS in 1-column viewer mode -->
|
|
1186
1187
|
<div
|
|
1187
1188
|
class={isTwoColumnMode
|
|
1188
|
-
? 'w-
|
|
1189
|
+
? 'w-72 flex-shrink-0 h-full overflow-hidden border-r border-slate-200 dark:border-slate-700'
|
|
1189
1190
|
: (viewMode === 'tree' ? 'w-full h-full overflow-hidden' : 'hidden')}
|
|
1190
1191
|
>
|
|
1191
1192
|
<div class="h-full overflow-auto" bind:this={treeScrollContainer}>
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { projectState } from '$frontend/lib/stores/core/projects.svelte';
|
|
6
6
|
import { showError, showInfo } from '$frontend/lib/stores/ui/notification.svelte';
|
|
7
7
|
import { debug } from '$shared/utils/logger';
|
|
8
|
+
import { settings } from '$frontend/lib/stores/features/settings.svelte';
|
|
8
9
|
import ws from '$frontend/lib/utils/ws';
|
|
9
10
|
import { getFileIcon } from '$frontend/lib/utils/file-icon-mappings';
|
|
10
11
|
import { getGitStatusLabel, getGitStatusColor } from '$frontend/lib/utils/git-status';
|
|
@@ -131,7 +132,7 @@
|
|
|
131
132
|
// Container width for responsive layout (same threshold as Files: 800)
|
|
132
133
|
let containerRef = $state<HTMLDivElement | null>(null);
|
|
133
134
|
let containerWidth = $state(0);
|
|
134
|
-
const TWO_COLUMN_THRESHOLD =
|
|
135
|
+
const TWO_COLUMN_THRESHOLD = $derived(Math.round(600 * (settings.fontSize / 13)));
|
|
135
136
|
const isTwoColumnMode = $derived(containerWidth >= TWO_COLUMN_THRESHOLD);
|
|
136
137
|
|
|
137
138
|
// Track last project for re-fetch
|
|
@@ -1504,7 +1505,7 @@
|
|
|
1504
1505
|
<!-- Left panel: Changes list (w-80 like Files panel tree) -->
|
|
1505
1506
|
<div
|
|
1506
1507
|
class={isTwoColumnMode
|
|
1507
|
-
? 'w-
|
|
1508
|
+
? 'w-72 flex-shrink-0 h-full overflow-hidden border-r border-slate-200 dark:border-slate-700 flex flex-col'
|
|
1508
1509
|
: (viewMode === 'list' ? 'w-full h-full overflow-hidden flex flex-col' : 'hidden')}
|
|
1509
1510
|
>
|
|
1510
1511
|
{@render changesList()}
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Single source of truth for chat stream notifications (sound + push).
|
|
5
5
|
*
|
|
6
|
-
* Listens for
|
|
7
|
-
*
|
|
6
|
+
* Listens for backend events and triggers notifications for ALL projects —
|
|
7
|
+
* both active and non-active:
|
|
8
|
+
* - chat:stream-finished → stream completed/errored/cancelled
|
|
9
|
+
* - chat:waiting-input → AskUserQuestion requires user input
|
|
8
10
|
*
|
|
9
|
-
* The backend uses ws.emit.projectMembers()
|
|
10
|
-
*
|
|
11
|
-
* to a different project.
|
|
11
|
+
* The backend uses ws.emit.projectMembers() so notifications reach users
|
|
12
|
+
* even when they are on a different project or session.
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
15
|
import { soundNotification, pushNotification } from '$frontend/lib/services/notification';
|
|
@@ -19,27 +20,27 @@ import ws from '$frontend/lib/utils/ws';
|
|
|
19
20
|
class GlobalStreamMonitor {
|
|
20
21
|
private initialized = false;
|
|
21
22
|
|
|
23
|
+
/** Track notified AskUserQuestion tool_use IDs to ensure once-only notification */
|
|
24
|
+
private notifiedToolUseIds = new Set<string>();
|
|
25
|
+
|
|
22
26
|
/**
|
|
23
|
-
* Initialize the monitor - subscribes to
|
|
27
|
+
* Initialize the monitor - subscribes to WS events.
|
|
24
28
|
* Safe to call multiple times (idempotent).
|
|
25
|
-
*
|
|
26
|
-
* Triggers sound/push for ALL projects the user is a member of,
|
|
27
|
-
* not just the active one — background streams deserve notifications too.
|
|
28
29
|
*/
|
|
29
30
|
initialize(): void {
|
|
30
31
|
if (this.initialized) return;
|
|
31
32
|
this.initialized = true;
|
|
32
33
|
|
|
33
|
-
debug.log('notification', 'GlobalStreamMonitor: Initializing WS
|
|
34
|
+
debug.log('notification', 'GlobalStreamMonitor: Initializing WS listeners');
|
|
34
35
|
|
|
36
|
+
// Stream finished — notify on completion
|
|
35
37
|
ws.on('chat:stream-finished', async (data) => {
|
|
36
|
-
const { projectId, status,
|
|
38
|
+
const { projectId, status, chatSessionId } = data;
|
|
39
|
+
|
|
40
|
+
debug.log('notification', 'GlobalStreamMonitor: Stream finished', { projectId, status });
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
status,
|
|
41
|
-
timestamp
|
|
42
|
-
});
|
|
42
|
+
// Clean up notified IDs for this session (stream is done)
|
|
43
|
+
this.clearSessionNotifications(chatSessionId);
|
|
43
44
|
|
|
44
45
|
// Play sound notification
|
|
45
46
|
try {
|
|
@@ -63,12 +64,51 @@ class GlobalStreamMonitor {
|
|
|
63
64
|
debug.error('notification', 'Error sending push notification:', error);
|
|
64
65
|
}
|
|
65
66
|
});
|
|
67
|
+
|
|
68
|
+
// Waiting for input — notify once per AskUserQuestion
|
|
69
|
+
ws.on('chat:waiting-input', async (data) => {
|
|
70
|
+
const { projectId, chatSessionId, toolUseId } = data;
|
|
71
|
+
|
|
72
|
+
// Deduplicate: only notify once per tool_use ID
|
|
73
|
+
if (this.notifiedToolUseIds.has(toolUseId)) return;
|
|
74
|
+
this.notifiedToolUseIds.add(toolUseId);
|
|
75
|
+
|
|
76
|
+
debug.log('notification', 'GlobalStreamMonitor: Waiting for input', { projectId, chatSessionId, toolUseId });
|
|
77
|
+
|
|
78
|
+
// Play sound notification
|
|
79
|
+
try {
|
|
80
|
+
await soundNotification.play();
|
|
81
|
+
} catch (error) {
|
|
82
|
+
debug.error('notification', 'Error playing sound notification:', error);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Send push notification
|
|
86
|
+
try {
|
|
87
|
+
const projectName = projectState.projects.find(p => p.id === projectId)?.name || 'Unknown';
|
|
88
|
+
await pushNotification.sendChatComplete(`Waiting for your input in "${projectName}"`);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
debug.error('notification', 'Error sending push notification:', error);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Remove tracked tool_use IDs for a finished session
|
|
97
|
+
*/
|
|
98
|
+
private clearSessionNotifications(chatSessionId: string): void {
|
|
99
|
+
// Tool IDs are globally unique, so a simple clear-on-stream-finish
|
|
100
|
+
// is enough — they won't collide across sessions.
|
|
101
|
+
// For long-running apps, periodically trim to avoid unbounded growth.
|
|
102
|
+
if (this.notifiedToolUseIds.size > 500) {
|
|
103
|
+
this.notifiedToolUseIds.clear();
|
|
104
|
+
}
|
|
66
105
|
}
|
|
67
106
|
|
|
68
107
|
/**
|
|
69
108
|
* Clear state (for cleanup/testing)
|
|
70
109
|
*/
|
|
71
110
|
clear(): void {
|
|
111
|
+
this.notifiedToolUseIds.clear();
|
|
72
112
|
debug.log('notification', 'GlobalStreamMonitor: Clearing state');
|
|
73
113
|
}
|
|
74
114
|
}
|