@ai-matrx/records-ui 0.65.0 → 0.66.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/CHANGELOG.md CHANGED
@@ -1,5 +1,47 @@
1
1
  # Changelog — @ai-matrx/records-ui
2
2
 
3
+ ## 0.66.0
4
+
5
+ **A screen stops talking to a developer.** (lane TAILS-6.)
6
+
7
+ Two sightings on ONE table's rail, watched from the admin seat by the GUIDE lane on 2026-09-21:
8
+
9
+ > Checklists — "Something went wrong at our end" ·
10
+ > `@ai-matrx/records did not recognise SQLSTATE from checklistTemplates`
11
+ >
12
+ > Dashboards — "… bind `openRecords` on `<RecordsUiProvider>` …"
13
+
14
+ Both are the same defect in two coats: a sentence written for whoever maintains the code,
15
+ printed to the person who owns the business.
16
+
17
+ * **`refusalForAPerson` no longer promotes an engineer's hint to the person's remedy.** It
18
+ prefers the door's own hint over its table, which is right — but it assumed every hint came
19
+ from the store, and the machine-identity filter only catches a five-character SQLSTATE.
20
+ New `isEngineerText` / `ENGINEER_TEXT` judge a sentence by SHAPE — a package name, the word
21
+ SQLSTATE, a `<Component>`, a camelCase identifier, a backticked token, "bind X on …" — and
22
+ send it to `forEngineers` wherever it came from. `error.diagnostic` from
23
+ `@ai-matrx/records` 0.47.0 joins the same line.
24
+ * **Titles and remedies for `unreachable`, `needs_approval`, `stale_read` and `timed_out`,**
25
+ which had none and fell through to "Something went wrong at our end". A call that never
26
+ reached the store now says so and says nothing was changed.
27
+ * **Every unbound-port sentence rewritten for the person it is shown to** — the nine of them,
28
+ not the two that were caught: `NO_OPEN_RECORDS_REASON`, `NO_REASK_REASON`,
29
+ `NO_SAVED_VIEWS_REASON`, `NO_MEMBERS_REASON`, `NO_SHARE_REASON`, `NO_UPLOAD_REASON`,
30
+ `NO_CHAT_REASON`, `NO_ENRICH_REASON`, `IN_MEMORY_QUEUE_REASON`. Each still says what is
31
+ missing and now says what to do instead; the instruction for whoever maintains the app moved
32
+ into the TSDoc above it, where it always belonged.
33
+ * **`TablePage` gains `activeGroupField` and `cameFrom`,** so a host CAN bind `openRecords`.
34
+ The store has no door that returns the rows behind an aggregate filter, so a click cannot
35
+ promise a narrowed list — what it can do is open this table's board with its columns set to
36
+ the field the chart grouped by, and `cameFromLine` says plainly that this is every record
37
+ grouped that way rather than only the ones the number counted.
38
+ * **Guard:** `src/no-developer-sentences.test.tsx` — shown failing on the pre-fix bytes with
39
+ the exact sentence the GUIDE lane read, then passing. It needs no server.
40
+
41
+ **Consumer action:** bind `openRecords` on your `RecordsUiProvider` and pass `activeGroupField`
42
+ / `cameFrom` through from the address, or leave it unbound and the canvas says so in plain
43
+ English. `matrx-frontend`'s `/data-v2/[tableId]` route is the worked example.
44
+
3
45
  ## 0.65.0
4
46
 
5
47
  **A condition about a relation column stops asking a person to type a uuid.**
@@ -37,6 +79,16 @@ version somebody else's tag already names.*
37
79
 
38
80
  ## 0.64.0
39
81
 
