gbs-add-block 2.1.0 → 2.3.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 (129) hide show
  1. package/.gbs/skills/gbs-components/SKILL.md +190 -134
  2. package/.gbs/skills/gbs-components/references/install.md +25 -3
  3. package/.gbs/skills/gbs-components/references/styling.md +246 -207
  4. package/CHANGELOG.md +74 -0
  5. package/README.md +154 -23
  6. package/index.cjs +212 -3
  7. package/package.json +41 -10
  8. package/schema/passport-v1.schema.json +204 -0
  9. package/source/beta-components/accordion/passport.json +259 -0
  10. package/source/beta-components/accordion/styles.css +207 -208
  11. package/source/beta-components/alert/passport.json +250 -0
  12. package/source/beta-components/alert/styles.css +154 -155
  13. package/source/beta-components/avatar/passport.json +294 -0
  14. package/source/beta-components/avatar/styles.css +225 -226
  15. package/source/beta-components/badge/passport.json +332 -0
  16. package/source/beta-components/badge/styles.css +203 -204
  17. package/source/beta-components/breadcrumb/passport.json +243 -0
  18. package/source/beta-components/breadcrumb/styles.css +138 -140
  19. package/source/beta-components/button/passport.json +402 -0
  20. package/source/beta-components/button/passport.manual.json +31 -0
  21. package/source/beta-components/button/styles.css +232 -234
  22. package/source/beta-components/card/passport.json +337 -0
  23. package/source/beta-components/card/styles.css +230 -232
  24. package/source/beta-components/checkbox/passport.json +456 -0
  25. package/source/beta-components/checkbox/styles.css +211 -213
  26. package/source/beta-components/combobox/passport.json +456 -0
  27. package/source/beta-components/combobox/styles.css +419 -417
  28. package/source/beta-components/data-grid/agent/coerce.ts +368 -0
  29. package/source/beta-components/data-grid/agent/contract.ts +410 -0
  30. package/source/beta-components/data-grid/agent/dataset.ts +92 -0
  31. package/source/beta-components/data-grid/agent/engine.ts +470 -0
  32. package/source/beta-components/data-grid/agent/executors.ts +155 -0
  33. package/source/beta-components/data-grid/agent/index.ts +79 -0
  34. package/source/beta-components/data-grid/agent/intent.ts +324 -0
  35. package/source/beta-components/data-grid/agent/operations.ts +335 -0
  36. package/source/beta-components/data-grid/agent/validate.ts +630 -0
  37. package/source/beta-components/data-grid/agent/webmcp.ts +107 -0
  38. package/source/beta-components/data-grid/index.ts +14 -7
  39. package/source/beta-components/data-grid/passport.json +1051 -0
  40. package/source/beta-components/data-grid/passport.manual.json +255 -0
  41. package/source/beta-components/data-grid/react/AskGrid.tsx +164 -0
  42. package/source/beta-components/data-grid/react/DataGrid.tsx +39 -0
  43. package/source/beta-components/data-grid/styles.css +874 -716
  44. package/source/beta-components/date-picker/passport.json +407 -0
  45. package/source/beta-components/date-picker/styles.css +445 -446
  46. package/source/beta-components/dialog/passport.json +344 -0
  47. package/source/beta-components/dialog/styles.css +280 -279
  48. package/source/beta-components/file-uploader/passport.json +518 -0
  49. package/source/beta-components/file-uploader/styles.css +394 -396
  50. package/source/beta-components/input/passport.json +536 -0
  51. package/source/beta-components/input/styles.css +295 -297
  52. package/source/beta-components/menu/passport.json +322 -0
  53. package/source/beta-components/menu/styles.css +224 -223
  54. package/source/beta-components/modal/passport.json +289 -0
  55. package/source/beta-components/modal/styles.css +241 -240
  56. package/source/beta-components/number-input/passport.json +541 -0
  57. package/source/beta-components/number-input/styles.css +230 -231
  58. package/source/beta-components/popover/passport.json +238 -0
  59. package/source/beta-components/popover/styles.css +148 -147
  60. package/source/beta-components/progress/passport.json +270 -0
  61. package/source/beta-components/progress/styles.css +200 -201
  62. package/source/beta-components/radio-group/passport.json +477 -0
  63. package/source/beta-components/radio-group/styles.css +269 -270
  64. package/source/beta-components/shared/core/agent/adapter.ts +65 -0
  65. package/source/beta-components/shared/core/agent/history.ts +120 -0
  66. package/source/beta-components/shared/core/agent/index.ts +46 -0
  67. package/source/beta-components/shared/core/agent/numbers.ts +217 -0
  68. package/source/beta-components/shared/core/agent/schema.ts +180 -0
  69. package/source/beta-components/shared/core/agent/types.ts +169 -0
  70. package/source/beta-components/shared/core/agent/webmcp.ts +328 -0
  71. package/source/beta-components/shared/index.ts +9 -0
  72. package/source/beta-components/shared/react/GramproAIProvider.tsx +50 -0
  73. package/source/beta-components/shared/react/useAskAgent.ts +217 -0
  74. package/source/beta-components/shared/styles.css +79 -0
  75. package/source/beta-components/shared/version.json +4 -4
  76. package/source/beta-components/shared/version.ts +6 -6
  77. package/source/beta-components/skeleton/passport.json +251 -0
  78. package/source/beta-components/skeleton/styles.css +185 -187
  79. package/source/beta-components/spinner/passport.json +245 -0
  80. package/source/beta-components/spinner/styles.css +173 -174
  81. package/source/beta-components/switch/passport.json +421 -0
  82. package/source/beta-components/switch/styles.css +227 -229
  83. package/source/beta-components/tabs/passport.json +315 -0
  84. package/source/beta-components/tabs/styles.css +263 -264
  85. package/source/beta-components/textarea/passport.json +382 -0
  86. package/source/beta-components/textarea/styles.css +158 -160
  87. package/source/beta-components/toaster/passport.json +221 -0
  88. package/source/beta-components/toaster/styles.css +282 -282
  89. package/source/beta-components/tooltip/passport.json +170 -0
  90. package/source/beta-components/tooltip/styles.css +71 -73
  91. package/tools/env.cjs +61 -0
  92. package/tools/passport/cli.cjs +79 -0
  93. package/tools/passport/extract.cjs +493 -0
  94. package/tools/passport/index.cjs +185 -0
  95. package/tools/passport/merge.cjs +131 -0
  96. package/tools/passport/policy.cjs +65 -0
  97. package/tools/passport/validate.cjs +277 -0
  98. package/tools/ts-require.cjs +79 -0
  99. package/source/beta-components/accordion/__tests__/core.test.ts +0 -58
  100. package/source/beta-components/alert/__tests__/core.test.ts +0 -17
  101. package/source/beta-components/avatar/__tests__/core.test.ts +0 -88
  102. package/source/beta-components/badge/__tests__/core.test.ts +0 -46
  103. package/source/beta-components/breadcrumb/__tests__/core.test.ts +0 -58
  104. package/source/beta-components/button/__tests__/core.test.ts +0 -31
  105. package/source/beta-components/card/__tests__/core.test.ts +0 -57
  106. package/source/beta-components/checkbox/__tests__/core.test.ts +0 -40
  107. package/source/beta-components/combobox/__tests__/core.test.ts +0 -134
  108. package/source/beta-components/data-grid/__tests__/core.test.ts +0 -356
  109. package/source/beta-components/data-grid/__tests__/export.test.ts +0 -70
  110. package/source/beta-components/data-grid/__tests__/pdf.test.ts +0 -209
  111. package/source/beta-components/date-picker/__tests__/core.test.ts +0 -273
  112. package/source/beta-components/dialog/__tests__/core.test.ts +0 -86
  113. package/source/beta-components/file-uploader/__tests__/core.test.ts +0 -395
  114. package/source/beta-components/input/__tests__/core.test.ts +0 -75
  115. package/source/beta-components/menu/__tests__/core.test.ts +0 -120
  116. package/source/beta-components/modal/__tests__/core.test.ts +0 -55
  117. package/source/beta-components/number-input/__tests__/core.test.ts +0 -151
  118. package/source/beta-components/progress/__tests__/core.test.ts +0 -56
  119. package/source/beta-components/radio-group/__tests__/core.test.ts +0 -64
  120. package/source/beta-components/shared/__tests__/boundaries.test.ts +0 -95
  121. package/source/beta-components/shared/__tests__/core.test.ts +0 -55
  122. package/source/beta-components/shared/__tests__/position.test.ts +0 -143
  123. package/source/beta-components/skeleton/__tests__/core.test.ts +0 -41
  124. package/source/beta-components/spinner/__tests__/core.test.ts +0 -48
  125. package/source/beta-components/switch/__tests__/core.test.ts +0 -64
  126. package/source/beta-components/tabs/__tests__/core.test.ts +0 -51
  127. package/source/beta-components/textarea/__tests__/core.test.ts +0 -38
  128. package/source/beta-components/toaster/__tests__/core.test.ts +0 -256
  129. package/source/beta-components/tooltip/__tests__/core.test.ts +0 -42
