@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
|
@@ -2,6 +2,7 @@ import type { LucideIcon } from "lucide-react";
|
|
|
2
2
|
import {
|
|
3
3
|
Bot,
|
|
4
4
|
BrainCircuit,
|
|
5
|
+
AlarmClock,
|
|
5
6
|
Cpu,
|
|
6
7
|
Download,
|
|
7
8
|
KeyRound,
|
|
@@ -25,6 +26,11 @@ export type AppNavigationItem = {
|
|
|
25
26
|
icon: LucideIcon;
|
|
26
27
|
};
|
|
27
28
|
|
|
29
|
+
export type AppNavigationSection = {
|
|
30
|
+
label: string;
|
|
31
|
+
items: AppNavigationItem[];
|
|
32
|
+
};
|
|
33
|
+
|
|
28
34
|
function matchesRouteTarget(pathname: string, target: string): boolean {
|
|
29
35
|
const normalizedPath = pathname.toLowerCase();
|
|
30
36
|
const normalizedTarget = target.toLowerCase();
|
|
@@ -80,6 +86,33 @@ export function getMobileBottomNavItems(
|
|
|
80
86
|
];
|
|
81
87
|
}
|
|
82
88
|
|
|
89
|
+
export function getMainSidebarNavItems(
|
|
90
|
+
translate: Translate,
|
|
91
|
+
): AppNavigationItem[] {
|
|
92
|
+
return [
|
|
93
|
+
{
|
|
94
|
+
target: "/chat",
|
|
95
|
+
label: translate("chat"),
|
|
96
|
+
icon: MessageCircle,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
target: "/chat/cron",
|
|
100
|
+
label: translate("cron"),
|
|
101
|
+
icon: AlarmClock,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
target: "/chat/skills",
|
|
105
|
+
label: translate("marketplaceFilterSkills"),
|
|
106
|
+
icon: BrainCircuit,
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
target: "/agents",
|
|
110
|
+
label: translate("agentsPageTitle"),
|
|
111
|
+
icon: Bot,
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
|
|
83
116
|
export function getSettingsNavItems(
|
|
84
117
|
translate: Translate,
|
|
85
118
|
): AppNavigationItem[] {
|
|
@@ -142,6 +175,22 @@ export function getSettingsNavItems(
|
|
|
142
175
|
];
|
|
143
176
|
}
|
|
144
177
|
|
|
178
|
+
export function getSettingsNavSections(
|
|
179
|
+
translate: Translate,
|
|
180
|
+
): AppNavigationSection[] {
|
|
181
|
+
const items = getSettingsNavItems(translate);
|
|
182
|
+
return [
|
|
183
|
+
{
|
|
184
|
+
label: translate("settingsGroupBasic"),
|
|
185
|
+
items: items.slice(0, 3),
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
label: translate("settingsGroupAdvanced"),
|
|
189
|
+
items: items.slice(3),
|
|
190
|
+
},
|
|
191
|
+
];
|
|
192
|
+
}
|
|
193
|
+
|
|
145
194
|
export function getMobileSettingsNavItems(
|
|
146
195
|
translate: Translate,
|
|
147
196
|
): AppNavigationItem[] {
|
|
@@ -284,6 +284,88 @@
|
|
|
284
284
|
}
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
+
@layer base {
|
|
288
|
+
:root[data-theme="work"] {
|
|
289
|
+
/* ========================================
|
|
290
|
+
BRAND COLORS (Work White Ink)
|
|
291
|
+
======================================== */
|
|
292
|
+
--brand-50: 0 0% 97%;
|
|
293
|
+
--brand-100: 0 0% 94%;
|
|
294
|
+
--brand-200: 0 0% 88%;
|
|
295
|
+
--brand-300: 0 0% 78%;
|
|
296
|
+
--brand-400: 0 0% 58%;
|
|
297
|
+
--brand-500: 0 0% 23%;
|
|
298
|
+
--brand-600: 0 0% 18%;
|
|
299
|
+
--brand-700: 0 0% 12%;
|
|
300
|
+
--brand-800: 0 0% 7%;
|
|
301
|
+
--brand-900: 0 0% 4%;
|
|
302
|
+
|
|
303
|
+
/* ========================================
|
|
304
|
+
NEUTRAL COLORS (White Canvas / Soft Gray Rail)
|
|
305
|
+
======================================== */
|
|
306
|
+
--gray-50: 0 0% 100%;
|
|
307
|
+
--gray-100: 0 0% 95%;
|
|
308
|
+
--gray-200: 0 0% 90%;
|
|
309
|
+
--gray-300: 0 0% 84%;
|
|
310
|
+
--gray-400: 0 0% 70%;
|
|
311
|
+
--gray-500: 0 0% 46%;
|
|
312
|
+
--gray-600: 0 0% 34%;
|
|
313
|
+
--gray-700: 0 0% 28%;
|
|
314
|
+
--gray-800: 0 0% 13%;
|
|
315
|
+
--gray-900: 0 0% 7%;
|
|
316
|
+
|
|
317
|
+
/* Semantic Colors */
|
|
318
|
+
--background: var(--gray-50);
|
|
319
|
+
--background-secondary: var(--gray-100);
|
|
320
|
+
--background-tertiary: var(--gray-200);
|
|
321
|
+
--foreground: var(--gray-800);
|
|
322
|
+
--foreground-secondary: var(--gray-700);
|
|
323
|
+
--foreground-tertiary: var(--gray-500);
|
|
324
|
+
--foreground-muted: var(--gray-400);
|
|
325
|
+
--primary: var(--brand-500);
|
|
326
|
+
--primary-hover: var(--brand-600);
|
|
327
|
+
--primary-active: var(--brand-700);
|
|
328
|
+
--primary-foreground: 0 0% 100%;
|
|
329
|
+
--secondary: var(--gray-100);
|
|
330
|
+
--secondary-hover: var(--gray-200);
|
|
331
|
+
--secondary-foreground: var(--gray-800);
|
|
332
|
+
--accent: var(--gray-200);
|
|
333
|
+
--accent-foreground: var(--gray-800);
|
|
334
|
+
--muted: var(--gray-100);
|
|
335
|
+
--muted-foreground: var(--gray-500);
|
|
336
|
+
--card: 0 0% 100%;
|
|
337
|
+
--card-foreground: var(--gray-800);
|
|
338
|
+
--card-border: var(--gray-200);
|
|
339
|
+
--popover: 0 0% 100%;
|
|
340
|
+
--popover-foreground: var(--gray-800);
|
|
341
|
+
--border: var(--gray-200);
|
|
342
|
+
--border-hover: var(--gray-300);
|
|
343
|
+
--border-active: var(--gray-600);
|
|
344
|
+
--input: 0 0% 100%;
|
|
345
|
+
--input-border: var(--gray-200);
|
|
346
|
+
--input-focus: var(--gray-600);
|
|
347
|
+
--ring: var(--gray-600);
|
|
348
|
+
--ring-offset: var(--gray-50);
|
|
349
|
+
|
|
350
|
+
/* Shadows (bright neutral tint) */
|
|
351
|
+
--shadow-xs: 0 1px 2px 0 rgb(20 24 28 / 0.03);
|
|
352
|
+
--shadow-sm: 0 1px 3px 0 rgb(20 24 28 / 0.05), 0 1px 2px -1px rgb(20 24 28 / 0.04);
|
|
353
|
+
--shadow-md: 0 4px 6px -1px rgb(20 24 28 / 0.05), 0 2px 4px -2px rgb(20 24 28 / 0.04);
|
|
354
|
+
--shadow-lg: 0 10px 15px -3px rgb(20 24 28 / 0.06), 0 4px 6px -4px rgb(20 24 28 / 0.04);
|
|
355
|
+
--shadow-xl: 0 20px 25px -5px rgb(20 24 28 / 0.08), 0 8px 10px -6px rgb(20 24 28 / 0.04);
|
|
356
|
+
--shadow-2xl: 0 25px 50px -12px rgb(20 24 28 / 0.14);
|
|
357
|
+
--shadow-card: 0 0 8px 0 rgba(20, 24, 28, 0.04);
|
|
358
|
+
--shadow-card-hover: 0 0 16px 0 rgba(20, 24, 28, 0.06);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
:root[data-theme="work"] .nextclaw-chat-message-user {
|
|
362
|
+
background-color: hsl(var(--gray-100));
|
|
363
|
+
border-color: transparent;
|
|
364
|
+
color: hsl(var(--gray-800));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
}
|
|
368
|
+
|
|
287
369
|
@layer base {
|
|
288
370
|
:root[data-theme="warm"] {
|
|
289
371
|
/* ========================================
|
|
@@ -5,6 +5,7 @@ import { BookOpen, ChevronDown } from 'lucide-react';
|
|
|
5
5
|
import { toast } from 'sonner';
|
|
6
6
|
import { LogoBadge } from '@/shared/components/common/logo-badge';
|
|
7
7
|
import { Button } from '@/shared/components/ui/button';
|
|
8
|
+
import { FormActions } from '@/shared/components/ui/actions/form-actions';
|
|
8
9
|
import { StatusDot } from '@/shared/components/status/status-dot';
|
|
9
10
|
import { nextclawClient } from '@/shared/lib/api';
|
|
10
11
|
import { useConfig, useConfigMeta, useConfigSchema, useExecuteConfigAction, useUpdateChannel } from '@/shared/hooks/use-config';
|
|
@@ -363,17 +364,19 @@ function ChannelFormEditor({
|
|
|
363
364
|
/>
|
|
364
365
|
</ConfigSplitPaneBody>
|
|
365
366
|
|
|
366
|
-
<ConfigSplitPaneFooter
|
|
367
|
-
<
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
{
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
367
|
+
<ConfigSplitPaneFooter>
|
|
368
|
+
<FormActions align="between">
|
|
369
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
370
|
+
{actions.filter((action) => action.trigger === 'manual').map((action) => (
|
|
371
|
+
<Button key={action.id} type="button" size="sm" onClick={() => void handleManualAction(action)} disabled={disabled} variant="secondary">
|
|
372
|
+
{runningActionId === action.id ? t('connecting') : action.title}
|
|
373
|
+
</Button>
|
|
374
|
+
))}
|
|
375
|
+
</div>
|
|
376
|
+
<Button type="submit" size="sm" disabled={disabled}>
|
|
377
|
+
{updateChannel.isPending ? t('saving') : t('save')}
|
|
378
|
+
</Button>
|
|
379
|
+
</FormActions>
|
|
377
380
|
</ConfigSplitPaneFooter>
|
|
378
381
|
</form>
|
|
379
382
|
</ConfigSplitDetailPane>
|
|
@@ -224,6 +224,46 @@ describe("ChatConversationHeaderSection", () => {
|
|
|
224
224
|
|
|
225
225
|
expect(screen.queryByRole("button", { name: /Switch session/ })).toBeNull();
|
|
226
226
|
expect(screen.getByText("Parent Task")).toBeTruthy();
|
|
227
|
+
expect(screen.queryByText("Native")).toBeNull();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("keeps the runtime badge for a non-native session", () => {
|
|
231
|
+
useChatQueryStore.setState({
|
|
232
|
+
snapshot: {
|
|
233
|
+
...useChatQueryStore.getState().snapshot,
|
|
234
|
+
sessionsQuery: {
|
|
235
|
+
data: {
|
|
236
|
+
sessions: [
|
|
237
|
+
createSummary({
|
|
238
|
+
sessionId: "parent-session-1",
|
|
239
|
+
metadata: {
|
|
240
|
+
label: "Parent Task",
|
|
241
|
+
session_type: "codex",
|
|
242
|
+
},
|
|
243
|
+
}),
|
|
244
|
+
],
|
|
245
|
+
total: 1,
|
|
246
|
+
},
|
|
247
|
+
} as never,
|
|
248
|
+
sessionTypesQuery: {
|
|
249
|
+
data: {
|
|
250
|
+
defaultType: "native",
|
|
251
|
+
options: [{ value: "codex", label: "Codex", ready: true }],
|
|
252
|
+
},
|
|
253
|
+
} as never,
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
mocks.sessionItems = [
|
|
257
|
+
createSessionListItem({
|
|
258
|
+
key: "parent-session-1",
|
|
259
|
+
label: "Parent Task",
|
|
260
|
+
sessionType: "codex",
|
|
261
|
+
}),
|
|
262
|
+
];
|
|
263
|
+
|
|
264
|
+
renderHeaderSection();
|
|
265
|
+
|
|
266
|
+
expect(screen.getByText("Codex")).toBeTruthy();
|
|
227
267
|
});
|
|
228
268
|
|
|
229
269
|
it("keeps session switching available from a collapsed new-session header", async () => {
|
|
@@ -53,6 +53,8 @@ export function ChatConversationHeaderSection({
|
|
|
53
53
|
sessionTypeOptions.find((option) => option.value === sessionType) ?? null;
|
|
54
54
|
const sessionTypeLabel =
|
|
55
55
|
sessionTypeOption?.label ?? resolveSessionTypeLabel(sessionType);
|
|
56
|
+
const shouldShowSessionTypeBadge =
|
|
57
|
+
sessionType !== DEFAULT_SESSION_TYPE && Boolean(sessionTypeLabel);
|
|
56
58
|
const sessionProjectRoot = selectedSession?.projectRoot ?? null;
|
|
57
59
|
const sessionProjectName =
|
|
58
60
|
selectedSession?.projectName ?? getSessionProjectName(sessionProjectRoot);
|
|
@@ -110,7 +112,7 @@ export function ChatConversationHeaderSection({
|
|
|
110
112
|
) : null
|
|
111
113
|
}
|
|
112
114
|
sessionTypeBadge={
|
|
113
|
-
|
|
115
|
+
shouldShowSessionTypeBadge ? (
|
|
114
116
|
<span className="inline-flex shrink-0 items-center gap-1.5 rounded-full border border-border bg-muted px-2 py-0.5 text-[11px] font-medium text-muted-foreground">
|
|
115
117
|
{sessionTypeOption?.icon?.src ? (
|
|
116
118
|
<span className="inline-flex h-[1.125rem] w-[1.125rem] items-center justify-center">
|
|
@@ -34,7 +34,7 @@ export function ChatConversationPanel({
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
return (
|
|
37
|
-
<section className="flex-1 min-h-0 flex overflow-hidden bg-background">
|
|
37
|
+
<section className="relative flex-1 min-h-0 flex overflow-hidden bg-background">
|
|
38
38
|
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
|
39
39
|
<ChatConversationParentBanner />
|
|
40
40
|
<ChatConversationHeaderSection
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
ChatSidebarDesktopToolbar,
|
|
5
|
+
ChatSidebarMobileToolbar,
|
|
6
|
+
} from "@/features/chat/components/layout/chat-sidebar-toolbar";
|
|
7
|
+
import type { ChatSessionTypeOption } from "@/features/chat/features/session-type/utils/chat-session-type.utils";
|
|
8
|
+
|
|
9
|
+
const sessionTypeOptions: ChatSessionTypeOption[] = [
|
|
10
|
+
{ value: "native", label: "Native", icon: null, ready: true },
|
|
11
|
+
{ value: "codex", label: "Codex", icon: null, ready: true },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const toolbarProps = {
|
|
15
|
+
query: "",
|
|
16
|
+
defaultSessionType: "native",
|
|
17
|
+
sessionTypeOptions,
|
|
18
|
+
nonDefaultSessionTypeOptions: [sessionTypeOptions[1]],
|
|
19
|
+
selectedNewSessionType: "native",
|
|
20
|
+
selectedNewSessionTypeOption: sessionTypeOptions[0],
|
|
21
|
+
isCreateMenuOpen: false,
|
|
22
|
+
onCreateMenuOpenChange: vi.fn(),
|
|
23
|
+
onCreateSession: vi.fn(),
|
|
24
|
+
onSelectNewSessionType: vi.fn(),
|
|
25
|
+
onQueryChange: vi.fn(),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function searchIconClassName() {
|
|
29
|
+
return (
|
|
30
|
+
screen
|
|
31
|
+
.getByPlaceholderText("Search conversations...")
|
|
32
|
+
.previousElementSibling?.getAttribute("class") ?? ""
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("ChatSidebarToolbar", () => {
|
|
37
|
+
it("keeps desktop search icon transparent to pointer input", () => {
|
|
38
|
+
render(<ChatSidebarDesktopToolbar {...toolbarProps} />);
|
|
39
|
+
|
|
40
|
+
expect(searchIconClassName()).toContain("pointer-events-none");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("keeps mobile search icon transparent to pointer input", () => {
|
|
44
|
+
render(<ChatSidebarMobileToolbar {...toolbarProps} />);
|
|
45
|
+
|
|
46
|
+
expect(searchIconClassName()).toContain("pointer-events-none");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import { MemoryRouter } from "react-router-dom";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { ChatSidebarUtilityMenu } from "@/features/chat/components/layout/chat-sidebar-utility-menu";
|
|
5
|
+
|
|
6
|
+
function renderUtilityMenu() {
|
|
7
|
+
return render(
|
|
8
|
+
<MemoryRouter>
|
|
9
|
+
<ChatSidebarUtilityMenu
|
|
10
|
+
isOpen
|
|
11
|
+
onOpenChange={vi.fn()}
|
|
12
|
+
currentTheme="warm"
|
|
13
|
+
currentThemeLabel="Warm"
|
|
14
|
+
themeOptions={[{ value: "warm", label: "Warm" }]}
|
|
15
|
+
onSelectTheme={vi.fn()}
|
|
16
|
+
currentLanguage="en"
|
|
17
|
+
currentLanguageLabel="English"
|
|
18
|
+
languageOptions={[{ value: "en", label: "English" }]}
|
|
19
|
+
onSelectLanguage={vi.fn()}
|
|
20
|
+
onOpenDocs={vi.fn()}
|
|
21
|
+
onOpenApps={vi.fn()}
|
|
22
|
+
/>
|
|
23
|
+
</MemoryRouter>,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function appendComposerFocusTarget(): HTMLElement {
|
|
28
|
+
const composerShell = document.createElement("div");
|
|
29
|
+
composerShell.className = "nextclaw-chat-input-bar-shell";
|
|
30
|
+
const composer = document.createElement("div");
|
|
31
|
+
composer.setAttribute("contenteditable", "true");
|
|
32
|
+
composer.setAttribute("role", "textbox");
|
|
33
|
+
composer.tabIndex = 0;
|
|
34
|
+
composerShell.appendChild(composer);
|
|
35
|
+
document.body.appendChild(composerShell);
|
|
36
|
+
return composer;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("ChatSidebarUtilityMenu", () => {
|
|
40
|
+
it("stays open when the streaming composer restores focus", () => {
|
|
41
|
+
const composer = appendComposerFocusTarget();
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
renderUtilityMenu();
|
|
45
|
+
|
|
46
|
+
expect(screen.getByRole("button", { name: "Help Docs" })).toBeTruthy();
|
|
47
|
+
|
|
48
|
+
fireEvent.focusIn(composer);
|
|
49
|
+
|
|
50
|
+
expect(screen.getByRole("button", { name: "Help Docs" })).toBeTruthy();
|
|
51
|
+
} finally {
|
|
52
|
+
composer.closest(".nextclaw-chat-input-bar-shell")?.remove();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -134,7 +134,6 @@ export function ChatSidebarSessionArea({
|
|
|
134
134
|
isCollapsed,
|
|
135
135
|
isLoading,
|
|
136
136
|
isProjectFirstView,
|
|
137
|
-
onCreateSession,
|
|
138
137
|
onSelectMode,
|
|
139
138
|
projectGroups,
|
|
140
139
|
renderSessionItem,
|
|
@@ -145,7 +144,6 @@ export function ChatSidebarSessionArea({
|
|
|
145
144
|
isCollapsed: boolean;
|
|
146
145
|
isLoading: boolean;
|
|
147
146
|
isProjectFirstView: boolean;
|
|
148
|
-
onCreateSession: (sessionType: string, projectRoot?: string | null) => void;
|
|
149
147
|
onSelectMode: (mode: "time-first" | "project-first") => void;
|
|
150
148
|
projectGroups: ReturnType<typeof groupSessionsByProject>;
|
|
151
149
|
renderSessionItem: (item: NcpSessionListItemView) => JSX.Element;
|
|
@@ -183,7 +181,6 @@ export function ChatSidebarSessionArea({
|
|
|
183
181
|
defaultSessionType={defaultSessionType}
|
|
184
182
|
sessionTypeOptions={sessionTypeOptions}
|
|
185
183
|
renderSessionItem={renderSessionItem}
|
|
186
|
-
onCreateSession={onCreateSession}
|
|
187
184
|
/>
|
|
188
185
|
</div>
|
|
189
186
|
</>
|
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
SIDEBAR_RAIL_CONTROL_CLASS,
|
|
17
17
|
SIDEBAR_RAIL_ICON_CLASS,
|
|
18
18
|
SIDEBAR_RAIL_ITEM_GAP_CLASS,
|
|
19
|
-
SIDEBAR_RAIL_PRIMARY_SURFACE_CLASS,
|
|
20
19
|
SIDEBAR_RAIL_SURFACE_CLASS,
|
|
21
20
|
} from "@/app/components/layout/sidebar-rail.styles";
|
|
22
21
|
|
|
@@ -150,7 +149,7 @@ export function ChatSidebarDesktopToolbar(props: ChatSidebarToolbarProps) {
|
|
|
150
149
|
label={t("chatSidebarNewTask")}
|
|
151
150
|
className={cn(
|
|
152
151
|
SIDEBAR_RAIL_CONTROL_CLASS,
|
|
153
|
-
|
|
152
|
+
SIDEBAR_RAIL_SURFACE_CLASS,
|
|
154
153
|
)}
|
|
155
154
|
onClick={() => {
|
|
156
155
|
onCreateMenuOpenChange(false);
|
|
@@ -259,7 +258,7 @@ export function ChatSidebarDesktopToolbar(props: ChatSidebarToolbarProps) {
|
|
|
259
258
|
|
|
260
259
|
<div className="px-4 pb-3">
|
|
261
260
|
<div className="relative">
|
|
262
|
-
<Search className="absolute left-3 top-2.5 h-3.5 w-3.5 text-muted-foreground/70" />
|
|
261
|
+
<Search className="pointer-events-none absolute left-3 top-2.5 h-3.5 w-3.5 text-muted-foreground/70" />
|
|
263
262
|
<Input
|
|
264
263
|
value={query}
|
|
265
264
|
onChange={(event) => onQueryChange(event.target.value)}
|
|
@@ -294,7 +293,7 @@ export function ChatSidebarMobileToolbar(props: ChatSidebarToolbarProps) {
|
|
|
294
293
|
<div className="px-4 pb-2 pt-1">
|
|
295
294
|
<div className="flex items-center gap-2">
|
|
296
295
|
<div className="relative min-w-0 flex-1">
|
|
297
|
-
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground/70" />
|
|
296
|
+
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground/70" />
|
|
298
297
|
<Input
|
|
299
298
|
value={query}
|
|
300
299
|
onChange={(event) => onQueryChange(event.target.value)}
|
|
@@ -34,10 +34,7 @@ import {
|
|
|
34
34
|
type LucideIcon,
|
|
35
35
|
} from "lucide-react";
|
|
36
36
|
|
|
37
|
-
type ChatSidebarUtilityOption<Value extends string> = {
|
|
38
|
-
value: Value;
|
|
39
|
-
label: string;
|
|
40
|
-
};
|
|
37
|
+
type ChatSidebarUtilityOption<Value extends string> = { value: Value; label: string };
|
|
41
38
|
|
|
42
39
|
type ChatSidebarUtilityMenuProps = {
|
|
43
40
|
isOpen: boolean;
|
|
@@ -55,6 +52,12 @@ type ChatSidebarUtilityMenuProps = {
|
|
|
55
52
|
collapsed?: boolean;
|
|
56
53
|
};
|
|
57
54
|
|
|
55
|
+
function isChatComposerFocusTarget(target: EventTarget | null): boolean {
|
|
56
|
+
return target instanceof HTMLElement && Boolean(
|
|
57
|
+
target.closest('.nextclaw-chat-input-bar-shell [role="textbox"][contenteditable="true"]'),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
58
61
|
export function ChatSidebarUtilityMenu({
|
|
59
62
|
isOpen,
|
|
60
63
|
onOpenChange,
|
|
@@ -70,16 +73,6 @@ export function ChatSidebarUtilityMenu({
|
|
|
70
73
|
onOpenApps,
|
|
71
74
|
collapsed = false,
|
|
72
75
|
}: ChatSidebarUtilityMenuProps) {
|
|
73
|
-
const handleOpenDocs = () => {
|
|
74
|
-
onOpenDocs();
|
|
75
|
-
onOpenChange(false);
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const handleOpenApps = () => {
|
|
79
|
-
onOpenApps();
|
|
80
|
-
onOpenChange(false);
|
|
81
|
-
};
|
|
82
|
-
|
|
83
76
|
return (
|
|
84
77
|
<Popover open={isOpen} onOpenChange={onOpenChange}>
|
|
85
78
|
{collapsed ? (
|
|
@@ -129,6 +122,11 @@ export function ChatSidebarUtilityMenu({
|
|
|
129
122
|
side={collapsed ? "right" : "top"}
|
|
130
123
|
align={collapsed ? "end" : "start"}
|
|
131
124
|
className="w-64 p-2"
|
|
125
|
+
onFocusOutside={(event) => {
|
|
126
|
+
if (isChatComposerFocusTarget(event.detail.originalEvent.target)) {
|
|
127
|
+
event.preventDefault();
|
|
128
|
+
}
|
|
129
|
+
}}
|
|
132
130
|
>
|
|
133
131
|
<div className="space-y-1">
|
|
134
132
|
<NavLink
|
|
@@ -139,22 +137,23 @@ export function ChatSidebarUtilityMenu({
|
|
|
139
137
|
<Settings className="h-4 w-4 text-muted-foreground/70" />
|
|
140
138
|
<span className="flex-1 text-left">{t("settings")}</span>
|
|
141
139
|
</NavLink>
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
140
|
+
{[
|
|
141
|
+
{ key: "docs", icon: BookOpen, label: t("docBrowserHelp"), action: onOpenDocs },
|
|
142
|
+
{ key: "apps", icon: Boxes, label: t("appsTitle"), action: onOpenApps },
|
|
143
|
+
].map(({ key, icon: Icon, label, action }) => (
|
|
144
|
+
<button
|
|
145
|
+
key={key}
|
|
146
|
+
type="button"
|
|
147
|
+
onClick={() => {
|
|
148
|
+
action();
|
|
149
|
+
onOpenChange(false);
|
|
150
|
+
}}
|
|
151
|
+
className="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-foreground transition-colors hover:bg-gray-200/60 hover:text-gray-900"
|
|
152
|
+
>
|
|
153
|
+
<Icon className="h-4 w-4 text-muted-foreground/70" />
|
|
154
|
+
<span className="flex-1 text-left">{label}</span>
|
|
155
|
+
</button>
|
|
156
|
+
))}
|
|
158
157
|
</div>
|
|
159
158
|
|
|
160
159
|
<div className="my-2 h-px bg-border" />
|
|
@@ -105,7 +105,7 @@ export function ChatSidebar({
|
|
|
105
105
|
const { language, setLanguage } = useI18n();
|
|
106
106
|
const { theme, setTheme } = useTheme();
|
|
107
107
|
const currentThemeLabel = t(
|
|
108
|
-
THEME_OPTIONS.find((o) => o.value === theme)?.labelKey ?? "
|
|
108
|
+
THEME_OPTIONS.find((o) => o.value === theme)?.labelKey ?? "themeWork",
|
|
109
109
|
);
|
|
110
110
|
const currentLanguageLabel =
|
|
111
111
|
LANGUAGE_OPTIONS.find((o) => o.value === language)?.label ?? language;
|
|
@@ -132,6 +132,14 @@ export function ChatSidebar({
|
|
|
132
132
|
),
|
|
133
133
|
[agentsQuery.data?.agents],
|
|
134
134
|
);
|
|
135
|
+
const pinnedSessionKeys = useMemo(
|
|
136
|
+
() => new Set(listSnapshot.pinnedSessionKeys),
|
|
137
|
+
[listSnapshot.pinnedSessionKeys],
|
|
138
|
+
);
|
|
139
|
+
const pinnedProjectRoots = useMemo(
|
|
140
|
+
() => new Set(listSnapshot.pinnedProjectRoots),
|
|
141
|
+
[listSnapshot.pinnedProjectRoots],
|
|
142
|
+
);
|
|
135
143
|
const sortedItems = useMemo(
|
|
136
144
|
() => sortSessionItemsByActivityAtDesc(items),
|
|
137
145
|
[items],
|
|
@@ -140,10 +148,13 @@ export function ChatSidebar({
|
|
|
140
148
|
() => groupChildSessionsByParentKey(items),
|
|
141
149
|
[items],
|
|
142
150
|
);
|
|
143
|
-
const groups = useMemo(
|
|
151
|
+
const groups = useMemo(
|
|
152
|
+
() => groupSessionsByDate(sortedItems, pinnedSessionKeys),
|
|
153
|
+
[pinnedSessionKeys, sortedItems],
|
|
154
|
+
);
|
|
144
155
|
const projectGroups = useMemo(
|
|
145
|
-
() => groupSessionsByProject(sortedItems),
|
|
146
|
-
[sortedItems],
|
|
156
|
+
() => groupSessionsByProject(sortedItems, pinnedSessionKeys, pinnedProjectRoots),
|
|
157
|
+
[pinnedProjectRoots, pinnedSessionKeys, sortedItems],
|
|
147
158
|
);
|
|
148
159
|
const sessionTypeOptions = useMemo(
|
|
149
160
|
() => buildSessionTypeOptions(sessionTypesData?.options ?? []),
|
|
@@ -200,6 +211,7 @@ export function ChatSidebar({
|
|
|
200
211
|
draftLabel={draftLabel}
|
|
201
212
|
savingSessionKey={savingSessionKey}
|
|
202
213
|
sessionTypeOptions={sessionTypeOptions}
|
|
214
|
+
isPinned={pinnedSessionKeys.has(item.session.key)}
|
|
203
215
|
sessionTitle={getSessionTitle}
|
|
204
216
|
onSelectSession={presenter.chatSessionListManager.selectSession}
|
|
205
217
|
onOpenChildSessions={(parentSessionKey, activeChildSessionKey) =>
|
|
@@ -212,6 +224,9 @@ export function ChatSidebar({
|
|
|
212
224
|
onDraftLabelChange={setDraftLabel}
|
|
213
225
|
onSaveSessionLabel={saveSessionLabel}
|
|
214
226
|
onCancelEditingSessionLabel={cancelEditingSessionLabel}
|
|
227
|
+
onTogglePinned={() =>
|
|
228
|
+
presenter.chatSessionListManager.toggleSessionPinned(item.session.key)
|
|
229
|
+
}
|
|
215
230
|
/>
|
|
216
231
|
);
|
|
217
232
|
const createSessionAndOpenIfNeeded = (
|
|
@@ -233,9 +248,9 @@ export function ChatSidebar({
|
|
|
233
248
|
: shouldCollapse
|
|
234
249
|
? cn(
|
|
235
250
|
SIDEBAR_RAIL_WIDTH_CLASS,
|
|
236
|
-
"shrink-0
|
|
251
|
+
"shrink-0",
|
|
237
252
|
)
|
|
238
|
-
: "w-[280px] shrink-0
|
|
253
|
+
: "w-[280px] shrink-0",
|
|
239
254
|
)}
|
|
240
255
|
data-sidebar-collapsed={shouldCollapse ? "true" : "false"}
|
|
241
256
|
>
|
|
@@ -295,7 +310,6 @@ export function ChatSidebar({
|
|
|
295
310
|
isCollapsed={shouldCollapse}
|
|
296
311
|
isLoading={isLoading}
|
|
297
312
|
isProjectFirstView={isProjectFirstView}
|
|
298
|
-
onCreateSession={createSessionAndOpenIfNeeded}
|
|
299
313
|
onSelectMode={presenter.chatSessionListManager.setListMode}
|
|
300
314
|
projectGroups={projectGroups}
|
|
301
315
|
renderSessionItem={renderSessionItem}
|
package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx
CHANGED
|
@@ -32,8 +32,13 @@ vi.mock('@/app/presenters/app.presenter', () => ({
|
|
|
32
32
|
}));
|
|
33
33
|
|
|
34
34
|
vi.mock('@/features/panel-apps', () => ({
|
|
35
|
+
PANEL_APP_IFRAME_SANDBOX: 'allow-scripts allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-downloads allow-pointer-lock allow-presentation',
|
|
35
36
|
findPanelAppEntryByDisplayId: (entries: Array<{ appId: string; id: string; title: string }>, value: string) =>
|
|
36
37
|
entries.find((entry) => [entry.appId, entry.id, entry.title].includes(value)) ?? null,
|
|
38
|
+
focusPanelAppIframe: (iframe: HTMLIFrameElement | null) => {
|
|
39
|
+
iframe?.focus({ preventScroll: true });
|
|
40
|
+
iframe?.contentWindow?.focus();
|
|
41
|
+
},
|
|
37
42
|
usePanelApps: () => mocks.panelApps,
|
|
38
43
|
}));
|
|
39
44
|
|
|
@@ -68,7 +73,6 @@ it('renders inline panel apps as bounded card-mode iframes with an expand action
|
|
|
68
73
|
expect(iframe.getAttribute('src')).toBe(
|
|
69
74
|
'/api/panel-apps/weather-card/content?nextclawDisplayMode=card&nextclawPlacement=inline',
|
|
70
75
|
);
|
|
71
|
-
expect(iframe.getAttribute('scrolling')).toBe('auto');
|
|
72
76
|
|
|
73
77
|
fireEvent.click(screen.getByLabelText('chatPanelCardExpand'));
|
|
74
78
|
|
|
@@ -80,6 +84,23 @@ it('renders inline panel apps as bounded card-mode iframes with an expand action
|
|
|
80
84
|
}));
|
|
81
85
|
});
|
|
82
86
|
|
|
87
|
+
it('focuses the inline panel app iframe when the pointer enters it', () => {
|
|
88
|
+
render(<ChatInlinePanelAppCard panelApp={{
|
|
89
|
+
appId: 'weather-card',
|
|
90
|
+
title: 'Weather',
|
|
91
|
+
}} />);
|
|
92
|
+
|
|
93
|
+
const iframe = screen.getByTitle('Weather') as HTMLIFrameElement;
|
|
94
|
+
const iframeFocus = vi.spyOn(iframe, 'focus');
|
|
95
|
+
const contentWindowFocus = vi.spyOn(iframe.contentWindow!, 'focus').mockImplementation(() => undefined);
|
|
96
|
+
|
|
97
|
+
fireEvent.pointerOver(iframe);
|
|
98
|
+
|
|
99
|
+
expect(iframe.getAttribute('tabindex')).toBe('0');
|
|
100
|
+
expect(iframeFocus).toHaveBeenCalledWith({ preventScroll: true });
|
|
101
|
+
expect(contentWindowFocus).toHaveBeenCalled();
|
|
102
|
+
});
|
|
103
|
+
|
|
83
104
|
it('can render inline panel apps without a side-panel expand action', () => {
|
|
84
105
|
render(
|
|
85
106
|
<ChatInlinePanelAppCard
|