@nextclaw/ui 0.14.4 → 0.15.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 (222) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/dist/assets/api-b1R9VdeH.js +13 -0
  3. package/dist/assets/appearance-settings-page-D2O9pch9.js +1 -0
  4. package/dist/assets/book-open-B77lQ2Tg.js +1 -0
  5. package/dist/assets/channels-list-page-DcsIQc6A.js +8 -0
  6. package/dist/assets/chat-page-CZ7WxPir.js +1 -0
  7. package/dist/assets/{config-split-page-CRhlZ_pH.js → config-split-page-BWQ6_Raq.js} +1 -1
  8. package/dist/assets/confirm-dialog-C-NXSioX.js +5 -0
  9. package/dist/assets/desktop-update-config-usHpXugP.js +1 -0
  10. package/dist/assets/{dist-CikkhOlq.js → dist-YtoqU-AT.js} +1 -1
  11. package/dist/assets/{dist-PVA_xL1D.js → dist-jdGmbb6w.js} +1 -1
  12. package/dist/assets/doc-browser-B_Tf9n70.js +1 -0
  13. package/dist/assets/doc-browser-BdZPFOVa.js +1 -0
  14. package/dist/assets/{doc-browser-context-MTw4EvKd.js → doc-browser-context-DwokemUn.js} +1 -1
  15. package/dist/assets/es2015-bNpL2LKy.js +41 -0
  16. package/dist/assets/{external-link-DVJIQf_G.js → external-link-BM6fk8q-.js} +1 -1
  17. package/dist/assets/host-capabilities-9bB_998Z.js +1 -0
  18. package/dist/assets/i18n-provider-CntWolRA.js +1 -0
  19. package/dist/assets/index-BIcEw90u.js +104 -0
  20. package/dist/assets/index-BiNSTKTU.css +1 -0
  21. package/dist/assets/loader-circle-MBgNSVIH.js +1 -0
  22. package/dist/assets/mcp-marketplace-page-CLmUstLN.js +9 -0
  23. package/dist/assets/mcp-marketplace-page-rZkmWPMy.js +1 -0
  24. package/dist/assets/model-config-page-Di0ySJD2.js +1 -0
  25. package/dist/assets/plus-vDqW5Kxp.js +1 -0
  26. package/dist/assets/provider-scoped-model-input-Cm_a4GHf.js +1 -0
  27. package/dist/assets/providers-config-page-BAf2W0MV.js +1 -0
  28. package/dist/assets/{react-VNqT5Zp7.js → react-BN8uzPnp.js} +1 -1
  29. package/dist/assets/refresh-cw-BpVcFPmJ.js +1 -0
  30. package/dist/assets/remote-CLQ0IBM8.js +1 -0
  31. package/dist/assets/rotate-cw-CvNBv7_w.js +1 -0
  32. package/dist/assets/runtime-config-page-CflvvoDc.js +1 -0
  33. package/dist/assets/{save-BOJ477H-.js → save-BpFW5fpk.js} +1 -1
  34. package/dist/assets/{search-BEti5beA.js → search-D0uO7enf.js} +1 -1
  35. package/dist/assets/search-config-page-D0klFUXd.js +1 -0
  36. package/dist/assets/{secrets-config-page--IzrIOid.js → secrets-config-page-dVZ631Wp.js} +2 -2
  37. package/dist/assets/security-config-C5gnlkuj.js +1 -0
  38. package/dist/assets/select-Bh-CrA4p.js +1 -0
  39. package/dist/assets/setting-row-BJfCHRcb.js +1 -0
  40. package/dist/assets/settings-2-Blzi1vr6.js +1 -0
  41. package/dist/assets/side-dock-ZdaSrQVB.js +3 -0
  42. package/dist/assets/skeleton-BLlEzebZ.js +1 -0
  43. package/dist/assets/switch-CFLJFNSd.js +1 -0
  44. package/dist/assets/tag-chip-CzGihiQx.js +1 -0
  45. package/dist/assets/tooltip-CdcrBjrC.js +1 -0
  46. package/dist/assets/use-config-CVPgsBmU.js +1 -0
  47. package/dist/assets/use-confirm-dialog-CSeBk9SF.js +1 -0
  48. package/dist/assets/x-BF2KQ--V.js +1 -0
  49. package/dist/index.html +53 -17
  50. package/index.html +25 -2
  51. package/package.json +9 -9
  52. package/src/app/components/layout/__tests__/app-layout.test.tsx +27 -0
  53. package/src/app/components/layout/__tests__/settings-entry-page.test.tsx +9 -1
  54. package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +6 -3
  55. package/src/app/components/layout/app-layout.tsx +10 -3
  56. package/src/app/components/layout/sidebar-items.tsx +8 -8
  57. package/src/app/components/layout/sidebar-rail.styles.ts +4 -2
  58. package/src/app/components/layout/sidebar.tsx +21 -28
  59. package/src/app/configs/app-navigation.config.ts +11 -5
  60. package/src/app/index.tsx +8 -0
  61. package/src/app/managers/__tests__/viewport-layout.manager.test.ts +59 -0
  62. package/src/app/managers/viewport-layout.manager.ts +39 -0
  63. package/src/app/presenters/app.presenter.ts +25 -2
  64. package/src/app/styles/design-system.css +91 -4
  65. package/src/features/agents/components/__tests__/agents-page.test.tsx +135 -2
  66. package/src/features/agents/components/agent-advanced-config-fields.tsx +70 -0
  67. package/src/features/agents/components/agent-details-dialog.tsx +398 -0
  68. package/src/features/agents/components/agent-dialogs.tsx +20 -47
  69. package/src/features/agents/components/agents-page.tsx +76 -10
  70. package/src/features/agents/types/agent-form.types.ts +21 -0
  71. package/src/features/agents/utils/agent-form.utils.ts +39 -0
  72. package/src/features/chat/components/conversation/__tests__/chat-conversation-content.test.tsx +86 -0
  73. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +132 -1
  74. package/src/features/chat/components/conversation/chat-conversation-content.tsx +47 -17
  75. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +8 -0
  76. package/src/features/chat/components/conversation/chat-conversation-header.tsx +7 -3
  77. package/src/features/chat/components/layout/__tests__/chat-sidebar-read-state.test.tsx +7 -0
  78. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +9 -5
  79. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +5 -5
  80. package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +66 -2
  81. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +170 -1
  82. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +23 -1
  83. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +24 -7
  84. package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +96 -0
  85. package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-controller.test.tsx +181 -0
  86. package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +351 -89
  87. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +29 -4
  88. package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +28 -3
  89. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +108 -8
  90. package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +50 -14
  91. package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +77 -6
  92. package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +21 -12
  93. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +167 -1
  94. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +19 -11
  95. package/src/features/chat/features/message/components/chat-message-list.container.tsx +38 -64
  96. package/src/features/chat/features/message/utils/__tests__/chat-message-process-summary.utils.test.ts +59 -0
  97. package/src/features/chat/features/message/utils/__tests__/chat-message-show-content-tool-card.utils.test.ts +56 -1
  98. package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +0 -2
  99. package/src/features/chat/features/message/utils/chat-message-process-summary.utils.ts +80 -0
  100. package/src/features/chat/features/message/utils/chat-message-show-content-tool-card.utils.ts +12 -3
  101. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +60 -0
  102. package/src/features/chat/features/message/utils/chat-message.utils.ts +3 -0
  103. package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime.test.tsx +62 -1
  104. package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-session-list-view.test.tsx +74 -0
  105. package/src/features/chat/features/ncp/hooks/use-ncp-session-list-view.ts +3 -2
  106. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +2 -2
  107. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +1 -1
  108. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +3 -1
  109. package/src/features/chat/features/session/components/session-header/chat-session-metadata-dialog.tsx +1 -1
  110. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +294 -0
  111. package/src/features/chat/features/session/utils/__tests__/chat-session-display.utils.test.ts +15 -1
  112. package/src/features/chat/features/session/utils/__tests__/ncp-session-adapter.utils.test.ts +24 -4
  113. package/src/features/chat/features/session/utils/chat-session-display.utils.ts +3 -0
  114. package/src/features/chat/features/session/utils/ncp-session-adapter.utils.ts +1 -0
  115. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +304 -115
  116. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumb-browser.tsx +121 -0
  117. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +75 -20
  118. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +64 -5
  119. package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +46 -0
  120. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab-persistence.utils.ts +83 -0
  121. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +126 -1
  122. package/src/features/chat/managers/__tests__/chat-ui.manager.test.ts +2 -0
  123. package/src/features/chat/managers/chat-thread.manager.ts +15 -3
  124. package/src/features/chat/managers/chat-ui.manager.ts +5 -1
  125. package/src/features/chat/presenters/chat.presenter.ts +2 -1
  126. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +4 -0
  127. package/src/features/chat/stores/chat-thread.store.ts +6 -64
  128. package/src/features/chat/types/chat-message.types.ts +4 -0
  129. package/src/features/marketplace/components/__tests__/marketplace-page-detail.test.tsx +33 -26
  130. package/src/features/marketplace/components/__tests__/marketplace-page.test.tsx +1 -0
  131. package/src/features/marketplace/components/curated-shelves/__tests__/marketplace-curated-scene-route.test.tsx +1 -0
  132. package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.tsx +115 -0
  133. package/src/features/marketplace/components/marketplace-detail-doc.tsx +441 -0
  134. package/src/features/marketplace/components/mcp/__tests__/mcp-marketplace-page.test.tsx +3 -3
  135. package/src/features/marketplace/components/mcp/mcp-marketplace-doc.ts +0 -60
  136. package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +64 -29
  137. package/src/features/marketplace/hooks/use-marketplace-item-detail.ts +42 -31
  138. package/src/features/marketplace/index.ts +3 -0
  139. package/src/features/marketplace/stores/marketplace-detail-doc.store.ts +38 -0
  140. package/src/features/panel-apps/components/__tests__/panel-apps-list.test.tsx +32 -2
  141. package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +11 -0
  142. package/src/features/pwa/managers/pwa-shell-theme.manager.ts +8 -3
  143. package/src/features/settings/pages/__tests__/appearance-settings-page.test.tsx +25 -0
  144. package/src/features/settings/pages/appearance-settings-page.tsx +40 -0
  145. package/src/features/side-dock/components/__tests__/side-dock.test.tsx +44 -2
  146. package/src/features/side-dock/components/side-dock.tsx +100 -13
  147. package/src/features/side-dock/configs/side-dock-built-in-items.config.ts +10 -0
  148. package/src/features/side-dock/managers/__tests__/side-dock.manager.test.ts +32 -2
  149. package/src/features/side-dock/managers/side-dock.manager.ts +17 -7
  150. package/src/features/side-dock/stores/side-dock.store.ts +9 -0
  151. package/src/features/side-dock/types/side-dock.types.ts +10 -3
  152. package/src/features/side-dock/utils/side-dock-item.utils.ts +15 -2
  153. package/src/features/system-status/components/__tests__/desktop-update-config.test.tsx +97 -5
  154. package/src/features/system-status/components/desktop-update-config.tsx +91 -2
  155. package/src/features/system-status/utils/update-release-notes.utils.ts +94 -0
  156. package/src/index.css +501 -27
  157. package/src/platforms/desktop/components/desktop-window-chrome.tsx +1 -1
  158. package/src/shared/components/doc-browser/__tests__/doc-browser-context.test.tsx +30 -1
  159. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +206 -38
  160. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +34 -11
  161. package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +1 -1
  162. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +23 -5
  163. package/src/shared/components/doc-browser/doc-browser.tsx +128 -75
  164. package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +9 -1
  165. package/src/shared/components/doc-browser/utils/doc-browser-floating-panel.utils.ts +83 -0
  166. package/src/shared/components/ui/__tests__/dialog.test.tsx +27 -0
  167. package/src/shared/components/ui/actions/icon-action-button.tsx +14 -2
  168. package/src/shared/components/ui/dialog.tsx +2 -2
  169. package/src/shared/components/ui/popover.tsx +1 -1
  170. package/src/shared/components/ui/select.tsx +1 -1
  171. package/src/shared/components/ui/switch.tsx +4 -3
  172. package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +54 -0
  173. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +4 -5
  174. package/src/shared/components/ui/tooltip.tsx +1 -1
  175. package/src/shared/lib/api/ncp-session.types.ts +1 -1
  176. package/src/shared/lib/api/types.ts +12 -3
  177. package/src/shared/lib/api/utils/server-path.utils.ts +27 -1
  178. package/src/shared/lib/i18n/locales/en-US/agents.json +41 -0
  179. package/src/shared/lib/i18n/locales/en-US/chat.json +18 -6
  180. package/src/shared/lib/i18n/locales/en-US/core.json +2 -0
  181. package/src/shared/lib/i18n/locales/en-US/desktop-update.json +8 -0
  182. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +11 -0
  183. package/src/shared/lib/i18n/locales/en-US/marketplace.json +5 -0
  184. package/src/shared/lib/i18n/locales/zh-CN/agents.json +41 -0
  185. package/src/shared/lib/i18n/locales/zh-CN/chat.json +18 -6
  186. package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -0
  187. package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +8 -0
  188. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +11 -0
  189. package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +5 -0
  190. package/src/shared/lib/session-project/__tests__/workspace-file-breadcrumb.test.ts +14 -0
  191. package/src/shared/lib/session-project/index.ts +1 -1
  192. package/src/shared/lib/session-project/{workspace-file-breadcrumb.ts → workspace-file-breadcrumb.utils.ts} +62 -8
  193. package/src/shared/lib/theme/index.ts +51 -13
  194. package/dist/assets/api-BP_8IaxB.js +0 -13
  195. package/dist/assets/channels-list-page-DEh5Jjqx.js +0 -8
  196. package/dist/assets/chat-page-BRB0N5WW.js +0 -105
  197. package/dist/assets/confirm-dialog-DsivFJ4r.js +0 -5
  198. package/dist/assets/desktop-update-config-B0w9p2ep.js +0 -1
  199. package/dist/assets/doc-browser-BoNJ7-tE.js +0 -1
  200. package/dist/assets/doc-browser-CdJ_d4ik.js +0 -1
  201. package/dist/assets/ellipsis-y6vDtq4M.js +0 -1
  202. package/dist/assets/index-BiE-K8-q.css +0 -1
  203. package/dist/assets/index-D9BAE_L_.js +0 -105
  204. package/dist/assets/mcp-marketplace-page-B1SZNY5m.js +0 -40
  205. package/dist/assets/mcp-marketplace-page-Cq_Tw42T.js +0 -1
  206. package/dist/assets/model-config-page-DotaTUHb.js +0 -1
  207. package/dist/assets/plus-BbZqquJe.js +0 -1
  208. package/dist/assets/provider-scoped-model-input-DsFRuWJH.js +0 -1
  209. package/dist/assets/providers-config-page-DW4tQSLN.js +0 -1
  210. package/dist/assets/remote-B8TiUfwW.js +0 -1
  211. package/dist/assets/rotate-cw-DjHDTkCv.js +0 -1
  212. package/dist/assets/runtime-config-page-S6ErRHR8.js +0 -1
  213. package/dist/assets/search-config-page-DGj5am03.js +0 -1
  214. package/dist/assets/select-hoj8dK4K.js +0 -41
  215. package/dist/assets/tag-chip-T8-h-die.js +0 -1
  216. package/dist/assets/use-config-CjRV2hwL.js +0 -1
  217. package/dist/assets/x-D9PXs_xP.js +0 -1
  218. package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.ts +0 -40
  219. package/src/features/marketplace/components/detail-doc/marketplace-detail-doc-renderer.ts +0 -201
  220. package/src/features/marketplace/components/marketplace-detail-doc.ts +0 -142
  221. /package/dist/assets/{config-hints-BkYBc7z0.js → config-hints-fGnUjDe9.js} +0 -0
  222. /package/dist/assets/{middleware-CkFT3R4v.js → middleware-BILWbJE4.js} +0 -0
