@nextclaw/ui 0.12.4 → 0.12.5

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 (115) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/assets/{ChannelsList-CobWeI2V.js → ChannelsList-C6-lh55g.js} +2 -2
  3. package/dist/assets/ChatPage-DOW0gPc2.js +45 -0
  4. package/dist/assets/DocBrowser-CGyeswYP.js +1 -0
  5. package/dist/assets/{DocBrowser-NSzgVKka.js → DocBrowser-QUZ3nfmH.js} +1 -1
  6. package/dist/assets/{DocBrowserContext-DpgVdRgk.js → DocBrowserContext-CpiIfhJO.js} +1 -1
  7. package/dist/assets/{LogoBadge-CHS4YNLw.js → LogoBadge-BUK13xK5.js} +1 -1
  8. package/dist/assets/MarketplacePage-BDVwhIYE.js +1 -0
  9. package/dist/assets/MarketplacePage-LnKKL3xK.js +49 -0
  10. package/dist/assets/McpMarketplacePage-BG4T_Pcx.js +40 -0
  11. package/dist/assets/ModelConfig-LtWuogIw.js +1 -0
  12. package/dist/assets/ProviderScopedModelInput-DGn6sFEN.js +1 -0
  13. package/dist/assets/ProvidersList-ma-_MlLo.js +1 -0
  14. package/dist/assets/{RemoteAccessPage-yfbrveNQ.js → RemoteAccessPage-ff15qO-c.js} +1 -1
  15. package/dist/assets/RuntimeConfig-TgPandXF.js +1 -0
  16. package/dist/assets/SearchConfig-C9iBt7pl.js +1 -0
  17. package/dist/assets/{SecretsConfig-CLFSSoTl.js → SecretsConfig-Bew4EF2A.js} +2 -2
  18. package/dist/assets/{SessionsConfig-vYrvc2Fk.js → SessionsConfig-2r2yAGZg.js} +2 -2
  19. package/dist/assets/{book-open-C7TAghTk.js → book-open-CJG8Yz3U.js} +1 -1
  20. package/dist/assets/{chat-session-display-5dVFkJyw.js → chat-session-display-DkAC5OMC.js} +1 -1
  21. package/dist/assets/{chunk-JZWAC4HX-DbL4EmiT.js → chunk-JZWAC4HX-D5b3Iyas.js} +1 -1
  22. package/dist/assets/{config-CMiW0yaK.js → config-zvnxSXSP.js} +1 -1
  23. package/dist/assets/{createLucideIcon-BRLFtf-8.js → createLucideIcon-_FMJqZw2.js} +1 -1
  24. package/dist/assets/{dist-DP-JKR4G.js → dist-B1fpOuON.js} +1 -1
  25. package/dist/assets/{dist-BFc_H-lY.js → dist-BCXX7FD-.js} +2 -2
  26. package/dist/assets/{external-link-BkJkiWbH.js → external-link-b7gAJWYY.js} +1 -1
  27. package/dist/assets/{hash-CbP6-6R9.js → hash-Bhy4TwfZ.js} +1 -1
  28. package/dist/assets/{i18n-C_2dKw6w.js → i18n-DJg9BPYk.js} +1 -1
  29. package/dist/assets/index-BoJbxdvZ.css +1 -0
  30. package/dist/assets/index-CtlT4E9Y.js +6 -0
  31. package/dist/assets/infiniteQueryBehavior-CTcVlD9s.js +1 -0
  32. package/dist/assets/loader-circle-B60I0hEk.js +1 -0
  33. package/dist/assets/{logos-N3dbS6-I.js → logos-GMeYU9vc.js} +1 -1
  34. package/dist/assets/{page-layout-DyuvlNrg.js → page-layout-C8UbWuMt.js} +1 -1
  35. package/dist/assets/plus-CR7RfK3H.js +1 -0
  36. package/dist/assets/{popover-BKKWGUaG.js → popover-8HSx9wQj.js} +1 -1
  37. package/dist/assets/react-BB4jko2M.js +1 -0
  38. package/dist/assets/{refresh-ccw-BGMdiNGq.js → refresh-ccw-CA4_C7Zg.js} +1 -1
  39. package/dist/assets/{save-Dh4GQzzX.js → save-BtvMy4lk.js} +1 -1
  40. package/dist/assets/search-C60UA27E.js +1 -0
  41. package/dist/assets/security-config-BkFDYZ6j.js +1 -0
  42. package/dist/assets/{select-BtIi5fnh.js → select-xp_Ac8ip.js} +1 -1
  43. package/dist/assets/skeleton-uxz_5h3A.js +1 -0
  44. package/dist/assets/{status-dot-C4O-2jZP.js → status-dot-Cn4Pp7DZ.js} +1 -1
  45. package/dist/assets/{switch-DPegGIa_.js → switch-BTi6UOij.js} +1 -1
  46. package/dist/assets/{tabs-custom-x5GZexrF.js → tabs-custom-BiiN8DME.js} +1 -1
  47. package/dist/assets/{trash-2-CU3LYIpQ.js → trash-2-BpsF0N-r.js} +1 -1
  48. package/dist/assets/use-infinite-scroll-loader-C8jBv11-.js +1 -0
  49. package/dist/assets/{useConfirmDialog-S5WsGOGf.js → useConfirmDialog-BJIwUZjH.js} +1 -1
  50. package/dist/assets/{useMutation-DSinpgEq.js → useMutation-BjBOKHj_.js} +1 -1
  51. package/dist/assets/x-BfTu-g7D.js +1 -0
  52. package/dist/index.html +19 -18
  53. package/package.json +4 -4
  54. package/src/account/components/account-panel.tsx +46 -4
  55. package/src/account/managers/account.manager.ts +19 -4
  56. package/src/api/remote.ts +9 -0
  57. package/src/api/remote.types.ts +5 -0
  58. package/src/components/chat/ChatConversationPanel.test.tsx +183 -141
  59. package/src/components/chat/adapters/chat-message-tool-agent-id.test.ts +11 -11
  60. package/src/components/chat/adapters/chat-message.adapter.test.ts +43 -6
  61. package/src/components/chat/adapters/chat-message.session-request-tool-card.ts +182 -44
  62. package/src/components/chat/adapters/chat-message.session-spawn-tool-card.test.ts +104 -0
  63. package/src/components/chat/chat-child-session-panel.tsx +103 -45
  64. package/src/components/chat/chat-page-runtime.test.ts +16 -19
  65. package/src/components/chat/chat-session-preference-sync.test.ts +13 -0
  66. package/src/components/chat/chat-session-preference-sync.ts +9 -7
  67. package/src/components/chat/hooks/use-chat-session-project.test.tsx +5 -5
  68. package/src/components/chat/hooks/use-chat-session-project.ts +0 -5
  69. package/src/components/chat/hooks/use-chat-session-update.test.tsx +75 -0
  70. package/src/components/chat/hooks/use-chat-session-update.ts +4 -2
  71. package/src/components/chat/managers/chat-session-list.manager.test.ts +45 -5
  72. package/src/components/chat/managers/chat-session-list.manager.ts +18 -4
  73. package/src/components/chat/ncp/NcpChatPage.tsx +32 -51
  74. package/src/components/chat/ncp/ncp-chat-input.manager.ts +3 -5
  75. package/src/components/chat/ncp/ncp-chat-page-data.ts +0 -1
  76. package/src/components/chat/ncp/ncp-chat.presenter.ts +1 -11
  77. package/src/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view.ts +20 -7
  78. package/src/components/chat/stores/chat-session-list.store.ts +3 -0
  79. package/src/components/chat/useChatSessionTypeState.test.tsx +0 -3
  80. package/src/components/chat/useChatSessionTypeState.ts +3 -5
  81. package/src/components/config/ChannelsList.test.tsx +68 -0
  82. package/src/components/config/ChannelsList.tsx +22 -4
  83. package/src/components/config/ProvidersList.tsx +17 -3
  84. package/src/components/config/providers-list.test.tsx +68 -0
  85. package/src/components/layout/Sidebar.tsx +13 -13
  86. package/src/components/layout/sidebar.layout.test.tsx +32 -1
  87. package/src/components/marketplace/MarketplacePage.tsx +30 -30
  88. package/src/components/marketplace/marketplace-page-parts.tsx +16 -24
  89. package/src/components/marketplace/mcp/McpMarketplacePage.tsx +28 -26
  90. package/src/hooks/marketplace-list-pages.ts +27 -0
  91. package/src/hooks/use-infinite-scroll-loader.ts +88 -0
  92. package/src/hooks/useMarketplace.ts +14 -3
  93. package/src/hooks/useMcpMarketplace.ts +14 -3
  94. package/src/lib/i18n.remote.ts +15 -0
  95. package/dist/assets/ChatPage-ZIdFFVAv.js +0 -43
  96. package/dist/assets/DocBrowser-D55C0iyl.js +0 -1
  97. package/dist/assets/MarketplacePage-BFYsRss_.js +0 -49
  98. package/dist/assets/MarketplacePage-DII-q-Y1.js +0 -1
  99. package/dist/assets/McpMarketplacePage-CPqsGJzz.js +0 -40
  100. package/dist/assets/ModelConfig-Bvuo_IpS.js +0 -1
  101. package/dist/assets/ProviderScopedModelInput-BfY8rGsf.js +0 -1
  102. package/dist/assets/ProvidersList-3tlaqwSS.js +0 -1
  103. package/dist/assets/RuntimeConfig-CAd5Kta3.js +0 -1
  104. package/dist/assets/SearchConfig-DFwgaAa7.js +0 -1
  105. package/dist/assets/index-ChUXhq0G.css +0 -1
  106. package/dist/assets/index-DAE8Srx-.js +0 -6
  107. package/dist/assets/label-D8yyejJS.js +0 -1
  108. package/dist/assets/loader-circle-B0sKKO29.js +0 -1
  109. package/dist/assets/marketplace-localization-CxSTG9wr.js +0 -1
  110. package/dist/assets/plus-CYXs3JtZ.js +0 -1
  111. package/dist/assets/react-8EIEQjMP.js +0 -1
  112. package/dist/assets/search-DOsLw-P9.js +0 -1
  113. package/dist/assets/security-config-CM_tQRXQ.js +0 -1
  114. package/dist/assets/skeleton-GbHLjPC0.js +0 -1
  115. package/dist/assets/x-Bnco_K8b.js +0 -1
