@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,74 @@
1
+ import { renderHook } from "@testing-library/react";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { useNcpSessionListView } from "@/features/chat/features/ncp/hooks/use-ncp-session-list-view";
4
+ import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
5
+ import type { NcpSessionSummaryView } from "@/shared/lib/api";
6
+
7
+ const mocks = vi.hoisted(() => ({
8
+ sessions: [] as NcpSessionSummaryView[],
9
+ }));
10
+
11
+ vi.mock("@/features/chat/features/ncp/hooks/use-ncp-session-queries", () => ({
12
+ useNcpSessions: () => ({
13
+ data: {
14
+ sessions: mocks.sessions,
15
+ total: mocks.sessions.length,
16
+ },
17
+ isLoading: false,
18
+ }),
19
+ }));
20
+
21
+ function createSummary(
22
+ sessionId: string,
23
+ label: string,
24
+ ): NcpSessionSummaryView {
25
+ return {
26
+ sessionId,
27
+ status: "idle",
28
+ updatedAt: "2026-06-18T00:00:00.000Z",
29
+ lastMessageAt: "2026-06-18T00:00:00.000Z",
30
+ messageCount: 1,
31
+ metadata: {
32
+ label,
33
+ session_type: "native",
34
+ },
35
+ };
36
+ }
37
+
38
+ describe("useNcpSessionListView", () => {
39
+ beforeEach(() => {
40
+ mocks.sessions = [
41
+ createSummary("session:alpha", "Alpha Task"),
42
+ createSummary("session:beta", "Beta Task"),
43
+ ];
44
+ useChatSessionListStore.setState({
45
+ snapshot: {
46
+ ...useChatSessionListStore.getState().snapshot,
47
+ query: "",
48
+ },
49
+ });
50
+ });
51
+
52
+ it("uses the sidebar query by default", () => {
53
+ useChatSessionListStore.getState().setSnapshot({ query: "Alpha" });
54
+
55
+ const { result } = renderHook(() => useNcpSessionListView());
56
+
57
+ expect(result.current.items.map((item) => item.session.key)).toEqual([
58
+ "session:alpha",
59
+ ]);
60
+ });
61
+
62
+ it("can override the hidden sidebar query for header switching", () => {
63
+ useChatSessionListStore.getState().setSnapshot({ query: "Alpha" });
64
+
65
+ const { result } = renderHook(() =>
66
+ useNcpSessionListView({ query: "" }),
67
+ );
68
+
69
+ expect(result.current.items.map((item) => item.session.key)).toEqual([
70
+ "session:alpha",
71
+ "session:beta",
72
+ ]);
73
+ });
74
+ });
@@ -22,8 +22,9 @@ function shouldShowSessionInSidebar(session: SessionEntryView): boolean {
22
22
  return session.isPromotedChildSession === true;
23
23
  }
24
24
 
