@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.
- package/CHANGELOG.md +66 -0
- package/dist/assets/ChannelsList-D8p4OlM6.js +8 -0
- package/dist/assets/ChatPage-A45t1Rmf.js +58 -0
- package/dist/assets/DocBrowser-B2MpsnU9.js +1 -0
- package/dist/assets/{DocBrowser-NSzgVKka.js → DocBrowser-Cse_F8Nn.js} +1 -1
- package/dist/assets/{DocBrowserContext-DpgVdRgk.js → DocBrowserContext-Bai1WU2H.js} +1 -1
- package/dist/assets/{LogoBadge-CHS4YNLw.js → LogoBadge-BdxMPc9v.js} +1 -1
- package/dist/assets/MarketplacePage-BNZ3Jx5d.js +1 -0
- package/dist/assets/MarketplacePage-BbpAkllU.js +49 -0
- package/dist/assets/McpMarketplacePage-CxPFOgxv.js +40 -0
- package/dist/assets/ModelConfig-3GLqQ5GY.js +1 -0
- package/dist/assets/ProviderScopedModelInput-BYNouw-i.js +1 -0
- package/dist/assets/ProvidersList-BR1gJ4Dm.js +1 -0
- package/dist/assets/{RemoteAccessPage-yfbrveNQ.js → RemoteAccessPage-DyYVWsyK.js} +1 -1
- package/dist/assets/RuntimeConfig-ChdfK4Y_.js +1 -0
- package/dist/assets/SearchConfig-DTeJvp8m.js +1 -0
- package/dist/assets/{SecretsConfig-CLFSSoTl.js → SecretsConfig-CCYO6NcV.js} +2 -2
- package/dist/assets/SessionsConfig-Du39vDgt.js +2 -0
- package/dist/assets/app-query-client-Dr5d-K8d.js +1 -0
- package/dist/assets/{book-open-C7TAghTk.js → book-open-Da4OEPqB.js} +1 -1
- package/dist/assets/chat-session-display-CAlPrnlV.js +1 -0
- package/dist/assets/{chunk-JZWAC4HX-DbL4EmiT.js → chunk-JZWAC4HX-CoFVxHXV.js} +1 -1
- package/dist/assets/client-CSk58DcF.js +7 -0
- package/dist/assets/config-D8KzikVB.js +1 -0
- package/dist/assets/{createLucideIcon-BRLFtf-8.js → createLucideIcon-83gaZMtv.js} +1 -1
- package/dist/assets/desktop-update-config-CfoVwf-w.js +1 -0
- package/dist/assets/dist-aTmhMDVh.js +9 -0
- package/dist/assets/{dist-DP-JKR4G.js → dist-toEYs-MZ.js} +1 -1
- package/dist/assets/{external-link-BkJkiWbH.js → external-link-QQ0TC6X4.js} +1 -1
- package/dist/assets/{hash-CbP6-6R9.js → hash-DaFBEkmi.js} +1 -1
- package/dist/assets/i18n-C3jb83S6.js +1 -0
- package/dist/assets/index-CE4N7ItL.css +1 -0
- package/dist/assets/index-riX7Sg0_.js +6 -0
- package/dist/assets/infiniteQueryBehavior-BmHX_ayZ.js +1 -0
- package/dist/assets/loader-circle-BjMg63eu.js +1 -0
- package/dist/assets/{logos-N3dbS6-I.js → logos-Dzlz30M3.js} +1 -1
- package/dist/assets/{page-layout-DyuvlNrg.js → page-layout-D2eRufRQ.js} +1 -1
- package/dist/assets/plus-CIXME2pD.js +1 -0
- package/dist/assets/{popover-BKKWGUaG.js → popover-BSXxm5bj.js} +1 -1
- package/dist/assets/{refresh-ccw-BGMdiNGq.js → refresh-ccw-B3zMtN-_.js} +1 -1
- package/dist/assets/refresh-cw-DlZkIHnJ.js +1 -0
- package/dist/assets/{save-Dh4GQzzX.js → save-Us9fg4Sj.js} +1 -1
- package/dist/assets/search-B_Qr0f6C.js +1 -0
- package/dist/assets/security-config-BGWYwxNr.js +1 -0
- package/dist/assets/{select-BtIi5fnh.js → select-DLYqySQK.js} +1 -1
- package/dist/assets/skeleton-CYQJazv6.js +1 -0
- package/dist/assets/{status-dot-C4O-2jZP.js → status-dot-DGayudyB.js} +1 -1
- package/dist/assets/{switch-DPegGIa_.js → switch-Dz2ScsKx.js} +1 -1
- package/dist/assets/{tabs-custom-x5GZexrF.js → tabs-custom-CdKyjiGk.js} +1 -1
- package/dist/assets/{trash-2-CU3LYIpQ.js → trash-2-Db-mZOZs.js} +1 -1
- package/dist/assets/use-infinite-scroll-loader-DBJX5hj0.js +1 -0
- package/dist/assets/{useConfirmDialog-S5WsGOGf.js → useConfirmDialog-DL0a-oGC.js} +1 -1
- package/dist/assets/useMutation-BdZm-9PL.js +1 -0
- package/dist/assets/x-B8Tho_xC.js +1 -0
- package/dist/index.html +20 -18
- package/package.json +6 -6
- package/src/App.tsx +2 -0
- package/src/account/components/account-panel.tsx +46 -4
- package/src/account/managers/account.manager.ts +19 -4
- package/src/api/raw-client.test.ts +37 -0
- package/src/api/raw-client.ts +51 -8
- package/src/api/remote.ts +9 -0
- package/src/api/remote.types.ts +5 -0
- package/src/components/chat/ChatConversationPanel.test.tsx +344 -142
- package/src/components/chat/ChatSidebar.test.tsx +109 -4
- package/src/components/chat/ChatSidebar.tsx +62 -9
- package/src/components/chat/adapters/chat-message-tool-agent-id.test.ts +11 -11
- package/src/components/chat/adapters/chat-message.adapter.test.ts +43 -6
- package/src/components/chat/adapters/chat-message.session-request-tool-card.ts +182 -44
- package/src/components/chat/adapters/chat-message.session-spawn-tool-card.test.ts +104 -0
- package/src/components/chat/chat-child-session-panel.tsx +155 -59
- package/src/components/chat/chat-page-runtime.test.ts +16 -19
- package/src/components/chat/chat-session-preference-sync.test.ts +13 -0
- package/src/components/chat/chat-session-preference-sync.ts +9 -7
- package/src/components/chat/chat-sidebar-session-item.tsx +189 -121
- package/src/components/chat/containers/chat-message-list.container.test.tsx +21 -3
- package/src/components/chat/containers/chat-message-list.container.tsx +14 -0
- package/src/components/chat/hooks/use-chat-session-project.test.tsx +5 -5
- package/src/components/chat/hooks/use-chat-session-project.ts +0 -5
- package/src/components/chat/hooks/use-chat-session-update.test.tsx +75 -0
- package/src/components/chat/hooks/use-chat-session-update.ts +4 -2
- package/src/components/chat/managers/chat-session-list.manager.test.ts +79 -5
- package/src/components/chat/managers/chat-session-list.manager.ts +31 -4
- package/src/components/chat/ncp/NcpChatPage.tsx +32 -51
- package/src/components/chat/ncp/ncp-app-client-fetch.test.ts +1 -1
- package/src/components/chat/ncp/ncp-app-client-fetch.ts +45 -5
- package/src/components/chat/ncp/ncp-chat-input.manager.ts +3 -5
- package/src/components/chat/ncp/ncp-chat-page-data.ts +0 -1
- package/src/components/chat/ncp/ncp-chat.presenter.ts +1 -11
- package/src/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view.ts +35 -9
- package/src/components/chat/stores/chat-session-list.store.ts +99 -5
- package/src/components/chat/useChatSessionTypeState.test.tsx +0 -3
- package/src/components/chat/useChatSessionTypeState.ts +3 -5
- package/src/components/config/ChannelsList.test.tsx +68 -0
- package/src/components/config/ChannelsList.tsx +22 -4
- package/src/components/config/ProviderForm.tsx +9 -15
- package/src/components/config/ProvidersList.tsx +17 -3
- package/src/components/config/desktop-update-config.tsx +230 -0
- package/src/components/config/providers-list.test.tsx +68 -0
- package/src/components/layout/Sidebar.tsx +19 -14
- package/src/components/layout/sidebar.layout.test.tsx +33 -1
- package/src/components/marketplace/MarketplacePage.tsx +30 -30
- package/src/components/marketplace/marketplace-page-parts.tsx +16 -24
- package/src/components/marketplace/mcp/McpMarketplacePage.tsx +28 -26
- package/src/desktop/desktop-update.types.ts +36 -0
- package/src/desktop/managers/desktop-update.manager.ts +163 -0
- package/src/desktop/stores/desktop-update.store.ts +18 -0
- package/src/hooks/marketplace-list-pages.ts +27 -0
- package/src/hooks/use-infinite-scroll-loader.ts +88 -0
- package/src/hooks/useMarketplace.ts +14 -3
- package/src/hooks/useMcpMarketplace.ts +14 -3
- package/src/lib/desktop-update-labels.utils.ts +72 -0
- package/src/lib/i18n.chat.ts +13 -0
- package/src/lib/i18n.remote.ts +15 -0
- package/src/lib/i18n.ts +3 -9
- package/src/lib/ui-document-title.ts +1 -0
- package/src/transport/local.transport.ts +57 -18
- package/src/vite-env.d.ts +10 -0
- package/dist/assets/ChannelsList-CobWeI2V.js +0 -8
- package/dist/assets/ChatPage-ZIdFFVAv.js +0 -43
- package/dist/assets/DocBrowser-D55C0iyl.js +0 -1
- package/dist/assets/MarketplacePage-BFYsRss_.js +0 -49
- package/dist/assets/MarketplacePage-DII-q-Y1.js +0 -1
- package/dist/assets/McpMarketplacePage-CPqsGJzz.js +0 -40
- package/dist/assets/ModelConfig-Bvuo_IpS.js +0 -1
- package/dist/assets/ProviderScopedModelInput-BfY8rGsf.js +0 -1
- package/dist/assets/ProvidersList-3tlaqwSS.js +0 -1
- package/dist/assets/RuntimeConfig-CAd5Kta3.js +0 -1
- package/dist/assets/SearchConfig-DFwgaAa7.js +0 -1
- package/dist/assets/SessionsConfig-vYrvc2Fk.js +0 -2
- package/dist/assets/chat-session-display-5dVFkJyw.js +0 -1
- package/dist/assets/config-CMiW0yaK.js +0 -1
- package/dist/assets/dist-BFc_H-lY.js +0 -15
- package/dist/assets/i18n-C_2dKw6w.js +0 -1
- package/dist/assets/index-ChUXhq0G.css +0 -1
- package/dist/assets/index-DAE8Srx-.js +0 -6
- package/dist/assets/label-D8yyejJS.js +0 -1
- package/dist/assets/loader-circle-B0sKKO29.js +0 -1
- package/dist/assets/marketplace-localization-CxSTG9wr.js +0 -1
- package/dist/assets/plus-CYXs3JtZ.js +0 -1
- package/dist/assets/react-8EIEQjMP.js +0 -1
- package/dist/assets/search-DOsLw-P9.js +0 -1
- package/dist/assets/security-config-CM_tQRXQ.js +0 -1
- package/dist/assets/skeleton-GbHLjPC0.js +0 -1
- package/dist/assets/useMutation-DSinpgEq.js +0 -1
- package/dist/assets/x-Bnco_K8b.js +0 -1
- package/src/components/chat/ChatSessionsSidebar.tsx +0 -100
- /package/dist/assets/{config-hints-WtpHP_DW.js → config-hints-GSUMvmSo.js} +0 -0
- /package/dist/assets/{config-layout-LQ10ozRC.js → config-layout-CgBMG7OL.js} +0 -0
|
@@ -18,8 +18,11 @@ const mocks = vi.hoisted(() => ({
|
|
|
18
18
|
isLoading: false
|
|
19
19
|
}));
|
|
20
20
|
|
|
21
|
-
function createSessionItem(
|
|
22
|
-
|
|
21
|
+
function createSessionItem(
|
|
22
|
+
session: NcpSessionListItemView['session'],
|
|
23
|
+
runStatus?: NcpSessionListItemView['runStatus'],
|
|
24
|
+
): NcpSessionListItemView {
|
|
25
|
+
return { session, runStatus };
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
vi.mock('@/components/chat/presenter/chat-presenter-context', () => ({
|
|
@@ -28,7 +31,12 @@ vi.mock('@/components/chat/presenter/chat-presenter-context', () => ({
|
|
|
28
31
|
createSession: mocks.createSession,
|
|
29
32
|
setQuery: mocks.setQuery,
|
|
30
33
|
setListMode: mocks.setListMode,
|
|
31
|
-
selectSession: mocks.selectSession
|
|
34
|
+
selectSession: mocks.selectSession,
|
|
35
|
+
markSessionRead: (sessionKey: string | null | undefined, updatedAt: string | null | undefined) =>
|
|
36
|
+
sessionKey ? useChatSessionListStore.getState().markSessionRead(sessionKey, updatedAt) : undefined,
|
|
37
|
+
hydrateReadWatermarks: (
|
|
38
|
+
entries: readonly { sessionKey: string; updatedAt: string | null | undefined }[],
|
|
39
|
+
) => useChatSessionListStore.getState().hydrateReadWatermarks(entries)
|
|
32
40
|
}
|
|
33
41
|
})
|
|
34
42
|
}));
|
|
@@ -124,10 +132,13 @@ function resetSidebarTestState() {
|
|
|
124
132
|
}
|
|
125
133
|
});
|
|
126
134
|
useChatSessionListStore.setState({
|
|
135
|
+
readUpdatedAtBySessionKey: {},
|
|
136
|
+
hasHydratedReadWatermarks: false,
|
|
127
137
|
snapshot: {
|
|
128
138
|
...useChatSessionListStore.getState().snapshot,
|
|
129
139
|
query: '',
|
|
130
|
-
listMode: 'time-first'
|
|
140
|
+
listMode: 'time-first',
|
|
141
|
+
selectedSessionKey: null
|
|
131
142
|
}
|
|
132
143
|
});
|
|
133
144
|
}
|
|
@@ -496,4 +507,98 @@ describe('ChatSidebar session item interactions', () => {
|
|
|
496
507
|
expect(screen.queryByDisplayValue('Should Not Persist')).toBeNull();
|
|
497
508
|
expect(screen.getByText('Cancelable Label')).not.toBeNull();
|
|
498
509
|
});
|
|
510
|
+
|
|
511
|
+
it('shows an unread dot only after a non-active session finishes its newer update', () => {
|
|
512
|
+
mocks.sessionItems = [
|
|
513
|
+
createSessionItem({
|
|
514
|
+
key: 'session:ncp-1',
|
|
515
|
+
createdAt: '2026-03-19T09:00:00.000Z',
|
|
516
|
+
updatedAt: '2026-03-19T09:05:00.000Z',
|
|
517
|
+
label: 'Current Task',
|
|
518
|
+
sessionType: 'native',
|
|
519
|
+
sessionTypeMutable: false,
|
|
520
|
+
messageCount: 1
|
|
521
|
+
}),
|
|
522
|
+
createSessionItem({
|
|
523
|
+
key: 'session:ncp-2',
|
|
524
|
+
createdAt: '2026-03-19T10:00:00.000Z',
|
|
525
|
+
updatedAt: '2026-03-19T10:05:00.000Z',
|
|
526
|
+
label: 'Background Task',
|
|
527
|
+
sessionType: 'native',
|
|
528
|
+
sessionTypeMutable: false,
|
|
529
|
+
messageCount: 1
|
|
530
|
+
}, 'running')
|
|
531
|
+
];
|
|
532
|
+
useChatSessionListStore.setState({
|
|
533
|
+
snapshot: {
|
|
534
|
+
...useChatSessionListStore.getState().snapshot,
|
|
535
|
+
selectedSessionKey: 'session:ncp-1'
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
const { rerender } = render(
|
|
540
|
+
<MemoryRouter>
|
|
541
|
+
<ChatSidebar />
|
|
542
|
+
</MemoryRouter>
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
expect(screen.queryByLabelText('Session has unread updates')).toBeNull();
|
|
546
|
+
|
|
547
|
+
mocks.sessionItems = [
|
|
548
|
+
mocks.sessionItems[0]!,
|
|
549
|
+
createSessionItem({
|
|
550
|
+
key: 'session:ncp-2',
|
|
551
|
+
createdAt: '2026-03-19T10:00:00.000Z',
|
|
552
|
+
updatedAt: '2026-03-19T10:06:00.000Z',
|
|
553
|
+
label: 'Background Task',
|
|
554
|
+
sessionType: 'native',
|
|
555
|
+
sessionTypeMutable: false,
|
|
556
|
+
messageCount: 2
|
|
557
|
+
}, 'running')
|
|
558
|
+
];
|
|
559
|
+
|
|
560
|
+
rerender(
|
|
561
|
+
<MemoryRouter>
|
|
562
|
+
<ChatSidebar />
|
|
563
|
+
</MemoryRouter>
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
expect(screen.queryByLabelText('Session has unread updates')).toBeNull();
|
|
567
|
+
|
|
568
|
+
mocks.sessionItems = [
|
|
569
|
+
mocks.sessionItems[0]!,
|
|
570
|
+
createSessionItem({
|
|
571
|
+
key: 'session:ncp-2',
|
|
572
|
+
createdAt: '2026-03-19T10:00:00.000Z',
|
|
573
|
+
updatedAt: '2026-03-19T10:06:00.000Z',
|
|
574
|
+
label: 'Background Task',
|
|
575
|
+
sessionType: 'native',
|
|
576
|
+
sessionTypeMutable: false,
|
|
577
|
+
messageCount: 2
|
|
578
|
+
})
|
|
579
|
+
];
|
|
580
|
+
|
|
581
|
+
rerender(
|
|
582
|
+
<MemoryRouter>
|
|
583
|
+
<ChatSidebar />
|
|
584
|
+
</MemoryRouter>
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
expect(screen.getByLabelText('Session has unread updates')).toBeTruthy();
|
|
588
|
+
|
|
589
|
+
useChatSessionListStore.setState({
|
|
590
|
+
snapshot: {
|
|
591
|
+
...useChatSessionListStore.getState().snapshot,
|
|
592
|
+
selectedSessionKey: 'session:ncp-2'
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
rerender(
|
|
597
|
+
<MemoryRouter>
|
|
598
|
+
<ChatSidebar />
|
|
599
|
+
</MemoryRouter>
|
|
600
|
+
);
|
|
601
|
+
|
|
602
|
+
expect(screen.queryByLabelText('Session has unread updates')).toBeNull();
|
|
603
|
+
});
|
|
499
604
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMemo, useState } from 'react';
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
2
2
|
import type { SessionEntryView } from '@/api/types';
|
|
3
3
|
import { Button } from '@/components/ui/button';
|
|
4
4
|
import { BrandHeader } from '@/components/common/BrandHeader';
|
|
@@ -17,7 +17,10 @@ import { useChatSessionLabel } from '@/components/chat/hooks/use-chat-session-la
|
|
|
17
17
|
import { useNcpSessionListView, type NcpSessionListItemView } from '@/components/chat/ncp/use-ncp-session-list-view';
|
|
18
18
|
import { usePresenter } from '@/components/chat/presenter/chat-presenter-context';
|
|
19
19
|
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
shouldShowUnreadSessionIndicator,
|
|
22
|
+
useChatSessionListStore
|
|
23
|
+
} from '@/components/chat/stores/chat-session-list.store';
|
|
21
24
|
import { useAgents } from '@/hooks/agents/useAgents';
|
|
22
25
|
import { getSessionProjectName } from '@/lib/session-project/session-project.utils';
|
|
23
26
|
import { cn } from '@/lib/utils';
|
|
@@ -143,6 +146,50 @@ const navItems = [
|
|
|
143
146
|
{ target: '/agents', label: () => t('agentsPageTitle'), icon: Bot },
|
|
144
147
|
];
|
|
145
148
|
|
|
149
|
+
function useChatSessionUnreadState(
|
|
150
|
+
items: readonly NcpSessionListItemView[],
|
|
151
|
+
selectedSessionKey: string | null,
|
|
152
|
+
markSessionRead: (sessionKey: string | null | undefined, updatedAt: string | null | undefined) => void,
|
|
153
|
+
hydrateReadWatermarks: (
|
|
154
|
+
entries: readonly { sessionKey: string; updatedAt: string | null | undefined }[],
|
|
155
|
+
) => void,
|
|
156
|
+
): Record<string, string> {
|
|
157
|
+
const readUpdatedAtBySessionKey = useChatSessionListStore((state) => state.readUpdatedAtBySessionKey);
|
|
158
|
+
const hasHydratedReadWatermarks = useChatSessionListStore((state) => state.hasHydratedReadWatermarks);
|
|
159
|
+
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
const syncHydratedReadWatermarks = () => {
|
|
162
|
+
if (hasHydratedReadWatermarks || items.length === 0) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
hydrateReadWatermarks(
|
|
166
|
+
items.map(({ session }) => ({
|
|
167
|
+
sessionKey: session.key,
|
|
168
|
+
updatedAt: session.updatedAt
|
|
169
|
+
}))
|
|
170
|
+
);
|
|
171
|
+
};
|
|
172
|
+
syncHydratedReadWatermarks();
|
|
173
|
+
}, [hasHydratedReadWatermarks, hydrateReadWatermarks, items]);
|
|
174
|
+
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
const syncSelectedSessionReadState = () => {
|
|
177
|
+
if (!selectedSessionKey) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const selectedItem = items.find(({ session }) => session.key === selectedSessionKey);
|
|
181
|
+
if (!selectedItem) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const { session: selectedSession } = selectedItem;
|
|
185
|
+
markSessionRead(selectedSession.key, selectedSession.updatedAt);
|
|
186
|
+
};
|
|
187
|
+
syncSelectedSessionReadState();
|
|
188
|
+
}, [items, markSessionRead, selectedSessionKey]);
|
|
189
|
+
|
|
190
|
+
return readUpdatedAtBySessionKey;
|
|
191
|
+
}
|
|
192
|
+
|
|
146
193
|
export function ChatSidebar() {
|
|
147
194
|
const presenter = usePresenter();
|
|
148
195
|
const docBrowser = useDocBrowser();
|
|
@@ -164,7 +211,6 @@ export function ChatSidebar() {
|
|
|
164
211
|
() => new Map((agentsQuery.data?.agents ?? []).map((agent) => [agent.id, agent])),
|
|
165
212
|
[agentsQuery.data?.agents]
|
|
166
213
|
);
|
|
167
|
-
|
|
168
214
|
const sortedItems = useMemo(() => sortSessionItemsByUpdatedAtDesc(items), [items]);
|
|
169
215
|
const groups = useMemo(() => groupSessionsByDate(sortedItems), [sortedItems]);
|
|
170
216
|
const projectGroups = useMemo(() => groupSessionsByProject(sortedItems), [sortedItems]);
|
|
@@ -174,24 +220,26 @@ export function ChatSidebar() {
|
|
|
174
220
|
[defaultSessionType, inputSnapshot.sessionTypeOptions]
|
|
175
221
|
);
|
|
176
222
|
const isProjectFirstView = listSnapshot.listMode === 'project-first';
|
|
177
|
-
|
|
223
|
+
const readUpdatedAtBySessionKey = useChatSessionUnreadState(
|
|
224
|
+
items,
|
|
225
|
+
listSnapshot.selectedSessionKey,
|
|
226
|
+
presenter.chatSessionListManager.markSessionRead,
|
|
227
|
+
presenter.chatSessionListManager.hydrateReadWatermarks,
|
|
228
|
+
);
|
|
178
229
|
const handleLanguageSwitch = (nextLang: I18nLanguage) => {
|
|
179
230
|
if (language === nextLang) return;
|
|
180
231
|
setLanguage(nextLang);
|
|
181
232
|
window.location.reload();
|
|
182
233
|
};
|
|
183
|
-
|
|
184
234
|
const startEditingSessionLabel = (session: SessionEntryView) => {
|
|
185
235
|
setEditingSessionKey(session.key);
|
|
186
236
|
setDraftLabel(session.label?.trim() ?? '');
|
|
187
237
|
};
|
|
188
|
-
|
|
189
238
|
const cancelEditingSessionLabel = () => {
|
|
190
239
|
setEditingSessionKey(null);
|
|
191
240
|
setDraftLabel('');
|
|
192
241
|
setSavingSessionKey(null);
|
|
193
242
|
};
|
|
194
|
-
|
|
195
243
|
const saveSessionLabel = async (session: SessionEntryView) => {
|
|
196
244
|
const normalizedLabel = draftLabel.trim();
|
|
197
245
|
const currentLabel = session.label?.trim() ?? '';
|
|
@@ -211,9 +259,14 @@ export function ChatSidebar() {
|
|
|
211
259
|
setSavingSessionKey(null);
|
|
212
260
|
}
|
|
213
261
|
};
|
|
214
|
-
|
|
215
262
|
const renderSessionItem = ({ session, runStatus }: NcpSessionListItemView) => {
|
|
216
263
|
const active = listSnapshot.selectedSessionKey === session.key;
|
|
264
|
+
const showUnreadDot = shouldShowUnreadSessionIndicator({
|
|
265
|
+
active,
|
|
266
|
+
updatedAt: session.updatedAt,
|
|
267
|
+
readUpdatedAt: readUpdatedAtBySessionKey[session.key],
|
|
268
|
+
runStatus,
|
|
269
|
+
});
|
|
217
270
|
const context = resolveSessionContextView(session, inputSnapshot.sessionTypeOptions);
|
|
218
271
|
const isEditing = editingSessionKey === session.key;
|
|
219
272
|
const isSaving = savingSessionKey === session.key;
|
|
@@ -222,6 +275,7 @@ export function ChatSidebar() {
|
|
|
222
275
|
key={session.key}
|
|
223
276
|
session={session}
|
|
224
277
|
active={active}
|
|
278
|
+
showUnreadDot={showUnreadDot}
|
|
225
279
|
runStatus={runStatus}
|
|
226
280
|
context={context}
|
|
227
281
|
title={sessionTitle(session)}
|
|
@@ -239,7 +293,6 @@ export function ChatSidebar() {
|
|
|
239
293
|
/>
|
|
240
294
|
);
|
|
241
295
|
};
|
|
242
|
-
|
|
243
296
|
return (
|
|
244
297
|
<aside className="w-[280px] shrink-0 flex flex-col h-full bg-secondary border-r border-gray-200/60">
|
|
245
298
|
<div className="px-5 pt-5 pb-3">
|
|
@@ -33,19 +33,19 @@ function adapt(uiMessages: ChatMessageSource[]) {
|
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
it("exposes agentId on
|
|
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: "
|
|
48
|
-
args: '{"agentId":"planner-agent","
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
|
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: "
|
|
238
|
-
args: '{"
|
|
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: "
|
|
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: '{"
|
|
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:",
|