@kortix/ui 0.14.9 → 0.14.10

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 (72) 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/apps/app-preview-pool.js +19 -2
  7. package/dist/artifacts/artifact-frame.js +7 -3
  8. package/dist/customize/sections/component/channel-access-panel.js +4 -4
  9. package/dist/customize/sections/component/telegram-channel-row.js +7 -5
  10. package/dist/customize/sections/view/agent-editor-catalog.js +5 -0
  11. package/dist/customize/sections/view/channels-view.d.ts +2 -1
  12. package/dist/customize/sections/view/channels-view.js +12 -7
  13. package/dist/customize/settings/rail.js +9 -0
  14. package/dist/customize/settings/settings-tabs.d.ts +1 -1
  15. package/dist/customize/settings/settings-tabs.js +1 -0
  16. package/dist/customize/settings/tabs/chrome-connections-tab.d.ts +6 -0
  17. package/dist/customize/settings/tabs/chrome-connections-tab.js +99 -0
  18. package/dist/customize/shared/catalog/catalog-empty-state.d.ts +3 -3
  19. package/dist/customize/shared/catalog/catalog-empty-state.js +6 -4
  20. package/dist/files/renderers/image-frame.d.ts +3 -1
  21. package/dist/files/renderers/image-frame.js +2 -2
  22. package/dist/i18n/en.d.ts +189 -0
  23. package/dist/i18n/en.js +190 -1
  24. package/dist/internal/apps-and-extensions/apps-modal.d.ts +9 -0
  25. package/dist/internal/apps-and-extensions/apps-modal.js +107 -0
  26. package/dist/internal/chrome/browser-icon.d.ts +24 -0
  27. package/dist/internal/chrome/browser-icon.js +58 -0
  28. package/dist/internal/chrome/chrome-card-state.d.ts +16 -0
  29. package/dist/internal/chrome/chrome-card-state.js +9 -0
  30. package/dist/internal/chrome/chrome-connect.d.ts +69 -0
  31. package/dist/internal/chrome/chrome-connect.js +178 -0
  32. package/dist/internal/chrome/chrome-mock.d.ts +4 -0
  33. package/dist/internal/chrome/chrome-mock.js +15 -0
  34. package/dist/internal/chrome/chrome-names.d.ts +15 -0
  35. package/dist/internal/chrome/chrome-names.js +35 -0
  36. package/dist/internal/chrome/extension.d.ts +47 -0
  37. package/dist/internal/chrome/extension.js +91 -0
  38. package/dist/internal/chrome/use-chrome-offered.d.ts +11 -0
  39. package/dist/internal/chrome/use-chrome-offered.js +22 -0
  40. package/dist/internal/connector-logo/connector-logo-source.d.ts +2 -1
  41. package/dist/internal/connector-logo/connector-logo-source.js +7 -6
  42. package/dist/internal/icons/edge.d.ts +5 -0
  43. package/dist/internal/icons/edge.js +10 -0
  44. package/dist/internal/lib/browser-noise/rules/extensions.js +17 -3
  45. package/dist/internal/lib/detect-os.d.ts +43 -0
  46. package/dist/internal/lib/detect-os.js +70 -0
  47. package/dist/internal/lib/use-project-feature-flags.js +5 -1
  48. package/dist/internal/stores/apps-modal-store.d.ts +8 -0
  49. package/dist/internal/stores/apps-modal-store.js +6 -0
  50. package/dist/internal/stores/composer-prefill-store.d.ts +12 -5
  51. package/dist/markdown/setup-links/approval-decision.js +3 -1
  52. package/dist/primitives/empty-state.d.ts +2 -2
  53. package/dist/primitives/empty-state.js +6 -5
  54. package/dist/primitives/mermaid-renderer.js +1 -1
  55. package/dist/session/panel/session-audit-shared.js +3 -1
  56. package/dist/session/participants/session-participants.d.ts +3 -1
  57. package/dist/session/participants/session-participants.js +2 -2
  58. package/dist/session/prompts/approval-request.js +2 -0
  59. package/dist/session/thread/session-thread.d.ts +13 -1
  60. package/dist/session/thread/session-thread.js +15 -5
  61. package/dist/session/thread/session-transcript-class.d.ts +5 -1
  62. package/dist/session/thread/session-transcript-class.js +5 -1
  63. package/dist/session/tool/tools/image-gen-tool.js +4 -3
  64. package/dist/session/turn/user-message.d.ts +7 -5
  65. package/dist/session/turn/user-message.js +14 -8
  66. package/dist/styles.css +1 -1
  67. package/dist/workspace/sessions/session-label-badges.js +2 -1
  68. package/dist/workspace/sidebar/footer/project-chrome-nav.d.ts +10 -0
  69. package/dist/workspace/sidebar/footer/project-chrome-nav.js +56 -0
  70. package/dist/workspace/sidebar/project-sidebar.js +5 -1
  71. package/package.json +43 -3
  72. package/skills/kortix-ui/references/modules.md +1 -1
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Platform detection for the /download page and the per-platform redirect
3
+ * routes.
4
+ *
5
+ * Detection runs on the SERVER, from the request's user-agent header, so the
6
+ * page paints the correct filled button (and, on a phone, the mobile card
7
+ * first) on first render. A client-side `useEffect` swap would let a visitor click the wrong
8
+ * installer during the flash — the exact failure this page exists to prevent.
9
+ *
10
+ * Architecture is deliberately NOT detected: macOS ships a single universal
11
+ * .dmg, and macOS user agents misreport arch anyway (they always claim Intel).
12
+ */
13
+ /** Fixed row order. Detection never reorders rows; it only picks the filled button. */
14
+ export const DESKTOP_ORDER = ['macos', 'windows', 'linux'];
15
+ export const MOBILE_ORDER = ['ios', 'android'];
16
+ const PLATFORM_ALIASES = {
17
+ mac: 'macos',
18
+ macos: 'macos',
19
+ osx: 'macos',
20
+ darwin: 'macos',
21
+ apple: 'macos',
22
+ win: 'windows',
23
+ windows: 'windows',
24
+ linux: 'linux',
25
+ ios: 'ios',
26
+ iphone: 'ios',
27
+ ipad: 'ios',
28
+ android: 'android',
29
+ };
30
+ /** Map an explicit `?platform=` value or `/download/<segment>` to a platform. */
31
+ export function normalizePlatform(raw) {
32
+ return PLATFORM_ALIASES[(raw || '').toLowerCase()] ?? null;
33
+ }
34
+ /**
35
+ * Best-effort platform from a user-agent string.
36
+ *
37
+ * THE ORDER OF THESE CHECKS IS LOAD-BEARING. Two user agents carry another
38
+ * platform's token:
39
+ * iPad "Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X)" contains "mac"
40
+ * Android "Mozilla/5.0 (Linux; Android 14; Pixel 8)" contains "linux"
41
+ * So iOS is tested before macOS, and Android before Linux. `detect-os.test.ts`
42
+ * pins both; if you reorder these lines, those tests fail.
43
+ *
44
+ * KNOWN LIMITATION: an iPad with "Request Desktop Website" enabled sends a UA
45
+ * byte-identical to a Mac's. No server can separate them — real detection needs
46
+ * `navigator.maxTouchPoints`, which is not in any request header. That visitor
47
+ * gets the Desktop card first; the Mobile card is still on the page, one tap
48
+ * away. Fixing it client-side would reintroduce the wrong-file-click window
49
+ * this module exists to close.
50
+ *
51
+ * Defaults to macOS: the largest desktop segment for this product, and every
52
+ * other platform stays one click away on the page.
53
+ */
54
+ export function detectPlatform(userAgent) {
55
+ const ua = (userAgent || '').toLowerCase();
56
+ if (ua.includes('iphone') || ua.includes('ipad') || ua.includes('ipod'))
57
+ return 'ios';
58
+ if (ua.includes('android'))
59
+ return 'android';
60
+ if (ua.includes('windows'))
61
+ return 'windows';
62
+ if (ua.includes('mac') || ua.includes('darwin'))
63
+ return 'macos';
64
+ if (ua.includes('linux'))
65
+ return 'linux';
66
+ return 'macos';
67
+ }
68
+ export function isMobilePlatform(platform) {
69
+ return platform === 'ios' || platform === 'android';
70
+ }
@@ -39,9 +39,11 @@ export function useProjectFeatureFlags(projectId) {
39
39
  const agentRooms = useFeatureFlag(projectId, 'agent_rooms');
40
40
  const goals = useFeatureFlag(projectId, 'goals');
41
41
  const artifacts = useFeatureFlag(projectId, 'artifacts');
42
+ const artifactComments = useFeatureFlag(projectId, 'artifact_comments');
42
43
  const emailChannel = useFeatureFlag(projectId, 'email_channel');
43
44
  const workspaceVolume = useFeatureFlag(projectId, 'workspace_volume');
44
45
  const sessionGroups = useFeatureFlag(projectId, 'session_groups');
46
+ const chrome = useFeatureFlag(projectId, 'chrome');
45
47
  return {
46
48
  flags: {
47
49
  marketplace: marketplace.enabled,
@@ -62,11 +64,13 @@ export function useProjectFeatureFlags(projectId) {
62
64
  agent_rooms: agentRooms.enabled,
63
65
  goals: goals.enabled,
64
66
  artifacts: artifacts.enabled,
67
+ artifact_comments: artifactComments.enabled,
65
68
  email_channel: emailChannel.enabled,
66
69
  workspace_volume: workspaceVolume.enabled,
67
70
  session_groups: sessionGroups.enabled,
71
+ chrome: chrome.enabled,
68
72
  },
69
73
  // The trailing hook's loading state — keep this on the LAST hook above.
70
- isLoading: sessionGroups.isLoading,
74
+ isLoading: chrome.isLoading,
71
75
  };
72
76
  }
@@ -0,0 +1,8 @@
1
+ /** @internal Kortix apps only. No semver: it may change in any release. */
2
+ /** "Apps and extensions" dialog: any trigger (account menu, sidebar row, `?apps=1`) opens the one mounted in the project sidebar. */
3
+ interface AppsModalState {
4
+ open: boolean;
5
+ setOpen: (open: boolean) => void;
6
+ }
7
+ export declare const useAppsModalStore: import("zustand").UseBoundStore<import("zustand").StoreApi<AppsModalState>>;
8
+ export {};
@@ -0,0 +1,6 @@
1
+ 'use client';
2
+ import { create } from 'zustand';
3
+ export const useAppsModalStore = create((set) => ({
4
+ open: false,
5
+ setOpen: (open) => set({ open }),
6
+ }));
@@ -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). */
@@ -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',
@@ -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':
@@ -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')
@@ -11,6 +11,9 @@
11
11
  * Alone, it has no side panel: its "open in the panel" actions (Preview, file
12
12
  * chips, Terminal, Browser) open an overlay (`SessionPanelFallback`). Inside
