@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,824 +1,131 @@
1
- import { render, screen, within } from "@testing-library/react";
2
- import type { ReactNode } from "react";
1
+ import { render, screen } from "@testing-library/react";
3
2
  import userEvent from "@testing-library/user-event";
4
3
  import { beforeEach, describe, expect, it, vi } from "vitest";
5
4
  import { ChatConversationPanel } from "@/features/chat/components/conversation/chat-conversation-panel";
6
- import { ChatSessionWorkspacePanel } from "@/features/chat/features/workspace/components/chat-session-workspace-panel";
7
- import type { ResolvedChildSessionTab } from "@/features/chat/features/ncp/hooks/use-ncp-child-session-tabs-view";
8
- import type { CronJobView, NcpSessionSummaryView } from "@/shared/lib/api";
9
- import { useChatInputStore } from "@/features/chat/stores/chat-input.store";
10
- import { useChatQueryStore } from "@/features/chat/stores/ncp-chat-query.store";
11
5
  import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
12
- import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
13
- import type { ChatQuerySnapshot } from "@/features/chat/stores/ncp-chat-query.store";
14
6
 
15
7
  const mocks = vi.hoisted(() => ({
16
- deleteSession: vi.fn(),
17
- goToProviders: vi.fn(),
18
- createSession: vi.fn(() => "draft-session-2"),
19
- goToChatRoot: vi.fn(),
20
8
  goToSession: vi.fn(),
21
- openSessionCronPanel: vi.fn(),
22
- goBackWorkspacePanel: vi.fn(),
23
- goForwardWorkspacePanel: vi.fn(),
24
- deleteCronJob: vi.fn(),
25
- cronJobs: [] as CronJobView[],
26
- agents: [
27
- { id: "main", displayName: "Main", runtime: "native" },
28
- { id: "engineer", displayName: "Engineer", runtime: "codex" },
29
- ],
30
- setSelectedAgentId: vi.fn(),
31
- setPendingSessionType: vi.fn(),
32
- stickyBottomScroll: vi.fn(() => ({
33
- onScroll: vi.fn(),
34
- })),
35
- resolvedChildTabs: [
36
- {
37
- sessionKey: "child-session-1",
38
- parentSessionKey: "parent-session-1",
39
- title: "北京天气",
40
- agentId: "weather",
41
- updatedAt: "2026-04-10T09:00:00.000Z",
42
- lastMessageAt: "2026-04-10T09:00:00.000Z",
43
- readAt: "2026-04-10T09:00:00.000Z",
44
- sessionTypeLabel: "Codex",
45
- preferredModel: "openai/gpt-5.3-codex",
46
- projectName: "project-alpha",
47
- projectRoot: "/Users/demo/project-alpha",
48
- },
49
- ] as ResolvedChildSessionTab[],
9
+ isAtChatRoot: vi.fn(() => true),
50
10
  }));
11
+ const persistStorage = new Map<string, unknown>();
51
12
 
52
- vi.mock("@nextclaw/agent-chat-ui", async (importOriginal) => {
53
- const actual = await importOriginal();
13
+ function createPersistStorage() {
54
14
  return {
55
- ...(actual as object),
56
- useStickyBottomScroll: mocks.stickyBottomScroll,
15
+ getItem: (name: string) => persistStorage.get(name) ?? null,
16
+ setItem: (name: string, value: unknown) => {
17
+ persistStorage.set(name, value);
18
+ },
19
+ removeItem: (name: string) => {
20
+ persistStorage.delete(name);
21
+ },
57
22
  };
58
- });
59
-
60
- vi.mock(
61
- "@/features/chat/features/input/components/chat-input-bar.container",
62
- () => ({
63
- ChatInputBarContainer: ({ surface = "default" }: { surface?: string }) => (
64
- <div data-testid="chat-input-bar" data-surface={surface} />
65
- ),
66
- }),
67
- );
68
-
69
- vi.mock(
70
- "@/features/chat/features/message/components/chat-message-list.container",
71
- () => ({
72
- ChatMessageListContainer: ({
73
- isSending,
74
- messages,
75
- }: {
76
- isSending: boolean;
77
- messages: readonly unknown[];
78
- }) => (
79
- <div
80
- data-testid="chat-message-list"
81
- data-message-count={String(messages.length)}
82
- data-sending={String(isSending)}
83
- />
84
- ),
85
- }),
86
- );
87
-
88
- vi.mock(
89
- "@/features/chat/features/workspace/components/chat-session-workspace-file-preview",
90
- () => ({
91
- ChatSessionWorkspaceFilePreview: ({ file }: { file: { path: string } }) => (
92
- <div data-testid="workspace-file-preview">{file.path}</div>
93
- ),
94
- }),
95
- );
23
+ }
96
24
 
