@jini-ai/chat 0.6.15-rc.1 → 0.6.15-rc.2

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.
@@ -54,10 +54,13 @@ export function recordedSurfaceAnswers({ events }, _options = {}) {
54
54
  seen.add(receipt.id);
55
55
  const document = latest.get(receipt.surfaceUri);
56
56
  if (document !== undefined)
57
- answers.set(document, typeof receipt.answerText === 'string' ? receipt.answerText : receipt.text);
57
+ answers.set(document, surfaceAnswerText({ receipt }));
58
58
  }
59
59
  return answers;
60
60
  }
61
+ function surfaceAnswerText({ receipt }, _options = {}) {
62
+ return typeof receipt.answerText === 'string' ? receipt.answerText : receipt.text;
63
+ }
61
64
  /** Project a persisted answer through the same receipt owner as composer input. */
62
65
  function questionAnswerEvent({ event, input }, { includeTitle = false } = {}) {
63
66
  if (event.kind !== 'tool_result')
@@ -177,12 +180,15 @@ function collapseDeliveryReceipts({ events }, _options = {}) {
177
180
  continue;
178
181
  }
179
182
  if (input)
180
- for (const id of acceptedInputIds({ input }))
181
- positions.set(id, result.length);
183
+ recordInputPosition({ input, positions, position: result.length });
182
184
  result.push(event);
183
185
  }
184
186
  return result;
185
187
  }
188
+ function recordInputPosition({ input, positions, position }, _options = {}) {
189
+ for (const id of acceptedInputIds({ input }))
190
+ positions.set(id, position);
191
+ }
186
192
  /** Projects saved answers and reconciles optimistic bubbles with wire events, occurrence for
187
193
  * occurrence. Legacy steering receipts have no client id: only unmatched optimistic input may
188
194
  * match by text; two real events with equal text always remain distinct.
@@ -29,15 +29,20 @@ function selectedAnswerText({ result, input }, _options = {}) {
29
29
  if (typeof result.choice === 'string')
30
30
  labels.push(optionLabel({ group: question.singleSelect ?? question, value: result.choice }));
31
31
  if (Array.isArray(result.selections)) {
32
- for (const selection of result.selections) {
33
- if (typeof selection === 'string')
34
- labels.push(optionLabel({ group: question.multiSelect ?? question, value: selection }));
35
- }
32
+ labels.push(...selectionLabels({ selections: result.selections, question }));
36
33
  }
37
34
  if (labels.length > 0)
38
35
  return labels.join(', ');
39
36
  return Array.isArray(result.selections) && result.selections.length === 0 ? 'None selected' : undefined;
40
37
  }
38
+ function selectionLabels({ selections, question }, _options = {}) {
39
+ const labels = [];
40
+ for (const selection of selections) {
41
+ if (typeof selection === 'string')
42
+ labels.push(optionLabel({ group: question.multiSelect ?? question, value: selection }));
43
+ }
44
+ return labels;
45
+ }
41
46
  /** Read a classified result or delivered callback as plain user text. Decline wins over other fields;
42
47
  * expiry, invalid answers and unrecognized results produce no user message.
43
48
  * @returns Answer text, or undefined when no supported human answer is present; never throws.
@@ -77,11 +77,14 @@ export function turnUsage({ events }, { sessions = new Map(), agentId = '' } = {
77
77
  if (event.stopReason !== undefined)
78
78
  total.stopReason = event.stopReason;
79
79
  const cost = usageCost({ event, agentId, sessions });
80
- if (cost !== undefined)
81
- total.costUsd = (total.costUsd ?? 0) + cost;
80
+ addReportedCost({ total, cost });
82
81
  }
83
82
  return total;
84
83
  }
84
+ function addReportedCost({ total, cost }, _options = {}) {
85
+ if (cost !== undefined)
86
+ total.costUsd = (total.costUsd ?? 0) + cost;
87
+ }
85
88
  /** Add an event's well-formed token and duration counts into `total`, in place. */
