@bitovi/vybit 0.4.4

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.
@@ -0,0 +1,130 @@
1
+ // WebSocket server setup
2
+
3
+ import { WebSocketServer, type WebSocket } from "ws";
4
+ import type { Server } from "http";
5
+
6
+ import { addPatch, commitDraft, getQueueUpdate, discardDraftPatch } from "./queue.js";
7
+ import type { Patch } from "../shared/types.js";
8
+
9
+ export interface WebSocketDeps {
10
+ broadcastPatchUpdate: () => void;
11
+ broadcastTo: (role: string, data: object, exclude?: WebSocket) => void;
12
+ }
13
+
14
+ export function setupWebSocket(httpServer: Server): WebSocketDeps {
15
+ const wss = new WebSocketServer({ server: httpServer, maxPayload: 10 * 1024 * 1024 });
16
+ const clientRoles = new Map<WebSocket, string>();
17
+
18
+ function broadcastTo(role: string, data: object, exclude?: WebSocket): void {
19
+ const payload = JSON.stringify(data);
20
+ for (const [client, clientRole] of clientRoles) {
21
+ if (clientRole === role && client !== exclude && client.readyState === 1) {
22
+ client.send(payload);
23
+ }
24
+ }
25
+ }
26
+
27
+ function broadcastPatchUpdate(): void {
28
+ broadcastTo("panel", { type: "QUEUE_UPDATE", ...getQueueUpdate() });
29
+ }
30
+
31
+ wss.on("connection", (ws: WebSocket) => {
32
+ console.error("[ws] Client connected");
33
+
34
+ ws.on("message", (raw) => {
35
+ try {
36
+ const msg = JSON.parse(String(raw));
37
+
38
+ if (msg.type === "REGISTER") {
39
+ const role = msg.role;
40
+ if (role === "overlay" || role === "panel" || role === "design") {
41
+ clientRoles.set(ws, role);
42
+ console.error(`[ws] Client registered as: ${role}`);
43
+ if (role === "panel") {
44
+ ws.send(JSON.stringify({ type: "QUEUE_UPDATE", ...getQueueUpdate() }));
45
+ }
46
+ }
47
+ return;
48
+ }
49
+
50
+ // Route messages with a "to" field to all clients of that role
51
+ if (msg.to) {
52
+ broadcastTo(msg.to, msg, ws);
53
+ return;
54
+ }
55
+
56
+ // Server-handled messages (no "to" field)
57
+ if (msg.type === "PATCH_STAGED") {
58
+ const patch = addPatch({ ...msg.patch, kind: msg.patch.kind ?? 'class-change' });
59
+ console.error(`[ws] Patch staged: #${patch.id}`);
60
+ broadcastPatchUpdate();
61
+ } else if (msg.type === "MESSAGE_STAGE") {
62
+ const patch = addPatch({
63
+ id: msg.id,
64
+ kind: 'message',
65
+ elementKey: msg.elementKey ?? '',
66
+ status: 'staged',
67
+ originalClass: '',
68
+ newClass: '',
69
+ property: '',
70
+ timestamp: new Date().toISOString(),
71
+ message: msg.message,
72
+ component: msg.component,
73
+ });
74
+ console.error(`[ws] Message patch staged: #${patch.id}`);
75
+ broadcastPatchUpdate();
76
+ } else if (msg.type === "PATCH_COMMIT") {
77
+ const commit = commitDraft(msg.ids);
78
+ console.error(`[ws] Commit created: #${commit.id} (${commit.patches.length} patches)`);
79
+ broadcastPatchUpdate();
80
+ } else if (msg.type === "DISCARD_DRAFTS") {
81
+ const ids: string[] = msg.ids ?? [];
82
+ for (const id of ids) {
83
+ discardDraftPatch(id);
84
+ }
85
+ console.error(`[ws] Discarded ${ids.length} draft patch(es)`);
86
+ broadcastPatchUpdate();
87
+ } else if (msg.type === "PING") {
88
+ ws.send(JSON.stringify({ type: "PONG" }));
89
+ } else if (msg.type === "DESIGN_SUBMIT") {
90
+ const patch: Patch = {
91
+ id: crypto.randomUUID(),
92
+ kind: 'design',
93
+ elementKey: `${msg.target?.tag ?? ''}.${(msg.target?.classes ?? '').split(' ')[0]}`,
94
+ status: 'staged',
95
+ originalClass: '',
96
+ newClass: '',
97
+ property: 'design',
98
+ timestamp: new Date().toISOString(),
99
+ component: msg.componentName ? { name: msg.componentName } : undefined,
100
+ target: msg.target,
101
+ context: msg.context,
102
+ image: msg.image,
103
+ insertMode: msg.insertMode,
104
+ canvasWidth: msg.canvasWidth,
105
+ canvasHeight: msg.canvasHeight,
106
+ };
107
+ addPatch(patch);
108
+ broadcastPatchUpdate();
109
+ // Tell the overlay to replace the canvas with a static preview
110
+ broadcastTo("overlay", {
111
+ type: "DESIGN_SUBMITTED",
112
+ image: msg.image,
113
+ }, ws);
114
+ console.error(`[ws] Design patch staged: ${patch.id}`);
115
+ } else if (msg.type === "DESIGN_CLOSE") {
116
+ broadcastTo("overlay", { type: "DESIGN_CLOSE" }, ws);
117
+ }
118
+ } catch (err) {
119
+ console.error("[ws] Bad message:", err);
120
+ }
121
+ });
122
+
123
+ ws.on("close", () => {
124
+ clientRoles.delete(ws);
125
+ console.error("[ws] Client disconnected");
126
+ });
127
+ });
128
+
129
+ return { broadcastPatchUpdate, broadcastTo };
130
+ }
@@ -0,0 +1,304 @@
1
+ // Shared types for the PATCH protocol.
2
+ // Imported by overlay (esbuild), panel (Vite), and server (tsx).
3
+
4
+ export type ContainerName = 'modal' | 'popover' | 'sidebar' | 'popup';
5
+
6
+ export type PatchKind = 'class-change' | 'message' | 'design';
7
+
8
+ export type PatchStatus = 'staged' | 'committed' | 'implementing' | 'implemented' | 'error';
9
+
10
+ export interface Patch {
11
+ id: string; // UUID
12
+ kind: PatchKind; // discriminator
13
+ elementKey: string; // stable identifier (empty string for general messages)
14
+ status: PatchStatus;
15
+ // Class-change fields (used when kind === 'class-change'):
16
+ originalClass: string; // classToken before edit ('' if adding new)
17
+ newClass: string; // classToken after edit
18
+ property: string; // prefix of the change
19
+ timestamp: string; // ISO 8601
20
+ // Populated at stage time by the overlay (has DOM access):
21
+ pageUrl?: string; // URL of the inspected page
22
+ component?: { name: string; instanceCount?: number };
23
+ target?: { tag: string; classes: string; innerText: string };
24
+ context?: string;
25
+ errorMessage?: string;
26
+ // Message field (used when kind === 'message'):
27
+ message?: string; // free-form text
28
+ // Design fields (used when kind === 'design'):
29
+ image?: string; // data URL of the canvas snapshot
30
+ insertMode?: string; // before | after | first-child | last-child
31
+ canvasWidth?: number;
32
+ canvasHeight?: number;
33
+ // Commit reference:
34
+ commitId?: string; // Set when committed into a Commit
35
+ }
36
+
37
+ export type CommitStatus = 'staged' | 'committed' | 'implementing' | 'implemented' | 'partial' | 'error';
38
+
39
+ export interface Commit {
40
+ id: string; // UUID
41
+ patches: Patch[]; // Ordered: class-changes AND messages interleaved
42
+ status: CommitStatus;
43
+ timestamp: string; // ISO 8601 — set when committed
44
+ }
45
+
46
+ /** Lightweight patch info for UI display (omits context/target for smaller WS payloads) */
47
+ export interface PatchSummary {
48
+ id: string;
49
+ kind: PatchKind;
50
+ elementKey: string;
51
+ status: PatchStatus;
52
+ originalClass: string;
53
+ newClass: string;
54
+ property: string;
55
+ timestamp: string;
56
+ component?: { name: string; instanceCount?: number };
57
+ errorMessage?: string;
58
+ message?: string;
59
+ image?: string;
60
+ }
61
+
62
+ export interface CommitSummary {
63
+ id: string;
64
+ status: CommitStatus;
65
+ timestamp: string;
66
+ patches: PatchSummary[]; // ordered — class-changes and messages interleaved
67
+ }
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // WebSocket messages
71
+ // ---------------------------------------------------------------------------
72
+
73
+ // Kept unchanged
74
+ export interface RegisterMessage {
75
+ type: 'REGISTER';
76
+ role: 'overlay' | 'panel';
77
+ }
78
+
79
+ export interface ElementSelectedMessage {
80
+ type: 'ELEMENT_SELECTED';
81
+ to: 'panel';
82
+ componentName: string;
83
+ instanceCount: number;
84
+ classes: string;
85
+ tailwindConfig: any;
86
+ }
87
+
88
+ export interface ClearHighlightsMessage {
89
+ type: 'CLEAR_HIGHLIGHTS';
90
+ to: 'overlay';
91
+ }
92
+
93
+ export interface SwitchContainerMessage {
94
+ type: 'SWITCH_CONTAINER';
95
+ to: 'overlay';
96
+ container: ContainerName;
97
+ }
98
+
99
+ export interface PingMessage {
100
+ type: 'PING';
101
+ }
102
+
103
+ export interface PongMessage {
104
+ type: 'PONG';
105
+ }
106
+
107
+ // New PATCH_* messages
108
+
109
+ /** Panel → Overlay: live-preview a class swap */
110
+ export interface PatchPreviewMessage {
111
+ type: 'PATCH_PREVIEW';
112
+ to: 'overlay';
113
+ oldClass: string;
114
+ newClass: string;
115
+ }
116
+
117
+ /** Panel → Overlay: revert any active preview */
118
+ export interface PatchRevertMessage {
119
+ type: 'PATCH_REVERT';
120
+ to: 'overlay';
121
+ }
122
+
123
+ /** Panel → Overlay: stage a change (overlay fills context, sends PATCH_STAGED to server) */
124
+ export interface PatchStageMessage {
125
+ type: 'PATCH_STAGE';
126
+ to: 'overlay';
127
+ id: string;
128
+ oldClass: string;
129
+ newClass: string;
130
+ property: string;
131
+ }
132
+
133
+ /** Overlay → Server: patch with full context, added to server queue */
134
+ export interface PatchStagedMessage {
135
+ type: 'PATCH_STAGED';
136
+ patch: Patch;
137
+ }
138
+
139
+ /** Panel → Server: move staged patches to committed status */
140
+ export interface PatchCommitMessage {
141
+ type: 'PATCH_COMMIT';
142
+ ids: string[]; // includes both class-change AND message patch IDs
143
+ }
144
+
145
+ /** Panel → Server: stage a message patch */
146
+ export interface MessageStageMessage {
147
+ type: 'MESSAGE_STAGE';
148
+ id: string; // UUID (generated by panel)
149
+ message: string; // the user's text
150
+ elementKey?: string; // optional — current element, or empty for general context
151
+ component?: { name: string; instanceCount?: number };
152
+ }
153
+
154
+ /** Server → Panel/Overlay: broadcast full queue state */
155
+ export interface QueueUpdateMessage {
156
+ type: 'QUEUE_UPDATE';
157
+ // Counts for the footer pills
158
+ draftCount: number; // patches in the draft (staged, not yet committed)
159
+ committedCount: number; // commits with status 'committed'
160
+ implementingCount: number;
161
+ implementedCount: number;
162
+ partialCount: number;
163
+ errorCount: number;
164
+ // Draft: the in-progress group (ordered by insertion)
165
+ draft: PatchSummary[]; // all staged patches in order (class-changes + messages)
166
+ // Finalized commits by status
167
+ commits: CommitSummary[];
168
+ }
169
+
170
+ /** @deprecated Use QueueUpdateMessage instead */
171
+ export interface PatchUpdateMessage {
172
+ type: 'PATCH_UPDATE';
173
+ staged: number;
174
+ committed: number;
175
+ implementing: number;
176
+ implemented: number;
177
+ patches: {
178
+ staged: PatchSummary[];
179
+ committed: PatchSummary[];
180
+ implementing: PatchSummary[];
181
+ implemented: PatchSummary[];
182
+ };
183
+ }
184
+
185
+ /** Server → Panel: agent reports work-in-progress */
186
+ export interface PatchImplementingMessage {
187
+ type: 'PATCH_IMPLEMENTING';
188
+ ids: string[];
189
+ }
190
+
191
+ /** Server → Panel: agent marks changes done */
192
+ export interface PatchImplementedMessage {
193
+ type: 'PATCH_IMPLEMENTED';
194
+ ids: string[];
195
+ }
196
+
197
+ /** Server → Panel: error on a specific patch */
198
+ export interface PatchErrorMessage {
199
+ type: 'PATCH_ERROR';
200
+ id: string;
201
+ errorMessage: string;
202
+ }
203
+
204
+ // ---------------------------------------------------------------------------
205
+ // Design canvas messages
206
+ // ---------------------------------------------------------------------------
207
+
208
+ export type InsertMode = 'before' | 'after' | 'first-child' | 'last-child';
209
+
210
+ /** Panel → Overlay: request to inject a design canvas */
211
+ export interface InsertDesignCanvasMessage {
212
+ type: 'INSERT_DESIGN_CANVAS';
213
+ to: 'overlay';
214
+ insertMode: InsertMode;
215
+ }
216
+
217
+ /** Overlay → Design iframe: element context for the canvas */
218
+ export interface ElementContextMessage {
219
+ type: 'ELEMENT_CONTEXT';
220
+ to: 'design';
221
+ componentName: string;
222
+ instanceCount: number;
223
+ target: {
224
+ tag: string;
225
+ classes: string;
226
+ innerText: string;
227
+ };
228
+ context: string;
229
+ insertMode: InsertMode;
230
+ }
231
+
232
+ /** Design iframe → Server: submit the sketch */
233
+ export interface DesignSubmitMessage {
234
+ type: 'DESIGN_SUBMIT';
235
+ image: string;
236
+ componentName: string;
237
+ target: {
238
+ tag: string;
239
+ classes: string;
240
+ innerText: string;
241
+ };
242
+ context: string;
243
+ insertMode: InsertMode;
244
+ canvasWidth: number;
245
+ canvasHeight: number;
246
+ }
247
+
248
+ /** Design iframe → Overlay: close the canvas wrapper */
249
+ export interface DesignCloseMessage {
250
+ type: 'DESIGN_CLOSE';
251
+ }
252
+
253
+ // ---------------------------------------------------------------------------
254
+ // Union types
255
+ // ---------------------------------------------------------------------------
256
+
257
+ export type OverlayToPanel = ElementSelectedMessage;
258
+ export type PanelToOverlay =
259
+ | PatchPreviewMessage
260
+ | PatchRevertMessage
261
+ | PatchStageMessage
262
+ | ClearHighlightsMessage
263
+ | SwitchContainerMessage
264
+ | InsertDesignCanvasMessage;
265
+ export type OverlayToServer = PatchStagedMessage;
266
+ export type PanelToServer = PatchCommitMessage | MessageStageMessage;
267
+ export type ClientToServer =
268
+ | RegisterMessage
269
+ | PatchStagedMessage
270
+ | PatchCommitMessage
271
+ | MessageStageMessage
272
+ | DesignSubmitMessage
273
+ | DesignCloseMessage
274
+ | PingMessage;
275
+ export type ServerToClient =
276
+ | PongMessage
277
+ | QueueUpdateMessage
278
+ | PatchUpdateMessage
279
+ | PatchImplementingMessage
280
+ | PatchImplementedMessage
281
+ | PatchErrorMessage;
282
+
283
+ export type AnyMessage =
284
+ | RegisterMessage
285
+ | ElementSelectedMessage
286
+ | PatchPreviewMessage
287
+ | PatchRevertMessage
288
+ | PatchStageMessage
289
+ | PatchStagedMessage
290
+ | PatchCommitMessage
291
+ | MessageStageMessage
292
+ | QueueUpdateMessage
293
+ | PatchUpdateMessage
294
+ | PatchImplementingMessage
295
+ | PatchImplementedMessage
296
+ | PatchErrorMessage
297
+ | ClearHighlightsMessage
298
+ | SwitchContainerMessage
299
+ | InsertDesignCanvasMessage
300
+ | ElementContextMessage
301
+ | DesignSubmitMessage
302
+ | DesignCloseMessage
303
+ | PingMessage
304
+ | PongMessage;