@nextclaw/ui 0.14.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 (165) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/assets/{api-DPcvCLfa.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-BCJhqdK9.js → config-split-page-B5b9B0uE.js} +1 -1
  6. package/dist/assets/{confirm-dialog-CWh5FfK2.js → confirm-dialog-ClH_Ec7n.js} +1 -1
  7. package/dist/assets/{desktop-update-config-DsRhBizi.js → desktop-update-config-DPNDnVpk.js} +1 -1
  8. package/dist/assets/{dist-DPrgcEC0.js → dist-BtCS2Ucq.js} +1 -1
  9. package/dist/assets/{dist-DsE4OsSS.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-B81FStLm.js → ellipsis-CtJXjIol.js} +1 -1
  14. package/dist/assets/{external-link-C_dmG-WJ.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-sZz5HSCm.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-MzjoiHri.js → save-DKA0x4zk.js} +1 -1
  29. package/dist/assets/{search-B29dE4GN.js → search-BNlcoj5c.js} +1 -1
  30. package/dist/assets/{search-config-page-Dl5sZzob.js → search-config-page-Cq9uJ-N8.js} +1 -1
  31. package/dist/assets/{secrets-config-page-C_l-IFCK.js → secrets-config-page-CxSbZLBo.js} +2 -2
  32. package/dist/assets/{select-BKmh05hd.js → select-CvTlDZTQ.js} +1 -1
  33. package/dist/assets/{tag-chip-BS_7Ueom.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 +16 -14
  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 +80 -7
  60. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +82 -25
  61. package/src/features/chat/components/layout/chat-sidebar.tsx +59 -92
  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/input-surface-plugins/__tests__/panel-app-reference-plugin.test.ts +77 -0
  73. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +108 -0
  74. package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +10 -0
  75. package/src/features/chat/features/input/input-surface-plugins/input-surface-search.utils.ts +108 -0
  76. package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +108 -0
  77. package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +161 -0
  78. package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +22 -1
  79. package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +26 -13
  80. package/src/features/chat/features/input/utils/__tests__/ncp-chat-input-availability.utils.test.ts +7 -28
  81. package/src/features/chat/features/input/utils/chat-composer-state.utils.ts +6 -0
  82. package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +26 -51
  83. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +2 -0
  84. package/src/features/chat/features/input/utils/ncp-chat-input-availability.utils.ts +5 -6
  85. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +47 -12
  86. package/src/features/chat/features/message/components/chat-message-list.container.tsx +93 -12
  87. package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +31 -12
  88. package/src/features/chat/features/message/utils/chat-message-markdown-part.utils.ts +38 -0
  89. package/src/features/chat/features/message/utils/chat-message-part.utils.ts +1 -1
  90. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +2 -2
  91. package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +2 -2
  92. package/src/features/chat/features/session/hooks/__tests__/use-chat-session-project.test.tsx +3 -30
  93. package/src/features/chat/features/session/hooks/use-chat-session-project.ts +0 -5
  94. package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +8 -0
  95. package/src/features/chat/features/session-type/components/__tests__/chat-session-type-menu.test.tsx +2 -2
  96. package/src/features/chat/features/session-type/components/__tests__/chat-session-type-option-item.test.tsx +2 -0
  97. package/src/features/chat/features/session-type/components/chat-session-type-menu.tsx +2 -2
  98. package/src/features/chat/features/session-type/components/chat-session-type-option-item.tsx +2 -2
  99. package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +49 -31
  100. package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +2 -2
  101. package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +21 -10
  102. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +2 -2
  103. package/src/features/chat/features/welcome/components/chat-welcome.tsx +2 -2
  104. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +54 -0
  105. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +44 -76
  106. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +5 -1
  107. package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +7 -1
  108. package/src/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state.ts +40 -31
  109. package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +59 -1
  110. package/src/features/chat/features/workspace/utils/chat-thread-workspace-session.utils.ts +109 -0
  111. package/src/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils.ts +44 -4
  112. package/src/features/chat/index.ts +0 -1
  113. package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +51 -99
  114. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +135 -21
  115. package/src/features/chat/managers/chat-session-list.manager.ts +12 -43
  116. package/src/features/chat/managers/chat-thread.manager.ts +108 -34
  117. package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +48 -1
  118. package/src/features/chat/pages/__tests__/ncp-chat-page.test.tsx +72 -0
  119. package/src/features/chat/pages/ncp-chat-page.tsx +0 -122
  120. package/src/features/chat/presenters/chat.presenter.ts +2 -19
  121. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +31 -0
  122. package/src/features/chat/stores/chat-thread.store.ts +42 -26
  123. package/src/features/panel-apps/hooks/use-panel-apps.ts +2 -1
  124. package/src/platforms/desktop/components/__tests__/desktop-app-shell.test.tsx +50 -10
  125. package/src/platforms/desktop/components/desktop-app-shell.tsx +34 -10
  126. package/src/platforms/desktop/components/desktop-window-chrome.tsx +41 -16
  127. package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +24 -1
  128. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +8 -4
  129. package/src/shared/lib/i18n/locales/en-US/chat.json +20 -2
  130. package/src/shared/lib/i18n/locales/en-US/core.json +2 -0
  131. package/src/shared/lib/i18n/locales/zh-CN/chat.json +20 -2
  132. package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -0
  133. package/src/shared/lib/ui-document-title/__tests__/ui-document-title.test.ts +49 -0
  134. package/src/shared/lib/ui-document-title/index.ts +50 -21
  135. package/dist/assets/channels-list-page-D6jtziu4.js +0 -8
  136. package/dist/assets/chat-page-BBnNJoKG.js +0 -105
  137. package/dist/assets/doc-browser-C8xOF_9F.js +0 -1
  138. package/dist/assets/doc-browser-DgQedeAn.js +0 -1
  139. package/dist/assets/doc-browser-context-B46tQGmO.js +0 -1
  140. package/dist/assets/index-7o1WdUbU.js +0 -103
  141. package/dist/assets/index-DQWmEWzk.css +0 -1
  142. package/dist/assets/mcp-marketplace-page-CWZN7ohK.js +0 -1
  143. package/dist/assets/mcp-marketplace-page-Dzull_4-.js +0 -40
  144. package/dist/assets/model-config-page-Cv0O3BIj.js +0 -1
  145. package/dist/assets/plus-Bnt56oqG.js +0 -1
  146. package/dist/assets/provider-scoped-model-input-Duuh7i3h.js +0 -1
  147. package/dist/assets/providers-config-page-CPDz0Lxc.js +0 -1
  148. package/dist/assets/remote-B0LMs8u7.js +0 -1
  149. package/dist/assets/rotate-cw-BfyG_KEF.js +0 -1
  150. package/dist/assets/runtime-config-page-q0fvv8ME.js +0 -1
  151. package/dist/assets/use-config-DZkBWIdL.js +0 -1
  152. package/dist/assets/x-CW2KO-Hz.js +0 -1
  153. package/src/features/chat/components/conversation/chat-conversation-alerts.tsx +0 -37
  154. package/src/features/chat/features/input/components/__tests__/chat-attachment-upload-limit.test.ts +0 -38
  155. package/src/features/chat/features/input/components/chat-input-bar.container.tsx +0 -496
  156. package/src/features/chat/features/message/utils/chat-message-inline-content.utils.ts +0 -95
  157. package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +0 -448
  158. package/src/features/chat/managers/__tests__/chat-run-snapshot.manager.test.ts +0 -129
  159. package/src/features/chat/managers/__tests__/chat-run.manager.test.ts +0 -236
  160. package/src/features/chat/managers/__tests__/chat-session-preference-sync.manager.test.ts +0 -219
  161. package/src/features/chat/managers/chat-input.manager.ts +0 -430
  162. package/src/features/chat/managers/chat-run.manager.ts +0 -110
  163. package/src/features/chat/managers/chat-session-preference-sync.manager.ts +0 -142
  164. package/src/features/chat/stores/chat-input.store.ts +0 -104
  165. /package/dist/assets/{config-hints-CTqBnCDp.js → config-hints-BkYBc7z0.js} +0 -0
@@ -1,7 +1,6 @@
1
1
  import { act, renderHook } from '@testing-library/react';
2
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { afterEach, describe, expect, it, vi } from 'vitest';
3
3
  import { toast } from 'sonner';
4
- import { useChatInputStore } from '@/features/chat/stores/chat-input.store';
5
4
  import { useChatSessionProject } from '@/features/chat/features/session/hooks/use-chat-session-project';
6
5
 
7
6
  const mocks = vi.hoisted(() => ({
@@ -19,21 +18,11 @@ vi.mock('@/features/chat/features/session/hooks/use-chat-session-update', () =>
19
18
  }));
20
19
 
21
20
  describe('useChatSessionProject', () => {
22
- beforeEach(() => {
23
- useChatInputStore.setState((state) => ({
24
- snapshot: {
25
- ...state.snapshot,
26
- pendingProjectRoot: null,
27
- pendingProjectRootSessionKey: null
28
- }
29
- }));
30
- });
31
-
32
21
  afterEach(() => {
33
22
  vi.clearAllMocks();
34
23
  });
35
24
 
36
- it('stores the draft project root locally when the session does not exist yet', async () => {
25
+ it('does not persist draft project root through the session update hook', async () => {
37
26
  const { result } = renderHook(() => useChatSessionProject());
38
27
 
39
28
  await act(async () => {
@@ -45,14 +34,10 @@ describe('useChatSessionProject', () => {
45
34
  });
46
35
 
47
36
  expect(mocks.updateSession).not.toHaveBeenCalled();
48
- expect(useChatInputStore.getState().snapshot).toMatchObject({
49
- pendingProjectRoot: '/tmp/project-alpha',
50
- pendingProjectRootSessionKey: 'draft-session-1'
51
- });
52
37
  expect(toast.success).toHaveBeenCalledTimes(1);
53
38
  });
54
39
 
55
- it('keeps an explicit draft override when clearing the project root locally', async () => {
40
+ it('does not persist draft project clearing through the session update hook', async () => {
56
41
  const { result } = renderHook(() => useChatSessionProject());
57
42
 
58
43
  await act(async () => {
@@ -64,10 +49,6 @@ describe('useChatSessionProject', () => {
64
49
  });
65
50
 
66
51
  expect(mocks.updateSession).not.toHaveBeenCalled();
67
- expect(useChatInputStore.getState().snapshot).toMatchObject({
68
- pendingProjectRoot: null,
69
- pendingProjectRootSessionKey: 'draft-session-1'
70
- });
71
52
  expect(toast.success).toHaveBeenCalledTimes(1);
72
53
  });
73
54
 
@@ -87,10 +68,6 @@ describe('useChatSessionProject', () => {
87
68
  patch: { projectRoot: '/tmp/project-beta' },
88
69
  successMessage: 'Project directory updated'
89
70
  });
90
- expect(useChatInputStore.getState().snapshot).toMatchObject({
91
- pendingProjectRoot: null,
92
- pendingProjectRootSessionKey: null
93
- });
94
71
  });
95
72
 
96
73
  it('persists clearing to the server without keeping a session-scoped local override', async () => {
@@ -109,9 +86,5 @@ describe('useChatSessionProject', () => {
109
86
  patch: { projectRoot: null },
110
87
  successMessage: 'Project directory cleared'
111
88
  });
112
- expect(useChatInputStore.getState().snapshot).toMatchObject({
113
- pendingProjectRoot: null,
114
- pendingProjectRootSessionKey: null
115
- });
116
89
  });
117
90
  });
@@ -1,6 +1,5 @@
1
1
  import { toast } from 'sonner';
2
2
  import { t } from '@/shared/lib/i18n';
3
- import { useChatInputStore } from '@/features/chat/stores/chat-input.store';
4
3
  import { useChatSessionUpdate } from '@/features/chat/features/session/hooks/use-chat-session-update';
5
4
 
6
5
  type UpdateChatSessionProjectParams = {
@@ -17,10 +16,6 @@ export function useChatSessionProject() {
17
16
  const successMessage = projectRoot ? t('chatSessionProjectUpdated') : t('chatSessionProjectCleared');
18
17
 
19
18
  if (!persistToServer) {
20
- useChatInputStore.getState().setSnapshot({
21
- pendingProjectRoot: projectRoot,
22
- pendingProjectRootSessionKey: sessionKey
23
- });
24
19
  toast.success(successMessage);
25
20
  return;
26
21
  }
@@ -1,3 +1,7 @@
1
+ import {
2
+ CHAT_SESSION_MATERIALIZATION_METADATA_KEY,
3
+ type AgentRunSessionMaterializationMetadata,
4
+ } from "@nextclaw/shared";
1
5
  import { buildInlineSkillTokensFromComposer, CHAT_UI_INLINE_TOKENS_METADATA_KEY } from "@/features/chat/features/input/utils/chat-inline-token.utils";
2
6
  import { normalizeRequestedSkills } from "@/features/chat/features/runtime/utils/chat-runtime.utils";
3
7
  import { normalizeSessionProjectRootValue } from "@/shared/lib/session-project";
@@ -17,6 +21,7 @@ export function buildChatRunMetadata(payload: {
17
21
  projectRoot?: string | null;
18
22
  requestedSkills?: string[];
19
23
  composerNodes?: Parameters<typeof buildInlineSkillTokensFromComposer>[0];
24
+ sessionMaterialization?: AgentRunSessionMaterializationMetadata | null;
20
25
  }): Record<string, unknown> {
21
26
  const projectRoot = normalizeSessionProjectRootValue(payload.projectRoot);
22
27
  const metadata: Record<string, unknown> = {
@@ -36,6 +41,9 @@ export function buildChatRunMetadata(payload: {
36
41
  if (inlineSkillTokens.length > 0) {
37
42
  metadata[CHAT_UI_INLINE_TOKENS_METADATA_KEY] = inlineSkillTokens;
38
43
  }
44
+ if (payload.sessionMaterialization) {
45
+ metadata[CHAT_SESSION_MATERIALIZATION_METADATA_KEY] = payload.sessionMaterialization;
46
+ }
39
47
  return metadata;
40
48
  }
41
49
 
@@ -4,8 +4,8 @@ import { ChatSessionTypeMenu } from "@/features/chat/features/session-type/compo
4
4
  import { createPopoverAvailableHeightLimit } from "@/shared/components/ui/popover";
5
5
 
6
6
  const options = [
7
- { value: "native", label: "Native", ready: true },
8
- { value: "codex", label: "Codex", ready: true },
7
+ { value: "native", label: "Native", icon: null, ready: true },
8
+ { value: "codex", label: "Codex", icon: null, ready: true },
9
9
  ];
10
10
 
11
11
  describe("ChatSessionTypeMenu", () => {
@@ -50,6 +50,7 @@ describe("ChatSessionTypeOptionItem", () => {
50
50
  option={{
51
51
  value: "native",
52
52
  label: "Native",
53
+ icon: null,
53
54
  ready: true,
54
55
  }}
55
56
  onSelect={vi.fn()}
@@ -70,6 +71,7 @@ describe("ChatSessionTypeOptionItem", () => {
70
71
  option={{
71
72
  value: "claude",
72
73
  label: "Claude",
74
+ icon: null,
73
75
  ready: false,
74
76
  reasonMessage: "Configure a provider API key first.",
75
77
  }}
@@ -1,10 +1,10 @@
1
1
  import { ChatSessionTypeOptionItem } from "@/features/chat/features/session-type/components/chat-session-type-option-item";
2
- import type { ChatInputSnapshot } from "@/features/chat/stores/chat-input.store";
2
+ import type { ChatSessionTypeOption } from "@/features/chat/features/session-type/utils/chat-session-type.utils";
3
3
  import { createPopoverAvailableHeightLimit } from "@/shared/components/ui/popover";
4
4
  import { t } from "@/shared/lib/i18n";
5
5
  import { cn } from "@/shared/lib/utils";
6
6
 
7
- type SessionTypeOption = ChatInputSnapshot["sessionTypeOptions"][number];
7
+ type SessionTypeOption = ChatSessionTypeOption;
8
8
  const CHAT_SESSION_TYPE_MENU_MAX_HEIGHT =
9
9
  createPopoverAvailableHeightLimit("18rem");
10
10
  const CHAT_SESSION_TYPE_MENU_STYLE = {
@@ -1,11 +1,11 @@
1
- import type { ChatInputSnapshot } from "@/features/chat/stores/chat-input.store";
2
1
  import { SessionContextIconNode } from "@/features/chat/features/session/components/session-context-icon";
2
+ import type { ChatSessionTypeOption } from "@/features/chat/features/session-type/utils/chat-session-type.utils";
3
3
  import { StatusDot } from "@/shared/components/status/status-dot";
4
4
  import { t } from "@/shared/lib/i18n";
5
5
  import { cn } from "@/shared/lib/utils";
6
6
  import { Bot, Check } from "lucide-react";
7
7
 
8
- type SessionTypeOption = ChatInputSnapshot["sessionTypeOptions"][number];
8
+ type SessionTypeOption = ChatSessionTypeOption;
9
9
 
10
10
  export function ChatSessionTypeOptionItem(props: {
11
11
  option: SessionTypeOption;
@@ -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
+ });