@@ -0,0 +1,470 @@
1
+ /*
2
+ * The grid agent: contract in, validated command out, state changed.
3
+ *
4
+ * live DataGrid → runtime contract → validated command → executor → new GridState
5
+ *
6
+ * There is no model anywhere in that line, and that is deliberate. Everything
7
+ * this file provides works with a form, a keyboard shortcut or a saved
8
+ * bookmark as the caller: saved views, shareable URLs, an audit trail, and
9
+ * undo across every operation the grid has. A model, when one is added, plugs
10
+ * into the front — it produces the intent, and the five validation layers
11
+ * treat it exactly as they treat any other untrusted input.
12
+ *
13
+ * Caveat worth knowing: undo restores state through `GridApi.setState`, which
14
+ * can only write the keys the grid actually owns. If the host controls
15
+ * `sorting` or `filters` through the `state` prop, it owns undoing them too.
16
+ */
17
+
18
+ import {
19
+ checkSchema,
20
+ createSnapshotHistory,
21
+ type ConfirmRequest,
22
+ type JsonSchema,
23
+ type RegisteredExecutor,
24
+ type SnapshotEntry,
25
+ type ValidationIssue,
26
+ type ValidationLayer,
27
+ } from "../../shared/core/agent";
28
+ import type { GridApi } from "../core/grid";
29
+ import type { GridOptions, GridState } from "../core/types";
30
+ import {
31
+ buildGridContract,
32
+ type GridAgentPolicy,
33
+ type GridColumnSemantics,
34
+ type GridRuntimeContract,
35
+ } from "./contract";
36
+ import { createDataset, type GridDataset } from "./dataset";
37
+ import {
38
+ buildIntentSchema,
39
+ buildResponseSchema,
40
+ type GeneratedIntentSchema,
41
+ type GridResponse,
42
+ } from "./intent";
43
+ import { createGridExecutors } from "./executors";
44
+ import { GRID_OPERATIONS, type GridOperationDefinition, type GridOperationName } from "./operations";
45
+ import { validateBatch, validateIntent, type GridCommand, type GridExplanation } from "./validate";
46
+
47
+ export interface GridView {
48
+ /** View format, not the contract version. */
49
+ v: 1;
50
+ instanceId: string;
51
+ label?: string;
52
+ state: GridState;
53
+ }
54
+
55
+ export type GridExecution =
56
+ | { status: "done"; commands: GridCommand[]; warnings: ValidationIssue[]; state: GridState }
57
+ | {
58
+ status: "needs-confirmation";
59
+ commands: GridCommand[];
60
+ warnings: ValidationIssue[];
61
+ confirm: ConfirmRequest;
62
+ }
63
+ | {
64
+ status: "rejected";
65
+ reason: string;
66
+ code: string;
67
+ layer: ValidationLayer;
68
+ suggestion?: string;
69
+ issues: ValidationIssue[];
70
+ }
71
+ /* The producer asked a question instead of answering. Nothing runs. */
72
+ | { status: "clarify"; question: string; options?: string[] }
73
+ /* The producer refused. Nothing runs, and that is a correct outcome. */
74
+ | { status: "declined"; reason: string };
75
+
76
+ export interface GridAgentOptions<T> {
77
+ api: GridApi<T>;
78
+ /** The very object handed to `<DataGrid>`: data, columns and feature flags. */
79
+ options: GridOptions<T>;
80
+ /** Stable across renders. Defaults to a hash of the column ids. */
81
+ instanceId?: string;
82
+ /** What a machine cannot read off a column definition. */
83
+ semantics?: Readonly<Record<string, GridColumnSemantics>>;
84
+ policy?: GridAgentPolicy;
85
+ /** BCP 47 locale for the explanations. */
86
+ locale?: string;
87
+ /** Column summaries and derived enum values in the contract. Default true. */
88
+ stats?: boolean;
89
+ maxEnumValues?: number;
90
+ /** Undo depth. Default 50. */
91
+ historyLimit?: number;
92
+ /** Reference date for relative phrases such as "last month". */
93
+ now?: () => Date;
94
+ /** Replace the default `GridApi`-backed executors. */
95
+ executors?: Map<GridOperationName, RegisteredExecutor<GridRuntimeContract>>;
96
+ }
97
+
98
+ export interface GridAgent<T> {
99
+ /** What this instance can do, right now. Recomputed when state changes. */
100
+ contract(): GridRuntimeContract;
101
+ /** The generated JSON Schema for one intent on this instance. */
102
+ schema(): JsonSchema;
103
+ /**
104
+ * The schema a producer of intents is held to: a command, a question, or a
105
+ * refusal. This is the one to hand a constrained decoder.
106
+ */
107
+ responseSchema(): JsonSchema;
108
+ /**
109
+ * Validate a whole response envelope. Clarifying and declining are checked
110
+ * against the schema like anything else, so a `clarify` with no question is
111
+ * malformed output rather than a clarification.
112
+ */
113
+ respond(response: unknown): GridExecution;
114
+ /** The static operation definitions this instance offers. */
115
+ operations(): GridOperationDefinition[];
116
+ /** Validate without changing anything. */
117
+ validate(intent: unknown): GridExecution;
118
+ /** Alias for `validate`, for callers showing a confirmation step. */
119
+ preview(intent: unknown): GridExecution;
120
+ execute(intent: unknown, options?: { confirm?: boolean }): Promise<GridExecution>;
121
+ undo(): boolean;
122
+ redo(): boolean;
123
+ canUndo(): boolean;
124
+ canRedo(): boolean;
125
+ /** Oldest first. Suitable as an audit trail. */
126
+ history(): readonly SnapshotEntry<GridState>[];
127
+ clearHistory(): void;
128
+ saveView(label?: string): GridView;
129
+ applyView(view: GridView): boolean;
130
+ /** Hand the agent this render's props when `data`, `columns` or flags change. */
131
+ update(options: GridOptions<T>): void;
132
+ dataset(): GridDataset<T>;
133
+ }
134
+
135
+ // --------------------------------------------------------------- view coding
136
+
137
+ const toBase64Url = (text: string): string => {
138
+ const bytes = new TextEncoder().encode(text);
139
+ let binary = "";
140
+ for (const byte of bytes) binary += String.fromCharCode(byte);
141
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
142
+ };
143
+
144
+ const fromBase64Url = (text: string): string => {
145
+ const padded = text.replace(/-/g, "+").replace(/_/g, "/");
146
+ const binary = atob(padded + "=".repeat((4 - (padded.length % 4)) % 4));
147
+ const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
148
+ return new TextDecoder().decode(bytes);
149
+ };
150
+
151
+ /** A saved view as one URL-safe string. */
152
+ export const encodeGridView = (view: GridView): string => toBase64Url(JSON.stringify(view));
153
+
154
+ /** Reads a string from `encodeGridView`, or null when it is not one. */
155
+ export function decodeGridView(text: string): GridView | null {
156
+ try {
157
+ const parsed = JSON.parse(fromBase64Url(text)) as GridView;
158
+ return parsed && parsed.v === 1 && typeof parsed.state === "object" ? parsed : null;
159
+ } catch {
160
+ return null;
161
+ }
162
+ }
163
+
164
+ // --------------------------------------------------------------------- agent
165
+
166
+ export function createGridAgent<T>(input: GridAgentOptions<T>): GridAgent<T> {
167
+ let options = input.options;
168
+ const { api, semantics, policy = {}, locale, stats, maxEnumValues, instanceId } = input;
169
+ const now = input.now ?? (() => new Date());
170
+
171
+ const history = createSnapshotHistory<GridState>({
172
+ read: () => api.getState(),
173
+ write: (snapshot) => api.setState(() => snapshot),
174
+ limit: input.historyLimit ?? 50,
175
+ });
176
+
177
+ const executors = input.executors ?? createGridExecutors(api, history);
178
+
179
+ let datasetCache: { key: unknown[]; value: GridDataset<T> } | null = null;
180
+ const dataset = (): GridDataset<T> => {
181
+ const key = [options.data, options.columns, options.getRowId, options.mode, options.rowCount];
182
+ if (!datasetCache || datasetCache.key.some((part, i) => part !== key[i])) {
183
+ datasetCache = { key, value: createDataset(options, locale) };
184
+ }
185
+ return datasetCache.value;
186
+ };
187
+
188
+ let contractCache: { key: unknown[]; value: GridRuntimeContract } | null = null;
189
+ const contract = (): GridRuntimeContract => {
190
+ const state = api.getState();
191
+ const key = [options, state, history.canUndo(), history.canRedo(), dataset()];
192
+ if (!contractCache || contractCache.key.some((part, i) => part !== key[i])) {
193
+ contractCache = {
194
+ key,
195
+ value: buildGridContract({
196
+ options,
197
+ dataset: dataset(),
198
+ state,
199
+ ...(instanceId ? { instanceId } : {}),
200
+ ...(semantics ? { semantics } : {}),
201
+ policy,
202
+ ...(stats !== undefined ? { stats } : {}),
203
+ ...(maxEnumValues !== undefined ? { maxEnumValues } : {}),
204
+ canUndo: history.canUndo(),
205
+ canRedo: history.canRedo(),
206
+ }),
207
+ };
208
+ }
209
+ return contractCache.value;
210
+ };
211
+
212
+ let schemaCache: { key: GridRuntimeContract; value: GeneratedIntentSchema } | null = null;
213
+ const generated = (): GeneratedIntentSchema => {
214
+ const current = contract();
215
+ if (!schemaCache || schemaCache.key !== current) {
216
+ schemaCache = { key: current, value: buildIntentSchema(current) };
217
+ }
218
+ return schemaCache.value;
219
+ };
220
+
221
+ const asList = (intent: unknown): unknown[] => (Array.isArray(intent) ? intent : [intent]);
222
+
223
+ /**
224
+ * Validate a full response envelope: a command, a question, or a refusal.
225
+ *
226
+ * Clarifying is not a way around the validator: `{ result: "clarify" }` with
227
+ * no question comes back rejected, because a label is not a question.
228
+ *
229
+ * A command's *intents* are deliberately not checked against the response
230
+ * schema here. They go to `check`, which triages the column first and can
231
+ * therefore say "Email cannot be filtered" where the schema could only say
232
+ * "matched no branch" — same verdict, but one of them tells you what to do.
233
+ * The full response schema remains what a constrained decoder is given.
234
+ */
235
+ function respond(response: unknown): GridExecution {
236
+ // A bare list of intents is the batch form that `validate` already takes.
237
+ if (Array.isArray(response)) return check(response);
238
+
239
+ if (response === null || typeof response !== "object") {
240
+ return {
241
+ status: "rejected",
242
+ reason: "A response must be a JSON object, or a list of intents.",
243
+ code: "not-an-object",
244
+ layer: "schema",
245
+ issues: [],
246
+ };
247
+ }
248
+
249
+ const envelope = response as Record<string, unknown>;
250
+ // No envelope at all: treat it as a bare intent, which is what the direct
251
+ // callers of `validate` send.
252
+ if (envelope.result === undefined) return check(response);
253
+
254
+ const branch = (buildResponseSchema(contract()).oneOf ?? []).find(
255
+ (entry) => entry.properties?.result?.const === envelope.result,
256
+ );
257
+ if (!branch) {
258
+ /*
259
+ * `String()` on an object gives "[object Object]", which tells a reader
260
+ * nothing. A nested envelope — `{ result: { clarify: "..." } }` — is a
261
+ * real and recoverable mistake, so name it.
262
+ */
263
+ const value = envelope.result;
264
+ const shape =
265
+ typeof value === "string"
266
+ ? `"${value}"`
267
+ : Array.isArray(value)
268
+ ? "a list"
269
+ : value === null
270
+ ? "null"
271
+ : typeof value === "object"
272
+ ? "an object"
273
+ : `a ${typeof value}`;
274
+ // Only an object can plausibly be a mis-nested envelope; a list cannot.
275
+ const key =
276
+ value !== null && typeof value === "object" && !Array.isArray(value)
277
+ ? Object.keys(value)[0]
278
+ : undefined;
279
+ const nested =
280
+ key && ["command", "clarify", "declined"].includes(key)
281
+ ? ` Did you mean { "result": "${key}", ... }?`
282
+ : "";
283
+ return {
284
+ status: "rejected",
285
+ reason:
286
+ `"result" must be the string "command", "clarify" or "declined"; got ${shape}.${nested}`,
287
+ code: "unknown-result",
288
+ layer: "schema",
289
+ issues: [],
290
+ };
291
+ }
292
+
293
+ // For a command, the schema's job here is the envelope only; `check` owns
294
+ // the intents and gives better reasons than a union miss would.
295
+ const shape =
296
+ envelope.result === "command"
297
+ ? { ...branch, properties: { ...branch.properties, intents: { type: "array" as const, minItems: 1 } } }
298
+ : branch;
299
+
300
+ const problems = checkSchema(response, shape);
301
+ if (problems.length > 0) {
302
+ const first = problems[0];
303
+ return {
304
+ status: "rejected",
305
+ reason: `${first.path || "response"}: ${first.message}`,
306
+ code: first.code,
307
+ layer: "schema",
308
+ issues: problems.map((p) => ({
309
+ layer: "schema" as const,
310
+ code: p.code,
311
+ message: `${p.path || "response"}: ${p.message}`,
312
+ ...(p.path ? { path: p.path } : {}),
313
+ })),
314
+ };
315
+ }
316
+
317
+ const valid = response as GridResponse;
318
+ if (valid.result === "clarify") {
319
+ return {
320
+ status: "clarify",
321
+ question: valid.question,
322
+ ...(valid.options ? { options: valid.options } : {}),
323
+ };
324
+ }
325
+ if (valid.result === "declined") return { status: "declined", reason: valid.reason };
326
+ return check(valid.intents);
327
+ }
328
+
329
+ function check(intent: unknown): GridExecution {
330
+ const list = asList(intent);
331
+ const current = contract();
332
+ const context = {
333
+ contract: current,
334
+ schema: generated(),
335
+ dataset: dataset(),
336
+ state: api.getState(),
337
+ policy,
338
+ ...(locale ? { locale } : {}),
339
+ now: now(),
340
+ };
341
+
342
+ const result =
343
+ list.length === 1
344
+ ? mapSingle(validateIntent(list[0], context))
345
+ : validateBatch(list, context);
346
+
347
+ if (!result.ok) {
348
+ return {
349
+ status: "rejected",
350
+ reason: result.reason,
351
+ code: result.code,
352
+ layer: result.layer,
353
+ ...(result.suggestion ? { suggestion: result.suggestion } : {}),
354
+ issues: result.issues,
355
+ };
356
+ }
357
+
358
+ const history2 = result.command.filter(
359
+ (command) => command.operation === "undo" || command.operation === "redo",
360
+ );
361
+ if (history2.length > 0 && result.command.length > 1) {
362
+ return {
363
+ status: "rejected",
364
+ reason: "Undo and redo cannot be combined with other operations in one batch.",
365
+ code: "history-in-batch",
366
+ layer: "schema",
367
+ issues: [],
368
+ };
369
+ }
370
+
371
+ return result.confirm
372
+ ? {
373
+ status: "needs-confirmation",
374
+ commands: result.command,
375
+ warnings: result.warnings,
376
+ confirm: result.confirm,
377
+ }
378
+ : { status: "done", commands: result.command, warnings: result.warnings, state: api.getState() };
379
+ }
380
+
381
+ async function execute(intent: unknown, runOptions: { confirm?: boolean } = {}): Promise<GridExecution> {
382
+ const checked = respond(intent);
383
+ if (checked.status === "rejected" || checked.status === "clarify" || checked.status === "declined") {
384
+ return checked;
385
+ }
386
+ if (checked.status === "needs-confirmation" && runOptions.confirm !== true) return checked;
387
+
388
+ const commands = checked.commands;
389
+ const current = contract();
390
+ const context = { contract: current, dryRun: false };
391
+
392
+ const run = async () => {
393
+ for (const command of commands) {
394
+ const executor = executors.get(command.operation);
395
+ if (!executor) {
396
+ throw new Error(`[DataGrid] No executor registered for "${command.operation}".`);
397
+ }
398
+ await executor.execute(command.intent as never, context);
399
+ }
400
+ };
401
+
402
+ const historyOnly =
403
+ commands.length === 1 && (commands[0].operation === "undo" || commands[0].operation === "redo");
404
+
405
+ if (historyOnly) {
406
+ // Runs outside `record`, because it *is* the history moving.
407
+ await run();
408
+ } else {
409
+ const label = commands.map((command) => command.explain.summary).join("; ");
410
+ await history.record(label, run);
411
+ }
412
+
413
+ return {
414
+ status: "done",
415
+ commands,
416
+ warnings: checked.status === "needs-confirmation" ? checked.warnings : checked.warnings,
417
+ state: api.getState(),
418
+ };
419
+ }
420
+
421
+ return {
422
+ contract,
423
+ schema: () => generated().schema,
424
+ responseSchema: () => buildResponseSchema(contract()),
425
+ respond,
426
+ operations: () => contract().operations.map((name) => GRID_OPERATIONS[name]),
427
+ validate: check,
428
+ preview: check,
429
+ execute,
430
+ undo: () => history.undo(),
431
+ redo: () => history.redo(),
432
+ canUndo: () => history.canUndo(),
433
+ canRedo: () => history.canRedo(),
434
+ history: () => history.entries(),
435
+ clearHistory: () => history.clear(),
436
+
437
+ saveView: (label) => ({
438
+ v: 1,
439
+ instanceId: contract().instanceId,
440
+ ...(label ? { label } : {}),
441
+ state: api.getState(),
442
+ }),
443
+
444
+ applyView(view) {
445
+ if (view.v !== 1) return false;
446
+ history.record(view.label ? `Apply view "${view.label}"` : "Apply saved view", () => {
447
+ api.setState(() => view.state);
448
+ });
449
+ return true;
450
+ },
451
+
452
+ update(next) {
453
+ options = next;
454
+ },
455
+
456
+ dataset,
457
+ };
458
+ }
459
+
460
+ /** One result and a list of results differ only in shape; keep the rest shared. */
461
+ function mapSingle(
462
+ result: ReturnType<typeof validateIntent>,
463
+ ): | { ok: true; command: GridCommand[]; warnings: ValidationIssue[]; confirm: ConfirmRequest | null }
464
+ | Extract<ReturnType<typeof validateIntent>, { ok: false }> {
465
+ return result.ok
466
+ ? { ok: true, command: [result.command], warnings: result.warnings, confirm: result.confirm }
467
+ : result;
468
+ }
469
+
470
+ export type { GridCommand, GridExplanation };
@@ -0,0 +1,155 @@
1
+ /*
2
+ * How a DataGrid performs its operations.
3
+ *
4
+ * Every executor here closes over a `GridApi`, because that is how this
5
+ * particular component is driven. Nothing above this file knows that. Swap in
6
+ * a registry whose executors call a server, or a host's `setState`, and the
7
+ * contract, the schema, the validator and the history all carry on unchanged
8
+ * — which is the whole reason the executor is a separate thing from the
9
+ * operation.
10
+ *
11
+ * `undo` and `redo` are the demonstration: they are real operations with no
12
+ * `GridApi` method behind them at all.
13
+ */
14
+
15
+ import type { GridApi } from "../core/grid";
16
+ import type { PinSide, SortItem } from "../core/types";
17
+ import type { ExecutionContext, RegisteredExecutor } from "../../shared/core/agent";
18
+ import type { GridRuntimeContract } from "./contract";
19
+ import type { GridIntent } from "./intent";
20
+ import type { GridOperationName } from "./operations";
21
+
22
+ export type GridExecutionContext = ExecutionContext<GridRuntimeContract>;
23
+
24
+ export interface GridExecutorHost {
25
+ undo(): boolean;
26
+ redo(): boolean;
27
+ }
28
+
29
+ type Intent<A extends GridOperationName> = Extract<GridIntent, { action: A }>;
30
+
31
+ function executor<A extends GridOperationName>(
32
+ operation: A,
33
+ run: (input: Intent<A>, context: GridExecutionContext) => void | Promise<void>,
34
+ apiMethod?: string,
35
+ ): RegisteredExecutor<GridRuntimeContract> {
36
+ return {
37
+ operation,
38
+ ...(apiMethod ? { apiMethod } : {}),
39
+ execute: run as (input: never, context: GridExecutionContext) => void | Promise<void>,
40
+ };
41
+ }
42
+
43
+ /**
44
+ * The default registry: one executor per operation, each naming the `GridApi`
45
+ * method it calls so an audit trail can record it.
46
+ */
47
+ export function createGridExecutors<T>(
48
+ api: GridApi<T>,
49
+ host: GridExecutorHost,
50
+ ): Map<GridOperationName, RegisteredExecutor<GridRuntimeContract>> {
51
+ const entries: RegisteredExecutor<GridRuntimeContract>[] = [
52
+ executor("search", (intent) => api.setGlobalFilter(intent.text), "setGlobalFilter"),
53
+
54
+ executor(
55
+ "filter",
56
+ (intent) =>
57
+ api.setFilter(intent.column, {
58
+ operator: intent.operator,
59
+ ...(intent.value !== undefined ? { value: intent.value } : {}),
60
+ ...(intent.value2 !== undefined ? { value2: intent.value2 } : {}),
61
+ }),
62
+ "setFilter",
63
+ ),
64
+
65
+ executor("clearFilters", () => api.clearFilters(), "clearFilters"),
66
+
67
+ executor(
68
+ "sort",
69
+ (intent) => {
70
+ const item: SortItem = { columnId: intent.column, desc: intent.direction === "desc" };
71
+ // `setSorting` replaces; appending is a read-modify-write on the
72
+ // current sort, with the same column replaced rather than duplicated.
73
+ if (!intent.append) {
74
+ api.setSorting([item]);
75
+ return;
76
+ }
77
+ const existing = api.getState().sorting.filter((entry) => entry.columnId !== intent.column);
78
+ api.setSorting([...existing, item]);
79
+ },
80
+ "setSorting",
81
+ ),
82
+
83
+ executor("clearSort", () => api.setSorting([]), "setSorting"),
84
+
85
+ executor(
86
+ "selectRows",
87
+ (intent) => {
88
+ const value = intent.value !== false;
89
+ for (const rowId of intent.rowIds) api.toggleRowSelected(rowId, { value });
90
+ },
91
+ "toggleRowSelected",
92
+ ),
93
+
94
+ executor("selectAll", (intent) => api.toggleAllRowsSelected(intent.value !== false), "toggleAllRowsSelected"),
95
+ executor("clearSelection", () => api.clearSelection(), "clearSelection"),
96
+
97
+ executor(
98
+ "setColumnVisibility",
99
+ (intent) => api.setColumnVisibility(intent.column, intent.visible),
100
+ "setColumnVisibility",
101
+ ),
102
+
103
+ executor(
104
+ "pinColumn",
105
+ (intent) => api.pinColumn(intent.column, (intent.side ?? false) as PinSide | false),
106
+ "pinColumn",
107
+ ),
108
+
109
+ executor(
110
+ "moveColumn",
111
+ (intent) => api.moveColumn(intent.column, intent.target, intent.placement),
112
+ "moveColumn",
113
+ ),
114
+
115
+ executor("setColumnWidth", (intent) => api.setColumnWidth(intent.column, intent.width), "setColumnWidth"),
116
+ executor("resetColumns", () => api.resetColumns(), "resetColumns"),
117
+
118
+ executor("setPage", (intent) => api.setPageIndex(intent.index), "setPageIndex"),
119
+ executor("setPageSize", (intent) => api.setPageSize(intent.size), "setPageSize"),
120
+ executor("setDensity", (intent) => api.setDensity(intent.density), "setDensity"),
121
+
122
+ // One operation, three methods — so no single `apiMethod` is recorded.
123
+ executor("export", (intent) => {
124
+ const options = {
125
+ scope: intent.scope ?? ("filtered" as const),
126
+ ...(intent.fileName ? { fileName: intent.fileName } : {}),
127
+ };
128
+ if (intent.format === "csv") return api.exportCsv(options);
129
+ if (intent.format === "excel") return api.exportExcel(options);
130
+ return api.exportPdf(options);
131
+ }),
132
+
133
+ executor(
134
+ "print",
135
+ (intent) =>
136
+ api.print({
137
+ scope: intent.scope ?? "filtered",
138
+ ...(intent.title ? { title: intent.title } : {}),
139
+ }),
140
+ "print",
141
+ ),
142
+
143
+ executor("copy", () => api.copyToClipboard(), "copyToClipboard"),
144
+
145
+ // Performed by the agent's history. Proof that `apiMethod` is optional.
146
+ executor("undo", () => {
147
+ host.undo();
148
+ }),
149
+ executor("redo", () => {
150
+ host.redo();
151
+ }),
152
+ ];
153
+
154
+ return new Map(entries.map((entry) => [entry.operation as GridOperationName, entry]));
155
+ }
@@ -0,0 +1,79 @@
1
+ /*
2
+ * The DataGrid agent runtime.
3
+ *
4
+ * Framework-free: no React import anywhere under `agent/`, so the same code
5
+ * can validate a command on a server before it is ever sent to a browser.
6
+ */
7
+
8
+ export {
9
+ buildGridContract,
10
+ GRID_CONTRACT_VERSION,
11
+ type BuildContractOptions,
12
+ type GridAgentPolicy,
13
+ type GridCapabilities,
14
+ type GridColumnSemantics,
15
+ type GridColumnStats,
16
+ type GridContractColumn,
17
+ type GridContractState,
18
+ type GridContractStats,
19
+ type GridRuntimeContract,
20
+ } from "./contract";
21
+
22
+ export { createDataset, type GridDataset } from "./dataset";
23
+
24
+ export {
25
+ GRID_OPERATIONS,
26
+ GRID_OPERATION_NAMES,
27
+ isGridOperation,
28
+ type GridOperationDefinition,
29
+ type GridOperationName,
30
+ } from "./operations";
31
+
32
+ export {
33
+ buildIntentSchema,
34
+ buildResponseSchema,
35
+ COLUMN_REQUIREMENT,
36
+ type GeneratedIntentSchema,
37
+ type GridIntent,
38
+ type GridResponse,
39
+ } from "./intent";
40
+
41
+ export {
42
+ coerceFilter,
43
+ nearest,
44
+ resolveRelativeDate,
45
+ type CoercionNote,
46
+ type CoerceOptions,
47
+ type DateRange,
48
+ } from "./coerce";
49
+
50
+ export {
51
+ projectQuery,
52
+ validateBatch,
53
+ validateIntent,
54
+ type GridCommand,
55
+ type GridExplanation,
56
+ type ValidateContext,
57
+ } from "./validate";
58
+
59
+ export { createGridExecutors, type GridExecutionContext, type GridExecutorHost } from "./executors";
60
+
61
+ export {
62
+ createGridAgent,
63
+ decodeGridView,
64
+ encodeGridView,
65
+ type GridAgent,
66
+ type GridAgentOptions,
67
+ type GridExecution,
68
+ type GridView,
69
+ } from "./engine";
70
+
71
+ export {
72
+ GRID_TOOL_NAME,
73
+ registerGridTool,
74
+ type ModelContext,
75
+ type RegisterGridToolOptions,
76
+ type RegistrationResult,
77
+ type ToolCallLog,
78
+ type ToolResultPayload,
79
+ } from "./webmcp";