@kirosnn/mosaic 0.71.0 → 0.73.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -5
- package/package.json +4 -2
- package/src/agent/Agent.ts +353 -131
- package/src/agent/context.ts +4 -4
- package/src/agent/prompts/systemPrompt.ts +15 -6
- package/src/agent/prompts/toolsPrompt.ts +75 -10
- package/src/agent/provider/anthropic.ts +100 -100
- package/src/agent/provider/google.ts +102 -102
- package/src/agent/provider/mistral.ts +95 -95
- package/src/agent/provider/ollama.ts +77 -60
- package/src/agent/provider/openai.ts +42 -38
- package/src/agent/provider/rateLimit.ts +178 -0
- package/src/agent/provider/xai.ts +99 -99
- package/src/agent/tools/definitions.ts +19 -9
- package/src/agent/tools/executor.ts +95 -85
- package/src/agent/tools/exploreExecutor.ts +8 -10
- package/src/agent/tools/grep.ts +30 -29
- package/src/agent/tools/question.ts +7 -1
- package/src/agent/types.ts +9 -8
- package/src/components/App.tsx +45 -45
- package/src/components/CustomInput.tsx +214 -36
- package/src/components/Main.tsx +1146 -954
- package/src/components/Setup.tsx +1 -1
- package/src/components/Welcome.tsx +1 -1
- package/src/components/main/ApprovalPanel.tsx +4 -3
- package/src/components/main/ChatPage.tsx +858 -675
- package/src/components/main/HomePage.tsx +53 -38
- package/src/components/main/QuestionPanel.tsx +52 -7
- package/src/components/main/ThinkingIndicator.tsx +2 -1
- package/src/index.tsx +50 -20
- package/src/mcp/approvalPolicy.ts +148 -0
- package/src/mcp/cli/add.ts +185 -0
- package/src/mcp/cli/doctor.ts +77 -0
- package/src/mcp/cli/index.ts +85 -0
- package/src/mcp/cli/list.ts +50 -0
- package/src/mcp/cli/logs.ts +24 -0
- package/src/mcp/cli/manage.ts +99 -0
- package/src/mcp/cli/show.ts +53 -0
- package/src/mcp/cli/tools.ts +77 -0
- package/src/mcp/config.ts +223 -0
- package/src/mcp/index.ts +80 -0
- package/src/mcp/processManager.ts +299 -0
- package/src/mcp/rateLimiter.ts +50 -0
- package/src/mcp/registry.ts +151 -0
- package/src/mcp/schemaConverter.ts +100 -0
- package/src/mcp/servers/navigation.ts +854 -0
- package/src/mcp/toolCatalog.ts +169 -0
- package/src/mcp/types.ts +95 -0
- package/src/utils/approvalBridge.ts +17 -5
- package/src/utils/commands/compact.ts +30 -0
- package/src/utils/commands/echo.ts +1 -1
- package/src/utils/commands/index.ts +4 -6
- package/src/utils/commands/new.ts +15 -0
- package/src/utils/commands/types.ts +3 -0
- package/src/utils/config.ts +3 -1
- package/src/utils/diffRendering.tsx +1 -3
- package/src/utils/exploreBridge.ts +10 -0
- package/src/utils/markdown.tsx +163 -99
- package/src/utils/models.ts +31 -9
- package/src/utils/questionBridge.ts +36 -1
- package/src/utils/tokenEstimator.ts +32 -0
- package/src/utils/toolFormatting.ts +268 -7
- package/src/web/app.tsx +72 -72
- package/src/web/components/HomePage.tsx +7 -7
- package/src/web/components/MessageItem.tsx +22 -22
- package/src/web/components/QuestionPanel.tsx +72 -12
- package/src/web/components/Sidebar.tsx +0 -2
- package/src/web/components/ThinkingIndicator.tsx +1 -0
- package/src/web/server.tsx +767 -683
- package/src/utils/commands/redo.ts +0 -74
- package/src/utils/commands/sessions.ts +0 -129
- package/src/utils/commands/undo.ts +0 -75
- package/src/utils/undoRedo.ts +0 -429
- package/src/utils/undoRedoBridge.ts +0 -45
- package/src/utils/undoRedoDb.ts +0 -338
package/src/web/server.tsx
CHANGED
|
@@ -1,114 +1,176 @@
|
|
|
1
|
-
import { serve } from "bun";
|
|
2
|
-
import { join } from "path";
|
|
3
|
-
import { existsSync, readdirSync
|
|
4
|
-
import { build } from "bun";
|
|
1
|
+
import { serve } from "bun";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { existsSync, readdirSync } from "fs";
|
|
4
|
+
import { build } from "bun";
|
|
5
5
|
import { createCliRenderer, TextAttributes } from "@opentui/core";
|
|
6
6
|
import { createRoot } from "@opentui/react";
|
|
7
7
|
import React from "react";
|
|
8
8
|
import { exec } from "child_process";
|
|
9
9
|
import type { ImagePart, TextPart, UserContent } from "ai";
|
|
10
10
|
import type { ImageAttachment } from "../utils/images";
|
|
11
|
-
|
|
12
|
-
const PORT = 8192;
|
|
13
|
-
const HOST = "127.0.0.1";
|
|
14
|
-
|
|
15
|
-
import { subscribeQuestion, answerQuestion } from "../utils/questionBridge";
|
|
11
|
+
|
|
12
|
+
const PORT = 8192;
|
|
13
|
+
const HOST = "127.0.0.1";
|
|
14
|
+
|
|
15
|
+
import { subscribeQuestion, answerQuestion } from "../utils/questionBridge";
|
|
16
16
|
import { subscribeApproval, respondApproval, getCurrentApproval } from "../utils/approvalBridge";
|
|
17
|
-
|
|
18
|
-
let currentAbortController: AbortController | null = null;
|
|
19
|
-
|
|
20
|
-
const HTML_TEMPLATE = `<!DOCTYPE html>
|
|
21
|
-
<html lang="en">
|
|
22
|
-
<head>
|
|
23
|
-
<meta charset="UTF-8">
|
|
24
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
25
|
-
<title>Mosaic</title>
|
|
26
|
-
<link rel="icon" type="image/svg+xml" href="/logo_black.svg" media="(prefers-color-scheme: light)">
|
|
27
|
-
<link rel="icon" type="image/svg+xml" href="/logo_white.svg" media="(prefers-color-scheme: dark)">
|
|
28
|
-
<link rel="stylesheet" href="/app.css">
|
|
29
|
-
</head>
|
|
30
|
-
<body>
|
|
31
|
-
<div id="root"></div>
|
|
32
|
-
<script type="module" src="/app.js"></script>
|
|
33
|
-
</body>
|
|
34
|
-
</html>`;
|
|
35
|
-
|
|
36
|
-
type LogEntry = { message: string; timestamp: string };
|
|
37
|
-
|
|
38
|
-
const logs: LogEntry[] = [];
|
|
39
|
-
const listeners: Set<() => void> = new Set();
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
17
|
+
|
|
18
|
+
let currentAbortController: AbortController | null = null;
|
|
19
|
+
|
|
20
|
+
const HTML_TEMPLATE = `<!DOCTYPE html>
|
|
21
|
+
<html lang="en">
|
|
22
|
+
<head>
|
|
23
|
+
<meta charset="UTF-8">
|
|
24
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
25
|
+
<title>Mosaic</title>
|
|
26
|
+
<link rel="icon" type="image/svg+xml" href="/logo_black.svg" media="(prefers-color-scheme: light)">
|
|
27
|
+
<link rel="icon" type="image/svg+xml" href="/logo_white.svg" media="(prefers-color-scheme: dark)">
|
|
28
|
+
<link rel="stylesheet" href="/app.css">
|
|
29
|
+
</head>
|
|
30
|
+
<body>
|
|
31
|
+
<div id="root"></div>
|
|
32
|
+
<script type="module" src="/app.js"></script>
|
|
33
|
+
</body>
|
|
34
|
+
</html>`;
|
|
35
|
+
|
|
36
|
+
type LogEntry = { message: string; timestamp: string };
|
|
37
|
+
|
|
38
|
+
const logs: LogEntry[] = [];
|
|
39
|
+
const listeners: Set<() => void> = new Set();
|
|
40
|
+
const MAX_HISTORY_MESSAGES = 24;
|
|
41
|
+
const PROVIDER_CONCURRENCY = 1;
|
|
42
|
+
const PROVIDER_QUEUE_TIMEOUT_MS = 30000;
|
|
43
|
+
|
|
44
|
+
type ReleaseFn = () => void;
|
|
45
|
+
|
|
46
|
+
type QueueItem = {
|
|
47
|
+
resolve: (release: ReleaseFn) => void;
|
|
48
|
+
cancelled: boolean;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type QueueEntry = {
|
|
52
|
+
inflight: number;
|
|
53
|
+
queue: QueueItem[];
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const providerQueues = new Map<string, QueueEntry>();
|
|
57
|
+
|
|
58
|
+
function createRelease(entry: QueueEntry): ReleaseFn {
|
|
59
|
+
let released = false;
|
|
60
|
+
return () => {
|
|
61
|
+
if (released) return;
|
|
62
|
+
released = true;
|
|
63
|
+
entry.inflight = Math.max(0, entry.inflight - 1);
|
|
64
|
+
while (entry.queue.length > 0) {
|
|
65
|
+
const next = entry.queue.shift();
|
|
66
|
+
if (!next || next.cancelled) continue;
|
|
67
|
+
entry.inflight += 1;
|
|
68
|
+
next.resolve(createRelease(entry));
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function acquireProviderSlot(key: string, limit = PROVIDER_CONCURRENCY): { promise: Promise<ReleaseFn>; cancel: () => void } {
|
|
75
|
+
let entry = providerQueues.get(key);
|
|
76
|
+
if (!entry) {
|
|
77
|
+
entry = { inflight: 0, queue: [] };
|
|
78
|
+
providerQueues.set(key, entry);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (entry.inflight < limit) {
|
|
82
|
+
entry.inflight += 1;
|
|
83
|
+
return {
|
|
84
|
+
promise: Promise.resolve(createRelease(entry)),
|
|
85
|
+
cancel: () => { }
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let item: QueueItem | null = null;
|
|
90
|
+
const promise = new Promise<ReleaseFn>((resolve) => {
|
|
91
|
+
item = { resolve, cancelled: false };
|
|
92
|
+
entry!.queue.push(item);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
promise,
|
|
97
|
+
cancel: () => {
|
|
98
|
+
if (item) item.cancelled = true;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function addLog(message: string) {
|
|
104
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
105
|
+
const clean = String(message ?? "").replace(/\r/g, "").trimEnd();
|
|
106
|
+
if (!clean) return;
|
|
107
|
+
|
|
108
|
+
const lines = clean.split("\n");
|
|
109
|
+
for (const line of lines) {
|
|
110
|
+
if (!line) continue;
|
|
111
|
+
logs.push({ message: line, timestamp });
|
|
112
|
+
|
|
113
|
+
}
|
|
114
|
+
while (logs.length > 50) logs.shift();
|
|
115
|
+
listeners.forEach((l) => l());
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function installExternalLogCapture() {
|
|
119
|
+
const originalLog = console.log.bind(console);
|
|
120
|
+
const originalInfo = console.info.bind(console);
|
|
121
|
+
const originalWarn = console.warn.bind(console);
|
|
122
|
+
const originalError = console.error.bind(console);
|
|
123
|
+
|
|
124
|
+
console.log = (...args: any[]) => {
|
|
125
|
+
addLog(args.map(String).join(" "));
|
|
126
|
+
originalLog(...args);
|
|
127
|
+
};
|
|
128
|
+
console.info = (...args: any[]) => {
|
|
129
|
+
addLog(args.map(String).join(" "));
|
|
130
|
+
originalInfo(...args);
|
|
131
|
+
};
|
|
132
|
+
console.warn = (...args: any[]) => {
|
|
133
|
+
addLog(args.map(String).join(" "));
|
|
134
|
+
originalWarn(...args);
|
|
135
|
+
};
|
|
136
|
+
console.error = (...args: any[]) => {
|
|
137
|
+
addLog(args.map(String).join(" "));
|
|
138
|
+
originalError(...args);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
if (typeof process !== "undefined" && process?.stdout?.write) {
|
|
142
|
+
const originalStdoutWrite = process.stdout.write.bind(process.stdout) as (
|
|
143
|
+
chunk: any,
|
|
144
|
+
encoding?: any,
|
|
145
|
+
cb?: any
|
|
146
|
+
) => boolean;
|
|
147
|
+
|
|
148
|
+
process.stdout.write = ((chunk: any, encoding?: any, cb?: any) => {
|
|
149
|
+
try {
|
|
150
|
+
const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
|
|
151
|
+
addLog(text);
|
|
152
|
+
} catch { }
|
|
153
|
+
return originalStdoutWrite(chunk, encoding as any, cb as any);
|
|
154
|
+
}) as any;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (typeof process !== "undefined" && process?.stderr?.write) {
|
|
158
|
+
const originalStderrWrite = process.stderr.write.bind(process.stderr) as (
|
|
159
|
+
chunk: any,
|
|
160
|
+
encoding?: any,
|
|
161
|
+
cb?: any
|
|
162
|
+
) => boolean;
|
|
163
|
+
|
|
164
|
+
process.stderr.write = ((chunk: any, encoding?: any, cb?: any) => {
|
|
165
|
+
try {
|
|
166
|
+
const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
|
|
167
|
+
addLog(text);
|
|
168
|
+
} catch { }
|
|
169
|
+
return originalStderrWrite(chunk, encoding as any, cb as any);
|
|
170
|
+
}) as any;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
112
174
|
installExternalLogCapture();
|
|
113
175
|
|
|
114
176
|
let appJsContent: string | null = null;
|
|
@@ -128,9 +190,9 @@ function buildConversationHistory(
|
|
|
128
190
|
history: Array<{ role: string; content: string; images?: ImageAttachment[] }>,
|
|
129
191
|
allowImages: boolean
|
|
130
192
|
) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
193
|
+
const filtered = history.filter((m) => m.role === "user" || m.role === "assistant");
|
|
194
|
+
const sliced = filtered.slice(-MAX_HISTORY_MESSAGES);
|
|
195
|
+
return sliced.map((m) => {
|
|
134
196
|
if (m.role === "user") {
|
|
135
197
|
const content = allowImages ? buildUserContent(m.content, m.images) : m.content;
|
|
136
198
|
return { role: "user" as const, content };
|
|
@@ -138,230 +200,227 @@ function buildConversationHistory(
|
|
|
138
200
|
return { role: "assistant" as const, content: m.content };
|
|
139
201
|
});
|
|
140
202
|
}
|
|
141
|
-
|
|
142
|
-
async function buildApp() {
|
|
143
|
-
const appPath = join(__dirname, "app.tsx");
|
|
144
|
-
|
|
145
|
-
if (!existsSync(appPath)) {
|
|
146
|
-
throw new Error(`App file not found at: ${appPath}`);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const buildResult = await build({
|
|
150
|
-
entrypoints: [appPath],
|
|
151
|
-
target: "browser",
|
|
152
|
-
format: "esm",
|
|
153
|
-
minify: false,
|
|
154
|
-
splitting: false,
|
|
155
|
-
sourcemap: "none",
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (!buildResult.success) {
|
|
160
|
-
throw new Error("Build failed");
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const outputs = buildResult.outputs;
|
|
164
|
-
if (outputs.length === 0) {
|
|
165
|
-
throw new Error("No build output generated");
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
for (const output of outputs) {
|
|
169
|
-
if (output.path.endsWith('.js') || output.kind === 'entry-point') {
|
|
170
|
-
appJsContent = await output.text();
|
|
171
|
-
} else if (output.path.endsWith('.css') || output.type === 'text/css') {
|
|
172
|
-
appCssContent = await output.text();
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
try {
|
|
178
|
-
await buildApp();
|
|
179
|
-
addLog("App built");
|
|
180
|
-
|
|
181
|
-
const projectPath = process.env.MOSAIC_PROJECT_PATH;
|
|
182
|
-
if (projectPath) {
|
|
183
|
-
const { addRecentProject } = await import("../utils/config");
|
|
184
|
-
addRecentProject(projectPath);
|
|
185
|
-
addLog(`Project added to recents: ${projectPath}`);
|
|
186
|
-
}
|
|
187
|
-
} catch (error) {
|
|
188
|
-
console.error("Failed to build app:", error);
|
|
189
|
-
throw error;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
let currentPort = PORT;
|
|
194
|
-
|
|
195
|
-
async function startServer(port: number, maxRetries = 10) {
|
|
196
|
-
try {
|
|
197
|
-
const server = serve({
|
|
198
|
-
port: port,
|
|
199
|
-
hostname: HOST,
|
|
200
|
-
idleTimeout: 0,
|
|
201
|
-
async fetch(request) {
|
|
202
|
-
const url = new URL(request.url);
|
|
203
|
-
|
|
204
|
-
try {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
isDirectory
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
})
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
|
|
203
|
+
|
|
204
|
+
async function buildApp() {
|
|
205
|
+
const appPath = join(__dirname, "app.tsx");
|
|
206
|
+
|
|
207
|
+
if (!existsSync(appPath)) {
|
|
208
|
+
throw new Error(`App file not found at: ${appPath}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const buildResult = await build({
|
|
212
|
+
entrypoints: [appPath],
|
|
213
|
+
target: "browser",
|
|
214
|
+
format: "esm",
|
|
215
|
+
minify: false,
|
|
216
|
+
splitting: false,
|
|
217
|
+
sourcemap: "none",
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
if (!buildResult.success) {
|
|
222
|
+
throw new Error("Build failed");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const outputs = buildResult.outputs;
|
|
226
|
+
if (outputs.length === 0) {
|
|
227
|
+
throw new Error("No build output generated");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
for (const output of outputs) {
|
|
231
|
+
if (output.path.endsWith('.js') || output.kind === 'entry-point') {
|
|
232
|
+
appJsContent = await output.text();
|
|
233
|
+
} else if (output.path.endsWith('.css') || output.type === 'text/css') {
|
|
234
|
+
appCssContent = await output.text();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
await buildApp();
|
|
241
|
+
addLog("App built");
|
|
242
|
+
|
|
243
|
+
const projectPath = process.env.MOSAIC_PROJECT_PATH;
|
|
244
|
+
if (projectPath) {
|
|
245
|
+
const { addRecentProject } = await import("../utils/config");
|
|
246
|
+
addRecentProject(projectPath);
|
|
247
|
+
addLog(`Project added to recents: ${projectPath}`);
|
|
248
|
+
}
|
|
249
|
+
} catch (error) {
|
|
250
|
+
console.error("Failed to build app:", error);
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
let currentPort = PORT;
|
|
256
|
+
|
|
257
|
+
async function startServer(port: number, maxRetries = 10) {
|
|
258
|
+
try {
|
|
259
|
+
const server = serve({
|
|
260
|
+
port: port,
|
|
261
|
+
hostname: HOST,
|
|
262
|
+
idleTimeout: 0,
|
|
263
|
+
async fetch(request) {
|
|
264
|
+
const url = new URL(request.url);
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
if (url.pathname === "/" || url.pathname === "/home" || url.pathname.startsWith("/chat")) {
|
|
268
|
+
addLog(`${request.method} ${url.pathname}`);
|
|
269
|
+
return new Response(HTML_TEMPLATE, {
|
|
270
|
+
headers: { "Content-Type": "text/html" },
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (url.pathname === "/app.js") {
|
|
275
|
+
if (!appJsContent) {
|
|
276
|
+
addLog("App not built");
|
|
277
|
+
return new Response("App not built", { status: 500 });
|
|
278
|
+
|
|
279
|
+
}
|
|
280
|
+
addLog(`${request.method} /app.js`);
|
|
281
|
+
return new Response(appJsContent, {
|
|
282
|
+
headers: {
|
|
283
|
+
"Content-Type": "application/javascript",
|
|
284
|
+
"Cache-Control": "no-cache",
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (url.pathname === "/app.css") {
|
|
291
|
+
if (!appCssContent) {
|
|
292
|
+
return new Response("", { headers: { "Content-Type": "text/css" } });
|
|
293
|
+
|
|
294
|
+
}
|
|
295
|
+
addLog(`${request.method} /app.css`);
|
|
296
|
+
return new Response(appCssContent, {
|
|
297
|
+
headers: {
|
|
298
|
+
"Content-Type": "text/css",
|
|
299
|
+
"Cache-Control": "no-cache",
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (url.pathname === "/logo_black.svg") {
|
|
306
|
+
const logoPath = join(__dirname, "logo_black.svg");
|
|
307
|
+
if (existsSync(logoPath)) {
|
|
308
|
+
return new Response(Bun.file(logoPath), {
|
|
309
|
+
headers: { "Content-Type": "image/svg+xml" }
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
}
|
|
313
|
+
return new Response("Not Found", { status: 404 });
|
|
314
|
+
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (url.pathname === "/logo_white.svg") {
|
|
318
|
+
const logoPath = join(__dirname, "logo_white.svg");
|
|
319
|
+
if (existsSync(logoPath)) {
|
|
320
|
+
return new Response(Bun.file(logoPath), {
|
|
321
|
+
headers: { "Content-Type": "image/svg+xml" }
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
}
|
|
325
|
+
return new Response("Not Found", { status: 404 });
|
|
326
|
+
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (url.pathname === "/favicon.ico") {
|
|
330
|
+
const faviconPath = join(__dirname, "favicon.ico");
|
|
331
|
+
if (existsSync(faviconPath)) {
|
|
332
|
+
return new Response(Bun.file(faviconPath));
|
|
333
|
+
}
|
|
334
|
+
return new Response("Not Found", { status: 404 });
|
|
335
|
+
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (url.pathname === "/favicon.png") {
|
|
339
|
+
const faviconPath = join(__dirname, "favicon.png");
|
|
340
|
+
if (existsSync(faviconPath)) {
|
|
341
|
+
return new Response(Bun.file(faviconPath));
|
|
342
|
+
}
|
|
343
|
+
return new Response("Not Found", { status: 404 });
|
|
344
|
+
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (url.pathname === "/api/workspace" && request.method === "GET") {
|
|
348
|
+
const workspace = process.cwd();
|
|
349
|
+
return new Response(JSON.stringify({ workspace }), {
|
|
350
|
+
headers: { "Content-Type": "application/json" },
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (url.pathname === "/api/workspace" && request.method === "POST") {
|
|
355
|
+
const body = (await request.json()) as { path: string };
|
|
356
|
+
if (!body.path || typeof body.path !== "string") {
|
|
357
|
+
return new Response(JSON.stringify({ error: "Invalid path" }), {
|
|
358
|
+
status: 400,
|
|
359
|
+
headers: { "Content-Type": "application/json" },
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
process.chdir(body.path);
|
|
365
|
+
return new Response(JSON.stringify({ success: true, workspace: process.cwd() }), {
|
|
366
|
+
headers: { "Content-Type": "application/json" },
|
|
367
|
+
});
|
|
368
|
+
} catch (error) {
|
|
369
|
+
return new Response(JSON.stringify({ error: "Failed to change directory" }), {
|
|
370
|
+
status: 500,
|
|
371
|
+
headers: { "Content-Type": "application/json" },
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (url.pathname === "/api/files" && request.method === "GET") {
|
|
377
|
+
const urlObj = new URL(request.url);
|
|
378
|
+
const queryPath = urlObj.searchParams.get("path");
|
|
379
|
+
const currentPath = queryPath || process.cwd();
|
|
380
|
+
|
|
381
|
+
try {
|
|
382
|
+
if (!existsSync(currentPath)) {
|
|
383
|
+
return new Response(JSON.stringify({ error: "Path does not exist" }), {
|
|
384
|
+
status: 404,
|
|
385
|
+
headers: { "Content-Type": "application/json" },
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const items = readdirSync(currentPath, { withFileTypes: true });
|
|
390
|
+
const files = items.map((item) => ({
|
|
391
|
+
name: item.name,
|
|
392
|
+
isDirectory: item.isDirectory(),
|
|
393
|
+
path: join(currentPath, item.name)
|
|
394
|
+
})).sort((a, b) => {
|
|
395
|
+
if (a.isDirectory === b.isDirectory) {
|
|
396
|
+
return a.name.localeCompare(b.name);
|
|
397
|
+
}
|
|
398
|
+
return a.isDirectory ? -1 : 1;
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
return new Response(JSON.stringify({
|
|
402
|
+
path: currentPath,
|
|
403
|
+
files
|
|
404
|
+
}), {
|
|
405
|
+
headers: { "Content-Type": "application/json" },
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
} catch (error) {
|
|
409
|
+
return new Response(JSON.stringify({ error: "Failed to list files" }), {
|
|
410
|
+
status: 500,
|
|
411
|
+
headers: { "Content-Type": "application/json" },
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (url.pathname === "/api/recent-projects" && request.method === "GET") {
|
|
417
|
+
const { getRecentProjects } = await import("../utils/config");
|
|
418
|
+
const recentProjects = getRecentProjects();
|
|
419
|
+
return new Response(JSON.stringify(recentProjects), {
|
|
420
|
+
headers: { "Content-Type": "application/json" },
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
365
424
|
if (url.pathname === "/api/config" && request.method === "GET") {
|
|
366
425
|
const { readConfig } = await import("../utils/config");
|
|
367
426
|
const config = readConfig();
|
|
@@ -401,13 +460,13 @@ async function startServer(port: number, maxRetries = 10) {
|
|
|
401
460
|
headers: { "Content-Type": "application/json" },
|
|
402
461
|
});
|
|
403
462
|
}
|
|
404
|
-
|
|
405
|
-
if (url.pathname === "/api/tui-conversations" && request.method === "GET") {
|
|
406
|
-
const { loadConversations } = await import("../utils/history");
|
|
407
|
-
const historyConversations = loadConversations();
|
|
408
|
-
const mapped = historyConversations.map((conv) => {
|
|
409
|
-
const steps = Array.isArray(conv.steps) ? conv.steps : [];
|
|
410
|
-
const baseTimestamp = typeof conv.timestamp === "number" ? conv.timestamp : Date.now();
|
|
463
|
+
|
|
464
|
+
if (url.pathname === "/api/tui-conversations" && request.method === "GET") {
|
|
465
|
+
const { loadConversations } = await import("../utils/history");
|
|
466
|
+
const historyConversations = loadConversations();
|
|
467
|
+
const mapped = historyConversations.map((conv) => {
|
|
468
|
+
const steps = Array.isArray(conv.steps) ? conv.steps : [];
|
|
469
|
+
const baseTimestamp = typeof conv.timestamp === "number" ? conv.timestamp : Date.now();
|
|
411
470
|
const messages = steps.map((step, index) => ({
|
|
412
471
|
id: `${conv.id}_${index}`,
|
|
413
472
|
role: step.type === "tool" ? "tool" : step.type,
|
|
@@ -416,104 +475,104 @@ async function startServer(port: number, maxRetries = 10) {
|
|
|
416
475
|
toolName: step.toolName,
|
|
417
476
|
toolArgs: step.toolArgs,
|
|
418
477
|
toolResult: step.toolResult,
|
|
419
|
-
timestamp: step.timestamp,
|
|
420
|
-
responseDuration: step.responseDuration,
|
|
421
|
-
blendWord: step.blendWord
|
|
422
|
-
}));
|
|
423
|
-
|
|
424
|
-
return {
|
|
425
|
-
id: `tui_${conv.id}`,
|
|
426
|
-
title: conv.title ?? null,
|
|
427
|
-
messages,
|
|
428
|
-
workspace: conv.workspace ?? null,
|
|
429
|
-
createdAt: baseTimestamp,
|
|
430
|
-
updatedAt: baseTimestamp
|
|
431
|
-
};
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
return new Response(JSON.stringify(mapped), {
|
|
435
|
-
headers: { "Content-Type": "application/json" },
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
if (url.pathname === "/api/tui-conversation/rename" && request.method === "POST") {
|
|
440
|
-
const body = (await request.json()) as { id: string; title: string | null };
|
|
441
|
-
if (!body?.id || typeof body.id !== "string") {
|
|
442
|
-
return new Response(JSON.stringify({ error: "Invalid id" }), {
|
|
443
|
-
status: 400,
|
|
444
|
-
headers: { "Content-Type": "application/json" },
|
|
445
|
-
});
|
|
446
|
-
}
|
|
447
|
-
const historyId = body.id.startsWith("tui_") ? body.id.slice(4) : body.id;
|
|
448
|
-
const { updateConversationTitle } = await import("../utils/history");
|
|
449
|
-
const success = updateConversationTitle(historyId, body.title ?? null);
|
|
450
|
-
return new Response(JSON.stringify({ success }), {
|
|
451
|
-
headers: { "Content-Type": "application/json" },
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
if (url.pathname === "/api/tui-conversation/delete" && request.method === "POST") {
|
|
456
|
-
const body = (await request.json()) as { id: string };
|
|
457
|
-
if (!body?.id || typeof body.id !== "string") {
|
|
458
|
-
return new Response(JSON.stringify({ error: "Invalid id" }), {
|
|
459
|
-
status: 400,
|
|
460
|
-
headers: { "Content-Type": "application/json" },
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
const historyId = body.id.startsWith("tui_") ? body.id.slice(4) : body.id;
|
|
464
|
-
const { deleteConversation } = await import("../utils/history");
|
|
465
|
-
const success = deleteConversation(historyId);
|
|
466
|
-
return new Response(JSON.stringify({ success }), {
|
|
467
|
-
headers: { "Content-Type": "application/json" },
|
|
468
|
-
});
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
if (url.pathname === "/api/add-recent-project" && request.method === "POST") {
|
|
472
|
-
const body = (await request.json()) as { path: string };
|
|
473
|
-
if (!body.path || typeof body.path !== "string") {
|
|
474
|
-
return new Response(JSON.stringify({ error: "Invalid path" }), {
|
|
475
|
-
status: 400,
|
|
476
|
-
headers: { "Content-Type": "application/json" },
|
|
477
|
-
});
|
|
478
|
-
}
|
|
479
|
-
const { addRecentProject } = await import("../utils/config");
|
|
480
|
-
addRecentProject(body.path);
|
|
481
|
-
addLog(`Added recent project: ${body.path}`);
|
|
482
|
-
return new Response(JSON.stringify({ success: true }), {
|
|
483
|
-
headers: { "Content-Type": "application/json" },
|
|
484
|
-
});
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
if (url.pathname === "/api/question/answer" && request.method === "POST") {
|
|
488
|
-
const body = (await request.json()) as { index: number; customText?: string };
|
|
489
|
-
answerQuestion(body.index, body.customText);
|
|
490
|
-
return new Response(JSON.stringify({ success: true }), {
|
|
491
|
-
headers: { "Content-Type": "application/json" },
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
if (url.pathname === "/api/approval/respond" && request.method === "POST") {
|
|
496
|
-
const body = (await request.json()) as { approved: boolean; customResponse?: string };
|
|
497
|
-
respondApproval(body.approved, body.customResponse);
|
|
498
|
-
return new Response(JSON.stringify({ success: true }), {
|
|
499
|
-
headers: { "Content-Type": "application/json" },
|
|
500
|
-
});
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
if (url.pathname === "/api/stop" && request.method === "POST") {
|
|
504
|
-
if (currentAbortController) {
|
|
505
|
-
currentAbortController.abort();
|
|
506
|
-
currentAbortController = null;
|
|
507
|
-
addLog("Agent stopped by user");
|
|
508
|
-
return new Response(JSON.stringify({ success: true, message: "Agent stopped" }), {
|
|
509
|
-
headers: { "Content-Type": "application/json" },
|
|
510
|
-
});
|
|
511
|
-
}
|
|
512
|
-
return new Response(JSON.stringify({ success: false, message: "No agent running" }), {
|
|
513
|
-
headers: { "Content-Type": "application/json" },
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
|
|
478
|
+
timestamp: step.timestamp,
|
|
479
|
+
responseDuration: step.responseDuration,
|
|
480
|
+
blendWord: step.blendWord
|
|
481
|
+
}));
|
|
482
|
+
|
|
483
|
+
return {
|
|
484
|
+
id: `tui_${conv.id}`,
|
|
485
|
+
title: conv.title ?? null,
|
|
486
|
+
messages,
|
|
487
|
+
workspace: conv.workspace ?? null,
|
|
488
|
+
createdAt: baseTimestamp,
|
|
489
|
+
updatedAt: baseTimestamp
|
|
490
|
+
};
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
return new Response(JSON.stringify(mapped), {
|
|
494
|
+
headers: { "Content-Type": "application/json" },
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (url.pathname === "/api/tui-conversation/rename" && request.method === "POST") {
|
|
499
|
+
const body = (await request.json()) as { id: string; title: string | null };
|
|
500
|
+
if (!body?.id || typeof body.id !== "string") {
|
|
501
|
+
return new Response(JSON.stringify({ error: "Invalid id" }), {
|
|
502
|
+
status: 400,
|
|
503
|
+
headers: { "Content-Type": "application/json" },
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
const historyId = body.id.startsWith("tui_") ? body.id.slice(4) : body.id;
|
|
507
|
+
const { updateConversationTitle } = await import("../utils/history");
|
|
508
|
+
const success = updateConversationTitle(historyId, body.title ?? null);
|
|
509
|
+
return new Response(JSON.stringify({ success }), {
|
|
510
|
+
headers: { "Content-Type": "application/json" },
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (url.pathname === "/api/tui-conversation/delete" && request.method === "POST") {
|
|
515
|
+
const body = (await request.json()) as { id: string };
|
|
516
|
+
if (!body?.id || typeof body.id !== "string") {
|
|
517
|
+
return new Response(JSON.stringify({ error: "Invalid id" }), {
|
|
518
|
+
status: 400,
|
|
519
|
+
headers: { "Content-Type": "application/json" },
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
const historyId = body.id.startsWith("tui_") ? body.id.slice(4) : body.id;
|
|
523
|
+
const { deleteConversation } = await import("../utils/history");
|
|
524
|
+
const success = deleteConversation(historyId);
|
|
525
|
+
return new Response(JSON.stringify({ success }), {
|
|
526
|
+
headers: { "Content-Type": "application/json" },
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (url.pathname === "/api/add-recent-project" && request.method === "POST") {
|
|
531
|
+
const body = (await request.json()) as { path: string };
|
|
532
|
+
if (!body.path || typeof body.path !== "string") {
|
|
533
|
+
return new Response(JSON.stringify({ error: "Invalid path" }), {
|
|
534
|
+
status: 400,
|
|
535
|
+
headers: { "Content-Type": "application/json" },
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
const { addRecentProject } = await import("../utils/config");
|
|
539
|
+
addRecentProject(body.path);
|
|
540
|
+
addLog(`Added recent project: ${body.path}`);
|
|
541
|
+
return new Response(JSON.stringify({ success: true }), {
|
|
542
|
+
headers: { "Content-Type": "application/json" },
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (url.pathname === "/api/question/answer" && request.method === "POST") {
|
|
547
|
+
const body = (await request.json()) as { index: number; customText?: string };
|
|
548
|
+
answerQuestion(body.index, body.customText);
|
|
549
|
+
return new Response(JSON.stringify({ success: true }), {
|
|
550
|
+
headers: { "Content-Type": "application/json" },
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (url.pathname === "/api/approval/respond" && request.method === "POST") {
|
|
555
|
+
const body = (await request.json()) as { approved: boolean; customResponse?: string };
|
|
556
|
+
respondApproval(body.approved, body.customResponse);
|
|
557
|
+
return new Response(JSON.stringify({ success: true }), {
|
|
558
|
+
headers: { "Content-Type": "application/json" },
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (url.pathname === "/api/stop" && request.method === "POST") {
|
|
563
|
+
if (currentAbortController) {
|
|
564
|
+
currentAbortController.abort();
|
|
565
|
+
currentAbortController = null;
|
|
566
|
+
addLog("Agent stopped by user");
|
|
567
|
+
return new Response(JSON.stringify({ success: true, message: "Agent stopped" }), {
|
|
568
|
+
headers: { "Content-Type": "application/json" },
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
return new Response(JSON.stringify({ success: false, message: "No agent running" }), {
|
|
572
|
+
headers: { "Content-Type": "application/json" },
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
|
|
517
576
|
if (url.pathname === "/api/message" && request.method === "POST") {
|
|
518
577
|
const body = (await request.json()) as {
|
|
519
578
|
message?: string;
|
|
@@ -541,88 +600,113 @@ async function startServer(port: number, maxRetries = 10) {
|
|
|
541
600
|
}
|
|
542
601
|
|
|
543
602
|
addLog("Message received");
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
603
|
+
|
|
604
|
+
const { readConfig } = await import("../utils/config");
|
|
605
|
+
const config = readConfig();
|
|
606
|
+
const providerKey = `${config.provider ?? "unknown"}:${config.model ?? "unknown"}`;
|
|
607
|
+
const { promise, cancel } = acquireProviderSlot(providerKey, PROVIDER_CONCURRENCY);
|
|
608
|
+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
609
|
+
const release = await Promise.race([
|
|
610
|
+
promise,
|
|
611
|
+
new Promise<ReleaseFn | null>((resolve) => {
|
|
612
|
+
timeoutId = setTimeout(() => resolve(null), PROVIDER_QUEUE_TIMEOUT_MS);
|
|
613
|
+
})
|
|
614
|
+
]);
|
|
615
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
616
|
+
|
|
617
|
+
if (!release) {
|
|
618
|
+
cancel();
|
|
619
|
+
return new Response(JSON.stringify({ error: "Rate limit: too many concurrent requests. Try again shortly." }), {
|
|
620
|
+
status: 429,
|
|
621
|
+
headers: { "Content-Type": "application/json" },
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
const releaseSlot = release;
|
|
625
|
+
|
|
626
|
+
currentAbortController = new AbortController();
|
|
627
|
+
const abortSignal = currentAbortController.signal;
|
|
628
|
+
|
|
629
|
+
const encoder = new TextEncoder();
|
|
630
|
+
const stream = new ReadableStream({
|
|
631
|
+
async start(controller) {
|
|
632
|
+
let keepAlive: ReturnType<typeof setInterval> | null = null;
|
|
633
|
+
let aborted = false;
|
|
634
|
+
let released = false;
|
|
635
|
+
|
|
636
|
+
const cleanup = () => {
|
|
637
|
+
if (keepAlive) clearInterval(keepAlive);
|
|
638
|
+
currentAbortController = null;
|
|
639
|
+
if (!released) {
|
|
640
|
+
released = true;
|
|
641
|
+
releaseSlot();
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
const safeEnqueue = (text: string) => {
|
|
646
|
+
if (aborted) return false;
|
|
647
|
+
try {
|
|
648
|
+
controller.enqueue(encoder.encode(text));
|
|
649
|
+
return true;
|
|
650
|
+
} catch {
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
abortSignal.addEventListener('abort', () => {
|
|
656
|
+
aborted = true;
|
|
657
|
+
safeEnqueue(JSON.stringify({ type: 'stopped', message: 'Agent stopped by user' }) + "\n");
|
|
658
|
+
cleanup();
|
|
659
|
+
questionUnsub();
|
|
660
|
+
approvalUnsub();
|
|
661
|
+
exploreUnsub?.();
|
|
662
|
+
try { controller.close(); } catch { }
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
const questionUnsub = subscribeQuestion((req) => {
|
|
666
|
+
safeEnqueue(JSON.stringify({ type: 'question', request: req }) + "\n");
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
const approvalUnsub = subscribeApproval((req) => {
|
|
671
|
+
safeEnqueue(JSON.stringify({ type: 'approval', request: req }) + "\n");
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
keepAlive = setInterval(() => {
|
|
675
|
+
safeEnqueue(JSON.stringify({ type: 'ping' }) + "\n");
|
|
676
|
+
}, 5000);
|
|
677
|
+
|
|
678
|
+
let exploreUnsub: (() => void) | null = null;
|
|
679
|
+
|
|
680
|
+
try {
|
|
681
|
+
const { Agent } = await import("../agent");
|
|
682
|
+
const { subscribeExploreTool } = await import("../utils/exploreBridge");
|
|
683
|
+
|
|
684
|
+
addLog("[EXPLORE] Subscribing...");
|
|
685
|
+
exploreUnsub = subscribeExploreTool((event) => {
|
|
686
|
+
addLog(`[EXPLORE] Tool: ${event.toolName}`);
|
|
687
|
+
safeEnqueue(JSON.stringify({ type: 'explore-tool', ...event }) + "\n");
|
|
688
|
+
});
|
|
689
|
+
addLog("[EXPLORE] Subscribed");
|
|
690
|
+
const providerStatus = await Agent.ensureProviderReady();
|
|
691
|
+
|
|
692
|
+
if (!providerStatus.ready) {
|
|
693
|
+
safeEnqueue(
|
|
694
|
+
JSON.stringify({
|
|
695
|
+
type: "error",
|
|
696
|
+
error: providerStatus.error || "Provider not ready",
|
|
697
|
+
}) + "\n"
|
|
698
|
+
);
|
|
699
|
+
cleanup();
|
|
700
|
+
questionUnsub();
|
|
701
|
+
approvalUnsub();
|
|
702
|
+
exploreUnsub?.();
|
|
703
|
+
controller.close();
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
|
|
621
707
|
const agent = new Agent();
|
|
622
708
|
let allowImages = false;
|
|
623
709
|
try {
|
|
624
|
-
const { readConfig } = await import("../utils/config");
|
|
625
|
-
const config = readConfig();
|
|
626
710
|
if (config.model) {
|
|
627
711
|
const { findModelsDevModelById, modelAcceptsImages } = await import("../utils/models");
|
|
628
712
|
const result = await findModelsDevModelById(config.model);
|
|
@@ -639,172 +723,172 @@ async function startServer(port: number, maxRetries = 10) {
|
|
|
639
723
|
|
|
640
724
|
|
|
641
725
|
for await (const event of agent.streamMessages(conversationHistory as any, {})) {
|
|
642
|
-
if (aborted) break;
|
|
643
|
-
if (!safeEnqueue(JSON.stringify(event) + "\n")) break;
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
cleanup();
|
|
647
|
-
questionUnsub();
|
|
648
|
-
approvalUnsub();
|
|
649
|
-
exploreUnsub?.();
|
|
650
|
-
if (!aborted) controller.close();
|
|
651
|
-
} catch (error) {
|
|
652
|
-
if (!aborted) {
|
|
653
|
-
safeEnqueue(
|
|
654
|
-
JSON.stringify({
|
|
655
|
-
type: "error",
|
|
656
|
-
error: error instanceof Error ? error.message : "Unknown error",
|
|
657
|
-
}) + "\n"
|
|
658
|
-
);
|
|
659
|
-
}
|
|
660
|
-
cleanup();
|
|
661
|
-
questionUnsub();
|
|
662
|
-
approvalUnsub();
|
|
663
|
-
exploreUnsub?.();
|
|
664
|
-
try { controller.close(); } catch { }
|
|
665
|
-
}
|
|
666
|
-
},
|
|
667
|
-
});
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
return new Response(stream, {
|
|
671
|
-
headers: {
|
|
672
|
-
"Content-Type": "text/event-stream",
|
|
673
|
-
"Cache-Control": "no-cache",
|
|
674
|
-
Connection: "keep-alive",
|
|
675
|
-
},
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
addLog(`${request.method} ${url.pathname} (404)`);
|
|
681
|
-
return new Response("Not Found", { status: 404 });
|
|
682
|
-
|
|
683
|
-
} catch (error) {
|
|
684
|
-
console.error("Request error:", error);
|
|
685
|
-
addLog(`Server error: ${error instanceof Error ? error.message : "Unknown"}`);
|
|
686
|
-
return new Response("Internal Server Error", { status: 500 });
|
|
687
|
-
}
|
|
688
|
-
},
|
|
689
|
-
error(error) {
|
|
690
|
-
console.error("Server error:", error);
|
|
691
|
-
return new Response("Internal Server Error", { status: 500 });
|
|
692
|
-
},
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
currentPort = port;
|
|
696
|
-
const serverUrl = `http://${HOST}:${port}`;
|
|
697
|
-
const openCommand = process.platform === "win32" ? `start ${serverUrl}` :
|
|
698
|
-
process.platform === "darwin" ? `open ${serverUrl}` :
|
|
699
|
-
`xdg-open ${serverUrl}`;
|
|
700
|
-
|
|
701
|
-
exec(openCommand, (error) => {
|
|
702
|
-
if (error) {
|
|
703
|
-
console.error("Failed to open browser:", error);
|
|
704
|
-
}
|
|
705
|
-
});
|
|
706
|
-
|
|
707
|
-
return server;
|
|
708
|
-
} catch (err: any) {
|
|
709
|
-
if (err.code === "EADDRINUSE") {
|
|
710
|
-
if (maxRetries > 0) {
|
|
711
|
-
console.log(`Port ${port} is in use, trying ${port + 1}...`);
|
|
712
|
-
return startServer(port + 1, maxRetries - 1);
|
|
713
|
-
} else {
|
|
714
|
-
console.error(`Failed to find an available port after retries.`);
|
|
715
|
-
throw err;
|
|
716
|
-
}
|
|
717
|
-
} else {
|
|
718
|
-
throw err;
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
await startServer(PORT);
|
|
724
|
-
|
|
725
|
-
function ServerStatus() {
|
|
726
|
-
const [logList, setLogList] = React.useState<LogEntry[]>(logs);
|
|
727
|
-
const [scrollOffset, setScrollOffset] = React.useState(0);
|
|
728
|
-
const [terminalHeight, setTerminalHeight] = React.useState(process.stdout.rows || 24);
|
|
729
|
-
|
|
730
|
-
React.useEffect(() => {
|
|
731
|
-
const listener = () => {
|
|
732
|
-
setLogList([...logs]);
|
|
733
|
-
setScrollOffset(Math.max(0, logs.length - (terminalHeight - 6)));
|
|
734
|
-
};
|
|
735
|
-
listeners.add(listener);
|
|
736
|
-
return () => {
|
|
737
|
-
listeners.delete(listener);
|
|
738
|
-
};
|
|
739
|
-
}, [terminalHeight]);
|
|
740
|
-
|
|
741
|
-
React.useEffect(() => {
|
|
742
|
-
const handleResize = () => {
|
|
743
|
-
setTerminalHeight(process.stdout.rows || 24);
|
|
744
|
-
};
|
|
745
|
-
process.stdout.on('resize', handleResize);
|
|
746
|
-
return () => {
|
|
747
|
-
process.stdout.off('resize', handleResize);
|
|
748
|
-
};
|
|
749
|
-
}, []);
|
|
750
|
-
|
|
751
|
-
React.useEffect(() => {
|
|
752
|
-
const handleData = (data: Buffer) => {
|
|
753
|
-
const str = data.toString();
|
|
754
|
-
if (str.includes('\x03')) {
|
|
755
|
-
process.exit(0);
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
if (str.match(/\x1b\[<64;\d+;\d+M/)) {
|
|
759
|
-
setScrollOffset(prev => Math.max(0, prev - 1));
|
|
760
|
-
} else if (str.match(/\x1b\[<65;\d+;\d+M/)) {
|
|
761
|
-
setScrollOffset(prev => prev + 1);
|
|
762
|
-
}
|
|
763
|
-
};
|
|
764
|
-
|
|
765
|
-
if (process.stdin.isTTY) {
|
|
766
|
-
process.stdin.setRawMode(true);
|
|
767
|
-
process.stdout.write('\x1b[?1000h\x1b[?1006h\x1b[?1003h');
|
|
768
|
-
process.stdin.on('data', handleData);
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
return () => {
|
|
772
|
-
if (process.stdin.isTTY) {
|
|
773
|
-
process.stdin.off('data', handleData);
|
|
774
|
-
process.stdout.write('\x1b[?1000l\x1b[?1006l\x1b[?1003l');
|
|
775
|
-
process.stdin.setRawMode(false);
|
|
776
|
-
}
|
|
777
|
-
};
|
|
778
|
-
}, []);
|
|
779
|
-
|
|
780
|
-
const logsHeight = Math.max(5, terminalHeight - 6);
|
|
781
|
-
const visibleLogs = logList.slice(scrollOffset, scrollOffset + logsHeight);
|
|
782
|
-
|
|
783
|
-
return (
|
|
784
|
-
<box flexDirection="column" width="100%" height="100%" justifyContent="flex-start" alignItems="center" paddingTop={1}>
|
|
785
|
-
<box flexDirection="row" marginBottom={1}>
|
|
786
|
-
<text fg="#ffca38" attributes={TextAttributes.BOLD}>
|
|
787
|
-
Web interface:{" "}
|
|
788
|
-
</text>
|
|
789
|
-
<text fg="gray">http://{HOST}:{currentPort}</text>
|
|
790
|
-
</box>
|
|
791
|
-
|
|
792
|
-
<box flexDirection="column" width={80} height={logsHeight} borderStyle="rounded" borderColor="gray" title={`Server Logs`}>
|
|
793
|
-
{logList.length === 0 ? (
|
|
794
|
-
<text fg="gray" attributes={TextAttributes.DIM}>
|
|
795
|
-
No logs yet...
|
|
796
|
-
</text>
|
|
797
|
-
) : (
|
|
798
|
-
visibleLogs.map((log, i) => (
|
|
799
|
-
<text key={i} fg="gray">
|
|
800
|
-
[{log.timestamp}] {log.message}
|
|
801
|
-
</text>
|
|
802
|
-
))
|
|
803
|
-
)}
|
|
804
|
-
</box>
|
|
805
|
-
</box>
|
|
806
|
-
);
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
const renderer = await createCliRenderer();
|
|
726
|
+
if (aborted) break;
|
|
727
|
+
if (!safeEnqueue(JSON.stringify(event) + "\n")) break;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
cleanup();
|
|
731
|
+
questionUnsub();
|
|
732
|
+
approvalUnsub();
|
|
733
|
+
exploreUnsub?.();
|
|
734
|
+
if (!aborted) controller.close();
|
|
735
|
+
} catch (error) {
|
|
736
|
+
if (!aborted) {
|
|
737
|
+
safeEnqueue(
|
|
738
|
+
JSON.stringify({
|
|
739
|
+
type: "error",
|
|
740
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
741
|
+
}) + "\n"
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
cleanup();
|
|
745
|
+
questionUnsub();
|
|
746
|
+
approvalUnsub();
|
|
747
|
+
exploreUnsub?.();
|
|
748
|
+
try { controller.close(); } catch { }
|
|
749
|
+
}
|
|
750
|
+
},
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
return new Response(stream, {
|
|
755
|
+
headers: {
|
|
756
|
+
"Content-Type": "text/event-stream",
|
|
757
|
+
"Cache-Control": "no-cache",
|
|
758
|
+
Connection: "keep-alive",
|
|
759
|
+
},
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
addLog(`${request.method} ${url.pathname} (404)`);
|
|
765
|
+
return new Response("Not Found", { status: 404 });
|
|
766
|
+
|
|
767
|
+
} catch (error) {
|
|
768
|
+
console.error("Request error:", error);
|
|
769
|
+
addLog(`Server error: ${error instanceof Error ? error.message : "Unknown"}`);
|
|
770
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
771
|
+
}
|
|
772
|
+
},
|
|
773
|
+
error(error) {
|
|
774
|
+
console.error("Server error:", error);
|
|
775
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
776
|
+
},
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
currentPort = port;
|
|
780
|
+
const serverUrl = `http://${HOST}:${port}`;
|
|
781
|
+
const openCommand = process.platform === "win32" ? `start ${serverUrl}` :
|
|
782
|
+
process.platform === "darwin" ? `open ${serverUrl}` :
|
|
783
|
+
`xdg-open ${serverUrl}`;
|
|
784
|
+
|
|
785
|
+
exec(openCommand, (error) => {
|
|
786
|
+
if (error) {
|
|
787
|
+
console.error("Failed to open browser:", error);
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
return server;
|
|
792
|
+
} catch (err: any) {
|
|
793
|
+
if (err.code === "EADDRINUSE") {
|
|
794
|
+
if (maxRetries > 0) {
|
|
795
|
+
console.log(`Port ${port} is in use, trying ${port + 1}...`);
|
|
796
|
+
return startServer(port + 1, maxRetries - 1);
|
|
797
|
+
} else {
|
|
798
|
+
console.error(`Failed to find an available port after retries.`);
|
|
799
|
+
throw err;
|
|
800
|
+
}
|
|
801
|
+
} else {
|
|
802
|
+
throw err;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
await startServer(PORT);
|
|
808
|
+
|
|
809
|
+
function ServerStatus() {
|
|
810
|
+
const [logList, setLogList] = React.useState<LogEntry[]>(logs);
|
|
811
|
+
const [scrollOffset, setScrollOffset] = React.useState(0);
|
|
812
|
+
const [terminalHeight, setTerminalHeight] = React.useState(process.stdout.rows || 24);
|
|
813
|
+
|
|
814
|
+
React.useEffect(() => {
|
|
815
|
+
const listener = () => {
|
|
816
|
+
setLogList([...logs]);
|
|
817
|
+
setScrollOffset(Math.max(0, logs.length - (terminalHeight - 6)));
|
|
818
|
+
};
|
|
819
|
+
listeners.add(listener);
|
|
820
|
+
return () => {
|
|
821
|
+
listeners.delete(listener);
|
|
822
|
+
};
|
|
823
|
+
}, [terminalHeight]);
|
|
824
|
+
|
|
825
|
+
React.useEffect(() => {
|
|
826
|
+
const handleResize = () => {
|
|
827
|
+
setTerminalHeight(process.stdout.rows || 24);
|
|
828
|
+
};
|
|
829
|
+
process.stdout.on('resize', handleResize);
|
|
830
|
+
return () => {
|
|
831
|
+
process.stdout.off('resize', handleResize);
|
|
832
|
+
};
|
|
833
|
+
}, []);
|
|
834
|
+
|
|
835
|
+
React.useEffect(() => {
|
|
836
|
+
const handleData = (data: Buffer) => {
|
|
837
|
+
const str = data.toString();
|
|
838
|
+
if (str.includes('\x03')) {
|
|
839
|
+
process.exit(0);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
if (str.match(/\x1b\[<64;\d+;\d+M/)) {
|
|
843
|
+
setScrollOffset(prev => Math.max(0, prev - 1));
|
|
844
|
+
} else if (str.match(/\x1b\[<65;\d+;\d+M/)) {
|
|
845
|
+
setScrollOffset(prev => prev + 1);
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
if (process.stdin.isTTY) {
|
|
850
|
+
process.stdin.setRawMode(true);
|
|
851
|
+
process.stdout.write('\x1b[?1000h\x1b[?1006h\x1b[?1003h');
|
|
852
|
+
process.stdin.on('data', handleData);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
return () => {
|
|
856
|
+
if (process.stdin.isTTY) {
|
|
857
|
+
process.stdin.off('data', handleData);
|
|
858
|
+
process.stdout.write('\x1b[?1000l\x1b[?1006l\x1b[?1003l');
|
|
859
|
+
process.stdin.setRawMode(false);
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
}, []);
|
|
863
|
+
|
|
864
|
+
const logsHeight = Math.max(5, terminalHeight - 6);
|
|
865
|
+
const visibleLogs = logList.slice(scrollOffset, scrollOffset + logsHeight);
|
|
866
|
+
|
|
867
|
+
return (
|
|
868
|
+
<box flexDirection="column" width="100%" height="100%" justifyContent="flex-start" alignItems="center" paddingTop={1}>
|
|
869
|
+
<box flexDirection="row" marginBottom={1}>
|
|
870
|
+
<text fg="#ffca38" attributes={TextAttributes.BOLD}>
|
|
871
|
+
Web interface:{" "}
|
|
872
|
+
</text>
|
|
873
|
+
<text fg="gray">http://{HOST}:{currentPort}</text>
|
|
874
|
+
</box>
|
|
875
|
+
|
|
876
|
+
<box flexDirection="column" width={80} height={logsHeight} borderStyle="rounded" borderColor="gray" title={`Server Logs`}>
|
|
877
|
+
{logList.length === 0 ? (
|
|
878
|
+
<text fg="gray" attributes={TextAttributes.DIM}>
|
|
879
|
+
No logs yet...
|
|
880
|
+
</text>
|
|
881
|
+
) : (
|
|
882
|
+
visibleLogs.map((log, i) => (
|
|
883
|
+
<text key={i} fg="gray">
|
|
884
|
+
[{log.timestamp}] {log.message}
|
|
885
|
+
</text>
|
|
886
|
+
))
|
|
887
|
+
)}
|
|
888
|
+
</box>
|
|
889
|
+
</box>
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
const renderer = await createCliRenderer();
|
|
810
894
|
createRoot(renderer).render(<ServerStatus />);
|