@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.
Files changed (108) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +114 -0
  3. package/dist/agent/agent-fallback.d.ts +50 -0
  4. package/dist/agent/agent-fallback.d.ts.map +1 -0
  5. package/dist/agent/agent-fallback.js +31 -0
  6. package/dist/agent/agent-fallback.js.map +1 -0
  7. package/dist/agent/cli-agent-fallback.d.ts +79 -0
  8. package/dist/agent/cli-agent-fallback.d.ts.map +1 -0
  9. package/dist/agent/cli-agent-fallback.js +1056 -0
  10. package/dist/agent/cli-agent-fallback.js.map +1 -0
  11. package/dist/integration/index.d.ts +28 -0
  12. package/dist/integration/index.d.ts.map +1 -0
  13. package/dist/integration/index.js +389 -0
  14. package/dist/integration/index.js.map +1 -0
  15. package/dist/resolver/astro-resolver.d.ts +28 -0
  16. package/dist/resolver/astro-resolver.d.ts.map +1 -0
  17. package/dist/resolver/astro-resolver.js +835 -0
  18. package/dist/resolver/astro-resolver.js.map +1 -0
  19. package/dist/resolver/types.d.ts +39 -0
  20. package/dist/resolver/types.d.ts.map +1 -0
  21. package/dist/resolver/types.js +2 -0
  22. package/dist/resolver/types.js.map +1 -0
  23. package/dist/shared/protocol.d.ts +150 -0
  24. package/dist/shared/protocol.d.ts.map +1 -0
  25. package/dist/shared/protocol.js +28 -0
  26. package/dist/shared/protocol.js.map +1 -0
  27. package/dist/shared/selection-context.d.ts +75 -0
  28. package/dist/shared/selection-context.d.ts.map +1 -0
  29. package/dist/shared/selection-context.js +2 -0
  30. package/dist/shared/selection-context.js.map +1 -0
  31. package/dist/shared/visual-components.d.ts +15 -0
  32. package/dist/shared/visual-components.d.ts.map +1 -0
  33. package/dist/shared/visual-components.js +2 -0
  34. package/dist/shared/visual-components.js.map +1 -0
  35. package/dist/toolbar/action-model.d.ts +20 -0
  36. package/dist/toolbar/action-model.d.ts.map +1 -0
  37. package/dist/toolbar/action-model.js +49 -0
  38. package/dist/toolbar/action-model.js.map +1 -0
  39. package/dist/toolbar/app.d.ts +3 -0
  40. package/dist/toolbar/app.d.ts.map +1 -0
  41. package/dist/toolbar/app.js +274 -0
  42. package/dist/toolbar/app.js.map +1 -0
  43. package/dist/toolbar/chat-drawer.d.ts +111 -0
  44. package/dist/toolbar/chat-drawer.d.ts.map +1 -0
  45. package/dist/toolbar/chat-drawer.js +1575 -0
  46. package/dist/toolbar/chat-drawer.js.map +1 -0
  47. package/dist/toolbar/chat-windows.d.ts +62 -0
  48. package/dist/toolbar/chat-windows.d.ts.map +1 -0
  49. package/dist/toolbar/chat-windows.js +287 -0
  50. package/dist/toolbar/chat-windows.js.map +1 -0
  51. package/dist/toolbar/contextual-actions.d.ts +24 -0
  52. package/dist/toolbar/contextual-actions.d.ts.map +1 -0
  53. package/dist/toolbar/contextual-actions.js +371 -0
  54. package/dist/toolbar/contextual-actions.js.map +1 -0
  55. package/dist/toolbar/diagnostic-actions.d.ts +22 -0
  56. package/dist/toolbar/diagnostic-actions.d.ts.map +1 -0
  57. package/dist/toolbar/diagnostic-actions.js +127 -0
  58. package/dist/toolbar/diagnostic-actions.js.map +1 -0
  59. package/dist/toolbar/layers.d.ts +26 -0
  60. package/dist/toolbar/layers.d.ts.map +1 -0
  61. package/dist/toolbar/layers.js +31 -0
  62. package/dist/toolbar/layers.js.map +1 -0
  63. package/dist/toolbar/overlay.d.ts +40 -0
  64. package/dist/toolbar/overlay.d.ts.map +1 -0
  65. package/dist/toolbar/overlay.js +619 -0
  66. package/dist/toolbar/overlay.js.map +1 -0
  67. package/dist/visual/capability-resolver.d.ts +11 -0
  68. package/dist/visual/capability-resolver.d.ts.map +1 -0
  69. package/dist/visual/capability-resolver.js +69 -0
  70. package/dist/visual/capability-resolver.js.map +1 -0
  71. package/dist/visual/command-engine.d.ts +10 -0
  72. package/dist/visual/command-engine.d.ts.map +1 -0
  73. package/dist/visual/command-engine.js +199 -0
  74. package/dist/visual/command-engine.js.map +1 -0
  75. package/dist/visual/commands.d.ts +36 -0
  76. package/dist/visual/commands.d.ts.map +1 -0
  77. package/dist/visual/commands.js +2 -0
  78. package/dist/visual/commands.js.map +1 -0
  79. package/dist/visual/patch-transactions.d.ts +57 -0
  80. package/dist/visual/patch-transactions.d.ts.map +1 -0
  81. package/dist/visual/patch-transactions.js +259 -0
  82. package/dist/visual/patch-transactions.js.map +1 -0
  83. package/dist/vite/build-ai-plugin.d.ts +11 -0
  84. package/dist/vite/build-ai-plugin.d.ts.map +1 -0
  85. package/dist/vite/build-ai-plugin.js +45 -0
  86. package/dist/vite/build-ai-plugin.js.map +1 -0
  87. package/package.json +85 -0
  88. package/src/agent/agent-fallback.ts +95 -0
  89. package/src/agent/cli-agent-fallback.ts +1382 -0
  90. package/src/integration/index.ts +475 -0
  91. package/src/resolver/astro-resolver.ts +1024 -0
  92. package/src/resolver/types.ts +47 -0
  93. package/src/shared/protocol.ts +181 -0
  94. package/src/shared/selection-context.ts +91 -0
  95. package/src/shared/visual-components.ts +16 -0
  96. package/src/toolbar/action-model.ts +67 -0
  97. package/src/toolbar/app.ts +294 -0
  98. package/src/toolbar/chat-drawer.ts +1742 -0
  99. package/src/toolbar/chat-windows.ts +331 -0
  100. package/src/toolbar/contextual-actions.ts +401 -0
  101. package/src/toolbar/diagnostic-actions.ts +151 -0
  102. package/src/toolbar/layers.ts +32 -0
  103. package/src/toolbar/overlay.ts +638 -0
  104. package/src/visual/capability-resolver.ts +83 -0
  105. package/src/visual/command-engine.ts +250 -0
  106. package/src/visual/commands.ts +37 -0
  107. package/src/visual/patch-transactions.ts +270 -0
  108. package/src/vite/build-ai-plugin.ts +46 -0
