@nextclaw/ui 0.12.7 → 0.12.9

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 (171) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/dist/assets/ChannelsList-Ita2Zm1_.js +8 -0
  3. package/dist/assets/{DocBrowser-Cse_F8Nn.js → DocBrowser-6ReNjvzF.js} +1 -1
  4. package/dist/assets/DocBrowser-BNwbPHf4.js +1 -0
  5. package/dist/assets/{DocBrowserContext-Bai1WU2H.js → DocBrowserContext-B6SpA7Qs.js} +1 -1
  6. package/dist/assets/{LogoBadge-BdxMPc9v.js → LogoBadge-ByNLYg65.js} +1 -1
  7. package/dist/assets/MarketplacePage-CjX2MWww.js +1 -0
  8. package/dist/assets/{MarketplacePage-BbpAkllU.js → MarketplacePage-D0sDlYX4.js} +1 -1
  9. package/dist/assets/McpMarketplacePage-BGKJm1sJ.js +40 -0
  10. package/dist/assets/{ModelConfig-3GLqQ5GY.js → ModelConfig-BzZenCH-.js} +1 -1
  11. package/dist/assets/{ProviderScopedModelInput-BYNouw-i.js → ProviderScopedModelInput-Da7khnBA.js} +1 -1
  12. package/dist/assets/{ProvidersList-BR1gJ4Dm.js → ProvidersList-BbVzRxjY.js} +1 -1
  13. package/dist/assets/RemoteAccessPage-BaDH_X1Q.js +1 -0
  14. package/dist/assets/RuntimeConfig-F_XKGgLm.js +1 -0
  15. package/dist/assets/{SearchConfig-DTeJvp8m.js → SearchConfig-BGkzXQP-.js} +1 -1
  16. package/dist/assets/{SecretsConfig-CCYO6NcV.js → SecretsConfig-D281Rotl.js} +2 -2
  17. package/dist/assets/{SessionsConfig-Du39vDgt.js → SessionsConfig-ChHQ7M5c.js} +2 -2
  18. package/dist/assets/{app-query-client-Dr5d-K8d.js → app-query-client-VnFElj4E.js} +1 -1
  19. package/dist/assets/{book-open-Da4OEPqB.js → book-open-BdcxxoQu.js} +1 -1
  20. package/dist/assets/chat-page-Doe0yTtB.js +58 -0
  21. package/dist/assets/chat-session-display-cw78aiI_.js +1 -0
  22. package/dist/assets/{chunk-JZWAC4HX-CoFVxHXV.js → chunk-JZWAC4HX-DK5HPmIK.js} +1 -1
  23. package/dist/assets/{client-CSk58DcF.js → client-_i4MU2bB.js} +1 -1
  24. package/dist/assets/{config-D8KzikVB.js → config-DtIQwrHF.js} +1 -1
  25. package/dist/assets/{createLucideIcon-83gaZMtv.js → createLucideIcon-BSeTgkZW.js} +1 -1
  26. package/dist/assets/desktop-update-config-Dpcf4BKG.js +1 -0
  27. package/dist/assets/{dist-toEYs-MZ.js → dist-6TrrnPCR.js} +1 -1
  28. package/dist/assets/{dist-aTmhMDVh.js → dist-ccBFUi-o.js} +1 -1
  29. package/dist/assets/download-BhDxnyvU.js +1 -0
  30. package/dist/assets/{external-link-QQ0TC6X4.js → external-link-BgErLCNT.js} +1 -1
  31. package/dist/assets/{hash-DaFBEkmi.js → hash-Bl7dr_UG.js} +1 -1
  32. package/dist/assets/i18n-eDHeDY0n.js +1 -0
  33. package/dist/assets/index-CF9xve0E.js +6 -0
  34. package/dist/assets/index-FgA52VBt.css +1 -0
  35. package/dist/assets/{infiniteQueryBehavior-BmHX_ayZ.js → infiniteQueryBehavior-ZDS92Qpp.js} +1 -1
  36. package/dist/assets/loader-circle-ACM1s51e.js +1 -0
  37. package/dist/assets/{logos-Dzlz30M3.js → logos-x89HbrZ4.js} +1 -1
  38. package/dist/assets/{page-layout-D2eRufRQ.js → page-layout-vZnghcFy.js} +1 -1
  39. package/dist/assets/play-CFUwCA2E.js +1 -0
  40. package/dist/assets/plus-rYsv72JG.js +1 -0
  41. package/dist/assets/{popover-BSXxm5bj.js → popover-Bg1VoTZ6.js} +1 -1
  42. package/dist/assets/{refresh-ccw-B3zMtN-_.js → refresh-ccw-DT98i__E.js} +1 -1
  43. package/dist/assets/{refresh-cw-DlZkIHnJ.js → refresh-cw-C47QSEwg.js} +1 -1
  44. package/dist/assets/rotate-cw-JtFzpNn6.js +1 -0
  45. package/dist/assets/{save-Us9fg4Sj.js → save-3S6-H3Xw.js} +1 -1
  46. package/dist/assets/search-3kFR_zh9.js +1 -0
  47. package/dist/assets/{security-config-BGWYwxNr.js → security-config-BWaiARNk.js} +1 -1
  48. package/dist/assets/{select-DLYqySQK.js → select-DJ2MUjBB.js} +1 -1
  49. package/dist/assets/skeleton-ByQepn0M.js +1 -0
  50. package/dist/assets/{status-dot-DGayudyB.js → status-dot-vbanNPFU.js} +1 -1
  51. package/dist/assets/{switch-Dz2ScsKx.js → switch-BsLtHOH-.js} +1 -1
  52. package/dist/assets/{tabs-custom-CdKyjiGk.js → tabs-custom-D3HYMt6k.js} +1 -1
  53. package/dist/assets/{trash-2-Db-mZOZs.js → trash-2-G48scll7.js} +1 -1
  54. package/dist/assets/{use-infinite-scroll-loader-DBJX5hj0.js → use-infinite-scroll-loader-DkNhD-42.js} +1 -1
  55. package/dist/assets/{useConfirmDialog-DL0a-oGC.js → useConfirmDialog-BkvTN-vd.js} +1 -1
  56. package/dist/assets/{useMutation-BdZm-9PL.js → useMutation-CBWjE2uj.js} +1 -1
  57. package/dist/assets/x-ByDbItbq.js +1 -0
  58. package/dist/index.html +95 -21
  59. package/dist/manifest.webmanifest +30 -0
  60. package/dist/offline.html +102 -0
  61. package/dist/pwa-192.png +0 -0
  62. package/dist/pwa-512.png +0 -0
  63. package/dist/sw.js +80 -0
  64. package/index.html +73 -1
  65. package/package.json +6 -6
  66. package/public/manifest.webmanifest +30 -0
  67. package/public/offline.html +102 -0
  68. package/public/pwa-192.png +0 -0
  69. package/public/pwa-512.png +0 -0
  70. package/public/sw.js +80 -0
  71. package/src/api/runtime-control.ts +34 -0
  72. package/src/api/runtime-control.types.ts +58 -0
  73. package/src/api/server-path.ts +27 -4
  74. package/src/api/types.ts +30 -10
  75. package/src/{App.test.tsx → app.test.tsx} +1 -1
  76. package/src/{App.tsx → app.tsx} +10 -1
  77. package/src/components/chat/ChatSidebar.test.tsx +79 -8
  78. package/src/components/chat/ChatSidebar.tsx +43 -26
  79. package/src/components/chat/adapters/chat-message.summary-truncation.test.ts +66 -0
  80. package/src/components/chat/adapters/file-operation/card.ts +9 -0
  81. package/src/components/chat/adapters/file-operation/diff.ts +14 -0
  82. package/src/components/chat/{ChatConversationPanel.test.tsx → chat-conversation-panel.test.tsx} +118 -155
  83. package/src/components/chat/chat-conversation-panel.tsx +412 -0
  84. package/src/components/chat/chat-page-runtime.test.ts +1 -1
  85. package/src/components/chat/chat-page-shell.tsx +1 -1
  86. package/src/components/chat/{ChatPage.tsx → chat-page.tsx} +1 -1
  87. package/src/components/chat/chat-session-workspace-file-preview.test.tsx +91 -0
  88. package/src/components/chat/chat-session-workspace-file-preview.tsx +307 -0
  89. package/src/components/chat/chat-session-workspace-panel-nav.tsx +197 -0
  90. package/src/components/chat/chat-session-workspace-panel.tsx +318 -0
  91. package/src/components/chat/chat-sidebar-session-item.tsx +32 -2
  92. package/src/components/chat/containers/chat-message-list.container.test.tsx +49 -0
  93. package/src/components/chat/containers/chat-message-list.container.tsx +4 -0
  94. package/src/components/chat/managers/chat-session-list.manager.test.ts +94 -31
  95. package/src/components/chat/managers/chat-session-list.manager.ts +86 -14
  96. package/src/components/chat/managers/chat-ui.manager.ts +2 -0
  97. package/src/components/chat/ncp/README.md +1 -1
  98. package/src/components/chat/ncp/ncp-chat-input.manager.ts +7 -1
  99. package/src/components/chat/ncp/ncp-chat-page-data.test.ts +1 -1
  100. package/src/components/chat/ncp/{NcpChatPage.tsx → ncp-chat-page.tsx} +7 -7
  101. package/src/components/chat/ncp/ncp-chat-thread.manager.ts +179 -41
  102. package/src/components/chat/ncp/ncp-session-adapter.test.ts +40 -2
  103. package/src/components/chat/ncp/ncp-session-adapter.ts +29 -0
  104. package/src/components/chat/ncp/page/ncp-chat-derived-state.ts +54 -11
  105. package/src/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view.ts +4 -0
  106. package/src/components/chat/ncp/tests/ncp-chat-input.manager.test.ts +99 -0
  107. package/src/components/chat/ncp/tests/ncp-chat-thread.manager.test.ts +189 -0
  108. package/src/components/chat/presenter/chat-presenter-context.tsx +13 -2
  109. package/src/components/chat/session-header/chat-session-header-actions.test.tsx +26 -0
  110. package/src/components/chat/session-header/chat-session-header-actions.tsx +19 -1
  111. package/src/components/chat/stores/chat-session-list.store.ts +25 -54
  112. package/src/components/chat/stores/chat-thread.store.ts +24 -0
  113. package/src/components/common/ProviderScopedModelInput.tsx +12 -2
  114. package/src/components/config/ModelConfig.test.tsx +108 -2
  115. package/src/components/config/RuntimeConfig.tsx +154 -7
  116. package/src/components/config/desktop-update-config.test.tsx +85 -0
  117. package/src/components/config/desktop-update-config.tsx +44 -3
  118. package/src/components/config/runtime-control-card.test.tsx +255 -0
  119. package/src/components/config/runtime-control-card.tsx +301 -0
  120. package/src/components/config/runtime-presence-card.test.tsx +154 -0
  121. package/src/components/config/runtime-presence-card.tsx +163 -0
  122. package/src/components/layout/AppLayout.tsx +1 -1
  123. package/src/components/providers/ThemeProvider.tsx +5 -0
  124. package/src/desktop/desktop-update.types.ts +25 -0
  125. package/src/desktop/managers/desktop-presence.manager.ts +91 -0
  126. package/src/desktop/managers/desktop-update.manager.ts +37 -1
  127. package/src/desktop/stores/desktop-presence.store.ts +18 -0
  128. package/src/desktop/stores/desktop-update.store.ts +7 -1
  129. package/src/hooks/server-path/use-server-path-read.ts +20 -0
  130. package/src/hooks/use-runtime-control.ts +24 -0
  131. package/src/lib/chat-message.ts +14 -3
  132. package/src/lib/desktop-update-labels.utils.ts +28 -2
  133. package/src/lib/i18n.chat.ts +12 -1
  134. package/src/lib/i18n.pwa.ts +62 -0
  135. package/src/lib/i18n.runtime-control.ts +120 -0
  136. package/src/lib/i18n.ts +4 -6
  137. package/src/main.tsx +1 -1
  138. package/src/pwa/components/pwa-install-entry.test.tsx +110 -0
  139. package/src/pwa/components/pwa-install-entry.tsx +205 -0
  140. package/src/pwa/managers/pwa-install.manager.test.ts +160 -0
  141. package/src/pwa/managers/pwa-install.manager.ts +232 -0
  142. package/src/pwa/managers/pwa-runtime.manager.ts +196 -0
  143. package/src/pwa/managers/pwa-shell-theme.manager.test.ts +30 -0
  144. package/src/pwa/managers/pwa-shell-theme.manager.ts +46 -0
  145. package/src/pwa/pwa-install-banner.storage.ts +55 -0
  146. package/src/pwa/pwa.types.ts +22 -0
  147. package/src/pwa/register-pwa.ts +14 -0
  148. package/src/pwa/stores/pwa.store.ts +17 -0
  149. package/src/runtime-control/runtime-control.manager.ts +118 -0
  150. package/src/vite-env.d.ts +9 -0
  151. package/dist/assets/ChannelsList-D8p4OlM6.js +0 -8
  152. package/dist/assets/ChatPage-A45t1Rmf.js +0 -58
  153. package/dist/assets/DocBrowser-B2MpsnU9.js +0 -1
  154. package/dist/assets/MarketplacePage-BNZ3Jx5d.js +0 -1
  155. package/dist/assets/McpMarketplacePage-CxPFOgxv.js +0 -40
  156. package/dist/assets/RemoteAccessPage-DyYVWsyK.js +0 -1
  157. package/dist/assets/RuntimeConfig-ChdfK4Y_.js +0 -1
  158. package/dist/assets/chat-session-display-CAlPrnlV.js +0 -1
  159. package/dist/assets/desktop-update-config-CfoVwf-w.js +0 -1
  160. package/dist/assets/i18n-C3jb83S6.js +0 -1
  161. package/dist/assets/index-CE4N7ItL.css +0 -1
  162. package/dist/assets/index-riX7Sg0_.js +0 -6
  163. package/dist/assets/loader-circle-BjMg63eu.js +0 -1
  164. package/dist/assets/plus-CIXME2pD.js +0 -1
  165. package/dist/assets/search-B_Qr0f6C.js +0 -1
  166. package/dist/assets/skeleton-CYQJazv6.js +0 -1
  167. package/dist/assets/x-B8Tho_xC.js +0 -1
  168. package/src/components/chat/ChatConversationPanel.tsx +0 -256
  169. package/src/components/chat/chat-child-session-panel.tsx +0 -262
  170. /package/dist/assets/{config-hints-GSUMvmSo.js → config-hints-BhTmc9P1.js} +0 -0
  171. /package/dist/assets/{config-layout-CgBMG7OL.js → config-layout-CHs0mAaR.js} +0 -0
