@nextclaw/ui 0.12.24 → 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 +68 -29
- 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-C3D7Be0p.js → dialog-BKo0RItd.js} +1 -1
- package/dist/assets/{dist-CPlbUgwU.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-xqN1slyW.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-BFDbKQDA.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-B86Dbfhf.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-CJ0wbo3D.js → select-BUTwE_lC.js} +1 -1
- package/dist/assets/{setting-row-D1Yygqp7.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-FrkmkT8r.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 +54 -23
- 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 +45 -8
- package/src/features/chat/components/layout/chat-sidebar.tsx +29 -46
- 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-ncp-agent-runtime.test.tsx +14 -0
- 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 +1 -1
- package/src/features/chat/hooks/use-ncp-child-session-tabs-view.ts +2 -8
- package/src/features/chat/hooks/use-ncp-session-list-view.ts +1 -2
- package/src/features/chat/managers/chat-session-list.manager.test.ts +7 -9
- package/src/features/chat/managers/chat-session-list.manager.ts +5 -10
- package/src/features/chat/managers/ncp-chat-input.manager.test.ts +0 -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 +5 -4
- package/src/features/chat/stores/chat-session-list.store.ts +0 -2
- package/src/features/chat/stores/chat-thread.store.ts +4 -0
- 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 +32 -0
- 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.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-D2xRKmZd.js +0 -15
- package/dist/assets/app-manager-provider-CNaZboG4.js +0 -1
- package/dist/assets/app-navigation.config-Ihhrrt--.js +0 -1
- package/dist/assets/channels-list-page-p26lgxLk.js +0 -8
- package/dist/assets/chat-Dkh2qtuz.js +0 -61
- package/dist/assets/chat-page-DoTmE2wx.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-DlpzDfKM.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-D8MKmXtO.css +0 -1
- package/dist/assets/index-pBvbJ5Mt.js +0 -2
- 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-Cql0kDi-.js +0 -1
- package/dist/assets/marketplace-page-m4P5g_Ht.js +0 -49
- package/dist/assets/mcp-marketplace-page-9WVKl1m1.js +0 -1
- package/dist/assets/mcp-marketplace-page-ByzBQZcx.js +0 -40
- package/dist/assets/message-square-z_osm9c0.js +0 -1
- package/dist/assets/model-config-Dbr_0APb.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-DFm6N2f7.js +0 -1
- package/dist/assets/providers-list-BJcLOjun.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-BOxo9iwd.js +0 -1
- package/dist/assets/runtime-config-page-CjLhnbSl.js +0 -1
- package/dist/assets/search-config-J4Htco-P.js +0 -1
- package/dist/assets/secrets-config-CUdERjco.js +0 -3
- package/dist/assets/sessions-config-page-DpK991fs.js +0 -2
- package/dist/assets/settings-drbWqzA4.js +0 -1
- package/dist/assets/skeleton-BK1SOSRA.js +0 -1
- package/dist/assets/theme-provider-0hxjiPc_.js +0 -2
- package/dist/assets/tooltip-Cj4yA0gH.js +0 -1
- package/dist/assets/trash-2-CBsHCfqq.js +0 -1
- package/dist/assets/use-config-38Ur-89i.js +0 -1
- package/dist/assets/use-confirm-dialog-DPQThaeU.js +0 -1
- package/dist/assets/use-infinite-scroll-loader-5Gf1xQi7.js +0 -1
- package/dist/assets/use-viewport-layout-D1XzKeip.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
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { useNcpChatPageData } from './use-ncp-chat-page-data';
|
|
4
|
+
|
|
5
|
+
const useNcpSessionSkillsMock = vi.fn();
|
|
6
|
+
|
|
7
|
+
vi.mock('@/shared/hooks/use-config', () => ({
|
|
8
|
+
useConfig: () => ({
|
|
9
|
+
data: {
|
|
10
|
+
agents: { defaults: {} },
|
|
11
|
+
providers: {}
|
|
12
|
+
},
|
|
13
|
+
isFetched: true,
|
|
14
|
+
isSuccess: true
|
|
15
|
+
}),
|
|
16
|
+
useConfigMeta: () => ({
|
|
17
|
+
data: { providers: [] },
|
|
18
|
+
isFetched: true,
|
|
19
|
+
isSuccess: true
|
|
20
|
+
}),
|
|
21
|
+
useNcpSessions: () => ({
|
|
22
|
+
data: { sessions: [] }
|
|
23
|
+
}),
|
|
24
|
+
useNcpSessionSkills: (params: unknown) => useNcpSessionSkillsMock(params)
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
vi.mock('./use-ncp-chat-session-types', () => ({
|
|
28
|
+
useNcpChatSessionTypes: () => ({
|
|
29
|
+
data: {
|
|
30
|
+
defaultType: 'native',
|
|
31
|
+
options: [{ value: 'native', label: 'Native' }]
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
function renderPageData(params: { sessionKey: string | null }) {
|
|
37
|
+
return renderHook(() =>
|
|
38
|
+
useNcpChatPageData({
|
|
39
|
+
sessionKey: params.sessionKey,
|
|
40
|
+
query: '',
|
|
41
|
+
currentSelectedModel: '',
|
|
42
|
+
pendingSessionType: '',
|
|
43
|
+
setPendingSessionType: vi.fn(),
|
|
44
|
+
setSelectedModel: vi.fn(),
|
|
45
|
+
setSelectedThinkingLevel: vi.fn()
|
|
46
|
+
})
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('useNcpChatPageData skills query', () => {
|
|
51
|
+
it('loads draft-session skills before a new chat materializes', () => {
|
|
52
|
+
useNcpSessionSkillsMock.mockReturnValue({ data: { records: [] }, isLoading: false });
|
|
53
|
+
|
|
54
|
+
renderPageData({ sessionKey: null });
|
|
55
|
+
|
|
56
|
+
expect(useNcpSessionSkillsMock).toHaveBeenCalledWith({
|
|
57
|
+
sessionId: 'draft-session'
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('loads real session skills after materialization', () => {
|
|
62
|
+
useNcpSessionSkillsMock.mockReturnValue({ data: { records: [] }, isLoading: false });
|
|
63
|
+
|
|
64
|
+
renderPageData({ sessionKey: 'session-1' });
|
|
65
|
+
|
|
66
|
+
expect(useNcpSessionSkillsMock).toHaveBeenCalledWith({
|
|
67
|
+
sessionId: 'session-1'
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -158,7 +158,7 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
|
|
|
158
158
|
const sessionsQuery = useNcpSessions({ limit: 200 });
|
|
159
159
|
const sessionTypesQuery = useNcpChatSessionTypes();
|
|
160
160
|
const sessionSkillsQuery = useNcpSessionSkills({
|
|
161
|
-
sessionId: sessionKey
|
|
161
|
+
sessionId: sessionKey?.trim() || 'draft-session',
|
|
162
162
|
...(Object.prototype.hasOwnProperty.call(params, 'projectRootOverride')
|
|
163
163
|
? { projectRoot: projectRootOverride ?? null }
|
|
164
164
|
: {})
|
|
@@ -49,16 +49,10 @@ export function useNcpChildSessionTabsView(
|
|
|
49
49
|
return new Map(sessions.map((session) => [session.key, session]));
|
|
50
50
|
}, [summaries]);
|
|
51
51
|
|
|
52
|
-
const summaryByKey = useMemo(
|
|
53
|
-
() => new Map(summaries.map((summary) => [summary.sessionId, summary])),
|
|
54
|
-
[summaries],
|
|
55
|
-
);
|
|
56
|
-
|
|
57
52
|
return useMemo(
|
|
58
53
|
() =>
|
|
59
54
|
tabs.map((tab) => {
|
|
60
55
|
const session = sessionByKey.get(tab.sessionKey) ?? null;
|
|
61
|
-
const summary = summaryByKey.get(tab.sessionKey) ?? null;
|
|
62
56
|
const agentId = tab.agentId?.trim() || session?.agentId || null;
|
|
63
57
|
return {
|
|
64
58
|
sessionKey: tab.sessionKey,
|
|
@@ -68,7 +62,7 @@ export function useNcpChildSessionTabsView(
|
|
|
68
62
|
updatedAt: session?.updatedAt ?? null,
|
|
69
63
|
lastMessageAt: session?.lastMessageAt ?? null,
|
|
70
64
|
readAt: session?.readAt ?? null,
|
|
71
|
-
runStatus:
|
|
65
|
+
runStatus: session?.status === "running" ? "running" : undefined,
|
|
72
66
|
sessionTypeLabel: session?.sessionType
|
|
73
67
|
? resolveSessionTypeLabel(session.sessionType)
|
|
74
68
|
: null,
|
|
@@ -77,6 +71,6 @@ export function useNcpChildSessionTabsView(
|
|
|
77
71
|
projectRoot: session?.projectRoot?.trim() || null,
|
|
78
72
|
};
|
|
79
73
|
}),
|
|
80
|
-
[sessionByKey,
|
|
74
|
+
[sessionByKey, tabs],
|
|
81
75
|
);
|
|
82
76
|
}
|
|
@@ -32,11 +32,10 @@ export function useNcpSessionListView(params: { limit?: number } = {}) {
|
|
|
32
32
|
shouldShowSessionInSidebar,
|
|
33
33
|
);
|
|
34
34
|
const filteredSessions = filterSessionsByQuery(sessions, query);
|
|
35
|
-
const summaryBySessionId = new Map(summaries.map((summary) => [summary.sessionId, summary]));
|
|
36
35
|
|
|
37
36
|
return filteredSessions.map((session) => ({
|
|
38
37
|
session,
|
|
39
|
-
runStatus:
|
|
38
|
+
runStatus: session.status === 'running' ? 'running' : undefined
|
|
40
39
|
}));
|
|
41
40
|
}, [query, sessionsQuery.data?.sessions]);
|
|
42
41
|
|
|
@@ -35,7 +35,6 @@ describe('ChatSessionListManager', () => {
|
|
|
35
35
|
snapshot: {
|
|
36
36
|
...useChatSessionListStore.getState().snapshot,
|
|
37
37
|
selectedSessionKey: 'session-1',
|
|
38
|
-
draftSessionKey: 'draft-root-1',
|
|
39
38
|
listMode: 'time-first'
|
|
40
39
|
}
|
|
41
40
|
});
|
|
@@ -65,8 +64,8 @@ describe('ChatSessionListManager', () => {
|
|
|
65
64
|
expect(streamActionsManager.resetStreamState).toHaveBeenCalledTimes(1);
|
|
66
65
|
expect(uiManager.goToChatRoot).toHaveBeenCalledTimes(1);
|
|
67
66
|
expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
|
|
68
|
-
expect(useChatSessionListStore.getState().snapshot.draftSessionKey).toBeNull();
|
|
69
67
|
expect(useChatThreadStore.getState().snapshot.sessionKey).toBeNull();
|
|
68
|
+
expect(useChatThreadStore.getState().snapshot.hasSubmittedDraftMessage).toBe(false);
|
|
70
69
|
expect(useChatInputStore.getState().snapshot.pendingSessionType).toBe('codex');
|
|
71
70
|
expect(useChatInputStore.getState().snapshot.pendingProjectRoot).toBeNull();
|
|
72
71
|
expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).toBeNull();
|
|
@@ -89,8 +88,8 @@ describe('ChatSessionListManager', () => {
|
|
|
89
88
|
expect(uiManager.goToChatRoot).toHaveBeenCalledTimes(1);
|
|
90
89
|
expect(useChatSessionListStore.getState().snapshot.selectedAgentId).toBe('researcher');
|
|
91
90
|
expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
|
|
92
|
-
expect(useChatSessionListStore.getState().snapshot.draftSessionKey).toBeNull();
|
|
93
91
|
expect(useChatThreadStore.getState().snapshot.sessionKey).toBeNull();
|
|
92
|
+
expect(useChatThreadStore.getState().snapshot.hasSubmittedDraftMessage).toBe(false);
|
|
94
93
|
expect(useChatInputStore.getState().snapshot.pendingSessionType).toBe('codex');
|
|
95
94
|
expect(useChatInputStore.getState().snapshot.pendingProjectRoot).toBeNull();
|
|
96
95
|
expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).toBeNull();
|
|
@@ -117,8 +116,7 @@ describe('ChatSessionListManager', () => {
|
|
|
117
116
|
useChatSessionListStore.setState({
|
|
118
117
|
snapshot: {
|
|
119
118
|
...useChatSessionListStore.getState().snapshot,
|
|
120
|
-
selectedSessionKey: null
|
|
121
|
-
draftSessionKey: 'draft-root-2'
|
|
119
|
+
selectedSessionKey: null
|
|
122
120
|
}
|
|
123
121
|
});
|
|
124
122
|
const uiManager = {
|
|
@@ -137,6 +135,7 @@ describe('ChatSessionListManager', () => {
|
|
|
137
135
|
expect(uiManager.goToChatRoot).not.toHaveBeenCalled();
|
|
138
136
|
expect(uiManager.goToSession).not.toHaveBeenCalled();
|
|
139
137
|
expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
|
|
138
|
+
expect(useChatThreadStore.getState().snapshot.hasSubmittedDraftMessage).toBe(true);
|
|
140
139
|
});
|
|
141
140
|
|
|
142
141
|
it('does not eagerly replace the old selected session before the route finishes switching', () => {
|
|
@@ -225,12 +224,11 @@ describe('ChatSessionListManager', () => {
|
|
|
225
224
|
expect(mocks.updateNcpSession).not.toHaveBeenCalled();
|
|
226
225
|
});
|
|
227
226
|
|
|
228
|
-
it('routes to the backend-materialized root session
|
|
227
|
+
it('routes to the backend-materialized root session without duplicating route-owned selection state', () => {
|
|
229
228
|
useChatSessionListStore.setState({
|
|
230
229
|
snapshot: {
|
|
231
230
|
...useChatSessionListStore.getState().snapshot,
|
|
232
231
|
selectedSessionKey: null,
|
|
233
|
-
draftSessionKey: 'draft-root-2',
|
|
234
232
|
}
|
|
235
233
|
});
|
|
236
234
|
const uiManager = {
|
|
@@ -246,8 +244,8 @@ describe('ChatSessionListManager', () => {
|
|
|
246
244
|
manager.ensureDraftSession('native');
|
|
247
245
|
manager.materializeRootSessionRoute('ncp-materialized-session');
|
|
248
246
|
|
|
249
|
-
expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).
|
|
250
|
-
expect(useChatThreadStore.getState().snapshot.sessionKey).
|
|
247
|
+
expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
|
|
248
|
+
expect(useChatThreadStore.getState().snapshot.sessionKey).toBeNull();
|
|
251
249
|
expect(uiManager.goToSession).toHaveBeenCalledWith('ncp-materialized-session', { replace: true });
|
|
252
250
|
});
|
|
253
251
|
});
|
|
@@ -12,7 +12,7 @@ export class ChatSessionListManager {
|
|
|
12
12
|
private streamActionsManager: ChatStreamActionsManager
|
|
13
13
|
) {}
|
|
14
14
|
|
|
15
|
-
private syncDraftThreadState = () => {
|
|
15
|
+
private syncDraftThreadState = (hasSubmittedDraftMessage = false) => {
|
|
16
16
|
useChatThreadStore.getState().setSnapshot({
|
|
17
17
|
sessionKey: null,
|
|
18
18
|
sessionDisplayName: undefined,
|
|
@@ -21,9 +21,11 @@ export class ChatSessionListManager {
|
|
|
21
21
|
messages: [],
|
|
22
22
|
isSending: false,
|
|
23
23
|
isAwaitingAssistantOutput: false,
|
|
24
|
+
hasSubmittedDraftMessage,
|
|
24
25
|
parentSessionKey: null,
|
|
25
26
|
parentSessionLabel: null,
|
|
26
27
|
workspacePanelParentKey: null,
|
|
28
|
+
activeWorkspacePanelKind: null,
|
|
27
29
|
childSessionTabs: [],
|
|
28
30
|
activeChildSessionKey: null,
|
|
29
31
|
activeWorkspaceFileKey: null,
|
|
@@ -108,7 +110,6 @@ export class ChatSessionListManager {
|
|
|
108
110
|
this.streamActionsManager.resetStreamState();
|
|
109
111
|
useChatSessionListStore.getState().setSnapshot({
|
|
110
112
|
selectedSessionKey: null,
|
|
111
|
-
draftSessionKey: null
|
|
112
113
|
});
|
|
113
114
|
this.syncDraftThreadState();
|
|
114
115
|
useChatInputStore.getState().setSnapshot({
|
|
@@ -134,7 +135,7 @@ export class ChatSessionListManager {
|
|
|
134
135
|
typeof sessionType === 'string' && sessionType.trim().length > 0
|
|
135
136
|
? sessionType.trim()
|
|
136
137
|
: null;
|
|
137
|
-
this.syncDraftThreadState();
|
|
138
|
+
this.syncDraftThreadState(true);
|
|
138
139
|
if (normalizedSessionType) {
|
|
139
140
|
useChatInputStore.getState().setSnapshot({ pendingSessionType: normalizedSessionType });
|
|
140
141
|
}
|
|
@@ -149,19 +150,13 @@ export class ChatSessionListManager {
|
|
|
149
150
|
if (!this.uiManager.isAtChatRoot()) {
|
|
150
151
|
return;
|
|
151
152
|
}
|
|
152
|
-
useChatSessionListStore.getState().setSnapshot({
|
|
153
|
-
selectedSessionKey: normalizedSessionKey,
|
|
154
|
-
draftSessionKey: null,
|
|
155
|
-
});
|
|
156
|
-
useChatThreadStore.getState().setSnapshot({
|
|
157
|
-
sessionKey: normalizedSessionKey,
|
|
158
|
-
});
|
|
159
153
|
this.uiManager.goToSession(normalizedSessionKey, { replace: true });
|
|
160
154
|
};
|
|
161
155
|
|
|
162
156
|
selectSession = (sessionKey: string) => {
|
|
163
157
|
useChatThreadStore.getState().setSnapshot({
|
|
164
158
|
workspacePanelParentKey: null,
|
|
159
|
+
activeWorkspacePanelKind: null,
|
|
165
160
|
activeChildSessionKey: null,
|
|
166
161
|
activeWorkspaceFileKey: null,
|
|
167
162
|
});
|
|
@@ -58,7 +58,6 @@ describe('NcpChatInputManager', () => {
|
|
|
58
58
|
snapshot: {
|
|
59
59
|
...useChatSessionListStore.getState().snapshot,
|
|
60
60
|
selectedSessionKey: 'stale-selected-session',
|
|
61
|
-
draftSessionKey: 'draft-root-session',
|
|
62
61
|
selectedAgentId: 'main',
|
|
63
62
|
},
|
|
64
63
|
});
|
|
@@ -109,7 +108,6 @@ describe('NcpChatInputManager', () => {
|
|
|
109
108
|
snapshot: {
|
|
110
109
|
...useChatSessionListStore.getState().snapshot,
|
|
111
110
|
selectedSessionKey: null,
|
|
112
|
-
draftSessionKey: null,
|
|
113
111
|
},
|
|
114
112
|
});
|
|
115
113
|
const streamActionsManager = {
|
|
@@ -18,4 +18,10 @@ export class NcpChatPresenter {
|
|
|
18
18
|
this.chatSessionListManager,
|
|
19
19
|
this.chatStreamActionsManager
|
|
20
20
|
);
|
|
21
|
+
|
|
22
|
+
startAgentCreationDraft = (prompt: string) => {
|
|
23
|
+
this.chatSessionListManager.createSession();
|
|
24
|
+
this.chatSessionListManager.setSelectedAgentId('main');
|
|
25
|
+
this.chatInputManager.setDraft(prompt);
|
|
26
|
+
};
|
|
21
27
|
}
|
|
@@ -3,6 +3,7 @@ import { appQueryClient } from '@/app-query-client';
|
|
|
3
3
|
import { NcpChatThreadManager } from '@/features/chat/managers/ncp-chat-thread.manager';
|
|
4
4
|
import { useChatSessionListStore } from '@/features/chat/stores/chat-session-list.store';
|
|
5
5
|
import { useChatThreadStore } from '@/features/chat/stores/chat-thread.store';
|
|
6
|
+
import type * as SharedApi from '@/shared/lib/api';
|
|
6
7
|
|
|
7
8
|
const { deleteNcpSessionMock, deleteSummaryMock } = vi.hoisted(() => ({
|
|
8
9
|
deleteNcpSessionMock: vi.fn(async () => ({ deleted: true, sessionId: 'parent-session-1' })),
|
|
@@ -10,7 +11,7 @@ const { deleteNcpSessionMock, deleteSummaryMock } = vi.hoisted(() => ({
|
|
|
10
11
|
}));
|
|
11
12
|
|
|
12
13
|
vi.mock('@/shared/lib/api', async (importOriginal) => {
|
|
13
|
-
const actual = await importOriginal<typeof
|
|
14
|
+
const actual = await importOriginal<typeof SharedApi>();
|
|
14
15
|
return {
|
|
15
16
|
...actual,
|
|
16
17
|
deleteNcpSession: deleteNcpSessionMock,
|
|
@@ -97,6 +98,56 @@ describe('NcpChatThreadManager', () => {
|
|
|
97
98
|
expect(uiManager.goToSession).toHaveBeenCalledWith('parent-session-1');
|
|
98
99
|
});
|
|
99
100
|
|
|
101
|
+
it('opens the session cron panel without changing route when already selected', () => {
|
|
102
|
+
const uiManager = {
|
|
103
|
+
goToSession: vi.fn(),
|
|
104
|
+
goToChatRoot: vi.fn(),
|
|
105
|
+
goToProviders: vi.fn(),
|
|
106
|
+
confirm: vi.fn(),
|
|
107
|
+
} as unknown as ConstructorParameters<typeof NcpChatThreadManager>[0];
|
|
108
|
+
|
|
109
|
+
const manager = new NcpChatThreadManager(
|
|
110
|
+
uiManager,
|
|
111
|
+
{} as ConstructorParameters<typeof NcpChatThreadManager>[1],
|
|
112
|
+
{} as ConstructorParameters<typeof NcpChatThreadManager>[2],
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
manager.openSessionCronPanel('parent-session-1');
|
|
116
|
+
|
|
117
|
+
expect(useChatThreadStore.getState().snapshot).toMatchObject({
|
|
118
|
+
workspacePanelParentKey: 'parent-session-1',
|
|
119
|
+
activeWorkspacePanelKind: 'cron',
|
|
120
|
+
activeChildSessionKey: null,
|
|
121
|
+
activeWorkspaceFileKey: null,
|
|
122
|
+
});
|
|
123
|
+
expect(uiManager.goToSession).not.toHaveBeenCalled();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('routes to the session before opening its cron panel when needed', () => {
|
|
127
|
+
useChatSessionListStore.setState({
|
|
128
|
+
snapshot: {
|
|
129
|
+
...useChatSessionListStore.getState().snapshot,
|
|
130
|
+
selectedSessionKey: 'another-session',
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
const uiManager = {
|
|
134
|
+
goToSession: vi.fn(),
|
|
135
|
+
goToChatRoot: vi.fn(),
|
|
136
|
+
goToProviders: vi.fn(),
|
|
137
|
+
confirm: vi.fn(),
|
|
138
|
+
} as unknown as ConstructorParameters<typeof NcpChatThreadManager>[0];
|
|
139
|
+
|
|
140
|
+
const manager = new NcpChatThreadManager(
|
|
141
|
+
uiManager,
|
|
142
|
+
{} as ConstructorParameters<typeof NcpChatThreadManager>[1],
|
|
143
|
+
{} as ConstructorParameters<typeof NcpChatThreadManager>[2],
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
manager.openSessionCronPanel('parent-session-1');
|
|
147
|
+
|
|
148
|
+
expect(uiManager.goToSession).toHaveBeenCalledWith('parent-session-1');
|
|
149
|
+
});
|
|
150
|
+
|
|
100
151
|
it('keeps preview and diff for the same file as separate workspace tabs', () => {
|
|
101
152
|
const uiManager = {
|
|
102
153
|
goToSession: vi.fn(),
|
|
@@ -62,6 +62,7 @@ export class NcpChatThreadManager {
|
|
|
62
62
|
parentSessionKey: null,
|
|
63
63
|
parentSessionLabel: null,
|
|
64
64
|
workspacePanelParentKey: null,
|
|
65
|
+
activeWorkspacePanelKind: null,
|
|
65
66
|
childSessionTabs: [],
|
|
66
67
|
activeChildSessionKey: null,
|
|
67
68
|
workspaceFileTabs: [],
|
|
@@ -156,6 +157,7 @@ export class NcpChatThreadManager {
|
|
|
156
157
|
const activeChildSessionKey = params.activeChildSessionKey?.trim() || null;
|
|
157
158
|
useChatThreadStore.getState().setSnapshot({
|
|
158
159
|
workspacePanelParentKey: parentSessionKey,
|
|
160
|
+
activeWorkspacePanelKind: 'child-session',
|
|
159
161
|
activeChildSessionKey,
|
|
160
162
|
activeWorkspaceFileKey: null,
|
|
161
163
|
});
|
|
@@ -170,6 +172,7 @@ export class NcpChatThreadManager {
|
|
|
170
172
|
}
|
|
171
173
|
useChatThreadStore.getState().setSnapshot({
|
|
172
174
|
workspacePanelParentKey: parentSessionKey,
|
|
175
|
+
activeWorkspacePanelKind: 'file',
|
|
173
176
|
workspaceFileTabs: this.upsertWorkspaceFileTab(nextTab),
|
|
174
177
|
activeWorkspaceFileKey: nextTab.key,
|
|
175
178
|
activeChildSessionKey: null,
|
|
@@ -196,6 +199,7 @@ export class NcpChatThreadManager {
|
|
|
196
199
|
}
|
|
197
200
|
useChatThreadStore.getState().setSnapshot({
|
|
198
201
|
workspacePanelParentKey: null,
|
|
202
|
+
activeWorkspacePanelKind: null,
|
|
199
203
|
activeChildSessionKey: null,
|
|
200
204
|
activeWorkspaceFileKey: null,
|
|
201
205
|
});
|
|
@@ -214,6 +218,7 @@ export class NcpChatThreadManager {
|
|
|
214
218
|
useChatThreadStore.getState().setSnapshot({
|
|
215
219
|
activeChildSessionKey: normalizedSessionKey,
|
|
216
220
|
activeWorkspaceFileKey: null,
|
|
221
|
+
activeWorkspacePanelKind: 'child-session',
|
|
217
222
|
});
|
|
218
223
|
};
|
|
219
224
|
|
|
@@ -229,6 +234,7 @@ export class NcpChatThreadManager {
|
|
|
229
234
|
useChatThreadStore.getState().setSnapshot({
|
|
230
235
|
activeWorkspaceFileKey: normalizedFileKey,
|
|
231
236
|
activeChildSessionKey: null,
|
|
237
|
+
activeWorkspacePanelKind: 'file',
|
|
232
238
|
});
|
|
233
239
|
};
|
|
234
240
|
|
|
@@ -254,11 +260,26 @@ export class NcpChatThreadManager {
|
|
|
254
260
|
closeWorkspacePanel = () => {
|
|
255
261
|
useChatThreadStore.getState().setSnapshot({
|
|
256
262
|
workspacePanelParentKey: null,
|
|
263
|
+
activeWorkspacePanelKind: null,
|
|
257
264
|
activeChildSessionKey: null,
|
|
258
265
|
activeWorkspaceFileKey: null,
|
|
259
266
|
});
|
|
260
267
|
};
|
|
261
268
|
|
|
269
|
+
openSessionCronPanel = (sessionKey: string) => {
|
|
270
|
+
const parentSessionKey = sessionKey.trim();
|
|
271
|
+
if (!parentSessionKey) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
useChatThreadStore.getState().setSnapshot({
|
|
275
|
+
workspacePanelParentKey: parentSessionKey,
|
|
276
|
+
activeWorkspacePanelKind: 'cron',
|
|
277
|
+
activeChildSessionKey: null,
|
|
278
|
+
activeWorkspaceFileKey: null,
|
|
279
|
+
});
|
|
280
|
+
this.ensureWorkspaceParentRoute(parentSessionKey);
|
|
281
|
+
};
|
|
282
|
+
|
|
262
283
|
closeChildSessionDetail = () => {
|
|
263
284
|
this.closeWorkspacePanel();
|
|
264
285
|
};
|
|
@@ -219,15 +219,16 @@ function useNcpChatPageState(presenter: NcpChatPresenter) {
|
|
|
219
219
|
selectedSessionType,
|
|
220
220
|
sessionTypeOptions,
|
|
221
221
|
});
|
|
222
|
+
const currentSessionRunning = agent.isRunning || selectedSession?.status === "running";
|
|
222
223
|
return {
|
|
223
224
|
...baseState,
|
|
224
225
|
availableAgents,
|
|
225
226
|
effectiveSessionProjectRoot,
|
|
226
227
|
effectiveSessionProjectName,
|
|
227
|
-
isSending: agent.isSending ||
|
|
228
|
-
isAwaitingAssistantOutput:
|
|
229
|
-
canStopCurrentRun:
|
|
230
|
-
stopDisabledReason:
|
|
228
|
+
isSending: agent.isSending || currentSessionRunning,
|
|
229
|
+
isAwaitingAssistantOutput: currentSessionRunning,
|
|
230
|
+
canStopCurrentRun: currentSessionRunning,
|
|
231
|
+
stopDisabledReason: currentSessionRunning ? null : "__preparing__",
|
|
231
232
|
lastSendError:
|
|
232
233
|
isRuntimeBlocked
|
|
233
234
|
? null
|
|
@@ -3,7 +3,6 @@ import type { SessionRunStatus } from '@/features/chat/types/session-run-status.
|
|
|
3
3
|
export type ChatSessionListMode = 'time-first' | 'project-first';
|
|
4
4
|
export type ChatSessionListSnapshot = {
|
|
5
5
|
selectedSessionKey: string | null;
|
|
6
|
-
draftSessionKey: string | null;
|
|
7
6
|
selectedAgentId: string;
|
|
8
7
|
query: string;
|
|
9
8
|
listMode: ChatSessionListMode;
|
|
@@ -49,7 +48,6 @@ type ChatSessionListStoreSet = Parameters<StateCreator<ChatSessionListStore>>[0]
|
|
|
49
48
|
|
|
50
49
|
const initialSnapshot: ChatSessionListSnapshot = {
|
|
51
50
|
selectedSessionKey: null,
|
|
52
|
-
draftSessionKey: null,
|
|
53
51
|
selectedAgentId: 'main',
|
|
54
52
|
query: '',
|
|
55
53
|
listMode: 'time-first'
|
|
@@ -55,9 +55,11 @@ export type ChatThreadSnapshot = {
|
|
|
55
55
|
messages: readonly NcpMessage[];
|
|
56
56
|
isSending: boolean;
|
|
57
57
|
isAwaitingAssistantOutput: boolean;
|
|
58
|
+
hasSubmittedDraftMessage: boolean;
|
|
58
59
|
parentSessionKey?: string | null;
|
|
59
60
|
parentSessionLabel?: string | null;
|
|
60
61
|
workspacePanelParentKey?: string | null;
|
|
62
|
+
activeWorkspacePanelKind?: "child-session" | "file" | "cron" | null;
|
|
61
63
|
childSessionTabs: ChatChildSessionTab[];
|
|
62
64
|
activeChildSessionKey?: string | null;
|
|
63
65
|
workspaceFileTabs: ChatWorkspaceFileTab[];
|
|
@@ -92,9 +94,11 @@ const initialSnapshot: ChatThreadSnapshot = {
|
|
|
92
94
|
messages: [],
|
|
93
95
|
isSending: false,
|
|
94
96
|
isAwaitingAssistantOutput: false,
|
|
97
|
+
hasSubmittedDraftMessage: false,
|
|
95
98
|
parentSessionKey: null,
|
|
96
99
|
parentSessionLabel: null,
|
|
97
100
|
workspacePanelParentKey: null,
|
|
101
|
+
activeWorkspacePanelKind: null,
|
|
98
102
|
childSessionTabs: [],
|
|
99
103
|
activeChildSessionKey: null,
|
|
100
104
|
workspaceFileTabs: [],
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import type { SessionEntryView } from '@/shared/lib/api';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
formatSessionListTime,
|
|
5
|
+
sessionActivityPreviewText,
|
|
6
|
+
sessionDisplayName,
|
|
7
|
+
sessionMatchesQuery
|
|
8
|
+
} from './chat-session-display.utils';
|
|
4
9
|
|
|
5
10
|
function createSession(overrides: Partial<SessionEntryView> = {}): SessionEntryView {
|
|
6
11
|
return {
|
|
@@ -52,4 +57,81 @@ describe('chat-session-display', () => {
|
|
|
52
57
|
it('treats an empty query as a match', () => {
|
|
53
58
|
expect(sessionMatchesQuery(createSession({ label: 'Anything' }), ' ')).toBe(true);
|
|
54
59
|
});
|
|
60
|
+
|
|
61
|
+
it('shows running activity before the previous reply preview', () => {
|
|
62
|
+
expect(
|
|
63
|
+
sessionActivityPreviewText(
|
|
64
|
+
createSession({
|
|
65
|
+
activityPreview: {
|
|
66
|
+
state: 'running',
|
|
67
|
+
statusText: '正在调用工具:shell',
|
|
68
|
+
replyText: '之前的回复',
|
|
69
|
+
timestamp: '2026-05-16T01:00:00.000Z'
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
)
|
|
73
|
+
).toBe('正在调用工具:shell');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('shows the final assistant reply after completion', () => {
|
|
77
|
+
expect(
|
|
78
|
+
sessionActivityPreviewText(
|
|
79
|
+
createSession({
|
|
80
|
+
activityPreview: {
|
|
81
|
+
state: 'completed',
|
|
82
|
+
statusText: '工具调用完成',
|
|
83
|
+
replyText: '最终回复内容',
|
|
84
|
+
timestamp: '2026-05-16T01:00:00.000Z'
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
)
|
|
88
|
+
).toBe('最终回复内容');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('formats today activity as time like WeChat session lists', () => {
|
|
92
|
+
expect(
|
|
93
|
+
formatSessionListTime(
|
|
94
|
+
new Date(2026, 4, 16, 9, 5),
|
|
95
|
+
'zh',
|
|
96
|
+
new Date(2026, 4, 16, 12, 0)
|
|
97
|
+
)
|
|
98
|
+
).toBe('09:05');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('formats yesterday activity as yesterday like WeChat session lists', () => {
|
|
102
|
+
expect(
|
|
103
|
+
formatSessionListTime(
|
|
104
|
+
new Date(2026, 4, 15, 23, 30),
|
|
105
|
+
'zh',
|
|
106
|
+
new Date(2026, 4, 16, 12, 0)
|
|
107
|
+
)
|
|
108
|
+
).toBe('昨天');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('formats recent activity within a week as weekday', () => {
|
|
112
|
+
expect(
|
|
113
|
+
formatSessionListTime(
|
|
114
|
+
new Date(2026, 4, 12, 18, 30),
|
|
115
|
+
'zh',
|
|
116
|
+
new Date(2026, 4, 16, 12, 0)
|
|
117
|
+
)
|
|
118
|
+
).toBe('星期二');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('formats older activity as date and keeps year only across years', () => {
|
|
122
|
+
expect(
|
|
123
|
+
formatSessionListTime(
|
|
124
|
+
new Date(2026, 4, 1, 18, 30),
|
|
125
|
+
'zh',
|
|
126
|
+
new Date(2026, 4, 16, 12, 0)
|
|
127
|
+
)
|
|
128
|
+
).toBe('5月1日');
|
|
129
|
+
expect(
|
|
130
|
+
formatSessionListTime(
|
|
131
|
+
new Date(2025, 11, 31, 18, 30),
|
|
132
|
+
'zh',
|
|
133
|
+
new Date(2026, 4, 16, 12, 0)
|
|
134
|
+
)
|
|
135
|
+
).toBe('2025年12月31日');
|
|
136
|
+
});
|
|
55
137
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SessionEntryView } from '@/shared/lib/api';
|
|
2
|
+
import { getLanguage, getLocale, t, type I18nLanguage } from '@/shared/lib/i18n';
|
|
2
3
|
|
|
3
4
|
export function sessionDisplayName(session: SessionEntryView): string {
|
|
4
5
|
const label = session.label?.trim();
|
|
@@ -9,6 +10,78 @@ export function sessionDisplayName(session: SessionEntryView): string {
|
|
|
9
10
|
return chunks[chunks.length - 1] || session.key;
|
|
10
11
|
}
|
|
11
12
|
|
|
13
|
+
export function sessionActivityPreviewText(session: SessionEntryView): string | null {
|
|
14
|
+
const preview = session.activityPreview;
|
|
15
|
+
if (!preview) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
if (preview.state === 'failed' || preview.state === 'running') {
|
|
19
|
+
return preview.statusText ?? preview.replyText ?? null;
|
|
20
|
+
}
|
|
21
|
+
if (preview.state === 'completed') {
|
|
22
|
+
return preview.replyText ?? preview.statusText ?? null;
|
|
23
|
+
}
|
|
24
|
+
return preview.statusText ?? preview.replyText ?? null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function startOfLocalDate(date: Date): number {
|
|
28
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isSameLocalYear(left: Date, right: Date): boolean {
|
|
32
|
+
return left.getFullYear() === right.getFullYear();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function formatChineseSessionDate(date: Date, now: Date): string {
|
|
36
|
+
const monthDay = `${date.getMonth() + 1}月${date.getDate()}日`;
|
|
37
|
+
return isSameLocalYear(date, now) ? monthDay : `${date.getFullYear()}年${monthDay}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function formatSessionListTime(
|
|
41
|
+
value?: string | Date,
|
|
42
|
+
lang: I18nLanguage = getLanguage(),
|
|
43
|
+
now: Date = new Date()
|
|
44
|
+
): string {
|
|
45
|
+
if (!value) {
|
|
46
|
+
return '-';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
50
|
+
if (Number.isNaN(date.getTime())) {
|
|
51
|
+
return typeof value === 'string' ? value : '-';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const locale = getLocale(lang);
|
|
55
|
+
const dateStart = startOfLocalDate(date);
|
|
56
|
+
const todayStart = startOfLocalDate(now);
|
|
57
|
+
const daysAgo = Math.floor((todayStart - dateStart) / 86_400_000);
|
|
58
|
+
|
|
59
|
+
if (daysAgo === 0) {
|
|
60
|
+
return new Intl.DateTimeFormat(locale, {
|
|
61
|
+
hour: '2-digit',
|
|
62
|
+
minute: '2-digit',
|
|
63
|
+
hour12: false
|
|
64
|
+
}).format(date);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (daysAgo === 1) {
|
|
68
|
+
return t('chatSidebarYesterday', lang);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (daysAgo > 1 && daysAgo < 7) {
|
|
72
|
+
return new Intl.DateTimeFormat(locale, { weekday: 'long' }).format(date);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (lang === 'zh') {
|
|
76
|
+
return formatChineseSessionDate(date, now);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const options: Intl.DateTimeFormatOptions = isSameLocalYear(date, now)
|
|
80
|
+
? { month: 'numeric', day: 'numeric' }
|
|
81
|
+
: { year: 'numeric', month: 'numeric', day: 'numeric' };
|
|
82
|
+
return new Intl.DateTimeFormat(locale, options).format(date);
|
|
83
|
+
}
|
|
84
|
+
|
|
12
85
|
function normalizeSessionSearchValue(value: string): string {
|
|
13
86
|
return value.trim().toLowerCase();
|
|
14
87
|
}
|