Skip to content

feat(ui-core): extract DOM-free components from @kopai/ui#123

Merged
Vunovati merged 2 commits into
mainfrom
feat/ui-core
Apr 16, 2026
Merged

feat(ui-core): extract DOM-free components from @kopai/ui#123
Vunovati merged 2 commits into
mainfrom
feat/ui-core

Conversation

@Vunovati
Copy link
Copy Markdown
Collaborator

@Vunovati Vunovati commented Apr 16, 2026

  • allows reuse in ReactNative

Summary by CodeRabbit

  • New Features

    • Introduced @kopai/ui-core: DOM-free UI primitives for React Native/SSR/CLI.
    • Typed component catalog and renderer to build and render data-driven UI trees.
    • Added observability component catalog and prompt-generation utility for catalog-driven instructions.
    • Added Kopai SDK provider plus data hooks: useKopaiData and useLiveLogs.
  • Documentation

    • Updated README and example project to reference and demonstrate @kopai/ui-core.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 16, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5d1e7fd3-2b9d-4cc7-a84e-3b52a081405d

📥 Commits

Reviewing files that changed from the base of the PR and between e31c11a and f8348a3.

📒 Files selected for processing (1)
  • packages/ui-core/package.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/ui-core/package.json

📝 Walkthrough

Walkthrough

Adds a new DOM-free package @kopai/ui-core with typed component catalog, UI tree renderer, observability catalog, prompt-generation, SDK provider, React Query integration, data- and live-log hooks, LogBuffer, tests, and example updates switching ui-react-app to depend on @kopai/ui-core.

Changes

