@jini-ai/chat 0.6.15 → 0.6.17

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.
Files changed (75) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +6 -1
  3. package/dist/core/accepted-input.d.ts +12 -0
  4. package/dist/core/accepted-input.js +29 -1
  5. package/dist/core/antigravity/tool-presentation.d.ts +13 -0
  6. package/dist/core/antigravity/tool-presentation.js +20 -0
  7. package/dist/core/assistant-client/ag-ui-transport.js +13 -1
  8. package/dist/core/assistant-client/transport.js +2 -0
  9. package/dist/core/events.d.ts +5 -1
  10. package/dist/core/messages.d.ts +2 -0
  11. package/dist/core/tool-events.d.ts +18 -8
  12. package/dist/core/tool-events.js +72 -16
  13. package/dist/core/tool-presentation.d.ts +14 -0
  14. package/dist/core/tool-presentation.js +8 -0
  15. package/dist/core/transport.d.ts +2 -0
  16. package/dist/core/user-text-redaction.d.ts +2 -1
  17. package/dist/core/user-text-redaction.js +37 -4
  18. package/dist/embed-widget/browser/frontend-session-bridge.d.ts +99 -0
  19. package/dist/embed-widget/browser/frontend-session-bridge.js +384 -0
  20. package/dist/embed-widget/browser/session-store.d.ts +3 -9
  21. package/dist/embed-widget/browser/session-store.js +6 -38
  22. package/dist/embed-widget/react/embed.d.ts +3 -10
  23. package/dist/embed-widget/react/embed.js +8 -52
  24. package/dist/embed-widget/vanilla/index.d.ts +0 -5
  25. package/dist/embed-widget/vanilla/index.js +4 -14
  26. package/dist/embed-widget/vanilla/state.d.ts +3 -11
  27. package/dist/embed-widget/vanilla/state.js +6 -37
  28. package/dist/embed-widget/vanilla/style.css +1 -1
  29. package/dist/react/agent-bridge/frontend-session-bridge.d.ts +2 -56
  30. package/dist/react/agent-bridge/frontend-session-bridge.js +2 -248
  31. package/dist/react/components/Composer.js +2 -1
  32. package/dist/react/components/CopyMessageButton.d.ts +4 -1
  33. package/dist/react/components/CopyMessageButton.js +3 -2
  34. package/dist/react/components/FolderChips.d.ts +13 -0
  35. package/dist/react/components/FolderChips.js +32 -0
  36. package/dist/react/components/Icon.d.ts +1 -1
  37. package/dist/react/components/Icon.js +2 -0
  38. package/dist/react/components/Markdown.d.ts +1 -0
  39. package/dist/react/components/Markdown.js +1 -1
  40. package/dist/react/components/MessageList.d.ts +2 -1
  41. package/dist/react/components/MessageList.js +2 -1
  42. package/dist/react/components/MessageRow.d.ts +3 -0
  43. package/dist/react/components/MessageRow.js +12 -9
  44. package/dist/react/components/MidRunUserMessage.js +3 -2
  45. package/dist/react/components/ToolCard.d.ts +3 -1
  46. package/dist/react/components/ToolCard.js +77 -58
  47. package/dist/react/components/ToolCard.rules.d.ts +20 -0
  48. package/dist/react/components/ToolCard.rules.js +92 -0
  49. package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +18 -6
  50. package/dist/react/features/chat-pane/components/ChatPane.js +4 -3
  51. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +3 -1
  52. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +42 -19
  53. package/dist/react/features/chat-pane/styles.d.ts +1 -1
  54. package/dist/react/features/chat-pane/styles.js +96 -9
  55. package/dist/react/features/chat-pane/types.d.ts +7 -0
  56. package/dist/react/hooks/composer-draft-cache.d.ts +9 -6
  57. package/dist/react/hooks/composer-draft-cache.js +24 -0
  58. package/dist/react/hooks/useComposer.d.ts +7 -0
  59. package/dist/react/hooks/useComposer.js +31 -4
  60. package/dist/react/hooks/useConversation.d.ts +2 -0
  61. package/dist/react/hooks/useConversation.js +4 -1
  62. package/dist/react/hooks/useUserTextGuard.d.ts +1 -0
  63. package/dist/react/hooks/useUserTextGuard.js +3 -1
  64. package/dist/react/styles/reference.css +6 -0
  65. package/dist/react/tool-call-display.d.ts +11 -0
  66. package/dist/react/tool-call-display.js +59 -2
  67. package/dist/store/legacy/sqlite/messages.d.ts +12 -10
  68. package/dist/store/legacy/sqlite/messages.js +4 -1
  69. package/dist/store/runtime/durable-run-store.js +35 -3
  70. package/dist/store/sql/store.js +5 -1
  71. package/package.json +91 -86
  72. package/dist/core/browser/page-actions.d.ts +0 -121
  73. package/dist/core/browser/page-actions.js +0 -115
  74. package/dist/embed-widget/browser/page-actions.d.ts +0 -2
  75. package/dist/embed-widget/browser/page-actions.js +0 -3
