@kortix/ui 0.14.4 → 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 (228) hide show
  1. package/README.md +8 -4
  2. package/dist/access/access/access-dialog.js +8 -5
  3. package/dist/access/access/copy-row.js +2 -2
  4. package/dist/access/sharing-picker.js +2 -1
  5. package/dist/browser/app-in-browser.js +5 -3
  6. package/dist/browser/browser-panel.js +6 -6
  7. package/dist/browser/sandbox-browser-chrome.d.ts +4 -1
  8. package/dist/browser/sandbox-browser-chrome.js +4 -3
  9. package/dist/customize/agents/agent-access.d.ts +20 -0
  10. package/dist/customize/agents/agent-access.js +17 -0
  11. package/dist/customize/agents/agent-grant-pages.js +5 -6
  12. package/dist/customize/agents/agent-page.js +2 -1
  13. package/dist/customize/agents/agents-page.js +18 -1
  14. package/dist/customize/connectors/add-mcp/add-mcp-server.js +4 -2
  15. package/dist/customize/connectors/connector-routes.d.ts +3 -1
  16. package/dist/customize/connectors/connector-routes.js +18 -0
  17. package/dist/customize/connectors/connectors-page.js +23 -4
  18. package/dist/customize/connectors/detail/connector-advanced-tools.js +4 -6
  19. package/dist/customize/providers/provider-accordion.js +2 -1
  20. package/dist/customize/providers/provider-connect.js +2 -1
  21. package/dist/customize/sections/component/telegram-channel-row.js +1 -1
  22. package/dist/customize/sections/connector-connection-modal.js +11 -9
  23. package/dist/customize/sections/connectors-view.js +11 -13
  24. package/dist/customize/sections/llm-provider/constants.d.ts +0 -1
  25. package/dist/customize/sections/llm-provider/constants.js +0 -2
  26. package/dist/customize/sections/llm-provider/models-tab-group.d.ts +1 -1
  27. package/dist/customize/sections/llm-provider/models-tab-group.js +7 -4
  28. package/dist/customize/sections/llm-provider/models-tab-row.d.ts +2 -18
  29. package/dist/customize/sections/llm-provider/models-tab-row.js +6 -30
  30. package/dist/customize/sections/llm-provider/models-tab.d.ts +0 -1
  31. package/dist/customize/sections/llm-provider/models-tab.js +1 -10
  32. package/dist/customize/sections/llm-provider/use-connected-providers.js +2 -2
  33. package/dist/customize/sections/llm-provider/utils.d.ts +1 -1
  34. package/dist/customize/sections/llm-provider/utils.js +7 -14
  35. package/dist/customize/sections/view/gateway/_shared.js +1 -2
  36. package/dist/customize/sections/view/gateway/gateway-routing.js +18 -20
  37. package/dist/customize/sections/view/sandbox-provider-coverage.d.ts +3 -18
  38. package/dist/customize/sections/view/sandbox-provider-coverage.js +7 -27
  39. package/dist/customize/settings/rail-copy-context.js +5 -3
  40. package/dist/customize/settings/tabs/sandbox-tab.d.ts +2 -27
  41. package/dist/customize/settings/tabs/sandbox-tab.js +13 -112
  42. package/dist/customize/settings/tabs/sandbox-template-form.js +9 -12
  43. package/dist/customize/shared/capability-tabs.js +2 -1
  44. package/dist/customize/shared/entity/entity-modal.d.ts +5 -1
  45. package/dist/customize/shared/entity/entity-modal.js +7 -4
  46. package/dist/customize/skills/remove-skill-button.d.ts +16 -0
  47. package/dist/customize/skills/remove-skill-button.js +42 -0
  48. package/dist/customize/skills/skills-page.js +4 -6
  49. package/dist/customize/triggers/schedule/event-trigger-copy.js +3 -2
  50. package/dist/customize/triggers/schedule/schedule-builder.js +7 -4
  51. package/dist/customize/triggers/schedule/trigger-composer.js +5 -2
  52. package/dist/customize/triggers/schedule/trigger-list.d.ts +0 -2
  53. package/dist/customize/triggers/schedule/trigger-list.js +1 -18
  54. package/dist/customize/triggers/schedule/trigger-tile.d.ts +1 -32
  55. package/dist/customize/triggers/schedule/trigger-tile.js +1 -16
  56. package/dist/customize/triggers/schedule/when-event.js +1 -1
  57. package/dist/diff/diff-view.js +9 -10
  58. package/dist/files/editor/code-editor.js +2 -3
  59. package/dist/files/project/components/change-request-detail-dialog.d.ts +3 -1
  60. package/dist/files/project/components/change-request-detail-dialog.js +3 -5
  61. package/dist/files/project/components/change-request-status-badge.d.ts +12 -0
  62. package/dist/files/project/components/change-request-status-badge.js +27 -0
  63. package/dist/files/project/components/change-requests-panel.js +1 -1
  64. package/dist/files/project/components/drive-explorer.js +2 -1
  65. package/dist/files/project/components/drive-grid-view.js +11 -7
  66. package/dist/files/project/components/drive-list-view.js +4 -4
  67. package/dist/files/project/components/file-breadcrumbs.js +1 -1
  68. package/dist/files/project/components/file-explorer-page.js +1 -1
  69. package/dist/files/project/components/file-history-popover.js +1 -6
  70. package/dist/files/project/components/file-search.js +6 -1
  71. package/dist/files/project/components/open-change-request-dialog.js +2 -1
  72. package/dist/files/project/components/version-selector-logic.d.ts +11 -0
  73. package/dist/files/project/components/version-selector-logic.js +26 -0
  74. package/dist/files/project/components/version-selector.js +11 -20
  75. package/dist/files/project/hooks/use-change-requests.d.ts +14 -1
  76. package/dist/files/project/hooks/use-change-requests.js +44 -25
  77. package/dist/files/project/hooks/use-server-health.js +17 -15
  78. package/dist/files/renderers/docx/docx-viewer.js +4 -6
  79. package/dist/files/renderers/image-renderer.js +6 -24
  80. package/dist/files/renderers/pdf/pdf-renderer.d.ts +3 -1
  81. package/dist/files/renderers/pdf/pdf-renderer.js +2 -2
  82. package/dist/files/renderers/pdf/pdf-viewer.d.ts +3 -0
  83. package/dist/files/renderers/pdf/pdf-viewer.js +16 -7
  84. package/dist/files/renderers/presentation/FullScreenPresentationViewer.js +2 -11
  85. package/dist/files/renderers/show-content-renderer.js +1 -1
  86. package/dist/files/renderers/sqlite-renderer.js +8 -3
  87. package/dist/files/renderers/xlsx/xlsx-viewer.js +15 -16
  88. package/dist/files/tree/components/file-tree-item.js +1 -1
  89. package/dist/files/viewer/file-content-renderer.d.ts +4 -1
  90. package/dist/files/viewer/file-content-renderer.js +2 -2
  91. package/dist/files/viewer/file-preview-modal.js +1 -1
  92. package/dist/files/viewer/use-file-content-state.js +1 -1
  93. package/dist/i18n/en.d.ts +36 -35
  94. package/dist/i18n/en.js +45 -44
  95. package/dist/index.d.ts +1 -1
  96. package/dist/internal/desktop/desktop-back-button.js +5 -3
  97. package/dist/internal/hooks/use-auto-scroll.js +5 -3
  98. package/dist/internal/hooks/use-notification-center.js +21 -15
  99. package/dist/internal/hooks/use-review-session-summary.d.ts +6 -0
  100. package/dist/internal/hooks/use-review-session-summary.js +11 -0
  101. package/dist/internal/lib/browser-error-noise.d.ts +1 -1
  102. package/dist/internal/lib/browser-error-noise.js +1 -1
  103. package/dist/internal/lib/browser-noise/rules/rejections.d.ts +10 -0
  104. package/dist/internal/lib/browser-noise/rules/rejections.js +27 -1
  105. package/dist/internal/lib/provider-branding.js +14 -3
  106. package/dist/internal/lib/trigger-clock.d.ts +42 -0
  107. package/dist/internal/lib/trigger-clock.js +34 -0
  108. package/dist/internal/lib/web-notifications.d.ts +6 -0
  109. package/dist/internal/lib/web-notifications.js +57 -7
  110. package/dist/internal/notifications/notification-bell.d.ts +13 -0
  111. package/dist/internal/notifications/notification-bell.js +1 -1
  112. package/dist/internal/notifications/notification-rows.js +2 -1
  113. package/dist/internal/outcomes/outcome-types.d.ts +5 -0
  114. package/dist/internal/reminders/reminder-filter.d.ts +12 -0
  115. package/dist/internal/reminders/reminder-filter.js +34 -0
  116. package/dist/internal/reminders/reminder-format.d.ts +6 -0
  117. package/dist/internal/reminders/reminder-format.js +20 -2
  118. package/dist/internal/stores/kortix-computer-store.d.ts +1 -1
  119. package/dist/internal/stores/session-hover-store.d.ts +20 -5
  120. package/dist/internal/stores/session-hover-store.js +31 -13
  121. package/dist/internal/stores/session-panel-memory-store.js +2 -0
  122. package/dist/markdown/code/inline-code.js +8 -0
  123. package/dist/markdown/code/output-paths-context.d.ts +18 -0
  124. package/dist/markdown/code/output-paths-context.js +35 -0
  125. package/dist/markdown/genui/components/charts.js +6 -5
  126. package/dist/primitives/count-badge.d.ts +3 -1
  127. package/dist/primitives/count-badge.js +3 -3
  128. package/dist/primitives/glyph-paths-source.js +1 -1
  129. package/dist/primitives/glyph-paths.generated.js +1 -1
  130. package/dist/primitives/map.js +2 -1
  131. package/dist/primitives/preview-image.js +4 -2
  132. package/dist/primitives/textarea.js +1 -1
  133. package/dist/primitives/typed-title.js +12 -14
  134. package/dist/primitives/typing-animation.js +4 -2
  135. package/dist/providers/ui-provider.d.ts +57 -0
  136. package/dist/providers/ui-provider.js +4 -0
  137. package/dist/session/composer/ComposerCard.d.ts +2 -0
  138. package/dist/session/composer/ComposerCard.js +5 -1
  139. package/dist/session/composer/agent-selector.js +17 -5
  140. package/dist/session/composer/composer-chat-input.js +5 -3
  141. package/dist/session/composer/composer-logic.js +8 -7
  142. package/dist/session/composer/composer-toolbar.js +4 -2
  143. package/dist/session/composer/composer-underbar.js +3 -1
  144. package/dist/session/composer/composer.js +34 -5
  145. package/dist/session/composer/draft/composer-draft.js +4 -4
  146. package/dist/session/composer/menus/slash-actions.d.ts +22 -2
  147. package/dist/session/composer/menus/slash-actions.js +20 -0
  148. package/dist/session/composer/menus/slash-items.js +19 -3
  149. package/dist/session/composer/pasted-text.js +3 -2
  150. package/dist/session/composer/stores/queued-draft-store.js +3 -2
  151. package/dist/session/header/reminder-parts.d.ts +294 -0
  152. package/dist/session/header/reminder-parts.js +120 -0
  153. package/dist/session/header/session-reminders-indicator.d.ts +13 -0
  154. package/dist/session/header/session-reminders-indicator.js +142 -35
  155. package/dist/session/media/sandbox-url-detector.js +0 -7
  156. package/dist/session/models/model-availability.d.ts +23 -0
  157. package/dist/session/models/model-availability.js +20 -0
  158. package/dist/session/models/model-grouping.js +1 -4
  159. package/dist/session/models/model-selector.js +23 -14
  160. package/dist/session/models/use-model-connection-gate.js +4 -1
  161. package/dist/session/outcomes/change-request-outcomes.js +24 -3
  162. package/dist/session/outcomes/session-outcomes-provider.d.ts +3 -1
  163. package/dist/session/outcomes/session-outcomes-provider.js +31 -3
  164. package/dist/session/overrides/provider-secret-pool-editor.js +2 -1
  165. package/dist/session/overrides/session-overrides-composer.d.ts +1 -1
  166. package/dist/session/overrides/session-overrides-composer.js +5 -1
  167. package/dist/session/panel/advanced/advanced-panel.js +3 -7
  168. package/dist/session/panel/easy/app-preview.js +12 -2
  169. package/dist/session/panel/easy/easy-panel-logic.d.ts +1 -1
  170. package/dist/session/panel/easy/easy-panel-logic.js +1 -1
  171. package/dist/session/panel/easy/outputs-card.js +2 -1
  172. package/dist/session/panel/easy/panel-card.js +5 -3
  173. package/dist/session/panel/easy/preview-load.d.ts +14 -0
  174. package/dist/session/panel/easy/preview-load.js +15 -0
  175. package/dist/session/panel/easy/step-detail-body.js +2 -7
  176. package/dist/session/panel/mobile-tool-drawer.js +4 -2
  177. package/dist/session/panel/open-session-quick-view.js +2 -1
  178. package/dist/session/panel/panel-fallback.js +8 -4
  179. package/dist/session/panel/session-panel-provider.d.ts +1 -0
  180. package/dist/session/panel/session-panel-provider.js +19 -3
  181. package/dist/session/panel/session-reminders-panel.d.ts +4 -0
  182. package/dist/session/panel/session-reminders-panel.js +109 -0
  183. package/dist/session/panel/shared/use-panel-derivations.js +6 -4
  184. package/dist/session/panel/shared/use-panel-handoffs.d.ts +2 -2
  185. package/dist/session/panel/shared/use-panel-handoffs.js +5 -1
  186. package/dist/session/panel/use-session-panel-layout.js +7 -5
  187. package/dist/session/print/session-print-header.d.ts +0 -10
  188. package/dist/session/print/session-print-header.js +25 -9
  189. package/dist/session/status/session-surface.js +1 -1
  190. package/dist/session/thread/optimistic-turn.d.ts +5 -1
  191. package/dist/session/thread/optimistic-turn.js +8 -6
  192. package/dist/session/thread/session-chat.d.ts +6 -1
  193. package/dist/session/thread/session-chat.js +34 -12
  194. package/dist/session/thread/session-history.js +3 -1
  195. package/dist/session/thread/session-thread.d.ts +10 -1
  196. package/dist/session/thread/session-thread.js +2 -2
  197. package/dist/session/thread/transcript.js +13 -7
  198. package/dist/session/tool/shared/file-list.js +19 -2
  199. package/dist/session/tool/shared/infrastructure-contexts.d.ts +7 -0
  200. package/dist/session/tool/shared/infrastructure-contexts.js +7 -0
  201. package/dist/session/tool/shared/infrastructure-output.js +9 -5
  202. package/dist/session/tool/shared/infrastructure-row-header.js +9 -1
  203. package/dist/session/tool/shared/infrastructure-rows.js +7 -5
  204. package/dist/session/tool/tools/bash-tool.js +10 -6
  205. package/dist/session/tool/tools/web-search-tool.js +25 -3
  206. package/dist/session/turn/burst-step-body.js +4 -0
  207. package/dist/session/turn/recovered-failures.d.ts +8 -0
  208. package/dist/session/turn/recovered-failures.js +32 -0
  209. package/dist/session/turn/streaming-cadence.js +5 -8
  210. package/dist/styles.css +1 -1
  211. package/dist/terminal/session-terminal-panel.js +3 -5
  212. package/dist/theme/theme.js +2 -1
  213. package/dist/workspace/projects/modal/github-import-pickers.js +7 -6
  214. package/dist/workspace/review/review-center.js +13 -10
  215. package/dist/workspace/session-workspace.js +5 -9
  216. package/dist/workspace/sessions/deleted-session-row.js +5 -2
  217. package/dist/workspace/sidebar/footer/project-computer-nav.js +6 -4
  218. package/dist/workspace/sidebar/footer/project-mcp-nav.js +6 -2
  219. package/dist/workspace/sidebar/modal/rename-session-modal.js +8 -3
  220. package/dist/workspace/sidebar/project-session-list.js +52 -55
  221. package/dist/workspace/sidebar/session-brief-hover-card.d.ts +35 -18
  222. package/dist/workspace/sidebar/session-brief-hover-card.js +114 -37
  223. package/dist/workspace/sidebar/session-status-mark.d.ts +15 -1
  224. package/dist/workspace/sidebar/session-status-mark.js +63 -17
  225. package/package.json +7 -3
  226. package/skills/kortix-ui/SKILL.md +1 -1
  227. package/dist/customize/sections/view/sandbox-provider-result.d.ts +0 -47
  228. package/dist/customize/sections/view/sandbox-provider-result.js +0 -79