Cohort / File(s) Summary
Package manifest & build config
.changeset/ui-core-initial.md, packages/ui-core/package.json, packages/ui-core/tsconfig.json, packages/ui-core/tsdown.config.ts
New @kopai/ui-core package manifest, exports/entrypoints for ESM/CJS, workspace deps/peer react, build/tsdown and tsconfig added.
Public exports
packages/ui-core/src/index.ts
New public entry re-exporting catalog, renderer, prompt generator, provider/hooks, LogBuffer and related types.
Component catalog
packages/ui-core/src/lib/component-catalog.ts, packages/ui-core/src/lib/component-catalog.test.ts
Zod-based dataSource discriminated union, component definition schemas, createCatalog with strongly-typed UI tree schema and tests.
Observability catalog
packages/ui-core/src/lib/observability-catalog.ts
Prebuilt observabilityCatalog with component entries, prop schemas, hasChildren flags, descriptions and acceptsDataFrom lists.
Renderer
packages/ui-core/src/lib/renderer.tsx, packages/ui-core/src/lib/renderer.test.tsx
Typed tree renderer + factory createRendererFromCatalog, data-source integration via useKopaiData, enforcement of acceptsDataFrom, param overrides, and comprehensive tests.
Prompt generation
packages/ui-core/src/lib/generate-prompt-instructions.ts, ...test.ts
generatePromptInstructions produces Markdown from catalog and unified JSON schema; snapshot-tested.
Log buffer
packages/ui-core/src/lib/log-buffer.ts, ...test.ts
LogBuffer class with dedupe/merge/sort/trim/clear and unit tests.
Data & live-log hooks
packages/ui-core/src/hooks/use-kopai-data.ts, ...test.ts, packages/ui-core/src/hooks/use-live-logs.ts, ...test.ts
useKopaiData (React Query wrapper, AbortSignal support) and useLiveLogs (incremental log streaming, buffer, polling, liveness control) plus tests.
SDK provider & query client
packages/ui-core/src/providers/kopai-provider.tsx
KopaiSDKProvider, typed KopaiClient surface, shared queryClient, and useKopaiSDK hook.
Docs & example wiring
README.md, examples/ui-react-app/README.md, examples/ui-react-app/package.json, examples/ui-react-app/src/*.tsx
README updated with package/example entries; ui-react-app switched from @kopai/ui@kopai/ui-core and README examples updated to reference DOM-free primitives.

Sequence Diagram(s)

sequenceDiagram
  participant App
  participant Provider as KopaiSDKProvider
  participant Renderer
  participant Hook as useKopaiData
  participant ReactQuery as QueryClient
  participant Client as KopaiClient

  App->>Provider: Wrap app with KopaiSDKProvider(client)
  App->>Renderer: Render tree (passes registry)
  Renderer->>Hook: request data for element (dataSource)
  Hook->>ReactQuery: useQuery(queryKey, queryFn)
  ReactQuery->>Client: invoke SDK method (with AbortSignal)
  Client-->>ReactQuery: return response / throw error
  ReactQuery-->>Hook: resolve data / error
  Hook-->>Renderer: provide response, loading, error, refetch, updateParams
  Renderer-->>App: render component with data props
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Feat/UI cleanup #116 — Modifies the same catalog/renderer/hooks surfaces and data/response naming that this PR introduces/exposes.
  • Feat/UI examples #121 — Adds/adjusts the same API surface (createCatalog, renderer, KopaiSDKProvider) across packages; strong overlap in exported symbols.
  • Feat/metrics aggregation #105 — Extends SDK methods (e.g., searchAggregatedMetrics) that affect useKopaiData dispatch paths and tests added here.

Poem

🐰
A tiny rabbit hops and sees,
A catalog born without DOM trees,
Hooks that fetch and buffers bright,
Renderers ready for SSR night.
Hooray — ui-core bounds into the light!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.90% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: extracting DOM-free components from @kopai/ui into a new @kopai/ui-core package, which is clear and specific.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ui-core

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (2)
packages/ui-core/src/lib/log-buffer.ts (1)

20-22: Guard maxSize at construction time.

If maxSize <= 0 (or non-integer), buffer behavior becomes unintuitive (immediate/full trimming). A constructor guard will fail fast and avoid hidden misconfiguration.

🔧 Add validation for `maxSize`
   constructor(maxSize = 1000) {
+    if (!Number.isInteger(maxSize) || maxSize <= 0) {
+      throw new Error("maxSize must be a positive integer");
+    }
     this.maxSize = maxSize;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui-core/src/lib/log-buffer.ts` around lines 20 - 22, Add a
construction-time guard in the LogBuffer constructor to ensure the incoming
maxSize is a positive integer; replace the direct assignment this.maxSize =
maxSize with a validation that uses Number.isInteger(maxSize) && maxSize > 0 and
throw a clear RangeError (or TypeError) if it fails so misconfiguration fails
fast while keeping the default 1000 behavior when no arg is provided.
packages/ui-core/src/providers/kopai-provider.tsx (1)

25-33: Shared queryClient singleton may cause issues in SSR or multiple-provider scenarios.

The exported queryClient instance works well for client-side SPAs and testing (as seen in use-live-logs.test.ts using queryClient.clear()). However, this singleton pattern can cause cache sharing issues if:

  • Multiple KopaiSDKProvider instances are mounted simultaneously
  • The package is used in SSR where request isolation is required

If SSR support or multiple isolated providers are needed in the future, consider allowing an optional queryClient prop to override the default.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui-core/src/providers/kopai-provider.tsx` around lines 25 - 33, The
current exported singleton queryClient can cause cross-request or multi-provider
cache sharing; change KopaiSDKProvider to accept an optional queryClient prop
and stop relying on the module-level exported queryClient as the default —
create a new QueryClient inside KopaiSDKProvider when no prop is passed (use the
same defaultOptions), remove or deprecate the global export for runtime
consumers (keep for tests only if necessary), and ensure all internal uses
reference the provider's prop (or internal instance) rather than the
module-level queryClient; target symbols: queryClient and KopaiSDKProvider.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/ui-core/package.json`:
- Around line 16-26: The package's conditional "exports" currently only points
"types" to "./dist/index.d.mts", which leaves CJS consumers without type
declarations; update the "exports" mapping for "." to provide conditional types
for both ESM and CJS (e.g., under "import" set "types" -> "./dist/index.d.mts"
and "default" -> "./dist/index.mjs", and under "require" set "types" ->
"./dist/index.d.cts" and "default" -> "./dist/index.cjs"), and then either align
or remove the top-level "types" field so it doesn't conflict with the
conditional exports (ensure references to dist/index.d.mts and dist/index.d.cts
match the generated declaration files).

In `@packages/ui-core/src/hooks/use-live-logs.ts`:
- Line 35: The hook currently initializes and resumes live mode regardless of
the passed-in enabled flag (isLive is set true by default and setLive(true)
always calls refetch()), so gate live behavior behind enabled: initialize isLive
from enabled (or set to false if enabled is false), make setLive respect enabled
(only allow setting true and calling refetch() when enabled is true), and ensure
refetchInterval and the manual refetch() invocation are conditional on enabled;
update references in use-live-logs.ts for isLive, setIsLiveState/setLive,
refetch(), and refetchInterval so no network activity or “live” state is exposed
when enabled is false.

In `@packages/ui-core/src/lib/component-catalog.ts`:
- Around line 68-77: The public schema currently allows props: z.unknown(), but
code in createCatalog (when building uiTreeSchema) and
generatePromptInstructions (calling z.toJSONSchema()) expects component.props to
be a Zod schema; change the componentDefinitionSchema and any other
component-like Zod objects (the other occurrence around the 132-156 block) to
use props: z.ZodTypeAny(), update types to reflect ZodTypeAny for
component.props, and ensure callers (createCatalog, uiTreeSchema construction,
generatePromptInstructions) treat component.props as a Zod schema rather than
plain objects so runtime z.toJSONSchema() calls are safe.

In `@packages/ui-core/src/lib/generate-prompt-instructions.ts`:
- Around line 154-156: The code is overwriting any existing JSON Schema
definitions by replacing treeJsonSchema.$defs; instead, preserve existing $defs
and inject DataSource into the map. Update the logic around
replaceDataSourceWithRef and the assignment to treeJsonSchema.$defs so you merge
into the existing treeJsonSchema.$defs (or create it if missing) and set only
the DataSource key to dataSourceJsonSchema, leaving other $defs intact to avoid
dangling $ref references.

In `@packages/ui-core/src/lib/log-buffer.ts`:
- Around line 5-13: The dedupe key in function logKey is using a 32-bit rolling
int hash of OtelLogsRow.Body which can collide and silently drop distinct logs;
update logKey to compute a cryptographic hash (e.g., SHA-256) of row.Body and
use its hex (or base64) output instead of the current 32-bit int so the returned
key (`${row.Timestamp}-${row.ServiceName ?? ""}-${hash}`) reliably distinguishes
different bodies; locate logKey and replace the bitwise hash logic with a secure
hash computation (using Node's crypto.createHash('sha256') or Web Crypto) while
keeping Timestamp and ServiceName in the key.

In `@packages/ui-core/src/lib/observability-catalog.ts`:
- Around line 8-15: The Zod schemas for component props (e.g., the Card.props
z.object in observability-catalog and the similar schemas for Grid, Stack,
Heading, Text, Badge, and metric widgets) use .nullable(), which still requires
the key to be present and causes validation failures when props are omitted;
update these prop fields to allow omission by using .nullish() or
.optional().nullable() for each prop type inside the props z.object used by
createCatalog() so that { props: {} } and the example tree validate correctly.

In `@packages/ui-core/src/lib/renderer.test.tsx`:
- Around line 128-134: The test uses createRendererFromCatalog incorrectly: it
returns an object { Renderer, registry } rather than a component, and the second
argument is not the component registry. Change each instantiation (e.g. the
TestRenderer/LocalRenderer/IntegrationRenderer assignments) to destructure the
result: const { Renderer, registry } = createRendererFromCatalog(_testCatalog,
/* options if any */); then update places that call createElement(TestRenderer,
{ tree }) to use createElement(Renderer, { tree, registry }) or otherwise supply
the returned registry to the Renderer invocation. Apply the same fix to the
other occurrences referenced (lines near 260-266, 429-435, 561-564, 655-658,
671-673, 695-699, 726-731) so tests use the Renderer component and registry
returned from createRendererFromCatalog instead of treating the call as a
component or registry argument.

