@kirosnn/mosaic 0.0.7
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/.mosaic/mosaic.local.jsonc +0 -0
- package/MOSAIC.md +188 -0
- package/README.md +127 -0
- package/docs/mosaic.png +0 -0
- package/package.json +42 -0
- package/src/agent/Agent.ts +131 -0
- package/src/agent/context.ts +96 -0
- package/src/agent/index.ts +2 -0
- package/src/agent/prompts/systemPrompt.ts +138 -0
- package/src/agent/prompts/toolsPrompt.ts +139 -0
- package/src/agent/provider/anthropic.ts +122 -0
- package/src/agent/provider/google.ts +124 -0
- package/src/agent/provider/mistral.ts +117 -0
- package/src/agent/provider/ollama.ts +531 -0
- package/src/agent/provider/openai.ts +220 -0
- package/src/agent/provider/xai.ts +122 -0
- package/src/agent/tools/bash.ts +20 -0
- package/src/agent/tools/definitions.ts +27 -0
- package/src/agent/tools/edit.ts +23 -0
- package/src/agent/tools/executor.ts +751 -0
- package/src/agent/tools/explore.ts +18 -0
- package/src/agent/tools/exploreExecutor.ts +320 -0
- package/src/agent/tools/glob.ts +16 -0
- package/src/agent/tools/grep.ts +19 -0
- package/src/agent/tools/index.ts +4 -0
- package/src/agent/tools/list.ts +20 -0
- package/src/agent/tools/question.ts +20 -0
- package/src/agent/tools/read.ts +15 -0
- package/src/agent/tools/write.ts +21 -0
- package/src/agent/types.ts +155 -0
- package/src/components/App.tsx +174 -0
- package/src/components/CommandsModal.tsx +77 -0
- package/src/components/CustomInput.tsx +328 -0
- package/src/components/Main.tsx +1112 -0
- package/src/components/Notification.tsx +91 -0
- package/src/components/SelectList.tsx +47 -0
- package/src/components/Setup.tsx +528 -0
- package/src/components/ShortcutsModal.tsx +67 -0
- package/src/components/Welcome.tsx +39 -0
- package/src/components/main/ApprovalPanel.tsx +134 -0
- package/src/components/main/ChatPage.tsx +516 -0
- package/src/components/main/HomePage.tsx +111 -0
- package/src/components/main/QuestionPanel.tsx +85 -0
- package/src/components/main/ThinkingIndicator.tsx +101 -0
- package/src/components/main/types.ts +55 -0
- package/src/components/main/wrapText.ts +41 -0
- package/src/index.tsx +212 -0
- package/src/utils/approvalBridge.ts +129 -0
- package/src/utils/commands/echo.ts +22 -0
- package/src/utils/commands/help.ts +25 -0
- package/src/utils/commands/index.ts +68 -0
- package/src/utils/commands/init.ts +68 -0
- package/src/utils/commands/redo.ts +74 -0
- package/src/utils/commands/registry.ts +29 -0
- package/src/utils/commands/sessions.ts +129 -0
- package/src/utils/commands/types.ts +20 -0
- package/src/utils/commands/undo.ts +75 -0
- package/src/utils/commands/web.ts +77 -0
- package/src/utils/config.ts +357 -0
- package/src/utils/diff.ts +201 -0
- package/src/utils/diffRendering.tsx +62 -0
- package/src/utils/exploreBridge.ts +87 -0
- package/src/utils/fileChangeTracker.ts +98 -0
- package/src/utils/fileChangesBridge.ts +18 -0
- package/src/utils/history.ts +106 -0
- package/src/utils/markdown.tsx +232 -0
- package/src/utils/models.ts +304 -0
- package/src/utils/questionBridge.ts +122 -0
- package/src/utils/terminalUtils.ts +25 -0
- package/src/utils/toolFormatting.ts +384 -0
- package/src/utils/undoRedo.ts +429 -0
- package/src/utils/undoRedoBridge.ts +45 -0
- package/src/utils/undoRedoDb.ts +338 -0
- package/src/utils/uninstall.ts +45 -0
- package/src/utils/version.ts +3 -0
- package/src/web/app.tsx +606 -0
- package/src/web/assets/css/ChatPage.css +212 -0
- package/src/web/assets/css/FileExplorer.css +202 -0
- package/src/web/assets/css/HomePage.css +119 -0
- package/src/web/assets/css/Markdown.css +178 -0
- package/src/web/assets/css/MessageItem.css +160 -0
- package/src/web/assets/css/Sidebar.css +208 -0
- package/src/web/assets/css/SidebarModal.css +137 -0
- package/src/web/assets/css/ThinkingIndicator.css +47 -0
- package/src/web/assets/css/ToolMessage.css +148 -0
- package/src/web/assets/css/global.css +226 -0
- package/src/web/assets/fonts/Geist-Black.woff2 +0 -0
- package/src/web/assets/fonts/Geist-BlackItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-Bold.woff2 +0 -0
- package/src/web/assets/fonts/Geist-BoldItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-ExtraBold.woff2 +0 -0
- package/src/web/assets/fonts/Geist-ExtraBoldItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-ExtraLight.woff2 +0 -0
- package/src/web/assets/fonts/Geist-ExtraLightItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-Italic[wght].woff2 +0 -0
- package/src/web/assets/fonts/Geist-Light.woff2 +0 -0
- package/src/web/assets/fonts/Geist-LightItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-Medium.woff2 +0 -0
- package/src/web/assets/fonts/Geist-MediumItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-Regular.woff2 +0 -0
- package/src/web/assets/fonts/Geist-RegularItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-SemiBold.woff2 +0 -0
- package/src/web/assets/fonts/Geist-SemiBoldItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-Thin.woff2 +0 -0
- package/src/web/assets/fonts/Geist-ThinItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Black.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-BlackItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Bold.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-BoldItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-ExtraBold.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-ExtraBoldItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-ExtraLight.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-ExtraLightItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Italic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Italic[wght].woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Light.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-LightItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Medium.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-MediumItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Regular.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-SemiBold.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-SemiBoldItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Thin.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-ThinItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono[wght].woff2 +0 -0
- package/src/web/assets/fonts/Geist[wght].woff2 +0 -0
- package/src/web/assets/fonts/blauer-nue-regular.woff2 +0 -0
- package/src/web/assets/fonts/neue-montreal-regular.woff2 +0 -0
- package/src/web/assets/images/favicon-v2.svg +6 -0
- package/src/web/assets/images/favicon.png +0 -0
- package/src/web/assets/images/foruse.svg +5 -0
- package/src/web/assets/images/logo_black.svg +5 -0
- package/src/web/assets/images/logo_white.svg +5 -0
- package/src/web/assets/images/logoblack.png +0 -0
- package/src/web/assets/images/logowhite.png +0 -0
- package/src/web/build.ts +23 -0
- package/src/web/components/ApprovalPanel.tsx +191 -0
- package/src/web/components/ChatPage.tsx +273 -0
- package/src/web/components/FileExplorer.tsx +162 -0
- package/src/web/components/HomePage.tsx +121 -0
- package/src/web/components/MessageItem.tsx +178 -0
- package/src/web/components/Modal.tsx +30 -0
- package/src/web/components/QuestionPanel.tsx +149 -0
- package/src/web/components/Setup.tsx +211 -0
- package/src/web/components/Sidebar.tsx +292 -0
- package/src/web/components/ThinkingIndicator.tsx +85 -0
- package/src/web/logo_black.svg +5 -0
- package/src/web/logo_white.svg +5 -0
- package/src/web/router.ts +46 -0
- package/src/web/server.tsx +662 -0
- package/src/web/storage.ts +92 -0
- package/src/web/types.ts +17 -0
- package/src/web/utils.ts +61 -0
- package/tsconfig.json +33 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { CommandResult } from './types';
|
|
2
|
+
import { commandRegistry } from './registry';
|
|
3
|
+
import { echoCommand } from './echo';
|
|
4
|
+
import { helpCommand } from './help';
|
|
5
|
+
import { initCommand } from './init';
|
|
6
|
+
import { undoCommand } from './undo';
|
|
7
|
+
import { redoCommand } from './redo';
|
|
8
|
+
import { sessionsCommand } from './sessions';
|
|
9
|
+
import { webCommand } from './web';
|
|
10
|
+
|
|
11
|
+
export { commandRegistry } from './registry';
|
|
12
|
+
export type { Command, CommandResult, CommandRegistry } from './types';
|
|
13
|
+
|
|
14
|
+
export function isCommand(input: string): boolean {
|
|
15
|
+
return input.trim().startsWith('/');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function parseCommand(input: string): { command: string; args: string[] } | null {
|
|
19
|
+
const trimmed = input.trim();
|
|
20
|
+
if (!trimmed.startsWith('/')) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const withoutSlash = trimmed.slice(1);
|
|
25
|
+
const parts = withoutSlash.split(/\s+/);
|
|
26
|
+
const command = parts[0]!.toLowerCase();
|
|
27
|
+
const args = parts.slice(1);
|
|
28
|
+
|
|
29
|
+
return { command, args };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function executeCommand(input: string): Promise<CommandResult | null> {
|
|
33
|
+
const parsed = parseCommand(input);
|
|
34
|
+
if (!parsed) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const { command, args } = parsed;
|
|
39
|
+
const cmd = commandRegistry.get(command);
|
|
40
|
+
|
|
41
|
+
if (!cmd) {
|
|
42
|
+
return {
|
|
43
|
+
success: false,
|
|
44
|
+
content: `Unknown command: /${command}. Type /help for available commands.`,
|
|
45
|
+
shouldAddToHistory: false
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
return await cmd.execute(args, input);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return {
|
|
53
|
+
success: false,
|
|
54
|
+
content: `Error executing command /${command}: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
55
|
+
shouldAddToHistory: false
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function initializeCommands(): void {
|
|
61
|
+
commandRegistry.register(echoCommand);
|
|
62
|
+
commandRegistry.register(helpCommand);
|
|
63
|
+
commandRegistry.register(initCommand);
|
|
64
|
+
commandRegistry.register(undoCommand);
|
|
65
|
+
commandRegistry.register(redoCommand);
|
|
66
|
+
commandRegistry.register(sessionsCommand);
|
|
67
|
+
commandRegistry.register(webCommand);
|
|
68
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { mkdir } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import type { Command } from './types';
|
|
4
|
+
|
|
5
|
+
const INIT_PROMPT = `Analyze the codebase and create or update the MOSAIC.md file in the root of the current workspace.
|
|
6
|
+
|
|
7
|
+
IMPORTANT: MOSAIC.md is NOT a README. It's a specialized context file that helps AI agents (like me) understand this project better when working in this workspace in the future.
|
|
8
|
+
|
|
9
|
+
The MOSAIC.md file should contain:
|
|
10
|
+
|
|
11
|
+
1. **Project Overview** - What this project does and its main purpose
|
|
12
|
+
2. **Architecture** - Key architectural patterns, design decisions, and how the codebase is organized
|
|
13
|
+
3. **Development Guidelines** - Important coding standards, naming conventions, and best practices specific to this project
|
|
14
|
+
4. **Key Files & Directories** - Critical files/folders and their purposes
|
|
15
|
+
5. **Common Tasks** - Frequent development tasks and how to accomplish them
|
|
16
|
+
|
|
17
|
+
First, analyze the existing codebase:
|
|
18
|
+
- Check package.json, README.md, and main source files
|
|
19
|
+
- Identify the technologies, frameworks, and tools used
|
|
20
|
+
- Understand the project structure and organization
|
|
21
|
+
- Look for existing documentation or comments
|
|
22
|
+
|
|
23
|
+
**If MOSAIC.md already exists:**
|
|
24
|
+
- Read the existing MOSAIC.md file first
|
|
25
|
+
- Analyze if any sections need updates based on current codebase state
|
|
26
|
+
- Add any missing architectural patterns, new tools, or important workflows discovered
|
|
27
|
+
- Update outdated information (dependencies versions, file paths, deprecated patterns)
|
|
28
|
+
- Improve clarity and add details where needed
|
|
29
|
+
- Keep the existing structure and relevant information
|
|
30
|
+
- DO NOT recreate the file from scratch - use the edit tool to make targeted improvements
|
|
31
|
+
|
|
32
|
+
**If MOSAIC.md does not exist:**
|
|
33
|
+
- Create a comprehensive MOSAIC.md file that will serve as a contextual guide for AI agents working in this workspace
|
|
34
|
+
|
|
35
|
+
Even if the file seems complete, always look for potential improvements:
|
|
36
|
+
- Are there new features or patterns in the code not documented?
|
|
37
|
+
- Are all critical workflows explained?
|
|
38
|
+
- Could any section be clearer or more detailed?
|
|
39
|
+
- Are there undocumented conventions or best practices?
|
|
40
|
+
|
|
41
|
+
Make it clear, concise, and practical. Focus on what an AI agent needs to know to be effective in this codebase.
|
|
42
|
+
|
|
43
|
+
DO NOT create a .mosaic directory - it has already been created automatically.`;
|
|
44
|
+
|
|
45
|
+
export const initCommand: Command = {
|
|
46
|
+
name: 'init',
|
|
47
|
+
description: 'Initialize the current workspace with Mosaic configuration (creates or updates MOSAIC.md and .mosaic folder)',
|
|
48
|
+
usage: '/init',
|
|
49
|
+
aliases: ['i'],
|
|
50
|
+
execute: async (): Promise<{ success: boolean; content: string; shouldAddToHistory?: boolean }> => {
|
|
51
|
+
try {
|
|
52
|
+
const mosaicDir = join(process.cwd(), '.mosaic');
|
|
53
|
+
await mkdir(mosaicDir, { recursive: true });
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return {
|
|
56
|
+
success: false,
|
|
57
|
+
content: `Failed to create .mosaic directory: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
58
|
+
shouldAddToHistory: false
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
success: true,
|
|
64
|
+
content: INIT_PROMPT,
|
|
65
|
+
shouldAddToHistory: true
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Command, CommandRegistry } from './types';
|
|
2
|
+
|
|
3
|
+
class CommandRegistryImpl implements CommandRegistry {
|
|
4
|
+
private commands = new Map<string, Command>();
|
|
5
|
+
|
|
6
|
+
register(command: Command): void {
|
|
7
|
+
this.commands.set(command.name, command);
|
|
8
|
+
|
|
9
|
+
if (command.aliases) {
|
|
10
|
+
for (const alias of command.aliases) {
|
|
11
|
+
this.commands.set(alias, command);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get(name: string): Command | undefined {
|
|
17
|
+
return this.commands.get(name);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
getAll(): Map<string, Command> {
|
|
21
|
+
return new Map(this.commands);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
has(name: string): boolean {
|
|
25
|
+
return this.commands.has(name);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const commandRegistry = new CommandRegistryImpl();
|
|
@@ -0,0 +1,129 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface CommandResult {
|
|
2
|
+
success: boolean;
|
|
3
|
+
content: string;
|
|
4
|
+
shouldAddToHistory?: boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface Command {
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
usage?: string;
|
|
11
|
+
aliases?: string[];
|
|
12
|
+
execute: (args: string[], fullCommand: string) => Promise<CommandResult> | CommandResult;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CommandRegistry {
|
|
16
|
+
register: (command: Command) => void;
|
|
17
|
+
get: (name: string) => Command | undefined;
|
|
18
|
+
getAll: () => Map<string, Command>;
|
|
19
|
+
has: (name: string) => boolean;
|
|
20
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Command } from './index';
|
|
2
|
+
|
|
3
|
+
export const webCommand: Command = {
|
|
4
|
+
name: 'web',
|
|
5
|
+
description: 'Launch the web interface on http://127.0.0.1:8192',
|
|
6
|
+
usage: '/web',
|
|
7
|
+
aliases: ['w'],
|
|
8
|
+
execute: async (): Promise<{ success: boolean; content: string; shouldAddToHistory?: boolean }> => {
|
|
9
|
+
try {
|
|
10
|
+
const { spawn } = await import('child_process');
|
|
11
|
+
const path = await import('path');
|
|
12
|
+
const fs = await import('fs');
|
|
13
|
+
|
|
14
|
+
const serverPath = path.join(__dirname, '..', '..', 'web', 'server.ts');
|
|
15
|
+
|
|
16
|
+
if (!fs.existsSync(serverPath)) {
|
|
17
|
+
return {
|
|
18
|
+
success: false,
|
|
19
|
+
content: `Web server file not found at: ${serverPath}`,
|
|
20
|
+
shouldAddToHistory: false
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const serverProcess = spawn('bun', ['run', serverPath], {
|
|
25
|
+
detached: true,
|
|
26
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
27
|
+
env: {
|
|
28
|
+
...process.env,
|
|
29
|
+
MOSAIC_PROJECT_PATH: process.cwd()
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
let startupError = '';
|
|
34
|
+
|
|
35
|
+
serverProcess.stderr?.on('data', (data) => {
|
|
36
|
+
startupError += data.toString();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
await new Promise<void>((resolve, reject) => {
|
|
40
|
+
const timeout = setTimeout(() => {
|
|
41
|
+
if (startupError) {
|
|
42
|
+
reject(new Error(startupError));
|
|
43
|
+
} else {
|
|
44
|
+
resolve();
|
|
45
|
+
}
|
|
46
|
+
}, 2000);
|
|
47
|
+
|
|
48
|
+
serverProcess.on('error', (error) => {
|
|
49
|
+
clearTimeout(timeout);
|
|
50
|
+
reject(error);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
serverProcess.stdout?.on('data', (data) => {
|
|
54
|
+
const output = data.toString();
|
|
55
|
+
if (output.includes('running on')) {
|
|
56
|
+
clearTimeout(timeout);
|
|
57
|
+
resolve();
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
serverProcess.unref();
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
success: true,
|
|
66
|
+
content: 'Web interface started successfully!\n\nOpen your browser at: http://127.0.0.1:8192',
|
|
67
|
+
shouldAddToHistory: false
|
|
68
|
+
};
|
|
69
|
+
} catch (error) {
|
|
70
|
+
return {
|
|
71
|
+
success: false,
|
|
72
|
+
content: `Failed to start web interface: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
73
|
+
shouldAddToHistory: false
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|