@@ -6,6 +6,7 @@ const ICON_GLYPHS = {
6
6
  check: _jsx("path", { d: "M20 6 9 17l-5-5" }),
7
7
  close: CROSS,
8
8
  x: CROSS,
9
+ retry: _jsxs(_Fragment, { children: [_jsx("path", { d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }), _jsx("path", { d: "M3 3v5h5" })] }),
9
10
  'chevron-down': _jsx("path", { d: "m6 9 6 6 6-6" }),
10
11
  /** Conversation-switcher trigger — a speech bubble reads as "chat history" without a label. */
11
12
  comment: (_jsx("path", { d: "M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" })),
@@ -14,6 +15,7 @@ const ICON_GLYPHS = {
14
15
  'chevron-left': _jsx("path", { d: "m15 18-6-6 6-6" }),
15
16
  'chevron-right': _jsx("path", { d: "m9 18 6-6-6-6" }),
16
17
  attach: _jsx("path", { d: "m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" }),
18
+ folder: _jsx("path", { d: "M3 7V5a2 2 0 0 1 2-2h5l2 3h7a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7Z" }),
17
19
  file: _jsxs(_Fragment, { children: [_jsx("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" }), _jsx("path", { d: "M14 2v6h6" }), _jsx("path", { d: "M8 13h8M8 17h6" })] }),
18
20
  image: _jsxs(_Fragment, { children: [_jsx("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }), _jsx("circle", { cx: "9", cy: "9", r: "2" }), _jsx("path", { d: "m21 15-5-5L5 21" })] }),
19
21
  pencil: _jsxs(_Fragment, { children: [_jsx("path", { d: "m16 3 5 5L8 21H3v-5L16 3Z" }), _jsx("path", { d: "m14 5 5 5" })] }),
@@ -2,4 +2,5 @@ export interface MarkdownProps {
2
2
  children: string;
3
3
  }
4
4
  export declare function Markdown({ children }: MarkdownProps): import("react").JSX.Element;
5
+ export declare const SAFE_LINK_HREF: RegExp;
5
6
  //# sourceMappingURL=Markdown.d.ts.map
@@ -415,7 +415,7 @@ const MD_LINK_PARTS = /^\[([^\]\n]+)\]\((.+)\)$/u;
415
415
  const INLINE_RE = new RegExp(String.raw `(\x60[^\x60]+\x60)|(${MD_LINK})|(\*\*[^*]+\*\*)|(\*[^*]+\*|${UNDERSCORE_EM})|(https?:\/\/[^\s)]+)`, 'gu');
416
416
  // Only these targets become anchors; anything else (`javascript:`, `data:`, protocol-relative
417
417
  // `//host`) stays literal text, since the text is untrusted model output.
418
- const SAFE_LINK_HREF = /^(?:https?:\/\/|mailto:|\/(?!\/)|#)/iu;
418
+ export const SAFE_LINK_HREF = /^(?:https?:\/\/|mailto:|\/(?!\/)|#)/iu;
419
419
  // Every link opens in a new tab — an in-app path too, so following one never navigates the tab the
420
420
  // chat lives in (owner 2026-10-08). Only a `#` same-document anchor stays in place. ChatPane's
421
421
  // click guard (`useChatPaneNewTabLinks`) covers links this renderer does not draw.
@@ -3,10 +3,11 @@ import { type MessageRowProps } from './MessageRow.js';
3
3
  interface PendingPrompt {
4
4
  id?: string;
5
5
  text: string;
6
+ folders?: string[];
6
7
  attachments?: ChatAttachment[];
7
8
  onCancel: () => void;
8
9
  }
9
- export interface MessageListProps extends Pick<MessageRowProps, 'projectFileNames' | 'onRequestOpenFile' | 'renderAttachment' | 'showUsageTotal' | 'showUsageLine' | 'toolCallDisplay' | 'showThinking'> {
10
+ export interface MessageListProps extends Pick<MessageRowProps, 'projectFileNames' | 'onRequestOpenFile' | 'renderAttachment' | 'showUsageTotal' | 'showUsageLine' | 'toolCallDisplay' | 'showThinking' | 'sendingLabel'> {
10
11
  messages: ChatMessage[];
11
12
  isStreaming?: boolean;
12
13
  /** Mirrors `useConversation().scrollIntent` — when `true`, this component scrolls to the bottom on mount/update. */
@@ -10,6 +10,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
10
10
  * scroll-anchoring state lives in `useMessageListAutoScroll`
11
11
  * (`MessageList.hooks.ts`); see that file for why it is presentational.
12
12
  */
13
+ import { FolderChips } from './FolderChips.js';
13
14
  import { memo, useMemo } from 'react';
14
15
  import { projectAcceptedInputMessages } from '../../core/accepted-input.js';
15
16
  import { isTerminalRunStatus } from '../../core/index.js';
@@ -55,6 +56,6 @@ function QueuedMessage({ prompt, renderAttachment }) {
55
56
  const t = useT();
56
57
  const attachments = prompt.attachments ?? [];
57
58
  const gallery = useAttachmentGallery({ attachments });
58
- return (_jsxs("div", { className: "jini-chat-pane__queued", role: "status", "aria-label": t('Message queued — not yet sent, will send once the current run finishes'), "data-testid": "chat-pane-queued", "data-agent-element": "chat-message-pending", "data-agent-role": "region", "data-agent-label": "A message queued to send once the current run finishes", children: [_jsx("div", { children: t('Queued — sends when this reply finishes') }), _jsx("div", { className: "jini-chat-pane__queued-text", children: prompt.text }), attachments.length > 0 && _jsx("div", { className: "jini-message-attachments", children: attachments.map(attachment => _jsx(SentAttachmentButton, { attachment: attachment, onOpen: gallery.open, renderAttachment: renderAttachment }, attachment.path)) }), gallery.opened !== null && _jsx(AttachmentPreviewModal, { attachment: gallery.opened, onClose: gallery.close, navigation: gallery.navigation }), _jsx("div", { className: "jini-chat-pane__queued-actions", children: _jsx("button", { type: "button", className: "jini-chat-pane__queued-cancel", onClick: () => prompt.onCancel(), title: t('Cancel queued message'), children: t('Cancel') }) })] }));
59
+ return (_jsxs("div", { className: "jini-chat-pane__queued", role: "status", "aria-label": t('Message queued — not yet sent, will send once the current run finishes'), "data-testid": "chat-pane-queued", "data-agent-element": "chat-message-pending", "data-agent-role": "region", "data-agent-label": "A message queued to send once the current run finishes", children: [_jsx("div", { children: t('Queued — sends when this reply finishes') }), prompt.folders?.length ? _jsx("div", { className: "jini-message-attachments", children: _jsx(FolderChips, { folders: prompt.folders }) }) : null, _jsx("div", { className: "jini-chat-pane__queued-text", children: prompt.text }), attachments.length > 0 && _jsx("div", { className: "jini-message-attachments", children: attachments.map(attachment => _jsx(SentAttachmentButton, { attachment: attachment, onOpen: gallery.open, renderAttachment: renderAttachment }, attachment.path)) }), gallery.opened !== null && _jsx(AttachmentPreviewModal, { attachment: gallery.opened, onClose: gallery.close, navigation: gallery.navigation }), _jsx("div", { className: "jini-chat-pane__queued-actions", children: _jsx("button", { type: "button", className: "jini-chat-pane__queued-cancel", onClick: () => prompt.onCancel(), title: t('Cancel queued message'), children: t('Cancel') }) })] }));
59
60
  }
60
61
  //# sourceMappingURL=MessageList.js.map
@@ -73,6 +73,9 @@ export interface MessageRowProps {
73
73
  runStreaming?: boolean;
74
74
  /** Whether the server has yet to accept this reply's run (its user turn is still pending). */
75
75
  runSending?: boolean;
76
+ /** Host-localized text shown verbatim only while this run is active and sending.
77
+ * Omit to use the translated `Sending…` label. */
78
+ sendingLabel?: string;
76
79
  runSucceeded?: boolean;
77
80
  /** Whether this message's question-form (if any) is still the active/answerable one. */
78
81
  questionFormInteractive?: boolean;
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { FolderChips } from './FolderChips.js';
2
3
  import { projectAcceptedInputEvents } from '../../core/accepted-input.js';
3
4
  import { MidRunUserMessage, MID_RUN_USER_MESSAGE_EXT_EVENT_NAME } from './MidRunUserMessage.js';
4
5
  import { useSurfaceAnswerContexts } from './mcp-ui-question-input.js';
@@ -71,7 +72,7 @@ import { Markdown } from './Markdown.js';
71
72
  import { ToolCard } from './ToolCard.js';
72
73
  import { QuestionForm } from './QuestionForm.js';
73
74
  import { ToolCallGroup } from './ToolCallGroup.js';
74
- import { applyToolCallDisplay, toolRowsForDisplay } from '../tool-call-display.js';
75
+ import { applyToolCallDisplay, retriedToolRowIds, toolRowsForDisplay } from '../tool-call-display.js';
75
76
  /**
76
77
  * True while an assistant message is still fully pending — no visible text
77
78
  * and no tool rows have arrived yet. Shared by the "Thinking…" placeholder
@@ -157,9 +158,9 @@ function ThinkingBlock({ text }) {
157
158
  function messageAwaitingAnswer({ runStreaming, runInProgress, events }, { isAnsweredCard } = {}) {
158
159
  return runStreaming === true && runInProgress && isAwaitingAnswer({ events }, { isAnsweredCard });
159
160
  }
160
- function AssistantMessageFooter({ message, usageEvent, showUsageTotal, terminalNotice, pending, runInProgress, runStreaming, runSending, visibleContent, isAnsweredCard }, _optional = {}) {
161
+ function AssistantMessageFooter({ message, usageEvent, showUsageTotal, terminalNotice, pending, runInProgress, runStreaming, runSending, sendingLabel, visibleContent, isAnsweredCard }, _optional = {}) {
161
162
  const t = useT();
162
- return _jsxs(_Fragment, { children: [usageEvent ? _jsx(UsageSummary, { usage: usageEvent, showTotal: showUsageTotal, message: message, awaitingAnswer: isAwaitingAnswer({ events: message.events }, { isAnsweredCard }) }) : null, terminalNotice ? _jsx("div", { className: "jini-message-error", role: "status", children: t(terminalNotice) }) : null, !pending && !runInProgress ? (_jsx("div", { className: "jini-message-actions jini-message-actions--assistant", children: _jsx(CopyMessageButton, { text: visibleContent, label: t('Copy message') }) })) : null, runInProgress ? _jsx(RunActivityLine, { events: message.events, active: pending || (runStreaming === true && runInProgress), isAnsweredCard: isAnsweredCard, sending: runSending }) : null] });
163
+ return _jsxs(_Fragment, { children: [usageEvent ? _jsx(UsageSummary, { usage: usageEvent, showTotal: showUsageTotal, message: message, awaitingAnswer: isAwaitingAnswer({ events: message.events }, { isAnsweredCard }) }) : null, terminalNotice ? _jsx("div", { className: "jini-message-error", role: "status", children: t(terminalNotice) }) : null, !pending && !runInProgress ? (_jsx("div", { className: "jini-message-actions jini-message-actions--assistant", children: _jsx(CopyMessageButton, { text: visibleContent, label: t('Copy message') }) })) : null, runInProgress ? _jsx(RunActivityLine, { events: message.events, active: pending || (runStreaming === true && runInProgress), isAnsweredCard: isAnsweredCard, sending: runSending, sendingLabel: sendingLabel }) : null] });
163
164
  }
164
165
  /** Stable empty list for a message with no attachments. */
165
166
  const NO_ATTACHMENTS = [];
@@ -184,9 +185,9 @@ function UserMessageRow({ message, renderAttachment }) {
184
185
  // different messages opening their own attachments independently is fine and expected.
185
186
  // Previous/next steps through this message's images only.
186
187
  const gallery = useAttachmentGallery({ attachments: message.attachments ?? NO_ATTACHMENTS });
187
- return (_jsxs("div", { className: "jini-message jini-message-user", "data-message-id": message.id, "data-agent-element": `chat-message-${message.id}`, "data-agent-role": "region", "data-agent-label": "A message from the user", children: [message.attachments && message.attachments.length > 0 ? (_jsx("div", { className: "jini-message-attachments", children: message.attachments.map((a) => (_jsx(SentAttachmentButton, { attachment: a, onOpen: gallery.open, renderAttachment: renderAttachment }, a.path))) })) : null, _jsx("div", { className: "jini-message-content", children: message.content }), _jsx("div", { className: "jini-message-actions jini-message-actions--user", children: _jsx(CopyMessageButton, { text: message.content, label: t('Copy message') }) }), gallery.opened ? (_jsx(AttachmentPreviewModal, { attachment: gallery.opened, onClose: gallery.close, navigation: gallery.navigation })) : null] }));
188
+ return (_jsxs("div", { className: "jini-message jini-message-user", "data-message-id": message.id, "data-agent-element": `chat-message-${message.id}`, "data-agent-role": "region", "data-agent-label": "A message from the user", children: [(message.attachments?.length || message.folders?.length) ? (_jsxs("div", { className: "jini-message-attachments", children: [_jsx(FolderChips, { folders: message.folders }), (message.attachments ?? []).map((a) => (_jsx(SentAttachmentButton, { attachment: a, onOpen: gallery.open, renderAttachment: renderAttachment }, a.path)))] })) : null, _jsx("div", { className: "jini-message-content", children: message.content }), _jsx("div", { className: "jini-message-actions jini-message-actions--user", children: _jsx(CopyMessageButton, { text: message.content, label: t('Copy message') }) }), gallery.opened ? (_jsx(AttachmentPreviewModal, { attachment: gallery.opened, onClose: gallery.close, navigation: gallery.navigation })) : null] }));
188
189
  }
189
- function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageLine, toolCallDisplay, showThinking, runStreaming, runSending, runSucceeded, questionFormInteractive, questionFormSubmittedAnswers, onQuestionFormSubmit, projectFileNames, onRequestOpenFile, }) {
190
+ function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageLine, toolCallDisplay, showThinking, runStreaming, runSending, sendingLabel, runSucceeded, questionFormInteractive, questionFormSubmittedAnswers, onQuestionFormSubmit, projectFileNames, onRequestOpenFile, }) {
190
191
  const registry = useExtEventRendererRegistry();
191
192
  const messageUsage = useMessageUsage({ message, ...(usageSummary === undefined ? {} : { summary: usageSummary }) });
192
193
  const timeline = useToolTimeline({ events: message.events }, { runStreaming, runSucceeded });
@@ -206,7 +207,8 @@ function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageL
206
207
  });
207
208
  const footerUsage = showUsageLine ? usage.footer : undefined;
208
209
  const lastFinishedReply = !runInProgress && !usage.footer ? usage.replies.findLastIndex(value => value !== undefined) : -1;
209
- const renderToolCard = (row) => (_jsx(ToolCard, { use: row.use, result: row.result, runStreaming: runStreaming, runSucceeded: runSucceeded, ...(projectFileNames !== undefined ? { projectFileNames } : {}), ...(onRequestOpenFile !== undefined ? { onRequestOpenFile } : {}) }, row.id));
210
+ const retriedRows = retriedToolRowIds({ rows: timeline.rows });
211
+ const renderToolCard = (row) => (_jsx(ToolCard, { retried: retriedRows.has(row.id), use: row.use, result: row.result, runStreaming: runStreaming, runSucceeded: runSucceeded, ...(projectFileNames !== undefined ? { projectFileNames } : {}), ...(onRequestOpenFile !== undefined ? { onRequestOpenFile } : {}) }, row.id));
210
212
  // Hidden mode reuses the same disclosure and card renderer for unrecovered failures.
211
213
  const renderTools = (rows, key) => (toolCallDisplay !== 'show'
212
214
  ? _jsx(ToolCallGroup, { rows: rows, renderRow: renderToolCard, failureOnly: toolCallDisplay === 'hidden' }, key)
@@ -279,7 +281,7 @@ function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageL
279
281
  ? 'jini-message jini-message-assistant jini-message-assistant--usage-actions'
280
282
  : 'jini-message jini-message-assistant', "data-message-id": message.id, "data-run-status": message.runStatus, "data-agent-element": `chat-message-${message.id}`, "data-agent-role": "region", "data-agent-label": "A reply from the assistant", children: [message.agentName ? _jsx("div", { className: "jini-message-agent", children: message.agentName }) : null, section
281
283
  ? section.map(renderBlock)
282
- : (_jsxs(_Fragment, { children: [renderTextSegments(visibleContent, 'text'), flatTools.map((group) => renderTools(group.rows, group.key)), flatExtGroups.length > 0 ? _jsx("div", { className: "jini-message-ext-events", children: flatExtGroups.map(renderExtGroup) }) : null] })), footer && _jsx(AssistantMessageFooter, { message, usageEvent: footerUsage, showUsageTotal, terminalNotice, pending, runInProgress, runStreaming, runSending, visibleContent, isAnsweredCard: answeredCards.isAnsweredCard })] }, footer ? message.id : key));
284
+ : (_jsxs(_Fragment, { children: [renderTextSegments(visibleContent, 'text'), flatTools.map((group) => renderTools(group.rows, group.key)), flatExtGroups.length > 0 ? _jsx("div", { className: "jini-message-ext-events", children: flatExtGroups.map(renderExtGroup) }) : null] })), footer && _jsx(AssistantMessageFooter, { message, usageEvent: footerUsage, showUsageTotal, terminalNotice, pending, runInProgress, runStreaming, runSending, sendingLabel, visibleContent, isAnsweredCard: answeredCards.isAnsweredCard })] }, footer ? message.id : key));
283
285
  // The lossless fallback keeps the host's text intact; user receipts still own separate rows,