@@ -0,0 +1,86 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import type { NcpMessage } from "@nextclaw/ncp";
4
+ import { beforeEach, expect, it, vi } from "vitest";
5
+ import { ChatConversationContent } from "@/features/chat/components/conversation/chat-conversation-content";
6
+
7
+ const captures = vi.hoisted(() => ({
8
+ isAtBottom: true,
9
+ onScroll: vi.fn(),
10
+ scrollToBottom: vi.fn(),
11
+ }));
12
+
13
+ vi.mock("@nextclaw/agent-chat-ui", () => ({
14
+ useStickyBottomScroll: () => ({
15
+ isAtBottom: captures.isAtBottom,
16
+ onScroll: captures.onScroll,
17
+ scrollToBottom: captures.scrollToBottom,
18
+ }),
19
+ }));
20
+
21
+ vi.mock("@/features/chat/features/message/components/chat-message-list.container", () => ({
22
+ ChatMessageListContainer: () => <div data-testid="chat-message-list" />,
23
+ }));
24
+
25
+ vi.mock("@/shared/lib/i18n", () => ({
26
+ t: (key: string) => key,
27
+ }));
28
+
29
+ const messages = [
30
+ {
31
+ id: "message-1",
32
+ sessionId: "session-1",
33
+ role: "user",
34
+ status: "final",
35
+ timestamp: "2026-07-04T10:00:00.000Z",
36
+ parts: [{ type: "text", text: "hello" }],
37
+ },
38
+ ] as unknown as readonly NcpMessage[];
39
+
40
+ function renderContent(options: { bottomSlot?: React.ReactNode } = {}) {
41
+ return render(
42
+ <ChatConversationContent
43
+ bottomSlot={options.bottomSlot}
44
+ isAwaitingAssistantOutput={false}
45
+ isHistoryLoading={false}
46
+ isSending={false}
47
+ messages={messages}
48
+ sessionKey="session-1"
49
+ showWelcome={false}
50
+ />,
51
+ );
52
+ }
53
+
54
+ beforeEach(() => {
55
+ captures.isAtBottom = true;
56
+ captures.onScroll.mockReset();
57
+ captures.scrollToBottom.mockReset();
58
+ });
59
+
60
+ it("hides the scroll-to-bottom action while the conversation is already at the bottom", () => {
61
+ renderContent();
62
+
63
+ expect(screen.queryByRole("button", { name: "chatScrollToBottom" })).toBeNull();
64
+ });
65
+
66
+ it("scrolls to the latest message when the user clicks the floating action", async () => {
67
+ const user = userEvent.setup();
68
+ captures.isAtBottom = false;
69
+
70
+ renderContent();
71
+ await user.click(screen.getByRole("button", { name: "chatScrollToBottom" }));
72
+
73
+ expect(captures.scrollToBottom).toHaveBeenCalledOnce();
74
+ });
75
+
76
+ it("renders bottom slot after the message list", () => {
77
+ renderContent({
78
+ bottomSlot: <div data-testid="conversation-bottom-slot">Run failed</div>,
79
+ });
80
+
81
+ const messageList = screen.getByTestId("chat-message-list");
82
+ const bottomSlot = screen.getByTestId("conversation-bottom-slot");
83
+ expect(
84
+ messageList.compareDocumentPosition(bottomSlot) & Node.DOCUMENT_POSITION_FOLLOWING,
85
+ ).toBeTruthy();
86
+ });
@@ -7,12 +7,17 @@ import { ChatConversationHeaderSection } from "@/features/chat/components/conver
7
7
  import { useChatQueryStore } from "@/features/chat/stores/ncp-chat-query.store";