@@ -1,44 +1,128 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  /**
4
4
  * Header chip for this session's reminders (scheduled check-ins that re-prompt
5
- * the session). Renders nothing until the session has a reminder that can
6
- * still fire; then an alarm with the active count, and a popover to pause,
7
- * resume, or remove one, or to open the Customize Reminders tab on this
8
- * session. The session reminders read needs `project.session.start`, so a
9
- * read-only viewer's 403 simply leaves the chip hidden. The tab link shows only
10
- * to a caller who has the tab (`capabilityAccess()`): a plain member manages
11
- * their reminders here.
5
+ * the session), Paper R08 "Countdown rings". Renders nothing until the session
6
+ * has a reminder that can still fire; then `ReminderMark`, and a popover that
7
+ * lists each reminder behind a ring of how much of its wait has passed. On a
8
+ * pointer that hovers, a row's details and actions open in a hover card on the
9
+ * popover's left; on a touch screen the pause and remove buttons sit in the row.
10
+ * Past `REMINDERS_POPOVER_MAX` the popover would be a wall to scroll, so the
11
+ * chip opens the panel's Reminders view instead (`session-reminders-panel.tsx`).
12
+ * The session reminders read needs `project.session.start`, so a read-only
13
+ * viewer's 403 simply leaves the chip hidden. The "Open in Reminders" link
14
+ * shows only to a caller who has the tab (`capabilityAccess()`).
12
15
  */
