@nextclaw/ui 0.14.1 → 0.14.3

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 (224) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/assets/{api-DPcvCLfa.js → api-BP_8IaxB.js} +7 -7
  3. package/dist/assets/channels-list-page-DEh5Jjqx.js +8 -0
  4. package/dist/assets/chat-page-BRB0N5WW.js +105 -0
  5. package/dist/assets/config-split-page-CRhlZ_pH.js +1 -0
  6. package/dist/assets/{confirm-dialog-CWh5FfK2.js → confirm-dialog-DsivFJ4r.js} +2 -2
  7. package/dist/assets/{desktop-update-config-DsRhBizi.js → desktop-update-config-B0w9p2ep.js} +1 -1
  8. package/dist/assets/{dist-DPrgcEC0.js → dist-CikkhOlq.js} +1 -1
  9. package/dist/assets/{dist-DsE4OsSS.js → dist-PVA_xL1D.js} +1 -1
  10. package/dist/assets/doc-browser-BoNJ7-tE.js +1 -0
  11. package/dist/assets/doc-browser-CdJ_d4ik.js +1 -0
  12. package/dist/assets/doc-browser-context-MTw4EvKd.js +1 -0
  13. package/dist/assets/{ellipsis-B81FStLm.js → ellipsis-y6vDtq4M.js} +1 -1
  14. package/dist/assets/{external-link-C_dmG-WJ.js → external-link-DVJIQf_G.js} +1 -1
  15. package/dist/assets/index-BiE-K8-q.css +1 -0
  16. package/dist/assets/index-D9BAE_L_.js +105 -0
  17. package/dist/assets/mcp-marketplace-page-B1SZNY5m.js +40 -0
  18. package/dist/assets/mcp-marketplace-page-Cq_Tw42T.js +1 -0
  19. package/dist/assets/middleware-CkFT3R4v.js +1 -0
  20. package/dist/assets/model-config-page-DotaTUHb.js +1 -0
  21. package/dist/assets/plus-BbZqquJe.js +1 -0
  22. package/dist/assets/provider-scoped-model-input-DsFRuWJH.js +1 -0
  23. package/dist/assets/providers-config-page-DW4tQSLN.js +1 -0
  24. package/dist/assets/{react-sZz5HSCm.js → react-VNqT5Zp7.js} +1 -1
  25. package/dist/assets/remote-B8TiUfwW.js +1 -0
  26. package/dist/assets/rotate-cw-DjHDTkCv.js +1 -0
  27. package/dist/assets/runtime-config-page-S6ErRHR8.js +1 -0
  28. package/dist/assets/{save-MzjoiHri.js → save-BOJ477H-.js} +1 -1
  29. package/dist/assets/{search-B29dE4GN.js → search-BEti5beA.js} +1 -1
  30. package/dist/assets/{search-config-page-Dl5sZzob.js → search-config-page-DGj5am03.js} +1 -1
  31. package/dist/assets/{secrets-config-page-C_l-IFCK.js → secrets-config-page--IzrIOid.js} +2 -2
  32. package/dist/assets/{select-BKmh05hd.js → select-hoj8dK4K.js} +2 -2
  33. package/dist/assets/{tag-chip-BS_7Ueom.js → tag-chip-T8-h-die.js} +1 -1
  34. package/dist/assets/use-config-CjRV2hwL.js +1 -0
  35. package/dist/assets/x-D9PXs_xP.js +1 -0
  36. package/dist/index.html +70 -19
  37. package/index.html +55 -5
  38. package/package.json +9 -9
  39. package/src/app/components/app-presenter-provider.tsx +11 -2
  40. package/src/app/components/layout/__tests__/app-layout.test.tsx +11 -2
  41. package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +110 -79
  42. package/src/app/components/layout/app-layout.tsx +18 -4
  43. package/src/app/components/layout/header.tsx +3 -3
  44. package/src/app/components/layout/page-layout.tsx +2 -2
  45. package/src/app/components/layout/runtime-status-entry.tsx +7 -7
  46. package/src/app/components/layout/sidebar-items.tsx +194 -71
  47. package/src/app/components/layout/sidebar-rail.styles.ts +15 -0
  48. package/src/app/components/layout/sidebar.tsx +340 -83
  49. package/src/app/managers/viewport-layout.manager.ts +10 -0
  50. package/src/app/stores/viewport-layout.store.ts +39 -3
  51. package/src/app/styles/design-system.css +388 -26
  52. package/src/app/test/vitest-setup.ts +31 -0
  53. package/src/features/agents/components/__tests__/agents-page.test.tsx +62 -39
  54. package/src/features/agents/components/agents-page.tsx +10 -6
  55. package/src/features/chat/components/chat-sidebar-list-mode-switch.tsx +3 -3
  56. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +145 -0
  57. package/src/features/chat/components/conversation/__tests__/chat-conversation-header.test.tsx +4 -2
  58. package/src/features/chat/components/conversation/__tests__/chat-conversation-panel.test.tsx +69 -762
  59. package/src/features/chat/components/conversation/__tests__/chat-conversation-workspace-section.test.tsx +132 -0
  60. package/src/features/chat/components/conversation/chat-conversation-content.tsx +22 -15
  61. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +15 -16
  62. package/src/features/chat/components/conversation/chat-conversation-header.tsx +7 -6
  63. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +19 -19
  64. package/src/features/chat/components/conversation/chat-conversation-skeleton.tsx +13 -13
  65. package/src/features/chat/components/conversation/chat-conversation-workspace-section.tsx +10 -4
  66. package/src/features/chat/components/layout/__tests__/chat-sidebar-read-state.test.tsx +52 -11
  67. package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +18 -16
  68. package/src/features/chat/components/layout/chat-page-shell.tsx +1 -1
  69. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +247 -0
  70. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +93 -20
  71. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +93 -36
  72. package/src/features/chat/components/layout/chat-sidebar.tsx +60 -93
  73. package/src/features/chat/components/providers/chat-presenter.provider.tsx +0 -6
  74. package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +225 -0
  75. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +199 -0
  76. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +308 -0
  77. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +439 -0
  78. package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +194 -0
  79. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-attachments.ts +66 -0
  80. package/src/features/chat/features/{input/hooks/use-chat-input-bar-query-state.ts → conversation/hooks/use-session-conversation-input-query.ts} +51 -20
  81. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-state.ts +281 -0
  82. package/src/features/chat/features/conversation/utils/session-conversation-input-toolbar.utils.ts +158 -0
  83. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +56 -0
  84. package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +117 -0
  85. package/src/features/chat/features/input/input-surface-plugins/__tests__/panel-app-reference-plugin.test.ts +77 -0
  86. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +108 -0
  87. package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +10 -0
  88. package/src/features/chat/features/input/input-surface-plugins/input-surface-search.utils.ts +108 -0
  89. package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +108 -0
  90. package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +161 -0
  91. package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +22 -1
  92. package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +26 -13
  93. package/src/features/chat/features/input/utils/__tests__/ncp-chat-input-availability.utils.test.ts +7 -28
  94. package/src/features/chat/features/input/utils/chat-composer-state.utils.ts +6 -0
  95. package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +26 -51
  96. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +2 -0
  97. package/src/features/chat/features/input/utils/ncp-chat-input-availability.utils.ts +5 -6
  98. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +47 -12
  99. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +7 -7
  100. package/src/features/chat/features/message/components/chat-message-list.container.tsx +99 -18
  101. package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +31 -12
  102. package/src/features/chat/features/message/utils/chat-message-markdown-part.utils.ts +38 -0
  103. package/src/features/chat/features/message/utils/chat-message-part.utils.ts +1 -1
  104. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +7 -7
  105. package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +6 -6
  106. package/src/features/chat/features/session/components/session-context-icon.tsx +2 -2
  107. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-badge.test.tsx +2 -2
  108. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +1 -1
  109. package/src/features/chat/features/session/components/session-header/chat-session-header-menu-item.tsx +1 -1
  110. package/src/features/chat/features/session/components/session-header/chat-session-metadata-dialog.tsx +1 -1
  111. package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +3 -3
  112. package/src/features/chat/features/session/components/session-run-badge.tsx +1 -1
  113. package/src/features/chat/features/session/hooks/__tests__/use-chat-session-project.test.tsx +3 -30
  114. package/src/features/chat/features/session/hooks/use-chat-session-project.ts +0 -5
  115. package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +8 -0
  116. package/src/features/chat/features/session-type/components/__tests__/chat-session-type-menu.test.tsx +2 -2
  117. package/src/features/chat/features/session-type/components/__tests__/chat-session-type-option-item.test.tsx +2 -0
  118. package/src/features/chat/features/session-type/components/chat-session-type-menu.tsx +2 -2
  119. package/src/features/chat/features/session-type/components/chat-session-type-option-item.tsx +2 -2
  120. package/src/features/chat/features/session-type/hooks/use-chat-session-type-state.ts +10 -2
  121. package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +49 -31
  122. package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +2 -9
  123. package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +21 -10
  124. package/src/features/chat/features/welcome/components/chat-welcome-agent-picker.tsx +3 -3
  125. package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +4 -4
  126. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +14 -19
  127. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +5 -5
  128. package/src/features/chat/features/welcome/components/chat-welcome.tsx +6 -6
  129. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +6 -8
  130. package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +1 -16
  131. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +54 -0
  132. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +44 -76
  133. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +5 -1
  134. package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +7 -1
  135. package/src/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state.ts +40 -31
  136. package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +59 -1
  137. package/src/features/chat/features/workspace/utils/chat-thread-workspace-session.utils.ts +109 -0
  138. package/src/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils.ts +44 -4
  139. package/src/features/chat/index.ts +0 -1
  140. package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +51 -99
  141. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +135 -21
  142. package/src/features/chat/managers/chat-session-list.manager.ts +12 -43
  143. package/src/features/chat/managers/chat-thread.manager.ts +108 -34
  144. package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +48 -1
  145. package/src/features/chat/pages/__tests__/ncp-chat-page.test.tsx +72 -0
  146. package/src/features/chat/pages/ncp-chat-page.tsx +0 -122
  147. package/src/features/chat/presenters/chat.presenter.ts +2 -19
  148. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +31 -0
  149. package/src/features/chat/stores/chat-thread.store.ts +42 -26
  150. package/src/features/panel-apps/components/panel-app-list-item.tsx +8 -8
  151. package/src/features/panel-apps/components/panel-app-toolbar.tsx +4 -4
  152. package/src/features/panel-apps/components/panel-apps-list.tsx +20 -20
  153. package/src/features/panel-apps/hooks/use-panel-apps.ts +2 -1
  154. package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +70 -6
  155. package/src/features/pwa/managers/pwa-shell-theme.manager.ts +10 -5
  156. package/src/features/service-apps/components/service-action-authorization-dialog.tsx +8 -8
  157. package/src/features/service-apps/components/service-apps-panel.tsx +24 -24
  158. package/src/features/settings/components/config/provider-advanced-settings-section.tsx +5 -5
  159. package/src/features/settings/components/config/provider-auth-section.tsx +7 -7
  160. package/src/features/settings/components/config/provider-form.tsx +10 -10
  161. package/src/features/settings/components/config/provider-models-section.tsx +16 -16
  162. package/src/features/settings/components/config/provider-pill-selector.tsx +1 -1
  163. package/src/features/settings/pages/providers-config-page.tsx +17 -17
  164. package/src/platforms/desktop/components/__tests__/desktop-app-shell.test.tsx +50 -10
  165. package/src/platforms/desktop/components/desktop-app-shell.tsx +34 -10
  166. package/src/platforms/desktop/components/desktop-window-chrome.tsx +41 -16
  167. package/src/shared/components/config-split-page.tsx +11 -11
  168. package/src/shared/components/doc-browser/doc-browser-home-page.tsx +2 -2
  169. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +4 -4
  170. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +4 -4
  171. package/src/shared/components/doc-browser/doc-browser.tsx +3 -3
  172. package/src/shared/components/resizable-right-panel/resizable-right-panel.tsx +1 -1
  173. package/src/shared/components/ui/actions/icon-action-button.tsx +1 -1
  174. package/src/shared/components/ui/button.tsx +4 -4
  175. package/src/shared/components/ui/card.tsx +4 -4
  176. package/src/shared/components/ui/dialog.tsx +5 -5
  177. package/src/shared/components/ui/input.tsx +1 -1
  178. package/src/shared/components/ui/label.tsx +1 -1
  179. package/src/shared/components/ui/popover.tsx +1 -1
  180. package/src/shared/components/ui/select.tsx +3 -3
  181. package/src/shared/components/ui/switch.tsx +3 -3
  182. package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +24 -1
  183. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +8 -4
  184. package/src/shared/components/ui/tabs.tsx +4 -4
  185. package/src/shared/components/ui/textarea.tsx +1 -1
  186. package/src/shared/lib/i18n/locales/en-US/chat.json +20 -2
  187. package/src/shared/lib/i18n/locales/en-US/core.json +9 -2
  188. package/src/shared/lib/i18n/locales/zh-CN/chat.json +20 -2
  189. package/src/shared/lib/i18n/locales/zh-CN/core.json +9 -2
  190. package/src/shared/lib/theme/index.ts +94 -52
  191. package/src/shared/lib/ui-document-title/__tests__/ui-document-title.test.ts +49 -0
  192. package/src/shared/lib/ui-document-title/index.ts +50 -21
  193. package/dist/assets/channels-list-page-D6jtziu4.js +0 -8
  194. package/dist/assets/chat-page-BBnNJoKG.js +0 -105
  195. package/dist/assets/config-split-page-BCJhqdK9.js +0 -1
  196. package/dist/assets/doc-browser-C8xOF_9F.js +0 -1
  197. package/dist/assets/doc-browser-DgQedeAn.js +0 -1
  198. package/dist/assets/doc-browser-context-B46tQGmO.js +0 -1
  199. package/dist/assets/index-7o1WdUbU.js +0 -103
  200. package/dist/assets/index-DQWmEWzk.css +0 -1
  201. package/dist/assets/mcp-marketplace-page-CWZN7ohK.js +0 -1
  202. package/dist/assets/mcp-marketplace-page-Dzull_4-.js +0 -40
  203. package/dist/assets/model-config-page-Cv0O3BIj.js +0 -1
  204. package/dist/assets/plus-Bnt56oqG.js +0 -1
  205. package/dist/assets/provider-scoped-model-input-Duuh7i3h.js +0 -1
  206. package/dist/assets/providers-config-page-CPDz0Lxc.js +0 -1
  207. package/dist/assets/remote-B0LMs8u7.js +0 -1
  208. package/dist/assets/rotate-cw-BfyG_KEF.js +0 -1
  209. package/dist/assets/runtime-config-page-q0fvv8ME.js +0 -1
  210. package/dist/assets/use-config-DZkBWIdL.js +0 -1
  211. package/dist/assets/x-CW2KO-Hz.js +0 -1
  212. package/src/features/chat/components/conversation/chat-conversation-alerts.tsx +0 -37
  213. package/src/features/chat/features/input/components/__tests__/chat-attachment-upload-limit.test.ts +0 -38
  214. package/src/features/chat/features/input/components/chat-input-bar.container.tsx +0 -496
  215. package/src/features/chat/features/message/utils/chat-message-inline-content.utils.ts +0 -95
  216. package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +0 -448
  217. package/src/features/chat/managers/__tests__/chat-run-snapshot.manager.test.ts +0 -129
  218. package/src/features/chat/managers/__tests__/chat-run.manager.test.ts +0 -236
  219. package/src/features/chat/managers/__tests__/chat-session-preference-sync.manager.test.ts +0 -219
  220. package/src/features/chat/managers/chat-input.manager.ts +0 -430
  221. package/src/features/chat/managers/chat-run.manager.ts +0 -110
  222. package/src/features/chat/managers/chat-session-preference-sync.manager.ts +0 -142
  223. package/src/features/chat/stores/chat-input.store.ts +0 -104
  224. /package/dist/assets/{config-hints-CTqBnCDp.js → config-hints-BkYBc7z0.js} +0 -0
