@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,662 @@
|
|
|
1
|
+
import { serve } from "bun";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { existsSync, readdirSync, statSync } from "fs";
|
|
4
|
+
import { build } from "bun";
|
|
5
|
+
import { createCliRenderer, TextAttributes } from "@opentui/core";
|
|
6
|
+
import { createRoot } from "@opentui/react";
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { exec } from "child_process";
|
|
9
|
+
|
|
10
|
+
const PORT = 8192;
|
|
11
|
+
const HOST = "127.0.0.1";
|
|
12
|
+
|
|
13
|
+
import { subscribeQuestion, answerQuestion } from "../utils/questionBridge";
|
|
14
|
+
import { subscribeApproval, respondApproval } from "../utils/approvalBridge";
|
|
15
|
+
|
|
16
|
+
let currentAbortController: AbortController | null = null;
|
|
17
|
+
|
|
18
|
+
const HTML_TEMPLATE = `<!DOCTYPE html>
|
|
19
|
+
<html lang="en">
|
|
20
|
+
<head>
|
|
21
|
+
<meta charset="UTF-8">
|
|
22
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
23
|
+
<title>Mosaic</title>
|
|
24
|
+
<link rel="icon" type="image/svg+xml" href="/logo_black.svg" media="(prefers-color-scheme: light)">
|
|
25
|
+
<link rel="icon" type="image/svg+xml" href="/logo_white.svg" media="(prefers-color-scheme: dark)">
|
|
26
|
+
<link rel="stylesheet" href="/app.css">
|
|
27
|
+
</head>
|
|
28
|
+
<body>
|
|
29
|
+
<div id="root"></div>
|
|
30
|
+
<script type="module" src="/app.js"></script>
|
|
31
|
+
</body>
|
|
32
|
+
</html>`;
|
|
33
|
+
|
|
34
|
+
type LogEntry = { message: string; timestamp: string };
|
|
35
|
+
|
|
36
|
+
const logs: LogEntry[] = [];
|
|
37
|
+
const listeners: Set<() => void> = new Set();
|
|
38
|
+
|
|
39
|
+
function addLog(message: string) {
|
|
40
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
41
|
+
const clean = String(message ?? "").replace(/\r/g, "").trimEnd();
|
|
42
|
+
if (!clean) return;
|
|
43
|
+
|
|
44
|
+
const lines = clean.split("\n");
|
|
45
|
+
for (const line of lines) {
|
|
46
|
+
if (!line) continue;
|
|
47
|
+
logs.push({ message: line, timestamp });
|
|
48
|
+
|
|
49
|
+
}
|
|
50
|
+
while (logs.length > 50) logs.shift();
|
|
51
|
+
listeners.forEach((l) => l());
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function installExternalLogCapture() {
|
|
55
|
+
const originalLog = console.log.bind(console);
|
|
56
|
+
const originalInfo = console.info.bind(console);
|
|
57
|
+
const originalWarn = console.warn.bind(console);
|
|
58
|
+
const originalError = console.error.bind(console);
|
|
59
|
+
|
|
60
|
+
console.log = (...args: any[]) => {
|
|
61
|
+
addLog(args.map(String).join(" "));
|
|
62
|
+
originalLog(...args);
|
|
63
|
+
};
|
|
64
|
+
console.info = (...args: any[]) => {
|
|
65
|
+
addLog(args.map(String).join(" "));
|
|
66
|
+
originalInfo(...args);
|
|
67
|
+
};
|
|
68
|
+
console.warn = (...args: any[]) => {
|
|
69
|
+
addLog(args.map(String).join(" "));
|
|
70
|
+
originalWarn(...args);
|
|
71
|
+
};
|
|
72
|
+
console.error = (...args: any[]) => {
|
|
73
|
+
addLog(args.map(String).join(" "));
|
|
74
|
+
originalError(...args);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
if (typeof process !== "undefined" && process?.stdout?.write) {
|
|
78
|
+
const originalStdoutWrite = process.stdout.write.bind(process.stdout) as (
|
|
79
|
+
chunk: any,
|
|
80
|
+
encoding?: any,
|
|
81
|
+
cb?: any
|
|
82
|
+
) => boolean;
|
|
83
|
+
|
|
84
|
+
process.stdout.write = ((chunk: any, encoding?: any, cb?: any) => {
|
|
85
|
+
try {
|
|
86
|
+
const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
|
|
87
|
+
addLog(text);
|
|
88
|
+
} catch { }
|
|
89
|
+
return originalStdoutWrite(chunk, encoding as any, cb as any);
|
|
90
|
+
}) as any;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (typeof process !== "undefined" && process?.stderr?.write) {
|
|
94
|
+
const originalStderrWrite = process.stderr.write.bind(process.stderr) as (
|
|
95
|
+
chunk: any,
|
|
96
|
+
encoding?: any,
|
|
97
|
+
cb?: any
|
|
98
|
+
) => boolean;
|
|
99
|
+
|
|
100
|
+
process.stderr.write = ((chunk: any, encoding?: any, cb?: any) => {
|
|
101
|
+
try {
|
|
102
|
+
const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
|
|
103
|
+
addLog(text);
|
|
104
|
+
} catch { }
|
|
105
|
+
return originalStderrWrite(chunk, encoding as any, cb as any);
|
|
106
|
+
}) as any;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
installExternalLogCapture();
|
|
111
|
+
|
|
112
|
+
let appJsContent: string | null = null;
|
|
113
|
+
let appCssContent: string | null = null;
|
|
114
|
+
|
|
115
|
+
async function buildApp() {
|
|
116
|
+
const appPath = join(__dirname, "app.tsx");
|
|
117
|
+
|
|
118
|
+
if (!existsSync(appPath)) {
|
|
119
|
+
throw new Error(`App file not found at: ${appPath}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const buildResult = await build({
|
|
123
|
+
entrypoints: [appPath],
|
|
124
|
+
target: "browser",
|
|
125
|
+
format: "esm",
|
|
126
|
+
minify: false,
|
|
127
|
+
splitting: false,
|
|
128
|
+
sourcemap: "none",
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
if (!buildResult.success) {
|
|
133
|
+
throw new Error("Build failed");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const outputs = buildResult.outputs;
|
|
137
|
+
if (outputs.length === 0) {
|
|
138
|
+
throw new Error("No build output generated");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const output of outputs) {
|
|
142
|
+
if (output.path.endsWith('.js') || output.kind === 'entry-point') {
|
|
143
|
+
appJsContent = await output.text();
|
|
144
|
+
} else if (output.path.endsWith('.css') || output.type === 'text/css') {
|
|
145
|
+
appCssContent = await output.text();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
await buildApp();
|
|
152
|
+
addLog("App built");
|
|
153
|
+
|
|
154
|
+
const projectPath = process.env.MOSAIC_PROJECT_PATH;
|
|
155
|
+
if (projectPath) {
|
|
156
|
+
const { addRecentProject } = await import("../utils/config");
|
|
157
|
+
addRecentProject(projectPath);
|
|
158
|
+
addLog(`Project added to recents: ${projectPath}`);
|
|
159
|
+
}
|
|
160
|
+
} catch (error) {
|
|
161
|
+
console.error("Failed to build app:", error);
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
let currentPort = PORT;
|
|
167
|
+
|
|
168
|
+
async function startServer(port: number, maxRetries = 10) {
|
|
169
|
+
try {
|
|
170
|
+
const server = serve({
|
|
171
|
+
port: port,
|
|
172
|
+
hostname: HOST,
|
|
173
|
+
idleTimeout: 0,
|
|
174
|
+
async fetch(request) {
|
|
175
|
+
const url = new URL(request.url);
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
const isApiRoute = url.pathname.startsWith('/api/');
|
|
179
|
+
const isStaticFile = url.pathname.match(/\.(js|css|svg|ico|png|jpg|jpeg|gif|webp|woff|woff2|ttf|eot)$/);
|
|
180
|
+
|
|
181
|
+
if (url.pathname === "/" || url.pathname === "/home" || url.pathname.startsWith("/chat")) {
|
|
182
|
+
addLog(`${request.method} ${url.pathname}`);
|
|
183
|
+
return new Response(HTML_TEMPLATE, {
|
|
184
|
+
headers: { "Content-Type": "text/html" },
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (url.pathname === "/app.js") {
|
|
189
|
+
if (!appJsContent) {
|
|
190
|
+
addLog("App not built");
|
|
191
|
+
return new Response("App not built", { status: 500 });
|
|
192
|
+
|
|
193
|
+
}
|
|
194
|
+
addLog(`${request.method} /app.js`);
|
|
195
|
+
return new Response(appJsContent, {
|
|
196
|
+
headers: {
|
|
197
|
+
"Content-Type": "application/javascript",
|
|
198
|
+
"Cache-Control": "no-cache",
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (url.pathname === "/app.css") {
|
|
205
|
+
if (!appCssContent) {
|
|
206
|
+
return new Response("", { headers: { "Content-Type": "text/css" } });
|
|
207
|
+
|
|
208
|
+
}
|
|
209
|
+
addLog(`${request.method} /app.css`);
|
|
210
|
+
return new Response(appCssContent, {
|
|
211
|
+
headers: {
|
|
212
|
+
"Content-Type": "text/css",
|
|
213
|
+
"Cache-Control": "no-cache",
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (url.pathname === "/logo_black.svg") {
|
|
220
|
+
const logoPath = join(__dirname, "logo_black.svg");
|
|
221
|
+
if (existsSync(logoPath)) {
|
|
222
|
+
return new Response(Bun.file(logoPath), {
|
|
223
|
+
headers: { "Content-Type": "image/svg+xml" }
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
}
|
|
227
|
+
return new Response("Not Found", { status: 404 });
|
|
228
|
+
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (url.pathname === "/logo_white.svg") {
|
|
232
|
+
const logoPath = join(__dirname, "logo_white.svg");
|
|
233
|
+
if (existsSync(logoPath)) {
|
|
234
|
+
return new Response(Bun.file(logoPath), {
|
|
235
|
+
headers: { "Content-Type": "image/svg+xml" }
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
}
|
|
239
|
+
return new Response("Not Found", { status: 404 });
|
|
240
|
+
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (url.pathname === "/favicon.ico") {
|
|
244
|
+
const faviconPath = join(__dirname, "favicon.ico");
|
|
245
|
+
if (existsSync(faviconPath)) {
|
|
246
|
+
return new Response(Bun.file(faviconPath));
|
|
247
|
+
}
|
|
248
|
+
return new Response("Not Found", { status: 404 });
|
|
249
|
+
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (url.pathname === "/favicon.png") {
|
|
253
|
+
const faviconPath = join(__dirname, "favicon.png");
|
|
254
|
+
if (existsSync(faviconPath)) {
|
|
255
|
+
return new Response(Bun.file(faviconPath));
|
|
256
|
+
}
|
|
257
|
+
return new Response("Not Found", { status: 404 });
|
|
258
|
+
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (url.pathname === "/api/workspace" && request.method === "GET") {
|
|
262
|
+
const workspace = process.cwd();
|
|
263
|
+
return new Response(JSON.stringify({ workspace }), {
|
|
264
|
+
headers: { "Content-Type": "application/json" },
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (url.pathname === "/api/workspace" && request.method === "POST") {
|
|
269
|
+
const body = (await request.json()) as { path: string };
|
|
270
|
+
if (!body.path || typeof body.path !== "string") {
|
|
271
|
+
return new Response(JSON.stringify({ error: "Invalid path" }), {
|
|
272
|
+
status: 400,
|
|
273
|
+
headers: { "Content-Type": "application/json" },
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
process.chdir(body.path);
|
|
279
|
+
return new Response(JSON.stringify({ success: true, workspace: process.cwd() }), {
|
|
280
|
+
headers: { "Content-Type": "application/json" },
|
|
281
|
+
});
|
|
282
|
+
} catch (error) {
|
|
283
|
+
return new Response(JSON.stringify({ error: "Failed to change directory" }), {
|
|
284
|
+
status: 500,
|
|
285
|
+
headers: { "Content-Type": "application/json" },
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (url.pathname === "/api/files" && request.method === "GET") {
|
|
291
|
+
const urlObj = new URL(request.url);
|
|
292
|
+
const queryPath = urlObj.searchParams.get("path");
|
|
293
|
+
const currentPath = queryPath || process.cwd();
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
if (!existsSync(currentPath)) {
|
|
297
|
+
return new Response(JSON.stringify({ error: "Path does not exist" }), {
|
|
298
|
+
status: 404,
|
|
299
|
+
headers: { "Content-Type": "application/json" },
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const items = readdirSync(currentPath, { withFileTypes: true });
|
|
304
|
+
const files = items.map((item) => ({
|
|
305
|
+
name: item.name,
|
|
306
|
+
isDirectory: item.isDirectory(),
|
|
307
|
+
path: join(currentPath, item.name)
|
|
308
|
+
})).sort((a, b) => {
|
|
309
|
+
if (a.isDirectory === b.isDirectory) {
|
|
310
|
+
return a.name.localeCompare(b.name);
|
|
311
|
+
}
|
|
312
|
+
return a.isDirectory ? -1 : 1;
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
return new Response(JSON.stringify({
|
|
316
|
+
path: currentPath,
|
|
317
|
+
files
|
|
318
|
+
}), {
|
|
319
|
+
headers: { "Content-Type": "application/json" },
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
} catch (error) {
|
|
323
|
+
return new Response(JSON.stringify({ error: "Failed to list files" }), {
|
|
324
|
+
status: 500,
|
|
325
|
+
headers: { "Content-Type": "application/json" },
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (url.pathname === "/api/recent-projects" && request.method === "GET") {
|
|
331
|
+
const { getRecentProjects } = await import("../utils/config");
|
|
332
|
+
const recentProjects = getRecentProjects();
|
|
333
|
+
return new Response(JSON.stringify(recentProjects), {
|
|
334
|
+
headers: { "Content-Type": "application/json" },
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (url.pathname === "/api/config" && request.method === "GET") {
|
|
339
|
+
const { readConfig } = await import("../utils/config");
|
|
340
|
+
const config = readConfig();
|
|
341
|
+
return new Response(JSON.stringify({
|
|
342
|
+
provider: config.provider,
|
|
343
|
+
model: config.model
|
|
344
|
+
}), {
|
|
345
|
+
headers: { "Content-Type": "application/json" },
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (url.pathname === "/api/add-recent-project" && request.method === "POST") {
|
|
350
|
+
const body = (await request.json()) as { path: string };
|
|
351
|
+
if (!body.path || typeof body.path !== "string") {
|
|
352
|
+
return new Response(JSON.stringify({ error: "Invalid path" }), {
|
|
353
|
+
status: 400,
|
|
354
|
+
headers: { "Content-Type": "application/json" },
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
const { addRecentProject } = await import("../utils/config");
|
|
358
|
+
addRecentProject(body.path);
|
|
359
|
+
addLog(`Added recent project: ${body.path}`);
|
|
360
|
+
return new Response(JSON.stringify({ success: true }), {
|
|
361
|
+
headers: { "Content-Type": "application/json" },
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (url.pathname === "/api/question/answer" && request.method === "POST") {
|
|
366
|
+
const body = (await request.json()) as { index: number; customText?: string };
|
|
367
|
+
answerQuestion(body.index, body.customText);
|
|
368
|
+
return new Response(JSON.stringify({ success: true }), {
|
|
369
|
+
headers: { "Content-Type": "application/json" },
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (url.pathname === "/api/approval/respond" && request.method === "POST") {
|
|
374
|
+
const body = (await request.json()) as { approved: boolean; customResponse?: string };
|
|
375
|
+
respondApproval(body.approved, body.customResponse);
|
|
376
|
+
return new Response(JSON.stringify({ success: true }), {
|
|
377
|
+
headers: { "Content-Type": "application/json" },
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (url.pathname === "/api/stop" && request.method === "POST") {
|
|
382
|
+
if (currentAbortController) {
|
|
383
|
+
currentAbortController.abort();
|
|
384
|
+
currentAbortController = null;
|
|
385
|
+
addLog("Agent stopped by user");
|
|
386
|
+
return new Response(JSON.stringify({ success: true, message: "Agent stopped" }), {
|
|
387
|
+
headers: { "Content-Type": "application/json" },
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
return new Response(JSON.stringify({ success: false, message: "No agent running" }), {
|
|
391
|
+
headers: { "Content-Type": "application/json" },
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (url.pathname === "/api/message" && request.method === "POST") {
|
|
396
|
+
const body = (await request.json()) as {
|
|
397
|
+
message: string;
|
|
398
|
+
history: Array<{ role: string; content: string }>;
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
if (!body.message || typeof body.message !== "string") {
|
|
402
|
+
addLog("Invalid message format");
|
|
403
|
+
return new Response(JSON.stringify({ error: "Invalid message format" }), {
|
|
404
|
+
status: 400,
|
|
405
|
+
headers: { "Content-Type": "application/json" },
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
addLog("Message received");
|
|
411
|
+
|
|
412
|
+
currentAbortController = new AbortController();
|
|
413
|
+
const abortSignal = currentAbortController.signal;
|
|
414
|
+
|
|
415
|
+
const encoder = new TextEncoder();
|
|
416
|
+
const stream = new ReadableStream({
|
|
417
|
+
async start(controller) {
|
|
418
|
+
let keepAlive: ReturnType<typeof setInterval> | null = null;
|
|
419
|
+
let aborted = false;
|
|
420
|
+
|
|
421
|
+
const cleanup = () => {
|
|
422
|
+
if (keepAlive) clearInterval(keepAlive);
|
|
423
|
+
currentAbortController = null;
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
const safeEnqueue = (text: string) => {
|
|
427
|
+
if (aborted) return false;
|
|
428
|
+
try {
|
|
429
|
+
controller.enqueue(encoder.encode(text));
|
|
430
|
+
return true;
|
|
431
|
+
} catch {
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
abortSignal.addEventListener('abort', () => {
|
|
437
|
+
aborted = true;
|
|
438
|
+
safeEnqueue(JSON.stringify({ type: 'stopped', message: 'Agent stopped by user' }) + "\n");
|
|
439
|
+
cleanup();
|
|
440
|
+
questionUnsub();
|
|
441
|
+
approvalUnsub();
|
|
442
|
+
exploreUnsub?.();
|
|
443
|
+
try { controller.close(); } catch { }
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
const questionUnsub = subscribeQuestion((req) => {
|
|
447
|
+
safeEnqueue(JSON.stringify({ type: 'question', request: req }) + "\n");
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
const approvalUnsub = subscribeApproval((req) => {
|
|
452
|
+
safeEnqueue(JSON.stringify({ type: 'approval', request: req }) + "\n");
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
keepAlive = setInterval(() => {
|
|
456
|
+
safeEnqueue(JSON.stringify({ type: 'ping' }) + "\n");
|
|
457
|
+
}, 5000);
|
|
458
|
+
|
|
459
|
+
let exploreUnsub: (() => void) | null = null;
|
|
460
|
+
|
|
461
|
+
try {
|
|
462
|
+
const { Agent } = await import("../agent");
|
|
463
|
+
const { subscribeExploreTool } = await import("../utils/exploreBridge");
|
|
464
|
+
|
|
465
|
+
addLog("[EXPLORE] Subscribing...");
|
|
466
|
+
exploreUnsub = subscribeExploreTool((event) => {
|
|
467
|
+
addLog(`[EXPLORE] Tool: ${event.toolName}`);
|
|
468
|
+
safeEnqueue(JSON.stringify({ type: 'explore-tool', ...event }) + "\n");
|
|
469
|
+
});
|
|
470
|
+
addLog("[EXPLORE] Subscribed");
|
|
471
|
+
const providerStatus = await Agent.ensureProviderReady();
|
|
472
|
+
|
|
473
|
+
if (!providerStatus.ready) {
|
|
474
|
+
safeEnqueue(
|
|
475
|
+
JSON.stringify({
|
|
476
|
+
type: "error",
|
|
477
|
+
error: providerStatus.error || "Provider not ready",
|
|
478
|
+
}) + "\n"
|
|
479
|
+
);
|
|
480
|
+
cleanup();
|
|
481
|
+
questionUnsub();
|
|
482
|
+
approvalUnsub();
|
|
483
|
+
exploreUnsub?.();
|
|
484
|
+
controller.close();
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const agent = new Agent();
|
|
489
|
+
const conversationHistory = body.history || [];
|
|
490
|
+
conversationHistory.push({ role: "user", content: body.message });
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
for await (const event of agent.streamMessages(conversationHistory as any, {})) {
|
|
494
|
+
if (aborted) break;
|
|
495
|
+
if (!safeEnqueue(JSON.stringify(event) + "\n")) break;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
cleanup();
|
|
499
|
+
questionUnsub();
|
|
500
|
+
approvalUnsub();
|
|
501
|
+
exploreUnsub?.();
|
|
502
|
+
if (!aborted) controller.close();
|
|
503
|
+
} catch (error) {
|
|
504
|
+
if (!aborted) {
|
|
505
|
+
safeEnqueue(
|
|
506
|
+
JSON.stringify({
|
|
507
|
+
type: "error",
|
|
508
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
509
|
+
}) + "\n"
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
cleanup();
|
|
513
|
+
questionUnsub();
|
|
514
|
+
approvalUnsub();
|
|
515
|
+
exploreUnsub?.();
|
|
516
|
+
try { controller.close(); } catch { }
|
|
517
|
+
}
|
|
518
|
+
},
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
return new Response(stream, {
|
|
523
|
+
headers: {
|
|
524
|
+
"Content-Type": "text/event-stream",
|
|
525
|
+
"Cache-Control": "no-cache",
|
|
526
|
+
Connection: "keep-alive",
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
addLog(`${request.method} ${url.pathname} (404)`);
|
|
533
|
+
return new Response("Not Found", { status: 404 });
|
|
534
|
+
|
|
535
|
+
} catch (error) {
|
|
536
|
+
console.error("Request error:", error);
|
|
537
|
+
addLog(`Server error: ${error instanceof Error ? error.message : "Unknown"}`);
|
|
538
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
539
|
+
}
|
|
540
|
+
},
|
|
541
|
+
error(error) {
|
|
542
|
+
console.error("Server error:", error);
|
|
543
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
544
|
+
},
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
currentPort = port;
|
|
548
|
+
const serverUrl = `http://${HOST}:${port}`;
|
|
549
|
+
const openCommand = process.platform === "win32" ? `start ${serverUrl}` :
|
|
550
|
+
process.platform === "darwin" ? `open ${serverUrl}` :
|
|
551
|
+
`xdg-open ${serverUrl}`;
|
|
552
|
+
|
|
553
|
+
exec(openCommand, (error) => {
|
|
554
|
+
if (error) {
|
|
555
|
+
console.error("Failed to open browser:", error);
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
return server;
|
|
560
|
+
} catch (err: any) {
|
|
561
|
+
if (err.code === "EADDRINUSE") {
|
|
562
|
+
if (maxRetries > 0) {
|
|
563
|
+
console.log(`Port ${port} is in use, trying ${port + 1}...`);
|
|
564
|
+
return startServer(port + 1, maxRetries - 1);
|
|
565
|
+
} else {
|
|
566
|
+
console.error(`Failed to find an available port after retries.`);
|
|
567
|
+
throw err;
|
|
568
|
+
}
|
|
569
|
+
} else {
|
|
570
|
+
throw err;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
await startServer(PORT);
|
|
576
|
+
|
|
577
|
+
function ServerStatus() {
|
|
578
|
+
const [logList, setLogList] = React.useState<LogEntry[]>(logs);
|
|
579
|
+
const [scrollOffset, setScrollOffset] = React.useState(0);
|
|
580
|
+
const [terminalHeight, setTerminalHeight] = React.useState(process.stdout.rows || 24);
|
|
581
|
+
|
|
582
|
+
React.useEffect(() => {
|
|
583
|
+
const listener = () => {
|
|
584
|
+
setLogList([...logs]);
|
|
585
|
+
setScrollOffset(Math.max(0, logs.length - (terminalHeight - 6)));
|
|
586
|
+
};
|
|
587
|
+
listeners.add(listener);
|
|
588
|
+
return () => {
|
|
589
|
+
listeners.delete(listener);
|
|
590
|
+
};
|
|
591
|
+
}, [terminalHeight]);
|
|
592
|
+
|
|
593
|
+
React.useEffect(() => {
|
|
594
|
+
const handleResize = () => {
|
|
595
|
+
setTerminalHeight(process.stdout.rows || 24);
|
|
596
|
+
};
|
|
597
|
+
process.stdout.on('resize', handleResize);
|
|
598
|
+
return () => {
|
|
599
|
+
process.stdout.off('resize', handleResize);
|
|
600
|
+
};
|
|
601
|
+
}, []);
|
|
602
|
+
|
|
603
|
+
React.useEffect(() => {
|
|
604
|
+
const handleData = (data: Buffer) => {
|
|
605
|
+
const str = data.toString();
|
|
606
|
+
if (str.includes('\x03')) {
|
|
607
|
+
process.exit(0);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (str.match(/\x1b\[<64;\d+;\d+M/)) {
|
|
611
|
+
setScrollOffset(prev => Math.max(0, prev - 1));
|
|
612
|
+
} else if (str.match(/\x1b\[<65;\d+;\d+M/)) {
|
|
613
|
+
setScrollOffset(prev => prev + 1);
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
if (process.stdin.isTTY) {
|
|
618
|
+
process.stdin.setRawMode(true);
|
|
619
|
+
process.stdout.write('\x1b[?1000h\x1b[?1006h\x1b[?1003h');
|
|
620
|
+
process.stdin.on('data', handleData);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return () => {
|
|
624
|
+
if (process.stdin.isTTY) {
|
|
625
|
+
process.stdin.off('data', handleData);
|
|
626
|
+
process.stdout.write('\x1b[?1000l\x1b[?1006l\x1b[?1003l');
|
|
627
|
+
process.stdin.setRawMode(false);
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
}, []);
|
|
631
|
+
|
|
632
|
+
const logsHeight = Math.max(5, terminalHeight - 6);
|
|
633
|
+
const visibleLogs = logList.slice(scrollOffset, scrollOffset + logsHeight);
|
|
634
|
+
|
|
635
|
+
return (
|
|
636
|
+
<box flexDirection="column" width="100%" height="100%" justifyContent="flex-start" alignItems="center" paddingTop={1}>
|
|
637
|
+
<box flexDirection="row" marginBottom={1}>
|
|
638
|
+
<text fg="#ffca38" attributes={TextAttributes.BOLD}>
|
|
639
|
+
Web interface:{" "}
|
|
640
|
+
</text>
|
|
641
|
+
<text fg="gray">http://{HOST}:{currentPort}</text>
|
|
642
|
+
</box>
|
|
643
|
+
|
|
644
|
+
<box flexDirection="column" width={80} height={logsHeight} borderStyle="rounded" borderColor="gray" title={`Server Logs`}>
|
|
645
|
+
{logList.length === 0 ? (
|
|
646
|
+
<text fg="gray" attributes={TextAttributes.DIM}>
|
|
647
|
+
No logs yet...
|
|
648
|
+
</text>
|
|
649
|
+
) : (
|
|
650
|
+
visibleLogs.map((log, i) => (
|
|
651
|
+
<text key={i} fg="gray">
|
|
652
|
+
[{log.timestamp}] {log.message}
|
|
653
|
+
</text>
|
|
654
|
+
))
|
|
655
|
+
)}
|
|
656
|
+
</box>
|
|
657
|
+
</box>
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const renderer = await createCliRenderer();
|
|
662
|
+
createRoot(renderer).render(<ServerStatus />);
|