@sudodevstudio/astro-ai 0.1.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/LICENSE +21 -0
- package/README.md +114 -0
- package/dist/agent/agent-fallback.d.ts +50 -0
- package/dist/agent/agent-fallback.d.ts.map +1 -0
- package/dist/agent/agent-fallback.js +31 -0
- package/dist/agent/agent-fallback.js.map +1 -0
- package/dist/agent/cli-agent-fallback.d.ts +79 -0
- package/dist/agent/cli-agent-fallback.d.ts.map +1 -0
- package/dist/agent/cli-agent-fallback.js +1056 -0
- package/dist/agent/cli-agent-fallback.js.map +1 -0
- package/dist/integration/index.d.ts +28 -0
- package/dist/integration/index.d.ts.map +1 -0
- package/dist/integration/index.js +389 -0
- package/dist/integration/index.js.map +1 -0
- package/dist/resolver/astro-resolver.d.ts +28 -0
- package/dist/resolver/astro-resolver.d.ts.map +1 -0
- package/dist/resolver/astro-resolver.js +835 -0
- package/dist/resolver/astro-resolver.js.map +1 -0
- package/dist/resolver/types.d.ts +39 -0
- package/dist/resolver/types.d.ts.map +1 -0
- package/dist/resolver/types.js +2 -0
- package/dist/resolver/types.js.map +1 -0
- package/dist/shared/protocol.d.ts +150 -0
- package/dist/shared/protocol.d.ts.map +1 -0
- package/dist/shared/protocol.js +28 -0
- package/dist/shared/protocol.js.map +1 -0
- package/dist/shared/selection-context.d.ts +75 -0
- package/dist/shared/selection-context.d.ts.map +1 -0
- package/dist/shared/selection-context.js +2 -0
- package/dist/shared/selection-context.js.map +1 -0
- package/dist/shared/visual-components.d.ts +15 -0
- package/dist/shared/visual-components.d.ts.map +1 -0
- package/dist/shared/visual-components.js +2 -0
- package/dist/shared/visual-components.js.map +1 -0
- package/dist/toolbar/action-model.d.ts +20 -0
- package/dist/toolbar/action-model.d.ts.map +1 -0
- package/dist/toolbar/action-model.js +49 -0
- package/dist/toolbar/action-model.js.map +1 -0
- package/dist/toolbar/app.d.ts +3 -0
- package/dist/toolbar/app.d.ts.map +1 -0
- package/dist/toolbar/app.js +274 -0
- package/dist/toolbar/app.js.map +1 -0
- package/dist/toolbar/chat-drawer.d.ts +111 -0
- package/dist/toolbar/chat-drawer.d.ts.map +1 -0
- package/dist/toolbar/chat-drawer.js +1575 -0
- package/dist/toolbar/chat-drawer.js.map +1 -0
- package/dist/toolbar/chat-windows.d.ts +62 -0
- package/dist/toolbar/chat-windows.d.ts.map +1 -0
- package/dist/toolbar/chat-windows.js +287 -0
- package/dist/toolbar/chat-windows.js.map +1 -0
- package/dist/toolbar/contextual-actions.d.ts +24 -0
- package/dist/toolbar/contextual-actions.d.ts.map +1 -0
- package/dist/toolbar/contextual-actions.js +371 -0
- package/dist/toolbar/contextual-actions.js.map +1 -0
- package/dist/toolbar/diagnostic-actions.d.ts +22 -0
- package/dist/toolbar/diagnostic-actions.d.ts.map +1 -0
- package/dist/toolbar/diagnostic-actions.js +127 -0
- package/dist/toolbar/diagnostic-actions.js.map +1 -0
- package/dist/toolbar/layers.d.ts +26 -0
- package/dist/toolbar/layers.d.ts.map +1 -0
- package/dist/toolbar/layers.js +31 -0
- package/dist/toolbar/layers.js.map +1 -0
- package/dist/toolbar/overlay.d.ts +40 -0
- package/dist/toolbar/overlay.d.ts.map +1 -0
- package/dist/toolbar/overlay.js +619 -0
- package/dist/toolbar/overlay.js.map +1 -0
- package/dist/visual/capability-resolver.d.ts +11 -0
- package/dist/visual/capability-resolver.d.ts.map +1 -0
- package/dist/visual/capability-resolver.js +69 -0
- package/dist/visual/capability-resolver.js.map +1 -0
- package/dist/visual/command-engine.d.ts +10 -0
- package/dist/visual/command-engine.d.ts.map +1 -0
- package/dist/visual/command-engine.js +199 -0
- package/dist/visual/command-engine.js.map +1 -0
- package/dist/visual/commands.d.ts +36 -0
- package/dist/visual/commands.d.ts.map +1 -0
- package/dist/visual/commands.js +2 -0
- package/dist/visual/commands.js.map +1 -0
- package/dist/visual/patch-transactions.d.ts +57 -0
- package/dist/visual/patch-transactions.d.ts.map +1 -0
- package/dist/visual/patch-transactions.js +259 -0
- package/dist/visual/patch-transactions.js.map +1 -0
- package/dist/vite/build-ai-plugin.d.ts +11 -0
- package/dist/vite/build-ai-plugin.d.ts.map +1 -0
- package/dist/vite/build-ai-plugin.js +45 -0
- package/dist/vite/build-ai-plugin.js.map +1 -0
- package/package.json +85 -0
- package/src/agent/agent-fallback.ts +95 -0
- package/src/agent/cli-agent-fallback.ts +1382 -0
- package/src/integration/index.ts +475 -0
- package/src/resolver/astro-resolver.ts +1024 -0
- package/src/resolver/types.ts +47 -0
- package/src/shared/protocol.ts +181 -0
- package/src/shared/selection-context.ts +91 -0
- package/src/shared/visual-components.ts +16 -0
- package/src/toolbar/action-model.ts +67 -0
- package/src/toolbar/app.ts +294 -0
- package/src/toolbar/chat-drawer.ts +1742 -0
- package/src/toolbar/chat-windows.ts +331 -0
- package/src/toolbar/contextual-actions.ts +401 -0
- package/src/toolbar/diagnostic-actions.ts +151 -0
- package/src/toolbar/layers.ts +32 -0
- package/src/toolbar/overlay.ts +638 -0
- package/src/visual/capability-resolver.ts +83 -0
- package/src/visual/command-engine.ts +250 -0
- package/src/visual/commands.ts +37 -0
- package/src/visual/patch-transactions.ts +270 -0
- package/src/vite/build-ai-plugin.ts +46 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
|
|
3
|
+
import type { AstroIntegration } from 'astro';
|
|
4
|
+
import type { ViteDevServer } from 'vite';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
UnavailableAgentFallback,
|
|
8
|
+
type AgentFallback,
|
|
9
|
+
} from '../agent/agent-fallback.js';
|
|
10
|
+
import {
|
|
11
|
+
CliAgentFallback,
|
|
12
|
+
type CliAgentProvider,
|
|
13
|
+
} from '../agent/cli-agent-fallback.js';
|
|
14
|
+
import { AstroResolver } from '../resolver/astro-resolver.js';
|
|
15
|
+
import type { VisualComponentDefinition } from '../shared/visual-components.js';
|
|
16
|
+
import {
|
|
17
|
+
CLIENT_EVENTS,
|
|
18
|
+
DEFAULT_SESSION_ID,
|
|
19
|
+
PROTOCOL_VERSION,
|
|
20
|
+
SERVER_EVENTS,
|
|
21
|
+
type ClientReadyMessage,
|
|
22
|
+
type AgentCancelMessage,
|
|
23
|
+
type AgentInstructionMessage,
|
|
24
|
+
type AgentFileAttachment,
|
|
25
|
+
type AgentOperationEvent,
|
|
26
|
+
type AgentSessionClosedMessage,
|
|
27
|
+
type ExecuteVisualCommandMessage,
|
|
28
|
+
type HistoryCommandMessage,
|
|
29
|
+
type InspectSelectionMessage,
|
|
30
|
+
type InsertionZonesRequestMessage,
|
|
31
|
+
type InsertionZonesResolvedMessage,
|
|
32
|
+
type ServerReadyMessage,
|
|
33
|
+
type VisualEditorErrorMessage,
|
|
34
|
+
} from '../shared/protocol.js';
|
|
35
|
+
import { VisualCommandEngine } from '../visual/command-engine.js';
|
|
36
|
+
import { PatchTransactionStore } from '../visual/patch-transactions.js';
|
|
37
|
+
import { VisualCapabilityResolver } from '../visual/capability-resolver.js';
|
|
38
|
+
import { buildAIVitePlugin } from '../vite/build-ai-plugin.js';
|
|
39
|
+
|
|
40
|
+
const TOOLBAR_APP_ID = 'astro-ai';
|
|
41
|
+
const TOOLBAR_APP_ENTRYPOINT = new URL('../toolbar/app.js', import.meta.url);
|
|
42
|
+
const MAX_AGENT_FILE_ATTACHMENTS = 5;
|
|
43
|
+
const MAX_AGENT_FILE_BYTES = 256_000;
|
|
44
|
+
const MAX_AGENT_ATTACHMENT_BYTES = 512_000;
|
|
45
|
+
const MAX_AGENT_IMAGE_BYTES = 5_000_000;
|
|
46
|
+
const MAX_AGENT_IMAGE_ATTACHMENT_BYTES = 10_000_000;
|
|
47
|
+
const SUPPORTED_IMAGE_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp', 'image/gif']);
|
|
48
|
+
|
|
49
|
+
export type BuildWithAIOptions = {
|
|
50
|
+
agent?: CliAgentProvider | {
|
|
51
|
+
provider: CliAgentProvider;
|
|
52
|
+
command?: string;
|
|
53
|
+
model?: string;
|
|
54
|
+
agentTimeoutMs?: number;
|
|
55
|
+
diagnosticsTimeoutMs?: number;
|
|
56
|
+
} | false;
|
|
57
|
+
excludeDirectories?: string[];
|
|
58
|
+
skills?: string[];
|
|
59
|
+
maxRecoveryFiles?: number;
|
|
60
|
+
/** Required to expose the credentialed agent bridge when Astro is bound beyond loopback. */
|
|
61
|
+
allowNetworkAgent?: boolean;
|
|
62
|
+
visualComponents?: VisualComponentDefinition[];
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export { BUILD_AI_VITE_PLUGIN_NAME, buildAIVitePlugin } from '../vite/build-ai-plugin.js';
|
|
66
|
+
export type {
|
|
67
|
+
ClientToServerMessages,
|
|
68
|
+
ServerToClientMessages,
|
|
69
|
+
} from '../shared/protocol.js';
|
|
70
|
+
export type {
|
|
71
|
+
SelectionContext,
|
|
72
|
+
VisualCapabilities,
|
|
73
|
+
} from '../shared/selection-context.js';
|
|
74
|
+
export type { VisualComponentDefinition } from '../shared/visual-components.js';
|
|
75
|
+
|
|
76
|
+
export function agentSelectionReferences(
|
|
77
|
+
message: Pick<AgentInstructionMessage, 'attachments'>,
|
|
78
|
+
): NonNullable<AgentInstructionMessage['attachments']> {
|
|
79
|
+
return message.attachments ?? [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function normalizeAgentFileAttachments(
|
|
83
|
+
files: AgentFileAttachment[] | undefined,
|
|
84
|
+
): AgentFileAttachment[] {
|
|
85
|
+
if (files === undefined) return [];
|
|
86
|
+
if (!Array.isArray(files) || files.length > MAX_AGENT_FILE_ATTACHMENTS) {
|
|
87
|
+
throw new Error(`Attach no more than ${MAX_AGENT_FILE_ATTACHMENTS} files.`);
|
|
88
|
+
}
|
|
89
|
+
let totalBytes = 0;
|
|
90
|
+
let totalImageBytes = 0;
|
|
91
|
+
return files.map((file) => {
|
|
92
|
+
if (typeof file !== 'object' || file === null) throw new Error('Invalid file attachment.');
|
|
93
|
+
const name = typeof file.name === 'string' ? file.name.trim() : '';
|
|
94
|
+
const content = typeof file.content === 'string' ? file.content : '';
|
|
95
|
+
if (name === '' || name.length > 200 || /[\\/]/.test(name) || name === '.' || name === '..') {
|
|
96
|
+
throw new Error('Each attachment must use a plain file name without a path.');
|
|
97
|
+
}
|
|
98
|
+
const image = file.kind === 'image' || file.encoding === 'base64';
|
|
99
|
+
if (image) {
|
|
100
|
+
const mediaType = file.mediaType;
|
|
101
|
+
if (file.encoding !== 'base64' || typeof mediaType !== 'string' || !SUPPORTED_IMAGE_TYPES.has(mediaType)) {
|
|
102
|
+
throw new Error('Screenshots must be PNG, JPEG, WebP, or GIF images.');
|
|
103
|
+
}
|
|
104
|
+
if (!isCanonicalBase64(content)) throw new Error(`${name} contains invalid image data.`);
|
|
105
|
+
const size = Buffer.from(content, 'base64').byteLength;
|
|
106
|
+
if (size > MAX_AGENT_IMAGE_BYTES) {
|
|
107
|
+
throw new Error(`${name} exceeds the ${MAX_AGENT_IMAGE_BYTES / 1_000_000} MB image limit.`);
|
|
108
|
+
}
|
|
109
|
+
totalImageBytes += size;
|
|
110
|
+
if (totalImageBytes > MAX_AGENT_IMAGE_ATTACHMENT_BYTES) {
|
|
111
|
+
throw new Error(`Images exceed the ${MAX_AGENT_IMAGE_ATTACHMENT_BYTES / 1_000_000} MB total limit.`);
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
name,
|
|
115
|
+
content,
|
|
116
|
+
size,
|
|
117
|
+
mediaType,
|
|
118
|
+
kind: 'image' as const,
|
|
119
|
+
encoding: 'base64' as const,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (content.includes('\0')) throw new Error('Only text files and supported images can be attached.');
|
|
123
|
+
const size = Buffer.byteLength(content);
|
|
124
|
+
if (size > MAX_AGENT_FILE_BYTES) {
|
|
125
|
+
throw new Error(`${name} exceeds the ${Math.round(MAX_AGENT_FILE_BYTES / 1_000)} KB attachment limit.`);
|
|
126
|
+
}
|
|
127
|
+
totalBytes += size;
|
|
128
|
+
if (totalBytes > MAX_AGENT_ATTACHMENT_BYTES) {
|
|
129
|
+
throw new Error(`Attachments exceed the ${Math.round(MAX_AGENT_ATTACHMENT_BYTES / 1_000)} KB total limit.`);
|
|
130
|
+
}
|
|
131
|
+
const mediaType = typeof file.mediaType === 'string' && file.mediaType.length <= 100
|
|
132
|
+
? file.mediaType
|
|
133
|
+
: undefined;
|
|
134
|
+
return { name, content, size, ...(mediaType === undefined ? {} : { mediaType }) };
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isCanonicalBase64(value: string): boolean {
|
|
139
|
+
if (value === '' || value.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(value)) return false;
|
|
140
|
+
return Buffer.from(value, 'base64').toString('base64') === value;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export default function buildWithAI(
|
|
144
|
+
options: BuildWithAIOptions = {},
|
|
145
|
+
): AstroIntegration {
|
|
146
|
+
let resolver: AstroResolver | undefined;
|
|
147
|
+
let engine: VisualCommandEngine | undefined;
|
|
148
|
+
let viteServer: ViteDevServer | undefined;
|
|
149
|
+
let agent: AgentFallback = new UnavailableAgentFallback();
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
name: TOOLBAR_APP_ID,
|
|
153
|
+
hooks: {
|
|
154
|
+
'astro:config:setup': ({ config, command, addDevToolbarApp, updateConfig, logger }) => {
|
|
155
|
+
if (command !== 'dev') return;
|
|
156
|
+
|
|
157
|
+
resolver = new AstroResolver(
|
|
158
|
+
fileURLToPath(config.root),
|
|
159
|
+
new VisualCapabilityResolver(options.visualComponents),
|
|
160
|
+
options.skills ?? [],
|
|
161
|
+
);
|
|
162
|
+
engine = new VisualCommandEngine(
|
|
163
|
+
resolver,
|
|
164
|
+
new PatchTransactionStore(resolver, {
|
|
165
|
+
onHistoryWarning(message) {
|
|
166
|
+
logger.warn(`[astro-ai] ${message}`);
|
|
167
|
+
},
|
|
168
|
+
...(options.maxRecoveryFiles === undefined ? {} : { maxRecoveryFiles: options.maxRecoveryFiles }),
|
|
169
|
+
async beforeApply(files) {
|
|
170
|
+
if (viteServer === undefined) return;
|
|
171
|
+
try {
|
|
172
|
+
await viteServer.watcher.unwatch(files);
|
|
173
|
+
} catch {
|
|
174
|
+
// A missed pause is safe; the post-apply event still refreshes
|
|
175
|
+
// every affected module from the completed transaction.
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
afterApply(files) {
|
|
179
|
+
if (viteServer === undefined) return;
|
|
180
|
+
viteServer.watcher.add(files);
|
|
181
|
+
for (const file of files) {
|
|
182
|
+
const modules = viteServer.moduleGraph.getModulesByFile(file);
|
|
183
|
+
if (modules !== undefined) {
|
|
184
|
+
for (const module of modules) {
|
|
185
|
+
viteServer.moduleGraph.invalidateModule(module);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
viteServer.watcher.emit('change', file);
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
}),
|
|
192
|
+
);
|
|
193
|
+
if (options.agent !== undefined && options.agent !== false) {
|
|
194
|
+
const cli = typeof options.agent === 'string'
|
|
195
|
+
? { provider: options.agent }
|
|
196
|
+
: options.agent;
|
|
197
|
+
try {
|
|
198
|
+
agent = new CliAgentFallback({
|
|
199
|
+
provider: cli.provider,
|
|
200
|
+
projectRoot: fileURLToPath(config.root),
|
|
201
|
+
...(options.excludeDirectories === undefined ? {} : { excludeDirectories: options.excludeDirectories }),
|
|
202
|
+
...(options.skills === undefined ? {} : { skills: options.skills }),
|
|
203
|
+
...('command' in cli && cli.command !== undefined ? { command: cli.command } : {}),
|
|
204
|
+
...('model' in cli && cli.model !== undefined ? { model: cli.model } : {}),
|
|
205
|
+
...('agentTimeoutMs' in cli && cli.agentTimeoutMs !== undefined ? { agentTimeoutMs: cli.agentTimeoutMs } : {}),
|
|
206
|
+
...('diagnosticsTimeoutMs' in cli && cli.diagnosticsTimeoutMs !== undefined ? { diagnosticsTimeoutMs: cli.diagnosticsTimeoutMs } : {}),
|
|
207
|
+
});
|
|
208
|
+
} catch (error) {
|
|
209
|
+
const detail = error instanceof Error ? error.message : 'Invalid agent configuration.';
|
|
210
|
+
logger.error(`[astro-ai] ${detail}`);
|
|
211
|
+
agent = new UnavailableAgentFallback(`Agent configuration error: ${detail}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
addDevToolbarApp({
|
|
216
|
+
id: TOOLBAR_APP_ID,
|
|
217
|
+
name: 'Build with AI',
|
|
218
|
+
icon: 'star',
|
|
219
|
+
entrypoint: TOOLBAR_APP_ENTRYPOINT,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
updateConfig({
|
|
223
|
+
vite: {
|
|
224
|
+
plugins: [buildAIVitePlugin(resolver)],
|
|
225
|
+
server: {
|
|
226
|
+
fs: {
|
|
227
|
+
allow: [
|
|
228
|
+
fileURLToPath(config.root),
|
|
229
|
+
fileURLToPath(new URL('../../', import.meta.url)),
|
|
230
|
+
],
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
},
|
|
236
|
+
'astro:server:setup': ({ server, toolbar, logger }) => {
|
|
237
|
+
if (resolver === undefined || engine === undefined) return;
|
|
238
|
+
viteServer = server;
|
|
239
|
+
const activeResolver = resolver;
|
|
240
|
+
const activeEngine = engine;
|
|
241
|
+
const activeRequests = new Map<string, AbortController>();
|
|
242
|
+
const recentAgentEvents = new Map<string, AgentOperationEvent>();
|
|
243
|
+
const networkExposed = isExternallyBound(server?.config?.server?.host);
|
|
244
|
+
const activeAgent = networkExposed && options.allowNetworkAgent !== true
|
|
245
|
+
? new UnavailableAgentFallback('The AI agent bridge is disabled because Astro is listening beyond loopback. Set allowNetworkAgent: true only on a trusted network.')
|
|
246
|
+
: agent;
|
|
247
|
+
|
|
248
|
+
const sendAgentEvent = (event: AgentOperationEvent): void => {
|
|
249
|
+
recentAgentEvents.delete(event.requestId);
|
|
250
|
+
recentAgentEvents.set(event.requestId, event);
|
|
251
|
+
while (recentAgentEvents.size > 20) {
|
|
252
|
+
const oldest = recentAgentEvents.keys().next().value;
|
|
253
|
+
if (oldest === undefined) break;
|
|
254
|
+
recentAgentEvents.delete(oldest);
|
|
255
|
+
}
|
|
256
|
+
toolbar.send<AgentOperationEvent>(SERVER_EVENTS.agentEvent, event);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
toolbar.on<ClientReadyMessage>(CLIENT_EVENTS.ready, async (message) => {
|
|
260
|
+
if (message.protocolVersion !== PROTOCOL_VERSION) {
|
|
261
|
+
logger.warn(
|
|
262
|
+
`Ignoring toolbar protocol version ${message.protocolVersion}; expected ${PROTOCOL_VERSION}.`,
|
|
263
|
+
);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
logger.debug(`Toolbar connected for route ${message.route}.`);
|
|
268
|
+
await activeEngine.transactions.ready();
|
|
269
|
+
toolbar.send<ServerReadyMessage>(SERVER_EVENTS.ready, {
|
|
270
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
271
|
+
history: activeEngine.transactions.state(),
|
|
272
|
+
agent: await activeAgent.status(),
|
|
273
|
+
});
|
|
274
|
+
for (const requestId of message.pendingAgentRequestIds ?? []) {
|
|
275
|
+
const event = recentAgentEvents.get(requestId);
|
|
276
|
+
if (event !== undefined) toolbar.send<AgentOperationEvent>(SERVER_EVENTS.agentEvent, event);
|
|
277
|
+
}
|
|
278
|
+
// A reload can drop chat windows; release the workspaces they held.
|
|
279
|
+
if (message.activeSessionIds !== undefined) {
|
|
280
|
+
await activeAgent.retainSessions(message.activeSessionIds);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
toolbar.on<InspectSelectionMessage>(CLIENT_EVENTS.inspect, (message) => {
|
|
285
|
+
try {
|
|
286
|
+
toolbar.send(SERVER_EVENTS.selection, {
|
|
287
|
+
requestId: message.requestId,
|
|
288
|
+
context: activeResolver.resolveSelection(message.nodeId, message.route),
|
|
289
|
+
});
|
|
290
|
+
} catch (error) {
|
|
291
|
+
sendError(toolbar, logger, error, message.requestId, false);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
toolbar.on<InsertionZonesRequestMessage>(CLIENT_EVENTS.insertionZones, (message) => {
|
|
296
|
+
toolbar.send<InsertionZonesResolvedMessage>(SERVER_EVENTS.insertionZones, {
|
|
297
|
+
requestId: message.requestId,
|
|
298
|
+
zones: activeResolver.findInsertionPointsForRoute(message.route),
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
toolbar.on<ExecuteVisualCommandMessage>(CLIENT_EVENTS.execute, async (message) => {
|
|
303
|
+
try {
|
|
304
|
+
const transaction = await activeEngine.execute(message.command);
|
|
305
|
+
toolbar.send(SERVER_EVENTS.operation, {
|
|
306
|
+
requestId: message.requestId,
|
|
307
|
+
transaction,
|
|
308
|
+
history: activeEngine.transactions.state(),
|
|
309
|
+
});
|
|
310
|
+
} catch (error) {
|
|
311
|
+
sendError(toolbar, logger, error, message.requestId, true);
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
toolbar.on<HistoryCommandMessage>(CLIENT_EVENTS.undo, async (message) => {
|
|
316
|
+
try {
|
|
317
|
+
const transaction = await activeEngine.transactions.undo();
|
|
318
|
+
toolbar.send(SERVER_EVENTS.operation, {
|
|
319
|
+
requestId: message.requestId,
|
|
320
|
+
transaction,
|
|
321
|
+
history: activeEngine.transactions.state(),
|
|
322
|
+
});
|
|
323
|
+
} catch (error) {
|
|
324
|
+
sendError(toolbar, logger, error, message.requestId, false);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
toolbar.on<HistoryCommandMessage>(CLIENT_EVENTS.redo, async (message) => {
|
|
329
|
+
try {
|
|
330
|
+
const transaction = await activeEngine.transactions.redo();
|
|
331
|
+
toolbar.send(SERVER_EVENTS.operation, {
|
|
332
|
+
requestId: message.requestId,
|
|
333
|
+
transaction,
|
|
334
|
+
history: activeEngine.transactions.state(),
|
|
335
|
+
});
|
|
336
|
+
} catch (error) {
|
|
337
|
+
sendError(toolbar, logger, error, message.requestId, false);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
toolbar.on<AgentInstructionMessage>(CLIENT_EVENTS.agentInstruction, async (message) => {
|
|
342
|
+
const controller = new AbortController();
|
|
343
|
+
const sessionId = message.sessionId ?? DEFAULT_SESSION_ID;
|
|
344
|
+
activeRequests.set(message.requestId, controller);
|
|
345
|
+
try {
|
|
346
|
+
const references = agentSelectionReferences(message);
|
|
347
|
+
const files = normalizeAgentFileAttachments(message.files);
|
|
348
|
+
const selections = references.map(({ nodeId, route }) => (
|
|
349
|
+
activeResolver.resolveSelection(nodeId, route)
|
|
350
|
+
));
|
|
351
|
+
if (message.locked === true && selections.length === 0) {
|
|
352
|
+
throw new Error('A locked AI edit scope requires at least one resolved source selection.');
|
|
353
|
+
}
|
|
354
|
+
const editableFiles = message.locked === true
|
|
355
|
+
? [...new Set(selections.map(({ selectedNode }) => selectedNode.source.file))]
|
|
356
|
+
: undefined;
|
|
357
|
+
sendAgentEvent({
|
|
358
|
+
requestId: message.requestId,
|
|
359
|
+
sessionId,
|
|
360
|
+
state: 'planning',
|
|
361
|
+
message: message.externalContext !== undefined
|
|
362
|
+
? `Planning a fix for the attached ${message.externalContext.kind}…`
|
|
363
|
+
: selections.length === 0 && files.length === 0
|
|
364
|
+
? 'Planning a page-level change…'
|
|
365
|
+
: selections.length === 0
|
|
366
|
+
? `Planning with ${files.length} attached file${files.length === 1 ? '' : 's'}…`
|
|
367
|
+
: selections.length === 1
|
|
368
|
+
? 'Planning with the attached source selection…'
|
|
369
|
+
: `Planning with ${selections.length} attached source selections…`,
|
|
370
|
+
});
|
|
371
|
+
const result = await activeAgent.execute(
|
|
372
|
+
{
|
|
373
|
+
instruction: message.instruction,
|
|
374
|
+
reason: 'The user explicitly chose Ask AI.',
|
|
375
|
+
sessionId,
|
|
376
|
+
mode: message.mode ?? 'auto',
|
|
377
|
+
...(selections.length === 0 ? {} : { selections }),
|
|
378
|
+
...(editableFiles === undefined ? {} : { editableFiles }),
|
|
379
|
+
...(message.externalContext === undefined
|
|
380
|
+
? {}
|
|
381
|
+
: { externalContext: message.externalContext }),
|
|
382
|
+
...(files.length === 0 ? {} : { files }),
|
|
383
|
+
signal: controller.signal,
|
|
384
|
+
onProgress(state, progressMessage) {
|
|
385
|
+
sendAgentEvent({
|
|
386
|
+
requestId: message.requestId,
|
|
387
|
+
sessionId,
|
|
388
|
+
state,
|
|
389
|
+
message: progressMessage,
|
|
390
|
+
});
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
activeEngine.transactions,
|
|
394
|
+
);
|
|
395
|
+
sendAgentEvent({
|
|
396
|
+
requestId: message.requestId,
|
|
397
|
+
sessionId,
|
|
398
|
+
state: 'completion',
|
|
399
|
+
message: result.transaction === undefined
|
|
400
|
+
? 'Agent response completed without changing source files.'
|
|
401
|
+
: 'Agent operation completed; Vite HMR is updating the page.',
|
|
402
|
+
response: result.response,
|
|
403
|
+
...(result.provider === undefined ? {} : { provider: result.provider }),
|
|
404
|
+
...(result.transaction === undefined ? {} : { transaction: result.transaction }),
|
|
405
|
+
});
|
|
406
|
+
if (result.transaction !== undefined) {
|
|
407
|
+
toolbar.send(SERVER_EVENTS.history, activeEngine.transactions.state());
|
|
408
|
+
}
|
|
409
|
+
} catch (error) {
|
|
410
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
411
|
+
sendAgentEvent({
|
|
412
|
+
requestId: message.requestId,
|
|
413
|
+
sessionId,
|
|
414
|
+
state: 'cancellation',
|
|
415
|
+
message: 'Agent operation cancelled. No source transaction was created.',
|
|
416
|
+
});
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const detail = error instanceof Error ? error.message : 'Unknown agent error.';
|
|
420
|
+
logger.warn(detail);
|
|
421
|
+
sendAgentEvent({
|
|
422
|
+
requestId: message.requestId,
|
|
423
|
+
sessionId,
|
|
424
|
+
state: 'failure',
|
|
425
|
+
message: detail,
|
|
426
|
+
});
|
|
427
|
+
} finally {
|
|
428
|
+
activeRequests.delete(message.requestId);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
toolbar.on<AgentCancelMessage>(CLIENT_EVENTS.agentCancel, (message) => {
|
|
433
|
+
const request = activeRequests.get(message.requestId);
|
|
434
|
+
if (request !== undefined) request.abort();
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
toolbar.on<AgentSessionClosedMessage>(CLIENT_EVENTS.sessionClose, async (message) => {
|
|
438
|
+
if (typeof message?.sessionId !== 'string') return;
|
|
439
|
+
try {
|
|
440
|
+
await activeAgent.closeSession(message.sessionId);
|
|
441
|
+
} catch (error) {
|
|
442
|
+
logger.warn(
|
|
443
|
+
`Failed to release chat session ${message.sessionId}: ${error instanceof Error ? error.message : 'unknown error'}`,
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
server?.httpServer?.once('close', () => { void activeAgent.dispose(); });
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export function isExternallyBound(host: string | boolean | undefined): boolean {
|
|
454
|
+
return host === true || (typeof host === 'string' && !['localhost', '127.0.0.1', '::1'].includes(host));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function sendError(
|
|
458
|
+
toolbar: {
|
|
459
|
+
send<T>(event: string, payload: T): void;
|
|
460
|
+
},
|
|
461
|
+
logger: {
|
|
462
|
+
warn(message: string): void;
|
|
463
|
+
},
|
|
464
|
+
error: unknown,
|
|
465
|
+
requestId: string,
|
|
466
|
+
fallbackEligible: boolean,
|
|
467
|
+
): void {
|
|
468
|
+
const message = error instanceof Error ? error.message : 'Unknown visual editor error.';
|
|
469
|
+
logger.warn(message);
|
|
470
|
+
toolbar.send<VisualEditorErrorMessage>(SERVER_EVENTS.error, {
|
|
471
|
+
requestId,
|
|
472
|
+
message,
|
|
473
|
+
fallbackEligible,
|
|
474
|
+
});
|
|
475
|
+
}
|