@jini-ai/chat 0.6.5 → 0.6.7

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 (161) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/core/accepted-input.d.ts +54 -0
  3. package/dist/core/accepted-input.js +165 -0
  4. package/dist/core/agent-plugin-chat-state.d.ts +43 -0
  5. package/dist/core/agent-plugin-chat-state.js +48 -0
  6. package/dist/core/assistant-client/a2ui-action-poster.js +32 -28
  7. package/dist/core/assistant-client/messages.d.ts +1 -1
  8. package/dist/core/assistant-client/messages.js +2 -2
  9. package/dist/core/assistant-client/transport.d.ts +2 -2
  10. package/dist/core/assistant-client/transport.js +184 -97
  11. package/dist/core/assistant-content.d.ts +2 -1
  12. package/dist/core/assistant-content.js +9 -11
  13. package/dist/core/browser/page-actions.js +4 -2
  14. package/dist/core/composer-history.d.ts +16 -9
  15. package/dist/core/composer-history.js +28 -16
  16. package/dist/core/conversation-title.d.ts +3 -0
  17. package/dist/core/conversation-title.js +3 -0
  18. package/dist/core/durable-projection.js +20 -13
  19. package/dist/core/durable-runs/continuation.d.ts +4 -0
  20. package/dist/core/durable-runs/continuation.js +35 -6
  21. package/dist/core/durable-runs/ledger-contracts.d.ts +2 -1
  22. package/dist/core/durable-runs/policies.d.ts +1 -0
  23. package/dist/core/durable-runs/policies.js +1 -0
  24. package/dist/core/durable-runs/ports.d.ts +6 -2
  25. package/dist/core/index.d.ts +2 -0
  26. package/dist/core/index.js +2 -0
  27. package/dist/core/messages.d.ts +2 -0
  28. package/dist/core/partial-json.d.ts +6 -7
  29. package/dist/core/partial-json.js +81 -47
  30. package/dist/core/persistence/title.js +4 -2
  31. package/dist/core/question-answer.d.ts +23 -0
  32. package/dist/core/question-answer.js +66 -0
  33. package/dist/core/question-form/format.d.ts +6 -6
  34. package/dist/core/question-form/format.js +16 -17
  35. package/dist/core/question-form/normalize.d.ts +18 -0
  36. package/dist/core/question-form/normalize.js +112 -122
  37. package/dist/core/question-form/scan.js +141 -203
  38. package/dist/core/run-activity.js +62 -49
  39. package/dist/core/run-events/entry.d.ts +10 -1
  40. package/dist/core/run-events/entry.js +117 -87
  41. package/dist/core/todos.d.ts +1 -1
  42. package/dist/core/todos.js +60 -85
  43. package/dist/core/tool-events.js +14 -12
  44. package/dist/core/transcript.js +62 -16
  45. package/dist/core/turn-usage.d.ts +2 -1
  46. package/dist/core/turn-usage.js +56 -27
  47. package/dist/core/user-text-redaction.js +3 -0
  48. package/dist/core/util/manifest.js +77 -82
  49. package/dist/core/util/markdown-context.js +51 -53
  50. package/dist/core/util/parser.js +136 -144
  51. package/dist/core/util/pointer.js +16 -16
  52. package/dist/core/util/strip.js +57 -67
  53. package/dist/core/util/validate.js +15 -17
  54. package/dist/embed-widget/browser/session-store.js +21 -12
  55. package/dist/embed-widget/transports/fetch-sse.js +108 -95
  56. package/dist/embed-widget/vanilla/index.js +81 -70
  57. package/dist/react/agent-bridge/frontend-session-bridge.js +80 -52
  58. package/dist/react/components/AttachmentPreviewModal.js +25 -1
  59. package/dist/react/components/ChatSettingsDialog.d.ts +13 -0
  60. package/dist/react/components/ChatSettingsDialog.hooks.d.ts +115 -0
  61. package/dist/react/components/ChatSettingsDialog.hooks.js +40 -0
  62. package/dist/react/components/ChatSettingsDialog.js +72 -0
  63. package/dist/react/components/ChatSettingsSegmented.d.ts +18 -0
  64. package/dist/react/components/ChatSettingsSegmented.js +11 -0
  65. package/dist/react/components/Composer.d.ts +5 -1
  66. package/dist/react/components/Composer.js +126 -82
  67. package/dist/react/components/ConversationList.js +25 -39
  68. package/dist/react/components/ConversationTitleEditor.d.ts +27 -0
  69. package/dist/react/components/ConversationTitleEditor.js +54 -0
  70. package/dist/react/components/Icon.d.ts +1 -1
  71. package/dist/react/components/Icon.js +25 -45
  72. package/dist/react/components/Markdown.js +134 -123
  73. package/dist/react/components/McpUiSurfaceCard.d.ts +2 -2
  74. package/dist/react/components/McpUiSurfaceCard.js +30 -9
  75. package/dist/react/components/MessageList.d.ts +13 -13
  76. package/dist/react/components/MessageList.hooks.d.ts +6 -1
  77. package/dist/react/components/MessageList.hooks.js +19 -3
  78. package/dist/react/components/MessageList.js +34 -10
  79. package/dist/react/components/MessageRow.js +48 -32
  80. package/dist/react/components/MessageRow.usage.d.ts +5 -2
  81. package/dist/react/components/MessageRow.usage.js +20 -11
  82. package/dist/react/components/MidRunUserMessage.js +16 -3
  83. package/dist/react/components/QuestionForm.js +148 -76
  84. package/dist/react/components/QuestionsPanel.js +1 -1
  85. package/dist/react/components/TodoCard.js +12 -6
  86. package/dist/react/components/ToolCard.js +47 -40
  87. package/dist/react/components/composer-discovery.js +6 -1
  88. package/dist/react/components/mcp-ui-surface-state.d.ts +6 -11
  89. package/dist/react/components/mcp-ui-surface-state.js +23 -8
  90. package/dist/react/ext-event-renderer-registry.d.ts +8 -0
  91. package/dist/react/ext-event-renderer-registry.js +5 -2
  92. package/dist/react/features/chat-pane/components/AgentPluginUsage.d.ts +7 -0
  93. package/dist/react/features/chat-pane/components/AgentPluginUsage.js +45 -0
  94. package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +55 -19
  95. package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
  96. package/dist/react/features/chat-pane/components/ChatPane.js +192 -74
  97. package/dist/react/features/chat-pane/components/RuntimeDropUp.d.ts +29 -0
  98. package/dist/react/features/chat-pane/components/RuntimeDropUp.js +45 -0
  99. package/dist/react/features/chat-pane/create-daemon-attachment-uploader.js +80 -47
  100. package/dist/react/features/chat-pane/create-mcp-ui-tool-caller.js +44 -40
  101. package/dist/react/features/chat-pane/create-mid-run-message-poster.d.ts +6 -6
  102. package/dist/react/features/chat-pane/create-mid-run-message-poster.js +8 -8
  103. package/dist/react/features/chat-pane/create-typed-answer-poster.d.ts +3 -3
  104. package/dist/react/features/chat-pane/create-typed-answer-poster.js +3 -3
  105. package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.d.ts +6 -1
  106. package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +11 -5
  107. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +12 -7
  108. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +340 -149
  109. package/dist/react/features/chat-pane/hooks/useChatPaneControlsHeight.hooks.js +7 -6
  110. package/dist/react/features/chat-pane/hooks/useChatPaneNewTabLinks.hooks.js +28 -10
  111. package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.d.ts +3 -3
  112. package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +60 -12
  113. package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.d.ts +49 -0
  114. package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.js +143 -0
  115. package/dist/react/features/chat-pane/model-options.d.ts +18 -1
  116. package/dist/react/features/chat-pane/model-options.js +49 -20
  117. package/dist/react/features/chat-pane/rules.d.ts +8 -9
  118. package/dist/react/features/chat-pane/rules.js +84 -46
  119. package/dist/react/features/chat-pane/styles.d.ts +1 -1
  120. package/dist/react/features/chat-pane/styles.js +156 -37
  121. package/dist/react/features/chat-pane/types.d.ts +17 -8
  122. package/dist/react/hooks/assistant-chats.js +12 -5
  123. package/dist/react/hooks/composer-draft-cache.js +116 -118
  124. package/dist/react/hooks/composer-history-storage.js +5 -5
  125. package/dist/react/hooks/fab-position.js +7 -4
  126. package/dist/react/hooks/useAnsweredCards.d.ts +21 -4
  127. package/dist/react/hooks/useAnsweredCards.js +38 -11
  128. package/dist/react/hooks/useArtifactStream.js +28 -22
  129. package/dist/react/hooks/useComposer.d.ts +20 -0
  130. package/dist/react/hooks/useComposer.js +22 -9
  131. package/dist/react/hooks/useComposerHistory.hooks.d.ts +6 -1
  132. package/dist/react/hooks/useComposerHistory.hooks.js +39 -19
  133. package/dist/react/hooks/useComposerHistoryKeys.hooks.js +24 -14
  134. package/dist/react/hooks/useConversation.d.ts +7 -0
  135. package/dist/react/hooks/useConversation.js +23 -10
  136. package/dist/react/hooks/useExtEventGroups.js +39 -39
  137. package/dist/react/hooks/useQuestionForms.js +21 -26
  138. package/dist/react/hooks/useRunStream.js +10 -2
  139. package/dist/react/index.d.ts +7 -1
  140. package/dist/react/index.js +9 -0
  141. package/dist/react/message-blocks.d.ts +6 -4
  142. package/dist/react/message-blocks.js +94 -72
  143. package/dist/react/slots.d.ts +2 -2
  144. package/dist/react/tool-call-display.js +19 -17
  145. package/dist/server/durable-runs/recover.d.ts +1 -1
  146. package/dist/server/durable-runs/recover.js +139 -76
  147. package/dist/server/run-finalizer/entry.js +25 -15
  148. package/dist/store/legacy/sqlite/conversations.d.ts +11 -11
  149. package/dist/store/legacy/sqlite/conversations.js +69 -98
  150. package/dist/store/legacy/sqlite/messages.d.ts +10 -2
  151. package/dist/store/legacy/sqlite/messages.js +113 -71
  152. package/dist/store/runtime/chat-history-store.js +9 -3
  153. package/dist/store/runtime/durable-run-store.d.ts +2 -2
  154. package/dist/store/runtime/durable-run-store.js +165 -117
  155. package/dist/store/runtime/history-tools.js +118 -82
  156. package/dist/store/runtime/run-ledger.d.ts +4 -1
  157. package/dist/store/runtime/run-ledger.js +56 -16
  158. package/dist/store/sql/cursor.js +38 -18
  159. package/dist/store/sql/store.d.ts +18 -0
  160. package/dist/store/sql/store.js +46 -15
  161. package/package.json +12 -10