284
286
  // in their original receipt order, outside the assistant's actions and usage footer.
285
287
  // Keep the same child-array nesting as the interleaved path so React preserves the footer.
@@ -318,13 +320,14 @@ function isUserSection({ section }, _options = {}) {
318
320
  * The live "what is it doing" line under a running reply (see `core/run-activity.ts`). Replaces the
319
321
  * old "Thinking…" placeholder, which vanished for good as soon as the first tool row appeared.
320
322
  * Kept a separate component so its one-second tick re-renders only this line, not the whole row.
323
+ * A host's already-localized sending label is used verbatim until acceptance.
321
324
  */
322
- function RunActivityLine({ events, active, isAnsweredCard, sending }) {
325
+ function RunActivityLine({ events, active, isAnsweredCard, sending, sendingLabel }) {
323
326
  const t = useT();
324
327
  const activityLabel = useRunActivity({ events: events, active: active }, { isAnsweredCard });
325
328
  // Until the server accepts the start the turn is still on its way: "Thinking…" there read as the
326
329
  // model working while the text had not even arrived (owner, 2026-10-27). No timeout.
327
- const label = active && sending ? t('Sending…') : continuingRunNotice({ events, active }, {}) ?? activityLabel;
330
+ const label = active && sending ? sendingLabel ?? t('Sending…') : continuingRunNotice({ events, active }, {}) ?? activityLabel;
328
331
  if (label === null)
329
332
  return null;
330
333
  return (_jsx("div", { className: "jini-message-pending jini-run-activity", "aria-live": "off", children: label }));
@@ -10,6 +10,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
10
10
  * It is stored as an event of the assistant turn, not a `ChatMessage` of its own, because the agent folded it
11
11
  * into the turn it was already running — there is no separate reply to hang off it.
12
12
  */
13
+ import { FolderChips } from './FolderChips.js';
13
14
  import { acceptedInputEvent } from '../../core/accepted-input.js';
14
15
  import { SentAttachmentButton } from './SentAttachment.js';
15
16
  import { CopyMessageButton } from './CopyMessageButton.js';
@@ -37,7 +38,7 @@ export function MidRunUserMessage({ events, runStreaming = false, transcriptRow
37
38
  return null;
38
39
  const receipt = acceptedInputEvent({ event: { kind: 'ext', name: MID_RUN_USER_MESSAGE_EVENT_NAME, data } });
39
40
  // MOVE: A single live delivery may contain several queued inputs; display their original receipts.
40
- const inputs = receipt?.data.inputs ?? [{ id: field(data, 'id') ?? 'mid-run', text }];
41
+ const inputs = receipt?.data.inputs ?? [receipt?.data ?? { id: field(data, 'id') ?? 'mid-run', text }];
41
42
  // A run that ended without reading the message hands it to the next turn; it is no longer queued here.
42
43
  return _jsx(_Fragment, { children: inputs.map(input => _jsx(MidRunInput, { input: input, transcriptRow: transcriptRow, queued: runStreaming && (input.deliveryState ?? receipt?.data.deliveryState) === 'queued' }, input.id)) });
43
44
  }
@@ -48,7 +49,7 @@ function MidRunInput({ input, queued, transcriptRow }) {
48
49
  const t = useT();
49
50
  const attachments = input.attachments ?? [];
50
51
  const gallery = useAttachmentGallery({ attachments });
51
- return (_jsxs("div", { className: `jini-message jini-message-user${transcriptRow ? '' : ' jini-message-user--mid-run'}`, "data-agent-role": "region", "data-agent-label": "A message from the user, sent while the agent was working", children: [queued && _jsx("div", { className: "jini-message-user__queued", role: "status", children: t('Queued — read after the current step') }), _jsx("div", { className: "jini-message-content", children: input.text }), attachments.length > 0 && _jsx("div", { className: "jini-message-attachments", children: attachments.map(attachment => _jsx(SentAttachmentButton, { attachment: attachment, onOpen: gallery.open }, attachment.path)) }), _jsx("div", { className: "jini-message-actions jini-message-actions--user", children: _jsx(CopyMessageButton, { text: input.text, label: t('Copy message') }) }), gallery.opened !== null && _jsx(AttachmentPreviewModal, { attachment: gallery.opened, onClose: gallery.close, navigation: gallery.navigation })] }));
52
+ return (_jsxs("div", { className: `jini-message jini-message-user${transcriptRow ? '' : ' jini-message-user--mid-run'}`, "data-agent-role": "region", "data-agent-label": "A message from the user, sent while the agent was working", children: [queued && _jsx("div", { className: "jini-message-user__queued", role: "status", children: t('Queued — read after the current step') }), input.folders?.length ? _jsx("div", { className: "jini-message-attachments", children: _jsx(FolderChips, { folders: input.folders }) }) : null, _jsx("div", { className: "jini-message-content", children: input.text }), attachments.length > 0 && _jsx("div", { className: "jini-message-attachments", children: attachments.map(attachment => _jsx(SentAttachmentButton, { attachment: attachment, onOpen: gallery.open }, attachment.path)) }), _jsx("div", { className: "jini-message-actions jini-message-actions--user", children: _jsx(CopyMessageButton, { text: input.text, label: t('Copy message') }) }), gallery.opened !== null && _jsx(AttachmentPreviewModal, { attachment: gallery.opened, onClose: gallery.close, navigation: gallery.navigation })] }));
52
53
  }
53
54
  /**
54
55
  * Registers {@link MidRunUserMessage} against the ext-event registry.
@@ -16,7 +16,9 @@ export interface ToolCardProps {
16
16
  projectFileNames?: Set<string>;
17
17
  /** Lifts a basename up to the host so it can focus the matching tab in its own file viewer. */
18
18
  onRequestOpenFile?: (name: string) => void;
19
+ /** A later call of the same tool in this turn succeeded (`retriedToolRowIds`): a failure shows a neutral "Retried" badge, not red. */
20
+ retried?: boolean;
19
21
  }
20
- export declare function ToolCard({ use, result, runStreaming, runSucceeded, projectFileNames, onRequestOpenFile }: ToolCardProps): import("react").JSX.Element | null;
22
+ export declare function ToolCard({ use, result, runStreaming, runSucceeded, projectFileNames, onRequestOpenFile, retried }: ToolCardProps): import("react").JSX.Element | null;
21
23
  export {};
22
24
  //# sourceMappingURL=ToolCard.d.ts.map
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  const RUNNING_TITLE_CLASS = ' shimmer-text';
3
3
  /**
4
4
  * @module ToolCard
@@ -21,18 +21,29 @@ const RUNNING_TITLE_CLASS = ' shimmer-text';
21
21
  * `AskUserQuestion`) falls through to `GenericCard`, which is a correct,
22
22
  * generic rendering for it.
23
23
  */
24
- import { useId, useState } from 'react';
25
- import { COLLAPSED_DETAIL_MAX_CHARS, describeCollapsedToolCall, describeToolCallDetail, isDelegatedWrapperToolName, isTodoWriteToolName, parseTodoWriteInput, redactUserText, toRenderProps } from '../../core/index.js';
24
+ import { createContext, useContext, useId, useMemo, useState } from 'react';
25
+ import { COLLAPSED_DETAIL_MAX_CHARS, describeCollapsedToolCall, describeToolCallDetail, humanizeToolName, isDelegatedWrapperToolName, isTodoWriteToolName, parseTodoWriteInput, redactUserText, toRenderProps } from '../../core/index.js';
26
26
  import { useT, useToolRendererRegistry } from '../hooks/context.js';
27
27
  import { Icon } from './Icon.js';
28
+ import { SAFE_LINK_HREF } from './Markdown.js';
28
29
  import { TodoCard } from './TodoCard.js';
30
+ import { toolRowTarget } from './ToolCard.rules.js';
31
+ import { toolPresentation } from '../../core/tool-presentation.js';
32
+ /**
33
+ * Read by `ErrorBadge` only. A context, not one more prop threaded through every family card,
34
+ * because each card renders its own badge and all of them must agree.
35
+ */
36
+ const RetriedContext = createContext(false);
29
37
  /** 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. */
30
38
  const JINI_TOOL_ID_PATTERN = /^[a-z][a-z_]*(\.[a-z][a-z_]*)+$/;
31
- export function ToolCard({ use, result, runStreaming, runSucceeded, projectFileNames, onRequestOpenFile }) {
39
+ export function ToolCard({ use, result, runStreaming, runSucceeded, projectFileNames, onRequestOpenFile, retried = false }) {
32
40
  const registry = useToolRendererRegistry();
41
+ // Resolve vendor dialects for rendering only; tool ids and stored events stay intact.
42
+ const presentation = toolPresentation(use);
43
+ use = { ...use, ...presentation };
33
44
  const name = use.name;
34
45
  const card = { input: use.input, result: result, runStreaming: runStreaming ?? false, runSucceeded: runSucceeded ?? false };
35
- const content = renderToolCardBody({ use, card, fileCtx: { projectFileNames, onRequestOpenFile }, registry });
46
+ const content = renderToolCardBody({ use, card, wrapper: presentation.wrapper, fileCtx: { projectFileNames, onRequestOpenFile }, registry });
36
47
  if (content === null)
37
48
  return null;
38
49
  // Tagged once here, at the single dispatch point every branch below funnels through, rather than
@@ -40,7 +51,7 @@ export function ToolCard({ use, result, runStreaming, runSucceeded, projectFileN
40
51
  // host's own custom-registered renderer) gets it automatically. `use.id` is this tool call's own
41
52
  // stable identifier (unique per call within a message), never reused, so this handle is never
42
53
  // ambiguous the way a repeated literal handle would be.
43
- 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 }));
54
+ return (_jsx("div", { "data-agent-element": `tool-call-${use.id}`, "data-agent-role": "region", "data-agent-label": `A ${humanizeToolName({ name: name })} tool call the AI made`, children: _jsx(RetriedContext.Provider, { value: retried, children: content }) }));
44
55
  }
45
56
  // Hardcoded family cards for tools shipped with this package, keyed by every name each tool goes
46
57
  // by. A `Map`, not an object literal, so a tool named `constructor`/`toString` can never resolve
@@ -90,7 +101,8 @@ function renderToolCardBody(args) {
90
101
  // `useToolTimeline`: `foldDelegatedWrapperCalls` drops it in favour of that canonical row. This
91
102
  // check still covers a host that renders `<ToolCard>` straight from raw events.)
92
103
  // Covers the read-only gateway (`execute_readonly_delegated_tool`) too.
93
- if (isDelegatedWrapperToolName({ name: name }) && card.result && !card.result.isError)
104
+ // An outer transport receipt may have no bridge row; keep its inner call visible.
105
+ if (!args.wrapper && isDelegatedWrapperToolName({ name: name }) && card.result && !card.result.isError)
94
106
  return null;
95
107
  if (isTodoWriteToolName({ name: name }))
96
108
  return _jsx(TodoCard, { todos: parseTodoWriteInput({ input: use.input }), runStreaming: card.runStreaming });
@@ -108,28 +120,29 @@ function renderToolCardBody(args) {
108
120
  }
109
121
  return _jsx(GenericCard, { name: name, ...card });
110
122
  }
111
- /** `"page.fill"` -> `"Page Fill"`, `"daemon.db.vacuum"` -> `"Daemon Db Vacuum"`. Cosmetic only — never used to make a routing decision. */
112
- function humanizeToolId(id) {
113
- return id
114
- .split('.')
115
- .map((segment) => segment.split('_').map((word) => (word ? word[0].toUpperCase() + word.slice(1) : word)).join(' '))
116
- .join(' ');
117
- }
118
- /** The one argument that best identifies *what* a call acted on, for the card's collapsed summary — an element handle, a page id, or (for tools with neither) whatever the first string-valued argument is. */
119
- function primaryTarget(input) {
120
- if (input == null || typeof input !== 'object')
121
- return undefined;
122
- const obj = input;
123
- for (const key of ['element', 'page', 'handle', 'toolId', 'id']) {
124
- const v = obj[key];
125
- if (typeof v === 'string' && v)
126
- return v;
127
- }
128
- for (const v of Object.values(obj)) {
129
- if (typeof v === 'string' && v)
130
- return v;
131
- }
132
- return undefined;
123
+ /**
124
+ * The head every Jini/MCP/unknown tool row shares: status, "<Humanized Name>" (announced as
125
+ * "Tool call · <Humanized Name>"), and the
126
+ * target ({@link toolRowTarget}). One place, so a tool id the family table doesn't know (`content_post_update`,
127
+ * `mcp__tovu__theme_list_files`) still reads as a label and never as its raw name and JSON args.
128
+ */
129
+ function ToolRowHead({ name, input, result, runStreaming, runSucceeded }) {
130
+ const t = useT();
131
+ // Streaming deltas recreate result envelopes; parse only when the target's inputs change.
132
+ // A failed event's payload describes the error even when it contains human-looking names.
133
+ const resultContent = result?.content;
134
+ const resultIsError = result?.isError;
135
+ const target = useMemo(() => toolRowTarget({ input: input, resultContent: resultIsError ? undefined : resultContent }), [input, resultContent, resultIsError]);
136
+ const summary = target ? headSummary({ text: target }) : undefined;
137
+ const prefix = `${t('Tool call')} · `;
138
+ const toolName = humanizeToolName({ name: name });
139
+ const isRunning = runStreaming && !result;
140
+ // Name and target each carry their full text as a tooltip: on a narrow dock the name truncates
141
+ // first and the target is capped (styles.ts `.op-title`/`.op-meta`), so either can be cut.
142
+ // The "Tool call · " prefix is visually hidden (owner bug 11-01: it took half a ~333px dock row, so
143
+ // names read "Tool call · Cont…") but stays in the row's accessible name. The name's tooltip must
144
+ // not repeat it: trees that read a child's title before its text announced "Tool call · Tool call ·".
145
+ return (_jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: "op-sr-prefix", children: prefix }), _jsx("span", { className: `op-title${isRunning ? RUNNING_TITLE_CLASS : ''}`, title: toolName, children: toolName }), summary ? _jsx("span", { className: "op-meta op-target", title: summary, children: summary }) : null] }));
133
146
  }
