@nextclaw/ui 0.20.1 → 0.20.3
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 +30 -0
- package/dist/assets/{api-BBFMpRGl.js → api-iMkU_o5P.js} +3 -3
- package/dist/assets/{appearance-settings-page-C1Erayzl.js → appearance-settings-page-CU5k4b-M.js} +1 -1
- package/dist/assets/{book-open-BhTIpBKg.js → book-open-nD8gdMhA.js} +1 -1
- package/dist/assets/channels-list-page-Drlt62kY.js +8 -0
- package/dist/assets/{config-split-page-By7p-4X-.js → config-split-page-BtJZp1E5.js} +1 -1
- package/dist/assets/{confirm-dialog-xpl1Yl7r.js → confirm-dialog-k2-Jq_x3.js} +1 -1
- package/dist/assets/{createLucideIcon-B_8khHn8.js → createLucideIcon-DxlwKLr-.js} +1 -1
- package/dist/assets/{desktop-update-config-CGFfa7-Z.js → desktop-update-config-BiZmy7FC.js} +1 -1
- package/dist/assets/{dist-CXN62DSs.js → dist-Cy_e4kNb.js} +1 -1
- package/dist/assets/{dist-DdE-kCbi.js → dist-m08DUswB.js} +1 -1
- package/dist/assets/doc-browser-CPsPHh12.js +1 -0
- package/dist/assets/{doc-browser-context-B8NCuMv5.js → doc-browser-context-jiy-O4F8.js} +1 -1
- package/dist/assets/doc-browser-npOvWtgf.js +1 -0
- package/dist/assets/{extensions-CMctxyTf.js → extensions-CIb17__o.js} +1 -1
- package/dist/assets/{form-actions-BFgTdxqI.js → form-actions-DGf5zUg_.js} +1 -1
- package/dist/assets/index-C86hxgv5.css +1 -0
- package/dist/assets/index-CYBXR7zN.js +109 -0
- package/dist/assets/loader-circle-BlLuRFVW.js +1 -0
- package/dist/assets/mcp-marketplace-page-B12JNh7Q.js +1 -0
- package/dist/assets/mcp-marketplace-page-kLWW6kzt.js +9 -0
- package/dist/assets/model-config-page-DSeAjVT_.js +1 -0
- package/dist/assets/{navigation-link-BwWrm4k1.js → navigation-link-DAfwFCj4.js} +1 -1
- package/dist/assets/plus-D4LtWtTk.js +1 -0
- package/dist/assets/privacy-settings-page-tN0Mt15x.js +1 -0
- package/dist/assets/providers-config-page-CgPrXR1u.js +1 -0
- package/dist/assets/react-8iBFECZE.js +8 -0
- package/dist/assets/{refresh-cw-B7NmPJ_Z.js → refresh-cw-D_Eibznw.js} +1 -1
- package/dist/assets/remote-Derr_AQp.js +1 -0
- package/dist/assets/{rotate-cw-B0ajMaHu.js → rotate-cw-QQjmoWuq.js} +1 -1
- package/dist/assets/runtime-config-page-Crq96Jjq.js +1 -0
- package/dist/assets/{save-DWnYYpgQ.js → save-W2xthJJ2.js} +1 -1
- package/dist/assets/{search-D6pX7FC-.js → search-Vih1R2BD.js} +1 -1
- package/dist/assets/{search-config-page-Cx3o_675.js → search-config-page-CtBcL2uc.js} +1 -1
- package/dist/assets/{secrets-config-page-tyFl7Rvt.js → secrets-config-page-Q6XfIj0h.js} +2 -2
- package/dist/assets/security-config-CE9rkQvq.js +1 -0
- package/dist/assets/{select-qEtFTWB5.js → select-lNdozU4o.js} +1 -1
- package/dist/assets/skeleton-B__aTcly.js +1 -0
- package/dist/assets/{tag-chip-vcjAYdV_.js → tag-chip-gdaU9bE7.js} +1 -1
- package/dist/assets/x-CWdWBu_U.js +1 -0
- package/dist/index.html +21 -21
- package/package.json +7 -7
- package/src/app/components/layout/sidebar.tsx +7 -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-read-state.test.tsx +4 -3
- package/src/features/chat/components/layout/__tests__/chat-sidebar-session-area.test.tsx +27 -6
- package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +17 -17
- package/src/features/chat/components/layout/chat-page-shell.tsx +19 -2
- package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +33 -5
- package/src/features/chat/components/layout/chat-sidebar.tsx +31 -21
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-controller.test.tsx +41 -1
- package/src/features/chat/features/conversation/hooks/use-session-pending-input-actions.ts +22 -4
- package/src/features/chat/features/message/utils/__tests__/chat-message-timeline-compaction.utils.test.ts +2 -2
- package/src/features/chat/features/message/utils/__tests__/chat-message-trigger-details.utils.test.ts +35 -0
- package/src/features/chat/features/message/utils/chat-message-timeline.utils.ts +2 -2
- package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime-queue.test.tsx +50 -0
- package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-session-list-view.test.tsx +35 -6
- package/src/features/chat/features/ncp/hooks/use-ncp-session-list-view.ts +43 -20
- package/src/features/chat/features/ncp/hooks/use-ncp-session-queries.ts +15 -1
- package/src/features/chat/features/session/components/__tests__/chat-sidebar-project-groups.test.tsx +26 -2
- package/src/features/chat/features/session/components/__tests__/chat-sidebar-session-item.test.tsx +55 -27
- package/src/features/chat/features/session/components/chat-sidebar-context-card.tsx +60 -0
- package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +95 -54
- package/src/features/chat/features/session/components/chat-sidebar-session-entry.tsx +9 -8
- package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +141 -95
- package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +11 -2
- package/src/features/chat/features/session/hooks/use-chat-sidebar-context-counts.ts +40 -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__/chat-session-workspace-file-preview.test.tsx +16 -9
- package/src/features/chat/features/workspace/components/__tests__/session-cron-job-content.test.tsx +78 -0
- package/src/features/chat/features/workspace/components/__tests__/workspace-markdown-outline.test.tsx +128 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +3 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +65 -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/components/workspace-markdown-outline.tsx +156 -0
- package/src/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state.ts +5 -0
- 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/inbox/pages/inbox-page.tsx +18 -2
- package/src/features/marketplace/components/marketplace-page.tsx +7 -0
- package/src/features/panel-apps/pages/panel-app-main-page.tsx +8 -0
- package/src/features/settings/pages/__tests__/privacy-settings-page.test.tsx +14 -5
- package/src/features/settings/pages/privacy-settings-page.tsx +38 -2
- package/src/platforms/desktop/components/desktop-app-shell.tsx +10 -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/actions/icon-action-button.tsx +1 -8
- package/src/shared/components/ui/tooltip.tsx +16 -1
- package/src/shared/hooks/use-config.ts +11 -0
- 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/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 +9 -0
- package/src/shared/lib/api/utils/config.utils.ts +5 -0
- package/src/shared/lib/api/utils/ncp-session.utils.ts +3 -0
- package/src/shared/lib/i18n/locales/en-US/chat.json +15 -5
- package/src/shared/lib/i18n/locales/en-US/core.json +17 -9
- 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/zh-CN/chat.json +15 -5
- package/src/shared/lib/i18n/locales/zh-CN/core.json +17 -9
- 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/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/channels-list-page-OTCkjMOW.js +0 -8
- package/dist/assets/doc-browser-5xrHcQ2z.js +0 -1
- package/dist/assets/doc-browser-c2yO477-.js +0 -1
- package/dist/assets/index-DUCA8m2w.js +0 -109
- package/dist/assets/index-QrdUWHfm.css +0 -1
- package/dist/assets/loader-circle-CbdTrSp3.js +0 -1
- package/dist/assets/mcp-marketplace-page-DGve0muV.js +0 -9
- package/dist/assets/mcp-marketplace-page-_37VzG2q.js +0 -1
- package/dist/assets/model-config-page-DyC5H0bu.js +0 -1
- package/dist/assets/plus-DbeE4r-Q.js +0 -1
- package/dist/assets/privacy-settings-page--MOnhAZ6.js +0 -1
- package/dist/assets/providers-config-page-ChL3FUvK.js +0 -1
- package/dist/assets/react-DVMmnPWz.js +0 -8
- package/dist/assets/remote-DFoxj4ls.js +0 -1
- package/dist/assets/runtime-config-page-BQdT3nmd.js +0 -1
- package/dist/assets/security-config-CUTUm6JT.js +0 -1
- package/dist/assets/skeleton-7MYPzn8w.js +0 -1
- package/dist/assets/x-DThaS5ZL.js +0 -1
package/src/features/chat/features/workspace/components/overview/chat-session-token-usage.tsx
CHANGED
|
@@ -8,6 +8,10 @@ function formatTokenCount(value: number | null): string {
|
|
|
8
8
|
return value === null ? '—' : new Intl.NumberFormat().format(value);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
function formatCount(value: number | null): string {
|
|
12
|
+
return value === null ? '—' : new Intl.NumberFormat().format(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
11
15
|
function formatCacheHitRate(value: number | null): string {
|
|
12
16
|
return value === null
|
|
13
17
|
? '—'
|
|
@@ -119,6 +123,14 @@ export function ChatSessionTokenUsage({ sessionKey }: { sessionKey: string | nul
|
|
|
119
123
|
label={t('chatWorkspaceTokenUsageRuns')}
|
|
120
124
|
value={String(usage.runCount)}
|
|
121
125
|
/>
|
|
126
|
+
<TokenUsageMetric
|
|
127
|
+
label={t('chatWorkspaceTokenUsageModelCalls')}
|
|
128
|
+
value={formatCount(usage.modelCallCount)}
|
|
129
|
+
/>
|
|
130
|
+
<TokenUsageMetric
|
|
131
|
+
label={t('chatWorkspaceTokenUsageReportedModelCalls')}
|
|
132
|
+
value={formatCount(usage.reportedModelCallCount)}
|
|
133
|
+
/>
|
|
122
134
|
</div>
|
|
123
135
|
<div>
|
|
124
136
|
<div className="mb-1.5 text-[11px] font-medium text-gray-700">
|
|
@@ -136,6 +148,11 @@ export function ChatSessionTokenUsage({ sessionKey }: { sessionKey: string | nul
|
|
|
136
148
|
<div className="mt-1 text-[10px] text-gray-500">
|
|
137
149
|
{t('chatWorkspaceTokenUsageModelRunCount').replace('{count}', String(modelUsage.runCount))}
|
|
138
150
|
{' · '}
|
|
151
|
+
{t('chatWorkspaceTokenUsageModelCallCount').replace(
|
|
152
|
+
'{count}',
|
|
153
|
+
formatCount(modelUsage.modelCallCount),
|
|
154
|
+
)}
|
|
155
|
+
{' · '}
|
|
139
156
|
{t('chatWorkspaceTokenUsageModelCacheHitRate').replace(
|
|
140
157
|
'{rate}',
|
|
141
158
|
formatCacheHitRate(modelUsage.cacheHitRate),
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useEffect,
|
|
3
|
+
useRef,
|
|
4
|
+
useState,
|
|
5
|
+
type RefObject,
|
|
6
|
+
} from "react";
|
|
7
|
+
import { ListTree } from "lucide-react";
|
|
8
|
+
import { ChatPopoverContent } from "@/features/chat/components/chat-popover-content";
|
|
9
|
+
import { IconActionButton } from "@/shared/components/ui/actions/icon-action-button";
|
|
10
|
+
import { Popover, PopoverTrigger } from "@/shared/components/ui/popover";
|
|
11
|
+
import { t } from "@/shared/lib/i18n";
|
|
12
|
+
|
|
13
|
+
const HEADING_SELECTOR = "h1, h2, h3, h4, h5, h6";
|
|
14
|
+
const HEADING_INDEX_ATTRIBUTE = "data-workspace-markdown-heading-index";
|
|
15
|
+
const HEADING_TOP_GAP = 16;
|
|
16
|
+
|
|
17
|
+
type WorkspaceMarkdownOutlineItem = {
|
|
18
|
+
depth: number;
|
|
19
|
+
index: number;
|
|
20
|
+
label: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function collectWorkspaceMarkdownOutline(
|
|
24
|
+
container: HTMLElement,
|
|
25
|
+
): WorkspaceMarkdownOutlineItem[] {
|
|
26
|
+
const levelStack: number[] = [];
|
|
27
|
+
const items: WorkspaceMarkdownOutlineItem[] = [];
|
|
28
|
+
|
|
29
|
+
container
|
|
30
|
+
.querySelectorAll<HTMLElement>(HEADING_SELECTOR)
|
|
31
|
+
.forEach((heading) => {
|
|
32
|
+
heading.removeAttribute(HEADING_INDEX_ATTRIBUTE);
|
|
33
|
+
const label = heading.textContent?.replace(/\s+/g, " ").trim();
|
|
34
|
+
if (!label) return;
|
|
35
|
+
|
|
36
|
+
const level = Number(heading.tagName.slice(1));
|
|
37
|
+
while (
|
|
38
|
+
levelStack.length > 0 &&
|
|
39
|
+
levelStack[levelStack.length - 1]! >= level
|
|
40
|
+
) {
|
|
41
|
+
levelStack.pop();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const index = items.length;
|
|
45
|
+
heading.setAttribute(HEADING_INDEX_ATTRIBUTE, String(index));
|
|
46
|
+
items.push({ depth: levelStack.length, index, label });
|
|
47
|
+
levelStack.push(level);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return items;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function WorkspaceMarkdownOutline({
|
|
54
|
+
contentKey,
|
|
55
|
+
documentKey,
|
|
56
|
+
scrollContainerRef,
|
|
57
|
+
}: {
|
|
58
|
+
contentKey: string;
|
|
59
|
+
documentKey: string;
|
|
60
|
+
scrollContainerRef: RefObject<HTMLDivElement>;
|
|
61
|
+
}) {
|
|
62
|
+
const [items, setItems] = useState<WorkspaceMarkdownOutlineItem[]>([]);
|
|
63
|
+
const [open, setOpen] = useState(false);
|
|
64
|
+
const outlineScrollRef = useRef<HTMLElement | null>(null);
|
|
65
|
+
const outlineScrollTopRef = useRef(0);
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
outlineScrollTopRef.current = 0;
|
|
69
|
+
setOpen(false);
|
|
70
|
+
const container = scrollContainerRef.current;
|
|
71
|
+
if (!container) {
|
|
72
|
+
setItems([]);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
setItems(collectWorkspaceMarkdownOutline(container));
|
|
77
|
+
return () => {
|
|
78
|
+
container
|
|
79
|
+
.querySelectorAll<HTMLElement>(`[${HEADING_INDEX_ATTRIBUTE}]`)
|
|
80
|
+
.forEach((heading) => heading.removeAttribute(HEADING_INDEX_ATTRIBUTE));
|
|
81
|
+
};
|
|
82
|
+
}, [contentKey, documentKey, scrollContainerRef]);
|
|
83
|
+
|
|
84
|
+
if (items.length === 0) return null;
|
|
85
|
+
|
|
86
|
+
const label = t("chatWorkspaceMarkdownOutline");
|
|
87
|
+
const handleOpenChange = (nextOpen: boolean) => {
|
|
88
|
+
if (!nextOpen && outlineScrollRef.current) {
|
|
89
|
+
outlineScrollTopRef.current = outlineScrollRef.current.scrollTop;
|
|
90
|
+
}
|
|
91
|
+
setOpen(nextOpen);
|
|
92
|
+
};
|
|
93
|
+
const handleSelect = (index: number) => {
|
|
94
|
+
const container = scrollContainerRef.current;
|
|
95
|
+
const heading = container?.querySelector<HTMLElement>(
|
|
96
|
+
`[${HEADING_INDEX_ATTRIBUTE}="${index}"]`,
|
|
97
|
+
);
|
|
98
|
+
if (!container || !heading) return;
|
|
99
|
+
|
|
100
|
+
handleOpenChange(false);
|
|
101
|
+
const top =
|
|
102
|
+
container.scrollTop +
|
|
103
|
+
heading.getBoundingClientRect().top -
|
|
104
|
+
container.getBoundingClientRect().top;
|
|
105
|
+
container.scrollTo({
|
|
106
|
+
top: Math.max(0, top - HEADING_TOP_GAP),
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<Popover open={open} onOpenChange={handleOpenChange}>
|
|
112
|
+
<PopoverTrigger asChild>
|
|
113
|
+
<IconActionButton
|
|
114
|
+
icon={<ListTree className="h-3.5 w-3.5" />}
|
|
115
|
+
label={label}
|
|
116
|
+
size="sm"
|
|
117
|
+
tone="surface"
|
|
118
|
+
className="rounded-sm data-[state=open]:bg-gray-200/80 data-[state=open]:text-gray-900"
|
|
119
|
+
/>
|
|
120
|
+
</PopoverTrigger>
|
|
121
|
+
|
|
122
|
+
<ChatPopoverContent
|
|
123
|
+
align="end"
|
|
124
|
+
className="w-[min(20rem,calc(100vw-1.5rem))] rounded-lg p-1.5"
|
|
125
|
+
data-testid="workspace-markdown-outline-popover"
|
|
126
|
+
>
|
|
127
|
+
<nav
|
|
128
|
+
ref={(node) => {
|
|
129
|
+
outlineScrollRef.current = node;
|
|
130
|
+
if (node) node.scrollTop = outlineScrollTopRef.current;
|
|
131
|
+
}}
|
|
132
|
+
aria-label={label}
|
|
133
|
+
className="max-h-80 overflow-y-auto custom-scrollbar"
|
|
134
|
+
>
|
|
135
|
+
<ul className="space-y-0.5">
|
|
136
|
+
{items.map((item) => (
|
|
137
|
+
<li key={item.index}>
|
|
138
|
+
<button
|
|
139
|
+
type="button"
|
|
140
|
+
className="block w-full truncate rounded-md py-1.5 pr-2 text-left text-xs leading-4 text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-950 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-border active:bg-gray-200"
|
|
141
|
+
style={{
|
|
142
|
+
paddingInlineStart: `${0.5 + item.depth * 0.75}rem`,
|
|
143
|
+
}}
|
|
144
|
+
title={item.label}
|
|
145
|
+
onClick={() => handleSelect(item.index)}
|
|
146
|
+
>
|
|
147
|
+
{item.label}
|
|
148
|
+
</button>
|
|
149
|
+
</li>
|
|
150
|
+
))}
|
|
151
|
+
</ul>
|
|
152
|
+
</nav>
|
|
153
|
+
</ChatPopoverContent>
|
|
154
|
+
</Popover>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
@@ -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,
|
|
@@ -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 产品雷达" })
|