@kortix/ui 0.14.9 → 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 (104) hide show
  1. package/dist/_vendor/kortix-shared.js +55 -2
  2. package/dist/_vendor/shared/artifact-comments.d.ts +235 -0
  3. package/dist/_vendor/shared/channel-descriptors.d.ts +96 -0
  4. package/dist/_vendor/shared/channel-message.d.ts +13 -2
  5. package/dist/_vendor/shared/index.d.ts +1 -0
  6. package/dist/_vendor/shared/runtime-versions.d.ts +9 -0
  7. package/dist/apps/app-preview-pool.js +19 -2
  8. package/dist/artifacts/artifact-frame.js +7 -3
  9. package/dist/customize/sections/component/channel-access-panel.js +4 -4
  10. package/dist/customize/sections/component/telegram-channel-row.js +7 -5
  11. package/dist/customize/sections/view/agent-editor-catalog.js +5 -0
  12. package/dist/customize/sections/view/channels-view.d.ts +2 -1
  13. package/dist/customize/sections/view/channels-view.js +12 -7
  14. package/dist/customize/settings/rail.js +9 -0
  15. package/dist/customize/settings/settings-tabs.d.ts +1 -1
  16. package/dist/customize/settings/settings-tabs.js +1 -0
  17. package/dist/customize/settings/tabs/chrome-connections-tab.d.ts +6 -0
  18. package/dist/customize/settings/tabs/chrome-connections-tab.js +99 -0
  19. package/dist/customize/settings/tabs/sandbox-template-form.js +8 -1
  20. package/dist/customize/shared/catalog/catalog-empty-state.d.ts +3 -3
  21. package/dist/customize/shared/catalog/catalog-empty-state.js +6 -4
  22. package/dist/files/renderers/image-frame.d.ts +3 -1
  23. package/dist/files/renderers/image-frame.js +2 -2
  24. package/dist/files/renderers/show-content-renderer.d.ts +2 -2
  25. package/dist/files/renderers/show-content-renderer.js +2 -1
  26. package/dist/i18n/en.d.ts +206 -0
  27. package/dist/i18n/en.js +219 -13
  28. package/dist/i18n/ui-catalog/remaining-ui-translation-keys.generated.js +3 -3
  29. package/dist/internal/apps-and-extensions/apps-modal.d.ts +9 -0
  30. package/dist/internal/apps-and-extensions/apps-modal.js +107 -0
  31. package/dist/internal/branding/branding-provider.js +3 -3
  32. package/dist/internal/chrome/browser-icon.d.ts +24 -0
  33. package/dist/internal/chrome/browser-icon.js +58 -0
  34. package/dist/internal/chrome/chrome-card-state.d.ts +16 -0
  35. package/dist/internal/chrome/chrome-card-state.js +9 -0
  36. package/dist/internal/chrome/chrome-connect.d.ts +69 -0
  37. package/dist/internal/chrome/chrome-connect.js +178 -0
  38. package/dist/internal/chrome/chrome-mock.d.ts +4 -0
  39. package/dist/internal/chrome/chrome-mock.js +15 -0
  40. package/dist/internal/chrome/chrome-names.d.ts +15 -0
  41. package/dist/internal/chrome/chrome-names.js +35 -0
  42. package/dist/internal/chrome/extension.d.ts +47 -0
  43. package/dist/internal/chrome/extension.js +91 -0
  44. package/dist/internal/chrome/use-chrome-offered.d.ts +11 -0
  45. package/dist/internal/chrome/use-chrome-offered.js +22 -0
  46. package/dist/internal/connector-logo/connector-logo-source.d.ts +2 -1
  47. package/dist/internal/connector-logo/connector-logo-source.js +7 -6
  48. package/dist/internal/hooks/use-notification-center.d.ts +10 -6
  49. package/dist/internal/hooks/use-notification-center.js +32 -10
  50. package/dist/internal/icons/edge.d.ts +5 -0
  51. package/dist/internal/icons/edge.js +10 -0
  52. package/dist/internal/lib/browser-noise/rules/extensions.js +17 -3
  53. package/dist/internal/lib/detect-os.d.ts +43 -0
  54. package/dist/internal/lib/detect-os.js +70 -0
  55. package/dist/internal/lib/site-metadata.js +4 -4
  56. package/dist/internal/lib/use-project-feature-flags.js +5 -1
  57. package/dist/internal/notifications/notification-bell.js +4 -3
  58. package/dist/internal/notifications/notification-rows.d.ts +1 -1
  59. package/dist/internal/notifications/notification-rows.js +2 -0
  60. package/dist/internal/stores/apps-modal-store.d.ts +8 -0
  61. package/dist/internal/stores/apps-modal-store.js +6 -0
  62. package/dist/internal/stores/composer-prefill-store.d.ts +12 -5
  63. package/dist/internal/stores/kortix-computer-store.d.ts +1 -1
  64. package/dist/markdown/setup-links/approval-decision.js +3 -1
  65. package/dist/primitives/empty-state.d.ts +2 -2
  66. package/dist/primitives/empty-state.js +6 -5
  67. package/dist/primitives/mermaid-renderer.js +1 -1
  68. package/dist/session/composer/send-stop-control.js +8 -3
  69. package/dist/session/header/session-site-header.js +3 -1
  70. package/dist/session/panel/easy/detail-view.js +4 -0
  71. package/dist/session/panel/mobile-tool-drawer.js +5 -3
  72. package/dist/session/panel/open-session-quick-view.js +2 -2
  73. package/dist/session/panel/panel-fallback.js +10 -7
  74. package/dist/session/panel/session-audit-shared.js +3 -1
  75. package/dist/session/panel/session-desktop.d.ts +6 -0
  76. package/dist/session/panel/session-desktop.js +74 -0
  77. package/dist/session/panel/session-panel-provider.d.ts +1 -0
  78. package/dist/session/panel/session-panel-provider.js +17 -1
  79. package/dist/session/panel/shared/use-panel-handoffs.d.ts +2 -2
  80. package/dist/session/panel/shared/use-panel-handoffs.js +5 -1
  81. package/dist/session/panel/use-session-panel-layout.js +8 -0
  82. package/dist/session/participants/session-participants.d.ts +3 -1
  83. package/dist/session/participants/session-participants.js +2 -2
  84. package/dist/session/prompts/approval-request.js +2 -0
  85. package/dist/session/status/session-tab-title.d.ts +1 -1
  86. package/dist/session/status/session-tab-title.js +1 -1
  87. package/dist/session/thread/session-chat.js +12 -0
  88. package/dist/session/thread/session-context-sub-sessions.js +1 -1
  89. package/dist/session/thread/session-thread.d.ts +13 -1
  90. package/dist/session/thread/session-thread.js +15 -5
  91. package/dist/session/thread/session-transcript-class.d.ts +5 -1
  92. package/dist/session/thread/session-transcript-class.js +5 -1
  93. package/dist/session/tool/tools/image-gen-tool.d.ts +1 -1
  94. package/dist/session/tool/tools/image-gen-tool.js +21 -11
  95. package/dist/session/turn/user-message.d.ts +7 -5
  96. package/dist/session/turn/user-message.js +14 -8
  97. package/dist/styles.css +1 -1
  98. package/dist/workspace/session-workspace.js +24 -5
  99. package/dist/workspace/sessions/session-label-badges.js +2 -1
  100. package/dist/workspace/sidebar/footer/project-chrome-nav.d.ts +10 -0
  101. package/dist/workspace/sidebar/footer/project-chrome-nav.js +56 -0
  102. package/dist/workspace/sidebar/project-sidebar.js +5 -1
  103. package/package.json +43 -3
  104. package/skills/kortix-ui/references/modules.md +1 -1
