@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
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { appQueryClient } from '@/app-query-client';
|
|
2
2
|
import { deleteNcpSessionSummaryInQueryClient } from '@/api/ncp-session-query-cache';
|
|
3
3
|
import { deleteNcpSession as deleteNcpSessionApi } from '@/api/ncp-session';
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
ChatFileOpenActionViewModel,
|
|
6
|
+
ChatToolActionViewModel,
|
|
7
|
+
} from '@nextclaw/agent-chat-ui';
|
|
5
8
|
import type { ChatSessionListManager } from '@/components/chat/managers/chat-session-list.manager';
|
|
6
9
|
import type { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
|
|
7
10
|
import type { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
|
|
8
11
|
import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
|
|
9
12
|
import type {
|
|
10
|
-
ChatChildSessionTab,
|
|
11
13
|
ChatThreadSnapshot,
|
|
14
|
+
ChatWorkspaceFileTab,
|
|
12
15
|
} from '@/components/chat/stores/chat-thread.store';
|
|
13
16
|
import { useChatThreadStore } from '@/components/chat/stores/chat-thread.store';
|
|
14
17
|
import { t } from '@/lib/i18n';
|
|
@@ -37,6 +40,98 @@ export class NcpChatThreadManager {
|
|
|
37
40
|
useChatThreadStore.getState().setSnapshot(patch);
|
|
38
41
|
};
|
|
39
42
|
|
|
43
|
+
private clearDeletedSessionState = (sessionKey: string) => {
|
|
44
|
+
if (useChatSessionListStore.getState().snapshot.selectedSessionKey === sessionKey) {
|
|
45
|
+
this.sessionListManager.setSelectedSessionKey(null);
|
|
46
|
+
}
|
|
47
|
+
useChatThreadStore.getState().setSnapshot({
|
|
48
|
+
sessionKey: null,
|
|
49
|
+
sessionTypeLabel: null,
|
|
50
|
+
agentId: null,
|
|
51
|
+
agentDisplayName: null,
|
|
52
|
+
agentAvatarUrl: null,
|
|
53
|
+
sessionDisplayName: undefined,
|
|
54
|
+
sessionProjectRoot: null,
|
|
55
|
+
sessionProjectName: null,
|
|
56
|
+
canDeleteSession: false,
|
|
57
|
+
isHistoryLoading: false,
|
|
58
|
+
messages: [],
|
|
59
|
+
isSending: false,
|
|
60
|
+
isAwaitingAssistantOutput: false,
|
|
61
|
+
parentSessionKey: null,
|
|
62
|
+
parentSessionLabel: null,
|
|
63
|
+
workspacePanelParentKey: null,
|
|
64
|
+
childSessionTabs: [],
|
|
65
|
+
activeChildSessionKey: null,
|
|
66
|
+
workspaceFileTabs: [],
|
|
67
|
+
activeWorkspaceFileKey: null,
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
private resolveWorkspaceParentSessionKey = (): string | null => {
|
|
72
|
+
const threadSessionKey = useChatThreadStore.getState().snapshot.sessionKey?.trim();
|
|
73
|
+
if (threadSessionKey) {
|
|
74
|
+
return threadSessionKey;
|
|
75
|
+
}
|
|
76
|
+
return useChatSessionListStore.getState().snapshot.selectedSessionKey ?? null;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
private buildWorkspaceFileTab = (
|
|
80
|
+
action: ChatFileOpenActionViewModel,
|
|
81
|
+
parentSessionKey: string | null,
|
|
82
|
+
): ChatWorkspaceFileTab | null => {
|
|
83
|
+
const normalizedPath = action.path.trim();
|
|
84
|
+
if (!normalizedPath) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const normalizedParentSessionKey = parentSessionKey?.trim() || null;
|
|
88
|
+
const key =
|
|
89
|
+
`${normalizedParentSessionKey ?? 'draft'}::${action.viewMode}::${normalizedPath}`;
|
|
90
|
+
return {
|
|
91
|
+
key,
|
|
92
|
+
parentSessionKey: normalizedParentSessionKey,
|
|
93
|
+
path: normalizedPath,
|
|
94
|
+
label: action.label?.trim() || null,
|
|
95
|
+
viewMode: action.viewMode,
|
|
96
|
+
line: action.line ?? null,
|
|
97
|
+
column: action.column ?? null,
|
|
98
|
+
rawText: action.rawText ?? null,
|
|
99
|
+
beforeText: action.beforeText ?? null,
|
|
100
|
+
afterText: action.afterText ?? null,
|
|
101
|
+
patchText: action.patchText ?? null,
|
|
102
|
+
oldStartLine: action.oldStartLine ?? null,
|
|
103
|
+
newStartLine: action.newStartLine ?? null,
|
|
104
|
+
fullLines: action.fullLines,
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
private upsertWorkspaceFileTab = (nextTab: ChatWorkspaceFileTab): ChatWorkspaceFileTab[] => {
|
|
109
|
+
const { workspaceFileTabs } = useChatThreadStore.getState().snapshot;
|
|
110
|
+
const existingIndex = workspaceFileTabs.findIndex((tab) => tab.key === nextTab.key);
|
|
111
|
+
if (existingIndex === -1) {
|
|
112
|
+
return [nextTab, ...workspaceFileTabs];
|
|
113
|
+
}
|
|
114
|
+
const nextTabs = [...workspaceFileTabs];
|
|
115
|
+
nextTabs.splice(existingIndex, 1);
|
|
116
|
+
nextTabs.unshift({
|
|
117
|
+
...workspaceFileTabs[existingIndex],
|
|
118
|
+
...nextTab,
|
|
119
|
+
});
|
|
120
|
+
return nextTabs;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
private ensureWorkspaceParentRoute = (parentSessionKey: string | null) => {
|
|
124
|
+
if (!parentSessionKey) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const {
|
|
128
|
+
snapshot: { selectedSessionKey },
|
|
129
|
+
} = useChatSessionListStore.getState();
|
|
130
|
+
if (selectedSessionKey !== parentSessionKey) {
|
|
131
|
+
this.uiManager.goToSession(parentSessionKey);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
40
135
|
deleteSession = () => {
|
|
41
136
|
void this.deleteCurrentSession();
|
|
42
137
|
};
|
|
@@ -49,21 +144,36 @@ export class NcpChatThreadManager {
|
|
|
49
144
|
this.uiManager.goToProviders();
|
|
50
145
|
};
|
|
51
146
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
147
|
+
openChildSessionPanel = (params: {
|
|
148
|
+
parentSessionKey: string;
|
|
149
|
+
activeChildSessionKey?: string | null;
|
|
150
|
+
}) => {
|
|
151
|
+
const parentSessionKey = params.parentSessionKey.trim();
|
|
152
|
+
if (!parentSessionKey) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const activeChildSessionKey = params.activeChildSessionKey?.trim() || null;
|
|
156
|
+
useChatThreadStore.getState().setSnapshot({
|
|
157
|
+
workspacePanelParentKey: parentSessionKey,
|
|
158
|
+
activeChildSessionKey,
|
|
159
|
+
activeWorkspaceFileKey: null,
|
|
160
|
+
});
|
|
161
|
+
this.ensureWorkspaceParentRoute(parentSessionKey);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
openFilePreview = (action: ChatFileOpenActionViewModel) => {
|
|
165
|
+
const parentSessionKey = this.resolveWorkspaceParentSessionKey();
|
|
166
|
+
const nextTab = this.buildWorkspaceFileTab(action, parentSessionKey);
|
|
167
|
+
if (!nextTab) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
63
170
|
useChatThreadStore.getState().setSnapshot({
|
|
64
|
-
|
|
65
|
-
|
|
171
|
+
workspacePanelParentKey: parentSessionKey,
|
|
172
|
+
workspaceFileTabs: this.upsertWorkspaceFileTab(nextTab),
|
|
173
|
+
activeWorkspaceFileKey: nextTab.key,
|
|
174
|
+
activeChildSessionKey: null,
|
|
66
175
|
});
|
|
176
|
+
this.ensureWorkspaceParentRoute(parentSessionKey);
|
|
67
177
|
};
|
|
68
178
|
|
|
69
179
|
openSessionFromToolAction = (action: ChatToolActionViewModel) => {
|
|
@@ -75,14 +185,19 @@ export class NcpChatThreadManager {
|
|
|
75
185
|
action.parentSessionId?.trim() ||
|
|
76
186
|
useChatSessionListStore.getState().snapshot.selectedSessionKey ||
|
|
77
187
|
null;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
188
|
+
if (parentSessionKey) {
|
|
189
|
+
this.openChildSessionPanel({
|
|
190
|
+
parentSessionKey,
|
|
191
|
+
activeChildSessionKey: action.sessionId,
|
|
192
|
+
});
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
85
195
|
}
|
|
196
|
+
useChatThreadStore.getState().setSnapshot({
|
|
197
|
+
workspacePanelParentKey: null,
|
|
198
|
+
activeChildSessionKey: null,
|
|
199
|
+
activeWorkspaceFileKey: null,
|
|
200
|
+
});
|
|
86
201
|
this.uiManager.goToSession(action.sessionId);
|
|
87
202
|
};
|
|
88
203
|
|
|
@@ -97,34 +212,56 @@ export class NcpChatThreadManager {
|
|
|
97
212
|
}
|
|
98
213
|
useChatThreadStore.getState().setSnapshot({
|
|
99
214
|
activeChildSessionKey: normalizedSessionKey,
|
|
215
|
+
activeWorkspaceFileKey: null,
|
|
100
216
|
});
|
|
101
217
|
};
|
|
102
218
|
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
childSessionTabs,
|
|
107
|
-
activeChildSessionKey,
|
|
108
|
-
} = useChatThreadStore.getState().snapshot;
|
|
109
|
-
if (!sessionKey) {
|
|
110
|
-
useChatThreadStore.getState().setSnapshot({
|
|
111
|
-
childSessionTabs: [],
|
|
112
|
-
activeChildSessionKey: null,
|
|
113
|
-
});
|
|
219
|
+
selectWorkspaceFile = (fileKey: string) => {
|
|
220
|
+
const normalizedFileKey = fileKey.trim();
|
|
221
|
+
if (!normalizedFileKey) {
|
|
114
222
|
return;
|
|
115
223
|
}
|
|
116
|
-
const
|
|
117
|
-
|
|
224
|
+
const { workspaceFileTabs } = useChatThreadStore.getState().snapshot;
|
|
225
|
+
if (!workspaceFileTabs.some((tab) => tab.key === normalizedFileKey)) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
useChatThreadStore.getState().setSnapshot({
|
|
229
|
+
activeWorkspaceFileKey: normalizedFileKey,
|
|
230
|
+
activeChildSessionKey: null,
|
|
231
|
+
});
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
closeWorkspaceFile = (fileKey: string) => {
|
|
235
|
+
const normalizedFileKey = fileKey.trim();
|
|
236
|
+
if (!normalizedFileKey) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const { snapshot } = useChatThreadStore.getState();
|
|
240
|
+
const { activeWorkspaceFileKey, workspaceFileTabs } = snapshot;
|
|
241
|
+
const nextTabs = workspaceFileTabs.filter(
|
|
242
|
+
(tab) => tab.key !== normalizedFileKey,
|
|
118
243
|
);
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
244
|
+
const nextPatch: Partial<ChatThreadSnapshot> = {
|
|
245
|
+
workspaceFileTabs: nextTabs,
|
|
246
|
+
};
|
|
247
|
+
if (activeWorkspaceFileKey === normalizedFileKey) {
|
|
248
|
+
nextPatch.activeWorkspaceFileKey = null;
|
|
249
|
+
}
|
|
250
|
+
useChatThreadStore.getState().setSnapshot(nextPatch);
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
closeWorkspacePanel = () => {
|
|
122
254
|
useChatThreadStore.getState().setSnapshot({
|
|
123
|
-
|
|
124
|
-
activeChildSessionKey:
|
|
255
|
+
workspacePanelParentKey: null,
|
|
256
|
+
activeChildSessionKey: null,
|
|
257
|
+
activeWorkspaceFileKey: null,
|
|
125
258
|
});
|
|
126
259
|
};
|
|
127
260
|
|
|
261
|
+
closeChildSessionDetail = () => {
|
|
262
|
+
this.closeWorkspacePanel();
|
|
263
|
+
};
|
|
264
|
+
|
|
128
265
|
goToParentSession = () => {
|
|
129
266
|
const {
|
|
130
267
|
parentSessionKey,
|
|
@@ -139,7 +276,7 @@ export class NcpChatThreadManager {
|
|
|
139
276
|
if (!resolvedParentSessionKey) {
|
|
140
277
|
return;
|
|
141
278
|
}
|
|
142
|
-
this.
|
|
279
|
+
this.closeWorkspacePanel();
|
|
143
280
|
this.uiManager.goToSession(resolvedParentSessionKey);
|
|
144
281
|
};
|
|
145
282
|
|
|
@@ -171,6 +308,7 @@ export class NcpChatThreadManager {
|
|
|
171
308
|
deleteNcpSessionSummaryInQueryClient(appQueryClient, selectedSessionKey);
|
|
172
309
|
appQueryClient.removeQueries({ queryKey: ['ncp-session-messages', selectedSessionKey] });
|
|
173
310
|
this.streamActionsManager.resetStreamState();
|
|
311
|
+
this.clearDeletedSessionState(selectedSessionKey);
|
|
174
312
|
this.uiManager.goToChatRoot({ replace: true });
|
|
175
313
|
} finally {
|
|
176
314
|
useChatThreadStore.getState().setSnapshot({ isDeletePending: false });
|
|
@@ -21,12 +21,14 @@ describe('adaptNcpSessionSummary', () => {
|
|
|
21
21
|
const adapted = adaptNcpSessionSummary(
|
|
22
22
|
createSummary({
|
|
23
23
|
agentId: 'engineer',
|
|
24
|
+
lastMessageAt: '2026-03-18T00:00:00.000Z',
|
|
24
25
|
metadata: {
|
|
25
26
|
label: 'NCP Planning Thread',
|
|
26
27
|
model: 'openai/gpt-5',
|
|
27
28
|
preferred_thinking: 'medium',
|
|
28
29
|
project_root: '/Users/demo/workspace/project-alpha',
|
|
29
|
-
session_type: 'native'
|
|
30
|
+
session_type: 'native',
|
|
31
|
+
ui_last_read_at: '2026-03-17T23:59:00.000Z'
|
|
30
32
|
}
|
|
31
33
|
})
|
|
32
34
|
);
|
|
@@ -39,6 +41,8 @@ describe('adaptNcpSessionSummary', () => {
|
|
|
39
41
|
preferredThinking: 'medium',
|
|
40
42
|
projectRoot: '/Users/demo/workspace/project-alpha',
|
|
41
43
|
projectName: 'project-alpha',
|
|
44
|
+
lastMessageAt: '2026-03-18T00:00:00.000Z',
|
|
45
|
+
readAt: '2026-03-17T23:59:00.000Z',
|
|
42
46
|
sessionType: 'native',
|
|
43
47
|
sessionTypeMutable: false,
|
|
44
48
|
isChildSession: false,
|
|
@@ -67,7 +71,7 @@ describe('adaptNcpSessionSummary', () => {
|
|
|
67
71
|
});
|
|
68
72
|
});
|
|
69
73
|
|
|
70
|
-
describe('adaptNcpMessageToUiMessage', () => {
|
|
74
|
+
describe('adaptNcpMessageToUiMessage file rendering', () => {
|
|
71
75
|
it('preserves mixed text and image part order for message rendering', () => {
|
|
72
76
|
const adapted = adaptNcpMessageToUiMessage({
|
|
73
77
|
id: 'ncp-message-1',
|
|
@@ -107,6 +111,40 @@ describe('adaptNcpMessageToUiMessage', () => {
|
|
|
107
111
|
]);
|
|
108
112
|
});
|
|
109
113
|
|
|
114
|
+
it('maps assetUri file parts into asset content urls for rendering', () => {
|
|
115
|
+
const adapted = adaptNcpMessageToUiMessage({
|
|
116
|
+
id: 'ncp-message-asset-1',
|
|
117
|
+
sessionId: 'ncp-session-1',
|
|
118
|
+
role: 'assistant',
|
|
119
|
+
status: 'final',
|
|
120
|
+
timestamp: '2026-04-16T00:00:00.000Z',
|
|
121
|
+
parts: [
|
|
122
|
+
{
|
|
123
|
+
type: 'file',
|
|
124
|
+
name: 'diagram.png',
|
|
125
|
+
mimeType: 'image/png',
|
|
126
|
+
assetUri: 'asset://store/2026/04/16/asset_123',
|
|
127
|
+
sizeBytes: 42,
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
expect(adapted.parts).toHaveLength(1);
|
|
133
|
+
expect(adapted.parts[0]).toMatchObject({
|
|
134
|
+
type: 'file',
|
|
135
|
+
name: 'diagram.png',
|
|
136
|
+
mimeType: 'image/png',
|
|
137
|
+
data: '',
|
|
138
|
+
sizeBytes: 42,
|
|
139
|
+
});
|
|
140
|
+
expect((adapted.parts[0] as { url?: string }).url).toMatch(
|
|
141
|
+
/\/api\/ncp\/assets\/content\?uri=asset%3A%2F%2Fstore%2F2026%2F04%2F16%2Fasset_123$/,
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('adaptNcpMessageToUiMessage tool rendering', () => {
|
|
110
148
|
it('keeps streamed native file tool args renderable as a preview before the tool result arrives', () => {
|
|
111
149
|
const uiMessage = adaptNcpMessageToUiMessage({
|
|
112
150
|
id: 'ncp-message-tool-1',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ToolInvocationStatus, type UIMessage } from '@nextclaw/agent-chat';
|
|
2
2
|
import type { NcpMessagePart } from '@nextclaw/ncp';
|
|
3
3
|
import type { NcpMessageView, NcpSessionSummaryView, SessionEntryView, ThinkingLevel } from '@/api/types';
|
|
4
|
+
import { API_BASE } from '@/api/api-base';
|
|
4
5
|
import {
|
|
5
6
|
getSessionProjectName,
|
|
6
7
|
normalizeSessionProjectRootValue,
|
|
@@ -19,6 +20,11 @@ function stringifyUnknown(value: unknown): string {
|
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
function buildNcpAssetContentUrl(assetUri: string): string {
|
|
24
|
+
const query = new URLSearchParams({ uri: assetUri });
|
|
25
|
+
return `${API_BASE}/api/ncp/assets/content?${query.toString()}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
22
28
|
function readOptionalString(value: unknown): string | null {
|
|
23
29
|
if (typeof value !== 'string') {
|
|
24
30
|
return null;
|
|
@@ -80,6 +86,14 @@ function readNcpSessionProjectRoot(summary: NcpSessionSummaryView): string | nul
|
|
|
80
86
|
return normalizeSessionProjectRootValue(metadata.project_root ?? metadata.projectRoot);
|
|
81
87
|
}
|
|
82
88
|
|
|
89
|
+
function readNcpSessionReadAt(summary: NcpSessionSummaryView): string | null {
|
|
90
|
+
const metadata = readMetadata(summary);
|
|
91
|
+
if (!metadata) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return readOptionalString(metadata.ui_last_read_at);
|
|
95
|
+
}
|
|
96
|
+
|
|
83
97
|
function readNcpSessionType(summary: NcpSessionSummaryView): string {
|
|
84
98
|
const metadata = readMetadata(summary);
|
|
85
99
|
if (!metadata) {
|
|
@@ -199,6 +213,17 @@ function toUiParts(parts: NcpMessagePart[]): UIMessage['parts'] {
|
|
|
199
213
|
});
|
|
200
214
|
continue;
|
|
201
215
|
}
|
|
216
|
+
if (part.type === 'file' && part.assetUri) {
|
|
217
|
+
uiParts.push({
|
|
218
|
+
type: 'file',
|
|
219
|
+
...(part.name ? { name: part.name } : {}),
|
|
220
|
+
mimeType: part.mimeType ?? 'application/octet-stream',
|
|
221
|
+
data: '',
|
|
222
|
+
url: buildNcpAssetContentUrl(part.assetUri),
|
|
223
|
+
...(typeof part.sizeBytes === 'number' ? { sizeBytes: part.sizeBytes } : {})
|
|
224
|
+
});
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
202
227
|
if (part.type === 'step-start') {
|
|
203
228
|
uiParts.push({ type: 'step-start' });
|
|
204
229
|
continue;
|
|
@@ -249,6 +274,8 @@ export function adaptNcpSessionSummary(summary: NcpSessionSummaryView): SessionE
|
|
|
249
274
|
const preferredModel = readNcpSessionPreferredModel(summary);
|
|
250
275
|
const preferredThinking = readNcpSessionPreferredThinking(summary);
|
|
251
276
|
const projectRoot = readNcpSessionProjectRoot(summary);
|
|
277
|
+
const readAt = readNcpSessionReadAt(summary);
|
|
278
|
+
const lastMessageAt = readOptionalString(summary.lastMessageAt);
|
|
252
279
|
const projectName = getSessionProjectName(projectRoot);
|
|
253
280
|
const context = parseSessionContext(summary.sessionId);
|
|
254
281
|
const parentSessionId = readNcpParentSessionId(summary);
|
|
@@ -258,6 +285,8 @@ export function adaptNcpSessionSummary(summary: NcpSessionSummaryView): SessionE
|
|
|
258
285
|
key: summary.sessionId,
|
|
259
286
|
createdAt: summary.updatedAt,
|
|
260
287
|
updatedAt: summary.updatedAt,
|
|
288
|
+
...(lastMessageAt ? { lastMessageAt } : {}),
|
|
289
|
+
...(readAt ? { readAt } : {}),
|
|
261
290
|
...(typeof summary.agentId === 'string' && summary.agentId.trim().length > 0 ? { agentId: summary.agentId.trim() } : {}),
|
|
262
291
|
...(label ? { label } : {}),
|
|
263
292
|
...context,
|
|
@@ -10,8 +10,30 @@ import { adaptNcpSessionSummary } from '@/components/chat/ncp/ncp-session-adapte
|
|
|
10
10
|
import type { NcpChatPresenter } from '@/components/chat/ncp/ncp-chat.presenter';
|
|
11
11
|
import type { UseHydratedNcpAgentResult } from '@nextclaw/ncp-react';
|
|
12
12
|
import type { ChatModelOption } from '@/components/chat/chat-input.types';
|
|
13
|
+
import type { ChatChildSessionTab } from '@/components/chat/stores/chat-thread.store';
|
|
13
14
|
import { resolveSessionTypeLabel } from '@/components/chat/useChatSessionTypeState';
|
|
15
|
+
|
|
16
|
+
function buildChildSessionTabs(params: {
|
|
17
|
+
parentSessionKey: string | null;
|
|
18
|
+
sessionSummaries: NcpSessionSummaryView[];
|
|
19
|
+
}): ChatChildSessionTab[] {
|
|
20
|
+
if (!params.parentSessionKey) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
return params.sessionSummaries
|
|
24
|
+
.map(adaptNcpSessionSummary)
|
|
25
|
+
.filter((session) => session.parentSessionId === params.parentSessionKey)
|
|
26
|
+
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))
|
|
27
|
+
.map((session) => ({
|
|
28
|
+
sessionKey: session.key,
|
|
29
|
+
parentSessionKey: session.parentSessionId ?? null,
|
|
30
|
+
label: session.label ?? null,
|
|
31
|
+
agentId: session.agentId ?? null,
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
|
|
14
35
|
export function useNcpChatDerivedState(params: {
|
|
36
|
+
sessionKey: string | null;
|
|
15
37
|
selectedSession: SessionEntryView | null;
|
|
16
38
|
selectedAgentId: string;
|
|
17
39
|
availableAgents: AgentProfileView[];
|
|
@@ -20,32 +42,51 @@ export function useNcpChatDerivedState(params: {
|
|
|
20
42
|
selectedSessionType: string;
|
|
21
43
|
sessionTypeOptions: Array<{ value: string; label: string }>;
|
|
22
44
|
}) {
|
|
23
|
-
const
|
|
24
|
-
|
|
45
|
+
const {
|
|
46
|
+
availableAgents,
|
|
47
|
+
parentSessionId,
|
|
48
|
+
selectedAgentId,
|
|
49
|
+
selectedSession,
|
|
50
|
+
selectedSessionType,
|
|
51
|
+
sessionKey,
|
|
52
|
+
sessionSummaries,
|
|
53
|
+
sessionTypeOptions,
|
|
54
|
+
} = params;
|
|
55
|
+
const currentSessionDisplayName = selectedSession
|
|
56
|
+
? sessionDisplayName(selectedSession)
|
|
25
57
|
: undefined;
|
|
26
|
-
const currentAgentId =
|
|
58
|
+
const currentAgentId = selectedSession?.agentId ?? selectedAgentId;
|
|
27
59
|
const currentAgent =
|
|
28
|
-
|
|
60
|
+
availableAgents.find((agent) => agent.id === currentAgentId) ?? null;
|
|
29
61
|
const parentSession = useMemo(() => {
|
|
30
|
-
if (!
|
|
62
|
+
if (!parentSessionId) {
|
|
31
63
|
return null;
|
|
32
64
|
}
|
|
33
65
|
const parentSummary =
|
|
34
|
-
|
|
35
|
-
(summary) => summary.sessionId ===
|
|
66
|
+
sessionSummaries.find(
|
|
67
|
+
(summary) => summary.sessionId === parentSessionId,
|
|
36
68
|
) ?? null;
|
|
37
69
|
return parentSummary ? adaptNcpSessionSummary(parentSummary) : null;
|
|
38
|
-
}, [
|
|
70
|
+
}, [parentSessionId, sessionSummaries]);
|
|
39
71
|
const currentSessionTypeLabel =
|
|
40
|
-
|
|
41
|
-
?.label ?? resolveSessionTypeLabel(
|
|
72
|
+
sessionTypeOptions.find((option) => option.value === selectedSessionType)
|
|
73
|
+
?.label ?? resolveSessionTypeLabel(selectedSessionType);
|
|
74
|
+
const currentChildSessionTabs = useMemo(
|
|
75
|
+
() =>
|
|
76
|
+
buildChildSessionTabs({
|
|
77
|
+
parentSessionKey: sessionKey,
|
|
78
|
+
sessionSummaries,
|
|
79
|
+
}),
|
|
80
|
+
[sessionKey, sessionSummaries],
|
|
81
|
+
);
|
|
42
82
|
|
|
43
83
|
return {
|
|
44
84
|
currentSessionDisplayName,
|
|
45
85
|
currentAgentId,
|
|
46
86
|
currentAgent,
|
|
47
87
|
parentSession,
|
|
48
|
-
currentSessionTypeLabel
|
|
88
|
+
currentSessionTypeLabel,
|
|
89
|
+
currentChildSessionTabs,
|
|
49
90
|
};
|
|
50
91
|
}
|
|
51
92
|
|
|
@@ -78,6 +119,7 @@ export function useNcpChatSnapshotSync(params: {
|
|
|
78
119
|
agent: Pick<UseHydratedNcpAgentResult, 'isHydrating' | 'visibleMessages'>;
|
|
79
120
|
isAwaitingAssistantOutput: boolean;
|
|
80
121
|
parentSession: SessionEntryView | null;
|
|
122
|
+
childSessionTabs: ChatChildSessionTab[];
|
|
81
123
|
}) {
|
|
82
124
|
useEffect(() => {
|
|
83
125
|
params.presenter.chatInputManager.syncSnapshot({
|
|
@@ -121,6 +163,7 @@ export function useNcpChatSnapshotSync(params: {
|
|
|
121
163
|
parentSessionLabel: params.parentSession
|
|
122
164
|
? sessionDisplayName(params.parentSession)
|
|
123
165
|
: null,
|
|
166
|
+
childSessionTabs: params.childSessionTabs,
|
|
124
167
|
});
|
|
125
168
|
}, [
|
|
126
169
|
params
|
|
@@ -13,6 +13,8 @@ export type ResolvedChildSessionTab = {
|
|
|
13
13
|
title: string;
|
|
14
14
|
agentId: string | null;
|
|
15
15
|
updatedAt: string | null;
|
|
16
|
+
lastMessageAt: string | null;
|
|
17
|
+
readAt: string | null;
|
|
16
18
|
runStatus?: SessionRunStatus;
|
|
17
19
|
sessionTypeLabel: string | null;
|
|
18
20
|
preferredModel: string | null;
|
|
@@ -64,6 +66,8 @@ export function useNcpChildSessionTabsView(
|
|
|
64
66
|
title: resolveChildSessionTitle(tab, session),
|
|
65
67
|
agentId,
|
|
66
68
|
updatedAt: session?.updatedAt ?? null,
|
|
69
|
+
lastMessageAt: session?.lastMessageAt ?? null,
|
|
70
|
+
readAt: session?.readAt ?? null,
|
|
67
71
|
runStatus: summary?.status === "running" ? "running" : undefined,
|
|
68
72
|
sessionTypeLabel: session?.sessionType
|
|
69
73
|
? resolveSessionTypeLabel(session.sessionType)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { createChatComposerTextNode } from '@nextclaw/agent-chat-ui';
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { NcpChatInputManager } from '@/components/chat/ncp/ncp-chat-input.manager';
|
|
4
|
+
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
5
|
+
import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
|
|
6
|
+
import { useChatThreadStore } from '@/components/chat/stores/chat-thread.store';
|
|
7
|
+
|
|
8
|
+
describe('NcpChatInputManager', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
useChatInputStore.setState({
|
|
11
|
+
snapshot: {
|
|
12
|
+
...useChatInputStore.getState().snapshot,
|
|
13
|
+
draft: 'hello from current thread',
|
|
14
|
+
composerNodes: [createChatComposerTextNode('hello from current thread')],
|
|
15
|
+
attachments: [],
|
|
16
|
+
selectedSkills: [],
|
|
17
|
+
selectedSessionType: 'native',
|
|
18
|
+
selectedModel: '',
|
|
19
|
+
selectedThinkingLevel: null,
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
useChatSessionListStore.setState({
|
|
23
|
+
optimisticReadAtBySessionKey: {},
|
|
24
|
+
snapshot: {
|
|
25
|
+
...useChatSessionListStore.getState().snapshot,
|
|
26
|
+
selectedSessionKey: 'stale-selected-session',
|
|
27
|
+
draftSessionKey: 'draft-root-session',
|
|
28
|
+
selectedAgentId: 'main',
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
useChatThreadStore.setState({
|
|
32
|
+
snapshot: {
|
|
33
|
+
...useChatThreadStore.getState().snapshot,
|
|
34
|
+
sessionKey: 'current-route-session',
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('sends through the current thread session when selected session state is stale', async () => {
|
|
40
|
+
const streamActionsManager = {
|
|
41
|
+
sendMessage: vi.fn().mockResolvedValue(undefined),
|
|
42
|
+
stopCurrentRun: vi.fn().mockResolvedValue(undefined),
|
|
43
|
+
} as unknown as ConstructorParameters<typeof NcpChatInputManager>[1];
|
|
44
|
+
const sessionListManager = {
|
|
45
|
+
ensureDraftSession: vi.fn(() => 'draft-session'),
|
|
46
|
+
promoteRootDraftSessionRoute: vi.fn(),
|
|
47
|
+
} as unknown as ConstructorParameters<typeof NcpChatInputManager>[2];
|
|
48
|
+
const manager = new NcpChatInputManager(
|
|
49
|
+
{} as ConstructorParameters<typeof NcpChatInputManager>[0],
|
|
50
|
+
streamActionsManager,
|
|
51
|
+
sessionListManager,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
await manager.send();
|
|
55
|
+
|
|
56
|
+
expect(streamActionsManager.sendMessage).toHaveBeenCalledTimes(1);
|
|
57
|
+
expect(streamActionsManager.sendMessage).toHaveBeenCalledWith(
|
|
58
|
+
expect.objectContaining({
|
|
59
|
+
sessionKey: 'current-route-session',
|
|
60
|
+
message: 'hello from current thread',
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
expect(sessionListManager.ensureDraftSession).not.toHaveBeenCalled();
|
|
64
|
+
expect(sessionListManager.promoteRootDraftSessionRoute).toHaveBeenCalledWith('current-route-session');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('keeps sending through the current root draft session while /chat is still in blank-draft mode', async () => {
|
|
68
|
+
useChatThreadStore.setState({
|
|
69
|
+
snapshot: {
|
|
70
|
+
...useChatThreadStore.getState().snapshot,
|
|
71
|
+
sessionKey: 'draft-root-session',
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
const streamActionsManager = {
|
|
75
|
+
sendMessage: vi.fn().mockResolvedValue(undefined),
|
|
76
|
+
stopCurrentRun: vi.fn().mockResolvedValue(undefined),
|
|
77
|
+
} as unknown as ConstructorParameters<typeof NcpChatInputManager>[1];
|
|
78
|
+
const sessionListManager = {
|
|
79
|
+
ensureDraftSession: vi.fn(() => 'materialized-draft-session'),
|
|
80
|
+
promoteRootDraftSessionRoute: vi.fn(),
|
|
81
|
+
} as unknown as ConstructorParameters<typeof NcpChatInputManager>[2];
|
|
82
|
+
const manager = new NcpChatInputManager(
|
|
83
|
+
{} as ConstructorParameters<typeof NcpChatInputManager>[0],
|
|
84
|
+
streamActionsManager,
|
|
85
|
+
sessionListManager,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
await manager.send();
|
|
89
|
+
|
|
90
|
+
expect(sessionListManager.ensureDraftSession).not.toHaveBeenCalled();
|
|
91
|
+
expect(streamActionsManager.sendMessage).toHaveBeenCalledWith(
|
|
92
|
+
expect.objectContaining({
|
|
93
|
+
sessionKey: 'draft-root-session',
|
|
94
|
+
message: 'hello from current thread',
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
expect(sessionListManager.promoteRootDraftSessionRoute).toHaveBeenCalledWith('draft-root-session');
|
|
98
|
+
});
|
|
99
|
+
});
|