@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,73 @@
1
+ /** Server-side optional integration; never import into a browser renderer. */
2
+ import {
3
+ Annotation,
4
+ StateGraph,
5
+ MemorySaver,
6
+ START,
7
+ END,
8
+ interrupt,
9
+ Command,
10
+ isInterrupted,
11
+ } from "@langchain/langgraph";
12
+ import {
13
+ parseReviewDecision,
14
+ parseReviewProposal,
15
+ parseReviewReceipt,
16
+ type ReviewProposal,
17
+ type ReviewDecision,
18
+ type ReviewReceipt,
19
+ } from "../review/types.js";
20
+ export function createLangGraphReviewDriver(
21
+ commit: (decision: ReviewDecision) => Promise<ReviewReceipt>,
22
+ ) {
23
+ const State = Annotation.Root({
24
+ proposal: Annotation<ReviewProposal>(),
25
+ decision: Annotation<ReviewDecision | null>(),
26
+ receipt: Annotation<ReviewReceipt | null>(),
27
+ });
28
+ const graph = new StateGraph(State)
29
+ .addNode("review", (state) => ({
30
+ decision: parseReviewDecision(interrupt({ proposal: state.proposal })),
31
+ }))
32
+ // The host's gate rechecks permissions/revision and atomically records the receipt.
33
+ .addNode("apply", async (state) => {
34
+ if (!state.decision) throw new Error("Decision required");
35
+ return { receipt: await commit(state.decision) };
36
+ })
37
+ .addEdge(START, "review")
38
+ .addEdge("review", "apply")
39
+ .addEdge("apply", END)
40
+ .compile({ checkpointer: new MemorySaver() });
41
+ const config = (threadId: string) => ({ configurable: { thread_id: threadId } });
42
+ return {
43
+ async start(proposal: ReviewProposal, threadId: string) {
44
+ const result = await graph.invoke(
45
+ { proposal: parseReviewProposal(proposal), decision: null, receipt: null },
46
+ config(threadId),
47
+ );
48
+ if (!isInterrupted(result) || !result.__interrupt__?.length)
49
+ throw new Error("Approval did not pause");
50
+ return result.__interrupt__[0];
51
+ },
52
+ async resume(value: ReviewDecision, threadId: string) {
53
+ const decision = parseReviewDecision(value);
54
+ const snapshot = await graph.getState(config(threadId));
55
+ const proposal = parseReviewProposal(snapshot.values.proposal);
56
+ if (proposal.id !== decision.proposalId || proposal.revision !== decision.revision)
57
+ throw new Error("Decision does not match the paused proposal");
58
+ const stored = snapshot.values.decision
59
+ ? parseReviewDecision(snapshot.values.decision)
60
+ : null;
61
+ if (stored && JSON.stringify(stored) !== JSON.stringify(decision))
62
+ throw new Error("A different decision already owns this checkpoint");
63
+ if (snapshot.values.receipt) return parseReviewReceipt(snapshot.values.receipt);
64
+ // Retry a failed commit with the original approved decision; do not reinterpret it as a new resume.
65
+ const result = await graph.invoke(
66
+ stored ? null : new Command({ resume: decision }),
67
+ config(threadId),
68
+ );
69
+ if (!result.receipt) throw new Error("Missing decision receipt");
70
+ return parseReviewReceipt(result.receipt);
71
+ },
72
+ };
73
+ }
@@ -0,0 +1,263 @@
1
+ import {
2
+ ReviewFault,
3
+ parseReviewRecord,
4
+ parseReviewReceipt,
5
+ parseReviewRecovery,
6
+ type ReviewAdapter,
7
+ type ReviewDecision,
8
+ type ReviewRecord,
9
+ type ReviewReceipt,
10
+ type ReviewSnapshot,
11
+ } from "./types.js";
12
+ function freezeSnapshot(value: ReviewSnapshot): ReviewSnapshot {
13
+ if (value.record) {
14
+ value.record.proposal.changes.forEach(Object.freeze);
15
+ Object.freeze(value.record.proposal.changes);
16
+ Object.freeze(value.record.proposal);
17
+ Object.freeze(value.record.appliedIds);
18
+ Object.freeze(value.record.remainingIds);
19
+ Object.freeze(value.record);
20
+ }
21
+ if (value.decision) {
22
+ Object.freeze(value.decision.changeIds);
23
+ Object.freeze(value.decision);
24
+ }
25
+ Object.freeze(value.selectedIds);
26
+ return Object.freeze(value);
27
+ }
28
+ const empty: ReviewSnapshot = freezeSnapshot({
29
+ status: "idle",
30
+ record: null,
31
+ selectedIds: [],
32
+ decision: null,
33
+ issue: null,
34
+ });
35
+ type Action =
36
+ | { type: "loading" }
37
+ | { type: "loaded"; record: ReviewRecord }
38
+ | { type: "selected"; ids: readonly string[] }
39
+ | { type: "prepared"; decision: ReviewDecision }
40
+ | { type: "back" }
41
+ | { type: "sending" }
42
+ | { type: "checking" }
43
+ | { type: "settled"; receipt: ReviewReceipt }
44
+ | { type: "unapplied" }
45
+ | { type: "failed"; status: "load-error" | "uncertain" | "conflict" | "blocked" };
46
+ function recordStatus(record: ReviewRecord): ReviewSnapshot["status"] {
47
+ if (record.outcome === "complete" || record.outcome === "denied") return record.outcome;
48
+ if (!record.proposal.canDecide) return "blocked";
49
+ return record.outcome === "partial" ? "partial" : "ready";
50
+ }
51
+ /** One owner for the task, selection and immutable confirmation across all presentations. */
52
+ export function reviewReducer(state: ReviewSnapshot, action: Action): ReviewSnapshot {
53
+ switch (action.type) {
54
+ case "loading":
55
+ return { ...state, status: "loading", decision: null, issue: null };
56
+ case "loaded":
57
+ return { ...empty, status: recordStatus(action.record), record: action.record };
58
+ case "selected":
59
+ return { ...state, selectedIds: action.ids, issue: null };
60
+ case "prepared":
61
+ return { ...state, status: "confirming", decision: action.decision, issue: null };
62
+ case "back":
63
+ return {
64
+ ...state,
65
+ status: state.record?.outcome === "partial" ? "partial" : "ready",
66
+ decision: null,
67
+ issue: null,
68
+ };
69
+ case "sending":
70
+ return { ...state, status: "submitting", issue: null };
71
+ case "checking":
72
+ return { ...state, status: "checking", issue: null };
73
+ case "settled":
74
+ return {
75
+ ...empty,
76
+ record: action.receipt.record,
77
+ status: recordStatus(action.receipt.record),
78
+ selectedIds:
79
+ action.receipt.record.outcome === "partial" ? action.receipt.record.remainingIds : [],
80
+ };
81
+ case "unapplied":
82
+ return { ...state, status: "confirming", issue: "not-applied" };
83
+ case "failed":
84
+ return {
85
+ ...state,
86
+ status: action.status,
87
+ decision:
88
+ action.status === "conflict" || action.status === "blocked" ? null : state.decision,
89
+ };
90
+ }
91
+ }
92
+ export class ReviewController {
93
+ private state: ReviewSnapshot = empty;
94
+ private listeners = new Set<() => void>();
95
+ private operation: AbortController | null = null;
96
+ private generation = 0;
97
+ constructor(
98
+ private adapter: ReviewAdapter,
99
+ private newId: () => string = () => crypto.randomUUID(),
100
+ ) {}
101
+ getSnapshot = (): ReviewSnapshot => this.state;
102
+ subscribe = (callback: () => void): (() => void) => {
103
+ this.listeners.add(callback);
104
+ return () => this.listeners.delete(callback);
105
+ };
106
+ private dispatch(action: Action) {
107
+ this.state = freezeSnapshot(reviewReducer(this.state, action));
108
+ this.listeners.forEach((listener) => listener());
109
+ }
110
+ private async run(
111
+ task: (signal: AbortSignal) => Promise<void>,
112
+ failure: "load-error" | "uncertain",
113
+ ) {
114
+ const generation = ++this.generation;
115
+ this.operation?.abort();
116
+ const operation = new AbortController();
117
+ this.operation = operation;
118
+ try {
119
+ await task(operation.signal);
120
+ } catch (error) {
121
+ if (generation === this.generation && !operation.signal.aborted)
122
+ this.dispatch({
123
+ type: "failed",
124
+ status: error instanceof ReviewFault ? error.kind : failure,
125
+ });
126
+ } finally {
127
+ if (generation === this.generation) this.operation = null;
128
+ }
129
+ }
130
+ load = (): boolean => {
131
+ if (
132
+ this.operation ||
133
+ this.state.decision ||
134
+ !["idle", "load-error", "conflict", "blocked"].includes(this.state.status)
135
+ )
136
+ return false;
137
+ this.dispatch({ type: "loading" });
138
+ void this.run(async (signal) => {
139
+ const record = parseReviewRecord(await this.adapter.load(signal));
140
+ if (!signal.aborted) this.dispatch({ type: "loaded", record });
141
+ }, "load-error");
142
+ return true;
143
+ };
144
+ select = (id: string, selected: boolean): boolean => {
145
+ if (
146
+ !["ready", "partial"].includes(this.state.status) ||
147
+ !this.state.record?.proposal.canDecide ||
148
+ !this.state.record.remainingIds.includes(id)
149
+ )
150
+ return false;
151
+ const ids = new Set(this.state.selectedIds);
152
+ if (selected) ids.add(id);
153
+ else ids.delete(id);
154
+ this.dispatch({ type: "selected", ids: [...ids] });
155
+ return true;
156
+ };
157
+ prepare = (choice: ReviewDecision["choice"]): boolean => {
158
+ const { record, status, selectedIds } = this.state;
159
+ if (
160
+ !record?.proposal.canDecide ||
161
+ !["ready", "partial"].includes(status) ||
162
+ (choice === "approve" && !selectedIds.length)
163
+ )
164
+ return false;
165
+ this.dispatch({
166
+ type: "prepared",
167
+ decision: {
168
+ requestId: this.newId(),
169
+ proposalId: record.proposal.id,
170
+ revision: record.proposal.revision,
171
+ choice,
172
+ changeIds: choice === "approve" ? [...selectedIds].sort() : [],
173
+ },
174
+ });
175
+ return true;
176
+ };
177
+ back = (): boolean => {
178
+ // Receipt absence is a snapshot, not proof that an earlier request cannot still arrive.
179
+ if (this.state.status !== "confirming" || this.state.issue === "not-applied") return false;
180
+ this.dispatch({ type: "back" });
181
+ return true;
182
+ };
183
+ private verifyReceipt(receipt: ReviewReceipt, decision: ReviewDecision) {
184
+ if (
185
+ receipt.requestId !== decision.requestId ||
186
+ receipt.choice !== decision.choice ||
187
+ receipt.record.proposal.id !== decision.proposalId ||
188
+ receipt.appliedIds.some((id) => !decision.changeIds.includes(id)) ||
189
+ receipt.record.proposal.revision <= decision.revision
190
+ )
191
+ throw new Error("Receipt does not match decision");
192
+ const previous = this.state.record?.appliedIds ?? [];
193
+ const added = receipt.record.appliedIds.filter((id) => !previous.includes(id));
194
+ if (
195
+ previous.some((id) => !receipt.record.appliedIds.includes(id)) ||
196
+ added.length !== receipt.appliedIds.length ||
197
+ added.some((id) => !receipt.appliedIds.includes(id)) ||
198
+ receipt.record.remainingIds.some((id) => !decision.changeIds.includes(id)) ||
199
+ (decision.choice === "approve" &&
200
+ decision.changeIds.some(
201
+ (id) => !receipt.appliedIds.includes(id) && !receipt.record.remainingIds.includes(id),
202
+ ))
203
+ )
204
+ throw new Error("Receipt has an unexpected scope");
205
+ }
206
+ submit = (): boolean => {
207
+ const decision = this.state.decision;
208
+ if (this.state.status !== "confirming" || !decision || this.operation) return false;
209
+ this.dispatch({ type: "sending" });
210
+ void this.run(async (signal) => {
211
+ const receipt = parseReviewReceipt(await this.adapter.decide(decision, signal));
212
+ this.verifyReceipt(receipt, decision);
213
+ if (!signal.aborted) this.dispatch({ type: "settled", receipt });
214
+ }, "uncertain");
215
+ return true;
216
+ };
217
+ recover = (): boolean => {
218
+ const decision = this.state.decision;
219
+ if (
220
+ !(
221
+ this.state.status === "uncertain" ||
222
+ (this.state.status === "confirming" && this.state.issue === "not-applied")
223
+ ) ||
224
+ !decision ||
225
+ this.operation
226
+ )
227
+ return false;
228
+ this.dispatch({ type: "checking" });
229
+ void this.run(async (signal) => {
230
+ const recovery = parseReviewRecovery(await this.adapter.recover(decision.requestId, signal));
231
+ if (signal.aborted) return;
232
+ if (recovery.receipt) {
233
+ this.verifyReceipt(recovery.receipt, decision);
234
+ this.dispatch({ type: "settled", receipt: recovery.receipt });
235
+ } else if (
236
+ recovery.record.proposal.id !== decision.proposalId ||
237
+ recovery.record.proposal.revision !== decision.revision ||
238
+ recovery.record.outcome === "complete" ||
239
+ recovery.record.outcome === "denied"
240
+ )
241
+ this.dispatch({ type: "failed", status: "conflict" });
242
+ else if (!recovery.record.proposal.canDecide)
243
+ this.dispatch({ type: "failed", status: "blocked" });
244
+ else this.dispatch({ type: "unapplied" });
245
+ }, "uncertain");
246
+ return true;
247
+ };
248
+ /** End host-owned observation explicitly, never on a presentation unmount. Cannot undo server work. */
249
+ disconnect = () => {
250
+ ++this.generation;
251
+ this.operation?.abort();
252
+ this.operation = null;
253
+ const status = ["submitting", "checking"].includes(this.state.status)
254
+ ? "uncertain"
255
+ : this.state.status === "loading"
256
+ ? "idle"
257
+ : this.state.status;
258
+ if (status !== this.state.status) {
259
+ this.state = freezeSnapshot({ ...this.state, status });
260
+ this.listeners.forEach((listener) => listener());
261
+ }
262
+ };
263
+ }
@@ -0,0 +1,47 @@
1
+ export * from "./types.js";
2
+ export { ReviewController, reviewReducer } from "./controller.js";
3
+ import {
4
+ ReviewFault,
5
+ parseReviewRecord,
6
+ parseReviewReceipt,
7
+ parseReviewRecovery,
8
+ type ReviewAdapter,
9
+ } from "./types.js";
10
+ export interface HttpReviewOptions {
11
+ /** URL for an existing customer-owned review. Never place credentials in this URL. */
12
+ endpoint: string;
13
+ headers?: () => HeadersInit | Promise<HeadersInit>;
14
+ credentials?: RequestCredentials;
15
+ fetch?: typeof globalThis.fetch;
16
+ timeoutMs?: number;
17
+ }
18
+ export function createHttpReviewAdapter(options: HttpReviewOptions): ReviewAdapter {
19
+ const endpoint = options.endpoint.replace(/\/$/, "");
20
+ async function request(path: string, signal: AbortSignal, body?: unknown): Promise<unknown> {
21
+ const timeout = AbortSignal.timeout(options.timeoutMs ?? 15_000);
22
+ const response = await (options.fetch ?? globalThis.fetch)(endpoint + path, {
23
+ signal: AbortSignal.any([signal, timeout]),
24
+ method: body === undefined ? "GET" : "POST",
25
+ credentials: options.credentials ?? "same-origin",
26
+ headers: {
27
+ ...Object.fromEntries(new Headers(await options.headers?.()).entries()),
28
+ ...(body === undefined ? {} : { "content-type": "application/json" }),
29
+ },
30
+ body: body === undefined ? undefined : JSON.stringify(body),
31
+ });
32
+ if (response.status === 409) throw new ReviewFault("conflict");
33
+ if (response.status === 401 || response.status === 403) throw new ReviewFault("blocked");
34
+ if (!response.ok || !response.headers.get("content-type")?.startsWith("application/json"))
35
+ throw new Error("Review unavailable");
36
+ const content = await response.text();
37
+ if (content.length > 220_000) throw new Error("Review response too large");
38
+ return JSON.parse(content);
39
+ }
40
+ return {
41
+ load: async (signal) => parseReviewRecord(await request("", signal)),
42
+ decide: async (decision, signal) =>
43
+ parseReviewReceipt(await request("/decisions", signal, decision)),
44
+ recover: async (requestId, signal) =>
45
+ parseReviewRecovery(await request(`/receipts/${encodeURIComponent(requestId)}`, signal)),
46
+ };
47
+ }
@@ -0,0 +1,189 @@
1
+ /** Portable review contract. The host owns authorization, revisions and side effects. */
2
+ export interface ReviewChange {
3
+ readonly id: string;
4
+ readonly title: string;
5
+ readonly before: string;
6
+ readonly after: string;
7
+ readonly reason: string;
8
+ }
9
+ export interface ReviewProposal {
10
+ readonly id: string;
11
+ readonly revision: number;
12
+ readonly title: string;
13
+ readonly destination: string;
14
+ readonly changes: readonly ReviewChange[];
15
+ readonly canDecide: boolean;
16
+ }
17
+ export interface ReviewRecord {
18
+ readonly proposal: ReviewProposal;
19
+ readonly appliedIds: readonly string[];
20
+ readonly remainingIds: readonly string[];
21
+ readonly outcome: "pending" | "partial" | "complete" | "denied";
22
+ }
23
+ export interface ReviewDecision {
24
+ readonly requestId: string;
25
+ readonly proposalId: string;
26
+ readonly revision: number;
27
+ readonly choice: "approve" | "deny";
28
+ readonly changeIds: readonly string[];
29
+ }
30
+ export interface ReviewReceipt {
31
+ readonly requestId: string;
32
+ readonly choice: "approve" | "deny";
33
+ readonly appliedIds: readonly string[];
34
+ readonly record: ReviewRecord;
35
+ }
36
+ export interface ReviewRecovery {
37
+ readonly record: ReviewRecord;
38
+ readonly receipt: ReviewReceipt | null;
39
+ }
40
+ export interface ReviewAdapter {
41
+ load(signal: AbortSignal): Promise<ReviewRecord>;
42
+ decide(decision: ReviewDecision, signal: AbortSignal): Promise<ReviewReceipt>;
43
+ recover(requestId: string, signal: AbortSignal): Promise<ReviewRecovery>;
44
+ }
45
+ export type ReviewStatus =
46
+ | "idle"
47
+ | "loading"
48
+ | "ready"
49
+ | "confirming"
50
+ | "submitting"
51
+ | "checking"
52
+ | "load-error"
53
+ | "uncertain"
54
+ | "conflict"
55
+ | "blocked"
56
+ | "partial"
57
+ | "complete"
58
+ | "denied";
59
+ export interface ReviewSnapshot {
60
+ readonly status: ReviewStatus;
61
+ readonly record: ReviewRecord | null;
62
+ readonly selectedIds: readonly string[];
63
+ readonly decision: ReviewDecision | null;
64
+ readonly issue: "not-applied" | null;
65
+ }
66
+ export class ReviewFault extends Error {
67
+ constructor(readonly kind: "conflict" | "blocked") {
68
+ super(kind);
69
+ }
70
+ }
71
+
72
+ function object(value: unknown): Record<string, unknown> {
73
+ if (!value || typeof value !== "object" || Array.isArray(value))
74
+ throw new Error("Invalid review data");
75
+ return value as Record<string, unknown>;
76
+ }
77
+ function text(value: unknown, max = 6000): string {
78
+ if (typeof value !== "string" || !value.trim() || value.length > max)
79
+ throw new Error("Invalid review text");
80
+ return value;
81
+ }
82
+ function id(value: unknown): string {
83
+ const result = text(value, 100);
84
+ if (!/^[\w-]+$/.test(result)) throw new Error("Invalid review id");
85
+ return result;
86
+ }
87
+ function ids(value: unknown): string[] {
88
+ if (!Array.isArray(value) || value.length > 32) throw new Error("Invalid review selection");
89
+ const result = value.map(id);
90
+ if (new Set(result).size !== result.length) throw new Error("Duplicate review ids");
91
+ return result;
92
+ }
93
+ function revision(value: unknown): number {
94
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1)
95
+ throw new Error("Invalid review revision");
96
+ return value;
97
+ }
98
+ export function parseReviewProposal(value: unknown): ReviewProposal {
99
+ const p = object(value);
100
+ if (!Array.isArray(p.changes) || p.changes.length > 32 || typeof p.canDecide !== "boolean")
101
+ throw new Error("Invalid proposal");
102
+ const changes = p.changes.map((value): ReviewChange => {
103
+ const c = object(value);
104
+ return {
105
+ id: id(c.id),
106
+ title: text(c.title, 200),
107
+ before: text(c.before),
108
+ after: text(c.after),
109
+ reason: text(c.reason, 1000),
110
+ };
111
+ });
112
+ if (new Set(changes.map((c) => c.id)).size !== changes.length)
113
+ throw new Error("Duplicate changes");
114
+ return {
115
+ id: id(p.id),
116
+ revision: revision(p.revision),
117
+ title: text(p.title, 200),
118
+ destination: text(p.destination, 200),
119
+ changes,
120
+ canDecide: p.canDecide,
121
+ };
122
+ }
123
+ export function parseReviewDecision(value: unknown): ReviewDecision {
124
+ const d = object(value);
125
+ if (
126
+ Object.keys(d).sort().join() !==
127
+ ["changeIds", "choice", "proposalId", "requestId", "revision"].sort().join()
128
+ )
129
+ throw new Error("Invalid decision fields");
130
+ if (d.choice !== "approve" && d.choice !== "deny") throw new Error("Invalid decision");
131
+ const changeIds = ids(d.changeIds).sort();
132
+ if ((d.choice === "approve" && !changeIds.length) || (d.choice === "deny" && changeIds.length))
133
+ throw new Error("Invalid decision selection");
134
+ return {
135
+ requestId: id(d.requestId),
136
+ proposalId: id(d.proposalId),
137
+ revision: revision(d.revision),
138
+ choice: d.choice,
139
+ changeIds,
140
+ };
141
+ }
142
+ export function parseReviewRecord(value: unknown): ReviewRecord {
143
+ const r = object(value);
144
+ const proposal = parseReviewProposal(r.proposal);
145
+ if (!["pending", "partial", "complete", "denied"].includes(String(r.outcome)))
146
+ throw new Error("Invalid review outcome");
147
+ const appliedIds = ids(r.appliedIds),
148
+ remainingIds = ids(r.remainingIds);
149
+ const available = new Set(proposal.changes.map((c) => c.id));
150
+ if (
151
+ [...appliedIds, ...remainingIds].some((i) => !available.has(i)) ||
152
+ remainingIds.some((i) => appliedIds.includes(i))
153
+ )
154
+ throw new Error("Invalid review scope");
155
+ if ((r.outcome === "complete" || r.outcome === "denied") && remainingIds.length)
156
+ throw new Error("Closed review still has pending changes");
157
+ if (r.outcome === "denied" && appliedIds.length)
158
+ throw new Error("Denied review has applied changes");
159
+ if (r.outcome === "pending" && appliedIds.length)
160
+ throw new Error("Pending review has applied changes");
161
+ if (r.outcome === "partial" && (!appliedIds.length || !remainingIds.length))
162
+ throw new Error("Invalid partial result");
163
+ return { proposal, appliedIds, remainingIds, outcome: r.outcome as ReviewRecord["outcome"] };
164
+ }
165
+ export function parseReviewReceipt(value: unknown): ReviewReceipt {
166
+ const r = object(value);
167
+ const record = parseReviewRecord(r.record);
168
+ if (r.choice !== "approve" && r.choice !== "deny") throw new Error("Invalid receipt");
169
+ const appliedIds = ids(r.appliedIds);
170
+ if (
171
+ appliedIds.some((i) => !record.appliedIds.includes(i)) ||
172
+ (r.choice === "deny" &&
173
+ (appliedIds.length ||
174
+ !(
175
+ record.outcome === "denied" ||
176
+ (record.outcome === "complete" && record.appliedIds.length > 0)
177
+ ))) ||
178
+ (r.choice === "approve" && !["complete", "partial"].includes(record.outcome))
179
+ )
180
+ throw new Error("Invalid receipt outcome");
181
+ return { requestId: id(r.requestId), choice: r.choice, appliedIds, record };
182
+ }
183
+ export function parseReviewRecovery(value: unknown): ReviewRecovery {
184
+ const r = object(value);
185
+ return {
186
+ record: parseReviewRecord(r.record),
187
+ receipt: r.receipt === null ? null : parseReviewReceipt(r.receipt),
188
+ };
189
+ }
@@ -0,0 +1,25 @@
1
+ /** Standalone React web foundations, styled by the host's SuperXD tokens. */
2
+ export * from "../../components/ui/selection.js";
3
+ export * from "../../components/ui/navigation.js";
4
+ export * from "../../components/ui/display.js";
5
+ export { uiCopy } from "../../components/ui/copy.js";
6
+ export { Button, ButtonLink, TextLink, buttonStyles } from "../../components/button.js";
7
+ export type {
8
+ ButtonProps,
9
+ ButtonLinkProps,
10
+ ButtonSize,
11
+ ButtonVariant,
12
+ } from "../../components/button.js";
13
+ export type { TextFieldProps } from "../../components/text-field.js";
14
+ export { Switch } from "../../components/switch.js";
15
+ export { SearchPicker } from "../../components/search-picker.js";
16
+ export { SegmentedControl } from "../../components/segmented-control.js";
17
+ export { Callout } from "../../components/callout.js";
18
+ export { Avatar } from "../../components/avatar.js";
19
+ export { Skeleton } from "../../components/skeleton.js";
20
+ export { Spinner } from "../../components/spinner.js";
21
+ export { ScrollArea } from "../../components/scroll-area.js";
22
+ export { ResponsiveOverlay } from "../../components/responsive-overlay.js";
23
+ export { DateTimeField } from "../../components/date-time-field.js";
24
+
25
+ export { ScenarioPicker } from "../../components/scenario-picker/index.js";
@@ -0,0 +1,48 @@
1
+ import { parseChatFile, type UploadAdapter } from "../core/index.js";
2
+
3
+ export interface HttpUploadOptions {
4
+ endpoint: string;
5
+ headers?: () => HeadersInit | Promise<HeadersInit>;
6
+ credentials?: RequestCredentials;
7
+ fetch?: typeof globalThis.fetch;
8
+ /** Consumer-provided deletion endpoint, only for draft references it owns. */
9
+ releaseEndpoint?: (id: string) => string;
10
+ }
11
+ /** Raw binary upload, with Unicode-safe filename metadata. Auth is consumer-owned. */
12
+ export function createHttpUploadAdapter(options: HttpUploadOptions): UploadAdapter<Blob> {
13
+ const request = options.fetch ?? globalThis.fetch;
14
+ const releaseEndpoint = options.releaseEndpoint;
15
+ return {
16
+ async upload(source, signal) {
17
+ const response = await request(options.endpoint, {
18
+ method: "POST",
19
+ signal,
20
+ credentials: options.credentials ?? "same-origin",
21
+ headers: {
22
+ ...Object.fromEntries(new Headers(await options.headers?.()).entries()),
23
+ "content-type": source.mediaType,
24
+ "x-file-name": encodeURIComponent(source.name),
25
+ "x-upload-id": source.id ?? globalThis.crypto.randomUUID(),
26
+ },
27
+ body: source.data,
28
+ });
29
+ if (!response.ok || !response.headers.get("content-type")?.startsWith("application/json"))
30
+ throw new Error("Upload unavailable");
31
+ const body = await response.text();
32
+ if (body.length > 10_000) throw new Error("Upload reference too large");
33
+ return parseChatFile(JSON.parse(body));
34
+ },
35
+ ...(releaseEndpoint
36
+ ? {
37
+ async release(file) {
38
+ const response = await request(releaseEndpoint(file.id), {
39
+ method: "DELETE",
40
+ credentials: options.credentials ?? "same-origin",
41
+ headers: await options.headers?.(),
42
+ });
43
+ if (!response.ok) throw new Error("Release unavailable");
44
+ },
45
+ }
46
+ : {}),
47
+ };
48
+ }
@@ -0,0 +1,23 @@
1
+ export const attachmentCopy = {
2
+ label: "Attachments",
3
+ add: "Add files",
4
+ empty: "No files attached",
5
+ uploading: "Uploading",
6
+ ready: "Ready",
7
+ error: "Upload failed",
8
+ cancelled: "Upload stopped",
9
+ rejected: "Can’t attach this file",
10
+ remove: "Remove",
11
+ retry: "Retry upload",
12
+ cancel: "Stop upload",
13
+ type: "Choose a supported file type.",
14
+ size: "This file is empty or exceeds the size limit.",
15
+ count: "The file limit is reached. Remove a file before adding more.",
16
+ failed: "The file couldn’t upload. Retry or remove it; your message is still here.",
17
+ unavailable: "File uploads aren’t available in this connection.",
18
+ selected: "files selected",
19
+ image: "Image",
20
+ document: "Document",
21
+ file: "File",
22
+ } as const;
23
+ export type AttachmentCopy = { [Key in keyof typeof attachmentCopy]: string };