13
13
  * `SessionWorkspace` they open in the side panel.
14
+ *
15
+ * A host that just created the session passes `firstPrompt`: the prompt shows
16
+ * at once, as the turn the transcript will draw, while the computer boots.
14
17
  */
15
18
  import type { UseSessionResult } from '@kortix/sdk/react';
16
19
  import '../tool/tools/register.js';
@@ -32,6 +35,15 @@ export interface SessionThreadProps {
32
35
  id: number;
33
36
  text: string;
34
37
  } | null;
38
+ /**
39
+ * The first prompt of a session this host just created, as sent. Drawn at
40
+ * once (the prompt and the agent's Thinking row) while the computer boots,
41
+ * then handed to the transcript without a jump. Pass it only for that
42
+ * fresh session: the transcript draws its prompts from then on.
43
+ */
44
+ firstPrompt?: string;
45
+ /** `compact`: tighter gutters for a narrow column (a side sheet). Default: the session page's. */
46
+ density?: 'default' | 'compact';
35
47
  className?: string;
36
48
  }
37
- export declare function SessionThread({ session, hideHeader, readOnly, composerPlaceholder, quote, className, }: SessionThreadProps): import("react").JSX.Element;
49
+ export declare function SessionThread({ session, hideHeader, readOnly, composerPlaceholder, quote, firstPrompt, density, className, }: SessionThreadProps): import("react").JSX.Element;
@@ -1,20 +1,30 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useContext } from 'react';
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useContext, useEffect } from 'react';
4
4
  import { cn } from '../../lib/utils.js';