134
147
  /**
135
148
  * A card for one `page.*`/`daemon.db.*`/`chat.*`/… call — the canonical event
@@ -139,15 +152,12 @@ function primaryTarget(input) {
139
152
  * blob this replaces.
140
153
  */
141
154
  function DelegatedToolCard({ name, input, result, runStreaming, runSucceeded }) {
142
- const t = useT();
143
155
  // Wrapper form (only reached on a failed wrapper call, per ToolCard's suppression comment):
144
156
  // the real id and args are nested under {toolId, input}, not `input` itself.
145
157
  const wrapper = (input ?? {});
146
158
  const toolId = isDelegatedWrapperToolName({ name: name }) ? (wrapper.toolId ?? name) : name;
147
159
  const args = isDelegatedWrapperToolName({ name: name }) ? wrapper.input : input;
148
- const target = primaryTarget(args);
149
- const isRunning = runStreaming && !result;
150
- return (_jsx(ExpandableCard, { className: "op-jini-tool", call: { input: args ?? {}, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }, head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsxs("span", { className: `op-title${isRunning ? RUNNING_TITLE_CLASS : ''}`, children: [t('Tool call'), " \u00B7 ", humanizeToolId(toolId)] }), target ? _jsx("span", { className: "op-meta", children: headSummary({ text: target }) }) : null] }), after: _jsx(FileErrorDetail, { result: result }), children: _jsx(ToolCallDetail, { input: args ?? {}, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded, children: _jsx(ToolResultMedia, { result: result }) }) }));
160
+ return (_jsx(ExpandableCard, { className: "op-jini-tool", call: { input: args ?? {}, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }, head: _jsx(ToolRowHead, { name: toolId, input: args, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), after: _jsx(FileErrorDetail, { result: result }), children: _jsx(ToolCallDetail, { input: args ?? {}, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded, children: _jsx(ToolResultMedia, { result: result }) }) }));
151
161
  }
