@kirosnn/mosaic 0.71.0 → 0.73.0
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/README.md +1 -5
- package/package.json +4 -2
- package/src/agent/Agent.ts +353 -131
- package/src/agent/context.ts +4 -4
- package/src/agent/prompts/systemPrompt.ts +15 -6
- package/src/agent/prompts/toolsPrompt.ts +75 -10
- package/src/agent/provider/anthropic.ts +100 -100
- package/src/agent/provider/google.ts +102 -102
- package/src/agent/provider/mistral.ts +95 -95
- package/src/agent/provider/ollama.ts +77 -60
- package/src/agent/provider/openai.ts +42 -38
- package/src/agent/provider/rateLimit.ts +178 -0
- package/src/agent/provider/xai.ts +99 -99
- package/src/agent/tools/definitions.ts +19 -9
- package/src/agent/tools/executor.ts +95 -85
- package/src/agent/tools/exploreExecutor.ts +8 -10
- package/src/agent/tools/grep.ts +30 -29
- package/src/agent/tools/question.ts +7 -1
- package/src/agent/types.ts +9 -8
- package/src/components/App.tsx +45 -45
- package/src/components/CustomInput.tsx +214 -36
- package/src/components/Main.tsx +1146 -954
- package/src/components/Setup.tsx +1 -1
- package/src/components/Welcome.tsx +1 -1
- package/src/components/main/ApprovalPanel.tsx +4 -3
- package/src/components/main/ChatPage.tsx +858 -675
- package/src/components/main/HomePage.tsx +53 -38
- package/src/components/main/QuestionPanel.tsx +52 -7
- package/src/components/main/ThinkingIndicator.tsx +2 -1
- package/src/index.tsx +50 -20
- package/src/mcp/approvalPolicy.ts +148 -0
- package/src/mcp/cli/add.ts +185 -0
- package/src/mcp/cli/doctor.ts +77 -0
- package/src/mcp/cli/index.ts +85 -0
- package/src/mcp/cli/list.ts +50 -0
- package/src/mcp/cli/logs.ts +24 -0
- package/src/mcp/cli/manage.ts +99 -0
- package/src/mcp/cli/show.ts +53 -0
- package/src/mcp/cli/tools.ts +77 -0
- package/src/mcp/config.ts +223 -0
- package/src/mcp/index.ts +80 -0
- package/src/mcp/processManager.ts +299 -0
- package/src/mcp/rateLimiter.ts +50 -0
- package/src/mcp/registry.ts +151 -0
- package/src/mcp/schemaConverter.ts +100 -0
- package/src/mcp/servers/navigation.ts +854 -0
- package/src/mcp/toolCatalog.ts +169 -0
- package/src/mcp/types.ts +95 -0
- package/src/utils/approvalBridge.ts +17 -5
- package/src/utils/commands/compact.ts +30 -0
- package/src/utils/commands/echo.ts +1 -1
- package/src/utils/commands/index.ts +4 -6
- package/src/utils/commands/new.ts +15 -0
- package/src/utils/commands/types.ts +3 -0
- package/src/utils/config.ts +3 -1
- package/src/utils/diffRendering.tsx +1 -3
- package/src/utils/exploreBridge.ts +10 -0
- package/src/utils/markdown.tsx +163 -99
- package/src/utils/models.ts +31 -9
- package/src/utils/questionBridge.ts +36 -1
- package/src/utils/tokenEstimator.ts +32 -0
- package/src/utils/toolFormatting.ts +268 -7
- package/src/web/app.tsx +72 -72
- package/src/web/components/HomePage.tsx +7 -7
- package/src/web/components/MessageItem.tsx +22 -22
- package/src/web/components/QuestionPanel.tsx +72 -12
- package/src/web/components/Sidebar.tsx +0 -2
- package/src/web/components/ThinkingIndicator.tsx +1 -0
- package/src/web/server.tsx +767 -683
- package/src/utils/commands/redo.ts +0 -74
- package/src/utils/commands/sessions.ts +0 -129
- package/src/utils/commands/undo.ts +0 -75
- package/src/utils/undoRedo.ts +0 -429
- package/src/utils/undoRedoBridge.ts +0 -45
- package/src/utils/undoRedoDb.ts +0 -338
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import type { Command, CommandResult } from './types';
|
|
2
|
-
import { redo, canRedo, isGitRepository } from '../undoRedo';
|
|
3
|
-
import { notifyUndoRedo } from '../undoRedoBridge';
|
|
4
|
-
|
|
5
|
-
export const redoCommand: Command = {
|
|
6
|
-
name: 'redo',
|
|
7
|
-
description: 'Redo a previously undone message. Only available after using /undo. Any file changes will also be restored. Internally, this uses Git to manage the file changes if available.',
|
|
8
|
-
usage: '/redo',
|
|
9
|
-
aliases: ['r'],
|
|
10
|
-
execute: async (): Promise<CommandResult> => {
|
|
11
|
-
if (!canRedo()) {
|
|
12
|
-
return {
|
|
13
|
-
success: false,
|
|
14
|
-
content: 'Nothing to redo. Use /undo first, or the redo stack is empty.',
|
|
15
|
-
shouldAddToHistory: false
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const result = redo();
|
|
20
|
-
if (!result) {
|
|
21
|
-
return {
|
|
22
|
-
success: false,
|
|
23
|
-
content: 'Failed to redo. Could not retrieve state.',
|
|
24
|
-
shouldAddToHistory: false
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (!result.success) {
|
|
29
|
-
return {
|
|
30
|
-
success: false,
|
|
31
|
-
content: `Failed to redo file changes:\n${result.error || 'Unknown error'}`,
|
|
32
|
-
shouldAddToHistory: false
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
notifyUndoRedo(result.state, 'redo');
|
|
37
|
-
|
|
38
|
-
const messageCountText = result.state.messages.length === 0 ? 'conversation restored' : `${result.state.messages.length} message(s) restored`;
|
|
39
|
-
|
|
40
|
-
let details = '';
|
|
41
|
-
|
|
42
|
-
if (result.gitChanges && result.gitChanges.length > 0) {
|
|
43
|
-
const fileDetails = result.gitChanges.map(f => {
|
|
44
|
-
switch (f.status) {
|
|
45
|
-
case 'M': return ` • ${f.path} (changes reapplied)`;
|
|
46
|
-
case 'A': return ` • ${f.path} (created/restored)`;
|
|
47
|
-
case 'D': return ` • ${f.path} (deleted)`;
|
|
48
|
-
case 'R': return ` • ${f.path} (renamed)`;
|
|
49
|
-
default: return ` • ${f.path} (status: ${f.status})`;
|
|
50
|
-
}
|
|
51
|
-
}).join('\n');
|
|
52
|
-
details = `\n- Files affected (${result.gitChanges.length}):\n${fileDetails}`;
|
|
53
|
-
} else if (result.state.fileSnapshots.length > 0) {
|
|
54
|
-
const fileDetails = result.state.fileSnapshots.map(f => {
|
|
55
|
-
if (!f.existed) {
|
|
56
|
-
return ` • ${f.path} (recreated)`;
|
|
57
|
-
} else if (f.content === '') {
|
|
58
|
-
return ` • ${f.path} (deleted)`;
|
|
59
|
-
} else {
|
|
60
|
-
return ` • ${f.path} (restored)`;
|
|
61
|
-
}
|
|
62
|
-
}).join('\n');
|
|
63
|
-
details = `\n- Files affected (${result.state.fileSnapshots.length}):\n${fileDetails}`;
|
|
64
|
-
} else if (result.state.gitCommitHash) {
|
|
65
|
-
details = `\n- Files restored via Git (commit: ${result.state.gitCommitHash.slice(0, 7)})`;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return {
|
|
69
|
-
success: true,
|
|
70
|
-
content: `Redone: conversation and file changes restored.${details}\n- ${messageCountText}`,
|
|
71
|
-
shouldAddToHistory: false
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
};
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import type { Command, CommandResult } from './types';
|
|
2
|
-
import { getAllSessions, setCurrentSession, deleteSession } from '../undoRedoDb';
|
|
3
|
-
|
|
4
|
-
export const sessionsCommand: Command = {
|
|
5
|
-
name: 'sessions',
|
|
6
|
-
description: 'Manage undo/redo sessions. List all sessions or switch to a specific session.',
|
|
7
|
-
usage: '/sessions [list|switch <session-id>|delete <session-id>]',
|
|
8
|
-
aliases: ['s'],
|
|
9
|
-
execute: async (args: string[], _fullCommand: string): Promise<CommandResult> => {
|
|
10
|
-
const parts = args;
|
|
11
|
-
const action = parts[0] || 'list';
|
|
12
|
-
|
|
13
|
-
if (action === 'list') {
|
|
14
|
-
const sessions = getAllSessions();
|
|
15
|
-
|
|
16
|
-
if (sessions.length === 0) {
|
|
17
|
-
return {
|
|
18
|
-
success: true,
|
|
19
|
-
content: 'No sessions found.',
|
|
20
|
-
shouldAddToHistory: false
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const lines = ['Available sessions:\n'];
|
|
25
|
-
for (const session of sessions) {
|
|
26
|
-
const current = session.isCurrent ? ' (current)' : '';
|
|
27
|
-
const date = new Date(session.lastAccessedAt).toLocaleString();
|
|
28
|
-
lines.push(`- ${session.id}${current}`);
|
|
29
|
-
lines.push(` Last accessed: ${date}`);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return {
|
|
33
|
-
success: true,
|
|
34
|
-
content: lines.join('\n'),
|
|
35
|
-
shouldAddToHistory: false
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (action === 'switch') {
|
|
40
|
-
const sessionId = parts[1];
|
|
41
|
-
if (!sessionId) {
|
|
42
|
-
return {
|
|
43
|
-
success: false,
|
|
44
|
-
content: 'Please provide a session ID to switch to.\nUsage: /sessions switch <session-id>',
|
|
45
|
-
shouldAddToHistory: false
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const sessions = getAllSessions();
|
|
50
|
-
const targetSession = sessions.find(s => s.id === sessionId);
|
|
51
|
-
|
|
52
|
-
if (!targetSession) {
|
|
53
|
-
return {
|
|
54
|
-
success: false,
|
|
55
|
-
content: `Session not found: ${sessionId}`,
|
|
56
|
-
shouldAddToHistory: false
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
setCurrentSession(sessionId);
|
|
62
|
-
return {
|
|
63
|
-
success: true,
|
|
64
|
-
content: `Switched to session: ${sessionId}`,
|
|
65
|
-
shouldAddToHistory: false
|
|
66
|
-
};
|
|
67
|
-
} catch (error) {
|
|
68
|
-
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
69
|
-
return {
|
|
70
|
-
success: false,
|
|
71
|
-
content: `Failed to switch session: ${errorMsg}`,
|
|
72
|
-
shouldAddToHistory: false
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (action === 'delete') {
|
|
78
|
-
const sessionId = parts[1];
|
|
79
|
-
if (!sessionId) {
|
|
80
|
-
return {
|
|
81
|
-
success: false,
|
|
82
|
-
content: 'Please provide a session ID to delete.\nUsage: /sessions delete <session-id>',
|
|
83
|
-
shouldAddToHistory: false
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const sessions = getAllSessions();
|
|
88
|
-
const targetSession = sessions.find(s => s.id === sessionId);
|
|
89
|
-
|
|
90
|
-
if (!targetSession) {
|
|
91
|
-
return {
|
|
92
|
-
success: false,
|
|
93
|
-
content: `Session not found: ${sessionId}`,
|
|
94
|
-
shouldAddToHistory: false
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (targetSession.isCurrent) {
|
|
99
|
-
return {
|
|
100
|
-
success: false,
|
|
101
|
-
content: 'Cannot delete the current session. Switch to another session first.',
|
|
102
|
-
shouldAddToHistory: false
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
try {
|
|
107
|
-
deleteSession(sessionId);
|
|
108
|
-
return {
|
|
109
|
-
success: true,
|
|
110
|
-
content: `Deleted session: ${sessionId}`,
|
|
111
|
-
shouldAddToHistory: false
|
|
112
|
-
};
|
|
113
|
-
} catch (error) {
|
|
114
|
-
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
115
|
-
return {
|
|
116
|
-
success: false,
|
|
117
|
-
content: `Failed to delete session: ${errorMsg}`,
|
|
118
|
-
shouldAddToHistory: false
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return {
|
|
124
|
-
success: false,
|
|
125
|
-
content: `Unknown action: ${action}\nUsage: /sessions [list|switch <session-id>|delete <session-id>]`,
|
|
126
|
-
shouldAddToHistory: false
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
};
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import type { Command, CommandResult } from './types';
|
|
2
|
-
import { undo, canUndo, isGitRepository } from '../undoRedo';
|
|
3
|
-
import { notifyUndoRedo } from '../undoRedoBridge';
|
|
4
|
-
|
|
5
|
-
export const undoCommand: Command = {
|
|
6
|
-
name: 'undo',
|
|
7
|
-
description: 'Undo last message in the conversation. Removes the most recent user message, all subsequent responses, and any file changes. Any file changes made will also be reverted. Internally, this uses Git to manage the file changes if available (local repository works too).',
|
|
8
|
-
usage: '/undo',
|
|
9
|
-
aliases: ['u'],
|
|
10
|
-
execute: async (): Promise<CommandResult> => {
|
|
11
|
-
if (!canUndo()) {
|
|
12
|
-
return {
|
|
13
|
-
success: false,
|
|
14
|
-
content: 'Nothing to undo. The undo stack is empty.',
|
|
15
|
-
shouldAddToHistory: false
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const result = undo();
|
|
20
|
-
if (!result) {
|
|
21
|
-
return {
|
|
22
|
-
success: false,
|
|
23
|
-
content: 'Failed to undo. Could not retrieve previous state.',
|
|
24
|
-
shouldAddToHistory: false
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (!result.success) {
|
|
29
|
-
return {
|
|
30
|
-
success: false,
|
|
31
|
-
content: `Failed to undo file changes:\n${result.error || 'Unknown error'}`,
|
|
32
|
-
shouldAddToHistory: false
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
notifyUndoRedo(result.state, 'undo');
|
|
37
|
-
|
|
38
|
-
const messageCountText = result.state.messages.length === 0 ? 'conversation cleared' : `${result.state.messages.length} message(s) restored`;
|
|
39
|
-
|
|
40
|
-
let details = '';
|
|
41
|
-
|
|
42
|
-
if (result.gitChanges && result.gitChanges.length > 0) {
|
|
43
|
-
const fileDetails = result.gitChanges.map(f => {
|
|
44
|
-
switch (f.status) {
|
|
45
|
-
case 'M': return ` • ${f.path} (reverted changes)`;
|
|
46
|
-
case 'A': return ` • ${f.path} (restored - was deleted)`;
|
|
47
|
-
case 'D': return ` • ${f.path} (deleted - was created)`;
|
|
48
|
-
case 'R': return ` • ${f.path} (renamed back)`;
|
|
49
|
-
default: return ` • ${f.path} (status: ${f.status})`;
|
|
50
|
-
}
|
|
51
|
-
}).join('\n');
|
|
52
|
-
details = `\n- Files affected (${result.gitChanges.length}):\n${fileDetails}`;
|
|
53
|
-
} else if (result.state.fileSnapshots.length > 0) {
|
|
54
|
-
const filesAffected = result.state.fileSnapshots.map(f => f.path).join(', ');
|
|
55
|
-
const fileDetails = result.state.fileSnapshots.map(f => {
|
|
56
|
-
if (!f.existed) {
|
|
57
|
-
return ` • ${f.path} (deleted - was created)`;
|
|
58
|
-
} else if (f.content === '') {
|
|
59
|
-
return ` • ${f.path} (restored - was deleted)`;
|
|
60
|
-
} else {
|
|
61
|
-
return ` • ${f.path} (restored)`;
|
|
62
|
-
}
|
|
63
|
-
}).join('\n');
|
|
64
|
-
details = `\n- Files affected (${result.state.fileSnapshots.length}):\n${fileDetails}`;
|
|
65
|
-
} else if (result.state.gitCommitHash) {
|
|
66
|
-
details = `\n- Files reverted via Git (commit: ${result.state.gitCommitHash.slice(0, 7)})`;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return {
|
|
70
|
-
success: true,
|
|
71
|
-
content: `Undone last user message and all responses.${details}\n- ${messageCountText}`,
|
|
72
|
-
shouldAddToHistory: false
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
};
|
package/src/utils/undoRedo.ts
DELETED
|
@@ -1,429 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, writeFileSync, unlinkSync } from 'fs';
|
|
2
|
-
import { join, resolve } from 'path';
|
|
3
|
-
import { execSync } from 'child_process';
|
|
4
|
-
import type { Message } from '../components/main/types';
|
|
5
|
-
import {
|
|
6
|
-
createSession,
|
|
7
|
-
getCurrentSession,
|
|
8
|
-
pushUndoState,
|
|
9
|
-
popUndoState,
|
|
10
|
-
pushRedoState,
|
|
11
|
-
popRedoState,
|
|
12
|
-
clearRedoStates,
|
|
13
|
-
getUndoCount,
|
|
14
|
-
getRedoCount,
|
|
15
|
-
cleanupOldSessions,
|
|
16
|
-
type UndoRedoState,
|
|
17
|
-
type FileSnapshot
|
|
18
|
-
} from './undoRedoDb';
|
|
19
|
-
|
|
20
|
-
export type { UndoRedoState, FileSnapshot };
|
|
21
|
-
|
|
22
|
-
let currentSessionId: string | null = null;
|
|
23
|
-
let pendingFileSnapshots: FileSnapshot[] = [];
|
|
24
|
-
|
|
25
|
-
function getWorkspaceMosaicDir(): string {
|
|
26
|
-
const workspace = process.cwd();
|
|
27
|
-
const mosaicDir = join(workspace, '.mosaic');
|
|
28
|
-
|
|
29
|
-
if (!existsSync(mosaicDir)) {
|
|
30
|
-
mkdirSync(mosaicDir, { recursive: true });
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return mosaicDir;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function generateId(): string {
|
|
37
|
-
return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function isGitRepository(): boolean {
|
|
41
|
-
try {
|
|
42
|
-
execSync('git rev-parse --git-dir', { stdio: 'ignore', cwd: process.cwd() });
|
|
43
|
-
return true;
|
|
44
|
-
} catch {
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function getGitStatus(): { clean: boolean; hasChanges: boolean } {
|
|
50
|
-
try {
|
|
51
|
-
const status = execSync('git status --porcelain', { encoding: 'utf-8', cwd: process.cwd() });
|
|
52
|
-
const hasChanges = status.trim().length > 0;
|
|
53
|
-
return { clean: !hasChanges, hasChanges };
|
|
54
|
-
} catch {
|
|
55
|
-
return { clean: false, hasChanges: false };
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function initializeSession(): void {
|
|
60
|
-
cleanupOldSessions(7);
|
|
61
|
-
|
|
62
|
-
const existingSession = getCurrentSession();
|
|
63
|
-
if (existingSession) {
|
|
64
|
-
currentSessionId = existingSession.id;
|
|
65
|
-
} else {
|
|
66
|
-
currentSessionId = createSession();
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
pendingFileSnapshots = [];
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function getCurrentSessionId(): string | null {
|
|
73
|
-
return currentSessionId;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function captureFileSnapshot(filePath: string): void {
|
|
77
|
-
const workspace = process.cwd();
|
|
78
|
-
const fullPath = resolve(workspace, filePath);
|
|
79
|
-
|
|
80
|
-
let content = '';
|
|
81
|
-
let existed = false;
|
|
82
|
-
|
|
83
|
-
try {
|
|
84
|
-
const fs = require('fs');
|
|
85
|
-
content = fs.readFileSync(fullPath, 'utf-8');
|
|
86
|
-
existed = true;
|
|
87
|
-
} catch {
|
|
88
|
-
existed = false;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const alreadyCaptured = pendingFileSnapshots.some(s => s.path === filePath);
|
|
92
|
-
if (!alreadyCaptured) {
|
|
93
|
-
pendingFileSnapshots.push({
|
|
94
|
-
path: filePath,
|
|
95
|
-
content,
|
|
96
|
-
existed
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function createGitCommit(message: string): string | null {
|
|
102
|
-
try {
|
|
103
|
-
const workspace = process.cwd();
|
|
104
|
-
|
|
105
|
-
execSync('git add -A', { cwd: workspace, stdio: 'ignore' });
|
|
106
|
-
|
|
107
|
-
execSync(`git commit -m "${message.replace(/"/g, '\\"')}"`, {
|
|
108
|
-
cwd: workspace,
|
|
109
|
-
stdio: 'ignore',
|
|
110
|
-
env: {
|
|
111
|
-
...process.env,
|
|
112
|
-
GIT_AUTHOR_NAME: 'Mosaic',
|
|
113
|
-
GIT_AUTHOR_EMAIL: 'mosaic@local',
|
|
114
|
-
GIT_COMMITTER_NAME: 'Mosaic',
|
|
115
|
-
GIT_COMMITTER_EMAIL: 'mosaic@local'
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
const hash = execSync('git rev-parse HEAD', { encoding: 'utf-8', cwd: workspace }).trim();
|
|
120
|
-
return hash;
|
|
121
|
-
} catch (error) {
|
|
122
|
-
console.error('Failed to create Git commit:', error);
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export function saveState(messages: Message[]): void {
|
|
128
|
-
if (!currentSessionId) {
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const useGit = isGitRepository();
|
|
133
|
-
let gitCommitHash: string | undefined;
|
|
134
|
-
|
|
135
|
-
if (useGit) {
|
|
136
|
-
const gitStatus = getGitStatus();
|
|
137
|
-
if (gitStatus.hasChanges) {
|
|
138
|
-
const hash = createGitCommit(`[Mosaic] Save state - ${new Date().toISOString()}`);
|
|
139
|
-
if (hash) {
|
|
140
|
-
gitCommitHash = hash;
|
|
141
|
-
}
|
|
142
|
-
} else {
|
|
143
|
-
try {
|
|
144
|
-
const currentHash = execSync('git rev-parse HEAD', { encoding: 'utf-8', cwd: process.cwd() }).trim();
|
|
145
|
-
gitCommitHash = currentHash;
|
|
146
|
-
} catch {
|
|
147
|
-
gitCommitHash = undefined;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const state: UndoRedoState = {
|
|
153
|
-
id: generateId(),
|
|
154
|
-
timestamp: Date.now(),
|
|
155
|
-
messages: JSON.parse(JSON.stringify(messages)),
|
|
156
|
-
gitCommitHash,
|
|
157
|
-
fileSnapshots: JSON.parse(JSON.stringify(pendingFileSnapshots)),
|
|
158
|
-
useGit
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
pushUndoState(currentSessionId, state);
|
|
162
|
-
clearRedoStates(currentSessionId);
|
|
163
|
-
pendingFileSnapshots = [];
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function restoreFileSnapshots(snapshots: FileSnapshot[]): { success: boolean; errors: string[] } {
|
|
167
|
-
const errors: string[] = [];
|
|
168
|
-
const workspace = process.cwd();
|
|
169
|
-
|
|
170
|
-
for (const snapshot of snapshots) {
|
|
171
|
-
try {
|
|
172
|
-
const fullPath = resolve(workspace, snapshot.path);
|
|
173
|
-
if (snapshot.existed) {
|
|
174
|
-
writeFileSync(fullPath, snapshot.content, 'utf-8');
|
|
175
|
-
} else {
|
|
176
|
-
if (existsSync(fullPath)) {
|
|
177
|
-
unlinkSync(fullPath);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
} catch (error) {
|
|
181
|
-
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
182
|
-
errors.push(`Failed to restore ${snapshot.path}: ${errorMsg}`);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return {
|
|
187
|
-
success: errors.length === 0,
|
|
188
|
-
errors
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export function canUndo(): boolean {
|
|
193
|
-
if (!currentSessionId) return false;
|
|
194
|
-
return getUndoCount(currentSessionId) > 0;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
export function canRedo(): boolean {
|
|
198
|
-
if (!currentSessionId) return false;
|
|
199
|
-
return getRedoCount(currentSessionId) > 0;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
function getUntrackedFiles(): string[] {
|
|
204
|
-
try {
|
|
205
|
-
const workspace = process.cwd();
|
|
206
|
-
const output = execSync('git ls-files --others --exclude-standard', { encoding: 'utf-8', cwd: workspace });
|
|
207
|
-
return output.trim().split('\n').filter(Boolean);
|
|
208
|
-
} catch {
|
|
209
|
-
return [];
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function getFileSnapshot(filePath: string): FileSnapshot {
|
|
214
|
-
const workspace = process.cwd();
|
|
215
|
-
const fullPath = resolve(workspace, filePath);
|
|
216
|
-
let content = '';
|
|
217
|
-
let existed = false;
|
|
218
|
-
|
|
219
|
-
try {
|
|
220
|
-
if (existsSync(fullPath)) {
|
|
221
|
-
const fs = require('fs');
|
|
222
|
-
content = fs.readFileSync(fullPath, 'utf-8');
|
|
223
|
-
existed = true;
|
|
224
|
-
}
|
|
225
|
-
} catch {
|
|
226
|
-
existed = false;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
return { path: filePath, content, existed };
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function getGitChanges(targetHash: string): FileChange[] {
|
|
233
|
-
if (!isGitRepository()) {
|
|
234
|
-
return [];
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
try {
|
|
238
|
-
const workspace = process.cwd();
|
|
239
|
-
const diffOutput = execSync(`git diff --name-status HEAD ${targetHash}`, {
|
|
240
|
-
encoding: 'utf-8',
|
|
241
|
-
cwd: workspace
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
const changes = diffOutput
|
|
245
|
-
.trim()
|
|
246
|
-
.split('\n')
|
|
247
|
-
.filter(line => line.length > 0)
|
|
248
|
-
.map(line => {
|
|
249
|
-
const parts = line.split('\t');
|
|
250
|
-
const status = parts[0];
|
|
251
|
-
const pathParts = parts.slice(1);
|
|
252
|
-
|
|
253
|
-
if (!status) return null;
|
|
254
|
-
|
|
255
|
-
return {
|
|
256
|
-
status: status.charAt(0),
|
|
257
|
-
path: pathParts.join('\t')
|
|
258
|
-
};
|
|
259
|
-
})
|
|
260
|
-
.filter((item): item is FileChange => item !== null);
|
|
261
|
-
|
|
262
|
-
const untracked = getUntrackedFiles();
|
|
263
|
-
const untrackedChanges = untracked.map(path => ({
|
|
264
|
-
status: 'D',
|
|
265
|
-
path
|
|
266
|
-
}));
|
|
267
|
-
|
|
268
|
-
return [...changes, ...untrackedChanges];
|
|
269
|
-
} catch (error) {
|
|
270
|
-
console.error('Failed to get git changes:', error);
|
|
271
|
-
return [];
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
export interface FileChange {
|
|
276
|
-
path: string;
|
|
277
|
-
status: string;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
export function undo(): { state: UndoRedoState; success: boolean; error?: string; currentState?: UndoRedoState; gitChanges?: FileChange[] } | null {
|
|
281
|
-
if (!currentSessionId || !canUndo()) {
|
|
282
|
-
return null;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const workspace = process.cwd();
|
|
286
|
-
const useGit = isGitRepository();
|
|
287
|
-
|
|
288
|
-
let currentStateForRedo: UndoRedoState;
|
|
289
|
-
|
|
290
|
-
try {
|
|
291
|
-
if (useGit) {
|
|
292
|
-
let currentHash: string | undefined;
|
|
293
|
-
try {
|
|
294
|
-
currentHash = execSync('git rev-parse HEAD', { encoding: 'utf-8', cwd: workspace }).trim();
|
|
295
|
-
} catch {
|
|
296
|
-
currentHash = undefined;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const untrackedFiles = getUntrackedFiles();
|
|
300
|
-
const untrackedSnapshots = untrackedFiles.map(f => getFileSnapshot(f));
|
|
301
|
-
|
|
302
|
-
currentStateForRedo = {
|
|
303
|
-
id: generateId(),
|
|
304
|
-
timestamp: Date.now(),
|
|
305
|
-
messages: [],
|
|
306
|
-
gitCommitHash: currentHash,
|
|
307
|
-
fileSnapshots: untrackedSnapshots,
|
|
308
|
-
useGit: true
|
|
309
|
-
};
|
|
310
|
-
} else {
|
|
311
|
-
currentStateForRedo = {
|
|
312
|
-
id: generateId(),
|
|
313
|
-
timestamp: Date.now(),
|
|
314
|
-
messages: [],
|
|
315
|
-
gitCommitHash: undefined,
|
|
316
|
-
fileSnapshots: pendingFileSnapshots.length > 0 ? JSON.parse(JSON.stringify(pendingFileSnapshots)) : [],
|
|
317
|
-
useGit: false
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
} catch (error) {
|
|
321
|
-
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
322
|
-
return { state: { id: '', timestamp: 0, messages: [], fileSnapshots: [], useGit: false }, success: false, error: errorMsg };
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const state = popUndoState(currentSessionId);
|
|
326
|
-
if (!state) {
|
|
327
|
-
return null;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
let gitChanges: FileChange[] = [];
|
|
331
|
-
|
|
332
|
-
try {
|
|
333
|
-
if (state.useGit && state.gitCommitHash) {
|
|
334
|
-
gitChanges = getGitChanges(state.gitCommitHash);
|
|
335
|
-
|
|
336
|
-
const filesToClean = getUntrackedFiles();
|
|
337
|
-
|
|
338
|
-
execSync(`git reset --hard ${state.gitCommitHash}`, { cwd: workspace, stdio: 'ignore' });
|
|
339
|
-
execSync('git clean -fd', { cwd: workspace, stdio: 'ignore' });
|
|
340
|
-
if (filesToClean.length > 0) {
|
|
341
|
-
const fs = require('fs');
|
|
342
|
-
for (const file of filesToClean) {
|
|
343
|
-
const fullPath = resolve(workspace, file);
|
|
344
|
-
if (existsSync(fullPath)) {
|
|
345
|
-
try {
|
|
346
|
-
const stat = fs.lstatSync(fullPath);
|
|
347
|
-
if (stat.isDirectory()) {
|
|
348
|
-
fs.rmSync(fullPath, { recursive: true, force: true });
|
|
349
|
-
} else {
|
|
350
|
-
fs.unlinkSync(fullPath);
|
|
351
|
-
}
|
|
352
|
-
} catch (cleanupError) {
|
|
353
|
-
console.error(`Failed to force delete ${file}:`, cleanupError);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
} else {
|
|
359
|
-
const restoreResult = restoreFileSnapshots(state.fileSnapshots);
|
|
360
|
-
if (!restoreResult.success) {
|
|
361
|
-
throw new Error(restoreResult.errors.join('\n'));
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
pushRedoState(currentSessionId, currentStateForRedo);
|
|
366
|
-
|
|
367
|
-
return { state, success: true, currentState: currentStateForRedo, gitChanges };
|
|
368
|
-
} catch (error) {
|
|
369
|
-
pushUndoState(currentSessionId, state);
|
|
370
|
-
|
|
371
|
-
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
372
|
-
return { state, success: false, error: errorMsg };
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
export function redo(): { state: UndoRedoState; success: boolean; error?: string; gitChanges?: FileChange[] } | null {
|
|
377
|
-
if (!currentSessionId || !canRedo()) {
|
|
378
|
-
return null;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
const state = popRedoState(currentSessionId);
|
|
382
|
-
if (!state) {
|
|
383
|
-
return null;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
let gitChanges: FileChange[] = [];
|
|
387
|
-
|
|
388
|
-
try {
|
|
389
|
-
if (state.useGit && state.gitCommitHash) {
|
|
390
|
-
const workspace = process.cwd();
|
|
391
|
-
gitChanges = getGitChanges(state.gitCommitHash);
|
|
392
|
-
|
|
393
|
-
execSync(`git reset --hard ${state.gitCommitHash}`, { cwd: workspace, stdio: 'ignore' });
|
|
394
|
-
restoreFileSnapshots(state.fileSnapshots);
|
|
395
|
-
} else {
|
|
396
|
-
const restoreResult = restoreFileSnapshots(state.fileSnapshots);
|
|
397
|
-
if (!restoreResult.success) {
|
|
398
|
-
throw new Error(restoreResult.errors.join('\n'));
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
pushUndoState(currentSessionId, state);
|
|
403
|
-
|
|
404
|
-
return { state, success: true, gitChanges };
|
|
405
|
-
} catch (error) {
|
|
406
|
-
pushRedoState(currentSessionId, state);
|
|
407
|
-
|
|
408
|
-
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
409
|
-
return { state, success: false, error: errorMsg };
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
export function getUndoStack(): UndoRedoState[] {
|
|
414
|
-
return [];
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
export function getRedoStack(): UndoRedoState[] {
|
|
418
|
-
return [];
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
export function clearSession(): void {
|
|
422
|
-
if (!currentSessionId) return;
|
|
423
|
-
|
|
424
|
-
clearRedoStates(currentSessionId);
|
|
425
|
-
currentSessionId = null;
|
|
426
|
-
pendingFileSnapshots = [];
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
export { getAllSessions, setCurrentSession, deleteSession } from './undoRedoDb';
|