@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
|
@@ -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
|
}
|
|
@@ -75,14 +75,7 @@ const IconActionButton = React.forwardRef<HTMLButtonElement, IconActionButtonPro
|
|
|
75
75
|
return (
|
|
76
76
|
<TooltipProvider delayDuration={250}>
|
|
77
77
|
<Tooltip disableHoverableContent>
|
|
78
|
-
<TooltipTrigger
|
|
79
|
-
asChild
|
|
80
|
-
onFocus={(event) => {
|
|
81
|
-
if (!event.currentTarget.matches(':focus-visible')) {
|
|
82
|
-
event.preventDefault();
|
|
83
|
-
}
|
|
84
|
-
}}
|
|
85
|
-
>
|
|
78
|
+
<TooltipTrigger asChild>
|
|
86
79
|
{disabled ? <span className="inline-flex">{button}</span> : button}
|
|
87
80
|
</TooltipTrigger>
|
|
88
81
|
<TooltipContent side={tooltipSide} className="text-xs">{content}</TooltipContent>
|
|
@@ -7,7 +7,22 @@ const TooltipProvider = TooltipPrimitive.Provider;
|
|
|
7
7
|
|
|
8
8
|
const Tooltip = TooltipPrimitive.Root;
|
|
9
9
|
|
|
10
|
-
const TooltipTrigger =
|
|
10
|
+
const TooltipTrigger = React.forwardRef<
|
|
11
|
+
React.ElementRef<typeof TooltipPrimitive.Trigger>,
|
|
12
|
+
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Trigger>
|
|
13
|
+
>(({ onFocus, ...props }, ref) => (
|
|
14
|
+
<TooltipPrimitive.Trigger
|
|
15
|
+
ref={ref}
|
|
16
|
+
onFocus={(event) => {
|
|
17
|
+
onFocus?.(event);
|
|
18
|
+
if (!event.defaultPrevented && !event.currentTarget.matches(':focus-visible')) {
|
|
19
|
+
event.preventDefault();
|
|
20
|
+
}
|
|
21
|
+
}}
|
|
22
|
+
{...props}
|
|
23
|
+
/>
|
|
24
|
+
));
|
|
25
|
+
TooltipTrigger.displayName = TooltipPrimitive.Trigger.displayName;
|
|
11
26
|
|
|
12
27
|
const TooltipContent = React.forwardRef<
|
|
13
28
|
React.ElementRef<typeof TooltipPrimitive.Content>,
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
updateChannel,
|
|
20
20
|
updateRuntime,
|
|
21
21
|
updateProductAnalytics,
|
|
22
|
+
fetchProductAnalyticsStatus,
|
|
22
23
|
updateSecrets,
|
|
23
24
|
executeConfigAction
|
|
24
25
|
} from '@/shared/lib/api';
|
|
@@ -43,6 +44,15 @@ export function useConfigMeta() {
|
|
|
43
44
|
});
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
export function useProductAnalyticsStatus() {
|
|
48
|
+
return useQuery({
|
|
49
|
+
queryKey: ['product-analytics-status'],
|
|
50
|
+
queryFn: fetchProductAnalyticsStatus,
|
|
51
|
+
staleTime: 15_000,
|
|
52
|
+
refetchOnWindowFocus: true
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
46
56
|
export function useProviders() {
|
|
47
57
|
return useQuery({
|
|
48
58
|
queryKey: ['providers'],
|
|
@@ -108,6 +118,7 @@ export function useUpdateProductAnalytics() {
|
|
|
108
118
|
queryClient.setQueryData<ConfigView>(['config'], (current) => current
|
|
109
119
|
? { ...current, productAnalytics }
|
|
110
120
|
: current);
|
|
121
|
+
queryClient.invalidateQueries({ queryKey: ['product-analytics-status'] });
|
|
111
122
|
toast.success(t('configSavedApplied'));
|
|
112
123
|
},
|
|
113
124
|
onError: (error: Error) => {
|
|
@@ -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
|
+
}
|
|
@@ -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;
|
|
@@ -488,6 +488,7 @@ export type PanelAppListView = {
|
|
|
488
488
|
workspacePath: string;
|
|
489
489
|
panelsPath: string;
|
|
490
490
|
entries: PanelAppEntryView[];
|
|
491
|
+
unavailablePackages?: Array<{ appId: string; message: string }>;
|
|
491
492
|
};
|
|
492
493
|
|
|
493
494
|
export type PanelAppPreferencesUpdateView = {
|
|
@@ -638,6 +639,7 @@ export type SecretsConfigUpdate = {
|
|
|
638
639
|
export type ProductAnalyticsAudience = 'external' | 'internal' | 'qa';
|
|
639
640
|
|
|
640
641
|
export type ProductAnalyticsView = {
|
|
642
|
+
schemaVersion: 2;
|
|
641
643
|
enabled: boolean;
|
|
642
644
|
audience: ProductAnalyticsAudience;
|
|
643
645
|
};
|
|
@@ -647,6 +649,13 @@ export type ProductAnalyticsConfigUpdate = {
|
|
|
647
649
|
audience?: ProductAnalyticsAudience;
|
|
648
650
|
};
|
|
649
651
|
|
|
652
|
+
export type ProductAnalyticsStatusView = {
|
|
653
|
+
lastAttemptAt: string | null;
|
|
654
|
+
lastSuccessAt: string | null;
|
|
655
|
+
lastError: string | null;
|
|
656
|
+
pendingReceiptCount: number;
|
|
657
|
+
};
|
|
658
|
+
|
|
650
659
|
export type ChannelConfigUpdate = Record<string, unknown>;
|
|
651
660
|
|
|
652
661
|
export type ConfigView = {
|
|
@@ -31,6 +31,7 @@ import type {
|
|
|
31
31
|
ProviderCreateResult,
|
|
32
32
|
ProviderDeleteResult,
|
|
33
33
|
ProductAnalyticsConfigUpdate,
|
|
34
|
+
ProductAnalyticsStatusView,
|
|
34
35
|
ProductAnalyticsView,
|
|
35
36
|
RuntimeConfigUpdate,
|
|
36
37
|
SecretsConfigUpdate,
|
|
@@ -178,6 +179,10 @@ export async function updateProductAnalytics(
|
|
|
178
179
|
return await nextclawClient.config.updateProductAnalytics(data);
|
|
179
180
|
}
|
|
180
181
|
|
|
182
|
+
export async function fetchProductAnalyticsStatus(): Promise<ProductAnalyticsStatusView> {
|
|
183
|
+
return await nextclawClient.config.fetchProductAnalyticsStatus();
|
|
184
|
+
}
|
|
185
|
+
|
|
181
186
|
export async function executeConfigAction(
|
|
182
187
|
actionId: string,
|
|
183
188
|
data: ConfigActionExecuteRequest
|
|
@@ -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;
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"chatWorkspaceBack": "Back in workspace",
|
|
45
45
|
"chatWorkspaceForward": "Forward in workspace",
|
|
46
46
|
"chatWorkspaceRefreshFile": "Refresh preview",
|
|
47
|
+
"chatWorkspaceMarkdownOutline": "Document outline",
|
|
47
48
|
"chatWorkspaceMaximizePanel": "Maximize workspace panel",
|
|
48
49
|
"chatWorkspaceRestorePanel": "Restore workspace panel",
|
|
49
50
|
"chatWorkspaceClosePanel": "Close workspace panel",
|
|
@@ -79,7 +80,7 @@
|
|
|
79
80
|
"chatWorkspaceContinuousAttentionEmpty": "No continuous attention",
|
|
80
81
|
"chatWorkspaceContinuousAttentionEmptyDescription": "The Agent has not established this type of ongoing relationship for the session.",
|
|
81
82
|
"chatWorkspaceTokenUsage": "Token usage",
|
|
82
|
-
"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.",
|
|
83
84
|
"chatWorkspaceTokenUsageLoading": "Calculating token usage…",
|
|
84
85
|
"chatWorkspaceTokenUsageLoadFailed": "Couldn’t load token usage.",
|
|
85
86
|
"chatWorkspaceTokenUsageRetry": "Retry",
|
|
@@ -88,11 +89,14 @@
|
|
|
88
89
|
"chatWorkspaceTokenUsageInput": "Input",
|
|
89
90
|
"chatWorkspaceTokenUsageOutput": "Output",
|
|
90
91
|
"chatWorkspaceTokenUsageCachedInput": "Cached input",
|
|
91
|
-
"chatWorkspaceTokenUsageCacheHitRate": "Cache
|
|
92
|
-
"chatWorkspaceTokenUsageRuns": "
|
|
92
|
+
"chatWorkspaceTokenUsageCacheHitRate": "Cache read ratio",
|
|
93
|
+
"chatWorkspaceTokenUsageRuns": "Agent rounds",
|
|
94
|
+
"chatWorkspaceTokenUsageModelCalls": "Model calls",
|
|
95
|
+
"chatWorkspaceTokenUsageReportedModelCalls": "Reported calls",
|
|
93
96
|
"chatWorkspaceTokenUsageByModel": "By model",
|
|
94
|
-
"chatWorkspaceTokenUsageModelRunCount": "
|
|
95
|
-
"
|
|
97
|
+
"chatWorkspaceTokenUsageModelRunCount": "{count} agent rounds",
|
|
98
|
+
"chatWorkspaceTokenUsageModelCallCount": "{count} model calls",
|
|
99
|
+
"chatWorkspaceTokenUsageModelCacheHitRate": "Cache read ratio: {rate}",
|
|
96
100
|
"chatWorkspaceTokenUsageReported": "Usage reported",
|
|
97
101
|
"chatWorkspaceTokenUsagePartial": "Partial usage",
|
|
98
102
|
"chatWorkspaceTokenUsageUnavailable": "Usage unavailable",
|
|
@@ -594,6 +598,12 @@
|
|
|
594
598
|
"chatSidebarUnpinProject": "Unpin project",
|
|
595
599
|
"chatSidebarExpandProject": "Expand project",
|
|
596
600
|
"chatSidebarCollapseProject": "Collapse project",
|
|
601
|
+
"chatSidebarContextSessions": "Sessions",
|
|
602
|
+
"chatSidebarContextProject": "Project",
|
|
603
|
+
"chatSidebarContextPath": "Path",
|
|
604
|
+
"chatSidebarContextChildSessions": "Child sessions",
|
|
605
|
+
"chatSidebarContextScheduledTasks": "Scheduled tasks",
|
|
606
|
+
"chatSidebarContextNoProject": "No project",
|
|
597
607
|
"chatSidebarProjectViewEmpty": "No projects yet",
|
|
598
608
|
"chatBackgroundReplyFallbackTitle": "New reply",
|
|
599
609
|
"chatBackgroundReplyFallbackPreview": "A reply is ready. Open the session to view it.",
|