In `@packages/ui-core/src/lib/renderer.tsx`:
- Around line 225-239: The hook ordering is unstable because updateParams
(created via useCallback) is declared after a conditional early return that runs
when methodIsAccepted is false; move the useCallback declaration for
updateParams (and any other hooks) above the conditional that returns null so
hooks run in the same order every render, keeping the same dependencies (e.g.,
setParamsOverride) and preserving the function name updateParams; ensure the
conditional that logs the error (using methodIsAccepted, element.dataSource,
acceptsDataFrom) still returns null when appropriate after the move.
- Around line 26-30: The UITree and UIElement types are currently unbound to a
specific catalog; change them to generics so renderer instances are
catalog-safe: introduce a generic constraint like CatalogLike and redefine
UITree as UITree<C extends CatalogLike> = z.infer<C["uiTreeSchema"]> and
UIElement as UIElement<C extends CatalogLike> = UITree<C>["elements"][string];
then update all renderer-related signatures (e.g., the Renderer type, render
function parameters, and any functions referencing UITree or UIElement) to
accept UITree<C> or UIElement<C> and ensure createCatalog usages and type
parameters propagate the specific catalog type C through the renderer factory.

In `@packages/ui-core/tsdown.config.ts`:
- Line 8: The tsdown.config.ts marks "zod" as external, so update
packages/ui-core/package.json to list "zod" in peerDependencies (e.g., a semver
range like "^x.y.z") so consumers know they must provide it; also remove it from
runtime dependencies if present (or keep it only as a devDependency for local
builds/tests) and ensure the version in peerDependencies matches the version
used in devDependencies/tests; reference the package.json’s peerDependencies
section and the tsdown.config.ts external array to make the change.