25
- export function useNcpSessionListView(params: { limit?: number } = {}) {
26
- const query = useChatSessionListStore((state) => state.snapshot.query);
25
+ export function useNcpSessionListView(params: { limit?: number; query?: string | null } = {}) {
26
+ const storedQuery = useChatSessionListStore((state) => state.snapshot.query);
27
+ const query = params.query ?? storedQuery;
27
28
  const sessionsQuery = useNcpSessions({ limit: params.limit ?? 200 });
28
29
 
29
30
  const items = useMemo<NcpSessionListItemView[]>(() => {
@@ -70,7 +70,7 @@ export function ChatSidebarProjectGroups(props: ChatSidebarProjectGroupsProps) {
70
70
  icon={<Plus className="h-3.5 w-3.5" />}
71
71
  label={actionLabel}
72
72
  tooltip={false}
73
- className="h-7 w-7 shrink-0 rounded-lg text-muted-foreground hover:bg-accent hover:text-accent-foreground"
73
+ className="h-7 w-7 shrink-0 rounded-lg text-muted-foreground hover:bg-gray-200/60 hover:text-gray-900"
74
74
  />
75
75
  </PopoverTrigger>
76
76
  <PopoverContent
@@ -91,7 +91,7 @@ export function ChatSidebarProjectGroups(props: ChatSidebarProjectGroupsProps) {
91
91
  <IconActionButton
92
92
  icon={<Plus className="h-3.5 w-3.5" />}
93
93
  label={actionLabel}
94
- className="h-7 w-7 shrink-0 rounded-lg text-muted-foreground hover:bg-accent hover:text-accent-foreground"
94
+ className="h-7 w-7 shrink-0 rounded-lg text-muted-foreground hover:bg-gray-200/60 hover:text-gray-900"
95
95
  onClick={() => onCreateSession(preferredSessionType, group.projectRoot)}
96
96
  />
97
97
  )}
@@ -230,7 +230,7 @@ export function ChatSidebarSessionItem({
230
230
  className={cn(
231
231
  'w-full rounded-xl px-3 py-2.5 text-left transition-all text-[13px]',
232
232
  active
233
- ? 'bg-gray-200 text-gray-900 font-semibold shadow-sm'
233
+ ? 'bg-gray-200/80 text-gray-900 font-semibold shadow-sm'
234
234
  : 'text-gray-700 hover:bg-gray-200/60 hover:text-gray-900'
235
235
  )}
236
236
  >
@@ -92,7 +92,9 @@ describe('ChatSessionHeaderActions', () => {
92
92
  await user.click(screen.getByRole('button', { name: 'View Metadata' }));
93
93
 
94
94
  expect(screen.getByRole('dialog', { name: 'Session Metadata' })).toBeTruthy();
95
- expect(screen.getByText(/codex_thread_id/)).toBeTruthy();
95
+ const metadataBlock = screen.getByText(/codex_thread_id/);
96
+ expect(metadataBlock.className).toContain('bg-muted/60');
97
+ expect(metadataBlock.className).toContain('text-foreground');
96
98
  expect(screen.getByText(/thread-123/)).toBeTruthy();
97
99
  });
98
100
 
@@ -36,7 +36,7 @@ export function ChatSessionMetadataDialog({
36
36
  </DialogDescription>
37
37
  </DialogHeader>
38
38
  <div className="px-5 pb-5 pt-4">
39
- <pre className="max-h-[60vh] overflow-auto rounded-lg border border-gray-200 bg-gray-950 p-4 text-xs leading-relaxed text-gray-100">
39
+ <pre className="max-h-[60vh] overflow-auto rounded-lg border border-border bg-muted/60 p-4 font-mono text-xs leading-6 text-foreground shadow-inner custom-scrollbar">
40
40
  {formatSessionMetadata(metadata)}
41
41
  </pre>
42
42
  </div>
@@ -0,0 +1,294 @@
1
+ import { useMemo, useState } from "react";
2
+ import {
3
+ Check,
4
+ ChevronDown,
5
+ Loader2,
6
+ MessageSquareText,
7
+ Search,
8
+ } from "lucide-react";
9
+ import { usePresenter } from "@/features/chat/components/providers/chat-presenter.provider";
10
+ import {
11
+ useNcpSessionListView,
12
+ type NcpSessionListItemView,
13
+ } from "@/features/chat/features/ncp/hooks/use-ncp-session-list-view";
14
+ import { SessionContextIconNode } from "@/features/chat/features/session/components/session-context-icon";
15
+ import { SessionRunBadge } from "@/features/chat/features/session/components/session-run-badge";
16
+ import {
17
+ formatSessionListTime,
18
+ sessionActivityPreviewText,
19
+ sessionDisplayName,
20
+ } from "@/features/chat/features/session/utils/chat-session-display.utils";
21
+ import { sortSessionItemsByActivityAtDesc } from "@/features/chat/features/session/utils/chat-sidebar-session-groups.utils";
22
+ import { resolveSessionContextView } from "@/features/chat/features/session/utils/session-context.utils";
23
+ import {
24
+ buildSessionTypeOptions,
25
+ type ChatSessionTypeOption,
26
+ } from "@/features/chat/features/session-type/utils/chat-session-type.utils";
27
+ import {
28
+ shouldShowUnreadSessionIndicator,
29
+ useChatSessionListStore,
30
+ } from "@/features/chat/stores/chat-session-list.store";
31
+ import { useChatQueryStore } from "@/features/chat/stores/ncp-chat-query.store";
32
+ import { useViewportLayoutStore } from "@/app/stores/viewport-layout.store";
33
+ import {
34
+ Popover,
35
+ PopoverContent,
36
+ PopoverTrigger,
37
+ } from "@/shared/components/ui/popover";
38
+ import { Input } from "@/shared/components/ui/input";
39
+ import { t } from "@/shared/lib/i18n";
40
+ import { cn } from "@/shared/lib/utils";
41
+
42
+ const SWITCHER_TRIGGER_CLASS =
43
+ "group inline-flex min-w-0 max-w-full items-center gap-1.5 rounded-md px-1.5 py-1 text-sm font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40";
44
+
45
+ function ChatSessionTitle({ title }: { title: string }) {
46
+ return (
47
+ <span className="text-sm font-medium text-foreground truncate">
48
+ {title}
49
+ </span>
50
+ );
51
+ }
52
+
53
+ function ChatSessionTitleSwitcherEmptyState({ label }: { label: string }) {
54
+ return (
55
+ <div className="flex items-center gap-2 px-3 py-4 text-xs text-muted-foreground">
56
+ <MessageSquareText className="h-4 w-4 shrink-0 text-muted-foreground/55" />
57
+ <span>{label}</span>
58
+ </div>
59
+ );
60
+ }
61
+
62
+ function ChatSessionTitleSwitcherLoadingState() {
63
+ return (
64
+ <div className="flex items-center gap-2 px-3 py-4 text-xs text-muted-foreground">
65
+ <Loader2 className="h-4 w-4 shrink-0 animate-spin" />
66
+ <span>{t("sessionsLoading")}</span>
67
+ </div>
68
+ );
69
+ }
70
+
71
+ function ChatSessionSwitchItem({
72
+ item,
73
+ selectedSessionKey,
74
+ optimisticReadAtBySessionKey,
75
+ sessionTypeOptions,
76
+ onSelect,
77
+ }: {
78
+ item: NcpSessionListItemView;
79
+ selectedSessionKey: string | null;
80
+ optimisticReadAtBySessionKey: Record<string, string>;
81
+ sessionTypeOptions: ChatSessionTypeOption[];
82
+ onSelect: (sessionKey: string) => void;
83
+ }) {
84
+ const { session, runStatus } = item;
85
+ const active = selectedSessionKey === session.key;
86
+ const optimisticReadAt = optimisticReadAtBySessionKey[session.key];
87
+ const effectiveReadAt =
88
+ optimisticReadAt && session.readAt
89
+ ? optimisticReadAt.localeCompare(session.readAt) > 0
90
+ ? optimisticReadAt
91
+ : session.readAt
92
+ : (optimisticReadAt ?? session.readAt);
93
+ const shouldShowUnread = shouldShowUnreadSessionIndicator({
94
+ active,
95
+ lastMessageAt: session.lastMessageAt,
96
+ readAt: effectiveReadAt,
97
+ runStatus,
98
+ });
99
+ const previewText =
100
+ sessionActivityPreviewText(session) ??
101
+ session.projectName ??
102
+ session.projectRoot ??
103
+ `${session.messageCount}`;
104
+ const trailingText = formatSessionListTime(
105
+ session.lastMessageAt ?? session.createdAt,
106
+ );
107
+ const sessionContext = resolveSessionContextView(session, sessionTypeOptions);
108
+
109
+ return (
110
+ <button
111
+ type="button"
112
+ aria-current={active ? "page" : undefined}
113
+ className={cn(
114
+ "flex w-full min-w-0 items-start gap-2 rounded-lg px-2.5 py-2 text-left transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40",
115
+ active
116
+ ? "bg-accent text-accent-foreground"
117
+ : "text-popover-foreground hover:bg-accent/70",
118
+ )}
119
+ onClick={() => onSelect(session.key)}
120
+ >
121
+ <span className="mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center">
122
+ {active ? (
123
+ <Check className="h-3.5 w-3.5" />
124
+ ) : shouldShowUnread ? (
125
+ <span
126
+ aria-label={t("chatSessionUnread")}
127
+ className="h-2 w-2 rounded-full bg-primary"
128
+ />
129
+ ) : null}
130
+ </span>
131
+ <span className="min-w-0 flex-1">
132
+ <span className="flex min-w-0 items-center gap-1.5">
133
+ <span className="min-w-0 truncate text-[13px] font-medium">
134
+ {sessionDisplayName(session)}
135
+ </span>
136
+ {sessionContext.label ? (
137
+ <span className="shrink-0 rounded-full border border-border/70 px-1.5 py-0.5 text-[10px] font-semibold leading-none text-muted-foreground">
138
+ {sessionContext.label}
139
+ </span>
140
+ ) : null}
141
+ {sessionContext.icon ? (
142
+ <span className="inline-flex h-[1.125rem] w-[1.125rem] shrink-0 items-center justify-center text-muted-foreground">
143
+ <SessionContextIconNode icon={sessionContext.icon} />
144
+ </span>
145
+ ) : null}
146
+ </span>
147
+ <span className="mt-0.5 flex min-w-0 items-center gap-2 text-[11px] text-muted-foreground">
148
+ <span className="min-w-0 truncate">{previewText}</span>
149
+ <span className="shrink-0">{trailingText}</span>
150
+ </span>
151
+ </span>
152
+ {runStatus ? (
153
+ <span className="mt-0.5 shrink-0">
154
+ <SessionRunBadge status={runStatus} />
155
+ </span>
156
+ ) : null}
157
+ </button>
158
+ );
159
+ }
160
+
161
+ function ChatSessionTitleSwitcherPopover({
162
+ selectedSessionKey,
163
+ title,
164
+ }: {
165
+ selectedSessionKey: string | null;
166
+ title: string;
167
+ }) {
168
+ const presenter = usePresenter();
169
+ const [isOpen, setIsOpen] = useState(false);
170
+ const [searchQuery, setSearchQuery] = useState("");
171
+ const optimisticReadAtBySessionKey = useChatSessionListStore(
172
+ (state) => state.optimisticReadAtBySessionKey,
173
+ );
174
+ const sessionTypesData = useChatQueryStore(
175
+ (state) => state.snapshot.sessionTypesQuery?.data ?? null,
176
+ );
177
+ const { isLoading, items } = useNcpSessionListView({ query: searchQuery });
178
+ const sortedItems = useMemo(
179
+ () => sortSessionItemsByActivityAtDesc(items),
180
+ [items],
181
+ );
182
+ const sessionTypeOptions = useMemo(
183
+ () => buildSessionTypeOptions(sessionTypesData?.options ?? []),
184
+ [sessionTypesData?.options],
185
+ );
186
+
187
+ const selectSession = (sessionKey: string) => {
188
+ setIsOpen(false);
189
+ setSearchQuery("");
190
+ if (sessionKey === selectedSessionKey) {
191
+ return;
192
+ }
193
+ presenter.chatSessionListManager.selectSession(sessionKey);
194
+ };
195
+ const updateOpen = (open: boolean) => {
196
+ setIsOpen(open);
197
+ if (!open) {
198
+ setSearchQuery("");
199
+ }
200
+ };
201
+
202
+ return (
203
+ <Popover open={isOpen} onOpenChange={updateOpen}>
204
+ <PopoverTrigger asChild>
205
+ <button
206
+ type="button"
207
+ aria-label={`${t("chatSessionSwitch")}: ${title}`}
208
+ aria-expanded={isOpen}
209
+ className={SWITCHER_TRIGGER_CLASS}
210
+ >
211
+ <span className="min-w-0 truncate">{title}</span>
212
+ <ChevronDown
213
+ className={cn(
214
+ "h-3.5 w-3.5 shrink-0 text-muted-foreground transition-transform group-hover:text-accent-foreground",
215
+ isOpen ? "rotate-180" : null,
216
+ )}
217
+ />
218
+ </button>
219
+ </PopoverTrigger>
220
+ <PopoverContent
221
+ align="start"
222
+ className="w-[22rem] max-w-[calc(100vw-2rem)] p-0"
223
+ >
224
+ <div className="space-y-2 border-b border-border px-3 py-2">
225
+ <div className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground/75">
226
+ {t("chatSessionSwitcherTitle")}
227
+ </div>
228
+ <div className="relative">
229
+ <Search className="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground/65" />
230
+ <Input
231
+ value={searchQuery}
232
+ onChange={(event) => setSearchQuery(event.target.value)}
233
+ placeholder={t("chatSearchSessionPlaceholder")}
234
+ aria-label={t("chatSearchSessionPlaceholder")}
235
+ className="h-8 rounded-lg bg-background pl-8 pr-2 text-xs"
236
+ autoFocus
237
+ />
238
+ </div>
239
+ </div>
240
+ <div className="max-h-80 overflow-y-auto p-1.5">
241
+ {isLoading ? (
242
+ <ChatSessionTitleSwitcherLoadingState />
243
+ ) : sortedItems.length > 0 ? (
244
+ <div className="space-y-1">
245
+ {sortedItems.map((item) => (
246
+ <ChatSessionSwitchItem
247
+ key={item.session.key}
248
+ item={item}
249
+ selectedSessionKey={selectedSessionKey}
250
+ optimisticReadAtBySessionKey={optimisticReadAtBySessionKey}
251
+ sessionTypeOptions={sessionTypeOptions}
252
+ onSelect={selectSession}
253
+ />
254
+ ))}
255
+ </div>
256
+ ) : (
257
+ <ChatSessionTitleSwitcherEmptyState
258
+ label={
259
+ searchQuery.trim()
260
+ ? t("chatSessionSwitcherNoResults")
261
+ : t("sessionsEmpty")
262
+ }
263
+ />
264
+ )}
265
+ </div>
266
+ </PopoverContent>
267
+ </Popover>
268
+ );
269
+ }
270
+
271
+ export function ChatSessionTitleSwitcher({
272
+ layoutMode,
273
+ selectedSessionKey,
274
+ title,
275
+ }: {
276
+ layoutMode: "desktop" | "mobile";
277
+ selectedSessionKey: string | null;
278
+ title: string;
279
+ }) {
280
+ const isSidebarCollapsed = useViewportLayoutStore(
281
+ (state) => state.isSidebarCollapsed,
282
+ );
283
+
284
+ if (layoutMode !== "desktop" || !isSidebarCollapsed) {
285
+ return <ChatSessionTitle title={title} />;
286
+ }
287
+
288
+ return (
289
+ <ChatSessionTitleSwitcherPopover
290
+ selectedSessionKey={selectedSessionKey}
291
+ title={title}
292
+ />
293
+ );
294
+ }
@@ -5,7 +5,7 @@ import {
5
5
  sessionActivityPreviewText,
6
6
  sessionDisplayName,
7
7
  sessionMatchesQuery
8
- } from '../chat-session-display.utils';
8
+ } from '@/features/chat/features/session/utils/chat-session-display.utils';
9
9
 
10
10
  function createSession(overrides: Partial<SessionEntryView> = {}): SessionEntryView {
11
11
  return {
@@ -88,6 +88,20 @@ describe('chat-session-display', () => {
88
88
  ).toBe('最终回复内容');
89
89
  });
90
90
 
91
+ it('does not show cancelled activity as ordinary session preview text', () => {
92
+ expect(
93
+ sessionActivityPreviewText(
94
+ createSession({
95
+ activityPreview: {
96
+ state: 'cancelled',
97
+ statusText: 'Run interrupted: User stopped the current run.',
98
+ timestamp: '2026-05-16T01:00:00.000Z'
99
+ }
100
+ })
101
+ )
102
+ ).toBeNull();
103
+ });
104
+
91
105
  it('formats today activity as time like WeChat session lists', () => {
92
106
  expect(
93
107
  formatSessionListTime(
@@ -105,8 +105,8 @@ describe('adaptNcpSessionSummary', () => {
105
105
  metadata: {
106
106
  last_activity_preview: {
107
107
  state: 'completed',
108
- replyText: '已经整理好方案',
109
- statusText: '工具调用完成',
108
+ replyText: 'Plan is ready',
109
+ statusText: 'Tool call completed',
110
110
  timestamp: '2026-05-16T01:00:00.000Z',
111
111
  },
112
112
  },
@@ -115,8 +115,28 @@ describe('adaptNcpSessionSummary', () => {
115
115
 
116
116
  expect(adapted.activityPreview).toEqual({
117
117
  state: 'completed',
118
- replyText: '已经整理好方案',
119
- statusText: '工具调用完成',
118
+ replyText: 'Plan is ready',
119
+ statusText: 'Tool call completed',
120
+ timestamp: '2026-05-16T01:00:00.000Z',
121
+ });
122
+ });
123
+
124
+ it('maps cancelled session activity preview metadata into the session entry', () => {
125
+ const adapted = adaptNcpSessionSummary(
126
+ createSummary({
127
+ metadata: {
128
+ last_activity_preview: {
129
+ state: 'cancelled',
130
+ statusText: 'Run interrupted: User stopped the current run.',
131
+ timestamp: '2026-05-16T01:00:00.000Z',
132
+ },
133
+ },
134
+ }),
135
+ );
136
+
137
+ expect(adapted.activityPreview).toEqual({
138
+ state: 'cancelled',
139
+ statusText: 'Run interrupted: User stopped the current run.',
120
140
  timestamp: '2026-05-16T01:00:00.000Z',
121
141
  });
122
142
  });
@@ -15,6 +15,9 @@ export function sessionActivityPreviewText(session: SessionEntryView): string |
15
15
  if (!preview) {
16
16
  return null;
17
17
  }
18
+ if (preview.state === 'cancelled') {
19
+ return null;
20
+ }
18
21
  if (preview.state === 'failed' || preview.state === 'running') {
19
22
  return preview.statusText ?? preview.replyText ?? null;
20
23
  }
@@ -18,6 +18,7 @@ const SESSION_ACTIVITY_PREVIEW_STATE_SET = new Set<SessionActivityPreviewView['s
18
18
  'running',
19
19
  'completed',
20
20
  'failed',
21
+ 'cancelled',
21
22
  'idle'
22
23
  ]);
23
24