@kortix/ui 0.14.10 → 0.14.12

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 (46) hide show
  1. package/dist/_vendor/shared/runtime-versions.d.ts +9 -0
  2. package/dist/browser/browser-panel.d.ts +2 -1
  3. package/dist/browser/browser-panel.js +12 -2
  4. package/dist/customize/settings/tabs/sandbox-template-form.js +8 -1
  5. package/dist/files/renderers/show-content-renderer.d.ts +2 -2
  6. package/dist/files/renderers/show-content-renderer.js +2 -1
  7. package/dist/i18n/en.d.ts +18 -0
  8. package/dist/i18n/en.js +30 -12
  9. package/dist/i18n/ui-catalog/remaining-ui-translation-keys.generated.js +3 -3
  10. package/dist/internal/branding/branding-provider.js +3 -3
  11. package/dist/internal/hooks/use-notification-center.d.ts +10 -6
  12. package/dist/internal/hooks/use-notification-center.js +32 -10
  13. package/dist/internal/lib/browser-noise/rules/extensions.js +3 -0
  14. package/dist/internal/lib/site-metadata.js +4 -4
  15. package/dist/internal/notifications/notification-bell.js +4 -3
  16. package/dist/internal/notifications/notification-rows.d.ts +1 -1
  17. package/dist/internal/notifications/notification-rows.js +2 -0
  18. package/dist/internal/stores/kortix-computer-store.d.ts +3 -2
  19. package/dist/internal/stores/kortix-computer-store.js +5 -3
  20. package/dist/session/composer/index.d.ts +1 -0
  21. package/dist/session/composer/index.js +1 -0
  22. package/dist/session/composer/send-stop-control.js +8 -3
  23. package/dist/session/header/session-site-header.js +3 -1
  24. package/dist/session/panel/easy/detail-view.js +11 -0
  25. package/dist/session/panel/mobile-tool-drawer.js +10 -4
  26. package/dist/session/panel/open-session-quick-view.js +3 -3
  27. package/dist/session/panel/panel-fallback.js +10 -7
  28. package/dist/session/panel/session-desktop.d.ts +6 -0
  29. package/dist/session/panel/session-desktop.js +74 -0
  30. package/dist/session/panel/session-panel-provider.d.ts +1 -0
  31. package/dist/session/panel/session-panel-provider.js +17 -1
  32. package/dist/session/panel/shared/use-panel-handoffs.d.ts +2 -2
  33. package/dist/session/panel/shared/use-panel-handoffs.js +5 -1
  34. package/dist/session/panel/use-session-panel-layout.js +8 -0
  35. package/dist/session/status/session-tab-title.d.ts +1 -1
  36. package/dist/session/status/session-tab-title.js +1 -1
  37. package/dist/session/thread/session-chat.js +12 -0
  38. package/dist/session/thread/session-context-sub-sessions.js +1 -1
  39. package/dist/session/tool/tools/image-gen-tool.d.ts +1 -1
  40. package/dist/session/tool/tools/image-gen-tool.js +17 -8
  41. package/dist/session/turn/user-message.d.ts +5 -4
  42. package/dist/session/turn/user-message.js +7 -5
  43. package/dist/styles.css +1 -1
  44. package/dist/workspace/session-workspace.js +24 -5
  45. package/package.json +3 -3
  46. package/skills/kortix-ui/references/modules.md +1 -1
@@ -4,7 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
4
4
  * The bell (KRTX-1742): the person's notification inbox, across projects.
5
5
  *
6
6
  * It sits in the project sidebar header and in `AccountTopBar`, only while the
7
- * `notification_center` flag is on (`useNotificationCenter`). The badge is
7
+ * inbox gate is on (`useNotificationInboxEnabled`). The badge is
8
8
  * the server's unread count, clamped at 99+. A row opens its session (or the
9
9
  * Triggers page for an alert) and marks itself read. When this browser shows
10
10
  * no notifications yet, the panel's footer offers to turn them on.
@@ -33,7 +33,7 @@ import { useNotificationInbox } from '@kortix/sdk/react';
33
33
  import { BellIcon, ChatCircleIcon, QuestionIcon, ShareIcon, ShieldWarningIcon, WarningIcon, XCircleIcon, XIcon, } from '@phosphor-icons/react';
34
34
  import { useState } from 'react';
35
35
  import { badgeCount, tabRows, tabUnreadCount, withoutNotificationParam, } from './notification-rows.js';