@@ -1,10 +1,10 @@
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 { useChatInputStore } from "@/components/chat/stores/chat-input.store";
7
+ import { useChatThreadStore } from "@/components/chat/stores/chat-thread.store";
8
8
 
9
9
  const mocks = vi.hoisted(() => ({
10
10
  deleteSession: vi.fn(),
@@ -12,41 +12,44 @@ const mocks = vi.hoisted(() => ({
12
12
  createSession: vi.fn(),
13
13
  setSelectedAgentId: vi.fn(),
14
14
  setPendingSessionType: vi.fn(),
15
+ stickyBottomScroll: vi.fn(() => ({
16
+ onScroll: vi.fn(),
17
+ })),
15
18
  resolvedChildTabs: [
16
19
  {
17
- sessionKey: 'child-session-1',
18
- parentSessionKey: 'parent-session-1',
19
- title: '北京天气',
20
- agentId: 'weather',
21
- agentDisplayName: 'Weather',
22
- agentAvatarUrl: null,
20
+ sessionKey: "child-session-1",
21
+ parentSessionKey: "parent-session-1",
22
+ title: "北京天气",
23
+ agentId: "weather",
24
+ sessionTypeLabel: "Codex",
25
+ preferredModel: "openai/gpt-5.3-codex",
26
+ projectName: "project-alpha",
27
+ projectRoot: "/Users/demo/project-alpha",
23
28
  },
24
29
  ],
25
30
  }));
26
31
 
27
- vi.mock('@nextclaw/agent-chat-ui', async (importOriginal) => {
32
+ vi.mock("@nextclaw/agent-chat-ui", async (importOriginal) => {
28
33
  const actual = await importOriginal();
29
34
  return {
30
35
  ...(actual as object),
31
- useStickyBottomScroll: () => ({
32
- onScroll: vi.fn()
33
- })
36
+ useStickyBottomScroll: mocks.stickyBottomScroll,
34
37
  };
35
38
  });
36
39
 
37
- vi.mock('@/components/chat/nextclaw', () => ({
40
+ vi.mock("@/components/chat/nextclaw", () => ({
38
41
  ChatInputBarContainer: () => <div data-testid="chat-input-bar" />,
39
- ChatMessageListContainer: () => <div data-testid="chat-message-list" />
42
+ ChatMessageListContainer: () => <div data-testid="chat-message-list" />,
40
43
  }));
41
44
 
42
- vi.mock('@/components/chat/containers/chat-message-list.container', () => ({
45
+ vi.mock("@/components/chat/containers/chat-message-list.container", () => ({
43
46
  ChatMessageListContainer: () => <div data-testid="child-chat-message-list" />,
44
47
  }));
45
48
 
46
- vi.mock('@/components/chat/ChatWelcome', () => ({
49
+ vi.mock("@/components/chat/ChatWelcome", () => ({
47
50
  ChatWelcome: ({
48
51
  onCreateSession,
49
- onSelectAgent
52
+ onSelectAgent,
50
53
  }: {
51
54
  onCreateSession: () => void;
52
55
  onSelectAgent: (agentId: string) => void;
@@ -55,87 +58,102 @@ vi.mock('@/components/chat/ChatWelcome', () => ({
55
58
  <button type="button" onClick={onCreateSession}>
56
59
  create draft session
57
60
  </button>
58
- <button type="button" onClick={() => onSelectAgent('engineer')}>
61
+ <button type="button" onClick={() => onSelectAgent("engineer")}>
59
62
  switch draft agent
60
63
  </button>
61
64
  </div>
62
- )
65
+ ),
63
66
  }));
64
67
 
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
- },
68
+ vi.mock("@/components/chat/presenter/chat-presenter-context", () => ({
69
+ usePresenter: () => ({
70
+ chatThreadManager: {
71
+ deleteSession: mocks.deleteSession,
72
+ goToProviders: mocks.goToProviders,
73
+ openSessionFromToolAction: vi.fn(),
74
+ selectChildSessionDetail: vi.fn(),
75
+ closeChildSessionDetail: vi.fn(),
76
+ goToParentSession: vi.fn(),
77
+ },
75
78
  chatSessionListManager: {
76
79
  selectSession: vi.fn(),
77
80
  createSession: mocks.createSession,
78
- setSelectedAgentId: mocks.setSelectedAgentId
81
+ setSelectedAgentId: mocks.setSelectedAgentId,
79
82
  },
80
83
  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" />
84
+ setPendingSessionType: mocks.setPendingSessionType,
85
+ },
86
+ }),
88
87
  }));
89
88
 
90
- vi.mock('@/components/chat/session-header/chat-session-project-badge', () => ({
91
- ChatSessionProjectBadge: ({ projectName }: { projectName: string }) => <button>{projectName}</button>
89
+ vi.mock("@/components/chat/session-header/chat-session-header-actions", () => ({
90
+ ChatSessionHeaderActions: () => <button aria-label="More actions" />,
92
91
  }));
93
92
 
94
- vi.mock('@/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view', () => ({
95
- useNcpChildSessionTabsView: () => mocks.resolvedChildTabs,
93
+ vi.mock("@/components/chat/session-header/chat-session-project-badge", () => ({
94
+ ChatSessionProjectBadge: ({ projectName }: { projectName: string }) => (
95
+ <button>{projectName}</button>
96
+ ),
96
97
  }));
97
98
 
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,
99
+ vi.mock(
100
+ "@/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view",
101
+ () => ({
102
+ useNcpChildSessionTabsView: () => mocks.resolvedChildTabs,
104
103
  }),
105
- }));
104
+ );
105
+
106
+ vi.mock(
107
+ "@/components/chat/ncp/session-conversation/use-ncp-session-conversation",
108
+ () => ({
109
+ useNcpSessionConversation: () => ({
110
+ visibleMessages: [],
111
+ isHydrating: false,
112
+ hydrateError: null,
113
+ isRunning: false,
114
+ }),
115
+ }),
116
+ );
106
117
 
107
- vi.mock('@/components/common/AgentAvatar', () => ({
118
+ vi.mock("@/components/common/AgentAvatar", () => ({
108
119
  AgentAvatar: ({ agentId }: { agentId: string }) => (
109
120
  <div data-testid="agent-avatar">{agentId}</div>
110
121
  ),
111
122
  }));
112
123
 
113
- vi.mock('@/components/common/agent-identity', () => ({
124
+ vi.mock("@/components/common/agent-identity", () => ({
114
125
  AgentIdentityAvatar: ({ agentId }: { agentId: string }) => (
115
126
  <div data-testid="agent-identity-avatar">{agentId}</div>
116
127
  ),
117
128
  }));
118
129
 
119
- describe('ChatConversationPanel', () => {
130
+ describe("ChatConversationPanel", () => {
120
131
  beforeEach(() => {
121
132
  mocks.deleteSession.mockReset();
122
133
  mocks.goToProviders.mockReset();
123
134
  mocks.createSession.mockReset();
124
135
  mocks.setSelectedAgentId.mockReset();
125
136
  mocks.setPendingSessionType.mockReset();
137
+ mocks.stickyBottomScroll.mockClear();
126
138
  useChatInputStore.setState({
127
139
  snapshot: {
128
140
  ...useChatInputStore.getState().snapshot,
129
- defaultSessionType: 'native'
130
- }
141
+ defaultSessionType: "native",
142
+ },
131
143
  });
132
144
  useChatThreadStore.setState({
133
145
  snapshot: {
134
146
  ...useChatThreadStore.getState().snapshot,
135
147
  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',
148
+ modelOptions: [
149
+ {
150
+ value: "openai/gpt-5.1",
151
+ modelLabel: "gpt-5.1",
152
+ providerLabel: "OpenAI",
153
+ } as never,
154
+ ],
155
+ sessionTypeLabel: "Codex",
156
+ sessionKey: "draft-session-1",
139
157
  sessionDisplayName: undefined,
140
158
  agentId: null,
141
159
  agentDisplayName: null,
@@ -150,121 +168,127 @@ describe('ChatConversationPanel', () => {
150
168
  parentSessionKey: null,
151
169
  parentSessionLabel: null,
152
170
  availableAgents: [
153
- { id: 'main', displayName: 'Main', runtime: 'native' },
154
- { id: 'engineer', displayName: 'Engineer', runtime: 'codex' }
171
+ { id: "main", displayName: "Main", runtime: "native" },
172
+ { id: "engineer", displayName: "Engineer", runtime: "codex" },
155
173
  ],
156
174
  childSessionTabs: [],
157
175
  activeChildSessionKey: null,
158
- }
176
+ },
159
177
  });
160
178
  });
161
179
 
162
- it('shows the draft session type in the conversation header', () => {
180
+ it("shows the draft session type in the conversation header", () => {
163
181
  render(<ChatConversationPanel />);
164
182
 
165
- expect(screen.getByText('New Task')).toBeTruthy();
166
- expect(screen.getByText('Codex')).toBeTruthy();
167
- expect(screen.getByLabelText('More actions')).toBeTruthy();
183
+ expect(screen.getByText("New Task")).toBeTruthy();
184
+ expect(screen.getByText("Codex")).toBeTruthy();
185
+ expect(screen.getByLabelText("More actions")).toBeTruthy();
168
186
  });
169
187
 
170
- it('shows the selected session project badge and more actions trigger', () => {
188
+ it("shows the selected session project badge and more actions trigger", () => {
171
189
  useChatThreadStore.setState({
172
190
  snapshot: {
173
191
  ...useChatThreadStore.getState().snapshot,
174
- sessionKey: 'session-1',
175
- sessionDisplayName: 'Project Thread',
176
- sessionProjectRoot: '/Users/demo/workspace/project-alpha',
177
- sessionProjectName: 'project-alpha',
192
+ sessionKey: "session-1",
193
+ sessionDisplayName: "Project Thread",
194
+ sessionProjectRoot: "/Users/demo/workspace/project-alpha",
195
+ sessionProjectName: "project-alpha",
178
196
  canDeleteSession: true,
179
- }
197
+ },
180
198
  });
181
199
 
182
200
  render(<ChatConversationPanel />);
183
201
 
184
- expect(screen.getByText('Project Thread')).toBeTruthy();
185
- expect(screen.getByText('project-alpha')).toBeTruthy();
186
- expect(screen.getByLabelText('More actions')).toBeTruthy();
202
+ expect(screen.getByText("Project Thread")).toBeTruthy();
203
+ expect(screen.getByText("project-alpha")).toBeTruthy();
204
+ expect(screen.getByLabelText("More actions")).toBeTruthy();
187
205
  });
188
206
 
189
- it('does not show a header agent marker for the main agent', () => {
207
+ it("does not show a header agent marker for the main agent", () => {
190
208
  useChatThreadStore.setState({
191
209
  snapshot: {
192
210
  ...useChatThreadStore.getState().snapshot,
193
- agentId: 'main',
194
- agentDisplayName: 'Main',
195
- }
211
+ agentId: "main",
212
+ agentDisplayName: "Main",
213
+ },
196
214
  });
197
215
 
198
216
  render(<ChatConversationPanel />);
199
217
 
200
- expect(screen.queryByTestId('agent-avatar')).toBeNull();
218
+ expect(screen.queryByTestId("agent-avatar")).toBeNull();
201
219
  });
202
220
 
203
- it('shows only a lightweight avatar marker for a specialist agent', () => {
221
+ it("shows only a lightweight avatar marker for a specialist agent", () => {
204
222
  useChatThreadStore.setState({
205
223
  snapshot: {
206
224
  ...useChatThreadStore.getState().snapshot,
207
- agentId: 'engineer',
208
- agentDisplayName: 'Engineer',
209
- }
225
+ agentId: "engineer",
226
+ agentDisplayName: "Engineer",
227
+ },
210
228
  });
211
229
 
212
230
  render(<ChatConversationPanel />);
213
231
 
214
- expect(screen.getByTestId('agent-avatar').textContent).toBe('engineer');
215
- expect(screen.queryByText('Engineer')).toBeNull();
232
+ expect(screen.getByTestId("agent-avatar").textContent).toBe("engineer");
233
+ expect(screen.queryByText("Engineer")).toBeNull();
216
234
  });
217
235
 
218
- it('creates a draft session with the selected draft agent runtime', async () => {
236
+ it("creates a draft session with the selected draft agent runtime", async () => {
219
237
  const user = userEvent.setup();
220
238
 
221
239
  useChatThreadStore.setState({
222
240
  snapshot: {
223
241
  ...useChatThreadStore.getState().snapshot,
224
- agentId: 'engineer',
225
- agentDisplayName: 'Engineer'
226
- }
242
+ agentId: "engineer",
243
+ agentDisplayName: "Engineer",
244
+ },
227
245
  });
228
246
 
229
247
  render(<ChatConversationPanel />);
230
248
 
231
- await user.click(screen.getByRole('button', { name: 'create draft session' }));
249
+ await user.click(
250
+ screen.getByRole("button", { name: "create draft session" }),
251
+ );
232
252
 
233
- expect(mocks.createSession).toHaveBeenCalledWith('codex');
253
+ expect(mocks.createSession).toHaveBeenCalledWith("codex");
234
254
  });
235
255
 
236
- it('syncs the pending session type when switching the draft agent', async () => {
256
+ it("syncs the pending session type when switching the draft agent", async () => {
237
257
  const user = userEvent.setup();
238
258
 
239
259
  render(<ChatConversationPanel />);
240
260
 
241
- await user.click(screen.getByRole('button', { name: 'switch draft agent' }));
261
+ await user.click(
262
+ screen.getByRole("button", { name: "switch draft agent" }),
263
+ );
242
264
 
243
- expect(mocks.setSelectedAgentId).toHaveBeenCalledWith('engineer');
244
- expect(mocks.setPendingSessionType).toHaveBeenCalledWith('codex');
265
+ expect(mocks.setSelectedAgentId).toHaveBeenCalledWith("engineer");
266
+ expect(mocks.setPendingSessionType).toHaveBeenCalledWith("codex");
245
267
  });
246
268
  });
247
269
 
248
- describe('ChatChildSessionPanel', () => {
249
- it('keeps the header compact for a single child session', () => {
270
+ describe("ChatChildSessionPanel", () => {
271
+ it("keeps the header compact for a single child session", () => {
250
272
  mocks.resolvedChildTabs = [
251
273
  {
252
- sessionKey: 'child-session-1',
253
- parentSessionKey: 'parent-session-1',
254
- title: '北京天气',
255
- agentId: 'weather',
256
- agentDisplayName: 'Weather',
257
- agentAvatarUrl: null,
274
+ sessionKey: "child-session-1",
275
+ parentSessionKey: "parent-session-1",
276
+ title: "北京天气",
277
+ agentId: "weather",
278
+ sessionTypeLabel: "Codex",
279
+ preferredModel: "openai/gpt-5.3-codex",
280
+ projectName: "project-alpha",
281
+ projectRoot: "/Users/demo/project-alpha",
258
282
  },
259
283
  ];
260
284
  render(
261
285
  <ChatChildSessionPanel
262
286
  tabs={[
263
287
  {
264
- sessionKey: 'child-session-1',
265
- parentSessionKey: 'parent-session-1',
266
- label: '北京天气',
267
- agentId: 'weather',
288
+ sessionKey: "child-session-1",
289
+ parentSessionKey: "parent-session-1",
290
+ label: "北京天气",
291
+ agentId: "weather",
268
292
  },
269
293
  ]}
270
294
  activeSessionKey="child-session-1"
@@ -274,28 +298,42 @@ describe('ChatChildSessionPanel', () => {
274
298
  />,
275
299
  );
276
300
 
277
- expect(screen.getByText('北京天气')).toBeTruthy();
278
- expect(screen.queryByText('Child Sessions')).toBeNull();
279
- expect(screen.queryByText('child-session-1')).toBeNull();
301
+ expect(screen.getByText("北京天气")).toBeTruthy();
302
+ expect(screen.getByText("Codex")).toBeTruthy();
303
+ expect(screen.getByText("openai/gpt-5.3-codex")).toBeTruthy();
304
+ expect(screen.getByText("project-alpha")).toBeTruthy();
305
+ expect(screen.getByText("/Users/demo/project-alpha")).toBeTruthy();
306
+ expect(screen.queryByText("Child Sessions")).toBeNull();
307
+ expect(screen.queryByText("child-session-1")).toBeNull();
308
+ expect(mocks.stickyBottomScroll).toHaveBeenCalledWith(
309
+ expect.objectContaining({
310
+ resetKey: "child-session-1",
311
+ stickyThresholdPx: 20,
312
+ }),
313
+ );
280
314
  });
281
315
 
282
- it('uses tabs as the only title layer when multiple child sessions are open', () => {
316
+ it("uses tabs as the only title layer when multiple child sessions are open", () => {
283
317
  mocks.resolvedChildTabs = [
284
318
  {
285
- sessionKey: 'child-session-1',
286
- parentSessionKey: 'parent-session-1',
287
- title: '北京天气',
288
- agentId: 'weather',
289
- agentDisplayName: 'Weather',
290
- agentAvatarUrl: null,
319
+ sessionKey: "child-session-1",
320
+ parentSessionKey: "parent-session-1",
321
+ title: "北京天气",
322
+ agentId: "weather",
323
+ sessionTypeLabel: "Codex",
324
+ preferredModel: "openai/gpt-5.3-codex",
325
+ projectName: "project-alpha",
326
+ projectRoot: "/Users/demo/project-alpha",
291
327
  },
292
328
  {
293
- sessionKey: 'child-session-2',
294
- parentSessionKey: 'parent-session-1',
295
- title: '上海天气',
296
- agentId: 'weather',
297
- agentDisplayName: 'Weather',
298
- agentAvatarUrl: null,
329
+ sessionKey: "child-session-2",
330
+ parentSessionKey: "parent-session-1",
331
+ title: "上海天气",
332
+ agentId: "weather",
333
+ sessionTypeLabel: "Claude Code",
334
+ preferredModel: "anthropic/claude-sonnet-4",
335
+ projectName: "project-beta",
336
+ projectRoot: "/Users/demo/project-beta",
299
337
  },
300
338
  ];
301
339
 
@@ -303,16 +341,16 @@ describe('ChatChildSessionPanel', () => {
303
341
  <ChatChildSessionPanel
304
342
  tabs={[
305
343
  {
306
- sessionKey: 'child-session-1',
307
- parentSessionKey: 'parent-session-1',
308
- label: '北京天气',
309
- agentId: 'weather',
344
+ sessionKey: "child-session-1",
345
+ parentSessionKey: "parent-session-1",
346
+ label: "北京天气",
347
+ agentId: "weather",
310
348
  },
311
349
  {
312
- sessionKey: 'child-session-2',
313
- parentSessionKey: 'parent-session-1',
314
- label: '上海天气',
315
- agentId: 'weather',
350
+ sessionKey: "child-session-2",
351
+ parentSessionKey: "parent-session-1",
352
+ label: "上海天气",
353
+ agentId: "weather",
316
354
  },
317
355
  ]}
318
356
  activeSessionKey="child-session-1"
@@ -322,13 +360,17 @@ describe('ChatChildSessionPanel', () => {
322
360
  />,
323
361
  );
324
362
 
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
- );
363
+ expect(screen.getAllByText("北京天气")).toHaveLength(1);
364
+ expect(screen.getByText("上海天气")).toBeTruthy();
365
+ expect(screen.getByText("Codex")).toBeTruthy();
366
+ expect(screen.getByText("openai/gpt-5.3-codex")).toBeTruthy();
367
+ expect(screen.getByText("project-alpha")).toBeTruthy();
368
+ expect(screen.getByText("/Users/demo/project-alpha")).toBeTruthy();
369
+ const tabButtons = screen
370
+ .getAllByRole("button")
371
+ .filter((element) => element.getAttribute("aria-pressed") !== null);
330
372
  expect(tabButtons).toHaveLength(2);
331
- expect(tabButtons[0]?.getAttribute('aria-pressed')).toBe('true');
332
- expect(tabButtons[1]?.getAttribute('aria-pressed')).toBe('false');
373
+ expect(tabButtons[0]?.getAttribute("aria-pressed")).toBe("true");
374
+ expect(tabButtons[1]?.getAttribute("aria-pressed")).toBe("false");
333
375
  });
334
376
  });
@@ -33,19 +33,19 @@ function adapt(uiMessages: ChatMessageSource[]) {
33
33
  });
34
34
  }
35
35
 
36
- it("exposes agentId on spawn call cards when the invocation args include it", () => {
36
+ it("exposes agentId on sessions_spawn call cards when the invocation args include it", () => {
37
37
  const adapted = adapt([
38
38
  {
39
- id: "assistant-spawn-call",
39
+ id: "assistant-sessions-spawn-call",
40
40
  role: "assistant",
41
41
  parts: [
42
42
  {
43
43
  type: "tool-invocation",
44
44
  toolInvocation: {
45
45
  status: ToolInvocationStatus.PARTIAL_CALL,
46
- toolCallId: "spawn-call-args-1",
47
- toolName: "spawn",
48
- args: '{"agentId":"planner-agent","label":"Planner","task":"Plan the rollout"}',
46
+ toolCallId: "sessions-spawn-call-args-1",
47
+ toolName: "sessions_spawn",
48
+ args: '{"agentId":"planner-agent","scope":"child","title":"Planner","task":"Plan the rollout","request":{"notify":"final_reply"}}',
49
49
  result: {
50
50
  kind: "nextclaw.session_request",
51
51
  requestId: "request-3",
@@ -64,7 +64,7 @@ it("exposes agentId on spawn call cards when the invocation args include it", ()
64
64
  expect(adapted[0]?.parts[0]).toMatchObject({
65
65
  type: "tool-card",
66
66
  card: {
67
- toolName: "spawn",
67
+ toolName: "sessions_spawn",
68
68
  agentId: "planner-agent",
69
69
  statusTone: "running",
70
70
  },
@@ -74,16 +74,16 @@ it("exposes agentId on spawn call cards when the invocation args include it", ()
74
74
  it("exposes agentId on running tool call cards even before a session-request result exists", () => {
75
75
  const adapted = adapt([
76
76
  {
77
- id: "assistant-spawn-call-running",
77
+ id: "assistant-sessions-spawn-call-running",
78
78
  role: "assistant",
79
79
  parts: [
80
80
  {
81
81
  type: "tool-invocation",
82
82
  toolInvocation: {
83
83
  status: ToolInvocationStatus.PARTIAL_CALL,
84
- toolCallId: "spawn-call-running-1",
85
- toolName: "spawn",
86
- args: '{"agentId":"planner-agent","task":"Plan the rollout"}',
84
+ toolCallId: "sessions-spawn-call-running-1",
85
+ toolName: "sessions_spawn",
86
+ args: '{"agentId":"planner-agent","scope":"child","task":"Plan the rollout"}',
87
87
  },
88
88
  },
89
89
  ],
@@ -93,7 +93,7 @@ it("exposes agentId on running tool call cards even before a session-request res
93
93
  expect(adapted[0]?.parts[0]).toMatchObject({
94
94
  type: "tool-card",
95
95
  card: {
96
- toolName: "spawn",
96
+ toolName: "sessions_spawn",
97
97
  agentId: "planner-agent",
98
98
  statusTone: "running",
99
99
  titleLabel: "Tool Call",