@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
package/src/features/chat/components/conversation/session-header/chat-session-header-actions.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
|
-
import { FolderOpen, GitBranch, MoreHorizontal, Trash2 } from 'lucide-react';
|
|
2
|
+
import { AlarmClock, FolderOpen, GitBranch, MoreHorizontal, Trash2 } from 'lucide-react';
|
|
3
3
|
import { Button } from '@/shared/components/ui/button';
|
|
4
4
|
import { Popover, PopoverContent, PopoverTrigger } from '@/shared/components/ui/popover';
|
|
5
5
|
import { useChatSessionProject } from '@/features/chat/hooks/use-chat-session-project';
|
|
@@ -7,13 +7,18 @@ import { ChatSessionHeaderMenuItem } from './chat-session-header-menu-item';
|
|
|
7
7
|
import { ChatSessionProjectDialog } from './chat-session-project-dialog';
|
|
8
8
|
import { t } from '@/shared/lib/i18n';
|
|
9
9
|
|
|
10
|
+
const SESSION_HEADER_ACTION_GROUP_CLASS = 'flex shrink-0 items-center gap-1.5';
|
|
11
|
+
const SESSION_HEADER_ACTION_BUTTON_CLASS = 'h-7 w-7 rounded-lg shrink-0 text-gray-400 hover:text-gray-700';
|
|
12
|
+
|
|
10
13
|
type ChatSessionHeaderActionsProps = {
|
|
11
14
|
sessionKey: string;
|
|
12
15
|
canDeleteSession: boolean;
|
|
13
16
|
isDeletePending: boolean;
|
|
14
17
|
projectRoot?: string | null;
|
|
15
18
|
childSessionCount?: number;
|
|
19
|
+
sessionCronJobCount?: number;
|
|
16
20
|
onOpenChildSessions?: () => void;
|
|
21
|
+
onOpenSessionCronJobs?: () => void;
|
|
17
22
|
onDeleteSession: () => void;
|
|
18
23
|
};
|
|
19
24
|
|
|
@@ -23,7 +28,9 @@ export function ChatSessionHeaderActions({
|
|
|
23
28
|
isDeletePending,
|
|
24
29
|
projectRoot,
|
|
25
30
|
childSessionCount = 0,
|
|
31
|
+
sessionCronJobCount = 0,
|
|
26
32
|
onOpenChildSessions,
|
|
33
|
+
onOpenSessionCronJobs,
|
|
27
34
|
onDeleteSession,
|
|
28
35
|
}: ChatSessionHeaderActionsProps) {
|
|
29
36
|
const updateSessionProject = useChatSessionProject();
|
|
@@ -49,13 +56,13 @@ export function ChatSessionHeaderActions({
|
|
|
49
56
|
};
|
|
50
57
|
|
|
51
58
|
return (
|
|
52
|
-
|
|
59
|
+
<div className={SESSION_HEADER_ACTION_GROUP_CLASS}>
|
|
53
60
|
{childSessionCount > 0 && onOpenChildSessions ? (
|
|
54
61
|
<Button
|
|
55
62
|
type="button"
|
|
56
63
|
variant="ghost"
|
|
57
64
|
size="icon"
|
|
58
|
-
className=
|
|
65
|
+
className={SESSION_HEADER_ACTION_BUTTON_CLASS}
|
|
59
66
|
aria-label={t('chatSessionOpenChildSessions')}
|
|
60
67
|
title={t('chatSessionOpenChildSessions')}
|
|
61
68
|
onClick={onOpenChildSessions}
|
|
@@ -64,12 +71,26 @@ export function ChatSessionHeaderActions({
|
|
|
64
71
|
<GitBranch className="h-4 w-4" />
|
|
65
72
|
</Button>
|
|
66
73
|
) : null}
|
|
74
|
+
{sessionCronJobCount > 0 && onOpenSessionCronJobs ? (
|
|
75
|
+
<Button
|
|
76
|
+
type="button"
|
|
77
|
+
variant="ghost"
|
|
78
|
+
size="icon"
|
|
79
|
+
className={SESSION_HEADER_ACTION_BUTTON_CLASS}
|
|
80
|
+
aria-label={t('chatSessionOpenCronJobs')}
|
|
81
|
+
title={t('chatSessionOpenCronJobs')}
|
|
82
|
+
onClick={onOpenSessionCronJobs}
|
|
83
|
+
disabled={isBusy}
|
|
84
|
+
>
|
|
85
|
+
<AlarmClock className="h-4 w-4" />
|
|
86
|
+
</Button>
|
|
87
|
+
) : null}
|
|
67
88
|
<Popover open={isMenuOpen} onOpenChange={setIsMenuOpen}>
|
|
68
89
|
<PopoverTrigger asChild>
|
|
69
90
|
<Button
|
|
70
91
|
variant="ghost"
|
|
71
92
|
size="icon"
|
|
72
|
-
className=
|
|
93
|
+
className={SESSION_HEADER_ACTION_BUTTON_CLASS}
|
|
73
94
|
aria-label={t('chatSessionMoreActions')}
|
|
74
95
|
disabled={isBusy}
|
|
75
96
|
>
|
|
@@ -108,6 +129,6 @@ export function ChatSessionHeaderActions({
|
|
|
108
129
|
onOpenChange={setIsDialogOpen}
|
|
109
130
|
onSave={runProjectUpdate}
|
|
110
131
|
/>
|
|
111
|
-
|
|
132
|
+
</div>
|
|
112
133
|
);
|
|
113
134
|
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { NavLink } from 'react-router-dom';
|
|
2
|
+
import { Popover, PopoverContent, PopoverTrigger } from '@/shared/components/ui/popover';
|
|
3
|
+
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/shared/components/ui/select';
|
|
4
|
+
import { t, type I18nLanguage } from '@/shared/lib/i18n';
|
|
5
|
+
import type { UiTheme } from '@/shared/lib/theme';
|
|
6
|
+
import {
|
|
7
|
+
BookOpen,
|
|
8
|
+
ChevronRight,
|
|
9
|
+
Languages,
|
|
10
|
+
Palette,
|
|
11
|
+
Settings,
|
|
12
|
+
type LucideIcon,
|
|
13
|
+
} from 'lucide-react';
|
|
14
|
+
|
|
15
|
+
type ChatSidebarUtilityOption<Value extends string> = {
|
|
16
|
+
value: Value;
|
|
17
|
+
label: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type ChatSidebarUtilityMenuProps = {
|
|
21
|
+
isOpen: boolean;
|
|
22
|
+
onOpenChange: (open: boolean) => void;
|
|
23
|
+
currentTheme: UiTheme;
|
|
24
|
+
currentThemeLabel: string;
|
|
25
|
+
themeOptions: ChatSidebarUtilityOption<UiTheme>[];
|
|
26
|
+
onSelectTheme: (theme: UiTheme) => void;
|
|
27
|
+
currentLanguage: I18nLanguage;
|
|
28
|
+
currentLanguageLabel: string;
|
|
29
|
+
languageOptions: ChatSidebarUtilityOption<I18nLanguage>[];
|
|
30
|
+
onSelectLanguage: (language: I18nLanguage) => void;
|
|
31
|
+
onOpenDocs: () => void;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function ChatSidebarUtilityMenu({
|
|
35
|
+
isOpen,
|
|
36
|
+
onOpenChange,
|
|
37
|
+
currentTheme,
|
|
38
|
+
currentThemeLabel,
|
|
39
|
+
themeOptions,
|
|
40
|
+
onSelectTheme,
|
|
41
|
+
currentLanguage,
|
|
42
|
+
currentLanguageLabel,
|
|
43
|
+
languageOptions,
|
|
44
|
+
onSelectLanguage,
|
|
45
|
+
onOpenDocs,
|
|
46
|
+
}: ChatSidebarUtilityMenuProps) {
|
|
47
|
+
const handleOpenDocs = () => {
|
|
48
|
+
onOpenDocs();
|
|
49
|
+
onOpenChange(false);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Popover open={isOpen} onOpenChange={onOpenChange}>
|
|
54
|
+
<PopoverTrigger asChild>
|
|
55
|
+
<button
|
|
56
|
+
type="button"
|
|
57
|
+
aria-label={currentLanguage === 'zh' ? '设置菜单' : 'Settings menu'}
|
|
58
|
+
className="flex w-full items-center gap-2.5 rounded-xl px-3 py-2 text-[13px] font-medium text-gray-600 transition-all duration-base hover:bg-gray-200/60 hover:text-gray-800"
|
|
59
|
+
>
|
|
60
|
+
<Settings className="h-4 w-4 shrink-0 text-gray-400" />
|
|
61
|
+
<span className="min-w-0 flex-1 text-left">{t('settings')}</span>
|
|
62
|
+
<span className="max-w-[112px] truncate text-[13px] text-gray-500">
|
|
63
|
+
{currentThemeLabel} / {currentLanguageLabel}
|
|
64
|
+
</span>
|
|
65
|
+
</button>
|
|
66
|
+
</PopoverTrigger>
|
|
67
|
+
<PopoverContent side="top" align="start" className="w-64 p-2">
|
|
68
|
+
<div className="space-y-1">
|
|
69
|
+
<NavLink
|
|
70
|
+
to="/settings"
|
|
71
|
+
onClick={() => onOpenChange(false)}
|
|
72
|
+
className="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-gray-700 transition-colors hover:bg-gray-100"
|
|
73
|
+
>
|
|
74
|
+
<Settings className="h-4 w-4 text-gray-400" />
|
|
75
|
+
<span className="flex-1 text-left">{t('settings')}</span>
|
|
76
|
+
</NavLink>
|
|
77
|
+
<button
|
|
78
|
+
type="button"
|
|
79
|
+
onClick={handleOpenDocs}
|
|
80
|
+
className="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-gray-700 transition-colors hover:bg-gray-100"
|
|
81
|
+
>
|
|
82
|
+
<BookOpen className="h-4 w-4 text-gray-400" />
|
|
83
|
+
<span className="flex-1 text-left">{t('docBrowserHelp')}</span>
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div className="my-2 h-px bg-gray-200/70" />
|
|
88
|
+
|
|
89
|
+
<ChatSidebarUtilitySelect
|
|
90
|
+
icon={Palette}
|
|
91
|
+
label={t('theme')}
|
|
92
|
+
options={themeOptions}
|
|
93
|
+
value={currentTheme}
|
|
94
|
+
valueLabel={currentThemeLabel}
|
|
95
|
+
onSelect={onSelectTheme}
|
|
96
|
+
onCloseMenu={() => onOpenChange(false)}
|
|
97
|
+
/>
|
|
98
|
+
|
|
99
|
+
<ChatSidebarUtilitySelect
|
|
100
|
+
icon={Languages}
|
|
101
|
+
label={t('language')}
|
|
102
|
+
options={languageOptions}
|
|
103
|
+
value={currentLanguage}
|
|
104
|
+
valueLabel={currentLanguageLabel}
|
|
105
|
+
onSelect={onSelectLanguage}
|
|
106
|
+
onCloseMenu={() => onOpenChange(false)}
|
|
107
|
+
/>
|
|
108
|
+
</PopoverContent>
|
|
109
|
+
</Popover>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function ChatSidebarUtilitySelect<Value extends string>({
|
|
114
|
+
icon: Icon,
|
|
115
|
+
label,
|
|
116
|
+
options,
|
|
117
|
+
value,
|
|
118
|
+
valueLabel,
|
|
119
|
+
onSelect,
|
|
120
|
+
onCloseMenu,
|
|
121
|
+
}: {
|
|
122
|
+
icon: LucideIcon;
|
|
123
|
+
label: string;
|
|
124
|
+
options: ChatSidebarUtilityOption<Value>[];
|
|
125
|
+
value: Value;
|
|
126
|
+
valueLabel: string;
|
|
127
|
+
onSelect: (value: Value) => void;
|
|
128
|
+
onCloseMenu: () => void;
|
|
129
|
+
}) {
|
|
130
|
+
return (
|
|
131
|
+
<Select
|
|
132
|
+
value={value}
|
|
133
|
+
onOpenChange={(open) => {
|
|
134
|
+
if (!open) {
|
|
135
|
+
onCloseMenu();
|
|
136
|
+
}
|
|
137
|
+
}}
|
|
138
|
+
onValueChange={(nextValue) => onSelect(nextValue as Value)}
|
|
139
|
+
>
|
|
140
|
+
<SelectTrigger
|
|
141
|
+
aria-label={label}
|
|
142
|
+
className="h-auto w-full rounded-lg border-0 bg-transparent px-3 py-2 text-[13px] font-medium text-gray-700 shadow-none hover:bg-gray-100 focus:ring-0"
|
|
143
|
+
indicator={<ChevronRight className="h-4 w-4 text-gray-400" />}
|
|
144
|
+
>
|
|
145
|
+
<div className="flex min-w-0 items-center gap-2.5">
|
|
146
|
+
<Icon className="h-4 w-4 shrink-0 text-gray-400" />
|
|
147
|
+
<span className="text-left">{label}</span>
|
|
148
|
+
</div>
|
|
149
|
+
<span className="ml-auto max-w-[96px] truncate text-[13px] text-gray-500">
|
|
150
|
+
{valueLabel}
|
|
151
|
+
</span>
|
|
152
|
+
</SelectTrigger>
|
|
153
|
+
<SelectContent
|
|
154
|
+
side="right"
|
|
155
|
+
align="center"
|
|
156
|
+
sideOffset={6}
|
|
157
|
+
className="z-[var(--z-tooltip)] min-w-[7rem] rounded-lg"
|
|
158
|
+
viewportClassName="h-auto min-w-[7rem] w-auto"
|
|
159
|
+
>
|
|
160
|
+
{options.map((option) => {
|
|
161
|
+
return (
|
|
162
|
+
<SelectItem
|
|
163
|
+
key={option.value}
|
|
164
|
+
value={option.value}
|
|
165
|
+
className="text-[13px]"
|
|
166
|
+
>
|
|
167
|
+
{option.label}
|
|
168
|
+
</SelectItem>
|
|
169
|
+
);
|
|
170
|
+
})}
|
|
171
|
+
</SelectContent>
|
|
172
|
+
</Select>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
@@ -9,11 +9,14 @@ import { useChatSessionListStore } from '@/features/chat/stores/chat-session-lis
|
|
|
9
9
|
const mocks = vi.hoisted(() => ({
|
|
10
10
|
createSession: vi.fn(() => 'draft-session-key'),
|
|
11
11
|
goToSession: vi.fn(),
|
|
12
|
+
goToChatRoot: vi.fn(),
|
|
12
13
|
setQuery: vi.fn(),
|
|
13
14
|
setListMode: vi.fn(),
|
|
14
15
|
selectSession: vi.fn(),
|
|
15
16
|
openChildSessionPanel: vi.fn(),
|
|
16
17
|
docOpen: vi.fn(),
|
|
18
|
+
setLanguage: vi.fn(),
|
|
19
|
+
setTheme: vi.fn(),
|
|
17
20
|
updateNcpSession: vi.fn(),
|
|
18
21
|
agents: [] as Array<{ id: string; displayName?: string; avatarUrl?: string | null }>,
|
|
19
22
|
sessionItems: [] as NcpSessionListItemView[],
|
|
@@ -31,6 +34,7 @@ vi.mock('@/features/chat/components/providers/chat-presenter.provider', () => ({
|
|
|
31
34
|
usePresenter: () => ({
|
|
32
35
|
chatUiManager: {
|
|
33
36
|
goToSession: mocks.goToSession,
|
|
37
|
+
goToChatRoot: mocks.goToChatRoot,
|
|
34
38
|
},
|
|
35
39
|
chatSessionListManager: {
|
|
36
40
|
createSession: mocks.createSession,
|
|
@@ -100,14 +104,14 @@ vi.mock('@/shared/hooks/use-agents', () => ({
|
|
|
100
104
|
vi.mock('@/app/components/i18n-provider', () => ({
|
|
101
105
|
useI18n: () => ({
|
|
102
106
|
language: 'en',
|
|
103
|
-
setLanguage:
|
|
107
|
+
setLanguage: mocks.setLanguage
|
|
104
108
|
})
|
|
105
109
|
}));
|
|
106
110
|
|
|
107
111
|
vi.mock('@/app/components/theme-provider', () => ({
|
|
108
112
|
useTheme: () => ({
|
|
109
113
|
theme: 'warm',
|
|
110
|
-
setTheme:
|
|
114
|
+
setTheme: mocks.setTheme
|
|
111
115
|
})
|
|
112
116
|
}));
|
|
113
117
|
|
|
@@ -121,11 +125,14 @@ function resetSidebarTestState() {
|
|
|
121
125
|
mocks.createSession.mockReset();
|
|
122
126
|
mocks.createSession.mockReturnValue('draft-session-key');
|
|
123
127
|
mocks.goToSession.mockReset();
|
|
128
|
+
mocks.goToChatRoot.mockReset();
|
|
124
129
|
mocks.setQuery.mockReset();
|
|
125
130
|
mocks.setListMode.mockReset();
|
|
126
131
|
mocks.selectSession.mockReset();
|
|
127
132
|
mocks.openChildSessionPanel.mockReset();
|
|
128
133
|
mocks.docOpen.mockReset();
|
|
134
|
+
mocks.setLanguage.mockReset();
|
|
135
|
+
mocks.setTheme.mockReset();
|
|
129
136
|
mocks.updateNcpSession.mockReset();
|
|
130
137
|
mocks.updateNcpSession.mockResolvedValue({});
|
|
131
138
|
mocks.agents = [];
|
|
@@ -166,7 +173,7 @@ describe('ChatSidebar create and list basics', () => {
|
|
|
166
173
|
fireEvent.click(screen.getByLabelText('Session Type'));
|
|
167
174
|
fireEvent.click(screen.getByText('Codex'));
|
|
168
175
|
|
|
169
|
-
expect(mocks.createSession).toHaveBeenCalledWith('codex');
|
|
176
|
+
expect(mocks.createSession).toHaveBeenCalledWith('codex', undefined);
|
|
170
177
|
await waitFor(() => {
|
|
171
178
|
expect(screen.queryByText('Codex')).toBeNull();
|
|
172
179
|
});
|
|
@@ -233,8 +240,38 @@ describe('ChatSidebar create and list basics', () => {
|
|
|
233
240
|
fireEvent.click(screen.getByText('Codex'));
|
|
234
241
|
|
|
235
242
|
expect(mocks.setQuery).toHaveBeenCalledWith('release notes');
|
|
236
|
-
expect(mocks.createSession).toHaveBeenCalledWith('codex');
|
|
237
|
-
expect(mocks.
|
|
243
|
+
expect(mocks.createSession).toHaveBeenCalledWith('codex', undefined);
|
|
244
|
+
expect(mocks.goToChatRoot).toHaveBeenCalledWith();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('keeps low-frequency utility choices behind nested selectors', () => {
|
|
248
|
+
render(
|
|
249
|
+
<MemoryRouter>
|
|
250
|
+
<ChatSidebar />
|
|
251
|
+
</MemoryRouter>
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
expect(screen.queryByRole('button', { name: 'Help Docs' })).toBeNull();
|
|
255
|
+
expect(screen.queryByText('Language')).toBeNull();
|
|
256
|
+
expect(screen.queryByRole('option', { name: 'Cool' })).toBeNull();
|
|
257
|
+
|
|
258
|
+
fireEvent.click(screen.getByRole('button', { name: 'Settings menu' }));
|
|
259
|
+
expect(screen.getByText('Theme')).not.toBeNull();
|
|
260
|
+
expect(screen.queryByRole('option', { name: 'Cool' })).toBeNull();
|
|
261
|
+
|
|
262
|
+
fireEvent.click(screen.getByRole('button', { name: 'Help Docs' }));
|
|
263
|
+
|
|
264
|
+
expect(mocks.docOpen).toHaveBeenCalledWith(undefined, {
|
|
265
|
+
kind: 'docs',
|
|
266
|
+
newTab: true,
|
|
267
|
+
title: 'Docs',
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
fireEvent.click(screen.getByRole('button', { name: 'Settings menu' }));
|
|
271
|
+
fireEvent.click(screen.getByRole('combobox', { name: 'Theme' }));
|
|
272
|
+
fireEvent.click(screen.getByRole('option', { name: 'Cool' }));
|
|
273
|
+
|
|
274
|
+
expect(mocks.setTheme).toHaveBeenCalledWith('cool');
|
|
238
275
|
});
|
|
239
276
|
|
|
240
277
|
it('creates the default session directly from the compact mobile add button when no menu is needed', () => {
|
|
@@ -254,8 +291,8 @@ describe('ChatSidebar create and list basics', () => {
|
|
|
254
291
|
|
|
255
292
|
fireEvent.click(screen.getByRole('button', { name: 'New Task' }));
|
|
256
293
|
|
|
257
|
-
expect(mocks.createSession).toHaveBeenCalledWith('native');
|
|
258
|
-
expect(mocks.
|
|
294
|
+
expect(mocks.createSession).toHaveBeenCalledWith('native', undefined);
|
|
295
|
+
expect(mocks.goToChatRoot).toHaveBeenCalledWith();
|
|
259
296
|
});
|
|
260
297
|
|
|
261
298
|
it('shows a session type badge for non-native sessions in the list', () => {
|
|
@@ -564,7 +601,7 @@ describe('ChatSidebar project-first mode', () => {
|
|
|
564
601
|
fireEvent.click(screen.getByText('Codex'));
|
|
565
602
|
|
|
566
603
|
expect(mocks.createSession).toHaveBeenCalledWith('codex', '/tmp/project-mobile');
|
|
567
|
-
expect(mocks.
|
|
604
|
+
expect(mocks.goToChatRoot).toHaveBeenCalledWith();
|
|
568
605
|
});
|
|
569
606
|
});
|
|
570
607
|
|
|
@@ -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,26 +13,24 @@ 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;
|
|
@@ -186,6 +183,7 @@ export function ChatSidebar({
|
|
|
186
183
|
const presenter = usePresenter();
|
|
187
184
|
const docBrowser = useDocBrowser();
|
|
188
185
|
const [isCreateMenuOpen, setIsCreateMenuOpen] = useState(false);
|
|
186
|
+
const [isUtilityMenuOpen, setIsUtilityMenuOpen] = useState(false);
|
|
189
187
|
const inputSnapshot = useChatInputStore((state) => state.snapshot);
|
|
190
188
|
const listSnapshot = useChatSessionListStore((state) => state.snapshot);
|
|
191
189
|
const systemStatus = useSystemStatus();
|
|
@@ -195,6 +193,14 @@ export function ChatSidebar({
|
|
|
195
193
|
const { theme, setTheme } = useTheme();
|
|
196
194
|
const currentThemeLabel = t(THEME_OPTIONS.find((o) => o.value === theme)?.labelKey ?? 'themeWarm');
|
|
197
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
|
+
);
|
|
198
204
|
const agentsById = useMemo(
|
|
199
205
|
() => new Map((agentsQuery.data?.agents ?? []).map((agent) => [agent.id, agent])),
|
|
200
206
|
[agentsQuery.data?.agents]
|
|
@@ -262,10 +268,10 @@ export function ChatSidebar({
|
|
|
262
268
|
: 'w-[280px] shrink-0 border-r border-gray-200/60',
|
|
263
269
|
)}
|
|
264
270
|
>
|
|
265
|
-
{!isMobileVariant ? (
|
|
266
|
-
<div className=
|
|
271
|
+
{!isMobileVariant && !isWindowsDesktopHost() ? (
|
|
272
|
+
<div className={cn('px-5 pb-3', isMacDesktopHost() ? 'pt-1.5' : 'pt-5')}>
|
|
267
273
|
<BrandHeader
|
|
268
|
-
className="flex items-center gap-2
|
|
274
|
+
className="flex min-w-0 items-center gap-2"
|
|
269
275
|
suffix={<StatusBadge status={systemStatus.connectionStatus} />}
|
|
270
276
|
/>
|
|
271
277
|
</div>
|
|
@@ -361,43 +367,20 @@ export function ChatSidebar({
|
|
|
361
367
|
</div>
|
|
362
368
|
|
|
363
369
|
{!isMobileVariant ? (
|
|
364
|
-
<div className="px-3 py-3 border-t border-gray-200/60
|
|
365
|
-
<
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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' })}
|
|
370
383
|
/>
|
|
371
|
-
<SidebarActionItem
|
|
372
|
-
onClick={() => docBrowser.open(undefined, { kind: 'docs', newTab: true, title: 'Docs' })}
|
|
373
|
-
icon={BookOpen}
|
|
374
|
-
label={t('docBrowserHelp')}
|
|
375
|
-
density="compact"
|
|
376
|
-
/>
|
|
377
|
-
<SidebarSelectItem
|
|
378
|
-
value={theme}
|
|
379
|
-
onValueChange={(value) => setTheme(value as UiTheme)}
|
|
380
|
-
icon={Palette}
|
|
381
|
-
label={t('theme')}
|
|
382
|
-
valueLabel={currentThemeLabel}
|
|
383
|
-
density="compact"
|
|
384
|
-
>
|
|
385
|
-
{THEME_OPTIONS.map((option) => (
|
|
386
|
-
<SelectItem key={option.value} value={option.value} className="text-xs">{t(option.labelKey)}</SelectItem>
|
|
387
|
-
))}
|
|
388
|
-
</SidebarSelectItem>
|
|
389
|
-
<SidebarSelectItem
|
|
390
|
-
value={language}
|
|
391
|
-
onValueChange={(value) => handleLanguageSwitch(value as I18nLanguage)}
|
|
392
|
-
icon={Languages}
|
|
393
|
-
label={t('language')}
|
|
394
|
-
valueLabel={currentLanguageLabel}
|
|
395
|
-
density="compact"
|
|
396
|
-
>
|
|
397
|
-
{LANGUAGE_OPTIONS.map((option) => (
|
|
398
|
-
<SelectItem key={option.value} value={option.value} className="text-xs">{option.label}</SelectItem>
|
|
399
|
-
))}
|
|
400
|
-
</SidebarSelectItem>
|
|
401
384
|
</div>
|
|
402
385
|
) : null}
|
|
403
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
|
+
}
|
|
@@ -176,4 +176,18 @@ describe("useNcpAgentRuntime", () => {
|
|
|
176
176
|
"assistant-1",
|
|
177
177
|
]);
|
|
178
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
|
+
});
|
|
179
193
|
});
|