@jini-ai/chat 0.6.6 → 0.6.10

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 (160) hide show
  1. package/CHANGELOG.md +129 -1
  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 +187 -98
  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 +31 -6
  21. package/dist/core/durable-runs/ledger-contracts.d.ts +2 -1
  22. package/dist/core/durable-runs/ports.d.ts +6 -2
  23. package/dist/core/index.d.ts +2 -0
  24. package/dist/core/index.js +2 -0
  25. package/dist/core/messages.d.ts +2 -0
  26. package/dist/core/partial-json.d.ts +6 -7
  27. package/dist/core/partial-json.js +81 -47
  28. package/dist/core/persistence/title.js +4 -2
  29. package/dist/core/question-answer.d.ts +23 -0
  30. package/dist/core/question-answer.js +66 -0
  31. package/dist/core/question-form/format.d.ts +6 -6
  32. package/dist/core/question-form/format.js +16 -17
  33. package/dist/core/question-form/normalize.d.ts +18 -0
  34. package/dist/core/question-form/normalize.js +112 -122
  35. package/dist/core/question-form/scan.js +141 -203
  36. package/dist/core/run-activity.js +62 -49
  37. package/dist/core/run-events/entry.d.ts +10 -1
  38. package/dist/core/run-events/entry.js +117 -87
  39. package/dist/core/todos.d.ts +1 -1
  40. package/dist/core/todos.js +60 -85
  41. package/dist/core/tool-events.js +14 -12
  42. package/dist/core/transcript.js +62 -16
  43. package/dist/core/turn-usage.js +54 -32
  44. package/dist/core/user-text-redaction.js +3 -0
  45. package/dist/core/util/manifest.js +77 -82
  46. package/dist/core/util/markdown-context.js +51 -53
  47. package/dist/core/util/parser.js +136 -144
  48. package/dist/core/util/pointer.js +16 -16
  49. package/dist/core/util/strip.js +57 -67
  50. package/dist/core/util/validate.js +15 -17
  51. package/dist/embed-widget/browser/session-store.js +21 -12
  52. package/dist/embed-widget/transports/fetch-sse.js +108 -95
  53. package/dist/embed-widget/vanilla/index.js +81 -70
  54. package/dist/react/agent-bridge/frontend-session-bridge.js +80 -52
  55. package/dist/react/components/AttachmentPreviewModal.js +25 -1
  56. package/dist/react/components/ChatSettingsDialog.d.ts +13 -0
  57. package/dist/react/components/ChatSettingsDialog.hooks.d.ts +115 -0
  58. package/dist/react/components/ChatSettingsDialog.hooks.js +40 -0
  59. package/dist/react/components/ChatSettingsDialog.js +72 -0
  60. package/dist/react/components/ChatSettingsSegmented.d.ts +18 -0
  61. package/dist/react/components/ChatSettingsSegmented.js +11 -0
  62. package/dist/react/components/Composer.d.ts +5 -1
  63. package/dist/react/components/Composer.js +126 -82
  64. package/dist/react/components/ConversationList.js +25 -39
  65. package/dist/react/components/ConversationTitleEditor.d.ts +27 -0
  66. package/dist/react/components/ConversationTitleEditor.js +54 -0
  67. package/dist/react/components/Icon.d.ts +1 -1
  68. package/dist/react/components/Icon.js +25 -45
  69. package/dist/react/components/Markdown.js +134 -123
  70. package/dist/react/components/McpUiSurfaceCard.d.ts +2 -2
  71. package/dist/react/components/McpUiSurfaceCard.js +30 -9
  72. package/dist/react/components/MessageList.d.ts +13 -13
  73. package/dist/react/components/MessageList.hooks.d.ts +6 -1
  74. package/dist/react/components/MessageList.hooks.js +19 -3
  75. package/dist/react/components/MessageList.js +34 -10
  76. package/dist/react/components/MessageRow.d.ts +1 -1
  77. package/dist/react/components/MessageRow.js +75 -47
  78. package/dist/react/components/MessageRow.usage.js +16 -11
  79. package/dist/react/components/MidRunUserMessage.js +16 -3
  80. package/dist/react/components/QuestionForm.js +148 -76
  81. package/dist/react/components/QuestionsPanel.js +1 -1
  82. package/dist/react/components/TodoCard.js +12 -6
  83. package/dist/react/components/ToolCallGroup.d.ts +4 -3
  84. package/dist/react/components/ToolCallGroup.js +7 -5
  85. package/dist/react/components/ToolCard.js +47 -40
  86. package/dist/react/components/composer-discovery.js +6 -1
  87. package/dist/react/components/mcp-ui-surface-state.d.ts +6 -11
  88. package/dist/react/components/mcp-ui-surface-state.js +23 -8
  89. package/dist/react/ext-event-renderer-registry.d.ts +8 -0
  90. package/dist/react/ext-event-renderer-registry.js +5 -2
  91. package/dist/react/features/chat-pane/components/AgentPluginUsage.d.ts +7 -0
  92. package/dist/react/features/chat-pane/components/AgentPluginUsage.js +45 -0
  93. package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +55 -19
  94. package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
  95. package/dist/react/features/chat-pane/components/ChatPane.js +192 -74
  96. package/dist/react/features/chat-pane/components/RuntimeDropUp.d.ts +29 -0
  97. package/dist/react/features/chat-pane/components/RuntimeDropUp.js +45 -0
  98. package/dist/react/features/chat-pane/create-daemon-attachment-uploader.js +80 -47
  99. package/dist/react/features/chat-pane/create-mcp-ui-tool-caller.js +44 -40
  100. package/dist/react/features/chat-pane/create-mid-run-message-poster.d.ts +6 -6
  101. package/dist/react/features/chat-pane/create-mid-run-message-poster.js +8 -8
  102. package/dist/react/features/chat-pane/create-typed-answer-poster.d.ts +3 -3
  103. package/dist/react/features/chat-pane/create-typed-answer-poster.js +3 -3
  104. package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.d.ts +6 -1
  105. package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +11 -5
  106. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +12 -7
  107. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +340 -149
  108. package/dist/react/features/chat-pane/hooks/useChatPaneControlsHeight.hooks.js +7 -6
  109. package/dist/react/features/chat-pane/hooks/useChatPaneNewTabLinks.hooks.js +28 -10
  110. package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.d.ts +3 -3
  111. package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +60 -12
  112. package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.d.ts +49 -0
  113. package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.js +143 -0
  114. package/dist/react/features/chat-pane/model-options.d.ts +14 -0
  115. package/dist/react/features/chat-pane/model-options.js +45 -25
  116. package/dist/react/features/chat-pane/rules.d.ts +8 -9
  117. package/dist/react/features/chat-pane/rules.js +84 -46
  118. package/dist/react/features/chat-pane/styles.d.ts +1 -1
  119. package/dist/react/features/chat-pane/styles.js +152 -36
  120. package/dist/react/features/chat-pane/types.d.ts +19 -9
  121. package/dist/react/hooks/assistant-chats.js +12 -5
  122. package/dist/react/hooks/composer-draft-cache.js +116 -118
  123. package/dist/react/hooks/composer-history-storage.js +5 -5
  124. package/dist/react/hooks/fab-position.js +7 -4
  125. package/dist/react/hooks/useAnsweredCards.d.ts +21 -4
  126. package/dist/react/hooks/useAnsweredCards.js +38 -11
  127. package/dist/react/hooks/useArtifactStream.js +28 -22
  128. package/dist/react/hooks/useComposer.d.ts +20 -0
  129. package/dist/react/hooks/useComposer.js +22 -9
  130. package/dist/react/hooks/useComposerHistory.hooks.d.ts +6 -1
  131. package/dist/react/hooks/useComposerHistory.hooks.js +39 -19
  132. package/dist/react/hooks/useComposerHistoryKeys.hooks.js +24 -14
  133. package/dist/react/hooks/useConversation.d.ts +7 -0
  134. package/dist/react/hooks/useConversation.js +23 -10
  135. package/dist/react/hooks/useExtEventGroups.js +39 -39
  136. package/dist/react/hooks/useQuestionForms.js +21 -26
  137. package/dist/react/hooks/useRunStream.js +10 -2
  138. package/dist/react/index.d.ts +7 -1
  139. package/dist/react/index.js +9 -0
  140. package/dist/react/message-blocks.d.ts +6 -4
  141. package/dist/react/message-blocks.js +94 -72
  142. package/dist/react/slots.d.ts +2 -2
  143. package/dist/react/tool-call-display.d.ts +12 -6
  144. package/dist/react/tool-call-display.js +53 -24
  145. package/dist/server/durable-runs/recover.js +132 -80
  146. package/dist/server/run-finalizer/entry.js +25 -15
  147. package/dist/store/legacy/sqlite/conversations.d.ts +11 -11
  148. package/dist/store/legacy/sqlite/conversations.js +69 -98
  149. package/dist/store/legacy/sqlite/messages.d.ts +10 -2
  150. package/dist/store/legacy/sqlite/messages.js +113 -71
  151. package/dist/store/runtime/chat-history-store.js +9 -3
  152. package/dist/store/runtime/durable-run-store.d.ts +2 -2
  153. package/dist/store/runtime/durable-run-store.js +165 -117
  154. package/dist/store/runtime/history-tools.js +118 -82
  155. package/dist/store/runtime/run-ledger.d.ts +4 -1
  156. package/dist/store/runtime/run-ledger.js +56 -16
  157. package/dist/store/sql/cursor.js +38 -18
  158. package/dist/store/sql/store.d.ts +18 -0
  159. package/dist/store/sql/store.js +46 -15
  160. package/package.json +18 -16
@@ -44,12 +44,13 @@ export function useChatPaneControlsHeight() {
44
44
  if (!root || !controls || typeof ResizeObserver !== 'function')
45
45
  return;
46
46
  const observer = new ResizeObserver((entries) => {
47
- const entry = entries[0];
48
- if (!entry)
49
- return;
50
- // Rounded up: a fractional px short would let one glyph's descender clip against the
51
- // gradient's opaque end, which `Math.ceil` never risks understating.
52
- root.style.setProperty(CHAT_PANE_CONTROLS_HEIGHT_CSS_VAR, `${Math.ceil(entry.contentRect.height)}px`);
47
+ // Only `controls` is observed, so a notification carries at most one entry: `slice(0, 1)`
48
+ // reads it without a branch for the empty list the ResizeObserver spec never delivers.
49
+ for (const entry of entries.slice(0, 1)) {
50
+ // Rounded up: a fractional px short would let one glyph's descender clip against the
51
+ // gradient's opaque end, which `Math.ceil` never risks understating.
52
+ root.style.setProperty(CHAT_PANE_CONTROLS_HEIGHT_CSS_VAR, `${Math.ceil(entry.contentRect.height)}px`);
53
+ }
53
54
  });
54
55
  observer.observe(controls);
55
56
  return () => observer.disconnect();
@@ -25,6 +25,32 @@ const NEW_TAB_PROTOCOLS = new Set(['http:', 'https:']);
25
25
  function openInNewTabDefault(url) {
26
26
  window.open(url, '_blank', 'noopener,noreferrer');
27
27
  }
28
+ /**
29
+ * Whether the click is a plain primary click nobody has handled yet: modified or non-primary
30
+ * clicks are left to the browser, which already opens those however the user asked.
31
+ *
32
+ * @param input.event - The native click.
33
+ * @returns `true` when the guard may take the click over.
34
+ */
35
+ function isPlainPrimaryClick({ event }) {
36
+ if (event.defaultPrevented || event.button !== 0)
37
+ return false;
38
+ return !(event.metaKey || event.ctrlKey || event.shiftKey || event.altKey);
39
+ }
40
+ /**
41
+ * Whether an anchor is an ordinary in-tab web link the guard should redirect to a new tab — not
42
+ * one already opening elsewhere, a download, an action styled as a link, or a `#` same-document jump.
43
+ *
44
+ * @param input.link - An anchor matched by `a[href]`, so its `href` attribute is present.
45
+ * @returns `true` when following the link would navigate the chat's own tab to an http(s) page.
46
+ */
47
+ function isInTabWebLink({ link }) {
48
+ if (link.target === '_blank' || link.hasAttribute('download') || link.getAttribute('role') === 'button')
49
+ return false;
50
+ if (link.getAttribute('href').startsWith('#'))
51
+ return false;
52
+ return NEW_TAB_PROTOCOLS.has(link.protocol);
53
+ }
28
54
  /**
29
55
  * Decides whether a click inside the pane should open a link in a new tab.
30
56
  *
@@ -34,21 +60,13 @@ function openInNewTabDefault(url) {
34
60
  * @complexity O(d) for `closest` over the target's ancestor depth d.
35
61
  */
36
62
  export function newTabLinkHref({ event, root }) {
37
- if (event.defaultPrevented || event.button !== 0)
38
- return null;
39
- if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
40
- return null;
41
63
  const target = event.target;
42
- if (!(target instanceof Element))
64
+ if (!isPlainPrimaryClick({ event }) || !(target instanceof Element))
43
65
  return null;
44
66
  const link = target.closest('a[href]');
45
67
  if (!(link instanceof HTMLAnchorElement) || !root.contains(link))
46
68
  return null;
47
- if (link.target === '_blank' || link.hasAttribute('download') || link.getAttribute('role') === 'button')
48
- return null;
49
- if (link.getAttribute('href').startsWith('#'))
50
- return null;
51
- return NEW_TAB_PROTOCOLS.has(link.protocol) ? link.href : null;
69
+ return isInTabWebLink({ link }) ? link.href : null;
52
70
  }
53
71
  /**
54
72
  * The pane root's `onClickCapture` handler: opens qualifying link clicks in a new tab and cancels
@@ -3,7 +3,7 @@ export interface UseChatPaneRuntimeInventoryOptions {
3
3
  access?: ChatPaneRuntimeAccess;
4
4
  initialAgents?: readonly ChatPaneAgent[];
5
5
  pollIntervalMs?: number;
6
- /** Backoff between retries of a failed inventory load; the last entry repeats. */
6
+ /** Failure backoff; pending discovery doubles the final step until its finite window ends. */
7
7
  retryDelaysMs?: readonly number[];
8
8
  }
