@nextclaw/ui 0.14.1-beta.1 → 0.14.2

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 (179) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/dist/assets/{api-dQ6uQSZG.js → api-a14tR_z1.js} +7 -7
  3. package/dist/assets/channels-list-page-CFGkm4DT.js +8 -0
  4. package/dist/assets/chat-page-CEdavJXc.js +105 -0
  5. package/dist/assets/{config-split-page-CeFfbaqc.js → config-split-page-B5b9B0uE.js} +1 -1
  6. package/dist/assets/{confirm-dialog-C95Y4QPC.js → confirm-dialog-ClH_Ec7n.js} +1 -1
  7. package/dist/assets/{desktop-update-config-CcK6jiEb.js → desktop-update-config-DPNDnVpk.js} +1 -1
  8. package/dist/assets/{dist-DTGzeil6.js → dist-BtCS2Ucq.js} +1 -1
  9. package/dist/assets/{dist-fGogBmPA.js → dist-CLGGLm8b.js} +1 -1
  10. package/dist/assets/doc-browser-5nMWYKBD.js +1 -0
  11. package/dist/assets/doc-browser-BJRLhztv.js +1 -0
  12. package/dist/assets/doc-browser-context-D4JwMMxK.js +1 -0
  13. package/dist/assets/{ellipsis-D2F4KMiU.js → ellipsis-CtJXjIol.js} +1 -1
  14. package/dist/assets/{external-link-CcgtBK26.js → external-link-CdJiG13p.js} +1 -1
  15. package/dist/assets/index-BDN1zzMQ.js +105 -0
  16. package/dist/assets/index-g3uSZXoc.css +1 -0
  17. package/dist/assets/mcp-marketplace-page-BU0kIjhR.js +40 -0
  18. package/dist/assets/mcp-marketplace-page-sMNFSq_T.js +1 -0
  19. package/dist/assets/middleware-CkFT3R4v.js +1 -0
  20. package/dist/assets/model-config-page-D6hYXROd.js +1 -0
  21. package/dist/assets/plus-DPjb0piO.js +1 -0
  22. package/dist/assets/provider-scoped-model-input-DAX2HgmR.js +1 -0
  23. package/dist/assets/providers-config-page-BzAeaACA.js +1 -0
  24. package/dist/assets/{react-CTqdhFMk.js → react-C6cZgP44.js} +1 -1
  25. package/dist/assets/remote-B0G80uPr.js +1 -0
  26. package/dist/assets/rotate-cw-DnEB1Rju.js +1 -0
  27. package/dist/assets/runtime-config-page-C5xKaV3i.js +1 -0
  28. package/dist/assets/{save-C9fKWfVB.js → save-DKA0x4zk.js} +1 -1
  29. package/dist/assets/{search-D5FQ5q0X.js → search-BNlcoj5c.js} +1 -1
  30. package/dist/assets/{search-config-page-AMEJJNIS.js → search-config-page-Cq9uJ-N8.js} +1 -1
  31. package/dist/assets/{secrets-config-page-CcgJnvBO.js → secrets-config-page-CxSbZLBo.js} +2 -2
  32. package/dist/assets/{select-Cu5n0E-3.js → select-CvTlDZTQ.js} +1 -1
  33. package/dist/assets/{tag-chip-C5aOtjxu.js → tag-chip-8ffF2y2C.js} +1 -1
  34. package/dist/assets/use-config-DmT0o1gv.js +1 -0
  35. package/dist/assets/x-BiSSpgqB.js +1 -0
  36. package/dist/index.html +15 -14
  37. package/package.json +9 -9
  38. package/src/app/components/app-presenter-provider.tsx +11 -2
  39. package/src/app/components/layout/__tests__/app-layout.test.tsx +11 -2
  40. package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +110 -79
  41. package/src/app/components/layout/app-layout.tsx +18 -4
  42. package/src/app/components/layout/sidebar-items.tsx +194 -71
  43. package/src/app/components/layout/sidebar-rail.styles.ts +15 -0
  44. package/src/app/components/layout/sidebar.tsx +340 -83
  45. package/src/app/managers/viewport-layout.manager.ts +10 -0
  46. package/src/app/stores/viewport-layout.store.ts +39 -3
  47. package/src/features/agents/components/__tests__/agents-page.test.tsx +62 -39
  48. package/src/features/agents/components/agents-page.tsx +10 -6
  49. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +145 -0
  50. package/src/features/chat/components/conversation/__tests__/chat-conversation-panel.test.tsx +69 -762
  51. package/src/features/chat/components/conversation/__tests__/chat-conversation-workspace-section.test.tsx +132 -0
  52. package/src/features/chat/components/conversation/chat-conversation-content.tsx +22 -15
  53. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +14 -15
  54. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +18 -18
  55. package/src/features/chat/components/conversation/chat-conversation-workspace-section.tsx +10 -4
  56. package/src/features/chat/components/layout/__tests__/chat-sidebar-read-state.test.tsx +52 -11
  57. package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +77 -12
  58. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +247 -0
  59. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +182 -21
  60. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +82 -25
  61. package/src/features/chat/components/layout/chat-sidebar.tsx +79 -93
  62. package/src/features/chat/components/providers/chat-presenter.provider.tsx +0 -6
  63. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +289 -0
  64. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +427 -0
  65. package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +204 -0
  66. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-attachments.ts +66 -0
  67. package/src/features/chat/features/{input/hooks/use-chat-input-bar-query-state.ts → conversation/hooks/use-session-conversation-input-query.ts} +32 -17
  68. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-state.ts +281 -0
  69. package/src/features/chat/features/conversation/utils/session-conversation-input-toolbar.utils.ts +158 -0
  70. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +56 -0
  71. package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +117 -0
  72. package/src/features/chat/features/input/hooks/use-chat-model-favorites.ts +2 -1
  73. package/src/features/chat/features/input/input-surface-plugins/__tests__/panel-app-reference-plugin.test.ts +77 -0
  74. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +108 -0
  75. package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +10 -0
  76. package/src/features/chat/features/input/input-surface-plugins/input-surface-search.utils.ts +108 -0
  77. package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +108 -0
  78. package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +161 -0
  79. package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +22 -1
  80. package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +26 -13
  81. package/src/features/chat/features/input/utils/__tests__/ncp-chat-input-availability.utils.test.ts +7 -28
  82. package/src/features/chat/features/input/utils/chat-composer-state.utils.ts +6 -0
  83. package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +26 -51
  84. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +2 -0
  85. package/src/features/chat/features/input/utils/ncp-chat-input-availability.utils.ts +5 -6
  86. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +47 -12
  87. package/src/features/chat/features/message/components/chat-message-list.container.tsx +93 -12
  88. package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +31 -12
  89. package/src/features/chat/features/message/utils/chat-message-markdown-part.utils.ts +38 -0
  90. package/src/features/chat/features/message/utils/chat-message-part.utils.ts +1 -1
  91. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +5 -4
  92. package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +2 -2
  93. package/src/features/chat/features/session/hooks/__tests__/use-chat-session-project.test.tsx +3 -30
  94. package/src/features/chat/features/session/hooks/use-chat-session-project.ts +0 -5
  95. package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +8 -0
  96. package/src/features/chat/features/session-type/components/__tests__/{chat-sidebar-create-menu.test.tsx → chat-session-type-menu.test.tsx} +5 -5
  97. package/src/features/chat/features/session-type/components/__tests__/chat-session-type-option-item.test.tsx +29 -1
  98. package/src/features/chat/features/session-type/components/{chat-sidebar-create-menu.tsx → chat-session-type-menu.tsx} +13 -9
  99. package/src/features/chat/features/session-type/components/chat-session-type-option-item.tsx +17 -5
  100. package/src/features/chat/features/session-type/hooks/__tests__/use-chat-new-session-type-preference.test.tsx +152 -0
  101. package/src/features/chat/features/session-type/hooks/use-chat-new-session-type-preference.ts +159 -0
  102. package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +49 -31
  103. package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +2 -2
  104. package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +21 -10
  105. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +2 -2
  106. package/src/features/chat/features/welcome/components/chat-welcome.tsx +2 -2
  107. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +54 -0
  108. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +44 -76
  109. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +5 -1
  110. package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +7 -1
  111. package/src/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state.ts +40 -31
  112. package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +59 -1
  113. package/src/features/chat/features/workspace/utils/chat-thread-workspace-session.utils.ts +109 -0
  114. package/src/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils.ts +44 -4
  115. package/src/features/chat/index.ts +0 -1
  116. package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +51 -99
  117. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +135 -21
  118. package/src/features/chat/managers/chat-session-list.manager.ts +12 -43
  119. package/src/features/chat/managers/chat-thread.manager.ts +108 -34
  120. package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +48 -1
  121. package/src/features/chat/pages/__tests__/ncp-chat-page.test.tsx +72 -0
  122. package/src/features/chat/pages/ncp-chat-page.tsx +0 -122
  123. package/src/features/chat/presenters/chat.presenter.ts +2 -19
  124. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +31 -0
  125. package/src/features/chat/stores/chat-thread.store.ts +42 -26
  126. package/src/features/marketplace/components/__tests__/marketplace-page.test.tsx +24 -0
  127. package/src/features/marketplace/components/curated-shelves/marketplace-shelf-card.tsx +8 -3
  128. package/src/features/marketplace/components/marketplace-item-list-view.tsx +2 -2
  129. package/src/features/marketplace/components/marketplace-list-card.tsx +124 -98
  130. package/src/features/marketplace/components/marketplace-page-parts.tsx +11 -6
  131. package/src/features/panel-apps/hooks/use-panel-apps.ts +2 -1
  132. package/src/platforms/desktop/components/__tests__/desktop-app-shell.test.tsx +50 -10
  133. package/src/platforms/desktop/components/desktop-app-shell.tsx +34 -10
  134. package/src/platforms/desktop/components/desktop-window-chrome.tsx +41 -16
  135. package/src/shared/components/common/__tests__/agent-avatar.test.tsx +3 -1
  136. package/src/shared/components/common/agent-avatar.tsx +5 -2
  137. package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +24 -1
  138. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +8 -4
  139. package/src/shared/lib/api/index.ts +1 -0
  140. package/src/shared/lib/api/preferences/preference-keys.config.ts +9 -0
  141. package/src/shared/lib/i18n/locales/en-US/chat.json +20 -2
  142. package/src/shared/lib/i18n/locales/en-US/core.json +2 -0
  143. package/src/shared/lib/i18n/locales/en-US/marketplace.json +2 -0
  144. package/src/shared/lib/i18n/locales/zh-CN/chat.json +20 -2
  145. package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -0
  146. package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +2 -0
  147. package/src/shared/lib/ui-document-title/__tests__/ui-document-title.test.ts +49 -0
  148. package/src/shared/lib/ui-document-title/index.ts +50 -21
  149. package/dist/assets/channels-list-page-DfpjB8XB.js +0 -8
  150. package/dist/assets/chat-page-D3QaEqjs.js +0 -105
  151. package/dist/assets/doc-browser-CuBTcBEZ.js +0 -1
  152. package/dist/assets/doc-browser-DVHhEb96.js +0 -1
  153. package/dist/assets/doc-browser-context-BfMXwnrz.js +0 -1
  154. package/dist/assets/index-ByJCbQqr.css +0 -1
  155. package/dist/assets/index-DjXqiloY.js +0 -103
  156. package/dist/assets/mcp-marketplace-page-98-V__3u.js +0 -1
  157. package/dist/assets/mcp-marketplace-page-BNq_PNmO.js +0 -40
  158. package/dist/assets/model-config-page-aAUkZkSX.js +0 -1
  159. package/dist/assets/plus-1Rxl5FlV.js +0 -1
  160. package/dist/assets/provider-scoped-model-input-we4_Z4gP.js +0 -1
  161. package/dist/assets/providers-config-page-CtMkPSyu.js +0 -1
  162. package/dist/assets/remote-fQ7N_r8u.js +0 -1
  163. package/dist/assets/rotate-cw-CzfIeAJj.js +0 -1
  164. package/dist/assets/runtime-config-page-TD1UrTbG.js +0 -1
  165. package/dist/assets/use-config-D8TESeo6.js +0 -1
  166. package/dist/assets/x-1qETdQ70.js +0 -1
  167. package/src/features/chat/components/conversation/chat-conversation-alerts.tsx +0 -37
  168. package/src/features/chat/features/input/components/__tests__/chat-attachment-upload-limit.test.ts +0 -38
  169. package/src/features/chat/features/input/components/chat-input-bar.container.tsx +0 -496
  170. package/src/features/chat/features/message/utils/chat-message-inline-content.utils.ts +0 -95
  171. package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +0 -448
  172. package/src/features/chat/managers/__tests__/chat-run-snapshot.manager.test.ts +0 -129
  173. package/src/features/chat/managers/__tests__/chat-run.manager.test.ts +0 -236
  174. package/src/features/chat/managers/__tests__/chat-session-preference-sync.manager.test.ts +0 -219
  175. package/src/features/chat/managers/chat-input.manager.ts +0 -430
  176. package/src/features/chat/managers/chat-run.manager.ts +0 -110
  177. package/src/features/chat/managers/chat-session-preference-sync.manager.ts +0 -142
  178. package/src/features/chat/stores/chat-input.store.ts +0 -104
  179. /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(
@@ -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
  }