---

Nitpick comments:
In `@packages/ui-core/src/lib/log-buffer.ts`:
- Around line 20-22: Add a construction-time guard in the LogBuffer constructor
to ensure the incoming maxSize is a positive integer; replace the direct
assignment this.maxSize = maxSize with a validation that uses
Number.isInteger(maxSize) && maxSize > 0 and throw a clear RangeError (or
TypeError) if it fails so misconfiguration fails fast while keeping the default
1000 behavior when no arg is provided.

In `@packages/ui-core/src/providers/kopai-provider.tsx`:
- Around line 25-33: The current exported singleton queryClient can cause
cross-request or multi-provider cache sharing; change KopaiSDKProvider to accept
an optional queryClient prop and stop relying on the module-level exported
queryClient as the default — create a new QueryClient inside KopaiSDKProvider
when no prop is passed (use the same defaultOptions), remove or deprecate the
global export for runtime consumers (keep for tests only if necessary), and
ensure all internal uses reference the provider's prop (or internal instance)
rather than the module-level queryClient; target symbols: queryClient and
KopaiSDKProvider.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 33a95be8-4109-4b93-8c01-30e674b4ad55

📥 Commits

Reviewing files that changed from the base of the PR and between ca9fa88 and e31c11a.

⛔ Files ignored due to path filters (2)
  • packages/ui-core/src/lib/__snapshots__/generate-prompt-instructions.test.ts.snap is excluded by !**/*.snap
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (25)
  • .changeset/ui-core-initial.md
  • README.md
  • examples/ui-react-app/README.md
  • examples/ui-react-app/package.json
  • examples/ui-react-app/src/custom-observability-catalog.tsx
  • examples/ui-react-app/src/dynamic-kopai-data.tsx
  • examples/ui-react-app/src/static-data.tsx
  • packages/ui-core/package.json
  • packages/ui-core/src/hooks/use-kopai-data.test.ts
  • packages/ui-core/src/hooks/use-kopai-data.ts
  • packages/ui-core/src/hooks/use-live-logs.test.ts
  • packages/ui-core/src/hooks/use-live-logs.ts
  • packages/ui-core/src/index.ts
  • packages/ui-core/src/lib/component-catalog.test.ts
  • packages/ui-core/src/lib/component-catalog.ts
  • packages/ui-core/src/lib/generate-prompt-instructions.test.ts
  • packages/ui-core/src/lib/generate-prompt-instructions.ts
  • packages/ui-core/src/lib/log-buffer.test.ts
  • packages/ui-core/src/lib/log-buffer.ts
  • packages/ui-core/src/lib/observability-catalog.ts
  • packages/ui-core/src/lib/renderer.test.tsx
  • packages/ui-core/src/lib/renderer.tsx
  • packages/ui-core/src/providers/kopai-provider.tsx
  • packages/ui-core/tsconfig.json
  • packages/ui-core/tsdown.config.ts

Comment thread packages/ui-core/package.json
const client = useKopaiSDK();
const bufferRef = useRef(new LogBuffer(maxLogs));
const [version, setVersion] = useState(0);
const [isLive, setIsLiveState] = useState(true);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

enabled is bypassed when live mode is resumed.

The hook starts with isLive === true regardless of enabled, and setLive(true) always calls refetch(). A consumer that passes enabled: false can still trigger network traffic and sees a misleading live state. Gate the live state, refetchInterval, and the manual refetch() path behind enabled.

Also applies to: 86-99

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui-core/src/hooks/use-live-logs.ts` at line 35, The hook currently
initializes and resumes live mode regardless of the passed-in enabled flag
(isLive is set true by default and setLive(true) always calls refetch()), so
gate live behavior behind enabled: initialize isLive from enabled (or set to
false if enabled is false), make setLive respect enabled (only allow setting
true and calling refetch() when enabled is true), and ensure refetchInterval and
the manual refetch() invocation are conditional on enabled; update references in
use-live-logs.ts for isLive, setIsLiveState/setLive, refetch(), and
refetchInterval so no network activity or “live” state is exposed when enabled
is false.

Comment on lines +68 to +77
export const componentDefinitionSchema = z
.object({
hasChildren: z.boolean(),
description: z
.string()
.describe(
"Component description to be displayed by the prompt generator"
),
props: z.unknown(),
acceptsDataFrom: z.array(dataSourceMethodSchema).readonly().optional(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

props should be constrained to a Zod schema.

This API currently accepts props: unknown, but createCatalog() immediately treats component.props as a Zod schema when building uiTreeSchema, and generatePromptInstructions() does the same for z.toJSONSchema(). A plain object can type-check here and then blow up at runtime. Tighten the public type/signature to z.ZodTypeAny.

Also applies to: 132-156

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui-core/src/lib/component-catalog.ts` around lines 68 - 77, The
public schema currently allows props: z.unknown(), but code in createCatalog
(when building uiTreeSchema) and generatePromptInstructions (calling
z.toJSONSchema()) expects component.props to be a Zod schema; change the
componentDefinitionSchema and any other component-like Zod objects (the other
occurrence around the 132-156 block) to use props: z.ZodTypeAny(), update types
to reflect ZodTypeAny for component.props, and ensure callers (createCatalog,
uiTreeSchema construction, generatePromptInstructions) treat component.props as
a Zod schema rather than plain objects so runtime z.toJSONSchema() calls are
safe.

Comment on lines +154 to +156
replaceDataSourceWithRef(treeJsonSchema);
treeJsonSchema.$defs = { DataSource: dataSourceJsonSchema };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't overwrite existing $defs when injecting DataSource.

z.toJSONSchema(treeSchema) can already produce $defs. Replacing the whole object here will leave dangling $refs as soon as any component prop schema emits shared definitions or recursion. Merge into the existing map instead of clobbering it.

Suggested fix
-  treeJsonSchema.$defs = { DataSource: dataSourceJsonSchema };
+  treeJsonSchema.$defs = {
+    ...(treeJsonSchema.$defs as Record<string, unknown> | undefined),
+    DataSource: dataSourceJsonSchema,
+  };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui-core/src/lib/generate-prompt-instructions.ts` around lines 154 -
156, The code is overwriting any existing JSON Schema definitions by replacing
treeJsonSchema.$defs; instead, preserve existing $defs and inject DataSource
into the map. Update the logic around replaceDataSourceWithRef and the
assignment to treeJsonSchema.$defs so you merge into the existing
treeJsonSchema.$defs (or create it if missing) and set only the DataSource key
to dataSourceJsonSchema, leaving other $defs intact to avoid dangling $ref
references.

Comment on lines +5 to +13
function logKey(row: OtelLogsRow): string {
const body = row.Body ?? "";
let hash = 0;
for (let i = 0; i < body.length; i++) {
hash = (hash << 5) - hash + body.charCodeAt(i);
hash = hash & hash;
}
return `${row.Timestamp}-${row.ServiceName ?? ""}-${Math.abs(hash).toString(36)}`;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Dedupe key can silently drop distinct logs.

On Line 12, the key uses a 32-bit hash of Body. Different bodies can collide under the same Timestamp + ServiceName, causing false dedupe and data loss.

💡 Safer key construction
 function logKey(row: OtelLogsRow): string {
-  const body = row.Body ?? "";
-  let hash = 0;
-  for (let i = 0; i < body.length; i++) {
-    hash = (hash << 5) - hash + body.charCodeAt(i);
-    hash = hash & hash;
-  }
-  return `${row.Timestamp}-${row.ServiceName ?? ""}-${Math.abs(hash).toString(36)}`;
+  return JSON.stringify([
+    row.Timestamp,
+    row.ServiceName ?? "",
+    row.Body ?? "",
+  ]);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui-core/src/lib/log-buffer.ts` around lines 5 - 13, The dedupe key
in function logKey is using a 32-bit rolling int hash of OtelLogsRow.Body which
can collide and silently drop distinct logs; update logKey to compute a
cryptographic hash (e.g., SHA-256) of row.Body and use its hex (or base64)
output instead of the current 32-bit int so the returned key
(`${row.Timestamp}-${row.ServiceName ?? ""}-${hash}`) reliably distinguishes
different bodies; locate logKey and replace the bitwise hash logic with a secure
hash computation (using Node's crypto.createHash('sha256') or Web Crypto) while
keeping Timestamp and ServiceName in the key.

Comment on lines +8 to +15
Card: {
props: z.object({
title: z.string().nullable(),
description: z.string().nullable(),
padding: z.enum(["sm", "md", "lg"]).nullable(),
}),
hasChildren: true,
description: "A card container with optional title",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

nullable() is making these props mandatory.

These schemas read like optional UI props, but in Zod nullable() still requires the key to exist. Because createCatalog() validates props against these exact shapes, { props: {} } will fail for components like Card, Grid, Stack, Heading, Text, Badge, and several metric widgets, and the example tree generated in this PR will not validate either. Use .nullish() or .optional().nullable() wherever omission is supposed to be allowed.

Also applies to: 18-35, 38-87, 91-159

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui-core/src/lib/observability-catalog.ts` around lines 8 - 15, The
Zod schemas for component props (e.g., the Card.props z.object in
observability-catalog and the similar schemas for Grid, Stack, Heading, Text,
Badge, and metric widgets) use .nullable(), which still requires the key to be
present and causes validation failures when props are omitted; update these prop
fields to allow omission by using .nullish() or .optional().nullable() for each
prop type inside the props z.object used by createCatalog() so that { props: {}
} and the example tree validate correctly.

Comment on lines +128 to +134
const TestRenderer = createRendererFromCatalog(_testCatalog, {
Box,
Text,
Capture,
DataComponent,
RefetchComponent,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

This file is using createRendererFromCatalog with the wrong API shape.

Per packages/ui-core/src/lib/renderer.tsx, createRendererFromCatalog() returns { Renderer, registry }; it is not itself a React component, and the second parameter is not the component registry object. That makes TestRenderer, LocalRenderer, and IntegrationRenderer objects here, so the createElement(..., { tree }) calls below will fail.

Also applies to: 260-266, 429-435, 561-564, 655-658, 671-673, 695-699, 726-731

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui-core/src/lib/renderer.test.tsx` around lines 128 - 134, The test
uses createRendererFromCatalog incorrectly: it returns an object { Renderer,
registry } rather than a component, and the second argument is not the component
registry. Change each instantiation (e.g. the
TestRenderer/LocalRenderer/IntegrationRenderer assignments) to destructure the
result: const { Renderer, registry } = createRendererFromCatalog(_testCatalog,
/* options if any */); then update places that call createElement(TestRenderer,
{ tree }) to use createElement(Renderer, { tree, registry }) or otherwise supply
the returned registry to the Renderer invocation. Apply the same fix to the
other occurrences referenced (lines near 260-266, 429-435, 561-564, 655-658,
671-673, 695-699, 726-731) so tests use the Renderer component and registry
returned from createRendererFromCatalog instead of treating the call as a
component or registry argument.

