@kortix/ui 0.14.4 → 0.14.5

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 (118) hide show
  1. package/README.md +8 -4
  2. package/dist/customize/sections/llm-provider/constants.d.ts +0 -1
  3. package/dist/customize/sections/llm-provider/constants.js +0 -2
  4. package/dist/customize/sections/llm-provider/models-tab-group.d.ts +1 -1
  5. package/dist/customize/sections/llm-provider/models-tab-group.js +7 -4
  6. package/dist/customize/sections/llm-provider/models-tab-row.d.ts +2 -18
  7. package/dist/customize/sections/llm-provider/models-tab-row.js +6 -30
  8. package/dist/customize/sections/llm-provider/models-tab.d.ts +0 -1
  9. package/dist/customize/sections/llm-provider/models-tab.js +1 -10
  10. package/dist/customize/sections/llm-provider/use-connected-providers.js +2 -2
  11. package/dist/customize/sections/llm-provider/utils.d.ts +1 -1
  12. package/dist/customize/sections/llm-provider/utils.js +7 -14
  13. package/dist/customize/sections/view/gateway/_shared.js +1 -2
  14. package/dist/customize/sections/view/gateway/gateway-routing.js +3 -2
  15. package/dist/customize/sections/view/sandbox-provider-coverage.d.ts +3 -18
  16. package/dist/customize/sections/view/sandbox-provider-coverage.js +7 -27
  17. package/dist/customize/settings/tabs/sandbox-tab.d.ts +2 -27
  18. package/dist/customize/settings/tabs/sandbox-tab.js +13 -112
  19. package/dist/customize/shared/entity/entity-modal.d.ts +5 -1
  20. package/dist/customize/shared/entity/entity-modal.js +7 -4
  21. package/dist/customize/skills/remove-skill-button.d.ts +16 -0
  22. package/dist/customize/skills/remove-skill-button.js +42 -0
  23. package/dist/customize/skills/skills-page.js +1 -1
  24. package/dist/customize/triggers/schedule/trigger-list.d.ts +0 -2
  25. package/dist/customize/triggers/schedule/trigger-list.js +1 -18
  26. package/dist/customize/triggers/schedule/trigger-tile.d.ts +1 -32
  27. package/dist/customize/triggers/schedule/trigger-tile.js +1 -16
  28. package/dist/files/project/hooks/use-change-requests.d.ts +14 -1
  29. package/dist/files/project/hooks/use-change-requests.js +44 -25
  30. package/dist/files/renderers/docx/docx-viewer.js +4 -1
  31. package/dist/files/renderers/pdf/pdf-renderer.d.ts +3 -1
  32. package/dist/files/renderers/pdf/pdf-renderer.js +2 -2
  33. package/dist/files/renderers/pdf/pdf-viewer.d.ts +3 -0
  34. package/dist/files/renderers/pdf/pdf-viewer.js +15 -6
  35. package/dist/files/viewer/file-content-renderer.d.ts +4 -1
  36. package/dist/files/viewer/file-content-renderer.js +2 -2
  37. package/dist/i18n/en.d.ts +26 -35
  38. package/dist/i18n/en.js +35 -44
  39. package/dist/index.d.ts +1 -1
  40. package/dist/internal/hooks/use-notification-center.js +21 -15
  41. package/dist/internal/hooks/use-review-session-summary.d.ts +6 -0
  42. package/dist/internal/hooks/use-review-session-summary.js +11 -0
  43. package/dist/internal/lib/browser-error-noise.d.ts +1 -1
  44. package/dist/internal/lib/browser-error-noise.js +1 -1
  45. package/dist/internal/lib/browser-noise/rules/rejections.d.ts +10 -0
  46. package/dist/internal/lib/browser-noise/rules/rejections.js +27 -1
  47. package/dist/internal/lib/provider-branding.js +14 -3
  48. package/dist/internal/lib/trigger-clock.d.ts +42 -0
  49. package/dist/internal/lib/trigger-clock.js +34 -0
  50. package/dist/internal/lib/web-notifications.d.ts +6 -0
  51. package/dist/internal/lib/web-notifications.js +57 -7
  52. package/dist/internal/notifications/notification-bell.d.ts +13 -0
  53. package/dist/internal/notifications/notification-bell.js +1 -1
  54. package/dist/internal/reminders/reminder-filter.d.ts +12 -0
  55. package/dist/internal/reminders/reminder-filter.js +34 -0
  56. package/dist/internal/reminders/reminder-format.d.ts +6 -0
  57. package/dist/internal/reminders/reminder-format.js +20 -2
  58. package/dist/internal/stores/kortix-computer-store.d.ts +1 -1
  59. package/dist/internal/stores/session-hover-store.d.ts +20 -5
  60. package/dist/internal/stores/session-hover-store.js +31 -13
  61. package/dist/internal/stores/session-panel-memory-store.js +2 -0
  62. package/dist/primitives/count-badge.d.ts +3 -1
  63. package/dist/primitives/count-badge.js +3 -3
  64. package/dist/primitives/glyph-paths-source.js +1 -1
  65. package/dist/primitives/glyph-paths.generated.js +1 -1
  66. package/dist/providers/ui-provider.d.ts +57 -0
  67. package/dist/providers/ui-provider.js +4 -0
  68. package/dist/session/composer/ComposerCard.d.ts +2 -0
  69. package/dist/session/composer/ComposerCard.js +5 -1
  70. package/dist/session/composer/agent-selector.js +12 -5
  71. package/dist/session/composer/composer-toolbar.js +4 -2
  72. package/dist/session/composer/composer-underbar.js +3 -1
  73. package/dist/session/composer/composer.js +34 -5
  74. package/dist/session/composer/menus/slash-actions.d.ts +22 -2
  75. package/dist/session/composer/menus/slash-actions.js +20 -0
  76. package/dist/session/composer/menus/slash-items.js +19 -3
  77. package/dist/session/header/reminder-parts.d.ts +285 -0
  78. package/dist/session/header/reminder-parts.js +115 -0
  79. package/dist/session/header/session-reminders-indicator.d.ts +13 -0
  80. package/dist/session/header/session-reminders-indicator.js +142 -35
  81. package/dist/session/models/model-availability.d.ts +23 -0
  82. package/dist/session/models/model-availability.js +20 -0
  83. package/dist/session/models/model-grouping.js +1 -4
  84. package/dist/session/models/model-selector.js +23 -14
  85. package/dist/session/models/use-model-connection-gate.js +4 -1
  86. package/dist/session/outcomes/session-outcomes-provider.d.ts +3 -1
  87. package/dist/session/outcomes/session-outcomes-provider.js +31 -3
  88. package/dist/session/overrides/session-overrides-composer.d.ts +1 -1
  89. package/dist/session/overrides/session-overrides-composer.js +5 -1
  90. package/dist/session/panel/mobile-tool-drawer.js +4 -2
  91. package/dist/session/panel/open-session-quick-view.js +2 -1
  92. package/dist/session/panel/panel-fallback.js +8 -4
  93. package/dist/session/panel/session-panel-provider.d.ts +1 -0
  94. package/dist/session/panel/session-panel-provider.js +13 -1
  95. package/dist/session/panel/session-reminders-panel.d.ts +4 -0
  96. package/dist/session/panel/session-reminders-panel.js +109 -0
  97. package/dist/session/panel/shared/use-panel-handoffs.d.ts +2 -2
  98. package/dist/session/panel/shared/use-panel-handoffs.js +5 -1
  99. package/dist/session/print/session-print-header.d.ts +0 -10
  100. package/dist/session/print/session-print-header.js +16 -1
  101. package/dist/session/status/session-surface.js +1 -1
  102. package/dist/session/thread/optimistic-turn.d.ts +5 -1
  103. package/dist/session/thread/optimistic-turn.js +8 -6
  104. package/dist/session/thread/session-chat.d.ts +6 -1
  105. package/dist/session/thread/session-chat.js +22 -3
  106. package/dist/session/thread/session-thread.d.ts +10 -1
  107. package/dist/session/thread/session-thread.js +2 -2
  108. package/dist/session/turn/burst-step-body.js +4 -0
  109. package/dist/styles.css +1 -1
  110. package/dist/workspace/sidebar/project-session-list.js +52 -55
  111. package/dist/workspace/sidebar/session-brief-hover-card.d.ts +35 -18
  112. package/dist/workspace/sidebar/session-brief-hover-card.js +114 -37
  113. package/dist/workspace/sidebar/session-status-mark.d.ts +15 -1
  114. package/dist/workspace/sidebar/session-status-mark.js +63 -17
  115. package/package.json +7 -3
  116. package/skills/kortix-ui/SKILL.md +1 -1
  117. package/dist/customize/sections/view/sandbox-provider-result.d.ts +0 -47
  118. package/dist/customize/sections/view/sandbox-provider-result.js +0 -79
