@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
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Commit Message Generator Handler
|
|
3
|
+
*
|
|
4
|
+
* Uses AI engines to generate structured commit messages from staged diffs.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { t } from 'elysia';
|
|
8
|
+
import { createRouter } from '$shared/utils/ws-server';
|
|
9
|
+
import { execGit } from '../../git/git-executor';
|
|
10
|
+
import { projectQueries } from '../../database/queries/project-queries';
|
|
11
|
+
import { initializeEngine } from '../../engine';
|
|
12
|
+
import { parseModelId } from '$shared/constants/engines';
|
|
13
|
+
import type { EngineType } from '$shared/types/engine';
|
|
14
|
+
import type { GeneratedCommitMessage } from '$shared/types/git';
|
|
15
|
+
import { debug } from '$shared/utils/logger';
|
|
16
|
+
|
|
17
|
+
const COMMIT_MESSAGE_SCHEMA = {
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: {
|
|
20
|
+
type: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
enum: ['feat', 'fix', 'refactor', 'docs', 'test', 'chore', 'style', 'perf', 'ci', 'build'],
|
|
23
|
+
description: 'The conventional commit type'
|
|
24
|
+
},
|
|
25
|
+
scope: {
|
|
26
|
+
type: 'string',
|
|
27
|
+
description: 'Optional scope of the change (e.g., component name, module)'
|
|
28
|
+
},
|
|
29
|
+
subject: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
description: 'Short imperative description, lowercase, no period, max 72 chars'
|
|
32
|
+
},
|
|
33
|
+
body: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
description: 'Optional longer description explaining the why behind the change'
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
required: ['type', 'subject']
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const commitMessageHandler = createRouter()
|
|
42
|
+
.http('git:generate-commit-message', {
|
|
43
|
+
data: t.Object({
|
|
44
|
+
projectId: t.String(),
|
|
45
|
+
engine: t.String(),
|
|
46
|
+
model: t.String(),
|
|
47
|
+
format: t.Union([t.Literal('single-line'), t.Literal('multi-line')])
|
|
48
|
+
}),
|
|
49
|
+
response: t.Object({
|
|
50
|
+
message: t.String()
|
|
51
|
+
})
|
|
52
|
+
}, async ({ data }) => {
|
|
53
|
+
const project = projectQueries.getById(data.projectId);
|
|
54
|
+
if (!project) throw new Error('Project not found');
|
|
55
|
+
|
|
56
|
+
// Get raw staged diff text
|
|
57
|
+
const diffResult = await execGit(['diff', '--cached'], project.path);
|
|
58
|
+
const rawDiff = diffResult.stdout;
|
|
59
|
+
|
|
60
|
+
if (!rawDiff.trim()) {
|
|
61
|
+
throw new Error('No staged changes to generate a commit message for');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const engineType = data.engine as EngineType;
|
|
65
|
+
const engine = await initializeEngine(engineType);
|
|
66
|
+
|
|
67
|
+
if (!engine.generateStructured) {
|
|
68
|
+
throw new Error(`Engine "${engineType}" does not support structured generation`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const formatInstruction = data.format === 'multi-line'
|
|
72
|
+
? 'Generate a multi-line conventional commit message with type, scope, subject, AND body fields. The body should explain WHY the change was made.'
|
|
73
|
+
: 'Generate a single-line conventional commit message with type, optional scope, and subject. Leave body empty.';
|
|
74
|
+
|
|
75
|
+
const prompt = `Analyze the following git diff and generate a conventional commit message.
|
|
76
|
+
|
|
77
|
+
Rules:
|
|
78
|
+
- type: one of feat, fix, refactor, docs, test, chore, style, perf, ci, build
|
|
79
|
+
- scope: optional, the area of the codebase affected (e.g., git, settings, engine)
|
|
80
|
+
- subject: imperative mood, lowercase, no period at end, max 72 characters
|
|
81
|
+
- ${formatInstruction}
|
|
82
|
+
|
|
83
|
+
Git diff:
|
|
84
|
+
${rawDiff}`;
|
|
85
|
+
|
|
86
|
+
// Parse model ID: "claude-code:haiku" → modelId "haiku", "opencode:anthropic/claude-sonnet" → modelId "anthropic/claude-sonnet"
|
|
87
|
+
const { modelId } = parseModelId(data.model);
|
|
88
|
+
|
|
89
|
+
debug.log('git', `Generating commit message via ${engineType}/${modelId}`);
|
|
90
|
+
|
|
91
|
+
const result = await engine.generateStructured<GeneratedCommitMessage>({
|
|
92
|
+
prompt,
|
|
93
|
+
model: modelId,
|
|
94
|
+
schema: COMMIT_MESSAGE_SCHEMA,
|
|
95
|
+
projectPath: project.path
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Format structured output into conventional commit string
|
|
99
|
+
const scopePart = result.scope ? `(${result.scope})` : '';
|
|
100
|
+
const subject = `${result.type}${scopePart}: ${result.subject}`;
|
|
101
|
+
|
|
102
|
+
let message = subject;
|
|
103
|
+
if (data.format === 'multi-line' && result.body) {
|
|
104
|
+
message = `${subject}\n\n${result.body}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { message };
|
|
108
|
+
});
|
package/backend/ws/git/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { branchHandler } from './branch';
|
|
|
12
12
|
import { logHandler } from './log';
|
|
13
13
|
import { remoteHandler } from './remote';
|
|
14
14
|
import { conflictHandler } from './conflict';
|
|
15
|
+
import { commitMessageHandler } from './commit-message';
|
|
15
16
|
|
|
16
17
|
export const gitRouter = createRouter()
|
|
17
18
|
.merge(statusHandler)
|
|
@@ -21,4 +22,5 @@ export const gitRouter = createRouter()
|
|
|
21
22
|
.merge(branchHandler)
|
|
22
23
|
.merge(logHandler)
|
|
23
24
|
.merge(remoteHandler)
|
|
24
|
-
.merge(conflictHandler)
|
|
25
|
+
.merge(conflictHandler)
|
|
26
|
+
.merge(commitMessageHandler);
|
|
@@ -12,6 +12,15 @@ import { createRouter } from '$shared/utils/ws-server';
|
|
|
12
12
|
import { messageQueries } from '../../database/queries';
|
|
13
13
|
import { formatDatabaseMessage } from '$shared/utils/message-formatter';
|
|
14
14
|
|
|
15
|
+
function extractTextContent(content: unknown): string {
|
|
16
|
+
if (typeof content === 'string') return content;
|
|
17
|
+
if (!Array.isArray(content)) return '';
|
|
18
|
+
return content
|
|
19
|
+
.filter((c: any) => c.type === 'text')
|
|
20
|
+
.map((b: any) => b.text || '')
|
|
21
|
+
.join(' ');
|
|
22
|
+
}
|
|
23
|
+
|
|
15
24
|
export const crudHandler = createRouter()
|
|
16
25
|
// List messages
|
|
17
26
|
.http('messages:list', {
|
|
@@ -33,6 +42,49 @@ export const crudHandler = createRouter()
|
|
|
33
42
|
}
|
|
34
43
|
})
|
|
35
44
|
|
|
45
|
+
// Bulk session preview — returns title, summary, and message counts for multiple sessions
|
|
46
|
+
// without loading all messages. Used by the Sessions/History modal.
|
|
47
|
+
.http('sessions:preview', {
|
|
48
|
+
data: t.Object({
|
|
49
|
+
session_ids: t.Array(t.String())
|
|
50
|
+
}),
|
|
51
|
+
response: t.Array(t.Any())
|
|
52
|
+
}, ({ data }) => {
|
|
53
|
+
return data.session_ids.map(sessionId => {
|
|
54
|
+
const preview = messageQueries.getSessionPreview(sessionId);
|
|
55
|
+
|
|
56
|
+
// Title from first user message
|
|
57
|
+
let title = 'New Conversation';
|
|
58
|
+
if (preview.firstUserMessage) {
|
|
59
|
+
const sdk = JSON.parse(preview.firstUserMessage.sdk_message);
|
|
60
|
+
const textContent = extractTextContent(sdk.message?.content).trim();
|
|
61
|
+
if (textContent) {
|
|
62
|
+
title = textContent.slice(0, 60) + (textContent.length > 60 ? '...' : '');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Summary from last assistant message
|
|
67
|
+
let summary = 'No messages yet';
|
|
68
|
+
if (preview.lastAssistantMessage) {
|
|
69
|
+
const sdk = JSON.parse(preview.lastAssistantMessage.sdk_message);
|
|
70
|
+
const rawText = extractTextContent(sdk.message?.content);
|
|
71
|
+
const cleanText = rawText.replace(/```[\s\S]*?```/g, '').trim();
|
|
72
|
+
if (cleanText) {
|
|
73
|
+
summary = cleanText.slice(0, 100) + (cleanText.length > 100 ? '...' : '');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
session_id: sessionId,
|
|
79
|
+
title,
|
|
80
|
+
summary,
|
|
81
|
+
userCount: preview.userCount,
|
|
82
|
+
assistantCount: preview.assistantCount,
|
|
83
|
+
count: preview.userCount + preview.assistantCount
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
})
|
|
87
|
+
|
|
36
88
|
// Get message by ID
|
|
37
89
|
.http('messages:get', {
|
|
38
90
|
data: t.Object({
|
|
@@ -8,7 +8,13 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { createRouter } from '$shared/utils/ws-server';
|
|
11
|
+
import { t } from 'elysia';
|
|
11
12
|
import { operationsHandler } from './operations';
|
|
12
13
|
|
|
13
14
|
export const systemRouter = createRouter()
|
|
14
|
-
.merge(operationsHandler)
|
|
15
|
+
.merge(operationsHandler)
|
|
16
|
+
// Declare system:update-completed event (broadcast after successful package update)
|
|
17
|
+
.emit('system:update-completed', t.Object({
|
|
18
|
+
fromVersion: t.String(),
|
|
19
|
+
toVersion: t.String()
|
|
20
|
+
}));
|
|
@@ -13,6 +13,10 @@ import { readFileSync } from 'node:fs';
|
|
|
13
13
|
import { createRouter } from '$shared/utils/ws-server';
|
|
14
14
|
import { initializeDatabase, getDatabase } from '../../database';
|
|
15
15
|
import { debug } from '$shared/utils/logger';
|
|
16
|
+
import { ws } from '$backend/utils/ws';
|
|
17
|
+
|
|
18
|
+
/** In-memory flag: set after successful update, cleared on server restart */
|
|
19
|
+
let pendingUpdate: { fromVersion: string; toVersion: string } | null = null;
|
|
16
20
|
|
|
17
21
|
/** Read current version from package.json */
|
|
18
22
|
function getCurrentVersion(): string {
|
|
@@ -56,7 +60,12 @@ export const operationsHandler = createRouter()
|
|
|
56
60
|
response: t.Object({
|
|
57
61
|
currentVersion: t.String(),
|
|
58
62
|
latestVersion: t.String(),
|
|
59
|
-
updateAvailable: t.Boolean()
|
|
63
|
+
updateAvailable: t.Boolean(),
|
|
64
|
+
pendingRestart: t.Boolean(),
|
|
65
|
+
pendingUpdate: t.Optional(t.Object({
|
|
66
|
+
fromVersion: t.String(),
|
|
67
|
+
toVersion: t.String()
|
|
68
|
+
}))
|
|
60
69
|
})
|
|
61
70
|
}, async () => {
|
|
62
71
|
const currentVersion = getCurrentVersion();
|
|
@@ -67,7 +76,13 @@ export const operationsHandler = createRouter()
|
|
|
67
76
|
|
|
68
77
|
debug.log('server', `Latest version: ${latestVersion}, update available: ${updateAvailable}`);
|
|
69
78
|
|
|
70
|
-
return {
|
|
79
|
+
return {
|
|
80
|
+
currentVersion,
|
|
81
|
+
latestVersion,
|
|
82
|
+
updateAvailable,
|
|
83
|
+
pendingRestart: pendingUpdate !== null,
|
|
84
|
+
pendingUpdate: pendingUpdate ?? undefined
|
|
85
|
+
};
|
|
71
86
|
})
|
|
72
87
|
|
|
73
88
|
// Run package update
|
|
@@ -98,9 +113,20 @@ export const operationsHandler = createRouter()
|
|
|
98
113
|
throw new Error(`Update failed (exit code ${exitCode}): ${output}`);
|
|
99
114
|
}
|
|
100
115
|
|
|
116
|
+
const fromVersion = getCurrentVersion();
|
|
117
|
+
|
|
101
118
|
// Re-fetch to confirm new version
|
|
102
119
|
const newVersion = await fetchLatestVersion();
|
|
103
120
|
|
|
121
|
+
// Set pending restart flag (persists until server restarts)
|
|
122
|
+
pendingUpdate = { fromVersion, toVersion: newVersion };
|
|
123
|
+
|
|
124
|
+
// Broadcast to all connected clients
|
|
125
|
+
ws.emit.global('system:update-completed', {
|
|
126
|
+
fromVersion,
|
|
127
|
+
toVersion: newVersion
|
|
128
|
+
});
|
|
129
|
+
|
|
104
130
|
debug.log('server', `Update completed. New version: ${newVersion}`);
|
|
105
131
|
|
|
106
132
|
return { success: true, output, newVersion };
|
package/bin/clopen.ts
CHANGED
|
@@ -23,6 +23,7 @@ if (typeof globalThis.Bun === 'undefined') {
|
|
|
23
23
|
|
|
24
24
|
import { existsSync, copyFileSync, readFileSync, writeFileSync } from 'fs';
|
|
25
25
|
import { join } from 'path';
|
|
26
|
+
import { loadEnvFile } from '../backend/utils/env';
|
|
26
27
|
|
|
27
28
|
// CLI Options interface
|
|
28
29
|
interface CLIOptions {
|
|
@@ -88,7 +89,7 @@ const MAX_PORT = 65535;
|
|
|
88
89
|
|
|
89
90
|
function showHelp() {
|
|
90
91
|
console.log(`
|
|
91
|
-
Clopen -
|
|
92
|
+
Clopen - All-in-one web workspace for Claude Code & OpenCode
|
|
92
93
|
|
|
93
94
|
USAGE:
|
|
94
95
|
clopen [OPTIONS]
|
|
@@ -106,7 +107,7 @@ OPTIONS:
|
|
|
106
107
|
|
|
107
108
|
EXAMPLES:
|
|
108
109
|
clopen # Start with default settings (port ${DEFAULT_PORT})
|
|
109
|
-
clopen --port
|
|
110
|
+
clopen --port 9145 # Start on port 9145
|
|
110
111
|
clopen --host 0.0.0.0 # Bind to all network interfaces
|
|
111
112
|
clopen update # Update to the latest version
|
|
112
113
|
clopen reset-pat # Regenerate admin login token
|
|
@@ -262,8 +263,8 @@ async function recoverAdminToken() {
|
|
|
262
263
|
console.log(`\x1b[36mClopen\x1b[0m v${version} — Admin Token Recovery\n`);
|
|
263
264
|
|
|
264
265
|
// Initialize database (import dynamically to avoid loading full backend)
|
|
265
|
-
const { initializeDatabase } = await import('../backend/
|
|
266
|
-
const { listUsers, regeneratePAT } = await import('../backend/
|
|
266
|
+
const { initializeDatabase } = await import('../backend/database/index');
|
|
267
|
+
const { listUsers, regeneratePAT } = await import('../backend/auth/auth-service');
|
|
267
268
|
|
|
268
269
|
await initializeDatabase();
|
|
269
270
|
|
|
@@ -367,21 +368,20 @@ async function startServer(options: CLIOptions) {
|
|
|
367
368
|
updateLoading('Starting server...');
|
|
368
369
|
await delay();
|
|
369
370
|
|
|
370
|
-
//
|
|
371
|
-
|
|
371
|
+
// Delegate to scripts/start.ts — handles port resolution (IPv4 + IPv6
|
|
372
|
+
// zombie detection) and starts backend in a single consistent path.
|
|
373
|
+
const startScript = join(__dirname, 'scripts/start.ts');
|
|
372
374
|
|
|
373
375
|
stopLoading();
|
|
374
376
|
|
|
375
|
-
//
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
if (options.host)
|
|
381
|
-
env.HOST = options.host;
|
|
382
|
-
}
|
|
377
|
+
// Overlay clopen's own .env on top of process.env to override any
|
|
378
|
+
// pollution from a .env file in the directory where `clopen` was invoked.
|
|
379
|
+
// CLI args take highest priority on top of that.
|
|
380
|
+
const env = { ...process.env, ...loadEnvFile(ENV_FILE) };
|
|
381
|
+
if (options.port) env.PORT = options.port.toString();
|
|
382
|
+
if (options.host) env.HOST = options.host;
|
|
383
383
|
|
|
384
|
-
const serverProc = Bun.spawn(['bun',
|
|
384
|
+
const serverProc = Bun.spawn(['bun', startScript], {
|
|
385
385
|
cwd: __dirname,
|
|
386
386
|
stdout: 'inherit',
|
|
387
387
|
stderr: 'inherit',
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
services:
|
|
2
|
+
clopen:
|
|
3
|
+
build:
|
|
4
|
+
context: .
|
|
5
|
+
dockerfile_inline: |
|
|
6
|
+
FROM oven/bun:latest
|
|
7
|
+
USER root
|
|
8
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
9
|
+
git \
|
|
10
|
+
curl \
|
|
11
|
+
chromium \
|
|
12
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
13
|
+
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
|
|
14
|
+
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
|
|
15
|
+
RUN curl -fsSL https://claude.ai/install.sh | bash
|
|
16
|
+
ENV PATH="/root/.claude/local:$PATH"
|
|
17
|
+
RUN curl -fsSL https://opencode.ai/install | bash
|
|
18
|
+
RUN bun add -g @myrialabs/clopen
|
|
19
|
+
EXPOSE 9141
|
|
20
|
+
CMD ["clopen", "--host", "0.0.0.0"]
|
|
21
|
+
restart: unless-stopped
|
|
22
|
+
ports:
|
|
23
|
+
- "${PORT:-9141}:9141"
|
|
24
|
+
volumes:
|
|
25
|
+
- clopen_data:/root/.clopen
|
|
26
|
+
- ${PROJECTS_DIR:-./projects}:/root/projects
|
|
27
|
+
environment:
|
|
28
|
+
- NODE_ENV=production
|
|
29
|
+
|
|
30
|
+
volumes:
|
|
31
|
+
clopen_data:
|
package/frontend/App.svelte
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import WorkspaceLayout from '$frontend/components/workspace/WorkspaceLayout.svelte';
|
|
5
5
|
import ConnectionBanner from '$frontend/components/common/feedback/ConnectionBanner.svelte';
|
|
6
6
|
import UpdateBanner from '$frontend/components/common/feedback/UpdateBanner.svelte';
|
|
7
|
+
import RestartRequiredModal from '$frontend/components/common/feedback/RestartRequiredModal.svelte';
|
|
7
8
|
import LoadingScreen from '$frontend/components/common/feedback/LoadingScreen.svelte';
|
|
8
9
|
import SetupPage from '$frontend/components/auth/SetupPage.svelte';
|
|
9
10
|
import LoginPage from '$frontend/components/auth/LoginPage.svelte';
|
|
@@ -70,4 +71,6 @@
|
|
|
70
71
|
</WorkspaceLayout>
|
|
71
72
|
</div>
|
|
72
73
|
</div>
|
|
74
|
+
|
|
75
|
+
<RestartRequiredModal />
|
|
73
76
|
{/if}
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
const stepLabels: Record<WizardStep, { label: string; icon: IconName }> = {
|
|
88
88
|
'auth-mode': { label: 'Login', icon: 'lucide:shield' },
|
|
89
89
|
'admin-account': { label: 'Account', icon: 'lucide:user-plus' },
|
|
90
|
-
'engines': { label: 'Engines', icon: 'lucide:
|
|
90
|
+
'engines': { label: 'Engines', icon: 'lucide:plug' },
|
|
91
91
|
'preferences': { label: 'Preferences', icon: 'lucide:palette' }
|
|
92
92
|
};
|
|
93
93
|
|
|
@@ -100,14 +100,41 @@
|
|
|
100
100
|
authModeLoading = true;
|
|
101
101
|
|
|
102
102
|
try {
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
103
|
+
if (!isExistingUser) {
|
|
104
|
+
// Fresh setup — existing behavior
|
|
105
|
+
if (selectedAuthMode === 'none') {
|
|
106
|
+
await authStore.setupNoAuth();
|
|
107
|
+
completedSteps.add('auth-mode');
|
|
108
|
+
completedSteps.add('admin-account');
|
|
109
|
+
completedSteps = new Set(completedSteps);
|
|
110
|
+
currentStep = 'engines';
|
|
111
|
+
} else {
|
|
112
|
+
goToNextStep();
|
|
113
|
+
}
|
|
109
114
|
} else {
|
|
110
|
-
|
|
115
|
+
// Returning user (wizard shown again after refresh) — apply selected mode
|
|
116
|
+
const previousMode = authStore.authMode;
|
|
117
|
+
if (selectedAuthMode === 'none' && previousMode !== 'none') {
|
|
118
|
+
// with-auth → no-auth: update mode, skip admin-account
|
|
119
|
+
await authStore.switchToNoAuth();
|
|
120
|
+
completedSteps.add('auth-mode');
|
|
121
|
+
completedSteps.add('admin-account');
|
|
122
|
+
completedSteps = new Set(completedSteps);
|
|
123
|
+
currentStep = 'engines';
|
|
124
|
+
} else if (selectedAuthMode === 'required' && previousMode !== 'required') {
|
|
125
|
+
// no-auth → with-auth: update mode, regenerate PAT, go to admin-account
|
|
126
|
+
await authStore.switchToWithAuth();
|
|
127
|
+
goToNextStep();
|
|
128
|
+
} else if (selectedAuthMode === 'none') {
|
|
129
|
+
// Same mode (none) — skip admin-account, go to engines
|
|
130
|
+
completedSteps.add('auth-mode');
|
|
131
|
+
completedSteps.add('admin-account');
|
|
132
|
+
completedSteps = new Set(completedSteps);
|
|
133
|
+
currentStep = 'engines';
|
|
134
|
+
} else {
|
|
135
|
+
// Same mode (required) — advance to admin-account
|
|
136
|
+
goToNextStep();
|
|
137
|
+
}
|
|
111
138
|
}
|
|
112
139
|
} catch (err) {
|
|
113
140
|
authModeError = err instanceof Error ? err.message : 'Setup failed';
|
|
@@ -133,11 +160,16 @@
|
|
|
133
160
|
adminLoading = true;
|
|
134
161
|
try {
|
|
135
162
|
if (isExistingUser) {
|
|
136
|
-
// Existing user —
|
|
163
|
+
// Existing user — update name if changed
|
|
137
164
|
if (adminName.trim() !== existingUserName) {
|
|
138
165
|
await authStore.updateName(adminName.trim());
|
|
139
166
|
}
|
|
140
|
-
|
|
167
|
+
// If a PAT was just generated (e.g. switched from no-auth to with-auth), show it
|
|
168
|
+
if (authStore.personalAccessToken) {
|
|
169
|
+
showPAT = true;
|
|
170
|
+
} else {
|
|
171
|
+
goToNextStep();
|
|
172
|
+
}
|
|
141
173
|
} else {
|
|
142
174
|
await authStore.setup(adminName.trim());
|
|
143
175
|
showPAT = true;
|
|
@@ -324,8 +356,8 @@
|
|
|
324
356
|
});
|
|
325
357
|
|
|
326
358
|
// ─── Step 4: Preferences ───
|
|
327
|
-
const FONT_SIZE_MIN =
|
|
328
|
-
const FONT_SIZE_MAX =
|
|
359
|
+
const FONT_SIZE_MIN = 8;
|
|
360
|
+
const FONT_SIZE_MAX = 24;
|
|
329
361
|
|
|
330
362
|
function handleFontSizeChange(e: Event) {
|
|
331
363
|
const value = Number((e.target as HTMLInputElement).value);
|
|
@@ -570,7 +602,7 @@
|
|
|
570
602
|
{:else if currentStep === 'engines'}
|
|
571
603
|
<div class="space-y-4">
|
|
572
604
|
<div class="text-center">
|
|
573
|
-
<h2 class="text-base font-semibold text-slate-900 dark:text-slate-100 mb-1">
|
|
605
|
+
<h2 class="text-base font-semibold text-slate-900 dark:text-slate-100 mb-1">Engines</h2>
|
|
574
606
|
<p class="text-sm text-slate-500 dark:text-slate-400">
|
|
575
607
|
Check your AI engine installations.
|
|
576
608
|
</p>
|
|
@@ -141,7 +141,7 @@
|
|
|
141
141
|
|
|
142
142
|
const chatPlaceholder = $derived.by(() => {
|
|
143
143
|
if (chatBlockedReason === 'no-claude-account') {
|
|
144
|
-
return 'No Claude Code account connected. Configure it in Settings →
|
|
144
|
+
return 'No Claude Code account connected. Configure it in Settings → Engines → Claude Code → Accounts.';
|
|
145
145
|
}
|
|
146
146
|
if (chatBlockedReason === 'no-model') {
|
|
147
147
|
return 'No model selected. Please select a model to start chatting.';
|
|
@@ -24,7 +24,9 @@
|
|
|
24
24
|
import TokenUsageModal from '../modal/TokenUsageModal.svelte';
|
|
25
25
|
import DebugModal from '../modal/DebugModal.svelte';
|
|
26
26
|
import Dialog from '$frontend/components/common/overlay/Dialog.svelte';
|
|
27
|
-
import
|
|
27
|
+
import ConflictResolutionModal from '$frontend/components/checkpoint/ConflictResolutionModal.svelte';
|
|
28
|
+
import { snapshotService } from '$frontend/services/snapshot/snapshot.service';
|
|
29
|
+
import type { RestoreConflict, ConflictResolution } from '$frontend/services/snapshot/snapshot.service';
|
|
28
30
|
|
|
29
31
|
const {
|
|
30
32
|
message,
|
|
@@ -39,6 +41,11 @@
|
|
|
39
41
|
let showTokenUsagePopup = $state(false);
|
|
40
42
|
let showRestoreConfirm = $state(false);
|
|
41
43
|
|
|
44
|
+
// Conflict resolution state
|
|
45
|
+
let showConflictModal = $state(false);
|
|
46
|
+
let conflictList = $state<RestoreConflict[]>([]);
|
|
47
|
+
let processingRestore = $state(false);
|
|
48
|
+
|
|
42
49
|
// Format timestamp
|
|
43
50
|
const formatTime = (timestamp?: string) => {
|
|
44
51
|
if (!timestamp) return 'Unknown';
|
|
@@ -268,15 +275,8 @@
|
|
|
268
275
|
showDebugPopup = false;
|
|
269
276
|
}
|
|
270
277
|
|
|
271
|
-
// Handle restore button click
|
|
278
|
+
// Handle restore button click - check conflicts first
|
|
272
279
|
async function handleRestore() {
|
|
273
|
-
showRestoreConfirm = true;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Confirm restore action
|
|
277
|
-
async function confirmRestore() {
|
|
278
|
-
showRestoreConfirm = false;
|
|
279
|
-
|
|
280
280
|
if (!messageId) {
|
|
281
281
|
addNotification({
|
|
282
282
|
type: 'error',
|
|
@@ -287,16 +287,41 @@
|
|
|
287
287
|
return;
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
const currentSessionId = sessionState.currentSession?.id;
|
|
291
|
+
if (!currentSessionId) return;
|
|
292
|
+
|
|
290
293
|
try {
|
|
291
|
-
|
|
292
|
-
|
|
294
|
+
const conflictCheck = await snapshotService.checkConflicts(messageId, currentSessionId);
|
|
295
|
+
|
|
296
|
+
if (conflictCheck.hasConflicts) {
|
|
297
|
+
// Show conflict resolution modal
|
|
298
|
+
conflictList = conflictCheck.conflicts;
|
|
299
|
+
showConflictModal = true;
|
|
300
|
+
} else {
|
|
301
|
+
// No conflicts - show simple confirmation
|
|
302
|
+
showRestoreConfirm = true;
|
|
303
|
+
}
|
|
304
|
+
} catch (error) {
|
|
305
|
+
debug.error('chat', 'Error checking conflicts:', error);
|
|
306
|
+
// Fallback to simple confirmation
|
|
307
|
+
showRestoreConfirm = true;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Execute restore with optional conflict resolutions
|
|
312
|
+
async function executeRestore(resolutions?: ConflictResolution) {
|
|
313
|
+
if (!messageId || !sessionState.currentSession?.id) return;
|
|
314
|
+
|
|
315
|
+
processingRestore = true;
|
|
316
|
+
|
|
317
|
+
try {
|
|
318
|
+
await snapshotService.restore(
|
|
293
319
|
messageId,
|
|
294
|
-
|
|
295
|
-
|
|
320
|
+
sessionState.currentSession.id,
|
|
321
|
+
resolutions
|
|
322
|
+
);
|
|
296
323
|
|
|
297
|
-
|
|
298
|
-
await loadMessagesForSession(sessionState.currentSession.id);
|
|
299
|
-
}
|
|
324
|
+
await loadMessagesForSession(sessionState.currentSession.id);
|
|
300
325
|
} catch (error) {
|
|
301
326
|
debug.error('chat', 'Restore error:', error);
|
|
302
327
|
addNotification({
|
|
@@ -305,9 +330,22 @@
|
|
|
305
330
|
message: error instanceof Error ? error.message : 'Unknown error',
|
|
306
331
|
duration: 5000
|
|
307
332
|
});
|
|
333
|
+
} finally {
|
|
334
|
+
processingRestore = false;
|
|
308
335
|
}
|
|
309
336
|
}
|
|
310
337
|
|
|
338
|
+
// Confirm simple restore (no conflicts)
|
|
339
|
+
async function confirmRestore() {
|
|
340
|
+
showRestoreConfirm = false;
|
|
341
|
+
await executeRestore();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Confirm restore with conflict resolutions
|
|
345
|
+
async function confirmConflictRestore(resolutions: ConflictResolution) {
|
|
346
|
+
await executeRestore(resolutions);
|
|
347
|
+
}
|
|
348
|
+
|
|
311
349
|
// Handle edit button click
|
|
312
350
|
async function handleEdit() {
|
|
313
351
|
if (!messageId) {
|
|
@@ -473,6 +511,16 @@ This will restore your conversation to this point.`}
|
|
|
473
511
|
}}
|
|
474
512
|
/>
|
|
475
513
|
|
|
514
|
+
<!-- Conflict Resolution Modal -->
|
|
515
|
+
<ConflictResolutionModal
|
|
516
|
+
bind:isOpen={showConflictModal}
|
|
517
|
+
conflicts={conflictList}
|
|
518
|
+
onConfirm={confirmConflictRestore}
|
|
519
|
+
onClose={() => {
|
|
520
|
+
conflictList = [];
|
|
521
|
+
}}
|
|
522
|
+
/>
|
|
523
|
+
|
|
476
524
|
<style>
|
|
477
525
|
/* Animate chevron icon when details are opened */
|
|
478
526
|
:global(details[open] summary .iconify) {
|