152
162
  function SearchToolsCard({ input, result, runStreaming, runSucceeded }) {
153
163
  const t = useT();
@@ -236,8 +246,7 @@ function WebSearchCard({ input, result, runStreaming, runSucceeded }) {
236
246
  }
237
247
  /** Any tool this package has no family card for — an MCP tool, or a card tool such as `assistant_ask_choice` whose form renders elsewhere in the turn. The row still opens to the full call. */
238
248
  function GenericCard({ name, input, result, runStreaming, runSucceeded }) {
239
- const summary = describeInput(input);
240
- return (_jsx(ExpandableCard, { className: "op-generic", call: { input: input, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }, head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: "op-title", children: name }), summary ? _jsx("span", { className: "op-meta", children: headSummary({ text: summary }) }) : null] }), after: _jsx(ToolResultMedia, { result: result }), children: _jsx(ToolCallDetail, { input: input, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }) }));
249
+ return (_jsx(ExpandableCard, { className: "op-generic", call: { input: input, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }, head: _jsx(ToolRowHead, { name: name, input: input, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), after: _jsx(ToolResultMedia, { result: result }), children: _jsx(ToolCallDetail, { input: input, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }) }));
241
250
  }
242
251
  /**
243
252
  * The one row shell every card shares: a real `<button>` head (Enter/Space toggle it) that reports
@@ -255,7 +264,14 @@ function ExpandableCard({ className, head, after, call, children }) {
255
264
  }
256
265
  /** The collapsed row's stand-in for its panel — see {@link ExpandableCard} and `describeCollapsedToolCall`. */
