@nextclaw/ui 0.20.2 → 0.21.0
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 +35 -0
- package/dist/assets/{api-DvpQWjTS.js → api-DNLQPHfQ.js} +5 -5
- package/dist/assets/{appearance-settings-page-U0m0ktu3.js → appearance-settings-page-BlAFMOGV.js} +1 -1
- package/dist/assets/{book-open-DRofSTWr.js → book-open-BryfPTTa.js} +1 -1
- package/dist/assets/{channels-list-page-BURQarD0.js → channels-list-page-Cfbgubyv.js} +2 -2
- package/dist/assets/{config-split-page-BrAQYTIl.js → config-split-page-Dd8xG0Jk.js} +1 -1
- package/dist/assets/{confirm-dialog-DDJomLnO.js → confirm-dialog-CZIWjnJ6.js} +2 -2
- package/dist/assets/{createLucideIcon-DzkNHr5Z.js → createLucideIcon-BxwVe8ZD.js} +1 -1
- package/dist/assets/desktop-capabilities-page-C6YxsHeV.js +1 -0
- package/dist/assets/desktop-update-config-BVN4QPJn.js +1 -0
- package/dist/assets/{dist-NskPB_yW.js → dist-BRloKr3S.js} +1 -1
- package/dist/assets/{dist-DXbLo2b1.js → dist-Cd8Nm3BW.js} +1 -1
- package/dist/assets/doc-browser-Bzgqmmiu.js +1 -0
- package/dist/assets/doc-browser-D0qI0Z_R.js +1 -0
- package/dist/assets/{doc-browser-context-CnKV4gJy.js → doc-browser-context-3JGufiMA.js} +1 -1
- package/dist/assets/{extensions-DvlFMpAs.js → extensions-Dedd2oDI.js} +1 -1
- package/dist/assets/{form-actions-UnPEZBkx.js → form-actions-FWCEof3V.js} +1 -1
- package/dist/assets/index-CXkHcESm.css +1 -0
- package/dist/assets/index-D25tRg1p.js +109 -0
- package/dist/assets/loader-circle-BoyCoFps.js +1 -0
- package/dist/assets/mcp-marketplace-page-Cn-9CDh3.js +1 -0
- package/dist/assets/mcp-marketplace-page-e9Rmfz-d.js +10 -0
- package/dist/assets/model-config-page-C173Ssil.js +1 -0
- package/dist/assets/{navigation-link-NjUdG6Vp.js → navigation-link-DTPOvGGL.js} +1 -1
- package/dist/assets/plus-D1R4Vipq.js +1 -0
- package/dist/assets/privacy-settings-page-CRserBVF.js +1 -0
- package/dist/assets/providers-config-page-C8jUtnZb.js +1 -0
- package/dist/assets/react-BAzO6LZS.js +8 -0
- package/dist/assets/{refresh-cw-CkeninE0.js → refresh-cw-BCj-yjde.js} +1 -1
- package/dist/assets/remote-6BHWP5zi.js +1 -0
- package/dist/assets/{rotate-cw-DP33PuXU.js → rotate-cw-BPnW4qUl.js} +1 -1
- package/dist/assets/runtime-config-page-aeQ1Dryz.js +1 -0
- package/dist/assets/{save-Cw2Xz-pB.js → save-KT0MumfJ.js} +1 -1
- package/dist/assets/{search-C2CFAVvd.js → search-Cv1Hx_nx.js} +1 -1
- package/dist/assets/{search-config-page-dqNT0Swf.js → search-config-page-b283V-fo.js} +1 -1
- package/dist/assets/{secrets-config-page-De6J3wVx.js → secrets-config-page-ss7x3BOo.js} +2 -2
- package/dist/assets/security-config-HHz62oxH.js +1 -0
- package/dist/assets/{select-DSuMhwVP.js → select-BVgD4EpG.js} +1 -1
- package/dist/assets/skeleton-ChUmcyys.js +1 -0
- package/dist/assets/{tag-chip-DFBLgsGf.js → tag-chip-DZ4ircUB.js} +1 -1
- package/dist/assets/x-BWDRfVWL.js +1 -0
- package/dist/index.html +21 -21
- package/package.json +5 -5
- package/src/app/components/layout/sidebar.tsx +12 -1
- package/src/app/configs/app-navigation.config.ts +17 -2
- package/src/app/hooks/use-feature-controls.ts +11 -0
- package/src/app/index.tsx +10 -0
- package/src/features/apps/components/app-package-card.tsx +29 -7
- package/src/features/apps/components/app-packages-panel.tsx +4 -0
- package/src/features/chat/components/conversation/chat-conversation-content.tsx +18 -2
- package/src/features/chat/components/conversation/chat-conversation-workspace-section.tsx +6 -0
- package/src/features/chat/components/layout/__tests__/chat-sidebar-session-area.test.tsx +16 -2
- package/src/features/chat/components/layout/chat-page-shell.tsx +19 -2
- package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +16 -0
- package/src/features/chat/components/layout/chat-sidebar.tsx +5 -1
- package/src/features/chat/features/message/utils/__tests__/chat-message-timeline-compaction.utils.test.ts +2 -2
- package/src/features/chat/features/message/utils/chat-message-timeline.utils.ts +2 -2
- package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-session-list-view.test.tsx +11 -3
- package/src/features/chat/features/ncp/hooks/use-ncp-session-list-view.ts +12 -5
- package/src/features/chat/features/ncp/hooks/use-ncp-session-queries.ts +15 -1
- package/src/features/chat/features/session/components/__tests__/chat-sidebar-session-item.test.tsx +12 -0
- package/src/features/chat/features/session/components/chat-sidebar-session-entry.tsx +3 -0
- package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +14 -1
- package/src/features/chat/features/session/utils/chat-session-deletion.utils.ts +25 -0
- package/src/features/chat/features/workspace/components/__tests__/chat-session-token-usage.test.tsx +4 -1
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.scroll-restoration.test.tsx +182 -0
- package/src/features/chat/features/workspace/components/__tests__/session-cron-job-content.test.tsx +78 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +47 -2
- package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +14 -1
- package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +9 -0
- package/src/features/chat/features/workspace/components/overview/chat-session-token-usage.tsx +17 -0
- package/src/features/chat/features/workspace/components/session-cron-job-content.tsx +269 -75
- package/src/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state.ts +5 -0
- package/src/features/chat/managers/__tests__/chat-thread-deletion.manager.test.ts +125 -0
- package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +0 -37
- package/src/features/chat/managers/chat-thread.manager.ts +17 -28
- package/src/features/cron/components/__tests__/cron-config.test.tsx +16 -5
- package/src/features/cron/components/cron-config.tsx +20 -46
- package/src/features/cron/components/cron-job-row.tsx +1 -1
- package/src/features/cron/hooks/__tests__/use-cron-job-actions.test.tsx +95 -0
- package/src/features/cron/hooks/use-cron-job-actions.ts +109 -0
- package/src/features/cron/index.ts +2 -0
- package/src/features/desktop-capabilities/components/desktop-authorization-dialog.test.tsx +58 -0
- package/src/features/desktop-capabilities/components/desktop-authorization-dialog.tsx +161 -0
- package/src/features/desktop-capabilities/hooks/use-desktop-capabilities.test.ts +10 -0
- package/src/features/desktop-capabilities/hooks/use-desktop-capabilities.ts +55 -0
- package/src/features/desktop-capabilities/index.ts +1 -0
- package/src/features/desktop-capabilities/managers/desktop-capability.manager.ts +25 -0
- package/src/features/desktop-capabilities/pages/desktop-capabilities-page.test.tsx +44 -0
- package/src/features/desktop-capabilities/pages/desktop-capabilities-page.tsx +171 -0
- package/src/features/desktop-capabilities/stores/desktop-authorization.store.ts +21 -0
- package/src/features/inbox/pages/inbox-page.tsx +18 -2
- package/src/features/marketplace/components/marketplace-page.tsx +7 -0
- package/src/features/marketplace/components/mcp/__tests__/mcp-connection-dialog.test.tsx +23 -0
- package/src/features/marketplace/components/mcp/mcp-connection-dialog.tsx +35 -0
- package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +20 -2
- package/src/features/panel-apps/pages/panel-app-main-page.tsx +8 -0
- package/src/platforms/desktop/components/desktop-app-shell.tsx +10 -1
- package/src/platforms/mobile/components/mobile-settings-shell.tsx +5 -1
- package/src/shared/components/doc-browser/doc-browser.tsx +0 -1
- package/src/shared/components/doc-browser/hooks/use-doc-browser-scroll-restoration.ts +9 -113
- package/src/shared/components/ui/__tests__/confirm-dialog.test.tsx +31 -0
- package/src/shared/components/ui/confirm-dialog.tsx +11 -1
- package/src/shared/hooks/use-panel-app-scroll-restoration.test.tsx +66 -0
- package/src/shared/hooks/use-panel-app-scroll-restoration.ts +107 -0
- package/src/shared/hooks/use-scroll-restoration.test.tsx +62 -0
- package/src/shared/hooks/use-scroll-restoration.ts +61 -0
- package/src/shared/lib/api/index.ts +1 -0
- package/src/shared/lib/api/ncp-session-query-cache.utils.ts +1 -0
- package/src/shared/lib/api/ncp-session.types.ts +3 -0
- package/src/shared/lib/api/types.ts +1 -0
- package/src/shared/lib/api/utils/mcp-connection.utils.ts +10 -0
- package/src/shared/lib/api/utils/ncp-session.utils.ts +3 -0
- package/src/shared/lib/i18n/locales/en-US/chat.json +10 -5
- package/src/shared/lib/i18n/locales/en-US/core.json +43 -0
- package/src/shared/lib/i18n/locales/en-US/cron.json +4 -2
- package/src/shared/lib/i18n/locales/en-US/doc-browser.json +1 -0
- package/src/shared/lib/i18n/locales/en-US/marketplace.json +29 -1
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +10 -5
- package/src/shared/lib/i18n/locales/zh-CN/core.json +43 -0
- package/src/shared/lib/i18n/locales/zh-CN/cron.json +4 -2
- package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +1 -0
- package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +29 -1
- package/src/shared/lib/navigation-history/index.ts +1 -0
- package/src/shared/lib/navigation-history/scroll-restoration.manager.test.ts +37 -0
- package/src/shared/lib/navigation-history/scroll-restoration.manager.ts +60 -0
- package/dist/assets/desktop-update-config-BzZFhMv9.js +0 -1
- package/dist/assets/doc-browser-DB22tQKU.js +0 -1
- package/dist/assets/doc-browser-sJjw9fl5.js +0 -1
- package/dist/assets/index-Dc_ZMkmo.css +0 -1
- package/dist/assets/index-N1HnesBp.js +0 -109
- package/dist/assets/loader-circle-BlPXxgDB.js +0 -1
- package/dist/assets/mcp-marketplace-page-BIgFTbMq.js +0 -9
- package/dist/assets/mcp-marketplace-page-CZN4ovdK.js +0 -1
- package/dist/assets/model-config-page-CnSm7kfa.js +0 -1
- package/dist/assets/plus-DVXol9Q5.js +0 -1
- package/dist/assets/privacy-settings-page-DVn2HU7W.js +0 -1
- package/dist/assets/providers-config-page-CzHJS3eg.js +0 -1
- package/dist/assets/react-DCVbGroo.js +0 -8
- package/dist/assets/remote-B2lsJF1c.js +0 -1
- package/dist/assets/runtime-config-page-D69buyGp.js +0 -1
- package/dist/assets/security-config-Dcu9C4OL.js +0 -1
- package/dist/assets/skeleton-QoWydNLp.js +0 -1
- package/dist/assets/x-BXvAzpjb.js +0 -1
|
@@ -1,103 +1,297 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
CalendarClock,
|
|
4
|
+
Check,
|
|
5
|
+
Clock,
|
|
6
|
+
MoreVertical,
|
|
7
|
+
Play,
|
|
8
|
+
TriangleAlert,
|
|
9
|
+
} from "lucide-react";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
CronJobDetailDialog,
|
|
13
|
+
useCronJobActions,
|
|
14
|
+
} from "@/features/cron";
|
|
2
15
|
import type { CronJobView } from "@/shared/lib/api";
|
|
3
16
|
import { Button } from "@/shared/components/ui/button";
|
|
4
|
-
import {
|
|
5
|
-
|
|
17
|
+
import {
|
|
18
|
+
Popover,
|
|
19
|
+
PopoverContent,
|
|
20
|
+
PopoverTrigger,
|
|
21
|
+
} from "@/shared/components/ui/popover";
|
|
22
|
+
import { Switch } from "@/shared/components/ui/switch";
|
|
23
|
+
import {
|
|
24
|
+
Tooltip,
|
|
25
|
+
TooltipContent,
|
|
26
|
+
TooltipProvider,
|
|
27
|
+
TooltipTrigger,
|
|
28
|
+
} from "@/shared/components/ui/tooltip";
|
|
6
29
|
import {
|
|
7
30
|
describeCronSchedule,
|
|
8
|
-
|
|
9
|
-
formatCronDate,
|
|
31
|
+
formatRelativeTime,
|
|
10
32
|
} from "@/shared/lib/cron";
|
|
11
33
|
import { t } from "@/shared/lib/i18n";
|
|
34
|
+
import { cn } from "@/shared/lib/utils";
|
|
35
|
+
|
|
36
|
+
function CronJobStatus({ job }: { readonly job: CronJobView }) {
|
|
37
|
+
const needsAttention = job.state.lastStatus === "error";
|
|
38
|
+
const label = needsAttention
|
|
39
|
+
? t("cronNeedsAttention")
|
|
40
|
+
: job.enabled
|
|
41
|
+
? t("cronRunningNormally")
|
|
42
|
+
: t("cronPaused");
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<span
|
|
46
|
+
className={cn(
|
|
47
|
+
"inline-flex shrink-0 items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-medium",
|
|
48
|
+
needsAttention
|
|
49
|
+
? "bg-red-50 text-red-700"
|
|
50
|
+
: job.enabled
|
|
51
|
+
? "bg-emerald-50 text-emerald-700"
|
|
52
|
+
: "bg-muted text-muted-foreground",
|
|
53
|
+
)}
|
|
54
|
+
>
|
|
55
|
+
<span
|
|
56
|
+
className={cn(
|
|
57
|
+
"h-1.5 w-1.5 rounded-full",
|
|
58
|
+
needsAttention
|
|
59
|
+
? "bg-red-500"
|
|
60
|
+
: job.enabled
|
|
61
|
+
? "bg-emerald-500"
|
|
62
|
+
: "bg-muted-foreground/50",
|
|
63
|
+
)}
|
|
64
|
+
/>
|
|
65
|
+
{label}
|
|
66
|
+
</span>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function SessionCronJobCard({
|
|
71
|
+
cronActions,
|
|
72
|
+
job,
|
|
73
|
+
onOpenDetails,
|
|
74
|
+
}: {
|
|
75
|
+
readonly cronActions: ReturnType<typeof useCronJobActions>;
|
|
76
|
+
readonly job: CronJobView;
|
|
77
|
+
readonly onOpenDetails: (job: CronJobView) => void;
|
|
78
|
+
}) {
|
|
79
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
80
|
+
const isPending = cronActions.isPending(job);
|
|
81
|
+
const needsAttention = job.state.lastStatus === "error";
|
|
82
|
+
const StatusIcon = needsAttention ? TriangleAlert : Check;
|
|
83
|
+
const jobLabel = job.name || job.id;
|
|
12
84
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
85
|
+
return (
|
|
86
|
+
<article className="rounded-2xl border border-border/60 bg-background px-3.5 py-3 shadow-sm transition-colors hover:bg-muted/20 focus-within:bg-muted/20">
|
|
87
|
+
<div className="flex items-start gap-3">
|
|
88
|
+
<span
|
|
89
|
+
className={cn(
|
|
90
|
+
"mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-xl",
|
|
91
|
+
needsAttention
|
|
92
|
+
? "bg-amber-50 text-amber-600"
|
|
93
|
+
: job.enabled
|
|
94
|
+
? "bg-emerald-50 text-emerald-600"
|
|
95
|
+
: "bg-muted text-muted-foreground",
|
|
96
|
+
)}
|
|
97
|
+
>
|
|
98
|
+
<StatusIcon className="h-3.5 w-3.5" aria-hidden="true" />
|
|
99
|
+
</span>
|
|
100
|
+
<div className="min-w-0 flex-1">
|
|
101
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
102
|
+
<h3 className="min-w-0 flex-1 truncate text-sm font-semibold text-foreground">
|
|
103
|
+
{jobLabel}
|
|
104
|
+
</h3>
|
|
105
|
+
<CronJobStatus job={job} />
|
|
106
|
+
</div>
|
|
107
|
+
<div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-[11px] text-muted-foreground">
|
|
108
|
+
<span className="inline-flex items-center gap-1">
|
|
109
|
+
<CalendarClock className="h-3.5 w-3.5" aria-hidden="true" />
|
|
110
|
+
{describeCronSchedule(job)}
|
|
111
|
+
</span>
|
|
112
|
+
{job.enabled && job.state.nextRunAt ? (
|
|
113
|
+
<span className="inline-flex items-center gap-1">
|
|
114
|
+
<Clock className="h-3.5 w-3.5" aria-hidden="true" />
|
|
115
|
+
{t("cronNextRun")} {formatRelativeTime(job.state.nextRunAt)}
|
|
116
|
+
</span>
|
|
117
|
+
) : null}
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
<TooltipProvider delayDuration={250}>
|
|
121
|
+
<Tooltip>
|
|
122
|
+
<TooltipTrigger asChild>
|
|
123
|
+
<span>
|
|
124
|
+
<Switch
|
|
125
|
+
checked={job.enabled}
|
|
126
|
+
disabled={isPending}
|
|
127
|
+
onCheckedChange={(enabled) => {
|
|
128
|
+
void cronActions.toggleJob(job, enabled);
|
|
129
|
+
}}
|
|
130
|
+
aria-label={job.enabled ? t("cronDisable") : t("cronEnable")}
|
|
131
|
+
/>
|
|
132
|
+
</span>
|
|
133
|
+
</TooltipTrigger>
|
|
134
|
+
<TooltipContent>
|
|
135
|
+
{job.enabled ? t("cronDisable") : t("cronEnable")}
|
|
136
|
+
</TooltipContent>
|
|
137
|
+
</Tooltip>
|
|
138
|
+
</TooltipProvider>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<p className="mt-3 line-clamp-2 text-xs leading-5 text-muted-foreground">
|
|
142
|
+
{job.payload.message}
|
|
143
|
+
</p>
|
|
144
|
+
{!job.enabled ? (
|
|
145
|
+
<p className="mt-2 text-[11px] text-muted-foreground">
|
|
146
|
+
{t("cronToggleHint")}
|
|
147
|
+
</p>
|
|
148
|
+
) : null}
|
|
149
|
+
{job.state.lastError ? (
|
|
150
|
+
<p className="mt-2 line-clamp-2 text-[11px] leading-5 text-red-700">
|
|
151
|
+
{job.state.lastError}
|
|
152
|
+
</p>
|
|
153
|
+
) : null}
|
|
154
|
+
|
|
155
|
+
<div className="mt-3 flex items-center justify-between border-t border-border/50 pt-2.5">
|
|
156
|
+
<Button
|
|
157
|
+
type="button"
|
|
158
|
+
variant="ghost"
|
|
159
|
+
size="sm"
|
|
160
|
+
className="h-7 px-2 text-xs"
|
|
161
|
+
onClick={() => onOpenDetails(job)}
|
|
162
|
+
>
|
|
163
|
+
{t("cronEditDetails")}
|
|
164
|
+
</Button>
|
|
165
|
+
<TooltipProvider delayDuration={250}>
|
|
166
|
+
<Popover open={menuOpen} onOpenChange={setMenuOpen}>
|
|
167
|
+
<Tooltip>
|
|
168
|
+
<TooltipTrigger asChild>
|
|
169
|
+
<PopoverTrigger asChild>
|
|
170
|
+
<Button
|
|
171
|
+
type="button"
|
|
172
|
+
variant="ghost"
|
|
173
|
+
size="icon"
|
|
174
|
+
className="h-7 w-7"
|
|
175
|
+
disabled={isPending}
|
|
176
|
+
aria-label={`${t("cronMoreActions")} ${jobLabel}`}
|
|
177
|
+
>
|
|
178
|
+
<MoreVertical className="h-4 w-4" />
|
|
179
|
+
</Button>
|
|
180
|
+
</PopoverTrigger>
|
|
181
|
+
</TooltipTrigger>
|
|
182
|
+
<TooltipContent>{t("cronMoreActions")}</TooltipContent>
|
|
183
|
+
</Tooltip>
|
|
184
|
+
<PopoverContent align="end" sideOffset={4} className="w-40 p-1">
|
|
185
|
+
<button
|
|
186
|
+
type="button"
|
|
187
|
+
disabled={isPending}
|
|
188
|
+
onClick={() => {
|
|
189
|
+
setMenuOpen(false);
|
|
190
|
+
void cronActions.runJob(job);
|
|
191
|
+
}}
|
|
192
|
+
className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm text-foreground transition-colors hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50"
|
|
193
|
+
>
|
|
194
|
+
<Play className="h-3.5 w-3.5 text-muted-foreground" />
|
|
195
|
+
{t("cronRunNow")}
|
|
196
|
+
</button>
|
|
197
|
+
<button
|
|
198
|
+
type="button"
|
|
199
|
+
disabled={isPending}
|
|
200
|
+
onClick={() => {
|
|
201
|
+
setMenuOpen(false);
|
|
202
|
+
void cronActions.deleteJob(job);
|
|
203
|
+
}}
|
|
204
|
+
className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm text-red-600 transition-colors hover:bg-red-50 disabled:cursor-not-allowed disabled:opacity-50"
|
|
205
|
+
>
|
|
206
|
+
{t("delete")}
|
|
207
|
+
</button>
|
|
208
|
+
</PopoverContent>
|
|
209
|
+
</Popover>
|
|
210
|
+
</TooltipProvider>
|
|
211
|
+
</div>
|
|
212
|
+
</article>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
16
215
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
216
|
+
export function SessionCronJobContent({
|
|
217
|
+
jobs,
|
|
218
|
+
isError = false,
|
|
219
|
+
isLoading = false,
|
|
220
|
+
onRetry,
|
|
221
|
+
}: {
|
|
222
|
+
readonly jobs: readonly CronJobView[];
|
|
223
|
+
readonly isError?: boolean;
|
|
224
|
+
readonly isLoading?: boolean;
|
|
225
|
+
readonly onRetry?: () => void;
|
|
226
|
+
}) {
|
|
227
|
+
const [selectedJobId, setSelectedJobId] = useState<string | null>(null);
|
|
228
|
+
const cronActions = useCronJobActions();
|
|
229
|
+
const selectedJob = jobs.find((job) => job.id === selectedJobId) ?? null;
|
|
28
230
|
|
|
29
231
|
return (
|
|
30
232
|
<div className="h-full overflow-y-auto custom-scrollbar px-4 py-4">
|
|
31
233
|
<div className="mb-4">
|
|
32
|
-
<div className="text-sm font-semibold text-
|
|
234
|
+
<div className="text-sm font-semibold text-foreground">
|
|
33
235
|
{t("chatWorkspaceSessionCronJobs")}
|
|
34
236
|
</div>
|
|
35
|
-
<div className="mt-1 text-xs text-
|
|
237
|
+
<div className="mt-1 text-xs text-muted-foreground">
|
|
36
238
|
{t("cronTotalLabel")}: {jobs.length}
|
|
37
239
|
</div>
|
|
38
240
|
</div>
|
|
39
|
-
{
|
|
40
|
-
<div className="py-8 text-center text-sm text-
|
|
241
|
+
{isLoading ? (
|
|
242
|
+
<div className="py-8 text-center text-sm text-muted-foreground">
|
|
243
|
+
{t("cronLoading")}
|
|
244
|
+
</div>
|
|
245
|
+
) : isError ? (
|
|
246
|
+
<div className="py-8 text-center text-sm text-muted-foreground">
|
|
247
|
+
<p>{t("cronLoadFailed")}</p>
|
|
248
|
+
{onRetry ? (
|
|
249
|
+
<Button
|
|
250
|
+
type="button"
|
|
251
|
+
variant="ghost"
|
|
252
|
+
size="sm"
|
|
253
|
+
className="mt-2"
|
|
254
|
+
onClick={onRetry}
|
|
255
|
+
>
|
|
256
|
+
{t("cronRetry")}
|
|
257
|
+
</Button>
|
|
258
|
+
) : null}
|
|
259
|
+
</div>
|
|
260
|
+
) : jobs.length === 0 ? (
|
|
261
|
+
<div className="py-8 text-center text-sm text-muted-foreground">
|
|
41
262
|
{t("chatWorkspaceCronJobEmpty")}
|
|
42
263
|
</div>
|
|
43
264
|
) : (
|
|
44
|
-
<div className="space-y-
|
|
265
|
+
<div className="space-y-2.5">
|
|
45
266
|
{jobs.map((job) => (
|
|
46
|
-
<
|
|
267
|
+
<SessionCronJobCard
|
|
47
268
|
key={job.id}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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("cronSessionLabel")}:</span>{" "}
|
|
90
|
-
{describeCronSession(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>
|
|
269
|
+
cronActions={cronActions}
|
|
270
|
+
job={job}
|
|
271
|
+
onOpenDetails={(item) => setSelectedJobId(item.id)}
|
|
272
|
+
/>
|
|
97
273
|
))}
|
|
98
274
|
</div>
|
|
99
275
|
)}
|
|
100
|
-
<
|
|
276
|
+
<CronJobDetailDialog
|
|
277
|
+
job={selectedJob}
|
|
278
|
+
open={Boolean(selectedJob)}
|
|
279
|
+
onOpenChange={(open) => {
|
|
280
|
+
if (!open) {
|
|
281
|
+
setSelectedJobId(null);
|
|
282
|
+
}
|
|
283
|
+
}}
|
|
284
|
+
onDelete={(job) => {
|
|
285
|
+
void cronActions.deleteJob(job, () => setSelectedJobId(null));
|
|
286
|
+
}}
|
|
287
|
+
onRun={(job) => {
|
|
288
|
+
void cronActions.runJob(job);
|
|
289
|
+
}}
|
|
290
|
+
onToggle={(job, enabled) => {
|
|
291
|
+
void cronActions.toggleJob(job, enabled);
|
|
292
|
+
}}
|
|
293
|
+
/>
|
|
294
|
+
<cronActions.ConfirmDialog />
|
|
101
295
|
</div>
|
|
102
296
|
);
|
|
103
297
|
}
|
|
@@ -62,6 +62,11 @@ export function useChatConversationWorkspaceState(
|
|
|
62
62
|
activeSideChatDraft,
|
|
63
63
|
workspaceFileTabs,
|
|
64
64
|
sessionCronJobs,
|
|
65
|
+
sessionCronJobsError: cronQuery.isError,
|
|
66
|
+
sessionCronJobsLoading: cronQuery.isLoading,
|
|
67
|
+
retrySessionCronJobs: () => {
|
|
68
|
+
void cronQuery.refetch();
|
|
69
|
+
},
|
|
65
70
|
showWorkspacePanel:
|
|
66
71
|
snapshot.workspacePanelParentKey === sessionKey &&
|
|
67
72
|
snapshot.activeWorkspacePanelKind != null,
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { appQueryClient } from "@/app-query-client";
|
|
3
|
+
import { ChatThreadManager } from "@/features/chat/managers/chat-thread.manager";
|
|
4
|
+
import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
|
|
5
|
+
import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
|
|
6
|
+
import type * as SharedApi from "@/shared/lib/api";
|
|
7
|
+
|
|
8
|
+
const { deleteNcpSessionMock, deleteSummaryMock, toast } = vi.hoisted(() => ({
|
|
9
|
+
deleteNcpSessionMock: vi.fn(),
|
|
10
|
+
deleteSummaryMock: vi.fn(),
|
|
11
|
+
toast: { success: vi.fn(), error: vi.fn() },
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock("@/shared/lib/api", async (importOriginal) => {
|
|
15
|
+
const actual = await importOriginal<typeof SharedApi>();
|
|
16
|
+
return {
|
|
17
|
+
...actual,
|
|
18
|
+
deleteNcpSession: deleteNcpSessionMock,
|
|
19
|
+
deleteNcpSessionSummaryInQueryClient: deleteSummaryMock,
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
vi.mock("sonner", () => ({ toast }));
|
|
24
|
+
|
|
25
|
+
function createUiManager() {
|
|
26
|
+
return {
|
|
27
|
+
confirm: vi.fn(async () => true),
|
|
28
|
+
goToChatRoot: vi.fn(),
|
|
29
|
+
} as unknown as ConstructorParameters<typeof ChatThreadManager>[0];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
deleteNcpSessionMock.mockReset();
|
|
34
|
+
deleteNcpSessionMock.mockResolvedValue({
|
|
35
|
+
deleted: true,
|
|
36
|
+
sessionId: "parent-session-1",
|
|
37
|
+
});
|
|
38
|
+
deleteSummaryMock.mockReset();
|
|
39
|
+
toast.success.mockReset();
|
|
40
|
+
toast.error.mockReset();
|
|
41
|
+
useChatSessionListStore.setState({
|
|
42
|
+
snapshot: {
|
|
43
|
+
...useChatSessionListStore.getInitialState().snapshot,
|
|
44
|
+
selectedSessionKey: "parent-session-1",
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
useChatThreadStore.setState({
|
|
48
|
+
snapshot: {
|
|
49
|
+
...useChatThreadStore.getInitialState().snapshot,
|
|
50
|
+
sessionKey: "parent-session-1",
|
|
51
|
+
canDeleteSession: true,
|
|
52
|
+
childSessionTabs: [
|
|
53
|
+
{
|
|
54
|
+
sessionKey: "child-session-1",
|
|
55
|
+
parentSessionKey: "parent-session-1",
|
|
56
|
+
label: "Child Session 1",
|
|
57
|
+
agentId: "reviewer",
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("ChatThreadManager deletion", () => {
|
|
65
|
+
it("clears the current thread and confirms the deletion", async () => {
|
|
66
|
+
const uiManager = createUiManager();
|
|
67
|
+
const removeQueries = vi
|
|
68
|
+
.spyOn(appQueryClient, "removeQueries")
|
|
69
|
+
.mockImplementation(async () => undefined);
|
|
70
|
+
const manager = new ChatThreadManager(
|
|
71
|
+
uiManager,
|
|
72
|
+
{} as ConstructorParameters<typeof ChatThreadManager>[1],
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
await manager.deleteSession();
|
|
76
|
+
|
|
77
|
+
expect(useChatThreadStore.getState().snapshot.sessionKey).toBeNull();
|
|
78
|
+
expect(deleteSummaryMock).toHaveBeenCalledWith(
|
|
79
|
+
appQueryClient,
|
|
80
|
+
"parent-session-1",
|
|
81
|
+
);
|
|
82
|
+
expect(removeQueries).toHaveBeenCalledWith({
|
|
83
|
+
queryKey: ["ncp-session-messages", "parent-session-1"],
|
|
84
|
+
});
|
|
85
|
+
expect(uiManager.goToChatRoot).toHaveBeenCalledWith({ replace: true });
|
|
86
|
+
expect(toast.success).toHaveBeenCalledWith("Session deleted.");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("preserves the current thread when deleting another session", async () => {
|
|
90
|
+
const uiManager = createUiManager();
|
|
91
|
+
const manager = new ChatThreadManager(
|
|
92
|
+
uiManager,
|
|
93
|
+
{} as ConstructorParameters<typeof ChatThreadManager>[1],
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
await manager.deleteSession("other-session-1");
|
|
97
|
+
|
|
98
|
+
expect(deleteNcpSessionMock).toHaveBeenCalledWith("other-session-1");
|
|
99
|
+
expect(useChatThreadStore.getState().snapshot.sessionKey).toBe(
|
|
100
|
+
"parent-session-1",
|
|
101
|
+
);
|
|
102
|
+
expect(uiManager.goToChatRoot).not.toHaveBeenCalled();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("keeps the session visible and reports a deletion error", async () => {
|
|
106
|
+
deleteNcpSessionMock.mockRejectedValueOnce(
|
|
107
|
+
new Error("network unavailable"),
|
|
108
|
+
);
|
|
109
|
+
const uiManager = createUiManager();
|
|
110
|
+
const manager = new ChatThreadManager(
|
|
111
|
+
uiManager,
|
|
112
|
+
{} as ConstructorParameters<typeof ChatThreadManager>[1],
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
await manager.deleteSession();
|
|
116
|
+
|
|
117
|
+
expect(useChatThreadStore.getState().snapshot.sessionKey).toBe(
|
|
118
|
+
"parent-session-1",
|
|
119
|
+
);
|
|
120
|
+
expect(uiManager.goToChatRoot).not.toHaveBeenCalled();
|
|
121
|
+
expect(toast.error).toHaveBeenCalledWith(
|
|
122
|
+
"Could not delete session: network unavailable",
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { appQueryClient } from '@/app-query-client';
|
|
3
2
|
import { ChatThreadManager } from '@/features/chat/managers/chat-thread.manager';
|
|
4
3
|
import { useChatSessionListStore } from '@/features/chat/stores/chat-session-list.store';
|
|
5
4
|
import { useChatThreadStore } from '@/features/chat/stores/chat-thread.store';
|
|
@@ -841,39 +840,3 @@ describe('ChatThreadManager visible workspace selection', () => {
|
|
|
841
840
|
expect(sessionListManager.markVisibleWorkspaceChildRead).toHaveBeenCalledWith(tab);
|
|
842
841
|
});
|
|
843
842
|
});
|
|
844
|
-
|
|
845
|
-
describe('ChatThreadManager deletion', () => {
|
|
846
|
-
it('clears thread state while leaving route-derived selection to navigation', async () => {
|
|
847
|
-
const removeQueries = vi.spyOn(appQueryClient, 'removeQueries').mockImplementation(async () => undefined);
|
|
848
|
-
const uiManager = {
|
|
849
|
-
goToSession: vi.fn(),
|
|
850
|
-
goToChatRoot: vi.fn(),
|
|
851
|
-
goToProviders: vi.fn(),
|
|
852
|
-
confirm: vi.fn(async () => true),
|
|
853
|
-
} as unknown as ConstructorParameters<typeof ChatThreadManager>[0];
|
|
854
|
-
const sessionListManager = {} as ConstructorParameters<typeof ChatThreadManager>[1];
|
|
855
|
-
const manager = new ChatThreadManager(uiManager, sessionListManager);
|
|
856
|
-
|
|
857
|
-
await manager.deleteSession();
|
|
858
|
-
|
|
859
|
-
expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBe('parent-session-1');
|
|
860
|
-
expect(useChatThreadStore.getState().snapshot).toMatchObject({
|
|
861
|
-
sessionKey: null,
|
|
862
|
-
canDeleteSession: false,
|
|
863
|
-
workspacePanelParentKey: null,
|
|
864
|
-
childSessionTabs: [],
|
|
865
|
-
activeChildSessionKey: null,
|
|
866
|
-
workspaceFileTabs: [],
|
|
867
|
-
activeWorkspaceFileKey: null,
|
|
868
|
-
workspaceNavigationHistory: [],
|
|
869
|
-
workspaceNavigationHistoryIndex: 0,
|
|
870
|
-
});
|
|
871
|
-
expect(deleteSummaryMock).toHaveBeenCalledWith(appQueryClient, 'parent-session-1');
|
|
872
|
-
expect(removeQueries).toHaveBeenCalledWith({
|
|
873
|
-
queryKey: ['ncp-session-messages', 'parent-session-1'],
|
|
874
|
-
});
|
|
875
|
-
expect(uiManager.goToChatRoot).toHaveBeenCalledWith({ replace: true });
|
|
876
|
-
|
|
877
|
-
removeQueries.mockRestore();
|
|
878
|
-
});
|
|
879
|
-
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { appQueryClient } from '@/app-query-client';
|
|
2
2
|
import { deleteNcpSession as deleteNcpSessionApi, deleteNcpSessionSummaryInQueryClient } from '@/shared/lib/api';
|
|
3
|
+
import { toast } from 'sonner';
|
|
3
4
|
import type {
|
|
4
5
|
ChatFileOpenActionViewModel,
|
|
5
6
|
ChatUiShowContentRequest,
|
|
@@ -17,6 +18,7 @@ import type {
|
|
|
17
18
|
ChatWorkspaceFileTab,
|
|
18
19
|
} from '@/features/chat/stores/chat-thread.store';
|
|
19
20
|
import { useChatThreadStore } from '@/features/chat/stores/chat-thread.store';
|
|
21
|
+
import { createDeletedChatThreadStatePatch } from '@/features/chat/features/session/utils/chat-session-deletion.utils';
|
|
20
22
|
import {
|
|
21
23
|
normalizeChatWorkspaceExplorerWidth,
|
|
22
24
|
normalizeChatWorkspacePanelWidth,
|
|
@@ -80,27 +82,7 @@ export class ChatThreadManager {
|
|
|
80
82
|
};
|
|
81
83
|
|
|
82
84
|
private clearDeletedSessionState = () => {
|
|
83
|
-
useChatThreadStore.getState().setSnapshot(
|
|
84
|
-
sessionKey: null,
|
|
85
|
-
sessionTypeLabel: null,
|
|
86
|
-
agentId: null,
|
|
87
|
-
sessionDisplayName: undefined,
|
|
88
|
-
draftProjectRoot: null,
|
|
89
|
-
sessionProjectName: null,
|
|
90
|
-
canDeleteSession: false,
|
|
91
|
-
parentSessionKey: null,
|
|
92
|
-
parentSessionLabel: null,
|
|
93
|
-
workspacePanelParentKey: null,
|
|
94
|
-
activeWorkspacePanelKind: null,
|
|
95
|
-
childSessionTabs: [],
|
|
96
|
-
activeChildSessionKey: null,
|
|
97
|
-
activeSideChatDraft: null,
|
|
98
|
-
workspaceFileTabs: [],
|
|
99
|
-
activeWorkspaceFileKey: null,
|
|
100
|
-
closedWorkspaceTabEntries: [],
|
|
101
|
-
workspaceNavigationHistory: [],
|
|
102
|
-
workspaceNavigationHistoryIndex: 0,
|
|
103
|
-
});
|
|
85
|
+
useChatThreadStore.getState().setSnapshot(createDeletedChatThreadStatePatch());
|
|
104
86
|
};
|
|
105
87
|
|
|
106
88
|
private resolveWorkspaceParentSessionKey = (): string | null => {
|
|
@@ -565,11 +547,12 @@ export class ChatThreadManager {
|
|
|
565
547
|
this.uiManager.goToSession(resolvedParentSessionKey);
|
|
566
548
|
};
|
|
567
549
|
|
|
568
|
-
deleteSession = async () => {
|
|
550
|
+
deleteSession = async (rawSessionKey?: string) => {
|
|
569
551
|
const {
|
|
570
552
|
snapshot: { selectedSessionKey },
|
|
571
553
|
} = useChatSessionListStore.getState();
|
|
572
|
-
|
|
554
|
+
const sessionKey = rawSessionKey?.trim() || selectedSessionKey;
|
|
555
|
+
if (!sessionKey) {
|
|
573
556
|
return;
|
|
574
557
|
}
|
|
575
558
|
const confirmed = await this.uiManager.confirm({
|
|
@@ -582,13 +565,19 @@ export class ChatThreadManager {
|
|
|
582
565
|
}
|
|
583
566
|
useChatThreadStore.getState().setSnapshot({ isDeletePending: true });
|
|
584
567
|
try {
|
|
585
|
-
await deleteNcpSessionApi(
|
|
586
|
-
deleteNcpSessionSummaryInQueryClient(appQueryClient,
|
|
568
|
+
await deleteNcpSessionApi(sessionKey);
|
|
569
|
+
deleteNcpSessionSummaryInQueryClient(appQueryClient, sessionKey);
|
|
587
570
|
appQueryClient.removeQueries({
|
|
588
|
-
queryKey: ['ncp-session-messages',
|
|
571
|
+
queryKey: ['ncp-session-messages', sessionKey],
|
|
589
572
|
});
|
|
590
|
-
|
|
591
|
-
|
|
573
|
+
toast.success(t('chatDeleteSessionSucceeded'));
|
|
574
|
+
if (sessionKey === selectedSessionKey) {
|
|
575
|
+
this.clearDeletedSessionState();
|
|
576
|
+
this.uiManager.goToChatRoot({ replace: true });
|
|
577
|
+
}
|
|
578
|
+
} catch (error) {
|
|
579
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
580
|
+
toast.error(`${t('chatDeleteSessionFailed')}: ${message}`);
|
|
592
581
|
} finally {
|
|
593
582
|
useChatThreadStore.getState().setSnapshot({ isDeletePending: false });
|
|
594
583
|
}
|
|
@@ -52,6 +52,16 @@ vi.mock("@/features/cron/hooks/use-cron-jobs", () => ({
|
|
|
52
52
|
useToggleCronJob: () => ({ mutate: mocks.toggleJob }),
|
|
53
53
|
}));
|
|
54
54
|
|
|
55
|
+
vi.mock("@/features/cron/hooks/use-cron-job-actions", () => ({
|
|
56
|
+
useCronJobActions: () => ({
|
|
57
|
+
ConfirmDialog: () => null,
|
|
58
|
+
deleteJob: mocks.deleteJob,
|
|
59
|
+
isPending: () => false,
|
|
60
|
+
runJob: mocks.runJob,
|
|
61
|
+
toggleJob: mocks.toggleJob,
|
|
62
|
+
}),
|
|
63
|
+
}));
|
|
64
|
+
|
|
55
65
|
vi.mock("@/shared/hooks/use-confirm-dialog", () => ({
|
|
56
66
|
useConfirmDialog: () => ({
|
|
57
67
|
confirm: mocks.confirm,
|
|
@@ -268,7 +278,7 @@ describe("CronConfig", () => {
|
|
|
268
278
|
renderCronConfig();
|
|
269
279
|
|
|
270
280
|
await user.click(
|
|
271
|
-
screen.getByRole("button", { name: "
|
|
281
|
+
screen.getByRole("button", { name: "查看任务详情 Agent 产品雷达" }),
|
|
272
282
|
);
|
|
273
283
|
expect(screen.getByRole("dialog")).toBeTruthy();
|
|
274
284
|
expect(screen.getByText("当前系统只保存最近一次执行快照。")).toBeTruthy();
|
|
@@ -283,10 +293,11 @@ describe("CronConfig", () => {
|
|
|
283
293
|
renderCronConfig();
|
|
284
294
|
|
|
285
295
|
await user.click(screen.getAllByRole("switch")[0]);
|
|
286
|
-
expect(mocks.toggleJob).toHaveBeenCalledWith(
|
|
287
|
-
id: "agent-radar",
|
|
288
|
-
|
|
289
|
-
|
|
296
|
+
expect(mocks.toggleJob).toHaveBeenCalledWith(
|
|
297
|
+
expect.objectContaining({ id: "agent-radar" }),
|
|
298
|
+
false,
|
|
299
|
+
expect.any(Function),
|
|
300
|
+
);
|
|
290
301
|
expect(
|
|
291
302
|
screen
|
|
292
303
|
.getByRole("button", { name: "展开任务 Agent 产品雷达" })
|