@@ -0,0 +1,47 @@
1
+ import type {
2
+ DataProvenance,
3
+ EditableProp,
4
+ RepeatContext,
5
+ SourceKind,
6
+ SourceLocation,
7
+ } from '../shared/selection-context.js';
8
+
9
+ export type SourceRange = {
10
+ start: number;
11
+ end: number;
12
+ };
13
+
14
+ export type SourceProp = EditableProp & {
15
+ range: SourceRange;
16
+ syntax: 'quoted' | 'expression' | 'shorthand';
17
+ };
18
+
19
+ export type SourceNodeRecord = {
20
+ nodeId: string;
21
+ filePath: string;
22
+ sourceLanguage: 'astro' | 'jsx' | 'tsx';
23
+ sourceHash: string;
24
+ structuralPath: string;
25
+ source: SourceLocation;
26
+ range: SourceRange;
27
+ openingRange: SourceRange;
28
+ textRange?: SourceRange;
29
+ textValue?: string;
30
+ tagName?: string;
31
+ componentName?: string;
32
+ parentNodeId?: string;
33
+ siblingGroupId?: string;
34
+ previousSiblingId?: string;
35
+ nextSiblingId?: string;
36
+ parentComponents: Array<{
37
+ name: string;
38
+ source?: SourceLocation;
39
+ }>;
40
+ literalProps: SourceProp[];
41
+ sourceKind: SourceKind;
42
+ dataProvenance: DataProvenance;
43
+ repeatContext?: RepeatContext;
44
+ hydratedIsland: boolean;
45
+ instrumentable: boolean;
46
+ selfClosing: boolean;
47
+ };
@@ -0,0 +1,181 @@
1
+ import type { SelectionContext } from './selection-context.js';
2
+ import type { SourceInsertionZone } from './selection-context.js';
3
+ import type { DeterministicVisualCommand } from '../visual/commands.js';
4
+ import type {
5
+ PatchHistoryState,
6
+ PatchTransactionSummary,
7
+ } from '../visual/patch-transactions.js';
8
+
9
+ export const PROTOCOL_VERSION = 2 as const;
10
+
11
+ export const CLIENT_EVENTS = {
12
+ ready: 'astro-ai:client-ready',
13
+ inspect: 'astro-ai:inspect',
14
+ execute: 'astro-ai:execute',
15
+ undo: 'astro-ai:undo',
16
+ redo: 'astro-ai:redo',
17
+ agentInstruction: 'astro-ai:agent-instruction',
18
+ agentCancel: 'astro-ai:agent-cancel',
19
+ sessionClose: 'astro-ai:session-close',
20
+ insertionZones: 'astro-ai:insertion-zones',
21
+ } as const;
22
+
23
+ /**
24
+ * Identifies one chat window's conversation. Every window keeps its own turn
25
+ * history and provider workspace; source history stays shared across windows
26
+ * because all windows edit the same project files.
27
+ */
28
+ export const DEFAULT_SESSION_ID = 'default';
29
+
30
+ export const SERVER_EVENTS = {
31
+ ready: 'astro-ai:server-ready',
32
+ selection: 'astro-ai:selection',
33
+ operation: 'astro-ai:operation',
34
+ history: 'astro-ai:history',
35
+ error: 'astro-ai:error',
36
+ agentEvent: 'astro-ai:agent-event',
37
+ insertionZones: 'astro-ai:insertion-zones-result',
38
+ } as const;
39
+
40
+ export type ClientReadyMessage = {
41
+ protocolVersion: typeof PROTOCOL_VERSION;
42
+ route: string;
43
+ pendingAgentRequestIds?: string[];
44
+ /** Chat windows the page still has open; idle server sessions outside this list are released. */
45
+ activeSessionIds?: string[];
46
+ };
47
+
48
+ export type ServerReadyMessage = {
49
+ protocolVersion: typeof PROTOCOL_VERSION;
50
+ history: PatchHistoryState;
51
+ agent: {
52
+ provider: string;
53
+ available: boolean;
54
+ authenticated: boolean;
55
+ message: string;
56
+ };
57
+ };
58
+
59
+ export type InspectSelectionMessage = {
60
+ requestId: string;
61
+ nodeId: string;
62
+ route: string;
63
+ };
64
+
65
+ export type ExecuteVisualCommandMessage = {
66
+ requestId: string;
67
+ command: DeterministicVisualCommand;
68
+ };
69
+
70
+ export type HistoryCommandMessage = {
71
+ requestId: string;
72
+ };
73
+
74
+ export type AgentSelectionReference = {
75
+ nodeId: string;
76
+ route: string;
77
+ };
78
+
79
+ export type AgentExternalContext = {
80
+ kind: 'error' | 'audit';
81
+ title: string;
82
+ message: string;
83
+ file?: string;
84
+ line?: number;
85
+ };
86
+
87
+ export type AgentRequestMode = 'auto' | 'answer';
88
+
89
+ export type AgentFileAttachment = {
90
+ name: string;
91
+ content: string;
92
+ size: number;
93
+ mediaType?: string;
94
+ kind?: 'text' | 'image';
95
+ encoding?: 'utf8' | 'base64';
96
+ };
97
+
98
+ export type AgentInstructionMessage = {
99
+ requestId: string;
100
+ sessionId?: string;
101
+ instruction: string;
102
+ mode?: AgentRequestMode;
103
+ attachments?: AgentSelectionReference[];
104
+ locked?: boolean;
105
+ files?: AgentFileAttachment[];
106
+ externalContext?: AgentExternalContext;
107
+ };
108
+
109
+ export type AgentCancelMessage = {
110
+ requestId: string;
111
+ };
112
+
113
+ export type AgentSessionClosedMessage = {
114
+ sessionId: string;
115
+ };
116
+
117
+ export type InsertionZonesRequestMessage = { requestId: string; route: string };
118
+ export type InsertionZonesResolvedMessage = { requestId: string; zones: SourceInsertionZone[] };
119
+
120
+ export type AgentOperationState =
121
+ | 'planning'
122
+ | 'queued'
123
+ | 'reading'
124
+ | 'editing'
125
+ | 'validation'
126
+ | 'diagnostics'
127
+ | 'tool'
128
+ | 'completion'
129
+ | 'cancellation'
130
+ | 'failure';
131
+
132
+ export type AgentOperationEvent = {
133
+ requestId: string;
134
+ sessionId?: string;
135
+ state: AgentOperationState;
136
+ message: string;
137
+ response?: string;
138
+ provider?: string;
139
+ transaction?: PatchTransactionSummary;
140
+ };
141
+
142
+ export type SelectionResolvedMessage = {
143
+ requestId: string;
144
+ context: SelectionContext;
145
+ };
146
+
147
+ export type OperationCompletedMessage = {
148
+ requestId: string;
149
+ transaction: PatchTransactionSummary;
150
+ history: PatchHistoryState;
151
+ };
152
+
153
+ export type HistoryChangedMessage = PatchHistoryState;
154
+
155
+ export type VisualEditorErrorMessage = {
156
+ requestId?: string;
157
+ message: string;
158
+ fallbackEligible: boolean;
159
+ };
160
+
161
+ export type ClientToServerMessages = {
162
+ [CLIENT_EVENTS.ready]: ClientReadyMessage;
163
+ [CLIENT_EVENTS.inspect]: InspectSelectionMessage;
164
+ [CLIENT_EVENTS.execute]: ExecuteVisualCommandMessage;
165
+ [CLIENT_EVENTS.undo]: HistoryCommandMessage;
166
+ [CLIENT_EVENTS.redo]: HistoryCommandMessage;
167
+ [CLIENT_EVENTS.agentInstruction]: AgentInstructionMessage;
168
+ [CLIENT_EVENTS.agentCancel]: AgentCancelMessage;
169
+ [CLIENT_EVENTS.sessionClose]: AgentSessionClosedMessage;
170
+ [CLIENT_EVENTS.insertionZones]: InsertionZonesRequestMessage;
171
+ };
172
+
173
+ export type ServerToClientMessages = {
174
+ [SERVER_EVENTS.ready]: ServerReadyMessage;
175
+ [SERVER_EVENTS.selection]: SelectionResolvedMessage;
176
+ [SERVER_EVENTS.operation]: OperationCompletedMessage;
177
+ [SERVER_EVENTS.history]: HistoryChangedMessage;
178
+ [SERVER_EVENTS.error]: VisualEditorErrorMessage;
179
+ [SERVER_EVENTS.agentEvent]: AgentOperationEvent;
180
+ [SERVER_EVENTS.insertionZones]: InsertionZonesResolvedMessage;
181
+ };
@@ -0,0 +1,91 @@
1
+ export type SourcePosition = {
2
+ line: number;
3
+ column: number;
4
+ offset: number;
5
+ };
6
+
7
+ export type SourceLocation = {
8
+ file: string;
9
+ start: SourcePosition;
10
+ end: SourcePosition;
11
+ };
12
+
13
+ export type SourceKind =
14
+ | 'literal-source'
15
+ | 'local-variable'
16
+ | 'prop'
17
+ | 'external'
18
+ | 'repeated-template'
19
+ | 'generated-unknown'
20
+ | 'component'
21
+ | 'hydrated-island';
22
+
23
+ export type DataProvenance = {
24
+ kind: 'literal' | 'local' | 'prop' | 'import' | 'external' | 'unknown';
25
+ description: string;
26
+ symbol?: string;
27
+ declaredAt?: SourceLocation;
28
+ readOnly: boolean;
29
+ sourceType?: 'api' | 'action' | 'content-collection' | 'graphql' | 'import';
30
+ sourceFile?: string;
31
+ };
32
+
33
+ export type RepeatContext = {
34
+ kind: 'map';
35
+ description: string;
36
+ source: SourceLocation;
37
+ affectsAllInstances: true;
38
+ };
39
+
40
+ export type EditableProp = {
41
+ name: string;
42
+ type: 'string' | 'number' | 'boolean';
43
+ value: string | number | boolean;
44
+ allowedValues?: Array<string | number | boolean>;
45
+ control?: 'text' | 'number' | 'boolean' | 'enum' | 'design-token';
46
+ };
47
+
48
+ export type VisualCapabilities = {
49
+ editableText: boolean;
50
+ movable: boolean;
51
+ reorderable: boolean;
52
+ removable: boolean;
53
+ editableProps: EditableProp[];
54
+ allowedParentSlots: string[];
55
+ sourceKind: SourceKind;
56
+ dataProvenance: DataProvenance;
57
+ repeatContext?: RepeatContext;
58
+ reorderTargets: {
59
+ previous?: string;
60
+ next?: string;
61
+ };
62
+ };
63
+
64
+ export type SelectedSourceNode = {
65
+ nodeId: string;
66
+ tagName?: string;
67
+ componentName?: string;
68
+ literalText?: string;
69
+ source: SourceLocation;
70
+ };
71
+
72
+ export type SelectionContext = {
73
+ route: string;
74
+ selectedNode: SelectedSourceNode;
75
+ parentComponents: Array<{
76
+ name: string;
77
+ source?: SourceLocation;
78
+ }>;
79
+ capabilities: VisualCapabilities;
80
+ relevantFiles: string[];
81
+ skillFiles: string[];
82
+ };
83
+
84
+ export type SourceInsertionZone = {
85
+ id: string;
86
+ file: string;
87
+ offset: number;
88
+ parentNodeId?: string;
89
+ slot?: string;
90
+ acceptedChildTypes: string[];
91
+ };
@@ -0,0 +1,16 @@
1
+ export type VisualPropDefinition = {
2
+ control: 'text' | 'number' | 'boolean' | 'enum' | 'design-token';
3
+ values?: Array<string | number | boolean>;
4
+ };
5
+
6
+ export type VisualSlotDefinition = {
7
+ name: string;
8
+ accepts?: string[];
9
+ };
10
+
11
+ export type VisualComponentDefinition = {
12
+ name: string;
13
+ props?: Record<string, VisualPropDefinition>;
14
+ slots?: VisualSlotDefinition[];
15
+ layout?: 'flow' | 'flex' | 'grid' | 'freeform';
16
+ };
@@ -0,0 +1,67 @@
1
+ import type { SelectionContext } from '../shared/selection-context.js';
2
+
3
+ export type ContextualActionId = 'click' | 'edit' | 'props' | 'move' | 'remove' | 'ask-ai' | 'source';
4
+
5
+ export type ContextualAction = {
6
+ id: ContextualActionId;
7
+ label: string;
8
+ };
9
+
10
+ export type SelectionAttachment = {
11
+ nodeId: string;
12
+ route: string;
13
+ label: string;
14
+ source: SelectionContext['selectedNode']['source'];
15
+ sourceKind: SelectionContext['capabilities']['sourceKind'];
16
+ provenance: SelectionContext['capabilities']['dataProvenance'];
17
+ repeatContext?: SelectionContext['capabilities']['repeatContext'];
18
+ parentComponents: SelectionContext['parentComponents'];
19
+ };
20
+
21
+ export function contextualActions(context: SelectionContext): ContextualAction[] {
22
+ // Selection mode swallows page clicks, so driving the app to the state you
23
+ // want to edit needs an explicit way through. It leads because it changes no
24
+ // source and is often the first thing you do on a selected element.
25
+ const actions: ContextualAction[] = [{ id: 'click', label: 'Click' }];
26
+ if (context.capabilities.editableText) actions.push({ id: 'edit', label: 'Edit' });
27
+ if (context.capabilities.editableProps.length > 0) actions.push({ id: 'props', label: 'Props' });
28
+ if (context.capabilities.movable || context.capabilities.reorderable) {
29
+ actions.push({ id: 'move', label: 'Move' });
30
+ }
31
+ if (context.capabilities.removable) actions.push({ id: 'remove', label: 'Remove' });
32
+ actions.push({ id: 'ask-ai', label: 'Ask AI' }, { id: 'source', label: 'Source' });
33
+ return actions;
34
+ }
35
+
36
+ export function createSelectionAttachment(context: SelectionContext): SelectionAttachment {
37
+ const attachment: SelectionAttachment = {
38
+ nodeId: context.selectedNode.nodeId,
39
+ route: context.route,
40
+ label: context.selectedNode.componentName ?? context.selectedNode.tagName ?? 'Astro node',
41
+ source: clone(context.selectedNode.source),
42
+ sourceKind: context.capabilities.sourceKind,
43
+ provenance: clone(context.capabilities.dataProvenance),
44
+ parentComponents: clone(context.parentComponents),
45
+ };
46
+ if (context.capabilities.repeatContext !== undefined) {
47
+ attachment.repeatContext = clone(context.capabilities.repeatContext);
48
+ }
49
+ return attachment;
50
+ }
51
+
52
+ export function middleTruncatePath(path: string, maxLength = 42): string {
53
+ if (path.length <= maxLength) return path;
54
+ const basename = path.slice(path.lastIndexOf('/') + 1);
55
+ if (basename.length < maxLength - 2) {
56
+ const prefixLength = maxLength - basename.length - 1;
57
+ return `${path.slice(0, prefixLength)}…${basename}`;
58
+ }
59
+ const available = Math.max(2, maxLength - 1);
60
+ const left = Math.ceil(available / 2);
61
+ const right = Math.floor(available / 2);
62
+ return `${path.slice(0, left)}…${path.slice(-right)}`;
63
+ }
64
+
65
+ function clone<T>(value: T): T {
66
+ return JSON.parse(JSON.stringify(value)) as T;
67
+ }
@@ -0,0 +1,294 @@
1
+ import { defineToolbarApp } from 'astro/toolbar';
2
+
3
+ import {
4
+ CLIENT_EVENTS,
5
+ PROTOCOL_VERSION,
6
+ SERVER_EVENTS,
7
+ type AgentOperationEvent,
8
+ type ClientReadyMessage,
9
+ type HistoryChangedMessage,
10
+ type InsertionZonesResolvedMessage,
11
+ type OperationCompletedMessage,
12
+ type SelectionResolvedMessage,
13
+ type ServerReadyMessage,
14
+ type VisualEditorErrorMessage,
15
+ } from '../shared/protocol.js';
16
+ import type { DeterministicVisualCommand } from '../visual/commands.js';
17
+ import { createChatDrawerStyle } from './chat-drawer.js';
18
+ import { ChatWindowManager } from './chat-windows.js';
19
+ import { DiagnosticActionBridge } from './diagnostic-actions.js';
20
+ import { SelectionOverlay } from './overlay.js';
21
+
22
+ const APP_OPEN_KEY = 'astro-ai:app-open';
23
+
24
+ export default defineToolbarApp({
25
+ init(canvas, app, server) {
26
+ let disposed = false;
27
+ let restoringOpenState = readSession(APP_OPEN_KEY) === 'true';
28
+ const pendingInspectRequests = new Map<string, string>();
29
+ let insertionZonesRequestId: string | undefined;
30
+ let overlay: SelectionOverlay;
31
+ const windows = new ChatWindowManager({
32
+ onSubmit({ requestId, sessionId, instruction, mode, attachments, locked, files, externalContext }) {
33
+ server.send(CLIENT_EVENTS.agentInstruction, {
34
+ requestId,
35
+ sessionId,
36
+ instruction,
37
+ mode,
38
+ ...(attachments === undefined
39
+ ? {}
40
+ : {
41
+ attachments: attachments.map(({ nodeId, route }) => ({ nodeId, route })),
42
+ }),
43
+ ...(locked === true ? { locked: true } : {}),
44
+ ...(files === undefined ? {} : { files }),
45
+ ...(externalContext === undefined ? {} : { externalContext }),
46
+ });
47
+ windows.noticeFor(sessionId, 'AI agent operation started…');
48
+ },
49
+ onCancel(requestId) {
50
+ server.send(CLIENT_EVENTS.agentCancel, { requestId });
51
+ },
52
+ onUndo() {
53
+ windows.broadcastNotice('Undoing the last source transaction…');
54
+ server.send(CLIENT_EVENTS.undo, { requestId: createRequestId() });
55
+ },
56
+ onRedo() {
57
+ windows.broadcastNotice('Redoing the source transaction…');
58
+ server.send(CLIENT_EVENTS.redo, { requestId: createRequestId() });
59
+ },
60
+ onSessionClose(sessionId) {
61
+ server.send(CLIENT_EVENTS.sessionClose, { sessionId });
62
+ },
63
+ onEmpty() {
64
+ app.toggleState({ state: false });
65
+ },
66
+ onSelectionPaused(paused) {
67
+ if (paused) {
68
+ overlay.disable();
69
+ windows.broadcastNotice('Selection mode paused while every chat is minimized.');
70
+ } else {
71
+ overlay.enable();
72
+ overlay.start();
73
+ windows.broadcastNotice('Click to select · Shift-click to add · drag to marquee');
74
+ requestInsertionZones();
75
+ }
76
+ },
77
+ });
78
+
79
+ overlay = new SelectionOverlay({
80
+ onInspect(nodeId) {
81
+ const requestId = createRequestId();
82
+ pendingInspectRequests.set(requestId, nodeId);
83
+ windows.broadcastNotice('Resolving source capabilities…');
84
+ server.send(CLIENT_EVENTS.inspect, {
85
+ requestId,
86
+ nodeId,
87
+ route: window.location.pathname,
88
+ });
89
+ },
90
+ onActiveChange() {},
91
+ onCommand(command) {
92
+ execute(command);
93
+ },
94
+ onAskAI(contexts, elements) {
95
+ windows.openWithSelections(contexts, elements);
96
+ },
97
+ onClear() {
98
+ windows.broadcastNotice('Click to select · Shift-click to add · drag to marquee');
99
+ },
100
+ onSelectionChange(contexts, elements) {
101
+ windows.setCurrentSelections(contexts, elements);
102
+ if (contexts.length > 1) {
103
+ windows.broadcastNotice(`${contexts.length} source-backed elements selected.`);
104
+ }
105
+ },
106
+ onSelectionAnchorChange(rect) {
107
+ windows.setSelectionAnchor(rect);
108
+ },
109
+ });
110
+ const diagnosticActions = new DiagnosticActionBridge({
111
+ onFix(context) {
112
+ overlay.clearSelection();
113
+ app.toggleState({ state: true });
114
+ window.setTimeout(() => windows.openWithExternalContext(context), 0);
115
+ },
116
+ });
117
+
118
+ const drawerStyle = createChatDrawerStyle();
119
+ canvas.replaceChildren(drawerStyle, windows.element);
120
+ windows.hideAll(false);
121
+
122
+ /**
123
+ * Astro's app canvas rewrites its own shadow root in `connectedCallback`,
124
+ * and a client-side navigation re-appends the toolbar to the swapped body,
125
+ * which reconnects the canvas and throws away everything rendered into it.
126
+ * The chat windows survive as objects, so they only need remounting.
127
+ */
128
+ function ensureMounted(): void {
129
+ if (windows.element.parentNode === canvas) return;
130
+ canvas.append(drawerStyle, windows.element);
131
+ }
132
+
133
+ app.onToggled(({ state }) => {
134
+ writeSession(APP_OPEN_KEY, String(state));
135
+ if (state) {
136
+ ensureMounted();
137
+ overlay.enable();
138
+ overlay.start();
139
+ // Astro re-applies app status when the toolbar reconnects after a
140
+ // navigation. That is a re-assert, not the user opening the app, so it
141
+ // must not steal focus or expand a window they deliberately collapsed.
142
+ const opening = !restoringOpenState && !windows.visible;
143
+ windows.openAll(opening, true, opening);
144
+ windows.broadcastNotice('Click to select · Shift-click to add · drag to marquee');
145
+ requestInsertionZones();
146
+ } else {
147
+ overlay.disable();
148
+ windows.hideAll();
149
+ }
150
+ restoringOpenState = false;
151
+ });
152
+
153
+ server.on<ServerReadyMessage>(SERVER_EVENTS.ready, ({ protocolVersion, history, agent }) => {
154
+ if (disposed || protocolVersion !== PROTOCOL_VERSION) return;
155
+ windows.setProvider(agent);
156
+ windows.setHistory(history);
157
+ requestInsertionZones();
158
+ if (agent.provider !== 'none' && !agent.authenticated) {
159
+ windows.broadcastNotice(agent.message, true);
160
+ }
161
+ });
162
+ server.on<SelectionResolvedMessage>(SERVER_EVENTS.selection, ({ requestId, context }) => {
163
+ if (disposed || !pendingInspectRequests.delete(requestId)) return;
164
+ overlay.setSelection(context);
165
+ windows.broadcastNotice('Source-backed selection resolved locally.');
166
+ });
167
+ server.on<InsertionZonesResolvedMessage>(SERVER_EVENTS.insertionZones, ({ requestId, zones }) => {
168
+ if (disposed || requestId !== insertionZonesRequestId) return;
169
+ insertionZonesRequestId = undefined;
170
+ overlay.setInsertionZones(zones);
171
+ });
172
+ server.on<OperationCompletedMessage>(SERVER_EVENTS.operation, ({ transaction, history }) => {
173
+ if (disposed) return;
174
+ windows.setHistory(history);
175
+ windows.broadcastNotice(`${humanize(transaction.kind)} applied; Vite HMR is updating the page.`);
176
+ requestInsertionZones();
177
+ if (transaction.kind === 'reorder-sibling') overlay.clearSelection();
178
+ });
179
+ server.on<HistoryChangedMessage>(SERVER_EVENTS.history, (history) => {
180
+ if (!disposed) windows.setHistory(history);
181
+ });
182
+ server.on<VisualEditorErrorMessage>(SERVER_EVENTS.error, ({ requestId, message, fallbackEligible }) => {
183
+ if (disposed) return;
184
+ if (requestId !== undefined) {
185
+ const nodeId = pendingInspectRequests.get(requestId);
186
+ pendingInspectRequests.delete(requestId);
187
+ if (nodeId !== undefined) overlay.rejectSelection(nodeId);
188
+ }
189
+ windows.broadcastNotice(
190
+ fallbackEligible ? `${message} Ask AI or open the source to continue.` : message,
191
+ true,
192
+ );
193
+ });
194
+ server.on<AgentOperationEvent>(SERVER_EVENTS.agentEvent, (event) => {
195
+ if (disposed) return;
196
+ // Progress belongs to the window that started the run; a sibling window
197
+ // must not report a run it never made.
198
+ const drawer = windows.handleAgentEvent(event);
199
+ drawer?.setNotice(
200
+ event.state === 'completion'
201
+ ? event.transaction === undefined
202
+ ? 'AI response completed without changing source.'
203
+ : 'AI changes applied. Undo is available.'
204
+ : event.state === 'failure'
205
+ ? 'The AI run failed. Review the message for details.'
206
+ : `AI agent · ${humanize(event.state)}…`,
207
+ event.state === 'failure',
208
+ );
209
+ });
210
+
211
+ server.send<ClientReadyMessage>(CLIENT_EVENTS.ready, {
212
+ protocolVersion: PROTOCOL_VERSION,
213
+ route: window.location.pathname,
214
+ pendingAgentRequestIds: windows.pendingRequestIds(),
215
+ activeSessionIds: windows.sessionIds(),
216
+ });
217
+
218
+ if (readSession(APP_OPEN_KEY) === 'true') {
219
+ window.setTimeout(() => app.toggleState({ state: true }), 0);
220
+ }
221
+
222
+ // A client-side navigation swaps the document body and moves the toolbar
223
+ // into it. Every selection and node id then refers to a page that is gone,
224
+ // and the view transition can leave the chat windows hidden behind its
225
+ // snapshot, so the editor re-establishes itself against the new page.
226
+ const onNavigation = (): void => {
227
+ if (disposed) return;
228
+ ensureMounted();
229
+ overlay.clearSelection();
230
+ windows.handleNavigation();
231
+ if (windows.visible || readSession(APP_OPEN_KEY) === 'true') {
232
+ overlay.enable();
233
+ overlay.start();
234
+ windows.openAll(false, true, false);
235
+ }
236
+ requestInsertionZones();
237
+ };
238
+ document.addEventListener('astro:after-swap', onNavigation);
239
+ document.addEventListener('astro:page-load', onNavigation);
240
+ window.addEventListener('popstate', onNavigation);
241
+
242
+ const cleanup = (): void => {
243
+ if (disposed) return;
244
+ disposed = true;
245
+ document.removeEventListener('astro:after-swap', onNavigation);
246
+ document.removeEventListener('astro:page-load', onNavigation);
247
+ window.removeEventListener('popstate', onNavigation);
248
+ overlay.destroy();
249
+ diagnosticActions.destroy();
250
+ windows.destroy();
251
+ };
252
+ import.meta.hot?.dispose(cleanup);
253
+
254
+ function execute(command: DeterministicVisualCommand): void {
255
+ windows.broadcastNotice('Applying a deterministic source transformation…');
256
+ server.send(CLIENT_EVENTS.execute, {
257
+ requestId: createRequestId(),
258
+ command,
259
+ });
260
+ }
261
+
262
+ function requestInsertionZones(): void {
263
+ insertionZonesRequestId = createRequestId();
264
+ server.send(CLIENT_EVENTS.insertionZones, {
265
+ requestId: insertionZonesRequestId,
266
+ route: window.location.pathname,
267
+ });
268
+ }
269
+ },
270
+ });
271
+
272
+ function createRequestId(): string {
273
+ return crypto.randomUUID();
274
+ }
275
+
276
+ function humanize(kind: string): string {
277
+ return kind.replaceAll('-', ' ');
278
+ }
279
+
280
+ function readSession(key: string): string | null {
281
+ try {
282
+ return sessionStorage.getItem(key);
283
+ } catch {
284
+ return null;
285
+ }
286
+ }
287
+
288
+ function writeSession(key: string, value: string): void {
289
+ try {
290
+ sessionStorage.setItem(key, value);
291
+ } catch {
292
+ // The editor remains usable if session storage is restricted.
293
+ }
294
+ }