@grampro/kit 2.7.2 → 2.7.3

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 (75) hide show
  1. package/.gbs/skills/gbs-components/SKILL.md +233 -209
  2. package/.gbs/skills/gbs-components/references/data-grid.md +9 -0
  3. package/.gbs/skills/gbs-components/references/forms.md +12 -0
  4. package/.gbs/skills/gbs-components/references/install.md +14 -3
  5. package/CHANGELOG.md +963 -861
  6. package/README.md +11 -3
  7. package/index.cjs +108 -123
  8. package/package.json +1 -1
  9. package/source/components/accordion/passport.json +1 -1
  10. package/source/components/alert/passport.json +1 -1
  11. package/source/components/avatar/passport.json +1 -1
  12. package/source/components/badge/passport.json +1 -1
  13. package/source/components/breadcrumb/passport.json +1 -1
  14. package/source/components/button/passport.json +1 -1
  15. package/source/components/card/passport.json +1 -1
  16. package/source/components/checkbox/passport.json +2 -2
  17. package/source/components/checkbox/passport.manual.json +1 -1
  18. package/source/components/checkbox/react/Checkbox.tsx +14 -0
  19. package/source/components/combobox/passport.json +3 -2
  20. package/source/components/combobox/passport.manual.json +1 -1
  21. package/source/components/combobox/react/MultiSelect.tsx +13 -0
  22. package/source/components/combobox/react/Select.tsx +13 -0
  23. package/source/components/combobox/react/surface.ts +70 -0
  24. package/source/components/data-grid/passport.json +6 -6
  25. package/source/components/data-grid/react/DataGrid.tsx +353 -351
  26. package/source/components/date-picker/passport.json +3 -2
  27. package/source/components/date-picker/passport.manual.json +1 -1
  28. package/source/components/date-picker/react/DatePicker.tsx +16 -2
  29. package/source/components/date-picker/react/DateRangePicker.tsx +16 -2
  30. package/source/components/date-picker/react/surface.ts +68 -0
  31. package/source/components/dialog/passport.json +1 -1
  32. package/source/components/file-uploader/passport.json +1 -1
  33. package/source/components/input/passport.json +2 -2
  34. package/source/components/input/passport.manual.json +1 -1
  35. package/source/components/input/react/Input.tsx +21 -0
  36. package/source/components/menu/passport.json +1 -1
  37. package/source/components/modal/passport.json +1 -1
  38. package/source/components/number-input/passport.json +2 -2
  39. package/source/components/number-input/passport.manual.json +1 -1
  40. package/source/components/number-input/react/NumberInput.tsx +30 -0
  41. package/source/components/popover/passport.json +1 -1
  42. package/source/components/progress/passport.json +1 -1
  43. package/source/components/radio-group/passport.json +2 -2
  44. package/source/components/radio-group/passport.manual.json +1 -1
  45. package/source/components/radio-group/react/RadioGroup.tsx +33 -0
  46. package/source/components/shared/core/agent/field-executors.ts +173 -137
  47. package/source/components/shared/core/agent/index.ts +21 -0
  48. package/source/components/shared/core/agent/surface.ts +437 -0
  49. package/source/components/shared/core/agent/webmcp.ts +332 -328
  50. package/source/components/shared/core/agent/write.ts +62 -58
  51. package/source/components/shared/index.ts +7 -0
  52. package/source/components/shared/react/AgentSurface.tsx +145 -0
  53. package/source/components/shared/react/agentSurfaceContext.ts +92 -0
  54. package/source/components/shared/react/useFieldAgent.ts +62 -88
  55. package/source/components/shared/version.json +4 -4
  56. package/source/components/shared/version.ts +6 -6
  57. package/source/components/skeleton/passport.json +1 -1
  58. package/source/components/spinner/passport.json +1 -1
  59. package/source/components/switch/passport.json +2 -2
  60. package/source/components/switch/passport.manual.json +1 -1
  61. package/source/components/switch/react/Switch.tsx +16 -0
  62. package/source/components/tabs/passport.json +1 -1
  63. package/source/components/textarea/passport.json +2 -2
  64. package/source/components/textarea/passport.manual.json +1 -1
  65. package/source/components/textarea/react/Textarea.tsx +16 -0
  66. package/source/components/toaster/passport.json +1 -1
  67. package/source/components/tooltip/passport.json +1 -1
  68. package/tools/doctor/baseline.cjs +5 -4
  69. package/tools/mcp/server.cjs +13 -1
  70. package/tools/passport/extract.cjs +20 -65
  71. package/tools/passport/hash.cjs +18 -1
  72. package/tools/passport/index.cjs +4 -2
  73. package/tools/passport/typescript.cjs +220 -0
  74. package/tools/prompt.cjs +205 -0
  75. package/tools/ui.cjs +67 -0
