@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.
Files changed (123) hide show
  1. package/dist/_vendor/kortix-shared.js +489 -381
  2. package/dist/customize/agents/agent-authority-card.js +1 -2
  3. package/dist/customize/agents/agent-detail-aside.js +15 -14
  4. package/dist/customize/agents/agent-drives-section.js +5 -6
  5. package/dist/customize/agents/agent-grant-pages.js +68 -70
  6. package/dist/customize/agents/agent-nav.prototype.d.ts +35 -0
  7. package/dist/customize/agents/agent-nav.prototype.js +95 -0
  8. package/dist/customize/agents/agent-page.js +57 -22
  9. package/dist/customize/agents/agent-people-section.js +18 -17
  10. package/dist/customize/agents/agent-triggers-section.js +6 -5
  11. package/dist/customize/connectors/catalog/catalog-entry.d.ts +14 -0
  12. package/dist/customize/connectors/catalog/catalog-entry.js +18 -1
  13. package/dist/customize/connectors/catalog/connector-browse.d.ts +28 -0
  14. package/dist/customize/connectors/catalog/connector-browse.js +18 -5
  15. package/dist/customize/connectors/detail/app-page.js +19 -5
  16. package/dist/customize/connectors/detail/connector-modal.d.ts +10 -13
  17. package/dist/customize/connectors/detail/connector-modal.js +18 -101
  18. package/dist/customize/connectors/detail/connector-notice.js +6 -2
  19. package/dist/customize/connectors/detail/connector-page.d.ts +30 -0
  20. package/dist/customize/connectors/detail/connector-page.js +26 -13
  21. package/dist/customize/sections/llm-provider/custom-provider-form.js +24 -7
  22. package/dist/customize/sections/llm-provider/custom-provider-panel.js +44 -8
  23. package/dist/customize/sections/llm-provider/types.d.ts +3 -0
  24. package/dist/customize/sections/llm-provider/utils.js +2 -2
  25. package/dist/customize/sections/view/agent-editor-access-fields.d.ts +9 -16
  26. package/dist/customize/sections/view/agent-editor-access-fields.js +30 -30
  27. package/dist/customize/sections/view/agent-editor-basics-fields.js +3 -10
  28. package/dist/customize/sections/view/agent-editor-primitives.d.ts +28 -10
  29. package/dist/customize/sections/view/agent-editor-primitives.js +32 -19
  30. package/dist/customize/sections/view/agent-editor.js +3 -3
  31. package/dist/customize/sections/view/gateway/gateway-routing.js +3 -3
  32. package/dist/customize/sections/view/grant-mode-field.d.ts +2 -5
  33. package/dist/customize/sections/view/grant-mode-field.js +10 -18
  34. package/dist/customize/sections/view/permission-editor.js +11 -8
  35. package/dist/customize/sections/view/secrets-view.js +2 -1
  36. package/dist/customize/settings/tabs/general-tab.d.ts +9 -2
  37. package/dist/customize/settings/tabs/general-tab.js +33 -11
  38. package/dist/customize/settings/tabs/transfer-workspace-dialog.d.ts +4 -0
  39. package/dist/customize/settings/tabs/transfer-workspace-dialog.js +82 -0
  40. package/dist/files/project/api/runtime-files.js +3 -2
  41. package/dist/files/project/file-source.js +18 -1
  42. package/dist/files/project/hooks/use-file-content.js +2 -0
  43. package/dist/files/viewer/file-header.js +1 -1
  44. package/dist/files/viewer/file-preview-modal.d.ts +1 -1
  45. package/dist/files/viewer/file-preview-modal.js +12 -2
  46. package/dist/files/viewer/file-source.d.ts +3 -1
  47. package/dist/files/viewer/use-file-content-state.js +26 -7
  48. package/dist/files/viewer/viewer-actions.js +2 -2
  49. package/dist/i18n/en.d.ts +345 -12
  50. package/dist/i18n/en.js +350 -17
  51. package/dist/internal/files/types.d.ts +2 -0
  52. package/dist/internal/goals/goal-meta.d.ts +30 -0
  53. package/dist/internal/goals/goal-meta.js +55 -0
  54. package/dist/internal/lib/query-client-singleton.d.ts +9 -3
  55. package/dist/internal/lib/query-client-singleton.js +18 -12
  56. package/dist/internal/share/session-shares-modal.js +3 -7
  57. package/dist/internal/stores/kortix-computer-store.js +14 -2
  58. package/dist/internal/stores/session-hover-store.d.ts +32 -0
  59. package/dist/internal/stores/session-hover-store.js +38 -0
  60. package/dist/internal/stores/sidebar-nav-store.d.ts +1 -1
  61. package/dist/internal/stores/sidebar-nav-store.js +3 -1
  62. package/dist/lib/routes.d.ts +2 -0
  63. package/dist/lib/routes.js +1 -0
  64. package/dist/primitives/settings-section-header.d.ts +1 -1
  65. package/dist/session/composer/attachment-tile.d.ts +0 -2
  66. package/dist/session/composer/attachment-tile.js +0 -8
  67. package/dist/session/composer/clipboard-files.d.ts +2 -1
  68. package/dist/session/composer/composer-draft-recovery.d.ts +1 -2
  69. package/dist/session/composer/composer-draft-recovery.js +0 -13
  70. package/dist/session/composer/context-ring.d.ts +1 -1
  71. package/dist/session/composer/context-ring.js +2 -2
  72. package/dist/session/composer/context-usage.d.ts +3 -6
  73. package/dist/session/composer/context-usage.js +3 -6
  74. package/dist/session/composer/inbox-row-claims.d.ts +2 -2
  75. package/dist/session/composer/inbox-row-claims.js +1 -1
  76. package/dist/session/composer/queue-projection.d.ts +3 -2
  77. package/dist/session/composer/queued-prompt-edit.d.ts +1 -1
  78. package/dist/session/composer/reasoning-effort-selector.d.ts +2 -1
  79. package/dist/session/composer/session-composer-readiness.d.ts +7 -22
  80. package/dist/session/composer/session-composer-readiness.js +12 -40
  81. package/dist/session/composer/token-progress.d.ts +1 -1
  82. package/dist/session/composer/token-progress.js +2 -2
  83. package/dist/session/header/session-goal-indicator.d.ts +1 -0
  84. package/dist/session/header/session-goal-indicator.js +41 -0
  85. package/dist/session/header/session-site-header.js +2 -1
  86. package/dist/session/panel/session-action-panel-column.js +11 -1
  87. package/dist/session/panel/session-audit-shared.d.ts +0 -4
  88. package/dist/session/panel/session-audit-shared.js +1 -1
  89. package/dist/session/panel/session-changes-shared.d.ts +2 -1
  90. package/dist/session/prompts/session-permission-prompt.js +6 -3
  91. package/dist/session/status/provisioning-failure.d.ts +2 -1
  92. package/dist/session/status/session-error-banner.js +8 -6
  93. package/dist/session/status/session-starting-loader.d.ts +24 -31
  94. package/dist/session/status/session-starting-loader.js +32 -81
  95. package/dist/session/thread/chat-minimap-items.d.ts +4 -4
  96. package/dist/session/thread/first-prompt-handover.d.ts +3 -2
  97. package/dist/session/thread/message-parsing.d.ts +3 -3
  98. package/dist/session/thread/session-chat.js +3 -10
  99. package/dist/session/thread/session-history-scroll.d.ts +2 -1
  100. package/dist/session/thread/session-thread.js +1 -1
  101. package/dist/styles.css +1 -1
  102. package/dist/terminal/pty-connection.d.ts +5 -4
  103. package/dist/workspace/index.d.ts +1 -0
  104. package/dist/workspace/index.js +1 -0
  105. package/dist/workspace/saved-session-skeleton-shape.d.ts +4 -3
  106. package/dist/workspace/saved-session-skeleton.d.ts +5 -1
  107. package/dist/workspace/saved-session-skeleton.js +5 -4
  108. package/dist/workspace/session-workspace.js +4 -5
  109. package/dist/workspace/sidebar/footer/project-goals-nav.d.ts +6 -0
  110. package/dist/workspace/sidebar/footer/project-goals-nav.js +26 -0
  111. package/dist/workspace/sidebar/modal/session-embed-section.d.ts +25 -0
  112. package/dist/workspace/sidebar/modal/session-embed-section.js +71 -0
  113. package/dist/workspace/sidebar/modal/share-session-modal.js +2 -2
  114. package/dist/workspace/sidebar/nav-row-meta.js +14 -2
  115. package/dist/workspace/sidebar/project-nav-items.js +3 -0
  116. package/dist/workspace/sidebar/project-session-list.js +12 -3
  117. package/dist/workspace/sidebar/session-brief-hover-card.js +5 -8
  118. package/package.json +4 -4
  119. package/skills/kortix-ui/references/modules.md +2 -0
  120. package/dist/session/status/use-readiness-settling.d.ts +0 -18
  121. package/dist/session/status/use-readiness-settling.js +0 -43
  122. package/dist/workspace/sidebar/modal/session-public-link-section.d.ts +0 -13
  123. 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, Fragment as _Fragment } from "react/jsx-runtime";
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 { InfoBanner } from '../../../primitives/info-banner.js';
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 { foldKey } from '../catalog/catalog-entry.js';
23
- import { ConnectorDetailTabs } from './connector-detail-tabs.js';
24
- import { CONNECTOR_TAB_LABEL_KEY, connectorTabs } from './connector-tabs.js';
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
- * Connector detail — header identity + tab nav + content pane.
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
- * Header: icon, name, description, primary connect action.
31
- * Left: Accounts / Tools / Triggers / Settings nav.
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
- return (_jsx(Modal, { open: open, onOpenChange: onOpenChange, children: _jsx(ModalContent, { className: "bg-popover h-[80vh] space-y-0 lg:max-w-6xl", "aria-describedby": undefined, showCloseButton: false, children: connector ? (_jsx(ConnectorModalBody, { projectId: projectId, connector: connector, canWrite: canWrite, onChanged: onChanged, onRemoved: onRemoved }, connector.slug)) : isResolving ? (_jsx(ConnectorModalSkeleton, {})) : null }) }));
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, while `?c=<slug>` is resolving against a list that has not
43
- * arrived. Shape-matched to `ConnectorModalBody` — same header height, same
44
- * left rail width — so the handover fills the placeholders in place instead of
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(_Fragment, { children: [_jsxs(ModalHeader, { className: "flex-row items-start gap-2.5 border-b pb-4", children: [_jsx(VisuallyHidden, { children: _jsx(ModalTitle, { children: tI18nComplete.raw('text0c21b0363778') }) }), _jsx("span", { className: "p-1", children: _jsx(Skeleton, { className: "size-10 rounded-md" }) }), _jsxs("div", { className: "min-w-0 flex-1 space-y-2 pt-1", children: [_jsx(Skeleton, { className: "h-4 w-40 rounded-sm" }), _jsx(Skeleton, { className: "h-3 w-64 rounded-sm" })] })] }), _jsx(ModalBody, { className: "max-h-[70vh] overflow-hidden p-0", children: _jsxs("div", { className: "flex min-h-0 flex-col lg:h-[70vh] lg:flex-row", children: [_jsx("div", { className: "lg:border-border shrink-0 gap-1 p-3 lg:h-full lg:w-64 lg:border-r", children: _jsx("div", { className: "hidden space-y-1.5 lg:block", children: [0, 1, 2].map((i) => (_jsx(Skeleton, { className: "h-9 w-full rounded-md" }, i))) }) }), _jsxs("div", { className: "min-w-0 flex-1 space-y-3 px-5 py-4 lg:px-6", children: [_jsx(Skeleton, { className: "h-5 w-32 rounded-sm" }), _jsx(Skeleton, { className: "h-20 w-full rounded-md" }), _jsx(Skeleton, { className: "h-20 w-full rounded-md" })] })] }) })] }));
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 = /[:\s]*\bHTTP (\d{3})\b/.exec(reason);
75
+ const match = /\bHTTP (\d{3})\b/.exec(reason);
76
76
  if (!match)
