@kortix/ui 0.14.5 → 0.14.6

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 (124) hide show
  1. package/dist/access/access/access-dialog.js +8 -5
  2. package/dist/access/access/copy-row.js +2 -2
  3. package/dist/access/sharing-picker.js +2 -1
  4. package/dist/browser/app-in-browser.js +5 -3
  5. package/dist/browser/browser-panel.js +6 -6
  6. package/dist/browser/sandbox-browser-chrome.d.ts +4 -1
  7. package/dist/browser/sandbox-browser-chrome.js +4 -3
  8. package/dist/customize/agents/agent-access.d.ts +20 -0
  9. package/dist/customize/agents/agent-access.js +17 -0
  10. package/dist/customize/agents/agent-grant-pages.js +5 -6
  11. package/dist/customize/agents/agent-page.js +2 -1
  12. package/dist/customize/agents/agents-page.js +18 -1
  13. package/dist/customize/connectors/add-mcp/add-mcp-server.js +4 -2
  14. package/dist/customize/connectors/connector-routes.d.ts +3 -1
  15. package/dist/customize/connectors/connector-routes.js +18 -0
  16. package/dist/customize/connectors/connectors-page.js +23 -4
  17. package/dist/customize/connectors/detail/connector-advanced-tools.js +4 -6
  18. package/dist/customize/providers/provider-accordion.js +2 -1
  19. package/dist/customize/providers/provider-connect.js +2 -1
  20. package/dist/customize/sections/component/telegram-channel-row.js +1 -1
  21. package/dist/customize/sections/connector-connection-modal.js +11 -9
  22. package/dist/customize/sections/connectors-view.js +11 -13
  23. package/dist/customize/sections/view/gateway/gateway-routing.js +15 -18
  24. package/dist/customize/settings/rail-copy-context.js +5 -3
  25. package/dist/customize/settings/tabs/sandbox-template-form.js +9 -12
  26. package/dist/customize/shared/capability-tabs.js +2 -1
  27. package/dist/customize/skills/skills-page.js +3 -5
  28. package/dist/customize/triggers/schedule/event-trigger-copy.js +3 -2
  29. package/dist/customize/triggers/schedule/schedule-builder.js +7 -4
  30. package/dist/customize/triggers/schedule/trigger-composer.js +5 -2
  31. package/dist/customize/triggers/schedule/when-event.js +1 -1
  32. package/dist/diff/diff-view.js +9 -10
  33. package/dist/files/editor/code-editor.js +2 -3
  34. package/dist/files/project/components/change-request-detail-dialog.d.ts +3 -1
  35. package/dist/files/project/components/change-request-detail-dialog.js +3 -5
  36. package/dist/files/project/components/change-request-status-badge.d.ts +12 -0
  37. package/dist/files/project/components/change-request-status-badge.js +27 -0
  38. package/dist/files/project/components/change-requests-panel.js +1 -1
  39. package/dist/files/project/components/drive-explorer.js +2 -1
  40. package/dist/files/project/components/drive-grid-view.js +11 -7
  41. package/dist/files/project/components/drive-list-view.js +4 -4
  42. package/dist/files/project/components/file-breadcrumbs.js +1 -1
  43. package/dist/files/project/components/file-explorer-page.js +1 -1
  44. package/dist/files/project/components/file-history-popover.js +1 -6
  45. package/dist/files/project/components/file-search.js +6 -1
  46. package/dist/files/project/components/open-change-request-dialog.js +2 -1
  47. package/dist/files/project/components/version-selector-logic.d.ts +11 -0
  48. package/dist/files/project/components/version-selector-logic.js +26 -0
  49. package/dist/files/project/components/version-selector.js +11 -20
  50. package/dist/files/project/hooks/use-server-health.js +17 -15
  51. package/dist/files/renderers/docx/docx-viewer.js +0 -5
  52. package/dist/files/renderers/image-renderer.js +6 -24
  53. package/dist/files/renderers/pdf/pdf-viewer.js +1 -1
  54. package/dist/files/renderers/presentation/FullScreenPresentationViewer.js +2 -11
  55. package/dist/files/renderers/show-content-renderer.js +1 -1
  56. package/dist/files/renderers/sqlite-renderer.js +8 -3
  57. package/dist/files/renderers/xlsx/xlsx-viewer.js +15 -16
  58. package/dist/files/tree/components/file-tree-item.js +1 -1
  59. package/dist/files/viewer/file-preview-modal.js +1 -1
  60. package/dist/files/viewer/use-file-content-state.js +1 -1
  61. package/dist/i18n/en.d.ts +10 -0
  62. package/dist/i18n/en.js +10 -0
  63. package/dist/internal/desktop/desktop-back-button.js +5 -3
  64. package/dist/internal/hooks/use-auto-scroll.js +5 -3
  65. package/dist/internal/notifications/notification-rows.js +2 -1
  66. package/dist/internal/outcomes/outcome-types.d.ts +5 -0
  67. package/dist/markdown/code/inline-code.js +8 -0
  68. package/dist/markdown/code/output-paths-context.d.ts +18 -0
  69. package/dist/markdown/code/output-paths-context.js +35 -0
  70. package/dist/markdown/genui/components/charts.js +6 -5
  71. package/dist/primitives/map.js +2 -1
  72. package/dist/primitives/preview-image.js +4 -2
  73. package/dist/primitives/textarea.js +1 -1
  74. package/dist/primitives/typed-title.js +12 -14
  75. package/dist/primitives/typing-animation.js +4 -2
  76. package/dist/session/composer/agent-selector.js +6 -1
  77. package/dist/session/composer/composer-chat-input.js +5 -3
  78. package/dist/session/composer/composer-logic.js +8 -7
  79. package/dist/session/composer/draft/composer-draft.js +4 -4
  80. package/dist/session/composer/pasted-text.js +3 -2
  81. package/dist/session/composer/stores/queued-draft-store.js +3 -2
  82. package/dist/session/header/reminder-parts.d.ts +11 -2
  83. package/dist/session/header/reminder-parts.js +9 -4
  84. package/dist/session/media/sandbox-url-detector.js +0 -7
  85. package/dist/session/outcomes/change-request-outcomes.js +24 -3
  86. package/dist/session/overrides/provider-secret-pool-editor.js +2 -1
  87. package/dist/session/panel/advanced/advanced-panel.js +3 -7
  88. package/dist/session/panel/easy/app-preview.js +12 -2
  89. package/dist/session/panel/easy/easy-panel-logic.d.ts +1 -1
  90. package/dist/session/panel/easy/easy-panel-logic.js +1 -1
  91. package/dist/session/panel/easy/outputs-card.js +2 -1
  92. package/dist/session/panel/easy/panel-card.js +5 -3
  93. package/dist/session/panel/easy/preview-load.d.ts +14 -0
  94. package/dist/session/panel/easy/preview-load.js +15 -0
  95. package/dist/session/panel/easy/step-detail-body.js +2 -7
  96. package/dist/session/panel/session-panel-provider.js +6 -2
  97. package/dist/session/panel/shared/use-panel-derivations.js +6 -4
  98. package/dist/session/panel/use-session-panel-layout.js +7 -5
  99. package/dist/session/print/session-print-header.js +9 -8
  100. package/dist/session/thread/session-chat.js +12 -9
  101. package/dist/session/thread/session-history.js +3 -1
  102. package/dist/session/thread/transcript.js +13 -7
  103. package/dist/session/tool/shared/file-list.js +19 -2
  104. package/dist/session/tool/shared/infrastructure-contexts.d.ts +7 -0
  105. package/dist/session/tool/shared/infrastructure-contexts.js +7 -0
  106. package/dist/session/tool/shared/infrastructure-output.js +9 -5
  107. package/dist/session/tool/shared/infrastructure-row-header.js +9 -1
  108. package/dist/session/tool/shared/infrastructure-rows.js +7 -5
  109. package/dist/session/tool/tools/bash-tool.js +10 -6
  110. package/dist/session/tool/tools/web-search-tool.js +25 -3
  111. package/dist/session/turn/recovered-failures.d.ts +8 -0
  112. package/dist/session/turn/recovered-failures.js +32 -0
  113. package/dist/session/turn/streaming-cadence.js +5 -8
  114. package/dist/styles.css +1 -1
  115. package/dist/terminal/session-terminal-panel.js +3 -5
  116. package/dist/theme/theme.js +2 -1
  117. package/dist/workspace/projects/modal/github-import-pickers.js +7 -6
  118. package/dist/workspace/review/review-center.js +13 -10
  119. package/dist/workspace/session-workspace.js +5 -9
  120. package/dist/workspace/sessions/deleted-session-row.js +5 -2
  121. package/dist/workspace/sidebar/footer/project-computer-nav.js +6 -4
  122. package/dist/workspace/sidebar/footer/project-mcp-nav.js +6 -2
  123. package/dist/workspace/sidebar/modal/rename-session-modal.js +8 -3
  124. package/package.json +3 -3