8
8
  import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
9
9
  import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
10
- import type { NcpSessionSummaryView } from "@/shared/lib/api";
10
+ import { viewportLayoutManager } from "@/app/managers/viewport-layout.manager";
11
+ import type { NcpSessionListItemView } from "@/features/chat/features/ncp/hooks/use-ncp-session-list-view";
12
+ import type { NcpSessionSummaryView, SessionEntryView } from "@/shared/lib/api";
11
13
 
12
14
  const mocks = vi.hoisted(() => ({
13
15
  deleteSession: vi.fn(),
14
16
  openChildSessionPanel: vi.fn(),
15
17
  openSessionCronPanel: vi.fn(),
18
+ selectSession: vi.fn(),
19
+ sessionItems: [] as NcpSessionListItemView[],
20
+ isSessionListLoading: false,
16
21
  }));
17
22
 
18
23
  vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
@@ -22,9 +27,35 @@ vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
22
27
  openChildSessionPanel: mocks.openChildSessionPanel,
23
28
  openSessionCronPanel: mocks.openSessionCronPanel,
24
29
  },
30
+ chatSessionListManager: {
31
+ selectSession: mocks.selectSession,
32
+ },
25
33
  }),
26
34
  }));
27
35
 
36
+ vi.mock("@/features/chat/features/ncp/hooks/use-ncp-session-list-view", () => ({
37
+ useNcpSessionListView: (params?: { query?: string | null }) => {
38
+ const query = params?.query?.trim().toLowerCase() ?? "";
39
+ const items = query
40
+ ? mocks.sessionItems.filter(({ session }) =>
41
+ [
42
+ session.key,
43
+ session.label ?? "",
44
+ session.projectRoot ?? "",
45
+ session.projectName ?? "",
46
+ ]
47
+ .join(" ")
48
+ .toLowerCase()
49
+ .includes(query),
50
+ )
51
+ : mocks.sessionItems;
52
+ return {
53
+ isLoading: mocks.isSessionListLoading,
54
+ items,
55
+ };
56
+ },
57
+ }));
58
+
28
59
  vi.mock("@/features/cron", () => ({
29
60
  useCronJobs: () => ({ data: { jobs: [] } }),
30
61
  }));
