@kortix/ui 0.14.6 → 0.14.7
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/dist/_vendor/kortix-shared.js +489 -381
- package/dist/customize/agents/agent-authority-card.js +1 -2
- package/dist/customize/agents/agent-detail-aside.js +15 -14
- package/dist/customize/agents/agent-drives-section.js +5 -6
- package/dist/customize/agents/agent-grant-pages.js +68 -70
- package/dist/customize/agents/agent-nav.prototype.d.ts +35 -0
- package/dist/customize/agents/agent-nav.prototype.js +95 -0
- package/dist/customize/agents/agent-page.js +57 -22
- package/dist/customize/agents/agent-people-section.js +18 -17
- package/dist/customize/agents/agent-triggers-section.js +6 -5
- package/dist/customize/connectors/catalog/catalog-entry.d.ts +14 -0
- package/dist/customize/connectors/catalog/catalog-entry.js +18 -1
- package/dist/customize/connectors/catalog/connector-browse.d.ts +28 -0
- package/dist/customize/connectors/catalog/connector-browse.js +18 -5
- package/dist/customize/connectors/detail/app-page.js +19 -5
- package/dist/customize/connectors/detail/connector-modal.d.ts +10 -13
- package/dist/customize/connectors/detail/connector-modal.js +18 -101
- package/dist/customize/connectors/detail/connector-notice.js +6 -2
- package/dist/customize/connectors/detail/connector-page.d.ts +30 -0
- package/dist/customize/connectors/detail/connector-page.js +26 -13
- package/dist/customize/sections/llm-provider/custom-provider-form.js +24 -7
- package/dist/customize/sections/llm-provider/custom-provider-panel.js +44 -8
- package/dist/customize/sections/llm-provider/types.d.ts +3 -0
- package/dist/customize/sections/llm-provider/utils.js +2 -2
- package/dist/customize/sections/view/agent-editor-access-fields.d.ts +9 -16
- package/dist/customize/sections/view/agent-editor-access-fields.js +30 -30
- package/dist/customize/sections/view/agent-editor-basics-fields.js +3 -10
- package/dist/customize/sections/view/agent-editor-primitives.d.ts +28 -10
- package/dist/customize/sections/view/agent-editor-primitives.js +32 -19
- package/dist/customize/sections/view/agent-editor.js +3 -3
- package/dist/customize/sections/view/gateway/gateway-routing.js +3 -3
- package/dist/customize/sections/view/grant-mode-field.d.ts +2 -5
- package/dist/customize/sections/view/grant-mode-field.js +10 -18
- package/dist/customize/sections/view/permission-editor.js +11 -8
- package/dist/customize/sections/view/secrets-view.js +2 -1
- package/dist/customize/settings/tabs/general-tab.d.ts +9 -2
- package/dist/customize/settings/tabs/general-tab.js +33 -11
- package/dist/customize/settings/tabs/transfer-workspace-dialog.d.ts +4 -0
- package/dist/customize/settings/tabs/transfer-workspace-dialog.js +82 -0
- package/dist/files/project/api/runtime-files.js +3 -2
- package/dist/files/project/file-source.js +18 -1
- package/dist/files/project/hooks/use-file-content.js +2 -0
- package/dist/files/viewer/file-header.js +1 -1
- package/dist/files/viewer/file-preview-modal.d.ts +1 -1
- package/dist/files/viewer/file-preview-modal.js +12 -2
- package/dist/files/viewer/file-source.d.ts +3 -1
- package/dist/files/viewer/use-file-content-state.js +26 -7
- package/dist/files/viewer/viewer-actions.js +2 -2
- package/dist/i18n/en.d.ts +345 -12
- package/dist/i18n/en.js +350 -17
- package/dist/internal/files/types.d.ts +2 -0
- package/dist/internal/goals/goal-meta.d.ts +30 -0
- package/dist/internal/goals/goal-meta.js +55 -0
- package/dist/internal/lib/query-client-singleton.d.ts +9 -3
- package/dist/internal/lib/query-client-singleton.js +18 -12
- package/dist/internal/share/session-shares-modal.js +3 -7
- package/dist/internal/stores/kortix-computer-store.js +14 -2
- package/dist/internal/stores/session-hover-store.d.ts +32 -0
- package/dist/internal/stores/session-hover-store.js +38 -0
- package/dist/internal/stores/sidebar-nav-store.d.ts +1 -1
- package/dist/internal/stores/sidebar-nav-store.js +3 -1
- package/dist/lib/routes.d.ts +2 -0
- package/dist/lib/routes.js +1 -0
- package/dist/primitives/settings-section-header.d.ts +1 -1
- package/dist/session/composer/attachment-tile.d.ts +0 -2
- package/dist/session/composer/attachment-tile.js +0 -8
- package/dist/session/composer/clipboard-files.d.ts +2 -1
- package/dist/session/composer/composer-draft-recovery.d.ts +1 -2
- package/dist/session/composer/composer-draft-recovery.js +0 -13
- package/dist/session/composer/context-ring.d.ts +1 -1
- package/dist/session/composer/context-ring.js +2 -2
- package/dist/session/composer/context-usage.d.ts +3 -6
- package/dist/session/composer/context-usage.js +3 -6
- package/dist/session/composer/inbox-row-claims.d.ts +2 -2
- package/dist/session/composer/inbox-row-claims.js +1 -1
- package/dist/session/composer/queue-projection.d.ts +3 -2
- package/dist/session/composer/queued-prompt-edit.d.ts +1 -1
- package/dist/session/composer/reasoning-effort-selector.d.ts +2 -1
- package/dist/session/composer/session-composer-readiness.d.ts +7 -22
- package/dist/session/composer/session-composer-readiness.js +12 -40
- package/dist/session/composer/token-progress.d.ts +1 -1
- package/dist/session/composer/token-progress.js +2 -2
- package/dist/session/header/session-goal-indicator.d.ts +1 -0
- package/dist/session/header/session-goal-indicator.js +41 -0
- package/dist/session/header/session-site-header.js +2 -1
- package/dist/session/panel/session-action-panel-column.js +11 -1
- package/dist/session/panel/session-audit-shared.d.ts +0 -4
- package/dist/session/panel/session-audit-shared.js +1 -1
- package/dist/session/panel/session-changes-shared.d.ts +2 -1
- package/dist/session/prompts/session-permission-prompt.js +6 -3
- package/dist/session/status/provisioning-failure.d.ts +2 -1
- package/dist/session/status/session-error-banner.js +8 -6
- package/dist/session/status/session-starting-loader.d.ts +24 -31
- package/dist/session/status/session-starting-loader.js +32 -81
- package/dist/session/thread/chat-minimap-items.d.ts +4 -4
- package/dist/session/thread/first-prompt-handover.d.ts +3 -2
- package/dist/session/thread/message-parsing.d.ts +3 -3
- package/dist/session/thread/session-chat.js +3 -10
- package/dist/session/thread/session-history-scroll.d.ts +2 -1
- package/dist/session/thread/session-thread.js +1 -1
- package/dist/styles.css +1 -1
- package/dist/terminal/pty-connection.d.ts +5 -4
- package/dist/workspace/index.d.ts +1 -0
- package/dist/workspace/index.js +1 -0
- package/dist/workspace/saved-session-skeleton-shape.d.ts +4 -3
- package/dist/workspace/saved-session-skeleton.d.ts +5 -1
- package/dist/workspace/saved-session-skeleton.js +5 -4
- package/dist/workspace/session-workspace.js +4 -5
- package/dist/workspace/sidebar/footer/project-goals-nav.d.ts +6 -0
- package/dist/workspace/sidebar/footer/project-goals-nav.js +26 -0
- package/dist/workspace/sidebar/modal/session-embed-section.d.ts +25 -0
- package/dist/workspace/sidebar/modal/session-embed-section.js +71 -0
- package/dist/workspace/sidebar/modal/share-session-modal.js +2 -2
- package/dist/workspace/sidebar/nav-row-meta.js +14 -2
- package/dist/workspace/sidebar/project-nav-items.js +3 -0
- package/dist/workspace/sidebar/project-session-list.js +12 -3
- package/dist/workspace/sidebar/session-brief-hover-card.js +5 -8
- package/package.json +4 -4
- package/skills/kortix-ui/references/modules.md +2 -0
- package/dist/session/status/use-readiness-settling.d.ts +0 -18
- package/dist/session/status/use-readiness-settling.js +0 -43
- package/dist/workspace/sidebar/modal/session-public-link-section.d.ts +0 -13
- package/dist/workspace/sidebar/modal/session-public-link-section.js +0 -66
|
@@ -1,117 +1,34 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useTranslations } from '../../../i18n/index.js';
|
|
4
|
-
import { listPipedreamApps } from '@kortix/sdk';
|
|
5
|
-
import { KeyIcon, PlusIcon } from '@phosphor-icons/react';
|
|
6
4
|
import { VisuallyHidden } from '@radix-ui/react-visually-hidden';
|
|
7
|
-
import { useQuery } from '@tanstack/react-query';
|
|
8
|
-
import { useState } from 'react';
|
|
9
|
-
import { Badge } from '../../../primitives/badge.js';
|
|
10
5
|
import { Button } from '../../../primitives/button.js';
|
|
11
|
-
import {
|
|
12
|
-
import Loading from '../../../primitives/loading.js';
|
|
13
|
-
import { Modal, ModalBody, ModalClose, ModalContent, ModalHeader, ModalTitle, } from '../../../primitives/modal.js';
|
|
6
|
+
import { Modal, ModalClose, ModalContent, ModalTitle } from '../../../primitives/modal.js';
|
|
14
7
|
import { Skeleton } from '../../../primitives/skeleton.js';
|
|
15
|
-
import { Tabs, TabsList, TabsTrigger } from '../../../primitives/tabs.js';
|
|
16
|
-
import { ComputerConnectModal } from '../../../internal/tunnel/computer-connect.js';
|
|
17
|
-
import { SetCredentialModal } from '../../sections/connectors-view.js';
|
|
18
|
-
import { ConnectorAppIcon, ConnectorStatusBadge, } from '../connector-identity.js';
|
|
19
|
-
import { cn } from '../../../internal/lib/utils.js';
|
|
20
|
-
import { ButtonGroup } from '../../../primitives/button-group.js';
|
|
21
8
|
import { Close } from '../../../icons/close.js';
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
import { useConnectorEventTriggers } from './connector-triggers.js';
|
|
26
|
-
import { useConnectorDetail } from './use-connector-detail.js';
|
|
9
|
+
import { ConnectorDetailView } from './connector-page.js';
|
|
10
|
+
/** The scroll column inside the modal: the page's column, tighter. */
|
|
11
|
+
const MODAL_COLUMN = 'w-full space-y-6 px-6 py-6';
|
|
27
12
|
/**
|
|
28
|
-
*
|
|
13
|
+
* One connector over the agent editor, without leaving it (a navigation would
|
|
14
|
+
* drop the agent's unsaved draft). The body IS the connector page
|
|
15
|
+
* (`ConnectorDetailView`): the same header, status strip, notice, tabs and
|
|
16
|
+
* split credential sheet, so the modal and the page cannot drift. The modal
|
|
17
|
+
* adds only a close button and keeps its tab in state instead of the URL.
|
|
29
18
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* Right: the active tab.
|
|
33
|
-
*
|
|
34
|
-
* `ConnectorModalBody` is keyed on `connector.slug` so picking a different card
|
|
35
|
-
* while the modal stays open resets the active tab without remounting
|
|
36
|
-
* `Modal`/`ModalContent` (which would replay the open animation).
|
|
19
|
+
* The view is keyed on `connector.slug` so picking a different connector while
|
|
20
|
+
* the modal stays open resets the tab without replaying the open animation.
|
|
37
21
|
*/
|
|
38
22
|
export function ConnectorModal({ projectId, connector, canWrite, open, isResolving = false, onOpenChange, onChanged, onRemoved, }) {
|
|
39
|
-
|
|
23
|
+
const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
|
|
24
|
+
return (_jsx(Modal, { open: open, onOpenChange: onOpenChange, children: _jsx(ModalContent, { className: "bg-background flex max-h-[92vh] min-h-[85vh] flex-col space-y-0 overflow-hidden p-0 lg:max-w-5xl", "aria-describedby": undefined, showCloseButton: false, children: connector ? (_jsx("div", { className: "flex min-h-0 flex-1 flex-col", children: _jsx(ConnectorDetailView, { projectId: projectId, connector: connector, canWrite: canWrite, onChanged: onChanged, onRemoved: onChanged, onLeave: onRemoved, urlState: false, columnClassName: MODAL_COLUMN, wrapTitle: (title) => _jsx(ModalTitle, { asChild: true, children: title }), headerEnd: _jsx(ModalClose, { asChild: true, children: _jsx(Button, { variant: "secondary", size: "icon-sm", className: "shrink-0", "aria-label": tI18nComplete.raw('text7d9eb7acb13e'), children: _jsx(Close, { className: "text-foreground size-4 stroke-1" }) }) }) }, connector.slug) })) : isResolving ? (_jsx(ConnectorModalSkeleton, {})) : null }) }));
|
|
40
25
|
}
|
|
41
26
|
/**
|
|
42
|
-
* The shell
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* relaying the modal out from under the pointer.
|
|
46
|
-
*
|
|
47
|
-
* The `ModalTitle` is real and visually hidden: Radix's Dialog needs an
|
|
48
|
-
* accessible name at all times, and a screen reader announcing "Loading
|
|
49
|
-
* connector" is the honest answer during this window.
|
|
27
|
+
* The shell while the selection resolves. Shape-matched to the view — icon and
|
|
28
|
+
* name, the strip, the tabs, two rows — so the handover fills it in place.
|
|
29
|
+
* The visually hidden title gives the dialog its accessible name meanwhile.
|
|
50
30
|
*/
|
|
51
31
|
function ConnectorModalSkeleton() {
|
|
52
32
|
const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
|
|
53
|
-
return (_jsxs(
|
|
54
|
-
}
|
|
55
|
-
function ConnectorModalBody({ projectId, connector, canWrite, onChanged, onRemoved, }) {
|
|
56
|
-
const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
|
|
57
|
-
const { displayName, isManagedProvider, isChannel, isComputer, connectionsQuery, connected, canManageConnections, headerCta, connectPending, refreshAccounts, replaceSoleAccount, startPrivateSession, credentialTarget, setCredentialTarget, computerOpen, setComputerOpen, } = useConnectorDetail({ projectId, connector, canWrite, onChanged });
|
|
58
|
-
// Opens the account list's "Add account" dialog: name and who may use it.
|
|
59
|
-
const [addRequest, setAddRequest] = useState(0);
|
|
60
|
-
const tSharing = useTranslations('accessSharing');
|
|
61
|
-
const isPipedream = connector.provider === 'pipedream';
|
|
62
|
-
const eventTriggers = useConnectorEventTriggers(projectId, connector);
|
|
63
|
-
const tabs = connectorTabs(connector, { canWrite, hasEvents: eventTriggers.hasEvents });
|
|
64
|
-
const [selectedTab, setSelectedTab] = useState('accounts');
|
|
65
|
-
const tab = tabs.includes(selectedTab) ? selectedTab : (tabs[0] ?? 'accounts');
|
|
66
|
-
// Best-effort catalogue description. Never blocks first paint — the header
|
|
67
|
-
// renders without it, then fills in. That is the main open-latency fix:
|
|
68
|
-
// previously this query sat in the critical path of feeling "ready".
|
|
69
|
-
const appDescriptionQuery = useQuery({
|
|
70
|
-
queryKey: ['connector-app-description', projectId, connector.slug, displayName],
|
|
71
|
-
queryFn: async () => {
|
|
72
|
-
const result = await listPipedreamApps(projectId, displayName);
|
|
73
|
-
const match = result.apps.find((app) => foldKey(app.slug) === foldKey(connector.slug) ||
|
|
74
|
-
foldKey(app.name) === foldKey(displayName));
|
|
75
|
-
return match?.description ?? null;
|
|
76
|
-
},
|
|
77
|
-
enabled: isPipedream,
|
|
78
|
-
staleTime: 5 * 60_000,
|
|
79
|
-
});
|
|
80
|
-
const appDescription = isPipedream ? (appDescriptionQuery.data ?? null) : null;
|
|
81
|
-
const addAccountMenu = isComputer ? (_jsxs(Button, { size: "sm", className: "gap-1.5", onClick: () => setComputerOpen(true), children: [_jsx(PlusIcon, { className: "size-4 shrink-0", weight: "bold" }), tI18nComplete.raw('text1a2303ede074')] })) : (_jsxs(Button, { size: "sm", className: "gap-1.5", onClick: () => {
|
|
82
|
-
setSelectedTab('accounts');
|
|
83
|
-
setAddRequest((n) => n + 1);
|
|
84
|
-
}, disabled: connectPending, children: [_jsx(PlusIcon, { className: "size-4 shrink-0", weight: "bold" }), tSharing('addAccount')] }));
|
|
85
|
-
return (_jsxs(_Fragment, { children: [_jsxs(ModalHeader, { className: "flex-row items-start gap-2.5 border-b pb-4", children: [_jsx("span", { className: "p-1", children: _jsx(ConnectorAppIcon, { connector: connector, size: "lg" }) }), _jsxs("div", { className: "min-w-0 flex-1 space-y-0", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-2 gap-y-1", children: [_jsx(ModalTitle, { className: "truncate text-lg font-semibold", children: displayName }), _jsx(ConnectorStatusBadge, { connector: connector })] }), appDescription ? (_jsx("p", { className: "text-muted-foreground text-sm text-pretty", children: appDescription })) : null] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(ButtonGroup, { className: "shrink-0", children: [headerCta === 'connect' ? addAccountMenu : null, headerCta === 'finish' ? (_jsxs(Button, { size: "sm", variant: "outline", className: "gap-1.5 active:scale-[0.96]", onClick: () => setSelectedTab('accounts'), children: [_jsx(KeyIcon, { className: "size-4 shrink-0" }), isManagedProvider
|
|
86
|
-
? tI18nComplete.raw('text1a2303ede074')
|
|
87
|
-
: tI18nComplete.raw('text3d6627454174')] })) : null, headerCta === 'replace' ? (_jsxs(Button, { size: "sm", variant: "outline", className: "gap-1.5 active:scale-[0.96]", onClick: replaceSoleAccount, disabled: connectPending, children: [isManagedProvider ? (connectPending ? (_jsx(Loading, { className: "size-4 shrink-0" })) : null) : (_jsx(KeyIcon, { className: "size-4 shrink-0" })), isManagedProvider
|
|
88
|
-
? tI18nComplete.raw('textbf8a9eab9e7e')
|
|
89
|
-
: tI18nComplete.raw('text54483ce856e0')] })) : null] }), _jsx(ModalClose, { asChild: true, children: _jsx(Button, { variant: "secondary", size: "icon", className: "size-8 shrink-0 rounded-md", "aria-label": tI18nComplete.raw('text7d9eb7acb13e'), children: _jsx(Close, { className: "text-foreground size-4 stroke-1" }) }) })] })] }), _jsxs(ModalBody, { className: "flex max-h-[70vh] flex-col overflow-hidden p-0", children: [!connected && !isChannel && !isComputer && connectionsQuery.isSuccess ? (_jsx(InfoBanner, { tone: "warning", title: tI18nComplete('text6506d9ea4341', { value0: displayName }), className: "shrink-0 rounded-none border-x-0 border-t-0", action: headerCta === 'connect' ? (addAccountMenu) : headerCta === 'finish' ? (_jsx(Button, { size: "sm", variant: "outline", onClick: () => setSelectedTab('accounts'), children: isManagedProvider
|
|
90
|
-
? tI18nComplete.raw('text1a2303ede074')
|
|
91
|
-
: tI18nComplete.raw('text3d6627454174') })) : undefined, children: canWrite
|
|
92
|
-
? tI18nComplete.raw('text929505ef815a')
|
|
93
|
-
: tI18nComplete.raw('text6a05ddf8cca1') })) : null, _jsxs(Tabs, { value: tab, onValueChange: (next) => setSelectedTab(next), className: "flex min-h-0 flex-1 flex-col gap-0 overflow-y-auto lg:flex-row lg:overflow-hidden", children: [_jsx(TabsList, { type: "underline", underlineSize: "md", size: "sm", "aria-label": `${displayName} sections`, className: cn('h-auto w-full shrink-0 justify-start gap-1 rounded-none px-2', 'overflow-x-auto', 'lg:border-border lg:h-full lg:w-64 lg:flex-col lg:items-stretch lg:gap-0.5', 'lg:overflow-x-visible lg:overflow-y-auto lg:border-r lg:border-b-0 lg:p-3', 'lg:**:data-[slot=tabs-trigger]:after:hidden', 'lg:**:data-[slot=tabs-trigger]:h-auto lg:**:data-[slot=tabs-trigger]:w-full', 'lg:**:data-[slot=tabs-trigger]:justify-between lg:**:data-[slot=tabs-trigger]:rounded-md', 'lg:**:data-[slot=tabs-trigger]:px-3 lg:**:data-[slot=tabs-trigger]:py-2', 'lg:**:data-[slot=tabs-trigger]:data-[state=active]:bg-primary/6', 'lg:**:data-[slot=tabs-trigger]:data-[state=active]:font-medium', 'lg:**:data-[slot=tabs-trigger]:data-[state=inactive]:hover:bg-primary/3'), children: tabs.map((value) => (_jsxs(TabsTrigger, { value: value, className: "w-fit flex-none gap-2 px-3 py-2.5 active:scale-[0.98] lg:w-full", children: [tI18nComplete.raw(CONNECTOR_TAB_LABEL_KEY[value]), value === 'triggers' ? (_jsx(Badge, { variant: "secondary", size: "sm", children: eventTriggers.rows.length })) : null] }, value))) }), _jsx("div", { className: "bg-popover min-w-0 flex-1 overflow-hidden overflow-y-auto px-5 py-4 lg:px-6", children: _jsx(ConnectorDetailTabs, { connectionsQuery: connectionsQuery, accounts: {
|
|
94
|
-
projectId,
|
|
95
|
-
connector,
|
|
96
|
-
displayName,
|
|
97
|
-
canWrite,
|
|
98
|
-
canManageConnections,
|
|
99
|
-
onChanged,
|
|
100
|
-
onStartSession: startPrivateSession,
|
|
101
|
-
addRequest,
|
|
102
|
-
onAddRequestHandled: () => setAddRequest(0),
|
|
103
|
-
onRemoved,
|
|
104
|
-
}, settings: {
|
|
105
|
-
projectId: projectId,
|
|
106
|
-
connector: connector,
|
|
107
|
-
displayName: displayName,
|
|
108
|
-
onChanged: onChanged,
|
|
109
|
-
onRemoved: onRemoved,
|
|
110
|
-
} }) })] })] }), _jsx(SetCredentialModal, { projectId: projectId, connector: credentialTarget ? connector : null, connectionId: credentialTarget?.connectionId ?? null, owner: credentialTarget?.owner ?? 'me', open: credentialTarget !== null, onOpenChange: (open) => {
|
|
111
|
-
if (!open)
|
|
112
|
-
setCredentialTarget(null);
|
|
113
|
-
}, onSaved: () => {
|
|
114
|
-
setCredentialTarget(null);
|
|
115
|
-
refreshAccounts();
|
|
116
|
-
} }), isComputer ? (_jsx(ComputerConnectModal, { projectId: projectId, open: computerOpen, onOpenChange: setComputerOpen, onConnected: refreshAccounts })) : null] }));
|
|
33
|
+
return (_jsxs("div", { className: MODAL_COLUMN, "aria-busy": true, children: [_jsx(VisuallyHidden, { children: _jsx(ModalTitle, { children: tI18nComplete.raw('text0c21b0363778') }) }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Skeleton, { className: "size-9 rounded-md" }), _jsx(Skeleton, { className: "h-6 w-48 rounded-sm" })] }), _jsx(Skeleton, { className: "h-16 w-full rounded-md" }), _jsx(Skeleton, { className: "h-9 w-64 rounded-md" }), _jsx(Skeleton, { className: "h-20 w-full rounded-md" })] }));
|
|
117
34
|
}
|
|
@@ -72,8 +72,12 @@ export function noticeButton(notice, target, managed) {
|
|
|
72
72
|
}
|
|
73
73
|
/** A failure line split for the log row: `MCP tools/list failed: HTTP 401` → `401` + `MCP tools/list failed`. */
|
|
74
74
|
export function splitNoticeReason(reason) {
|
|
75
|
-
const match =
|
|
75
|
+
const match = /\bHTTP (\d{3})\b/.exec(reason);
|
|
76
76
|
if (!match)
|
|
77
77
|
return { code: null, detail: reason };
|
|
78
|
-
|
|
78
|
+
// Drop the `: ` before the status with a loop: a `[:\s]*` regex prefix is quadratic on long whitespace runs.
|
|
79
|
+
let end = match.index;
|
|
80
|
+
while (end > 0 && /[:\s]/.test(reason[end - 1]))
|
|
81
|
+
end--;
|
|
82
|
+
return { code: match[1], detail: (reason.slice(0, end) + reason.slice(match.index + match[0].length)).trim() };
|
|
79
83
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type AdminConnector } from '@kortix/sdk';
|
|
2
|
+
import { type ReactNode } from 'react';
|
|
1
3
|
/** The column `CapabilityPageShell` gives the list page beside this one. */
|
|
2
4
|
/**
|
|
3
5
|
* `/projects/<id>/customize/connectors/<app>/<connector>` — one connector, as
|
|
@@ -12,6 +14,34 @@ export declare function ConnectorPage({ projectId, appSegment, connectorSlug, }:
|
|
|
12
14
|
appSegment: string;
|
|
13
15
|
connectorSlug: string;
|
|
14
16
|
}): import("react").JSX.Element;
|
|
17
|
+
export interface ConnectorDetailViewProps {
|
|
18
|
+
projectId: string;
|
|
19
|
+
connector: AdminConnector;
|
|
20
|
+
canWrite: boolean;
|
|
21
|
+
onChanged: () => void;
|
|
22
|
+
/** The connector was deleted: refresh everything but its own queries. */
|
|
23
|
+
onRemoved: () => void;
|
|
24
|
+
/** Above the header: the page's Back link. */
|
|
25
|
+
back?: ReactNode;
|
|
26
|
+
/** At the header's right edge, after its actions: the modal's close button. */
|
|
27
|
+
headerEnd?: ReactNode;
|
|
28
|
+
/** Wraps the `h1`: the modal passes `ModalTitle asChild` for its accessible name. */
|
|
29
|
+
wrapTitle?: (title: ReactNode) => ReactNode;
|
|
30
|
+
/** The view's own connector is gone: leave it (the page routes back, the
|
|
31
|
+
* modal closes). */
|
|
32
|
+
onLeave: () => void;
|
|
33
|
+
/** The tab and the install hand-offs live in the URL on the page. The modal
|
|
34
|
+
* keeps its tab in state, so it never rewrites its host page's URL. */
|
|
35
|
+
urlState?: boolean;
|
|
36
|
+
/** The scroll column's classes; the modal uses tighter padding. */
|
|
37
|
+
columnClassName?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* One connector — the strip, its notice, and the Accounts / Tools / Settings
|
|
41
|
+
* tabs. The page (`/connectors/<app>/<connector>`) and the agent editor's
|
|
42
|
+
* connector modal render this same view, so the two cannot drift.
|
|
43
|
+
*/
|
|
44
|
+
export declare function ConnectorDetailView({ projectId, connector, canWrite, onChanged, onRemoved, back, headerEnd, wrapTitle, onLeave, urlState, columnClassName, }: ConnectorDetailViewProps): import("react").JSX.Element;
|
|
15
45
|
/**
|
|
16
46
|
* The strip's bottom row when something is wrong: a one-line title, the raw
|
|
17
47
|
* failure as a quiet log line, and the buttons that fix it. No icon tile; the
|
|
@@ -99,7 +99,17 @@ export function ConnectorPage({ projectId, appSegment, connectorSlug, }) {
|
|
|
99
99
|
}
|
|
100
100
|
return (_jsx(ConnectorPageBody, { projectId: projectId, connector: connector, canWrite: canWrite, backHref: backHref, onChanged: invalidate, onRemoved: invalidateAfterRemove }, connector.slug));
|
|
101
101
|
}
|
|
102
|
-
function ConnectorPageBody({
|
|
102
|
+
function ConnectorPageBody({ backHref, ...props }) {
|
|
103
|
+
const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
|
|
104
|
+
const router = useUiRouter();
|
|
105
|
+
return (_jsx(ConnectorDetailView, { ...props, back: _jsx(Button, { asChild: true, variant: "text", size: "sm", className: "-ml-1 w-fit px-0 has-[>svg]:px-0", children: _jsxs(Link, { href: backHref, children: [_jsx(CaretLeftIcon, { className: "size-3.5 shrink-0" }), tI18nComplete.raw('textc3d2e79ebdd0')] }) }), onLeave: () => router.push(connectorsHref(props.projectId, 'connected')) }));
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* One connector — the strip, its notice, and the Accounts / Tools / Settings
|
|
109
|
+
* tabs. The page (`/connectors/<app>/<connector>`) and the agent editor's
|
|
110
|
+
* connector modal render this same view, so the two cannot drift.
|
|
111
|
+
*/
|
|
112
|
+
export function ConnectorDetailView({ projectId, connector, canWrite, onChanged, onRemoved, back, headerEnd, wrapTitle = (title) => title, onLeave, urlState = true, columnClassName = PAGE_COLUMN, }) {
|
|
103
113
|
const t = useTranslations('connectorPages');
|
|
104
114
|
const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
|
|
105
115
|
const tSharing = useTranslations('accessSharing');
|
|
@@ -116,19 +126,22 @@ function ConnectorPageBody({ projectId, connector, canWrite, backHref, onChanged
|
|
|
116
126
|
// The tab is in the URL, so a link opens the right one.
|
|
117
127
|
const eventTriggers = useConnectorEventTriggers(projectId, connector);
|
|
118
128
|
const tabs = connectorTabs(connector, { canWrite, hasEvents: eventTriggers.hasEvents });
|
|
119
|
-
const
|
|
129
|
+
const [localTab, setLocalTab] = useState('accounts');
|
|
130
|
+
const requestedTab = urlState ? search?.get('tab') : localTab;
|
|
120
131
|
const tab = tabs.find((value) => value === requestedTab) ?? tabs[0] ?? 'accounts';
|
|
121
|
-
const setTab = (next) =>
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
132
|
+
const setTab = (next) => urlState
|
|
133
|
+
? replaceParams((params) => {
|
|
134
|
+
if (next === 'accounts')
|
|
135
|
+
params.delete('tab');
|
|
136
|
+
else
|
|
137
|
+
params.set('tab', next);
|
|
138
|
+
})
|
|
139
|
+
: setLocalTab(next);
|
|
127
140
|
// The install hand-off: `?connect=<connection id>` opens credential entry
|
|
128
141
|
// for that account once, then leaves the URL. The id is untrusted, so
|
|
129
142
|
// `connectHandoff` opens only one of THIS connector's accounts and takes the
|
|
130
143
|
// owner from the row; the URL's `owner` param is stripped, never read.
|
|
131
|
-
const connectId = search?.get('connect') ?? null;
|
|
144
|
+
const connectId = urlState ? (search?.get('connect') ?? null) : null;
|
|
132
145
|
// Same cache entry as the probe in `useConnectorDetail`; read here for its
|
|
133
146
|
// loading state, so a shared account is not refused before the answer lands.
|
|
134
147
|
const accountId = useProjectAccountId(projectId);
|
|
@@ -195,7 +208,7 @@ function ConnectorPageBody({ projectId, connector, canWrite, backHref, onChanged
|
|
|
195
208
|
};
|
|
196
209
|
// Right after Install (`?add=1`): ask for the first account once, then
|
|
197
210
|
// leave the URL. Adjusted during render, not in an effect.
|
|
198
|
-
const addParam = search?.get('add') === '1';
|
|
211
|
+
const addParam = urlState && search?.get('add') === '1';
|
|
199
212
|
const [addHandled, setAddHandled] = useState(false);
|
|
200
213
|
if (addParam && !addHandled && canWrite && !isChannel) {
|
|
201
214
|
setAddHandled(true);
|
|
@@ -301,7 +314,7 @@ function ConnectorPageBody({ projectId, connector, canWrite, backHref, onChanged
|
|
|
301
314
|
setCredentialTarget(null);
|
|
302
315
|
};
|
|
303
316
|
const addAccountControl = isComputer ? (_jsxs(Button, { size: "sm", className: "gap-1.5", onClick: () => setComputerOpen(true), children: [_jsx(PlusIcon, { className: "size-4 shrink-0" }), tI18nComplete.raw('text1a2303ede074')] })) : (_jsxs(Button, { size: "sm", className: "gap-1.5", onClick: requestAdd, disabled: connectPending, children: [_jsx(PlusIcon, { className: "size-4 shrink-0" }), tSharing('addAccount')] }));
|
|
304
|
-
return (_jsxs(SplitSheet, { open: sheetOpen, onOpenChange: closeCredential, size: "lg", className: "flex-1", children: [_jsx(SplitSheetMain, { children: _jsxs("div", { className:
|
|
317
|
+
return (_jsxs(SplitSheet, { open: sheetOpen, onOpenChange: closeCredential, size: "lg", className: "flex-1", children: [_jsx(SplitSheetMain, { children: _jsxs("div", { className: columnClassName, children: [back, _jsxs("header", { className: "flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-3", children: [_jsx(ConnectorAppIcon, { connector: connector, size: "lg" }), wrapTitle(_jsx("h1", { ref: headingRef, tabIndex: -1, className: "text-foreground min-w-0 text-xl font-medium text-balance wrap-break-word", children: displayName }))] }), _jsxs("div", { className: "flex shrink-0 flex-wrap items-center gap-2", children: [isComputer && headerCta === 'connect' ? addAccountControl : null, connected ? (_jsx(Button, { size: "sm", variant: "outline", onClick: () => startPrivateSession(), children: t('newSession') })) : null, headerEnd] })] }), _jsxs("div", { className: "bg-popover divide-border divide-y overflow-hidden rounded-md border", children: [_jsx("dl", { className: "divide-border flex divide-x", children: stripCells.map((cell) => (_jsxs("div", { className: "min-w-0 flex-1 space-y-0.5 px-4 py-2.5", children: [_jsxs("dt", { className: "text-muted-foreground flex h-4 items-center justify-between gap-2 text-xs", children: [_jsx("span", { className: "truncate", children: cell.label }), cell.labelAddon] }), _jsx("dd", { className: "text-foreground flex min-w-0 items-center gap-1.5 truncate text-sm font-medium", children: cell.value })] }, cell.key))) }), notice.kind !== 'none' ? (_jsxs(ConnectorNoticeRow, { title: t(NOTICE_TITLE[notice.kind], { name: displayName }), detail: notice.kind === 'error' && notice.reason ? (_jsx(NoticeLogLine, { reason: notice.reason })) : (_jsx("p", { className: "text-muted-foreground text-xs text-pretty", children: t(notice.kind === 'needs_credential' && isManagedProvider
|
|
305
318
|
? 'noticeReconnectBody'
|
|
306
319
|
: NOTICE_BODY[notice.kind], { name: displayName }) })), children: [notice.kind === 'error' && canWrite ? (_jsxs(Button, { size: "sm", variant: "ghost", className: "gap-1.5", onClick: () => retry.mutate(), disabled: retry.isPending, children: [retry.isPending ? _jsx(Loading, { className: "size-4 shrink-0" }) : null, tI18nComplete.raw('text942087cc2d41')] })) : null, fix?.kind === 'add_account' ? (addAccountControl) : fix?.kind === 'sign_in' ? (_jsxs(Button, { size: "sm", className: "gap-1.5", onClick: runFix, disabled: signIn.isPending, children: [signIn.isPending ? _jsx(SessionDotMatrix, { size: 14, className: "shrink-0" }) : null, t('noticeSignInAction')] })) : fix ? (
|
|
307
320
|
// Fix it here: credential entry for a direct account, the
|
|
@@ -321,7 +334,7 @@ function ConnectorPageBody({ projectId, connector, canWrite, backHref, onChanged
|
|
|
321
334
|
onStartSession: startPrivateSession,
|
|
322
335
|
addRequest,
|
|
323
336
|
onAddRequestHandled: () => setAddRequest(0),
|
|
324
|
-
onRemoved:
|
|
337
|
+
onRemoved: onLeave,
|
|
325
338
|
onSetCredential: setCredentialTarget,
|
|
326
339
|
onConnect: needsSignIn ? connectAccount : undefined,
|
|
327
340
|
showAccountInfo: true,
|
|
@@ -331,7 +344,7 @@ function ConnectorPageBody({ projectId, connector, canWrite, backHref, onChanged
|
|
|
331
344
|
displayName: displayName,
|
|
332
345
|
onChanged: onChanged,
|
|
333
346
|
onRemoved: () => {
|
|
334
|
-
|
|
347
|
+
onLeave();
|
|
335
348
|
onRemoved();
|
|
336
349
|
},
|
|
337
350
|
} })] })] }) }), credentialTarget ? (
|
|
@@ -6,6 +6,7 @@ import { InfoBanner } from '../../../primitives/info-banner.js';
|
|
|
6
6
|
import { Input } from '../../../primitives/input.js';
|
|
7
7
|
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, } from '../../../primitives/input-group.js';
|
|
8
8
|
import Loading from '../../../primitives/loading.js';
|
|
9
|
+
import { Switch } from '../../../primitives/switch.js';
|
|
9
10
|
import { successToast } from '../../../primitives/toast.js';
|
|
10
11
|
import { useTranslations } from '../../../i18n/index.js';
|
|
11
12
|
import { upsertProjectSecret } from '@kortix/sdk';
|
|
@@ -52,7 +53,12 @@ export function CustomProviderForm({ projectId, initialId = '', onSaved, }) {
|
|
|
52
53
|
const name = form.name.trim();
|
|
53
54
|
const baseUrl = form.baseURL.trim();
|
|
54
55
|
const apiKey = form.apiKey.trim();
|
|
55
|
-
const model = {
|
|
56
|
+
const model = {
|
|
57
|
+
id: form.modelId.trim(),
|
|
58
|
+
name: form.modelName.trim(),
|
|
59
|
+
...(form.attachment !== undefined ? { attachment: form.attachment } : {}),
|
|
60
|
+
...(form.reasoning !== undefined ? { reasoning: form.reasoning } : {}),
|
|
61
|
+
};
|
|
56
62
|
if (!providerId || !name || !baseUrl) {
|
|
57
63
|
throw new Error('Provider ID, name, and base URL are required');
|
|
58
64
|
}
|
|
@@ -69,11 +75,7 @@ export function CustomProviderForm({ projectId, initialId = '', onSaved, }) {
|
|
|
69
75
|
throw new Error('Model ID cannot contain spaces');
|
|
70
76
|
}
|
|
71
77
|
const existing = custom.providers.find((p) => p.id === providerId && p.configured);
|
|
72
|
-
|
|
73
|
-
name,
|
|
74
|
-
base_url: baseUrl,
|
|
75
|
-
models: [...(existing?.models ?? []).filter((m) => m.id !== model.id), model],
|
|
76
|
-
});
|
|
78
|
+
// The key first: saving the endpoint reads its `GET /models` for each model's limits.
|
|
77
79
|
if (apiKey) {
|
|
78
80
|
// LLM provider credentials are always project-wide (see
|
|
79
81
|
// api-key-connect-form.tsx) — a per-user key is invisible to the
|
|
@@ -85,6 +87,21 @@ export function CustomProviderForm({ projectId, initialId = '', onSaved, }) {
|
|
|
85
87
|
consumer: 'llm_gateway',
|
|
86
88
|
});
|
|
87
89
|
}
|
|
90
|
+
await custom.save(providerId, {
|
|
91
|
+
name,
|
|
92
|
+
base_url: baseUrl,
|
|
93
|
+
models: [
|
|
94
|
+
...(existing?.models ?? [])
|
|
95
|
+
.filter((m) => m.id !== model.id)
|
|
96
|
+
.map(({ id, name, attachment, reasoning }) => ({
|
|
97
|
+
id,
|
|
98
|
+
name,
|
|
99
|
+
...(attachment !== undefined ? { attachment } : {}),
|
|
100
|
+
...(reasoning !== undefined ? { reasoning } : {}),
|
|
101
|
+
})),
|
|
102
|
+
model,
|
|
103
|
+
],
|
|
104
|
+
});
|
|
88
105
|
return providerId;
|
|
89
106
|
},
|
|
90
107
|
onSuccess: (providerId) => {
|
|
@@ -110,5 +127,5 @@ export function CustomProviderForm({ projectId, initialId = '', onSaved, }) {
|
|
|
110
127
|
// app — a pasted key used to sit in plaintext on screen.
|
|
111
128
|
type: revealKey ? 'text' : 'password', autoComplete: "off", spellCheck: false, "data-1p-ignore": "", "data-lpignore": "true", "data-form-type": "other", value: form.apiKey, onChange: (e) => setField('apiKey', e.target.value), placeholder: tI18nComplete.raw('textb009c6c5299f'), className: "font-mono font-normal" }), form.apiKey ? (_jsx(InputGroupAddon, { align: "inline-end", children: _jsx(InputGroupButton, { size: "icon-xs", onClick: () => setRevealKey((current) => !current), title: revealKey ? 'Hide' : 'Show', "aria-label": revealKey
|
|
112
129
|
? tI18nComplete.raw('text3d0d8c59beb7')
|
|
113
|
-
: tI18nComplete.raw('textcb0b50a0d111'), "aria-pressed": revealKey, className: "text-muted-foreground hover:text-foreground", children: revealKey ? _jsx(EyeSlash, { className: "size-3.5" }) : _jsx(Eye, { className: "size-3.5" }) }) })) : null] }), form.providerId.trim() ? (_jsx("code", { className: "text-muted-foreground font-mono text-xs break-all", children: customProviderSecretName(form.providerId) })) : null] }), _jsxs("div", { className: "flex gap-3", children: [_jsx(FormField, { htmlFor: "custom-model-id", label: tI18nComplete.raw('text089ef2b6dbb3'), children: _jsx(Input, { id: "custom-model-id", type: "text", value: form.modelId, onChange: (e) => setField('modelId', e.target.value), placeholder: tI18nComplete.raw('text34d316295094'), className: "font-mono font-normal" }) }), _jsx(FormField, { htmlFor: "custom-model-name", label: tI18nComplete.raw('texta3c3370cc732'), children: _jsx(Input, { id: "custom-model-name", type: "text", value: form.modelName, onChange: (e) => setField('modelName', e.target.value), placeholder: tI18nComplete.raw('textb70354a1faae'), className: "font-normal" }) })] }), error ? (_jsx(InfoBanner, { tone: "destructive", icon: TriangleAlert, title: tI18nComplete.raw('text464de08899db'), children: error })) : null, _jsx("div", { className: "flex gap-2 pt-1", children: _jsx(Button, { type: "submit", size: "sm", disabled: save.isPending, className: "gap-1.5", children: save.isPending ? (_jsxs(_Fragment, { children: [_jsx(Loading, { className: "size-3.5 shrink-0" }), t('saving')] })) : (t('saveProvider')) }) })] }) }) }));
|
|
130
|
+
: tI18nComplete.raw('textcb0b50a0d111'), "aria-pressed": revealKey, className: "text-muted-foreground hover:text-foreground", children: revealKey ? _jsx(EyeSlash, { className: "size-3.5" }) : _jsx(Eye, { className: "size-3.5" }) }) })) : null] }), form.providerId.trim() ? (_jsx("code", { className: "text-muted-foreground font-mono text-xs break-all", children: customProviderSecretName(form.providerId) })) : null] }), _jsxs("div", { className: "flex gap-3", children: [_jsx(FormField, { htmlFor: "custom-model-id", label: tI18nComplete.raw('text089ef2b6dbb3'), children: _jsx(Input, { id: "custom-model-id", type: "text", value: form.modelId, onChange: (e) => setField('modelId', e.target.value), placeholder: tI18nComplete.raw('text34d316295094'), className: "font-mono font-normal" }) }), _jsx(FormField, { htmlFor: "custom-model-name", label: tI18nComplete.raw('texta3c3370cc732'), children: _jsx(Input, { id: "custom-model-name", type: "text", value: form.modelName, onChange: (e) => setField('modelName', e.target.value), placeholder: tI18nComplete.raw('textb70354a1faae'), className: "font-normal" }) })] }), _jsxs(Field, { className: "gap-2", children: [_jsxs("div", { className: "text-foreground flex flex-wrap items-center gap-x-5 gap-y-2 text-sm", children: [_jsxs("label", { className: "flex items-center gap-2", children: [_jsx(Switch, { checked: form.attachment === true, onCheckedChange: (attachment) => setField('attachment', attachment) }), t('readsImages')] }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx(Switch, { checked: form.reasoning === true, onCheckedChange: (reasoning) => setField('reasoning', reasoning) }), t('reasoning')] })] }), _jsx(FieldDescription, { children: t('traitsHint') })] }), error ? (_jsx(InfoBanner, { tone: "destructive", icon: TriangleAlert, title: tI18nComplete.raw('text464de08899db'), children: error })) : null, _jsx("div", { className: "flex gap-2 pt-1", children: _jsx(Button, { type: "submit", size: "sm", disabled: save.isPending, className: "gap-1.5", children: save.isPending ? (_jsxs(_Fragment, { children: [_jsx(Loading, { className: "size-3.5 shrink-0" }), t('saving')] })) : (t('saveProvider')) }) })] }) }) }));
|
|
114
131
|
}
|
|
@@ -36,6 +36,7 @@ import { Input } from '../../../primitives/input.js';
|
|
|
36
36
|
import { InputGroup, InputGroupInput } from '../../../primitives/input-group.js';
|
|
37
37
|
import Loading from '../../../primitives/loading.js';
|
|
38
38
|
import { Skeleton } from '../../../primitives/skeleton.js';
|
|
39
|
+
import { Switch } from '../../../primitives/switch.js';
|
|
39
40
|
import { SplitSheetBody, SplitSheetDescription, SplitSheetFooter, SplitSheetHeader, SplitSheetTitle, } from '../../../primitives/split-sheet.js';
|
|
40
41
|
import { errorToast, successToast } from '../../../primitives/toast.js';
|
|
41
42
|
import { ProviderLogoStatus } from '../../providers/provider-connect.js';
|
|
@@ -46,6 +47,7 @@ import { CaretRightIcon, XIcon } from '@phosphor-icons/react';
|
|
|
46
47
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
47
48
|
import { useModelsPageSheet } from '../models-page-sheet.js';
|
|
48
49
|
import { CustomProviderForm } from './custom-provider-form.js';
|
|
50
|
+
import { formatTokenCount } from './utils.js';
|
|
49
51
|
/** One saved custom provider: a configured row opens the sheet, a legacy row
|
|
50
52
|
* pre-fills the form. */
|
|
51
53
|
export function CustomProviderRow({ provider, selected, onSelect, }) {
|
|
@@ -57,6 +59,22 @@ export function CustomProviderRow({ provider, selected, onSelect, }) {
|
|
|
57
59
|
function DetailRow({ label, value, mono = true }) {
|
|
58
60
|
return (_jsxs("div", { className: "flex min-h-10 items-center justify-between gap-4 py-2", children: [_jsx("span", { className: "text-muted-foreground shrink-0 text-sm", children: label }), _jsx("span", { className: cn('text-foreground min-w-0 truncate', mono ? 'font-mono text-xs' : 'text-sm'), children: value })] }));
|
|
59
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* A model as the PUT takes it: the switches travel, the limits do not, so
|
|
64
|
+
* every save reads the window again from the endpoint's `GET /models`.
|
|
65
|
+
*/
|
|
66
|
+
const modelInput = ({ id, name, attachment, reasoning }) => ({
|
|
67
|
+
id,
|
|
68
|
+
name,
|
|
69
|
+
...(attachment !== undefined ? { attachment } : {}),
|
|
70
|
+
...(reasoning !== undefined ? { reasoning } : {}),
|
|
71
|
+
});
|
|
72
|
+
/** One saved model: its window, and the image and reasoning switches (each saves at once). */
|
|
73
|
+
function ModelRow({ model, disabled, onToggle, }) {
|
|
74
|
+
const t = useTranslations('customProvider');
|
|
75
|
+
const context = formatTokenCount(model.context);
|
|
76
|
+
return (_jsxs("div", { className: "space-y-2 py-2", children: [_jsxs("div", { className: "flex min-h-6 items-center justify-between gap-4", children: [_jsx("span", { className: "text-muted-foreground shrink-0 text-sm", children: model.name }), _jsx("span", { className: "text-foreground min-w-0 truncate font-mono text-xs", children: model.id })] }), _jsxs("div", { className: "text-muted-foreground flex flex-wrap items-center gap-x-5 gap-y-2 text-xs", children: [_jsx("span", { className: "tabular-nums", children: context ? t('contextWindow', { tokens: context }) : t('contextDefault') }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx(Switch, { checked: model.attachment === true, disabled: disabled, onCheckedChange: (attachment) => onToggle({ attachment }) }), t('readsImages')] }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx(Switch, { checked: model.reasoning === true, disabled: disabled, onCheckedChange: (reasoning) => onToggle({ reasoning }) }), t('reasoning')] })] })] }));
|
|
77
|
+
}
|
|
60
78
|
/**
|
|
61
79
|
* The endpoint editor in the sheet. The PUT replaces the provider's whole
|
|
62
80
|
* definition, so this edits the full model list in place.
|
|
@@ -72,7 +90,7 @@ function EndpointEditor({ projectId, provider, onClose, }) {
|
|
|
72
90
|
const save = useMutation({
|
|
73
91
|
mutationFn: () => {
|
|
74
92
|
const trimmed = models
|
|
75
|
-
.map((m) => ({ id: m.id.trim(), name: m.name.trim() }))
|
|
93
|
+
.map((m) => modelInput({ ...m, id: m.id.trim(), name: m.name.trim() }))
|
|
76
94
|
.filter((m) => m.id || m.name);
|
|
77
95
|
if (!name.trim() || !/^https?:\/\//.test(baseUrl.trim())) {
|
|
78
96
|
throw new Error('Name and an http:// or https:// base URL are required');
|
|
@@ -108,12 +126,22 @@ function CustomProviderDetail({ projectId, provider, onDeleted, }) {
|
|
|
108
126
|
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
109
127
|
const [value, setValue] = useState('');
|
|
110
128
|
const replace = useMutation({
|
|
111
|
-
mutationFn: () =>
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
129
|
+
mutationFn: async () => {
|
|
130
|
+
await upsertProjectSecret(projectId, {
|
|
131
|
+
name: provider.secret_name,
|
|
132
|
+
value: value.trim(),
|
|
133
|
+
strategy: 'broker',
|
|
134
|
+
consumer: 'llm_gateway',
|
|
135
|
+
});
|
|
136
|
+
// Re-save the endpoint: it reads the model limits from `GET /models` with the new key.
|
|
137
|
+
if (provider.configured && provider.base_url) {
|
|
138
|
+
await custom.save(provider.id, {
|
|
139
|
+
name: provider.name,
|
|
140
|
+
base_url: provider.base_url,
|
|
141
|
+
models: provider.models.map(modelInput),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
},
|
|
117
145
|
onSuccess: () => {
|
|
118
146
|
successToast(t('keyReplaced'));
|
|
119
147
|
setValue('');
|
|
@@ -124,6 +152,14 @@ function CustomProviderDetail({ projectId, provider, onDeleted, }) {
|
|
|
124
152
|
},
|
|
125
153
|
onError: (err) => errorToast(err instanceof Error ? err.message : tI18nComplete.raw('text2c07997249ab')),
|
|
126
154
|
});
|
|
155
|
+
const toggle = useMutation({
|
|
156
|
+
mutationFn: ({ id, patch }) => custom.save(provider.id, {
|
|
157
|
+
name: provider.name,
|
|
158
|
+
base_url: provider.base_url ?? '',
|
|
159
|
+
models: provider.models.map((m) => modelInput(m.id === id ? { ...m, ...patch } : m)),
|
|
160
|
+
}),
|
|
161
|
+
onError: (err) => errorToast(err instanceof Error ? err.message : t('saveFailed')),
|
|
162
|
+
});
|
|
127
163
|
const remove = useMutation({
|
|
128
164
|
mutationFn: () => custom.remove(provider.id),
|
|
129
165
|
onSuccess: () => {
|
|
@@ -139,7 +175,7 @@ function CustomProviderDetail({ projectId, provider, onDeleted, }) {
|
|
|
139
175
|
if (value.trim() && !replace.isPending)
|
|
140
176
|
replace.mutate();
|
|
141
177
|
};
|
|
142
|
-
return (_jsxs(_Fragment, { children: [_jsx(SplitSheetHeader, { closeLabel: tSheet('close'), className: "px-5 py-4", children: _jsxs("div", { className: "flex min-w-0 items-center gap-3", children: [_jsx(ProviderLogoStatus, { providerId: provider.id, label: provider.name, status: "connected", size: "large" }), _jsxs("div", { className: "min-w-0 space-y-0.5", children: [_jsx(SplitSheetTitle, { className: "truncate text-base font-semibold", children: provider.name }), _jsx(SplitSheetDescription, { className: "font-mono", children: provider.id })] })] }) }), _jsxs(SplitSheetBody, { className: "space-y-7", children: [_jsxs("section", { className: "space-y-1", children: [_jsx("h3", { className: "text-foreground text-sm font-medium", children: t('endpoint') }), editing ? (_jsx(EndpointEditor, { projectId: projectId, provider: provider, onClose: () => setEditing(false) })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "divide-border divide-y", children: [_jsx(DetailRow, { label: tI18nComplete.raw('text70589413a3c9'), value: provider.base_url ?? '' }), _jsx(DetailRow, { label: tI18nComplete.raw('text16f0ee47f993'), value: provider.has_key ? provider.secret_name : t('noKey'), mono: provider.has_key })] }), _jsx("h3", { className: "text-foreground pt-4 text-sm font-medium", children: t('models') }), _jsx("div", { className: "divide-border divide-y", children: provider.models.map((model) => (_jsx(
|
|
178
|
+
return (_jsxs(_Fragment, { children: [_jsx(SplitSheetHeader, { closeLabel: tSheet('close'), className: "px-5 py-4", children: _jsxs("div", { className: "flex min-w-0 items-center gap-3", children: [_jsx(ProviderLogoStatus, { providerId: provider.id, label: provider.name, status: "connected", size: "large" }), _jsxs("div", { className: "min-w-0 space-y-0.5", children: [_jsx(SplitSheetTitle, { className: "truncate text-base font-semibold", children: provider.name }), _jsx(SplitSheetDescription, { className: "font-mono", children: provider.id })] })] }) }), _jsxs(SplitSheetBody, { className: "space-y-7", children: [_jsxs("section", { className: "space-y-1", children: [_jsx("h3", { className: "text-foreground text-sm font-medium", children: t('endpoint') }), editing ? (_jsx(EndpointEditor, { projectId: projectId, provider: provider, onClose: () => setEditing(false) })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "divide-border divide-y", children: [_jsx(DetailRow, { label: tI18nComplete.raw('text70589413a3c9'), value: provider.base_url ?? '' }), _jsx(DetailRow, { label: tI18nComplete.raw('text16f0ee47f993'), value: provider.has_key ? provider.secret_name : t('noKey'), mono: provider.has_key })] }), _jsx("h3", { className: "text-foreground pt-4 text-sm font-medium", children: t('models') }), _jsx("div", { className: "divide-border divide-y", children: provider.models.map((model) => (_jsx(ModelRow, { model: model, disabled: toggle.isPending || !provider.base_url, onToggle: (patch) => toggle.mutate({ id: model.id, patch }) }, model.id))) }), _jsx(Button, { type: "button", size: "sm", variant: "secondary", className: "mt-3", onClick: () => setEditing(true), children: t('editEndpoint') })] }))] }), _jsx("section", { className: "space-y-1", children: replacing ? (_jsxs("form", { onSubmit: submit, className: "space-y-3", "data-custom-provider-replace": "", children: [_jsx("label", { htmlFor: `custom-replace-${provider.id}`, className: "sr-only", children: tI18nComplete.raw('text16f0ee47f993') }), _jsx(InputGroup, { children: _jsx(InputGroupInput, { id: `custom-replace-${provider.id}`, type: "password", autoComplete: "off", spellCheck: false, autoFocus: true, "data-1p-ignore": "", "data-lpignore": "true", "data-form-type": "other", value: value, onChange: (event) => setValue(event.target.value), placeholder: tI18nComplete.raw('textb009c6c5299f'), className: "font-mono" }) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Button, { type: "submit", size: "sm", disabled: !value.trim() || replace.isPending, className: "gap-1.5", children: [replace.isPending ? _jsx(Loading, { className: "size-3.5 shrink-0" }) : null, tSheet('saveKey')] }), _jsx(Button, { type: "button", size: "sm", variant: "ghost", disabled: replace.isPending, onClick: () => {
|
|
143
179
|
setValue('');
|
|
144
180
|
setReplacing(false);
|
|
145
181
|
}, children: tSheet('cancel') })] })] })) : (_jsx(Button, { type: "button", size: "sm", variant: "secondary", onClick: () => setReplacing(true), children: provider.has_key ? t('replaceKey') : t('addKey') })) }), _jsx("p", { className: "text-muted-foreground text-xs text-pretty", children: t('gatewayNote') })] }), _jsxs(SplitSheetFooter, { className: "justify-between", children: [_jsx(Button, { type: "button", size: "sm", variant: "ghost", className: "text-destructive hover:text-destructive -ml-2", onClick: () => setConfirmDelete(true), children: t('deleteProvider') }), _jsx("span", { className: "text-muted-foreground text-xs", children: tSheet('storedEncrypted') })] }), _jsx(ConfirmDialog, { open: confirmDelete, onOpenChange: setConfirmDelete, title: t('deleteTitle'), description: t('deleteBody', { name: provider.name }), confirmLabel: t('deleteProvider'), confirmVariant: "destructive", isPending: remove.isPending, onConfirm: () => remove.mutate() })] }));
|
|
@@ -35,6 +35,9 @@ export interface CustomFormState {
|
|
|
35
35
|
apiKey: string;
|
|
36
36
|
modelId: string;
|
|
37
37
|
modelName: string;
|
|
38
|
+
/** Unset until the user flips the switch: the endpoint's `GET /models` decides. */
|
|
39
|
+
attachment?: boolean;
|
|
40
|
+
reasoning?: boolean;
|
|
38
41
|
}
|
|
39
42
|
export type ChatGptPhase = 'idle' | 'waiting' | 'done';
|
|
40
43
|
export type ChatGptChallenge = {
|
|
@@ -170,8 +170,8 @@ export function formatTokenCount(tokens) {
|
|
|
170
170
|
if (!tokens || tokens <= 0)
|
|
171
171
|
return '';
|
|
172
172
|
if (tokens >= 1_000_000) {
|
|
173
|
-
|
|
174
|
-
return `${Number.
|
|
173
|
+
// 1,048,576 reads "1M", not "1.0M".
|
|
174
|
+
return `${Number(Math.round((tokens / 1_000_000) * 10) / 10)}M`;
|
|
175
175
|
}
|
|
176
176
|
if (tokens >= 1_000)
|
|
177
177
|
return `${Math.round(tokens / 1000)}K`;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentConfigBlock,
|
|
1
|
+
import type { AgentConfigBlock, SandboxTemplate } from '@kortix/sdk';
|
|
2
2
|
import { type GrantOption } from './grant-mode-field.js';
|
|
3
3
|
type SetKortix = <K extends keyof AgentConfigBlock>(key: K, value: AgentConfigBlock[K]) => void;
|
|
4
4
|
/**
|
|
@@ -12,25 +12,18 @@ export declare function RequiredConnectorToggle({ active, onToggle, }: {
|
|
|
12
12
|
active: boolean;
|
|
13
13
|
onToggle: () => void;
|
|
14
14
|
}): import("react").JSX.Element;
|
|
15
|
-
/** The summary chip at a grant card's right edge — "All", "3 picked", "None". */
|
|
16
|
-
export declare function GrantChip({ value }: {
|
|
17
|
-
value: AgentGrantSetV2 | undefined;
|
|
18
|
-
}): import("react").JSX.Element;
|
|
19
15
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
16
|
+
* The section action of a grant page: one `secondary` button to the
|
|
17
|
+
* project-wide page for the same thing. A grant page picks from what the
|
|
18
|
+
* project has; adding, editing or removing one happens on its own Customize
|
|
19
|
+
* tab. The grant mode row already states All / Pick / None, so the header
|
|
20
|
+
* carries no summary chip.
|
|
25
21
|
*/
|
|
26
|
-
export declare function
|
|
27
|
-
value: AgentGrantSetV2 | undefined;
|
|
22
|
+
export declare function ManageButton({ tab, href, }: {
|
|
28
23
|
tab?: 'skills' | 'connectors' | 'secrets';
|
|
29
|
-
/** A page `tab` cannot name
|
|
30
|
-
* through `capabilityTabHref(projectId, 'apps')`. */
|
|
24
|
+
/** A page `tab` cannot name, such as Apps. */
|
|
31
25
|
href?: (projectId: string) => string | null;
|
|
32
|
-
|
|
33
|
-
}): import("react").JSX.Element;
|
|
26
|
+
}): import("react").JSX.Element | null;
|
|
34
27
|
interface GrantSectionProps {
|
|
35
28
|
draft: AgentConfigBlock;
|
|
36
29
|
set: SetKortix;
|