@@ -86,9 +86,11 @@ export function diffAgentGrants(current, next) {
86
86
  if (next.mode === 'all')
87
87
  return { add: [], remove: [...currentIds] };
88
88
  const nextIds = next.ids;
89
+ const currentSet = new Set(currentIds);
90
+ const nextSet = new Set(nextIds);
89
91
  return {
90
- add: nextIds.filter((id) => !currentIds.includes(id)),
91
- remove: currentIds.filter((id) => !nextIds.includes(id)),
92
+ add: nextIds.filter((id) => !currentSet.has(id)),
93
+ remove: currentIds.filter((id) => !nextSet.has(id)),
92
94
  };
93
95
  }
94
96
  /**
@@ -339,10 +341,11 @@ function PrincipalAccessDialog({ open, onOpenChange, accountId, scope, mode, acc
339
341
  ...contract('inventory'),
340
342
  });
341
343
  const projectAgents = useMemo(() => resourceGrantsQuery.data?.resources.agents ?? [], [resourceGrantsQuery.data]);
344
+ const selectedAgentIdSet = useMemo(() => new Set(agents.ids), [agents.ids]);
342
345
  const selectedAgentDeclares = useMemo(() => {
343
346
  if (agents.mode !== 'subset' || agents.ids.length === 0)
344
347
  return null;
345
- const picked = projectAgents.filter((a) => agents.ids.includes(a.id));
348
+ const picked = projectAgents.filter((a) => selectedAgentIdSet.has(a.id));
346
349
  if (picked.length === 0)
347
350
  return null;
348
351
  const union = (key) => {
@@ -351,7 +354,7 @@ function PrincipalAccessDialog({ open, onOpenChange, accountId, scope, mode, acc
351
354
  return [...new Set(picked.flatMap((a) => a.declares?.[key] ?? []))];
352
355
  };
353
356
  return { secrets: union('secrets'), connectors: union('connectors') };
354
- }, [agents, projectAgents]);
357
+ }, [agents, projectAgents, selectedAgentIdSet]);
355
358
  // ── Expiry ────────────────────────────────────────────────────────────
356
359
  // `expires_at` is a column on every assignment, so every grant this dialog
357
360
  // writes can carry one. It used to be offered only where the underlying store
@@ -879,7 +882,7 @@ function PrincipalAccessDialog({ open, onOpenChange, accountId, scope, mode, acc
879
882
  ? ['member', 'admin']
880
883
  : undefined }), scope.kind === 'project' && principals.groupIds.length > 0 ? (_jsx(FieldDescription, { children: tI18nComplete.raw('text237f1a28cf06') })) : null] })) : null, scope.kind === 'project' && everyoneSelected ? (_jsx(InfoBanner, { tone: "neutral", icon: UsersThreeIcon, children: tSharing('everyoneRoleNote', { project: scope.projectName }) })) : null, scope.kind === 'project' && showAgentCeilingNote ? (_jsx(InfoBanner, { tone: "neutral", icon: RobotIcon, title: tAgents('ceilingNoteTitle'), children: _jsx("span", { "data-testid": "agent-ceiling-note", children: tAgents('ceilingNote') }) })) : null, showAgents ? (_jsxs(Field, { className: "gap-1.5", children: [_jsx(FieldLabel, { children: tI18nComplete.raw('text279b44d2ab4b') }), resourceGrantsQuery.isError ? (_jsx(InfoBanner, { tone: "destructive", action: _jsx(Button, { size: "sm", variant: "outline", onClick: () => void resourceGrantsQuery.refetch(), children: tCommon('retry') }), children: resourceGrantsQuery.error.message })) : resourceGrantsQuery.isLoading ? (_jsx(Skeleton, { className: "h-6 w-full rounded-md" })) : null, _jsx(Tabs, { value: agents.mode, onValueChange: (next) => setAgents(next === 'all' ? ALL_AGENTS : { mode: 'subset', ids: agents.ids }), children: _jsxs(TabsListCompact, { children: [_jsx(TabsTriggerCompact, { value: "all", children: tI18nComplete.raw('text54c32d3e2cfa') }), _jsx(TabsTriggerCompact, { value: "subset", children: tI18nComplete.raw('textb080141d6840') })] }) }), agents.mode === 'all' ? (_jsx(FieldDescription, { children: projectAgents.length === 0
881
884
  ? tI18nComplete.raw('text7bf9d07dcb99')
882
- : tI18nComplete('textc5bb9fda9711', { value0: projectAgents.length }) })) : null, agents.mode === 'subset' ? (resourceGrantsQuery.isLoading ? (_jsx(Skeleton, { className: "h-24 w-full rounded-md" })) : projectAgents.length === 0 ? (_jsx("p", { className: "text-muted-foreground text-xs", children: tI18nComplete.raw('text7bf9d07dcb99') })) : (_jsx("div", { className: "border-border max-h-40 overflow-y-auto rounded-md border p-1", children: projectAgents.map((agent) => (_jsx(Checkbox, { label: agent.name, checked: agents.ids.includes(agent.id), disabled: pending, onCheckedChange: () => setAgents((prev) => ({
885
+ : tI18nComplete('textc5bb9fda9711', { value0: projectAgents.length }) })) : null, agents.mode === 'subset' ? (resourceGrantsQuery.isLoading ? (_jsx(Skeleton, { className: "h-24 w-full rounded-md" })) : projectAgents.length === 0 ? (_jsx("p", { className: "text-muted-foreground text-xs", children: tI18nComplete.raw('text7bf9d07dcb99') })) : (_jsx("div", { className: "border-border max-h-40 overflow-y-auto rounded-md border p-1", children: projectAgents.map((agent) => (_jsx(Checkbox, { label: agent.name, checked: selectedAgentIdSet.has(agent.id), disabled: pending, onCheckedChange: () => setAgents((prev) => ({
883
886
  mode: 'subset',
884
887
  ids: prev.ids.includes(agent.id)
885
888
  ? prev.ids.filter((x) => x !== agent.id)
@@ -13,7 +13,7 @@ import { errorToast, successToast } from '../../primitives/toast.js';
13
13
  import { useCopy } from '../../internal/hooks/use-copy.js';
14
14
  import { cn } from '../../internal/lib/utils.js';
15
15
  import { CheckIcon, CopyIcon } from '@phosphor-icons/react';
16
- import { AnimatePresence, motion } from 'motion/react';
16
+ import { AnimatePresence, m } from 'motion/react';
17
17
  /**
18
18
  * Imperative copy for call sites that are not a `CopyRow` (a kebab item, a
19
19
  * toast action button). Same toast pair the three private copies used.
@@ -37,5 +37,5 @@ export function CopyRow({ value, label, display, successMessage, actions, classN
37
37
  const copyLabel = copied
38
38
  ? tI18nComplete.raw('text8d525e5f158b')
39
39
  : tI18nComplete.raw('texte21f935f11d7');
40
- return (_jsxs("div", { className: cn('space-y-1.5', className), children: [label ? _jsx("span", { className: "text-muted-foreground text-xs font-medium", children: label }) : null, _jsxs("div", { className: "bg-popover flex items-center gap-2 rounded-md border px-3 py-2", children: [_jsx("code", { className: "text-foreground min-w-0 flex-1 truncate font-mono text-xs", children: display ?? value }), actions, _jsx(Hint, { label: copyLabel, children: _jsx(Button, { type: "button", variant: "ghost", size: "icon-xs", "aria-label": copyLabel, onClick: () => void copy(value), className: "text-muted-foreground hover:text-foreground shrink-0", children: _jsx("span", { className: "relative inline-flex size-3.5 items-center justify-center", children: _jsx(AnimatePresence, { initial: false, mode: "popLayout", children: _jsx(motion.span, { initial: { scale: 0.25, opacity: 0, filter: 'blur(4px)' }, animate: { scale: 1, opacity: 1, filter: 'blur(0px)' }, exit: { scale: 0.25, opacity: 0, filter: 'blur(4px)' }, transition: { type: 'spring', duration: 0.3, bounce: 0 }, className: "absolute inset-0 inline-flex items-center justify-center", children: copied ? (_jsx(CheckIcon, { className: "text-kortix-green size-3.5" })) : (_jsx(CopyIcon, { className: "size-3.5" })) }, copied ? 'check' : 'copy') }) }) }) })] })] }));
40
+ return (_jsxs("div", { className: cn('space-y-1.5', className), children: [label ? _jsx("span", { className: "text-muted-foreground text-xs font-medium", children: label }) : null, _jsxs("div", { className: "bg-popover flex items-center gap-2 rounded-md border px-3 py-2", children: [_jsx("code", { className: "text-foreground min-w-0 flex-1 truncate font-mono text-xs", children: display ?? value }), actions, _jsx(Hint, { label: copyLabel, children: _jsx(Button, { type: "button", variant: "ghost", size: "icon-xs", "aria-label": copyLabel, onClick: () => void copy(value), className: "text-muted-foreground hover:text-foreground shrink-0", children: _jsx("span", { className: "relative inline-flex size-3.5 items-center justify-center", children: _jsx(AnimatePresence, { initial: false, mode: "popLayout", children: _jsx(m.span, { initial: { scale: 0.25, opacity: 0, filter: 'blur(4px)' }, animate: { scale: 1, opacity: 1, filter: 'blur(0px)' }, exit: { scale: 0.25, opacity: 0, filter: 'blur(4px)' }, transition: { type: 'spring', duration: 0.3, bounce: 0 }, className: "absolute inset-0 inline-flex items-center justify-center", children: copied ? (_jsx(CheckIcon, { className: "text-kortix-green size-3.5" })) : (_jsx(CopyIcon, { className: "size-3.5" })) }, copied ? 'check' : 'copy') }) }) }) })] })] }));
41
41
  }
@@ -42,6 +42,7 @@ export function SharingPicker({ projectId, value, onChange, copy, showHeading =
42
42
  const offerMembers = !hideMembers && (accountApiEnabled || value.mode === 'members');
43
43
  return (_jsxs("div", { className: "space-y-3", children: [showHeading && _jsx(Label, { children: c.heading }), _jsxs(RadioGroup, { value: value.mode, onValueChange: (v) => onChange({ ...value, mode: v }), className: "space-y-2", children: [_jsx(ShareOption, { value: "project", label: c.project.label, desc: c.project.desc, disabled: isDisabled('project') }), _jsx(ShareOption, { value: "private", label: c.private.label, desc: c.private.desc, disabled: isDisabled('private') }), offerMembers && (_jsx(ShareOption, { value: "members", label: c.members.label, desc: c.members.desc, disabled: isDisabled('members') }))] }), offerMembers && value.mode === 'members' && (_jsx(SubjectPicker, { projectId: projectId, memberIds: value.memberIds, groupIds: value.groupIds, onChange: (memberIds, groupIds) => onChange({ ...value, memberIds, groupIds }) })), hideMembers && !legacyMembers && (_jsxs("p", { className: "text-muted-foreground text-xs leading-relaxed", children: [tI18nComplete.raw('text558b36e065df'), ' ', _jsx("span", { className: "text-foreground/80 font-medium", children: tI18nComplete.raw('textd4f0bc5a29de') }), ' ', tI18nComplete.raw('text3a71ebb2d593')] })), legacyMembers && (_jsx("p", { className: "text-xs leading-relaxed text-amber-600 dark:text-amber-400", children: tI18nComplete.raw('text64f4c4bd39c9') }))] }));
44
44
  }
45
+ const NO_INVITE_EMAILS = [];
45
46
  /**
46
47
  * Legacy adapter over {@link PrincipalPicker}, kept ONLY for the
47
48
  * non-access callers of this module (`features/apps/apps-view.tsx` and
@@ -52,7 +53,7 @@ export function SharingPicker({ projectId, value, onChange, copy, showHeading =
52
53
  * @deprecated Use `PrincipalPicker` from
53
54
  * `@/features/workspace/shared/access`.
54
55
  */
