@nextclaw/ui 0.12.9 → 0.12.11

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 (245) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/dist/assets/ChannelsList-SQ7Oxotv.js +8 -0
  3. package/dist/assets/DocBrowser-BCO2k6XD.js +1 -0
  4. package/dist/assets/{DocBrowser-6ReNjvzF.js → DocBrowser-rDOjI3ga.js} +1 -1
  5. package/dist/assets/{DocBrowserContext-B6SpA7Qs.js → DocBrowserContext-BUq3Wo8O.js} +1 -1
  6. package/dist/assets/{LogoBadge-ByNLYg65.js → LogoBadge-DP8Ye7wJ.js} +1 -1
  7. package/dist/assets/ModelConfig-C77Ae9ru.js +1 -0
  8. package/dist/assets/ProviderScopedModelInput-CEnK61uo.js +1 -0
  9. package/dist/assets/ProvidersList-BCupBayq.js +1 -0
  10. package/dist/assets/RuntimeConfig-Ad-CAcmy.js +1 -0
  11. package/dist/assets/SearchConfig-BfCz4wJ4.js +1 -0
  12. package/dist/assets/SecretsConfig-DjmBIhyy.js +3 -0
  13. package/dist/assets/{SessionsConfig-ChHQ7M5c.js → SessionsConfig-CvjxU40H.js} +2 -2
  14. package/dist/assets/{book-open-BdcxxoQu.js → book-open-BE8M56IM.js} +1 -1
  15. package/dist/assets/chat-page-JKC6ln-y.js +58 -0
  16. package/dist/assets/chat-session-display-YcRMrAMa.js +1 -0
  17. package/dist/assets/{chunk-JZWAC4HX-DK5HPmIK.js → chunk-JZWAC4HX-erTUn3b8.js} +1 -1
  18. package/dist/assets/client-CszWMVKi.js +7 -0
  19. package/dist/assets/config-split-page-BAGSzUR3.js +1 -0
  20. package/dist/assets/{createLucideIcon-BSeTgkZW.js → createLucideIcon-CCiTGX8L.js} +1 -1
  21. package/dist/assets/desktop-DfkLlkG2.js +1 -0
  22. package/dist/assets/desktop-update-config-BXeGlqHD.js +1 -0
  23. package/dist/assets/dialog-BghZFPch.js +5 -0
  24. package/dist/assets/{dist-6TrrnPCR.js → dist-Dd9cr-kz.js} +1 -1
  25. package/dist/assets/dist-ZwoAXs46.js +9 -0
  26. package/dist/assets/{download-BhDxnyvU.js → download-D7LOizcW.js} +1 -1
  27. package/dist/assets/es2015-CEAreese.js +41 -0
  28. package/dist/assets/{external-link-BgErLCNT.js → external-link-qsnCMhw1.js} +1 -1
  29. package/dist/assets/{hash-Bl7dr_UG.js → hash-0zjWsNl-.js} +1 -1
  30. package/dist/assets/{i18n-eDHeDY0n.js → i18n-DvzXOGQX.js} +1 -1
  31. package/dist/assets/index-DvVTC9FF.css +1 -0
  32. package/dist/assets/index-lr6rQUSd.js +2 -0
  33. package/dist/assets/key-round-BLe9D8ND.js +1 -0
  34. package/dist/assets/loader-circle-wj7kARHv.js +1 -0
  35. package/dist/assets/{logos-x89HbrZ4.js → logos-_v5b2SdG.js} +1 -1
  36. package/dist/assets/marketplace-page-CAAk1Khc.js +1 -0
  37. package/dist/assets/marketplace-page-CfCiq90S.js +49 -0
  38. package/dist/assets/mcp-marketplace-page-D0Pp9Hs-.js +40 -0
  39. package/dist/assets/play-o6NmwGTi.js +1 -0
  40. package/dist/assets/plus-I9pBS4Fl.js +1 -0
  41. package/dist/assets/{refresh-cw-C47QSEwg.js → refresh-cw-MNqgR3LZ.js} +1 -1
  42. package/dist/assets/remote-C9fXm4V5.js +1 -0
  43. package/dist/assets/{save-3S6-H3Xw.js → save-D4bObrmH.js} +1 -1
  44. package/dist/assets/search-DxmL3IWE.js +1 -0
  45. package/dist/assets/security-config-BUm6FFfl.js +1 -0
  46. package/dist/assets/select-BILPf7zs.js +1 -0
  47. package/dist/assets/setting-row-BATDgg4r.js +1 -0
  48. package/dist/assets/skeleton-COKMAnJy.js +1 -0
  49. package/dist/assets/{switch-BsLtHOH-.js → switch-CBOzecWS.js} +1 -1
  50. package/dist/assets/{tabs-custom-D3HYMt6k.js → tabs-custom-Bx3cNhD-.js} +1 -1
  51. package/dist/assets/tag-chip-zUaDE2-H.js +1 -0
  52. package/dist/assets/{trash-2-G48scll7.js → trash-2-CQUgYyRn.js} +1 -1
  53. package/dist/assets/use-infinite-scroll-loader-B5V2Klve.js +1 -0
  54. package/dist/assets/useConfirmDialog-patAnl1g.js +1 -0
  55. package/dist/assets/{useMutation-CBWjE2uj.js → useMutation-__AYv-Pz.js} +1 -1
  56. package/dist/assets/x-BHUGQIUv.js +1 -0
  57. package/dist/index.html +22 -22
  58. package/dist/runtime-icons/claude.ico +0 -0
  59. package/dist/runtime-icons/codex-openai.svg +6 -0
  60. package/dist/runtime-icons/hermes-agent.png +0 -0
  61. package/module-structure.config.json +7 -0
  62. package/package.json +6 -6
  63. package/public/runtime-icons/claude.ico +0 -0
  64. package/public/runtime-icons/codex-openai.svg +6 -0
  65. package/public/runtime-icons/hermes-agent.png +0 -0
  66. package/src/api/chat-session-type.types.ts +7 -0
  67. package/src/api/config.ts +10 -0
  68. package/src/api/raw-client.test.ts +1 -1
  69. package/src/api/{raw-client.ts → raw-client.utils.ts} +2 -0
  70. package/src/api/runtime-control.types.ts +8 -0
  71. package/src/api/types.ts +48 -0
  72. package/src/app/components/app-manager-provider.tsx +20 -0
  73. package/src/app/managers/app.manager.ts +12 -0
  74. package/src/app.tsx +223 -59
  75. package/src/components/agents/agent-dialogs.tsx +499 -0
  76. package/src/components/agents/agents-page.test.tsx +238 -0
  77. package/src/components/agents/agents-page.tsx +435 -0
  78. package/src/components/chat/chat-conversation-panel.test.tsx +30 -0
  79. package/src/components/chat/chat-conversation-panel.tsx +83 -13
  80. package/src/components/chat/chat-input/ncp-chat-input-availability.utils.test.ts +92 -0
  81. package/src/components/chat/chat-input/ncp-chat-input-availability.utils.ts +45 -0
  82. package/src/components/chat/chat-page-shell.tsx +19 -13
  83. package/src/components/chat/chat-session-type-option-item.test.tsx +46 -0
  84. package/src/components/chat/chat-session-type-option-item.tsx +68 -0
  85. package/src/components/chat/chat-session-workspace-file-preview.test.tsx +87 -0
  86. package/src/components/chat/chat-session-workspace-file-preview.tsx +14 -43
  87. package/src/components/chat/chat-session-workspace-panel-nav.tsx +8 -2
  88. package/src/components/chat/chat-sidebar-project-groups.tsx +11 -36
  89. package/src/components/chat/containers/chat-input-bar.container.tsx +24 -12
  90. package/src/components/chat/{ChatSidebar.test.tsx → containers/chat-sidebar.test.tsx} +5 -4
  91. package/src/components/chat/{ChatSidebar.tsx → containers/chat-sidebar.tsx} +24 -72
  92. package/src/components/chat/hooks/use-chat-sidebar-session-label-editor.ts +49 -0
  93. package/src/components/chat/ncp/__tests__/ncp-session-adapter.cancelled-tool.test.ts +77 -0
  94. package/src/components/chat/ncp/ncp-app-client-fetch.ts +3 -0
  95. package/src/components/chat/ncp/ncp-chat-input.manager.ts +13 -5
  96. package/src/components/chat/ncp/ncp-chat-page.tsx +23 -2
  97. package/src/components/chat/ncp/ncp-session-adapter.test.ts +1 -0
  98. package/src/components/chat/ncp/ncp-session-adapter.ts +3 -0
  99. package/src/components/chat/ncp/page/ncp-chat-derived-state.ts +10 -4
  100. package/src/components/chat/ncp/session-conversation/use-ncp-session-conversation.test.tsx +48 -4
  101. package/src/components/chat/ncp/session-conversation/use-ncp-session-conversation.ts +43 -5
  102. package/src/components/chat/ncp/tests/ncp-chat-input.manager.test.ts +51 -1
  103. package/src/components/chat/stores/chat-input.store.ts +2 -1
  104. package/src/components/chat/stores/chat-thread.store.ts +3 -1
  105. package/src/components/chat/useChatSessionTypeState.ts +10 -1
  106. package/src/components/chat/workspace/chat-session-workspace-file-breadcrumbs.tsx +86 -0
  107. package/src/components/common/BrandHeader.tsx +3 -1
  108. package/src/components/common/session-context-icon.tsx +15 -2
  109. package/src/components/common/{TagInput.tsx → tag-input.tsx} +25 -17
  110. package/src/components/config/ChannelForm.test.tsx +89 -3
  111. package/src/components/config/ChannelForm.tsx +157 -188
  112. package/src/components/config/ChannelsList.test.tsx +163 -119
  113. package/src/components/config/ChannelsList.tsx +90 -101
  114. package/src/components/config/ProviderForm.tsx +108 -146
  115. package/src/components/config/ProvidersList.tsx +100 -123
  116. package/src/components/config/SearchConfig.tsx +423 -393
  117. package/src/components/config/channel-form-fields-section.tsx +70 -37
  118. package/src/components/config/config-split-page.tsx +109 -0
  119. package/src/components/config/desktop-update-config.test.tsx +10 -4
  120. package/src/components/config/desktop-update-config.tsx +5 -3
  121. package/src/components/config/provider-enabled-field.tsx +17 -10
  122. package/src/components/config/runtime-control-card.test.tsx +136 -158
  123. package/src/components/config/runtime-control-card.tsx +43 -68
  124. package/src/components/config/runtime-presence-card.test.tsx +10 -14
  125. package/src/components/config/runtime-presence-card.tsx +97 -81
  126. package/src/components/layout/AppLayout.tsx +25 -37
  127. package/src/components/layout/Sidebar.tsx +4 -4
  128. package/src/components/layout/app-layout.test.tsx +46 -14
  129. package/src/components/layout/runtime-status-entry.test.tsx +101 -0
  130. package/src/components/layout/runtime-status-entry.tsx +95 -0
  131. package/src/components/layout/sidebar.layout.test.tsx +11 -5
  132. package/src/components/marketplace/marketplace-detail-doc.ts +93 -0
  133. package/src/components/marketplace/marketplace-list-card.tsx +288 -0
  134. package/src/components/marketplace/marketplace-page-data.ts +129 -0
  135. package/src/components/marketplace/marketplace-page.test.tsx +339 -0
  136. package/src/components/marketplace/marketplace-page.tsx +596 -0
  137. package/src/components/marketplace/mcp/mcp-marketplace-card.tsx +128 -0
  138. package/src/components/marketplace/mcp/mcp-marketplace-dialogs.tsx +191 -0
  139. package/src/components/marketplace/mcp/mcp-marketplace-doc.ts +152 -0
  140. package/src/components/marketplace/mcp/mcp-marketplace-page.test.tsx +223 -0
  141. package/src/components/marketplace/mcp/mcp-marketplace-page.tsx +414 -0
  142. package/src/components/ui/notice-card.tsx +129 -0
  143. package/src/components/ui/setting-row.tsx +51 -0
  144. package/src/components/ui/tag-chip.tsx +39 -0
  145. package/src/components/ui/textarea.tsx +19 -0
  146. package/src/features/account/components/account-panel.tsx +255 -0
  147. package/src/features/account/index.ts +6 -0
  148. package/src/{account → features/account}/managers/account.manager.ts +6 -5
  149. package/src/features/remote/components/remote-access-page.test.tsx +104 -0
  150. package/src/features/remote/components/remote-access-page.tsx +250 -0
  151. package/src/{hooks/useRemoteAccess.ts → features/remote/hooks/use-remote-access.ts} +1 -1
  152. package/src/features/remote/index.ts +27 -0
  153. package/src/{remote → features/remote}/managers/remote-access.manager.ts +3 -4
  154. package/src/{remote → features/remote/services}/remote-access-feedback.service.test.ts +1 -1
  155. package/src/features/system-status/hooks/use-system-status.ts +104 -0
  156. package/src/features/system-status/index.ts +12 -0
  157. package/src/features/system-status/managers/system-status.manager.bootstrap-polling.test.ts +126 -0
  158. package/src/features/system-status/managers/system-status.manager.test.ts +142 -0
  159. package/src/features/system-status/managers/system-status.manager.ts +511 -0
  160. package/src/features/system-status/stores/system-status.store.ts +32 -0
  161. package/src/features/system-status/types/system-status.types.ts +73 -0
  162. package/src/features/system-status/utils/system-status.utils.test.ts +132 -0
  163. package/src/features/system-status/utils/system-status.utils.ts +202 -0
  164. package/src/hooks/use-realtime-query-bridge.ts +34 -18
  165. package/src/hooks/useConfig.ts +2 -1
  166. package/src/index.css +24 -0
  167. package/src/lib/app-resource-uri.test.ts +20 -0
  168. package/src/lib/app-resource-uri.ts +29 -0
  169. package/src/lib/i18n.chat.ts +8 -0
  170. package/src/lib/i18n.remote.ts +1 -1
  171. package/src/lib/i18n.runtime-control.ts +31 -0
  172. package/src/lib/i18n.ts +5 -8
  173. package/src/lib/session-context.utils.test.ts +71 -0
  174. package/src/lib/session-context.utils.ts +28 -3
  175. package/src/lib/session-project/workspace-file-breadcrumb.test.ts +83 -0
  176. package/src/lib/session-project/workspace-file-breadcrumb.ts +188 -0
  177. package/src/platforms/desktop/index.ts +20 -0
  178. package/src/{desktop → platforms/desktop}/managers/desktop-presence.manager.ts +2 -2
  179. package/src/{desktop → platforms/desktop}/managers/desktop-update.manager.ts +2 -2
  180. package/src/{desktop → platforms/desktop}/stores/desktop-presence.store.ts +1 -1
  181. package/src/{desktop → platforms/desktop}/stores/desktop-update.store.ts +1 -1
  182. package/src/stores/ui.store.ts +0 -9
  183. package/src/transport/{app-client.ts → app-client.service.ts} +9 -9
  184. package/src/transport/app-client.test.ts +9 -5
  185. package/src/transport/index.ts +1 -1
  186. package/src/transport/{local.transport.ts → local-transport.service.ts} +14 -12
  187. package/dist/assets/ChannelsList-Ita2Zm1_.js +0 -8
  188. package/dist/assets/DocBrowser-BNwbPHf4.js +0 -1
  189. package/dist/assets/MarketplacePage-CjX2MWww.js +0 -1
  190. package/dist/assets/MarketplacePage-D0sDlYX4.js +0 -49
  191. package/dist/assets/McpMarketplacePage-BGKJm1sJ.js +0 -40
  192. package/dist/assets/ModelConfig-BzZenCH-.js +0 -1
  193. package/dist/assets/ProviderScopedModelInput-Da7khnBA.js +0 -1
  194. package/dist/assets/ProvidersList-BbVzRxjY.js +0 -1
  195. package/dist/assets/RemoteAccessPage-BaDH_X1Q.js +0 -1
  196. package/dist/assets/RuntimeConfig-F_XKGgLm.js +0 -1
  197. package/dist/assets/SearchConfig-BGkzXQP-.js +0 -1
  198. package/dist/assets/SecretsConfig-D281Rotl.js +0 -3
  199. package/dist/assets/app-query-client-VnFElj4E.js +0 -1
  200. package/dist/assets/chat-page-Doe0yTtB.js +0 -58
  201. package/dist/assets/chat-session-display-cw78aiI_.js +0 -1
  202. package/dist/assets/client-_i4MU2bB.js +0 -7
  203. package/dist/assets/config-DtIQwrHF.js +0 -1
  204. package/dist/assets/config-layout-CHs0mAaR.js +0 -1
  205. package/dist/assets/desktop-update-config-Dpcf4BKG.js +0 -1
  206. package/dist/assets/dist-ccBFUi-o.js +0 -9
  207. package/dist/assets/index-CF9xve0E.js +0 -6
  208. package/dist/assets/index-FgA52VBt.css +0 -1
  209. package/dist/assets/infiniteQueryBehavior-ZDS92Qpp.js +0 -1
  210. package/dist/assets/loader-circle-ACM1s51e.js +0 -1
  211. package/dist/assets/page-layout-vZnghcFy.js +0 -1
  212. package/dist/assets/play-CFUwCA2E.js +0 -1
  213. package/dist/assets/plus-rYsv72JG.js +0 -1
  214. package/dist/assets/popover-Bg1VoTZ6.js +0 -1
  215. package/dist/assets/refresh-ccw-DT98i__E.js +0 -1
  216. package/dist/assets/rotate-cw-JtFzpNn6.js +0 -1
  217. package/dist/assets/search-3kFR_zh9.js +0 -1
  218. package/dist/assets/security-config-BWaiARNk.js +0 -1
  219. package/dist/assets/select-DJ2MUjBB.js +0 -41
  220. package/dist/assets/skeleton-ByQepn0M.js +0 -1
  221. package/dist/assets/status-dot-vbanNPFU.js +0 -1
  222. package/dist/assets/use-infinite-scroll-loader-DkNhD-42.js +0 -1
  223. package/dist/assets/useConfirmDialog-BkvTN-vd.js +0 -1
  224. package/dist/assets/x-ByDbItbq.js +0 -1
  225. package/src/account/components/account-panel.tsx +0 -135
  226. package/src/components/agents/AgentDialogs.tsx +0 -400
  227. package/src/components/agents/AgentsPage.test.tsx +0 -217
  228. package/src/components/agents/AgentsPage.tsx +0 -352
  229. package/src/components/config/config-layout.ts +0 -10
  230. package/src/components/marketplace/MarketplacePage.test.tsx +0 -322
  231. package/src/components/marketplace/MarketplacePage.tsx +0 -827
  232. package/src/components/marketplace/mcp/McpMarketplacePage.test.tsx +0 -208
  233. package/src/components/marketplace/mcp/McpMarketplacePage.tsx +0 -580
  234. package/src/components/remote/RemoteAccessPage.test.tsx +0 -103
  235. package/src/components/remote/RemoteAccessPage.tsx +0 -144
  236. package/src/hooks/use-runtime-control.ts +0 -24
  237. package/src/presenter/app-presenter-context.tsx +0 -20
  238. package/src/presenter/app.presenter.ts +0 -12
  239. package/src/runtime-control/runtime-control.manager.ts +0 -118
  240. /package/dist/assets/{config-hints-BhTmc9P1.js → config-hints-DSQQbeOA.js} +0 -0
  241. /package/src/{account → features/account}/stores/account.store.ts +0 -0
  242. /package/src/{remote → features/remote/services}/remote-access-feedback.service.ts +0 -0
  243. /package/src/{remote/remote-access.query.ts → features/remote/services/remote-access-query.service.ts} +0 -0
  244. /package/src/{remote → features/remote}/stores/remote-access.store.ts +0 -0
  245. /package/src/{desktop → platforms/desktop/types}/desktop-update.types.ts +0 -0