@@ -54,6 +85,24 @@ function createSummary(
54
85
  };
55
86
  }
56
87
 
88
+ function createSessionListItem(
89
+ overrides: Partial<SessionEntryView> & Pick<SessionEntryView, "key">,
90
+ ): NcpSessionListItemView {
91
+ return {
92
+ session: {
93
+ createdAt: "2026-06-18T00:00:00.000Z",
94
+ updatedAt: "2026-06-18T00:00:00.000Z",
95
+ lastMessageAt: "2026-06-18T00:00:00.000Z",
96
+ readAt: "2026-06-18T00:00:00.000Z",
97
+ sessionType: "native",
98
+ sessionTypeMutable: false,
99
+ messageCount: 1,
100
+ ...overrides,
101
+ },
102
+ runStatus: overrides.status === "running" ? "running" : undefined,
103
+ };
104
+ }
105
+
57
106
  function renderHeaderSection() {
58
107
  const queryClient = new QueryClient({
59
108
  defaultOptions: {
@@ -73,6 +122,9 @@ describe("ChatConversationHeaderSection", () => {
73
122
  mocks.deleteSession.mockReset();
74
123
  mocks.openChildSessionPanel.mockReset();
75
124
  mocks.openSessionCronPanel.mockReset();
125
+ mocks.selectSession.mockReset();
126
+ mocks.isSessionListLoading = false;
127
+ viewportLayoutManager.resetForTests();
76
128
  useChatSessionListStore.persist.setOptions({
77
129
  storage: new MemoryPersistStorage(),
78
130
  });
@@ -129,6 +181,18 @@ describe("ChatConversationHeaderSection", () => {
129
181
  } as never,
130
182
  },
131
183
  });
184
+ mocks.sessionItems = [
185
+ createSessionListItem({
186
+ key: "parent-session-1",
187
+ label: "Parent Task",
188
+ }),
189
+ createSessionListItem({
190
+ key: "session:ncp-2",
191
+ label: "Background Task",
192
+ lastMessageAt: "2026-06-18T01:00:00.000Z",
193
+ readAt: "2026-06-18T01:00:00.000Z",
194
+ }),
195
+ ];
132
196
  });
133
197
 
134
198
  it("shows the child-session entry from session query state before a tool card opens it", async () => {
@@ -142,4 +206,71 @@ describe("ChatConversationHeaderSection", () => {
142
206
  activeChildSessionKey: "child-session-1",
143
207
  });
144
208
  });
209
+
210
+ it("uses the collapsed desktop title as a session switcher", async () => {
211
+ const user = userEvent.setup();
212
+ viewportLayoutManager.setSidebarCollapsed(true);
213
+
214
+ renderHeaderSection();
215
+
216
+ await user.click(screen.getByRole("button", { name: /Switch session/ }));
217
+ await user.click(screen.getByRole("button", { name: /Background Task/ }));
218
+
219
+ expect(mocks.selectSession).toHaveBeenCalledWith("session:ncp-2");
220
+ });
221
+
222
+ it("keeps the desktop title plain while the sidebar is expanded", () => {
223
+ renderHeaderSection();
224
+
225
+ expect(screen.queryByRole("button", { name: /Switch session/ })).toBeNull();
226
+ expect(screen.getByText("Parent Task")).toBeTruthy();
227
+ });
228
+
229
+ it("keeps session switching available from a collapsed new-session header", async () => {
230
+ const user = userEvent.setup();
231
+ viewportLayoutManager.setSidebarCollapsed(true);
232
+ useChatSessionListStore.setState({
233
+ snapshot: {
234
+ ...useChatSessionListStore.getState().snapshot,
235
+ selectedSessionKey: null,
236
+ },
237
+ });
238
+ useChatThreadStore.setState({
239
+ snapshot: {
240
+ ...useChatThreadStore.getState().snapshot,
241
+ sessionKey: null,
242
+ },
243
+ });
244
+
245
+ renderHeaderSection();
246
+
247
+ await user.click(screen.getByRole("button", { name: /Switch session/ }));
248
+ await user.click(screen.getByRole("button", { name: /Background Task/ }));
249
+
250
+ expect(mocks.selectSession).toHaveBeenCalledWith("session:ncp-2");
251
+ });
252
+
253
+ it("filters the collapsed title switcher with a local search query", async () => {
254
+ const user = userEvent.setup();
255
+ viewportLayoutManager.setSidebarCollapsed(true);
256
+
257
+ renderHeaderSection();
258
+
259
+ await user.click(screen.getByRole("button", { name: /Switch session/ }));
260
+ const searchInput = screen.getByRole("textbox", {
261
+ name: "Search session key / label",
262
+ });
263
+ await user.type(searchInput, "Background");
264
+ await user.click(screen.getByRole("button", { name: /Background Task/ }));
265
+
266
+ expect(mocks.selectSession).toHaveBeenCalledWith("session:ncp-2");
267
+
268
+ await user.click(screen.getByRole("button", { name: /Switch session/ }));
269
+ await user.type(
270
+ screen.getByRole("textbox", { name: "Search session key / label" }),
271
+ "missing",
272
+ );
273
+
274
+ expect(screen.getByText("No matching sessions")).toBeTruthy();
275
+ });
145
276
  });