@@ -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[];
@@ -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[];
@@ -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
+ });
@@ -1,15 +1,11 @@
1
- import { useRef } from "react";
2
- import { useStickyBottomScroll } from "@nextclaw/agent-chat-ui";
3
- import { FolderGit2, Loader2 } from "lucide-react";
1
+ import { MessageSquarePlus } from "lucide-react";
4
2
  import type { CronJobView } from "@/shared/lib/api";
5
- import { ChatMessageListContainer } from "@/features/chat/features/message/components/chat-message-list.container";
6
3
  import { usePresenter } from "@/features/chat/components/providers/chat-presenter.provider";
4
+ import { SessionConversationArea } from "@/features/chat/features/conversation/components/session-conversation-area";
7
5
  import { ChatSessionWorkspaceFilePreview } from "@/features/chat/features/workspace/components/chat-session-workspace-file-preview";
8
6
  import { SessionCronJobContent } from "@/features/chat/features/workspace/components/session-cron-job-content";
9
7
  import type { ResolvedChildSessionTab } from "@/features/chat/features/ncp/hooks/use-ncp-child-session-tabs-view";
10
- import { useNcpSessionConversation } from "@/features/chat/features/ncp/hooks/use-ncp-session-conversation";
11
8
  import type { WorkspaceSelection } from "@/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils";
