@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.
- package/dist/_vendor/kortix-shared.js +55 -2
- package/dist/_vendor/shared/artifact-comments.d.ts +235 -0
- package/dist/_vendor/shared/channel-descriptors.d.ts +96 -0
- package/dist/_vendor/shared/channel-message.d.ts +13 -2
- package/dist/_vendor/shared/index.d.ts +1 -0
- package/dist/apps/app-preview-pool.js +19 -2
- package/dist/artifacts/artifact-frame.js +7 -3
- package/dist/customize/sections/component/channel-access-panel.js +4 -4
- package/dist/customize/sections/component/telegram-channel-row.js +7 -5
- package/dist/customize/sections/view/agent-editor-catalog.js +5 -0
- package/dist/customize/sections/view/channels-view.d.ts +2 -1
- package/dist/customize/sections/view/channels-view.js +12 -7
- package/dist/customize/settings/rail.js +9 -0
- package/dist/customize/settings/settings-tabs.d.ts +1 -1
- package/dist/customize/settings/settings-tabs.js +1 -0
- package/dist/customize/settings/tabs/chrome-connections-tab.d.ts +6 -0
- package/dist/customize/settings/tabs/chrome-connections-tab.js +99 -0
- package/dist/customize/shared/catalog/catalog-empty-state.d.ts +3 -3
- package/dist/customize/shared/catalog/catalog-empty-state.js +6 -4
- package/dist/files/renderers/image-frame.d.ts +3 -1
- package/dist/files/renderers/image-frame.js +2 -2
- package/dist/i18n/en.d.ts +189 -0
- package/dist/i18n/en.js +190 -1
- package/dist/internal/apps-and-extensions/apps-modal.d.ts +9 -0
- package/dist/internal/apps-and-extensions/apps-modal.js +107 -0
- package/dist/internal/chrome/browser-icon.d.ts +24 -0
- package/dist/internal/chrome/browser-icon.js +58 -0
- package/dist/internal/chrome/chrome-card-state.d.ts +16 -0
- package/dist/internal/chrome/chrome-card-state.js +9 -0
- package/dist/internal/chrome/chrome-connect.d.ts +69 -0
- package/dist/internal/chrome/chrome-connect.js +178 -0
- package/dist/internal/chrome/chrome-mock.d.ts +4 -0
- package/dist/internal/chrome/chrome-mock.js +15 -0
- package/dist/internal/chrome/chrome-names.d.ts +15 -0
- package/dist/internal/chrome/chrome-names.js +35 -0
- package/dist/internal/chrome/extension.d.ts +47 -0
- package/dist/internal/chrome/extension.js +91 -0
- package/dist/internal/chrome/use-chrome-offered.d.ts +11 -0
- package/dist/internal/chrome/use-chrome-offered.js +22 -0
- package/dist/internal/connector-logo/connector-logo-source.d.ts +2 -1
- package/dist/internal/connector-logo/connector-logo-source.js +7 -6
- package/dist/internal/icons/edge.d.ts +5 -0
- package/dist/internal/icons/edge.js +10 -0
- package/dist/internal/lib/browser-noise/rules/extensions.js +17 -3
- package/dist/internal/lib/detect-os.d.ts +43 -0
- package/dist/internal/lib/detect-os.js +70 -0
- package/dist/internal/lib/use-project-feature-flags.js +5 -1
- package/dist/internal/stores/apps-modal-store.d.ts +8 -0
- package/dist/internal/stores/apps-modal-store.js +6 -0
- package/dist/internal/stores/composer-prefill-store.d.ts +12 -5
- package/dist/markdown/setup-links/approval-decision.js +3 -1
- package/dist/primitives/empty-state.d.ts +2 -2
- package/dist/primitives/empty-state.js +6 -5
- package/dist/primitives/mermaid-renderer.js +1 -1
- package/dist/session/panel/session-audit-shared.js +3 -1
- package/dist/session/participants/session-participants.d.ts +3 -1
- package/dist/session/participants/session-participants.js +2 -2
- package/dist/session/prompts/approval-request.js +2 -0
- package/dist/session/thread/session-thread.d.ts +13 -1
- package/dist/session/thread/session-thread.js +15 -5
- package/dist/session/thread/session-transcript-class.d.ts +5 -1
- package/dist/session/thread/session-transcript-class.js +5 -1
- package/dist/session/tool/tools/image-gen-tool.js +4 -3
- package/dist/session/turn/user-message.d.ts +7 -5
- package/dist/session/turn/user-message.js +14 -8
- package/dist/styles.css +1 -1
- package/dist/workspace/sessions/session-label-badges.js +2 -1
- package/dist/workspace/sidebar/footer/project-chrome-nav.d.ts +10 -0
- package/dist/workspace/sidebar/footer/project-chrome-nav.js +56 -0
- package/dist/workspace/sidebar/project-sidebar.js +5 -1
- package/package.json +43 -3
- 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:
|
|
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 {};
|
|
@@ -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
|
|
11
|
-
*
|
|
12
|
-
* bar (`components/desktop/desktop-quick-entry.tsx`)
|
|
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
|
-
/**
|
|
22
|
-
|
|
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:
|
|
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 `
|
|
5
|
+
/** Custom artwork in place of `EmptyArt`. */
|
|
6
6
|
media?: ReactNode;
|
|
7
|
-
title:
|
|
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.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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: [
|
|
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,
|
|
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
|
-
|
|
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 (
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
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 });
|