55
- export function SubjectPicker({ projectId, accountId, memberIds, groupIds, inviteEmails = [], onChange, allowInvite = false, kinds = ['member', 'group'], excludeUserIds, emptyLabel = 'No members or groups in this project yet.', allExcludedLabel = 'Everyone is already added.', }) {
56
+ export function SubjectPicker({ projectId, accountId, memberIds, groupIds, inviteEmails = NO_INVITE_EMAILS, onChange, allowInvite = false, kinds = ['member', 'group'], excludeUserIds, emptyLabel = 'No members or groups in this project yet.', allExcludedLabel = 'Everyone is already added.', }) {
56
57
  const value = useMemo(() => ({ memberIds, groupIds, inviteEmails }), [memberIds, groupIds, inviteEmails]);
57
58
  const scope = projectId
58
59
  ? { kind: 'project', projectId }
@@ -11,7 +11,7 @@
11
11
  import { isKortixAppUrl } from '../apps/kortix-app-url.js';
12
12
  import { APP_ACCESS_PARAM, withoutAppAccessToken } from '../internal/lib/utils/app-access-token.js';
13
13
  import { useAppAccess, useProjectApps } from '@kortix/sdk/react';
14
- import { useRef } from 'react';
14
+ import { useLayoutEffect, useRef } from 'react';
15
15
  import { appCan } from '../apps/app-shared.js';
16
16
  export { withoutAppAccessToken };
17
17
  function hostOf(url) {
@@ -72,8 +72,10 @@ export function useAppInBrowser(projectId, url) {
72
72
  }
73
73
  }
74
74
  const pinned = useRef(null);
75
- if (url && resolved && pinned.current?.url !== url)
76
- pinned.current = { url, src: resolved };
77
75
  const src = url && pinned.current?.url === url ? pinned.current.src : resolved;
76
+ useLayoutEffect(() => {
77
+ if (url && src && pinned.current?.url !== url)
78
+ pinned.current = { url, src };
79
+ });
78
80
  return { app, apps: apps.data ?? [], src };
79
81
  }
@@ -23,7 +23,7 @@ import { buildWebProxyUrl, isExternalUrl, isWebProxyUrl, normalizeExternalInput,
23
23
  import { useBrowserRecentsStore } from '../internal/stores/browser-recents-store.js';
24
24
  import { useTabStore } from '../internal/stores/tab-store.js';
25
25
  import { WarningIcon as AlertTriangle, ArrowSquareOutIcon, GlobeIcon as Globe, LinkSimpleIcon as Link2, DotsThreeIcon as MoreHorizontal, ArrowClockwiseIcon as RefreshCw, GearSixIcon as Settings2, } from '@phosphor-icons/react';
26
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
26
+ import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
27
27
  import { BrowserLanding } from './browser-landing.js';
28
28
  import { PreviewLoadingOverlay, SandboxAddressBar } from './sandbox-browser-chrome.js';
29
29
  function normalizePreviewLabel(value, fallback) {
@@ -34,6 +34,7 @@ function normalizePreviewLabel(value, fallback) {
34
34
  return fallback;
35
35
  return trimmed;
36
36
  }
37
+ const subscribeNoop = () => () => { };
37
38
  /**
38
39
  * Preview tab content — renders a proxied sandbox URL in an iframe
39
40
  * with a browser-like toolbar: editable address bar, refresh, back/forward, open externally.
@@ -56,8 +57,7 @@ export function BrowserPanel({ tabId, projectId, projectSessionId }) {
56
57
  const loadTimeoutRef = useRef(null);
57
58
  // Recents come from a persisted store — render them only after mount so the
58
59
  // server and first client render agree.
59
- const [mounted, setMounted] = useState(false);
60
- useEffect(() => setMounted(true), []);
60
+ const mounted = useSyncExternalStore(subscribeNoop, () => true, () => false);
61
61
  // Extract metadata from tab
62
62
  const rawPreviewUrl = tab?.metadata?.url || '';
63
63
  const port = tab?.metadata?.port || 0;
@@ -199,7 +199,7 @@ export function BrowserPanel({ tabId, projectId, projectSessionId }) {
199
199
  });
200
200
  pushHistory(newProxyUrl);
201
201
  handleRefresh();
202
- }, [subdomainOpts, rewritePortPath, tabId, updateTabMetadata, pushHistory, handleRefresh]);
202
+ }, [subdomainOpts, rewritePortPath, tabId, appPreviewTitle, updateTabMetadata, pushHistory, handleRefresh]);
203
203
  const canGoBack = historyIndex > 0;
204
204
  const canGoForward = historyIndex < history.length - 1;
205
205
  /** Walk the history stack in either direction; the two directions were two
@@ -248,7 +248,7 @@ export function BrowserPanel({ tabId, projectId, projectSessionId }) {
248
248
  handleRefresh();
249
249
  }
250
250
  }
251
- }, [canGoBack, canGoForward, historyIndex, history, tabId, updateTabMetadata, handleRefresh]);
251
+ }, [canGoBack, canGoForward, historyIndex, history, tabId, appPreviewTitle, updateTabMetadata, handleRefresh]);
252
252
  /** Home: the landing (Apps + recents). An empty url is what renders it. */
253
253
  const handleHome = useCallback(() => {
254
254
  updateTabMetadata({
@@ -290,7 +290,7 @@ export function BrowserPanel({ tabId, projectId, projectSessionId }) {
290
290
  path,
291
291
  },
292
292
  };
293
- }, [fullUrl, isExternalBrowsing, originalUrl, port, tab?.metadata?.path, tab?.title]);
293
+ }, [fullUrl, isExternalBrowsing, originalUrl, port, tab?.metadata?.path, tab?.title, appPreviewTitle]);
294
294
  const hasPreview = !!previewUrl || hostPreview.failed;
295
295
  const emptyState = (_jsx(EmptyState, { icon: Globe, className: "h-full", title: tHardcodedUi.raw('autoComponentsTabsPreviewTabContentJsxTextPreviewBrowser8136da05'), description: _jsxs(_Fragment, { children: [tHardcodedUi.raw('autoComponentsTabsPreviewTabContentJsxTextOpenAnAppda305669'), ' ', _jsx("span", { className: "text-foreground/80 font-mono", children: "3000" }), ' ', tHardcodedUi.raw('autoComponentsTabsPreviewTabContentJsxTextIfYouKnow6745fa88')] }) }));
296
296
  // Copy-public-link action, surfaced from the "⋯" overflow menu.
@@ -64,6 +64,9 @@ export declare function SandboxAddressBar({ displayValue, hasPreview, isLoading,
64
64
  * Cross-origin iframes frequently never fire onLoad, so neither surface may
65
65
  * trust the event alone — the parent decides when to show and dismiss this.
66
66
  */
67
- export declare function PreviewLoadingOverlay({ label }: {
67
+ export declare function PreviewLoadingOverlay({ label, detail, action, }: {
68
68
  label: string;
69
+ /** A second line once the load is slow: why, and the way out. */
70
+ detail?: string;
71
+ action?: React.ReactNode;
69
72
  }): import("react").JSX.Element;
@@ -80,6 +80,7 @@ export function parseAddressInput(value) {
80
80
  return null;
81
81
  return toInternalUrl(parsed.port, parsed.path);
82
82
  }
83
+ const NO_SUGGESTIONS = [];
83
84
  /**
84
85
  * The back / forward buttons and the home / address / reload group every
85
86
  * sandbox preview surface renders, with the hostname highlighted at rest. The bar
@@ -88,7 +89,7 @@ export function parseAddressInput(value) {
88
89
  * controls around it (`BrowserPanel`'s overflow menu, `AppPreview`'s viewer
89
90
  * actions and close) — the component is a fragment, so it slots between them.
90
91
  */
91
- export function SandboxAddressBar({ displayValue, hasPreview, isLoading, canGoBack, canGoForward, onBack, onForward, onReload, onHome, onNavigate, suggestions = [], placeholder, ariaLabel, inputRef, resetOnEscape = true, trailing, }) {
92
+ export function SandboxAddressBar({ displayValue, hasPreview, isLoading, canGoBack, canGoForward, onBack, onForward, onReload, onHome, onNavigate, suggestions = NO_SUGGESTIONS, placeholder, ariaLabel, inputRef, resetOnEscape = true, trailing, }) {
92
93
  const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
93
94
  const [addressValue, setAddressValue] = useState(displayValue);
94
95
  const [isEditing, setIsEditing] = useState(false);
@@ -144,6 +145,6 @@ export function SandboxAddressBar({ displayValue, hasPreview, isLoading, canGoBa
144
145
  * Cross-origin iframes frequently never fire onLoad, so neither surface may
145
146
  * trust the event alone — the parent decides when to show and dismiss this.
146
147
  */
147
- export function PreviewLoadingOverlay({ label }) {
148
- return (_jsx("div", { className: "bg-background/80 absolute inset-0 z-10 flex items-center justify-center", children: _jsxs("div", { className: "text-muted-foreground flex flex-col items-center gap-2", children: [_jsx(Loading, { className: "size-4" }), _jsx("p", { className: "text-xs", children: label })] }) }));
148
+ export function PreviewLoadingOverlay({ label, detail, action, }) {
149
+ return (_jsx("div", { className: "bg-background/80 absolute inset-0 z-10 flex items-center justify-center", children: _jsxs("div", { className: "text-muted-foreground flex max-w-xs flex-col items-center gap-2 px-4 text-center", children: [_jsx(Loading, { className: "size-4" }), _jsx("p", { className: "text-xs", children: label }), detail && _jsx("p", { className: "text-xs text-pretty", children: detail }), action] }) }));
149
150
  }
@@ -0,0 +1,20 @@
1
+ import type { ProjectConfigSummary } from '@kortix/sdk';
2
+ type AgentScope = NonNullable<ProjectConfigSummary['agents'][number]['scope']>;
3
+ /** One grant on the card: everything (`'all'`) or how many are listed. */
4
+ export interface AgentAccessPart {
5
+ kind: 'connectors' | 'secrets' | 'actions';
6
+ value: 'all' | number;
7
+ }
8
+ /**
9
+ * What an agent's configuration grants, for its card. The card used to show
10
+ * only the agent's own description, and the starter's says "full tool access"
11
+ * whatever the configuration grants (KRTX-3025). Null for an agent that
12
+ * kortix.yaml does not govern: there is nothing to summarize.
13
+ */
14
+ export declare function agentAccess(scope: AgentScope | undefined): {
15
+ kind: 'full';
16
+ } | {
17
+ kind: 'scoped';
18
+ parts: AgentAccessPart[];
19
+ } | null;
20
+ export {};
@@ -0,0 +1,17 @@
1
+ /**
2
+ * What an agent's configuration grants, for its card. The card used to show
3
+ * only the agent's own description, and the starter's says "full tool access"
4
+ * whatever the configuration grants (KRTX-3025). Null for an agent that
5
+ * kortix.yaml does not govern: there is nothing to summarize.
6
+ */
7
+ export function agentAccess(scope) {
8
+ if (!scope)
9
+ return null;
10
+ const grant = (value) => value === 'all' ? 'all' : (value?.length ?? 0);
11
+ const parts = [
12
+ { kind: 'connectors', value: grant(scope.connectors) },
13
+ { kind: 'secrets', value: grant(scope.env) },
14
+ { kind: 'actions', value: grant(scope.kortix_permissions ?? scope.kortix_cli) },
15
+ ];
16
+ return parts.every((part) => part.value === 'all') ? { kind: 'full' } : { kind: 'scoped', parts };
17
+ }
@@ -39,7 +39,7 @@ import { listConnectors, listProjectSecrets, } from '@kortix/sdk';
39
39
  import { contract, qk, useProjectAccountId, useProjectApps, } from '@kortix/sdk/react';
40
40
  import { GlobeIcon, KeyIcon, PlusIcon, SparkleIcon } from '@phosphor-icons/react';
41
41
  import { useQuery, useQueryClient } from '@tanstack/react-query';
42
- import { useEffect, useState } from 'react';
42
+ import { useState } from 'react';
43
43
  import { appGrantRows } from './agent-apps-grant.js';
44
44
  /**
45
45
  * All · Pick · None over a grid of selectable cards. `items` is the project's
@@ -110,18 +110,17 @@ export function ConnectorsGrantPage({ projectId, editor, }) {
110
110
  record: connectors.find((c) => c.slug === detailSlug),
111
111
  isSuccess: connectorsQuery.isSuccess,
112
112
  });
113
- useEffect(() => {
114
- if (detail.isMissing)
115
- setDetailSlug(null);
116
- }, [detail.isMissing]);
113
+ if (detail.isMissing)
114
+ setDetailSlug(null);
117
115
  const invalidate = () => queryClient.invalidateQueries({ queryKey: qk.project.connectors(projectId) });
118
116
  const { draft, set } = editor;
119
117
  return (_jsxs(_Fragment, { children: [_jsx(EditorSection, { title: tI18nComplete.raw('textc3d2e79ebdd0'), description: tI18nComplete.raw('textc174fc077197'), trailing: _jsx(GrantHeaderTrailing, { value: draft.connectors, tab: "connectors", label: tI18nComplete.raw('textd83250185d41') }), children: _jsxs("div", { className: "space-y-2 py-4", children: [_jsx(GrantCatalog, { value: draft.connectors, onChange: (v) => {
120
118
  set('connectors', v);
121
119
  const required = draft.connectors_required ?? [];
120
+ const picked = new Set(Array.isArray(v) ? v : []);
122
121
  const kept = v === 'all' || v === 'none' || !Array.isArray(v)
123
122
  ? []
124
- : required.filter((slug) => v.includes(slug));
123
+ : required.filter((slug) => picked.has(slug));
125
124
  if (kept.length !== required.length) {
126
125
  set('connectors_required', kept.length ? kept : undefined);
127
126
  }
@@ -166,7 +166,8 @@ function AgentPageFrame({ header, section, sections, sectionHref, pane, footer,
166
166
  const tI18nComplete = useI18nTranslations('hardcodedUi.i18nComplete');
167
167
  const isMobile = useIsMobile();
168
168
  const localizedConfig = useLocalizedAgentConfigCatalog();
169
- const items = localizedConfig.sections.filter((s) => sections.includes(s.key));
169
+ const sectionKeys = new Set(sections);
170
+ const items = localizedConfig.sections.filter((s) => sectionKeys.has(s.key));
170
171
  // The Preferences rail's rule: a heading over a lone group labels nothing.
171
172
  const groups = localizedConfig.groups
172
173
  .map((group) => ({
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
3
3
  import { customizePath, uiRoutes } from '../../lib/routes.js';
4
4
  import { NewEntityMenu } from '../shared/new-entity-menu.js';
5
5
  import { useTranslations as useI18nTranslations } from '../../i18n/index.js';
6
+ import { agentAccess } from './agent-access.js';
6
7
  import { useMemo, useRef, useState } from 'react';
7
8
  import { Badge } from '../../primitives/badge.js';
8
9
  import { Button } from '../../primitives/button.js';
@@ -163,10 +164,26 @@ function CardCaret() {
163
164
  function AgentCardFacts({ agent, triggerCount, peopleCount, }) {
164
165
  const tI18nComplete = useI18nTranslations('hardcodedUi.i18nComplete');
165
166
  const model = agent.model ? agent.model.split('/').pop() : null;
167
+ const accessLabel = accessLabelOf(agentAccess(agent.scope), tI18nComplete);
166
168
  const sep = (_jsx("span", { "aria-hidden": true, className: "text-muted-foreground", children: "\u00B7" }));
167
169
  return (_jsxs(_Fragment, { children: [_jsx("span", { className: cn('truncate', model && 'font-mono'), children: model ?? tI18nComplete.raw('text3840d9d29421') }), sep, _jsxs("span", { className: "tabular-nums", children: [triggerCount, " ", triggerCount === 1 ? 'trigger' : 'triggers'] }), peopleCount !== null ? (_jsxs(_Fragment, { children: [sep, _jsx("span", { className: "tabular-nums", children: peopleCount === 0
168
170
  ? tI18nComplete.raw('textb6c70926c1aa')
169
- : `${peopleCount} ${peopleCount === 1 ? 'grant' : 'grants'}` })] })) : null] }));
171
+ : `${peopleCount} ${peopleCount === 1 ? 'grant' : 'grants'}` })] })) : null, accessLabel ? _jsx("span", { className: "basis-full tabular-nums", children: accessLabel }) : null] }));
172
+ }
173
+ /** The access slot: what the configuration grants (KRTX-3025). */
174
+ function accessLabelOf(access, t) {
175
+ if (!access)
176
+ return null;
177
+ if (access.kind === 'full')
178
+ return t.raw('textf19611c61ca5');
179
+ const LABELS = {
180
+ connectors: { all: 'textd83250185d41', count: 'text15589a542412' },
181
+ secrets: { all: 'text0b38e3daeb93', count: 'texte96d99ad9896' },
182
+ actions: { all: 'text70b8d7e10749', count: 'text2d3f4433505a' },
183
+ };
184
+ return access.parts
185
+ .map(({ kind, value }) => value === 'all' ? t.raw(LABELS[kind].all) : t(LABELS[kind].count, { count: value }))
186
+ .join(', ');
170
187
  }
171
188
  /**
172
189
  * Card badges — "also a subagent", the default-agent star, disabled.
@@ -25,8 +25,9 @@ export function mcpServerIdentity(rawUrl, takenSlugs) {
25
25
  labels.pop();
26
26
  const base = [...labels].reverse().find((label) => !GENERIC_LABELS.has(label)) ?? labels[labels.length - 1] ?? 'mcp';
27
27
  const root = normalizeConnectorConnectionSlug(base) || 'mcp';
28
+ const taken = new Set(takenSlugs);
28
29
  let slug = root;
29
- for (let n = 2; takenSlugs.includes(slug); n++)
30
+ for (let n = 2; taken.has(slug); n++)
30
31
  slug = `${root}-${n}`;
31
32
  const name = root.charAt(0).toUpperCase() + root.slice(1);
32
33
  return { slug, name };
@@ -37,8 +38,9 @@ export function mcpServerIdentity(rawUrl, takenSlugs) {
37
38
  */
38
39
  export function mcpAppIdentity(appName, takenSlugs) {
39
40
  const root = normalizeConnectorConnectionSlug(appName) || 'mcp';
41
+ const taken = new Set(takenSlugs);
40
42
  let slug = root;
41
- for (let n = 2; takenSlugs.includes(slug); n++)
43
+ for (let n = 2; taken.has(slug); n++)
42
44
  slug = `${root}-${n}`;
43
45
  return { slug, name: appName };
44
46
  }
@@ -18,7 +18,9 @@ export type AppRef = {
18
18
  slug: string;
19
19
  };
20
20
  export declare function connectorsHref(projectId: string, scope?: 'all' | 'connected' | 'channels'): string;
21
- /** `null` for the computer entry: a computer has no app page. */
21
+ export declare function connectorSearchFromParams(search: Pick<URLSearchParams, 'get'> | null | undefined): string;
22
+ /** Sets `?q=` to the trimmed query, or removes it when the query is empty. */
23
+ export declare function withConnectorSearch(params: URLSearchParams, query: string): URLSearchParams;
22
24
  export declare function appRefFromEntry(entry: CatalogEntry): AppRef | null;
23
25
  export declare function appHref(projectId: string, app: AppRef): string;
24
26
  export declare function appRefFromLocation(appSegment: string, search: URLSearchParams | null): AppRef | null;
@@ -23,6 +23,24 @@ export function connectorsHref(projectId, scope) {
23
23
  return scope ? `${base(projectId)}?scope=${scope}` : base(projectId);
24
24
  }
25
25
  /** `null` for the computer entry: a computer has no app page. */
26
+ /**
27
+ * The catalogue search, held in `?q=`. Opening a connector is a route change
28
+ * that unmounts the catalogue; a search kept only in state was gone on browser
29
+ * Back (KRTX-3018).
30
+ */
31
+ const SEARCH_PARAM = 'q';
32
+ export function connectorSearchFromParams(search) {
33
+ return search?.get(SEARCH_PARAM)?.trim() ?? '';
34
+ }
35
+ /** Sets `?q=` to the trimmed query, or removes it when the query is empty. */
36
+ export function withConnectorSearch(params, query) {
37
+ const trimmed = query.trim();
38
+ if (trimmed)
39
+ params.set(SEARCH_PARAM, trimmed);
40
+ else
41
+ params.delete(SEARCH_PARAM);
42
+ return params;
43
+ }
26
44
  export function appRefFromEntry(entry) {
27
45
  if (entry.source === 'discover') {
28
46
  return { source: 'discover', slug: entry.slug, id: entry.connector.id };
@@ -8,7 +8,8 @@ import { contract, FRESHNESS, qk, useFeatureFlag, useProjectAccountId } from '@k
8
8
  import { MagnifyingGlassIcon, PlugIcon, PlugsConnectedIcon } from '@phosphor-icons/react';
9
9
  import { useQuery, useQueryClient } from '@tanstack/react-query';
10
10
  import { lazyClient } from '../../lib/lazy-client.js';
11
- import { useUiPathname, useUiRouter, useUiSearchParams } from '../../providers/ui-provider.js';
11
+ import { useUiPathname, useUiRouter, useUiSearchParams, useUiShallowNavigate, } from '../../providers/ui-provider.js';
12
+ import { useDebounce } from '../../hooks/use-debounce.js';
12
13
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
13
14
  import { PoliciesPanel } from '../../workspace/projects/policies-panel.js';
14
15
  import { Button } from '../../primitives/button.js';
@@ -37,7 +38,7 @@ import { useTunnelConnections } from '@kortix/sdk/react';
37
38
  import { useSlashFocus } from '../../internal/hooks/index.js';
38
39
  import { connectorDisplayName, connectorSummary, filterConnectors, } from './connector-filter.js';
39
40
  import { ConnectorMovesBanner } from './connector-moves-banner.js';
40
- import { appHref, appRefFromEntry, connectorHref, legacyDetailRedirect } from './connector-routes.js';
41
+ import { appHref, appRefFromEntry, connectorHref, connectorSearchFromParams, legacyDetailRedirect, withConnectorSearch, } from './connector-routes.js';
41
42
  import { useInstall } from './install/use-install.js';
42
43
  /**
43
44
  * The click-gated Add form, split out of this route's initial chunk.
@@ -172,7 +173,11 @@ export function ConnectorsPage({ projectId }) {
172
173
  const canWrite = useProjectCan(projectId, PROJECT_ACTIONS.PROJECT_CONNECTOR_WRITE, { accountId }).allowed ===
173
174
  true;
174
175
  const queryClient = useQueryClient();
175
- const [query, setQuery] = useState('');
176
+ const { search, router, pathname, replaceParams, setScope, rulesOpen, setRulesOpen } = useConnectorNavigation(projectId);
177
+ // The search lives in `?q=`: opening a connector unmounts this page, and
178
+ // browser Back must bring the same results back (KRTX-3018).
179
+ const [query, setQuery] = useState(() => connectorSearchFromParams(search));
180
+ useSearchInUrl(query, search, pathname);
176
181
  const [panel, setPanel] = useState(null);
177
182
  // The catalogue app whose own MCP server the MCP dialog connects. Null for
178
183
  // a server the user pastes (New → MCP server).
@@ -180,7 +185,6 @@ export function ConnectorsPage({ projectId }) {
180
185
  const [computerOpen, setComputerOpen] = useState(false);
181
186
  const tSharing = useI18nTranslations('accessSharing');
182
187
  const tPages = useI18nTranslations('connectorPages');
183
- const { search, router, pathname, replaceParams, setScope, rulesOpen, setRulesOpen } = useConnectorNavigation(projectId);
184
188
  const connectorsQuery = useQuery({
185
189
  queryKey: qk.project.connectors(projectId),
186
190
  queryFn: () => listConnectors(projectId, { includeSchemas: false }),
@@ -374,6 +378,21 @@ export function ConnectorsPage({ projectId }) {
374
378
  router.push(connectorHref(projectId, slug, { addAccount }));
375
379
  } }) })] }) })] }) }), _jsxs(SplitSheetContent, { children: [_jsxs(SplitSheetHeader, { children: [_jsx(SplitSheetTitle, { children: tI18nComplete.raw('text1d59a5e09714') }), _jsx(SplitSheetDescription, { children: tI18nComplete.raw('text014d10bd3c64') })] }), _jsx(SplitSheetBody, { children: _jsx(PoliciesPanel, { projectId: projectId }) })] })] }));
376
380
  }
381
+ /**
382
+ * Mirrors the settled search into `?q=` with a shallow replace: no history
383
+ * entry per keystroke, and no server round trip for a view already on screen.
384
+ */
385
+ function useSearchInUrl(query, search, pathname) {
386
+ const shallowNavigate = useUiShallowNavigate();
387
+ const { debouncedValue } = useDebounce(query.trim(), 300);
388
+ useEffect(() => {
389
+ if (connectorSearchFromParams(search) === debouncedValue)
390
+ return;
391
+ const params = withConnectorSearch(new URLSearchParams(search?.toString() ?? ''), debouncedValue);
392
+ const suffix = params.toString();
393
+ shallowNavigate(suffix ? `${pathname}?${suffix}` : pathname, { replace: true });
394
+ }, [debouncedValue, search, pathname, shallowNavigate]);
395
+ }
377
396
  function useConnectorNavigation(projectId) {
378
397
  const search = useUiSearchParams();
379
398
  const router = useUiRouter();
@@ -49,13 +49,11 @@ export function ConnectorAdvancedTools({ connector, advancedRules, toolRules, lo
49
49
  // re-fired whenever a rule was added or removed, so the panel sprang back open
50
50
  // moments after the user collapsed it, which read as the disclosure opening by
51
51
  // itself. After the first reveal the panel is the user's to control.
52
- const revealedExistingRules = useRef(false);
53
- useEffect(() => {
54
- if (revealedExistingRules.current || advancedRules.length === 0)
55
- return;
56
- revealedExistingRules.current = true;
52
+ const [revealedExistingRules, setRevealedExistingRules] = useState(false);
53
+ if (!revealedExistingRules && advancedRules.length > 0) {
54
+ setRevealedExistingRules(true);
57
55
  setAdvancedOpen(true);
58
- }, [advancedRules.length]);
56
+ }
59
57
  const savePatternRules = () => onSave([...toolRules, ...draftToRules(draft)]);
60
58
  return (_jsx(_Fragment, { children: hasTools || advancedRules.length > 0 ? (_jsxs(Disclosure, { variant: "outline", className: "overflow-hidden", open: advancedOpen, onOpenChange: setAdvancedOpen, children: [_jsx(DisclosureTrigger, { variant: "outline", children: _jsxs(Button, { variant: "popover", children: [tI18nComplete.raw('text9f088dbebd6c'), advancedRules.length > 0 ? (_jsx(Badge, { variant: "secondary", size: "sm", children: advancedRules.length })) : null, _jsx("span", { className: "text-muted-foreground ml-auto text-xs font-normal", children: tI18nComplete.raw('text59da2efb2423') })] }) }), _jsx(DisclosureContent, { variant: "outline", contentClassName: "border-border border-t", children: _jsxs("div", { className: "space-y-5 px-4 py-5", children: [_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-foreground text-sm font-medium", children: tI18nComplete.raw('text594bdd4c19b2') }), _jsx("p", { className: "text-muted-foreground mt-0.5 text-xs text-pretty", children: tI18nComplete.raw('textf900377c2048') })] }), _jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [sensitivePending ? _jsx(Loading, { className: "size-4 shrink-0" }) : null, _jsx(Switch, { checked: connector.sensitive === true, onCheckedChange: onSensitive, disabled: frozen || sensitivePending, "aria-label": tI18nComplete.raw('text594bdd4c19b2') })] })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: tI18nComplete.raw('text380c0b05bcfc') }), _jsxs("p", { className: "text-muted-foreground text-xs text-pretty", children: [tI18nComplete.raw('textde00adeaf21b'), _jsx("code", { className: "font-mono", children: "delete_*" }), tI18nComplete.raw('text3a13855164e7'), _jsx("code", { className: "font-mono", children: "/^send/i" }), tI18nComplete.raw('texte45481e75a5e')] }), draft.map((row) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { value: row.match, placeholder: tI18nComplete.raw('text74ad536dca05'), variant: "popover", size: "xs", className: "flex-1 font-mono", "aria-label": tI18nComplete.raw('text5c9c672f4cd3'), disabled: frozen, onChange: (event) => setDraft((rows) => rows.map((candidate) => candidate.id === row.id
61
59
  ? { ...candidate, match: event.target.value }
@@ -217,6 +217,7 @@ export function ProviderAccordion({ projectId, canWrite = false, enabled = true,
217
217
  connectedIds,
218
218
  search,
219
219
  });
220
+ const disabledProviders = new Set(access.data?.enforced === true ? access.data.disabledProviders : []);
220
221
  const rows = ordered.map((entry) => {
221
222
  const method = providerMethod(entry);
222
223
  return {
@@ -233,7 +234,7 @@ export function ProviderAccordion({ projectId, canWrite = false, enabled = true,
233
234
  ? 'connected'
234
235
  : 'none',
235
236
  via: connectedVia(entry.id, connectedIds),
236
- disabled: access.data?.enforced === true && access.data.disabledProviders.includes(entry.id),
237
+ disabled: disabledProviders.has(entry.id),
237
238
  };
238
239
  });
239
240
  return {
@@ -235,6 +235,7 @@ export function ProviderConnect({ projectId, canWrite = false, enabled = true, s
235
235
  connectedIds,
236
236
  search,
237
237
  });
238
+ const disabledProviders = new Set(access.data?.enforced === true ? access.data.disabledProviders : []);
238
239
  const rows = ordered.map((entry) => {
239
240
  const method = providerMethod(entry);
240
241
  return {
@@ -252,7 +253,7 @@ export function ProviderConnect({ projectId, canWrite = false, enabled = true, s
252
253
  ? 'connected'
253
254
  : 'none',
254
255
  via: connectedVia(entry.id, connectedIds),
255
- disabled: access.data?.enforced === true && access.data.disabledProviders.includes(entry.id),
256
+ disabled: disabledProviders.has(entry.id),
256
257
  };
257
258
  });
258
259
  return {
@@ -56,7 +56,7 @@ function TelegramConnectForm({ projectId, onDone }) {
56
56
  function TelegramUsersForm({ projectId, initial, onDone, }) {
57
57
  const t = useTranslations('hardcodedUi.i18nComplete');
58
58
  const save = useSetTelegramAllowedUsers();
59
- const [users, setUsers] = useState(initial.join(', '));
59
+ const [users, setUsers] = useState(() => initial.join(', '));
60
60
  const ids = parseTelegramUserIds(users);
61
61
  return (_jsxs("form", { className: "space-y-4", onSubmit: (event) => {
62
62
  event.preventDefault();
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useTranslations as useI18nTranslations } from '../../i18n/index.js';
4
- import { useEffect, useState } from 'react';
4
+ import { useState } from 'react';
5
5
  import { Button } from '../../primitives/button.js';
6
6
  import { Disclosure, DisclosureContent, DisclosureTrigger } from '../../primitives/disclosure.js';
7
7
  import { Field, FieldDescription, FieldGroup, FieldLabel } from '../../primitives/field.js';
@@ -35,14 +35,16 @@ export function ConnectorConnectionModal({ open, idPrefix, title, initialName, i
35
35
  const [slug, setSlug] = useState(initialSlug);
36
36
  const [slugEdited, setSlugEdited] = useState(false);
37
37
  const [optionsOpen, setOptionsOpen] = useState(false);
38
- useEffect(() => {
39
- if (!open)
40
- return;
41
- setName(proposedName);
42
- setSlug(initialSlug);
43
- setSlugEdited(false);
44
- setOptionsOpen(false);
45
- }, [proposedName, initialSlug, open]);
38
+ const [resetFor, setResetFor] = useState(null);
39
+ if (!resetFor || resetFor[0] !== proposedName || resetFor[1] !== initialSlug || resetFor[2] !== open) {
40
+ setResetFor([proposedName, initialSlug, open]);
41
+ if (open) {
42
+ setName(proposedName);
43
+ setSlug(initialSlug);
44
+ setSlugEdited(false);
45
+ setOptionsOpen(false);
46
+ }
47
+ }
46
48
  const slugAvailable = isConnectorConnectionSlugAvailable(slug, existingSlugs);
47
49
  const slugInvalid = slug.length > 0 && !slugAvailable;
48
50
  const slugDescriptionId = `${idPrefix}-slug-description`;
@@ -421,14 +421,16 @@ function EmailSenderPolicyEditor({ projectId, connectorSlug, policy, canWrite =
421
421
  const [domains, setDomains] = useState(() => initial.allowedDomains.join('\n'));
422
422
  const [regex, setRegex] = useState(initial.allowedRegex ?? '');
423
423
  const [error, setError] = useState(null);
424
- useEffect(() => {
424
+ const [syncedPolicy, setSyncedPolicy] = useState(policy);
425
+ if (policy !== syncedPolicy) {
426
+ setSyncedPolicy(policy);
425
427
  const next = normalizeEmailSenderPolicy(policy);
426
428
  setRestricted(next.mode === 'restricted');
427
429
  setEmails(next.allowedEmails.join('\n'));
428
430
  setDomains(next.allowedDomains.join('\n'));
429
431
  setRegex(next.allowedRegex ?? '');
430
432
  setError(null);
431
- }, [policy]);
433
+ }
432
434
  const nextPolicy = () => ({
433
435
  mode: restricted ? 'restricted' : 'allow_all',
434
436
  allowedEmails: splitPolicyList(emails),
@@ -948,17 +950,13 @@ export function CustomConnectorForm({ projectId, emailChannelEnabled, onAdded, }
948
950
  const timer = window.setTimeout(() => setDiscoveryDraft(draft), 400);
949
951
  return () => window.clearTimeout(timer);
950
952
  }, [draft]);
951
- useEffect(() => {
952
- if (!emailChannelEnabled && draft.provider === 'channel' && draft.platform === 'email') {
953
- setDraft((current) => ({ ...current, platform: 'slack' }));
954
- }
955
- }, [draft.platform, draft.provider, emailChannelEnabled]);
956
- useEffect(() => {
957
- // Channel connectors have no OAuth2-at-creation offer.
958
- if (draft.provider === 'channel' && oauth2Selected) {
959
- setOauth2Selected(false);
960
- }
961
- }, [draft.provider, oauth2Selected]);
953
+ if (!emailChannelEnabled && draft.provider === 'channel' && draft.platform === 'email') {
954
+ setDraft((current) => ({ ...current, platform: 'slack' }));
955
+ }
956
+ // Channel connectors have no OAuth2-at-creation offer.
957
+ if (draft.provider === 'channel' && oauth2Selected) {
958
+ setOauth2Selected(false);
959
+ }
962
960
  const save = useCreateCustomConnector(projectId, onAdded);
963
961
  const discovery = useQuery({
964
962
  queryKey: ['connector-auth-discovery', projectId, discoveryDraft],