257
266
  function CollapsedReaderText({ call }) {
258
- const text = describeCollapsedToolCall({ input: call.input, result: call.result }, { runStreaming: call.runStreaming, runSucceeded: call.runSucceeded });
267
+ const input = call.input;
268
+ const resultContent = call.result?.content;
269
+ const resultIsError = call.result?.isError ?? false;
270
+ // Streaming deltas recreate envelopes; only changed reader text needs formatting/redaction.
271
+ // Run state explains a missing result, but cannot change the text of a recorded result.
272
+ const runStreaming = !call.result && call.runStreaming;
273
+ const runSucceeded = !call.result && call.runSucceeded;
274
+ const text = useMemo(() => describeCollapsedToolCall({ input, result: resultContent === undefined ? undefined : { content: resultContent, isError: resultIsError } }, { runStreaming, runSucceeded }), [input, resultContent, resultIsError, runStreaming, runSucceeded]);
259
275
  return _jsx("span", { className: "op-card-reader-text", hidden: true, children: text });
260
276
  }
261
277
  /** The call's input, its result (or why there is none), and any extra `children` — see `describeToolCallDetail` for the redaction and formatting rules. */
@@ -266,6 +282,13 @@ function ToolCallDetail({ input, result, runStreaming, runSucceeded, children })
266
282
  const detail = describeToolCallDetail({ input: input, result: result }, { runStreaming: runStreaming, runSucceeded: runSucceeded, maxChars: Infinity });