package/README.md CHANGED
@@ -14,6 +14,9 @@ Figma design.
14
14
  npx @grampro/kit add DataGrid
15
15
  ```
16
16
 
17
+ Or `npx @grampro/kit add` with no names, to pick from the list: type to search,
18
+ space to select, enter to install.
19
+
17
20
  The source lands in `component-lib/`, or `src/component-lib/` in a project
18
21
  with a `src/` folder; `--dest` puts it anywhere you like, and later installs
19
22
  follow the first. There is no runtime package to depend on, and nothing to
@@ -218,6 +221,10 @@ version. What is built and tested:
218
221
  application supplies. No model is bundled, downloaded or named.
219
222
  - **WebMCP** — one tool, `operate_grid`, exposing the same validated boundary to
220
223
  a browser agent. Needs no model at all.
224
+ - **`<AgentSurface>`** — wrap a page and every field and grid in it describes
225
+ itself to a browser agent, from its own props, and is operated through its own
226
+ validator. One WebMCP tool, `operate_page`, for the whole page; no tool
227
+ definitions to write
221
228
  - A model evaluation harness, with the measurements that justify both
222
229
 
223
230
  Every producer — a model, a browser agent, a form — proposes. The validator
@@ -227,9 +234,10 @@ it is why no AI path gets a second route into the component.
227
234
  Nine of the 27 are agent-operable. The executor abstraction was the open
228
235
  question — a grid is driven through an imperative API, and a field is not — and
229
236
  it is settled: Combobox and DatePicker are driven through their handles,
230
- Input, Textarea, NumberInput, Checkbox, Switch and RadioGroup through the
231
- element their ref already hands back. Neither the contract, the schemas, the
232
- validator nor the refusals know which.
237
+ Input, Textarea, Checkbox and Switch through the element their ref already
238
+ hands back, and NumberInput and RadioGroup, inside an `<AgentSurface>`, through
239
+ their own commit path — a number is not text, and a group is not one radio.
240
+ Neither the contract, the schemas, the validator nor the refusals know which.
233
241
 
234
242
  Still open: the components whose value is in what they contain rather than what
235
243
  they hold — Menu, Tabs, Accordion, Modal, FileUploader — and the question of
package/index.cjs CHANGED
@@ -24,7 +24,7 @@ const path = require("path");
24
24
  const crypto = require("crypto");
25
25
  const yargs = require("yargs/yargs");
26
26
  const { hideBin } = require("yargs/helpers");
27
- const readline = require("readline");
27
+ const ui = require("./tools/ui.cjs");
28
28
 
29
29
  // Configuration
30
30
  const CONFIG = {
@@ -391,7 +391,7 @@ const copyCommonFiles = async (destPath) => {
391
391
  const src = path.join(LEGACY_SOURCE_PATH, ...file.src);
392
392
  const dest = path.join(destPath, file.dest);
393
393
  await fs.copy(src, dest, { overwrite: true });
394
- console.log(`✓ ${file.dest} copied successfully`);
394
+ console.log(`${ui.check()} ${file.dest} copied successfully`);
395
395
  }
396
396
  };
397
397
 
@@ -467,7 +467,7 @@ const installShared = async (destPath, { force = false } = {}) => {
467
467
 
468
468
  if (order > 0) {
469
469
  console.error(
470
- `\n✗ This project has shared v${installedVersion}, newer than the v${srcVersion} in this installer.`,
470
+ `\n${ui.cross()} This project has shared v${installedVersion}, newer than the v${srcVersion} in this installer.`,
471
471
  );
472
472
  console.error(
473
473
  " Installing it would downgrade the folder and break the components already here.",
@@ -478,11 +478,17 @@ const installShared = async (destPath, { force = false } = {}) => {
478
478
  process.exit(1);
479
479
  }
480
480
 
481
- if (order === 0) {
481
+ // The same version is only "already installed" when it is the same files:
482
+ // a release that changed shared/ without bumping it must still reach the
483
+ // project, or the components it brings import files that are not there.
484
+ const sameFiles = listFiles(src).every(
485
+ (file) => manifest.files[file] === sha256(path.join(src, file)),
486
+ );
487
+ if (order === 0 && sameFiles) {
482
488
  console.log(
483
489
  edited.length > 0
484
- ? `✓ shared v${installedVersion} is already installed (keeping your ${edited.length} edited file(s))`
485
- : `✓ shared v${installedVersion} is already installed`,
490
+ ? `${ui.check()} shared v${installedVersion} is already installed (keeping your ${edited.length} edited file(s))`
491
+ : `${ui.check()} shared v${installedVersion} is already installed`,
486
492
  );
487
493
  return;
488
494
  }
@@ -490,7 +496,7 @@ const installShared = async (destPath, { force = false } = {}) => {
490
496
 
491
497
  if (edited.length > 0 && !force) {
492
498
  console.error(
493
- `\n✗ These files in component-lib/${SHARED_DIR} have local changes:`,
499
+ `\n${ui.cross()} These files in component-lib/${SHARED_DIR} have local changes:`,
494
500
  );
495
501
  edited.forEach((file) => console.error(` - ${file}`));
496
502
  console.error(
@@ -541,8 +547,8 @@ const installShared = async (destPath, { force = false } = {}) => {
541
547
 
542
548
  console.log(
543
549
  installedVersion
544
- ? `✓ shared updated ${installedVersion} → ${srcVersion}`
545
- : `✓ shared v${srcVersion} installed`,
550
+ ? `${ui.check()} shared updated ${installedVersion} → ${srcVersion}`
551
+ : `${ui.check()} shared v${srcVersion} installed`,
546
552
  );
547
553
  };
548
554
 
@@ -656,7 +662,7 @@ const writeAntigravityRule = async (root) => {
656
662
 
657
663
  /**
658
664
  * Codex: a pointer in AGENTS.md rather than the skill itself, because that file
659
- * is always in context and the skill is 5 KB. Everything outside our markers is
665
+ * is always in context and the skill is about 10 KB. Everything outside our markers is
660
666
  * left exactly as it was.
661
667
  */
662
668
  const writeAgentsPointer = async (root) => {
@@ -727,7 +733,7 @@ const installSkill = async (
727
733
  const edited = locallyEdited(destRoot, manifest);
728
734
 
729
735
  if (edited.length > 0 && !force) {
730
- console.error(`${NL}✗ These skill files have local changes:`);
736
+ console.error(`${NL}${ui.cross()} These skill files have local changes:`);
731
737
  edited.forEach((file) => console.error(` - ${file}`));
732
738
  console.error(
733
739
  NL +
@@ -793,8 +799,8 @@ const installSkill = async (
793
799
 
794
800
  console.log(
795
801
  installedVersion
796
- ? `✓ skill updated ${installedVersion} → ${PACKAGE_VERSION}`
797
- : `✓ skill v${PACKAGE_VERSION} installed`,
802
+ ? `${ui.check()} skill updated ${installedVersion} → ${PACKAGE_VERSION}`
803
+ : `${ui.check()} skill v${PACKAGE_VERSION} installed`,
798
804
  );
799
805
  console.log(` ${SKILL_DEST_DIR}/ (canonical, ${listFiles(src).length} files)`);
800
806
  if (targets.includes("claude")) {
@@ -902,10 +908,17 @@ const runPassport = async ({ check: checkOnly, dest: requestedDest }) => {
902
908
  process.exit(1);
903
909
  }
904
910
 
905
- const pkgPath = path.join(process.cwd(), "package.json");
906
- const libraryVersion = fs.existsSync(pkgPath)
907
- ? readJson(pkgPath, {}).version || "unknown"
908
- : "unknown";
911
+ /*
912
+ * Each component is stamped with the kit version it was installed from —
913
+ * its install record, else the passport it shipped with — not with this
914
+ * project's own version. Stamping the project's version made every
915
+ * regenerated passport differ from the shipped one, so `passport --check`
916
+ * failed for components nobody had touched.
917
+ */
918
+ const libraryVersion = (name, dir) =>
919
+ readJson(path.join(dir, ".install-manifest.json"), {})?.version ||
920
+ readJson(path.join(dir, "passport.json"), {})?.source?.libraryVersion ||
921
+ PACKAGE_VERSION;
909
922
 
910
923
  const options = {
911
924
  libRoot: dest,
@@ -913,6 +926,7 @@ const runPassport = async ({ check: checkOnly, dest: requestedDest }) => {
913
926
  libraryVersion,
914
927
  includeLocal: true,
915
928
  coverageThreshold: undefined,
929
+ regenerate: `npx ${CLI_NAME} passport`,
916
930
  };
917
931
 
918
932
  try {
@@ -924,7 +938,7 @@ const runPassport = async ({ check: checkOnly, dest: requestedDest }) => {
924
938
  continue;
925
939
  }
926
940
  for (const issue of row.problems) {
927
- console.log(` ${issue.level === "error" ? "✗" : "!"} ${issue.component}: ${issue.message}`);
941
+ console.log(` ${issue.level === "error" ? ui.cross(process.stdout) : ui.c.yellow("!")} ${issue.component}: ${issue.message}`);
928
942
  }
929
943
  }
930
944
  console.log(`${NL}${report.length} component(s) · ${errors} error(s) · ${warnings} warning(s)`);
@@ -934,9 +948,9 @@ const runPassport = async ({ check: checkOnly, dest: requestedDest }) => {
934
948
 
935
949
  const { results, written } = passport.generate(options);
936
950
  const errors = results.flatMap((r) => r.issues.filter((i) => i.level === "error"));
937
- for (const issue of errors) console.error(` ✗ ${issue.component}: ${issue.message}`);
951
+ for (const issue of errors) console.error(` ${ui.cross()} ${issue.component}: ${issue.message}`);
938
952
  console.log(
939
- `${NL}✓ ${results.length} passport(s) regenerated · ${written.length} changed`,
953
+ `${NL}${ui.check()} ${results.length} passport(s) regenerated · ${written.length} changed`,
940
954
  );
941
955
  if (errors.length > 0) process.exit(1);
942
956
  } catch (error) {
@@ -1038,14 +1052,14 @@ const copyComponent = async (component, destPath, current = false, { force = fal
1038
1052
  if (outcome === "updated") {
1039
1053
  console.log(
1040
1054
  previous && previous !== PACKAGE_VERSION
1041
- ? `✓ Component ${component} updated ${previous} → ${PACKAGE_VERSION}`
1042
- : `✓ Component ${component} replaced with ${PACKAGE_VERSION}${force ? " (--force)" : ""}`,
1055
+ ? `${ui.check()} Component ${component} updated ${previous} → ${PACKAGE_VERSION}`
1056
+ : `${ui.check()} Component ${component} replaced with ${PACKAGE_VERSION}${force ? " (--force)" : ""}`,
1043
1057
  );
1044
1058
  } else if (outcome === "unchanged") {
1045
- console.log(`✓ Component ${component} already up to date (${PACKAGE_VERSION})`);
1059
+ console.log(`${ui.check()} Component ${component} already up to date (${PACKAGE_VERSION})`);
1046
1060
  } else {
1047
1061
  console.log(
1048
- `✓ Component ${component} installed successfully ${
1062
+ `${ui.check()} Component ${component} installed successfully ${
1049
1063
  component === "Grid"
1050
1064
  ? "This Version of Grid will be deprecated soon. Please Install The New Data Grid Component"
1051
1065
  : ""
@@ -1069,8 +1083,8 @@ const reportKept = (component, status) => {
1069
1083
  files.length ? ` ${label}: ${files.slice(0, 6).join(", ")}${files.length > 6 ? `, +${files.length - 6} more` : ""}` : null;
1070
1084
  console.error(
1071
1085
  status.state === "unknown"
1072
- ? `✗ ${component} was not replaced: it was installed before 2.7.2 and differs from ${PACKAGE_VERSION}, so it may hold your changes.`
1073
- : `✗ ${component} was not replaced: it has changes made since it was installed.`,
1086
+ ? `${ui.cross()} ${component} was not replaced: it was installed before 2.7.2 and differs from ${PACKAGE_VERSION}, so it may hold your changes.`
1087
+ : `${ui.cross()} ${component} was not replaced: it has changes made since it was installed.`,
1074
1088
  );
1075
1089
  for (const line of [list("changed", status.changed), list("deleted", status.missing)].filter(Boolean)) {
1076
1090
  console.error(line);
@@ -1098,7 +1112,7 @@ const installComponentWithDependencies = async (
1098
1112
 
1099
1113
  if (pendingInstalls.length === 0) {
1100
1114
  console.log(
1101
- `✓ ${component} and all its dependencies are already installed.`,
1115
+ `${ui.check()} ${component} and all its dependencies are already installed.`,
1102
1116
  );
1103
1117
  return;
1104
1118
  }
@@ -1144,7 +1158,7 @@ const installMultipleComponents = async (
1144
1158
 
1145
1159
  if (pendingInstalls.length === 0) {
1146
1160
  console.log(
1147
- "✓ All selected components and their dependencies are already installed.",
1161
+ `${ui.check()} All selected components and their dependencies are already installed.`,
1148
1162
  );
1149
1163
  return;
1150
1164
  }
@@ -1175,95 +1189,44 @@ const installMultipleComponents = async (
1175
1189
  console.log(`\nFor documentation visit: ${CONFIG.docs}`);
1176
1190
  };
1177
1191
 
1178
- const interactiveComponentSelector = async (current = false) => {
1179
- return new Promise((resolve) => {
1180
- const rl = readline.createInterface({
1181
- input: process.stdin,
1182
- output: process.stdout,
1183
- });
1184
-
1185
- let selectedComponents = new Set();
1186
- let currentIndex = 0;
1187
-
1188
- const renderMenu = () => {
1189
- console.clear();
1190
- console.log("🚀 Component Installer - Interactive Mode");
1191
- console.log(
1192
- "Use ↑/↓ arrow keys to navigate, SPACE to select/deselect, ENTER to install\n",
1193
- );
1194
-
1195
- const availableComponents = getAvailableComponents(current);
1196
-
1197
- availableComponents.forEach((component, index) => {
1198
- const isSelected = selectedComponents.has(component);
1199
- const isCurrentIndex = index === currentIndex;
1200
- const deps = CONFIG.dependencies[component]
1201
- ? ` (requires: ${CONFIG.dependencies[component].join(", ")})`
1202
- : "";
1203
-
1204
- const prefix = isCurrentIndex ? ">" : " ";
1205
- const checkbox = isSelected ? "☑" : "☐";
1206
- const line = `${prefix} ${checkbox} ${component}${deps}`;
1207
-
1208
- if (isCurrentIndex) {
1209
- console.log(`\x1b[36m${line}\x1b[0m`); // Cyan highlight
1210
- } else {
1211
- console.log(line);
1212
- }
1213
- });
1214
-
1215
- console.log(`\nSelected: ${selectedComponents.size} components`);
1216
- console.log("Press ENTER to install selected components, or 'q' to quit");
1217
- };
1218
-
1219
- const handleKeyPress = (key) => {
1220
- switch (key) {
1221
- case "": // Up arrow
1222
- currentIndex = Math.max(0, currentIndex - 1);
1223
- renderMenu();
1224
- break;
1225
- case "": // Down arrow
1226
- currentIndex = Math.min(
1227
- getAvailableComponents(current).length - 1,
1228
- currentIndex + 1,
1229
- );
1230
- renderMenu();
1231
- break;
1232
- case " ": // Space bar
1233
- const component = getAvailableComponents(current)[currentIndex];
1234
- if (selectedComponents.has(component)) {
1235
- selectedComponents.delete(component);
1236
- } else {
1237
- selectedComponents.add(component);
1238
- }
1239
- renderMenu();
1240
- break;
1241
- case "\r": // Enter
1242
- if (selectedComponents.size > 0) {
1243
- rl.close();
1244
- resolve(Array.from(selectedComponents));
1245
- }
1246
- break;
1247
- case "q":
1248
- rl.close();
1249
- resolve([]);
1250
- break;
1251
- }
1252
- };
1253
-
1254
- // Enable raw mode to capture arrow keys
1255
- process.stdin.setRawMode(true);
1256
- process.stdin.resume();
1257
- process.stdin.setEncoding("utf8");
1258
- process.stdin.on("data", handleKeyPress);
1259
-
1260
- renderMenu();
1192
+ /*
1193
+ * Where each component sits in the docs' navigation, shown beside it in the
1194
+ * picker and searchable: typing "overlay" narrows the list to the overlays.
1195
+ */
1196
+ const GROUPS = {
1197
+ Inputs: ["Button", "Checkbox", "Combobox", "DatePicker", "FileUploader", "Input", "NumberInput", "RadioGroup", "Switch", "Textarea"],
1198
+ Data: ["DataGrid"],
1199
+ Overlays: ["Dialog", "Menu", "Modal", "Popover", "Tooltip"],
1200
+ Feedback: ["Alert", "Progress", "Skeleton", "Spinner", "Toaster"],
1201
+ Display: ["Accordion", "Avatar", "Badge", "Card"],
1202
+ Navigation: ["Breadcrumb", "Tabs"],
1203
+ };
1204
+ const groupOf = (component) =>
1205
+ Object.keys(GROUPS).find((group) => GROUPS[group].includes(component)) || "";
1206
+
1207
+ /** The one-line purpose a component's passport gives, for the picker's hint line. */
1208
+ const purposeOf = (component, current) => {
1209
+ if (!current) return "";
1210
+ const passport = readJson(path.join(SOURCE_PATH, folderFor(component, true), "passport.json"));
1211
+ return (passport && passport.purpose) || "";
1212
+ };
1261
1213
 
1262
- rl.on("close", () => {
1263
- process.stdin.setRawMode(false);
1264
- process.stdin.pause();
1214
+ const interactiveComponentSelector = async (current = false, destPath) => {
1215
+ const { multiselect } = require("./tools/prompt.cjs");
1216
+ const options = getAvailableComponents(current)
1217
+ .map((component) => ({
1218
+ value: component,
1219
+ tag: current ? groupOf(component) : "",
1220
+ hint: purposeOf(component, current),
1221
+ note: destPath && checkComponentExists(component, destPath, current) ? "installed" : "",
1222
+ }))
1223
+ // The docs' order of groups, alphabetical within each.
1224
+ .sort((a, b) => {
1225
+ const order = Object.keys(GROUPS);
1226
+ return order.indexOf(a.tag) - order.indexOf(b.tag) || a.value.localeCompare(b.value);
1265
1227
  });
1266
- });
1228
+ const picked = await multiselect({ message: "Which components?", options });
1229
+ return picked || [];
1267
1230
  };
1268
1231
 
1269
1232
  const parseMultipleComponents = (componentString) => {
@@ -1420,7 +1383,7 @@ const main = async () => {
1420
1383
  .example("$0 mcp install --for codex", "Only write the Codex config")
1421
1384
  .example("$0 secret set figma", "Store a Figma token in the OS credential store")
1422
1385
  .example("$0 design ./design.json", "Split a whole-file Figma export into design/")
1423
- .example("$0 -i", "Interactive selection")
1386
+ .example("$0 add", "Pick components interactively (same as -i)")
1424
1387
  .help().argv;
1425
1388
 
1426
1389
  /*
@@ -1497,10 +1460,15 @@ const main = async () => {
1497
1460
  if (parsed.command === "passport") argv.passport = true;
1498
1461
  if (parsed.command === "add") {
1499
1462
  if (parsed.operands.length === 0) {
1500
- console.log(`Which component? Try: ${CLI_NAME} add Button (or: ${CLI_NAME} list)`);
1501
- return;
1463
+ // In a terminal, a bare `add` opens the picker; in a script it says what to type.
1464
+ if (process.stdin.isTTY && process.stdout.isTTY) argv.interactive = true;
1465
+ else {
1466
+ console.log(`Which component? Try: ${CLI_NAME} add Button (or: ${CLI_NAME} list)`);
1467
+ return;
1468
+ }
1469
+ } else {
1470
+ argv.add = parsed.operands.join(",");
1502
1471
  }
1503
- argv.add = parsed.operands.join(",");
1504
1472
  }
1505
1473
  } else if (argv.add || argv.list || argv.skill || argv.passport) {
1506
1474
  console.log(`Tip: the shorter form is \`${CLI_NAME} add <Component>\`.`);
@@ -1532,21 +1500,38 @@ const main = async () => {
1532
1500
 
1533
1501
  // Interactive mode
1534
1502
  if (argv.interactive) {
1535
- console.log("Starting interactive component selector...\n");
1536
- const selectedComponents = await interactiveComponentSelector(current);
1503
+ const destPath = resolveDestPath(argv.dest);
1504
+ const where = path.relative(process.cwd(), destPath) || ".";
1505
+ ui.intro(`GramproKit ${PACKAGE_VERSION}`);
1506
+ let selectedComponents;
1507
+ try {
1508
+ selectedComponents = await interactiveComponentSelector(current, destPath);
1509
+ } catch (error) {
1510
+ ui.outro(ui.c.red(error.message));
1511
+ process.exitCode = 1;
1512
+ return;
1513
+ }
1537
1514
 
1538
1515
  if (selectedComponents.length === 0) {
1539
- console.log("No components selected. Exiting...");
1516
+ ui.outro(ui.c.dim("Nothing installed."));
1540
1517
  return;
1541
1518
  }
1519
+ console.log(ui.c.gray(ui.s.bar));
1520
+ console.log(`${ui.c.gray(ui.s.bar)} ${ui.c.dim(`Installing into ${where}/`)}`);
1521
+ console.log("");
1542
1522
 
1543
- const destPath = resolveDestPath(argv.dest);
1544
1523
  await fs.ensureDir(destPath);
1545
1524
 
1546
1525
  await copySupportFiles(destPath, current, { force: argv.force });
1547
1526
 
1548
1527
  // Install selected components
1549
1528
  await installMultipleComponents(selectedComponents, destPath, current, { force: argv.force });
1529
+ console.log("");
1530
+ ui.outro(
1531
+ process.exitCode
1532
+ ? ui.c.yellow("Done, with the warnings above.")
1533
+ : `${ui.c.green("Done.")} Import them from ${ui.c.cyan(`${where}/`)}`,
1534
+ );
1550
1535
  return;
1551
1536
  }
1552
1537
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grampro/kit",
3
- "version": "2.7.2",
3
+ "version": "2.7.3",
4
4
  "description": "GramproKit — an agent-native UI runtime for React. Copy-in components you own, each publishing a contract that software can read.",
5
5
  "type": "module",
6
6
  "files": [
@@ -10,7 +10,7 @@
10
10
  ]
11
11
  },
12
12
  "source": {
13
- "libraryVersion": "2.7.2",
13
+ "libraryVersion": "2.7.3",
14
14
  "files": [
15
15
  "core/index.ts",
16
16
  "core/open.ts",
@@ -8,7 +8,7 @@
8
8
  ]
9
9
  },
10
10
  "source": {
11
- "libraryVersion": "2.7.2",
11
+ "libraryVersion": "2.7.3",
12
12
  "files": [
13
13
  "core/index.ts",
14
14
  "core/live.ts",
@@ -9,7 +9,7 @@
9
9
  ]
10
10
  },
11
11
  "source": {
12
- "libraryVersion": "2.7.2",
12
+ "libraryVersion": "2.7.3",
13
13
  "files": [
14
14
  "core/identity.ts",
15
15
  "core/index.ts",
@@ -9,7 +9,7 @@
9
9
  ]
10
10
  },
11
11
  "source": {
12
- "libraryVersion": "2.7.2",
12
+ "libraryVersion": "2.7.3",
13
13
  "files": [
14
14
  "core/count.ts",
15
15
  "core/index.ts",
@@ -8,7 +8,7 @@
8
8
  ]
9
9
  },
10
10
  "source": {
11
- "libraryVersion": "2.7.2",
11
+ "libraryVersion": "2.7.3",
12
12
  "files": [
13
13
  "core/index.ts",
14
14
  "core/trail.ts",
@@ -8,7 +8,7 @@
8
8
  ]
9
9
  },
10
10
  "source": {
11
- "libraryVersion": "2.7.2",
11
+ "libraryVersion": "2.7.3",
12
12
  "files": [
13
13
  "core/index.ts",
14
14
  "core/state.ts",
@@ -12,7 +12,7 @@
12
12
  ]
13
13
  },
14
14
  "source": {
15
- "libraryVersion": "2.7.2",
15
+ "libraryVersion": "2.7.3",
16
16
  "files": [
17
17
  "core/index.ts",
18
18
  "core/trend.ts",
@@ -10,7 +10,7 @@
10
10
  ]
11
11
  },
12
12
  "source": {
13
- "libraryVersion": "2.7.2",
13
+ "libraryVersion": "2.7.3",
14
14
  "files": [
15
15
  "core/agent.ts",
16
16
  "core/group.ts",
@@ -23,7 +23,7 @@
23
23
  "react/locale.ts",
24
24
  "react/props.ts"
25
25
  ],
26
- "sourceHash": "sha256:864a782462120954cea168efe6df111474541556f8518107677c1098cceaa035"
26
+ "sourceHash": "sha256:3bb3c0d76ef4b8d42f6b3bc0a3cdd203a90a1662175ec62ae40eba49ce7ad423"
27
27
  },
28
28
  "purpose": "A single native checkbox, and CheckboxGroup for several that share a name, with mixed state, a select-all and group-level validation.",
29
29
  "props": [
@@ -5,7 +5,7 @@
5
5
  "intentDomains": [
6
6
  "ActionIntent"
7
7
  ],
8
- "$operationsNote": "Mirrors shared/core/agent/field.ts, which is what the runtime dispatches on. source/components/shared/core/agent/__tests__/field-passport.test.ts fails if the two drift. No `apiMethod`: a field is written through its ref — the element for most of these, a handle for Combobox and DatePicker — and that is a normal case, not a gap.",
8
+ "$operationsNote": "Mirrors shared/core/agent/field.ts, which is what the runtime dispatches on. source/components/shared/core/agent/__tests__/field-passport.test.ts fails if the two drift. No `apiMethod`: a field is written through its ref — the element for most of these, a handle for Combobox and DatePicker — and that is a normal case, not a gap. Inside an <AgentSurface> the component registers itself with these operations; NumberInput and RadioGroup are then written through their own commit path rather than the element.",
9
9
  "operations": {
10
10
  "setValue": {
11
11
  "summary": "Write a value into the field, as though a person had entered it.",
@@ -14,6 +14,7 @@ import {
14
14
  } from "react";
15
15
  import { useControllableState } from "../../shared/react/useControllableState";
16
16
  import { describeField } from "../../shared/core/field";
17
+ import { labelText, useAgentSurfaceField } from "../../shared/react/agentSurfaceContext";
17
18
  import type { CheckboxSize, CheckedState } from "../core/types";
18
19
  import { CheckboxGroupContext } from "./context";
19
20
  import { cx, type CheckboxSlot } from "./props";
@@ -107,6 +108,19 @@ export function Checkbox(props: CheckboxProps) {
107
108
  onCheckedChange?.(next);
108
109
  };
109
110
 
111
+ // Inside an <AgentSurface>, offered to an agent as a yes/no it can set.
112
+ useAgentSurfaceField(() => ({
113
+ contract: {
114
+ component: "Checkbox",
115
+ kind: "boolean",
116
+ label: labelText(label, inputProps["aria-label"], name, value),
117
+ disabled: disabled || group?.disabled,
118
+ required,
119
+ operations: ["setValue", "clear", "focus"],
120
+ },
121
+ target: () => inputRef.current,
122
+ }));
123
+
110
124
  const invalid = Boolean(error) || (group?.invalid ?? false);
111
125
  const describedBy = describeField(id, { extra: describedByProp, description, error });
112
126
 
@@ -9,7 +9,7 @@
9
9
  ]
10
10
  },
11
11
  "source": {
12
- "libraryVersion": "2.7.2",
12
+ "libraryVersion": "2.7.3",
13
13
  "files": [
14
14
  "core/agent.ts",
15
15
  "core/filter.ts",
@@ -24,9 +24,10 @@
24
24
  "react/icons.tsx",
25
25
  "react/locale.ts",
26
26
  "react/props.ts",
27
+ "react/surface.ts",
27
28
  "react/useCombobox.ts"
28
29
  ],
29
- "sourceHash": "sha256:5243c2cc6863f8c2dcd248c2fd46417390fec66a8d5b0d405177a372e9c0566f"
30
+ "sourceHash": "sha256:c8001ea7c65beeed1b50d9f35cb0fcc447d01cea88128dda3d8b3584bd542496"
30
31
  },
31
32
  "purpose": "Select and MultiSelect: a searchable combobox for one or many values, with options in the browser's top layer, optional virtualisation and create-on-type.",
32
33
  "props": [
@@ -7,7 +7,7 @@
7
7
  "Recommendation",
8
8
  "Classification"
9
9
  ],
10
- "$operationsNote": "Mirrors shared/core/agent/field.ts, which is what the runtime dispatches on. source/components/shared/core/agent/__tests__/field-passport.test.ts fails if the two drift. No `apiMethod`: a field is written through its ref — the element for most of these, a handle for Combobox and DatePicker — and that is a normal case, not a gap.",
10
+ "$operationsNote": "Mirrors shared/core/agent/field.ts, which is what the runtime dispatches on. source/components/shared/core/agent/__tests__/field-passport.test.ts fails if the two drift. No `apiMethod`: a field is written through its ref — the element for most of these, a handle for Combobox and DatePicker — and that is a normal case, not a gap. Inside an <AgentSurface> the component registers itself with these operations; NumberInput and RadioGroup are then written through their own commit path rather than the element.",
11
11
  "operations": {
12
12
  "setValue": {
13
13
  "summary": "Write a value into the field, as though a person had entered it.",
@@ -8,6 +8,7 @@ import { Listbox } from "./Listbox";
8
8
  import { defaultComboboxText } from "./locale";
9
9
  import { ComboboxPopover } from "./Popover";
10
10
  import { cx, type ComboboxHandle, type ComboboxSharedProps } from "./props";
11
+ import { useComboboxSurface } from "./surface";
11
12
  import { useCombobox } from "./useCombobox";
12
13
 
13
14
  export interface MultiSelectProps<V extends OptionValue = string> extends ComboboxSharedProps<V> {
@@ -89,6 +90,18 @@ export function MultiSelect<V extends OptionValue = string>(props: MultiSelectPr
89
90
  const visibleTags = hidden > 0 ? selectedOptions.slice(0, maxVisibleTags) : selectedOptions;
90
91
 
91
92
  useImperativeHandle(ref, () => combobox.handle, [combobox.handle]);
93
+ useComboboxSurface({
94
+ component: "MultiSelect",
95
+ label,
96
+ ariaLabel: props["aria-label"],
97
+ name,
98
+ required,
99
+ disabled,
100
+ options,
101
+ mode: props.mode,
102
+ max: max,
103
+ handle: combobox.handle,
104
+ });
92
105
 
93
106
  const selectableVisible = combobox.entries.filter(
94
107
  (entry) => !entry.option.disabled && !combobox.isSelected(entry.option.value),
@@ -8,6 +8,7 @@ import { ChevronIcon, XIcon } from "./icons";
8
8
  import { Listbox } from "./Listbox";
9
9
  import { ComboboxPopover } from "./Popover";
10
10
  import { cx, type ComboboxHandle, type ComboboxSharedProps } from "./props";
11
+ import { useComboboxSurface } from "./surface";
11
12
  import { useCombobox } from "./useCombobox";
12
13
 
13
14
  export interface SelectProps<V extends OptionValue = string> extends ComboboxSharedProps<V> {
@@ -83,6 +84,18 @@ export function Select<V extends OptionValue = string>(props: SelectProps<V>) {
83
84
  const selected = combobox.selectedOptions[0];
84
85
 
85
86
  useImperativeHandle(ref, () => combobox.handle, [combobox.handle]);
87
+ useComboboxSurface({
88
+ component: "Select",
89
+ label,
90
+ ariaLabel: props["aria-label"],
91
+ name,
92
+ required,
93
+ disabled,
94
+ options,
95
+ mode: props.mode,
96
+ max: undefined,
97
+ handle: combobox.handle,
98
+ });
86
99
 
87
100
  return (
88
101
  <div