8 Commits

Author SHA1 Message Date
Tyler Slaton b3b339f544 Revert "feat(web-inspector): add Event Snippets and save-as-snippet (#6649)"
This reverts commit ba4260ad66, reversing
changes made to 47c5510b49.
2026-08-26 02:11:19 +02:00
Alem Tuzlak 76c8e23a0b feat(web-inspector): add Event Snippets and save-as-snippet
Developers can compile, save, and replay AG-UI events from Inspector.

Localhost chat can save a live turn as a snippet.
2026-08-21 19:39:18 +02:00
Maxim 4b17ea7d35 fix(scripts): tokenize before hunting loader calls in the purity gate
The #4893 hard-fail gate's loader-call detector gave WRONG VERDICTS IN BOTH
DIRECTIONS. It layered two regexes — a comment/string/template alternation that
blanked only the comment branch, and `\b(?:import|require(?:\.resolve)?)\s*\(`
over the result — then classified an argument as static from the FIRST CHARACTER
after the paren. All nine shapes below were reproduced against the real gate
before the rewrite:

  false FAIL  throw new Error("use require(path) instead")
  false FAIL  `import(${x})` inside a template
  false FAIL  o.import(y) / mod.require(x)          (member calls, not loaders)
  false PASS  /https:\/\//; …import(n)              (the regex's `//` blanked the
                                                     rest of the line, hiding a
                                                     real dynamic call)
  false PASS  import(`stream${n}`)                  (merely STARTS with a quote)
  false PASS  import("zo" + n)                      (same)
  false PASS  import(`${base}/v2/index.mjs`)        (same — the fat entry)
  false PASS  __require(name)                       (no \b inside `__require`)

Replaced with `scanSource`, a single-pass tokenizer that classifies every
character as code / comment / string / template / regex and returns a
length-preserving masked view plus a literal-span list. The one surviving regex
now only ever sees code, so import-shaped TEXT cannot reach it at all; an
argument counts as static only when it is one COMPLETE literal with no
concatenation or interpolation; `__require` is matched; and a member call is
rejected both by lookbehind and by a whitespace-skipping back-scan (so
`m\n  .import(x)` is not a loader either).

Proven in both directions: nine innocent/violation pairs run through the real
`assertEntryPurity`, each innocent form CLEAN and each matching real violation
FAIL. Re-proved end-to-end by prepending `import "streamdown"` to the real
dist/v2/headless.mjs — exit 1 naming all five families — then restoring it
byte-identically. On the untouched dist the scan sees 66 loader calls in the
`.cjs` graph and classifies all 66 static, so it passes because it LOOKED.

Also adds the first `.cjs` fixtures: every existing fixture was `.mjs`, leaving
the script's `format: "cjs"` branch and the `require()` shape asserted by
nothing. Tests 24 → 47.

`stripComments` is renamed `maskNonCode`, since it now blanks literals and
regexes too; it had no caller outside this script and its test. The RN guard
keeps its own copy, untouched.

dev-docs/bundle-size.md: the four holes a sibling agent documented as known
limitations this round are closed and removed from that list; what genuinely
remains (regex-vs-division heuristic, no JSX/TS, indirect loaders) replaces them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 17:27:47 +02:00
Maxim 1b39c12e36 fix(scripts): stop the headless CLI gates skipping themselves on odd paths
Both scripts decided "am I the entrypoint?" by comparing `import.meta.url` to a
`file://`-concatenated `process.argv[1]`. `import.meta.url` is percent-encoded
and symlink-resolved; raw argv[1] is neither. So the comparison was false for
any checkout path containing a space, for any invocation through a symlink
(macOS /tmp is one), and on Windows — and a false guard skipped the whole CLI
block. Reproduced before fixing: the #4893 purity gate and the bundle-size
measurement both exited 0 having printed nothing and asserted nothing, which is
worse than a gate with holes because it manufactures confidence. The guard was
added by this PR so the modules could export internals to their new negative
tests; making the gates testable introduced a way for them not to run.

Both now compare real filesystem paths through an exported `isEntrypoint`:
`fileURLToPath` defeats the encoding and Windows forms, `fs.realpathSync` on
both sides defeats symlinks, and a `path.resolve` fallback keeps a nonexistent
argv[1] from throwing.

Each `node --test` suite gains five entry-guard tests, including an end-to-end
spawn of the real script through a symlinked package-root alias whose name
contains a space — the only case that catches the call site regressing back to
a string comparison (verified: it fails against the old expression). The unit
cases assert the naive comparison really would have failed, so none of them can
pass vacuously. Both negative gates were re-proven to still bite: a doctored
dist entry pulling streamdown fails the purity gate, and a stubbed dist entry
trips the measurement's plausibility floor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 17:09:29 +02:00
Maxim b4cfcf6f98 fix(react-core): stop the purity gate crashing opaquely on the declared Node floor
`assert-headless-purity.mjs` resolved its dist directory with
`import.meta.dirname`, which landed in Node 20.11 and is `undefined` below it.
The root package.json declares `engines: { "node": ">=18" }`, so a contributor
or runner on Node 18 hit this hard-fail CI gate as:

    TypeError [ERR_INVALID_ARG_TYPE]: The "paths[0]" argument must be of type
    string. Received undefined
        at Object.resolve (node:path:1115:7)
        at .../scripts/assert-headless-purity.mjs:71:19

