@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,85 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { TextAttributes } from '@opentui/core';
|
|
3
|
+
import { useKeyboard } from '@opentui/react';
|
|
4
|
+
import { CustomInput } from '../CustomInput';
|
|
5
|
+
import type { QuestionRequest } from '../../utils/questionBridge';
|
|
6
|
+
|
|
7
|
+
interface QuestionPanelProps {
|
|
8
|
+
request: QuestionRequest;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
onAnswer: (index: number, customText?: string) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function QuestionPanel({ request, disabled = false, onAnswer }: QuestionPanelProps) {
|
|
14
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
setSelectedIndex(0);
|
|
18
|
+
}, [request.id]);
|
|
19
|
+
|
|
20
|
+
useKeyboard((key) => {
|
|
21
|
+
if (disabled) return;
|
|
22
|
+
|
|
23
|
+
if (key.name === 'up' || key.name === 'k') {
|
|
24
|
+
setSelectedIndex(prev => (prev === 0 ? request.options.length - 1 : prev - 1));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (key.name === 'down' || key.name === 'j') {
|
|
29
|
+
setSelectedIndex(prev => (prev === request.options.length - 1 ? 0 : prev + 1));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (key.name === 'return') {
|
|
34
|
+
onAnswer(selectedIndex);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (key.name && /^[1-9]$/.test(key.name)) {
|
|
39
|
+
const idx = Number(key.name) - 1;
|
|
40
|
+
if (idx >= 0 && idx < request.options.length) {
|
|
41
|
+
onAnswer(idx);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const handleCustomSubmit = (text: string) => {
|
|
47
|
+
if (!text || !text.trim()) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
onAnswer(0, text);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<box flexDirection="column" width="100%" backgroundColor="#1a1a1a" paddingLeft={1} paddingRight={1} paddingTop={1} paddingBottom={1}>
|
|
55
|
+
<box flexDirection="row" marginBottom={1}>
|
|
56
|
+
<text fg="#ffca38" attributes={TextAttributes.BOLD}>Question</text>
|
|
57
|
+
</box>
|
|
58
|
+
|
|
59
|
+
<box flexDirection="column" marginBottom={1}>
|
|
60
|
+
{request.prompt.split('\n').map((line, index) => (
|
|
61
|
+
<text key={`prompt-line-${index}`} attributes={TextAttributes.BOLD}>{line || ' '}</text>
|
|
62
|
+
))}
|
|
63
|
+
</box>
|
|
64
|
+
|
|
65
|
+
<box flexDirection="column" marginBottom={1}>
|
|
66
|
+
{request.options.map((option, index) => {
|
|
67
|
+
const selected = index === selectedIndex;
|
|
68
|
+
const prefix = selected ? '> ' : ' ';
|
|
69
|
+
const number = index <= 8 ? `${index + 1}. ` : ' ';
|
|
70
|
+
return (
|
|
71
|
+
<box key={`${request.id}-${index}`} flexDirection="row" backgroundColor={selected ? '#2a2a2a' : 'transparent'} paddingLeft={1} paddingRight={1}>
|
|
72
|
+
<text fg={selected ? '#ffca38' : 'white'} attributes={selected ? TextAttributes.BOLD : TextAttributes.NONE}>
|
|
73
|
+
{prefix}{number}{option.label}
|
|
74
|
+
</text>
|
|
75
|
+
</box>
|
|
76
|
+
);
|
|
77
|
+
})}
|
|
78
|
+
</box>
|
|
79
|
+
|
|
80
|
+
<box flexDirection="row">
|
|
81
|
+
<CustomInput onSubmit={handleCustomSubmit} placeholder="Tell Mosaic what it should do and press Enter" focused={!disabled} disableHistory={true} />
|
|
82
|
+
</box>
|
|
83
|
+
</box>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { TextAttributes } from "@opentui/core";
|
|
3
|
+
import { THINKING_WORDS } from "./types";
|
|
4
|
+
|
|
5
|
+
interface ThinkingIndicatorProps {
|
|
6
|
+
isProcessing: boolean;
|
|
7
|
+
hasQuestion: boolean;
|
|
8
|
+
startTime?: number | null;
|
|
9
|
+
tokens?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getInputBarBaseLines(): number {
|
|
13
|
+
return 3;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getInputAreaTotalLines(): number {
|
|
17
|
+
return getInputBarBaseLines() + 1;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function shouldShowThinkingIndicator({ isProcessing, hasQuestion }: ThinkingIndicatorProps): boolean {
|
|
21
|
+
return isProcessing && !hasQuestion;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getBottomReservedLinesForInputBar(props: ThinkingIndicatorProps): number {
|
|
25
|
+
return getInputBarBaseLines() + (shouldShowThinkingIndicator(props) ? 2 : 0) + 2;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function formatElapsedTime(ms: number | null | undefined, fromStartTime: boolean = true): string {
|
|
29
|
+
if (!ms) return "";
|
|
30
|
+
const elapsed = fromStartTime ? Math.floor((Date.now() - ms) / 1000) : Math.floor(ms / 1000);
|
|
31
|
+
const hours = Math.floor(elapsed / 3600);
|
|
32
|
+
const minutes = Math.floor((elapsed % 3600) / 60);
|
|
33
|
+
const seconds = elapsed % 60;
|
|
34
|
+
if (hours > 0) {
|
|
35
|
+
return `${hours}h ${minutes}m ${seconds}s`;
|
|
36
|
+
}
|
|
37
|
+
if (minutes > 0) {
|
|
38
|
+
return `${minutes}m ${seconds}s`;
|
|
39
|
+
}
|
|
40
|
+
return `${seconds}s`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function ThinkingIndicator(props: ThinkingIndicatorProps) {
|
|
44
|
+
const [shimmerPos, setShimmerPos] = useState(-2);
|
|
45
|
+
const [, setTick] = useState(0);
|
|
46
|
+
const [thinkingWord] = useState(() => THINKING_WORDS[Math.floor(Math.random() * THINKING_WORDS.length)]);
|
|
47
|
+
const text = `${thinkingWord}...`;
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!shouldShowThinkingIndicator(props)) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const interval = setInterval(() => {
|
|
55
|
+
setShimmerPos((prev) => {
|
|
56
|
+
const limit = text.length + 20;
|
|
57
|
+
return prev >= limit ? -2 : prev + 1;
|
|
58
|
+
});
|
|
59
|
+
setTick((prev) => prev + 1);
|
|
60
|
+
}, 50);
|
|
61
|
+
|
|
62
|
+
return () => clearInterval(interval);
|
|
63
|
+
}, [props, text.length]);
|
|
64
|
+
|
|
65
|
+
if (!shouldShowThinkingIndicator(props)) return null;
|
|
66
|
+
|
|
67
|
+
const elapsedStr = formatElapsedTime(props.startTime, true);
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<box flexDirection="row" width="100%">
|
|
71
|
+
<text fg="#ffca38" attributes={TextAttributes.BOLD}>⁘ </text>
|
|
72
|
+
{text.split("").map((char, index) => {
|
|
73
|
+
const inShimmer = index === shimmerPos || index === shimmerPos - 1;
|
|
74
|
+
return (
|
|
75
|
+
<text
|
|
76
|
+
key={index}
|
|
77
|
+
attributes={inShimmer ? TextAttributes.BOLD : TextAttributes.DIM}
|
|
78
|
+
>
|
|
79
|
+
{char}
|
|
80
|
+
</text>
|
|
81
|
+
);
|
|
82
|
+
})}
|
|
83
|
+
{elapsedStr && <text attributes={TextAttributes.DIM}> — {elapsedStr}</text>}
|
|
84
|
+
<text attributes={TextAttributes.DIM}> — esc to cancel</text>
|
|
85
|
+
{props.tokens !== undefined && props.tokens > 0 && <text attributes={TextAttributes.DIM}> — {props.tokens.toLocaleString()} tokens</text>}
|
|
86
|
+
</box>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function ThinkingIndicatorBlock(props: ThinkingIndicatorProps) {
|
|
91
|
+
if (!shouldShowThinkingIndicator(props)) return null;
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<box flexDirection="column" width="100%">
|
|
95
|
+
<ThinkingIndicator {...props} />
|
|
96
|
+
<box flexDirection="row" width="100%">
|
|
97
|
+
<text> </text>
|
|
98
|
+
</box>
|
|
99
|
+
</box>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export const BLEND_WORDS = [
|
|
2
|
+
"Blended",
|
|
3
|
+
"Crafted",
|
|
4
|
+
"Brewed",
|
|
5
|
+
"Cooked",
|
|
6
|
+
"Forged",
|
|
7
|
+
"Woven",
|
|
8
|
+
"Composed",
|
|
9
|
+
"Rendered",
|
|
10
|
+
"Conjured",
|
|
11
|
+
"Distilled",
|
|
12
|
+
"Worked"
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export const THINKING_WORDS = [
|
|
16
|
+
"Thinking",
|
|
17
|
+
"Processing",
|
|
18
|
+
"Analyzing",
|
|
19
|
+
"Reasoning",
|
|
20
|
+
"Computing",
|
|
21
|
+
"Pondering",
|
|
22
|
+
"Crafting",
|
|
23
|
+
"Working",
|
|
24
|
+
"Brewing",
|
|
25
|
+
"Weaving",
|
|
26
|
+
"Revolutionizing"
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
export interface Message {
|
|
30
|
+
id: string;
|
|
31
|
+
role: "user" | "assistant" | "tool" | "slash";
|
|
32
|
+
displayRole?: "user" | "assistant" | "tool" | "slash";
|
|
33
|
+
displayContent?: string;
|
|
34
|
+
content: string;
|
|
35
|
+
toolName?: string;
|
|
36
|
+
toolArgs?: Record<string, unknown>;
|
|
37
|
+
toolResult?: unknown;
|
|
38
|
+
success?: boolean;
|
|
39
|
+
isError?: boolean;
|
|
40
|
+
responseDuration?: number;
|
|
41
|
+
blendWord?: string;
|
|
42
|
+
thinkingContent?: string;
|
|
43
|
+
isRunning?: boolean;
|
|
44
|
+
runningStartTime?: number;
|
|
45
|
+
timestamp?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface MainProps {
|
|
49
|
+
pasteRequestId?: number;
|
|
50
|
+
copyRequestId?: number;
|
|
51
|
+
onCopy?: (text: string) => void;
|
|
52
|
+
shortcutsOpen?: boolean;
|
|
53
|
+
commandsOpen?: boolean;
|
|
54
|
+
initialMessage?: string;
|
|
55
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const wrapText = (text: string, maxWidth: number): string[] => {
|
|
2
|
+
if (!text) return [''];
|
|
3
|
+
if (maxWidth <= 0) return [text];
|
|
4
|
+
if (text.length <= maxWidth) return [text];
|
|
5
|
+
|
|
6
|
+
const lines: string[] = [];
|
|
7
|
+
let currentLine = '';
|
|
8
|
+
let i = 0;
|
|
9
|
+
|
|
10
|
+
while (i < text.length) {
|
|
11
|
+
const char = text[i];
|
|
12
|
+
|
|
13
|
+
if (char === ' ' && currentLine.length === maxWidth) {
|
|
14
|
+
lines.push(currentLine);
|
|
15
|
+
currentLine = '';
|
|
16
|
+
i++;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (currentLine.length + 1 > maxWidth) {
|
|
21
|
+
const lastSpaceIndex = currentLine.lastIndexOf(' ');
|
|
22
|
+
if (lastSpaceIndex > 0) {
|
|
23
|
+
lines.push(currentLine.slice(0, lastSpaceIndex));
|
|
24
|
+
currentLine = currentLine.slice(lastSpaceIndex + 1) + char;
|
|
25
|
+
} else {
|
|
26
|
+
lines.push(currentLine);
|
|
27
|
+
currentLine = char || '';
|
|
28
|
+
}
|
|
29
|
+
} else {
|
|
30
|
+
currentLine += char;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
i++;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (currentLine) {
|
|
37
|
+
lines.push(currentLine);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return lines.length > 0 ? lines : [''];
|
|
41
|
+
};
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
4
|
+
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
5
|
+
|
|
6
|
+
process.stderr.write = ((chunk: any, ...args: any[]) => {
|
|
7
|
+
const str = chunk.toString();
|
|
8
|
+
if (str.includes('Cannot add child') ||
|
|
9
|
+
str.includes('Aborted()') ||
|
|
10
|
+
str.includes('Nodes with measure functions') ||
|
|
11
|
+
str.includes('Maximum update depth')) {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
return originalStderrWrite(chunk, ...args);
|
|
15
|
+
}) as typeof process.stderr.write;
|
|
16
|
+
|
|
17
|
+
process.stdout.write = ((chunk: any, ...args: any[]) => {
|
|
18
|
+
const str = chunk.toString();
|
|
19
|
+
if (str.includes('Cannot add child') ||
|
|
20
|
+
str.includes('Aborted()') ||
|
|
21
|
+
str.includes('Nodes with measure functions')) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
return originalStdoutWrite(chunk, ...args);
|
|
25
|
+
}) as typeof process.stdout.write;
|
|
26
|
+
|
|
27
|
+
import { createCliRenderer } from "@opentui/core";
|
|
28
|
+
import { createRoot } from "@opentui/react";
|
|
29
|
+
import { App } from "./components/App";
|
|
30
|
+
import { existsSync, statSync } from 'fs';
|
|
31
|
+
import { resolve } from 'path';
|
|
32
|
+
|
|
33
|
+
interface ParsedArgs {
|
|
34
|
+
directory?: string;
|
|
35
|
+
help?: boolean;
|
|
36
|
+
initialMessage?: string;
|
|
37
|
+
uninstall?: boolean;
|
|
38
|
+
forceUninstall?: boolean;
|
|
39
|
+
webServer?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class CLI {
|
|
43
|
+
parseArgs(args: string[]): ParsedArgs {
|
|
44
|
+
const parsed: ParsedArgs = {};
|
|
45
|
+
let i = 0;
|
|
46
|
+
|
|
47
|
+
while (i < args.length) {
|
|
48
|
+
const arg = args[i]!;
|
|
49
|
+
|
|
50
|
+
if (arg === '--help' || arg === '-h') {
|
|
51
|
+
parsed.help = true;
|
|
52
|
+
i++;
|
|
53
|
+
} else if (arg === '--directory' || arg === '-d') {
|
|
54
|
+
parsed.directory = args[i + 1];
|
|
55
|
+
i += 2;
|
|
56
|
+
} else if (arg === 'run') {
|
|
57
|
+
parsed.initialMessage = args[i + 1];
|
|
58
|
+
i += 2;
|
|
59
|
+
} else if (arg === 'uninstall') {
|
|
60
|
+
parsed.uninstall = true;
|
|
61
|
+
if (args[i + 1] === '--force') {
|
|
62
|
+
parsed.forceUninstall = true;
|
|
63
|
+
i += 2;
|
|
64
|
+
} else {
|
|
65
|
+
i++;
|
|
66
|
+
}
|
|
67
|
+
} else if (arg === 'web') {
|
|
68
|
+
parsed.webServer = true;
|
|
69
|
+
i++;
|
|
70
|
+
} else if (!arg.startsWith('-')) {
|
|
71
|
+
parsed.directory = arg;
|
|
72
|
+
i++;
|
|
73
|
+
} else {
|
|
74
|
+
i++;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return parsed;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
showHelp(): void {
|
|
82
|
+
const gold = (text: string) => `\x1b[38;2;255;202;56m${text}\x1b[0m`;
|
|
83
|
+
|
|
84
|
+
console.log('');
|
|
85
|
+
console.log(gold('███╗ ███╗'));
|
|
86
|
+
console.log(gold('████╗ ████║'));
|
|
87
|
+
console.log(gold('███╔████╔███║'));
|
|
88
|
+
console.log(`
|
|
89
|
+
Mosaic - AI-powered coding agent
|
|
90
|
+
|
|
91
|
+
Usage:
|
|
92
|
+
mosaic [options] [directory]
|
|
93
|
+
|
|
94
|
+
Options:
|
|
95
|
+
-h, --help Show this help message
|
|
96
|
+
-d, --directory <path> Open in specific directory
|
|
97
|
+
run "<message>" Launch with a message to execute
|
|
98
|
+
web Start the web interface server
|
|
99
|
+
uninstall [--force] Uninstall Mosaic
|
|
100
|
+
|
|
101
|
+
Examples:
|
|
102
|
+
mosaic Start in current directory
|
|
103
|
+
mosaic ./my-project Start in specific directory
|
|
104
|
+
mosaic run "fix the bug" Launch with a task
|
|
105
|
+
mosaic web Start web server on http://127.0.0.1:8192
|
|
106
|
+
mosaic uninstall Interactive uninstall
|
|
107
|
+
mosaic uninstall --force Force uninstall (removes all data)
|
|
108
|
+
`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async uninstall(force: boolean = false): Promise<void> {
|
|
112
|
+
const { uninstallMosaic } = await import('./utils/uninstall');
|
|
113
|
+
await uninstallMosaic(force);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const cli = new CLI();
|
|
118
|
+
const args = process.argv.slice(2);
|
|
119
|
+
const parsed = cli.parseArgs(args);
|
|
120
|
+
|
|
121
|
+
if (parsed.help) {
|
|
122
|
+
cli.showHelp();
|
|
123
|
+
process.exit(0);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (parsed.uninstall) {
|
|
127
|
+
await cli.uninstall(parsed.forceUninstall);
|
|
128
|
+
process.exit(0);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (parsed.webServer) {
|
|
132
|
+
const { spawn } = await import('child_process');
|
|
133
|
+
const path = await import('path');
|
|
134
|
+
const { fileURLToPath } = await import('url');
|
|
135
|
+
const { dirname } = await import('path');
|
|
136
|
+
|
|
137
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
138
|
+
const __dirname = dirname(__filename);
|
|
139
|
+
const serverPath = path.join(__dirname, 'web', 'server.tsx');
|
|
140
|
+
|
|
141
|
+
if (!existsSync(serverPath)) {
|
|
142
|
+
console.error(`Error: Web server file not found at: ${serverPath}`);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const serverProcess = spawn('bun', ['run', serverPath], {
|
|
147
|
+
detached: false,
|
|
148
|
+
stdio: 'inherit',
|
|
149
|
+
env: {
|
|
150
|
+
...process.env,
|
|
151
|
+
MOSAIC_PROJECT_PATH: process.cwd()
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
serverProcess.on('error', (error) => {
|
|
156
|
+
console.error(`Failed to start web server: ${error.message}`);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
process.on('SIGINT', () => {
|
|
161
|
+
serverProcess.kill();
|
|
162
|
+
process.exit(0);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
process.on('SIGTERM', () => {
|
|
166
|
+
serverProcess.kill();
|
|
167
|
+
process.exit(0);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
await new Promise(() => { });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (parsed.directory) {
|
|
174
|
+
const resolvedPath = resolve(parsed.directory);
|
|
175
|
+
|
|
176
|
+
if (!existsSync(resolvedPath)) {
|
|
177
|
+
console.error(`Error: Directory "${parsed.directory}" does not exist.`);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!statSync(resolvedPath).isDirectory()) {
|
|
182
|
+
console.error(`Error: "${parsed.directory}" is not a directory.`);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
process.chdir(resolvedPath);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
import { addRecentProject } from './utils/config';
|
|
190
|
+
addRecentProject(process.cwd());
|
|
191
|
+
|
|
192
|
+
process.title = '⁘ Mosaic';
|
|
193
|
+
|
|
194
|
+
const cleanup = (code = 0) => {
|
|
195
|
+
process.stdout.write('\x1b[?25h');
|
|
196
|
+
process.exit(code);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
process.on('SIGINT', () => cleanup(0));
|
|
200
|
+
process.on('SIGTERM', () => cleanup(0));
|
|
201
|
+
process.on('uncaughtException', () => cleanup(1));
|
|
202
|
+
process.on('unhandledRejection', () => cleanup(1));
|
|
203
|
+
|
|
204
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const renderer = await createCliRenderer();
|
|
208
|
+
createRoot(renderer).render(<App initialMessage={parsed.initialMessage} />);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
console.error(error);
|
|
211
|
+
cleanup(1);
|
|
212
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
export interface ApprovalRequest {
|
|
2
|
+
id: string;
|
|
3
|
+
toolName: 'write' | 'edit' | 'bash';
|
|
4
|
+
preview: {
|
|
5
|
+
title: string;
|
|
6
|
+
content: string;
|
|
7
|
+
details?: string[];
|
|
8
|
+
};
|
|
9
|
+
args: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ApprovalResponse {
|
|
13
|
+
id: string;
|
|
14
|
+
approved: boolean;
|
|
15
|
+
customResponse?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ApprovalAccepted {
|
|
19
|
+
toolName: 'write' | 'edit' | 'bash';
|
|
20
|
+
args: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type ApprovalListener = (request: ApprovalRequest | null) => void;
|
|
24
|
+
type ApprovalAcceptedListener = (accepted: ApprovalAccepted) => void;
|
|
25
|
+
|
|
26
|
+
let currentRequest: ApprovalRequest | null = null;
|
|
27
|
+
let listeners = new Set<ApprovalListener>();
|
|
28
|
+
let acceptedListeners = new Set<ApprovalAcceptedListener>();
|
|
29
|
+
let pendingResolve: ((response: ApprovalResponse) => void) | null = null;
|
|
30
|
+
let pendingReject: ((reason?: any) => void) | null = null;
|
|
31
|
+
|
|
32
|
+
function notify(): void {
|
|
33
|
+
for (const listener of listeners) {
|
|
34
|
+
listener(currentRequest);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createId(): string {
|
|
39
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function subscribeApproval(listener: ApprovalListener): () => void {
|
|
43
|
+
listeners.add(listener);
|
|
44
|
+
listener(currentRequest);
|
|
45
|
+
return () => {
|
|
46
|
+
listeners.delete(listener);
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function subscribeApprovalAccepted(listener: ApprovalAcceptedListener): () => void {
|
|
51
|
+
acceptedListeners.add(listener);
|
|
52
|
+
return () => {
|
|
53
|
+
acceptedListeners.delete(listener);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function notifyApprovalAccepted(toolName: 'write' | 'edit' | 'bash', args: Record<string, unknown>): void {
|
|
58
|
+
for (const listener of acceptedListeners) {
|
|
59
|
+
listener({ toolName, args });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getCurrentApproval(): ApprovalRequest | null {
|
|
64
|
+
return currentRequest;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function requestApproval(
|
|
68
|
+
toolName: 'write' | 'edit' | 'bash',
|
|
69
|
+
args: Record<string, unknown>,
|
|
70
|
+
preview: { title: string; content: string; details?: string[] }
|
|
71
|
+
): Promise<{ approved: boolean; customResponse?: string }> {
|
|
72
|
+
if (pendingResolve) {
|
|
73
|
+
throw new Error('An approval request is already pending');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const request: ApprovalRequest = {
|
|
77
|
+
id: createId(),
|
|
78
|
+
toolName,
|
|
79
|
+
preview,
|
|
80
|
+
args,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
currentRequest = request;
|
|
84
|
+
notify();
|
|
85
|
+
|
|
86
|
+
const response = await new Promise<ApprovalResponse>((resolve, reject) => {
|
|
87
|
+
pendingResolve = resolve;
|
|
88
|
+
pendingReject = reject;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return { approved: response.approved, customResponse: response.customResponse };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function respondApproval(approved: boolean, customResponse?: string): void {
|
|
95
|
+
if (!currentRequest || !pendingResolve) return;
|
|
96
|
+
|
|
97
|
+
const response: ApprovalResponse = {
|
|
98
|
+
id: currentRequest.id,
|
|
99
|
+
approved,
|
|
100
|
+
customResponse,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const resolve = pendingResolve;
|
|
104
|
+
const toolName = currentRequest.toolName;
|
|
105
|
+
const args = currentRequest.args;
|
|
106
|
+
|
|
107
|
+
pendingResolve = null;
|
|
108
|
+
pendingReject = null;
|
|
109
|
+
currentRequest = null;
|
|
110
|
+
notify();
|
|
111
|
+
|
|
112
|
+
if (approved) {
|
|
113
|
+
notifyApprovalAccepted(toolName, args);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
resolve(response);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function cancelApproval(): void {
|
|
120
|
+
if (!currentRequest || !pendingReject) return;
|
|
121
|
+
|
|
122
|
+
const reject = pendingReject;
|
|
123
|
+
pendingResolve = null;
|
|
124
|
+
pendingReject = null;
|
|
125
|
+
currentRequest = null;
|
|
126
|
+
notify();
|
|
127
|
+
|
|
128
|
+
reject(new Error('Interrupted by user'));
|
|
129
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Command } from './types';
|
|
2
|
+
|
|
3
|
+
export const echoCommand: Command = {
|
|
4
|
+
name: 'echo',
|
|
5
|
+
description: 'Echo the provided text back to the user',
|
|
6
|
+
usage: '/echo <text>',
|
|
7
|
+
aliases: ['e'],
|
|
8
|
+
execute: (args: string[], fullCommand: string): { success: boolean; content: string } => {
|
|
9
|
+
if (args.length === 0) {
|
|
10
|
+
return {
|
|
11
|
+
success: false,
|
|
12
|
+
content: 'Error: /echo requires text to echo. Usage: /echo <text>'
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const text = args.join(' ');
|
|
17
|
+
return {
|
|
18
|
+
success: true,
|
|
19
|
+
content: text
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Command } from './types';
|
|
2
|
+
import { commandRegistry } from './registry';
|
|
3
|
+
|
|
4
|
+
export const helpCommand: Command = {
|
|
5
|
+
name: 'help',
|
|
6
|
+
description: 'Show available commands',
|
|
7
|
+
usage: '/help',
|
|
8
|
+
aliases: ['h'],
|
|
9
|
+
execute: (): { success: boolean; content: string } => {
|
|
10
|
+
const commands = commandRegistry.getAll();
|
|
11
|
+
const commandList = Array.from(commands.entries())
|
|
12
|
+
.filter(([name, cmd]) => name === cmd.name)
|
|
13
|
+
.map(([name, cmd]) => {
|
|
14
|
+
const usage = cmd.usage ? ` - ${cmd.usage}` : '';
|
|
15
|
+
const aliases = cmd.aliases && cmd.aliases.length > 0 ? ` (aliases: ${cmd.aliases.join(', ')})` : '';
|
|
16
|
+
return `/${name}${usage}${aliases}\n ${cmd.description}`;
|
|
17
|
+
})
|
|
18
|
+
.join('\n\n');
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
success: true,
|
|
22
|
+
content: `Available commands:\n\n${commandList}`
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
};
|