@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.
- package/README.md +8 -4
- package/dist/access/access/access-dialog.js +8 -5
- package/dist/access/access/copy-row.js +2 -2
- package/dist/access/sharing-picker.js +2 -1
- package/dist/browser/app-in-browser.js +5 -3
- package/dist/browser/browser-panel.js +6 -6
- package/dist/browser/sandbox-browser-chrome.d.ts +4 -1
- package/dist/browser/sandbox-browser-chrome.js +4 -3
- package/dist/customize/agents/agent-access.d.ts +20 -0
- package/dist/customize/agents/agent-access.js +17 -0
- package/dist/customize/agents/agent-grant-pages.js +5 -6
- package/dist/customize/agents/agent-page.js +2 -1
- package/dist/customize/agents/agents-page.js +18 -1
- package/dist/customize/connectors/add-mcp/add-mcp-server.js +4 -2
- package/dist/customize/connectors/connector-routes.d.ts +3 -1
- package/dist/customize/connectors/connector-routes.js +18 -0
- package/dist/customize/connectors/connectors-page.js +23 -4
- package/dist/customize/connectors/detail/connector-advanced-tools.js +4 -6
- package/dist/customize/providers/provider-accordion.js +2 -1
- package/dist/customize/providers/provider-connect.js +2 -1
- package/dist/customize/sections/component/telegram-channel-row.js +1 -1
- package/dist/customize/sections/connector-connection-modal.js +11 -9
- package/dist/customize/sections/connectors-view.js +11 -13
- package/dist/customize/sections/llm-provider/constants.d.ts +0 -1
- package/dist/customize/sections/llm-provider/constants.js +0 -2
- package/dist/customize/sections/llm-provider/models-tab-group.d.ts +1 -1
- package/dist/customize/sections/llm-provider/models-tab-group.js +7 -4
- package/dist/customize/sections/llm-provider/models-tab-row.d.ts +2 -18
- package/dist/customize/sections/llm-provider/models-tab-row.js +6 -30
- package/dist/customize/sections/llm-provider/models-tab.d.ts +0 -1
- package/dist/customize/sections/llm-provider/models-tab.js +1 -10
- package/dist/customize/sections/llm-provider/use-connected-providers.js +2 -2
- package/dist/customize/sections/llm-provider/utils.d.ts +1 -1
- package/dist/customize/sections/llm-provider/utils.js +7 -14
- package/dist/customize/sections/view/gateway/_shared.js +1 -2
- package/dist/customize/sections/view/gateway/gateway-routing.js +18 -20
- package/dist/customize/sections/view/sandbox-provider-coverage.d.ts +3 -18
- package/dist/customize/sections/view/sandbox-provider-coverage.js +7 -27
- package/dist/customize/settings/rail-copy-context.js +5 -3
- package/dist/customize/settings/tabs/sandbox-tab.d.ts +2 -27
- package/dist/customize/settings/tabs/sandbox-tab.js +13 -112
- package/dist/customize/settings/tabs/sandbox-template-form.js +9 -12
- package/dist/customize/shared/capability-tabs.js +2 -1
- package/dist/customize/shared/entity/entity-modal.d.ts +5 -1
- package/dist/customize/shared/entity/entity-modal.js +7 -4
- package/dist/customize/skills/remove-skill-button.d.ts +16 -0
- package/dist/customize/skills/remove-skill-button.js +42 -0
- package/dist/customize/skills/skills-page.js +4 -6
- package/dist/customize/triggers/schedule/event-trigger-copy.js +3 -2
- package/dist/customize/triggers/schedule/schedule-builder.js +7 -4
- package/dist/customize/triggers/schedule/trigger-composer.js +5 -2
- package/dist/customize/triggers/schedule/trigger-list.d.ts +0 -2
- package/dist/customize/triggers/schedule/trigger-list.js +1 -18
- package/dist/customize/triggers/schedule/trigger-tile.d.ts +1 -32
- package/dist/customize/triggers/schedule/trigger-tile.js +1 -16
- package/dist/customize/triggers/schedule/when-event.js +1 -1
- package/dist/diff/diff-view.js +9 -10
- package/dist/files/editor/code-editor.js +2 -3
- package/dist/files/project/components/change-request-detail-dialog.d.ts +3 -1
- package/dist/files/project/components/change-request-detail-dialog.js +3 -5
- package/dist/files/project/components/change-request-status-badge.d.ts +12 -0
- package/dist/files/project/components/change-request-status-badge.js +27 -0
- package/dist/files/project/components/change-requests-panel.js +1 -1
- package/dist/files/project/components/drive-explorer.js +2 -1
- package/dist/files/project/components/drive-grid-view.js +11 -7
- package/dist/files/project/components/drive-list-view.js +4 -4
- package/dist/files/project/components/file-breadcrumbs.js +1 -1
- package/dist/files/project/components/file-explorer-page.js +1 -1
- package/dist/files/project/components/file-history-popover.js +1 -6
- package/dist/files/project/components/file-search.js +6 -1
- package/dist/files/project/components/open-change-request-dialog.js +2 -1
- package/dist/files/project/components/version-selector-logic.d.ts +11 -0
- package/dist/files/project/components/version-selector-logic.js +26 -0
- package/dist/files/project/components/version-selector.js +11 -20
- package/dist/files/project/hooks/use-change-requests.d.ts +14 -1
- package/dist/files/project/hooks/use-change-requests.js +44 -25
- package/dist/files/project/hooks/use-server-health.js +17 -15
- package/dist/files/renderers/docx/docx-viewer.js +4 -6
- package/dist/files/renderers/image-renderer.js +6 -24
- package/dist/files/renderers/pdf/pdf-renderer.d.ts +3 -1
- package/dist/files/renderers/pdf/pdf-renderer.js +2 -2
- package/dist/files/renderers/pdf/pdf-viewer.d.ts +3 -0
- package/dist/files/renderers/pdf/pdf-viewer.js +16 -7
- package/dist/files/renderers/presentation/FullScreenPresentationViewer.js +2 -11
- package/dist/files/renderers/show-content-renderer.js +1 -1
- package/dist/files/renderers/sqlite-renderer.js +8 -3
- package/dist/files/renderers/xlsx/xlsx-viewer.js +15 -16
- package/dist/files/tree/components/file-tree-item.js +1 -1
- package/dist/files/viewer/file-content-renderer.d.ts +4 -1
- package/dist/files/viewer/file-content-renderer.js +2 -2
- package/dist/files/viewer/file-preview-modal.js +1 -1
- package/dist/files/viewer/use-file-content-state.js +1 -1
- package/dist/i18n/en.d.ts +36 -35
- package/dist/i18n/en.js +45 -44
- package/dist/index.d.ts +1 -1
- package/dist/internal/desktop/desktop-back-button.js +5 -3
- package/dist/internal/hooks/use-auto-scroll.js +5 -3
- package/dist/internal/hooks/use-notification-center.js +21 -15
- package/dist/internal/hooks/use-review-session-summary.d.ts +6 -0
- package/dist/internal/hooks/use-review-session-summary.js +11 -0
- package/dist/internal/lib/browser-error-noise.d.ts +1 -1
- package/dist/internal/lib/browser-error-noise.js +1 -1
- package/dist/internal/lib/browser-noise/rules/rejections.d.ts +10 -0
- package/dist/internal/lib/browser-noise/rules/rejections.js +27 -1
- package/dist/internal/lib/provider-branding.js +14 -3
- package/dist/internal/lib/trigger-clock.d.ts +42 -0
- package/dist/internal/lib/trigger-clock.js +34 -0
- package/dist/internal/lib/web-notifications.d.ts +6 -0
- package/dist/internal/lib/web-notifications.js +57 -7
- package/dist/internal/notifications/notification-bell.d.ts +13 -0
- package/dist/internal/notifications/notification-bell.js +1 -1
- package/dist/internal/notifications/notification-rows.js +2 -1
- package/dist/internal/outcomes/outcome-types.d.ts +5 -0
- package/dist/internal/reminders/reminder-filter.d.ts +12 -0
- package/dist/internal/reminders/reminder-filter.js +34 -0
- package/dist/internal/reminders/reminder-format.d.ts +6 -0
- package/dist/internal/reminders/reminder-format.js +20 -2
- package/dist/internal/stores/kortix-computer-store.d.ts +1 -1
- package/dist/internal/stores/session-hover-store.d.ts +20 -5
- package/dist/internal/stores/session-hover-store.js +31 -13
- package/dist/internal/stores/session-panel-memory-store.js +2 -0
- package/dist/markdown/code/inline-code.js +8 -0
- package/dist/markdown/code/output-paths-context.d.ts +18 -0
- package/dist/markdown/code/output-paths-context.js +35 -0
- package/dist/markdown/genui/components/charts.js +6 -5
- package/dist/primitives/count-badge.d.ts +3 -1
- package/dist/primitives/count-badge.js +3 -3
- package/dist/primitives/glyph-paths-source.js +1 -1
- package/dist/primitives/glyph-paths.generated.js +1 -1
- package/dist/primitives/map.js +2 -1
- package/dist/primitives/preview-image.js +4 -2
- package/dist/primitives/textarea.js +1 -1
- package/dist/primitives/typed-title.js +12 -14
- package/dist/primitives/typing-animation.js +4 -2
- package/dist/providers/ui-provider.d.ts +57 -0
- package/dist/providers/ui-provider.js +4 -0
- package/dist/session/composer/ComposerCard.d.ts +2 -0
- package/dist/session/composer/ComposerCard.js +5 -1
- package/dist/session/composer/agent-selector.js +17 -5
- package/dist/session/composer/composer-chat-input.js +5 -3
- package/dist/session/composer/composer-logic.js +8 -7
- package/dist/session/composer/composer-toolbar.js +4 -2
- package/dist/session/composer/composer-underbar.js +3 -1
- package/dist/session/composer/composer.js +34 -5
- package/dist/session/composer/draft/composer-draft.js +4 -4
- package/dist/session/composer/menus/slash-actions.d.ts +22 -2
- package/dist/session/composer/menus/slash-actions.js +20 -0
- package/dist/session/composer/menus/slash-items.js +19 -3
- package/dist/session/composer/pasted-text.js +3 -2
- package/dist/session/composer/stores/queued-draft-store.js +3 -2
- package/dist/session/header/reminder-parts.d.ts +294 -0
- package/dist/session/header/reminder-parts.js +120 -0
- package/dist/session/header/session-reminders-indicator.d.ts +13 -0
- package/dist/session/header/session-reminders-indicator.js +142 -35
- package/dist/session/media/sandbox-url-detector.js +0 -7
- package/dist/session/models/model-availability.d.ts +23 -0
- package/dist/session/models/model-availability.js +20 -0
- package/dist/session/models/model-grouping.js +1 -4
- package/dist/session/models/model-selector.js +23 -14
- package/dist/session/models/use-model-connection-gate.js +4 -1
- package/dist/session/outcomes/change-request-outcomes.js +24 -3
- package/dist/session/outcomes/session-outcomes-provider.d.ts +3 -1
- package/dist/session/outcomes/session-outcomes-provider.js +31 -3
- package/dist/session/overrides/provider-secret-pool-editor.js +2 -1
- package/dist/session/overrides/session-overrides-composer.d.ts +1 -1
- package/dist/session/overrides/session-overrides-composer.js +5 -1
- package/dist/session/panel/advanced/advanced-panel.js +3 -7
- package/dist/session/panel/easy/app-preview.js +12 -2
- package/dist/session/panel/easy/easy-panel-logic.d.ts +1 -1
- package/dist/session/panel/easy/easy-panel-logic.js +1 -1
- package/dist/session/panel/easy/outputs-card.js +2 -1
- package/dist/session/panel/easy/panel-card.js +5 -3
- package/dist/session/panel/easy/preview-load.d.ts +14 -0
- package/dist/session/panel/easy/preview-load.js +15 -0
- package/dist/session/panel/easy/step-detail-body.js +2 -7
- package/dist/session/panel/mobile-tool-drawer.js +4 -2
- package/dist/session/panel/open-session-quick-view.js +2 -1
- package/dist/session/panel/panel-fallback.js +8 -4
- package/dist/session/panel/session-panel-provider.d.ts +1 -0
- package/dist/session/panel/session-panel-provider.js +19 -3
- package/dist/session/panel/session-reminders-panel.d.ts +4 -0
- package/dist/session/panel/session-reminders-panel.js +109 -0
- package/dist/session/panel/shared/use-panel-derivations.js +6 -4
- package/dist/session/panel/shared/use-panel-handoffs.d.ts +2 -2
- package/dist/session/panel/shared/use-panel-handoffs.js +5 -1
- package/dist/session/panel/use-session-panel-layout.js +7 -5
- package/dist/session/print/session-print-header.d.ts +0 -10
- package/dist/session/print/session-print-header.js +25 -9
- package/dist/session/status/session-surface.js +1 -1
- package/dist/session/thread/optimistic-turn.d.ts +5 -1
- package/dist/session/thread/optimistic-turn.js +8 -6
- package/dist/session/thread/session-chat.d.ts +6 -1
- package/dist/session/thread/session-chat.js +34 -12
- package/dist/session/thread/session-history.js +3 -1
- package/dist/session/thread/session-thread.d.ts +10 -1
- package/dist/session/thread/session-thread.js +2 -2
- package/dist/session/thread/transcript.js +13 -7
- package/dist/session/tool/shared/file-list.js +19 -2
- package/dist/session/tool/shared/infrastructure-contexts.d.ts +7 -0
- package/dist/session/tool/shared/infrastructure-contexts.js +7 -0
- package/dist/session/tool/shared/infrastructure-output.js +9 -5
- package/dist/session/tool/shared/infrastructure-row-header.js +9 -1
- package/dist/session/tool/shared/infrastructure-rows.js +7 -5
- package/dist/session/tool/tools/bash-tool.js +10 -6
- package/dist/session/tool/tools/web-search-tool.js +25 -3
- package/dist/session/turn/burst-step-body.js +4 -0
- package/dist/session/turn/recovered-failures.d.ts +8 -0
- package/dist/session/turn/recovered-failures.js +32 -0
- package/dist/session/turn/streaming-cadence.js +5 -8
- package/dist/styles.css +1 -1
- package/dist/terminal/session-terminal-panel.js +3 -5
- package/dist/theme/theme.js +2 -1
- package/dist/workspace/projects/modal/github-import-pickers.js +7 -6
- package/dist/workspace/review/review-center.js +13 -10
- package/dist/workspace/session-workspace.js +5 -9
- package/dist/workspace/sessions/deleted-session-row.js +5 -2
- package/dist/workspace/sidebar/footer/project-computer-nav.js +6 -4
- package/dist/workspace/sidebar/footer/project-mcp-nav.js +6 -2
- package/dist/workspace/sidebar/modal/rename-session-modal.js +8 -3
- package/dist/workspace/sidebar/project-session-list.js +52 -55
- package/dist/workspace/sidebar/session-brief-hover-card.d.ts +35 -18
- package/dist/workspace/sidebar/session-brief-hover-card.js +114 -37
- package/dist/workspace/sidebar/session-status-mark.d.ts +15 -1
- package/dist/workspace/sidebar/session-status-mark.js +63 -17
- package/package.json +7 -3
- package/skills/kortix-ui/SKILL.md +1 -1
- package/dist/customize/sections/view/sandbox-provider-result.d.ts +0 -47
- package/dist/customize/sections/view/sandbox-provider-result.js +0 -79
|
@@ -1,44 +1,128 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx,
|
|
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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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 {
|
|
17
|
-
import
|
|
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 {
|
|
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 {
|
|
25
|
-
import {
|
|
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 =
|
|
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 "
|
|
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
|
|
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
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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 {
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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;
|