@@ -1,9 +1,13 @@
1
1
  import { useRef, type ReactNode } from "react";
2
2
  import { useStickyBottomScroll } from "@nextclaw/agent-chat-ui";
3
3
  import type { NcpMessage } from "@nextclaw/ncp";
4
+ import { ArrowDown } from "lucide-react";
4
5
  import { ChatMessageListContainer } from "@/features/chat/features/message/components/chat-message-list.container";
6
+ import { IconActionButton } from "@/shared/components/ui/actions/icon-action-button";
7
+ import { t } from "@/shared/lib/i18n";
5
8
 
6
9
  type ChatConversationContentProps = {
10
+ bottomSlot?: ReactNode;
7
11
  isAwaitingAssistantOutput: boolean;
8
12
  isHistoryLoading: boolean;
9
13
  isSending: boolean;
@@ -14,6 +18,7 @@ type ChatConversationContentProps = {
14
18
  };
15
19
 
16
20
  export function ChatConversationContent({
21
+ bottomSlot,
17
22
  isAwaitingAssistantOutput,
18
23
  isHistoryLoading,
19
24
  isSending,
@@ -28,7 +33,11 @@ export function ChatConversationContent({
28
33
  messages.length === 0 &&
29
34
  !isSending &&
30
35
  !isAwaitingAssistantOutput;
31
- const { onScroll } = useStickyBottomScroll({
36
+ const {
37
+ isAtBottom,
38
+ onScroll,
39
+ scrollToBottom,
40
+ } = useStickyBottomScroll({
32
41
  scrollRef: threadRef,
33
42
  resetKey: sessionKey,
34
43
  isLoading: isHistoryLoading,
@@ -36,24 +45,45 @@ export function ChatConversationContent({
36
45
  contentVersion: messages[messages.length - 1] ?? null,
37
46
  });
38
47
  const hasMessages = messages.length > 0;
48
+ const showScrollToBottom = hasMessages && !showWelcome && !isAtBottom;
39
49
 
40
50
  return (
41
- <div
42
- ref={threadRef}
43
- onScroll={onScroll}
44
- data-chat-scroll-container="true"
45
- className="flex-1 min-h-0 overflow-y-auto custom-scrollbar"
46
- >
47
- {showWelcome ? (
48
- welcomeSlot ?? null
49
- ) : hideEmptyHint || !hasMessages ? null : (
50
- <div className="mx-auto w-full max-w-[min(1120px,100%)] px-4 py-4 sm:px-6 sm:py-5">
51
- <ChatMessageListContainer
52
- messages={messages}
53
- isSending={hasMessages && isSending && isAwaitingAssistantOutput}
54
- />
55
- </div>
56
- )}
51
+ <div className="relative min-h-0 flex-1">
52
+ <div
53
+ ref={threadRef}
54
+ onScroll={onScroll}
55
+ data-chat-scroll-container="true"
56
+ className="h-full overflow-y-auto custom-scrollbar"
57
+ >
58
+ {showWelcome ? (
59
+ welcomeSlot ?? null
60
+ ) : (
61
+ <>
62
+ {hideEmptyHint || !hasMessages ? null : (
63
+ <div className="mx-auto w-full max-w-[min(1120px,100%)] px-4 py-4 sm:px-6 sm:py-5">
64
+ <ChatMessageListContainer
65
+ messages={messages}
66
+ isSending={hasMessages && isSending && isAwaitingAssistantOutput}
67
+ />
68
+ </div>
69
+ )}
70
+ {bottomSlot ? (
71
+ <div className="mx-auto w-full max-w-[min(1120px,100%)] px-4 pb-4 sm:px-6 sm:pb-5">
72
+ {bottomSlot}
73
+ </div>
74
+ ) : null}
75
+ </>
76
+ )}
77
+ </div>
78
+ {showScrollToBottom ? (
79
+ <IconActionButton
80
+ icon={<ArrowDown className="h-4 w-4" />}
81
+ label={t("chatScrollToBottom")}
82
+ onClick={scrollToBottom}
83
+ tooltipSide="top"
84
+ className="absolute bottom-4 left-1/2 z-10 h-9 w-9 -translate-x-1/2 rounded-full border border-border bg-background/90 text-foreground shadow-lg backdrop-blur hover:bg-accent hover:text-accent-foreground"
85
+ />
86
+ ) : null}
57
87
  </div>
58
88
  );
59
89
  }
@@ -2,6 +2,7 @@ import { useMemo } from "react";
2
2
  import { ChatConversationHeader } from "@/features/chat/components/conversation/chat-conversation-header";
3
3
  import { ChatSessionHeaderActions } from "@/features/chat/features/session/components/session-header/chat-session-header-actions";
4
4
  import { ChatSessionProjectBadge } from "@/features/chat/features/session/components/session-header/chat-session-project-badge";
5
+ import { ChatSessionTitleSwitcher } from "@/features/chat/features/session/components/session-header/chat-session-title-switcher";
5
6
  import { usePresenter } from "@/features/chat/components/providers/chat-presenter.provider";
6
7
  import { SessionContextIconNode } from "@/features/chat/features/session/components/session-context-icon";
7
8
  import { useChatConversationWorkspaceState } from "@/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state";
@@ -89,6 +90,13 @@ export function ChatConversationHeaderSection({
89
90
  <ChatConversationHeader
90
91
  layoutMode={layoutMode}
91
92
  title={sessionHeaderTitle}
93
+ titleContent={
94
+ <ChatSessionTitleSwitcher
95
+ layoutMode={layoutMode}
96
+ selectedSessionKey={sessionKey}
97
+ title={sessionHeaderTitle}
98
+ />
99
+ }
92
100
  shouldShow={shouldShowSessionHeader}
93
101
  onBackToList={onBackToList}
94
102
  leading={
@@ -41,6 +41,7 @@ export function ChatConversationHeader({
41
41
  sessionTypeBadge,
42
42
  shouldShow,
43
43
  title,
44
+ titleContent,
44
45
  }: {
45
46
  layoutMode: "desktop" | "mobile";
46
47
  actions?: ReactNode;
@@ -50,6 +51,7 @@ export function ChatConversationHeader({
50
51
  sessionTypeBadge?: ReactNode;
51
52
  shouldShow: boolean;
52
53
  title: string;
54
+ titleContent?: ReactNode;
53
55
  }) {
54
56
  const isMobileLayout = layoutMode === "mobile";
55
57
 
@@ -78,9 +80,11 @@ export function ChatConversationHeader({
78
80
  />
79
81
  ) : null}
80
82
  {leading}
81
- <span className="text-sm font-medium text-foreground truncate">
82
- {title}
83
- </span>
83
+ {titleContent ?? (
84
+ <span className="text-sm font-medium text-foreground truncate">
85
+ {title}
86
+ </span>
87
+ )}
84
88
  {sessionTypeBadge}
85
89
  {projectBadge}
86
90
  </div>
@@ -188,6 +188,13 @@ describe("ChatSidebar read state sync", () => {
188
188
  it("collapses the desktop sidebar into accessible icon actions", () => {
189
189
  const { container } = renderReadStateSidebar(createTestQueryClient());
190
190
 
191
+ expect(
192
+ screen.getByText("Sessions").parentElement?.nextElementSibling?.className,
193
+ ).toContain("[mask-image:linear-gradient");
194
+ expect(
195
+ screen.getByRole("button", { name: "Settings menu" }).parentElement
196
+ ?.className,
197
+ ).not.toContain("border-t");
191
198
  fireEvent.click(screen.getByRole("button", { name: "Collapse sidebar" }));
192
199
 
193
200
  const persistedState = JSON.parse(
@@ -29,6 +29,7 @@ import {
29
29
  SIDEBAR_RAIL_PADDING_X_CLASS,
30
30
  SIDEBAR_RAIL_STACK_CLASS,
31
31
  SIDEBAR_RAIL_SURFACE_CLASS,
32
+ SIDEBAR_SCROLL_EDGE_FADE_CLASS,
32
33
  } from "@/app/components/layout/sidebar-rail.styles";
33
34
 
34
35
  const navItems = [
@@ -67,9 +68,7 @@ export function ChatSidebarDesktopHeader({
67
68
  isCollapsed: boolean;
68
69
  }) {
69
70
  const isWindowsHost = isWindowsDesktopHost();
70
- const shouldReserveMacWindowControls =
71
- typeof window !== "undefined" &&
72
- window.nextclawDesktop?.platform === "darwin";
71
+ const shouldReserveMacWindowControls = typeof window !== "undefined" && window.nextclawDesktop?.platform === "darwin";
73
72
 
74
73
  return (
75
74
  <div
@@ -170,7 +169,12 @@ export function ChatSidebarSessionArea({
170
169
  />
171
170
  </div>
172
171
 
173
- <div className="flex-1 min-h-0 overflow-y-auto custom-scrollbar px-3 py-2">
172
+ <div
173
+ className={cn(
174
+ "custom-scrollbar min-h-0 flex-1 overflow-y-auto px-3 pb-7 pt-2",
175
+ SIDEBAR_SCROLL_EDGE_FADE_CLASS,
176
+ )}
177
+ >
174
178
  <ChatSidebarSessionList
175
179
  isLoading={isLoading}
176
180
  isProjectFirstView={isProjectFirstView}
@@ -221,7 +225,7 @@ export function ChatSidebarDesktopFooter({
221
225
  return (
222
226
  <div
223
227
  className={cn(
224
- "border-t border-border/70 py-3",
228
+ "py-3",
225
229
  isCollapsed
226
230
  ? cn("flex justify-center", SIDEBAR_RAIL_PADDING_X_CLASS)
227
231
  : "px-3",
@@ -115,7 +115,7 @@ export function ChatSidebarUtilityMenu({
115
115
  <button
116
116
  type="button"
117
117
  aria-label={t("settingsMenu")}
118
- className="flex w-full items-center gap-2.5 rounded-xl px-3 py-2 text-[13px] font-medium text-muted-foreground transition-all duration-base hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35"
118
+ className="flex w-full items-center gap-2.5 rounded-xl px-3 py-2 text-[13px] font-medium text-muted-foreground transition-colors duration-base hover:bg-gray-200/60 hover:text-gray-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35"
119
119
  >
120
120
  <Settings className="h-4 w-4 shrink-0 text-muted-foreground/70" />
121
121
  <span className="min-w-0 flex-1 text-left">{t("settings")}</span>
@@ -134,7 +134,7 @@ export function ChatSidebarUtilityMenu({
134
134
  <NavLink
135
135
  to="/settings"
136
136
  onClick={() => onOpenChange(false)}
137
- className="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
137
+ className="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-foreground transition-colors hover:bg-gray-200/60 hover:text-gray-900"
138
138
  >
139
139
  <Settings className="h-4 w-4 text-muted-foreground/70" />
140
140
  <span className="flex-1 text-left">{t("settings")}</span>
@@ -142,7 +142,7 @@ export function ChatSidebarUtilityMenu({
142
142
  <button
143
143
  type="button"
144
144
  onClick={handleOpenDocs}
145
- className="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
145
+ className="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-foreground transition-colors hover:bg-gray-200/60 hover:text-gray-900"
146
146
  >
147
147
  <BookOpen className="h-4 w-4 text-muted-foreground/70" />
148
148
  <span className="flex-1 text-left">{t("docBrowserHelp")}</span>
@@ -150,7 +150,7 @@ export function ChatSidebarUtilityMenu({
150
150
  <button
151
151
  type="button"
152
152
  onClick={handleOpenApps}
153
- className="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
153
+ className="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-foreground transition-colors hover:bg-gray-200/60 hover:text-gray-900"
154
154
  >
155
155
  <Boxes className="h-4 w-4 text-muted-foreground/70" />
156
156
  <span className="flex-1 text-left">{t("appsTitle")}</span>
@@ -212,7 +212,7 @@ function ChatSidebarUtilitySelect<Value extends string>({
212
212
  >
213
213
  <SelectTrigger
214
214
  aria-label={label}
215
- className="h-auto w-full rounded-lg border-0 bg-transparent px-3 py-2 text-[13px] font-medium text-foreground shadow-none hover:bg-accent hover:text-accent-foreground focus:ring-0"
215
+ className="h-auto w-full rounded-lg border-0 bg-transparent px-3 py-2 text-[13px] font-medium text-foreground shadow-none hover:bg-gray-200/60 hover:text-gray-900 focus:ring-0"
216
216
  indicator={<ChevronRight className="h-4 w-4 text-muted-foreground/70" />}
217
217
  >
218
218
  <div className="flex min-w-0 items-center gap-2.5">
@@ -7,6 +7,7 @@ import { SessionConversationArea } from '@/features/chat/features/conversation/c
7
7
 
8
8
  const mocks = vi.hoisted(() => {
9
9
  const inputRenderSpy = vi.fn();
10
+ const controllerParamsSpy = vi.fn();
10
11
  const inputActions = {
11
12
  update: vi.fn(),
12
13
  syncComposer: vi.fn(),
@@ -47,7 +48,14 @@ const mocks = vi.hoisted(() => {
47
48
  isProviderStateResolved: true,
48
49
  isSkillsLoading: false,
49
50
  modelOptions: [],
50
- selectedSession: null,
51
+ selectedSession: null as null | {
52
+ activityPreview?: {
53
+ state: 'running' | 'completed' | 'failed' | 'cancelled' | 'idle';
54
+ statusText?: string;
55
+ replyText?: string;
56
+ };
57
+ status?: string;
58
+ },
51
59
  selectedSessionKey: null,
52
60
  sessionTypeState: {
53
61
  sessionTypeOptions: [],
@@ -62,8 +70,11 @@ const mocks = vi.hoisted(() => {
62
70
  };
63
71
  const controller = {
64
72
  canStopGeneration: true,
73
+ deleteQueuedInput: vi.fn(),
74
+ editQueuedInput: vi.fn(),
65
75
  hasSendableDraft: true,
66
76
  isSending: true,
77
+ queuedInputs: [],
67
78
  send: vi.fn(),
68
79
  sendDisabled: true,
69
80
  stop: vi.fn(),
@@ -87,6 +98,7 @@ const mocks = vi.hoisted(() => {
87
98
  return {
88
99
  agent,
89
100
  controller,
101
+ controllerParamsSpy,
90
102
  inputActions,
91
103
  inputQuery,
92
104
  inputRenderSpy,
@@ -120,16 +132,19 @@ vi.mock('@/features/chat/components/providers/chat-presenter.provider', () => ({
120
132
 
121
133
  vi.mock('@/features/chat/components/conversation/chat-conversation-content', () => ({
122
134
  ChatConversationContent: ({
135
+ bottomSlot,
123
136
  messages,
124
137
  showWelcome,
125
138
  welcomeSlot,
126
139
  }: {
140
+ bottomSlot?: ReactNode;
127
141
  messages: readonly unknown[];
128
142
  showWelcome: boolean;
129
143
  welcomeSlot?: ReactNode;
130
144
  }) => (
131
145
  <div data-testid="conversation-content" data-show-welcome={String(showWelcome)}>
132
146
  {showWelcome ? welcomeSlot : <div data-testid="message-count">{messages.length}</div>}
147
+ {bottomSlot ? <div data-testid="conversation-bottom-slot">{bottomSlot}</div> : null}
133
148
  </div>
134
149
  ),
135
150
  }));
@@ -162,7 +177,10 @@ vi.mock('@/features/chat/features/conversation/hooks/use-session-conversation-in
162
177
  }));
163
178
 
164
179
  vi.mock('@/features/chat/features/conversation/hooks/use-session-conversation-controller', () => ({
165
- useSessionConversationController: () => mocks.controller,
180
+ useSessionConversationController: (params: unknown) => {
181
+ mocks.controllerParamsSpy(params);
182
+ return mocks.controller;
183
+ },
166
184
  }));
167
185
 
168
186
  vi.mock('@/features/chat/stores/chat-session-list.store', () => ({
@@ -197,6 +215,21 @@ describe('SessionConversationArea input boundary', () => {
197
215
  mocks.agent.isRunning = true;
198
216
  mocks.agent.isSending = true;
199
217
  mocks.agent.snapshot.contextWindow = null;
218
+ mocks.inputQuery.selectedSession = null;
219
+ });
220
+
221
+ it('passes running state without treating an active run as a send request in flight', () => {
222
+ mocks.agent.isRunning = true;
223
+ mocks.agent.isSending = false;
224
+
225
+ renderArea('session-1');
226
+
227
+ expect(mocks.controllerParamsSpy).toHaveBeenCalled();
228
+ const params = mocks.controllerParamsSpy.mock.calls[0]?.[0] as {
229
+ agent: { isRunning: boolean; isSending: boolean };
230
+ };
231
+ expect(params.agent.isRunning).toBe(true);
232
+ expect(params.agent.isSending).toBe(false);
200
233
  });
201
234
 
202
235
  it('keeps the composer input subtree stable when only streamed messages change', () => {
@@ -222,4 +255,35 @@ describe('SessionConversationArea input boundary', () => {
222
255
  expect(screen.getByTestId('welcome')).toBeTruthy();
223
256
  expect(screen.getByTestId('conversation-input')).toBeTruthy();
224
257
  });
258
+
259
+ it('surfaces selected-session failure previews at the conversation bottom', () => {
260
+ mocks.inputQuery.selectedSession = {
261
+ activityPreview: {
262
+ state: 'failed',
263
+ statusText: 'Run failed: Invalid API Key',
264
+ },
265
+ status: 'idle',
266
+ };
267
+
268
+ renderArea('session-1');
269
+
270
+ expect(screen.getByTestId('conversation-bottom-slot')).toBeTruthy();
271
+ expect(screen.getByText(/出错了|Something went wrong/)).toBeTruthy();
272
+ expect(screen.getByText('Run failed: Invalid API Key')).toBeTruthy();
273
+ });
274
+
275
+ it('does not surface user-cancelled previews as conversation errors', () => {
276
+ mocks.inputQuery.selectedSession = {
277
+ activityPreview: {
278
+ state: 'cancelled',
279
+ statusText: 'Run interrupted: User stopped the current run.',
280
+ },
281
+ status: 'idle',
282
+ };
283
+
284
+ renderArea('session-1');
285
+
286
+ expect(screen.queryByTestId('conversation-bottom-slot')).toBeNull();
287
+ expect(screen.queryByText(/出错了|Something went wrong/)).toBeNull();
288
+ });
225
289
  });