Comment on lines +26 to +30
type Catalog = ReturnType<typeof createCatalog>;

export type UITree = z.infer<Catalog["uiTreeSchema"]>;

type UIElement = UITree["elements"][string];
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

wc -l packages/ui-core/src/lib/renderer.tsx

Repository: kopai-app/kopai-mono

Length of output: 105


🏁 Script executed:

cat -n packages/ui-core/src/lib/renderer.tsx | head -50

Repository: kopai-app/kopai-mono

Length of output: 1640


🏁 Script executed:

cat -n packages/ui-core/src/lib/renderer.tsx | sed -n '20,40p'

Repository: kopai-app/kopai-mono

Length of output: 677


🏁 Script executed:

cat -n packages/ui-core/src/lib/renderer.tsx | sed -n '150,180p'

Repository: kopai-app/kopai-mono

Length of output: 1133


🏁 Script executed:

cat -n packages/ui-core/src/lib/renderer.tsx | sed -n '320,345p'

Repository: kopai-app/kopai-mono

Length of output: 1021


🏁 Script executed:

cat -n packages/ui-core/src/lib/component-catalog.ts | head -100

Repository: kopai-app/kopai-mono

Length of output: 3827


🏁 Script executed:

rg "createCatalog" packages/ui-core/src/lib/ -A 10 --max-count 5

