@coffer-org/plugin-webchat 4.0.0 → 6.0.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.
@@ -1,5 +1,5 @@
1
1
  import * as __fedReact from "react";
2
- import { Dialog, DialogContent, getPluginRoutes, getSiteLinks, getSiteShelves, uploadFile, useMinWidth } from "@coffer-org/web-ui";
2
+ import { Dialog, DialogContent, classifySiteHref, uploadFile, useMinWidth } from "@coffer-org/web-ui";
3
3
  import { createContext, createElement, forwardRef, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
4
4
  import { useLocation, useNavigate } from "react-router-dom";
5
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -23722,54 +23722,6 @@ function remarkGfm(options) {
23722
23722
  toMarkdownExtensions.push(gfmToMarkdown(settings));
23723
23723
  }
23724
23724
  //#endregion
23725
- //#region src/ui/internal-link.ts
23726
- var SEGMENT = /^[A-Za-z0-9._~%-]+$/;
23727
- /** Returns captured ':param' values on a match, or null. '*' is legal only as the
23728
- * last pattern segment, so this branch may return immediately without reading got to the end. */
23729
- function matchesPattern(pathname, pattern) {
23730
- const got = pathname.split("/").filter(Boolean);
23731
- const want = pattern.split("/").filter(Boolean);
23732
- const captured = {};
23733
- for (let i = 0; i < want.length; i++) {
23734
- const w = want[i];
23735
- if (w === "*") return got.length >= i ? captured : null;
23736
- const g = got[i];
23737
- if (g === void 0) return null;
23738
- if (w.startsWith(":")) {
23739
- if (!SEGMENT.test(g)) return null;
23740
- captured[w.slice(1)] = g;
23741
- } else if (w !== g) return null;
23742
- }
23743
- return got.length === want.length ? captured : null;
23744
- }
23745
- function classifyHref(href, origin, routes, pairs = []) {
23746
- const raw = href.trim();
23747
- if (!raw || raw.startsWith("//")) return { kind: "text" };
23748
- if (!raw.startsWith("/") && !/^https?:\/\//i.test(raw)) return { kind: "text" };
23749
- let url;
23750
- try {
23751
- url = new URL(raw, origin);
23752
- } catch {
23753
- return { kind: "text" };
23754
- }
23755
- if (url.origin !== origin) return {
23756
- kind: "external",
23757
- href: url.href
23758
- };
23759
- for (const r of routes) {
23760
- const captured = matchesPattern(url.pathname, r.pattern);
23761
- if (!captured) continue;
23762
- if (pairs.length && captured["library"] !== void 0 && captured["shelf"] !== void 0) {
23763
- if (!pairs.some((p) => p.library === captured["library"] && p.shelf === captured["shelf"])) return { kind: "text" };
23764
- }
23765
- return {
23766
- kind: "internal",
23767
- to: `${url.pathname}${url.search}${url.hash}`
23768
- };
23769
- }
23770
- return { kind: "text" };
23771
- }
23772
- //#endregion
23773
23725
  //#region src/ui/chat-nav.tsx
23774
23726
  /** What to do with the widget when the user follows a link from a response.
23775
23727
  * ChatWidget supplies the callback (it owns mode/open), avoiding a prop
@@ -23811,8 +23763,7 @@ function ChatTable(props) {
23811
23763
  function ChatLink({ href, children }) {
23812
23764
  const onNavigate = useChatNav();
23813
23765
  const navigate = useNavigate();
23814
- const routes = [...getSiteLinks(), ...getPluginRoutes().map((r) => ({ pattern: `/${r.path}` }))];
23815
- const kind = classifyHref(href ?? "", window.location.origin, routes, getSiteShelves());
23766
+ const kind = classifySiteHref(href ?? "");
23816
23767
  if (kind.kind === "internal") return /* @__PURE__ */ jsx("a", {
23817
23768
  href: kind.to,
23818
23769
  onClick: (event) => {
@@ -23966,15 +23917,6 @@ function starLadder(rows, multi) {
23966
23917
  }
23967
23918
  return byLevel.every((r) => r !== void 0) ? byLevel : null;
23968
23919
  }
23969
- /** The stars as a picture: `filled` of `max` lit. Same palette as the `f.rating` widget
23970
- * (filled yellow, empty at border colour), so the gesture reads the same in both places. */
23971
- function Stars({ filled, max, size }) {
23972
- return /* @__PURE__ */ jsx(Fragment, { children: Array.from({ length: max }, (_, i) => /* @__PURE__ */ jsx(Star, {
23973
- size,
23974
- "aria-hidden": true,
23975
- className: i < filled ? "shrink-0 fill-yellow-400 text-yellow-400" : "shrink-0 text-border"
23976
- }, i)) });
23977
- }
23978
23920
  /** Flatten the catalog to one list: with a single registered agent the agent
23979
23921
  * level is noise, and with several the agent name simply prefixes its presets. */
23980
23922
  function AgentPicker({ agents, defaultAgentId, selection, onChange }) {
@@ -24001,9 +23943,6 @@ function AgentPicker({ agents, defaultAgentId, selection, onChange }) {
24001
23943
  document.removeEventListener("keydown", onKeyDown);
24002
23944
  };
24003
23945
  }, [open]);
24004
- useEffect(() => {
24005
- if (!open) setPreview(null);
24006
- }, [open]);
24007
23946
  const multi = agents.length > 1;
24008
23947
  const rows = agents.flatMap((a) => a.presets.map((p) => ({
24009
23948
  agentId: a.id,
@@ -24017,73 +23956,58 @@ function AgentPicker({ agents, defaultAgentId, selection, onChange }) {
24017
23956
  if (rows.length === 0) return null;
24018
23957
  const current = rows.find((r) => r.agentId === selection.agentId && r.presetId === selection.presetId) ?? rows.find((r) => r.agentId === defaultAgentId && r.isDefault) ?? rows.find((r) => r.isDefault) ?? rows[0];
24019
23958
  const ladder = starLadder(rows, multi);
24020
- const level = ladder ? ladder.indexOf(current) + 1 : 0;
24021
- const shown = ladder ? preview ?? level : 0;
24022
- const shownRow = ladder ? ladder[shown - 1] : void 0;
24023
- const levelName = (row, n) => `${row.label} · ${t("webchat.level", {
24024
- value: n,
24025
- max: ladder.length
24026
- })}`;
23959
+ if (ladder) {
23960
+ const level = ladder.indexOf(current) + 1;
23961
+ const shown = preview ?? level;
23962
+ const levelName = (row, n) => `${row.label} · ${t("webchat.level", {
23963
+ value: n,
23964
+ max: ladder.length
23965
+ })}`;
23966
+ return /* @__PURE__ */ jsx("ul", {
23967
+ "aria-label": t("webchat.agentPicker"),
23968
+ className: "flex shrink-0 items-center",
23969
+ onPointerLeave: () => setPreview(null),
23970
+ children: ladder.map((r, i) => {
23971
+ const n = i + 1;
23972
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("button", {
23973
+ type: "button",
23974
+ ...r === current ? { "aria-current": true } : {},
23975
+ "aria-label": levelName(r, n),
23976
+ className: "flex rounded p-1 focus-visible:outline-2 focus-visible:outline-accent",
23977
+ onPointerEnter: () => setPreview(n),
23978
+ onFocus: () => setPreview(n),
23979
+ onBlur: () => setPreview(null),
23980
+ onClick: () => onChange({
23981
+ agentId: r.agentId,
23982
+ presetId: r.presetId
23983
+ }),
23984
+ children: /* @__PURE__ */ jsx(Star, {
23985
+ size: 16,
23986
+ "aria-hidden": true,
23987
+ className: n <= shown ? "fill-yellow-400 text-yellow-400" : "text-border"
23988
+ })
23989
+ }) }, `${r.agentId}/${r.presetId}`);
23990
+ })
23991
+ });
23992
+ }
24027
23993
  return /* @__PURE__ */ jsxs("div", {
24028
23994
  className: "relative",
24029
23995
  ref: rootRef,
24030
- children: [/* @__PURE__ */ jsx("button", {
23996
+ children: [/* @__PURE__ */ jsxs("button", {
24031
23997
  ref: triggerRef,
24032
23998
  type: "button",
24033
23999
  "aria-haspopup": "menu",
24034
24000
  "aria-expanded": open,
24035
24001
  className: "flex items-center gap-1 rounded-md px-2 py-1 text-xs text-muted hover:text-text focus-visible:outline-2 focus-visible:outline-accent",
24036
24002
  onClick: () => setOpen((v) => !v),
24037
- ...ladder ? { "aria-label": levelName(current, level) } : {},
24038
- children: ladder ? /* @__PURE__ */ jsx("span", {
24039
- className: "flex items-center gap-0.5",
24040
- children: /* @__PURE__ */ jsx(Stars, {
24041
- filled: level,
24042
- max: ladder.length,
24043
- size: 14
24044
- })
24045
- }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
24003
+ children: [/* @__PURE__ */ jsx("span", {
24046
24004
  className: "max-w-40 truncate",
24047
24005
  children: current.label
24048
24006
  }), /* @__PURE__ */ jsx(ChevronDown, {
24049
24007
  size: 12,
24050
24008
  className: "shrink-0"
24051
- })] })
24052
- }), open && (ladder ? /* @__PURE__ */ jsxs("div", {
24053
- className: "absolute bottom-full left-0 z-10 mb-1 rounded-md border border-border bg-elevated px-2 py-1.5 shadow-lg",
24054
- children: [/* @__PURE__ */ jsx("ul", {
24055
- "aria-label": t("webchat.agentPicker"),
24056
- className: "flex items-center",
24057
- onPointerLeave: () => setPreview(null),
24058
- children: ladder.map((r, i) => {
24059
- const n = i + 1;
24060
- return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("button", {
24061
- type: "button",
24062
- ...r === current ? { "aria-current": true } : {},
24063
- "aria-label": levelName(r, n),
24064
- className: "flex p-0.5 focus-visible:outline-2 focus-visible:outline-accent",
24065
- onPointerEnter: () => setPreview(n),
24066
- onFocus: () => setPreview(n),
24067
- onBlur: () => setPreview(null),
24068
- onClick: () => {
24069
- setOpen(false);
24070
- onChange({
24071
- agentId: r.agentId,
24072
- presetId: r.presetId
24073
- });
24074
- },
24075
- children: /* @__PURE__ */ jsx(Star, {
24076
- size: 20,
24077
- "aria-hidden": true,
24078
- className: n <= shown ? "fill-yellow-400 text-yellow-400" : "text-border"
24079
- })
24080
- }) }, `${r.agentId}/${r.presetId}`);
24081
- })
24082
- }), /* @__PURE__ */ jsx("p", {
24083
- className: "mt-0.5 text-center text-xs text-muted",
24084
- children: shownRow?.hint || shownRow?.label
24085
24009
  })]
24086
- }) : /* @__PURE__ */ jsx("ul", {
24010
+ }), open && /* @__PURE__ */ jsx("ul", {
24087
24011
  "aria-label": t("webchat.agentPicker"),
24088
24012
  className: "absolute bottom-full left-0 z-10 mb-1 max-h-64 w-64 max-w-[calc(100vw-2rem)] overflow-y-auto rounded-md border border-border bg-elevated py-1 shadow-lg",
24089
24013
  children: rows.map((r) => {
@@ -24116,7 +24040,7 @@ function AgentPicker({ agents, defaultAgentId, selection, onChange }) {
24116
24040
  ]
24117
24041
  }) }, `${r.agentId}/${r.presetId}`);
24118
24042
  })
24119
- }))]
24043
+ })]
24120
24044
  });