5
5
  import { ProjectScope } from '../../internal/project-scope.js';
6
+ import { useFirstPromptPreviewStore } from '../composer/stores/session-composer-handoff-store.js';
6
7
  import { ProviderPoolDraftBoundary } from '../overrides/provider-pool-draft-context.js';
7
8
  import { SessionPanelFallback } from '../panel/panel-fallback.js';
8
9
  import { SessionChat } from './session-chat.js';
9
10
  import { SessionStartingLoader } from '../status/session-starting-loader.js';
11
+ import { OptimisticTurn } from './optimistic-turn.js';
12
+ import { SESSION_TRANSCRIPT_CLASS } from './session-transcript-class.js';
10
13
  import { ThreadBootRows } from './thread-boot-rows.js';
11
14
  import { TranscriptRowsContext } from './transcript-rows.js';
12
15
  import '../tool/tools/register.js';
13
- export function SessionThread({ session, hideHeader = false, readOnly = false, composerPlaceholder, quote, className, }) {
16
+ export function SessionThread({ session, hideHeader = false, readOnly = false, composerPlaceholder, quote, firstPrompt, density = 'default', className, }) {
14
17
  const { projectId, sessionId, runtimeSessionId } = session;
15
18
  const rows = useContext(TranscriptRowsContext) !== null;
16
- return (_jsx(ProjectScope, { projectId: projectId, sessionId: sessionId, children: _jsx(ProviderPoolDraftBoundary, { identity: `${projectId}/${sessionId}`, children: _jsxs("div", { "data-slot": "session-thread", className: cn('relative flex h-full min-h-0 flex-col', className), children: [runtimeSessionId ? (_jsx(SessionChat, { sessionId: runtimeSessionId, projectSessionId: sessionId, projectId: projectId, sessionState: session, hideHeader: hideHeader, readOnly: readOnly, composerPlaceholder: composerPlaceholder, hostQuote: quote }, runtimeSessionId)) : rows ? (
19
+ // The same stand-in the instant session shell hands `SessionChat`: it draws
20
+ // the prompt until its transcript has it, then forgets it.
21
+ const bootPrompt = useFirstPromptPreviewStore((state) => state.previewBySession[sessionId]?.text ?? null) ?? (firstPrompt?.trim() || null);
22
+ useEffect(() => {
23
+ if (firstPrompt?.trim())
24
+ useFirstPromptPreviewStore.getState().setFirstPromptPreview(sessionId, firstPrompt, []);
25
+ }, [sessionId, firstPrompt]);
26
+ return (_jsx(ProjectScope, { projectId: projectId, sessionId: sessionId, children: _jsx(ProviderPoolDraftBoundary, { identity: `${projectId}/${sessionId}`, children: _jsxs("div", { "data-slot": "session-thread", "data-density": density, className: cn('group/thread relative flex h-full min-h-0 flex-col', className), children: [runtimeSessionId ? (_jsx(SessionChat, { sessionId: runtimeSessionId, projectSessionId: sessionId, projectId: projectId, sessionState: session, hideHeader: hideHeader, readOnly: readOnly, composerPlaceholder: composerPlaceholder, hostQuote: quote }, runtimeSessionId)) : rows ? (
17
27
  // A channel thread: its prompt, the agent's busy row and the reply
18
28
  // box from the first frame, while the computer comes up.
19
- _jsx(ThreadBootRows, { projectId: projectId, sessionId: sessionId, composerPlaceholder: composerPlaceholder, readOnly: readOnly })) : (_jsx(SessionStartingLoader, { projectId: projectId, sessionId: sessionId })), _jsx(SessionPanelFallback, { projectId: projectId, projectSessionId: sessionId, runtimeSessionId: runtimeSessionId })] }) }) }));
29
+ _jsx(ThreadBootRows, { projectId: projectId, sessionId: sessionId, composerPlaceholder: composerPlaceholder, readOnly: readOnly })) : bootPrompt ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "scrollbar-hide min-h-0 flex-1 overflow-y-auto", children: _jsx("div", { className: SESSION_TRANSCRIPT_CLASS, children: _jsx(OptimisticTurn, { text: bootPrompt, deferPreview: true, sessionId: sessionId }) }) }), _jsx("div", { className: "shrink-0", children: _jsx(SessionStartingLoader, { projectId: projectId, sessionId: sessionId }) })] })) : (_jsx(SessionStartingLoader, { projectId: projectId, sessionId: sessionId })), _jsx(SessionPanelFallback, { projectId: projectId, projectSessionId: sessionId, runtimeSessionId: runtimeSessionId })] }) }) }));
20
30
  }