36
- import { useNotificationCenter } from '../hooks/use-notification-center.js';
36
+ import { useNotificationInboxEnabled } from '../hooks/use-notification-center.js';
37
37
  import { webPushSupported } from './web-push.js';
38
38
  /** Every kind glyph is solid: Phosphor at `weight="fill"`, the done check is `SolidCheckIcon`. */
39
39
  const solid = (Icon) => function SolidKindIcon(props) {
@@ -51,6 +51,7 @@ const KIND_MARK = {
51
51
  capture_device_down: { icon: solid(WarningIcon), className: 'text-kortix-red' },
52
52
  capture_device_recovered: { icon: SolidCheckIcon, className: 'text-kortix-green' },
53
53
  message: { icon: solid(ChatCircleIcon), className: 'text-muted-foreground' },
54
+ artifact_comment: { icon: solid(ChatCircleIcon), className: 'text-muted-foreground' },
54
55
  };
55
56
  /** Props only: the header, the All / Needs you / Done tabs, the rows, and the footer offer. */
56
57
  export function NotificationPanel({ rows, state, unreadCount, prompt, onOpenRow, onMarkAllRead, onRetry, onTurnOn, onDismissPrompt, }) {
@@ -96,7 +97,7 @@ export function NotificationBell({ projectId, onNavigate, surface = 'bg-backgrou
96
97
  const t = useTranslations('notifications');
97
98
  const viewerId = useUiViewerId();
98
99
  const [open, setOpen] = useState(false);
99
- const notificationCenter = useNotificationCenter(projectId);
100
+ const notificationCenter = useNotificationInboxEnabled(projectId);
100
101
  const inbox = useNotificationInbox({ userId: viewerId, enabled: notificationCenter });
101
102
  const notificationsOn = useWebNotificationStore((s) => s.preferences.enabled);
102
103
  const permission = useWebNotificationStore((s) => s.permission);
@@ -55,7 +55,7 @@ export declare function notificationTag(row: InboxNotification): string;
55
55
  * conversation (a message), its session, the Reminders page (a reminder
56
56
  * alert), or the Triggers page.
57
57
  */
58
- export declare function rowDestination(row: InboxNotification): 'room' | 'session' | 'reminders' | 'triggers';
58
+ export declare function rowDestination(row: InboxNotification): 'artifact' | 'room' | 'session' | 'reminders' | 'triggers';
59
59
  /** The message `public/sw.js` posts to a tab already on a clicked notification's page. */
60
60
  export declare const NOTIFICATION_OPEN_MESSAGE = "kortix:notification-open";
61
61
  /** The notification id a service-worker message says this tab opened, or null. */
@@ -71,6 +71,8 @@ export function notificationTag(row) {
71
71
  * alert), or the Triggers page.
72
72
  */
73
73
  export function rowDestination(row) {
74
+ if (row.artifact_id)
75
+ return 'artifact';
74
76
  if (row.room_id)
75
77
  return 'room';
76
78
  if (row.session_id)
@@ -12,7 +12,7 @@ export interface ReadyChipState {
12
12
  primaryName?: string;
13
13
  }
14
14
  /** The panel surfaces reachable from outside the panel. */
15
- export type QuickView = 'terminal' | 'audit' | 'browser' | 'files' | 'reminders';
15
+ export type QuickView = 'terminal' | 'audit' | 'browser' | 'files' | 'reminders' | 'desktop';
16
16
  /**
17
17
  * Extra aim for a quick-view request — "open the browser" vs "open the browser
18
18
  * ON THIS URL".
@@ -100,6 +100,7 @@ interface KortixComputerState {
100
100
  target?: QuickViewTarget;
101
101
  } | null;
102
102
  mobileToolView: QuickView | null;
103
+ mobileToolTarget: QuickViewTarget | null;
103
104
  openFileInComputer: (filePath: string, filePathList?: string[], targetLine?: number) => void;
104
105
  openFileBrowser: () => void;
105
106
  navigateToToolCall: (toolIndex: number) => void;
@@ -196,7 +197,7 @@ interface KortixComputerState {
196
197
  target?: QuickViewTarget;
197
198
  } | null;
198
199
  /** Mobile only — open `view` in the standalone tool drawer. */
199
- openMobileTool: (view: QuickView) => void;
200
+ openMobileTool: (view: QuickView, target?: QuickViewTarget) => void;
200
201
  closeMobileTool: () => void;
201
202
  reset: () => void;
202
203
  }
@@ -29,6 +29,7 @@ const initialState = {
29
29
  pendingPrimaryOpenSessionId: null,
30
30
  pendingQuickView: null,
31
31
  mobileToolView: null,
32
+ mobileToolTarget: null,
32
33
  /** A quick-view consumed by the transient BOOT shell (e.g. Terminal clicked
33
34
  * while the sandbox boots). The shell's provider dies at the boot→ready
34
35
  * handoff, taking the view with it — this remembers the intent so
@@ -283,6 +284,7 @@ export const useKortixComputerStore = create()(devtools((set, get) => ({
283
284
  pendingToolNavIndex: null,
284
285
  // A tool drawer belonging to the session the user just left.
285
286
  mobileToolView: null,
287
+ mobileToolTarget: null,
286
288
  // The ⌘I restore memory dies with the session. Within a session the
287
289
  // key is a minimise/restore pair; across one it is not, and a detail
288
290
  // last seen on a page the user has navigated away from must never be
@@ -421,11 +423,11 @@ export const useKortixComputerStore = create()(devtools((set, get) => ({
421
423
  }
422
424
  return { view: pending.view, target: pending.target };
423
425
  },
424
- openMobileTool: (view) => {
425
- set({ mobileToolView: view });
426
+ openMobileTool: (view, target) => {
427
+ set({ mobileToolView: view, mobileToolTarget: target ? { ...target } : null });
426
428
  },
427
429
  closeMobileTool: () => {
428
- set({ mobileToolView: null });
430
+ set({ mobileToolView: null, mobileToolTarget: null });
429
431
  },
430
432
  reset: () => {
431
433
  console.log('[KortixComputerStore] Full reset');
@@ -15,6 +15,7 @@ export { type DraftScope } from './draft/composer-draft.js';
15
15
  export { ComposerEditor, type ComposerEditorHandle } from './editor/composer-editor.js';
16
16
  export { baseExtensions } from './editor/extensions.js';
17
17
  export { MentionNode } from './editor/mention-node.js';
18
+ export { MentionChip } from '../thread/mention-chip.js';
18
19
  export { baseSuggestion, type MenuController } from './editor/suggestion.js';
19
20
  export { ReasoningEffortSelector } from './reasoning-effort-selector.js';
20
21
  export { SessionChatInput, type SessionChatInputProps } from './session-chat-input.js';
@@ -14,6 +14,7 @@ export { ComposerUnderbar } from './composer-underbar.js';
14
14
  export { ComposerEditor } from './editor/composer-editor.js';
15
15
  export { baseExtensions } from './editor/extensions.js';
16
16
  export { MentionNode } from './editor/mention-node.js';
17
+ export { MentionChip } from '../thread/mention-chip.js';
17
18
  export { baseSuggestion } from './editor/suggestion.js';
18
19
  export { ReasoningEffortSelector } from './reasoning-effort-selector.js';
19
20
  export { SessionChatInput } from './session-chat-input.js';
@@ -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, }) {
@@ -22,6 +22,7 @@ import { ButtonGroup } from '../../../primitives/button-group.js';
22
22
  import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from '../../../primitives/drawer.js';
23
23
  import { isPlanWriteTool } from '../../turn/plan-anchor.js';
24
24
  import { cn } from '../../../lib/utils.js';
25
+ import { hasOpenFloatingLayer } from '../../../lib/z-stack.js';
25
26
  import { focusWithoutScroll } from '../../../internal/lib/utils/focus-without-scroll.js';
26
27
  import { CaretLeftIcon as ChevronLeft, CaretRightIcon as ChevronRight, } from '@phosphor-icons/react';
27
28
  import { AnimatePresence, m, useReducedMotion, } from 'motion/react';
@@ -116,6 +117,16 @@ function useDetailKeyboard(detail, isMobile, onBack, detailRef) {
116
117
  if (isTypingTarget(event.target))
117
118
  return;
118
119
  if (event.key === 'Escape') {
120
+ if (event.defaultPrevented || hasOpenFloatingLayer())
121
+ return;
122
+ const dialog = detailRef.current;
123
+ const coveringDialog = Array.from(document.querySelectorAll('[role="dialog"][data-state="open"],[role="alertdialog"][data-state="open"]')).some((layer) => !dialog || !layer.contains(dialog));
124
+ if (coveringDialog)
125
+ return;
126
+ // Mark the press consumed: the page-wide triple-ESC-to-stop
127
+ // (`session-chat.tsx`) skips a `defaultPrevented` Escape. Without it,
128
+ // closing three full-screen images in a row stopped a running turn.
129
+ event.preventDefault();
119
130
  onBack();
120
131
  return;
121
132
  }
@@ -19,9 +19,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
19
19
  */
20
20
  import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from '../../primitives/drawer.js';
21
21
  import { BrowserPanel } from '../../browser/browser-panel.js';
22
+ import { ArtifactPanelPreview } from '../../artifacts/artifact-session.js';
23
+ import { parseArtifactUrl } from '../../artifacts/frame-policy.js';
22
24
  import { CloseButton } from './easy/detail-view.js';
23
25
  import { SessionAuditPanel } from './session-audit-panel.js';
24
26
  import { SessionRemindersPanel } from './session-reminders-panel.js';
27
+ import { SessionDesktop } from './session-desktop.js';
25
28
  import { SessionFilesExplorer } from './session-files-explorer.js';
26
29
  import { SessionTerminalPanel } from '../../terminal/session-terminal-panel.js';
27
30
  import { SessionBootStatus } from '../status/session-starting-loader.js';
@@ -29,22 +32,25 @@ import { useOptionalSessionPanel } from './session-panel-provider.js';
29
32
  import { useLocalizedUiCatalog } from '../../i18n/ui-catalog/use-localized-ui-catalog.js';
30
33
  import { useKortixComputerStore } from '../../internal/stores/kortix-computer-store.js';
31
34
  import { sessionPreviewTabId } from '../../internal/stores/session-browser-store.js';
32
- import { AlarmIcon, FolderOpenIcon, GlobeIcon, ScrollIcon, TerminalWindowIcon, } from '@phosphor-icons/react';
35
+ import { AlarmIcon, FolderOpenIcon, GlobeIcon, MonitorIcon, ScrollIcon, TerminalWindowIcon, } from '@phosphor-icons/react';
33
36
  const TOOL_META = {
34
37
  terminal: { label: 'Terminal', Icon: TerminalWindowIcon },
35
38
  browser: { label: 'Browser', Icon: GlobeIcon },
36
39
  files: { label: 'Files', Icon: FolderOpenIcon },
37
40
  audit: { label: 'Audit', Icon: ScrollIcon },
38
41
  reminders: { label: 'Reminders', Icon: AlarmIcon },
42
+ desktop: { label: 'Desktop', Icon: MonitorIcon },
39
43
  };
40
44
  export function MobileToolDrawer({ sessionId, projectId, projectSessionId, }) {
41
45
  const view = useKortixComputerStore((s) => s.mobileToolView);
46
+ const target = useKortixComputerStore((s) => s.mobileToolTarget);
42
47
  const closeMobileTool = useKortixComputerStore((s) => s.closeMobileTool);
43
48
  const toolMeta = useLocalizedUiCatalog(TOOL_META);
44
49
  const meta = view ? toolMeta[view] : null;
50
+ const artifactUrl = view === 'browser' && target?.url && parseArtifactUrl(target.url) ? target.url : null;
45
51
  // Terminal, Browser and Files need the computer: while it boots, the drawer
46
52
  // shows the wake status in their place.
47
- 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 }) }))] })] }) }));
53
+ const booting = !!useOptionalSessionPanel()?.booting && !artifactUrl &&
54
+ (view === 'terminal' || view === 'browser' || view === 'files' || view === 'desktop');
55
+ 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 })), artifactUrl && _jsx(ArtifactPanelPreview, { url: artifactUrl, onClose: closeMobileTool }, artifactUrl), !booting && view === 'browser' && !artifactUrl && (_jsx(BrowserPanel, { tabId: sessionPreviewTabId(sessionId), projectId: projectId, projectSessionId: projectSessionId, targetUrl: target?.url })), !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
56
  }
@@ -43,7 +43,7 @@ target) {
43
43
  // for behind the close button. The panel toggle still opens the panel;
44
44
  // this path is only the "show me this one tool" intent.
45
45
  if (isMobileViewport()) {
46
- useKortixComputerStore.getState().openMobileTool(view);
46
+ useKortixComputerStore.getState().openMobileTool(view, target);
47
47
  track('panel_opened', { source, surface: 'mobile-tool', view });
48
48
  return;
49
49
  }
@@ -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;