12
- import { AgentIdentityAvatar } from "@/shared/components/common/agent-identity";
13
9
  import { t } from "@/shared/lib/i18n";
14
10
 
15
11
  type ChatSessionWorkspacePanelContentProps = {
@@ -19,53 +15,9 @@ type ChatSessionWorkspacePanelContentProps = {
19
15
  sessionWorkingDir: string | null;
20
16
  };
21
17
 
22
- function ChildSessionContent({ sessionKey }: { sessionKey: string }) {
23
- const agent = useNcpSessionConversation(sessionKey);
24
- const messages = agent.visibleMessages;
25
- const scrollRef = useRef<HTMLDivElement>(null);
26
- const { onScroll } = useStickyBottomScroll({
27
- scrollRef,
28
- resetKey: sessionKey,
29
- isLoading: agent.isHydrating,
30
- hasContent: messages.length > 0,
31
- contentVersion: messages[messages.length - 1] ?? null,
32
- stickyThresholdPx: 20,
33
- });
34
-
35
- return (
36
- <div
37
- ref={scrollRef}
38
- onScroll={onScroll}
39
- className="h-full overflow-y-auto custom-scrollbar"
40
- >
41
- {agent.isHydrating ? (
42
- <div className="flex h-full items-center justify-center text-sm text-gray-500">
43
- <Loader2 className="mr-2 h-4 w-4 animate-spin" />
44
- {t("chatChildSessionLoading")}
45
- </div>
46
- ) : agent.hydrateError ? (
47
- <div className="px-4 py-5 text-sm text-rose-600">
48
- {agent.hydrateError.message}
49
- </div>
50
- ) : messages.length === 0 && !agent.isRunning ? (
51
- <div className="px-4 py-5 text-sm text-gray-500">
52
- {t("chatChildSessionEmpty")}
53
- </div>
54
- ) : (
55
- <div className="px-4 py-5">
56
- <ChatMessageListContainer
57
- messages={messages}
58
- isSending={agent.isRunning}
59
- />
60
- </div>
61
- )}
62
- </div>
63
- );
64
- }
65
-
66
18
  function ChildSessionMetaChip({ value }: { value: string }) {
67
19
  return (
68
- <span className="inline-flex max-w-full items-center rounded border border-gray-200 bg-gray-50 px-2 py-0.5 text-[10px] font-medium text-gray-600">
20
+ <span className="inline-flex max-w-full shrink-0 items-center rounded border border-gray-200 bg-gray-50 px-1.5 py-0.5 text-[10px] font-medium text-gray-600">
69
21
  <span className="truncate">{value}</span>
70
22
  </span>
71
23
  );
@@ -83,41 +35,32 @@ function ChildSessionMetaStrip({ tab }: { tab: ResolvedChildSessionTab }) {
83
35
  }
84
36
 
85
37
  return (
86
- <div className="mt-3 space-y-2">
87
- {metaItems.length > 0 ? (
88
- <div className="flex flex-wrap gap-1.5">
89
- {metaItems.map((item) => (
90
- <ChildSessionMetaChip key={item} value={item} />
91
- ))}
92
- </div>
93
- ) : null}
38
+ <div className="flex min-h-9 min-w-0 items-center gap-1.5 border-b border-gray-200/70 px-3 py-1.5 text-[11px] text-gray-500">
39
+ {metaItems.map((item) => (
40
+ <ChildSessionMetaChip key={item} value={item} />
41
+ ))}
94
42
  {tab.projectRoot ? (
95
- <div
43
+ <span
96
44
  title={tab.projectRoot}
97
- className="truncate rounded border border-gray-200 bg-gray-50 px-2 py-1.5 font-mono text-[11px] text-gray-500"
45
+ className="min-w-0 flex-1 truncate rounded border border-gray-200 bg-gray-50 px-1.5 py-0.5 font-mono text-[10px] text-gray-500"
98
46
  >
99
47
  {tab.projectRoot}
100
- </div>
48
+ </span>
101
49
  ) : null}
102
50
  </div>
103
51
  );
104
52
  }
105
53
 
106
- function WorkspaceActiveChildHeader({ tab }: { tab: ResolvedChildSessionTab }) {
54
+ function WorkspaceSideChatDraftHeader() {
107
55
  return (
108
56
  <div className="border-b border-gray-200/70 px-4 py-3">
109
57
  <div className="flex min-w-0 items-center gap-2 text-sm font-semibold text-gray-900">
110
- {tab.agentId ? (
111
- <AgentIdentityAvatar
112
- agentId={tab.agentId}
113
- className="h-4 w-4 shrink-0"
114
- />
115
- ) : (
116
- <FolderGit2 className="h-4 w-4 shrink-0 text-gray-400" />
117
- )}
118
- <span className="truncate">{tab.title}</span>
58
+ <MessageSquarePlus className="h-4 w-4 shrink-0 text-primary" />
59
+ <span className="truncate">{t("chatWorkspaceSideChatDraftTitle")}</span>
119
60
  </div>
120
- <ChildSessionMetaStrip tab={tab} />
61
+ <p className="mt-1 text-xs leading-5 text-gray-500">
62
+ {t("chatWorkspaceSideChatDraftSubtitle")}
63
+ </p>
121
64
  </div>
122
65
  );
123
66
  }
@@ -130,12 +73,37 @@ export function ChatSessionWorkspacePanelContent({
130
73
  }: ChatSessionWorkspacePanelContentProps) {
131
74
  const presenter = usePresenter();
132
75
 
76
+ if (activeSelection.kind === "side-chat-draft") {
77
+ return (
78
+ <>
79
+ <WorkspaceSideChatDraftHeader />
80
+ <div className="flex min-h-0 flex-1 flex-col">
81
+ <SessionConversationArea
82
+ materializationContext={{
83
+ kind: "child",
84
+ parentSessionKey: activeSelection.draft.parentSessionKey,
85
+ inheritContext: true,
86
+ }}
87
+ sessionKey={null}
88
+ showWelcomeForDraft={false}
89
+ onSessionMaterialized={(sessionKey) =>
90
+ presenter.chatThreadManager.materializeSideChatDraft({
91
+ draftKey: activeSelection.draft.draftKey,
92
+ sessionKey,
93
+ })
94
+ }
95
+ />
96
+ </div>
97
+ </>
98
+ );
99
+ }
100
+
133
101
  if (activeSelection.kind === "child-session") {
134
102
  return (
135
103
  <>
136
- <WorkspaceActiveChildHeader tab={activeSelection.tab} />
137
- <div className="flex-1 min-h-0">
138
- <ChildSessionContent sessionKey={activeSelection.tab.sessionKey} />
104
+ <ChildSessionMetaStrip tab={activeSelection.tab} />
105
+ <div className="flex min-h-0 flex-1 flex-col">
106
+ <SessionConversationArea sessionKey={activeSelection.tab.sessionKey} />
139
107
  </div>
140
108
  </>
141
109
  );
@@ -1,4 +1,4 @@
1
- import { AlarmClock, ArrowLeft, ArrowRight, FileCode2, MessageSquareText, X } from "lucide-react";
1
+ import { AlarmClock, ArrowLeft, ArrowRight, FileCode2, MessageSquarePlus, MessageSquareText, X } from "lucide-react";
2
2
  import type { WorkspaceTabViewModel } from "@/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils";
3
3
  import { AgentIdentityAvatar } from "@/shared/components/common/agent-identity";
4
4
  import {
@@ -17,6 +17,10 @@ function WorkspaceTabIcon({ agentId, kind }: Pick<WorkspaceTabViewModel, "agentI
17
17
  return <FileCode2 className="h-3.5 w-3.5 shrink-0 text-gray-400" />;
18
18
  }
19
19
 
20
+ if (kind === "side-chat-draft") {
21
+ return <MessageSquarePlus className="h-3.5 w-3.5 shrink-0 text-primary" />;
22
+ }
23
+
20
24
  if (agentId) {
21
25
  return (
22
26
  <AgentIdentityAvatar agentId={agentId} className="h-3.5 w-3.5 shrink-0" />
@@ -4,6 +4,7 @@ import { useNcpChildSessionTabsView } from "@/features/chat/features/ncp/hooks/u
4
4
  import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
5
5
  import type {
6
6
  ChatChildSessionTab,
7
+ ChatWorkspaceSideChatDraft,
7
8
  ChatWorkspaceNavigationEntry,
8
9
  ChatWorkspaceFileTab,
9
10
  } from "@/features/chat/stores/chat-thread.store";
@@ -26,11 +27,12 @@ type ChatSessionWorkspacePanelProps = {
26
27
  sessionKey: string | null;
27
28
  childSessionTabs: readonly ChatChildSessionTab[];
28
29
  activeChildSessionKey: string | null;
30
+ activeSideChatDraft: ChatWorkspaceSideChatDraft | null;
29
31
  workspaceFileTabs: readonly ChatWorkspaceFileTab[];
30
32
  activeWorkspaceFileKey: string | null;
31
33
  workspaceNavigationHistory?: readonly ChatWorkspaceNavigationEntry[];
32
34
  workspaceNavigationHistoryIndex?: number;
33
- activePanelKind?: "child-session" | "file" | "cron" | null;
35
+ activePanelKind?: "child-session" | "side-chat-draft" | "file" | "cron" | null;
34
36
  sessionCronJobs?: readonly CronJobView[];
35
37
  sessionProjectRoot: string | null;
36
38
  sessionWorkingDir: string | null;
@@ -41,6 +43,7 @@ export function ChatSessionWorkspacePanel({
41
43
  sessionKey,
42
44
  childSessionTabs,
43
45
  activeChildSessionKey,
46
+ activeSideChatDraft,
44
47
  workspaceFileTabs,
45
48
  activeWorkspaceFileKey,
46
49
  workspaceNavigationHistory = [],
@@ -58,6 +61,7 @@ export function ChatSessionWorkspacePanel({
58
61
  );
59
62
  const activeSelection = resolveWorkspaceSelection({
60
63
  activeChildSessionKey,
64
+ activeSideChatDraft,
61
65
  activeWorkspaceFileKey,
62
66
  activePanelKind,
63
67
  childSessionTabs: resolvedChildTabs,
@@ -77,6 +81,7 @@ export function ChatSessionWorkspacePanel({
77
81
  () =>
78
82
  buildWorkspaceTabsViewModel({
79
83
  resolvedChildTabs,
84
+ activeSideChatDraft,
80
85
  workspaceFileTabs,
81
86
  sessionCronJobCount: sessionCronJobs.length,
82
87
  activeSelection,
@@ -91,6 +96,7 @@ export function ChatSessionWorkspacePanel({
91
96
  }),
92
97
  [
93
98
  activeSelection,
99
+ activeSideChatDraft,
94
100
  optimisticReadAtBySessionKey,
95
101
  presenter.chatThreadManager,
96
102
  resolvedChildTabs,