@@ -15,5 +15,9 @@
15
15
  * Its own module with no imports, so a surface that only needs the column (the
16
16
  * first chat on project home) does not pull `session-body.tsx`'s action panel
17
17
  * into its bundle. `session-body.tsx` re-exports it for the session surfaces.
18
+ *
19
+ * Inside a compact `SessionThread` (a narrow side column) the conversation's
20
+ * gutters and top inset tighten; the composer keeps its own. The gutters then
21
+ * match the composer's (`px-4`), so messages line up with the input card's edges.
18
22
  */
19
- export declare const SESSION_TRANSCRIPT_CLASS = "mx-auto w-full max-w-3xl min-w-0 px-7 pt-6";
23
+ export declare const SESSION_TRANSCRIPT_CLASS = "mx-auto w-full max-w-3xl min-w-0 px-7 pt-6 group-data-[density=compact]/thread:px-4 group-data-[density=compact]/thread:pt-4";
@@ -15,5 +15,9 @@
15
15
  * Its own module with no imports, so a surface that only needs the column (the
16
16
  * first chat on project home) does not pull `session-body.tsx`'s action panel
17
17
  * into its bundle. `session-body.tsx` re-exports it for the session surfaces.
18
+ *
19
+ * Inside a compact `SessionThread` (a narrow side column) the conversation's
20
+ * gutters and top inset tighten; the composer keeps its own. The gutters then
21
+ * match the composer's (`px-4`), so messages line up with the input card's edges.
18
22
  */