@@ -7,9 +7,11 @@
7
7
  * mount and immediately clears, so the prefill only applies once. Scoped
8
8
  * per-project so prefills don't leak across projects.
9
9
  *
10
- * A prefill fills the box. Only `submit: true` also sends it, and only the
11
- * desktop Quick Entry sets that: a prompt the person typed into the app's own
12
- * bar (`components/desktop/desktop-quick-entry.tsx`). A URL (`?q=`) never
10
+ * A prefill fills the box. Only `submit: true` also sends it, and only two
11
+ * callers set that, each for a prompt the person typed into Kortix's own
12
+ * composer: the desktop Quick Entry bar (`components/desktop/desktop-quick-entry.tsx`)
13
+ * and the Kortix in Chrome side panel (`features/chrome/panel/chrome-panel-home.tsx`).
14
+ * A URL (`?q=`) never
13
15
  * sends, because anyone can write a link. The onboarding hand-off that used to
14
16
  * auto-send its kickoff prompt now opens the first chat instead
15
17
  * (`first-chat-store.ts`).
@@ -18,8 +20,13 @@ import type { AttachedFile } from '../../session/composer/types.js';
18
20
  import type { SessionPromptOverrides } from '@kortix/sdk';
19
21
  interface ComposerPrefill {
20
22
  text: string;
21
- /** Send once it lands, exactly as if the person pressed Enter. */
22
- submit?: boolean;
23
+ /**
24
+ * Send once it lands, exactly as if the person pressed Enter. `'when-ready'`
25
+ * waits until the composer can send (models and entitlements loaded, Send
26
+ * enabled): an image for a model that cannot read one then stays in the box,
27
+ * where the composer says why.
28
+ */
29
+ submit?: boolean | 'when-ready';
23
30
  /** Files to attach (desktop Quick Entry screenshots and files). */
24
31
  files?: AttachedFile[];
25
32
  /** The agent, model and effort to select before it lands (desktop Quick Entry). */
@@ -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".
@@ -215,7 +215,9 @@ export function ApprovalDecisionPanel({ details, outcome, busyDecision, error, o
215
215
  const reviewable = approvalReviewable(details.args_preview, details.review_complete, details.resolved_preview);
216
216
  const label = resolvedLabel({
217
217
  status: expired ? 'expired' : details.status,
218
- resolution: !details.pending && details.status === 'ok'
218
+ resolution:
219
+ // `approved`: Kortix runs the held call; `ok`: an approve the caller repeats.
220
+ !details.pending && (details.status === 'ok' || details.status === 'approved')
219
221
  ? 'approve'
220
222
  : !details.pending && details.status === 'denied'
221
223
  ? 'deny'
@@ -2,9 +2,9 @@ import { type Icon as IconMynauiType, type Icon as IconType, type Icon as Lucide
2
2
  import type { ReactNode } from 'react';
3
3
  export interface EmptyStateProps {
4
4
  icon?: LucideIcon | IconMynauiType | IconType;
5
- /** Custom artwork in place of `icon`. */
5
+ /** Custom artwork in place of `EmptyArt`. */
6
6
  media?: ReactNode;
7
- title: string;
7
+ title: ReactNode;
8
8
  description?: ReactNode;
9
9
  action?: ReactNode;
10
10
  secondaryAction?: ReactNode;
@@ -3,9 +3,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  /**
4
4
  * Kortix <EmptyState> — centered empty view.
5
5
  *
6
- * Minimal. An icon, a one-line headline, an optional body, and up to two
7
- * actions (primary + secondary). A calm teaching moment rather than a
8
- * brick wall.
6
+ * Minimal. The fanned-card artwork (`EmptyArt`, with `icon` on its front card,
7
+ * or a blank document without one), a one-line headline, an optional body,
8
+ * and up to two actions (primary + secondary). Every empty view draws the
9
+ * same picture, title and body; only the glyph and the words change.
9
10
  *
10
11
  * <EmptyState
11
12
  * icon={IconInbox}
@@ -14,10 +15,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
14
15
  * action={<Button>New issue</Button>}
15
16
  * />
16
17
  */
18
+ import { EmptyArt } from './empty-art.js';
17
19
  import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, } from './empty.js';
18
20
  import { cn } from '../lib/utils.js';
19
21
  export function EmptyState({ icon: IconComponent, media, title, description, action, secondaryAction, size = 'default', className, }) {
20
- const iconSize = size === 'sm' ? 'h-8 w-8' : 'h-10 w-10';
21
22
  const maxW = size === 'sm' ? 'max-w-[240px]' : 'max-w-[320px]';
22
- return (_jsxs(Empty, { className: cn('', className), children: [_jsxs(EmptyHeader, { className: maxW, children: [media ? (_jsx(EmptyMedia, { className: "mb-5", children: media })) : (IconComponent && (_jsx(EmptyMedia, { className: "mb-4", children: _jsx(IconComponent, { className: cn(iconSize, 'text-muted-foreground') }) }))), _jsx(EmptyTitle, { className: "text-foreground text-sm font-semibold tracking-tight", children: title }), description && (_jsx(EmptyDescription, { className: "text-muted-foreground/80 mt-1.5", children: description }))] }), (action || secondaryAction) && (_jsxs(EmptyContent, { className: "flex-row justify-center gap-2", children: [action, secondaryAction] }))] }));
23
+ return (_jsxs(Empty, { className: cn('', className), children: [_jsxs(EmptyHeader, { className: maxW, children: [_jsx(EmptyMedia, { className: "mb-5", children: media ?? _jsx(EmptyArt, { icon: IconComponent }) }), _jsx(EmptyTitle, { className: "text-foreground text-sm font-semibold tracking-tight", children: title }), description && (_jsx(EmptyDescription, { className: "text-muted-foreground mt-1.5", children: description }))] }), (action || secondaryAction) && (_jsxs(EmptyContent, { className: "flex-row justify-center gap-2", children: [action, secondaryAction] }))] }));
23
24
  }
@@ -259,7 +259,7 @@ export const MermaidRenderer = React.memo(({ chart, className, enableFullscreen
259
259
  fill: var(--background) !important;
260
260
  }
261
261
  `,
262
- } }), _jsxs("div", { className: cn('mermaid-container group bg-background relative my-4 w-full overflow-auto rounded-lg border', className), children: [_jsx("div", { className: "absolute top-2 right-2 z-10 opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100", children: _jsxs(ButtonGroup, { children: [actions, enableFullscreen && (_jsx(Hint, { label: "Fullscreen", side: "top", children: _jsx(Button, { variant: "outline", size: "icon-sm", onClick: handleFullscreenOpen, className: "bg-background/90 backdrop-blur-sm", "aria-label": "Open diagram fullscreen", children: _jsx(Maximize2, { className: "size-3.5" }) }) })), _jsx(Hint, { label: copied ? 'Copied' : 'Copy source', side: "top", children: _jsx(Button, { variant: "outline", size: "icon-sm", onClick: handleCopySource, className: "bg-background/90 backdrop-blur-sm", "aria-label": "Copy diagram source", children: copied ? (_jsx(Check, { className: "text-kortix-green size-3.5" })) : (_jsx(Copy, { className: "size-3.5" })) }) })] }) }), _jsx("div", { ref: containerRef, dangerouslySetInnerHTML: { __html: renderedContent } })] }), _jsx(Modal, { open: isFullscreenOpen, onOpenChange: setIsFullscreenOpen, children: _jsxs(ModalContent, { variant: "base", showCloseButton: false, className: "flex h-[90vh] flex-col space-y-0 overflow-hidden lg:h-[90vh] lg:max-w-7xl", closeButtonChildren: _jsxs(ButtonGroup, { children: [_jsx(Hint, { label: "Zoom in", side: "bottom", children: _jsx(Button, { variant: "outline", size: "icon", onClick: handleZoomIn, "aria-label": "Zoom in", children: _jsx(ZoomIn, { className: "size-4" }) }) }), _jsx(Hint, { label: "Zoom out", side: "bottom", children: _jsx(Button, { variant: "outline", size: "icon", onClick: handleZoomOut, "aria-label": "Zoom out", children: _jsx(ZoomOut, { className: "size-4" }) }) }), _jsx(Hint, { label: "Rotate", side: "bottom", children: _jsx(Button, { variant: "outline", size: "icon", onClick: handleRotate, "aria-label": "Rotate diagram", children: _jsx(RotateCcw, { className: "size-4" }) }) }), _jsx(ModalClose, { asChild: true, children: _jsx(Button, { variant: "outline", size: "icon", "aria-label": "Close", children: _jsx(X, { className: "size-4" }) }) })] }), children: [_jsx(ModalTitle, { className: "sr-only", children: "Diagram" }), _jsx(ModalBody, { className: "relative min-h-0 flex-1 space-y-0 p-0", children: _jsx("div", { ref: canvasRef, className: "bg-muted/10 absolute inset-0 touch-none overflow-hidden select-none", onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUp, onMouseLeave: handleMouseUp, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, style: { cursor: isDragging ? 'grabbing' : 'grab' }, children: renderedContent ? (_jsx("div", { className: "mermaid-container absolute inset-0 flex items-center justify-center", style: {
262
+ } }), _jsxs("div", { className: cn('mermaid-container group bg-background relative my-4 w-full overflow-auto rounded-lg border', className), children: [_jsx("div", { className: "absolute top-2 right-2 z-10 opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100", children: _jsxs(ButtonGroup, { children: [actions, _jsx(Hint, { label: copied ? 'Copied' : 'Copy source', side: "top", children: _jsx(Button, { variant: "secondary", size: "icon-sm", onClick: handleCopySource, "aria-label": "Copy diagram source", children: copied ? (_jsx(Check, { className: "text-kortix-green size-3.5" })) : (_jsx(Copy, { className: "size-3.5" })) }) }), enableFullscreen && (_jsx(Hint, { label: "Fullscreen", side: "top", children: _jsx(Button, { variant: "secondary", size: "icon-sm", onClick: handleFullscreenOpen, "aria-label": "Open diagram fullscreen", children: _jsx(Maximize2, { className: "size-3.5" }) }) }))] }) }), _jsx("div", { ref: containerRef, dangerouslySetInnerHTML: { __html: renderedContent } })] }), _jsx(Modal, { open: isFullscreenOpen, onOpenChange: setIsFullscreenOpen, children: _jsxs(ModalContent, { variant: "base", showCloseButton: false, className: "flex h-[90vh] flex-col space-y-0 overflow-hidden lg:h-[90vh] lg:max-w-7xl", closeButtonChildren: _jsxs(ButtonGroup, { children: [_jsx(Hint, { label: "Zoom in", side: "bottom", children: _jsx(Button, { variant: "outline", size: "icon", onClick: handleZoomIn, "aria-label": "Zoom in", children: _jsx(ZoomIn, { className: "size-4" }) }) }), _jsx(Hint, { label: "Zoom out", side: "bottom", children: _jsx(Button, { variant: "outline", size: "icon", onClick: handleZoomOut, "aria-label": "Zoom out", children: _jsx(ZoomOut, { className: "size-4" }) }) }), _jsx(Hint, { label: "Rotate", side: "bottom", children: _jsx(Button, { variant: "outline", size: "icon", onClick: handleRotate, "aria-label": "Rotate diagram", children: _jsx(RotateCcw, { className: "size-4" }) }) }), _jsx(ModalClose, { asChild: true, children: _jsx(Button, { variant: "outline", size: "icon", "aria-label": "Close", children: _jsx(X, { className: "size-4" }) }) })] }), children: [_jsx(ModalTitle, { className: "sr-only", children: "Diagram" }), _jsx(ModalBody, { className: "relative min-h-0 flex-1 space-y-0 p-0", children: _jsx("div", { ref: canvasRef, className: "bg-muted/10 absolute inset-0 touch-none overflow-hidden select-none", onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUp, onMouseLeave: handleMouseUp, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, style: { cursor: isDragging ? 'grabbing' : 'grab' }, children: renderedContent ? (_jsx("div", { className: "mermaid-container absolute inset-0 flex items-center justify-center", style: {
263
263
  transform: `translate(${panOffset.x}px, ${panOffset.y}px) scale(${zoom}) rotate(${rotation}deg)`,
264
264
  transformOrigin: 'center center',
265
265
  transition: isDragging ? 'none' : 'transform 0.15s ease-out',
@@ -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
  }
@@ -151,7 +151,7 @@ export function riskTone(risk) {
151
151
  }
152
152
  /** Terminal outcome of a gated action → badge tone. */
153
153
  export function statusTone(status) {
154
- if (status === 'ok')
154
+ if (status === 'ok' || status === 'approved')
155
155
  return 'success';
156
156
  if (status === 'denied' || status === 'error')
157
157
  return 'destructive';
@@ -164,6 +164,8 @@ export function statusLabel(status) {
164
164
  switch (status) {
165
165
  case 'ok':
166
166
  return 'Allowed';
167
+ case 'approved':
168
+ return 'Approved';
167
169
  case 'denied':
168
170
  return 'Denied';
169
171
  case 'error':
@@ -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) {
@@ -27,7 +27,9 @@ export declare function hiddenParticipantCount(participants: SessionParticipants
27
27
  * Hover lists them in the sub-agent card's layout. Read-only: nothing in it
28
28
  * is clickable. Renders nothing unless two or more people can open it.
29
29
  */
30
- export declare function SessionParticipantStack({ participants, }: {
30
+ export declare function SessionParticipantStack({ participants, label: labelOverride, }: {
31
31
  participants: SessionParticipants | undefined;
32
+ /** The stack's accessible name, for a host that is not a session (an artifact's people with access). */
33
+ label?: string;
32
34
  }): import("react").JSX.Element | null;
33
35
  export {};
@@ -47,7 +47,7 @@ export function hiddenParticipantCount(participants) {
47
47
  * Hover lists them in the sub-agent card's layout. Read-only: nothing in it
48
48
  * is clickable. Renders nothing unless two or more people can open it.
49
49
  */
50
- export function SessionParticipantStack({ participants, }) {
50
+ export function SessionParticipantStack({ participants, label: labelOverride, }) {
51
51
  const t = useTranslations('sessionParticipants');
52
52
  const [open, setOpen] = useState(false);
53
53
  if (!participants?.multi_user || participants.total < 2)
@@ -58,7 +58,7 @@ export function SessionParticipantStack({ participants, }) {
58
58
  .map((person) => participantLabel(person))
59
59
  .filter(Boolean)
60
60
  .join(', ');
61
- const label = more > 0 ? t('peopleMore', { names, count: more }) : t('people', { names });
61
+ const label = labelOverride ?? (more > 0 ? t('peopleMore', { names, count: more }) : t('people', { names }));
62
62
  return (_jsxs(HoverCard, { open: open, onOpenChange: setOpen, openDelay: 300, closeDelay: 100, children: [_jsx(HoverCardTrigger, { asChild: true, children: _jsx("span", { role: "img", "aria-label": label, className: "hover:bg-secondary flex h-7 cursor-pointer items-center rounded-md p-0.5", children: _jsxs(AvatarGroup, { className: "-space-x-1.5", children: [shown.map((person) => (_jsx(ParticipantAvatar, { person: person, size: "sm" }, person.user_id))), more > 0 && (
63
63
  // `min-w-6 w-auto`: the tile grows with the count (+97), never under 24px.
64
64
  _jsxs(AvatarGroupCount, { className: "h-6 w-auto min-w-6 px-1.5 text-xs tabular-nums", children: ["+", more] }))] }) }) }), _jsxs(HoverCardContent, { side: "bottom", align: "end", sideOffset: 6, animated: false, className: "w-72 overflow-hidden p-0", onEscapeKeyDown: () => setOpen(false), children: [_jsxs("div", { className: "text-muted-foreground flex items-center justify-between px-3.5 pt-2.5 pb-1 text-xs", children: [_jsx("span", { children: t('title') }), _jsx("span", { className: "tabular-nums", children: participants.total })] }), _jsx(FadedScrollArea, { fadeColor: "from-popover", fadeSize: "6", className: "max-h-64 overscroll-contain", children: _jsx("ul", { className: "p-1 pt-0", children: participants.participants.map((person) => (_jsx("li", { children: _jsxs("div", { className: "text-foreground flex min-h-9 items-center gap-2.5 px-2 py-1 text-sm", children: [_jsx(ParticipantAvatar, { person: person, size: "md" }), _jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [_jsx("span", { className: "truncate leading-tight", children: participantLabel(person) }), person.email ? (_jsx("span", { className: "text-muted-foreground truncate text-xs leading-tight", children: person.email })) : null] })] }) }, person.user_id))) }) }), hiddenParticipantCount(participants) > 0 ? (_jsx("p", { className: "text-muted-foreground border-border border-t px-3.5 py-2 text-xs tabular-nums", children: t('more', { count: hiddenParticipantCount(participants) }) })) : null] })] }));
@@ -75,6 +75,8 @@ export function resolvedLabel(request, outcome) {
75
75
  return 'Approved';
76
76
  if (decision === 'deny')
77
77
  return 'Denied';
78
+ if (request.status === 'approved')
79
+ return 'Approved';
78
80
  if (request.status === 'denied')
79
81
  return 'Denied';
80
82
  if (request.status === 'error')
@@ -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