13
16
  import { HoverPrefetchLink } from '../../internal/components/hover-prefetch-link.js';
14
- import { Badge } from '../../primitives/badge.js';
15
17
  import { Button } from '../../primitives/button.js';
16
- import { ConfirmDialog } from '../../primitives/confirm-dialog.js';
17
- import Hint from '../../primitives/hint.js';
18
+ import { CountBadge } from '../../primitives/count-badge.js';
19
+ import { FLOATING_PANEL_SURFACE } from '../../primitives/menu-recipe.js';
18
20
  import { Popover, PopoverContent, PopoverTrigger } from '../../primitives/popover.js';
19
- import { errorToast, successToast } from '../../primitives/toast.js';
20
21
  import { remindersHref } from '../../internal/lib/capability-tab-routes.js';
21
22
  import { useCapabilityAccess } from '../../internal/layout/project-settings-nav.js';
22
- import { formatFireTime, reminderTitle, } from '../../internal/reminders/reminder-format.js';
23
+ import { BELL_REFLECTION_MASK } from '../../internal/notifications/notification-bell.js';
24
+ import { badgeCount } from '../../internal/notifications/notification-rows.js';
25
+ import { reminderTitle } from '../../internal/reminders/reminder-format.js';
23
26
  import { useNow, useRefetchAfterFire, } from '../../internal/reminders/use-refetch-after-fire.js';
24
- import { useLocale, useTranslations } from '../../i18n/index.js';
25
- import { qk, useFeatureFlag, useSessionReminders, useSessionWorking } from '@kortix/sdk/react';
27
+ import { cn } from '../../internal/lib/utils.js';
28
+ import { useTranslations } from '../../i18n/index.js';
29
+ import { qk, useFeatureFlag, useSessionWorking } from '@kortix/sdk/react';
26
30
  import { AlarmIcon, PauseIcon, PlayIcon, TrashIcon } from '@phosphor-icons/react';
27
31
  import { useQueryClient } from '@tanstack/react-query';
28
32
  import { useSessionIds } from '../../providers/session-ids.js';