19
- export const SESSION_TRANSCRIPT_CLASS = 'mx-auto w-full max-w-3xl min-w-0 px-7 pt-6';
23
+ export const SESSION_TRANSCRIPT_CLASS = 'mx-auto w-full max-w-3xl min-w-0 px-7 pt-6 group-data-[density=compact]/thread:px-4 group-data-[density=compact]/thread:pt-4';
@@ -125,9 +125,10 @@ function ImageGenFrame({ image, seed, busy, model, fill, }) {
125
125
  useKortixComputerStore.getState().setIsExpanded(true);
126
126
  }
127
127
  : undefined;
128
- return (_jsxs("div", { className: cn('group relative', fill && 'h-full'), children: [_jsx(ImageFrame, { aspectRatio: image.aspectRatio, fill: fill, maxHeight: fill ? undefined : IMAGE_MAX_HEIGHT, openLabel: image.prompt ? t('openFullScreenPrompt', { prompt: image.prompt }) : t('openFullScreen'), src: src, alt: image.prompt || t('generating'), pending: generating || (!src && !blobError), label: generating ? (busy) : blobError ? (_jsx(ImageUnavailable, { onRetry: retry })) : !src ? (_jsx(BusyLabel, { seed: seed, label: t('loading') })) : null, onOpen: openFullScreen, naturalRatio: naturalRatio, onNaturalSize: onNaturalSize }), src && openFullScreen ? (
129
- // The one hint that the image opens full screen, on hover only; the whole frame is the button.
130
- _jsx("span", { "data-image-gen-expand": "", "aria-hidden": "true", className: "bg-popover text-foreground pointer-events-none absolute top-1.5 right-1.5 flex size-6 items-center justify-center rounded-md opacity-0 shadow-xs transition-opacity duration-fast group-focus-within:opacity-100 group-hover:opacity-100", children: _jsx(ArrowsOutSimpleIcon, { className: "size-3.5" }) })) : null] }));
128
+ return (_jsx(ImageFrame, { className: "group", aspectRatio: image.aspectRatio, fill: fill, maxHeight: fill ? undefined : IMAGE_MAX_HEIGHT, openLabel: image.prompt ? t('openFullScreenPrompt', { prompt: image.prompt }) : t('openFullScreen'), src: src, alt: image.prompt || t('generating'), pending: generating || (!src && !blobError), label: generating ? (busy) : blobError ? (_jsx(ImageUnavailable, { onRetry: retry })) : !src ? (_jsx(BusyLabel, { seed: seed, label: t('loading') })) : null, onOpen: openFullScreen, naturalRatio: naturalRatio, onNaturalSize: onNaturalSize, children: src && openFullScreen ? (
129
+ // The one hint that the image opens full screen, on hover only; the whole frame is the button.
130
+ // A child of the frame: a tall image narrows the frame, and the hint stays on its corner.
131
+ _jsx("span", { "data-image-gen-expand": "", "aria-hidden": "true", className: "bg-popover text-foreground pointer-events-none absolute top-1.5 right-1.5 flex size-6 items-center justify-center rounded-md opacity-0 shadow-xs transition-opacity duration-fast group-focus-within:opacity-100 group-hover:opacity-100", children: _jsx(ArrowsOutSimpleIcon, { className: "size-3.5" }) })) : null }));
131
132
  }
