@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
@@ -269,6 +269,23 @@ describe("ChatConversationPanel", () => {
269
269
  expect(screen.queryByText("Engineer")).toBeNull();
270
270
  });
271
271
 
272
+ it("renders a fuller loading skeleton before provider state settles", () => {
273
+ useChatThreadStore.setState({
274
+ snapshot: {
275
+ ...useChatThreadStore.getState().snapshot,
276
+ isProviderStateResolved: false,
277
+ },
278
+ });
279
+
280
+ render(<ChatConversationPanel />);
281
+
282
+ expect(screen.getByTestId("chat-conversation-skeleton")).toBeTruthy();
283
+ expect(
284
+ screen.getAllByTestId("chat-conversation-skeleton-bubble"),
285
+ ).toHaveLength(4);
286
+ expect(screen.queryByTestId("chat-input-bar")).toBeNull();
287
+ });
288
+
272
289
  it("keeps the message area clean while a session history is hydrating", () => {
273
290
  useChatThreadStore.setState({
274
291
  snapshot: {
@@ -288,6 +305,16 @@ describe("ChatConversationPanel", () => {
288
305
  expect(screen.queryByText("No messages yet. Send one to start.")).toBeNull();
289
306
  });
290
307
 
308
+ it("does not render runtime lifecycle copy in the conversation alert strip", () => {
309
+ render(<ChatConversationPanel />);
310
+
311
+ expect(
312
+ screen.queryByText(
313
+ "聊天能力正在初始化。你可以先输入内容,完成后即可发送。",
314
+ ),
315
+ ).toBeNull();
316
+ });
317
+
291
318
  it("does not auto-open the child-session panel until the panel is explicitly opened", () => {
292
319
  useChatThreadStore.setState({
293
320
  snapshot: {
@@ -495,5 +522,8 @@ describe("ChatSessionWorkspacePanel", () => {
495
522
  expect(screen.getByTestId("workspace-file-preview").textContent).toBe(
496
523
  "README.md",
497
524
  );
525
+ expect(screen.getByTestId("workspace-tabs-bar").className).toContain(
526
+ "workspace-horizontal-scrollbar",
527
+ );
498
528
  });
499
529
  });
@@ -9,35 +9,93 @@ import {
9
9
  import { ChatWelcome } from "@/components/chat/ChatWelcome";
10
10
  import { ChatSessionWorkspacePanel } from "@/components/chat/chat-session-workspace-panel";
11
11
  import { AgentAvatar } from "@/components/common/AgentAvatar";
12
+ import { SessionContextIconNode } from "@/components/common/session-context-icon";
12
13
  import { usePresenter } from "@/components/chat/presenter/chat-presenter-context";
13
14
  import { ChatSessionHeaderActions } from "@/components/chat/session-header/chat-session-header-actions";
14
15
  import { ChatSessionProjectBadge } from "@/components/chat/session-header/chat-session-project-badge";
15
16
  import { useChatInputStore } from "@/components/chat/stores/chat-input.store";
16
17
  import { useChatThreadStore } from "@/components/chat/stores/chat-thread.store";
18
+ import { Skeleton } from "@/components/ui/skeleton";
17
19
  import { resolveAgentRuntimeSessionType } from "@/components/chat/useChatSessionTypeState";
18
20
  import { t } from "@/lib/i18n";
19
21
  import { cn } from "@/lib/utils";
20
22
 
23
+ const CHAT_CONVERSATION_SKELETON_BUBBLES = [
24
+ {
25
+ key: "hero",
26
+ alignmentClassName: "justify-start",
27
+ bubbleClassName: "max-w-[78%] h-32 rounded-[30px]",
28
+ },
29
+ {
30
+ key: "follow-up",
31
+ alignmentClassName: "justify-start",
32
+ bubbleClassName: "max-w-[62%] h-24 rounded-[28px]",
33
+ },
34
+ {
35
+ key: "reply",
36
+ alignmentClassName: "justify-end",
37
+ bubbleClassName: "max-w-[70%] h-24 rounded-[28px]",
38
+ },
39
+ {
40
+ key: "detail",
41
+ alignmentClassName: "justify-start",
42
+ bubbleClassName: "max-w-[88%] h-36 rounded-[30px]",
43
+ },
44
+ ] as const;
45
+
21
46
  function ChatConversationSkeleton() {
22
47
  return (
23
- <section className="flex-1 min-h-0 flex flex-col overflow-hidden bg-gradient-to-b from-gray-50/60 to-white">
48
+ <section
49
+ data-testid="chat-conversation-skeleton"
50
+ className="flex-1 min-h-0 flex flex-col overflow-hidden bg-gradient-to-b from-gray-50/60 to-white"
51
+ >
24
52
  <div className="flex-1 min-h-0 overflow-y-auto custom-scrollbar">
25
- <div className="mx-auto w-full max-w-[min(1120px,100%)] px-6 py-5">
26
- <div className="space-y-4">
27
- <div className="h-6 w-48 animate-pulse rounded bg-gray-200" />
28
- <div className="h-24 w-[78%] animate-pulse rounded-2xl bg-gray-200/80" />
29
- <div className="h-20 w-[62%] animate-pulse rounded-2xl bg-gray-200/80" />
30
- <div className="h-28 w-[84%] animate-pulse rounded-2xl bg-gray-200/80" />
53
+ <div className="mx-auto flex min-h-full w-full max-w-[min(1120px,100%)] flex-col px-6 py-5">
54
+ <div className="flex flex-1 flex-col gap-8">
55
+ <div className="space-y-6">
56
+ <Skeleton className="h-6 w-52 rounded-lg bg-gray-200/90" />
57
+ <div className="space-y-5">
58
+ {CHAT_CONVERSATION_SKELETON_BUBBLES.map((bubble) => (
59
+ <div
60
+ key={bubble.key}
61
+ className={cn("flex w-full", bubble.alignmentClassName)}
62
+ >
63
+ <Skeleton
64
+ data-testid="chat-conversation-skeleton-bubble"
65
+ className={cn(
66
+ "w-full bg-gray-200/80",
67
+ bubble.bubbleClassName,
68
+ )}
69
+ />
70
+ </div>
71
+ ))}
72
+ </div>
73
+ </div>
74
+ <div className="mt-auto grid gap-4 pb-2 sm:grid-cols-[minmax(0,1fr)_minmax(180px,240px)] sm:items-end">
75
+ <div className="space-y-3">
76
+ <Skeleton className="h-4 w-40 rounded-full bg-gray-200/70" />
77
+ <Skeleton className="h-[112px] w-full rounded-[30px] bg-gray-200/70" />
78
+ </div>
79
+ <div className="hidden justify-end sm:flex">
80
+ <Skeleton className="h-10 w-36 rounded-full bg-gray-200/75" />
81
+ </div>
82
+ </div>
31
83
  </div>
32
84
  </div>
33
85
  </div>
34
86
  <div className="border-t border-gray-200/80 bg-white p-4">
35
87
  <div className="mx-auto w-full max-w-[min(1120px,100%)]">
36
- <div className="rounded-2xl border border-gray-200 bg-white shadow-card p-4">
37
- <div className="h-16 w-full animate-pulse rounded-xl bg-gray-200/80" />
38
- <div className="mt-3 flex items-center justify-between">
39
- <div className="h-8 w-36 animate-pulse rounded-lg bg-gray-200/80" />
40
- <div className="h-8 w-20 animate-pulse rounded-lg bg-gray-200/80" />
88
+ <div className="overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-card">
89
+ <div className="px-4 py-2.5">
90
+ <Skeleton className="h-[84px] w-full rounded-[28px] bg-gray-200/80" />
91
+ </div>
92
+ <div className="flex items-center justify-between gap-3 px-3 pb-3">
93
+ <div className="flex items-center gap-2">
94
+ <Skeleton className="h-8 w-20 rounded-full bg-gray-200/75" />
95
+ <Skeleton className="h-8 w-28 rounded-full bg-gray-200/75" />
96
+ <Skeleton className="hidden h-8 w-24 rounded-full bg-gray-200/70 sm:block" />
97
+ </div>
98
+ <Skeleton className="h-8 w-8 rounded-full bg-gray-200/85" />
41
99
  </div>
42
100
  </div>
43
101
  </div>
@@ -130,7 +188,19 @@ function ChatConversationHeader({
130
188
  {sessionHeaderTitle}
131
189
  </span>
132
190
  {snapshot.sessionTypeLabel ? (
133
- <span className="shrink-0 rounded-full border border-gray-200 bg-gray-100 px-2 py-0.5 text-[11px] font-medium text-gray-600">
191
+ <span className="inline-flex shrink-0 items-center gap-1.5 rounded-full border border-gray-200 bg-gray-100 px-2 py-0.5 text-[11px] font-medium text-gray-600">
192
+ {snapshot.sessionTypeIcon?.src ? (
193
+ <span className="inline-flex h-[1.125rem] w-[1.125rem] items-center justify-center">
194
+ <SessionContextIconNode
195
+ icon={{
196
+ kind: "runtime-image",
197
+ src: snapshot.sessionTypeIcon.src,
198
+ alt: snapshot.sessionTypeIcon.alt ?? null,
199
+ name: snapshot.sessionTypeLabel
200
+ }}
201
+ />
202
+ </span>
203
+ ) : null}
134
204
  {snapshot.sessionTypeLabel}
135
205
  </span>
136
206
  ) : null}
@@ -0,0 +1,92 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ hasNcpChatModelOptions,
4
+ isNcpChatComposerDisabled,
5
+ isNcpChatModelOptionsEmpty,
6
+ isNcpChatModelOptionsLoading,
7
+ isNcpChatSendDisabled,
8
+ } from '@/components/chat/chat-input/ncp-chat-input-availability.utils';
9
+ import type { ChatInputSnapshot } from '@/components/chat/stores/chat-input.store';
10
+
11
+ function createSnapshot(
12
+ overrides: Partial<ChatInputSnapshot> = {}
13
+ ): ChatInputSnapshot {
14
+ return {
15
+ isProviderStateResolved: false,
16
+ composerNodes: [],
17
+ attachments: [],
18
+ draft: '',
19
+ pendingSessionType: 'native',
20
+ pendingProjectRoot: null,
21
+ pendingProjectRootSessionKey: null,
22
+ defaultSessionType: 'native',
23
+ canStopGeneration: false,
24
+ stopDisabledReason: null,
25
+ sendError: null,
26
+ isSending: false,
27
+ modelOptions: [],
28
+ selectedModel: '',
29
+ selectedThinkingLevel: null,
30
+ sessionTypeOptions: [],
31
+ selectedSessionType: 'native',
32
+ stopSupported: false,
33
+ stopReason: undefined,
34
+ canEditSessionType: true,
35
+ sessionTypeUnavailable: false,
36
+ skillRecords: [],
37
+ isSkillsLoading: false,
38
+ selectedSkills: [],
39
+ ...overrides,
40
+ };
41
+ }
42
+
43
+ describe('ncp-chat-input-availability.utils', () => {
44
+ it('keeps the composer editable during cold start while send remains blocked', () => {
45
+ const snapshot = createSnapshot({
46
+ isProviderStateResolved: false,
47
+ modelOptions: [],
48
+ sessionTypeUnavailable: false,
49
+ });
50
+
51
+ expect(hasNcpChatModelOptions(snapshot)).toBe(false);
52
+ expect(isNcpChatModelOptionsLoading(snapshot)).toBe(true);
53
+ expect(isNcpChatComposerDisabled(snapshot)).toBe(false);
54
+ expect(
55
+ isNcpChatSendDisabled({
56
+ snapshot,
57
+ hasSendableDraft: true,
58
+ isRuntimeBlocked: true,
59
+ })
60
+ ).toBe(true);
61
+ });
62
+
63
+ it('marks model options as empty only after provider state resolves', () => {
64
+ const loadingSnapshot = createSnapshot({
65
+ isProviderStateResolved: false,
66
+ modelOptions: [],
67
+ });
68
+ const emptySnapshot = createSnapshot({
69
+ isProviderStateResolved: true,
70
+ modelOptions: [],
71
+ });
72
+
73
+ expect(isNcpChatModelOptionsEmpty(loadingSnapshot)).toBe(false);
74
+ expect(isNcpChatModelOptionsEmpty(emptySnapshot)).toBe(true);
75
+ });
76
+
77
+ it('disables both editing and sending when the session type is unavailable', () => {
78
+ const snapshot = createSnapshot({
79
+ isProviderStateResolved: true,
80
+ sessionTypeUnavailable: true,
81
+ });
82
+
83
+ expect(isNcpChatComposerDisabled(snapshot)).toBe(true);
84
+ expect(
85
+ isNcpChatSendDisabled({
86
+ snapshot,
87
+ hasSendableDraft: true,
88
+ isRuntimeBlocked: false,
89
+ })
90
+ ).toBe(true);
91
+ });
92
+ });
@@ -0,0 +1,45 @@
1
+ import type { ChatInputSnapshot } from '@/components/chat/stores/chat-input.store';
2
+
3
+ type NcpChatInputAvailabilitySnapshot = Pick<
4
+ ChatInputSnapshot,
5
+ 'isProviderStateResolved' | 'modelOptions' | 'sessionTypeUnavailable'
6
+ >;
7
+
8
+ export function hasNcpChatModelOptions(
9
+ snapshot: NcpChatInputAvailabilitySnapshot
10
+ ): boolean {
11
+ return snapshot.modelOptions.length > 0;
12
+ }
13
+
14
+ export function isNcpChatModelOptionsLoading(
15
+ snapshot: NcpChatInputAvailabilitySnapshot
16
+ ): boolean {
17
+ return !snapshot.isProviderStateResolved && !hasNcpChatModelOptions(snapshot);
18
+ }
19
+
20
+ export function isNcpChatModelOptionsEmpty(
21
+ snapshot: NcpChatInputAvailabilitySnapshot
22
+ ): boolean {
23
+ return snapshot.isProviderStateResolved && !hasNcpChatModelOptions(snapshot);
24
+ }
25
+
26
+ export function isNcpChatComposerDisabled(
27
+ snapshot: NcpChatInputAvailabilitySnapshot
28
+ ): boolean {
29
+ return snapshot.sessionTypeUnavailable;
30
+ }
31
+
32
+ export function isNcpChatSendDisabled(params: {
33
+ hasSendableDraft: boolean;
34
+ snapshot: NcpChatInputAvailabilitySnapshot;
35
+ isRuntimeBlocked: boolean;
36
+ }): boolean {
37
+ const { hasSendableDraft, isRuntimeBlocked, snapshot } = params;
38
+
39
+ return (
40
+ isRuntimeBlocked ||
41
+ !hasSendableDraft ||
42
+ !hasNcpChatModelOptions(snapshot) ||
43
+ snapshot.sessionTypeUnavailable
44
+ );
45
+ }
@@ -1,12 +1,12 @@
1
- import { useEffect } from 'react';
2
- import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
3
- import { ChatSidebar } from '@/components/chat/ChatSidebar';
4
- import { ChatConversationPanel } from '@/components/chat/chat-conversation-panel';
5
- import { AgentsPage } from '@/components/agents/AgentsPage';
6
- import { CronConfig } from '@/components/config/CronConfig';
7
- import { MarketplacePage } from '@/components/marketplace/MarketplacePage';
1
+ import { useEffect } from "react";
2
+ import type { Dispatch, MutableRefObject, SetStateAction } from "react";
3
+ import { ChatSidebar } from "@/components/chat/containers/chat-sidebar";
4
+ import { ChatConversationPanel } from "@/components/chat/chat-conversation-panel";
5
+ import { AgentsPage } from "@/components/agents/agents-page";
6
+ import { CronConfig } from "@/components/config/CronConfig";
7
+ import { MarketplacePage } from "@/components/marketplace/marketplace-page";
8
8
 
9
- export type MainPanelView = 'chat' | 'cron' | 'skills' | 'agents';
9
+ export type MainPanelView = "chat" | "cron" | "skills" | "agents";
10
10
 
11
11
  export type ChatPageProps = {
12
12
  view: MainPanelView;
@@ -32,7 +32,7 @@ export function useChatSessionSync(params: UseChatSessionSyncParams): void {
32
32
  } = params;
33
33
 
34
34
  useEffect(() => {
35
- if (view !== 'chat') {
35
+ if (view !== "chat") {
36
36
  return;
37
37
  }
38
38
  if (routeSessionKey) {
@@ -45,7 +45,13 @@ export function useChatSessionSync(params: UseChatSessionSyncParams): void {
45
45
  setSelectedSessionKey(null);
46
46
  resetStreamState();
47
47
  }
48
- }, [resetStreamState, routeSessionKey, selectedSessionKey, setSelectedSessionKey, view]);
48
+ }, [
49
+ resetStreamState,
50
+ routeSessionKey,
51
+ selectedSessionKey,
52
+ setSelectedSessionKey,
53
+ view,
54
+ ]);
49
55
 
50
56
  useEffect(() => {
51
57
  selectedSessionKeyRef.current = selectedSessionKey;
@@ -62,17 +68,17 @@ export function ChatPageLayout({ view, confirmDialog }: ChatPageLayoutProps) {
62
68
  <div className="h-full flex">
63
69
  <ChatSidebar />
64
70
 
65
- {view === 'chat' ? (
71
+ {view === "chat" ? (
66
72
  <ChatConversationPanel />
67
73
  ) : (
68
74
  <section className="flex-1 min-h-0 overflow-hidden bg-gradient-to-b from-gray-50/60 to-white">
69
- {view === 'cron' ? (
75
+ {view === "cron" ? (
70
76
  <div className="h-full overflow-auto custom-scrollbar">
71
77
  <div className="mx-auto w-full max-w-[min(1120px,100%)] px-6 py-5">
72
78
  <CronConfig />
73
79
  </div>
74
80
  </div>
75
- ) : view === 'agents' ? (
81
+ ) : view === "agents" ? (
76
82
  <div className="h-full overflow-auto custom-scrollbar">
77
83
  <div className="mx-auto w-full max-w-[min(1180px,100%)] px-6 py-5">
78
84
  <AgentsPage />
@@ -0,0 +1,46 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { ChatSessionTypeOptionItem } from "@/components/chat/chat-session-type-option-item";
4
+
5
+ describe("ChatSessionTypeOptionItem", () => {
6
+ it("renders a runtime icon image when the session type option provides an app resource URI", () => {
7
+ render(
8
+ <ChatSessionTypeOptionItem
9
+ option={{
10
+ value: "codex",
11
+ label: "Codex",
12
+ icon: {
13
+ kind: "image",
14
+ src: "app://runtime-icons/codex-openai.svg",
15
+ alt: "Codex",
16
+ },
17
+ ready: true,
18
+ }}
19
+ onSelect={vi.fn()}
20
+ />,
21
+ );
22
+
23
+ const runtimeIcon = screen.getByRole("img", { name: "Codex logo" });
24
+ expect(runtimeIcon.getAttribute("src")).toBe("/runtime-icons/codex-openai.svg");
25
+ });
26
+
27
+ it("keeps ready options visually compact without repeating helper copy", () => {
28
+ render(
29
+ <ChatSessionTypeOptionItem
30
+ option={{
31
+ value: "claude",
32
+ label: "Claude",
33
+ icon: {
34
+ kind: "image",
35
+ src: "app://runtime-icons/claude.ico",
36
+ alt: "Claude",
37
+ },
38
+ ready: true,
39
+ }}
40
+ onSelect={vi.fn()}
41
+ />,
42
+ );
43
+
44
+ expect(screen.getAllByText("Ready")).toHaveLength(1);
45
+ });
46
+ });
@@ -0,0 +1,68 @@
1
+ import type { ChatInputSnapshot } from "@/components/chat/stores/chat-input.store";
2
+ import { SessionContextIconNode } from "@/components/common/session-context-icon";
3
+ import { t } from "@/lib/i18n";
4
+ import { cn } from "@/lib/utils";
5
+
6
+ type SessionTypeOption = ChatInputSnapshot["sessionTypeOptions"][number];
7
+
8
+ export function ChatSessionTypeOptionItem(props: {
9
+ option: SessionTypeOption;
10
+ onSelect: () => void;
11
+ }) {
12
+ const { option, onSelect } = props;
13
+ const helperText =
14
+ option.ready === false
15
+ ? option.reasonMessage?.trim() || t("statusSetup")
16
+ : null;
17
+
18
+ return (
19
+ <button
20
+ type="button"
21
+ onClick={onSelect}
22
+ className="w-full rounded-2xl px-3 py-2.5 text-left transition-colors hover:bg-gray-50"
23
+ >
24
+ <div className="flex items-start gap-3">
25
+ <div className="flex min-w-0 flex-1 items-start gap-2.5">
26
+ {option.icon?.src ? (
27
+ <span className="inline-flex h-5 w-5 shrink-0 items-center justify-center pt-0.5">
28
+ <SessionContextIconNode
29
+ icon={{
30
+ kind: "runtime-image",
31
+ src: option.icon.src,
32
+ alt: option.icon.alt ?? null,
33
+ name: option.label,
34
+ }}
35
+ />
36
+ </span>
37
+ ) : null}
38
+ <div className="min-w-0 flex-1">
39
+ <div className="flex items-center justify-between gap-3">
40
+ <div className="truncate text-[13px] font-semibold text-gray-900">
41
+ {option.label}
42
+ </div>
43
+ <span
44
+ className={cn(
45
+ "inline-flex shrink-0 items-center gap-1.5 text-[11px] font-medium",
46
+ option.ready === false ? "text-amber-700" : "text-emerald-600",
47
+ )}
48
+ >
49
+ <span
50
+ className={cn(
51
+ "h-1.5 w-1.5 rounded-full",
52
+ option.ready === false ? "bg-amber-500" : "bg-emerald-500",
53
+ )}
54
+ />
55
+ {option.ready === false ? t("statusSetup") : t("statusReady")}
56
+ </span>
57
+ </div>
58
+ {helperText ? (
59
+ <div className="mt-1 pr-4 text-[11px] leading-4 text-gray-500">
60
+ {helperText}
61
+ </div>
62
+ ) : null}
63
+ </div>
64
+ </div>
65
+ </div>
66
+ </button>
67
+ );
68
+ }
@@ -2,6 +2,7 @@ import { render, screen } from "@testing-library/react";
2
2
  import { beforeEach, describe, expect, it, vi } from "vitest";
3
3
  import { ChatSessionWorkspaceFilePreview } from "@/components/chat/chat-session-workspace-file-preview";
4
4
  import type { ChatWorkspaceFileTab } from "@/components/chat/stores/chat-thread.store";
5
+ import { t } from "@/lib/i18n";
5
6
 
6
7
  const serverPathReadMock = vi.fn();
7
8
 
@@ -88,4 +89,90 @@ describe("ChatSessionWorkspaceFilePreview", () => {
88
89
  "workspace",
89
90
  );
90
91
  });
92
+
93
+ it("does not repeat the preview badge inside the workspace header", () => {
94
+ serverPathReadMock.mockReturnValue({
95
+ isLoading: false,
96
+ error: null,
97
+ data: {
98
+ kind: "text",
99
+ resolvedPath: "/tmp/example.ts",
100
+ text: "const answer = 42;\n",
101
+ truncated: false,
102
+ },
103
+ });
104
+
105
+ render(
106
+ <ChatSessionWorkspaceFilePreview
107
+ file={buildWorkspaceFile({ viewMode: "preview" })}
108
+ sessionProjectRoot="/tmp"
109
+ onFileOpen={vi.fn()}
110
+ />,
111
+ );
112
+
113
+ expect(screen.queryByText(t("chatWorkspacePreview"))).toBeNull();
114
+ expect(screen.getByTitle("/tmp/example.ts")).toBeTruthy();
115
+ expect(screen.getByText("tmp")).toBeTruthy();
116
+ expect(screen.getByText("example.ts")).toBeTruthy();
117
+ });
118
+
119
+ it("renders project-relative breadcrumbs when the file is inside the workspace", () => {
120
+ serverPathReadMock.mockReturnValue({
121
+ isLoading: false,
122
+ error: null,
123
+ data: {
124
+ kind: "text",
125
+ resolvedPath: "/tmp/workspace/src/example.ts",
126
+ text: "const answer = 42;\n",
127
+ truncated: false,
128
+ },
129
+ });
130
+
131
+ render(
132
+ <ChatSessionWorkspaceFilePreview
133
+ file={buildWorkspaceFile({ viewMode: "preview" })}
134
+ sessionProjectRoot="/tmp/workspace"
135
+ onFileOpen={vi.fn()}
136
+ />,
137
+ );
138
+
139
+ expect(screen.getByText("workspace")).toBeTruthy();
140
+ expect(screen.getByText("src")).toBeTruthy();
141
+ expect(screen.getByText("example.ts")).toBeTruthy();
142
+ expect(
143
+ screen.getByTestId("workspace-file-breadcrumb-scroll").className,
144
+ ).toContain("py-1.5");
145
+ expect(screen.getByTestId("workspace-file-breadcrumbs").className).toContain(
146
+ "workspace-horizontal-scrollbar",
147
+ );
148
+ });
149
+
150
+ it("keeps line and truncation metadata without the duplicated type badge", () => {
151
+ serverPathReadMock.mockReturnValue({
152
+ isLoading: false,
153
+ error: null,
154
+ data: {
155
+ kind: "text",
156
+ resolvedPath: "/tmp/example.ts",
157
+ text: "const answer = 42;\n",
158
+ truncated: true,
159
+ },
160
+ });
161
+
162
+ render(
163
+ <ChatSessionWorkspaceFilePreview
164
+ file={buildWorkspaceFile({
165
+ viewMode: "preview",
166
+ line: 12,
167
+ column: 4,
168
+ })}
169
+ sessionProjectRoot="/tmp"
170
+ onFileOpen={vi.fn()}
171
+ />,
172
+ );
173
+
174
+ expect(screen.getByText("L12:4")).toBeTruthy();
175
+ expect(screen.getByText(t("chatWorkspacePreviewTruncated"))).toBeTruthy();
176
+ expect(screen.queryByText(t("chatWorkspacePreview"))).toBeNull();
177
+ });
91
178
  });