@@ -1,8 +1,8 @@
1
1
  import { render, screen } from "@testing-library/react";
2
2
  import userEvent from "@testing-library/user-event";
3
3
  import { beforeEach, describe, expect, it, vi } from "vitest";
4
- import { ChatChildSessionPanel } from "@/components/chat/chat-child-session-panel";
5
- import { ChatConversationPanel } from "@/components/chat/ChatConversationPanel";
4
+ import { ChatConversationPanel } from "@/components/chat/chat-conversation-panel";
5
+ import { ChatSessionWorkspacePanel } from "@/components/chat/chat-session-workspace-panel";
6
6
  import type { ResolvedChildSessionTab } from "@/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view";
7
7
  import { useChatInputStore } from "@/components/chat/stores/chat-input.store";
8
8
  import { useChatSessionListStore } from "@/components/chat/stores/chat-session-list.store";
@@ -24,6 +24,8 @@ const mocks = vi.hoisted(() => ({
24
24
  title: "北京天气",
25
25
  agentId: "weather",
26
26
  updatedAt: "2026-04-10T09:00:00.000Z",
27
+ lastMessageAt: "2026-04-10T09:00:00.000Z",
28
+ readAt: "2026-04-10T09:00:00.000Z",
27
29
  sessionTypeLabel: "Codex",
28
30
  preferredModel: "openai/gpt-5.3-codex",
29
31
  projectName: "project-alpha",
@@ -49,6 +51,14 @@ vi.mock("@/components/chat/containers/chat-message-list.container", () => ({
49
51
  ChatMessageListContainer: () => <div data-testid="child-chat-message-list" />,
50
52
  }));
51
53
 
54
+ vi.mock("@/components/chat/chat-session-workspace-file-preview", () => ({
55
+ ChatSessionWorkspaceFilePreview: ({
56
+ file,
57
+ }: {
58
+ file: { path: string };
59
+ }) => <div data-testid="workspace-file-preview">{file.path}</div>,
60
+ }));
61
+
52
62
  vi.mock("@/components/chat/ChatWelcome", () => ({
53
63
  ChatWelcome: ({
54
64
  onCreateSession,
@@ -73,9 +83,13 @@ vi.mock("@/components/chat/presenter/chat-presenter-context", () => ({
73
83
  chatThreadManager: {
74
84
  deleteSession: mocks.deleteSession,
75
85
  goToProviders: mocks.goToProviders,
86
+ openChildSessionPanel: vi.fn(),
87
+ openFilePreview: vi.fn(),
76
88
  openSessionFromToolAction: vi.fn(),
77
89
  selectChildSessionDetail: vi.fn(),
78
- closeChildSessionDetail: vi.fn(),
90
+ selectWorkspaceFile: vi.fn(),
91
+ closeWorkspaceFile: vi.fn(),
92
+ closeWorkspacePanel: vi.fn(),
79
93
  goToParentSession: vi.fn(),
80
94
  },
81
95
  chatSessionListManager: {
@@ -84,17 +98,14 @@ vi.mock("@/components/chat/presenter/chat-presenter-context", () => ({
84
98
  setSelectedAgentId: mocks.setSelectedAgentId,
85
99
  markSessionRead: (
86
100
  sessionKey: string | null | undefined,
87
- updatedAt: string | null | undefined,
101
+ readAt: string | null | undefined,
88
102
  ) =>
89
103
  sessionKey
90
104
  ? useChatSessionListStore.getState().markSessionRead(
91
105
  sessionKey,
92
- updatedAt,
106
+ readAt,
93
107
  )
94
108
  : undefined,
95
- hydrateReadWatermarks: (
96
- entries: readonly { sessionKey: string; updatedAt: string | null | undefined }[],
97
- ) => useChatSessionListStore.getState().hydrateReadWatermarks(entries),
98
109
  },
99
110
  chatInputManager: {
100
111
  setPendingSessionType: mocks.setPendingSessionType,
@@ -183,17 +194,19 @@ describe("ChatConversationPanel", () => {
183
194
  isAwaitingAssistantOutput: false,
184
195
  parentSessionKey: null,
185
196
  parentSessionLabel: null,
197
+ workspacePanelParentKey: null,
186
198
  availableAgents: [
187
199
  { id: "main", displayName: "Main", runtime: "native" },
188
200
  { id: "engineer", displayName: "Engineer", runtime: "codex" },
189
201
  ],
190
202
  childSessionTabs: [],
191
203
  activeChildSessionKey: null,
204
+ workspaceFileTabs: [],
205
+ activeWorkspaceFileKey: null,
192
206
  },
193
207
  });
194
208
  useChatSessionListStore.setState({
195
- readUpdatedAtBySessionKey: {},
196
- hasHydratedReadWatermarks: false,
209
+ optimisticReadAtBySessionKey: {},
197
210
  snapshot: {
198
211
  ...useChatSessionListStore.getState().snapshot,
199
212
  },
@@ -256,6 +269,50 @@ describe("ChatConversationPanel", () => {
256
269
  expect(screen.queryByText("Engineer")).toBeNull();
257
270
  });
258
271
 
272
+ it("keeps the message area clean while a session history is hydrating", () => {
273
+ useChatThreadStore.setState({
274
+ snapshot: {
275
+ ...useChatThreadStore.getState().snapshot,
276
+ sessionKey: "session-1",
277
+ canDeleteSession: true,
278
+ isHistoryLoading: true,
279
+ messages: [],
280
+ },
281
+ });
282
+
283
+ render(<ChatConversationPanel />);
284
+
285
+ expect(
286
+ screen.queryByRole("status", { name: "Loading session history..." }),
287
+ ).toBeNull();
288
+ expect(screen.queryByText("No messages yet. Send one to start.")).toBeNull();
289
+ });
290
+
291
+ it("does not auto-open the child-session panel until the panel is explicitly opened", () => {
292
+ useChatThreadStore.setState({
293
+ snapshot: {
294
+ ...useChatThreadStore.getState().snapshot,
295
+ sessionKey: "parent-session-1",
296
+ sessionDisplayName: "Parent Session",
297
+ canDeleteSession: true,
298
+ childSessionTabs: [
299
+ {
300
+ sessionKey: "child-session-1",
301
+ parentSessionKey: "parent-session-1",
302
+ label: "北京天气",
303
+ agentId: "weather",
304
+ },
305
+ ],
306
+ activeChildSessionKey: "child-session-1",
307
+ workspacePanelParentKey: null,
308
+ },
309
+ });
310
+
311
+ render(<ChatConversationPanel />);
312
+
313
+ expect(screen.queryByLabelText("Close child session panel")).toBeNull();
314
+ });
315
+
259
316
  it("creates a draft session with the selected draft agent runtime", async () => {
260
317
  const user = userEvent.setup();
261
318
 
@@ -290,8 +347,8 @@ describe("ChatConversationPanel", () => {
290
347
  });
291
348
  });
292
349
 
293
- describe("ChatChildSessionPanel", () => {
294
- it("keeps the header compact for a single child session", () => {
350
+ describe("ChatSessionWorkspacePanel", () => {
351
+ it("renders child session tabs and active child metadata in the workspace sidebar", () => {
295
352
  mocks.resolvedChildTabs = [
296
353
  {
297
354
  sessionKey: "child-session-1",
@@ -299,15 +356,18 @@ describe("ChatChildSessionPanel", () => {
299
356
  title: "北京天气",
300
357
  agentId: "weather",
301
358
  updatedAt: "2026-04-10T09:00:00.000Z",
359
+ lastMessageAt: "2026-04-10T09:00:00.000Z",
360
+ readAt: "2026-04-10T09:00:00.000Z",
302
361
  sessionTypeLabel: "Codex",
303
362
  preferredModel: "openai/gpt-5.3-codex",
304
363
  projectName: "project-alpha",
305
364
  projectRoot: "/Users/demo/project-alpha",
306
365
  },
307
366
  ];
367
+
308
368
  render(
309
- <ChatChildSessionPanel
310
- tabs={[
369
+ <ChatSessionWorkspacePanel
370
+ childSessionTabs={[
311
371
  {
312
372
  sessionKey: "child-session-1",
313
373
  parentSessionKey: "parent-session-1",
@@ -315,20 +375,26 @@ describe("ChatChildSessionPanel", () => {
315
375
  agentId: "weather",
316
376
  },
317
377
  ]}
318
- activeSessionKey="child-session-1"
378
+ activeChildSessionKey="child-session-1"
379
+ workspaceFileTabs={[]}
380
+ activeWorkspaceFileKey={null}
381
+ sessionProjectRoot="/Users/demo/project-alpha"
319
382
  onSelectSession={vi.fn()}
383
+ onSelectFile={vi.fn()}
384
+ onCloseFile={vi.fn()}
320
385
  onClose={vi.fn()}
321
386
  onBackToParent={vi.fn()}
387
+ onFileOpen={vi.fn()}
322
388
  />,
323
389
  );
324
390
 
325
- expect(screen.getByText("北京天气")).toBeTruthy();
391
+ expect(screen.queryByText("Child sessions")).toBeNull();
392
+ expect(screen.getAllByText("北京天气")).toHaveLength(2);
326
393
  expect(screen.getByText("Codex")).toBeTruthy();
327
394
  expect(screen.getByText("openai/gpt-5.3-codex")).toBeTruthy();
328
395
  expect(screen.getByText("project-alpha")).toBeTruthy();
329
396
  expect(screen.getByText("/Users/demo/project-alpha")).toBeTruthy();
330
- expect(screen.queryByText("Child Sessions")).toBeNull();
331
- expect(screen.queryByText("child-session-1")).toBeNull();
397
+ expect(screen.getByText("No child session messages yet.")).toBeTruthy();
332
398
  expect(mocks.stickyBottomScroll).toHaveBeenCalledWith(
333
399
  expect.objectContaining({
334
400
  resetKey: "child-session-1",
@@ -337,7 +403,7 @@ describe("ChatChildSessionPanel", () => {
337
403
  );
338
404
  });
339
405
 
340
- it("uses tabs as the only title layer when multiple child sessions are open", () => {
406
+ it("shows unread state for inactive child session tabs", () => {
341
407
  mocks.resolvedChildTabs = [
342
408
  {
343
409
  sessionKey: "child-session-1",
@@ -345,6 +411,8 @@ describe("ChatChildSessionPanel", () => {
345
411
  title: "北京天气",
346
412
  agentId: "weather",
347
413
  updatedAt: "2026-04-10T09:00:00.000Z",
414
+ lastMessageAt: "2026-04-10T09:00:00.000Z",
415
+ readAt: "2026-04-10T09:00:00.000Z",
348
416
  sessionTypeLabel: "Codex",
349
417
  preferredModel: "openai/gpt-5.3-codex",
350
418
  projectName: "project-alpha",
@@ -356,6 +424,8 @@ describe("ChatChildSessionPanel", () => {
356
424
  title: "上海天气",
357
425
  agentId: "weather",
358
426
  updatedAt: "2026-04-10T09:05:00.000Z",
427
+ lastMessageAt: "2026-04-10T09:06:00.000Z",
428
+ readAt: "2026-04-10T09:05:00.000Z",
359
429
  sessionTypeLabel: "Claude Code",
360
430
  preferredModel: "anthropic/claude-sonnet-4",
361
431
  projectName: "project-beta",
@@ -364,8 +434,8 @@ describe("ChatChildSessionPanel", () => {
364
434
  ];
365
435
 
366
436
  render(
367
- <ChatChildSessionPanel
368
- tabs={[
437
+ <ChatSessionWorkspacePanel
438
+ childSessionTabs={[
369
439
  {
370
440
  sessionKey: "child-session-1",
371
441
  parentSessionKey: "parent-session-1",
@@ -379,158 +449,51 @@ describe("ChatChildSessionPanel", () => {
379
449
  agentId: "weather",
380
450
  },
381
451
  ]}
382
- activeSessionKey="child-session-1"
452
+ activeChildSessionKey="child-session-1"
453
+ workspaceFileTabs={[]}
454
+ activeWorkspaceFileKey={null}
455
+ sessionProjectRoot="/Users/demo/project-alpha"
383
456
  onSelectSession={vi.fn()}
457
+ onSelectFile={vi.fn()}
458
+ onCloseFile={vi.fn()}
384
459
  onClose={vi.fn()}
385
460
  onBackToParent={vi.fn()}
461
+ onFileOpen={vi.fn()}
386
462
  />,
387
463
  );
388
464
 
389
- expect(screen.getAllByText("北京天气")).toHaveLength(1);
390
- expect(screen.getByText("上海天气")).toBeTruthy();
391
- expect(screen.getByText("Codex")).toBeTruthy();
392
- expect(screen.getByText("openai/gpt-5.3-codex")).toBeTruthy();
393
- expect(screen.getByText("project-alpha")).toBeTruthy();
394
- expect(screen.getByText("/Users/demo/project-alpha")).toBeTruthy();
395
- const tabButtons = screen
396
- .getAllByRole("button")
397
- .filter((element) => element.getAttribute("aria-pressed") !== null);
398
- expect(tabButtons).toHaveLength(2);
399
- expect(tabButtons[0]?.getAttribute("aria-pressed")).toBe("true");
400
- expect(tabButtons[1]?.getAttribute("aria-pressed")).toBe("false");
465
+ expect(screen.getByLabelText("Session has unread updates")).toBeTruthy();
401
466
  });
402
467
 
403
- it("shows an unread dot for inactive child tabs until the user opens them", () => {
404
- mocks.resolvedChildTabs = [
405
- {
406
- sessionKey: "child-session-1",
407
- parentSessionKey: "parent-session-1",
408
- title: "北京天气",
409
- agentId: "weather",
410
- updatedAt: "2026-04-10T09:00:00.000Z",
411
- sessionTypeLabel: "Codex",
412
- preferredModel: "openai/gpt-5.3-codex",
413
- projectName: "project-alpha",
414
- projectRoot: "/Users/demo/project-alpha",
415
- },
416
- {
417
- sessionKey: "child-session-2",
418
- parentSessionKey: "parent-session-1",
419
- title: "上海天气",
420
- agentId: "weather",
421
- updatedAt: "2026-04-10T09:05:00.000Z",
422
- runStatus: "running",
423
- sessionTypeLabel: "Claude Code",
424
- preferredModel: "anthropic/claude-sonnet-4",
425
- projectName: "project-beta",
426
- projectRoot: "/Users/demo/project-beta",
427
- },
428
- ];
429
-
430
- const { rerender } = render(
431
- <ChatChildSessionPanel
432
- tabs={[
433
- {
434
- sessionKey: "child-session-1",
435
- parentSessionKey: "parent-session-1",
436
- label: "北京天气",
437
- agentId: "weather",
438
- },
439
- {
440
- sessionKey: "child-session-2",
441
- parentSessionKey: "parent-session-1",
442
- label: "上海天气",
443
- agentId: "weather",
444
- },
445
- ]}
446
- activeSessionKey="child-session-1"
447
- onSelectSession={vi.fn()}
448
- onClose={vi.fn()}
449
- onBackToParent={vi.fn()}
450
- />,
451
- );
452
-
453
- expect(
454
- screen.queryByLabelText("Session has unread updates"),
455
- ).toBeNull();
456
-
457
- mocks.resolvedChildTabs = [
458
- {
459
- sessionKey: "child-session-1",
460
- parentSessionKey: "parent-session-1",
461
- title: "北京天气",
462
- agentId: "weather",
463
- updatedAt: "2026-04-10T09:00:00.000Z",
464
- sessionTypeLabel: "Codex",
465
- preferredModel: "openai/gpt-5.3-codex",
466
- projectName: "project-alpha",
467
- projectRoot: "/Users/demo/project-alpha",
468
- },
469
- {
470
- sessionKey: "child-session-2",
471
- parentSessionKey: "parent-session-1",
472
- title: "上海天气",
473
- agentId: "weather",
474
- updatedAt: "2026-04-10T09:05:00.000Z",
475
- sessionTypeLabel: "Claude Code",
476
- preferredModel: "anthropic/claude-sonnet-4",
477
- projectName: "project-beta",
478
- projectRoot: "/Users/demo/project-beta",
479
- },
480
- ];
481
-
482
- rerender(
483
- <ChatChildSessionPanel
484
- tabs={[
485
- {
486
- sessionKey: "child-session-1",
487
- parentSessionKey: "parent-session-1",
488
- label: "北京天气",
489
- agentId: "weather",
490
- },
468
+ it("shows opened files as top tabs and renders the file preview pane", () => {
469
+ render(
470
+ <ChatSessionWorkspacePanel
471
+ childSessionTabs={[]}
472
+ activeChildSessionKey={null}
473
+ workspaceFileTabs={[
491
474
  {
492
- sessionKey: "child-session-2",
475
+ key: "parent-session-1::preview::README.md",
493
476
  parentSessionKey: "parent-session-1",
494
- label: "上海天气",
495
- agentId: "weather",
477
+ path: "README.md",
478
+ label: "README.md",
479
+ viewMode: "preview",
496
480
  },
497
481
  ]}
498
- activeSessionKey="child-session-1"
482
+ activeWorkspaceFileKey="parent-session-1::preview::README.md"
483
+ sessionProjectRoot="/Users/demo/project-alpha"
499
484
  onSelectSession={vi.fn()}
485
+ onSelectFile={vi.fn()}
486
+ onCloseFile={vi.fn()}
500
487
  onClose={vi.fn()}
501
488
  onBackToParent={vi.fn()}
489
+ onFileOpen={vi.fn()}
502
490
  />,
503
491
  );
504
492
 
505
- expect(
506
- screen.getByLabelText("Session has unread updates"),
507
- ).toBeTruthy();
508
-
509
- rerender(
510
- <ChatChildSessionPanel
511
- tabs={[
512
- {
513
- sessionKey: "child-session-1",
514
- parentSessionKey: "parent-session-1",
515
- label: "北京天气",
516
- agentId: "weather",
517
- },
518
- {
519
- sessionKey: "child-session-2",
520
- parentSessionKey: "parent-session-1",
521
- label: "上海天气",
522
- agentId: "weather",
523
- },
524
- ]}
525
- activeSessionKey="child-session-2"
526
- onSelectSession={vi.fn()}
527
- onClose={vi.fn()}
528
- onBackToParent={vi.fn()}
529
- />,
493
+ expect(screen.queryByText("Open files")).toBeNull();
494
+ expect(screen.getAllByText("README.md").length).toBeGreaterThan(0);
495
+ expect(screen.getByTestId("workspace-file-preview").textContent).toBe(
496
+ "README.md",
530
497
  );
531
-
532
- expect(
533
- screen.queryByLabelText("Session has unread updates"),
534
- ).toBeNull();
535
498
  });
536
499
  });