@superxd-studio/adptr 0.5.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 (221) hide show
  1. package/GENERATIVE.md +101 -0
  2. package/INTERACTIONS.md +51 -0
  3. package/LICENSE +21 -0
  4. package/README.md +57 -0
  5. package/REUSE.md +170 -0
  6. package/REVIEW.md +68 -0
  7. package/THIRD-PARTY-NOTICES.md +28 -0
  8. package/ai-sdk/index.js +96 -0
  9. package/assets/ibm-plex-mono-cyrillic-400-normal.woff2 +0 -0
  10. package/assets/ibm-plex-mono-cyrillic-500-normal.woff2 +0 -0
  11. package/assets/ibm-plex-mono-cyrillic-ext-400-normal.woff2 +0 -0
  12. package/assets/ibm-plex-mono-cyrillic-ext-500-normal.woff2 +0 -0
  13. package/assets/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  14. package/assets/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  15. package/assets/ibm-plex-mono-latin-ext-400-normal.woff2 +0 -0
  16. package/assets/ibm-plex-mono-latin-ext-500-normal.woff2 +0 -0
  17. package/assets/ibm-plex-mono-vietnamese-400-normal.woff2 +0 -0
  18. package/assets/ibm-plex-mono-vietnamese-500-normal.woff2 +0 -0
  19. package/assets/instrument-sans-latin-ext-wght-normal.woff2 +0 -0
  20. package/assets/instrument-sans-latin-wght-normal.woff2 +0 -0
  21. package/assistant-ui/index.js +139 -0
  22. package/chunks/callout-DKMHSnjc.js +342 -0
  23. package/chunks/canvas-BJ15-XxE.js +598 -0
  24. package/chunks/controller-Cfpg0C7v.js +180 -0
  25. package/chunks/core-BCqVRLUy.js +312 -0
  26. package/chunks/text-field-BLtoaZFn.js +41 -0
  27. package/chunks/types-BGF7r1my.js +115 -0
  28. package/chunks/ui-9DyNsYXY.js +967 -0
  29. package/core/index.js +4 -0
  30. package/custom/index.js +64 -0
  31. package/examples/ai-sdk-backend/package.json +11 -0
  32. package/examples/ai-sdk-backend/server.mjs +316 -0
  33. package/examples/custom-backend/copy.mjs +7 -0
  34. package/examples/custom-backend/server.mjs +118 -0
  35. package/examples/generative/specs.json +2094 -0
  36. package/examples/review-backend/copy.mjs +27 -0
  37. package/examples/review-backend/package.json +11 -0
  38. package/examples/review-backend/server.mjs +276 -0
  39. package/examples/web-ai-sdk/index.html +12 -0
  40. package/examples/web-ai-sdk/package.json +21 -0
  41. package/examples/web-ai-sdk/src/copy.ts +1 -0
  42. package/examples/web-ai-sdk/src/main.tsx +64 -0
  43. package/examples/web-ai-sdk/tsconfig.json +13 -0
  44. package/examples/web-generative/index.html +12 -0
  45. package/examples/web-generative/package.json +23 -0
  46. package/examples/web-generative/src/copy.ts +42 -0
  47. package/examples/web-generative/src/main.tsx +74 -0
  48. package/examples/web-generative/tsconfig.json +14 -0
  49. package/examples/web-independent/index.html +12 -0
  50. package/examples/web-independent/package.json +20 -0
  51. package/examples/web-independent/src/main.tsx +32 -0
  52. package/examples/web-independent/tsconfig.json +14 -0
  53. package/examples/web-interactions/index.html +12 -0
  54. package/examples/web-interactions/package.json +21 -0
  55. package/examples/web-interactions/src/copy.ts +215 -0
  56. package/examples/web-interactions/src/examples.tsx +628 -0
  57. package/examples/web-interactions/src/main.tsx +116 -0
  58. package/examples/web-interactions/tsconfig.json +14 -0
  59. package/examples/web-review/index.html +12 -0
  60. package/examples/web-review/package.json +20 -0
  61. package/examples/web-review/src/main.tsx +59 -0
  62. package/examples/web-review/tsconfig.json +14 -0
  63. package/fonts.css +117 -0
  64. package/generative/index.js +1560 -0
  65. package/interactions/index.js +1591 -0
  66. package/langgraph-review/index.js +36 -0
  67. package/licenses/ai-sdk.txt +13 -0
  68. package/licenses/assistant-ui-generative.txt +21 -0
  69. package/licenses/assistant-ui.txt +23 -0
  70. package/licenses/ibm-plex-mono.txt +93 -0
  71. package/licenses/instrument-sans.txt +93 -0
  72. package/licenses/internationalized-date.txt +201 -0
  73. package/licenses/langgraph.txt +21 -0
  74. package/licenses/lucide.txt +43 -0
  75. package/licenses/react-markdown.txt +21 -0
  76. package/licenses/remark-gfm.txt +22 -0
  77. package/licenses/zod.txt +21 -0
  78. package/package.json +138 -0
  79. package/review/index.js +31 -0
  80. package/source/components/avatar.tsx +50 -0
  81. package/source/components/button.tsx +155 -0
  82. package/source/components/callout.tsx +65 -0
  83. package/source/components/canvas/connector.tsx +14 -0
  84. package/source/components/canvas/copy.ts +34 -0
  85. package/source/components/canvas/index.tsx +257 -0
  86. package/source/components/canvas/recorder.ts +36 -0
  87. package/source/components/date-time-field.tsx +77 -0
  88. package/source/components/flow-motion.tsx +109 -0
  89. package/source/components/icons.tsx +275 -0
  90. package/source/components/responsive-overlay.tsx +91 -0
  91. package/source/components/scenario-picker/copy.ts +5 -0
  92. package/source/components/scenario-picker/index.tsx +142 -0
  93. package/source/components/scroll-area.tsx +59 -0
  94. package/source/components/search-picker.tsx +127 -0
  95. package/source/components/segmented-control.tsx +123 -0
  96. package/source/components/skeleton.tsx +11 -0
  97. package/source/components/spinner.tsx +19 -0
  98. package/source/components/superxd-arrow.tsx +22 -0
  99. package/source/components/switch.tsx +45 -0
  100. package/source/components/text-field.tsx +81 -0
  101. package/source/components/thinking.tsx +16 -0
  102. package/source/components/ui/copy.ts +12 -0
  103. package/source/components/ui/display.tsx +276 -0
  104. package/source/components/ui/navigation.tsx +346 -0
  105. package/source/components/ui/portal-theme.tsx +28 -0
  106. package/source/components/ui/selection.tsx +336 -0
  107. package/source/lib/cn.ts +23 -0
  108. package/source/reusable/ai-sdk/index.ts +114 -0
  109. package/source/reusable/assistant-ui/index.tsx +266 -0
  110. package/source/reusable/core/attachments.ts +163 -0
  111. package/source/reusable/core/index.ts +417 -0
  112. package/source/reusable/custom/index.ts +90 -0
  113. package/source/reusable/generative/actions.ts +188 -0
  114. package/source/reusable/generative/copy.ts +47 -0
  115. package/source/reusable/generative/data.tsx +126 -0
  116. package/source/reusable/generative/index.tsx +517 -0
  117. package/source/reusable/generative/schema.ts +327 -0
  118. package/source/reusable/generative/timeline.tsx +214 -0
  119. package/source/reusable/generative/vocabulary.tsx +847 -0
  120. package/source/reusable/interactions/agent.ts +259 -0
  121. package/source/reusable/interactions/artifact.ts +236 -0
  122. package/source/reusable/interactions/context.tsx +38 -0
  123. package/source/reusable/interactions/copy.ts +117 -0
  124. package/source/reusable/interactions/feedback.ts +73 -0
  125. package/source/reusable/interactions/index.ts +11 -0
  126. package/source/reusable/interactions/presentation.tsx +483 -0
  127. package/source/reusable/interactions/store.ts +74 -0
  128. package/source/reusable/interactions/threads.ts +126 -0
  129. package/source/reusable/interactions/web.tsx +574 -0
  130. package/source/reusable/langgraph-review/index.ts +73 -0
  131. package/source/reusable/review/controller.ts +263 -0
  132. package/source/reusable/review/index.ts +47 -0
  133. package/source/reusable/review/types.ts +189 -0
  134. package/source/reusable/ui/index.ts +25 -0
  135. package/source/reusable/uploads/index.ts +48 -0
  136. package/source/reusable/web/attachments/copy.ts +23 -0
  137. package/source/reusable/web/attachments/index.tsx +233 -0
  138. package/source/reusable/web/copy.ts +22 -0
  139. package/source/reusable/web/index.tsx +470 -0
  140. package/source/reusable/web/message/copy.ts +26 -0
  141. package/source/reusable/web/message/index.tsx +388 -0
  142. package/source/reusable/web/message/streaming.ts +64 -0
  143. package/source/reusable/web/message/styles.css +21 -0
  144. package/source/reusable/web/review/copy.ts +78 -0
  145. package/source/reusable/web/review/flow.ts +24 -0
  146. package/source/reusable/web/review/index.tsx +506 -0
  147. package/source/reusable/web/styles.ts +2 -0
  148. package/source/styles/library.css +612 -0
  149. package/source/styles/theme.css +228 -0
  150. package/styles.css +3 -0
  151. package/styles.d.ts +1 -0
  152. package/types/components/avatar.d.ts +14 -0
  153. package/types/components/button.d.ts +36 -0
  154. package/types/components/callout.d.ts +19 -0
  155. package/types/components/canvas/connector.d.ts +2 -0
  156. package/types/components/canvas/copy.d.ts +34 -0
  157. package/types/components/canvas/index.d.ts +33 -0
  158. package/types/components/canvas/recorder.d.ts +10 -0
  159. package/types/components/date-time-field.d.ts +11 -0
  160. package/types/components/flow-motion.d.ts +38 -0
  161. package/types/components/icons.d.ts +138 -0
  162. package/types/components/responsive-overlay.d.ts +15 -0
  163. package/types/components/scenario-picker/copy.d.ts +5 -0
  164. package/types/components/scenario-picker/index.d.ts +14 -0
  165. package/types/components/scroll-area.d.ts +14 -0
  166. package/types/components/search-picker.d.ts +27 -0
  167. package/types/components/segmented-control.d.ts +23 -0
  168. package/types/components/skeleton.d.ts +4 -0
  169. package/types/components/spinner.d.ts +4 -0
  170. package/types/components/superxd-arrow.d.ts +5 -0
  171. package/types/components/switch.d.ts +8 -0
  172. package/types/components/text-field.d.ts +19 -0
  173. package/types/components/thinking.d.ts +8 -0
  174. package/types/components/ui/copy.d.ts +12 -0
  175. package/types/components/ui/display.d.ts +50 -0
  176. package/types/components/ui/navigation.d.ts +63 -0
  177. package/types/components/ui/portal-theme.d.ts +8 -0
  178. package/types/components/ui/selection.d.ts +37 -0
  179. package/types/lib/cn.d.ts +3 -0
  180. package/types/reusable/ai-sdk/index.d.ts +15 -0
  181. package/types/reusable/assistant-ui/index.d.ts +12 -0
  182. package/types/reusable/core/attachments.d.ts +54 -0
  183. package/types/reusable/core/index.d.ts +107 -0
  184. package/types/reusable/custom/index.d.ts +14 -0
  185. package/types/reusable/generative/actions.d.ts +45 -0
  186. package/types/reusable/generative/copy.d.ts +48 -0
  187. package/types/reusable/generative/data.d.ts +4 -0
  188. package/types/reusable/generative/index.d.ts +30 -0
  189. package/types/reusable/generative/schema.d.ts +303 -0
  190. package/types/reusable/generative/timeline.d.ts +21 -0
  191. package/types/reusable/generative/vocabulary.d.ts +34 -0
  192. package/types/reusable/interactions/agent.d.ts +98 -0
  193. package/types/reusable/interactions/artifact.d.ts +118 -0
  194. package/types/reusable/interactions/context.d.ts +130 -0
  195. package/types/reusable/interactions/copy.d.ts +115 -0
  196. package/types/reusable/interactions/feedback.d.ts +34 -0
  197. package/types/reusable/interactions/index.d.ts +10 -0
  198. package/types/reusable/interactions/presentation.d.ts +97 -0
  199. package/types/reusable/interactions/store.d.ts +29 -0
  200. package/types/reusable/interactions/threads.d.ts +38 -0
  201. package/types/reusable/interactions/web.d.ts +22 -0
  202. package/types/reusable/langgraph-review/index.d.ts +5 -0
  203. package/types/reusable/review/controller.d.ts +52 -0
  204. package/types/reusable/review/index.d.ts +12 -0
  205. package/types/reusable/review/types.d.ts +61 -0
  206. package/types/reusable/ui/index.d.ts +19 -0
  207. package/types/reusable/uploads/index.d.ts +11 -0
  208. package/types/reusable/web/attachments/copy.d.ts +25 -0
  209. package/types/reusable/web/attachments/index.d.ts +24 -0
  210. package/types/reusable/web/copy.d.ts +24 -0
  211. package/types/reusable/web/index.d.ts +41 -0
  212. package/types/reusable/web/message/copy.d.ts +27 -0
  213. package/types/reusable/web/message/index.d.ts +56 -0
  214. package/types/reusable/web/message/streaming.d.ts +27 -0
  215. package/types/reusable/web/review/copy.d.ts +72 -0
  216. package/types/reusable/web/review/flow.d.ts +3 -0
  217. package/types/reusable/web/review/index.d.ts +38 -0
  218. package/types/reusable/web/styles.d.ts +2 -0
  219. package/ui/index.js +4 -0
  220. package/uploads/index.js +34 -0
  221. package/web/index.js +931 -0