@@ -22,7 +22,10 @@ import {
22
22
  } from "@/components/chat/chat-session-route";
23
23
  import { useNcpChatPageData } from "@/components/chat/ncp/ncp-chat-page-data";
24
24
  import { NcpChatPresenter } from "@/components/chat/ncp/ncp-chat.presenter";
25
- import { useNcpSessionConversation } from "@/components/chat/ncp/session-conversation/use-ncp-session-conversation";
25
+ import {
26
+ isNcpAgentStartupUnavailableErrorMessage,
27
+ useNcpSessionConversation,
28
+ } from "@/components/chat/ncp/session-conversation/use-ncp-session-conversation";
26
29
  import { useNcpChatDerivedState, useNcpChatSnapshotSync } from "@/components/chat/ncp/page/ncp-chat-derived-state";
27
30
  import { ChatPresenterProvider } from "@/components/chat/presenter/chat-presenter-context";
28
31
  import type { ResumeRunParams } from "@/components/chat/chat-stream/types";
@@ -31,6 +34,10 @@ import { useChatSessionListStore } from "@/components/chat/stores/chat-session-l
31
34
  import { useConfirmDialog } from "@/hooks/useConfirmDialog";
32
35
  import { useAgents } from "@/hooks/agents/useAgents";
33
36
  import { normalizeRequestedSkills } from "@/lib/chat-runtime-utils";