@@ -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) => {
@@ -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) {
@@ -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;
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useMemo } from 'react';
4
4
  import { SessionDrivesWatch } from '../../files/drives/session-drives-chip.js';
5
5
  import { useProjectSession } from '@kortix/sdk/react';
6
+ import { useUiHostConfig } from '../../providers/ui-provider.js';
6
7
  import { ProviderPoolDraftBoundary } from './provider-pool-draft-context.js';
7
8
  import { SessionOverridesToolbar } from './session-overrides-toolbar.js';
8
9
  /**
@@ -28,10 +29,13 @@ import { SessionOverridesToolbar } from './session-overrides-toolbar.js';
28
29
  * `session-chat.tsx`) build the panel from one place.
29
30
  */
30
31
  export function SessionOverridesComposer({ projectId, sessionId, onCommittedDraft, providerSecretPools, onProviderSecretPoolsChange, selectedAgent, sandboxSlot, }) {
31
- const sessionRow = useProjectSession(projectId, sessionId, { enabled: Boolean(sessionId) });
32
+ const { sessionOverridesEnabled } = useUiHostConfig();
33
+ const sessionRow = useProjectSession(projectId, sessionId, { enabled: Boolean(sessionId) && sessionOverridesEnabled });
32
34
  const sandbox = useMemo(() => ({
33
35
  slug: sessionRow.data?.metadata?.sandbox_slug ?? null,
34
36
  provider: sessionRow.data?.sandbox_provider ?? null,
35
37
  }), [sessionRow.data?.metadata, sessionRow.data?.sandbox_provider]);
38
+ if (!sessionOverridesEnabled)
39
+ return null;
36
40
  return (_jsxs(ProviderPoolDraftBoundary, { identity: `${projectId}/${sessionId ?? 'new'}`, children: [sessionId ? _jsx(SessionDrivesWatch, { projectId: projectId, sessionId: sessionId }) : null, _jsx(SessionOverridesToolbar, { projectId: projectId, sessionId: sessionId, agentName: selectedAgent ?? undefined, onCommittedDraft: onCommittedDraft, providerSecretPools: providerSecretPools, onProviderSecretPoolsChange: onProviderSecretPoolsChange, sandbox: sandbox, sandboxSlot: sandboxSlot }, `${projectId}/${sessionId ?? 'new'}`)] }));
37
41
  }