— a stack trace into node internals, at module load, that names neither the
gate nor the real problem. Reproduced against a real Node 18.20.8.

Switch to `path.dirname(fileURLToPath(import.meta.url))`, which both sibling
scripts in this CI job already use (react-core's measure-copilotchat.mjs and
react-native's measure-headless.mjs), so all three read the same and none of
them carries a hidden runtime floor its own package does not declare.

Verified under real Node 18.20.8: the script now walks all four entries (650 /
646 / 649 / 645 modules) and exits 0, and still exits 1 with the full
`links the heavy render stack` report when a forbidden dep is injected into a
dist entry. The metafile-driven graph walk, the loud failure on unresolvable
edges and all 17 negative tests are untouched (`test:scripts`: 19 pass).

Skill-staleness check (reskinnable-demo CLAUDE.md rule): not applicable — this
touches packages/react-core, nothing under .claude/skills/reskin/.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 23:27:41 +02:00
Maxim e7f3d7644d fix(react-core): make the #4893 purity gate scan the graph it claimed to scan
`scripts/assert-headless-purity.mjs` is a hard-fail CI gate, and it did not do
what its header said. It read four built entry files and asked
`code.includes(dep)`. That is weaker than the claim in both directions, and every
item below was reproduced against a real build before this rewrite:

1. It never followed an edge out of those four files. Re-exporting one hook from
   the fat `@copilotkit/react-core/v2` entry — which links shiki, mermaid,
   cytoscape, katex and streamdown — left `dist/v2/headless.mjs` importing that
   entry by name, and the gate printed "clean" for all four files, exit 0. Same
   for a heavy dep reached through `@copilotkit/core`, which is external to this
   build: the entry says only `from "@copilotkit/core"` and there is nothing to
   grep. A split-out relative chunk escaped identically.
2. The header claimed the check "follows into node_modules". It followed nothing
   — not node_modules, not a relative sibling chunk.
3. `code.includes(dep)` is unanchored, so it matched comments and strings. Not
   hypothetical in either direction: the built artifact is comment-PRESERVING
   (233 lines of block comments survive in dist/v2/headless.mjs), and the five
   banned tokens sit in `src/v2/headless.ts`'s own banner. They are absent from
   dist only because that module is a re-export shell whose banner attaches to no
   retained code — moving the same sentence into a module that ships code
   hard-failed CI on all five tokens while linking none of them.

The gate now drives esbuild with `metafile: true` over each built entry and
matches on the RESOLVED graph, so it follows relative chunk edges and into
node_modules for real, resolves `exports` maps, subpaths and pnpm symlinks, and
cannot be fooled or tripped by a comment. Matching is anchored at the package
name (`@shikijs/langs` and `cytoscape-fcose` count; `shikimori` does not) and
also covers a forbidden dep left external, which resolves to no graph input at
all. Unresolvable edges FAIL LOUDLY instead of reading as clean, as does a graph
that does not contain its own entry.

One edge shape survives a bundler: `import(name)` with a non-literal argument,
which esbuild leaves alone without even warning. For that the gate reads text —
the only place it does — over the graph's first-party files, using the
`stripComments` helper ported from the sibling RN guard so a documented
counter-example cannot trip it.

Adds `scripts/__tests__/assert-headless-purity.test.mjs` (17 tests, wired into
`test:scripts` next to measure-copilotchat's), because a hard-fail gate with no
coverage of its own failure mode is how this shipped. Proven after the fix: both
false negatives now exit 1, a clean build exits 0, and a banned token that
appears only in a comment exits 0.

esbuild is already this package's devDependency and already runs in the same CI
job, so the gate needs no workflow change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 23:18:57 +02:00
Maxim f4031f3a62 fix(rn): extend /v2/context purity guard and document render closure-staleness
Final review fix wave for the RN render-tool convergence branch.

Substantive:
- Extend packages/react-core/scripts/assert-headless-purity.mjs to also scan the
  built /v2/context chunk (context.mjs/context.cjs), not just /v2/headless.
  /v2/context carries CopilotKitCoreReact and is imported by react-native, so a
  future shiki/mermaid/katex leak through it would bloat RN bundles (#4893) while
  neither hard-fail guard fired. Comment and failure message updated to name both
  RN-reachable entries. Mutation-verified against context.mjs.
- Document the closure-staleness convergence: render is now captured at
  registration (passed into useFrontendTool) and only refreshed when deps change,
  no longer re-read every render. Consumers whose render closes over changing
  state must now pass deps. Documented in the useRenderTool JSDoc, the
  useRenderTool.mdx reference, and the changeset migration notes.

Minor sweep:
- CopilotChat extraData now lists what renderItem actually reads
  ({ isRunning, renderToolCall, toolMessages }); drop unused executingToolCallIds.
- headless-type-exports.test-d.ts imports React explicitly instead of relying on
  the ambient UMD global.
- useRenderTool.mdx migration heading no longer names the uncut 1.67.0 version.
- Changeset marks @copilotkit/react-core minor (new public type export), matching
  its body.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-08 16:57:25 +02:00
Maxim 68a30c2535 test(react-core): hard-fail if the /v2/headless chunk links the render stack (#4893) 2026-08-08 14:36:45 +02:00