agent-vision-mcp 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 +117 -0
- package/dist/browser/cdp/browser-cdp-discovery-service.d.ts +10 -0
- package/dist/browser/cdp/browser-cdp-discovery-service.js +28 -0
- package/dist/browser/cdp/browser-live-tab-service.d.ts +16 -0
- package/dist/browser/cdp/browser-live-tab-service.js +42 -0
- package/dist/browser/cdp/browser-see-service.d.ts +33 -0
- package/dist/browser/cdp/browser-see-service.js +76 -0
- package/dist/browser/cdp/browser-tab-context-service.d.ts +23 -0
- package/dist/browser/cdp/browser-tab-context-service.js +90 -0
- package/dist/browser/cdp/browser-tab-resolution-service.d.ts +9 -0
- package/dist/browser/cdp/browser-tab-resolution-service.js +65 -0
- package/dist/browser/cdp/browser-tab-screenshot-service.d.ts +20 -0
- package/dist/browser/cdp/browser-tab-screenshot-service.js +59 -0
- package/dist/browser/cdp/cdp-websocket-session.d.ts +9 -0
- package/dist/browser/cdp/cdp-websocket-session.js +99 -0
- package/dist/browser/cdp/chrome-cdp-client.d.ts +12 -0
- package/dist/browser/cdp/chrome-cdp-client.js +141 -0
- package/dist/browser/cdp/live-browser-tab-registry.d.ts +12 -0
- package/dist/browser/cdp/live-browser-tab-registry.js +96 -0
- package/dist/browser/cdp/png-metadata.d.ts +5 -0
- package/dist/browser/cdp/png-metadata.js +16 -0
- package/dist/browser/cdp/tab-model.d.ts +33 -0
- package/dist/browser/cdp/tab-model.js +15 -0
- package/dist/browser/cdp/tab-resolution.d.ts +27 -0
- package/dist/browser/cdp/tab-resolution.js +48 -0
- package/dist/browser/cdp/types.d.ts +71 -0
- package/dist/browser/cdp/types.js +1 -0
- package/dist/capture/capture-pipeline.d.ts +5 -0
- package/dist/capture/capture-pipeline.js +1 -0
- package/dist/capture/create-screen-capture-provider.d.ts +3 -0
- package/dist/capture/create-screen-capture-provider.js +8 -0
- package/dist/capture/in-memory-capture-pipeline.d.ts +13 -0
- package/dist/capture/in-memory-capture-pipeline.js +52 -0
- package/dist/capture/in-memory-image-compositor.d.ts +5 -0
- package/dist/capture/in-memory-image-compositor.js +34 -0
- package/dist/capture/linux-portal-screenshot-provider.d.ts +8 -0
- package/dist/capture/linux-portal-screenshot-provider.js +181 -0
- package/dist/capture/mock-screen-capture-provider.d.ts +5 -0
- package/dist/capture/mock-screen-capture-provider.js +22 -0
- package/dist/capture/png-metadata.d.ts +5 -0
- package/dist/capture/png-metadata.js +18 -0
- package/dist/capture/screen-capture-provider.d.ts +4 -0
- package/dist/capture/screen-capture-provider.js +1 -0
- package/dist/capture/types.d.ts +38 -0
- package/dist/capture/types.js +1 -0
- package/dist/cdp-demo.d.ts +1 -0
- package/dist/cdp-demo.js +41 -0
- package/dist/demo.d.ts +1 -0
- package/dist/demo.js +54 -0
- package/dist/desktop/capture-now.d.ts +1 -0
- package/dist/desktop/capture-now.js +48 -0
- package/dist/desktop/controller.d.ts +25 -0
- package/dist/desktop/controller.js +77 -0
- package/dist/desktop/main.d.ts +1 -0
- package/dist/desktop/main.js +80 -0
- package/dist/desktop/preload.d.ts +1 -0
- package/dist/desktop/preload.js +26 -0
- package/dist/desktop/types.d.ts +31 -0
- package/dist/desktop/types.js +1 -0
- package/dist/errors/app-error.d.ts +7 -0
- package/dist/errors/app-error.js +11 -0
- package/dist/flow/types.d.ts +48 -0
- package/dist/flow/types.js +1 -0
- package/dist/flow/visual-capture-flow.d.ts +13 -0
- package/dist/flow/visual-capture-flow.js +196 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/logging/logger.d.ts +15 -0
- package/dist/logging/logger.js +28 -0
- package/dist/mcp/stdio-server.d.ts +19 -0
- package/dist/mcp/stdio-server.js +272 -0
- package/dist/mcp/tool-registry.d.ts +21 -0
- package/dist/mcp/tool-registry.js +33 -0
- package/dist/mcp-stdio.d.ts +2 -0
- package/dist/mcp-stdio.js +8 -0
- package/dist/overlay/local-overlay-agent.d.ts +46 -0
- package/dist/overlay/local-overlay-agent.js +551 -0
- package/dist/overlay/overlay-bundle-factory.d.ts +4 -0
- package/dist/overlay/overlay-bundle-factory.js +24 -0
- package/dist/overlay/types.d.ts +83 -0
- package/dist/overlay/types.js +1 -0
- package/dist/server.d.ts +19 -0
- package/dist/server.js +158 -0
- package/dist/session/capture-session-service.d.ts +21 -0
- package/dist/session/capture-session-service.js +50 -0
- package/dist/session/session-manager.d.ts +29 -0
- package/dist/session/session-manager.js +217 -0
- package/dist/session/session-store.d.ts +8 -0
- package/dist/session/session-store.js +15 -0
- package/dist/session/session-waiter.d.ts +14 -0
- package/dist/session/session-waiter.js +102 -0
- package/dist/types/annotation.d.ts +32 -0
- package/dist/types/annotation.js +1 -0
- package/dist/types/capture.d.ts +33 -0
- package/dist/types/capture.js +1 -0
- package/dist/types/session.d.ts +36 -0
- package/dist/types/session.js +1 -0
- package/package.json +38 -0
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { AppError } from "../errors/app-error.js";
|
|
3
|
+
const TERMINAL_STATUSES = new Set([
|
|
4
|
+
"sent",
|
|
5
|
+
"cancelled",
|
|
6
|
+
"failed",
|
|
7
|
+
"expired"
|
|
8
|
+
]);
|
|
9
|
+
const OVERLAY_SHORTCUTS = {
|
|
10
|
+
select: "V",
|
|
11
|
+
rect: "B",
|
|
12
|
+
arrow: "A",
|
|
13
|
+
text: "T",
|
|
14
|
+
redact: "R"
|
|
15
|
+
};
|
|
16
|
+
const TOOL_LABELS = {
|
|
17
|
+
select: "Select",
|
|
18
|
+
rect: "Box",
|
|
19
|
+
arrow: "Arrow",
|
|
20
|
+
text: "Text",
|
|
21
|
+
redact: "Redact"
|
|
22
|
+
};
|
|
23
|
+
const TOOL_DESCRIPTIONS = {
|
|
24
|
+
select: "Create or adjust the capture region.",
|
|
25
|
+
rect: "Highlight an area with a rectangle.",
|
|
26
|
+
arrow: "Point at a detail that needs attention.",
|
|
27
|
+
text: "Add a short note for the model.",
|
|
28
|
+
redact: "Hide sensitive content before sending."
|
|
29
|
+
};
|
|
30
|
+
const OVERLAY_TERMINAL_BY_CAPTURE_STATUS = {
|
|
31
|
+
completed: "sent",
|
|
32
|
+
cancelled: "cancelled",
|
|
33
|
+
failed: "failed",
|
|
34
|
+
expired: "expired"
|
|
35
|
+
};
|
|
36
|
+
const TOOL_ORDER = ["select", "rect", "arrow", "text", "redact"];
|
|
37
|
+
const buildToolDescriptors = () => TOOL_ORDER.map((tool) => ({
|
|
38
|
+
tool,
|
|
39
|
+
label: TOOL_LABELS[tool],
|
|
40
|
+
shortcut: OVERLAY_SHORTCUTS[tool],
|
|
41
|
+
description: TOOL_DESCRIPTIONS[tool]
|
|
42
|
+
}));
|
|
43
|
+
export class LocalOverlayAgent {
|
|
44
|
+
captureSessions;
|
|
45
|
+
capturePipeline;
|
|
46
|
+
logger;
|
|
47
|
+
overlaySessions = new Map();
|
|
48
|
+
constructor(captureSessions, capturePipeline, logger) {
|
|
49
|
+
this.captureSessions = captureSessions;
|
|
50
|
+
this.capturePipeline = capturePipeline;
|
|
51
|
+
this.logger = logger;
|
|
52
|
+
}
|
|
53
|
+
launch(sessionId) {
|
|
54
|
+
const captureSession = this.captureSessions.getSession(sessionId);
|
|
55
|
+
const existing = this.overlaySessions.get(sessionId);
|
|
56
|
+
if (existing && !this.isTerminal(existing.status)) {
|
|
57
|
+
throw new AppError("SESSION_CONFLICT", "Overlay session is already active", { sessionId });
|
|
58
|
+
}
|
|
59
|
+
const now = new Date().toISOString();
|
|
60
|
+
const overlaySession = this.present({
|
|
61
|
+
sessionId: captureSession.id,
|
|
62
|
+
command: captureSession.command,
|
|
63
|
+
status: "armed",
|
|
64
|
+
createdAt: now,
|
|
65
|
+
updatedAt: now,
|
|
66
|
+
activeTool: "select",
|
|
67
|
+
annotations: [],
|
|
68
|
+
shortcuts: OVERLAY_SHORTCUTS,
|
|
69
|
+
toolDescriptors: buildToolDescriptors(),
|
|
70
|
+
selectionSummary: { label: "No region selected yet", areaPx: 0 },
|
|
71
|
+
annotationSummary: { total: 0, byType: {} },
|
|
72
|
+
preview: {
|
|
73
|
+
canSend: false,
|
|
74
|
+
showSelectionOutline: false,
|
|
75
|
+
showAnnotationLayer: false,
|
|
76
|
+
emptyStateMessage: "Select a region to start the preview."
|
|
77
|
+
},
|
|
78
|
+
hud: {
|
|
79
|
+
title: "Capture Armed",
|
|
80
|
+
subtitle: "Switch to the target screen and drag a region to begin.",
|
|
81
|
+
statusBadge: "Waiting for selection",
|
|
82
|
+
primaryActionLabel: "Select Region",
|
|
83
|
+
secondaryActionLabel: "Cancel",
|
|
84
|
+
activeToolLabel: "Select (V)",
|
|
85
|
+
onboardingHint: "Tip: start with a tight crop, then annotate only what matters."
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
this.overlaySessions.set(sessionId, overlaySession);
|
|
89
|
+
this.logger.info("Launched overlay capture session", { sessionId });
|
|
90
|
+
return overlaySession;
|
|
91
|
+
}
|
|
92
|
+
get(sessionId) {
|
|
93
|
+
return this.syncWithCaptureSession(this.requireOverlaySession(sessionId));
|
|
94
|
+
}
|
|
95
|
+
list() {
|
|
96
|
+
return Array.from(this.overlaySessions.values())
|
|
97
|
+
.map((session) => this.syncWithCaptureSession(session))
|
|
98
|
+
.sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
99
|
+
}
|
|
100
|
+
setActiveTool(sessionId, tool) {
|
|
101
|
+
const session = this.requireMutableOverlaySession(sessionId);
|
|
102
|
+
const updated = this.persist({
|
|
103
|
+
...session,
|
|
104
|
+
activeTool: tool
|
|
105
|
+
});
|
|
106
|
+
this.logger.debug("Set overlay active tool", { sessionId, tool });
|
|
107
|
+
return updated;
|
|
108
|
+
}
|
|
109
|
+
selectRegion(sessionId, input) {
|
|
110
|
+
this.assertValidBounds(input);
|
|
111
|
+
const session = this.requireMutableOverlaySession(sessionId);
|
|
112
|
+
const updated = this.persist({
|
|
113
|
+
...session,
|
|
114
|
+
status: "selected",
|
|
115
|
+
selection: {
|
|
116
|
+
x: input.x,
|
|
117
|
+
y: input.y,
|
|
118
|
+
width: input.width,
|
|
119
|
+
height: input.height
|
|
120
|
+
},
|
|
121
|
+
context: {
|
|
122
|
+
displayId: input.displayId,
|
|
123
|
+
activeAppName: input.activeAppName,
|
|
124
|
+
activeWindowTitle: input.activeWindowTitle
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
this.logger.debug("Selected overlay region", { sessionId, selection: updated.selection });
|
|
128
|
+
return updated;
|
|
129
|
+
}
|
|
130
|
+
moveSelection(sessionId, input) {
|
|
131
|
+
const session = this.requireSelectedOverlaySession(sessionId);
|
|
132
|
+
const selection = session.selection;
|
|
133
|
+
const updated = this.persist({
|
|
134
|
+
...session,
|
|
135
|
+
selection: {
|
|
136
|
+
...selection,
|
|
137
|
+
x: selection.x + input.dx,
|
|
138
|
+
y: selection.y + input.dy
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
this.logger.debug("Moved overlay selection", { sessionId, selection: updated.selection });
|
|
142
|
+
return updated;
|
|
143
|
+
}
|
|
144
|
+
resizeSelection(sessionId, input) {
|
|
145
|
+
const session = this.requireSelectedOverlaySession(sessionId);
|
|
146
|
+
const selection = session.selection;
|
|
147
|
+
const nextSelection = {
|
|
148
|
+
x: input.x ?? selection.x,
|
|
149
|
+
y: input.y ?? selection.y,
|
|
150
|
+
width: input.width ?? selection.width,
|
|
151
|
+
height: input.height ?? selection.height
|
|
152
|
+
};
|
|
153
|
+
this.assertValidBounds(nextSelection);
|
|
154
|
+
const updated = this.persist({
|
|
155
|
+
...session,
|
|
156
|
+
selection: nextSelection
|
|
157
|
+
});
|
|
158
|
+
this.logger.debug("Resized overlay selection", { sessionId, selection: updated.selection });
|
|
159
|
+
return updated;
|
|
160
|
+
}
|
|
161
|
+
addAnnotation(sessionId, input) {
|
|
162
|
+
const session = this.requireSelectedOverlaySession(sessionId);
|
|
163
|
+
this.assertAnnotation(input.annotation);
|
|
164
|
+
const now = new Date().toISOString();
|
|
165
|
+
const record = {
|
|
166
|
+
id: input.id ?? randomUUID(),
|
|
167
|
+
annotation: input.annotation,
|
|
168
|
+
createdAt: now,
|
|
169
|
+
updatedAt: now
|
|
170
|
+
};
|
|
171
|
+
const updated = this.persist({
|
|
172
|
+
...session,
|
|
173
|
+
annotations: [...session.annotations, record]
|
|
174
|
+
});
|
|
175
|
+
this.logger.debug("Added overlay annotation", {
|
|
176
|
+
sessionId,
|
|
177
|
+
annotationId: record.id,
|
|
178
|
+
annotationType: record.annotation.type
|
|
179
|
+
});
|
|
180
|
+
return updated;
|
|
181
|
+
}
|
|
182
|
+
updateAnnotation(sessionId, input) {
|
|
183
|
+
const session = this.requireSelectedOverlaySession(sessionId);
|
|
184
|
+
this.assertAnnotation(input.annotation);
|
|
185
|
+
const existing = session.annotations.find((annotation) => annotation.id === input.annotationId);
|
|
186
|
+
if (!existing) {
|
|
187
|
+
throw new AppError("NOT_FOUND", "Overlay annotation not found", {
|
|
188
|
+
sessionId,
|
|
189
|
+
annotationId: input.annotationId
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
const updated = this.persist({
|
|
193
|
+
...session,
|
|
194
|
+
annotations: session.annotations.map((record) => record.id === input.annotationId
|
|
195
|
+
? {
|
|
196
|
+
...record,
|
|
197
|
+
annotation: input.annotation,
|
|
198
|
+
updatedAt: new Date().toISOString()
|
|
199
|
+
}
|
|
200
|
+
: record)
|
|
201
|
+
});
|
|
202
|
+
this.logger.debug("Updated overlay annotation", {
|
|
203
|
+
sessionId,
|
|
204
|
+
annotationId: input.annotationId,
|
|
205
|
+
annotationType: input.annotation.type
|
|
206
|
+
});
|
|
207
|
+
return updated;
|
|
208
|
+
}
|
|
209
|
+
removeAnnotation(sessionId, annotationId) {
|
|
210
|
+
const session = this.requireSelectedOverlaySession(sessionId);
|
|
211
|
+
const annotationExists = session.annotations.some((record) => record.id === annotationId);
|
|
212
|
+
if (!annotationExists) {
|
|
213
|
+
throw new AppError("NOT_FOUND", "Overlay annotation not found", {
|
|
214
|
+
sessionId,
|
|
215
|
+
annotationId
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
const updated = this.persist({
|
|
219
|
+
...session,
|
|
220
|
+
annotations: session.annotations.filter((record) => record.id !== annotationId)
|
|
221
|
+
});
|
|
222
|
+
this.logger.debug("Removed overlay annotation", { sessionId, annotationId });
|
|
223
|
+
return updated;
|
|
224
|
+
}
|
|
225
|
+
clearAnnotations(sessionId) {
|
|
226
|
+
const session = this.requireSelectedOverlaySession(sessionId);
|
|
227
|
+
const updated = this.persist({
|
|
228
|
+
...session,
|
|
229
|
+
annotations: []
|
|
230
|
+
});
|
|
231
|
+
this.logger.debug("Cleared overlay annotations", { sessionId });
|
|
232
|
+
return updated;
|
|
233
|
+
}
|
|
234
|
+
async send(sessionId) {
|
|
235
|
+
const overlaySession = this.requireSelectedOverlaySession(sessionId);
|
|
236
|
+
try {
|
|
237
|
+
const bundle = await this.capturePipeline.createBundle({
|
|
238
|
+
sessionId,
|
|
239
|
+
command: overlaySession.command,
|
|
240
|
+
selection: overlaySession.selection,
|
|
241
|
+
annotations: overlaySession.annotations.map((entry) => entry.annotation),
|
|
242
|
+
context: overlaySession.context
|
|
243
|
+
});
|
|
244
|
+
const completed = this.captureSessions.completeSession({
|
|
245
|
+
sessionId,
|
|
246
|
+
bundle
|
|
247
|
+
});
|
|
248
|
+
this.persist({
|
|
249
|
+
...overlaySession,
|
|
250
|
+
status: "sent",
|
|
251
|
+
errorMessage: undefined
|
|
252
|
+
});
|
|
253
|
+
this.logger.info("Sent overlay capture session", {
|
|
254
|
+
sessionId,
|
|
255
|
+
annotationCount: overlaySession.annotations.length,
|
|
256
|
+
backend: bundle.image.backend,
|
|
257
|
+
byteLength: bundle.image.byteLength
|
|
258
|
+
});
|
|
259
|
+
return completed;
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
263
|
+
const failed = this.fail(sessionId, `Overlay send failed: ${message}`);
|
|
264
|
+
throw new AppError("INTERNAL_ERROR", "Overlay capture send failed", {
|
|
265
|
+
sessionId,
|
|
266
|
+
cause: message,
|
|
267
|
+
resultingStatus: failed.status
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
cancel(sessionId) {
|
|
272
|
+
const overlaySession = this.syncWithCaptureSession(this.requireOverlaySession(sessionId));
|
|
273
|
+
if (!this.isTerminal(overlaySession.status)) {
|
|
274
|
+
this.persist({
|
|
275
|
+
...overlaySession,
|
|
276
|
+
status: "cancelled"
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
const cancelled = this.captureSessions.cancelSession(sessionId);
|
|
280
|
+
this.logger.info("Cancelled overlay capture session", { sessionId });
|
|
281
|
+
return cancelled;
|
|
282
|
+
}
|
|
283
|
+
fail(sessionId, errorMessage) {
|
|
284
|
+
const overlaySession = this.syncWithCaptureSession(this.requireOverlaySession(sessionId));
|
|
285
|
+
if (!this.isTerminal(overlaySession.status)) {
|
|
286
|
+
this.persist({
|
|
287
|
+
...overlaySession,
|
|
288
|
+
status: "failed",
|
|
289
|
+
errorMessage
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
const failed = this.captureSessions.failSession(sessionId, errorMessage);
|
|
293
|
+
this.logger.error("Failed overlay capture session", { sessionId, errorMessage });
|
|
294
|
+
return failed;
|
|
295
|
+
}
|
|
296
|
+
reapTerminalSessions(maxAgeMs) {
|
|
297
|
+
const now = Date.now();
|
|
298
|
+
const removedSessionIds = [];
|
|
299
|
+
for (const session of this.list()) {
|
|
300
|
+
if (!this.isTerminal(session.status)) {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
const updatedAtMs = Date.parse(session.updatedAt);
|
|
304
|
+
if (now - updatedAtMs < maxAgeMs) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
this.overlaySessions.delete(session.sessionId);
|
|
308
|
+
removedSessionIds.push(session.sessionId);
|
|
309
|
+
}
|
|
310
|
+
if (removedSessionIds.length > 0) {
|
|
311
|
+
this.logger.info("Reaped terminal overlay sessions", {
|
|
312
|
+
removedSessionIds,
|
|
313
|
+
maxAgeMs
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
return { removedSessionIds };
|
|
317
|
+
}
|
|
318
|
+
requireSelectedOverlaySession(sessionId) {
|
|
319
|
+
const session = this.requireMutableOverlaySession(sessionId);
|
|
320
|
+
if (!session.selection) {
|
|
321
|
+
throw new AppError("SESSION_CONFLICT", "Overlay session does not have a selected region", {
|
|
322
|
+
sessionId
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
return session;
|
|
326
|
+
}
|
|
327
|
+
requireMutableOverlaySession(sessionId) {
|
|
328
|
+
const session = this.syncWithCaptureSession(this.requireOverlaySession(sessionId));
|
|
329
|
+
if (this.isTerminal(session.status)) {
|
|
330
|
+
throw new AppError("SESSION_CONFLICT", "Overlay session is no longer mutable", {
|
|
331
|
+
sessionId,
|
|
332
|
+
status: session.status
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
return session;
|
|
336
|
+
}
|
|
337
|
+
requireOverlaySession(sessionId) {
|
|
338
|
+
const session = this.overlaySessions.get(sessionId);
|
|
339
|
+
if (!session) {
|
|
340
|
+
throw new AppError("NOT_FOUND", "Overlay session not found", { sessionId });
|
|
341
|
+
}
|
|
342
|
+
return session;
|
|
343
|
+
}
|
|
344
|
+
syncWithCaptureSession(session) {
|
|
345
|
+
let captureSession;
|
|
346
|
+
try {
|
|
347
|
+
captureSession = this.captureSessions.getSession(session.sessionId);
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
return session;
|
|
351
|
+
}
|
|
352
|
+
const mappedStatus = OVERLAY_TERMINAL_BY_CAPTURE_STATUS[captureSession.status];
|
|
353
|
+
if (!mappedStatus || session.status === mappedStatus) {
|
|
354
|
+
return session;
|
|
355
|
+
}
|
|
356
|
+
const updated = this.persist({
|
|
357
|
+
...session,
|
|
358
|
+
status: mappedStatus,
|
|
359
|
+
errorMessage: captureSession.errorMessage ?? session.errorMessage
|
|
360
|
+
});
|
|
361
|
+
this.logger.debug("Synchronized overlay session with capture session", {
|
|
362
|
+
sessionId: session.sessionId,
|
|
363
|
+
captureStatus: captureSession.status,
|
|
364
|
+
overlayStatus: updated.status
|
|
365
|
+
});
|
|
366
|
+
return updated;
|
|
367
|
+
}
|
|
368
|
+
persist(session) {
|
|
369
|
+
const updated = this.present({
|
|
370
|
+
...session,
|
|
371
|
+
updatedAt: new Date().toISOString()
|
|
372
|
+
});
|
|
373
|
+
this.overlaySessions.set(updated.sessionId, updated);
|
|
374
|
+
return updated;
|
|
375
|
+
}
|
|
376
|
+
present(session) {
|
|
377
|
+
const selectionSummary = this.buildSelectionSummary(session.selection);
|
|
378
|
+
const annotationSummary = this.buildAnnotationSummary(session.annotations);
|
|
379
|
+
const preview = this.buildPreviewState(session, selectionSummary, annotationSummary);
|
|
380
|
+
const hud = this.buildHudState(session, selectionSummary, annotationSummary, preview);
|
|
381
|
+
return {
|
|
382
|
+
...session,
|
|
383
|
+
toolDescriptors: buildToolDescriptors(),
|
|
384
|
+
selectionSummary,
|
|
385
|
+
annotationSummary,
|
|
386
|
+
preview,
|
|
387
|
+
hud
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
buildSelectionSummary(selection) {
|
|
391
|
+
if (!selection) {
|
|
392
|
+
return {
|
|
393
|
+
label: "No region selected yet",
|
|
394
|
+
areaPx: 0
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
const areaPx = selection.width * selection.height;
|
|
398
|
+
return {
|
|
399
|
+
label: `${selection.width} x ${selection.height} at (${selection.x}, ${selection.y})`,
|
|
400
|
+
areaPx,
|
|
401
|
+
bounds: selection
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
buildAnnotationSummary(annotations) {
|
|
405
|
+
const byType = {};
|
|
406
|
+
for (const entry of annotations) {
|
|
407
|
+
byType[entry.annotation.type] = (byType[entry.annotation.type] ?? 0) + 1;
|
|
408
|
+
}
|
|
409
|
+
return {
|
|
410
|
+
total: annotations.length,
|
|
411
|
+
byType,
|
|
412
|
+
latestAnnotationId: annotations.at(-1)?.id
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
buildPreviewState(session, selectionSummary, annotationSummary) {
|
|
416
|
+
const hasSelection = Boolean(session.selection);
|
|
417
|
+
const canSend = hasSelection && !this.isTerminal(session.status);
|
|
418
|
+
return {
|
|
419
|
+
canSend,
|
|
420
|
+
showSelectionOutline: hasSelection,
|
|
421
|
+
showAnnotationLayer: annotationSummary.total > 0,
|
|
422
|
+
emptyStateMessage: hasSelection
|
|
423
|
+
? undefined
|
|
424
|
+
: "Select a region to activate the preview and annotation layer."
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
buildHudState(session, selectionSummary, annotationSummary, preview) {
|
|
428
|
+
const activeToolLabel = `${TOOL_LABELS[session.activeTool]} (${session.shortcuts[session.activeTool]})`;
|
|
429
|
+
if (session.status === "armed") {
|
|
430
|
+
return {
|
|
431
|
+
title: "Capture Armed",
|
|
432
|
+
subtitle: "Switch to the target screen and drag a region to begin.",
|
|
433
|
+
statusBadge: "Waiting for selection",
|
|
434
|
+
primaryActionLabel: "Select Region",
|
|
435
|
+
secondaryActionLabel: "Cancel",
|
|
436
|
+
activeToolLabel,
|
|
437
|
+
onboardingHint: "Start with a focused crop. You can annotate after selecting."
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
if (session.status === "selected") {
|
|
441
|
+
return {
|
|
442
|
+
title: annotationSummary.total > 0 ? "Preview Ready" : "Selection Ready",
|
|
443
|
+
subtitle: annotationSummary.total > 0
|
|
444
|
+
? `${annotationSummary.total} annotation${annotationSummary.total === 1 ? "" : "s"} added.`
|
|
445
|
+
: "Add markup or send the current selection as-is.",
|
|
446
|
+
statusBadge: preview.canSend ? "Ready to send" : "Preview unavailable",
|
|
447
|
+
primaryActionLabel: preview.canSend ? "Send Capture" : "Select Region",
|
|
448
|
+
secondaryActionLabel: annotationSummary.total > 0 ? "Clear Annotations" : "Cancel",
|
|
449
|
+
activeToolLabel,
|
|
450
|
+
onboardingHint: `Selection: ${selectionSummary.label}`
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
if (session.status === "sent") {
|
|
454
|
+
return {
|
|
455
|
+
title: "Capture Sent",
|
|
456
|
+
subtitle: "The capture bundle has been returned to the LLM workflow.",
|
|
457
|
+
statusBadge: "Delivered",
|
|
458
|
+
primaryActionLabel: "Done",
|
|
459
|
+
secondaryActionLabel: "Close",
|
|
460
|
+
activeToolLabel,
|
|
461
|
+
onboardingHint: "Nothing was persisted by default."
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
if (session.status === "failed") {
|
|
465
|
+
return {
|
|
466
|
+
title: "Capture Failed",
|
|
467
|
+
subtitle: session.errorMessage ?? "The overlay session failed before completion.",
|
|
468
|
+
statusBadge: "Error",
|
|
469
|
+
primaryActionLabel: "Retry",
|
|
470
|
+
secondaryActionLabel: "Close",
|
|
471
|
+
activeToolLabel,
|
|
472
|
+
onboardingHint: "Inspect the error and start a new capture session if needed."
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
if (session.status === "expired") {
|
|
476
|
+
return {
|
|
477
|
+
title: "Capture Expired",
|
|
478
|
+
subtitle: "The session timed out before send.",
|
|
479
|
+
statusBadge: "Expired",
|
|
480
|
+
primaryActionLabel: "Retry",
|
|
481
|
+
secondaryActionLabel: "Close",
|
|
482
|
+
activeToolLabel,
|
|
483
|
+
onboardingHint: "Start a new session to continue."
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
title: "Capture Cancelled",
|
|
488
|
+
subtitle: "The overlay session was cancelled.",
|
|
489
|
+
statusBadge: "Cancelled",
|
|
490
|
+
primaryActionLabel: "Retry",
|
|
491
|
+
secondaryActionLabel: "Close",
|
|
492
|
+
activeToolLabel,
|
|
493
|
+
onboardingHint: "You can start a new session whenever you are ready."
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
assertValidBounds(bounds) {
|
|
497
|
+
if (bounds.width <= 0 || bounds.height <= 0) {
|
|
498
|
+
throw new AppError("INVALID_ARGUMENT", "Selection width and height must be greater than zero", {
|
|
499
|
+
bounds
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
assertAnnotation(annotation) {
|
|
504
|
+
switch (annotation.type) {
|
|
505
|
+
case "rect":
|
|
506
|
+
this.assertBoxAnnotation(annotation);
|
|
507
|
+
return;
|
|
508
|
+
case "redact":
|
|
509
|
+
this.assertRedactAnnotation(annotation);
|
|
510
|
+
return;
|
|
511
|
+
case "arrow":
|
|
512
|
+
this.assertArrowAnnotation(annotation);
|
|
513
|
+
return;
|
|
514
|
+
case "text":
|
|
515
|
+
this.assertTextAnnotation(annotation);
|
|
516
|
+
return;
|
|
517
|
+
default:
|
|
518
|
+
throw new AppError("INVALID_ARGUMENT", "Unsupported annotation type", {
|
|
519
|
+
annotation
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
assertBoxAnnotation(annotation) {
|
|
524
|
+
this.assertValidBounds(annotation);
|
|
525
|
+
}
|
|
526
|
+
assertRedactAnnotation(annotation) {
|
|
527
|
+
this.assertValidBounds(annotation);
|
|
528
|
+
}
|
|
529
|
+
assertArrowAnnotation(annotation) {
|
|
530
|
+
if (Number.isNaN(annotation.from.x) || Number.isNaN(annotation.from.y)) {
|
|
531
|
+
throw new AppError("INVALID_ARGUMENT", "Arrow annotation start point must be numeric", {
|
|
532
|
+
annotation
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
if (Number.isNaN(annotation.to.x) || Number.isNaN(annotation.to.y)) {
|
|
536
|
+
throw new AppError("INVALID_ARGUMENT", "Arrow annotation end point must be numeric", {
|
|
537
|
+
annotation
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
assertTextAnnotation(annotation) {
|
|
542
|
+
if (annotation.text.trim() === "") {
|
|
543
|
+
throw new AppError("INVALID_ARGUMENT", "Text annotation must include non-empty text", {
|
|
544
|
+
annotation
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
isTerminal(status) {
|
|
549
|
+
return TERMINAL_STATUSES.has(status);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CaptureBundle } from "../types/capture.js";
|
|
2
|
+
import type { CaptureSession } from "../types/session.js";
|
|
3
|
+
import type { OverlaySession } from "./types.js";
|
|
4
|
+
export declare const createOverlayCaptureBundle: (session: CaptureSession, overlaySession: OverlaySession) => CaptureBundle;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const createOverlayCaptureBundle = (session, overlaySession) => {
|
|
2
|
+
if (!overlaySession.selection) {
|
|
3
|
+
throw new Error("Cannot build capture bundle without a selection");
|
|
4
|
+
}
|
|
5
|
+
const selection = overlaySession.selection;
|
|
6
|
+
return {
|
|
7
|
+
sessionId: session.id,
|
|
8
|
+
command: session.command,
|
|
9
|
+
image: {
|
|
10
|
+
mimeType: "image/png",
|
|
11
|
+
bytesBase64: "cGg0LW92ZXJsYXktYW5ub3RhdGVkLWltYWdl",
|
|
12
|
+
width: Math.max(selection.width, 1),
|
|
13
|
+
height: Math.max(selection.height, 1)
|
|
14
|
+
},
|
|
15
|
+
selection,
|
|
16
|
+
annotations: overlaySession.annotations.map((entry) => entry.annotation),
|
|
17
|
+
context: {
|
|
18
|
+
activeAppName: overlaySession.context?.activeAppName ?? "Prototype App",
|
|
19
|
+
activeWindowTitle: overlaySession.context?.activeWindowTitle ?? "Overlay Prototype Window",
|
|
20
|
+
capturedAt: new Date().toISOString(),
|
|
21
|
+
displayId: overlaySession.context?.displayId ?? "display-1"
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { Annotation } from "../types/annotation.js";
|
|
2
|
+
import type { CaptureCommand, SelectionBounds } from "../types/capture.js";
|
|
3
|
+
export type OverlaySessionStatus = "armed" | "selected" | "sent" | "cancelled" | "failed" | "expired";
|
|
4
|
+
export type OverlayTool = "select" | "rect" | "arrow" | "text" | "redact";
|
|
5
|
+
export type OverlaySelectionContext = {
|
|
6
|
+
displayId?: string;
|
|
7
|
+
activeAppName?: string;
|
|
8
|
+
activeWindowTitle?: string;
|
|
9
|
+
};
|
|
10
|
+
export type OverlayAnnotationRecord = {
|
|
11
|
+
id: string;
|
|
12
|
+
annotation: Annotation;
|
|
13
|
+
createdAt: string;
|
|
14
|
+
updatedAt: string;
|
|
15
|
+
};
|
|
16
|
+
export type OverlayToolDescriptor = {
|
|
17
|
+
tool: OverlayTool;
|
|
18
|
+
label: string;
|
|
19
|
+
shortcut: string;
|
|
20
|
+
description: string;
|
|
21
|
+
};
|
|
22
|
+
export type OverlaySelectionSummary = {
|
|
23
|
+
label: string;
|
|
24
|
+
areaPx: number;
|
|
25
|
+
bounds?: SelectionBounds;
|
|
26
|
+
};
|
|
27
|
+
export type OverlayAnnotationSummary = {
|
|
28
|
+
total: number;
|
|
29
|
+
byType: Partial<Record<Annotation["type"], number>>;
|
|
30
|
+
latestAnnotationId?: string;
|
|
31
|
+
};
|
|
32
|
+
export type OverlayPreviewState = {
|
|
33
|
+
canSend: boolean;
|
|
34
|
+
showSelectionOutline: boolean;
|
|
35
|
+
showAnnotationLayer: boolean;
|
|
36
|
+
emptyStateMessage?: string;
|
|
37
|
+
};
|
|
38
|
+
export type OverlayHudState = {
|
|
39
|
+
title: string;
|
|
40
|
+
subtitle: string;
|
|
41
|
+
statusBadge: string;
|
|
42
|
+
primaryActionLabel: string;
|
|
43
|
+
secondaryActionLabel: string;
|
|
44
|
+
activeToolLabel: string;
|
|
45
|
+
onboardingHint: string;
|
|
46
|
+
};
|
|
47
|
+
export type OverlaySession = {
|
|
48
|
+
sessionId: string;
|
|
49
|
+
command: CaptureCommand;
|
|
50
|
+
status: OverlaySessionStatus;
|
|
51
|
+
createdAt: string;
|
|
52
|
+
updatedAt: string;
|
|
53
|
+
selection?: SelectionBounds;
|
|
54
|
+
context?: OverlaySelectionContext;
|
|
55
|
+
activeTool: OverlayTool;
|
|
56
|
+
annotations: OverlayAnnotationRecord[];
|
|
57
|
+
shortcuts: Record<OverlayTool, string>;
|
|
58
|
+
toolDescriptors: OverlayToolDescriptor[];
|
|
59
|
+
selectionSummary: OverlaySelectionSummary;
|
|
60
|
+
annotationSummary: OverlayAnnotationSummary;
|
|
61
|
+
preview: OverlayPreviewState;
|
|
62
|
+
hud: OverlayHudState;
|
|
63
|
+
errorMessage?: string;
|
|
64
|
+
};
|
|
65
|
+
export type OverlayRegionInput = SelectionBounds & OverlaySelectionContext;
|
|
66
|
+
export type OverlayMoveInput = {
|
|
67
|
+
dx: number;
|
|
68
|
+
dy: number;
|
|
69
|
+
};
|
|
70
|
+
export type OverlayResizeInput = {
|
|
71
|
+
x?: number;
|
|
72
|
+
y?: number;
|
|
73
|
+
width?: number;
|
|
74
|
+
height?: number;
|
|
75
|
+
};
|
|
76
|
+
export type CreateAnnotationInput = {
|
|
77
|
+
id?: string;
|
|
78
|
+
annotation: Annotation;
|
|
79
|
+
};
|
|
80
|
+
export type UpdateAnnotationInput = {
|
|
81
|
+
annotationId: string;
|
|
82
|
+
annotation: Annotation;
|
|
83
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ConsoleLogger } from "./logging/logger.js";
|
|
2
|
+
import { ToolRegistry } from "./mcp/tool-registry.js";
|
|
3
|
+
export declare class VisualContextServer {
|
|
4
|
+
readonly logger: ConsoleLogger;
|
|
5
|
+
private readonly cdpClient;
|
|
6
|
+
private readonly cdpDiscovery;
|
|
7
|
+
private readonly liveTabs;
|
|
8
|
+
private readonly browserLiveTabs;
|
|
9
|
+
private readonly tabResolution;
|
|
10
|
+
private readonly tabScreenshots;
|
|
11
|
+
private readonly tabContext;
|
|
12
|
+
private readonly browserSee;
|
|
13
|
+
private readonly tools;
|
|
14
|
+
constructor();
|
|
15
|
+
listTools(): ReturnType<ToolRegistry["list"]>;
|
|
16
|
+
callTool(name: string, args?: Record<string, unknown>): Promise<unknown>;
|
|
17
|
+
start(): void;
|
|
18
|
+
private registerTools;
|
|
19
|
+
}
|