97
- vi.mock("@/features/chat/features/welcome/components/chat-welcome", () => ({
98
- ChatWelcome: ({ inputSlot }: { inputSlot?: ReactNode }) => (
99
- <div data-testid="chat-welcome">{inputSlot}</div>
100
- ),
25
+ vi.mock("@/features/chat/features/ncp/hooks/use-ncp-chat-derived-state", () => ({
26
+ useNcpChatProviderStateResolved: () => true,
101
27
  }));
102
28
 
103
29
  vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
104
30
  usePresenter: () => ({
105
31
  chatUiManager: {
106
- goToChatRoot: mocks.goToChatRoot,
107
32
  goToSession: mocks.goToSession,
108
- goToProviders: mocks.goToProviders,
109
- },
110
- chatThreadManager: {
111
- deleteSession: mocks.deleteSession,
112
- openChildSessionPanel: vi.fn(),
113
- openSessionCronPanel: mocks.openSessionCronPanel,
114
- openFilePreview: vi.fn(),
115
- handleToolAction: vi.fn(),
116
- openSessionFromToolAction: vi.fn(),
117
- showContent: vi.fn(),
118
- syncVisibleWorkspaceSelection: vi.fn(),
119
- selectChildSessionDetail: vi.fn(),
120
- selectWorkspaceFile: vi.fn(),
121
- closeWorkspaceFile: vi.fn(),
122
- closeWorkspacePanel: vi.fn(),
123
- goBackWorkspacePanel: mocks.goBackWorkspacePanel,
124
- goForwardWorkspacePanel: mocks.goForwardWorkspacePanel,
125
- goToParentSession: vi.fn(),
126
- },
127
- chatSessionListManager: {
128
- selectSession: vi.fn(),
129
- createSession: mocks.createSession,
130
- setSelectedAgentId: mocks.setSelectedAgentId,
131
- markSessionRead: (
132
- sessionKey: string | null | undefined,
133
- readAt: string | null | undefined,
134
- ) =>
135
- sessionKey
136
- ? useChatSessionListStore
137
- .getState()
138
- .markSessionRead(sessionKey, readAt)
139
- : undefined,
140
- markVisibleWorkspaceChildRead: (tab: {
141
- sessionKey: string | null | undefined;
142
- lastMessageAt?: string | null;
143
- readAt?: string | null;
144
- runStatus?: string | null;
145
- }) => {
146
- if (tab.runStatus === "running") {
147
- return;
148
- }
149
- if (!tab.sessionKey || !tab.lastMessageAt) {
150
- return;
151
- }
152
- useChatSessionListStore
153
- .getState()
154
- .markSessionRead(tab.sessionKey, tab.lastMessageAt);
155
- },
156
- },
157
- chatInputManager: {
158
- setPendingSessionType: mocks.setPendingSessionType,
33
+ isAtChatRoot: mocks.isAtChatRoot,
159
34
  },
160
35
  }),
161
36
  }));
162
37
 
