@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,126 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { PANEL_APP_SCROLL_RESTORATION_CONTRACT } from '@nextclaw/shared';
|
|
1
|
+
import type { RefObject } from 'react';
|
|
3
2
|
import type { DocBrowserTab } from '@/shared/components/doc-browser/doc-browser-context';
|
|
4
|
-
|
|
5
|
-
type ScrollTarget =
|
|
6
|
-
| { kind: 'document' }
|
|
7
|
-
| { kind: 'element'; path: Array<{ index: number; tagName: string }> };
|
|
8
|
-
|
|
9
|
-
type ScrollPosition = {
|
|
10
|
-
currentUrl: string;
|
|
11
|
-
target: ScrollTarget;
|
|
12
|
-
x: number;
|
|
13
|
-
y: number;
|
|
14
|
-
};
|
|
3
|
+
import { usePanelAppScrollRestoration } from '@/shared/hooks/use-panel-app-scroll-restoration';
|
|
15
4
|
|
|
16
5
|
type UseDocBrowserScrollRestorationParams = {
|
|
17
6
|
currentTab?: DocBrowserTab;
|
|
18
|
-
iframeRef: RefObject<HTMLIFrameElement
|
|
7
|
+
iframeRef: RefObject<HTMLIFrameElement>;
|
|
19
8
|
isEnabled: boolean;
|
|
20
|
-
tabs: DocBrowserTab[];
|
|
21
9
|
};
|
|
22
10
|
|
|
23
|
-
function isScrollTarget(value: unknown): value is ScrollTarget {
|
|
24
|
-
if (!value || typeof value !== 'object') {
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
const candidate = value as Partial<ScrollTarget>;
|
|
28
|
-
if (candidate.kind === 'document') {
|
|
29
|
-
return true;
|
|
30
|
-
}
|
|
31
|
-
return candidate.kind === 'element'
|
|
32
|
-
&& Array.isArray(candidate.path)
|
|
33
|
-
&& candidate.path.length > 0
|
|
34
|
-
&& candidate.path.length <= 30
|
|
35
|
-
&& candidate.path.every((segment) => (
|
|
36
|
-
typeof segment?.index === 'number'
|
|
37
|
-
&& Number.isInteger(segment.index)
|
|
38
|
-
&& segment.index >= 0
|
|
39
|
-
&& segment.index <= 1000
|
|
40
|
-
&& typeof segment.tagName === 'string'
|
|
41
|
-
&& segment.tagName.length > 0
|
|
42
|
-
&& segment.tagName.length <= 32
|
|
43
|
-
));
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function isScrollMessage(value: unknown): value is {
|
|
47
|
-
target: ScrollTarget;
|
|
48
|
-
type: string;
|
|
49
|
-
version: number;
|
|
50
|
-
x: number;
|
|
51
|
-
y: number;
|
|
52
|
-
} {
|
|
53
|
-
if (!value || typeof value !== 'object') {
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
const candidate = value as Partial<{
|
|
57
|
-
target: unknown;
|
|
58
|
-
type: string;
|
|
59
|
-
version: number;
|
|
60
|
-
x: number;
|
|
61
|
-
y: number;
|
|
62
|
-
}>;
|
|
63
|
-
return candidate.type === PANEL_APP_SCROLL_RESTORATION_CONTRACT.scrollMessageType
|
|
64
|
-
&& candidate.version === PANEL_APP_SCROLL_RESTORATION_CONTRACT.version
|
|
65
|
-
&& isScrollTarget(candidate.target)
|
|
66
|
-
&& typeof candidate.x === 'number'
|
|
67
|
-
&& Number.isFinite(candidate.x)
|
|
68
|
-
&& candidate.x >= 0
|
|
69
|
-
&& typeof candidate.y === 'number'
|
|
70
|
-
&& Number.isFinite(candidate.y)
|
|
71
|
-
&& candidate.y >= 0;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
11
|
export function useDocBrowserScrollRestoration({
|
|
75
12
|
currentTab,
|
|
76
13
|
iframeRef,
|
|
77
14
|
isEnabled,
|
|
78
|
-
tabs,
|
|
79
15
|
}: UseDocBrowserScrollRestorationParams) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
positionsRef.current.delete(tabId);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}, [tabs]);
|
|
90
|
-
|
|
91
|
-
useEffect(() => {
|
|
92
|
-
if (!currentTab || !isEnabled) {
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
const onMessage = (event: MessageEvent) => {
|
|
96
|
-
if (event.source !== iframeRef.current?.contentWindow || !isScrollMessage(event.data)) {
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
positionsRef.current.set(currentTab.id, {
|
|
100
|
-
currentUrl: currentTab.currentUrl,
|
|
101
|
-
target: event.data.target,
|
|
102
|
-
x: event.data.x,
|
|
103
|
-
y: event.data.y,
|
|
104
|
-
});
|
|
105
|
-
};
|
|
106
|
-
window.addEventListener('message', onMessage);
|
|
107
|
-
return () => window.removeEventListener('message', onMessage);
|
|
108
|
-
}, [currentTab, iframeRef, isEnabled]);
|
|
109
|
-
|
|
110
|
-
return useCallback(() => {
|
|
111
|
-
if (!currentTab || !isEnabled) {
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
const position = positionsRef.current.get(currentTab.id);
|
|
115
|
-
if (!position || position.currentUrl !== currentTab.currentUrl) {
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
iframeRef.current?.contentWindow?.postMessage({
|
|
119
|
-
type: PANEL_APP_SCROLL_RESTORATION_CONTRACT.restoreScrollMessageType,
|
|
120
|
-
version: PANEL_APP_SCROLL_RESTORATION_CONTRACT.version,
|
|
121
|
-
target: position.target,
|
|
122
|
-
x: position.x,
|
|
123
|
-
y: position.y,
|
|
124
|
-
}, '*');
|
|
125
|
-
}, [currentTab, iframeRef, isEnabled]);
|
|
16
|
+
return usePanelAppScrollRestoration({
|
|
17
|
+
currentUrl: currentTab?.currentUrl ?? null,
|
|
18
|
+
iframeRef,
|
|
19
|
+
isEnabled,
|
|
20
|
+
restorationKey: currentTab ? `panel-app:doc-browser:${currentTab.id}` : null,
|
|
21
|
+
});
|
|
126
22
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { ConfirmDialog } from '@/shared/components/ui/confirm-dialog';
|
|
5
|
+
|
|
6
|
+
describe('ConfirmDialog', () => {
|
|
7
|
+
it('focuses the confirm action so Enter confirms the dialog', async () => {
|
|
8
|
+
const user = userEvent.setup();
|
|
9
|
+
const onConfirm = vi.fn();
|
|
10
|
+
|
|
11
|
+
render(
|
|
12
|
+
<ConfirmDialog
|
|
13
|
+
open
|
|
14
|
+
onOpenChange={vi.fn()}
|
|
15
|
+
title="Delete session"
|
|
16
|
+
description="This action cannot be undone."
|
|
17
|
+
confirmLabel="Delete"
|
|
18
|
+
cancelLabel="Cancel"
|
|
19
|
+
onConfirm={onConfirm}
|
|
20
|
+
onCancel={vi.fn()}
|
|
21
|
+
/>,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const confirmButton = await screen.findByRole('button', { name: 'Delete' });
|
|
25
|
+
expect(document.activeElement).toBe(confirmButton);
|
|
26
|
+
|
|
27
|
+
await user.keyboard('{Enter}');
|
|
28
|
+
|
|
29
|
+
expect(onConfirm).toHaveBeenCalledOnce();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -35,6 +35,8 @@ export const ConfirmDialog = ({
|
|
|
35
35
|
onConfirm,
|
|
36
36
|
onCancel
|
|
37
37
|
}: ConfirmDialogProps) => {
|
|
38
|
+
const confirmButtonRef = React.useRef<HTMLButtonElement>(null);
|
|
39
|
+
|
|
38
40
|
const handleConfirm = () => {
|
|
39
41
|
onConfirm();
|
|
40
42
|
onOpenChange(false);
|
|
@@ -45,7 +47,14 @@ export const ConfirmDialog = ({
|
|
|
45
47
|
};
|
|
46
48
|
return (
|
|
47
49
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
48
|
-
<DialogContent
|
|
50
|
+
<DialogContent
|
|
51
|
+
className="[&>:last-child]:hidden"
|
|
52
|
+
onOpenAutoFocus={(event) => {
|
|
53
|
+
event.preventDefault();
|
|
54
|
+
confirmButtonRef.current?.focus();
|
|
55
|
+
}}
|
|
56
|
+
onCloseAutoFocus={(event) => event.preventDefault()}
|
|
57
|
+
>
|
|
49
58
|
<DialogHeader>
|
|
50
59
|
<DialogTitle>{title}</DialogTitle>
|
|
51
60
|
{description ? <DialogDescription>{description}</DialogDescription> : null}
|
|
@@ -55,6 +64,7 @@ export const ConfirmDialog = ({
|
|
|
55
64
|
{cancelLabel}
|
|
56
65
|
</Button>
|
|
57
66
|
<Button
|
|
67
|
+
ref={confirmButtonRef}
|
|
58
68
|
type="button"
|
|
59
69
|
variant={variant === 'destructive' ? 'destructive' : 'default'}
|
|
60
70
|
onClick={handleConfirm}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { fireEvent, render } from "@testing-library/react";
|
|
2
|
+
import { useRef } from "react";
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { PANEL_APP_SCROLL_RESTORATION_CONTRACT } from "@nextclaw/shared";
|
|
5
|
+
import { usePanelAppScrollRestoration } from "@/shared/hooks/use-panel-app-scroll-restoration";
|
|
6
|
+
import { scrollRestorationManager } from "@/shared/lib/navigation-history";
|
|
7
|
+
|
|
8
|
+
function PanelAppFrame({ currentUrl = "/app", restorationKey = "panel-app:test" }: {
|
|
9
|
+
currentUrl?: string;
|
|
10
|
+
restorationKey?: string;
|
|
11
|
+
}) {
|
|
12
|
+
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
13
|
+
const restoreScroll = usePanelAppScrollRestoration({
|
|
14
|
+
currentUrl,
|
|
15
|
+
iframeRef,
|
|
16
|
+
isEnabled: true,
|
|
17
|
+
restorationKey,
|
|
18
|
+
});
|
|
19
|
+
return <iframe ref={iframeRef} data-testid="panel-app-frame" onLoad={restoreScroll} />;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("usePanelAppScrollRestoration", () => {
|
|
23
|
+
beforeEach(() => scrollRestorationManager.clear());
|
|
24
|
+
|
|
25
|
+
it("records only scroll messages from its iframe and restores them after load", () => {
|
|
26
|
+
const { getByTestId } = render(<PanelAppFrame />);
|
|
27
|
+
const iframe = getByTestId("panel-app-frame") as HTMLIFrameElement;
|
|
28
|
+
const postMessage = vi.spyOn(iframe.contentWindow!, "postMessage");
|
|
29
|
+
const target = { kind: "document" } as const;
|
|
30
|
+
|
|
31
|
+
window.dispatchEvent(new MessageEvent("message", {
|
|
32
|
+
data: {
|
|
33
|
+
type: PANEL_APP_SCROLL_RESTORATION_CONTRACT.scrollMessageType,
|
|
34
|
+
version: PANEL_APP_SCROLL_RESTORATION_CONTRACT.version,
|
|
35
|
+
target,
|
|
36
|
+
x: 8,
|
|
37
|
+
y: 64,
|
|
38
|
+
},
|
|
39
|
+
source: iframe.contentWindow,
|
|
40
|
+
}));
|
|
41
|
+
fireEvent.load(iframe);
|
|
42
|
+
|
|
43
|
+
expect(postMessage).toHaveBeenCalledWith({
|
|
44
|
+
type: PANEL_APP_SCROLL_RESTORATION_CONTRACT.restoreScrollMessageType,
|
|
45
|
+
version: PANEL_APP_SCROLL_RESTORATION_CONTRACT.version,
|
|
46
|
+
target,
|
|
47
|
+
x: 8,
|
|
48
|
+
y: 64,
|
|
49
|
+
}, "*");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("does not restore a position from another resource URL", () => {
|
|
53
|
+
scrollRestorationManager.save("panel-app:test", {
|
|
54
|
+
x: 0,
|
|
55
|
+
y: 64,
|
|
56
|
+
payload: { currentUrl: "/old-app", target: { kind: "document" } },
|
|
57
|
+
});
|
|
58
|
+
const { getByTestId } = render(<PanelAppFrame currentUrl="/new-app" />);
|
|
59
|
+
const iframe = getByTestId("panel-app-frame") as HTMLIFrameElement;
|
|
60
|
+
const postMessage = vi.spyOn(iframe.contentWindow!, "postMessage");
|
|
61
|
+
|
|
62
|
+
fireEvent.load(iframe);
|
|
63
|
+
|
|
64
|
+
expect(postMessage).not.toHaveBeenCalled();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { useCallback, useEffect, type RefObject } from "react";
|
|
2
|
+
import { PANEL_APP_SCROLL_RESTORATION_CONTRACT } from "@nextclaw/shared";
|
|
3
|
+
import { scrollRestorationManager } from "@/shared/lib/navigation-history";
|
|
4
|
+
|
|
5
|
+
type ScrollTarget =
|
|
6
|
+
| { kind: "document" }
|
|
7
|
+
| { kind: "element"; path: Array<{ index: number; tagName: string }> };
|
|
8
|
+
|
|
9
|
+
type PanelAppScrollSnapshot = {
|
|
10
|
+
currentUrl: string;
|
|
11
|
+
target: ScrollTarget;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type UsePanelAppScrollRestorationParams = {
|
|
15
|
+
currentUrl: string | null;
|
|
16
|
+
iframeRef: RefObject<HTMLIFrameElement>;
|
|
17
|
+
isEnabled: boolean;
|
|
18
|
+
restorationKey: string | null;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function isScrollTarget(value: unknown): value is ScrollTarget {
|
|
22
|
+
if (!value || typeof value !== "object") return false;
|
|
23
|
+
const candidate = value as Partial<ScrollTarget>;
|
|
24
|
+
if (candidate.kind === "document") return true;
|
|
25
|
+
return candidate.kind === "element"
|
|
26
|
+
&& Array.isArray(candidate.path)
|
|
27
|
+
&& candidate.path.length > 0
|
|
28
|
+
&& candidate.path.length <= 30
|
|
29
|
+
&& candidate.path.every((segment) => (
|
|
30
|
+
typeof segment?.index === "number"
|
|
31
|
+
&& Number.isInteger(segment.index)
|
|
32
|
+
&& segment.index >= 0
|
|
33
|
+
&& segment.index <= 1000
|
|
34
|
+
&& typeof segment.tagName === "string"
|
|
35
|
+
&& segment.tagName.length > 0
|
|
36
|
+
&& segment.tagName.length <= 32
|
|
37
|
+
));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isPanelAppScrollSnapshot(value: unknown): value is PanelAppScrollSnapshot {
|
|
41
|
+
if (!value || typeof value !== "object") return false;
|
|
42
|
+
const candidate = value as Partial<PanelAppScrollSnapshot>;
|
|
43
|
+
return typeof candidate.currentUrl === "string" && isScrollTarget(candidate.target);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isScrollMessage(value: unknown): value is {
|
|
47
|
+
target: ScrollTarget;
|
|
48
|
+
x: number;
|
|
49
|
+
y: number;
|
|
50
|
+
} {
|
|
51
|
+
if (!value || typeof value !== "object") return false;
|
|
52
|
+
const candidate = value as Partial<{
|
|
53
|
+
target: unknown;
|
|
54
|
+
type: string;
|
|
55
|
+
version: number;
|
|
56
|
+
x: number;
|
|
57
|
+
y: number;
|
|
58
|
+
}>;
|
|
59
|
+
return candidate.type === PANEL_APP_SCROLL_RESTORATION_CONTRACT.scrollMessageType
|
|
60
|
+
&& candidate.version === PANEL_APP_SCROLL_RESTORATION_CONTRACT.version
|
|
61
|
+
&& isScrollTarget(candidate.target)
|
|
62
|
+
&& typeof candidate.x === "number"
|
|
63
|
+
&& Number.isFinite(candidate.x)
|
|
64
|
+
&& candidate.x >= 0
|
|
65
|
+
&& typeof candidate.y === "number"
|
|
66
|
+
&& Number.isFinite(candidate.y)
|
|
67
|
+
&& candidate.y >= 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Bridges the Panel App iframe scroll protocol to the shared navigation history. */
|
|
71
|
+
export function usePanelAppScrollRestoration({
|
|
72
|
+
currentUrl,
|
|
73
|
+
iframeRef,
|
|
74
|
+
isEnabled,
|
|
75
|
+
restorationKey,
|
|
76
|
+
}: UsePanelAppScrollRestorationParams) {
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (!currentUrl || !isEnabled || !restorationKey) return;
|
|
79
|
+
const onMessage = (event: MessageEvent) => {
|
|
80
|
+
if (event.source !== iframeRef.current?.contentWindow || !isScrollMessage(event.data)) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
scrollRestorationManager.save(restorationKey, {
|
|
84
|
+
x: event.data.x,
|
|
85
|
+
y: event.data.y,
|
|
86
|
+
payload: { currentUrl, target: event.data.target } satisfies PanelAppScrollSnapshot,
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
window.addEventListener("message", onMessage);
|
|
90
|
+
return () => window.removeEventListener("message", onMessage);
|
|
91
|
+
}, [currentUrl, iframeRef, isEnabled, restorationKey]);
|
|
92
|
+
|
|
93
|
+
return useCallback(() => {
|
|
94
|
+
if (!currentUrl || !isEnabled || !restorationKey) return;
|
|
95
|
+
const position = scrollRestorationManager.read(restorationKey);
|
|
96
|
+
if (!position || !isPanelAppScrollSnapshot(position.payload) || position.payload.currentUrl !== currentUrl) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
iframeRef.current?.contentWindow?.postMessage({
|
|
100
|
+
type: PANEL_APP_SCROLL_RESTORATION_CONTRACT.restoreScrollMessageType,
|
|
101
|
+
version: PANEL_APP_SCROLL_RESTORATION_CONTRACT.version,
|
|
102
|
+
target: position.payload.target,
|
|
103
|
+
x: position.x,
|
|
104
|
+
y: position.y,
|
|
105
|
+
}, "*");
|
|
106
|
+
}, [currentUrl, iframeRef, isEnabled, restorationKey]);
|
|
107
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { fireEvent, render } from "@testing-library/react";
|
|
2
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { useScrollRestoration } from "@/shared/hooks/use-scroll-restoration";
|
|
4
|
+
import { scrollRestorationManager } from "@/shared/lib/navigation-history";
|
|
5
|
+
|
|
6
|
+
function ScrollSurface({ restorationKey }: { restorationKey: string }) {
|
|
7
|
+
const scrollRestoration = useScrollRestoration<HTMLDivElement>({ restorationKey });
|
|
8
|
+
const { onScroll, scrollRef } = scrollRestoration;
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
ref={scrollRef}
|
|
12
|
+
data-testid="scroll-surface"
|
|
13
|
+
onScroll={onScroll}
|
|
14
|
+
/>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("useScrollRestoration", () => {
|
|
19
|
+
beforeEach(() => scrollRestorationManager.clear());
|
|
20
|
+
|
|
21
|
+
it("restores an existing position during layout and saves scroll events", () => {
|
|
22
|
+
scrollRestorationManager.save("page:first", { x: 16, y: 48 });
|
|
23
|
+
const { getByTestId } = render(<ScrollSurface restorationKey="page:first" />);
|
|
24
|
+
const surface = getByTestId("scroll-surface") as HTMLDivElement;
|
|
25
|
+
|
|
26
|
+
expect(surface.scrollLeft).toBe(16);
|
|
27
|
+
expect(surface.scrollTop).toBe(48);
|
|
28
|
+
|
|
29
|
+
surface.scrollLeft = 24;
|
|
30
|
+
surface.scrollTop = 72;
|
|
31
|
+
fireEvent.scroll(surface);
|
|
32
|
+
|
|
33
|
+
expect(scrollRestorationManager.read("page:first")).toEqual({ x: 24, y: 72 });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("saves the previous key and restores the next key when navigation changes", () => {
|
|
37
|
+
scrollRestorationManager.save("page:second", { x: 0, y: 96 });
|
|
38
|
+
const view = render(<ScrollSurface restorationKey="page:first" />);
|
|
39
|
+
const surface = view.getByTestId("scroll-surface") as HTMLDivElement;
|
|
40
|
+
surface.scrollTop = 32;
|
|
41
|
+
fireEvent.scroll(surface);
|
|
42
|
+
|
|
43
|
+
view.rerender(<ScrollSurface restorationKey="page:second" />);
|
|
44
|
+
|
|
45
|
+
expect(scrollRestorationManager.read("page:first")).toEqual({ x: 0, y: 32 });
|
|
46
|
+
expect(surface.scrollTop).toBe(96);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("keeps the last scroll event when React resets the element during navigation", () => {
|
|
50
|
+
const view = render(<ScrollSurface restorationKey="page:first" />);
|
|
51
|
+
const surface = view.getByTestId("scroll-surface") as HTMLDivElement;
|
|
52
|
+
surface.scrollTop = 144;
|
|
53
|
+
fireEvent.scroll(surface);
|
|
54
|
+
|
|
55
|
+
// Switching the content can reset the DOM scroll position before the
|
|
56
|
+
// previous layout effect's cleanup runs.
|
|
57
|
+
surface.scrollTop = 0;
|
|
58
|
+
view.rerender(<ScrollSurface restorationKey="page:second" />);
|
|
59
|
+
|
|
60
|
+
expect(scrollRestorationManager.read("page:first")).toEqual({ x: 0, y: 144 });
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { useCallback, useLayoutEffect, useRef, type RefObject, type UIEvent } from "react";
|
|
2
|
+
import {
|
|
3
|
+
scrollRestorationManager,
|
|
4
|
+
type ScrollRestorationPosition,
|
|
5
|
+
} from "@/shared/lib/navigation-history";
|
|
6
|
+
|
|
7
|
+
type UseScrollRestorationParams<T extends HTMLElement> = {
|
|
8
|
+
restorationKey: string | null;
|
|
9
|
+
scrollRef?: RefObject<T>;
|
|
10
|
+
isEnabled?: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** Connects one DOM scroll surface to the shared in-memory navigation history. */
|
|
14
|
+
export function useScrollRestoration<T extends HTMLElement>({
|
|
15
|
+
restorationKey,
|
|
16
|
+
scrollRef,
|
|
17
|
+
isEnabled = true,
|
|
18
|
+
}: UseScrollRestorationParams<T>) {
|
|
19
|
+
const internalScrollRef = useRef<T>(null);
|
|
20
|
+
const lastKnownPositionRef = useRef<ScrollRestorationPosition | null>(null);
|
|
21
|
+
const resolvedScrollRef = scrollRef ?? internalScrollRef;
|
|
22
|
+
|
|
23
|
+
useLayoutEffect(() => {
|
|
24
|
+
if (!isEnabled || !restorationKey) return;
|
|
25
|
+
const element = resolvedScrollRef.current;
|
|
26
|
+
const position = scrollRestorationManager.read(restorationKey);
|
|
27
|
+
if (element && position) {
|
|
28
|
+
element.scrollLeft = position.x;
|
|
29
|
+
element.scrollTop = position.y;
|
|
30
|
+
}
|
|
31
|
+
if (element) {
|
|
32
|
+
lastKnownPositionRef.current = {
|
|
33
|
+
x: element.scrollLeft,
|
|
34
|
+
y: element.scrollTop,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return () => {
|
|
39
|
+
if (!element) return;
|
|
40
|
+
scrollRestorationManager.save(
|
|
41
|
+
restorationKey,
|
|
42
|
+
lastKnownPositionRef.current ?? {
|
|
43
|
+
x: element.scrollLeft,
|
|
44
|
+
y: element.scrollTop,
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
}, [isEnabled, restorationKey, resolvedScrollRef]);
|
|
49
|
+
|
|
50
|
+
const onScroll = useCallback((event: UIEvent<T>) => {
|
|
51
|
+
if (!isEnabled || !restorationKey) return;
|
|
52
|
+
const position = {
|
|
53
|
+
x: event.currentTarget.scrollLeft,
|
|
54
|
+
y: event.currentTarget.scrollTop,
|
|
55
|
+
};
|
|
56
|
+
lastKnownPositionRef.current = position;
|
|
57
|
+
scrollRestorationManager.save(restorationKey, position);
|
|
58
|
+
}, [isEnabled, restorationKey]);
|
|
59
|
+
|
|
60
|
+
return { onScroll, scrollRef: resolvedScrollRef };
|
|
61
|
+
}
|
|
@@ -8,6 +8,7 @@ export * from './managers/client.manager';
|
|
|
8
8
|
export * from './utils/config.utils';
|
|
9
9
|
export * from './utils/marketplace.utils';
|
|
10
10
|
export * from './utils/mcp-marketplace.utils';
|
|
11
|
+
export * from './utils/mcp-connection.utils';
|
|
11
12
|
export * from './utils/ncp-attachments.utils';
|
|
12
13
|
export * from './utils/ncp-session.utils';
|
|
13
14
|
export * from './projects/project.utils';
|
|
@@ -149,6 +149,7 @@ export function applyNcpSessionRealtimeEvent(
|
|
|
149
149
|
queryClient: QueryClient | undefined,
|
|
150
150
|
event: Extract<WsEvent, { type: 'session.run-status' | 'session.summary.upsert' | 'session.summary.delete' }>
|
|
151
151
|
): void {
|
|
152
|
+
void queryClient?.invalidateQueries({ queryKey: ['ncp-session-pages'] });
|
|
152
153
|
if (event.type === 'session.run-status') {
|
|
153
154
|
updateNcpSessionRunStatusInQueryClient(queryClient, event.payload);
|
|
154
155
|
return;
|
|
@@ -103,6 +103,9 @@ export type NcpSessionSummaryView = NcpSessionSummary;
|
|
|
103
103
|
export type NcpSessionsListView = {
|
|
104
104
|
sessions: NcpSessionSummaryView[];
|
|
105
105
|
total: number;
|
|
106
|
+
page?: number;
|
|
107
|
+
pageSize?: number;
|
|
108
|
+
hasMore?: boolean;
|
|
106
109
|
};
|
|
107
110
|
|
|
108
111
|
export type NcpMessageView = NcpMessage;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { McpConnectionCreateResult, McpConnectionRequest, McpConnectionTestResult } from "@nextclaw/client-sdk";
|
|
2
|
+
import { nextclawClient } from "@/shared/lib/api/managers/client.manager";
|
|
3
|
+
|
|
4
|
+
export async function testMcpConnection(request: McpConnectionRequest): Promise<McpConnectionTestResult> {
|
|
5
|
+
return await nextclawClient.mcp.testConnection(request);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function createMcpConnection(request: McpConnectionRequest): Promise<McpConnectionCreateResult> {
|
|
9
|
+
return await nextclawClient.mcp.createConnection(request);
|
|
10
|
+
}
|
|
@@ -14,6 +14,9 @@ import type {
|
|
|
14
14
|
// GET /api/ncp/sessions
|
|
15
15
|
export async function fetchNcpSessions(params?: {
|
|
16
16
|
limit?: number;
|
|
17
|
+
page?: number;
|
|
18
|
+
pageSize?: number;
|
|
19
|
+
query?: string;
|
|
17
20
|
peerId?: string;
|
|
18
21
|
}): Promise<NcpSessionsListView> {
|
|
19
22
|
return (await nextclawClient.sessions.list(params)) as NcpSessionsListView;
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"chatWorkspaceContinuousAttentionEmpty": "No continuous attention",
|
|
81
81
|
"chatWorkspaceContinuousAttentionEmptyDescription": "The Agent has not established this type of ongoing relationship for the session.",
|
|
82
82
|
"chatWorkspaceTokenUsage": "Token usage",
|
|
83
|
-
"chatWorkspaceTokenUsageDescription": "
|
|
83
|
+
"chatWorkspaceTokenUsageDescription": "Summarizes this session's recorded Agent rounds. Cached input is included in input tokens; cache read ratio is cached reads divided by all input tokens.",
|
|
84
84
|
"chatWorkspaceTokenUsageLoading": "Calculating token usage…",
|
|
85
85
|
"chatWorkspaceTokenUsageLoadFailed": "Couldn’t load token usage.",
|
|
86
86
|
"chatWorkspaceTokenUsageRetry": "Retry",
|
|
@@ -89,11 +89,14 @@
|
|
|
89
89
|
"chatWorkspaceTokenUsageInput": "Input",
|
|
90
90
|
"chatWorkspaceTokenUsageOutput": "Output",
|
|
91
91
|
"chatWorkspaceTokenUsageCachedInput": "Cached input",
|
|
92
|
-
"chatWorkspaceTokenUsageCacheHitRate": "Cache
|
|
93
|
-
"chatWorkspaceTokenUsageRuns": "
|
|
92
|
+
"chatWorkspaceTokenUsageCacheHitRate": "Cache read ratio",
|
|
93
|
+
"chatWorkspaceTokenUsageRuns": "Agent rounds",
|
|
94
|
+
"chatWorkspaceTokenUsageModelCalls": "Model calls",
|
|
95
|
+
"chatWorkspaceTokenUsageReportedModelCalls": "Reported calls",
|
|
94
96
|
"chatWorkspaceTokenUsageByModel": "By model",
|
|
95
|
-
"chatWorkspaceTokenUsageModelRunCount": "
|
|
96
|
-
"
|
|
97
|
+
"chatWorkspaceTokenUsageModelRunCount": "{count} agent rounds",
|
|
98
|
+
"chatWorkspaceTokenUsageModelCallCount": "{count} model calls",
|
|
99
|
+
"chatWorkspaceTokenUsageModelCacheHitRate": "Cache read ratio: {rate}",
|
|
97
100
|
"chatWorkspaceTokenUsageReported": "Usage reported",
|
|
98
101
|
"chatWorkspaceTokenUsagePartial": "Partial usage",
|
|
99
102
|
"chatWorkspaceTokenUsageUnavailable": "Usage unavailable",
|
|
@@ -473,6 +476,8 @@
|
|
|
473
476
|
"chatSteeringPending": "Waiting for the next step",
|
|
474
477
|
"chatDeleteSession": "Delete Session",
|
|
475
478
|
"chatDeleteSessionConfirm": "Delete the current session?",
|
|
479
|
+
"chatDeleteSessionSucceeded": "Session deleted.",
|
|
480
|
+
"chatDeleteSessionFailed": "Could not delete session",
|
|
476
481
|
"chatSessionMoreActions": "More actions",
|
|
477
482
|
"chatSessionCopyId": "Copy session ID",
|
|
478
483
|
"chatSessionCopyIdSuccess": "Session ID copied",
|
|
@@ -26,6 +26,49 @@
|
|
|
26
26
|
"sidebarExpand": "Expand sidebar",
|
|
27
27
|
"security": "Sign-in Management",
|
|
28
28
|
"privacy": "Privacy & Analytics",
|
|
29
|
+
"desktopCapabilities": "Desktop Access",
|
|
30
|
+
"desktopCapabilitiesPageTitle": "Desktop Access",
|
|
31
|
+
"desktopCapabilitiesPageDescription": "Check NextClaw Desktop system permissions and manage which desktop apps AI agents and Extensions can access.",
|
|
32
|
+
"desktopCapabilitiesSystemTitle": "System permission",
|
|
33
|
+
"desktopCapabilitiesSystemDescription": "macOS only needs permission for NextClaw Desktop. Each AI agent or Extension still needs separate access to an app.",
|
|
34
|
+
"desktopCapabilitiesHostStatus": "Desktop Host",
|
|
35
|
+
"desktopCapabilitiesHostOnline": "Connected and ready to check desktop apps and system permissions.",
|
|
36
|
+
"desktopCapabilitiesHostOffline": "Not connected. Start NextClaw Desktop first.",
|
|
37
|
+
"desktopCapabilitiesRecheck": "Check again",
|
|
38
|
+
"desktopCapabilitiesAccessibility": "Accessibility",
|
|
39
|
+
"desktopCapabilitiesOpenSettings": "Open System Settings",
|
|
40
|
+
"desktopCapabilitiesRequestPermission": "Request permission",
|
|
41
|
+
"desktopCapabilitiesPermissionGranted": "NextClaw Desktop can control local apps.",
|
|
42
|
+
"desktopCapabilitiesPermissionNotGranted": "Not allowed yet. Open System Settings and enable Accessibility for NextClaw Desktop.",
|
|
43
|
+
"desktopCapabilitiesPermissionNotSupported": "Desktop access is not supported on this system.",
|
|
44
|
+
"desktopCapabilitiesPermissionUnknown": "The permission state is currently unavailable.",
|
|
45
|
+
"desktopCapabilitiesGrantsTitle": "App access",
|
|
46
|
+
"desktopCapabilitiesGrantsDescription": "These permissions decide which AI agent or Extension can read or operate an app. Revoking access stops related operations immediately.",
|
|
47
|
+
"desktopCapabilitiesNoGrants": "No app access yet",
|
|
48
|
+
"desktopCapabilitiesNoGrantsDescription": "NextClaw will ask for permission when an AI agent or Extension first accesses a desktop app.",
|
|
49
|
+
"desktopCapabilitiesAgent": "AI",
|
|
50
|
+
"desktopCapabilitiesExtension": "Extension",
|
|
51
|
+
"desktopCapabilitiesUnknownApplication": "Unknown app",
|
|
52
|
+
"desktopCapabilitiesRevoke": "Revoke access",
|
|
53
|
+
"desktopAuthorizationTitle": "Allow desktop access?",
|
|
54
|
+
"desktopAuthorizationDescription": "This action needs access to a local app. Check who is asking, which app, and what they can do.",
|
|
55
|
+
"desktopAuthorizationCaller": "Requested by",
|
|
56
|
+
"desktopAuthorizationApplication": "App",
|
|
57
|
+
"desktopAuthorizationAccess": "Access",
|
|
58
|
+
"desktopAuthorizationRead": "Read visible content",
|
|
59
|
+
"desktopAuthorizationObserve": "Watch visible changes",
|
|
60
|
+
"desktopAuthorizationWrite": "Write to the app",
|
|
61
|
+
"desktopAuthorizationScreenCapture": "Capture visible window content",
|
|
62
|
+
"desktopAuthorizationPointerInput": "Click inside the app window",
|
|
63
|
+
"desktopAuthorizationReadRisk": "Once allowed, this requester can read or watch currently visible content in this app until you revoke access in Desktop Access.",
|
|
64
|
+
"desktopAuthorizationWriteRisk": "Once allowed, this AI agent can write a draft into this app. This permission does not allow sending or confirming.",
|
|
65
|
+
"desktopAuthorizationScreenCaptureRisk": "Once allowed, this requester can capture the currently visible content of this app window for AI understanding until you revoke access in Desktop Access.",
|
|
66
|
+
"desktopAuthorizationPointerInputRisk": "Once allowed, this AI can click inside this app's authorized window. It cannot use keys, scroll, drag, send, or confirm.",
|
|
67
|
+
"desktopAuthorizationReject": "Don't allow",
|
|
68
|
+
"desktopAuthorizationAllow": "Allow",
|
|
69
|
+
"desktopAuthorizationAllowing": "Allowing…",
|
|
70
|
+
"desktopAuthorizationAllowed": "Access allowed. The AI can retry the previous action.",
|
|
71
|
+
"desktopAuthorizationFailed": "Desktop access could not be saved.",
|
|
29
72
|
"backToMain": "Back to Main",
|
|
30
73
|
"enabled": "Enabled",
|
|
31
74
|
"disabled": "Disabled",
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
"cronNextPage": "Next page",
|
|
18
18
|
"cronTotalLabel": "Total",
|
|
19
19
|
"cronLoading": "Loading scheduled tasks...",
|
|
20
|
+
"cronLoadFailed": "Scheduled tasks could not be loaded right now.",
|
|
21
|
+
"cronRetry": "Retry",
|
|
20
22
|
"cronEmpty": "No scheduled tasks yet.",
|
|
21
23
|
"cronScheduleLabel": "Schedule",
|
|
22
24
|
"cronSessionLabel": "Execution session",
|
|
@@ -64,8 +66,8 @@
|
|
|
64
66
|
"cronRunFailed": "Latest run failed",
|
|
65
67
|
"cronBoundSession": "Bound session",
|
|
66
68
|
"cronDedicatedSession": "Dedicated session",
|
|
67
|
-
"cronEditDetails": "View
|
|
68
|
-
"cronDetailDescription": "Review the task and its latest run.
|
|
69
|
+
"cronEditDetails": "View task details",
|
|
70
|
+
"cronDetailDescription": "Review the task and its latest run. To change it, return to the execution session and describe what you need.",
|
|
69
71
|
"cronTaskPrompt": "Task prompt",
|
|
70
72
|
"cronTaskDetails": "Task settings",
|
|
71
73
|
"cronAgentLabel": "Execution Agent",
|