@@ -20,7 +20,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
20
20
  import { useRef, useState } from 'react';
21
21
  import { useT } from '../hooks/context.js';
22
22
  import { QuestionForm } from './QuestionForm.js';
23
- export function QuestionsPanel({ form, interactive, submitDisabled = false, submittedAnswers, generating = false, onSubmit, redactUserText, onSecretRedacted }) {
23
+ export function QuestionsPanel({ form, interactive, submitDisabled, submittedAnswers, generating = false, onSubmit, redactUserText, onSecretRedacted }) {
24
24
  const t = useT();
25
25
  const formRef = useRef(null);
26
26
  const [ready, setReady] = useState(false);
@@ -12,18 +12,24 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
12
12
  import { useState } from 'react';
13
13
  import { useT } from '../hooks/context.js';
14
14
  import { Icon } from './Icon.js';
15
- export function TodoCard({ todos, runStreaming = false, onDismiss }) {
15
+ export function TodoCard({ todos, runStreaming, onDismiss }) {
16
16
  const t = useT();
17
- const hasInProgress = todos.some((todo) => todo.status === 'in_progress');
18
- const hasPending = todos.some((todo) => todo.status === 'pending' || todo.status === 'in_progress');
19
- const defaultExpanded = todos.length > 0 && (hasInProgress || hasPending || runStreaming);
17
+ const { defaultExpanded, inProgressTodo, done, allComplete } = todoProgress({ todos }, { runStreaming });
20
18
  const [overrideExpanded, setOverrideExpanded] = useState(null);
21
19
  const expanded = overrideExpanded ?? defaultExpanded;
20
+ const showDismiss = Boolean(onDismiss) && allComplete;
21
+ return (_jsxs("div", { className: `op-card op-todo${expanded ? '' : ' op-todo-collapsed'}`, children: [_jsxs("div", { className: "op-card-head op-todo-head", children: [_jsxs("button", { type: "button", className: "op-todo-toggle", "aria-expanded": expanded, onClick: () => setOverrideExpanded(!expanded), title: expanded ? t('Collapse todos') : t('Expand todos'), children: [_jsx("span", { className: "op-icon", "aria-hidden": true, children: "\u2610" }), _jsx("span", { className: "op-title", children: t('Todos') }), _jsxs("span", { className: "op-meta", children: [done, "/", todos.length] }), !expanded && inProgressTodo ? _jsx("span", { className: "op-todo-current", children: todoLabel({ todo: inProgressTodo }) }) : null, _jsx("span", { className: "op-todo-chev", "aria-hidden": true, children: expanded ? _jsx(Icon, { name: "chevron-down", size: 11 }) : _jsx(Icon, { name: "chevron-right", size: 11 }) })] }), showDismiss ? (_jsx("button", { type: "button", className: "op-todo-done", onClick: () => onDismiss?.(), title: t('Dismiss'), children: t('Done') })) : null] }), _jsx("div", { className: `accordion-collapsible${expanded ? ' open' : ''}`, children: _jsx("div", { className: "accordion-collapsible-inner", children: _jsx("ul", { className: "todo-list", children: todos.map((todo, i) => (_jsxs("li", { className: `todo-item todo-${todo.status}`, children: [_jsx("span", { className: "todo-check", "aria-hidden": true, children: todo.status === 'completed' ? '✓' : todo.status === 'in_progress' ? '◐' : todo.status === 'stopped' ? '!' : '○' }), _jsx("span", { className: "todo-text", children: todoLabel({ todo }) })] }, i))) }) }) })] }));
22
+ }
23
+ function todoProgress({ todos }, { runStreaming = false } = {}) {
24
+ const hasPending = todos.some((todo) => todo.status === 'pending' || todo.status === 'in_progress');
25
+ const defaultExpanded = todos.length > 0 && (hasPending || runStreaming);
22
26
  const inProgressTodo = todos.find((todo) => todo.status === 'in_progress');
23
27
  const completed = todos.filter((todo) => todo.status === 'completed').length;
24
28
  const done = todos.filter((todo) => todo.status === 'completed' || todo.status === 'in_progress').length;
25
29
  const allComplete = todos.length > 0 && completed === todos.length;
26
- const showDismiss = Boolean(onDismiss) && allComplete;
27
- return (_jsxs("div", { className: `op-card op-todo${expanded ? '' : ' op-todo-collapsed'}`, children: [_jsxs("div", { className: "op-card-head op-todo-head", children: [_jsxs("button", { type: "button", className: "op-todo-toggle", "aria-expanded": expanded, onClick: () => setOverrideExpanded(!expanded), title: expanded ? t('Collapse todos') : t('Expand todos'), children: [_jsx("span", { className: "op-icon", "aria-hidden": true, children: "\u2610" }), _jsx("span", { className: "op-title", children: t('Todos') }), _jsxs("span", { className: "op-meta", children: [done, "/", todos.length] }), !expanded && inProgressTodo ? _jsx("span", { className: "op-todo-current", children: inProgressTodo.activeForm || inProgressTodo.content }) : null, _jsx("span", { className: "op-todo-chev", "aria-hidden": true, children: expanded ? _jsx(Icon, { name: "chevron-down", size: 11 }) : _jsx(Icon, { name: "chevron-right", size: 11 }) })] }), showDismiss ? (_jsx("button", { type: "button", className: "op-todo-done", onClick: () => onDismiss?.(), title: t('Dismiss'), children: t('Done') })) : null] }), _jsx("div", { className: `accordion-collapsible${expanded ? ' open' : ''}`, children: _jsx("div", { className: "accordion-collapsible-inner", children: _jsx("ul", { className: "todo-list", children: todos.map((todo, i) => (_jsxs("li", { className: `todo-item todo-${todo.status}`, children: [_jsx("span", { className: "todo-check", "aria-hidden": true, children: todo.status === 'completed' ? '✓' : todo.status === 'in_progress' ? '◐' : todo.status === 'stopped' ? '!' : '○' }), _jsx("span", { className: "todo-text", children: todo.status === 'in_progress' && todo.activeForm ? todo.activeForm : todo.content })] }, i))) }) }) })] }));
30
+ return { defaultExpanded, inProgressTodo, done, allComplete };
31
+ }
32
+ function todoLabel({ todo }, _options = {}) {
33
+ return todo.status === 'in_progress' && todo.activeForm ? todo.activeForm : todo.content;
28
34
  }
29
35
  //# sourceMappingURL=TodoCard.js.map
@@ -27,14 +27,11 @@ import { Icon } from './Icon.js';
27
27
  import { TodoCard } from './TodoCard.js';
28
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. */
29
29
  const JINI_TOOL_ID_PATTERN = /^[a-z][a-z_]*(\.[a-z][a-z_]*)+$/;
30
- const SEARCH_TOOLS_NAMES = new Set(['search_tools', 'mcp__jini__search_tools']);
31
- const DESCRIBE_TOOL_NAMES = new Set(['describe_tool', 'mcp__jini__describe_tool']);
32
30
  export function ToolCard({ use, result, runStreaming, runSucceeded, projectFileNames, onRequestOpenFile }) {
33
31
  const registry = useToolRendererRegistry();
34
32
  const name = use.name;
35
- const isStreaming = runStreaming ?? false;
36
- const isSucceeded = runSucceeded ?? false;
37
- const content = renderToolCardBody(name, use, result, isStreaming, isSucceeded, { projectFileNames, onRequestOpenFile }, registry);
33
+ const card = { input: use.input, result: result, runStreaming: runStreaming ?? false, runSucceeded: runSucceeded ?? false };
34
+ const content = renderToolCardBody({ use, card, fileCtx: { projectFileNames, onRequestOpenFile }, registry });
38
35
  if (content === null)
39
36
  return null;
40
37
  // Tagged once here, at the single dispatch point every branch below funnels through, rather than
@@ -44,18 +41,42 @@ export function ToolCard({ use, result, runStreaming, runSucceeded, projectFileN
44
41
  // ambiguous the way a repeated literal handle would be.
45
42
  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
43
  }
47
- function renderToolCardBody(name, use, result, isStreaming, isSucceeded, fileCtx, registry) {
48
- const custom = registry.get({ name });
49
- if (custom) {
50
- try {
51
- const node = custom(toRenderProps({ use: use, result: result, runStreaming: isStreaming }, { runSucceeded: isSucceeded }));
52
- if (node !== undefined && node !== null && node !== false)
53
- return node;
54
- }
55
- catch (err) {
56
- console.error(`[ToolCard] custom renderer for "${name}" threw; falling back`, err);
57
- }
44
+ // Hardcoded family cards for tools shipped with this package, keyed by every name each tool goes
45
+ // by. A `Map`, not an object literal, so a tool named `constructor`/`toString` can never resolve
46
+ // to an inherited `Object.prototype` member.
47
+ const FAMILY_CARDS = new Map([
48
+ [['Write', 'write', 'create_file'], (card, fileCtx) => _jsx(FileWriteCard, { ...card, ctx: fileCtx })],
49
+ [['Edit', 'str_replace_edit'], (card, fileCtx) => _jsx(FileEditCard, { ...card, ctx: fileCtx })],
50
+ [['Read', 'read_file'], (card, fileCtx) => _jsx(FileReadCard, { ...card, ctx: fileCtx })],
51
+ [['Bash'], (card) => _jsx(BashCard, { ...card })],
52
+ [['Glob', 'list_files'], (card) => _jsx(GlobCard, { ...card })],
53
+ [['Grep'], (card) => _jsx(GrepCard, { ...card })],
54
+ [['WebFetch', 'web_fetch'], (card) => _jsx(WebFetchCard, { ...card })],
55
+ [['WebSearch', 'web_search'], (card) => _jsx(WebSearchCard, { ...card })],
56
+ [['search_tools', 'mcp__jini__search_tools'], (card) => _jsx(SearchToolsCard, { ...card })],
57
+ [['describe_tool', 'mcp__jini__describe_tool'], (card) => _jsx(DescribeToolCard, { ...card })],
58
+ ].flatMap(([names, render]) => names.map((name) => [name, render])));
59
+ /** A host-registered renderer's output, or `undefined` when none is registered, it renders nothing, or it throws (logged) — every one of those falls back to the built-in cards. */
60
+ function renderRegistered({ use, card, registry }) {
61
+ const custom = registry.get({ name: use.name });
62
+ if (!custom)
63
+ return undefined;
64
+ try {
65
+ const node = custom(toRenderProps({ use: use, result: card.result, runStreaming: card.runStreaming }, { runSucceeded: card.runSucceeded }));
66
+ if (node !== undefined && node !== null && node !== false)
67
+ return node;
68
+ }
69
+ catch (err) {
70
+ console.error(`[ToolCard] custom renderer for "${use.name}" threw; falling back`, err);
58
71
  }
72
+ return undefined;
73
+ }
74
+ function renderToolCardBody(args) {
75
+ const custom = renderRegistered(args);
76
+ if (custom !== undefined)
77
+ return custom;
78
+ const { use, card, fileCtx } = args;
79
+ const name = use.name;
59
80
  // `execute_delegated_tool` is a transport wrapper: `@jini-ai/daemon`'s `delegated-tool-bridge.ts`
60
81
  // always emits a second, canonical `tool_use` named after the real Jini tool id (`page.fill`,
61
82
  // not `execute_delegated_tool`) alongside it — before calling `ToolExecutor`, so it exists even
@@ -68,37 +89,23 @@ function renderToolCardBody(name, use, result, isStreaming, isSucceeded, fileCtx
68
89
  // `useToolTimeline`: `foldDelegatedWrapperCalls` drops it in favour of that canonical row. This
69
90
  // check still covers a host that renders `<ToolCard>` straight from raw events.)
70
91
  // Covers the read-only gateway (`execute_readonly_delegated_tool`) too.
71
- if (isDelegatedWrapperToolName({ name: name }) && result && !result.isError)
92
+ if (isDelegatedWrapperToolName({ name: name }) && card.result && !card.result.isError)
72
93
  return null;
73
94
  if (isTodoWriteToolName({ name: name }))
74
- return _jsx(TodoCard, { todos: parseTodoWriteInput({ input: use.input }), runStreaming: isStreaming });
75
- if (name === 'Write' || name === 'write' || name === 'create_file')
76
- return _jsx(FileWriteCard, { input: use.input, result: result, runStreaming: isStreaming, runSucceeded: isSucceeded, ctx: fileCtx });
77
- if (name === 'Edit' || name === 'str_replace_edit')
78
- return _jsx(FileEditCard, { input: use.input, result: result, runStreaming: isStreaming, runSucceeded: isSucceeded, ctx: fileCtx });
79
- if (name === 'Read' || name === 'read_file')
80
- return _jsx(FileReadCard, { input: use.input, result: result, runStreaming: isStreaming, runSucceeded: isSucceeded, ctx: fileCtx });
81
- if (name === 'Bash')
82
- return _jsx(BashCard, { input: use.input, result: result, runStreaming: isStreaming, runSucceeded: isSucceeded });
83
- if (name === 'Glob' || name === 'list_files')
84
- return _jsx(GlobCard, { input: use.input, result: result, runStreaming: isStreaming, runSucceeded: isSucceeded });
85
- if (name === 'Grep')
86
- return _jsx(GrepCard, { input: use.input, result: result, runStreaming: isStreaming, runSucceeded: isSucceeded });
87
- if (name === 'WebFetch' || name === 'web_fetch')
88
- return _jsx(WebFetchCard, { input: use.input, result: result, runStreaming: isStreaming, runSucceeded: isSucceeded });
89
- if (name === 'WebSearch' || name === 'web_search')
90
- return _jsx(WebSearchCard, { input: use.input, result: result, runStreaming: isStreaming, runSucceeded: isSucceeded });
95
+ return _jsx(TodoCard, { todos: parseTodoWriteInput({ input: use.input }), runStreaming: card.runStreaming });
96
+ // Jini tool ids are dotted and wrapper names are `execute_*delegated_tool`, so neither ever
97
+ // collides with a family name — the search/describe cards can sit in the same table as the rest
98
+ // even though they are looked up ahead of the Jini-id check below.
99
+ const family = FAMILY_CARDS.get(name);
100
+ if (family)
101
+ return family(card, fileCtx);
91
102
  if (JINI_TOOL_ID_PATTERN.test(name) || isDelegatedWrapperToolName({ name: name })) {
92
103
  // The unsuppressed (failed-wrapper) case falls through here too: `name` is still
93
104
  // `execute_delegated_tool`, so `jiniToolIdFromInput` recovers the real id from
94
105
  // `{toolId, input}` instead of `DelegatedToolCard` receiving its own wrapper name as the title.
95
- return _jsx(DelegatedToolCard, { name: name, input: use.input, result: result, runStreaming: isStreaming, runSucceeded: isSucceeded });
106
+ return _jsx(DelegatedToolCard, { name: name, ...card });
96
107
  }
97
- if (SEARCH_TOOLS_NAMES.has(name))
98
- return _jsx(SearchToolsCard, { input: use.input, result: result, runStreaming: isStreaming, runSucceeded: isSucceeded });
99
- if (DESCRIBE_TOOL_NAMES.has(name))
100
- return _jsx(DescribeToolCard, { input: use.input, result: result, runStreaming: isStreaming, runSucceeded: isSucceeded });
101
- return _jsx(GenericCard, { name: name, input: use.input, result: result, runStreaming: isStreaming, runSucceeded: isSucceeded });
108
+ return _jsx(GenericCard, { name: name, ...card });
102
109
  }
103
110
  /** `"page.fill"` -> `"Page Fill"`, `"daemon.db.vacuum"` -> `"Daemon Db Vacuum"`. Cosmetic only — never used to make a routing decision. */
104
111
  function humanizeToolId(id) {
@@ -9,8 +9,13 @@ export function parseComposerSlashQuery({ draft }) {
9
9
  const match = SLASH_QUERY_RE.exec(draft);
10
10
  if (!match)
11
11
  return null;
12
+ // The `as string` casts are guaranteed by SLASH_QUERY_RE itself, not assumed: group 1 is a bare
13
+ // `*` capture outside any optional group, so it always participates (possibly as ''); group 3 is
14
+ // `*` inside the same optional group as group 2, so it participates whenever a separator does.
15
+ // `noUncheckedIndexedAccess` cannot see either guarantee, and the `?? ''` fallbacks that used to
16
+ // satisfy it were unreachable branches.
12
17
  const [, command, separator, rest] = match;
13
- return { command: command ?? '', argument: separator === undefined ? null : (rest ?? '') };
18
+ return { command: command, argument: separator === undefined ? null : rest };
14
19
  }
15
20
  /**
16
21
  * Once a separator has been typed, only an item whose declared `command` exactly equals the typed
@@ -1,10 +1,3 @@
1
- /**
2
- * @module components/mcp-ui-surface-state
3
- *
4
- * Whether an MCP-UI card in a chat transcript is closed, and with which label. Kept out of
5
- * `McpUiSurfaceCard.tsx` so the component only renders; moved here from that file 2026-10-04 when
6
- * the card's own deadline (`useSurfaceExpiry`) became a second reason to close it.
7
- */
8
1
  import type { McpUiToolCallHandler } from '@jini-ai/ui/mcp-ui';
9
2
  import type { ExtEventCall } from '../ext-event-renderer-registry.js';
10
3
  import type { useT } from '../hooks/context.js';
@@ -35,14 +28,16 @@ export declare function closedSurfaceLabel({ t, call, runStreaming, documentsFor
35
28
  export declare function isDeliveredAnswer(result: unknown): boolean;
36
29
  /**
37
30
  * Wraps a card's tool executor so a delivered answer is reported once it resolves. The result is
38
- * passed through unchanged, and a rejected call reports nothing (the dialog shows its error and can
39
- * be answered again).
31
+ * passed through unchanged; onAnswerText receives readable choice text when available. A rejected call
32
+ * reports nothing (the dialog shows its error and can be answered again).
40
33
  *
41
- * @complexity O(1) per call, plus `onToolCall`'s own cost.
34
+ * @complexity O(p + s*m) answer-label mapping, plus `onToolCall`'s own cost (see questionAnswerText).
42
35
  */
43
- export declare function reportDeliveredAnswer({ onToolCall, onAnswered }: {
36
+ export declare function reportDeliveredAnswer({ onToolCall, onAnswered, onAnswerText, input }: {
44
37
  onToolCall: McpUiToolCallHandler;
45
38
  onAnswered: () => void;
39
+ onAnswerText?: ((answerText: string) => void) | undefined;
40
+ input?: unknown;
46
41
  }): McpUiToolCallHandler;
47
42
  export {};
48
43
  //# sourceMappingURL=mcp-ui-surface-state.d.ts.map
@@ -1,3 +1,11 @@
1
+ /**
2
+ * @module components/mcp-ui-surface-state
3
+ *
4
+ * Whether an MCP-UI card in a chat transcript is closed, and with which label. Kept out of
5
+ * `McpUiSurfaceCard.tsx` so the component only renders; moved here from that file 2026-10-04 when
6
+ * the card's own deadline (`useSurfaceExpiry`) became a second reason to close it.
7
+ */
8
+ import { questionAnswerText } from '../../core/question-answer.js';
1
9
  /** Words in a finished call's result that mean nobody answered in time. */
2
10
  const UNANSWERED_RESULT = /\b(expired|abandoned|did not respond|run ended)\b/i;
3
11
  /**
@@ -6,17 +14,20 @@ const UNANSWERED_RESULT = /\b(expired|abandoned|did not respond|run ended)\b/i;
6
14
  * A held-open question (the tool shows the card and waits) is over once its tool call returns, and
7
15
  * dead once its run ends without that return. Its first document stays on screen either way, so
8
16
  * without this an expired, answered or orphaned question still looked answerable (host stuck-chat
9
- * investigation, 2026-09-27). Only a card still showing that FIRST document closes: a tool that sent
10
- * a follow-up document for the same `ui://` URI (an outcome after the answer) keeps showing it.
17
+ * investigation, 2026-09-27). A completed tool's follow-up document for the same `ui://` URI remains
18
+ * visible as its outcome; repeated documents without a tool result cannot reopen an orphaned question.
11
19
  *
12
20
  * The labels are read from the call's result text, so they are a best guess for a producer this
13
21
  * package has not seen; the card is closed either way, which is the property that matters.
14
22
  */
15
23
  function settledLabel(t, call, runStreaming, documentsForUri) {
16
- if (call === undefined || documentsForUri > 1)
24
+ if (call === undefined)
17
25
  return undefined;
26
+ // MOVE: Replayed surface documents cannot keep an orphaned question live after its run ends.
18
27
  if (call.result === undefined)
19
28
  return runStreaming ? undefined : t('This question expired');
29
+ if (documentsForUri > 1)
30
+ return undefined;
20
31
  return UNANSWERED_RESULT.test(call.result.content) ? t('This question expired') : t('Answered');
21
32
  }
22
33
  /**
@@ -43,16 +54,20 @@ export function isDeliveredAnswer(result) {
43
54
  }
44
55
  /**
45
56
  * Wraps a card's tool executor so a delivered answer is reported once it resolves. The result is
46
- * passed through unchanged, and a rejected call reports nothing (the dialog shows its error and can
47
- * be answered again).
57
+ * passed through unchanged; onAnswerText receives readable choice text when available. A rejected call
58
+ * reports nothing (the dialog shows its error and can be answered again).
48
59
  *
49
- * @complexity O(1) per call, plus `onToolCall`'s own cost.
60
+ * @complexity O(p + s*m) answer-label mapping, plus `onToolCall`'s own cost (see questionAnswerText).
50
61
  */
51
- export function reportDeliveredAnswer({ onToolCall, onAnswered }) {
62
+ export function reportDeliveredAnswer({ onToolCall, onAnswered, onAnswerText, input }) {
52
63
  return async (call) => {
53
64
  const result = await onToolCall(call);
54
- if (isDeliveredAnswer(result))
65
+ if (isDeliveredAnswer(result)) {
66
+ const text = questionAnswerText({ answer: call.arguments, input });
67
+ if (text !== undefined)
68
+ onAnswerText?.(text);
55
69
  onAnswered();
70
+ }
56
71
  return result;
57
72
  };
58
73
  }
@@ -26,6 +26,8 @@ import type { ReactNode } from 'react';
26
26
  * the call is still waiting, or its run ended before it returned.
27
27
  */
28
28
  export interface ExtEventCall {
29
+ /** Stable owning tool identity for matching persisted answer receipts. */
30
+ readonly id?: string;
29
31
  readonly name: string;
30
32
  readonly input: unknown;
31
33
  readonly result?: {
@@ -58,6 +60,12 @@ export interface ExtEventRenderProps {
58
60
  * the work the answer started, and the run must stop reading as "waiting for your answer".
59
61
  */
60
62
  onAnswered?: () => void;
63
+ /** Optional readable answer delivered through the card; emitted before onAnswered. */
64
+ onAnswerText?: (answerText: string) => void;
65
+ /** Delivered answer text while the owning tool is still finishing. */
66
+ answerText?: string;
67
+ /** A matching user_message receipt already owns the visible bubble. */
68
+ answerRecorded?: boolean;
61
69
  }
62
70
  /**
63
71
  * Ext-event render callback. Runs inline during `MessageRow`'s render — hook-free, like
@@ -1,3 +1,4 @@
1
+ import { acceptedInputEvent } from '../core/accepted-input.js';
1
2
  /** Create independent extension renderer state, including each protocol's slot grouping.
2
3
  * @returns A keyed registry with identity-safe unregister handles.
3
4
  * @example const extensions = createExtEventRendererRegistry({}); extensions.register({ name: 'card', renderer });
@@ -26,8 +27,10 @@ export function createExtEventRendererRegistry(_required = {}) {
26
27
  */
27
28
  function slot({ name, data }) {
28
29
  const slotKey = renderers.get(name)?.slotKey;
29
- if (!slotKey)
30
- return name;
30
+ if (!slotKey) {
31
+ const input = acceptedInputEvent({ event: { kind: 'ext', name, data } });
32
+ return input ? `${name}:${input.data.id}` : name;
33
+ }
31
34
  let key;
32
35
  try {
33
36
  key = slotKey({ data });
@@ -0,0 +1,7 @@
1
+ import type { ChatMessage } from '../../../../core/messages.js';
2
+ /** One per-chat usage strip; removing a name persists its exclusion for future turns. */
3
+ export declare function AgentPluginUsage({ conversationId, messages }: {
4
+ conversationId?: string | null;
5
+ messages: readonly ChatMessage[];
6
+ }): import("react").JSX.Element | null;
7
+ //# sourceMappingURL=AgentPluginUsage.d.ts.map
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import { AGENT_PLUGIN_STATE_CHANGED, readAgentPluginChatState, writeAgentPluginChatState } from '../../../../core/agent-plugin-chat-state.js';
4
+ import { useT } from '../../../hooks/context.js';
5
+ /** The newest `agent_plugins` usage event across the transcript, if any turn reported one. */
6
+ function findLatestAgentPluginUsage(messages) {
7
+ let latest;
8
+ for (const message of messages)
9
+ for (const event of message.events ?? []) {
10
+ if (event.kind === 'ext' && event.name === 'agent_plugins')
11
+ latest = event.data;
12
+ }
13
+ return latest;
14
+ }
15
+ /** What the strip shows for this chat: the latest usage's plugins minus the ones the human
16
+ * excluded — or `null` when the latest usage belongs to another chat or nothing is left. */
17
+ function resolveAgentPluginStrip({ messages }, { conversationId }) {
18
+ const latest = findLatestAgentPluginUsage(messages);
19
+ const id = conversationId ?? latest?.conversationId;
20
+ if (!id || !latest || latest.conversationId !== id)
21
+ return null;
22
+ const state = readAgentPluginChatState({ conversationId: id });
23
+ const plugins = latest.plugins.filter(plugin => !state.excluded.includes(plugin.id));
24
+ if (!plugins.length)
25
+ return null;
26
+ return { id, state, plugins };
27
+ }
28
+ /** One per-chat usage strip; removing a name persists its exclusion for future turns. */
29
+ export function AgentPluginUsage({ conversationId, messages }) {
30
+ const t = useT();
31
+ const [revision, setRevision] = useState(0);
32
+ useEffect(() => {
33
+ const refresh = () => setRevision(value => value + 1);
34
+ window.addEventListener(AGENT_PLUGIN_STATE_CHANGED, refresh);
35
+ window.addEventListener('storage', refresh);
36
+ return () => { window.removeEventListener(AGENT_PLUGIN_STATE_CHANGED, refresh); window.removeEventListener('storage', refresh); };
37
+ }, []);
38
+ void revision;
39
+ const strip = resolveAgentPluginStrip({ messages }, { conversationId });
40
+ if (!strip)
41
+ return null;
42
+ const { id, state, plugins } = strip;
43
+ return _jsxs("div", { role: "status", style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 6, padding: '6px 12px', fontSize: 12 }, children: [_jsx("span", { children: t('Using:') }), plugins.map(plugin => _jsxs("span", { style: { display: 'inline-flex', alignItems: 'center', gap: 4, border: '1px solid currentColor', borderRadius: 12, padding: '1px 7px' }, children: [plugin.name, _jsx("button", { type: "button", "aria-label": `${t('Stop using')} ${plugin.name}`, title: t('Stops future use in this chat. Guidance already loaded stays in the current context.'), onClick: () => writeAgentPluginChatState({ conversationId: id, state: { ...state, excluded: [...state.excluded, plugin.id] } }), style: { background: 'none', border: 0, color: 'inherit', cursor: 'pointer', padding: '0 2px', fontSize: 16 }, children: "\u00D7" })] }, plugin.id))] });
44
+ }
45
+ //# sourceMappingURL=AgentPluginUsage.js.map
@@ -1,8 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { pickerModelValue, pickerCatalogNote, pickerModelOptions } from '../model-options.js';
2
+ import { pickerModelValue, pickerCatalogNote, pickerModelOptions, shortModelLabel } from '../model-options.js';
3
3
  import { createPortal } from 'react-dom';
4
4
  import { AgentIcon, RemixIcon, SearchableModelSelect } from '@jini-ai/ui';
5
5
  import { useT } from '../../../hooks/context.js';
6
+ import { definedProps } from '../../../util/defined-props.js';
7
+ import { RuntimeDropUp } from './RuntimeDropUp.js';
6
8
  import { defaultChatPaneSelection } from '../rules.js';
7
9
  import { runtimeOptionLabel, runtimePopoverPosition, useAgentRuntimePicker, } from '../hooks/useAgentRuntimePicker.hooks.js';
8
10
  // Re-exported so this module's public surface is unchanged by the behavior extraction; both are
@@ -25,23 +27,34 @@ function RuntimeAgentList({ orderedAgents, value, onChange, agentIconBasePath, t
25
27
  return (_jsx(_Fragment, { children: orderedAgents.map((agent) => {
26
28
  const active = agent.id === value.agentId;
27
29
  const status = runtimeAgentStatus({ agent: agent });
28
- return (_jsxs("button", { type: "button", role: "radio", "aria-checked": active, className: `jini-runtime-agent${active ? ' is-active' : ''}`, "aria-current": active ? 'true' : undefined, "aria-label": `${agent.name} · ${status}`, onClick: () => onChange(defaultChatPaneSelection({ agent: agent })), children: [_jsx(AgentIcon, { id: agent.id, size: 20, className: "jini-runtime-agent-icon", ...(agentIconBasePath === undefined ? {} : { basePath: agentIconBasePath }) }), _jsx("span", { className: "jini-runtime-agent__copy", children: _jsx("strong", { children: agent.name }) }), agent.supportsTools === false ? (_jsx("span", { className: "jini-runtime-agent__badge", title: t("This CLI has no MCP support, so it cannot use this app's tools."), children: t('No tools') })) : null, _jsx("span", { className: "jini-runtime-agent__status", children: active ? t('selected') : status }), active ? (_jsx(RemixIcon, { name: "check-line", size: 19, className: "jini-runtime-check" })) : null] }, agent.id));
30
+ return (_jsxs("button", { type: "button", role: "radio", "aria-checked": active, className: `jini-runtime-agent${active ? ' is-active' : ''}`, "aria-current": active ? 'true' : undefined, "aria-label": `${agent.name} · ${status}`, onClick: () => onChange(defaultChatPaneSelection({ agent: agent })), children: [_jsx(AgentIcon, { id: agent.id, size: 20, className: "jini-runtime-agent-icon", ...definedProps({ source: { basePath: agentIconBasePath } }) }), _jsx("span", { className: "jini-runtime-agent__copy", children: _jsx("strong", { children: agent.name }) }), agent.supportsTools === false ? (_jsx("span", { className: "jini-runtime-agent__badge", title: t("This CLI has no MCP support, so it cannot use this app's tools."), children: t('No tools') })) : null, _jsx("span", { className: "jini-runtime-agent__status", children: active ? t('selected') : status }), active ? (_jsx(RemixIcon, { name: "check-line", size: 19, className: "jini-runtime-check" })) : null] }, agent.id));
29
31
  }) }));
30
32
  }
31
- function RuntimeModelSelects({ selectedAgent, value, onChange, t }) {
33
+ /**
34
+ * The composer bar's "Model ⌄" and "Effort ⌄" drop-ups for the selected CLI. These replace the
35
+ * Model / Reasoning selects that used to sit in the runtime popover under the agent list (owner
36
+ * 2026-10-18: pick them from the bar and keep the popover small); they read and write the same
37
+ * selection, so nothing else changed. Either one renders nothing when the agent publishes no
38
+ * choices for it, rather than an empty menu.
39
+ */
40
+ function RuntimeBarMenus({ selectedAgent, value, onChange, modelLabel, t }) {
32
41
  if (!selectedAgent)
33
42
  return null;
34
43
  const models = pickerModelOptions(selectedAgent);
35
- const note = pickerCatalogNote(selectedAgent, t);
36
- return (_jsxs("div", { className: "jini-runtime-models", children: [note ? _jsx("small", { children: note }) : null, models.length > 0 ? (_jsxs("label", { className: "jini-runtime-select", children: [_jsx("span", { children: t('Model') }), _jsxs("select", { "aria-label": t('Model'), value: pickerModelValue(selectedAgent, value.model), onChange: (event) => onChange({
37
- agentId: value.agentId,
38
- model: event.target.value,
39
- ...(value.reasoning ? { reasoning: value.reasoning } : {}),
40
- }), children: [_jsx("option", { value: "", disabled: true, children: t('Choose model') }), models.map((model) => (_jsx("option", { value: model.id, children: model.label }, model.id)))] })] })) : null, (selectedAgent.reasoningOptions?.length ?? 0) > 0 ? (_jsxs("label", { className: "jini-runtime-select", children: [_jsx("span", { children: t('Reasoning') }), _jsx("select", { "aria-label": t('Reasoning'), value: value.reasoning ?? '', onChange: (event) => onChange({
41
- agentId: value.agentId,
42
- ...(value.model ? { model: value.model } : {}),
43
- reasoning: event.target.value,
44
- }), children: selectedAgent.reasoningOptions?.map((option) => (_jsx("option", { value: option.id, children: option.label }, option.id))) })] })) : null] }));
44
+ const efforts = selectedAgent.reasoningOptions ?? [];
45
+ return (_jsxs(_Fragment, { children: [models.length > 0 ? (_jsx(RuntimeDropUp, { label: t('Model'), valueLabel: modelLabel,
46
+ // The agent icon sits right beside it, so the bar drops the vendor word and caps the rest.
47
+ triggerLabel: shortModelLabel({ label: modelLabel, agentId: selectedAgent.id }), value: pickerModelValue(selectedAgent, value.model), options: models, note: pickerCatalogNote(selectedAgent, t), width: 260, onSelect: (model) => onChange({
48
+ agentId: value.agentId,
49
+ model,
50
+ ...(value.reasoning ? { reasoning: value.reasoning } : {}),
51
+ }) })) : null, efforts.length > 0 ? (_jsx(RuntimeDropUp, { label: t('Reasoning'),
52
+ // An unset reasoning shows the first option, as the old native select did.
53
+ valueLabel: efforts.find((option) => option.id === value.reasoning)?.label ?? efforts[0].label, value: value.reasoning ?? '', options: efforts, width: 180, className: "jini-runtime-dropup-trigger--effort", onSelect: (reasoning) => onChange({
54
+ agentId: value.agentId,
55
+ ...(value.model ? { model: value.model } : {}),
56
+ reasoning,
57
+ }) })) : null] }));
45
58
  }
46
59
  /**
47
60
  * What the popover shows in place of the CLI inventory while `executionMode` is `'api'`: the model
@@ -64,17 +77,21 @@ function RuntimeModelSelects({ selectedAgent, value, onChange, t }) {
64
77
  * disabled dropdown would read as "editable once something is enabled" when nothing here will
65
78
  * enable it.
66
79
  */
67
- function RuntimeByokDetails({ byokRuntime, onByokModelChange, t }) {
80
+ function resolveByokModel({ onByokModelChange }, { byokRuntime }) {
68
81
  const model = byokRuntime?.model?.trim();
69
82
  const models = byokRuntime?.models ?? [];
70
- const editable = onByokModelChange !== undefined && models.length > 0;
71
- return (_jsxs("div", { className: "jini-runtime-models", children: [_jsxs("div", { className: "jini-runtime-select jini-runtime-byok-model", children: [_jsx("span", { children: t('Model') }), editable ? (_jsx(SearchableModelSelect, { models: models, value: model ?? '', onChange: (next) => onByokModelChange(next), ariaLabel: t('Model'), searchPlaceholder: t('Search models'),
83
+ const pickModel = onByokModelChange !== undefined && models.length > 0 ? onByokModelChange : undefined;
84
+ return { model, models, pickModel };
85
+ }
86
+ function RuntimeByokDetails({ byokRuntime, onByokModelChange, t }) {
87
+ const { model, models, pickModel } = resolveByokModel({ onByokModelChange }, { byokRuntime });
88
+ return (_jsxs("div", { className: "jini-runtime-models", children: [_jsxs("div", { className: "jini-runtime-select jini-runtime-byok-model", children: [_jsx("span", { children: t('Model') }), pickModel ? (_jsx(SearchableModelSelect, { models: models, value: model ?? '', onChange: (next) => pickModel(next), ariaLabel: t('Model'), searchPlaceholder: t('Search models'),
72
89
  // The menu portals to `document.body` at the package default `z-index: 60`, while this
73
90
  // popover positions itself at 1000 — so without this the model list opens BEHIND the
74
91
  // popover that owns it. Raised here rather than in the shared stylesheet because 60 is
75
92
  // right for every other consumer; only this one opens a portalled menu from inside an
76
93
  // already-elevated surface.
77
- menuClassName: "jini-runtime-model-menu" })) : (_jsx("strong", { children: model || t('No model configured') }))] }), !model && !editable ? (_jsx("div", { className: "jini-runtime-empty", children: t('Set a model in the BYOK settings before sending a message.') })) : null] }));
94
+ menuClassName: "jini-runtime-model-menu" })) : (_jsx("strong", { children: model || t('No model configured') }))] }), !model && !pickModel ? (_jsx("div", { className: "jini-runtime-empty", children: t('Set a model in the BYOK settings before sending a message.') })) : null] }));
78
95
  }
79
96
  function RuntimeRescanFooter({ onRescan, scanning, t }) {
80
97
  if (onRescan) {
@@ -85,6 +102,25 @@ function RuntimeRescanFooter({ onRescan, scanning, t }) {
85
102
  }
86
103
  return null;
87
104
  }
105
+ function RuntimeTriggerIcon({ picker, isApi, byokRuntime, agentIconBasePath }) {
106
+ const basePath = definedProps({ source: { basePath: agentIconBasePath } });
107
+ if (!isApi) {
108
+ return _jsx(AgentIcon, { id: picker.selectedAgent?.id ?? '', size: 22, className: "jini-runtime-agent-icon", ...basePath });
109
+ }
110
+ if (byokRuntime?.iconId) {
111
+ return _jsx(AgentIcon, { id: byokRuntime.iconId, size: 22, className: "jini-runtime-agent-icon", ...basePath });
112
+ }
113
+ return _jsx(RemixIcon, { name: "link", size: 20, className: "jini-runtime-agent-icon" });
114
+ }
115
+ function RuntimeTrigger({ picker, isApi, byokRuntime, agentIconBasePath, t }) {
116
+ return (_jsxs("button", { ref: picker.triggerRef, type: "button", className: isApi ? 'jini-runtime-trigger' : 'jini-runtime-trigger jini-runtime-trigger--icon-only', "aria-label": t('Choose AI runtime'), "aria-haspopup": "dialog", "aria-expanded": picker.open, "aria-controls": picker.dialogId, title: `${picker.triggerTitle} · ${picker.modelLabel}`, onClick: picker.toggleOpen, children: [_jsx(RuntimeTriggerIcon, { picker: picker, isApi: isApi, byokRuntime: byokRuntime, agentIconBasePath: agentIconBasePath }), _jsxs("span", { className: "jini-runtime-trigger__copy", children: [_jsx("strong", { children: picker.triggerTitle }), isApi ? _jsx("small", { children: picker.modelLabel }) : null] }), isApi ? (_jsx(RemixIcon, { name: "arrow-down-s-line", size: 15, className: "jini-runtime-trigger__chevron" })) : null] }));
117
+ }
118
+ function RuntimePopover({ picker, value, onChange, onRescan, scanning, daemonOnline, executionMode, apiModeAvailable, onExecutionModeChange, byokRuntime, onByokModelChange, agentIconBasePath, t, }) {
119
+ if (!picker.open || !picker.position)
120
+ return null;
121
+ const isApi = executionMode === 'api';
122
+ return createPortal(_jsxs("div", { ref: picker.popoverRef, className: "jini-runtime-popover", ...(picker.originTheme ? { 'data-theme': picker.originTheme } : {}), id: picker.dialogId, role: "dialog", "aria-label": t('Choose AI runtime'), "aria-modal": "false", style: picker.position, onKeyDown: picker.handlePopoverKeyDown, children: [_jsxs("div", { className: "jini-runtime-popover__head", children: [_jsx("strong", { children: executionMode === 'local' ? t('Local CLI') : t('API · BYOK') }), _jsx("span", { children: picker.runtimeSummary })] }), _jsx(RuntimeModeButtons, { executionMode: executionMode, daemonOnline: daemonOnline, apiModeAvailable: apiModeAvailable, onExecutionModeChange: onExecutionModeChange, t: t }), isApi ? (_jsx(RuntimeByokDetails, { byokRuntime: byokRuntime, onByokModelChange: onByokModelChange, t: t })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "jini-runtime-section-label", children: t('Code agent') }), _jsx("div", { role: "radiogroup", "aria-label": t('Code agent'), children: _jsx(RuntimeAgentList, { orderedAgents: picker.orderedAgents, value: value, onChange: onChange, ...definedProps({ source: { agentIconBasePath } }), t: t }) }), _jsx(RuntimeRescanFooter, { onRescan: onRescan, scanning: scanning, t: t })] }))] }), document.body);
123
+ }
88
124
  /**
89
125
  * Renders the package-owned local/API, agent, model, and reasoning picker.
90
126
  *
@@ -97,13 +133,13 @@ export function AgentRuntimePicker({ agents, value, onChange, onRescan, scanning
97
133
  // its own asset directory can pass this explicitly, same as before.
98
134
  agentIconBasePath, }) {
99
135
  const t = useT();
100
- const { dialogId, open, position, triggerRef, popoverRef, orderedAgents, selectedAgent, modelLabel, runtimeSummary, triggerTitle, originTheme, toggleOpen, handlePopoverKeyDown, } = useAgentRuntimePicker({ agents, value, placement, t, executionMode, byokRuntime });
136
+ const picker = useAgentRuntimePicker({ agents, value, placement, t, executionMode, byokRuntime });
101
137
  // The one branch this component makes. Everything below the mode buttons describes a detected CLI
102
138
  // — the agent radios, its model and reasoning selects, and the PATH rescan — and none of it is
103
139
  // true of, or has any effect on, an API turn. Previously all of it rendered in both modes, so an
104
140
  // operator on BYOK saw a CLI named as "selected" and a model reading "Default (CLI config)"
105
141
  // while their messages went to a provider over an API key.
106
142
  const isApi = executionMode === 'api';
107
- return (_jsxs("div", { className: "jini-runtime-picker", children: [_jsxs("button", { ref: triggerRef, type: "button", className: "jini-runtime-trigger", "aria-label": t('Choose AI runtime'), "aria-haspopup": "dialog", "aria-expanded": open, "aria-controls": dialogId, onClick: toggleOpen, children: [isApi ? (byokRuntime?.iconId ? (_jsx(AgentIcon, { id: byokRuntime.iconId, size: 22, className: "jini-runtime-agent-icon", ...(agentIconBasePath === undefined ? {} : { basePath: agentIconBasePath }) })) : (_jsx(RemixIcon, { name: "link", size: 20, className: "jini-runtime-agent-icon" }))) : (_jsx(AgentIcon, { id: selectedAgent?.id ?? '', size: 22, className: "jini-runtime-agent-icon", ...(agentIconBasePath === undefined ? {} : { basePath: agentIconBasePath }) })), _jsxs("span", { className: "jini-runtime-trigger__copy", children: [_jsx("strong", { children: triggerTitle }), _jsx("small", { children: modelLabel })] }), _jsx(RemixIcon, { name: "arrow-down-s-line", size: 15, className: "jini-runtime-trigger__chevron" })] }), open && position ? createPortal(_jsxs("div", { ref: popoverRef, className: "jini-runtime-popover", ...(originTheme ? { 'data-theme': originTheme } : {}), id: dialogId, role: "dialog", "aria-label": t('Choose AI runtime'), "aria-modal": "false", style: position, onKeyDown: handlePopoverKeyDown, children: [_jsxs("div", { className: "jini-runtime-popover__head", children: [_jsx("strong", { children: executionMode === 'local' ? t('Local CLI') : t('API · BYOK') }), _jsx("span", { children: runtimeSummary })] }), _jsx(RuntimeModeButtons, { executionMode: executionMode, daemonOnline: daemonOnline, apiModeAvailable: apiModeAvailable, onExecutionModeChange: onExecutionModeChange, t: t }), isApi ? (_jsx(RuntimeByokDetails, { byokRuntime: byokRuntime, onByokModelChange: onByokModelChange, t: t })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "jini-runtime-section-label", children: t('Code agent') }), _jsx("div", { role: "radiogroup", "aria-label": t('Code agent'), children: _jsx(RuntimeAgentList, { orderedAgents: orderedAgents, value: value, onChange: onChange, ...(agentIconBasePath === undefined ? {} : { agentIconBasePath }), t: t }) }), _jsx(RuntimeModelSelects, { selectedAgent: selectedAgent, value: value, onChange: onChange, t: t }), _jsx(RuntimeRescanFooter, { onRescan: onRescan, scanning: scanning, t: t })] }))] }), document.body) : null] }));
143
+ return (_jsxs("div", { className: "jini-runtime-picker", children: [_jsx(RuntimeTrigger, { picker: picker, isApi: isApi, byokRuntime: byokRuntime, agentIconBasePath: agentIconBasePath, t: t }), isApi ? null : (_jsx(RuntimeBarMenus, { selectedAgent: picker.selectedAgent, value: value, onChange: onChange, modelLabel: picker.modelLabel, t: t })), _jsx(RuntimePopover, { picker: picker, value: value, onChange: onChange, onRescan: onRescan, scanning: scanning, daemonOnline: daemonOnline, executionMode: executionMode, apiModeAvailable: apiModeAvailable, onExecutionModeChange: onExecutionModeChange, byokRuntime: byokRuntime, onByokModelChange: onByokModelChange, agentIconBasePath: agentIconBasePath, t: t })] }));
108
144
  }
109
145
  //# sourceMappingURL=AgentRuntimePicker.js.map
@@ -6,5 +6,5 @@ import type { ChatPaneProps } from '../types.js';
6
6
  * @complexity Time/space: O(n) in rendered messages, agents, and suggestions.
7
7
  * @overallScore 100/100
8
8
  */
9
- export declare function ChatPane({ transport, agents: injectedAgents, runtimeAccess, runtimeStatusPollMs, title, variant, initialMessages, refreshSignal, conversationId, initialSelection, selection: controlledSelection, onSelectionChange, runContext, agentControl, onActivityChange, onMessagesChange, redactUserText, onSecretRedacted, onRescanAgents, scanningAgents, daemonOnline, executionMode, apiModeAvailable, onExecutionModeChange, byokRuntime, onByokModelChange, initialDraft, composerHistoryScope, composerHistoryStorage, validateAttachments, deliverTypedAnswer, deliverMidRunMessage, composerHandle, placeholder, placeholders, suggestions, emptyState, workingDirectory, initialWorkingDirectory, onChangeWorkingDirectory, workingDirectoryAccess, workingDirectoryControlPlacement, projectFileNames, toolCallDisplay, showUsageLine, showThinking, uploadAttachments, attachmentAccept, disabled, runtimePickerPlacement, composerSlots, header, leadingAccessory, footer, className, style, }: ChatPaneProps): import("react").JSX.Element;
9
+ export declare function ChatPane(props: ChatPaneProps): import("react").JSX.Element;
10
10
  //# sourceMappingURL=ChatPane.d.ts.map