@@ -1,9 +1,9 @@
1
1
  import type { ReactNode } from "react";
2
+ import type { SetStateAction } from "react";
2
3
  import { render, screen } from "@testing-library/react";
3
4
  import userEvent from "@testing-library/user-event";
4
5
  import { beforeEach, describe, expect, it, vi } from "vitest";
5
6
  import { ChatConversationWelcome } from "@/features/chat/features/welcome/components/chat-conversation-welcome";
6
- import { useChatInputStore } from "@/features/chat/stores/chat-input.store";
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";
@@ -11,14 +11,24 @@ import type { ChatQuerySnapshot } from "@/features/chat/stores/ncp-chat-query.st
11
11
 
12
12
  const mocks = vi.hoisted(() => ({
13
13
  setSelectedAgentId: vi.fn(),
14
- applyPromptSuggestion: vi.fn(),
15
- setPendingProjectRoot: vi.fn(),
16
- setPendingSessionType: vi.fn(),
17
14
  agents: [
18
15
  { id: "main", displayName: "Main", runtime: "native" },
19
16
  { id: "engineer", displayName: "Engineer", runtime: "codex" },
20
17
  ],
21
18
  }));
19
+ const persistStorage = new Map<string, unknown>();
20
+
21
+ function createPersistStorage() {
22
+ return {
23
+ getItem: (name: string) => persistStorage.get(name) ?? null,
24
+ setItem: (name: string, value: unknown) => {
25
+ persistStorage.set(name, value);
26
+ },
27
+ removeItem: (name: string) => {
28
+ persistStorage.delete(name);
29
+ },
30
+ };
31
+ }
22
32
 
