sygnal 6.3.0 → 6.4.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
@@ -2,6 +2,57 @@
2
2
 
3
3
  All notable changes to Sygnal are listed here. Versions follow [semantic versioning](https://semver.org). Releases before 5.4.0 are described in the [GitHub releases](https://github.com/tpresley/sygnal/releases) and tags.
4
4
 
5
+ ## 6.4.0 — 2026-10-14
6
+
7
+ Fixes for issues an agent hit while building a real app on 6.3.0, and a step-by-step chat tutorial in the AI showcase. Additive: an `event()` payload function may now return `ABORT`, `makeServiceWorkerDriver` takes `enabled`, and a new diagnostic code (SYG427) reports a `CHILD.select()` of something that isn't a component. Dev-time checks get fewer false positives (SYG103, SYG223).
8
+
9
+ **Measured impact.** Size: the kanban gate is (a) 42,516 B with `nativeGlobalThis: false` (was 42,504 B; budget 42,700 B) and (b) 38,497 B by default (was 38,485 B): `event()`'s `ABORT` check adds 12 B. The SYG103 view words are dev-server only (never in `vite build`); SYG427's runtime check is in the dev diagnostics entry (0 production bytes).
10
+
11
+ **Companion packages.**
12
+ - **`sygnal-check` 0.8.0:** new SYG427 (error: a component imported from a module that doesn't export it), no false SYG223 for a `uses` slice in `persist({ pick })`, and SYG609's fix names `makeServiceWorkerDriver` for an `SW` sink. Its caret range doesn't reach 0.8.0 from `^0.7.0`, so update the dev dependency.
13
+ - **`create-sygnal-app` 2.6.0:** every template depends on `sygnal` ^6.4.0 and `sygnal-check` ^0.8.0.
14
+ - **VS Code extension 0.5.0:** bundles `sygnal-check` 0.8.0. The extension itself doesn't change.
15
+ - **Claude Code plugin 0.1.3:** the `sygnal-dev` skill says that an `event()` payload function returns `ABORT` to send nothing.
16
+
17
+ ### Added
18
+
19
+ - **`event()` can send nothing.** A payload function that returns `ABORT` sends no event, as `ABORT` does from any other sink: `EVENTS: event('SAVED', (state) => (state.dirty ? { id: state.id } : ABORT))`. Before, the event went out with `ABORT` as its data, and strict mode (which rules out the raw `EVENTS: (state) => …` form) left no way to skip one; apps split actions or sent placeholder events instead. Typed: the payload function may return `ABORT`. `emit()` gets it too.
20
+ - **`makeServiceWorkerDriver(url, { enabled })`.** `enabled: false` registers no service worker, ignores commands and emits nothing, so an app keeps its `SW` wiring on a dev server, where a worker must not run: `makeServiceWorkerDriver('/sw.js', { enabled: import.meta.env.PROD })`. Before, leaving the driver out in development logged SYG609 on every load, and the fix was a hand-written no-op driver.
21
+ - **SYG427 (error): `CHILD.select()` of something that is not a component.** A component imported by a name its module doesn't export is `undefined` under Vitest (the browser refuses to load the module), so `CHILD.select(Child)` never emitted and a test that never rendered `<Child />` passed with no diagnostic. The dev diagnostics now report it when the parent is created, so `expectNoDiagnostics()` fails in every test of the parent; `sygnal-check` reports a component imported from a relative module that doesn't export it, used as a JSX tag, in `CHILD.select()` or as a Collection's `of`.
22
+ - **`examples/ai-showcase`: a Building Chat tab.** The page is now two tabs: Features (the ten sections) and Building Chat, one chat built up in eight steps on one model (Kimi K3 on Kindo): streaming; stop, retry and clear; the model's reasoning and a token tally; follow-up suggestions from a structured-output request; `persist()`; an `agent` declaration the model reads (an answer style) and acts on (a shopping list, with approval before clearing it); and a client tool answered with a click. Each step is a complete component, and Show code marks the lines new since the step before. Example chips fill in the prompt without sending it.
23
+
24
+ ### Fixed
25
+
26
+ - **SYG103 no longer warns about markup drawn by a view helper.** The dev check that takes a not-yet-rendered selector as conditionally rendered read only the view function's source, so a button drawn by `panel()` (called from the view, in the same file or imported) counted as missing and warned after a few renders; the only way out was a component per branch. In dev, `sygnal/vite` now registers the words of a component module's string literals and of its relative imports, and the check reads them too. Strings that are selectors or paths are left out, so a typo in the intent's own selector is still reported. Without the plugin the check works as before.
27
+ - **SYG223 no longer flags a behavior's slice.** `persist({ pick: ['chat'] })` on a component with `uses = { chat: chat() }` warned that `chat` isn't a key of `initialState` (dev diagnostics and `sygnal-check`), but a `uses` slice is state and is saved; both checkers now count `uses` keys.
28
+ - **SYG609's fix names the right driver.** An `SW` sink now gets `makeServiceWorkerDriver('/sw.js', { enabled: import.meta.env.PROD })` instead of `makeFetchDriver()`, and a `WS` sink `makeSocketDriver()`, without the URL argument the fix text gave it (the socket driver takes none; connections come from the `connections` static). Runtime and `sygnal-check`.
29
+
30
+ ## create-sygnal-app 2.5.0 — 2026-10-13
31
+
32
+ The `mcp-app` starter, new projects on sygnal-check 0.7.0, docs and examples; `sygnal` itself is unchanged (6.3.0). Testing the starter in Claude Desktop showed that a view's `updateModelContext` never reaches the model in its chat, so the starter now tells the model what the user picked through a server tool.
33
+
34
+ ### Changed
35
+
36
+ - **`create-sygnal-app`: the `mcp-app` template (JS and TS) tells the model what the user picked through the server.** Hosts differ in what they do with `ui/update-model-context`: Cursor adds it to the next turn, claude.ai and Claude Desktop's Code tab let the model fetch it with a built-in tool, and Claude Desktop's chat accepts it (it lists `{ text, image }`) but its model never sees it (tested 2026-10-12). When the user picks a day, the view now calls `select_day`, a tool only the view can call (`_meta.ui.visibility: ['app']`), and the model reads the selection with `get_selected_day`, which `get_forecast`'s description names; the view still sends the selection as model context, now as text plus `structuredContent` (a host that doesn't list `structuredContent` may drop an update with only that). The README and `AGENTS.md` explain the pattern, and the README says to reconnect the server after a rebuild (Claude Desktop keeps the view and the tool list per connection).
37
+ - **`create-sygnal-app`: new projects get `sygnal-check` ^0.7.0** (was ^0.6.0; a caret range on 0.x doesn't cross minors), so the editor highlights of sygnal-check 0.7.0 (below) work in them from the start. `sygnal` stays ^6.3.0.
38
+
39
+ ### Added
40
+
41
+ - **Docs: Native AI Support**, a section above the Guide with seven narrative pages (overview, streaming chat, decision models, UI composed by a model, agents in your app, the agentic web, built for coding agents) that link to the guides, recipes and API reference. The home page lists the AI card right after Pure Components.
42
+ - **Docs: Model context** in [MCP Apps](https://sygnal.js.org/guide/mcp-apps/#model-context): `message` vs `updateModelContext` vs a server tool, how hosts handle each, and the selection tools' server code.
43
+ - **`examples/ai-showcase`**: one page with ten sections, each a small Sygnal app showing a part of `sygnal/ai` against real models (Ollama, Kindo through its Open Responses, Chat Completions and Messages endpoints, TypeSafe Jev, Chrome's built-in model): any model with the same component, structured output, tools, decision models, the command bar, an assistant that operates the app, composed UI, cards in a chat, WebMCP, and an MCP App. Each section has its source behind Show code. Provider keys stay on the dev server (a Keys dialog or key files outside the repository; `apply: 'serve'` routes that refuse cross-site requests).
44
+ - **`examples/mcp-kindo-agents`**: an MCP App that runs Kindo agents from a conversation: the model fills in the inputs, the card starts the run, follows it through a server-side long poll (step evaluations, a live elapsed time) and hands the result back to the model (`get_agent_run`).
45
+ - **`examples/mcp-app`**: the `mcp-app` starter linked to this checkout (part of `npm run test:examples`), with `scripts/add-to-claude-desktop.sh`, which adds the server to Claude Desktop while it is quit (Claude Desktop rewrites `claude_desktop_config.json` while it runs, dropping edits made then).
46
+
47
+ ## sygnal-check 0.7.0, VS Code extension 0.4.0 — 2026-10-13
48
+
49
+ Highlights of Sygnal's wiring in the editor; `sygnal` itself is unchanged (6.3.0).
50
+
51
+ ### Added
52
+
53
+ - **`sygnal-check lsp` highlights the wiring** as semantic tokens (`textDocument/semanticTokens/full`, custom types `sygnalAction`, `sygnalEvent`, `sygnalSelector`, `sygnalTarget`, `sygnalChild`): action names wherever they're wired (`unhandled` when no model entry handles an intent action), EVENTS types, intent selectors (`untargeted` when nothing the component or a child renders can match, `unverified` when only a dynamic class name in the view might), the view classes / ids / controls an intent of their scope selects, and child components selected with `CHILD.select` (the call, `<Child>` and `<Collection of={Child}>` in the parent's view, the child's name). Only names the checker resolved get a token. `workspace/semanticTokens/refresh` after a check that changed them; the setting `sygnal.highlights` turns them off. A client that keeps one semantic-token provider per file can opt out of the provider (`initializationOptions.semanticTokens: false`) and draw the decoded tokens of `sygnal/highlights` itself. Neovim layers them over its TypeScript server's ([highlight groups](https://sygnal.js.org/integration/editors/#neovim)).
54
+ - **VS Code extension: highlights**, drawn as decorations over the theme's colors (VS Code asks only one semantic-token provider per file, so TypeScript's semantic highlighting stays): bold tinted action names, italic EVENTS types, underlined selectors (amber dashed when unverified, red struck through when nothing renders them), tinted targets, underlined selected children. Theme colors `sygnal.highlight.*`, setting `sygnal.highlights` (unset: on).
55
+
5
56
  ## 6.3.0 — 2026-10-11
6
57
 
7
58
  Composing UI with a model (PLAN-9). An app declares its parts once in a catalog, the user describes a page or an edit in words, and a model builds it from those parts: a decision model answering multiple-choice questions (TypeSafe Jev, Ollama's `nimble`) or an LLM with structured output, with the same catalog, app code and tests for both. The composition is plain JSON in state, each part is an ordinary child component, and follow-up requests edit the page while keeping the state of the parts they don't touch. The same catalog lets a chat assistant ask the user with a card instead of in text (`chat({ ask })`), on top of client tools for the `chat` behavior. Around it: `decide()` splits large question sets, `byId()` lenses, and a component's `key` is now its identity.
@@ -742,6 +742,12 @@ if (import.meta.hot) installMcpBridge(import.meta.hot, { agentTools, confirm: ${
742
742
  * /__sygnal/mcp (./mcp.ts: streamable HTTP, loopback / local Host / local Origin only)
743
743
  * and the dev entries also import 'virtual:sygnal/mcp', which connects the page over the
744
744
  * HMR channel (installMcpBridge from 'sygnal/devtools'). Never in `vite build` or Vitest.
745
+ * 11. SYG103 view words (dev server with diagnostics on; never `vite build`, SSR or Vitest): a
746
+ * module that sets `X.intent` or `X.uses` gets a tail registering, for each such X, the
747
+ * words of its string literals and of the relative modules it imports, in
748
+ * `globalThis.__SYGNAL_VIEW_TOKENS__`. The runtime SYG103 check then takes a selector drawn
749
+ * by a view helper (`{state.open && panel()}`) as conditionally rendered, as it does for
750
+ * markup in the view function itself (see viewTokensTail).
745
751
  *
746
752
  * Why not Vite's `define`? Vite's dependency optimizer does not apply user
747
753
  * `define` replacements to pre-bundled dependencies (only process.env.NODE_ENV),
@@ -979,9 +985,20 @@ function sygnal(options = {}) {
979
985
  transform(code, id, opts) {
980
986
  // SYG666 (PLAN-5 W-2): an adapter entry imported without its optional peers; every mode
981
987
  const adapters = adapterImports(this, code, id);
982
- if (adapters)
983
- return checkPeers(this, adapters, id).then(() => devTransform.call(this, code, id, opts));
984
- return devTransform.call(this, code, id, opts);
988
+ const result = adapters
989
+ ? checkPeers(this, adapters, id).then(() => devTransform.call(this, code, id, opts))
990
+ : devTransform.call(this, code, id, opts);
991
+ // 11. the words SYG103 may take as conditionally rendered markup (dev server, diagnostics on)
992
+ const tokens = isServe && !isVitest && devOn && !opts?.ssr ? viewTokensTail(this, code, id) : null;
993
+ if (!tokens)
994
+ return result;
995
+ return Promise.all([result, tokens]).then(([r, tail]) => {
996
+ if (!tail)
997
+ return r;
998
+ if (r == null)
999
+ return withSourcemap(code, [], tail, id);
1000
+ return typeof r == 'string' ? r + tail : { ...r, code: r.code + tail };
1001
+ });
985
1002
  },
986
1003
  };
987
1004
  function devTransform(code, id, opts) {
@@ -1064,6 +1081,55 @@ function sygnal(options = {}) {
1064
1081
  return done();
1065
1082
  }
1066
1083
  }
1084
+ // 11. SYG103 (the dev runtime check of an intent selector that never matched) lets a selector
1085
+ // through as conditionally rendered when its words appear in the component's view; the runtime
1086
+ // can read only the view function's own source. A component module (one that sets `X.intent` or
1087
+ // `X.uses`) gets, in dev, a tail registering for each such component the words of its string
1088
+ // literals and those of the relative modules it imports, where view helpers live
1089
+ // (globalThis.__SYGNAL_VIEW_TOKENS__, a WeakMap read by checks/dom.ts).
1090
+ const COMPONENT_WITH_SELECTORS_RE = /(?:^|[^\w$.])([A-Z][\w$]*)\.(?:intent|uses)\s*=(?!=)/g;
1091
+ const STRING_RE = /(['"])((?:\\.|(?!\1)[^\\\n])*)\1|`((?:\\[\s\S]|[^\\`])*)`/g;
1092
+ const RELATIVE_IMPORT_RE = /(?:\bfrom|\bimport)\s*\(?\s*(['"])(\.{1,2}\/[^'"]+)\1/g;
1093
+ // A string that is itself a selector ('.save', '#id', 'form.ask', '[data-x]') or a path / URL: its
1094
+ // words aren't markup (the intent's own DOM.click('.svae') would otherwise vouch for its typo)
1095
+ const NOT_MARKUP_RE = /(?:^|[\s>+~,(])[\w-]*[.#[][\w-]|^\s*(?:\/|[\w+-]+:\/\/)/;
1096
+ /** the class / id / tag-like words in a module's string and template literals (comments skipped) */
1097
+ function stringWords(code, into) {
1098
+ for (const m of blankOut(code, false).matchAll(STRING_RE)) {
1099
+ const text = m[2] ?? m[3] ?? '';
1100
+ if (NOT_MARKUP_RE.test(text))
1101
+ continue;
1102
+ for (const w of text.match(/[A-Za-z_][\w-]*/g) || [])
1103
+ into.add(w);
1104
+ }
1105
+ }
1106
+ function viewTokensTail(ctx, code, id) {
1107
+ const file = cleanId(id);
1108
+ if (!/\.[jt]sx?$/.test(file) || id.includes('node_modules') || id.startsWith('\0'))
1109
+ return null;
1110
+ const names = new Set();
1111
+ for (const m of blankOut(code, true).matchAll(COMPONENT_WITH_SELECTORS_RE))
1112
+ names.add(m[1]);
1113
+ if (!names.size)
1114
+ return null;
1115
+ const words = new Set();
1116
+ stringWords(code, words);
1117
+ const specs = new Set(Array.from(blankOut(code, false).matchAll(RELATIVE_IMPORT_RE), m => m[2]));
1118
+ return Promise.all([...specs].map(async (spec) => {
1119
+ try {
1120
+ const r = await ctx.resolve(spec, id);
1121
+ const dep = r && !r.external && cleanId(r.id);
1122
+ if (!dep || !/\.[cm]?[jt]sx?$/.test(dep) || dep.includes('node_modules'))
1123
+ return;
1124
+ ctx.addWatchFile?.(dep);
1125
+ stringWords(await fs.promises.readFile(dep, 'utf8'), words);
1126
+ }
1127
+ catch (_) { /* unreadable: its words are just not added */ }
1128
+ })).then(() => {
1129
+ const comps = [...names].map(n => `typeof ${n} == 'function' && ${n}`).join(', ');
1130
+ return `\n;(() => { try { const m = globalThis.__SYGNAL_VIEW_TOKENS__ ||= new WeakMap(), t = ${JSON.stringify([...words].join(' '))}; for (const c of [${comps}]) if (c) m.set(c, t) } catch (_) {} })();\n`;
1131
+ });
1132
+ }
1067
1133
  // PLAN-5 W-2 (D209): the adapter entries and the optional peer dependencies each one imports
1068
1134
  // (the Zag packages pinned together: they depend on each other's exact versions)
1069
1135
  const ZAG = '~1.45.0';