@nextclaw/ui 0.12.3 → 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 (123) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/dist/assets/{ChannelsList-DZWam3Ob.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-C7-1sXqo.js → DocBrowser-QUZ3nfmH.js} +1 -1
  6. package/dist/assets/{DocBrowserContext-DN5tjUoS.js → DocBrowserContext-CpiIfhJO.js} +1 -1
  7. package/dist/assets/{LogoBadge-DDS1sU_U.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-COnjm8_x.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-Cefg1LFJ.js → SecretsConfig-Bew4EF2A.js} +2 -2
  18. package/dist/assets/{SessionsConfig-BZnmVTIu.js → SessionsConfig-2r2yAGZg.js} +2 -2
  19. package/dist/assets/{book-open-DvWqOode.js → book-open-CJG8Yz3U.js} +1 -1
  20. package/dist/assets/{chat-session-display-D4bYa0b8.js → chat-session-display-DkAC5OMC.js} +1 -1
  21. package/dist/assets/{chunk-JZWAC4HX-CxfKRD7X.js → chunk-JZWAC4HX-D5b3Iyas.js} +1 -1
  22. package/dist/assets/{config-BeGwf2Ao.js → config-zvnxSXSP.js} +1 -1
  23. package/dist/assets/{createLucideIcon-C7MmdIX3.js → createLucideIcon-_FMJqZw2.js} +1 -1
  24. package/dist/assets/{dist-B6VMuIQN.js → dist-B1fpOuON.js} +1 -1
  25. package/dist/assets/{dist-RWNFhxvR.js → dist-BCXX7FD-.js} +2 -2
  26. package/dist/assets/{external-link-U86Acd1t.js → external-link-b7gAJWYY.js} +1 -1
  27. package/dist/assets/{hash-D-OVfV3Z.js → hash-Bhy4TwfZ.js} +1 -1
  28. package/dist/assets/i18n-DJg9BPYk.js +1 -0
  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-U1_qDA3U.js → logos-GMeYU9vc.js} +1 -1
  34. package/dist/assets/{page-layout-Z1klaUFW.js → page-layout-C8UbWuMt.js} +1 -1
  35. package/dist/assets/plus-CR7RfK3H.js +1 -0
  36. package/dist/assets/{popover-xWbqMnIN.js → popover-8HSx9wQj.js} +1 -1
  37. package/dist/assets/react-BB4jko2M.js +1 -0
  38. package/dist/assets/{refresh-ccw-JQh1lwq-.js → refresh-ccw-CA4_C7Zg.js} +1 -1
  39. package/dist/assets/{save-4VRlzkii.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-DF-AUoie.js → select-xp_Ac8ip.js} +1 -1
  43. package/dist/assets/skeleton-uxz_5h3A.js +1 -0
  44. package/dist/assets/{status-dot-Bq_8Ojvv.js → status-dot-Cn4Pp7DZ.js} +1 -1
  45. package/dist/assets/{switch-D7JF_RZ-.js → switch-BTi6UOij.js} +1 -1
  46. package/dist/assets/{tabs-custom-CLksZ2bO.js → tabs-custom-BiiN8DME.js} +1 -1
  47. package/dist/assets/{trash-2-VV8jvziy.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-CuQqiPx7.js → useConfirmDialog-BJIwUZjH.js} +1 -1
  50. package/dist/assets/{useMutation-DBTWPbTg.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 +5 -5
  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/ChatSidebar.test.tsx +168 -28
  60. package/src/components/chat/ChatSidebar.tsx +103 -28
  61. package/src/components/chat/adapters/chat-message-tool-agent-id.test.ts +11 -11
  62. package/src/components/chat/adapters/chat-message.adapter.test.ts +43 -6
  63. package/src/components/chat/adapters/chat-message.session-request-tool-card.ts +182 -44
  64. package/src/components/chat/adapters/chat-message.session-spawn-tool-card.test.ts +104 -0
  65. package/src/components/chat/chat-child-session-panel.tsx +103 -45
  66. package/src/components/chat/chat-page-runtime.test.ts +16 -19
  67. package/src/components/chat/chat-session-preference-sync.test.ts +13 -0
  68. package/src/components/chat/chat-session-preference-sync.ts +9 -7
  69. package/src/components/chat/chat-sidebar-list-mode-switch.tsx +43 -0
  70. package/src/components/chat/chat-sidebar-project-groups.tsx +152 -0
  71. package/src/components/chat/hooks/use-chat-session-project.test.tsx +5 -5
  72. package/src/components/chat/hooks/use-chat-session-project.ts +0 -5
  73. package/src/components/chat/hooks/use-chat-session-update.test.tsx +75 -0
  74. package/src/components/chat/hooks/use-chat-session-update.ts +4 -2
  75. package/src/components/chat/managers/chat-session-list.manager.test.ts +46 -6
  76. package/src/components/chat/managers/chat-session-list.manager.ts +19 -6
  77. package/src/components/chat/ncp/NcpChatPage.tsx +33 -38
  78. package/src/components/chat/ncp/ncp-chat-input.manager.ts +3 -5
  79. package/src/components/chat/ncp/ncp-chat-page-data.ts +0 -1
  80. package/src/components/chat/ncp/ncp-chat.presenter.ts +2 -16
  81. package/src/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view.ts +20 -7
  82. package/src/components/chat/session-header/chat-session-project-badge.test.tsx +16 -0
  83. package/src/components/chat/session-header/chat-session-project-badge.tsx +2 -2
  84. package/src/components/chat/stores/chat-session-list.store.ts +3 -0
  85. package/src/components/chat/useChatSessionTypeState.test.tsx +0 -3
  86. package/src/components/chat/useChatSessionTypeState.ts +3 -5
  87. package/src/components/config/ChannelsList.test.tsx +68 -0
  88. package/src/components/config/ChannelsList.tsx +22 -4
  89. package/src/components/config/ProvidersList.tsx +17 -3
  90. package/src/components/config/providers-list.test.tsx +68 -0
  91. package/src/components/layout/Sidebar.tsx +13 -13
  92. package/src/components/layout/sidebar.layout.test.tsx +32 -1
  93. package/src/components/marketplace/MarketplacePage.tsx +30 -30
  94. package/src/components/marketplace/marketplace-page-parts.tsx +16 -24
  95. package/src/components/marketplace/mcp/McpMarketplacePage.tsx +28 -26
  96. package/src/hooks/marketplace-list-pages.ts +27 -0
  97. package/src/hooks/use-infinite-scroll-loader.ts +88 -0
  98. package/src/hooks/useMarketplace.ts +14 -3
  99. package/src/hooks/useMcpMarketplace.ts +14 -3
  100. package/src/lib/i18n.chat.ts +3 -0
  101. package/src/lib/i18n.remote.ts +15 -0
  102. package/dist/assets/ChatPage-YBL7iJ1X.js +0 -43
  103. package/dist/assets/DocBrowser-DQjtSsY3.js +0 -1
  104. package/dist/assets/MarketplacePage-2tWWgwAb.js +0 -49
  105. package/dist/assets/MarketplacePage-BorWJftJ.js +0 -1
  106. package/dist/assets/McpMarketplacePage-N-fB4HID.js +0 -40
  107. package/dist/assets/ModelConfig-DvsBTUiE.js +0 -1
  108. package/dist/assets/ProviderScopedModelInput-D9woCARc.js +0 -1
  109. package/dist/assets/ProvidersList-D-qPGgC4.js +0 -1
  110. package/dist/assets/RuntimeConfig-BHpqcaHm.js +0 -1
  111. package/dist/assets/SearchConfig-DIT6M65Q.js +0 -1
  112. package/dist/assets/i18n-hM3v-3YG.js +0 -1
  113. package/dist/assets/index-CpxuJa9o.css +0 -1
  114. package/dist/assets/index-DHmCjcxq.js +0 -6
  115. package/dist/assets/label-CHJ1ATds.js +0 -1
  116. package/dist/assets/loader-circle-C8cpaL0w.js +0 -1
  117. package/dist/assets/marketplace-localization-CxSTG9wr.js +0 -1
  118. package/dist/assets/plus-CrkO1kob.js +0 -1
  119. package/dist/assets/react-3YE87-lE.js +0 -1
  120. package/dist/assets/search-EX-Papzl.js +0 -1
  121. package/dist/assets/security-config-DEgOD4VX.js +0 -1
  122. package/dist/assets/skeleton-B0mmt1vo.js +0 -1
  123. package/dist/assets/x-B4sxJkGY.js +0 -1
@@ -9,6 +9,7 @@ import { useChatSessionListStore } from '@/components/chat/stores/chat-session-l
9
9
  const mocks = vi.hoisted(() => ({
10
10
  createSession: vi.fn(),
11
11
  setQuery: vi.fn(),
12
+ setListMode: vi.fn(),
12
13
  selectSession: vi.fn(),
13
14
  docOpen: vi.fn(),
14
15
  updateNcpSession: vi.fn(),
@@ -26,6 +27,7 @@ vi.mock('@/components/chat/presenter/chat-presenter-context', () => ({
26
27
  chatSessionListManager: {
27
28
  createSession: mocks.createSession,
28
29
  setQuery: mocks.setQuery,
30
+ setListMode: mocks.setListMode,
29
31
  selectSession: mocks.selectSession
30
32
  }
31
33
  })
@@ -99,35 +101,39 @@ vi.mock('@/stores/ui.store', () => ({
99
101
  selector({ connectionStatus: 'connected' })
100
102
  }));
101
103
 
102
- describe('ChatSidebar', () => {
103
- beforeEach(() => {
104
- mocks.createSession.mockReset();
105
- mocks.setQuery.mockReset();
106
- mocks.selectSession.mockReset();
107
- mocks.docOpen.mockReset();
108
- mocks.updateNcpSession.mockReset();
109
- mocks.updateNcpSession.mockResolvedValue({});
110
- mocks.agents = [];
111
- mocks.sessionItems = [];
112
- mocks.isLoading = false;
113
-
114
- useChatInputStore.setState({
115
- snapshot: {
116
- ...useChatInputStore.getState().snapshot,
117
- defaultSessionType: 'native',
118
- sessionTypeOptions: [
119
- { value: 'native', label: 'Native', ready: true },
120
- { value: 'codex', label: 'Codex', ready: true }
121
- ]
122
- }
123
- });
124
- useChatSessionListStore.setState({
125
- snapshot: {
126
- ...useChatSessionListStore.getState().snapshot,
127
- query: ''
128
- }
129
- });
104
+ function resetSidebarTestState() {
105
+ mocks.createSession.mockReset();
106
+ mocks.setQuery.mockReset();
107
+ mocks.setListMode.mockReset();
108
+ mocks.selectSession.mockReset();
109
+ mocks.docOpen.mockReset();
110
+ mocks.updateNcpSession.mockReset();
111
+ mocks.updateNcpSession.mockResolvedValue({});
112
+ mocks.agents = [];
113
+ mocks.sessionItems = [];
114
+ mocks.isLoading = false;
115
+
116
+ useChatInputStore.setState({
117
+ snapshot: {
118
+ ...useChatInputStore.getState().snapshot,
119
+ defaultSessionType: 'native',
120
+ sessionTypeOptions: [
121
+ { value: 'native', label: 'Native', ready: true },
122
+ { value: 'codex', label: 'Codex', ready: true }
123
+ ]
124
+ }
125
+ });
126
+ useChatSessionListStore.setState({
127
+ snapshot: {
128
+ ...useChatSessionListStore.getState().snapshot,
129
+ query: '',
130
+ listMode: 'time-first'
131
+ }
130
132
  });
133
+ }
134
+
135
+ describe('ChatSidebar create and list basics', () => {
136
+ beforeEach(resetSidebarTestState);
131
137
 
132
138
  it('closes the create-session menu after choosing a non-default session type', async () => {
133
139
  render(
@@ -174,6 +180,19 @@ describe('ChatSidebar', () => {
174
180
  expect(screen.getByText('Configure a provider API key first.')).not.toBeNull();
175
181
  });
176
182
 
183
+ it('renders the lightweight list mode switch in the session header row and toggles to project view', () => {
184
+ render(
185
+ <MemoryRouter>
186
+ <ChatSidebar />
187
+ </MemoryRouter>
188
+ );
189
+
190
+ expect(screen.getByText('Sessions')).not.toBeNull();
191
+ fireEvent.click(screen.getByRole('button', { name: 'Project' }));
192
+
193
+ expect(mocks.setListMode).toHaveBeenCalledWith('project-first');
194
+ });
195
+
177
196
  it('shows a session type badge for non-native sessions in the list', () => {
178
197
  mocks.sessionItems = [
179
198
  createSessionItem({
@@ -255,6 +274,127 @@ describe('ChatSidebar', () => {
255
274
  expect(screen.getByText('Native Task')).not.toBeNull();
256
275
  expect(screen.queryByText('Native')).toBeNull();
257
276
  });
277
+ });
278
+
279
+ describe('ChatSidebar project-first mode', () => {
280
+ beforeEach(resetSidebarTestState);
281
+
282
+ it('shows project groups only in project-first mode and hides sessions without a project', () => {
283
+ useChatSessionListStore.setState({
284
+ snapshot: {
285
+ ...useChatSessionListStore.getState().snapshot,
286
+ listMode: 'project-first'
287
+ }
288
+ });
289
+ mocks.sessionItems = [
290
+ createSessionItem({
291
+ key: 'session:project-1',
292
+ createdAt: '2026-03-19T09:00:00.000Z',
293
+ updatedAt: '2026-03-19T11:05:00.000Z',
294
+ label: 'Project Alpha Task',
295
+ projectRoot: '/tmp/project-alpha',
296
+ projectName: 'project-alpha',
297
+ sessionType: 'native',
298
+ sessionTypeMutable: false,
299
+ messageCount: 2
300
+ }),
301
+ createSessionItem({
302
+ key: 'session:plain-1',
303
+ createdAt: '2026-03-19T08:00:00.000Z',
304
+ updatedAt: '2026-03-19T08:05:00.000Z',
305
+ label: 'Loose Task',
306
+ sessionType: 'native',
307
+ sessionTypeMutable: false,
308
+ messageCount: 1
309
+ })
310
+ ];
311
+
312
+ render(
313
+ <MemoryRouter>
314
+ <ChatSidebar />
315
+ </MemoryRouter>
316
+ );
317
+
318
+ expect(screen.getByText('project-alpha')).not.toBeNull();
319
+ expect(screen.getByText('Project Alpha Task')).not.toBeNull();
320
+ expect(screen.queryByText('Loose Task')).toBeNull();
321
+ });
322
+
323
+ it('lets the user choose a runtime type when creating a project-bound draft', () => {
324
+ useChatSessionListStore.setState({
325
+ snapshot: {
326
+ ...useChatSessionListStore.getState().snapshot,
327
+ listMode: 'project-first'
328
+ }
329
+ });
330
+ mocks.sessionItems = [
331
+ createSessionItem({
332
+ key: 'session:project-2',
333
+ createdAt: '2026-03-19T09:00:00.000Z',
334
+ updatedAt: '2026-03-19T11:05:00.000Z',
335
+ label: 'Grouped Task',
336
+ projectRoot: '/tmp/project-beta',
337
+ projectName: 'project-beta',
338
+ sessionType: 'native',
339
+ sessionTypeMutable: false,
340
+ messageCount: 2
341
+ })
342
+ ];
343
+
344
+ render(
345
+ <MemoryRouter>
346
+ <ChatSidebar />
347
+ </MemoryRouter>
348
+ );
349
+
350
+ fireEvent.click(screen.getByRole('button', { name: 'New Task · project-beta' }));
351
+ fireEvent.click(screen.getByText('Codex'));
352
+
353
+ expect(mocks.createSession).toHaveBeenCalledWith('codex', '/tmp/project-beta');
354
+ });
355
+
356
+ it('creates immediately when there is only one available runtime type', () => {
357
+ useChatInputStore.setState({
358
+ snapshot: {
359
+ ...useChatInputStore.getState().snapshot,
360
+ defaultSessionType: 'native',
361
+ sessionTypeOptions: [{ value: 'native', label: 'Native', ready: true }]
362
+ }
363
+ });
364
+ useChatSessionListStore.setState({
365
+ snapshot: {
366
+ ...useChatSessionListStore.getState().snapshot,
367
+ listMode: 'project-first'
368
+ }
369
+ });
370
+ mocks.sessionItems = [
371
+ createSessionItem({
372
+ key: 'session:project-3',
373
+ createdAt: '2026-03-19T09:00:00.000Z',
374
+ updatedAt: '2026-03-19T11:05:00.000Z',
375
+ label: 'Single Runtime Task',
376
+ projectRoot: '/tmp/project-gamma',
377
+ projectName: 'project-gamma',
378
+ sessionType: 'native',
379
+ sessionTypeMutable: false,
380
+ messageCount: 2
381
+ })
382
+ ];
383
+
384
+ render(
385
+ <MemoryRouter>
386
+ <ChatSidebar />
387
+ </MemoryRouter>
388
+ );
389
+
390
+ fireEvent.click(screen.getByRole('button', { name: 'New Task · project-gamma' }));
391
+
392
+ expect(mocks.createSession).toHaveBeenCalledWith('native', '/tmp/project-gamma');
393
+ });
394
+ });
395
+
396
+ describe('ChatSidebar session item interactions', () => {
397
+ beforeEach(resetSidebarTestState);
258
398
 
259
399
  it('hides the sidebar agent avatar for the main agent but keeps specialist avatars', () => {
260
400
  mocks.agents = [
@@ -7,6 +7,11 @@ import { Input } from '@/components/ui/input';
7
7
  import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
8
8
  import { SelectItem } from '@/components/ui/select';
9
9
  import { ChatSidebarSessionItem } from '@/components/chat/chat-sidebar-session-item';
10
+ import { ChatSidebarListModeSwitch } from '@/components/chat/chat-sidebar-list-mode-switch';
11
+ import {
12
+ ChatSidebarProjectGroups,
13
+ type ChatSidebarProjectGroup
14
+ } from '@/components/chat/chat-sidebar-project-groups';
10
15
  import { resolveSessionContextView } from '@/lib/session-context.utils';
11
16
  import { useChatSessionLabel } from '@/components/chat/hooks/use-chat-session-label';
12
17
  import { useNcpSessionListView, type NcpSessionListItemView } from '@/components/chat/ncp/use-ncp-session-list-view';
@@ -14,6 +19,7 @@ import { usePresenter } from '@/components/chat/presenter/chat-presenter-context
14
19
  import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
15
20
  import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
16
21
  import { useAgents } from '@/hooks/agents/useAgents';
22
+ import { getSessionProjectName } from '@/lib/session-project/session-project.utils';
17
23
  import { cn } from '@/lib/utils';
18
24
  import { LANGUAGE_OPTIONS, t, type I18nLanguage } from '@/lib/i18n';
19
25
  import { THEME_OPTIONS, type UiTheme } from '@/lib/theme';
@@ -41,6 +47,14 @@ type DateGroup = {
41
47
  items: NcpSessionListItemView[];
42
48
  };
43
49
 
50
+ function getSessionUpdatedAtTimestamp(item: NcpSessionListItemView): number {
51
+ return new Date(item.session.updatedAt).getTime();
52
+ }
53
+
54
+ function sortSessionItemsByUpdatedAtDesc(items: NcpSessionListItemView[]): NcpSessionListItemView[] {
55
+ return [...items].sort((left, right) => getSessionUpdatedAtTimestamp(right) - getSessionUpdatedAtTimestamp(left));
56
+ }
57
+
44
58
  function groupSessionsByDate(items: NcpSessionListItemView[]): DateGroup[] {
45
59
  const now = new Date();
46
60
  const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
@@ -74,6 +88,37 @@ function groupSessionsByDate(items: NcpSessionListItemView[]): DateGroup[] {
74
88
  return groups;
75
89
  }
76
90
 
91
+ function groupSessionsByProject(items: NcpSessionListItemView[]): ChatSidebarProjectGroup[] {
92
+ const grouped = new Map<string, ChatSidebarProjectGroup>();
93
+
94
+ for (const item of items) {
95
+ const projectRoot = item.session.projectRoot?.trim();
96
+ if (!projectRoot) {
97
+ continue;
98
+ }
99
+ const existingGroup = grouped.get(projectRoot);
100
+ const updatedAt = getSessionUpdatedAtTimestamp(item);
101
+ if (existingGroup) {
102
+ existingGroup.items.push(item);
103
+ existingGroup.latestUpdatedAt = Math.max(existingGroup.latestUpdatedAt, updatedAt);
104
+ continue;
105
+ }
106
+ grouped.set(projectRoot, {
107
+ projectRoot,
108
+ projectName: item.session.projectName?.trim() || getSessionProjectName(projectRoot) || projectRoot,
109
+ items: [item],
110
+ latestUpdatedAt: updatedAt
111
+ });
112
+ }
113
+
114
+ return [...grouped.values()]
115
+ .map((group) => ({
116
+ ...group,
117
+ items: sortSessionItemsByUpdatedAtDesc(group.items)
118
+ }))
119
+ .sort((left, right) => right.latestUpdatedAt - left.latestUpdatedAt);
120
+ }
121
+
77
122
  function sessionTitle(session: SessionEntryView): string {
78
123
  if (session.label && session.label.trim()) {
79
124
  return session.label.trim();
@@ -120,12 +165,15 @@ export function ChatSidebar() {
120
165
  [agentsQuery.data?.agents]
121
166
  );
122
167
 
123
- const groups = useMemo(() => groupSessionsByDate(items), [items]);
168
+ const sortedItems = useMemo(() => sortSessionItemsByUpdatedAtDesc(items), [items]);
169
+ const groups = useMemo(() => groupSessionsByDate(sortedItems), [sortedItems]);
170
+ const projectGroups = useMemo(() => groupSessionsByProject(sortedItems), [sortedItems]);
124
171
  const defaultSessionType = inputSnapshot.defaultSessionType || 'native';
125
172
  const nonDefaultSessionTypeOptions = useMemo(
126
173
  () => inputSnapshot.sessionTypeOptions.filter((option) => option.value !== defaultSessionType),
127
174
  [defaultSessionType, inputSnapshot.sessionTypeOptions]
128
175
  );
176
+ const isProjectFirstView = listSnapshot.listMode === 'project-first';
129
177
 
130
178
  const handleLanguageSwitch = (nextLang: I18nLanguage) => {
131
179
  if (language === nextLang) return;
@@ -164,6 +212,34 @@ export function ChatSidebar() {
164
212
  }
165
213
  };
166
214
 
215
+ const renderSessionItem = ({ session, runStatus }: NcpSessionListItemView) => {
216
+ const active = listSnapshot.selectedSessionKey === session.key;
217
+ const context = resolveSessionContextView(session, inputSnapshot.sessionTypeOptions);
218
+ const isEditing = editingSessionKey === session.key;
219
+ const isSaving = savingSessionKey === session.key;
220
+ return (
221
+ <ChatSidebarSessionItem
222
+ key={session.key}
223
+ session={session}
224
+ active={active}
225
+ runStatus={runStatus}
226
+ context={context}
227
+ title={sessionTitle(session)}
228
+ agentId={session.agentId ?? null}
229
+ agentLabel={session.agentId ? (agentsById.get(session.agentId)?.displayName ?? session.agentId) : null}
230
+ agentAvatarUrl={session.agentId ? (agentsById.get(session.agentId)?.avatarUrl ?? null) : null}
231
+ isEditing={isEditing}
232
+ draftLabel={draftLabel}
233
+ isSaving={isSaving}
234
+ onSelect={() => presenter.chatSessionListManager.selectSession(session.key)}
235
+ onStartEditing={() => startEditingSessionLabel(session)}
236
+ onDraftLabelChange={setDraftLabel}
237
+ onSave={() => saveSessionLabel(session)}
238
+ onCancel={cancelEditingSessionLabel}
239
+ />
240
+ );
241
+ };
242
+
167
243
  return (
168
244
  <aside className="w-[280px] shrink-0 flex flex-col h-full bg-secondary border-r border-gray-200/60">
169
245
  <div className="px-5 pt-5 pb-3">
@@ -272,9 +348,34 @@ export function ChatSidebar() {
272
348
 
273
349
  <div className="mx-4 border-t border-gray-200/60" />
274
350
 
351
+ <div className="flex items-center justify-between px-5 pb-2 pt-3">
352
+ <div className="text-[11px] font-medium uppercase tracking-wider text-gray-400">
353
+ {t('chatSidebarTaskRecords')}
354
+ </div>
355
+ <ChatSidebarListModeSwitch
356
+ isProjectFirstView={isProjectFirstView}
357
+ onSelectMode={presenter.chatSessionListManager.setListMode}
358
+ />
359
+ </div>
360
+
275
361
  <div className="flex-1 min-h-0 overflow-y-auto custom-scrollbar px-3 py-2">
276
362
  {isLoading ? (
277
363
  <div className="text-xs text-gray-500 p-3">{t('sessionsLoading')}</div>
364
+ ) : isProjectFirstView ? (
365
+ projectGroups.length === 0 ? (
366
+ <div className="p-4 text-center">
367
+ <MessageSquareText className="h-6 w-6 mx-auto mb-2 text-gray-300" />
368
+ <div className="text-xs text-gray-500">{t('chatSidebarProjectViewEmpty')}</div>
369
+ </div>
370
+ ) : (
371
+ <ChatSidebarProjectGroups
372
+ groups={projectGroups}
373
+ defaultSessionType={defaultSessionType}
374
+ sessionTypeOptions={inputSnapshot.sessionTypeOptions}
375
+ renderSessionItem={renderSessionItem}
376
+ onCreateSession={presenter.chatSessionListManager.createSession}
377
+ />
378
+ )
278
379
  ) : groups.length === 0 ? (
279
380
  <div className="p-4 text-center">
280
381
  <MessageSquareText className="h-6 w-6 mx-auto mb-2 text-gray-300" />
@@ -288,33 +389,7 @@ export function ChatSidebar() {
288
389
  {group.label}
289
390
  </div>
290
391
  <div className="space-y-0.5">
291
- {group.items.map(({ session, runStatus }) => {
292
- const active = listSnapshot.selectedSessionKey === session.key;
293
- const context = resolveSessionContextView(session, inputSnapshot.sessionTypeOptions);
294
- const isEditing = editingSessionKey === session.key;
295
- const isSaving = savingSessionKey === session.key;
296
- return (
297
- <ChatSidebarSessionItem
298
- key={session.key}
299
- session={session}
300
- active={active}
301
- runStatus={runStatus}
302
- context={context}
303
- title={sessionTitle(session)}
304
- agentId={session.agentId ?? null}
305
- agentLabel={session.agentId ? (agentsById.get(session.agentId)?.displayName ?? session.agentId) : null}
306
- agentAvatarUrl={session.agentId ? (agentsById.get(session.agentId)?.avatarUrl ?? null) : null}
307
- isEditing={isEditing}
308
- draftLabel={draftLabel}
309
- isSaving={isSaving}
310
- onSelect={() => presenter.chatSessionListManager.selectSession(session.key)}
311
- onStartEditing={() => startEditingSessionLabel(session)}
312
- onDraftLabelChange={setDraftLabel}
313
- onSave={() => saveSessionLabel(session)}
314
- onCancel={cancelEditingSessionLabel}
315
- />
316
- );
317
- })}
392
+ {group.items.map(renderSessionItem)}
318
393
  </div>
319
394
  </div>
320
395
  ))}
@@ -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",
@@ -223,7 +223,7 @@ it("keeps structured terminal results as structured data instead of raw json out
223
223
  });
224
224
  });
225
225
 
226
- it("renders session request tool cards from structured child-session status updates", () => {
226
+ it("renders child-session request cards for sessions_spawn when the new child starts immediately", () => {
227
227
  const adapted = adapt([
228
228
  {
229
229
  id: "assistant-subagent",
@@ -233,18 +233,21 @@ it("renders session request tool cards from structured child-session status upda
233
233
  type: "tool-invocation",
234
234
  toolInvocation: {
235
235
  status: ToolInvocationStatus.RESULT,
236
- toolCallId: "spawn-call-1",
237
- toolName: "spawn",
238
- args: '{"label":"Verifier","task":"Verify 1+1=2"}',
236
+ toolCallId: "sessions-spawn-call-1",
237
+ toolName: "sessions_spawn",
238
+ args: '{"scope":"child","title":"Verifier","task":"Verify 1+1=2","request":{"notify":"final_reply"}}',
239
239
  result: {
240
240
  kind: "nextclaw.session_request",
241
241
  requestId: "request-1",
242
242
  sessionId: "child-session-1",
243
243
  agentId: "verifier-agent",
244
244
  isChildSession: true,
245
+ lifecycle: "persistent",
245
246
  title: "Verifier",
246
247
  task: "Verify 1+1=2",
247
248
  status: "completed",
249
+ notify: "final_reply",
250
+ spawnedByRequestId: "request-1",
248
251
  finalResponseText: "Verified 1+1=2.",
249
252
  parentSessionId: "parent-session-1",
250
253
  },
@@ -257,9 +260,17 @@ it("renders session request tool cards from structured child-session status upda
257
260
  expect(adapted[0]?.parts[0]).toMatchObject({
258
261
  type: "tool-card",
259
262
  card: {
260
- toolName: "spawn",
263
+ toolName: "sessions_spawn",
261
264
  agentId: "verifier-agent",
262
265
  summary: "title: Verifier · session: child-session-1 · task: Verify 1+1=2",
266
+ input: `{
267
+ "scope": "child",
268
+ "title": "Verifier",
269
+ "task": "Verify 1+1=2",
270
+ "request": {
271
+ "notify": "final_reply"
272
+ }
273
+ }`,
263
274
  output: [
264
275
  "Request ID: request-1",
265
276
  "",
@@ -267,6 +278,16 @@ it("renders session request tool cards from structured child-session status upda
267
278
  "",
268
279
  "Target: child",
269
280
  "",
281
+ "Status: completed",
282
+ "",
283
+ "Notify: final_reply",
284
+ "",
285
+ "Lifecycle: persistent",
286
+ "",
287
+ "Parent Session ID: parent-session-1",
288
+ "",
289
+ "Spawned By Request ID: request-1",
290
+ "",
270
291
  "Title: Verifier",
271
292
  "",
272
293
  "Task:",
@@ -302,16 +323,18 @@ it("renders regular session request tool cards with session navigation instead o
302
323
  status: ToolInvocationStatus.RESULT,
303
324
  toolCallId: "session-request-call-1",
304
325
  toolName: "sessions_request",
305
- args: '{"sessionId":"session-2","task":"Summarize the latest findings"}',
326
+ args: '{"target":{"session_id":"session-2"},"task":"Summarize the latest findings","notify":"none","title":"Research thread"}',
306
327
  result: {
307
328
  kind: "nextclaw.session_request",
308
329
  requestId: "request-2",
309
330
  sessionId: "session-2",
310
331
  agentId: "research-agent",
311
332
  isChildSession: false,
333
+ lifecycle: "persistent",
312
334
  title: "Research thread",
313
335
  task: "Summarize the latest findings",
314
336
  status: "completed",
337
+ notify: "none",
315
338
  finalResponseText: "Here is the summary.",
316
339
  },
317
340
  },
@@ -326,6 +349,14 @@ it("renders regular session request tool cards with session navigation instead o
326
349
  toolName: "sessions_request",
327
350
  agentId: "research-agent",
328
351
  summary: "title: Research thread · session: session-2 · task: Summarize the latest findings",
352
+ input: `{
353
+ "target": {
354
+ "session_id": "session-2"
355
+ },
356
+ "task": "Summarize the latest findings",
357
+ "notify": "none",
358
+ "title": "Research thread"
359
+ }`,
329
360
  output: [
330
361
  "Request ID: request-2",
331
362
  "",
@@ -333,6 +364,12 @@ it("renders regular session request tool cards with session navigation instead o
333
364
  "",
334
365
  "Target: session",
335
366
  "",
367
+ "Status: completed",
368
+ "",
369
+ "Notify: none",
370
+ "",
371
+ "Lifecycle: persistent",
372
+ "",
336
373
  "Title: Research thread",
337
374
  "",
338
375
  "Task:",