@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
@@ -0,0 +1,132 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import type { PersistStorage, StorageValue } from "zustand/middleware";
4
+ import { ChatConversationWorkspaceSection } from "@/features/chat/components/conversation/chat-conversation-workspace-section";
5
+ import { useChatQueryStore } from "@/features/chat/stores/ncp-chat-query.store";
6
+ import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
7
+ import type { NcpSessionSummaryView } from "@/shared/lib/api";
8
+
9
+ const mocks = vi.hoisted(() => ({
10
+ panelProps: vi.fn(),
11
+ }));
12
+
13
+ vi.mock("@/features/chat/features/workspace/components/chat-session-workspace-panel", () => ({
14
+ ChatSessionWorkspacePanel: ({
15
+ sessionProjectRoot,
16
+ sessionWorkingDir,
17
+ }: {
18
+ sessionProjectRoot: string | null;
19
+ sessionWorkingDir: string | null;
20
+ }) => {
21
+ mocks.panelProps({ sessionProjectRoot, sessionWorkingDir });
22
+ return (
23
+ <div
24
+ data-testid="workspace-panel"
25
+ data-project-root={sessionProjectRoot ?? ""}
26
+ data-working-dir={sessionWorkingDir ?? ""}
27
+ />
28
+ );
29
+ },
30
+ }));
31
+
32
+ vi.mock("@/features/cron", () => ({
33
+ useCronJobs: () => ({ data: { jobs: [] } }),
34
+ }));
35
+
36
+ class MemoryPersistStorage implements PersistStorage<unknown> {
37
+ private readonly values = new Map<string, StorageValue<unknown>>();
38
+
39
+ getItem = (name: string) => this.values.get(name) ?? null;
40
+
41
+ setItem = (name: string, value: StorageValue<unknown>) => {
42
+ this.values.set(name, value);
43
+ };
44
+
45
+ removeItem = (name: string) => {
46
+ this.values.delete(name);
47
+ };
48
+ }
49
+
50
+ function createSummary(
51
+ overrides: Partial<NcpSessionSummaryView> & Pick<NcpSessionSummaryView, "sessionId">,
52
+ ): NcpSessionSummaryView {
53
+ return {
54
+ messageCount: 1,
55
+ status: "idle",
56
+ updatedAt: "2026-06-19T00:00:00.000Z",
57
+ ...overrides,
58
+ };
59
+ }
60
+
61
+ describe("ChatConversationWorkspaceSection", () => {
62
+ beforeEach(() => {
63
+ mocks.panelProps.mockReset();
64
+ useChatThreadStore.persist.setOptions({
65
+ storage: new MemoryPersistStorage(),
66
+ });
67
+ useChatThreadStore.setState({
68
+ snapshot: {
69
+ ...useChatThreadStore.getState().snapshot,
70
+ workspacePanelParentKey: "session-1",
71
+ activeWorkspacePanelKind: "file",
72
+ workspaceFileTabs: [
73
+ {
74
+ key: "file-tab",
75
+ parentSessionKey: "session-1",
76
+ path: "docs/designs/2026-06-10-core-kernel-responsibility.design.md",
77
+ label: "core-kernel-responsibility",
78
+ viewMode: "preview",
79
+ },
80
+ ],
81
+ activeWorkspaceFileKey: "file-tab",
82
+ childSessionTabs: [],
83
+ activeChildSessionKey: null,
84
+ workspaceNavigationHistory: [],
85
+ workspaceNavigationHistoryIndex: 0,
86
+ sessionProjectRoot: null,
87
+ sessionWorkingDir: null,
88
+ },
89
+ });
90
+ useChatQueryStore.setState({
91
+ snapshot: {
92
+ sessionsQuery: {
93
+ data: {
94
+ sessions: [
95
+ createSummary({
96
+ sessionId: "session-1",
97
+ workingDir: "/Users/peiwang/Projects/nextbot",
98
+ metadata: {
99
+ project_root: "/Users/peiwang/Projects/nextbot",
100
+ },
101
+ }),
102
+ ],
103
+ total: 1,
104
+ },
105
+ } as never,
106
+ },
107
+ });
108
+ });
109
+
110
+ it("uses selected session workingDir as the workspace file preview base path", () => {
111
+ render(
112
+ <ChatConversationWorkspaceSection
113
+ layoutMode="desktop"
114
+ sessionKey="session-1"
115
+ />,
116
+ );
117
+
118
+ const panel = screen.getByTestId("workspace-panel");
119
+ expect(panel.getAttribute("data-project-root")).toBe(
120
+ "/Users/peiwang/Projects/nextbot",
121
+ );
122
+ expect(panel.getAttribute("data-working-dir")).toBe(
123
+ "/Users/peiwang/Projects/nextbot",
124
+ );
125
+ expect(mocks.panelProps).toHaveBeenCalledWith(
126
+ expect.objectContaining({
127
+ sessionProjectRoot: "/Users/peiwang/Projects/nextbot",
128
+ sessionWorkingDir: "/Users/peiwang/Projects/nextbot",
129
+ }),
130
+ );
131
+ });
132
+ });
@@ -1,34 +1,41 @@
1
1
  import { useRef, type ReactNode } from "react";