267
283
  return (_jsxs("div", { className: "op-card-detail", children: [detail.inputText !== null ? _jsx("pre", { className: "op-command op-input", children: detail.inputText }) : null, detail.outputText !== null ? _jsx("pre", { className: "op-output", children: detail.outputText }) : null, detail.noteKey !== null ? _jsx("p", { className: "op-detail-note", children: t(detail.noteKey) }) : null, children] }));
268
284
  }
285
+ /** A failed result: neutral "Retried" when a later call of the same tool succeeded, else red. */
286
+ function ErrorBadge({ result }) {
287
+ const t = useT();
288
+ if (useContext(RetriedContext))
289
+ return (_jsx("span", { className: "op-status op-status-retried", title: t('Retried'), children: _jsx(Icon, { name: "retry", size: 14 }) }));
290
+ return (_jsx("span", { className: "op-status op-status-error", title: describeToolCallDetail({ input: null, result }, { maxChars: COLLAPSED_DETAIL_MAX_CHARS }).outputText ?? t('Error'), children: _jsx(Icon, { name: "close", size: 14 }) }));
291
+ }
269
292
  function ResultBadge({ result, runStreaming, runSucceeded }) {
270
293
  const t = useT();
271
294
  if (!result && runStreaming)
@@ -273,7 +296,7 @@ function ResultBadge({ result, runStreaming, runSucceeded }) {
273
296
  if (!result && !runSucceeded)
274
297
  return (_jsx("span", { className: "op-status op-status-error", title: t('Error'), children: _jsx(Icon, { name: "close", size: 14 }) }));
275
298
  if (result?.isError)
276
- return (_jsx("span", { className: "op-status op-status-error", title: describeToolCallDetail({ input: null, result }, { maxChars: COLLAPSED_DETAIL_MAX_CHARS }).outputText ?? t('Error'), children: _jsx(Icon, { name: "close", size: 14 }) }));
299
+ return _jsx(ErrorBadge, { result: result });
277
300
  return (_jsx("span", { className: "op-status op-status-ok", title: t('Done'), children: _jsx(Icon, { name: "check", size: 14 }) }));
278
301
  }
279
302
  /**
@@ -295,7 +318,23 @@ function ToolResultMedia({ result }) {
295
318
  const images = blocks.filter((block) => block.type === 'image');
296
319
  if (images.length === 0)
297
320
  return null;
298
- return (_jsx("div", { className: "op-media", children: images.map((block, index) => (_jsx("img", { className: "op-media-image", src: `data:${block.mimeType};base64,${block.data}`, alt: "", style: { maxWidth: '100%', height: 'auto', display: 'block' } }, index))) }));
321
+ return (_jsx("div", { className: "op-media", children: images.map((block, index) => _jsx(ToolResultImageFigure, { block: block }, index)) }));
322
+ }
323
+ /**
324
+ * One tool image: captioned by its `title` (also the alt text) and linked to its `href` in a new
325
+ * tab, so a picture a tool just saved opens full size. The href is tool output, so it gets the same
326
+ * safe-target rule as chat links (`Markdown.tsx`); `javascript:`/`data:` stays an unlinked image.
327
+ */
328
+ function ToolResultImageFigure({ block }) {
329
+ const img = (_jsx("img", { className: "op-media-image", src: `data:${block.mimeType};base64,${block.data}`, alt: block.title ?? '', style: { maxWidth: '100%', height: 'auto', display: 'block' } }));
330
+ const linked = block.href !== undefined && SAFE_LINK_HREF.test(block.href) && !block.href.startsWith('#')
331
+ ? _jsx("a", { className: "op-media-link", href: block.href, target: "_blank", rel: "noopener noreferrer", children: img })
332
+ : img;
333
+ if (block.title === undefined)
334
+ return linked;
335
+ return (
336
+ // No host stylesheet styles op-media yet; reset the UA figure margin (40px sides) inline, like the img.
337
+ _jsxs("figure", { className: "op-media-figure", style: { margin: 0 }, children: [linked, _jsx("figcaption", { className: "op-media-caption", children: block.title })] }));
299
338
  }
300
339
  function FileErrorDetail({ result }) {
301
340
  if (!result?.isError)
@@ -305,26 +344,6 @@ function FileErrorDetail({ result }) {
305
344
  const detail = describeToolCallDetail({ input: null, result }, { maxChars: COLLAPSED_DETAIL_MAX_CHARS });
306
345
  return detail.outputText === null ? null : _jsx("pre", { className: "op-output", children: detail.outputText });
307
346
  }
308
- function describeInput(input) {
309
- if (input == null)
310
- return '';
311
- if (typeof input === 'string')
312
- return input;
313
- if (typeof input !== 'object')
314
- return String(input);
315
- const obj = input;
316
- for (const key of ['file_path', 'path', 'pattern', 'url', 'query', 'name', 'command']) {
317
- const v = obj[key];
318
- if (typeof v === 'string')
319
- return v;
320
- }
321
- try {
322
- return JSON.stringify(obj);
323
- }
324
- catch {
325
- return '';
326
- }
327
- }
328
347
  /**
329
348
  * A head's one-line summary of an arbitrary argument. Heads stay mounted for every collapsed row,
330
349
  * and the fallback argument can be a 20k-char payload or a key, so it gets the reader text's
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @file What a generic tool row names as its target — pure, so `ToolCard.tsx`'s `ToolRowHead` only
3
+ * draws it. Moved out of `ToolCard.tsx` (11-01) when the target learned to read the result too.
4
+ */
5
+ /**
6
+ * What a tool row names as its target, for a person, best first: an argument that names it (see
7
+ * {@link SUMMARY_KEYS}: handle, title, slug, path, query…), the name the result returned, a shallow
8
+ * batch summary (after the result name, so an incidental array argument like `tags` never hides the
9
+ * record a single-record call returned), the record's kind ("post"), then any other readable string argument. Never the JSON of the whole
10
+ * object — that read as raw machine output in the head (`{"id":"30127d…","kind":"post",…`); the
11
+ * full arguments are in the expanded detail — and never an opaque id (owner bug 11-01: "Content
12
+ * Post Update 30127d31-…"). `undefined` when nothing reads as a target.
13
+ *
14
+ * @complexity O(k) in the argument and result key counts; one `JSON.parse` of the result.
15
+ */
16
+ export declare function toolRowTarget({ input, resultContent }: {
17
+ input: unknown;
18
+ resultContent?: string | undefined;
19
+ }): string | undefined;
20
+ //# sourceMappingURL=ToolCard.rules.d.ts.map
@@ -0,0 +1,92 @@
1
+ /**
2
+ * @file What a generic tool row names as its target — pure, so `ToolCard.tsx`'s `ToolRowHead` only
3
+ * draws it. Moved out of `ToolCard.tsx` (11-01) when the target learned to read the result too.
4
+ */
5
+ /** Argument keys that name *what* a call acted on, best first: an element handle or page, then a human title/name/slug, then a path or query, then an id. */
6
+ const SUMMARY_KEYS = ['element', 'page', 'handle', 'title', 'name', 'slug', 'file_path', 'path', 'pattern', 'url', 'query', 'command', 'toolId', 'id'];
7
+ /** Result fields that name a record for a person — what a write or read returns about its target. */
8
+ const RESULT_NAME_KEYS = ['title', 'name', 'slug'];
9
+ /** Argument keys that say what *kind* of thing a call acted on — the last-resort target. */
10
+ const KIND_KEYS = ['kind', 'entityType', 'type', 'collection'];
11
+ /** A UUID or a long bare hex id: meaningless to a person, so never shown as a row's target. */
12
+ const OPAQUE_ID = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|[0-9a-f]{16,})$/i;
13
+ function readable(value) {
14
+ return typeof value === 'string' && value !== '' && !OPAQUE_ID.test(value);
15
+ }
16
+ function firstReadable(obj, keys) {
17
+ for (const key of keys) {
18
+ const value = obj[key];
19
+ if (readable(value))
20
+ return value;
21
+ }
22
+ return undefined;
23
+ }
24
+ /** Failure metadata describes the error, never the acted-on record. @complexity O(1). */
25
+ function isErrorResult(obj) {
26
+ const hasError = obj.error != null && obj.error !== false;
27
+ const failedStatus = obj.status === 'failed' || obj.status === 'error';
28
+ return hasError || failedStatus || obj.isError === true || obj.success === false;
29
+ }
30
+ /**
31
+ * The record name a JSON tool result carries — at its top level or one object level down
32
+ * (`{ post: { title } }`). `undefined` for failures, non-JSON text or a result with no name field.
33
+ *
34
+ * @complexity O(n + k) for content length n and top-level key count k; one `JSON.parse`.
35
+ */
36
+ function resultName(content) {
37
+ if (!content || !content.trimStart().startsWith('{'))
38
+ return undefined;
39
+ let parsed;
40
+ try {
41
+ parsed = JSON.parse(content);
42
+ }
43
+ catch {
44
+ return undefined;
45
+ }
46
+ if (parsed == null || typeof parsed !== 'object')
47
+ return undefined;
48
+ const obj = parsed;
49
+ if (isErrorResult(obj))
50
+ return undefined;
51
+ const nested = Object.values(obj).filter((v) => v != null && typeof v === 'object' && !Array.isArray(v));
52
+ return firstReadable(obj, RESULT_NAME_KEYS) ?? nested.map((v) => firstReadable(v, RESULT_NAME_KEYS)).find(Boolean);
53
+ }
54
+ /** A batch member's own target: a readable string member, or an object member's summary key. O(k). */
55
+ function memberTarget(first) {
56
+ if (readable(first))
57
+ return first;
58
+ return first != null && typeof first === 'object' ? firstReadable(first, SUMMARY_KEYS) : undefined;
59
+ }
60
+ /** A shallow batch target, independent of CLI and collection key. No document-body traversal.
61
+ * O(k) in the envelope and first member's key counts; never scans or serializes all members.
62
+ */
63
+ function batchTarget(input) {
64
+ const members = Array.isArray(input) ? input : Object.values(input).find(Array.isArray);
65
+ if (!members?.length)
66
+ return undefined;
67
+ const target = memberTarget(members[0]);
68
+ if (!target)
69
+ return `${members.length} ${members.length === 1 ? 'item' : 'items'}`;
70
+ return members.length === 1 ? target : `${target} +${members.length - 1}`;
71
+ }
72
+ /**
73
+ * What a tool row names as its target, for a person, best first: an argument that names it (see
74
+ * {@link SUMMARY_KEYS}: handle, title, slug, path, query…), the name the result returned, a shallow
75
+ * batch summary (after the result name, so an incidental array argument like `tags` never hides the
76
+ * record a single-record call returned), the record's kind ("post"), then any other readable string argument. Never the JSON of the whole
77
+ * object — that read as raw machine output in the head (`{"id":"30127d…","kind":"post",…`); the
78
+ * full arguments are in the expanded detail — and never an opaque id (owner bug 11-01: "Content
79
+ * Post Update 30127d31-…"). `undefined` when nothing reads as a target.
80
+ *
81
+ * @complexity O(k) in the argument and result key counts; one `JSON.parse` of the result.
82
+ */
83
+ export function toolRowTarget({ input, resultContent }) {
84
+ if (typeof input === 'string')
85
+ return readable(input) ? input : undefined;
86
+ if (input != null && typeof input !== 'object')
87
+ return String(input);
88
+ const args = (input ?? {});
89
+ const named = firstReadable(args, SUMMARY_KEYS) ?? resultName(resultContent);
90
+ return named ?? batchTarget(args) ?? firstReadable(args, KIND_KEYS) ?? Object.values(args).find(readable);
91
+ }
92
+ //# sourceMappingURL=ToolCard.rules.js.map