@@ -0,0 +1,417 @@
1
+ /** Public, platform-neutral contract. No browser, React or runtime-vendor types. */
2
+ export { AttachmentController } from "./attachments.js";
3
+ export type {
4
+ AttachmentItem,
5
+ AttachmentIssue,
6
+ AttachmentSnapshot,
7
+ UploadAdapter,
8
+ UploadPolicy,
9
+ UploadSource,
10
+ } from "./attachments.js";
11
+ export type ChatStatus =
12
+ "idle" | "waiting" | "streaming" | "complete" | "cancelled" | "error" | "interrupted";
13
+ export interface ChatMessage {
14
+ readonly id: string;
15
+ readonly role: "user" | "assistant";
16
+ readonly text: string;
17
+ readonly files?: readonly ChatFile[];
18
+ readonly metadata?: Readonly<Record<string, unknown>>;
19
+ }
20
+ export interface ChatCapabilities {
21
+ readonly send: boolean;
22
+ readonly cancel: boolean;
23
+ /** Restart a response, not continuation of an interrupted stream. */
24
+ readonly retry: boolean;
25
+ readonly attachments?: boolean;
26
+ /** Host explicitly permits replaying a question; this does not undo server work. */
27
+ readonly edit?: boolean;
28
+ readonly regenerate?: boolean;
29
+ readonly branches?: boolean;
30
+ }
31
+ export interface ChatBranch {
32
+ readonly index: number;
33
+ readonly count: number;
34
+ }
35
+ /** A verified reference from the consumer's upload service, never a browser File. */
36
+ export interface ChatFile {
37
+ readonly id: string;
38
+ readonly name: string;
39
+ readonly mediaType: string;
40
+ readonly size: number;
41
+ readonly url: string;
42
+ }
43
+ export interface ChatSnapshot {
44
+ readonly messages: readonly ChatMessage[];
45
+ readonly status: ChatStatus;
46
+ readonly capabilities: ChatCapabilities;
47
+ readonly branches?: Readonly<Record<string, ChatBranch>>;
48
+ }
49
+ export interface ChatRequest {
50
+ readonly requestId: string;
51
+ readonly messages: readonly ChatMessage[];
52
+ }
53
+ export type ChatEvent =
54
+ | { type: "text"; text: string }
55
+ | { type: "replace-text"; text: string }
56
+ | { type: "metadata"; metadata: Readonly<Record<string, unknown>> }
57
+ | { type: "finish" }
58
+ | { type: "error" };
59
+ export interface ChatAdapter {
60
+ readonly capabilities: ChatCapabilities;
61
+ run(request: ChatRequest, signal: AbortSignal): AsyncIterable<ChatEvent>;
62
+ }
63
+ export interface ChatActions {
64
+ send(text: string, files?: readonly ChatFile[]): boolean;
65
+ cancel(): void;
66
+ retry(): boolean;
67
+ edit?(messageId: string, text: string): boolean;
68
+ regenerate?(messageId: string): boolean;
69
+ selectBranch?(messageId: string, direction: "previous" | "next"): boolean;
70
+ }
71
+
72
+ export class ChatInterrupted extends Error {
73
+ constructor() {
74
+ super("The stream ended before its finish event.");
75
+ }
76
+ }
77
+
78
+ export function parseChatFile(value: unknown): ChatFile {
79
+ if (typeof value !== "object" || value === null) throw new Error("Invalid file");
80
+ const file = value as Record<string, unknown>;
81
+ if (
82
+ typeof file.id !== "string" ||
83
+ !/^[\w-]{1,100}$/.test(file.id) ||
84
+ typeof file.name !== "string" ||
85
+ !file.name.trim() ||
86
+ file.name.length > 255 ||
87
+ typeof file.mediaType !== "string" ||
88
+ !/^[\w.+-]+\/[\w.+-]+$/.test(file.mediaType) ||
89
+ typeof file.size !== "number" ||
90
+ !Number.isSafeInteger(file.size) ||
91
+ file.size < 1 ||
92
+ file.size > 10_000_000 ||
93
+ typeof file.url !== "string" ||
94
+ file.url.length > 2000
95
+ )
96
+ throw new Error("Invalid file");
97
+ const url = new URL(file.url);
98
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password)
99
+ throw new Error("Invalid file URL");
100
+ return {
101
+ id: file.id,
102
+ name: file.name,
103
+ mediaType: file.mediaType,
104
+ size: file.size,
105
+ url: file.url,
106
+ };
107
+ }
108
+
109
+ /** Validates the small wire protocol. Provider/server errors never become UI copy. */
110
+ export function parseChatEvent(value: unknown): ChatEvent {
111
+ if (typeof value !== "object" || value === null || !("type" in value))
112
+ throw new Error("Invalid chat event");
113
+ if (
114
+ (value.type === "text" || value.type === "replace-text") &&
115
+ "text" in value &&
116
+ typeof value.text === "string" &&
117
+ value.text.length <= 100_000
118
+ )
119
+ return { type: value.type, text: value.text };
120
+ if (
121
+ value.type === "metadata" &&
122
+ "metadata" in value &&
123
+ typeof value.metadata === "object" &&
124
+ value.metadata !== null &&
125
+ !Array.isArray(value.metadata) &&
126
+ JSON.stringify(value.metadata).length <= 200_000
127
+ )
128
+ return { type: "metadata", metadata: value.metadata as Record<string, unknown> };
129
+ if (value.type === "finish" || value.type === "error") return { type: value.type };
130
+ throw new Error("Unsupported chat event");
131
+ }
132
+
133
+ export function parseChatRequest(value: unknown): ChatRequest {
134
+ if (
135
+ typeof value !== "object" ||
136
+ value === null ||
137
+ !("requestId" in value) ||
138
+ typeof value.requestId !== "string" ||
139
+ !/^[\w-]{1,100}$/.test(value.requestId) ||
140
+ !("messages" in value) ||
141
+ !Array.isArray(value.messages) ||
142
+ value.messages.length < 1 ||
143
+ value.messages.length > 100
144
+ )
145
+ throw new Error("Invalid chat request");
146
+ const messages: ChatMessage[] = value.messages.map((message: unknown) => {
147
+ if (
148
+ typeof message !== "object" ||
149
+ message === null ||
150
+ !("id" in message) ||
151
+ typeof message.id !== "string" ||
152
+ message.id.length > 100 ||
153
+ !("role" in message) ||
154
+ (message.role !== "user" && message.role !== "assistant") ||
155
+ !("text" in message) ||
156
+ typeof message.text !== "string" ||
157
+ message.text.length > (message.role === "user" ? 20_000 : 100_000)
158
+ )
159
+ throw new Error("Invalid message");
160
+ const files =
161
+ "files" in message && message.files !== undefined
162
+ ? Array.isArray(message.files) && message.files.length <= 4
163
+ ? message.files.map(parseChatFile)
164
+ : (() => {
165
+ throw new Error("Invalid files");
166
+ })()
167
+ : undefined;
168
+ return {
169
+ id: message.id,
170
+ role: message.role,
171
+ text: message.text,
172
+ ...(files?.length ? { files } : {}),
173
+ };
174
+ });
175
+ if (
176
+ messages.at(-1)?.role !== "user" ||
177
+ new Set(messages.map((m) => m.id)).size !== messages.length
178
+ )
179
+ throw new Error("Invalid message order");
180
+ return { requestId: value.requestId, messages };
181
+ }
182
+
183
+ interface MessageNode {
184
+ message: ChatMessage;
185
+ parent: string | null;
186
+ status: ChatStatus;
187
+ }
188
+
189
+ /** One bounded, in-memory conversation tree. Snapshots contain only its selected path. */
190
+ export class ChatController implements ChatActions {
191
+ private snapshot: ChatSnapshot;
192
+ private readonly listeners = new Set<() => void>();
193
+ private readonly nodes = new Map<string, MessageNode>();
194
+ private readonly children = new Map<string | null, string[]>();
195
+ private readonly selected = new Map<string | null, string>();
196
+ private active: AbortController | undefined;
197
+ private generation = 0;
198
+ constructor(
199
+ private readonly adapter: ChatAdapter,
200
+ private readonly id: () => string,
201
+ ) {
202
+ this.snapshot = { messages: [], status: "idle", capabilities: adapter.capabilities };
203
+ }
204
+ getSnapshot = (): ChatSnapshot => this.snapshot;
205
+ subscribe = (listener: () => void): (() => void) => {
206
+ this.listeners.add(listener);
207
+ return () => this.listeners.delete(listener);
208
+ };
209
+ private path(): MessageNode[] {
210
+ const path: MessageNode[] = [];
211
+ let current = this.selected.get(null);
212
+ while (current) {
213
+ const node = this.nodes.get(current);
214
+ if (!node) break;
215
+ path.push(node);
216
+ current = this.selected.get(current);
217
+ }
218
+ return path;
219
+ }
220
+ private publish() {
221
+ const path = this.path();
222
+ const branches: Record<string, ChatBranch> = {};
223
+ for (const node of path) {
224
+ const siblings = this.children.get(node.parent) ?? [];
225
+ if (siblings.length > 1)
226
+ branches[node.message.id] = {
227
+ index: siblings.indexOf(node.message.id),
228
+ count: siblings.length,
229
+ };
230
+ }
231
+ this.snapshot = {
232
+ ...this.snapshot,
233
+ messages: path.map((node) => node.message),
234
+ status: path.at(-1)?.status ?? "idle",
235
+ branches,
236
+ };
237
+ for (const listener of this.listeners) listener();
238
+ }
239
+ private add(message: ChatMessage, parent: string | null, status: ChatStatus) {
240
+ const node = { message, parent, status };
241
+ this.nodes.set(message.id, node);
242
+ this.children.set(parent, [...(this.children.get(parent) ?? []), message.id]);
243
+ this.selected.set(parent, message.id);
244
+ return node;
245
+ }
246
+ private room(parent: string | null, count = 1) {
247
+ return this.nodes.size + count <= 500 && (this.children.get(parent)?.length ?? 0) < 20;
248
+ }
249
+ send = (text: string, files: readonly ChatFile[] = []): boolean => {
250
+ const parent = this.snapshot.messages.at(-1)?.id ?? null;
251
+ if (
252
+ this.active ||
253
+ !this.adapter.capabilities.send ||
254
+ (!text.trim() && !files.length) ||
255
+ (files.length > 0 && !this.adapter.capabilities.attachments) ||
256
+ files.length > 4 ||
257
+ text.length > 20_000 ||
258
+ this.snapshot.messages.length >= 98 ||
259
+ !this.room(parent, 2)
260
+ )
261
+ return false;
262
+ let validated: ChatFile[];
263
+ try {
264
+ validated = files.map(parseChatFile);
265
+ } catch {
266
+ return false;
267
+ }
268
+ const user = this.add(
269
+ { id: this.id(), role: "user", text, ...(validated.length ? { files: validated } : {}) },
270
+ parent,
271
+ "idle",
272
+ );
273
+ this.begin(user);
274
+ return true;
275
+ };
276
+ edit = (messageId: string, text: string): boolean => {
277
+ const index = this.snapshot.messages.findIndex(
278
+ (message) => message.id === messageId && message.role === "user",
279
+ );
280
+ const source = this.nodes.get(messageId);
281
+ if (
282
+ this.active ||
283
+ !this.adapter.capabilities.send ||
284
+ !this.adapter.capabilities.edit ||
285
+ !source ||
286
+ index < 0 ||
287
+ text.length > 20_000 ||
288
+ (!text.trim() && !source.message.files?.length) ||
289
+ text === source.message.text ||
290
+ !this.room(source.parent, 2)
291
+ )
292
+ return false;
293
+ const user = this.add({ ...source.message, id: this.id(), text }, source.parent, "idle");
294
+ this.begin(user);
295
+ return true;
296
+ };
297
+ regenerate = (messageId: string): boolean => {
298
+ const source = this.nodes.get(messageId);
299
+ const index = this.snapshot.messages.findIndex(
300
+ (message) => message.id === messageId && message.role === "assistant",
301
+ );
302
+ const parent = source?.parent ? this.nodes.get(source.parent) : undefined;
303
+ if (
304
+ this.active ||
305
+ !this.adapter.capabilities.send ||
306
+ !this.adapter.capabilities.regenerate ||
307
+ index < 0 ||
308
+ parent?.message.role !== "user" ||
309
+ !this.room(parent.message.id)
310
+ )
311
+ return false;
312
+ this.begin(parent);
313
+ return true;
314
+ };
315
+ selectBranch = (messageId: string, direction: "previous" | "next"): boolean => {
316
+ const node = this.nodes.get(messageId);
317
+ if (
318
+ this.active ||
319
+ !this.adapter.capabilities.branches ||
320
+ !node ||
321
+ !this.snapshot.messages.some((message) => message.id === messageId)
322
+ )
323
+ return false;
324
+ const siblings = this.children.get(node.parent) ?? [];
325
+ const next = siblings[siblings.indexOf(messageId) + (direction === "previous" ? -1 : 1)];
326
+ if (!next) return false;
327
+ this.selected.set(node.parent, next);
328
+ this.publish();
329
+ return true;
330
+ };
331
+ retry = (): boolean => {
332
+ const response = this.nodes.get(this.snapshot.messages.at(-1)?.id ?? "");
333
+ const user = response?.parent ? this.nodes.get(response.parent) : undefined;
334
+ if (
335
+ this.active ||
336
+ !this.adapter.capabilities.retry ||
337
+ user?.message.role !== "user" ||
338
+ !["error", "interrupted", "cancelled"].includes(this.snapshot.status) ||
339
+ !this.room(user.message.id)
340
+ )
341
+ return false;
342
+ this.begin(user);
343
+ return true;
344
+ };
345
+ cancel = (): void => {
346
+ if (!this.active || !this.adapter.capabilities.cancel) return;
347
+ ++this.generation;
348
+ this.active.abort();
349
+ this.active = undefined;
350
+ const node = this.nodes.get(this.snapshot.messages.at(-1)?.id ?? "");
351
+ if (node) node.status = "cancelled";
352
+ this.publish();
353
+ };
354
+ dispose = (): void => {
355
+ ++this.generation;
356
+ this.active?.abort();
357
+ this.active = undefined;
358
+ this.listeners.clear();
359
+ };
360
+ private begin(user: MessageNode) {
361
+ const token = ++this.generation;
362
+ const abort = new AbortController();
363
+ this.active = abort;
364
+ const response = this.add(
365
+ { id: this.id(), role: "assistant", text: "" },
366
+ user.message.id,
367
+ "waiting",
368
+ );
369
+ this.publish();
370
+ const history = this.snapshot.messages.slice(0, -1);
371
+ void this.consume(history, response, token, abort);
372
+ }
373
+ private async consume(
374
+ history: readonly ChatMessage[],
375
+ response: MessageNode,
376
+ token: number,
377
+ abort: AbortController,
378
+ ) {
379
+ try {
380
+ let finished = false;
381
+ for await (const event of this.adapter.run(
382
+ { requestId: this.id(), messages: history },
383
+ abort.signal,
384
+ )) {
385
+ if (token !== this.generation || abort.signal.aborted) return;
386
+ if (event.type === "error") throw new Error("Response failed");
387
+ if (event.type === "finish") {
388
+ finished = true;
389
+ break;
390
+ }
391
+ if (event.type === "metadata")
392
+ response.message = { ...response.message, metadata: event.metadata };
393
+ else {
394
+ const text =
395
+ event.type === "replace-text" ? event.text : response.message.text + event.text;
396
+ if (text.length > 100_000) throw new Error("Response too large");
397
+ response.message = { ...response.message, text };
398
+ response.status = "streaming";
399
+ }
400
+ this.publish();
401
+ }
402
+ if (token !== this.generation) return;
403
+ if (!finished) throw new ChatInterrupted();
404
+ this.active = undefined;
405
+ response.status = "complete";
406
+ this.publish();
407
+ } catch (error) {
408
+ if (token !== this.generation) return;
409
+ this.active = undefined;
410
+ response.status = error instanceof ChatInterrupted ? "interrupted" : "error";
411
+ this.publish();
412
+ }
413
+ }
414
+ }
415
+
416
+ export * from "../review/types.js";
417
+ export { ReviewController, reviewReducer } from "../review/controller.js";
@@ -0,0 +1,90 @@
1
+ import {
2
+ ChatInterrupted,
3
+ parseChatEvent,
4
+ type ChatAdapter,
5
+ type ChatEvent,
6
+ } from "../core/index.js";
7
+
8
+ export interface HttpChatOptions {
9
+ endpoint: string;
10
+ /** Opt into editing, regeneration and versions. Restart is controlled separately by retry. */
11
+ conversationActions?: boolean;
12
+ /** Allow Restart as a new run. Defaults to true; disable for endpoints without replay safety. */
13
+ retry?: boolean;
14
+ /** Consumer-owned, evaluated each request so refreshed auth is used. */
15
+ headers?: () => HeadersInit | Promise<HeadersInit>;
16
+ credentials?: RequestCredentials;
17
+ fetch?: typeof globalThis.fetch;
18
+ }
19
+
20
+ /** Newline-delimited JSON over customer-owned HTTP. No hosted account required. */
21
+ export function createHttpChatAdapter(options: HttpChatOptions): ChatAdapter {
22
+ return {
23
+ capabilities: {
24
+ send: true,
25
+ cancel: true,
26
+ retry: options.retry ?? true,
27
+ ...(options.conversationActions ? { edit: true, regenerate: true, branches: true } : {}),
28
+ },
29
+ async *run(request, signal): AsyncGenerator<ChatEvent> {
30
+ const response = await (options.fetch ?? globalThis.fetch)(options.endpoint, {
31
+ method: "POST",
32
+ signal,
33
+ credentials: options.credentials ?? "same-origin",
34
+ headers: {
35
+ ...Object.fromEntries(new Headers(await options.headers?.()).entries()),
36
+ "content-type": "application/json",
37
+ },
38
+ body: JSON.stringify(request),
39
+ });
40
+ if (
41
+ !response.ok ||
42
+ !response.body ||
43
+ !response.headers.get("content-type")?.startsWith("application/x-ndjson")
44
+ )
45
+ throw new Error("Response unavailable");
46
+ const reader = response.body.getReader();
47
+ const decoder = new TextDecoder();
48
+ let pending = "";
49
+ let finished = false;
50
+ const abort = () => {
51
+ void reader.cancel().catch(() => {});
52
+ };
53
+ signal.addEventListener("abort", abort, { once: true });
54
+ try {
55
+ if (signal.aborted) return;
56
+ while (!finished) {
57
+ const { done, value } = await reader.read();
58
+ if (signal.aborted) return;
59
+ pending += decoder.decode(value, { stream: !done });
60
+ let newline: number;
61
+ while ((newline = pending.indexOf("\n")) !== -1) {
62
+ if (newline > 200_000) throw new Error("Stream frame too large");
63
+ const line = pending.slice(0, newline);
64
+ pending = pending.slice(newline + 1);
65
+ if (!line.trim()) continue;
66
+ const event = parseChatEvent(JSON.parse(line));
67
+ yield event;
68
+ if (event.type === "finish" || event.type === "error") {
69
+ finished = true;
70
+ break;
71
+ }
72
+ }
73
+ if (!finished && pending.length > 200_000) throw new Error("Stream frame too large");
74
+ if (done) {
75
+ if (!finished) throw new ChatInterrupted();
76
+ break;
77
+ }
78
+ }
79
+ } catch (error) {
80
+ if (signal.aborted) return;
81
+ if (error instanceof TypeError) throw new ChatInterrupted();
82
+ throw error;
83
+ } finally {
84
+ signal.removeEventListener("abort", abort);
85
+ await reader.cancel().catch(() => {});
86
+ reader.releaseLock();
87
+ }
88
+ },
89
+ };
90
+ }
@@ -0,0 +1,188 @@
1
+ import { z } from "zod";
2
+ import { actionSchema, assertBoundedJson, type StructuredAction } from "./schema.js";
3
+ import { generativeCopy } from "./copy.js";
4
+
5
+ export interface GenerativeRequest {
6
+ readonly key: string;
7
+ readonly action: StructuredAction;
8
+ }
9
+ export interface GenerativeReceipt {
10
+ key: string;
11
+ type: string;
12
+ state: "complete" | "not-applied";
13
+ message: string;
14
+ }
15
+ export interface GenerativeActionHandler {
16
+ /** Host-owned copy and intent. A model cannot mark an action safe or grant permission. */
17
+ label: string;
18
+ description: string;
19
+ kind: "inspect" | "mutate" | "dismiss";
20
+ /** Must enforce authorization and idempotency at the destination. The frozen request survives retries. */
21
+ execute: (request: GenerativeRequest, signal: AbortSignal) => Promise<GenerativeReceipt>;
22
+ /** Return null only if no receipt is available yet; this does not prove an action wasn't applied. */
23
+ lookup?: (request: GenerativeRequest, signal: AbortSignal) => Promise<GenerativeReceipt | null>;
24
+ }
25
+ export type GenerativeActions = Readonly<Record<string, GenerativeActionHandler>>;
26
+ export type GenerativeStage =
27
+ | "ready"
28
+ | "confirm"
29
+ | "submitting"
30
+ | "failed"
31
+ | "uncertain"
32
+ | "checking"
33
+ | "complete"
34
+ | "dismissed";
35
+ export interface GenerativeActionSnapshot {
36
+ stage: GenerativeStage;
37
+ request?: GenerativeRequest;
38
+ label?: string;
39
+ description?: string;
40
+ message?: string;
41
+ noReceipt?: boolean;
42
+ }
43
+ const receiptSchema = z.strictObject({
44
+ key: z.string().min(1).max(160),
45
+ type: z.string().min(1).max(160),
46
+ state: z.enum(["complete", "not-applied"]),
47
+ message: z.string().min(1).max(1000),
48
+ });
49
+ function freeze<T>(data: T): T {
50
+ if (data && typeof data === "object") {
51
+ Object.values(data).forEach(freeze);
52
+ Object.freeze(data);
53
+ }
54
+ return data;
55
+ }
56
+
57
+ /** One state owner. No execution before explicit confirmation of host-declared mutations. */
58
+ export function createGenerativeActionController(
59
+ actions: GenerativeActions,
60
+ makeKey = () => crypto.randomUUID(),
61
+ ) {
62
+ const registry = Object.fromEntries(
63
+ Object.entries(actions).map(([type, handler]) => [type, { ...handler }]),
64
+ );
65
+ let snapshot: GenerativeActionSnapshot = { stage: "ready" };
66
+ let operation: AbortController | undefined;
67
+ let disposed = false;
68
+ const listeners = new Set<() => void>();
69
+ const update = (next: GenerativeActionSnapshot) => {
70
+ if (disposed) return;
71
+ snapshot = freeze(next);
72
+ listeners.forEach((l) => l());
73
+ };
74
+ const find = () =>
75
+ snapshot.request && Object.hasOwn(registry, snapshot.request.action.type)
76
+ ? registry[snapshot.request.action.type]
77
+ : undefined;
78
+ const apply = async (check = false) => {
79
+ const handler = find(),
80
+ request = snapshot.request;
81
+ if (disposed || !handler || !request || operation) return;
82
+ const prior = snapshot;
83
+ const abort = new AbortController();
84
+ operation = abort;
85
+ update({
86
+ ...prior,
87
+ stage: check ? "checking" : "submitting",
88
+ message: undefined,
89
+ noReceipt: false,
90
+ });
91
+ try {
92
+ const result = check
93
+ ? await handler.lookup?.(request, abort.signal)
94
+ : await handler.execute(request, abort.signal);
95
+ if (disposed || operation !== abort) return;
96
+ if (result == null) {
97
+ if (!check) throw new Error("Missing receipt");
98
+ update({ ...prior, stage: "uncertain", noReceipt: true });
99
+ } else {
100
+ const receipt = receiptSchema.parse(result);
101
+ if (receipt.key !== request.key || receipt.type !== request.action.type)
102
+ throw new Error("Receipt does not match");
103
+ update({
104
+ ...prior,
105
+ stage: receipt.state === "complete" ? "complete" : "failed",
106
+ message: receipt.message,
107
+ noReceipt: false,
108
+ });
109
+ }
110
+ } catch {
111
+ if (!disposed && operation === abort)
112
+ update({ ...prior, stage: "uncertain", message: undefined });
113
+ } finally {
114
+ if (operation === abort) operation = undefined;
115
+ }
116
+ };
117
+ return {
118
+ subscribe: (listener: () => void) => {
119
+ listeners.add(listener);
120
+ return () => {
121
+ listeners.delete(listener);
122
+ };
123
+ },
124
+ getSnapshot: () => snapshot,
125
+ request: (input: unknown) => {
126
+ if (disposed || snapshot.stage !== "ready") return false;
127
+ try {
128
+ assertBoundedJson(input, 8000);
129
+ } catch {
130
+ return false;
131
+ }
132
+ const parsed = actionSchema.safeParse(input);
133
+ if (!parsed.success || JSON.stringify(parsed.data).length > 8000) return false;
134
+ const handler = Object.hasOwn(registry, parsed.data.type)
135
+ ? registry[parsed.data.type]
136
+ : undefined;
137
+ if (!handler) return false;
138
+ if (handler.kind === "dismiss") {
139
+ update({ stage: "dismissed", message: handler.label });
140
+ return true;
141
+ }
142
+ const key = makeKey();
143
+ if (typeof key !== "string" || !key || key.length > 160) return false;
144
+ const request = freeze({ key, action: structuredClone(parsed.data) });
145
+ update({
146
+ stage: handler.kind === "mutate" ? "confirm" : "ready",
147
+ request,
148
+ label: handler.label,
149
+ description: handler.description,
150
+ });
151
+ if (handler.kind === "inspect") void apply();
152
+ return true;
153
+ },
154
+ confirm: () => {
155
+ if (snapshot.stage !== "confirm") return false;
156
+ void apply();
157
+ return true;
158
+ },
159
+ back: () => {
160
+ if (!["confirm", "failed"].includes(snapshot.stage)) return false;
161
+ update({ stage: "ready" });
162
+ return true;
163
+ },
164
+ retry: () => {
165
+ if (!["failed", "uncertain"].includes(snapshot.stage)) return false;
166
+ void apply();
167
+ return true;
168
+ },
169
+ check: () => {
170
+ if (snapshot.stage !== "uncertain" || !find()?.lookup) return false;
171
+ void apply(true);
172
+ return true;
173
+ },
174
+ reset: () => {
175
+ if (!["complete", "dismissed"].includes(snapshot.stage)) return false;
176
+ update({ stage: "ready" });
177
+ return true;
178
+ },
179
+ dispose: () => {
180
+ disposed = true;
181
+ operation?.abort();
182
+ operation = undefined;
183
+ listeners.clear();
184
+ },
185
+ };
186
+ }
187
+ export type GenerativeActionController = ReturnType<typeof createGenerativeActionController>;
188
+ export const unavailableActionMessage = generativeCopy.noHandler;