163
- vi.mock("@/features/cron", () => ({
164
- useCronJobs: () => ({
165
- data: { jobs: mocks.cronJobs, total: mocks.cronJobs.length },
166
- }),
167
- useDeleteCronJob: () => ({
168
- mutate: mocks.deleteCronJob,
169
- isPending: false,
170
- }),
38
+ vi.mock("@/features/chat/components/conversation/chat-conversation-parent-banner", () => ({
39
+ ChatConversationParentBanner: () => null,
171
40
  }));
172
41
 
173
- vi.mock(
174
- "@/features/chat/features/session/components/session-header/chat-session-header-actions",
175
- () => ({
176
- ChatSessionHeaderActions: () => <button aria-label="More actions" />,
177
- }),
178
- );
179
-
180
- vi.mock(
181
- "@/features/chat/features/session/components/session-header/chat-session-project-badge",
182
- () => ({
183
- ChatSessionProjectBadge: ({ projectName }: { projectName: string }) => (
184
- <button>{projectName}</button>
185
- ),
186
- }),
187
- );
188
-
189
- vi.mock("@/features/chat/features/ncp/hooks/use-ncp-child-session-tabs-view", () => ({
190
- useNcpChildSessionTabsView: () => mocks.resolvedChildTabs,
42
+ vi.mock("@/features/chat/components/conversation/chat-conversation-header-section", () => ({
43
+ ChatConversationHeaderSection: () => <div data-testid="chat-header" />,
191
44
  }));
192
45
 
193
- vi.mock("@/features/chat/features/ncp/hooks/use-ncp-session-conversation", () => ({
194
- useNcpSessionConversation: () => ({
195
- visibleMessages: [],
196
- isHydrating: false,
197
- hydrateError: null,
198
- isRunning: false,
199
- }),
46
+ vi.mock("@/features/chat/components/conversation/chat-conversation-skeleton", () => ({
47
+ ChatConversationSkeleton: () => <div data-testid="chat-skeleton" />,
200
48
  }));
201
49
 
202
- vi.mock("@/shared/components/common/agent-avatar", () => ({
203
- AgentAvatar: ({ agentId }: { agentId: string }) => (
204
- <div data-testid="agent-avatar">{agentId}</div>
50
+ vi.mock("@/features/chat/components/conversation/chat-conversation-workspace-section", () => ({
51
+ ChatConversationWorkspaceSection: ({ sessionKey }: { sessionKey: string | null }) => (
52
+ <div data-testid="workspace-section" data-session-key={sessionKey ?? ""} />
205
53
  ),
206
54
  }));
207
55
 
208
- vi.mock("@/shared/hooks/use-agents", () => ({
209
- useAgents: () => ({
210
- data: { agents: mocks.agents },
211
- }),
212
- }));
213
-
214
- vi.mock("@/shared/components/common/agent-identity", () => ({
215
- AgentIdentityAvatar: ({ agentId }: { agentId: string }) => (
216
- <div data-testid="agent-identity-avatar">{agentId}</div>
56
+ vi.mock("@/features/chat/features/conversation/components/session-conversation-area", () => ({
57
+ SessionConversationArea: ({
58
+ consumeDraftIntent,
59
+ onSessionMaterialized,
60
+ sessionKey,
61
+ }: {
62
+ consumeDraftIntent?: boolean;
63
+ onSessionMaterialized?: (sessionKey: string) => void;
64
+ sessionKey: string | null;
65
+ }) => (
66
+ <button
67
+ type="button"
68
+ data-testid="session-conversation-area"
69
+ data-consume-draft-intent={String(Boolean(consumeDraftIntent))}
70
+ data-session-key={sessionKey ?? ""}
71
+ onClick={() => onSessionMaterialized?.("materialized-session")}
72
+ >
73
+ area
74
+ </button>
217
75
  ),
218
76
  }));
219
77
 
220
- function createFetchedQuery<TData>(data: TData) {
221
- return {
222
- data,
223
- error: null,
224
- fetchStatus: "idle",
225
- isFetched: true,
226
- isFetching: false,
227
- isLoading: false,
228
- isSuccess: true,
229
- status: "success",
230
- };
231
- }
232
-
233
- function createSessionSummary(
234
- overrides: Partial<NcpSessionSummaryView> & Pick<NcpSessionSummaryView, "sessionId">,
235
- ): NcpSessionSummaryView {
236
- return {
237
- sessionId: overrides.sessionId,
238
- agentId: overrides.agentId ?? "main",
239
- createdAt: overrides.createdAt ?? "2026-04-10T08:00:00.000Z",
240
- updatedAt: overrides.updatedAt ?? "2026-04-10T09:00:00.000Z",
241
- lastMessageAt: overrides.lastMessageAt ?? "2026-04-10T09:00:00.000Z",
242
- messageCount: overrides.messageCount ?? 1,
243
- metadata: overrides.metadata ?? {},
244
- status: overrides.status ?? "idle",
245
- workingDir: overrides.workingDir,
246
- };
247
- }
248
-
249
- function resetChatConversationPanelTestState() {
250
- mocks.deleteSession.mockReset();
251
- mocks.goToProviders.mockReset();
252
- mocks.createSession.mockReset();
253
- mocks.createSession.mockReturnValue("draft-session-2");
254
- mocks.goToChatRoot.mockReset();
255
- mocks.goToSession.mockReset();
256
- mocks.openSessionCronPanel.mockReset();
257
- mocks.goBackWorkspacePanel.mockReset();
258
- mocks.goForwardWorkspacePanel.mockReset();
259
- mocks.deleteCronJob.mockReset();
260
- mocks.cronJobs = [];
261
- mocks.agents = [
262
- { id: "main", displayName: "Main", runtime: "native" },
263
- { id: "engineer", displayName: "Engineer", runtime: "codex" },
264
- ];
265
- mocks.resolvedChildTabs = [
266
- {
267
- sessionKey: "child-session-1",
268
- parentSessionKey: "parent-session-1",
269
- title: "北京天气",
270
- agentId: "weather",
271
- updatedAt: "2026-04-10T09:00:00.000Z",
272
- lastMessageAt: "2026-04-10T09:00:00.000Z",
273
- readAt: "2026-04-10T09:00:00.000Z",
274
- sessionTypeLabel: "Codex",
275
- preferredModel: "openai/gpt-5.3-codex",
276
- projectName: "project-alpha",
277
- projectRoot: "/Users/demo/project-alpha",
278
- },
279
- ];
280
- mocks.setSelectedAgentId.mockReset();
281
- mocks.setPendingSessionType.mockReset();
282
- mocks.stickyBottomScroll.mockClear();
283
- useChatInputStore.setState({
284
- snapshot: {
285
- ...useChatInputStore.getState().snapshot,
286
- isProviderStateResolved: false,
287
- defaultSessionType: "native",
288
- pendingSessionType: "codex",
289
- sessionTypeUnavailable: false,
290
- sessionTypeUnavailableMessage: null,
291
- modelOptions: [
292
- {
293
- value: "openai/gpt-5.1",
294
- modelLabel: "gpt-5.1",
295
- providerLabel: "OpenAI",
296
- } as never,
297
- ],
298
- },
299
- });
300
- useChatQueryStore.setState({
301
- snapshot: {
302
- configQuery: createFetchedQuery({ agents: { defaults: {} }, providers: {} }) as unknown as ChatQuerySnapshot["configQuery"],
303
- providersQuery: createFetchedQuery({ providers: {} }) as unknown as ChatQuerySnapshot["providersQuery"],
304
- providerTemplatesQuery: createFetchedQuery({ providerTemplates: [] }) as unknown as ChatQuerySnapshot["providerTemplatesQuery"],
305
- sessionTypesQuery: createFetchedQuery({
306
- defaultType: "native",
307
- options: [
308
- { value: "native", label: "Native", ready: true },
309
- { value: "codex", label: "Codex", ready: true },
310
- ],
311
- }) as unknown as ChatQuerySnapshot["sessionTypesQuery"],
312
- },
313
- });
314
- useChatThreadStore.setState({
315
- snapshot: {
316
- ...useChatThreadStore.getState().snapshot,
317
- sessionTypeLabel: "Codex",
318
- sessionKey: "draft-session-1",
319
- sessionDisplayName: undefined,
320
- agentId: null,
321
- sessionProjectRoot: null,
322
- sessionProjectName: null,
323
- canDeleteSession: false,
324
- isDeletePending: false,
325
- isHistoryLoading: false,
326
- messages: [],
327
- isSending: false,
328
- isAwaitingAssistantOutput: false,
329
- hasSubmittedDraftMessage: false,
330
- parentSessionKey: null,
331
- parentSessionLabel: null,
332
- workspacePanelParentKey: null,
333
- childSessionTabs: [],
334
- activeChildSessionKey: null,
335
- workspaceFileTabs: [],
336
- activeWorkspaceFileKey: null,
337
- workspaceNavigationHistory: [],
338
- workspaceNavigationHistoryIndex: 0,
339
- },
340
- });
341
- useChatSessionListStore.setState({
342
- optimisticReadAtBySessionKey: {},
343
- snapshot: {
344
- ...useChatSessionListStore.getState().snapshot,
345
- },
346
- });
347
- }
348
-
349
78
  describe("ChatConversationPanel", () => {
350
- beforeEach(resetChatConversationPanelTestState);
351
-
352
- it("shows the draft session type in the conversation header", () => {
353
- render(<ChatConversationPanel />);
354
-
355
- expect(screen.getByText("New Task")).toBeTruthy();
356
- expect(screen.getByText("Codex")).toBeTruthy();
357
- expect(screen.getByLabelText("More actions")).toBeTruthy();
358
- });
359
-
360
- it("uses the mobile conversation header as the chat detail back entry", async () => {
361
- const user = userEvent.setup();
362
- const onBackToList = vi.fn();
363
-
364
- render(
365
- <ChatConversationPanel layoutMode="mobile" onBackToList={onBackToList} />,
366
- );
367
-
368
- await user.click(screen.getByRole("button", { name: "Chat" }));
369
-
370
- expect(onBackToList).toHaveBeenCalledTimes(1);
371
- });
372
-
373
- it("shows the selected session project badge and more actions trigger", () => {
79
+ beforeEach(() => {
80
+ persistStorage.clear();
81
+ useChatSessionListStore.persist.setOptions({ storage: createPersistStorage() as never });
82
+ mocks.goToSession.mockReset();
83
+ mocks.isAtChatRoot.mockReset();
84
+ mocks.isAtChatRoot.mockReturnValue(true);
374
85
  useChatSessionListStore.setState({
375
86
  snapshot: {
376
87
  ...useChatSessionListStore.getState().snapshot,
377
- selectedSessionKey: "session-1",
378
- },
379
- });
380
- useChatQueryStore.setState({
381
- snapshot: {
382
- ...useChatQueryStore.getState().snapshot,
383
- sessionsQuery: createFetchedQuery({
384
- sessions: [
385
- createSessionSummary({
386
- sessionId: "session-1",
387
- metadata: {
388
- label: "Project Thread",
389
- project_root: "/Users/demo/workspace/project-alpha",
390
- session_type: "codex",
391
- },
392
- }),
393
- ],
394
- total: 1,
395
- }) as unknown as ChatQuerySnapshot["sessionsQuery"],
396
- },
397
- });
398
- useChatThreadStore.setState({
399
- snapshot: {
400
- ...useChatThreadStore.getState().snapshot,
401
- sessionKey: "session-1",
88
+ selectedSessionKey: null,
402
89
  },
403
90
  });
404
-
405
- render(<ChatConversationPanel />);
406
-
407
- expect(screen.getByText("Project Thread")).toBeTruthy();
408
- expect(screen.getByText("project-alpha")).toBeTruthy();
409
- expect(screen.getByLabelText("More actions")).toBeTruthy();
410
91
  });
411
92
 
412
- it("does not show a header agent marker for the main agent", () => {
413
- useChatThreadStore.setState({
414
- snapshot: {
415
- ...useChatThreadStore.getState().snapshot,
416
- agentId: "main",
417
- },
418
- });
419
-
420
- render(<ChatConversationPanel />);
421
-
422
- expect(screen.queryByTestId("agent-avatar")).toBeNull();
423
- });
424
-
425
- it("shows only a lightweight avatar marker for a specialist agent", () => {
426
- useChatThreadStore.setState({
427
- snapshot: {
428
- ...useChatThreadStore.getState().snapshot,
429
- agentId: "engineer",
430
- },
431
- });
432
-
433
- render(<ChatConversationPanel />);
434
-
435
- expect(screen.getByTestId("agent-identity-avatar").textContent).toBe("engineer");
436
- expect(screen.queryByText("Engineer")).toBeNull();
437
- });
438
-
439
- it("renders a fuller loading skeleton before provider state settles", () => {
440
- useChatQueryStore.setState({ snapshot: {} });
441
-
442
- render(<ChatConversationPanel />);
443
-
444
- expect(screen.getByTestId("chat-conversation-skeleton")).toBeTruthy();
445
- expect(
446
- screen.getAllByTestId("chat-conversation-skeleton-bubble"),
447
- ).toHaveLength(4);
448
- expect(screen.queryByTestId("chat-input-bar")).toBeNull();
449
- });
450
-
451
- it("does not keep the conversation skeleton after provider queries resolve", () => {
452
- render(<ChatConversationPanel />);
453
-
454
- expect(screen.queryByTestId("chat-conversation-skeleton")).toBeNull();
455
- const welcome = screen.getByTestId("chat-welcome");
456
- const inputBar = within(welcome).getByTestId("chat-input-bar");
457
- expect(inputBar.dataset.surface).toBe("embedded");
458
- expect(screen.getAllByTestId("chat-input-bar")).toHaveLength(1);
459
- });
460
-
461
- it("keeps the message area clean while a session history is hydrating", () => {
462
- useChatThreadStore.setState({
463
- snapshot: {
464
- ...useChatThreadStore.getState().snapshot,
465
- sessionKey: "session-1",
466
- canDeleteSession: true,
467
- isHistoryLoading: true,
468
- messages: [],
469
- },
470
- });
471
-
472
- render(<ChatConversationPanel />);
473
-
474
- expect(
475
- screen.queryByRole("status", { name: "Loading session history..." }),
476
- ).toBeNull();
477
- expect(
478
- screen.queryByText("No messages yet. Send one to start."),
479
- ).toBeNull();
480
- });
481
-
482
- it("keeps the message list mounted while waiting for the first assistant token", () => {
483
- useChatThreadStore.setState({
93
+ it("passes the selected session key into the self-contained conversation area and workspace", () => {
94
+ useChatSessionListStore.setState({
484
95
  snapshot: {
485
- ...useChatThreadStore.getState().snapshot,
486
- sessionKey: "session-1",
487
- messages: [
488
- {
489
- id: "user-1",
490
- sessionId: "session-1",
491
- role: "user",
492
- status: "final",
493
- parts: [{ type: "text", text: "hello" }],
494
- timestamp: "2026-05-19T00:00:00.000Z",
495
- } as never,
496
- ],
497
- isSending: true,
498
- isAwaitingAssistantOutput: true,
96
+ ...useChatSessionListStore.getState().snapshot,
97
+ selectedSessionKey: "session-1",
499
98
  },
500
99
  });
501
100
 
502
101
  render(<ChatConversationPanel />);
503
102
 
504
- expect(screen.getByTestId("chat-message-list").dataset).toMatchObject({
505
- messageCount: "1",
506
- sending: "true",
103
+ expect(screen.getByTestId("chat-header")).toBeTruthy();
104
+ expect(screen.getByTestId("session-conversation-area").dataset).toMatchObject({
105
+ sessionKey: "session-1",
106
+ consumeDraftIntent: "true",
507
107
  });
508
- expect(
509
- screen
510
- .getByTestId("chat-message-list")
511
- .closest('[data-chat-scroll-container="true"]'),
512
- ).toBeTruthy();
513
- expect(
514
- screen.queryByText("No messages yet. Send one to start."),
515
- ).toBeNull();
108
+ expect(screen.getByTestId("workspace-section").dataset.sessionKey).toBe("session-1");
516
109
  });
517
110
 
518
- it("does not show assistant waiting copy before the first message is visible", () => {
519
- useChatThreadStore.setState({
520
- snapshot: {
521
- ...useChatThreadStore.getState().snapshot,
522
- sessionKey: "session-1",
523
- messages: [],
524
- isSending: true,
525
- isAwaitingAssistantOutput: true,
526
- },
527
- });
528
-
529
- render(<ChatConversationPanel />);
530
-
531
- expect(screen.queryByTestId("chat-message-list")).toBeNull();
532
- expect(
533
- screen.queryByText("No messages yet. Send one to start."),
534
- ).toBeNull();
535
- });
536
-
537
- it("does not reopen the welcome panel after a root draft send fails", () => {
538
- useChatThreadStore.setState({
539
- snapshot: {
540
- ...useChatThreadStore.getState().snapshot,
541
- sessionKey: null,
542
- messages: [],
543
- isSending: false,
544
- isAwaitingAssistantOutput: false,
545
- hasSubmittedDraftMessage: true,
546
- },
547
- });
548
-
549
- render(<ChatConversationPanel />);
550
-
551
- expect(screen.queryByTestId("chat-welcome")).toBeNull();
552
- expect(
553
- screen.queryByText("No messages yet. Send one to start."),
554
- ).toBeNull();
555
- });
111
+ it("materializes root draft sessions through the UI manager route owner", async () => {
112
+ const user = userEvent.setup();
556
113
 
557
- it("does not render runtime lifecycle copy in the conversation alert strip", () => {
558
114
  render(<ChatConversationPanel />);
115
+ await user.click(screen.getByTestId("session-conversation-area"));
559
116
 
560
- expect(
561
- screen.queryByText(
562
- "聊天能力正在初始化。你可以先输入内容,完成后即可发送。",
563
- ),
564
- ).toBeNull();
565
- });
566
-
567
- it("does not auto-open the child-session panel until the panel is explicitly opened", () => {
568
- useChatThreadStore.setState({
569
- snapshot: {
570
- ...useChatThreadStore.getState().snapshot,
571
- sessionKey: "parent-session-1",
572
- sessionDisplayName: "Parent Session",
573
- canDeleteSession: true,
574
- childSessionTabs: [
575
- {
576
- sessionKey: "child-session-1",
577
- parentSessionKey: "parent-session-1",
578
- label: "北京天气",
579
- agentId: "weather",
580
- },
581
- ],
582
- activeChildSessionKey: "child-session-1",
583
- workspacePanelParentKey: null,
584
- },
117
+ expect(mocks.goToSession).toHaveBeenCalledWith("materialized-session", {
118
+ replace: true,
585
119
  });
586
-
587
- render(<ChatConversationPanel />);
588
-
589
- expect(screen.queryByLabelText("Close child session panel")).toBeNull();
590
- });
591
-
592
- });
593
-
594
- describe("ChatSessionWorkspacePanel", () => {
595
- beforeEach(resetChatConversationPanelTestState);
596
-
597
- it("renders child session tabs and active child metadata in the workspace sidebar", () => {
598
- mocks.resolvedChildTabs = [
599
- {
600
- sessionKey: "child-session-1",
601
- parentSessionKey: "parent-session-1",
602
- title: "北京天气",
603
- agentId: "weather",
604
- updatedAt: "2026-04-10T09:00:00.000Z",
605
- lastMessageAt: "2026-04-10T09:00:00.000Z",
606
- readAt: "2026-04-10T09:00:00.000Z",
607
- sessionTypeLabel: "Codex",
608
- preferredModel: "openai/gpt-5.3-codex",
609
- projectName: "project-alpha",
610
- projectRoot: "/Users/demo/project-alpha",
611
- },
612
- ];
613
-
614
- render(
615
- <ChatSessionWorkspacePanel
616
- sessionKey="parent-session-1"
617
- childSessionTabs={[
618
- {
619
- sessionKey: "child-session-1",
620
- parentSessionKey: "parent-session-1",
621
- label: "北京天气",
622
- agentId: "weather",
623
- },
624
- ]}
625
- activeChildSessionKey="child-session-1"
626
- workspaceFileTabs={[]}
627
- activeWorkspaceFileKey={null}
628
- sessionProjectRoot="/Users/demo/project-alpha"
629
- sessionWorkingDir="/Users/demo/project-alpha"
630
- />,
631
- );
632
-
633
- expect(screen.queryByText("Child sessions")).toBeNull();
634
- expect(screen.getByTestId("resizable-right-panel-handle")).toBeTruthy();
635
- expect(screen.getAllByText("北京天气")).toHaveLength(2);
636
- expect(screen.getByText("Codex")).toBeTruthy();
637
- expect(screen.getByText("openai/gpt-5.3-codex")).toBeTruthy();
638
- expect(screen.getByText("project-alpha")).toBeTruthy();
639
- expect(screen.getByText("/Users/demo/project-alpha")).toBeTruthy();
640
- expect(screen.getByText("No child session messages yet.")).toBeTruthy();
641
- expect(mocks.stickyBottomScroll).toHaveBeenCalledWith(
642
- expect.objectContaining({
643
- resetKey: "child-session-1",
644
- stickyThresholdPx: 20,
645
- }),
646
- );
647
- });
648
-
649
- it("shows unread state for inactive child session tabs", () => {
650
- mocks.resolvedChildTabs = [
651
- {
652
- sessionKey: "child-session-1",
653
- parentSessionKey: "parent-session-1",
654
- title: "北京天气",
655
- agentId: "weather",
656
- updatedAt: "2026-04-10T09:00:00.000Z",
657
- lastMessageAt: "2026-04-10T09:00:00.000Z",
658
- readAt: "2026-04-10T09:00:00.000Z",
659
- sessionTypeLabel: "Codex",
660
- preferredModel: "openai/gpt-5.3-codex",
661
- projectName: "project-alpha",
662
- projectRoot: "/Users/demo/project-alpha",
663
- },
664
- {
665
- sessionKey: "child-session-2",
666
- parentSessionKey: "parent-session-1",
667
- title: "上海天气",
668
- agentId: "weather",
669
- updatedAt: "2026-04-10T09:05:00.000Z",
670
- lastMessageAt: "2026-04-10T09:06:00.000Z",
671
- readAt: "2026-04-10T09:05:00.000Z",
672
- sessionTypeLabel: "Claude Code",
673
- preferredModel: "anthropic/claude-sonnet-4",
674
- projectName: "project-beta",
675
- projectRoot: "/Users/demo/project-beta",
676
- },
677
- ];
678
-
679
- render(
680
- <ChatSessionWorkspacePanel
681
- sessionKey="parent-session-1"
682
- childSessionTabs={[
683
- {
684
- sessionKey: "child-session-1",
685
- parentSessionKey: "parent-session-1",
686
- label: "北京天气",
687
- agentId: "weather",
688
- },
689
- {
690
- sessionKey: "child-session-2",
691
- parentSessionKey: "parent-session-1",
692
- label: "上海天气",
693
- agentId: "weather",
694
- },
695
- ]}
696
- activeChildSessionKey="child-session-1"
697
- workspaceFileTabs={[]}
698
- activeWorkspaceFileKey={null}
699
- sessionProjectRoot="/Users/demo/project-alpha"
700
- sessionWorkingDir="/Users/demo/project-alpha"
701
- />,
702
- );
703
-
704
- expect(screen.getByLabelText("Session has unread updates")).toBeTruthy();
705
120
  });
706
121
 
707
- it("shows opened files as top tabs and renders the file preview pane", () => {
708
- render(
709
- <ChatSessionWorkspacePanel
710
- sessionKey="parent-session-1"
711
- childSessionTabs={[]}
712
- activeChildSessionKey={null}
713
- workspaceFileTabs={[
714
- {
715
- key: "parent-session-1::preview::README.md",
716
- parentSessionKey: "parent-session-1",
717
- path: "README.md",
718
- label: "README.md",
719
- viewMode: "preview",
720
- },
721
- ]}
722
- activeWorkspaceFileKey="parent-session-1::preview::README.md"
723
- sessionProjectRoot="/Users/demo/project-alpha"
724
- sessionWorkingDir="/Users/demo/project-alpha"
725
- />,
726
- );
727
-
728
- expect(screen.queryByText("Open files")).toBeNull();
729
- expect(screen.getAllByText("README.md").length).toBeGreaterThan(0);
730
- expect(screen.getByTestId("workspace-file-preview").textContent).toBe(
731
- "README.md",
732
- );
733
- expect(
734
- screen.getByTestId("workspace-tabs-scroll").parentElement?.className,
735
- ).toContain("workspace-horizontal-scrollbar");
736
- });
737
-
738
- it("uses workspace-local backward and forward history actions in the tab bar", async () => {
122
+ it("does not materialize through the root route callback outside chat root", async () => {
739
123
  const user = userEvent.setup();
124
+ mocks.isAtChatRoot.mockReturnValue(false);
740
125
 
741
- render(
742
- <ChatSessionWorkspacePanel
743
- sessionKey="parent-session-1"
744
- childSessionTabs={[]}
745
- activeChildSessionKey={null}
746
- workspaceFileTabs={[
747
- {
748
- key: "parent-session-1::preview::README.md",
749
- parentSessionKey: "parent-session-1",
750
- path: "README.md",
751
- label: "README.md",
752
- viewMode: "preview",
753
- },
754
- ]}
755
- activeWorkspaceFileKey="parent-session-1::preview::README.md"
756
- workspaceNavigationHistory={[
757
- { kind: "child-session", key: "child-session-1" },
758
- { kind: "file", key: "parent-session-1::preview::README.md" },
759
- { kind: "cron" },
760
- ]}
761
- workspaceNavigationHistoryIndex={1}
762
- sessionProjectRoot="/Users/demo/project-alpha"
763
- sessionWorkingDir="/Users/demo/project-alpha"
764
- />,
765
- );
766
-
767
- await user.click(screen.getByRole("button", { name: "Back in workspace" }));
768
- await user.click(
769
- screen.getByRole("button", { name: "Forward in workspace" }),
770
- );
771
-
772
- expect(mocks.goBackWorkspacePanel).toHaveBeenCalledTimes(1);
773
- expect(mocks.goForwardWorkspacePanel).toHaveBeenCalledTimes(1);
774
- expect(screen.queryByRole("button", { name: "Back to parent" })).toBeNull();
775
- });
776
-
777
- it("renders session cron jobs in the workspace sidebar and deletes with a neutral confirmation", async () => {
778
- const user = userEvent.setup();
779
- const job: CronJobView = {
780
- id: "job-1",
781
- name: "Follow up",
782
- enabled: true,
783
- schedule: { kind: "every", everyMs: 3600000 },
784
- payload: {
785
- kind: "agent_turn",
786
- message: "Continue this session later",
787
- sessionId: "parent-session-1",
788
- },
789
- state: {
790
- nextRunAt: "2026-05-15T10:00:00.000Z",
791
- lastRunAt: null,
792
- lastStatus: null,
793
- lastError: null,
794
- },
795
- createdAt: "2026-05-15T09:00:00.000Z",
796
- updatedAt: "2026-05-15T09:00:00.000Z",
797
- deleteAfterRun: false,
798
- };
799
-
800
- render(
801
- <ChatSessionWorkspacePanel
802
- sessionKey="parent-session-1"
803
- childSessionTabs={[]}
804
- activeChildSessionKey={null}
805
- workspaceFileTabs={[]}
806
- activeWorkspaceFileKey={null}
807
- activePanelKind="cron"
808
- sessionCronJobs={[job]}
809
- sessionProjectRoot="/Users/demo/project-alpha"
810
- sessionWorkingDir="/Users/demo/project-alpha"
811
- />,
812
- );
813
-
814
- expect(screen.getAllByText("Session cron jobs").length).toBeGreaterThan(0);
815
- expect(screen.getByText("Follow up")).toBeTruthy();
816
- expect(screen.getByText("Continue this session later")).toBeTruthy();
817
-
818
- await user.click(screen.getByRole("button", { name: "Delete" }));
819
- expect(screen.getByText("Delete cron job?")).toBeTruthy();
820
- await user.click(screen.getAllByRole("button", { name: "Delete" }).at(-1)!);
126
+ render(<ChatConversationPanel />);
127
+ await user.click(screen.getByTestId("session-conversation-area"));
821
128
 
822
- expect(mocks.deleteCronJob).toHaveBeenCalledWith({ id: "job-1" });
129
+ expect(mocks.goToSession).not.toHaveBeenCalled();
823
130
  });
824
131
  });