@nextclaw/ui 0.12.23 → 0.12.25
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 +136 -0
- package/dist/assets/api-DGD9_Bg4.js +15 -0
- package/dist/assets/app-manager-provider-oYdeYPSv.js +1 -0
- package/dist/assets/{book-open-DDlN5MvX.js → book-open-BcnAiKde.js} +1 -1
- package/dist/assets/channels-list-page-FJDuPwU6.js +8 -0
- package/dist/assets/chat-page-D1fMNBrT.js +1 -0
- package/dist/assets/config-split-page-CcrEUtwu.js +1 -0
- package/dist/assets/cpu-DPPwMzoC.js +3 -0
- package/dist/assets/{createLucideIcon-BLMK3QUd.js → createLucideIcon-DzY6wN61.js} +1 -1
- package/dist/assets/desktop-kk7qvZ-v.js +3 -0
- package/dist/assets/desktop-update-config-CP8dFYXK.js +1 -0
- package/dist/assets/{dialog-dxsKz7jJ.js → dialog-BKo0RItd.js} +1 -1
- package/dist/assets/{dist-DsYTOyq7.js → dist-CFiwgaLs.js} +1 -1
- package/dist/assets/doc-browser-CAhfnm0D.js +1 -0
- package/dist/assets/{doc-browser-context-BJuMaI3o.js → doc-browser-context-FukQHvyo.js} +1 -1
- package/dist/assets/doc-browser-p9DDNPWB.js +1 -0
- package/dist/assets/doc-browser-rZIQIjuw.js +1 -0
- package/dist/assets/download-CMM8po31.js +1 -0
- package/dist/assets/{es2015-V75WQJ2s.js → es2015-BhznEEyJ.js} +1 -1
- package/dist/assets/{external-link-DwfSfTLB.js → external-link-CpEvG65F.js} +1 -1
- package/dist/assets/i18n-D1144VAA.js +1 -0
- package/dist/assets/index-D-AAMKCt.js +103 -0
- package/dist/assets/index-DnBeV2Xm.css +1 -0
- package/dist/assets/{key-round-CJ5gDAAG.js → key-round-DUq47t0P.js} +1 -1
- package/dist/assets/marketplace-page-BrCLRIc4.js +105 -0
- package/dist/assets/marketplace-page-odDpPYEs.js +1 -0
- package/dist/assets/mcp-marketplace-page-CfbOBgKK.js +1 -0
- package/dist/assets/mcp-marketplace-page-DIq_SpMe.js +40 -0
- package/dist/assets/model-config-Bc6VVnxy.js +1 -0
- package/dist/assets/{notice-card-D1RNsTn_.js → notice-card-Dr6xCwva.js} +1 -1
- package/dist/assets/play-AqrNslHI.js +1 -0
- package/dist/assets/plus-B-YHtTNC.js +1 -0
- package/dist/assets/{popover-BMyiifTA.js → popover-BDFNiLlg.js} +1 -1
- package/dist/assets/provider-scoped-model-input-BMTp4BEH.js +1 -0
- package/dist/assets/providers-list-DN0tvISH.js +1 -0
- package/dist/assets/refresh-cw-CrbD8EkT.js +1 -0
- package/dist/assets/remote-Dr3jcfWP.js +1 -0
- package/dist/assets/{rotate-cw-BZ2JObNs.js → rotate-cw-BN9yjccP.js} +1 -1
- package/dist/assets/runtime-config-page-CRWOwBbl.js +1 -0
- package/dist/assets/{save-euRxl8pI.js → save-CO_4qf6b.js} +1 -1
- package/dist/assets/{search-CLd7m0M7.js → search-CRtQwr-h.js} +1 -1
- package/dist/assets/search-config-C4c1yZSP.js +1 -0
- package/dist/assets/secrets-config-zAF30YfO.js +3 -0
- package/dist/assets/{select-DTdzR8j8.js → select-BUTwE_lC.js} +1 -1
- package/dist/assets/{setting-row-CvKngoNI.js → setting-row-BavcnXw1.js} +1 -1
- package/dist/assets/settings-MWL2SMyk.js +1 -0
- package/dist/assets/{sparkles-DVfeSVJQ.js → sparkles-BmgOD4nY.js} +1 -1
- package/dist/assets/{status-dot-ChvPCib9.js → status-dot-l3kPFdq_.js} +1 -1
- package/dist/assets/{tabs-custom-Hia_ong0.js → tabs-custom-D48zdZoc.js} +1 -1
- package/dist/assets/{tag-chip-BywQeHJj.js → tag-chip-Dm2Lqnpu.js} +1 -1
- package/dist/assets/use-config-Cyv5IuSt.js +1 -0
- package/dist/assets/use-infinite-scroll-loader-Cvz8ZteY.js +1 -0
- package/dist/assets/x-BeyYA_h6.js +1 -0
- package/dist/index.html +29 -40
- package/package.json +9 -9
- package/src/app/components/layout/sidebar.layout.test.tsx +2 -4
- package/src/app/components/theme-provider.tsx +1 -0
- package/src/app/configs/app-navigation.config.ts +0 -6
- package/src/app/index.tsx +4 -7
- package/src/features/agents/components/agents-page.test.tsx +25 -15
- package/src/features/agents/components/agents-page.tsx +133 -172
- package/src/features/channels/components/config/channel-form.test.tsx +1 -0
- package/src/features/channels/components/config/channel-form.tsx +4 -3
- package/src/features/channels/components/config/weixin-channel-auth-section.test.tsx +38 -1
- package/src/features/channels/components/config/weixin-channel-auth-section.tsx +137 -40
- package/src/features/channels/index.ts +1 -1
- package/src/features/channels/utils/channel-form-fields.utils.test.ts +26 -0
- package/src/features/channels/utils/channel-form-fields.utils.ts +32 -18
- package/src/features/chat/components/chat-session-workspace-panel-nav.tsx +23 -4
- package/src/features/chat/components/chat-session-workspace-panel.tsx +34 -2
- package/src/features/chat/components/chat-sidebar-session-item.tsx +9 -3
- package/src/features/chat/components/conversation/chat-conversation-header.test.tsx +71 -0
- package/src/features/chat/components/conversation/chat-conversation-header.tsx +6 -0
- package/src/features/chat/components/conversation/chat-conversation-panel.test.tsx +181 -61
- package/src/features/chat/components/conversation/chat-conversation-panel.tsx +56 -25
- package/src/features/chat/components/conversation/session-header/chat-session-header-actions.test.tsx +24 -0
- package/src/features/chat/components/conversation/session-header/chat-session-header-actions.tsx +26 -5
- package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +174 -0
- package/src/features/chat/components/layout/chat-sidebar.test.tsx +119 -8
- package/src/features/chat/components/layout/chat-sidebar.tsx +57 -75
- package/src/features/chat/components/providers/chat-presenter.provider.tsx +2 -0
- package/src/features/chat/components/workspace/session-cron-job-content.tsx +103 -0
- package/src/features/chat/hooks/use-hydrated-ncp-agent.test.tsx +6 -0
- package/src/features/chat/hooks/use-ncp-agent-runtime.test.tsx +172 -69
- package/src/features/chat/hooks/use-ncp-chat-derived-state.ts +2 -2
- package/src/features/chat/hooks/use-ncp-chat-page-data.test.tsx +70 -0
- package/src/features/chat/hooks/use-ncp-chat-page-data.ts +7 -7
- package/src/features/chat/hooks/use-ncp-child-session-tabs-view.ts +2 -8
- package/src/features/chat/hooks/use-ncp-session-conversation.test.tsx +10 -0
- package/src/features/chat/hooks/use-ncp-session-conversation.ts +2 -1
- package/src/features/chat/hooks/use-ncp-session-list-view.ts +1 -2
- package/src/features/chat/hooks/use-selected-session-context-window-indicator.ts +2 -4
- package/src/features/chat/managers/chat-session-list.manager.test.ts +21 -20
- package/src/features/chat/managers/chat-session-list.manager.ts +15 -24
- package/src/features/chat/managers/ncp-chat-input.manager.test.ts +22 -13
- package/src/features/chat/managers/ncp-chat-input.manager.ts +4 -2
- package/src/features/chat/managers/ncp-chat-presenter.manager.ts +6 -0
- package/src/features/chat/managers/ncp-chat-thread.manager.test.ts +52 -1
- package/src/features/chat/managers/ncp-chat-thread.manager.ts +21 -0
- package/src/features/chat/pages/ncp-chat-page.tsx +28 -17
- package/src/features/chat/stores/chat-session-list.store.ts +0 -3
- package/src/features/chat/stores/chat-thread.store.ts +4 -0
- package/src/features/chat/types/chat-stream.types.ts +1 -1
- package/src/features/chat/utils/chat-session-display.utils.test.ts +83 -1
- package/src/features/chat/utils/chat-session-display.utils.ts +73 -0
- package/src/features/chat/utils/ncp-session-adapter.utils.test.ts +22 -0
- package/src/features/chat/utils/ncp-session-adapter.utils.ts +33 -1
- package/src/features/marketplace/components/curated-shelves/marketplace-curated-scene-route.test.tsx +235 -0
- package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.config.ts +162 -0
- package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.tsx +355 -0
- package/src/features/marketplace/components/curated-shelves/marketplace-shelf-card.tsx +118 -0
- package/src/features/marketplace/components/detail-doc/marketplace-detail-doc-renderer.ts +201 -0
- package/src/features/marketplace/components/detail-doc/marketplace-detail-doc.test.ts +40 -0
- package/src/features/marketplace/components/marketplace-catalog-grid.tsx +114 -0
- package/src/features/marketplace/components/marketplace-detail-doc.ts +73 -24
- package/src/features/marketplace/components/marketplace-item-icon.tsx +45 -0
- package/src/features/marketplace/components/marketplace-list-card.tsx +177 -93
- package/src/features/marketplace/components/marketplace-page-detail.test.tsx +9 -2
- package/src/features/marketplace/components/marketplace-page-parts.tsx +1 -1
- package/src/features/marketplace/components/marketplace-page.test.tsx +25 -6
- package/src/features/marketplace/components/marketplace-page.tsx +154 -132
- package/src/features/marketplace/hooks/use-marketplace-curated-scene-route.ts +97 -0
- package/src/features/marketplace/hooks/use-marketplace.ts +59 -3
- package/src/features/system-status/components/config/runtime-agent-list-card.tsx +4 -8
- package/src/features/system-status/components/config/runtime-binding-list-card.tsx +5 -7
- package/src/features/system-status/components/config/runtime-config-editor.tsx +1 -19
- package/src/features/system-status/components/config/runtime-entry-list-card.tsx +10 -11
- package/src/features/system-status/components/config/runtime-settings-card.tsx +15 -23
- package/src/features/system-status/components/runtime-control-card.test.tsx +8 -6
- package/src/features/system-status/components/runtime-control-card.tsx +7 -6
- package/src/features/system-status/pages/runtime-config-page.test.tsx +19 -9
- package/src/features/system-status/pages/runtime-config-page.tsx +2 -3
- package/src/features/system-status/utils/runtime-config-agent.utils.ts +4 -4
- package/src/features/system-status/utils/system-status.utils.ts +31 -6
- package/src/index.css +8 -0
- package/src/platforms/desktop/components/desktop-app-shell.test.tsx +67 -0
- package/src/platforms/desktop/components/desktop-app-shell.tsx +46 -18
- package/src/platforms/desktop/components/desktop-window-chrome.tsx +30 -0
- package/src/platforms/desktop/index.ts +6 -0
- package/src/platforms/desktop/types/desktop-update.types.ts +3 -0
- package/src/platforms/desktop/utils/desktop-host.utils.ts +56 -0
- package/src/shared/components/common/brand-header.tsx +36 -16
- package/src/shared/components/config/provider-form-support.ts +2 -22
- package/src/shared/components/cron-config.tsx +12 -58
- package/src/shared/components/doc-browser/doc-browser.tsx +4 -4
- package/src/shared/components/ui/select.tsx +19 -7
- package/src/shared/lib/api/channel-auth.types.ts +1 -0
- package/src/shared/lib/api/ncp-session-query-cache.test.ts +26 -1
- package/src/shared/lib/api/ncp-session-query-cache.ts +5 -1
- package/src/shared/lib/api/ncp-session.types.ts +9 -0
- package/src/shared/lib/api/types.ts +12 -1
- package/src/shared/lib/api/utils/marketplace.utils.ts +7 -1
- package/src/shared/lib/cron/cron-job-view.utils.ts +59 -0
- package/src/shared/lib/cron/index.ts +1 -0
- package/src/shared/lib/i18n/{channel-auth.ts → channel-auth.constants.ts} +31 -0
- package/src/shared/lib/i18n/chat-labels.utils.ts +3 -2
- package/src/shared/lib/i18n/index.ts +20 -59
- package/src/shared/lib/i18n/{runtime-control.ts → runtime-control-labels.utils.ts} +30 -1
- package/src/shared/lib/provider-models/index.test.ts +39 -0
- package/src/shared/lib/provider-models/index.ts +1 -3
- package/src/shared/lib/ui-document-title/index.ts +0 -1
- package/tsconfig.json +1 -0
- package/vite.config.ts +1 -1
- package/vitest.config.ts +1 -1
- package/dist/assets/api-BGd3rgv_.js +0 -15
- package/dist/assets/app-manager-provider-BuJ_U9eC.js +0 -1
- package/dist/assets/app-navigation.config-BTdUuqXS.js +0 -1
- package/dist/assets/channels-list-page-BrwymXPe.js +0 -8
- package/dist/assets/chat-DGM6K3Qs.js +0 -61
- package/dist/assets/chat-page-DpmXMWNS.js +0 -1
- package/dist/assets/chunk-JZWAC4HX-Kydj4yEz.js +0 -3
- package/dist/assets/config-split-page-DIOCjj2Q.js +0 -1
- package/dist/assets/desktop-update-config-BGKiqc6q.js +0 -1
- package/dist/assets/doc-browser-C8FM5fC0.js +0 -1
- package/dist/assets/doc-browser-RJUOL_GO.js +0 -1
- package/dist/assets/doc-browser-p82AdNO-.js +0 -1
- package/dist/assets/folder-CeJKPx5P.js +0 -1
- package/dist/assets/hash-BqxRTZW5.js +0 -1
- package/dist/assets/i18n-DnTGDIRw.js +0 -1
- package/dist/assets/index-BrEdR78s.js +0 -2
- package/dist/assets/index-D8MKmXtO.css +0 -1
- package/dist/assets/loader-circle-fd-vQKtW.js +0 -1
- package/dist/assets/logo-badge-KAe-7d8c.js +0 -1
- package/dist/assets/logos-C4sYP1Vl.js +0 -1
- package/dist/assets/marketplace-page-B2Pm2RDJ.js +0 -1
- package/dist/assets/marketplace-page-CPHxlYL8.js +0 -49
- package/dist/assets/mcp-marketplace-page-BcjVmw36.js +0 -1
- package/dist/assets/mcp-marketplace-page-CswPXSjf.js +0 -40
- package/dist/assets/message-square-z_osm9c0.js +0 -1
- package/dist/assets/model-config-Cmruiqdx.js +0 -1
- package/dist/assets/play-Dv6Nr1Ew.js +0 -1
- package/dist/assets/plus-D8eKFY7h.js +0 -1
- package/dist/assets/provider-scoped-model-input-D7ACiMAO.js +0 -1
- package/dist/assets/providers-list-gg7LrfuB.js +0 -1
- package/dist/assets/refresh-ccw-ByVwmnN_.js +0 -1
- package/dist/assets/refresh-cw-PcqoYB3K.js +0 -1
- package/dist/assets/remote-Db2M39Cv.js +0 -1
- package/dist/assets/runtime-config-page-BT_VV41p.js +0 -1
- package/dist/assets/search-config-0VTPpz-w.js +0 -1
- package/dist/assets/secrets-config-DwQbLLEy.js +0 -3
- package/dist/assets/sessions-config-page-CAG7Zevv.js +0 -2
- package/dist/assets/settings-drbWqzA4.js +0 -1
- package/dist/assets/skeleton-BK1SOSRA.js +0 -1
- package/dist/assets/theme-provider-COAwWFv8.js +0 -2
- package/dist/assets/tooltip-BOYp8Ue7.js +0 -1
- package/dist/assets/trash-2-CBsHCfqq.js +0 -1
- package/dist/assets/use-config-DTwhNDQE.js +0 -1
- package/dist/assets/use-confirm-dialog-oeSqhmrx.js +0 -1
- package/dist/assets/use-infinite-scroll-loader-X3KGuME8.js +0 -1
- package/dist/assets/use-viewport-layout-C0NJAVXs.js +0 -1
- package/dist/assets/x-CM-XDMpk.js +0 -1
- package/src/features/chat/components/config/sessions-config-detail-pane.tsx +0 -244
- package/src/features/chat/pages/sessions-config-page.test.tsx +0 -152
- package/src/features/chat/pages/sessions-config-page.tsx +0 -192
- /package/dist/assets/{config-hints-MogHYQ8G.js → config-hints-BNfpOL4J.js} +0 -0
|
@@ -2,7 +2,6 @@ import { useEffect, useMemo, useState } from 'react';
|
|
|
2
2
|
import type { SessionEntryView } from '@/shared/lib/api';
|
|
3
3
|
import { BrandHeader } from '@/shared/components/common/brand-header';
|
|
4
4
|
import { StatusBadge } from '@/shared/components/common/status-badge';
|
|
5
|
-
import { SelectItem } from '@/shared/components/ui/select';
|
|
6
5
|
import { ChatSidebarListModeSwitch, ChatSidebarProjectGroups, type ChatSidebarProjectGroup } from '@/features/chat';
|
|
7
6
|
import { useChatSidebarSessionLabelEditor } from '@/features/chat/hooks/use-chat-sidebar-session-label-editor';
|
|
8
7
|
import { useNcpSessionListView, type NcpSessionListItemView } from '@/features/chat/hooks/use-ncp-session-list-view';
|
|
@@ -14,38 +13,36 @@ import { useAgents } from '@/shared/hooks/use-agents';
|
|
|
14
13
|
import { getSessionProjectName } from '@/shared/lib/session-project';
|
|
15
14
|
import { cn } from '@/shared/lib/utils';
|
|
16
15
|
import { LANGUAGE_OPTIONS, t, type I18nLanguage } from '@/shared/lib/i18n';
|
|
17
|
-
import { THEME_OPTIONS
|
|
16
|
+
import { THEME_OPTIONS } from '@/shared/lib/theme';
|
|
18
17
|
import { useI18n } from '@/app/components/i18n-provider';
|
|
19
18
|
import { useTheme } from '@/app/components/theme-provider';
|
|
20
19
|
import { useDocBrowser } from '@/shared/components/doc-browser';
|
|
21
|
-
import {
|
|
20
|
+
import { SidebarNavLinkItem } from '@/app/components/layout/sidebar-items';
|
|
22
21
|
import {
|
|
23
22
|
AlarmClock,
|
|
24
23
|
Bot,
|
|
25
|
-
BookOpen,
|
|
26
24
|
BrainCircuit,
|
|
27
|
-
Languages,
|
|
28
25
|
MessageSquareText,
|
|
29
|
-
Palette,
|
|
30
|
-
Settings
|
|
31
26
|
} from 'lucide-react';
|
|
32
27
|
import { ChatSidebarSessionEntry } from '@/features/chat/components/layout/chat-sidebar-session-entry';
|
|
33
28
|
import {
|
|
34
29
|
ChatSidebarDesktopToolbar,
|
|
35
30
|
ChatSidebarMobileToolbar,
|
|
36
31
|
} from '@/features/chat/components/layout/chat-sidebar-toolbar';
|
|
32
|
+
import { ChatSidebarUtilityMenu } from '@/features/chat/components/layout/chat-sidebar-utility-menu';
|
|
33
|
+
import { isMacDesktopHost, isWindowsDesktopHost } from '@/platforms/desktop';
|
|
37
34
|
|
|
38
35
|
type DateGroup = {
|
|
39
36
|
label: string;
|
|
40
37
|
items: NcpSessionListItemView[];
|
|
41
38
|
};
|
|
42
39
|
|
|
43
|
-
function
|
|
44
|
-
return new Date(item.session.
|
|
40
|
+
function getSessionActivityAtTimestamp(item: NcpSessionListItemView): number {
|
|
41
|
+
return new Date(item.session.lastMessageAt ?? item.session.createdAt).getTime();
|
|
45
42
|
}
|
|
46
43
|
|
|
47
|
-
function
|
|
48
|
-
return [...items].sort((left, right) =>
|
|
44
|
+
function sortSessionItemsByActivityAtDesc(items: NcpSessionListItemView[]): NcpSessionListItemView[] {
|
|
45
|
+
return [...items].sort((left, right) => getSessionActivityAtTimestamp(right) - getSessionActivityAtTimestamp(left));
|
|
49
46
|
}
|
|
50
47
|
|
|
51
48
|
function groupSessionsByDate(items: NcpSessionListItemView[]): DateGroup[] {
|
|
@@ -60,8 +57,7 @@ function groupSessionsByDate(items: NcpSessionListItemView[]): DateGroup[] {
|
|
|
60
57
|
const older: NcpSessionListItemView[] = [];
|
|
61
58
|
|
|
62
59
|
for (const item of items) {
|
|
63
|
-
const
|
|
64
|
-
const ts = new Date(session.updatedAt).getTime();
|
|
60
|
+
const ts = getSessionActivityAtTimestamp(item);
|
|
65
61
|
if (ts >= todayStart) {
|
|
66
62
|
today.push(item);
|
|
67
63
|
} else if (ts >= yesterdayStart) {
|
|
@@ -90,7 +86,7 @@ function groupSessionsByProject(items: NcpSessionListItemView[]): ChatSidebarPro
|
|
|
90
86
|
continue;
|
|
91
87
|
}
|
|
92
88
|
const existingGroup = grouped.get(projectRoot);
|
|
93
|
-
const updatedAt =
|
|
89
|
+
const updatedAt = getSessionActivityAtTimestamp(item);
|
|
94
90
|
if (existingGroup) {
|
|
95
91
|
existingGroup.items.push(item);
|
|
96
92
|
existingGroup.latestUpdatedAt = Math.max(existingGroup.latestUpdatedAt, updatedAt);
|
|
@@ -107,11 +103,28 @@ function groupSessionsByProject(items: NcpSessionListItemView[]): ChatSidebarPro
|
|
|
107
103
|
return [...grouped.values()]
|
|
108
104
|
.map((group) => ({
|
|
109
105
|
...group,
|
|
110
|
-
items:
|
|
106
|
+
items: sortSessionItemsByActivityAtDesc(group.items)
|
|
111
107
|
}))
|
|
112
108
|
.sort((left, right) => right.latestUpdatedAt - left.latestUpdatedAt);
|
|
113
109
|
}
|
|
114
110
|
|
|
111
|
+
function groupChildSessionsByParentKey(items: NcpSessionListItemView[]): Map<string, NcpSessionListItemView[]> {
|
|
112
|
+
const grouped = new Map<string, NcpSessionListItemView[]>();
|
|
113
|
+
for (const item of items) {
|
|
114
|
+
const parentSessionKey = item.session.parentSessionId?.trim();
|
|
115
|
+
if (!parentSessionKey) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const bucket = grouped.get(parentSessionKey) ?? [];
|
|
119
|
+
bucket.push(item);
|
|
120
|
+
grouped.set(parentSessionKey, bucket);
|
|
121
|
+
}
|
|
122
|
+
for (const bucket of grouped.values()) {
|
|
123
|
+
bucket.sort((left, right) => getSessionActivityAtTimestamp(right) - getSessionActivityAtTimestamp(left));
|
|
124
|
+
}
|
|
125
|
+
return grouped;
|
|
126
|
+
}
|
|
127
|
+
|
|
115
128
|
function sessionTitle(session: SessionEntryView): string {
|
|
116
129
|
if (session.label && session.label.trim()) {
|
|
117
130
|
return session.label.trim();
|
|
@@ -170,6 +183,7 @@ export function ChatSidebar({
|
|
|
170
183
|
const presenter = usePresenter();
|
|
171
184
|
const docBrowser = useDocBrowser();
|
|
172
185
|
const [isCreateMenuOpen, setIsCreateMenuOpen] = useState(false);
|
|
186
|
+
const [isUtilityMenuOpen, setIsUtilityMenuOpen] = useState(false);
|
|
173
187
|
const inputSnapshot = useChatInputStore((state) => state.snapshot);
|
|
174
188
|
const listSnapshot = useChatSessionListStore((state) => state.snapshot);
|
|
175
189
|
const systemStatus = useSystemStatus();
|
|
@@ -179,27 +193,20 @@ export function ChatSidebar({
|
|
|
179
193
|
const { theme, setTheme } = useTheme();
|
|
180
194
|
const currentThemeLabel = t(THEME_OPTIONS.find((o) => o.value === theme)?.labelKey ?? 'themeWarm');
|
|
181
195
|
const currentLanguageLabel = LANGUAGE_OPTIONS.find((o) => o.value === language)?.label ?? language;
|
|
196
|
+
const utilityThemeOptions = useMemo(
|
|
197
|
+
() => THEME_OPTIONS.map((option) => ({ value: option.value, label: t(option.labelKey) })),
|
|
198
|
+
[],
|
|
199
|
+
);
|
|
200
|
+
const utilityLanguageOptions = useMemo(
|
|
201
|
+
() => LANGUAGE_OPTIONS.map((option) => ({ value: option.value, label: option.label })),
|
|
202
|
+
[],
|
|
203
|
+
);
|
|
182
204
|
const agentsById = useMemo(
|
|
183
205
|
() => new Map((agentsQuery.data?.agents ?? []).map((agent) => [agent.id, agent])),
|
|
184
206
|
[agentsQuery.data?.agents]
|
|
185
207
|
);
|
|
186
|
-
const sortedItems = useMemo(() =>
|
|
187
|
-
const childSessionsByParentKey = useMemo(() =>
|
|
188
|
-
const grouped = new Map<string, NcpSessionListItemView[]>();
|
|
189
|
-
for (const item of items) {
|
|
190
|
-
const parentSessionKey = item.session.parentSessionId?.trim();
|
|
191
|
-
if (!parentSessionKey) {
|
|
192
|
-
continue;
|
|
193
|
-
}
|
|
194
|
-
const bucket = grouped.get(parentSessionKey) ?? [];
|
|
195
|
-
bucket.push(item);
|
|
196
|
-
grouped.set(parentSessionKey, bucket);
|
|
197
|
-
}
|
|
198
|
-
for (const bucket of grouped.values()) {
|
|
199
|
-
bucket.sort((left, right) => getSessionUpdatedAtTimestamp(right) - getSessionUpdatedAtTimestamp(left));
|
|
200
|
-
}
|
|
201
|
-
return grouped;
|
|
202
|
-
}, [items]);
|
|
208
|
+
const sortedItems = useMemo(() => sortSessionItemsByActivityAtDesc(items), [items]);
|
|
209
|
+
const childSessionsByParentKey = useMemo(() => groupChildSessionsByParentKey(items), [items]);
|
|
203
210
|
const groups = useMemo(() => groupSessionsByDate(sortedItems), [sortedItems]);
|
|
204
211
|
const projectGroups = useMemo(() => groupSessionsByProject(sortedItems), [sortedItems]);
|
|
205
212
|
const defaultSessionType = inputSnapshot.defaultSessionType || 'native';
|
|
@@ -248,10 +255,8 @@ export function ChatSidebar({
|
|
|
248
255
|
/>
|
|
249
256
|
);
|
|
250
257
|
const createSessionAndOpenIfNeeded = (sessionType: string, projectRoot?: string | null) => {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
: presenter.chatSessionListManager.createSession(sessionType);
|
|
254
|
-
if (isMobileVariant) presenter.chatUiManager.goToSession(sessionKey);
|
|
258
|
+
presenter.chatSessionListManager.createSession(sessionType, typeof projectRoot === "string" ? projectRoot : undefined);
|
|
259
|
+
if (isMobileVariant) presenter.chatUiManager.goToChatRoot();
|
|
255
260
|
};
|
|
256
261
|
|
|
257
262
|
return (
|
|
@@ -263,10 +268,10 @@ export function ChatSidebar({
|
|
|
263
268
|
: 'w-[280px] shrink-0 border-r border-gray-200/60',
|
|
264
269
|
)}
|
|
265
270
|
>
|
|
266
|
-
{!isMobileVariant ? (
|
|
267
|
-
<div className=
|
|
271
|
+
{!isMobileVariant && !isWindowsDesktopHost() ? (
|
|
272
|
+
<div className={cn('px-5 pb-3', isMacDesktopHost() ? 'pt-1.5' : 'pt-5')}>
|
|
268
273
|
<BrandHeader
|
|
269
|
-
className="flex items-center gap-2
|
|
274
|
+
className="flex min-w-0 items-center gap-2"
|
|
270
275
|
suffix={<StatusBadge status={systemStatus.connectionStatus} />}
|
|
271
276
|
/>
|
|
272
277
|
</div>
|
|
@@ -362,43 +367,20 @@ export function ChatSidebar({
|
|
|
362
367
|
</div>
|
|
363
368
|
|
|
364
369
|
{!isMobileVariant ? (
|
|
365
|
-
<div className="px-3 py-3 border-t border-gray-200/60
|
|
366
|
-
<
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
370
|
+
<div className="px-3 py-3 border-t border-gray-200/60">
|
|
371
|
+
<ChatSidebarUtilityMenu
|
|
372
|
+
isOpen={isUtilityMenuOpen}
|
|
373
|
+
onOpenChange={setIsUtilityMenuOpen}
|
|
374
|
+
currentTheme={theme}
|
|
375
|
+
currentThemeLabel={currentThemeLabel}
|
|
376
|
+
themeOptions={utilityThemeOptions}
|
|
377
|
+
onSelectTheme={setTheme}
|
|
378
|
+
currentLanguage={language}
|
|
379
|
+
currentLanguageLabel={currentLanguageLabel}
|
|
380
|
+
languageOptions={utilityLanguageOptions}
|
|
381
|
+
onSelectLanguage={handleLanguageSwitch}
|
|
382
|
+
onOpenDocs={() => docBrowser.open(undefined, { kind: 'docs', newTab: true, title: 'Docs' })}
|
|
377
383
|
/>
|
|
378
|
-
<SidebarSelectItem
|
|
379
|
-
value={theme}
|
|
380
|
-
onValueChange={(value) => setTheme(value as UiTheme)}
|
|
381
|
-
icon={Palette}
|
|
382
|
-
label={t('theme')}
|
|
383
|
-
valueLabel={currentThemeLabel}
|
|
384
|
-
density="compact"
|
|
385
|
-
>
|
|
386
|
-
{THEME_OPTIONS.map((option) => (
|
|
387
|
-
<SelectItem key={option.value} value={option.value} className="text-xs">{t(option.labelKey)}</SelectItem>
|
|
388
|
-
))}
|
|
389
|
-
</SidebarSelectItem>
|
|
390
|
-
<SidebarSelectItem
|
|
391
|
-
value={language}
|
|
392
|
-
onValueChange={(value) => handleLanguageSwitch(value as I18nLanguage)}
|
|
393
|
-
icon={Languages}
|
|
394
|
-
label={t('language')}
|
|
395
|
-
valueLabel={currentLanguageLabel}
|
|
396
|
-
density="compact"
|
|
397
|
-
>
|
|
398
|
-
{LANGUAGE_OPTIONS.map((option) => (
|
|
399
|
-
<SelectItem key={option.value} value={option.value} className="text-xs">{option.label}</SelectItem>
|
|
400
|
-
))}
|
|
401
|
-
</SidebarSelectItem>
|
|
402
384
|
</div>
|
|
403
385
|
) : null}
|
|
404
386
|
</aside>
|
|
@@ -33,6 +33,7 @@ export type ChatThreadManagerLike = {
|
|
|
33
33
|
createSession: () => void;
|
|
34
34
|
goToProviders: () => void;
|
|
35
35
|
openChildSessionPanel: (params: { parentSessionKey: string; activeChildSessionKey?: string | null }) => void;
|
|
36
|
+
openSessionCronPanel: (sessionKey: string) => void;
|
|
36
37
|
openFilePreview: (action: ChatFileOpenActionViewModel) => void;
|
|
37
38
|
openSessionFromToolAction: (action: ChatToolActionViewModel) => void;
|
|
38
39
|
selectChildSessionDetail: (sessionKey: string) => void;
|
|
@@ -48,6 +49,7 @@ export type ChatPresenterLike = {
|
|
|
48
49
|
chatInputManager: ChatInputManagerLike;
|
|
49
50
|
chatSessionListManager: ChatSessionListManager;
|
|
50
51
|
chatThreadManager: ChatThreadManagerLike;
|
|
52
|
+
startAgentCreationDraft: (prompt: string) => void;
|
|
51
53
|
};
|
|
52
54
|
|
|
53
55
|
const ChatPresenterContext = createContext<ChatPresenterLike | null>(null);
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Trash2 } from "lucide-react";
|
|
2
|
+
import type { CronJobView } from "@/shared/lib/api";
|
|
3
|
+
import { Button } from "@/shared/components/ui/button";
|
|
4
|
+
import { useConfirmDialog } from "@/shared/hooks/use-confirm-dialog";
|
|
5
|
+
import { useDeleteCronJob } from "@/shared/hooks/use-config";
|
|
6
|
+
import {
|
|
7
|
+
describeCronDelivery,
|
|
8
|
+
describeCronSchedule,
|
|
9
|
+
formatCronDate,
|
|
10
|
+
} from "@/shared/lib/cron";
|
|
11
|
+
import { t } from "@/shared/lib/i18n";
|
|
12
|
+
|
|
13
|
+
export function SessionCronJobContent({ jobs }: { jobs: readonly CronJobView[] }) {
|
|
14
|
+
const deleteCronJob = useDeleteCronJob();
|
|
15
|
+
const { confirm, ConfirmDialog } = useConfirmDialog();
|
|
16
|
+
|
|
17
|
+
const handleDelete = async (job: CronJobView) => {
|
|
18
|
+
const confirmed = await confirm({
|
|
19
|
+
title: `${t("cronDeleteConfirm")}?`,
|
|
20
|
+
description: job.name ? `${job.name} (${job.id})` : job.id,
|
|
21
|
+
confirmLabel: t("delete"),
|
|
22
|
+
});
|
|
23
|
+
if (!confirmed) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
deleteCronJob.mutate({ id: job.id });
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="h-full overflow-y-auto custom-scrollbar px-4 py-4">
|
|
31
|
+
<div className="mb-4">
|
|
32
|
+
<div className="text-sm font-semibold text-gray-900">
|
|
33
|
+
{t("chatWorkspaceSessionCronJobs")}
|
|
34
|
+
</div>
|
|
35
|
+
<div className="mt-1 text-xs text-gray-500">
|
|
36
|
+
{t("cronTotalLabel")}: {jobs.length}
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
{jobs.length === 0 ? (
|
|
40
|
+
<div className="py-8 text-center text-sm text-gray-500">
|
|
41
|
+
{t("chatWorkspaceCronJobEmpty")}
|
|
42
|
+
</div>
|
|
43
|
+
) : (
|
|
44
|
+
<div className="space-y-3">
|
|
45
|
+
{jobs.map((job) => (
|
|
46
|
+
<div
|
|
47
|
+
key={job.id}
|
|
48
|
+
className="rounded-lg border border-gray-200 bg-white px-3 py-3"
|
|
49
|
+
>
|
|
50
|
+
<div className="flex items-start justify-between gap-3">
|
|
51
|
+
<div className="min-w-0">
|
|
52
|
+
<div className="truncate text-sm font-semibold text-gray-900">
|
|
53
|
+
{job.name || job.id}
|
|
54
|
+
</div>
|
|
55
|
+
<div className="mt-1 text-[11px] text-gray-400">
|
|
56
|
+
{job.id}
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
<Button
|
|
60
|
+
type="button"
|
|
61
|
+
variant="subtle"
|
|
62
|
+
size="sm"
|
|
63
|
+
className="h-7 gap-1 rounded-lg px-2"
|
|
64
|
+
onClick={() => void handleDelete(job)}
|
|
65
|
+
disabled={deleteCronJob.isPending}
|
|
66
|
+
>
|
|
67
|
+
<Trash2 className="h-3.5 w-3.5" />
|
|
68
|
+
{t("delete")}
|
|
69
|
+
</Button>
|
|
70
|
+
</div>
|
|
71
|
+
<div className="mt-3 space-y-1.5 text-xs text-gray-500">
|
|
72
|
+
<div>
|
|
73
|
+
<span className="font-medium text-gray-700">{t("cronScheduleLabel")}:</span>{" "}
|
|
74
|
+
{describeCronSchedule(job)}
|
|
75
|
+
</div>
|
|
76
|
+
<div>
|
|
77
|
+
<span className="font-medium text-gray-700">{t("cronNextRun")}:</span>{" "}
|
|
78
|
+
{formatCronDate(job.state.nextRunAt)}
|
|
79
|
+
</div>
|
|
80
|
+
<div>
|
|
81
|
+
<span className="font-medium text-gray-700">{t("cronLastRun")}:</span>{" "}
|
|
82
|
+
{formatCronDate(job.state.lastRunAt)}
|
|
83
|
+
</div>
|
|
84
|
+
<div>
|
|
85
|
+
<span className="font-medium text-gray-700">{t("cronLastStatus")}:</span>{" "}
|
|
86
|
+
{job.state.lastStatus ?? "-"}
|
|
87
|
+
</div>
|
|
88
|
+
<div>
|
|
89
|
+
<span className="font-medium text-gray-700">{t("cronDeliverTo")}:</span>{" "}
|
|
90
|
+
{describeCronDelivery(job)}
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
<div className="mt-3 whitespace-pre-wrap break-words text-sm text-gray-700">
|
|
94
|
+
{job.payload.message}
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
))}
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
<ConfirmDialog />
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -7,6 +7,11 @@ const mocks = vi.hoisted(() => ({
|
|
|
7
7
|
manager: {
|
|
8
8
|
reset: vi.fn(),
|
|
9
9
|
hydrate: vi.fn(),
|
|
10
|
+
getSnapshot: vi.fn(() => ({
|
|
11
|
+
messages: [],
|
|
12
|
+
streamingMessage: null,
|
|
13
|
+
activeRun: null,
|
|
14
|
+
})),
|
|
10
15
|
},
|
|
11
16
|
runtime: {
|
|
12
17
|
snapshot: {
|
|
@@ -37,6 +42,7 @@ describe("useHydratedNcpAgent", () => {
|
|
|
37
42
|
beforeEach(() => {
|
|
38
43
|
mocks.manager.reset.mockReset();
|
|
39
44
|
mocks.manager.hydrate.mockReset();
|
|
45
|
+
mocks.manager.getSnapshot.mockClear();
|
|
40
46
|
mocks.runtime.send.mockReset();
|
|
41
47
|
mocks.runtime.abort.mockReset();
|
|
42
48
|
mocks.runtime.streamRun.mockReset();
|
|
@@ -1,90 +1,193 @@
|
|
|
1
|
-
import { act, renderHook } from "@testing-library/react";
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
2
|
+
import {
|
|
3
|
+
type NcpAgentClientEndpoint,
|
|
4
|
+
type NcpAgentSendEnvelope,
|
|
5
|
+
type NcpEndpointEvent,
|
|
6
|
+
type NcpEndpointManifest,
|
|
7
|
+
type NcpEndpointSubscriber,
|
|
8
|
+
NcpEventType,
|
|
9
|
+
} from "@nextclaw/ncp";
|
|
10
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
11
|
+
import { DefaultNcpAgentConversationStateManager } from "../../../../../ncp-packages/nextclaw-ncp-toolkit/src/agent/agent-conversation-state-manager.ts";
|
|
4
12
|
import { useNcpAgentRuntime } from "../../../../../ncp-packages/nextclaw-ncp-react/src/hooks/use-ncp-agent-runtime.ts";
|
|
5
13
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
const now = "2026-05-14T00:00:00.000Z";
|
|
15
|
+
|
|
16
|
+
class DeferredSendClient implements NcpAgentClientEndpoint {
|
|
17
|
+
readonly manifest: NcpEndpointManifest = {
|
|
18
|
+
endpointKind: "agent",
|
|
19
|
+
endpointId: "deferred-send-client",
|
|
20
|
+
version: "0.1.0",
|
|
21
|
+
supportsStreaming: true,
|
|
22
|
+
supportsAbort: true,
|
|
23
|
+
supportsProactiveMessages: false,
|
|
24
|
+
supportsLiveSessionStream: true,
|
|
25
|
+
supportedPartTypes: ["text"],
|
|
26
|
+
expectedLatency: "seconds",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
readonly stop = vi.fn(async () => {});
|
|
30
|
+
readonly start = vi.fn(async () => {});
|
|
31
|
+
readonly stream = vi.fn(async () => {});
|
|
32
|
+
readonly abort = vi.fn(async () => {});
|
|
33
|
+
private listeners = new Set<NcpEndpointSubscriber>();
|
|
34
|
+
private releaseCompletion: (() => void) | null = null;
|
|
35
|
+
private completionGate = new Promise<void>((resolve) => {
|
|
36
|
+
this.releaseCompletion = resolve;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
emit = async (event: NcpEndpointEvent): Promise<void> => {
|
|
40
|
+
this.publish(event);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
subscribe = (listener: NcpEndpointSubscriber): (() => void) => {
|
|
44
|
+
this.listeners.add(listener);
|
|
45
|
+
return () => {
|
|
46
|
+
this.listeners.delete(listener);
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
send = vi.fn(async (_envelope: NcpAgentSendEnvelope): Promise<void> => {
|
|
51
|
+
this.publish({
|
|
52
|
+
type: NcpEventType.MessageSent,
|
|
53
|
+
payload: {
|
|
54
|
+
sessionId: "session-created",
|
|
55
|
+
message: {
|
|
56
|
+
id: "user-1",
|
|
57
|
+
sessionId: "session-created",
|
|
58
|
+
role: "user",
|
|
59
|
+
status: "final",
|
|
60
|
+
parts: [{ type: "text", text: "hello" }],
|
|
61
|
+
timestamp: now,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
this.publish({
|
|
66
|
+
type: NcpEventType.RunStarted,
|
|
67
|
+
payload: {
|
|
68
|
+
sessionId: "session-created",
|
|
69
|
+
messageId: "assistant-1",
|
|
70
|
+
runId: "run-1",
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
await this.completionGate;
|
|
74
|
+
this.publish({
|
|
75
|
+
type: NcpEventType.MessageTextStart,
|
|
76
|
+
payload: {
|
|
77
|
+
sessionId: "session-created",
|
|
78
|
+
messageId: "assistant-1",
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
this.publish({
|
|
82
|
+
type: NcpEventType.MessageTextDelta,
|
|
83
|
+
payload: {
|
|
84
|
+
sessionId: "session-created",
|
|
85
|
+
messageId: "assistant-1",
|
|
86
|
+
delta: "done",
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
this.publish({
|
|
90
|
+
type: NcpEventType.MessageTextEnd,
|
|
91
|
+
payload: {
|
|
92
|
+
sessionId: "session-created",
|
|
93
|
+
messageId: "assistant-1",
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
this.publish({
|
|
97
|
+
type: NcpEventType.MessageCompleted,
|
|
98
|
+
payload: {
|
|
99
|
+
sessionId: "session-created",
|
|
100
|
+
message: {
|
|
101
|
+
id: "assistant-1",
|
|
102
|
+
sessionId: "session-created",
|
|
103
|
+
role: "assistant",
|
|
104
|
+
status: "final",
|
|
105
|
+
parts: [{ type: "text", text: "done" }],
|
|
106
|
+
timestamp: now,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
this.publish({
|
|
111
|
+
type: NcpEventType.RunFinished,
|
|
112
|
+
payload: {
|
|
113
|
+
sessionId: "session-created",
|
|
114
|
+
runId: "run-1",
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
release = (): void => {
|
|
120
|
+
this.releaseCompletion?.();
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
private publish = (event: NcpEndpointEvent): void => {
|
|
124
|
+
for (const listener of this.listeners) {
|
|
125
|
+
listener(event);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
16
128
|
}
|
|
17
129
|
|
|
18
130
|
describe("useNcpAgentRuntime", () => {
|
|
19
131
|
beforeEach(() => {
|
|
20
|
-
vi.
|
|
132
|
+
vi.clearAllMocks();
|
|
21
133
|
});
|
|
22
134
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
};
|
|
35
|
-
const client = {
|
|
36
|
-
subscribe: vi.fn((callback: (event: NcpEndpointEvent) => void) => {
|
|
37
|
-
subscriber = callback;
|
|
38
|
-
return () => {
|
|
39
|
-
subscriber = null;
|
|
40
|
-
};
|
|
41
|
-
}),
|
|
42
|
-
stop: vi.fn().mockResolvedValue(undefined),
|
|
43
|
-
send: vi.fn().mockResolvedValue(undefined),
|
|
44
|
-
abort: vi.fn().mockResolvedValue(undefined),
|
|
45
|
-
stream: vi.fn().mockResolvedValue(undefined),
|
|
135
|
+
it("keeps the active send stream alive when a new root chat materializes a session id", async () => {
|
|
136
|
+
const client = new DeferredSendClient();
|
|
137
|
+
const manager = new DefaultNcpAgentConversationStateManager();
|
|
138
|
+
const envelope: NcpAgentSendEnvelope = {
|
|
139
|
+
message: {
|
|
140
|
+
id: "user-1",
|
|
141
|
+
role: "user",
|
|
142
|
+
status: "final",
|
|
143
|
+
parts: [{ type: "text", text: "hello" }],
|
|
144
|
+
timestamp: now,
|
|
145
|
+
},
|
|
46
146
|
};
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
dispatchBatch: vi.fn().mockResolvedValue(undefined),
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
renderHook(() =>
|
|
55
|
-
useNcpAgentRuntime({
|
|
56
|
-
sessionId: "session-1",
|
|
57
|
-
client: client as never,
|
|
58
|
-
manager: manager as never,
|
|
59
|
-
}),
|
|
147
|
+
const { result, rerender } = renderHook(
|
|
148
|
+
({ sessionId }: { sessionId?: string }) =>
|
|
149
|
+
useNcpAgentRuntime({ sessionId, client, manager: manager as never }),
|
|
150
|
+
{ initialProps: { sessionId: undefined as string | undefined } },
|
|
60
151
|
);
|
|
61
152
|
|
|
62
|
-
|
|
63
|
-
|
|
153
|
+
let sendPromise: Promise<void>;
|
|
64
154
|
act(() => {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
),
|
|
71
|
-
);
|
|
155
|
+
sendPromise = result.current.send(envelope);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
await waitFor(() => {
|
|
159
|
+
expect(result.current.snapshot.activeRun?.sessionId).toBe("session-created");
|
|
72
160
|
});
|
|
73
161
|
|
|
74
|
-
|
|
162
|
+
rerender({ sessionId: "session-created" });
|
|
163
|
+
|
|
164
|
+
expect(client.stop).not.toHaveBeenCalled();
|
|
75
165
|
|
|
76
166
|
await act(async () => {
|
|
77
|
-
|
|
78
|
-
await
|
|
167
|
+
client.release();
|
|
168
|
+
await sendPromise;
|
|
79
169
|
});
|
|
80
170
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
),
|
|
171
|
+
await waitFor(() => {
|
|
172
|
+
expect(result.current.snapshot.activeRun).toBeNull();
|
|
173
|
+
});
|
|
174
|
+
expect(result.current.visibleMessages.map((message) => message.id)).toEqual([
|
|
175
|
+
"user-1",
|
|
176
|
+
"assistant-1",
|
|
88
177
|
]);
|
|
89
178
|
});
|
|
179
|
+
|
|
180
|
+
it("aborts by session id even before a hydrated active run reaches local state", async () => {
|
|
181
|
+
const client = new DeferredSendClient();
|
|
182
|
+
const manager = new DefaultNcpAgentConversationStateManager();
|
|
183
|
+
const { result } = renderHook(() =>
|
|
184
|
+
useNcpAgentRuntime({ sessionId: "session-running", client, manager: manager as never }),
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
await act(async () => {
|
|
188
|
+
await result.current.abort();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
expect(client.abort).toHaveBeenCalledWith({ sessionId: "session-running" });
|
|
192
|
+
});
|
|
90
193
|
});
|
|
@@ -113,7 +113,7 @@ export function useNcpChatSnapshotSync(params: {
|
|
|
113
113
|
sessionTypeUnavailableMessage: string | null;
|
|
114
114
|
currentSessionTypeLabel: string;
|
|
115
115
|
currentSessionTypeIcon: ChatSessionTypeOption['icon'];
|
|
116
|
-
sessionKey: string;
|
|
116
|
+
sessionKey: string | null | undefined;
|
|
117
117
|
currentAgentId: string;
|
|
118
118
|
currentAgent: AgentProfileView | null;
|
|
119
119
|
availableAgents: AgentProfileView[];
|
|
@@ -152,7 +152,7 @@ export function useNcpChatSnapshotSync(params: {
|
|
|
152
152
|
sessionTypeUnavailableMessage: params.sessionTypeUnavailableMessage,
|
|
153
153
|
sessionTypeLabel: params.currentSessionTypeLabel,
|
|
154
154
|
sessionTypeIcon: params.currentSessionTypeIcon,
|
|
155
|
-
sessionKey: params.sessionKey,
|
|
155
|
+
sessionKey: params.sessionKey ?? null,
|
|
156
156
|
agentId: params.currentAgentId,
|
|
157
157
|
agentDisplayName: params.currentAgent?.displayName ?? null,
|
|
158
158
|
agentAvatarUrl: params.currentAgent?.avatarUrl ?? null,
|