37
+ import {
38
+ systemStatusManager,
39
+ useChatRuntimeAvailability,
40
+ } from "@/features/system-status";
34
41
  import {
35
42
  getSessionProjectName,
36
43
  normalizeSessionProjectRootValue,
@@ -119,6 +126,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
119
126
  const pendingProjectRootSessionKey = useChatInputStore(
120
127
  (state) => state.snapshot.pendingProjectRootSessionKey,
121
128
  );
129
+ const runtimeAvailability = useChatRuntimeAvailability();
122
130
  const agentsQuery = useAgents();
123
131
  const currentSelectedModel = useChatInputStore(
124
132
  (state) => state.snapshot.selectedModel,
@@ -182,8 +190,19 @@ export function NcpChatPage({ view }: ChatPageProps) {
182
190
  const isAwaitingAssistantOutput = agent.isRunning;
183
191
  const canStopCurrentRun = agent.isRunning;
184
192
  const stopDisabledReason = agent.isRunning ? null : "__preparing__";
185
- const lastSendError =
193
+ const rawLastSendError =
186
194
  agent.hydrateError?.message ?? agent.snapshot.error?.message ?? null;
195
+ const filteredLastSendError =
196
+ runtimeAvailability.phase === "ready" &&
197
+ isNcpAgentStartupUnavailableErrorMessage(rawLastSendError)
198
+ ? null
199
+ : rawLastSendError;
200
+ const lastSendError =
201
+ runtimeAvailability.isBlocked
202
+ ? null
203
+ : runtimeAvailability.phase === "ready"
204
+ ? filteredLastSendError
205
+ : systemStatusManager.getDisplayMessage(filteredLastSendError);
187
206
 
188
207
  useEffect(() => {
189
208
  presenter.chatStreamActionsManager.bind({
@@ -304,6 +323,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
304
323
  currentAgent,
305
324
  parentSession,
306
325
  currentSessionTypeLabel,
326
+ currentSessionTypeIcon,
307
327
  currentChildSessionTabs,
308
328
  } = useNcpChatDerivedState({
309
329
  sessionKey,
@@ -340,6 +360,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
340
360
  isSkillsLoading: sessionSkillsQuery.isLoading,
341
361
  sessionTypeUnavailableMessage,
342
362
  currentSessionTypeLabel,
363
+ currentSessionTypeIcon,
343
364
  sessionKey,
344
365
  currentAgentId,
345
366
  currentAgent,
@@ -316,6 +316,7 @@ describe('adaptNcpMessageToUiMessage tool rendering', () => {
316
316
  },
317
317
  });
318
318
  });
319
+
319
320
  });
320
321
 
321
322
  describe('readNcpSessionPreferredThinking', () => {
@@ -151,6 +151,9 @@ function parseSessionContext(sessionKey: string): { channel?: string; type?: str
151
151
  }
152
152
 
153
153
  function mapToolStatus(part: Extract<NcpMessagePart, { type: 'tool-invocation' }>): ToolInvocationStatus {
154
+ if (part.state === 'cancelled') {
155
+ return ToolInvocationStatus.CANCELLED;
156
+ }
154
157
  if (part.state === 'result') {
155
158
  return ToolInvocationStatus.RESULT;
156
159
  }
@@ -11,6 +11,7 @@ import type { NcpChatPresenter } from '@/components/chat/ncp/ncp-chat.presenter'
11
11
  import type { UseHydratedNcpAgentResult } from '@nextclaw/ncp-react';
12
12
  import type { ChatModelOption } from '@/components/chat/chat-input.types';
13
13
  import type { ChatChildSessionTab } from '@/components/chat/stores/chat-thread.store';
14
+ import type { ChatSessionTypeOption } from '@/components/chat/useChatSessionTypeState';
14
15
  import { resolveSessionTypeLabel } from '@/components/chat/useChatSessionTypeState';
15
16
 
16
17
  function buildChildSessionTabs(params: {
@@ -40,7 +41,7 @@ export function useNcpChatDerivedState(params: {
40
41
  parentSessionId: string | null;
41
42
  sessionSummaries: NcpSessionSummaryView[];
42
43
  selectedSessionType: string;
43
- sessionTypeOptions: Array<{ value: string; label: string }>;
44
+ sessionTypeOptions: ChatSessionTypeOption[];
44
45
  }) {
45
46
  const {
46
47
  availableAgents,
@@ -68,9 +69,11 @@ export function useNcpChatDerivedState(params: {
68
69
  ) ?? null;
69
70
  return parentSummary ? adaptNcpSessionSummary(parentSummary) : null;
70
71
  }, [parentSessionId, sessionSummaries]);
72
+ const currentSessionTypeOption =
73
+ sessionTypeOptions.find((option) => option.value === selectedSessionType) ?? null;
71
74
  const currentSessionTypeLabel =
72
- sessionTypeOptions.find((option) => option.value === selectedSessionType)
73
- ?.label ?? resolveSessionTypeLabel(selectedSessionType);
75
+ currentSessionTypeOption?.label ?? resolveSessionTypeLabel(selectedSessionType);
76
+ const currentSessionTypeIcon = currentSessionTypeOption?.icon ?? null;
74
77
  const currentChildSessionTabs = useMemo(
75
78
  () =>
76
79
  buildChildSessionTabs({
@@ -86,6 +89,7 @@ export function useNcpChatDerivedState(params: {
86
89
  currentAgent,
87
90
  parentSession,
88
91
  currentSessionTypeLabel,
92
+ currentSessionTypeIcon,
89
93
  currentChildSessionTabs,
90
94
  };
91
95
  }
@@ -99,7 +103,7 @@ export function useNcpChatSnapshotSync(params: {
99
103
  lastSendError: string | null;
100
104
  isSending: boolean;
101
105
  modelOptions: ChatModelOption[];
102
- sessionTypeOptions: Array<{ value: string; label: string }>;
106
+ sessionTypeOptions: ChatSessionTypeOption[];
103
107
  selectedSessionType: string;
104
108
  canEditSessionType: boolean;
105
109
  sessionTypeUnavailable: boolean;
@@ -107,6 +111,7 @@ export function useNcpChatSnapshotSync(params: {
107
111
  isSkillsLoading: boolean;
108
112
  sessionTypeUnavailableMessage: string | null;
109
113
  currentSessionTypeLabel: string;
114
+ currentSessionTypeIcon: ChatSessionTypeOption['icon'];
110
115
  sessionKey: string;
111
116
  currentAgentId: string;
112
117
  currentAgent: AgentProfileView | null;
@@ -145,6 +150,7 @@ export function useNcpChatSnapshotSync(params: {
145
150
  sessionTypeUnavailable: params.sessionTypeUnavailable,
146
151
  sessionTypeUnavailableMessage: params.sessionTypeUnavailableMessage,
147
152
  sessionTypeLabel: params.currentSessionTypeLabel,
153
+ sessionTypeIcon: params.currentSessionTypeIcon,
148
154
  sessionKey: params.sessionKey,
149
155
  agentId: params.currentAgentId,
150
156
  agentDisplayName: params.currentAgent?.displayName ?? null,
@@ -1,16 +1,20 @@
1
- import { renderHook } from "@testing-library/react";
1
+ import { act, renderHook, waitFor } from "@testing-library/react";
2
2
  import { beforeEach, describe, expect, it, vi } from "vitest";
3
3
  import { fetchNcpSessionConversationSeed, useNcpSessionConversation } from "./use-ncp-session-conversation";
4
4
 
5
5
  const mocks = vi.hoisted(() => ({
6
6
  fetchNcpSessionMessages: vi.fn(),
7
- hydratedCalls: [] as Array<{ client: unknown }>,
7
+ hydratedCalls: [] as Array<{ client: unknown; loadSeed: unknown }>,
8
+ runtimeAvailability: {
9
+ phase: "cold-starting" as "cold-starting" | "ready",
10
+ lastReadyAt: null as number | null,
11
+ },
8
12
  useHydratedNcpAgent: vi.fn(() => ({
9
13
  snapshot: {
10
14
  messages: [],
11
15
  streamingMessage: null,
12
16
  activeRun: null,
13
- error: null,
17
+ error: null as Error | null,
14
18
  },
15
19
  visibleMessages: [],
16
20
  activeRunId: null,
@@ -30,7 +34,7 @@ vi.mock("@/api/ncp-session", () => ({
30
34
  }));
31
35
 
32
36
  vi.mock("@nextclaw/ncp-react", () => ({
33
- useHydratedNcpAgent: vi.fn((params: { client: unknown }) => {
37
+ useHydratedNcpAgent: vi.fn((params: { client: unknown; loadSeed: unknown }) => {
34
38
  mocks.hydratedCalls.push(params);
35
39
  return mocks.useHydratedNcpAgent();
36
40
  }),
@@ -42,12 +46,18 @@ vi.mock("@nextclaw/ncp-http-agent-client", () => ({
42
46
  }),
43
47
  }));
44
48
 
49
+ vi.mock("@/features/system-status", () => ({
50
+ useChatRuntimeAvailability: vi.fn(() => mocks.runtimeAvailability),
51
+ }));
52
+
45
53
  describe("useNcpSessionConversation", () => {
46
54
  beforeEach(() => {
47
55
  mocks.fetchNcpSessionMessages.mockReset();
48
56
  mocks.useHydratedNcpAgent.mockClear();
49
57
  mocks.hydratedCalls.length = 0;
50
58
  mocks.clientInstances.length = 0;
59
+ mocks.runtimeAvailability.phase = "cold-starting";
60
+ mocks.runtimeAvailability.lastReadyAt = null;
51
61
  });
52
62
 
53
63
  it("hydrates seed from the shared session messages endpoint payload", async () => {
@@ -98,4 +108,38 @@ describe("useNcpSessionConversation", () => {
98
108
  expect(mocks.hydratedCalls[0]?.client).toBe(mocks.clientInstances[0]);
99
109
  expect(mocks.hydratedCalls[1]?.client).toBe(mocks.clientInstances[1]);
100
110
  });
111
+
112
+ it("retries hydration once the runtime becomes ready after a startup placeholder error", async () => {
113
+ mocks.useHydratedNcpAgent.mockImplementation(() => ({
114
+ snapshot: {
115
+ messages: [],
116
+ streamingMessage: null,
117
+ activeRun: null,
118
+ error: new Error("ncp agent unavailable during startup") as Error | null,
119
+ },
120
+ visibleMessages: [],
121
+ activeRunId: null,
122
+ isRunning: false,
123
+ isSending: false,
124
+ send: vi.fn(),
125
+ abort: vi.fn(),
126
+ streamRun: vi.fn(),
127
+ isHydrating: false,
128
+ hydrateError: null,
129
+ }));
130
+
131
+ const { rerender } = renderHook(() => useNcpSessionConversation("session-a"));
132
+ const initialLoadSeed = mocks.hydratedCalls[0]?.loadSeed;
133
+
134
+ act(() => {
135
+ mocks.runtimeAvailability.phase = "ready";
136
+ mocks.runtimeAvailability.lastReadyAt = 123;
137
+ });
138
+ rerender();
139
+
140
+ await waitFor(() => {
141
+ expect(mocks.hydratedCalls.length).toBeGreaterThan(2);
142
+ });
143
+ expect(mocks.hydratedCalls[mocks.hydratedCalls.length - 1]?.loadSeed).not.toBe(initialLoadSeed);
144
+ });
101
145
  });
@@ -1,11 +1,13 @@
1
- import { useCallback, useState } from "react";
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
2
  import { NcpHttpAgentClientEndpoint } from "@nextclaw/ncp-http-agent-client";
3
3
  import { useHydratedNcpAgent, type NcpConversationSeed } from "@nextclaw/ncp-react";
4
4
  import { API_BASE } from "@/api/api-base";
5
5
  import { fetchNcpSessionMessages } from "@/api/ncp-session";
6
6
  import { createNcpAppClientFetch } from "@/components/chat/ncp/ncp-app-client-fetch";
7
+ import { useChatRuntimeAvailability } from "@/features/system-status";
7
8
 
8
9
  const DEFAULT_MESSAGE_LIMIT = 300;
10
+ const NCP_AGENT_UNAVAILABLE_DURING_STARTUP = "ncp agent unavailable during startup";
9
11
 
10
12
  type UseNcpSessionConversationOptions = {
11
13
  messageLimit?: number;
@@ -18,6 +20,15 @@ function isMissingNcpSessionError(error: unknown): boolean {
18
20
  return error.message.includes("ncp session not found:");
19
21
  }
20
22
 
23
+ export function isNcpAgentStartupUnavailableErrorMessage(
24
+ message: string | null | undefined,
25
+ ): boolean {
26
+ return (
27
+ message?.trim().toLowerCase().includes(NCP_AGENT_UNAVAILABLE_DURING_STARTUP) ??
28
+ false
29
+ );
30
+ }
31
+
21
32
  export function createNcpSessionConversationClient(): NcpHttpAgentClientEndpoint {
22
33
  return new NcpHttpAgentClientEndpoint({
23
34
  baseUrl: API_BASE,
@@ -57,16 +68,43 @@ export function useNcpSessionConversation(
57
68
  options: UseNcpSessionConversationOptions = {},
58
69
  ) {
59
70
  const [client] = useState(() => createNcpSessionConversationClient());
71
+ const runtimeAvailability = useChatRuntimeAvailability();
72
+ const [hydrationRetryNonce, setHydrationRetryNonce] = useState(0);
73
+ const retriedReadySignatureRef = useRef<string | null>(null);
60
74
  const messageLimit = options.messageLimit ?? DEFAULT_MESSAGE_LIMIT;
61
75
  const loadSeed = useCallback(
62
- (targetSessionId: string, signal: AbortSignal) =>
63
- fetchNcpSessionConversationSeed(targetSessionId, signal, messageLimit),
64
- [messageLimit],
76
+ (targetSessionId: string, signal: AbortSignal) => {
77
+ void hydrationRetryNonce;
78
+ return fetchNcpSessionConversationSeed(targetSessionId, signal, messageLimit);
79
+ },
80
+ [hydrationRetryNonce, messageLimit],
65
81
  );
66
82
 
67
- return useHydratedNcpAgent({
83
+ const agent = useHydratedNcpAgent({
68
84
  sessionId,
69
85
  client,
70
86
  loadSeed,
71
87
  });
88
+
89
+ const currentAgentError =
90
+ agent.hydrateError?.message ?? agent.snapshot.error?.message ?? null;
91
+ const readyRetrySignature =
92
+ runtimeAvailability.phase === "ready" &&
93
+ isNcpAgentStartupUnavailableErrorMessage(currentAgentError)
94
+ ? `${sessionId}:${runtimeAvailability.lastReadyAt ?? 0}`
95
+ : null;
96
+
97
+ useEffect(() => {
98
+ if (!readyRetrySignature) {
99
+ retriedReadySignatureRef.current = null;
100
+ return;
101
+ }
102
+ if (retriedReadySignatureRef.current === readyRetrySignature) {
103
+ return;
104
+ }
105
+ retriedReadySignatureRef.current = readyRetrySignature;
106
+ setHydrationRetryNonce((current) => current + 1);
107
+ }, [readyRetrySignature]);
108
+
109
+ return agent;
72
110
  }
@@ -4,6 +4,7 @@ import { NcpChatInputManager } from '@/components/chat/ncp/ncp-chat-input.manage
4
4
  import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
5
5
  import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
6
6
  import { useChatThreadStore } from '@/components/chat/stores/chat-thread.store';
7
+ import { useSystemStatusStore } from '@/features/system-status';
7
8
 
8
9
  describe('NcpChatInputManager', () => {
9
10
  beforeEach(() => {
@@ -15,8 +16,23 @@ describe('NcpChatInputManager', () => {
15
16
  attachments: [],
16
17
  selectedSkills: [],
17
18
  selectedSessionType: 'native',
18
- selectedModel: '',
19
+ selectedModel: 'gpt-5',
19
20
  selectedThinkingLevel: null,
21
+ isProviderStateResolved: true,
22
+ modelOptions: [
23
+ {
24
+ value: 'gpt-5',
25
+ modelLabel: 'GPT-5',
26
+ providerLabel: 'OpenAI',
27
+ thinkingCapability: null,
28
+ },
29
+ ],
30
+ },
31
+ });
32
+ useSystemStatusStore.setState({
33
+ state: {
34
+ ...useSystemStatusStore.getState().state,
35
+ lifecyclePhase: 'ready',
20
36
  },
21
37
  });
22
38
  useChatSessionListStore.setState({
@@ -96,4 +112,38 @@ describe('NcpChatInputManager', () => {
96
112
  );
97
113
  expect(sessionListManager.promoteRootDraftSessionRoute).toHaveBeenCalledWith('draft-root-session');
98
114
  });
115
+
116
+ it('does not send while the runtime is still blocked during startup', async () => {
117
+ useChatInputStore.setState({
118
+ snapshot: {
119
+ ...useChatInputStore.getState().snapshot,
120
+ isProviderStateResolved: false,
121
+ modelOptions: [],
122
+ },
123
+ });
124
+ useSystemStatusStore.setState({
125
+ state: {
126
+ ...useSystemStatusStore.getState().state,
127
+ lifecyclePhase: 'cold-starting',
128
+ },
129
+ });
130
+ const streamActionsManager = {
131
+ sendMessage: vi.fn().mockResolvedValue(undefined),
132
+ stopCurrentRun: vi.fn().mockResolvedValue(undefined),
133
+ } as unknown as ConstructorParameters<typeof NcpChatInputManager>[1];
134
+ const sessionListManager = {
135
+ ensureDraftSession: vi.fn(() => 'draft-session'),
136
+ promoteRootDraftSessionRoute: vi.fn(),
137
+ } as unknown as ConstructorParameters<typeof NcpChatInputManager>[2];
138
+ const manager = new NcpChatInputManager(
139
+ {} as ConstructorParameters<typeof NcpChatInputManager>[0],
140
+ streamActionsManager,
141
+ sessionListManager,
142
+ );
143
+
144
+ await manager.send();
145
+
146
+ expect(streamActionsManager.sendMessage).not.toHaveBeenCalled();
147
+ expect(sessionListManager.promoteRootDraftSessionRoute).not.toHaveBeenCalled();
148
+ });
99
149
  });
@@ -1,7 +1,7 @@
1
1
  import { create } from 'zustand';
2
2
  import type { ChatComposerNode } from '@nextclaw/agent-chat-ui';
3
3
  import type { NcpDraftAttachment } from '@nextclaw/ncp-react';
4
- import type { SessionSkillEntryView, ThinkingLevel } from '@/api/types';
4
+ import type { SessionSkillEntryView, SessionTypeIconView, ThinkingLevel } from '@/api/types';
5
5
  import type { ChatModelOption } from '@/components/chat/chat-input.types';
6
6
  import { createInitialChatComposerNodes } from '@/components/chat/chat-composer-state';
7
7
 
@@ -24,6 +24,7 @@ export type ChatInputSnapshot = {
24
24
  sessionTypeOptions: Array<{
25
25
  value: string;
26
26
  label: string;
27
+ icon?: SessionTypeIconView | null;
27
28
  ready?: boolean;
28
29
  reason?: string | null;
29
30
  reasonMessage?: string | null;
@@ -3,7 +3,7 @@ import type { MutableRefObject } from 'react';
3
3
  import type { NcpMessage } from '@nextclaw/ncp';
4
4
  import type { ChatFileOperationLineViewModel } from '@nextclaw/agent-chat-ui';
5
5
  import type { ChatModelOption } from '@/components/chat/chat-input.types';
6
- import type { AgentProfileView } from '@/api/types';
6
+ import type { AgentProfileView, SessionTypeIconView } from '@/api/types';
7
7
 
8
8
  export type ChatChildSessionTab = {
9
9
  sessionKey: string;
@@ -35,6 +35,7 @@ export type ChatThreadSnapshot = {
35
35
  sessionTypeUnavailable: boolean;
36
36
  sessionTypeUnavailableMessage?: string | null;
37
37
  sessionTypeLabel?: string | null;
38
+ sessionTypeIcon?: SessionTypeIconView | null;
38
39
  sessionKey: string | null;
39
40
  agentId?: string | null;
40
41
  agentDisplayName?: string | null;
@@ -70,6 +71,7 @@ const initialSnapshot: ChatThreadSnapshot = {
70
71
  sessionTypeUnavailable: false,
71
72
  sessionTypeUnavailableMessage: null,
72
73
  sessionTypeLabel: null,
74
+ sessionTypeIcon: null,
73
75
  sessionKey: null,
74
76
  agentId: null,
75
77
  agentDisplayName: null,
@@ -1,6 +1,11 @@
1
1
  import { useEffect, useMemo, useRef } from 'react';
2
2
  import type { Dispatch, SetStateAction } from 'react';
3
- import type { AgentProfileView, ChatSessionTypeOptionView, SessionEntryView } from '@/api/types';
3
+ import type {
4
+ AgentProfileView,
5
+ ChatSessionTypeOptionView,
6
+ SessionEntryView,
7
+ SessionTypeIconView
8
+ } from '@/api/types';
4
9
  import { t } from '@/lib/i18n';
5
10
 
6
11
  export const DEFAULT_SESSION_TYPE = 'native';
@@ -8,6 +13,7 @@ export const DEFAULT_SESSION_TYPE = 'native';
8
13
  export type ChatSessionTypeOption = {
9
14
  value: string;
10
15
  label: string;
16
+ icon: SessionTypeIconView | null;
11
17
  ready: boolean;
12
18
  reason?: string | null;
13
19
  reasonMessage?: string | null;
@@ -71,6 +77,7 @@ export function buildSessionTypeOptions(
71
77
  deduped.set(value, {
72
78
  value,
73
79
  label: option.label?.trim() || resolveSessionTypeLabel(value),
80
+ icon: option.icon ?? null,
74
81
  ready: option.ready ?? true,
75
82
  reason: option.reason ?? null,
76
83
  reasonMessage: option.reasonMessage ?? null,
@@ -83,6 +90,7 @@ export function buildSessionTypeOptions(
83
90
  deduped.set(DEFAULT_SESSION_TYPE, {
84
91
  value: DEFAULT_SESSION_TYPE,
85
92
  label: resolveSessionTypeLabel(DEFAULT_SESSION_TYPE),
93
+ icon: null,
86
94
  ready: true,
87
95
  reason: null,
88
96
  reasonMessage: null,
@@ -129,6 +137,7 @@ export function useChatSessionTypeState(params: UseChatSessionTypeStateParams):
129
137
  options.push({
130
138
  value: currentSessionType,
131
139
  label: resolveSessionTypeLabel(currentSessionType),
140
+ icon: null,
132
141
  ready: true,
133
142
  reason: null,
134
143
  reasonMessage: null,
@@ -0,0 +1,86 @@
1
+ import { Fragment } from "react";
2
+ import { ChevronRight, FileCode2, FolderTree } from "lucide-react";
3
+ import type { WorkspaceFileBreadcrumbViewModel } from "@/lib/session-project/workspace-file-breadcrumb";
4
+ import { t } from "@/lib/i18n";
5
+ import { cn } from "@/lib/utils";
6
+
7
+ function WorkspaceBreadcrumbMetaChip({
8
+ tone = "neutral",
9
+ value,
10
+ }: {
11
+ tone?: "neutral" | "warning";
12
+ value: string;
13
+ }) {
14
+ return (
15
+ <span
16
+ className={cn(
17
+ "inline-flex h-5 items-center rounded-sm border px-1.5 text-[10px] font-medium leading-none",
18
+ tone === "warning"
19
+ ? "border-amber-200 bg-amber-50 text-amber-700"
20
+ : "border-gray-200 bg-gray-50 text-gray-500",
21
+ )}
22
+ >
23
+ {value}
24
+ </span>
25
+ );
26
+ }
27
+
28
+ export function ChatSessionWorkspaceFileBreadcrumbs({
29
+ breadcrumb,
30
+ }: {
31
+ breadcrumb: WorkspaceFileBreadcrumbViewModel;
32
+ }) {
33
+ return (
34
+ <div
35
+ data-testid="workspace-file-breadcrumbs"
36
+ title={breadcrumb.fullPath}
37
+ className="workspace-horizontal-scrollbar overflow-x-auto overflow-y-hidden border-b border-gray-200/80 bg-gray-50/55"
38
+ >
39
+ <div
40
+ data-testid="workspace-file-breadcrumb-scroll"
41
+ className="flex min-w-max items-center gap-2.5 px-3 py-1.5"
42
+ >
43
+ <div className="flex min-w-0 flex-1 items-center gap-1 pr-1">
44
+ {breadcrumb.segments.map((segment, index) => (
45
+ <Fragment key={segment.key}>
46
+ <span
47
+ className={cn(
48
+ "inline-flex h-5 items-center gap-1 rounded-sm px-1 text-[11px] leading-none",
49
+ segment.kind === "workspace"
50
+ ? "bg-primary/8 text-primary"
51
+ : segment.isCurrent
52
+ ? "bg-gray-200/70 text-gray-900"
53
+ : "text-gray-500",
54
+ )}
55
+ >
56
+ {segment.kind === "workspace" ? (
57
+ <FolderTree className="h-3 w-3 shrink-0" />
58
+ ) : segment.isCurrent ? (
59
+ <FileCode2 className="h-3 w-3 shrink-0" />
60
+ ) : null}
61
+ <span>{segment.label}</span>
62
+ </span>
63
+ {index < breadcrumb.segments.length - 1 ? (
64
+ <ChevronRight className="h-3 w-3 shrink-0 text-gray-300" />
65
+ ) : null}
66
+ </Fragment>
67
+ ))}
68
+ </div>
69
+
70
+ {breadcrumb.locationLabel || breadcrumb.truncated ? (
71
+ <div className="flex shrink-0 flex-wrap items-center justify-end gap-1">
72
+ {breadcrumb.locationLabel ? (
73
+ <WorkspaceBreadcrumbMetaChip value={breadcrumb.locationLabel} />
74
+ ) : null}
75
+ {breadcrumb.truncated ? (
76
+ <WorkspaceBreadcrumbMetaChip
77
+ tone="warning"
78
+ value={t("chatWorkspacePreviewTruncated")}
79
+ />
80
+ ) : null}
81
+ </div>
82
+ ) : null}
83
+ </div>
84
+ </div>
85
+ );
86
+ }
@@ -1,5 +1,6 @@
1
1
  import { useAppMeta } from '@/hooks/useConfig';
2
2
  import type { ReactNode } from 'react';
3
+ import { RuntimeStatusEntry } from '@/components/layout/runtime-status-entry';
3
4
 
4
5
  type BrandHeaderProps = {
5
6
  className?: string;
@@ -10,6 +11,7 @@ export function BrandHeader({ className, suffix }: BrandHeaderProps) {
10
11
  const { data } = useAppMeta();
11
12
  const productName = data?.name ?? 'NextClaw';
12
13
  const productVersion = data?.productVersion?.trim();
14
+ const resolvedSuffix = suffix ?? <RuntimeStatusEntry />;
13
15
 
14
16
  return (
15
17
  <div className={className ?? 'flex items-center gap-2.5'}>
@@ -19,7 +21,7 @@ export function BrandHeader({ className, suffix }: BrandHeaderProps) {
19
21
  <div className="flex items-baseline gap-2 min-w-0">
20
22
  <span className="truncate text-[15px] font-semibold tracking-[-0.01em] text-gray-800">{productName}</span>
21
23
  {productVersion ? <span className="text-[13px] font-medium text-gray-500">v{productVersion}</span> : null}
22
- {suffix ? <span className="inline-flex items-center shrink-0">{suffix}</span> : null}
24
+ {resolvedSuffix ? <span className="inline-flex items-center shrink-0">{resolvedSuffix}</span> : null}
23
25
  </div>
24
26
  </div>
25
27
  );
@@ -1,4 +1,5 @@
1
1
  import { type SessionContextIcon } from '@/lib/session-context.utils';
2
+ import { resolveAppResourceUri } from '@/lib/app-resource-uri';
2
3
  import { LogoBadge } from '@/components/common/LogoBadge';
3
4
  import { getChannelLogo } from '@/lib/logos';
4
5
  import { cn } from '@/lib/utils';
@@ -8,6 +9,18 @@ export function SessionContextIconNode({ icon, className }: { icon: SessionConte
8
9
  if (icon.kind === 'channel-logo') {
9
10
  return <ChannelLogoIcon channel={icon.channel} className={className} />;
10
11
  }
12
+ if (icon.kind === 'runtime-image') {
13
+ const runtimeIconSrc = resolveAppResourceUri(icon.src);
14
+ return (
15
+ <LogoBadge
16
+ name={icon.name?.trim() || icon.alt?.trim() || 'runtime'}
17
+ src={runtimeIconSrc ?? undefined}
18
+ className={cn('h-[1.125rem] w-[1.125rem]', className)}
19
+ imgClassName="h-full w-full object-contain"
20
+ fallback={<Bot className={cn('h-3 w-3 text-gray-500', className)} />}
21
+ />
22
+ );
23
+ }
11
24
  if (icon.icon === 'heartbeat') {
12
25
  return <HeartPulse className={cn('h-3.5 w-3.5', className)} />;
13
26
  }
@@ -22,8 +35,8 @@ function ChannelLogoIcon(
22
35
  <LogoBadge
23
36
  name={channel}
24
37
  src={logoSrc}
25
- className={cn('h-4 w-4 rounded-[4px] border border-gray-200/80 bg-white', className)}
26
- imgClassName="h-3 w-3 object-contain"
38
+ className={cn('h-[1.125rem] w-[1.125rem]', className)}
39
+ imgClassName="h-full w-full object-contain"
27
40
  fallback={<Bot className="h-3 w-3 text-gray-500" />}
28
41
  />
29
42
  );