@jini-ai/chat 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 0.6.1 — 2026-10-13
2
+
3
+ - Owned renderer registries: `createToolRendererRegistry` / `createExtEventRendererRegistry`, optional `JiniChatProvider` props `toolRendererRegistry` / `extEventRendererRegistry`, and `registerMcpUiSurfaceRenderer({ registry })`. The global register/get/clear functions keep working against a compatibility default. A stale unregister handle no longer removes a replacement registered with the same callback.
4
+
1
5
  ## 0.6.0 — 2026-10-13
2
6
 
3
7
  - Remove Recharts peer/dev dependencies. Legacy chart wire components now use the neutral UI host with a styled table fallback; require the extracted UI ^0.5.0 so older built-in Recharts imports cannot leak into a clean consumer.
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * @module JiniChatProvider
3
3
  *
4
- * Wires transport, project, analytics, i18n, and the artifact-renderer
5
- * registry into context — exactly the `JiniChatProviderProps` shape from
4
+ * Wires transport, project, analytics, i18n, and the artifact/tool/extension renderer
5
+ * registries into context — exactly the `JiniChatProviderProps` shape from
6
6
  * `ADS-memory/reports/jini-port/recon/r4b-webui-design.md` §2. This is the ONE file in
7
7
  * this package's presentational layer allowed to bind concrete
8
8
  * adapters/slots to context (mirrors the OD slice discipline's
@@ -10,7 +10,7 @@
10
10
  * boundary lint item 3 — the composition-root analogue for a package
11
11
  * instead of a feature slice).
12
12
  *
13
- * `slots`/`onFeedback`/`toolRegistry` are threaded through unchanged for a
13
+ * `slots`/`onFeedback` are threaded through unchanged for a
14
14
  * consuming host to read via `useJiniChatSlots()`/`useOnFeedback()` — this
15
15
  * package's own components (`<Composer>`, `<MessageRow>`, ...) take the
16
16
  * slots they need directly as props rather than reading them from context
@@ -22,6 +22,8 @@ import { type ReactNode } from 'react';
22
22
  import type { ChatTransport, OnFeedback } from '../../core/index.js';
23
23
  import type { AnalyticsAdapter, AttachmentTraySlot, ComposerSlots, FilePreviewSlot, I18nAdapter, ModelAgentPickerSlot, AnnotationAdapter, ProjectContextValue } from '../slots.js';
24
24
  import type { RendererRegistry } from '../artifact-types.js';