Repository: kopai-app/kopai-mono

Length of output: 14927


🏁 Script executed:

rg "UITree" packages/ui-core/src/ -B 2 -A 2

Repository: kopai-app/kopai-mono

Length of output: 7630


🏁 Script executed:

sed -n '200,250p' packages/ui-core/src/lib/component-catalog.ts

Repository: kopai-app/kopai-mono

Length of output: 46


🏁 Script executed:

rg "return {" packages/ui-core/src/lib/component-catalog.ts -A 15 | head -40

Repository: kopai-app/kopai-mono

Length of output: 143


🏁 Script executed:

sed -n '150,220p' packages/ui-core/src/lib/component-catalog.ts

Repository: kopai-app/kopai-mono

Length of output: 1306


Make UITree and Renderer types generic to enforce catalog-specific tree shapes.

UITree is derived from an unbound ReturnType<typeof createCatalog> instead of the specific catalog C passed to the renderer. This allows a renderer created for one catalog to accept trees from any other catalog at compile time, only catching mismatches at runtime when the registry's prop validation fails.

Apply the suggested fix to make UITree and UIElement generic, constrained to a CatalogLike interface, and update all renderer function signatures to use UITree<C> instead of the unbound types.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui-core/src/lib/renderer.tsx` around lines 26 - 30, The UITree and
UIElement types are currently unbound to a specific catalog; change them to
generics so renderer instances are catalog-safe: introduce a generic constraint
like CatalogLike and redefine UITree as UITree<C extends CatalogLike> =
z.infer<C["uiTreeSchema"]> and UIElement as UIElement<C extends CatalogLike> =
UITree<C>["elements"][string]; then update all renderer-related signatures
(e.g., the Renderer type, render function parameters, and any functions
referencing UITree or UIElement) to accept UITree<C> or UIElement<C> and ensure
createCatalog usages and type parameters propagate the specific catalog type C
through the renderer factory.

Comment on lines +225 to +239
const { data, loading, error, refetch } = useKopaiData(effectiveDataSource);

if (!methodIsAccepted && element.dataSource) {
const accepted = acceptsDataFrom?.length
? acceptsDataFrom.join(", ")
: "none";
console.error(
`[ui-core] Component "${element.type}" does not accept dataSource method "${element.dataSource.method}". Accepted methods: ${accepted}.`
);
return null;
}

const updateParams = useCallback((params: Record<string, unknown>) => {
setParamsOverride((prev) => ({ ...prev, ...params }));
}, []);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cd packages/ui-core/src/lib && wc -l renderer.tsx

Repository: kopai-app/kopai-mono

Length of output: 80


🏁 Script executed:

cd packages/ui-core/src/lib && sed -n '200,260p' renderer.tsx | cat -n

Repository: kopai-app/kopai-mono

Length of output: 2220


Move useCallback above the invalid-dataSource return.

This branch sometimes returns before calling useCallback and sometimes does not. If methodIsAccepted flips between renders, React will see a different hook order and can throw a hooks-ordering error.

Suggested fix
   const { data, loading, error, refetch } = useKopaiData(effectiveDataSource);
+
+  const updateParams = useCallback((params: Record<string, unknown>) => {
+    setParamsOverride((prev) => ({ ...prev, ...params }));
+  }, []);
 
   if (!methodIsAccepted && element.dataSource) {
     const accepted = acceptsDataFrom?.length
       ? acceptsDataFrom.join(", ")
       : "none";
     console.error(
       `[ui-core] Component "${element.type}" does not accept dataSource method "${element.dataSource.method}". Accepted methods: ${accepted}.`
     );
     return null;
   }
-
-  const updateParams = useCallback((params: Record<string, unknown>) => {
-    setParamsOverride((prev) => ({ ...prev, ...params }));
-  }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui-core/src/lib/renderer.tsx` around lines 225 - 239, The hook
ordering is unstable because updateParams (created via useCallback) is declared
after a conditional early return that runs when methodIsAccepted is false; move
the useCallback declaration for updateParams (and any other hooks) above the
conditional that returns null so hooks run in the same order every render,
keeping the same dependencies (e.g., setParamsOverride) and preserving the
function name updateParams; ensure the conditional that logs the error (using
methodIsAccepted, element.dataSource, acceptsDataFrom) still returns null when
appropriate after the move.

format: ["esm", "cjs"],
dts: true,
clean: true,
external: ["zod"],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how zod is declared in package.json
cat packages/ui-core/package.json | jq '{dependencies: .dependencies.zod, peerDependencies: .peerDependencies.zod}'

Repository: kopai-app/kopai-mono

Length of output: 122


Add zod to peerDependencies in packages/ui-core/package.json.

zod is marked as external in the build config, which means it won't be bundled into the output and must be provided by consumers. While it's currently listed in dependencies, it should also be declared in peerDependencies to explicitly communicate this requirement. This aligns the package metadata with the build configuration and follows the standard pattern for external dependencies.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui-core/tsdown.config.ts` at line 8, The tsdown.config.ts marks
"zod" as external, so update packages/ui-core/package.json to list "zod" in
peerDependencies (e.g., a semver range like "^x.y.z") so consumers know they
must provide it; also remove it from runtime dependencies if present (or keep it
only as a devDependency for local builds/tests) and ensure the version in
peerDependencies matches the version used in devDependencies/tests; reference
the package.json’s peerDependencies section and the tsdown.config.ts external
array to make the change.

@Vunovati Vunovati merged commit c626c2b into main Apr 16, 2026
2 checks passed
This was referenced May 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant