@agent-native/core 0.125.0 → 0.127.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +79 -0
  3. package/corpus/core/docs/content/external-agents-catalog.mdx +14 -11
  4. package/corpus/core/docs/content/mcp-protocol.mdx +9 -4
  5. package/corpus/core/package.json +1 -1
  6. package/corpus/core/src/a2a/index.ts +2 -0
  7. package/corpus/core/src/agent/production-agent.ts +6 -1
  8. package/corpus/core/src/agent/run-manager.ts +37 -0
  9. package/corpus/core/src/agent/run-store.ts +18 -0
  10. package/corpus/core/src/client/MultiTabAssistantChat.tsx +75 -43
  11. package/corpus/core/src/client/agent-chat.ts +19 -1
  12. package/corpus/core/src/client/chat/tool-call-display.tsx +25 -0
  13. package/corpus/core/src/client/resources/mcp-integration-catalog.ts +23 -0
  14. package/corpus/core/src/client/settings/AgentsSection.tsx +565 -71
  15. package/corpus/core/src/client/settings/SecretsSection.tsx +71 -36
  16. package/corpus/core/src/client/use-chat-models.ts +54 -37
  17. package/corpus/core/src/localization/default-messages.ts +11 -0
  18. package/corpus/core/src/mcp/ask-app-task-handle.ts +183 -0
  19. package/corpus/core/src/mcp/builtin-tools.ts +146 -34
  20. package/corpus/core/src/server/agent-capabilities.ts +8 -1
  21. package/corpus/core/src/server/agent-chat-plugin.ts +17 -0
  22. package/corpus/core/src/server/agent-discovery.ts +1 -1
  23. package/corpus/core/src/server/agent-peer-probe.ts +133 -0
  24. package/corpus/core/src/server/core-routes-plugin.ts +67 -0
  25. package/corpus/core/src/server/email-template.ts +13 -0
  26. package/corpus/core/src/server/email.ts +40 -4
  27. package/corpus/core/src/shared/mcp-embed-headers.ts +4 -0
  28. package/corpus/core/src/sharing/actions/share-resource.ts +55 -5
  29. package/corpus/core/src/sharing/registry.ts +41 -1
  30. package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +159 -119
  31. package/corpus/templates/assets/actions/edit-image.ts +31 -18
  32. package/corpus/templates/assets/actions/refine-image.ts +43 -29
  33. package/corpus/templates/assets/actions/restyle-image.ts +30 -19
  34. package/corpus/templates/assets/actions/variant-slots.ts +26 -0
  35. package/corpus/templates/assets/app/components/generation/GenerationResults.tsx +85 -17
  36. package/corpus/templates/clips/changelog/2026-07-27-share-notification-emails-now-include-a-clickable-recording-.md +6 -0
  37. package/corpus/templates/clips/server/db/index.ts +28 -6
  38. package/corpus/templates/clips/server/lib/share-email-hero.ts +105 -0
  39. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +100 -12
  40. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +17 -0
  41. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +169 -22
  42. package/corpus/templates/design/app/hooks/use-question-flow.ts +21 -1
  43. package/corpus/templates/design/changelog/2026-07-28-the-model-you-pick-when-starting-a-new-design-is-now-used-fo.md +6 -0
  44. package/corpus/templates/dispatch/changelog/2026-07-28-pending-apps-are-hidden-by-default-and-app-ownership-details.md +6 -0
  45. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +5 -1
  46. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +5 -0
  47. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +31 -8
  48. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +15 -0
  49. package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +78 -8
  50. package/corpus/templates/slides/app/components/presentation/PresenterView.tsx +179 -0
  51. package/corpus/templates/slides/app/components/presentation/present-channel.ts +15 -0
  52. package/corpus/templates/slides/app/i18n/en-US.ts +10 -1
  53. package/corpus/templates/slides/app/lib/export-google-slides-client.ts +67 -0
  54. package/corpus/templates/slides/app/lib/export-pptx-client.ts +16 -3
  55. package/corpus/templates/slides/app/pages/DeckEditor.tsx +11 -0
  56. package/corpus/templates/slides/app/pages/Presentation.tsx +11 -3
  57. package/corpus/templates/slides/changelog/2026-07-28-export-to-google-slides-now-creates-the-deck-directly-in-you.md +6 -0
  58. package/corpus/templates/slides/changelog/2026-07-28-opening-a-presentation-link-directly-no-longer-bounces-back-.md +6 -0
  59. package/corpus/templates/slides/changelog/2026-07-28-presenter-view-open-a-second-window-while-presenting-to-see-.md +6 -0
  60. package/corpus/templates/slides/server/routes/api/exports/google-slides.post.ts +90 -0
  61. package/corpus/toolkit/CHANGELOG.md +6 -0
  62. package/corpus/toolkit/package.json +1 -1
  63. package/corpus/toolkit/src/composer/TiptapComposer.tsx +143 -111
  64. package/corpus/toolkit/src/composer/runtime-adapters.tsx +2 -0
  65. package/dist/a2a/index.d.ts +2 -0
  66. package/dist/a2a/index.d.ts.map +1 -1
  67. package/dist/a2a/index.js +1 -0
  68. package/dist/a2a/index.js.map +1 -1
  69. package/dist/agent/production-agent.d.ts +2 -2
  70. package/dist/agent/production-agent.d.ts.map +1 -1
  71. package/dist/agent/production-agent.js +6 -3
  72. package/dist/agent/production-agent.js.map +1 -1
  73. package/dist/agent/run-manager.d.ts +11 -0
  74. package/dist/agent/run-manager.d.ts.map +1 -1
  75. package/dist/agent/run-manager.js +34 -1
  76. package/dist/agent/run-manager.js.map +1 -1
  77. package/dist/agent/run-store.d.ts +8 -0
  78. package/dist/agent/run-store.d.ts.map +1 -1
  79. package/dist/agent/run-store.js +16 -0
  80. package/dist/agent/run-store.js.map +1 -1
  81. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  82. package/dist/client/MultiTabAssistantChat.js +60 -31
  83. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  84. package/dist/client/agent-chat.d.ts +6 -0
  85. package/dist/client/agent-chat.d.ts.map +1 -1
  86. package/dist/client/agent-chat.js +13 -1
  87. package/dist/client/agent-chat.js.map +1 -1
  88. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  89. package/dist/client/chat/tool-call-display.js +14 -0
  90. package/dist/client/chat/tool-call-display.js.map +1 -1
  91. package/dist/client/resources/mcp-integration-catalog.d.ts +6 -0
  92. package/dist/client/resources/mcp-integration-catalog.d.ts.map +1 -1
  93. package/dist/client/resources/mcp-integration-catalog.js +16 -0
  94. package/dist/client/resources/mcp-integration-catalog.js.map +1 -1
  95. package/dist/client/settings/AgentsSection.d.ts.map +1 -1
  96. package/dist/client/settings/AgentsSection.js +250 -30
  97. package/dist/client/settings/AgentsSection.js.map +1 -1
  98. package/dist/client/settings/SecretsSection.d.ts.map +1 -1
  99. package/dist/client/settings/SecretsSection.js +16 -2
  100. package/dist/client/settings/SecretsSection.js.map +1 -1
  101. package/dist/client/use-chat-models.d.ts.map +1 -1
  102. package/dist/client/use-chat-models.js +46 -27
  103. package/dist/client/use-chat-models.js.map +1 -1
  104. package/dist/collab/routes.d.ts +2 -2
  105. package/dist/collab/struct-routes.d.ts +1 -1
  106. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  107. package/dist/localization/default-messages.d.ts +9 -0
  108. package/dist/localization/default-messages.d.ts.map +1 -1
  109. package/dist/localization/default-messages.js +9 -0
  110. package/dist/localization/default-messages.js.map +1 -1
  111. package/dist/mcp/ask-app-task-handle.d.ts +25 -0
  112. package/dist/mcp/ask-app-task-handle.d.ts.map +1 -0
  113. package/dist/mcp/ask-app-task-handle.js +136 -0
  114. package/dist/mcp/ask-app-task-handle.js.map +1 -0
  115. package/dist/mcp/builtin-tools.d.ts +3 -3
  116. package/dist/mcp/builtin-tools.d.ts.map +1 -1
  117. package/dist/mcp/builtin-tools.js +115 -38
  118. package/dist/mcp/builtin-tools.js.map +1 -1
  119. package/dist/observability/routes.d.ts +1 -1
  120. package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
  121. package/dist/provider-api/actions/provider-api.d.ts +6 -6
  122. package/dist/secrets/routes.d.ts +9 -9
  123. package/dist/server/agent-capabilities.d.ts +7 -1
  124. package/dist/server/agent-capabilities.d.ts.map +1 -1
  125. package/dist/server/agent-capabilities.js +1 -0
  126. package/dist/server/agent-capabilities.js.map +1 -1
  127. package/dist/server/agent-chat-plugin.d.ts +7 -0
  128. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  129. package/dist/server/agent-chat-plugin.js +16 -1
  130. package/dist/server/agent-chat-plugin.js.map +1 -1
  131. package/dist/server/agent-discovery.d.ts +1 -0
  132. package/dist/server/agent-discovery.d.ts.map +1 -1
  133. package/dist/server/agent-discovery.js +1 -1
  134. package/dist/server/agent-discovery.js.map +1 -1
  135. package/dist/server/agent-peer-probe.d.ts +37 -0
  136. package/dist/server/agent-peer-probe.d.ts.map +1 -0
  137. package/dist/server/agent-peer-probe.js +90 -0
  138. package/dist/server/agent-peer-probe.js.map +1 -0
  139. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  140. package/dist/server/core-routes-plugin.js +53 -0
  141. package/dist/server/core-routes-plugin.js.map +1 -1
  142. package/dist/server/email-template.d.ts +6 -0
  143. package/dist/server/email-template.d.ts.map +1 -1
  144. package/dist/server/email-template.js +6 -0
  145. package/dist/server/email-template.js.map +1 -1
  146. package/dist/server/email.d.ts +7 -0
  147. package/dist/server/email.d.ts.map +1 -1
  148. package/dist/server/email.js +31 -6
  149. package/dist/server/email.js.map +1 -1
  150. package/dist/shared/mcp-embed-headers.d.ts +1 -0
  151. package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
  152. package/dist/shared/mcp-embed-headers.js +4 -0
  153. package/dist/shared/mcp-embed-headers.js.map +1 -1
  154. package/dist/sharing/actions/share-resource.d.ts.map +1 -1
  155. package/dist/sharing/actions/share-resource.js +49 -5
  156. package/dist/sharing/actions/share-resource.js.map +1 -1
  157. package/dist/sharing/registry.d.ts +40 -1
  158. package/dist/sharing/registry.d.ts.map +1 -1
  159. package/dist/sharing/registry.js.map +1 -1
  160. package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +159 -119
  161. package/docs/content/external-agents-catalog.mdx +14 -11
  162. package/docs/content/mcp-protocol.mdx +9 -4
  163. package/package.json +2 -2
  164. package/src/a2a/index.ts +2 -0
  165. package/src/agent/production-agent.ts +6 -1
  166. package/src/agent/run-manager.ts +37 -0
  167. package/src/agent/run-store.ts +18 -0
  168. package/src/client/MultiTabAssistantChat.tsx +75 -43
  169. package/src/client/agent-chat.ts +19 -1
  170. package/src/client/chat/tool-call-display.tsx +25 -0
  171. package/src/client/resources/mcp-integration-catalog.ts +23 -0
  172. package/src/client/settings/AgentsSection.tsx +565 -71
  173. package/src/client/settings/SecretsSection.tsx +71 -36
  174. package/src/client/use-chat-models.ts +54 -37
  175. package/src/localization/default-messages.ts +11 -0
  176. package/src/mcp/ask-app-task-handle.ts +183 -0
  177. package/src/mcp/builtin-tools.ts +146 -34
  178. package/src/server/agent-capabilities.ts +8 -1
  179. package/src/server/agent-chat-plugin.ts +17 -0
  180. package/src/server/agent-discovery.ts +1 -1
  181. package/src/server/agent-peer-probe.ts +133 -0
  182. package/src/server/core-routes-plugin.ts +67 -0
  183. package/src/server/email-template.ts +13 -0
  184. package/src/server/email.ts +40 -4
  185. package/src/shared/mcp-embed-headers.ts +4 -0
  186. package/src/sharing/actions/share-resource.ts +55 -5
  187. package/src/sharing/registry.ts +41 -1
  188. package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +159 -119
@@ -15,6 +15,8 @@ import {
15
15
  getRunEventsSince,
16
16
  getRunById,
17
17
  getRunByThread,
18
+ getRunTurnRef,
19
+ markTurnAborted,
18
20
  cleanupOldRuns,
19
21
  updateRunHeartbeat,
20
22
  bumpRunProgress,
@@ -2095,5 +2097,40 @@ export async function abortRunDurably(
2095
2097
  return abortedInMemory;
2096
2098
  }
2097
2099
 
2100
+ /**
2101
+ * Stop the whole turn `runId` belongs to, not just that run.
2102
+ *
2103
+ * A turn is executed as a chain of runs: every `loop_limit` / `auto_continue`
2104
+ * boundary and every background handoff starts a successor run with a NEW run
2105
+ * id under the same turn id. Aborting one run therefore only ends the current
2106
+ * chunk — the successor claims itself and the turn keeps going, which is what
2107
+ * users see as "Stop didn't stop it". The durable turn-abort marker is the only
2108
+ * thing the successor-claim path (`isTurnAborted`) consults.
2109
+ */
2110
+ export async function abortTurnDurably(
2111
+ runId: string,
2112
+ reason: string = "user",
2113
+ ): Promise<void> {
2114
+ const memRun = activeRuns.get(runId);
2115
+ // In-memory first: a foreground run's SQL insert is async, so the row may not
2116
+ // exist yet when Stop lands moments after send.
2117
+ const ref = memRun
2118
+ ? { threadId: memRun.threadId, turnId: memRun.turnId }
2119
+ : await getRunTurnRef(runId).catch(() => null);
2120
+ if (!ref) return;
2121
+ try {
2122
+ await markTurnAborted(ref.threadId, ref.turnId, reason);
2123
+ } catch (error) {
2124
+ // The current run is already stopped; a failed marker write must not turn
2125
+ // Stop into a 500. Successors will keep running — capture it so that is
2126
+ // visible rather than silent.
2127
+ captureError(error, {
2128
+ route: "/_agent-native/agent-chat/runs/:id/abort",
2129
+ tags: { source: "agent-run-manager", phase: "abort-turn" },
2130
+ extra: { runId, reason, ...ref },
2131
+ });
2132
+ }
2133
+ }
2134
+
2098
2135
  // Re-export so callers can avoid importing from run-store directly.
2099
2136
  export { tryClaimRunSlot } from "./run-store.js";
@@ -2053,6 +2053,24 @@ export async function markTurnAborted(
2053
2053
  );
2054
2054
  }