23
33
  vi.mock("@/features/chat/features/welcome/components/chat-welcome", () => ({
24
34
  ChatWelcome: ({
@@ -60,11 +70,6 @@ vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
60
70
  chatSessionListManager: {
61
71
  setSelectedAgentId: mocks.setSelectedAgentId,
62
72
  },
63
- chatInputManager: {
64
- applyPromptSuggestion: mocks.applyPromptSuggestion,
65
- setPendingProjectRoot: mocks.setPendingProjectRoot,
66
- setPendingSessionType: mocks.setPendingSessionType,
67
- },
68
73
  }),
69
74
  }));
70
75
 
@@ -88,19 +93,10 @@ function createFetchedQuery<TData>(data: TData) {
88
93
  }
89
94
 
90
95
  function resetWelcomeTestState() {
96
+ persistStorage.clear();
97
+ useChatSessionListStore.persist.setOptions({ storage: createPersistStorage() as never });
98
+ useChatThreadStore.persist.setOptions({ storage: createPersistStorage() as never });
91
99
  mocks.setSelectedAgentId.mockReset();
92
- mocks.applyPromptSuggestion.mockReset();
93
- mocks.setPendingProjectRoot.mockReset();
94
- mocks.setPendingSessionType.mockReset();
95
- useChatInputStore.setState({
96
- snapshot: {
97
- ...useChatInputStore.getState().snapshot,
98
- defaultSessionType: "native",
99
- pendingSessionType: "native",
100
- selectedSessionType: undefined,
101
- pendingProjectRoot: null,
102
- },
103
- });
104
100
  useChatSessionListStore.setState({
105
101
  snapshot: {
106
102
  ...useChatSessionListStore.getState().snapshot,
@@ -138,56 +134,78 @@ function resetWelcomeTestState() {
138
134
  });
139
135
  }
140
136
 
137
+ function renderWelcome(overrides: Partial<{
138
+ onSelectPrompt: (prompt: string) => void;
139
+ onSelectProjectRoot: (projectRoot: string | null) => void;
140
+ onSelectSessionType: (sessionType: SetStateAction<string>) => void;
141
+ }> = {}) {
142
+ return render(
143
+ <ChatConversationWelcome
144
+ inputSlot={<div data-testid="input-slot" />}
145
+ pendingProjectRoot={null}
146
+ pendingSessionType="native"
147
+ selectedSessionTypeValue={null}
148
+ onSelectPrompt={overrides.onSelectPrompt ?? vi.fn()}
149
+ onSelectProjectRoot={overrides.onSelectProjectRoot ?? vi.fn()}
150
+ onSelectSessionType={overrides.onSelectSessionType ?? vi.fn()}
151
+ />,
152
+ );
153
+ }
154
+
141
155
  describe("ChatConversationWelcome", () => {
142
156
  beforeEach(resetWelcomeTestState);
143
157
 
144
- it("stores the selected welcome project through the input manager", async () => {
158
+ it("emits the selected welcome project through the local input callback", async () => {
145
159
  const user = userEvent.setup();
160
+ const onSelectProjectRoot = vi.fn();
146
161
 
147
- render(<ChatConversationWelcome inputSlot={<div data-testid="input-slot" />} />);
162
+ renderWelcome({ onSelectProjectRoot });
148
163
 
149
164
  await user.click(
150
165
  screen.getByRole("button", { name: "select draft project" }),
151
166
  );
152
167
 
153
- expect(mocks.setPendingProjectRoot).toHaveBeenCalledWith("/tmp/project-alpha");
168
+ expect(onSelectProjectRoot).toHaveBeenCalledWith("/tmp/project-alpha");
154
169
  });
155
170
 
156
- it("applies a selected prompt suggestion through the input manager", async () => {
171
+ it("emits a selected prompt suggestion through the local input callback", async () => {
157
172
  const user = userEvent.setup();
173
+ const onSelectPrompt = vi.fn();
158
174
 
159
- render(<ChatConversationWelcome inputSlot={<div data-testid="input-slot" />} />);
175
+ renderWelcome({ onSelectPrompt });
160
176
 
161
177
  await user.click(
162
178
  screen.getByRole("button", { name: "pick prompt" }),
163
179
  );
164
180
 
165
- expect(mocks.applyPromptSuggestion).toHaveBeenCalledWith("example prompt");
181
+ expect(onSelectPrompt).toHaveBeenCalledWith("example prompt");
166
182
  });
167
183
 
168
184
 
169
185
  it("syncs the pending session type when switching the draft agent", async () => {
170
186
  const user = userEvent.setup();
187
+ const onSelectSessionType = vi.fn();
171
188
 
172
- render(<ChatConversationWelcome inputSlot={<div data-testid="input-slot" />} />);
189
+ renderWelcome({ onSelectSessionType });
173
190
 
174
191
  await user.click(
175
192
  screen.getByRole("button", { name: "switch draft agent" }),
176
193
  );
177
194
 
178
195
  expect(mocks.setSelectedAgentId).toHaveBeenCalledWith("engineer");
179
- expect(mocks.setPendingSessionType).toHaveBeenCalledWith("codex");
196
+ expect(onSelectSessionType).toHaveBeenCalledWith("codex");
180
197
  });
181
198
 
182
199
  it("stores an explicit welcome session type selection", async () => {
183
200
  const user = userEvent.setup();
201
+ const onSelectSessionType = vi.fn();
184
202
 
185
- render(<ChatConversationWelcome inputSlot={<div data-testid="input-slot" />} />);
203
+ renderWelcome({ onSelectSessionType });
186
204
 
187
205
  await user.click(
188
206
  screen.getByRole("button", { name: "switch session type" }),
189
207
  );
190
208
 
191
- expect(mocks.setPendingSessionType).toHaveBeenCalledWith("codex");
209
+ expect(onSelectSessionType).toHaveBeenCalledWith("codex");
192
210
  });
193
211
  });
@@ -10,8 +10,8 @@ vi.mock(
10
10
  );
11
11
 
12
12
  const sessionTypeOptions = [
13
- { value: 'native', label: 'Native', ready: true },
14
- { value: 'codex', label: 'Codex', ready: true },
13
+ { value: 'native', label: 'Native', icon: null, ready: true },
14
+ { value: 'codex', label: 'Codex', icon: null, ready: true },
15
15
  ];
16
16
 
17
17
  function renderWelcome(
@@ -89,17 +89,10 @@ describe('ChatWelcome', () => {
89
89
  defaultProjectRoot: '/Users/demo/.nextclaw/workspace',
90
90
  onSelectProjectRoot,
91
91
  projectOptions: [
92
- {
93
- projectRoot: '/Users/demo/.nextclaw/workspace',
94
- projectName: 'workspace',
95
- sessionCount: 0,
96
- isDefault: true,
97
- },
98
92
  {
99
93
  projectRoot: '/tmp/project-alpha',
100
94
  projectName: 'project-alpha',
101
95
  sessionCount: 3,
102
- isDefault: false,
103
96
  },
104
97
  ],
105
98
  });
@@ -1,4 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
+ import type { SetStateAction } from "react";
2
3
  import { usePresenter } from "@/features/chat/components/providers/chat-presenter.provider";
3
4
  import {
4
5
  buildSessionTypeOptions,
@@ -11,7 +12,6 @@ import {
11
12
  resolveChatWelcomeSelectedSessionType,
12
13
  } from "@/features/chat/features/welcome/utils/chat-welcome-selection.utils";
13
14
  import { buildChatWelcomeProjectOptions } from "@/features/chat/features/welcome/utils/chat-welcome-project-options.utils";
14
- import { useChatInputStore } from "@/features/chat/stores/chat-input.store";
15
15
  import { useChatQueryStore } from "@/features/chat/stores/ncp-chat-query.store";
16
16
  import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
17
17
  import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
@@ -23,13 +23,24 @@ const EMPTY_NCP_SESSION_SUMMARIES: NcpSessionSummaryView[] = [];
23
23
 
24
24
  type ChatConversationWelcomeProps = {
25
25
  inputSlot: ReactNode;
26
+ pendingProjectRoot: string | null;
27
+ pendingSessionType: string;
28
+ selectedSessionTypeValue: string | null;
29
+ onSelectProjectRoot: (projectRoot: string | null) => void;
30
+ onSelectPrompt: (prompt: string) => void;
31
+ onSelectSessionType: (sessionType: SetStateAction<string>) => void;
26
32
  };
27
33
 
28
34
  export function ChatConversationWelcome({
29
35
  inputSlot,
36
+ pendingProjectRoot,
37
+ pendingSessionType,
38
+ selectedSessionTypeValue,
39
+ onSelectProjectRoot,
40
+ onSelectPrompt,
41
+ onSelectSessionType,
30
42
  }: ChatConversationWelcomeProps) {
31
43
  const presenter = usePresenter();
32
- const inputSnapshot = useChatInputStore((state) => state.snapshot);
33
44
  const selectedAgentId = useChatSessionListStore(
34
45
  (state) => state.snapshot.selectedAgentId,
35
46
  );
@@ -53,18 +64,18 @@ export function ChatConversationWelcome({
53
64
  config?.agents.defaults.workspace,
54
65
  );
55
66
  const selectedProjectRoot = normalizeSessionProjectRootValue(
56
- inputSnapshot.pendingProjectRoot,
67
+ pendingProjectRoot,
57
68
  );
58
69
  const sessionTypeOptions = buildSessionTypeOptions(
59
70
  sessionTypesData?.options ?? [],
60
71
  );
61
72
  const defaultSessionType = normalizeSessionType(
62
- sessionTypesData?.defaultType ?? inputSnapshot.defaultSessionType ?? DEFAULT_SESSION_TYPE,
73
+ sessionTypesData?.defaultType ?? DEFAULT_SESSION_TYPE,
63
74
  );
64
75
  const selectedSessionType = resolveChatWelcomeSelectedSessionType({
65
76
  defaultSessionType,
66
- pendingSessionType: inputSnapshot.pendingSessionType,
67
- selectedSessionType: inputSnapshot.selectedSessionType,
77
+ pendingSessionType,
78
+ selectedSessionType: selectedSessionTypeValue,
68
79
  sessionTypeOptions,
69
80
  });
70
81
  const projectOptions = buildChatWelcomeProjectOptions({
@@ -73,7 +84,7 @@ export function ChatConversationWelcome({
73
84
  });
74
85
  const selectDraftAgent = (agentId: string) => {
75
86
  presenter.chatSessionListManager.setSelectedAgentId(agentId);
76
- presenter.chatInputManager.setPendingSessionType(
87
+ onSelectSessionType(
77
88
  resolveChatWelcomeSelectedSessionType({
78
89
  agents: availableAgents,
79
90
  agentId,
@@ -96,9 +107,9 @@ export function ChatConversationWelcome({
96
107
  selectedSessionType={selectedSessionType}
97
108
  sessionTypeOptions={sessionTypeOptions}
98
109
  onSelectAgent={selectDraftAgent}
99
- onSelectPrompt={presenter.chatInputManager.applyPromptSuggestion}
100
- onSelectProjectRoot={presenter.chatInputManager.setPendingProjectRoot}
101
- onSelectSessionType={presenter.chatInputManager.setPendingSessionType}
110
+ onSelectPrompt={onSelectPrompt}
111
+ onSelectProjectRoot={onSelectProjectRoot}
112
+ onSelectSessionType={onSelectSessionType}
102
113
  />
103
114
  );
104
115
  }
@@ -28,7 +28,7 @@ export function ChatWelcomeAgentPicker({
28
28
  <Select value={selectedAgentId} onValueChange={onSelectAgent}>
29
29
  <SelectTrigger
30
30
  aria-label={t('chatDraftAgentTitle')}
31
- className="h-auto w-auto min-w-0 gap-1 rounded-lg border-0 bg-transparent px-2 py-1.5 text-gray-500 shadow-none hover:bg-gray-100 hover:text-gray-800 focus:ring-0"
31
+ className="h-auto w-auto min-w-0 gap-1 rounded-lg border-0 bg-transparent px-2 py-1.5 text-muted-foreground shadow-none hover:bg-accent hover:text-accent-foreground focus:ring-0"
32
32
  >
33
33
  <span className="sr-only">{t('chatDraftAgentTitle')}</span>
34
34
  <div className="flex min-w-0 items-center gap-1.5">
@@ -48,7 +48,7 @@ export function ChatWelcomeAgentPicker({
48
48
  </div>
49
49
  </SelectTrigger>
50
50
  <SelectContent
51
- className="rounded-xl border-gray-200/80 shadow-lg"
51
+ className="rounded-xl border-border shadow-lg"
52
52
  style={{ maxHeight: AGENT_PICKER_MAX_HEIGHT }}
53
53
  >
54
54
  {agents.map((agent) => (
@@ -60,7 +60,7 @@ export function ChatWelcomeAgentPicker({
60
60
  avatarUrl={agent.avatarUrl}
61
61
  className="h-5 w-5 shrink-0"
62
62
  />
63
- <span className="truncate text-sm font-medium text-gray-700">
63
+ <span className="truncate text-sm font-medium text-foreground">
64
64
  {agent.displayName?.trim() || agent.id}
65
65
  </span>
66
66
  </div>
@@ -35,15 +35,15 @@ export function ChatWelcomeCapabilityGrid({
35
35
  <button
36
36
  key={capability.titleKey}
37
37
  onClick={() => onSelectPrompt(t(capability.promptKey))}
38
- className="min-w-0 rounded-2xl border border-gray-200 bg-white p-3 text-left shadow-card transition-shadow hover:shadow-card-hover sm:p-4"
38
+ className="min-w-0 rounded-2xl border border-border bg-card p-3 text-left text-card-foreground shadow-card transition-[border-color,box-shadow,transform] hover:border-primary/30 hover:shadow-card-hover sm:p-4"
39
39
  >
40
- <div className="mb-3 flex h-9 w-9 items-center justify-center rounded-xl bg-primary/8">
40
+ <div className="mb-3 flex h-9 w-9 items-center justify-center rounded-xl bg-accent">
41
41
  <Icon className="h-4.5 w-4.5 text-primary" />
42
42
  </div>
43
- <div className="mb-1 text-sm font-semibold text-gray-900">
43
+ <div className="mb-1 text-sm font-semibold text-foreground">
44
44
  {t(capability.titleKey)}
45
45
  </div>
46
- <div className="text-[11px] leading-relaxed text-gray-500">
46
+ <div className="text-[11px] leading-relaxed text-muted-foreground">
47
47
  {t(capability.descKey)}
48
48
  </div>
49
49
  </button>
@@ -49,7 +49,7 @@ export function ChatWelcomeProjectPicker({
49
49
  <PopoverTrigger asChild>
50
50
  <button
51
51
  type="button"
52
- className="inline-flex min-w-0 max-w-full items-center gap-1.5 rounded-lg px-2 py-1.5 text-left font-medium text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/25"
52
+ className="inline-flex min-w-0 max-w-full items-center gap-1.5 rounded-lg px-2 py-1.5 text-left font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/25"
53
53
  title={projectRoot ?? undefined}
54
54
  aria-label={t('chatWelcomeProjectPickerLabel')}
55
55
  disabled={!selectable || isSaving}
@@ -57,19 +57,19 @@ export function ChatWelcomeProjectPicker({
57
57
  <FolderOpen className="h-4 w-4 shrink-0" />
58
58
  <span className="truncate">{projectLabel}</span>
59
59
  {isDefaultProject ? (
60
- <span className="shrink-0 text-xs font-normal text-gray-400">
60
+ <span className="shrink-0 text-xs font-normal text-muted-foreground/70">
61
61
  {t('chatWelcomeProjectDefaultBadge')}
62
62
  </span>
63
63
  ) : null}
64
- <ChevronDown className="h-3.5 w-3.5 shrink-0 text-gray-400" />
64
+ <ChevronDown className="h-3.5 w-3.5 shrink-0 text-muted-foreground/70" />
65
65
  </button>
66
66
  </PopoverTrigger>
67
67
  <PopoverContent
68
68
  align="start"
69
- className="flex w-[min(20rem,calc(100vw-1rem))] flex-col overflow-hidden rounded-2xl border border-gray-200/80 bg-white p-0 shadow-[0_24px_60px_-28px_rgba(15,23,42,0.38)]"
69
+ className="flex w-[min(20rem,calc(100vw-1rem))] flex-col overflow-hidden rounded-2xl border border-border bg-popover p-0 text-popover-foreground shadow-[0_24px_60px_-28px_rgba(15,23,42,0.38)]"
70
70
  style={{ maxHeight: PROJECT_PICKER_MAX_HEIGHT }}
71
71
  >
72
- <div className="shrink-0 px-3 pb-2 pt-3 text-[10px] font-semibold uppercase tracking-[0.18em] text-gray-400">
72
+ <div className="shrink-0 px-3 pb-2 pt-3 text-[10px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
73
73
  {t('chatWelcomeProjectRecentTitle')}
74
74
  </div>
75
75
  <div className="min-h-0 flex-1 overflow-y-auto px-1.5 pb-1.5">
@@ -78,51 +78,46 @@ export function ChatWelcomeProjectPicker({
78
78
  <button
79
79
  key={option.projectRoot}
80
80
  type="button"
81
- className="flex w-full min-w-0 items-start gap-2 rounded-xl px-2 py-2 text-left transition-colors hover:bg-gray-50"
81
+ className="flex w-full min-w-0 items-start gap-2 rounded-xl px-2 py-2 text-left transition-colors hover:bg-accent"
82
82
  title={option.projectRoot}
83
83
  onClick={() => {
84
84
  void selectProjectRoot(option.projectRoot);
85
85
  }}
86
86
  >
87
- <FolderOpen className="mt-0.5 h-4 w-4 shrink-0 text-gray-400" />
87
+ <FolderOpen className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground/70" />
88
88
  <span className="min-w-0 flex-1">
89
89
  <span className="flex min-w-0 items-center gap-1.5">
90
- <span className="truncate text-[13px] font-semibold text-gray-800">
90
+ <span className="truncate text-[13px] font-semibold text-foreground">
91
91
  {option.projectName}
92
92
  </span>
93
- {option.isDefault ? (
94
- <span className="shrink-0 text-[10px] font-medium text-gray-400">
95
- {t('chatWelcomeProjectDefaultBadge')}
96
- </span>
97
- ) : null}
98
93
  </span>
99
- <span className="block truncate text-[11px] leading-4 text-gray-500">
94
+ <span className="block truncate text-[11px] leading-4 text-muted-foreground">
100
95
  {option.projectRoot}
101
96
  </span>
102
97
  </span>
103
98
  {option.sessionCount > 0 ? (
104
- <span className="mt-0.5 shrink-0 rounded-full bg-gray-100 px-1.5 py-0.5 text-[10px] font-medium text-gray-500">
99
+ <span className="mt-0.5 shrink-0 rounded-full bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
105
100
  {option.sessionCount}
106
101
  </span>
107
102
  ) : null}
108
103
  </button>
109
104
  ))
110
105
  ) : (
111
- <div className="px-2 py-5 text-center text-xs text-gray-400">
106
+ <div className="px-2 py-5 text-center text-xs text-muted-foreground">
112
107
  {t('chatWelcomeProjectNoRecent')}
113
108
  </div>
114
109
  )}
115
110
  </div>
116
- <div className="shrink-0 border-t border-gray-100 p-1.5">
111
+ <div className="shrink-0 border-t border-border p-1.5">
117
112
  <button
118
113
  type="button"
119
- className="flex w-full items-center gap-2 rounded-xl px-2.5 py-2 text-left text-[13px] font-semibold text-gray-700 transition-colors hover:bg-gray-50"
114
+ className="flex w-full items-center gap-2 rounded-xl px-2.5 py-2 text-left text-[13px] font-semibold text-foreground transition-colors hover:bg-accent"
120
115
  onClick={() => {
121
116
  setIsOpen(false);
122
117
  onOpenProjectDialog();
123
118
  }}
124
119
  >
125
- <FolderOpen className="h-4 w-4 shrink-0 text-gray-400" />
120
+ <FolderOpen className="h-4 w-4 shrink-0 text-muted-foreground/70" />
126
121
  <span>{t('chatWelcomeProjectOpenFolder')}</span>
127
122
  </button>
128
123
  </div>
@@ -2,7 +2,7 @@ import { useState } from 'react';
2
2
  import { Bot, ChevronDown } from 'lucide-react';
3
3
  import { SessionContextIconNode } from '@/features/chat/features/session/components/session-context-icon';
4
4
  import { ChatSessionTypeOptionItem } from '@/features/chat/features/session-type/components/chat-session-type-option-item';
5
- import type { ChatInputSnapshot } from '@/features/chat/stores/chat-input.store';
5
+ import type { ChatSessionTypeOption } from '@/features/chat/features/session-type/utils/chat-session-type.utils';
6
6
  import {
7
7
  Popover,
8
8
  PopoverContent,
@@ -11,7 +11,7 @@ import {
11
11
  } from '@/shared/components/ui/popover';
12
12
  import { t } from '@/shared/lib/i18n';
13
13
 
14
- type SessionTypeOption = ChatInputSnapshot['sessionTypeOptions'][number];
14
+ type SessionTypeOption = ChatSessionTypeOption;
15
15
 
16
16
  type ChatWelcomeSessionTypePickerProps = {
17
17
  options: readonly SessionTypeOption[];
@@ -39,7 +39,7 @@ export function ChatWelcomeSessionTypePicker({
39
39
  <PopoverTrigger asChild>
40
40
  <button
41
41
  type="button"
42
- className="inline-flex min-w-0 max-w-full items-center gap-1.5 rounded-lg px-2 py-1.5 text-left font-medium text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/25"
42
+ className="inline-flex min-w-0 max-w-full items-center gap-1.5 rounded-lg px-2 py-1.5 text-left font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/25"
43
43
  aria-label={t('chatWelcomeSessionTypePickerLabel')}
44
44
  >
45
45
  {selectedOption.icon?.src ? (
@@ -56,12 +56,12 @@ export function ChatWelcomeSessionTypePicker({
56
56
  <Bot className="h-4 w-4 shrink-0" />
57
57
  )}
58
58
  <span className="truncate">{selectedOption.label}</span>
59
- <ChevronDown className="h-3.5 w-3.5 shrink-0 text-gray-400" />
59
+ <ChevronDown className="h-3.5 w-3.5 shrink-0 text-muted-foreground/70" />
60
60
  </button>
61
61
  </PopoverTrigger>
62
62
  <PopoverContent
63
63
  align="start"
64
- className="flex w-[min(16rem,calc(100vw-1rem))] flex-col overflow-hidden rounded-2xl border border-gray-200/80 bg-white p-1.5 shadow-[0_24px_60px_-28px_rgba(15,23,42,0.38)]"
64
+ className="flex w-[min(16rem,calc(100vw-1rem))] flex-col overflow-hidden rounded-2xl border border-border bg-popover p-1.5 text-popover-foreground shadow-[0_24px_60px_-28px_rgba(15,23,42,0.38)]"
65
65
  style={{ maxHeight: SESSION_TYPE_PICKER_MAX_HEIGHT }}
66
66
  >
67
67
  <div className="min-h-0 flex-1 overflow-y-auto">
@@ -7,11 +7,11 @@ import { ChatWelcomeCapabilityGrid } from '@/features/chat/features/welcome/comp
7
7
  import { ChatWelcomeProjectPicker } from '@/features/chat/features/welcome/components/chat-welcome-project-picker';
8
8
  import { ChatWelcomeSessionTypePicker } from '@/features/chat/features/welcome/components/chat-welcome-session-type-picker';
9
9
  import type { ChatWelcomeProjectOption } from '@/features/chat/features/welcome/utils/chat-welcome-project-options.utils';
10
- import type { ChatInputSnapshot } from '@/features/chat/stores/chat-input.store';
10
+ import type { ChatSessionTypeOption } from '@/features/chat/features/session-type/utils/chat-session-type.utils';
11
11
  import type { AgentProfileView } from '@/shared/lib/api';
12
12
  import { t } from '@/shared/lib/i18n';
13
13
 
14
- type SessionTypeOption = ChatInputSnapshot['sessionTypeOptions'][number];
14
+ type SessionTypeOption = ChatSessionTypeOption;
15
15
 
16
16
  type ChatWelcomeProps = {
17
17
  agents: AgentProfileView[];
@@ -64,22 +64,22 @@ export function ChatWelcome({
64
64
  return (
65
65
  <div className="flex min-h-full items-center justify-center px-4 py-8 sm:p-8">
66
66
  <div className="min-w-0 w-full max-w-[min(760px,100%)]">
67
- <div className="mx-auto mb-5 flex h-14 w-14 items-center justify-center rounded-2xl bg-primary/10">
67
+ <div className="mx-auto mb-5 flex h-14 w-14 items-center justify-center rounded-2xl border border-border bg-accent shadow-card">
68
68
  <Bot className="h-8 w-8 text-primary" />
69
69
  </div>
70
70
 
71
71
  <div className="text-center">
72
- <h2 className="text-2xl font-semibold text-gray-950 sm:text-3xl">
72
+ <h2 className="text-2xl font-semibold text-foreground sm:text-3xl">
73
73
  {t('chatWelcomeTitle')}
74
74
  </h2>
75
- <p className="mt-3 text-sm font-medium text-gray-500 sm:text-base">
75
+ <p className="mt-3 text-sm font-medium text-muted-foreground sm:text-base">
76
76
  {t('chatWelcomeSubtitle')}
77
77
  </p>
78
78
  </div>
79
79
 
80
80
  {inputSlot ? <div className="mt-8">{inputSlot}</div> : null}
81
81
 
82
- <div className="mt-3 flex flex-wrap items-center gap-2 px-1 text-sm text-gray-500">
82
+ <div className="mt-3 flex flex-wrap items-center gap-2 px-1 text-sm text-muted-foreground">
83
83
  {onSelectProjectRoot ? (
84
84
  <ChatWelcomeProjectPicker
85
85
  defaultProjectRoot={defaultProjectRoot}
@@ -18,10 +18,15 @@ function createSummary(
18
18
  }
19
19
 
20
20
  describe("buildChatWelcomeProjectOptions", () => {
21
- it("builds recent project options from session summaries", () => {
21
+ it("builds recent project options without treating the default workspace as a project", () => {
22
22
  const options = buildChatWelcomeProjectOptions({
23
23
  defaultProjectRoot: "/Users/demo/.nextclaw/workspace",
24
24
  sessionSummaries: [
25
+ createSummary({
26
+ sessionId: "session-default",
27
+ lastMessageAt: "2026-06-12T10:00:00.000Z",
28
+ metadata: { project_root: "/Users/demo/.nextclaw/workspace" },
29
+ }),
25
30
  createSummary({
26
31
  sessionId: "session-1",
27
32
  lastMessageAt: "2026-06-10T10:00:00.000Z",
@@ -36,17 +41,10 @@ describe("buildChatWelcomeProjectOptions", () => {
36
41
  });
37
42
 
38
43
  expect(options).toEqual([
39
- {
40
- projectRoot: "/Users/demo/.nextclaw/workspace",
41
- projectName: "workspace",
42
- sessionCount: 0,
43
- isDefault: true,
44
- },
45
44
  {
46
45
  projectRoot: "/tmp/project-alpha",
47
46
  projectName: "project-alpha",
48
47
  sessionCount: 2,
49
- isDefault: false,
50
48
  },
51
49
  ]);
52
50
  });
@@ -6,7 +6,6 @@ export type ChatWelcomeProjectOption = {
6
6
  projectRoot: string;
7
7
  projectName: string;
8
8
  sessionCount: number;
9
- isDefault: boolean;
10
9
  };
11
10
 
12
11
  function readSessionActivityAt(session: {
@@ -28,7 +27,7 @@ export function buildChatWelcomeProjectOptions(params: {
28
27
 
29
28
  for (const session of adaptNcpSessionSummaries([...sessionSummaries])) {
30
29
  const projectRoot = session.projectRoot?.trim();
31
- if (!projectRoot) {
30
+ if (!projectRoot || projectRoot === defaultProjectRoot) {
32
31
  continue;
33
32
  }
34
33
  const existing = groups.get(projectRoot);
@@ -45,26 +44,12 @@ export function buildChatWelcomeProjectOptions(params: {
45
44
  getSessionProjectName(projectRoot) ||
46
45
  projectRoot,
47
46
  sessionCount: 1,
48
- isDefault: projectRoot === defaultProjectRoot,
49
47
  latestUpdatedAt,
50
48
  });
51
49
  }
52
50
 
53
- if (defaultProjectRoot && !groups.has(defaultProjectRoot)) {
54
- groups.set(defaultProjectRoot, {
55
- projectRoot: defaultProjectRoot,
56
- projectName: getSessionProjectName(defaultProjectRoot) ?? defaultProjectRoot,
57
- sessionCount: 0,
58
- isDefault: true,
59
- latestUpdatedAt: Number.POSITIVE_INFINITY,
60
- });
61
- }
62
-
63
51
  return [...groups.values()]
64
52
  .sort((left, right) => {
65
- if (left.isDefault !== right.isDefault) {
66
- return left.isDefault ? -1 : 1;
67
- }
68
53
  return right.latestUpdatedAt - left.latestUpdatedAt;
69
54
  })
70
55
  .map(({ latestUpdatedAt: _latestUpdatedAt, ...option }) => option);
@@ -0,0 +1,54 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { expect, it, vi } from "vitest";
3
+ import { ChatSessionWorkspacePanelContent } from "@/features/chat/features/workspace/components/chat-session-workspace-panel-content";
4
+ import type { ResolvedChildSessionTab } from "@/features/chat/features/ncp/hooks/use-ncp-child-session-tabs-view";
5
+
6
+ vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
7
+ usePresenter: () => ({
8
+ chatThreadManager: {
9
+ materializeSideChatDraft: vi.fn(),
10
+ openFilePreview: vi.fn(),
11
+ },
12
+ }),
13
+ }));
14
+
15
+ vi.mock("@/features/chat/features/conversation/components/session-conversation-area", () => ({
16
+ SessionConversationArea: () => <div data-testid="session-conversation-area" />,
17
+ }));
18
+
19
+ function createChildTab(): ResolvedChildSessionTab {
20
+ return {
21
+ sessionKey: "child-1",
22
+ parentSessionKey: "parent-1",
23
+ title: "Child title",
24
+ agentId: "agent-1",
25
+ updatedAt: null,
26
+ lastMessageAt: null,
27
+ readAt: null,
28
+ runStatus: undefined,
29
+ sessionTypeLabel: "原生",
30
+ preferredModel: "minimax/MiniMax-M3",
31
+ projectName: "nextbot",
32
+ projectRoot: "/Users/peiwang/Projects/nextbot",
33
+ };
34
+ }
35
+
36
+ it("shows compact child session metadata without repeating the tab title", () => {
37
+ render(
38
+ <ChatSessionWorkspacePanelContent
39
+ activeSelection={{
40
+ kind: "child-session",
41
+ tab: createChildTab(),
42
+ }}
43
+ sessionCronJobs={[]}
44
+ sessionProjectRoot={null}
45
+ sessionWorkingDir={null}
46
+ />,
47
+ );
48
+
49
+ expect(screen.queryByText("Child title")).toBeNull();
50
+ expect(screen.getByText("原生")).toBeTruthy();
51
+ expect(screen.getByText("minimax/MiniMax-M3")).toBeTruthy();
52
+ expect(screen.getByText("nextbot")).toBeTruthy();
53
+ expect(screen.getByTitle("/Users/peiwang/Projects/nextbot")).toBeTruthy();
54
+ });