86
89
  function addReportedCounts({ total, event }, _optional = {}) {
87
90
  for (const field of ['inputTokens', 'outputTokens', 'durationMs']) {
@@ -10,7 +10,7 @@ export function usageEndLabel({ usage, message, awaitingAnswer = false }, _optio
10
10
  if (status === 'failed')
11
11
  return 'Failed';
12
12
  if (status === 'canceled') {
13
- return /stopped/i.test(terminalMessageNotice({ message: message }) ?? '') ? 'Stopped' : 'Canceled';
13
+ return canceledUsageLabel({ message: message });
14
14
  }
15
15
  if (awaitingAnswer)
16
16
  return 'Waiting for your answer';
@@ -19,6 +19,9 @@ export function usageEndLabel({ usage, message, awaitingAnswer = false }, _optio
19
19
  return 'Failed';
20
20
  return /abort|interrupt|cancel|max_tokens|max_tool_turns|length|tool_use/i.test(reason) ? 'Interrupted' : 'Done';
21
21
  }
22
+ function canceledUsageLabel({ message }, _options = {}) {
23
+ return /stopped/i.test(terminalMessageNotice({ message }) ?? '') ? 'Stopped' : 'Canceled';
24
+ }
22
25
  /** A standalone row has one turn; MessageList supplies the full conversation projection. */
23
26
  export function useMessageUsage({ message, summary }, _optional = {}) {
24
27
  return useMemo(() => summary ?? conversationUsage({ messages: [message] }).get(message.id), [message, summary]);
@@ -24,11 +24,6 @@ const EMPTY_AGENTS = [];
24
24
  function defaultHeader(title, onReset, t) {
25
25
  return (_jsxs("div", { className: "jini-chat-pane__header", children: [_jsxs("div", { className: "jini-chat-pane__heading", children: [_jsx("span", { className: "jini-chat-pane__eyebrow", children: t('Workspace chat') }), _jsx("h1", { className: "jini-chat-pane__title", children: title ?? t('Chat') })] }), _jsx("button", { type: "button", className: "jini-chat-pane__new-thread", onClick: onReset, children: t('New thread') })] }));
26
26
  }
27
- /** Collapses `header ?? defaultHeader(...)` out of `ChatPane` itself — nesting is what taxes the
28
- * `??` in the caller's score, not the branch itself. */
29
- function resolveChatPaneHeader(header, title, onReset, t) {
30
- return header ?? defaultHeader(title, onReset, t);
31
- }
32
27
  function chatPaneClassName(variant, className) {
33
28
  return `jini-chat-pane jini-chat-pane--${variant}${className ? ` ${className}` : ''}`;
34
29
  }
@@ -407,6 +402,6 @@ export function ChatPane(props) {
407
402
  const selectSuggestion = useChatPaneSuggestion({ rootRef, setDraft: pane.composer.setDraft });
408
403
  const openLinkInNewTab = useChatPaneNewTabLinks();
409
404
  const { pendingPrompts, projectFileNameSet } = useChatPaneTranscriptProps({ pane }, definedProps({ source: { projectFileNames: p.projectFileNames } }));
410
- return (_jsxs("section", { className: chatPaneClassName(p.variant, p.className), style: p.style, "data-activity": pane.activity, ref: rootRef, onClickCapture: openLinkInNewTab, children: [_jsx("style", { "data-jini-chat-pane-styles": "true", children: CHAT_PANE_STYLES }), resolveChatPaneHeader(p.header, p.title, pane.reset, t), p.agentPluginsMode !== 'off' && _jsx(AgentPluginUsage, { conversationId: pane.conversation.conversationId, messages: pane.conversation.messages }), _jsxs("div", { className: "jini-chat-pane__body", children: [_jsx(ChatPaneTranscript, { pane: pane, emptyState: p.emptyState, embed: surface.embed, suggestions: p.suggestions, onSelectSuggestion: selectSuggestion, pendingPrompts: pendingPrompts, projectFileNameSet: projectFileNameSet, toolCallDisplay: p.toolCallDisplay, showUsageLine: p.showUsageLine, showThinking: p.showThinking, t: t }), _jsxs("div", { className: "jini-chat-pane__controls", ref: controlsRef, children: [showsControlsSuggestions(surface.embed, pane.conversation.messages.length, p.emptyState) && (_jsx(ChatPaneSuggestionsRow, { suggestions: p.suggestions, onSelect: selectSuggestion, t: t })), pane.secretRedacted ? _jsx("div", { className: "jini-chat-pane__notice", role: "status", children: t(SECRET_REDACTED_NOTICE) }) : null, _jsx(ChatPaneStatusMessages, { unavailable: unavailable, scanningAgents: runtimeView.scanningAgents, connectingAgents: runtimeView.connectingAgents, executionMode: p.executionMode, apiModeAvailable: p.apiModeAvailable, onExecutionModeChange: p.onExecutionModeChange, sendError: pane.conversation.sendError, onRetrySend: () => void pane.retrySend(), attachmentError: pane.attachmentError, dropReadError: fileDrop.dropReadError, workingDirectoryError: pane.workingDirectoryError, workingDirectoryPending: pane.workingDirectoryPending, workingDirectoryInvalid: pane.workingDirectoryInvalid, runtimeInventoryError: inventory.runtimeInventoryError, typedAnswerNotice: pane.typedAnswerNotice, t: t }), _jsx(ChatPaneComposerArea, { fileDrop: fileDrop, uploadAttachments: surface.uploadAttachments, pane: pane, disabled: p.disabled, unavailable: unavailable, placeholder: composerPlaceholder, slots: slots, attachmentAccept: p.attachmentAccept, workingDirectoryAccess: p.workingDirectoryAccess, workingDirectoryControlPlacement: surface.workingDirectoryControlPlacement, t: t })] })] }), p.footer] }));
405
+ return (_jsxs("section", { className: chatPaneClassName(p.variant, p.className), style: p.style, "data-activity": pane.activity, ref: rootRef, onClickCapture: openLinkInNewTab, children: [_jsx("style", { "data-jini-chat-pane-styles": "true", children: CHAT_PANE_STYLES }), p.header ?? defaultHeader(p.title, pane.reset, t), p.agentPluginsMode !== 'off' && _jsx(AgentPluginUsage, { conversationId: pane.conversation.conversationId, messages: pane.conversation.messages }), _jsxs("div", { className: "jini-chat-pane__body", children: [_jsx(ChatPaneTranscript, { pane: pane, emptyState: p.emptyState, embed: surface.embed, suggestions: p.suggestions, onSelectSuggestion: selectSuggestion, pendingPrompts: pendingPrompts, projectFileNameSet: projectFileNameSet, toolCallDisplay: p.toolCallDisplay, showUsageLine: p.showUsageLine, showThinking: p.showThinking, t: t }), _jsxs("div", { className: "jini-chat-pane__controls", ref: controlsRef, children: [showsControlsSuggestions(surface.embed, pane.conversation.messages.length, p.emptyState) && (_jsx(ChatPaneSuggestionsRow, { suggestions: p.suggestions, onSelect: selectSuggestion, t: t })), pane.secretRedacted ? _jsx("div", { className: "jini-chat-pane__notice", role: "status", children: t(SECRET_REDACTED_NOTICE) }) : null, _jsx(ChatPaneStatusMessages, { unavailable: unavailable, scanningAgents: runtimeView.scanningAgents, connectingAgents: runtimeView.connectingAgents, executionMode: p.executionMode, apiModeAvailable: p.apiModeAvailable, onExecutionModeChange: p.onExecutionModeChange, sendError: pane.conversation.sendError, onRetrySend: () => void pane.retrySend(), attachmentError: pane.attachmentError, dropReadError: fileDrop.dropReadError, workingDirectoryError: pane.workingDirectoryError, workingDirectoryPending: pane.workingDirectoryPending, workingDirectoryInvalid: pane.workingDirectoryInvalid, runtimeInventoryError: inventory.runtimeInventoryError, typedAnswerNotice: pane.typedAnswerNotice, t: t }), _jsx(ChatPaneComposerArea, { fileDrop: fileDrop, uploadAttachments: surface.uploadAttachments, pane: pane, disabled: p.disabled, unavailable: unavailable, placeholder: composerPlaceholder, slots: slots, attachmentAccept: p.attachmentAccept, workingDirectoryAccess: p.workingDirectoryAccess, workingDirectoryControlPlacement: surface.workingDirectoryControlPlacement, t: t })] })] }), p.footer] }));
411
406
  }
412
407
  //# sourceMappingURL=ChatPane.js.map
@@ -20,15 +20,18 @@ function unconsumedQueuedInputs({ inputs, messages }) {
20
20
  const receipt = acceptedInputEvent({ event });
21
21
  if (receipt?.data.deliveryState !== 'consumed')
22
22
  continue;
23
- for (const input of [receipt.data, ...(receipt.data.inputs ?? [])]) {
24
- consumed.add(input.id);
25
- for (const alias of input.aliases ?? [])
26
- consumed.add(alias);
27
- }
23
+ recordConsumedInputs({ receipt: receipt.data, consumed });
28
24
  }
29
25
  }
30
26
  return inputs.filter(input => !consumed.has(input.liveMessageId ?? input.id));
31
27
  }
28
+ function recordConsumedInputs({ receipt, consumed }) {
29
+ for (const input of [receipt, ...(receipt.inputs ?? [])]) {
30
+ consumed.add(input.id);
31
+ for (const alias of input.aliases ?? [])
32
+ consumed.add(alias);
33
+ }
34
+ }
32
35
  /** The conversation copies run events onto its messages one effect after streaming stops, so a
33
36
  * consumed receipt can still be missing from the transcript when the send blocker clears. A
34
37
  * live-accepted copy therefore waits until the run's own message reaches a terminal status. */
@@ -1,18 +1,7 @@
1
1
  import { useEffect, useRef } from 'react';
2
+ import { getAgentModelContext } from '@jini-ai/agentic/dom';
2
3
  import { CHAT_PANE_AGENT_TOOLS } from '../agent-tools.js';
3
4
  import { definedProps } from '../../../util/defined-props.js';
4
- function getModelContext() {
5
- const doc = globalThis.document;
6
- if (isModelContext(doc?.modelContext))
7
- return doc.modelContext;
8
- const nav = globalThis.navigator;
9
- if (isModelContext(nav?.modelContext))
10
- return nav.modelContext;
11
- return undefined;
12
- }
13
- function isModelContext(value) {
14
- return typeof value === 'object' && value !== null && typeof value.registerTool === 'function';
15
- }
16
5
  function requireStringField(input, key) {
17
6
  const value = input[key];
18
7
  if (typeof value !== 'string' || value.length === 0)
@@ -206,26 +195,33 @@ export function useChatPaneAgentControl({ pane }, options = {}) {
206
195
  const cleanups = [];
207
196
  // Not registered unless the host opted in: see `webmcp`'s own doc for why this path cannot be
208
197
  // gated and therefore must not be on by default.
209
- const modelContext = options.webmcp === true ? getModelContext() : undefined;
198
+ // Feature-detect the draft surface through its shared owner; no `@mcp-b/*` dependency.
199
+ const modelContext = options.webmcp === true ? getAgentModelContext({
200
+ host: { candidates: () => [
201
+ globalThis.document?.modelContext,
202
+ globalThis.navigator?.modelContext,
203
+ ] },
204
+ }) : undefined;
210
205
  if (modelContext) {
211
206
  const controller = new AbortController();
212
207
  for (const tool of CHAT_PANE_AGENT_TOOLS) {
213
208
  // `registerTool` is async per the draft IDL, and a polyfill may return undefined instead.
214
209
  // Normalize both, then swallow: registration is best-effort, and a rejected one must not
215
210
  // surface as an unhandled rejection from a React effect.
216
- const registered = modelContext.registerTool({
217
- name: tool.id,
218
- description: tool.description,
219
- inputSchema: tool.inputSchema,
220
- execute: (args) => runChatPaneAction(paneRef.current, tool.id, args ?? {}),
221
- }, { signal: controller.signal });
211
+ const registered = modelContext.registerTool({ tool: {
212
+ name: tool.id,
213
+ description: tool.description,
214
+ inputSchema: tool.inputSchema,
215
+ execute: (args) => runChatPaneAction(paneRef.current, tool.id, args ?? {}),
216
+ } }, { signal: controller.signal });
222
217
  void Promise.resolve(registered).catch(() => undefined);
223
218
  }
224
219
  cleanups.push(() => {
220
+ // Aborting the signal is the spec cleanup path; unregisterTool is optional for polyfills.
225
221
  controller.abort();
226
222
  if (modelContext.unregisterTool) {
227
223
  for (const tool of CHAT_PANE_AGENT_TOOLS)
228
- modelContext.unregisterTool(tool.id);
224
+ modelContext.unregisterTool({ name: tool.id });
229
225
  }
230
226
  });
231
227
  }
@@ -58,8 +58,7 @@ function hideToolCalls({ blocks, runFailed }) {
58
58
  const rows = toolRowsForDisplay({ rows: block.rows, mode: 'hidden' }, { recovered, runFailed: failedReply });
59
59
  if (rows.length > 0)
60
60
  out.push({ ...block, rows });
61
- if (block.rows.some((row) => row.status === 'complete'))
62
- recovered = true;
61
+ recovered = block.rows.some((row) => row.status === 'complete') || recovered;
63
62
  }
64
63
  return collapseToolCalls({ blocks: out.reverse() });
65
64
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jini-ai/chat",
3
- "version": "0.6.15-rc.1",
3
+ "version": "0.6.15-rc.2",
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,12 +196,12 @@
196
196
  "registry": "https://registry.npmjs.org"
197
197
  },
198
198
  "dependencies": {
199
- "@jini-ai/core": "^0.6.15-rc.1"
199
+ "@jini-ai/core": "^0.6.15-rc.2"
200
200
  },
201
201
  "peerDependencies": {
202
202
  "react": "^18.3.0 || ^19.0.0",
203
203
  "react-dom": "^18.3.0 || ^19.0.0",
204
- "@jini-ai/db": "^0.6.15-rc.1",
204
+ "@jini-ai/db": "^0.6.15-rc.2",
205
205
  "kysely": "^0.29.6",
206
206
  "better-sqlite3": "^13.0.0",
207
207
  "@electric-sql/pglite": "0.5.8",
@@ -213,11 +213,11 @@
213
213
  "@radix-ui/react-radio-group": "^1.4.7",
214
214
  "@radix-ui/react-select": "^2.3.7",
215
215
  "@radix-ui/react-slot": "^1.3.3",
216
- "@jini-ai/agent-runtime": "^0.6.15-rc.1",
217
- "@jini-ai/agentic": "^0.6.15-rc.1",
218
- "@jini-ai/protocol": "^0.6.15-rc.1",
219
- "@jini-ai/ui": "^0.6.15-rc.1",
220
- "@jini-ai/diagnostics": "^0.6.15-rc.1"
216
+ "@jini-ai/agent-runtime": "^0.6.15-rc.2",
217
+ "@jini-ai/agentic": "^0.6.15-rc.2",
218
+ "@jini-ai/protocol": "^0.6.15-rc.2",
219
+ "@jini-ai/ui": "^0.6.15-rc.2",
220
+ "@jini-ai/diagnostics": "^0.6.15-rc.2"
221
221
  },
222
222
  "peerDependenciesMeta": {
223
223
  "react": {
@@ -303,13 +303,13 @@
303
303
  "pg": "8.23.0",
304
304
  "@ag-ui/core": "0.0.58",
305
305
  "fast-check": "4.3.0",
306
- "@jini-ai/db": "0.6.15-rc.1",
307
- "@jini-ai/ui": "^0.6.15-rc.1",
308
- "@jini-ai/agentic": "^0.6.15-rc.1",
309
- "@jini-ai/diagnostics": "^0.6.15-rc.1",
310
- "@jini-ai/core": "^0.6.15-rc.1",
311
- "@jini-ai/protocol": "^0.6.15-rc.1",
312
- "@jini-ai/agent-runtime": "0.6.15-rc.1"
306
+ "@jini-ai/db": "0.6.15-rc.2",
307
+ "@jini-ai/agentic": "^0.6.15-rc.2",
308
+ "@jini-ai/agent-runtime": "0.6.15-rc.2",
309
+ "@jini-ai/ui": "^0.6.15-rc.2",
310
+ "@jini-ai/core": "^0.6.15-rc.2",
311
+ "@jini-ai/protocol": "^0.6.15-rc.2",
312
+ "@jini-ai/diagnostics": "^0.6.15-rc.2"
313
313
  },
314
314
  "typesVersions": {
315
315
  "*": {