132
133
  /**
133
134
  * The tallest a lone image or the bento may be (R33). The box narrows to
@@ -29,13 +29,15 @@ export declare function ChannelOrigin({ info, platform }: {
29
29
  }): import("react").JSX.Element;
30
30
  /**
31
31
  * A message that arrived from a chat channel (Slack / Microsoft Teams /
32
- * Telegram): a source pill — mark, platform, sender — over the same bubble a
33
- * typed message gets. Every `@name` in the text (`@Kortix`, `@KortixDev`,
34
- * `@here`) is a `MentionChip`, the chip the composer draws, static because a
35
- * channel mention opens nothing here. Exported for `channel-message-card.test.tsx`.
32
+ * Telegram): a source pill — mark, platform, sender — over the files the
33
+ * sender sent (the strip a composer upload gets) and the same bubble a typed
34
+ * message gets. Every `@name` in the text (`@Kortix`, `@KortixDev`, `@here`)
35
+ * is a `MentionChip`, the chip the composer draws, static because a channel
36
+ * mention opens nothing here. Exported for `channel-message-card.test.tsx`.
36
37
  */
37
- export declare function ChannelMessage({ info, actions, }: {
38
+ export declare function ChannelMessage({ info, attachments, actions, }: {
38
39
  info: ChannelMessageInfo;
40
+ attachments?: NormalizedAttachment[];
39
41
  actions?: React.ReactNode;
40
42
  }): import("react").JSX.Element;
41
43
  export interface NormalizedAttachment {
@@ -13,7 +13,7 @@ import { fetchSessionAttachment, isSessionAttachmentRef, } from '@kortix/sdk';
13
13
  /** Moved from session-chat.tsx (`UserMessageRow`) so the turn module owns the
14
14
  * user-message card. Full-width card, no reference chips. */
15
15
  import { useTranslations } from '../../i18n/index.js';
16
- import { expandPastedContent, neutralizePastedTags, sanitizePromptUploadFilename, serializePromptWithPastes, splitPastedContent, } from '../../_vendor/kortix-shared.js';
16
+ import { channelPromptTail, isChannelFileLabel, expandPastedContent, neutralizePastedTags, sanitizePromptUploadFilename, serializePromptWithPastes, splitPastedContent, } from '../../_vendor/kortix-shared.js';
17
17
  import { useContext, useEffect, useMemo, useRef, useState } from 'react';
18
18
  import { PencilSimpleIcon, AlarmIcon, ChatsTeardropIcon, LightningIcon, } from '@phosphor-icons/react';
19
19
  import { CopyButton } from '../../markdown/copy-button.js';
@@ -99,15 +99,16 @@ export function ChannelOrigin({ info, platform }) {
99
99
  }
100
100
  /**
101
101
  * A message that arrived from a chat channel (Slack / Microsoft Teams /
102
- * Telegram): a source pill — mark, platform, sender — over the same bubble a
103
- * typed message gets. Every `@name` in the text (`@Kortix`, `@KortixDev`,
104
- * `@here`) is a `MentionChip`, the chip the composer draws, static because a
105
- * channel mention opens nothing here. Exported for `channel-message-card.test.tsx`.
102
+ * Telegram): a source pill — mark, platform, sender — over the files the
103
+ * sender sent (the strip a composer upload gets) and the same bubble a typed
104
+ * message gets. Every `@name` in the text (`@Kortix`, `@KortixDev`, `@here`)
105
+ * is a `MentionChip`, the chip the composer draws, static because a channel
106
+ * mention opens nothing here. Exported for `channel-message-card.test.tsx`.
106
107
  */
107
- export function ChannelMessage({ info, actions, }) {
108
+ export function ChannelMessage({ info, attachments = [], actions, }) {
108
109
  const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
109
110
  const platform = channelPlatformLabel(info.platform, tI18nComplete);
110
- return (_jsxs("div", { className: "flex flex-col items-end gap-1.5", children: [_jsx(SourcePill, { mark: _jsx(ChannelBrandMark, { platform: info.platform, className: "size-3 shrink-0" }), source: platform, sourceColor: CHANNEL_BRAND_COLOR[info.platform], sender: info.userName, card: _jsx(ChannelOrigin, { info: info, platform: platform }) }), info.messageText && (_jsx("div", { className: cn(BUBBLE_SURFACE, 'max-w-[80%]'), children: _jsx("div", { className: BUBBLE_TEXT, children: buildMentionSegments({ text: info.messageText }).map((segment, i) => segment.type ? (_jsx(MentionChip, { kind: "user", label: segment.text.slice(1) }, i)) : (_jsx("span", { children: segment.text }, i))) }) })), actions] }));
111
+ return (_jsxs("div", { className: "flex flex-col items-end gap-1.5", children: [_jsx(SourcePill, { mark: _jsx(ChannelBrandMark, { platform: info.platform, className: "size-3 shrink-0" }), source: platform, sourceColor: CHANNEL_BRAND_COLOR[info.platform], sender: info.userName, card: _jsx(ChannelOrigin, { info: info, platform: platform }) }), _jsx(MessageAttachments, { attachments: attachments }), info.messageText && !(attachments.length > 0 && isChannelFileLabel(info.messageText)) && (_jsx("div", { className: cn(BUBBLE_SURFACE, 'max-w-[80%]'), children: _jsx("div", { className: BUBBLE_TEXT, children: buildMentionSegments({ text: info.messageText }).map((segment, i) => segment.type ? (_jsx(MentionChip, { kind: "user", label: segment.text.slice(1) }, i)) : (_jsx("span", { children: segment.text }, i))) }) })), actions] }));
111
112
  }
112
113
  /**
113
114
  * A prompt the platform wrote — a reminder fire or a trigger fire — drawn the
@@ -900,6 +901,11 @@ export function UserMessage({ message, author, showAuthor, agentNames, commandIn
900
901
  // scaffolds these prompts with ids and turn instructions the person never
901
902
  // typed, so the card shows only the platform, the sender, and their words.
902
903
  const channelMessageInfo = useMemo(() => parseChannelMessage(rawText), [rawText]);
904
+ // Its files: the `<file>` tags Kortix wrote after the sender's text, and nothing else. A tag the sender
905
+ // typed is text; the image block pi records for a tag repeats it, and names no path when the sender typed one.
906
+ const channelAttachments = useMemo(() => channelMessageInfo
907
+ ? normalizeAttachments([], parseFileReferences(channelPromptTail(rawText, channelMessageInfo.platform)).files)
908
+ : [], [channelMessageInfo, rawText]);
903
909
  // Detect trigger_event in user message
904
910
  const triggerEventInfo = useMemo(() => parseTriggerEvent(rawText), [rawText]);
905
911
  // A reminder fire: platform-written `[REMINDER …]` header + the reminder text.
@@ -1061,7 +1067,7 @@ export function UserMessage({ message, author, showAuthor, agentNames, commandIn
1061
1067
  }
1062
1068
  // Channel messages (Slack / Microsoft Teams / Telegram): a branded card with the sender
1063
1069
  if (channelMessageInfo) {
1064
- return _jsx(ChannelMessage, { info: channelMessageInfo, actions: actions });
1070
+ return _jsx(ChannelMessage, { info: channelMessageInfo, attachments: channelAttachments, actions: actions });
1065
1071
  }
1066
1072
  if (reminderInfo) {
1067
1073
  return _jsx(ReminderMessage, { info: reminderInfo, actions: actions });