@nextclaw/ui 0.15.20 → 0.15.22
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 +42 -0
- package/dist/assets/{api-tWUa6l9J.js → api-B1yHQYs-.js} +6 -6
- package/dist/assets/app-presenter-provider-uP9waOzv.js +108 -0
- package/dist/assets/appearance-settings-page-C9Zq3hP9.js +1 -0
- package/dist/assets/{book-open-BQ29ZmDE.js → book-open-oSmyVXea.js} +1 -1
- package/dist/assets/{card-CtKueklV.js → card-CGvJZTZp.js} +1 -1
- package/dist/assets/{channels-list-page-C8y3rqYt.js → channels-list-page-5BTxpQHw.js} +2 -2
- package/dist/assets/chat-page-jTvli8Jp.js +1 -0
- package/dist/assets/config-split-page-BgBsUmp1.js +1 -0
- package/dist/assets/confirm-dialog-h849QRWO.js +5 -0
- package/dist/assets/desktop-update-config-BGGHxClR.js +1 -0
- package/dist/assets/{dist-rYHnBo8t.js → dist-CxUhurq9.js} +1 -1
- package/dist/assets/doc-browser-0eL8AX42.js +1 -0
- package/dist/assets/{doc-browser-D8ng4EUc.js → doc-browser-DEqE9lsh.js} +1 -1
- package/dist/assets/{doc-browser-context-CPHNzQ8m.js → doc-browser-context-DkzFIUoK.js} +1 -1
- package/dist/assets/{eye-CiJz3ODY.js → eye-gOWQGt8N.js} +1 -1
- package/dist/assets/{form-actions-BnZVzLZf.js → form-actions-CgjYZUzM.js} +1 -1
- package/dist/assets/index-DSAUWCfl.js +2 -0
- package/dist/assets/index-G7cBryJ4.css +1 -0
- package/dist/assets/loader-circle-BAuycGf6.js +1 -0
- package/dist/assets/{logo-badge-BXI-Kdhd.js → logo-badge-BNcsDsUM.js} +1 -1
- package/dist/assets/{mcp-marketplace-page-CxNe-Q9Q.js → mcp-marketplace-page-BX39MtPa.js} +3 -3
- package/dist/assets/mcp-marketplace-page-CkDtOEA4.js +1 -0
- package/dist/assets/{model-config-page-CR_mgEVp.js → model-config-page-7MIXg2fQ.js} +1 -1
- package/dist/assets/{navigation-link-C1cvB7y4.js → navigation-link-B3iFOdxx.js} +1 -1
- package/dist/assets/plus-yvhFgfal.js +1 -0
- package/dist/assets/{popover-ZoDwVwwd.js → popover-Cpep_rJe.js} +1 -1
- package/dist/assets/{provider-models-CXUgDxGR.js → provider-models-CUrptZ-o.js} +1 -1
- package/dist/assets/provider-scoped-model-input-CHmOzz7x.js +1 -0
- package/dist/assets/providers-config-page-A6J7Y0II.js +1 -0
- package/dist/assets/react-BCJGCjrP.js +8 -0
- package/dist/assets/{refresh-cw-DKewcdAl.js → refresh-cw-CkKwPztT.js} +1 -1
- package/dist/assets/remote-FD-ZfSIn.js +1 -0
- package/dist/assets/rotate-cw-DrJMfHhp.js +1 -0
- package/dist/assets/runtime-config-page-DwjO-Vs6.js +1 -0
- package/dist/assets/{save-BrckuAXY.js → save-BINMWate.js} +1 -1
- package/dist/assets/{search-BhXQB2TO.js → search-7M38azRp.js} +1 -1
- package/dist/assets/{search-config-page-DFOQ6SDJ.js → search-config-page-T6tMhXbP.js} +1 -1
- package/dist/assets/{secrets-config-page-BqqeClqE.js → secrets-config-page-2mVejXf2.js} +2 -2
- package/dist/assets/security-config-DD5RrjDe.js +1 -0
- package/dist/assets/{select-CVzGGT38.js → select-BvidP5Oa.js} +1 -1
- package/dist/assets/{settings-2-BP6L2enG.js → settings-2-DqZstREV.js} +1 -1
- package/dist/assets/skeleton-BEQW2Dva.js +1 -0
- package/dist/assets/{tabs-B-QWjKCL.js → tabs-BeLxVmkt.js} +1 -1
- package/dist/assets/{tag-chip-C88mxjwx.js → tag-chip-BoDaLnWt.js} +1 -1
- package/dist/assets/{trash-2-ck3t33I3.js → trash-2-CYUskMMN.js} +1 -1
- package/dist/assets/{use-config-BAr5bH2c.js → use-config-CxKRXXCl.js} +1 -1
- package/dist/assets/use-confirm-dialog-HASTvOP2.js +1 -0
- package/dist/assets/{use-viewport-layout-k1isXfNm.js → use-viewport-layout-CJxkjoRz.js} +1 -1
- package/dist/assets/x-DdUwjre5.js +1 -0
- package/dist/index.html +29 -29
- package/package.json +8 -8
- package/src/app/components/__tests__/app-notification-runtime.test.tsx +32 -0
- package/src/app/components/app-notification-runtime.tsx +15 -0
- package/src/app/components/layout/sidebar-items.tsx +23 -9
- package/src/app/components/layout/sidebar-rail.styles.ts +0 -2
- package/src/app/components/layout/sidebar.tsx +2 -2
- package/src/app/configs/app-navigation.config.ts +21 -0
- package/src/app/index.tsx +22 -2
- package/src/app/presenters/app.presenter.ts +8 -0
- package/src/app/styles/design-system.css +12 -0
- package/src/features/chat/components/chat-sidebar-list-mode-switch.tsx +2 -2
- package/src/features/chat/components/conversation/__tests__/chat-conversation-content.test.tsx +19 -1
- package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +1 -1
- package/src/features/chat/components/conversation/chat-conversation-content.tsx +5 -6
- package/src/features/chat/components/conversation/chat-conversation-header.tsx +1 -1
- package/src/features/chat/components/layout/__tests__/chat-sidebar-read-state.test.tsx +1 -1
- package/src/features/chat/components/layout/__tests__/chat-sidebar-session-area.test.tsx +52 -0
- package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +11 -17
- package/src/features/chat/components/layout/chat-page-shell.tsx +9 -13
- package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +40 -38
- package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +13 -64
- package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +1 -4
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +1 -1
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +15 -4
- package/src/features/chat/features/conversation/components/session-conversation-area.tsx +7 -7
- package/src/features/chat/features/conversation/components/session-conversation-input.tsx +4 -2
- package/src/features/chat/features/conversation/hooks/use-session-conversation-input-attachments.ts +1 -0
- package/src/features/chat/features/conversation/utils/session-conversation-input-toolbar.utils.ts +1 -0
- package/src/features/chat/features/conversation/utils/session-queued-input.utils.ts +1 -0
- package/src/features/chat/features/input/utils/__tests__/chat-input-bar.utils.test.ts +3 -0
- package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +0 -1
- package/src/features/chat/features/input/utils/chat-input-toolbar.utils.ts +3 -0
- package/src/features/chat/features/message/utils/__tests__/chat-message-timeline.utils.test.ts +29 -0
- package/src/features/chat/features/message/utils/chat-message-timeline.utils.ts +4 -1
- package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +10 -10
- package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +2 -2
- package/src/features/chat/features/session/components/session-header/chat-session-header-menu-item.tsx +1 -1
- package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +1 -1
- package/src/features/chat/features/session/components/session-header/chat-session-switcher-groups.tsx +1 -1
- package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +1 -1
- package/src/features/chat/features/session/utils/__tests__/chat-session-display.utils.test.ts +26 -0
- package/src/features/chat/features/session/utils/__tests__/ncp-session-adapter.utils.test.ts +4 -2
- package/src/features/chat/features/session/utils/chat-session-display.utils.ts +48 -12
- package/src/features/chat/features/session/utils/ncp-session-adapter.utils.ts +13 -4
- package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +6 -0
- package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +10 -7
- package/src/features/chat/features/welcome/components/chat-welcome-agent-picker.tsx +1 -1
- package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +3 -14
- package/src/features/chat/features/welcome/components/chat-welcome-prompt-suggestions.tsx +37 -0
- package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +1 -1
- package/src/features/chat/features/welcome/components/chat-welcome.tsx +33 -31
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +1 -1
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel.test.tsx +1 -1
- package/src/features/chat/index.ts +1 -0
- package/src/features/chat/managers/__tests__/chat-completion-notification.manager.test.ts +177 -0
- package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +15 -3
- package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +3 -10
- package/src/features/chat/managers/chat-completion-notification.manager.ts +146 -0
- package/src/features/chat/managers/chat-session-list.manager.ts +3 -23
- package/src/features/chat/managers/chat-thread.manager.ts +2 -5
- package/src/features/chat/pages/__tests__/ncp-chat-page.test.tsx +32 -11
- package/src/features/chat/pages/ncp-chat-page.tsx +9 -1
- package/src/features/chat/types/chat-input-bar.types.ts +1 -0
- package/src/features/cron/components/cron-task-discovery.tsx +1 -1
- package/src/features/inbox/components/inbox-delivery-content.test.tsx +57 -0
- package/src/features/inbox/components/inbox-delivery-content.tsx +118 -0
- package/src/features/inbox/components/inbox-reader-dialog.test.tsx +77 -0
- package/src/features/inbox/components/inbox-reader-dialog.tsx +224 -0
- package/src/features/inbox/components/inbox-runtime.tsx +14 -0
- package/src/features/inbox/hooks/use-inbox-deliveries.ts +14 -0
- package/src/features/inbox/index.ts +7 -0
- package/src/features/inbox/managers/inbox.manager.test.ts +86 -0
- package/src/features/inbox/managers/inbox.manager.ts +178 -0
- package/src/features/inbox/pages/inbox-page.test.ts +28 -0
- package/src/features/inbox/pages/inbox-page.tsx +385 -0
- package/src/features/inbox/stores/inbox.store.ts +23 -0
- package/src/features/marketplace/components/marketplace-page-parts.tsx +1 -1
- package/src/features/notifications/components/__tests__/app-notification-toast.test.tsx +51 -0
- package/src/features/notifications/components/app-notification-toast.tsx +80 -0
- package/src/features/notifications/index.ts +8 -0
- package/src/features/notifications/managers/__tests__/app-notification.manager.test.ts +75 -0
- package/src/features/notifications/managers/app-notification.manager.ts +41 -0
- package/src/features/panel-apps/components/panel-app-list-item.tsx +1 -1
- package/src/features/panel-apps/components/panel-app-toolbar.tsx +2 -2
- package/src/features/panel-apps/components/panel-apps-list.tsx +4 -4
- package/src/features/service-apps/components/service-apps-panel.tsx +1 -1
- package/src/features/settings/components/config/provider-auth-section.tsx +90 -59
- package/src/features/settings/components/config/provider-form.tsx +10 -17
- package/src/features/settings/components/config/provider-status-badge.tsx +4 -2
- package/src/features/settings/pages/__tests__/providers-config-page.test.tsx +12 -0
- package/src/features/settings/pages/providers-config-page.tsx +2 -1
- package/src/platforms/mobile/components/__tests__/mobile-app-shell.test.tsx +13 -7
- package/src/platforms/mobile/components/__tests__/mobile-bottom-nav.test.tsx +18 -20
- package/src/platforms/mobile/components/mobile-bottom-nav.tsx +14 -7
- package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +1 -1
- package/src/shared/components/path-picker/server-path-picker-directory-list.tsx +1 -1
- package/src/shared/components/path-picker/server-path-picker-locations.tsx +1 -1
- package/src/shared/components/path-picker/server-path-picker-navigation-bar.tsx +3 -3
- package/src/shared/components/ui/actions/__tests__/icon-action-button.test.tsx +1 -1
- package/src/shared/components/ui/actions/icon-action-button.tsx +1 -1
- package/src/shared/components/ui/button.tsx +4 -4
- package/src/shared/components/ui/dialog.tsx +1 -1
- package/src/shared/components/ui/input.tsx +1 -1
- package/src/shared/components/ui/scroll-area.tsx +2 -0
- package/src/shared/components/ui/select.tsx +1 -1
- package/src/shared/components/ui/switch.tsx +1 -1
- package/src/shared/components/ui/tabs.tsx +1 -1
- package/src/shared/components/ui/textarea.tsx +1 -1
- package/src/shared/lib/api/ncp-session.types.ts +1 -0
- package/src/shared/lib/api/types.ts +2 -0
- package/src/shared/lib/i18n/index.ts +4 -0
- package/src/shared/lib/i18n/locales/en-US/chat.json +16 -14
- package/src/shared/lib/i18n/locales/en-US/inbox.json +30 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +16 -14
- package/src/shared/lib/i18n/locales/zh-CN/inbox.json +30 -0
- package/src/shared/lib/provider-models/__tests__/anonymous-provider-configuration.test.ts +40 -0
- package/src/shared/lib/provider-models/index.ts +4 -3
- package/dist/assets/app-presenter-provider-s-FrOinZ.js +0 -106
- package/dist/assets/appearance-settings-page-dMRBD7ni.js +0 -1
- package/dist/assets/chat-page-MXBltS3G.js +0 -1
- package/dist/assets/config-split-page-uC6d_r9c.js +0 -1
- package/dist/assets/confirm-dialog-C1pbqHZX.js +0 -5
- package/dist/assets/desktop-update-config-DssP8LCk.js +0 -1
- package/dist/assets/doc-browser-AXF1zRsM.js +0 -1
- package/dist/assets/index-CocHWEsI.js +0 -2
- package/dist/assets/index-DN6KM7WD.css +0 -1
- package/dist/assets/loader-circle-AOpK5kkW.js +0 -1
- package/dist/assets/mcp-marketplace-page-5ogeBHnl.js +0 -1
- package/dist/assets/plus-DI37QuHc.js +0 -1
- package/dist/assets/provider-scoped-model-input-DIL6Ex_d.js +0 -1
- package/dist/assets/providers-config-page-D2NXVctW.js +0 -1
- package/dist/assets/react-39CDNZlM.js +0 -8
- package/dist/assets/remote-DYOOO1YG.js +0 -1
- package/dist/assets/rotate-cw-PmqA8LQT.js +0 -1
- package/dist/assets/runtime-config-page-0lb5NyKc.js +0 -1
- package/dist/assets/security-config-13AtPIlm.js +0 -1
- package/dist/assets/skeleton-OwCNQJ7M.js +0 -1
- package/dist/assets/use-confirm-dialog-C4e0nG_4.js +0 -1
- package/dist/assets/x-C1ijNWcE.js +0 -1
- package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +0 -59
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Link } from "react-router-dom";
|
|
2
|
+
|
|
3
|
+
export type AppNotificationToastProps = {
|
|
4
|
+
title: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
href?: string;
|
|
7
|
+
iconSrc?: string;
|
|
8
|
+
ariaLabel?: string;
|
|
9
|
+
onDismiss: () => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const NOTIFICATION_CARD_CLASS =
|
|
13
|
+
"group ml-auto flex min-h-[74px] w-[320px] max-w-[calc(100vw-2rem)] items-center gap-3 rounded-[20px] border border-border/80 bg-background px-[18px] py-3 text-left text-foreground shadow-[0_8px_18px_rgba(0,0,0,0.12),0_2px_5px_rgba(0,0,0,0.06)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background";
|
|
14
|
+
|
|
15
|
+
function AppNotificationContent({
|
|
16
|
+
title,
|
|
17
|
+
description,
|
|
18
|
+
iconSrc,
|
|
19
|
+
}: Pick<AppNotificationToastProps, "title" | "description" | "iconSrc">) {
|
|
20
|
+
return (
|
|
21
|
+
<>
|
|
22
|
+
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-md border border-border/80 bg-background p-[3px]">
|
|
23
|
+
<img
|
|
24
|
+
aria-hidden="true"
|
|
25
|
+
alt=""
|
|
26
|
+
src={iconSrc ?? "/logo.svg"}
|
|
27
|
+
className="h-full w-full object-contain"
|
|
28
|
+
/>
|
|
29
|
+
</span>
|
|
30
|
+
<span className="min-w-0 flex-1">
|
|
31
|
+
<span className="block truncate text-[15px] font-semibold leading-5 tracking-[-0.01em]">
|
|
32
|
+
{title}
|
|
33
|
+
</span>
|
|
34
|
+
{description ? (
|
|
35
|
+
<span className="mt-0.5 block truncate text-[14px] leading-5 text-muted-foreground">
|
|
36
|
+
{description}
|
|
37
|
+
</span>
|
|
38
|
+
) : null}
|
|
39
|
+
</span>
|
|
40
|
+
</>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function AppNotificationToast({
|
|
45
|
+
title,
|
|
46
|
+
description,
|
|
47
|
+
href,
|
|
48
|
+
iconSrc,
|
|
49
|
+
ariaLabel,
|
|
50
|
+
onDismiss,
|
|
51
|
+
}: AppNotificationToastProps) {
|
|
52
|
+
const accessibleLabel = ariaLabel ?? [title, description].filter(Boolean).join(": ");
|
|
53
|
+
|
|
54
|
+
if (href) {
|
|
55
|
+
return (
|
|
56
|
+
<Link
|
|
57
|
+
to={href}
|
|
58
|
+
aria-label={accessibleLabel}
|
|
59
|
+
className={NOTIFICATION_CARD_CLASS}
|
|
60
|
+
onClick={onDismiss}
|
|
61
|
+
>
|
|
62
|
+
<AppNotificationContent
|
|
63
|
+
title={title}
|
|
64
|
+
description={description}
|
|
65
|
+
iconSrc={iconSrc}
|
|
66
|
+
/>
|
|
67
|
+
</Link>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div role="status" aria-label={accessibleLabel} className={NOTIFICATION_CARD_CLASS}>
|
|
73
|
+
<AppNotificationContent
|
|
74
|
+
title={title}
|
|
75
|
+
description={description}
|
|
76
|
+
iconSrc={iconSrc}
|
|
77
|
+
/>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { isValidElement } from "react";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
AppNotificationManager,
|
|
5
|
+
AppNotificationToast,
|
|
6
|
+
type AppNotificationToastProps,
|
|
7
|
+
} from "@/features/notifications";
|
|
8
|
+
|
|
9
|
+
const mocks = vi.hoisted(() => ({
|
|
10
|
+
custom: vi.fn(),
|
|
11
|
+
dismiss: vi.fn(),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock("sonner", () => ({
|
|
15
|
+
toast: {
|
|
16
|
+
custom: mocks.custom,
|
|
17
|
+
dismiss: mocks.dismiss,
|
|
18
|
+
},
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
describe("AppNotificationManager", () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
mocks.custom.mockReset();
|
|
24
|
+
mocks.dismiss.mockReset();
|
|
25
|
+
mocks.custom.mockReturnValue("toast-result");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("uses the shared custom notification surface and stable lifecycle defaults", () => {
|
|
29
|
+
const manager = new AppNotificationManager();
|
|
30
|
+
const result = manager.show({
|
|
31
|
+
id: "reply:message-1",
|
|
32
|
+
title: "Research complete",
|
|
33
|
+
description: "The findings are ready.",
|
|
34
|
+
href: "/chat/session-1",
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(result).toBe("toast-result");
|
|
38
|
+
expect(mocks.custom).toHaveBeenCalledWith(expect.any(Function), {
|
|
39
|
+
id: "reply:message-1",
|
|
40
|
+
duration: 8_000,
|
|
41
|
+
unstyled: true,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const renderer = mocks.custom.mock.calls[0]?.[0] as (
|
|
45
|
+
id: string | number,
|
|
46
|
+
) => unknown;
|
|
47
|
+
const element = renderer("toast-rendered");
|
|
48
|
+
expect(isValidElement<AppNotificationToastProps>(element)).toBe(true);
|
|
49
|
+
if (!isValidElement<AppNotificationToastProps>(element)) {
|
|
50
|
+
throw new Error("Expected AppNotificationToast element");
|
|
51
|
+
}
|
|
52
|
+
expect(element.type).toBe(AppNotificationToast);
|
|
53
|
+
expect(element.props).toEqual(expect.objectContaining({
|
|
54
|
+
title: "Research complete",
|
|
55
|
+
description: "The findings are ready.",
|
|
56
|
+
href: "/chat/session-1",
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
element.props.onDismiss();
|
|
60
|
+
expect(mocks.dismiss).toHaveBeenCalledWith("toast-rendered");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("supports a caller-owned duration and explicit dismiss", () => {
|
|
64
|
+
const manager = new AppNotificationManager();
|
|
65
|
+
manager.show({ title: "Saved", durationMs: 2_500 });
|
|
66
|
+
manager.dismiss("saved-toast");
|
|
67
|
+
|
|
68
|
+
expect(mocks.custom).toHaveBeenCalledWith(expect.any(Function), {
|
|
69
|
+
id: undefined,
|
|
70
|
+
duration: 2_500,
|
|
71
|
+
unstyled: true,
|
|
72
|
+
});
|
|
73
|
+
expect(mocks.dismiss).toHaveBeenCalledWith("saved-toast");
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createElement } from "react";
|
|
2
|
+
import { toast } from "sonner";
|
|
3
|
+
import { AppNotificationToast } from "@/features/notifications/components/app-notification-toast";
|
|
4
|
+
|
|
5
|
+
export type AppNotification = {
|
|
6
|
+
id?: string | number;
|
|
7
|
+
title: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
href?: string;
|
|
10
|
+
iconSrc?: string;
|
|
11
|
+
ariaLabel?: string;
|
|
12
|
+
durationMs?: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const DEFAULT_NOTIFICATION_DURATION_MS = 8_000;
|
|
16
|
+
|
|
17
|
+
export class AppNotificationManager {
|
|
18
|
+
show = (notification: AppNotification): string | number =>
|
|
19
|
+
toast.custom(
|
|
20
|
+
(toastId) =>
|
|
21
|
+
createElement(AppNotificationToast, {
|
|
22
|
+
title: notification.title,
|
|
23
|
+
description: notification.description,
|
|
24
|
+
href: notification.href,
|
|
25
|
+
iconSrc: notification.iconSrc,
|
|
26
|
+
ariaLabel: notification.ariaLabel,
|
|
27
|
+
onDismiss: () => {
|
|
28
|
+
toast.dismiss(toastId);
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
{
|
|
32
|
+
id: notification.id,
|
|
33
|
+
duration: notification.durationMs ?? DEFAULT_NOTIFICATION_DURATION_MS,
|
|
34
|
+
unstyled: true,
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
dismiss = (id: string | number): void => {
|
|
39
|
+
toast.dismiss(id);
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -118,7 +118,7 @@ function PanelAppMenuItem({
|
|
|
118
118
|
type="button"
|
|
119
119
|
className={cn(
|
|
120
120
|
'flex w-full items-center gap-2 rounded-lg px-2.5 py-2 text-left text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-50',
|
|
121
|
-
destructive ? 'text-destructive hover:bg-destructive/10' : 'text-muted-foreground hover:bg-
|
|
121
|
+
destructive ? 'text-destructive hover:bg-destructive/10' : 'text-muted-foreground hover:bg-[var(--interaction-hover)] hover:text-accent-foreground',
|
|
122
122
|
)}
|
|
123
123
|
disabled={disabled}
|
|
124
124
|
onClick={onClick}
|
|
@@ -17,7 +17,7 @@ export function PanelAppToolbar({
|
|
|
17
17
|
<button
|
|
18
18
|
type="button"
|
|
19
19
|
onClick={onOpenApps}
|
|
20
|
-
className="inline-flex shrink-0 items-center rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-
|
|
20
|
+
className="inline-flex shrink-0 items-center rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-[var(--interaction-hover)] hover:text-accent-foreground"
|
|
21
21
|
title={t('appsTitle')}
|
|
22
22
|
aria-label={t('appsTitle')}
|
|
23
23
|
>
|
|
@@ -32,7 +32,7 @@ export function PanelAppToolbar({
|
|
|
32
32
|
<button
|
|
33
33
|
type="button"
|
|
34
34
|
onClick={onRefresh}
|
|
35
|
-
className="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-
|
|
35
|
+
className="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-[var(--interaction-hover)] hover:text-accent-foreground"
|
|
36
36
|
title={t('panelAppsRefreshCurrent')}
|
|
37
37
|
aria-label={t('panelAppsRefreshCurrent')}
|
|
38
38
|
>
|
|
@@ -104,7 +104,7 @@ export function PanelAppsList({
|
|
|
104
104
|
<TooltipProvider delayDuration={250}>
|
|
105
105
|
<Tooltip>
|
|
106
106
|
<TooltipTrigger asChild>
|
|
107
|
-
<button type="button" className="rounded p-0.5 text-muted-foreground/70 transition-colors hover:bg-
|
|
107
|
+
<button type="button" className="rounded p-0.5 text-muted-foreground/70 transition-colors hover:bg-[var(--interaction-hover)] hover:text-accent-foreground" aria-label={t('panelAppsTitle')}><HelpCircle className="h-3.5 w-3.5" /></button>
|
|
108
108
|
</TooltipTrigger>
|
|
109
109
|
<TooltipContent side="bottom" className="max-w-[320px] break-all font-mono text-xs">{panelApps.data.panelsPath}</TooltipContent>
|
|
110
110
|
</Tooltip>
|
|
@@ -114,7 +114,7 @@ export function PanelAppsList({
|
|
|
114
114
|
<button
|
|
115
115
|
type="button"
|
|
116
116
|
onClick={() => void panelApps.refetch()}
|
|
117
|
-
className="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-
|
|
117
|
+
className="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-[var(--interaction-hover)] hover:text-accent-foreground"
|
|
118
118
|
title={t('panelAppsRefresh')}
|
|
119
119
|
aria-label={t('panelAppsRefresh')}
|
|
120
120
|
>
|
|
@@ -204,7 +204,7 @@ function PanelAppsEmptyGuide({
|
|
|
204
204
|
<button
|
|
205
205
|
type="button"
|
|
206
206
|
onClick={onRefresh}
|
|
207
|
-
className="mt-4 inline-flex items-center justify-center gap-1.5 rounded-md border border-border px-3 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:bg-
|
|
207
|
+
className="mt-4 inline-flex items-center justify-center gap-1.5 rounded-md border border-border px-3 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:bg-[var(--interaction-hover)] hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
|
|
208
208
|
>
|
|
209
209
|
<RefreshCw className="h-3.5 w-3.5" />
|
|
210
210
|
{t('panelAppsRefresh')}
|
|
@@ -245,7 +245,7 @@ function PanelAppsEmptyGuideStep({
|
|
|
245
245
|
<button
|
|
246
246
|
type="button"
|
|
247
247
|
onClick={onAction}
|
|
248
|
-
className="flex w-full gap-2 rounded-md bg-muted/60 px-3 py-2.5 text-left transition-colors hover:bg-
|
|
248
|
+
className="flex w-full gap-2 rounded-md bg-muted/60 px-3 py-2.5 text-left transition-colors hover:bg-[var(--interaction-hover)] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
|
|
249
249
|
>
|
|
250
250
|
{content}
|
|
251
251
|
</button>
|
|
@@ -463,7 +463,7 @@ function ServiceAppMenuItem({
|
|
|
463
463
|
type="button"
|
|
464
464
|
className={cn(
|
|
465
465
|
'flex w-full items-center gap-2 rounded-lg px-2.5 py-2 text-left text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-50',
|
|
466
|
-
destructive ? 'text-destructive hover:bg-destructive/10' : 'text-muted-foreground hover:bg-
|
|
466
|
+
destructive ? 'text-destructive hover:bg-destructive/10' : 'text-muted-foreground hover:bg-[var(--interaction-hover)] hover:text-accent-foreground',
|
|
467
467
|
)}
|
|
468
468
|
disabled={disabled}
|
|
469
469
|
onClick={onClick}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Button } from '@/shared/components/ui/button';
|
|
2
|
+
import { MaskedInput } from '@/shared/components/common/masked-input';
|
|
2
3
|
import { Label } from '@/shared/components/ui/label';
|
|
3
4
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select';
|
|
4
5
|
import { t } from '@/shared/lib/i18n';
|
|
@@ -6,6 +7,10 @@ import type { ProviderTemplateView } from '@/shared/lib/api';
|
|
|
6
7
|
import { ProviderPillSelector } from './provider-pill-selector';
|
|
7
8
|
|
|
8
9
|
type ProviderAuthSectionProps = {
|
|
10
|
+
apiKey: string;
|
|
11
|
+
apiKeyRequired: boolean;
|
|
12
|
+
apiKeySet: boolean;
|
|
13
|
+
apiKeyPlaceholder: string;
|
|
9
14
|
providerAuth: ProviderTemplateView['auth'];
|
|
10
15
|
providerAuthNote: string;
|
|
11
16
|
providerAuthMethodOptions: Array<{ value: string; label: string }>;
|
|
@@ -20,10 +25,15 @@ type ProviderAuthSectionProps = {
|
|
|
20
25
|
importPending: boolean;
|
|
21
26
|
authSessionId: string | null;
|
|
22
27
|
authStatusMessage: string;
|
|
28
|
+
onApiKeyChange: (value: string) => void;
|
|
23
29
|
};
|
|
24
30
|
|
|
25
31
|
export function ProviderAuthSection(props: ProviderAuthSectionProps) {
|
|
26
32
|
const {
|
|
33
|
+
apiKey,
|
|
34
|
+
apiKeyRequired,
|
|
35
|
+
apiKeySet,
|
|
36
|
+
apiKeyPlaceholder,
|
|
27
37
|
providerAuth,
|
|
28
38
|
providerAuthNote,
|
|
29
39
|
providerAuthMethodOptions,
|
|
@@ -37,75 +47,96 @@ export function ProviderAuthSection(props: ProviderAuthSectionProps) {
|
|
|
37
47
|
startPending,
|
|
38
48
|
importPending,
|
|
39
49
|
authSessionId,
|
|
40
|
-
authStatusMessage
|
|
50
|
+
authStatusMessage,
|
|
51
|
+
onApiKeyChange
|
|
41
52
|
} = props;
|
|
42
53
|
|
|
43
|
-
|
|
54
|
+
const apiKeyField = apiKeyRequired ? (
|
|
55
|
+
<div className='space-y-2'>
|
|
56
|
+
<Label htmlFor='apiKey' className='text-sm font-medium text-foreground'>
|
|
57
|
+
{t('apiKey')}
|
|
58
|
+
</Label>
|
|
59
|
+
<MaskedInput
|
|
60
|
+
id='apiKey'
|
|
61
|
+
value={apiKey}
|
|
62
|
+
isSet={apiKeySet}
|
|
63
|
+
onChange={(event) => onApiKeyChange(event.target.value)}
|
|
64
|
+
placeholder={apiKeyPlaceholder}
|
|
65
|
+
className='rounded-xl'
|
|
66
|
+
/>
|
|
67
|
+
<p className='text-xs text-muted-foreground'>{t('leaveBlankToKeepUnchanged')}</p>
|
|
68
|
+
</div>
|
|
69
|
+
) : null;
|
|
70
|
+
|
|
71
|
+
if (providerAuth?.kind !== 'device_code') return apiKeyField;
|
|
44
72
|
|
|
45
73
|
return (
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
variant='outline'
|
|
81
|
-
size='sm'
|
|
82
|
-
onClick={onStartProviderAuth}
|
|
83
|
-
disabled={startPending || Boolean(authSessionId)}
|
|
84
|
-
>
|
|
85
|
-
{startPending
|
|
86
|
-
? t('providerAuthStarting')
|
|
87
|
-
: authSessionId
|
|
88
|
-
? t('providerAuthAuthorizing')
|
|
89
|
-
: t('providerAuthAuthorizeInBrowser')}
|
|
90
|
-
</Button>
|
|
91
|
-
{providerAuth.supportsCliImport ? (
|
|
74
|
+
<>
|
|
75
|
+
{apiKeyField}
|
|
76
|
+
<div className='space-y-2 rounded-xl bg-muted/45 p-3'>
|
|
77
|
+
<Label className='text-sm font-medium text-foreground'>
|
|
78
|
+
{providerAuth.displayName || t('providerAuthSectionTitle')}
|
|
79
|
+
</Label>
|
|
80
|
+
{providerAuthNote ? <p className='text-xs text-muted-foreground'>{providerAuthNote}</p> : null}
|
|
81
|
+
{providerAuthMethodsCount > 1 ? (
|
|
82
|
+
<div className='space-y-2'>
|
|
83
|
+
<Label className='text-xs font-medium text-foreground'>{t('providerAuthMethodLabel')}</Label>
|
|
84
|
+
{shouldUseAuthMethodPills ? (
|
|
85
|
+
<ProviderPillSelector
|
|
86
|
+
value={resolvedAuthMethodId}
|
|
87
|
+
onChange={onAuthMethodChange}
|
|
88
|
+
options={providerAuthMethodOptions}
|
|
89
|
+
/>
|
|
90
|
+
) : (
|
|
91
|
+
<Select value={resolvedAuthMethodId} onValueChange={onAuthMethodChange}>
|
|
92
|
+
<SelectTrigger className='h-8 rounded-lg'>
|
|
93
|
+
<SelectValue placeholder={t('providerAuthMethodPlaceholder')} />
|
|
94
|
+
</SelectTrigger>
|
|
95
|
+
<SelectContent>
|
|
96
|
+
{providerAuthMethodOptions.map((method) => (
|
|
97
|
+
<SelectItem key={method.value} value={method.value}>
|
|
98
|
+
{method.label}
|
|
99
|
+
</SelectItem>
|
|
100
|
+
))}
|
|
101
|
+
</SelectContent>
|
|
102
|
+
</Select>
|
|
103
|
+
)}
|
|
104
|
+
{selectedAuthMethodHint ? <p className='text-xs text-muted-foreground'>{selectedAuthMethodHint}</p> : null}
|
|
105
|
+
</div>
|
|
106
|
+
) : null}
|
|
107
|
+
<div className='flex flex-wrap items-center gap-2'>
|
|
92
108
|
<Button
|
|
93
109
|
type='button'
|
|
94
110
|
variant='outline'
|
|
95
111
|
size='sm'
|
|
96
|
-
onClick={
|
|
97
|
-
disabled={
|
|
112
|
+
onClick={onStartProviderAuth}
|
|
113
|
+
disabled={startPending || Boolean(authSessionId)}
|
|
98
114
|
>
|
|
99
|
-
{
|
|
115
|
+
{startPending
|
|
116
|
+
? t('providerAuthStarting')
|
|
117
|
+
: authSessionId
|
|
118
|
+
? t('providerAuthAuthorizing')
|
|
119
|
+
: t('providerAuthAuthorizeInBrowser')}
|
|
100
120
|
</Button>
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
121
|
+
{providerAuth.supportsCliImport ? (
|
|
122
|
+
<Button
|
|
123
|
+
type='button'
|
|
124
|
+
variant='outline'
|
|
125
|
+
size='sm'
|
|
126
|
+
onClick={onImportProviderAuthFromCli}
|
|
127
|
+
disabled={importPending}
|
|
128
|
+
>
|
|
129
|
+
{importPending ? t('providerAuthImporting') : t('providerAuthImportFromCli')}
|
|
130
|
+
</Button>
|
|
131
|
+
) : null}
|
|
132
|
+
{authSessionId ? (
|
|
133
|
+
<span className='text-xs text-muted-foreground'>
|
|
134
|
+
{t('providerAuthSessionLabel')}: {authSessionId.slice(0, 8)}…
|
|
135
|
+
</span>
|
|
136
|
+
) : null}
|
|
137
|
+
</div>
|
|
138
|
+
{authStatusMessage ? <p className='text-xs text-muted-foreground'>{authStatusMessage}</p> : null}
|
|
107
139
|
</div>
|
|
108
|
-
|
|
109
|
-
</div>
|
|
140
|
+
</>
|
|
110
141
|
);
|
|
111
142
|
}
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
useTestProviderConnection,
|
|
10
10
|
useUpdateProvider
|
|
11
11
|
} from '@/shared/hooks/use-config';
|
|
12
|
-
import { MaskedInput } from '@/shared/components/common/masked-input';
|
|
13
12
|
import { Button } from '@/shared/components/ui/button';
|
|
14
13
|
import { Input } from '@/shared/components/ui/input';
|
|
15
14
|
import { Label } from '@/shared/components/ui/label';
|
|
@@ -445,7 +444,11 @@ function ProviderFormDetailPane(props: ProviderFormDetailPaneProps) {
|
|
|
445
444
|
>
|
|
446
445
|
<Trash2 className='h-4 w-4' />
|
|
447
446
|
</button>
|
|
448
|
-
<ProviderStatusBadge
|
|
447
|
+
<ProviderStatusBadge
|
|
448
|
+
enabled={context.currentEnabled}
|
|
449
|
+
apiKeyRequired={context.resolvedProviderConfig.apiKeyRequired}
|
|
450
|
+
apiKeySet={context.resolvedProviderConfig.apiKeySet}
|
|
451
|
+
/>
|
|
449
452
|
</div>
|
|
450
453
|
</div>
|
|
451
454
|
</ConfigSplitPaneHeader>
|
|
@@ -467,22 +470,11 @@ function ProviderFormDetailPane(props: ProviderFormDetailPaneProps) {
|
|
|
467
470
|
<p className='text-xs text-muted-foreground'>{t('providerDisplayNameHelpShort')}</p>
|
|
468
471
|
</div>
|
|
469
472
|
|
|
470
|
-
<div className='space-y-2'>
|
|
471
|
-
<Label htmlFor='apiKey' className='text-sm font-medium text-foreground'>
|
|
472
|
-
{t('apiKey')}
|
|
473
|
-
</Label>
|
|
474
|
-
<MaskedInput
|
|
475
|
-
id='apiKey'
|
|
476
|
-
value={apiKey}
|
|
477
|
-
isSet={context.resolvedProviderConfig.apiKeySet}
|
|
478
|
-
onChange={(e) => onApiKeyChange(e.target.value)}
|
|
479
|
-
placeholder={context.apiKeyHint?.placeholder ?? t('enterApiKey')}
|
|
480
|
-
className='rounded-xl'
|
|
481
|
-
/>
|
|
482
|
-
<p className='text-xs text-muted-foreground'>{t('leaveBlankToKeepUnchanged')}</p>
|
|
483
|
-
</div>
|
|
484
|
-
|
|
485
473
|
<ProviderAuthSection
|
|
474
|
+
apiKey={apiKey}
|
|
475
|
+
apiKeyRequired={context.resolvedProviderConfig.apiKeyRequired !== false}
|
|
476
|
+
apiKeySet={context.resolvedProviderConfig.apiKeySet}
|
|
477
|
+
apiKeyPlaceholder={context.apiKeyHint?.placeholder ?? t('enterApiKey')}
|
|
486
478
|
providerAuth={context.providerAuth}
|
|
487
479
|
providerAuthNote={context.providerAuth?.note?.[language] || context.providerAuth?.note?.en || context.providerAuth?.displayName || ''}
|
|
488
480
|
providerAuthMethodOptions={context.providerAuthMethodOptions}
|
|
@@ -497,6 +489,7 @@ function ProviderFormDetailPane(props: ProviderFormDetailPaneProps) {
|
|
|
497
489
|
importPending={importPending}
|
|
498
490
|
authSessionId={authSessionId}
|
|
499
491
|
authStatusMessage={authStatusMessage}
|
|
492
|
+
onApiKeyChange={onApiKeyChange}
|
|
500
493
|
/>
|
|
501
494
|
|
|
502
495
|
<div className='space-y-2'>
|
|
@@ -3,13 +3,15 @@ import { t } from '@/shared/lib/i18n';
|
|
|
3
3
|
|
|
4
4
|
type ProviderStatusBadgeProps = {
|
|
5
5
|
enabled: boolean;
|
|
6
|
+
apiKeyRequired?: boolean;
|
|
6
7
|
apiKeySet: boolean;
|
|
7
8
|
className?: string;
|
|
8
9
|
};
|
|
9
10
|
|
|
10
|
-
export function ProviderStatusBadge({ apiKeySet, className, enabled }: ProviderStatusBadgeProps) {
|
|
11
|
+
export function ProviderStatusBadge({ apiKeyRequired = true, apiKeySet, className, enabled }: ProviderStatusBadgeProps) {
|
|
11
12
|
if (!enabled) {
|
|
12
13
|
return <StatusDot status="inactive" label={t('disabled')} className={className} />;
|
|
13
14
|
}
|
|
14
|
-
|
|
15
|
+
const isReady = !apiKeyRequired || apiKeySet;
|
|
16
|
+
return <StatusDot status={isReady ? 'ready' : 'setup'} label={isReady ? t('statusReady') : t('statusSetup')} className={className} />;
|
|
15
17
|
}
|
|
@@ -11,6 +11,7 @@ const mocks = vi.hoisted(() => ({
|
|
|
11
11
|
providers: {
|
|
12
12
|
openai: { enabled: true, apiKeySet: true },
|
|
13
13
|
anthropic: { enabled: true, apiKeySet: true },
|
|
14
|
+
opencode: { enabled: true, apiKeyRequired: false, apiKeySet: false },
|
|
14
15
|
nextclaw: { enabled: true, apiKeySet: true },
|
|
15
16
|
},
|
|
16
17
|
},
|
|
@@ -22,6 +23,7 @@ const mocks = vi.hoisted(() => ({
|
|
|
22
23
|
{ name: 'nextclaw', displayName: 'NextClaw Builtin', defaultApiBase: 'https://ai-gateway-api.nextclaw.io/v1' },
|
|
23
24
|
{ name: 'openai', displayName: 'OpenAI', defaultApiBase: 'https://api.openai.com/v1' },
|
|
24
25
|
{ name: 'anthropic', displayName: 'Anthropic', defaultApiBase: 'https://api.anthropic.com/v1' },
|
|
26
|
+
{ name: 'opencode', displayName: 'OpenCode Zen Free Trial', defaultApiBase: 'https://opencode.ai/zen/v1' },
|
|
25
27
|
],
|
|
26
28
|
},
|
|
27
29
|
},
|
|
@@ -46,6 +48,7 @@ vi.mock('@/shared/hooks/use-config', () => ({
|
|
|
46
48
|
isBuiltInType: providerId === 'nextclaw',
|
|
47
49
|
isCustom: false,
|
|
48
50
|
enabled: provider.enabled,
|
|
51
|
+
apiKeyRequired: 'apiKeyRequired' in provider ? provider.apiKeyRequired : undefined,
|
|
49
52
|
apiKeySet: provider.apiKeySet,
|
|
50
53
|
},
|
|
51
54
|
]),
|
|
@@ -108,4 +111,13 @@ describe('ProvidersConfigPage', () => {
|
|
|
108
111
|
expect.stringContaining('NextClaw Builtin'),
|
|
109
112
|
]);
|
|
110
113
|
});
|
|
114
|
+
|
|
115
|
+
it('shows an enabled provider without a required API key as ready', () => {
|
|
116
|
+
const { container } = render(<ProvidersConfigPage />);
|
|
117
|
+
const opencodeCard = Array.from(container.querySelectorAll('[role="button"]'))
|
|
118
|
+
.find((element) => element.textContent?.includes('OpenCode Zen Free Trial'));
|
|
119
|
+
|
|
120
|
+
expect(opencodeCard?.textContent).toContain('Ready');
|
|
121
|
+
expect(opencodeCard?.textContent).not.toContain('Setup');
|
|
122
|
+
});
|
|
111
123
|
});
|
|
@@ -17,6 +17,7 @@ import { ConfirmDialog } from '@/shared/components/ui/confirm-dialog';
|
|
|
17
17
|
import { StatusDot } from '@/shared/components/status/status-dot';
|
|
18
18
|
import { t } from '@/shared/lib/i18n';
|
|
19
19
|
import { cn } from '@/shared/lib/utils';
|
|
20
|
+
import { isProviderConfigured } from '@/shared/lib/provider-models';
|
|
20
21
|
import { ProviderForm } from '@/features/settings/components/config/provider-form';
|
|
21
22
|
import { useViewportLayout } from '@/app/hooks/use-viewport-layout';
|
|
22
23
|
import {
|
|
@@ -232,7 +233,7 @@ export function ProvidersConfigPage() {
|
|
|
232
233
|
{filteredProviders.map((provider) => {
|
|
233
234
|
const template = provider.providerType ? templateByType.get(provider.providerType) : null;
|
|
234
235
|
const isEnabled = provider.enabled !== false;
|
|
235
|
-
const isReady =
|
|
236
|
+
const isReady = isProviderConfigured(provider);
|
|
236
237
|
const providerLabel = provider.displayName?.trim() || template?.displayName || provider.providerId;
|
|
237
238
|
const description =
|
|
238
239
|
formatBasePreview(provider.apiBase || template?.defaultApiBase || '') ||
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
2
3
|
import { MemoryRouter } from "react-router-dom";
|
|
3
4
|
import { describe, expect, it, vi } from "vitest";
|
|
4
5
|
import { I18nProvider } from "@/app/components/i18n-provider";
|
|
@@ -9,14 +10,19 @@ vi.mock("@/shared/components/doc-browser", () => ({
|
|
|
9
10
|
}));
|
|
10
11
|
|
|
11
12
|
function renderShell(pathname: string) {
|
|
13
|
+
const queryClient = new QueryClient({
|
|
14
|
+
defaultOptions: { queries: { retry: false } },
|
|
15
|
+
});
|
|
12
16
|
const view = render(
|
|
13
|
-
<
|
|
14
|
-
<
|
|
15
|
-
<
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
<QueryClientProvider client={queryClient}>
|
|
18
|
+
<I18nProvider>
|
|
19
|
+
<MemoryRouter initialEntries={[pathname]}>
|
|
20
|
+
<MobileAppShell pathname={pathname} isDocBrowserOpen={false}>
|
|
21
|
+
<div>Content</div>
|
|
22
|
+
</MobileAppShell>
|
|
23
|
+
</MemoryRouter>
|
|
24
|
+
</I18nProvider>
|
|
25
|
+
</QueryClientProvider>,
|
|
20
26
|
);
|
|
21
27
|
return {
|
|
22
28
|
...view,
|