25
+ import type { ToolRendererRegistry } from '../tool-renderer-registry.js';
26
+ import type { ExtEventRendererRegistry } from '../ext-event-renderer-registry.js';
25
27
  export interface JiniChatSlots {
26
28
  modelPicker?: ModelAgentPickerSlot;
27
29
  composer?: ComposerSlots;
@@ -35,11 +37,14 @@ export interface JiniChatProviderProps {
35
37
  analytics?: AnalyticsAdapter;
36
38
  i18n?: I18nAdapter;
37
39
  artifactRegistry?: RendererRegistry;
40
+ /** Optional owned state; omitted bindings inherit the nearest provider or global default. */
41
+ toolRendererRegistry?: ToolRendererRegistry;
42
+ extEventRendererRegistry?: ExtEventRendererRegistry;
38
43
  slots?: JiniChatSlots;
39
44
  onFeedback?: OnFeedback;
40
45
  children: ReactNode;
41
46
  }
42
- export declare function JiniChatProvider({ transport, project, analytics, i18n, artifactRegistry, slots, onFeedback, children }: JiniChatProviderProps): import("react").JSX.Element;
47
+ export declare function JiniChatProvider({ transport, project, analytics, i18n, artifactRegistry, toolRendererRegistry, extEventRendererRegistry, slots, onFeedback, children }: JiniChatProviderProps): import("react").JSX.Element;
43
48
  export declare function useJiniChatSlots(): JiniChatSlots;
44
49
  export declare function useOnFeedback(): OnFeedback | undefined;
45
50
  //# sourceMappingURL=JiniChatProvider.d.ts.map
@@ -2,8 +2,8 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  /**
3
3
  * @module JiniChatProvider
4
4
  *
5
- * Wires transport, project, analytics, i18n, and the artifact-renderer
6
- * registry into context — exactly the `JiniChatProviderProps` shape from
5
+ * Wires transport, project, analytics, i18n, and the artifact/tool/extension renderer
6
+ * registries into context — exactly the `JiniChatProviderProps` shape from
7
7
  * `ADS-memory/reports/jini-port/recon/r4b-webui-design.md` §2. This is the ONE file in
8
8
  * this package's presentational layer allowed to bind concrete
9
9
  * adapters/slots to context (mirrors the OD slice discipline's
@@ -11,7 +11,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
11
11
  * boundary lint item 3 — the composition-root analogue for a package
12
12
  * instead of a feature slice).
13
13
  *
14
- * `slots`/`onFeedback`/`toolRegistry` are threaded through unchanged for a
14
+ * `slots`/`onFeedback` are threaded through unchanged for a
15
15
  * consuming host to read via `useJiniChatSlots()`/`useOnFeedback()` — this
16
16
  * package's own components (`<Composer>`, `<MessageRow>`, ...) take the
17
17
  * slots they need directly as props rather than reading them from context
@@ -20,11 +20,15 @@ import { jsx as _jsx } from "react/jsx-runtime";
20
20
  * without re-threading props through every layer.
21
21
  */
22
22
  import { createContext, useContext } from 'react';
23
- import { AnalyticsContext, ArtifactRegistryContext, ChatTransportContext, I18nContext, ProjectContext } from '../hooks/context.js';
23
+ import { AnalyticsContext, ArtifactRegistryContext, ToolRendererRegistryContext, ExtEventRendererRegistryContext, ChatTransportContext, I18nContext, ProjectContext } from '../hooks/context.js';
24
24
  const JiniChatSlotsContext = createContext({});
25
25
  const OnFeedbackContext = createContext(undefined);
26
- export function JiniChatProvider({ transport, project, analytics, i18n, artifactRegistry, slots, onFeedback, children }) {
26
+ export function JiniChatProvider({ transport, project, analytics, i18n, artifactRegistry, toolRendererRegistry, extEventRendererRegistry, slots, onFeedback, children }) {
27
27
  let tree = (_jsx(ChatTransportContext.Provider, { value: transport, children: _jsx(JiniChatSlotsContext.Provider, { value: slots ?? {}, children: _jsx(OnFeedbackContext.Provider, { value: onFeedback, children: children }) }) }));
28
+ if (toolRendererRegistry)
29
+ tree = _jsx(ToolRendererRegistryContext.Provider, { value: toolRendererRegistry, children: tree });
30
+ if (extEventRendererRegistry)
31
+ tree = _jsx(ExtEventRendererRegistryContext.Provider, { value: extEventRendererRegistry, children: tree });
28
32
  if (artifactRegistry)
29
33
  tree = _jsx(ArtifactRegistryContext.Provider, { value: artifactRegistry, children: tree });
30
34
  if (project)
@@ -1,5 +1,5 @@
1
1
  import { type McpUiToolCallHandler } from '@jini-ai/ui/mcp-ui';
2
- import { type ExtEventRenderProps } from '../ext-event-renderer-registry.js';
2
+ import { type ExtEventRendererRegistry, type ExtEventRenderProps } from '../ext-event-renderer-registry.js';
3
3
  import { type SurfaceExpiryClock } from '../hooks/useSurfaceExpiry.js';
4
4
  /** The `kind: 'ext'` event name this renderer claims. */
5
5
  export declare const MCP_UI_EXT_EVENT_NAME = "mcp-ui";
@@ -55,6 +55,7 @@ export declare function mcpUiSurfaceSlotKey({ data }: {
55
55
  * @param options.maxHeight - See {@link McpUiSurfaceCardProps.maxHeight}. A host with a fixed-width
56
56
  * pane (a docked sidebar rather than a full-width transcript) should set this once here instead of
57
57
  * accepting the library's full-width-oriented 720px default.
58
+ * @param options.registry - Owned registry bound to JiniChatProvider; omitted for the global default.
58
59
  * @returns An unregister handle, so a test or a hot reload can dispose cleanly.
59
60
  */
60
61
  export declare function registerMcpUiSurfaceRenderer(options: {
@@ -63,5 +64,7 @@ export declare function registerMcpUiSurfaceRenderer(options: {
63
64
  onOpenLink?: (url: string) => void;
64
65
  name?: string;
65
66
  maxHeight?: number;
67
+ /** Register in owned state when supplied; otherwise preserve the global API. */
68
+ registry?: ExtEventRendererRegistry;
66
69
  }): () => void;
67
70
  //# sourceMappingURL=McpUiSurfaceCard.d.ts.map
@@ -4,8 +4,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
4
  *
5
5
  * Renders MCP-UI Views inline in a chat transcript, fed by a message's `kind: 'ext', name: 'mcp-ui'`
6
6
  * events — the same extensibility seam `A2uiSurfaceCard.tsx` uses, and registered the same way: a
7
- * host calls {@link registerMcpUiSurfaceRenderer} once at module scope and every `ChatPane` that
8
- * surfaces a matching event renders through it thereafter.
7
+ * host calls {@link registerMcpUiSurfaceRenderer} with its owned registry (or the compatibility
8
+ * default) and matching events in that registry's chat tree render through it.
9
9
  *
10
10
  * Each event's `data` is expected to be an MCP `EmbeddedResource` carrying UI — exactly the object a
11
11
  * tool result's `content` array holds, unwrapped. One shape, deliberately: accepting several
@@ -63,7 +63,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
63
63
  */
64
64
  import { Fragment, useMemo } from 'react';
65
65
  import { McpUiHost, parseUIResource, readActionPlan, readExpiresAt, readPreferredFrameSize, } from '@jini-ai/ui/mcp-ui';
66
- import { registerExtEventRenderer } from '../ext-event-renderer-registry.js';
66
+ import { defaultExtEventRendererRegistry } from '../ext-event-renderer-registry.js';
67
67
  import { useT } from '../hooks/context.js';
68
68
  import { useSurfaceExpiry } from '../hooks/useSurfaceExpiry.js';
69
69
  import { closedSurfaceLabel, reportDeliveredAnswer } from './mcp-ui-surface-state.js';
@@ -181,9 +181,10 @@ export function mcpUiSurfaceSlotKey({ data }) {
181
181
  * @param options.maxHeight - See {@link McpUiSurfaceCardProps.maxHeight}. A host with a fixed-width
182
182
  * pane (a docked sidebar rather than a full-width transcript) should set this once here instead of
183
183
  * accepting the library's full-width-oriented 720px default.
184
+ * @param options.registry - Owned registry bound to JiniChatProvider; omitted for the global default.
184
185
  * @returns An unregister handle, so a test or a hot reload can dispose cleanly.
185
186
  */
186
187
  export function registerMcpUiSurfaceRenderer(options) {
187
- return registerExtEventRenderer({ name: options.name ?? MCP_UI_EXT_EVENT_NAME, renderer: (props) => (_jsx(McpUiSurfaceCard, { ...props, sandboxProxyUrl: options.sandboxProxyUrl, ...(options.onToolCall === undefined ? {} : { onToolCall: options.onToolCall }), ...(options.onOpenLink === undefined ? {} : { onOpenLink: options.onOpenLink }), ...(options.maxHeight === undefined ? {} : { maxHeight: options.maxHeight }) })) }, { slotKey: mcpUiSurfaceSlotKey });
188
+ return (options.registry ?? defaultExtEventRendererRegistry).register({ name: options.name ?? MCP_UI_EXT_EVENT_NAME, renderer: (props) => (_jsx(McpUiSurfaceCard, { ...props, sandboxProxyUrl: options.sandboxProxyUrl, ...(options.onToolCall === undefined ? {} : { onToolCall: options.onToolCall }), ...(options.onOpenLink === undefined ? {} : { onOpenLink: options.onOpenLink }), ...(options.maxHeight === undefined ? {} : { maxHeight: options.maxHeight }) })) }, { slotKey: mcpUiSurfaceSlotKey });
188
189
  }
189
190
  //# sourceMappingURL=McpUiSurfaceCard.js.map
@@ -59,8 +59,7 @@ import { useCopyToClipboard } from '../hooks/useCopyToClipboard.js';
59
59
  import { useRunActivity } from '../hooks/useRunActivity.js';
60
60
  import { useAnsweredCards } from '../hooks/useAnsweredCards.js';
61
61
  import { interleaveMessageBlocks } from '../message-blocks.js';
62
- import { useT } from '../hooks/context.js';
63
- import { extEventSlot, getExtEventRenderer } from '../ext-event-renderer-registry.js';
62
+ import { useT, useExtEventRendererRegistry } from '../hooks/context.js';
64
63
  import { ExtEventErrorBoundary } from './ExtEventErrorBoundary.js';
65
64
  import { AttachmentPreviewModal } from './AttachmentPreviewModal.js';
66
65
  import { SentAttachmentButton } from './SentAttachment.js';
@@ -116,7 +115,7 @@ function UsageSummary({ usage, showTotal }) {
116
115
  return null;
117
116
  return (_jsxs("div", { className: "jini-message-usage", title: title, role: "note", "aria-label": ariaLabel, children: [_jsx("span", { className: "jini-message-usage-dot", "aria-hidden": true, children: "\u25CF" }), t('Done'), " \u00B7 ", text] }));
118
117
  }
119
- function messageBlocks({ visibleContent, message, rows }, { toolCallDisplay = 'show', includeThinking = false } = {}) {
118
+ function messageBlocks({ visibleContent, message, rows, registry }, { toolCallDisplay = 'show', includeThinking = false } = {}) {
120
119
  // Interleaving is skipped outright when an artifact was stripped. `stripArtifact` operates on the
121
120
  // whole string and trims, so the surviving text no longer lines up with the per-run offsets the
122
121
  // event walk produces, and an artifact block could legitimately span a tool call. Rather than
@@ -124,7 +123,7 @@ function messageBlocks({ visibleContent, message, rows }, { toolCallDisplay = 's
124
123
  // messages are dominated by the artifact panel anyway, so the ordering matters least there.
125
124
  if (visibleContent !== message.content)
126
125
  return null;
127
- const blocks = interleaveMessageBlocks({ events: message.events, content: message.content, rows }, { slotOf: (ev) => extEventSlot({ name: ev.name, data: ev.data }), includeThinking });
126
+ const blocks = interleaveMessageBlocks({ events: message.events, content: message.content, rows }, { slotOf: (ev) => registry.slot({ name: ev.name, data: ev.data }), includeThinking });
128
127
  return blocks ? applyToolCallDisplay({ blocks, mode: toolCallDisplay }) : null;
129
128
  }
130
129
  /** The model's reasoning, folded under a "Thinking" summary so it never crowds the answer. */
@@ -164,10 +163,11 @@ function CopyMessageButton({ text, label }) {
164
163
  */
165
164
  export const MessageRow = memo(function MessageRow({ message, usageSummary, showUsageTotal = true, showUsageLine = true, toolCallDisplay = 'show', showThinking = false, runStreaming = false, runSucceeded = false, questionFormInteractive = false, questionFormSubmittedAnswers, onQuestionFormSubmit, projectFileNames, onRequestOpenFile, renderAttachment, }) {
166
165
  const t = useT();
166
+ const registry = useExtEventRendererRegistry();
167
167
  const messageUsage = useMessageUsage({ message, ...(usageSummary === undefined ? {} : { summary: usageSummary }) });
168
168
  const timeline = useToolTimeline({ events: message.events }, { runStreaming, runSucceeded });
169
- const extGroups = useExtEventGroups({ events: message.events }, { slotOf: (name, data) => extEventSlot({ name, data }) });
170
- const answeredCards = useAnsweredCards({ slotOf: extEventSlot });
169
+ const extGroups = useExtEventGroups({ events: message.events }, { slotOf: (name, data) => registry.slot({ name, data }) });
170
+ const answeredCards = useAnsweredCards({ slotOf: registry.slot });
171
171
  // Which attachment chip (if any) this row's preview modal is open for. Row-local, not
172
172
  // module-level: two attachments in the same message can never be "open" at once, but two
173
173
  // different messages opening their own attachments independently is fine and expected.
@@ -178,7 +178,7 @@ export const MessageRow = memo(function MessageRow({ message, usageSummary, show
178
178
  const terminalNotice = terminalMessageNotice({ message }, {});
179
179
  const visibleContent = stripArtifact({ content: message.content });
180
180
  const segments = splitOnQuestionForms({ input: visibleContent });
181
- const blocks = messageBlocks({ visibleContent, message, rows: timeline.rows }, { toolCallDisplay, includeThinking: showThinking });
181
+ const blocks = messageBlocks({ visibleContent, message, rows: timeline.rows, registry }, { toolCallDisplay, includeThinking: showThinking });
182
182
  const pending = isPendingWithNoContent(message, visibleContent, timeline.rows.length);
183
183
  const runInProgress = isRunInProgress(message.runStatus);
184
184
  const awaitingAnswer = messageAwaitingAnswer({ runStreaming, runInProgress, events: message.events }, { isAnsweredCard: answeredCards.isAnsweredCard });
@@ -191,7 +191,7 @@ export const MessageRow = memo(function MessageRow({ message, usageSummary, show
191
191
  : _jsx("div", { className: "jini-message-tools", children: rows.map(renderToolCard) }, key));
192
192
  const flatToolRows = toolRowsForDisplay({ rows: timeline.rows, mode: toolCallDisplay });
193
193
  const renderExtGroup = (group) => {
194
- const renderer = getExtEventRenderer({ name: group.name });
194
+ const renderer = registry.get({ name: group.name });
195
195
  if (!renderer)
196
196
  return null;
197
197
  const node = renderer({
@@ -22,8 +22,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
22
22
  */
23
23
  import { useId, useState } from 'react';
24
24
  import { describeCollapsedToolCall, describeToolCallDetail, formatToolOutputForDisplay, isDelegatedWrapperToolName, isTodoWriteToolName, parseTodoWriteInput, toRenderProps } from '../../core/index.js';
25
- import { useT } from '../hooks/context.js';
26
- import { getToolRenderer } from '../tool-renderer-registry.js';
25
+ import { useT, useToolRendererRegistry } from '../hooks/context.js';
27
26
  import { Icon } from './Icon.js';
28
27
  import { TodoCard } from './TodoCard.js';
29
28
  /** A Jini registry tool id: dotted, lowercase, snake_case segments — `page.fill`, `daemon.db.vacuum`, `chat.send_message`. Never matches an agent-vendor tool name (`Bash`, `Write`, `mcp__jini__execute_delegated_tool`), which is the point — this is how a canonical delegated-tool event is told apart from everything else. */
@@ -31,10 +30,11 @@ const JINI_TOOL_ID_PATTERN = /^[a-z][a-z_]*(\.[a-z][a-z_]*)+$/;
31
30
  const SEARCH_TOOLS_NAMES = new Set(['search_tools', 'mcp__jini__search_tools']);
32
31
  const DESCRIBE_TOOL_NAMES = new Set(['describe_tool', 'mcp__jini__describe_tool']);
33
32
  export function ToolCard({ use, result, runStreaming, runSucceeded, projectFileNames, onRequestOpenFile }) {
33
+ const registry = useToolRendererRegistry();
34
34
  const name = use.name;
35
35
  const isStreaming = runStreaming ?? false;
36
36
  const isSucceeded = runSucceeded ?? false;
37
- const content = renderToolCardBody(name, use, result, isStreaming, isSucceeded, { projectFileNames, onRequestOpenFile });
37
+ const content = renderToolCardBody(name, use, result, isStreaming, isSucceeded, { projectFileNames, onRequestOpenFile }, registry);
38
38
  if (content === null)
39
39
  return null;
40
40
  // Tagged once here, at the single dispatch point every branch below funnels through, rather than
@@ -44,8 +44,8 @@ export function ToolCard({ use, result, runStreaming, runSucceeded, projectFileN
44
44
  // ambiguous the way a repeated literal handle would be.
45
45
  return (_jsx("div", { "data-agent-element": `tool-call-${use.id}`, "data-agent-role": "region", "data-agent-label": `A ${humanizeToolId(name)} tool call the AI made`, children: content }));
46
46
  }
47
- function renderToolCardBody(name, use, result, isStreaming, isSucceeded, fileCtx) {
48
- const custom = getToolRenderer({ name: name });
47
+ function renderToolCardBody(name, use, result, isStreaming, isSucceeded, fileCtx, registry) {
48
+ const custom = registry.get({ name });
49
49
  if (custom) {
50
50
  try {
51
51
  const node = custom(toRenderProps({ use: use, result: result, runStreaming: isStreaming }, { runSucceeded: isSucceeded }));
@@ -88,26 +88,41 @@ export interface ExtEventRendererOptions {
88
88
  /** See {@link ExtEventSlotKey}. Omit for one shared slot per `name`. */
89
89
  slotKey?: ExtEventSlotKey;
90
90
  }
91
- /**
92
- * Register a renderer for an ext-event `name`. Returns an unregister handle so tests / hot-reloads
93
- * can dispose cleanly. Re-registering the same name overwrites — last writer wins.
91
+ /** Owned renderer and slot-key state for one chat tree. */
92
+ export interface ExtEventRendererRegistry {
93
+ register(required: {
94
+ name: string;
95
+ renderer: ExtEventRenderer;
96
+ }, options?: ExtEventRendererOptions): () => void;
97
+ get(required: {
98
+ name: string;
99
+ }): ExtEventRenderer | undefined;
100
+ slot(required: {
101
+ name: string;
102
+ data: unknown;
103
+ }): string;
104
+ clear(): void;
105
+ }
106
+ /** Create independent extension renderer state, including each protocol's slot grouping.
107
+ * @returns A keyed registry with identity-safe unregister handles.
108
+ * @example const extensions = createExtEventRendererRegistry({}); extensions.register({ name: 'card', renderer });
94
109
  */
95
- export declare function registerExtEventRenderer({ name, renderer }: {
110
+ export declare function createExtEventRendererRegistry(_required?: Record<string, never>): ExtEventRendererRegistry;
111
+ /** Compatibility default for hosts that have not supplied owned state. */
112
+ export declare const defaultExtEventRendererRegistry: ExtEventRendererRegistry;
113
+ /** Register against the compatibility default; see the owned registry's registration contract. */
114
+ export declare function registerExtEventRenderer(required: {
96
115
  name: string;
97
116
  renderer: ExtEventRenderer;
98
117
  }, options?: ExtEventRendererOptions): () => void;
99
- export declare function getExtEventRenderer({ name }: {
118
+ export declare function getExtEventRenderer(required: {
100
119
  name: string;
101
120
  }): ExtEventRenderer | undefined;
102
- /**
103
- * The render slot an ext event belongs to: its `name`, or `name:key` when the name's renderer was
104
- * registered with a {@link ExtEventSlotKey} that recognizes this event. A throwing key function
105
- * falls back to the shared slot rather than taking the whole message down.
106
- */
107
- export declare function extEventSlot({ name, data }: {
121
+ /** Resolve a slot using the compatibility default's registered slot key. */
122
+ export declare function extEventSlot(required: {
108
123
  name: string;
109
124
  data: unknown;
110
125
  }): string;
111
- /** Visible mainly for tests. */
126
+ /** Visible mainly for tests using the compatibility default. */
112
127
  export declare function clearExtEventRenderers(): void;
113
128
  //# sourceMappingURL=ext-event-renderer-registry.d.ts.map
@@ -1,39 +1,59 @@
1
- const renderers = new Map();
2
- /**
3
- * Register a renderer for an ext-event `name`. Returns an unregister handle so tests / hot-reloads
4
- * can dispose cleanly. Re-registering the same name overwrites — last writer wins.
1
+ /** Create independent extension renderer state, including each protocol's slot grouping.
2
+ * @returns A keyed registry with identity-safe unregister handles.
3
+ * @example const extensions = createExtEventRendererRegistry({}); extensions.register({ name: 'card', renderer });
5
4
  */
6
- export function registerExtEventRenderer({ name, renderer }, options = {}) {
7
- const entry = { renderer, slotKey: options.slotKey };
8
- renderers.set(name, entry);
9
- return () => {
10
- if (renderers.get(name) === entry)
11
- renderers.delete(name);
12
- };
13
- }
14
- export function getExtEventRenderer({ name }) {
15
- return renderers.get(name)?.renderer;
16
- }
17
- /**
18
- * The render slot an ext event belongs to: its `name`, or `name:key` when the name's renderer was
19
- * registered with a {@link ExtEventSlotKey} that recognizes this event. A throwing key function
20
- * falls back to the shared slot rather than taking the whole message down.
21
- */
22
- export function extEventSlot({ name, data }) {
23
- const slotKey = renderers.get(name)?.slotKey;
24
- if (!slotKey)
25
- return name;
26
- let key;
27
- try {
28
- key = slotKey({ data });
5
+ export function createExtEventRendererRegistry(_required = {}) {
6
+ const renderers = new Map();
7
+ /**
8
+ * Register a renderer for an ext-event `name`. Returns an unregister handle so tests / hot-reloads
9
+ * can dispose cleanly. Re-registering the same name overwrites — last writer wins.
10
+ */
11
+ function register({ name, renderer }, options = {}) {
12
+ const entry = { renderer, slotKey: options.slotKey };
13
+ renderers.set(name, entry);
14
+ return () => {
15
+ if (renderers.get(name) === entry)
16
+ renderers.delete(name);
17
+ };
29
18
  }
30
- catch {
31
- key = undefined;
19
+ function get({ name }) {
20
+ return renderers.get(name)?.renderer;
32
21
  }
33
- return key === undefined || key === '' ? name : `${name}:${key}`;
22
+ /**
23
+ * The render slot an ext event belongs to: its `name`, or `name:key` when the name's renderer was
24
+ * registered with a {@link ExtEventSlotKey} that recognizes this event. A throwing key function
25
+ * falls back to the shared slot rather than taking the whole message down.
26
+ */
27
+ function slot({ name, data }) {
28
+ const slotKey = renderers.get(name)?.slotKey;
29
+ if (!slotKey)
30
+ return name;
31
+ let key;
32
+ try {
33
+ key = slotKey({ data });
34
+ }
35
+ catch {
36
+ key = undefined;
37
+ }
38
+ return key === undefined || key === '' ? name : `${name}:${key}`;
39
+ }
40
+ /** Visible mainly for tests. */
41
+ function clear() { renderers.clear(); }
42
+ return { register, get, slot, clear };
43
+ }
44
+ /** Compatibility default for hosts that have not supplied owned state. */
45
+ export const defaultExtEventRendererRegistry = createExtEventRendererRegistry({});
46
+ /** Register against the compatibility default; see the owned registry's registration contract. */
47
+ export function registerExtEventRenderer(required, options = {}) {
48
+ return defaultExtEventRendererRegistry.register(required, options);
49
+ }
50
+ export function getExtEventRenderer(required) {
51
+ return defaultExtEventRendererRegistry.get(required);
34
52
  }
35
- /** Visible mainly for tests. */
36
- export function clearExtEventRenderers() {
37
- renderers.clear();
53
+ /** Resolve a slot using the compatibility default's registered slot key. */
54
+ export function extEventSlot(required) {
55
+ return defaultExtEventRendererRegistry.slot(required);
38
56
  }
57
+ /** Visible mainly for tests using the compatibility default. */
58
+ export function clearExtEventRenderers() { defaultExtEventRendererRegistry.clear(); }
39
59
  //# sourceMappingURL=ext-event-renderer-registry.js.map
@@ -1,10 +1,14 @@
1
1
  import type { AnalyticsAdapter, I18nAdapter, ProjectContextValue } from '../slots.js';
2
2
  import type { ChatTransport } from '../../core/index.js';
3
3
  import type { RendererRegistry } from '../artifact-types.js';
4
+ import { type ToolRendererRegistry } from '../tool-renderer-registry.js';
5
+ import { type ExtEventRendererRegistry } from '../ext-event-renderer-registry.js';
4
6
  export declare const I18nContext: import("react").Context<I18nAdapter>;
5
7
  export declare const AnalyticsContext: import("react").Context<AnalyticsAdapter>;
6
8
  export declare const ProjectContext: import("react").Context<ProjectContextValue | undefined>;
7
9
  export declare const ChatTransportContext: import("react").Context<ChatTransport | undefined>;
10
+ export declare const ToolRendererRegistryContext: import("react").Context<ToolRendererRegistry>;
11
+ export declare const ExtEventRendererRegistryContext: import("react").Context<ExtEventRendererRegistry>;
8
12
  export declare const ArtifactRegistryContext: import("react").Context<RendererRegistry | undefined>;
9
13
  /** Convenience for call sites that only need the translator function. */
10
14
  export declare function useT(): I18nAdapter['t'];
@@ -15,4 +19,8 @@ export declare function useProjectContext(): ProjectContextValue | undefined;
15
19
  /** Throws when read outside `<JiniChatProvider>` — unlike the other contexts, hooks that need a transport (`useConversation`/`useRunStream`) have no sensible no-op fallback. */
16
20
  export declare function useChatTransport(): ChatTransport;
17
21
  export declare function useArtifactRegistry(): RendererRegistry | undefined;
22
+ /** Read the nearest tool binding, or the compatibility default outside an owned provider. */
23
+ export declare function useToolRendererRegistry(): ToolRendererRegistry;
24
+ /** Renderer lookup and grouping must read the same provider binding. */
25
+ export declare function useExtEventRendererRegistry(): ExtEventRendererRegistry;
18
26
  //# sourceMappingURL=context.d.ts.map
@@ -15,6 +15,8 @@
15
15
  * allowed dependency per `ADS-memory/reports/jini-port/recon/r4b-webui-design.md` §1).
16
16
  */
17
17
  import { createContext, useContext } from 'react';
18
+ import { defaultToolRendererRegistry } from '../tool-renderer-registry.js';
19
+ import { defaultExtEventRendererRegistry } from '../ext-event-renderer-registry.js';
18
20
  const PASSTHROUGH_I18N = {
19
21
  t: (key, vars) => interpolate(key, vars),
20
22
  locale: 'en',
@@ -32,6 +34,8 @@ export const I18nContext = createContext(PASSTHROUGH_I18N);
32
34
  export const AnalyticsContext = createContext(NOOP_ANALYTICS);
33
35
  export const ProjectContext = createContext(undefined);
34
36
  export const ChatTransportContext = createContext(undefined);
37
+ export const ToolRendererRegistryContext = createContext(defaultToolRendererRegistry);
38
+ export const ExtEventRendererRegistryContext = createContext(defaultExtEventRendererRegistry);
35
39
  export const ArtifactRegistryContext = createContext(undefined);
36
40
  /** Convenience for call sites that only need the translator function. */
37
41
  export function useT() {
@@ -57,4 +61,8 @@ export function useChatTransport() {
57
61
  export function useArtifactRegistry() {
58
62
  return useContext(ArtifactRegistryContext);
59
63
  }
64
+ /** Read the nearest tool binding, or the compatibility default outside an owned provider. */
65
+ export function useToolRendererRegistry() { return useContext(ToolRendererRegistryContext); }
66
+ /** Renderer lookup and grouping must read the same provider binding. */
67
+ export function useExtEventRendererRegistry() { return useContext(ExtEventRendererRegistryContext); }
60
68
  //# sourceMappingURL=context.js.map
@@ -7,10 +7,10 @@ export type { ChatTransport, FeedbackChange, OnFeedback, ReattachRunOptions, Run
7
7
  export { RendererRegistry, } from './artifact-types.js';
8
8
  export type { ArtifactFile, ArtifactRenderContext, ArtifactRenderMatch, ArtifactRenderer, } from './artifact-types.js';
9
9
  export type { AgentOption, AgentSelection, AnalyticsAdapter, AnnotationAdapter, AttachmentTraySlot, ComposerDiscoveryArgument, ComposerDiscoveryGroup, ComposerDiscoveryItem, ComposerDiscoveryOutcome, ComposerDiscoverySelection, ComposerPlusItem, ComposerSlots, FilePreviewSlot, I18nAdapter, MentionResult, MentionSource, ModelAgentPickerSlot, ProjectContextValue, } from './slots.js';
10
- export { clearToolRenderers, getToolRenderer, registerToolRenderer, } from './tool-renderer-registry.js';
11
- export type { ToolRenderer, } from './tool-renderer-registry.js';
12
- export { clearExtEventRenderers, extEventSlot, getExtEventRenderer, registerExtEventRenderer, } from './ext-event-renderer-registry.js';
13
- export type { ExtEventRenderProps, ExtEventRenderer, ExtEventRendererOptions, ExtEventSlotKey, } from './ext-event-renderer-registry.js';
10
+ export { createToolRendererRegistry, clearToolRenderers, getToolRenderer, registerToolRenderer, } from './tool-renderer-registry.js';
11
+ export type { ToolRendererRegistry, ToolRenderer, } from './tool-renderer-registry.js';
12
+ export { createExtEventRendererRegistry, clearExtEventRenderers, extEventSlot, getExtEventRenderer, registerExtEventRenderer, } from './ext-event-renderer-registry.js';
13
+ export type { ExtEventRendererRegistry, ExtEventRenderProps, ExtEventRenderer, ExtEventRendererOptions, ExtEventSlotKey, } from './ext-event-renderer-registry.js';
14
14
  /**
15
15
  * @deprecated REF-001 Step C (2026-08-05): `ChatPane` and its companions moved to their own
16
16
  * subpath, `@jini-ai/chat/react/chat-pane` — import from there in new code. Re-exported here,
@@ -1,6 +1,6 @@
1
1
  export { RendererRegistry, } from './artifact-types.js';
2
- export { clearToolRenderers, getToolRenderer, registerToolRenderer, } from './tool-renderer-registry.js';
3
- export { clearExtEventRenderers, extEventSlot, getExtEventRenderer, registerExtEventRenderer, } from './ext-event-renderer-registry.js';
2
+ export { createToolRendererRegistry, clearToolRenderers, getToolRenderer, registerToolRenderer, } from './tool-renderer-registry.js';
3
+ export { createExtEventRendererRegistry, clearExtEventRenderers, extEventSlot, getExtEventRenderer, registerExtEventRenderer, } from './ext-event-renderer-registry.js';
4
4
  // `features/model-picker/` (REF-001 Step B, 2026-08-05): NOT re-exported here. It was, on the
5
5
  // theory in the comment this replaced — "an independent slice... for a consumer that wants
6
6
  // everything from one barrel" — but that consumer never existed. Checked twice, once before and
@@ -28,19 +28,36 @@ import type { ToolRenderProps } from '../core/index.js';
28
28
  * step (a built-in family card, then the generic fallback).
29
29
  */
30
30
  export type ToolRenderer = (props: ToolRenderProps) => ReactNode;
31
+ /** Owned lookup and registration state for one chat tree. */
32
+ export interface ToolRendererRegistry {
33
+ register(required: {
34
+ name: string;
35
+ renderer: ToolRenderer;
36
+ }): () => void;
37
+ get(required: {
38
+ name: string;
39
+ }): ToolRenderer | undefined;
40
+ clear(required: Record<string, never>): void;
41
+ }
42
+ /** Create independent tool renderer state; registrations never leak to another tree.
43
+ * @returns A keyed registry with identity-safe unregister handles.
44
+ * @example const tools = createToolRendererRegistry({}); tools.register({ name: 'Read', renderer });
45
+ */
46
+ export declare function createToolRendererRegistry(_required?: Record<string, never>): ToolRendererRegistry;
47
+ /** Compatibility default for hosts that have not supplied owned state. */
48
+ export declare const defaultToolRendererRegistry: ToolRendererRegistry;
31
49
  /**
32
- * Register a renderer for a tool name. Returns an unregister handle so
33
- * tests / hot-reloads can dispose cleanly. Names are matched case-sensitively
34
- * against `tool_use.name`. Re-registering the same name overwrites — last
35
- * writer wins.
50
+ * Register a renderer for a tool name in the compatibility default. Returns an unregister handle so
51
+ * tests / hot-reloads can dispose cleanly. Names are matched case-sensitively against
52
+ * `tool_use.name`. Re-registering the same name overwrites — last writer wins.
36
53
  */
37
- export declare function registerToolRenderer({ name, renderer }: {
54
+ export declare function registerToolRenderer(required: {
38
55
  name: string;
39
56
  renderer: ToolRenderer;
40
57
  }): () => void;
41
- export declare function getToolRenderer({ name }: {
58
+ export declare function getToolRenderer(required: {
42
59
  name: string;
43
60
  }): ToolRenderer | undefined;
44
- /** Visible mainly for tests. */
45
- export declare function clearToolRenderers(_requiredArgs: Record<string, never>): void;
61
+ /** Visible mainly for tests using the compatibility default. */
62
+ export declare function clearToolRenderers(required: Record<string, never>): void;
46
63
  //# sourceMappingURL=tool-renderer-registry.d.ts.map
@@ -1,22 +1,37 @@
1
- const renderers = new Map();
2
- /**
3
- * Register a renderer for a tool name. Returns an unregister handle so
4
- * tests / hot-reloads can dispose cleanly. Names are matched case-sensitively
5
- * against `tool_use.name`. Re-registering the same name overwrites — last
6
- * writer wins.
1
+ /** Create independent tool renderer state; registrations never leak to another tree.
2
+ * @returns A keyed registry with identity-safe unregister handles.
3
+ * @example const tools = createToolRendererRegistry({}); tools.register({ name: 'Read', renderer });
7
4
  */
8
- export function registerToolRenderer({ name, renderer }) {
9
- renderers.set(name, renderer);
10
- return () => {
11
- if (renderers.get(name) === renderer)
12
- renderers.delete(name);
5
+ export function createToolRendererRegistry(_required = {}) {
6
+ const renderers = new Map();
7
+ return {
8
+ // Names are case-sensitive; last writer wins. Compare the registration entry rather than
9
+ // the callback: re-registering the same callback still invalidates its old cleanup handle.
10
+ register({ name, renderer }) {
11
+ const entry = { renderer };
12
+ renderers.set(name, entry);
13
+ return () => { if (renderers.get(name) === entry)
14
+ renderers.delete(name); };
15
+ },
16
+ get({ name }) { return renderers.get(name)?.renderer; },
17
+ clear(_required) { renderers.clear(); },
13
18
  };
14
19
  }
15
- export function getToolRenderer({ name }) {
16
- return renderers.get(name);
20
+ /** Compatibility default for hosts that have not supplied owned state. */
21
+ export const defaultToolRendererRegistry = createToolRendererRegistry({});
22
+ /**
23
+ * Register a renderer for a tool name in the compatibility default. Returns an unregister handle so
24
+ * tests / hot-reloads can dispose cleanly. Names are matched case-sensitively against
25
+ * `tool_use.name`. Re-registering the same name overwrites — last writer wins.
26
+ */
27
+ export function registerToolRenderer(required) {
28
+ return defaultToolRendererRegistry.register(required);
29
+ }
30
+ export function getToolRenderer(required) {
31
+ return defaultToolRendererRegistry.get(required);
17
32
  }
18
- /** Visible mainly for tests. */
19
- export function clearToolRenderers(_requiredArgs) {
20
- renderers.clear();
33
+ /** Visible mainly for tests using the compatibility default. */
34
+ export function clearToolRenderers(required) {
35
+ defaultToolRendererRegistry.clear(required);
21
36
  }
22
37
  //# sourceMappingURL=tool-renderer-registry.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jini-ai/chat",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Jini's chat/artifact frontend, consolidated into one package with two subpaths: framework-free chat vocabulary and pure parsers in `/core`, headless hooks + presentational components + slots in `/react`. Import only the layer you need.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -196,7 +196,7 @@
196
196
  "registry": "https://registry.npmjs.org"
197
197
  },
198
198
  "dependencies": {
199
- "@jini-ai/core": "^0.4.4"
199
+ "@jini-ai/core": "^0.4.5"
200
200
  },
201
201
  "peerDependencies": {
202
202
  "react": "^18.3.0 || ^19.0.0",
@@ -304,11 +304,11 @@
304
304
  "@ag-ui/core": "0.0.58",
305
305
  "@jini-ai/db": "0.3.4",
306
306
  "@jini-ai/agent-runtime": "0.4.8",
307
- "@jini-ai/core": "^0.4.4",
308
- "@jini-ai/protocol": "^0.4.4",
309
- "@jini-ai/ui": "^0.5.0",
307
+ "@jini-ai/core": "^0.4.5",
310
308
  "@jini-ai/diagnostics": "^0.5.2",
311
- "@jini-ai/agentic": "^0.4.4"
309
+ "@jini-ai/ui": "^0.5.1",
310
+ "@jini-ai/agentic": "^0.4.4",
311
+ "@jini-ai/protocol": "^0.4.4"
312
312
  },
313
313
  "typesVersions": {
314
314
  "*": {