@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,178 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { platformConfig } from '@kortix/sdk';
|
|
4
|
+
import { useChromeConnections } from '@kortix/sdk/react';
|
|
5
|
+
import { AppWindowIcon, ArrowUpRightIcon, LinkSimpleIcon, PuzzlePieceIcon, SignInIcon, } from '@phosphor-icons/react';
|
|
6
|
+
import { useMutation, useQuery } from '@tanstack/react-query';
|
|
7
|
+
import { useState, useSyncExternalStore } from 'react';
|
|
8
|
+
import { Button } from '../../primitives/button.js';
|
|
9
|
+
import Loading from '../../primitives/loading.js';
|
|
10
|
+
import { Modal, ModalContent, ModalDescription, ModalTitle } from '../../primitives/modal.js';
|
|
11
|
+
import { BeamsShader } from '../../primitives/paper-wallpaper-shaders.js';
|
|
12
|
+
import { errorToast } from '../../primitives/toast.js';
|
|
13
|
+
import { SolidCheckIcon } from '../icons/solid-check-icon.js';
|
|
14
|
+
import { useCopy } from '../hooks/use-copy.js';
|
|
15
|
+
import { useTranslations } from '../../i18n/index.js';
|
|
16
|
+
import { isDesktop } from '../lib/desktop.js';
|
|
17
|
+
import { cn } from '../lib/utils.js';
|
|
18
|
+
import { BrowserIcon, BrowserLogoTile } from './browser-icon.js';
|
|
19
|
+
import { apiOrigin, CHROME_INSTALL_DOCS_PATH, CHROME_STORE_LISTED, chromeSettingsLink, connectExtension, isChromiumBrowser, KORTIX_EXTENSION_STORE_URL, pingExtension, } from './extension.js';
|
|
20
|
+
const subscribeNever = () => () => { };
|
|
21
|
+
function detectEnvironment() {
|
|
22
|
+
return isChromiumBrowser(navigator.userAgent) && !isDesktop() ? 'supported' : 'unsupported';
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* `fresh`: the person adds ANOTHER browser. This browser's own extension state
|
|
26
|
+
* does not count: no ping, and only a browser that was not in the list when the
|
|
27
|
+
* dialog opened completes it.
|
|
28
|
+
*/
|
|
29
|
+
export function useChromeConnect({ fresh = false } = {}) {
|
|
30
|
+
const t = useTranslations('chrome');
|
|
31
|
+
const environment = useSyncExternalStore(subscribeNever, detectEnvironment, () => 'checking');
|
|
32
|
+
const ping = useQuery({
|
|
33
|
+
queryKey: ['chrome-extension-ping'],
|
|
34
|
+
queryFn: () => pingExtension(),
|
|
35
|
+
enabled: environment === 'supported' && !fresh,
|
|
36
|
+
refetchInterval: 2_000,
|
|
37
|
+
staleTime: 0,
|
|
38
|
+
gcTime: 0,
|
|
39
|
+
});
|
|
40
|
+
const connections = useChromeConnections({ refetchInterval: 3_000 });
|
|
41
|
+
// The browsers that existed when the steps appeared. A browser outside it is
|
|
42
|
+
// the one this person is connecting now.
|
|
43
|
+
const [baseline, setBaseline] = useState(null);
|
|
44
|
+
if (baseline === null && connections.isSuccess) {
|
|
45
|
+
setBaseline(new Set(connections.data.map((chrome) => chrome.id)));
|
|
46
|
+
}
|
|
47
|
+
const status = fresh ? null : (ping.data ?? null);
|
|
48
|
+
const connected = connections.data?.find((chrome) => baseline && !baseline.has(chrome.id)) ??
|
|
49
|
+
// The extension keeps its browser id after a sign-out: trust it only while signed in.
|
|
50
|
+
(status?.connected && status.chromeId
|
|
51
|
+
? connections.data?.find((chrome) => chrome.id === status.chromeId && chrome.online)
|
|
52
|
+
: undefined);
|
|
53
|
+
const connect = useMutation({
|
|
54
|
+
// A retry would open the sign-in window again.
|
|
55
|
+
retry: false,
|
|
56
|
+
mutationFn: () => connectExtension(apiOrigin(platformConfig().backendUrl || '', window.location.origin)),
|
|
57
|
+
onSuccess: (result) => {
|
|
58
|
+
if (!result.ok) {
|
|
59
|
+
// `unavailable`: the page could not reach the extension.
|
|
60
|
+
errorToast(t(result.error === 'unavailable' ? 'extensionMissing' : 'connectFailed'));
|
|
61
|
+
}
|
|
62
|
+
void ping.refetch();
|
|
63
|
+
void connections.refetch();
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
environment,
|
|
68
|
+
/** The first ping has not answered yet. */
|
|
69
|
+
checking: environment === 'checking' || (environment === 'supported' && !fresh && !ping.isFetched),
|
|
70
|
+
// A page opened before the install cannot reach the extension until it
|
|
71
|
+
// reloads. The extension's own tab finishes the sign-in, and the browser
|
|
72
|
+
// then shows up in the list, so a connected browser completes every step.
|
|
73
|
+
installed: status !== null || Boolean(connected),
|
|
74
|
+
signedIn: status?.connected === true || Boolean(connected),
|
|
75
|
+
connected,
|
|
76
|
+
connect,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** The Chrome mark on a small white tile, for a button label. */
|
|
80
|
+
export function ChromeButtonMark() {
|
|
81
|
+
return _jsx(BrowserLogoTile, { brand: "chrome", className: "size-5 shrink-0 ring-0", iconClassName: "size-3.5" });
|
|
82
|
+
}
|
|
83
|
+
function StepTile({ done, icon: StepIcon, brand }) {
|
|
84
|
+
// The last step shows the connected browser's own mark.
|
|
85
|
+
if (done && brand)
|
|
86
|
+
return _jsx(BrowserLogoTile, { brand: brand, className: "size-10" });
|
|
87
|
+
return (_jsx("span", { className: cn('flex size-10 shrink-0 items-center justify-center rounded-sm', done ? 'bg-kortix-green/15' : 'bg-muted text-muted-foreground'), children: done ? _jsx(SolidCheckIcon, { className: "text-kortix-green size-5" }) : _jsx(StepIcon, { className: "size-5" }) }));
|
|
88
|
+
}
|
|
89
|
+
export function ChromeOnlineDot({ online, className }) {
|
|
90
|
+
const t = useTranslations('chrome');
|
|
91
|
+
return (_jsx("span", { role: "img", "aria-label": online ? t('online') : t('offline'), className: cn('inline-block size-2 shrink-0 rounded-full', online ? 'bg-kortix-green' : 'bg-muted-foreground', className) }));
|
|
92
|
+
}
|
|
93
|
+
/** Copies the link that opens Settings → Kortix in Chrome, for a browser that cannot run the extension. */
|
|
94
|
+
export function ChromeCopyLink({ size = 'sm', className }) {
|
|
95
|
+
const t = useTranslations('chrome');
|
|
96
|
+
const { copy, copied } = useCopy({ toast: false });
|
|
97
|
+
return (_jsxs(Button, { variant: "secondary", size: size, className: cn('gap-1.5', className), onClick: async () => {
|
|
98
|
+
if (!(await copy(chromeSettingsLink(window.location.origin))))
|
|
99
|
+
errorToast(t('copyFailed'));
|
|
100
|
+
}, children: [copied ? _jsx(SolidCheckIcon, { className: "size-3.5 shrink-0" }) : _jsx(LinkSimpleIcon, { className: "size-3.5 shrink-0" }), copied ? t('linkCopied') : t('copyLink')] }));
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The action that moves this browser forward: Connect once the extension is
|
|
104
|
+
* there, otherwise Connect plus "How to install" (or "Add to Chrome" once the
|
|
105
|
+
* Web Store listing is public). Renders nothing for a connected browser.
|
|
106
|
+
*/
|
|
107
|
+
export function ChromeInstallActions({ state, size = 'sm', className, showGuide = true, }) {
|
|
108
|
+
const t = useTranslations('chrome');
|
|
109
|
+
const { checking, installed, connected, connect } = state;
|
|
110
|
+
const [openedStore, setOpenedStore] = useState(false);
|
|
111
|
+
if (connected)
|
|
112
|
+
return null;
|
|
113
|
+
const full = size === 'lg' ? 'w-full' : '';
|
|
114
|
+
const connectButton = (_jsxs(Button, { size: size, className: full, disabled: connect.isPending, onClick: () => connect.mutate(), children: [connect.isPending ? _jsx(Loading, { className: "size-4 shrink-0" }) : null, connect.isPending ? t('connecting') : t('connect')] }));
|
|
115
|
+
if (installed)
|
|
116
|
+
return _jsx("div", { className: className, children: connectButton });
|
|
117
|
+
if (CHROME_STORE_LISTED) {
|
|
118
|
+
return (_jsxs("div", { className: cn('space-y-2', className), children: [_jsx(Button, { asChild: true, size: size, className: full, "aria-disabled": checking, children: _jsxs("a", { href: KORTIX_EXTENSION_STORE_URL, target: "_blank", rel: "noreferrer", onClick: () => setOpenedStore(true), children: [_jsx(ChromeButtonMark, {}), t('addToChrome')] }) }), openedStore ? (_jsx("p", { className: "text-muted-foreground text-center text-xs text-pretty", children: t('finishInChrome') })) : null] }));
|
|
119
|
+
}
|
|
120
|
+
return (_jsxs("div", { className: cn(size === 'lg' ? 'flex flex-col gap-2' : 'flex flex-wrap items-center gap-x-3 gap-y-2', className), children: [connectButton, showGuide ? (_jsx(Button, { asChild: true, variant: "ghost", size: size, className: cn('gap-1.5', size === 'sm' && 'text-muted-foreground'), children: _jsxs("a", { href: CHROME_INSTALL_DOCS_PATH, target: "_blank", rel: "noreferrer", children: [t('howToInstall'), _jsx(ArrowUpRightIcon, { className: "size-3.5 shrink-0" })] }) })) : null] }));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The three steps and the one action that moves them. Used in the connect
|
|
124
|
+
* dialog and, with no browser connected yet, inline in Settings → Kortix in
|
|
125
|
+
* Chrome (`showActions={false}`: the page header holds the action there).
|
|
126
|
+
*/
|
|
127
|
+
export function ChromeConnectSteps({ state, onDone, showActions = true, }) {
|
|
128
|
+
const t = useTranslations('chrome');
|
|
129
|
+
const { environment, installed, signedIn, connected } = state;
|
|
130
|
+
if (environment === 'unsupported') {
|
|
131
|
+
return (_jsxs("div", { className: "mt-auto space-y-3", children: [_jsx("p", { className: "text-muted-foreground text-sm text-pretty", children: t('unsupported') }), showActions ? _jsx(ChromeCopyLink, { size: "lg", className: "w-full" }) : null] }));
|
|
132
|
+
}
|
|
133
|
+
const steps = [
|
|
134
|
+
{
|
|
135
|
+
key: 'install',
|
|
136
|
+
icon: PuzzlePieceIcon,
|
|
137
|
+
done: installed,
|
|
138
|
+
description: installed ? (t('steps.install.done')) : CHROME_STORE_LISTED ? (t('steps.install.description')) : (_jsxs(_Fragment, { children: [t('steps.install.descriptionUnlisted'), showActions ? null : (_jsxs(_Fragment, { children: [' ', _jsx("a", { href: CHROME_INSTALL_DOCS_PATH, target: "_blank", rel: "noreferrer", className: "text-foreground underline underline-offset-2", children: t('howToInstall') })] }))] })),
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
key: 'connect',
|
|
142
|
+
icon: SignInIcon,
|
|
143
|
+
done: signedIn,
|
|
144
|
+
description: t(signedIn ? 'steps.connect.done' : 'steps.connect.description'),
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
key: 'done',
|
|
148
|
+
icon: AppWindowIcon,
|
|
149
|
+
done: Boolean(connected),
|
|
150
|
+
brand: connected?.brand,
|
|
151
|
+
description: connected ? (_jsxs("span", { className: "flex min-w-0 items-center gap-1.5", children: [_jsx(ChromeOnlineDot, { online: connected.online }), _jsx("span", { className: "truncate", children: connected.name })] })) : (t('steps.done.description')),
|
|
152
|
+
},
|
|
153
|
+
];
|
|
154
|
+
return (_jsxs(_Fragment, { children: [_jsx("ol", { className: "divide-border divide-y", children: steps.map(({ key, icon, done, brand, description }) => (_jsxs("li", { className: "flex items-center gap-3 py-3", children: [_jsx(StepTile, { done: done, icon: icon, brand: brand }), _jsxs("div", { className: "min-w-0 flex-1 space-y-0.5", children: [_jsx("p", { className: "text-sm font-medium", children: t(`steps.${key}.title`) }), _jsx("div", { className: "text-muted-foreground line-clamp-2 text-xs text-pretty", children: description })] })] }, key))) }), showActions || connected ? (_jsx("div", { className: "mt-auto space-y-2", children: connected ? (onDone ? (_jsx(Button, { size: "lg", className: "w-full", onClick: onDone, children: t('done') })) : null) : (_jsx(ChromeInstallActions, { state: state, size: "lg" })) })) : null] }));
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* "Add another browser": the other browser, or another Chrome profile, signs
|
|
158
|
+
* itself in. This page cannot do that for it, so the dialog says where to go
|
|
159
|
+
* and waits for the new browser to show up in the list.
|
|
160
|
+
*/
|
|
161
|
+
function ChromeAddAnother({ onDone }) {
|
|
162
|
+
const t = useTranslations('chrome');
|
|
163
|
+
const { connected } = useChromeConnect({ fresh: true });
|
|
164
|
+
return (_jsxs(_Fragment, { children: [_jsx("ol", { className: "divide-border divide-y", children: _jsxs("li", { className: "flex items-center gap-3 py-3", children: [_jsx(StepTile, { done: Boolean(connected), icon: AppWindowIcon, brand: connected?.brand }), _jsxs("div", { className: "min-w-0 flex-1 space-y-0.5", children: [_jsx("p", { className: "text-sm font-medium", children: t('another.title') }), _jsx("div", { className: "text-muted-foreground line-clamp-2 text-xs text-pretty", children: connected ? (_jsxs("span", { className: "flex min-w-0 items-center gap-1.5", children: [_jsx(ChromeOnlineDot, { online: connected.online }), _jsx("span", { className: "truncate", children: connected.name })] })) : (t('another.waiting')) })] })] }) }), _jsx("div", { className: "mt-auto space-y-2", children: connected ? (onDone ? (_jsx(Button, { size: "lg", className: "w-full", onClick: onDone, children: t('done') })) : null) : (_jsx(ChromeCopyLink, { size: "lg", className: "w-full" })) })] }));
|
|
165
|
+
}
|
|
166
|
+
function ChromeConnectBody({ onDone }) {
|
|
167
|
+
const state = useChromeConnect();
|
|
168
|
+
return _jsx(ChromeConnectSteps, { state: state, onDone: onDone });
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* "Add Kortix to Chrome": add the Kortix extension to Chrome, sign it in, and
|
|
172
|
+
* watch the browser appear. `fresh`: add ANOTHER browser. Same frame as
|
|
173
|
+
* `ComputerConnectModal`.
|
|
174
|
+
*/
|
|
175
|
+
export function ChromeConnectModal({ open, onOpenChange, fresh = false, }) {
|
|
176
|
+
const t = useTranslations('chrome');
|
|
177
|
+
return (_jsx(Modal, { open: open, onOpenChange: onOpenChange, children: _jsx(ModalContent, { className: "space-y-0 lg:max-w-3xl", closeClassName: "max-sm:bg-background border-0 overflow-hidden", children: _jsxs("div", { className: "grid sm:grid-cols-5 lg:min-h-128", children: [_jsxs("div", { "aria-hidden": "true", className: "dark bg-background relative isolate flex h-48 items-center justify-center overflow-hidden rounded-t-xl border-b sm:col-span-2 sm:h-auto sm:rounded-tr-none sm:border-r sm:border-b-0 lg:rounded-bl-xl", children: [_jsx(BeamsShader, {}), _jsxs("span", { className: "bg-foreground text-background relative flex items-center gap-2 rounded-full px-4 py-2 text-sm font-medium", children: [_jsx(BrowserIcon, { brand: "chrome", tone: "current", className: "size-4" }), t('pill')] })] }), _jsxs("div", { className: "flex min-w-0 flex-col gap-5 p-5 sm:col-span-3 lg:p-8", children: [_jsxs("header", { className: "space-y-1.5 pr-10", children: [_jsx(ModalTitle, { className: "text-lg font-medium text-balance", children: t('connectTitle') }), _jsx(ModalDescription, { className: "text-pretty", children: t(fresh ? 'another.description' : 'connectDescription') })] }), open ? (fresh ? (_jsx(ChromeAddAnother, { onDone: () => onOpenChange(false) })) : (_jsx(ChromeConnectBody, { onDone: () => onOpenChange(false) }))) : null] })] }) }) }));
|
|
178
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { CursorIcon } from '@phosphor-icons/react';
|
|
3
|
+
import { useTranslations } from '../../i18n/index.js';
|
|
4
|
+
import { cn } from '../lib/utils.js';
|
|
5
|
+
/**
|
|
6
|
+
* What the person sees while a session works in their chrome: the Kortix tab
|
|
7
|
+
* group, the page with a halo, the agent cursor and the control bar. Static,
|
|
8
|
+
* decorative, built from tokens. Blue marks agent state only. It mirrors the
|
|
9
|
+
* extension's welcome page (`apps/chrome-extension/src/pages/welcome.html`).
|
|
10
|
+
*/
|
|
11
|
+
const MOCK_URL = 'shop.example.com/returns';
|
|
12
|
+
export function ChromeMock({ className }) {
|
|
13
|
+
const t = useTranslations('appsModal.mock');
|
|
14
|
+
return (_jsx("div", { "aria-hidden": "true", className: cn('bg-muted rounded-md p-3 sm:p-4', className), children: _jsxs("div", { className: "bg-background overflow-hidden rounded-sm border", children: [_jsx("div", { className: "bg-card flex items-center gap-2 border-b px-2 py-1.5", children: _jsxs("span", { className: "bg-kortix-base/15 flex min-w-0 items-center gap-1.5 rounded-sm px-2 py-0.5 text-xs", children: [_jsx("span", { className: "bg-kortix-base size-1.5 shrink-0 rounded-full" }), _jsx("span", { className: "truncate", children: t('group') })] }) }), _jsx("div", { className: "border-b px-2 py-1.5", children: _jsx("span", { className: "bg-muted text-muted-foreground block truncate rounded-sm px-2 py-0.5 font-mono text-xs", children: MOCK_URL }) }), _jsxs("div", { className: "relative space-y-3 px-3 pt-3 pb-14 sm:px-4 sm:pt-4", children: [_jsx("span", { className: "bg-muted block h-2 w-24 rounded-sm" }), _jsxs("div", { className: "flex items-center justify-between gap-3 rounded-sm border p-3", children: [_jsxs("span", { className: "space-y-1.5", children: [_jsx("span", { className: "bg-muted block h-2 w-20 rounded-sm" }), _jsx("span", { className: "bg-muted block h-2 w-12 rounded-sm" })] }), _jsxs("span", { className: "border-ring ring-ring/15 relative rounded-sm border px-2 py-1 text-xs ring-3", children: [t('button'), _jsxs("span", { className: "absolute top-full right-1 -mt-0.5 flex flex-col items-end", children: [_jsx(CursorIcon, { weight: "fill", className: "text-kortix-base size-4" }), _jsx("span", { className: "bg-popover -mt-0.5 rounded-sm border px-1.5 py-0.5 text-xs", children: "Kortix" })] })] })] }), _jsxs("span", { className: "bg-popover absolute bottom-3 left-1/2 flex -translate-x-1/2 items-center gap-2 rounded-full border px-3 py-1 text-xs whitespace-nowrap", children: [_jsx("span", { className: "bg-kortix-base size-1.5 rounded-full" }), t('working'), _jsx("span", { className: "text-muted-foreground", children: t('takeOver') }), _jsx("span", { className: "text-muted-foreground", children: t('stop') })] })] })] }) }));
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** @internal Kortix apps only. No semver: it may change in any release. */
|
|
2
|
+
import type { ChromeConnection } from '@kortix/sdk';
|
|
3
|
+
/**
|
|
4
|
+
* Two browsers can share a default name ("Chromium on macOS"). Where they do,
|
|
5
|
+
* the list tells them apart: the profile label when it has one, else a count
|
|
6
|
+
* in connection order. The stored name never changes; this is display only.
|
|
7
|
+
*/
|
|
8
|
+
export declare function chromeDisplayNames(list: readonly ChromeConnection[]): Map<string, string>;
|
|
9
|
+
/** "N browsers · M online" for the Apps modal. */
|
|
10
|
+
export declare function chromeCounts(list: readonly ChromeConnection[]): {
|
|
11
|
+
count: number;
|
|
12
|
+
online: number;
|
|
13
|
+
};
|
|
14
|
+
/** Offline for longer than `thresholdMs`: only then is "last seen" worth saying. */
|
|
15
|
+
export declare function offlineLongerThan(lastSeenAt: string | null, thresholdMs: number, now?: number): boolean;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two browsers can share a default name ("Chromium on macOS"). Where they do,
|
|
3
|
+
* the list tells them apart: the profile label when it has one, else a count
|
|
4
|
+
* in connection order. The stored name never changes; this is display only.
|
|
5
|
+
*/
|
|
6
|
+
export function chromeDisplayNames(list) {
|
|
7
|
+
const byName = new Map();
|
|
8
|
+
for (const chrome of list)
|
|
9
|
+
byName.set(chrome.name, [...(byName.get(chrome.name) ?? []), chrome]);
|
|
10
|
+
const out = new Map();
|
|
11
|
+
for (const chrome of list) {
|
|
12
|
+
const same = byName.get(chrome.name) ?? [];
|
|
13
|
+
if (same.length < 2) {
|
|
14
|
+
out.set(chrome.id, chrome.name);
|
|
15
|
+
}
|
|
16
|
+
else if (chrome.profileLabel && !chrome.name.includes(chrome.profileLabel)) {
|
|
17
|
+
out.set(chrome.id, `${chrome.name} · ${chrome.profileLabel}`);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
out.set(chrome.id, `${chrome.name} #${same.indexOf(chrome) + 1}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
/** "N browsers · M online" for the Apps modal. */
|
|
26
|
+
export function chromeCounts(list) {
|
|
27
|
+
return { count: list.length, online: list.filter((chrome) => chrome.online).length };
|
|
28
|
+
}
|
|
29
|
+
/** Offline for longer than `thresholdMs`: only then is "last seen" worth saying. */
|
|
30
|
+
export function offlineLongerThan(lastSeenAt, thresholdMs, now = Date.now()) {
|
|
31
|
+
if (!lastSeenAt)
|
|
32
|
+
return false;
|
|
33
|
+
const seen = Date.parse(lastSeenAt);
|
|
34
|
+
return Number.isFinite(seen) && now - seen > thresholdMs;
|
|
35
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** @internal Kortix apps only. No semver: it may change in any release. */
|
|
2
|
+
/**
|
|
3
|
+
* The Kortix Chrome extension, seen from the web app.
|
|
4
|
+
*
|
|
5
|
+
* A page talks to the extension through `chrome.runtime.sendMessage(id, …)`
|
|
6
|
+
* (`externally_connectable`). Chrome exposes `chrome.runtime` to a page only
|
|
7
|
+
* when the extension is installed and lists the page's origin. Safari, Firefox
|
|
8
|
+
* and the desktop shell never expose it, so `pingExtension` answers `null`.
|
|
9
|
+
*/
|
|
10
|
+
export declare const KORTIX_EXTENSION_ID: string;
|
|
11
|
+
export declare const KORTIX_EXTENSION_STORE_URL: string;
|
|
12
|
+
/**
|
|
13
|
+
* The Web Store listing is public. While `false`, no surface offers "Add to
|
|
14
|
+
* Chrome": each one shows "Connect" and a "How to install" link to the docs.
|
|
15
|
+
* Flip it with the listing, nothing else changes.
|
|
16
|
+
*/
|
|
17
|
+
export declare const CHROME_STORE_LISTED = false;
|
|
18
|
+
/** Where "How to install" points: the docs page that explains Load unpacked. */
|
|
19
|
+
export declare const CHROME_INSTALL_DOCS_PATH = "/docs/connect/chrome";
|
|
20
|
+
/** Opens Settings → Kortix in Chrome. The link a person copies to open it in Chrome. */
|
|
21
|
+
export declare function chromeSettingsLink(origin: string): string;
|
|
22
|
+
export interface ExtensionStatus {
|
|
23
|
+
version: string;
|
|
24
|
+
/** The extension holds a Kortix sign-in and a browser id. */
|
|
25
|
+
connected: boolean;
|
|
26
|
+
apiUrl: string | null;
|
|
27
|
+
chromeId: string | null;
|
|
28
|
+
}
|
|
29
|
+
export type ExtensionConnectResult = {
|
|
30
|
+
ok: true;
|
|
31
|
+
chromeId: string;
|
|
32
|
+
} | {
|
|
33
|
+
ok: false;
|
|
34
|
+
error: string;
|
|
35
|
+
};
|
|
36
|
+
/** Chrome, Edge, Brave, Arc and Opera on a computer. Not Electron, not Chrome on iOS. */
|
|
37
|
+
export declare function isChromiumBrowser(userAgent: string): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* The API origin the extension signs in to. The web app's backend URL can be
|
|
40
|
+
* root-relative (`/v1`, same-origin deployments), so resolve it against the
|
|
41
|
+
* page. The extension appends `/v1/...` itself. `''` when there is none.
|
|
42
|
+
*/
|
|
43
|
+
export declare function apiOrigin(backendUrl: string, pageOrigin: string): string;
|
|
44
|
+
/** The extension's state, or `null`: not installed, not Chromium, or no answer in 1.5 s. */
|
|
45
|
+
export declare function pingExtension(timeoutMs?: number): Promise<ExtensionStatus | null>;
|
|
46
|
+
/** Starts the extension's Kortix sign-in. Resolves when the person finishes it. */
|
|
47
|
+
export declare function connectExtension(apiUrl: string): Promise<ExtensionConnectResult>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Kortix Chrome extension, seen from the web app.
|
|
3
|
+
*
|
|
4
|
+
* A page talks to the extension through `chrome.runtime.sendMessage(id, …)`
|
|
5
|
+
* (`externally_connectable`). Chrome exposes `chrome.runtime` to a page only
|
|
6
|
+
* when the extension is installed and lists the page's origin. Safari, Firefox
|
|
7
|
+
* and the desktop shell never expose it, so `pingExtension` answers `null`.
|
|
8
|
+
*/
|
|
9
|
+
export const KORTIX_EXTENSION_ID = process.env.NEXT_PUBLIC_KORTIX_EXTENSION_ID || 'gefbbnjgoiljpbnalgafbhbfdhikgjek';
|
|
10
|
+
export const KORTIX_EXTENSION_STORE_URL = `https://chromewebstore.google.com/detail/${KORTIX_EXTENSION_ID}`;
|
|
11
|
+
/**
|
|
12
|
+
* The Web Store listing is public. While `false`, no surface offers "Add to
|
|
13
|
+
* Chrome": each one shows "Connect" and a "How to install" link to the docs.
|
|
14
|
+
* Flip it with the listing, nothing else changes.
|
|
15
|
+
*/
|
|
16
|
+
export const CHROME_STORE_LISTED = false;
|
|
17
|
+
/** Where "How to install" points: the docs page that explains Load unpacked. */
|
|
18
|
+
export const CHROME_INSTALL_DOCS_PATH = '/docs/connect/chrome';
|
|
19
|
+
/** Opens Settings → Kortix in Chrome. The link a person copies to open it in Chrome. */
|
|
20
|
+
export function chromeSettingsLink(origin) {
|
|
21
|
+
return `${origin}/settings/chrome`;
|
|
22
|
+
}
|
|
23
|
+
const PING_TIMEOUT_MS = 1_500;
|
|
24
|
+
// Sign-in happens in a Chrome window the person must finish.
|
|
25
|
+
const CONNECT_TIMEOUT_MS = 120_000;
|
|
26
|
+
/** Chrome, Edge, Brave, Arc and Opera on a computer. Not Electron, not Chrome on iOS. */
|
|
27
|
+
export function isChromiumBrowser(userAgent) {
|
|
28
|
+
return /Chrome\/\d/.test(userAgent) && !/Electron\//.test(userAgent);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The API origin the extension signs in to. The web app's backend URL can be
|
|
32
|
+
* root-relative (`/v1`, same-origin deployments), so resolve it against the
|
|
33
|
+
* page. The extension appends `/v1/...` itself. `''` when there is none.
|
|
34
|
+
*/
|
|
35
|
+
export function apiOrigin(backendUrl, pageOrigin) {
|
|
36
|
+
if (!backendUrl)
|
|
37
|
+
return '';
|
|
38
|
+
try {
|
|
39
|
+
return new URL(backendUrl, pageOrigin).origin;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return '';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function send(message, timeoutMs, parse) {
|
|
46
|
+
const runtime = globalThis.chrome?.runtime;
|
|
47
|
+
if (!runtime?.sendMessage)
|
|
48
|
+
return Promise.resolve(null);
|
|
49
|
+
return new Promise((resolve) => {
|
|
50
|
+
const timer = setTimeout(() => resolve(null), timeoutMs);
|
|
51
|
+
const done = (value) => {
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
resolve(value);
|
|
54
|
+
};
|
|
55
|
+
try {
|
|
56
|
+
runtime.sendMessage(KORTIX_EXTENSION_ID, message, (response) => {
|
|
57
|
+
// Reading `lastError` marks it handled. Set when the extension is absent.
|
|
58
|
+
done(runtime.lastError ? null : parse(response));
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
done(null);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/** The extension's state, or `null`: not installed, not Chromium, or no answer in 1.5 s. */
|
|
67
|
+
export function pingExtension(timeoutMs = PING_TIMEOUT_MS) {
|
|
68
|
+
return send({ type: 'kortix.ping' }, timeoutMs, (response) => {
|
|
69
|
+
const r = response;
|
|
70
|
+
if (!r || r.ok !== true)
|
|
71
|
+
return null;
|
|
72
|
+
return {
|
|
73
|
+
version: String(r.version ?? ''),
|
|
74
|
+
connected: r.connected === true,
|
|
75
|
+
apiUrl: typeof r.apiUrl === 'string' ? r.apiUrl : null,
|
|
76
|
+
chromeId: typeof r.chromeId === 'string' ? r.chromeId : null,
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/** Starts the extension's Kortix sign-in. Resolves when the person finishes it. */
|
|
81
|
+
export async function connectExtension(apiUrl) {
|
|
82
|
+
const result = await send({ type: 'kortix.connect', apiUrl }, CONNECT_TIMEOUT_MS, (response) => {
|
|
83
|
+
const r = response;
|
|
84
|
+
if (r?.ok === true)
|
|
85
|
+
return { ok: true, chromeId: String(r.chromeId ?? '') };
|
|
86
|
+
if (r)
|
|
87
|
+
return { ok: false, error: typeof r.error === 'string' ? r.error : 'failed' };
|
|
88
|
+
return null;
|
|
89
|
+
});
|
|
90
|
+
return result ?? { ok: false, error: 'unavailable' };
|
|
91
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** @internal Kortix apps only. No semver: it may change in any release. */
|
|
2
|
+
/**
|
|
3
|
+
* Whether this platform offers Kortix in Chrome at all. Until launch the API
|
|
4
|
+
* answers 404 on /v1/chrome/* where KORTIX_CHROME_ENABLED is off (staging,
|
|
5
|
+
* prod), so the account-level surfaces — Settings → Kortix in Chrome and its
|
|
6
|
+
* command-palette entry — show only once the browser list answers. Project
|
|
7
|
+
* surfaces read the project's `chrome` flag instead.
|
|
8
|
+
*
|
|
9
|
+
* `enabled`: ask only while the surface that needs the answer is open.
|
|
10
|
+
*/
|
|
11
|
+
export declare function useChromeOffered(enabled?: boolean): boolean;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { listChromeConnections } from '@kortix/sdk';
|
|
3
|
+
import { chromeKeys } from '@kortix/sdk/react';
|
|
4
|
+
import { useQuery } from '@tanstack/react-query';
|
|
5
|
+
/**
|
|
6
|
+
* Whether this platform offers Kortix in Chrome at all. Until launch the API
|
|
7
|
+
* answers 404 on /v1/chrome/* where KORTIX_CHROME_ENABLED is off (staging,
|
|
8
|
+
* prod), so the account-level surfaces — Settings → Kortix in Chrome and its
|
|
9
|
+
* command-palette entry — show only once the browser list answers. Project
|
|
10
|
+
* surfaces read the project's `chrome` flag instead.
|
|
11
|
+
*
|
|
12
|
+
* `enabled`: ask only while the surface that needs the answer is open.
|
|
13
|
+
*/
|
|
14
|
+
export function useChromeOffered(enabled = true) {
|
|
15
|
+
return useQuery({
|
|
16
|
+
queryKey: chromeKeys.list(),
|
|
17
|
+
queryFn: listChromeConnections,
|
|
18
|
+
enabled,
|
|
19
|
+
staleTime: 60_000,
|
|
20
|
+
retry: false,
|
|
21
|
+
}).isSuccess;
|
|
22
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** @internal Kortix apps only. No semver: it may change in any release. */
|
|
2
|
+
import { type ChannelPlatformId } from '../../_vendor/shared/channel-descriptors.js';
|
|
2
3
|
/**
|
|
3
4
|
* Which logo a connector shows. ONE rule for every surface — catalogue card,
|
|
4
5
|
* app page, connector page, Connected list, account rows, pickers, channel
|
|
@@ -17,7 +18,7 @@
|
|
|
17
18
|
* would learn it (the API holds the same line, `connector-icon.ts`).
|
|
18
19
|
* 4. Else the neutral fallback glyph.
|
|
19
20
|
*/
|
|
20
|
-
export type NativeConnectorApp =
|
|
21
|
+
export type NativeConnectorApp = ChannelPlatformId | 'computer';
|
|
21
22
|
/** Whatever a surface knows about the connector; every field optional. */
|
|
22
23
|
export interface ConnectorLogoSource {
|
|
23
24
|
/** Connector slug (`kortix_email`, `computer`) or catalogue id (`native:email`). */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { CHANNEL_PLATFORM_IDS } from '../../_vendor/kortix-shared.js';
|
|
1
2
|
import { faviconUrlForHostname } from '../../lib/favicon.js';
|
|
2
|
-
const NATIVE_APPS = [
|
|
3
|
+
const NATIVE_APPS = [...CHANNEL_PLATFORM_IDS, 'computer'];
|
|
3
4
|
function isNative(value) {
|
|
4
5
|
return !!value && NATIVE_APPS.includes(value);
|
|
5
6
|
}
|
|
@@ -11,13 +12,13 @@ export function nativeConnectorApp(source) {
|
|
|
11
12
|
if (source.provider === 'channel' && isNative(source.platform))
|
|
12
13
|
return source.platform;
|
|
13
14
|
const slug = (source.slug ?? '').toLowerCase();
|
|
14
|
-
const
|
|
15
|
-
if (
|
|
16
|
-
return
|
|
15
|
+
const named = /^(?:native:|kortix_)([a-z]+)$/.exec(slug)?.[1];
|
|
16
|
+
if (isNative(named))
|
|
17
|
+
return named;
|
|
17
18
|
if (slug === 'computer' || slug === 'computers')
|
|
18
19
|
return 'computer';
|
|
19
|
-
const file = /\/connector-icons\/(
|
|
20
|
-
return file ? file
|
|
20
|
+
const file = /\/connector-icons\/([a-z]+)\.svg$/.exec(source.icon ?? '')?.[1];
|
|
21
|
+
return isNative(file) ? file : null;
|
|
21
22
|
}
|
|
22
23
|
export function resolveConnectorLogo(source) {
|
|
23
24
|
const native = nativeConnectorApp(source);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** @internal Kortix apps only. No semver: it may change in any release. */
|
|
2
|
+
/** Edge brand mark, full colour. Source: gilbarbara/logos (CC0), `logos:microsoft-edge`. */
|
|
3
|
+
export declare const Edge: ({ className }: {
|
|
4
|
+
className?: string;
|
|
5
|
+
}) => import("react").JSX.Element;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useId } from 'react';
|
|
4
|
+
import { cn } from '@kortix/ui';
|
|
5
|
+
/** Edge brand mark, full colour. Source: gilbarbara/logos (CC0), `logos:microsoft-edge`. */
|
|
6
|
+
export const Edge = ({ className }) => {
|
|
7
|
+
// Gradient ids are per instance, so two marks on one page never share a definition.
|
|
8
|
+
const uid = useId().replace(/[^a-zA-Z0-9_-]/g, '');
|
|
9
|
+
return (_jsxs("svg", { viewBox: "0 0 256 256", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", className: cn('size-4', className), children: [_jsxs("defs", { children: [_jsxs("radialGradient", { id: `${uid}SVGHFrboeCQ`, cx: "161.83", cy: "788.401", r: "95.38", gradientTransform: "matrix(.9999 0 0 .9498 -4.622 -570.387)", gradientUnits: "userSpaceOnUse", children: [_jsx("stop", { offset: ".72", stopOpacity: "0" }), _jsx("stop", { offset: ".95", stopOpacity: ".53" }), _jsx("stop", { offset: "1" })] }), _jsxs("radialGradient", { id: `${uid}SVGVG94vbPK`, cx: "-773.636", cy: "746.715", r: "143.24", gradientTransform: "matrix(.15 -.9898 .8 .12 -410.718 -656.341)", gradientUnits: "userSpaceOnUse", children: [_jsx("stop", { offset: ".76", stopOpacity: "0" }), _jsx("stop", { offset: ".95", stopOpacity: ".5" }), _jsx("stop", { offset: "1" })] }), _jsxs("radialGradient", { id: `${uid}SVGw2d8JdCl`, cx: "230.593", cy: "-106.038", r: "202.43", gradientTransform: "matrix(-.04 .9998 -2.1299 -.07998 -190.775 -191.635)", gradientUnits: "userSpaceOnUse", children: [_jsx("stop", { offset: "0", stopColor: "#35c1f1" }), _jsx("stop", { offset: ".11", stopColor: "#34c1ed" }), _jsx("stop", { offset: ".23", stopColor: "#2fc2df" }), _jsx("stop", { offset: ".31", stopColor: "#2bc3d2" }), _jsx("stop", { offset: ".67", stopColor: "#36c752" })] }), _jsxs("radialGradient", { id: `${uid}SVGIqWItcPR`, cx: "536.357", cy: "-117.703", r: "97.34", gradientTransform: "matrix(.28 .9598 -.78 .23 -1.928 -410.318)", gradientUnits: "userSpaceOnUse", children: [_jsx("stop", { offset: "0", stopColor: "#66eb6e" }), _jsx("stop", { offset: "1", stopColor: "#66eb6e", stopOpacity: "0" })] }), _jsxs("linearGradient", { id: `${uid}SVGktZSUcGK`, x1: "63.334", x2: "241.617", y1: "757.83", y2: "757.83", gradientTransform: "translate(-4.63 -580.81)", gradientUnits: "userSpaceOnUse", children: [_jsx("stop", { offset: "0", stopColor: "#0c59a4" }), _jsx("stop", { offset: "1", stopColor: "#114a8b" })] }), _jsxs("linearGradient", { id: `${uid}SVGaPqZBebW`, x1: "157.401", x2: "46.028", y1: "680.556", y2: "801.868", gradientTransform: "translate(-4.63 -580.81)", gradientUnits: "userSpaceOnUse", children: [_jsx("stop", { offset: "0", stopColor: "#1b9de2" }), _jsx("stop", { offset: ".16", stopColor: "#1595df" }), _jsx("stop", { offset: ".67", stopColor: "#0680d7" }), _jsx("stop", { offset: "1", stopColor: "#0078d4" })] })] }), _jsx("path", { fill: `url(#${uid}SVGktZSUcGK)`, d: "M231 190.5c-3.4 1.8-6.9 3.4-10.5 4.7c-11.5 4.3-23.6 6.5-35.9 6.5c-47.3 0-88.5-32.5-88.5-74.3c.1-11.4 6.4-21.9 16.4-27.3c-42.8 1.8-53.8 46.4-53.8 72.5c0 73.9 68.1 81.4 82.8 81.4c7.9 0 19.8-2.3 27-4.6l1.3-.4c27.6-9.5 51-28.1 66.6-52.8c1.2-1.9.6-4.3-1.2-5.5c-1.3-.8-2.9-.9-4.2-.2" }), _jsx("path", { fill: `url(#${uid}SVGHFrboeCQ)`, d: "M231 190.5c-3.4 1.8-6.9 3.4-10.5 4.7c-11.5 4.3-23.6 6.5-35.9 6.5c-47.3 0-88.5-32.5-88.5-74.3c.1-11.4 6.4-21.9 16.4-27.3c-42.8 1.8-53.8 46.4-53.8 72.5c0 73.9 68.1 81.4 82.8 81.4c7.9 0 19.8-2.3 27-4.6l1.3-.4c27.6-9.5 51-28.1 66.6-52.8c1.2-1.9.6-4.3-1.2-5.5c-1.3-.8-2.9-.9-4.2-.2", opacity: ".35" }), _jsx("path", { fill: `url(#${uid}SVGaPqZBebW)`, d: "M105.7 241.4c-8.9-5.5-16.6-12.8-22.7-21.3c-26.3-36-18.4-86.5 17.6-112.8c3.8-2.7 7.7-5.2 11.9-7.2c3.1-1.5 8.4-4.1 15.5-4c10.1.1 19.6 4.9 25.7 13c4 5.4 6.3 11.9 6.4 18.7c0-.2 24.5-79.6-80-79.6c-43.9 0-80 41.7-80 78.2c-.2 19.3 4 38.5 12.1 56c27.6 58.8 94.8 87.6 156.4 67.1c-21.1 6.6-44.1 3.7-62.9-8.1" }), _jsx("path", { fill: `url(#${uid}SVGVG94vbPK)`, d: "M105.7 241.4c-8.9-5.5-16.6-12.8-22.7-21.3c-26.3-36-18.4-86.5 17.6-112.8c3.8-2.7 7.7-5.2 11.9-7.2c3.1-1.5 8.4-4.1 15.5-4c10.1.1 19.6 4.9 25.7 13c4 5.4 6.3 11.9 6.4 18.7c0-.2 24.5-79.6-80-79.6c-43.9 0-80 41.7-80 78.2c-.2 19.3 4 38.5 12.1 56c27.6 58.8 94.8 87.6 156.4 67.1c-21.1 6.6-44.1 3.7-62.9-8.1", opacity: ".41" }), _jsx("path", { fill: `url(#${uid}SVGw2d8JdCl)`, d: "M152.3 148.9c-.8 1-3.3 2.5-3.3 5.7c0 2.6 1.7 5.1 4.7 7.2c14.4 10 41.5 8.7 41.6 8.7c10.7 0 21.1-2.9 30.3-8.3c18.8-11 30.4-31.1 30.4-52.9c.3-22.4-8-37.3-11.3-43.9C223.5 23.9 177.7 0 128 0C58 0 1 56.2 0 126.2c.5-36.5 36.8-66 80-66c3.5 0 23.5.3 42 10.1c16.3 8.6 24.9 18.9 30.8 29.2c6.2 10.7 7.3 24.1 7.3 29.5c0 5.3-2.7 13.3-7.8 19.9" }), _jsx("path", { fill: `url(#${uid}SVGIqWItcPR)`, d: "M152.3 148.9c-.8 1-3.3 2.5-3.3 5.7c0 2.6 1.7 5.1 4.7 7.2c14.4 10 41.5 8.7 41.6 8.7c10.7 0 21.1-2.9 30.3-8.3c18.8-11 30.4-31.1 30.4-52.9c.3-22.4-8-37.3-11.3-43.9C223.5 23.9 177.7 0 128 0C58 0 1 56.2 0 126.2c.5-36.5 36.8-66 80-66c3.5 0 23.5.3 42 10.1c16.3 8.6 24.9 18.9 30.8 29.2c6.2 10.7 7.3 24.1 7.3 29.5c0 5.3-2.7 13.3-7.8 19.9" })] }));
|
|
10
|
+
};
|
|
@@ -615,8 +615,9 @@ export function isCaptchaInterceptorNoise(input) {
|
|
|
615
615
|
}
|
|
616
616
|
/**
|
|
617
617
|
* An injected wallet provider rejecting a pending request with the EIP-1474
|
|
618
|
-
* default internal error, `{ code: -32603, message: 'Internal JSON-RPC error.' }
|
|
619
|
-
*
|
|
618
|
+
* default internal error, `{ code: -32603, message: 'Internal JSON-RPC error.' }`,
|
|
619
|
+
* or the exact missing-account error captured in KRTX-2150 (code 4001).
|
|
620
|
+
* Both use the wallet provider's own wording. The rejected value is a plain
|
|
620
621
|
* object with no `stack`, so Sentry reports a frameless synthetic
|
|
621
622
|
* "Object captured as promise rejection with keys: code, message" and the
|
|
622
623
|
* extension-stack anchor above cannot see it. Better Stack, Kortix Frontend
|
|
@@ -633,7 +634,11 @@ function isWalletInternalRpcRejectionNoise(input) {
|
|
|
633
634
|
const value = serialized;
|
|
634
635
|
if (Object.keys(value).sort().join(',') !== 'code,message')
|
|
635
636
|
return false;
|
|
636
|
-
|
|
637
|
+
// KRTX-2150 recurred with a second frameless wallet payload (2026-10-29).
|
|
638
|
+
// Match the exact pair, not EIP-1193 code 4001 alone (user rejection).
|
|
639
|
+
const internal = value.code === -32603 && value.message === 'Internal JSON-RPC error.';
|
|
640
|
+
const missingAccount = value.code === 4001 && value.message === 'wallet must has at least one account';
|
|
641
|
+
if (!internal && !missingAccount)
|
|
637
642
|
return false;
|
|
638
643
|
return !sourcesOf(input).some((source) => isBrowserBundleSource(source) || isFirstPartyResolvedSource(source));
|
|
639
644
|
}
|
|
@@ -677,6 +682,15 @@ function isWalletProviderDisconnectedRejectionNoise(input) {
|
|
|
677
682
|
return !sourcesOf(input).some((source) => isBrowserBundleSource(source) || isFirstPartyResolvedSource(source));
|
|
678
683
|
}
|
|
679
684
|
export const EXTENSION_RULES = [
|
|
685
|
+
{
|
|
686
|
+
// Talisman's injected page.js rejects before wallet onboarding (KRTX-3678).
|
|
687
|
+
// Require its exact message and response frame; page.js alone is ambiguous.
|
|
688
|
+
id: 'talisman-onboarding',
|
|
689
|
+
appliesTo: 'sentry',
|
|
690
|
+
match: ({ message, frames }) => normalizeString(message) === 'Talisman extension has not been configured yet. Please continue with onboarding.' &&
|
|
691
|
+
!frames.some((frame) => isFirstPartyResolvedSource(frame.filename)) &&
|
|
692
|
+
frames.some((frame) => frame.filename === 'app:///page.js' && frame.function === 'ne.handleResponse'),
|
|
693
|
+
},
|
|
680
694
|
{
|
|
681
695
|
id: 'injected-app-source',
|
|
682
696
|
appliesTo: 'both',
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** @internal Kortix apps only. No semver: it may change in any release. */
|
|
2
|
+
/**
|
|
3
|
+
* Platform detection for the /download page and the per-platform redirect
|
|
4
|
+
* routes.
|
|
5
|
+
*
|
|
6
|
+
* Detection runs on the SERVER, from the request's user-agent header, so the
|
|
7
|
+
* page paints the correct filled button (and, on a phone, the mobile card
|
|
8
|
+
* first) on first render. A client-side `useEffect` swap would let a visitor click the wrong
|
|
9
|
+
* installer during the flash — the exact failure this page exists to prevent.
|
|
10
|
+
*
|
|
11
|
+
* Architecture is deliberately NOT detected: macOS ships a single universal
|
|
12
|
+
* .dmg, and macOS user agents misreport arch anyway (they always claim Intel).
|
|
13
|
+
*/
|
|
14
|
+
export type DesktopOs = 'macos' | 'windows' | 'linux';
|
|
15
|
+
export type MobileOs = 'ios' | 'android';
|
|
16
|
+
export type Platform = DesktopOs | MobileOs;
|
|
17
|
+
/** Fixed row order. Detection never reorders rows; it only picks the filled button. */
|
|
18
|
+
export declare const DESKTOP_ORDER: readonly DesktopOs[];
|
|
19
|
+
export declare const MOBILE_ORDER: readonly MobileOs[];
|
|
20
|
+
/** Map an explicit `?platform=` value or `/download/<segment>` to a platform. */
|
|
21
|
+
export declare function normalizePlatform(raw: string | null | undefined): Platform | null;
|
|
22
|
+
/**
|
|
23
|
+
* Best-effort platform from a user-agent string.
|
|
24
|
+
*
|
|
25
|
+
* THE ORDER OF THESE CHECKS IS LOAD-BEARING. Two user agents carry another
|
|
26
|
+
* platform's token:
|
|
27
|
+
* iPad "Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X)" contains "mac"
|
|
28
|
+
* Android "Mozilla/5.0 (Linux; Android 14; Pixel 8)" contains "linux"
|
|
29
|
+
* So iOS is tested before macOS, and Android before Linux. `detect-os.test.ts`
|
|
30
|
+
* pins both; if you reorder these lines, those tests fail.
|
|
31
|
+
*
|
|
32
|
+
* KNOWN LIMITATION: an iPad with "Request Desktop Website" enabled sends a UA
|
|
33
|
+
* byte-identical to a Mac's. No server can separate them — real detection needs
|
|
34
|
+
* `navigator.maxTouchPoints`, which is not in any request header. That visitor
|
|
35
|
+
* gets the Desktop card first; the Mobile card is still on the page, one tap
|
|
36
|
+
* away. Fixing it client-side would reintroduce the wrong-file-click window
|
|
37
|
+
* this module exists to close.
|
|
38
|
+
*
|
|
39
|
+
* Defaults to macOS: the largest desktop segment for this product, and every
|
|
40
|
+
* other platform stays one click away on the page.
|
|
41
|
+
*/
|
|
42
|
+
export declare function detectPlatform(userAgent: string | null | undefined): Platform;
|
|
43
|
+
export declare function isMobilePlatform(platform: Platform): platform is MobileOs;
|