@kortix/ui 0.14.10 → 0.14.11
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/dist/_vendor/shared/runtime-versions.d.ts +9 -0
- package/dist/customize/settings/tabs/sandbox-template-form.js +8 -1
- package/dist/files/renderers/show-content-renderer.d.ts +2 -2
- package/dist/files/renderers/show-content-renderer.js +2 -1
- package/dist/i18n/en.d.ts +17 -0
- package/dist/i18n/en.js +29 -12
- package/dist/i18n/ui-catalog/remaining-ui-translation-keys.generated.js +3 -3
- package/dist/internal/branding/branding-provider.js +3 -3
- package/dist/internal/hooks/use-notification-center.d.ts +10 -6
- package/dist/internal/hooks/use-notification-center.js +32 -10
- package/dist/internal/lib/site-metadata.js +4 -4
- package/dist/internal/notifications/notification-bell.js +4 -3
- package/dist/internal/notifications/notification-rows.d.ts +1 -1
- package/dist/internal/notifications/notification-rows.js +2 -0
- package/dist/internal/stores/kortix-computer-store.d.ts +1 -1
- package/dist/session/composer/send-stop-control.js +8 -3
- package/dist/session/header/session-site-header.js +3 -1
- package/dist/session/panel/easy/detail-view.js +4 -0
- package/dist/session/panel/mobile-tool-drawer.js +5 -3
- package/dist/session/panel/open-session-quick-view.js +2 -2
- package/dist/session/panel/panel-fallback.js +10 -7
- package/dist/session/panel/session-desktop.d.ts +6 -0
- package/dist/session/panel/session-desktop.js +74 -0
- package/dist/session/panel/session-panel-provider.d.ts +1 -0
- package/dist/session/panel/session-panel-provider.js +17 -1
- 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 +8 -0
- package/dist/session/status/session-tab-title.d.ts +1 -1
- package/dist/session/status/session-tab-title.js +1 -1
- package/dist/session/thread/session-chat.js +12 -0
- package/dist/session/thread/session-context-sub-sessions.js +1 -1
- package/dist/session/tool/tools/image-gen-tool.d.ts +1 -1
- package/dist/session/tool/tools/image-gen-tool.js +17 -8
- package/dist/styles.css +1 -1
- package/dist/workspace/session-workspace.js +24 -5
- package/package.json +3 -3
|
@@ -4,6 +4,7 @@ import { Button } from '../../primitives/button.js';
|
|
|
4
4
|
import Hint from '../../primitives/hint.js';
|
|
5
5
|
import { Kbd } from '../../primitives/kbd.js';
|
|
6
6
|
import Loading from '../../primitives/loading.js';
|
|
7
|
+
import { cn } from '../../lib/utils.js';
|
|
7
8
|
import { ArrowUpIcon as ArrowUp, SquareIcon } from '@phosphor-icons/react';
|
|
8
9
|
import { AnimatePresence, m } from 'motion/react';
|
|
9
10
|
import { useTranslations } from '../../i18n/index.js';
|
|
@@ -24,6 +25,7 @@ const ICON_SWAP = {
|
|
|
24
25
|
export function SendStopControl({ isSending, isBusy, onStop, stopDisabled, canStop = true, escCount, lockForQuestion, questionButtonLabel, submitLabel = null, questionCanAct, hasText, canSubmit, submitDisabled, disabled, modelUnavailable, agentUnavailable = false, attachmentFailed = false, attachmentUnsupported = null, onSubmit, }) {
|
|
25
26
|
const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
|
|
26
27
|
const t = useTranslations('threads');
|
|
28
|
+
const tDesktop = useTranslations('sessionDesktop');
|
|
27
29
|
const tAttachments = useTranslations('hardcodedUi.composerAttachments');
|
|
28
30
|
// One line, the same one the agent picker's tooltip carries — the two
|
|
29
31
|
// controls are refusing for one reason and must not word it two ways.
|
|
@@ -54,9 +56,12 @@ export function SendStopControl({ isSending, isBusy, onStop, stopDisabled, canSt
|
|
|
54
56
|
// alone, a click meant to send it ended the run and left it unsent. Send
|
|
55
57
|
// goes out the way Enter does; Stop steps back to secondary beside it.
|
|
56
58
|
const followUp = hasText || canSubmit;
|
|
57
|
-
const stop = (_jsxs("div", { className: "relative flex items-center", children: [escCount > 0 && (
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
const stop = (_jsxs("div", { className: "relative flex items-center", children: [escCount > 0 && (
|
|
60
|
+
// Progress toward the triple-ESC stop: one pip per press, so the
|
|
61
|
+
// count reads at a glance instead of as a bare "×2".
|
|
62
|
+
_jsx("div", { role: "status", className: "animate-in fade-in-0 zoom-in-95 slide-in-from-bottom-2 pointer-events-none absolute right-1/2 bottom-full mb-2 translate-x-1/2 duration-(--duration-normal)", children: _jsxs("div", { className: "bg-background text-foreground inline-flex w-fit items-center gap-2 rounded-md border px-2 py-1 text-xs whitespace-nowrap", children: [_jsx("span", { className: "flex items-center gap-1", "aria-hidden": true, children: [0, 1, 2].map((pip) => (_jsx("span", { className: cn('size-1.5 rounded-full', pip < escCount ? 'bg-foreground' : 'bg-muted-foreground/30') }, pip))) }), _jsxs("span", { className: "flex items-center gap-1", children: [_jsx(Kbd, { className: "h-4 w-auto", children: tDesktop('escape') }), _jsx("span", { className: "tabular-nums", children: escCount === 1
|
|
63
|
+
? tI18nComplete.raw('textb9ef397631ca')
|
|
64
|
+
: tI18nComplete.raw('text9754e2d14f9e') })] })] }) })), _jsx(Hint, { side: "top", label: _jsxs("span", { className: "flex items-center gap-2", children: [tI18nComplete.raw('textcae7d57bc067'), _jsxs("span", { className: "flex items-center gap-1 opacity-60", children: [_jsx(Kbd, { className: "h-4 w-auto", children: tDesktop('escape') }), _jsx("span", { className: "tabular-nums", children: "\u00D73" })] })] }), children: _jsx(Button, { size: "icon-base", variant: followUp ? 'secondary' : 'default', "aria-label": t('stop'), onClick: onStop, disabled: stopDisabled || !onStop, className: ICON_BUTTON, children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: _jsx(m.span, { className: "flex items-center", ...ICON_SWAP, children: _jsx(SquareIcon, { weight: "fill", className: "size-4 shrink-0" }) }, "stop") }) }) })] }));
|
|
60
65
|
if (!followUp)
|
|
61
66
|
return stop;
|
|
62
67
|
return (_jsxs("div", { className: "flex items-center gap-1.5", children: [stop, sendButton] }));
|
|
@@ -42,7 +42,7 @@ import { useSessionHistoryViewStore } from '../../internal/stores/session-histor
|
|
|
42
42
|
import { useIsActionPanelOpen, useReadyChip, useToggleActionPanel, } from '../../internal/stores/kortix-computer-store.js';
|
|
43
43
|
import { createProjectSession, directSubsessions, restartProjectSession, stopProjectSession } from '@kortix/sdk';
|
|
44
44
|
import { qk, useProjectSession, useRuntimeSupports, useSessionParticipants, useSessionWatch, } from '@kortix/sdk/react';
|
|
45
|
-
import { ArchiveIcon, ArrowsClockwiseIcon, BellIcon, BellSlashIcon, CaretDoubleLeftIcon, GitForkIcon, LinkSimpleIcon, CaretDownIcon, CodeSimpleIcon as Code2, FileArrowDownIcon as FileDown, FolderSimpleIcon, GlobeSimpleIcon, StackIcon as Layers, PencilSimpleIcon, PushPinIcon, PushPinSlashIcon, ArrowCounterClockwiseIcon as RotateCcw, ShareIcon as Share, SquareIcon as Square, TerminalIcon, TrashIcon, } from '@phosphor-icons/react';
|
|
45
|
+
import { ArchiveIcon, ArrowsClockwiseIcon, BellIcon, BellSlashIcon, CaretDoubleLeftIcon, GitForkIcon, LinkSimpleIcon, CaretDownIcon, CodeSimpleIcon as Code2, FileArrowDownIcon as FileDown, FolderSimpleIcon, GlobeSimpleIcon, MonitorIcon, StackIcon as Layers, PencilSimpleIcon, PushPinIcon, PushPinSlashIcon, ArrowCounterClockwiseIcon as RotateCcw, ShareIcon as Share, SquareIcon as Square, TerminalIcon, TrashIcon, } from '@phosphor-icons/react';
|
|
46
46
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
47
47
|
import { useMemo, useRef, useState } from 'react';
|
|
48
48
|
/** Sandbox surfaces reachable from the header. One list drives both the
|
|
@@ -51,6 +51,8 @@ import { useMemo, useRef, useState } from 'react';
|
|
|
51
51
|
const DEV_TOOLS = [
|
|
52
52
|
{ view: 'terminal', label: 'Terminal', Icon: TerminalIcon },
|
|
53
53
|
{ view: 'browser', label: 'Browser', Icon: GlobeSimpleIcon },
|
|
54
|
+
// The computer's live screen (`session-desktop.tsx`).
|
|
55
|
+
{ view: 'desktop', label: 'Desktop', Icon: MonitorIcon },
|
|
54
56
|
{ view: 'files', label: 'Files', Icon: FolderSimpleIcon },
|
|
55
57
|
];
|
|
56
58
|
export function SessionSiteHeader({ sessionId, sessionTitle, isMobileView, leadingAction, parent, }) {
|
|
@@ -116,6 +116,10 @@ function useDetailKeyboard(detail, isMobile, onBack, detailRef) {
|
|
|
116
116
|
if (isTypingTarget(event.target))
|
|
117
117
|
return;
|
|
118
118
|
if (event.key === 'Escape') {
|
|
119
|
+
// Mark the press consumed: the page-wide triple-ESC-to-stop
|
|
120
|
+
// (`session-chat.tsx`) skips a `defaultPrevented` Escape. Without it,
|
|
121
|
+
// closing three full-screen images in a row stopped a running turn.
|
|
122
|
+
event.preventDefault();
|
|
119
123
|
onBack();
|
|
120
124
|
return;
|
|
121
125
|
}
|
|
@@ -22,6 +22,7 @@ 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
24
|
import { SessionRemindersPanel } from './session-reminders-panel.js';
|
|
25
|
+
import { SessionDesktop } from './session-desktop.js';
|
|
25
26
|
import { SessionFilesExplorer } from './session-files-explorer.js';
|
|
26
27
|
import { SessionTerminalPanel } from '../../terminal/session-terminal-panel.js';
|
|
27
28
|
import { SessionBootStatus } from '../status/session-starting-loader.js';
|
|
@@ -29,13 +30,14 @@ import { useOptionalSessionPanel } from './session-panel-provider.js';
|
|
|
29
30
|
import { useLocalizedUiCatalog } from '../../i18n/ui-catalog/use-localized-ui-catalog.js';
|
|
30
31
|
import { useKortixComputerStore } from '../../internal/stores/kortix-computer-store.js';
|
|
31
32
|
import { sessionPreviewTabId } from '../../internal/stores/session-browser-store.js';
|
|
32
|
-
import { AlarmIcon, FolderOpenIcon, GlobeIcon, ScrollIcon, TerminalWindowIcon, } from '@phosphor-icons/react';
|
|
33
|
+
import { AlarmIcon, FolderOpenIcon, GlobeIcon, MonitorIcon, ScrollIcon, TerminalWindowIcon, } from '@phosphor-icons/react';
|
|
33
34
|
const TOOL_META = {
|
|
34
35
|
terminal: { label: 'Terminal', Icon: TerminalWindowIcon },
|
|
35
36
|
browser: { label: 'Browser', Icon: GlobeIcon },
|
|
36
37
|
files: { label: 'Files', Icon: FolderOpenIcon },
|
|
37
38
|
audit: { label: 'Audit', Icon: ScrollIcon },
|
|
38
39
|
reminders: { label: 'Reminders', Icon: AlarmIcon },
|
|
40
|
+
desktop: { label: 'Desktop', Icon: MonitorIcon },
|
|
39
41
|
};
|
|
40
42
|
export function MobileToolDrawer({ sessionId, projectId, projectSessionId, }) {
|
|
41
43
|
const view = useKortixComputerStore((s) => s.mobileToolView);
|
|
@@ -45,6 +47,6 @@ export function MobileToolDrawer({ sessionId, projectId, projectSessionId, }) {
|
|
|
45
47
|
// Terminal, Browser and Files need the computer: while it boots, the drawer
|
|
46
48
|
// shows the wake status in their place.
|
|
47
49
|
const booting = !!useOptionalSessionPanel()?.booting &&
|
|
48
|
-
(view === 'terminal' || view === 'browser' || view === 'files');
|
|
49
|
-
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: [booting && _jsx(SessionBootStatus, { projectId: projectId, sessionId: projectSessionId }), !booting && view === 'terminal' && (_jsx(SessionTerminalPanel, { sessionId: sessionId, projectId: projectId, projectSessionId: projectSessionId })), !booting && view === 'browser' && (_jsx(BrowserPanel, { tabId: sessionPreviewTabId(sessionId), projectId: projectId, projectSessionId: projectSessionId })), !booting && 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 }) }))] })] }) }));
|
|
50
|
+
(view === 'terminal' || view === 'browser' || view === 'files' || view === 'desktop');
|
|
51
|
+
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: [booting && _jsx(SessionBootStatus, { projectId: projectId, sessionId: projectSessionId }), !booting && view === 'terminal' && (_jsx(SessionTerminalPanel, { sessionId: sessionId, projectId: projectId, projectSessionId: projectSessionId })), !booting && view === 'browser' && (_jsx(BrowserPanel, { tabId: sessionPreviewTabId(sessionId), projectId: projectId, projectSessionId: projectSessionId })), !booting && view === 'files' && (_jsx(SessionFilesExplorer, { chatSessionId: sessionId, projectId: projectId, projectSessionId: projectSessionId })), !booting && view === 'desktop' && (_jsx(SessionDesktop, { projectId: projectId, projectSessionId: projectSessionId, onClose: closeMobileTool })), 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 }) }))] })] }) }));
|
|
50
52
|
}
|
|
@@ -55,8 +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
|
-
// Reminders
|
|
59
|
-
if (panelMode === 'advanced' && view !== 'reminders') {
|
|
58
|
+
// Reminders and Desktop are Easy details with no Advanced tab: they take the request path in both modes.
|
|
59
|
+
if (panelMode === 'advanced' && view !== 'reminders' && view !== 'desktop') {
|
|
60
60
|
if (activePanelSessionId) {
|
|
61
61
|
// `SessionPanelView` calls the file explorer 'explorer'; its 'files'
|
|
62
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 SessionDesktop = lazy(() => import('./session-desktop.js').then((m) => ({ default: m.SessionDesktop })));
|
|
29
30
|
const SessionRemindersPanel = lazy(() => import('./session-reminders-panel.js').then((m) => ({ default: m.SessionRemindersPanel })));
|
|
30
31
|
export function SessionPanelFallback({ projectId, projectSessionId, runtimeSessionId, }) {
|
|
31
32
|
useEffect(() => registerPanelFallbackHost(), []);
|
|
@@ -49,15 +50,17 @@ function PanelViewModal({ projectId, projectSessionId, runtimeSessionId }) {
|
|
|
49
50
|
? tI18nComplete.raw('textbb6aea287396')
|
|
50
51
|
: view === 'reminders'
|
|
51
52
|
? tReminders('title')
|
|
52
|
-
:
|
|
53
|
-
? '
|
|
54
|
-
:
|
|
55
|
-
|
|
53
|
+
: view === 'desktop'
|
|
54
|
+
? 'Desktop'
|
|
55
|
+
: target?.changes
|
|
56
|
+
? 'Changes'
|
|
57
|
+
: 'Files';
|
|
58
|
+
const body = view === 'browser' ? (_jsx(AppPreview, { url: target?.url ?? '', name: title, projectId: projectId, shareContext: { projectId, sessionId: projectSessionId }, onClose: closePanelFallbackView })) : view === 'desktop' ? (_jsx(SessionDesktop, { projectId: projectId, projectSessionId: 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' }));
|
|
56
59
|
return (_jsx(Modal, { open: true, onOpenChange: (open) => !open && closePanelFallbackView(), children: _jsxs(ModalContent
|
|
57
|
-
// The browser
|
|
60
|
+
// The browser and the desktop have their own close button in their toolbar.
|
|
58
61
|
, {
|
|
59
|
-
// The browser
|
|
60
|
-
showCloseButton: view !== 'browser',
|
|
62
|
+
// The browser and the desktop have their own close button in their toolbar.
|
|
63
|
+
showCloseButton: view !== 'browser' && view !== 'desktop',
|
|
61
64
|
// A single full-bleed pane: no stacked header/body/footer spacing.
|
|
62
65
|
className: "flex h-[85vh] flex-col gap-0 space-y-0 overflow-hidden p-0 lg:max-h-[85vh] lg:max-w-5xl", "data-slot": "session-panel-fallback", children: [_jsx(VisuallyHidden, { children: _jsx(ModalTitle, { children: title }) }), _jsx("div", { className: "relative min-h-0 flex-1 overflow-hidden", children: _jsx(Suspense, { fallback: _jsx(Centered, { children: _jsx(Loading, { className: "size-5" }) }), children: body }) })] }) }));
|
|
63
66
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* `SessionDesktop` — the session computer's screen, live.
|
|
5
|
+
*
|
|
6
|
+
* Every session image runs a desktop that voxl streams on port 6901
|
|
7
|
+
* (`packages/shared/src/sandbox/desktop.ts`). This frames voxl's own web
|
|
8
|
+
* client through the sandbox proxy, the same way `AppPreview` frames a port.
|
|
9
|
+
*
|
|
10
|
+
* Control is explicit, and the screen says who has it. Kortix has it by
|
|
11
|
+
* default: a transparent shield takes every pointer and key, so a user who
|
|
12
|
+
* scrolls or clicks past the panel never moves the agent's mouse. "Take over"
|
|
13
|
+
* (or a click on the screen) drops the shield, rings the screen and focuses
|
|
14
|
+
* it; "Return control" puts the shield back.
|
|
15
|
+
*
|
|
16
|
+
* The frame unmounts while this browser tab is hidden. voxl paces every
|
|
17
|
+
* viewer to the slowest one, and a background tab stops acking frames, so a
|
|
18
|
+
* forgotten tab would slow the stream for everyone watching. It reconnects,
|
|
19
|
+
* on a fresh keyframe, when the tab is shown again.
|
|
20
|
+
*/
|
|
21
|
+
import { useTranslations } from '../../i18n/index.js';
|
|
22
|
+
import { Button } from '../../primitives/button.js';
|
|
23
|
+
import { ButtonGroup } from '../../primitives/button-group.js';
|
|
24
|
+
import Loading from '../../primitives/loading.js';
|
|
25
|
+
import { useSandboxProxy } from '../../internal/hooks/use-sandbox-proxy.js';
|
|
26
|
+
import { usePreviewFrameUrl } from '../../internal/hooks/use-preview-frame-url.js';
|
|
27
|
+
import { useIsMobile } from '../../internal/hooks/index.js';
|
|
28
|
+
import { framePolicy } from '../../files/viewer/preview-policy.js';
|
|
29
|
+
import { PanelWidthButton } from '../../files/viewer/viewer-actions.js';
|
|
30
|
+
import { CloseButton, DetailSidebarToggle } from '../../files/viewer/viewer-chrome.js';
|
|
31
|
+
import { cn } from '../../lib/utils.js';
|
|
32
|
+
import { probePreviewPort } from '@kortix/sdk';
|
|
33
|
+
import { ArrowSquareOutIcon, MonitorIcon } from '@phosphor-icons/react';
|
|
34
|
+
import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
|
35
|
+
export const DESKTOP_URL = 'http://localhost:6901/';
|
|
36
|
+
const subscribeVisibility = (onChange) => {
|
|
37
|
+
document.addEventListener('visibilitychange', onChange);
|
|
38
|
+
return () => document.removeEventListener('visibilitychange', onChange);
|
|
39
|
+
};
|
|
40
|
+
const tabVisible = () => document.visibilityState === 'visible';
|
|
41
|
+
export function SessionDesktop({ projectId, projectSessionId, onClose, }) {
|
|
42
|
+
const t = useTranslations('sessionDesktop');
|
|
43
|
+
const isMobile = useIsMobile();
|
|
44
|
+
const { proxyUrl } = useSandboxProxy();
|
|
45
|
+
const src = usePreviewFrameUrl(DESKTOP_URL, proxyUrl(DESKTOP_URL) ?? DESKTOP_URL, {
|
|
46
|
+
projectId,
|
|
47
|
+
sessionId: projectSessionId,
|
|
48
|
+
}).url;
|
|
49
|
+
const frameRef = useRef(null);
|
|
50
|
+
const [control, setControl] = useState(false);
|
|
51
|
+
// 'off' = the proxy answered 502/503/504: nothing listens on 6901.
|
|
52
|
+
const [state, setState] = useState('probing');
|
|
53
|
+
const [attempt, setAttempt] = useState(0);
|
|
54
|
+
const visible = useSyncExternalStore(subscribeVisibility, tabVisible, () => true);
|
|
55
|
+
const live = !!src && state === 'on';
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!src)
|
|
58
|
+
return;
|
|
59
|
+
const abort = new AbortController();
|
|
60
|
+
void probePreviewPort(src, { signal: abort.signal }).then((probe) => {
|
|
61
|
+
if (!abort.signal.aborted)
|
|
62
|
+
setState(probe === 'unreachable' ? 'off' : 'on');
|
|
63
|
+
});
|
|
64
|
+
return () => abort.abort();
|
|
65
|
+
}, [src, attempt]);
|
|
66
|
+
const takeOver = () => {
|
|
67
|
+
setControl(true);
|
|
68
|
+
frameRef.current?.focus();
|
|
69
|
+
};
|
|
70
|
+
return (_jsxs("div", { className: "bg-background flex h-full min-h-0 min-w-0 flex-col", children: [_jsxs("div", { className: "flex shrink-0 items-center gap-1 px-2 py-1", children: [_jsx(DetailSidebarToggle, { className: "size-7" }), _jsx(MonitorIcon, { className: "text-muted-foreground ml-1 size-3.5 shrink-0" }), _jsx("span", { className: "truncate text-sm font-medium", children: t('title') }), _jsx("div", { className: "flex-1" }), _jsxs(ButtonGroup, { className: "shrink-0", children: [_jsx(Button, { variant: "secondary", size: "icon", className: "size-7", disabled: !live, "aria-label": t('openTab'), onClick: () => src && window.open(src, '_blank', 'noopener,noreferrer'), children: _jsx(ArrowSquareOutIcon, { className: "size-3.5" }) }), _jsx(PanelWidthButton, { isMobile: isMobile }), _jsx(CloseButton, { onClose: onClose })] })] }), _jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-2 px-3 pb-3", children: [_jsx("div", { "data-slot": "desktop-screen", "data-control": control ? 'user' : 'agent', className: cn('bg-muted relative min-h-0 flex-1 overflow-hidden rounded-xl ring-1 ring-border transition-shadow', control && 'ring-2 ring-primary'), children: state === 'off' ? (_jsxs("div", { className: "text-muted-foreground flex h-full flex-col items-center justify-center gap-3 px-6 text-center text-sm", children: [_jsx(MonitorIcon, { className: "size-10 opacity-20" }), _jsx("p", { children: t('offline') }), _jsxs("p", { className: "text-xs", children: [t('start'), " ", _jsx("code", { children: "kortix-desktop start" }), "."] }), _jsx(Button, { variant: "secondary", size: "sm", onClick: () => {
|
|
71
|
+
setState('probing');
|
|
72
|
+
setAttempt((n) => n + 1);
|
|
73
|
+
}, children: t('retry') })] })) : !live || !visible ? (_jsx("div", { className: "flex h-full items-center justify-center", children: _jsx(Loading, { className: "size-4" }) })) : (_jsxs(_Fragment, { children: [_jsx("iframe", { ref: frameRef, src: src, title: t('title'), className: "h-full w-full border-0", sandbox: framePolicy('app', src).sandbox, allow: "clipboard-read; clipboard-write; autoplay; fullscreen", tabIndex: control ? 0 : -1 }), !control && (_jsx("button", { type: "button", "aria-label": t('takeOver'), "data-slot": "desktop-shield", className: "absolute inset-0 cursor-pointer", onClick: takeOver }))] })) }), _jsx("div", { className: "flex h-9 shrink-0 items-center justify-center gap-3 text-sm", children: live && (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-muted-foreground", children: control ? t('userControl') : t('agentControl') }), _jsx(Button, { variant: control ? 'secondary' : 'default', size: "sm", className: "h-7 rounded-full px-3", "aria-pressed": control, onClick: control ? () => setControl(false) : takeOver, children: control ? t('returnControl') : t('takeOver') })] })) })] })] }));
|
|
74
|
+
}
|
|
@@ -29,6 +29,7 @@ import { useTranslations } from '../../i18n/index.js';
|
|
|
29
29
|
*/
|
|
30
30
|
import { SessionAuditPanel } from './session-audit-panel.js';
|
|
31
31
|
import { SessionRemindersPanel } from './session-reminders-panel.js';
|
|
32
|
+
import { SessionDesktop } from './session-desktop.js';
|
|
32
33
|
import { SessionFilesBooting, SessionFilesExplorer } from './session-files-explorer.js';
|
|
33
34
|
import { SessionBootStatus } from '../status/session-starting-loader.js';
|
|
34
35
|
import { useSandboxProxy } from '../../internal/hooks/use-sandbox-proxy.js';
|
|
@@ -83,6 +84,7 @@ export function SessionBootGate({ boot, children }) {
|
|
|
83
84
|
}
|
|
84
85
|
export function SessionPanelProvider({ sessionId, isSessionBusy = false, projectId, projectSessionId, booting = false, children, }) {
|
|
85
86
|
const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
|
|
87
|
+
const tDesktop = useTranslations('sessionDesktop');
|
|
86
88
|
const tReminders = useTranslations('reminders');
|
|
87
89
|
const { steps, outputs, context, apps, files, isRunning, outcome } = usePanelDerivations({
|
|
88
90
|
sessionId, projectId, projectSessionId, isSessionBusy,
|
|
@@ -409,6 +411,18 @@ export function SessionPanelProvider({ sessionId, isSessionBusy = false, project
|
|
|
409
411
|
body: _jsx(SessionRemindersPanel, { projectId: projectId, projectSessionId: projectSessionId }),
|
|
410
412
|
});
|
|
411
413
|
}, [openDetail, projectId, projectSessionId, tReminders]);
|
|
414
|
+
/** The session computer's live screen (`SessionDesktop`), at the wide split
|
|
415
|
+
* a presentation gets: the stream is a 16:10 screen, not a text column. */
|
|
416
|
+
const openDesktop = useCallback(() => {
|
|
417
|
+
openDetail({
|
|
418
|
+
key: 'desktop',
|
|
419
|
+
title: tDesktop('title'),
|
|
420
|
+
hideHeader: true,
|
|
421
|
+
padded: false,
|
|
422
|
+
body: (_jsx(SessionBootGate, { boot: _jsx(SessionBootStatus, { projectId: projectId, sessionId: projectSessionId }), children: _jsx(SessionDesktop, { projectId: projectId, projectSessionId: projectSessionId, onClose: closeDetail }) })),
|
|
423
|
+
});
|
|
424
|
+
setPanelSplit(70);
|
|
425
|
+
}, [openDetail, closeDetail, setPanelSplit, projectId, projectSessionId, tDesktop]);
|
|
412
426
|
/**
|
|
413
427
|
* The opt-in File Explorer (Marko's ask). Never a default view and never a
|
|
414
428
|
* tab — it opens only when asked for, exactly like Terminal and Audit, so
|
|
@@ -451,7 +465,7 @@ export function SessionPanelProvider({ sessionId, isSessionBusy = false, project
|
|
|
451
465
|
}, [apps, handleOpenOutput]);
|
|
452
466
|
const { outputsDefaultOpen, pendingQuickView } = usePanelHandoffs({
|
|
453
467
|
sessionId, projectId, projectSessionId, isRunning, outcome, apps, files, steps,
|
|
454
|
-
detail, handleOpenOutput, openDetail, openTerminal, openAudit, openReminders, openBrowser, openFiles,
|
|
468
|
+
detail, handleOpenOutput, openDetail, openTerminal, openAudit, openReminders, openDesktop, openBrowser, openFiles,
|
|
455
469
|
});
|
|
456
470
|
// The panel comes back with the session: what it showed is remembered while
|
|
457
471
|
// it is open, and reopened on the way back while the session still runs.
|
|
@@ -584,6 +598,7 @@ export function SessionPanelProvider({ sessionId, isSessionBusy = false, project
|
|
|
584
598
|
openFiles,
|
|
585
599
|
openAudit,
|
|
586
600
|
openReminders,
|
|
601
|
+
openDesktop,
|
|
587
602
|
}), [
|
|
588
603
|
sessionId,
|
|
589
604
|
projectId,
|
|
@@ -605,6 +620,7 @@ export function SessionPanelProvider({ sessionId, isSessionBusy = false, project
|
|
|
605
620
|
openFiles,
|
|
606
621
|
openAudit,
|
|
607
622
|
openReminders,
|
|
623
|
+
openDesktop,
|
|
608
624
|
]);
|
|
609
625
|
return (_jsx(SessionPanelContext.Provider, { value: value, children: _jsx(OutputPathsContext.Provider, { value: outputPaths, children: children }) }));
|
|
610
626
|
}
|
|
@@ -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' | '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): {
|
|
3
|
+
type Handoffs = Pick<SessionPanelValue, 'sessionId' | 'projectId' | 'projectSessionId' | 'detail' | 'handleOpenOutput' | 'openDetail' | 'openTerminal' | 'openAudit' | 'openReminders' | 'openDesktop' | '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, openDesktop, 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, openReminders, openBrowser, openFiles, }) {
|
|
9
|
+
export function usePanelHandoffs({ sessionId, projectId, projectSessionId, isRunning, outcome, apps, files, steps, detail, handleOpenOutput, openDetail, openTerminal, openAudit, openReminders, openDesktop, 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.
|
|
@@ -153,6 +153,9 @@ export function usePanelHandoffs({ sessionId, projectId, projectSessionId, isRun
|
|
|
153
153
|
else if (view === 'reminders' && projectId && projectSessionId) {
|
|
154
154
|
openReminders();
|
|
155
155
|
}
|
|
156
|
+
else if (view === 'desktop') {
|
|
157
|
+
openDesktop();
|
|
158
|
+
}
|
|
156
159
|
else if (view === 'browser') {
|
|
157
160
|
openBrowser(target);
|
|
158
161
|
}
|
|
@@ -167,6 +170,7 @@ export function usePanelHandoffs({ sessionId, projectId, projectSessionId, isRun
|
|
|
167
170
|
openTerminal,
|
|
168
171
|
openAudit,
|
|
169
172
|
openReminders,
|
|
173
|
+
openDesktop,
|
|
170
174
|
openBrowser,
|
|
171
175
|
openFiles,
|
|
172
176
|
]);
|
|
@@ -157,6 +157,14 @@ function usePanelGlide(input) {
|
|
|
157
157
|
prevAspectRef.current = panelAspect;
|
|
158
158
|
prevSideSizeRef.current = sideSize;
|
|
159
159
|
const skipAnimation = consumeSkipNextExpandAnimation();
|
|
160
|
+
// Full screen is a layer OVER the chat (`SessionPanelSide`), not a split
|
|
161
|
+
// at 100%: the split stays exactly where it was, so the chat never
|
|
162
|
+
// squeezes to zero under the opening viewer. Leaving full screen falls
|
|
163
|
+
// through below as an `expandChanged` glide to the split it now wants.
|
|
164
|
+
if (isExpanded) {
|
|
165
|
+
disablePanelTransition();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
160
168
|
const changed = expandChanged || splitChanged || aspectChanged;
|
|
161
169
|
const shouldAnimate = changed && shouldShowPanel && !skipAnimation;
|
|
162
170
|
if (shouldAnimate) {
|
|
@@ -17,7 +17,7 @@ import type { ProjectSession } from '@kortix/sdk';
|
|
|
17
17
|
/** Longest session name kept before it is elided, INCLUDING the ellipsis. */
|
|
18
18
|
export declare const SESSION_TAB_TITLE_MAX_NAME = 60;
|
|
19
19
|
/**
|
|
20
|
-
* `<session name> — Kortix`. An absent name means the session could not be
|
|
20
|
+
* `<session name> — Kortix CompanyOS`. An absent name means the session could not be
|
|
21
21
|
* read, so it gets the explicit unavailable label: never an empty title, never
|
|
22
22
|
* a raw id.
|
|
23
23
|
*/
|
|
@@ -37,7 +37,7 @@ function elide(value) {
|
|
|
37
37
|
return `${value.slice(0, SESSION_TAB_TITLE_MAX_NAME - 1).trimEnd()}…`;
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
|
-
* `<session name> — Kortix`. An absent name means the session could not be
|
|
40
|
+
* `<session name> — Kortix CompanyOS`. An absent name means the session could not be
|
|
41
41
|
* read, so it gets the explicit unavailable label: never an empty title, never
|
|
42
42
|
* a raw id.
|
|
43
43
|
*/
|
|
@@ -2924,6 +2924,9 @@ export function SessionChat({ sessionId, projectSessionId, sessionState, project
|
|
|
2924
2924
|
focusInOverlay,
|
|
2925
2925
|
isComposing: e.isComposing,
|
|
2926
2926
|
})) {
|
|
2927
|
+
// A press something else consumed breaks the run: three Escapes
|
|
2928
|
+
// that each closed a viewer must never add up to a stop.
|
|
2929
|
+
clearEscHint();
|
|
2927
2930
|
return;
|
|
2928
2931
|
}
|
|
2929
2932
|
e.preventDefault();
|
|
@@ -2961,11 +2964,20 @@ export function SessionChat({ sessionId, projectSessionId, sessionState, project
|
|
|
2961
2964
|
}, 4000);
|
|
2962
2965
|
}
|
|
2963
2966
|
};
|
|
2967
|
+
// A click anywhere also breaks the run. Esc, click an image, Esc, click
|
|
2968
|
+
// the next, Esc is three separate intents, not one stop request — and it
|
|
2969
|
+
// covers any layer whose Escape handler forgets to preventDefault.
|
|
2970
|
+
const onPointerDown = () => {
|
|
2971
|
+
if (escDeadlineRef.current)
|
|
2972
|
+
clearEscHint();
|
|
2973
|
+
};
|
|
2964
2974
|
window.addEventListener('keydown', onKeyDownCapture, true);
|
|
2965
2975
|
window.addEventListener('keydown', onKeyDown);
|
|
2976
|
+
window.addEventListener('pointerdown', onPointerDown, true);
|
|
2966
2977
|
return () => {
|
|
2967
2978
|
window.removeEventListener('keydown', onKeyDownCapture, true);
|
|
2968
2979
|
window.removeEventListener('keydown', onKeyDown);
|
|
2980
|
+
window.removeEventListener('pointerdown', onPointerDown, true);
|
|
2969
2981
|
};
|
|
2970
2982
|
}, [isActiveSessionTab, readOnly, isBusy, canStop, handleStop, clearEscHint, escCount]);
|
|
2971
2983
|
// Reset when session goes idle
|
|
@@ -52,7 +52,7 @@ function WithOneThread({ source, id, rest, pricingLookup, acc, children, }) {
|
|
|
52
52
|
}
|
|
53
53
|
/**
|
|
54
54
|
* Loads a sub-agent's thread into the store, as the sub-agent's own view does (`useSessionSync`
|
|
55
|
-
* with `savedChild`):
|
|
55
|
+
* with `savedChild`): the session's log, then the runtime's. The section mounts one per requested
|
|
56
56
|
* id (a row that scrolled into view, or "Load all"), outside the tree, so collapsing a row
|
|
57
57
|
* does not unload its children. Reports whether its last runtime read failed.
|
|
58
58
|
*/
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { ToolProps } from '../shared/types.js';
|
|
2
|
-
export declare function ImageGenTool({ call }: ToolProps): import("react").JSX.Element;
|
|
2
|
+
export declare function ImageGenTool({ call }: ToolProps): import("react").JSX.Element | null;
|
|
@@ -162,6 +162,8 @@ const cappedWidth = (rect) => `min(100%, calc(${IMAGE_MAX_HEIGHT} * ${rect}))`;
|
|
|
162
162
|
* Five or more images (R38): ONE grid of six columns whose rows are the
|
|
163
163
|
* bento patterns (`imageGenGrid().rows`: 5 = 3+2, 8 = 4+4, …). Every tile sits
|
|
164
164
|
* in the same grid, placed by line numbers, so a tile is never re-parented.
|
|
165
|
+
* A strip (portrait tiles, `imageGenGrid().strip`) uses this grid at any count,
|
|
166
|
+
* with twelve columns: each row is its tiles side by side, one track tall.
|
|
165
167
|
*
|
|
166
168
|
* The grid is as wide as its tallest row allows under the cap (`width`), and
|
|
167
169
|
* each row is exactly `width / rects[k]` tall (`rects[k]` = its tile ratio ×
|
|
@@ -172,19 +174,21 @@ const cappedWidth = (rect) => `min(100%, calc(${IMAGE_MAX_HEIGHT} * ${rect}))`;
|
|
|
172
174
|
* summed below 1, e.g. 16:9 tiles.) A two-track row subtracts the gap between
|
|
173
175
|
* its tracks, so a large tile spanning both is the row's height.
|
|
174
176
|
*/
|
|
175
|
-
function gridPlacement(rows, rects) {
|
|
177
|
+
function gridPlacement(rows, rects, strip) {
|
|
176
178
|
const width = `min(100cqw, ${IMAGE_MAX_HEIGHT} * ${Math.min(...rects)})`;
|
|
177
179
|
const tracks = [];
|
|
178
180
|
const places = [];
|
|
179
181
|
rows.forEach((count, row) => {
|
|
180
182
|
const track = tracks.length + 1;
|
|
181
183
|
const height = `${width} / ${rects[row]}`;
|
|
182
|
-
if (count === 2)
|
|
184
|
+
if (strip || count === 2)
|
|
183
185
|
tracks.push(`calc(${height})`);
|
|
184
186
|
else
|
|
185
187
|
tracks.push(`calc((${height} - var(--spacing)) / 2)`, `calc((${height} - var(--spacing)) / 2)`);
|
|
186
188
|
for (let i = 0; i < count; i++) {
|
|
187
|
-
if (
|
|
189
|
+
if (strip)
|
|
190
|
+
places.push({ gridColumn: `${1 + (12 / count) * i} / span ${12 / count}`, gridRow: `${track}` });
|
|
191
|
+
else if (count === 2)
|
|
188
192
|
places.push({ gridColumn: `${1 + 3 * i} / span 3`, gridRow: `${track}` });
|
|
189
193
|
else if (count === 4)
|
|
190
194
|
places.push({ gridColumn: `${1 + 3 * (i % 2)} / span 3`, gridRow: `${track + Math.floor(i / 2)}` });
|
|
@@ -206,13 +210,17 @@ export function ImageGenTool({ call }) {
|
|
|
206
210
|
const views = useMemo(() => (run ? run.map(sessionLogOpenCall) : [call]).map((one) => imageGenView(one, inTurn ? live : undefined)), [run, call, inTurn, live]);
|
|
207
211
|
const grid = useMemo(() => imageGenGrid(views), [views]);
|
|
208
212
|
const view = views[0];
|
|
209
|
-
|
|
213
|
+
// One tile: a lone image, or one left beside calls the image cap refused.
|
|
214
|
+
const solo = grid.tiles.length === 1;
|
|
210
215
|
// One fix per card, even when several frames failed for the same reason.
|
|
211
216
|
const repairable = views.find((one) => one.error?.connectModel)?.error ??
|
|
212
217
|
views.flatMap((one) => one.images).find((image) => image.error?.connectModel)?.error;
|
|
213
218
|
const action = repairable ? _jsx(ConnectAction, { code: repairable.code, provider: repairable.provider }) : null;
|
|
214
219
|
// The card carries no prompt and no model (R29): the frames are the content,
|
|
215
220
|
// their status (busy, loading, an error) the only text.
|
|
221
|
+
// The image cap refused every call before any request: nothing was asked for, so nothing to show.
|
|
222
|
+
if (!grid.tiles.length)
|
|
223
|
+
return null;
|
|
216
224
|
if (views.length === 1 && !view.images.length) {
|
|
217
225
|
// The whole call failed or stopped.
|
|
218
226
|
return (_jsx("div", { className: "w-full max-w-md", children: _jsx(FailureNote, { error: view.error, action: action }) }));
|
|
@@ -236,14 +244,15 @@ export function ImageGenTool({ call }) {
|
|
|
236
244
|
return (_jsxs("figure", { className: "flex w-full flex-col gap-2", children: [frame(grid.tiles[0]), action ? _jsx("div", { className: "flex flex-col items-start", children: action }) : null] }));
|
|
237
245
|
}
|
|
238
246
|
const count = grid.tiles.length;
|
|
239
|
-
const bento = grid.rows.length === 1 ? BENTO[count] : null;
|
|
247
|
+
const bento = !grid.strip && grid.rows.length === 1 ? BENTO[count] : null;
|
|
248
|
+
// A row's rectangle over its tile ratio: a strip row is its tiles side by side.
|
|
240
249
|
const rowRects = grid.rows.map((size, row) => {
|
|
241
250
|
const firstTile = grid.tiles[grid.rows.slice(0, row).reduce((sum, n) => sum + n, 0)];
|
|
242
|
-
return ratioValue(views[firstTile.call].aspectRatio) * BENTO[size].scale;
|
|
251
|
+
return ratioValue(views[firstTile.call].aspectRatio) * (grid.strip ? size : BENTO[size].scale);
|
|
243
252
|
});
|
|
244
|
-
const placed = bento ? null : gridPlacement(grid.rows, rowRects);
|
|
253
|
+
const placed = bento ? null : gridPlacement(grid.rows, rowRects, grid.strip);
|
|
245
254
|
return (
|
|
246
255
|
// An inline-size container: the merged grid sizes its rows from its width (`cqw`).
|
|
247
|
-
_jsxs("figure", { className: cn('flex w-full flex-col gap-2', !bento && '@container'), children: [_jsx("div", { ...(bento ? { 'data-image-gen-bento': count } : { 'data-image-gen-grid': grid.rows.join('+') }), className: cn('grid min-h-0 w-full gap-1', bento ? bento.grid : 'grid-cols-6'), style: placed ? placed.style : { aspectRatio: rowRects[0], width: cappedWidth(rowRects[0]) }, children: grid.tiles.map((tile, i) => (_jsx("div", { "data-image-gen-tile": i, className: cn('min-h-0 min-w-0', bento && i === 0 && bento.first), style: placed?.places[i], children: frame(tile) }, tileKey(tile, i)))) }), action ? _jsx("div", { className: "flex flex-col items-start", children: action }) : null] }));
|
|
256
|
+
_jsxs("figure", { className: cn('flex w-full flex-col gap-2', !bento && '@container'), children: [_jsx("div", { ...(bento ? { 'data-image-gen-bento': count } : { 'data-image-gen-grid': grid.rows.join('+'), ...(grid.strip && { 'data-image-gen-strip': '' }) }), className: cn('grid min-h-0 w-full gap-1', bento ? bento.grid : grid.strip ? 'grid-cols-12' : 'grid-cols-6'), style: placed ? placed.style : { aspectRatio: rowRects[0], width: cappedWidth(rowRects[0]) }, children: grid.tiles.map((tile, i) => (_jsx("div", { "data-image-gen-tile": i, className: cn('min-h-0 min-w-0', bento && i === 0 && bento.first), style: placed?.places[i], children: frame(tile) }, tileKey(tile, i)))) }), action ? _jsx("div", { className: "flex flex-col items-start", children: action }) : null] }));
|
|
248
257
|
}
|
|
249
258
|
ToolRegistry.register('image-gen', ImageGenTool);
|