@nextclaw/ui 0.15.1 → 0.15.2
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 +31 -0
- package/dist/assets/{api-b1R9VdeH.js → api-B7Qigvtm.js} +2 -2
- package/dist/assets/{appearance-settings-page-D2O9pch9.js → appearance-settings-page-CuE0Iiqj.js} +1 -1
- package/dist/assets/{book-open-B77lQ2Tg.js → book-open-Ba2f7YiG.js} +1 -1
- package/dist/assets/channels-list-page-CTsM6TcP.js +8 -0
- package/dist/assets/{chat-page-CZ7WxPir.js → chat-page-C_5dcuE3.js} +1 -1
- package/dist/assets/config-split-page-Fbb4PtTf.js +1 -0
- package/dist/assets/{confirm-dialog-C-NXSioX.js → confirm-dialog-D2EG3nqE.js} +1 -1
- package/dist/assets/{desktop-update-config-usHpXugP.js → desktop-update-config-BGR3HUi1.js} +1 -1
- package/dist/assets/{dist-jdGmbb6w.js → dist-7JTjycxE.js} +1 -1
- package/dist/assets/{dist-YtoqU-AT.js → dist-wXLhk4hl.js} +1 -1
- package/dist/assets/doc-browser-bHyXnxlj.js +1 -0
- package/dist/assets/doc-browser-context-CYAGm5d5.js +1 -0
- package/dist/assets/doc-browser-gTrQ3jCF.js +1 -0
- package/dist/assets/{es2015-bNpL2LKy.js → es2015-j1SRCIX5.js} +1 -1
- package/dist/assets/{external-link-BM6fk8q-.js → external-link-DxuPaDoK.js} +1 -1
- package/dist/assets/form-actions-BztL7P_T.js +1 -0
- package/dist/assets/{i18n-provider-CntWolRA.js → i18n-provider-B4oMxOFx.js} +1 -1
- package/dist/assets/index-CvrvblgX.css +1 -0
- package/dist/assets/index-Obd4JYU2.js +104 -0
- package/dist/assets/loader-circle-CdzgK4eH.js +1 -0
- package/dist/assets/{mcp-marketplace-page-CLmUstLN.js → mcp-marketplace-page-Bt7CuOoq.js} +2 -2
- package/dist/assets/mcp-marketplace-page-Ck3t78_n.js +1 -0
- package/dist/assets/model-config-page-DeKk9gvE.js +1 -0
- package/dist/assets/plus-Bx7wQbhG.js +1 -0
- package/dist/assets/{provider-scoped-model-input-Cm_a4GHf.js → provider-scoped-model-input-DsH2LF1q.js} +1 -1
- package/dist/assets/providers-config-page-BFnGp68F.js +1 -0
- package/dist/assets/{react-BN8uzPnp.js → react-UirzUuST.js} +1 -1
- package/dist/assets/{refresh-cw-BpVcFPmJ.js → refresh-cw-eT8BF-8B.js} +1 -1
- package/dist/assets/remote-CDkHZtA8.js +1 -0
- package/dist/assets/rotate-cw-1JqxHTuw.js +1 -0
- package/dist/assets/runtime-config-page-BelAqiNG.js +1 -0
- package/dist/assets/{save-BpFW5fpk.js → save-Cb2g0BOi.js} +1 -1
- package/dist/assets/{search-D0uO7enf.js → search-BcHjwdsb.js} +1 -1
- package/dist/assets/search-config-page-zPm6--Ig.js +1 -0
- package/dist/assets/secrets-config-page-Cxyp-KuQ.js +3 -0
- package/dist/assets/security-config-B_gCq9fn.js +1 -0
- package/dist/assets/{select-Bh-CrA4p.js → select-CdSEOwVM.js} +1 -1
- package/dist/assets/{setting-row-BJfCHRcb.js → setting-row-CTAtGexr.js} +1 -1
- package/dist/assets/{settings-2-Blzi1vr6.js → settings-2-2DWauOjY.js} +1 -1
- package/dist/assets/{side-dock-ZdaSrQVB.js → side-dock-DPvakJ0a.js} +2 -2
- package/dist/assets/{skeleton-BLlEzebZ.js → skeleton-P5oM14E1.js} +1 -1
- package/dist/assets/{switch-CFLJFNSd.js → switch-BU_UEULb.js} +1 -1
- package/dist/assets/{tag-chip-CzGihiQx.js → tag-chip-D-WsKoT1.js} +1 -1
- package/dist/assets/{tooltip-CdcrBjrC.js → tooltip-CjkOaHW7.js} +1 -1
- package/dist/assets/{use-config-CVPgsBmU.js → use-config--hx5oxQn.js} +1 -1
- package/dist/assets/{use-confirm-dialog-CSeBk9SF.js → use-confirm-dialog-CD9a_SK1.js} +1 -1
- package/dist/assets/x-DT5x45JU.js +1 -0
- package/dist/index.html +38 -30
- package/index.html +12 -4
- package/package.json +9 -9
- package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +29 -1
- package/src/app/components/layout/sidebar-items.tsx +43 -4
- package/src/app/components/layout/sidebar.tsx +83 -61
- package/src/app/configs/app-navigation.config.ts +49 -0
- package/src/app/styles/design-system.css +82 -0
- package/src/features/channels/components/config/channel-form.tsx +14 -11
- package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +40 -0
- package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +3 -1
- package/src/features/chat/components/conversation/chat-conversation-panel.tsx +1 -1
- package/src/features/chat/components/layout/__tests__/chat-sidebar-toolbar.test.tsx +48 -0
- package/src/features/chat/components/layout/__tests__/chat-sidebar-utility-menu.test.tsx +55 -0
- package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +0 -3
- package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +3 -4
- package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +29 -30
- package/src/features/chat/components/layout/chat-sidebar.tsx +21 -7
- package/src/features/chat/features/conversation/components/session-conversation-input.tsx +2 -1
- package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +22 -1
- package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +10 -5
- package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +0 -11
- package/src/features/chat/features/session/components/__tests__/chat-sidebar-project-groups.test.tsx +116 -0
- package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +108 -46
- package/src/features/chat/features/session/components/chat-sidebar-session-entry.tsx +6 -0
- package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +48 -24
- package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +0 -3
- package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +0 -4
- package/src/features/chat/features/session/utils/chat-sidebar-session-groups.utils.ts +25 -3
- package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +9 -4
- package/src/features/chat/features/welcome/components/chat-welcome.tsx +0 -5
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +159 -56
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +1 -0
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel.test.tsx +144 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +99 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +3 -1
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +66 -7
- package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +3 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +44 -1
- package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +45 -3
- package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +33 -2
- package/src/features/chat/managers/chat-session-list.manager.ts +32 -0
- package/src/features/chat/stores/chat-session-list.store.ts +28 -5
- package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +5 -12
- package/src/features/panel-apps/components/panel-app-list-item.tsx +6 -6
- package/src/features/panel-apps/index.ts +1 -0
- package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +2 -10
- package/src/features/panel-apps/utils/panel-app-iframe.utils.ts +6 -0
- package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +11 -3
- package/src/features/pwa/managers/pwa-shell-theme.manager.ts +3 -1
- package/src/features/service-apps/components/service-apps-panel.tsx +145 -35
- package/src/features/settings/components/config/provider-auth-section.tsx +1 -1
- package/src/features/settings/components/config/provider-form.tsx +11 -8
- package/src/features/settings/components/config/provider-models-section.tsx +7 -7
- package/src/features/settings/components/config/provider-pill-selector.tsx +1 -1
- package/src/features/settings/components/config/secrets-config-form.tsx +5 -4
- package/src/features/settings/pages/model-config-page.tsx +26 -25
- package/src/features/settings/pages/providers-config-page.tsx +10 -13
- package/src/features/settings/pages/search-config-page.tsx +8 -5
- package/src/features/system-status/components/config/runtime-config-editor.tsx +5 -4
- package/src/index.css +18 -0
- package/src/shared/components/config-split-page.tsx +6 -6
- package/src/shared/components/doc-browser/__tests__/doc-browser-url.utils.test.ts +73 -0
- package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +48 -0
- package/src/shared/components/doc-browser/doc-browser-context.tsx +4 -0
- package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +5 -1
- package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +2 -1
- package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +10 -10
- package/src/shared/components/doc-browser/doc-browser.tsx +6 -4
- package/src/shared/components/doc-browser/utils/doc-browser-url.utils.ts +62 -4
- package/src/shared/components/resizable-right-panel/__tests__/resizable-right-panel.test.tsx +18 -0
- package/src/shared/components/resizable-right-panel/resizable-right-panel.tsx +28 -3
- package/src/shared/components/ui/actions/__tests__/icon-action-button.test.tsx +36 -0
- package/src/shared/components/ui/actions/form-actions.tsx +25 -0
- package/src/shared/components/ui/actions/icon-action-button.tsx +25 -2
- package/src/shared/components/ui/card.tsx +12 -4
- package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +2 -1
- package/src/shared/hooks/use-server-path-browse.ts +26 -4
- package/src/shared/hooks/use-server-path-read.ts +11 -1
- package/src/shared/lib/api/utils/server-path.utils.ts +1 -0
- package/src/shared/lib/i18n/locales/en-US/chat.json +16 -0
- package/src/shared/lib/i18n/locales/en-US/core.json +3 -0
- package/src/shared/lib/i18n/locales/en-US/doc-browser.json +5 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +16 -0
- package/src/shared/lib/i18n/locales/zh-CN/core.json +3 -0
- package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +5 -0
- package/src/shared/lib/session-project/workspace-file-breadcrumb.utils.ts +9 -5
- package/src/shared/lib/theme/index.test.ts +21 -0
- package/src/shared/lib/theme/index.ts +6 -4
- package/src/vite-env.d.ts +2 -0
- package/dist/assets/channels-list-page-DcsIQc6A.js +0 -8
- package/dist/assets/config-split-page-BWQ6_Raq.js +0 -1
- package/dist/assets/doc-browser-B_Tf9n70.js +0 -1
- package/dist/assets/doc-browser-BdZPFOVa.js +0 -1
- package/dist/assets/doc-browser-context-DwokemUn.js +0 -1
- package/dist/assets/index-BIcEw90u.js +0 -104
- package/dist/assets/index-BiNSTKTU.css +0 -1
- package/dist/assets/loader-circle-MBgNSVIH.js +0 -1
- package/dist/assets/mcp-marketplace-page-rZkmWPMy.js +0 -1
- package/dist/assets/model-config-page-Di0ySJD2.js +0 -1
- package/dist/assets/plus-vDqW5Kxp.js +0 -1
- package/dist/assets/providers-config-page-BAf2W0MV.js +0 -1
- package/dist/assets/remote-CLQ0IBM8.js +0 -1
- package/dist/assets/rotate-cw-CvNBv7_w.js +0 -1
- package/dist/assets/runtime-config-page-CflvvoDc.js +0 -1
- package/dist/assets/search-config-page-D0klFUXd.js +0 -1
- package/dist/assets/secrets-config-page-dVZ631Wp.js +0 -3
- package/dist/assets/security-config-C5gnlkuj.js +0 -1
- package/dist/assets/x-BF2KQ--V.js +0 -1
- /package/dist/assets/{config-hints-fGnUjDe9.js → config-hints-C_iP1zl_.js} +0 -0
package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx
CHANGED
|
@@ -4,12 +4,20 @@ import type {
|
|
|
4
4
|
ChatFileOperationBlockViewModel,
|
|
5
5
|
ChatFilePreviewViewer,
|
|
6
6
|
} from "@nextclaw/agent-chat-ui";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
ChatMessageMarkdown,
|
|
9
|
+
FileOperationCodeSurface,
|
|
10
|
+
} from "@nextclaw/agent-chat-ui";
|
|
8
11
|
import type { ChatWorkspaceFileTab } from "@/features/chat/stores/chat-thread.store";
|
|
12
|
+
import { ChatSessionWorkspaceDirectoryBrowser } from "./chat-session-workspace-directory-browser";
|
|
9
13
|
import { ChatSessionWorkspaceFileBreadcrumbs } from "./chat-session-workspace-file-breadcrumbs";
|
|
14
|
+
import { useServerPathBrowse } from "@/shared/hooks/use-server-path-browse";
|
|
10
15
|
import { useServerPathRead } from "@/shared/hooks/use-server-path-read";
|
|
11
16
|
import { buildServerPathContentUrl } from "@/shared/lib/api";
|
|
12
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
buildLineDiff,
|
|
19
|
+
buildPreviewLines,
|
|
20
|
+
} from "@/features/chat/features/message/utils/file-operation/line-builder.utils";
|
|
13
21
|
import { t } from "@/shared/lib/i18n";
|
|
14
22
|
import { buildWorkspaceFileBreadcrumb } from "@/shared/lib/session-project";
|
|
15
23
|
import { cn } from "@/shared/lib/utils";
|
|
@@ -31,6 +39,13 @@ function resolveFilePreviewViewer(params: {
|
|
|
31
39
|
return params.viewer === "rendered" && /\.html?$/i.test(params.path) ? "rendered" : "source";
|
|
32
40
|
}
|
|
33
41
|
|
|
42
|
+
function appendPreviewRefreshVersion(url: string, refreshVersion: number): string {
|
|
43
|
+
if (refreshVersion <= 0) {
|
|
44
|
+
return url;
|
|
45
|
+
}
|
|
46
|
+
return `${url}${url.includes("?") ? "&" : "?"}refresh=${refreshVersion}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
34
49
|
function buildPreviewBlock(params: {
|
|
35
50
|
path: string;
|
|
36
51
|
text: string;
|
|
@@ -161,6 +176,7 @@ function WorkspaceHtmlRenderedPreview({
|
|
|
161
176
|
}
|
|
162
177
|
|
|
163
178
|
function WorkspacePreviewBody({
|
|
179
|
+
directoryQuery,
|
|
164
180
|
onFileOpen,
|
|
165
181
|
previewBlock,
|
|
166
182
|
previewKind,
|
|
@@ -169,6 +185,7 @@ function WorkspacePreviewBody({
|
|
|
169
185
|
previewUrl,
|
|
170
186
|
previewViewer,
|
|
171
187
|
}: {
|
|
188
|
+
directoryQuery: ReturnType<typeof useServerPathBrowse>;
|
|
172
189
|
onFileOpen: (action: ChatFileOpenActionViewModel) => void;
|
|
173
190
|
previewBlock: ChatFileOperationBlockViewModel | null;
|
|
174
191
|
previewKind: "text" | "markdown" | "binary";
|
|
@@ -177,8 +194,26 @@ function WorkspacePreviewBody({
|
|
|
177
194
|
previewUrl: string | null;
|
|
178
195
|
previewViewer: "source" | "rendered";
|
|
179
196
|
}) {
|
|
180
|
-
if (
|
|
181
|
-
return
|
|
197
|
+
if (directoryQuery.data) {
|
|
198
|
+
return (
|
|
199
|
+
<ChatSessionWorkspaceDirectoryBrowser
|
|
200
|
+
browseQuery={directoryQuery}
|
|
201
|
+
onFileOpen={onFileOpen}
|
|
202
|
+
renderStatus={(params) => <WorkspaceFilePreviewStatus {...params} />}
|
|
203
|
+
/>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (directoryQuery.isLoading && previewQuery.error && !previewBlock) {
|
|
208
|
+
return (
|
|
209
|
+
<WorkspaceFilePreviewStatus text={t("chatWorkspaceLoadingDirectory")} />
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if ((directoryQuery.isLoading || previewQuery.isLoading) && !previewBlock) {
|
|
214
|
+
return (
|
|
215
|
+
<WorkspaceFilePreviewStatus text={t("chatWorkspaceLoadingPreview")} />
|
|
216
|
+
);
|
|
182
217
|
}
|
|
183
218
|
|
|
184
219
|
if (previewQuery.data?.kind === "binary") {
|
|
@@ -229,6 +264,7 @@ function WorkspacePreviewBody({
|
|
|
229
264
|
|
|
230
265
|
type ChatSessionWorkspaceFilePreviewProps = {
|
|
231
266
|
file: ChatWorkspaceFileTab;
|
|
267
|
+
refreshVersion?: number;
|
|
232
268
|
sessionProjectRoot: string | null;
|
|
233
269
|
sessionWorkingDir: string | null;
|
|
234
270
|
onFileOpen: (action: ChatFileOpenActionViewModel) => void;
|
|
@@ -236,6 +272,7 @@ type ChatSessionWorkspaceFilePreviewProps = {
|
|
|
236
272
|
|
|
237
273
|
export function ChatSessionWorkspaceFilePreview({
|
|
238
274
|
file,
|
|
275
|
+
refreshVersion = 0,
|
|
239
276
|
sessionProjectRoot,
|
|
240
277
|
sessionWorkingDir,
|
|
241
278
|
onFileOpen,
|
|
@@ -246,6 +283,12 @@ export function ChatSessionWorkspaceFilePreview({
|
|
|
246
283
|
basePath: sessionWorkingDir,
|
|
247
284
|
enabled: isPreviewMode,
|
|
248
285
|
});
|
|
286
|
+
const directoryQuery = useServerPathBrowse({
|
|
287
|
+
path: file.path,
|
|
288
|
+
basePath: sessionWorkingDir,
|
|
289
|
+
includeFiles: true,
|
|
290
|
+
enabled: isPreviewMode,
|
|
291
|
+
});
|
|
249
292
|
const diffBlock = useMemo(
|
|
250
293
|
() => (file.viewMode === "diff" ? buildDiffBlock(file) : null),
|
|
251
294
|
[file],
|
|
@@ -256,14 +299,21 @@ export function ChatSessionWorkspaceFilePreview({
|
|
|
256
299
|
path: previewQuery.data?.resolvedPath ?? file.path,
|
|
257
300
|
serverKind: previewQuery.data?.kind,
|
|
258
301
|
});
|
|
259
|
-
const resolvedPath =
|
|
302
|
+
const resolvedPath =
|
|
303
|
+
directoryQuery.data?.currentPath ??
|
|
304
|
+
previewQuery.data?.resolvedPath ??
|
|
305
|
+
file.path;
|
|
306
|
+
const previewPathKind = directoryQuery.data ? "directory" : "file";
|
|
260
307
|
const previewViewer = resolveFilePreviewViewer({
|
|
261
308
|
path: resolvedPath,
|
|
262
309
|
viewer: file.previewViewer,
|
|
263
310
|
});
|
|
264
311
|
const previewUrl =
|
|
265
312
|
previewViewer === "rendered" && previewQuery.data?.resolvedPath
|
|
266
|
-
?
|
|
313
|
+
? appendPreviewRefreshVersion(
|
|
314
|
+
buildServerPathContentUrl(previewQuery.data.resolvedPath),
|
|
315
|
+
refreshVersion,
|
|
316
|
+
)
|
|
267
317
|
: null;
|
|
268
318
|
const previewBlock = useMemo(() => {
|
|
269
319
|
if (!isPreviewMode || !previewText) {
|
|
@@ -289,12 +339,20 @@ export function ChatSessionWorkspaceFilePreview({
|
|
|
289
339
|
() =>
|
|
290
340
|
buildWorkspaceFileBreadcrumb({
|
|
291
341
|
path: resolvedPath,
|
|
342
|
+
kind: previewPathKind,
|
|
292
343
|
sessionProjectRoot: breadcrumbBasePath,
|
|
293
344
|
line: file.line,
|
|
294
345
|
column: file.column,
|
|
295
346
|
truncated: isTruncated,
|
|
296
347
|
}),
|
|
297
|
-
[
|
|
348
|
+
[
|
|
349
|
+
breadcrumbBasePath,
|
|
350
|
+
file.column,
|
|
351
|
+
file.line,
|
|
352
|
+
isTruncated,
|
|
353
|
+
previewPathKind,
|
|
354
|
+
resolvedPath,
|
|
355
|
+
],
|
|
298
356
|
);
|
|
299
357
|
|
|
300
358
|
return (
|
|
@@ -309,6 +367,7 @@ export function ChatSessionWorkspaceFilePreview({
|
|
|
309
367
|
<WorkspaceDiffBody diffBlock={diffBlock} />
|
|
310
368
|
) : (
|
|
311
369
|
<WorkspacePreviewBody
|
|
370
|
+
directoryQuery={directoryQuery}
|
|
312
371
|
onFileOpen={onFileOpen}
|
|
313
372
|
previewBlock={previewBlock}
|
|
314
373
|
previewKind={previewKind}
|
package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx
CHANGED
|
@@ -10,6 +10,7 @@ import { t } from "@/shared/lib/i18n";
|
|
|
10
10
|
|
|
11
11
|
type ChatSessionWorkspacePanelContentProps = {
|
|
12
12
|
activeSelection: WorkspaceSelection;
|
|
13
|
+
filePreviewRefreshVersion: number;
|
|
13
14
|
sessionCronJobs: readonly CronJobView[];
|
|
14
15
|
sessionProjectRoot: string | null;
|
|
15
16
|
sessionWorkingDir: string | null;
|
|
@@ -67,6 +68,7 @@ function WorkspaceSideChatDraftHeader() {
|
|
|
67
68
|
|
|
68
69
|
export function ChatSessionWorkspacePanelContent({
|
|
69
70
|
activeSelection,
|
|
71
|
+
filePreviewRefreshVersion,
|
|
70
72
|
sessionCronJobs,
|
|
71
73
|
sessionProjectRoot,
|
|
72
74
|
sessionWorkingDir,
|
|
@@ -113,6 +115,7 @@ export function ChatSessionWorkspacePanelContent({
|
|
|
113
115
|
return (
|
|
114
116
|
<ChatSessionWorkspaceFilePreview
|
|
115
117
|
file={activeSelection.file}
|
|
118
|
+
refreshVersion={filePreviewRefreshVersion}
|
|
116
119
|
sessionProjectRoot={sessionProjectRoot}
|
|
117
120
|
sessionWorkingDir={sessionWorkingDir}
|
|
118
121
|
onFileOpen={presenter.chatThreadManager.openFilePreview}
|
package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
AlarmClock,
|
|
3
|
+
ArrowLeft,
|
|
4
|
+
ArrowRight,
|
|
5
|
+
FileCode2,
|
|
6
|
+
Maximize2,
|
|
7
|
+
MessageSquarePlus,
|
|
8
|
+
MessageSquareText,
|
|
9
|
+
Minimize2,
|
|
10
|
+
RefreshCw,
|
|
11
|
+
X,
|
|
12
|
+
} from "lucide-react";
|
|
2
13
|
import type { WorkspaceTabViewModel } from "@/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils";
|
|
3
14
|
import { AgentIdentityAvatar } from "@/shared/components/common/agent-identity";
|
|
4
15
|
import {
|
|
@@ -56,22 +67,54 @@ function buildCompactWorkspaceTabs(
|
|
|
56
67
|
export function WorkspaceTabsBar({
|
|
57
68
|
canGoBack,
|
|
58
69
|
canGoForward,
|
|
70
|
+
isMaximized = false,
|
|
59
71
|
onClose,
|
|
60
72
|
onGoBack,
|
|
61
73
|
onGoForward,
|
|
74
|
+
onRefreshFile,
|
|
75
|
+
onToggleMaximize,
|
|
62
76
|
tabs,
|
|
63
77
|
}: {
|
|
64
78
|
canGoBack: boolean;
|
|
65
79
|
canGoForward: boolean;
|
|
80
|
+
isMaximized?: boolean;
|
|
66
81
|
onClose: () => void;
|
|
67
82
|
onGoBack: () => void;
|
|
68
83
|
onGoForward: () => void;
|
|
84
|
+
onRefreshFile?: () => void;
|
|
85
|
+
onToggleMaximize?: () => void;
|
|
69
86
|
tabs: readonly WorkspaceTabViewModel[];
|
|
70
87
|
}) {
|
|
71
88
|
const compactTabs = buildCompactWorkspaceTabs(tabs);
|
|
72
89
|
const actions: CompactTabStripAction[] = [
|
|
73
90
|
{ key: "back", icon: <ArrowLeft className="h-4 w-4" />, label: t("chatWorkspaceBack"), disabled: !canGoBack, onClick: onGoBack },
|
|
74
91
|
{ key: "forward", icon: <ArrowRight className="h-4 w-4" />, label: t("chatWorkspaceForward"), disabled: !canGoForward, onClick: onGoForward },
|
|
92
|
+
...(onRefreshFile
|
|
93
|
+
? [
|
|
94
|
+
{
|
|
95
|
+
key: "refresh-file",
|
|
96
|
+
icon: <RefreshCw className="h-4 w-4" />,
|
|
97
|
+
label: t("chatWorkspaceRefreshFile"),
|
|
98
|
+
onClick: onRefreshFile,
|
|
99
|
+
},
|
|
100
|
+
]
|
|
101
|
+
: []),
|
|
102
|
+
...(onToggleMaximize
|
|
103
|
+
? [
|
|
104
|
+
{
|
|
105
|
+
key: "maximize",
|
|
106
|
+
icon: isMaximized ? (
|
|
107
|
+
<Minimize2 className="h-4 w-4" />
|
|
108
|
+
) : (
|
|
109
|
+
<Maximize2 className="h-4 w-4" />
|
|
110
|
+
),
|
|
111
|
+
label: isMaximized
|
|
112
|
+
? t("chatWorkspaceRestorePanel")
|
|
113
|
+
: t("chatWorkspaceMaximizePanel"),
|
|
114
|
+
onClick: onToggleMaximize,
|
|
115
|
+
},
|
|
116
|
+
]
|
|
117
|
+
: []),
|
|
75
118
|
{ key: "close", icon: <X className="h-4 w-4" />, label: t("chatWorkspaceClosePanel"), onClick: onClose },
|
|
76
119
|
];
|
|
77
120
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { useEffect, useMemo } from "react";
|
|
1
|
+
import { useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
2
3
|
import type { CronJobView } from "@/shared/lib/api";
|
|
3
4
|
import { useNcpChildSessionTabsView } from "@/features/chat/features/ncp/hooks/use-ncp-child-session-tabs-view";
|
|
4
5
|
import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
|
|
@@ -22,6 +23,8 @@ import {
|
|
|
22
23
|
canGoBackInNavigationHistory,
|
|
23
24
|
canGoForwardInNavigationHistory,
|
|
24
25
|
} from "@/shared/lib/navigation-history";
|
|
26
|
+
import { buildServerPathBrowseQueryKey } from "@/shared/hooks/use-server-path-browse";
|
|
27
|
+
import { buildServerPathReadQueryKey } from "@/shared/hooks/use-server-path-read";
|
|
25
28
|
|
|
26
29
|
type ChatSessionWorkspacePanelProps = {
|
|
27
30
|
sessionKey: string | null;
|
|
@@ -55,6 +58,9 @@ export function ChatSessionWorkspacePanel({
|
|
|
55
58
|
displayMode = "docked",
|
|
56
59
|
}: ChatSessionWorkspacePanelProps) {
|
|
57
60
|
const presenter = usePresenter();
|
|
61
|
+
const queryClient = useQueryClient();
|
|
62
|
+
const [isMaximized, setIsMaximized] = useState(false);
|
|
63
|
+
const [filePreviewRefreshVersion, setFilePreviewRefreshVersion] = useState(0);
|
|
58
64
|
const resolvedChildTabs = useNcpChildSessionTabsView(childSessionTabs);
|
|
59
65
|
const optimisticReadAtBySessionKey = useChatSessionListStore(
|
|
60
66
|
(state) => state.optimisticReadAtBySessionKey,
|
|
@@ -110,30 +116,66 @@ export function ChatSessionWorkspacePanel({
|
|
|
110
116
|
return null;
|
|
111
117
|
}
|
|
112
118
|
|
|
119
|
+
const isContainerMaximized = displayMode === "docked" && isMaximized;
|
|
120
|
+
const isOverlayPanel = displayMode === "overlay" || isContainerMaximized;
|
|
121
|
+
const activeFile =
|
|
122
|
+
activeSelection.kind === "file" && activeSelection.file.viewMode === "preview"
|
|
123
|
+
? activeSelection.file
|
|
124
|
+
: null;
|
|
125
|
+
const refreshActiveFile = activeFile
|
|
126
|
+
? () => {
|
|
127
|
+
void queryClient.invalidateQueries({
|
|
128
|
+
queryKey: buildServerPathReadQueryKey({
|
|
129
|
+
path: activeFile.path,
|
|
130
|
+
basePath: sessionWorkingDir,
|
|
131
|
+
}),
|
|
132
|
+
});
|
|
133
|
+
void queryClient.invalidateQueries({
|
|
134
|
+
queryKey: buildServerPathBrowseQueryKey({
|
|
135
|
+
path: activeFile.path,
|
|
136
|
+
basePath: sessionWorkingDir,
|
|
137
|
+
includeFiles: true,
|
|
138
|
+
}),
|
|
139
|
+
});
|
|
140
|
+
setFilePreviewRefreshVersion((value) => value + 1);
|
|
141
|
+
}
|
|
142
|
+
: undefined;
|
|
143
|
+
|
|
113
144
|
return (
|
|
114
145
|
<ResizableRightPanel
|
|
146
|
+
data-testid="chat-session-workspace-panel"
|
|
115
147
|
className={cn(
|
|
116
|
-
|
|
148
|
+
isOverlayPanel
|
|
117
149
|
? "bg-white"
|
|
118
150
|
: "hidden border-gray-200/70 bg-white/95 backdrop-blur-sm md:flex",
|
|
151
|
+
isContainerMaximized ? "shadow-xl" : null,
|
|
119
152
|
)}
|
|
120
153
|
defaultWidth={480}
|
|
121
154
|
minWidth={360}
|
|
122
155
|
maxWidth={860}
|
|
123
|
-
overlay={
|
|
156
|
+
overlay={isOverlayPanel}
|
|
157
|
+
overlayScope={isContainerMaximized ? "container" : "viewport"}
|
|
124
158
|
>
|
|
125
159
|
<WorkspaceTabsBar
|
|
126
160
|
tabs={workspaceTabs}
|
|
127
161
|
canGoBack={canGoBackInNavigationHistory(workspaceHistory)}
|
|
128
162
|
canGoForward={canGoForwardInNavigationHistory(workspaceHistory)}
|
|
163
|
+
isMaximized={isContainerMaximized}
|
|
129
164
|
onGoBack={presenter.chatThreadManager.goBackWorkspacePanel}
|
|
130
165
|
onGoForward={presenter.chatThreadManager.goForwardWorkspacePanel}
|
|
166
|
+
onRefreshFile={refreshActiveFile}
|
|
167
|
+
onToggleMaximize={
|
|
168
|
+
displayMode === "docked"
|
|
169
|
+
? () => setIsMaximized((value) => !value)
|
|
170
|
+
: undefined
|
|
171
|
+
}
|
|
131
172
|
onClose={presenter.chatThreadManager.closeWorkspacePanel}
|
|
132
173
|
/>
|
|
133
174
|
|
|
134
175
|
<div className="flex min-h-0 flex-1 flex-col bg-white">
|
|
135
176
|
<ChatSessionWorkspacePanelContent
|
|
136
177
|
activeSelection={activeSelection}
|
|
178
|
+
filePreviewRefreshVersion={filePreviewRefreshVersion}
|
|
137
179
|
sessionCronJobs={sessionCronJobs}
|
|
138
180
|
sessionProjectRoot={sessionProjectRoot}
|
|
139
181
|
sessionWorkingDir={sessionWorkingDir}
|
|
@@ -42,7 +42,10 @@ function resetChatSessionListManagerState() {
|
|
|
42
42
|
snapshot: {
|
|
43
43
|
...useChatSessionListStore.getState().snapshot,
|
|
44
44
|
selectedSessionKey: 'session-1',
|
|
45
|
-
listMode: 'time-first'
|
|
45
|
+
listMode: 'time-first',
|
|
46
|
+
pinnedSessionKeys: [],
|
|
47
|
+
pinnedProjectRoots: [],
|
|
48
|
+
collapsedProjectRoots: [],
|
|
46
49
|
}
|
|
47
50
|
});
|
|
48
51
|
useChatThreadStore.setState({
|
|
@@ -191,6 +194,31 @@ describe('ChatSessionListManager list preference and read state', () => {
|
|
|
191
194
|
});
|
|
192
195
|
});
|
|
193
196
|
|
|
197
|
+
it('persists session and project list preferences through the list owner', () => {
|
|
198
|
+
const manager = new ChatSessionListManager(
|
|
199
|
+
{} as ConstructorParameters<typeof ChatSessionListManager>[0]
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
manager.toggleSessionPinned('session-2');
|
|
203
|
+
manager.toggleProjectPinned('/tmp/project-alpha');
|
|
204
|
+
manager.toggleProjectCollapsed('/tmp/project-alpha');
|
|
205
|
+
|
|
206
|
+
expect(useChatSessionListStore.getState().snapshot).toMatchObject({
|
|
207
|
+
pinnedSessionKeys: ['session-2'],
|
|
208
|
+
pinnedProjectRoots: ['/tmp/project-alpha'],
|
|
209
|
+
collapsedProjectRoots: ['/tmp/project-alpha'],
|
|
210
|
+
});
|
|
211
|
+
expect(persistStorage.get(chatSessionListModeStorageKey)).toMatchObject({
|
|
212
|
+
state: {
|
|
213
|
+
snapshot: {
|
|
214
|
+
pinnedSessionKeys: ['session-2'],
|
|
215
|
+
pinnedProjectRoots: ['/tmp/project-alpha'],
|
|
216
|
+
collapsedProjectRoots: ['/tmp/project-alpha'],
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
194
222
|
it('marks a session as read through the session list owner boundary', () => {
|
|
195
223
|
const manager = new ChatSessionListManager(
|
|
196
224
|
{} as ConstructorParameters<typeof ChatSessionListManager>[0]
|
|
@@ -266,7 +294,10 @@ describe('ChatSessionListStore persistence', () => {
|
|
|
266
294
|
useChatSessionListStore.setState({
|
|
267
295
|
snapshot: {
|
|
268
296
|
...useChatSessionListStore.getState().snapshot,
|
|
269
|
-
listMode: 'time-first'
|
|
297
|
+
listMode: 'time-first',
|
|
298
|
+
pinnedSessionKeys: [],
|
|
299
|
+
pinnedProjectRoots: [],
|
|
300
|
+
collapsedProjectRoots: [],
|
|
270
301
|
}
|
|
271
302
|
});
|
|
272
303
|
});
|
|
@@ -13,6 +13,17 @@ type WorkspaceChildReadState = {
|
|
|
13
13
|
readAt?: string | null;
|
|
14
14
|
runStatus?: string | null;
|
|
15
15
|
};
|
|
16
|
+
|
|
17
|
+
function toggleListValue(values: string[], value: string): string[] {
|
|
18
|
+
const normalizedValue = value.trim();
|
|
19
|
+
if (!normalizedValue) {
|
|
20
|
+
return values;
|
|
21
|
+
}
|
|
22
|
+
return values.includes(normalizedValue)
|
|
23
|
+
? values.filter((item) => item !== normalizedValue)
|
|
24
|
+
: [normalizedValue, ...values];
|
|
25
|
+
}
|
|
26
|
+
|
|
16
27
|
export class ChatSessionListManager {
|
|
17
28
|
constructor(private uiManager: ChatUiManager) {}
|
|
18
29
|
|
|
@@ -110,6 +121,27 @@ export class ChatSessionListManager {
|
|
|
110
121
|
useChatSessionListStore.getState().setSnapshot({ listMode: value });
|
|
111
122
|
};
|
|
112
123
|
|
|
124
|
+
toggleSessionPinned = (sessionKey: string) => {
|
|
125
|
+
const { pinnedSessionKeys } = useChatSessionListStore.getState().snapshot;
|
|
126
|
+
useChatSessionListStore.getState().setSnapshot({
|
|
127
|
+
pinnedSessionKeys: toggleListValue(pinnedSessionKeys, sessionKey),
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
toggleProjectPinned = (projectRoot: string) => {
|
|
132
|
+
const { pinnedProjectRoots } = useChatSessionListStore.getState().snapshot;
|
|
133
|
+
useChatSessionListStore.getState().setSnapshot({
|
|
134
|
+
pinnedProjectRoots: toggleListValue(pinnedProjectRoots, projectRoot),
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
toggleProjectCollapsed = (projectRoot: string) => {
|
|
139
|
+
const { collapsedProjectRoots } = useChatSessionListStore.getState().snapshot;
|
|
140
|
+
useChatSessionListStore.getState().setSnapshot({
|
|
141
|
+
collapsedProjectRoots: toggleListValue(collapsedProjectRoots, projectRoot),
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
|
|
113
145
|
markSessionRead = (
|
|
114
146
|
sessionKey: string | null | undefined,
|
|
115
147
|
readAt: string | null | undefined,
|
|
@@ -7,6 +7,9 @@ export type ChatSessionListSnapshot = {
|
|
|
7
7
|
selectedAgentId: string;
|
|
8
8
|
query: string;
|
|
9
9
|
listMode: ChatSessionListMode;
|
|
10
|
+
pinnedSessionKeys: string[];
|
|
11
|
+
pinnedProjectRoots: string[];
|
|
12
|
+
collapsedProjectRoots: string[];
|
|
10
13
|
};
|
|
11
14
|
|
|
12
15
|
const CHAT_SESSION_LIST_MODE_STORAGE_KEY = 'nextclaw.chat.session-list.mode';
|
|
@@ -15,6 +18,9 @@ const CHAT_SESSION_LIST_DEFAULT_MODE: ChatSessionListMode = 'time-first';
|
|
|
15
18
|
type PersistedChatSessionListStore = {
|
|
16
19
|
snapshot?: {
|
|
17
20
|
listMode?: unknown;
|
|
21
|
+
pinnedSessionKeys?: unknown;
|
|
22
|
+
pinnedProjectRoots?: unknown;
|
|
23
|
+
collapsedProjectRoots?: unknown;
|
|
18
24
|
};
|
|
19
25
|
};
|
|
20
26
|
|
|
@@ -22,6 +28,13 @@ function isChatSessionListMode(value: unknown): value is ChatSessionListMode {
|
|
|
22
28
|
return value === 'time-first' || value === 'project-first';
|
|
23
29
|
}
|
|
24
30
|
|
|
31
|
+
function normalizePersistedStringList(value: unknown): string[] {
|
|
32
|
+
if (!Array.isArray(value)) {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
return [...new Set(value.filter((item): item is string => typeof item === 'string').map((item) => item.trim()).filter(Boolean))];
|
|
36
|
+
}
|
|
37
|
+
|
|
25
38
|
function resolvePersistedChatSessionListMode(persistedState: unknown): ChatSessionListMode | null {
|
|
26
39
|
if (!persistedState || typeof persistedState !== 'object') {
|
|
27
40
|
return null;
|
|
@@ -75,7 +88,10 @@ const initialSnapshot: ChatSessionListSnapshot = {
|
|
|
75
88
|
selectedSessionKey: null,
|
|
76
89
|
selectedAgentId: 'main',
|
|
77
90
|
query: '',
|
|
78
|
-
listMode: CHAT_SESSION_LIST_DEFAULT_MODE
|
|
91
|
+
listMode: CHAT_SESSION_LIST_DEFAULT_MODE,
|
|
92
|
+
pinnedSessionKeys: [],
|
|
93
|
+
pinnedProjectRoots: [],
|
|
94
|
+
collapsedProjectRoots: [],
|
|
79
95
|
};
|
|
80
96
|
|
|
81
97
|
function createSetSnapshotAction(set: ChatSessionListStoreSet) {
|
|
@@ -123,19 +139,26 @@ export const useChatSessionListStore = create<ChatSessionListStore>()(
|
|
|
123
139
|
storage: createJSONStorage(() => window.localStorage),
|
|
124
140
|
partialize: (state): PersistedChatSessionListStore => ({
|
|
125
141
|
snapshot: {
|
|
126
|
-
listMode: state.snapshot.listMode
|
|
142
|
+
listMode: state.snapshot.listMode,
|
|
143
|
+
pinnedSessionKeys: state.snapshot.pinnedSessionKeys,
|
|
144
|
+
pinnedProjectRoots: state.snapshot.pinnedProjectRoots,
|
|
145
|
+
collapsedProjectRoots: state.snapshot.collapsedProjectRoots,
|
|
127
146
|
}
|
|
128
147
|
}),
|
|
129
148
|
merge: (persistedState, currentState) => {
|
|
130
|
-
|
|
131
|
-
if (!listMode) {
|
|
149
|
+
if (!persistedState || typeof persistedState !== 'object') {
|
|
132
150
|
return currentState;
|
|
133
151
|
}
|
|
152
|
+
const { snapshot } = persistedState as PersistedChatSessionListStore;
|
|
153
|
+
const listMode = resolvePersistedChatSessionListMode(persistedState);
|
|
134
154
|
return {
|
|
135
155
|
...currentState,
|
|
136
156
|
snapshot: {
|
|
137
157
|
...currentState.snapshot,
|
|
138
|
-
listMode
|
|
158
|
+
...(listMode ? { listMode } : {}),
|
|
159
|
+
pinnedSessionKeys: normalizePersistedStringList(snapshot?.pinnedSessionKeys),
|
|
160
|
+
pinnedProjectRoots: normalizePersistedStringList(snapshot?.pinnedProjectRoots),
|
|
161
|
+
collapsedProjectRoots: normalizePersistedStringList(snapshot?.collapsedProjectRoots),
|
|
139
162
|
}
|
|
140
163
|
};
|
|
141
164
|
}
|
|
@@ -266,21 +266,14 @@ export function McpMarketplacePage() {
|
|
|
266
266
|
return (
|
|
267
267
|
<PageLayout className="flex h-full min-h-0 flex-col pb-0 px-0">
|
|
268
268
|
<div className="flex flex-col gap-6 w-full max-w-[1400px] h-full min-h-0 mx-auto">
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
<div className="absolute top-0 right-0 -m-32 opacity-30 pointer-events-none mix-blend-screen scale-150 transform-gpu">
|
|
273
|
-
<div className="w-[500px] h-[500px] rounded-full bg-gradient-to-tl from-fuchsia-500/40 via-purple-500/30 to-indigo-500/20 blur-[80px]"></div>
|
|
274
|
-
</div>
|
|
275
|
-
<div className="absolute bottom-0 left-0 -m-32 opacity-20 pointer-events-none mix-blend-screen transform-gpu">
|
|
276
|
-
<div className="w-[400px] h-[400px] rounded-full bg-gradient-to-tr from-blue-500/40 to-cyan-500/20 blur-[80px]"></div>
|
|
277
|
-
</div>
|
|
278
|
-
|
|
269
|
+
<div className="relative isolate overflow-hidden rounded-[28px] border border-border bg-card px-10 py-12 text-card-foreground shadow-card">
|
|
270
|
+
<div className="absolute inset-y-0 left-0 w-1 bg-primary/80" />
|
|
271
|
+
|
|
279
272
|
<div className="relative z-10 flex flex-col gap-3">
|
|
280
|
-
<h1 className="text-[38px] font-extrabold
|
|
273
|
+
<h1 className="text-[38px] font-extrabold leading-tight text-foreground">
|
|
281
274
|
{t("marketplaceMcpPageTitle")}
|
|
282
275
|
</h1>
|
|
283
|
-
<p className="text-[17px] font-medium
|
|
276
|
+
<p className="max-w-2xl text-[17px] font-medium leading-relaxed text-muted-foreground">
|
|
284
277
|
{t("marketplaceMcpPageDescription")}
|
|
285
278
|
</p>
|
|
286
279
|
</div>
|
|
@@ -38,11 +38,11 @@ export function PanelAppListItem({
|
|
|
38
38
|
};
|
|
39
39
|
|
|
40
40
|
return (
|
|
41
|
-
<div className="group w-full min-w-0 rounded-lg border border-border bg-card px-2.5 py-2.5 transition-colors hover:
|
|
41
|
+
<div className="group w-full min-w-0 rounded-lg border border-border/60 bg-card px-2.5 py-2.5 transition-colors hover:bg-muted/40">
|
|
42
42
|
<div className="flex min-w-0 items-start gap-2">
|
|
43
43
|
<button type="button" onClick={onOpen} className="min-w-0 flex-1 text-left">
|
|
44
44
|
<span className="flex min-w-0 items-center gap-2">
|
|
45
|
-
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-
|
|
45
|
+
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-muted text-base text-muted-foreground">
|
|
46
46
|
<PanelAppIcon icon={entry.icon} title={entry.title} />
|
|
47
47
|
</span>
|
|
48
48
|
<span className="block min-w-0 flex-1 truncate text-sm font-medium text-foreground">{entry.title}</span>
|
|
@@ -52,22 +52,22 @@ export function PanelAppListItem({
|
|
|
52
52
|
) : null}
|
|
53
53
|
<span className="mt-0.5 block truncate text-[11px] leading-4 text-muted-foreground/70">{secondary}</span>
|
|
54
54
|
</button>
|
|
55
|
-
<div className="flex shrink-0 items-center gap-
|
|
55
|
+
<div className="flex shrink-0 items-center gap-0.5 opacity-70 transition-opacity group-hover:opacity-100">
|
|
56
56
|
<button
|
|
57
57
|
type="button"
|
|
58
58
|
onClick={handleFavorite}
|
|
59
59
|
disabled={favoritePending}
|
|
60
|
-
className="rounded-md p-1.5 text-muted-foreground/70 transition-colors hover:bg-
|
|
60
|
+
className="rounded-md p-1.5 text-muted-foreground/70 transition-colors hover:bg-muted hover:text-foreground disabled:opacity-50"
|
|
61
61
|
title={favoriteLabel}
|
|
62
62
|
aria-label={favoriteLabel}
|
|
63
63
|
>
|
|
64
|
-
<Star className={entry.favorite ? 'h-4 w-4 fill-
|
|
64
|
+
<Star className={entry.favorite ? 'h-4 w-4 fill-current text-foreground' : 'h-4 w-4'} />
|
|
65
65
|
</button>
|
|
66
66
|
<Popover open={isMenuOpen} onOpenChange={setIsMenuOpen}>
|
|
67
67
|
<PopoverTrigger asChild>
|
|
68
68
|
<button
|
|
69
69
|
type="button"
|
|
70
|
-
className="rounded-md p-1.5 text-muted-foreground/70 transition-colors hover:bg-
|
|
70
|
+
className="rounded-md p-1.5 text-muted-foreground/70 transition-colors hover:bg-muted hover:text-foreground"
|
|
71
71
|
aria-label={t('panelAppsMoreActions')}
|
|
72
72
|
disabled={deletePending}
|
|
73
73
|
>
|
|
@@ -2,6 +2,7 @@ export { PanelAppsList } from './components/panel-apps-list';
|
|
|
2
2
|
export { usePanelApps } from './hooks/use-panel-apps';
|
|
3
3
|
export { PanelAppBridgeManager } from './managers/panel-app-bridge.manager';
|
|
4
4
|
export { findPanelAppEntryByDisplayId } from './utils/panel-app-entry-match.utils';
|
|
5
|
+
export { PANEL_APP_IFRAME_SANDBOX, focusPanelAppIframe } from './utils/panel-app-iframe.utils';
|
|
5
6
|
export {
|
|
6
7
|
openApps,
|
|
7
8
|
PANEL_APPS_DOC_BROWSER_RENDERERS,
|
|
@@ -14,20 +14,11 @@ import {
|
|
|
14
14
|
RightPanelResourceHomePage,
|
|
15
15
|
} from '@/features/right-panel-resources';
|
|
16
16
|
import { t } from '@/shared/lib/i18n';
|
|
17
|
+
import { PANEL_APP_IFRAME_SANDBOX, focusPanelAppIframe } from './panel-app-iframe.utils';
|
|
17
18
|
|
|
18
19
|
export const APPS_TAB_KIND = RIGHT_PANEL_APPS_TAB_KIND;
|
|
19
20
|
export const PANEL_APP_TAB_KIND = RIGHT_PANEL_PANEL_APP_TAB_KIND;
|
|
20
21
|
const DEFAULT_APPS_PANEL_TAB: AppsPanelTab = 'panel-apps';
|
|
21
|
-
const PANEL_APP_IFRAME_SANDBOX = [
|
|
22
|
-
'allow-scripts',
|
|
23
|
-
'allow-forms',
|
|
24
|
-
'allow-modals',
|
|
25
|
-
'allow-popups',
|
|
26
|
-
'allow-popups-to-escape-sandbox',
|
|
27
|
-
'allow-downloads',
|
|
28
|
-
'allow-pointer-lock',
|
|
29
|
-
'allow-presentation',
|
|
30
|
-
].join(' ');
|
|
31
22
|
|
|
32
23
|
function isAppsPanelTab(value: unknown): value is AppsPanelTab {
|
|
33
24
|
return value === 'panel-apps' || value === 'service-apps';
|
|
@@ -79,6 +70,7 @@ export const PANEL_APPS_DOC_BROWSER_RENDERERS: DocBrowserCustomTabRenderers = {
|
|
|
79
70
|
getIframeSandbox: () => PANEL_APP_IFRAME_SANDBOX,
|
|
80
71
|
getTitle: (tab) => tab.title || t('panelAppsTitle'),
|
|
81
72
|
onIframeMessage: (params) => getPresenter().panelAppBridgeManager.handleIframeMessage(params),
|
|
73
|
+
onIframePointerOver: (event) => focusPanelAppIframe(event.currentTarget),
|
|
82
74
|
renderIcon: () => <AppWindow className="w-4 h-4 text-primary shrink-0" />,
|
|
83
75
|
renderToolbar: ({ open, refreshIframe, tab }) => (
|
|
84
76
|
<PanelAppToolbar
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const PANEL_APP_IFRAME_SANDBOX = 'allow-scripts allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-downloads allow-pointer-lock allow-presentation';
|
|
2
|
+
|
|
3
|
+
export function focusPanelAppIframe(iframe: HTMLIFrameElement | null): void {
|
|
4
|
+
iframe?.focus({ preventScroll: true });
|
|
5
|
+
iframe?.contentWindow?.focus();
|
|
6
|
+
}
|