@@ -21,22 +21,24 @@ import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from '../../primitiv
21
21
  import { BrowserPanel } from '../../browser/browser-panel.js';
22
22
  import { CloseButton } from './easy/detail-view.js';
23
23
  import { SessionAuditPanel } from './session-audit-panel.js';
24
+ import { SessionRemindersPanel } from './session-reminders-panel.js';
24
25
  import { SessionFilesExplorer } from './session-files-explorer.js';
25
26
  import { SessionTerminalPanel } from '../../terminal/session-terminal-panel.js';
26
27
  import { useLocalizedUiCatalog } from '../../i18n/ui-catalog/use-localized-ui-catalog.js';
27
28
  import { useKortixComputerStore } from '../../internal/stores/kortix-computer-store.js';
28
29
  import { sessionPreviewTabId } from '../../internal/stores/session-browser-store.js';
29
- import { FolderOpenIcon, GlobeIcon, ScrollIcon, TerminalWindowIcon, } from '@phosphor-icons/react';
30
+ import { AlarmIcon, FolderOpenIcon, GlobeIcon, ScrollIcon, TerminalWindowIcon, } from '@phosphor-icons/react';
30
31
  const TOOL_META = {
31
32
  terminal: { label: 'Terminal', Icon: TerminalWindowIcon },
32
33
  browser: { label: 'Browser', Icon: GlobeIcon },
33
34
  files: { label: 'Files', Icon: FolderOpenIcon },
34
35
  audit: { label: 'Audit', Icon: ScrollIcon },
36
+ reminders: { label: 'Reminders', Icon: AlarmIcon },
35
37
  };
36
38
  export function MobileToolDrawer({ sessionId, projectId, projectSessionId, }) {
37
39
  const view = useKortixComputerStore((s) => s.mobileToolView);
38
40
  const closeMobileTool = useKortixComputerStore((s) => s.closeMobileTool);
39
41
  const toolMeta = useLocalizedUiCatalog(TOOL_META);
40
42
  const meta = view ? toolMeta[view] : null;
41
- return (_jsx(Drawer, { open: view !== null, onOpenChange: (next) => !next && closeMobileTool(), children: _jsxs(DrawerContent, { className: "flex h-[95dvh] max-h-[95dvh] min-h-[95dvh] flex-col overflow-hidden p-0", children: [_jsx(DrawerHeader, { className: "shrink-0 px-4 py-3 text-left", children: _jsxs(DrawerTitle, { className: "flex items-center justify-between gap-2 text-base", children: [_jsxs("span", { className: "flex items-center gap-2.5", children: [meta && _jsx(meta.Icon, { className: "text-muted-foreground size-4 shrink-0" }), meta?.label] }), _jsx(CloseButton, { onClose: closeMobileTool, variant: "ghost" })] }) }), _jsxs("div", { className: "min-h-0 flex-1 overflow-hidden", children: [view === 'terminal' && (_jsx(SessionTerminalPanel, { sessionId: sessionId, projectId: projectId, projectSessionId: projectSessionId })), view === 'browser' && (_jsx(BrowserPanel, { tabId: sessionPreviewTabId(sessionId), projectId: projectId, projectSessionId: projectSessionId })), view === 'files' && (_jsx(SessionFilesExplorer, { chatSessionId: sessionId, projectId: projectId, projectSessionId: projectSessionId })), view === 'audit' && (_jsx(SessionAuditPanel, { projectId: projectId, projectSessionId: projectSessionId }))] })] }) }));
43
+ return (_jsx(Drawer, { open: view !== null, onOpenChange: (next) => !next && closeMobileTool(), children: _jsxs(DrawerContent, { className: "flex h-[95dvh] max-h-[95dvh] min-h-[95dvh] flex-col overflow-hidden p-0", children: [_jsx(DrawerHeader, { className: "shrink-0 px-4 py-3 text-left", children: _jsxs(DrawerTitle, { className: "flex items-center justify-between gap-2 text-base", children: [_jsxs("span", { className: "flex items-center gap-2.5", children: [meta && _jsx(meta.Icon, { className: "text-muted-foreground size-4 shrink-0" }), meta?.label] }), _jsx(CloseButton, { onClose: closeMobileTool, variant: "ghost" })] }) }), _jsxs("div", { className: "min-h-0 flex-1 overflow-hidden", children: [view === 'terminal' && (_jsx(SessionTerminalPanel, { sessionId: sessionId, projectId: projectId, projectSessionId: projectSessionId })), view === 'browser' && (_jsx(BrowserPanel, { tabId: sessionPreviewTabId(sessionId), projectId: projectId, projectSessionId: projectSessionId })), view === 'files' && (_jsx(SessionFilesExplorer, { chatSessionId: sessionId, projectId: projectId, projectSessionId: projectSessionId })), view === 'audit' && (_jsx(SessionAuditPanel, { projectId: projectId, projectSessionId: projectSessionId })), view === 'reminders' && (_jsx("div", { className: "h-full overflow-y-auto p-4", children: _jsx(SessionRemindersPanel, { projectId: projectId, projectSessionId: projectSessionId }) }))] })] }) }));
42
44
  }