24121
24045
  }
24122
24046
  //#endregion
@@ -24363,12 +24287,31 @@ function ChatDropSurface({ children, onFiles, className }) {
24363
24287
  })]
24364
24288
  });
24365
24289
  }
24366
- /** What to do with the widget after following a link from a response. On desktop a 380px
24367
- * panel does not obstruct the page (a darkened modal does, so it collapses into it);
24368
- * on narrow screens the chat remains expanded so the user does not lose the active
24369
- * conversation after an in-app navigation. */
24370
- function nextModeAfterNav(wideNav) {
24371
- return wideNav ? "panel" : "modal";
24290
+ /** The states the VIEWPORT offers, ordered from least to most expanded. `panel` the 380px
24291
+ * floating window — exists only where it does not obstruct the page: on a narrow screen the
24292
+ * widget has exactly two states, the bubble and the full sheet. Every mode transition is a
24293
+ * move along this ladder, so no call site can put the widget into a mode its screen lacks. */
24294
+ function chatModes(wide) {
24295
+ return wide ? [
24296
+ "bubble",
24297
+ "panel",
24298
+ "modal"
24299
+ ] : ["bubble", "modal"];
24300
+ }
24301
+ /** Where the widget rests once it is opened — expanded, but as little as the viewport allows.
24302
+ * Also where it returns after following a link from a response: on a wide screen the panel
24303
+ * does not obstruct the page the user just navigated to, while on a narrow one the sheet is
24304
+ * the only visible state, so the active conversation survives the navigation either way. */
24305
+ function restingMode(wide) {
24306
+ return chatModes(wide)[1];
24307
+ }
24308
+ /** One step along the ladder, clamped at both ends: `-1` is what collapse, Esc and a dialog
24309
+ * dismiss do, `+1` is expand. A mode this viewport no longer offers is re-homed by the effect
24310
+ * in the widget, not by stepping — the step always returns a mode from the current ladder. */
24311
+ function stepMode(mode, wide, delta) {
24312
+ const ladder = chatModes(wide);
24313
+ const next = ladder.indexOf(mode) + delta;
24314
+ return ladder[Math.min(ladder.length - 1, Math.max(0, next))];
24372
24315
  }
24373
24316
  function ChatWidget() {
24374
24317
  const { t } = useTranslation();
@@ -24382,20 +24325,22 @@ function ChatWidget() {
24382
24325
  const [uploadError, setUploadError] = useState(null);
24383
24326
  const [retryFiles, setRetryFiles] = useState([]);
24384
24327
  const wide = useMinWidth("sm", 640);
24385
- const wideNav = useMinWidth("nav", 768);
24386
24328
  const onChatNavigate = useCallback(() => {
24387
- setMode(nextModeAfterNav(wideNav));
24388
- }, [wideNav]);
24329
+ setMode(restingMode(wide));
24330
+ }, [wide]);
24389
24331
  const expandBtnRef = useRef(null);
24390
24332
  const prevModeRef = useRef("bubble");
24391
24333
  useEffect(() => {
24392
24334
  if (mode !== "modal") return;
24393
24335
  const onKey = (e) => {
24394
- if (e.key === "Escape") setMode("panel");
24336
+ if (e.key === "Escape") setMode(stepMode("modal", wide, -1));
24395
24337
  };
24396
24338
  window.addEventListener("keydown", onKey);
24397
24339
  return () => window.removeEventListener("keydown", onKey);
24398
- }, [mode]);
24340
+ }, [mode, wide]);
24341
+ useEffect(() => {
24342
+ if (!chatModes(wide).includes(mode)) setMode(restingMode(wide));
24343
+ }, [mode, wide]);
24399
24344
  useEffect(() => {
24400
24345
  if (mode === "panel" && prevModeRef.current === "modal") expandBtnRef.current?.focus();
24401
24346
  if (mode !== "modal") setThreadsOpen(false);
@@ -24498,11 +24443,11 @@ function ChatWidget() {
24498
24443
  onClick: startNew,
24499
24444
  children: /* @__PURE__ */ jsx(Plus, { size: 16 })
24500
24445
  }),
24501
- /* @__PURE__ */ jsx("button", {
24446
+ chatModes(wide).includes("panel") && /* @__PURE__ */ jsx("button", {
24502
24447
  ref: expandBtnRef,
24503
24448
  "aria-label": mode === "modal" ? t("webchat.collapse") : t("webchat.expand"),
24504
24449
  className: "rounded p-1 hover:bg-elevated",
24505
- onClick: () => setMode(mode === "modal" ? "panel" : "modal"),
24450
+ onClick: () => setMode(stepMode(mode, wide, mode === "modal" ? -1 : 1)),
24506
24451
  children: mode === "modal" ? /* @__PURE__ */ jsx(Minimize2, { size: 16 }) : /* @__PURE__ */ jsx(Maximize2, { size: 16 })
24507
24452
  }),
24508
24453
  /* @__PURE__ */ jsx("button", {
@@ -24558,7 +24503,7 @@ function ChatWidget() {
24558
24503
  });
24559
24504
  if (mode === "bubble") return /* @__PURE__ */ jsx("button", {
24560
24505
  "aria-label": t("webchat.open"),
24561
- onClick: () => setMode(wide ? "panel" : "modal"),
24506
+ onClick: () => setMode(restingMode(wide)),
24562
24507
  className: "fixed bottom-20 right-4 z-40 grid h-14 w-14 place-items-center rounded-full bg-accent text-white shadow-lg nav:bottom-4",
24563
24508
  children: /* @__PURE__ */ jsx(MessageCircle, { size: 24 })
24564
24509
  });
@@ -24570,7 +24515,7 @@ function ChatWidget() {
24570
24515
  return /* @__PURE__ */ jsxs(Dialog, {
24571
24516
  open: true,
24572
24517
  modal: false,
24573
- onOpenChange: (open) => !open && setMode("panel"),
24518
+ onOpenChange: (open) => !open && setMode(stepMode("modal", wide, -1)),
24574
24519
  children: [/* @__PURE__ */ jsx("div", {
24575
24520
  "data-chat-backdrop": true,
24576
24521
  "aria-hidden": "true",
@@ -24605,4 +24550,4 @@ function ChatWidget() {
24605
24550
  });
24606
24551
  }
24607
24552
  //#endregion
24608
- export { ChatWidget, ChatWidget as default, nextModeAfterNav };
24553
+ export { ChatWidget, ChatWidget as default, chatModes, restingMode, stepMode };
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ export default definePlugin({
12
12
  label: 'webchat.settings.reasoning_display',
13
13
  default: 'live',
14
14
  strict: true,
15
- view: { noSearch: true },
15
+ noSearch: true,
16
16
  options: [
17
17
  { value: 'live', title: 'webchat.settings.reasoning_display_live' },
18
18
  { value: 'off', title: 'webchat.settings.reasoning_display_off' },
@@ -1 +1,2 @@
1
1
  export declare const WEB_FORMAT: string;
2
+ export declare function webChannelSystem(): Promise<string>;
@@ -1,3 +1,4 @@
1
+ import { frontendInstructions } from '@coffer-org/server/frontend-agent';
1
2
  export const WEB_FORMAT = [
2
3
  'CRITICAL OUTPUT FORMAT — MARKDOWN ONLY. Format the reply strictly as Markdown (GFM). NEVER emit raw HTML tags — they are stripped before rendering and their content may be lost.',
3
4
  'Inline: **bold**, *italic*, `inline code`, ~~strikethrough~~. Links: [text](https://…).',
@@ -8,3 +9,7 @@ export const WEB_FORMAT = [
8
9
  'The reply is read in a narrow chat panel (~360px) — keep it short and skimmable: short paragraphs, lists over prose, narrow tables.',
9
10
  'Use Unicode symbols and emoji to structure replies: arrows (→), status marks (✅ ⚠️ ❌ ⏳), topic emoji (📅 💰 🏥 🛒 🌱 …).',
10
11
  ].join('\n');
12
+ export async function webChannelSystem() {
13
+ const site = await frontendInstructions('');
14
+ return [WEB_FORMAT, site].filter(Boolean).join('\n\n');
15
+ }
@@ -3,7 +3,7 @@ import { handleIncoming, liveAgentId } from '@coffer-org/server/orchestrator';
3
3
  import { chatIdFor, recordUser, buildChain, history, readSelectionForTurn } from "./chain-store.js";
4
4
  import { policy } from "./config.js";
5
5
  import { makeStreamingConnector } from "./connector.js";
6
- import { WEB_FORMAT } from "./format.js";
6
+ import { webChannelSystem } from "./format.js";
7
7
  import { loadReasoningDisplay } from "./settings.js";
8
8
  export function pageContext(ctx) {
9
9
  if (typeof ctx !== 'object' || ctx === null)
@@ -47,11 +47,11 @@ export async function sendAction(body, ctx, deps = {}) {
47
47
  emit: ctx.emit,
48
48
  display: await loadReasoningDisplay(),
49
49
  });
50
- const volatileSystem = pageContext(body['context']);
50
+ const turnContext = pageContext(body['context']);
51
51
  await doHandle(connector, {
52
52
  connectorId: 'webchat',
53
- channelSystem: WEB_FORMAT,
54
- ...(volatileSystem ? { volatileSystem } : {}),
53
+ channelSystem: await webChannelSystem(),
54
+ ...(turnContext ? { turnContext } : {}),
55
55
  ...(agentId ? { agentId } : {}),
56
56
  ...(selection.presetId ? { presetId: selection.presetId } : {}),
57
57
  chatId,
package/dist/schema.js CHANGED
@@ -4082,6 +4082,23 @@ function typeErr(code = "invalid_type") {
4082
4082
  function reqTypeErr() {
4083
4083
  return { error: (iss) => iss.code === "invalid_type" ? iss.input === void 0 ? vmsg("required") : vmsg("invalid_type") : void 0 };
4084
4084
  }
4085
+ /**
4086
+ * A string schema whose CONTENT checks only speak once the value is known to be a string.
4087
+ *
4088
+ * zod's length checks are not type-guarded: `min`/`max` read `input.length`, and an ARRAY has
4089
+ * one. So `z.string().min(1)` answers `[]` with `invalid_type` AND `too_small` — two messages
4090
+ * about a single wrongness, both of which reach the user, since `mutate.ts` and `extend-io.ts`
4091
+ * turn every issue into its own `ValidationError`. (A number answers with `invalid_type`
4092
+ * alone, having no `length` to read — so the redundancy was array-shaped and invisible until
4093
+ * the golden matrix started recording the whole issue list instead of `issues[0]`.)
4094
+ *
4095
+ * Piping puts the type question first and alone: `add` never runs on a non-string. Pass the
4096
+ * content checks as a builder rather than chaining them onto the result, because a `ZodPipe`
4097
+ * has no `.min`/`.max`/`.regex` to chain.
4098
+ */
4099
+ function stringContent(add) {
4100
+ return string$1(reqTypeErr()).pipe(add(string$1()));
4101
+ }
4085
4102
  /** Parse a JSON string, else return the native value (object/array) as-is.
4086
4103
  * Tolerant input for native form-state AND legacy JSON-string payloads.
4087
4104
  * A string that fails JSON.parse is returned unchanged — callers detect
@@ -4337,6 +4354,13 @@ function wrapKey(key, opts, meta) {
4337
4354
  editor: opts.editor
4338
4355
  }
4339
4356
  };
4357
+ if (opts.activate) m = {
4358
+ ...m,
4359
+ hints: {
4360
+ ...m.hints,
4361
+ activate: opts.activate
4362
+ }
4363
+ };
4340
4364
  if (opts.span !== void 0) m = {
4341
4365
  ...m,
4342
4366
  hints: {
@@ -4709,18 +4733,37 @@ var UNITS_MAP = {
4709
4733
  label: "core.units.ppm"
4710
4734
  }]
4711
4735
  };
4736
+ /**
4737
+ * Resolve a unit spec — a named scale or an explicit option list — to its options.
4738
+ *
4739
+ * Throws rather than returning `undefined`, because both ways of reaching `undefined` here
4740
+ * produce a field that CONSTRUCTS and then crashes when someone tries to save through it:
4741
+ * `measured`'s unit role closes over these options and calls `.some(…)` on them inside its
4742
+ * refine, so a missing spec or a typo'd scale name turns a user's save into a
4743
+ * `Cannot read properties of undefined` instead of a validation message. The golden matrix
4744
+ * recorded exactly that for `f.measured({})`.
4745
+ *
4746
+ * A declaration error belongs at declaration time, where the plugin author sees it and the
4747
+ * message can name the scale they meant — the same reason the host-services seam fails at
4748
+ * registration rather than at first render.
4749
+ */
4712
4750
  function resolveUnits(u) {
4713
- return typeof u === "string" ? UNITS_MAP[u] : u;
4751
+ if (typeof u === "string") {
4752
+ const known = UNITS_MAP[u];
4753
+ if (!known) throw new Error(`units: unknown scale '${u}' (known: ${Object.keys(UNITS_MAP).sort().join(", ")})`);
4754
+ return known;
4755
+ }
4756
+ if (!Array.isArray(u)) throw new Error("units: a unit spec is required — a named scale, or an explicit list of options");
4757
+ return u;
4714
4758
  }
4715
4759
  //#endregion
4716
4760
  //#region ../sdk/src/fields/normalize.ts
4717
4761
  function normalizeOpts(rawIn) {
4718
4762
  const raw = rawIn;
4763
+ if (raw["view"] !== void 0) throw new Error("[normalizeOpts] `view` is gone — presentational options live in `ui: {}` (see CLAUDE.md § field author options and the field-types skill).");
4764
+ if (raw.ui?.["agent"] !== void 0) throw new Error("[normalizeOpts] `agent` is not presentation — declare it at the top level of the field call, not in `ui: {}`.");
4719
4765
  const r = raw.rules ?? {};
4720
- const v = {
4721
- ...raw.view ?? {},
4722
- ...raw.ui ?? {}
4723
- };
4766
+ const v = raw.ui ?? {};
4724
4767
  const config = {};
4725
4768
  if (r.min != null && typeof r.min === "number") config.min = r.min;
4726
4769
  if (r.max != null && typeof r.max === "number") config.max = r.max;
@@ -4729,7 +4772,7 @@ function normalizeOpts(rawIn) {
4729
4772
  if (r.messageKey != null) config.messageKey = r.messageKey;
4730
4773
  if (r.granularity != null) config.granularity = r.granularity;
4731
4774
  if (r.language != null) config.language = r.language;
4732
- const { rules: _r, view: _v, ui: _ui, ...flat } = raw;
4775
+ const { rules: _r, ui: _ui, ...flat } = raw;
4733
4776
  const result = {
4734
4777
  ...flat,
4735
4778
  ...Object.keys(config).length ? { config } : {},
@@ -4744,7 +4787,6 @@ function normalizeOpts(rawIn) {
4744
4787
  step: r.step,
4745
4788
  hidden: v.hidden,
4746
4789
  noEditControl: v.noEditControl,
4747
- noSearch: v.noSearch,
4748
4790
  display: v.display,
4749
4791
  kind: v.kind,
4750
4792
  icon: v.icon,
@@ -4752,7 +4794,12 @@ function normalizeOpts(rawIn) {
4752
4794
  ...v.keyLabel !== void 0 ? { keyLabel: v.keyLabel } : {},
4753
4795
  ...v.valueLabel !== void 0 ? { valueLabel: v.valueLabel } : {},
4754
4796
  compareWith: v.compareWith,
4755
- span: v.span
4797
+ span: v.span,
4798
+ emphasis: v.emphasis,
4799
+ noLabel: v.noLabel,
4800
+ role: v.role,
4801
+ editor: v.editor,
4802
+ activate: v.activate
4756
4803
  };
4757
4804
  return Object.fromEntries(Object.entries(result).filter(([, val]) => val !== void 0));
4758
4805
  }
@@ -5002,7 +5049,7 @@ function internalApiToken(o) {
5002
5049
  label: o.label,
5003
5050
  multiple: true,
5004
5051
  rules: { unique: ["name"] },
5005
- view: { kind: "internalApiToken" },
5052
+ ui: { kind: "internalApiToken" },
5006
5053
  fields: {
5007
5054
  name: string({}),
5008
5055
  token: password({}),
@@ -5028,7 +5075,7 @@ function internalOauthGrants(o) {
5028
5075
  scope: "nest",
5029
5076
  label: o.label,
5030
5077
  multiple: true,
5031
- view: { kind: "internalOauthGrants" },
5078
+ ui: { kind: "internalOauthGrants" },
5032
5079
  fields: {
5033
5080
  clientName: string({}),
5034
5081
  createdAt: string({}),
@@ -5655,10 +5702,8 @@ function country(raw) {
5655
5702
  ...raw,
5656
5703
  options: "countries",
5657
5704
  strict: true,
5658
- view: {
5659
- ...raw.view ?? {},
5660
- noSearch: true
5661
- }
5705
+ ui: { ...raw.ui ?? {} },
5706
+ noSearch: true
5662
5707
  });
5663
5708
  }
5664
5709
  /**
@@ -5682,10 +5727,8 @@ function currency(raw) {
5682
5727
  ...raw,
5683
5728
  options: "currencies",
5684
5729
  strict: true,
5685
- view: {
5686
- ...raw.view ?? {},
5687
- noSearch: true
5688
- }
5730
+ ui: { ...raw.ui ?? {} },
5731
+ noSearch: true
5689
5732
  });
5690
5733
  }
5691
5734
  /**
@@ -5845,7 +5888,7 @@ function bento(opts) {
5845
5888
  label: opts.label,
5846
5889
  icon: opts.icon,
5847
5890
  fields,
5848
- view: { kind: "bento" }
5891
+ ui: { kind: "bento" }
5849
5892
  }),
5850
5893
  view: {
5851
5894
  kind: "bento",
@@ -5874,7 +5917,7 @@ function split(opts) {
5874
5917
  return {
5875
5918
  ...group({
5876
5919
  fields: merge("split", opts.columns[0], opts.columns[1]),
5877
- view: { kind: "split" }
5920
+ ui: { kind: "split" }
5878
5921
  }),
5879
5922
  view: {
5880
5923
  kind: "split",
@@ -5898,7 +5941,7 @@ function grid(opts) {
5898
5941
  label: opts.label,
5899
5942
  icon: opts.icon,
5900
5943
  fields: opts.fields,
5901
- view: { kind: "grid" }
5944
+ ui: { kind: "grid" }
5902
5945
  }),
5903
5946
  view: {
5904
5947
  kind: "grid",
@@ -5923,7 +5966,7 @@ function spread(opts) {
5923
5966
  return {
5924
5967
  ...group({
5925
5968
  fields: merge("spread", visual, quote, opts.facts),
5926
- view: { kind: "spread" }
5969
+ ui: { kind: "spread" }
5927
5970
  }),
5928
5971
  view: {
5929
5972
  kind: "spread",
@@ -5948,7 +5991,7 @@ function aside(opts) {
5948
5991
  return {
5949
5992
  ...group({
5950
5993
  fields: merge("aside", opts.fields, opts.aside),
5951
- view: { kind: "aside" }
5994
+ ui: { kind: "aside" }
5952
5995
  }),
5953
5996
  view: {
5954
5997
  kind: "aside",
@@ -5980,7 +6023,7 @@ function tabs(opts) {
5980
6023
  return {
5981
6024
  ...group({
5982
6025
  fields,
5983
- view: { kind: "tabs" }
6026
+ ui: { kind: "tabs" }
5984
6027
  }),
5985
6028
  view: {
5986
6029
  kind: "tabs",
@@ -6012,7 +6055,7 @@ function accordion(opts) {
6012
6055
  return {
6013
6056
  ...group({
6014
6057
  fields,
6015
- view: { kind: "accordion" }
6058
+ ui: { kind: "accordion" }
6016
6059
  }),
6017
6060
  view: {
6018
6061
  kind: "accordion",
@@ -6042,7 +6085,7 @@ function prose(opts) {
6042
6085
  label: opts.label,
6043
6086
  icon: opts.icon,
6044
6087
  fields: opts.fields,
6045
- view: { kind: "prose" }
6088
+ ui: { kind: "prose" }
6046
6089
  }),
6047
6090
  view: {
6048
6091
  kind: "prose",
@@ -6068,7 +6111,7 @@ function plaque(opts) {
6068
6111
  label: opts.label,
6069
6112
  icon: opts.icon,
6070
6113
  fields: merge("plaque", title, opts.subtitle),
6071
- view: { kind: "plaque" }
6114
+ ui: { kind: "plaque" }
6072
6115
  }),
6073
6116
  view: {
6074
6117
  kind: "plaque",
@@ -6091,7 +6134,7 @@ function ledger(opts) {
6091
6134
  label: opts.label,
6092
6135
  icon: opts.icon,
6093
6136
  fields: opts.fields,
6094
- view: { kind: "ledger" }
6137
+ ui: { kind: "ledger" }
6095
6138
  }),
6096
6139
  view: { kind: "ledger" }
6097
6140
  };
@@ -6110,7 +6153,7 @@ function stats(opts) {
6110
6153
  label: opts.label,
6111
6154
  icon: opts.icon,
6112
6155
  fields: opts.fields,
6113
- view: { kind: "stats" }
6156
+ ui: { kind: "stats" }
6114
6157
  }),
6115
6158
  view: { kind: "stats" }
6116
6159
  };
@@ -6129,7 +6172,7 @@ function facets(opts) {
6129
6172
  label: opts.label,
6130
6173
  icon: opts.icon,
6131
6174
  fields: opts.fields,
6132
- view: { kind: "facets" }
6175
+ ui: { kind: "facets" }
6133
6176
  }),
6134
6177
  view: { kind: "facets" }
6135
6178
  };
@@ -6148,7 +6191,7 @@ function terminal(opts) {
6148
6191
  label: opts.label,
6149
6192
  icon: opts.icon,
6150
6193
  fields: opts.fields,
6151
- view: { kind: "terminal" }
6194
+ ui: { kind: "terminal" }
6152
6195
  }),
6153
6196
  view: {
6154
6197
  kind: "terminal",
@@ -6170,7 +6213,7 @@ function callout(opts) {
6170
6213
  label: opts.label,
6171
6214
  icon: opts.icon,
6172
6215
  fields: opts.fields,
6173
- view: { kind: "callout" }
6216
+ ui: { kind: "callout" }
6174
6217
  }),
6175
6218
  view: {
6176
6219
  kind: "callout",
@@ -6195,7 +6238,7 @@ function compare(opts) {
6195
6238
  label: opts.label,
6196
6239
  icon: opts.icon,
6197
6240
  fields: merge("compare", opts.left, opts.right),
6198
- view: { kind: "compare" }
6241
+ ui: { kind: "compare" }
6199
6242
  }),
6200
6243
  view: {
6201
6244
  kind: "compare",
@@ -6226,7 +6269,7 @@ function figure(opts) {
6226
6269
  return {
6227
6270
  ...group({
6228
6271
  fields: merge("figure", image, caption),
6229
- view: { kind: "figure" }
6272
+ ui: { kind: "figure" }
6230
6273
  }),
6231
6274
  view: {
6232
6275
  kind: "figure",
@@ -6251,7 +6294,7 @@ function epigraph(opts) {
6251
6294
  return {
6252
6295
  ...group({
6253
6296
  fields: merge("epigraph", source, attribution),
6254
- view: { kind: "epigraph" }
6297
+ ui: { kind: "epigraph" }
6255
6298
  }),
6256
6299
  view: {
6257
6300
  kind: "epigraph",
@@ -6277,7 +6320,7 @@ function masthead(opts) {
6277
6320
  return {
6278
6321
  ...group({
6279
6322
  fields: merge("masthead", overline, title, opts.meta),
6280
- view: { kind: "masthead" }
6323
+ ui: { kind: "masthead" }
6281
6324
  }),
6282
6325
  view: {
6283
6326
  kind: "masthead",
@@ -6301,7 +6344,7 @@ function timeline(opts) {
6301
6344
  label: opts.label,
6302
6345
  icon: opts.icon,
6303
6346
  fields: opts.fields,
6304
- view: { kind: "timeline" }
6347
+ ui: { kind: "timeline" }
6305
6348
  }),
6306
6349
  view: { kind: "timeline" }
6307
6350
  };
@@ -6320,7 +6363,7 @@ function countdown(opts) {
6320
6363
  ...group({
6321
6364
  label: opts.label,
6322
6365
  fields: source,
6323
- view: { kind: "countdown" }
6366
+ ui: { kind: "countdown" }
6324
6367
  }),
6325
6368
  view: {
6326
6369
  kind: "countdown",
@@ -6343,7 +6386,7 @@ function deck(opts) {
6343
6386
  ...group({
6344
6387
  label: opts.label,
6345
6388
  fields: source,
6346
- view: { kind: "deck" }
6389
+ ui: { kind: "deck" }
6347
6390
  }),
6348
6391
  view: {
6349
6392
  kind: "deck",
@@ -6365,7 +6408,7 @@ function people(opts) {
6365
6408
  ...group({
6366
6409
  label: opts.label,
6367
6410
  fields: source,
6368
- view: { kind: "people" }
6411
+ ui: { kind: "people" }
6369
6412
  }),
6370
6413
  view: {
6371
6414
  kind: "people",
@@ -6391,7 +6434,7 @@ function identity(opts) {
6391
6434
  return {
6392
6435
  ...group({
6393
6436
  fields: merge("identity", avatar, name, role, opts.channels),
6394
- view: { kind: "identity" }
6437
+ ui: { kind: "identity" }
6395
6438
  }),
6396
6439
  view: {
6397
6440
  kind: "identity",
@@ -6417,7 +6460,7 @@ function score(opts) {
6417
6460
  ...group({
6418
6461
  label: opts.label,
6419
6462
  fields: merge("score", source, verdict),
6420
- view: { kind: "score" }
6463
+ ui: { kind: "score" }
6421
6464
  }),
6422
6465
  view: {
6423
6466
  kind: "score",
@@ -6442,7 +6485,7 @@ function status(opts) {
6442
6485
  ...group({
6443
6486
  label: opts.label,
6444
6487
  fields: merge("status", source, since),
6445
- view: { kind: "status" }
6488
+ ui: { kind: "status" }
6446
6489
  }),
6447
6490
  view: {
6448
6491
  kind: "status",
@@ -6466,7 +6509,7 @@ function meter(opts) {
6466
6509
  ...group({
6467
6510
  label: opts.label,
6468
6511
  fields: merge("meter", source, of),
6469
- view: { kind: "meter" }
6512
+ ui: { kind: "meter" }
6470
6513
  }),
6471
6514
  view: {
6472
6515
  kind: "meter",
@@ -6493,7 +6536,7 @@ function receipt(opts) {
6493
6536
  ...group({
6494
6537
  label: opts.label,
6495
6538
  fields: merge("receipt", opts.fields, total),
6496
- view: { kind: "receipt" }
6539
+ ui: { kind: "receipt" }
6497
6540
  }),
6498
6541
  view: {
6499
6542
  kind: "receipt",
@@ -6516,7 +6559,7 @@ function balance(opts) {
6516
6559
  ...group({
6517
6560
  label: opts.label,
6518
6561
  fields: source,
6519
- view: { kind: "balance" }
6562
+ ui: { kind: "balance" }
6520
6563
  }),
6521
6564
  view: {
6522
6565
  kind: "balance",
@@ -6544,7 +6587,7 @@ function route(opts) {
6544
6587
  return {
6545
6588
  ...group({
6546
6589
  fields: merge("route", from, to, depart, arrive, duration),
6547
- view: { kind: "route" }
6590
+ ui: { kind: "route" }
6548
6591
  }),
6549
6592
  view: {
6550
6593
  kind: "route",
@@ -6693,7 +6736,7 @@ function nutrition(opts) {
6693
6736
  ...group({
6694
6737
  label: opts.label,
6695
6738
  fields: merge("nutrition", energy, opts.fields),
6696
- view: { kind: "nutrition" }
6739
+ ui: { kind: "nutrition" }
6697
6740
  }),
6698
6741
  view: {
6699
6742
  kind: "nutrition",
@@ -6719,7 +6762,7 @@ function specimen(opts) {
6719
6762
  return {
6720
6763
  ...group({
6721
6764
  fields: merge("specimen", title, subtitle, opts.conditions),
6722
- view: { kind: "specimen" }
6765
+ ui: { kind: "specimen" }
6723
6766
  }),
6724
6767
  view: {
6725
6768
  kind: "specimen",
@@ -6796,7 +6839,7 @@ function idcard(opts) {
6796
6839
  return {
6797
6840
  ...group({
6798
6841
  fields: merge("idcard", overline, number, opts.meta),
6799
- view: { kind: "idcard" }
6842
+ ui: { kind: "idcard" }
6800
6843
  }),
6801
6844
  view: {
6802
6845
  kind: "idcard",
@@ -6822,7 +6865,7 @@ function properties(opts) {
6822
6865
  label: opts.label,
6823
6866
  icon: opts.icon,
6824
6867
  fields: opts.fields,
6825
- view: { kind: "properties" }
6868
+ ui: { kind: "properties" }
6826
6869
  }),
6827
6870
  view: {
6828
6871
  kind: "properties",
@@ -6844,7 +6887,7 @@ function stack(opts) {
6844
6887
  label: opts.label,
6845
6888
  icon: opts.icon,
6846
6889
  fields: opts.fields,
6847
- view: { kind: "stack" }
6890
+ ui: { kind: "stack" }
6848
6891
  }),
6849
6892
  view: { kind: "stack" }
6850
6893
  };
@@ -7725,8 +7768,9 @@ var hasChildren = (x) => Array.isArray(x.fields);
7725
7768
  * @example f.group({ scope: 'nest', label: 'mod.fields.contact', fields: { name: f.string({}) } })
7726
7769
  */
7727
7770
  function group(o) {
7771
+ if (o["view"] !== void 0) throw new Error("[group] `view` is gone — presentational options live in `ui: {}` (see CLAUDE.md § field author options and the field-types skill).");
7728
7772
  const r = o.rules ?? {};
7729
- const v = o.view ?? {};
7773
+ const v = o.ui ?? {};
7730
7774
  const fields = materializeTree(o.fields);
7731
7775
  const scope = o.scope ?? "hoist";
7732
7776
  if (scope === "nest") {
@@ -7772,7 +7816,7 @@ function row(o) {
7772
7816
  multiple: o.multiple,
7773
7817
  required: o.required,
7774
7818
  rules: o.rules,
7775
- view: { display: "scroll" }
7819
+ ui: { display: "scroll" }
7776
7820
  });
7777
7821
  }
7778
7822
  /**
@@ -7800,7 +7844,7 @@ function sheet(o) {
7800
7844
  label: o.label,
7801
7845
  icon: o.icon,
7802
7846
  fields: rows,
7803
- view: { display: "sheet" }
7847
+ ui: { display: "sheet" }
7804
7848
  });
7805
7849
  }
7806
7850
  /**
@@ -7819,7 +7863,7 @@ function url(o) {
7819
7863
  scope: "nest",
7820
7864
  label: o.label,
7821
7865
  required: o.required,
7822
- view: { kind: "url" },
7866
+ ui: { kind: "url" },
7823
7867
  fields: {
7824
7868
  scheme: string({}),
7825
7869
  username: string({}),
@@ -7961,13 +8005,15 @@ registerType("string", {
7961
8005
  build(o) {
7962
8006
  const cfg = o.config ?? {};
7963
8007
  const min = cfg.min ?? (o.required === true ? 1 : 0);
7964
- let s = string$1(reqTypeErr());
7965
- if (min > 0) s = s.min(min, { message: vmsg("min_length", { min }) });
7966
- if (cfg.max != null) s = s.max(cfg.max, { message: vmsg("max_length", { max: cfg.max }) });
7967
- if (cfg.pattern) s = s.regex(new RegExp(cfg.pattern), { message: vmsg("pattern", cfg.messageKey ? { messageKey: cfg.messageKey } : void 0) });
7968
8008
  return {
7969
8009
  column: "text",
7970
- zod: s,
8010
+ zod: stringContent((str) => {
8011
+ let c = str;
8012
+ if (min > 0) c = c.min(min, { message: vmsg("min_length", { min }) });
8013
+ if (cfg.max != null) c = c.max(cfg.max, { message: vmsg("max_length", { max: cfg.max }) });
8014
+ if (cfg.pattern) c = c.regex(new RegExp(cfg.pattern), { message: vmsg("pattern", cfg.messageKey ? { messageKey: cfg.messageKey } : void 0) });
8015
+ return c;
8016
+ }),
7971
8017
  hints: {
7972
8018
  minLength: cfg.min,
7973
8019
  maxLength: cfg.max
@@ -7988,9 +8034,7 @@ function string(o) {
7988
8034
  }
7989
8035
  /** Shared by localDir/localFile's build(): a server path, min-length-1 when required. */
7990
8036
  function pathZod(o) {
7991
- let s = string$1(reqTypeErr());
7992
- if (o.required === true) s = s.min(1, { message: vmsg("min_length", { min: 1 }) });
7993
- return s;
8037
+ return stringContent((s) => o.required === true ? s.min(1, { message: vmsg("min_length", { min: 1 }) }) : s);
7994
8038
  }
7995
8039
  registerType("localDir", {
7996
8040
  kind: "localDir",
@@ -8652,7 +8696,8 @@ var MEASURED_ROLES = (opts) => {
8652
8696
  label,
8653
8697
  required,
8654
8698
  value,
8655
- view: viewOf(d?.opts),
8699
+ noSearch: d?.opts.noSearch,
8700
+ ui: uiOf(d?.opts),
8656
8701
  options: opts.units,
8657
8702
  strict: true
8658
8703
  });
@@ -9021,7 +9066,7 @@ function periodMkSubDecl(granularity) {
9021
9066
  * (`mkSubDecl`), so it writes its own `normalize` rather than using `role()`. A CORRECTED
9022
9067
  * filling still keeps the composite's own granularity (`canonical.opts`) — the same
9023
9068
  * `date`/`datetime` config the role's own default carries — with the author's
9024
- * label/required/view/value layered on top, exactly like `retypeTo`'s default.
9069
+ * label/required/ui/value layered on top, exactly like `retypeTo`'s default.
9025
9070
  */
9026
9071
  function periodRole(key, label, mkSubDecl) {
9027
9072
  const def = {
@@ -9037,7 +9082,8 @@ function periodRole(key, label, mkSubDecl) {
9037
9082
  label: l ?? canonical.opts.label,
9038
9083
  required,
9039
9084
  value,
9040
- view: viewOf(d.opts)
9085
+ noSearch: d.opts.noSearch,
9086
+ ui: uiOf(d.opts)
9041
9087
  });
9042
9088
  }
9043
9089
  };
@@ -9505,10 +9551,11 @@ var field = new Proxy({}, {
9505
9551
  has: (_t, k) => k in composedField()
9506
9552
  });
9507
9553
  function toClient(field) {
9508
- const { kind, label, required, prim, hints, options, strict, relation, json, hidden, derived, parts, cache } = field;
9554
+ const { kind, label, agent, required, prim, hints, options, strict, relation, json, hidden, derived, parts, cache } = field;
9509
9555
  return {
9510
9556
  kind,
9511
9557
  label,
9558
+ ...agent !== void 0 && { agent },
9512
9559
  required,
9513
9560
  prim,
9514
9561
  hints,
@@ -9542,15 +9589,14 @@ function toClient(field) {
9542
9589
  * open: a magnitude may be real/int/rating, a unit may be a select or a relation.
9543
9590
  */
9544
9591
  /**
9545
- * `NormalizedOpts` (what `FieldDecl.opts` actually is) has no single `view` property — the
9546
- * author's `view`/`ui` are flattened into individual fields (`hidden`, `display`, `kind`, …)
9547
- * by `normalizeOpts`. A correction that wants to carry "view" forward has to collect those
9548
- * flat fields back into the nested shape `declare()`'s own `normalizeOpts` call expects.
9592
+ * `NormalizedOpts` (what `FieldDecl.opts` actually is) has no single `ui` property — the
9593
+ * author's `ui` is flattened into individual fields (`hidden`, `display`, `kind`, …)
9594
+ * by `normalizeOpts`. A correction that wants to carry presentation forward has to collect
9595
+ * those flat fields back into the nested shape `declare()`'s own `normalizeOpts` call expects.
9549
9596
  */
9550
- var VIEW_KEYS = [
9597
+ var UI_KEYS = [
9551
9598
  "hidden",
9552
9599
  "noEditControl",
9553
- "noSearch",
9554
9600
  "display",
9555
9601
  "kind",
9556
9602
  "icon",
@@ -9561,11 +9607,11 @@ var VIEW_KEYS = [
9561
9607
  "span"
9562
9608
  ];
9563
9609
  /** The author's presentation opts, re-nested from `opts`'s flat fields — `undefined` when
9564
- * none were set, so a correction never adds an empty `view: {}` no filling ever had. */
9565
- function viewOf(opts) {
9610
+ * none were set, so a correction never adds an empty `ui: {}` no filling ever had. */
9611
+ function uiOf(opts) {
9566
9612
  if (!opts) return void 0;
9567
9613
  const v = {};
9568
- for (const k of VIEW_KEYS) if (opts[k] !== void 0) v[k] = opts[k];
9614
+ for (const k of UI_KEYS) if (opts[k] !== void 0) v[k] = opts[k];
9569
9615
  return Object.keys(v).length ? v : void 0;
9570
9616
  }
9571
9617
  /** A filling built from a ported factory arrives as a FieldDecl — build it into the
@@ -9588,7 +9634,7 @@ function accepts(def, d) {
9588
9634
  }
9589
9635
  /**
9590
9636
  * The correction every role gets for free: keep what the author said ABOUT the field
9591
- * (`label`, `required`, `view`, `value` — a constant or a ComputeFn), replace the type, and
9637
+ * (`label`, `required`, `ui`, `value` — a constant or a ComputeFn), replace the type, and
9592
9638
  * drop what belonged to the OLD type (`rules` — a number's min/max mean nothing to a
9593
9639
  * picture — and `multiple`, because a composite role owns exactly one column). A role whose
9594
9640
  * own default needs more than a bare factory (`measured`'s unit, `period`'s endpoints)
@@ -9601,7 +9647,8 @@ function retypeTo(factory) {
9601
9647
  label,
9602
9648
  required,
9603
9649
  value,
9604
- view: viewOf(d?.opts)
9650
+ noSearch: d?.opts.noSearch,
9651
+ ui: uiOf(d?.opts)
9605
9652
  });
9606
9653
  };
9607
9654
  }
@@ -9784,56 +9831,13 @@ function partColumns(parts) {
9784
9831
  }
9785
9832
  return cols;
9786
9833
  }
9787
- /**
9788
- * A composite's write-time row shape, DERIVED from its resolved parts. `shape` carries
9789
- * the composite's DEFAULT schema per role — the literal the factory writes by hand
9790
- * (`{ value: numSchema, unit: z.string().refine(…) }`) — and each part decides which
9791
- * schema guards its (role-named) key:
9792
- *
9793
- * - a plain STORED part is keyed by its role name, matching the value object and the
9794
- * column `<field>__<role>` that `flattenEmbeddedAt`/`nestEmbeddedAt` read. A role left
9795
- * to its default filling keeps the default schema — byte-identical to the hand-written
9796
- * literal, so every existing structure/unit/range code and message is preserved.
9797
- * - an OVERRIDDEN stored part is validated by its OWN zod (`meta.zod`) instead of the
9798
- * default schema for that role. This is what makes a RETYPED filling real: an
9799
- * `f.int({})` magnitude rejects 5.5 (its own `int` check) and gets an integer column,
9800
- * and an `f.relation({…})` currency accepts a record id instead of being measured
9801
- * against an ISO-4217 string rule it was never meant to satisfy. The filling also
9802
- * brings its own optionality and its own min/max — an author replacing a filling
9803
- * takes over that role's constraints, so `f.real({ rules: { min: 0 } })` is how a
9804
- * replaced magnitude keeps a bound.
9805
- * - a COMPUTED-AND-STORED part ('computedStored') is keyed by its role name like any
9806
- * other part, but its schema is `z.unknown().optional()`: the SERVER owns the value, so
9807
- * whatever the client sends there is ignored — `stripServerOwnedParts` deletes it a
9808
- * moment later and the write path recomputes the column. It is stripped rather than
9809
- * `z.never()`-rejected because a read hands the client that key (it is a real column,
9810
- * present in every response), so rejecting it would break a read → PATCH-the-whole-object
9811
- * round trip.
9812
- * - a CLIENT-OWNED stored part of a composite that is not `required: true` also accepts
9813
- * an explicit `null` (`tolerateNull`), which is what closes the read → write round trip
9814
- * for a PARTIALLY FILLED composite. A SELECT returns every sub-column, so a record whose
9815
- * magnitude column is set and whose unit column is empty reads back as
9816
- * `{value: 900, unit: null}` (`nestEmbeddedAt` drops the key only when EVERY column is
9817
- * empty — `0` is a value), and feeding that exact object back used to fail with
9818
- * `measured_structure`: the write path refused the shape its own read produced. The null
9819
- * slot now parses through and is written back as NULL, so the round trip is closed and
9820
- * clearing ONE role of a filled composite works instead of erroring.
9821
- * NULL, not absence: a read emits every stored role (empty ones as `null`), so `null` is
9822
- * how "this record has no value there" arrives, while an ABSENT key is a writer that
9823
- * never mentioned a role it owns — which stays the structure error it has always been
9824
- * (`{unit: 'kg'}` with no magnitude, `{lat: 50}` with no longitude). That is the
9825
- * all-or-nothing rule for a DECLARATION, and it is untouched.
9826
- * A `required: true` composite keeps every client-owned slot MANDATORY, null included —
9827
- * "tolerate null" must not turn a required field optional, so an empty or half-empty
9828
- * value still fails with the same structure code it fails with today. A filling the
9829
- * author declared `required` keeps its own mandate too.
9830
- */
9831
- function partsRowShape(shape, parts, tolerateNull = false) {
9834
+ function partsRowShape(shape, parts, tolerance = "strict") {
9832
9835
  const out = { ...shape };
9833
9836
  for (const p of parts) if (p.mode === "computedStored") out[p.key] = unknown().optional();
9834
9837
  else {
9835
9838
  const own = p.overridden ? p.meta.zod : shape[p.role] ?? unknown().optional();
9836
- out[p.key] = tolerateNull && !(p.overridden && p.meta.required === true) ? own.nullable() : own;
9839
+ const relaxed = tolerance !== "strict" && !(p.overridden && p.meta.required === true);
9840
+ out[p.key] = relaxed ? tolerance === "null-or-missing" ? own.nullish() : own.nullable() : own;
9837
9841
  }
9838
9842
  return out;
9839
9843
  }
@@ -9874,16 +9878,17 @@ function stripServerOwnedParts(value, parts) {
9874
9878
  function buildComposite(def, opts, parts) {
9875
9879
  const required = opts.required ?? false;
9876
9880
  const roleZod = def.roleZod(opts, parts);
9877
- const rowSchema = object(partsRowShape(roleZod, parts, required !== true));
9878
- const absentRowSchema = required === true ? object(partsRowShape(roleZod, parts, true)) : rowSchema;
9881
+ const rowSchema = object(partsRowShape(roleZod, parts, required === true ? "strict" : "null"));
9882
+ const absentRowSchema = required === true ? object(partsRowShape(roleZod, parts, "null-or-missing")) : rowSchema;
9879
9883
  const roleKeys = (which) => parts.filter((p) => p.mode === "stored" && (which?.(p) ?? true)).map(partValueKey);
9880
9884
  const absenceKeys = def.absenceRoles ? roleKeys((p) => def.absenceRoles.includes(p.role)) : void 0;
9881
9885
  const clientKeys = def.absenceRoles ? roleKeys() : void 0;
9882
9886
  const zod = unknown().transform((raw, ctx) => {
9883
9887
  const parsed = jsonValue(raw);
9884
9888
  const p = parsed;
9889
+ const isRow = p == null || typeof p === "object" && !Array.isArray(p);
9885
9890
  /** Every one of `keys` empty in this value — `null`/absent alike. */
9886
- const allEmpty = (keys) => p == null || typeof p === "object" && keys.length > 0 && keys.every((k) => p[k] == null);
9891
+ const allEmpty = (keys) => isRow && (p == null || keys.length > 0 && keys.every((k) => p[k] == null));
9887
9892
  let absent = false;
9888
9893
  if (absenceKeys && clientKeys) {
9889
9894
  absent = allEmpty(absenceKeys);
@@ -9957,6 +9962,7 @@ function materialize(decl, name) {
9957
9962
  const base = {
9958
9963
  kind: type.kind,
9959
9964
  label: opts.label ?? "",
9965
+ ...opts.agent !== void 0 && { agent: opts.agent },
9960
9966
  required,
9961
9967
  prim: type.prim,
9962
9968
  column: built.column,
@@ -10000,6 +10006,7 @@ function collectionGroup(key, opts, parts, collectionView) {
10000
10006
  multiple: true,
10001
10007
  required: opts.required ?? false,
10002
10008
  label: opts.label ?? key,
10009
+ ...opts.agent !== void 0 && { agent: opts.agent },
10003
10010
  display: "wrap",
10004
10011
  kind: collectionView,
10005
10012
  fields: rowFields
@@ -10132,7 +10139,7 @@ var src_default = definePlugin({
10132
10139
  label: "webchat.settings.reasoning_display",
10133
10140
  default: "live",
10134
10141
  strict: true,
10135
- view: { noSearch: true },
10142
+ noSearch: true,
10136
10143
  options: [{
10137
10144
  value: "live",
10138
10145
  title: "webchat.settings.reasoning_display_live"
package/dist/web.js CHANGED
@@ -11,7 +11,7 @@ import { definePluginUI } from "@coffer-org/web-ui";
11
11
  var ui_default = definePluginUI({ slots: [{
12
12
  slot: "overlay",
13
13
  id: "webchat",
14
- load: () => import("./ChatWidget-Bs8zrQVZ.js")
14
+ load: () => import("./ChatWidget-DFVDexpD.js")
15
15
  }] });
16
16
  //#endregion
17
17
  export { ui_default as default };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coffer-org/plugin-webchat",
3
- "version": "4.0.0",
3
+ "version": "6.0.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"
@@ -27,8 +27,8 @@
27
27
  "test:ui": "vitest run --root ."
28
28
  },
29
29
  "dependencies": {
30
- "@coffer-org/sdk": "^4.0.0",
31
- "@coffer-org/server": "^4.0.0",
30
+ "@coffer-org/sdk": "^6.0.0",
31
+ "@coffer-org/server": "^6.0.0",
32
32
  "lucide-react": "^1.17.0",
33
33
  "react-markdown": "^10.1.0",
34
34
  "remark-gfm": "^4.0.1"