gbs-add-block 2.0.4 → 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.
- package/.gbs/skills/gbs-components/SKILL.md +190 -134
- package/.gbs/skills/gbs-components/references/install.md +25 -3
- package/.gbs/skills/gbs-components/references/styling.md +246 -201
- package/CHANGELOG.md +74 -0
- package/README.md +157 -11
- package/index.cjs +212 -3
- package/package.json +41 -10
- package/schema/passport-v1.schema.json +204 -0
- package/source/beta-components/accordion/passport.json +259 -0
- package/source/beta-components/accordion/styles.css +207 -208
- package/source/beta-components/alert/passport.json +250 -0
- package/source/beta-components/alert/styles.css +154 -155
- package/source/beta-components/avatar/passport.json +294 -0
- package/source/beta-components/avatar/styles.css +201 -203
- package/source/beta-components/badge/passport.json +332 -0
- package/source/beta-components/badge/styles.css +203 -204
- package/source/beta-components/breadcrumb/passport.json +243 -0
- package/source/beta-components/breadcrumb/styles.css +138 -139
- package/source/beta-components/button/passport.json +402 -0
- package/source/beta-components/button/passport.manual.json +31 -0
- package/source/beta-components/button/styles.css +232 -233
- package/source/beta-components/card/passport.json +337 -0
- package/source/beta-components/card/styles.css +230 -231
- package/source/beta-components/checkbox/passport.json +456 -0
- package/source/beta-components/checkbox/styles.css +211 -212
- package/source/beta-components/combobox/passport.json +456 -0
- package/source/beta-components/combobox/styles.css +419 -417
- package/source/beta-components/data-grid/agent/coerce.ts +368 -0
- package/source/beta-components/data-grid/agent/contract.ts +410 -0
- package/source/beta-components/data-grid/agent/dataset.ts +92 -0
- package/source/beta-components/data-grid/agent/engine.ts +470 -0
- package/source/beta-components/data-grid/agent/executors.ts +155 -0
- package/source/beta-components/data-grid/agent/index.ts +79 -0
- package/source/beta-components/data-grid/agent/intent.ts +324 -0
- package/source/beta-components/data-grid/agent/operations.ts +335 -0
- package/source/beta-components/data-grid/agent/validate.ts +630 -0
- package/source/beta-components/data-grid/agent/webmcp.ts +107 -0
- package/source/beta-components/data-grid/index.ts +14 -7
- package/source/beta-components/data-grid/passport.json +1051 -0
- package/source/beta-components/data-grid/passport.manual.json +255 -0
- package/source/beta-components/data-grid/react/AskGrid.tsx +164 -0
- package/source/beta-components/data-grid/react/DataGrid.tsx +39 -0
- package/source/beta-components/data-grid/styles.css +874 -717
- package/source/beta-components/date-picker/passport.json +407 -0
- package/source/beta-components/date-picker/styles.css +445 -446
- package/source/beta-components/dialog/passport.json +344 -0
- package/source/beta-components/dialog/styles.css +280 -278
- package/source/beta-components/file-uploader/passport.json +518 -0
- package/source/beta-components/file-uploader/styles.css +394 -395
- package/source/beta-components/input/passport.json +536 -0
- package/source/beta-components/input/styles.css +295 -296
- package/source/beta-components/menu/passport.json +322 -0
- package/source/beta-components/menu/styles.css +224 -222
- package/source/beta-components/modal/passport.json +289 -0
- package/source/beta-components/modal/styles.css +241 -239
- package/source/beta-components/number-input/passport.json +541 -0
- package/source/beta-components/number-input/styles.css +230 -231
- package/source/beta-components/popover/passport.json +238 -0
- package/source/beta-components/popover/styles.css +148 -146
- package/source/beta-components/progress/passport.json +270 -0
- package/source/beta-components/progress/styles.css +200 -202
- package/source/beta-components/radio-group/passport.json +477 -0
- package/source/beta-components/radio-group/styles.css +241 -242
- package/source/beta-components/shared/core/agent/adapter.ts +65 -0
- package/source/beta-components/shared/core/agent/history.ts +120 -0
- package/source/beta-components/shared/core/agent/index.ts +46 -0
- package/source/beta-components/shared/core/agent/numbers.ts +217 -0
- package/source/beta-components/shared/core/agent/schema.ts +180 -0
- package/source/beta-components/shared/core/agent/types.ts +169 -0
- package/source/beta-components/shared/core/agent/webmcp.ts +328 -0
- package/source/beta-components/shared/index.ts +9 -0
- package/source/beta-components/shared/react/GramproAIProvider.tsx +50 -0
- package/source/beta-components/shared/react/useAskAgent.ts +217 -0
- package/source/beta-components/shared/styles.css +79 -0
- package/source/beta-components/shared/version.json +4 -4
- package/source/beta-components/shared/version.ts +6 -6
- package/source/beta-components/skeleton/passport.json +251 -0
- package/source/beta-components/skeleton/styles.css +185 -186
- package/source/beta-components/spinner/passport.json +245 -0
- package/source/beta-components/spinner/styles.css +145 -146
- package/source/beta-components/switch/passport.json +421 -0
- package/source/beta-components/switch/styles.css +194 -196
- package/source/beta-components/tabs/passport.json +315 -0
- package/source/beta-components/tabs/styles.css +234 -235
- package/source/beta-components/textarea/passport.json +382 -0
- package/source/beta-components/textarea/styles.css +158 -159
- package/source/beta-components/toaster/passport.json +221 -0
- package/source/beta-components/toaster/styles.css +282 -283
- package/source/beta-components/tooltip/passport.json +170 -0
- package/source/beta-components/tooltip/styles.css +71 -72
- package/tools/env.cjs +61 -0
- package/tools/passport/cli.cjs +79 -0
- package/tools/passport/extract.cjs +493 -0
- package/tools/passport/index.cjs +185 -0
- package/tools/passport/merge.cjs +131 -0
- package/tools/passport/policy.cjs +65 -0
- package/tools/passport/validate.cjs +277 -0
- package/tools/ts-require.cjs +79 -0
- package/source/beta-components/accordion/__tests__/core.test.ts +0 -58
- package/source/beta-components/alert/__tests__/core.test.ts +0 -17
- package/source/beta-components/avatar/__tests__/core.test.ts +0 -88
- package/source/beta-components/badge/__tests__/core.test.ts +0 -46
- package/source/beta-components/breadcrumb/__tests__/core.test.ts +0 -58
- package/source/beta-components/button/__tests__/core.test.ts +0 -31
- package/source/beta-components/card/__tests__/core.test.ts +0 -57
- package/source/beta-components/checkbox/__tests__/core.test.ts +0 -40
- package/source/beta-components/combobox/__tests__/core.test.ts +0 -134
- package/source/beta-components/data-grid/__tests__/core.test.ts +0 -356
- package/source/beta-components/data-grid/__tests__/export.test.ts +0 -70
- package/source/beta-components/data-grid/__tests__/pdf.test.ts +0 -209
- package/source/beta-components/date-picker/__tests__/core.test.ts +0 -273
- package/source/beta-components/dialog/__tests__/core.test.ts +0 -86
- package/source/beta-components/file-uploader/__tests__/core.test.ts +0 -395
- package/source/beta-components/input/__tests__/core.test.ts +0 -75
- package/source/beta-components/menu/__tests__/core.test.ts +0 -120
- package/source/beta-components/modal/__tests__/core.test.ts +0 -55
- package/source/beta-components/number-input/__tests__/core.test.ts +0 -151
- package/source/beta-components/progress/__tests__/core.test.ts +0 -56
- package/source/beta-components/radio-group/__tests__/core.test.ts +0 -64
- package/source/beta-components/shared/__tests__/boundaries.test.ts +0 -95
- package/source/beta-components/shared/__tests__/core.test.ts +0 -55
- package/source/beta-components/shared/__tests__/position.test.ts +0 -143
- package/source/beta-components/skeleton/__tests__/core.test.ts +0 -41
- package/source/beta-components/spinner/__tests__/core.test.ts +0 -48
- package/source/beta-components/switch/__tests__/core.test.ts +0 -64
- package/source/beta-components/tabs/__tests__/core.test.ts +0 -51
- package/source/beta-components/textarea/__tests__/core.test.ts +0 -38
- package/source/beta-components/toaster/__tests__/core.test.ts +0 -256
- 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";
|