@@ -55,7 +55,8 @@ target) {
55
55
  // is tab-gated — relying on the latter silently dropped the terminal open
56
56
  // on /projects/:id/sessions/:id.
57
57
  const activePanelSessionId = getActivePanelSessionId();
58
- if (panelMode === 'advanced') {
58
+ // Reminders is an Easy detail with no Advanced tab: it takes the request path in both modes.
59
+ if (panelMode === 'advanced' && view !== 'reminders') {
59
60
  if (activePanelSessionId) {
60
61
  // `SessionPanelView` calls the file explorer 'explorer'; its 'files'
61
62
  // member is the git-changes diff view. The quick-view vocabulary uses
@@ -26,6 +26,7 @@ const AppPreview = lazy(() => import('./easy/app-preview.js').then((m) => ({ def
26
26
  const SessionTerminalPanel = lazy(() => import('../../terminal/session-terminal-panel.js').then((m) => ({ default: m.SessionTerminalPanel })));
27
27
  const SessionFilesExplorer = lazy(() => import('./session-files-explorer.js').then((m) => ({ default: m.SessionFilesExplorer })));
28
28
  const SessionAuditPanel = lazy(() => import('./session-audit-panel.js').then((m) => ({ default: m.SessionAuditPanel })));
29
+ const SessionRemindersPanel = lazy(() => import('./session-reminders-panel.js').then((m) => ({ default: m.SessionRemindersPanel })));
29
30
  export function SessionPanelFallback({ projectId, projectSessionId, runtimeSessionId, }) {
30
31
  useEffect(() => registerPanelFallbackHost(), []);
31
32
  const panelMounted = useSessionBrowserStore((s) => s.activeSessionId !== null);
@@ -35,6 +36,7 @@ export function SessionPanelFallback({ projectId, projectSessionId, runtimeSessi
35
36
  }
36
37
  function PanelViewModal({ projectId, projectSessionId, runtimeSessionId }) {
37
38
  const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
39
+ const tReminders = useTranslations('reminders');
38
40
  const request = usePanelFallbackStore((s) => s.request);
39
41
  if (!request)
40
42
  return null;
@@ -45,10 +47,12 @@ function PanelViewModal({ projectId, projectSessionId, runtimeSessionId }) {
45
47
  ? 'Terminal'
46
48
  : view === 'audit'
47
49
  ? tI18nComplete.raw('textbb6aea287396')
48
- : target?.changes
49
- ? 'Changes'
50
- : 'Files';
51
- const body = view === 'browser' ? (_jsx(AppPreview, { url: target?.url ?? '', name: title, projectId: projectId, shareContext: { projectId, sessionId: projectSessionId }, onClose: closePanelFallbackView })) : view === 'audit' ? (_jsx(SessionAuditPanel, { projectId: projectId, projectSessionId: projectSessionId })) : !runtimeSessionId ? (_jsx(Centered, { children: _jsx(Loading, { className: "size-5" }) })) : view === 'terminal' ? (_jsx(SessionTerminalPanel, { sessionId: runtimeSessionId, projectId: projectId, projectSessionId: projectSessionId })) : (_jsx(SessionFilesExplorer, { chatSessionId: runtimeSessionId, projectId: projectId, projectSessionId: projectSessionId, ephemeral: true, initialMode: target?.changes ? 'changes' : 'files' }));
50
+ : view === 'reminders'
51
+ ? tReminders('title')
52
+ : target?.changes
53
+ ? 'Changes'
54
+ : 'Files';
55
+ const body = view === 'browser' ? (_jsx(AppPreview, { url: target?.url ?? '', name: title, projectId: projectId, shareContext: { projectId, sessionId: projectSessionId }, onClose: closePanelFallbackView })) : view === 'audit' ? (_jsx(SessionAuditPanel, { projectId: projectId, projectSessionId: projectSessionId })) : view === 'reminders' ? (_jsx("div", { className: "p-4", children: _jsx(SessionRemindersPanel, { projectId: projectId, projectSessionId: projectSessionId }) })) : !runtimeSessionId ? (_jsx(Centered, { children: _jsx(Loading, { className: "size-5" }) })) : view === 'terminal' ? (_jsx(SessionTerminalPanel, { sessionId: runtimeSessionId, projectId: projectId, projectSessionId: projectSessionId })) : (_jsx(SessionFilesExplorer, { chatSessionId: runtimeSessionId, projectId: projectId, projectSessionId: projectSessionId, ephemeral: true, initialMode: target?.changes ? 'changes' : 'files' }));
52
56
  return (_jsx(Modal, { open: true, onOpenChange: (open) => !open && closePanelFallbackView(), children: _jsxs(ModalContent
53
57
  // The browser has its own close button in its toolbar.
54
58
  , {
@@ -28,6 +28,7 @@ export interface SessionPanelValue {
28
28
  }) => void;
29
29
  openFiles: (changes?: boolean) => void;
30
30
  openAudit: () => void;
31
+ openReminders: () => void;
31
32
  }
32
33
  /**
33
34
  * Exported so a test can inject a stub value and render the cards (or
@@ -27,6 +27,7 @@ import { useTranslations } from '../../i18n/index.js';
27
27
  * that would drop a live PTY WebSocket. See `PersistentLayer`.
28
28
  */
29
29
  import { SessionAuditPanel } from './session-audit-panel.js';
30
+ import { SessionRemindersPanel } from './session-reminders-panel.js';
30
31
  import { SessionFilesExplorer } from './session-files-explorer.js';
31
32
  import { useSandboxProxy } from '../../internal/hooks/use-sandbox-proxy.js';
32
33
  import { useIsMobile } from '../../hooks/use-mobile.js';
@@ -71,6 +72,7 @@ export function useOptionalSessionPanel() {
71
72
  }
72
73
  export function SessionPanelProvider({ sessionId, isSessionBusy = false, projectId, projectSessionId, children, }) {
73
74
  const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
75
+ const tReminders = useTranslations('reminders');
74
76
  const { steps, outputs, context, apps, files, isRunning, outcome } = usePanelDerivations({
75
77
  sessionId, projectId, projectSessionId, isSessionBusy,
76
78
  });
@@ -376,6 +378,14 @@ export function SessionPanelProvider({ sessionId, isSessionBusy = false, project
376
378
  body: _jsx(SessionAuditPanel, { projectId: projectId, projectSessionId: projectSessionId }),
377
379
  });
378
380
  }, [openDetail, projectId, projectSessionId, tI18nComplete]);
381
+ // A session with more reminders than the header popover holds opens them here.
382
+ const openReminders = useCallback(() => {
383
+ openDetail({
384
+ key: 'reminders',
385
+ title: tReminders('title'),
386
+ body: _jsx(SessionRemindersPanel, { projectId: projectId, projectSessionId: projectSessionId }),
387
+ });
388
+ }, [openDetail, projectId, projectSessionId, tReminders]);
379
389
  /**
380
390
  * The opt-in File Explorer (Marko's ask). Never a default view and never a
381
391
  * tab — it opens only when asked for, exactly like Terminal and Audit, so
@@ -418,7 +428,7 @@ export function SessionPanelProvider({ sessionId, isSessionBusy = false, project
418
428
  }, [apps, handleOpenOutput]);
419
429
  const { outputsDefaultOpen, pendingQuickView } = usePanelHandoffs({
420
430
  sessionId, projectId, projectSessionId, isRunning, outcome, apps, files, steps,
421
- detail, handleOpenOutput, openDetail, openTerminal, openAudit, openBrowser, openFiles,
431
+ detail, handleOpenOutput, openDetail, openTerminal, openAudit, openReminders, openBrowser, openFiles,
422
432
  });
423
433
  // The panel comes back with the session: what it showed is remembered while
424
434
  // it is open, and reopened on the way back while the session still runs.
@@ -545,6 +555,7 @@ export function SessionPanelProvider({ sessionId, isSessionBusy = false, project
545
555
  openBrowser,
546
556
  openFiles,
547
557
  openAudit,
558
+ openReminders,
548
559
  }), [
549
560
  sessionId,
550
561
  projectId,
@@ -564,6 +575,7 @@ export function SessionPanelProvider({ sessionId, isSessionBusy = false, project
564
575
  openBrowser,
565
576
  openFiles,
566
577
  openAudit,
578
+ openReminders,
567
579
  ]);
568
580
  return _jsx(SessionPanelContext.Provider, { value: value, children: children });
569
581
  }
@@ -0,0 +1,4 @@
1
+ export declare function SessionRemindersPanel({ projectId, projectSessionId, }: {
2
+ projectId?: string;
3
+ projectSessionId?: string;
4
+ }): import("react").JSX.Element;
@@ -0,0 +1,109 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /**
4
+ * The session's reminders as a side-panel view, for a session with more than
5
+ * the header popover holds (`REMINDERS_POPOVER_MAX`). It is the Triggers list
6
+ * (`customize/triggers/schedule/trigger-list.tsx`) for reminders: the search
7
+ * field and underline tabs from the Triggers page, a time filter by next run,
8
+ * and the same 56px rows — the clock that carries its health, the name over
9
+ * its schedule, the next run over the last run. A row opens its prompt, facts
10
+ * and actions in place.
11
+ */
12
+ import { Button } from '../../primitives/button.js';
13
+ import { EmptyState } from '../../primitives/empty-state.js';
14
+ import { ErrorState } from '../../primitives/error-state.js';
15
+ import { InputGroupSearch, InputGroupSearchClear, InputGroupSearchIcon, InputGroupSearchInput, } from '../../primitives/input-group.js';
16
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../primitives/select.js';
17
+ import { Skeleton } from '../../primitives/skeleton.js';
18
+ import { Tabs, TabsList, TabsTrigger } from '../../primitives/tabs.js';
19
+ import { REFLECTION_MASK } from '../../primitives/toast.js';
20
+ import { useDebounce } from '../../hooks/use-debounce.js';
21
+ import { remindersHref } from '../../internal/lib/capability-tab-routes.js';
22
+ import { useCapabilityAccess } from '../../internal/layout/project-settings-nav.js';
23
+ import { reminderTitle, scheduleLabel } from '../../internal/reminders/reminder-format.js';
24
+ import { matchesQuery, matchesStatus, matchesTime, REMINDER_STATUS_FILTERS, REMINDER_TIME_FILTERS, } from '../../internal/reminders/reminder-filter.js';
25
+ import { useNow } from '../../internal/reminders/use-refetch-after-fire.js';
26
+ import { cn } from '../../internal/lib/utils.js';
27
+ import { useLocale, useTranslations } from '../../i18n/index.js';
28
+ import { AlarmIcon, MagnifyingGlassIcon as SearchIcon } from '@phosphor-icons/react';
29
+ import { useState } from 'react';
30
+ import { CLOCK_TONE, lastRunLabel } from '../../internal/lib/trigger-clock.js';
31
+ import { ReminderActions, ReminderFacts, isFailedReminder, sortReminders, useReminderControls, } from '../header/reminder-parts.js';
32
+ const STATUS_LABEL_KEY = {
33
+ all: 'statusAll',
34
+ active: 'tabActive',
35
+ paused: 'tabPaused',
36
+ failed: 'calendarFailed',
37
+ };
38
+ const TIME_LABEL_KEY = {
39
+ any: 'timeAny',
40
+ hour: 'timeHour',
41
+ day: 'timeDay',
42
+ week: 'timeWeek',
43
+ later: 'timeLater',
44
+ };
45
+ /** The Triggers clock (`TriggerTile`) in a reminder's health, drawn with the alarm the header chip uses. */
46
+ function ReminderTile({ reminder }) {
47
+ const tone = isFailedReminder(reminder)
48
+ ? CLOCK_TONE.failed
49
+ : reminder.state !== 'active'
50
+ ? CLOCK_TONE.paused
51
+ : CLOCK_TONE.ok;
52
+ return (_jsxs("span", { "aria-hidden": true, className: "relative size-5 shrink-0", children: [_jsx("span", { className: "text-muted-foreground absolute inset-0 flex items-center justify-center [&_svg]:size-5", children: _jsx(AlarmIcon, {}) }), _jsx("span", { className: cn('absolute inset-0 flex items-center justify-center [&_svg]:size-5', tone.reflection), style: { maskImage: REFLECTION_MASK, WebkitMaskImage: REFLECTION_MASK }, children: _jsx(AlarmIcon, {}) }), _jsx("span", { className: "absolute -right-0.75 -bottom-0.75 flex size-3 items-center justify-center rounded-full bg-(--row-surface)", children: _jsx("span", { className: cn('size-1.5 rounded-full', tone.dot) }) })] }));
53
+ }
54
+ /** "in 25m", "in 8h", "in 3d": the next run in the last-run label's narrow style. */
55
+ function nextRunIn(iso, locale, now) {
56
+ const at = iso ? Date.parse(iso) : Number.NaN;
57
+ if (!Number.isFinite(at))
58
+ return '—';
59
+ const rtf = new Intl.RelativeTimeFormat(locale, { style: 'narrow', numeric: 'auto' });
60
+ const min = Math.max(0, Math.round((at - now) / 60_000));
61
+ if (min < 60)
62
+ return rtf.format(min, 'minute');
63
+ if (min < 24 * 60)
64
+ return rtf.format(Math.round(min / 60), 'hour');
65
+ return rtf.format(Math.round(min / (24 * 60)), 'day');
66
+ }
67
+ function ReminderListRow({ reminder, schedule, now, open, onToggle, controls, manageHref, }) {
68
+ const t = useTranslations('reminders');
69
+ const tTriggers = useTranslations('hardcodedUi.i18nComplete');
70
+ const locale = useLocale();
71
+ const active = reminder.state === 'active';
72
+ // The Triggers last-run label takes a trigger's shape.
73
+ const asTrigger = { last_fired_at: reminder.last_fired_at, event: null };
74
+ return (_jsxs("li", { className: cn('rounded-md',
75
+ // One variable paints the row and the clock's dot disc, so the cutout
76
+ // always matches: rest = card, hover = muted at 60% over card, open = muted.
77
+ 'bg-(--row-surface) [--row-surface:var(--card)] transition-[background-color] duration-(--duration-fast)', open
78
+ ? '[--row-surface:var(--muted)]'
79
+ : 'hover:[--row-surface:color-mix(in_oklab,var(--muted)_60%,var(--card))] has-[button:focus-visible]:[--row-surface:color-mix(in_oklab,var(--muted)_60%,var(--card))]'), children: [_jsxs("button", { type: "button", onClick: onToggle, "aria-expanded": open, "data-testid": "session-reminder-panel-row", className: "flex h-14 w-full items-center gap-3 rounded-md px-3 text-left outline-none", children: [_jsx("span", { className: "flex size-5 shrink-0 items-center justify-center", children: _jsx(ReminderTile, { reminder: reminder }) }), _jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [_jsx("span", { className: "text-foreground truncate text-sm leading-tight font-medium", children: reminderTitle(reminder) }), _jsx("span", { className: "text-muted-foreground truncate text-xs leading-tight", children: schedule })] }), _jsxs("span", { className: "flex shrink-0 flex-col items-end text-xs tabular-nums", children: [_jsx("span", { className: cn('whitespace-nowrap', active ? 'text-foreground' : 'text-muted-foreground'), children: active ? nextRunIn(reminder.next_fire_at, locale, now) : t('pausedLabel') }), _jsx("span", { className: "text-muted-foreground whitespace-nowrap", children: lastRunLabel(asTrigger, tTriggers, locale, now) })] })] }), open ? (
80
+ // Starts under the name: the row's px-3, the 20px clock slot, gap-3.
81
+ _jsxs("div", { className: "space-y-3 pr-3 pb-3 pl-11", children: [reminder.name ? (_jsx("p", { className: "text-muted-foreground text-xs text-pretty whitespace-pre-line", children: reminder.prompt })) : null, _jsx(ReminderFacts, { reminder: reminder, now: now }), _jsx(ReminderActions, { reminder: reminder, controls: controls, manageHref: manageHref, className: "-ml-1.5" })] })) : null] }));
82
+ }
83
+ export function SessionRemindersPanel({ projectId, projectSessionId, }) {
84
+ const t = useTranslations('reminders');
85
+ const now = useNow();
86
+ const { reminders, controls, confirm } = useReminderControls(projectId ?? null, projectSessionId);
87
+ const hasTab = useCapabilityAccess(projectId).tabs.includes('reminders');
88
+ const manageHref = hasTab && projectId ? `${remindersHref(projectId)}?session=${projectSessionId}` : null;
89
+ const [query, setQuery] = useState('');
90
+ // The input shows every keystroke; the list filters 200 ms after the last one.
91
+ const { debouncedValue: debouncedQuery } = useDebounce(query, 200);
92
+ const [status, setStatus] = useState('all');
93
+ const [time, setTime] = useState('any');
94
+ const [openId, setOpenId] = useState(null);
95
+ if (reminders.isPending) {
96
+ return (_jsx("div", { className: "space-y-1", children: Array.from({ length: 6 }, (_, index) => (_jsx(Skeleton, { className: "h-14 rounded-md" }, index))) }));
97
+ }
98
+ if (reminders.isError) {
99
+ return (_jsx(ErrorState, { size: "sm", title: t('loadFailed'), action: _jsx(Button, { variant: "outline", size: "sm", onClick: () => void reminders.refetch(), children: t('retry') }) }));
100
+ }
101
+ const schedule = (reminder) => scheduleLabel(reminder, (key, values) => t(key, values));
102
+ const live = sortReminders((reminders.data?.reminders ?? []).filter((reminder) => reminder.state !== 'done'));
103
+ const shown = live.filter((reminder) => matchesStatus(reminder, status) &&
104
+ matchesTime(reminder, time, now) &&
105
+ matchesQuery(reminder, debouncedQuery, schedule(reminder)));
106
+ return (_jsxs("div", { className: "space-y-3", children: [_jsxs(InputGroupSearch, { children: [_jsx(InputGroupSearchIcon, { children: _jsx(SearchIcon, {}) }), _jsx(InputGroupSearchInput, { placeholder: t('searchPlaceholder'), value: query, onChange: (event) => setQuery(event.target.value), size: "sm" }), _jsx(InputGroupSearchClear, { onClick: () => setQuery('') })] }), _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx(Tabs, { value: status, onValueChange: (next) => setStatus(next), className: "min-w-0 overflow-x-auto", children: _jsx(TabsList, { type: "underline", underlineSize: "md", size: "sm", className: "h-8 justify-start gap-5", children: REMINDER_STATUS_FILTERS.map((value) => (_jsx(TabsTrigger, { value: value, className: "flex-none px-0 text-xs font-medium", children: t(STATUS_LABEL_KEY[value]) }, value))) }) }), _jsxs(Select, { value: time, onValueChange: (next) => setTime(next), children: [_jsx(SelectTrigger, { variant: "transparent", size: "sm", "aria-label": t('timeFilterLabel'), className: "text-muted-foreground h-8 shrink-0 text-xs", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { align: "end", children: REMINDER_TIME_FILTERS.map((value) => (_jsx(SelectItem, { value: value, children: t(TIME_LABEL_KEY[value]) }, value))) })] })] }), shown.length > 0 ? (
107
+ // The Triggers list's resting panel: `bg-card`, border, `rounded-lg`.
108
+ _jsx("ul", { className: "bg-card border-border flex flex-col rounded-lg border p-1", children: shown.map((reminder) => (_jsx(ReminderListRow, { reminder: reminder, schedule: schedule(reminder), now: now, open: openId === reminder.id, onToggle: () => setOpenId((current) => (current === reminder.id ? null : reminder.id)), controls: controls, manageHref: manageHref }, reminder.id))) })) : (_jsx(EmptyState, { size: "sm", title: t('noMatch') })), shown.length < live.length ? (_jsx("p", { className: "text-muted-foreground px-1 text-xs tabular-nums", children: t('filterFooter', { count: shown.length, total: live.length }) })) : null, confirm] }));
109
+ }
@@ -1,7 +1,7 @@
1
1
  import type { SessionPanelValue } from '../session-panel-provider.js';
2
2
  import type { usePanelDerivations } from './use-panel-derivations.js';
3
- type Handoffs = Pick<SessionPanelValue, 'sessionId' | 'projectId' | 'projectSessionId' | 'detail' | 'handleOpenOutput' | 'openDetail' | 'openTerminal' | 'openAudit' | 'openBrowser' | 'openFiles'> & Pick<ReturnType<typeof usePanelDerivations>, 'isRunning' | 'outcome' | 'apps' | 'files' | 'steps'>;
4
- export declare function usePanelHandoffs({ sessionId, projectId, projectSessionId, isRunning, outcome, apps, files, steps, detail, handleOpenOutput, openDetail, openTerminal, openAudit, openBrowser, openFiles, }: Handoffs): {
3
+ type Handoffs = Pick<SessionPanelValue, 'sessionId' | 'projectId' | 'projectSessionId' | 'detail' | 'handleOpenOutput' | 'openDetail' | 'openTerminal' | 'openAudit' | 'openReminders' | 'openBrowser' | 'openFiles'> & Pick<ReturnType<typeof usePanelDerivations>, 'isRunning' | 'outcome' | 'apps' | 'files' | 'steps'>;
4
+ export declare function usePanelHandoffs({ sessionId, projectId, projectSessionId, isRunning, outcome, apps, files, steps, detail, handleOpenOutput, openDetail, openTerminal, openAudit, openReminders, openBrowser, openFiles, }: Handoffs): {
5
5
  outputsDefaultOpen: boolean;
6
6
  pendingQuickView: {
7
7
  sessionId: string;
@@ -6,7 +6,7 @@ import { pathOutput, shouldAutoExpandOutputs, shouldAutoOpenPayoff, stepForCallI
6
6
  import { StepDetailBody } from '../easy/step-detail-body.js';
7
7
  import { StepIcon } from '../easy/step-icon.js';
8
8
  import { selectPrimaryDeliverable } from './output-priority.js';
9
- export function usePanelHandoffs({ sessionId, projectId, projectSessionId, isRunning, outcome, apps, files, steps, detail, handleOpenOutput, openDetail, openTerminal, openAudit, openBrowser, openFiles, }) {
9
+ export function usePanelHandoffs({ sessionId, projectId, projectSessionId, isRunning, outcome, apps, files, steps, detail, handleOpenOutput, openDetail, openTerminal, openAudit, openReminders, openBrowser, openFiles, }) {
10
10
  // Auto-expand Outputs the moment a run finishes with something to show —
11
11
  // never on every render of an already-finished (or still-running) run, and
12
12
  // never on a failed one: a pile of half-written files is not a celebration.
@@ -150,6 +150,9 @@ export function usePanelHandoffs({ sessionId, projectId, projectSessionId, isRun
150
150
  else if (view === 'audit' && projectId && projectSessionId) {
151
151
  openAudit();
152
152
  }
153
+ else if (view === 'reminders' && projectId && projectSessionId) {
154
+ openReminders();
155
+ }
153
156
  else if (view === 'browser') {
154
157
  openBrowser(target);
155
158
  }
@@ -163,6 +166,7 @@ export function usePanelHandoffs({ sessionId, projectId, projectSessionId, isRun
163
166
  projectSessionId,
164
167
  openTerminal,
165
168
  openAudit,
169
+ openReminders,
166
170
  openBrowser,
167
171
  openFiles,
168
172
  ]);