77
77
  return { code: null, detail: reason };
78
- return { code: match[1], detail: reason.replace(match[0], '').trim() };
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({ projectId, connector, canWrite, backHref, onChanged, onRemoved, }) {
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 requestedTab = search?.get('tab');
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) => replaceParams((params) => {
122
- if (next === 'accounts')
123
- params.delete('tab');
124
- else
125
- params.set('tab', next);
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: PAGE_COLUMN, children: [_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')] }) }), _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" }), _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] })] }), _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
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: () => router.push(connectorsHref(projectId, 'connected')),
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
- router.push(connectorsHref(projectId, 'connected'));
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 = { id: form.modelId.trim(), name: form.modelName.trim() };
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
- await custom.save(providerId, {
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: () => upsertProjectSecret(projectId, {
112
- name: provider.secret_name,
113
- value: value.trim(),
114
- strategy: 'broker',
115
- consumer: 'llm_gateway',
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(DetailRow, { label: model.name, value: model.id }, 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: () => {
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
- const millions = tokens / 1_000_000;
174
- return `${Number.isInteger(millions) ? millions : millions.toFixed(1)}M`;
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, AgentGrantSetV2, SandboxTemplate } from '@kortix/sdk';
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
- * Chip plus a link to the project-wide page for the same thing. A grant page
21
- * picks from what the project has; adding, editing or removing one of them
22
- * happens on its own Customize tab (Marko, 2026-09-03: "for now … point to
23
- * the general page … to see & browse em all"). Full CRUD in place is the
24
- * follow-up.
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 GrantHeaderTrailing({ value, tab, href, label, }: {
27
- value: AgentGrantSetV2 | undefined;
22
+ export declare function ManageButton({ tab, href, }: {
28
23
  tab?: 'skills' | 'connectors' | 'secrets';
29
- /** A page `tab` cannot name — Apps is a Customize tab, reached
30
- * through `capabilityTabHref(projectId, 'apps')`. */
24
+ /** A page `tab` cannot name, such as Apps. */
31
25
  href?: (projectId: string) => string | null;
32
- label: string;
33
- }): import("react").JSX.Element;
26
+ }): import("react").JSX.Element | null;
34
27
  interface GrantSectionProps {
35
28
  draft: AgentConfigBlock;
36
29
  set: SetKortix;