@nextclaw/ui 0.12.8 → 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 (142) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/assets/ChannelsList-Ita2Zm1_.js +8 -0
  3. package/dist/assets/{DocBrowser-BMxf9CIK.js → DocBrowser-6ReNjvzF.js} +1 -1
  4. package/dist/assets/DocBrowser-BNwbPHf4.js +1 -0
  5. package/dist/assets/{DocBrowserContext-Ce28gRXt.js → DocBrowserContext-B6SpA7Qs.js} +1 -1
  6. package/dist/assets/{LogoBadge-o92MOA2L.js → LogoBadge-ByNLYg65.js} +1 -1
  7. package/dist/assets/MarketplacePage-CjX2MWww.js +1 -0
  8. package/dist/assets/{MarketplacePage-BySqkYDh.js → MarketplacePage-D0sDlYX4.js} +1 -1
  9. package/dist/assets/McpMarketplacePage-BGKJm1sJ.js +40 -0
  10. package/dist/assets/{ModelConfig-IrmzoslW.js → ModelConfig-BzZenCH-.js} +1 -1
  11. package/dist/assets/{ProviderScopedModelInput-CmTIzgI7.js → ProviderScopedModelInput-Da7khnBA.js} +1 -1
  12. package/dist/assets/{ProvidersList-8_Kalfwl.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-DNBR-UbE.js → SearchConfig-BGkzXQP-.js} +1 -1
  16. package/dist/assets/{SecretsConfig-Ba1RPJaG.js → SecretsConfig-D281Rotl.js} +2 -2
  17. package/dist/assets/{SessionsConfig-Doqp5ghH.js → SessionsConfig-ChHQ7M5c.js} +2 -2
  18. package/dist/assets/{app-query-client-DniXoIN5.js → app-query-client-VnFElj4E.js} +1 -1
  19. package/dist/assets/{book-open-DocgeQtR.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-BvKvh1R8.js → chunk-JZWAC4HX-DK5HPmIK.js} +1 -1
  23. package/dist/assets/{client-CVqPF5ie.js → client-_i4MU2bB.js} +1 -1
  24. package/dist/assets/{config-Bop2oB18.js → config-DtIQwrHF.js} +1 -1
  25. package/dist/assets/{createLucideIcon-DVv8taGY.js → createLucideIcon-BSeTgkZW.js} +1 -1
  26. package/dist/assets/desktop-update-config-Dpcf4BKG.js +1 -0
  27. package/dist/assets/{dist-Da5Gm_pO.js → dist-6TrrnPCR.js} +1 -1
  28. package/dist/assets/{dist-DmAlInRu.js → dist-ccBFUi-o.js} +1 -1
  29. package/dist/assets/download-BhDxnyvU.js +1 -0
  30. package/dist/assets/{external-link-DFjw3x1B.js → external-link-BgErLCNT.js} +1 -1
  31. package/dist/assets/{hash-DJtaCejM.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-DHSEQ3OH.js → infiniteQueryBehavior-ZDS92Qpp.js} +1 -1
  36. package/dist/assets/loader-circle-ACM1s51e.js +1 -0
  37. package/dist/assets/{logos-DEFUIR12.js → logos-x89HbrZ4.js} +1 -1
  38. package/dist/assets/{page-layout-Da3i3r6G.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-C_mWOFzI.js → popover-Bg1VoTZ6.js} +1 -1
  42. package/dist/assets/{refresh-ccw-D6HkNtfz.js → refresh-ccw-DT98i__E.js} +1 -1
  43. package/dist/assets/{refresh-cw-DRcvRrnc.js → refresh-cw-C47QSEwg.js} +1 -1
  44. package/dist/assets/{rotate-cw-BmDKfXtH.js → rotate-cw-JtFzpNn6.js} +1 -1
  45. package/dist/assets/{save-DHGmi2e9.js → save-3S6-H3Xw.js} +1 -1
  46. package/dist/assets/search-3kFR_zh9.js +1 -0
  47. package/dist/assets/{security-config-CbXfPZzr.js → security-config-BWaiARNk.js} +1 -1
  48. package/dist/assets/{select-Caud8QvU.js → select-DJ2MUjBB.js} +1 -1
  49. package/dist/assets/skeleton-ByQepn0M.js +1 -0
  50. package/dist/assets/{status-dot-DurKKSwA.js → status-dot-vbanNPFU.js} +1 -1
  51. package/dist/assets/{switch-0rmPBRKI.js → switch-BsLtHOH-.js} +1 -1
  52. package/dist/assets/{tabs-custom-5JLVL6v8.js → tabs-custom-D3HYMt6k.js} +1 -1
  53. package/dist/assets/{trash-2-C6caKPoz.js → trash-2-G48scll7.js} +1 -1
  54. package/dist/assets/{use-infinite-scroll-loader-dwnaa_qi.js → use-infinite-scroll-loader-DkNhD-42.js} +1 -1
  55. package/dist/assets/{useConfirmDialog-mMeWD_yo.js → useConfirmDialog-BkvTN-vd.js} +1 -1
  56. package/dist/assets/{useMutation-BmxxvCNf.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/server-path.ts +27 -4
  72. package/src/api/types.ts +17 -10
  73. package/src/app.tsx +9 -0
  74. package/src/components/chat/ChatSidebar.test.tsx +43 -1
  75. package/src/components/chat/ChatSidebar.tsx +24 -0
  76. package/src/components/chat/adapters/chat-message.summary-truncation.test.ts +66 -0
  77. package/src/components/chat/adapters/file-operation/card.ts +9 -0
  78. package/src/components/chat/adapters/file-operation/diff.ts +14 -0
  79. package/src/components/chat/{ChatConversationPanel.test.tsx → chat-conversation-panel.test.tsx} +107 -206
  80. package/src/components/chat/chat-conversation-panel.tsx +412 -0
  81. package/src/components/chat/chat-page-shell.tsx +1 -1
  82. package/src/components/chat/chat-session-workspace-file-preview.test.tsx +91 -0
  83. package/src/components/chat/chat-session-workspace-file-preview.tsx +307 -0
  84. package/src/components/chat/chat-session-workspace-panel-nav.tsx +197 -0
  85. package/src/components/chat/chat-session-workspace-panel.tsx +318 -0
  86. package/src/components/chat/chat-sidebar-session-item.tsx +32 -2
  87. package/src/components/chat/containers/chat-message-list.container.test.tsx +49 -0
  88. package/src/components/chat/containers/chat-message-list.container.tsx +4 -0
  89. package/src/components/chat/managers/chat-session-list.manager.test.ts +12 -0
  90. package/src/components/chat/managers/chat-session-list.manager.ts +7 -0
  91. package/src/components/chat/ncp/ncp-chat-page.tsx +7 -7
  92. package/src/components/chat/ncp/ncp-chat-thread.manager.ts +179 -41
  93. package/src/components/chat/ncp/ncp-session-adapter.test.ts +35 -1
  94. package/src/components/chat/ncp/ncp-session-adapter.ts +17 -0
  95. package/src/components/chat/ncp/page/ncp-chat-derived-state.ts +54 -11
  96. package/src/components/chat/ncp/tests/ncp-chat-thread.manager.test.ts +189 -0
  97. package/src/components/chat/presenter/chat-presenter-context.tsx +13 -2
  98. package/src/components/chat/session-header/chat-session-header-actions.test.tsx +26 -0
  99. package/src/components/chat/session-header/chat-session-header-actions.tsx +19 -1
  100. package/src/components/chat/stores/chat-thread.store.ts +24 -0
  101. package/src/components/config/RuntimeConfig.tsx +141 -2
  102. package/src/components/layout/AppLayout.tsx +1 -1
  103. package/src/components/providers/ThemeProvider.tsx +5 -0
  104. package/src/hooks/server-path/use-server-path-read.ts +20 -0
  105. package/src/lib/chat-message.ts +14 -3
  106. package/src/lib/i18n.chat.ts +12 -1
  107. package/src/lib/i18n.pwa.ts +62 -0
  108. package/src/lib/i18n.ts +2 -2
  109. package/src/pwa/components/pwa-install-entry.test.tsx +110 -0
  110. package/src/pwa/components/pwa-install-entry.tsx +205 -0
  111. package/src/pwa/managers/pwa-install.manager.test.ts +160 -0
  112. package/src/pwa/managers/pwa-install.manager.ts +232 -0
  113. package/src/pwa/managers/pwa-runtime.manager.ts +196 -0
  114. package/src/pwa/managers/pwa-shell-theme.manager.test.ts +30 -0
  115. package/src/pwa/managers/pwa-shell-theme.manager.ts +46 -0
  116. package/src/pwa/pwa-install-banner.storage.ts +55 -0
  117. package/src/pwa/pwa.types.ts +22 -0
  118. package/src/pwa/register-pwa.ts +14 -0
  119. package/src/pwa/stores/pwa.store.ts +17 -0
  120. package/src/vite-env.d.ts +9 -0
  121. package/dist/assets/ChannelsList-KIQIxluX.js +0 -8
  122. package/dist/assets/DocBrowser-CyDgAtO9.js +0 -1
  123. package/dist/assets/MarketplacePage-C0olZaek.js +0 -1
  124. package/dist/assets/McpMarketplacePage-DqKaiXO9.js +0 -40
  125. package/dist/assets/RemoteAccessPage-CyQlSjPf.js +0 -1
  126. package/dist/assets/RuntimeConfig-Bk0uYBhf.js +0 -1
  127. package/dist/assets/chat-page-Bph8M5zo.js +0 -58
  128. package/dist/assets/chat-session-display-CoN3Wmn-.js +0 -1
  129. package/dist/assets/desktop-update-config-1KBrqLBC.js +0 -1
  130. package/dist/assets/i18n-CwHZ-9vt.js +0 -1
  131. package/dist/assets/index-DafCdM4F.css +0 -1
  132. package/dist/assets/index-DdksE6U3.js +0 -6
  133. package/dist/assets/loader-circle-PsSP0H9n.js +0 -1
  134. package/dist/assets/play-DBQbBxTA.js +0 -1
  135. package/dist/assets/plus-DUOVbsyQ.js +0 -1
  136. package/dist/assets/search-MChQRYR1.js +0 -1
  137. package/dist/assets/skeleton-B-4vRq_Z.js +0 -1
  138. package/dist/assets/x-DuMhMATD.js +0 -1
  139. package/src/components/chat/ChatConversationPanel.tsx +0 -256
  140. package/src/components/chat/chat-child-session-panel.tsx +0 -270
  141. /package/dist/assets/{config-hints-BZoDjXye.js → config-hints-BhTmc9P1.js} +0 -0
  142. /package/dist/assets/{config-layout-DmlGaay2.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";
@@ -51,6 +51,14 @@ vi.mock("@/components/chat/containers/chat-message-list.container", () => ({
51
51
  ChatMessageListContainer: () => <div data-testid="child-chat-message-list" />,
52
52
  }));
53
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
+
54
62
  vi.mock("@/components/chat/ChatWelcome", () => ({
55
63
  ChatWelcome: ({
56
64
  onCreateSession,
@@ -75,9 +83,13 @@ vi.mock("@/components/chat/presenter/chat-presenter-context", () => ({
75
83
  chatThreadManager: {
76
84
  deleteSession: mocks.deleteSession,
77
85
  goToProviders: mocks.goToProviders,
86
+ openChildSessionPanel: vi.fn(),
87
+ openFilePreview: vi.fn(),
78
88
  openSessionFromToolAction: vi.fn(),
79
89
  selectChildSessionDetail: vi.fn(),
80
- closeChildSessionDetail: vi.fn(),
90
+ selectWorkspaceFile: vi.fn(),
91
+ closeWorkspaceFile: vi.fn(),
92
+ closeWorkspacePanel: vi.fn(),
81
93
  goToParentSession: vi.fn(),
82
94
  },
83
95
  chatSessionListManager: {
@@ -182,12 +194,15 @@ describe("ChatConversationPanel", () => {
182
194
  isAwaitingAssistantOutput: false,
183
195
  parentSessionKey: null,
184
196
  parentSessionLabel: null,
197
+ workspacePanelParentKey: null,
185
198
  availableAgents: [
186
199
  { id: "main", displayName: "Main", runtime: "native" },
187
200
  { id: "engineer", displayName: "Engineer", runtime: "codex" },
188
201
  ],
189
202
  childSessionTabs: [],
190
203
  activeChildSessionKey: null,
204
+ workspaceFileTabs: [],
205
+ activeWorkspaceFileKey: null,
191
206
  },
192
207
  });
193
208
  useChatSessionListStore.setState({
@@ -254,6 +269,50 @@ describe("ChatConversationPanel", () => {
254
269
  expect(screen.queryByText("Engineer")).toBeNull();
255
270
  });
256
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
+
257
316
  it("creates a draft session with the selected draft agent runtime", async () => {
258
317
  const user = userEvent.setup();
259
318
 
@@ -288,8 +347,8 @@ describe("ChatConversationPanel", () => {
288
347
  });
289
348
  });
290
349
 
291
- describe("ChatChildSessionPanel", () => {
292
- 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", () => {
293
352
  mocks.resolvedChildTabs = [
294
353
  {
295
354
  sessionKey: "child-session-1",
@@ -305,9 +364,10 @@ describe("ChatChildSessionPanel", () => {
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,129 +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", () => {
341
- mocks.resolvedChildTabs = [
342
- {
343
- sessionKey: "child-session-1",
344
- parentSessionKey: "parent-session-1",
345
- title: "北京天气",
346
- agentId: "weather",
347
- updatedAt: "2026-04-10T09:00:00.000Z",
348
- lastMessageAt: "2026-04-10T09:00:00.000Z",
349
- readAt: "2026-04-10T09:00:00.000Z",
350
- sessionTypeLabel: "Codex",
351
- preferredModel: "openai/gpt-5.3-codex",
352
- projectName: "project-alpha",
353
- projectRoot: "/Users/demo/project-alpha",
354
- },
355
- {
356
- sessionKey: "child-session-2",
357
- parentSessionKey: "parent-session-1",
358
- title: "上海天气",
359
- agentId: "weather",
360
- updatedAt: "2026-04-10T09:05:00.000Z",
361
- lastMessageAt: "2026-04-10T09:05:00.000Z",
362
- readAt: "2026-04-10T09:05:00.000Z",
363
- sessionTypeLabel: "Claude Code",
364
- preferredModel: "anthropic/claude-sonnet-4",
365
- projectName: "project-beta",
366
- projectRoot: "/Users/demo/project-beta",
367
- },
368
- ];
369
-
370
- render(
371
- <ChatChildSessionPanel
372
- tabs={[
373
- {
374
- sessionKey: "child-session-1",
375
- parentSessionKey: "parent-session-1",
376
- label: "北京天气",
377
- agentId: "weather",
378
- },
379
- {
380
- sessionKey: "child-session-2",
381
- parentSessionKey: "parent-session-1",
382
- label: "上海天气",
383
- agentId: "weather",
384
- },
385
- ]}
386
- activeSessionKey="child-session-1"
387
- onSelectSession={vi.fn()}
388
- onClose={vi.fn()}
389
- onBackToParent={vi.fn()}
390
- />,
391
- );
392
-
393
- expect(screen.getAllByText("北京天气")).toHaveLength(1);
394
- expect(screen.getByText("上海天气")).toBeTruthy();
395
- expect(screen.getByText("Codex")).toBeTruthy();
396
- expect(screen.getByText("openai/gpt-5.3-codex")).toBeTruthy();
397
- expect(screen.getByText("project-alpha")).toBeTruthy();
398
- expect(screen.getByText("/Users/demo/project-alpha")).toBeTruthy();
399
- const tabButtons = screen
400
- .getAllByRole("button")
401
- .filter((element) => element.getAttribute("aria-pressed") !== null);
402
- expect(tabButtons).toHaveLength(2);
403
- expect(tabButtons[0]?.getAttribute("aria-pressed")).toBe("true");
404
- expect(tabButtons[1]?.getAttribute("aria-pressed")).toBe("false");
405
- });
406
-
407
- it("shows an unread dot for inactive child tabs until the user opens them", () => {
408
- mocks.resolvedChildTabs = [
409
- {
410
- sessionKey: "child-session-1",
411
- parentSessionKey: "parent-session-1",
412
- title: "北京天气",
413
- agentId: "weather",
414
- updatedAt: "2026-04-10T09:00:00.000Z",
415
- lastMessageAt: "2026-04-10T09:00:00.000Z",
416
- readAt: "2026-04-10T09:00:00.000Z",
417
- sessionTypeLabel: "Codex",
418
- preferredModel: "openai/gpt-5.3-codex",
419
- projectName: "project-alpha",
420
- projectRoot: "/Users/demo/project-alpha",
421
- },
422
- {
423
- sessionKey: "child-session-2",
424
- parentSessionKey: "parent-session-1",
425
- title: "上海天气",
426
- agentId: "weather",
427
- updatedAt: "2026-04-10T09:05:00.000Z",
428
- lastMessageAt: "2026-04-10T09:05:00.000Z",
429
- readAt: "2026-04-10T09:05:00.000Z",
430
- runStatus: "running",
431
- sessionTypeLabel: "Claude Code",
432
- preferredModel: "anthropic/claude-sonnet-4",
433
- projectName: "project-beta",
434
- projectRoot: "/Users/demo/project-beta",
435
- },
436
- ];
437
-
438
- const { rerender } = render(
439
- <ChatChildSessionPanel
440
- tabs={[
441
- {
442
- sessionKey: "child-session-1",
443
- parentSessionKey: "parent-session-1",
444
- label: "北京天气",
445
- agentId: "weather",
446
- },
447
- {
448
- sessionKey: "child-session-2",
449
- parentSessionKey: "parent-session-1",
450
- label: "上海天气",
451
- agentId: "weather",
452
- },
453
- ]}
454
- activeSessionKey="child-session-1"
455
- onSelectSession={vi.fn()}
456
- onClose={vi.fn()}
457
- onBackToParent={vi.fn()}
458
- />,
459
- );
460
-
461
- expect(screen.queryByLabelText("Session has unread updates")).toBeNull();
462
-
406
+ it("shows unread state for inactive child session tabs", () => {
463
407
  mocks.resolvedChildTabs = [
464
408
  {
465
409
  sessionKey: "child-session-1",
@@ -489,9 +433,9 @@ describe("ChatChildSessionPanel", () => {
489
433
  },
490
434
  ];
491
435
 
492
- rerender(
493
- <ChatChildSessionPanel
494
- tabs={[
436
+ render(
437
+ <ChatSessionWorkspacePanel
438
+ childSessionTabs={[
495
439
  {
496
440
  sessionKey: "child-session-1",
497
441
  parentSessionKey: "parent-session-1",
@@ -505,94 +449,51 @@ describe("ChatChildSessionPanel", () => {
505
449
  agentId: "weather",
506
450
  },
507
451
  ]}
508
- activeSessionKey="child-session-1"
452
+ activeChildSessionKey="child-session-1"
453
+ workspaceFileTabs={[]}
454
+ activeWorkspaceFileKey={null}
455
+ sessionProjectRoot="/Users/demo/project-alpha"
509
456
  onSelectSession={vi.fn()}
457
+ onSelectFile={vi.fn()}
458
+ onCloseFile={vi.fn()}
510
459
  onClose={vi.fn()}
511
460
  onBackToParent={vi.fn()}
461
+ onFileOpen={vi.fn()}
512
462
  />,
513
463
  );
514
464
 
515
465
  expect(screen.getByLabelText("Session has unread updates")).toBeTruthy();
516
-
517
- rerender(
518
- <ChatChildSessionPanel
519
- tabs={[
520
- {
521
- sessionKey: "child-session-1",
522
- parentSessionKey: "parent-session-1",
523
- label: "北京天气",
524
- agentId: "weather",
525
- },
526
- {
527
- sessionKey: "child-session-2",
528
- parentSessionKey: "parent-session-1",
529
- label: "上海天气",
530
- agentId: "weather",
531
- },
532
- ]}
533
- activeSessionKey="child-session-2"
534
- onSelectSession={vi.fn()}
535
- onClose={vi.fn()}
536
- onBackToParent={vi.fn()}
537
- />,
538
- );
539
-
540
- expect(screen.queryByLabelText("Session has unread updates")).toBeNull();
541
466
  });
542
467
 
543
- it("does not show an unread dot for child tabs without a persisted ui read baseline", () => {
544
- mocks.resolvedChildTabs = [
545
- {
546
- sessionKey: "child-session-1",
547
- parentSessionKey: "parent-session-1",
548
- title: "北京天气",
549
- agentId: "weather",
550
- updatedAt: "2026-04-10T09:00:00.000Z",
551
- lastMessageAt: "2026-04-10T09:00:00.000Z",
552
- readAt: null,
553
- sessionTypeLabel: "Codex",
554
- preferredModel: "openai/gpt-5.3-codex",
555
- projectName: "project-alpha",
556
- projectRoot: "/Users/demo/project-alpha",
557
- },
558
- {
559
- sessionKey: "child-session-2",
560
- parentSessionKey: "parent-session-1",
561
- title: "上海天气",
562
- agentId: "weather",
563
- updatedAt: "2026-04-10T09:05:00.000Z",
564
- lastMessageAt: "2026-04-10T09:06:00.000Z",
565
- readAt: null,
566
- sessionTypeLabel: "Claude Code",
567
- preferredModel: "anthropic/claude-sonnet-4",
568
- projectName: "project-beta",
569
- projectRoot: "/Users/demo/project-beta",
570
- },
571
- ];
572
-
468
+ it("shows opened files as top tabs and renders the file preview pane", () => {
573
469
  render(
574
- <ChatChildSessionPanel
575
- tabs={[
576
- {
577
- sessionKey: "child-session-1",
578
- parentSessionKey: "parent-session-1",
579
- label: "北京天气",
580
- agentId: "weather",
581
- },
470
+ <ChatSessionWorkspacePanel
471
+ childSessionTabs={[]}
472
+ activeChildSessionKey={null}
473
+ workspaceFileTabs={[
582
474
  {
583
- sessionKey: "child-session-2",
475
+ key: "parent-session-1::preview::README.md",
584
476
  parentSessionKey: "parent-session-1",
585
- label: "上海天气",
586
- agentId: "weather",
477
+ path: "README.md",
478
+ label: "README.md",
479
+ viewMode: "preview",
587
480
  },
588
481
  ]}
589
- activeSessionKey="child-session-1"
482
+ activeWorkspaceFileKey="parent-session-1::preview::README.md"
483
+ sessionProjectRoot="/Users/demo/project-alpha"
590
484
  onSelectSession={vi.fn()}
485
+ onSelectFile={vi.fn()}
486
+ onCloseFile={vi.fn()}
591
487
  onClose={vi.fn()}
592
488
  onBackToParent={vi.fn()}
489
+ onFileOpen={vi.fn()}
593
490
  />,
594
491
  );
595
492
 
596
- expect(screen.queryByLabelText("Session has unread updates")).toBeNull();
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",
497
+ );
597
498
  });
598
499
  });