29
- import { useEffect, useRef, useState } from 'react';
33
+ import { useEffect, useLayoutEffect, useRef, useState } from 'react';
34
+ import { openSessionQuickView } from '../panel/open-session-quick-view.js';
35
+ import { CountdownRing, isFailedReminder, REMINDER_ROW_GRID, ReminderActions, ReminderFacts, ReminderRowCells, sortReminders, useReminderControls, useReminderView, } from './reminder-parts.js';
36
+ /** More live reminders than this open in the side panel, not the popover. */
37
+ export const REMINDERS_POPOVER_MAX = 30;
38
+ /**
39
+ * The clock in `BellMark`'s badge-glow style: the glyph, then again in the
40
+ * count's colour under the bell's corner mask, so the colour reaches in from
41
+ * the corner the count sits on. Purple for active reminders, red while one's
42
+ * last run failed. With nothing active the glyph is muted and has no count.
43
+ */
44
+ export function ReminderMark({ count, failed: anyFailed, surface = 'bg-background', }) {
45
+ const tone = anyFailed
46
+ ? { reflection: 'text-kortix-red', disc: 'bg-kortix-red' }
47
+ : { reflection: 'text-kortix-purple', disc: 'bg-kortix-purple' };
48
+ return (_jsxs("span", { className: "relative size-4 shrink-0", children: [_jsx(AlarmIcon, { className: cn('size-4', count === 0 && 'text-muted-foreground') }), count > 0 ? (_jsxs(_Fragment, { children: [_jsx(AlarmIcon, { "aria-hidden": true, className: cn('absolute inset-0 size-4', tone.reflection), style: { maskImage: BELL_REFLECTION_MASK, WebkitMaskImage: BELL_REFLECTION_MASK } }), _jsx("span", { "aria-hidden": true, className: cn('absolute -right-1.5 -bottom-1.5 flex rounded-full p-px', surface), children: _jsx("span", { "data-slot": "reminder-count", className: cn('text-background flex h-3 min-w-3 items-center justify-center rounded-full px-0.5 text-[9px] leading-none font-semibold tabular-nums', // audit:allow a numeral in an 11px disc on a 16px glyph; no type rung fits
49
+ tone.disc), children: badgeCount(count) }) })] })) : null] }));
50
+ }
51
+ const CARD_OPEN_DELAY_MS = 200;
52
+ /** While open, a move to another row waits this long; a newer row restarts it. */
53
+ const CARD_MOVE_DEBOUNCE_MS = 50;
54
+ const CARD_CLOSE_DELAY_MS = 100;
55
+ const CARD_EDGE_PX = 8;
56
+ /**
57
+ * The reminder card's position beside the popover. The first hover opens it
58
+ * after a delay; while it is open, hovering another row moves it there after a
59
+ * short debounce, so a sweep across rows moves it once, and it glides (no glide
60
+ * and no debounce for keyboard focus; no glide under reduced motion).
61
+ */
62
+ function useGlidingCard() {
63
+ const panelRef = useRef(null);
64
+ const cardRef = useRef(null);
65
+ const [active, setActive] = useState(null);
66
+ const openTimer = useRef(undefined);
67
+ const closeTimer = useRef(undefined);
68
+ const clearTimers = () => {
69
+ window.clearTimeout(openTimer.current);
70
+ window.clearTimeout(closeTimer.current);
71
+ };
72
+ useEffect(() => clearTimers, []);
73
+ // Before paint: keep the card inside the viewport, then place it.
74
+ useLayoutEffect(() => {
75
+ const panel = panelRef.current;
76
+ const el = cardRef.current;
77
+ if (!active || !panel || !el)
78
+ return;
79
+ const panelTop = panel.getBoundingClientRect().top;
80
+ const max = window.innerHeight - CARD_EDGE_PX - panelTop - el.offsetHeight;
81
+ const top = Math.max(CARD_EDGE_PX - panelTop, Math.min(active.top, max));
82
+ el.style.transform = `translateY(${top}px)`;
83
+ }, [active]);
84
+ const show = (id, row, keyboard) => {
85
+ const panel = panelRef.current;
86
+ if (!panel)
87
+ return;
88
+ clearTimers();
89
+ const panelRect = panel.getBoundingClientRect();
90
+ const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
91
+ const place = (glide) => setActive({
92
+ id,
93
+ top: row.getBoundingClientRect().top - panelRect.top,
94
+ // 288px card + 12px bridge; the right side only when the left has no room.
95
+ side: panelRect.left >= 300 + CARD_EDGE_PX ? 'left' : 'right',
96
+ glide,
97
+ });
98
+ if (keyboard)
99
+ place(false);
100
+ else if (active)
101
+ openTimer.current = window.setTimeout(() => place(!reduce), CARD_MOVE_DEBOUNCE_MS);
102
+ else
103
+ openTimer.current = window.setTimeout(() => place(false), CARD_OPEN_DELAY_MS);
104
+ };
105
+ const leave = () => {
106
+ clearTimers();
107
+ closeTimer.current = window.setTimeout(() => setActive(null), CARD_CLOSE_DELAY_MS);
108
+ };
109
+ const close = () => {
110
+ clearTimers();
111
+ setActive(null);
112
+ };
113
+ return { panelRef, cardRef, active, show, leave, close, cancelClose: clearTimers };
114
+ }
30
115
  export function SessionRemindersIndicator({ runtimeSessionId }) {
31
116
  const t = useTranslations('reminders');
32
- const locale = useLocale();
33
117
  // useSessionIds: the project and the Kortix session reminders key on.
34
118
  const { projectId, sessionId } = useSessionIds();
35
119
  const gate = useFeatureFlag(projectId, 'reminders');
36
- const reminders = useSessionReminders(gate.enabled ? projectId : null, sessionId);
120
+ const { reminders, controls, confirm } = useReminderControls(gate.enabled ? projectId : null, sessionId);
37
121
  useRefetchAfterFire(reminders.data?.reminders, reminders.refetch);
38
122
  const now = useNow();
39
123
  const hasTab = useCapabilityAccess(projectId).tabs.includes('reminders');
40
- // Null when the host serves no customize pages: no "Manage" link.
41
- const manageHref = hasTab ? remindersHref(projectId) : null;
124
+ // Null when the host serves no customize pages: no "Open in Reminders" link.
125
+ const manageHref = hasTab ? `${remindersHref(projectId)}?session=${sessionId}` : null;
42
126
  // An agent sets reminders mid-turn with `kortix remind`; nothing announces
43
127
  // that to this query. The turn ending is the moment to look again.
44
128
  const working = useSessionWorking(projectId ?? '', sessionId ?? '', {
@@ -56,23 +140,46 @@ export function SessionRemindersIndicator({ runtimeSessionId }) {
56
140
  wasWorking.current = isWorking;
57
141
  }, [isWorking, projectId, queryClient]);
58
142
  const [open, setOpen] = useState(false);
59
- const [removing, setRemoving] = useState(null);
143
+ const { panelRef, cardRef, active: activeCard, show: showCard, leave: leaveCard, close: closeCard, cancelClose } = useGlidingCard();
144
+ const view = useReminderView(now);
60
145
  const live = (reminders.data?.reminders ?? []).filter((reminder) => reminder.state !== 'done');
61
146
  if (live.length === 0)
62
147
  return null;
63
148
  const activeCount = live.filter((reminder) => reminder.state === 'active').length;
64
- const setEnabled = (reminder, enabled) => reminders.update.mutate({ reminderId: reminder.id, enabled }, {
65
- onSuccess: () => successToast(enabled ? t('resumed') : t('paused')),
66
- onError: (error) => errorToast(error instanceof Error ? error.message : t('updateFailed')),
67
- });
68
- return (_jsxs(_Fragment, { children: [_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "icon", "aria-label": t('chipLabel', { count: activeCount }), className: "relative", "data-testid": "session-reminders-chip", children: [_jsx(AlarmIcon, { className: "size-4" }), activeCount > 0 ? (_jsx(Badge, { variant: "secondary", size: "tabular", className: "absolute -top-1 -right-1", children: activeCount })) : null] }) }), _jsxs(PopoverContent, { align: "end", sideOffset: 8, className: "w-80 overflow-hidden p-0", children: [_jsx("div", { className: "border-b px-4 pt-4 pb-3", children: _jsx("h3", { className: "text-foreground text-sm font-medium", children: t('chipTitle') }) }), _jsx("ul", { className: "max-h-64 divide-y overflow-auto", children: live.map((reminder) => (_jsxs("li", { className: "flex items-center gap-2 px-4 py-2.5", children: [_jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [_jsx("p", { className: "text-foreground truncate text-sm", title: reminder.prompt, children: reminderTitle(reminder) }), _jsx("p", { className: "text-muted-foreground truncate text-xs", children: reminder.state === 'active' && reminder.next_fire_at
69
- ? t('nextFire', { time: formatFireTime(reminder.next_fire_at, locale, now) })
70
- : t('pausedLabel') })] }), _jsx(Hint, { label: reminder.state === 'active' ? t('pause') : t('resume'), children: _jsx(Button, { variant: "ghost", size: "icon", "aria-label": reminder.state === 'active' ? t('pause') : t('resume'), disabled: reminders.update.isPending, onClick: () => setEnabled(reminder, reminder.state !== 'active'), children: reminder.state === 'active' ? (_jsx(PauseIcon, { className: "size-4 shrink-0" })) : (_jsx(PlayIcon, { className: "size-4 shrink-0" })) }) }), _jsx(Hint, { label: t('remove'), children: _jsx(Button, { variant: "ghost", size: "icon", "aria-label": t('remove'), onClick: () => setRemoving(reminder), children: _jsx(TrashIcon, { className: "size-4 shrink-0" }) }) })] }, reminder.id))) }), manageHref ? (_jsx("div", { className: "border-t px-4 py-2", children: _jsx(Button, { asChild: true, variant: "ghost", size: "sm", className: "w-full justify-center", children: _jsx(HoverPrefetchLink, { href: `${manageHref}?session=${sessionId}`, onClick: () => setOpen(false), children: t('manage') }) }) })) : null] })] }), _jsx(ConfirmDialog, { open: !!removing, onOpenChange: (next) => !next && setRemoving(null), title: t('removeTitle'), description: t('removeDescription'), confirmLabel: t('remove'), confirmVariant: "destructive", isPending: reminders.remove.isPending, onConfirm: () => removing &&
71
- reminders.remove.mutate(removing.id, {
72
- onSuccess: () => {
73
- successToast(t('removed'));
74
- setRemoving(null);
75
- },
76
- onError: (error) => errorToast(error instanceof Error ? error.message : t('updateFailed')),
77
- }) })] }));
149
+ const rows = sortReminders(live);
150
+ const mark = _jsx(ReminderMark, { count: activeCount, failed: live.some(isFailedReminder) });
151
+ if (live.length > REMINDERS_POPOVER_MAX) {
152
+ return (_jsx(Button, { variant: "ghost", size: "icon", "aria-label": t('chipLabel', { count: activeCount }), className: "relative", "data-testid": "session-reminders-chip", onClick: () => openSessionQuickView('reminders', 'chip'), children: mark }));
153
+ }
154
+ const shown = activeCard ? rows.find((reminder) => reminder.id === activeCard?.id) : undefined;
155
+ return (_jsxs(_Fragment, { children: [_jsxs(Popover, { open: open, onOpenChange: (next) => {
156
+ setOpen(next);
157
+ if (!next)
158
+ closeCard();
159
+ }, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", "aria-label": t('chipLabel', { count: activeCount }), className: "relative", "data-testid": "session-reminders-chip", children: mark }) }), _jsxs(PopoverContent, { ref: panelRef, align: "end", sideOffset: 8, className: "relative w-96 p-0", children: [_jsxs("div", { className: "flex items-center justify-between gap-2 px-4 pt-3 pb-1", children: [_jsxs("h3", { className: "text-foreground flex items-center gap-2 text-sm font-medium", children: [t('title'), activeCount > 0 ? _jsx(CountBadge, { count: activeCount, tone: "reminder" }) : null] }), manageHref ? (_jsx(Button, { asChild: true, variant: "ghost", size: "xs", className: "text-muted-foreground -mr-2.5", children: _jsx(HoverPrefetchLink, { href: manageHref, onClick: () => setOpen(false), children: t('cardManage') }) })) : null] }), _jsx("ul", { className: "max-h-96 overflow-y-auto p-1", onPointerLeave: leaveCard, onScroll: closeCard, children: rows.map((reminder) => {
160
+ const v = view(reminder);
161
+ return (_jsx("li", { children: _jsxs("div", { tabIndex: 0, "data-testid": "session-reminder-row", "data-active": activeCard?.id === reminder.id || undefined, onPointerEnter: (event) => {
162
+ if (event.pointerType !== 'touch')
163
+ showCard(reminder.id, event.currentTarget, false);
164
+ }, onFocus: (event) => {
165
+ if (event.currentTarget.matches(':focus-visible'))
166
+ showCard(reminder.id, event.currentTarget, true);
167
+ }, className: cn(REMINDER_ROW_GRID, 'hover:bg-hover focus-visible:ring-ring data-active:bg-hover rounded-lg px-2.5 py-1.5 outline-none focus-visible:ring-2'), children: [_jsx(ReminderRowCells, { reminder: reminder, view: v }), _jsxs("span", { className: "col-start-3 row-span-2 row-start-1 hidden items-center pointer-coarse:flex", children: [_jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": v.pauseLabel, disabled: controls.pending, onClick: () => controls.setEnabled(reminder, v.paused), children: v.paused ? _jsx(PlayIcon, { className: "size-4" }) : _jsx(PauseIcon, { className: "size-4" }) }), _jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": t('remove'), onClick: () => controls.requestRemove(reminder), children: _jsx(TrashIcon, { className: "size-4" }) })] })] }) }, reminder.id));
168
+ }) }), shown && activeCard
169
+ ? (() => {
170
+ const { paused, next, nextAt, countdown, share, tone } = view(shown);
171
+ const [major, minor] = countdown;
172
+ return (
173
+ // One card for the whole list: hovering the next row moves it
174
+ // instead of closing one card and opening another. The padding
175
+ // on the popover side bridges the gap, so the pointer never
176
+ // leaves on its way into the card.
177
+ _jsx("div", { ref: cardRef, onPointerEnter: cancelClose, onPointerLeave: leaveCard, className: cn('absolute top-0 starting:opacity-0 pointer-coarse:hidden', activeCard.side === 'left' ? 'right-full pr-3' : 'left-full pl-3'), style: {
178
+ transition: activeCard.glide
179
+ ? 'transform var(--duration-normal) var(--ease-in-out)'
180
+ : 'opacity var(--duration-fast) var(--ease-out)',
181
+ }, children: _jsxs("div", { "data-testid": "session-reminder-card", className: cn(FLOATING_PANEL_SURFACE, 'w-72 overflow-hidden'), children: [_jsxs("div", { className: "flex items-center gap-3 px-4 pt-4 pb-3", children: [_jsx(CountdownRing, { share: share, size: 44, stroke: 3, tone: tone, paused: paused, children: !paused && Number.isFinite(nextAt) ? (_jsxs("span", { className: "flex flex-col items-center leading-none tabular-nums", children: [_jsx("span", { className: "text-foreground text-xs font-medium", children: major }), minor ? (_jsx("span", { className: "text-muted-foreground mt-0.5 text-[10px]" // audit:allow the minor unit inside a 44px ring
182
+ , children: minor })) : null] })) : null }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "text-foreground text-sm font-medium text-pretty", children: reminderTitle(shown) }), _jsx("p", { className: "text-muted-foreground mt-0.5 text-xs", children: next ? t('nextRun', { time: next }) : t('pausedLabel') })] })] }), shown.name ? (_jsx("p", { className: "text-muted-foreground line-clamp-4 px-4 pb-3 text-xs text-pretty whitespace-pre-line", children: shown.prompt })) : null, _jsx(ReminderFacts, { reminder: shown, now: now, className: "border-t px-4 py-3" }), _jsx(ReminderActions, { reminder: shown, controls: controls, manageHref: manageHref, onNavigate: () => setOpen(false), className: "border-t p-2" })] }) }));
183
+ })()
184
+ : null] })] }), confirm] }));
78
185
  }
