@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
|
@@ -17,12 +17,8 @@ import {
|
|
|
17
17
|
CronTaskComposer,
|
|
18
18
|
CronTemplateGallery,
|
|
19
19
|
} from "@/features/cron/components/cron-task-discovery";
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
useDeleteCronJob,
|
|
23
|
-
useRunCronJob,
|
|
24
|
-
useToggleCronJob,
|
|
25
|
-
} from "@/features/cron/hooks/use-cron-jobs";
|
|
20
|
+
import { useCronJobs } from "@/features/cron/hooks/use-cron-jobs";
|
|
21
|
+
import { useCronJobActions } from "@/features/cron/hooks/use-cron-job-actions";
|
|
26
22
|
import { Button } from "@/shared/components/ui/button";
|
|
27
23
|
import { IconActionButton } from "@/shared/components/ui/actions/icon-action-button";
|
|
28
24
|
import { Input } from "@/shared/components/ui/input";
|
|
@@ -31,7 +27,6 @@ import type {
|
|
|
31
27
|
CronListStatus,
|
|
32
28
|
CronListSummaryView,
|
|
33
29
|
} from "@/shared/lib/api";
|
|
34
|
-
import { useConfirmDialog } from "@/shared/hooks/use-confirm-dialog";
|
|
35
30
|
import { t } from "@/shared/lib/i18n";
|
|
36
31
|
import { cn } from "@/shared/lib/utils";
|
|
37
32
|
|
|
@@ -167,10 +162,7 @@ export function CronConfig() {
|
|
|
167
162
|
query: deferredQuery,
|
|
168
163
|
status,
|
|
169
164
|
});
|
|
170
|
-
const
|
|
171
|
-
const toggleCronJob = useToggleCronJob();
|
|
172
|
-
const runCronJob = useRunCronJob();
|
|
173
|
-
const { confirm, ConfirmDialog } = useConfirmDialog();
|
|
165
|
+
const cronActions = useCronJobActions();
|
|
174
166
|
const jobs = cronQuery.data?.jobs ?? [];
|
|
175
167
|
const total = cronQuery.data?.total ?? 0;
|
|
176
168
|
const summary = cronQuery.data?.summary;
|
|
@@ -190,46 +182,28 @@ export function CronConfig() {
|
|
|
190
182
|
composerRef.current?.focus();
|
|
191
183
|
};
|
|
192
184
|
|
|
193
|
-
const handleDelete =
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
185
|
+
const handleDelete = (job: CronJobView) => {
|
|
186
|
+
void cronActions.deleteJob(job, () => {
|
|
187
|
+
setExpandedJobId(null);
|
|
188
|
+
setSelectedJobId(null);
|
|
189
|
+
if (jobs.length === 1 && page > 0) {
|
|
190
|
+
setPage(page - 1);
|
|
191
|
+
}
|
|
199
192
|
});
|
|
200
|
-
if (!confirmed) {
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
setExpandedJobId(null);
|
|
204
|
-
setSelectedJobId(null);
|
|
205
|
-
if (jobs.length === 1 && page > 0) {
|
|
206
|
-
setPage(page - 1);
|
|
207
|
-
}
|
|
208
|
-
deleteCronJob.mutate({ id: job.id });
|
|
209
193
|
};
|
|
210
194
|
|
|
211
195
|
const handleToggle = (job: CronJobView, enabled: boolean) => {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
196
|
+
void cronActions.toggleJob(job, enabled, () => {
|
|
197
|
+
const leavesCurrentFilter =
|
|
198
|
+
(status === "enabled" && !enabled) || (status === "disabled" && enabled);
|
|
199
|
+
if (leavesCurrentFilter && jobs.length === 1 && page > 0) {
|
|
200
|
+
setPage(page - 1);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
218
203
|
};
|
|
219
204
|
|
|
220
|
-
const handleRun =
|
|
221
|
-
|
|
222
|
-
if (force) {
|
|
223
|
-
const confirmed = await confirm({
|
|
224
|
-
title: `${t("cronRunForceConfirm")}?`,
|
|
225
|
-
description: job.name ? `${job.name} (${job.id})` : job.id,
|
|
226
|
-
confirmLabel: t("cronRunNow"),
|
|
227
|
-
});
|
|
228
|
-
if (!confirmed) {
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
runCronJob.mutate({ id: job.id, force });
|
|
205
|
+
const handleRun = (job: CronJobView) => {
|
|
206
|
+
void cronActions.runJob(job);
|
|
233
207
|
};
|
|
234
208
|
|
|
235
209
|
const handlePageChange = (nextPage: number) => {
|
|
@@ -446,7 +420,7 @@ export function CronConfig() {
|
|
|
446
420
|
onRun={handleRun}
|
|
447
421
|
onToggle={handleToggle}
|
|
448
422
|
/>
|
|
449
|
-
<ConfirmDialog />
|
|
423
|
+
<cronActions.ConfirmDialog />
|
|
450
424
|
</PageLayout>
|
|
451
425
|
);
|
|
452
426
|
}
|
|
@@ -166,7 +166,7 @@ export function CronJobRow({
|
|
|
166
166
|
size="icon"
|
|
167
167
|
className="h-8 w-8"
|
|
168
168
|
onClick={() => onEdit(job)}
|
|
169
|
-
aria-label={`${t("
|
|
169
|
+
aria-label={`${t("cronEditDetails")} ${jobLabel}`}
|
|
170
170
|
>
|
|
171
171
|
<Pencil className="h-3.5 w-3.5" />
|
|
172
172
|
</Button>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { act, renderHook } from "@testing-library/react";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
import type { CronJobView } from "@/shared/lib/api";
|
|
5
|
+
import { setLanguage } from "@/shared/lib/i18n";
|
|
6
|
+
import { useCronJobActions } from "@/features/cron/hooks/use-cron-job-actions";
|
|
7
|
+
|
|
8
|
+
const mocks = vi.hoisted(() => ({
|
|
9
|
+
confirm: vi.fn(),
|
|
10
|
+
deleteJob: vi.fn(),
|
|
11
|
+
runJob: vi.fn(),
|
|
12
|
+
toggleJob: vi.fn(),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock("@/features/cron/hooks/use-cron-jobs", () => ({
|
|
16
|
+
useDeleteCronJob: () => ({ mutateAsync: mocks.deleteJob }),
|
|
17
|
+
useRunCronJob: () => ({ mutateAsync: mocks.runJob }),
|
|
18
|
+
useToggleCronJob: () => ({ mutateAsync: mocks.toggleJob }),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock("@/shared/hooks/use-confirm-dialog", () => ({
|
|
22
|
+
useConfirmDialog: () => ({
|
|
23
|
+
confirm: mocks.confirm,
|
|
24
|
+
ConfirmDialog: () => null,
|
|
25
|
+
}),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
function createJob(overrides: Partial<CronJobView> = {}): CronJobView {
|
|
29
|
+
return {
|
|
30
|
+
id: "paint-daily",
|
|
31
|
+
name: "每日绘画",
|
|
32
|
+
enabled: true,
|
|
33
|
+
schedule: { kind: "cron", expr: "0 9 * * *", tz: "Asia/Shanghai" },
|
|
34
|
+
payload: {
|
|
35
|
+
kind: "agent_turn",
|
|
36
|
+
message: "生成一张今日绘画。",
|
|
37
|
+
agentId: "main",
|
|
38
|
+
sessionId: "session:paint",
|
|
39
|
+
},
|
|
40
|
+
state: {
|
|
41
|
+
nextRunAt: "2026-08-27T01:00:00.000Z",
|
|
42
|
+
lastRunAt: null,
|
|
43
|
+
lastStatus: null,
|
|
44
|
+
lastError: null,
|
|
45
|
+
},
|
|
46
|
+
createdAt: "2026-08-26T01:00:00.000Z",
|
|
47
|
+
updatedAt: "2026-08-26T01:00:00.000Z",
|
|
48
|
+
deleteAfterRun: false,
|
|
49
|
+
...overrides,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe("useCronJobActions", () => {
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
setLanguage("zh");
|
|
56
|
+
vi.clearAllMocks();
|
|
57
|
+
mocks.confirm.mockResolvedValue(true);
|
|
58
|
+
mocks.deleteJob.mockResolvedValue({ deleted: true });
|
|
59
|
+
mocks.runJob.mockResolvedValue({ job: createJob(), executed: true });
|
|
60
|
+
mocks.toggleJob.mockResolvedValue({ job: createJob() });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("requires confirmation and forces a paused task to run without enabling it", async () => {
|
|
64
|
+
const { result } = renderHook(() => useCronJobActions());
|
|
65
|
+
const job = createJob({ enabled: false });
|
|
66
|
+
|
|
67
|
+
await act(async () => {
|
|
68
|
+
await result.current.runJob(job);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
expect(mocks.confirm).toHaveBeenCalledWith({
|
|
72
|
+
title: "任务已暂停,仍要立即执行?",
|
|
73
|
+
description: "每日绘画 (paint-daily)",
|
|
74
|
+
confirmLabel: "立即执行",
|
|
75
|
+
});
|
|
76
|
+
expect(mocks.runJob).toHaveBeenCalledWith({ id: "paint-daily", force: true });
|
|
77
|
+
expect(mocks.toggleJob).not.toHaveBeenCalled();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("uses a destructive confirmation before deleting", async () => {
|
|
81
|
+
const { result } = renderHook(() => useCronJobActions());
|
|
82
|
+
|
|
83
|
+
await act(async () => {
|
|
84
|
+
await result.current.deleteJob(createJob());
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(mocks.confirm).toHaveBeenCalledWith({
|
|
88
|
+
title: "确认删除定时任务?",
|
|
89
|
+
description: "每日绘画 (paint-daily)",
|
|
90
|
+
confirmLabel: "删除",
|
|
91
|
+
variant: "destructive",
|
|
92
|
+
});
|
|
93
|
+
expect(mocks.deleteJob).toHaveBeenCalledWith({ id: "paint-daily" });
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
|
|
3
|
+
import type { CronJobView } from "@/shared/lib/api";
|
|
4
|
+
import { useConfirmDialog } from "@/shared/hooks/use-confirm-dialog";
|
|
5
|
+
import { t } from "@/shared/lib/i18n";
|
|
6
|
+
import {
|
|
7
|
+
useDeleteCronJob,
|
|
8
|
+
useRunCronJob,
|
|
9
|
+
useToggleCronJob,
|
|
10
|
+
} from "./use-cron-jobs";
|
|
11
|
+
|
|
12
|
+
type AfterSuccess = () => void;
|
|
13
|
+
|
|
14
|
+
function describeJob(job: CronJobView): string {
|
|
15
|
+
return job.name ? `${job.name} (${job.id})` : job.id;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function useCronJobActions() {
|
|
19
|
+
const deleteCronJob = useDeleteCronJob();
|
|
20
|
+
const runCronJob = useRunCronJob();
|
|
21
|
+
const toggleCronJob = useToggleCronJob();
|
|
22
|
+
const { confirm, ConfirmDialog } = useConfirmDialog();
|
|
23
|
+
const [pendingJobIds, setPendingJobIds] = useState<ReadonlySet<string>>(
|
|
24
|
+
new Set(),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const execute = async (
|
|
28
|
+
job: CronJobView,
|
|
29
|
+
action: () => Promise<unknown>,
|
|
30
|
+
afterSuccess?: AfterSuccess,
|
|
31
|
+
): Promise<boolean> => {
|
|
32
|
+
setPendingJobIds((ids) => new Set(ids).add(job.id));
|
|
33
|
+
try {
|
|
34
|
+
await action();
|
|
35
|
+
afterSuccess?.();
|
|
36
|
+
return true;
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
} finally {
|
|
40
|
+
setPendingJobIds((ids) => {
|
|
41
|
+
const nextIds = new Set(ids);
|
|
42
|
+
nextIds.delete(job.id);
|
|
43
|
+
return nextIds;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const deleteJob = async (
|
|
49
|
+
job: CronJobView,
|
|
50
|
+
afterSuccess?: AfterSuccess,
|
|
51
|
+
): Promise<boolean> => {
|
|
52
|
+
const confirmed = await confirm({
|
|
53
|
+
title: `${t("cronDeleteConfirm")}?`,
|
|
54
|
+
description: describeJob(job),
|
|
55
|
+
confirmLabel: t("delete"),
|
|
56
|
+
variant: "destructive",
|
|
57
|
+
});
|
|
58
|
+
if (!confirmed) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
return await execute(
|
|
62
|
+
job,
|
|
63
|
+
() => deleteCronJob.mutateAsync({ id: job.id }),
|
|
64
|
+
afterSuccess,
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const runJob = async (
|
|
69
|
+
job: CronJobView,
|
|
70
|
+
afterSuccess?: AfterSuccess,
|
|
71
|
+
): Promise<boolean> => {
|
|
72
|
+
const force = !job.enabled;
|
|
73
|
+
if (force) {
|
|
74
|
+
const confirmed = await confirm({
|
|
75
|
+
title: `${t("cronRunForceConfirm")}?`,
|
|
76
|
+
description: describeJob(job),
|
|
77
|
+
confirmLabel: t("cronRunNow"),
|
|
78
|
+
});
|
|
79
|
+
if (!confirmed) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return await execute(
|
|
84
|
+
job,
|
|
85
|
+
() => runCronJob.mutateAsync({ id: job.id, force }),
|
|
86
|
+
afterSuccess,
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const toggleJob = async (
|
|
91
|
+
job: CronJobView,
|
|
92
|
+
enabled: boolean,
|
|
93
|
+
afterSuccess?: AfterSuccess,
|
|
94
|
+
): Promise<boolean> => {
|
|
95
|
+
return await execute(
|
|
96
|
+
job,
|
|
97
|
+
() => toggleCronJob.mutateAsync({ id: job.id, enabled }),
|
|
98
|
+
afterSuccess,
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
ConfirmDialog,
|
|
104
|
+
deleteJob,
|
|
105
|
+
isPending: (job: CronJobView) => pendingJobIds.has(job.id),
|
|
106
|
+
runJob,
|
|
107
|
+
toggleJob,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { DesktopAuthorizationDialog } from "./desktop-authorization-dialog";
|
|
4
|
+
import { useDesktopAuthorizationStore } from "../stores/desktop-authorization.store";
|
|
5
|
+
|
|
6
|
+
const mocks = vi.hoisted(() => ({ grantAccess: vi.fn() }));
|
|
7
|
+
|
|
8
|
+
vi.mock("../managers/desktop-capability.manager", () => ({
|
|
9
|
+
desktopCapabilityManager: {
|
|
10
|
+
grantAccess: mocks.grantAccess,
|
|
11
|
+
},
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
describe("DesktopAuthorizationDialog", () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
mocks.grantAccess.mockReset();
|
|
17
|
+
mocks.grantAccess.mockResolvedValue({});
|
|
18
|
+
useDesktopAuthorizationStore.getState().clear();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("shows the trusted subject and grants only after explicit approval", async () => {
|
|
22
|
+
useDesktopAuthorizationStore.getState().present({
|
|
23
|
+
applicationId: "wechat",
|
|
24
|
+
request: {
|
|
25
|
+
subject: { type: "agent", id: "main" },
|
|
26
|
+
resource: {
|
|
27
|
+
type: "desktop.application",
|
|
28
|
+
target: { applicationId: "wechat", platform: "darwin", bundleId: "com.tencent.xinWeChat" },
|
|
29
|
+
},
|
|
30
|
+
access: ["ui.write"],
|
|
31
|
+
declarationFingerprint: "fingerprint",
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
render(<DesktopAuthorizationDialog />);
|
|
36
|
+
expect(screen.getByText("AI · main")).toBeTruthy();
|
|
37
|
+
expect(screen.getByText("wechat")).toBeTruthy();
|
|
38
|
+
expect(mocks.grantAccess).not.toHaveBeenCalled();
|
|
39
|
+
|
|
40
|
+
fireEvent.click(screen.getByRole("button", { name: "Allow" }));
|
|
41
|
+
await waitFor(() => expect(mocks.grantAccess).toHaveBeenCalledOnce());
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("explains the bounded pointer permission in user terms", () => {
|
|
45
|
+
useDesktopAuthorizationStore.getState().present({
|
|
46
|
+
applicationId: "wechat",
|
|
47
|
+
request: {
|
|
48
|
+
subject: { type: "agent", id: "main" },
|
|
49
|
+
resource: { type: "desktop.application", target: { applicationId: "wechat", platform: "darwin", bundleId: "com.tencent.xinWeChat" } },
|
|
50
|
+
access: ["input.pointer"],
|
|
51
|
+
declarationFingerprint: "fingerprint",
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
render(<DesktopAuthorizationDialog />);
|
|
55
|
+
expect(screen.getByText("Click inside the app window")).toBeTruthy();
|
|
56
|
+
expect(screen.getByText(/cannot use keys, scroll, drag, send, or confirm/i)).toBeTruthy();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { ShieldAlert } from "lucide-react";
|
|
3
|
+
import { toast } from "sonner";
|
|
4
|
+
import {
|
|
5
|
+
Dialog,
|
|
6
|
+
DialogContent,
|
|
7
|
+
DialogDescription,
|
|
8
|
+
DialogFooter,
|
|
9
|
+
DialogHeader,
|
|
10
|
+
DialogTitle,
|
|
11
|
+
} from "@/shared/components/ui/dialog";
|
|
12
|
+
import { Button } from "@/shared/components/ui/button";
|
|
13
|
+
import { appQueryClient } from "@/app-query-client";
|
|
14
|
+
import { nextclawClient } from "@/shared/lib/api/managers/client.manager";
|
|
15
|
+
import { t } from "@/shared/lib/i18n";
|
|
16
|
+
import { desktopCapabilityManager } from "../managers/desktop-capability.manager";
|
|
17
|
+
import {
|
|
18
|
+
type DesktopAuthorizationRequest,
|
|
19
|
+
useDesktopAuthorizationStore,
|
|
20
|
+
} from "../stores/desktop-authorization.store";
|
|
21
|
+
|
|
22
|
+
const AUTHORIZATION_EVENT_TYPE = "desktop.authorization.required";
|
|
23
|
+
|
|
24
|
+
export function DesktopAuthorizationDialog() {
|
|
25
|
+
const pending = useDesktopAuthorizationStore((state) => state.pending);
|
|
26
|
+
const present = useDesktopAuthorizationStore((state) => state.present);
|
|
27
|
+
const clear = useDesktopAuthorizationStore((state) => state.clear);
|
|
28
|
+
const [isGranting, setIsGranting] = useState(false);
|
|
29
|
+
|
|
30
|
+
useEffect(() => nextclawClient.eventBus.subscribeAll((event) => {
|
|
31
|
+
if (event.type !== AUTHORIZATION_EVENT_TYPE) return;
|
|
32
|
+
const request = readAuthorizationRequest(event.payload);
|
|
33
|
+
if (request) present(request);
|
|
34
|
+
}), [present]);
|
|
35
|
+
|
|
36
|
+
const allow = async () => {
|
|
37
|
+
if (!pending || isGranting) return;
|
|
38
|
+
setIsGranting(true);
|
|
39
|
+
try {
|
|
40
|
+
await desktopCapabilityManager.grantAccess(pending.request);
|
|
41
|
+
await appQueryClient.invalidateQueries({
|
|
42
|
+
queryKey: ["desktop-capability", "grants"],
|
|
43
|
+
});
|
|
44
|
+
clear();
|
|
45
|
+
toast.success(t("desktopAuthorizationAllowed"));
|
|
46
|
+
} catch (error) {
|
|
47
|
+
toast.error(
|
|
48
|
+
error instanceof Error
|
|
49
|
+
? error.message
|
|
50
|
+
: t("desktopAuthorizationFailed"),
|
|
51
|
+
);
|
|
52
|
+
} finally {
|
|
53
|
+
setIsGranting(false);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<Dialog
|
|
59
|
+
open={Boolean(pending)}
|
|
60
|
+
onOpenChange={(open) => {
|
|
61
|
+
if (!open && !isGranting) clear();
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
<DialogContent className="[&>:last-child]:hidden">
|
|
65
|
+
<DialogHeader>
|
|
66
|
+
<DialogTitle className="flex items-center gap-2">
|
|
67
|
+
<ShieldAlert className="h-4 w-4 text-amber-600" />
|
|
68
|
+
{t("desktopAuthorizationTitle")}
|
|
69
|
+
</DialogTitle>
|
|
70
|
+
<DialogDescription>
|
|
71
|
+
{t("desktopAuthorizationDescription")}
|
|
72
|
+
</DialogDescription>
|
|
73
|
+
</DialogHeader>
|
|
74
|
+
{pending ? (
|
|
75
|
+
<div className="space-y-3 rounded-xl bg-muted/55 p-4 text-sm">
|
|
76
|
+
<AuthorizationField
|
|
77
|
+
label={t("desktopAuthorizationCaller")}
|
|
78
|
+
value={formatSubject(pending)}
|
|
79
|
+
/>
|
|
80
|
+
<AuthorizationField
|
|
81
|
+
label={t("desktopAuthorizationApplication")}
|
|
82
|
+
value={pending.applicationId}
|
|
83
|
+
/>
|
|
84
|
+
<AuthorizationField
|
|
85
|
+
label={t("desktopAuthorizationAccess")}
|
|
86
|
+
value={pending.request.access.map(formatAccess).join(", ")}
|
|
87
|
+
/>
|
|
88
|
+
<p className="text-xs leading-5 text-muted-foreground">
|
|
89
|
+
{formatRisk(pending.request.access)}
|
|
90
|
+
</p>
|
|
91
|
+
</div>
|
|
92
|
+
) : null}
|
|
93
|
+
<DialogFooter className="gap-2 sm:gap-0">
|
|
94
|
+
<Button
|
|
95
|
+
type="button"
|
|
96
|
+
variant="outline"
|
|
97
|
+
disabled={isGranting}
|
|
98
|
+
onClick={clear}
|
|
99
|
+
>
|
|
100
|
+
{t("desktopAuthorizationReject")}
|
|
101
|
+
</Button>
|
|
102
|
+
<Button type="button" disabled={isGranting} onClick={() => void allow()}>
|
|
103
|
+
{isGranting ? t("desktopAuthorizationAllowing") : t("desktopAuthorizationAllow")}
|
|
104
|
+
</Button>
|
|
105
|
+
</DialogFooter>
|
|
106
|
+
</DialogContent>
|
|
107
|
+
</Dialog>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function AuthorizationField({ label, value }: { label: string; value: string }) {
|
|
112
|
+
return (
|
|
113
|
+
<div className="grid grid-cols-[5rem_minmax(0,1fr)] gap-3">
|
|
114
|
+
<div className="text-xs font-medium text-muted-foreground">{label}</div>
|
|
115
|
+
<div className="min-w-0 break-words text-xs text-foreground">{value}</div>
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function readAuthorizationRequest(value: unknown): DesktopAuthorizationRequest | null {
|
|
121
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
122
|
+
const payload = value as Record<string, unknown>;
|
|
123
|
+
const { request } = payload;
|
|
124
|
+
if (!request || typeof request !== "object" || Array.isArray(request)) return null;
|
|
125
|
+
const candidate = request as Record<string, unknown>;
|
|
126
|
+
if (
|
|
127
|
+
typeof payload.applicationId !== "string" ||
|
|
128
|
+
!candidate.subject ||
|
|
129
|
+
!candidate.resource ||
|
|
130
|
+
!Array.isArray(candidate.access) ||
|
|
131
|
+
typeof candidate.declarationFingerprint !== "string"
|
|
132
|
+
) return null;
|
|
133
|
+
return {
|
|
134
|
+
applicationId: payload.applicationId,
|
|
135
|
+
request: candidate as DesktopAuthorizationRequest["request"],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function formatSubject(pending: DesktopAuthorizationRequest): string {
|
|
140
|
+
const { subject } = pending.request;
|
|
141
|
+
const label = subject.type === "agent"
|
|
142
|
+
? t("desktopCapabilitiesAgent")
|
|
143
|
+
: t("desktopCapabilitiesExtension");
|
|
144
|
+
return `${label} · ${subject.id}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function formatAccess(access: string): string {
|
|
148
|
+
if (access === "ui.read") return t("desktopAuthorizationRead");
|
|
149
|
+
if (access === "ui.observe") return t("desktopAuthorizationObserve");
|
|
150
|
+
if (access === "ui.write") return t("desktopAuthorizationWrite");
|
|
151
|
+
if (access === "screen.capture-window") return t("desktopAuthorizationScreenCapture");
|
|
152
|
+
if (access === "input.pointer") return t("desktopAuthorizationPointerInput");
|
|
153
|
+
return access;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function formatRisk(access: string[]): string {
|
|
157
|
+
if (access.includes("ui.write")) return t("desktopAuthorizationWriteRisk");
|
|
158
|
+
if (access.includes("screen.capture-window")) return t("desktopAuthorizationScreenCaptureRisk");
|
|
159
|
+
if (access.includes("input.pointer")) return t("desktopAuthorizationPointerInputRisk");
|
|
160
|
+
return t("desktopAuthorizationReadRisk");
|
|
161
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { isDesktopAutomationAvailable } from "./use-desktop-capabilities";
|
|
3
|
+
|
|
4
|
+
describe("isDesktopAutomationAvailable", () => {
|
|
5
|
+
it("uses only the backend product feature-control value", () => {
|
|
6
|
+
expect(isDesktopAutomationAvailable({ desktopAutomation: { available: true } })).toBe(true);
|
|
7
|
+
expect(isDesktopAutomationAvailable({ desktopAutomation: { available: false } })).toBe(false);
|
|
8
|
+
expect(isDesktopAutomationAvailable(undefined)).toBe(false);
|
|
9
|
+
});
|
|
10
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
2
|
+
import type { CapabilityGrantView, ProductFeatureControlsView } from "@nextclaw/client-sdk";
|
|
3
|
+
import { useFeatureControls } from "@/app/hooks/use-feature-controls";
|
|
4
|
+
import { desktopCapabilityManager } from "../managers/desktop-capability.manager";
|
|
5
|
+
|
|
6
|
+
const STATUS_QUERY_KEY = ["desktop-capability", "status"] as const;
|
|
7
|
+
const GRANTS_QUERY_KEY = ["desktop-capability", "grants"] as const;
|
|
8
|
+
|
|
9
|
+
export function useDesktopCapabilityStatus() {
|
|
10
|
+
return useQuery({
|
|
11
|
+
queryKey: STATUS_QUERY_KEY,
|
|
12
|
+
queryFn: desktopCapabilityManager.getStatus,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function useDesktopCapabilityAvailability(): boolean {
|
|
17
|
+
const featureControls = useFeatureControls();
|
|
18
|
+
return isDesktopAutomationAvailable(featureControls.data);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isDesktopAutomationAvailable(controls: ProductFeatureControlsView | undefined): boolean {
|
|
22
|
+
return controls?.desktopAutomation.available === true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function useDesktopCapabilityGrants() {
|
|
26
|
+
return useQuery({
|
|
27
|
+
queryKey: GRANTS_QUERY_KEY,
|
|
28
|
+
queryFn: desktopCapabilityManager.listGrants,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function useRequestDesktopSystemPermission() {
|
|
33
|
+
const queryClient = useQueryClient();
|
|
34
|
+
return useMutation({
|
|
35
|
+
mutationFn: desktopCapabilityManager.requestSystemPermission,
|
|
36
|
+
onSuccess: async () => {
|
|
37
|
+
await queryClient.invalidateQueries({ queryKey: STATUS_QUERY_KEY });
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function useOpenDesktopSystemSettings() {
|
|
43
|
+
return useMutation({ mutationFn: desktopCapabilityManager.openSystemSettings });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function useRevokeDesktopGrant() {
|
|
47
|
+
const queryClient = useQueryClient();
|
|
48
|
+
return useMutation({
|
|
49
|
+
mutationFn: async (grant: CapabilityGrantView) =>
|
|
50
|
+
await desktopCapabilityManager.revokeGrant(grant),
|
|
51
|
+
onSuccess: async () => {
|
|
52
|
+
await queryClient.invalidateQueries({ queryKey: GRANTS_QUERY_KEY });
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DesktopAuthorizationDialog } from "./components/desktop-authorization-dialog";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CapabilityGrantRequestView,
|
|
3
|
+
CapabilityGrantView,
|
|
4
|
+
} from "@nextclaw/client-sdk";
|
|
5
|
+
import { nextclawClient } from "@/shared/lib/api/managers/client.manager";
|
|
6
|
+
|
|
7
|
+
export class DesktopCapabilityManager {
|
|
8
|
+
getStatus = async () => await nextclawClient.capabilityAccess.getDesktopStatus();
|
|
9
|
+
|
|
10
|
+
listGrants = async () => await nextclawClient.capabilityAccess.listDesktopGrants();
|
|
11
|
+
|
|
12
|
+
grantAccess = async (request: CapabilityGrantRequestView) =>
|
|
13
|
+
await nextclawClient.capabilityAccess.grantDesktopAccess(request);
|
|
14
|
+
|
|
15
|
+
requestSystemPermission = async () =>
|
|
16
|
+
await nextclawClient.capabilityAccess.requestDesktopPermissions();
|
|
17
|
+
|
|
18
|
+
openSystemSettings = async () =>
|
|
19
|
+
await nextclawClient.capabilityAccess.openDesktopPermissionSettings();
|
|
20
|
+
|
|
21
|
+
revokeGrant = async (grant: CapabilityGrantView) =>
|
|
22
|
+
await nextclawClient.capabilityAccess.revokeDesktopAccess(grant);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const desktopCapabilityManager = new DesktopCapabilityManager();
|