82
+ > 🚨 **0.64.0 NEVER PUBLISHED, AND EVERYTHING IN IT SHIPPED INSIDE 0.65.0.** Its publish run
83
+ > (35638062219) was **cancelled — not failed** — because `npm/records/*` and `npm/records-ui/*`
84
+ > share ONE concurrency slot that keeps only the newest pending run, and `npm/records-ui/v0.65.0`
85
+ > displaced it. Nothing was lost: 0.65.0 is built from `origin/main`, which contains this
86
+ > commit, and the tarball carries `fieldKindFor`, `coerceTypedAnswer`, `valueFromPastedText`
87
+ > and `enumLabels` — verified by `npm pack`, not by version number. `npm view` goes
88
+ > 0.63.0 → 0.65.0 with no 0.64.0 in between. The entry is kept as written so the work is
89
+ > still findable by what it did. *(Recorded by lane RELATION-DISPLAY-2, whose tag did the
90
+ > displacing.)*
91
+
40
92
  **The paste and the editor stop owning their own opinion of a column, and the capture sheet
41
93
  stops minting NaN.**
42
94
 
package/dist/index.cjs CHANGED
@@ -69,6 +69,7 @@ __export(src_exports, {
69
69
  DocRender: () => DocRender,
70
70
  DocTemplate: () => DocTemplate,
71
71
  EMPTY_PRESENTATION: () => EMPTY_PRESENTATION,
72
+ ENGINEER_TEXT: () => ENGINEER_TEXT,
72
73
  EmbedFrame: () => EmbedFrame,
73
74
  EnrichBadge: () => EnrichBadge,
74
75
  EnrichPanel: () => EnrichPanel,
@@ -192,6 +193,7 @@ __export(src_exports, {
192
193
  blockFromSpec: () => blockFromSpec,
193
194
  bodyForReading: () => bodyForReading,
194
195
  bodyFromKeys: () => bodyFromKeys,
196
+ cameFromLine: () => cameFromLine,
195
197
  cellState: () => cellState,
196
198
  chooseSurface: () => chooseSurface,
197
199
  colorFromTheValue: () => colorFromTheValue,
@@ -223,6 +225,7 @@ __export(src_exports, {
223
225
  hintIsMachineIdentity: () => hintIsMachineIdentity,
224
226
  humanize: () => humanize,
225
227
  idsOf: () => idsOf,
228
+ isEngineerText: () => isEngineerText,
226
229
  isId: () => isId,
227
230
  isMachineIdentity: () => isMachineIdentity,
228
231
  isPlainFieldType: () => isPlainFieldType,
@@ -373,12 +376,12 @@ var NO_RIGHTS = tableRights(NOT_ANSWERED_YET);
373
376
  function tableRightsAt(level) {
374
377
  return tableRights(whatYouMayDo(level, true));
375
378
  }
376
- var NO_SAVED_VIEWS_REASON = "No saved-view port is bound, so there is nothing here to subscribe to. A subscription points at a `platform.saved_view`, which the record store has no door onto \u2014 bind `savedViews` on <RecordsUiProvider> with your host's own list and the picker appears.";
377
- var NO_MEMBERS_REASON = "No membership port is bound, so this question cannot offer anybody. A person field points at the kernel Person Table, and who is in this organization is the platform's answer rather than the record store's \u2014 bind `members` on <RecordsUiProvider> with your host's own roster and the picker appears.";
378
- var NO_SHARE_REASON = "No share dialog is bound, so this screen offers no Share button rather than one that opens nothing. Bind `share` on <RecordsUiProvider> with the app's own sharing dialog \u2014 AI Matrx has exactly one (features/sharing), and this package deliberately does not ship a second.";
379
- var NO_OPEN_RECORDS_REASON = "These numbers do not click through yet. Every one of them is a count of real records, and where this app puts its grid is the host's routing rather than this package's \u2014 bind `openRecords` on <RecordsUiProvider> and a bar, a slice or a row opens the records it counted.";
380
- var NO_REASK_REASON = "Asking for a change in words needs an agent, and this package deliberately reaches one through the host rather than calling a model itself. Bind `onReask` on <RecordsUiProvider> \u2014 it drives `dashboard_propose` on the server's `records` tool with this dashboard's id \u2014 and this box works.";
381
- var NO_UPLOAD_REASON = "No file store is bound, so this question cannot take a file. Bind `upload` on <RecordsUiProvider> with your host's file service and the chooser appears.";
379
+ var NO_SAVED_VIEWS_REASON = "There are no saved views here yet, so there is nothing to be told about. Save a view of this table first and it will be offered here.";
380
+ var NO_MEMBERS_REASON = "This app cannot offer the people in your organization on this screen yet, so there is nobody to pick. Type the person's name into a text field for now, and tell us \u2014 it is on our list, not yours.";
381
+ var NO_SHARE_REASON = "Sharing is not available on this screen, so there is no Share button here rather than one that opens nothing. Share this table from the tables list instead.";
382
+ var NO_OPEN_RECORDS_REASON = "These numbers do not click through yet. Every one of them is a count of real records \u2014 open the records view and group it by the same field to see them. It is on our list, not yours.";
383
+ var NO_REASK_REASON = "Changing this dashboard by describing it is not switched on here yet, so this box is off rather than one that goes nowhere. Add and remove the blocks by hand above. It is on our list, not yours.";
384
+ var NO_UPLOAD_REASON = "This screen cannot take a file yet, so there is no chooser here rather than one that loses what you pick. Put a link to the file in a text field for now. It is on our list, not yours.";
382
385
  var HostContext = (0, import_react.createContext)(null);
383
386
  function RecordsUiProvider({ value, children }) {
384
387
  const bound = (0, import_react.useMemo)(() => value, [value]);
@@ -453,6 +456,10 @@ var TITLE = {
453
456
  not_supported: "That cannot be done here",
454
457
  store_limit: "This has got as big as it can",
455
458
  store_not_served: "This organization does not keep its data here",
459
+ unreachable: "We could not reach your data",
460
+ needs_approval: "This is waiting on someone's approval",
461
+ stale_read: "Your view of this is a moment behind",
462
+ timed_out: "That took too long to answer",
456
463
  internal: "Something went wrong at our end"
457
464
  };
458
465
  var REMEDY = {
@@ -467,8 +474,35 @@ var REMEDY = {
467
474
  not_supported: "There is another way to do this \u2014 ask, and we will point you at it.",
468
475
  store_limit: "Split it up, or ask us to raise the ceiling for your organization.",
469
476
  store_not_served: "An administrator of this organization turns the record store on from the data settings screen.",
477
+ // 🚨 NOTHING WAS WRITTEN AND NOTHING WAS REFUSED (lane TAILS-6). The call
478
+ // never arrived, so "try again" is the honest instruction rather than a
479
+ // consolation — and saying nothing changed is the half a person actually
480
+ // worries about.
481
+ unreachable: "Nothing was changed. Check your connection and open this again.",
482
+ needs_approval: "Nothing was changed yet. The people who can approve it have been asked.",
483
+ stale_read: "Open this again in a moment and it will be up to date.",
484
+ timed_out: "Nothing was changed. Try a smaller page, or try again in a moment.",
470
485
  internal: "Nothing you did caused this. Try again, and tell us if it keeps happening."
471
486
  };
487
+ var ENGINEER_TEXT = [
488
+ /@ai-matrx\//,
489
+ /\bSQLSTATE\b/i,
490
+ /\bPostgREST\b/i,
491
+ /\bIndexedDB\b/i,
492
+ // `<RecordsUiProvider>` — a component, on a screen.
493
+ /<\/?[A-Z][A-Za-z0-9]*\s*\/?>/,
494
+ // `savedViews`, `openRecords`, `onReask` — an identifier, however it is
495
+ // quoted. English does not write camelCase; code does.
496
+ /\b[a-z][a-z0-9]*[A-Z][A-Za-z0-9]*\b/,
497
+ // A backticked token is a thing you type into an editor, not a thing you do.
498
+ /`[A-Za-z_][A-Za-z0-9_.]*`/,
499
+ // "bind X on Y", "pass X", "import X" — an instruction to whoever maintains
500
+ // the app, arriving on the screen of somebody who cannot act on it.
501
+ /\bbind\s+[`'"A-Za-z]/i
502
+ ];
503
+ function isEngineerText(text) {
504
+ return ENGINEER_TEXT.some((shape) => shape.test(text));
505
+ }
472
506
  function withoutMachineClauses(sentence2) {
473
507
  const dropped = [];
474
508
  const footnotes = [];
@@ -506,11 +540,17 @@ function refusalForAPerson(error) {
506
540
  const message = plainSentence(String(error.message ?? ""));
507
541
  const hint = plainSentence(String(error.hint ?? ""));
508
542
  const title = TITLE[code] ?? "The store refused this";
509
- const remedy = hint.kept ?? REMEDY[code] ?? REMEDY["internal"];
510
- const said = (message.kept ?? "").trim();
543
+ const hintIsForEngineers = isEngineerText(String(error.hint ?? ""));
544
+ const remedy = (hintIsForEngineers ? null : hint.kept) ?? REMEDY[code] ?? REMEDY["internal"];
545
+ const messageIsForEngineers = isEngineerText(String(error.message ?? ""));
546
+ const said = (messageIsForEngineers ? "" : message.kept ?? "").trim();
511
547
  const forEngineers = [
512
548
  error.sqlstate ? `SQLSTATE ${error.sqlstate}.` : null,
513
- error.hint && hint.kept !== error.hint ? `Hint: ${error.hint}` : null,
549
+ // The client's own note about a class it did not recognise, or a call that
550
+ // never reached the store. It is rendered out of sight, never as a sentence.
551
+ error.diagnostic ? error.diagnostic : null,
552
+ messageIsForEngineers ? `Message: ${error.message}` : null,
553
+ error.hint && (hintIsForEngineers || hint.kept !== error.hint) ? `Hint: ${error.hint}` : null,
514
554
  message.dropped.length > 0 || hint.dropped.length > 0 ? `Dropped from the person's view: ${[...message.dropped, ...hint.dropped].join(" | ")}` : null
515
555
  ].filter(Boolean).join(" ");
516
556
  return {
@@ -2704,7 +2744,7 @@ var import_react17 = require("react");
2704
2744
  var import_react18 = require("@ai-matrx/records/react");
2705
2745
  var import_design_system8 = require("@ai-matrx/design-system");
2706
2746
  var import_jsx_runtime11 = require("react/jsx-runtime");
2707
- var NO_ENRICH_REASON = "No enrich port is bound, so this panel does not offer to fill a column it cannot fill. The value has to be written by the SERVER with actor `agent` on behalf of you \u2014 a browser that stamped itself `agent` would be a browser claiming to be one \u2014 so bind `enrich` on <RecordsUiProvider> with a call to the server's own `records` tool (action `enrich_propose`). Everything else on this panel works without it.";
2747
+ var NO_ENRICH_REASON = "Having an agent fill this column is not switched on here yet, so this panel does not offer it rather than offering something that cannot happen. Everything else on this panel works. It is on our list, not yours.";
2708
2748
  function cellState(cell) {
2709
2749
  if (!cell || !cell.agent_owned) return "none";
2710
2750
  if (cell.pinned) return "yours";
@@ -6011,7 +6051,7 @@ var import_react36 = require("react");
6011
6051
  var import_react37 = require("@ai-matrx/records/react");
6012
6052
  var import_design_system18 = require("@ai-matrx/design-system");
6013
6053
  var import_jsx_runtime21 = require("react/jsx-runtime");
6014
- var NO_CHAT_REASON = "No chat port is bound, so this panel is absent rather than showing a composer that goes nowhere. Bind `chat` on <RecordsUiProvider> with your host's own chat surface \u2014 in AI Matrx that is `AgentConversationColumn`, launched through a Mandate \u2014 and this panel becomes that surface with this record as its context. This package deliberately ships no second chat UI.";
6054
+ var NO_CHAT_REASON = "Chatting about this record is not switched on here yet, so this panel is empty rather than showing a box that goes nowhere. It is on our list, not yours.";
6015
6055
  function RecordChat({ tableId, recordId, className }) {
6016
6056
  const client = (0, import_react37.useRecordsClient)();
6017
6057
  const host = useRecordsUi();
@@ -13949,7 +13989,7 @@ function CaptureRun({ sheetId, face: given, className }) {
13949
13989
  // src/CaptureSheet.tsx
13950
13990
  var import_jsx_runtime52 = require("react/jsx-runtime");
13951
13991
  var CAPTURE_MODES = ["reading", "photo", "voice"];
13952
- var IN_MEMORY_QUEUE_REASON = "No capture queue is bound, so anything that cannot be sent right now is held in this page's memory and is lost if the page closes. Bind `captureQueue` on <RecordsUiProvider> with your host's own durable store (IndexedDB in a browser) and a queued capture survives the tab, the reload and the flight. It is said here rather than discovered later.";
13992
+ var IN_MEMORY_QUEUE_REASON = "Anything you capture while offline is held only in this page until it can be sent, and it is lost if you close the tab. Keep this page open until you are back on a connection. It is said here rather than discovered later.";
13953
13993
  function mintClientKey() {
13954
13994
  const c = globalThis.crypto;
13955
13995
  if (typeof c?.randomUUID === "function") return `capture:${c.randomUUID()}`;
@@ -14825,6 +14865,10 @@ var import_react111 = require("react");
14825
14865
  var import_react112 = require("@ai-matrx/records/react");
14826
14866
  var import_design_system54 = require("@ai-matrx/design-system");
14827
14867
  var import_jsx_runtime58 = require("react/jsx-runtime");
14868
+ function cameFromLine(label, groupField) {
14869
+ const grouped = groupField ? `grouped into the same columns the chart used` : `for this table`;
14870
+ return `You came here from \u201C${label}\u201D. This is every record ${grouped} \u2014 not only the ones that number counted \u2014 so the column you clicked is here with the rest beside it.`;
14871
+ }
14828
14872
  var TABLE_NOT_REACHABLE = "This table is not in the organization you are working in, so there is nothing here to show. Switch to the organization that owns it and open it again \u2014 or it may have been deleted.";
14829
14873
  var DEFAULT_VIEW_NAME = "All records";
14830
14874
  var VIEW_NOT_SAVED_YET = "This table has no saved view yet, so the layout you pick here is not being kept. Save one from the bar above and it is remembered.";
@@ -14868,6 +14912,8 @@ function TablePage({
14868
14912
  activeRecordId,
14869
14913
  activeView,
14870
14914
  onViewChanged,
14915
+ activeGroupField,
14916
+ cameFrom,
14871
14917
  className
14872
14918
  }) {
14873
14919
  const client = (0, import_react112.useRecordsClient)();
@@ -15061,10 +15107,24 @@ function TablePage({
15061
15107
  ] }),
15062
15108
  main === "dashboards" ? /* @__PURE__ */ (0, import_jsx_runtime58.jsx)(DashboardCanvas, { tableId, activeDashboardId: activeDashboardId ?? null }) : /* @__PURE__ */ (0, import_jsx_runtime58.jsxs)(import_jsx_runtime58.Fragment, { children: [
15063
15109
  opened.unknown ? /* @__PURE__ */ (0, import_jsx_runtime58.jsx)("p", { className: "rounded-md border border-amber-600/40 bg-amber-500/5 px-3 py-2 text-xs leading-relaxed text-amber-700 dark:border-amber-400/40 dark:text-amber-300", children: unknownViewLine(opened.unknown) }) : null,
15110
+ cameFrom ? /* @__PURE__ */ (0, import_jsx_runtime58.jsx)(
15111
+ "p",
15112
+ {
15113
+ "data-testid": "came-from-a-number",
15114
+ className: "rounded-md border px-3 py-2 text-xs leading-relaxed text-muted-foreground",
15115
+ children: cameFromLine(cameFrom, activeGroupField ?? null)
15116
+ }
15117
+ ) : null,
15064
15118
  /* @__PURE__ */ (0, import_jsx_runtime58.jsx)(
15065
15119
  ViewSwitcher,
15066
15120
  {
15067
- view: { ...view ?? defaultView(tableId), layout: shownLayout },
15121
+ view: {
15122
+ ...view ?? defaultView(tableId),
15123
+ layout: shownLayout,
15124
+ // The address's field wins for this visit; silence keeps the
15125
+ // saved view's own choice, exactly as it always did.
15126
+ ...activeGroupField ? { groupField: activeGroupField } : {}
15127
+ },
15068
15128
  pageSize,
15069
15129
  onLayoutChange: (layout) => {
15070
15130
  setLayoutFromLink(layout);