@nextclaw/ui 0.12.7 → 0.12.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +85 -0
- package/dist/assets/ChannelsList-Ita2Zm1_.js +8 -0
- package/dist/assets/{DocBrowser-Cse_F8Nn.js → DocBrowser-6ReNjvzF.js} +1 -1
- package/dist/assets/DocBrowser-BNwbPHf4.js +1 -0
- package/dist/assets/{DocBrowserContext-Bai1WU2H.js → DocBrowserContext-B6SpA7Qs.js} +1 -1
- package/dist/assets/{LogoBadge-BdxMPc9v.js → LogoBadge-ByNLYg65.js} +1 -1
- package/dist/assets/MarketplacePage-CjX2MWww.js +1 -0
- package/dist/assets/{MarketplacePage-BbpAkllU.js → MarketplacePage-D0sDlYX4.js} +1 -1
- package/dist/assets/McpMarketplacePage-BGKJm1sJ.js +40 -0
- package/dist/assets/{ModelConfig-3GLqQ5GY.js → ModelConfig-BzZenCH-.js} +1 -1
- package/dist/assets/{ProviderScopedModelInput-BYNouw-i.js → ProviderScopedModelInput-Da7khnBA.js} +1 -1
- package/dist/assets/{ProvidersList-BR1gJ4Dm.js → ProvidersList-BbVzRxjY.js} +1 -1
- package/dist/assets/RemoteAccessPage-BaDH_X1Q.js +1 -0
- package/dist/assets/RuntimeConfig-F_XKGgLm.js +1 -0
- package/dist/assets/{SearchConfig-DTeJvp8m.js → SearchConfig-BGkzXQP-.js} +1 -1
- package/dist/assets/{SecretsConfig-CCYO6NcV.js → SecretsConfig-D281Rotl.js} +2 -2
- package/dist/assets/{SessionsConfig-Du39vDgt.js → SessionsConfig-ChHQ7M5c.js} +2 -2
- package/dist/assets/{app-query-client-Dr5d-K8d.js → app-query-client-VnFElj4E.js} +1 -1
- package/dist/assets/{book-open-Da4OEPqB.js → book-open-BdcxxoQu.js} +1 -1
- package/dist/assets/chat-page-Doe0yTtB.js +58 -0
- package/dist/assets/chat-session-display-cw78aiI_.js +1 -0
- package/dist/assets/{chunk-JZWAC4HX-CoFVxHXV.js → chunk-JZWAC4HX-DK5HPmIK.js} +1 -1
- package/dist/assets/{client-CSk58DcF.js → client-_i4MU2bB.js} +1 -1
- package/dist/assets/{config-D8KzikVB.js → config-DtIQwrHF.js} +1 -1
- package/dist/assets/{createLucideIcon-83gaZMtv.js → createLucideIcon-BSeTgkZW.js} +1 -1
- package/dist/assets/desktop-update-config-Dpcf4BKG.js +1 -0
- package/dist/assets/{dist-toEYs-MZ.js → dist-6TrrnPCR.js} +1 -1
- package/dist/assets/{dist-aTmhMDVh.js → dist-ccBFUi-o.js} +1 -1
- package/dist/assets/download-BhDxnyvU.js +1 -0
- package/dist/assets/{external-link-QQ0TC6X4.js → external-link-BgErLCNT.js} +1 -1
- package/dist/assets/{hash-DaFBEkmi.js → hash-Bl7dr_UG.js} +1 -1
- package/dist/assets/i18n-eDHeDY0n.js +1 -0
- package/dist/assets/index-CF9xve0E.js +6 -0
- package/dist/assets/index-FgA52VBt.css +1 -0
- package/dist/assets/{infiniteQueryBehavior-BmHX_ayZ.js → infiniteQueryBehavior-ZDS92Qpp.js} +1 -1
- package/dist/assets/loader-circle-ACM1s51e.js +1 -0
- package/dist/assets/{logos-Dzlz30M3.js → logos-x89HbrZ4.js} +1 -1
- package/dist/assets/{page-layout-D2eRufRQ.js → page-layout-vZnghcFy.js} +1 -1
- package/dist/assets/play-CFUwCA2E.js +1 -0
- package/dist/assets/plus-rYsv72JG.js +1 -0
- package/dist/assets/{popover-BSXxm5bj.js → popover-Bg1VoTZ6.js} +1 -1
- package/dist/assets/{refresh-ccw-B3zMtN-_.js → refresh-ccw-DT98i__E.js} +1 -1
- package/dist/assets/{refresh-cw-DlZkIHnJ.js → refresh-cw-C47QSEwg.js} +1 -1
- package/dist/assets/rotate-cw-JtFzpNn6.js +1 -0
- package/dist/assets/{save-Us9fg4Sj.js → save-3S6-H3Xw.js} +1 -1
- package/dist/assets/search-3kFR_zh9.js +1 -0
- package/dist/assets/{security-config-BGWYwxNr.js → security-config-BWaiARNk.js} +1 -1
- package/dist/assets/{select-DLYqySQK.js → select-DJ2MUjBB.js} +1 -1
- package/dist/assets/skeleton-ByQepn0M.js +1 -0
- package/dist/assets/{status-dot-DGayudyB.js → status-dot-vbanNPFU.js} +1 -1
- package/dist/assets/{switch-Dz2ScsKx.js → switch-BsLtHOH-.js} +1 -1
- package/dist/assets/{tabs-custom-CdKyjiGk.js → tabs-custom-D3HYMt6k.js} +1 -1
- package/dist/assets/{trash-2-Db-mZOZs.js → trash-2-G48scll7.js} +1 -1
- package/dist/assets/{use-infinite-scroll-loader-DBJX5hj0.js → use-infinite-scroll-loader-DkNhD-42.js} +1 -1
- package/dist/assets/{useConfirmDialog-DL0a-oGC.js → useConfirmDialog-BkvTN-vd.js} +1 -1
- package/dist/assets/{useMutation-BdZm-9PL.js → useMutation-CBWjE2uj.js} +1 -1
- package/dist/assets/x-ByDbItbq.js +1 -0
- package/dist/index.html +95 -21
- package/dist/manifest.webmanifest +30 -0
- package/dist/offline.html +102 -0
- package/dist/pwa-192.png +0 -0
- package/dist/pwa-512.png +0 -0
- package/dist/sw.js +80 -0
- package/index.html +73 -1
- package/package.json +6 -6
- package/public/manifest.webmanifest +30 -0
- package/public/offline.html +102 -0
- package/public/pwa-192.png +0 -0
- package/public/pwa-512.png +0 -0
- package/public/sw.js +80 -0
- package/src/api/runtime-control.ts +34 -0
- package/src/api/runtime-control.types.ts +58 -0
- package/src/api/server-path.ts +27 -4
- package/src/api/types.ts +30 -10
- package/src/{App.test.tsx → app.test.tsx} +1 -1
- package/src/{App.tsx → app.tsx} +10 -1
- package/src/components/chat/ChatSidebar.test.tsx +79 -8
- package/src/components/chat/ChatSidebar.tsx +43 -26
- package/src/components/chat/adapters/chat-message.summary-truncation.test.ts +66 -0
- package/src/components/chat/adapters/file-operation/card.ts +9 -0
- package/src/components/chat/adapters/file-operation/diff.ts +14 -0
- package/src/components/chat/{ChatConversationPanel.test.tsx → chat-conversation-panel.test.tsx} +118 -155
- package/src/components/chat/chat-conversation-panel.tsx +412 -0
- package/src/components/chat/chat-page-runtime.test.ts +1 -1
- package/src/components/chat/chat-page-shell.tsx +1 -1
- package/src/components/chat/{ChatPage.tsx → chat-page.tsx} +1 -1
- package/src/components/chat/chat-session-workspace-file-preview.test.tsx +91 -0
- package/src/components/chat/chat-session-workspace-file-preview.tsx +307 -0
- package/src/components/chat/chat-session-workspace-panel-nav.tsx +197 -0
- package/src/components/chat/chat-session-workspace-panel.tsx +318 -0
- package/src/components/chat/chat-sidebar-session-item.tsx +32 -2
- package/src/components/chat/containers/chat-message-list.container.test.tsx +49 -0
- package/src/components/chat/containers/chat-message-list.container.tsx +4 -0
- package/src/components/chat/managers/chat-session-list.manager.test.ts +94 -31
- package/src/components/chat/managers/chat-session-list.manager.ts +86 -14
- package/src/components/chat/managers/chat-ui.manager.ts +2 -0
- package/src/components/chat/ncp/README.md +1 -1
- package/src/components/chat/ncp/ncp-chat-input.manager.ts +7 -1
- package/src/components/chat/ncp/ncp-chat-page-data.test.ts +1 -1
- package/src/components/chat/ncp/{NcpChatPage.tsx → ncp-chat-page.tsx} +7 -7
- package/src/components/chat/ncp/ncp-chat-thread.manager.ts +179 -41
- package/src/components/chat/ncp/ncp-session-adapter.test.ts +40 -2
- package/src/components/chat/ncp/ncp-session-adapter.ts +29 -0
- package/src/components/chat/ncp/page/ncp-chat-derived-state.ts +54 -11
- package/src/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view.ts +4 -0
- package/src/components/chat/ncp/tests/ncp-chat-input.manager.test.ts +99 -0
- package/src/components/chat/ncp/tests/ncp-chat-thread.manager.test.ts +189 -0
- package/src/components/chat/presenter/chat-presenter-context.tsx +13 -2
- package/src/components/chat/session-header/chat-session-header-actions.test.tsx +26 -0
- package/src/components/chat/session-header/chat-session-header-actions.tsx +19 -1
- package/src/components/chat/stores/chat-session-list.store.ts +25 -54
- package/src/components/chat/stores/chat-thread.store.ts +24 -0
- package/src/components/common/ProviderScopedModelInput.tsx +12 -2
- package/src/components/config/ModelConfig.test.tsx +108 -2
- package/src/components/config/RuntimeConfig.tsx +154 -7
- package/src/components/config/desktop-update-config.test.tsx +85 -0
- package/src/components/config/desktop-update-config.tsx +44 -3
- package/src/components/config/runtime-control-card.test.tsx +255 -0
- package/src/components/config/runtime-control-card.tsx +301 -0
- package/src/components/config/runtime-presence-card.test.tsx +154 -0
- package/src/components/config/runtime-presence-card.tsx +163 -0
- package/src/components/layout/AppLayout.tsx +1 -1
- package/src/components/providers/ThemeProvider.tsx +5 -0
- package/src/desktop/desktop-update.types.ts +25 -0
- package/src/desktop/managers/desktop-presence.manager.ts +91 -0
- package/src/desktop/managers/desktop-update.manager.ts +37 -1
- package/src/desktop/stores/desktop-presence.store.ts +18 -0
- package/src/desktop/stores/desktop-update.store.ts +7 -1
- package/src/hooks/server-path/use-server-path-read.ts +20 -0
- package/src/hooks/use-runtime-control.ts +24 -0
- package/src/lib/chat-message.ts +14 -3
- package/src/lib/desktop-update-labels.utils.ts +28 -2
- package/src/lib/i18n.chat.ts +12 -1
- package/src/lib/i18n.pwa.ts +62 -0
- package/src/lib/i18n.runtime-control.ts +120 -0
- package/src/lib/i18n.ts +4 -6
- package/src/main.tsx +1 -1
- package/src/pwa/components/pwa-install-entry.test.tsx +110 -0
- package/src/pwa/components/pwa-install-entry.tsx +205 -0
- package/src/pwa/managers/pwa-install.manager.test.ts +160 -0
- package/src/pwa/managers/pwa-install.manager.ts +232 -0
- package/src/pwa/managers/pwa-runtime.manager.ts +196 -0
- package/src/pwa/managers/pwa-shell-theme.manager.test.ts +30 -0
- package/src/pwa/managers/pwa-shell-theme.manager.ts +46 -0
- package/src/pwa/pwa-install-banner.storage.ts +55 -0
- package/src/pwa/pwa.types.ts +22 -0
- package/src/pwa/register-pwa.ts +14 -0
- package/src/pwa/stores/pwa.store.ts +17 -0
- package/src/runtime-control/runtime-control.manager.ts +118 -0
- package/src/vite-env.d.ts +9 -0
- package/dist/assets/ChannelsList-D8p4OlM6.js +0 -8
- package/dist/assets/ChatPage-A45t1Rmf.js +0 -58
- package/dist/assets/DocBrowser-B2MpsnU9.js +0 -1
- package/dist/assets/MarketplacePage-BNZ3Jx5d.js +0 -1
- package/dist/assets/McpMarketplacePage-CxPFOgxv.js +0 -40
- package/dist/assets/RemoteAccessPage-DyYVWsyK.js +0 -1
- package/dist/assets/RuntimeConfig-ChdfK4Y_.js +0 -1
- package/dist/assets/chat-session-display-CAlPrnlV.js +0 -1
- package/dist/assets/desktop-update-config-CfoVwf-w.js +0 -1
- package/dist/assets/i18n-C3jb83S6.js +0 -1
- package/dist/assets/index-CE4N7ItL.css +0 -1
- package/dist/assets/index-riX7Sg0_.js +0 -6
- package/dist/assets/loader-circle-BjMg63eu.js +0 -1
- package/dist/assets/plus-CIXME2pD.js +0 -1
- package/dist/assets/search-B_Qr0f6C.js +0 -1
- package/dist/assets/skeleton-CYQJazv6.js +0 -1
- package/dist/assets/x-B8Tho_xC.js +0 -1
- package/src/components/chat/ChatConversationPanel.tsx +0 -256
- package/src/components/chat/chat-child-session-panel.tsx +0 -262
- /package/dist/assets/{config-hints-GSUMvmSo.js → config-hints-BhTmc9P1.js} +0 -0
- /package/dist/assets/{config-layout-CgBMG7OL.js → config-layout-CHs0mAaR.js} +0 -0
|
@@ -2,9 +2,20 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
2
2
|
import { ChatSessionListManager } from '@/components/chat/managers/chat-session-list.manager';
|
|
3
3
|
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
4
4
|
import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
|
|
5
|
+
import { useChatThreadStore } from '@/components/chat/stores/chat-thread.store';
|
|
6
|
+
|
|
7
|
+
const mocks = vi.hoisted(() => ({
|
|
8
|
+
updateNcpSession: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock('@/api/ncp-session', () => ({
|
|
12
|
+
updateNcpSession: mocks.updateNcpSession,
|
|
13
|
+
}));
|
|
5
14
|
|
|
6
15
|
describe('ChatSessionListManager', () => {
|
|
7
16
|
beforeEach(() => {
|
|
17
|
+
mocks.updateNcpSession.mockReset();
|
|
18
|
+
mocks.updateNcpSession.mockResolvedValue({});
|
|
8
19
|
useChatInputStore.setState({
|
|
9
20
|
snapshot: {
|
|
10
21
|
...useChatInputStore.getState().snapshot,
|
|
@@ -15,8 +26,7 @@ describe('ChatSessionListManager', () => {
|
|
|
15
26
|
}
|
|
16
27
|
});
|
|
17
28
|
useChatSessionListStore.setState({
|
|
18
|
-
|
|
19
|
-
hasHydratedReadWatermarks: false,
|
|
29
|
+
optimisticReadAtBySessionKey: {},
|
|
20
30
|
snapshot: {
|
|
21
31
|
...useChatSessionListStore.getState().snapshot,
|
|
22
32
|
selectedSessionKey: 'session-1',
|
|
@@ -24,22 +34,33 @@ describe('ChatSessionListManager', () => {
|
|
|
24
34
|
listMode: 'time-first'
|
|
25
35
|
}
|
|
26
36
|
});
|
|
37
|
+
useChatThreadStore.setState({
|
|
38
|
+
snapshot: {
|
|
39
|
+
...useChatThreadStore.getState().snapshot,
|
|
40
|
+
workspacePanelParentKey: 'session-1',
|
|
41
|
+
activeChildSessionKey: 'child-session-1',
|
|
42
|
+
activeWorkspaceFileKey: 'session-1::/tmp/demo.md',
|
|
43
|
+
},
|
|
44
|
+
});
|
|
27
45
|
});
|
|
28
46
|
|
|
29
47
|
it('applies the requested session type when creating a session', () => {
|
|
30
48
|
const uiManager = {
|
|
31
|
-
|
|
49
|
+
goToChatRoot: vi.fn(),
|
|
50
|
+
goToSession: vi.fn(),
|
|
51
|
+
isAtChatRoot: vi.fn(() => true),
|
|
32
52
|
} as unknown as ConstructorParameters<typeof ChatSessionListManager>[0];
|
|
33
53
|
const streamActionsManager = {
|
|
34
54
|
resetStreamState: vi.fn()
|
|
35
55
|
} as unknown as ConstructorParameters<typeof ChatSessionListManager>[1];
|
|
36
56
|
|
|
37
57
|
const manager = new ChatSessionListManager(uiManager, streamActionsManager);
|
|
38
|
-
manager.createSession('codex');
|
|
58
|
+
const sessionKey = manager.createSession('codex');
|
|
39
59
|
|
|
40
60
|
expect(streamActionsManager.resetStreamState).toHaveBeenCalledTimes(1);
|
|
41
|
-
expect(uiManager.
|
|
42
|
-
expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).
|
|
61
|
+
expect(uiManager.goToChatRoot).toHaveBeenCalledTimes(1);
|
|
62
|
+
expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
|
|
63
|
+
expect(useChatSessionListStore.getState().snapshot.draftSessionKey).toBe(sessionKey);
|
|
43
64
|
expect(useChatSessionListStore.getState().snapshot.draftSessionKey).not.toBe('draft-root-1');
|
|
44
65
|
expect(useChatInputStore.getState().snapshot.pendingSessionType).toBe('codex');
|
|
45
66
|
expect(useChatInputStore.getState().snapshot.pendingProjectRoot).toBeNull();
|
|
@@ -48,20 +69,22 @@ describe('ChatSessionListManager', () => {
|
|
|
48
69
|
|
|
49
70
|
it('hydrates the draft project root when creating a session inside a project group', () => {
|
|
50
71
|
const uiManager = {
|
|
51
|
-
|
|
72
|
+
goToChatRoot: vi.fn(),
|
|
73
|
+
goToSession: vi.fn(),
|
|
74
|
+
isAtChatRoot: vi.fn(() => true),
|
|
52
75
|
} as unknown as ConstructorParameters<typeof ChatSessionListManager>[0];
|
|
53
76
|
const streamActionsManager = {
|
|
54
77
|
resetStreamState: vi.fn()
|
|
55
78
|
} as unknown as ConstructorParameters<typeof ChatSessionListManager>[1];
|
|
56
79
|
|
|
57
80
|
const manager = new ChatSessionListManager(uiManager, streamActionsManager);
|
|
58
|
-
manager.createSession('native', '/tmp/project-alpha');
|
|
81
|
+
const sessionKey = manager.createSession('native', '/tmp/project-alpha');
|
|
59
82
|
|
|
60
83
|
expect(useChatInputStore.getState().snapshot.pendingProjectRoot).toBe('/tmp/project-alpha');
|
|
61
|
-
expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).toBe(
|
|
84
|
+
expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).toBe(sessionKey);
|
|
62
85
|
});
|
|
63
86
|
|
|
64
|
-
it('
|
|
87
|
+
it('reuses the current root draft when send flow needs a concrete session key', () => {
|
|
65
88
|
useChatSessionListStore.setState({
|
|
66
89
|
snapshot: {
|
|
67
90
|
...useChatSessionListStore.getState().snapshot,
|
|
@@ -70,7 +93,9 @@ describe('ChatSessionListManager', () => {
|
|
|
70
93
|
}
|
|
71
94
|
});
|
|
72
95
|
const uiManager = {
|
|
73
|
-
|
|
96
|
+
goToChatRoot: vi.fn(),
|
|
97
|
+
goToSession: vi.fn(),
|
|
98
|
+
isAtChatRoot: vi.fn(() => true),
|
|
74
99
|
} as unknown as ConstructorParameters<typeof ChatSessionListManager>[0];
|
|
75
100
|
const streamActionsManager = {
|
|
76
101
|
resetStreamState: vi.fn()
|
|
@@ -80,28 +105,33 @@ describe('ChatSessionListManager', () => {
|
|
|
80
105
|
const sessionKey = manager.ensureDraftSession('native');
|
|
81
106
|
|
|
82
107
|
expect(sessionKey).toBe('draft-root-2');
|
|
83
|
-
expect(uiManager.
|
|
108
|
+
expect(uiManager.goToChatRoot).not.toHaveBeenCalled();
|
|
109
|
+
expect(uiManager.goToSession).not.toHaveBeenCalled();
|
|
84
110
|
expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
|
|
85
111
|
});
|
|
86
112
|
|
|
87
113
|
it('does not eagerly replace the old selected session before the route finishes switching', () => {
|
|
88
114
|
const uiManager = {
|
|
89
|
-
|
|
115
|
+
goToChatRoot: vi.fn(),
|
|
116
|
+
goToSession: vi.fn(),
|
|
117
|
+
isAtChatRoot: vi.fn(() => true),
|
|
90
118
|
} as unknown as ConstructorParameters<typeof ChatSessionListManager>[0];
|
|
91
119
|
const streamActionsManager = {
|
|
92
120
|
resetStreamState: vi.fn()
|
|
93
121
|
} as unknown as ConstructorParameters<typeof ChatSessionListManager>[1];
|
|
94
122
|
|
|
95
123
|
const manager = new ChatSessionListManager(uiManager, streamActionsManager);
|
|
96
|
-
manager.createSession('native', '/tmp/project-alpha');
|
|
124
|
+
const sessionKey = manager.createSession('native', '/tmp/project-alpha');
|
|
97
125
|
|
|
98
|
-
expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).
|
|
99
|
-
expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).toBe(
|
|
126
|
+
expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
|
|
127
|
+
expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).toBe(sessionKey);
|
|
100
128
|
});
|
|
101
129
|
|
|
102
130
|
it('delegates existing-session selection to routing without eagerly mutating the selected session state', () => {
|
|
103
131
|
const uiManager = {
|
|
104
|
-
|
|
132
|
+
goToChatRoot: vi.fn(),
|
|
133
|
+
goToSession: vi.fn(),
|
|
134
|
+
isAtChatRoot: vi.fn(() => true),
|
|
105
135
|
} as unknown as ConstructorParameters<typeof ChatSessionListManager>[0];
|
|
106
136
|
const streamActionsManager = {
|
|
107
137
|
resetStreamState: vi.fn()
|
|
@@ -112,10 +142,15 @@ describe('ChatSessionListManager', () => {
|
|
|
112
142
|
|
|
113
143
|
expect(uiManager.goToSession).toHaveBeenCalledWith('session-2');
|
|
114
144
|
expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBe('session-1');
|
|
145
|
+
expect(useChatThreadStore.getState().snapshot.workspacePanelParentKey).toBeNull();
|
|
146
|
+
expect(useChatThreadStore.getState().snapshot.activeChildSessionKey).toBeNull();
|
|
147
|
+
expect(useChatThreadStore.getState().snapshot.activeWorkspaceFileKey).toBeNull();
|
|
115
148
|
});
|
|
116
149
|
|
|
117
150
|
it('updates the sidebar list mode without touching other session list state', () => {
|
|
118
|
-
const uiManager = {
|
|
151
|
+
const uiManager = {
|
|
152
|
+
isAtChatRoot: vi.fn(() => true),
|
|
153
|
+
} as unknown as ConstructorParameters<typeof ChatSessionListManager>[0];
|
|
119
154
|
const streamActionsManager = {} as ConstructorParameters<typeof ChatSessionListManager>[1];
|
|
120
155
|
|
|
121
156
|
const manager = new ChatSessionListManager(uiManager, streamActionsManager);
|
|
@@ -127,33 +162,61 @@ describe('ChatSessionListManager', () => {
|
|
|
127
162
|
|
|
128
163
|
it('marks a session as read through the session list owner boundary', () => {
|
|
129
164
|
const manager = new ChatSessionListManager(
|
|
130
|
-
{
|
|
165
|
+
{
|
|
166
|
+
isAtChatRoot: vi.fn(() => true),
|
|
167
|
+
} as unknown as ConstructorParameters<typeof ChatSessionListManager>[0],
|
|
131
168
|
{} as ConstructorParameters<typeof ChatSessionListManager>[1]
|
|
132
169
|
);
|
|
133
170
|
|
|
134
171
|
manager.markSessionRead('session-2', '2026-04-10T10:00:00.000Z');
|
|
135
172
|
|
|
136
|
-
expect(useChatSessionListStore.getState().
|
|
173
|
+
expect(useChatSessionListStore.getState().optimisticReadAtBySessionKey['session-2']).toBe(
|
|
137
174
|
'2026-04-10T10:00:00.000Z'
|
|
138
175
|
);
|
|
176
|
+
expect(mocks.updateNcpSession).toHaveBeenCalledWith('session-2', {
|
|
177
|
+
uiReadAt: '2026-04-10T10:00:00.000Z'
|
|
178
|
+
});
|
|
139
179
|
});
|
|
140
180
|
|
|
141
|
-
it('
|
|
181
|
+
it('skips persisting read state when the backend already has the same watermark', () => {
|
|
142
182
|
const manager = new ChatSessionListManager(
|
|
143
|
-
{
|
|
183
|
+
{
|
|
184
|
+
isAtChatRoot: vi.fn(() => true),
|
|
185
|
+
} as unknown as ConstructorParameters<typeof ChatSessionListManager>[0],
|
|
144
186
|
{} as ConstructorParameters<typeof ChatSessionListManager>[1]
|
|
145
187
|
);
|
|
146
188
|
|
|
147
|
-
manager.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
updatedAt: '2026-04-10T10:00:00.000Z'
|
|
151
|
-
}
|
|
152
|
-
]);
|
|
153
|
-
|
|
154
|
-
expect(useChatSessionListStore.getState().readUpdatedAtBySessionKey['session-2']).toBe(
|
|
189
|
+
manager.markSessionRead(
|
|
190
|
+
'session-2',
|
|
191
|
+
'2026-04-10T10:00:00.000Z',
|
|
155
192
|
'2026-04-10T10:00:00.000Z'
|
|
156
193
|
);
|
|
157
|
-
|
|
194
|
+
|
|
195
|
+
expect(useChatSessionListStore.getState().optimisticReadAtBySessionKey['session-2']).toBeUndefined();
|
|
196
|
+
expect(mocks.updateNcpSession).not.toHaveBeenCalled();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('promotes the active root draft to a concrete session route after the draft has been sent successfully', () => {
|
|
200
|
+
useChatSessionListStore.setState({
|
|
201
|
+
snapshot: {
|
|
202
|
+
...useChatSessionListStore.getState().snapshot,
|
|
203
|
+
selectedSessionKey: null,
|
|
204
|
+
draftSessionKey: 'draft-root-2',
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
const uiManager = {
|
|
208
|
+
goToChatRoot: vi.fn(),
|
|
209
|
+
goToSession: vi.fn(),
|
|
210
|
+
isAtChatRoot: vi.fn(() => true),
|
|
211
|
+
} as unknown as ConstructorParameters<typeof ChatSessionListManager>[0];
|
|
212
|
+
const streamActionsManager = {
|
|
213
|
+
resetStreamState: vi.fn()
|
|
214
|
+
} as unknown as ConstructorParameters<typeof ChatSessionListManager>[1];
|
|
215
|
+
const manager = new ChatSessionListManager(uiManager, streamActionsManager);
|
|
216
|
+
|
|
217
|
+
manager.ensureDraftSession('native');
|
|
218
|
+
manager.promoteRootDraftSessionRoute('draft-root-2');
|
|
219
|
+
|
|
220
|
+
expect(uiManager.goToSession).toHaveBeenCalledWith('draft-root-2', { replace: true });
|
|
158
221
|
});
|
|
159
222
|
});
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
|
|
2
2
|
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
3
|
+
import { useChatThreadStore } from '@/components/chat/stores/chat-thread.store';
|
|
3
4
|
import type { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
|
|
4
5
|
import type { SetStateAction } from 'react';
|
|
5
6
|
import type { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
|
|
6
7
|
import { normalizeSessionProjectRootValue } from '@/lib/session-project/session-project.utils';
|
|
7
8
|
import { createNcpSessionId } from '@/components/chat/ncp/ncp-session-adapter';
|
|
9
|
+
import { updateNcpSession } from '@/api/ncp-session';
|
|
8
10
|
|
|
9
11
|
export class ChatSessionListManager {
|
|
10
12
|
constructor(
|
|
@@ -12,6 +14,24 @@ export class ChatSessionListManager {
|
|
|
12
14
|
private streamActionsManager: ChatStreamActionsManager
|
|
13
15
|
) {}
|
|
14
16
|
|
|
17
|
+
private syncDraftThreadState = (sessionKey: string) => {
|
|
18
|
+
useChatThreadStore.getState().setSnapshot({
|
|
19
|
+
sessionKey,
|
|
20
|
+
sessionDisplayName: undefined,
|
|
21
|
+
canDeleteSession: false,
|
|
22
|
+
isHistoryLoading: false,
|
|
23
|
+
messages: [],
|
|
24
|
+
isSending: false,
|
|
25
|
+
isAwaitingAssistantOutput: false,
|
|
26
|
+
parentSessionKey: null,
|
|
27
|
+
parentSessionLabel: null,
|
|
28
|
+
workspacePanelParentKey: null,
|
|
29
|
+
childSessionTabs: [],
|
|
30
|
+
activeChildSessionKey: null,
|
|
31
|
+
activeWorkspaceFileKey: null,
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
|
|
15
35
|
private resolveUpdateValue = <T>(prev: T, next: SetStateAction<T>): T => {
|
|
16
36
|
if (typeof next === 'function') {
|
|
17
37
|
return (next as (value: T) => T)(prev);
|
|
@@ -19,6 +39,22 @@ export class ChatSessionListManager {
|
|
|
19
39
|
return next;
|
|
20
40
|
};
|
|
21
41
|
|
|
42
|
+
private shouldPersistReadAt = (
|
|
43
|
+
sessionKey: string,
|
|
44
|
+
readAt: string,
|
|
45
|
+
currentReadAt?: string | null,
|
|
46
|
+
): boolean => {
|
|
47
|
+
const optimisticReadAt = useChatSessionListStore.getState().optimisticReadAtBySessionKey[sessionKey];
|
|
48
|
+
const effectiveCurrentReadAt =
|
|
49
|
+
optimisticReadAt && currentReadAt
|
|
50
|
+
? (optimisticReadAt.localeCompare(currentReadAt) > 0 ? optimisticReadAt : currentReadAt)
|
|
51
|
+
: optimisticReadAt ?? currentReadAt ?? undefined;
|
|
52
|
+
if (!effectiveCurrentReadAt) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
return readAt.localeCompare(effectiveCurrentReadAt) > 0;
|
|
56
|
+
};
|
|
57
|
+
|
|
22
58
|
setSelectedAgentId = (next: SetStateAction<string>) => {
|
|
23
59
|
const prev = useChatSessionListStore.getState().snapshot.selectedAgentId;
|
|
24
60
|
const value = this.resolveUpdateValue(prev, next);
|
|
@@ -46,22 +82,25 @@ export class ChatSessionListManager {
|
|
|
46
82
|
useChatSessionListStore.getState().setSnapshot({ listMode: value });
|
|
47
83
|
};
|
|
48
84
|
|
|
49
|
-
markSessionRead = (
|
|
50
|
-
|
|
85
|
+
markSessionRead = (
|
|
86
|
+
sessionKey: string | null | undefined,
|
|
87
|
+
readAt: string | null | undefined,
|
|
88
|
+
currentReadAt?: string | null,
|
|
89
|
+
) => {
|
|
90
|
+
const normalizedSessionKey = sessionKey?.trim();
|
|
91
|
+
const normalizedReadAt = readAt?.trim();
|
|
92
|
+
if (!normalizedSessionKey || !normalizedReadAt) {
|
|
51
93
|
return;
|
|
52
94
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
) => {
|
|
59
|
-
useChatSessionListStore.getState().hydrateReadWatermarks(entries);
|
|
95
|
+
if (!this.shouldPersistReadAt(normalizedSessionKey, normalizedReadAt, currentReadAt)) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
useChatSessionListStore.getState().markSessionRead(normalizedSessionKey, normalizedReadAt);
|
|
99
|
+
void updateNcpSession(normalizedSessionKey, { uiReadAt: normalizedReadAt }).catch(() => undefined);
|
|
60
100
|
};
|
|
61
101
|
|
|
62
102
|
createSession = (sessionType?: string, projectRoot?: string | null): string => {
|
|
63
103
|
const { snapshot } = useChatInputStore.getState();
|
|
64
|
-
const { snapshot: sessionListSnapshot } = useChatSessionListStore.getState();
|
|
65
104
|
const { defaultSessionType: configuredDefaultSessionType } = snapshot;
|
|
66
105
|
const defaultSessionType = configuredDefaultSessionType || 'native';
|
|
67
106
|
const nextSessionType =
|
|
@@ -69,17 +108,19 @@ export class ChatSessionListManager {
|
|
|
69
108
|
? sessionType.trim()
|
|
70
109
|
: defaultSessionType;
|
|
71
110
|
const normalizedProjectRoot = normalizeSessionProjectRootValue(projectRoot);
|
|
72
|
-
const nextSessionKey =
|
|
111
|
+
const nextSessionKey = createNcpSessionId();
|
|
73
112
|
this.streamActionsManager.resetStreamState();
|
|
74
113
|
useChatSessionListStore.getState().setSnapshot({
|
|
75
|
-
|
|
114
|
+
selectedSessionKey: null,
|
|
115
|
+
draftSessionKey: nextSessionKey
|
|
76
116
|
});
|
|
117
|
+
this.syncDraftThreadState(nextSessionKey);
|
|
77
118
|
useChatInputStore.getState().setSnapshot({
|
|
78
119
|
pendingSessionType: nextSessionType,
|
|
79
120
|
pendingProjectRoot: normalizedProjectRoot,
|
|
80
121
|
pendingProjectRootSessionKey: normalizedProjectRoot ? nextSessionKey : null
|
|
81
122
|
});
|
|
82
|
-
this.uiManager.
|
|
123
|
+
this.uiManager.goToChatRoot();
|
|
83
124
|
return nextSessionKey;
|
|
84
125
|
};
|
|
85
126
|
|
|
@@ -88,10 +129,41 @@ export class ChatSessionListManager {
|
|
|
88
129
|
if (snapshot.selectedSessionKey) {
|
|
89
130
|
return snapshot.selectedSessionKey;
|
|
90
131
|
}
|
|
91
|
-
|
|
132
|
+
const normalizedSessionType =
|
|
133
|
+
typeof sessionType === 'string' && sessionType.trim().length > 0
|
|
134
|
+
? sessionType.trim()
|
|
135
|
+
: null;
|
|
136
|
+
this.syncDraftThreadState(snapshot.draftSessionKey);
|
|
137
|
+
if (normalizedSessionType) {
|
|
138
|
+
useChatInputStore.getState().setSnapshot({ pendingSessionType: normalizedSessionType });
|
|
139
|
+
}
|
|
140
|
+
return snapshot.draftSessionKey;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
promoteRootDraftSessionRoute = (sessionKey: string) => {
|
|
144
|
+
const normalizedSessionKey = sessionKey.trim();
|
|
145
|
+
if (!normalizedSessionKey) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const { snapshot } = useChatSessionListStore.getState();
|
|
149
|
+
const { sessionKey: currentThreadSessionKey } = useChatThreadStore.getState().snapshot;
|
|
150
|
+
if (
|
|
151
|
+
snapshot.selectedSessionKey !== null ||
|
|
152
|
+
snapshot.draftSessionKey !== normalizedSessionKey ||
|
|
153
|
+
currentThreadSessionKey !== normalizedSessionKey ||
|
|
154
|
+
!this.uiManager.isAtChatRoot()
|
|
155
|
+
) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
this.uiManager.goToSession(normalizedSessionKey, { replace: true });
|
|
92
159
|
};
|
|
93
160
|
|
|
94
161
|
selectSession = (sessionKey: string) => {
|
|
162
|
+
useChatThreadStore.getState().setSnapshot({
|
|
163
|
+
workspacePanelParentKey: null,
|
|
164
|
+
activeChildSessionKey: null,
|
|
165
|
+
activeWorkspaceFileKey: null,
|
|
166
|
+
});
|
|
95
167
|
this.uiManager.goToSession(sessionKey);
|
|
96
168
|
};
|
|
97
169
|
|
|
@@ -59,6 +59,8 @@ export class ChatUiManager {
|
|
|
59
59
|
this.navigateTo('/chat', options);
|
|
60
60
|
};
|
|
61
61
|
|
|
62
|
+
isAtChatRoot = () => this.state.pathname === '/chat';
|
|
63
|
+
|
|
62
64
|
goToSession = (sessionKey: string, options?: NavigateOptions) => {
|
|
63
65
|
this.navigateTo(buildSessionPath(sessionKey), options);
|
|
64
66
|
};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
## 子树边界豁免
|
|
2
2
|
|
|
3
|
-
- 原因:`chat/ncp/` 目录是 NCP 聊天运行时的装配子树,需要并列保留页面数据、派生状态、输入/线程 manager、session adapter 与测试文件。本次新增派生状态模块是为了拆短 `
|
|
3
|
+
- 原因:`chat/ncp/` 目录是 NCP 聊天运行时的装配子树,需要并列保留页面数据、派生状态、输入/线程 manager、session adapter 与测试文件。本次新增派生状态模块是为了拆短 `ncp-chat-page.tsx`,属于职责下沉,而不是继续把复杂度堆回页面壳。
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from '@/components/chat/chat-composer-state';
|
|
16
16
|
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
17
17
|
import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
|
|
18
|
+
import { useChatThreadStore } from '@/components/chat/stores/chat-thread.store';
|
|
18
19
|
import type { ChatInputSnapshot } from '@/components/chat/stores/chat-input.store';
|
|
19
20
|
import type { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
|
|
20
21
|
import type { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
|
|
@@ -170,6 +171,7 @@ export class NcpChatInputManager {
|
|
|
170
171
|
send = async () => {
|
|
171
172
|
const inputSnapshot = useChatInputStore.getState().snapshot;
|
|
172
173
|
const sessionSnapshot = useChatSessionListStore.getState().snapshot;
|
|
174
|
+
const threadSnapshot = useChatThreadStore.getState().snapshot;
|
|
173
175
|
const message = inputSnapshot.draft.trim();
|
|
174
176
|
const attachments = inputSnapshot.attachments;
|
|
175
177
|
const parts = deriveNcpMessagePartsFromComposer(inputSnapshot.composerNodes, attachments);
|
|
@@ -180,7 +182,10 @@ export class NcpChatInputManager {
|
|
|
180
182
|
return;
|
|
181
183
|
}
|
|
182
184
|
const { selectedSkills: requestedSkills, composerNodes } = inputSnapshot;
|
|
183
|
-
const sessionKey =
|
|
185
|
+
const sessionKey =
|
|
186
|
+
threadSnapshot.sessionKey ??
|
|
187
|
+
sessionSnapshot.selectedSessionKey ??
|
|
188
|
+
this.sessionListManager.ensureDraftSession(inputSnapshot.selectedSessionType);
|
|
184
189
|
this.setComposerNodes(createInitialChatComposerNodes());
|
|
185
190
|
await this.streamActionsManager.sendMessage({
|
|
186
191
|
message,
|
|
@@ -196,6 +201,7 @@ export class NcpChatInputManager {
|
|
|
196
201
|
restoreDraftOnError: true,
|
|
197
202
|
composerNodes
|
|
198
203
|
});
|
|
204
|
+
this.sessionListManager.promoteRootDraftSessionRoute(sessionKey);
|
|
199
205
|
};
|
|
200
206
|
|
|
201
207
|
stop = async () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { buildNcpSendMetadata } from '@/components/chat/ncp/
|
|
2
|
+
import { buildNcpSendMetadata } from '@/components/chat/ncp/ncp-chat-page';
|
|
3
3
|
import { filterModelOptionsBySessionType } from '@/components/chat/ncp/ncp-chat-page-data';
|
|
4
4
|
import type { ChatModelOption } from '@/components/chat/chat-input.types';
|
|
5
5
|
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
useEffect,
|
|
3
3
|
useMemo,
|
|
4
4
|
useRef,
|
|
5
|
+
useState,
|
|
5
6
|
} from "react";
|
|
6
7
|
import {
|
|
7
8
|
buildNcpRequestEnvelope,
|
|
@@ -98,11 +99,7 @@ export function shouldClearPendingProjectRootOverride(params: {
|
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
export function NcpChatPage({ view }: ChatPageProps) {
|
|
101
|
-
const
|
|
102
|
-
if (!presenterRef.current) {
|
|
103
|
-
presenterRef.current = new NcpChatPresenter();
|
|
104
|
-
}
|
|
105
|
-
const presenter = presenterRef.current;
|
|
102
|
+
const [presenter] = useState(() => new NcpChatPresenter());
|
|
106
103
|
const query = useChatSessionListStore((state) => state.snapshot.query);
|
|
107
104
|
const selectedSessionKey = useChatSessionListStore(
|
|
108
105
|
(state) => state.snapshot.selectedSessionKey,
|
|
@@ -306,8 +303,10 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
306
303
|
currentAgentId,
|
|
307
304
|
currentAgent,
|
|
308
305
|
parentSession,
|
|
309
|
-
currentSessionTypeLabel
|
|
306
|
+
currentSessionTypeLabel,
|
|
307
|
+
currentChildSessionTabs,
|
|
310
308
|
} = useNcpChatDerivedState({
|
|
309
|
+
sessionKey,
|
|
311
310
|
selectedSession,
|
|
312
311
|
selectedAgentId,
|
|
313
312
|
availableAgents,
|
|
@@ -352,7 +351,8 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
352
351
|
threadRef,
|
|
353
352
|
agent,
|
|
354
353
|
isAwaitingAssistantOutput,
|
|
355
|
-
parentSession
|
|
354
|
+
parentSession,
|
|
355
|
+
childSessionTabs: currentChildSessionTabs,
|
|
356
356
|
});
|
|
357
357
|
|
|
358
358
|
return (
|