@@ -102,7 +102,6 @@ function InlineIframePreview({ proxyUrl, port }) {
102
102
  }
103
103
  function SandboxUrlChip({ detected, proxyUrl, }) {
104
104
  const tHardcodedUi = useTranslations('hardcodedUi');
105
- const [copied, setCopied] = useState(false);
106
105
  const tabId = `preview:${detected.port}`;
107
106
  const tabHref = `/p/${detected.port}`;
108
107
  const internalUrl = toInternalUrl(detected.port, detected.path);
@@ -120,12 +119,6 @@ function SandboxUrlChip({ detected, proxyUrl, }) {
120
119
  }),
121
120
  }, undefined, shallowNavigate);
122
121
  }, [tabId, tHardcodedUi, detected.port, tabHref, proxyUrl, internalUrl, shallowNavigate]);
123
- const handleCopyUrl = useCallback(() => {
124
- navigator.clipboard.writeText(proxyUrl).then(() => {
125
- setCopied(true);
126
- setTimeout(() => setCopied(false), 2000);
127
- });
128
- }, [proxyUrl]);
129
122
  const openPreview = useOpenPreviewLink();
130
123
  const handleOpenExternal = useCallback(() => {
131
124
  if (openPreview?.(null, detected.originalUrl, proxyUrl))
@@ -1,6 +1,29 @@
1
1
  import type { ModelKey } from '@kortix/sdk/react';
2
2
  export declare const NO_MODEL_AVAILABLE_MESSAGE = "No models available for this session yet.";
3
3
  export declare const NO_MODEL_AVAILABLE_ACTION_MESSAGE = "Connect a model via provider first or upgrade your Kortix subscription.";
4
+ /**
5
+ * Whether the picker may offer a model as a choice: on for the project
6
+ * (`enabled`) and runnable for the caller (`available`). `/model-picker` keeps
7
+ * a model a default names even when its provider is not connected, marked
8
+ * `available: false`, so defaults stay editable; choosing it would only fail
9
+ * after Send ("Connect Codex to use this model.", KRTX-3011).
10
+ */
11
+ export declare function isRunnableModel(model: {
12
+ enabled?: boolean;
13
+ available?: boolean;
14
+ }): boolean;
15
+ /**
16
+ * What selecting an unavailable model row opens instead of choosing it: the
17
+ * ChatGPT accounts modal for a ChatGPT model when members may connect one,
18
+ * else the provider connection. `null` = the model is runnable.
19
+ */
20
+ export declare function unavailableModelConnection(model: {
21
+ enabled?: boolean;
22
+ available?: boolean;
23
+ availability?: 'login' | 'provider';
24
+ }, options: {
25
+ chatGptAvailable: boolean;
26
+ }): 'chatgpt' | 'provider' | null;
4
27
  export declare function resolveAvailableSelectedModel(selectedModel: ModelKey | null | undefined, isSelectableModel: (model: ModelKey) => boolean): ModelKey | null;
5
28
  /**
6
29
  * The name of the selected model when it cannot read an attached image, else null.
@@ -1,5 +1,25 @@
1
1
  export const NO_MODEL_AVAILABLE_MESSAGE = 'No models available for this session yet.';
2
2
  export const NO_MODEL_AVAILABLE_ACTION_MESSAGE = 'Connect a model via provider first or upgrade your Kortix subscription.';
3
+ /**
4
+ * Whether the picker may offer a model as a choice: on for the project
5
+ * (`enabled`) and runnable for the caller (`available`). `/model-picker` keeps
6
+ * a model a default names even when its provider is not connected, marked
7
+ * `available: false`, so defaults stay editable; choosing it would only fail
8
+ * after Send ("Connect Codex to use this model.", KRTX-3011).
9
+ */
10
+ export function isRunnableModel(model) {
11
+ return model.enabled !== false && model.available !== false;
12
+ }
13
+ /**
14
+ * What selecting an unavailable model row opens instead of choosing it: the
15
+ * ChatGPT accounts modal for a ChatGPT model when members may connect one,
16
+ * else the provider connection. `null` = the model is runnable.
17
+ */
18
+ export function unavailableModelConnection(model, options) {
19
+ if (model.available !== false)
20
+ return null;
21
+ return model.availability === 'login' && options.chatGptAvailable ? 'chatgpt' : 'provider';
22
+ }
3
23
  export function resolveAvailableSelectedModel(selectedModel, isSelectableModel) {
4
24
  if (!selectedModel)
5
25
  return null;
@@ -1,5 +1,4 @@
1
- import { DEFAULT_MANAGED_MODEL_IDS, PROVIDER_LABELS } from '@kortix/llm-catalog';
2
- const MANAGED_MODEL_IDS = new Set(DEFAULT_MANAGED_MODEL_IDS);
1
+ import { PROVIDER_LABELS } from '@kortix/llm-catalog';
3
2
  // The gateway exposes its whole catalog through a single `kortix` provider, with
4
3
  // model ids namespaced as `<provider>/<model>`. For the picker we recover the
5
4
  // REAL provider: platform-managed defaults stay under the "Kortix" group, while
@@ -26,8 +25,6 @@ export function pickerGroupId(model) {
26
25
  if (model.providerID !== 'kortix') {
27
26
  return model.providerID;
28
27
  }
29
- if (MANAGED_MODEL_IDS.has(model.modelID))
30
- return model.provider ?? model.providerID;
31
28
  if (model.provider)
32
29
  return model.provider;
33
30
  const slash = model.modelID.indexOf('/');
@@ -13,7 +13,7 @@ import { useSessionIds } from '../../providers/session-ids.js';
13
13
  import { useUiComponents } from '../../providers/ui-provider.js';
14
14
  import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
15
15
  import { ModelSearchEmpty } from './model-search-empty.js';
16
- import { resolveAvailableSelectedModel } from './model-availability.js';
16
+ import { isRunnableModel, resolveAvailableSelectedModel, unavailableModelConnection, } from './model-availability.js';
17
17
  import { isPickerGroupOpen, modelItemValue, pickerGroupId, pickerGroupLabel, buildPickerSections, splitModelLabel, } from './model-grouping.js';
18
18
  import { modelInDefaultView } from '@kortix/sdk';
19
19
  import { isSubscriptionModel, pickerModelName, shouldShowFreeTag } from './model-tags.js';
@@ -50,8 +50,11 @@ import { Tag } from '../../primitives/tag.js';
50
50
  * `model.providerID`: under the gateway every model is registered as `kortix`,
51
51
  * so the model's own id would paint the Kortix mark on every row.
52
52
  */
53
- function ModelRow({ model, groupProviderID, groupProviderName, isSelected, isAccountDefault, defaultControls, onSelect, scope, showSubscriptionTag = true, }) {
53
+ function ModelRow({ model, groupProviderID, groupProviderName, isSelected, isAccountDefault, defaultControls, onSelect, scope, showSubscriptionTag = true, connectLabel, }) {
54
54
  const t = useTranslations('modelSelector');
55
+ // Kept in the list so a default stays visible, but not runnable: the row
56
+ // reads "Connect" and selecting it opens the connection (KRTX-3011).
57
+ const unavailable = model.available === false;
55
58
  const isFree = shouldShowFreeTag(model);
56
59
  const isSubscription = isSubscriptionModel(model);
57
60
  // Display name only — `model.modelName` still carries " (ChatGPT)" for search
@@ -83,7 +86,7 @@ function ModelRow({ model, groupProviderID, groupProviderName, isSelected, isAcc
83
86
  /* Stripped of the tinted tile so it reads as a mark on the row, not a
84
87
  chip — `cn` puts these last, so they win over the component's own
85
88
  `bg-zinc-*`. */
86
- className: "size-4 rounded-none bg-transparent" }), _jsxs("span", { className: "min-w-0 flex-1 truncate leading-tight", children: [_jsx("span", { className: "text-foreground font-medium", children: lead }), trail ? _jsxs("span", { className: "text-muted-foreground font-normal", children: [" ", trail] }) : null] }), isFree ? (_jsx(Tag, { variant: "free", children: t('free') })) : isSubscription && showSubscriptionTag ? (_jsx(Tag, { children: t('included') })) : null, _jsxs("span", { className: "relative flex size-6 shrink-0 items-center justify-center", children: [isSelected && (_jsx(Check, { className: "text-foreground size-4 group-hover:opacity-0 group-data-[selected=true]:opacity-0" })), defaultControls && (_jsx("button", { type: "button", "aria-label": isAccountDefault
89
+ className: "size-4 rounded-none bg-transparent" }), _jsxs("span", { className: cn('min-w-0 flex-1 truncate leading-tight', unavailable && 'opacity-60'), children: [_jsx("span", { className: "text-foreground font-medium", children: lead }), trail ? _jsxs("span", { className: "text-muted-foreground font-normal", children: [" ", trail] }) : null] }), unavailable ? (_jsx(Tag, { children: connectLabel })) : isFree ? (_jsx(Tag, { variant: "free", children: t('free') })) : isSubscription && showSubscriptionTag ? (_jsx(Tag, { children: t('included') })) : null, _jsxs("span", { className: "relative flex size-6 shrink-0 items-center justify-center", children: [isSelected && (_jsx(Check, { className: "text-foreground size-4 group-hover:opacity-0 group-data-[selected=true]:opacity-0" })), defaultControls && (_jsx("button", { type: "button", "aria-label": isAccountDefault
87
90
  ? t('defaultAria', { model: model.modelName })
88
91
  : t('setDefaultAria', { model: model.modelName }), title: isAccountDefault ? t('defaultTitle') : t('setDefaultTitle'),
89
92
  /* No `aria-pressed`: that promises a toggle, and clicking the
@@ -212,7 +215,7 @@ export function ModelSelector({ models, selectedModel, onSelect, defaultControls
212
215
  const hasAnyModel = useMemo(() => models.some((m) => m.enabled !== false), [models]);
213
216
  /** Does this member already have ChatGPT subscription models? Then the row
214
217
  * manages their accounts instead of offering to connect one. */
215
- const hasChatGptModels = useMemo(() => models.some((m) => m.enabled !== false && pickerGroupId(m) === 'codex'), [models]);
218
+ const hasChatGptModels = useMemo(() => models.some((m) => isRunnableModel(m) && pickerGroupId(m) === 'codex'), [models]);
216
219
  const grouped = useMemo(() => {
217
220
  const groups = new Map();
218
221
  for (const m of visibleModels) {
@@ -278,24 +281,30 @@ export function ModelSelector({ models, selectedModel, onSelect, defaultControls
278
281
  providerName: pickerGroupLabel(groupID, model),
279
282
  };
280
283
  }, [defaultControls?.accountDefault, visibleModels]);
281
- const handleSelect = useCallback((model) => {
282
- onSelect({ providerID: model.providerID, modelID: model.modelID });
283
- setOpen(false);
284
- }, [onSelect, setOpen]);
285
284
  const handleOpenProviderModal = useCallback((tab) => {
286
285
  setOpen(false);
287
286
  openConnectProvider(tab);
288
287
  }, [openConnectProvider, setOpen]);
289
- const handleUpgrade = useCallback(() => {
290
- setOpen(false);
291
- openUpgrade();
292
- }, [openUpgrade, setOpen]);
293
288
  const handleOpenChatGpt = useCallback(() => {
294
289
  setOpen(false);
295
290
  setChatGptOpen(true);
296
291
  }, [setOpen]);
292
+ const handleSelect = useCallback((model) => {
293
+ // An unavailable model is never chosen: the turn would fail after Send.
294
+ const connect = unavailableModelConnection(model, { chatGptAvailable });
295
+ if (connect === 'chatgpt')
296
+ return handleOpenChatGpt();
297
+ if (connect === 'provider')
298
+ return handleOpenProviderModal('providers');
299
+ onSelect({ providerID: model.providerID, modelID: model.modelID });
300
+ setOpen(false);
301
+ }, [onSelect, setOpen, chatGptAvailable, handleOpenChatGpt, handleOpenProviderModal]);
302
+ const handleUpgrade = useCallback(() => {
303
+ setOpen(false);
304
+ openUpgrade();
305
+ }, [openUpgrade, setOpen]);
297
306
  return (_jsxs(_Fragment, { children: [connectionModal, projectId && chatGptPossible && ProviderAccountsModal && (_jsx(ProviderAccountsModal, { projectId: projectId, providerId: "codex", open: chatGptOpen, onOpenChange: setChatGptOpen })), _jsxs(CommandPopover, { open: disabled ? false : open, onOpenChange: (next) => !disabled && setOpen(next), children: [_jsx(CommandPopoverTrigger, { children: _jsxs(Button, { type: "button", variant: triggerVariant, size: "sm", className: cn('rounded-lg hit-area-y-3', triggerVariant === 'ghost' && 'text-muted-foreground'), children: [_jsx("span", { className: cn('max-w-30 truncate', triggerLabelClassName), children: displayName }), _jsx(ChevronDown, { className: cn('duration-moderate size-3 transition-transform ease-out', open && 'rotate-180') })] }) }), _jsx(CommandPopoverContent, { side: "top", align: "start", sideOffset: 8, className: "w-[min(300px,calc(100vw-1.5rem))]", children: _jsxs(_Fragment, { children: [_jsx(CommandInput, { compact: true, placeholder: tModel('search'), value: search, onValueChange: setSearch, rightElement: canManageModels && canConnectProvider ? (_jsxs("div", { className: "-mr-0.5 flex shrink-0 items-center gap-0.5", children: [_jsx(Hint, { label: tModel('connectProvider'), side: "top", className: "z-50", children: _jsx("button", { type: "button", "aria-label": tModel('addProvider'), onClick: () => handleOpenProviderModal('providers'), className: "text-muted-foreground hover:text-foreground hover:bg-muted flex size-8 cursor-pointer items-center justify-center rounded-md transition-colors", children: _jsx(Plus, { className: "size-4" }) }) }), _jsx(Hint, { label: tModel('manageModels'), side: "top", className: "z-50", children: _jsx("button", { type: "button", "aria-label": tModel('manageModels'), onClick: () => handleOpenProviderModal('models'), className: "text-muted-foreground hover:text-foreground hover:bg-muted flex size-8 cursor-pointer items-center justify-center rounded-md transition-colors", children: _jsx(SlidersHorizontal, { className: "size-4" }) }) })] })) : undefined }), grouped.length > 1 && _jsx(CommandSeparator, { className: "bg-border/60" }), _jsx(CommandList, { className: "max-h-[380px]", children: modelsLoading || entitlementsPending ? (_jsx("div", { className: "flex min-h-32 items-center justify-center", role: "status", "aria-label": tModel('loading'), children: _jsx(Loading, { className: "text-muted-foreground size-4 shrink-0" }) })) : grouped.length > 0 ? (_jsxs(_Fragment, { children: [pinnedDefault && (_jsxs(_Fragment, { children: [_jsx(CommandGroup, { heading: _jsx(GroupHeading, { children: tModel('yourDefault') }), forceMount: true, children: _jsx(ModelRow, { model: pinnedDefault.model, groupProviderID: pinnedDefault.providerID, groupProviderName: pinnedDefault.providerName, isSelected: availableSelectedModel?.providerID === pinnedDefault.model.providerID &&
298
- availableSelectedModel?.modelID === pinnedDefault.model.modelID, isAccountDefault: true, defaultControls: rowDefaultControls, onSelect: handleSelect, scope: "pinned" }) }), _jsx(CommandSeparator, {})] })), sections.map((section, sectionIndex) => {
307
+ availableSelectedModel?.modelID === pinnedDefault.model.modelID, isAccountDefault: true, defaultControls: rowDefaultControls, onSelect: handleSelect, connectLabel: tPooled('connect'), scope: "pinned" }) }), _jsx(CommandSeparator, {})] })), sections.map((section, sectionIndex) => {
299
308
  const selectedInSection = section.models.find((m) => availableSelectedModel?.providerID === m.providerID &&
300
309
  availableSelectedModel?.modelID === m.modelID);
301
310
  const open = isPickerGroupOpen({
@@ -313,7 +322,7 @@ export function ModelSelector({ models, selectedModel, onSelect, defaultControls
313
322
  return (_jsxs(Fragment, { children: [sectionIndex > 0 && _jsx(CommandSeparator, {}), _jsxs(CommandGroup, { forceMount: true, children: [collapsible && (_jsxs(CommandItem, { value: `section-${section.id}`, onSelect: toggle, className: "cursor-pointer", children: [open ? (_jsx(ChevronDown, { className: "text-muted-foreground size-3.5 shrink-0" })) : (_jsx(CaretRight, { className: "text-muted-foreground size-3.5 shrink-0" })), _jsx("span", { className: "min-w-0 truncate text-sm font-medium", children: section.label }), _jsx("span", { className: "flex-1" }), section.models.some(isSubscriptionModel) && (_jsx(Tag, { children: tModel('included') })), !open && selectedInSection && (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-muted-foreground min-w-0 truncate text-xs", children: pickerModelName(selectedInSection) }), _jsx(Check, { className: "text-foreground size-4 shrink-0" })] })), !open && !selectedInSection && (_jsx("span", { className: "text-muted-foreground text-xs tabular-nums", children: section.models.length }))] })), open &&
314
323
  section.models.map((model) => (_jsx(ModelRow, { model: model, groupProviderID: section.providerID, groupProviderName: section.label, isSelected: availableSelectedModel?.providerID === model.providerID &&
315
324
  availableSelectedModel?.modelID === model.modelID, isAccountDefault: defaultControls?.accountDefault?.providerID === model.providerID &&
316
- defaultControls?.accountDefault?.modelID === model.modelID, defaultControls: rowDefaultControls, onSelect: handleSelect, scope: "model", showSubscriptionTag: false }, `${section.id}:${model.providerID}:${model.modelID}`)))] })] }, section.id));
325
+ defaultControls?.accountDefault?.modelID === model.modelID, defaultControls: rowDefaultControls, onSelect: handleSelect, connectLabel: tPooled('connect'), scope: "model", showSubscriptionTag: false }, `${section.id}:${model.providerID}:${model.modelID}`)))] })] }, section.id));
317
326
  }), chatGptAvailable && !searching && (_jsxs(_Fragment, { children: [_jsx(CommandSeparator, {}), _jsx(CommandGroup, { forceMount: true, children: _jsxs(CommandItem, { value: "chatgpt-subscription", onSelect: handleOpenChatGpt, className: "cursor-pointer", children: [_jsx(ProviderLogo, { providerID: "codex", name: "ChatGPT", size: "small", className: "size-4 rounded-none bg-transparent" }), _jsx("span", { className: "text-foreground min-w-0 flex-1 truncate text-sm font-medium", children: hasChatGptModels
318
327
  ? tPooled('manageChatGptAccounts')
319
328
  : tPooled('useYourChatGpt') }), !hasChatGptModels && (_jsx("span", { className: "text-muted-foreground text-xs", children: tPooled('connect') }))] }) })] }))] })) : hasAnyModel ? (
@@ -12,6 +12,7 @@ import { useUpgradeDialogStore } from '../../internal/stores/upgrade-dialog-stor
12
12
  import { getProject, platformConfig } from '@kortix/sdk';
13
13
  import { contract, qk } from '@kortix/sdk/react';
14
14
  import { resolveEntitlementsPending } from '../composer/entitlements-pending.js';
15
+ import { isRunnableModel } from './model-availability.js';
15
16
  /**
16
17
  * `ProviderModalTab` is the caller-facing vocabulary ('providers' | 'connected'
17
18
  * | 'models'). JAY-510 merged the modal's 'catalog' and 'connected' tabs into
@@ -98,7 +99,9 @@ export function useModelConnectionGate(models = [], options) {
98
99
  // `resolveAvailableSelectedModel` then nulled the pick, so the trigger stayed
99
100
  // on `unsetLabel`, no check mark rendered, and each click looked like a no-op
100
101
  // even though `onSelect` fired and the model store was written.
101
- const isModelOffered = useCallback((model) => model.enabled !== false, []);
102
+ // `isRunnableModel` also refuses a model `/model-picker` marks
103
+ // `available: false` (its provider is not connected for the caller).
104
+ const isModelOffered = useCallback((model) => isRunnableModel(model), []);
102
105
  const hasSelectableModels = useMemo(() => models.some(isModelOffered), [models, isModelOffered]);
103
106
  const modelsByKey = useMemo(() => new Map(models.map((model) => [`${model.providerID}:${model.modelID}`, model])), [models]);
104
107
  const isSelectableModel = useCallback((selectedModel) => {
@@ -51,6 +51,27 @@ export function changeRequestOutcomes(crs, sessionId, tI18nComplete) {
51
51
  // a server that sends `null` should degrade to the fallback sentence
52
52
  // rather than throw inside a transcript render.
53
53
  const summary = cr.description?.trim();
54
+ // An agent revises its work by closing this request and opening the next
55
+ // on the same branch. The card says so and opens the replacement
56
+ // (KRTX-3031). The card itself stays this request's: id and reference.
57
+ const replacement = cr.status === 'closed' ? (cr.superseded_by ?? null) : null;
58
+ const status = replacement
59
+ ? {
60
+ label: tI18nComplete
61
+ ? tI18nComplete('textddcd87c21894', { number: replacement.number })
62
+ : `Superseded by #${replacement.number}`,
63
+ tone: word.tone,
64
+ }
65
+ : { label: word.label, tone: word.tone };
66
+ const action = replacement
67
+ ? {
68
+ label: tI18nComplete
69
+ ? tI18nComplete('texta8ef31fa7d25', { number: replacement.number })
70
+ : `View #${replacement.number}`,
71
+ intent: 'open',
72
+ target: replacement.cr_id,
73
+ }
74
+ : { label: word.action, intent: 'open' };
54
75
  return {
55
76
  id: `cr:${cr.cr_id}`,
56
77
  kind: 'change_request',
@@ -62,7 +83,7 @@ export function changeRequestOutcomes(crs, sessionId, tI18nComplete) {
62
83
  // Kept for the modal, which has room for it. The card no longer renders
63
84
  // a description; see `outcome-card.tsx`.
64
85
  description: summary || tI18nComplete?.raw('text5c58b6d31ba9') || 'Ready for you to look over.',
65
- status: { label: word.label, tone: word.tone },
86
+ status,
66
87
  at: epoch(cr.created_at),
67
88
  // The number is a REFERENCE, not a name — it belongs in the quiet line
68
89
  // with the status. `into <base_ref>` is gone: a branch name is the one
@@ -73,8 +94,8 @@ export function changeRequestOutcomes(crs, sessionId, tI18nComplete) {
73
94
  ? tI18nComplete('texta4fc39798651', { number: cr.number })
74
95
  : `Change request #${cr.number}`,
75
96
  ],
76
- action: { label: word.action, intent: 'open' },
77
- resourceHref: `?cr=${cr.cr_id}`,
97
+ action,
98
+ resourceHref: `?cr=${replacement?.cr_id ?? cr.cr_id}`,
78
99
  };
79
100
  });
80
101
  }
@@ -1,10 +1,12 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import { type TurnSpan } from './anchor-outcomes.js';
3
3
  import type { Outcome } from '../../internal/outcomes/outcome-types.js';
4
- export declare function SessionOutcomesProvider({ projectSessionId, turnSpans, onOpen, children, }: {
4
+ export declare function SessionOutcomesProvider({ projectSessionId, turnSpans, busy, onOpen, children, }: {
5
5
  /** The git session id a change request records as `origin_session_id`. */
6
6
  projectSessionId: string | undefined;
7
7
  turnSpans: TurnSpan[];
8
+ /** The session is working. Its end re-reads the change requests. */
9
+ busy: boolean;
8
10
  onOpen: (outcome: Outcome) => void;
9
11
  children: ReactNode;
10
12
  }): import("react").JSX.Element;
@@ -38,16 +38,21 @@ import { jsx as _jsx } from "react/jsx-runtime";
38
38
  * because the hook takes its project id from that context rather than a
39
39
  * parameter. Task 5 mounts it.
40
40
  */
41
+ import { useQueryClient } from '@tanstack/react-query';
41
42
  import { useTranslations } from '../../i18n/index.js';
42
- import { createContext, useContext, useMemo } from 'react';
43
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef } from 'react';
44
+ import { useProjectContext } from '../../files/project/context.js';
43
45
  // NOT `@kortix/sdk/react`'s change-request hook. See the note above — this is
44
46
  // a cache-coherence requirement, not a style preference.
45
- import { useSessionChangeRequests } from '../../files/project/hooks/use-change-requests.js';
47
+ import { invalidateChangeRequestReads, useSessionChangeRequests, } from '../../files/project/hooks/use-change-requests.js';
46
48
  import { anchorOutcomes } from './anchor-outcomes.js';
47
49
  import { changeRequestOutcomes } from './change-request-outcomes.js';
48
50
  const EMPTY = [];
49
51
  const OutcomesContext = createContext(null);
50
- export function SessionOutcomesProvider({ projectSessionId, turnSpans, onOpen, children, }) {
52
+ /** A change request the agent opens as its turn closes can land after the
53
+ * turn-end read: read once more after this delay. */
54
+ const CHANGE_REQUEST_SETTLE_MS = 2_500;
55
+ export function SessionOutcomesProvider({ projectSessionId, turnSpans, busy, onOpen, children, }) {
51
56
  const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
52
57
  // No `projectId` argument: this hook reads it from `ProjectFilesProvider`,
53
58
  // which is what keeps it on the same cache entry the dialog invalidates.
@@ -56,6 +61,7 @@ export function SessionOutcomesProvider({ projectSessionId, turnSpans, onOpen, c
56
61
  // Only THIS session's change requests, filtered by the server; the key sits
57
62
  // under the project-files list scope the dialog's mutations invalidate.
58
63
  const { data } = useSessionChangeRequests(projectSessionId, { refetchInterval: 60_000 });
64
+ useRefreshChangeRequestsOnTurnEnd(busy);
59
65
  const all = useMemo(
60
66
  // Change requests are the only outcome a turn produces. The derived
61
67
  // pipeline (files, schedules, background tasks, connector links) was built
@@ -66,6 +72,28 @@ export function SessionOutcomesProvider({ projectSessionId, turnSpans, onOpen, c
66
72
  const value = useMemo(() => ({ byTurn, onOpen }), [byTurn, onOpen]);
67
73
  return _jsx(OutcomesContext.Provider, { value: value, children: children });
68
74
  }
75
+ /**
76
+ * The agent opens and closes change requests from its sandbox, and nothing
77
+ * tells the browser. Without this, the cards and the sidebar kept a closed
78
+ * proposal as "Review" and missed a new one until the 60 s poll or a reload
79
+ * (KRTX-3009, KRTX-3010). Only the busy -> idle edge reads: mounting an idle
80
+ * session already fetches.
81
+ */
82
+ function useRefreshChangeRequestsOnTurnEnd(busy) {
83
+ const qc = useQueryClient();
84
+ const projectId = useProjectContext()?.projectId ?? '';
85
+ const refresh = useCallback(() => invalidateChangeRequestReads(qc, projectId), [qc, projectId]);
86
+ const wasBusy = useRef(busy);
87
+ useEffect(() => {
88
+ const ended = wasBusy.current && !busy;
89
+ wasBusy.current = busy;
90
+ if (!ended)
91
+ return;
92
+ refresh();
93
+ const timer = setTimeout(refresh, CHANGE_REQUEST_SETTLE_MS);
94
+ return () => clearTimeout(timer);
95
+ }, [busy, refresh]);
96
+ }
69
97
  /** A turn's outcomes, oldest first. `EMPTY` is a module constant so a turn with
70
98
  * no outcomes gets the SAME array every render and never re-renders for it. */
71
99
  export function useTurnOutcomes(turnKey) {
@@ -57,9 +57,10 @@ function PoolChoices({ projectId, providers, providerId, onProviderChange, keys,
57
57
  const personalChatGpt = providerId === 'codex' && personalKeys;
58
58
  const available = new Set(keys.map((secret) => secret.secret_id));
59
59
  const unavailable = selected.filter((secretId) => !available.has(secretId));
60
+ const selectedIds = new Set(selected);
60
61
  return _jsxs(_Fragment, { children: [_jsxs(Field, { className: "gap-1.5", children: [_jsx(FieldLabel, { htmlFor: id, children: t('provider') }), _jsxs(Select, { value: providerId, onValueChange: onProviderChange, disabled: disabled, children: [_jsx(SelectTrigger, { id: id, children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: providers.map((provider) => _jsx(SelectItem, { value: provider, children: provider === 'codex' ? 'ChatGPT' : LLM_PROVIDER_BY_ID.get(provider)?.label ?? provider }, provider)) })] })] }), _jsx("p", { className: "text-muted-foreground text-xs", "aria-live": "polite", children: configured
61
62
  ? selected.length ? t(selected.length === 1 ? 'selectedOneForSession' : 'selectedForSession', { count: selected.length }) : t('providerDisabled')
62
- : t(personalChatGpt ? 'personalChatGptDefault' : 'projectDefault') }), unavailable.length > 0 && _jsxs("div", { className: "space-y-1", role: "status", children: [_jsx("p", { className: "text-muted-foreground text-xs", children: t('unavailableKeys', { count: unavailable.length }) }), _jsx(Button, { size: "sm", variant: "outline-ghost", disabled: disabled || readOnly, onClick: () => onChange(selected.filter((secretId) => available.has(secretId))), children: t('removeUnavailable') })] }), _jsx("div", { className: "max-h-44 space-y-1 overflow-y-auto", children: keys.map((secret) => _jsxs("label", { className: "hover:bg-hover has-[:focus-visible]:ring-ring flex min-h-10 items-center gap-2 rounded-md px-2 py-2 text-sm has-[:focus-visible]:ring-2 has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-50", children: [_jsx(Checkbox, { checked: selected.includes(secret.secret_id), disabled: disabled || readOnly || (!selected.includes(secret.secret_id) && selected.length >= 10), onCheckedChange: (checked) => onChange(checked === true ? [...selected, secret.secret_id] : selected.filter((value) => value !== secret.secret_id)) }), _jsx("span", { className: "text-foreground min-w-0 break-words", children: secret.label }), providerId === 'codex' && needsReconnection(secret) && (_jsx("span", { className: "text-muted-foreground ml-auto shrink-0 text-xs", children: t('needsReconnection') }))] }, secret.secret_id)) }), selected.length >= 10 && _jsx("p", { className: "text-muted-foreground text-xs", role: "status", children: t('selectionLimit') }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [configured && !readOnly && _jsx(Button, { size: "sm", variant: "secondary", disabled: disabled, onClick: onReset, children: t(personalChatGpt ? 'resetPersonalChatGptDefault' : 'resetDefault') }), providerId === 'codex'
63
+ : t(personalChatGpt ? 'personalChatGptDefault' : 'projectDefault') }), unavailable.length > 0 && _jsxs("div", { className: "space-y-1", role: "status", children: [_jsx("p", { className: "text-muted-foreground text-xs", children: t('unavailableKeys', { count: unavailable.length }) }), _jsx(Button, { size: "sm", variant: "outline-ghost", disabled: disabled || readOnly, onClick: () => onChange(selected.filter((secretId) => available.has(secretId))), children: t('removeUnavailable') })] }), _jsx("div", { className: "max-h-44 space-y-1 overflow-y-auto", children: keys.map((secret) => _jsxs("label", { className: "hover:bg-hover has-[:focus-visible]:ring-ring flex min-h-10 items-center gap-2 rounded-md px-2 py-2 text-sm has-[:focus-visible]:ring-2 has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-50", children: [_jsx(Checkbox, { checked: selectedIds.has(secret.secret_id), disabled: disabled || readOnly || (!selectedIds.has(secret.secret_id) && selected.length >= 10), onCheckedChange: (checked) => onChange(checked === true ? [...selected, secret.secret_id] : selected.filter((value) => value !== secret.secret_id)) }), _jsx("span", { className: "text-foreground min-w-0 break-words", children: secret.label }), providerId === 'codex' && needsReconnection(secret) && (_jsx("span", { className: "text-muted-foreground ml-auto shrink-0 text-xs", children: t('needsReconnection') }))] }, secret.secret_id)) }), selected.length >= 10 && _jsx("p", { className: "text-muted-foreground text-xs", role: "status", children: t('selectionLimit') }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [configured && !readOnly && _jsx(Button, { size: "sm", variant: "secondary", disabled: disabled, onClick: onReset, children: t(personalChatGpt ? 'resetPersonalChatGptDefault' : 'resetDefault') }), providerId === 'codex'
63
64
  ? _jsx(ChatGptAccountsButton, { projectId: projectId, variant: "outline-ghost", action: "manage", disabled: disabled })
64
65
  : !modelsHref
65
66
  ? null
@@ -41,4 +41,4 @@ export interface SessionOverridesComposerProps {
41
41
  * project-home one in `composer-chat-input.tsx` and the in-session one in
42
42
  * `session-chat.tsx`) build the panel from one place.
43
43
  */
44
- export declare function SessionOverridesComposer({ projectId, sessionId, onCommittedDraft, providerSecretPools, onProviderSecretPoolsChange, selectedAgent, sandboxSlot, }: SessionOverridesComposerProps): import("react").JSX.Element;
44
+ export declare function SessionOverridesComposer({ projectId, sessionId, onCommittedDraft, providerSecretPools, onProviderSecretPoolsChange, selectedAgent, sandboxSlot, }: SessionOverridesComposerProps): import("react").JSX.Element | null;