@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
@@ -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
  });
@@ -6,8 +6,9 @@ import { ChatSidebar } from "@/features/chat/components/layout/chat-sidebar";
6
6
  import type { NcpSessionListItemView } from "@/features/chat/features/ncp/hooks/use-ncp-session-list-view";
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
- import { PREFERENCE_KEYS, type ChatSessionTypeOptionView } from "@/shared/lib/api";
10
-
9
+ import { PREFERENCE_KEYS } from "@/shared/lib/api";
10
+ import type { ChatSessionTypeOptionView } from "@/shared/lib/api";
11
+ import { viewportLayoutManager } from "@/app/managers/viewport-layout.manager";
11
12
  const mocks = vi.hoisted(() => ({
12
13
  createSession: vi.fn(() => "draft-session-key"),
13
14
  goToSession: vi.fn(),
@@ -30,7 +31,6 @@ const mocks = vi.hoisted(() => ({
30
31
  sessionItems: [] as NcpSessionListItemView[],
31
32
  isLoading: false,
32
33
  }));
33
-
34
34
  vi.mock("@/shared/lib/api", async (importOriginal) => {
35
35
  const actual = await importOriginal();
36
36
  return {
@@ -39,14 +39,12 @@ vi.mock("@/shared/lib/api", async (importOriginal) => {
39
39
  updatePreference: mocks.updatePreference,
40
40
  };
41
41
  });
42
-
43
42
  function createSessionItem(
44
43
  session: NcpSessionListItemView["session"],
45
44
  runStatus?: NcpSessionListItemView["runStatus"],
46
45
  ): NcpSessionListItemView {
47
46
  return { session, runStatus };
48
47
  }
49
-
50
48
  function setSessionTypes(
51
49
  options: ChatSessionTypeOptionView[],
52
50
  defaultType = "native",
@@ -63,7 +61,6 @@ function setSessionTypes(
63
61
  },
64
62
  });
65
63
  }
66
-
67
64
  function sidebarElement(variant?: "desktop" | "mobile") {
68
65
  const queryClient = new QueryClient({
69
66
  defaultOptions: {
@@ -79,14 +76,14 @@ function sidebarElement(variant?: "desktop" | "mobile") {
79
76
  </QueryClientProvider>
80
77
  );
81
78
  }
82
-
83
79
  function renderSidebar(variant?: "desktop" | "mobile") {
84
80
  return render(sidebarElement(variant));
85
81
  }
86
-
87
82
  function expectCodexSelectedInSessionTypeMenu() {
88
83
  fireEvent.click(screen.getByLabelText("Session Type"));
89
- expect(screen.getByRole("button", { name: /Codex/i }).getAttribute("aria-pressed")).toBe("true");
84
+ expect(
85
+ screen.getByRole("button", { name: /Codex/i }).getAttribute("aria-pressed"),
86
+ ).toBe("true");
90
87
  }
91
88
 
92
89
  vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
@@ -191,6 +188,8 @@ vi.mock("@/features/system-status", () => ({
191
188
  }));
192
189
 
193
190
  function resetSidebarTestState() {
191
+ window.localStorage.clear();
192
+ viewportLayoutManager.resetForTests();
194
193
  mocks.createSession.mockReset();
195
194
  mocks.createSession.mockReturnValue("draft-session-key");
196
195
  mocks.goToSession.mockReset();
@@ -208,10 +207,13 @@ function resetSidebarTestState() {
208
207
  value: null,
209
208
  });
210
209
  mocks.updatePreference.mockReset();
211
- mocks.updatePreference.mockImplementation(async (key: string, value: string) => ({
212
- key, value,
213
- updatedAt: "2026-06-17T00:00:00.000Z",
214
- }));
210
+ mocks.updatePreference.mockImplementation(
211
+ async (key: string, value: string) => ({
212
+ key,
213
+ value,
214
+ updatedAt: "2026-06-17T00:00:00.000Z",
215
+ }),
216
+ );
215
217
  mocks.updateNcpSession.mockReset();
216
218
  mocks.updateNcpSession.mockResolvedValue({});
217
219
  mocks.agents = [];
@@ -291,7 +293,7 @@ describe("ChatSidebar create and list basics", () => {
291
293
 
292
294
  expect(
293
295
  screen.getByTestId("brand-header").parentElement?.className,
294
- ).toContain("py-2.5");
296
+ ).toMatch(/(?:^|\s)py-2(?:\s|$)/);
295
297
  });
296
298
 
297
299
  it("shows setup required status for runtime session types that are not ready yet", () => {