9
9
  export interface UseChatPaneRuntimeInventoryResult {
@@ -30,13 +30,13 @@ export interface UseChatPaneRuntimeInventoryResult {
30
30
  * daemon restarting under an open pane left it on "No usable CLI is selected" until a hard
31
31
  * reload). Until the first successful answer the hook retries on a backoff, and immediately when
32
32
  * the daemon goes from offline to online, the window regains focus, or the tab becomes visible.
33
- * A RESOLVED list — even an empty one — is a finished answer; hosts reject when theirs is not one.
33
+ * A RESOLVED list is an answer, including an empty one. Entries marked discoveryPending retry
34
+ * within a finite sweep window, honoring the host cache's next eligible refresh deadline.
34
35
  *
35
36
  * Health polling runs only while the tab is visible (hidden tabs made no-op status calls every
36
37
  * `pollIntervalMs` forever), and never overlaps a status call still in flight.
37
38
  *
38
39
  * @complexity Time: O(n) per inventory response; space: O(n) for the snapshot.
39
- * @overallScore 100/100
40
40
  */
41
41
  export declare function useChatPaneRuntimeInventory({ access, initialAgents, pollIntervalMs, retryDelaysMs, }: UseChatPaneRuntimeInventoryOptions): UseChatPaneRuntimeInventoryResult;
42
42
  //# sourceMappingURL=useChatPaneRuntimeInventory.hooks.d.ts.map
@@ -4,6 +4,11 @@ const EMPTY_AGENTS = [];
4
4
  /** Waits between inventory retries while `listAgents` keeps failing: quick at first (a daemon
5
5
  * restart takes seconds), then every 30s for as long as the pane stays mounted. */
6
6
  const DEFAULT_RETRY_DELAYS_MS = [1_000, 2_000, 4_000, 8_000, 15_000, 30_000];
7
+ // The native host serializes metadata for 29 definitions: version/help/auth checks and up to
8
+ // 15s per catalog can take minutes in a cold sweep. Include queue time and a final cache cooldown
9
+ // margin, then stop; extending backoff beyond 30s keeps this to ten default retries, not polling.
10
+ const PENDING_DISCOVERY_WINDOW_MS = 15 * 60_000;
11
+ const PENDING_DISCOVERY_RETRY_LIMIT = 10;
7
12
  /**
8
13
  * Owns agent inventory, explicit rescans, and daemon-health polling while the
9
14
  * host supplies only the environment-specific I/O effects.
@@ -15,13 +20,13 @@ const DEFAULT_RETRY_DELAYS_MS = [1_000, 2_000, 4_000, 8_000, 15_000, 30_000];
15
20
  * daemon restarting under an open pane left it on "No usable CLI is selected" until a hard
16
21
  * reload). Until the first successful answer the hook retries on a backoff, and immediately when
17
22
  * the daemon goes from offline to online, the window regains focus, or the tab becomes visible.
18
- * A RESOLVED list — even an empty one — is a finished answer; hosts reject when theirs is not one.
23
+ * A RESOLVED list is an answer, including an empty one. Entries marked discoveryPending retry
24
+ * within a finite sweep window, honoring the host cache's next eligible refresh deadline.
19
25
  *
20
26
  * Health polling runs only while the tab is visible (hidden tabs made no-op status calls every
21
27
  * `pollIntervalMs` forever), and never overlaps a status call still in flight.
22
28
  *
23
29
  * @complexity Time: O(n) per inventory response; space: O(n) for the snapshot.
24
- * @overallScore 100/100
25
30
  */
26
31
  export function useChatPaneRuntimeInventory({ access, initialAgents = EMPTY_AGENTS, pollIntervalMs = 5_000, retryDelaysMs = DEFAULT_RETRY_DELAYS_MS, }) {
27
32
  const [agents, setAgents] = useState(initialAgents);
@@ -51,16 +56,19 @@ export function useChatPaneRuntimeInventory({ access, initialAgents = EMPTY_AGEN
51
56
  if (!access)
52
57
  return;
53
58
  retry.cancel();
59
+ if (rescan)
60
+ retry.restart();
54
61
  setScanningAgents(true);
55
62
  setRuntimeInventoryError(null);
56
63
  await inventory.run({ body: async (token) => {
57
64
  const nextAgents = await (rescan ? access.rescanAgents() : access.listAgents());
58
65
  token.ensureCurrent();
59
- retry.answered();
60
- // Successful cached data paints immediately. Poll only while the host says its single
61
- // background scan is pending; reads never imply an explicit Rescan.
62
- if (nextAgents.some(agent => agent.discoveryPending))
63
- retry.schedule(() => void loadAgents(false));
66
+ const pending = nextAgents.some(agent => agent.discoveryPending);
67
+ retry.answered({ pending });
68
+ if (pending)
69
+ retry.schedule(() => void loadAgents(false), {
70
+ pending: true, nextRefreshAt: access.getAgentsRefreshAt?.() ?? Date.now(),
71
+ });
64
72
  setAgents([...nextAgents]);
65
73
  setConnectingAgents(false);
66
74
  setScanningAgents(false);
@@ -182,13 +190,15 @@ export function useChatPaneRuntimeInventory({ access, initialAgents = EMPTY_AGEN
182
190
  }
183
191
  /**
184
192
  * The retry bookkeeping `useChatPaneRuntimeInventory` needs, in refs so none of it re-renders:
185
- * whether an answer has arrived, the pending retry timer, the backoff step, and the last health
186
- * result (`null` until the first one, so the first "online" report is not a reconnect).
193
+ * whether an answer has arrived, the retry timer, separate failure/discovery steps, a finite
194
+ * discovery deadline, and the last health result (`null` until the first health response).
187
195
  */
188
196
  function useInventoryRetry(delaysMs) {
189
197
  const answeredRef = useRef(false);
190
198
  const timerRef = useRef(undefined);
191
199
  const stepRef = useRef(0);
200
+ const pendingStepRef = useRef(0);
201
+ const pendingDeadlineRef = useRef(undefined);
192
202
  const lastOnlineRef = useRef(null);
193
203
  const delaysRef = useRef(delaysMs);
194
204
  delaysRef.current = delaysMs;
@@ -200,15 +210,33 @@ function useInventoryRetry(delaysMs) {
200
210
  const restart = () => {
201
211
  cancel();
202
212
  stepRef.current = 0;
213
+ pendingStepRef.current = 0;
214
+ pendingDeadlineRef.current = undefined;
215
+ };
216
+ const schedulePending = (attempt, nextRefreshAt) => {
217
+ const now = Date.now();
218
+ pendingDeadlineRef.current ??= now + PENDING_DISCOVERY_WINDOW_MS;
219
+ const delay = pendingRetryDelayMs({
220
+ delaysMs: delaysRef.current, step: pendingStepRef.current, now, deadline: pendingDeadlineRef.current, nextRefreshAt,
221
+ });
222
+ if (delay === null)
223
+ return;
224
+ pendingStepRef.current += 1;
225
+ timerRef.current = window.setTimeout(attempt, delay);
203
226
  };
204
227
  return {
205
228
  hasAnswer: () => answeredRef.current,
206
- answered: () => {
229
+ answered: ({ pending }) => {
207
230
  answeredRef.current = true;
208
- restart();
231
+ if (!pending)
232
+ restart();
209
233
  },
210
- schedule: (attempt) => {
234
+ schedule: (attempt, { pending = false, nextRefreshAt = 0 } = {}) => {
211
235
  cancel();
236
+ if (pending) {
237
+ schedulePending(attempt, nextRefreshAt);
238
+ return;
239
+ }
212
240
  const delays = delaysRef.current;
213
241
  const delay = delays[Math.min(stepRef.current, delays.length - 1)] ?? 0;
214
242
  stepRef.current += 1;
@@ -230,4 +258,24 @@ function useInventoryRetry(delaysMs) {
230
258
  });
231
259
  return retry;
232
260
  }
261
+ /**
262
+ * The wait before the next pending-discovery retry: the backoff step (doubling the final step once
263
+ * the list runs out), pushed out to the host cache's next eligible refresh, capped at the window.
264
+ *
265
+ * @param input.delaysMs - Failure backoff steps; empty means no pending retries at all.
266
+ * @param input.step - Pending retries already made in this discovery window.
267
+ * @param input.now - Current epoch ms.
268
+ * @param input.deadline - Epoch ms the finite discovery window closes.
269
+ * @param input.nextRefreshAt - Earliest epoch ms the host cache can refresh.
270
+ * @returns Milliseconds to wait, or `null` when the retry budget or window is spent.
271
+ * @complexity O(1).
272
+ */
273
+ function pendingRetryDelayMs({ delaysMs, step, now, deadline, nextRefreshAt }) {
274
+ const lastDelay = delaysMs[delaysMs.length - 1];
275
+ if (lastDelay === undefined || step >= PENDING_DISCOVERY_RETRY_LIMIT || now >= deadline)
276
+ return null;
277
+ const delay = delaysMs[step] ?? lastDelay * 2 ** (step - delaysMs.length + 1);
278
+ const refreshAt = Math.max(nextRefreshAt, Math.min(now + delay, deadline));
279
+ return refreshAt > deadline ? null : refreshAt - now;
280
+ }
233
281
  //# sourceMappingURL=useChatPaneRuntimeInventory.hooks.js.map
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Mechanics for the composer bar's small model / reasoning-effort menus ("drop-ups"): open/close,
3
+ * outside-click and Escape dismissal, a viewport-fixed position anchored ABOVE the trigger, focus
4
+ * on open and back to the trigger on close, and arrow-key roving between the items.
5
+ *
6
+ * Same idiom as `useAgentRuntimePicker` (whose pure `resolveRovingIndex` this reuses), minus what a
7
+ * flat list of choices does not need: no portal (the menu renders inline, so it inherits the
8
+ * host's `data-theme` and the pane's scoped CSS; `position: fixed` still escapes the dock's
9
+ * `overflow: hidden`, as `composerDiscoveryMenuPosition` documents) and no Tab trap (Tab simply
10
+ * closes the menu and lets focus move on).
11
+ */
12
+ import { type CSSProperties, type KeyboardEvent as ReactKeyboardEvent, type RefObject } from 'react';
13
+ /**
14
+ * Viewport-fixed geometry for a drop-up: right-aligned with the trigger, clamped into the
15
+ * viewport and (when given) into `bounds`, bottom edge just above it, and never taller than the
16
+ * space above. Right-aligned because the model/effort cluster sits at the composer bar's trailing
17
+ * edge, so the menu grows back into the pane instead of past the dock's right edge; `bounds` (the
18
+ * composer box) keeps a menu wider than the room left of the trigger from spilling out of a narrow
19
+ * dock onto the page beside it.
20
+ *
21
+ * @complexity Time/space: O(1).
22
+ */
23
+ export declare function runtimeDropUpPosition({ trigger, width }: {
24
+ trigger: DOMRect;
25
+ width: number;
26
+ }, { bounds }?: {
27
+ bounds?: Pick<DOMRect, 'left' | 'right'>;
28
+ }): CSSProperties;
29
+ export interface UseRuntimeDropUpResult {
30
+ menuId: string;
31
+ open: boolean;
32
+ /** Undefined until the trigger has been measured; the menu does not render before then. */
33
+ position: CSSProperties | undefined;
34
+ triggerRef: RefObject<HTMLButtonElement | null>;
35
+ menuRef: RefObject<HTMLDivElement | null>;
36
+ toggleOpen: () => void;
37
+ /** ArrowUp/ArrowDown on the closed trigger opens the menu, like a native select. */
38
+ handleTriggerKeyDown: (event: ReactKeyboardEvent<HTMLButtonElement>) => void;
39
+ handleMenuKeyDown: (event: ReactKeyboardEvent<HTMLDivElement>) => void;
40
+ /** Runs the selection, closes, and returns focus to the trigger. */
41
+ choose: (apply: () => void) => void;
42
+ }
43
+ /**
44
+ * @complexity Time: O(1) per render; key handling is O(n) in the menu's item count.
45
+ */
46
+ export declare function useRuntimeDropUp({ width }: {
47
+ width: number;
48
+ }): UseRuntimeDropUpResult;
49
+ //# sourceMappingURL=useRuntimeDropUp.hooks.d.ts.map
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Mechanics for the composer bar's small model / reasoning-effort menus ("drop-ups"): open/close,
3
+ * outside-click and Escape dismissal, a viewport-fixed position anchored ABOVE the trigger, focus
4
+ * on open and back to the trigger on close, and arrow-key roving between the items.
5
+ *
6
+ * Same idiom as `useAgentRuntimePicker` (whose pure `resolveRovingIndex` this reuses), minus what a
7
+ * flat list of choices does not need: no portal (the menu renders inline, so it inherits the
8
+ * host's `data-theme` and the pane's scoped CSS; `position: fixed` still escapes the dock's
9
+ * `overflow: hidden`, as `composerDiscoveryMenuPosition` documents) and no Tab trap (Tab simply
10
+ * closes the menu and lets focus move on).
11
+ */
12
+ import { useCallback, useEffect, useId, useRef, useState, } from 'react';
13
+ import { resolveRovingIndex } from './useAgentRuntimePicker.hooks.js';
14
+ const MENU_ITEMS = '[role="menuitemradio"]';
15
+ /**
16
+ * Viewport-fixed geometry for a drop-up: right-aligned with the trigger, clamped into the
17
+ * viewport and (when given) into `bounds`, bottom edge just above it, and never taller than the
18
+ * space above. Right-aligned because the model/effort cluster sits at the composer bar's trailing
19
+ * edge, so the menu grows back into the pane instead of past the dock's right edge; `bounds` (the
20
+ * composer box) keeps a menu wider than the room left of the trigger from spilling out of a narrow
21
+ * dock onto the page beside it.
22
+ *
23
+ * @complexity Time/space: O(1).
24
+ */
25
+ export function runtimeDropUpPosition({ trigger, width }, { bounds } = {}) {
26
+ const margin = 12;
27
+ const gap = 6;
28
+ const minLeft = Math.max(margin, bounds?.left ?? -Infinity);
29
+ const maxRight = Math.min(window.innerWidth - margin, bounds?.right ?? Infinity);
30
+ const menuWidth = Math.max(0, Math.min(width, maxRight - minLeft));
31
+ return {
32
+ position: 'fixed',
33
+ left: Math.min(Math.max(trigger.right - menuWidth, minLeft), maxRight - menuWidth),
34
+ bottom: Math.max(margin, window.innerHeight - trigger.top + gap),
35
+ width: menuWidth,
36
+ maxHeight: Math.min(320, Math.max(0, trigger.top - gap - margin)),
37
+ overflowY: 'auto',
38
+ zIndex: 1000,
39
+ };
40
+ }
41
+ /**
42
+ * @complexity Time: O(1) per render; key handling is O(n) in the menu's item count.
43
+ */
44
+ export function useRuntimeDropUp({ width }) {
45
+ const menuId = useId();
46
+ const [open, setOpen] = useState(false);
47
+ const [position, setPosition] = useState();
48
+ const triggerRef = useRef(null);
49
+ const menuRef = useRef(null);
50
+ const closeToTrigger = useCallback(() => {
51
+ setOpen(false);
52
+ triggerRef.current?.focus();
53
+ }, []);
54
+ const toggleOpen = useCallback(() => {
55
+ setOpen((current) => !current);
56
+ }, []);
57
+ useEffect(() => {
58
+ if (!open)
59
+ return;
60
+ const closeOnOutsidePointer = (event) => {
61
+ const target = event.target;
62
+ if (triggerRef.current?.contains(target) || menuRef.current?.contains(target))
63
+ return;
64
+ // No focus restore: the pointer is already going somewhere else.
65
+ setOpen(false);
66
+ };
67
+ const closeOnEscape = (event) => {
68
+ if (event.key === 'Escape')
69
+ closeToTrigger();
70
+ };
71
+ document.addEventListener('mousedown', closeOnOutsidePointer);
72
+ document.addEventListener('keydown', closeOnEscape);
73
+ return () => {
74
+ document.removeEventListener('mousedown', closeOnOutsidePointer);
75
+ document.removeEventListener('keydown', closeOnEscape);
76
+ };
77
+ }, [closeToTrigger, open]);
78
+ useEffect(() => {
79
+ if (!open)
80
+ return;
81
+ const update = () => {
82
+ const rect = triggerRef.current?.getBoundingClientRect();
83
+ const bounds = triggerRef.current?.closest('.jini-composer')?.getBoundingClientRect();
84
+ if (rect)
85
+ setPosition(runtimeDropUpPosition({ trigger: rect, width }, bounds ? { bounds } : {}));
86
+ };
87
+ update();
88
+ window.addEventListener('resize', update);
89
+ // Capture phase: a scroll inside any ancestor moves the trigger too, and scroll does not bubble.
90
+ window.addEventListener('scroll', update, true);
91
+ return () => {
92
+ window.removeEventListener('resize', update);
93
+ window.removeEventListener('scroll', update, true);
94
+ };
95
+ }, [open, width]);
96
+ useEffect(() => {
97
+ if (!open || !position)
98
+ return;
99
+ const menu = menuRef.current;
100
+ const checked = menu?.querySelector(`${MENU_ITEMS}[aria-checked="true"]`);
101
+ (checked ?? menu?.querySelector(MENU_ITEMS))?.focus();
102
+ }, [open, position]);
103
+ const handleTriggerKeyDown = useCallback((event) => {
104
+ if (open || (event.key !== 'ArrowUp' && event.key !== 'ArrowDown'))
105
+ return;
106
+ event.preventDefault();
107
+ setOpen(true);
108
+ }, [open]);
109
+ const handleMenuKeyDown = useCallback((event) => {
110
+ if (event.key === 'Tab') {
111
+ setOpen(false);
112
+ return;
113
+ }
114
+ // Only reachable via the element `menuRef` is attached to, so the ref is set by now; and the
115
+ // menu is never rendered without items (the caller omits a drop-up that has no choices).
116
+ const items = [...menuRef.current.querySelectorAll(MENU_ITEMS)];
117
+ const next = resolveRovingIndex({
118
+ key: event.key,
119
+ currentIndex: items.indexOf(document.activeElement),
120
+ count: items.length,
121
+ });
122
+ if (next === null)
123
+ return;
124
+ event.preventDefault();
125
+ items[next]?.focus();
126
+ }, []);
127
+ const choose = useCallback((apply) => {
128
+ apply();
129
+ closeToTrigger();
130
+ }, [closeToTrigger]);
131
+ return {
132
+ menuId,
133
+ open,
134
+ position,
135
+ triggerRef,
136
+ menuRef,
137
+ toggleOpen,
138
+ handleTriggerKeyDown,
139
+ handleMenuKeyDown,
140
+ choose,
141
+ };
142
+ }
143
+ //# sourceMappingURL=useRuntimeDropUp.hooks.js.map
@@ -12,4 +12,18 @@ export declare function configuredPickerModel(agent: ChatPaneAgent): string | un
12
12
  export declare function pickerModelLabel(agent: ChatPaneAgent | undefined, requested?: string, t?: I18nAdapter['t']): string;
13
13
  export declare function pickerModelValue(agent: ChatPaneAgent, requested?: string): string;
14
14
  export declare function pickerCatalogNote(agent: ChatPaneAgent, t?: I18nAdapter['t']): string | undefined;
15
+ /**
16
+ * The composer bar's compact model name: the catalog label (or a prettified raw id when the catalog
17
+ * has no label), minus the leading vendor word the agent's icon already conveys, capped with "…".
18
+ * The full label stays in the drop-up list and the trigger's tooltip.
19
+ * @example shortModelLabel({ label: 'GPT-6.1-Sol', agentId: 'codex' }) // "6.1 Sol"
20
+ * @example shortModelLabel({ label: 'claude-opus-5-5', agentId: 'claude' }) // "Opus 5.5"
21
+ * @complexity O(n) in the label length. Pure.
22
+ */
23
+ export declare function shortModelLabel({ label, agentId }: {
24
+ label: string;
25
+ agentId?: string | undefined;
26
+ }, { max }?: {
27
+ max?: number;
28
+ }): string;
15
29
  //# sourceMappingURL=model-options.d.ts.map
@@ -1,8 +1,8 @@
1
+ import { concreteModelOptions, modelIdentityKind } from '@jini-ai/agent-runtime/model-selection';
1
2
  // Model names and timestamps are metadata; only surrounding picker copy is translated.
2
3
  const passthrough = key => key;
3
4
  export function isConcretePickerModel(id) {
4
- const token = id.slice(id.lastIndexOf('/') + 1);
5
- return !/^(?:opus|sonnet|haiku|fable|best|latest|claude|gemini|gpt|codex|swe)(?:\[.*\])?$/i.test(token) && !/^(default|auto|adaptive|smart|deep|rush|ultimate|premium|balanced|fast|grok-build|vercel-ai-gateway)$/i.test(token) && !/(?:^|[-/])(gateway|latest)$/i.test(id);
5
+ return modelIdentityKind(id) === 'concrete';
6
6
  }
7
7
  /** Aliases are selectable only as their evidence-backed concrete ID. Never render a Default row.
8
8
  * Keeps the first position per concrete ID, upgrading raw-ID labels from later human labels.
@@ -11,29 +11,7 @@ export function isConcretePickerModel(id) {
11
11
  * @complexity O(n) time and space in catalog rows, using insertion-ordered Map keys.
12
12
  */
13
13
  export function pickerModelOptions(agent) {
14
- if (agent.supportsConcreteModelSelection === false)
15
- return [];
16
- const choices = new Map();
17
- for (const row of agent.models || []) {
18
- const id = row.resolvedId || (row.identityKind === 'routing-mode' || row.identityKind === 'alias' ? undefined : row.id);
19
- if (!id || !isConcretePickerModel(id))
20
- continue;
21
- // Older cached inventories can attach a default-policy label to a resolved concrete ID.
22
- const label = row.id === 'default' || /^default\b/i.test(row.label) ? id : row.label;
23
- const existing = choices.get(id);
24
- if (!existing) {
25
- choices.set(id, { ...row, id, label, identityKind: 'concrete' });
26
- }
27
- else if (existing.label === id && label !== id) {
28
- choices.set(id, { ...existing, label });
29
- }
30
- }
31
- const options = [...choices.values()];
32
- const resolution = agent.defaultModelResolution;
33
- // A host may publish a resolved default outside its visible catalog. Preserve its concrete ID.
34
- if (resolution?.status === 'resolved' && isConcretePickerModel(resolution.id) && !choices.has(resolution.id))
35
- options.unshift({ id: resolution.id, label: resolution.id, identityKind: 'concrete' });
36
- return options;
14
+ return concreteModelOptions(agent);
37
15
  }
38
16
  export function configuredPickerModel(agent) {
39
17
  if (agent.supportsConcreteModelSelection === false)
@@ -61,4 +39,46 @@ export function pickerCatalogNote(agent, t = passthrough) {
61
39
  return t('Offline fallback');
62
40
  return undefined;
63
41
  }
42
+ // The vendor word each CLI's own icon already shows beside the bar label (owner 2026-10-18).
43
+ // Agents not listed keep every word: a Cursor icon says nothing about whether a model is GPT.
44
+ const BAR_VENDOR_WORDS = {
45
+ claude: ['claude', 'anthropic'],
46
+ codex: ['gpt', 'openai'],
47
+ antigravity: ['gemini', 'google'],
48
+ };
49
+ // A bare catalog id ("claude-opus-5-5", "gpt-6-astra[1m]", "anthropic/claude-sonnet-5") rather than a display name.
50
+ const RAW_MODEL_ID = /^(?:[a-z0-9._-]+\/)*[a-z0-9][a-z0-9._-]*(?:\[[^\]]*\])?$/;
51
+ const BRAND_CASING = { gpt: 'GPT' };
52
+ function prettifyRawModelId(id) {
53
+ // A routing prefix ("anthropic/") is the full name's job; the bar names only the model.
54
+ const tokens = id.slice(id.lastIndexOf('/') + 1).replace(/\[[^\]]*\]$/, '').split(/[-_]+/).filter((token) => token && !/^\d{8}$/.test(token));
55
+ const words = [];
56
+ for (const token of tokens) {
57
+ const previous = words[words.length - 1];
58
+ // Dash-separated version digits read as one dotted version: "5-5" → "5.5".
59
+ if (/^\d+$/.test(token) && previous && /^\d+(?:\.\d+)*$/.test(previous))
60
+ words[words.length - 1] = `${previous}.${token}`;
61
+ else
62
+ words.push(BRAND_CASING[token] ?? token.charAt(0).toUpperCase() + token.slice(1));
63
+ }
64
+ return words;
65
+ }
66
+ /**
67
+ * The composer bar's compact model name: the catalog label (or a prettified raw id when the catalog
68
+ * has no label), minus the leading vendor word the agent's icon already conveys, capped with "…".
69
+ * The full label stays in the drop-up list and the trigger's tooltip.
70
+ * @example shortModelLabel({ label: 'GPT-6.1-Sol', agentId: 'codex' }) // "6.1 Sol"
71
+ * @example shortModelLabel({ label: 'claude-opus-5-5', agentId: 'claude' }) // "Opus 5.5"
72
+ * @complexity O(n) in the label length. Pure.
73
+ */
74
+ export function shortModelLabel({ label, agentId }, { max = 16 } = {}) {
75
+ const trimmed = label.trim();
76
+ const raw = RAW_MODEL_ID.test(trimmed);
77
+ const words = raw ? prettifyRawModelId(trimmed) : trimmed.split(/[\s-]+/).filter(Boolean);
78
+ const vendors = (agentId && BAR_VENDOR_WORDS[agentId]) || [];
79
+ const dropVendor = words.length > 1 && vendors.includes(words[0].toLowerCase());
80
+ // A display name with no vendor word to drop is shown as the catalog wrote it.
81
+ const short = dropVendor ? words.slice(1).join(' ') : raw ? words.join(' ') : trimmed;
82
+ return short.length > max ? `${short.slice(0, max - 1).trimEnd()}…` : short;
83
+ }
64
84
  //# sourceMappingURL=model-options.js.map
@@ -108,7 +108,7 @@ export declare function isChatPaneQueueableBlocker({ blocker }: {
108
108
  *
109
109
  * A surface is attributed to the newest call still open when it arrived — the same rule
110
110
  * `useExtEventGroups` uses to give a card its `call` — so an older call returning does not settle
111
- * it. Once its call returns (answered, expired, abandoned), typing is a next turn again.
111
+ * it. A terminal run, a later turn, a returned call or an accepted answer receipt closes it.
112
112
  *
113
113
  * @complexity O(e·c): e events in the newest message, c calls open at once (one or two in practice).
114
114
  */
@@ -118,8 +118,7 @@ export declare function isAwaitingTypedAnswer({ messages, toolName }: {
118
118
  }): boolean;
119
119
  /**
120
120
  * The tool-use id of the question {@link isAwaitingTypedAnswer} sees waiting, or `null` — the newest
121
- * one when several are open. The id lets the pane tell one question from the next: a draft typed
122
- * for a question stays tied to THAT question after it closes (see `useChatPane`'s held answer).
121
+ * one when several are open. Only an active assistant run can hold a question open.
123
122
  *
124
123
  * @complexity O(e·c), as {@link isAwaitingTypedAnswer}.
125
124
  */
@@ -143,20 +142,20 @@ export declare function isTypedAnswerTurn({ blocker, attachmentCount, messages,
143
142
  }): boolean;
144
143
  /**
145
144
  * The run a composer send can go INTO instead of queueing behind, or `null`: a run is streaming
146
- * ({@link isChatPaneQueueableBlocker}), the turn has no attachments (a mid-run message carries text
147
- * only — the live agent's stdin takes no files), and the newest message is that run's assistant
145
+ * ({@link isChatPaneQueueableBlocker}), and the newest message is that run's assistant
148
146
  * turn with a known `runId`.
147
+ * Attachment support is decided by the host's delivery, never by dropping them here.
149
148
  *
150
149
  * @complexity O(1).
151
150
  */
152
- export declare function findMidRunMessageRunId({ blocker, attachmentCount, messages }: {
151
+ export declare function findMidRunMessageRunId({ blocker, messages }: {
153
152
  blocker: ChatPaneSendBlocker | null;
154
153
  attachmentCount: number;
155
154
  messages: readonly ChatMessage[];
156
155
  }): string | null;
157
156
  /**
158
- * The queued next turn after one more message joins it. The pane holds ONE queued turn, so a second
159
- * message sent behind the same run is appended to it (a blank line apart) rather than replacing it:
157
+ * MOVE: Display is per-message; delivery is ONE queued turn. A second message behind the same run
158
+ * is appended to that delivery (a blank line apart) rather than replacing it:
160
159
  * replacing silently dropped the first message (owner, 2026-10-10). One turn, not two, because both
161
160
  * were written for the run still going and the agent should read them together — two queued turns
162
161
  * would also start two paid runs back to back.
@@ -166,7 +165,7 @@ export declare function findMidRunMessageRunId({ blocker, attachmentCount, messa
166
165
  export declare function appendQueuedTurn({ queued, next }: {
167
166
  queued: UserTextRedaction | null;
168
167
  next: UserTextRedaction;
169
- }): UserTextRedaction;
168
+ }, _options?: {}): UserTextRedaction;
170
169
  /** The English copy (and i18n key) for why a typed answer was not sent. */
171
170
  export declare function describeTypedAnswerNotice({ notice }: {
172
171
  notice: TypedAnswerNotice;