@nextclaw/ui 0.12.4 → 0.12.6

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 (149) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/dist/assets/ChannelsList-D8p4OlM6.js +8 -0
  3. package/dist/assets/ChatPage-A45t1Rmf.js +58 -0
  4. package/dist/assets/DocBrowser-B2MpsnU9.js +1 -0
  5. package/dist/assets/{DocBrowser-NSzgVKka.js → DocBrowser-Cse_F8Nn.js} +1 -1
  6. package/dist/assets/{DocBrowserContext-DpgVdRgk.js → DocBrowserContext-Bai1WU2H.js} +1 -1
  7. package/dist/assets/{LogoBadge-CHS4YNLw.js → LogoBadge-BdxMPc9v.js} +1 -1
  8. package/dist/assets/MarketplacePage-BNZ3Jx5d.js +1 -0
  9. package/dist/assets/MarketplacePage-BbpAkllU.js +49 -0
  10. package/dist/assets/McpMarketplacePage-CxPFOgxv.js +40 -0
  11. package/dist/assets/ModelConfig-3GLqQ5GY.js +1 -0
  12. package/dist/assets/ProviderScopedModelInput-BYNouw-i.js +1 -0
  13. package/dist/assets/ProvidersList-BR1gJ4Dm.js +1 -0
  14. package/dist/assets/{RemoteAccessPage-yfbrveNQ.js → RemoteAccessPage-DyYVWsyK.js} +1 -1
  15. package/dist/assets/RuntimeConfig-ChdfK4Y_.js +1 -0
  16. package/dist/assets/SearchConfig-DTeJvp8m.js +1 -0
  17. package/dist/assets/{SecretsConfig-CLFSSoTl.js → SecretsConfig-CCYO6NcV.js} +2 -2
  18. package/dist/assets/SessionsConfig-Du39vDgt.js +2 -0
  19. package/dist/assets/app-query-client-Dr5d-K8d.js +1 -0
  20. package/dist/assets/{book-open-C7TAghTk.js → book-open-Da4OEPqB.js} +1 -1
  21. package/dist/assets/chat-session-display-CAlPrnlV.js +1 -0
  22. package/dist/assets/{chunk-JZWAC4HX-DbL4EmiT.js → chunk-JZWAC4HX-CoFVxHXV.js} +1 -1
  23. package/dist/assets/client-CSk58DcF.js +7 -0
  24. package/dist/assets/config-D8KzikVB.js +1 -0
  25. package/dist/assets/{createLucideIcon-BRLFtf-8.js → createLucideIcon-83gaZMtv.js} +1 -1
  26. package/dist/assets/desktop-update-config-CfoVwf-w.js +1 -0
  27. package/dist/assets/dist-aTmhMDVh.js +9 -0
  28. package/dist/assets/{dist-DP-JKR4G.js → dist-toEYs-MZ.js} +1 -1
  29. package/dist/assets/{external-link-BkJkiWbH.js → external-link-QQ0TC6X4.js} +1 -1
  30. package/dist/assets/{hash-CbP6-6R9.js → hash-DaFBEkmi.js} +1 -1
  31. package/dist/assets/i18n-C3jb83S6.js +1 -0
  32. package/dist/assets/index-CE4N7ItL.css +1 -0
  33. package/dist/assets/index-riX7Sg0_.js +6 -0
  34. package/dist/assets/infiniteQueryBehavior-BmHX_ayZ.js +1 -0
  35. package/dist/assets/loader-circle-BjMg63eu.js +1 -0
  36. package/dist/assets/{logos-N3dbS6-I.js → logos-Dzlz30M3.js} +1 -1
  37. package/dist/assets/{page-layout-DyuvlNrg.js → page-layout-D2eRufRQ.js} +1 -1
  38. package/dist/assets/plus-CIXME2pD.js +1 -0
  39. package/dist/assets/{popover-BKKWGUaG.js → popover-BSXxm5bj.js} +1 -1
  40. package/dist/assets/{refresh-ccw-BGMdiNGq.js → refresh-ccw-B3zMtN-_.js} +1 -1
  41. package/dist/assets/refresh-cw-DlZkIHnJ.js +1 -0
  42. package/dist/assets/{save-Dh4GQzzX.js → save-Us9fg4Sj.js} +1 -1
  43. package/dist/assets/search-B_Qr0f6C.js +1 -0
  44. package/dist/assets/security-config-BGWYwxNr.js +1 -0
  45. package/dist/assets/{select-BtIi5fnh.js → select-DLYqySQK.js} +1 -1
  46. package/dist/assets/skeleton-CYQJazv6.js +1 -0
  47. package/dist/assets/{status-dot-C4O-2jZP.js → status-dot-DGayudyB.js} +1 -1
  48. package/dist/assets/{switch-DPegGIa_.js → switch-Dz2ScsKx.js} +1 -1
  49. package/dist/assets/{tabs-custom-x5GZexrF.js → tabs-custom-CdKyjiGk.js} +1 -1
  50. package/dist/assets/{trash-2-CU3LYIpQ.js → trash-2-Db-mZOZs.js} +1 -1
  51. package/dist/assets/use-infinite-scroll-loader-DBJX5hj0.js +1 -0
  52. package/dist/assets/{useConfirmDialog-S5WsGOGf.js → useConfirmDialog-DL0a-oGC.js} +1 -1
  53. package/dist/assets/useMutation-BdZm-9PL.js +1 -0
  54. package/dist/assets/x-B8Tho_xC.js +1 -0
  55. package/dist/index.html +20 -18
  56. package/package.json +6 -6
  57. package/src/App.tsx +2 -0
  58. package/src/account/components/account-panel.tsx +46 -4
  59. package/src/account/managers/account.manager.ts +19 -4
  60. package/src/api/raw-client.test.ts +37 -0
  61. package/src/api/raw-client.ts +51 -8
  62. package/src/api/remote.ts +9 -0
  63. package/src/api/remote.types.ts +5 -0
  64. package/src/components/chat/ChatConversationPanel.test.tsx +344 -142
  65. package/src/components/chat/ChatSidebar.test.tsx +109 -4
  66. package/src/components/chat/ChatSidebar.tsx +62 -9
  67. package/src/components/chat/adapters/chat-message-tool-agent-id.test.ts +11 -11
  68. package/src/components/chat/adapters/chat-message.adapter.test.ts +43 -6
  69. package/src/components/chat/adapters/chat-message.session-request-tool-card.ts +182 -44
  70. package/src/components/chat/adapters/chat-message.session-spawn-tool-card.test.ts +104 -0
  71. package/src/components/chat/chat-child-session-panel.tsx +155 -59
  72. package/src/components/chat/chat-page-runtime.test.ts +16 -19
  73. package/src/components/chat/chat-session-preference-sync.test.ts +13 -0
  74. package/src/components/chat/chat-session-preference-sync.ts +9 -7
  75. package/src/components/chat/chat-sidebar-session-item.tsx +189 -121
  76. package/src/components/chat/containers/chat-message-list.container.test.tsx +21 -3
  77. package/src/components/chat/containers/chat-message-list.container.tsx +14 -0
  78. package/src/components/chat/hooks/use-chat-session-project.test.tsx +5 -5
  79. package/src/components/chat/hooks/use-chat-session-project.ts +0 -5
  80. package/src/components/chat/hooks/use-chat-session-update.test.tsx +75 -0
  81. package/src/components/chat/hooks/use-chat-session-update.ts +4 -2
  82. package/src/components/chat/managers/chat-session-list.manager.test.ts +79 -5
  83. package/src/components/chat/managers/chat-session-list.manager.ts +31 -4
  84. package/src/components/chat/ncp/NcpChatPage.tsx +32 -51
  85. package/src/components/chat/ncp/ncp-app-client-fetch.test.ts +1 -1
  86. package/src/components/chat/ncp/ncp-app-client-fetch.ts +45 -5
  87. package/src/components/chat/ncp/ncp-chat-input.manager.ts +3 -5
  88. package/src/components/chat/ncp/ncp-chat-page-data.ts +0 -1
  89. package/src/components/chat/ncp/ncp-chat.presenter.ts +1 -11
  90. package/src/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view.ts +35 -9
  91. package/src/components/chat/stores/chat-session-list.store.ts +99 -5
  92. package/src/components/chat/useChatSessionTypeState.test.tsx +0 -3
  93. package/src/components/chat/useChatSessionTypeState.ts +3 -5
  94. package/src/components/config/ChannelsList.test.tsx +68 -0
  95. package/src/components/config/ChannelsList.tsx +22 -4
  96. package/src/components/config/ProviderForm.tsx +9 -15
  97. package/src/components/config/ProvidersList.tsx +17 -3
  98. package/src/components/config/desktop-update-config.tsx +230 -0
  99. package/src/components/config/providers-list.test.tsx +68 -0
  100. package/src/components/layout/Sidebar.tsx +19 -14
  101. package/src/components/layout/sidebar.layout.test.tsx +33 -1
  102. package/src/components/marketplace/MarketplacePage.tsx +30 -30
  103. package/src/components/marketplace/marketplace-page-parts.tsx +16 -24
  104. package/src/components/marketplace/mcp/McpMarketplacePage.tsx +28 -26
  105. package/src/desktop/desktop-update.types.ts +36 -0
  106. package/src/desktop/managers/desktop-update.manager.ts +163 -0
  107. package/src/desktop/stores/desktop-update.store.ts +18 -0
  108. package/src/hooks/marketplace-list-pages.ts +27 -0
  109. package/src/hooks/use-infinite-scroll-loader.ts +88 -0
  110. package/src/hooks/useMarketplace.ts +14 -3
  111. package/src/hooks/useMcpMarketplace.ts +14 -3
  112. package/src/lib/desktop-update-labels.utils.ts +72 -0
  113. package/src/lib/i18n.chat.ts +13 -0
  114. package/src/lib/i18n.remote.ts +15 -0
  115. package/src/lib/i18n.ts +3 -9
  116. package/src/lib/ui-document-title.ts +1 -0
  117. package/src/transport/local.transport.ts +57 -18
  118. package/src/vite-env.d.ts +10 -0
  119. package/dist/assets/ChannelsList-CobWeI2V.js +0 -8
  120. package/dist/assets/ChatPage-ZIdFFVAv.js +0 -43
  121. package/dist/assets/DocBrowser-D55C0iyl.js +0 -1
  122. package/dist/assets/MarketplacePage-BFYsRss_.js +0 -49
  123. package/dist/assets/MarketplacePage-DII-q-Y1.js +0 -1
  124. package/dist/assets/McpMarketplacePage-CPqsGJzz.js +0 -40
  125. package/dist/assets/ModelConfig-Bvuo_IpS.js +0 -1
  126. package/dist/assets/ProviderScopedModelInput-BfY8rGsf.js +0 -1
  127. package/dist/assets/ProvidersList-3tlaqwSS.js +0 -1
  128. package/dist/assets/RuntimeConfig-CAd5Kta3.js +0 -1
  129. package/dist/assets/SearchConfig-DFwgaAa7.js +0 -1
  130. package/dist/assets/SessionsConfig-vYrvc2Fk.js +0 -2
  131. package/dist/assets/chat-session-display-5dVFkJyw.js +0 -1
  132. package/dist/assets/config-CMiW0yaK.js +0 -1
  133. package/dist/assets/dist-BFc_H-lY.js +0 -15
  134. package/dist/assets/i18n-C_2dKw6w.js +0 -1
  135. package/dist/assets/index-ChUXhq0G.css +0 -1
  136. package/dist/assets/index-DAE8Srx-.js +0 -6
  137. package/dist/assets/label-D8yyejJS.js +0 -1
  138. package/dist/assets/loader-circle-B0sKKO29.js +0 -1
  139. package/dist/assets/marketplace-localization-CxSTG9wr.js +0 -1
  140. package/dist/assets/plus-CYXs3JtZ.js +0 -1
  141. package/dist/assets/react-8EIEQjMP.js +0 -1
  142. package/dist/assets/search-DOsLw-P9.js +0 -1
  143. package/dist/assets/security-config-CM_tQRXQ.js +0 -1
  144. package/dist/assets/skeleton-GbHLjPC0.js +0 -1
  145. package/dist/assets/useMutation-DSinpgEq.js +0 -1
  146. package/dist/assets/x-Bnco_K8b.js +0 -1
  147. package/src/components/chat/ChatSessionsSidebar.tsx +0 -100
  148. /package/dist/assets/{config-hints-WtpHP_DW.js → config-hints-GSUMvmSo.js} +0 -0
  149. /package/dist/assets/{config-layout-LQ10ozRC.js → config-layout-CgBMG7OL.js} +0 -0