2
2
  import { useStickyBottomScroll } from "@nextclaw/agent-chat-ui";
3
+ import type { NcpMessage } from "@nextclaw/ncp";
3
4
  import { ChatMessageListContainer } from "@/features/chat/features/message/components/chat-message-list.container";
4
- import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
5
5
 
6
6
  type ChatConversationContentProps = {
7
+ isAwaitingAssistantOutput: boolean;
8
+ isHistoryLoading: boolean;
9
+ isSending: boolean;
10
+ messages: readonly NcpMessage[];
11
+ sessionKey: string | null;
7
12
  showWelcome: boolean;
8
13
  welcomeSlot?: ReactNode;
9
14
  };
10
15
 
11
16
  export function ChatConversationContent({
17
+ isAwaitingAssistantOutput,
18
+ isHistoryLoading,
19
+ isSending,
20
+ messages,
21
+ sessionKey,
12
22
  showWelcome,
13
23
  welcomeSlot,
14
24
  }: ChatConversationContentProps) {
15
- const snapshot = useChatThreadStore((state) => state.snapshot);
16
25
  const threadRef = useRef<HTMLDivElement | null>(null);
17
26
  const hideEmptyHint =
18
- snapshot.isHistoryLoading &&
19
- snapshot.messages.length === 0 &&
20
- !snapshot.isSending &&
21
- !snapshot.isAwaitingAssistantOutput;
27
+ isHistoryLoading &&
28
+ messages.length === 0 &&
29
+ !isSending &&
30
+ !isAwaitingAssistantOutput;
22
31
  const { onScroll } = useStickyBottomScroll({
23
32
  scrollRef: threadRef,
24
- resetKey: snapshot.sessionKey,
25
- isLoading: snapshot.isHistoryLoading,
26
- hasContent: snapshot.messages.length > 0,
27
- contentVersion: snapshot.messages[snapshot.messages.length - 1] ?? null,
33
+ resetKey: sessionKey,
34
+ isLoading: isHistoryLoading,
35
+ hasContent: messages.length > 0,
36
+ contentVersion: messages[messages.length - 1] ?? null,
28
37
  });
29
- const hasMessages = snapshot.messages.length > 0;
30
- const isAwaitingAssistantOutput =
31
- hasMessages && snapshot.isSending && snapshot.isAwaitingAssistantOutput;
38
+ const hasMessages = messages.length > 0;
32
39
 
33
40
  return (
34
41
  <div
@@ -42,8 +49,8 @@ export function ChatConversationContent({
42
49
  ) : hideEmptyHint || !hasMessages ? null : (
43
50
  <div className="mx-auto w-full max-w-[min(1120px,100%)] px-4 py-4 sm:px-6 sm:py-5">
44
51
  <ChatMessageListContainer
45
- messages={snapshot.messages}
46
- isSending={isAwaitingAssistantOutput}
52
+ messages={messages}
53
+ isSending={hasMessages && isSending && isAwaitingAssistantOutput}
47
54
  />
48
55
  </div>
49
56
  )}
@@ -9,10 +9,10 @@ import { useNcpChatSelectedSession } from "@/features/chat/features/ncp/hooks/us
9
9
  import { sessionDisplayName } from "@/features/chat/features/session/utils/chat-session-display.utils";
10
10
  import {
11
11
  buildSessionTypeOptions,
12
+ DEFAULT_SESSION_TYPE,
12
13
  normalizeSessionType,
13
14
  resolveSessionTypeLabel,
14
15
  } from "@/features/chat/features/session-type/utils/chat-session-type.utils";
15
- import { useChatInputStore } from "@/features/chat/stores/chat-input.store";
16
16
  import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
17
17
  import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
18
18
  import { useChatQueryStore } from "@/features/chat/stores/ncp-chat-query.store";
@@ -34,9 +34,8 @@ export function ChatConversationHeaderSection({
34
34
  const sessionKey = useChatSessionListStore(
35
35
  (state) => state.snapshot.selectedSessionKey,
36
36
  );
37
- const effectiveSessionKey = sessionKey ?? snapshot.sessionKey;
38
- const pendingSessionType = useChatInputStore(
39
- (state) => state.snapshot.pendingSessionType,
37
+ const selectedAgentId = useChatSessionListStore(
38
+ (state) => state.snapshot.selectedAgentId,
40
39
  );
41
40
  const sessionTypesData = useChatQueryStore(
42
41
  (state) => state.snapshot.sessionTypesQuery?.data ?? null,
@@ -47,7 +46,7 @@ export function ChatConversationHeaderSection({
47
46
  [sessionTypesData?.options],
48
47
  );
49
48
  const sessionType = normalizeSessionType(
50
- selectedSession?.sessionType ?? pendingSessionType,
49
+ selectedSession?.sessionType ?? sessionTypesData?.defaultType ?? DEFAULT_SESSION_TYPE,
51
50
  );
52
51
  const sessionTypeOption =
53
52
  sessionTypeOptions.find((option) => option.value === sessionType) ?? null;
@@ -58,8 +57,8 @@ export function ChatConversationHeaderSection({
58
57
  selectedSession?.projectName ?? getSessionProjectName(sessionProjectRoot);
59
58
  const canDeleteSession = Boolean(selectedSession);
60
59
  const { childSessionTabs, sessionCronJobs } =
61
- useChatConversationWorkspaceState(snapshot);
62
- const shouldShowSessionHeader = Boolean(effectiveSessionKey || sessionTypeLabel);
60
+ useChatConversationWorkspaceState(snapshot, sessionKey);
61
+ const shouldShowSessionHeader = Boolean(sessionKey || sessionTypeLabel);
63
62
  const sessionHeaderTitle =
64
63
  (selectedSession ? sessionDisplayName(selectedSession) : undefined) ||
65
64
  (canDeleteSession && sessionKey
@@ -67,23 +66,23 @@ export function ChatConversationHeaderSection({
67
66
  : null) ||
68
67
  t("chatSidebarNewTask");
69
68
  const normalizedAgentId =
70
- selectedSession?.agentId?.trim() ?? snapshot.agentId?.trim() ?? "";
69
+ selectedSession?.agentId?.trim() ?? selectedAgentId?.trim() ?? "";
71
70
  const shouldShowHeaderAgentAvatar =
72
71
  normalizedAgentId.length > 0 && normalizedAgentId.toLowerCase() !== "main";
73
72
  const openChildSessions = () => {
74
- if (!effectiveSessionKey) {
73
+ if (!sessionKey) {
75
74
  return;
76
75
  }
77
76
  presenter.chatThreadManager.openChildSessionPanel({
78
- parentSessionKey: effectiveSessionKey,
77
+ parentSessionKey: sessionKey,
79
78
  activeChildSessionKey: childSessionTabs[0]?.sessionKey ?? null,
80
79
  });
81
80
  };
82
81
  const openSessionCronJobs = () => {
83
- if (!effectiveSessionKey || sessionCronJobs.length === 0) {
82
+ if (!sessionKey || sessionCronJobs.length === 0) {
84
83
  return;
85
84
  }
86
- presenter.chatThreadManager.openSessionCronPanel(effectiveSessionKey);
85
+ presenter.chatThreadManager.openSessionCronPanel(sessionKey);
87
86
  };
88
87
 
89
88
  return (
@@ -124,7 +123,7 @@ export function ChatConversationHeaderSection({
124
123
  projectBadge={
125
124
  sessionProjectName ? (
126
125
  <ChatSessionProjectBadge
127
- sessionKey={effectiveSessionKey ?? "draft"}
126
+ sessionKey={sessionKey ?? "draft"}
128
127
  projectName={sessionProjectName}
129
128
  projectRoot={sessionProjectRoot}
130
129
  persistToServer={canDeleteSession}
@@ -132,9 +131,9 @@ export function ChatConversationHeaderSection({
132
131
  ) : null
133
132
  }
134
133
  actions={
135
- effectiveSessionKey ? (
134
+ sessionKey ? (
136
135
  <ChatSessionHeaderActions
137
- sessionKey={effectiveSessionKey}
136
+ sessionKey={sessionKey}
138
137
  canDeleteSession={canDeleteSession}
139
138
  isDeletePending={snapshot.isDeletePending}
140
139
  projectRoot={sessionProjectRoot}
@@ -1,14 +1,12 @@
1
- import { ChatInputBarContainer } from "@/features/chat/features/input/components/chat-input-bar.container";
2
- import { ChatConversationAlerts } from "@/features/chat/components/conversation/chat-conversation-alerts";
3
- import { ChatConversationContent } from "@/features/chat/components/conversation/chat-conversation-content";
1
+ import { useCallback } from "react";
4
2
  import { ChatConversationHeaderSection } from "@/features/chat/components/conversation/chat-conversation-header-section";
5
3
  import { ChatConversationParentBanner } from "@/features/chat/components/conversation/chat-conversation-parent-banner";
6
4
  import { ChatConversationSkeleton } from "@/features/chat/components/conversation/chat-conversation-skeleton";
7
5
  import { ChatConversationWorkspaceSection } from "@/features/chat/components/conversation/chat-conversation-workspace-section";
6
+ import { usePresenter } from "@/features/chat/components/providers/chat-presenter.provider";
7
+ import { SessionConversationArea } from "@/features/chat/features/conversation/components/session-conversation-area";
8
8
  import { useNcpChatProviderStateResolved } from "@/features/chat/features/ncp/hooks/use-ncp-chat-derived-state";
9
- import { ChatConversationWelcome } from "@/features/chat/features/welcome/components/chat-conversation-welcome";
10
- import { shouldShowChatWelcome } from "@/features/chat/features/welcome/utils/chat-welcome-visibility.utils";
11
- import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
9
+ import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
12
10
 
13
11
  type ChatConversationLayoutMode = "desktop" | "mobile";
14
12
 
@@ -19,10 +17,17 @@ export function ChatConversationPanel({
19
17
  layoutMode?: ChatConversationLayoutMode;
20
18
  onBackToList?: () => void;
21
19
  }) {
20
+ const presenter = usePresenter();
22
21
  const isProviderStateResolved = useNcpChatProviderStateResolved();
23
- const showWelcome = useChatThreadStore((state) =>
24
- shouldShowChatWelcome(state.snapshot),
22
+ const sessionKey = useChatSessionListStore(
23
+ (state) => state.snapshot.selectedSessionKey,
25
24
  );
25
+ const handleSessionMaterialized = useCallback((materializedSessionKey: string) => {
26
+ if (!presenter.chatUiManager.isAtChatRoot()) {
27
+ return;
28
+ }
29
+ presenter.chatUiManager.goToSession(materializedSessionKey, { replace: true });
30
+ }, [presenter]);
26
31
 
27
32
  if (!isProviderStateResolved) {
28
33
  return <ChatConversationSkeleton />;
@@ -36,19 +41,14 @@ export function ChatConversationPanel({
36
41
  layoutMode={layoutMode}
37
42
  onBackToList={onBackToList}
38
43
  />
39
- <ChatConversationAlerts />
40
- <ChatConversationContent
41
- showWelcome={showWelcome}
42
- welcomeSlot={
43
- <ChatConversationWelcome
44
- inputSlot={<ChatInputBarContainer surface="embedded" />}
45
- />
46
- }
44
+ <SessionConversationArea
45
+ consumeDraftIntent
46
+ sessionKey={sessionKey}
47
+ onSessionMaterialized={handleSessionMaterialized}
47
48
  />
48
- {showWelcome ? null : <ChatInputBarContainer />}
49
49
  </div>
50
50
 
51
- <ChatConversationWorkspaceSection layoutMode={layoutMode} />
51
+ <ChatConversationWorkspaceSection layoutMode={layoutMode} sessionKey={sessionKey} />
52
52
  </section>
53
53
  );
54
54
  }
@@ -1,21 +1,26 @@
1
1
  import { ChatSessionWorkspacePanel } from "@/features/chat/features/workspace/components/chat-session-workspace-panel";
2
+ import { useNcpChatSelectedSession } from "@/features/chat/features/ncp/hooks/use-ncp-chat-derived-state";
2
3
  import { useChatConversationWorkspaceState } from "@/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state";
3
4
  import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
4
5
 
5
6
  type ChatConversationWorkspaceSectionProps = {
6
7
  layoutMode: "desktop" | "mobile";
8
+ sessionKey: string | null;
7
9
  };
8
10
 
9
11
  export function ChatConversationWorkspaceSection({
10
12
  layoutMode,
13
+ sessionKey,
11
14
  }: ChatConversationWorkspaceSectionProps) {
12
15
  const snapshot = useChatThreadStore((state) => state.snapshot);
16
+ const selectedSession = useNcpChatSelectedSession(sessionKey);
13
17
  const {
14
18
  childSessionTabs,
19
+ activeSideChatDraft,
15
20
  workspaceFileTabs,
16
21
  sessionCronJobs,
17
22
  showWorkspacePanel,
18
- } = useChatConversationWorkspaceState(snapshot);
23
+ } = useChatConversationWorkspaceState(snapshot, sessionKey);
19
24
 
20
25
  if (!showWorkspacePanel) {
21
26
  return null;
@@ -23,18 +28,19 @@ export function ChatConversationWorkspaceSection({
23
28
 
24
29
  return (
25
30
  <ChatSessionWorkspacePanel
26
- sessionKey={snapshot.sessionKey}
31
+ sessionKey={sessionKey}
27
32
  childSessionTabs={childSessionTabs}
28
33
  activeChildSessionKey={snapshot.activeChildSessionKey ?? null}
34
+ activeSideChatDraft={activeSideChatDraft}
29
35
  workspaceFileTabs={workspaceFileTabs}
30
36
  activeWorkspaceFileKey={snapshot.activeWorkspaceFileKey ?? null}
31
37
  workspaceNavigationHistory={snapshot.workspaceNavigationHistory}
32
38
  workspaceNavigationHistoryIndex={snapshot.workspaceNavigationHistoryIndex}
33
39
  activePanelKind={snapshot.activeWorkspacePanelKind ?? null}
34
40
  sessionCronJobs={sessionCronJobs}
35
- sessionProjectRoot={snapshot.sessionProjectRoot ?? null}
41
+ sessionProjectRoot={selectedSession?.projectRoot ?? null}
36
42
  sessionWorkingDir={
37
- snapshot.sessionWorkingDir ?? snapshot.sessionProjectRoot ?? null
43
+ selectedSession?.workingDir ?? selectedSession?.projectRoot ?? null
38
44
  }
39
45
  displayMode={layoutMode === "mobile" ? "overlay" : "docked"}
40
46
  />
@@ -1,10 +1,12 @@
1
- import { render, waitFor } from "@testing-library/react";
1
+ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
2
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
2
3
  import { MemoryRouter } from "react-router-dom";
3
4
  import { beforeEach, describe, expect, it, vi } from "vitest";
4
5
  import { ChatSidebar } from "@/features/chat/components/layout/chat-sidebar";
5
6
  import type { NcpSessionListItemView } from "@/features/chat/features/ncp/hooks/use-ncp-session-list-view";
6
7
  import { useChatQueryStore } from "@/features/chat/stores/ncp-chat-query.store";
7
8
  import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
9
+ import { viewportLayoutManager } from "@/app/managers/viewport-layout.manager";
8
10
 
9
11
  const mocks = vi.hoisted(() => ({
10
12
  sessionItems: [] as NcpSessionListItemView[],
@@ -107,6 +109,8 @@ function createRunningSessionItem(
107
109
  }
108
110
 
109
111
  function resetReadStateTestState() {
112
+ window.localStorage.clear();
113
+ viewportLayoutManager.resetForTests();
110
114
  mocks.sessionItems = [];
111
115
  useChatQueryStore.setState({
112
116
  snapshot: {
@@ -130,16 +134,36 @@ function resetReadStateTestState() {
130
134
  });
131
135
  }
132
136
 
137
+ function createTestQueryClient() {
138
+ return new QueryClient({
139
+ defaultOptions: {
140
+ queries: { retry: false },
141
+ mutations: { retry: false },
142
+ },
143
+ });
144
+ }
145
+
146
+ function createReadStateSidebarElement(queryClient: QueryClient) {
147
+ return (
148
+ <QueryClientProvider client={queryClient}>
149
+ <MemoryRouter>
150
+ <ChatSidebar />
151
+ </MemoryRouter>
152
+ </QueryClientProvider>
153
+ );
154
+ }
155
+
156
+ function renderReadStateSidebar(queryClient: QueryClient) {
157
+ return render(createReadStateSidebarElement(queryClient));
158
+ }
159
+
133
160
  describe("ChatSidebar read state sync", () => {
134
161
  beforeEach(resetReadStateTestState);
135
162
 
136
163
  it("waits until the active running session is idle before persisting the read watermark", async () => {
137
164
  mocks.sessionItems = [createRunningSessionItem("running")];
138
- const { rerender } = render(
139
- <MemoryRouter>
140
- <ChatSidebar />
141
- </MemoryRouter>,
142
- );
165
+ const queryClient = createTestQueryClient();
166
+ const { rerender } = renderReadStateSidebar(queryClient);
143
167
 
144
168
  await waitFor(() => {
145
169
  expect(
@@ -150,11 +174,7 @@ describe("ChatSidebar read state sync", () => {
150
174
  });
151
175
 
152
176
  mocks.sessionItems = [createRunningSessionItem()];
153
- rerender(
154
- <MemoryRouter>
155
- <ChatSidebar />
156
- </MemoryRouter>,
157
- );
177
+ rerender(createReadStateSidebarElement(queryClient));
158
178
 
159
179
  await waitFor(() => {
160
180
  expect(
@@ -164,4 +184,25 @@ describe("ChatSidebar read state sync", () => {
164
184
  ).toBe("2026-03-19T09:05:00.000Z");
165
185
  });
166
186
  });
187
+
188
+ it("collapses the desktop sidebar into accessible icon actions", () => {
189
+ const { container } = renderReadStateSidebar(createTestQueryClient());
190
+
191
+ fireEvent.click(screen.getByRole("button", { name: "Collapse sidebar" }));
192
+
193
+ const persistedState = JSON.parse(
194
+ window.localStorage.getItem("nextclaw.app.viewport-layout") ?? "{}",
195
+ ) as { state?: { isSidebarCollapsed?: boolean } };
196
+
197
+ expect(
198
+ container.querySelector("aside")?.getAttribute("data-sidebar-collapsed"),
199
+ ).toBe("true");
200
+ expect(persistedState.state?.isSidebarCollapsed).toBe(true);
201
+ expect(screen.getByRole("button", { name: "New Task" }).textContent).toBe(
202
+ "",
203
+ );
204
+ expect(screen.queryByPlaceholderText("Search conversations...")).toBeNull();
205
+ expect(screen.getByRole("link", { name: "Scheduled Tasks" })).toBeTruthy();
206
+ expect(screen.getByRole("button", { name: "Settings menu" })).toBeTruthy();
207
+ });
167
208
  });
@@ -1,3 +1,4 @@
1
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
1
2
  import { fireEvent, render, screen, waitFor } from "@testing-library/react";
2
3
  import { MemoryRouter } from "react-router-dom";
3
4
  import { beforeEach, describe, expect, it, vi } from "vitest";
@@ -5,8 +6,9 @@ import { ChatSidebar } from "@/features/chat/components/layout/chat-sidebar";
5
6
  import type { NcpSessionListItemView } from "@/features/chat/features/ncp/hooks/use-ncp-session-list-view";
6
7
  import { useChatQueryStore } from "@/features/chat/stores/ncp-chat-query.store";
7
8
  import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
9
+ import { PREFERENCE_KEYS } from "@/shared/lib/api";
8
10
  import type { ChatSessionTypeOptionView } from "@/shared/lib/api";
9
-
11
+ import { viewportLayoutManager } from "@/app/managers/viewport-layout.manager";
10
12
  const mocks = vi.hoisted(() => ({
11
13
  createSession: vi.fn(() => "draft-session-key"),
12
14
  goToSession: vi.fn(),
@@ -18,6 +20,8 @@ const mocks = vi.hoisted(() => ({
18
20
  docOpen: vi.fn(),
19
21
  setLanguage: vi.fn(),
20
22
  setTheme: vi.fn(),
23
+ fetchPreference: vi.fn(),
24
+ updatePreference: vi.fn(),
21
25
  updateNcpSession: vi.fn(),
22
26
  agents: [] as Array<{
23
27
  id: string;
@@ -27,14 +31,20 @@ const mocks = vi.hoisted(() => ({
27
31
  sessionItems: [] as NcpSessionListItemView[],
28
32
  isLoading: false,
29
33
  }));
30
-
34
+ vi.mock("@/shared/lib/api", async (importOriginal) => {
35
+ const actual = await importOriginal();
36
+ return {
37
+ ...(actual as Record<string, unknown>),
38
+ fetchPreference: mocks.fetchPreference,
39
+ updatePreference: mocks.updatePreference,
40
+ };
41
+ });
31
42
  function createSessionItem(
32
43
  session: NcpSessionListItemView["session"],
33
44
  runStatus?: NcpSessionListItemView["runStatus"],
34
45
  ): NcpSessionListItemView {
35
46
  return { session, runStatus };
36
47
  }
37
-
38
48
  function setSessionTypes(
39
49
  options: ChatSessionTypeOptionView[],
40
50
  defaultType = "native",
@@ -51,18 +61,30 @@ function setSessionTypes(
51
61
  },
52
62
  });
53
63
  }
54
-
55
64
  function sidebarElement(variant?: "desktop" | "mobile") {
65
+ const queryClient = new QueryClient({
66
+ defaultOptions: {
67
+ queries: { retry: false },
68
+ mutations: { retry: false },
69
+ },
70
+ });
56
71
  return (
57
- <MemoryRouter>
58
- <ChatSidebar variant={variant} />
59
- </MemoryRouter>
72
+ <QueryClientProvider client={queryClient}>
73
+ <MemoryRouter>
74
+ <ChatSidebar variant={variant} />
75
+ </MemoryRouter>
76
+ </QueryClientProvider>
60
77
  );
61
78
  }
62
-
63
79
  function renderSidebar(variant?: "desktop" | "mobile") {
64
80
  return render(sidebarElement(variant));
65
81
  }
82
+ function expectCodexSelectedInSessionTypeMenu() {
83
+ fireEvent.click(screen.getByLabelText("Session Type"));
84
+ expect(
85
+ screen.getByRole("button", { name: /Codex/i }).getAttribute("aria-pressed"),
86
+ ).toBe("true");
87
+ }
66
88
 
67
89
  vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
68
90
  usePresenter: () => ({
@@ -166,6 +188,8 @@ vi.mock("@/features/system-status", () => ({
166
188
  }));
167
189
 
168
190
  function resetSidebarTestState() {
191
+ window.localStorage.clear();
192
+ viewportLayoutManager.resetForTests();
169
193
  mocks.createSession.mockReset();
170
194
  mocks.createSession.mockReturnValue("draft-session-key");
171
195
  mocks.goToSession.mockReset();
@@ -177,6 +201,19 @@ function resetSidebarTestState() {
177
201
  mocks.docOpen.mockReset();
178
202
  mocks.setLanguage.mockReset();
179
203
  mocks.setTheme.mockReset();
204
+ mocks.fetchPreference.mockReset();
205
+ mocks.fetchPreference.mockResolvedValue({
206
+ key: PREFERENCE_KEYS.chat.newSessionType,
207
+ value: null,
208
+ });
209
+ mocks.updatePreference.mockReset();
210
+ mocks.updatePreference.mockImplementation(
211
+ async (key: string, value: string) => ({
212
+ key,
213
+ value,
214
+ updatedAt: "2026-06-17T00:00:00.000Z",
215
+ }),
216
+ );
180
217
  mocks.updateNcpSession.mockReset();
181
218
  mocks.updateNcpSession.mockResolvedValue({});
182
219
  mocks.agents = [];
@@ -201,16 +238,44 @@ function resetSidebarTestState() {
201
238
  describe("ChatSidebar create and list basics", () => {
202
239
  beforeEach(resetSidebarTestState);
203
240
 
204
- it("closes the create-session menu after choosing a non-default session type", async () => {
241
+ it("switches the desktop new-session type without creating until the left button is clicked", async () => {
205
242
  renderSidebar();
206
243
 
207
244
  fireEvent.click(screen.getByLabelText("Session Type"));
208
- fireEvent.click(screen.getByText("Codex"));
245
+ fireEvent.click(screen.getByRole("button", { name: /Codex/i }));
246
+
247
+ expect(mocks.createSession).not.toHaveBeenCalled();
248
+ await waitFor(() => {
249
+ expect(mocks.updatePreference).toHaveBeenCalledWith(
250
+ PREFERENCE_KEYS.chat.newSessionType,
251
+ "codex",
252
+ );
253
+ expect(screen.getByLabelText("Session Type").textContent).toBe("");
254
+ });
255
+ expectCodexSelectedInSessionTypeMenu();
256
+
257
+ fireEvent.click(screen.getByRole("button", { name: "New Task" }));
209
258
 
210
259
  expect(mocks.createSession).toHaveBeenCalledWith("codex", undefined);
260
+ });
261
+
262
+ it("hydrates the desktop new-session type from stored preferences", async () => {
263
+ mocks.fetchPreference.mockResolvedValue({
264
+ key: PREFERENCE_KEYS.chat.newSessionType,
265
+ value: "codex",
266
+ });
267
+
268
+ renderSidebar();
269
+
211
270
  await waitFor(() => {
212
- expect(screen.queryByText("Codex")).toBeNull();
271
+ expect(screen.getByLabelText("Session Type").textContent).toBe("");
213
272
  });
273
+ expectCodexSelectedInSessionTypeMenu();
274
+ fireEvent.click(screen.getByLabelText("Session Type"));
275
+
276
+ fireEvent.click(screen.getByRole("button", { name: "New Task" }));
277
+
278
+ expect(mocks.createSession).toHaveBeenCalledWith("codex", undefined);
214
279
  });
215
280
 
216
281
  it("does not animate the desktop create task button width during runtime option hydration", () => {
@@ -228,7 +293,7 @@ describe("ChatSidebar create and list basics", () => {
228
293
 
229
294
  expect(
230
295
  screen.getByTestId("brand-header").parentElement?.className,
231
- ).toContain("py-2.5");
296
+ ).toMatch(/(?:^|\s)py-2(?:\s|$)/);
232
297
  });
233
298
 
234
299
  it("shows setup required status for runtime session types that are not ready yet", () => {