@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.
Files changed (37) hide show
  1. package/dist/_vendor/shared/runtime-versions.d.ts +9 -0
  2. package/dist/customize/settings/tabs/sandbox-template-form.js +8 -1
  3. package/dist/files/renderers/show-content-renderer.d.ts +2 -2
  4. package/dist/files/renderers/show-content-renderer.js +2 -1
  5. package/dist/i18n/en.d.ts +17 -0
  6. package/dist/i18n/en.js +29 -12
  7. package/dist/i18n/ui-catalog/remaining-ui-translation-keys.generated.js +3 -3
  8. package/dist/internal/branding/branding-provider.js +3 -3
  9. package/dist/internal/hooks/use-notification-center.d.ts +10 -6
  10. package/dist/internal/hooks/use-notification-center.js +32 -10
  11. package/dist/internal/lib/site-metadata.js +4 -4
  12. package/dist/internal/notifications/notification-bell.js +4 -3
  13. package/dist/internal/notifications/notification-rows.d.ts +1 -1
  14. package/dist/internal/notifications/notification-rows.js +2 -0
  15. package/dist/internal/stores/kortix-computer-store.d.ts +1 -1
  16. package/dist/session/composer/send-stop-control.js +8 -3
  17. package/dist/session/header/session-site-header.js +3 -1
  18. package/dist/session/panel/easy/detail-view.js +4 -0
  19. package/dist/session/panel/mobile-tool-drawer.js +5 -3
  20. package/dist/session/panel/open-session-quick-view.js +2 -2
  21. package/dist/session/panel/panel-fallback.js +10 -7
  22. package/dist/session/panel/session-desktop.d.ts +6 -0
  23. package/dist/session/panel/session-desktop.js +74 -0
  24. package/dist/session/panel/session-panel-provider.d.ts +1 -0
  25. package/dist/session/panel/session-panel-provider.js +17 -1
  26. package/dist/session/panel/shared/use-panel-handoffs.d.ts +2 -2
  27. package/dist/session/panel/shared/use-panel-handoffs.js +5 -1
  28. package/dist/session/panel/use-session-panel-layout.js +8 -0
  29. package/dist/session/status/session-tab-title.d.ts +1 -1
  30. package/dist/session/status/session-tab-title.js +1 -1
  31. package/dist/session/thread/session-chat.js +12 -0
  32. package/dist/session/thread/session-context-sub-sessions.js +1 -1
  33. package/dist/session/tool/tools/image-gen-tool.d.ts +1 -1
  34. package/dist/session/tool/tools/image-gen-tool.js +17 -8
  35. package/dist/styles.css +1 -1
  36. package/dist/workspace/session-workspace.js +24 -5
  37. 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 && (_jsx("div", { 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 z-[9999] inline-flex w-fit items-center gap-1.5 overflow-hidden rounded-sm border p-1 px-1.5 text-xs whitespace-nowrap", children: [_jsx(Kbd, { children: "ESC" }), _jsx("span", { children: escCount === 1
58
- ? tI18nComplete.raw('textb9ef397631ca')
59
- : tI18nComplete.raw('text9754e2d14f9e') })] }) })), _jsx(Hint, { side: "top", label: _jsxs("p", { children: [tI18nComplete.raw('textcae7d57bc067'), " ", _jsx(Kbd, { children: "ESC" }), " \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") }) }) })] }));
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 is an Easy detail with no Advanced tab: it takes the request path in both modes.
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
- : target?.changes
53
- ? 'Changes'
54
- : 'Files';
55
- const body = view === 'browser' ? (_jsx(AppPreview, { url: target?.url ?? '', name: title, projectId: projectId, shareContext: { projectId, sessionId: projectSessionId }, onClose: closePanelFallbackView })) : view === 'audit' ? (_jsx(SessionAuditPanel, { projectId: projectId, projectSessionId: projectSessionId })) : view === 'reminders' ? (_jsx("div", { className: "p-4", children: _jsx(SessionRemindersPanel, { projectId: projectId, projectSessionId: projectSessionId }) })) : !runtimeSessionId ? (_jsx(Centered, { children: _jsx(Loading, { className: "size-5" }) })) : view === 'terminal' ? (_jsx(SessionTerminalPanel, { sessionId: runtimeSessionId, projectId: projectId, projectSessionId: projectSessionId })) : (_jsx(SessionFilesExplorer, { chatSessionId: runtimeSessionId, projectId: projectId, projectSessionId: projectSessionId, ephemeral: true, initialMode: target?.changes ? 'changes' : 'files' }));
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 has its own close button in its toolbar.
60
+ // The browser and the desktop have their own close button in their toolbar.
58
61
  , {
59
- // The browser has its own close button in its toolbar.
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,6 @@
1
+ export declare const DESKTOP_URL = "http://localhost:6901/";
2
+ export declare function SessionDesktop({ projectId, projectSessionId, onClose, }: {
3
+ projectId?: string;
4
+ projectSessionId?: string;
5
+ onClose: () => void;
6
+ }): import("react").JSX.Element;
@@ -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
+ }
@@ -32,6 +32,7 @@ export interface SessionPanelValue {
32
32
  openFiles: (changes?: boolean) => void;
33
33
  openAudit: () => void;
34
34
  openReminders: () => void;
35
+ openDesktop: () => void;
35
36
  }
36
37
  /**
37
38
  * Exported so a test can inject a stub value and render the cards (or
@@ -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`): its saved window, then the runtime's. The section mounts one per requested
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 (count === 2)
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
- const solo = views.length === 1 && view.images.length === 1;
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);