@@ -1,10 +1,12 @@
1
- import { render, screen } from '@testing-library/react';
2
- import userEvent from '@testing-library/user-event';
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';
6
- import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
7
- import { useChatThreadStore } from '@/components/chat/stores/chat-thread.store';
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
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";
6
+ import type { ResolvedChildSessionTab } from "@/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view";
7
+ import { useChatInputStore } from "@/components/chat/stores/chat-input.store";
8
+ import { useChatSessionListStore } from "@/components/chat/stores/chat-session-list.store";
9
+ import { useChatThreadStore } from "@/components/chat/stores/chat-thread.store";
8
10
 
9
11
  const mocks = vi.hoisted(() => ({
10
12
  deleteSession: vi.fn(),
@@ -12,41 +14,45 @@ const mocks = vi.hoisted(() => ({
12
14
  createSession: vi.fn(),
13
15
  setSelectedAgentId: vi.fn(),
14
16
  setPendingSessionType: vi.fn(),
17
+ stickyBottomScroll: vi.fn(() => ({
18
+ onScroll: vi.fn(),
19
+ })),
15
20
  resolvedChildTabs: [
16
21
  {
17
- sessionKey: 'child-session-1',
18
- parentSessionKey: 'parent-session-1',
19
- title: '北京天气',
20
- agentId: 'weather',
21
- agentDisplayName: 'Weather',
22
- agentAvatarUrl: null,
22
+ sessionKey: "child-session-1",
23
+ parentSessionKey: "parent-session-1",
24
+ title: "北京天气",
25
+ agentId: "weather",
26
+ updatedAt: "2026-04-10T09:00:00.000Z",
27
+ sessionTypeLabel: "Codex",
28
+ preferredModel: "openai/gpt-5.3-codex",
29
+ projectName: "project-alpha",
30
+ projectRoot: "/Users/demo/project-alpha",
23
31
  },
24
- ],
32
+ ] as ResolvedChildSessionTab[],
25
33
  }));
26
34
 
27
- vi.mock('@nextclaw/agent-chat-ui', async (importOriginal) => {
35
+ vi.mock("@nextclaw/agent-chat-ui", async (importOriginal) => {
28
36
  const actual = await importOriginal();
29
37
  return {
30
38
  ...(actual as object),
31
- useStickyBottomScroll: () => ({
32
- onScroll: vi.fn()
33
- })
39
+ useStickyBottomScroll: mocks.stickyBottomScroll,
34
40
  };
35
41
  });
36
42
 
37
- vi.mock('@/components/chat/nextclaw', () => ({
43
+ vi.mock("@/components/chat/nextclaw", () => ({
38
44
  ChatInputBarContainer: () => <div data-testid="chat-input-bar" />,
39
- ChatMessageListContainer: () => <div data-testid="chat-message-list" />
45
+ ChatMessageListContainer: () => <div data-testid="chat-message-list" />,
40
46
  }));
41
47
 
42
- vi.mock('@/components/chat/containers/chat-message-list.container', () => ({
48
+ vi.mock("@/components/chat/containers/chat-message-list.container", () => ({
43
49
  ChatMessageListContainer: () => <div data-testid="child-chat-message-list" />,
44
50
  }));
45
51
 
46
- vi.mock('@/components/chat/ChatWelcome', () => ({
52
+ vi.mock("@/components/chat/ChatWelcome", () => ({
47
53
  ChatWelcome: ({
48
54
  onCreateSession,
49
- onSelectAgent
55
+ onSelectAgent,
50
56
  }: {
51
57
  onCreateSession: () => void;
52
58
  onSelectAgent: (agentId: string) => void;
@@ -55,87 +61,115 @@ vi.mock('@/components/chat/ChatWelcome', () => ({
55
61
  <button type="button" onClick={onCreateSession}>
56
62
  create draft session
57
63
  </button>
58
- <button type="button" onClick={() => onSelectAgent('engineer')}>
64
+ <button type="button" onClick={() => onSelectAgent("engineer")}>
59
65
  switch draft agent
60
66
  </button>
61
67
  </div>
62
- )
68
+ ),
63
69
  }));
64
70
 
65
- vi.mock('@/components/chat/presenter/chat-presenter-context', () => ({
66
- usePresenter: () => ({
67
- chatThreadManager: {
68
- deleteSession: mocks.deleteSession,
69
- goToProviders: mocks.goToProviders,
70
- openSessionFromToolAction: vi.fn(),
71
- selectChildSessionDetail: vi.fn(),
72
- closeChildSessionDetail: vi.fn(),
73
- goToParentSession: vi.fn(),
74
- },
71
+ vi.mock("@/components/chat/presenter/chat-presenter-context", () => ({
72
+ usePresenter: () => ({
73
+ chatThreadManager: {
74
+ deleteSession: mocks.deleteSession,
75
+ goToProviders: mocks.goToProviders,
76
+ openSessionFromToolAction: vi.fn(),
77
+ selectChildSessionDetail: vi.fn(),
78
+ closeChildSessionDetail: vi.fn(),
79
+ goToParentSession: vi.fn(),
80
+ },
75
81
  chatSessionListManager: {
76
82
  selectSession: vi.fn(),
77
83
  createSession: mocks.createSession,
78
- setSelectedAgentId: mocks.setSelectedAgentId
84
+ setSelectedAgentId: mocks.setSelectedAgentId,
85
+ markSessionRead: (
86
+ sessionKey: string | null | undefined,
87
+ updatedAt: string | null | undefined,
88
+ ) =>
89
+ sessionKey
90
+ ? useChatSessionListStore.getState().markSessionRead(
91
+ sessionKey,
92
+ updatedAt,
93
+ )
94
+ : undefined,
95
+ hydrateReadWatermarks: (
96
+ entries: readonly { sessionKey: string; updatedAt: string | null | undefined }[],
97
+ ) => useChatSessionListStore.getState().hydrateReadWatermarks(entries),
79
98
  },
80
99
  chatInputManager: {
81
- setPendingSessionType: mocks.setPendingSessionType
82
- }
83
- })
84
- }));
85
-
86
- vi.mock('@/components/chat/session-header/chat-session-header-actions', () => ({
87
- ChatSessionHeaderActions: () => <button aria-label="More actions" />
100
+ setPendingSessionType: mocks.setPendingSessionType,
101
+ },
102
+ }),
88
103
  }));
89
104
 
90
- vi.mock('@/components/chat/session-header/chat-session-project-badge', () => ({
91
- ChatSessionProjectBadge: ({ projectName }: { projectName: string }) => <button>{projectName}</button>
105
+ vi.mock("@/components/chat/session-header/chat-session-header-actions", () => ({
106
+ ChatSessionHeaderActions: () => <button aria-label="More actions" />,
92
107
  }));
93
108
 
94
- vi.mock('@/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view', () => ({
95
- useNcpChildSessionTabsView: () => mocks.resolvedChildTabs,
109
+ vi.mock("@/components/chat/session-header/chat-session-project-badge", () => ({
110
+ ChatSessionProjectBadge: ({ projectName }: { projectName: string }) => (
111
+ <button>{projectName}</button>
112
+ ),
96
113
  }));
97
114
 
98
- vi.mock('@/components/chat/ncp/session-conversation/use-ncp-session-conversation', () => ({
99
- useNcpSessionConversation: () => ({
100
- visibleMessages: [],
101
- isHydrating: false,
102
- hydrateError: null,
103
- isRunning: false,
115
+ vi.mock(
116
+ "@/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view",
117
+ () => ({
118
+ useNcpChildSessionTabsView: () => mocks.resolvedChildTabs,
104
119
  }),
105
- }));
120
+ );
121
+
122
+ vi.mock(
123
+ "@/components/chat/ncp/session-conversation/use-ncp-session-conversation",
124
+ () => ({
125
+ useNcpSessionConversation: () => ({
126
+ visibleMessages: [],
127
+ isHydrating: false,
128
+ hydrateError: null,
129
+ isRunning: false,
130
+ }),
131
+ }),
132
+ );
106
133
 
107
- vi.mock('@/components/common/AgentAvatar', () => ({
134
+ vi.mock("@/components/common/AgentAvatar", () => ({
108
135
  AgentAvatar: ({ agentId }: { agentId: string }) => (
109
136
  <div data-testid="agent-avatar">{agentId}</div>
110
137
  ),
111
138
  }));
112
139
 
113
- vi.mock('@/components/common/agent-identity', () => ({
140
+ vi.mock("@/components/common/agent-identity", () => ({
114
141
  AgentIdentityAvatar: ({ agentId }: { agentId: string }) => (
115
142
  <div data-testid="agent-identity-avatar">{agentId}</div>
116
143
  ),
117
144
  }));
118
145
 
119
- describe('ChatConversationPanel', () => {
146
+ describe("ChatConversationPanel", () => {
120
147
  beforeEach(() => {
121
148
  mocks.deleteSession.mockReset();
122
149
  mocks.goToProviders.mockReset();
123
150
  mocks.createSession.mockReset();
124
151
  mocks.setSelectedAgentId.mockReset();
125
152
  mocks.setPendingSessionType.mockReset();
153
+ mocks.stickyBottomScroll.mockClear();
126
154
  useChatInputStore.setState({
127
155
  snapshot: {
128
156
  ...useChatInputStore.getState().snapshot,
129
- defaultSessionType: 'native'
130
- }
157
+ defaultSessionType: "native",
158
+ },
131
159
  });
132
160
  useChatThreadStore.setState({
133
161
  snapshot: {
134
162
  ...useChatThreadStore.getState().snapshot,
135
163
  isProviderStateResolved: true,
136
- modelOptions: [{ value: 'openai/gpt-5.1', modelLabel: 'gpt-5.1', providerLabel: 'OpenAI' } as never],
137
- sessionTypeLabel: 'Codex',
138
- sessionKey: 'draft-session-1',
164
+ modelOptions: [
165
+ {
166
+ value: "openai/gpt-5.1",
167
+ modelLabel: "gpt-5.1",
168
+ providerLabel: "OpenAI",
169
+ } as never,
170
+ ],
171
+ sessionTypeLabel: "Codex",
172
+ sessionKey: "draft-session-1",
139
173
  sessionDisplayName: undefined,
140
174
  agentId: null,
141
175
  agentDisplayName: null,
@@ -150,121 +184,135 @@ describe('ChatConversationPanel', () => {
150
184
  parentSessionKey: null,
151
185
  parentSessionLabel: null,
152
186
  availableAgents: [
153
- { id: 'main', displayName: 'Main', runtime: 'native' },
154
- { id: 'engineer', displayName: 'Engineer', runtime: 'codex' }
187
+ { id: "main", displayName: "Main", runtime: "native" },
188
+ { id: "engineer", displayName: "Engineer", runtime: "codex" },
155
189
  ],
156
190
  childSessionTabs: [],
157
191
  activeChildSessionKey: null,
158
- }
192
+ },
193
+ });
194
+ useChatSessionListStore.setState({
195
+ readUpdatedAtBySessionKey: {},
196
+ hasHydratedReadWatermarks: false,
197
+ snapshot: {
198
+ ...useChatSessionListStore.getState().snapshot,
199
+ },
159
200
  });
160
201
  });
161
202
 
162
- it('shows the draft session type in the conversation header', () => {
203
+ it("shows the draft session type in the conversation header", () => {
163
204
  render(<ChatConversationPanel />);
164
205
 
165
- expect(screen.getByText('New Task')).toBeTruthy();
166
- expect(screen.getByText('Codex')).toBeTruthy();
167
- expect(screen.getByLabelText('More actions')).toBeTruthy();
206
+ expect(screen.getByText("New Task")).toBeTruthy();
207
+ expect(screen.getByText("Codex")).toBeTruthy();
208
+ expect(screen.getByLabelText("More actions")).toBeTruthy();
168
209
  });
169
210
 
170
- it('shows the selected session project badge and more actions trigger', () => {
211
+ it("shows the selected session project badge and more actions trigger", () => {
171
212
  useChatThreadStore.setState({
172
213
  snapshot: {
173
214
  ...useChatThreadStore.getState().snapshot,
174
- sessionKey: 'session-1',
175
- sessionDisplayName: 'Project Thread',
176
- sessionProjectRoot: '/Users/demo/workspace/project-alpha',
177
- sessionProjectName: 'project-alpha',
215
+ sessionKey: "session-1",
216
+ sessionDisplayName: "Project Thread",
217
+ sessionProjectRoot: "/Users/demo/workspace/project-alpha",
218
+ sessionProjectName: "project-alpha",
178
219
  canDeleteSession: true,
179
- }
220
+ },
180
221
  });
181
222
 
182
223
  render(<ChatConversationPanel />);
183
224
 
184
- expect(screen.getByText('Project Thread')).toBeTruthy();
185
- expect(screen.getByText('project-alpha')).toBeTruthy();
186
- expect(screen.getByLabelText('More actions')).toBeTruthy();
225
+ expect(screen.getByText("Project Thread")).toBeTruthy();
226
+ expect(screen.getByText("project-alpha")).toBeTruthy();
227
+ expect(screen.getByLabelText("More actions")).toBeTruthy();
187
228
  });
188
229
 
189
- it('does not show a header agent marker for the main agent', () => {
230
+ it("does not show a header agent marker for the main agent", () => {
190
231
  useChatThreadStore.setState({
191
232
  snapshot: {
192
233
  ...useChatThreadStore.getState().snapshot,
193
- agentId: 'main',
194
- agentDisplayName: 'Main',
195
- }
234
+ agentId: "main",
235
+ agentDisplayName: "Main",
236
+ },
196
237
  });
197
238
 
198
239
  render(<ChatConversationPanel />);
199
240
 
200
- expect(screen.queryByTestId('agent-avatar')).toBeNull();
241
+ expect(screen.queryByTestId("agent-avatar")).toBeNull();
201
242
  });
202
243
 
203
- it('shows only a lightweight avatar marker for a specialist agent', () => {
244
+ it("shows only a lightweight avatar marker for a specialist agent", () => {
204
245
  useChatThreadStore.setState({
205
246
  snapshot: {
206
247
  ...useChatThreadStore.getState().snapshot,
207
- agentId: 'engineer',
208
- agentDisplayName: 'Engineer',
209
- }
248
+ agentId: "engineer",
249
+ agentDisplayName: "Engineer",
250
+ },
210
251
  });
211
252
 
212
253
  render(<ChatConversationPanel />);
213
254
 
214
- expect(screen.getByTestId('agent-avatar').textContent).toBe('engineer');
215
- expect(screen.queryByText('Engineer')).toBeNull();
255
+ expect(screen.getByTestId("agent-avatar").textContent).toBe("engineer");
256
+ expect(screen.queryByText("Engineer")).toBeNull();
216
257
  });
217
258
 
218
- it('creates a draft session with the selected draft agent runtime', async () => {
259
+ it("creates a draft session with the selected draft agent runtime", async () => {
219
260
  const user = userEvent.setup();
220
261
 
221
262
  useChatThreadStore.setState({
222
263
  snapshot: {
223
264
  ...useChatThreadStore.getState().snapshot,
224
- agentId: 'engineer',
225
- agentDisplayName: 'Engineer'
226
- }
265
+ agentId: "engineer",
266
+ agentDisplayName: "Engineer",
267
+ },
227
268
  });
228
269
 
229
270
  render(<ChatConversationPanel />);
230
271
 
231
- await user.click(screen.getByRole('button', { name: 'create draft session' }));
272
+ await user.click(
273
+ screen.getByRole("button", { name: "create draft session" }),
274
+ );
232
275
 
233
- expect(mocks.createSession).toHaveBeenCalledWith('codex');
276
+ expect(mocks.createSession).toHaveBeenCalledWith("codex");
234
277
  });
235
278
 
236
- it('syncs the pending session type when switching the draft agent', async () => {
279
+ it("syncs the pending session type when switching the draft agent", async () => {
237
280
  const user = userEvent.setup();
238
281
 
239
282
  render(<ChatConversationPanel />);
240
283
 
241
- await user.click(screen.getByRole('button', { name: 'switch draft agent' }));
284
+ await user.click(
285
+ screen.getByRole("button", { name: "switch draft agent" }),
286
+ );
242
287
 
243
- expect(mocks.setSelectedAgentId).toHaveBeenCalledWith('engineer');
244
- expect(mocks.setPendingSessionType).toHaveBeenCalledWith('codex');
288
+ expect(mocks.setSelectedAgentId).toHaveBeenCalledWith("engineer");
289
+ expect(mocks.setPendingSessionType).toHaveBeenCalledWith("codex");
245
290
  });
246
291
  });
247
292
 
248
- describe('ChatChildSessionPanel', () => {
249
- it('keeps the header compact for a single child session', () => {
293
+ describe("ChatChildSessionPanel", () => {
294
+ it("keeps the header compact for a single child session", () => {
250
295
  mocks.resolvedChildTabs = [
251
296
  {
252
- sessionKey: 'child-session-1',
253
- parentSessionKey: 'parent-session-1',
254
- title: '北京天气',
255
- agentId: 'weather',
256
- agentDisplayName: 'Weather',
257
- agentAvatarUrl: null,
297
+ sessionKey: "child-session-1",
298
+ parentSessionKey: "parent-session-1",
299
+ title: "北京天气",
300
+ agentId: "weather",
301
+ updatedAt: "2026-04-10T09:00:00.000Z",
302
+ sessionTypeLabel: "Codex",
303
+ preferredModel: "openai/gpt-5.3-codex",
304
+ projectName: "project-alpha",
305
+ projectRoot: "/Users/demo/project-alpha",
258
306
  },
259
307
  ];
260
308
  render(
261
309
  <ChatChildSessionPanel
262
310
  tabs={[
263
311
  {
264
- sessionKey: 'child-session-1',
265
- parentSessionKey: 'parent-session-1',
266
- label: '北京天气',
267
- agentId: 'weather',
312
+ sessionKey: "child-session-1",
313
+ parentSessionKey: "parent-session-1",
314
+ label: "北京天气",
315
+ agentId: "weather",
268
316
  },
269
317
  ]}
270
318
  activeSessionKey="child-session-1"
@@ -274,28 +322,44 @@ describe('ChatChildSessionPanel', () => {
274
322
  />,
275
323
  );
276
324
 
277
- expect(screen.getByText('北京天气')).toBeTruthy();
278
- expect(screen.queryByText('Child Sessions')).toBeNull();
279
- expect(screen.queryByText('child-session-1')).toBeNull();
325
+ expect(screen.getByText("北京天气")).toBeTruthy();
326
+ expect(screen.getByText("Codex")).toBeTruthy();
327
+ expect(screen.getByText("openai/gpt-5.3-codex")).toBeTruthy();
328
+ expect(screen.getByText("project-alpha")).toBeTruthy();
329
+ expect(screen.getByText("/Users/demo/project-alpha")).toBeTruthy();
330
+ expect(screen.queryByText("Child Sessions")).toBeNull();
331
+ expect(screen.queryByText("child-session-1")).toBeNull();
332
+ expect(mocks.stickyBottomScroll).toHaveBeenCalledWith(
333
+ expect.objectContaining({
334
+ resetKey: "child-session-1",
335
+ stickyThresholdPx: 20,
336
+ }),
337
+ );
280
338
  });
281
339
 
282
- it('uses tabs as the only title layer when multiple child sessions are open', () => {
340
+ it("uses tabs as the only title layer when multiple child sessions are open", () => {
283
341
  mocks.resolvedChildTabs = [
284
342
  {
285
- sessionKey: 'child-session-1',
286
- parentSessionKey: 'parent-session-1',
287
- title: '北京天气',
288
- agentId: 'weather',
289
- agentDisplayName: 'Weather',
290
- agentAvatarUrl: null,
343
+ sessionKey: "child-session-1",
344
+ parentSessionKey: "parent-session-1",
345
+ title: "北京天气",
346
+ agentId: "weather",
347
+ updatedAt: "2026-04-10T09:00:00.000Z",
348
+ sessionTypeLabel: "Codex",
349
+ preferredModel: "openai/gpt-5.3-codex",
350
+ projectName: "project-alpha",
351
+ projectRoot: "/Users/demo/project-alpha",
291
352
  },
292
353
  {
293
- sessionKey: 'child-session-2',
294
- parentSessionKey: 'parent-session-1',
295
- title: '上海天气',
296
- agentId: 'weather',
297
- agentDisplayName: 'Weather',
298
- agentAvatarUrl: null,
354
+ sessionKey: "child-session-2",
355
+ parentSessionKey: "parent-session-1",
356
+ title: "上海天气",
357
+ agentId: "weather",
358
+ updatedAt: "2026-04-10T09:05:00.000Z",
359
+ sessionTypeLabel: "Claude Code",
360
+ preferredModel: "anthropic/claude-sonnet-4",
361
+ projectName: "project-beta",
362
+ projectRoot: "/Users/demo/project-beta",
299
363
  },
300
364
  ];
301
365
 
@@ -303,16 +367,16 @@ describe('ChatChildSessionPanel', () => {
303
367
  <ChatChildSessionPanel
304
368
  tabs={[
305
369
  {
306
- sessionKey: 'child-session-1',
307
- parentSessionKey: 'parent-session-1',
308
- label: '北京天气',
309
- agentId: 'weather',
370
+ sessionKey: "child-session-1",
371
+ parentSessionKey: "parent-session-1",
372
+ label: "北京天气",
373
+ agentId: "weather",
310
374
  },
311
375
  {
312
- sessionKey: 'child-session-2',
313
- parentSessionKey: 'parent-session-1',
314
- label: '上海天气',
315
- agentId: 'weather',
376
+ sessionKey: "child-session-2",
377
+ parentSessionKey: "parent-session-1",
378
+ label: "上海天气",
379
+ agentId: "weather",
316
380
  },
317
381
  ]}
318
382
  activeSessionKey="child-session-1"
@@ -322,13 +386,151 @@ describe('ChatChildSessionPanel', () => {
322
386
  />,
323
387
  );
324
388
 
325
- expect(screen.getAllByText('北京天气')).toHaveLength(1);
326
- expect(screen.getByText('上海天气')).toBeTruthy();
327
- const tabButtons = screen.getAllByRole('button').filter((element) =>
328
- element.getAttribute('aria-pressed') !== null,
329
- );
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);
330
398
  expect(tabButtons).toHaveLength(2);
331
- expect(tabButtons[0]?.getAttribute('aria-pressed')).toBe('true');
332
- expect(tabButtons[1]?.getAttribute('aria-pressed')).toBe('false');
399
+ expect(tabButtons[0]?.getAttribute("aria-pressed")).toBe("true");
400
+ expect(tabButtons[1]?.getAttribute("aria-pressed")).toBe("false");
401
+ });
402
+
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
+ },
491
+ {
492
+ sessionKey: "child-session-2",
493
+ parentSessionKey: "parent-session-1",
494
+ label: "上海天气",
495
+ agentId: "weather",
496
+ },
497
+ ]}
498
+ activeSessionKey="child-session-1"
499
+ onSelectSession={vi.fn()}
500
+ onClose={vi.fn()}
501
+ onBackToParent={vi.fn()}
502
+ />,
503
+ );
504
+
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
+ />,
530
+ );
531
+
532
+ expect(
533
+ screen.queryByLabelText("Session has unread updates"),
534
+ ).toBeNull();
333
535
  });
334
536
  });