2055
2055
 
2056
+ /**
2057
+ * Turn identity for a run, or null when the row is unknown. `turn_id` is null
2058
+ * for rows written before the column existed; those runs are their own turn.
2059
+ */
2060
+ export async function getRunTurnRef(
2061
+ runId: string,
2062
+ ): Promise<{ threadId: string; turnId: string } | null> {
2063
+ await ensureRunTables();
2064
+ const { rows } = await getDbExec().execute({
2065
+ sql: `SELECT thread_id, turn_id FROM agent_runs WHERE id = ?`,
2066
+ args: [runId],
2067
+ });
2068
+ const row = rows[0] as { thread_id?: unknown; turn_id?: unknown } | undefined;
2069
+ const threadId = row?.thread_id ? String(row.thread_id) : "";
2070
+ if (!threadId) return null;
2071
+ return { threadId, turnId: row?.turn_id ? String(row.turn_id) : runId };
2072
+ }
2073
+
2056
2074
  export async function isTurnAborted(
2057
2075
  threadId: string,
2058
2076
  turnId: string,
@@ -88,6 +88,8 @@ interface PendingSend {
88
88
  interface PendingDelivery {
89
89
  threadId: string | null;
90
90
  send: PendingSend;
91
+ /** Applied to whichever thread this send lands on; a queued send outlives the handler that parsed it. */
92
+ modelOverride?: ModelSelection;
91
93
  }
92
94
 
93
95
  /** The single path that hands a queued send to a mounted chat ref. */
@@ -154,28 +156,28 @@ function resolveModelSelection(
154
156
  groups: EngineModelGroup[],
155
157
  ): ModelSelection | undefined {
156
158
  if (!selection?.model) return undefined;
157
- if (groups.length === 0) {
158
- return {
159
- model: selection.model,
160
- effort: resolveReasoningEffortSelection(
161
- selection.model,
162
- selection.effort,
163
- ),
164
- };
165
- }
166
- const preferredGroup = groups.find(
167
- (group) =>
168
- group.engine === selection.engine &&
169
- group.models.includes(selection.model),
170
- );
171
- const fallbackGroup = groups.find((group) =>
172
- group.models.includes(selection.model),
173
- );
174
- if (groups.length > 0 && !preferredGroup && !fallbackGroup) {
175
- return undefined;
176
- }
159
+ // Engine precedence turns on whether the catalog OFFERS the supplied engine,
160
+ // not on whether that engine advertises this model:
161
+ // offered honor it. A gateway's advertised list is its built-in
162
+ // catalog, not what the endpoint serves, and one model id
163
+ // sits under several groups (claude-sonnet-5 under both
164
+ // anthropic and builder) — so a model-only match would
165
+ // reroute the turn to a different provider and bill it there.
166
+ // not offered → heal to a group that serves the model. A selection left
167
+ // on `builder` after disconnecting it must still run on the
168
+ // user's own key; the same model through another configured
169
+ // provider is the point of bring-your-own-key.
170
+ // `builder` is in the catalog exactly when Builder is connected, so this is a
171
+ // real availability signal rather than a guess.
172
+ const suppliedEngine = selection.engine?.trim()
173
+ ? selection.engine
174
+ : undefined;
177
175
  const engine =
178
- preferredGroup?.engine ?? fallbackGroup?.engine ?? selection.engine;
176
+ (groups.some((group) => group.engine === suppliedEngine)
177
+ ? suppliedEngine
178
+ : undefined) ??
179
+ groups.find((group) => group.models.includes(selection.model))?.engine ??
180
+ suppliedEngine;
179
181
  if (!engine && groups.length > 0) return undefined;
180
182
 
181
183
  const effort = resolveReasoningEffortSelection(
@@ -1120,7 +1122,14 @@ export function MultiTabAssistantChat({
1120
1122
  .catch(() => null),
1121
1123
  ])
1122
1124
  .then(([enginesData, envKeys, builderStatus]) => {
1123
- if (!enginesData?.engines) return;
1125
+ if (!enginesData?.engines) {
1126
+ // Leaves `availableModels` empty for the session, so an override with
1127
+ // no engine of its own has nothing to resolve against.
1128
+ console.warn(
1129
+ "[agent-chat] no engine list; model overrides cannot be catalog-resolved",
1130
+ );
1131
+ return;
1132
+ }
1124
1133
  const configuredKeys = new Set(
1125
1134
  (envKeys as Array<{ key: string; configured: boolean }>)
1126
1135
  .filter((k) => k.configured)
@@ -1598,6 +1607,7 @@ export function MultiTabAssistantChat({
1598
1607
  context,
1599
1608
  openSidebar,
1600
1609
  model,
1610
+ engine,
1601
1611
  effort,
1602
1612
  newTab,
1603
1613
  background,
@@ -1638,32 +1648,44 @@ export function MultiTabAssistantChat({
1638
1648
  ...(submitMessageId ? { submitMessageId } : {}),
1639
1649
  };
1640
1650
 
1651
+ // Resolved once, up front, and carried with the send until a thread
1652
+ // exists to key it by. Applying it only when `model` matched
1653
+ // `availableModels` dropped every override that arrived before the
1654
+ // engines fetch resolved — and the cold-start replay below runs at mount,
1655
+ // when that list is always still empty.
1656
+ let modelOverride: ModelSelection | undefined;
1657
+ if (model) {
1658
+ const catalogEngine = availableModels.find((g) =>
1659
+ g.models.includes(model),
1660
+ )?.engine;
1661
+ const overrideEngine = engine ?? catalogEngine;
1662
+ if (!overrideEngine && availableModels.length > 0) {
1663
+ console.warn(
1664
+ `[agent-chat] model override "${model}" is in no engine group and carries no engine; the server will substitute its default`,
1665
+ );
1666
+ }
1667
+ modelOverride = {
1668
+ model,
1669
+ ...(overrideEngine ? { engine: overrideEngine } : {}),
1670
+ effort: resolveReasoningEffortSelection(
1671
+ model,
1672
+ isReasoningEffort(effort) ? effort : undefined,
1673
+ ),
1674
+ };
1675
+ }
1676
+
1641
1677
  const sendToTab = (threadId: string) => {
1642
1678
  if (isAgentChatSubmitCancelled(submitMessageId)) return;
1643
- // If a model override was specified, apply it only if we recognize it
1644
- if (model) {
1645
- const matchedGroup = availableModels.find((g) =>
1646
- g.models.includes(model),
1647
- );
1648
- if (matchedGroup) {
1649
- const selectedEffort = resolveReasoningEffortSelection(
1650
- model,
1651
- isReasoningEffort(effort) ? effort : undefined,
1652
- );
1653
- threadModelRef.current.set(threadId, {
1654
- model,
1655
- engine: matchedGroup.engine,
1656
- effort: selectedEffort,
1657
- });
1658
- bumpModelSelectionVersion();
1659
- }
1679
+ if (modelOverride) {
1680
+ threadModelRef.current.set(threadId, modelOverride);
1681
+ bumpModelSelectionVersion();
1660
1682
  }
1661
1683
 
1662
1684
  const ref = chatRefs.current.get(threadId);
1663
1685
  if (ref) {
1664
1686
  deliverPendingSend(ref, send);
1665
1687
  } else {
1666
- pendingDeliveries.current.push({ threadId, send });
1688
+ pendingDeliveries.current.push({ threadId, send, modelOverride });
1667
1689
  }
1668
1690
  };
1669
1691
 
@@ -1709,7 +1731,11 @@ export function MultiTabAssistantChat({
1709
1731
  } else {
1710
1732
  // Cold start: no thread yet. Queue for the first active thread (the
1711
1733
  // bootstrap effect creates it) rather than racing a second create.
1712
- pendingDeliveries.current.push({ threadId: null, send });
1734
+ pendingDeliveries.current.push({
1735
+ threadId: null,
1736
+ send,
1737
+ modelOverride,
1738
+ });
1713
1739
  }
1714
1740
  }
1715
1741
  };
@@ -1762,16 +1788,22 @@ export function MultiTabAssistantChat({
1762
1788
  if (isAgentChatSubmitCancelled(delivery.send.submitMessageId)) continue;
1763
1789
  const threadId = delivery.threadId ?? active ?? null;
1764
1790
  const ref = threadId ? chatRefs.current.get(threadId) : null;
1791
+ if (threadId && delivery.modelOverride) {
1792
+ threadModelRef.current.set(threadId, delivery.modelOverride);
1793
+ bumpModelSelectionVersion();
1794
+ }
1765
1795
  if (threadId && ref) {
1766
1796
  const { send } = delivery;
1767
1797
  setTimeout(() => deliverPendingSend(ref, send), 50);
1768
1798
  } else {
1769
1799
  // Not ready — keep it, pinning the resolved threadId once known.
1770
- remaining.push(threadId ? { threadId, send: delivery.send } : delivery);
1800
+ remaining.push(
1801
+ threadId ? { ...delivery, threadId, send: delivery.send } : delivery,
1802
+ );
1771
1803
  }
1772
1804
  }
1773
1805
  pendingDeliveries.current = remaining;
1774
- }, [openTabIds, activeThreadId]);
1806
+ }, [openTabIds, activeThreadId, bumpModelSelectionVersion]);
1775
1807
 
1776
1808
  // Listen for chatRunning completion events
1777
1809
  useEffect(() => {
@@ -858,6 +858,17 @@ function normalizeAgentChatRequestMode(
858
858
  return value === "act" || value === "plan" ? value : undefined;
859
859
  }
860
860
 
861
+ /**
862
+ * Composers submit `engine: ""` whenever the engines list failed to load. An
863
+ * empty string is not nullish, so it survives `??` yet reads as falsy — one
864
+ * value meaning both "specified" and "absent". Absent is decided here, once.
865
+ */
866
+ function nonEmptyString(value: unknown): string | undefined {
867
+ if (typeof value !== "string") return undefined;
868
+ const trimmed = value.trim();
869
+ return trimmed ? trimmed : undefined;
870
+ }
871
+
861
872
  /** A normalized `agentNative.submitChat` payload — decode via {@link parseSubmitChatMessage}. */
862
873
  export interface ParsedSubmitChat {
863
874
  /** Visible prompt text (non-empty). */
@@ -867,6 +878,12 @@ export interface ParsedSubmitChat {
867
878
  submit: boolean;
868
879
  openSidebar?: boolean;
869
880
  model?: string;
881
+ /**
882
+ * Engine paired with `model`. The receiver cannot re-derive it for an id the
883
+ * catalog omits, and a model sent without one is normalized to the resolved
884
+ * engine's default server-side.
885
+ */
886
+ engine?: string;
870
887
  /** Raw effort hint; the receiver validates it against the model. */
871
888
  effort?: unknown;
872
889
  newTab?: boolean;
@@ -907,7 +924,8 @@ export function parseSubmitChatMessage(
907
924
  submit: raw.submit !== false,
908
925
  openSidebar:
909
926
  typeof raw.openSidebar === "boolean" ? raw.openSidebar : undefined,
910
- model: typeof raw.model === "string" ? raw.model : undefined,
927
+ model: nonEmptyString(raw.model),
928
+ engine: nonEmptyString(raw.engine),
911
929
  effort: raw.effort,
912
930
  newTab: typeof raw.newTab === "boolean" ? raw.newTab : undefined,
913
931
  background:
@@ -41,6 +41,7 @@ import {
41
41
  import { ConnectBuilderCard } from "../ConnectBuilderCard.js";
42
42
  import { useT } from "../i18n.js";
43
43
  import { McpAppRenderer } from "../mcp-apps/McpAppRenderer.js";
44
+ import { findMcpIntegrationForToolName } from "../resources/mcp-integration-catalog.js";
44
45
  import type { AgentCallProgress, ContentPart } from "../sse-event-processor.js";
45
46
  import {
46
47
  BashCell,
@@ -279,7 +280,31 @@ type ToolIconComponent = React.ComponentType<{
279
280
  size?: number | string;
280
281
  }>;
281
282
 
283
+ const brandIcons = new Map<string, ToolIconComponent>();
284
+
285
+ function brandToolIcon(logoUrl: string, name: string): ToolIconComponent {
286
+ const cached = brandIcons.get(logoUrl);
287
+ if (cached) return cached;
288
+ const Icon: ToolIconComponent = ({ className, size }) => (
289
+ <img
290
+ src={logoUrl}
291
+ alt=""
292
+ aria-hidden
293
+ width={size}
294
+ height={size}
295
+ title={name}
296
+ className={cn("rounded-[3px] object-contain", className)}
297
+ />
298
+ );
299
+ brandIcons.set(logoUrl, Icon);
300
+ return Icon;
301
+ }
302
+
282
303
  function resolveToolIcon(toolName: string): ToolIconComponent {
304
+ const integration = findMcpIntegrationForToolName(toolName);
305
+ if (integration) {
306
+ return brandToolIcon(integration.logoUrl, integration.name);
307
+ }
283
308
  const name = toolName.toLowerCase();
284
309
  if (name.includes("slack")) return IconBrandSlack;
285
310
  if (
@@ -803,6 +803,29 @@ export function findMcpIntegrationForText(
803
803
  return null;
804
804
  }
805
805
 
806
+ /**
807
+ * Matches the server segment of an `mcp__<server>__<tool>` name, not prose, so
808
+ * ambiguous brand words ("box", "monday", "linear") are safe here in a way they
809
+ * are not in `findMcpIntegrationForText`.
810
+ */
811
+ export function findMcpIntegrationForToolName(
812
+ toolName: string,
813
+ integrations: readonly DefaultMcpIntegration[] = DEFAULT_MCP_INTEGRATIONS,
814
+ ): DefaultMcpIntegration | null {
815
+ const server = /^mcp__(.+?)__/.exec(toolName.toLowerCase())?.[1];
816
+ if (!server) return null;
817
+ return (
818
+ integrations.find((integration) =>
819
+ [
820
+ integration.id,
821
+ integration.provider,
822
+ ...(integration.brandAliases ?? []),
823
+ ...(integration.aliases ?? []),
824
+ ].some((alias) => textContainsTerm(server, alias)),
825
+ ) ?? null
826
+ );
827
+ }
828
+
806
829
  export function isMcpConnectionFailureText(text: string): boolean {
807
830
  return /\b(?:can(?:not|'t|’t)|could(?: not|n't|n’t)|unable|failed|don't have access|don’t have access|not connected|not able)\b[\s\S]{0,80}\b(?:read|access|open|see|fetch|connect)\b/i.test(
808
831
  text,